Autotel

Spirograph experiment

I took inspiration in the action of drawing on a rotating wheel, and observing how the lines seemingly moved. The result was a bit boring, so I modified it a bit, so that the line segments would rotate at unequal speeds, resulting in an ever-deforming line.

Try clicking in the drawing area and dragging it around.


    // @ts-check
    /**
     * @typedef { [number, number]} Vector2
     */

    const canvas = document.createElement('canvas');
    const container = document.getElementById('code-target')
    const restartButton = document.createElement('button');
    if (!container) {
        throw new Error('Container not found');
    }
    container.appendChild(canvas);
    // container.appendChild(restartButton);
    restartButton.style.position = 'fixed';
    restartButton.style.bottom = '0';
    restartButton.style.right = '0';
    restartButton.style.zIndex = '1';
    restartButton.innerText = 'Restart';
    restartButton.onclick = () => {
        window.location.reload();
    }

    const context = canvas.getContext('2d');
    if (!context) {
        throw new Error('Canvas context not found');
    }

    /**
     * @typedef {Object} DrawScope
     * @property {CanvasRenderingContext2D} context - canvas context
     * @property {number} width - 
     * @property {number} height - 
     * @property {Vector2} middle -
     */


    /** 
     * @type {DrawScope}
     */
    const drawScope = {
        context,
        width: 0,
        height: 0,
        middle: [0, 0],
    }
    drawScope.width = canvas.width;
    drawScope.height = canvas.height;
    drawScope.middle = [drawScope.width / 2, drawScope.height / 2];


    let lastTime = new Date().getTime();
    /** @type {Vector2[]} */
    let currentTrace = [];
    /** @type {Vector2[][]} */
    const traces = [currentTrace];
    const tracesLength = 512;
    let traceCount = 0;

    /**
     * @param {Vector2} point
     */
    const addtracePoint = (point) => {
        currentTrace.push([...point]);
        traceCount += 1;
        while (traceCount > tracesLength) {
            if (traces[0].length === 0) {
                traces.shift();
                continue;
            }
            traces[0].shift();
            traceCount -= 1;
        }
    }
    const startNewTrace = () => {
        currentTrace = [];
        traces.push(currentTrace);
    }

    /**
     * @param {Vector2} coords
     * @param {number} angle
     * @returns {Vector2}
     */
    const rotateCoords = (coords, angle) => {
        const [x, y] = coords;
        const sin = Math.sin(angle);
        const cos = Math.cos(angle);
        return [
            x * cos - y * sin,
            x * sin + y * cos
        ];
    }
    const translateCoords = (coords, offset) => {
        const [x, y] = coords;
        const [dx, dy] = offset;
        return [x + dx, y + dy];
    }

    /**
     * @param {Vector2} coords
     * @param  {...Function} callbacks
     * @returns {Vector2}
     */
    const processCoords = (coords, ...callbacks) => {
        let res = coords;
        callbacks.forEach(callback => {
            res = callback(res);
        });
        return res;
    }

    /**
     * @param {number} time
     * @param {DrawScope} drawScope
     */
    let draw = (time, {
        context,
        width,
        height
    }) => {
        context.strokeStyle = 'white';
        context.fillStyle = 'white';
        context.lineWidth = 4;
        context.lineCap = 'round';
        const deltaTime = time - lastTime;
        lastTime = time;
        context.clearRect(0, 0, width, height);
        if (mouse.down) {
            addtracePoint(mouse.pos);
        }
        const rotationCenter = drawScope.middle;
        const negativeRotationCenter = rotationCenter.map((value) => -value);
        traces.forEach(trace => {
            trace.forEach((point, index) => {
                const [x, y] = processCoords(point, (point) => {
                    return translateCoords(point, negativeRotationCenter);
                }, (point) => {
                    return rotateCoords(point, (0.0001 + index / 200000) * deltaTime);
                }, (point) => {
                    return translateCoords(point, rotationCenter);
                });
                {
                    point[0] = x;
                    point[1] = y;
                }

                if (index === 0) {
                    context.beginPath();
                    context.moveTo(x, y);
                } else {
                    context.lineTo(x, y);
                }
            });
            context.stroke();
        });

    }
    
    const windowResizedListener = () => {
        const containerRect = container.getBoundingClientRect();
        canvas.width = containerRect.width;
        canvas.height = containerRect.width;
        drawScope.width = canvas.width;
        drawScope.height = canvas.width;
        drawScope.middle = [drawScope.width / 2, drawScope.height / 2];
    }

    window.addEventListener('resize', windowResizedListener);

    const frame = () => {
        const time = new Date().getTime();
        draw(time, drawScope);
        requestAnimationFrame(frame);
    }
    /**
     * @typedef {Object} Mouse
     * @property {Vector2} pos
     * @property {boolean} down
     */
    /** @type {Mouse} */
    const mouse = {
        pos: [0, 0],
        down: false,

    };
    canvas.addEventListener('mousemove', (event) => {
        mouse.pos = [event.offsetX, event.offsetY];
    });
    canvas.addEventListener('mousedown', () => {
        mouse.down = true;
        startNewTrace();
    });
    window.addEventListener('mouseup', () => {
        mouse.down = false;
    });

    window.addEventListener('load', () => {
        windowResizedListener();
    });

    frame();