Files
2026-08-28 01:01:12 +02:00

150 lines
5.5 KiB
JavaScript

export const H3_FPS = 24;
export const TIMELINE_VERSION = 2;
export const DEFAULT_FRAME_COUNT = 124;
export const DEFAULT_STATE = Object.freeze({
version: TIMELINE_VERSION,
fps: H3_FPS,
timeline: Object.freeze({
frame_count: DEFAULT_FRAME_COUNT,
selected_frame: 0,
}),
guides: Object.freeze([]),
});
export function cloneDefaultState() {
return {
version: TIMELINE_VERSION,
fps: H3_FPS,
timeline: { frame_count: DEFAULT_FRAME_COUNT, selected_frame: 0 },
guides: [],
};
}
function asSlot(value) {
if (value === null || value === undefined || value === '') return null;
const slot = Number.parseInt(value, 10);
return Number.isInteger(slot) && slot >= 0 ? slot : null;
}
function asFrame(value) {
return Math.max(0, Math.round(Number(value) || 0));
}
function normalizeGuide(value, index) {
return {
id: String(value?.id || `guide-${index + 1}`),
enabled: value?.enabled !== false,
frame: asFrame(value?.frame),
image_slot: asSlot(value?.image_slot),
audio_slot: asSlot(value?.audio_slot),
};
}
function normalizeTimeline(value, fallbackSelected = 0) {
const rawFrameCount = Number(value?.frame_count);
const frameCount = Math.max(1, Math.round(Number.isFinite(rawFrameCount) ? rawFrameCount : DEFAULT_FRAME_COUNT));
const selected = Math.min(frameCount - 1, asFrame(value?.selected_frame ?? fallbackSelected));
return { frame_count: frameCount, selected_frame: selected };
}
export function normalizeState(value) {
if (!value || ![1, TIMELINE_VERSION].includes(Number(value.version)) || Number(value.fps ?? H3_FPS) !== H3_FPS) {
return cloneDefaultState();
}
const guides = Array.isArray(value.guides) ? value.guides : [];
const legacySelected = value.reference?.selected_frame ?? 0;
const timeline = Number(value.version) === 1
? normalizeTimeline({ frame_count: DEFAULT_FRAME_COUNT, selected_frame: legacySelected })
: normalizeTimeline(value.timeline, legacySelected);
return {
version: TIMELINE_VERSION,
fps: H3_FPS,
timeline,
guides: guides.map(normalizeGuide),
};
}
export function parseState(json) {
if (!json) return cloneDefaultState();
try {
return normalizeState(typeof json === 'string' ? JSON.parse(json) : json);
} catch {
return cloneDefaultState();
}
}
export function serializeState(state) {
return JSON.stringify(normalizeState(state));
}
export function framesToSeconds(frame) {
return Math.max(0, Number(frame) || 0) / H3_FPS;
}
export function secondsToFrames(seconds) {
return Math.max(0, Math.round((Number(seconds) || 0) * H3_FPS));
}
const SNAP_STEPS = Object.freeze({ frame: 1, '6f': 6, '12f': 12, '24f': 24 });
function snapH3Frame(value) {
const first = 5;
if (value <= first) return first;
return first + Math.round((value - first) / 17) * 17;
}
export function snapFrame(frame, mode = 'frame') {
const value = Math.max(0, Number(frame) || 0);
if (mode === 'h3') return snapH3Frame(value);
const step = SNAP_STEPS[mode] || 1;
return Math.max(0, Math.round(value / step) * step);
}
export function frameCountFromNode(node, fallback = DEFAULT_FRAME_COUNT) {
const input = (node?.inputs || []).find((item) => String(item?.name || '').endsWith('frame_count'));
if (input?.link == null) return Math.max(1, Math.round(Number(fallback) || DEFAULT_FRAME_COUNT));
const widget = (node?.widgets || []).find((item) => item?.name === 'frame_count');
const link = node?.graph?.links?.[input.link] || node?.graph?.links?.[String(input.link)];
const source = link && (node.graph.getNodeById?.(link.origin_id) || node.graph._nodes_by_id?.[link.origin_id]);
const sourceWidget = source?.widgets?.find((item) => ['value', 'int', 'frames', 'frame_count'].includes(item?.name));
const sourceValue = sourceWidget?.value
?? source?.widgets_values_named?.value
?? source?.widgets_values_named?.int
?? source?.widgets_values_named?.frames
?? source?.widgets_values_named?.frame_count;
if (sourceValue != null) {
const sourceType = String(link?.type ?? source?.outputs?.[link?.origin_slot]?.type ?? 'INT');
const numeric = Number(sourceValue);
if (sourceType.split(',').includes('FLOAT') && Number.isFinite(numeric) && numeric > 0) {
const scaled = numeric * H3_FPS;
const lower = Math.floor(scaled);
const rounded = scaled - lower === 0.5 ? lower + (lower % 2) : Math.round(scaled);
const base = Math.max(5, rounded);
return Math.min(3600, base + ((5 - (base % 17)) + 17) % 17);
}
}
const raw = source ? sourceValue ?? fallback : widget?.value ?? node?.widgets_values_named?.frame_count ?? fallback;
const value = Number(raw);
return Number.isFinite(value) ? Math.min(3600, Math.max(1, Math.round(value))) : Math.max(1, Math.round(Number(fallback) || DEFAULT_FRAME_COUNT));
}
export function frameCountFromExecution(output, fallback = DEFAULT_FRAME_COUNT) {
const raw = Array.isArray(output?.frame_count) ? output.frame_count[0] : output?.frame_count;
const value = Number(raw);
return Number.isFinite(value) ? Math.min(3600, Math.max(1, Math.round(value))) : Math.max(1, Math.round(Number(fallback) || DEFAULT_FRAME_COUNT));
}
export function formatFrameLabel(frame) {
return `F${Math.max(0, Math.round(Number(frame) || 0))}`;
}
export function timelineTickFrames(totalFrames) {
const total = Math.max(1, Math.round(Number(totalFrames) || 1));
const step = total <= 144 ? 24 : total <= 288 ? 48 : total <= 576 ? 96 : 192;
const ticks = [];
for (let frame = 0; frame <= total; frame += step) ticks.push(frame);
if (ticks[ticks.length - 1] !== total) ticks.push(total);
return ticks;
}