diff options
-rw-r--r-- | components/icons.tsx | 5 | ||||
-rw-r--r-- | pages/editor.tsx | 57 | ||||
-rw-r--r-- | pages/present.tsx | 6 | ||||
-rw-r--r-- | timeline.schema.json | 57 | ||||
-rw-r--r-- | timeline.ts | 7 |
5 files changed, 87 insertions, 45 deletions
diff --git a/components/icons.tsx b/components/icons.tsx index 50bf8f0..f8d6668 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -1,5 +1,4 @@ -import { Ref } from 'react'; -import { keyframeTypes } from '../timeline'; +import { slideTypes } from '../timeline'; export function PressureIcon() { return <svg width='48' height='48' viewBox='0 0 48 48' fill='none' xmlns='http://www.w3.org/2000/svg'> @@ -117,7 +116,7 @@ export function PressureIcon() { } export function SlideKeyframe(props: { - type: keyframeTypes; + type: slideTypes; loopEnd?: boolean; }) { return <svg diff --git a/pages/editor.tsx b/pages/editor.tsx index e001dfd..7d8834c 100644 --- a/pages/editor.tsx +++ b/pages/editor.tsx @@ -1,8 +1,8 @@ import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react'; -import { animated, useSpring, useSprings } from 'react-spring'; +import { animated, useSpring } from 'react-spring'; import { useDrag } from 'react-use-gesture'; import create from 'zustand'; -import { delaySlide, loopSlide, slide, speedChangeSlide } from '../timeline'; +import { anySlide, loopSlide, slide } from '../timeline'; import { TimedVideoPlayer } from './present'; import AppBar from '@material-ui/core/AppBar'; @@ -15,15 +15,19 @@ import ZoomInRoundedIcon from '@material-ui/icons/ZoomInRounded'; import ZoomOutRoundedIcon from '@material-ui/icons/ZoomOutRounded'; import Icon from '@mdi/react'; -import { PressureIcon, SlideKeyframe } from '../components/icons'; -import Loop from '../components/loop'; - import FullscreenRoundedIcon from '@material-ui/icons/FullscreenRounded'; import NavigateBeforeRoundedIcon from '@material-ui/icons/NavigateBeforeRounded'; import NavigateNextRoundedIcon from '@material-ui/icons/NavigateNextRounded'; import PauseRoundedIcon from '@material-ui/icons/PauseRounded'; import SkipPreviousRoundedIcon from '@material-ui/icons/SkipPreviousRounded'; import { mdiCursorDefault } from '@mdi/js'; +import { PressureIcon, SlideKeyframe } from '../components/icons'; + +var player = new TimedVideoPlayer(); +var useWorkingTimeline = create(set => ({ + timeline: [], + setTimeline: (newTimeline: anySlide[]) => set(() => ({ timeline: newTimeline })), +})); var getTimelineZoom = create(set => ({ zoom: 0.687077725615, @@ -50,8 +54,17 @@ var useFrame = create(set => ({ })); function TimelineKeyframe(props: { - slide: slide | delaySlide | loopSlide | speedChangeSlide; + slide: slide; }) { + var workingTimeline = useWorkingTimeline((st: any) => st.timeline); + var setWorkingTimeline = useWorkingTimeline((st: any) => st.setTimeline); + + function modifySlide(newProps: Partial<anySlide>) { + var slide = workingTimeline.find((s: anySlide) => s.id == props.slide.id); + slide = Object.assign(slide, newProps); + setWorkingTimeline(workingTimeline); + } + var dragRef = useRef(null); var loopStartRef = useRef(null); var loopEndRef = useRef(null); @@ -83,8 +96,13 @@ function TimelineKeyframe(props: { endOffset = endFrame - grabFrameOffset; } api.start({ begin: frame + startOffset, frame: frame + endOffset }); + + modifySlide({ frame: frame + endOffset }); + modifySlide({ beginFrame: frame + startOffset }); } else { api.start({ frame }); + + modifySlide({ frame }); } }, { domTarget: dragRef, eventOptions: { passive: false } }); @@ -92,20 +110,35 @@ function TimelineKeyframe(props: { // loop start useDrag(({ xy: [x, _y] }) => { var frame = Math.max(0, Math.round(getFrameAtOffset(x - 240, timelineZoom)) - 1); + api.start({ begin: frame }); + + modifySlide({ beginFrame: frame }); }, { domTarget: loopStartRef, eventOptions: { passive: false } }); // loop end useDrag(({ xy: [x, _y] }) => { var frame = Math.max(0, Math.round(getFrameAtOffset(x - 240, timelineZoom)) - 1); + api.start({ frame }); + + modifySlide({ frame }); }, { domTarget: loopEndRef, eventOptions: { passive: false } }); } + var mouseUpListener = useRef(null); + + useDrag(({ last }) => { + if (!last) return; + player.timeline.slides = Array(...workingTimeline); + player.timeline.slides.sort((a: anySlide, b: anySlide) => a.frame - b.frame); + }, { domTarget: mouseUpListener, eventOptions: { passive: false } }); + return <animated.div className='frame posabs' style={{ '--frame': spring.frame } as CSSProperties} id={'slide-' + props.slide.id} + ref={mouseUpListener} > <div className='keyframeWrapper posabs abscenterh'> {props.slide.type == 'loop' @@ -128,14 +161,14 @@ function TimelineKeyframe(props: { </animated.div>; } -function TimelineEditor(props: { - player: TimedVideoPlayer; -}) { +function TimelineEditor(props: { player: TimedVideoPlayer; }) { var timelineZoom = getTimelineZoom((st: any) => st.zoom); var timelineLabels = useTimelineLabels((st: any) => st.labels); var setTimelineLabels = useTimelineLabels((st: any) => st.setLabels); + var workingTimeline = useWorkingTimeline((st: any) => st.timeline); + // var frame = useFrame((st: any) => st.currentFrame); var setFrame = useFrame((st: any) => st.setFrame); @@ -281,7 +314,7 @@ function TimelineEditor(props: { <div className='keyframes' style={{ '--total-frames': props.player?.timeline?.framecount.toString() } as CSSProperties} - children={props.player?.timeline?.slides.map(slide => <TimelineKeyframe slide={slide} />)} + children={workingTimeline.map((slide: anySlide) => <TimelineKeyframe slide={slide} />)} /> </div> </>; @@ -290,7 +323,8 @@ function TimelineEditor(props: { export default function Index() { var [dummy, setDummy] = useState(false); var rerender = () => setDummy(!dummy); - var [player, _setPlayer] = useState(new TimedVideoPlayer()); + + var setWorkingTimeline = useWorkingTimeline((st: any) => st.setTimeline); var timelineZoom = getTimelineZoom((st: any) => st.zoom); var setTimelineZoom = getTimelineZoom((st: any) => st.setZoom); @@ -375,6 +409,7 @@ export default function Index() { var reader = new FileReader(); reader.addEventListener('load', ev => { player.loadSlides(ev.target.result as string); + setWorkingTimeline(player.timeline.slides); rerender(); }); reader.readAsText(file); diff --git a/pages/present.tsx b/pages/present.tsx index f5955fc..9769577 100644 --- a/pages/present.tsx +++ b/pages/present.tsx @@ -58,6 +58,9 @@ export class TimedVideoPlayer { jumpToFrame(frame: number) { this.player.currentTime = this.frameToTimestamp(frame); this.frame = frame; + + var event = new CustomEvent('TimedVideoPlayerOnFrame', { detail: this.frame }); + this.dispatchEvent(event); } jumpToSlide(slide: slide) { @@ -128,7 +131,6 @@ export class TimedVideoPlayer { setInterval(() => { if (this.player.paused) return; - var lastFrame = this.frame; this.frame = this.timestampToFrame(this.player.currentTime); var event = new CustomEvent('TimedVideoPlayerOnFrame', { detail: this.frame }); @@ -169,6 +171,7 @@ export class TimedVideoPlayer { this.framerate = this.timeline.framerate; this.timeline.slides[-1] = { + id: '00000000-0000-0000-0000-000000000000', frame: 0, type: 'default', clickThroughBehaviour: 'ImmediatelySkip', @@ -202,6 +205,7 @@ export class TimedVideoPlayer { if (!this.registeredEventListeners) return; this.slide = Math.max(this.slide - 1, -1); + var event = new CustomEvent('TimedVideoPlayerSlide', { detail: this.slide }); this.dispatchEvent(event); diff --git a/timeline.schema.json b/timeline.schema.json index 2d8c6ff..75c5265 100644 --- a/timeline.schema.json +++ b/timeline.schema.json @@ -2,6 +2,22 @@ "$ref": "#/definitions/timeline", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "anySlide": { + "anyOf": [ + { + "$ref": "#/definitions/slide" + }, + { + "$ref": "#/definitions/delaySlide" + }, + { + "$ref": "#/definitions/speedChangeSlide" + }, + { + "$ref": "#/definitions/loopSlide" + } + ] + }, "delaySlide": { "additionalProperties": false, "properties": { @@ -22,7 +38,7 @@ "type": "string" }, "type": { - "$ref": "#/definitions/keyframeTypes" + "$ref": "#/definitions/slideTypes" } }, "required": [ @@ -34,15 +50,6 @@ ], "type": "object" }, - "keyframeTypes": { - "enum": [ - "default", - "delay", - "speedChange", - "loop" - ], - "type": "string" - }, "loopSlide": { "additionalProperties": false, "properties": { @@ -70,7 +77,7 @@ "type": "string" }, "type": { - "$ref": "#/definitions/keyframeTypes" + "$ref": "#/definitions/slideTypes" } }, "required": [ @@ -113,7 +120,7 @@ "type": "string" }, "type": { - "$ref": "#/definitions/keyframeTypes" + "$ref": "#/definitions/slideTypes" } }, "required": [ @@ -124,6 +131,15 @@ ], "type": "object" }, + "slideTypes": { + "enum": [ + "default", + "delay", + "speedChange", + "loop" + ], + "type": "string" + }, "speedChangeSlide": { "additionalProperties": false, "properties": { @@ -144,7 +160,7 @@ "type": "number" }, "type": { - "$ref": "#/definitions/keyframeTypes" + "$ref": "#/definitions/slideTypes" } }, "required": [ @@ -173,20 +189,7 @@ }, "slides": { "items": { - "anyOf": [ - { - "$ref": "#/definitions/slide" - }, - { - "$ref": "#/definitions/delaySlide" - }, - { - "$ref": "#/definitions/speedChangeSlide" - }, - { - "$ref": "#/definitions/loopSlide" - } - ] + "$ref": "#/definitions/anySlide" }, "type": "array" } diff --git a/timeline.ts b/timeline.ts index 764bc7a..5d0f51c 100644 --- a/timeline.ts +++ b/timeline.ts @@ -1,9 +1,10 @@ -export type keyframeTypes = 'default' | 'delay' | 'speedChange' | 'loop'; +export type slideTypes = 'default' | 'delay' | 'speedChange' | 'loop'; +export type anySlide = slide | delaySlide | speedChangeSlide | loopSlide; export interface slide { frame: number; clickThroughBehaviour: 'ImmediatelySkip' | 'PlayOut'; - type: keyframeTypes; + type: slideTypes; id: string; } @@ -25,7 +26,7 @@ export interface presentationSettings { } export default interface timeline { - slides: Array<slide | delaySlide | speedChangeSlide | loopSlide>; + slides: Array<anySlide>; framecount: number; framerate: number; name: string; |