aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package.json4
-rw-r--r--pages/editor.tsx208
-rw-r--r--styles/editor.css5
-rw-r--r--yarn.lock10
4 files changed, 114 insertions, 113 deletions
diff --git a/package.json b/package.json
index ea90dc2..0b8f060 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"license": "MIT",
"private": false,
"dependencies": {
+ "@hookstate/core": "^3.0.8",
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.58",
@@ -24,8 +25,7 @@
"react-use-gesture": "^9.1.3",
"timecode-boss": "^4.2.3",
"ts-json-schema-generator": "^0.92.0",
- "uuid": "^8.3.2",
- "zustand": "^3.5.1"
+ "uuid": "^8.3.2"
},
"devDependencies": {
"typescript": "^4.2.4"
diff --git a/pages/editor.tsx b/pages/editor.tsx
index c34d3bd..0695d5c 100644
--- a/pages/editor.tsx
+++ b/pages/editor.tsx
@@ -1,8 +1,8 @@
+import { createState, Downgraded, useState as useHookState } from '@hookstate/core';
import mousetrap from 'mousetrap';
import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react';
import { animated, useSpring } from 'react-spring';
import { useDrag } from 'react-use-gesture';
-import create from 'zustand';
import {
FullScreenControlsRoundedIcon,
@@ -47,26 +47,44 @@ var keyframeInAnimations: { [key: string]: { x: number; y: number; }; } = {};
var slideAPIs: { [key: string]: any; }[] = [];
var player = new TimedVideoPlayer();
-var useWorkingTimeline = create((set, get) => ({
- timeline: [],
- setTimeline: (newTimeline: anySlide[]) => set(() => ({ timeline: newTimeline })),
- refreshLiveTimeline: () => {
- player.timeline.slides = Array(...((get() as any).timeline));
- player.timeline.slides = player.timeline.slides.filter(slide => slide != null);
- player.timeline.slides.sort((a, b) => a.frame - b.frame);
- player.timeline.slides[-1] = { // TODO: dry
- id: '00000000-0000-0000-0000-000000000000',
- frame: 0,
- type: 'default',
- clickThroughBehaviour: 'ImmediatelySkip',
- };
- },
-}));
-var getTimelineZoom = create(set => ({
- zoom: 0.687077725615,
- setZoom: (newValue: number) => set(() => ({ zoom: newValue })),
-}));
+interface project {
+ timeline: {
+ playing: boolean;
+ frame: number;
+ labels: ReactNode[];
+ zoom: number;
+ workingTimeline: anySlide[];
+ tool: string;
+ };
+ update: {
+ refreshLiveTimeline: () => void;
+ };
+}
+
+var project = createState<project>({
+ timeline: {
+ playing: false,
+ frame: 0,
+ labels: [],
+ zoom: 0.687077725615,
+ workingTimeline: [],
+ tool: 'cursor',
+ },
+ update: {
+ refreshLiveTimeline: () => {
+ player.timeline.slides = Array(...(project.timeline.workingTimeline.value));
+ player.timeline.slides = player.timeline.slides.filter(slide => slide != null);
+ player.timeline.slides.sort((a, b) => a.frame - b.frame);
+ player.timeline.slides[-1] = { // TODO: dry
+ id: '00000000-0000-0000-0000-000000000000',
+ frame: 0,
+ type: 'default',
+ clickThroughBehaviour: 'ImmediatelySkip',
+ };
+ },
+ },
+});
var zoomToPx = (zoom: number) => (12 - 0.5) * zoom ** (1 / 0.4) + 0.5;
@@ -77,21 +95,6 @@ function getFrameAtOffset(offset: number, timelineZoom: number) {
return frame;
}
-var useTimelineLabels = create(set => ({
- labels: [],
- setLabels: (newLabels: Array<ReactNode>) => set(() => ({ labels: newLabels })),
-}));
-
-var useFrame = create(set => ({
- currentFrame: 0,
- setFrame: (newFrame: number) => set(() => ({ currentFrame: newFrame })),
-}));
-
-var usePlaying = create(set => ({
- playing: false,
- setPlaying: (playing: boolean) => set(() => ({ playing })),
-}));
-
function calculateSelectionOffsets(left: slideTypes, right: slideTypes) {
var offsets = {
default: { left: -6, right: 6 },
@@ -110,14 +113,14 @@ function calculateSelectionOffsets(left: slideTypes, right: slideTypes) {
function TimelineKeyframe(props: {
slide: slide;
}) {
- var workingTimeline = useWorkingTimeline((st: any) => st.timeline);
- var setWorkingTimeline = useWorkingTimeline((st: any) => st.setTimeline);
- var updateTimeline = useWorkingTimeline((st: any) => st.refreshLiveTimeline);
+ var workingTimeline = useHookState(project.timeline.workingTimeline);
+ var setWorkingTimeline = useHookState(project.timeline.workingTimeline).set;
+ var updateTimeline = useHookState(project.update.refreshLiveTimeline).value;
function modifySlide(newProps: Partial<anySlide>) {
- var slide = workingTimeline.find((s: anySlide) => s.id == props.slide.id);
+ var slide = workingTimeline.value.find(s => s.id == props.slide.id);
slide = Object.assign(slide, newProps);
- setWorkingTimeline(workingTimeline);
+ setWorkingTimeline(workingTimeline.value);
}
var dragRef = useRef(null);
@@ -146,13 +149,13 @@ function TimelineKeyframe(props: {
delete keyframeInAnimations[props.slide.id];
}, []);
- var timelineZoom = getTimelineZoom((st: any) => st.zoom);
+ var timelineZoom = useHookState(project.timeline.zoom);
// drag keyframe
var [startOffset, setStartOffset] = useState(0);
var [endOffset, setEndOffset] = useState(0);
useDrag(({ xy: [x, _y], first }) => {
- var frame = Math.max(0, Math.round(getFrameAtOffset(x - 240, timelineZoom)) - 1);
+ var frame = Math.max(0, Math.round(getFrameAtOffset(x - 240, timelineZoom.value)) - 1);
if (props.slide.type == 'loop') {
if (first) {
@@ -180,7 +183,7 @@ function TimelineKeyframe(props: {
if (props.slide.type == 'loop') {
// loop start
useDrag(({ xy: [x, _y] }) => {
- var frame = Math.max(0, Math.round(getFrameAtOffset(x - 240, timelineZoom)) - 1);
+ var frame = Math.max(0, Math.round(getFrameAtOffset(x - 240, timelineZoom.value)) - 1);
api.start({ begin: frame });
@@ -189,7 +192,7 @@ function TimelineKeyframe(props: {
// loop end
useDrag(({ xy: [x, _y] }) => {
- var frame = Math.max(0, Math.round(getFrameAtOffset(x - 240, timelineZoom)) - 1);
+ var frame = Math.max(0, Math.round(getFrameAtOffset(x - 240, timelineZoom.value)) - 1);
api.start({ frame });
@@ -233,30 +236,25 @@ function TimelineKeyframe(props: {
</animated.div>;
}
-function TimelineEditor(props: {
- player: TimedVideoPlayer;
- selectedTool: string;
-}) {
- var timelineZoom = getTimelineZoom((st: any) => st.zoom);
+function TimelineEditor() {
+ var timelineZoom = useHookState(project.timeline.zoom);
+ var timelineLabels = useHookState(project.timeline.labels);
+ var workingTimeline = useHookState(project.timeline.workingTimeline);
- var timelineLabels = useTimelineLabels((st: any) => st.labels);
- var setTimelineLabels = useTimelineLabels((st: any) => st.setLabels);
+ var refreshWorkingTimline = useHookState(project.update.refreshLiveTimeline).value;
+ var setFrame = useHookState(project.timeline.frame).set;
- var workingTimeline = useWorkingTimeline((st: any) => st.timeline);
- var setWorkingTimeline = useWorkingTimeline((st: any) => st.setTimeline);
- var refreshWorkingTimline = useWorkingTimeline((st: any) => st.refreshLiveTimeline);
-
- var setFrame = useFrame((st: any) => st.setFrame);
+ var tool = useHookState(project.timeline.tool);
useEffect(() => {
- props.player.addEventListener('TimedVideoPlayerOnFrame', (event: CustomEvent) => {
+ player.addEventListener('TimedVideoPlayerOnFrame', (event: CustomEvent) => {
setFrame(event.detail);
scrubberSpring.start({ frame: event.detail });
});
}, []);
useEffect(() => {
- props.player.addEventListener('TimedVideoPlayerSlide', (event: CustomEvent) => {
+ player.addEventListener('TimedVideoPlayerSlide', (event: CustomEvent) => {
document.querySelectorAll('.keyframes .frame').forEach(el => {
el.classList.remove('current');
if (event.detail && el.id == 'slide-' + (event.detail as slide).id) {
@@ -320,7 +318,7 @@ function TimelineEditor(props: {
left: Math.round(rect[0] + frameWidth / 2),
top: rect[1],
}}
- children={props.player.frameToTimestampString(frame - 1)}
+ children={player.frameToTimestampString(frame - 1)}
/>,
);
}
@@ -330,7 +328,7 @@ function TimelineEditor(props: {
a++;
}
- setTimelineLabels(labels);
+ project.timeline.labels.set(labels);
requestAnimationFrame(draw);
}
@@ -354,12 +352,11 @@ function TimelineEditor(props: {
}),
);
useDrag(({ xy: [x, _y] }) => {
- var frame = Math.max(0, Math.round(getFrameAtOffset(x - 240, timelineZoom)) - 1);
+ var frame = Math.max(0, Math.round(getFrameAtOffset(x - 240, timelineZoom.value)) - 1);
setFrame(frame);
scrubberSpring.start({ frame });
- if (props.player.player) {
- var player = props.player.player;
- player.currentTime = props.player.frameToTimestamp(frame + 1);
+ if (player.player) {
+ player.player.currentTime = player.frameToTimestamp(frame + 1);
}
}, { domTarget: scrubberDragRef, eventOptions: { passive: false } });
@@ -402,13 +399,13 @@ function TimelineEditor(props: {
useDrag(({ movement: [x, _y], last }) => {
if (!selectionPlaced) return;
if (selection.length < 1) return;
- var frameOffset = Math.round(x / zoomToPx(timelineZoom));
+ var frameOffset = Math.round(x / zoomToPx(timelineZoom.value));
selection.forEach((slide: anySlide) => {
var api = slideAPIs[slide.id];
switch (slide.type as slideTypes | 'loopBegin') {
case 'loopBegin': {
if (!api) break;
- var loop = workingTimeline.find((s: anySlide) => s.id == slide.id) as loopSlide;
+ var loop = workingTimeline.value.find(s => s.id == slide.id) as loopSlide;
var begin = loop.beginFrame + frameOffset;
api.start({ begin });
@@ -425,7 +422,8 @@ function TimelineEditor(props: {
api.start({ frame });
if (last) {
- workingTimeline.find((s: anySlide) => s.id == slide.id).frame = frame;
+ workingTimeline.value.find(s => s.id == slide.id).frame = frame;
+ project.timeline.workingTimeline.set(workingTimeline.value);
refreshWorkingTimline();
}
}
@@ -436,7 +434,7 @@ function TimelineEditor(props: {
});
}, { domTarget: selectionRef, eventOptions: { passive: false } });
useDrag(({ xy: [x, y], initial: [bx, by], first, last, movement: [ox, oy] }) => {
- if (props.selectedTool != 'cursor') return;
+ if (tool.value != 'cursor') return;
var minDistance = 5; // minimal drag distance in pixels to register selection
var distanceTraveled = Math.sqrt(ox ** 2 + oy ** 2);
@@ -467,7 +465,7 @@ function TimelineEditor(props: {
var x2 = x1 + Math.abs(sx);
var y2 = y1 + Math.abs(sy);
- var zoom = zoomToPx(timelineZoom);
+ var zoom = zoomToPx(timelineZoom.value);
var frameWidth = Math.abs(sx) / zoom;
var startingFrame = x1 / zoom;
@@ -528,9 +526,10 @@ function TimelineEditor(props: {
selection.forEach((slide: anySlide) => {
if (!slideTypes.includes(slide.type)) return;
- var index = workingTimeline.findIndex((s: anySlide) => s?.id == slide.id);
+ var index = workingTimeline.value.findIndex(s => s?.id == slide.id);
if (index == -1) return;
- delete workingTimeline[index];
+ delete workingTimeline.value[index];
+ // !!! AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
});
refreshWorkingTimline();
@@ -552,10 +551,10 @@ function TimelineEditor(props: {
// place new keyframe
var offset = -4; // keyframe offset
var x = event.clientX - 240 + offset;
- var frame = getFrameAtOffset(x, timelineZoom) - 0.5;
- var slide = new toolToSlide[props.selectedTool](Math.round(frame));
- workingTimeline.push(slide);
- setWorkingTimeline(workingTimeline);
+ var frame = getFrameAtOffset(x, timelineZoom.value) - 0.5;
+ var slide = new toolToSlide[tool.value](Math.round(frame));
+ workingTimeline.value.push(slide);
+ workingTimeline.set(workingTimeline.value);
keyframeInAnimations[slide.id] = {
x: frame,
y: event.clientY - window.innerHeight + 210,
@@ -563,9 +562,9 @@ function TimelineEditor(props: {
refreshWorkingTimline();
}}
/>
- <div className='labels' children={timelineLabels} />
+ <div className='labels' children={timelineLabels.attach(Downgraded).get()} />
<div className='scrubberJumpArea posabs h0 t0' ref={scrubberDragRef} />
- <div className={'timelineInner posabs a0' + (props.selectedTool != 'cursor' ? ' blur' : '')}>
+ <div className={'timelineInner posabs a0' + (tool.value != 'cursor' ? ' blur' : '')}>
<animated.div
className='scrubber posabs v0'
style={{ '--frame': scrubberPos.frame } as CSSProperties}
@@ -586,10 +585,10 @@ function TimelineEditor(props: {
</animated.div>
<div
className='keyframes'
- style={{ '--total-frames': props.player?.timeline?.framecount.toString() } as CSSProperties}
+ style={{ '--total-frames': player.timeline?.framecount.toString() } as CSSProperties}
>
<div className='selectionarea posabs v0' ref={selectionAreaRef} />
- {workingTimeline.map((slide: anySlide) => <TimelineKeyframe slide={slide} />)}
+ {workingTimeline.value.map(slide => <TimelineKeyframe slide={slide} />)}
<div
id='selection'
className={'posabs dispinbl ' + (selectionPlaced ? 'placed ' : '')}
@@ -612,7 +611,7 @@ function TimelineEditor(props: {
/>
</div>
</div>
- <div className={'ghostArea posabs a0' + (props.selectedTool != 'cursor' ? ' active' : '')}>
+ <div className={'ghostArea posabs a0' + (tool.value != 'cursor' ? ' active' : '')}>
<animated.div
id='ghost'
className='posabs dispinbl'
@@ -620,7 +619,7 @@ function TimelineEditor(props: {
top: ghost.y,
left: ghost.x,
}}
- children={<SlideKeyframe type={props.selectedTool as slideTypes} ghost />}
+ children={<SlideKeyframe type={tool.value as slideTypes} ghost />}
/>
</div>
</>;
@@ -628,8 +627,10 @@ function TimelineEditor(props: {
// https://material.io/design/navigation/navigation-transitions.html#peer-transitions
function DefaultSettings() {
- var setPlaying = usePlaying((st: any) => st.setPlaying);
- var setWorkingTimeline = useWorkingTimeline((st: any) => st.setTimeline);
+ var setPlaying = useHookState(project.timeline.playing).set;
+
+ var setWorkingTimeline = useHookState(project.timeline.workingTimeline).set;
+ var refreshLiveTimeline = useHookState(project.update.refreshLiveTimeline).value;
var [nextSlideKeybinds, setNextSlideKeybinds] = useState(['Space', 'n', 'Enter']);
var [previousSlideKeybinds, setPreviousSlideKeybinds] = useState(['Backspace', 'p']);
@@ -649,6 +650,7 @@ function DefaultSettings() {
<Select
labelId='demo-simple-select-filled-label'
id='demo-simple-select-filled'
+ value='FullScreen'
onChange={console.log}
IconComponent={ArrowDropDownRoundedIcon}
>
@@ -744,6 +746,7 @@ function DefaultSettings() {
reader.addEventListener('load', ev => {
player.loadSlides(ev.target.result as string);
setWorkingTimeline(player.timeline.slides);
+ refreshLiveTimeline();
});
reader.readAsText(file);
}}
@@ -778,14 +781,10 @@ function DefaultSettings() {
}
export default function Index() {
- var timelineZoom = getTimelineZoom((st: any) => st.zoom);
- var setTimelineZoom = getTimelineZoom((st: any) => st.setZoom);
-
- var frame = useFrame((st: any) => st.currentFrame);
-
- var [tool, setTool] = useState('cursor');
-
- var playing = usePlaying((st: any) => st.playing);
+ var timelineZoom = useHookState(project.timeline.zoom);
+ var frame = useHookState(project.timeline.frame);
+ var tool = useHookState(project.timeline.tool);
+ var playing = useHookState(project.timeline.playing);
var mouseX = 0;
@@ -796,12 +795,12 @@ export default function Index() {
function zoomAroundPoint(newZoom: number, pivot: number) {
var timeline = document.querySelector('.timeline .timelineInner');
- var frame = getFrameAtOffset(pivot, timelineZoom);
+ var frame = getFrameAtOffset(pivot, timelineZoom.value);
var newOffset = (frame * zoomToPx(newZoom)) - pivot;
timeline.scrollLeft = newOffset;
- setTimelineZoom(newZoom);
- timelineZoom = newZoom;
+ timelineZoom.set(newZoom);
+ /* timelineZoom = newZoom; */
}
useEffect(() => {
@@ -809,7 +808,7 @@ export default function Index() {
if (!e.ctrlKey && !e.altKey) return;
e.preventDefault();
- var newZoom = Math.min(1, Math.max(0, timelineZoom + (-e.deltaY / 1000)));
+ var newZoom = Math.min(1, Math.max(0, timelineZoom.value + (-e.deltaY / 1000)));
zoomAroundPoint(newZoom, mouseX);
}, { passive: false });
}, []);
@@ -865,7 +864,7 @@ export default function Index() {
player.player.play();
}}
children={<PlaySkipIconAni />}
- style={{ '--ani-state': playing ? 'skip' : 'play' } as CSSProperties}
+ style={{ '--ani-state': playing.get() ? 'skip' : 'play' } as CSSProperties}
/>
<Fab size='small' children={<NavigateBeforeRoundedIcon />} onClick={() => player.previous()} />
<Fab
@@ -890,16 +889,18 @@ export default function Index() {
<div className='tools'>
<div className='time posrel'>
<span className='framerate numbers posabs l0 t0'>@{player.framerate}fps</span>
- <h2 className='timecode numbers posabs r0 t0'>{player.frameToTimestampString(frame, false)}</h2>
+ <h2 className='timecode numbers posabs r0 t0'>
+ {player.frameToTimestampString(frame.value, false)}
+ </h2>
</div>
<ToggleButtonGroup
color='primary'
aria-label='outlined primary button group'
- value={tool}
+ value={tool.get()}
exclusive
onChange={(_event: any, newTool: string | null) => {
if (newTool === null) return;
- setTool(newTool);
+ tool.set(newTool);
}}
>
<ToggleButton value='cursor' children={<Icon path={mdiCursorDefault} size={1} />} />
@@ -917,7 +918,7 @@ export default function Index() {
<ZoomOutRoundedIcon />
<div className='spacing'>
<Slider
- value={timelineZoom}
+ value={timelineZoom.value}
onChange={(_event: any, newValue: number | number[]) => {
var center = document.querySelector('.timeline .timelineInner').clientWidth / 2;
zoomAroundPoint(newValue as number, center);
@@ -933,12 +934,9 @@ export default function Index() {
</div>
<div
className='timeline posrel'
- style={{ '--zoom': zoomToPx(timelineZoom) } as CSSProperties}
+ style={{ '--zoom': zoomToPx(timelineZoom.value) } as CSSProperties}
>
- <TimelineEditor
- player={player}
- selectedTool={tool}
- />
+ <TimelineEditor />
</div>
</div>
</>;
diff --git a/styles/editor.css b/styles/editor.css
index a3990c2..3263c19 100644
--- a/styles/editor.css
+++ b/styles/editor.css
@@ -63,12 +63,15 @@
.settings .inner .MuiFormControl-root
{ width: 100%; }
+:root { --selection-hover-color: rgba(93, 233, 174, 0.04); }
.MuiSwitch-root .MuiSwitch-thumb { color: var(--c700); }
.MuiSwitch-root .MuiSwitch-track { background-color: var(--c700); }
.MuiSwitch-root .Mui-checked .MuiSwitch-thumb { color: var(--gruble); }
.MuiSwitch-root .Mui-checked + .MuiSwitch-track { background-color: var(--gruble) !important; }
.MuiSwitch-root .Mui-checked { color: var(--gruble) !important; }
-.MuiSwitch-root .Mui-checked:hover { background-color: rgba(93, 233, 174, 0.04) !important; }
+.MuiSwitch-root .Mui-checked:hover { background-color: var(--selection-hover-color) !important; }
+.MuiSlider-thumb.Mui-focusVisible, .MuiSlider-thumb:hover { box-shadow: 0px 0px 0px 8px var(--selection-hover-color) !important; }
+.MuiSlider-thumb.MuiSlider-active { box-shadow: 0px 0px 0px 14px var(--selection-hover-color) !important; }
.MuiFormControl-root {
border-radius: 4px;
diff --git a/yarn.lock b/yarn.lock
index 39c1649..04fa928 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -71,6 +71,11 @@
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131"
integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==
+"@hookstate/core@^3.0.8":
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/@hookstate/core/-/core-3.0.8.tgz#d6838153d6d43c2f35cfca475c31248192564e62"
+ integrity sha512-blQagGIVIbNoUiNCRrvaXqFmUe7WGMY35ok/LENfl2pcIsLBjkreYIZiaSFi83tkycwq7ZOmcQz/R1nvLKhH8w==
+
"@material-ui/core@^4.11.4":
version "4.11.4"
resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.11.4.tgz#4fb9fe5dec5dcf780b687e3a40cff78b2b9640a4"
@@ -2425,8 +2430,3 @@ yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
-
-zustand@^3.5.1:
- version "3.5.1"
- resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.5.1.tgz#28970280e830924b185ce064061231957398c1d0"
- integrity sha512-7J56Ve814z4zap71iaKFD+t65LFI//jEq/Vf55BTSVqJZCm+w9rov8OMBg+YSwIPQk54bfoIWHTrOWuAbpEDMw==