Skip to content

Instantly share code, notes, and snippets.

@jakearchibald
Last active April 15, 2024 07:54
Show Gist options
  • Save jakearchibald/cb03f15670817001b1157e62a076fe95 to your computer and use it in GitHub Desktop.
Save jakearchibald/cb03f15670817001b1157e62a076fe95 to your computer and use it in GitHub Desktop.
export function animationInterval(ms, signal, callback) {
// Prefer currentTime, as it'll better sync animtions queued in the
// same frame, but if it isn't supported, performance.now() is fine.
const start = document.timeline ? document.timeline.currentTime : performance.now();
function frame(time) {
if (signal.aborted) return;
callback(time);
scheduleFrame(time);
}
function scheduleFrame(time) {
const elapsed = time - start;
const roundedElapsed = Math.round(elapsed / ms) * ms;
const targetNext = start + roundedElapsed + ms;
const delay = targetNext - performance.now();
setTimeout(() => requestAnimationFrame(frame), delay);
}
scheduleFrame(start);
}
// Usage
import { animationInterval } from './1.js';
const controller = new AbortController();
// Create an animation callback every second:
animationInterval(1000, controller.signal, time => {
console.log('tick!', time);
});
// And to stop it:
controller.abort();
@Norfeldt
Copy link

Norfeldt commented Jun 7, 2021

this is my typescript version of it @mfbx9da4

useAnimationInterval.ts

import React from 'react'

function animationInterval(ms: number, signal: AbortSignal, callback: (time: number) => void) {
  const start = document?.timeline?.currentTime || performance.now()

  function frame(time: number) {
    if (signal.aborted) return
    callback(time)
    scheduleFrame(time)
  }

  function scheduleFrame(time: number) {
    const elapsed = time - start
    const roundedElapsed = Math.round(elapsed / ms) * ms
    const targetNext = start + roundedElapsed + ms
    const delay = targetNext - performance.now()
    setTimeout(() => requestAnimationFrame(frame), delay)
  }

  scheduleFrame(start)
}

export function useAnimationInterval(ms: number, callback: (time: number) => void) {
  const callbackRef = React.useRef(callback)
  React.useEffect(() => {
    callbackRef.current = callback
  }, [callback])

  React.useEffect(() => {
    const controller = new AbortController()
    animationInterval(ms, controller.signal, callbackRef.current)
    return () => controller.abort()
  }, [ms])
}

// usage
// useAnimationFrame(1000, (time) => {
//   console.log(time)
//   setVisible((x) => !x)
// })

@ghana7989
Copy link

I am new to these core JS features can someone help me understand what exactly is a AbortController and controller.signal does?
Or at least provide a resource to learn Thanks

@delucis
Copy link

delucis commented Aug 23, 2021

@mfbx9da4 Thanks for the React hooks version! Just ran into a subtle bug with it that might be worth flagging:

    animationInterval(ms, controller.signal, callbackRef.current)

Here the current callbackRef is passed by reference to animationInterval, so animationInterval will keep using the callback that was current when it was called, rather than updating when the ref changes as desired.

To ensure the latest callback is always used, we can call the current ref like this instead:

    animationInterval(ms, controller.signal, (time) => callbackRef.current(time))

@mfbx9da4
Copy link

mfbx9da4 commented Aug 24, 2021

Ooo great point - good catch! Will update

@avi12
Copy link

avi12 commented Dec 15, 2021

I was wondering how Jake's function could be modified such that it could run in the background

@Sepush
Copy link

Sepush commented Dec 24, 2021

still have issue of double frame

@zizifn
Copy link

zizifn commented Jan 1, 2022

so if I just want the counter for second level precision, I just need set setInterval delay less than 1000ms, should be working fine?

    const start = Date.now();
    setInterval(() => {
        const gaps = (Date.now() - start);
        const seconds = Math.floor(gaps / 1000);
        updateUI(seconds)
    }, 900); // change less than 1000ms

@Sepush
Copy link

Sepush commented Jan 1, 2022

yeah finally I did it like this

@stevengrimaldo
Copy link

Is this gist something i can use to create a countdown timer with? like to a specific date. Or would this not be for something like that?

@jakearchibald
Copy link
Author

@zizifn no, that still drifts. That's why the code in this gist is more complicated.

@jakearchibald
Copy link
Author

@stevengrimaldo

Is this gist something i can use to create a countdown timer with?

Yes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment