Reverse engineering pico-8 lua into html5

Original code via

rainbow spiral | 64 second loop#pico8 #postcart (256 chars ~ src in reply)

zep.p8 (@lexaloffle.bsky.social) 2024-10-25T09:33:19.254Z

::★::cls()for r=4,128,4 do for i=0,7/4,1/4doq=(t()*(1+r/32)/8)%1v0=mid((q-i)*4,1) v1=mid((q-i)*4+2,1)a=i-1/8 x=64+cos(a)*r*0.71 y=64+sin(a)*r*0.71a+=3/8u=cos(a)v=sin(a)if(v1>v0)line(x+u*v0*r,y+v*v0*r,x+u*v1*r,y+v*v1*r,8+(r/4)%8)end end flip() goto ★// gif: www.lexaloffle.com/bbs/?tid=144…

zep.p8 (@lexaloffle.bsky.social) 2024-10-25T09:34:58.225Z

Reverse-engineered into a html5 widget

https://svonberg.org/wp-content/uploads/2026/09/square.html

<!DOCTYPE html>
<html lang=”en”>
<head>
    <meta charset=”UTF-8″>
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
    <title>Rotating Geometric Widget</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            background-color: #111; /* Dark background to match PICO-8 vibe */
            overflow: hidden;
        }
        /* Container to center and scale the canvas */
        #canvas-container {
            position: relative;
            /* Maintaining a square aspect ratio similar to PICO-8 */
            width: 90vmin;
            height: 90vmin;
            max-width: 512px;
            max-height: 512px;
            box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
        }
        canvas {
            display: block;
            width: 100%;
            height: 100%;
            background-color: #000; /* PICO-8 cls() default black */
            image-rendering: pixelated; /* Give it a retro feel */
        }
    </style>
</head>
<body>

    <div id=”canvas-container”>
        <canvas id=”pico-canvas” width=”128″ height=”128″></canvas>
    </div>

    <script>
        // Get canvas context
        const canvas = document.getElementById(‘pico-canvas’);
        const ctx = canvas.getContext(‘2d’);

        // PICO-8 color palette (approximations)
        const pico8Palette = [
            “#000000”, “#1D2B53”, “#7E2553”, “#008751”,
            “#AB5236”, “#5F574F”, “#C2C3C7”, “#FFF1E8”,
            “#FF004D”, “#FFA300”, “#FFEC27”, “#00E436”,
            “#29ADFF”, “#83769C”, “#FF77A8”, “#FFCCAA”
        ];

        // Helper functions matching PICO-8 behavior
        function mid(x, y) {
            // PICO-8 mid(x, y, z) usually takes 3 args to clamp.
            // The lua snippet uses mid(val, 1). This is a trick in PICO-8 where missing args default to 0.
            // So mid((q-i)*4, 1) is equivalent to mid(0, (q-i)*4, 1), which is clamping between 0 and 1.
            return Math.max(0, Math.min(x, 1)); // Clamping between 0 and 1, assuming implicit 0 as 3rd arg behavior
        }

        function t() {
            // Returns time in seconds
            return performance.now() / 1000;
        }

        // Main animation loop
        function draw() {
            // Equivalent to cls()
            ctx.fillStyle = pico8Palette[0];
            ctx.fillRect(0, 0, 128, 128);

            const time = t();

            // for r=4,128,4 do
            for (let r = 4; r <= 128; r += 4) {
                // for i=0,7/4,1/4 do
                for (let i = 0; i <= 7 / 4; i += 1 / 4) {
                    // q=(t()*(1+r/32)/8)%1
                    const q = (time * (1 + r / 32) / 8) % 1;

                    // v0=mid((q-i)*4,1)
                    // The original lua uses a 2-arg mid, which in PICO-8 defaults the 3rd to 0.
                    // effectively clamping between 0 and 1.
                    const val0 = (q – i) * 4;
                    const v0 = Math.max(0, Math.min(val0, 1));

                    // v1=mid((q-i)*4+2,1)
                    const val1 = (q – i) * 4 + 2;
                    const v1 = Math.max(0, Math.min(val1, 1));

                    // a=i-1/8 x=64+cos(a)*r*0.71 y=64+sin(a)*r*0.71
                    // Note: PICO-8 sin/cos use turns (0-1), standard JS uses radians (0-2PI)
                    let a = i – 1 / 8;
                    const a_rad = a * Math.PI * 2; // Convert turns to radians
                    // PICO-8 cos() is standard, but sin() is inverted (y goes down)
                    const x = 64 + Math.cos(a_rad) * r * 0.71;
                    // Invert sin for standard JS canvas to match PICO-8 visual (PICO-8 sin is negative of math.sin)
                    const y = 64 + (-Math.sin(a_rad)) * r * 0.71;

                    // a+=3/8 u=cos(a) v=sin(a)
                    a += 3 / 8;
                    const a_rad2 = a * Math.PI * 2;
                    const u = Math.cos(a_rad2);
                    const v = -Math.sin(a_rad2); // Invert sin

                    // if(v1>v0)line(x+u*v0*r,y+v*v0*r,x+u*v1*r,y+v*v1*r,8+(r/4)%8)
                    if (v1 > v0) {
                        const startX = x + u * v0 * r;
                        const startY = y + v * v0 * r;
                        const endX = x + u * v1 * r;
                        const endY = y + v * v1 * r;
                       
                        // Color index: 8+(r/4)%8
                        const colorIndex = 8 + (Math.floor(r / 4) % 8);
                       
                        ctx.beginPath();
                        ctx.moveTo(startX, startY);
                        ctx.lineTo(endX, endY);
                       
                        // PICO-8 lines are generally 1 pixel wide
                        ctx.lineWidth = 1;
                        // Use the palette, fallback to white if out of bounds
                        ctx.strokeStyle = pico8Palette[colorIndex] || “#FFFFFF”;
                        ctx.stroke();
                    }
                }
            }

            // Equivalent to flip() and goto loop
            requestAnimationFrame(draw);
        }

        // Start the loop
        window.onload = function() {
            draw();
        }
    </script>
</body>
</html>