aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/trails/CubicBezierEditor.tsx
diff options
context:
space:
mode:
authorNathan-SR <144961007+Nathan-SR@users.noreply.github.com>2024-06-03 13:33:37 -0400
committerNathan-SR <144961007+Nathan-SR@users.noreply.github.com>2024-06-03 13:33:37 -0400
commit9e77f980e7704999ef0a1c1845d660bccb13ff8a (patch)
tree14ca0da5915e4382a7bcb15f7d0b241941c8291f /src/client/views/nodes/trails/CubicBezierEditor.tsx
parent1be63695875c9242fba43d580465e8765cf3991d (diff)
parent202e994515392892676f8f080852db1e32b8dbd3 (diff)
Merge branch 'master' into nathan-starter
Diffstat (limited to 'src/client/views/nodes/trails/CubicBezierEditor.tsx')
-rw-r--r--src/client/views/nodes/trails/CubicBezierEditor.tsx202
1 files changed, 202 insertions, 0 deletions
diff --git a/src/client/views/nodes/trails/CubicBezierEditor.tsx b/src/client/views/nodes/trails/CubicBezierEditor.tsx
new file mode 100644
index 000000000..e1ad1e6e5
--- /dev/null
+++ b/src/client/views/nodes/trails/CubicBezierEditor.tsx
@@ -0,0 +1,202 @@
+import React, { useEffect, useState } from 'react';
+
+type Props = {
+ setFunc: (newPoints: { p1: number[]; p2: number[] }) => void;
+ currPoints: { p1: number[]; p2: number[] };
+};
+
+const ANIMATION_DURATION = 750;
+
+const CONTAINER_WIDTH = 200;
+const EDITOR_WIDTH = 100;
+const OFFSET = (CONTAINER_WIDTH - EDITOR_WIDTH) / 2;
+
+export const TIMING_DEFAULT_MAPPINGS = {
+ ease: 'cubic-bezier(0.25, 0.1, 0.25, 1.0)',
+ linear: 'cubic-bezier(0.0, 0.0, 1.0, 1.0)',
+ 'ease-in': 'cubic-bezier(0.42, 0, 1.0, 1.0)',
+ 'ease-out': 'cubic-bezier(0, 0, 0.58, 1.0)',
+ 'ease-in-out': 'cubic-bezier(0.42, 0, 0.58, 1.0)',
+};
+
+export function EaseFuncToPoints(func: string) {
+ let strPoints = func || 'ease';
+ if (!strPoints.startsWith('cubic')) {
+ switch (func) {
+ case 'linear':
+ strPoints = 'cubic-bezier(0.0, 0.0, 1.0, 1.0)';
+ break;
+ case 'ease':
+ strPoints = 'cubic-bezier(0.25, 0.1, 0.25, 1.0)';
+ break;
+ case 'ease-in':
+ strPoints = 'cubic-bezier(0.42, 0, 1.0, 1.0)';
+ break;
+ case 'ease-out':
+ strPoints = 'cubic-bezier(0, 0, 0.58, 1.0)';
+ break;
+ case 'ease-in-out':
+ strPoints = 'cubic-bezier(0.42, 0, 0.58, 1.0)';
+ break;
+ default:
+ strPoints = 'cubic-bezier(0.25, 0.1, 0.25, 1.0)';
+ }
+ }
+ const components = strPoints
+ .split('(')[1]
+ .split(')')[0]
+ .split(',')
+ .map(elem => parseFloat(elem));
+ return {
+ p1: [components[0], components[1]],
+ p2: [components[2], components[3]],
+ };
+}
+
+/**
+ * Visual editor for a bezier curve with draggable control points.
+ * */
+
+function CubicBezierEditor({ setFunc, currPoints }: Props) {
+ const [animating, setAnimating] = useState(false);
+ const [c1Down, setC1Down] = useState(false);
+ const [c2Down, setC2Down] = useState(false);
+
+ const roundToHundredth = (num: number) => Math.round(num * 100) / 100;
+
+ useEffect(() => {
+ if (animating) {
+ setTimeout(() => {
+ setAnimating(false);
+ }, ANIMATION_DURATION * 2);
+ }
+ }, [animating]);
+
+ useEffect(() => {
+ if (!c1Down) return undefined;
+ window.addEventListener('pointerup', () => {
+ setC1Down(false);
+ });
+ const handlePointerMove = (e: PointerEvent) => {
+ const newX = currPoints.p1[0] + e.movementX / EDITOR_WIDTH;
+ if (newX < 0 || newX > 1) {
+ return;
+ }
+
+ setFunc({
+ ...currPoints,
+ p1: [roundToHundredth(currPoints.p1[0] + e.movementX / EDITOR_WIDTH), roundToHundredth(currPoints.p1[1] - e.movementY / EDITOR_WIDTH)],
+ });
+ };
+
+ window.addEventListener('pointermove', handlePointerMove);
+
+ return () => window.removeEventListener('pointermove', handlePointerMove);
+ }, [c1Down, currPoints]);
+
+ // Sets up pointer events for moving the control points
+ useEffect(() => {
+ if (!c2Down) return undefined;
+ window.addEventListener('pointerup', () => {
+ setC2Down(false);
+ });
+ const handlePointerMove = (e: PointerEvent) => {
+ const newX = currPoints.p2[0] + e.movementX / EDITOR_WIDTH;
+ if (newX < 0 || newX > 1) {
+ return;
+ }
+
+ setFunc({
+ ...currPoints,
+ p2: [roundToHundredth(currPoints.p2[0] + e.movementX / EDITOR_WIDTH), roundToHundredth(currPoints.p2[1] - e.movementY / EDITOR_WIDTH)],
+ });
+ };
+
+ window.addEventListener('pointermove', handlePointerMove);
+
+ return () => window.removeEventListener('pointermove', handlePointerMove);
+ }, [c2Down, currPoints]);
+
+ return (
+ <div
+ onPointerMove={e => {
+ e.stopPropagation;
+ }}>
+ <svg className="presBox-bezier-editor" width={`${CONTAINER_WIDTH}`} height={`${CONTAINER_WIDTH}`} xmlns="http://www.w3.org/2000/svg">
+ {/* Outlines */}
+ <line x1={`${0 + OFFSET}`} y1={`${EDITOR_WIDTH + OFFSET}`} x2={`${EDITOR_WIDTH + OFFSET}`} y2={`${0 + OFFSET}`} stroke="#c1c1c1" strokeWidth="1" />
+ {/* Box Outline */}
+ <rect x={`${0 + OFFSET}`} y={`${0 + OFFSET}`} width={EDITOR_WIDTH} height={EDITOR_WIDTH} stroke="#c5c5c5" fill="transparent" strokeWidth="1" />
+ {/* Editor */}
+ <path
+ d={`M ${0 + OFFSET} ${EDITOR_WIDTH + OFFSET} C ${currPoints.p1[0] * EDITOR_WIDTH + OFFSET} ${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}, ${
+ currPoints.p2[0] * EDITOR_WIDTH + OFFSET
+ } ${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}, ${EDITOR_WIDTH + OFFSET} ${0 + OFFSET}`}
+ stroke="#ffffff"
+ fill="transparent"
+ />
+ {/* Bottom left */}
+ <line
+ onPointerDown={() => {
+ setC1Down(true);
+ }}
+ onPointerUp={() => {
+ setC1Down(false);
+ }}
+ x1={`${0 + OFFSET}`}
+ y1={`${EDITOR_WIDTH + OFFSET}`}
+ x2={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`}
+ y2={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`}
+ stroke="#00000000"
+ strokeWidth="5"
+ />
+ <line x1={`${0 + OFFSET}`} y1={`${EDITOR_WIDTH + OFFSET}`} x2={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`} stroke="#ffffff" strokeWidth="1" />
+ <circle
+ cx={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`}
+ cy={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`}
+ r="5"
+ fill={`${c1Down ? '#3fa9ff' : '#ffffff'}`}
+ onPointerDown={e => {
+ e.stopPropagation();
+ setC1Down(true);
+ }}
+ onPointerUp={() => {
+ setC1Down(false);
+ }}
+ />
+ {/* Top right */}
+ <line
+ onPointerDown={e => {
+ e.stopPropagation();
+ setC2Down(true);
+ }}
+ onPointerUp={() => {
+ setC2Down(false);
+ }}
+ x1={`${EDITOR_WIDTH + OFFSET}`}
+ y1={`${0 + OFFSET}`}
+ x2={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`}
+ y2={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`}
+ stroke="#00000000"
+ strokeWidth="5"
+ />
+ <line x1={`${EDITOR_WIDTH + OFFSET}`} y1={`${0 + OFFSET}`} x2={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`} stroke="#ffffff" strokeWidth="1" />
+ <circle
+ cx={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`}
+ cy={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`}
+ r="5"
+ fill={`${c2Down ? '#3fa9ff' : '#ffffff'}`}
+ onPointerDown={e => {
+ e.stopPropagation();
+ setC2Down(true);
+ }}
+ onPointerUp={() => {
+ setC2Down(false);
+ }}
+ />
+ </svg>
+ </div>
+ );
+}
+
+export default CubicBezierEditor;