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