UNPKG

@ginden/blinkstick-v2

Version:
147 lines 5.9 kB
"use strict"; 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