Skip to content

Instantly share code, notes, and snippets.

@rndmcnlly
Created January 21, 2026 23:17
Show Gist options
  • Select an option

  • Save rndmcnlly/21086ffb10bfb5b46d20c91756244853 to your computer and use it in GitHub Desktop.

Select an option

Save rndmcnlly/21086ffb10bfb5b46d20c91756244853 to your computer and use it in GitHub Desktop.
Prototyping a demo clip generation tool
<!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