From a4dbd32752d2ae28fe7ff58c676905a17b7f3fcd Mon Sep 17 00:00:00 2001
From: lonkaars <loek@pipeframe.xyz>
Date: Thu, 24 Jun 2021 16:06:58 +0200
Subject: selection working again with hookstate :tada:

---
 pages/editor.tsx | 396 ++++++++++++++++++++++++++++++-------------------------
 1 file changed, 220 insertions(+), 176 deletions(-)

(limited to 'pages/editor.tsx')

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' : '')}>
-- 
cgit v1.2.3

