Commit Graph

218 Commits

Author SHA1 Message Date
Skyler Lehmkuhl f1fba186c1 video: decode at the consumer's target resolution (Stage 1)
The decoder's output size was frozen to the document size at import, and export
reused that decoder — so exporting above document res upscaled the video (real
source detail discarded) and a document resize never re-targeted the decode.

Decode size is now chosen per get_frame call: VideoDecoder::get_frame and
VideoManager::get_frame take a target (w, h), capped to native (never upscale),
with the swscale context and frame caches keyed on the output size so preview
(preview res) and an in-progress export (export res) don't collide. The renderer
derives the target from the document->output base_transform, so export decodes
at export res (full detail) and the canvas at preview res. Thumbnails/asset
library pass small targets.
2026-06-26 00:36:35 -04:00
Skyler Lehmkuhl 5844a0f070 Composite grouped/nested video on the GPU path
Imported video is a Group[Video, Audio] that rendered as a Vello-baked
Vector layer, re-uploading the full frame to Vello's image atlas every
frame (~17ms/frame at 1080p, hitting playback and export alike). Extract
video frames out of the Group/clip scene recursion into
VideoRenderInstances so they composite via the GPU Video path; mixed
video+vector containers fall back to Vello (correct, unaccelerated).

Also route video through hardware sRGB decode: upload raw sRGB bytes to an
Rgba8UnormSrgb texture and blit with a non-unpremultiplying shader variant
(blit_straight), removing the per-frame per-pixel CPU sRGB->linear pass.
Add an F3 GPU-timestamp timer and a per-frame video texture cache.

Drops the live composite of a 1080p video from ~17ms to ~2-3ms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:38:30 -04:00
Skyler Lehmkuhl 83057b754a Clean up build warnings
Resolve all compiler warnings across daw-backend, lightningbeam-core, and
lightningbeam-editor:

- Delete dead code: the superseded CPU raster tools in raster_tool.rs
  (EffectBrush/Smudge/Gradient/Transform/Warp/Liquify/Selection — replaced by
  the GPU path), plus orphaned helpers and never-read struct fields.
- Mechanical fixes: drop unused imports/variables/mut, underscore unused params,
  `drop(&x)` -> `let _ = x`, deprecated egui::Rounding -> CornerRadius, snake_case
  rename, elided-lifetime Cow<'_, [u8]>.
- Keep the WIP CSS theming system (theme.rs/theme_render.rs) under
  #[allow(dead_code)] rather than deleting it.

Editor checks warning-free; 293 core tests pass.
2026-06-21 16:48:59 -04:00
Skyler Lehmkuhl 5fd04b5dd5 Lock editing on tween in-between frames
Block geometry/clip edits on frames that fall strictly inside a tween span,
where an edit would silently mutate the bracketing keyframe instead:

- Shape tweens: gate vector editing (vertices, curves, DCEL hits) and all
  geometry tools in stage.rs behind VectorLayer::is_tween_inbetween.
- Motion tweens: block selecting/dragging/transforming clip instances whose
  transform is mid-tween, via AnimationData::is_object_tweened_at.

Also: inserting a keyframe mid-tween now captures the interpolated geometry
shown at that frame (not the left keyframe's) and inherits the shape tween,
so the new keyframe continues morphing toward the right keyframe.
2026-06-21 16:01:47 -04:00
Skyler Lehmkuhl 39978e59b3 Unify selection systems and make region/lasso cut robust
Collapse the two parallel selection systems into one. The RegionSelect
tool (rect + lasso) now cuts the geometry along the region outline and
selects the resulting sub-pieces into the standard `Selection` ID-sets,
exactly like every other tool. The vestigial floating `RegionSelection`
(drag never wired; commit/delete/copy were stubbed) and all its plumbing
are removed, so Group, Convert-to-Movie-Clip, Delete, and Properties all
operate uniformly from lasso, rect, marquee, and click selections.

Region cutting is reworked onto a robust planar arrangement:

- Replace fragile incremental "split a fill by one cut edge" logic with
  planar face re-tracing (`retrace_fills_after_cut` + `trace_faces`),
  which correctly handles arbitrary holed/concave fills.
- `extract_subgraph` no longer frees vertices still referenced by kept
  boundary edges (fixed Group leaving freed-but-referenced vertices that
  a later alloc reused and corrupted).
- `split_fill_by_*` direction fix (was producing disconnected boundaries
  rendered as stray diagonals).
- `fill_interior_point` (area-centroid + inward-step fallback) for
  reliable inside/outside classification of non-convex pieces.
- Coincident-edge dedupe + degenerate-fill removal (edge-adjacent shapes
  no longer make zero-area sliver fills).
- Dangling-edge pruning, near-coincident endpoint welding, induced-
  subgraph expansion, and tracking of `split_edge` sub-edges, so
  self-intersecting freehand lassos cut correctly.

Region-select capture is available behind LIGHTNINGBEAM_DUMP_REGION=1 for
turning a misbehaving cut into a deterministic test. Extensive regression
tests added in vector_graph/tests/region_cut_select.rs.
2026-06-21 14:47:11 -04:00
Skyler Lehmkuhl 0883c77e2b Phase 4: prefetch upcoming images during playback
`assets_needed_at(document, time)` (core) enumerates the image asset ids referenced by
the visible vector layers' active keyframes at a time (top-level + group children).
During playback the stage decodes the images needed ~0.5s ahead into the bounded
ImageCache, so a keyframe that swaps image fills doesn't hitch when the playhead
reaches it. Gated on is_playing; nested clip-instance recursion + background decode
are refinements.

Completes Phase 4 (image asset paging: Tier 2 decoded-cache LRU, Tier 1 lazy bytes,
playback prefetch).
2026-06-21 01:12:37 -04:00
Skyler Lehmkuhl 3d0a334014 Phase 4 Tier 1: lazy image-asset bytes paged from the container
Project load no longer eager-reads all image bytes — `ImageAsset.data` stays empty
and the renderer's ImageCache pages compressed bytes from the `.beam` on a decode
miss (read_packed_media_readonly by asset id), decoding into the byte-bounded Tier-2
cache. Result: instant load, and compressed bytes don't accumulate on the heap.

- ImageCache: `container_path` + `resolve_bytes` (asset.data if resident — fresh
  import or old base64 project — else page from the container); decoders take `&[u8]`
  and use the decoded dimensions.
- Container path threaded App.current_file_path → SharedPaneState → VelloRenderContext,
  set on the cache each prepare.
- load_beam_sqlite drops the 3.5b eager read.

(Refinement: a persistent read connection instead of open-per-miss.)
2026-06-21 01:12:05 -04:00
Skyler Lehmkuhl aad2d5c515 Onion/image: make Image a fill-type tab (None | Solid | Gradient | Image)
Image fill is now a tab in the Fill type row rather than a separate dropdown. When
Image is active, an asset-picker combo selects which image; switching to None/Solid/
Gradient clears the image fill (it otherwise overrides them). The Image tab only
appears when there are imported image assets.
2026-06-21 00:37:33 -04:00
Skyler Lehmkuhl 6fc3a131a6 Phase 3.5a: SetImageFillAction + Info-Panel image-fill picker
- SetImageFillAction (core): set/clear `image_fill` on the selected VectorGraph fills,
  with per-fill undo (mirrors SetFillPaintAction). Image takes render priority; clearing
  reveals the colour/gradient underneath.
- Info Panel Shape section: an "Image:" combo listing the document's image assets (+ None)
  for the selected fill(s), showing the current assignment. Assign/clear pushes the action.

This lets an existing shape be given (or cleared of) an image fill, complementing the
import/drop placement. Next: 3.5b — persist image assets in the .beam container.
2026-06-21 00:30:01 -04:00
Skyler Lehmkuhl 6c9fcb1921 Phase 3.5a: place imported images on the canvas (image-filled rect)
Replaces the DCEL "not yet supported" stubs so importing/dropping an image actually
puts it in the vector scene.

- AddShapeAction gains an `image_fill` + `AddShapeAction::image_rect(...)` constructor:
  a borderless rectangle (invisible edges) whose enclosed region is paint-bucketed and
  tagged with an image asset id. The renderer already prioritises `image_fill`.
- Direct import (auto_place_asset): an imported image is placed centered on the canvas
  at native size on a vector layer.
- Drag from the asset library onto the stage: image-filled rect at the drop point
  (centered), native size, using the asset's dimensions.

Next: SetImageFillAction + an Info-Panel image-fill picker for existing shapes; then
3.5b container persistence.
2026-06-21 00:22:17 -04:00
Skyler Lehmkuhl 7445ee919f Onion skin settings: move from floating window to the Info Panel
The standalone egui window didn't fit the UI. Replaced it with a collapsible
"Onion Skin" section at the bottom of the Info Panel (Enabled checkbox + frames
before/after + opacity), available regardless of selection. SharedPaneState carries
a mutable `onion_skin` ref for the controls (distinct from the gated `onion` copy
used by rendering).
2026-06-20 23:49:38 -04:00
Skyler Lehmkuhl 10b4aa481e Onion skinning: vector-layer ghosts (tinted)
- Core compositor gains an optional screen-blend tint per CompositorLayer
  ([0,0,0,0] default = no-op, so existing compositing is unaffected);
  CompositorLayer::with_tint sets it — giving the Vello/vector path a tint hook.
- For the active VECTOR layer with onion on, build ghost scenes at each neighbouring
  keyframe's time via render_layer_isolated (reusing the prepare's image cache), then
  render each scene → sRGB → linear → composite with warm/cool tint + opacity falloff,
  behind the current frame. Off during playback; active layer only.

Completes onion skinning for all layer types (raster + vector).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 23:49:33 -04:00
Skyler Lehmkuhl 78109cda93 Fix onion ghosts: show on blank cels + fault in on seek
- Removed the early `continue` that skipped a layer with no resident content — it
  also skipped the active raster layer's onion ghosts when the current cel was blank
  (the exact case you trace a new cel from neighbours). Each render arm already
  guards its own empty case, so the skip was redundant.
- Ghost texture resolution now falls through cache → resident raw_pixels (upload) →
  in-memory proxy → request fault-in, so neighbours that were paged out (e.g. after a
  seek, or saved-this-session with no decoded proxy) page in and ghost in, instead of
  silently rendering nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 23:18:24 -04:00
Skyler Lehmkuhl 4dc937f9c0 Onion tint: screen-blend instead of multiply (so outlines tint too)
Multiplicative tint left blacks black, so outlines (the main thing to ghost in line
art) never picked up the warm/cool color. Switched the canvas-blit tint to a screen
blend (out = base + tint - base*tint): black → tint color, white unchanged, and a
clean no-op at tint=(0,0,0) for all normal blits. Reverted the default .w slots to 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 23:06:05 -04:00
Skyler Lehmkuhl 53cae1bfe5 Onion skinning (raster): toggle + tinted ghosts of the active layer
- OnionSkinSettings (editor view state): enabled, frames_before/after (default 2/2),
  opacity (0.35), warm past / cool future tints, linear falloff. Threaded App →
  SharedPaneState (gated off during playback) → VelloRenderContext.
- Toggle: View ▸ Onion Skinning + the `O` shortcut (AppAction::ToggleOnionSkin).
- Tint: packed an RGB multiply into the canvas-blit matrix uniform's unused .w slots
  (1,1,1 = no tint for all normal blits); BlitTransform::with_tint. Shader multiplies.
- Render: for the ACTIVE raster layer only, blit the N neighbouring keyframes (full
  texture if resident, else the low-res proxy — uploaded on demand) tinted + faint,
  composited behind the current frame. Off during playback.

Next: a settings panel (frame count/opacity) and vector-layer ghosts.
2026-06-20 23:05:57 -04:00
Skyler Lehmkuhl ed022995bd Keyframe diamonds: pointing-hand cursor + prefetch during playback
- Pointing-hand cursor when hovering a clickable keyframe diamond.
- Prefetch (Phase 3e, playback only): each update during playback, page in the next
  few upcoming keyframes (PREFETCH_AHEAD=4) per raster layer that aren't resident, via
  the existing async worker. Their full pixels land before the playhead reaches them,
  so playback shows full frames instead of the low-res proxy on every frame (the
  proxy→full pop was the "flicker"). Reactive faults still cover scrubbing.
2026-06-20 22:45:50 -04:00
Skyler Lehmkuhl 3188fc8bb6 Timeline: click a keyframe diamond to snap the playhead to it
render_layers now records each drawn keyframe diamond's screen rect + exact time in
`keyframe_diamond_hits`; handle_input hit-tests a click against them and sets the
playhead (and seeks the audio controller) to the keyframe's exact time. Uses the
previous frame's rects — diamonds don't move between frames, so the click lands
right — which sidesteps the input-before-render ordering and the drag/scroll Y math.
Works for both raster and vector keyframes.
2026-06-20 22:44:41 -04:00
Skyler Lehmkuhl 2cbaf67583 Raster keyframe timeline UI: display + explicit creation + no lazy create
Make raster layers behave like vector on the timeline.

- Timeline: draw a diamond per `RasterKeyframe` (mirrors the vector keyframe block).
- New Keyframe (K / menu): on a raster layer, insert a BLANK cel at the playhead via
  a new undoable `AddRasterKeyframeAction` (+ `RasterLayer::insert_blank_keyframe_at`
  / `remove_keyframe`). Vector path unchanged.
- Stop lazy creation: paint tools now edit the ACTIVE keyframe (at-or-before the
  playhead) instead of creating one. The brush captures the active keyframe's exact
  time; `RasterStroke`/`RasterFillAction` resolve via `keyframe_at_mut` (error if
  none); the tool-site `ensure_keyframe_at` blocks (brush/fill/bucket/wand/quick-
  select/floating-lift) are removed — each read already bails when no keyframe exists.
  New layers still seed a keyframe at the playhead, so there's normally one to paint
  into; painting before the first keyframe is now a no-op (as intended).

Next: onion skinning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:22:02 -04:00
Skyler Lehmkuhl a35cc6fa9f Bilinear-sample upscaled raster proxies (smooth, not blocky)
The canvas blit used a nearest sampler, so the upscaled low-res proxy looked
blocky. Added a Linear sampler + `CanvasBlitPipeline::blit_smooth`; the raster
render uses it only for the proxy path (full-res canvas stays nearest/crisp). The
bind-group layout already declares the canvas texture filterable, so no layout
change was needed.
2026-06-20 21:14:47 -04:00
Skyler Lehmkuhl 1bfd09f151 Phase 3a-3: low-res image proxy for cold-scrub raster frames
Scrubbing onto a paged-out raster keyframe flashed blank for the 1-2 frames its
full pixels took to page in. Now a low-res proxy is shown in that gap.

- core: `MediaKind::RasterProxy` (id derived from the keyframe id via
  `raster_proxy_media_id`); `brush_engine::encode_raster_proxy_png` downscales a full
  RGBA buffer to a ≤192px-long-edge PNG. Save writes a proxy beside each resident
  frame's full PNG (paged-out frames keep their existing proxy row, like the full).
  Load eagerly decodes proxies (small) into `RasterKeyframe::proxy`.
- editor: a separate `proxy_layer_cache` in the GPU brush (own recency LRU, budget 64
  since each is ~1/100th a full frame) + `ensure_proxy_texture`/`get_proxy_texture`.
  The raster render, when the full texture isn't resident, blits the proxy mapped to
  the keyframe's FULL logical dims so it upscales via the sampler. F3 VRAM figure now
  includes proxy textures.

When the full pixels land (async fault-in), the full path takes over automatically.
Proxies only exist after a save+reload; freshly-painted unsaved frames stay resident
so they need none.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:05:46 -04:00
Skyler Lehmkuhl 39dc402ba3 Phase 3b: bound resident raster pixels with an eviction LRU
Scrubbing a large paint project no longer accumulates every visited frame in RAM.
A fault-in-recency LRU keeps the most-recently-paged-in RASTER_RESIDENT_MAX (12)
keyframes resident and drops the pixels of older *clean* ones (re-arming their
fault-in so they re-page on revisit). The shown frame is always the most-recent
fault-in, so it's never evicted.

Data-loss safety: a new `dirty` flag marks any keyframe whose `raw_pixels` were
mutated by editing (stroke/fill/paint-bucket/floating-lift + their undo/redo) and
is NOT yet in the container. Dirty keyframes are NEVER evicted — they're only
unpinned from the LRU. The flag is cleared on a successful save, which also re-arms
the LRU for the now-clean resident frames so the bound still applies to frames
edited this session.

Also: the save loop now walks all layers (incl. nested) to match the load path's
recursive fault-in arming — evicted frames keep their existing container row
(media_exists), and nested raster keyframes are persisted + covered by live_media.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:56:37 -04:00
Skyler Lehmkuhl 4228864259 Phase 3a-1: lazy fault-in of raster keyframe pixels
Raster keyframes are no longer eagerly decoded at load — `raw_pixels` stays empty
and is paged in on demand from the project container, so a big paint project opens
instantly and only touched frames hit RAM.

- core: `read_packed_media_readonly` (fresh read-only connection, can't conflict
  with an in-place save) + `RasterStore` (holds the container path; `load_pixels`
  reads+decodes a keyframe's PNG by id). `load_beam_sqlite` stops eager-decoding and
  instead marks every raster keyframe `needs_fault_in` (recursively, incl. nested);
  a freshly-created keyframe stays false (blank-resident, nothing to page). Added
  `Document::all_layers_mut`.
- editor: the canvas records a fault-in request when it needs a paged-out keyframe
  (empty pixels && needs_fault_in); the App drains the sink at the top of update(),
  pages the pixels in via the store, clears the flag, and repaints. Store path is set
  on load and after save. Export faults in synchronously per frame.

Cold-scrub still shows a 1-frame gap and the page-in is synchronous; the image proxy
(3a-2) and async load (3a-3) remove those next.
2026-06-19 16:59:46 -04:00
Skyler Lehmkuhl 097345be76 Fix video thumbnail strip bugs + persist thumbnails (resumable)
Thumbnail rendering fixes:
- Strip now tiles from each clip's true (unclamped) origin and draws only the
  tiles intersecting the visible rect, so it scrolls correctly and shows the
  right frames when a clip is scrolled partly off the left. Both render sites
  (collapsed group + expanded track) share one draw_video_thumbnail_strip helper.
- On-clip strip no longer freezes on the first thumbnail: get_thumbnail_at now
  returns the actual thumbnail timestamp and the GPU texture cache keys on it, so
  tiles refresh as closer thumbnails finish generating.
- Hover preview derives content time from the clip's true origin too (matches the
  strip when scrolled off-screen).
- insert_thumbnail keeps the cache sorted + deduped (fixes a latent unsorted
  binary_search bug, and makes concurrent restore + resume race-safe).

Thumbnail persistence (mirrors waveform persistence):
- MediaKind::Thumbnail rows, keyed by thumbnail_media_id(clip_id) (clip id XOR a
  sentinel). Each clip's thumbnails PNG-encoded into one opaque LBTN blob (editor
  owns the format), snapshotted cheaply (Arc clones) and encoded off the UI thread.
- Save writes the packs (kept in place on re-save); load reads them into
  LoadedProject.thumbnail_blobs; the editor decodes + inserts them on a background
  thread, so reload shows thumbnails instantly with no re-decode (even if the
  source video file is missing).
- Partial sets are persisted with a complete flag and RESUMED on load:
  generate_keyframe_thumbnails takes a should_skip predicate so a save made
  mid-generation continues from where it left off instead of redoing the work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:36:56 -04:00
Skyler Lehmkuhl 3d7cff9ad0 Stream audio & video from .beam container; waveform LOD pyramid + persistence
Migrate the .beam container to SQLite and stream media from it instead of
decoding whole files into RAM on import/load.

Container & large files:
- SQLite .beam container (beam_archive) with in-place transactional saves and an
  incremental BlobReader; supports both packed (chunked blobs) and referenced
  (external path) media, with a user preference + first-import prompt for files
  over the large-media threshold.

Audio streaming:
- Stream packed compressed audio on load via an inversion-of-control blob factory
  (AudioBlobSourceFactory): daw-backend defines the trait, core implements it
  over BlobReader, so the audio engine stays container-agnostic.
- Bulk-activate disk streaming for all loaded clips after SetProject.
- Sample-accurate compressed seek (SeekMode::Accurate; Coarse mislands on VBR).

Video:
- Video frames decoded/streamed on demand; thumbnails generated asynchronously
  on a dedicated decoder so import/load never blocks the UI.
- The video's audio track is streamed on demand via an ffmpeg VideoAudioReader
  as a separate editable AudioClip (no /tmp WAV extraction).

Waveform overview:
- Streaming min/max LOD pyramid (waveform_pyramid), bounded memory, configurable
  floor B; serialized into the container and restored on load (or generated in
  the background from the packed blob when absent), so no re-decode on reload.
- GPU min/max upload path; integer-LOD textureLoad fixes zoom-dependent wobble.
2026-06-17 13:52:38 -04:00
Skyler Lehmkuhl 318720f89d Fix gamma handling and improve brush canvas performance
Color correctness:
- Unpremultiply before the sRGB OETF on the display and export blits;
  encoding premultiplied color corrupted antialiased/transparent edges.
- Tag exported video as full-range BT.709 (matrix/primaries/transfer).
- Run perception effects (invert, brightness/contrast, hue/saturation)
  in gamma space to match standard editors.
- Interpolate gradients in gamma space across the raster and vector paths.
- Render effect thumbnails in the same linear space as the live pipeline.

Brush performance:
- Store the raster canvas as Rgba16Float (no shadow banding from 8-bit
  linear), with an incremental per-tile ping-pong sync replacing the
  per-frame full-canvas copy.
- Do the linear->sRGB readback conversion on the GPU and reuse a cached
  scratch texture, dropping a ~110ms-per-stroke CPU decode.

Cleanup:
- Single COLOR_WGSL prelude and shared CPU sRGB scalars instead of ~8
  duplicated copies of the transfer functions.
- Shared compute-pipeline builder; smudge folded onto the tile-sync path.
2026-06-16 08:32:39 -04:00
Skyler Lehmkuhl 54d5764bd0 Make beats canonical representation rather than seconds 2026-06-02 13:06:36 -04:00
Skyler Lehmkuhl f372a84313 Massive tempo refactor - make beats canonical time rep and allow them to be non constant 2026-04-02 10:26:01 -04:00
Skyler Lehmkuhl ae146533d9 Update automation lanes too 2026-04-01 10:17:52 -04:00
Skyler Lehmkuhl 3fc4773ec3 Fix remaining sites that weren't updating properly on BPM changes 2026-04-01 09:33:35 -04:00
Skyler Lehmkuhl cfb8e4462b All events now have three time references for seconds, measures/beats, frames 2026-03-30 10:15:55 -04:00
Skyler Lehmkuhl 65a550d8f4 Add piano roll note snapping 2026-03-24 19:24:24 -04:00
Skyler Lehmkuhl fa40173562 Add automatable volume and pan control to default instruments 2026-03-23 23:27:05 -04:00
Skyler Lehmkuhl 434b488a4c Merge branch 'rust-ui' of https://git.skyler.io/skyler/Lightningbeam into rust-ui 2026-03-22 18:16:25 -04:00
Skyler Lehmkuhl f16e651610 work on vector graph 2026-03-22 18:16:17 -04:00
Skyler Lehmkuhl 0d7f15853c Set default timeline mode based on activity 2026-03-20 21:05:00 -04:00
Skyler Lehmkuhl 121fa3a50a Add count-in 2026-03-20 20:51:50 -04:00
Skyler Lehmkuhl c938ea44b0 Add metronome 2026-03-19 01:16:26 -04:00
Skyler Lehmkuhl 84a1a98452 Snap to beats in measures mode 2026-03-19 00:47:15 -04:00
Skyler Lehmkuhl 164ed2ba73 Add velocity and modulation editing 2026-03-18 23:35:18 -04:00
Skyler Lehmkuhl 6b6ae230a1 Add pitch bend support 2026-03-18 23:11:24 -04:00
Skyler Lehmkuhl 4f3da810d0 Add automation inputs for audio graphs 2026-03-18 11:25:48 -04:00
Skyler Lehmkuhl d7a29ee1dc Double CPU performance by using tiny-skia instead of vello CPU 2026-03-13 18:52:37 -04:00
Skyler Lehmkuhl 3bc980d08d Use audio engine as source of truth for audio tracks 2026-03-11 12:37:31 -04:00
Skyler Lehmkuhl b8f847e167 Add drawing tablet input support 2026-03-11 10:58:30 -04:00
Skyler Lehmkuhl ce7ed2586f Support Vello CPU fallback on systems with older GPUs 2026-03-10 21:39:01 -04:00
Skyler Lehmkuhl 7a3f522735 Give metatracks explicit node graphs 2026-03-10 20:20:46 -04:00
Skyler Lehmkuhl f9b62bb090 Add frames timeline mode 2026-03-10 15:54:54 -04:00
Skyler Lehmkuhl ac2b4ff8ab Improve idle performance 2026-03-10 02:41:44 -04:00
Skyler Lehmkuhl 26f06da5bf Add gradient support to vector graphics 2026-03-10 00:57:47 -04:00
Skyler Lehmkuhl 06973d185c Merge branch 'rust-ui' of https://git.skyler.io/skyler/Lightningbeam into rust-ui 2026-03-09 13:41:48 -04:00