Created
June 15, 2026 22:46
-
-
Save gkucmierz/a20230f2139ef4083b046480eb376cfb to your computer and use it in GitHub Desktop.
Run this code instantly in your browser: https://instacode.app/gist/a20230f2139ef4083b046480eb376cfb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { getContext, onResize, onPointerDown } from 'canvas'; | |
| // --- CONFIGURATION CONSTANTS --- | |
| const MAX_POINTS = 50000; // Total number of fractal points to generate | |
| const STAGE_1_POINTS = 15; // Number of points in slow educational phase | |
| const STAGE_1_DELAY_MS = 400; // Delay between steps in slow phase (milliseconds) | |
| const STAGE_2_POINTS = 100; // Number of points in normal phase (1 point/frame) | |
| const STAGE_3_POINTS = 500; // Number of points in medium phase (10 points/frame) | |
| const STAGE_4_SPEED = 250; // Number of points generated per frame in fast phase | |
| // Styling and Colors | |
| const COLOR_BACKGROUND = '#0f172a'; // Deep slate background | |
| const COLOR_FRACTAL = 'rgba(56, 189, 248, 0.55)'; // Sky blue for accumulated points | |
| const COLOR_VERTEX = 'rgba(244, 63, 94, 0.85)'; // Rose glow for main wierzcholki | |
| const COLOR_GUIDE = 'rgba(245, 158, 11, 0.35)'; // Amber for step line guides | |
| const COLOR_ACTIVE = '#fbbf24'; // Bright amber for active moving point | |
| // ------------------------------- | |
| const ctx = getContext('2d'); | |
| let width = 400; | |
| let height = 400; | |
| let vertices = []; | |
| let px = 0; | |
| let py = 0; | |
| let chosenVertex = null; | |
| let lastPx = 0; | |
| let lastPy = 0; | |
| // Typed arrays to store generated points (highly performant) | |
| const pointsX = new Float32Array(MAX_POINTS); | |
| const pointsY = new Float32Array(MAX_POINTS); | |
| let pointCount = 0; | |
| let lastStepTime = 0; | |
| let isFinished = false; | |
| // Initialize triangle vertices and starting point | |
| function initSimulation() { | |
| const size = Math.min(width, height) * 0.82; | |
| const h = size * Math.sin(Math.PI / 3); // Equilateral triangle height | |
| const centerX = width / 2; | |
| const centerY = height / 2 + 25; | |
| vertices = [ | |
| { name: 'A', x: centerX, y: centerY - h / 2 }, | |
| { name: 'B', x: centerX - size / 2, y: centerY + h / 2 }, | |
| { name: 'C', x: centerX + size / 2, y: centerY + h / 2 } | |
| ]; | |
| // Start with a random point in the center area | |
| px = centerX + (Math.random() - 0.5) * 50; | |
| py = centerY + (Math.random() - 0.5) * 50; | |
| pointCount = 0; | |
| isFinished = false; | |
| chosenVertex = null; | |
| } | |
| // Reset and restart simulation on user click | |
| onPointerDown(() => { | |
| initSimulation(); | |
| }); | |
| // Re-initialize layout on canvas resize | |
| onResize((w, h) => { | |
| width = w; | |
| height = h; | |
| initSimulation(); | |
| }); | |
| // Single step of the chaos game | |
| function step() { | |
| if (pointCount >= MAX_POINTS) { | |
| isFinished = true; | |
| return; | |
| } | |
| // 1. Pick a random vertex (A, B, or C) | |
| const randIdx = Math.floor(Math.random() * 3); | |
| chosenVertex = vertices[randIdx]; | |
| // 2. Store the current position as the last position | |
| lastPx = px; | |
| lastPy = py; | |
| // 3. Move the point halfway towards the chosen vertex | |
| px = (px + chosenVertex.x) / 2; | |
| py = (py + chosenVertex.y) / 2; | |
| // 4. Save the new point coordinates | |
| pointsX[pointCount] = px; | |
| pointsY[pointCount] = py; | |
| pointCount++; | |
| } | |
| // Animation loop | |
| function animate(time) { | |
| if (!isFinished) { | |
| if (pointCount < STAGE_1_POINTS) { | |
| // Stage 1: Slow educational step-by-step | |
| if (time - lastStepTime > STAGE_1_DELAY_MS) { | |
| step(); | |
| lastStepTime = time; | |
| } | |
| } else if (pointCount < STAGE_2_POINTS) { | |
| // Stage 2: Steady speed (1 point per frame) | |
| step(); | |
| } else if (pointCount < STAGE_3_POINTS) { | |
| // Stage 3: Faster (10 points per frame) | |
| for (let i = 0; i < 10; i++) step(); | |
| } else { | |
| // Stage 4: High-speed generation | |
| for (let i = 0; i < STAGE_4_SPEED; i++) step(); | |
| } | |
| } | |
| draw(); | |
| requestAnimationFrame(animate); | |
| } | |
| // Draw frame | |
| function draw() { | |
| // Clear background | |
| ctx.fillStyle = COLOR_BACKGROUND; | |
| ctx.fillRect(0, 0, width, height); | |
| // 1. Draw all accumulated points | |
| ctx.fillStyle = COLOR_FRACTAL; | |
| for (let i = 0; i < pointCount; i++) { | |
| ctx.fillRect(pointsX[i] - 0.5, pointsY[i] - 0.5, 1, 1); | |
| } | |
| // 2. Draw the three main vertices with glow | |
| vertices.forEach(v => { | |
| // Outer neon glow | |
| const grad = ctx.createRadialGradient(v.x, v.y, 2, v.x, v.y, 14); | |
| grad.addColorStop(0, COLOR_VERTEX); | |
| grad.addColorStop(1, 'rgba(0, 0, 0, 0)'); | |
| ctx.fillStyle = grad; | |
| ctx.beginPath(); | |
| ctx.arc(v.x, v.y, 14, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Solid core point | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.beginPath(); | |
| ctx.arc(v.x, v.y, 4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Vertex name tag | |
| ctx.fillStyle = '#f1f5f9'; | |
| ctx.font = 'bold 13px sans-serif'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'bottom'; | |
| ctx.fillText(v.name, v.x, v.y - 16); | |
| }); | |
| // 3. Draw educational guides (only during slow startup) | |
| if (pointCount < STAGE_2_POINTS && chosenVertex) { | |
| // Dotted line connecting current point to chosen vertex | |
| ctx.strokeStyle = COLOR_GUIDE; | |
| ctx.lineWidth = 1.5; | |
| ctx.setLineDash([4, 4]); | |
| ctx.beginPath(); | |
| ctx.moveTo(lastPx, lastPy); | |
| ctx.lineTo(chosenVertex.x, chosenVertex.y); | |
| ctx.stroke(); | |
| ctx.setLineDash([]); // Reset line dash | |
| // Highlight current active point with a glowing halo | |
| ctx.fillStyle = COLOR_ACTIVE; | |
| ctx.shadowColor = COLOR_ACTIVE; | |
| ctx.shadowBlur = 8; | |
| ctx.beginPath(); | |
| ctx.arc(px, py, 4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; // Reset shadow | |
| } | |
| // 4. UI texts and labels | |
| ctx.fillStyle = '#f8fafc'; | |
| ctx.font = 'bold 18px sans-serif'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'top'; | |
| ctx.fillText("The Chaos Game: Sierpinski Triangle", width / 2, 20); | |
| ctx.fillStyle = '#94a3b8'; | |
| ctx.font = '13px monospace'; | |
| ctx.fillText(`Points generated: ${pointCount.toLocaleString()}`, width / 2, 50); | |
| if (pointCount < STAGE_2_POINTS) { | |
| ctx.fillStyle = '#f59e0b'; | |
| ctx.font = 'italic 12px sans-serif'; | |
| ctx.fillText("Rule: Move 1/2 of the way to a random vertex", width / 2, 74); | |
| } else if (isFinished) { | |
| ctx.fillStyle = '#10b981'; // Emerald 500 | |
| ctx.font = 'bold 13px sans-serif'; | |
| ctx.fillText("Simulation complete! Click canvas to restart.", width / 2, 74); | |
| } else { | |
| ctx.fillStyle = '#38bdf8'; // Sky 400 | |
| ctx.font = '12px sans-serif'; | |
| ctx.fillText("Generating fractal at high speed...", width / 2, 74); | |
| } | |
| } | |
| // Start simulation | |
| initSimulation(); | |
| requestAnimationFrame(animate); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment