Once the readout held a time, drawReadout ran a canvas filter blur on the graph canvas three times a frame (LED halo, digits, tenths). Chrome sends every filtered draw through an offscreen layer the size of the whole canvas, so the entire graph redrew about 5x slower: 60 -> ~17 fps while panning a test graph (issue #77). The blurred passes now go into a small cached layer (cachedLayer in js/shared/run_timer.mjs) that is repainted only when the digits, LED, size or zoom bucket change; each frame copies it with drawImage and draws the crisp LED and digits directly. The readout is pixel-identical within one colour step.
84 lines
3.5 KiB
JavaScript
84 lines
3.5 KiB
JavaScript
// Run Timer decision logic: a tiny stopwatch reducer and the display
|
|
// formats. No DOM, no ComfyUI imports; the entry file wires api events.
|
|
|
|
export const HISTORY_LIMIT = 5;
|
|
|
|
export function createTimer(history = []) {
|
|
return {
|
|
running: false,
|
|
startedAt: null,
|
|
elapsed: 0,
|
|
outcome: null, // "done" | "error" | "interrupted" once a run has ended
|
|
history: Array.isArray(history) ? history.filter(isSeconds).slice(0, HISTORY_LIMIT) : [],
|
|
};
|
|
}
|
|
|
|
function isSeconds(value) {
|
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
}
|
|
|
|
export function startTimer(state, now) {
|
|
return { ...state, running: true, startedAt: now, elapsed: 0, outcome: null };
|
|
}
|
|
|
|
export function tickTimer(state, now) {
|
|
if (!state.running || state.startedAt === null) return state;
|
|
return { ...state, elapsed: Math.max(0, (now - state.startedAt) / 1000) };
|
|
}
|
|
|
|
// Ends a run. Only a completed run joins the history: an interrupted or
|
|
// failed one has no meaning as a reference time, so it shows but is not kept.
|
|
export function stopTimer(state, now, outcome = "done") {
|
|
if (!state.running) return { ...state, outcome };
|
|
const elapsed = state.startedAt === null ? state.elapsed : Math.max(0, (now - state.startedAt) / 1000);
|
|
const history = outcome === "done"
|
|
? [elapsed, ...state.history].slice(0, HISTORY_LIMIT)
|
|
: state.history;
|
|
return { ...state, running: false, startedAt: null, elapsed, outcome, history };
|
|
}
|
|
|
|
// "12.3 s" under a minute, "1:05.3" under an hour, "1:02:03" beyond.
|
|
export function formatElapsed(seconds) {
|
|
if (!isSeconds(seconds)) return "—";
|
|
if (seconds < 60) return `${seconds.toFixed(1)} s`;
|
|
const whole = Math.floor(seconds);
|
|
const hours = Math.floor(whole / 3600);
|
|
const minutes = Math.floor((whole % 3600) / 60);
|
|
const rest = whole % 60;
|
|
if (hours) return `${hours}:${String(minutes).padStart(2, "0")}:${String(rest).padStart(2, "0")}`;
|
|
const tenths = Math.floor((seconds - whole) * 10);
|
|
return `${minutes}:${String(rest).padStart(2, "0")}.${tenths}`;
|
|
}
|
|
|
|
// The small print under the big number: the previous runs, oldest last.
|
|
export function historyLine(history) {
|
|
if (!history?.length) return "";
|
|
return history.map(formatElapsed).join(" · ");
|
|
}
|
|
|
|
// The readout's glow is a blur, and a blur on the graph canvas is what
|
|
// dragged the whole frontend to ~17 fps (issue #77): Chrome sends every
|
|
// filtered draw through an offscreen layer the size of the entire canvas.
|
|
// So the blurred layer is painted into its own small canvas only when
|
|
// `key` changes, and every other frame gets that canvas back untouched.
|
|
export function cachedLayer(cache, key, width, height, paint, createCanvas) {
|
|
if (cache.canvas && cache.key === key) return cache.canvas;
|
|
const canvas = (cache.canvas ??= createCanvas());
|
|
// Setting the size, even to the same value, also clears the bitmap and
|
|
// resets the context's transform and state.
|
|
canvas.width = Math.max(1, Math.ceil(width));
|
|
canvas.height = Math.max(1, Math.ceil(height));
|
|
paint(canvas.getContext("2d"));
|
|
cache.key = key;
|
|
return canvas;
|
|
}
|
|
|
|
// Pixel density for that layer: the canvas's current scale rounded UP to a
|
|
// quarter octave, so a zoom gesture repaints it a handful of times rather
|
|
// than every frame, and capped so a deep zoom cannot allocate a huge bitmap.
|
|
export function layerScale(pixelScale, width, maxSide = 2048) {
|
|
const raw = Number.isFinite(pixelScale) && pixelScale > 0 ? pixelScale : 1;
|
|
const stepped = 2 ** (Math.ceil(Math.log2(raw) * 4) / 4);
|
|
return Math.min(stepped, maxSide / Math.max(1, width));
|
|
}
|