I like to make things. When those things fit inside an internet tube, I might put them in the "Experiments" section so other people can see how they work.

Consider this the hub of all the other bits of me strung about the internet. To get to some of those other bits, check out the Connect section below. Then you can see if you and I share space on some other company's hard drive.

Experiments


A collection of some experiments in interactive / generative code for web experiences. Feel free to use / alter for whatever purpose, but this code is provided "AS IS", without warranty of any kind, and may malfunction or explode without warning. Also, the source code may not be exactly the same as what's shown in the box below.

Aquamarine

Procedurally Generated Banner Graphics with p5.js

(Changes over time)

SourceCode

/*
Aquamarine
 Procedurally Generated Banner Graphics
 Theodore Michels 2015
 Last updated: 07/30/2015
 */

// Put this in a function instance so that everything is out of the global namespace
var aquamarine = function (a) {
    // An array of "brushes", which will create all of the strokes
    var numBrushes = 50;
    var theBrushes = [];

    a.setup = function () {
        canvas = a.createCanvas(a.windowWidth * 0.8, 480);
        a.smooth();

        for (var i = 0; i < numBrushes; i++) {
            theBrushes[i] = new Brush(a.random(a.width), a.random(a.height)); // Initialize all of the brushes at random positions
        }
        a.background(0); // Initialize the background.
    }

    a.draw = function () { // Note that no background is drawn here - previous frames are preserved.
        for (var i = 0; i < numBrushes; i++) {
            theBrushes[i].update(); // Update all of the brushes.
            if (theBrushes[i].strokeLifetime > theBrushes[i].strokeDuration * 2) {
                theBrushes[i] = new Brush(a.random(a.width), a.random(a.height)); // If a brush has been drawing for too long, then re-initialize it.
                
            }
        }
        a.strokeWeight(20);
        a.stroke(0);
        a.line(0,0, a.width, 0);
        a.line(0, 0, 0, a.height);
        a.line(0, a.height, a.width, a.height);
        a.line(a.width, 0, a.width, a.height);
    }

    // Consists of several "bristles", which are just ellipses of different sizes and colors,
    // which appear and disappear at different rates.
    function Brush(startX, startY) {
        this.numBristles = 25; // How many different points are drawing lines?

        this.brushPosition = new p5.Vector(startX, startY); // The central brush position.
        this.brushMovement = new p5.Vector(); // The movement vector.

        this.bristlePositions = []; // The position of each "bristle"
        this.bristleSpeeds = []; // Each bristle increases in size at a different rate.
        this.bristleSizes = []; // Keeps track of the current size of each bristle.
        this.bristleColors = []; // Each bristle should have a slightly different color.
        this.startOffsets = []; // Each bristle should start drawing at different times.
        this.maxBristleSize = []; // The maximum size of this bristle.

        this.brushWidth = a.random(30, 60); // How wide is the brush?

        this.strokeDuration = a.random(100, 400); // When should the stroke start tapering off?
        this.strokeLifetime = 0; // How long has the stroke been..."stroking"?

        this.moveCounter = 0; // For drawing curves.
        this.moveRate = a.random(-0.04, 0.04); // How fast is the curve drawn - also affects the "tightness" of the curve
        // Random colors.
        this.colorR = a.random(0, 100);
        this.colorG = a.random(100, 255);
        this.colorB = a.random(180, 255);

        // Each bristle is special and unique - like a snowflake!
        for (var i = 0; i < this.numBristles; i++) {
            this.bristleSizes[i] = 0.0;
            this.maxBristleSize[i] = a.random(1, 3.0);
            this.bristleSpeeds[i] = a.random(0.01, 0.2);
            this.startOffsets[i] = a.random(0, 60);
            this.bristleColors[i] = a.color(a.random(this.colorR / 2, this.colorR), a.random(this.colorG / 2, this.colorG), a.random(this.colorB / 2, this.colorB), 100);
        }

        this.update = function () {
            this.moveCounter += this.moveRate; // Increment the counter.
            this.brushMovement = new p5.Vector(a.sin(this.moveCounter), a.cos(this.moveCounter)); // The brush moves in a circle.
            this.strokeLifetime++; // How many frames has this been around?
            this.brushPosition.add(this.brushMovement); // Add the movement vector to the current position.
            this.perpVector = new p5.Vector(-this.brushMovement.y, this.brushMovement.x); // Get a vector perpendicular to the forward vector.
            this.perpVector.normalize();

            a.noStroke();
            for (var i = 0; i < this.numBristles; i++) {
                if (this.strokeLifetime > this.startOffsets[i]) { // If the stroke has been around longer than its offset, start increasing its size.
                    if (this.strokeLifetime > this.strokeDuration) { // If the lifetime is greater than the duration, start decreasing the stroke size.
                        if (this.strokeLifetime - this.strokeDuration > this.startOffsets[i]) { // Use the start offset again so that strokes start tapering at different times.
                            this.bristleSizes[i] -= this.bristleSpeeds[i];
                        }
                    } else {
                        this.bristleSizes[i] += this.bristleSpeeds[i]; // Increase the bristle size.
                        this.bristleSizes[i] += a.random(-0.4, 0.4); // Add random varation to bristle size for a more "organic" look.
                    }

                    // Clamp the bristle size between 0 and the max.
                    if (this.bristleSizes[i] > this.maxBristleSize[i]) {
                        this.bristleSizes[i] = this.maxBristleSize[i];
                    }
                    if (this.bristleSizes[i] < 0) {
                        this.bristleSizes[i] = 0;
                    }                    
                }

                this.bristlePositions[i] = this.perpVector.copy();
                // Arrange bristles perpendicular to the forward vector at varying distances.
                this.bristleOffset = (i + 0.5 - this.numBristles / 2.0) * (this.brushWidth / this.numBristles);
                this.bristlePositions[i].mult(this.bristleOffset);
                this.bristlePositions[i].add(this.brushPosition); // Add the offset vector to the current position.

                a.fill(this.bristleColors[i]); // Fill with this particular bristle's color.
                // Each bristle is an ellipses perpendicular to the forward vector.
                a.ellipse(this.bristlePositions[i].x, this.bristlePositions[i].y, this.bristleSizes[i], this.bristleSizes[i]);
            }
        }
    }
}

var p5_aq = new p5(aquamarine, "aquamarine");

Another procedural "painting". This sketch started as an attempt to mimic the effects of Photoshop brushes. Basically, all of the strokes drawn on the canvas are created by individual "brushes", each of which has a number of "bristles", which are just circles that change in size over time. Each brush moves along a circular path, the "forward vector", and each bristle is perpendicular to this vector. By preserving every frame of each brush's position, strokes are created. Inadvertently inspired by Jasper Johns

Assembler

Procedurally Generated Banner Graphics with p5.js

(Changes over time)

SourceCode

/*
Assembler
 Procedurally Generated Banner Graphics
 Theodore Michels 2015
 Last updated: 07/22/2015
 */

// Put this in a function instance so that everything is out of the global namespace
var assembler = function (a) {
    var numPoints = 50; // The starting number of points.

    var smartPoints = []; // Contains each of the points which will look for other points to connect to.

    var moving = false; // Is the composition currently moving?
    var moveDuration = 300; // For how long should the composition move (frames)?
    var timeMoving = 0;
    var stillDuration = 0; // For how long should the composition remain still?
    var timeStill = 0;

    /**** Colors ****/
    // Keep track of previous colors to interpolate between them.
    var bgColor;
    var bgColorLast;
    var currentBgColor = a.color(0, 0, 0);

    var triColor;
    var triColorLast;
    var currentTriColor = a.color(0, 0, 0);

    a.setup = function () {
        canvas = a.createCanvas(a.windowWidth, 480);
        a.smooth();
        a.background(0);
        for (var i = 0; i < numPoints; i++) {
            smartPoints[i] = new SmartPoint(); // Create the initial number of "Smart Points" (class listed below...)
        }
        // Random starting colors.
        bgColorLast = a.color(a.random(180), a.random(180), a.random(180));
        bgColor = a.color(a.random(180), a.random(180), a.random(180));
        triColorLast = a.color(a.random(255), a.random(255), a.random(255), 80);
        triColor = a.color(a.random(255), a.random(255), a.random(255), 80);
    }

    a.draw = function () {
        a.background(currentBgColor); // Fill with the currently lerped bgColor  
        // Move once the time is up.
        if (timeStill > stillDuration) {
            moving = true;
        } else {
            moving = false;
        }

        if (moving) {
            timeMoving++;
            // While moving, interpolate the colors of the background and triangles between random values
            currentBgColor = a.lerpColor(bgColorLast, bgColor, timeMoving / moveDuration);
            currentTriColor = a.lerpColor(triColorLast, triColor, timeMoving / moveDuration);
            // Once the movement time is up, generate some new random values and reset counters.
            if (timeMoving > moveDuration) {
                stillDuration = a.random(200, 600);
                moveDuration = a.random(100, 400);
                timeStill = 0;
                timeMoving = 0;

                bgColorLast = bgColor; // This color is now the last one; then generate a new one.
                bgColor = a.color(a.random(180), a.random(180), a.random(180));

                triColorLast = triColor;
                triColor = a.color(a.random(255), a.random(255), a.random(255), 80);
            }
        } else {
            timeStill++;
        }

        for (var i = 0; i < smartPoints.length; i++) {
            smartPoints[i].update(); // Update all points    

            if (smartPoints[i].done) {
                // After removing inactive points, add new ones.
                smartPoints[i] = new SmartPoint();
            }
        }
    }

    /******** CLASSES ********/
    function SmartPoint() {
        // This "SmartPoint" looks for its 2 nearest neighbors and draws a triangle using itself and the other 2 points.
        this.offscreen = false; // If this point is "out of bounds", it should stop looking for connections.
        this.done = false;

        this.size = a.random(10, 60);
        this.alphaValue = 0.0;

        this.currentPosition = new p5.Vector(a.random(a.width), a.random(a.height)); // Position of this point.
        this.currentEndPoint1 = new p5.Vector(); // Where are the points of the triangle currently?
        this.currentEndPoint2 = new p5.Vector();
        this.movement = p5.Vector.random2D(); // Generate a random movement direction for this point.
        this.movement.mult(a.random(0.2, 1.0));
        // Are the other ends of the triangle moving? If so, how far have they gotten?
        this.moving1 = false;
        this.moveCounter1 = 0.0;
        this.moving2 = false;
        this.moveCounter2 = 0.0;

        // The other "Smart Points" that this point is linked to.
        // Also keep track of the "OLD" points for interpolation.
        this.sibling_1 = this;
        this.sibling_1_OLD = this;
        this.sibling_2 = this;
        this.sibling_2_OLD = this;

        this.update = function () { // The class' central function

            // Check to see if the point is offscreen (with some padding)
            if (this.currentPosition.x > a.width + a.width / 4 ||
                this.currentPosition.x < 0 - a.width / 4 ||
                this.currentPosition.y > a.height + a.height / 4 ||
                this.currentPosition.y < 0 - a.height / 4) {
                if (!this.offscreen) { // This should only happen once.
                    this.offscreen = true;
                }
            }

            if (moving) {
                this.currentPosition.add(this.movement); // Add the random movement vector if applicable
            }
            /******** FIND 2 NEAREST POINTS ********/
            // If this point is not offscreen, and if neither of its points are moving, then continue looking for the closest points.
            if (!this.offscreen && !this.moving1 && !this.moving2) {
                for (var i = 0; i < smartPoints.length; i++) { // Look through every other point.
                    this.curPoint = smartPoints[i]; // Placeholder for simplification
                    if (this.curPoint != this && // Don't look at self.
                        this.curPoint != this.sibling_2 && // Don't look if already connected via "sibling_2"
                        this.curPoint.sibling_1 != this && // Ignore if other points are already connected to this one.
                        this.curPoint.sibling_2 != this) {
                        // Finally, if the currently examined point is closer that the current "sibling", then this point will be its new sibling.
                        if (this.sibling_1 == this ||
                            this.sibling_1.offscreen ||
                            p5.Vector.dist(this.currentPosition, this.curPoint.currentPosition) < p5.Vector.dist(this.currentPosition, this.sibling_1.currentPosition)
                        ) {
                            this.sibling_1 = this.curPoint;
                        }
                        // Now, do the same thing again to get the second point.
                        if (this.curPoint != this.sibling_1) { // Ignore sibling_1
                            if (this.sibling_2 == this ||
                                p5.Vector.dist(this.currentPosition, this.curPoint.currentPosition) < p5.Vector.dist(this.currentPosition, this.sibling_2.currentPosition) ||
                                this.sibling_2.offscreen
                            ) {
                                this.sibling_2 = this.curPoint;
                            }
                        }
                    }
                }
            }

            /******** MOVE TO NEAREST POINTS ********/
            /**** First Point ****/
            if (this.sibling_1 != this.sibling_1_OLD || this.offscreen) { // If the nearest sibling has changed...then move to it.
                this.moving1 = true;
            }

            if (this.moving1) { // If moving...interpolate between points based on a number of factors.
                this.moveCounter1 += 0.05; // Increment the counter for interpolation.
                if (!this.offscreen) { // If not offscreen, lerp between the old position and the new.
                    this.currentEndPoint1 = p5.Vector.lerp(this.sibling_1_OLD.currentPosition, this.sibling_1.currentPosition, this.moveCounter1);
                } else { // Otherwise, "retract" by lerping between the nearest point and the current position.
                    this.currentEndPoint1 = p5.Vector.lerp(this.sibling_1.currentPosition, this.currentPosition, this.moveCounter1);
                }
            } else { // If finished moving, the end point is just the sibling's position.
                this.currentEndPoint1 = this.sibling_1.currentPosition;
            }

            if (this.moveCounter1 >= 1.0) { // If the counter is greater than 1 (at the destination), then stop moving.
                this.moving1 = false; // Stop moving and reset the counter.
                this.moveCounter1 = 0.0;
                if (!this.offscreen) {
                    this.sibling_1_OLD = this.sibling_1; // The old and current siblings are now the same.
                } else {
                    this.done = true; // If offscreen, this point is ready for removal.
                }
            }
            /**** Second Point ****/
            if (this.sibling_2 != this.sibling_2_OLD || this.offscreen) { // Now do the same thing for the second point.
                this.moving2 = true;
            }

            if (this.moving2) {
                this.moveCounter2 += 0.05;
                if (!this.offscreen) {
                    this.currentEndPoint2 = p5.Vector.lerp(this.sibling_2_OLD.currentPosition, this.sibling_2.currentPosition, this.moveCounter2);
                } else {
                    this.currentEndPoint2 = p5.Vector.lerp(this.sibling_2.currentPosition, this.currentPosition, this.moveCounter2);
                }
            } else {
                this.currentEndPoint2 = this.sibling_2.currentPosition;
            }

            if (this.moveCounter2 >= 1.0) {
                this.moving2 = false;
                this.moveCounter2 = 0.0;
                if (!this.offscreen) {
                    this.sibling_2_OLD = this.sibling_2;
                } else {
                    this.done = true;
                }
            }

            /******** DISPLAY ********/
            a.noStroke(); // No stroke, we'll draw an "incomplete" border below with line()
            a.strokeWeight(1);
            a.fill(currentTriColor);
            // Draw a triangle between this point and its two siblings.
            a.triangle(this.currentPosition.x, this.currentPosition.y, this.currentEndPoint1.x, this.currentEndPoint1.y, this.currentEndPoint2.x, this.currentEndPoint2.y);

            // Fade in the ellipses
            if (this.alphaValue < 80) {
                this.alphaValue += 1;
                if (this.alphaValue >= 80) {
                    this.alphaValue = 80.0;
                }
            }

            a.stroke(255, this.alphaValue * 2.0);
            a.strokeWeight(2);
            a.noFill();
            a.ellipse(this.currentPosition.x, this.currentPosition.y, this.size * 1.2, this.size * 1.2); // The larger, outer ring.

            this.ellipseColor = a.lerpColor(currentTriColor, currentBgColor);
            this.ellipseColor = a.color(this.ellipseColor.getRed, this.ellipseColor.getGreen, this.ellipseColor.getBlue, this.alphaValue);
            a.fill(this.ellipseColor); // A color between the background and triangles.
            a.stroke(255, this.alphaValue);
            a.strokeWeight(1);
            a.ellipse(this.currentPosition.x, this.currentPosition.y, this.size, this.size); // The smaller, inner ring.

            // Find a bunch of points between all of the triangle's corners.
            this.start1 = p5.Vector.lerp(this.currentPosition, this.currentEndPoint1, 0.4);
            this.end1 = p5.Vector.lerp(this.currentPosition, this.currentEndPoint1, 0.6);
            this.start2 = p5.Vector.lerp(this.currentPosition, this.currentEndPoint2, 0.4);
            this.end2 = p5.Vector.lerp(this.currentPosition, this.currentEndPoint2, 0.6);
            this.start3 = p5.Vector.lerp(this.currentEndPoint1, this.currentEndPoint2, 0.4);
            this.end3 = p5.Vector.lerp(this.currentEndPoint1, this.currentEndPoint2, 0.6);
            a.strokeWeight(1);
            a.strokeCap(a.SQUARE);
            a.stroke(255, 80);
            // Draw the border lines
            a.line(this.currentPosition.x, this.currentPosition.y, this.start1.x, this.start1.y);
            a.line(this.currentEndPoint1.x, this.currentEndPoint1.y, this.end1.x, this.end1.y);
            a.line(this.currentPosition.x, this.currentPosition.y, this.start2.x, this.start2.y);
            a.line(this.currentEndPoint2.x, this.currentEndPoint2.y, this.end2.x, this.end2.y);
            a.line(this.currentEndPoint1.x, this.currentEndPoint1.y, this.start3.x, this.start3.y);
            a.line(this.currentEndPoint2.x, this.currentEndPoint2.y, this.end3.x, this.end3.y);
        }
    }
}

var p5_a = new p5(assembler, "assembler");

This project was meant to be a sort of generative, digital "painting" - partly inspired by Casey Reas' amazing work. The composition changes randomly over time - sometimes the results are quite nice, and other times they're quite ugly. It's also a kind of simple experiment in emergent behavior. Basically, each circle autonomously looks for its nearest two neighbors and then creates a triangle with the resulting points. By putting a bunch of these simple behaviors together, you can get some pretty complex results. I disobeyed many of the DRY commandments in this one, so the source code gets a bit redundant in places.

Raycaster

Interactive, Procedurally Generated Banner Graphics with p5.js

(Click square on lower left for interface)

Source Code

/*
Raycaster
Interactive, Procedurally Generated Banner Graphics
Theodore Michels 2015
Last updated: 07/12/2015
*/

// Put this in a function instance so that everything is out of the global namespace
var raycaster = function (r) {
    var numLines = 200; // How many lines at once?
    var rays = []; // Contains all of the "LineRay" objects.

    var rotationAngle = 135.0; // Angle to rotate all of the lines.

    /**** UI ****/
    var showUI = false;
    var overToggle = false;
    var overSlider = false;
    var toggleSize = 20;
    var sliders = [];

    var canvas
    r.setup = function () {
        canvas = r.createCanvas(r.windowWidth, 200);
        canvas.mousePressed(uiCheck); // Add mousePressed listener
        canvas.mouseReleased(releaseCheck);
        r.background(0);
        r.smooth();

        /**** INITIALIZE SLIDERS ****/
        sliders[0] = new Slider(r.width / 2, r.height - toggleSize, r.width / 2, toggleSize, r.color(255, 80)); // Speed slider

        sliders[1] = new Slider(toggleSize, r.height / 2, toggleSize, r.height / 2, r.color(255, 0, 0, 120)); // Red value slider
        sliders[1].sliderValue = 0.2; // Arbitrary initial values
        sliders[2] = new Slider(toggleSize * 3, r.height / 2, toggleSize, r.height / 2, r.color(0, 255, 0, 120)); // Green value slider
        sliders[2].sliderValue = 0.5;
        sliders[3] = new Slider(toggleSize * 5, r.height / 2, toggleSize, r.height / 2, r.color(0, 0, 255, 120)); // Blue value slider
        sliders[3].sliderValue = 0.5;

        /**** INITIALIZE RAYS ****/
        for (var i = 0; i < numLines; i++) {
            rays[i] = new LineRay();
            rays[i].init();
        }
    }

    r.draw = function () {
        r.background(0);
        /******** RAYS ********/
        // Translate rays to the center for easy rotation around a central point.
        r.push();
        r.translate(r.width / 2, r.height / 2);
        r.rotate(r.radians(rotationAngle));
        for (var i = 0; i < numLines; i++) {
            rays[i].update();
        }
        r.pop();

        /******** UI ********/

        /**** Toggle ****/
        r.noFill();
        r.stroke(255);
        r.rectMode(r.CENTER);
        // Check to see if the mouse is over the toggle
        if (r.mouseX > toggleSize - (toggleSize / 2) &&
            r.mouseX < toggleSize + (toggleSize / 2) &&
            r.mouseY < r.height - (toggleSize / 2) &&
            r.mouseY > r.height - toggleSize - (toggleSize / 2)) {
            overToggle = true;
        } else {
            overToggle = false;
        }
        if (overToggle) {
            r.strokeWeight(2);
        } // highlight on mouse over
        else {
            r.strokeWeight(1);
        }

        if (showUI) {
            r.fill(255, 80);
        } else {
            r.noFill();
        }
        r.rect(toggleSize, r.height - toggleSize, toggleSize, toggleSize); // Draw the toggle

        if (showUI) {
            /**** Sliders ****/
            overSlider = false;
            for (var i = 0; i < sliders.length; i++) {
                sliders[i].update();
                if (sliders[i].mouseOver) {
                    overSlider = true; // true if over ANY sliders
                }
            }
            /**** Rotator ****/
            r.strokeWeight(2);
            r.stroke(255);
            r.noFill();
            var rotatorSize = r.height / 2;

            if (r.mouseIsPressed && !overToggle & !overSlider) { // If not over the toggle or sliders, then activate the rotator!
                var endVec = new p5.Vector(r.mouseX, r.mouseY);
                var centralVector = new p5.Vector(r.width / 2, r.height / 2);
                var pointVector = endVec.copy();
                pointVector.sub(centralVector); // Points from the center of the screen to the mouse.
                pointVector.normalize();
                pointVector.mult(rotatorSize / 2); // Normalize and scale so the line lands on the circle

                r.push();
                var angle = r.degrees(p5.Vector.angleBetween(new p5.Vector(-1, 0), pointVector)); // Get angle between left vector and mouse
                var angleAdjust;
                var finalAngle;
                if (r.mouseY > r.height / 2) { // .angleBetween only return acute angles, so convert them here when necessary
                    angleAdjust = 180 - angle;
                    angle = 180 + angleAdjust;
                }
                rotationAngle = angle; // Rotate the lines accordingly
                r.translate(r.width / 2, r.height / 2); // Move to the middle to draw the pointVector line
                r.line(0, 0, pointVector.x, pointVector.y);
                r.fill(255, 40);
                r.pop();
            }

            r.push(); // Using push and pop here to simplify rotations
            r.translate(r.width / 2, r.height / 2); // Jump to the center to draw
            r.arc(0, 0, rotatorSize / 2, rotatorSize / 2, r.PI, r.radians(rotationAngle) + r.PI); // Draw an arc between the left vector and the pointer vector
            r.noFill();
            r.line(-rotatorSize / 2, 0, 0, 0); // Draw the ellipses and line
            r.ellipse(0, 0, rotatorSize, rotatorSize);
            r.ellipse(-rotatorSize / 2, 0, 20, 20);
            r.pop();
        }

        /**** Text ****/
        r.noStroke();
        r.textSize(60);
        r.textAlign(r.CENTER, r.CENTER);
        if (!showUI) {
            r.fill(255, 180);
        } else {
            r.fill(255, 20); // Fade out when showing UI
        }
        r.text("YOUR TEXT HERE", r.width / 2, r.height / 2); // Put whatever you want here!
    }

    /******** CLASSES ********/
    function LineRay() {
        this.lineWidth;
        this.lineSpeed;
        this.currentLerpPoint;

        this.currentLineAlpha; // Used to fade out the line
        this.lineColor;

        this.origin;
        this.destination;
        this.currentPosition;

        this.reset; // Should the line be reset?
        // Called to reset the line once it's offscreen.
        this.init = function () {
            this.reset = false;

            this.lineWidth = r.random(2, 20);
            this.lineSpeed = r.random(0.01, 0.02);

            this.currentLerpPoint = 0.0;
            this.currentLineAlpha = 80;

            this.lineColor = r.color( // Set the color based on color slider values
                r.random(100, 255) * sliders[1].sliderValue,
                r.random(100, 255) * sliders[2].sliderValue,
                r.random(100, 255) * sliders[3].sliderValue,
                this.currentLineAlpha);

            this.origin = new p5.Vector(-r.width, r.random(-r.width / 2, r.width / 2)); // Pick a random spot on the left
            this.destination = new p5.Vector(r.width, this.origin.y); // The destination is the same y-position, but on the right side of the canvas
        }

        this.update = function () {
            if (this.reset) {
                this.init();
            }
            this.currentPosition = p5.Vector.lerp(this.origin, this.destination, this.currentLerpPoint); // lerp between the origin and the destination 
            this.currentLerpPoint += this.lineSpeed * sliders[0].sliderValue; // increment lerp speed and multiply by slider value

            if (this.currentPosition.x > r.width) { // If off screen, then start fading
                this.currentLineAlpha -= 2;
                if (this.currentLineAlpha < 0) { // If completely faded, reset the line
                    this.currentLineAlpha = 0;
                    this.reset = true;
                }
            }
            r.strokeWeight(this.lineWidth);
            this.lineColor = r.color(r.red(this.lineColor), r.green(this.lineColor), r.blue(this.lineColor), this.currentLineAlpha);
            r.stroke(this.lineColor);
            r.strokeCap(r.SQUARE);

            r.line(this.origin.x, this.origin.y, this.currentPosition.x, this.currentPosition.y);
        }
    }

    function Slider(_x, _y, _width, _height, _fillColor) { // Just a UI class - nothing too interesting here.
        this.x = _x;
        this.y = _y;
        this.sWidth = _width;
        this.sHeight = _height;
        this.fillColor = _fillColor;

        this.mouseOver = false;
        this.grabbed = false;
        this.sliderValue = 0.5; // Ranges from 0 to 1

        this.LR = true; // Slider orientation
        // If wider than it is tall, it's a left-right slider, otherwise it's up-down
        if (this.sWidth > this.sHeight) {
            this.LR = true;
        } else {
            this.LR = false;
        }

        this.update = function () {
            r.rectMode(r.CENTER);

            if ( // Check for mouse over
                r.mouseX > this.x - this.sWidth / 2 &&
                r.mouseX < this.x + this.sWidth / 2 &&
                r.mouseY > this.y - this.sHeight / 2 &&
                r.mouseY < this.y + this.sHeight / 2) {
                this.mouseOver = true;
            } else if (!this.grabbed) {
                this.mouseOver = false;
            }
            // Slider Fill
            r.fill(this.fillColor);
            r.noStroke();
            if (this.LR) {
                r.rect(this.x, this.y, this.sWidth * this.sliderValue, this.sHeight); // Fill left to right
            } else {
                r.rect(this.x, this.y, this.sWidth, this.sHeight * this.sliderValue); // Fill bottom to top
            }

            if (this.mouseOver) { // If the mouse is over, highlight
                r.strokeWeight(2);
                if (this.grabbed) {
                    if (this.LR) { // Find distance from left or bottom as appropriate
                        this.distance = r.mouseX - (this.x - this.sWidth / 2);
                        this.sliderValue = this.distance / this.sWidth;

                    } else {
                        this.distance = (this.y + this.sHeight / 2) - r.mouseY;
                        this.sliderValue = this.distance / this.sHeight;
                    }
                    if (this.sliderValue < 0) this.sliderValue = 0.0;
                    if (this.sliderValue > 1) this.sliderValue = 1.0;
                }
            } else {
                r.strokeWeight(1);
            }
            r.stroke(255);
            r.noFill();
            r.rect(this.x, this.y, this.sWidth, this.sHeight); // Slider Border    
        }
    }

    function mousePressed() { // Toggle the UI when the toggle is clicked.
    }

    function mouseReleased() {}

    function uiCheck() {
        if (overToggle) {
            showUI = !showUI;
        }
        for (var i = 0; i < sliders.length; i++) {
            if (sliders[i].mouseOver) {
                sliders[i].grabbed = true; // Used to "latch on" to sliders so you can still affect them when not over them
            }
        }
    }

    function releaseCheck() { // If the mouse is released, then none of the sliders are grabbed.

        for (var i = 0; i < sliders.length; i++) {
            sliders[i].grabbed = false;
        }
    }
}

var p5_r = new p5(raycaster, "raycaster");

Nothing too complicated going on with this one (not any actual raycasting). An array of lines is drawn from one end of the canvas to the other. The UI is actually a lot more code than the visuals. That being said, don't forget to click the little box on the lower left of the banner to show the UI. Controls include: rotation, speed, red, green, and blue

Zerthis

Procedural Animation & Simple AI with p5.js

(Click to feed)

SourceCode

/*
Zerthis
Procedural "snake" animation with simple AI.
Theodore Michels 2015
Last updated: 07/11/2015
*/

// Put this in a function instance so that everything is out of the global namespace
var zerthis = function (z) {
    /******** APPEARANCE ********/
    var numSegments = 30;
    var segmentDistance = 10.0;
    var bodyLength = numSegments * (segmentDistance - 1);

    /******** MOVEMENT ********/
    var onTheMove = false;
    // The amplitude of the sinusoidal motion - varies with distance from the target.
    var motionAmplitude = 0;
    // The maximum amplitude of the sinusoidal motion.
    var ampMax = bodyLength / 6;

    // forwardVector always points from the sinusoidal motion's 0 crossing to the destination
    // (It oscillates around this vector)
    var forwardVector = new p5.Vector();
    var acceleration = new p5.Vector();
    var accelCounter = new p5.Vector();
    var topSpeed = 3.0;
    var movementCounter = 0.0;
    var sineCounter = 0.0;
    // A vector perpendicular to the "head" - used for sinusoidal motion
    var sineVector = new p5.Vector();

    // Positions of each body segment
    var positions = [];
    // Where each segment would like to go
    var destinations = [];
    // Where is the snake (head) trying to go?
    var destination = new p5.Vector();

    /******** OTHER ********/
    var food = [];
    var closestFood;
    var foodsEaten = [];

    var numFloaters = 100;
    var floaters = [];

    var eyePulseCounter = 0.0;
    // How quickly does the creature drift downward?
    var creatureGravity = new p5.Vector(0.0, 0.5);

    var canvas
    z.setup = function () {
        canvas = z.createCanvas(z.windowWidth, 480);
        canvas.mousePressed(addFood); // Add mousePressed listener
        z.smooth();

        // Initialize segments from top to bottom in the middle of the screen
        for (var i = 0; i < numSegments; i++) {
            positions[i] = new p5.Vector(z.width / 2, segmentDistance * i);
        }
        for (var i = 0; i < numFloaters; i++) {
            floaters[i] = new Floater(z.random(z.width), z.random(z.height));
        }
    }

    z.draw = function () {
        z.background(255);
        z.stroke(0, 20);
        z.line(0, 0, z.width, 0);
        z.line(0, z.height, z.width, z.height);
        /******** AUTO MOVEMENT ********/
        // Get the last position element in the array (the head) for simplicity - it's used a lot elsewhere...
        var headVector = positions[positions.length - 1].copy();

        // If the head is at the bottom of the screen (and the creature isn't already moving)
        // then move towards a random spot at the top of the screen.
        if (headVector.y > z.height - 40 && !onTheMove) {
            forwardVector = headVector.copy();
            movementCounter = 0.0;
            sineCounter = 0.0;
            onTheMove = true;
            // Pick a random destination at the top of the screen.
            destination = new p5.Vector(z.random(0, z.width), z.random(0, z.height / 4));
        }

        /******** FOOD FINDING ********/
        if (food.length > 0) { // If there is any food, then go get it!
            onTheMove = true;
            closestFood = food[0];

            if (food.length > 1) { // If there's more than 1 food, then find the closest one.
                for (var i = 0; i < food.length; i++) {
                    if (headVector.dist(food[i].position) < headVector.dist(closestFood.position)) {
                        closestFood = food[i];
                    }
                }
            }

            destination = closestFood.position.copy(); // The destination is now the closest piece of food.
            // If close enough to the food, then "eat" it.
            if (headVector.dist(destination) < 2.0) {
                foodsEaten.push(closestFood);
                food.splice(food.indexOf(closestFood), 1);
            }
        }

        /******** MOVEMENT ********/
        if (!onTheMove) {
            forwardVector = headVector; // If not moving, constantly set the forwardVector to the head's position
        }
        if (onTheMove) {
            // The movement counter affects the amplitude of sinusoidal motion (it's clamped from 0 to 1)
            movementCounter += 0.002; // Effectively, sinusoidal acceleration...
            if (movementCounter > 1) {
                movementCounter = 1;
            }
            // Increment other counters while moving.
            sineCounter += 0.1;
            accelCounter += 0.07;
            eyePulseCounter += 0.2;

            // If the current destination has already been reached (by checking distance), reset the counters and movement status.
            if (headVector.dist(destination) < 2.0) {
                sineCounter = 0.0;
                accelCounter = 0.0;
                movementCounter = 0.0;
                onTheMove = false;
            } else { // Otherwise, move towards the destination
                forwardVelocity = destination.copy(); // Copy the destination vector
                forwardVelocity.sub(forwardVector); // Subtract the head position from it to get a vector pointing from the head to the target.

                sineVector = new p5.Vector(-forwardVelocity.y, forwardVelocity.x); // Perpendicular to forwardVelocity

                forwardVelocity.normalize();

                if (accelCounter < topSpeed) {
                    forwardVelocity.mult(accelCounter);
                } else {
                    forwardVelocity.mult(topSpeed);
                }
                forwardVector.add(forwardVelocity); // Add to move the forward vector towards the target
                // How much should the creature oscillate?
                // First of all, it should oscillate less as it approaches its destination.
                // It also takes time to get up to speed (via movementCounter)
                motionAmplitude = forwardVector.dist(destination) * movementCounter;

                if (motionAmplitude > ampMax) {
                    motionAmplitude = ampMax; // Clamp to the maximum.
                }

                sineVector.normalize();
                sineVector.mult(z.sin(sineCounter) * motionAmplitude); // Multiply by the amplitude and add to the forward vector.
                sineVector.add(forwardVector);
                // Set the last position in the array (the head) to the sineVector (all others will follow)
                positions[numSegments - 1].set(sineVector);
            }
        }
        // Each segment's destination is the NEXT segment's current POSITION.
        for (var i = 0; i < numSegments - 1; i++) {
            destinations[i] = positions[i + 1];
        }

        /******** DISPLAY ********/
        // Contains the positions of each body segment's perpendicular "vertices"
        var sideVectorsR = [];
        var sideVectorsL = [];
        // Look through each body position...
        for (var i = 0; i < positions.length; i++) {
            positions[i].add(creatureGravity); // Add the external "force"
            var ellipseSize = 0;

            if (i != positions.length - 1) { // For all other positions (besides the head)
                z.stroke(20, 220, 120, 100); // Draw a central line
                z.line(positions[i].x, positions[i].y, positions[i + 1].x, positions[i + 1].y);

                /******** SEGMENT FOLLOWING ********/
                var segmentMove = destinations[i].copy();
                segmentMove.sub(positions[i]); // Vector pointing from this segment to it's destination

                // This feels hackey...basically, while the segment is too far from its destination...
                // Move towards it's destination.
                while (segmentMove.mag() > segmentDistance) {
                    segmentMove.normalize();
                    segmentMove.mult(0.01);
                    positions[i].add(segmentMove);
                    segmentMove = destinations[i].copy();
                    segmentMove.sub(positions[i]);
                }
                // Segments are larger near the head
                ellipseSize = i / 2;

                /******** SIDE VECTORS ********/
                var nextVec = positions[i + 1].copy(); // Get the next position in the array
                nextVec.sub(positions[i]); // Get a vector pointing to it.

                var sideVector = new p5.Vector(-nextVec.y, nextVec.x); // Then get its perpendicular vector
                sideVector.normalize(); // Resize...
                sideVector.mult(ellipseSize);

                var sideVectorOpp = sideVector.copy();
                sideVectorOpp.mult(-1); // Then get the vector on the opposite side
                // Add to the arrays.
                sideVector.add(positions[i]);
                sideVectorOpp.add(positions[i]);

                sideVectorsR[i] = sideVector.copy();
                sideVectorsL[i] = sideVectorOpp.copy();

                z.stroke(0, 100);
                z.line(sideVector.x, sideVector.y, sideVectorOpp.x, sideVectorOpp.y); // A line connecting the two sides

                /******** FOOD COLORING ********/
                // Change color of body segments based on food eaten.
                // Because the head is actually the last element in the array, this part ends up being a little hackey...				
                if (numSegments - 1 - foodsEaten.length <= i) { // -1 for the head...
                    if (i + 1 >= numSegments - foodsEaten.length) {
                        // Fills in colors in reverse order, like beads onto a string.
                        z.fill(foodsEaten[foodsEaten.length - (numSegments - 1 - i)].foodColor);
                    }
                } else {
                    z.noFill(); // otherwise, the segment is empty
                }
                z.ellipse(positions[i].x, positions[i].y, ellipseSize, ellipseSize); // Draw ellipses for each body segment.
            }
        }
        if (onTheMove) { // If moving, pulse the eye
            z.fill(20, 220, 120, 128 * z.sin(eyePulseCounter));
        } else {
            z.noFill();
        }
        z.ellipse(headVector.x, headVector.y, 10, 10); // The eye

        /******** SIDES ********/
        for (var i = 0; i < sideVectorsR.length; i++) {
            if (i != sideVectorsR.length - 1) {
                z.line(sideVectorsR[i].x, sideVectorsR[i].y, sideVectorsR[i + 1].x, sideVectorsR[i + 1].y);
                z.line(sideVectorsL[i].x, sideVectorsL[i].y, sideVectorsL[i + 1].x, sideVectorsL[i + 1].y);
            }
        }

        /******** HEAD ********/
        var noseVector = positions[numSegments - 1].copy(); // Find a point just ahead of the...head (the nose)
        noseVector.sub(positions[numSegments - 2]);
        noseVector.normalize();
        noseVector.mult(40);
        noseVector.add(positions[numSegments - 1]);
        // Line through middle of nose
        z.line(positions[numSegments - 1].x, positions[numSegments - 1].y, noseVector.x, noseVector.y);
        // Sides of the head
        z.line(sideVectorsR[sideVectorsR.length - 1].x, sideVectorsR[sideVectorsR.length - 1].y, noseVector.x, noseVector.y);
        z.line(sideVectorsL[sideVectorsL.length - 1].x, sideVectorsL[sideVectorsL.length - 1].y, noseVector.x, noseVector.y);

        /******** FOOD & PARTICLES********/
        for (var i = 0; i < food.length; i++) { // Draw all of the food.
            food[i].update();
            food[i].display();
        }

        for (var i = 0; i < floaters.length; i++) {
            floaters[i].update();
            floaters[i].display();
            // If the floater dies or is offscreen, reset it.
            if (floaters[i].currentLifeTime < 0 || floaters[i].position.y > z.height) {
                floaters[i] = new Floater(z.random(z.width), z.random(-5, -40));
            }
        }
    }

    /******** CLASSES ********/
    function Food(x, y) {

            this.position = new p5.Vector(x, y);
            this.size = z.random(8.0, 18.0);
            this.spinCounter = 0.0;
            this.spinSpeed = z.random(0.01, 0.1);

            this.gravity = new p5.Vector(0.0, z.random(0.1, 0.5));

            this.foodColor = z.color(z.random(0, 255), z.random(0, 255), z.random(0, 255), 128);

            this.update = function () {
                this.spinCounter += this.spinSpeed;
                this.position.add(this.gravity);
            }

            this.display = function () {
                z.noFill();
                z.push(); // Use push and pop to isolate the rotation.
                z.translate(this.position.x, this.position.y);
                z.rotate(this.spinCounter);
                z.line(-10, 0, 10, 0);
                z.line(0, 15, 0, -15);

                z.fill(this.foodColor);
                z.ellipse(0, 0, this.size, this.size);
                z.pop();
            }
        }
        // Floating particles (just for show)
    function Floater(x, y) {
        this.position = new p5.Vector(x, y);
        this.size = z.random(2, 5);
        this.gravity = new p5.Vector(0.0, z.random(0.1, 0.5)); // Random "gravities" help create depth.
        this.maxLifeTime = z.random(1000.0, 3000.0); // Used for fading out.
        this.currentLifeTime = this.maxLifeTime;

        this.update = function () {
            this.position.add(this.gravity);
            this.currentLifeTime -= 1;
        }
        this.display = function () {
            z.noStroke();
            z.fill(0, 65 * (this.currentLifeTime / this.maxLifeTime));
            z.ellipse(this.position.x, this.position.y, this.size, this.size);
        }
    }

    function mousePressed() {}

    function addFood() {
        food.push(new Food(z.mouseX, z.mouseY)); // Add food whenever the mouse is pressed.
    }
}

var p5_z = new p5(zerthis, "zerthis");

Overall I'm pretty happy with how this animation turned out - it's generally pretty organic. It's basically achieved by applying a sinusoidal oscillation around the forward vector (vector pointing from the head to the target). There are still some issues though - it could use a bit of deceleration on approach, and it could really use a steering algorithm to prevent it from jerking when it finds a new target. The implementation for segment following is a little hackey, but I actually like the "expanding/contracting" effect it gives to the animation.

Connect


Theodore Michels