Code Playground

Code collection display and playground studies

Masonry cards render code thumbnails, uploaded source studies, and collection elements authored through the CMS studio.

Exospherehtml
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">

    <title>Diffusion</title>


    <script type="text/javascript">

        var canvas;
        var ctx;

        var audioContext;
        var fmsynth;

        var compressor;
        var drone;
        var stab;
        var stab2;
        var lead;
        var pad;

        var fft;

        let world;
        let nodes = [];
        let paths = [];
        let networks = [];

        let start = 0;
        const speed = 5;


        var width;
        var height;
        let w, h;
        let wdt;

        var scl = 10;
        var cols, rows;

        var mouseX = 0;
        var mouseY = 0;

        const growth_occurence = 0.001;
        const sink_occurence = 0.001;
        const exchange_occurence = 0.001;
        const rnd_occurence = 0.0001
        const mitosis_occurence = 0.001;
        const merge_occurence = 0.00001;

        let color_palette;
        let backg_index;

        let debounce = false;

        const nNetwork = 5;
        const nPath = 18;

        var palette = [
            ["#40f2d0", "#999DFF", "#FF9751", "#545479", "#8EB49B", "#2F4858"],
            ["#082880", "#FD765D", "#FFB753", "#00BABB", "#5E5A8B", "#B02F37"],
            ["#7462f9", "#B5A6DC", "#68BAA6", "#EE1E2F", "#00098D", "#E0EEF0"],
            ["#f4b53f", "#2F4858", "#FFEDCB", "#4B8178", "#4E2D21", "#C1554E"],
            ["#1D1B33", "#2F4858", "#FFEDCB", "#4B8178", "#7197A8", "#362E37"],
            ["#F4B53F", "#2F4858", "#FFEDCB", "#4B8178", "#C1554E", "#6A4C57"],
            ["#453C4C", "#B3AA74", "#FFEDCB", "#9F9D8A", "#7A6154", "#204E5E"],
            ["#4C4459", "#82B59D", "#FFEDCB", "#F3AB4E", "#A44440", "#204E5E"],
            ["#203239", "#96977B", "#FFEDCB", "#DB986F", "#964744", "#2C3637"],
            ["#344C5C", "#698DA0", "#BFB8A6", "#E8CFC1", "#D96D59", "#2B3967"],
            ["#263A5B", "#61898D", "#B4C4C5", "#FAE3C0", "#7B4B5A", "#B3BC99"],
            ["#7A8671", "#D2B27C", "#E4CC91", "#BB605A", "#7B4B5A", "#F0E9A9"],
            ["#ffa943", "#2177f4", "#35fc93", "#f9cfd2", "#6eabf4", "#3714a1"],
            ["#ce2d42", "#7462f9", "#f4b53f", "#123676", "#9c223d", "#e6c7b4"],
            ["#06a0ba", "#6f3bff", "#f20a41", "#8777f7", "#4848c1", "#e6c7b4"],
            ["#71f2ff", "#81fcca", "#f91cb0", "#0239c1", "#05bdc6", "#f7f1b4"],
            ["#302D3B", "#DBF7BD", "#879369", "#9A5154", "#C3C590", "#CAA174"],
            ["#25164D", "#BFD4BF", "#316C6F", "#494190", "#D3B74F", "#ECE5DE"],
            ["#624565", "#9B9589", "#E49E81", "#DB6A60", "#FAB582", "#E3B69A"],
            ["#594C98", "#372B33", "#FE0878", "#82D6DB", "#92D0AF", "#721F4C"],
            ["#F0DEB4", "#A1A17A", "#5A8170", "#F4F3CC", "#4B8178", "#FFC7A1"],
            ["#A42534", "#3F352F", "#B74C3B", "#D4AA71", "#DCCFB2", "#693239"],
            ["#665B55", "#F5B488", "#B55053", "#8B2335", "#69837B", "#F0D2B1"],
            ["#313D51", "#FBE8AA", "#EB917B", "#B15552", "#809488", "#337F83"],
            ["#042882", "#81fcca", "#f91cb0", "#0239c1", "#8450d6", "#05bdc6"],
            ["#304B61", "#281733", "#377F86", "#D1D1AE", "#DB6D6A", "#9AC7C3"],
            ["#2F677E", "#B5B383", "#C35F4F", "#D2E1D9", "#7FD1AE", "#FAE7BF"],
            ["#2A2B41", "#673939", "#377F86", "#E3D5AE", "#EFC375", "#281733"],
            ["#FF7E42", "#2D2E3C", "#FFE1C8", "#4F9472", "#D1594D", "#384C7D"],
            ["#FF7306", "#C7B18E", "#FFE3A4", "#7F4E4D", "#233072", "#6B97A8"],
            ["#FF6705", "#ED9C7B", "#FFE1A2", "#7F4E4D", "#154150", "#BAC292"],
            ["#f2c079", "#3c3c67", "#f7edcf", "#84a0a4", "#d22f2f", "#cfd5ed"],
            ["#505978", "#8ab984", "#f7d8c6", "#7f655d", "#c6d8f7", "#78A39B"],
            ["#2F3C3E", "#7CAB93", "#B6CDA9", "#F4F3CF", "#666460", "#C9A889"],
            ["#C1554E", "#0C3E4D", "#076269", "#C5B65B", "#F7C862", "#22BB9B"],
            ["#C1554E", "#477F82", "#22BB9B", "#DFD2A8", "#F7C862", "#63AC9C"],
            ["#3E3649", "#6D7180", "#B0ACAD", "#DFD2A8", "#F7C862", "#A6BC99"],
            ["#44E2D2", "#2365B8", "#645EBA", "#664785", "#F38073", "#2E93B7"],
            ["#E7A564", "#AC966F", "#596358", "#234564", "#EB7952", "#F0BA81"],
            ["#131C3B", "#2B3C61", "#4F77C9", "#8FC2DE", "#D2DCC0", "#265670"],
            ["#D62A58", "#122959", "#4F77C9", "#8FC2DE", "#D2DCC0", "#466C88"],
            ["#DFE3DB", "#99BEA5", "#51525F", "#6C6A36", "#3F4159", "#572D54"],
            ["#066A74", "#352F51", "#601449", "#EC4E25", "#F7954A", "#792023"],
            ["#131133", "#0A405E", "#EF4F56", "#68C3A0", "#F3EFCA", "#A5282F"],
            ["#234357", "#33AFA6", "#8FE2AD", "#DBEFA9", "#EACF7B", "#408AA8"],
            ["#FDFCF8", "#A2A295", "#5A5F5F", "#2C3D3C", "#252929", "#31576E"],
            ["#2F2930", "#707485", "#99AABE", "#B6E6E8", "#FBF8F2", "#C4BEBE"],
            ["#F4B53F", "#6A4C57", "#DBE3AA", "#6EC699", "#3C4549", "#A66648"],
            ["#F4B53F", "#6A4C57", "#212D4E", "#274B64", "#628367", "#6B293C"],
            ["#F4B53F", "#6A4C57", "#21B29F", "#406B77", "#333F5B", "#3A8EA2"],
            ["#432D3A", "#42495F", "#6F7E67", "#C0B37A", "#E9C268", "#E0DEAB"],
            ["#432D3A", "#42495F", "#368991", "#E5DAB6", "#EDAB58", "#ED7B4A"],
            ["#F3E6CA", "#DCAF8A", "#A8AD75", "#40818B", "#32374A", "#96B7B7"],
            ["#492E1B", "#732737", "#5D5969", "#8F8E71", "#E8D993", "#B25963"],
            ["#25272C", "#FBEFBD", "#DCB26C", "#386B67", "#0D3844", "#497084"],
            ["#25272C", "#FBEFBD", "#AEAA9D", "#497084", "#303E61", "#3770A2"],
            ["#292127", "#9B464A", "#E0C985", "#2A979A", "#0D2F3C", "#ECEFDB"],
            ["#F4B53F", "#2F4858", "#0FB3BC", "#D6E3BD", "#C1554E", "#6A4C57"],
            ["#D4BE5B", "#A1D1B5", "#48ADB6", "#516C57", "#401D35", "#39447D"],
            ["#401D35", "#9E4557", "#F16E54", "#F2D89D", "#C4BB86", "#F9AD69"],
            ["#FEB613", "#DEE4D7", "#3EC2B2", "#356F8D", "#2E2A32", "#A5E1AC"],
            ["#8F4756", "#E7D7C4", "#A9A1A5", "#7A909D", "#352E3F", "#BA8B80"],
            ["#1A2739", "#223653", "#5C223D", "#CD253A", "#EA8353", "#15486B"],
            ["#385F8D", "#223653", "#E3564C", "#CD253A", "#764468", "#E1BAA9"],
            ["#BFE3D4", "#F1D499", "#6787A0", "#3E5277", "#341C33", "#9E667B"],
            ["#495069", "#87AD9F", "#D4C8AC", "#B67465", "#4B1F33", "#F28443"],
            ["#495069", "#528B8C", "#EBDCBE", "#F0B07D", "#BD5D60", "#82A2B9"],
            ["#507386", "#78B4AE", "#F2E1B9", "#C78379", "#8A6946", "#2D2543"],
        ];


        var notes;

        // list of all major notes with # and b
        var _notes = [['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'],
        ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']];

        let flavours = ["major", "minor"];
        let flavour;

        var musicKeys = [['C', 'G', 'D', 'A', 'A#', 'E', 'B', 'F#', 'Gb', 'Db', 'Ab', 'Eb', 'Bb', 'F'], // Major
        ['A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'Bb', 'F', 'C', 'G', 'D']]; // Minor

        let fifths = [["C", "G", "D", "A", "E", "B", "F#", "Db", "Ab", "Eb", "Bb", "F"],
        ["A", "E", "B", "F#", "C#", "G#", "D#", "Bb", "F", "C", "G", "D"]
        ];
        // generate complete scale from C2 to C7 with # and b
        let scale = [];

        const _degrees = [[0, 2, 4, 5],
        [0, 2, 4, 6],
        [0, 2, 4, 5],
        [0, 2, 3, 5],
        ]; // degrees in the scale to use for chords
        const _chords = [["I", "III", "IV", "V"],
        ["I", "III", "V", "VII"],
        ["I", "IV", "V", "VI"],
        ["IV", "III", "IV", "V"],

        ]; // chords to use for each degree

        var progressionIndex = 0;
        var progression;
        var degree;

        let scale2
        const chordArray = [];
        const freqArray = [];
        let droneArray = [];
        const droneFreqArray = [];

        let currentChord = 0;
        let nextChord = 0;

        let trigger = false;
        let play = false;


        function init() {

            var body = document.querySelector('body');

            canvas = document.createElement('canvas');
            body.appendChild(canvas);


            ctx = canvas.getContext('2d');

            audioContext = new AudioContext();


            w = window.innerWidth;

            h = window.innerHeight;




            const pixelRatio = window.devicePixelRatio;


            canvas.width = (w * pixelRatio) | 0;
            canvas.height = (h * pixelRatio) | 0;


            canvas.style.width = `${w}px`;
            canvas.style.height = `${h}px`;


            width = w;
            height = h;


            ctx.scale(pixelRatio, pixelRatio);


            ctx.imageSmoothingEnabled = true;
            ctx.lineWidth = 1;
            ctx.lineCap = "round";
            ctx.lineJoin = "round";

            cols = (width / scl + 1) | 0;
            rows = (height / scl + 1) | 0;

            start = Date.now();


            const palette_index = randomRange(0, palette.length) | 0;
            color_palette = palette[palette_index];
            console.log(palette_index)
            backg_index = randomRange(0, color_palette.length) | 0;


            // 1, 0.3, 0.001, 0.01
            let envelope = {
                attack: 2.5,
                decay: 0.1,
                sustain: 0.01,
                release: 0.03
            };
            // 0.001, 0.3, 0.01, 0.01
            let modulationEnvelope = {
                attack: 0.01,
                decay: 0.3,
                sustain: 0.002,
                release: 0.01
            };

            // drone envelope
            let droneEnvelope = {
                attack: 1,
                decay: 0.3,
                sustain: 0.1,
                release: 0.01
            };

            // drone modulation envelope
            let droneModulationEnvelope = {
                attack: 1.5,
                decay: 0.3,
                sustain: 0.1,
                release: 0.01
            };

            let stabEnvelope = {
                attack: 0.0002,
                decay: 0.1,
                sustain: 0.001,
                release: 0.005,
            };

            let stabModulationEnvelope = {
                attack: 0.0004,
                decay: 0.1,
                sustain: 0.001,
                release: 0.01,
            };

            let leadEnvelope = {
                attack: 0.5,
                decay: 0.6,
                sustain: 0.01,
                release: 0.4,
            };

            let leadModulationEnvelope = {
                attack: 0.02,
                decay: 0.6,
                sustain: 0.01,
                release: 0.2,
            };

            let padEnvelope = {
                attack: 0, // 0.001
                decay: 0,
                sustain: 0.1,
                release: 0.3,
            };

            let padModulationEnvelope = {
                attack: 0, // 0.002
                decay: 0,
                sustain: 0.1,
                release: 0.6,
            };


            compressor = new Compressor(audioContext);


            fmsynth = new FMSynth(audioContext, 5, envelope, modulationEnvelope, 0.01);
            fmsynth.connect(compressor.input);

            stab = new FMSynth(audioContext, 5, stabEnvelope, stabModulationEnvelope, 0.002);
            stab.connect(compressor.input);

            stab2 = new FMSynth(audioContext, 5, stabEnvelope, stabModulationEnvelope, 0.01);
            stab2.connect(compressor.input);

            lead = new FMSynth(audioContext, 5, leadEnvelope, leadModulationEnvelope, 0.002);
            lead.connect(compressor.input);


            drone = new DroneSynth(audioContext, 5, droneEnvelope, droneModulationEnvelope, 0.01);
            drone.connect(compressor.input);

            pad = new PadSynth(audioContext, 5, padEnvelope, padModulationEnvelope, 0.02);
            pad.connect(compressor.input);

            fft = new FFT(audioContext);
            compressor.connect(fft.analyser);
            compressor.connect(audioContext.destination);


            let index3 = mapRange(mathRand(), 0, 1, 0, flavours.length) | 0;
            flavour = flavours[index3];
            let keyIndex = mapRange(mathRand(), 0, 1, 0, fifths[index3].length) | 0;
            let key = fifths[index3][keyIndex];
            let keySignature = getKeySignature(key);
            let scaleIndex = getKeyListIndex(keySignature);
            notes = _notes[scaleIndex];
            console.log(flavour);
            console.log(notes)
            let note = key + "2";

            console.log(note)
            let scale2 = generateScale2(note, flavour);

            console.log("scale2", scale2)
            progressionIndex = mathRand() * _degrees.length | 0;
            degree = _degrees[progressionIndex];
            progression = _chords[progressionIndex];
            console.log(degree, progression);
            let fifths_progression = generateProgression(scale2, index3);
            console.log(fifths_progression)

            fifths_progression.forEach((note, index) => {
                let chord = generateChord(note);
                chordArray.push(chord);

            });
            console.log(chordArray)

            droneArray = decreaseOctave(chordArray);

            chordArray.forEach((chord, index) => {
                let freq = [];
                chord.forEach((note, index) => {
                    let freq2 = noteToFrequency(note);
                    freq.push(freq2);
                });
                freqArray.push(freq);
            });

            droneArray.forEach((chord, index) => {
                let freq = [];
                chord.forEach((note, index) => {
                    let freq2 = noteToFrequency(note);
                    freq.push(freq2);
                });
                droneFreqArray.push(freq);
            });

            world = new World();

            for (let k = 0; k < nNetwork; k++) {
                let theta2 = mapRange(k, 0, nNetwork, 0, Math.PI * 2);
                let x = randomRange(-width / 2, width / 2) | 0;
                let y = randomRange(-height / 2, height / 2) | 0;

                networks.push(new Network());

                for (let j = 0; j < nPath; j++) {
                    let path = new Path();

                    let theta1 = mapRange(j, 0, nPath, 0, Math.PI * 2);
                    let offsetX = Math.cos(theta1) * 200;
                    let offsetY = Math.sin(theta1) * 200;


                    let nodeN = randomRange(9, 12) | 0;
                    for (let i = 0; i < nodeN; i++) {

                        let theta = mapRange(i, 0, nodeN, 0, Math.PI * 2);
                        let node = new Node(x + offsetX + Math.cos(theta) * 10, y + offsetY + Math.sin(theta) * 10);

                        path.nodes.push(node);

                        nodes.push(node);
                    }

                    paths.push(path);


                }



            }



            for (let i = 0; i < paths.length; i++) {
                let path = paths[i];
                for (let j = 0; j < path.nodes.length; j++) {
                    let node = path.nodes[j];
                    // in node.neighbors add the node i-1 and i+1
                    if (j > 0 && j < path.nodes.length - 1) {
                        node.neighbors.push(path.nodes[j - 1]);
                        path.nodes[j - 1].neighbors.push(node);
                        node.neighbors.push(path.nodes[j + 1]);
                        path.nodes[j + 1].neighbors.push(node);
                    } else if (j === 0) {
                        node.neighbors.push(path.nodes[path.nodes.length - 1]);
                        path.nodes[path.nodes.length - 1].neighbors.push(node);
                    } else if (j === path.nodes.length - 1) {
                        node.neighbors.push(path.nodes[0]);
                        path.nodes[0].neighbors.push(node);
                    }
                }


            }




            // push each paths to each networks
            for (let i = 0; i < networks.length; i++) {
                let network = networks[i];
                for (let j = i * nPath; j < i * nPath + nPath; j++) {
                    network.paths.push(paths[j]);
                }
            }


            for (let i = 0; i < networks.length; i++) {
                let network = networks[i];
                for (let j = 0; j < network.paths.length; j++) {
                    let path = network.paths[j];
                    if (j === 0) {
                        path.neighbors.push(network.paths[network.paths.length - 1]);
                        network.paths[network.paths.length - 1].neighbors.push(path);
                    } else if (j === network.paths.length - 1) {
                        path.neighbors.push(network.paths[0]);
                        network.paths[0].neighbors.push(path);
                    } else {
                        path.neighbors.push(network.paths[j - 1]);
                        network.paths[j - 1].neighbors.push(path);
                        path.neighbors.push(network.paths[j + 1]);
                        network.paths[j + 1].neighbors.push(path);
                    }

                }
            }

            world.paths.push(...paths);




            window.$generativeTraits = {
                "Type": "Audio",
                "BPM": "60",
                "Key": key,
                "Flavour": flavour,
                "Chords": progression,
            };

            canvas.addEventListener("keypress", capture, false);
            canvas.addEventListener('click', audioPlay);
            canvas.addEventListener('touchstart', audioPlay);

            canvas.addEventListener('mousemove', mouseMove, false);
            canvas.addEventListener('touchmove', touchMove), false;


            window.requestAnimationFrame(anim);
        }


        function anim() {
            window.requestAnimationFrame(anim);

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

            ctx.fillStyle = color_palette[backg_index];
            // ctx.fillStyle = "rgba(227, 213, 174, 0.3)"; // = rgba(227, 213, 174, 1)
            ctx.fillRect(0, 0, width, height);


            ctx.save();

            /*
            world.polygons.forEach(polygon => {
                polygon.draw();
            });
            */

            ctx.translate(width / 2, height / 2);



            for (let k = 0; k < networks.length; k++) {
                let network = networks[k];
                for (let i = 0; i < network.paths.length; i++) {
                    // network.paths[i].update(network.paths[i].computeCenterOfMass());
                    network.paths[i].update(network.computeCenterOfMass());
                    network.paths[i].draw();

                    for (let j = 0; j < network.paths.length; j++) {
                        if (i !== j) {
                            network.paths[i].repelPaths(network.paths[j]);
                            network.paths[i].pushNodes(network.paths[j]);
                        }
                    }


                    // network.paths[i].attractNeighbors();
                    network.paths[i].clusterPaths();

                    network.paths[i].playNote2();



                    if (network.paths[i].nodes.length < 12) {
                        if (mathRand() < growth_occurence) {

                            network.paths[i].diffGrowth();

                        }

                    }

                    if (network.paths[i].nodes.length > 9) {
                        if (mathRand() < sink_occurence) {
                            network.paths[i].diffShrink();


                        }
                    }

                    if (mathRand() < rnd_occurence && network.paths.length < 18) {

                        network.paths[i].randomPath(k);

                    }

                    if (mathRand() < mitosis_occurence && network.paths[i].nodes.length > 12) {

                        network.paths[i].mitosis(k);


                    }

                    for (let j = 0; j < network.paths.length; j++) {
                        if (i !== j) {
                            if (mathRand() < merge_occurence) {

                                network.paths[i].mergePath2(network.paths[j], k);

                            }

                        }
                    }


                }
                network.update();

                if (mathRand() < 0.001 && network.paths.length > 8) {
                     network.exchangePaths();
                    // network.randomGrowth();
                }

                if (mathRand() < 0.0001 && networks.length < 6) {
                    // network.generateNetwork();
                }

                if (mathRand() < 0.0001 && networks.length > 5) {
                    // network.removeNetwork();
                }

                // network.clusterNetworks();
                // network.draw();
            }



            currentChord = nextChord;

            if (trigger) {
                fmsynth.start();
                drone.start();
                stab.start();
                stab2.start();
                lead.start();
                pad.start();

                let interval = setInterval(() => {
                    playChord();

                    let ite = mathRand() * 3 | 0;
                    let ite2 = mathRand() * 3 | 0;
                    let ite3 = mathRand() * 3 | 0;
                    let ite4 = mathRand() * 3 | 0;
                    let ite5 = mathRand() * 3 | 0;
                    let ite6 = mathRand() * 3 | 0;
                    let delay = [3 / 4, 3 / 8, 3 / 16]
                    fmsynth.delay.setDelayTime(delay[ite]);
                    fmsynth.pingPongDelay.setDelayTime(delay[ite2]);
                    stab.delay.setDelayTime(delay[ite3]);
                    stab.pingPongDelay.setDelayTime(delay[ite2]);
                    stab2.delay.setDelayTime(delay[ite4]);
                    stab2.pingPongDelay.setDelayTime(delay[ite3]);
                    lead.delay.setDelayTime(delay[ite4]);
                    lead.pingPongDelay.setDelayTime(delay[ite2]);
                    pad.delay.setDelayTime(delay[ite5]);
                    // pad.pingPongDelay.setDelayTime(delay[ite6]);

                    let ite7 = mathRand() * 3 | 0;
                    let ite8 = mathRand() * 3 | 0;
                    let ite9 = mathRand() * 3 | 0;
                    let ite10 = mathRand() * 3 | 0;

                    let feedback = [0.6, 0.7, 0.8]

                    let reverb = [0.4, 0.5, 0.3]

                    fmsynth.delay.setFeedback(feedback[ite5]);
                    fmsynth.pingPongDelay.setFeedback(feedback[ite6]);

                    stab.delay.setFeedback(feedback[ite7]);
                    stab.pingPongDelay.setFeedback(feedback[ite8]);
                    stab2.delay.setFeedback(feedback[ite9]);
                    stab2.pingPongDelay.setFeedback(feedback[ite10]);

                    lead.delay.setFeedback(feedback[ite9]);
                    lead.pingPongDelay.setFeedback(feedback[ite10]);

                    stab.reverb.setWet(reverb[ite7]);
                    stab2.reverb.setWet(reverb[ite8]);


                }, randomizeDelay(6850, 250));

                trigger = false;
            }

            function playChord() {
                drone.triggerAttack(droneFreqArray[currentChord]);
                fmsynth.triggerAttack(freqArray[currentChord]);

                if (mathRand() < 0.27) {

                    stab.triggerAttack(freqArray[currentChord]);
                }
                lead.triggerAttack(freqArray[currentChord]);
                pad.triggerAttack(freqArray[currentChord]);


                setTimeout(() => {
                    // drone.triggerRelease();
                    fmsynth.triggerRelease();

                    nextChord = (currentChord + 1) % chordArray.length;
                }, randomizeDelay(6300, 250));


                setTimeout(() => {
                    // drone.triggerRelease();

                    stab.triggerRelease();
                    lead.triggerRelease();

                    nextChord = (currentChord + 1) % chordArray.length;
                }, randomizeDelay(6300, 350));

                setTimeout(() => {
                    // drone.triggerRelease();
                    pad.triggerRelease();
                    nextChord = (currentChord + 1) % chordArray.length;
                }, randomizeDelay(6300, 150));


            }

            ctx.restore();


        }


        class Vector {
            constructor(x, y) {
                this.x = x;
                this.y = y;
            }
            add(v) {
                return new Vector(this.x + v.x, this.y + v.y);
            }
            sub(v) {
                return new Vector(this.x - v.x, this.y - v.y);
            }
            mult(s) {
                return new Vector(this.x * s, this.y * s);
            }
            div(s) {
                return new Vector(this.x / s, this.y / s);
            }
            mag() {
                return Math.sqrt(this.x * this.x + this.y * this.y);
            }
            normalize() {
                return this.div(this.mag());
            }
            limit(max) {
                if (this.mag() > max) {
                    return this.normalize().mult(max);
                } else {
                    return this;
                }
            }
            dist(v) {
                return this.sub(v).mag();
            }
            setMag(mag) {
                return this.normalize().mult(mag);
            }
            heading() {
                return Math.atan2(this.y, this.x);
            }
            rotate(angle) {
                let newHeading = this.heading() + angle;
                let mag = this.mag();
                this.x = Math.cos(newHeading) * mag;
                this.y = Math.sin(newHeading) * mag;
                return this;
            }
            lerp(v, t) {
                return this.add(v.sub(this).mult(t));
            }
            lerp2(v, t) {
                return this.sub(v.sub(this).mult(t));
            }
            copy() {
                return new Vector(this.x, this.y);
            }
            dot(v) {
                return this.x * v.x + this.y * v.y;
            }
            static lerp(v0, v1, t) {
                return v0.mult(1 - t).add(v1.mult(t));
            }
            static dot(v1, v2) {
                return v1.x * v2.x + v1.y * v2.y;
            }
        }

        class Node {
            constructor(x, y) {
                this.pos = new Vector(x, y);
                this.iniPos = new Vector(x, y);
                this.vel = new Vector(0, 0);
                this.acc = new Vector(0, 0);
                this.neighbors = [];
                this.minDistance = 3;
                this.attractionForce = 8.9; // Neigbours attraction
                this.repulsionForce = 0.39; // Neighbours repulsion
                this.attractionForce2 = 0.6; // non assigned
                this.repulsionForce2 = 12.9; // KNN search neighbours
                this.repulsionForce3 = 24.92; // Curvature
                this.attractionForce3 = 0.0; // non assigned
                this.attractionForce4 = 12.3; // Alignement
                this.repulsionForce4 = 39.8; // mouse interaction
                this.attractionForce5 = 0.0; // polygon grid
                this.attractionForce6 = 0.09; // non assigned
                this.repulsionForce6 = 16.8; // Repuse from center
                this.p = false;

                this.amplitude = 0;

                this.r = 2; // randomRange(1, 4) | 0;
                this.radius = this.r * 10;
                this.colorP = color_palette[randomIndexOmit(color_palette, backg_index)];

                this.init();
            }
            update(netCenter, centerOfMass) {
                // Calculate the total force acting on the node
                let totalForce = new Vector(0, 0);

                // Calculate the attraction force from each neighbor
                for (const neighbor of this.neighbors) {
                    const distance = neighbor.pos.sub(this.pos).mag();
                    const direction = neighbor.pos.sub(this.pos).normalize();
                    const attraction = direction.mult(this.attractionForce * (distance - this.minDistance));
                    totalForce = totalForce.add(attraction);
                }

                // Calculate the repulsion force from each neighbor

                for (const neighbor of this.neighbors) {
                    const distance7 = neighbor.pos.sub(this.pos).mag();
                    const direction7 = neighbor.pos.sub(this.pos).normalize();
                    const repulsion7 = direction7.mult(-this.repulsionForce * 1 / (distance7 * distance7));
                    totalForce = totalForce.add(repulsion7);
                }


                /*
                const distance4 = this.iniPos.sub(this.pos).mag();
                if (distance4 > 0.1) {
                    const direction4 = this.iniPos.sub(this.pos).normalize();
                    let attraction4 = direction4.mult(this.attractionForce3 * distance4);
                    totalForce = totalForce.add(attraction4);
                }
                */


                // Sinusoidal force
                const frequency = 5;
                const time = millis() * 0.001;
                this.applyAudioWaveForce();
                this.applySinusoidalForce(netCenter, this.amplitude, frequency, time);



                totalForce = totalForce.add(this.acc);


                for (const neighbor of this.knnSearch(this.radius)) {
                    const distance3 = neighbor.pos.sub(this.pos).mag();
                    const direction3 = neighbor.pos.sub(this.pos).normalize();
                    const repulsion3 = direction3.mult(-this.repulsionForce2 * 1 / distance3 * distance3);
                    totalForce = totalForce.add(repulsion3);
                }

                for (const other of nodes) {
                    if (other === this) continue;
                    const distance = other.pos.sub(this.pos).mag();

                    const direction = other.pos.sub(this.pos).normalize();
                    const repulsion = direction.mult(-this.repulsionForce * 1 / distance * distance);
                    totalForce = totalForce.add(repulsion);



                }

                // alignement force

                const midPoint = new Vector(0, 0);
                for (const neighbor of this.neighbors) {
                    midPoint.x += neighbor.pos.x;
                    midPoint.y += neighbor.pos.y;
                }
                midPoint.x /= this.neighbors.length;
                midPoint.y /= this.neighbors.length;
                const direction = midPoint.sub(this.pos).normalize();
                const alignment = direction.mult(this.attractionForce4 * (midPoint.sub(this.pos).mag() - this.minDistance));
                totalForce = totalForce.add(alignment);


                // curvature force

                const curvatureForce = new Vector(0, 0);
                for (let i = 0; i < this.neighbors.length; i++) {
                    const neighbor = this.neighbors[i];
                    const nextNeighbor = this.neighbors[(i + 1) % this.neighbors.length];
                    const tangent = neighbor.pos.sub(this.pos).normalize();
                    const nextTangent = nextNeighbor.pos.sub(this.pos).normalize();
                    const curvature = tangent.add(nextTangent).normalize();
                    curvatureForce.x += curvature.x;
                    curvatureForce.y += curvature.y;
                }
                curvatureForce.x /= this.neighbors.length;
                curvatureForce.y /= this.neighbors.length;

                const curvatureCenter = this.pos.add(curvatureForce.mult(0.5));
                const distance2 = curvatureCenter.sub(this.pos).mag();

                const direction2 = curvatureCenter.sub(this.pos).normalize();
                var attraction2 = direction2.mult(-this.repulsionForce3 * 1 / distance2 * distance2);

                totalForce = totalForce.add(attraction2);

                // repel nodes from the mouse

                const mouse = new Vector(mouseX, mouseY);
                const distance = mouse.sub(this.pos).mag();
                if (distance < 100) {
                    const direction = mouse.sub(this.pos).normalize();
                    const repulsion = direction.mult(-this.repulsionForce4 * 1 / distance * distance);
                    totalForce = totalForce.add(repulsion);
                }


                /*

                let posOffset = new Vector(this.pos.x + width / 2, this.pos.y + height / 2);
                let i = posOffset.x / scl | 0;
                let j = posOffset.y / scl | 0;
                let index = i + j * cols;

                let polygon = world.polygons[index];

                if (polygon) {
                    // if the node is within the polygon, an attraction force is applied to the node
                    // to keep it inside the polygon

                    if (polygon.spot) {


                        const direction = polygon.center.sub(posOffset).normalize();
                        const distance = polygon.center.sub(posOffset).mag();

                        const attraction = direction.mult(this.attractionForce5 * distance);
                        totalForce = totalForce.add(attraction);


                    }
                    polygon.spot = true;


                }
                */

                // repulse nodes from center 

                const distance5 = centerOfMass.sub(this.pos).mag();
                const direction5 = centerOfMass.sub(this.pos).normalize();
                const repulsion5 = direction5.mult(-this.repulsionForce6 * 1 / distance5 * distance5);
                totalForce = totalForce.add(repulsion5);



                this.acc = totalForce.div(1);



                // Update the pos and velocity of the node using the motion equations and integration
                const dt = 0.1; // Time step
                const damping = 0.5; // Damping factor

                const velocity = this.vel.add(this.acc.mult(dt)).mult(1 - damping * dt);
                this.pos = this.pos.add(velocity.mult(dt));
                this.vel = velocity;
                this.acc = new Vector(0, 0);
            }

            applySinusoidalForce(origin, amplitude, frequency, time) {
                let force = new Vector(0, 0);
                const direction = this.pos.sub(origin).normalize();
                const distance = this.pos.sub(origin).mag();
                const phase = frequency * time + distance * 0.05; // 0.05 is the phase shift
                const sinusoidal = Math.sin(phase) * this.amplitude;


                force.x = direction.x * sinusoidal;
                force.y = direction.y * sinusoidal;



                const perpendicular = new Vector(direction.y, -direction.x);
                force.x += perpendicular.x;
                force.y += perpendicular.y;


                this.acc = this.acc.add(force);

                /*
                        if (this.amplitude < 10 && !this.p) {
                            this.amplitude += 0.15;
                        }
                        if (this.amplitude > 0 && this.p) {
                            this.amplitude -= 0.05;
                        }
                        if (this.amplitude >= 10) {
                            this.p = true;
                        }
                        if (this.amplitude <= 0) {
                            this.p = false;
                        }
                */
            }

            applyAudioWaveForce() {

                const frequencyData = fft.getFrequencyData();

                // get the average amplitude of the frequency data
                let amp = 0;
                for (let i = 0; i < frequencyData.length; i++) {
                    amp += frequencyData[i];
                }
                amp /= frequencyData.length;

                // map the amplitude to a value between 0 and 1
                this.amplitude = mapRange(amp, 0, 255, 0, 1) * 740;


                /*

                if (this.amplitude < 10 && !this.p) {
                    this.amplitude += 0.15;
                }
                if (this.amplitude > 0 && this.p) {
                    this.amplitude -= 0.05;
                }
                if (this.amplitude >= 10) {
                    this.p = true;
                }
                if (this.amplitude <= 0) {
                    this.p = false;
                }
                */

            }

            knnSearch(radius) {
                let neighbors = [];
                for (const node of nodes) {
                    const distance = node.pos.sub(this.pos).mag();
                    if (distance <= radius && node !== this) {
                        neighbors.push(node);
                    }
                }
                return neighbors;
            }


            move() {
                if (0 <= this.t && this.t < this.t1) {
                    let nrm = norm(this.t, 0, this.t1 - 1);
                    // this.attractionForce2 = lerp(0, 0.8, easeOutQuint(nrm));
                    // this.repulsionForce2 = lerp(0.1, 0.9, easeOutQuint(nrm));
                    // this.amplitude = lerp(0, 0.1, easeOutQuint(nrm));

                }
                if (this.t1 < this.t) {
                    this.init();
                }
                this.t++;
            }

            init() {
                this.t = (-mathRand() * mapRange(speed, 1, 5, 5000, 1000)) | 0;
                this.t1 = mapRange(speed, 1, 5, 2200, 600);
            }

            display() {
                ctx.fillStyle = this.colorP;
                ctx.beginPath();
                ctx.arc(this.pos.x, this.pos.y, this.r * 2, 0, Math.PI * 2);
                ctx.fill();
            }

        }

        class Path {
            constructor() {
                this.nodes = [];
                this.neighbors = [];
                this.maxEdgeLength = 5;
                this.minEdgeLength = 10; // unassigned
                this.attractionForce = 2.3;
                this.repulsionForce = 12.1; // 24.1
                this.attractionForce2 = 2.3;
                this.repulsionForce2 = 16.1; // 24.1 12.1 // push nodes
                this.influenceRadius = 10;
                this.maxForce = 6;
                this.colorIndex = randomIndexOmit(color_palette, backg_index);
                this.color = color_palette[this.colorIndex];
                this.border = color_palette[randomIndexOmit(color_palette, this.colorIndex)];
                this.nucleusColor = color_palette[randomIndexOmit(color_palette, this.colorIndex)];
                this.nucleusBorder = color_palette[randomIndexOmit(color_palette, this.colorIndex)];
                this.chord = freqArray[mathRand() * freqArray.length | 0];
            }
            draw() {

                ctx.fillStyle = this.color;
                ctx.strokeStyle = this.border;
                /*
                for (let i = 0; i < this.nodes.length; i++) {
                    ctx.beginPath();
                    ctx.arc(this.nodes[i].pos.x, this.nodes[i].pos.y, 2, 0, Math.PI * 2);
                    ctx.stroke();
                }
                */


                ctx.beginPath();
                ctx.moveTo(this.nodes[0].pos.x, this.nodes[0].pos.y);
                for (let i = 1; i < this.nodes.length - 1; i++) {
                    const xc = (this.nodes[i].pos.x + this.nodes[i + 1].pos.x) / 2;
                    const yc = (this.nodes[i].pos.y + this.nodes[i + 1].pos.y) / 2;
                    ctx.quadraticCurveTo(this.nodes[i].pos.x, this.nodes[i].pos.y, xc, yc);
                }
                // ctx.quadraticCurveTo(this.nodes[this.nodes.length - 2].pos.x, this.nodes[this.nodes.length - 2].pos.y, this.nodes[this.nodes.length - 1].pos.x, this.nodes[this.nodes.length - 1].pos.y);
                ctx.quadraticCurveTo(this.nodes[this.nodes.length - 1].pos.x, this.nodes[this.nodes.length - 1].pos.y, this.nodes[0].pos.x, this.nodes[0].pos.y);

                ctx.fill();
                // ctx.stroke();


                // draw a nucleus at the center of the path

                ctx.fillStyle = this.nucleusColor;
                ctx.strokeStyle = this.nucleusBorder;
                ctx.beginPath();
                ctx.arc(this.computeCenterOfMass().x, this.computeCenterOfMass().y, 3, 0, Math.PI * 2);
                ctx.fill();
                // ctx.stroke();




            }
            update(netCenter) {
                for (let i = 0; i < this.nodes.length; i++) {
                    this.nodes[i].update(netCenter, this.computeCenterOfMass());
                    this.nodes[i].move();
                }

            }

            // repel each path from the other paths
            repelPaths(otherPath) {
                let delta = otherPath.computeCenterOfMass().sub(this.computeCenterOfMass())
                const d = delta.mag();
                delta = delta.normalize();
                if (d < this.diameter() / 2 + otherPath.diameter() / 2 + this.influenceRadius) {
                    const force = delta.mult(-this.repulsionForce / d * d);
                    for (const node of this.nodes) {
                        // let steer = force.sub(node.vel);
                        //  node.acc = node.acc.add(steer);
                        for (const otherNodes of otherPath.nodes) {
                            let delta2 = otherNodes.pos.sub(node.pos);
                            const d2 = delta2.mag();
                            delta2 = delta2.normalize();
                            if (d2 < this.influenceRadius) {
                                const force2 = delta2.mult(-this.repulsionForce / d2 * d2);
                                let steer2 = force2.sub(node.vel);
                                node.acc = node.acc.add(steer2);
                            }
                        }
                    }
                }


            }
            // cluster paths to the center of the canvas
            clusterPaths() {
                let center = new Vector(0, 0);
                let delta = center.sub(this.computeCenterOfMass());
                const d = delta.mag();
                delta = delta.normalize();

                for (const node of this.nodes) {
                    let force = delta.mult(this.attractionForce2 * d);
                    force = force.sub(node.vel);
                    node.acc = node.acc.lerp(force, 0.5);
                }


            }

            pushNodes(t) {
                for (let e = 0; e < this.nodes.length; e++) {
                    let i = this.nodes[e].pos.dist(this.computeCenterOfMass());
                    if (this.nodes[e].pos.dist(t.computeCenterOfMass()) < i)
                        for (let a = 0; a < t.nodes.length; a++) {
                            let o = this.computeCenterOfMass().sub(t.computeCenterOfMass()),
                                c = o.mag();
                            if (((o = o.normalize()), c < this.diameter() / 2 + t.diameter() / 2 + this.influenceRadius)) {
                                // let g = o.mult((this.repulsionForce2 / c) * c).sub(this.nodes[e].vel);
                                let g = o.mult((this.repulsionForce2 / c) * c)
                                this.nodes[e].acc = this.nodes[e].acc.add(g);
                            }
                        }
                }
            }

            diffGrowth() {
                // add a node on the path between 2 existing nodes if their distance is longer than maxedgelength, disconnect old neighbors and connect the new node to the neighbors
                for (let i = 0; i < this.nodes.length; i++) {
                    let node = this.nodes[i];
                    let j = (i + 1) % this.nodes.length;
                    let nextNode = this.nodes[j];
                    let distance = node.pos.dist(nextNode.pos);

                    if (distance > this.maxEdgeLength) {
                        let newNode = new Node((node.pos.x + nextNode.pos.x) / 2, (node.pos.y + nextNode.pos.y) / 2);
                        this.nodes.splice(j, 0, newNode);
                        nodes.push(newNode);
                        // remove neighbor j from node i and add the new node
                        node.neighbors.splice(node.neighbors.indexOf(nextNode), 1, newNode);
                        // node.neighbors.push(newNode);
                        newNode.neighbors.push(node);
                        // remove neighbor i from node j and add the new node
                        nextNode.neighbors.splice(nextNode.neighbors.indexOf(node), 1, newNode);
                        // nextNode.neighbors.push(newNode);
                        newNode.neighbors.push(nextNode);



                        break;
                    }


                }
            }

            diffShrink() {
                for (let i = 0; i < this.nodes.length; i++) {
                    let node = this.nodes[i];
                    let j = (i + 1) % this.nodes.length;
                    let nextNode = this.nodes[j];

                    // Check if the distance between node and nextNode is less than half of maxEdgeLength
                    let distance = node.pos.dist(nextNode.pos);
                    if (distance < this.maxEdgeLength / 2) {
                        // Remove the node in between (j)
                        let removedNode = this.nodes.splice(j, 1)[0];

                        // Reconnect neighbors of node and nextNode
                        let indexInNodeNeighbors = node.neighbors.indexOf(removedNode);
                        if (indexInNodeNeighbors !== -1) {
                            node.neighbors.splice(indexInNodeNeighbors, 1);
                            if (indexInNodeNeighbors < node.neighbors.length) {
                                node.neighbors[indexInNodeNeighbors].neighbors.push(node);
                            }
                        }

                        let indexInNextNodeNeighbors = nextNode.neighbors.indexOf(removedNode);
                        if (indexInNextNodeNeighbors !== -1) {
                            nextNode.neighbors.splice(indexInNextNodeNeighbors, 1);
                            if (indexInNextNodeNeighbors < nextNode.neighbors.length) {
                                nextNode.neighbors[indexInNextNodeNeighbors].neighbors.push(nextNode);
                            }
                        }

                        // Remove the removedNode from the global nodes array
                        let indexInGlobalNodes = nodes.indexOf(removedNode);
                        if (indexInGlobalNodes !== -1) {
                            nodes.splice(indexInGlobalNodes, 1);
                        }

                        break; // Only remove one node per call to diffShrink
                    }
                }
            }




            // generate a new path at a random position with a random number of nodes and add it to the network
            randomPath(k) {
                let path = new Path();
                let nodeN = randomRange(8, 12) | 0;
                for (let i = 0; i < nodeN; i++) {
                    let theta = mapRange(i, 0, nodeN, 0, Math.PI * 2);
                    let node = new Node(this.computeCenterOfMass().x + Math.cos(theta) * 10, this.computeCenterOfMass().y + Math.sin(theta) * 10);
                    path.nodes.push(node);
                    nodes.push(node);
                }
                paths.push(path);
                networks[k].paths.push(path);


                for (let i = 0; i < path.nodes.length; i++) {
                    let node = path.nodes[i];
                    if (i === 0) {
                        node.neighbors.push(path.nodes[path.nodes.length - 1]);
                        path.nodes[path.nodes.length - 1].neighbors.push(node);
                    } else if (i === path.nodes.length - 1) {
                        node.neighbors.push(path.nodes[0]);
                        path.nodes[0].neighbors.push(node);
                    } else {
                        node.neighbors.push(path.nodes[i - 1]);
                        path.nodes[i - 1].neighbors.push(node);
                        node.neighbors.push(path.nodes[i + 1]);
                        path.nodes[i + 1].neighbors.push(node);
                    }
                }

                this.playNote();
            }

            // mitosis, split this path and its nodes into two paths each containing half of the nodes of the original path
            // update the neighbors of the nodes accross all nodes between node1 and node2
            mitosis(k) {
                // split the path into two paths
                let node1 = 0;
                let node2 = this.nodes.length / 2 | 0;


                let nodes1 = this.nodes.slice(0, node2);
                let nodes2 = this.nodes.slice(node2, this.nodes.length);

                // generate half of a circular path
                for (let i = 0; i < node2; i++) {
                    let theta = mapRange(i, 0, node2, 0, Math.PI);
                    let node = new Node(this.computeCenterOfMass().x + Math.cos(theta) * 10, this.computeCenterOfMass().y + Math.sin(theta) * 10);
                    nodes1.push(node);
                }

                this.nodes = nodes1;


                // generate the other half of the circular path
                let path2 = new Path();
                for (let i = node2; i < this.nodes.length; i++) {
                    let theta = mapRange(i, node2, this.nodes.length, Math.PI, Math.PI * 2);
                    let node = new Node(this.computeCenterOfMass().x + Math.cos(theta) * 10, this.computeCenterOfMass().y + Math.sin(theta) * 10);
                    nodes2.push(node);
                    path2.nodes.push(node);
                }





                for (let i = 0; i < node1.length; i++) {
                    // remove the neighbors of the nodes
                    let node = nodes1[i];
                    for (let j = 0; j < node.neighbors.length; j++) {
                        let neighbor = node.neighbors[j];
                        neighbor.neighbors.splice(neighbor.neighbors.indexOf(node), 1);
                    }

                    // node.neighbors = [];
                }

                for (let i = 0; i < node2.length; i++) {
                    // remove the neighbors of the nodes
                    let node = nodes2[i];
                    for (let j = 0; j < node.neighbors.length; j++) {
                        let neighbor = node.neighbors[j];
                        neighbor.neighbors.splice(neighbor.neighbors.indexOf(node), 1);
                    }

                    // node.neighbors = [];
                }



                // update the neighbors of the nodes
                for (let i = 0; i < nodes1.length; i++) {
                    let node = nodes1[i];
                    if (i === 0) {
                        node.neighbors.push(nodes1[nodes1.length - 1]);
                        nodes1[nodes1.length - 1].neighbors.push(node);
                    } else if (i === nodes1.length - 1) {
                        node.neighbors.push(nodes1[0]);
                        nodes1[0].neighbors.push(node);
                    } else {
                        node.neighbors.push(nodes1[i - 1]);
                        nodes1[i - 1].neighbors.push(node);
                        node.neighbors.push(nodes1[i + 1]);
                        nodes1[i + 1].neighbors.push(node);
                    }
                }

                for (let i = 0; i < nodes2.length; i++) {
                    let node = nodes2[i];
                    if (i === 0) {
                        node.neighbors.push(nodes2[nodes2.length - 1]);
                        nodes2[nodes2.length - 1].neighbors.push(node);
                    } else if (i === nodes2.length - 1) {
                        node.neighbors.push(nodes2[0]);
                        nodes2[0].neighbors.push(node);
                    } else {
                        node.neighbors.push(nodes2[i - 1]);
                        nodes2[i - 1].neighbors.push(node);
                        node.neighbors.push(nodes2[i + 1]);
                        nodes2[i + 1].neighbors.push(node);
                    }
                }




                paths.push(path2);
                networks[k].paths.push(path2);

                 this.playNote();

            }

            // Merge two paths if nodes from each path are too close
            // remove the neighbors of the nodes that are merged
            // use half of the nodes from each path to create a new path
            mergePath(otherPath, n) {

                const HalfPath = this.nodes.length / 2 | 0;
                const HalfPath2 = otherPath.nodes.length / 2 | 0;

                const nodes1 = this.nodes.slice(0, HalfPath);
                const nodes2 = otherPath.nodes.slice(HalfPath2, otherPath.nodes.length);

                let merged = false;
                for (let i = 0; i < this.nodes.length; i++) {
                    const node1 = this.nodes[i];
                    const k = (i + 1) % this.nodes.length;
                    const nextNode = this.nodes[k];
                    for (let j = 0; j < otherPath.nodes.length; j++) {
                        const node2 = otherPath.nodes[j];
                        const l = (j + 1) % otherPath.nodes.length;
                        const nextOtherNode = otherPath.nodes[l];
                        const distance = node1.pos.dist(node2.pos);
                        const nextDistance = nextNode.pos.dist(nextOtherNode.pos);
                        if (distance < 5 && nextDistance < 5) {
                            // Remove neighbors of the nodes to be merged
                            this.removeNodeNeighbors(node1);
                            this.removeNodeNeighbors(node2);

                            // remove node
                            this.nodes.splice(this.nodes.indexOf(node1), 1);
                            otherPath.nodes.splice(otherPath.nodes.indexOf(node2), 1);

                            let mergedNodes = [];

                            // Create a new path with the merged nodes
                            mergedNodes = [...this.nodes, ...otherPath.nodes.reverse()];

                            // Update neighbors for the merged nodes
                            this.updateNeighbors(mergedNodes);

                            // Create a new path with the merged nodes

                            /*
                            const newPath = new Path();
                            newPath.nodes = mergedNodes;
                            paths.push(newPath);
                            networks[n].paths.push(newPath);
                            */

                            this.nodes = mergedNodes;

                            // Remove the original paths from paths array
                            // paths.splice(paths.indexOf(this), 1);
                            paths.splice(paths.indexOf(otherPath), 1);

                            // Remove the original paths from network paths array
                            // networks[n].paths.splice(networks[n].paths.indexOf(this), 1);
                            networks[n].paths.splice(networks[n].paths.indexOf(otherPath), 1);



                            merged = true;
                            break;
                        }
                    }
                    if (merged) break;
                }
            }

            removeNodeNeighbors(node) {
                for (let i = 0; i < node.neighbors.length; i++) {
                    let neighbor = node.neighbors[i];
                    neighbor.neighbors.splice(neighbor.neighbors.indexOf(node), 1);
                }
            }

            updateNeighbors(nodes) {
                for (let i = 0; i < nodes.length; i++) {
                    let node = nodes[i];
                    node.neighbors = [];
                    if (i === 0) {
                        node.neighbors.push(nodes[nodes.length - 1]);
                        nodes[nodes.length - 1].neighbors.push(node);
                    } else if (i === nodes.length - 1) {
                        node.neighbors.push(nodes[0]);
                        nodes[0].neighbors.push(node);
                    } else {
                        node.neighbors.push(nodes[i - 1]);
                        nodes[i - 1].neighbors.push(node);
                        node.neighbors.push(nodes[i + 1]);
                        nodes[i + 1].neighbors.push(node);
                    }
                }
            }



            mergePath2(path2, n) {
                // Create a new path to store the merged result
                let merged = false;
                let mergedPath = new Path();

                const halfPath = this.nodes.length / 2 | 0;
                const halfPath2 = path2.nodes.length / 2 | 0;

                const nodes1 = this.nodes.slice(0, halfPath);
                const nodes2 = path2.nodes.slice(halfPath2, path2.nodes.length);

                // Combine nodes from path1 and path2 into mergedPath
                mergedPath.nodes = nodes1.concat(nodes2.reverse());

                for (let m = 0; m < this.nodes.length; m++) {
                    let node = this.nodes[m];
                    for (let n = 0; n < path2.nodes.length; n++) {
                        let otherNode = path2.nodes[n];
                        let distance = node.pos.dist(otherNode.pos);
                        if (distance < 5) {
                            merged = true;
                            break;
                        }
                    }
                }

                if (!merged) {
                    return;
                }
                // Update neighbors for nodes in mergedPath
                for (let i = 0; i < mergedPath.nodes.length; i++) {
                    let node = mergedPath.nodes[i];
                    node.neighbors = []; // Clear existing neighbors

                    // Calculate indices for neighbors
                    let prevIndex = (i - 1 + mergedPath.nodes.length) % mergedPath.nodes.length;
                    let nextIndex = (i + 1) % mergedPath.nodes.length;

                    // Assign previous and next neighbors
                    node.neighbors.push(mergedPath.nodes[prevIndex]);
                    node.neighbors.push(mergedPath.nodes[nextIndex]);

                    // Update neighbors' connections
                    if (!mergedPath.nodes[prevIndex].neighbors.includes(node)) {
                        mergedPath.nodes[prevIndex].neighbors.push(node);
                    }
                    if (!mergedPath.nodes[nextIndex].neighbors.includes(node)) {
                        mergedPath.nodes[nextIndex].neighbors.push(node);
                    }
                }

                // remove path1 and path2 from paths array
                // paths.splice(paths.indexOf(this), 1);
                paths.splice(paths.indexOf(path2), 1);
                // networks[n].paths.splice(networks[n].paths.indexOf(this), 1);
                networks[n].paths.splice(networks[n].paths.indexOf(path2), 1);

                // Add mergedPath to paths array
                /*
                paths.push(mergedPath);
                networks[n].paths.push(mergedPath);
                */
                this.nodes = mergedPath.nodes;


            }


            computeCenterOfMass() {
                let sum = new Vector(0, 0);
                for (const node of this.nodes) {
                    sum = sum.add(node.pos);
                }
                const centerOfMass = sum.div(this.nodes.length);
                return centerOfMass;
            }

            // compute path diameter
            diameter() {
                let maxDist = 0;
                for (let i = 0; i < this.nodes.length; i++) {
                    for (let j = 0; j < this.nodes.length; j++) {
                        let dist = this.nodes[i].pos.dist(this.nodes[j].pos);
                        if (dist > maxDist) {
                            maxDist = dist;
                        }
                    }
                }
                return maxDist;
            }


            // if the mouse is within the path, play a chord, each notes associated with a node

            playNote() {
                /*
                const mouse = new Vector(mouseX, mouseY);
                const distance = mouse.sub(this.computeCenterOfMass()).mag();
                if (distance < this.diameter() / 2 - 10 && !debounce) { */
                    debounce = true;
                    // drone.triggerAttack(this.chord);
                   // fmsynth.triggerAttack(this.chord);
                    stab2.triggerAttack(freqArray[currentChord]);
                   // lead.triggerAttack(this.chord);
                   // pad.triggerAttack(this.chord);


                    setTimeout(() => {
                        // drone.triggerRelease();
                        // fmsynth.triggerRelease();
                        stab2.triggerRelease();
                        // lead.triggerRelease();
                        // pad.triggerRelease();
                        debounce = false;
                    }, randomizeDelay(1600, 250));
                //}
            }

            playNote2() {
             
                const mouse = new Vector(mouseX, mouseY);
                const distance = mouse.sub(this.computeCenterOfMass()).mag();
                if (distance < this.diameter() / 2 - 10 && !debounce) { 
                    debounce = true;
                    // drone.triggerAttack(this.chord);
                   // fmsynth.triggerAttack(this.chord);
                    stab2.triggerAttack(this.chord);
                   // lead.triggerAttack(this.chord);
                   // pad.triggerAttack(this.chord);


                    setTimeout(() => {
                        // drone.triggerRelease();
                        // fmsynth.triggerRelease();
                        stab2.triggerRelease();
                        // lead.triggerRelease();
                        // pad.triggerRelease();
                        debounce = false;
                    }, randomizeDelay(2100, 250));
                }
            }

            


        }

        // class network made of an array of paths, each networks are experiencing the same forces as the nodes
        // the network is made of paths that are connected to each other and the nodes are constrained to stay within the network

        class Network {
            constructor() {
                this.paths = [];
                this.attractionForce = 0.1;
                this.repulsionForce = 6.3; // repulsion between one another
                this.attractionForce2 = 1.2; // 0.3 0.6 0.9 // attraction force between each path centerOfMass
                this.repulsionForce2 = 0.9; // 0.9
                this.maxForce = 1;
                this.influenceRadius = 10;
                this.attractionForce3 = 0.1;
            }

            // apply an attraction force between each path and its neighbors
            // apply an attraction force between each path and its neighbors
            update() {
                let totalForce = new Vector(0, 0);

                // calculate the attraction force from a path and each neighbors
                /*
                for (let i = 0; i < this.paths.length; i++) {
                    for (let j = 0; j < this.paths[i].neighbors.length; j++) {
                        for (let k = 0; k < this.paths[i].nodes.length; k++) {
                            const distance = this.paths[i].neighbors[j].computeCenterOfMass().sub(this.paths[i].nodes[k].pos).mag();
                            const direction = this.paths[i].neighbors[j].computeCenterOfMass().sub(this.paths[i].nodes[k].pos).normalize();
                            const attraction = direction.mult(this.attractionForce * distance);
                            let force = attraction.div(1);
                            // force = force.sub(this.paths[i].nodes[k].vel);
                            this.paths[i].nodes[k].acc = this.paths[i].nodes[k].acc.add(force);
                        }

                    }
                }
                */


                // calculate the attraction force between each path and the center of the network

                for (let i = 0; i < this.paths.length; i++) {
                    let delta = this.computeCenterOfMass().sub(this.paths[i].computeCenterOfMass());
                    const d = delta.mag();
                    delta = delta.normalize();
                    if (d > 0.1) {
                        const force = delta.mult(this.attractionForce2 * d);
                        for (let j = 0; j < this.paths[i].nodes.length; j++) {
                            let steer = force.sub(this.paths[i].nodes[j].vel);
                            this.paths[i].nodes[j].acc = this.paths[i].nodes[j].acc.add(steer);
                        }
                    }
                }


                // calculate the repulsion force from each paths
                for (let l = 0; l < networks.length; l++) {
                    if (this !== networks[l]) {
                        const delta = networks[l].computeCenterOfMass().sub(this.computeCenterOfMass());
                        const d = delta.mag();
                        const direction = delta.normalize();
                        if (d < this.diameter() / 2 + networks[l].diameter() / 2) {
                            for (let path of this.paths) {
                                for (let otherPath of networks[l].paths) {
                                    for (let node of path.nodes) {
                                        for (let otherNode of otherPath.nodes) {
                                            let delta = otherNode.pos.sub(node.pos);
                                            const d2 = delta.mag();
                                            delta = delta.normalize();

                                            const force = delta.mult(-this.repulsionForce / d2 * d2);
                                            // let steer = force.sub(node.vel);
                                            node.acc = node.acc.add(force);

                                        }


                                    }
                                }
                            }
                        }
                    }
                }

                // calculate the attraction force from each path
                /*
                for (let i = 0; i < this.paths.length; i++) {
                    for (let j = 0; j < this.paths.length; j++) {
                        if (i !== j) {
                            const distance = this.paths[j].computeCenterOfMass().sub(this.paths[i].computeCenterOfMass()).mag();
                            const direction = this.paths[j].computeCenterOfMass().sub(this.paths[i].computeCenterOfMass()).normalize();
                            const attraction = direction.mult(this.attractionForce3 * distance);
                            let force = attraction.div(1);
                            // force = force.sub(this.paths[i].nodes[k].vel);
                            for (let k = 0; k < this.paths[i].nodes.length; k++) {
                                this.paths[i].nodes[k].acc = this.paths[i].nodes[k].acc.add(force);
                            }
                        }
                    }
                }
                */

                // calculate the attraction between 2 networks
                for (let l = 0; l < networks.length; l++) {
                    if (this !== networks[l]) {
                        const delta = networks[l].computeCenterOfMass().sub(this.computeCenterOfMass());
                        const d = delta.mag();
                        const direction = delta.normalize();
                        if (d < this.diameter() / 2 + networks[l].diameter() / 2) {
                            for (let path of this.paths) {
                                for (let otherPath of networks[l].paths) {
                                    for (let node of path.nodes) {
                                        for (let otherNode of otherPath.nodes) {
                                            let delta = otherNode.pos.sub(node.pos);
                                            const d2 = delta.mag();
                                            delta = delta.normalize();

                                            const force = delta.mult(this.attractionForce3 * d2);
                                            // let steer = force.sub(node.vel);
                                            node.acc = node.acc.add(force);

                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                // knn repulsion force between networks
                /*
                for (let i = 0; i < this.paths.length; i++) {
                    let path = this.paths[i];

                    for (const neighbor of this.knnSearch(this.radius)) {
                        const distance = neighbor.computeCenterOfMass().sub(path.computeCenterOfMass()).mag();
                        const direction = neighbor.computeCenterOfMass().sub(path.computeCenterOfMass()).normalize();
                        const repulsion = direction.mult(-this.repulsionForce2 / distance * distance);
                        for (let j = 0; j < path.nodes.length; j++) {
                            let steer = repulsion.sub(path.nodes[j].vel);
                            path.nodes[j].acc = path.nodes[j].acc.add(steer);
                        }
                    }
                }
                */


                // apply curvature force between neighboring paths
                /*
                for (let i = 0; i < this.paths.length; i++) {
                    for (let j = 0; j < this.paths[i].neighbors.length; j++) {
                        for (let k = 0; k < this.paths[i].nodes.length; k++) {
                            const curvatureForce = new Vector(0, 0);
                            for (let l = 0; l < this.paths[i].neighbors[j].nodes.length; l++) {
                                const tangent = this.paths[i].nodes[k].pos.sub(this.paths[i].neighbors[j].nodes[l].pos).normalize();
                                const nextTangent = this.paths[i].neighbors[j].nodes[(l + 1) % this.paths[i].neighbors[j].nodes.length].pos.sub(this.paths[i].nodes[k].pos).normalize();
                                const curvature = tangent.add(nextTangent).normalize();
                                curvatureForce.x += curvature.x;
                                curvatureForce.y += curvature.y;
                            }
                            curvatureForce.x /= this.paths[i].neighbors[j].nodes.length;
                            curvatureForce.y /= this.paths[i].neighbors[j].nodes.length;
                            const curvatureCenter = this.paths[i].nodes[k].pos.add(curvatureForce.mult(0.5));
                            const distance = curvatureCenter.sub(this.paths[i].nodes[k].pos).mag();
                            const direction = curvatureCenter.sub(this.paths[i].nodes[k].pos).normalize();
                            const attraction = direction.mult(-this.repulsionForce2 * 1 / distance * distance);
                            let force = attraction.div(1);
                            force = force.sub(this.paths[i].nodes[k].vel);
                            this.paths[i].nodes[k].acc = this.paths[i].nodes[k].acc.add(force);
                        }

                    }
                }
                

                // apply alignment force between neighboring paths
                for (let i = 0; i < this.paths.length; i++) {
                    for (let j = 0; j < this.paths[i].neighbors.length; j++) {
                        const midPoint = new Vector(0, 0);
                        for (let k = 0; k < this.paths[i].neighbors[j].nodes.length; k++) {
                            midPoint.x += this.paths[i].neighbors[j].nodes[k].pos.x;
                            midPoint.y += this.paths[i].neighbors[j].nodes[k].pos.y;
                        }
                        midPoint.x /= this.paths[i].neighbors[j].nodes.length;
                        midPoint.y /= this.paths[i].neighbors[j].nodes.length;
                        for (let k = 0; k < this.paths[i].neighbors[j].nodes.length; k++) {
                            const direction = midPoint.sub(this.paths[i].neighbors[j].nodes[k].pos).normalize();
                            const distance = midPoint.sub(this.paths[i].neighbors[j].nodes[k].pos).mag();
                            const alignment = direction.mult(this.attractionForce2 * (midPoint.sub(this.paths[i].neighbors[j].nodes[k].pos).mag() - this.paths[i].neighbors[j].nodes[k].radius));
                            let force = alignment.div(1);
                            force = force.sub(this.paths[i].neighbors[j].nodes[k].vel);
                            this.paths[i].neighbors[j].nodes[k].acc = this.paths[i].neighbors[j].nodes[k].acc.add(force);
                        }
                    }
                }
                */

                // force attrracting the path to center of the network


                /*
                for (let i = 0; i < this.paths.length; i++) {
                    let delta = this.computeCenterOfMass().sub(this.paths[i].computeCenterOfMass());
                    const d = delta.mag();
                    delta = delta.normalize();
                    if (d > 0.1) {
                        const force = delta.mult(this.attractionForce2 * d);
                        for (let j = 0; j < this.paths[i].nodes.length; j++) {
                            // let steer = force.sub(this.paths[i].nodes[j].vel);
                            this.paths[i].nodes[j].acc = this.paths[i].nodes[j].acc.add(force);
                        }
                    }
                }
                */

                // attract networks to the center of the canvas
                for (let i = 0; i < networks.length; i++) {
                    let delta = new Vector(0, 0);
                    delta = delta.sub(networks[i].computeCenterOfMass());
                    const d = delta.mag();
                    delta = delta.normalize();
                    if (d > 0.1) {
                        const force = delta.mult(this.attractionForce * d);
                        for (let j = 0; j < networks[i].paths.length; j++) {
                            for (let k = 0; k < networks[i].paths[j].nodes.length; k++) {
                                let steer = force.sub(networks[i].paths[j].nodes[k].vel);
                                networks[i].paths[j].nodes[k].acc = networks[i].paths[j].nodes[k].acc.add(force);
                            }
                        }
                    }
                }


                totalForce = totalForce.div(1);

                // apply the total force to each node of the path
                for (let i = 0; i < this.paths.length; i++) {




                    for (let j = 0; j < this.paths[i].nodes.length; j++) {




                    }


                }
            }

            // exchange paths between networks
            exchangePaths() {
                let i = mathRand() * this.paths.length | 0;
                let path = this.paths[i];
                let j = mathRand() * networks.length | 0;
                let network = networks[j];
                if (network !== this) {
                    this.paths.splice(i, 1);
                    network.paths.push(path);
                }
            }




            // attract networks to the center of the canvas
            clusterNetworks() {
                let center = new Vector(0, 0);
                let delta = center.sub(this.computeCenterOfMass());
                const d = delta.mag();
                delta = delta.normalize();
                for (const path of this.paths) {
                    for (const node of path.nodes) {
                        let force = delta.mult(this.attractionForce2 * d);
                        force = force.sub(node.vel);
                        node.acc = node.acc.lerp(force, 0.5);
                    }
                }
            }

            knnSearch(radius) {
                let neighbors = [];
                for (const path of this.paths) {
                    for (const node of path.nodes) {
                        for (const otherPath of this.paths) {
                            for (const otherNode of otherPath.nodes) {
                                const distance = otherNode.pos.sub(node.pos).mag();
                                if (distance <= radius && otherNode !== node) {
                                    neighbors.push(otherNode);
                                }
                            }
                        }
                    }
                }
                return neighbors;
            }


            // compute the center of mass of the network made of paths
            computeCenterOfMass() {
                let sum = new Vector(0, 0);
                for (const path of this.paths) {
                    sum = sum.add(path.computeCenterOfMass());
                }
                const centerOfMass = sum.div(this.paths.length);
                return centerOfMass;
            }

            // compute the diameter of the network
            diameter() {
                let maxDist = 0;
                for (let i = 0; i < this.paths.length; i++) {
                    for (let j = 0; j < this.paths.length; j++) {
                        let dist = this.paths[i].computeCenterOfMass().dist(this.paths[j].computeCenterOfMass());
                        // if (dist > maxDist) {
                        maxDist = dist;
                        // }
                    }
                }
                return maxDist;
            }

            generateNetwork() {
                let network = new Network();
                for (let i = 0; i < nPath; i++) {
                    let x = randomRange(-width / 2, width / 2) | 0;
                    let y = randomRange(-height / 2, height / 2) | 0;
                    let path = new Path();
                    let nodeN = randomRange(8, 12) | 0;
                    for (let i = 0; i < nodeN; i++) {
                        let theta = mapRange(i, 0, nodeN, 0, Math.PI * 2);
                        let node = new Node(x + Math.cos(theta) * 10, y + Math.sin(theta) * 10);
                        path.nodes.push(node);
                        nodes.push(node);
                    }
                    network.paths.push(path);
                }

               // update neighbors of the nodes
                for (let i = 0; i < network.paths.length; i++) {
                    let path = network.paths[i];
                    for (let j = 0; j < path.nodes.length; j++) {
                        let node = path.nodes[j];
                        if (j === 0) {
                            node.neighbors.push(path.nodes[path.nodes.length - 1]);
                            path.nodes[path.nodes.length - 1].neighbors.push(node);
                        } else if (j === path.nodes.length - 1) {
                            node.neighbors.push(path.nodes[0]);
                            path.nodes[0].neighbors.push(node);
                        } else {
                            node.neighbors.push(path.nodes[j - 1]);
                            path.nodes[j - 1].neighbors.push(node);
                            node.neighbors.push(path.nodes[j + 1]);
                            path.nodes[j + 1].neighbors.push(node);
                        }
                    }
                }

                networks.push(network);
                
            }

            removeNetwork() {
                let i = mathRand() * networks.length | 0;
                networks.splice(i, 1);
            }

            draw() {
                /*
                ctx.strokeStyle = "#000";
                ctx.beginPath();
                ctx.arc(this.computeCenterOfMass().x, this.computeCenterOfMass().y, 10, 0, Math.PI * 2);
                ctx.stroke();
                */
                // draw lines between the center of mass of each path
                /*
                 for (let i = 0; i < this.paths.length; i++) {
                     for (let j = 0; j < this.paths.length; j++) {
                         if (i !== j) {
                             ctx.strokeStyle = paths[i].color;
                             ctx.beginPath();
                             ctx.moveTo(this.paths[i].computeCenterOfMass().x, this.paths[i].computeCenterOfMass().y);
                             ctx.lineTo(this.paths[j].computeCenterOfMass().x, this.paths[j].computeCenterOfMass().y);
                             ctx.stroke();
                         }
                     }
                 }
                 */
                // draw curves accross the center of mass of random paths

                for (let i = 0; i < this.paths.length; i++) {
                    for (let j = 0; j < this.paths.length; j++) {
                        if (i !== j) {
                            ctx.strokeStyle = paths[i].color;
                            ctx.beginPath();
                            ctx.moveTo(this.paths[i].computeCenterOfMass().x, this.paths[i].computeCenterOfMass().y);
                            ctx.quadraticCurveTo(this.paths[i].computeCenterOfMass().x, this.paths[j].computeCenterOfMass().y, this.paths[j].computeCenterOfMass().x, this.paths[j].computeCenterOfMass().y);
                            ctx.stroke();
                        }
                    }
                }


            }

        }


        class World {
            constructor() {
                this.paths = [];
                this.polygons = this.generatePolygonGrid(rows, cols, scl);
                this.attractionForce = 3.6;
                this.maxForce = 1;
            }

            generatePolygonGrid(rowCount, colCount, polygonSize) {
                const polygons = [];
                const xOffset = (width - (colCount * polygonSize)) / 2; // Calculate horizontal offset
                const yOffset = (height - (rowCount * polygonSize)) / 2; // Calculate vertical offset

                for (let row = 0; row < rowCount; row++) {
                    for (let col = 0; col < colCount; col++) {
                        const x = xOffset + col * polygonSize; // Calculate x position
                        const y = yOffset + row * polygonSize; // Calculate y position
                        const polygon = new Polygon(x, y, polygonSize); // Create a new Polygon instance
                        polygons.push(polygon); // Add the polygon to the polygons array
                    }
                }

                return polygons; // Return the array of polygons
            }

        }

        // class Polygon, is a polygon that make a grid in the World canvas
        // the polygon are connected to each other and the path nodes are constrained to stay within the polygon

        class Polygon {
            constructor(x, y, size) {
                this.size = size;
                this.vertices = [];
                this.spot = false;
                this.center = new Vector(x + size / 2, y + size / 2);
                this.vertices.push(new Vector(x, y));
                this.vertices.push(new Vector(x + size, y));
                this.vertices.push(new Vector(x + size, y + size));
                this.vertices.push(new Vector(x, y + size));

            }
            draw = () => {
                ctx.strokeStyle = "#000";
                ctx.lineWidth = 1;
                ctx.beginPath();
                ctx.moveTo(this.vertices[0].x, this.vertices[0].y);
                for (let i = 1; i < this.vertices.length; i++) {
                    ctx.lineTo(this.vertices[i].x, this.vertices[i].y);
                }
                ctx.closePath();
                ctx.stroke();
                if (this.spot) {
                    ctx.fillStyle = "#000";
                    ctx.beginPath();
                    ctx.arc(this.center.x, this.center.y, 5, 0, Math.PI * 2);
                    ctx.fill();
                }
            }
        }
        window.onload = function () {
            init();
        }


        function capture(e) {
            if (e.key === "s") {
                var link = document.createElement('a');
                link.download = 'origin.png';
                link.href = canvas.toDataURL("image/png");
                link.click();
            }
        }


        function audioPlay(e) {
            if (!play) {
                trigger = true;
                console.log("play");
            } else {

                if (e.touches.length === 2) {
                    // stop the audio
                    drone.stop();
                    fmsynth.stop();
                    stab.stop();
                    lead.stop();
                    pad.stop();

                   // play = false;
                    
                }

            }
            play = true;
        }

        function mouseMove(event) {
            var rect = canvas.getBoundingClientRect();

            mouseX = (event.clientX - rect.left - width / 2) | 0;
            mouseY = (event.clientY - rect.top - height / 2) | 0;
        }

        function touchMove(e) {
            var touch = e.touches[0];
            var rect = canvas.getBoundingClientRect();
            mouseX = (touch.clientX - rect.left - width / 2) | 0;
            mouseY = (touch.clientY - rect.top - height / 2) | 0;
        }

        function mapRange(value, start1, stop1, start2, stop2) {
            return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1));
        }

        function randomRange(min, max) {
            let cal = (mathRand() * (max - min) + min);
            return parseFloat(cal);
        }

        function interpolate_coord(x, y, grid, cols) {
            let i = (mapRange(x, -width / 2, width / 2, 0, width) / grid) | 0;
            let j = (mapRange(y, -height / 2, height / 2, 0, height) / grid) | 0;

            var index = i + j * cols;
            return index;
        }

        function easeOutQuint(t) {
            return 1 + (--t) * t * t * t * t;
        }

        function lerp(start, stop, amt) {
            return amt * (stop - start) + start;
        };

        function norm(n, start, stop) {
            return mapRange(n, start, stop, 0, 1);
        };

        function millis() {
            return Date.now() - start;
        }


        // using html5 canvas and web audio api in native javascript no libraries. 
        // creating a fm synthesizer class made of 8 oscillators, a class and envelope with adsr and a modulation envelope. the synthesizer generate house dub techno coppery stab sounds. Use multiple oscillators and envelopes. The synthesizer should accept chords and have effect such as delay, reverb, pingpong delay lowpass filter and high pass filter. 
        // the synthesizer should be able to play chords.

        // Oscillator class
        class Oscillator {
            constructor(audioCtx, type, frequency, detune, volume) {
                this.audioCtx = audioCtx;
                this.type = type;
                this.frequency = frequency;
                this.detune = detune;
                this.oscillator = this.audioCtx.createOscillator();
                this.oscillator.type = this.type;
                this.oscillator.frequency.value = this.frequency;
                this.oscillator.detune.value = this.detune;
                this.gainNode = this.audioCtx.createGain();
                this.gainNode.gain.value = volume;
                this.lfo = new LFO(audioCtx, 'sine', 15, 0.5);
                this.lfo.connect(this.oscillator.frequency);
            }
            connect(node) {
                this.oscillator.connect(this.gainNode);
                this.gainNode.connect(node);
            }
            disconnect() {
                this.oscillator.disconnect();
            }
            setFrequency(frequency) {
                this.oscillator.frequency.value = frequency;
            }
            setDetune(detune) {
                this.oscillator.detune.value = detune;
            }
            setType(type) {
                this.oscillator.type = type;
            }
            start() {
                this.oscillator.start();
            }
            stop() {
                this.oscillator.stop();
            }
        }

        // create a class for the envelope
        class Envelope {
            constructor(audioCtx, attack, decay, sustain, release) {
                this.audioCtx = audioCtx;
                this.attack = attack;
                this.decay = decay;
                this.sustain = sustain;
                this.release = release;
                this.gain = this.audioCtx.createGain();
                this.gain.gain.value = 0.27;
                this.gain.connect(this.audioCtx.destination);
            }
            connect(node) {
                this.gain.connect(node);
            }
            disconnect() {
                this.gain.disconnect();
            }
            triggerAttack() {
                this.gain.gain.cancelScheduledValues(this.audioCtx.currentTime);
                this.gain.gain.setValueAtTime(0, this.audioCtx.currentTime);

                /*
                this.gain.gain.linearRampToValueAtTime(1, this.audioCtx.currentTime + this.attack);
                this.gain.gain.linearRampToValueAtTime(this.sustain, this.audioCtx.currentTime + this.attack + this.decay);*/

                const initialGain = 0.0001;
                this.gain.gain.linearRampToValueAtTime(initialGain, this.audioCtx.currentTime + this.attack * 0.2); // 20% of attack time
                this.gain.gain.linearRampToValueAtTime(1, this.audioCtx.currentTime + this.attack);
                this.gain.gain.linearRampToValueAtTime(this.sustain, this.audioCtx.currentTime + this.attack + this.decay);
            }
            triggerRelease() {
                this.gain.gain.cancelScheduledValues(this.audioCtx.currentTime);
                this.gain.gain.setValueAtTime(this.gain.gain.value, this.audioCtx.currentTime);
                // this.gain.gain.linearRampToValueAtTime(0, this.audioCtx.currentTime + this.release);
                this.gain.gain.exponentialRampToValueAtTime(0.0001, this.audioCtx.currentTime + this.release);
            }
        }

        // create a class for the modulation envelope
        class ModulationEnvelope {
            constructor(audioCtx, attack, decay, sustain, release) {
                this.audioCtx = audioCtx;
                this.attack = attack;
                this.decay = decay;
                this.sustain = sustain;
                this.release = release;
                this.gain = this.audioCtx.createGain();
                this.gain.gain.value = 0.32;
                this.gain.connect(this.audioCtx.destination);
            }
            connect(node) {
                this.gain.connect(node);
            }
            disconnect() {
                this.gain.disconnect();
            }
            triggerAttack() {
                this.gain.gain.cancelScheduledValues(this.audioCtx.currentTime);
                this.gain.gain.setValueAtTime(0, this.audioCtx.currentTime);
                /*
                this.gain.gain.linearRampToValueAtTime(1, this.audioCtx.currentTime + this.attack);
                this.gain.gain.linearRampToValueAtTime(this.sustain, this.audioCtx.currentTime + this.attack + this.decay);*/

                const initialGain = 0.0001;
                this.gain.gain.linearRampToValueAtTime(initialGain, this.audioCtx.currentTime + this.attack * 0.2); // 20% of attack time
                this.gain.gain.linearRampToValueAtTime(1, this.audioCtx.currentTime + this.attack);
                this.gain.gain.linearRampToValueAtTime(this.sustain, this.audioCtx.currentTime + this.attack + this.decay);
            }
            triggerRelease() {
                this.gain.gain.cancelScheduledValues(this.audioCtx.currentTime);
                this.gain.gain.setValueAtTime(this.gain.gain.value, this.audioCtx.currentTime);
                // this.gain.gain.linearRampToValueAtTime(0, this.audioCtx.currentTime + this.release);
                this.gain.gain.exponentialRampToValueAtTime(0.0001, this.audioCtx.currentTime + this.release);
            }
        }

        class FMSynth {
            constructor(audioCtx, numberOfOscillators, envelope, mod_envelope, volume) {
                this.audioCtx = audioCtx;
                this.numberOfOscillators = numberOfOscillators;
                this.oscillators = [];
                this.modulationEnvelopes = [];
                this.filterEnvelopes = [];
                this.mod_envelope = mod_envelope;
                this.volume = volume;
                this.envelope = new Envelope(this.audioCtx, envelope.attack, envelope.decay, envelope.sustain, envelope.release);
                this.output = this.audioCtx.createGain();
                this.output.gain.value = 0.3;
                this.output.connect(this.audioCtx.destination);
                this.envelope.connect(this.output);
                this.createOscillators();
                this.createModulationEnvelopes();
                this.createEffects();

            }
            createOscillators() {
                this.oscillators.push(new Oscillator(this.audioCtx, 'sine', 993, 3, this.volume));
                this.oscillators.push(new Oscillator(this.audioCtx, 'sine', 587, -5, this.volume));
                this.oscillators.push(new Oscillator(this.audioCtx, 'sine', 1174, -7, this.volume));
                this.oscillators.push(new Oscillator(this.audioCtx, 'sine', 496, 9, this.volume));
                this.oscillators.push(new Oscillator(this.audioCtx, 'sine', 392, -1, this.volume));
            }
            createModulationEnvelopes() {
                for (let i = 0; i < this.numberOfOscillators; i++) {
                    let modulationEnvelope = new ModulationEnvelope(this.audioCtx, this.mod_envelope.attack, this.mod_envelope.decay, this.mod_envelope.sustain, this.mod_envelope.release);
                    this.modulationEnvelopes.push(modulationEnvelope);
                }
            }
            createEffects() {
                this.reverb = new Reverb(this.audioCtx);
                this.reverb.connect(this.output);
                this.delay = new Delay(this.audioCtx);
                this.delay.connect(this.output);
                this.pingPongDelay = new PingPongDelay2(this.audioCtx);
                this.pingPongDelay.connect(this.output);
                this.pingPongDelay.startAutoPanner();
                this.flanger = new Flanger(this.audioCtx);
                this.flanger.connect(this.output);
                this.lowPassFilter = new Filter(this.audioCtx, 'lowpass', 2900, 1);
                this.lowPassFilter.connect(this.output);
                this.highPassFilter = new Filter(this.audioCtx, 'highpass', 90, 0.8);
                this.highPassFilter.connect(this.output);
                this.tapeSaturator = new TapeSaturator(this.audioCtx);
                this.tapeSaturator.connect(this.output);

                // connect effects to all the modulations envelopes
                for (let i = 0; i < this.numberOfOscillators; i++) {
                    this.modulationEnvelopes[i].connect(this.reverb.input);
                    this.modulationEnvelopes[i].connect(this.delay.input);
                    this.modulationEnvelopes[i].connect(this.pingPongDelay.input);
                    this.modulationEnvelopes[i].connect(this.flanger.input);
                    this.modulationEnvelopes[i].connect(this.lowPassFilter.input);
                    this.modulationEnvelopes[i].connect(this.highPassFilter.input);
                    this.modulationEnvelopes[i].connect(this.tapeSaturator.input);
                    // this.modulationEnvelopes[i].connect(this.envelope.gain);

                }
            }
            connect(node) {
                this.output.connect(node);
            }
            disconnect() {
                this.output.disconnect();
            }
            triggerAttack(notes) {
                for (let i = 0; i < this.numberOfOscillators; i++) {
                    let frequency = notes[i] | 0;
                    this.oscillators[i].setFrequency(frequency);
                    this.oscillators[i].connect(this.modulationEnvelopes[i].gain);
                    this.modulationEnvelopes[i].triggerAttack();
                    this.modulationEnvelopes[i].connect(this.output.gain);
                }
                this.envelope.triggerAttack();
                this.envelope.connect(this.modulationEnvelopes[0].gain);
            }
            triggerRelease() {
                for (let i = 0; i < this.numberOfOscillators; i++) {
                    this.oscillators[i].disconnect();
                    this.modulationEnvelopes[i].triggerRelease();
                }
                this.envelope.triggerRelease();
            }

            triggerAttackRelease(notes, duration) {
                this.triggerAttack(notes);
                setTimeout(() => {
                    this.triggerRelease();
                }, duration);
            }
            start() {
                this.oscillators.forEach(oscillator => {
                    oscillator.start();
                }
                );
            }
            stop() {
                this.oscillators.forEach(oscillator => {
                    oscillator.stop();
                }
                );
            }



        }

        class DroneSynth extends FMSynth {
            constructor(audioCtx, n, envelope, mod_envelope, volume) {
                super(audioCtx, n, envelope, mod_envelope, volume); // call the constructor of the parent class
                this.numberOfOscillators = n;
                this.createEffects();
            }
            createEffects() {
                this.chorus = new Chorus(this.audioCtx);
                this.chorus.connect(this.output);
                this.delay = new Delay(this.audioCtx);
                this.delay.connect(this.output);
                this.reverb = new Reverb(this.audioCtx);
                this.reverb.connect(this.output);
                this.drone = new Drone(this.audioCtx);
                this.drone.connect(this.output);

                // connect effects to all the modulations envelopes
                for (let i = 0; i < this.numberOfOscillators; i++) {
                    this.modulationEnvelopes[i].connect(this.chorus.input);
                    this.modulationEnvelopes[i].connect(this.delay.input);
                    this.modulationEnvelopes[i].connect(this.reverb.input);
                    this.modulationEnvelopes[i].connect(this.drone.input);
                    // this.modulationEnvelopes[i].connect(this.envelope.gain);
                }
            }

        }

        class PadSynth extends FMSynth {
            constructor(audioCtx, n, envelope, mod_envelope, volume) {
                super(audioCtx, n, envelope, mod_envelope, volume); // call the constructor of the parent class
                this.numberOfOscillators = n;
                this.createEffects();
            }
            createEffects() {
                this.chorus = new Chorus(this.audioCtx);
                this.chorus.connect(this.output);

                this.delay = new Delay(this.audioCtx);
                this.delay.connect(this.output);
                this.delay.setDelayTime(3 / 4);
                this.delay.setFeedback(0.8);
                this.reverb = new Reverb(this.audioCtx);
                this.reverb.connect(this.output);
                this.reverb.setDelayTime(0.5);
                this.reverb.setWet(0.8);
                this.autoFilter = new AutoFilter(this.audioCtx);
                this.autoFilter.connect(this.output);
                this.StereoWidener = new StereoWidener(this.audioCtx);
                this.StereoWidener.connect(this.output);
                this.StereoWidener.setWidth(2);
                this.tapeSaturator = new TapeSaturator(this.audioCtx);
                this.tapeSaturator.connect(this.output);
                this.tapeSaturator.setDrive(0.7);

                // connect effects to all the modulations envelopes
                for (let i = 0; i < this.numberOfOscillators; i++) {
                    this.modulationEnvelopes[i].connect(this.chorus.input);


                    this.modulationEnvelopes[i].connect(this.reverb.input);
                    this.modulationEnvelopes[i].connect(this.autoFilter.input);
                    // this.modulationEnvelopes[i].connect(this.delay.input);
                    this.modulationEnvelopes[i].connect(this.delay.input);
                    this.modulationEnvelopes[i].connect(this.StereoWidener.input);
                    this.modulationEnvelopes[i].connect(this.tapeSaturator.input);

                    // this.modulationEnvelopes[i].connect(this.envelope.gain);
                }

            }

        }


        // class stereo widening effect
        class StereoWidener {
            constructor(audioCtx) {
                this.audioCtx = audioCtx;
                this.input = this.audioCtx.createGain();
                this.output = this.audioCtx.createGain();
                this.input.connect(this.output);
                this.splitter = this.audioCtx.createChannelSplitter(2);
                this.merger = this.audioCtx.createChannelMerger(2);
                this.leftGain = this.audioCtx.createGain();
                this.rightGain = this.audioCtx.createGain();
                this.leftGain.gain.value = 0.5;
                this.rightGain.gain.value = 0.5;
                this.input.connect(this.splitter);
                this.splitter.connect(this.leftGain, 0);
                this.splitter.connect(this.rightGain, 1);
                this.leftGain.connect(this.merger, 0, 0);
                this.rightGain.connect(this.merger, 0, 1);
                this.merger.connect(this.output);
            }
            connect(node) {
                this.output.connect(node);
            }
            disconnect() {
                this.output.disconnect();
            }
            setWidth(width) {
                this.width = width;
                this.leftGain.gain.value = 0.5 * (1 - this.width);
                this.rightGain.gain.value = 0.5 * (1 + this.width);
            }

        }

        // class chorus effect
        class Chorus {
            constructor(audioCtx, depth = 0.8, delayTime = 0.6, feedback = 0.54) {
                this.audioCtx = audioCtx;
                this.input = this.audioCtx.createGain();
                this.output = this.audioCtx.createGain();
                this.input.connect(this.output);

                this.depth = depth; // LFO depth (0 to 1)
                this.delayTime = delayTime; // Delay time in seconds
                this.feedback = feedback; // Feedback amount (0 to 1)

                this.lfo = new LFO(audioCtx, 'sine', 5, 0.3);
                this.delay = this.audioCtx.createDelay();
                this.feedbackGain = this.audioCtx.createGain();
                this.feedbackGain.gain.value = this.feedback;
                this.input.connect(this.delay);
                this.delay.connect(this.feedbackGain);
                this.feedbackGain.connect(this.delay);
                this.lfo.connect(this.delay.delayTime);
            }

            connect(node) {
                this.output.connect(node);
            }

            disconnect() {
                this.output.disconnect();
            }
        }



        // class to tape saturate the audio to make it sound more analog
        class TapeSaturator {
            constructor(audioCtx) {
                this.audioCtx = audioCtx;
                this.input = this.audioCtx.createGain();
                this.output = this.audioCtx.createGain();
                this.input.connect(this.output);
                this.waveShaper = this.audioCtx.createWaveShaper();
                this.waveShaper.connect(this.output);
                this.input.connect(this.waveShaper);
                this.setDrive(0.7); // 
            }
            connect(node) {
                this.output.connect(node);
            }
            disconnect() {
                this.output.disconnect();
            }
            setDrive(drive) {
                this.drive = drive;
                this.makeDistortionCurve();
            }
            makeDistortionCurve() {
                let k = this.drive;
                let n_samples = this.audioCtx.sampleRate;
                let curve = new Float32Array(n_samples);
                let deg = Math.PI / 180;
                for (let i = 0; i < n_samples; ++i) {
                    let x = i * 2 / n_samples - 1;
                    curve[i] = (3 + k) * x * 20 * deg / (Math.PI + k * Math.abs(x));
                }
                this.waveShaper.curve = curve;
            }
        }

        // Auto filter creating a tremolo effect with a low pass filter by modulating the filter cutoff frequency
        // between a range of frequency at the rate of the LFO

        class AutoFilter {
            constructor(audioCtx, frequency = 1, gain = 1) {
                this.audioCtx = audioCtx;
                this.input = this.audioCtx.createGain();
                this.output = this.audioCtx.createGain();
                this.gain = this.audioCtx.createGain();
                this.input.connect(this.output);
                this.lfo = new LFO(audioCtx, 'sine', frequency, gain);
                this.filter = this.audioCtx.createBiquadFilter();
                this.filter.type = 'lowpass';
                this.filter.frequency.value = 1000;
                this.filter.Q.value = 5;
                this.lfo.connect(this.gain);
                this.gain.gain.value = 10000;
                this.gain.connect(this.filter.frequency);
                this.input.connect(this.filter);
                this.filter.connect(this.output);
            }
            connect(node) {
                this.output.connect(node);
            }
            disconnect() {
                this.output.disconnect();
            }
            setFrequency(frequency) {
                this.lfo.frequency = frequency;
            }
            setGain(gain) {
                this.lfo.gain = gain;
            }
        }





        // Stereo effect class to give width to the sound 0 = mono, 1 = stereo, 2 = wide stereo
        // the effect give an idea of space between the sounds

        class LFO {
            constructor(audioCtx, type, frequency, gain) {
                this.audioCtx = audioCtx;
                this.type = type;
                this.frequency = frequency;
                this.gain = gain;
                this.oscillator = this.audioCtx.createOscillator();
                this.oscillator.type = this.type;
                this.oscillator.frequency.value = this.frequency;
                this.gainNode = this.audioCtx.createGain();
                this.gainNode.gain.value = this.gain;
                this.oscillator.connect(this.gainNode);
                this.start();
            }
            start() {
                this.oscillator.start();
            }
            stop() {
                this.oscillator.stop();
            }
            connect(node) {
                this.gainNode.connect(node);
            }
            disconnect() {
                this.gainNode.disconnect();
            }
            setFrequency(frequency) {
                this.oscillator.frequency.value = frequency;
            }
            setGain(gain) {
                this.gainNode.gain.value = gain;
            }
        }


        // Effects class to loop back audio and access buffer to create a bass drone effect
        class Drone {
            constructor(audioCtx) {
                this.audioCtx = audioCtx;
                this.input = this.audioCtx.createGain();
                this.output = this.audioCtx.createGain();
                this.input.connect(this.output);
                this.delay = this.audioCtx.createDelay();
                this.delay.connect(this.output);
                // this.delay.connect(this.input);
                this.delay.delayTime.value = 3 / 4;

                this.feedback = this.audioCtx.createGain();
                this.feedback.gain.value = 0.2;
                this.delay.connect(this.feedback);
                this.input.connect(this.delay);
                this.feedback.connect(this.delay);
            }
            connect(node) {
                this.output.connect(node);
            }
            disconnect() {
                this.output.disconnect();
            }
        }



        class Reverb {
            constructor(audioCtx) {
                this.audioCtx = audioCtx;
                this.input = this.audioCtx.createGain();
                this.output = this.audioCtx.createGain();
                this.input.connect(this.output);
                this.convolver = this.audioCtx.createConvolver();
                this.convolver.buffer = this.audioCtx.createBuffer(2, audioCtx.sampleRate * 3, audioCtx.sampleRate);
                this.delay = this.audioCtx.createDelay(0.5);
                this.delay.delayTime.value = 3 / 8;
                this.wetGain = this.audioCtx.createGain();
                this.convolver.connect(this.wetGain);
                this.wetGain.connect(this.output);
                this.wetGain.gain.value = 0.82;
                this.input.connect(this.delay);
                this.delay.connect(this.convolver);
                this.input.connect(this.convolver);
                this.convolver.connect(this.output);
            }
            connect(node) {
                this.output.connect(node);
            }
            disconnect() {
                this.output.disconnect();
            }
            setWet(wet) {
                this.wetGain.gain.value = wet;
            }
            setDelayTime(delayTime) {
                this.delay.delayTime.value = delayTime;
            }
        }


        class Delay {
            constructor(audioCtx) {
                this.audioCtx = audioCtx;
                this.input = this.audioCtx.createGain();
                this.output = this.audioCtx.createGain();
                this.input.connect(this.output);
                this.delay = this.audioCtx.createDelay();
                this.delay.delayTime.value = 3 / 4;

                this.feedback = this.audioCtx.createGain();
                this.feedback.gain.value = 0.73;
                this.delay.connect(this.feedback);
                this.input.connect(this.delay);
                this.delay.connect(this.output);
                this.feedback.connect(this.input);
            }
            connect(node) {
                this.output.connect(node);
            }
            disconnect() {
                this.output.disconnect();
            }
            setDelayTime(delayTime) {
                this.delay.delayTime.value = delayTime;
            }
            setFeedback(feedback) {
                this.feedback.gain.value = feedback;
            }
        }

        class PingPongDelay2 {
            constructor(audioCtx) {
                this.audioCtx = audioCtx;
                this.input = this.audioCtx.createGain();
                this.output = this.audioCtx.createGain();
                this.input.connect(this.output);
                this.delay = this.audioCtx.createDelay();
                this.delay.delayTime.value = 3 / 4;
                this.panner1 = this.audioCtx.createStereoPanner();
                this.panner2 = this.audioCtx.createStereoPanner();
                this.feedback = this.audioCtx.createGain();
                this.feedback.gain.value = 0.8;
                this.wet = this.audioCtx.createGain();
                this.wet.gain.value = 0.89;
                this.input.connect(this.delay);
                this.delay.connect(this.panner1);
                this.panner1.connect(this.feedback);
                this.feedback.connect(this.delay);
                this.delay.connect(this.panner2);
                this.panner2.connect(this.wet);
                this.wet.connect(this.output);
                this.input.connect(this.output);
                // Auto panning effect
                this.autoPanner = this.audioCtx.createStereoPanner();
                this.autoPanner.pan.value = -1;
                this.autoPannerRate = 1; // rate of panning in Hz
                this.panner1.connect(this.autoPanner);
                this.autoPanner.connect(this.panner2);
            }
            connect(node) {
                this.output.connect(node);
            }
            disconnect() {
                this.output.disconnect();
            }
            setDelayTime(delayTime) {
                this.delay.delayTime.value = delayTime;
            }
            setAutoPannerRate(rate) {
                this.autoPannerRate = rate;
                this.autoPannerRateIncr = (2 / this.audioCtx.sampleRate) * this.autoPannerRate;
            }
            startAutoPanner() {
                const now = this.audioCtx.currentTime;
                this.autoPanner.pan.setValueAtTime(-1, now);
                this.autoPanner.pan.linearRampToValueAtTime(1, now + 1 / this.autoPannerRate);
                this.autoPanner.pan.setValueAtTime(1, now + 2 / this.autoPannerRate);
                this.autoPanner.pan.linearRampToValueAtTime(-1, now + 3 / this.autoPannerRate);
                this.autoPannerLoop = setInterval(() => {
                    const t = this.audioCtx.currentTime;
                    this.autoPanner.pan.setValueAtTime(-1, t);
                    this.autoPanner.pan.linearRampToValueAtTime(1, t + 1 / this.autoPannerRate);
                    this.autoPanner.pan.setValueAtTime(1, t + 2 / this.autoPannerRate);
                    this.autoPanner.pan.linearRampToValueAtTime(-1, t + 3 / this.autoPannerRate);
                }, 4 / this.autoPannerRate * 1000);
            }
            stopAutoPanner() {
                clearInterval(this.autoPannerLoop);
            }
            setDelayTime(delayTime) {
                this.delay.delayTime.value = delayTime;
            }
            setFeedback(feedback) {
                this.feedback.gain.value = feedback;
            }
            setWet(wet) {
                this.wet.gain.value = wet;
            }
        }



        class Filter {
            constructor(audioCtx, type, frequency, wet) {
                this.audioCtx = audioCtx;
                this.input = this.audioCtx.createGain();
                this.output = this.audioCtx.createGain();
                this.input.connect(this.output);
                this.filter = this.audioCtx.createBiquadFilter();
                this.filter.type = type;
                this.filter.frequency.value = frequency;
                // filter wet control
                this.filterWet = this.audioCtx.createGain();
                this.filterWet.gain.value = wet;
                this.input.connect(this.filterWet);
                this.filterWet.connect(this.filter);
                this.input.connect(this.filter);
                this.filter.connect(this.output);
            }

            connect(node) {
                this.output.connect(node);
            }
            disconnect() {
                this.output.disconnect();
            }
            setFrequency(frequency) {
                this.filter.frequency.value = frequency;
            }
        }

        // Flanger effect class
        class Flanger {
            constructor(audioCtx) {
                this.audioCtx = audioCtx;
                this.input = this.audioCtx.createGain();
                this.output = this.audioCtx.createGain();
                this.input.connect(this.output);
                this.delay = this.audioCtx.createDelay();
                this.delay.delayTime.value = 0.04;
                this.lfo = this.audioCtx.createOscillator();
                this.lfo.frequency.value = 0.5;
                this.lfoGain = this.audioCtx.createGain();
                this.lfoGain.gain.value = 0.2;
                this.lfo.connect(this.lfoGain);
                this.lfoGain.connect(this.delay.delayTime);
                this.lfo.start();
                this.input.connect(this.delay);
                this.delay.connect(this.output);
                this.input.connect(this.output);
            }
            connect(node) {
                this.output.connect(node);
            }
            disconnect() {
                this.output.disconnect();
            }
        }


        class Compressor {
            constructor(audioCtx) {
                this.audioCtx = audioCtx;
                this.input = this.audioCtx.createGain();
                this.output = this.audioCtx.createGain();
                this.input.connect(this.output);
                this.compressor = this.audioCtx.createDynamicsCompressor();
                this.compressor.threshold.value = -32; // -50
                this.compressor.knee.value = 30; // 40
                this.compressor.ratio.value = 12; // 12
                this.compressor.attack.value = 0.03; // 0.03
                this.compressor.release.value = 0.23; // 0.25
                this.input.connect(this.compressor);
                this.compressor.connect(this.output);

                this.makeupGain = this.audioCtx.createGain();
                this.makeupGain.gain.value = 0.1; // Adjust makeup gain to control dynamic range
                this.compressor.connect(this.makeupGain);
                this.makeupGain.connect(this.output);
            }
            connect(node) {
                this.output.connect(node);
            }
            disconnect() {
                this.output.disconnect();
            }
        }

        class FFT {
            constructor(audioCtx) {
                this.audioCtx = audioCtx;
                this.analyser = this.audioCtx.createAnalyser();
                this.analyser.fftSize = 2048;
                this.bufferLength = this.analyser.frequencyBinCount;
                this.dataArray = new Uint8Array(this.bufferLength);
            }

            connect(node) {
                this.analyser.connect(node);
            }

            disconnect() {
                this.analyser.disconnect();
            }

            getWaveform() {
                this.analyser.getByteTimeDomainData(this.dataArray);
                return this.dataArray;
            }

            getFrequencyData() {
                this.analyser.getByteFrequencyData(this.dataArray);
                return this.dataArray;
            }
            // get amplitude of the waveform
            getAmplitude() {
                const waveform = this.getWaveform();
                let sum = 0;
                for (let i = 0; i < waveform.length; i++) {
                    sum += waveform[i] / 128 - 1; // normalize the waveform data to be between -1 and 1
                }
                return Math.abs(sum / waveform.length);
            }
        }


        // function to convert music key and octave to frequency
        function noteToFrequency(note) {
            let keyNumber;
            if (note.length === 3) {
                keyNumber = notes.indexOf(note.slice(0, 2));
                keyNumber = keyNumber + (parseInt(note.slice(2)) + 1) * 12;
            } else {
                keyNumber = notes.indexOf(note.slice(0, 1));
                keyNumber = keyNumber + parseInt(note.slice(1)) * 12;
            }
            return 440 * Math.pow(2, (keyNumber - 49) / 12);
        }
        function voicing(note) {
            // randomly voicing up or down the note. up if the octave is below 5, down if the octave is above 3
            let octave = splitNoteOctave(note)[1];
            let key = splitNoteOctave(note)[0];

            // voicing direction 0 = no change, 1 = up, -1 = down
            let voicingDirection = 0;
            // randomly choose voicing direction between 0 = no change, 1 = up, -1 = down
            voicingDirection = Math.floor(Math.random() * 3) - 1;


            if (octave <= 3) {
                voicingDirection = 1;
            } else if (octave >= 5) {
                voicingDirection = -1;
            }
            const newOctave = octave + voicingDirection;
            return `${key}${newOctave}`;

        }

        const getNextChordNote = (note, nextNoteNumber) => {

            let chordScale = generateScale2(note, getKeyType(note));

            const nextNoteInScaleIndex = chordScale.indexOf(note) + nextNoteNumber - 1;
            let nextNote;

            if (typeof (chordScale[nextNoteInScaleIndex]) !== 'undefined') {
                nextNote = chordScale[nextNoteInScaleIndex];
            } else {
                nextNote = chordScale[nextNoteInScaleIndex - 7];
                //console.log(nextNote)
                if (nextNote.length === 3) {
                    const updatedOctave = parseInt(nextNote.slice(2)) + 1;
                    nextNote = `${nextNote.slice(0, 2)}${updatedOctave}`;
                } else {
                    const updatedOctave = parseInt(nextNote.slice(1)) + 1;
                    nextNote = `${nextNote.slice(0, 1)}${updatedOctave}`;
                }
            }
            return nextNote;
        }
        function generateChord(note) {

            /*
            const rootNote = note
            const thirdNote = voicing(getNextChordNote(note, 3));
            const fifthNote = voicing(getNextChordNote(note, 5));
            const seventhNote = voicing(getNextChordNote(note, 7));
            const ninethNote = voicing(getNextChordNote(note, 9));
            const chord = [rootNote, thirdNote, fifthNote, seventhNote, ninethNote];
            */

            const rootNote = note
            const thirdNote = getNextChordNote(note, 3);
            const fifthNote = getNextChordNote(note, 5);
            const seventhNote = getNextChordNote(note, 7);
            const ninethNote = getNextChordNote(note, 9);
            const chord = [rootNote, thirdNote, fifthNote, seventhNote, ninethNote];

            return chord;

        }


        // helper function to get an index from the key signature
        function getKeyListIndex(keySignature) {
            if (keySignature === 'sharp') {
                return 0;
            } else if (keySignature === 'flat') {
                return 1;
            } else {
                return 0;
            }
        }

        function generateProgression(scale, flavor) {
            let key_scale = [];
            let octave = [];
            // separate key and octave
            for (let i = 0; i < scale.length; i++) {
                key_scale.push(splitNoteOctave(scale[i])[0]);
                octave.push(splitNoteOctave(scale[i])[1]);
            }

            let keySignature = getKeySignature(key_scale[0]);
            let notesIndex = getKeyListIndex(keySignature);


            const progression2 = [];
            const circle_fifths = fifths[flavor];
            const fifthsIndex = [];
            for (let i = 0; i < key_scale.length; i++) {
                fifthsIndex.push(circle_fifths.indexOf(key_scale[i]));
            }

            for (let i = 0; i < fifthsIndex.length; i++) {
                if (fifthsIndex[i] === -1) { // if the key is not in the scale
                    //key_scale.splice(i, 1);
                    //octave.splice(i, 1);
                    fifthsIndex.splice(i, 1);
                    i--; // decrement index to account for the splice
                }
            }

            // generate the progression
            const degrees = degree; // degrees in the scale to use for chords
            const chords = progression; // chords to use for each degree
            for (let i = 0; i < degrees.length; i++) {
                const degreeIndex = degrees[i];
                const chord = chords[i];
                const key = key_scale[degreeIndex];

                const oct = octave[degreeIndex];
                progression2.push(key + oct); // add the root note
                const chordNotes = getChord(key, oct, chord, circle_fifths);

                for (let j = 0; j < chordNotes.length; j++) {
                    // add the note if it's in scal array
                    // if (scale.includes(chordNotes[j])) {
                    progression2.push(chordNotes[j]);
                    // }
                }
            }

            return progression2;
        }

        // helper function to get the note in a chord
        function getChord(root, octave, chordType, fifths) {
            const chordDegrees = getChordDegrees(chordType);
            const chord = [];
            for (let i = 0; i < chordDegrees.length; i++) {
                const degree = chordDegrees[i];
                const note = fifths[degree];
                chord.push(note + octave);
            }
            return chord;

        }

        // helper function to get the scale degrees in a chord
        function getChordDegrees(chordType) {
            switch (chordType) {
                case "I":
                    return [0, 2, 4];
                case "II":
                    return [1, 3, 5];
                case "III":
                    return [2, 4, 6];
                case "IV":
                    return [3, 5, 0];
                case "V":
                    return [4, 6, 1];
                case "VI":
                    return [5, 0, 2];
                case "VII":
                    return [6, 1, 3];
                default:
                    return [0, 2, 4]; // default to major chord
            }
        }

        function getCircularElement(arr, index) {
            const circularIndex = ((index % arr.length) + arr.length) % arr.length;
            return circularIndex;
        }

        // function to create a scale based on the root note and the scale type
        function generateScale2(rootNote, scaleType) {

            let note = splitNoteOctave(rootNote)[0];
            let octave = splitNoteOctave(rootNote)[1];

            let keySignature = getKeySignature(note);
            let notesIndex = getKeyListIndex(keySignature);


            let notes = _notes[notesIndex];

            const scale = [];
            const scaleIndex = notes.indexOf(note);
            if (scaleType === 'major') {
                scale.push(note + octave);
                scale.push(notes[getCircularElement(notes, scaleIndex + 2)] + (scaleIndex + 2 > notes.length ? (parseInt(octave) + 1) : octave));
                scale.push(notes[getCircularElement(notes, scaleIndex + 4)] + (scaleIndex + 4 > notes.length ? (parseInt(octave) + 1) : octave));
                scale.push(notes[getCircularElement(notes, scaleIndex + 5)] + (scaleIndex + 5 > notes.length ? (parseInt(octave) + 1) : octave));
                scale.push(notes[getCircularElement(notes, scaleIndex + 7)] + (scaleIndex + 7 > notes.length ? (parseInt(octave) + 1) : octave));
                scale.push(notes[getCircularElement(notes, scaleIndex + 9)] + (scaleIndex + 9 > notes.length ? (parseInt(octave) + 1) : octave));
                scale.push(notes[getCircularElement(notes, scaleIndex + 11)] + (scaleIndex + 11 > notes.length ? (parseInt(octave) + 1) : octave));
                scale.push(notes[getCircularElement(notes, scaleIndex + 12)] + (scaleIndex + 12 > notes.length ? (parseInt(octave) + 1) : octave));
            } else if (scaleType === 'minor') {
                scale.push(note + octave);
                scale.push(notes[getCircularElement(notes, scaleIndex + 2)] + (scaleIndex + 2 > notes.length ? (parseInt(octave) + 1) : octave));
                scale.push(notes[getCircularElement(notes, scaleIndex + 3)] + (scaleIndex + 3 > notes.length ? (parseInt(octave) + 1) : octave));
                scale.push(notes[getCircularElement(notes, scaleIndex + 5)] + (scaleIndex + 5 > notes.length ? (parseInt(octave) + 1) : octave));
                scale.push(notes[getCircularElement(notes, scaleIndex + 7)] + (scaleIndex + 7 > notes.length ? (parseInt(octave) + 1) : octave));
                scale.push(notes[getCircularElement(notes, scaleIndex + 8)] + (scaleIndex + 8 > notes.length ? (parseInt(octave) + 1) : octave));
                scale.push(notes[getCircularElement(notes, scaleIndex + 10)] + (scaleIndex + 10 > notes.length ? (parseInt(octave) + 1) : octave));
                scale.push(notes[getCircularElement(notes, scaleIndex + 12)] + (scaleIndex + 12 > notes.length ? (parseInt(octave) + 1) : octave));
            }
            return scale;
        }

        function getKeySignature(key) {
            const keySignatures = {
                'C': 'sharp',
                'C#': 'sharp',
                'Db': 'flat',
                'D': 'sharp',
                'D#': 'sharp',
                'Eb': 'flat',
                'E': 'sharp',
                'F': 'flat',
                'F#': 'sharp',
                'Gb': 'flat',
                'G': 'sharp',
                'G#': 'sharp',
                'Ab': 'flat',
                'A': 'sharp',
                'A#': 'sharp',
                'Bb': 'flat',
                'B': 'sharp'
            };
            return keySignatures[key];
        }

        // helper function to determine if a key is only major or minor or both
        function getKeyType(note) {

            const key = splitNoteOctave(note)[0];

            if (musicKeys[0].indexOf(key) !== -1 && musicKeys[1].indexOf(key) !== -1) {

                return flavour;
            } else if (musicKeys[0].indexOf(key) !== -1) {
                return 'major';
            } else if (musicKeys[1].indexOf(key) !== -1) {
                return 'minor';
            }
        }

        // helper function to split a note string into note and octave
        function splitNoteOctave(note) {
            let noteName, octave;
            if (note.length === 3) {
                noteName = note.slice(0, 2);
                octave = note.slice(2);
            } else {
                noteName = note.slice(0, 1);
                octave = note.slice(1);
            }
            return [noteName, octave];
        }

        // function to decrease an array of music chords by one octave (music chords are represented as an array of notes)
        function decreaseOctave(noteArray) {
            const newNoteArray = [];
            for (let i = 0; i < noteArray.length; i++) {
                const newChord = [];
                const chord = noteArray[i];
                for (let j = 0; j < chord.length; j++) {
                    let note, octave;
                    if (chord[j].length === 3) {
                        note = chord[j].slice(0, 2);
                        octave = chord[j].slice(2);


                        // if note is flat
                    } else {
                        note = chord[j].slice(0, 1);
                        octave = chord[j].slice(1);


                    }
                    newChord.push(note + (parseInt(octave) - 1));

                }
                newNoteArray.push(newChord);
            }
            return newNoteArray;
        }

        function randomizeDelay(delayTime, grooveAmount) {
            let minDelay = delayTime - (grooveAmount / 2);
            let maxDelay = delayTime + (grooveAmount / 2);
            return mathRand() * (maxDelay - minDelay) + minDelay;
        }

        function randomIndexOmit(array, omit) {
            let randomIndex;
            do {
                randomIndex = (mathRand() * array.length) | 0;
            } while (randomIndex === omit);
            return randomIndex;
        }       

    </script>

    <script id="snippet-random-code" type="text/javascript">

        // DO NOT EDIT THIS SECTION

        let seed = window.location.href.split('/').find(t => t.includes('i0'));

        if (seed == null) {
            const alphabet = "0123456789abcdefghijklmnopqrstuvwsyz";
            seed = new URLSearchParams(window.location.search).get("seed") || Array(64).fill(0).map(_ => alphabet[(Math.random() * alphabet.length) | 0]).join('') + "i0";
        } else {
            let pattern = "seed=";
            for (let i = 0; i < seed.length - pattern.length; ++i) {
                if (seed.substring(i, i + pattern.length) == pattern) {
                    seed = seed.substring(i + pattern.length);
                    break;
                }
            }
        }

        function cyrb128($) {
            let _ = 1779033703, u = 3144134277, i = 1013904242, l = 2773480762;
            for (let n = 0, r; n < $.length; n++) _ = u ^ Math.imul(_ ^ (r = $.charCodeAt(n)), 597399067), u = i ^ Math.imul(u ^ r, 2869860233), i = l ^ Math.imul(i ^ r, 951274213), l = _ ^ Math.imul(l ^ r, 2716044179);
            return _ = Math.imul(i ^ _ >>> 18, 597399067), u = Math.imul(l ^ u >>> 22, 2869860233), i = Math.imul(_ ^ i >>> 17, 951274213), l = Math.imul(u ^ l >>> 19, 2716044179), [(_ ^ u ^ i ^ l) >>> 0, (u ^ _) >>> 0, (i ^ _) >>> 0, (l ^ _) >>> 0]
        }

        function sfc32($, _, u, i) {
            return function () {
                u >>>= 0, i >>>= 0;
                var l = ($ >>>= 0) + (_ >>>= 0) | 0;
                return $ = _ ^ _ >>> 9, _ = u + (u << 3) | 0, u = (u = u << 21 | u >>> 11) + (l = l + (i = i + 1 | 0) | 0) | 0, (l >>> 0) / 4294967296
            }
        }


        let mathRand = sfc32(...cyrb128(seed));

    </script>
    <style>
        body {
            background-color: #FFFFFF;
            padding: 0;
            margin: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
        }

        canvas {
            position: absolute;
            /*Can also be `fixed`*/
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
            margin: auto;
            /*
            border-radius: 5px;
            box-shadow: -3px -2px rgba(0, 0, 0, 0.1), 5px 3px 3px rgba(0, 0, 0, 0.2); */
        }
    </style>
</head>

<body>

</body>

</html>
likes 0comments 0
Meshhtml
Network Dataviz
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Afrho-Pod Hypergraph Visualization</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            overflow: hidden;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #0a0e27 0%, #1a1f3a 100%);
            color: white;
        }
        
        #container {
            position: absolute;
            width: 100%;
            height: 100%;
            overflow: hidden;
        }
        
        #info {
            position: absolute;
            top: 10px;
            left: 10px;
            background: rgba(0, 0, 0, 0.7);
            padding: 10px 20px;
            border-radius: 8px;
            font-size: 14px;
            z-index: 100;
        }
        
        #controls {
            position: absolute;
            top: 10px;
            right: 10px;
            background: rgba(0, 0, 0, 0.7);
            padding: 15px;
            border-radius: 8px;
            z-index: 100;
        }
        
        .control-group {
            margin-bottom: 10px;
        }
        
        .control-group label {
            display: block;
            margin-bottom: 5px;
            font-size: 12px;
            color: #ccc;
        }
        
        button {
            background: #4a6bff;
            color: white;
            border: none;
            padding: 8px 15px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            margin-right: 5px;
            transition: background 0.3s;
        }
        
        button:hover {
            background: #3a5bef;
        }
        
        select, input {
            background: rgba(255, 255, 255, 0.1);
            color: white;
            border: 1px solid rgba(255, 255, 255, 0.2);
            padding: 8px;
            border-radius: 4px;
            font-size: 12px;
            width: 100%;
        }
        
        #stats {
            position: absolute;
            bottom: 10px;
            left: 10px;
            background: rgba(0, 0, 0, 0.7);
            padding: 10px 20px;
            border-radius: 8px;
            font-size: 12px;
            z-index: 100;
        }
        
        #node-info {
            position: absolute;
            bottom: 10px;
            right: 10px;
            background: rgba(0, 0, 0, 0.8);
            padding: 15px;
            border-radius: 8px;
            font-size: 12px;
            max-width: 300px;
            display: none;
            z-index: 100;
        }
        
        .loading {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: #4a6bff;
            font-size: 18px;
            z-index: 100;
        }
        
        .metric-card {
            background: rgba(0, 0, 0, 0.5);
            padding: 10px;
            border-radius: 6px;
            margin: 5px 0;
        }
        
        .metric-label {
            font-size: 10px;
            color: #aaa;
            margin-bottom: 3px;
        }
        
        .metric-value {
            font-size: 16px;
            font-weight: bold;
        }
    </style>
</head>
<body>
    <div id="loading" class="loading">Loading hypergraph visualization...</div>
    
    <div id="info">
        <strong>Afrho-Pod Hypergraph Visualization</strong>
        <div>Interactive 3D network topology explorer</div>
    </div>
    
    <div id="controls">
        <div class="control-group">
            <label>Visualization Mode</label>
            <select id="mode-select">
                <option value="force">Force-Directed Graph</option>
                <option value="hypergraph">Hypergraph</option>
                <option value="3d">3D Network</option>
                <option value="organic">Organic Art</option>
            </select>
        </div>
        
        <div class="control-group">
            <label>Node Type Filter</label>
            <select id="node-filter">
                <option value="all">All Nodes</option>
                <option value="server">Servers</option>
                <option value="pod">Pods</option>
                <option value="device">Devices</option>
                <option value="vpn">VPN</option>
            </select>
        </div>
        
        <div class="control-group">
            <button id="refresh-btn">Refresh Data</button>
            <button id="reset-btn">Reset View</button>
        </div>
        
        <div class="control-group">
            <label>Metrics</label>
            <button id="toggle-metrics">Show/Hide</button>
        </div>
    </div>
    
    <div id="stats">
        <div class="metric-card">
            <div class="metric-label">Total Nodes</div>
            <div class="metric-value" id="node-count">0</div>
        </div>
        <div class="metric-card">
            <div class="metric-label">Total Edges</div>
            <div class="metric-value" id="edge-count">0</div>
        </div>
        <div class="metric-card">
            <div class="metric-label">Active Connections</div>
            <div class="metric-value" id="active-connections">0</div>
        </div>
    </div>
    
    <div id="node-info">
        <h3 id="node-title">Node Information</h3>
        <div id="node-details"></div>
    </div>
    
    <div id="container"></div>
    
    <!-- Three.js and other libraries -->
    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/TrackballControls.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/GLTFLoader.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/geometries/ConvexGeometry.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/geometries/ConvexBufferGeometry.js"></script>
    
    <!-- D3.js for force-directed graph -->
    <script src="https://d3js.org/d3.v7.min.js"></script>
    
    <script>
        // Global variables
        let scene, camera, renderer, controls;
        let nodes = [], edges = [], nodeObjects = [], edgeObjects = [];
        let selectedNode = null;
        let visualizationMode = 'force';
        let nodeFilter = 'all';
        
        // Network data - this would be fetched from API in real implementation
        let networkData = {
            nodes: [],
            edges: []
        };
        
        // Color schemes
        const colorSchemes = {
            server: 0x4a6bff,
            pod: 0x28a745,
            device: 0xffc107,
            vpn: 0xdc3545,
            container: 0x6a5acd,
            vm: 0x17a2b8
        };
        
        // Initialize the scene
        function init() {
            // Create scene
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x0a0e27);
            
            // Create camera
            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
            camera.position.set(0, 0, 1000);
            
            // Create renderer
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(window.devicePixelRatio);
            document.getElementById('container').appendChild(renderer.domElement);
            
            // Create controls
            controls = new THREE.OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true;
            controls.dampingFactor = 0.05;
            controls.screenSpacePanning = false;
            controls.minDistance = 100;
            controls.maxDistance = 5000;
            
            // Add lights
            const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
            scene.add(ambientLight);
            
            const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
            directionalLight.position.set(1, 1, 1);
            scene.add(directionalLight);
            
            // Add grid helper
            const gridHelper = new THREE.GridHelper(2000, 20, 0x333333, 0x333333);
            scene.add(gridHelper);
            
            // Add axes helper
            const axesHelper = new THREE.AxesHelper(500);
            scene.add(axesHelper);
            
            // Load sample data
            loadSampleData();
            
            // Set up event listeners
            setupEventListeners();
            
            // Start animation loop
            animate();
            
            // Hide loading
            document.getElementById('loading').style.display = 'none';
        }
        
        // Load sample data (in real implementation, this would be fetched from API)
        function loadSampleData() {
            // Sample network topology
            networkData = {
                nodes: [
                    { id: 1, name: 'rhiz-ueth', type: 'server', x: 0, y: 0, z: 0, size: 30, color: colorSchemes.server },
                    { id: 2, name: 'afrho-pod', type: 'pod', x: 200, y: 100, z: 0, size: 25, color: colorSchemes.pod },
                    { id: 3, name: 'exosystem', type: 'server', x: -200, y: 100, z: 0, size: 20, color: colorSchemes.server },
                    { id: 4, name: 'wg0', type: 'vpn', x: 100, y: -100, z: 0, size: 15, color: colorSchemes.vpn },
                    { id: 5, name: 'operatau-sozv', type: 'vpn', x: -100, y: -100, z: 0, size: 15, color: colorSchemes.vpn },
                    { id: 6, name: 'limesdr', type: 'device', x: 300, y: 200, z: 0, size: 10, color: colorSchemes.device },
                    { id: 7, name: 'arduino', type: 'device', x: 350, y: -50, z: 0, size: 10, color: colorSchemes.device },
                    { id: 8, name: 'yubikey', type: 'device', x: 250, y: -150, z: 0, size: 10, color: colorSchemes.device }
                ],
                edges: [
                    { source: 1, target: 2, type: 'vpn', strength: 0.5 },
                    { source: 1, target: 3, type: 'network', strength: 0.3 },
                    { source: 1, target: 4, type: 'vpn', strength: 0.4 },
                    { source: 1, target: 5, type: 'vpn', strength: 0.4 },
                    { source: 2, target: 4, type: 'vpn', strength: 0.2 },
                    { source: 2, target: 5, type: 'vpn', strength: 0.2 },
                    { source: 2, target: 6, type: 'usb', strength: 0.1 },
                    { source: 2, target: 7, type: 'usb', strength: 0.1 },
                    { source: 2, target: 8, type: 'usb', strength: 0.1 }
                ]
            };
            
            // Update stats
            updateStats();
            
            // Create visualization based on current mode
            updateVisualization();
        }
        
        // Update statistics display
        function updateStats() {
            document.getElementById('node-count').textContent = networkData.nodes.length;
            document.getElementById('edge-count').textContent = networkData.edges.length;
            document.getElementById('active-connections').textContent = networkData.edges.length;
        }
        
        // Update visualization based on current mode
        function updateVisualization() {
            // Clear existing objects
            clearScene();
            
            if (visualizationMode === 'force') {
                createForceDirectedGraph();
            } else if (visualizationMode === 'hypergraph') {
                createHypergraph();
            } else if (visualizationMode === '3d') {
                create3DNetwork();
            } else if (visualizationMode === 'organic') {
                createOrganicArt();
            }
        }
        
        // Clear the scene
        function clearScene() {
            // Remove all node and edge objects
            nodeObjects.forEach(obj => scene.remove(obj));
            edgeObjects.forEach(obj => scene.remove(obj));
            
            nodeObjects = [];
            edgeObjects = [];
        }
        
        // Create force-directed graph visualization
        function createForceDirectedGraph() {
            // Create nodes
            networkData.nodes.forEach(node => {
                if (nodeFilter === 'all' || node.type === nodeFilter) {
                    const geometry = new THREE.SphereGeometry(node.size, 32, 32);
                    const material = new THREE.MeshPhongMaterial({
                        color: node.color,
                        shininess: 30,
                        transparent: true,
                        opacity: 0.8
                    });
                    
                    const sphere = new THREE.Mesh(geometry, material);
                    sphere.position.set(node.x, node.y, node.z);
                    sphere.userData = node;
                    
                    scene.add(sphere);
                    nodeObjects.push(sphere);
                }
            });
            
            // Create edges
            networkData.edges.forEach(edge => {
                const sourceNode = networkData.nodes.find(n => n.id === edge.source);
                const targetNode = networkData.nodes.find(n => n.id === edge.target);
                
                if (sourceNode && targetNode) {
                    const sourceObj = nodeObjects.find(obj => obj.userData.id === edge.source);
                    const targetObj = nodeObjects.find(obj => obj.userData.id === edge.target);
                    
                    if (sourceObj && targetObj) {
                        const material = new THREE.LineBasicMaterial({
                            color: 0x666666,
                            linewidth: edge.strength * 5,
                            transparent: true,
                            opacity: 0.6
                        });
                        
                        const geometry = new THREE.BufferGeometry();
                        const positions = [
                            sourceObj.position.x, sourceObj.position.y, sourceObj.position.z,
                            targetObj.position.x, targetObj.position.y, targetObj.position.z
                        ];
                        
                        geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
                        const line = new THREE.Line(geometry, material);
                        
                        scene.add(line);
                        edgeObjects.push(line);
                    }
                }
            });
            
            // Add labels
            addLabels();
        }
        
        // Create hypergraph visualization
        function createHypergraph() {
            // This would create a more complex hypergraph visualization
            // For now, we'll use a similar approach to force-directed but with different styling
            createForceDirectedGraph();
        }
        
        // Create 3D network visualization
        function create3DNetwork() {
            // Create nodes in 3D space
            networkData.nodes.forEach((node, index) => {
                if (nodeFilter === 'all' || node.type === nodeFilter) {
                    const geometry = new THREE.SphereGeometry(node.size, 32, 32);
                    const material = new THREE.MeshPhongMaterial({
                        color: node.color,
                        shininess: 30
                    });
                    
                    const sphere = new THREE.Mesh(geometry, material);
                    
                    // Position nodes in 3D space
                    const angle = (index / networkData.nodes.length) * Math.PI * 2;
                    const radius = 500;
                    sphere.position.set(
                        Math.cos(angle) * radius,
                        (Math.random() - 0.5) * 200,
                        Math.sin(angle) * radius
                    );
                    
                    sphere.userData = node;
                    scene.add(sphere);
                    nodeObjects.push(sphere);
                }
            });
            
            // Create curved edges
            networkData.edges.forEach(edge => {
                const sourceNode = networkData.nodes.find(n => n.id === edge.source);
                const targetNode = networkData.nodes.find(n => n.id === edge.target);
                
                if (sourceNode && targetNode) {
                    const sourceObj = nodeObjects.find(obj => obj.userData.id === edge.source);
                    const targetObj = nodeObjects.find(obj => obj.userData.id === edge.target);
                    
                    if (sourceObj && targetObj) {
                        // Create a curved line
                        const curve = new THREE.QuadraticBezierCurve3(
                            sourceObj.position,
                            new THREE.Vector3(
                                (sourceObj.position.x + targetObj.position.x) / 2,
                                (sourceObj.position.y + targetObj.position.y) / 2 + 200,
                                (sourceObj.position.z + targetObj.position.z) / 2
                            ),
                            targetObj.position
                        );
                        
                        const points = curve.getPoints(50);
                        const geometry = new THREE.BufferGeometry().setFromPoints(points);
                        
                        const material = new THREE.LineBasicMaterial({
                            color: 0x666666,
                            linewidth: edge.strength * 3
                        });
                        
                        const curveObject = new THREE.Line(geometry, material);
                        scene.add(curveObject);
                        edgeObjects.push(curveObject);
                    }
                }
            });
            
            // Add labels
            addLabels();
        }
        
        // Create organic art visualization
        function createOrganicArt() {
            // This would create a more artistic, organic visualization
            // For now, we'll create a simple particle system
            
            // Create a particle system for nodes
            const particleCount = networkData.nodes.length * 10;
            const particles = new THREE.BufferGeometry();
            const positions = [];
            const colors = [];
            const sizes = [];
            
            networkData.nodes.forEach(node => {
                if (nodeFilter === 'all' || node.type === nodeFilter) {
                    for (let i = 0; i < 10; i++) {
                        const angle = Math.random() * Math.PI * 2;
                        const distance = Math.random() * node.size * 2;
                        
                        positions.push(
                            node.x + Math.cos(angle) * distance,
                            node.y + Math.sin(angle) * distance,
                            node.z + (Math.random() - 0.5) * node.size
                        );
                        
                        colors.push(
                            (node.color >> 16) / 255,
                            ((node.color >> 8) & 0xFF) / 255,
                            (node.color & 0xFF) / 255
                        );
                        
                        sizes.push(Math.random() * 5 + 2);
                    }
                }
            });
            
            particles.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
            particles.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
            particles.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1));
            
            const particleMaterial = new THREE.PointsMaterial({
                size: 5,
                vertexColors: true,
                transparent: true,
                opacity: 0.8
            });
            
            const particleSystem = new THREE.Points(particles, particleMaterial);
            scene.add(particleSystem);
            nodeObjects.push(particleSystem);
            
            // Add some organic-looking connections
            networkData.edges.forEach(edge => {
                const sourceNode = networkData.nodes.find(n => n.id === edge.source);
                const targetNode = networkData.nodes.find(n => n.id === edge.target);
                
                if (sourceNode && targetNode) {
                    const sourcePos = new THREE.Vector3(sourceNode.x, sourceNode.y, sourceNode.z);
                    const targetPos = new THREE.Vector3(targetNode.x, targetNode.y, targetNode.z);
                    
                    // Create a sine wave connection
                    const points = [];
                    const segments = 50;
                    
                    for (let i = 0; i <= segments; i++) {
                        const t = i / segments;
                        const x = sourcePos.x + (targetPos.x - sourcePos.x) * t;
                        const y = sourcePos.y + (targetPos.y - sourcePos.y) * t + Math.sin(t * Math.PI * 2) * 50;
                        const z = sourcePos.z + (targetPos.z - sourcePos.z) * t;
                        
                        points.push(new THREE.Vector3(x, y, z));
                    }
                    
                    const geometry = new THREE.BufferGeometry().setFromPoints(points);
                    const material = new THREE.LineBasicMaterial({
                        color: 0x666666,
                        linewidth: edge.strength * 2,
                        transparent: true,
                        opacity: 0.5
                    });
                    
                    const line = new THREE.Line(geometry, material);
                    scene.add(line);
                    edgeObjects.push(line);
                }
            });
        }
        
        // Add labels to nodes
        function addLabels() {
            // Remove existing labels
            const existingLabels = scene.children.filter(obj => obj.userData && obj.userData.type === 'label');
            existingLabels.forEach(label => scene.remove(label));
            
            // Add new labels
            nodeObjects.forEach(nodeObj => {
                if (nodeObj.userData) {
                    const canvas = document.createElement('canvas');
                    const context = canvas.getContext('2d');
                    const fontSize = 24;
                    
                    context.font = `${fontSize}px Arial`;
                    const textWidth = context.measureText(nodeObj.userData.name).width;
                    
                    canvas.width = textWidth + 20;
                    canvas.height = fontSize + 20;
                    
                    context.fillStyle = 'rgba(0, 0, 0, 0.7)';
                    context.fillRect(0, 0, canvas.width, canvas.height);
                    
                    context.fillStyle = 'white';
                    context.font = `${fontSize}px Arial`;
                    context.textAlign = 'center';
                    context.textBaseline = 'middle';
                    context.fillText(nodeObj.userData.name, canvas.width / 2, canvas.height / 2);
                    
                    const texture = new THREE.CanvasTexture(canvas);
                    const spriteMaterial = new THREE.SpriteMaterial({ map: texture });
                    const sprite = new THREE.Sprite(spriteMaterial);
                    
                    sprite.position.copy(nodeObj.position);
                    sprite.position.y += nodeObj.userData.size + 20;
                    sprite.userData = { type: 'label', nodeId: nodeObj.userData.id };
                    
                    scene.add(sprite);
                }
            });
        }
        
        // Set up event listeners
        function setupEventListeners() {
            // Mode selection
            document.getElementById('mode-select').addEventListener('change', function() {
                visualizationMode = this.value;
                updateVisualization();
            });
            
            // Node filter
            document.getElementById('node-filter').addEventListener('change', function() {
                nodeFilter = this.value;
                updateVisualization();
            });
            
            // Refresh button
            document.getElementById('refresh-btn').addEventListener('click', function() {
                // In real implementation, this would fetch new data from API
                console.log('Refreshing data...');
                updateVisualization();
            });
            
            // Reset button
            document.getElementById('reset-btn').addEventListener('click', function() {
                controls.reset();
            });
            
            // Window resize
            window.addEventListener('resize', function() {
                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(window.innerWidth, window.innerHeight);
            });
            
            // Node click events
            window.addEventListener('click', function(event) {
                const mouse = new THREE.Vector2();
                mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
                mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
                
                const raycaster = new THREE.Raycaster();
                raycaster.setFromCamera(mouse, camera);
                
                const intersects = raycaster.intersectObjects(nodeObjects.filter(obj => obj.type === 'Mesh'));
                
                if (intersects.length > 0) {
                    const selected = intersects[0].object;
                    showNodeInfo(selected.userData);
                } else {
                    hideNodeInfo();
                }
            });
        }
        
        // Show node information
        function showNodeInfo(node) {
            selectedNode = node;
            
            const infoElement = document.getElementById('node-info');
            const titleElement = document.getElementById('node-title');
            const detailsElement = document.getElementById('node-details');
            
            titleElement.textContent = node.name;
            
            let detailsHTML = `
                <div><strong>Type:</strong> ${node.type}</div>
                <div><strong>ID:</strong> ${node.id}</div>
                <div><strong>Position:</strong> (${node.x.toFixed(1)}, ${node.y.toFixed(1)}, ${node.z.toFixed(1)})</div>
                <div><strong>Size:</strong> ${node.size}</div>
            `;
            
            // Add type-specific information
            if (node.type === 'server') {
                detailsHTML += '<div><strong>Role:</strong> Main Server</div>';
            } else if (node.type === 'pod') {
                detailsHTML += '<div><strong>Role:</strong> IoT Gateway</div>';
            } else if (node.type === 'device') {
                detailsHTML += '<div><strong>Role:</strong> Peripheral Device</div>';
            } else if (node.type === 'vpn') {
                detailsHTML += '<div><strong>Role:</strong> VPN Interface</div>';
            }
            
            detailsElement.innerHTML = detailsHTML;
            infoElement.style.display = 'block';
        }
        
        // Hide node information
        function hideNodeInfo() {
            selectedNode = null;
            document.getElementById('node-info').style.display = 'none';
        }
        
        // Animation loop
        function animate() {
            requestAnimationFrame(animate);
            
            // Update controls
            controls.update();
            
            // Rotate nodes slightly for organic feel
            if (visualizationMode === 'organic') {
                nodeObjects.forEach(node => {
                    if (node.rotation) {
                        node.rotation.x += 0.001;
                        node.rotation.y += 0.002;
                    }
                });
            }
            
            // Render scene
            renderer.render(scene, camera);
        }
        
        // Initialize the visualization
        init();
    </script>
</body>
</html>
likes 0comments 0
Amplitude Timelinehtml
Data Viz
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Multidimensional Timeline Calendar</title>
    <!-- Bootstrap 5 CDN -->
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
    />
    <style>
      :root {
        /* category colours (extend as needed) */
        --climate: #86d06d;
        --astronomy: #63a2ff;
        --culture: #ff66aa;
        --mythic: #f9bf3b;
        --economy: #ff8763;
      }
      html,
      body {
        height: 100%;
        margin: 0;
        font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica,
          Arial, sans-serif;
      }
      #app {
        height: 100%;
        display: flex;
        flex-direction: column;
      }
      /* controls */
      #controls {
        background: #212123;
        color: #f8f9fa;
      }
      #timelineWrapper {
        flex: 1 1 auto;
        overflow-y: auto;
        position: relative;
        background: #f7f0e6;
      }
      /* svg timeline */
      svg.timeline-svg {
        position: absolute;
        left: 0;
        top: 0;
      }
      .timeline-base {
        stroke: #212123;
        stroke-width: 2px;
      }
      .event-circle {
        stroke: #212123;
        stroke-width: 1;
        fill-opacity: 0.6;
        transition: r 0.2s ease, fill-opacity 0.2s ease;
        cursor: pointer;
      }
      .event-circle:hover {
        r: calc(var(--r) + 8px);
        fill-opacity: 1;
      }
      .event-label {
        font-size: 0.85rem;
        fill: #212123;
        pointer-events: none;
        user-select: none;
      }
      .event-date {
        font-size: 0.75rem;
        fill: #666;
        pointer-events: none;
        user-select: none;
      }
      /* colour mapping for categories */
      .cat-astronomy {
        fill: var(--astronomy);
      }
      .cat-climate {
        fill: var(--climate);
      }
      .cat-culture {
        fill: var(--culture);
      }
      .cat-mythic {
        fill: var(--mythic);
      }
      .cat-economy {
        fill: var(--economy);
      }
    </style>
  </head>
  <body>
    <div id="app">
      <!-- CONTROLS -->
      <nav id="controls" class="navbar navbar-dark px-3 py-2">
        <div class="navbar-brand">Multidimensional Timeline</div>
        <form class="d-flex gap-3 flex-wrap" role="search" id="controlForm">
          <select id="calendarSelect" class="form-select form-select-sm">
            <option value="gregorian">Gregorian</option>
            <option value="julian">Julian</option>
            <option value="mayan">Mayan Long Count</option>
            <option value="chinese">Chinese Zodiac</option>
            <option value="indian">Indian Lunisolar</option>
            <option value="aztec">Aztec Tonalpohualli</option>
            <option value="nazca">Nazca Horizon</option>
            <option value="abstract">Planetary Abstract</option>
          </select>
          <div class="form-check form-switch text-nowrap">
            <input
              class="form-check-input"
              type="checkbox"
              role="switch"
              id="liveDataSwitch"
            />
            <label class="form-check-label" for="liveDataSwitch"
              >Live Celestial Data</label
            >
          </div>
          <div class="form-check form-switch text-nowrap">
            <input
              class="form-check-input"
              type="checkbox"
              role="switch"
              id="fictionSwitch"
              checked
            />
            <label class="form-check-label" for="fictionSwitch"
              >Mythic &amp; Fictional</label
            >
          </div>
        </form>
      </nav>

      <!-- TIMELINE WRAPPER -->
      <div id="timelineWrapper">
        <svg class="timeline-svg" id="timelineSvg"></svg>
      </div>
    </div>

    <!-- Optionally, Bootstrap JS (popper included) -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

    <script>
      /* =============================================================
       *  Multidimensional Timeline Calendar – Vanilla JS (no libs)
       *  Author: ChatGPT o3, 2025
       *  =========================================================== */

      const SVG_NS = "http://www.w3.org/2000/svg";

      /* Utility: date helpers */
      function iso(date) {
        return date.toISOString().split("T")[0];
      }

      // Julian = Gregorian - 13 days (valid 1900‑2100 for demo)
      function toJulian(date) {
        const jul = new Date(date);
        jul.setDate(jul.getDate() - 13);
        return iso(jul);
      }

      // Stub converters (placeholders) – return symbolic strings
      function toMayanLongCount(date) {
        // Very rough: days since 2025‑06‑13 → add offset to 13.0.0.0.0 (Dec 21, 2012)
        const base = new Date("2012-12-21");
        const diff = Math.floor((date - base) / 864e5);
        return `13.${Math.floor(diff / 144000)}.${Math.floor((diff % 144000) / 7200)}.${Math.floor((diff % 7200) / 360)}.${diff % 360}`;
      }

      function toChineseZodiac(date) {
        const animals = [
          "Rat",
          "Ox",
          "Tiger",
          "Rabbit",
          "Dragon",
          "Snake",
          "Horse",
          "Goat",
          "Monkey",
          "Rooster",
          "Dog",
          "Pig",
        ];
        const baseYear = 1900; // Rat
        const idx = (date.getFullYear() - baseYear) % 12;
        return `${animals[idx]} Year`;
      }

      function toIndianLunisolar(date) {
        return `Paksha ${date.getDate() % 15}, Month ${(date.getMonth() + 1)}`;
      }

      function toAztec(date) {
        return `Tonalpohualli ${(date.getTime() / 864e5) % 260 | 0}`;
      }

      function toNazca(date) {
        return `Solstice offset ${(Math.abs(date - new Date(date.getFullYear(), 5, 21)) / 864e5).toFixed(0)}d`;
      }

      function toAbstract(date) {
        const k101start = new Date("2025-06-13");
        const days = Math.floor((date - k101start) / 864e5);
        return `K101+${days}`;
      }

      function convertDate(date, calendar) {
        switch (calendar) {
          case "julian":
            return toJulian(date);
          case "mayan":
            return toMayanLongCount(date);
          case "chinese":
            return toChineseZodiac(date);
          case "indian":
            return toIndianLunisolar(date);
          case "aztec":
            return toAztec(date);
          case "nazca":
            return toNazca(date);
          case "abstract":
            return toAbstract(date);
          default:
            return iso(date);
        }
      }

      /* Event generation (demo)  */
      const BASE_EVENTS = [
        {
          id: 1,
          date: new Date("2025-06-13"),
          label: "Friday 13 // K101 Era begins",
          category: "culture",
          intensity: 0.8,
        },
        {
          id: 2,
          date: new Date("2025-07-03"),
          label: "Earth Aphelion",
          category: "astronomy",
          intensity: 0.7,
        },
        {
          id: 3,
          date: new Date("2026-02-13"),
          label: "Super El Niño Peak",
          category: "climate",
          intensity: 0.9,
        },
      ];

      // Generate placeholder events year +/- range (fictional & real)
      function generateSyntheticEvents(yearsBack = 50, yearsForward = 50) {
        const synthetic = [];
        const categories = ["astronomy", "climate", "culture", "mythic", "economy"];
        const now = new Date();
        const startYear = now.getFullYear() - yearsBack;
        const endYear = now.getFullYear() + yearsForward;
        let id = 1000;
        for (let y = startYear; y <= endYear; y++) {
          // couple per year
          const count = Math.random() * 3 + 1;
          for (let i = 0; i < count; i++) {
            const d = new Date(y, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1);
            const cat = categories[(Math.random() * categories.length) | 0];
            synthetic.push({
              id: id++,
              date: d,
              label: `${cat.charAt(0).toUpperCase() + cat.slice(1)} Event`,
              category: cat,
              intensity: Math.random(),
            });
          }
        }
        return synthetic;
      }

      const EVENTS = [...BASE_EVENTS, ...generateSyntheticEvents()];

      /* SVG Timeline rendering */
      const timelineWrapper = document.getElementById("timelineWrapper");
      const svg = document.getElementById("timelineSvg");

      // timeline dimensions will be recalculated on resize/scroll
      let timelineX = 120;
      const marginTop = 40;
      const marginBottom = 40;
      const circleRadiusMin = 6;
      const circleRadiusMax = 28;

      // scale helpers
      function linearScale(domainMin, domainMax, rangeMin, rangeMax) {
        return (val) => {
          const ratio = (val - domainMin) / (domainMax - domainMin || 1);
          return rangeMin + ratio * (rangeMax - rangeMin);
        };
      }

      function renderTimeline() {
        // Clean svg
        svg.innerHTML = "";

        const wrapperRect = timelineWrapper.getBoundingClientRect();
        const width = wrapperRect.width;
        const height = EVENTS.length * 80 + marginTop + marginBottom; // rough initial height
        svg.setAttribute("width", width);
        svg.setAttribute("height", height);

        // baseline
        const baseLine = document.createElementNS(SVG_NS, "line");
        baseLine.setAttribute("x1", timelineX);
        baseLine.setAttribute("y1", 0);
        baseLine.setAttribute("x2", timelineX);
        baseLine.setAttribute("y2", height);
        baseLine.setAttribute("class", "timeline-base");
        svg.appendChild(baseLine);

        // domain (time) – earliest to latest
        const minDate = EVENTS.reduce((a, b) => (a < b.date ? a : b.date), EVENTS[0].date);
        const maxDate = EVENTS.reduce((a, b) => (a > b.date ? a : b.date), EVENTS[0].date);
        const domainMin = minDate.getTime();
        const domainMax = maxDate.getTime();
        const scaleY = linearScale(domainMin, domainMax, marginTop, height - marginBottom);

        // radius scale – intensity based
        const scaleR = linearScale(0, 1, circleRadiusMin, circleRadiusMax);

        // calendar conversion
        const calendarType = document.getElementById("calendarSelect").value;

        EVENTS.forEach((ev, index) => {
          const y = scaleY(ev.date.getTime());
          const r = scaleR(ev.intensity);

          // group
          const g = document.createElementNS(SVG_NS, "g");

          // circle
          const circle = document.createElementNS(SVG_NS, "circle");
          circle.setAttribute("cx", timelineX);
          circle.setAttribute("cy", y);
          circle.setAttribute("r", r);
          circle.setAttribute("class", `event-circle cat-${ev.category}`);
          circle.style.setProperty("--r", r + "px");

          // label
          const label = document.createElementNS(SVG_NS, "text");
          label.setAttribute("x", timelineX + r + 12);
          label.setAttribute("y", y + 4);
          label.setAttribute("class", "event-label");
          label.textContent = ev.label;

          // date
          const dateTxt = document.createElementNS(SVG_NS, "text");
          dateTxt.setAttribute("x", timelineX - r - 12);
          dateTxt.setAttribute("y", y + 4);
          dateTxt.setAttribute("text-anchor", "end");
          dateTxt.setAttribute("class", "event-date");
          dateTxt.textContent = convertDate(ev.date, calendarType);

          // tooltip (simple using title)
          circle.setAttribute("title", `${ev.label}\n${convertDate(ev.date, calendarType)}`);

          g.appendChild(circle);
          g.appendChild(label);
          g.appendChild(dateTxt);
          svg.appendChild(g);
        });
      }

      // UI interactions
      document.getElementById("calendarSelect").addEventListener("change", renderTimeline);
      document.getElementById("fictionSwitch").addEventListener("change", (e) => {
        if (e.target.checked) {
          // ensure mythic are visible
          EVENTS.forEach((ev) => {
            if (ev.category === "mythic") ev.hidden = false;
          });
        } else {
          EVENTS.forEach((ev) => {
            if (ev.category === "mythic") ev.hidden = true;
          });
        }
        renderTimeline();
      });

      // Live data fetch (placeholder)
      document.getElementById("liveDataSwitch").addEventListener("change", async (e) => {
        if (e.target.checked) {
          // Example: fetch today’s NASA APOD date and add to timeline
          try {
            const res = await fetch(
              "https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY"
            );
            const json = await res.json();
            EVENTS.push({
              id: Date.now(),
              date: new Date(json.date),
              label: `NASA APOD: ${json.title}`,
              category: "astronomy",
              intensity: 0.5,
            });
            renderTimeline();
          } catch (err) {
            console.error("API error", err);
          }
        }
      });

      // initial render
      renderTimeline();

      /* Infinite scroll: if near bottom, add more future events  */
      timelineWrapper.addEventListener("scroll", () => {
        const threshold = 300; // px from bottom
        if (
          timelineWrapper.scrollTop + timelineWrapper.clientHeight + threshold >=
          timelineWrapper.scrollHeight
        ) {
          // generate +10 future years events
          const futureEvents = generateSyntheticEvents(0, 10);
          EVENTS.push(...futureEvents);
          renderTimeline();
        }
      });

      // responsive resize
      window.addEventListener("resize", renderTimeline);
    </script>
  </body>
</html>
likes 0comments 0
Bubble Charthtml
Data viz model
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Bubble Chart – Native JS + Canvas + RK4 + Clusters</title>
  <style>
    html, body {
      margin: 0;
      padding: 0;
      width: 100%;
      height: 100%;
      background: #fff;
      overflow: hidden;
      font-family: sans-serif;
    }
    canvas { display: block; }
  </style>
</head>
<body>
<canvas id="bubbleCanvas"></canvas>
<script>
// ------------------------------
// 1. CSV Data and Parser
// ------------------------------
const csvData = `id,groupid,size,name
1,1,9080,α
2,9,4610,β
3,2,3810,γ
4,1,2990,δ
5,1,2820,ε
6,3,2430,ζ
7,6,2400,η
8,3,2090,θ
9,3,1580,ι
10,7,1290,κ
11,10,1230,λ
12,1,1210,μ
13,3,829,ν
14,3,768,ξ
15,1,745,ο
16,3,651,π
17,3,589,ρ
18,1,569,σ
19,2,502,τ
20,3,441,υ
21,2,425,φ
22,5,388,χ
23,11,378,ψ
24,11,373,ω
25,3,369,1
26,12,364,2
27,15,359,3
28,16,349,4
29,16,340,5
30,3,338,6
31,16,330,7
32,13,306,8
33,12,301,9
34,12,283,0
35,1,268
36,3,268
37,17,266
38,17,264
39,3,262
40,3,256
41,5,243
42,11,237
43,13,223
44,12,222
45,10,220
46,99,220
47,1,212
48,19,201
49,19,193
50,3,190
51,1,189
52,3,188
53,1,186
54,1,179
55,3,179
56,16,174
57,5,172
58,3,165
59,6,165
60,3,164
61,1,163
62,1,157
63,3,149
64,2,147
65,3,145
66,10,142
67,11,138
68,3,128
69,3,120
70,2,97`;

function parseCSV(str) {
  const lines = str.trim().split(/\r?\n/);
  const header = lines[0].split(",");
  const data = [];
  for (let i = 1; i < lines.length; i++) {
    const row = lines[i].split(",");
    const obj = {};
    for (let j = 0; j < header.length; j++) {
      const key = header[j].trim();
      obj[key] = row[j] !== undefined ? row[j].trim() : "";
    }
    data.push(obj);
  }
  return data;
}

// ------------------------------
// 2. Utility Functions
// ------------------------------
function mapRange(value, inMin, inMax, outMin, outMax) {
  return outMin + (outMax - outMin) * ((value - inMin) / (inMax - inMin));
}

// ------------------------------
// 3. Bubble Class
// ------------------------------
class Bubble {
  constructor(d, radiusScale) {
    this.id = +d.id;
    this.groupid = d.groupid;
    this.size = +d.size;
    this.name = d.name || "";
    this.radius = radiusScale(this.size);
    // When dragging, fx and fy will hold fixed positions.
    this.fx = null;
    this.fy = null;
  }
}

// ------------------------------
// 4. BubbleChart Class with RK4 Integration and Group Clustering
// ------------------------------
class BubbleChart {
  constructor(data) {
    this.data = data;
    // Define group colors (mapping group id to a color)
    var palette = [
            

            ["#1D1B33", "#2F4858", "#FFEDCB", "#4B8178", "#7197A8", "#362E37"],
            ["#F4B53F", "#2F4858", "#FFEDCB", "#4B8178", "#C1554E", "#6A4C57"],
            ["#453C4C", "#B3AA74", "#FFEDCB", "#9F9D8A", "#7A6154", "#204E5E"],
            ["#4C4459", "#82B59D", "#FFEDCB", "#F3AB4E", "#A44440", "#204E5E"],
            ["#203239", "#96977B", "#FFEDCB", "#DB986F", "#964744", "#2C3637"],
            ["#344C5C", "#698DA0", "#BFB8A6", "#E8CFC1", "#D96D59", "#2B3967"],
            ["#263A5B", "#61898D", "#B4C4C5", "#FAE3C0", "#7B4B5A", "#B3BC99"],
            ["#7A8671", "#D2B27C", "#E4CC91", "#BB605A", "#7B4B5A", "#F0E9A9"],
            ["#ffa943", "#2177f4", "#35fc93", "#f9cfd2", "#6eabf4", "#3714a1"],
            ["#ce2d42", "#7462f9", "#f4b53f", "#123676", "#9c223d", "#e6c7b4"],
            ["#06a0ba", "#6f3bff", "#f20a41", "#8777f7", "#4848c1", "#e6c7b4"],
            ["#71f2ff", "#81fcca", "#f91cb0", "#0239c1", "#05bdc6", "#f7f1b4"],
            ["#302D3B", "#DBF7BD", "#879369", "#9A5154", "#C3C590", "#CAA174"],
            ["#25164D", "#BFD4BF", "#316C6F", "#494190", "#D3B74F", "#ECE5DE"],
            ["#624565", "#9B9589", "#E49E81", "#DB6A60", "#FAB582", "#E3B69A"],
            ["#594C98", "#372B33", "#FE0878", "#82D6DB", "#92D0AF", "#721F4C"],
            ["#F0DEB4", "#A1A17A", "#5A8170", "#F4F3CC", "#4B8178", "#FFC7A1"],
            ["#A42534", "#3F352F", "#B74C3B", "#D4AA71", "#DCCFB2", "#693239"],
            ["#665B55", "#F5B488", "#B55053", "#8B2335", "#69837B", "#F0D2B1"],
            ["#313D51", "#FBE8AA", "#EB917B", "#B15552", "#809488", "#337F83"],
            ["#042882", "#81fcca", "#f91cb0", "#0239c1", "#8450d6", "#05bdc6"],
            ["#304B61", "#281733", "#377F86", "#D1D1AE", "#DB6D6A", "#9AC7C3"],
            ["#2F677E", "#B5B383", "#C35F4F", "#D2E1D9", "#7FD1AE", "#FAE7BF"],
            ["#2A2B41", "#673939", "#377F86", "#E3D5AE", "#EFC375", "#281733"],
            ["#FF7E42", "#2D2E3C", "#FFE1C8", "#4F9472", "#D1594D", "#384C7D"],
            ["#FF7306", "#C7B18E", "#FFE3A4", "#7F4E4D", "#233072", "#6B97A8"],
            ["#FF6705", "#ED9C7B", "#FFE1A2", "#7F4E4D", "#154150", "#BAC292"],
            ["#f2c079", "#3c3c67", "#f7edcf", "#84a0a4", "#d22f2f", "#cfd5ed"],
            ["#505978", "#8ab984", "#f7d8c6", "#7f655d", "#c6d8f7", "#78A39B"],
            ["#2F3C3E", "#7CAB93", "#B6CDA9", "#F4F3CF", "#666460", "#C9A889"],
            ["#C1554E", "#0C3E4D", "#076269", "#C5B65B", "#F7C862", "#22BB9B"],
            ["#C1554E", "#477F82", "#22BB9B", "#DFD2A8", "#F7C862", "#63AC9C"],
            ["#3E3649", "#6D7180", "#B0ACAD", "#DFD2A8", "#F7C862", "#A6BC99"],
            ["#44E2D2", "#2365B8", "#645EBA", "#664785", "#F38073", "#2E93B7"],
            ["#E7A564", "#AC966F", "#596358", "#234564", "#EB7952", "#F0BA81"],
            ["#131C3B", "#2B3C61", "#4F77C9", "#8FC2DE", "#D2DCC0", "#265670"],
            ["#D62A58", "#122959", "#4F77C9", "#8FC2DE", "#D2DCC0", "#466C88"],
            ["#DFE3DB", "#99BEA5", "#51525F", "#6C6A36", "#3F4159", "#572D54"],
            ["#066A74", "#352F51", "#601449", "#EC4E25", "#F7954A", "#792023"],
            ["#131133", "#0A405E", "#EF4F56", "#68C3A0", "#F3EFCA", "#A5282F"],
            ["#234357", "#33AFA6", "#8FE2AD", "#DBEFA9", "#EACF7B", "#408AA8"],
            ["#FDFCF8", "#A2A295", "#5A5F5F", "#2C3D3C", "#252929", "#31576E"],
            ["#2F2930", "#707485", "#99AABE", "#B6E6E8", "#FBF8F2", "#C4BEBE"],
            ["#F4B53F", "#6A4C57", "#DBE3AA", "#6EC699", "#3C4549", "#A66648"],
            ["#F4B53F", "#6A4C57", "#212D4E", "#274B64", "#628367", "#6B293C"],
            ["#F4B53F", "#6A4C57", "#21B29F", "#406B77", "#333F5B", "#3A8EA2"],
            ["#432D3A", "#42495F", "#6F7E67", "#C0B37A", "#E9C268", "#E0DEAB"],
            ["#432D3A", "#42495F", "#368991", "#E5DAB6", "#EDAB58", "#ED7B4A"],
            ["#F3E6CA", "#DCAF8A", "#A8AD75", "#40818B", "#32374A", "#96B7B7"],
            ["#492E1B", "#732737", "#5D5969", "#8F8E71", "#E8D993", "#B25963"],
            ["#25272C", "#FBEFBD", "#DCB26C", "#386B67", "#0D3844", "#497084"],
            ["#25272C", "#FBEFBD", "#AEAA9D", "#497084", "#303E61", "#3770A2"],
            ["#292127", "#9B464A", "#E0C985", "#2A979A", "#0D2F3C", "#ECEFDB"],
            ["#F4B53F", "#2F4858", "#0FB3BC", "#D6E3BD", "#C1554E", "#6A4C57"],
            ["#D4BE5B", "#A1D1B5", "#48ADB6", "#516C57", "#401D35", "#39447D"],
            ["#401D35", "#9E4557", "#F16E54", "#F2D89D", "#C4BB86", "#F9AD69"],
            ["#FEB613", "#DEE4D7", "#3EC2B2", "#356F8D", "#2E2A32", "#A5E1AC"],
            ["#8F4756", "#E7D7C4", "#A9A1A5", "#7A909D", "#352E3F", "#BA8B80"],
            ["#1A2739", "#223653", "#5C223D", "#CD253A", "#EA8353", "#15486B"],
            ["#385F8D", "#223653", "#E3564C", "#CD253A", "#764468", "#E1BAA9"],
            ["#BFE3D4", "#F1D499", "#6787A0", "#3E5277", "#341C33", "#9E667B"],
            ["#495069", "#87AD9F", "#D4C8AC", "#B67465", "#4B1F33", "#F28443"],
            ["#495069", "#528B8C", "#EBDCBE", "#F0B07D", "#BD5D60", "#82A2B9"],
            ["#507386", "#78B4AE", "#F2E1B9", "#C78379", "#8A6946", "#2D2543"],
        ];
   this.groupColors = {};
let key = 1;
for (let i = 0; i < palette.length; i++) {
  for (let j = 0; j < palette[i].length; j++) {
    this.groupColors[key.toString()] = palette[i][j];
    key++;
  }
}
    this.fallbackColor = "#FF851B";
    
    // Determine maximum size from the CSV data
    let maxSize = 0;
    for (let d of data) {
      let s = +d.size || 0;
      if (s > maxSize) maxSize = s;
    }
    // Define a square-root scale for the bubble radius
    this.radiusScale = (sz) => Math.sqrt(sz / maxSize) * 80;
    
    // Create Bubble objects
    this.bubbles = this.data.map(d => new Bubble(d, this.radiusScale));
    
    // Initialize simulation state for each bubble (position and velocity)
    this.state = [];
    for (let i = 0; i < this.bubbles.length; i++) {
      this.state.push({
        x: Math.random() * window.innerWidth,
        y: Math.random() * window.innerHeight,
        vx: 0,
        vy: 0
      });
    }
    
    // Force parameters (tweak these values as needed)
    this.groupForce = 0.0005;  // pulls each bubble toward its group's cluster center
    this.collisionPadding = 3; // extra space to avoid overlap

    // Compute cluster centers for each group so that bubbles of the same group are attracted together.
    this.computeClusterCenters();
    
    // Setup canvas and event listeners
    this.canvas = document.getElementById("bubbleCanvas");
    this.ctx = this.canvas.getContext("2d");
    this.onResize();
    window.addEventListener("resize", () => this.onResize());
    
    // Mouse dragging support
    this.draggingIndex = -1;
    this.dragOffset = { x: 0, y: 0 };
    this.canvas.addEventListener("mousedown", (e) => this.onMouseDown(e));
    this.canvas.addEventListener("mousemove", (e) => this.onMouseMove(e));
    this.canvas.addEventListener("mouseup", (e) => this.onMouseUp(e));
    
    // Start simulation loop
    requestAnimationFrame(() => this.update());
  }
  
  // Compute distinct group IDs and assign each a cluster center arranged on a circle around the canvas center.
  computeClusterCenters() {
    let groups = {};
    for (let bubble of this.bubbles) {
      groups[bubble.groupid] = true;
    }
    const groupIds = Object.keys(groups).sort();
    const numGroups = groupIds.length;
    this.clusterCenters = {};
    const rCluster = Math.min(window.innerWidth, window.innerHeight) * 0.3;
    const cx = window.innerWidth / 2;
    const cy = window.innerHeight / 2;
    for (let i = 0; i < numGroups; i++) {
      const angle = (2 * Math.PI * i) / numGroups;
      const x = cx + rCluster * Math.cos(angle);
      const y = cy + rCluster * Math.sin(angle);
      this.clusterCenters[groupIds[i]] = { x: x, y: y };
    }
  }
  
  onResize() {
    this.width = window.innerWidth;
    this.height = window.innerHeight;
    this.canvas.width = this.width;
    this.canvas.height = this.height;
    this.computeClusterCenters();
  }
  
  colorForGroup(gid) {
    return this.groupColors[gid] || this.fallbackColor;
  }
  
  // Find the bubble under the mouse (returns index or -1)
  findBubbleAt(mx, my) {
    for (let i = this.bubbles.length - 1; i >= 0; i--) {
      const s = this.state[i];
      const dx = mx - s.x, dy = my - s.y;
      if (Math.sqrt(dx * dx + dy * dy) <= this.bubbles[i].radius) {
        return i;
      }
    }
    return -1;
  }
  
  onMouseDown(e) {
    const rect = this.canvas.getBoundingClientRect();
    const mx = e.clientX - rect.left;
    const my = e.clientY - rect.top;
    const idx = this.findBubbleAt(mx, my);
    if (idx >= 0) {
      this.draggingIndex = idx;
      const s = this.state[idx];
      this.dragOffset.x = mx - s.x;
      this.dragOffset.y = my - s.y;
      this.bubbles[idx].fx = s.x;
      this.bubbles[idx].fy = s.y;
    }
  }
  
  onMouseMove(e) {
    if (this.draggingIndex >= 0) {
      const rect = this.canvas.getBoundingClientRect();
      const mx = e.clientX - rect.left;
      const my = e.clientY - rect.top;
      this.bubbles[this.draggingIndex].fx = mx - this.dragOffset.x;
      this.bubbles[this.draggingIndex].fy = my - this.dragOffset.y;
    }
  }
  
  onMouseUp(e) {
    if (this.draggingIndex >= 0) {
      this.bubbles[this.draggingIndex].fx = null;
      this.bubbles[this.draggingIndex].fy = null;
      this.draggingIndex = -1;
    }
  }
  
  // Compute the acceleration for bubble i. Here we add a force pulling the bubble toward its group's center.
  computeAcceleration(i, stateArray) {
    const bubble = this.bubbles[i];
    const s = stateArray[i];
    let ax = 0, ay = 0;
    // If the bubble is pinned, no acceleration.
    if (bubble.fx !== null || bubble.fy !== null) return { ax: 0, ay: 0 };
    
    // Compute group clustering force
    const groupTarget = this.clusterCenters[bubble.groupid];
    if (groupTarget) {
      ax += (groupTarget.x - s.x) * this.groupForce;
      ay += (groupTarget.y - s.y) * this.groupForce;
    }
    
    return { ax, ay };
  }
  
  // Handle collisions between bubbles (adjust positions to resolve overlaps)
  handleCollisions(stateArray) {
    const n = this.bubbles.length;
    for (let i = 0; i < n; i++) {
      const bi = this.bubbles[i];
      const si = stateArray[i];
      for (let j = i + 1; j < n; j++) {
        const bj = this.bubbles[j];
        const sj = stateArray[j];
        let dx = sj.x - si.x, dy = sj.y - si.y;
        const rSquared = dx * dx + dy * dy;
        const minDist = bi.radius + bj.radius + this.collisionPadding;
        if (rSquared < minDist * minDist) {
          const dist = Math.sqrt(rSquared) || 0.01;
          const overlap = (minDist - dist) / (2 * dist);
          dx *= overlap;
          dy *= overlap;
          if (bi.fx === null) { si.x -= dx; si.y -= dy; }
          if (bj.fx === null) { sj.x += dx; sj.y += dy; }
        }
      }
    }
  }
  
  // RK4 integration step for smooth simulation.
  RK4Step(dt) {
    const n = this.bubbles.length;
    const k1 = [], k2 = [], k3 = [], k4 = [];
    // k values: {dx, dy, dvx, dvy} for each bubble.
    // Step 1: Compute k1 from the current state.
    for (let i = 0; i < n; i++) {
      const s = this.state[i];
      const a = this.computeAcceleration(i, this.state);
      if (this.bubbles[i].fx !== null || this.bubbles[i].fy !== null)
        k1.push({ dx: 0, dy: 0, dvx: 0, dvy: 0 });
      else
        k1.push({ dx: s.vx, dy: s.vy, dvx: a.ax, dvy: a.ay });
    }
    // Step 2: Compute mid-state for k2.
    const state2 = [];
    for (let i = 0; i < n; i++) {
      const s = this.state[i];
      const k = k1[i];
      state2.push({
        x: s.x + 0.5 * dt * k.dx,
        y: s.y + 0.5 * dt * k.dy,
        vx: s.vx + 0.5 * dt * k.dvx,
        vy: s.vy + 0.5 * dt * k.dvy
      });
    }
    for (let i = 0; i < n; i++) {
      const a = this.computeAcceleration(i, state2);
      if (this.bubbles[i].fx !== null || this.bubbles[i].fy !== null)
        k2.push({ dx: 0, dy: 0, dvx: 0, dvy: 0 });
      else
        k2.push({ dx: state2[i].vx, dy: state2[i].vy, dvx: a.ax, dvy: a.ay });
    }
    // Step 3: Compute mid-state for k3.
    const state3 = [];
    for (let i = 0; i < n; i++) {
      const s = this.state[i];
      const k = k2[i];
      state3.push({
        x: s.x + 0.5 * dt * k.dx,
        y: s.y + 0.5 * dt * k.dy,
        vx: s.vx + 0.5 * dt * k.dvx,
        vy: s.vy + 0.5 * dt * k.dvy
      });
    }
    for (let i = 0; i < n; i++) {
      const a = this.computeAcceleration(i, state3);
      if (this.bubbles[i].fx !== null || this.bubbles[i].fy !== null)
        k3.push({ dx: 0, dy: 0, dvx: 0, dvy: 0 });
      else
        k3.push({ dx: state3[i].vx, dy: state3[i].vy, dvx: a.ax, dvy: a.ay });
    }
    // Step 4: Compute state for k4.
    const state4 = [];
    for (let i = 0; i < n; i++) {
      const s = this.state[i];
      const k = k3[i];
      state4.push({
        x: s.x + dt * k.dx,
        y: s.y + dt * k.dy,
        vx: s.vx + dt * k.dvx,
        vy: s.vy + dt * k.dvy
      });
    }
    for (let i = 0; i < n; i++) {
      const a = this.computeAcceleration(i, state4);
      if (this.bubbles[i].fx !== null || this.bubbles[i].fy !== null)
        k4.push({ dx: 0, dy: 0, dvx: 0, dvy: 0 });
      else
        k4.push({ dx: state4[i].vx, dy: state4[i].vy, dvx: a.ax, dvy: a.ay });
    }
    // Combine k's to update state.
    for (let i = 0; i < n; i++) {
      const bubble = this.bubbles[i];
      if (bubble.fx !== null || bubble.fy !== null) {
        this.state[i].x = bubble.fx;
        this.state[i].y = bubble.fy;
        this.state[i].vx = 0;
        this.state[i].vy = 0;
      } else {
        const dx = (k1[i].dx + 2 * k2[i].dx + 2 * k3[i].dx + k4[i].dx) / 6;
        const dy = (k1[i].dy + 2 * k2[i].dy + 2 * k3[i].dy + k4[i].dy) / 6;
        const dvx = (k1[i].dvx + 2 * k2[i].dvx + 2 * k3[i].dvx + k4[i].dvx) / 6;
        const dvy = (k1[i].dvy + 2 * k2[i].dvy + 2 * k3[i].dvy + k4[i].dvy) / 6;
        this.state[i].x += dx * dt;
        this.state[i].y += dy * dt;
        this.state[i].vx += dvx * dt;
        this.state[i].vy += dvy * dt;
      }
    }
  }
  
  // Single simulation step: apply RK4, resolve collisions, and damp velocities.
  simStep() {
    this.RK4Step(0.9);
    this.handleCollisions(this.state);
    for (let i = 0; i < this.bubbles.length; i++) {
      if (this.bubbles[i].fx === null) {
        this.state[i].vx *= 0.98;
        this.state[i].vy *= 0.98;
      } else {
        this.state[i].vx = 0;
        this.state[i].vy = 0;
        this.state[i].x = this.bubbles[i].fx;
        this.state[i].y = this.bubbles[i].fy;
      }
    }
  }
  
  update() {
    this.simStep();
    this.draw();
    requestAnimationFrame(() => this.update());
  }
  
  draw() {
    const ctx = this.ctx;
    ctx.clearRect(0, 0, this.width, this.height);
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.font = "16px Helvetica Neue";
    // Draw each bubble using the simulation state.
    for (let i = 0; i < this.bubbles.length; i++) {
      const b = this.bubbles[i];
      const s = this.state[i];
      ctx.fillStyle = this.colorForGroup(b.groupid);
      ctx.beginPath();
      ctx.arc(s.x, s.y, b.radius, 0, Math.PI * 2);
      ctx.fill();
      ctx.strokeStyle = "#333";
      ctx.lineWidth = 1;
      ctx.stroke();
      if (b.name) {
        ctx.fillStyle = "#fff";
        ctx.fillText(b.name, s.x, s.y);
      }
    }
  }
}

// ------------------------------
// 5. Instantiate and run the chart
// ------------------------------
document.addEventListener("DOMContentLoaded", () => {
  const data = parseCSV(csvData);
  new BubbleChart(data);
});
</script>
</body>
</html>
likes 0comments 0
Riemann Zetahtml
Complexe Function
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Riemann Zeta - Plain JS + Canvas (Smooth)</title>
  <style>
    html, body {
      margin: 0; 
      padding: 0; 
      width: 100%; 
      height: 100%;
      background: #000;
      overflow: hidden;
    }
    canvas {
      display: block;
      position: absolute; 
      left: 0; top: 0;
    }
  </style>
</head>
<body>
<canvas id="riemannCanvas"></canvas>

<script>
// ======================================================================
// 1) Basic Utility: map, random, minimal noise2D
// ======================================================================
function mapRange(value, inMin, inMax, outMin, outMax){
  return outMin + (outMax - outMin)*((value-inMin)/(inMax-inMin));
}
function lerp(a,b,t){ return a + (b-a)*t; }
function fade(t){ return t*t*t*(t*(t*6 -15)+10); }

function noise2D(x,y){
  // minimal "hash" approach
  let xi = Math.floor(x), yi= Math.floor(y);
  let xf= x-xi,        yf= y-yi;
  let r1 = randomCell2D(xi,   yi);
  let r2 = randomCell2D(xi+1, yi);
  let r3 = randomCell2D(xi,   yi+1);
  let r4 = randomCell2D(xi+1, yi+1);
  let u= fade(xf), v= fade(yf);
  let i1= lerp(r1, r2, u);
  let i2= lerp(r3, r4, u);
  return lerp(i1, i2, v);
}
function randomCell2D(ix,iy){
  let s = (ix*374761393 + iy*668265263)^0xBADC0FFE;
  s = (s<<13)^s;
  let r= (1.0 - ((s*(s*s*15731+789221)+1376312589)&0x7fffffff)/1073741824.0);
  return r;
}

// ======================================================================
// 2) Vec2 class (like p5's createVector) 
// ======================================================================
class Vec2 {
  constructor(x,y){this.x=x;this.y=y;}
  add(v){ return new Vec2(this.x+v.x, this.y+v.y);}
  sub(v){ return new Vec2(this.x-v.x, this.y-v.y);}
}

// ======================================================================
// 3) Complex & Zeta
// ======================================================================
function Complex(real, imag){
  if(real && real.real!==undefined && real.imag!==undefined) return real;
  return {real, imag: imag||0};
}
function complexNeg(a){ return Complex(-a.real, -a.imag);}
function complexMul(a,b){
  let re= a.real*b.real - a.imag*b.imag;
  let im= a.real*b.imag + a.imag*b.real;
  return Complex(re,im);
}
function complexAdd(a,b){
  return Complex(a.real+b.real, a.imag+b.imag);
}
function complexSub(a,b){
  return Complex(a.real-b.real, a.imag-b.imag);
}
function complexPow(a,b){
  //  (r e^{i\theta})^(x + i y) = ...
  let r= Math.sqrt(a.real*a.real + a.imag*a.imag);
  let theta= Math.atan2(a.imag,a.real);
  let br= b.real, bi= b.imag;
  let logR= Math.log(r);
  let x= Math.exp(br*logR - bi*theta);
  let phi= bi*logR + br*theta;
  return Complex(x*Math.cos(phi), x*Math.sin(phi));
}
function binomial(n, k){
  if(k>n)return 0;
  if(k> n-k)k=n-k;
  let r=1; 
  for(let i=0;i<k;i++){
    r*= (n-i)/(i+1);
  }
  return r;
}
function sign(k){ return (k%2)? -1: 1; }
function zeta3(s, t=150){
  // partial sum approach
  // s!=1
  // sum_{n=0..t} sum_{k=0..n} [ (-1)^k binomial(n,k) / (k+1)^s ] / 2^(n+1)
  // all divided by [1-2^(1-s)]
  // from your snippet
  let sum= Complex(0,0);
  for(let n=0;n<t;n++){
    let inn= Complex(0,0);
    for(let k=0;k<=n;k++){
      let p= complexPow(Complex(k+1,0), complexNeg(s));
      let sc= sign(k)* binomial(n,k);
      let tmp= complexMul(p, Complex(sc,0));
      inn= complexAdd(inn, tmp);
    }
    let factor= Math.pow(2, -(n+1));
    inn= Complex( inn.real*factor, inn.imag*factor );
    sum= complexAdd(sum, inn);
  }
  // factor = 1/(1- 2^(1-s))
  let two= Complex(2,0);
  let oneMinusS= Complex(1-s.real, -s.imag);
  let twoOneS= complexPow(two, oneMinusS);
  let denom= complexSub(Complex(1,0), twoOneS);
  // sum / denom
  // naive complexDiv
  let dnorm= denom.real*denom.real + denom.imag*denom.imag;
  let conjRe=  denom.real, conjIm= -denom.imag;
  let cross= complexMul(sum, {real: conjRe, imag: conjIm});
  return { 
    real: cross.real/dnorm,
    imag: cross.imag/dnorm
  };
}

// ======================================================================
// 4) Riemann Logic w/ Smooth Lines
// We store all zeta points in an array and draw one continuous smooth path
// ======================================================================
class RiemannApp {
  constructor(){
    this.canvas= document.getElementById('riemannCanvas');
    this.ctx= this.canvas.getContext('2d');
    this.width=0; this.height=0;
    this.resize();
    window.addEventListener('resize', ()=>this.resize());
    window.addEventListener('keydown', (e)=>this.keyDown(e));
    // store zeta coords
    this.points= [];
    this.index= -0.2;
    this.limit= 34;
    this.offset=200;
    this.start=0;

    requestAnimationFrame(()=>this.draw());
  }

  resize(){
    this.width= window.innerWidth;
    this.height= window.innerHeight;
    this.canvas.width= this.width;
    this.canvas.height= this.height;
    this.center= new Vec2(this.width*0.5, this.height*0.5);
  }

  keyDown(e){
    if(e.key==='c'){
      this.points= [];
      this.index= -0.2;
      this.limit=34;
    } else if(e.key==='p'){
      this.limit=64;
    }
  }

  draw(){
    requestAnimationFrame(()=>this.draw());
    // background
    this.ctx.fillStyle= "rgb(6,10,43)";
    this.ctx.fillRect(0,0,this.width,this.height);

    // generate next zeta point if index < limit
    if(this.index< this.limit){
      let s= Complex(0.5,this.index);
      let comp= zeta3(s);
      let x= mapRange(comp.real,-2,2, -this.offset, this.offset);
      let y= mapRange(comp.imag,-2,2, -this.offset, this.offset);
      // shift by center
      x+= this.center.x;
      y+= this.center.y;
      this.points.push({x,y});
      this.index+=0.05;
    }

    // draw circle etc. if you want
    this.ctx.strokeStyle="rgba(255,255,255,0.5)";
    this.ctx.beginPath();
    this.ctx.arc(this.center.x, this.center.y, 200, 0,Math.PI*2);
    this.ctx.stroke();

    // draw a smooth curve through all points
    if(this.points.length>2){
      this.drawCatmullRom();
    }

    this.start+= 0.0001;
  }

  drawCatmullRom(){
    // We'll do a standard Catmull–Rom approach for a “smooth function.”
    // p[i-1], p[i], p[i+1], p[i+2] => we create a segment
    let pts= this.points;
    this.ctx.lineWidth= 3;

    this.ctx.beginPath();
    // move to first
    this.ctx.moveTo(pts[0].x, pts[0].y);

    for(let i=1; i<pts.length-2; i++){
      let c1x= (pts[i].x + pts[i+1].x)/2;
      let c1y= (pts[i].y + pts[i+1].y)/2;
      this.ctx.quadraticCurveTo(pts[i].x, pts[i].y, c1x, c1y);
    }
    // last segment
    let penult= pts[pts.length-2];
    let last=   pts[pts.length-1];
    this.ctx.quadraticCurveTo(penult.x, penult.y, last.x, last.y);

    // color logic
    // we could pick a color from wave or from length
    // for now, a static color
    this.ctx.strokeStyle="rgba(255,200,100,0.8)";
    this.ctx.stroke();
  }
}

// let's go
document.addEventListener("DOMContentLoaded", ()=>{
  new RiemannApp();
});
</script>

</body>
</html>
likes 0comments 0
Alchemyhtml
Map
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Uniphi-Alchemy Map</title>
  <style>
    :root{
      --bg:#0b0f14;
      --panel:#101824;
      --panel2:#0f1620;
      --ink:#e7eef9;
      --muted:#9fb1c8;
      --line:#213044;
      --accent:#7dd3fc;
      --accent2:#a78bfa;
      --ok:#86efac;
      --warn:#fca5a5;
      --radius:14px;
      --shadow: 0 10px 40px rgba(0,0,0,.35);
      --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
      --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
    }
    *{box-sizing:border-box}
    body{
      margin:0;
      font-family:var(--sans);
      background:
        radial-gradient(900px 500px at 20% 10%, rgba(167,139,250,.14), transparent 60%),
        radial-gradient(900px 500px at 80% 0%, rgba(125,211,252,.12), transparent 55%),
        radial-gradient(800px 600px at 30% 90%, rgba(134,239,172,.08), transparent 60%),
        var(--bg);
      color:var(--ink);
      line-height:1.35;
    }
    header{
      padding:28px 18px 6px;
      max-width:1100px;
      margin:0 auto;
    }
    h1{
      margin:0 0 6px;
      letter-spacing:.2px;
      font-weight:800;
      font-size:24px;
    }
    .subtitle{
      color:var(--muted);
      max-width:70ch;
      font-size:14px;
    }
    main{
      max-width:1100px;
      margin:0 auto;
      padding:12px 18px 60px;
      display:grid;
      gap:14px;
    }
    .grid{
      display:grid;
      grid-template-columns: 1.1fr .9fr;
      gap:14px;
    }
    @media (max-width: 980px){
      .grid{grid-template-columns:1fr}
    }
    .card{
      background: linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.02));
      border:1px solid var(--line);
      border-radius:var(--radius);
      box-shadow: var(--shadow);
      overflow:hidden;
    }
    .card h2{
      margin:0;
      padding:14px 14px 10px;
      font-size:14px;
      letter-spacing:.3px;
      text-transform:uppercase;
      color:var(--muted);
      border-bottom:1px solid var(--line);
      background: rgba(0,0,0,.14);
    }
    .content{ padding:14px; }
    label{ display:block; font-size:12px; color:var(--muted); margin:10px 0 6px; }
    input[type="text"], input[type="date"], textarea, select{
      width:100%;
      border:1px solid var(--line);
      border-radius:12px;
      background: rgba(0,0,0,.18);
      color:var(--ink);
      padding:10px 11px;
      outline:none;
      transition: border .15s ease;
    }
    textarea{ min-height:84px; resize:vertical; }
    input:focus, textarea:focus, select:focus{ border-color: rgba(125,211,252,.6); }
    .row{
      display:grid;
      grid-template-columns: 1fr 1fr;
      gap:12px;
    }
    @media (max-width: 640px){
      .row{ grid-template-columns:1fr; }
    }
    .chips{
      display:flex; flex-wrap:wrap; gap:8px;
    }
    .chip{
      display:flex;
      gap:8px;
      align-items:flex-start;
      padding:10px 10px;
      border:1px solid var(--line);
      border-radius:999px;
      background: rgba(0,0,0,.14);
      font-size:12px;
      color:var(--ink);
      cursor:pointer;
      user-select:none;
    }
    .chip input{ margin-top:2px; }
    .chip small{
      display:block;
      color:var(--muted);
      font-size:11px;
      line-height:1.2;
      margin-top:2px;
    }
    .btnbar{
      display:flex;
      flex-wrap:wrap;
      gap:10px;
      align-items:center;
    }
    button{
      border:1px solid var(--line);
      background: rgba(0,0,0,.2);
      color:var(--ink);
      padding:10px 12px;
      border-radius:12px;
      cursor:pointer;
      font-weight:600;
    }
    button:hover{ border-color: rgba(125,211,252,.55); }
    button.primary{
      border-color: rgba(125,211,252,.45);
      background: linear-gradient(135deg, rgba(125,211,252,.20), rgba(167,139,250,.12));
    }
    button.good{
      border-color: rgba(134,239,172,.45);
      background: linear-gradient(135deg, rgba(134,239,172,.16), rgba(125,211,252,.10));
    }
    button.bad{
      border-color: rgba(252,165,165,.45);
      background: linear-gradient(135deg, rgba(252,165,165,.16), rgba(167,139,250,.08));
    }
    .hint{ color:var(--muted); font-size:12px; }
    .phasewrap{
      display:flex; gap:12px; align-items:center;
      padding:10px 12px;
      border:1px dashed rgba(125,211,252,.35);
      border-radius:12px;
      background: rgba(0,0,0,.12);
      margin-top:8px;
    }
    .phasewrap input[type="range"]{ width:100%; }
    .phasepill{
      font-family:var(--mono);
      font-size:12px;
      padding:6px 10px;
      border:1px solid var(--line);
      border-radius:999px;
      white-space:nowrap;
      background: rgba(0,0,0,.16);
    }
    .list{
      display:grid;
      gap:10px;
    }
    .ingredient{
      border:1px solid var(--line);
      border-radius:14px;
      background: rgba(0,0,0,.12);
      padding:12px;
    }
    .ingredient .top{
      display:flex; align-items:center; justify-content:space-between; gap:10px;
    }
    .ingredient strong{ font-size:13px; }
    .rating{
      display:flex; align-items:center; gap:10px;
      font-family:var(--mono);
      color:var(--muted);
      font-size:12px;
    }
    .rating input[type="range"]{ width:180px; }
    @media (max-width: 640px){
      .rating input[type="range"]{ width:140px; }
    }
    details{
      border:1px solid var(--line);
      border-radius:14px;
      background: rgba(0,0,0,.12);
      padding:10px 12px;
    }
    summary{
      cursor:pointer;
      font-weight:700;
      list-style:none;
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap:10px;
    }
    summary::-webkit-details-marker{ display:none; }
    .mono{ font-family:var(--mono); color:var(--muted); font-size:12px; }
    .foot{
      display:flex; justify-content:space-between; gap:10px; flex-wrap:wrap;
      margin-top:10px;
    }
    .status{
      font-size:12px;
      color:var(--muted);
    }
    .status b{ color:var(--ink); }
    .file{
      display:flex; align-items:center; gap:10px; flex-wrap:wrap;
    }
    .file input[type="file"]{
      border:1px solid var(--line);
      border-radius:12px;
      padding:8px;
      background: rgba(0,0,0,.12);
      color:var(--muted);
    }
  </style>
</head>
<body>
  <header>
    <h1>Uniphi-Alchemy Map</h1>
    <div class="subtitle">
      A single-page ritual for building worlds: <span style="color:var(--accent)">noise</span> becomes
      <span style="color:var(--accent2)">constellations</span>, and structure becomes a gentle handrail.
      Works offline. Exports Markdown + JSON.
    </div>
  </header>

  <main class="grid">
    <section class="card">
      <h2>Identity</h2>
      <div class="content">
        <div class="row">
          <div>
            <label>Project / Work</label>
            <input id="project" type="text" placeholder="e.g. Spectra — Nebula Field UI Pass" />
          </div>
          <div>
            <label>Date</label>
            <input id="date" type="date" />
          </div>
        </div>

        <label>Phase (0–6)</label>
        <div class="phasewrap">
          <span id="phasepill" class="phasepill">0 • spark</span>
          <input id="phase" type="range" min="0" max="6" step="1" value="0" />
        </div>
        <div class="hint" style="margin-top:8px">
          0 spark • 1 calcination • 2 dissolution • 3 separation • 4 conjunction • 5 distillation • 6 coagulation
        </div>

        <label>One-line intent</label>
        <textarea id="intent" placeholder="What is the smallest true north for this cycle?"></textarea>

        <div class="foot">
          <div class="status" id="autosave">Autosave: <b>on</b> (localStorage)</div>
          <div class="status" id="wordcount">Words: <b>0</b></div>
        </div>
      </div>
    </section>

    <section class="card">
      <h2>Axioms (pick 3)</h2>
      <div class="content">
        <div class="chips" id="axioms"></div>
        <div class="hint" style="margin-top:10px">
          Motto-style axioms: Latin + French + English. Keep it short enough to remember.
        </div>
      </div>
    </section>

    <section class="card">
      <h2>9 Ingredients</h2>
      <div class="content">
        <div class="hint" style="margin-bottom:10px">
          Rate each 0–5 and write one sentence. If it feels “super-artificial”: boost <b>Sulfur</b> + <b>Witness</b>, reduce <b>Vessel</b> + <b>Guardian</b>.
        </div>
        <div class="list" id="ingredients"></div>
      </div>
    </section>

    <section class="card">
      <h2>7 Operations</h2>
      <div class="content">
        <div class="list" id="operations"></div>
      </div>
    </section>

    <section class="card">
      <h2>Archetypes + Shadow</h2>
      <div class="content">
        <label>Active archetypes (2–3 is plenty)</label>
        <div class="chips" id="archetypes"></div>

        <label>Shadow loop watch</label>
        <select id="shadow_loop">
          <option value="none">(none)</option>
          <option value="complexity_armor">Complexity as armor</option>
          <option value="security_trance">Security trance (fence becomes cage)</option>
          <option value="abstraction_no_ground">Abstraction without grounding</option>
          <option value="release_shyness">Release shyness (“one more patch”)</option>
        </select>

        <label>Counter-spell (one sentence)</label>
        <textarea id="counter_spell" placeholder="A tiny vow that breaks the loop."></textarea>
      </div>
    </section>

    <section class="card">
      <h2>Balance + 60-second ritual</h2>
      <div class="content">
        <label>Balance: smallest next step</label>
        <textarea id="balance_next_step" placeholder="One actionable step that improves balance."></textarea>

        <label>Anchor (sensory / concrete)</label>
        <input id="ritual_anchor" type="text" placeholder="e.g. The laptop fan warms the room like a small sun." />

        <label>Stake (why it matters)</label>
        <input id="ritual_stake" type="text" placeholder="e.g. I want this to welcome curiosity." />

        <label>Spell (cosmic lens)</label>
        <input id="ritual_spell" type="text" placeholder="e.g. Noise becomes constellations; structure becomes a handrail." />

        <label>As a single paragraph</label>
        <textarea id="ritual_paragraph" placeholder="Auto-composed from Anchor + Stake + Spell (you can edit)."></textarea>

        <div class="btnbar" style="margin-top:12px">
          <button class="primary" id="btn_export_md">Export Markdown</button>
          <button class="good" id="btn_download_json">Download JSON</button>
          <button id="btn_copy_md">Copy Markdown</button>
          <button class="bad" id="btn_reset">Reset</button>
        </div>

        <div class="file" style="margin-top:12px">
          <label style="margin:0">Import JSON</label>
          <input id="import_file" type="file" accept="application/json,.json" />
          <button id="btn_load_example">Load example</button>
        </div>

        <div class="hint" style="margin-top:10px">
          Tip: Keep your map in git alongside the project. It’s a living README for your own nervous system.
        </div>
      </div>
    </section>
  </main>

<script>
(() => {
  const PHASES = ["spark","calcination","dissolution","separation","conjunction","distillation","coagulation"];

  const AXIOMS = [
    { id:"solve_et_coagula", la:"Solve et Coagula", fr:"Dissoudre et coaguler", en:"Dissolve and coagulate" },
    { id:"ordo_ab_chao", la:"Ordo ab Chao", fr:"L’ordre du chaos", en:"Order from chaos" },
    { id:"iterare_est_distillare", la:"Iterare est Distillare", fr:"Itérer, c’est distiller", en:"To iterate is to distill" },
    { id:"veritas_in_structura", la:"Veritas in Structura", fr:"La vérité dans la structure", en:"Truth lives in structure" },
    { id:"nomen_est_clavis", la:"Nomen est Clavis", fr:"Le nom est clé", en:"Name is a key" },
    { id:"mensura_est_magia", la:"Mensura est Magia", fr:"La mesure est magie", en:"Measurement is magic" },
    { id:"custodia_sine_carcere", la:"Custodia sine Carcere", fr:"Garder sans enfermer", en:"Guard without imprisoning" },
    { id:"ludus_est_labor", la:"Ludus est Labor", fr:"Le jeu est travail", en:"Play is work" },
    { id:"testis_facit_mundum", la:"Testis facit Mundum", fr:"Le témoin fait le monde", en:"The witness makes the world" },
  ];

  const INGREDIENTS = [
    { key:"aether", name:"Aether", desc:"Vision" },
    { key:"mercurius", name:"Mercurius", desc:"Flow / Interoperability" },
    { key:"sulfur", name:"Sulfur", desc:"Soul / Urgency" },
    { key:"sal", name:"Sal", desc:"Structure / Truth" },
    { key:"vessel", name:"Vessel", desc:"Infrastructure / Container" },
    { key:"catalyst", name:"Catalyst", desc:"Play / Risk" },
    { key:"noise", name:"Noise", desc:"Oracle / Randomness" },
    { key:"guardian", name:"Guardian", desc:"Security / Boundaries" },
    { key:"witness", name:"Witness", desc:"Story / Audience" },
  ];

  const OPS = [
    { key:"calcination", title:"Calcination", cue:"burn illusions, keep essence" },
    { key:"dissolution", title:"Dissolution", cue:"let it flow, prototype freely" },
    { key:"separation", title:"Separation", cue:"isolate the core" },
    { key:"conjunction", title:"Conjunction", cue:"merge art + system" },
    { key:"fermentation", title:"Fermentation", cue:"invite surprise" },
    { key:"distillation", title:"Distillation", cue:"refine and clarify" },
    { key:"coagulation", title:"Coagulation", cue:"ship the artifact" },
  ];

  const ARCHETYPES = [
    { id:"cosmic_chemist", label:"Cosmic Chemist", sub:"myth + physics into matter" },
    { id:"net_weaver", label:"Net Weaver", sub:"bridges, peers, tunnels" },
    { id:"vault_guardian", label:"Vault Guardian", sub:"keys, access, integrity" },
    { id:"trickster_debugger", label:"Trickster Debugger", sub:"chaos-wrangler" },
    { id:"archivist", label:"Archivist", sub:"names, versions, reproducibility" },
    { id:"poet_witness", label:"Poet-Witness", sub:"felt meaning, human door" },
  ];

  // Elements
  const $ = (id) => document.getElementById(id);
  const elAxioms = $("axioms");
  const elIngredients = $("ingredients");
  const elOps = $("operations");
  const elArchetypes = $("archetypes");

  const stateKey = "uniphi_alchemy_map_v1";

  function defaultState(){
    const today = new Date();
    const iso = today.toISOString().slice(0,10);
    return {
      project: "",
      date: iso,
      phase: 0,
      intent: "",
      axioms: [],
      ingredients: INGREDIENTS.map(x => ({ key:x.key, rating: 3, note: "" })),
      operations: OPS.map(x => ({ key:x.key, notes: "" })),
      archetypes: [],
      shadow_loop: "none",
      counter_spell: "",
      balance_next_step: "",
      ritual: { anchor:"", stake:"", spell:"", paragraph:"" }
    };
  }

  let state = load() || defaultState();

  function save(){
    localStorage.setItem(stateKey, JSON.stringify(state));
    $("autosave").innerHTML = 'Autosave: <b>on</b> (localStorage)';
  }

  function load(){
    try{
      const raw = localStorage.getItem(stateKey);
      if(!raw) return null;
      return JSON.parse(raw);
    }catch(e){
      return null;
    }
  }

  function renderChips(container, items, selectedIds, onToggle){
    container.innerHTML = "";
    items.forEach(item => {
      const lbl = document.createElement("label");
      lbl.className = "chip";
      const cb = document.createElement("input");
      cb.type = "checkbox";
      cb.checked = selectedIds.includes(item.id);
      cb.addEventListener("change", () => onToggle(item.id, cb.checked));
      const txt = document.createElement("div");
      txt.innerHTML = `<div><b>${escapeHtml(item.la || item.label)}</b><small>${escapeHtml(item.fr || item.sub)} — ${escapeHtml(item.en || "")}</small></div>`;
      lbl.appendChild(cb);
      lbl.appendChild(txt);
      container.appendChild(lbl);
    });
  }

  function escapeHtml(s){
    return (s ?? "").toString().replace(/[&<>"']/g, (c) => ({
      "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"
    }[c]));
  }

  function render(){
    // Identity
    $("project").value = state.project || "";
    $("date").value = state.date || "";
    $("phase").value = state.phase ?? 0;
    $("intent").value = state.intent || "";
    updatePhasePill();

    // Axioms
    renderChips(elAxioms, AXIOMS, state.axioms, (id, checked) => {
      if(checked){
        if(state.axioms.length >= 3){
          // soft rule: keep 3
          // uncheck immediately
          const inputs = elAxioms.querySelectorAll("input[type=checkbox]");
          inputs.forEach(inp => {
            const parent = inp.closest("label");
            const text = parent?.innerText || "";
            // no-op, we'll just revert this one:
          });
          checked = false;
        } else {
          state.axioms.push(id);
        }
      }else{
        state.axioms = state.axioms.filter(x => x !== id);
      }
      // hard enforce length <= 3
      state.axioms = state.axioms.slice(0,3);
      save();
      render(); // re-render to reflect enforcement
    });

    // Ingredients
    elIngredients.innerHTML = "";
    INGREDIENTS.forEach((meta, idx) => {
      const data = state.ingredients[idx] || { key: meta.key, rating: 3, note: "" };
      const wrap = document.createElement("div");
      wrap.className = "ingredient";
      wrap.innerHTML = `
        <div class="top">
          <div>
            <strong>${meta.name}</strong> <span class="mono">(${meta.desc})</span>
          </div>
          <div class="rating">
            <span>0</span>
            <input type="range" min="0" max="5" step="1" value="${data.rating ?? 3}" data-i="${idx}" />
            <span>5</span>
            <span class="phasepill" style="padding:4px 10px">${data.rating ?? 3}/5</span>
          </div>
        </div>
        <label style="margin-top:10px">Sentence</label>
        <textarea data-note="${idx}" placeholder="One sentence that makes it true.">${escapeHtml(data.note || "")}</textarea>
      `;
      elIngredients.appendChild(wrap);
    });

    // Operations
    elOps.innerHTML = "";
    OPS.forEach((meta, idx) => {
      const data = state.operations[idx] || { key: meta.key, notes:"" };
      const det = document.createElement("details");
      det.open = idx < 2; // open first two by default
      det.innerHTML = `
        <summary>
          <span>${meta.title} <span class="mono">— ${meta.cue}</span></span>
          <span class="mono">${idx+1}/7</span>
        </summary>
        <div style="margin-top:10px">
          <label>Notes</label>
          <textarea data-op="${idx}" placeholder="Write in bullets or prose.">${escapeHtml(data.notes || "")}</textarea>
        </div>
      `;
      elOps.appendChild(det);
    });

    // Archetypes
    renderChips(elArchetypes, ARCHETYPES, state.archetypes, (id, checked) => {
      if(checked){
        if(!state.archetypes.includes(id)) state.archetypes.push(id);
      }else{
        state.archetypes = state.archetypes.filter(x => x !== id);
      }
      save();
      updateWordCount();
    });

    // Shadow + other
    $("shadow_loop").value = state.shadow_loop || "none";
    $("counter_spell").value = state.counter_spell || "";
    $("balance_next_step").value = state.balance_next_step || "";
    $("ritual_anchor").value = state.ritual?.anchor || "";
    $("ritual_stake").value = state.ritual?.stake || "";
    $("ritual_spell").value = state.ritual?.spell || "";
    $("ritual_paragraph").value = state.ritual?.paragraph || "";

    updateWordCount();
  }

  function updatePhasePill(){
    const p = parseInt($("phase").value,10) || 0;
    $("phasepill").textContent = `${p} • ${PHASES[p] || "?"}`;
  }

  function bind(){
    $("project").addEventListener("input", e => { state.project = e.target.value; save(); updateWordCount(); });
    $("date").addEventListener("input", e => { state.date = e.target.value; save(); });
    $("phase").addEventListener("input", e => { state.phase = parseInt(e.target.value,10) || 0; updatePhasePill(); save(); });
    $("intent").addEventListener("input", e => { state.intent = e.target.value; save(); updateWordCount(); });

    // Ingredients (delegated)
    elIngredients.addEventListener("input", (e) => {
      const t = e.target;
      if(t.matches("input[type=range][data-i]")){
        const idx = parseInt(t.getAttribute("data-i"),10);
        state.ingredients[idx].rating = parseInt(t.value,10);
        save();
        render(); // update pill
      }
      if(t.matches("textarea[data-note]")){
        const idx = parseInt(t.getAttribute("data-note"),10);
        state.ingredients[idx].note = t.value;
        save();
        updateWordCount();
      }
    });

    // Operations
    elOps.addEventListener("input", (e) => {
      const t = e.target;
      if(t.matches("textarea[data-op]")){
        const idx = parseInt(t.getAttribute("data-op"),10);
        state.operations[idx].notes = t.value;
        save();
        updateWordCount();
      }
    });

    $("shadow_loop").addEventListener("change", e => { state.shadow_loop = e.target.value; save(); });
    $("counter_spell").addEventListener("input", e => { state.counter_spell = e.target.value; save(); updateWordCount(); });
    $("balance_next_step").addEventListener("input", e => { state.balance_next_step = e.target.value; save(); updateWordCount(); });

    // Ritual
    ["ritual_anchor","ritual_stake","ritual_spell"].forEach(id => {
      $(id).addEventListener("input", () => {
        state.ritual = state.ritual || {anchor:"",stake:"",spell:"",paragraph:""};
        state.ritual.anchor = $("ritual_anchor").value;
        state.ritual.stake  = $("ritual_stake").value;
        state.ritual.spell  = $("ritual_spell").value;
        // auto compose if paragraph empty or exactly previous auto
        const auto = autoParagraph();
        if(($("ritual_paragraph").value || "").trim() === "" || ($("ritual_paragraph").dataset.auto === "1")){
          $("ritual_paragraph").value = auto;
          $("ritual_paragraph").dataset.auto = "1";
          state.ritual.paragraph = auto;
        }
        save();
        updateWordCount();
      });
    });

    $("ritual_paragraph").addEventListener("input", e => {
      state.ritual = state.ritual || {anchor:"",stake:"",spell:"",paragraph:""};
      state.ritual.paragraph = e.target.value;
      e.target.dataset.auto = "0";
      save();
      updateWordCount();
    });

    // Buttons
    $("btn_download_json").addEventListener("click", () => downloadJson());
    $("btn_export_md").addEventListener("click", () => downloadMarkdown());
    $("btn_copy_md").addEventListener("click", () => copyMarkdown());
    $("btn_reset").addEventListener("click", () => {
      if(confirm("Reset the map? (local draft will be cleared)")){
        state = defaultState();
        save();
        render();
      }
    });
    $("btn_load_example").addEventListener("click", () => loadExample());

    $("import_file").addEventListener("change", async (e) => {
      const file = e.target.files && e.target.files[0];
      if(!file) return;
      try{
        const txt = await file.text();
        const obj = JSON.parse(txt);
        state = normalizeImported(obj);
        save();
        render();
        e.target.value = "";
      }catch(err){
        alert("Could not import JSON: " + err);
      }
    });
  }

  function normalizeImported(obj){
    // Light normalization; keep user content.
    const s = defaultState();
    s.project = obj.project ?? s.project;
    s.date = obj.date ?? s.date;
    s.phase = (typeof obj.phase === "number") ? obj.phase : s.phase;
    s.intent = obj.intent ?? s.intent;
    s.axioms = Array.isArray(obj.axioms) ? obj.axioms.slice(0,3) : s.axioms;
    if(Array.isArray(obj.ingredients) && obj.ingredients.length === 9){
      s.ingredients = obj.ingredients.map((x,i) => ({
        key: INGREDIENTS[i].key,
        rating: clampInt(x.rating ?? 3, 0, 5),
        note: (x.note ?? "").toString()
      }));
    }
    if(Array.isArray(obj.operations) && obj.operations.length === 7){
      s.operations = obj.operations.map((x,i) => ({
        key: OPS[i].key,
        notes: (x.notes ?? "").toString()
      }));
    }
    s.archetypes = Array.isArray(obj.archetypes) ? obj.archetypes.filter(Boolean) : s.archetypes;
    s.shadow_loop = obj.shadow_loop ?? s.shadow_loop;
    s.counter_spell = obj.counter_spell ?? s.counter_spell;
    s.balance_next_step = obj.balance_next_step ?? s.balance_next_step;
    s.ritual = obj.ritual ?? s.ritual;
    if(!s.ritual) s.ritual = {anchor:"",stake:"",spell:"",paragraph:""};
    return s;
  }

  function clampInt(n, a, b){
    const x = parseInt(n,10);
    if(Number.isNaN(x)) return a;
    return Math.max(a, Math.min(b, x));
  }

  function autoParagraph(){
    const a = ($("ritual_anchor").value || "").trim();
    const s = ($("ritual_stake").value || "").trim();
    const p = ($("ritual_spell").value || "").trim();
    const bits = [a,s,p].filter(Boolean);
    return bits.join(" ");
  }

  function downloadJson(){
    const data = JSON.stringify(stateToExport(), null, 2);
    const blob = new Blob([data], {type:"application/json"});
    const name = fileSlug(state.project || "uniphi-alchemy-map") + ".json";
    triggerDownload(blob, name);
  }

  function copyMarkdown(){
    const md = toMarkdown();
    navigator.clipboard.writeText(md).then(
      () => alert("Markdown copied to clipboard."),
      () => alert("Could not copy (browser permission). Use Export Markdown instead.")
    );
  }

  function downloadMarkdown(){
    const md = toMarkdown();
    const blob = new Blob([md], {type:"text/markdown"});
    const name = fileSlug(state.project || "uniphi-alchemy-map") + ".md";
    triggerDownload(blob, name);
  }

  function triggerDownload(blob, filename){
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 500);
  }

  function fileSlug(s){
    return s.toLowerCase()
      .replace(/[^a-z0-9]+/g,"-")
      .replace(/^-+|-+$/g,"")
      .slice(0,64) || "uniphi-alchemy-map";
  }

  function stateToExport(){
    // Keep stable ordering + keys
    return {
      project: state.project,
      date: state.date,
      phase: state.phase,
      intent: state.intent,
      axioms: (state.axioms || []).slice(0,3).map(id => {
        const ax = AXIOMS.find(x => x.id === id);
        return ax ? ax.la : id;
      }),
      ingredients: state.ingredients.map(x => ({ key:x.key, rating:x.rating, note:x.note })),
      operations: state.operations.map(x => ({ key:x.key, notes:x.notes })),
      archetypes: (state.archetypes || []).slice(),
      shadow_loop: state.shadow_loop,
      counter_spell: state.counter_spell,
      balance_next_step: state.balance_next_step,
      ritual: state.ritual
    };
  }

  function toMarkdown(){
    const d = stateToExport();
    const phaseLabel = PHASES[d.phase] || "";
    const axLines = (d.axioms || []).map(a => `- ${a}`).join("\n") || "- (none)";
    const nameMap = {
      aether:"Aether (Vision)",
      mercurius:"Mercurius (Flow / Interoperability)",
      sulfur:"Sulfur (Soul / Urgency)",
      sal:"Sal (Structure / Truth)",
      vessel:"Vessel (Infrastructure / Container)",
      catalyst:"Catalyst (Play / Risk)",
      noise:"Noise (Oracle / Randomness)",
      guardian:"Guardian (Security / Boundaries)",
      witness:"Witness (Story / Audience)",
    };
    const opMap = {
      calcination:"Calcination",
      dissolution:"Dissolution",
      separation:"Separation",
      conjunction:"Conjunction",
      fermentation:"Fermentation",
      distillation:"Distillation",
      coagulation:"Coagulation",
    };
    const archMap = {
      cosmic_chemist:"Cosmic Chemist",
      net_weaver:"Net Weaver",
      vault_guardian:"Vault Guardian",
      trickster_debugger:"Trickster Debugger",
      archivist:"Archivist",
      poet_witness:"Poet-Witness",
    };
    const ing = d.ingredients.map(x => `### ${nameMap[x.key] || x.key} — ${x.rating}/5\n\n${x.note || ""}\n`).join("\n");
    const ops = d.operations.map(x => `### ${opMap[x.key] || x.key}\n\n${x.notes || ""}\n`).join("\n");
    const arch = (d.archetypes || []).map(a => archMap[a] || a).join(", ") || "(none)";
    const r = d.ritual || {anchor:"",stake:"",spell:"",paragraph:""};

    return `# Uniphi-Alchemy Map

**Project / Work:** ${d.project || ""}  
**Date:** ${d.date || ""}  
**Phase (0–6):** ${d.phase} (${phaseLabel})  
**One-line intent:** ${d.intent || ""}

---

## Axioms
${axLines}

---

## 9 Ingredients
${ing}

---

## 7 Operations
${ops}

---

## Archetypes
${arch}

### Shadow Loop
${d.shadow_loop || "none"}

### Counter-spell
${d.counter_spell || ""}

---

## Balance Next Step
${d.balance_next_step || ""}

---

## 60-Second Ritual
**Anchor:** ${r.anchor || ""}  
**Stake:** ${r.stake || ""}  
**Spell:** ${r.spell || ""}

${r.paragraph || ""}

`;
  }

  function loadExample(){
    // a gentle example, not overwriting if user cancels
    if(!confirm("Load the example map? This will overwrite your current draft.")) return;
    state = {
      project: "Spectra — Nebula Field UI Pass",
      date: new Date().toISOString().slice(0,10),
      phase: 4,
      intent: "Unify shader controls + observability into a single calm cockpit that still feels alive.",
      axioms: ["solve_et_coagula","veritas_in_structura","testis_facit_mundum"],
      ingredients: [
        {key:"aether",rating:5,note:"The universe-feel is the north star: everything must glow with meaning."},
        {key:"mercurius",rating:4,note:"Controls, telemetry, and visuals must flow together without friction."},
        {key:"sulfur",rating:3,note:"Keep one human pulse in the interface: warmth, not just precision."},
        {key:"sal",rating:4,note:"Naming + grouping is the truth layer; no slider without a reason."},
        {key:"vessel",rating:4,note:"Single-file deployment, predictable state, no dependencies."},
        {key:"catalyst",rating:4,note:"Allow playful toggles (hue, EM, magnetism) to invite discovery."},
        {key:"noise",rating:3,note:"Seeded randomness as oracle, not as clutter."},
        {key:"guardian",rating:3,note:"Safe defaults, but not a cage; keep the door open."},
        {key:"witness",rating:4,note:"The ‘aha’ moment: one button that reveals the universe behind the numbers."},
      ],
      operations: [
        {key:"calcination",notes:"Kill duplicate controls; keep only what changes experience. Constraint: single-file, offline."},
        {key:"dissolution",notes:"Prototype UI grouping fast; let the shader run wild while controls settle."},
        {key:"separation",notes:"Core: (1) field params, (2) color/energy, (3) observability. Decorations later."},
        {key:"conjunction",notes:"Bind UI -> shader uniforms -> telemetry in one loop, with stable naming."},
        {key:"fermentation",notes:"Leave one unknown: emergent interference patterns from parameter coupling."},
        {key:"distillation",notes:"Refactor: remove 2 sliders, merge 2 metrics, rename 5 params for clarity."},
        {key:"coagulation",notes:"Ship: a single HTML file + a one-page readme; witness: screen-recorded 30s tour."},
      ],
      archetypes: ["cosmic_chemist","net_weaver","poet_witness"],
      shadow_loop: "release_shyness",
      counter_spell: "Ship the smallest living ritual; refinement can orbit the release.",
      balance_next_step: "Add one concrete anchor line in the UI (‘temperature of the void: …’) and publish the file.",
      ritual: {
        anchor:"The laptop fan warms the room like a small sun.",
        stake:"I want the cockpit to welcome curiosity instead of intimidating it.",
        spell:"So I let noise become constellations, and structure become a gentle handrail.",
        paragraph:"The laptop fan warms the room like a small sun. I want the cockpit to welcome curiosity instead of intimidating it. So I let noise become constellations, and structure become a gentle handrail."
      }
    };
    save();
    render();
  }

  function updateWordCount(){
    const md = toMarkdown();
    const words = md.trim().split(/\s+/).filter(Boolean).length;
    $("wordcount").innerHTML = `Words: <b>${words}</b>`;
  }

  // Boot
  function ensureDate(){
    if(!state.date){
      state.date = new Date().toISOString().slice(0,10);
    }
  }

  ensureDate();
  bind();
  render();
  save();

})();
</script>
</body>
</html>
likes 0comments 0
Ideohtml
Figure de Style
<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Figures de style – Jardin des Idées</title>
  <style>
    /* --------------------
       Color palette & base
    -------------------- */
    :root {
      --bg: #faf8f3;
      --fg: #213c34;
      --accent: #40f2d0;
      --accent-dark: #2ba899;
    }
    *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
    body{
      font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;
      background:var(--bg);
      color:var(--fg);
      line-height:1.55;
      min-height:100svh;
      overflow-x:hidden;
    }
    header,footer{padding:1.8rem 6vw;display:flex;justify-content:space-between;align-items:center;backdrop-filter:blur(3px)}
    header h1{font-size:clamp(1.4rem,2.5vw+1rem,2.8rem);font-weight:500;letter-spacing:.03em}
    main{padding:2rem 6vw;max-width:70rem;margin-inline:auto;position:relative;z-index:1}
    #search{width:100%;padding:.7rem 1rem;border:1px solid var(--accent-dark);border-radius:.6rem;font-size:1rem;margin-bottom:1.6rem}

    /* Accordions */
    details{background:rgba(255,255,255,.65);border:1px solid var(--accent);border-radius:1rem;margin-bottom:.8rem;overflow:hidden}
    summary{cursor:pointer;padding:1rem 1.4rem;font-size:1.12rem;list-style:none;display:flex;justify-content:space-between;align-items:center;user-select:none}
    summary::after{content:"➕";transition:transform .3s ease}
    details[open] summary::after{content:"➖";transform:rotate(180deg)}
    ul.category{padding:0 1.6rem 1.2rem 2rem}
    ul.category li{margin:.4rem 0;transition:background .2s ease;padding:.2rem .4rem;border-radius:.4rem}
    ul.category li:hover{background:var(--accent);color:#fff}

    /* Footer */
    footer p{font-size:.9rem;opacity:.65}

    /* --------------
       Background canvas
    -------------- */
    #bg{position:fixed;inset:0;z-index:-1;pointer-events:none}

    /* Responsive tweaks */
    @media(max-width:600px){
      header,footer{padding:1.2rem 4vw}
      main{padding:1.4rem 4vw}
    }
  </style>
</head>
<body>
  <canvas id="bg"></canvas>
  <header>
    <h1>Jardin des Idées</h1>
    <span aria-hidden="true">🌿</span>
  </header>

  <main>
    <input type="search" id="search" placeholder="Rechercher une figure…" />

    <!-- Les différentes figures de style -->
    <details open>
      <summary>Les figures d’analogie</summary>
      <ul class="category">
        <li>La comparaison</li>
        <li>La métaphore</li>
        <li>La personnification</li>
        <li>L’allégorie <em>(notion avancée)</em></li>
      </ul>
    </details>

    <details>
      <summary>Les figures d’amplification</summary>
      <ul class="category">
        <li>L’énumération</li>
        <li>La gradation</li>
        <li>L’hyperbole</li>
      </ul>
    </details>

    <details>
      <summary>Les figures d’insistance</summary>
      <ul class="category">
        <li>La répétition</li>
        <li>L’anaphore <em>(notion avancée)</em></li>
        <li>La redondance <em>(notion avancée)</em></li>
        <li>Le pléonasme <em>(notion avancée)</em></li>
      </ul>
    </details>

    <details>
      <summary>Les figures d’atténuation et d’omission</summary>
      <ul class="category">
        <li>L’euphémisme</li>
        <li>La litote</li>
        <li>L’ellipse <em>(notion avancée)</em></li>
      </ul>
    </details>

    <details>
      <summary>Les figures d’opposition</summary>
      <ul class="category">
        <li>L’antithèse</li>
        <li>L’oxymore</li>
        <li>L’ironie</li>
        <li>Le chiasme <em>(notion avancée)</em></li>
      </ul>
    </details>

    <details>
      <summary>Les figures de substitution</summary>
      <ul class="category">
        <li>La périphrase</li>
        <li>La métonymie</li>
        <li>La synecdoque <em>(notion avancée)</em></li>
      </ul>
    </details>
  </main>

  <footer>
    <p>&copy; 2025 Kobalt – Exploration des écosystèmes créatifs</p>
  </footer>

  <script>
    /* -----------------------------
       Instant search / filter list
    ----------------------------- */
    const searchInput = document.getElementById("search");
    searchInput.addEventListener("input", e => {
      const q = e.target.value.toLowerCase();
      document.querySelectorAll("ul.category li").forEach(li => {
        li.style.display = li.textContent.toLowerCase().includes(q) ? "list-item" : "none";
      });
    });

    /* -----------------------------------------
       Minimal generative background (organic points)
    ----------------------------------------- */
    const canvas = document.getElementById("bg");
    const ctx = canvas.getContext("2d");
    let W, H;
    const dots = [];
    const DOTS = 420;

    function resize() {
      W = canvas.width = window.innerWidth * devicePixelRatio;
      H = canvas.height = window.innerHeight * devicePixelRatio;
      canvas.style.width = window.innerWidth + "px";
      canvas.style.height = window.innerHeight + "px";
      ctx.scale(devicePixelRatio, devicePixelRatio);
    }
    resize();
    window.addEventListener("resize", resize);

    // create initial field of dots following differential‑growth-ish directions
    function initDots() {
      dots.length = 0;
      for (let i = 0; i < DOTS; i++) {
        dots.push({
          x: Math.random() * window.innerWidth,
          y: Math.random() * window.innerHeight,
          vx: (Math.random() - 0.5) * 0.6,
          vy: (Math.random() - 0.5) * 0.6
        });
      }
    }
    initDots();

    function step() {
      ctx.fillStyle = "rgba(250, 248, 243, 0.1)";
      ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
      ctx.fillStyle = "rgba(33, 60, 52, 0.9)";
      dots.forEach(p => {
        // simple edge‑bounce
        if (p.x < 0 || p.x > window.innerWidth) p.vx *= -1;
        if (p.y < 0 || p.y > window.innerHeight) p.vy *= -1;
        // subtle acceleration towards centre to mimic growth toward nutrients
        const dx = window.innerWidth / 2 - p.x;
        const dy = window.innerHeight / 2 - p.y;
        p.vx += dx * 0.000015;
        p.vy += dy * 0.000015;
        // speed limit
        const spd = Math.hypot(p.vx, p.vy);
        const max = 0.9;
        if (spd > max) { p.vx *= max / spd; p.vy *= max / spd; }
        p.x += p.vx;
        p.y += p.vy;
        ctx.beginPath();
        ctx.arc(p.x, p.y, 1.4, 0, Math.PI * 2);
        ctx.fill();
      });
      requestAnimationFrame(step);
    }
    step();
  </script>
</body>
</html>
likes 0comments 0