<canvas id='gridwormCanvas' width='1350' height='620' style='background-color: white;' ></canvas>   
/*
 *Animates connected nodes about a grid
 *-------------------------------------  
 *@date:      19th April, 2020  
 *@email:     redutron@protonmail.com
 */  
//set up the gridworm
class GridWorm
{ 
    constructor(point,interval,pointsList,screenWidth,screenHeight)
    {      
        this.radius  = 2;     
        this.xCoord  = point.x; 
        this.yCoord  = point.y; 
        this.interval= interval;
        this.color = this.getColor(1,true);//get random color object
        this.mainColor = this.color.color;//color of the head and body of the girdworm
        this.mainColorIndex = this.color.index;
        this.nColor = this.getColor(1,true);//get another random color object
        this.arrowHeadColor = this.nColor.color;//color of the arrrow points at the head of the gridworm
        this.arrowHeadColorIndex = this.nColor.index; 
        this.pointsList = pointsList;  
        this.screenWidth = screenWidth; 
        this.screenHeight= screenHeight; 
        this.speed   = 5;//the magnitude of the velocity
        this.velocity= this.getVelocity(); 
        this.junctionMemory = [{point:point,velocity:this.velocity}];//memory of each junction visited(helps to construct the worm)
        //the maximum number of junctions a gridworm can keep in memory(this determines how long the gridworm will be)
        this.junctionMemoryLength = 6; 
    } 
    getColor(opacity,isRandom = true,index = 0) 
    { 
        if(opacity < 0 || opacity > 1 || opacity === null || isNaN(opacity))//if opacity is incorrect
        {
            opacity = 1; 
        }
        var colors = 
        [
            `rgba(0,0,0,${opacity})`,`rgba(192,192,192,${opacity})`/*silver*/,`rgba(128,128,128,${opacity})`/*gray*/,`rgba(128,0,0,${opacity})`/*maroon*/,
            `rgba(255,0,0,${opacity})`/*red*/,`rgba(0,255,0,${opacity})`/*lime*/,`rgba(0,0,255,${opacity})`/*blue*/,`rgba(255,0,255,${opacity})`/*fuchsia*/,
            `rgba(128,128,0,${opacity})`/*olive*/,`rgba(0,128,0,${opacity})`/*green*/,`rgba(128,0,128,${opacity})`/*purple*/,
            `rgba(0,128,128,${opacity})`/*teal*/,`rgba(0,0,128,${opacity})`/*navy*/,`rgba(138,57,0,${opacity})`/*brown*/, `rgba(205,133,63,${opacity})`, 
            `rgba(244,164,96,${opacity})`,`rgba(139,105,30,${opacity})`,`rgba(165,42,42,${opacity})`,`rgba(178,34,34,${opacity})`,
            `rgba(220,20,60,${opacity})`,`rgba(255,140,0,${opacity})`,`rgba(255,165,0,${opacity})`,`rgba(255,215,0,${opacity})`,`rgba(184,134,11,${opacity})`,
            `rgba(218,165,32,${opacity})`,`rgba(218,165,32,${opacity})`,`rgba(238,232,170,${opacity})`,`rgba(189,183,107,${opacity})`,`rgba(240,230,140,${opacity})`,  
            `rgba(0,100,0,${opacity})`, `rgba(34,139,34,${opacity})`,`rgba(32,178,170,${opacity})`,`rgba(47,79,79,${opacity})`, 
            `rgba(0,139,139,${opacity})`,`rgba(95,158,160,${opacity})`,`rgba(70,130,180,${opacity})`,`rgba(25,25,112,${opacity})`,
            `rgba(0,0,128,${opacity})`,`rgba(0,0,139,${opacity})`,`rgba(72,61,139,${opacity})`,`rgba(75,0,130,${opacity})`,`rgba(139,0,139,${opacity})`, 
            `rgba(0,0,0,${opacity})`,`rgba(105,105,105,${opacity})`, `rgba(169,169,169,${opacity})` 
        ];
        if(isRandom)
        {
            let index = Math.floor(this.getRandomNumber(0,colors.length-1)); 
            let color = colors[index];
            return {color:color,index:index}; 
        }
        else//if specific
        {
            if(index >=0 && index < colors.length)
            {
                return colors[index];
            } 
            return colors[0];
        } 
    }
    getVelocity() 
    {
        let x,y;
        //flip a coin to decide if gridworm moves vertically or horizontally
        if( Math.random() > 0.5)//if gridworm moves vertically
        {
            x = 0;//no horizontal movement
            y = Math.random() > 0.5? -this.speed: this.speed;//flip a coin to decide if gridworm moves upwards or downwards
        }
        else//if gridworm moves horizontally
        {
            x = Math.random() > 0.5? -this.speed: this.speed;//flip a coin to decide if gridworm moves left or right
            y = 0;//no vertical movement
        } 
        return {x:x, y:y};
    }
    /**
    * Returns a random number between min (inclusive) and max (exclusive)
    * @param  {number} min The lesser of the two numbers. 
    * @param  {number} max The greater of the two numbers.  
    * @return {number} A random number between min (inclusive) and max (exclusive)
    */
    getRandomNumber(min, max) 
    {
        return Math.random() * (max - min) + min;
    }
    drawCircle(x,y,circleradius,ctx,colorIndex) 
    {
        for(let i = 0; i < 3; i++)
        {
            let color   = '';  
            let radius = 0; 
            switch(i)//create three circles with same center
            {
                case 0: 
                    radius  =circleradius;//smallest circle
                    color   = this.getColor(1,false,colorIndex); 
                    break; 
                case 1: 
                    radius  =circleradius *   2;//bigger circle 
                    color   = this.getColor(0.5,false,colorIndex);             
                    break; 
                case 2: 
                    radius  =circleradius *   6;//biggest circle 
                    color   = this.getColor(0.2,false,colorIndex); 
                    break; 
            }
            //draw the node
            ctx.beginPath(); 
            ctx.arc(x,y,radius,0,2*Math.PI);
            ctx.fillStyle = color; 
            ctx.fill(); 
            ctx.strokeStyle = color;
            ctx.stroke();
        }
    }
    drawArrowHead(x,y,circleradius,ctx,colorIndex) 
    { 
        let points = [];  
        if(this.velocity.x === 0)//if gridworm is moving vertically
        {
            if(this.velocity.y > 0)//if gridworm is moving down
            {
                points.push({x:x+this.interval/3,y:y});//point to the right
                points.push({x:x-this.interval/3,y:y});//point to the left 
                points.push({x:x,y:y+this.interval/3});//point below 
            }
            else//if gridworm is moving up
            {
                points.push({x:x+this.interval/3,y:y});//point to the right
                points.push({x:x-this.interval/3,y:y});//point to the left
                points.push({x:x,y:y-this.interval/3});//point above  
            }
        }
        else//if gridworm is moving horizontally
        {
            if(this.velocity.x > 0)//if gridworm is moving right
            {
                points.push({x:x+this.interval/3,y:y});//point to the right 
                points.push({x:x,y:y-this.interval/3});//point above
                points.push({x:x,y:y+this.interval/3});//point below 
            }
            else//if gridworm is moving left
            {     
                points.push({x:x-this.interval/3,y:y});//point to the left
                points.push({x:x,y:y-this.interval/3});//point above
                points.push({x:x,y:y+this.interval/3});//point below 
            }
        }
        //draw a circle about the points that make the arrow head
        for(let i = 0; i < points.length;i++)
        {
            let point = points[i];
            this.drawCircle(point.x,point.y,circleradius/2,ctx,colorIndex); 
        } 
        this.drawTriangle(points[0],points[1],points[2],ctx);//draw the arrow head 
    }
    drawTriangle(point1,point2,point3,ctx)
    {
        ctx.beginPath();
        ctx.moveTo(point1.x, point1.y);
        ctx.lineTo(point2.x, point2.y);
        ctx.lineTo(point3.x, point3.y);  
        ctx.fillStyle = 'rgba(0,0,0,0.1)';//transparent black  
        ctx.fill();   
    }
    draw(ctx)
    {    
        //draw the head of the gridworm 
        this.drawCircle(this.xCoord,this.yCoord,this.radius/2,ctx,this.mainColorIndex); 
        this.drawArrowHead(this.xCoord,this.yCoord,this.radius/2,ctx,this.arrowHeadColorIndex); 
        //draw circles and squares at every visited junctions in the gridworm's memory(not RAM)
        for(let i = 0; i < this.junctionMemory.length; i++)
        {   
            let junction = this.junctionMemory[this.junctionMemory.length -(i+1)];
            //draw a circle at each junction point
            this.drawCircle(junction.point.x, junction.point.y,this.radius/2,ctx,this.mainColorIndex);  
            //draw painted squares at every junction point
            ctx.fillStyle   = this.getColor(0.1,false,this.mainColorIndex); 
            ctx.fillRect(junction.point.x,junction.point.y,this.interval,this.interval);
            
        } 
        //draw the line connecting head to body
        ctx.strokeStyle = 'black';
        ctx.lineWidth = this.radius; 
        ctx.beginPath(); 
        ctx.moveTo(this.xCoord,this.yCoord); 
        //draw a line to link all the visited junctions in the gridworm's memory(not RAM)
        for(let i = 0; i < this.junctionMemory.length; i++)
        {   //starting from the most recent to the least recent(LIFO)[NB: more like a stack data structure]
            let junction = this.junctionMemory[this.junctionMemory.length -(i+1)]; 
            ctx.lineTo(junction.point.x, junction.point.y);   
        } 
        ctx.stroke(); 
        ctx.closePath(); 
    } 
    update(deltaTime)
    {       
        this.junctionMemoryLength = this.junctionMemoryLength < 1? 1: this.junctionMemoryLength; 
        //keep the gridworm moving in its current direction  
        this.xCoord += this.velocity.x;//if gridworm is going left or right, keep it going
        this.yCoord += this.velocity.y;//if gridworm is going up or down, keep it going   
        if(this.xCoord <= this.interval)//if gridworm reaches the leftmost point 
        {
            this.xCoord = this.interval;
            this.velocity.x  = -this.velocity.x;//move right 
            this.xCoord += this.velocity.x * 3;//nudge it a bit away from the edge
        }
        if(this.xCoord >= this.screenWidth - this.interval)//if gridworm reaches the rightmost point
        {
            this.xCoord = this.junctionMemory[this.junctionMemory.length-1].point.x; 
            this.velocity.x  = -this.velocity.x;//move left 
            this.xCoord += this.velocity.x * 3;//nudge it a bit away from the edge
        }
        if(this.yCoord <= this.interval)//if gridworm reaches the topmost most point
        {
            this.yCoord  = this.interval; 
            this.velocity.y  = -this.velocity.y; //move down
            this.yCoord  += this.velocity.y * 3;//nudge it a bit away from the edge
        }
        if(this.yCoord >= this.screenHeight - this.interval)//if gridworm reaches the lowest point) 
        {
            this.yCoord  = this.junctionMemory[this.junctionMemory.length-1].point.y; 
            this.velocity.y  = -this.velocity.y;//move up
            this.yCoord  += this.velocity.y * 4;//nudge it a bit away from the edge
        }
        let currentCoord    = {x:this.xCoord,y:this.yCoord}; 
        let latestJunction  = this.getJunctionReached(currentCoord); 
        if(latestJunction !== currentCoord)
        {   
            let originalVelocity = this.velocity; 
            let newVelocity = this.getVelocity();//flip a coin to decide to move up and down or right and left  
            if(originalVelocity.y === 0 )//if gridworm is moving horizontally
            {
                this.velocity = newVelocity;
                if(newVelocity.y === 0 && newVelocity.x === -originalVelocity.x )//if it continues the horizontal movement in the opposite direction
                {
                    //don't add the new junction to the memory queue
                }
                else 
                {
                    let memory = {point:latestJunction,velocity:this.velocity}; 
                    if(!this.isInMemory(memory))
                    {
                        this.junctionMemory.push(memory);//add new memory to the queue
                    }
                    //this.junctionMemory.push({point:latestJunction,velocity:this.velocity});//add new memory to the queue
                }
                //nudge it a bit away from the junction
                this.xCoord += this.velocity.x * 3; //not complete yet. Don't make it too much or too little.  
            }
            else //if gridworm is moving vertically 
            {
                this.velocity = newVelocity;
                if(newVelocity.x === 0 && newVelocity.y === -originalVelocity.y )//if it continues the verticalal movement in the opposite direction
                {
                    //don't add the new junction to the memory queue
                }
                else 
                {
                    let memory = {point:latestJunction,velocity:this.velocity}; 
                    if(!this.isInMemory(memory))
                    {
                        this.junctionMemory.push(memory);//add new memory to the queue
                    } 
                }
                //nudge it a bit away from the junction
                this.yCoord += this.velocity.y * 3; //not complete yet. Don't make it too much or too little. 
            } 
        }
        if(this.junctionMemory.length > this.junctionMemoryLength)//if memory is too long
        {
            this.junctionMemory.shift();//remove the first memory
        } 
    }  
    isInMemory(memory)//check if a junction is in memory
    { 
        this.junctionMemory.some(function(mem)
        {
            if(mem.point === memory.point)
            { 
                return true;//junction is in memory
            }
            return mem.point === memory.point; 
        }); 
        return false;//junction is NOT in memory
    }
    getJunctionReached(currentCoord)
    {
        for(let i = 0; i < this.pointsList.length; i++)
        {
            let point = this.pointsList[i];
            //if point(junction) is too far away, ignore it 
            if(Math.abs(currentCoord.x - point.x) > (2 * this.interval) || Math.abs(currentCoord.y - point.y) > (2 *this.interval) )
            {
                continue; 
            }
            let distance = this.getDistance(currentCoord,point);  
            if(distance <= (this.radius))//if gridworm head is close enough to a junction
            {  
                return point;  
            }
        } 
        return currentCoord;  
    }     
    getDistance(p1,p2)//the distance between two points, p1 and p2
    {
        let dx = p1.x - p2.x; 
        let dy = p1.y - p2.y; 
        let distance = Math.sqrt(dx*dx + dy*dy);
        return distance; 
    }
    
    
    /**
    * Let node correspond to window resizing.
    * @param  {number} screenHeight The height of the screen. 
    * @param  {number} screenWidth  The width of the screen.  
    * @param  {number} dy           The percentage change in browser window height 
    * @param  {number} dx           The percentage change in browser window width  .  
    */
    refreshScreenSize(screenHeight,screenWidth,dx,dy,points)
    {     
        
    }   
}

//sets up and controls all points and gridworms on the canvas 
class Painter
{
    constructor(screenWidth,screenHeight)
    {      
        this.screenWidth    = screenWidth;
        this.screenHeight   = screenHeight;   
        this.interval       = 40;//interval from one point to the next 
        this.points         = this.createPoints(); //coordinates of the vertices of all squares when the canvas is partitioned
        this.gridWorms      = this.createGridWorms(); 
        this.color          = this.getRandomColor(0.1);
        document.addEventListener('click',(event)=>//when user clicks on the canvas
        {   
            this.points     = this.createPoints();
            this.gridWorms  = this.createGridWorms();//spawn new gridworms
            this.color          = this.getRandomColor(0.1);
        });
    } 
    createGridWorms() 
    {
        let gridworms = [],
            numOfGridWorms = 30; 
        for(var i = 0; i < numOfGridWorms; i++)
        { 
            let point = this.points[Math.floor(this.getRandomNumber(0,this.points.length-1))];//randomly select a point
            gridworms.push(new GridWorm(point,this.interval,this.points,this.screenWidth,this.screenHeight));
        }
        return gridworms; 
    }
    createPoints()//divide the canvas into squares 
    {
        let points = [], 
            interval = this.interval;//interval from one point to the next 
        for(var y = interval; y < this.screenHeight; y+=interval)//get all points in the grid, starting from the top to the bottom
        { 
            if(y+interval > this.screenHeight)//if the next point is beyond the right edge of the canvas
            {
                continue; //skip
            } 
            for(var x = interval; x < this.screenWidth; x+=interval)//all the while, getting all the horizontal points at each level 
            { 
                if(x+interval > this.screenWidth)//if the next point is beyond the bottom edge of the canvas
                { 
                    continue; //skip
                } 
                points.push({x:x,y:y}); 
            } 
        }
        return points;  
    }  
    getRandomColor(opacity)
    {
        var colors = [
            `rgba(255,0,0,      ${opacity})`,//red
            `rgba(255, 242,0,   ${opacity})`,//yellow, 
            `rgba(0,0,255,      ${opacity})`,//blue
            `rgba(255,255,0,    ${opacity})`,//yellow
            `rgba(0,255,255,    ${opacity})`,//cyan
            `rgba(255,0,255,    ${opacity})`,//magenta/fuchsia
            `rgba(192,192,192,  ${opacity})`,//silver
            `rgba(128,128,128,  ${opacity})`,//gray 
            `rgba(128,0,0,      ${opacity})`,//maroon
            `rgba(128,128,0,    ${opacity})`,//olive
            `rgba(0,128,0,      ${opacity})`,//green
            `rgba(128,0,128,    ${opacity})`,//purple 
            `rgba(0,128,128,    ${opacity})`,//teal
            `rgba(0,0,128,      ${opacity})`,//navy 
            `rgba(0, 255, 0,    ${opacity})`,//green
            `rgba(77, 0, 255,   ${opacity})`,//blue
            `rgba(255, 0, 140,  ${opacity})`,//purple
            `rgba(0,255,0,      ${opacity})`//lime
        ];
        return colors[parseInt(this.getRandomNumber(0, colors.length))];
    }
    /**
    * Returns a random number between min (inclusive) and max (exclusive)
    * @param  {number} min The lesser of the two numbers. 
    * @param  {number} max The greater of the two numbers.  
    * @return {number} A random number between min (inclusive) and max (exclusive)
    */
    getRandomNumber(min, max) 
    {
        return Math.random() * (max - min) + min;
    } 
    /**
    * Let canvas respond to window resizing.
    * @param  {number} screenHeight The height of the screen. 
    * @param  {number} screenWidth  The width of the screen.  
    */
    refreshScreenSize(screenHeight,screenWidth)
    {   
        if(this.screenHeight !== screenHeight || this.screenWidth !== screenWidth)//if the screen size has changed
        {  
            this.screenHeight   = screenHeight;  
            this.screenWidth    = screenWidth;   
            this.points         = this.createPoints(); //coordinates of the vertices of all squares when the canvas is partitioned
            this.gridWorms      = this.createGridWorms();  
        } 
    }  
    update(deltaTime)
    {     
       this.gridWorms.forEach(function(gridworm)
        {
            gridworm.update(deltaTime); 
        }); 
    }  
    draw(ctx)
    {    
        /*
        for(var i = 0; i < this.points.length; i++)
        {
            let point = this.points[i];
            ctx.fillStyle   = Math.random() > 0.5? this.color:'white';//creates a disco effect 
            ctx.fillRect(point.x,point.y,this.interval,this.interval);
        }
        */
        this.gridWorms.forEach(function(gridworm)
        {
            gridworm.draw(ctx); 
        }); 
    }   
}

//set everything up 
function getBrowserWindowSize() 
{
    let win = window,
    doc     = document,
    offset  = 20,//
    docElem = doc.documentElement,
    body    = doc.getElementsByTagName('body')[0],
    browserWindowWidth  = win.innerWidth || docElem.clientWidth || body.clientWidth,
    browserWindowHeight = win.innerHeight|| docElem.clientHeight|| body.clientHeight; 
    return {x:browserWindowWidth-offset,y:browserWindowHeight-offset}; 
} 
let browserWindowSize   = getBrowserWindowSize(),
c   = document.getElementById("gridwormCanvas"),
ctx = c.getContext("2d"); 
//set size of canvas
c.width          = browserWindowSize.x; 
c.height         = browserWindowSize.y; 
let SCREEN_WIDTH = browserWindowSize.x,
    SCREEN_HEIGHT= browserWindowSize.y,   
    painter      = new Painter(SCREEN_WIDTH,SCREEN_HEIGHT),  
    lastTime     = 100,  
    windowSize;   
function onWindowResize()//called every time the window gets resized. 
{  
    windowSize     = getBrowserWindowSize();
    c.width        = windowSize.x; 
    c.height       = windowSize.y; 
    SCREEN_WIDTH   = windowSize.x;
    SCREEN_HEIGHT  = windowSize.y;  
}
window.addEventListener('resize',onWindowResize); 
function updateCanvas()
{
    ctx.clearRect(0,0,SCREEN_WIDTH,SCREEN_HEIGHT);    
    ctx.fillStyle   = 'white';  
    ctx.fillRect(0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
}
function doAnimationLoop(timestamp)
{           
    updateCanvas();
    painter.refreshScreenSize(SCREEN_HEIGHT,SCREEN_WIDTH);//let canvas respond to window resizing  
    let deltaTime  = timestamp - lastTime; 
        lastTime   = timestamp;
    painter.update(deltaTime);   
    painter.draw(ctx);  
    requestAnimationFrame(doAnimationLoop); 
} 
requestAnimationFrame(doAnimationLoop); 

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.