Last active 2 years ago

bezier.ts Raw
1import { vec2 } from 'gl-matrix';
2import './index.scss';
3
4const canvas = document.createElement('canvas');
5const ctx = canvas.getContext('2d')!;
6
7interface Bezier {
8 start: vec2;
9 controlS: vec2;
10 controlE: vec2;
11 end: vec2;
12}
13
14interface DrawBezier extends Bezier {
15 width: number;
16 color: string;
17}
18
19const cursor: vec2 = [0, 0];
20let movingPoint: vec2 | undefined;
21let currentCurve: Bezier | undefined;
22let currentPointIndex: number | undefined;
23let handleOffset: vec2 | undefined;
24let pointOffsets: vec2[] = [];
25let shiftDown = false;
26
27const drawCirclei = (x: number, y: number, radius: number) => {
28 ctx.beginPath();
29 ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
30 ctx.fill();
31 ctx.stroke();
32};
33const drawCirclep = (point: vec2, radius: number) =>
34 drawCirclei(point[0], point[1], radius);
35
36const drawBezier = (bezier: DrawBezier) => {
37 ctx.strokeStyle = bezier.color;
38 ctx.lineWidth = bezier.width;
39 ctx.beginPath();
40 ctx.moveTo(bezier.start[0], bezier.start[1]);
41 ctx.bezierCurveTo(
42 bezier.controlS[0],
43 bezier.controlS[1],
44 bezier.controlE[0],
45 bezier.controlE[1],
46 bezier.end[0],
47 bezier.end[1]
48 );
49 ctx.stroke();
50};
51
52const inCircle = (point: vec2, circle: vec2, radius: number) =>
53 vec2.dist(point, circle) < radius;
54
55const drawHandle = (point: vec2, radius: number) => {
56 ctx.fillStyle = inCircle(cursor, point, radius + 1)
57 ? 'rgba(0, 0, 0, 0.15)'
58 : 'transparent';
59 ctx.strokeStyle = '#000';
60 ctx.lineWidth = 1;
61 drawCirclep(point, radius);
62};
63
64const lines: DrawBezier[] = [
65 {
66 start: [100, 100],
67 controlS: [250, 250],
68 controlE: [500, 250],
69 end: [650, 100],
70 width: 15,
71 color: '#ff11aa',
72 },
73];
74
75const getControlledBezier = () => {
76 let bezier: Bezier | undefined;
77 let point: vec2 | undefined;
78 let pindex: number | undefined;
79
80 for (const curve of lines) {
81 if (inCircle(cursor, curve.start, curve.width + 5 + 1)) {
82 bezier = curve;
83 point = curve.start;
84 pindex = 0;
85 }
86
87 if (inCircle(cursor, curve.end, curve.width + 5 + 1)) {
88 bezier = curve;
89 point = curve.end;
90 pindex = 3;
91 }
92
93 if (inCircle(cursor, curve.controlS, 10 + 1)) {
94 bezier = curve;
95 point = curve.controlS;
96 pindex = 1;
97 }
98
99 if (inCircle(cursor, curve.controlE, 10 + 1)) {
100 bezier = curve;
101 point = curve.controlE;
102 pindex = 2;
103 }
104 }
105
106 return { bezier, point, pindex };
107};
108
109const getHandleOffset = () => {
110 if (!currentCurve || !movingPoint) {
111 return;
112 }
113
114 if (currentPointIndex === 1) {
115 handleOffset = vec2.sub(
116 vec2.create(),
117 currentCurve.start,
118 currentCurve.controlS
119 );
120 } else if (currentPointIndex === 2) {
121 handleOffset = vec2.sub(
122 vec2.create(),
123 currentCurve.end,
124 currentCurve.controlE
125 );
126 } else {
127 handleOffset = undefined;
128
129 if (currentPointIndex === 0 || currentPointIndex === 3) {
130 pointOffsets = [
131 vec2.sub(vec2.create(), currentCurve.start, movingPoint),
132 vec2.sub(vec2.create(), currentCurve.controlS, movingPoint),
133 vec2.sub(vec2.create(), currentCurve.controlE, movingPoint),
134 vec2.sub(vec2.create(), currentCurve.end, movingPoint),
135 ];
136 }
137 }
138};
139
140const relativeMove = () => {
141 if (!currentCurve || !movingPoint) {
142 return;
143 }
144
145 if (currentPointIndex === 1) {
146 vec2.add(currentCurve.start, movingPoint, handleOffset!);
147 } else if (currentPointIndex === 2) {
148 vec2.add(currentCurve.end, movingPoint, handleOffset!);
149 } else {
150 vec2.add(currentCurve.start, pointOffsets[0], movingPoint);
151 vec2.add(currentCurve.controlS, pointOffsets[1], movingPoint);
152 vec2.add(currentCurve.controlE, pointOffsets[2], movingPoint);
153 vec2.add(currentCurve.end, pointOffsets[3], movingPoint);
154 }
155};
156
157function resize() {
158 canvas.width = window.innerWidth;
159 canvas.height = window.innerHeight;
160}
161
162function draw() {
163 ctx.clearRect(0, 0, canvas.width, canvas.height);
164 ctx.lineCap = 'round';
165 lines.forEach((item) => {
166 drawBezier(item);
167 drawHandle(item.start, item.width + 5);
168 drawHandle(item.end, item.width + 5);
169 drawHandle(item.controlS, 10);
170 drawHandle(item.controlE, 10);
171 });
172}
173
174function loop() {
175 requestAnimationFrame(loop);
176 draw();
177}
178
179canvas.addEventListener('mousemove', (ev) => {
180 cursor[0] = ev.clientX;
181 cursor[1] = ev.clientY;
182
183 if (movingPoint) {
184 if (shiftDown && currentCurve) {
185 getHandleOffset();
186 }
187
188 vec2.copy(movingPoint, cursor);
189
190 if (shiftDown && currentCurve) {
191 relativeMove();
192 }
193 }
194});
195
196canvas.addEventListener('mousedown', (ev) => {
197 const { point, bezier, pindex } = getControlledBezier();
198 if (point && bezier) {
199 movingPoint = point;
200 currentCurve = bezier;
201 currentPointIndex = pindex;
202 }
203});
204
205canvas.addEventListener('mouseup', () => {
206 movingPoint = undefined;
207});
208
209window.addEventListener('keydown', (ev) => {
210 if (ev.key === 'Shift') {
211 shiftDown = true;
212 }
213});
214
215window.addEventListener('keyup', (ev) => {
216 if (ev.key === 'Shift') {
217 shiftDown = false;
218 }
219});
220resize();
221window.addEventListener('resize', resize);
222document.body.appendChild(canvas);
223loop();
224