Autotel

wavy generative animation

I made this one just to get the kick out of it. It's agent-based, and each agent changes in direction according to a set of frequency/amplitude pairs, akin to inverse FFT, only that frequencies are not restricted to bins. The trail of these "agents" is warped according, too, to sin/cos function to get a sort of 3d-wavy effect.

    // @ts-check
    const canvas = document.createElement('canvas');
    const container = document.getElementById('code-target')
    
    
    const listOfColors1 = [
        '#220055',
        '#280260',
        '#2e0365',
        '#34046a',
        '#3a056f',
        '#400674',
        '#460779',
        '#4c087e',
        '#520983',
        '#580a88',
        '#5e0b8d',
        '#640c92',
        '#6a0d97',
        '#700e9c',
        '#760fa1',
    ];
    const listOfColors2 = [
        '#550022',
        '#600228',
        '#65032e',
        '#6a0434',
        '#6f053a',
        '#740640',
        '#790746',
        '#7e084c',
        '#830952',
        '#880a58',
        '#8d0b5e',
        '#920c64',
        '#970d6a',
        '#9c0e70',
        '#a10f76',
    ];
    if (!container) {
        throw new Error('Container not found');
    }
    container.appendChild(canvas);
    const listOfThings = [];

    const makeThing = (mycolor) => {
        /** @type {Vector2} */
        const pos = [0, 0];
        /** @type {Vector2} */
        const speed = [0, 0];
        let index = listOfThings.length;
        /** @type {{jumped:boolean, v:Vector2}[]} */
        let pastPos = [];
        let phase = 0;
        let initialized = false;

        // representation of frequency and amplitude
        const mults = [
            [0.5, 1],

            // [0.25, 0.3],
            // [-0.5, -0.5],
            // [0.125, 1],
            
            [-0.0125, -1],
            [-0.01245, -1.1],


            [0.01, Math.random() * 0.001],

            [-2, 0.1],
            [2.2, -0.13],
            [-2.3, -0.15],
            [8, 0.12],
            [-12, -0.01],

        ]
        /** apply polar to cartesian into the array of frequency, amplitude  pairs*/
        const getRotatingSpeed = () => {
            let result = [0, 0];
            for (let i = 0; i < mults.length; i++) {
                const mult = mults[i];
                result[0] += Math.sin(phase * mult[0]) * mult[1];
                result[1] += Math.cos(phase * mult[0]) * mult[1];
            }
            return result;
        }
        /** 
         * @param {number} dt delta time
         * @param {CanvasRenderingContext2D} context
         */
        const frame = (dt, context) => {
            context.strokeStyle = mycolor
            context.lineWidth = 2;

            if (!initialized) {
                phase = (index / listOfThings.length) * Math.PI * 2;
                initialized = true;
            }
            phase += dt * 0.01
            const newSpeed = getRotatingSpeed();
            speed[0] = newSpeed[0] * 0.00025;
            speed[1] = newSpeed[1] * 0.00025;

            const { v: wrappedPos, jumped } = wrapCoords(pos);
            let drawPos = spaceToScreenCoords(wrappedPos);
            
            context.beginPath();
            const historyLength = 200;
            pastPos.forEach(({ v, jumped }, i) => {
                /** @type {Vector2} */
                const movedPoint = [
                    v[0] + Math.sin(phase * 0.03 - i / 30) * 0.01,
                    v[1] + Math.cos(phase * 0.07 - i / 30) * 0.02,
                    // ...v
                    // v[1]
                ]
                const drawPoint = spaceToScreenCoords(movedPoint);
                if (jumped) {
                    context.moveTo(...drawPoint);
                } else {
                    context.lineTo(...drawPoint);
                }
            });
            context.stroke();

            pos[0] = wrappedPos[0] + speed[0] * dt;
            pos[1] = wrappedPos[1] + speed[1] * dt;

            pastPos.push({ v: wrappedPos, jumped });
            if (pastPos.length > historyLength) {
                pastPos.shift();
            }
        }
        const newThing = {
            pos,
            speed,
            frame,
            set phase(value) {
                phase = value;
            },
            get phase() {
                return phase;
            }
        }
        listOfThings.push(newThing);
        return newThing;
    }

    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 - 
     */


    /** 
     * @type {DrawScope}
     */
    const drawScope = {
        context,
        width: 0,
        height: 0,
    }
    const view = {
        range: [1, 1],
        offset: [0.5, 0.5],
    }

    drawScope.width = canvas.width;
    drawScope.height = canvas.height;

    /**
     * @typedef { [number, number]} Vector2
     */

    /**
     * @param {Vector2} inputCords
     * @returns {{ jumped: boolean, v: Vector2 }}
     */
    const wrapCoords = (inputCords) => {
        const detransposedCoords = [
            inputCords[0] + view.offset[0],
            inputCords[1] + view.offset[1],
        ];
        while (detransposedCoords[0] < 0) {
            detransposedCoords[0] = 1 + detransposedCoords[0];
        }
        while (detransposedCoords[1] < 0) {
            detransposedCoords[1] = 1 + detransposedCoords[1];
        }

        /** @type {Vector2} */
        const v = [
            detransposedCoords[0] % 1,
            detransposedCoords[1] % 1,
        ];

        v[0] = v[0] - view.offset[0];
        v[1] = v[1] - view.offset[1];

        const res = {
            v,
            jumped: false,
        }
        const diff = [
            Math.abs(res.v[0] - inputCords[0]),
            Math.abs(res.v[1] - inputCords[1]),
        ];
        res.jumped = diff[0] > 0.5 || diff[1] > 0.5;
        return res;
    }
    /**
     * @param {Vector2} coords
     * @returns {Vector2}
     */
    const screenToSpaceCoords = ([x, y]) => {
        const viewTransformed = [
            x / view.range[0] - view.offset[0],
            y / view.range[1] - view.offset[1]
        ];
        return [
            viewTransformed[0] / drawScope.width,
            viewTransformed[1] / drawScope.height
        ]
    }
    /**
     * @param {Vector2} coords
     * @returns {Vector2}
     */
    const spaceToScreenCoords = ([x, y]) => {
        const viewTransformed = [
            (x + view.offset[0]) * view.range[0],
            (y + view.offset[1]) * view.range[1]
        ];
        return [
            viewTransformed[0] * drawScope.width,
            viewTransformed[1] * drawScope.height
        ]
    }


    let lastTime = new Date().getTime();

    const mouseHistory = [];
    const mouseHistoryLength = 255;

    /**
     * @param {Vector2} point
     */
    const addMouseHistoryPoint = (point) => {
        mouseHistory.push(point);
        if (mouseHistory.length > mouseHistoryLength) {
            mouseHistory.shift();
        }
    }

    listOfColors1.forEach((color, a) => {
        const thing = makeThing(listOfColors1[a % listOfColors1.length]);
    })
    listOfColors2.forEach((color, a) => {
        const thing = makeThing(listOfColors2[a % listOfColors2.length]);
        thing.pos[0] = 0.25
        thing.pos[1] = 0.25
        thing.phase += 123.1298719;
    })
    /**
     * @param {number} time
     * @param {DrawScope} drawScope
     */
    let draw = (time, {
        context,
        width,
        height
    }) => {
        const deltaTime = time - lastTime;
        lastTime = time;
        context.strokeStyle = 'white';
        context.fillStyle = 'white';

        context.clearRect(0, 0, width, height);

        listOfThings.forEach((thing, i) => {
            thing.frame(deltaTime, context);

        });

    }

    const windowResizedListener = () => {
        const containerRect = container.getBoundingClientRect();
        canvas.width = containerRect.width;
        canvas.height = containerRect.width;
        drawScope.width = canvas.width;
        drawScope.height = canvas.height;
    }

    window.addEventListener('resize', windowResizedListener);

    const frame = () => {
        const time = new Date().getTime();
        draw(time, drawScope);
        requestAnimationFrame(frame);
    }



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

    frame();