aboutsummaryrefslogtreecommitdiff
path: root/pages/editor.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'pages/editor.tsx')
-rw-r--r--pages/editor.tsx396
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' : '')}>