diff options
author | lonkaars <loek@pipeframe.xyz> | 2021-06-16 20:13:32 +0200 |
---|---|---|
committer | lonkaars <loek@pipeframe.xyz> | 2021-06-16 20:13:32 +0200 |
commit | 67d3c02a89cb170fc1819ae49ef7ba08f1d8305c (patch) | |
tree | 3c4ce9b92de2a832a312b34f46a1993fe23f2f71 | |
parent | bafd27bfec6e6e0cdda14ce577a26b1937697ca3 (diff) |
draggable slides
-rw-r--r-- | pages/editor.tsx | 61 | ||||
-rw-r--r-- | timeline.ts | 1 |
2 files changed, 57 insertions, 5 deletions
diff --git a/pages/editor.tsx b/pages/editor.tsx index 1f79699..0da9a0d 100644 --- a/pages/editor.tsx +++ b/pages/editor.tsx @@ -26,6 +26,7 @@ import PlaySkipIconAni from '../components/play-skip'; import Selection from '../components/selection'; var keyframeInAnimations: { [key: string]: { x: number; y: number; }; } = {}; +var slideAPIs: { [key: string]: any; }[] = []; var player = new TimedVideoPlayer(); var useWorkingTimeline = create((set, get) => ({ @@ -108,6 +109,8 @@ function TimelineKeyframe(props: { config: { mass: 0.5, tension: 500, friction: 20 }, })); + slideAPIs[props.slide.id] = api; + useEffect(() => { setFirstRender(false); var beginAnimation = keyframeInAnimations[props.slide.id]; @@ -318,15 +321,14 @@ function TimelineEditor(props: { window.addEventListener('resize', onresize); }, []); + // timeline scrubber var scrubberDragRef = useRef(null); - var [scrubberPos, scrubberSpring] = useSpring( () => ({ frame: 0, config: { mass: 0.5, tension: 500, friction: 20 }, }), ); - useDrag(({ xy: [x, _y] }) => { var frame = Math.max(0, Math.round(getFrameAtOffset(x - 240, timelineZoom)) - 1); setFrame(frame); @@ -337,6 +339,7 @@ function TimelineEditor(props: { } }, { domTarget: scrubberDragRef, eventOptions: { passive: false } }); + // slide placement ghost var [ghost, ghostApi] = useSpring(() => ({ x: 0, y: 0, @@ -351,6 +354,7 @@ function TimelineEditor(props: { }); }, []); + // selection var [selectionActive, setSelectionActive] = useState(false); var [selectionPlaced, setSelectionPlaced] = useState(false); var [selectionHidden, setSelectionHidden] = useState(true); @@ -368,7 +372,45 @@ function TimelineEditor(props: { widthOffset: 0, config: { mass: 0.5, tension: 500, friction: 20 }, })); + var selectionAreaRef = useRef(null); var selectionRef = useRef(null); + var [selection, setSelection] = useState([]); + useDrag(({ movement: [x, _y], last }) => { + if (!selectionPlaced) return; + if (selection.length < 1) return; + var frameOffset = Math.round(x / zoomToPx(timelineZoom)); + 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 begin = loop.beginFrame + frameOffset; + api.start({ begin }); + + if (last) { + loop.beginFrame = begin; + refreshWorkingTimline(); + } + + break; + } + default: { + if (!api) break; + var frame = slide.frame + frameOffset; + api.start({ frame }); + + if (last) { + workingTimeline.find((s: anySlide) => s.id == slide.id).frame = frame; + refreshWorkingTimline(); + } + } + } + if (last) return; + var selectionFrame = selection[0].frame; + selectionPosAPI.start({ startingFrame: selectionFrame + frameOffset }); + }); + }, { domTarget: selectionRef, eventOptions: { passive: false } }); useDrag(({ xy: [x, y], initial: [bx, by], first, last, movement: [ox, oy] }) => { if (props.selectedTool != 'cursor') return; var minDistance = 5; // minimal drag distance in pixels to register selection @@ -383,6 +425,8 @@ function TimelineEditor(props: { startOffset: 0, widthOffset: 0, }); + selection = []; + setSelection(selection); var timelineInner = document.querySelector('.timeline .timelineInner'); var timelineRects = timelineInner.getBoundingClientRect(); @@ -416,9 +460,11 @@ function TimelineEditor(props: { if (slide.type != 'loop') continue; var beginFrame = (slide as loopSlide).beginFrame; expandedTimeline.splice(i, 0, new loopBeginSlide(beginFrame)); + expandedTimeline[i].id = expandedTimeline[i + 1].id; i++; } - var keyframesInSelection = expandedTimeline.filter((slide: anySlide) => + + var keyframesInSelection = expandedTimeline.filter(slide => slide.frame >= Math.floor(startingFrame) && slide.frame <= Math.ceil(endingFrame) ); @@ -427,6 +473,9 @@ function TimelineEditor(props: { return; } + selection = keyframesInSelection; + setSelection(selection); + var left = keyframesInSelection[0]; var right = keyframesInSelection[keyframesInSelection.length - 1]; @@ -446,7 +495,7 @@ function TimelineEditor(props: { setSelectionPlaced(true); } } - }, { domTarget: selectionRef, eventOptions: { passive: false } }); + }, { domTarget: selectionAreaRef, eventOptions: { passive: false } }); return <> <canvas @@ -492,15 +541,17 @@ function TimelineEditor(props: { className='keyframes' style={{ '--total-frames': props.player?.timeline?.framecount.toString() } as CSSProperties} > - <div className='selectionarea posabs v0' ref={selectionRef} /> + <div className='selectionarea posabs v0' ref={selectionAreaRef} /> {workingTimeline.map((slide: anySlide) => <TimelineKeyframe slide={slide} />)} <div id='selection' className={'posabs dispinbl ' + (selectionPlaced ? 'placed ' : '')} + ref={selectionRef} style={{ left: `calc(var(--zoom) * ${selectionPos.startingFrame.toJSON() + selectionPos.center.toJSON()} * 1px - 6px + ${selectionPos.startOffset.toJSON()} * 1px)`, top: selectionPos.y1.toJSON() - 6, + pointerEvents: selectionPlaced ? 'all' : 'none', }} children={<Selection className={'' + (selectionActive ? 'active ' : '') + (selectionHidden ? 'hidden ' : '')} diff --git a/timeline.ts b/timeline.ts index ade936e..1611eb8 100644 --- a/timeline.ts +++ b/timeline.ts @@ -1,6 +1,7 @@ import { v4 as uuid } from 'uuid'; export type slideTypes = 'default' | 'delay' | 'speedChange' | 'loop'; +export var slideTypes = ['default', 'delay', 'speedChange', 'loop']; export type anySlide = slide | delaySlide | speedChangeSlide | loopSlide; export class slide { |