我有一个使用贝塞尔曲线的 SVG 路径,例如:
m 776,2226 c 0,0 258.61385,-173.7593 289.34025,-325.8576 57.158,-282.9367 15.5277,-622.2212 50.8732,-933.13714 12.8345,-112.89946 104.2775,-278.6582 22.2568,-340.66923 -50.5144,-38.19103 -158.97817,99.97904 -158.97817,99.97904
在我的应用中,我想从初始点(x=776,y=2226)开始,慢慢地画出路径。例如,当用户按下按钮时,路径会显示更多。
我想使用 HTML 画布来做这件事。
请注意,此路径不是封闭路径。
我想到了用Canvas的isPointInPath()
函数,从初始点开始,一一画像素。但是,如何找到路径中的所有点?
对此的替代方法是什么?
答案 0 :(得分:0)
也许stroke-dashoffset
?有一个很好的小解释器和 demo on CSS-Tricks.
答案 1 :(得分:0)
就像在 SVG 中一样,Canvas2D API 具有 dash-offset 和 stroke-dasharray 选项,分别通过 lineDashOffset
属性和 setLineDash 方法。
然而,这个 API 仍然缺乏正确的方法来测量路径的长度(早期有关于扩展 Path2D API 的讨论,但还没有具体的讨论)。
你可以自己计算那个长度,但路径越复杂,你就越有可能弄错......
因此,实际上最简单的方法可能是使用 SVGGeometry 元素,它确实公开了一个 .getTotalLength()
方法。
const declaration = `M 10,30
A 20,20 0,0,1 50,30
A 20,20 0,0,1 90,30
Q 90,60 50,90
Q 10,60 10,30 z`;
const geom = document.createElementNS( "http://www.w3.org/2000/svg", "path" );
geom.setAttribute( "d", declaration );
const length = geom.getTotalLength();
const path = new Path2D( declaration );
const canvas = document.querySelector( "canvas" );
const ctx = canvas.getContext( "2d" );
// [ dash - hole ]
ctx.setLineDash( [ length, length ] );
const duration = 2000;
const start = performance.now();
requestAnimationFrame( draw );
ctx.fillStyle = "green";
ctx.strokeStyle = "red";
function draw(now) {
ctx.clearRect( 0, 0, canvas.width, canvas.height );
const delta = (now % duration) / duration;
ctx.lineDashOffset = length - (length * delta);
ctx.stroke( path );
requestAnimationFrame( draw );
}
<canvas width="100" height="100"></canvas>
对于那些为了标题来到这里并想要沿路径移动形状的人,我们可以继续使用我们的 SVGGeometryElement 及其 getPointAtLength 方法:
const declaration = `M 10,30
A 20,20 0,0,1 50,30
A 20,20 0,0,1 90,30
Q 90,60 50,90
Q 10,60 10,30 z`;
const geom = document.createElementNS( "http://www.w3.org/2000/svg", "path" );
geom.setAttribute( "d", declaration );
const length = geom.getTotalLength();
const path = new Path2D( declaration );
const canvas = document.querySelector( "canvas" );
const ctx = canvas.getContext( "2d" );
const duration = 2000;
const start = performance.now();
requestAnimationFrame( draw );
ctx.fillStyle = "green";
ctx.strokeStyle = "red";
function draw(now) {
ctx.clearRect( 0, 0, canvas.width, canvas.height );
const delta = (now % duration) / duration;
const point = geom.getPointAtLength( length * delta );
ctx.fillRect( point.x - 10, point.y -10, 20, 20 );
ctx.stroke( path );
requestAnimationFrame( draw );
}
<canvas width="100" height="100"></canvas>
现在,在你的情况下,我想你仍然需要做一些工作来正确缩放返回值,就像你在画布上绘制时可能缩放路径一样,但我会把它作为练习(没有太复杂了)。
答案 2 :(得分:0)
我对贝塞尔曲线的数学以及它们在 SVG 中的表示方式进行了一些研究。
我的路径使用 m
命令表示路径的起点,使用 c
命令表示带有相对控制点和端点的贝塞尔曲线。不过曲线有多个段。
示例路径
m 1,2 c 3,4 5,6 7,8 9,10 11,12 13,14
意思是:
使用贝塞尔曲线的数学运算,我编写了这段代码来返回具有多个线段的贝塞尔曲线上的点。请注意,它仅处理路径以 m
开头,然后以 c
命令继续的情况。
const POINTS_IN_SEGMENT = 6;
class Point {
x;
y;
constructor(xStr, yStr) {
this.x = parseInt(xStr, 10);
this.y = parseInt(yStr, 10);
}
add(other) {
return new Point(this.x + other.x, this.y + other.y);
}
}
class BezierCurveSegment {
startPoint;
controlPoint1;
controlPoint2;
endPoint;
constructor(startPoint, controlPoint1, controlPoint2, endPoint) {
this.startPoint = startPoint;
this.controlPoint1 = controlPoint1;
this.controlPoint2 = controlPoint2;
this.endPoint = endPoint;
}
getPointsOnSegment(accuracy) {
const points = [];
for (let i = 0; i < 1; i += accuracy) {
const p = bezier(i, this.startPoint, this.controlPoint1, this.controlPoint2, this.endPoint);
points.push(new Point(p.x, p.y));
}
return points;
}
}
class BezierCurve {
startPoint;
segments;
constructor(startPoint, segments) {
this.startPoint = startPoint;
this.segments = segments;
}
getPointsOnCurve(pointCount) {
const accuracy = 1 / pointCount * this.segments.length;
let points = [];
for (let segment of this.segments) {
let pointsOnSegment = segment.getPointsOnSegment(accuracy);
points = points.concat(pointsOnSegment);
}
return points;
}
}
// https://stackoverflow.com/questions/16227300/
function bezier(t, p0, p1, p2, p3) {
// console.log("bezier", t, p0, p1, p2, p3)
var cX = 3 * (p1.x - p0.x),
bX = 3 * (p2.x - p1.x) - cX,
aX = p3.x - p0.x - cX - bX;
var cY = 3 * (p1.y - p0.y),
bY = 3 * (p2.y - p1.y) - cY,
aY = p3.y - p0.y - cY - bY;
var x = (aX * Math.pow(t, 3)) + (bX * Math.pow(t, 2)) + (cX * t) + p0.x;
var y = (aY * Math.pow(t, 3)) + (bY * Math.pow(t, 2)) + (cY * t) + p0.y;
return {x: x, y: y};
}
function createCurveFromSVGPath(pathStr) {
pathStr = pathStr.replaceAll(',', ' ');
let items = pathStr.split(' ');
if (items[0] !== "m") {
throw "First item in the path is not 'm' command (only relative is supported)";
}
if (items[3] !== "c") {
throw "Third item in the path is not 'c' command (only relative is supported)";
}
let startPoint = new Point(items[1], items[2]);
// done with "m x,y c" items
items = items.slice(4);
// divide by the number of points each segment has
let segmentCount = items.length / POINTS_IN_SEGMENT;
let segments = [];
for (let i = 0; i < segmentCount; i++) {
let lastSegmentEndPoint;
if (i === 0) {
lastSegmentEndPoint = startPoint;
} else {
lastSegmentEndPoint = segments[i - 1].endPoint;
}
// noinspection PointlessArithmeticExpressionJS
const segment = new BezierCurveSegment(
// all these numbers are relative to previous segment's end point
// see https://stackoverflow.com/questions/26675960/
lastSegmentEndPoint,
lastSegmentEndPoint.add(new Point(items[i * POINTS_IN_SEGMENT + 0], items[i * POINTS_IN_SEGMENT + 1])),
lastSegmentEndPoint.add(new Point(items[i * POINTS_IN_SEGMENT + 2], items[i * POINTS_IN_SEGMENT + 3])),
lastSegmentEndPoint.add(new Point(items[i * POINTS_IN_SEGMENT + 4], items[i * POINTS_IN_SEGMENT + 5])),
);
segments.push(segment);
}
return new BezierCurve(startPoint, segments);
}
为了验证它是否有效,我在画布上绘制了点,并使用 Path2D 绘制了相同的路径。
function drawPoints(ctx, points) {
ctx.moveTo(points[0].x, points[0].y);
for (let point of points) {
ctx.lineTo(point.x, point.y);
ctx.moveTo(point.x, point.y);
}
ctx.stroke();
}
let pathStr = 'm 776.65415,2226.6574 c 0,0 258.61385,-173.7593 289.34025,-325.8576 57.158,-282.9367 15.5277,-622.2212 50.8732,-933.13714 12.8345,-112.89946 104.2775,-278.6582 22.2568,-340.66923 -50.5144,-38.19103 -158.97817,99.97904 -158.97817,99.97904';
const curve = createCurveFromSVGPath(pathStr);
let pointsOnCurve = curve.getPointsOnCurve(100000);
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
drawPoints(ctx, pointsOnCurve);
ctx.strokeStyle = "#FF0000";
ctx.stroke(new Path2D(pathStr););