Created
July 2, 2022 12:22
-
-
Save Julien-Marcou/3182b1b4e75dba13be8447ca48644409 to your computer and use it in GitHub Desktop.
Bezier Curve Simulator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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