- TransformBox rotates with single-item selection (Figma-style), with
handles and resize math projected into rotated coordinate space
- Fix VideoSprite alphaMode crash by swapping texture to EMPTY before
destroying, preventing PixiJS render loop from reading null source
- Fix Ctrl+V double-paste: internal clipboard now always takes priority
over system clipboard PNG, with wasRecentInternalPaste() guard
- Add asset loading progress bar and suppress "Drop images here" flash
during initial scene load
Items were fading in from alpha 0 via spring animation, preventing
immediate interaction. Removed the entrance animation entirely — items
now appear at full opacity and are movable on first frame.
1. Move GIF loading behind TextureManager.loadGif/releaseGif with
ref-counting (fixes shared-source invalidation on duplicate GIFs)
2. Strip query strings/fragments before .gif extension check
3. GIFs now load paused — separate MAX_PLAYING_GIFS=8 budget in
culling system, only nearest N animate (others show static frame)
Register GifAsset extension for .gif loading, create AnimatedGifSprite
wrapper with lazy load/unload matching ImageSprite pattern, and detect
.gif assets in SceneManager to use animated rendering. GIFs stored as
type 'image' in scene format — no schema changes needed.
- Background media worker: polls media_jobs table, runs ffprobe+ffmpeg
with concurrency limit, generates video posters, emits socket events
- Non-blocking video upload: stores file + enqueues job, returns immediately
- Poster hydration on board load: GET /boards/:id injects poster/dimensions
from DB into canvas_state video objects
- SpatialGrid: fixed-cell (512px) spatial hash for O(nearby) culling instead
of O(all) item scanning, eliminates setTimeout violations
- Canvas-based video rendering: draws video frames to offscreen canvas then
uploads to GPU, completely eliminates GL_INVALID_OPERATION errors from
PixiJS VideoSource auto-update mechanism
- Server poster upgrade path: culling ticker and applyProcessedMedia() both
upgrade client-captured posters to server posters when available
- Pause restores server poster (paused video behaves like an image)
- Selection drag-end persistence: onObjectDragEnd broadcasts + saves + undo
- Live media:job:update socket handler patches scene data + VideoSprite
dimensions without broadcast fanout
1. Server posters no longer auto-load in VideoSprite constructor.
loadServerPoster() is now public and called by the culling system
only when within MAX_POSTER_VIDEOS budget. Videos outside the
budget stay as placeholders regardless of server poster availability.
2. video-utils cleanup() now uses statSync to distinguish files from
directories, unlinks files first, then rmdirs. Previously tmpDir
was passed through path.dirname() which resolved to /tmp instead
of the created temp directory.
Video memory:
- Lazy <video> creation: constructor makes placeholder only, initVideo()
creates element with preload='metadata' when near viewport
- Tiered budgets: 1 playing / 6 initialized / 4 poster textures max
- Aggressive teardown: offscreen videos lose <video> element, poster,
and all textures — zero memory for offscreen videos
- Poster textures dropped while playing (don't keep both resident)
- Dimension caching in scene data (nativeW/nativeH) avoids re-init
Server:
- HTTP Range support (206 Partial Content) for video seeking
- Proper end clamping and invalid range rejection (416)
Sync:
- Remove redundant broadcastSceneDebounced() from broadcastTransform()
- Remove duplicate broadcastElements() from drag-end handler
VideoControls:
- Event-driven (timeupdate/play/pause/seeked) instead of rAF polling
- Tracks actual HTMLVideoElement reference, rebinds on init/teardown
- Seek uses ref-based commit on pointerUp, not conditional onChange
When zoomed out, all items fall within the viewport so the culling
system tried to load everything at once. Now:
- Max 60 textures loaded at any time (sorted by distance to center)
- Max 5 new texture loads per tick (prevents spike)
- Items over budget or far away are unloaded immediately
Images no longer load textures eagerly on creation. Instead, the
culling ticker checks viewport proximity every 200ms:
- Load margin: 1x screen size beyond viewport (preload before visible)
- Unload margin: 2x screen size (hysteresis prevents thrash during pan)
ImageSprite changes:
- Constructor no longer calls loadTexture() — deferred to culling system
- New unloadTexture() restores placeholder and frees GPU memory
- Exposed texture getter for clipboard resolution detection
- loaded field now public for culling system to check state
This should allow hundreds of images without GPU OOM, since only
nearby textures consume GPU memory at any time.
- Skip thumbnail generation when scene has >50 items (extract.canvas
on full viewport with many textures causes GPU OOM and context loss)
- Add WebGL context lost/restored handlers — auto-reload scene on recovery
- Reduce texture memory budget from 512MB to 256MB for GPU headroom
- Make PixiJS canvas background transparent so SVG dot grid shows through
- Move dot grid SVG before canvas in DOM with zIndex 0 (behind images)
- Set wrapper div background to #1e1e1e
- Remove 600px cap on pasted images — preserve native dimensions
Runs a culling check every 200ms alongside the spring ticker.
Off-screen image sprites are hidden (visible=false), on-screen
ones get updateLOD() called with the current zoom level. Video
sprites get onVisibilityChange() to auto-play/pause based on
viewport visibility. Uses a 200px margin to avoid popping.
Implements the main React component wrapping PixiJS v8 Application and
pixi-viewport with: async init, viewport persistence per board,
space-to-pan, spring animation ticker, v1/v2 scene loading, ResizeObserver
resizing, and imperative handle (fitAll, zoom control, scene/viewport access).