<!-- Controls (t slider + buttons) above the canvas -->
<div class="controls">
<label for="tSlider"><strong>t =</strong> <span id="tVal">0.50</span></label>
<input id="tSlider" type="range" min="0" max="100" value="50" step="1" aria-label="Parameter t from 0 to 1">
<button id="animBtn" type="button">Animate</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<canvas id="cv" width="480" height="320" aria-label="De Casteljau scaffold collapsing to the Bézier curve point"></canvas>
<p class="hint" id="hint">Drag any white control point to reshape the curve.</p>
/* Layout, canvas and button styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; align-items: center; gap: .7rem; flex-wrap: wrap; margin-bottom: .5rem; }
label { font-size: .95rem; white-space: nowrap; }
input[type=range] { flex: 1; min-width: 120px; max-width: 280px; cursor: pointer; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 10px;
background: #f8fafc; cursor: crosshair; touch-action: none; max-width: 100%; }
.hint { font-size: .85rem; color: #555; margin: .4rem 0 0; }
// De Casteljau's algorithm: evaluate a cubic Bézier curve at parameter t
// by repeated linear interpolation of the control polygon.
const canvas = document.getElementById('cv');
const ctx = canvas.getContext('2d');
const slider = document.getElementById('tSlider');
const tValEl = document.getElementById('tVal');
const hintEl = document.getElementById('hint');
const animBtn = document.getElementById('animBtn');
// Hi-DPI canvas scaling
const DPR = window.devicePixelRatio || 1;
const W = canvas.width, H = canvas.height;
canvas.width = W * DPR; canvas.height = H * DPR;
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
ctx.scale(DPR, DPR);
// Control points (cubic: 4 points = degree 3)
let pts = [
{ x: 60, y: 260 },
{ x: 140, y: 60 },
{ x: 320, y: 60 },
{ x: 400, y: 260 }
];
// Color per interpolation layer: [control polygon, level 1, level 2, result]
const LAYER_COLORS = ['#adb5bd', '#e9c46a', '#e76f51', '#2a9d8f'];
let t = 0.5;
let dragging = null;
let animId = null;
let animDir = 1;
// Linear interpolation: blend two points by ratio t
function lerp(a, b, t) {
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
}
// Run De Casteljau and return ALL intermediate layers for drawing
function deCasteljau(points, t) {
const layers = [points.map(p => ({ ...p }))];
while (layers[layers.length - 1].length > 1) {
const prev = layers[layers.length - 1];
const next = [];
for (let i = 0; i < prev.length - 1; i++) {
next.push(lerp(prev[i], prev[i + 1], t));
}
layers.push(next);
}
return layers;
}
// Sample the full curve at many t values for the blue stroke
function bezierCurve(points, steps) {
const pts = [];
for (let i = 0; i <= steps; i++) {
const layers = deCasteljau(points, i / steps);
pts.push(layers[layers.length - 1][0]);
}
return pts;
}
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw the full Bézier curve in blue
const curve = bezierCurve(pts, 120);
ctx.beginPath();
ctx.moveTo(curve[0].x, curve[0].y);
for (let i = 1; i < curve.length; i++) ctx.lineTo(curve[i].x, curve[i].y);
ctx.strokeStyle = '#1d6fa8';
ctx.lineWidth = 2.5;
ctx.stroke();
// Draw each interpolation layer (scaffold)
const layers = deCasteljau(pts, t);
layers.forEach((layer, li) => {
if (layer.length < 2) return;
ctx.beginPath();
ctx.moveTo(layer[0].x, layer[0].y);
for (let i = 1; i < layer.length; i++) ctx.lineTo(layer[i].x, layer[i].y);
ctx.strokeStyle = LAYER_COLORS[li] || '#aaa';
ctx.lineWidth = li === 0 ? 1.2 : 1.8;
ctx.setLineDash(li === 0 ? [5, 4] : []);
ctx.stroke();
ctx.setLineDash([]);
// Draw dots at each intermediate point
layer.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, li === 0 ? 4.5 : 3.5, 0, 2 * Math.PI);
ctx.fillStyle = li === 0 ? '#fff' : (LAYER_COLORS[li] || '#aaa');
ctx.strokeStyle = LAYER_COLORS[li] || '#aaa';
ctx.lineWidth = 1.5;
ctx.fill();
ctx.stroke();
});
});
// Highlight the final result point in green
const result = layers[layers.length - 1][0];
ctx.beginPath();
ctx.arc(result.x, result.y, 7, 0, 2 * Math.PI);
ctx.fillStyle = '#2a9d8f';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
function update() {
t = slider.value / 100;
tValEl.textContent = t.toFixed(2);
draw();
}
// Animation: bounce t between 0 and 1
function tick() {
let v = parseInt(slider.value) + animDir;
if (v >= 100) { v = 100; animDir = -1; }
if (v <= 0) { v = 0; animDir = 1; }
slider.value = v;
update();
animId = requestAnimationFrame(tick);
}
animBtn.addEventListener('click', () => {
if (animId) {
cancelAnimationFrame(animId);
animId = null;
animBtn.textContent = 'Animate';
} else {
animDir = slider.value >= 100 ? -1 : 1;
animId = requestAnimationFrame(tick);
animBtn.textContent = 'Pause';
}
});
document.getElementById('resetBtn').addEventListener('click', () => {
pts = [{ x:60,y:260 },{ x:140,y:60 },{ x:320,y:60 },{ x:400,y:260 }];
slider.value = 50; update();
});
slider.addEventListener('input', update);
// Pointer drag to move control points
function ptAt(px, py) {
for (let i = 0; i < pts.length; i++) {
const dx = pts[i].x - px, dy = pts[i].y - py;
if (Math.sqrt(dx*dx + dy*dy) < 14) return i;
}
return null;
}
function clientXY(e) {
const r = canvas.getBoundingClientRect();
const src = e.touches ? e.touches[0] : e;
return { x: (src.clientX - r.left) * (W / r.width), y: (src.clientY - r.top) * (H / r.height) };
}
canvas.addEventListener('pointerdown', e => {
const p = clientXY(e);
dragging = ptAt(p.x, p.y);
if (dragging !== null) { canvas.setPointerCapture(e.pointerId); hintEl.textContent = 'Release to stop dragging.'; }
});
canvas.addEventListener('pointermove', e => {
if (dragging === null) return;
const p = clientXY(e);
pts[dragging] = { x: Math.max(0, Math.min(W, p.x)), y: Math.max(0, Math.min(H, p.y)) };
draw();
});
canvas.addEventListener('pointerup', () => { dragging = null; hintEl.textContent = 'Drag any white control point to reshape the curve.'; });
update();
<!-- Controls (t slider + buttons) above the canvas -->
<div class="controls">
<label for="tSlider"><strong>t =</strong> <span id="tVal">0.50</span></label>
<input id="tSlider" type="range" min="0" max="100" value="50" step="1" aria-label="Parameter t from 0 to 1">
<button id="animBtn" type="button">Animate</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<canvas id="cv" width="480" height="320" aria-label="De Casteljau scaffold collapsing to the Bézier curve point"></canvas>
<p class="hint" id="hint">Drag any white control point to reshape the curve.</p>
/* Layout, canvas and button styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; align-items: center; gap: .7rem; flex-wrap: wrap; margin-bottom: .5rem; }
label { font-size: .95rem; white-space: nowrap; }
input[type=range] { flex: 1; min-width: 120px; max-width: 280px; cursor: pointer; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 10px;
background: #f8fafc; cursor: crosshair; touch-action: none; max-width: 100%; }
.hint { font-size: .85rem; color: #555; margin: .4rem 0 0; }
// De Casteljau's algorithm: evaluate a cubic Bézier curve at parameter t
// by repeated linear interpolation of the control polygon.
const canvas = document.getElementById('cv');
const ctx = canvas.getContext('2d');
const slider = document.getElementById('tSlider');
const tValEl = document.getElementById('tVal');
const hintEl = document.getElementById('hint');
const animBtn = document.getElementById('animBtn');
// Hi-DPI canvas scaling
const DPR = window.devicePixelRatio || 1;
const W = canvas.width, H = canvas.height;
canvas.width = W * DPR; canvas.height = H * DPR;
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
ctx.scale(DPR, DPR);
// Control points (cubic: 4 points = degree 3)
let pts = [
{ x: 60, y: 260 },
{ x: 140, y: 60 },
{ x: 320, y: 60 },
{ x: 400, y: 260 }
];
// Color per interpolation layer: [control polygon, level 1, level 2, result]
const LAYER_COLORS = ['#adb5bd', '#e9c46a', '#e76f51', '#2a9d8f'];
let t = 0.5;
let dragging = null;
let animId = null;
let animDir = 1;
// Linear interpolation: blend two points by ratio t
function lerp(a, b, t) {
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
}
// Run De Casteljau and return ALL intermediate layers for drawing
function deCasteljau(points, t) {
const layers = [points.map(p => ({ ...p }))];
while (layers[layers.length - 1].length > 1) {
const prev = layers[layers.length - 1];
const next = [];
for (let i = 0; i < prev.length - 1; i++) {
next.push(lerp(prev[i], prev[i + 1], t));
}
layers.push(next);
}
return layers;
}
// Sample the full curve at many t values for the blue stroke
function bezierCurve(points, steps) {
const pts = [];
for (let i = 0; i <= steps; i++) {
const layers = deCasteljau(points, i / steps);
pts.push(layers[layers.length - 1][0]);
}
return pts;
}
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw the full Bézier curve in blue
const curve = bezierCurve(pts, 120);
ctx.beginPath();
ctx.moveTo(curve[0].x, curve[0].y);
for (let i = 1; i < curve.length; i++) ctx.lineTo(curve[i].x, curve[i].y);
ctx.strokeStyle = '#1d6fa8';
ctx.lineWidth = 2.5;
ctx.stroke();
// Draw each interpolation layer (scaffold)
const layers = deCasteljau(pts, t);
layers.forEach((layer, li) => {
if (layer.length < 2) return;
ctx.beginPath();
ctx.moveTo(layer[0].x, layer[0].y);
for (let i = 1; i < layer.length; i++) ctx.lineTo(layer[i].x, layer[i].y);
ctx.strokeStyle = LAYER_COLORS[li] || '#aaa';
ctx.lineWidth = li === 0 ? 1.2 : 1.8;
ctx.setLineDash(li === 0 ? [5, 4] : []);
ctx.stroke();
ctx.setLineDash([]);
// Draw dots at each intermediate point
layer.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, li === 0 ? 4.5 : 3.5, 0, 2 * Math.PI);
ctx.fillStyle = li === 0 ? '#fff' : (LAYER_COLORS[li] || '#aaa');
ctx.strokeStyle = LAYER_COLORS[li] || '#aaa';
ctx.lineWidth = 1.5;
ctx.fill();
ctx.stroke();
});
});
// Highlight the final result point in green
const result = layers[layers.length - 1][0];
ctx.beginPath();
ctx.arc(result.x, result.y, 7, 0, 2 * Math.PI);
ctx.fillStyle = '#2a9d8f';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
function update() {
t = slider.value / 100;
tValEl.textContent = t.toFixed(2);
draw();
}
// Animation: bounce t between 0 and 1
function tick() {
let v = parseInt(slider.value) + animDir;
if (v >= 100) { v = 100; animDir = -1; }
if (v <= 0) { v = 0; animDir = 1; }
slider.value = v;
update();
animId = requestAnimationFrame(tick);
}
animBtn.addEventListener('click', () => {
if (animId) {
cancelAnimationFrame(animId);
animId = null;
animBtn.textContent = 'Animate';
} else {
animDir = slider.value >= 100 ? -1 : 1;
animId = requestAnimationFrame(tick);
animBtn.textContent = 'Pause';
}
});
document.getElementById('resetBtn').addEventListener('click', () => {
pts = [{ x:60,y:260 },{ x:140,y:60 },{ x:320,y:60 },{ x:400,y:260 }];
slider.value = 50; update();
});
slider.addEventListener('input', update);
// Pointer drag to move control points
function ptAt(px, py) {
for (let i = 0; i < pts.length; i++) {
const dx = pts[i].x - px, dy = pts[i].y - py;
if (Math.sqrt(dx*dx + dy*dy) < 14) return i;
}
return null;
}
function clientXY(e) {
const r = canvas.getBoundingClientRect();
const src = e.touches ? e.touches[0] : e;
return { x: (src.clientX - r.left) * (W / r.width), y: (src.clientY - r.top) * (H / r.height) };
}
canvas.addEventListener('pointerdown', e => {
const p = clientXY(e);
dragging = ptAt(p.x, p.y);
if (dragging !== null) { canvas.setPointerCapture(e.pointerId); hintEl.textContent = 'Release to stop dragging.'; }
});
canvas.addEventListener('pointermove', e => {
if (dragging === null) return;
const p = clientXY(e);
pts[dragging] = { x: Math.max(0, Math.min(W, p.x)), y: Math.max(0, Math.min(H, p.y)) };
draw();
});
canvas.addEventListener('pointerup', () => { dragging = null; hintEl.textContent = 'Drag any white control point to reshape the curve.'; });
update();
Comments
Loading comments...