Skip to content

Instantly share code, notes, and snippets.

@Julien-Marcou
Created July 2, 2022 12:22
Show Gist options
  • Save Julien-Marcou/3182b1b4e75dba13be8447ca48644409 to your computer and use it in GitHub Desktop.
Save Julien-Marcou/3182b1b4e75dba13be8447ca48644409 to your computer and use it in GitHub Desktop.
Bezier Curve Simulator
import { Component, OnInit } from '@angular/core';
const FULL_CIRCLE = Math.PI * 2;
type BezierCoefficients = {
c1: number;
c2: number;
c3: number;
c4: number;
};
type Point = {
x: number;
y: number;
};
type Vector = {
dx: number;
dy: number;
};
type Tangent = {
point: Point;
vector: Vector;
};
type Drag<T> = {
element: T;
elementOrigin: Point;
dragOrigin: Point;
};
type Circle = Point & {
radius: number;
};
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
private readonly width = 600;
private readonly height = 600;
protected tangentCount = 19;
protected displayTangentPoint = true;
protected tangentPointRadius = 4;
protected displayTangentVectors = true;
protected tangentVectorLength = 100;
private readonly startPoint: Point;
private readonly endPoint: Point;
private readonly startControl: Circle;
private readonly endControl: Circle;
private canvas!: HTMLCanvasElement;
private context!: CanvasRenderingContext2D;
private canvasOffset!: Point;
private hoveredControlPoint?: Circle;
private drag?: Drag<Circle>;
constructor() {
this.startPoint = {x: 0, y: this.height};
this.endPoint = {x: this.width, y: 0};
this.startControl = {x: 100, y: 100, radius: 6};
this.endControl = {x: 500, y: 500, radius: 6};
}
public ngOnInit(): void {
this.canvas = document.getElementById('canvas') as HTMLCanvasElement;
this.canvas.width = this.width;
this.canvas.height = this.height;
const context = this.canvas.getContext('2d');
if (!context) {
throw new Error('Unable to retrieve context 2D');
}
this.context = context;
this.canvasOffset = {
x: this.canvas.offsetLeft,
y: this.canvas.offsetTop,
};
this.initEvents();
this.draw();
}
private initEvents(): void {
this.canvas.addEventListener('pointerdown', (event) => {
this.pointerDown(event);
});
this.canvas.addEventListener('pointermove', (event) => {
this.pointerMove(event);
});
this.canvas.addEventListener('pointerup', (event) => {
this.pointerUp(event);
});
this.canvas.addEventListener('pointerout', () => {
this.pointerOut();
});
}
protected draw(): void {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.context.strokeStyle = '#000';
this.context.fillStyle = '#ccc';
this.context.lineWidth = 1;
// Tangent vectors
if (this.displayTangentVectors) {
this.context.save();
this.context.strokeStyle = '#f00';
for (let i = 0; i <= 1; i += 1 / this.tangentCount) {
const tangent = this.getBezierTangent(i, this.startPoint, this.startControl, this.endControl, this.endPoint);
const tangentFactor = this.tangentVectorLength / Math.hypot(tangent.vector.dx, tangent.vector.dy);
[1, -1].forEach((factor) => {
this.context.beginPath();
this.context.moveTo(tangent.point.x, tangent.point.y);
this.context.lineTo(
tangent.point.x + tangent.vector.dx * factor * tangentFactor,
tangent.point.y + tangent.vector.dy * factor * tangentFactor,
);
this.context.stroke();
});
}
this.context.restore();
}
// Bezier curves
this.context.beginPath();
this.context.moveTo(
this.startPoint.x,
this.startPoint.y,
);
this.context.bezierCurveTo(
this.startControl.x,
this.startControl.y,
this.endControl.x,
this.endControl.y,
600,
0,
);
this.context.stroke();
// Tangent intersections
if (this.displayTangentPoint) {
for (let i = 0; i <= 1; i += 1 / this.tangentCount) {
const tangent = this.getBezierTangent(i, this.startPoint, this.startControl, this.endControl, this.endPoint);
this.context.beginPath();
this.context.arc(tangent.point.x, tangent.point.y, this.tangentPointRadius, 0, FULL_CIRCLE);
this.context.closePath();
this.context.fill();
this.context.stroke();
}
}
// Control lines
this.context.save();
this.context.strokeStyle = '#000';
this.context.setLineDash([10, 6]);
this.context.beginPath();
this.context.moveTo(
this.startControl.x,
this.startControl.y,
);
this.context.lineTo(
this.startPoint.x,
this.startPoint.y,
);
this.context.stroke();
this.context.beginPath();
this.context.moveTo(
this.endControl.x,
this.endControl.y,
);
this.context.lineTo(
this.endPoint.x,
this.endPoint.y,
);
this.context.stroke();
this.context.restore();
// Control points
[this.startControl, this.endControl].forEach((controlPoint) => {
this.context.beginPath();
this.context.arc(controlPoint.x, controlPoint.y, controlPoint.radius, 0, FULL_CIRCLE);
this.context.closePath();
this.context.fill();
this.context.stroke();
});
}
private pointerDown(event: PointerEvent): void {
this.canvas.setPointerCapture(event.pointerId);
if (this.hoveredControlPoint) {
const pointerPosition = this.getPointerPositionAt(event);
this.drag = {
element: this.hoveredControlPoint,
elementOrigin: {
x: this.hoveredControlPoint.x,
y: this.hoveredControlPoint.y,
},
dragOrigin: pointerPosition,
};
this.hoveredControlPoint = undefined;
this.canvas.classList.remove('draggable');
this.canvas.classList.add('dragging');
}
}
private pointerMove(event: PointerEvent): void {
const pointerPosition = this.getPointerPositionAt(event);
if (this.drag) {
const dragVector: Vector = {
dx: pointerPosition.x - this.drag.dragOrigin.x,
dy: pointerPosition.y - this.drag.dragOrigin.y,
};
this.drag.element.x = this.drag.elementOrigin.x + dragVector.dx;
this.drag.element.y = this.drag.elementOrigin.y + dragVector.dy;
this.draw();
}
else {
this.hoveredControlPoint = this.getControlPointAt(pointerPosition);
this.canvas.classList.toggle('draggable', this.hoveredControlPoint !== undefined);
}
}
private pointerUp(event: PointerEvent): void {
this.canvas.releasePointerCapture(event.pointerId);
if (this.drag) {
this.drag = undefined;
this.canvas.classList.remove('dragging');
}
}
private pointerOut(): void {
if (this.hoveredControlPoint) {
this.hoveredControlPoint = undefined;
this.canvas.classList.remove('draggable');
}
}
private getControlPointAt(point: Point): Circle | undefined {
return [this.startControl, this.endControl].find((controlPoint) => {
return Math.hypot(point.x - controlPoint.x, point.y - controlPoint.y) <= controlPoint.radius;
});
}
private getPointerPositionAt(event: PointerEvent): Point {
return {
x: event.pageX - this.canvasOffset.x,
y: event.pageY - this.canvasOffset.y,
};
}
private getBezierTangent(distance: number, startPoint: Point, startControlPoint: Point, endControlPoint: Point, endPoint: Point): Tangent {
const [x, dx] = this.getBezierTangentPart(distance, startPoint.x, startControlPoint.x, endControlPoint.x, endPoint.x);
const [y, dy] = this.getBezierTangentPart(distance, startPoint.y, startControlPoint.y, endControlPoint.y, endPoint.y);
return {
point: {x: x, y: y},
vector: {dx: dx, dy: dy},
};
}
private getBezierTangentPart(distance: number, start: number, startControl: number, endControl: number, end: number): [number, number] {
const coefficients = this.getBezierCoefficients(start, startControl, endControl, end);
return [
(coefficients.c1 * distance ** 3) + (coefficients.c2 * distance ** 2) + (coefficients.c3 * distance) + coefficients.c4,
(3 * coefficients.c1 * distance ** 2) + (2 * coefficients.c2 * distance) + coefficients.c3,
];
}
private getBezierCoefficients(start: number, startControl: number, endControl: number, end: number): BezierCoefficients {
return {
c1: end - (3 * endControl) + (3 * startControl) - start,
c2: (3 * endControl) - (6 * startControl) + (3 * start),
c3: (3 * startControl) - (3 * start),
c4: start,
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment