bezier.ts
· 5.0 KiB · TypeScript
Raw
import { vec2 } from 'gl-matrix';
import './index.scss';
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
interface Bezier {
start: vec2;
controlS: vec2;
controlE: vec2;
end: vec2;
}
interface DrawBezier extends Bezier {
width: number;
color: string;
}
const cursor: vec2 = [0, 0];
let movingPoint: vec2 | undefined;
let currentCurve: Bezier | undefined;
let currentPointIndex: number | undefined;
let handleOffset: vec2 | undefined;
let pointOffsets: vec2[] = [];
let shiftDown = false;
const drawCirclei = (x: number, y: number, radius: number) => {
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
ctx.fill();
ctx.stroke();
};
const drawCirclep = (point: vec2, radius: number) =>
drawCirclei(point[0], point[1], radius);
const drawBezier = (bezier: DrawBezier) => {
ctx.strokeStyle = bezier.color;
ctx.lineWidth = bezier.width;
ctx.beginPath();
ctx.moveTo(bezier.start[0], bezier.start[1]);
ctx.bezierCurveTo(
bezier.controlS[0],
bezier.controlS[1],
bezier.controlE[0],
bezier.controlE[1],
bezier.end[0],
bezier.end[1]
);
ctx.stroke();
};
const inCircle = (point: vec2, circle: vec2, radius: number) =>
vec2.dist(point, circle) < radius;
const drawHandle = (point: vec2, radius: number) => {
ctx.fillStyle = inCircle(cursor, point, radius + 1)
? 'rgba(0, 0, 0, 0.15)'
: 'transparent';
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
drawCirclep(point, radius);
};
const lines: DrawBezier[] = [
{
start: [100, 100],
controlS: [250, 250],
controlE: [500, 250],
end: [650, 100],
width: 15,
color: '#ff11aa',
},
];
const getControlledBezier = () => {
let bezier: Bezier | undefined;
let point: vec2 | undefined;
let pindex: number | undefined;
for (const curve of lines) {
if (inCircle(cursor, curve.start, curve.width + 5 + 1)) {
bezier = curve;
point = curve.start;
pindex = 0;
}
if (inCircle(cursor, curve.end, curve.width + 5 + 1)) {
bezier = curve;
point = curve.end;
pindex = 3;
}
if (inCircle(cursor, curve.controlS, 10 + 1)) {
bezier = curve;
point = curve.controlS;
pindex = 1;
}
if (inCircle(cursor, curve.controlE, 10 + 1)) {
bezier = curve;
point = curve.controlE;
pindex = 2;
}
}
return { bezier, point, pindex };
};
const getHandleOffset = () => {
if (!currentCurve || !movingPoint) {
return;
}
if (currentPointIndex === 1) {
handleOffset = vec2.sub(
vec2.create(),
currentCurve.start,
currentCurve.controlS
);
} else if (currentPointIndex === 2) {
handleOffset = vec2.sub(
vec2.create(),
currentCurve.end,
currentCurve.controlE
);
} else {
handleOffset = undefined;
if (currentPointIndex === 0 || currentPointIndex === 3) {
pointOffsets = [
vec2.sub(vec2.create(), currentCurve.start, movingPoint),
vec2.sub(vec2.create(), currentCurve.controlS, movingPoint),
vec2.sub(vec2.create(), currentCurve.controlE, movingPoint),
vec2.sub(vec2.create(), currentCurve.end, movingPoint),
];
}
}
};
const relativeMove = () => {
if (!currentCurve || !movingPoint) {
return;
}
if (currentPointIndex === 1) {
vec2.add(currentCurve.start, movingPoint, handleOffset!);
} else if (currentPointIndex === 2) {
vec2.add(currentCurve.end, movingPoint, handleOffset!);
} else {
vec2.add(currentCurve.start, pointOffsets[0], movingPoint);
vec2.add(currentCurve.controlS, pointOffsets[1], movingPoint);
vec2.add(currentCurve.controlE, pointOffsets[2], movingPoint);
vec2.add(currentCurve.end, pointOffsets[3], movingPoint);
}
};
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.lineCap = 'round';
lines.forEach((item) => {
drawBezier(item);
drawHandle(item.start, item.width + 5);
drawHandle(item.end, item.width + 5);
drawHandle(item.controlS, 10);
drawHandle(item.controlE, 10);
});
}
function loop() {
requestAnimationFrame(loop);
draw();
}
canvas.addEventListener('mousemove', (ev) => {
cursor[0] = ev.clientX;
cursor[1] = ev.clientY;
if (movingPoint) {
if (shiftDown && currentCurve) {
getHandleOffset();
}
vec2.copy(movingPoint, cursor);
if (shiftDown && currentCurve) {
relativeMove();
}
}
});
canvas.addEventListener('mousedown', (ev) => {
const { point, bezier, pindex } = getControlledBezier();
if (point && bezier) {
movingPoint = point;
currentCurve = bezier;
currentPointIndex = pindex;
}
});
canvas.addEventListener('mouseup', () => {
movingPoint = undefined;
});
window.addEventListener('keydown', (ev) => {
if (ev.key === 'Shift') {
shiftDown = true;
}
});
window.addEventListener('keyup', (ev) => {
if (ev.key === 'Shift') {
shiftDown = false;
}
});
resize();
window.addEventListener('resize', resize);
document.body.appendChild(canvas);
loop();
| 1 | import { vec2 } from 'gl-matrix'; |
| 2 | import './index.scss'; |
| 3 | |
| 4 | const canvas = document.createElement('canvas'); |
| 5 | const ctx = canvas.getContext('2d')!; |
| 6 | |
| 7 | interface Bezier { |
| 8 | start: vec2; |
| 9 | controlS: vec2; |
| 10 | controlE: vec2; |
| 11 | end: vec2; |
| 12 | } |
| 13 | |
| 14 | interface DrawBezier extends Bezier { |
| 15 | width: number; |
| 16 | color: string; |
| 17 | } |
| 18 | |
| 19 | const cursor: vec2 = [0, 0]; |
| 20 | let movingPoint: vec2 | undefined; |
| 21 | let currentCurve: Bezier | undefined; |
| 22 | let currentPointIndex: number | undefined; |
| 23 | let handleOffset: vec2 | undefined; |
| 24 | let pointOffsets: vec2[] = []; |
| 25 | let shiftDown = false; |
| 26 | |
| 27 | const 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 | }; |
| 33 | const drawCirclep = (point: vec2, radius: number) => |
| 34 | drawCirclei(point[0], point[1], radius); |
| 35 | |
| 36 | const 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 | |
| 52 | const inCircle = (point: vec2, circle: vec2, radius: number) => |
| 53 | vec2.dist(point, circle) < radius; |
| 54 | |
| 55 | const 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 | |
| 64 | const 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 | |
| 75 | const 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 | |
| 109 | const 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 | |
| 140 | const 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 | |
| 157 | function resize() { |
| 158 | canvas.width = window.innerWidth; |
| 159 | canvas.height = window.innerHeight; |
| 160 | } |
| 161 | |
| 162 | function 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 | |
| 174 | function loop() { |
| 175 | requestAnimationFrame(loop); |
| 176 | draw(); |
| 177 | } |
| 178 | |
| 179 | canvas.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 | |
| 196 | canvas.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 | |
| 205 | canvas.addEventListener('mouseup', () => { |
| 206 | movingPoint = undefined; |
| 207 | }); |
| 208 | |
| 209 | window.addEventListener('keydown', (ev) => { |
| 210 | if (ev.key === 'Shift') { |
| 211 | shiftDown = true; |
| 212 | } |
| 213 | }); |
| 214 | |
| 215 | window.addEventListener('keyup', (ev) => { |
| 216 | if (ev.key === 'Shift') { |
| 217 | shiftDown = false; |
| 218 | } |
| 219 | }); |
| 220 | resize(); |
| 221 | window.addEventListener('resize', resize); |
| 222 | document.body.appendChild(canvas); |
| 223 | loop(); |
| 224 |