add comment for hooks

This commit is contained in:
gantiro 2025-07-15 22:56:56 +07:00
parent b266678fe4
commit 52f003f25a
3 changed files with 96 additions and 19 deletions

View File

@ -1,13 +1,32 @@
import { useEffect } from "react";
/**
* useDisableBrowserZoom
*
* This React hook prevents users from zooming in/out the browser window using keyboard shortcuts,
* mouse wheel, or gesture events (such as pinch-to-zoom on trackpads or touch devices).
*
* Typical use case: apps with fixed-size UIs, custom editors, or when zooming would break layout.
*
* NOTE: Disabling browser zoom may negatively affect accessibility and user experience.
* Use with caution, especially for public-facing applications.
*/
export const useDisableBrowserZoom = () => {
useEffect(() => {
/**
* Prevents browser zoom when user holds Ctrl (or Cmd on Mac) and scrolls the mouse wheel.
*/
const handleWheel = (e: WheelEvent) => {
if (e.ctrlKey) {
e.preventDefault();
}
};
/**
* Prevents browser zoom via keyboard shortcuts:
* Ctrl/Cmd + '+', '-', '=', or '0' (reset zoom)
* Some keyboards emit '=' for '+' (without Shift).
*/
const handleKeyDown = (e: KeyboardEvent) => {
if (e.ctrlKey) {
const key = e.key;
@ -17,23 +36,22 @@ export const useDisableBrowserZoom = () => {
}
};
/**
* Prevents pinch-zoom gestures on supported browsers (mainly Safari/macOS/iOS).
* gesturestart, gesturechange, and gestureend are non-standard and not supported everywhere.
*/
const handleGesture = (e: Event) => {
e.preventDefault();
};
// Attach event listeners (passive: false is required for preventDefault to work)
window.addEventListener("wheel", handleWheel, { passive: false });
window.addEventListener("keydown", handleKeyDown, { passive: false });
window.addEventListener("gesturestart", handleGesture, { passive: false });
window.addEventListener("gesturechange", handleGesture, { passive: false });
window.addEventListener("gestureend", handleGesture, { passive: false });
window.addEventListener("gesturestart", handleGesture, {
passive: false,
});
window.addEventListener("gesturechange", handleGesture, {
passive: false,
});
window.addEventListener("gestureend", handleGesture, {
passive: false,
});
// Cleanup listeners on unmount to avoid memory leaks
return () => {
window.removeEventListener("wheel", handleWheel);
window.removeEventListener("keydown", handleKeyDown);

View File

@ -11,6 +11,13 @@ interface UseTimelinePlayheadProps {
rulerScrollRef: React.RefObject<HTMLDivElement>;
}
/**
* useTimelinePlayhead
*
* Custom hook to manage playhead (scrubbing) logic for a timeline editor.
* Handles mouse interaction for timeline ruler, calculates time from mouse,
* and enables smooth scrubbing without unnecessary re-renders.
*/
export function useTimelinePlayhead({
currentTime,
duration,
@ -19,17 +26,35 @@ export function useTimelinePlayhead({
rulerRef,
rulerScrollRef,
}: UseTimelinePlayheadProps) {
// Get current project info (especially FPS) from global store
const { activeProject } = useProjectStore();
// Ref to track if currently scrubbing
const isScrubbingRef = useRef(false);
// Ref to hold the playhead time while scrubbing (does not trigger re-render)
const scrubTimeRef = useRef<number | null>(null);
// State to force re-render when scrubbing ends
const [_, forceRerender] = useState(0);
/**
* Determines the actual playhead position:
* - If scrubbing, use the value in scrubTimeRef
* - Otherwise, use the currentTime (controlled by external state)
*/
const playheadPosition =
isScrubbingRef.current && scrubTimeRef.current !== null
? scrubTimeRef.current
: currentTime;
/**
* Calculate timeline time (in seconds) based on mouse X position.
* - Gets bounding rect of the ruler
* - Adjusts for scroll position if ruler is scrollable
* - Converts X pixel offset to seconds using current zoom level
* - Snaps to nearest frame using FPS
*/
const getTimeFromMouse = useCallback(
(e: MouseEvent | React.MouseEvent) => {
const ruler = rulerRef.current;
@ -43,6 +68,7 @@ export function useTimelinePlayhead({
const scrollLeft = scrollArea.scrollLeft;
const x = e.clientX - rect.left + scrollLeft;
// Calculate how many pixels represent one second, depending on zoom
const pixelsPerSecond = 50 * zoomLevel;
const rawTime = Math.max(0, Math.min(duration, x / pixelsPerSecond));
const time = snapTimeToFrame(rawTime, activeProject?.fps || 30);
@ -51,6 +77,13 @@ export function useTimelinePlayhead({
[rulerRef, rulerScrollRef, duration, zoomLevel, activeProject?.fps]
);
/**
* Handle mouse down event on the ruler:
* - Starts scrubbing and updates playhead immediately
* - Registers mousemove/mouseup events to allow scrubbing
* - Updates time as mouse moves and seeks to new time
* - Cleans up listeners and triggers re-render when scrubbing ends
*/
const handleRulerMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
@ -61,6 +94,7 @@ export function useTimelinePlayhead({
scrubTimeRef.current = time;
seek(time);
// Mouse move handler: update playhead and seek to new time
const onMouseMove = (e: MouseEvent) => {
const t = getTimeFromMouse(e);
if (t !== scrubTimeRef.current) {
@ -69,6 +103,7 @@ export function useTimelinePlayhead({
}
};
// Mouse up handler: stop scrubbing, cleanup, and force re-render
const onMouseUp = () => {
isScrubbingRef.current = false;
window.removeEventListener("mousemove", onMouseMove);
@ -76,6 +111,7 @@ export function useTimelinePlayhead({
forceRerender((v) => v + 1);
};
// Attach listeners to window for drag outside the ruler area
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
},
@ -83,7 +119,7 @@ export function useTimelinePlayhead({
);
return {
playheadPosition,
handleRulerMouseDown,
playheadPosition, // Current playhead position (real-time while scrubbing)
handleRulerMouseDown, // Attach to the ruler's onMouseDown
};
}

View File

@ -15,15 +15,30 @@ interface UseTimelineZoomReturn {
handleWheel: (e: React.WheelEvent) => void;
}
/**
* useTimelineZoom
*
* Custom hook to manage zoom logic for a timeline component.
* Handles zoom state, step calculation, level changes, and mouse wheel zooming (with ctrl/meta).
*/
export function useTimelineZoom(): UseTimelineZoomReturn {
// Current zoom level (1 = default)
const [zoomLevel, setZoomLevel] = useState(1);
/**
* Calculate the current zoom step based on the zoom level and a base step constant.
* Ensures minimum step is 1 (prevents zero or negative step).
*/
const zoomStep = useMemo(
() =>
Math.max(1, Math.round(zoomLevel / TIMELINE_CONSTANTS.ZOOM_STEP_BASE)),
[zoomLevel]
);
/**
* Update the zoom level using a given step.
* The zoom level is clamped to a minimum value defined in constants.
*/
const handleChangeZoomStep = useCallback((newStep: number) => {
setZoomLevel(
Math.max(
@ -33,6 +48,10 @@ export function useTimelineZoom(): UseTimelineZoomReturn {
);
}, []);
/**
* Update the zoom level directly.
* The new value is clamped between the defined minimum and maximum levels.
*/
const handleChangeZoomLevel = useCallback((newLevel: number) => {
setZoomLevel(
Math.max(
@ -42,7 +61,10 @@ export function useTimelineZoom(): UseTimelineZoomReturn {
);
}, []);
// Mouse wheel zoom, ctrl/meta gesture
/**
* Handle zooming using mouse wheel + ctrl/meta key (like browser zoom).
* Prevents default browser behavior and updates zoom level accordingly.
*/
const handleWheel = useCallback((e: React.WheelEvent) => {
if (e.ctrlKey || e.metaKey) {
e.preventDefault?.();
@ -59,12 +81,13 @@ export function useTimelineZoom(): UseTimelineZoomReturn {
}
}, []);
// Return all handlers and state values for use in timeline components
return {
zoomLevel,
zoomStep,
setZoomLevel,
handleChangeZoomStep,
handleChangeZoomLevel,
handleWheel,
zoomLevel, // Current zoom level
zoomStep, // Current zoom step (calculated from zoomLevel)
setZoomLevel, // Directly set the zoom level (accepts value or updater function)
handleChangeZoomStep, // Change zoom by step value
handleChangeZoomLevel, // Change zoom by level value
handleWheel, // Handler for mouse wheel zooming
};
}