fix: audit src/js and add a frame-accurate export path
Two halves of one pass; TODO-FIXES.md lists every finding. The audit fixed, among ~90 items: MP4 export dead-ending and locking the UI behind a stuck "Downloading..." button; height keyframes storing the width; shadow defaults and keyframes storing undefined because fabric's get() is not a path getter; every letter animation writing to the last letter; keyframe times drifting on each drag because data-time was layer-relative on expanded rows; O(n^2 log n) playback, now indexed once per frame; and a save() that rebuilt the record canvas on every edit, now debounced. Exports also lost audio-layer sound outright: MediaRecorder records only the first audio track, so every source now mixes through one AudioContext into one destination. The new half is src/js/render.js, an offline renderer. Real-time capture was the root cause of dropped frames on heavy scenes and smeared video layers: a bare currentTime assignment is async, so drawing straight after it captures the previous frame. Each frame is now seeked, awaited on 'seeked', drawn, and pushed through a VideoEncoder; audio is mixed in one OfflineAudioContext pass and encoded to Opus. webm-writer2.js gained a second Opus track and lost three bugs, including a MAX_CLUSTER_DURATION_MSEC of ~58 days that overflowed the signed 16-bit block timecode past ~32s. Real-time capture remains the fallback: the hand-rolled muxer's output is decoded in a <video> element before being handed over, and any failure returns null so record() falls back transparently. test/webm-muxer.test.js parses the muxer output with an EBML reader in plain Node. It caught the fixed-256-byte header overflow that inspection missed. Not verified in a browser: node --check passes on every script and the muxer test passes, but a running Chrome held the Playwright profile lock, so the manual pass listed at the end of TODO-FIXES.md is still outstanding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+26
-3
@@ -78,6 +78,20 @@ function initLines() {
|
||||
canvas.add(line_v);
|
||||
}
|
||||
|
||||
// Hide the alignment guides after a canvas reload.
|
||||
// They can be missing entirely in projects saved before they existed, so
|
||||
// never dereference them directly.
|
||||
function hideGuides(inst) {
|
||||
const h = inst.getItemById('line_h');
|
||||
const v = inst.getItemById('line_v');
|
||||
if (h) {
|
||||
h.set({ opacity: 0 });
|
||||
}
|
||||
if (v) {
|
||||
v.set({ opacity: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
function alignControls(object, type) {
|
||||
if (type == 'align-top') {
|
||||
object.set(
|
||||
@@ -122,8 +136,10 @@ function alignControls(object, type) {
|
||||
function alignObject() {
|
||||
const type = $(this).attr('id');
|
||||
const object = canvas.getActiveObject();
|
||||
console.log(canvas.getActiveObject().type);
|
||||
if (canvas.getActiveObject().type == 'activeSelection') {
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
if (object.type == 'activeSelection') {
|
||||
const tempselection = canvas.getActiveObject();
|
||||
canvas.discardActiveObject();
|
||||
tempselection._objects.forEach(function (object) {
|
||||
@@ -157,6 +173,8 @@ function alignObject() {
|
||||
);
|
||||
newKeyframe('top', object, currenttime, object.get('top'), true);
|
||||
}
|
||||
canvas.renderAll();
|
||||
save();
|
||||
}
|
||||
$(document).on('click', '.align', alignObject);
|
||||
|
||||
@@ -240,7 +258,12 @@ function centerLines(e) {
|
||||
canvas.renderAll();
|
||||
}
|
||||
}
|
||||
if (obj != e.target && obj != line_h && obj != line_v) {
|
||||
if (
|
||||
obj != e.target &&
|
||||
obj != line_h &&
|
||||
obj != line_v &&
|
||||
obj.visible !== false
|
||||
) {
|
||||
if (
|
||||
obj.get('id') == 'center_h' ||
|
||||
obj.get('id') == 'center_v'
|
||||
|
||||
Reference in New Issue
Block a user