Progress
ProgressEngine reports a 0..1 progress value on every frame, from one of two sources: a media
element (video or audio — it drives playback and tracks currentTime) or an arbitrary duration
in milliseconds (it owns the clock off the shared frame loop). Either way you get the same controls —
play, pause, reset — and the same events. play() continues from the current position, and
starts over from the beginning once complete, exactly like a native media element after ended.
One hook, two sources. The bars are written straight to the DOM from onProgress, so they animate at 60fps without re-rendering — with react-scan on, notice the bars never flash. The panels re-render only on a play/pause/complete transition.
useProgress(4000)No media — the engine advances a timeline off the shared frame loop and reports progress each frame.
useProgress(videoRef)Same hook, fed a <video> ref. Progress tracks currentTime; the buttons drive playback, and scrubbing the native controls updates the bar too.
One composable, two sources. The bars bind the reactive progress ref straight in the template — Vue's fine-grained reactivity updates just those bindings each frame, no component re-render.
useProgress(4000)No media — the engine advances a timeline off the shared frame loop and reports progress each frame.
useProgress(videoRef) Same composable, fed a <video> ref. Progress tracks currentTime; the buttons drive playback, and scrubbing the native controls updates the bar too.
High-frequency by design
Progress changes ~60 times a second, so neither adapter routes it through coarse component state. In
React the hook exposes a ref plus an onProgress callback you write straight to the DOM from —
zero re-renders per frame. In Vue it returns a reactive Ref<number> you can bind in the template:
fine-grained reactivity updates just those bindings. Only the coarse status (isRunning,
isComplete) is component state, and it updates on play/pause/complete transitions only — exactly
what a play/pause button needs.
useProgress(source, options?)
One hook returns everything: the per-frame progress, the coarse status, the controls, and the engine as an escape hatch.
import { useRef } from 'react';
import { useProgress } from '@60fps/ui-react/progress-engine';
function Player() {
const videoRef = useRef<HTMLVideoElement>(null);
const barRef = useRef<HTMLDivElement>(null);
// `source` is either a media ref or a duration in milliseconds.
const { isRunning, play, pause } = useProgress(videoRef, {
onProgress: (value) => {
// 60fps, no re-render: write it straight to the DOM.
barRef.current!.style.transform = `scaleX(${value})`;
},
onComplete: () => console.log('done')
});
return (
<>
<video ref={videoRef} src="/clip.mp4" muted playsInline />
<div ref={barRef} style={{ transformOrigin: 'left' }} />
<button onClick={isRunning ? pause : play}>{isRunning ? 'Pause' : 'Play'}</button>
</>
);
}
<script setup lang="ts">
import { ref } from 'vue';
import { useProgress } from '@60fps/ui-vue/progress-engine';
const video = ref<HTMLVideoElement | null>(null);
// `source` is either a media ref or a duration in milliseconds. `progress` is a reactive
// Ref<number> — bind it in the template, fine-grained reactivity handles the per-frame updates.
const { progress, isRunning, play, pause } = useProgress(video, {
onComplete: () => console.log('done')
});
</script>
<template>
<video ref="video" src="/clip.mp4" muted playsinline />
<div class="bar" :style="{ transform: `scaleX(${progress})`, transformOrigin: 'left' }" />
<button @click="isRunning ? pause() : play()">{{ isRunning ? 'Pause' : 'Play' }}</button>
</template>
| Argument | Type | Default | Description |
|---|---|---|---|
source | RefObject<HTMLMediaElement | null> | number | — | A media element ref (drives playback) or a duration in milliseconds. |
options.onProgress | (value: number) => void | — | Called every frame with 0..1. Runs at 60fps — keep it cheap, never set state here. |
options.onComplete | () => void | — | Called once when progress reaches the end. |
options.autoPlay | boolean | false | Start playing as soon as the source is ready. |
Return value
| Field | Type | Default | Description |
|---|---|---|---|
progressRef / progress | RefObject<number> (React) · Ref<number> (Vue) | — | The latest 0..1 value. React: read it imperatively, it never re-renders. Vue: reactive, bind it in the template. |
isRunning | boolean | — | Whether progress is currently advancing. Updates on transitions only, never per frame. |
isComplete | boolean | — | Whether progress has reached the end. |
play | () => void | — | Play from the current position — from the beginning when complete, like native media. |
pause | () => void | — | Hold at the current position. |
reset | () => void | — | Return to 0 and stop. |
engine | ProgressEngine | — | The underlying engine, for its events (engine.on(...)) or imperative use. |
Vanilla engine
The hook is a thin wrapper; you can drive ProgressEngine directly from any framework or none:
import { ProgressEngine } from '@60fps/ui-progress';
const engine = new ProgressEngine({ durationMs: 3000 });
engine.on('progress', (value) => (bar.style.transform = `scaleX(${value})`));
engine.on('complete', () => console.log('done'));
engine.play();