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 ( {/* Outlines */} {/* Box Outline */} {/* Editor */} {/* Bottom left */} { setC1Down(true); }} onPointerMove={e => { e.stopPropagation; }} 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" /> { e.stopPropagation(); setC1Down(true); }} onPointerUp={() => { setC1Down(false); }} /> {/* Top right */} { 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" /> { e.stopPropagation(); setC2Down(true); }} onPointerUp={() => { setC2Down(false); }} /> ); } export default CubicBezierEditor;