diff --git a/src/Timeline.jsx b/src/Timeline.jsx index a8a658a9..1ddc21b0 100644 --- a/src/Timeline.jsx +++ b/src/Timeline.jsx @@ -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 ( {waveformEnabled && shouldShowWaveform && waveform && ( - {currentTimePos !== undefined && } - {commandedTimePos !== undefined &&
} + {currentTimePercent !== undefined && } + {commandedTimePercent !== undefined &&
} {apparentCutSegments.map((seg, i) => { const { @@ -172,7 +216,6 @@ const Timeline = memo(({ cutStart={seg.start} cutEnd={seg.end} invertCutSegments={invertCutSegments} - zoomed={zoomed} /> ); })} diff --git a/src/normalizeWheel.js b/src/normalizeWheel.js new file mode 100644 index 00000000..126e7f56 --- /dev/null +++ b/src/normalizeWheel.js @@ -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;