← Mike Higgins Home
Custom Code Experiment •

Chromatophores 3

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html>
<head>
    <style>
        html, body {
            overflow: hidden;
            background: #000;
            padding: 0px;
            margin: 0px;
            width: 100%;
            height: 100%;
        }
        canvas {
            display: block;
        }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>
    <script>
        var canvas = document.getElementById("canvas");
        var ctx = canvas.getContext("2d");

        var w = canvas.width = window.innerWidth;
        var h = canvas.height = window.innerHeight;

        window.onresize = function() {
            w = canvas.width = window.innerWidth;
            h = canvas.height = window.innerHeight;
        };

        var pixels = [];
        var emitterCount = 8;
        var maxPixels = 280;
        var pixelSize = 18;
        var gap = 6;
        var lineWidth = 2;
        var maxLifetime = 4500;
        var minLifetime = 2000;
        var spawnRate = 250;
        var opacity = 0.28;
        var hue = 0;
        var hueRange = 60;
        var fadeAlpha = 0.018;
        var mouse = { x: -9999, y: -9999, down: false };
        var mouseAttractRadius = 160;
        var mouseRepelRadius = 80;
        var mouseForce = 3.5;

        canvas.addEventListener("mousemove", function(e) {
            mouse.x = e.clientX;
            mouse.y = e.clientY;
        });
        canvas.addEventListener("mousedown", function(e) {
            mouse.down = true;
            mouse.button = e.button;
            e.preventDefault();
        });
        canvas.addEventListener("mouseup", function() { mouse.down = false; });
        canvas.addEventListener("contextmenu", function(e) { e.preventDefault(); });
        canvas.addEventListener("mouseleave", function() { mouse.x = -9999; mouse.y = -9999; });
        canvas.addEventListener("touchmove", function(e) {
            mouse.x = e.touches[0].clientX;
            mouse.y = e.touches[0].clientY;
            e.preventDefault();
        }, { passive: false });
        canvas.addEventListener("touchstart", function(e) {
            mouse.down = true; mouse.button = 0;
            mouse.x = e.touches[0].clientX;
            mouse.y = e.touches[0].clientY;
        });
        canvas.addEventListener("touchend", function() { mouse.down = false; });

        var attractRadius = 180;
        var repelRadius = 35;
        var alignRadius = 100;
        var attractForce = 0.75;
        var repelForce = 2.2;
        var alignForce = 0.15;
        var maxSpeed = 4.0;
        var friction = 0.84;
        var shapes = ["rect", "arc", "triangle", "diamond", "cross", "star"];

        function drawShape(shape, x, y, size, pulse) {
            var s = size * pulse;
            ctx.beginPath();
            switch(shape) {
                case "rect":
                    ctx.rect(x - s/2, y - s/2, s, s);
                    break;
                case "arc":
                    ctx.arc(x, y, s / 2, 0, 2 * Math.PI);
                    break;
                case "triangle":
                    ctx.moveTo(x, y - s / 2);
                    ctx.lineTo(x + s / 2, y + s / 2);
                    ctx.lineTo(x - s / 2, y + s / 2);
                    ctx.closePath();
                    break;
                case "diamond":
                    ctx.moveTo(x, y - s / 2);
                    ctx.lineTo(x + s / 2, y);
                    ctx.lineTo(x, y + s / 2);
                    ctx.lineTo(x - s / 2, y);
                    ctx.closePath();
                    break;
                case "cross":
                    var t = s / 4;
                    ctx.rect(x - t/2, y - s/2, t, s);
                    ctx.rect(x - s/2, y - t/2, s, t);
                    break;
                case "star":
                    var spikes = 5;
                    var outerR = s / 2;
                    var innerR = s / 4.5;
                    var rot = (Math.PI / 2) * 3;
                    var step = Math.PI / spikes;
                    ctx.moveTo(x, y - outerR);
                    for (var k = 0; k < spikes; k++) {
                        ctx.lineTo(x + Math.cos(rot) * outerR, y + Math.sin(rot) * outerR);
                        rot += step;
                        ctx.lineTo(x + Math.cos(rot) * innerR, y + Math.sin(rot) * innerR);
                        rot += step;
                    }
                    ctx.lineTo(x, y - outerR);
                    ctx.closePath();
                    break;
            }
        }

        function emit() {
            for (var i = 0; i < emitterCount; i++) {
                if (pixels.length >= maxPixels) {
                    pixels.splice(0, 1);
                }
                pixels.push({
                    x: Math.random() * w,
                    y: Math.random() * h,
                    vx: (Math.random() - 0.5) * 2,
                    vy: (Math.random() - 0.5) * 2,
                    t: Math.random() * (maxLifetime - minLifetime) + minLifetime,
                    s: Date.now(),
                    h: Math.random() * ((hue + hueRange) - (hue - hueRange)) + (hue - hueRange),
                    shape: shapes[Math.floor(Math.random() * shapes.length)],
                    pulseOffset: Math.random() * Math.PI * 2,
                    pulseSpeed: 2 + Math.random() * 3
                });
            }
        }

        function draw() {
            var now = Date.now();
            for (var i = 0; i < pixels.length; i++) {
                var p = pixels[i];
                var diff = (now - p.s) / p.t;
                var perc = Math.round(diff * 100);
                var lifeAlpha = Math.sin(diff * Math.PI);
                var pulse = 1 + 0.4 * Math.sin(p.pulseOffset + (now / 1000) * p.pulseSpeed);

                ctx.save();
                ctx.globalAlpha = opacity * lifeAlpha;
                ctx.lineWidth = lineWidth;
                ctx.strokeStyle = "hsl(" + p.h + "," + perc + "%,55%)";
                ctx.fillStyle = "hsl(" + p.h + "," + perc + "%,55%)";

                drawShape(p.shape, p.x, p.y, pixelSize, pulse);

                ctx.stroke();
                ctx.restore();
            }
        }

        function animate() {
            var now = Date.now();
            for (var i = pixels.length - 1; i >= 0; i--) {
                var p = pixels[i];

                if (p.s + p.t < now) {
                    pixels.splice(i, 1);
                    continue;
                }

                var ax = 0, ay = 0;
                var sampleStep = Math.max(1, Math.floor(pixels.length / 60));
                var avgVx = 0, avgVy = 0, alignCount = 0;

                for (var j = 0; j < pixels.length; j += sampleStep) {
                    if (i === j) continue;
                    var q = pixels[j];
                    var dx = q.x - p.x;
                    var dy = q.y - p.y;
                    var dist = Math.sqrt(dx * dx + dy * dy) || 0.001;
                    var nx = dx / dist;
                    var ny = dy / dist;

                    if (dist < repelRadius) {
                        ax -= nx * repelForce * (1 - dist / repelRadius);
                        ay -= ny * repelForce * (1 - dist / repelRadius);
                    } else if (dist < attractRadius) {
                        ax += nx * attractForce * (1 - dist / attractRadius);
                        ay += ny * attractForce * (1 - dist / attractRadius);
                    }

                    if (dist < alignRadius) {
                        avgVx += q.vx;
                        avgVy += q.vy;
                        alignCount++;
                    }
                }

                if (alignCount > 0) {
                    ax += (avgVx / alignCount - p.vx) * alignForce;
                    ay += (avgVy / alignCount - p.vy) * alignForce;
                }

                var mdx = p.x - mouse.x;
                var mdy = p.y - mouse.y;
                var mdist = Math.sqrt(mdx * mdx + mdy * mdy) || 0.001;
                var mnx = mdx / mdist;
                var mny = mdy / mdist;

                if (mouse.down && mouse.button === 0 && mdist < mouseAttractRadius) {
                    ax -= mnx * mouseForce * (1 - mdist / mouseAttractRadius) * 2.5;
                    ay -= mny * mouseForce * (1 - mdist / mouseAttractRadius) * 2.5;
                } else if (mouse.down && mouse.button === 2 && mdist < mouseAttractRadius) {
                    ax += mnx * mouseForce * (1 - mdist / mouseAttractRadius) * 3.0;
                    ay += mny * mouseForce * (1 - mdist / mouseAttractRadius) * 3.0;
                } else if (!mouse.down && mdist < mouseRepelRadius) {
                    ax += mnx * mouseForce * (1 - mdist / mouseRepelRadius);
                    ay += mny * mouseForce * (1 - mdist / mouseRepelRadius);
                }

                ax += (Math.random() - 0.5) * 0.4;
                ay += (Math.random() - 0.5) * 0.4;
                p.vx = (p.vx + ax) * friction;
                p.vy = (p.vy + ay) * friction;
                var speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
                if (speed > maxSpeed) {
                    p.vx = (p.vx / speed) * maxSpeed;
                    p.vy = (p.vy / speed) * maxSpeed;
                }

                p.x += p.vx;
                p.y += p.vy;
                if (p.x < 0) p.x += w;
                if (p.x > w) p.x -= w;
                if (p.y < 0) p.y += h;
                if (p.y > h) p.y -= h;
            }
        }

        function render() {
            ctx.globalAlpha = fadeAlpha;
            ctx.fillStyle = "#000";
            ctx.fillRect(0, 0, w, h);
            ctx.globalAlpha = 1;

            draw();
            animate();
            requestAnimationFrame(render);
        }

        emit();
        setInterval(emit, spawnRate);
        requestAnimationFrame(render);
    </script>
</body>
</html>