aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package.json2
-rw-r--r--pages/editor.tsx56
-rw-r--r--styles/editor.css10
-rw-r--r--yarn.lock102
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 {
diff --git a/yarn.lock b/yarn.lock
index c2171ab..de857ca 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"