XTaylorSpenceX

Pulse Painter Pathways

Oct 1st, 2025
61
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 20.56 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <title>Pulse Painter Pathways</title>
  7. <style>
  8.   * { margin: 0; padding: 0; box-sizing: border-box; }
  9.   body {
  10.     background: linear-gradient(135deg, #0a0014 0%, #1a0829 50%, #0a0014 100%);
  11.     color: #fff;
  12.     font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
  13.     display: flex; justify-content: center; align-items: center;
  14.     min-height: 100vh; overflow: hidden;
  15.   }
  16.   .container { position: relative; display: flex; flex-direction: column; align-items: center; gap: 20px; }
  17.   .header { text-align: center; animation: float 4s ease-in-out infinite; }
  18.   @keyframes float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-10px)} }
  19.   h1 {
  20.     font-size: 2.5em; margin-bottom: 10px;
  21.     background: linear-gradient(45deg, #ff00ff, #00ffff, #ff00ff); background-size: 200% 200%;
  22.     -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  23.     animation: gradient 3s ease infinite;
  24.   }
  25.   @keyframes gradient { 0%{background-position:0% 50%} 50%{background-position:100% 50%} 100%{background-position:0% 50%} }
  26.   .info { display: flex; gap: 30px; font-size: 1.1em; margin-bottom: 10px; }
  27.   .info-item { padding: 8px 16px; background: rgba(255,255,255,0.1); border-radius: 20px; backdrop-filter: blur(10px); }
  28.   #gameCanvas {
  29.     border: 2px solid rgba(255,255,255,0.2); border-radius: 10px; background: #0a0014;
  30.     box-shadow: 0 0 50px rgba(128,0,255,0.3); cursor: crosshair; position: relative;
  31.   }
  32.   .controls { display: flex; gap: 15px; margin-top: 10px; }
  33.   button {
  34.     padding: 12px 24px; font-size: 1em; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  35.     color: white; border: none; border-radius: 25px; cursor: pointer; transition: all 0.25s ease;
  36.     box-shadow: 0 4px 15px rgba(102,126,234,0.4);
  37.   }
  38.   button:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(102,126,234,0.6); }
  39.   button:active { transform: translateY(0); }
  40.   .legend {
  41.     position: absolute; top: 20px; right: 20px; background: rgba(0,0,0,0.8);
  42.     padding: 15px; border-radius: 10px; backdrop-filter: blur(10px); font-size: 0.9em;
  43.   }
  44.   .legend-item { display: flex; align-items: center; gap: 10px; margin: 5px 0; }
  45.   .legend-color { width: 20px; height: 20px; border-radius: 50%; }
  46.   .pulse { animation: pulse 2s ease-in-out infinite; }
  47.   @keyframes pulse { 0%,100%{opacity:.3;transform:scale(.95)} 50%{opacity:1;transform:scale(1.05)} }
  48.   .glow { animation: glow 1.5s ease-in-out infinite alternate; }
  49.   @keyframes glow { from{ box-shadow:0 0 10px currentColor } to{ box-shadow:0 0 20px currentColor, 0 0 30px currentColor } }
  50.   .particle {
  51.     position: absolute; width: 4px; height: 4px; background: white; border-radius: 50%; pointer-events: none;
  52.     animation: particleFloat 3s ease-in-out infinite;
  53.   }
  54.   @keyframes particleFloat { 0%{transform:translateY(0) translateX(0);opacity:0} 10%{opacity:1} 90%{opacity:1} 100%{transform:translateY(-100px) translateX(50px);opacity:0} }
  55.  
  56.   /* ====== START OVERLAY ====== */
  57.   .overlay {
  58.     position: fixed; inset: 0; z-index: 9999; display: grid; place-items: center; padding: 24px;
  59.     background:
  60.       radial-gradient(1200px 600px at 20% 20%, rgba(255,0,255,0.08), transparent 70%),
  61.       radial-gradient(1000px 700px at 80% 70%, rgba(0,255,255,0.08), transparent 70%),
  62.       rgba(6, 2, 16, 0.85);
  63.     backdrop-filter: blur(6px);
  64.   }
  65.   .overlay-card {
  66.     width: min(880px, 92vw); border-radius: 16px; padding: 28px 28px 22px;
  67.     background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
  68.     border: 1px solid rgba(255,255,255,0.14); box-shadow: 0 20px 60px rgba(0,0,0,0.45);
  69.   }
  70.   .overlay h2 { font-size: clamp(22px, 3.6vw, 30px); margin-bottom: 12px; }
  71.   .overlay p { color: #e6e6ff; opacity: 0.9; line-height: 1.5; margin-bottom: 14px; }
  72.   .kbd {
  73.     display: inline-flex; align-items: center; justify-content: center; min-width: 1.75em; padding: 2px 8px;
  74.     border-radius: 8px; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.18);
  75.     font-weight: 600; font-size: 0.9em; margin: 0 2px; color: #fff;
  76.   }
  77.   .rules { display: grid; gap: 10px; margin: 14px 0 18px; }
  78.   .rule { display: grid; grid-template-columns: 24px 1fr; gap: 10px; align-items: start; }
  79.   .dot { width: 12px; height: 12px; border-radius: 50%; margin-top: 6px; }
  80.   .overlay-actions { display: flex; flex-wrap: wrap; gap: 10px; justify-content: flex-end; margin-top: 10px; }
  81.   .ghost-btn {
  82.     background: transparent; border: 1px solid rgba(255,255,255,0.25); box-shadow: none;
  83.   }
  84.   .hidden { display: none !important; }
  85.  
  86.   /* Dim canvas & controls visually while overlay is up */
  87.  body[data-gate="blocked"] #gameCanvas, body[data-gate="blocked"] .controls, body[data-gate="blocked"] .legend, body[data-gate="blocked"] .info {
  88.    filter: blur(1.5px) saturate(0.8); opacity: 0.75; pointer-events: none;
  89.   }
  90. </style>
  91. </head>
  92. <body data-gate="blocked">
  93.   <!-- Overlay Gate -->
  94.   <div id="startOverlay" class="overlay" aria-modal="true" role="dialog" aria-labelledby="overlayTitle">
  95.     <div class="overlay-card">
  96.       <h2 id="overlayTitle">Welcome to <span style="background:linear-gradient(45deg,#ff00ff,#00ffff);-webkit-background-clip:text;-webkit-text-fill-color:transparent">Pulse Painter Pathways</span></h2>
  97.       <p><strong>The instructions will disappear when you press <span class="kbd">Begin!</span>.</strong> Read these quick notes before you play.</p>
  98.       <div class="rules">
  99.         <div class="rule"><span class="dot" style="background:#00ff88"></span><p><strong>Start</strong>: Click-and-drag beginning from the glowing green node.</p></div>
  100.         <div class="rule"><span class="dot" style="background:#ffaa00"></span><p><strong>Goal</strong>: Reach the pulsing golden portal to clear the level.</p></div>
  101.         <div class="rule"><span class="dot" style="background:#ff00a8"></span><p><strong>Avoid pulses</strong>: Cells with intense magenta pulses are blocked while they’re bright.</p></div>
  102.         <div class="rule"><span class="dot" style="background:#8a2be2"></span><p><strong>Movement</strong>: Draw 4‑way or diagonal (8‑way) adjacent steps across the grid.</p></div>
  103.         <div class="rule"><span class="dot" style="background:#67e8f9"></span><p><strong>Tip</strong>: Time your path between wave peaks — pulses ebb and flow.</p></div>
  104.       </div>
  105.       <p><strong>Controls</strong>: <span class="kbd">Pause/Resume</span> to think; <span class="kbd">Reset</span> to restart the canvas.</p>
  106.       <div class="overlay-actions">
  107.         <button id="btnBegin">Begin!</button>
  108.       </div>
  109.     </div>
  110.   </div>
  111.  
  112.   <div class="container">
  113.     <div class="header">
  114.       <h1>Pulse Painter Pathways</h1>
  115.       <div class="info">
  116.         <div class="info-item">Score: <span id="score">0</span></div>
  117.         <div class="info-item">Level: <span id="level">1</span></div>
  118.         <div class="info-item">Time: <span id="timer">0</span>s</div>
  119.       </div>
  120.     </div>
  121.  
  122.     <canvas id="gameCanvas" width="800" height="600" aria-label="Game canvas"></canvas>
  123.  
  124.     <div class="controls" aria-label="Controls">
  125.       <button id="btnStartJourney" title="Restart the run">Start Journey</button>
  126.       <button id="btnReset">Reset Canvas</button>
  127.       <button id="btnPause">Pause/Resume</button>
  128.     </div>
  129.  
  130.     <div class="legend">
  131.       <div class="legend-item"><div class="legend-color" style="background:#00ff88"></div><span>Active Path</span></div>
  132.       <div class="legend-item"><div class="legend-color" style="background:#ff00ff"></div><span>Pulse Zone</span></div>
  133.       <div class="legend-item"><div class="legend-color" style="background:#ffaa00"></div><span>Goal Portal</span></div>
  134.     </div>
  135.   </div>
  136.  
  137. <script>
  138.   const canvas = document.getElementById('gameCanvas');
  139.   const ctx = canvas.getContext('2d');
  140.  
  141.   class PulsePainterGame {
  142.     constructor() {
  143.       this.gridSize = 20;
  144.       this.cols = Math.floor(canvas.width / this.gridSize);
  145.       this.rows = Math.floor(canvas.height / this.gridSize);
  146.       this.grid = [];
  147.       this.pulseZones = [];
  148.       this.playerPath = [];
  149.       this.playerPos = null;
  150.       this.goalPos = null;
  151.       this.score = 0;
  152.       this.level = 1;
  153.       this.time = 0;
  154.       this.isRunning = false;
  155.       this.isPaused = false;
  156.       this.pulsePhase = 0;
  157.       this.pulseSpeed = 0.02;
  158.       this.pathColor = '#00ff88';
  159.       this.drawing = false;
  160.  
  161.       this.init();
  162.     }
  163.  
  164.     init() {
  165.       // Initialize grid
  166.       for (let i = 0; i < this.rows; i++) {
  167.        this.grid[i] = [];
  168.        for (let j = 0; j < this.cols; j++) {
  169.          this.grid[i][j] = { type: 'empty', pulseIntensity: 0, visited: false };
  170.        }
  171.      }
  172.  
  173.      // Set start and goal positions
  174.      this.playerPos = { x: 1, y: Math.floor(this.rows / 2) };
  175.      this.goalPos   = { x: this.cols - 2, y: Math.floor(this.rows / 2) };
  176.  
  177.      // Create pulse zones
  178.      this.createPulseZones();
  179.  
  180.      // Input events (mouse + touch)
  181.      canvas.addEventListener('mousedown', (e) => this.startDrawing(e));
  182.       canvas.addEventListener('mousemove', (e) => this.draw(e));
  183.       canvas.addEventListener('mouseup',   ()  => this.stopDrawing());
  184.       canvas.addEventListener('mouseleave',()  => this.stopDrawing());
  185.       canvas.addEventListener('touchstart',(e) => { e.preventDefault(); this.startDrawing(e.touches[0]); }, {passive:false});
  186.       canvas.addEventListener('touchmove', (e) => { e.preventDefault(); this.draw(e.touches[0]); }, {passive:false});
  187.       canvas.addEventListener('touchend',  ()  => this.stopDrawing());
  188.  
  189.       // Wire up control buttons
  190.       document.getElementById('btnStartJourney').addEventListener('click', () => this.start());
  191.       document.getElementById('btnReset').addEventListener('click', () => this.reset());
  192.       document.getElementById('btnPause').addEventListener('click', () => this.togglePause());
  193.  
  194.       // Overlay gate buttons
  195.       document.getElementById('btnBegin').addEventListener('click', () => this.gatedStart());
  196.  
  197.       // Keyboard convenience: Enter starts, Space pauses (after started)
  198.       window.addEventListener('keydown', (e) => {
  199.         if (this.isRunning && (e.code === 'Space')) {
  200.          e.preventDefault();
  201.           this.togglePause();
  202.         }
  203.       });
  204.     }
  205.  
  206.     gatedStart() {
  207.       // Hide overlay and begin a fresh run
  208.       const overlay = document.getElementById('startOverlay');
  209.       overlay.classList.add('hidden');
  210.       document.body.dataset.gate = 'open';
  211.       this.start();
  212.     }
  213.  
  214.     createPulseZones() {
  215.       this.pulseZones = [];
  216.       const zoneCount = 3 + Math.floor(this.level / 2);
  217.  
  218.       for (let i = 0; i < zoneCount; i++) {
  219.        let zone, attempts = 0;
  220.        do {
  221.          const radius = 5 + Math.random() * 10; // grid units
  222.          zone = {
  223.            x: Math.random() * this.cols,
  224.            y: Math.random() * this.rows,
  225.            radius,
  226.            phase: Math.random() * Math.PI * 2,
  227.            speed: 0.01 + Math.random() * 0.03,
  228.            color: `hsl(${Math.random() * 360}, 100%, 50%)`,
  229.            moveSpeed: { x: (Math.random() - 0.5) * 0.1, y: (Math.random() - 0.5) * 0.1 }
  230.          };
  231.          const distStart = Math.hypot(zone.x - this.playerPos.x, zone.y - this.playerPos.y);
  232.          const distGoal  = Math.hypot(zone.x - this.goalPos.x,   zone.y - this.goalPos.y);
  233.          var isValid = distStart >= zone.radius + 2 && distGoal >= zone.radius + 2; // buffer away from start/goal
  234.           attempts++;
  235.           if (attempts > 50) break; // safety
  236.         } while (!isValid);
  237.         this.pulseZones.push(zone);
  238.       }
  239.     }
  240.  
  241.     startDrawing(e) {
  242.       if (!this.isRunning || this.isPaused) return;
  243.       const rect = canvas.getBoundingClientRect();
  244.       const x = Math.floor((e.clientX - rect.left) / this.gridSize);
  245.       const y = Math.floor((e.clientY - rect.top)  / this.gridSize);
  246.       const pos = { x, y };
  247.  
  248.       if (this.playerPath.length === 0) {
  249.         // Must begin exactly on start node; allow even if pulse is high at the origin
  250.         if (x === this.playerPos.x && y === this.playerPos.y) {
  251.          this.drawing = true;
  252.           this.playerPath = [pos];
  253.         }
  254.       } else {
  255.         const lastPoint = this.playerPath[this.playerPath.length - 1];
  256.         if (x === lastPoint.x && y === lastPoint.y && this.isValidMove(x, y)) {
  257.          this.drawing = true;
  258.         }
  259.       }
  260.     }
  261.  
  262.     draw(e) {
  263.       if (!this.drawing || !this.isRunning || this.isPaused) return;
  264.       const rect = canvas.getBoundingClientRect();
  265.       const x = Math.floor((e.clientX - rect.left) / this.gridSize);
  266.       const y = Math.floor((e.clientY - rect.top)  / this.gridSize);
  267.  
  268.       const lastPoint = this.playerPath[this.playerPath.length - 1];
  269.       if (!lastPoint || (x === lastPoint.x && y === lastPoint.y)) return;
  270.  
  271.       if (this.isValidMove(x, y) && this.isAdjacent(lastPoint, { x, y })) {
  272.        this.playerPath.push({ x, y });
  273.         if (x === this.goalPos.x && y === this.goalPos.y) {
  274.          this.levelComplete();
  275.         }
  276.       }
  277.     }
  278.  
  279.     stopDrawing() { this.drawing = false; }
  280.  
  281.     isValidMove(x, y) {
  282.       if (x < 0 || x >= this.cols || y < 0 || y >= this.rows) return false;
  283.       // Check if cell is active (not in blocked pulse zone)
  284.       const pulseIntensity = this.calculatePulseIntensity(x, y);
  285.       return pulseIntensity < 0.7; // can traverse only through calmer cells
  286.    }
  287.  
  288.    isAdjacent(p1, p2) {
  289.      const dx = Math.abs(p1.x - p2.x);
  290.      const dy = Math.abs(p1.y - p2.y);
  291.      return (dx === 1 && dy === 0) || (dx === 0 && dy === 1) || (dx === 1 && dy === 1);
  292.    }
  293.  
  294.    calculatePulseIntensity(x, y) {
  295.      let maxIntensity = 0;
  296.      for (const zone of this.pulseZones) {
  297.        const dist = Math.hypot(x - zone.x, y - zone.y);
  298.        if (dist < zone.radius) {
  299.          const intensity = (1 - dist / zone.radius) * (Math.sin(this.pulsePhase + zone.phase) * 0.5 + 0.5);
  300.          maxIntensity = Math.max(maxIntensity, intensity);
  301.        }
  302.      }
  303.      return maxIntensity;
  304.    }
  305.  
  306.    lose() {
  307.      alert("Lost! A pulse zone hit your path. Resetting level...");
  308.      this.playerPath = [];
  309.      this.createPulseZones();
  310.    }
  311.  
  312.    update() {
  313.      if (!this.isRunning || this.isPaused) return;
  314.      // Update pulse phase
  315.      this.pulsePhase += this.pulseSpeed;
  316.  
  317.      // Move pulse zones
  318.      for (const zone of this.pulseZones) {
  319.        zone.x += zone.moveSpeed.x;
  320.        zone.y += zone.moveSpeed.y;
  321.        // bounce edges
  322.        if (zone.x - zone.radius < 0 || zone.x + zone.radius > this.cols) zone.moveSpeed.x *= -1;
  323.         if (zone.y - zone.radius < 0 || zone.y + zone.radius > this.rows) zone.moveSpeed.y *= -1;
  324.         zone.phase += zone.speed;
  325.       }
  326.  
  327.       // Update grid pulse intensities
  328.       for (let y = 0; y < this.rows; y++) {
  329.        for (let x = 0; x < this.cols; x++) {
  330.          this.grid[y][x].pulseIntensity = this.calculatePulseIntensity(x, y);
  331.        }
  332.      }
  333.  
  334.      // Check for lose condition if path exists
  335.      if (this.playerPath.length > 0) {
  336.         for (const point of this.playerPath) {
  337.           if (this.calculatePulseIntensity(point.x, point.y) >= 0.7) { this.lose(); break; }
  338.         }
  339.       }
  340.  
  341.       // Update timer
  342.       this.time += 1/60;
  343.       document.getElementById('timer').textContent = Math.floor(this.time);
  344.     }
  345.  
  346.     render() {
  347.       ctx.clearRect(0, 0, canvas.width, canvas.height);
  348.  
  349.       // Draw grid with pulse effects
  350.       for (let y = 0; y < this.rows; y++) {
  351.        for (let x = 0; x < this.cols; x++) {
  352.          const intensity = this.grid[y][x].pulseIntensity;
  353.          // calm background
  354.          ctx.fillStyle = `rgba(128, 0, 255, ${intensity * 0.3})`;
  355.          ctx.fillRect(x * this.gridSize, y * this.gridSize, this.gridSize - 1, this.gridSize - 1);
  356.          // blocked zones
  357.          if (intensity > 0.7) {
  358.             ctx.fillStyle = `rgba(255, 0, 168, ${intensity})`;
  359.             ctx.fillRect(x * this.gridSize, y * this.gridSize, this.gridSize - 1, this.gridSize - 1);
  360.           }
  361.         }
  362.       }
  363.  
  364.       // Draw pulse zone outlines
  365.       for (const zone of this.pulseZones) {
  366.         const pulseAlpha = (Math.sin(this.pulsePhase + zone.phase) * 0.3 + 0.7);
  367.         ctx.strokeStyle = zone.color; ctx.globalAlpha = pulseAlpha; ctx.lineWidth = 2;
  368.         ctx.beginPath();
  369.         ctx.arc(zone.x * this.gridSize, zone.y * this.gridSize, zone.radius * this.gridSize, 0, Math.PI * 2);
  370.         ctx.stroke();
  371.         ctx.globalAlpha = 1;
  372.       }
  373.  
  374.       // Draw player path
  375.       if (this.playerPath.length > 0) {
  376.         ctx.strokeStyle = this.pathColor; ctx.lineWidth = 4; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
  377.         ctx.shadowBlur = 20; ctx.shadowColor = this.pathColor;
  378.         ctx.beginPath();
  379.         ctx.moveTo(this.playerPath[0].x * this.gridSize + this.gridSize/2, this.playerPath[0].y * this.gridSize + this.gridSize/2);
  380.         for (let i = 1; i < this.playerPath.length; i++) {
  381.          const p = this.playerPath[i];
  382.          ctx.lineTo(p.x * this.gridSize + this.gridSize/2, p.y * this.gridSize + this.gridSize/2);
  383.        }
  384.        ctx.stroke();
  385.        ctx.shadowBlur = 0;
  386.        // path particles
  387.        for (let i = 0; i < this.playerPath.length; i++) {
  388.          const p = this.playerPath[i];
  389.          const size = 3 + Math.sin(this.pulsePhase * 2 + i * 0.5) * 2;
  390.          ctx.fillStyle = this.pathColor;
  391.          ctx.beginPath();
  392.          ctx.arc(p.x * this.gridSize + this.gridSize/2, p.y * this.gridSize + this.gridSize/2, size, 0, Math.PI * 2);
  393.          ctx.fill();
  394.        }
  395.      }
  396.  
  397.      // Start node
  398.      ctx.fillStyle = '#00ff88'; ctx.shadowBlur = 15; ctx.shadowColor = '#00ff88';
  399.      ctx.beginPath();
  400.      ctx.arc(this.playerPos.x * this.gridSize + this.gridSize/2, this.playerPos.y * this.gridSize + this.gridSize/2, this.gridSize/3, 0, Math.PI * 2);
  401.      ctx.fill();
  402.  
  403.      // Goal portal
  404.      const goalPulse = Math.sin(this.pulsePhase * 3) * 0.3 + 0.7;
  405.      ctx.fillStyle = '#ffaa00'; ctx.shadowColor = '#ffaa00'; ctx.shadowBlur = 20 * goalPulse;
  406.      ctx.beginPath();
  407.      ctx.arc(this.goalPos.x * this.gridSize + this.gridSize/2, this.goalPos.y * this.gridSize + this.gridSize/2, this.gridSize/2.5 * goalPulse, 0, Math.PI * 2);
  408.      ctx.fill();
  409.      ctx.shadowBlur = 0;
  410.    }
  411.  
  412.    levelComplete() {
  413.      this.score += 100 * this.level; this.level++;
  414.      document.getElementById('score').textContent = this.score;
  415.      document.getElementById('level').textContent = this.level;
  416.      this.createCompletionEffect();
  417.      setTimeout(() => {
  418.         this.playerPath = [];
  419.         this.createPulseZones();
  420.         this.pulseSpeed = Math.min(0.05, this.pulseSpeed * 1.1);
  421.       }, 800);
  422.     }
  423.  
  424.     createCompletionEffect() {
  425.       for (let i = 0; i < 20; i++) {
  426.        setTimeout(() => {
  427.           const particle = document.createElement('div');
  428.           particle.className = 'particle';
  429.           particle.style.left = (Math.random() * canvas.width) + 'px';
  430.           particle.style.top  = (Math.random() * canvas.height) + 'px';
  431.           particle.style.background = `hsl(${Math.random() * 360}, 100%, 70%)`;
  432.           canvas.parentElement.appendChild(particle);
  433.           setTimeout(() => particle.remove(), 3000);
  434.         }, i * 50);
  435.       }
  436.     }
  437.  
  438.     start() {
  439.       this.isRunning = true; this.isPaused = false; this.time = 0; this.score = 0; this.level = 1; this.pulsePhase = 0;
  440.       this.playerPath = [];
  441.       document.getElementById('score').textContent = this.score;
  442.       document.getElementById('level').textContent = this.level;
  443.       document.getElementById('timer').textContent = '0';
  444.       this.createPulseZones();
  445.       // Kick off the loop if not already animating
  446.       if (!this._animating) { this._animating = true; this.animate(); }
  447.     }
  448.  
  449.     reset() {
  450.       this.isRunning = false; this.isPaused = false; this.playerPath = []; this.time = 0; this.score = 0; this.level = 1; this.pulsePhase = 0;
  451.       document.getElementById('score').textContent = this.score;
  452.       document.getElementById('level').textContent = this.level;
  453.       document.getElementById('timer').textContent = '0';
  454.       this.createPulseZones();
  455.       this.render();
  456.     }
  457.  
  458.     togglePause() { if (this.isRunning) { this.isPaused = !this.isPaused; } }
  459.  
  460.     animate() {
  461.       // Single RAF loop controlled by isRunning/isPaused flags
  462.       this.update();
  463.       this.render();
  464.       requestAnimationFrame(() => this.animate());
  465.     }
  466.   }
  467.  
  468.   const game = new PulsePainterGame();
  469.   game.render(); // static render while gated
  470. </script>
  471. </body>
  472. </html>
  473.  
Advertisement
Add Comment
Please, Sign In to add comment