# 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 `