mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 06:48:12 -05:00
feat: add polyline (#84)
This commit is contained in:
@@ -287,6 +287,7 @@ export class Layout extends Node {
|
||||
|
||||
@cloneable(false)
|
||||
@wrapper(Vector2)
|
||||
@initial({x: null, y: null})
|
||||
@compound({x: 'width', y: 'height'})
|
||||
public declare readonly size: Vector2LengthProperty<this>;
|
||||
|
||||
@@ -401,12 +402,12 @@ export class Layout extends Node {
|
||||
* inheritance).
|
||||
*/
|
||||
@computed()
|
||||
protected layoutEnabled(): boolean {
|
||||
public layoutEnabled(): boolean {
|
||||
return this.layout() ?? this.parentTransform()?.layoutEnabled() ?? false;
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected isLayoutRoot(): boolean {
|
||||
public isLayoutRoot(): boolean {
|
||||
return !this.layoutEnabled() || !this.parentTransform()?.layoutEnabled();
|
||||
}
|
||||
|
||||
@@ -422,7 +423,9 @@ export class Layout extends Node {
|
||||
}
|
||||
|
||||
protected getComputedLayout(): Rect {
|
||||
return new Rect(this.element.getBoundingClientRect());
|
||||
const rect = new Rect(this.element.getBoundingClientRect());
|
||||
rect.position = rect.position.add(this.getCustomOffset());
|
||||
return rect;
|
||||
}
|
||||
|
||||
@computed()
|
||||
@@ -448,7 +451,7 @@ export class Layout extends Node {
|
||||
@computed()
|
||||
protected computedSize(): Vector2 {
|
||||
this.requestLayoutUpdate();
|
||||
return new Vector2(this.getComputedLayout());
|
||||
return this.getComputedLayout().size;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -509,8 +512,9 @@ export class Layout extends Node {
|
||||
}
|
||||
|
||||
protected override getCacheRect(): Rect {
|
||||
const size = this.computedSize();
|
||||
return new Rect(size.scale(-0.5), size);
|
||||
const rect = Rect.fromSizeCentered(this.computedSize());
|
||||
rect.position = rect.position.sub(this.getCustomOffset());
|
||||
return rect;
|
||||
}
|
||||
|
||||
protected override draw(context: CanvasRenderingContext2D) {
|
||||
@@ -536,6 +540,7 @@ export class Layout extends Node {
|
||||
const size = this.computedSize();
|
||||
const offset = size.mul(this.offset()).scale(0.5).transformAsPoint(matrix);
|
||||
const rect = Rect.fromSizeCentered(size);
|
||||
rect.position = rect.position.sub(this.getCustomOffset());
|
||||
const layout = rect.transformCorners(matrix);
|
||||
const padding = rect
|
||||
.addSpacing(this.padding().scale(-1))
|
||||
@@ -673,7 +678,7 @@ export class Layout extends Node {
|
||||
|
||||
public override hit(position: Vector2): Node | null {
|
||||
const local = position.transformAsPoint(this.localToParent().inverse());
|
||||
if (this.getCacheRect().includes(local)) {
|
||||
if (this.cacheRect().includes(local)) {
|
||||
return super.hit(position) ?? this;
|
||||
}
|
||||
|
||||
|
||||
276
packages/2d/src/components/Line.ts
Normal file
276
packages/2d/src/components/Line.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import {Shape, ShapeProps} from './Shape';
|
||||
import {Node} from './Node';
|
||||
import {computed, initial, property} from '../decorators';
|
||||
import {arc, lineTo, moveTo, resolveCanvasStyle} from '../utils';
|
||||
import {Signal, SignalValue} from '@motion-canvas/core/lib/utils';
|
||||
import {Rect, Vector2} from '@motion-canvas/core/lib/types';
|
||||
import {clamp} from '@motion-canvas/core/lib/tweening';
|
||||
import {Length} from '../partials';
|
||||
import {Layout} from './Layout';
|
||||
import {
|
||||
CurveDrawingInfo,
|
||||
CurveProfile,
|
||||
getPolylineProfile,
|
||||
getPointAtDistance,
|
||||
CurvePoint,
|
||||
} from '../curves';
|
||||
|
||||
export interface LineProps extends ShapeProps {
|
||||
children: Node[];
|
||||
closed?: SignalValue<boolean>;
|
||||
radius?: SignalValue<number>;
|
||||
start?: SignalValue<number>;
|
||||
startOffset?: SignalValue<number>;
|
||||
startArrow?: SignalValue<boolean>;
|
||||
end?: SignalValue<number>;
|
||||
endOffset?: SignalValue<number>;
|
||||
endArrow?: SignalValue<boolean>;
|
||||
arrowSize?: SignalValue<number>;
|
||||
}
|
||||
|
||||
export class Line extends Shape {
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly radius: Signal<number, this>;
|
||||
|
||||
@initial(false)
|
||||
@property()
|
||||
public declare readonly closed: Signal<boolean, this>;
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly start: Signal<number, this>;
|
||||
|
||||
@initial(0)
|
||||
@property()
|
||||
public declare readonly startOffset: Signal<number, this>;
|
||||
|
||||
@initial(false)
|
||||
@property()
|
||||
public declare readonly startArrow: Signal<boolean, this>;
|
||||
|
||||
@initial(1)
|
||||
@property()
|
||||
public declare readonly end: Signal<number, this>;
|
||||
|
||||
@initial(1)
|
||||
@property()
|
||||
public declare readonly endOffset: Signal<number, this>;
|
||||
|
||||
@initial(false)
|
||||
@property()
|
||||
public declare readonly endArrow: Signal<boolean, this>;
|
||||
|
||||
@initial(24)
|
||||
@property()
|
||||
public declare readonly arrowSize: Signal<number, this>;
|
||||
|
||||
public getCustomWidth(): Length {
|
||||
return this.childrenRect().width;
|
||||
}
|
||||
public setCustomWidth() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public getCustomHeight(): Length {
|
||||
return this.childrenRect().height;
|
||||
}
|
||||
public setCustomHeight() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public constructor(props: LineProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected childrenRect() {
|
||||
return Rect.fromPoints(
|
||||
...this.children()
|
||||
.filter(child => !(child instanceof Layout) || child.isLayoutRoot())
|
||||
.map(child => child.position()),
|
||||
);
|
||||
}
|
||||
|
||||
@computed()
|
||||
public profile(): CurveProfile {
|
||||
const points = this.children().map(child => child.position());
|
||||
return getPolylineProfile(points, this.radius(), this.closed());
|
||||
}
|
||||
|
||||
public percentageToDistance(value: number): number {
|
||||
const {arcLength} = this.profile();
|
||||
const startOffset = this.startOffset();
|
||||
const endOffset = this.endOffset();
|
||||
const realLength = arcLength - startOffset - endOffset;
|
||||
return startOffset + realLength * value;
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected curveDrawingInfo(): CurveDrawingInfo {
|
||||
const path = new Path2D();
|
||||
const profile = this.profile();
|
||||
|
||||
let start = this.percentageToDistance(this.start());
|
||||
let end = this.percentageToDistance(this.end());
|
||||
if (start > end) {
|
||||
[start, end] = [end, start];
|
||||
}
|
||||
|
||||
const distance = end - start;
|
||||
const arrowSize = Math.min(distance / 2, this.arrowSize());
|
||||
|
||||
if (this.startArrow()) {
|
||||
start += arrowSize / 2;
|
||||
}
|
||||
|
||||
if (this.endArrow()) {
|
||||
end -= arrowSize / 2;
|
||||
}
|
||||
|
||||
let length = 0;
|
||||
let startPoint = null;
|
||||
let startTangent = null;
|
||||
let endPoint = null;
|
||||
let endTangent = null;
|
||||
for (const segment of profile.segments) {
|
||||
const previousLength = length;
|
||||
length += segment.arcLength;
|
||||
if (length < start) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relativeStart = (start - previousLength) / segment.arcLength;
|
||||
const relativeEnd = (end - previousLength) / segment.arcLength;
|
||||
|
||||
const clampedStart = clamp(0, 1, relativeStart);
|
||||
const clampedEnd = clamp(0, 1, relativeEnd);
|
||||
|
||||
const [
|
||||
currentStartPoint,
|
||||
currentStartTangent,
|
||||
currentEndPoint,
|
||||
currentEndTangent,
|
||||
] = segment.draw(path, clampedStart, clampedEnd, startPoint === null);
|
||||
|
||||
if (startPoint === null) {
|
||||
startPoint = currentStartPoint;
|
||||
startTangent = currentStartTangent;
|
||||
}
|
||||
|
||||
endPoint = currentEndPoint;
|
||||
endTangent = currentEndTangent;
|
||||
if (length > end) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
startPoint: startPoint ?? Vector2.zero,
|
||||
startTangent: startTangent ?? Vector2.up,
|
||||
endPoint: endPoint ?? Vector2.zero,
|
||||
endTangent: endTangent ?? Vector2.up,
|
||||
arrowSize,
|
||||
path,
|
||||
startOffset: start,
|
||||
};
|
||||
}
|
||||
|
||||
protected getPointAtDistance(value: number): CurvePoint {
|
||||
return getPointAtDistance(this.profile(), value + this.startOffset());
|
||||
}
|
||||
|
||||
public getPointAtPercentage(value: number): CurvePoint {
|
||||
return getPointAtDistance(this.profile(), this.percentageToDistance(value));
|
||||
}
|
||||
|
||||
protected override applyStyle(context: CanvasRenderingContext2D) {
|
||||
super.applyStyle(context);
|
||||
const {arcLength} = this.profile();
|
||||
context.lineDashOffset -= arcLength / 2;
|
||||
}
|
||||
|
||||
protected override getCustomOffset(): Vector2 {
|
||||
if (this.layoutEnabled()) {
|
||||
return this.childrenRect().center.flipped;
|
||||
} else {
|
||||
return Vector2.zero;
|
||||
}
|
||||
}
|
||||
|
||||
protected override getPath(): Path2D {
|
||||
return this.curveDrawingInfo().path;
|
||||
}
|
||||
|
||||
protected override getCacheRect(): Rect {
|
||||
const arrowSize = this.arrowSize();
|
||||
const lineWidth = this.lineWidth();
|
||||
return super.getCacheRect().expand(Math.max(0, arrowSize - lineWidth / 2));
|
||||
}
|
||||
|
||||
protected override drawShape(context: CanvasRenderingContext2D) {
|
||||
super.drawShape(context);
|
||||
const {startPoint, startTangent, endPoint, endTangent, arrowSize} =
|
||||
this.curveDrawingInfo();
|
||||
if (arrowSize < 0.001) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.save();
|
||||
context.beginPath();
|
||||
if (this.endArrow()) {
|
||||
this.drawArrow(context, endPoint, endTangent, arrowSize);
|
||||
}
|
||||
if (this.startArrow()) {
|
||||
this.drawArrow(context, startPoint, startTangent, arrowSize);
|
||||
}
|
||||
context.fillStyle = resolveCanvasStyle(this.stroke(), context);
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
private drawArrow(
|
||||
context: CanvasRenderingContext2D | Path2D,
|
||||
center: Vector2,
|
||||
tangent: Vector2,
|
||||
arrowSize: number,
|
||||
) {
|
||||
const normal = tangent.perpendicular;
|
||||
const origin = center.add(normal.scale(-arrowSize / 2));
|
||||
|
||||
moveTo(context, origin);
|
||||
lineTo(context, origin.add(normal.add(tangent).scale(arrowSize)));
|
||||
lineTo(context, origin.add(normal.sub(tangent).scale(arrowSize)));
|
||||
lineTo(context, origin);
|
||||
context.closePath();
|
||||
}
|
||||
|
||||
public override drawOverlay(
|
||||
context: CanvasRenderingContext2D,
|
||||
matrix: DOMMatrix,
|
||||
) {
|
||||
super.drawOverlay(context, matrix);
|
||||
|
||||
context.fillStyle = 'white';
|
||||
context.strokeStyle = 'black';
|
||||
context.lineWidth = 1;
|
||||
|
||||
const path = new Path2D();
|
||||
const points = this.children().map(child =>
|
||||
child.position().transformAsPoint(matrix),
|
||||
);
|
||||
moveTo(path, points[0]);
|
||||
for (const point of points) {
|
||||
lineTo(path, point);
|
||||
context.beginPath();
|
||||
arc(context, point, 4);
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
context.strokeStyle = 'white';
|
||||
context.stroke(path);
|
||||
}
|
||||
}
|
||||
@@ -337,6 +337,8 @@ export class Node implements Promisable<Node> {
|
||||
matrix.translateSelf(this.position.x(), this.position.y());
|
||||
matrix.rotateSelf(0, 0, this.rotation());
|
||||
matrix.scaleSelf(this.scale.x(), this.scale.y());
|
||||
const offset = this.getCustomOffset();
|
||||
matrix.translateSelf(offset.x, offset.y);
|
||||
|
||||
return matrix;
|
||||
}
|
||||
@@ -400,6 +402,13 @@ export class Node implements Promisable<Node> {
|
||||
return new DOMMatrix();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the custom offset for this node's children.
|
||||
*/
|
||||
protected getCustomOffset(): Vector2 {
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
@computed()
|
||||
public view(): View2D | null {
|
||||
return this.parent()?.view() ?? null;
|
||||
@@ -802,12 +811,19 @@ export class Node implements Promisable<Node> {
|
||||
*/
|
||||
public drawOverlay(context: CanvasRenderingContext2D, matrix: DOMMatrix) {
|
||||
const rect = this.cacheRect().transformCorners(matrix);
|
||||
const cache = this.getCacheRect().transformCorners(matrix);
|
||||
context.strokeStyle = 'white';
|
||||
context.lineWidth = 1;
|
||||
context.beginPath();
|
||||
drawLine(context, rect);
|
||||
context.closePath();
|
||||
context.stroke();
|
||||
|
||||
context.strokeStyle = 'blue';
|
||||
context.beginPath();
|
||||
drawLine(context, cache);
|
||||
context.closePath();
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
protected transformContext(context: CanvasRenderingContext2D) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './Circle';
|
||||
export * from './Image';
|
||||
export * from './Layout';
|
||||
export * from './Line';
|
||||
export * from './Node';
|
||||
export * from './Rect';
|
||||
export * from './Text';
|
||||
|
||||
66
packages/2d/src/curves/CircleSegment.ts
Normal file
66
packages/2d/src/curves/CircleSegment.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {Vector2} from '@motion-canvas/core/lib/types';
|
||||
import {Segment} from './Segment';
|
||||
|
||||
export class CircleSegment extends Segment {
|
||||
private readonly length: number;
|
||||
private readonly angle: number;
|
||||
|
||||
public constructor(
|
||||
private center: Vector2,
|
||||
private radius: number,
|
||||
private from: Vector2,
|
||||
private to: Vector2,
|
||||
private counter: boolean,
|
||||
) {
|
||||
super();
|
||||
this.angle = Math.acos(from.dot(to));
|
||||
this.length = Math.abs(this.angle * radius);
|
||||
}
|
||||
|
||||
public get arcLength(): number {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
public draw(
|
||||
context: CanvasRenderingContext2D | Path2D,
|
||||
from: number,
|
||||
to: number,
|
||||
): [Vector2, Vector2, Vector2, Vector2] {
|
||||
const counterFactor = this.counter ? -1 : 1;
|
||||
const startAngle = this.from.radians + from * this.angle * counterFactor;
|
||||
const endAngle = this.to.radians - (1 - to) * this.angle * counterFactor;
|
||||
|
||||
if (Math.abs(from - to) > 0.0001) {
|
||||
context.arc(
|
||||
this.center.x,
|
||||
this.center.y,
|
||||
this.radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
this.counter,
|
||||
);
|
||||
}
|
||||
|
||||
const startTangent = Vector2.fromRadians(startAngle);
|
||||
const endTangent = Vector2.fromRadians(endAngle);
|
||||
|
||||
return [
|
||||
this.center.add(startTangent.scale(this.radius)),
|
||||
this.counter ? startTangent : startTangent.flipped,
|
||||
this.center.add(endTangent.scale(this.radius)),
|
||||
this.counter ? endTangent.flipped : endTangent,
|
||||
];
|
||||
}
|
||||
|
||||
public getPoint(distance: number): [Vector2, Vector2] {
|
||||
const counterFactor = this.counter ? -1 : 1;
|
||||
const angle = this.from.radians + distance * this.angle * counterFactor;
|
||||
|
||||
const tangent = Vector2.fromRadians(angle);
|
||||
|
||||
return [
|
||||
this.center.add(tangent.scale(this.radius)),
|
||||
this.counter ? tangent : tangent.flipped,
|
||||
];
|
||||
}
|
||||
}
|
||||
11
packages/2d/src/curves/CurveDrawingInfo.ts
Normal file
11
packages/2d/src/curves/CurveDrawingInfo.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {Vector2} from '@motion-canvas/core/lib/types';
|
||||
|
||||
export interface CurveDrawingInfo {
|
||||
path: Path2D;
|
||||
arrowSize: number;
|
||||
endPoint: Vector2;
|
||||
endTangent: Vector2;
|
||||
startPoint: Vector2;
|
||||
startTangent: Vector2;
|
||||
startOffset: number;
|
||||
}
|
||||
6
packages/2d/src/curves/CurvePoint.ts
Normal file
6
packages/2d/src/curves/CurvePoint.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type {Vector2} from '@motion-canvas/core/lib/types';
|
||||
|
||||
export interface CurvePoint {
|
||||
position: Vector2;
|
||||
tangent: Vector2;
|
||||
}
|
||||
6
packages/2d/src/curves/CurveProfile.ts
Normal file
6
packages/2d/src/curves/CurveProfile.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import {Segment} from './Segment';
|
||||
|
||||
export interface CurveProfile {
|
||||
arcLength: number;
|
||||
segments: Segment[];
|
||||
}
|
||||
40
packages/2d/src/curves/LineSegment.ts
Normal file
40
packages/2d/src/curves/LineSegment.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {Vector2} from '@motion-canvas/core/lib/types';
|
||||
import {lineTo, moveTo} from '../utils';
|
||||
import {Segment} from './Segment';
|
||||
|
||||
export class LineSegment extends Segment {
|
||||
private readonly length: number;
|
||||
private readonly vector: Vector2;
|
||||
private readonly tangent: Vector2;
|
||||
|
||||
public constructor(private from: Vector2, private to: Vector2) {
|
||||
super();
|
||||
this.vector = to.sub(from);
|
||||
this.length = this.vector.magnitude;
|
||||
this.tangent = this.vector.perpendicular.normalized.safe;
|
||||
}
|
||||
|
||||
public get arcLength(): number {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
public draw(
|
||||
context: CanvasRenderingContext2D | Path2D,
|
||||
start = 0,
|
||||
end = 1,
|
||||
move = false,
|
||||
): [Vector2, Vector2, Vector2, Vector2] {
|
||||
const from = this.from.add(this.vector.scale(start));
|
||||
const to = this.from.add(this.vector.scale(end));
|
||||
if (move) {
|
||||
moveTo(context, from);
|
||||
}
|
||||
lineTo(context, to);
|
||||
return [from, this.tangent.flipped, to, this.tangent];
|
||||
}
|
||||
|
||||
public getPoint(distance: number): [Vector2, Vector2] {
|
||||
const point = this.from.add(this.vector.scale(distance));
|
||||
return [point, this.tangent.flipped];
|
||||
}
|
||||
}
|
||||
14
packages/2d/src/curves/Segment.ts
Normal file
14
packages/2d/src/curves/Segment.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {Vector2} from '@motion-canvas/core/lib/types';
|
||||
|
||||
export abstract class Segment {
|
||||
public abstract draw(
|
||||
context: CanvasRenderingContext2D | Path2D,
|
||||
start: number,
|
||||
end: number,
|
||||
move: boolean,
|
||||
): [Vector2, Vector2, Vector2, Vector2];
|
||||
|
||||
public abstract getPoint(distance: number): [Vector2, Vector2];
|
||||
|
||||
public abstract get arcLength(): number;
|
||||
}
|
||||
23
packages/2d/src/curves/getPointAtDistance.ts
Normal file
23
packages/2d/src/curves/getPointAtDistance.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {clamp} from '@motion-canvas/core/lib/tweening';
|
||||
import {Vector2} from '@motion-canvas/core/lib/types';
|
||||
import {CurveProfile} from './CurveProfile';
|
||||
import {CurvePoint} from './CurvePoint';
|
||||
|
||||
export function getPointAtDistance(
|
||||
profile: CurveProfile,
|
||||
distance: number,
|
||||
): CurvePoint {
|
||||
const clamped = clamp(0, profile.arcLength, distance);
|
||||
let length = 0;
|
||||
for (const segment of profile.segments) {
|
||||
const previousLength = length;
|
||||
length += segment.arcLength;
|
||||
if (length >= clamped) {
|
||||
const relative = (clamped - previousLength) / segment.arcLength;
|
||||
const [position, tangent] = segment.getPoint(clamp(0, 1, relative));
|
||||
return {position, tangent};
|
||||
}
|
||||
}
|
||||
|
||||
return {position: Vector2.zero, tangent: Vector2.up};
|
||||
}
|
||||
75
packages/2d/src/curves/getPolylineProfile.ts
Normal file
75
packages/2d/src/curves/getPolylineProfile.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import {Vector2} from '@motion-canvas/core/lib/types';
|
||||
import {CurveProfile} from './CurveProfile';
|
||||
import {LineSegment} from './LineSegment';
|
||||
import {CircleSegment} from './CircleSegment';
|
||||
|
||||
export function getPolylineProfile(
|
||||
points: Vector2[],
|
||||
radius: number,
|
||||
closed: boolean,
|
||||
): CurveProfile {
|
||||
const profile: CurveProfile = {
|
||||
arcLength: 0,
|
||||
segments: [],
|
||||
};
|
||||
|
||||
if (closed) {
|
||||
const middle = points[0].add(points[points.length - 1]).scale(0.5);
|
||||
points.unshift(middle);
|
||||
points.push(middle);
|
||||
}
|
||||
|
||||
let last = points[0];
|
||||
for (let i = 2; i < points.length; i++) {
|
||||
const start = points[i - 2];
|
||||
const center = points[i - 1];
|
||||
const end = points[i];
|
||||
|
||||
const centerToStart = start.sub(center);
|
||||
const centerToEnd = end.sub(center);
|
||||
const startVector = centerToStart.normalized;
|
||||
const endVector = centerToEnd.normalized;
|
||||
const angleBetween = Math.acos(startVector.dot(endVector));
|
||||
const angleTan = Math.tan(angleBetween / 2);
|
||||
|
||||
const safeRadius = Math.min(
|
||||
radius,
|
||||
angleTan * centerToStart.magnitude * (i === 2 ? 1 : 0.5),
|
||||
angleTan * centerToEnd.magnitude * (i === points.length - 1 ? 1 : 0.5),
|
||||
);
|
||||
|
||||
const circleOffsetDistance = safeRadius / Math.sin(angleBetween / 2);
|
||||
const pointOffsetDistance = safeRadius / angleTan;
|
||||
const circleDistance = startVector
|
||||
.add(endVector)
|
||||
.scale(1 / 2)
|
||||
.normalized.scale(circleOffsetDistance)
|
||||
.add(center);
|
||||
|
||||
const counter = startVector.perpendicular.dot(endVector) < 0;
|
||||
const line = new LineSegment(
|
||||
last,
|
||||
center.add(startVector.scale(pointOffsetDistance)),
|
||||
);
|
||||
const circle = new CircleSegment(
|
||||
circleDistance,
|
||||
safeRadius,
|
||||
startVector.perpendicular.scale(counter ? 1 : -1),
|
||||
endVector.perpendicular.scale(counter ? -1 : 1),
|
||||
counter,
|
||||
);
|
||||
|
||||
profile.segments.push(line);
|
||||
profile.segments.push(circle);
|
||||
|
||||
profile.arcLength += line.arcLength;
|
||||
profile.arcLength += circle.arcLength;
|
||||
|
||||
last = center.add(endVector.scale(pointOffsetDistance));
|
||||
}
|
||||
|
||||
const line = new LineSegment(last, points[points.length - 1]);
|
||||
profile.segments.push(line);
|
||||
profile.arcLength += line.arcLength;
|
||||
return profile;
|
||||
}
|
||||
8
packages/2d/src/curves/index.ts
Normal file
8
packages/2d/src/curves/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './CircleSegment';
|
||||
export * from './CurveDrawingInfo';
|
||||
export * from './CurvePoint';
|
||||
export * from './CurveProfile';
|
||||
export * from './getPointAtDistance';
|
||||
export * from './getPolylineProfile';
|
||||
export * from './LineSegment';
|
||||
export * from './Segment';
|
||||
@@ -246,9 +246,9 @@ export function property<T>(): PropertyDecorator {
|
||||
context.defaults[key] ?? meta.default,
|
||||
meta.interpolationFunction ?? deepLerp,
|
||||
meta.parser,
|
||||
target[`get${capitalize(<string>key)}`],
|
||||
target[`set${capitalize(<string>key)}`],
|
||||
target[`tween${capitalize(<string>key)}`],
|
||||
instance[`get${capitalize(<string>key)}`],
|
||||
instance[`set${capitalize(<string>key)}`],
|
||||
instance[`tween${capitalize(<string>key)}`],
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -134,3 +134,21 @@ export function drawLine(
|
||||
lineTo(context, point);
|
||||
}
|
||||
}
|
||||
|
||||
export function arc(
|
||||
context: CanvasRenderingContext2D | Path2D,
|
||||
center: Vector2,
|
||||
radius: number,
|
||||
startAngle = 0,
|
||||
endAngle = Math.PI * 2,
|
||||
counterclockwise = false,
|
||||
) {
|
||||
context.arc(
|
||||
center.x,
|
||||
center.y,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
counterclockwise,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,6 +123,11 @@ export class Rect implements Type {
|
||||
return new Vector2(this.x, this.y);
|
||||
}
|
||||
|
||||
public set position(value: Vector2) {
|
||||
this.x = value.x;
|
||||
this.y = value.y;
|
||||
}
|
||||
|
||||
public get size() {
|
||||
return new Vector2(this.width, this.height);
|
||||
}
|
||||
@@ -153,7 +158,7 @@ export class Rect implements Type {
|
||||
}
|
||||
|
||||
public set top(value: number) {
|
||||
this.width += this.y - value;
|
||||
this.height += this.y - value;
|
||||
this.y = value;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user