@ginden/blinkstick-v2
Version:
Improved Blickstick API for Node.js
154 lines • 6.21 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.smoothFps = smoothFps;
const tsafe_1 = require("tsafe");
const simple_frame_1 = require("../frame/simple-frame");
const complex_frame_1 = require("../frame/complex-frame");
const wait_frame_1 = require("../frame/wait-frame");
/**
* Compute per-LED weighted average colour for the upcoming slice.
*
* While iterating we *do not* mutate the queue – that is handled by the caller.
*/
function buildAveragedFrame(queue, sliceDuration, lastKnownColours) {
// Resolve LED count first – favour information from ComplexFrame, then
// `lastKnownColours`, and finally fall back to 1 (uniform colour).
let ledCount = null;
for (const { frame } of queue) {
if (frame instanceof complex_frame_1.ComplexFrame) {
ledCount = frame.colors.length;
break;
}
}
if (ledCount === null && lastKnownColours) {
ledCount = lastKnownColours.length;
}
if (ledCount === null)
ledCount = 1; // uniform single LED assumption
// Running sums per LED per channel.
const sums = Array.from({ length: ledCount }, () => [0, 0, 0]);
let needed = sliceDuration;
let idx = 0;
while (needed > 0 && idx < queue.length) {
const { frame, remaining } = queue[idx];
const consume = Math.min(remaining, needed);
// Helper to accumulate given rgb(s):
const accumulate = (rgb) => {
if (Array.isArray(rgb[0])) {
// Complex – per LED array
const arr = rgb;
for (let led = 0; led < ledCount; led += 1) {
const col = arr[led] ?? (arr.length > 0 ? arr[arr.length - 1] : [0, 0, 0]);
sums[led][0] += col[0] * consume;
sums[led][1] += col[1] * consume;
sums[led][2] += col[2] * consume;
}
}
else {
// Simple – same colour for every LED
const col = rgb;
for (let led = 0; led < ledCount; led += 1) {
sums[led][0] += col[0] * consume;
sums[led][1] += col[1] * consume;
sums[led][2] += col[2] * consume;
}
}
};
if (frame instanceof simple_frame_1.SimpleFrame) {
accumulate(frame.rgb);
}
else if (frame instanceof complex_frame_1.ComplexFrame) {
accumulate(frame.colors);
}
else if (frame instanceof wait_frame_1.WaitFrame) {
// WaitFrame keeps previous colours.
if (lastKnownColours) {
accumulate(lastKnownColours);
}
else {
accumulate([0, 0, 0]);
}
}
needed -= consume;
idx += 1;
}
// Derive averaged colours.
const averaged = sums.map((sum) => [
Math.round(sum[0] / sliceDuration),
Math.round(sum[1] / sliceDuration),
Math.round(sum[2] / sliceDuration),
]);
const isUniform = averaged.every((c) => c[0] === averaged[0][0] && c[1] === averaged[0][1] && c[2] === averaged[0][2]);
return isUniform && ledCount === 1
? new simple_frame_1.SimpleFrame(averaged[0], sliceDuration)
: new complex_frame_1.ComplexFrame(averaged, sliceDuration);
}
/**
* **Streaming** FPS smoother.
*
* Guarantees that no more than `maxFps` frames per second are produced **while
* preserving the original animation duration**. Colours inside every output
* slice are calculated as the duration-weighted average of the covered source
* frames. Works with both `Iterable` and `AsyncIterable` sources.
*
* @experimental
* @category Animation
*/
function smoothFps(animation, maxFps = 60) {
(0, tsafe_1.assert)(maxFps > 0, 'maxFps must be greater than 0');
const SLICE = 1000 / maxFps;
return {
[Symbol.asyncIterator]: async function* () {
const iterator = (async function* () {
for await (const f of animation) {
yield f;
}
})();
const queue = [];
let sourceDone = false;
let lastKnownColours = null;
while (true) {
// Fill queue until we have at least one full slice worth of time or source ends.
let queuedDuration = queue.reduce((sum, q) => sum + q.remaining, 0);
while (!sourceDone && queuedDuration < SLICE) {
const { value, done } = await iterator.next();
if (done) {
sourceDone = true;
break;
}
const frame = value;
queue.push({ frame, remaining: frame.duration });
queuedDuration += frame.duration;
if (frame instanceof simple_frame_1.SimpleFrame) {
lastKnownColours = [frame.rgb];
}
else if (frame instanceof complex_frame_1.ComplexFrame) {
lastKnownColours = frame.colors.slice();
}
}
if (queue.length === 0) {
// No work left – exit.
break;
}
const sliceDuration = sourceDone && queuedDuration < SLICE ? queuedDuration : SLICE;
// Build and emit averaged frame.
const outFrame = buildAveragedFrame(queue, sliceDuration, lastKnownColours);
yield outFrame;
// Consume time from queue.
let consume = sliceDuration;
while (consume > 0 && queue.length) {
const head = queue[0];
if (head.remaining <= consume) {
consume -= head.remaining;
queue.shift();
}
else {
head.remaining -= consume;
consume = 0;
}
}
}
},
};
}
//# sourceMappingURL=smooth-fps.js.map