Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div id="canvasContainer"></div>
              
            
!

CSS

              
                html, body{
  margin : 0px;
  width : 100%;
  height : 100%;
  overflow: hidden;
  background-color:#000000;
}

#canvasContainer{
  position: absolute;
  margin : 0px;
  width : 100%;
  height : 100%;
}

#controls{
  position: absolute;
}
              
            
!

JS

              
                
/*
 *
 * @author Sakri Rosenstrom
 * http://www.sakri.net
 * https://twitter.com/sakri
 * http://www.devstate.net
 * Sources for this can be found at:
 * https://github.com/sakri/sakriNetCommonJS
 */



(function (window){

    var Sakri = window.Sakri || {};
    window.Sakri = window.Sakri || Sakri;
    
	Sakri.MathUtil = {};
	
	//used for radiansToDegrees and degreesToRadians
	Sakri.MathUtil.PI_180 = Math.PI/180;
	Sakri.MathUtil.ONE80_PI = 180/Math.PI;
	
	//precalculations for values of 90, 270 and 360 in radians
	Sakri.MathUtil.PI2 = Math.PI*2;
	Sakri.MathUtil.HALF_PI = Math.PI/2;
	Sakri.MathUtil.PI_AND_HALF = Math.PI+ Math.PI/2;
	Sakri.MathUtil.NEGATIVE_HALF_PI = -Math.PI/2;

    //keep degrees between 0 and 360
    Sakri.MathUtil.constrainDegreeTo360 = function(degree){
        return (360 + degree % 360) % 360;//hmmm... looks a bit weird?!
    };

    Sakri.MathUtil.constrainRadianTo2PI = function(rad){
        return (Sakri.MathUtil.PI2 + rad % Sakri.MathUtil.PI2) % Sakri.MathUtil.PI2;//equally so...
    };

    Sakri.MathUtil.radiansToDegrees = function(rad){
        return rad*Sakri.MathUtil.ONE80_PI;
    };

    Sakri.MathUtil.degreesToRadians = function(degree){
        return degree * Sakri.MathUtil.PI_180;
    };

	//return number between 1 and 0
	Sakri.MathUtil.normalize = function(value, minimum, maximum){
		return (value - minimum) / (maximum - minimum);
	};

	//map normalized number to values
	Sakri.MathUtil.interpolate = function(normValue, minimum, maximum){
		return minimum + (maximum - minimum) * normValue;
	};

	//map a value from one set to another
	Sakri.MathUtil.map = function(value, min1, max1, min2, max2){
		return Sakri.MathUtil.interpolate( Sakri.MathUtil.normalize(value, min1, max1), min2, max2);
	};




    Sakri.MathUtil.clamp = function(min,max,value){
        if(value < min){
            return min;
        }
        if(value > max){
            return max;
        }
        return value;
    };

    Sakri.MathUtil.clampRGB = function(value){
        return Sakri.MathUtil.clamp(0, 255, value);
    };

	Sakri.MathUtil.getRandomNumberInRange = function(min, max){
		return min + Math.random() * (max - min);
	};
	
	Sakri.MathUtil.getRandomIntegerInRange = function(min, max){
		return Math.round(Sakri.MathUtil.getRandomNumberInRange(min, max));
	};

    //Move to geom?
	Sakri.MathUtil.getCircumferenceOfEllipse = function(width,height){
		return ((Math.sqrt(.5 * ((width * width) + (height * height)))) * (Math.PI * 2)) / 2;
	};


    //from : http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
    Sakri.MathUtil.rgbToHex = function(r, g, b) {
        return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
    }

    Sakri.MathUtil.hexToRgb = function(hex) {
        // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
        var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
        hex = hex.replace(shorthandRegex, function(m, r, g, b) {
            return r + r + g + g + b + b;
        });

        var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
        return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
        } : null;
    }

	
}(window));





//has a dependency on Sakri.MathUtil

(function (window){

    var Sakri = window.Sakri || {};
    window.Sakri = window.Sakri || Sakri;

	Sakri.Geom = {};

    //==================================================
    //=====================::POINT::====================
    //==================================================

    Sakri.Geom.Point = function (x,y){
        this.x = isNaN(x) ? 0 : x;
        this.y = isNaN(y) ? 0 : y;
    };

    Sakri.Geom.Point.prototype.clone = function(){
        return new Sakri.Geom.Point(this.x,this.y);
    };

    Sakri.Geom.Point.prototype.update = function(x, y){
        this.x = isNaN(x) ? this.x : x;
        this.y = isNaN(y) ? this.y : y;
    };

    Sakri.Geom.Point.prototype.add = function(x, y){
        this.x += isNaN(x) ? 0 : x;
        this.y += isNaN(y) ? 0 : y;
    };

    Sakri.Geom.Point.prototype.equals = function(point){
        return this.x==point.x && this.y==point.y;
    };

    Sakri.Geom.Point.prototype.toString = function(){
        return "{x:"+this.x+" , y:"+this.y+"}";
    };

    Sakri.Geom.Point.interpolate = function(pointA, pointB, normal){
        return new Sakri.Geom.Point(Sakri.MathUtil.interpolate(normal, pointA.x, pointB.x) , Sakri.MathUtil.interpolate(normal, pointA.y, pointB.y));
    };

    Sakri.Geom.Point.distanceBetweenTwoPoints = function( point1, point2 ){
        //console.log("Math.pow(point2.x - point1.x,2) : ",Math.pow(point2.x - point1.x,2));
        return Math.sqrt( Math.pow(point2.x - point1.x,2) + Math.pow(point2.y - point1.y,2) );
    };

    Sakri.Geom.Point.angleBetweenTwoPoints = function(p1,p2){
        return Math.atan2(p1.y-p2.y, p1.x-p2.x);
    };

    Sakri.Geom.mirrorPointInRectangle = function(point,rect){
        return new Sakri.Geom.Point(rect.width-point.x,rect.height-point.y);
    };

    Sakri.Geom.randomizePoint = function(point,randomValue){
        return new Sakri.Geom.Point(-randomValue+Math.random()*randomValue+point.x,-randomValue+Math.random()*randomValue+point.y);
    };


	//==================================================
	//===================::RECTANGLE::==================
	//==================================================

	Sakri.Geom.Rectangle = function (x, y, width, height){
		this.update(x, y, width, height);
	};
	
	Sakri.Geom.Rectangle.prototype.update = function(x, y, width, height){
		this.x = isNaN(x) ? 0 : x;
		this.y = isNaN(y) ? 0 : y;
		this.width = isNaN(width) ? 0 : width;
		this.height = isNaN(height) ? 0 : height;
	};

    //TODO : doesn't work
    Sakri.Geom.Rectangle.prototype.inflate = function(x, y){
        this.x -= isNaN(x) ? 0 : x;
        this.y -= isNaN(y) ? 0 : y;
        this.width += isNaN(x) ? 0 : x * 2;
        this.height += isNaN(y) ? 0 : y * 2;
    };
	
	Sakri.Geom.Rectangle.prototype.updateToRect = function(rect){
		this.x = rect.x;
		this.y = rect.y;
		this.width = rect.width;
		this.height = rect.height;
	};
	
	Sakri.Geom.Rectangle.prototype.scaleX = function(scaleBy){
		this.width *= scaleBy;
	};
	
	Sakri.Geom.Rectangle.prototype.scaleY = function(scaleBy){
		this.height *= scaleBy;
	};
	
	Sakri.Geom.Rectangle.prototype.scale = function(scaleBy){
		this.scaleX(scaleBy);
		this.scaleY(scaleBy);
	};

	Sakri.Geom.Rectangle.prototype.getRight = function(){
		return this.x + this.width;
	};
	
	Sakri.Geom.Rectangle.prototype.getBottom = function(){
		return this.y + this.height;
	};

    Sakri.Geom.Rectangle.prototype.getCenter = function(){
        return new Sakri.Geom.Point(this.getCenterX(), this.getCenterY());
    };

    Sakri.Geom.Rectangle.prototype.getCenterX = function(){
        return this.x + this.width/2;
    };

    Sakri.Geom.Rectangle.prototype.getCenterY=function(){
        return this.y + this.height/2;
    };

    Sakri.Geom.Rectangle.prototype.containsPoint = function(x, y){
        return x >= this.x && y >= this.y && x <= this.getRight() && y <= this.getBottom();
    };
    Sakri.Geom.Rectangle.prototype.containsRect = function(rect){
        return this.containsPoint(rect.x, rect.y) && this.containsPoint(rect.getRight(), rect.getBottom());
    };

	Sakri.Geom.Rectangle.prototype.isSquare = function(){
		return this.width == this.height;
	};

	Sakri.Geom.Rectangle.prototype.isLandscape = function(){
		return this.width > this.height;
	};

	Sakri.Geom.Rectangle.prototype.isPortrait = function(){
		return this.width < this.height;
	};
	
	Sakri.Geom.Rectangle.prototype.getSmallerSide = function(){
		return Math.min(this.width, this.height);
	};
	
	Sakri.Geom.Rectangle.prototype.getBiggerSide = function(){
		return Math.max(this.width,this.height);
	};
	
	Sakri.Geom.Rectangle.prototype.getArea = function(){
		return this.width * this.height;
	};
	
	Sakri.Geom.Rectangle.prototype.floor = function(){
		this.x = Math.floor(this.x);
		this.y = Math.floor(this.y);
		this.width = Math.floor(this.width);
		this.height = Math.floor(this.height);
	};
	
	Sakri.Geom.Rectangle.prototype.ceil = function(){
		this.x = Math.ceil(this.x);
		this.y = Math.ceil(this.y);
		this.width = Math.ceil(this.width);
		this.height = Math.ceil(this.height);
	};

	Sakri.Geom.Rectangle.prototype.round = function(){
		this.x=Math.round(this.x);
		this.y=Math.round(this.y);
		this.width=Math.round(this.width);
		this.height=Math.round(this.height);
	};

	Sakri.Geom.Rectangle.prototype.roundIn = function(){
		this.x = Math.ceil(this.x);
		this.y = Math.ceil(this.y);
		this.width = Math.floor(this.width);
		this.height = Math.floor(this.height);
	};

	Sakri.Geom.Rectangle.prototype.roundOut = function(){
		this.x = Math.floor(this.x);
		this.y = Math.floor(this.y);
		this.width = Math.ceil(this.width);
		this.height = Math.ceil(this.height);
	};
	
	Sakri.Geom.Rectangle.prototype.clone = function(){
		return new Sakri.Geom.Rectangle(this.x, this.y, this.width, this.height);
	};
	
	Sakri.Geom.Rectangle.prototype.toString = function(){
		return "Rectangle{x:"+this.x+" , y:"+this.y+" , width:"+this.width+" , height:"+this.height+"}";
	};
	

	
}(window));







/**
 * Created by sakri on 27-1-14.
 */
(function (window){

    var Sakri = window.Sakri || {};
    window.Sakri = window.Sakri || Sakri;

    Sakri.CanvasTextUtil = {};


    //this method renders text into a canvas, then resizes the image by shrinkPercent
    //loops through the non transparent pixels of the resized image and returns those as an array
    //fontProperties should be an object of type Sakri.CanvasTextProperties
    Sakri.CanvasTextUtil.createTextParticles = function(text, shrinkPercent, fontProps){
        var renderCanvas = document.createElement('canvas');
        var renderContext = renderCanvas.getContext('2d');

        var fontString = fontProperties.getFontString();
        //console.log(fontString);
        renderContext.font = fontString;
        renderContext.textBaseline = "top";
        //console.log(renderContext.measureText(text).width);
        renderCanvas.width = renderContext.measureText(text).width;
        renderCanvas.height = fontProps.fontSize + 10;//TODO : Need to implement getFirstNonTransparentPixel()

        //after a resize of a canvas, we have to reset these properties
        renderContext.font =  fontString;
        renderContext.textBaseline = "top";
        //console.log(renderCanvas.width, renderCanvas.height);
        renderContext.fillStyle = "#FF0000";
        renderContext.fillText(text, 0, 0);

        var shrunkenCanvas = document.createElement('canvas');
        shrunkenCanvas.width = Math.round(renderCanvas.width*shrinkPercent);
        shrunkenCanvas.height = Math.round(renderCanvas.height*shrinkPercent);
        var shrunkenContext = shrunkenCanvas.getContext('2d');
        shrunkenContext.drawImage(renderCanvas, 0, 0, shrunkenCanvas.width , shrunkenCanvas.height  );

        var pixels = shrunkenContext.getImageData(0, 0, shrunkenCanvas.width, shrunkenCanvas.height);
        var data = pixels.data;
        var particles = [];
        var i, x, y;
        for(i = 0; i < data.length; i += 4) {
            if(data[i]>200){
                x = ((i/4)%shrunkenCanvas.width)/shrinkPercent;
                y = Math.floor((i/4)/shrunkenCanvas.width)/shrinkPercent;
                particles.push(new Sakri.Geom.Point(x, y));
            }
        }
        delete renderCanvas;
        delete shrunkenCanvas;
        return particles;
    };


    Sakri.CanvasTextUtil.createImagesFromString = function(string, fillStyle, strokeStyle, strokeWidth, fontProps){
        var fontString = fontProps.getFontString();
        var characters = string.split("");
        var images = [];
        var canvas, context, image, metrics, i,character;
        canvas = document.createElement("canvas");

        for(i=0; i<characters.length; i++){
            character = characters[i];

            context = canvas.getContext("2d");
            context.textBaseline = "top";
            context.font = fontString;
            metrics = context.measureText(character);
            canvas.width = metrics.width;
            canvas.height = fontProps.fontSize;// TODO : use getFirstNonTransparentPixel for dynamic sizing

            //these properties have to be set twice as they vanish after setting a canvas width and height
            context = canvas.getContext("2d");
            context.textBaseline = "top";
            context.font = fontString;

            image = new Image();
            image.width = canvas.width;
            image.height = canvas.height;

            if(fillStyle){
                context.fillStyle = fillStyle;
                context.fillText (character,0, 0);
            }
            if(strokeStyle){
                context.strokeStyle = strokeStyle;
                context.lineWidth = strokeWidth;
                context.strokeText(character, 0, 0);
            }

            image.src = canvas.toDataURL();
            images[i] = image;
        }
        delete canvas;
        return images;
    };


    //=========================================================================================
    //==============::CANVAS TEXT PROPERTIES::====================================
    //========================================================

    Sakri.CanvasTextProperties = function(fontWeight, fontStyle, fontSize, fontFace){
        this.setFontWeight(fontWeight);
        this.setFontStyle(fontStyle);
        this.setFontSize(fontSize);
        this.fontFace = fontFace ? fontFace : "sans-serif";
    };

    Sakri.CanvasTextProperties.NORMAL = "normal";
    Sakri.CanvasTextProperties.BOLD = "bold";
    Sakri.CanvasTextProperties.BOLDER = "bolder";
    Sakri.CanvasTextProperties.LIGHTER = "lighter";

    Sakri.CanvasTextProperties.ITALIC = "italic";
    Sakri.CanvasTextProperties.OBLIQUE = "oblique";


    Sakri.CanvasTextProperties.prototype.setFontWeight = function(fontWeight){
        switch (fontWeight){
            case Sakri.CanvasTextProperties.NORMAL:
            case Sakri.CanvasTextProperties.BOLD:
            case Sakri.CanvasTextProperties.BOLDER:
            case Sakri.CanvasTextProperties.LIGHTER:
                this.fontWeight = fontWeight;
                break;
            default:
                this.fontWeight = Sakri.CanvasTextProperties.NORMAL;
        }
    };

    Sakri.CanvasTextProperties.prototype.setFontStyle = function(fontStyle){
        switch (fontStyle){
            case Sakri.CanvasTextProperties.NORMAL:
            case Sakri.CanvasTextProperties.ITALIC:
            case Sakri.CanvasTextProperties.OBLIQUE:
                this.fontStyle = fontStyle;
                break;
            default:
                this.fontStyle = Sakri.CanvasTextProperties.NORMAL;
        }
    };

    Sakri.CanvasTextProperties.prototype.setFontSize = function(fontSize){
        if(fontSize && fontSize.indexOf && fontSize.indexOf("px")>-1){
            var size = fontSize.split("px")[0];
            fontProperites.fontSize = isNaN(size) ? 24 : size;//24 is just an arbitrary number
            return;
        }
        this.fontSize = isNaN(fontSize) ? 24 : fontSize;//24 is just an arbitrary number
    };

    Sakri.CanvasTextProperties.prototype.getFontString = function(){
        return this.fontWeight + " " + this.fontStyle + " " + this.fontSize + "px " + this.fontFace;
    };

}(window));





/**
 * Created by sakri on 27-1-14.
 */
(function (window){

    var Sakri = window.Sakri || {};
    window.Sakri = window.Sakri || Sakri;

    Sakri.BitmapUtil = {};

    //TODO : rename "canvas" to "source", if it's an img, create a canvas and draw the img into it
    Sakri.BitmapUtil.getFirstNonTransparentPixelTopDown = function(canvas){
        var context = canvas.getContext("2d");
        var y, i, rowData;
        for(y=0; y<canvas.height; y++){
            rowData = context.getImageData(0, y, canvas.width, 1).data;
            for(i=0; i<rowData.length; i+=4){
                if(rowData[i+0] + rowData[i+1] + rowData[i+2] + rowData[i+3] > 0){
                    return new Sakri.Geom.Point(i/4, y);
                }
            }
        }
        return null;
    };

    Sakri.BitmapUtil.getFirstNonTransparentPixelBottomUp = function(canvas){
        var context = canvas.getContext("2d");
        var y, i, rowData;
        for(y = canvas.height-1; y>-1; y--){
            rowData = context.getImageData(0, y, canvas.width, 1).data;
            for(i=0; i<rowData.length; i+=4){
                if(rowData[i+0] + rowData[i+1] + rowData[i+2] + rowData[i+3] > 0){
                    return new Sakri.Geom.Point(i/4, y);
                }
            }
        }
        return null;
    };

    Sakri.BitmapUtil.getFirstNonTransparentPixelLeftToRight = function(canvas){
        var context = canvas.getContext("2d");
        var x, i, colData;
        for(x = 0; x < canvas.width; x++){
            colData = context.getImageData(x, 0, 1, canvas.height).data;
            for(i=0; i<colData.length; i+=4){
                if(colData[i+0] + colData[i+1] + colData[i+2] + colData[i+3] > 0){
                    return new Sakri.Geom.Point(x, i/4);
                }
            }
        }
        return null;
    };

    Sakri.BitmapUtil.getFirstNonTransparentPixelRightToLeft = function(canvas){
        var context = canvas.getContext("2d");
        var x, i, colData;
        for(x = canvas.width-1; x >-1; x--){
            colData = context.getImageData(x, 0, 1, canvas.height).data;
            for(i=0; i<colData.length; i+=4){
                if(colData[i+0] + colData[i+1] + colData[i+2] + colData[i+3] > 0){
                    return new Sakri.Geom.Point(x, i/4);
                }
            }
        }
        return null;
    };

    //cuts out rows and columns of pixels without color data from the top, bottom, left and right
    Sakri.BitmapUtil.trimImage = function(image){
        var trimCanvas = Sakri.BitmapUtil.createTrimmedCanvas(image);
        image.src = trimCanvas.toDataURL();
    };

    Sakri.BitmapUtil.trimCanvas = function(canvas){
        console.log("trimCanvas()", canvas.width, canvas.height);
        var trimCanvas = Sakri.BitmapUtil.createTrimmedCanvas(canvas);
        canvas.width = trimCanvas.width;
        canvas.height = trimCanvas.height;
        console.log("\t=>" , canvas.width, canvas.height);
        var context = canvas.getContext("2d");
        context.drawImage(trimCanvas, 0, 0);
    };

    Sakri.BitmapUtil.getCanvasTrimRectangle = function(canvas){
        var rect = new Sakri.Geom.Rectangle();
        rect.x = Sakri.BitmapUtil.getFirstNonTransparentPixelLeftToRight(canvas).x;
        rect.y = Sakri.BitmapUtil.getFirstNonTransparentPixelTopDown(canvas).y;
        rect.width = Sakri.BitmapUtil.getFirstNonTransparentPixelRightToLeft(canvas).x -  rect.x + 1;
        rect.height = Sakri.BitmapUtil.getFirstNonTransparentPixelBottomUp(canvas).y - rect.y + 1;
        return rect;
    }

    Sakri.BitmapUtil.createTrimmedCanvas = function(imageOrCanvas){
        var trimCanvas = document.createElement("canvas");
        var trimContext = trimCanvas.getContext("2d");
        trimCanvas.width = imageOrCanvas.width;
        trimCanvas.height = imageOrCanvas.height;
        trimContext.drawImage(imageOrCanvas, 0, 0);
        var rect = Sakri.BitmapUtil.getCanvasTrimRectangle(trimCanvas);
        //console.log("createTrimmedCanvas() ", rect.toString());
        trimCanvas.width = rect.width;
        trimCanvas.height = rect.height;
        trimContext = trimCanvas.getContext("2d");
        trimContext.drawImage(imageOrCanvas, rect.x, rect.y, rect.width, rect.height, 0, 0, rect.width, rect.height);
        return trimCanvas;
    };

    //capture rect is the content on canvas to be reflected, border defines the space between the original content and the reflection
    //captureRect must contain the properties x, y, width, height
    //For more interesting results add a gradient on top of the reflection
    Sakri.BitmapUtil.renderReflection = function(canvas, captureRect, border){
        if(!border){
            border = 5;
        }
        var context = canvas.getContext("2d");
        context.save();
        //move and flip vertically
        context.translate(captureRect.x, captureRect.y + captureRect.height*2 + border);
        context.scale(1, -1);

        context.drawImage(	canvas, captureRect.x, captureRect.y, captureRect.width, captureRect.height,
            0, 0, captureRect.width, captureRect.height);//img,sx,sy,swidth,sheight,x,y,width,height

        context.restore();

    };

}(window));



/**
 * Created by @sakri on 28-1-14.
 *
 * Somewhat Naive implementation in that there are cases where the edge detection gets stuck in an eternal loop.
 * This is currently "handled" by a MAX_POINTS variable.
 * This implementation is "good enough" for most use cases though.
 *
 */
(function (window){

    var Sakri = window.Sakri || {};
    window.Sakri = window.Sakri || Sakri;

    Sakri.MarchingSquares = {};

    //Update this when working with large shapes (large bitmaps)
    //the "edge detection loop" stops at this figure. This is in place in the event that an infinite loop somehow appears (Should never happen).
    Sakri.MarchingSquares.MAX_POINTS = 10000;

    //This is a lookup table of all possible 4 pixel grids, used to decide "scanning positions" during the edge detection process
    //Zeros represent transparent pixels, Ones represent a non transparent pixel
    Sakri.MarchingSquares.possibleGrids = {
        "0011":new Sakri.Geom.Point(1,0),
        "1011":new Sakri.Geom.Point(1,0),
        "0001":new Sakri.Geom.Point(1,0),
        "1001":new Sakri.Geom.Point(1,0),

        "0100":new Sakri.Geom.Point(0,-1),
        "0101":new Sakri.Geom.Point(0,-1),
        "0111":new Sakri.Geom.Point(0,-1),
        "0110":new Sakri.Geom.Point(0,-1),

        "1100":new Sakri.Geom.Point(-1,0),
        "1000":new Sakri.Geom.Point(-1,0),
        "1101":new Sakri.Geom.Point(-1,0),



        "1110":new Sakri.Geom.Point(0,1),
        "1010":new Sakri.Geom.Point(0,1),
        "0010":new Sakri.Geom.Point(0,1)

    };


    /**
     * Apparently there were (are?) cases where duplicate points are registered along horizontal or vertical sets
     * of adjacent points.  I haven't been able to reproduce this, but have left this option in place for now.
     */
    Sakri.MarchingSquares.getUniquePoints = function(points){
        console.log("MarchingSquares.getUniquePoints() points.length : ",points.length);
        var unique = {};
        var uniquePoints = [];
        var pointString, p, i;
        for(i=0; i < points.length; i++){
            p = points[i];
            pointString = p.x+":"+p.y;
            if(unique[pointString] == null){
                unique[pointString] = true;
                uniquePoints.push(p);
            }
        }
        console.log("MarchingSquares.getUniquePoints() uniquePoints.length : ",uniquePoints.length);
        return uniquePoints;
    };

    //source can be a Canvas or an img element. See comment above getUniquePoints concerning the checkUnique flag
    Sakri.MarchingSquares.getBlobOutlinePoints = function(source, checkUnique){
        //Create a copy with a one pixel blank "border" in case source image/canvas has pixels which touch the border
        //The edge scan operates with an offset of -1,-1 meaning the returned points are accurate
        var canvas = document.createElement("canvas");
        canvas.width = source.width + 2;
        canvas.height = source.height + 2;
        var context = canvas.getContext("2d");
        context.drawImage(source,1,1);

        if(checkUnique){
            return Sakri.MarchingSquares.getUniquePoints(Sakri.MarchingSquares.scanOutlinePoints(canvas));
        }else{
            return Sakri.MarchingSquares.scanOutlinePoints(canvas);
        }
     };

    //this should be private, should not be called directly
    Sakri.MarchingSquares.scanOutlinePoints = function(canvas){
        var points = [];
        points[0] = Sakri.BitmapUtil.getFirstNonTransparentPixelTopDown(canvas);
        if(points[0] == null){
            return points;
        }
        points[0].add(-1, -1);//in order for the lookup to work, we move the position up and back one
        var context = canvas.getContext("2d");
        var currentPosition = points[0];
        var gridString = Sakri.MarchingSquares.getGridStringFromPoint(context, currentPosition);
        var next, i;
        for(i=1; i<Sakri.MarchingSquares.MAX_POINTS; i++){
            next = Sakri.MarchingSquares.getNextEdgePoint(currentPosition, gridString);
            if(next.equals(points[0])){
                break;
            }
            points[i] = next;
            currentPosition = next;
            gridString = Sakri.MarchingSquares.getGridStringFromPoint(context, currentPosition);
        }
        //Failsafe when the marching squares get stuck in an eternal loop. See note at the top.
        if(i >= Sakri.MarchingSquares.MAX_POINTS){
            console.log("MarchingSquares.scanOutlinePoints Sakri.MarchingSquares.MAX_POINTS reached");
            return [];
        }
        return points;
    };

    Sakri.MarchingSquares.getGridStringFromPoint = function(context, point){
        var gridString = "";
        var data = context.getImageData(point.x, point.y, 2, 2).data;
        for(i=0; i<16; i+=4){
            gridString += (data[i+0] + data[i+1] + data[i+2] + data[i+3] > 0 ? "1" : "0");
        }
        return gridString;
    };

    Sakri.MarchingSquares.getNextEdgePoint = function(point, gridString){
        var offsetPoint = Sakri.MarchingSquares.possibleGrids[gridString];
        if(point==null){
            throw new Error("MarchingSquares Error : gridString:"+gridString+" , not found in possibleGrids");
        }
        return new Sakri.Geom.Point(point.x + offsetPoint.x, point.y + offsetPoint.y);
    };

}(window));




window.requestAnimationFrame =
        window.__requestAnimationFrame ||
                window.requestAnimationFrame ||
                window.webkitRequestAnimationFrame ||
                window.mozRequestAnimationFrame ||
                window.oRequestAnimationFrame ||
                window.msRequestAnimationFrame ||
                (function () {
                    return function (callback, element) {
                        var lastTime = element.__lastTime;
                        if (lastTime === undefined) {
                            lastTime = 0;
                        }
                        var currTime = Date.now();
                        var timeToCall = Math.max(1, 33 - (currTime - lastTime));
                        window.setTimeout(callback, timeToCall);
                        element.__lastTime = currTime + timeToCall;
                    };
                })();

var readyStateCheckInterval = setInterval( function() {
    if (document.readyState === "complete") {
        clearInterval(readyStateCheckInterval);
        init();
    }
}, 10);

//========================
//general properties for demo set up
//========================

var canvas;
var context;
var canvasContainer;
var htmlBounds;
var bounds;
var minimumStageWidth = 250;
var minimumStageHeight = 250;
var intervalId = -1;
var timeoutId = -1;
var resizeTimeoutId = -1;

function init(){
    canvas = document.createElement('canvas');
    canvas.style.position = "absolute";
    context = canvas.getContext("2d");
    canvasContainer = document.getElementById("canvasContainer");
    canvasContainer.appendChild(canvas);
    window.onresize = resizeHandler;
    commitResize();
}

function getWidth( element ){return Math.max(element.scrollWidth,element.offsetWidth,element.clientWidth );}
function getHeight( element ){return Math.max(element.scrollHeight,element.offsetHeight,element.clientHeight );}

//avoid running resize scripts repeatedly if a browser window is being resized by dragging
function resizeHandler(){
    renderBackground();
    clearTimeoutsAndIntervals();
    clearTimeout (resizeTimeoutId);
    resizeTimeoutId = setTimeout(commitResize, 300 );
}

function commitResize(){
    htmlBounds = new Sakri.Geom.Rectangle(0,0, getWidth(this.canvasContainer) , getHeight(canvasContainer));
    if(htmlBounds.width>=800){
        canvas.width = 800;
        canvas.style.left = htmlBounds.getCenterX() - 400+"px";
    }else{
        canvas.width = htmlBounds.width;
        canvas.style.left ="0px";
    }
    if(htmlBounds.height>500){
       canvas.height = 500;
       canvas.style.top = htmlBounds.getCenterY() - 300+"px";
    }else{
        canvas.height = htmlBounds.height;
        canvas.style.top ="0px";
    }
    bounds = new Sakri.Geom.Rectangle(0,0, canvas.width, canvas.height);
    console.log("commitResize() : "+bounds.toString());
    renderBackground();
    context.font = fontProperties.getFontString();
    context.textBaseline = "top";

    if(bounds.width<minimumStageWidth || bounds.height<minimumStageHeight){
        stageTooSmallHandler();
        return;
    }

    startDemo();
}

function stageTooSmallHandler(){
    var warning = "Sorry, bigger screen required :(";
    var props = new Sakri.CanvasTextProperties(null,null,24);
    context.font = props.getFontString();
    context.fillText(warning, bounds.getCenterX() - context.measureText(warning).width/2, bounds.getCenterY()-12);
}







//========================
//Demo specific properties
//========================


var words = ["GHOST", "SAKRI", "CANVAS", "CodePen", "DevState" ];
var currentWord;
var wordIndex = 0;
var wordBounds = new Sakri.Geom.Rectangle();
var wordAlpha = 0;

//var bgColor = "#0c0d43";
var bgColor = "#000000";
var outlines;//stores outline points of characters from marching squares

var shinePoint = new Sakri.Geom.Point();
var shineTarget = new Sakri.Geom.Point();
var maxRayLength = 80;
var outlinePointsSkip = 4;//the effect doesn't need all the outline points, this amount gets skipped. Poor bastards.
var minMoveSpeed = 2;
var maxMoveSpeed = 4;
var moveSpeed;
var maxEffectDistance;

var fontProperties = new Sakri.CanvasTextProperties(Sakri.CanvasTextProperties.BOLD, null, 160);

var charCenter = new Sakri.Geom.Point();
var average = new Sakri.Geom.Point();
var rayPoint = new Sakri.Geom.Point();
var furthestPoint = new Sakri.Geom.Point();
var animating = false;


function clearTimeoutsAndIntervals(){
    animating = false;
    clearInterval (intervalId);
    clearTimeout (timeoutId);
}

function startDemo(){
    renderBackground();
    currentWord = words[wordIndex];

    var testCanvas = document.createElement('canvas');
    var testContext = testCanvas.getContext("2d");
    testContext.fillStyle = "#000000";

    outlines = [];
    wordBounds.width = context.measureText(currentWord).width;
    wordBounds.height = fontProperties.fontSize;
    wordBounds.x = bounds.getCenterX() - wordBounds.width/2;
    wordBounds.y = bounds.getCenterY() - fontProperties.fontSize/2;//more or less
    maxEffectDistance = wordBounds.width/4;

    var xOffset = wordBounds.x + 0;
    var character, i, j, outlineCopy, point;

    for(i=0; i<currentWord.length; i++){
        character = currentWord.charAt(i);

        testContext.font = fontProperties.getFontString();
        testCanvas.width = testContext.measureText(character).width;
        testCanvas.height = fontProperties.fontSize * 1.5;//times 1.5 to be safe

        testContext.font = fontProperties.getFontString();
        testContext.textBaseline = "top";
        testContext.fillText(character,0,0);

        outlines[i] = Sakri.MarchingSquares.getBlobOutlinePoints(testCanvas);
        outlineCopy = [];

        for(j=0; j<outlines[i].length; j += outlinePointsSkip){
            point = outlines[i][j];
            point.x += xOffset;
            point.y += wordBounds.y;
            outlineCopy.push(point);
        }
        outlines[i] = outlineCopy;
        xOffset += testCanvas.width;
    }

    wordAlpha = 0;
    intervalId = setInterval(fadeCurrentWordIn, 20);
}

function fadeCurrentWordIn(){
    renderBackground();
    context.globalAlpha = wordAlpha;
    renderCurrentWord();
    context.globalAlpha = 1;
    wordAlpha += .05;
    if(wordAlpha>1){
        clearInterval(intervalId);
        startShine();
    }
}

function renderBackground(){
    context.fillStyle = bgColor;
    context.fillRect(0,0,bounds.width, bounds.height);
}

function renderCurrentWord(){
    context.fillStyle = "#FFFFFF";
    context.fillText(currentWord, wordBounds.x, wordBounds.y);
    context.strokeStyle = "#FFFFFF";
    context.lineWidth = 2;
    context.strokeText(currentWord, wordBounds.x, wordBounds.y);
}

function renderCurrentWordWithShading(){
    context.save();
    context.shadowColor = "#FFFFFF";
    charCenter.y = wordBounds.y+wordBounds.height/2;
    var dist, i;
    var xOffset = wordBounds.x;
    for(i=0; i<currentWord.length;i++){
        charCenter.x = xOffset + context.measureText(currentWord.charAt(i)).width/2;
        dist = Sakri.Geom.Point.distanceBetweenTwoPoints(shinePoint, charCenter);
        if(dist > maxEffectDistance){
            context.globalAlpha = 1;
            context.shadowBlur = 1;
            context.fillStyle = "#000011";
            context.strokeStyle = "#000011";
        }else{
            context.globalAlpha = Sakri.MathUtil.map(dist, 0, maxEffectDistance, .9 , .2);
            context.shadowBlur = Sakri.MathUtil.map(dist, 0, maxEffectDistance, 25 , 5);
            context.fillStyle = "#FFFFFF";
            context.strokeStyle = "#FFFFFF";
        }
        context.fillText(currentWord.charAt(i), xOffset, wordBounds.y);
        context.strokeText(currentWord.charAt(i), xOffset, wordBounds.y);
        xOffset += context.measureText(currentWord.charAt(i)).width;
    }
    context.restore();
}

function startShine(){

    //set start and end coordinates for shinePoint and shineTarget
    var xValues = [wordBounds.x - wordBounds.height, wordBounds.getRight() + wordBounds.height];
    shinePoint.x = Math.random()>.5 ? xValues.shift() : xValues.pop();
    shineTarget.x = xValues[0];
    shinePoint.y = shineTarget.y = wordBounds.y + Math.random() * wordBounds.height;

    moveSpeed = (shineTarget.x - shinePoint.x > 0 ? 1 : -1) * Sakri.MathUtil.getRandomNumberInRange(minMoveSpeed, maxMoveSpeed);

    renderBackground();
    renderCurrentWord();
    updateFunction = Math.random()>.5 ? renderBlobs : renderOutlines;

    animating = true;
    loop();
}

function loop(){
    updateShine()
    if(animating){
        window.requestAnimationFrame(loop, canvas);
    }
}

function updateShine(){
    shinePoint.x += moveSpeed;
    if(Math.abs(shineTarget.x-shinePoint.x) <= Math.abs(moveSpeed)){
        endShine();
        return;
    }

    context.globalAlpha = .1;
    renderBackground();
    context.globalAlpha = 1;

    context.shadowColor = "#FFFFFF";

    renderCurrentWordWithShading();

    context.shadowBlur = 20;
    context.shadowOffsetX = 0;
    context.shadowOffsetY = 0;

    for(var i=0; i<outlines.length; i++){
        updateFunction(outlines[i]);
    }
    context.globalAlpha = 1;
    context.shadowBlur = 0;
}

function renderBlobs(outline){
    var angle, point, i;
    var dist = Sakri.Geom.Point.distanceBetweenTwoPoints(shinePoint, outline[0]);
    if(dist > maxEffectDistance){
        return;
    }
    var rayLength = Sakri.MathUtil.map(dist, 0, maxEffectDistance, maxRayLength , maxRayLength/4);
    context.beginPath();
    furthestPoint.x = 0;
    point = outline[0];
    angle = Sakri.Geom.Point.angleBetweenTwoPoints(shinePoint, point) + Math.PI;
    context.moveTo(point.x + Math.cos(angle) * rayLength, point.y + Math.sin(angle) * rayLength);
    for(i=1; i<outline.length; i++){
        point = outline[i];
        angle = Sakri.Geom.Point.angleBetweenTwoPoints(shinePoint, point) + Math.PI;
        rayPoint.x = point.x + Math.cos(angle) * rayLength;
        rayPoint.y = point.y + Math.sin(angle) * rayLength;
        if(rayPoint.x > furthestPoint.x){
            furthestPoint.update(rayPoint.x, rayPoint.y);
        }
        average.x += rayPoint.x;
        average.y += rayPoint.y;
        context.lineTo(rayPoint.x, rayPoint.y);
    }
    average.x /= outline.length;
    average.y /= outline.length;
    context.closePath();

    var gradient = context.createRadialGradient(average.x, average.y, 10, average.x, average.y, Sakri.Geom.Point.distanceBetweenTwoPoints(average, furthestPoint));
    gradient.addColorStop(0,"rgba(255,255,255,.8)");
    gradient.addColorStop(1,"rgba(255,255,255,0)");

    context.fillStyle = gradient;

    //context.stroke();
    context.fill();
}

function renderOutlines(outline){
    var angle, point, i;
    var dist = Sakri.Geom.Point.distanceBetweenTwoPoints(shinePoint, outline[0]);
    if(dist > maxEffectDistance){
        return;
    }
    context.globalAlpha = Sakri.MathUtil.map(dist, 0, maxEffectDistance, .3, .05);
    var rayLength = Sakri.MathUtil.map(dist, 0, maxEffectDistance, maxRayLength , maxRayLength/4);
    context.beginPath();
    point = outline[0];
    angle = Sakri.Geom.Point.angleBetweenTwoPoints(shinePoint, point) + Math.PI;
    context.moveTo(point.x + Math.cos(angle) * rayLength, point.y + Math.sin(angle) * rayLength);
    for(i=1; i<outline.length; i++){
        point = outline[i];
        angle = Sakri.Geom.Point.angleBetweenTwoPoints(shinePoint, point) + Math.PI;
        context.lineTo(point.x + Math.cos(angle) * rayLength, point.y + Math.sin(angle) * rayLength);
    }
    context.closePath();
    context.stroke();
    context.globalAlpha = 1;
}



function endShine(){
    animating = false;
    wordAlpha = 1;
    intervalId = setInterval(fadeCurrentWordOut, 20);
}

function fadeCurrentWordOut(){
    renderBackground();
    context.globalAlpha = wordAlpha;
    renderCurrentWord();
    context.globalAlpha = 1;
    wordAlpha -= .05;
    if(wordAlpha <= 0){
        clearInterval(intervalId);
        wordIndex++;
        wordIndex %= words.length;
        timeoutId = setTimeout(startDemo, 500);
    }
}
              
            
!
999px

Console