Created
January 21, 2026 23:17
-
-
Save rndmcnlly/21086ffb10bfb5b46d20c91756244853 to your computer and use it in GitHub Desktop.
Prototyping a demo clip generation tool
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
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Midpoint Displacement Demo Recording</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| background: #1a1a2e; | |
| font-family: 'Consolas', 'Monaco', monospace; | |
| overflow: hidden; | |
| } | |
| .video-frame { | |
| width: 1280px; | |
| height: 720px; | |
| margin: 20px auto; | |
| background: #0a0a15; | |
| position: relative; | |
| border: 3px solid #333; | |
| overflow: hidden; | |
| } | |
| /* Main terrain canvas */ | |
| #terrainCanvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| /* Tracked values panel - top left, student-sloppy styling */ | |
| .tracked-panel { | |
| position: absolute; | |
| top: 15px; | |
| left: 15px; | |
| background: rgba(0, 0, 0, 0.75); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| padding: 12px 15px; | |
| min-width: 280px; | |
| font-size: 13px; | |
| color: #e0e0e0; | |
| } | |
| .tracked-panel-title { | |
| font-size: 11px; | |
| color: #888; | |
| margin-bottom: 10px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .tracked-item { | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| height: 22px; | |
| } | |
| .tracked-label { | |
| width: 120px; | |
| color: #aaa; | |
| font-size: 12px; | |
| } | |
| .tracked-slider-container { | |
| flex: 1; | |
| height: 8px; | |
| background: #333; | |
| border-radius: 4px; | |
| margin-right: 10px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .tracked-slider-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #4a9eff, #66b3ff); | |
| border-radius: 4px; | |
| transition: width 0.05s linear; | |
| } | |
| .tracked-value { | |
| width: 50px; | |
| text-align: right; | |
| font-size: 12px; | |
| color: #fff; | |
| font-family: 'Consolas', monospace; | |
| } | |
| .tracked-checkbox { | |
| display: flex; | |
| align-items: center; | |
| } | |
| .checkbox-visual { | |
| width: 16px; | |
| height: 16px; | |
| border: 2px solid #666; | |
| background: #222; | |
| margin-right: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 12px; | |
| } | |
| .checkbox-visual.checked { | |
| background: #4a9eff; | |
| border-color: #4a9eff; | |
| } | |
| .checkbox-visual.checked::after { | |
| content: '✓'; | |
| color: white; | |
| } | |
| /* Log panel - top right */ | |
| .log-panel { | |
| position: absolute; | |
| top: 15px; | |
| right: 15px; | |
| width: 320px; | |
| max-height: 200px; | |
| overflow: hidden; | |
| font-size: 11px; | |
| } | |
| .log-entry { | |
| background: rgba(0, 0, 0, 0.7); | |
| padding: 4px 8px; | |
| margin-bottom: 3px; | |
| border-left: 3px solid #666; | |
| color: #ccc; | |
| opacity: 1; | |
| transition: opacity 0.5s ease; | |
| } | |
| .log-entry.regen { | |
| border-left-color: #ffaa00; | |
| color: #ffcc66; | |
| } | |
| .log-entry.param { | |
| border-left-color: #4a9eff; | |
| color: #88ccff; | |
| } | |
| .log-entry.info { | |
| border-left-color: #888; | |
| } | |
| .log-timestamp { | |
| color: #666; | |
| margin-right: 8px; | |
| } | |
| /* Subtitle area - bottom center */ | |
| .subtitle-area { | |
| position: absolute; | |
| bottom: 40px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| text-align: center; | |
| max-width: 800px; | |
| } | |
| .subtitle { | |
| font-size: 22px; | |
| color: #fff; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.9), | |
| -1px -1px 2px rgba(0,0,0,0.9), | |
| 1px -1px 2px rgba(0,0,0,0.9), | |
| -1px 1px 2px rgba(0,0,0,0.9); | |
| padding: 8px 16px; | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| } | |
| .subtitle.visible { | |
| opacity: 1; | |
| } | |
| /* Recording indicator */ | |
| .rec-indicator { | |
| position: absolute; | |
| top: 15px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 14px; | |
| color: #ff4444; | |
| } | |
| .rec-dot { | |
| width: 12px; | |
| height: 12px; | |
| background: #ff4444; | |
| border-radius: 50%; | |
| animation: blink 1s infinite; | |
| } | |
| @keyframes blink { | |
| 0%, 50% { opacity: 1; } | |
| 51%, 100% { opacity: 0.3; } | |
| } | |
| .timecode { | |
| position: absolute; | |
| bottom: 15px; | |
| right: 15px; | |
| font-size: 14px; | |
| color: rgba(255,255,255,0.5); | |
| font-family: 'Consolas', monospace; | |
| } | |
| /* Iteration counter overlay */ | |
| .iteration-display { | |
| position: absolute; | |
| bottom: 15px; | |
| left: 15px; | |
| background: rgba(0,0,0,0.7); | |
| padding: 8px 12px; | |
| font-size: 13px; | |
| color: #aaa; | |
| } | |
| .iteration-display span { | |
| color: #fff; | |
| font-weight: bold; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="video-frame"> | |
| <canvas id="terrainCanvas" width="1280" height="720"></canvas> | |
| <!-- Tracked Values Panel --> | |
| <div class="tracked-panel"> | |
| <div class="tracked-panel-title">DemoRecorder.Track()</div> | |
| <div class="tracked-item"> | |
| <span class="tracked-label">roughness</span> | |
| <div class="tracked-slider-container"> | |
| <div class="tracked-slider-fill" id="roughnessSlider" style="width: 50%"></div> | |
| </div> | |
| <span class="tracked-value" id="roughnessValue">0.50</span> | |
| </div> | |
| <div class="tracked-item"> | |
| <span class="tracked-label">iterations</span> | |
| <div class="tracked-slider-container"> | |
| <div class="tracked-slider-fill" id="iterSlider" style="width: 62.5%"></div> | |
| </div> | |
| <span class="tracked-value" id="iterValue">5</span> | |
| </div> | |
| <div class="tracked-item"> | |
| <span class="tracked-label">initialHeight</span> | |
| <div class="tracked-slider-container"> | |
| <div class="tracked-slider-fill" id="heightSlider" style="width: 30%"></div> | |
| </div> | |
| <span class="tracked-value" id="heightValue">0.30</span> | |
| </div> | |
| <div class="tracked-item tracked-checkbox"> | |
| <span class="tracked-label">showPoints</span> | |
| <div class="checkbox-visual" id="showPointsCheck"></div> | |
| </div> | |
| <div class="tracked-item tracked-checkbox"> | |
| <span class="tracked-label">colorByHeight</span> | |
| <div class="checkbox-visual checked" id="colorCheck"></div> | |
| </div> | |
| <div class="tracked-item tracked-checkbox"> | |
| <span class="tracked-label">fillTerrain</span> | |
| <div class="checkbox-visual checked" id="fillCheck"></div> | |
| </div> | |
| </div> | |
| <!-- Log Panel --> | |
| <div class="log-panel" id="logPanel"> | |
| </div> | |
| <!-- Subtitle --> | |
| <div class="subtitle-area"> | |
| <div class="subtitle" id="subtitle"></div> | |
| </div> | |
| <!-- Recording indicator --> | |
| <div class="rec-indicator"> | |
| <div class="rec-dot"></div> | |
| <span>REC</span> | |
| </div> | |
| <!-- Timecode --> | |
| <div class="timecode" id="timecode">00:00.00</div> | |
| <!-- Iteration display --> | |
| <div class="iteration-display"> | |
| current points: <span id="pointCount">2</span> | | |
| displacement range: ±<span id="dispRange">0.00</span> | |
| </div> | |
| </div> | |
| <script> | |
| // ========== MIDPOINT DISPLACEMENT ALGORITHM ========== | |
| // This is a student's actual implementation | |
| class MidpointDisplacement { | |
| constructor() { | |
| this.points = []; | |
| this.roughness = 0.5; | |
| this.iterations = 5; | |
| this.initialHeight = 0.3; | |
| this.seed = 12345; | |
| } | |
| // student's seeded random - probably copied from stackoverflow | |
| seededRandom() { | |
| this.seed = (this.seed * 9301 + 49297) % 233280; | |
| return this.seed / 233280; | |
| } | |
| randomInRange(min, max) { | |
| return min + this.seededRandom() * (max - min); | |
| } | |
| generate() { | |
| // reset seed for reproducibility | |
| this.seed = 12345; | |
| // start with two endpoints | |
| this.points = [ | |
| { x: 0, y: 0.5 + this.randomInRange(-this.initialHeight, this.initialHeight) }, | |
| { x: 1, y: 0.5 + this.randomInRange(-this.initialHeight, this.initialHeight) } | |
| ]; | |
| let displacement = this.initialHeight; | |
| for (let iter = 0; iter < this.iterations; iter++) { | |
| const newPoints = []; | |
| for (let i = 0; i < this.points.length - 1; i++) { | |
| const p1 = this.points[i]; | |
| const p2 = this.points[i + 1]; | |
| // add first point | |
| newPoints.push(p1); | |
| // midpoint with displacement | |
| const midX = (p1.x + p2.x) / 2; | |
| const midY = (p1.y + p2.y) / 2 + this.randomInRange(-displacement, displacement); | |
| newPoints.push({ x: midX, y: Math.max(0.1, Math.min(0.9, midY)) }); | |
| } | |
| // add last point | |
| newPoints.push(this.points[this.points.length - 1]); | |
| this.points = newPoints; | |
| displacement *= this.roughness; // reduce displacement each iteration | |
| } | |
| return this.points; | |
| } | |
| // for showing intermediate states | |
| generateUpToIteration(maxIter) { | |
| this.seed = 12345; | |
| this.points = [ | |
| { x: 0, y: 0.5 + this.randomInRange(-this.initialHeight, this.initialHeight) }, | |
| { x: 1, y: 0.5 + this.randomInRange(-this.initialHeight, this.initialHeight) } | |
| ]; | |
| let displacement = this.initialHeight; | |
| for (let iter = 0; iter < maxIter; iter++) { | |
| const newPoints = []; | |
| for (let i = 0; i < this.points.length - 1; i++) { | |
| const p1 = this.points[i]; | |
| const p2 = this.points[i + 1]; | |
| newPoints.push(p1); | |
| const midX = (p1.x + p2.x) / 2; | |
| const midY = (p1.y + p2.y) / 2 + this.randomInRange(-displacement, displacement); | |
| newPoints.push({ x: midX, y: Math.max(0.1, Math.min(0.9, midY)) }); | |
| } | |
| newPoints.push(this.points[this.points.length - 1]); | |
| this.points = newPoints; | |
| displacement *= this.roughness; | |
| } | |
| this.currentDisplacement = displacement; | |
| return this.points; | |
| } | |
| } | |
| // ========== DEMO RECORDER SIMULATION ========== | |
| const terrain = new MidpointDisplacement(); | |
| const canvas = document.getElementById('terrainCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // State | |
| let state = { | |
| roughness: 0.5, | |
| iterations: 5, | |
| initialHeight: 0.3, | |
| showPoints: false, | |
| colorByHeight: true, | |
| fillTerrain: true, | |
| time: 0, | |
| logs: [], | |
| currentSubtitle: '', | |
| subtitleVisible: false | |
| }; | |
| // ========== RENDERING ========== | |
| function render() { | |
| // Clear | |
| ctx.fillStyle = '#0a0a15'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Draw grid (subtle) | |
| ctx.strokeStyle = 'rgba(255,255,255,0.05)'; | |
| ctx.lineWidth = 1; | |
| for (let x = 0; x < canvas.width; x += 80) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x, 0); | |
| ctx.lineTo(x, canvas.height); | |
| ctx.stroke(); | |
| } | |
| for (let y = 0; y < canvas.height; y += 80) { | |
| ctx.beginPath(); | |
| ctx.moveTo(0, y); | |
| ctx.lineTo(canvas.width, y); | |
| ctx.stroke(); | |
| } | |
| // Generate terrain | |
| terrain.roughness = state.roughness; | |
| terrain.initialHeight = state.initialHeight; | |
| const points = terrain.generateUpToIteration(Math.floor(state.iterations)); | |
| // Convert to screen coords | |
| const screenPoints = points.map(p => ({ | |
| x: p.x * canvas.width, | |
| y: (1 - p.y) * canvas.height * 0.8 + canvas.height * 0.1 | |
| })); | |
| // Fill terrain | |
| if (state.fillTerrain) { | |
| ctx.beginPath(); | |
| ctx.moveTo(0, canvas.height); | |
| screenPoints.forEach(p => ctx.lineTo(p.x, p.y)); | |
| ctx.lineTo(canvas.width, canvas.height); | |
| ctx.closePath(); | |
| if (state.colorByHeight) { | |
| const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); | |
| gradient.addColorStop(0, '#4a7c4e'); | |
| gradient.addColorStop(0.4, '#3d6b41'); | |
| gradient.addColorStop(0.7, '#5d4e37'); | |
| gradient.addColorStop(1, '#2a2520'); | |
| ctx.fillStyle = gradient; | |
| } else { | |
| ctx.fillStyle = '#3a5a40'; | |
| } | |
| ctx.fill(); | |
| } | |
| // Draw line | |
| ctx.beginPath(); | |
| ctx.moveTo(screenPoints[0].x, screenPoints[0].y); | |
| for (let i = 1; i < screenPoints.length; i++) { | |
| ctx.lineTo(screenPoints[i].x, screenPoints[i].y); | |
| } | |
| ctx.strokeStyle = state.colorByHeight ? '#8fbc8f' : '#66aa66'; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| // Draw points if enabled | |
| if (state.showPoints) { | |
| screenPoints.forEach((p, i) => { | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, 4, 0, Math.PI * 2); | |
| ctx.fillStyle = '#ffaa00'; | |
| ctx.fill(); | |
| ctx.strokeStyle = '#fff'; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| }); | |
| } | |
| // Update UI displays | |
| updateUI(); | |
| } | |
| function updateUI() { | |
| // Sliders | |
| document.getElementById('roughnessSlider').style.width = (state.roughness * 100) + '%'; | |
| document.getElementById('roughnessValue').textContent = state.roughness.toFixed(2); | |
| document.getElementById('iterSlider').style.width = (state.iterations / 8 * 100) + '%'; | |
| document.getElementById('iterValue').textContent = Math.floor(state.iterations); | |
| document.getElementById('heightSlider').style.width = (state.initialHeight * 100) + '%'; | |
| document.getElementById('heightValue').textContent = state.initialHeight.toFixed(2); | |
| // Checkboxes | |
| document.getElementById('showPointsCheck').className = 'checkbox-visual' + (state.showPoints ? ' checked' : ''); | |
| document.getElementById('colorCheck').className = 'checkbox-visual' + (state.colorByHeight ? ' checked' : ''); | |
| document.getElementById('fillCheck').className = 'checkbox-visual' + (state.fillTerrain ? ' checked' : ''); | |
| // Point count and displacement | |
| document.getElementById('pointCount').textContent = terrain.points.length; | |
| document.getElementById('dispRange').textContent = (terrain.currentDisplacement || 0).toFixed(3); | |
| // Timecode | |
| const secs = Math.floor(state.time); | |
| const ms = Math.floor((state.time % 1) * 100); | |
| document.getElementById('timecode').textContent = | |
| `00:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(2, '0')}`; | |
| // Subtitle | |
| const subtitleEl = document.getElementById('subtitle'); | |
| subtitleEl.textContent = state.currentSubtitle; | |
| subtitleEl.className = 'subtitle' + (state.subtitleVisible ? ' visible' : ''); | |
| } | |
| function addLog(message, type = 'info') { | |
| const logPanel = document.getElementById('logPanel'); | |
| const entry = document.createElement('div'); | |
| entry.className = 'log-entry ' + type; | |
| const secs = Math.floor(state.time); | |
| const ms = Math.floor((state.time % 1) * 100); | |
| const timestamp = `${secs.toString().padStart(2, '0')}:${ms.toString().padStart(2, '0')}`; | |
| entry.innerHTML = `<span class="log-timestamp">[${timestamp}]</span>${message}`; | |
| logPanel.insertBefore(entry, logPanel.firstChild); | |
| // Limit log entries | |
| while (logPanel.children.length > 8) { | |
| logPanel.removeChild(logPanel.lastChild); | |
| } | |
| // Fade old entries | |
| Array.from(logPanel.children).forEach((el, i) => { | |
| el.style.opacity = Math.max(0.3, 1 - i * 0.15); | |
| }); | |
| } | |
| function showSubtitle(text, duration = 3) { | |
| state.currentSubtitle = text; | |
| state.subtitleVisible = true; | |
| setTimeout(() => { | |
| if (state.currentSubtitle === text) { | |
| state.subtitleVisible = false; | |
| } | |
| }, duration * 1000); | |
| } | |
| // ========== DEMO SEQUENCE ========== | |
| // This simulates what a student's scripted demo might do | |
| const demoSequence = [ | |
| // [time, action] | |
| [0.0, () => { | |
| showSubtitle("Midpoint Displacement - showing iteration count effect", 4); | |
| addLog("Demo started", "info"); | |
| }], | |
| [0.5, () => { | |
| addLog("iterations = 1", "param"); | |
| state.iterations = 1; | |
| }], | |
| [1.5, () => { | |
| addLog("iterations = 2", "param"); | |
| state.iterations = 2; | |
| }], | |
| [2.2, () => { | |
| addLog("iterations = 3", "param"); | |
| state.iterations = 3; | |
| }], | |
| [2.8, () => { | |
| addLog("iterations = 4", "param"); | |
| state.iterations = 4; | |
| }], | |
| [3.3, () => { | |
| addLog("iterations = 5", "param"); | |
| state.iterations = 5; | |
| }], | |
| [3.7, () => { | |
| addLog("iterations = 6", "param"); | |
| state.iterations = 6; | |
| }], | |
| [4.0, () => { | |
| showSubtitle("Now sweeping roughness from 0.2 to 0.9", 3); | |
| }], | |
| // Roughness sweep will happen smoothly via animation | |
| [4.0, () => { state.roughnessSweep = true; state.roughnessTarget = 0.2; }], | |
| [5.5, () => { state.roughnessTarget = 0.9; addLog("roughness -> 0.9", "param"); }], | |
| [7.0, () => { | |
| state.roughnessSweep = false; | |
| state.roughness = 0.5; | |
| addLog("roughness = 0.5 (reset)", "param"); | |
| }], | |
| [7.2, () => { | |
| showSubtitle("Toggle showPoints to see subdivision", 2.5); | |
| }], | |
| [7.5, () => { | |
| state.showPoints = true; | |
| addLog("showPoints = true", "param"); | |
| }], | |
| [8.5, () => { | |
| state.iterations = 3; | |
| addLog("iterations = 3 (to see points clearly)", "param"); | |
| }], | |
| [9.0, () => { | |
| state.iterations = 4; | |
| addLog("iterations = 4", "param"); | |
| }], | |
| [9.5, () => { | |
| state.showPoints = false; | |
| state.iterations = 6; | |
| addLog("showPoints = false", "param"); | |
| }], | |
| [9.8, () => { | |
| showSubtitle("Regenerating with different seed...", 2); | |
| }], | |
| [10.0, () => { | |
| terrain.seed = 99999; | |
| addLog("Regenerate() - new seed", "regen"); | |
| }] | |
| ]; | |
| let sequenceIndex = 0; | |
| let startTime = null; | |
| let loopDuration = 10; // seconds | |
| function animate(timestamp) { | |
| if (!startTime) startTime = timestamp; | |
| const elapsed = (timestamp - startTime) / 1000; | |
| // Loop the demo | |
| state.time = elapsed % loopDuration; | |
| // Reset sequence on loop | |
| if (state.time < 0.1 && sequenceIndex > 0) { | |
| sequenceIndex = 0; | |
| // Reset state | |
| state.roughness = 0.5; | |
| state.iterations = 5; | |
| state.initialHeight = 0.3; | |
| state.showPoints = false; | |
| state.colorByHeight = true; | |
| state.fillTerrain = true; | |
| state.roughnessSweep = false; | |
| terrain.seed = 12345; | |
| document.getElementById('logPanel').innerHTML = ''; | |
| } | |
| // Execute sequence actions | |
| while (sequenceIndex < demoSequence.length && | |
| demoSequence[sequenceIndex][0] <= state.time) { | |
| demoSequence[sequenceIndex][1](); | |
| sequenceIndex++; | |
| } | |
| // Smooth roughness sweep | |
| if (state.roughnessSweep && state.roughnessTarget !== undefined) { | |
| const diff = state.roughnessTarget - state.roughness; | |
| state.roughness += diff * 0.05; | |
| } | |
| render(); | |
| requestAnimationFrame(animate); | |
| } | |
| // Start | |
| requestAnimationFrame(animate); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment