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; |