<!--
    Simulate a chaos game on an iterated function set (IFS)
    and plot the result on a canvas of given dimensions.
-->

<script>
    import { onMount, onDestroy, tick, afterUpdate } from 'svelte';
    import { Matrix } from 'ml-matrix';
    import { choose } from './util.js';

    /** Width of the canvas on which the chaos figure is rendered. */
    export let width = 500;

    /** Height of the canvas on which the chaos figure is rendered. */
    export let height = 500;

    /** Total number of iterations to perform. */
    export let totalIterations = 100000;

    /** Number of iterations to perform in one frame. */
    export let iterationsPerFrame = 1000;

    /**
     * Iterated function set to use for rendering the figure as a list of
     * pairs, each containing the weight of the function and the function
     * as a 3×3 matrix.
     */
    export let ifs = [[1, Matrix.identity(3)]];

    /** Initial point for the iteration, in homogeneous coordinates. */
    export let start = Matrix.columnVector([0, 0, 1]);

    // Canvas and drawing context on which the figure is rendered
    let canvas;
    let ctx;

    // Handle to the next scheduled frame
    let nextFrame = null;

    /**
     * Perform random chaos iteration on the given point, leaving traces
     * behind as the iteration goes.
     *
     * @param point Starting point for the iteration.
     * @param ifs Iterated function set to use.
     * @param iterations Number of iterations to perform.
     */
    const randomIterate = (point, ifs, iterations) =>
    {
        // Only perform a limited amount of iterations and
        // defer the remaining ones to the next frame
        for (let i = 0; i < iterationsPerFrame; ++i)
        {
            const [_, matrix] = choose(ifs);
            point = matrix.mmul(point);

            const x = Math.floor(point.get(0, 0));
            const y = Math.floor(point.get(1, 0));

            if (x >= 0 && x < width && y >= 0 && y < height)
            {
                ctx.fillRect(x, y, 1, 1);
            }
        }

        if (iterations > iterationsPerFrame)
        {
            nextFrame = window.requestAnimationFrame(() => randomIterate(
                point, ifs,
                iterations - iterationsPerFrame
            ));
        }
    };

    /**
     * Perform deterministic chaos iteration on each of the given points,
     * leaving traces behind as the iteration goes.
     *
     * @param points Seed points, modified in-place.
     * @param index Index from which to iterate in the points array.
     * @param ifs Iterated function set to use.
     * @param iterations Number of iterations to perform.
     */
    const deterministicIterate = (points, index, ifs, iterations) =>
    {
        let i = index;

        for (
            let frameIterations = 0;
            frameIterations < iterationsPerFrame;
            frameIterations += ifs.length,
            i = (i + ifs.length) % points.length
        )
        {
            points.splice(
                i, 1,
                ...ifs.map(([_, matrix]) =>
                {
                    const nextPoint = matrix.mmul(points[i]);
                    const x = Math.floor(nextPoint.get(0, 0));
                    const y = Math.floor(nextPoint.get(1, 0));

                    if (x >= 0 && x < width && y >= 0 && y < height)
                    {
                        ctx.fillRect(x, y, 1, 1);
                    }

                    return nextPoint;
                })
            );
        }

        if (iterations > iterationsPerFrame)
        {
            nextFrame = window.requestAnimationFrame(() => deterministicIterate(
                points, i, ifs,
                iterations - iterationsPerFrame
            ));
        }
    };

    /** Interrupt the iteration process. */
    const stopLoop = () =>
    {
        window.cancelAnimationFrame(nextFrame);
    };

    $: {
        stopLoop();

        if (ctx && ifs.length)
        {
            ctx.clearRect(0, 0, width, height);
            ctx.fillStyle = 'black';
            deterministicIterate([start], 0, ifs, totalIterations);
        }
    }

    onMount(() => ctx = canvas.getContext('2d'));
    onDestroy(() => stopLoop());
</script>

<canvas
    bind:this={canvas}
    draggable=false
    width={width} height={height}
/>
