diff options
Diffstat (limited to 'pages/editor.tsx')
-rw-r--r-- | pages/editor.tsx | 396 |
1 files changed, 220 insertions, 176 deletions
diff --git a/pages/editor.tsx b/pages/editor.tsx index f00c504..8fcb905 100644 --- a/pages/editor.tsx +++ b/pages/editor.tsx @@ -1,6 +1,6 @@ import { createState, Downgraded, State, useHookstate } from '@hookstate/core'; import mousetrap from 'mousetrap'; -import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react'; +import { CSSProperties, ReactNode, Ref, useEffect, useRef, useState } from 'react'; import { animated, useSpring } from 'react-spring'; import { useDrag } from 'react-use-gesture'; @@ -114,7 +114,6 @@ function TimelineKeyframe(props: { slide: slide; }) { var workingTimeline = useHookstate(project).timeline.workingTimeline; - var updateTimeline = useHookstate(project).update.refreshLiveTimeline.value; function modifySlide(newProps: Partial<anySlide>) { var slide = workingTimeline.find(s => s.value.id == props.slide.id); @@ -239,166 +238,16 @@ function TimelineLabels() { return <div className='labels' children={labels.attach(Downgraded).get()} />; } -function TimelineEditor() { - var timelineZoom = useHookstate(project).timeline.zoom; +function TimelineSelection(props: { selectionAreaRef: Ref<ReactNode>; }) { var workingTimeline = useHookstate(project).timeline.workingTimeline; var tool = useHookstate(project).timeline.tool; - var mouseX = 0; - - var timelineRef = useRef(null); - useEffect(() => { - timelineRef.current.addEventListener('wheel', (e: WheelEvent) => { - if (!e.ctrlKey && !e.altKey) return; - e.preventDefault(); - - var newZoom = Math.min(1, Math.max(0, project.timeline.zoom.value + (-e.deltaY / 1000))); - zoomAroundPoint(newZoom, mouseX); - }); - }, []); - - useEffect(() => { - var canvas = document.querySelector('.timeline .timeScale'); - window.addEventListener('mousemove', e => { - var rect = canvas.getBoundingClientRect(); - mouseX = e.clientX - rect.x; - }); - }, []); - - useEffect(() => { - player.addEventListener('TimedVideoPlayerOnFrame', (event: CustomEvent) => { - project.timeline.frame.set(event.detail); - scrubberSpring.start({ frame: event.detail }); - }); - }, []); - - useEffect(() => { - 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) { - el.classList.add('current'); - } - }); - }); - }, []); - - // timeline canvas stuff - useEffect(() => { - var canvas = document.getElementById('timeScaleCanvas') as HTMLCanvasElement; - var ctx = canvas.getContext('2d'); - - var css = (varname: string) => getComputedStyle(document.body).getPropertyValue(varname).trim(); - var baseColor = css('--c100'); - var frameColor = css('--c250'); - var markerFrame = css('--c400'); - - function draw() { - ctx.clearRect(0, 0, canvas.width, canvas.height); - - var labels: Array<ReactNode> = []; - - var offset = document.querySelector('.timeline .timelineInner').scrollLeft; - - var frameWidth = zoomToPx(project.timeline.zoom.value); - - var d = true; - var a = 0; - var ns = [300, 150, 120, 90, 60, 30, 30, 30, 15, 15, 10, 10, 10]; - var everyN = ns[Math.floor(frameWidth)]; - for (var x = -offset; x < canvas.width + offset; x += frameWidth) { - ctx.fillStyle = baseColor; - - var rect = [Math.round(x + (frameWidth - 2) / 2), 28, 2, canvas.height]; - var drawFrame = false; - var marker = false; - if (frameWidth >= 6) { - ctx.fillStyle = d ? baseColor : frameColor; - rect = [x, 28, frameWidth, canvas.height]; - drawFrame = !d; - } - if (a % everyN == 0) { - ctx.fillStyle = markerFrame; - drawFrame = true; - marker = true; - } - - if (drawFrame) { - ctx.fillRect(Math.round(rect[0]), Math.round(rect[1]), Math.round(rect[2]), Math.round(rect[3])); - - if (marker) { - var frame = Math.round(x / frameWidth + offset / frameWidth + 1); - labels.push( - <span - className='label numbers posabs nosel' - style={{ - left: Math.round(rect[0] + frameWidth / 2), - top: rect[1], - }} - children={player.frameToTimestampString(frame - 1)} - />, - ); - } - } - - d = !d; - a++; - } - - project.timeline.labels.set(labels); - - requestAnimationFrame(draw); - } - draw(); - - function onresize() { - var size = document.querySelector('.timeline .timelineInner'); - canvas.width = size.clientWidth; - canvas.height = size.clientHeight; - } - onresize(); - 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, project.timeline.zoom.value)) - 1); - scrubberSpring.start({ frame }); - if (player.player) { - var time = player.frameToTimestamp(frame + 1); - if (isFinite(time)) player.player.currentTime = time; - } - project.timeline.frame.set(frame); - }, { domTarget: scrubberDragRef, eventOptions: { passive: false } }); - - // slide placement ghost - var [ghost, ghostApi] = useSpring(() => ({ - x: 0, - y: 0, - config: { mass: 0.5, tension: 500, friction: 20 }, - })); - useEffect(() => { - document.querySelector('.timeline').addEventListener('mousemove', (e: MouseEvent) => { - var rect = document.querySelector('.timeline').getBoundingClientRect(); - var x = e.clientX - rect.left; - var y = e.clientY - rect.top; - ghostApi.start({ x, y }); - }); - }, []); - - // selection var [selectionActive, setSelectionActive] = useState(false); var [selectionPlaced, setSelectionPlaced] = useState(false); var [selectionHidden, setSelectionHidden] = useState(true); var [selectionLeftType, setSelectionLeftType] = useState(null); var [selectionRightType, setSelectionRightType] = useState(null); + var [selectionPos, selectionPosAPI] = useSpring(() => ({ x1: 0, y1: 0, @@ -411,9 +260,10 @@ function TimelineEditor() { widthOffset: 0, config: { mass: 0.5, tension: 500, friction: 20 }, })); - var selectionAreaRef = useRef(null); + var selectionRef = useRef(null); var [selection, setSelection] = useState<anySlide[]>([]); + // drag on selection useDrag(({ movement: [x, _y], last }) => { if (!selectionPlaced) return; if (selection.length < 1) return; @@ -451,6 +301,7 @@ function TimelineEditor() { selectionPosAPI.start({ startingFrame: selectionFrame + frameOffset }); }); }, { domTarget: selectionRef, eventOptions: { passive: false } }); + useDrag(({ xy: [x, y], initial: [bx, by], first, last, movement: [ox, oy] }) => { if (tool.value != 'cursor') return; var minDistance = 5; // minimal drag distance in pixels to register selection @@ -530,12 +381,24 @@ function TimelineEditor() { startOffset, widthOffset, }); + + setTimeout(() => { + selectionPosAPI.start({ + y1: 50, + y2: 62, + startingFrame: left.frame, + frameWidth: right.frame - left.frame, + center: 0.5, + startOffset, + widthOffset, + }); + }, 100); setSelectionLeftType(left.type); setSelectionRightType(right.type); setSelectionPlaced(true); } } - }, { domTarget: selectionAreaRef, eventOptions: { passive: false } }); + }, { domTarget: props.selectionAreaRef, eventOptions: { passive: false } }); useEffect(() => { var delkeys = ['del', 'backspace']; @@ -562,6 +425,206 @@ function TimelineEditor() { }; }, [selectionPlaced, workingTimeline]); + function CustomSelection(props: { + x1: number; + x2: number; + y1: number; + y2: number; + widthOffset: number; + frameWidth: number; + className: string; + }) { + return <Selection + className={props.className} + width={props.x2 - props.x1 + 12} + frameWidth={props.frameWidth} + height={props.y2 - props.y1 + 12} + left={selectionLeftType} + right={selectionRightType} + widthOffset={props.widthOffset} + />; + } + var AnimatedSelection = animated(props => <CustomSelection {...props} />); + + return <animated.div + id='selection' + className={'posabs dispinbl ' + (selectionPlaced ? 'placed ' : '')} + ref={selectionRef} + style={{ + '--starting-frame': selectionPos.startingFrame, + '--y': selectionPos.y1, + '--start-offset': selectionPos.startOffset, + '--center': selectionPos.center, + pointerEvents: selectionPlaced ? 'all' : 'none', + } as CSSProperties} + > + <AnimatedSelection + x1={selectionPos.x1} + x2={selectionPos.x2} + y1={selectionPos.y1} + y2={selectionPos.y2} + widthOffset={selectionPos.widthOffset} + frameWidth={selectionPos.frameWidth} + className={'' + (selectionActive ? 'active ' : '') + (selectionHidden ? 'hidden ' : '')} + /> + </animated.div>; +} + +function TimelineEditor() { + var timelineZoom = useHookstate(project).timeline.zoom; + var workingTimeline = useHookstate(project).timeline.workingTimeline; + var tool = useHookstate(project).timeline.tool; + + var mouseX = 0; + + var timelineRef = useRef(null); + var selectionAreaRef = useRef(null); + useEffect(() => { + timelineRef.current.addEventListener('wheel', (e: WheelEvent) => { + if (!e.ctrlKey && !e.altKey) return; + e.preventDefault(); + + var newZoom = Math.min(1, Math.max(0, project.timeline.zoom.value + (-e.deltaY / 1000))); + zoomAroundPoint(newZoom, mouseX); + }); + }, []); + + useEffect(() => { + var canvas = document.querySelector('.timeline .timeScale'); + window.addEventListener('mousemove', e => { + var rect = canvas.getBoundingClientRect(); + mouseX = e.clientX - rect.x; + }); + }, []); + + useEffect(() => { + player.addEventListener('TimedVideoPlayerOnFrame', (event: CustomEvent) => { + project.timeline.frame.set(event.detail); + scrubberSpring.start({ frame: event.detail }); + }); + }, []); + + useEffect(() => { + 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) { + el.classList.add('current'); + } + }); + }); + }, []); + + // timeline canvas stuff + useEffect(() => { + var canvas = document.getElementById('timeScaleCanvas') as HTMLCanvasElement; + var ctx = canvas.getContext('2d'); + + var css = (varname: string) => getComputedStyle(document.body).getPropertyValue(varname).trim(); + var baseColor = css('--c100'); + var frameColor = css('--c250'); + var markerFrame = css('--c400'); + + function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var labels: Array<ReactNode> = []; + + var offset = document.querySelector('.timeline .timelineInner').scrollLeft; + + var frameWidth = zoomToPx(project.timeline.zoom.value); + + var d = true; + var a = 0; + var ns = [300, 150, 120, 90, 60, 30, 30, 30, 15, 15, 10, 10, 10]; + var everyN = ns[Math.floor(frameWidth)]; + for (var x = -offset; x < canvas.width + offset; x += frameWidth) { + ctx.fillStyle = baseColor; + + var rect = [Math.round(x + (frameWidth - 2) / 2), 28, 2, canvas.height]; + var drawFrame = false; + var marker = false; + if (frameWidth >= 6) { + ctx.fillStyle = d ? baseColor : frameColor; + rect = [x, 28, frameWidth, canvas.height]; + drawFrame = !d; + } + if (a % everyN == 0) { + ctx.fillStyle = markerFrame; + drawFrame = true; + marker = true; + } + + if (drawFrame) { + ctx.fillRect(Math.round(rect[0]), Math.round(rect[1]), Math.round(rect[2]), Math.round(rect[3])); + + if (marker) { + var frame = Math.round(x / frameWidth + offset / frameWidth + 1); + labels.push( + <span + className='label numbers posabs nosel' + style={{ + left: Math.round(rect[0] + frameWidth / 2), + top: rect[1], + }} + children={player.frameToTimestampString(frame - 1)} + />, + ); + } + } + + d = !d; + a++; + } + + project.timeline.labels.set(labels); + + requestAnimationFrame(draw); + } + draw(); + + function onresize() { + var size = document.querySelector('.timeline .timelineInner'); + canvas.width = size.clientWidth; + canvas.height = size.clientHeight; + } + onresize(); + 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, project.timeline.zoom.value)) - 1); + scrubberSpring.start({ frame }); + if (player.player) { + var time = player.frameToTimestamp(frame + 1); + if (isFinite(time)) player.player.currentTime = time; + } + project.timeline.frame.set(frame); + }, { domTarget: scrubberDragRef, eventOptions: { passive: false } }); + + // slide placement ghost + var [ghost, ghostApi] = useSpring(() => ({ + x: 0, + y: 0, + config: { mass: 0.5, tension: 500, friction: 20 }, + })); + useEffect(() => { + document.querySelector('.timeline').addEventListener('mousemove', (e: MouseEvent) => { + var rect = document.querySelector('.timeline').getBoundingClientRect(); + var x = e.clientX - rect.left; + var y = e.clientY - rect.top; + ghostApi.start({ x, y }); + }); + }, []); + return <div className='timeline posrel' style={{ '--zoom': zoomToPx(timelineZoom.value) } as CSSProperties} @@ -612,26 +675,7 @@ function TimelineEditor() { > <div className='selectionarea posabs v0' ref={selectionAreaRef} /> {workingTimeline.value.map(slide => <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 ' : '')} - width={selectionPos.x2.toJSON() - selectionPos.x1.toJSON() + 12} - frameWidth={selectionPos.frameWidth.toJSON()} - height={selectionPos.y2.toJSON() - selectionPos.y1.toJSON() + 12} - left={selectionLeftType} - right={selectionRightType} - widthOffset={selectionPos.widthOffset.toJSON()} - />} - /> + <TimelineSelection selectionAreaRef={selectionAreaRef} /> </div> </div> <div className={'ghostArea posabs a0' + (tool.value != 'cursor' ? ' active' : '')}> |