Created
June 12, 2022 16:43
-
-
Save fuweichin/eacb356933be9243bac02c08505f9958 to your computer and use it in GitHub Desktop.
animate with specific frame rate (fps), based on requestAnimationFrame instead of setInterval/setTimeout
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function filterNums(nums, jitter = 0.2, downJitter = 1 - 1 / (1 + jitter)) { | |
let len = nums.length; | |
let mid = Math.floor(len % 2 === 0 ? len / 2 : (len - 1) / 2), low = mid, high = mid; | |
let lower = true, higher = true; | |
let sum = nums[mid], count = 1; | |
for (let i = 1, j, num; i <= mid; i += 1) { | |
if (higher) { | |
j = mid + i; | |
if (j === len) | |
break; | |
num = nums[j]; | |
if (num < (sum / count) * (1 + jitter)) { | |
sum += num; | |
count += 1; | |
high = j; | |
} else { | |
higher = false; | |
} | |
} | |
if (lower) { | |
j = mid - i; | |
num = nums[j]; | |
if (num > (sum / count) * (1 - downJitter)) { | |
sum += num; | |
count += 1; | |
low = j; | |
} else { | |
lower = false; | |
} | |
} | |
} | |
return nums.slice(low, high + 1); | |
} | |
function snapToOrRound(n, values, distance = 3) { | |
for (let i = 0, v; i < values.length; i += 1) { | |
v = values[i]; | |
if (n >= v - distance && n <= v + distance) { | |
return v; | |
} | |
} | |
return Math.round(n); | |
} | |
function detectAnimationFrameRate(numIntervals = 6) { | |
if (typeof numIntervals !== 'number' || !isFinite(numIntervals) || numIntervals < 2) { | |
throw new RangeError('Argument numIntervals should be a number not less than 2'); | |
} | |
return new Promise((resolve) => { | |
let num = Math.floor(numIntervals); | |
let numFrames = num + 1; | |
let last; | |
let intervals = []; | |
let i = 0; | |
let tick = () => { | |
let now = performance.now(); | |
i += 1; | |
if (i < numFrames) { | |
requestAnimationFrame(tick); | |
} | |
if (i === 1) { | |
last = now; | |
} else { | |
intervals.push(now - last); | |
last = now; | |
if (i === numFrames) { | |
let compareFn = (a, b) => a < b ? -1 : a > b ? 1 : 0; | |
let sortedIntervals = intervals.slice().sort(compareFn); | |
let selectedIntervals = filterNums(sortedIntervals, 0.2, 0.1); | |
let selectedDuration = selectedIntervals.reduce((s, n) => s + n, 0); | |
let seletedFrameRate = 1000 / (selectedDuration / selectedIntervals.length); | |
let finalFrameRate = snapToOrRound(seletedFrameRate, [60, 120, 90, 30], 5); | |
resolve(finalFrameRate); | |
} | |
} | |
}; | |
requestAnimationFrame(() => { | |
requestAnimationFrame(tick); | |
}); | |
}); | |
} | |
function buildFrameBitSet(animationFrameRate, desiredFrameRate) { | |
let bitSet = new Uint8Array(animationFrameRate); | |
let ratio = desiredFrameRate / animationFrameRate; | |
if (ratio >= 1) | |
return bitSet.fill(1); | |
for (let i = 0, prev = -1, curr; i < animationFrameRate; i += 1, prev = curr) { | |
curr = Math.floor(i * ratio); | |
bitSet[i] = (curr !== prev) ? 1 : 0; | |
} | |
return bitSet; | |
} | |
export {detectAnimationFrameRate, buildFrameBitSet}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div> | |
Animation Frame Rate: <span id="animationFrameRate">--</span> | |
</div> | |
<div> | |
Desired Frame Rate: <input id="frameRateInput" type="range" min="1" max="60" step="1" list="frameRates" /> | |
<output id="frameRateOutput"></output> | |
<datalist id="frameRates"> | |
<option>15</option> | |
<option>24</option> | |
<option>30</option> | |
<option>48</option> | |
<option>60</option> | |
</datalist> | |
</div> | |
<div> | |
Actual Frame Rate: <span id="actualFrameRate">--</span> | |
</div> | |
<canvas id="digitalClock" width="240" height="48"></canvas> | |
<script type="module"> | |
import {detectAnimationFrameRate, buildFrameBitSet} from './animation-utils.js'; | |
let $ = (s, c = document) => c.querySelector(s); | |
let $$ = (s, c = document) => Array.prototype.slice.call(c.querySelectorAll(s)); | |
async function main() { | |
let canvas = $('#digitalClock'); | |
let context2d = canvas.getContext('2d'); | |
await new Promise((resolve) => { | |
if (window.requestIdleCallback) { | |
requestIdleCallback(resolve, {timeout: 3000}); | |
} else { | |
setTimeout(resolve, 0, {didTimeout: false}); | |
} | |
}); | |
let animationFrameRate = await detectAnimationFrameRate(10); // 1. detect animation frame rate | |
let desiredFrameRate = 24; | |
let frameBits = buildFrameBitSet(animationFrameRate, desiredFrameRate); // 2. build a bit set | |
let handle; | |
let i = 0; | |
let count = 0, then, actualFrameRate = $('#actualFrameRate'); // debug-only | |
let draw = () => { | |
if (++i >= animationFrameRate) { // shoud use === if frameBits don't change dynamically | |
i = 0; | |
/* debug-only */ | |
let now = performance.now(); | |
let deltaT = now - then; | |
let fps = 1000 / (deltaT / count); | |
actualFrameRate.textContent = fps; | |
then = now; | |
count = 0; | |
} | |
if (frameBits[i] === 0) { // 3. lookup the bit set | |
handle = requestAnimationFrame(draw); | |
return; | |
} | |
count += 1; // debug-only | |
let d = new Date(); | |
let text = d.getHours().toString().padStart(2, '0') + ':' + | |
d.getMinutes().toString().padStart(2, '0') + ':' + | |
d.getSeconds().toString().padStart(2, '0') + '.' + | |
(d.getMilliseconds() / 10).toFixed(0).padStart(2, '0'); | |
context2d.fillStyle = '#000000'; | |
context2d.fillRect(0, 0, canvas.width, canvas.height); | |
context2d.font = '36px monospace'; | |
context2d.fillStyle = '#ffffff'; | |
context2d.fillText(text, 0, 36); | |
handle = requestAnimationFrame(draw); | |
}; | |
handle = requestAnimationFrame(() => { | |
then = performance.now(); | |
handle = requestAnimationFrame(draw); | |
}); | |
/* debug-only */ | |
$('#animationFrameRate').textContent = animationFrameRate; | |
let frameRateInput = $('#frameRateInput'); | |
let frameRateOutput = $('#frameRateOutput'); | |
frameRateInput.addEventListener('input', (e) => { | |
frameRateOutput.value = e.target.value; | |
}); | |
frameRateInput.max = animationFrameRate; | |
frameRateOutput.value = frameRateOutput.value = desiredFrameRate; | |
frameRateInput.addEventListener('change', (e) => { | |
desiredFrameRate = +e.target.value; | |
frameBits = buildFrameBitSet(animationFrameRate, desiredFrameRate); | |
}); | |
} | |
document.addEventListener('DOMContentLoaded', main); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment