timeline improvements

- Pan timeline when cursor moves out of timeline window
- normalize mouse wheel
pull/276/head
Mikael Finstad 2020-03-05 18:32:02 +08:00
rodzic 3b982c1b48
commit 939860d050
2 zmienionych plików z 234 dodań i 26 usunięć

Wyświetl plik

@ -5,6 +5,7 @@ import debounce from 'lodash/debounce';
import TimelineSeg from './TimelineSeg';
import InverseCutSegment from './InverseCutSegment';
import normalizeWheel from './normalizeWheel';
import { timelineBackground } from './colors';
@ -14,11 +15,11 @@ import { getSegColors } from './util';
const hammerOptions = { recognizers: {} };
const Waveform = memo(({ calculateTimelinePos, durationSafe, waveform, zoom, timelineHeight }) => {
const Waveform = memo(({ calculateTimelinePercent, durationSafe, waveform, zoom, timelineHeight }) => {
const imgRef = useRef();
const [style, setStyle] = useState({ display: 'none' });
const leftPos = calculateTimelinePos(waveform.from);
const leftPos = calculateTimelinePercent(waveform.from);
const toTruncated = Math.min(waveform.to, durationSafe);
@ -51,15 +52,24 @@ const Timeline = memo(({
const offsetCurrentTime = (getCurrentTime() || 0) + startTimeOffset;
const calculateTimelinePos = useCallback((time) => (time !== undefined && time < durationSafe ? `${(time / durationSafe) * 100}%` : undefined), [durationSafe]);
const calculateTimelinePos = useCallback((time) => (time !== undefined && time < durationSafe ? time / durationSafe : undefined), [durationSafe]);
const calculateTimelinePercent = useCallback((time) => {
const pos = calculateTimelinePos(time);
return pos !== undefined ? `${pos * 100}%` : undefined;
}, [calculateTimelinePos]);
const currentTimePos = useMemo(() => calculateTimelinePos(playerTime), [calculateTimelinePos, playerTime]);
const commandedTimePos = useMemo(() => calculateTimelinePos(commandedTime), [calculateTimelinePos, commandedTime]);
const currentTimePercent = useMemo(() => calculateTimelinePercent(playerTime), [calculateTimelinePercent, playerTime]);
const commandedTimePercent = useMemo(() => calculateTimelinePercent(commandedTime), [calculateTimelinePercent, commandedTime]);
const zoomed = zoom > 1;
const currentTimePosPixels = useMemo(() => {
const pos = calculateTimelinePos(playerTime);
if (pos != null) return pos * zoom * timelineScrollerRef.current.offsetWidth;
return undefined;
}, [calculateTimelinePos, playerTime, zoom]);
const currentTimeWidth = 1;
// Prevent it from overflowing (and causing scroll) when end of timeline
const zoomWindowStartTime = timelineScrollerRef.current
? (timelineScrollerRef.current.scrollLeft / (timelineScrollerRef.current.offsetWidth * zoom)) * duration
: 0;
useEffect(() => {
timelineScrollerSkipEventDebounce.current = debounce(() => {
@ -67,10 +77,30 @@ const Timeline = memo(({
}, 1000);
}, []);
// Keep cursor in view while zooming
useEffect(() => {
function suppressScrollerEvents() {
timelineScrollerSkipEventRef.current = true;
timelineScrollerSkipEventDebounce.current();
}
// Pan timeline when cursor moves out of timeline window
useEffect(() => {
if (currentTimePosPixels == null || timelineScrollerSkipEventRef.current) return;
if (currentTimePosPixels > timelineScrollerRef.current.scrollLeft + timelineScrollerRef.current.offsetWidth) {
suppressScrollerEvents();
timelineScrollerRef.current.scrollLeft += timelineScrollerRef.current.offsetWidth * 0.9;
} else if (currentTimePosPixels < timelineScrollerRef.current.scrollLeft) {
suppressScrollerEvents();
timelineScrollerRef.current.scrollLeft -= timelineScrollerRef.current.offsetWidth * 0.9;
}
}, [currentTimePosPixels, zoomWindowStartTime]);
const currentTimeWidth = 1;
// Keep cursor in middle while zooming
useEffect(() => {
suppressScrollerEvents();
if (zoom > 1) {
const zoomedTargetWidth = timelineScrollerRef.current.offsetWidth * zoom;
@ -78,21 +108,32 @@ const Timeline = memo(({
}
}, [zoom, durationSafe, getCurrentTime]);
// Keep cursor in view while scrolling
const onTimelineScroll = useCallback((e) => {
if (!zoomed) return;
const zoomWindowStartTime = timelineScrollerRef.current
? (timelineScrollerRef.current.scrollLeft / (timelineScrollerRef.current.offsetWidth * zoom)) * duration
: 0;
useEffect(() => {
const cancelWheel = (event) => event.preventDefault();
const scroller = timelineScrollerRef.current;
scroller.addEventListener('wheel', cancelWheel, { passive: false });
return () => {
scroller.removeEventListener('wheel', cancelWheel);
};
}, []);
const onTimelineScroll = useCallback(() => {
onZoomWindowStartTimeChange(zoomWindowStartTime);
}, [zoomWindowStartTime, onZoomWindowStartTimeChange]);
// Keep cursor in middle while scrolling
/* const onTimelineScroll = useCallback((e) => {
onZoomWindowStartTimeChange(zoomWindowStartTime);
if (timelineScrollerSkipEventRef.current) return;
if (!zoomed || timelineScrollerSkipEventRef.current) return;
seekAbs((((e.target.scrollLeft + (timelineScrollerRef.current.offsetWidth * 0.5))
/ (timelineScrollerRef.current.offsetWidth * zoom)) * duration));
}, [duration, seekAbs, zoomed, zoom, onZoomWindowStartTimeChange]);
}, [duration, seekAbs, zoomed, zoom, zoomWindowStartTime, onZoomWindowStartTimeChange]); */
const handleTap = useCallback((e) => {
const target = timelineWrapperRef.current;
@ -102,11 +143,14 @@ const Timeline = memo(({
}, [duration, seekAbs]);
const onWheel = useCallback((e) => {
const combinedDelta = e.deltaX + e.deltaY;
const { pixelX, pixelY } = normalizeWheel(e);
// console.log({ spinX, spinY, pixelX, pixelY });
if (e.ctrlKey) {
zoomRel(-e.deltaY / 15);
} else if (!zoomed) seekRel(combinedDelta / 15);
}, [seekRel, zoomRel, zoomed]);
zoomRel(-pixelY / 10);
} else {
seekRel((pixelX + pixelY) / 15);
}
}, [seekRel, zoomRel]);
return (
<Hammer
@ -124,7 +168,7 @@ const Timeline = memo(({
>
{waveformEnabled && shouldShowWaveform && waveform && (
<Waveform
calculateTimelinePos={calculateTimelinePos}
calculateTimelinePosPercent={calculateTimelinePercent}
durationSafe={durationSafe}
waveform={waveform}
zoom={zoom}
@ -150,8 +194,8 @@ const Timeline = memo(({
style={{ height: timelineHeight, width: `${zoom * 100}%`, position: 'relative', backgroundColor: timelineBackground }}
ref={timelineWrapperRef}
>
{currentTimePos !== undefined && <motion.div transition={{ type: 'spring', damping: 70, stiffness: 800 }} animate={{ left: currentTimePos }} style={{ position: 'absolute', bottom: 0, top: 0, zIndex: 3, backgroundColor: 'rgba(255,255,255,0.6)', width: currentTimeWidth, pointerEvents: 'none' }} />}
{commandedTimePos !== undefined && <div style={{ left: commandedTimePos, position: 'absolute', bottom: 0, top: 0, zIndex: 4, backgroundColor: 'white', width: currentTimeWidth, pointerEvents: 'none' }} />}
{currentTimePercent !== undefined && <motion.div transition={{ type: 'spring', damping: 70, stiffness: 800 }} animate={{ left: currentTimePercent }} style={{ position: 'absolute', bottom: 0, top: 0, zIndex: 3, backgroundColor: 'rgba(255,255,255,0.6)', width: currentTimeWidth, pointerEvents: 'none' }} />}
{commandedTimePercent !== undefined && <div style={{ left: commandedTimePercent, position: 'absolute', bottom: 0, top: 0, zIndex: 4, backgroundColor: 'white', width: currentTimeWidth, pointerEvents: 'none' }} />}
{apparentCutSegments.map((seg, i) => {
const {
@ -172,7 +216,6 @@ const Timeline = memo(({
cutStart={seg.start}
cutEnd={seg.end}
invertCutSegments={invertCutSegments}
zoomed={zoomed}
/>
);
})}

Wyświetl plik

@ -0,0 +1,165 @@
// Taken from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
/* eslint-disable */
/**
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule normalizeWheel
* @typechecks
*/
'use strict';
// Reasonable defaults
var PIXEL_STEP = 10;
var LINE_HEIGHT = 40;
var PAGE_HEIGHT = 800;
/**
* Mouse wheel (and 2-finger trackpad) support on the web sucks. It is
* complicated, thus this doc is long and (hopefully) detailed enough to answer
* your questions.
*
* If you need to react to the mouse wheel in a predictable way, this code is
* like your bestest friend. * hugs *
*
* As of today, there are 4 DOM event types you can listen to:
*
* 'wheel' -- Chrome(31+), FF(17+), IE(9+)
* 'mousewheel' -- Chrome, IE(6+), Opera, Safari
* 'MozMousePixelScroll' -- FF(3.5 only!) (2010-2013) -- don't bother!
* 'DOMMouseScroll' -- FF(0.9.7+) since 2003
*
* So what to do? The is the best:
*
* normalizeWheel.getEventType();
*
* In your event callback, use this code to get sane interpretation of the
* deltas. This code will return an object with properties:
*
* spinX -- normalized spin speed (use for zoom) - x plane
* spinY -- " - y plane
* pixelX -- normalized distance (to pixels) - x plane
* pixelY -- " - y plane
*
* Wheel values are provided by the browser assuming you are using the wheel to
* scroll a web page by a number of lines or pixels (or pages). Values can vary
* significantly on different platforms and browsers, forgetting that you can
* scroll at different speeds. Some devices (like trackpads) emit more events
* at smaller increments with fine granularity, and some emit massive jumps with
* linear speed or acceleration.
*
* This code does its best to normalize the deltas for you:
*
* - spin is trying to normalize how far the wheel was spun (or trackpad
* dragged). This is super useful for zoom support where you want to
* throw away the chunky scroll steps on the PC and make those equal to
* the slow and smooth tiny steps on the Mac. Key data: This code tries to
* resolve a single slow step on a wheel to 1.
*
* - pixel is normalizing the desired scroll delta in pixel units. You'll
* get the crazy differences between browsers, but at least it'll be in
* pixels!
*
* - positive value indicates scrolling DOWN/RIGHT, negative UP/LEFT. This
* should translate to positive value zooming IN, negative zooming OUT.
* This matches the newer 'wheel' event.
*
* Why are there spinX, spinY (or pixels)?
*
* - spinX is a 2-finger side drag on the trackpad, and a shift + wheel turn
* with a mouse. It results in side-scrolling in the browser by default.
*
* - spinY is what you expect -- it's the classic axis of a mouse wheel.
*
* - I dropped spinZ/pixelZ. It is supported by the DOM 3 'wheel' event and
* probably is by browsers in conjunction with fancy 3D controllers .. but
* you know.
*
* Implementation info:
*
* Examples of 'wheel' event if you scroll slowly (down) by one step with an
* average mouse:
*
* OS X + Chrome (mouse) - 4 pixel delta (wheelDelta -120)
* OS X + Safari (mouse) - N/A pixel delta (wheelDelta -12)
* OS X + Firefox (mouse) - 0.1 line delta (wheelDelta N/A)
* Win8 + Chrome (mouse) - 100 pixel delta (wheelDelta -120)
* Win8 + Firefox (mouse) - 3 line delta (wheelDelta -120)
*
* On the trackpad:
*
* OS X + Chrome (trackpad) - 2 pixel delta (wheelDelta -6)
* OS X + Firefox (trackpad) - 1 pixel delta (wheelDelta N/A)
*
* On other/older browsers.. it's more complicated as there can be multiple and
* also missing delta values.
*
* The 'wheel' event is more standard:
*
* http://www.w3.org/TR/DOM-Level-3-Events/#events-wheelevents
*
* The basics is that it includes a unit, deltaMode (pixels, lines, pages), and
* deltaX, deltaY and deltaZ. Some browsers provide other values to maintain
* backward compatibility with older events. Those other values help us
* better normalize spin speed. Example of what the browsers provide:
*
* | event.wheelDelta | event.detail
* ------------------+------------------+--------------
* Safari v5/OS X | -120 | 0
* Safari v5/Win7 | -120 | 0
* Chrome v17/OS X | -120 | 0
* Chrome v17/Win7 | -120 | 0
* IE9/Win7 | -120 | undefined
* Firefox v4/OS X | undefined | 1
* Firefox v4/Win7 | undefined | 3
*
*/
function normalizeWheel(/*object*/ event) /*object*/ {
var sX = 0, sY = 0, // spinX, spinY
pX = 0, pY = 0; // pixelX, pixelY
// Legacy
if ('detail' in event) { sY = event.detail; }
if ('wheelDelta' in event) { sY = -event.wheelDelta / 120; }
if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120; }
if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; }
// side scrolling on FF with DOMMouseScroll
if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) {
sX = sY;
sY = 0;
}
pX = sX * PIXEL_STEP;
pY = sY * PIXEL_STEP;
if ('deltaY' in event) { pY = event.deltaY; }
if ('deltaX' in event) { pX = event.deltaX; }
if ((pX || pY) && event.deltaMode) {
if (event.deltaMode == 1) { // delta in LINE units
pX *= LINE_HEIGHT;
pY *= LINE_HEIGHT;
} else { // delta in PAGE units
pX *= PAGE_HEIGHT;
pY *= PAGE_HEIGHT;
}
}
// Fall-back if spin cannot be determined
if (pX && !sX) { sX = (pX < 1) ? -1 : 1; }
if (pY && !sY) { sY = (pY < 1) ? -1 : 1; }
return { spinX : sX,
spinY : sY,
pixelX : pX,
pixelY : pY };
}
module.exports = normalizeWheel;