From b0de5480f42cc7934a02131082638dae3fadd6e6 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Tue, 29 Sep 2020 16:54:57 +0200
Subject: [PATCH] Introduce play head

---
 src/routes/player/PreviewBar.tsx              | 12 +--
 src/routes/player/SeekBar.tsx                 | 83 ++++++++++++-------
 src/util/mouse/getMousePosition.tsx           |  3 +-
 src/util/offset/boundingRect.ts               | 10 +++
 .../{useWidth.ts => useBoundingRect.ts}       |  9 +-
 src/util/offset/useOffset.ts                  |  9 --
 src/util/offset/useOffsetAbsolute.ts          | 14 ++++
 src/util/offset/useOffsetAbsoluteRef.ts       | 10 +++
 src/util/offset/useOffsetRef.ts               | 10 ---
 src/util/offset/useOffsetRelative.ts          | 14 ++++
 src/util/offset/useOffsetRelativeRef.ts       | 10 +++
 11 files changed, 126 insertions(+), 58 deletions(-)
 create mode 100644 src/util/offset/boundingRect.ts
 rename src/util/offset/{useWidth.ts => useBoundingRect.ts} (74%)
 delete mode 100644 src/util/offset/useOffset.ts
 create mode 100644 src/util/offset/useOffsetAbsolute.ts
 create mode 100644 src/util/offset/useOffsetAbsoluteRef.ts
 delete mode 100644 src/util/offset/useOffsetRef.ts
 create mode 100644 src/util/offset/useOffsetRelative.ts
 create mode 100644 src/util/offset/useOffsetRelativeRef.ts

diff --git a/src/routes/player/PreviewBar.tsx b/src/routes/player/PreviewBar.tsx
index fd72e87..1f53cab 100644
--- a/src/routes/player/PreviewBar.tsx
+++ b/src/routes/player/PreviewBar.tsx
@@ -1,19 +1,20 @@
 import React, {useMemo} from "react";
 import {createUseStyles} from "react-jss";
-import {useOffsetRef} from "../../util/offset/useOffsetRef";
+import {MousePosition} from "../../util/mouse/MousePosition";
+import {useOffsetAbsoluteRef} from "../../util/offset/useOffsetAbsoluteRef";
 import {PreviewViewer} from "./PreviewViewer";
 
 interface Props {
     previewTrack: HTMLTrackElement | null,
     duration: number,
-    position: number,
+    position: MousePosition | null,
     hidden: boolean,
 }
 
 export function PreviewBar({previewTrack, duration, position, hidden}: Props) {
     const classes = useStyles();
 
-    const [previewBarRef, previewHeadRef, offset] = useOffsetRef(position);
+    const [previewBarRef, previewHeadRef, offset] = useOffsetAbsoluteRef(position?.absolute || 0);
 
     return useMemo(() => (
         <div
@@ -30,7 +31,7 @@ export function PreviewBar({previewTrack, duration, position, hidden}: Props) {
             >
                 <PreviewViewer
                     previewTrack={previewTrack}
-                    position={position * duration}
+                    position={(position?.relative || 0) * duration}
                 />
             </div>
         </div>
@@ -40,11 +41,12 @@ export function PreviewBar({previewTrack, duration, position, hidden}: Props) {
 const useStyles = createUseStyles({
     previewBar: {
         position: "relative",
-        width: "40rem",
+        width: "45rem",
         display: "flex",
         flexDirection: "row",
         alignContent: "stretch",
         background: "#7c7",
+        marginLeft: 60,
     },
     previewHead: {
         maxWidth: "136rem",
diff --git a/src/routes/player/SeekBar.tsx b/src/routes/player/SeekBar.tsx
index ed5ffb0..b138c8e 100644
--- a/src/routes/player/SeekBar.tsx
+++ b/src/routes/player/SeekBar.tsx
@@ -1,7 +1,9 @@
 import React, {MouseEvent, useCallback, useMemo, useState} from "react";
 import {createUseStyles} from "react-jss";
 import {getMousePosition} from "../../util/mouse/getMousePosition";
-import {useOffsetRef} from "../../util/offset/useOffsetRef";
+import {MousePosition} from "../../util/mouse/MousePosition";
+import {useOffsetAbsolute} from "../../util/offset/useOffsetAbsolute";
+import {useOffsetRelative} from "../../util/offset/useOffsetRelative";
 import {PreviewBar} from "./PreviewBar";
 
 interface Props {
@@ -14,61 +16,74 @@ interface Props {
 export function SeekBar({video, previewTrack, duration, position}: Props) {
     const classes = useStyles();
 
-    const [seekPosition, setSeekPosition] = useState<number | null>(null);
-    const isVisible = seekPosition !== null;
+    const [mousePosition, setMousePosition] = useState<MousePosition | null>(null);
+    const isVisible = mousePosition !== null;
 
-    const [seekBarRef, seekHeadRef, offset] = useOffsetRef(seekPosition || 0);
+    const [seekBarRef, setSeekBarRef] = useState<HTMLDivElement | null>(null);
+    const [seekHeadRef, setSeekHeadRef] = useState<HTMLDivElement | null>(null);
+    const [playHeadRef, setPlayHeadRef] = useState<HTMLDivElement | null>(null);
+
+    const seekHeadOffset = useOffsetAbsolute(seekBarRef, seekHeadRef, mousePosition?.absolute || 0);
+    const playHeadOffset = useOffsetRelative(seekBarRef, playHeadRef, position / duration);
 
     const onMouseLeave = useCallback(() => {
-        setSeekPosition(null)
-    }, [setSeekPosition]);
+        setMousePosition(null)
+    }, []);
 
     const onClick = useCallback((event: MouseEvent<HTMLDivElement>) => {
         const position = getMousePosition(event);
-        if (position === null) {
-            return;
-        }
-        if (video) {
+        setMousePosition(position);
+        if (video && position) {
             video.currentTime = position.relative * duration;
         }
     }, [duration, video]);
 
     const onMouseMove = useCallback((event: MouseEvent<HTMLDivElement>) => {
         const position = getMousePosition(event);
-        if (position === null) {
-            return;
-        }
-        window.requestAnimationFrame(() => {
-            setSeekPosition(position.relative)
-        })
+        setMousePosition(position);
     }, []);
 
+    const seekHead = useMemo(() => (
+        <div
+            ref={setSeekHeadRef}
+            className={classes.seekHead}
+            style={{
+                opacity: isVisible ? 1 : 0,
+                transform: `translate3d(${seekHeadOffset}px, 0, 0)`
+            }}
+        />
+    ), [classes.seekHead, isVisible, seekHeadOffset]);
+
+    const playHead = useMemo(() => (
+        <div
+            ref={setPlayHeadRef}
+            className={classes.playHead}
+            style={{
+                transform: `translate3d(${playHeadOffset}px, 0, 0)`
+            }}
+        />
+    ), [classes.playHead, playHeadOffset]);
+
     const seekBar = useMemo(() => (
         <div
-            ref={seekBarRef}
+            ref={setSeekBarRef}
             className={classes.seekBar}
             onMouseLeave={onMouseLeave}
             onClick={onClick}
             onMouseMove={onMouseMove}
         >
-            <div
-                ref={seekHeadRef}
-                className={classes.seekHead}
-                style={{
-                    opacity: isVisible ? 1 : 0,
-                    transform: `translate3d(${offset}px, 0, 0)`
-                }}
-            />
+            {seekHead}
+            {playHead}
         </div>
-    ), [classes, isVisible, offset, onClick, onMouseLeave, onMouseMove, seekBarRef, seekHeadRef]);
+    ), [classes.seekBar, onClick, onMouseLeave, onMouseMove, playHead, seekHead]);
 
     return (
         <React.Fragment>
             <PreviewBar
                 previewTrack={previewTrack}
                 duration={duration}
-                position={seekPosition || 0}
-                hidden={seekPosition === null}
+                position={mousePosition}
+                hidden={mousePosition === null}
             />
             {seekBar}
         </React.Fragment>
@@ -81,12 +96,22 @@ const useStyles = createUseStyles({
         width: "40rem",
         display: "flex",
         flexDirection: "row",
-        alignContent: "stretch",
+        alignContent: "center",
+        alignItems: "center",
         height: "5rem",
         background: "#77c",
     },
     seekHead: {
         width: "0.1rem",
         background: "#f00",
+        position: "absolute",
+        height: "1rem",
+    },
+    playHead: {
+        width: "1rem",
+        background: "#f00",
+        position: "absolute",
+        height: "1rem",
+        borderRadius: "100%",
     }
 });
diff --git a/src/util/mouse/getMousePosition.tsx b/src/util/mouse/getMousePosition.tsx
index ded060c..898615a 100644
--- a/src/util/mouse/getMousePosition.tsx
+++ b/src/util/mouse/getMousePosition.tsx
@@ -4,9 +4,10 @@ import {MousePosition} from "./MousePosition";
 export function getMousePosition(event: MouseEvent<HTMLDivElement>): MousePosition | null {
     const position = event.clientX - event.currentTarget.offsetLeft;
     const width = event.currentTarget.offsetWidth;
+    if (position < 0) return null;
     if (position > width) return null;
     return {
-        absolute: position,
+        absolute: event.clientX,
         relative: position / width
     };
 }
diff --git a/src/util/offset/boundingRect.ts b/src/util/offset/boundingRect.ts
new file mode 100644
index 0000000..31ef1e4
--- /dev/null
+++ b/src/util/offset/boundingRect.ts
@@ -0,0 +1,10 @@
+export interface BoundingRect {
+    readonly x: number;
+    readonly y: number;
+    readonly width: number;
+    readonly height: number;
+    readonly top: number;
+    readonly right: number;
+    readonly bottom: number;
+    readonly left: number;
+}
diff --git a/src/util/offset/useWidth.ts b/src/util/offset/useBoundingRect.ts
similarity index 74%
rename from src/util/offset/useWidth.ts
rename to src/util/offset/useBoundingRect.ts
index 1321cb5..fd4aa53 100644
--- a/src/util/offset/useWidth.ts
+++ b/src/util/offset/useBoundingRect.ts
@@ -1,8 +1,9 @@
 import {useEffect, useState} from "react";
 import ResizeObserver from 'resize-observer-polyfill';
+import {BoundingRect} from "./boundingRect";
 
-export const useWidth = <T extends HTMLElement>(it: T | null) => {
-    const [width, setWidth] = useState<number>(0);
+export const useBoundingRect = <T extends HTMLElement>(it: T | null) => {
+    const [rect, setRect] = useState<BoundingRect | null>(null);
     useEffect(() => {
         if (it !== null) {
             let animationFrame: number | null = null;
@@ -12,7 +13,7 @@ export const useWidth = <T extends HTMLElement>(it: T | null) => {
             const observer = new ResizeObserver(([element]) => {
                 animationFrame = window.requestAnimationFrame(() => {
                     animationFrame = null;
-                    setWidth(element.contentRect.width || 0);
+                    setRect(it.getBoundingClientRect() || null);
                 });
             })
             observer.observe(it);
@@ -24,5 +25,5 @@ export const useWidth = <T extends HTMLElement>(it: T | null) => {
             }
         }
     }, [it]);
-    return width;
+    return rect;
 }
diff --git a/src/util/offset/useOffset.ts b/src/util/offset/useOffset.ts
deleted file mode 100644
index 811ed38..0000000
--- a/src/util/offset/useOffset.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import {useWidth} from "./useWidth";
-
-export const useOffset = <T extends HTMLElement, U extends HTMLElement>(parent: T | null, child: U | null, position: number) => {
-    const parentWidth = useWidth(parent);
-    const childWidth = useWidth(child);
-
-    const offset = parentWidth * position - childWidth / 2;
-    return Math.max(0, Math.min(offset, parentWidth - childWidth));
-}
diff --git a/src/util/offset/useOffsetAbsolute.ts b/src/util/offset/useOffsetAbsolute.ts
new file mode 100644
index 0000000..b1b545e
--- /dev/null
+++ b/src/util/offset/useOffsetAbsolute.ts
@@ -0,0 +1,14 @@
+import {useBoundingRect} from "./useBoundingRect";
+
+export const useOffsetAbsolute = <T extends HTMLElement, U extends HTMLElement>(parent: T | null, child: U | null, position: number) => {
+    const parentRect = useBoundingRect(parent);
+    const childRect = useBoundingRect(child);
+
+    if (parentRect === null || childRect === null) {
+        return 0;
+    }
+
+    const offset = position - parentRect.left - childRect.width / 2;
+    const maximum = parentRect.width - childRect.width;
+    return Math.max(0, Math.min(offset, maximum));
+}
diff --git a/src/util/offset/useOffsetAbsoluteRef.ts b/src/util/offset/useOffsetAbsoluteRef.ts
new file mode 100644
index 0000000..f65f807
--- /dev/null
+++ b/src/util/offset/useOffsetAbsoluteRef.ts
@@ -0,0 +1,10 @@
+import {useState} from "react";
+import {useOffsetAbsolute} from "./useOffsetAbsolute";
+
+export const useOffsetAbsoluteRef = <T extends HTMLElement, U extends HTMLElement>(position: number):
+    [(it: T | null) => void, (it: U | null) => void, number] => {
+    const [parent, setParent] = useState<T | null>(null);
+    const [child, setChild] = useState<U | null>(null);
+
+    return [setParent, setChild, useOffsetAbsolute(parent, child, position)];
+}
diff --git a/src/util/offset/useOffsetRef.ts b/src/util/offset/useOffsetRef.ts
deleted file mode 100644
index cad16f6..0000000
--- a/src/util/offset/useOffsetRef.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import {useState} from "react";
-import {useOffset} from "./useOffset";
-
-export const useOffsetRef = <T extends HTMLElement, U extends HTMLElement>(position: number):
-    [(it: T | null) => void, (it: U | null) => void, number] => {
-    const [parent, setParent] = useState<T | null>(null);
-    const [child, setChild] = useState<U | null>(null);
-
-    return [setParent, setChild, useOffset(parent, child, position)];
-}
diff --git a/src/util/offset/useOffsetRelative.ts b/src/util/offset/useOffsetRelative.ts
new file mode 100644
index 0000000..2acfa0c
--- /dev/null
+++ b/src/util/offset/useOffsetRelative.ts
@@ -0,0 +1,14 @@
+import {useBoundingRect} from "./useBoundingRect";
+
+export const useOffsetRelative = <T extends HTMLElement, U extends HTMLElement>(parent: T | null, child: U | null, position: number) => {
+    const parentRect = useBoundingRect(parent);
+    const childRect = useBoundingRect(child);
+
+    if (parentRect === null || childRect === null) {
+        return 0;
+    }
+
+    const offset = (position * parentRect.width) - parentRect.left;
+    const maximum = parentRect.width - childRect.width;
+    return Math.max(0, Math.min(offset, maximum));
+}
diff --git a/src/util/offset/useOffsetRelativeRef.ts b/src/util/offset/useOffsetRelativeRef.ts
new file mode 100644
index 0000000..707c5b4
--- /dev/null
+++ b/src/util/offset/useOffsetRelativeRef.ts
@@ -0,0 +1,10 @@
+import {useState} from "react";
+import {useOffsetAbsolute} from "./useOffsetAbsolute";
+
+export const useOffsetRelativeRef = <T extends HTMLElement, U extends HTMLElement>(position: number):
+    [(it: T | null) => void, (it: U | null) => void, number] => {
+    const [parent, setParent] = useState<T | null>(null);
+    const [child, setChild] = useState<U | null>(null);
+
+    return [setParent, setChild, useOffsetAbsolute(parent, child, position)];
+}
-- 
GitLab