Reputation: 43
I need to find a point and its angle on a cubic Bezier curve that can be dynamically changed using JavaScript. I asked ChatGPT about this, to which it generated the following code, but the angle is not calculated correctly, where am I or is ChatGPT wrong?
// Initialize with some initial control points
let points = [
{ x: 50, y: 100 }, // Start point
{ x: 150, y: 50 }, // First control point
{ x: 250, y: 150 }, // Second control point
{ x: 350, y: 100 } // End point
];
function deCasteljau(points, t) {
if (points.length === 1) {
return points[0];
}
const newPoints = [];
for (let i = 0; i < points.length - 1; i++) {
const x = (1 - t) * points[i].x + t * points[i + 1].x;
const y = (1 - t) * points[i].y + t * points[i + 1].y;
newPoints.push({ x, y });
}
return deCasteljau(newPoints, t);
}
function cubicBezierDerivative(points, t) {
const derivativePoints = [];
const n = points.length - 1;
for (let i = 0; i < n; i++) {
const dx = n * (points[i + 1].x - points[i].x);
const dy = n * (points[i + 1].y - points[i].y);
derivativePoints.push({ x: dx, y: dy });
}
return derivativePoints;
}
function bezierAngle(points, t) {
const dPoints = cubicBezierDerivative(points, t);
const point = deCasteljau(points, t);
const dx = dPoints[0].x;
const dy = dPoints[0].y;
const radian = Math.atan2(dy, dx);
//const angle = radian*180/Math.PI;
return radian;
}
const point = deCasteljau(points, 0.9);
const angle = bezierAngle(points, 0.9);
live demo:
const canvas = document.getElementById('splineCanvas');
const ctx = canvas.getContext('2d');
let points = []; // Array to hold control points
let selectedPointIndex = -1; // Index of the currently selected control point
// Event listener for mouse down to select control point
canvas.addEventListener('mousedown', function(event) {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
// Check if mouse is over any control point
for (let i = 0; i < points.length; i++) {
const dx = points[i].x - mouseX;
const dy = points[i].y - mouseY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 6) { // 6 is the radius for selecting control point
selectedPointIndex = i;
canvas.addEventListener('mousemove', onMouseMove);
canvas.addEventListener('mouseup', onMouseUp);
break;
}
}
});
// Event listener for mouse move to update control point position
function onMouseMove(event) {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
points[selectedPointIndex].x = mouseX;
points[selectedPointIndex].y = mouseY;
drawSpline();
}
// Event listener for mouse up to stop updating control point position
function onMouseUp() {
canvas.removeEventListener('mousemove', onMouseMove);
canvas.removeEventListener('mouseup', onMouseUp);
selectedPointIndex = -1;
}
let testAngle = 65;
// Draw spline function
function drawSpline() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length - 2; i+=3) {
ctx.bezierCurveTo(
points[i].x,
points[i].y,
points[i+1].x,
points[i+1].y,
points[i+2].x,
points[i+2].y,
);
}
ctx.stroke();
// Draw control points
for (const point of points) {
ctx.beginPath();
ctx.arc(point.x, point.y, 6, 0, Math.PI * 2);
ctx.fillStyle = "#ff0000";
ctx.fill();
ctx.closePath();
}
const point = deCasteljau(points, 0.9);
//console.log('point = ', point);
const angle = bezierAngle(points, 0.9);
ctx.save();
ctx.translate(point.x, point.y);
ctx.rotate(angle);
ctx.translate(-point.x, -point.y);
ctx.fillStyle = "green";
ctx.fillRect(point.x-5, point.y-5, 10, 10);
ctx.restore();
}
// Initialize with some initial control points
points = [
{ x: 50, y: 100 }, // Start point
{ x: 150, y: 50 }, // First control point
{ x: 250, y: 150 }, // Second control point
{ x: 350, y: 100 } // End point
];
function deCasteljau(points, t) {
if (points.length === 1) {
return points[0];
}
const newPoints = [];
for (let i = 0; i < points.length - 1; i++) {
const x = (1 - t) * points[i].x + t * points[i + 1].x;
const y = (1 - t) * points[i].y + t * points[i + 1].y;
newPoints.push({ x, y });
}
return deCasteljau(newPoints, t);
}
function cubicBezierDerivative(points, t) {
const derivativePoints = [];
const n = points.length - 1;
for (let i = 0; i < n; i++) {
const dx = n * (points[i + 1].x - points[i].x);
const dy = n * (points[i + 1].y - points[i].y);
derivativePoints.push({ x: dx, y: dy });
}
return derivativePoints;
}
function bezierAngle(points, t) {
const dPoints = cubicBezierDerivative(points, t);
const point = deCasteljau(points, t);
const dx = dPoints[0].x;
const dy = dPoints[0].y;
const radian = Math.atan2(dy, dx);
//const angle = radian*180/Math.PI;
return radian;
}
drawSpline();
<canvas id="splineCanvas" width="600" height="300"></canvas>
Upvotes: 4
Views: 151
Reputation: 17307
There is no need to separate the point on Bézier calculations from the tangent angle calculation.
When calculating points at t
we can simply save intermediate calculation results to get both the exact tangent angle as well as the point at t in one function call.
To get the point on a cubic Bézier we can use this common formula:
pt = {
x:
t1 ** 3 * p0.x +
3 * t1 ** 2 * t * cp1.x +
3 * t1 * t ** 2 * cp2.x +
t ** 3 * p.x,
y:
t1 ** 3 * p0.y +
3 * t1 ** 2 * t * cp1.y +
3 * t1 * t ** 2 * cp2.y +
t ** 3 * p.y,
};
What it actually does is combining multiple linear interpolations. A more verbose notation would be:
// point between starting point and cp1
let m0 = {
x: (cp1.x - p0.x) * t + p0.x,
y: (cp1.y - p0.y) * t + p0.y,
}
// x: (p2.x - p1.x) * t + p1.x,
// point between cp1 and cp2
let m1 = {
x: (cp2.x - cp1.x) * t + cp1.x,
y: (cp2.y - cp1.y) * t + cp1.y,
}
// point between cp2 and final on-path point
let m2 = {
x: (p.x - cp2.x) * t + cp2.x,
y: (p.y - cp2.y) * t + cp2.y,
}
// point between m0 and m1 - describing tangent starting point
let m3 = {
x: (m1.x - m0.x) * t + m0.x,
y: (m1.y - m0.y) * t + m0.y,
}
// point between m1 and m2 - describing tangent end point
let m4 = {
x: (m2.x - m1.x) * t + m1.x,
y: (m2.y - m1.y) * t + m1.y,
}
// calculate tangent angle from m3 and m4
pt.angle = Math.atan2(m4.y - m3.y, m4.x - m3.x);
// point at t
pt.x= (m4.x - m3.x) * t + m3.x
pt.y= (m4.y - m3.y) * t + m3.y
This way we can get the required intermediate point calculations (m3, m4) to calculate the tangent angles.
<style>
svg {
display: block;
outline: 1px solid #ccc;
overflow: visible;
max-height: 90vh
}
line {
stroke-dasharray: 0 4px;
stroke-linecap: round;
}
</style>
<p>
<label >Point at t</label>
<input id="inputT" type="range" value="0.5" min="0" max="1" step="0.05">
</p>
<svg id="svg" viewBox="50 10 300 150">
<path d="M 50 100 C 170 20 240 170 350 10" fill="none" stroke="#000" />
<g id="tangents"></g>
</svg>
<script>
let pts = [{
x: 50,
y: 100
}, // Start point
{
x: 170,
y: 20
}, // First control point
{
x: 240,
y: 170
}, // Second control point
{
x: 350,
y: 10
}
];
let pt1 = pointAtT(pts, 0.5);
inputT.addEventListener('input', (e) => {
let t = +inputT.value;
tangents.innerHTML = '';
let pt1 = pointAtT(pts, t);
})
function pointAtT(pts, t = 0.5) {
// get angle helper
const getAngle = (p1, p2) => {
let angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
return angle;
};
/**
* Linear interpolation (LERP) helper
*/
const interpolate = (p1, p2, t) => {
let pt = {
x: (p2.x - p1.x) * t + p1.x,
y: (p2.y - p1.y) * t + p1.y
};
pt.angle = getAngle(p1, p2);
return pt;
};
/**
* calculate single points on bézier segments
*/
const getPointAtCubicSegmentT = (p0, cp1, cp2, p, t) => {
let t1 = 1 - t;
// point between starting point and cp1
let m0 = interpolate(p0, cp1, t);
// point between cp1 and cp2
let m1 = interpolate(cp1, cp2, t);
// point between cp2 and final on-path point
let m2 = interpolate(cp2, p, t);
// point between m0 and m1
let m3 = interpolate(m0, m1, t);
// point between m1 and m2
let m4 = interpolate(m1, m2, t);
// point at t
pt = interpolate(m3, m4, t);
// angle
pt.angle = getAngle(m3, m4);
renderLine(tangents, [p0, cp1], '#ccc')
renderLine(tangents, [p, cp2], '#ccc')
renderLine(tangents, [m0, m1], 'blue')
renderLine(tangents, [m1, m2], 'blue')
renderLine(tangents, [m3, m4], 'red', '1.5%')
renderPoint(tangents, pt, 'green')
return pt;
};
let pt;
// cubic beziers
pt = getPointAtCubicSegmentT(pts[0], pts[1], pts[2], pts[3], t);
// normalize negative angles
if (pt.angle < 0) pt.angle += Math.PI * 2;
return pt;
}
function renderPoint(
svg,
coords,
fill = "red",
r = "2%",
opacity = "1",
id = "",
className = ""
) {
//console.log(coords);
if (Array.isArray(coords)) {
coords = {
x: coords[0],
y: coords[1]
};
}
let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
<title>${coords.x} ${coords.y}</title></circle>`;
svg.insertAdjacentHTML("beforeend", marker);
}
function renderLine(
svg,
pts,
stroke = "red",
strokeWidth = "1%",
opacity = "1",
id = "",
) {
let line = `<line stroke-opacity="${opacity}" id="${id}"
x1="${pts[0].x}" y1="${pts[0].y}"
x2="${pts[1].x}" y2="${pts[1].y}"
stroke="${stroke}"
stroke-width="${strokeWidth}" />`;
svg.insertAdjacentHTML("beforeend", line);
}
</script>
As you can see in the above example: the red line describes the actual tangent angle and is based on the intermediate points that are required to get the on-path point at the specified t
value.
Canvas demo is based on Helder Sepulveda's snippet
function pointAtT(pts, t = 0.5) {
// get angle helper
const getAngle = (p1, p2) => {
let angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
return angle;
};
/**
* Linear interpolation (LERP) helper
*/
const interpolate = (p1, p2, t) => {
let pt = {
x: (p2.x - p1.x) * t + p1.x,
y: (p2.y - p1.y) * t + p1.y
};
pt.angle = getAngle(p1, p2);
return pt;
};
/**
* calculate single points on bézier segments
*/
const getPointAtCubicSegmentT = (p0, cp1, cp2, p, t) => {
let t1 = 1 - t;
let pt = {
x: 0,
y: 0
};
// t=0: get angle between p0 and cp1
if (t === 0) {
pt.x = p0.x;
pt.y = p0.y;
pt.angle = getAngle(p0, cp1);
}
// t=1: get angle between cp2 and p
else if (t === 1) {
pt.x = p.x;
pt.y = p.y;
pt.angle = getAngle(cp2, p);
} else {
// point between starting point and cp1
let m0 = interpolate(p0, cp1, t);
// point between cp1 and cp2
let m1 = interpolate(cp1, cp2, t);
// point between cp2 and final on-path point
let m2 = interpolate(cp2, p, t);
// point between m0 and m1
let m3 = interpolate(m0, m1, t);
// point between m1 and m2
let m4 = interpolate(m1, m2, t);
// angle
pt.angle = getAngle(m3, m4);
// point at t
pt = interpolate(m3, m4, t);
}
return pt;
};
const getPointAtQuadraticSegmentT = (p0, cp1, p, t) => {
let t1 = 1 - t;
let pt = {
x: 0,
y: 0
};
if (t === 0) {
pt.x = p0.x;
pt.y = p0.y;
pt.angle = getAngle(p0, cp1);
} else if (t === 1) {
pt.x = p.x;
pt.y = p.y;
pt.angle = getAngle(cp1, p);
} else {
let m1 = interpolate(p0, cp1, t);
let m2 = interpolate(cp1, p, t);
pt = interpolate(m1, m2, t);
pt.angle = getAngle(m1, m2);
}
return pt;
};
let pt;
// cubic beziers
if (pts.length === 4) {
pt = getPointAtCubicSegmentT(pts[0], pts[1], pts[2], pts[3], t);
}
// quadratic beziers
else if (pts.length === 3) {
pt = getPointAtQuadraticSegmentT(pts[0], pts[1], pts[2], t);
}
// lines
else {
pt = interpolate(pts[0], pts[1], t);
}
// normalize negative angles
if (pt.angle < 0) pt.angle += Math.PI * 2;
return pt;
}
<canvas id="splineCanvas" width="400" height="180"></canvas>
<script>
window.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('splineCanvas');
const ctx = canvas.getContext('2d');
let points = [{
x: 50,
y: 100
}, // Start point
{
x: 170,
y: 20
}, // First control point
{
x: 240,
y: 170
}, // Second control point
{
x: 350,
y: 10
} // End point
]
let selectedPointIndex = -1;
// Event listener for mouse down to select control point
canvas.addEventListener('mousedown', function(event) {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
// Check if mouse is over any control point
for (let i = 0; i < points.length; i++) {
const dx = points[i].x - mouseX;
const dy = points[i].y - mouseY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 6) { // 6 is the radius for selecting control point
selectedPointIndex = i;
canvas.addEventListener('mousemove', onMouseMove);
canvas.addEventListener('mouseup', onMouseUp);
break;
}
}
});
// Event listener for mouse move to update control point position
function onMouseMove(event) {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
points[selectedPointIndex].x = mouseX;
points[selectedPointIndex].y = mouseY;
drawSpline();
}
// Event listener for mouse up to stop updating control point position
function onMouseUp() {
canvas.removeEventListener('mousemove', onMouseMove);
canvas.removeEventListener('mouseup', onMouseUp);
selectedPointIndex = -1;
}
let testAngle = 65;
// Draw spline function
function drawSpline() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
ctx.bezierCurveTo(
points[1].x, points[1].y,
points[2].x, points[2].y,
points[3].x, points[3].y,
);
ctx.stroke();
// Draw control points
for (const point of points) {
ctx.beginPath();
ctx.arc(point.x, point.y, 6, 0, Math.PI * 2);
ctx.fillStyle = "#ff0000";
ctx.fill();
ctx.closePath();
}
drawRect(0.2, "cyan")
drawRect(0.5, "blue")
drawRect(0.8, "green")
}
function drawRect(t, color) {
// get point and angle
let pt = pointAtT(points, t)
ctx.save();
ctx.globalAlpha = 0.6
ctx.translate(pt.x, pt.y);
ctx.rotate(pt.angle);
ctx.translate(-pt.x, -pt.y);
ctx.fillStyle = color;
ctx.fillRect(pt.x - 15, pt.y - 15, 30, 30);
ctx.restore();
}
drawSpline();
})
</script>
See also "Divide bezier curve into two equal halves"
Upvotes: 0
Reputation: 54089
Example code below gives exact tangent of curve at unit position on bezier.
The example normalizes the tangent to be more useful for various rendering tasks.
Solutions includes quadratic and cubic curves.
To convert the tangent to an angle just use Math.atan2(tangent.y, tangent.x);
though there is no need as a matrix can be constructed directly from the tangent (no need to mess with rotations, translations, etc...) E.G. ctx.setTransform(tangent.x, tangent.y, -tangent.y, tangent.x, pos.x, pos.y);
where pos
is the position on the curve.
const ctx = canvas.getContext("2d");
const TAU = Math.PI * 2;
const Vec2 = (x = 0, y = 0) => ({x, y});
const RotateVec90 = v => Vec2(-v.y, v.x);
const QBez = (p1, cp1, p2) => ({p1, cp1, p2}); // Quadratic bez 3 Vec2 p1, p2 start and end, cp1 control point
const CBez = (p1, cp1, cp2, p2) => ({p1, cp1, cp2, p2}); // Cubic bez 4 Vec2 p1, p2 start and end, cp1, cp2 control points
const Beziers = {
asArray(bez) {
return bez.cp2 === undefined ?
[bez.p1, bez.cp1, bez.p2] :
[bez.p1, bez.cp1, bez.cp2, bez.p2];
},
tangentAt(bez, pos, limit = true, tangent = Vec2()) {
if (limit) { pos = Math.min(1, Math.max(0, pos)); }
if (bez.cp2 === undefined) { /* is quadratic */
const a = (1 - pos) * 2;
const b = pos * 2;
tangent.x = a * (bez.cp1.x - bez.p1.x) + b * (bez.p2.x - bez.cp1.x);
tangent.y = a * (bez.cp1.y - bez.p1.y) + b * (bez.p2.y - bez.cp1.y);
} else { /* is cubic */
const a = (1 - pos)
const b = 6 * a * pos;
const c = a * 3 * a;
const d = 3 * pos * pos;
tangent.x = -bez.p1.x * c + bez.cp1.x * (c - b) + bez.cp2.x * (b - d) + bez.p2.x * d;
tangent.y = -bez.p1.y * c + bez.cp1.y * (c - b) + bez.cp2.y * (b - d) + bez.p2.y * d;
}
const u = 1.0 / (tangent.x * tangent.x + tangent.y * tangent.y) ** 0.5;
tangent.x *= u;
tangent.y *= u;
return tangent;
},
pointAt(bez, pos, limit = true, point = Vec2()) {
if (limit) {
if (pos <= 0) {
point.x = bez.p1.x;
point.y = bez.p1.y;
return point;
}
if (pos >= 1) {
point.x = bez.p2.x;
point.y = bez.p2.y;
return point;
}
}
const v1 = Vec2(bez.p1.x, bez.p1.y);
const v2 = Vec2(bez.cp1.x, bez.cp1.y);
const c = pos;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
if (bez.cp2 === undefined) { /* is quadratic */
v2.x += (bez.p2.x - v2.x) * c;
v2.y += (bez.p2.y - v2.y) * c;
point.x = v1.x + (v2.x - v1.x) * c;
point.y = v1.y + (v2.y - v1.y) * c;
return point;
}
const v3 = Vec2(bez.cp2.x, bez.cp2.y);
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
v3.x += (bez.p2.x - v3.x) * c;
v3.y += (bez.p2.y - v3.y) * c;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
point.x = v1.x + (v2.x - v1.x) * c;
point.y = v1.y + (v2.y - v1.y) * c;
return point;
},
};
const Render = {
draw(bez, width = 2, color = "#000") {
ctx.lineWidth = width;
ctx.strokeStyle = color;
ctx.beginPath();
ctx.lineTo(bez.p1.x, bez.p1.y);
if (bez.cp2 === undefined) { /* is quadratic */
ctx.quadraticCurveTo(bez.cp1.x, bez.cp1.y, bez.p2.x, bez.p2.y);
} else {
ctx.bezierCurveTo(bez.cp1.x, bez.cp1.y, bez.cp2.x, bez.cp2.y, bez.p2.x, bez.p2.y)
}
ctx.stroke();
},
drawPoints(radius, color, ...points) {
ctx.fillStyle = color;
ctx.beginPath();
for (const p of points) {
ctx.moveTo(p.x + radius, p.y);
ctx.arc(p.x, p.y, radius, 0, TAU);
}
ctx.fill();
},
drawVector(pos, unitVec, len, width = 2, color = "#000") {
ctx.lineWidth = width;
ctx.strokeStyle = color;
ctx.beginPath();
ctx.lineTo(pos.x, pos.y);
ctx.lineTo(pos.x + unitVec.x * len, pos.y + unitVec.y * len);
ctx.stroke();
}
};
const curve = CBez(Vec2(50, 100), Vec2(170, 10), Vec2(260, 270), Vec2(350, 10));
Render.draw(curve);
Render.drawPoints(4, "#A00", ...Beziers.asArray(curve));
var pos = 0.0;
while (pos <= 1.01) {
Render.drawVector(Beziers.pointAt(curve, pos, true), RotateVec90(Beziers.tangentAt(curve, pos, true)), 20, 1, "#0A0");
pos += 0.02;
}
<canvas id="canvas" width="400" height="400"></canvas>
Upvotes: 2
Reputation: 17624
Just from a math perspective I question what you mean by "finding a point and its angle" points in 2D space, like you represented in the points array, are just {x, y}
there is no angle ...
What we can do is calculate the angle between two points, and looks like that is what the functions cubicBezierDerivative
& bezierAngle
attempted to do, I'm assuming that is what you need/ask, I would base my code below on that, also I'm going to assume that the point returned by function deCasteljau
is correct, I'm not going to spend any research time into how is that doing what is doing.
So we can modify the function bezierAngle
to return the angle between two given points, the current point and and what I call "next" point:
const point = deCasteljau(points, x);
const next = deCasteljau(points, x + 0.01);
const angle = bezierAngle([point, next]);
with the angle between those two the square we draw is "facing" the right way.
In the code below you will see a new function drawRect
that is where we draw the squares, and since it is now a function we can have multiple square with different colors
const canvas = document.getElementById('splineCanvas');
const ctx = canvas.getContext('2d');
let points = [
{ x: 50, y: 100 }, // Start point
{ x: 170, y: 20 }, // First control point
{ x: 240, y: 170 }, // Second control point
{ x: 350, y: 10 } // End point
]
let selectedPointIndex = -1;
// Event listener for mouse down to select control point
canvas.addEventListener('mousedown', function(event) {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
// Check if mouse is over any control point
for (let i = 0; i < points.length; i++) {
const dx = points[i].x - mouseX;
const dy = points[i].y - mouseY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 6) { // 6 is the radius for selecting control point
selectedPointIndex = i;
canvas.addEventListener('mousemove', onMouseMove);
canvas.addEventListener('mouseup', onMouseUp);
break;
}
}
});
// Event listener for mouse move to update control point position
function onMouseMove(event) {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
points[selectedPointIndex].x = mouseX;
points[selectedPointIndex].y = mouseY;
drawSpline();
}
// Event listener for mouse up to stop updating control point position
function onMouseUp() {
canvas.removeEventListener('mousemove', onMouseMove);
canvas.removeEventListener('mouseup', onMouseUp);
selectedPointIndex = -1;
}
let testAngle = 65;
// Draw spline function
function drawSpline() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
ctx.bezierCurveTo(
points[1].x, points[1].y,
points[2].x, points[2].y,
points[3].x, points[3].y,
);
ctx.stroke();
// Draw control points
for (const point of points) {
ctx.beginPath();
ctx.arc(point.x, point.y, 6, 0, Math.PI * 2);
ctx.fillStyle = "#ff0000";
ctx.fill();
ctx.closePath();
}
drawRect(0.2, "cyan")
drawRect(0.5, "blue")
drawRect(0.8, "green")
}
function drawRect(x, color) {
const point = deCasteljau(points, x);
const next = deCasteljau(points, x + 0.01);
const angle = bezierAngle([point, next]);
ctx.save();
ctx.globalAlpha = 0.6
ctx.translate(point.x, point.y);
ctx.rotate(angle);
ctx.translate(-point.x, -point.y);
ctx.fillStyle = color;
ctx.fillRect(point.x-15, point.y-15, 30, 30);
//ctx.fillStyle = "black";
//ctx.fillText(angle, point.x, point.y);
ctx.restore();
}
function deCasteljau(points, t) {
if (points.length === 1) {
return points[0];
}
const newPoints = [];
for (let i = 0; i < points.length - 1; i++) {
const x = (1 - t) * points[i].x + t * points[i + 1].x;
const y = (1 - t) * points[i].y + t * points[i + 1].y;
newPoints.push({ x, y });
}
return deCasteljau(newPoints, t);
}
function bezierAngle(points) {
const dx = points[1].x - points[0].x;
const dy = points[1].y - points[0].y;
const radian = Math.atan2(dy, dx);
return radian;
}
drawSpline();
<canvas id="splineCanvas" width="400" height="180"></canvas>
Upvotes: 0