diff options
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | pages/editor.tsx | 56 | ||||
-rw-r--r-- | styles/editor.css | 10 | ||||
-rw-r--r-- | yarn.lock | 102 |
4 files changed, 160 insertions, 10 deletions
diff --git a/package.json b/package.json index b215add..7ada30e 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "next": "^10.2.0", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-spring": "^9.1.2", + "react-use-gesture": "^9.1.3", "timecode-boss": "^4.2.3", "ts-json-schema-generator": "^0.92.0", "zustand": "^3.5.1" diff --git a/pages/editor.tsx b/pages/editor.tsx index cb68601..56becaf 100644 --- a/pages/editor.tsx +++ b/pages/editor.tsx @@ -1,4 +1,6 @@ -import { CSSProperties, ReactNode, useEffect, useState } from 'react'; +import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react'; +import { animated, useSpring } from 'react-spring'; +import { useGesture } from 'react-use-gesture'; import create from 'zustand'; import { loopSlide } from '../timeline'; import { TimedVideoPlayer } from './present'; @@ -30,6 +32,13 @@ var getTimelineZoom = create(set => ({ var zoomToPx = (zoom: number) => (12 - 0.5) * zoom ** (1 / 0.4) + 0.5; +function getFrameAtOffset(offset: number, timelineZoom: number) { + var timeline = document.querySelector('.timeline .timelineInner'); + var currentOffset = timeline.scrollLeft; + var frame = (offset + currentOffset) / zoomToPx(timelineZoom); + return frame; +} + var useTimelineLabels = create(set => ({ labels: [], setLabels: (newLabels: Array<ReactNode>) => set(() => ({ labels: newLabels })), @@ -59,6 +68,8 @@ function TimelineEditor(props: { var frame = useFrame((st: any) => st.currentFrame); var setFrame = useFrame((st: any) => st.setFrame); + var timelineZoom = getTimelineZoom((st: any) => st.zoom); + useEffect(() => { var canvas = document.getElementById('timeScaleCanvas') as HTMLCanvasElement; var ctx = canvas.getContext('2d'); @@ -141,17 +152,39 @@ function TimelineEditor(props: { window.addEventListener('resize', onresize); }, []); + var scrubberRef = useRef(null); + var scrubberDragRef = useRef(null); + + var [scrubberPos, scrubberSpring] = useSpring( + () => ({ + x: 0, + config: { mass: 0.5, tension: 500, friction: 20 }, + }), + ); + + useGesture( + { + onDrag: ({ offset: [x, _y] }) => scrubberSpring.start({ x: Math.round(getFrameAtOffset(x, timelineZoom)) }), + }, + { domTarget: scrubberDragRef, eventOptions: { passive: false } }, + ); + return <> <canvas className='timeScale posabs a0' id='timeScaleCanvas' /> <div className='labels'>{timelineLabels}</div> <div className='timelineInner posabs a0'> - <div className='scrubber posabs v0' style={{ '--frame': frame.toString() } as CSSProperties}> + <animated.div + className='scrubber posabs v0' + style={{ '--frame': scrubberPos.x } as CSSProperties} + ref={scrubberRef} + > <svg width='20' height='28' viewBox='0 0 20 28' xmlns='http://www.w3.org/2000/svg' className='head posabs t0 abscenterh' + ref={scrubberDragRef} > <path d='M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V17.3431C20 18.404 19.5786 19.4214 18.8284 20.1716L11 28H9L1.17157 20.1716C0.421426 19.4214 0 18.404 0 17.3431V4Z' @@ -159,7 +192,7 @@ function TimelineEditor(props: { </svg> <div className='needle posabs a0' /> <div className='frameOverlay posabs v0' /> - </div> + </animated.div> <div className='keyframes' style={{ '--total-frames': props.player?.timeline?.framecount.toString() } as CSSProperties} @@ -173,7 +206,7 @@ function TimelineEditor(props: { export default function Index() { var [dummy, setDummy] = useState(false); var rerender = () => setDummy(!dummy); - var [player, setPlayer] = useState(new TimedVideoPlayer()); + var [player, _setPlayer] = useState(new TimedVideoPlayer()); var timelineZoom = getTimelineZoom((st: any) => st.zoom); var setTimelineZoom = getTimelineZoom((st: any) => st.setZoom); @@ -181,7 +214,7 @@ export default function Index() { var frame = useFrame((st: any) => st.currentFrame); var mouseX = 0; - var mouseY = 0; + // var mouseY = 0; useEffect(() => { var videoEL = document.getElementById('player') as HTMLVideoElement; @@ -190,8 +223,7 @@ export default function Index() { function zoomAroundPoint(newZoom: number, pivot: number) { var timeline = document.querySelector('.timeline .timelineInner'); - var currentOffset = timeline.scrollLeft; - var frame = (pivot + currentOffset) / zoomToPx(timelineZoom); + var frame = getFrameAtOffset(pivot, timelineZoom); var newOffset = (frame * zoomToPx(newZoom)) - pivot; timeline.scrollLeft = newOffset; @@ -214,10 +246,16 @@ export default function Index() { window.addEventListener('mousemove', e => { var rect = canvas.getBoundingClientRect(); mouseX = e.clientX - rect.x; - mouseY = e.clientY - rect.y; + // mouseY = e.clientY - rect.y; }); }, []); + useEffect(() => { + var preventDefault = (e: Event) => e.preventDefault(); + document.addEventListener('gesturestart', preventDefault); + document.addEventListener('gesturechange', preventDefault); + }, []); + return <> <div className='appGrid posabs a0'> <AppBar position='static' color='transparent' elevation={0}> @@ -320,7 +358,7 @@ export default function Index() { <div className='spacing'> <Slider value={timelineZoom} - onChange={(event: any, newValue: number | number[]) => { + onChange={(_event: any, newValue: number | number[]) => { var center = document.querySelector('.timeline .timelineInner').clientWidth / 2; zoomAroundPoint(newValue as number, center); }} diff --git a/styles/editor.css b/styles/editor.css index 8d02dff..c861ee0 100644 --- a/styles/editor.css +++ b/styles/editor.css @@ -221,7 +221,15 @@ opacity: .0; } -.timeline .scrubber .head { fill: var(--blue); } +.timeline .scrubber .head { + fill: var(--blue); + z-index: 1; + cursor: grab; +} +.timeline .scrubber .head:active { + cursor: grabbing; +} + .timeline .scrubber .needle { background-color: var(--blue); } .timeline .labels .label { @@ -196,6 +196,86 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.14.0.tgz#c67fc20a4d891447ca1a855d7d70fa79a3533001" integrity sha512-sDOAZcYwynHFTbLo6n8kIbLiVF3a3BLkrmehJUyEbT9F+Smbi47kLGS2gG2g0fjBLR/Lr1InPD7kXL7FaTqEkw== +"@react-spring/animated@~9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.1.2.tgz#e43b122160f8f4cbb0caac8a7f57acd76dd12369" + integrity sha512-nKOGk+3aWbNp46V/CB1J2vR3GJI/Vork8N1WTI5mt+32QekrSsBn5/YFt4/iPaDGhLjukFxF0IjLs6hRLqSObw== + dependencies: + "@react-spring/shared" "~9.1.2" + "@react-spring/types" "~9.1.2" + +"@react-spring/core@~9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.1.2.tgz#6d854a12fe9c3caa7942e51e708cb5fb4e2d1124" + integrity sha512-rgobYPCcLdDwbHBVqAmvtXhhX92G7MoPltJlzUge843yp1dNr47tkagFdCtw9NMGp6eHu/CE5byh/imlhLLAxw== + dependencies: + "@react-spring/animated" "~9.1.2" + "@react-spring/shared" "~9.1.2" + "@react-spring/types" "~9.1.2" + +"@react-spring/konva@~9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@react-spring/konva/-/konva-9.1.2.tgz#20567063efd8d441a268826e326bd5d7574bbc50" + integrity sha512-P60mhUHRYgPPhoTBQWzuzD3hfeCFWC0BQ7N0iHzpMTzDIrAvutyg+iAX59jSXo3yatrcx60NmlCsiG8tRxbw6w== + dependencies: + "@react-spring/animated" "~9.1.2" + "@react-spring/core" "~9.1.2" + "@react-spring/shared" "~9.1.2" + "@react-spring/types" "~9.1.2" + +"@react-spring/native@~9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@react-spring/native/-/native-9.1.2.tgz#d21a64c20ca08d2c5839cedcf9cc4842770f8ffc" + integrity sha512-d7+tCoKAnDPSoVtpyFFm4BWQhn1h833ocdP0d2POZzKTcR1iQ8YI7EQ22iKGLvwH+0vjymde039CgYy31INqWQ== + dependencies: + "@react-spring/animated" "~9.1.2" + "@react-spring/core" "~9.1.2" + "@react-spring/shared" "~9.1.2" + "@react-spring/types" "~9.1.2" + +"@react-spring/shared@~9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.1.2.tgz#c36d077d7eb31fd2cbcf8956d9d35037b2998613" + integrity sha512-sj/RrhFZAteCWAMk+W0t6Ku/skn/lbskCCs8B7ZnHNLMGPM+Zb3MOk+aVbX3T/D0iq/oTnKWyQYqrXDKiFcZ7g== + dependencies: + "@react-spring/types" "~9.1.2" + rafz "^0.1.14" + +"@react-spring/three@~9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@react-spring/three/-/three-9.1.2.tgz#49d1d4c0b9d059bd470712c78c9dd73af130677d" + integrity sha512-d/v94ykmfJGLTJxJ+jxlTAJSfFdD+SSf+yvXReS81hc7+9VYeEwIHVIEKOzckYnPy/MEOSVhIVKF/9wdFIIo6g== + dependencies: + "@react-spring/animated" "~9.1.2" + "@react-spring/core" "~9.1.2" + "@react-spring/shared" "~9.1.2" + "@react-spring/types" "~9.1.2" + +"@react-spring/types@~9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.1.2.tgz#3273a182f825b38f44ead2a2f3984344abad1e2b" + integrity sha512-NZNImL0ymRFbss1cGKX2qSEeFdFoOgnIJZEW4Uczt+wm04J7g0Zuf23Hf8hM35JtxDr8QO5okp8BBtCM5FzzMg== + +"@react-spring/web@~9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.1.2.tgz#6ec409e8559676834b67aa33f0a2d57643c3c555" + integrity sha512-E5W9Hmi2bO6CPorCNV/2iv12ux9LxHJAbpXmrBPKWFRqZixysiHoNQKKPG0DmSvUU1uKkvCvMC4VoB6pj/2kxw== + dependencies: + "@react-spring/animated" "~9.1.2" + "@react-spring/core" "~9.1.2" + "@react-spring/shared" "~9.1.2" + "@react-spring/types" "~9.1.2" + +"@react-spring/zdog@~9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@react-spring/zdog/-/zdog-9.1.2.tgz#edf270e93d5db8a94f65d4e94e4438352fbb454f" + integrity sha512-t5RobDp12HGVh6XJ1BZ+dFdxRQ/goEapYvjH5eqQa1vC97bSqJGLiG+SM/E360DtDlh8GXAyGSesd2pXzBkpPg== + dependencies: + "@react-spring/animated" "~9.1.2" + "@react-spring/core" "~9.1.2" + "@react-spring/shared" "~9.1.2" + "@react-spring/types" "~9.1.2" + "@types/json-schema@^7.0.7": version "7.0.7" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" @@ -1735,6 +1815,11 @@ querystring@^0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== +rafz@^0.1.14: + version "0.1.14" + resolved "https://registry.yarnpkg.com/rafz/-/rafz-0.1.14.tgz#164f01cf7cc6094e08467247ef351ef5c8d278fe" + integrity sha512-YiQkedSt1urYtYbvHhTQR3l67M8SZbUvga5eJFM/v4vx/GmDdtXlE2hjJIyRjhhO/PjcdGC+CXCYOUA4onit8w== + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -1784,6 +1869,18 @@ react-refresh@0.8.3: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg== +react-spring@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-9.1.2.tgz#a2392f5468bfd960976747d59361236536e1f303" + integrity sha512-xLmkierisElCQShCqAH3PpepjHhCyOK1wGSTdpvG7GGD+SbfG4Sac7wj6wrKTT5A5NUFM5OnVQUXZLe5HScIfA== + dependencies: + "@react-spring/core" "~9.1.2" + "@react-spring/konva" "~9.1.2" + "@react-spring/native" "~9.1.2" + "@react-spring/three" "~9.1.2" + "@react-spring/web" "~9.1.2" + "@react-spring/zdog" "~9.1.2" + react-transition-group@^4.4.0: version "4.4.1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" @@ -1794,6 +1891,11 @@ react-transition-group@^4.4.0: loose-envify "^1.4.0" prop-types "^15.6.2" +react-use-gesture@^9.1.3: + version "9.1.3" + resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-9.1.3.tgz#92bd143e4f58e69bd424514a5bfccba2a1d62ec0" + integrity sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg== + react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" |