# Bugs & Issues — audit + fixes Audit of `src/js`, then all findings fixed. Every file below passes `node --check`. No live browser smoke test was run (a Chrome instance held the Playwright profile lock), so the changes are verified statically only — see **Not verified** at the end. Legend: `[x]` fixed · P0 = feature broken · P1 = wrong data written · P2 = crash on edge path · P3 = silently wrong UI · P4 = perf/leak · P5 = cleanup --- ## P0 — Broken features - [x] **MP4 export dead-ended and locked the UI** — `src/js/functions.js` The mp4 branch of `downloadRecording` was `type = 'video/mp4'`, an implicit global assignment that did nothing: no download, `recording` stuck at `true`, the button stuck on "Downloading...", and `downloadModal()` refusing to reopen until reload. mp4 now goes through `convertStreams(blob, 'mp4')` like gif, and a new shared `resetRecordingUI()` unlocks the editor on every exit path (success, unknown format, worker error, FileReader error, `MediaRecorder` error). - [x] **Copying keyframes threw** — `src/js/events.js` `canvas.getActiveObject().isEditing` ran on `null`, because selecting keyframes clears the canvas selection — the normal path. The active object is now resolved once and guarded, and empty `$.grep` results are no longer pushed to the clipboard. - [x] **`canvas.getItemByid` typo** — `src/js/functions.js` Lowercase `i`; TypeError when pasting text/`charSpacing` keyframes. - [x] **Ctrl+Z crashed on an empty stack; Ctrl+Shift+Z was a no-op** — `src/js/events.js` Merged into one guarded handler: shift picks redo, and each branch checks its own stack length. Previously Ctrl+Shift+Z matched both `if` blocks (redo then undo). - [x] **Blur slider threw with nothing selected** — `src/js/events.js` `obj.applyFilters()` was outside the `if (canvas.getActiveObject())` guard. Also renamed the shadowed `x` in the blur/noise/chroma `find()` callbacks. ## P1 — Wrong values written - [x] **`height` keyframes stored the width** — `src/js/functions.js` (3 sites: twice in `keyframeChanges`, once in `crop`) - [x] **`var scaleX = obj, scaleX;`** — `src/js/text.js`; the fabric object was being assigned as a scale factor. - [x] **NaN letter delay + shadowed global `duration`** — `src/js/text.js` `delay = i * duration` read the hoisted local before its own initialiser. Renamed to `step`, computed before use, and `!(delay > 0)` now catches NaN. - [x] **Every letter animation wrote to the last letter** — `src/js/text.js` `index`, `animation`, `start` and `instance` are `let` per iteration instead of `var`. - [x] **Shadow defaults/keyframes stored `undefined`** — `src/js/functions.js` fabric's `get()` is not a path getter. Added `getPropValue()` (handles `shadow.*`) and `setDefaultValue()` / `getDefaultValue()`, and routed ~26 direct `.defaults.find(...).value = …` writes through them — which also creates the entry when a project predates a property instead of throwing. - [x] **WebGL filter backend clobbered with `undefined`** — `src/js/init.js`, `src/js/database.js` Both assignments are now conditional on the backend having constructed. ## P2 — Crashes on edge paths - [x] **`RangeError: Invalid array length`** — `src/js/functions.js` `temparr.length = findIndex(...)` went negative for a stale keyframe reference. `lastKeyframe` / `nextKeyframe` are now index lookups that return `false`. - [x] **`animate()` had no null guards** — `src/js/functions.js` Object and `p_keyframes` lookups are resolved once and guarded in every per-frame loop (`animate`, `recordAnimate`, the playback update loop, `playVideos`, `playAudio`), which also removed dozens of repeated `.find()` calls. - [x] **`getAssets()` recursed synchronously forever** — `src/js/database.js` Retries on a 250 ms timer, capped at 20 attempts, and rebuilds the asset arrays instead of appending duplicates on re-entry after an import. - [x] **`deleteObject` threw and leaked `files`** — `src/js/functions.js` The entry was compared to a string so it never matched; now filtered by `name`. Video elements are also paused and unloaded on delete. - [x] **Unguarded `keyarr[0]` / `.defaults.find(...)`** — `copyKeyframes`, `updateKeyframe`, `applyEasing`, `keyframeProperties`, `removeKeyframe`, `checkAnyKeyframe`. The four repetitive counterpart blocks were replaced by one `KEYFRAME_COUNTERPARTS` map, so a missing counterpart is skipped, not fatal. - [x] **Other unguarded lookups** — `deleteAsset`, `reGroup`, `renderLayer`, `renderProp`, `setDuration`, `setTimelineZoom`, `saveLayerName`, `updateInputs`, `updatePanel`, `updateStrokeValues`, `animateText`, `scrollIntoView` (3 sites), `object:modified` / `object:rotating` / `mouse:out` / `mouse:up` handlers. `importProject` validates the payload before touching `data.project[0]`, and `line_h`/`line_v` go through a new `hideGuides()` helper. ## P3 — Silently wrong behaviour - [x] **`document.onmousedown` permanently disabled** — `src/js/functions.js` `dragTimeline` installed a `return false` handler and never removed it. It now only overrides `onselectstart`, and restores it on mouseup. - [x] **Keyframe time drifted on every drag** — `src/js/functions.js` `data-time` is now always the absolute timeline time (the value every lookup keys off), with the visual offset applied as CSS only. This also fixes keyframes on an *expanded* row, whose `data-time` used to be layer-relative so no lookup matched. `updateKeyframe`'s unused third argument is gone. - [x] **Shift-deselect never removed a keyframe** — `this` inside a `$.grep` callback is not the element. - [x] **`e.shiftDown`** → `e.shiftKey`. - [x] **Snap guide never hid** — the row-local index was compared against the global `.keyframe` count. - [x] **Comparison / typo bugs** `canvas.getActiveObjects.length` → `getActiveObjects().length` · `strokeDashArray == [10, 5]` → element comparison · chroma `setValue(distance)` → `distance * 100` · `'#FFFFF'` → `'#FFFFFF'` · `videoPlayer.videoheight` → `videoHeight` (and both thumbnail helpers no longer draw the canvas onto itself) · `if (start && play && !paused)` in `playAudio` (`play` is the global function, always truthy) · `#redo` gated on `redo.length` · `:last-child()` → `:last-child`. - [x] **Paste loop closure** — `var imgObj` → `let`, so each thumbnail saves its own file. - [x] **Layers added mid-timeline were shortened** — `end: duration - currenttime` → `duration` (media layers clamp to `min(start + assetDuration, duration)`). - [x] **Malformed HTML** — 5 unterminated `` in `src/js/ui.js`. - [x] **Duplicate DOM ids** — `id="easing"` on both wrapper and `