Greasy Fork is available in English.

Interactive Reptile Cursor

Interactive creature animation that follows mouse

// ==UserScript==
// @name         Interactive Reptile Cursor
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Interactive creature animation that follows mouse
// @author       You
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Wait for page to load
    window.addEventListener('load', function() {
        // Create and inject styles
        const style = document.createElement('style');
        style.textContent = `
            #creatureCanvas {
                position: fixed !important;
                top: 0 !important;
                left: 0 !important;
                width: 100vw !important;
                height: 100vh !important;
                z-index: 999999 !important;
                pointer-events: none !important;
                background-color: transparent !important;
            }
        `;
        document.head.appendChild(style);

        // Input handling
        var Input = {
            keys: [],
            mouse: {
                left: false,
                right: false,
                middle: false,
                x: 0,
                y: 0
            }
        };

        for (var i = 0; i < 230; i++) {
            Input.keys.push(false);
        }

        document.addEventListener("keydown", function(event) {
            Input.keys[event.keyCode] = true;
        });

        document.addEventListener("keyup", function(event) {
            Input.keys[event.keyCode] = false;
        });

        document.addEventListener("mousedown", function(event) {
            if (event.button === 0) {
                Input.mouse.left = true;
            }
            if (event.button === 1) {
                Input.mouse.middle = true;
            }
            if (event.button === 2) {
                Input.mouse.right = true;
            }
        });

        document.addEventListener("mouseup", function(event) {
            if (event.button === 0) {
                Input.mouse.left = false;
            }
            if (event.button === 1) {
                Input.mouse.middle = false;
            }
            if (event.button === 2) {
                Input.mouse.right = false;
            }
        });

        document.addEventListener("mousemove", function(event) {
            Input.mouse.x = event.clientX;
            Input.mouse.y = event.clientY;
        });

        // Create canvas
        var canvas = document.createElement("canvas");
        canvas.id = "creatureCanvas";
        document.body.appendChild(canvas);

        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;

        // Handle window resize
        window.addEventListener('resize', function() {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
        });

        var ctx = canvas.getContext("2d");

        // Necessary classes
        var segmentCount = 0;

        class Segment {
            constructor(parent, size, angle, range, stiffness) {
                segmentCount++;
                this.isSegment = true;
                this.parent = parent;
                if (typeof parent.children == "object") {
                    parent.children.push(this);
                }
                this.children = [];
                this.size = size;
                this.relAngle = angle;
                this.defAngle = angle;
                this.absAngle = parent.absAngle + angle;
                this.range = range;
                this.stiffness = stiffness;
                this.updateRelative(false, true);
            }

            updateRelative(iter, flex) {
                this.relAngle = this.relAngle - 2 * Math.PI * Math.floor((this.relAngle - this.defAngle) / 2 / Math.PI + 1 / 2);
                if (flex) {
                    this.relAngle = Math.min(
                        this.defAngle + this.range / 2,
                        Math.max(
                            this.defAngle - this.range / 2,
                            (this.relAngle - this.defAngle) / this.stiffness + this.defAngle
                        )
                    );
                }
                this.absAngle = this.parent.absAngle + this.relAngle;
                this.x = this.parent.x + Math.cos(this.absAngle) * this.size;
                this.y = this.parent.y + Math.sin(this.absAngle) * this.size;
                if (iter) {
                    for (var i = 0; i < this.children.length; i++) {
                        this.children[i].updateRelative(iter, flex);
                    }
                }
            }

            draw(iter) {
                ctx.beginPath();
                ctx.moveTo(this.parent.x, this.parent.y);
                ctx.lineTo(this.x, this.y);
                ctx.stroke();
                if (iter) {
                    for (var i = 0; i < this.children.length; i++) {
                        this.children[i].draw(true);
                    }
                }
            }

            follow(iter) {
                var x = this.parent.x;
                var y = this.parent.y;
                var dist = Math.sqrt((this.x - x) ** 2 + (this.y - y) ** 2);
                this.x = x + this.size * (this.x - x) / dist;
                this.y = y + this.size * (this.y - y) / dist;
                this.absAngle = Math.atan2(this.y - y, this.x - x);
                this.relAngle = this.absAngle - this.parent.absAngle;
                this.updateRelative(false, true);
                if (iter) {
                    for (var i = 0; i < this.children.length; i++) {
                        this.children[i].follow(true);
                    }
                }
            }
        }

        class LimbSystem {
            constructor(end, length, speed, creature) {
                this.end = end;
                this.length = Math.max(1, length);
                this.creature = creature;
                this.speed = speed;
                creature.systems.push(this);
                this.nodes = [];
                var node = end;
                for (var i = 0; i < length; i++) {
                    this.nodes.unshift(node);
                    node = node.parent;
                    if (!node.isSegment) {
                        this.length = i + 1;
                        break;
                    }
                }
                this.hip = this.nodes[0].parent;
            }

            moveTo(x, y) {
                this.nodes[0].updateRelative(true, true);
                var dist = Math.sqrt((x - this.end.x) ** 2 + (y - this.end.y) ** 2);
                var len = Math.max(0, dist - this.speed);
                for (var i = this.nodes.length - 1; i >= 0; i--) {
                    var node = this.nodes[i];
                    var ang = Math.atan2(node.y - y, node.x - x);
                    node.x = x + len * Math.cos(ang);
                    node.y = y + len * Math.sin(ang);
                    x = node.x;
                    y = node.y;
                    len = node.size;
                }
                for (var i = 0; i < this.nodes.length; i++) {
                    var node = this.nodes[i];
                    node.absAngle = Math.atan2(node.y - node.parent.y, node.x - node.parent.x);
                    node.relAngle = node.absAngle - node.parent.absAngle;
                    for (var ii = 0; ii < node.children.length; ii++) {
                        var childNode = node.children[ii];
                        if (!this.nodes.includes(childNode)) {
                            childNode.updateRelative(true, false);
                        }
                    }
                }
            }

            update() {
                this.moveTo(Input.mouse.x, Input.mouse.y);
            }
        }

        class LegSystem extends LimbSystem {
            constructor(end, length, speed, creature) {
                super(end, length, speed, creature);
                this.goalX = end.x;
                this.goalY = end.y;
                this.step = 0;
                this.forwardness = 0;
                this.reach = 0.9 * Math.sqrt((this.end.x - this.hip.x) ** 2 + (this.end.y - this.hip.y) ** 2);
                var relAngle = this.creature.absAngle - Math.atan2(this.end.y - this.hip.y, this.end.x - this.hip.x);
                relAngle -= 2 * Math.PI * Math.floor(relAngle / 2 / Math.PI + 1 / 2);
                this.swing = -relAngle + (2 * (relAngle < 0) - 1) * Math.PI / 2;
                this.swingOffset = this.creature.absAngle - this.hip.absAngle;
            }

            update(x, y) {
                this.moveTo(this.goalX, this.goalY);
                if (this.step == 0) {
                    var dist = Math.sqrt((this.end.x - this.goalX) ** 2 + (this.end.y - this.goalY) ** 2);
                    if (dist > 1) {
                        this.step = 1;
                        this.goalX = this.hip.x + this.reach * Math.cos(this.swing + this.hip.absAngle + this.swingOffset) + (2 * Math.random() - 1) * this.reach / 2;
                        this.goalY = this.hip.y + this.reach * Math.sin(this.swing + this.hip.absAngle + this.swingOffset) + (2 * Math.random() - 1) * this.reach / 2;
                    }
                } else if (this.step == 1) {
                    var theta = Math.atan2(this.end.y - this.hip.y, this.end.x - this.hip.x) - this.hip.absAngle;
                    var dist = Math.sqrt((this.end.x - this.hip.x) ** 2 + (this.end.y - this.hip.y) ** 2);
                    var forwardness2 = dist * Math.cos(theta);
                    var dF = this.forwardness - forwardness2;
                    this.forwardness = forwardness2;
                    if (dF * dF < 1) {
                        this.step = 0;
                        this.goalX = this.hip.x + (this.end.x - this.hip.x);
                        this.goalY = this.hip.y + (this.end.y - this.hip.y);
                    }
                }
            }
        }

        class Creature {
            constructor(x, y, angle, fAccel, fFric, fRes, fThresh, rAccel, rFric, rRes, rThresh) {
                this.x = x;
                this.y = y;
                this.absAngle = angle;
                this.fSpeed = 0;
                this.fAccel = fAccel;
                this.fFric = fFric;
                this.fRes = fRes;
                this.fThresh = fThresh;
                this.rSpeed = 0;
                this.rAccel = rAccel;
                this.rFric = rFric;
                this.rRes = rRes;
                this.rThresh = rThresh;
                this.children = [];
                this.systems = [];
            }

            follow(x, y) {
                var dist = Math.sqrt((this.x - x) ** 2 + (this.y - y) ** 2);
                var angle = Math.atan2(y - this.y, x - this.x);
                var accel = this.fAccel;
                if (this.systems.length > 0) {
                    var sum = 0;
                    for (var i = 0; i < this.systems.length; i++) {
                        sum += this.systems[i].step == 0;
                    }
                    accel *= sum / this.systems.length;
                }
                this.fSpeed += accel * (dist > this.fThresh);
                this.fSpeed *= 1 - this.fRes;
                this.speed = Math.max(0, this.fSpeed - this.fFric);
                var dif = this.absAngle - angle;
                dif -= 2 * Math.PI * Math.floor(dif / (2 * Math.PI) + 1 / 2);
                if (Math.abs(dif) > this.rThresh && dist > this.fThresh) {
                    this.rSpeed -= this.rAccel * (2 * (dif > 0) - 1);
                }
                this.rSpeed *= 1 - this.rRes;
                if (Math.abs(this.rSpeed) > this.rFric) {
                    this.rSpeed -= this.rFric * (2 * (this.rSpeed > 0) - 1);
                } else {
                    this.rSpeed = 0;
                }
                this.absAngle += this.rSpeed;
                this.absAngle -= 2 * Math.PI * Math.floor(this.absAngle / (2 * Math.PI) + 1 / 2);
                this.x += this.speed * Math.cos(this.absAngle);
                this.y += this.speed * Math.sin(this.absAngle);
                this.absAngle += Math.PI;
                for (var i = 0; i < this.children.length; i++) {
                    this.children[i].follow(true, true);
                }
                for (var i = 0; i < this.systems.length; i++) {
                    this.systems[i].update(x, y);
                }
                this.absAngle -= Math.PI;
                this.draw(true);
            }

            draw(iter) {
                var r = 4;
                ctx.beginPath();
                ctx.arc(this.x, this.y, r, Math.PI / 4 + this.absAngle, 7 * Math.PI / 4 + this.absAngle);
                ctx.moveTo(this.x + r * Math.cos(7 * Math.PI / 4 + this.absAngle), this.y + r * Math.sin(7 * Math.PI / 4 + this.absAngle));
                ctx.lineTo(this.x + r * Math.cos(this.absAngle) * Math.sqrt(2), this.y + r * Math.sin(this.absAngle) * Math.sqrt(2));
                ctx.lineTo(this.x + r * Math.cos(Math.PI / 4 + this.absAngle), this.y + r * Math.sin(Math.PI / 4 + this.absAngle));
                ctx.stroke();
                if (iter) {
                    for (var i = 0; i < this.children.length; i++) {
                        this.children[i].draw(true);
                    }
                }
            }
        }

        function setupLizard(size, legs, tail) {
            var s = size;
            var critter = new Creature(canvas.width / 2, canvas.height / 2, 0, s * 10, s * 2, 0.5, 16, 0.5, 0.085, 0.5, 0.3);
            var spinal = critter;

            // Neck
            for (var i = 0; i < 6; i++) {
                spinal = new Segment(spinal, s * 4, 0, 3.1415 * 2 / 3, 1.1);
                for (var ii = -1; ii <= 1; ii += 2) {
                    var node = new Segment(spinal, s * 3, ii, 0.1, 2);
                    for (var iii = 0; iii < 3; iii++) {
                        node = new Segment(node, s * 0.1, -ii * 0.1, 0.1, 2);
                    }
                }
            }

            // Torso and legs
            for (var i = 0; i < legs; i++) {
                if (i > 0) {
                    for (var ii = 0; ii < 6; ii++) {
                        spinal = new Segment(spinal, s * 4, 0, 1.571, 1.5);
                        for (var iii = -1; iii <= 1; iii += 2) {
                            var node = new Segment(spinal, s * 3, iii * 1.571, 0.1, 1.5);
                            for (var iv = 0; iv < 3; iv++) {
                                node = new Segment(node, s * 3, -iii * 0.3, 0.1, 2);
                            }
                        }
                    }
                }

                for (var ii = -1; ii <= 1; ii += 2) {
                    var node = new Segment(spinal, s * 12, ii * 0.785, 0, 8);
                    node = new Segment(node, s * 16, -ii * 0.785, 6.28, 1);
                    node = new Segment(node, s * 16, ii * 1.571, 3.1415, 2);
                    for (var iii = 0; iii < 4; iii++) {
                        new Segment(node, s * 4, (iii / 3 - 0.5) * 1.571, 0.1, 4);
                    }
                    new LegSystem(node, 3, s * 12, critter);
                }
            }

            // Tail
            for (var i = 0; i < tail; i++) {
                spinal = new Segment(spinal, s * 4, 0, 3.1415 * 2 / 3, 1.1);
                for (var ii = -1; ii <= 1; ii += 2) {
                    var node = new Segment(spinal, s * 3, ii, 0.1, 2);
                    for (var iii = 0; iii < 3; iii++) {
                        node = new Segment(node, s * 3 * (tail - i) / tail, -ii * 0.1, 0.1, 2);
                    }
                }
            }

            // Animation loop
            function animate() {
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                ctx.strokeStyle = "white";
                critter.follow(Input.mouse.x, Input.mouse.y);
                requestAnimationFrame(animate);
            }

            animate();
        }

        // Start the animation with random parameters
        ctx.strokeStyle = "white";
        var legNum = Math.floor(1 + Math.random() * 12);
        setupLizard(8 / Math.sqrt(legNum), legNum, Math.floor(4 + Math.random() * legNum * 8));
    });
})();