Show code
<think>
Okay, I need to create a self-contained HTML file that animates the De Casteljau construction for a cubic Bezier curve using a canvas. Let me start by recalling how De Casteljau's algorithm works. For a cubic Bezier with four control points, at each parameter t, we compute intermediate points by linear interpolation between the control points step by step until we get the final point on the curve.
The user wants the control points to drift slowly. So I need to define four control points that change over time. Maybe I can make them move in random directions or follow some path, but keep it simple. Maybe each point has a velocity vector that's updated periodically. But since the animation uses requestAnimationFrame, I can adjust their positions each frame slightly. Let's think of each point having a small random movement each frame.
The animation should start immediately and loop when it finishes. So I need to handle the animation loop with requestAnimationFrame. The parameter t should sweep from 0 to 1, incrementing each frame. Once t reaches 1, reset everything including the control points to new random positions and restart.
The HTML structure will include a canvas element and some text elements to display the current t value and control points' coordinates. But wait, the user mentioned showing the control point positions. Hmm, how to do that. Maybe using a <div> for the text, but since it's a self-contained HTML file with no external dependencies, I can create a div with id and update it each frame.
Wait, but the user said "Show t and the control-point positions". So maybe add a div that's updated each frame with the current t value and the coordinates of each control point. Alternatively, since the canvas can have text drawing, maybe draw the text on the canvas itself. That might be better because it's all within the canvas context.
So, drawing text on the canvas: use context.fillText. I can display t at the bottom, and the control points' coordinates next to each point. But that might be too cluttered. Alternatively, just show t in the top-right corner and maybe the coordinates in a separate text box. Or perhaps a small info area on the canvas.
Let me outline the steps:
1. Set up the canvas in the HTML body.
2. In JavaScript:
a. Initialize control points with random positions.
b. Set up variables for animation, like t, which starts at 0 and increments each frame.
c. Define a function to update the control points' positions slowly. Maybe each control point has a velocity, so their position is adjusted by velocity * delta time? Or perhaps just a small fixed step each frame. Since the animation is smooth, maybe using a small delta each frame.
d. The main animation loop (requestAnimationFrame) will:
i. Clear the canvas.
ii. Update control points (maybe move them a tiny bit each frame).
iii. Calculate all intermediate points using De Casteljau for current t.
iv. Draw all the points, connecting lines, and the traced curve.
v. Display t and control point positions on the canvas.
vi. Update t, if it reaches 1, reset everything (including control points) and restart.
e. Start the animation loop.
Wait, how to implement the De Casteljau steps. For four control points (P0, P1, P2, P3):
At each step:
- First level: compute P01 = P0 + t*(P1-P0), similarly for others.
- Second level: compute P012 = P01 + t*(P12 - P01), etc.
- Third level: compute P0123 which is the point on the curve.
So for each t, the intermediate points are the ones at each level. Need to draw lines between these points as well. The lines would be between each pair in the same level, and the points themselves.
Wait, the lines connecting the interpolation points. So first level lines connect P0-P1, P1-P2, P2-P3 (but actually, the interpolation is between each pair to get the next level). Wait, actually in De Casteljau, each step reduces the number of points by one.
Wait, let me think again. For four points P0-P3 (cubic Bezier):
At step 1: between each pair, so three segments. The points computed here are P0+ t*(P1-P0), P1 + t*(P2-P1), etc. These form three new points.
Step 2: between those three points, so two new points.
Step 3: one point, which is on the Bezier curve.
So the lines to draw are between each adjacent pair at each step. So for step1, lines between the three points, step2 lines between the two points, and the final point.
Additionally, the traced curve itself (the Bezier curve) should be drawn over time as t progresses. To show the traced curve, we need to keep track of previous points as t increases. Maybe store an array of points along the curve and draw them with a line.
Wait, but if we're drawing the traced curve, each frame adds a new point, so building up the curve progressively.
Also, the intermediate points and lines should be drawn for the current t. The control points themselves should be shown as dots, maybe with a different color.
So the steps in the animation loop:
1. Clear the canvas.
2. Update control points (move them slightly each frame).
3. Compute all intermediate points for current t.
4. Draw control points (as small circles or crosses).
5. Draw lines connecting the control points (maybe the original Bezier's control polygon).
6. Draw the intermediate points and lines between them at each De Casteljau step.
7. Draw the traced curve (all previous points from 0 to current t).
8. Draw the current t value and possibly control points' coordinates on the canvas.
9. Update t, if t exceeds 1, reset (set t=0, reset control points to new random positions, clear the traced curve array?), then start over.
Wait, the traced curve array: Each time we compute the final point (the P0123 point), we can add it to an array, then draw a line connecting all those points. The array would grow over time as t increases from 0 to 1. Once t reaches 1, the array is cleared for the next animation cycle.
So variables needed:
- controlPoints: array of four objects with x and y coordinates, which drift.
- t: the current parameter (from 0 to 1).
- tracedCurve: array of points (x,y) for the Bezier curve.
- velocity for control points? Or maybe just a small random step each frame. To make them drift slowly, perhaps each control point has a very small velocity, which can be implemented by adding a small delta to x and y each frame. Alternatively, each point has a direction and speed, but that might complicate. Maybe each frame, add a small random delta to each control point's position. Or a fixed small step in some direction. Hmm, perhaps to keep it simple, each control point's position is updated by adding a small random value between -0.1 and 0.1 to x and y each frame. So over time they drift in some direction.
But that might cause them to move too much. Alternatively, have a velocity vector for each point that's reset periodically? Maybe not. Let's think of each control point moving in a circular path, but that might be overcomplicating.
Alternatively, each point's new position is their current position plus a small random delta each frame. So:
controlPoints[i].x += (Math.random() * 0.2 - 0.1);
controlPoints[i].y += (Math.random() * 0.2 - 0.1);
Wait, but that would cause them to move in random directions each frame. It might look like they're drifting. Alternatively, to make them drift smoothly, perhaps each point has a direction vector and speed, but that adds variables. To keep it simple, maybe just add a small random delta each frame. The key is that they should move slowly so the animation is visible but not too fast.
Alternatively, have each point's position change by a small amount each frame, but in a direction that drifts. Hmm, perhaps it's okay to just have a small random delta each frame. The exact implementation can be adjusted later.
Now, putting this into code structure:
First, in the HTML, have a canvas element:
<canvas id="bezierCanvas"></canvas>
Then in JavaScript:
var canvas = document.getElementById('bezierCanvas');
var ctx = canvas.getContext('2d');
Set canvas size, maybe 600x400 or similar. But in the code, setting width and height via attributes:
<canvas id="bezierCanvas" width="600" height="400"></canvas>
Then variables:
var controlPoints = [];
var t = 0;
var tracedCurve = [];
var dt = 0.01; // increment per frame
var animate;
Initialize controlPoints with random positions:
function init() {
controlPoints = [];
for (var i=0; i<4; i++) {
controlPoints.push( {
x: Math.random() * canvas.width,
y: Math.random() * canvas.height
});
}
tracedCurve = [];
t = 0;
}
Then, the update function in the animation loop:
function animate() {
requestAnimationFrame(animate);
ctx.clearRect(0,0,canvas.width,canvas.height);
// Update control points (drift slowly)
controlPoints.forEach( function(pt) {
pt.x += (Math.random() * 0.4 - 0.2);
pt.y += (Math.random() * 0.4 - 0.2);
// Constrain to canvas?
if (pt.x < 0) pt.x = 0;
if (pt.x > canvas.width) pt.x = canvas.width;
if (pt.y < 0) pt.y = 0;
if (pt.y > canvas.height) pt.y = canvas.height;
});
// Compute De Casteljau steps for current t
var points = [];
// Step 1: first level
points[0] = controlPoints.map( function(p) { return {x:p.x, y:p.y}; });
for (var i=1; i<=3; i++) {
points[i] = [];
for (var j=0; j < points[i-1].length -1; j++) {
var p0 = points[i-1][j];
var p1 = points[i-1][j+1];
points[i].push( {
x: p0.x + t*(p1.x - p0.x),
y: p0.y + t*(p1.y - p0.y)
});
}
}
// points[3][0] is the final point on the Bezier.
// Draw control points and polygon
ctx.strokeStyle = 'black';
ctx.beginPath();
ctx.moveTo( controlPoints[0].x, controlPoints[0].y );
for (var i=1; i<4; i++) {
ctx.lineTo( controlPoints[i].x, controlPoints[i].y );
}
ctx.stroke();
// Draw control points as dots
ctx.fillStyle = 'red';
controlPoints.forEach( function(p) {
ctx.beginPath();
ctx.arc(p.x, p.y, 3, 0, 2*Math.PI);
ctx.fill();
});
// Draw intermediate points and lines for each step
ctx.strokeStyle = 'blue';
// Step 1 lines (3 points)
ctx.beginPath();
ctx.moveTo(points[0][0].x, points[0][0].y);
for (var i=1; i < points[0].length; i++) {
ctx.lineTo(points[0][i].x, points[0][i].y);
}
ctx.stroke();
// Step 2 (2 points)
ctx.strokeStyle = 'green';
ctx.beginPath();
ctx.moveTo(points[1][0].x, points[1][0].y);
ctx.lineTo(points[1][1].x, points[1][1].y);
ctx.stroke();
// Step 3 (one point)
ctx.fillStyle = 'orange';
ctx.beginPath();
ctx.arc(points[2][0].x, points[2][0].y, 3, 0, 2*Math.PI);
ctx.fill();
// The final point is points[3][0], add to tracedCurve
tracedCurve.push( {x:points[3][0].x, y:points[3][0].y} );
// Draw the traced curve
if (tracedCurve.length >=2) {
ctx.strokeStyle = 'black';
ctx.beginPath();
ctx.moveTo(tracedCurve[0].x, tracedCurve[0].y);
for (var i=1; i<tracedCurve.length; i++) {
ctx.lineTo(tracedCurve[i].x, tracedCurve[i].y);
}
ctx.stroke();
}
// Update t
if (t >= 1) {
init(); // restart
return;
}
t += dt;
// Draw t value
ctx.fillStyle = 'black';
ctx.font = '12px Arial';
ctx.fillText('t = ' + t.toFixed(2), 10, 20);
// Draw control points coordinates (maybe just the first one?)
// Alternatively, display all 4 points' coordinates?
// To save space, maybe just show t and that's it. Or use a box.
// For simplicity, let's just show the coordinates of the first point.
// Or loop through all points and print their positions in text.
// Maybe too much, but user asked for control-point positions.
// Perhaps show each control point's coordinates near them?
// Draw coordinates next to each control point (maybe small text)
ctx.fillStyle = 'black';
ctx.font = '10px Arial';
controlPoints.forEach( function(p,i) {
ctx.fillText('P'+i+': ('+Math.round(p.x)+', '+Math.round(p.y)+')', p.x +5, p.y +5);
});
// Also, maybe display t in a more prominent place.
// Finally, redraw all intermediate steps.
}
Wait, but in the animate function, this code is going to run every frame via requestAnimationFrame. However, the initial setup needs to be called. So in the init function, we set t=0, control points, etc., then start the animation loop.
Wait, the initial setup should be done once before starting the animation. So in the script:
init();
animate(); // which calls requestAnimationFrame(animate), creating the loop.
But in code:
function animate() {
// ... the code above
// At the end, if t over 1, call init() and then return, so it stops the loop.
// But requestAnimationFrame is called again automatically. Wait, no: the function is recursive via requestAnimationFrame.
Wait, the animate function should call requestAnimationFrame again except when resetting. Wait, the code as written:
Inside animate():
requestAnimationFrame(animate); // first line.
Then, after updating, if t >=1, then call init(), and then return. So the next line after that return would not execute, so the next frame won't be scheduled again? Wait no: requestAnimationFrame is called before any other steps, so when t reaches 1, the next frame would still be scheduled, but when it runs, after the init, but then the next frame would not be scheduled again? Hmm, no. Let me think step by step.
When the function animate is called, it does:
requestAnimationFrame(animate); // schedules the next frame.
Then proceeds to process the current frame. If t reaches 1, then after processing, t is set to 0 (through init()), and returns. So after returning, the next animation frame is still scheduled. Wait, perhaps that approach may have issues because after resetting, the next call would be the same as a new cycle.
Alternatively, perhaps when t reaches 1, we should call init() and then restart the animation loop. Hmm, maybe better to do:
Inside animate():
if (t >=1) {
init(); // reset everything
return; // do not schedule the next frame?
}
But no, because requestAnimationFrame(animate) was already called first. Wait, the order is:
Inside animate():
1. Schedule next animation frame (requestAnimationFrame(animate))
2. Clear canvas etc.
3. ... processing ...
4. if t >=1: call init() and return, so no further processing in this frame.
The next frame will happen because step 1 scheduled it. So when t reaches 1, the next frame will be handled, but after init(), which sets t=0, so it will proceed again.
Alternatively, perhaps it's okay. The code flow might work as intended.
Potential issues:
- When control points move, their motion might be too erratic with the random deltas. Maybe better to have a small fixed step in some direction, but that would require more variables. Alternatively, use a per-point velocity. Let me think.
Alternatively, each point has a velocity vector (dx, dy), which is updated slowly. For example, each point has a velocity, and each frame, position += velocity * delta_time. But since delta_time is handled by requestAnimationFrame, which varies, but perhaps we can just use fixed small steps. Maybe to keep it simple, each frame, add a small random offset to each control point's position. For example:
pt.x += (Math.random() * 0.2 - 0.1); // between -0.1 and +0.1
This way, the points drift slowly. But with Math.random(), each frame the direction is random, so they might jitter. Alternatively, store a velocity vector for each control point, and update their position by adding velocity each frame, and update velocity occasionally. Hmm, that might be overcomplicating.
Alternatively, for simplicity, just let each control point's position be updated with small random steps. The exact movement might not be too important as long as they drift over time.
Another thing: the control points might go outside the canvas. So adding constraints to keep them within the canvas's boundaries. In the code above, I added:
if (pt.x < 0) pt.x = 0;
if (pt.x > canvas.width) pt.x = canvas.width;
// same for y.
This way, they can't go outside.
Another point: the tracedCurve is being added to every frame. So when t reaches 1 and the animation restarts, the tracedCurve is reset to empty, so the previous curve is erased. That's correct.
Testing the code:
- When t increases from 0 to 1, the intermediate points are computed step by step, and the tracedCurve is built, showing the Bezier curve.
Potential bugs:
- The points array in the De Casteljau steps: The code initializes points[0] as a copy of controlPoints. Then for each step, points[i] is computed based on points[i-1].
Wait, let me check:
points starts as an empty array.
points[0] = controlPoints.map(...) copies each point.
Then for i=1 to 3 (since there are 4 control points, steps 1,2,3 for 3 levels):
Level 1 (first iteration i=1):
points[1] will have length 3 (since previous had 4, so 3 steps between them).
Next i=2: points[2] has length 2 (because previous had 3 elements, so 2 steps)
i=3: points[3] has length 1 (from two elements in previous).
So yes, that's correct.
Thus, the final point is points[3][0].
The code correctly adds this to tracedCurve.
Now, the drawing of intermediate steps:
Step 1 lines (points[0] are control points, but the lines between them are already drawn as the control polygon. The intermediate steps are the first level (after first iteration), so perhaps the first intermediate points are points[1], but in the code, the first step is drawn as lines between points[0], which is the control points themselves. So that might be redundant.
Wait, in the code above:
// Draw intermediate points and lines for each step
ctx.strokeStyle = 'blue';
// Step 1 lines (3 points)
ctx.beginPath();
ctx.moveTo(points[0][0].x, points[0][0].y);
for (var i=1; i < points[0].length; i++) {
ctx.lineTo(points[0][i].x, points[0][i].y);
}
ctx.stroke();
But points[0] is the control points array, so this is just drawing the control polygon again. Which is already drawn previously. So that's a mistake.
Ah, right, that is redundant. The first level of De Casteljau (step 1) is the first iteration, so the points are between the control points. Wait, no: the De Casteljau steps are:
points[0] = control points
points[1] are the first level computed between points[0].
points[2] are the second level between points[1], etc.
So to draw the intermediate steps:
After computing all points, to display the intermediate steps:
- Level 1 (points[1]) connects the first computed points between control points.
- Level 2: points[2] connecting the next computed points.
- Level 3: points[3][0].
So in the code above, perhaps the drawing of intermediate steps should be for levels 1,2,3:
For step1 (points[1]):
ctx.beginPath();
for each point in points[1], draw lines between them?
Wait, points[1] is an array of length 3 (for 4 control points).
Wait, for 4 control points:
points[0] has 4 elements.
points[1] has 3 elements (each connecting the pairs of points[0]).
points[2] has 2 elements (connecting the pairs of points[1]).
points[3] has 1 element (connecting the pair of points[2]).
So to draw the intermediate lines for each step:
After level 1 (points[1]):
draw lines connecting points[1][0], points[1][1], points[1][2].
Similarly for level 2 (points[2]), lines between the two points.
Level 3 is just a point.
So in the code:
// Step1: points[1]
ctx.strokeStyle = 'blue';
ctx.beginPath();
ctx.moveTo(points[1][0].x, points[1][0].y);
for (var j=1; j < points[1].length; j++) {
ctx.lineTo(points[1][j].x, points[1][j].y);
}
ctx.stroke();
Similarly for points[2]:
ctx.strokeStyle = 'green';
ctx.beginPath();
ctx.moveTo(points[2][0].x, points[2][0].y);
if (points[2].length >1) {
ctx.lineTo(points[2][1].x, points[2][1].y);
}
ctx.stroke();
And points[3][0] is drawn as an orange dot.
So the original code had an error in drawing the first intermediate step. The code needs to be adjusted.
Therefore, the code for drawing the intermediate steps should be modified as follows:
After computing the points array:
// Draw intermediate points and lines for each step
// Step1: the first level (points[1])
ctx.strokeStyle = 'blue';
ctx.beginPath();
ctx.moveTo(points[1][0].x, points[1][0].y);
for (var j=1; j < points[1].length; j++) {
ctx.lineTo(points[1][j].x, points[1][j].y);
}
ctx.stroke();
// Step2: second level (points[2])
ctx.strokeStyle = 'green';
ctx.beginPath();
ctx.moveTo(points[2][0].x, points[2][0].y);
if (points[2].length > 1) {
ctx.lineTo(points[2][1].x, points[2][1].y);
}
ctx.stroke();
// Step3: third level (points[3][0])
ctx.fillStyle = 'orange';
ctx.beginPath();
ctx.arc(points[3][0].x, points[3][0].y, 3, 0, 2*Math.PI);
ctx.fill();
This way, the intermediate steps are drawn properly.
In the original code, the first intermediate step was drawing the control polygon again, which is a mistake. So that needs to be fixed.
Another point: The tracedCurve is being built by adding points[3][0] each frame. However, since t increments by dt each frame, the points are added in order, so the curve is drawn correctly.
Now, the problem of displaying the control points' coordinates:
The user requested to show the control-point positions, so perhaps we can display each point's coordinates near them. The code above already includes:
controlPoints.forEach( function(p,i) {
ctx.fillText('P'+i+': ('+Math.round(p.x)+', '+Math.round(p.y)+')', p.x +5, p.y +5);
});
This will write the coordinates next to each control point. However, depending on their position, the text might be cut off or not visible, but it's an attempt.
Alternatively, perhaps print them in a box on the side, but that might require more complex handling.
Now, putting all together into the final code.
But let's also consider code efficiency. Since the animate function is called every frame, we need to make sure loops are manageable. For a small number of control points (4), it's okay.
Now, let's structure the code properly.
The full code would be:
<canvas id="myCanvas" width="800" height="600"></canvas>
<script>
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var controlPoints = [];
var t = 0;
var tracedCurve = [];
var dt = 0.01;
function init() {
controlPoints = [];
for (var i = 0; i <4; i++) {
controlPoints.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height
});
}
tracedCurve = [];
t = 0;
}
function animate() {
requestAnimationFrame(animate);
ctx.clearRect(0,0, canvas.width, canvas.height);
// Update control points (drift slowly)
controlPoints.forEach( function(pt) {
pt.x += (Math.random() * 0.4 - 0.2);
pt.y += (Math.random() * 0.4 - 0.2);
// Constrain to canvas
pt.x = Math.max(0, Math.min(pt.x, canvas.width));
pt.y = Math.max(0, Math.min(pt.y, canvas.height));
});
// Compute De Casteljau steps for current t
var points = [];
// Step 0: control points
points[0] = controlPoints.map( function(p) { return {x:p.x, y:p.y}; });
for (var i =1; i <=3; i++) {
points[i] = [];
for (var j=0; j < points[i-1].length -1; j++) {
var p0 = points[i-1][j];
var p1 = points[i-1][j+1];
points[i].push({
x: p0.x + t*(p1.x - p0.x),
y: p0.y + t*(p1.y - p0.y)
});
}
}
// Draw control polygon and points
ctx.strokeStyle = 'black';
ctx.beginPath();
ctx.moveTo( controlPoints[0].x, controlPoints[0].y );
for (var p of controlPoints) {
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
// Draw intermediate steps
// Step1 (points[1])
ctx.strokeStyle = 'blue';
ctx.beginPath();
ctx.moveTo(points[1][0].x, points[1][0].y);
for (var j=1; j < points[1].length; j++) {
ctx.lineTo( points[1][j].x, points[1][j].y );
}
ctx.stroke();
// Step2 (points[2])
ctx.strokeStyle = 'green';
ctx.beginPath();
ctx.moveTo( points[2][0].x, points[2][0].y );
if (points[2].length >1 ) {
ctx.lineTo(points[2][1].x, points[2][1].y );
}
ctx.stroke();
// Step3 (points[3])
ctx.fillStyle = 'orange';
ctx.beginPath();
ctx.arc( points[3][0].x, points[3][0].y, 3, 0, 2*Math.PI );
ctx.fill();
// Draw the traced curve
if (tracedCurve.length >1) {
ctx.strokeStyle = '#00FF00';
ctx.beginPath();
ctx.moveTo( tracedCurve[0].x, tracedCurve[0].y );
for (var p of tracedCurve) {
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
}
// Add current point to tracedCurve
tracedCurve.push( {x: points[3][0].x, y: points[3][0].y } );
// Show control points coordinates
ctx.font = '12px Arial';
ctx.fillStyle = 'black';
controlPoints.forEach( function(p,i) {
ctx.fillText('P'+i+': ('+Math.round(p.x)+', '+Math.round(p.y)+')',
p.x +5, p.y +5 );
});
// Update time
t += dt;
if (t >= 1) {
init(); // reset after reaching t=1
}
}
init();
animate();
</script>
Wait, but in the above code, there is a problem: in the tracedCurve drawing, if tracedCurve has more than 1 point, it draws the entire tracedCurve every frame, which can be inefficient, but since it's a small array, it's okay.
Also, when resetting (t >=1), it calls init() which resets controlPoints, tracedCurve, and t. This will restart the animation with new control points.
Testing this code:
- The control points drift slowly around the canvas.
- The intermediate steps (blue, green, orange) show the De Casteljau algorithm steps.
- The tracedCurve is green, showing the Bezier curve.
- Coordinates are displayed next to each control point.
- When t reaches 1, the animation restarts with new random positions.
Potential improvements:
- The control points drift might cause overlapping coordinates labels.
- The tracedCurve could be cleared when resetting, but it's already handled by tracedCurve being reset in init().
- Maybe add a small delay before restarting to see the animation complete.
- The drifting mechanism could be made smoother, but it's a simple implementation.
This code should fulfill the requirements.