Autotel

Mycellium 1

My partner, Dorothy Zablah with a close person of hers, are founding a consultancy company. They use take inspiration on mycellium to provide facilitation consultancy. They are designing their website and it came to my mind that it would be so satisfying to create an algorithm that would simulate their growth. I thought of it as few agents containing position and angle, which would grow towards their (ever-changing) angle; and upon a certain time, they would branch out by creating a new agent that inherits their properties.

Note that if you resize the window, the drawing will get erased.

Possible improvements:

  • Fading out of existing lines, and end time of agents, so that the animation could continue endlessly
  • Gradual thinkening of older branches (perhaps using feathering + sharpening?)
    const canvas = document.createElement('canvas');
    const container = document.getElementById('code-target')
    const restartButton = document.createElement('button');
    container.appendChild(canvas);
    container.appendChild(restartButton);

    restartButton.innerText = 'Restart';
    restartButton.onclick = () => {
        window.location.reload();
    }


    /**
     * @typedef {Object} DrawScope
     * @property {CanvasRenderingContext2D} context
     * @property {number} width
     * @property {number} height
     */

    let drawScope = {
        context: null,
        width: 0,
        height: 0
    }

    /**
     * @type Vector2
     * @property {number} x
     * @property {number} y
     */

    const wrapCoords = (inputCords) => {
        const coords = { ...inputCords };
        if (coords.x < 0) {
            coords.x = 1 + coords.x;
        }
        if (coords.y < 0) {
            coords.y = 1 + coords.y;
        }
        const res = {
            x: coords.x % 1,
            y: coords.y % 1,
            jumped: false,
        }

        res.jumped = res.x !== inputCords.x || res.y !== inputCords.y;
        return res;
    }

    const rootWalkers = [];

    /**
     * @param {RootWalker | undefined} parent
     * @returns {RootWalker}
     */
    const makeRootWalker = (parent) => {
        const pxSpeed = 0.1;
        const existingOne = rootWalkers.find(walker => walker.inUse === false);
        // angle in range 0-1, I dont know what to call that
        const plop = (parent?.plop || Math.random() * 360) + 90;
        const inUse = true;
        const x = parent?.x || 0.5;
        const y = parent?.y || 0;
        const age = 0;
        const jumped = false;

        if (existingOne) {
            Object.assign(existingOne, {
                inUse, plop, x, y, age, jumped
            });
            return existingOne;
        } else {
            return {
                inUse, plop, x, y, age, jumped,
                update(deltaTime) {
                    if (!this.inUse) return;

                    const deltaMicros = deltaTime * 0.001;

                    const dx = pxSpeed * Math.cos(this.plop * Math.PI * 2) * deltaMicros;
                    const dy = pxSpeed * Math.sin(this.plop * Math.PI * 2) * deltaMicros;

                    Object.assign(this, wrapCoords({
                        x: this.x + dx,
                        y: this.y + dy
                    }));

                    this.age += deltaMicros;
                    this.plop += (Math.random() * 0.1 - 0.05) * deltaMicros * 10

                    if (this.age > 2000 * (Math.random() + 1)) {
                        this.inUse = false;
                    }
                }
            }
        }
    }

    rootWalkers.push(
        makeRootWalker()
    );

    const normalizeCoords = ({ x, y }) => {
        return {
            x: x / drawScope.width,
            y: y / drawScope.height
        }
    }

    const projectCoords = ({ x, y }) => {
        return {
            x: x * drawScope.width,
            y: y * drawScope.height
        }
    }

    const xy = (coords) => [coords.x, coords.y];

    let lastTime = new Date().getTime();
    /**
     * @param {number} time
     * @param {DrawScope} drawScope
     */
    let draw = (time, {
        context,
        width,
        height
    }) => {
        const deltaTime = time - lastTime;
        lastTime = time;
        const cv = 255;
        context.fillStyle = `rgba(0,0,0,0.001)`;
        context.fillRect(0, 0, width, height);
        context.strokeStyle = `rgb(${cv},${cv},${cv})`;
        context.lineWidth = 1;


        rootWalkers.forEach(walker => {
            context.beginPath();
            context.moveTo(...xy(projectCoords(walker)));
            walker.update(deltaTime);
            if (walker.jumped) {
                context.moveTo(...xy(projectCoords(walker)));
            }
            context.lineTo(...xy(projectCoords(walker)));
            context.stroke();
            const livingWalkersCount = rootWalkers.filter(walker => walker.inUse).length;
            // the more walkers exist, the longer it takes to spawn new ones
            if (walker.age > 1 + Math.random() * livingWalkersCount * livingWalkersCount) {
                rootWalkers.push(makeRootWalker(walker));
                walker.age = 0;
            }
        });


    }

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

    window.addEventListener('resize', windowResizedListener);

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


    drawScope.context = canvas.getContext('2d');
    drawScope.context.fillStyle = 'white';
    drawScope.context.fillRect(0, 0, drawScope.width, drawScope.height);
    windowResizedListener();
    frame();