@ginden/blinkstick-v2
Version:
Improved Blickstick API for Node.js
147 lines • 5.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AnimationRunner = void 0;
const tsafe_1 = require("tsafe");
const combine_1 = require("./helpers/combine");
const simple_frame_1 = require("./frame/simple-frame");
const complex_frame_1 = require("./frame/complex-frame");
const promises_1 = require("node:timers/promises");
const node_perf_hooks_1 = require("node:perf_hooks");
const convert_array_of_rgb_tuples_to_bulk_set_buffer_1 = require("../utils/convert-array-of-rgb-tuples-to-bulk-set-buffer");
const wait_frame_1 = require("./frame/wait-frame");
const abortError = new Error('Animation aborted');
let warningEverEmitted = false;
/**
* This class is responsible for running animations on a Blinkstick device.
* It handles the animation loop, applying frames to the device, and managing
* the animation state.
*
* It's bound to a single Blinkstick device and can only run one animation at a time.
* @category Animation
*/
class AnimationRunner {
constructor(blinkstick) {
this.blinkstick = blinkstick;
this.abortController = new AbortController();
this.isRunning = false;
this.ledGroup = blinkstick.leds();
this.leds = Array.from({ length: blinkstick.ledCount }, (_, index) => this.blinkstick.led(index));
this.buffer = Buffer.alloc(this.leds.length * 3);
this.ledCount = this.leds.length;
}
/**
* Stops current animation
*/
stop() {
this.abortController.abort(abortError);
this.abortController = new AbortController();
this.isRunning = false;
}
/**
* Runs the animation
* If the animation is already running, it will throw an error
*
* Will wait for the animation to finish before returning
* @param animations
*/
async run(animations, { signal } = {}) {
(0, tsafe_1.assert)(animations.length > 0, 'At least one animation must be provided');
if (this.isRunning) {
throw new Error('Animation is already running');
}
return this.runNew(animations, { signal });
}
/**
* Will replace the current animation and run the new one
* Optional callback will be called when the animation is finished
*/
runAndForget(animations, cb, { signal } = {}) {
void this.runNew(animations, { signal }).catch((err) => {
if (err === abortError) {
cb?.(null);
return;
}
else {
cb?.(err);
}
});
}
/**
* Runs the animation, replacing the current one
*
* If the animation is already running, it will be stopped
*
* You can pass an optional AbortSignal to cancel the animation at any time
*/
async runNew(animations, { signal: userSignal } = {}) {
this.isRunning = true;
this.abortController.abort(abortError);
this.abortController = new AbortController();
const currentAnimation = (0, combine_1.combine)(...animations);
const signal = AbortSignal.any([this.abortController.signal, userSignal].filter((s) => s !== undefined));
let sumFrameDuration = 0;
let sumTimeElapsed = 0;
try {
for await (const frame of currentAnimation) {
const t0 = node_perf_hooks_1.performance.now();
await this.applyFrame(frame, signal);
const timeElapsed = node_perf_hooks_1.performance.now() - t0;
const frameDuration = frame.duration;
if (frameDuration < 16 && !(frame instanceof wait_frame_1.WaitFrame)) {
this.emitWarningForShortFrameDuration(frame);
}
sumFrameDuration += frameDuration;
sumTimeElapsed += timeElapsed;
}
}
catch (err) {
if (err === abortError) {
return;
}
else {
throw err;
}
}
finally {
this.isRunning = false;
console.log(`Animation finished. Expected: ${sumFrameDuration | 0}ms, Actual: ${sumTimeElapsed | 0}ms`);
}
}
async applySimpleFrame(frame) {
await this.ledGroup.setColorAndForget(frame.rgb);
}
async applyComplexFrame(frame) {
(0, tsafe_1.assert)(this.leds.length === frame.colors.length, 'Frame and LEDs length mismatch');
await this.blinkstick.setColors(0, (0, convert_array_of_rgb_tuples_to_bulk_set_buffer_1.convertArrayOfRgbTuplesToBulkSetBuffer)(frame.colors, this.buffer));
}
/**
* Applies a frame to the device and waits for the duration of the frame.
* @protected
*/
async applyFrame(frame, signal) {
signal.throwIfAborted();
const { duration } = frame;
const t0 = node_perf_hooks_1.performance.now();
if (frame instanceof simple_frame_1.SimpleFrame) {
await this.applySimpleFrame(frame);
}
else if (frame instanceof complex_frame_1.ComplexFrame) {
await this.applyComplexFrame(frame);
}
const timeElapsed = node_perf_hooks_1.performance.now() - t0;
const waitTime = Math.max(Math.round(duration - timeElapsed), 0);
if (waitTime > 0)
await promises_1.scheduler.wait(waitTime, { signal });
}
emitWarningForShortFrameDuration(frame) {
if (warningEverEmitted)
return;
warningEverEmitted = true;
process.emitWarning(`Frame ${frame.constructor.name} duration is too short. It may cause flickering.`, {
code: 'AnimationFrameDurationTooShort',
detail: `Frame duration should be at least 16ms to avoid flickering. Current duration: ${frame.duration}ms`,
});
}
}
exports.AnimationRunner = AnimationRunner;
//# sourceMappingURL=animation-runner.js.map