Skip to content

Release Notes

What changed in each Caddis release. The app updates itself in the background — quit and relaunch to pick up the newest version listed here.

1.0.1 — August 4, 2026

The first update since launch, and it's a big one: the geometry attribute system grew up. Per-point and per-vertex data now survives every shape operation, every attribute field is a proper picker with built-in @ properties, and new selection tools — the Select node, named groups, Apply To — turn rules into groups without writing a single expression. Around it: node-graph quality of life (bindable node shortcuts, forgiving search, drop-a-wire-to-create), and fixes for point simulations, File Cache playback, and a subtle compositing seam.

Added

  • Attributes survive geometry ops. Nine shape operations silently destroyed per-vertex data — a color gradient through a Resample Shape rendered flat, Merge Shapes ate every array, clones came out unpainted. All nine now carry attributes under one registry of policies: continuous data (scale, opacity, color) blends, categorical data (glyphIndex, sourceIndex) snaps to the nearer parent, rotation blends the short way around — and no node gets to choose differently. Fixed: Resample Shape (blends at the arc position), Merge Shapes (true concatenation), Shape Morph (colors cross-fade A→B; one-sided data carries), Text On Path, Clone To Points (both single-shape and palette variants), Shape Exploder, Thicken (both stroke contours inherit the vertex they hug), Subdivide Shape (corners copy onto the boundary, interiors blend — gradients fill the mesh), and Shape Boolean (no vertex correspondence exists after clipping, so its arrays are present-and-defaulted rather than silently dropped). The rule is now enforced across the engine — no geometry operation may silently drop an attribute column — and the full survival matrix is published in the geometry attributes guide. This changes pixels in existing projects — chains that always should have shown gradients now do. Behavior change: index is an order, so deleting elements renumbers the survivors 0,1,2,… instead of leaving holes.
  • Attribute fields now show you what's on the stream. Every parameter that asks for an attribute name — fifteen of them, across Point Delete, Path Delete, Clone to Points, Draw Instances, Point Relax, Trim Path, Round Corners, Label Points, Shape Along Path, Attribute to Field and both Attributes nodes — was a text box barely nine characters wide with no list, no autocomplete and no way to know whether what you typed existed. They are now a proper picker: the @ properties with a line each explaining them, then the attributes actually flowing in from upstream with their type, count and value range, exactly as the Details tab shows them. You can still just type a name, including one nothing writes yet. An empty field states what the node does when it's empty — "All points" on Clone to Points, "Nothing deleted" on Point Delete — instead of leaving you to guess. A name that isn't on the incoming stream gets an amber warning listing what is; a name we couldn't check says nothing rather than crying wolf. And the two fields that create an attribute refuse an @ name outright, because that namespace belongs to the computed properties.
  • Point properties you can finally name. Every field that asks for an attribute name now also accepts a property the point already has, written with a leading @: @x and @y for position, @index for its place in the list, @indexnorm for that as 0–1, @n for the count, @id for the stable identity that survives filtering, and @rand01 for a random value that holds still as points come and go around it. Nothing has to be written upstream first. Point Delete filters by height with @y; Clone to Points clones a stable random fifth of a scatter with @rand01; Point Relax gives every point its own radius with @rand01; Label Points prints {@id}; and Attribute to Field turns any of them into a field, which hands position awareness to every field-driven node in the catalog at once. @ is reserved — your own attributes can never be named with one, so the two can never be confused. The per-path fields (Draw Instances' time offsets, Trim Path and Shape Along Path delays, Path Delete, Round Corners, Clone To Points' Group By, Shape Attributes' Group By) resolve through the same reader, with their own trio — @index, @num, @indexnorm — so delayAttribute: @indexnorm staggers a multi-path shape evenly with no tagging node, and Path Delete can drop "every path past the 3rd" directly. Point-only properties like @y are refused on path fields with a reason, not silently read as zero.
  • @index replaces @elem (and @indexnorm replaces @elemnorm). One word for one idea: @index is the element's position in the list, always available under Computed. Typing bare index reads the same value — unless something upstream deliberately wrote an index order (Point Attributes' Index target driven by a field), which then wins. Generators (Grid, Point, scatter, text, resample, connect…) no longer stamp a stored index column, so "On this stream" shows only data something chose to write. Old projects keep working: @elem/@elemnorm still parse as aliases, and bare index resolves everywhere it used to.
  • New node: Select — turn a rule into a group. One node answers "tag these": a range (first 50), every Nth element, an attribute comparison (scale At Least 2, @x Between −100 and 100), a stable random percent (keyed on point ids, so the subset survives edits upstream), or everything inside/outside a shape or mask field. Modes compose — chain a second Select set to Intersect for "right of center AND big" without any expression syntax — and the result is an ordinary attribute, so every Apply To field, Connect Points' From/To, Attribute to Field, and the delete nodes consume it directly. Point Ops category; works on points or shapes (per-point or per-path). While a Select is selected, the viewport shows its members bright and the rest dim — the group is visible before anything consumes it.
  • Named groups: tag two point sets, then connect one to the other. Tag each branch with its own group (Point Attributes, Custom target — group1 on one grid, group2 on the other), merge, and Connect Points' new From and To fields draw only the bridges — neither set webs internally. The same name in both fields meshes that group alone. A group is just an attribute name, so points can belong to several, and AttributeToField turns any group into a 0/1 mask field. The From/To fields also take comparisons — a Rule dropdown appears once a name is set, so "connect points with scale over 4 to the grp2 set" needs no tagging node at all. Works in Nearest, Radius and Delaunay.
  • Rule-based delete. Point Delete and Path Delete gain a Rule dropdown: alongside the classic attribute/mask threshold, Range deletes an element-order span and Every Nth thins periodically — "delete every 3rd point" no longer needs an attribute at all, and Invert flips any rule into "keep only these."
  • Apply To on six more geometry nodes. Draw Points, Shape Smooth, Subdivide Shape, Point Relax, Points Along Path and Point Advect gain the standard Apply To row at the top of the panel — name an attribute, intrinsic or group and the node acts only on that selection, with the same Rule/Value comparisons as Connect Points' From/To (scale · Greater Than · 4). Each node applies it the way you'd hope: Draw Points draws only the selection, Shape Smooth pins the rest as anchors, Subdivide Shape passes unselected paths through, Point Relax keeps unselected points as immovable repellers (pinned-anchor layouts), Points Along Path picks which sub-path to distribute along, and Point Advect holds unselected points in place while the rest flow. Picking an intrinsic like @x auto-switches the rule to a comparison (@x · Greater Than · 0 = right of center) — "member of a coordinate" was mental gymnastics, so the rule now speaks the attribute's own units. Applies to Connect Points' From/To too.
  • Merging no longer eats attributes that only one branch carries. Merge Points used to keep an attribute only when every input had it, which silently deleted per-branch tags. An attribute on any input now survives: missing entries fill sensibly (scale/opacity at 1 so merged-in points stay visible, group tags at 0 = not a member). index renumbers across the merged stream, and sourceIndex stays distinct however deeply merges nest.
  • Vectors and colors are readable wherever a name is. Pointing one of these fields at a Vec2, Vec3 or color attribute used to do nothing at all, silently — velocity from Track Points was unreadable everywhere. They now reduce the same way in every node: vectors by length, colors by luminance.
  • Connect Points: field-driven connection order. Sequential mode now follows the per-point index attribute (new orderBy param, default Index), so rewriting index with Point Attributes — from a Distance Field, noise, image luminance, any field — re-routes the polyline through the points in that order. Connect a scatter in SDF-distance order, pair with Trim Path for grows-outward reveals, or animate the field and watch the thread re-route live. Every point source stamps index positionally, so existing Sequential networks are unchanged; orderBy = Input forces raw point order when you're using index for variant selection instead.
  • New node: Point. One point at a position — that's it, and that's the point. No more coaxing a Grid into degenerate dimensions when you just want a single point to clone onto, advect, or build from. Count stacks several at the same spot with distinct identities, so a field or Random can fan them out — an instant burst from one location.
  • Shake a node to disconnect it. Wiggle a node side-to-side while dragging and it pops free of all its wires without interrupting the drag — grab a Blur out of one chain and drop it into another in a single motion. With Heal on delete enabled (the default), the chain reconnects around the departed node. The pop is one undo step, and the node flashes briefly so it never reads as a glitch.
  • Node shortcuts: bind your go-to nodes to keys. Preferences → Shortcuts has a new Node Shortcuts section — search for any node, press a key combination (say ⌥T for Transform 2D), and from then on that combination creates the node at your cursor whenever the node graph is focused, with the same auto-wiring as the menu. Shortcuts are per-machine, conflict-checked against existing bindings, and removable with one click.
  • Drop a wire on empty canvas to create the next node. Releasing a dragged connection over empty graph space now opens the node menu right there, filtered to nodes that can actually accept that wire, with a Suggested group on top — pick one and it's created and connected in a single step (and a single undo). Works dragging backward from inputs too. The regular right-click/Tab menu also shows Suggested picks when it's about to auto-connect to the selected node. Suggestions blend sensible defaults per port type with the nodes you actually create most (tracked on this machine only). Escape or clicking away still cancels the wire — and wires released over another panel no longer stick to the cursor.
  • Node search now forgives typos and knows nicknames. The node menu's search matches skipped-letter queries ("trns" finds Transform 2D), word initials ("cs" finds Color Split), and the names you might type coming from other tools — "t2d", "gaussian", "particles", "bloom", "wiggle", "pixelate", and friends. Exact and prefix matches still rank first, so existing muscle memory is untouched.
  • File → Export… menu item (⌘⇧E). The render dialog no longer depends on the timeline toolbar's Render button, which narrow windows can squeeze out of view — the File menu (and its shortcut) opens it from anywhere, even with the timeline panel closed. The same trial/license rules apply as the button.
  • Release notes on the docs site. docs.caddis.app now has a Release Notes page with the full history of what changed in every shipped version, updated automatically when a release goes out.

Changed

  • Properties sections remember whether you folded them. Collapsing a section now sticks — per node type, across sessions — so you set up a panel the way you like it once. A folded section that contains something you've changed shows a count next to its name, and a section you've never touched opens itself rather than hiding an active control behind a default you didn't pick.
  • Stray controls no longer sit above the ones you came for. A handful of parameters had no section assigned and, through a quirk of how the panel was built, that pushed them to the very top — so Trim Path opened on a delay field you can only use in one mode, and Text opened on eighteen OpenType checkboxes instead of the font. Unassigned controls now sit at the bottom where they belong.
  • Much faster frames with heavy point and shape geometry. The engine used to send every geometry node's full point list, path list and attribute summary back to the app on every single frame, whether or not anything was looking at it — at a million points that was two thirds of the frame spent serializing data straight into the bin, and it multiplied by the number of geometry nodes in your chain. It now sends only what the interface is actually showing: the selected nodes, whatever the viewport gizmo is editing, and one hop upstream of those. Nothing about what you see changes; scrubbing and editing dense point work just stops stalling.

Fixed

  • Point Advect no longer pulls points back to their seeds when the field animates down. Keyframing a field's strength (or evolution, scale — any animated field) invalidated the simulation every frame, so each frame re-ran the whole history with that frame's field frozen: as the field died, the replayed history integrated to nothing and every point snapped home. The simulation now replays history with the field evaluated at each past frame, so a weakening field slows points where they are — and scrubbing, exporting, or reopening a project reproduces sequential playback exactly, from any frame. Playback with an animated field is also much faster: it no longer re-simulates from frame zero on every frame.
  • Playback no longer freezes on one frame downstream of a baked File Cache. After baking, a File Cache serves frames from disk and skips evaluating the nodes above it — but the nodes below it were handed a cache key that never changed from frame to frame, so Draw Points and friends kept serving whichever frame rendered first, with occasional jumps when the quality level flipped. The served file's frame and identity now feed the downstream keys, so cached playback advances every frame like it should.
  • Dark seam where antialiased shapes overlap. Overlapping shapes showed a subtle dark outline along the top shape's edge (most visible white-on- white — up to 12% darker at half-coverage pixels). The GPU shape renderers produce premultiplied-alpha edges from MSAA, but the rest of the engine composites straight alpha, so edge pixels were effectively darkened twice. Every draw producer (Draw Shape, Draw Text, Draw Glass, Draw Instances, Draw Points) now converts to straight alpha before handing off. Antialiased edges also composite correctly over any backdrop now, not just this white-on-white case.
  • Hardened Draw Instances / Slitscan against garbage frames on memory-constrained GPUs. On some Windows machines, instance palettes could show solid blocks of uninitialized GPU memory (often white) after a non-fatal device error — Macs hid the same failure because their drivers hand back zeroed memory. Palette assembly now verifies source sizes and falls back to a full-coverage blit (no layer can be left uninitialized), array sampling avoids a shader pattern some drivers miscompile, and the status-console GPU warning now includes the device's own error message — naming exactly which allocation failed — instead of a generic notice.
  • Connection strands and trails now actually blend point colors. Point color has always been copied onto each strand's endpoints, and the docs promised gradients — but the renderer only applied per-vertex color to fills, and strands are open paths drawn as strokes, so they came out in the flat stroke color (white). Strokes now interpolate endpoint colors along their length on the GPU: connect a red group to a blue group and every bridge runs red→blue. (Dashed strokes and text outlines keep their flat color — the dash splitter and rich-text color rules are unchanged.)
  • Trim Path's staggered delays now complete — and don't snap. A delayed path could only ever reach partway (end can't exceed 1, and the delay was subtracted straight off the window), so at 0.999 a staggered shape sat half-drawn, then leapt to finished at exactly 1 through an unrelated shortcut. Delays are now a proper stagger: values are normalized order (0–1, exactly what @indexnorm supplies), a new Stagger control sets how much of the animation they occupy, and every path completes smoothly at exactly end = 1. Existing delay setups render differently — they were relying on the snap to finish at all.
  • Thickened gradient strokes rendered flat white. Thicken tags its contour pairs with a glyph group to knock out donut holes, and the per-vertex color renderer excluded glyph-grouped paths from colored strokes (a rich-text rule). Gradient outlines now stroke as gradients.
  • Subdivide Shape ignored its Grid Size. The bounds pre-pass read a differently-named parameter (always the 20px default) and its result was reused for rendering, so changing Grid Size did nothing. It now takes effect.
  • Point Trail's two outputs no longer overwrite each other. Point Trail emits both a shape and a point stream, and the app filed them under the same key — so panels and gizmos asking about one could be handed the other, depending on which arrived last. Each output is now tracked separately.
  • The Details panel and attribute pickers no longer go stale when you change selection. Selecting a node after a frame had cached could show an empty Details tab and an attribute list missing names that were really there — the cached frame was answering with the captures requested by the previous selection. Cached pixels are still reused; the inspection data now refreshes whenever the selection asks for more than the cache holds.
  • Point Delete no longer opens with an error on a control you haven't touched. Its Attribute field shipped pre-filled with selection — a name nothing in the app ever writes — so a freshly dropped node flagged itself as broken while doing exactly what it should (nothing). The field now starts empty, and existing projects carrying the old value are cleared on load.
  • An attribute field connected to a Mask input no longer describes the wrong behaviour. A connected Mask overrides the typed name entirely, but the field went on stating what would happen if it were empty. It now shows as driven, like every other overridden control.
  • A typo in Draw Instances' offset attribute no longer produces a plausible cascade. A missing or misspelled name used to fall back to the point index — sequential offsets that looked intentional. It now reads as no offset, and the field flags the unresolved name.
  • Stroke Color and Name are built-in attributes again. The geometry Details panel was badging strokeColor and name as custom attributes and sorting them below your own, because the editor kept its own hand-written copy of the built-in list and that copy had fallen behind the engine. The list is now generated from the engine, so it can't drift again.
  • Timecode readout clipped at larger text sizes. With Text Size at 125% or 140%, the timeline's current-time box (and the zoom percentage next to it) overflowed its fixed width — both now grow with the text setting.

Security

  • Caddis now refuses to run if its own code has been modified. The macOS and Windows builds ship with Electron's asar integrity fuses enabled, so the app verifies its bundled code against a hash sealed into the signed application before it starts, and will not load code from anywhere else. The debug entry points that let a packaged build be re-run as a plain scripting host are switched off in shipped builds too. This does not change anything you can see or do in the app.

1.0.0 — August 1, 2026

Added

  • A new opening moment. The sign-in and project screens now open onto a living version of the caddis.app wave gradient — the site's colors flowing and evolving under film grain, dimmed to app brightness — instead of a flat dark panel. It renders on the GPU for next to nothing, pauses when the window is hidden, and stays still if your system asks for reduced motion. Buttons and fields on these screens pick up light outlined strokes, and the purchase card drops its beta-era ask for a straighter pitch: one purchase covers all of Caddis V1.
  • Free 7-day trial — Caddis is out of beta. Signing in with any email now creates your account and starts a free 7-day trial of the full app: no waitlist, no manual activation, nothing held back. The trial clock starts the first time you open Caddis (not when you sign up), and the status bar shows how many days remain. Beta testers roll into the same fresh 7-day trial automatically — same sign-in, same projects, same devices.
  • "Get Caddis" purchase modal. Buying a license now has a proper in-app surface: what you get (a perpetual license for all of Caddis V1, unlimited exports, 2 devices — no subscription), the price, and a checkout button. Checkout opens in your browser with your email prefilled, and the license activates automatically when you come back to the app — no key to paste, no restart.

Added (nodes)

  • Connect Points + Label Points — point clouds that explain themselves. Two new Point Ops nodes. Connect Points turns any point stream into a line network: link each point to its nearest neighbors, join everything within a radius, build a clean Delaunay mesh, outline the convex hull, or thread one polyline through the points in order — the classic plexus / web / constellation looks in one node. A fade control dims longer strands, endpoint colors blend into per-strand gradients, and the output is ordinary shape geometry, so TrimPath draw-ons, ResampleShape, and per-path styling all just work. Label Points puts a live text label next to every point — {index}, {x}/{y} coordinates, or any per-point attribute via {attr:name} — rendered as real vector glyphs with font, size, offset, and anchor controls, plus the same mask selection convention as Point Delete for labeling only the points you care about.
  • DrawAscii: character size decoupled from grid density. New Char Scale parameter scales each glyph about its cell center while the sampling grid stays put — so a 4K comp can keep a dense, detailed grid without the letters shrinking to specks. Below 1 adds padding around each character; above 1 (up to 8) the letters grow past their cells and overlap their neighbors for chunky, poster-style ASCII, with the glyph atlas automatically re-rasterized at matching resolution so big letters stay sharp. Default 1.0 renders exactly as before.

Fixed

  • PointDeform Normal/Tangent modes now push evenly all the way around a circle. Driving them with a scalar field (a wave, noise) secretly scaled the displacement by each point's rotation angle — points near 135° and 315° barely moved and half the ring moved backwards. Radial rigs (wheels, rings, orbits) now respond to the wave uniformly.
  • Tab now hops cleanly between parameter fields. Typing a value and pressing Tab commits it and moves straight to the next field with its value selected — type immediately, no clicking. Shift+Tab goes backwards, and the cycle wraps within the Properties panel. Previously Tab wandered onto dropdowns and toggles (lighting them up with a web-style focus ring), and a second Tab could trigger the node graph's Tab shortcut and pop the node menu out of nowhere. The bright keyboard-focus ring on dropdowns is gone too.
  • Oscillate no longer clips tall waves into plateaus. Turning amplitude above 1 used to slice the top and bottom off the wave — a sine driving ShapeDeform produced flat-topped bumps instead of taller arcs. The wave now scales freely for geometric uses (deform, particle motion); only the normalized 0–1 reading (color ramps, opacity) still clips at the rails, as a normalized signal should.

Changed

  • A finished trial no longer locks you out. When a trial ends you can still sign in, open projects, and keep working — only rendering is gated. The Render button dims and opens the purchase modal instead of the export dialog, so nothing dead-ends into an error. (Beta builds locked the whole app at expiry.)

0.2.5 — July 26, 2026

Added

  • Link toggle for the graph editor's In/Out ease boxes. A chain-link button between the two influence fields (same icon as linked parameters like Scale) mirrors an edit on either side to both — each keyframe takes the sides it actually has.

Fixed

  • Imported SVG logos no longer render random letters as outlines or fill in letter counters. Whether a contour without an explicit closepath imported as closed depended on invisible floating-point differences in where it sat on the canvas — the same letter shape could fill at one position and import open (outline-only, with its counter hole filling solid) at another. Contours now close within a sensible tolerance, and fill-only artwork follows SVG's rule that fills always implicitly close.
  • Merge input order no longer changes on its own. Inserting a node onto one of a Merge's input wires (or healing around a deleted node) silently moved that input to a different position in the stack, and edges without a stored position could reshuffle after unrelated graph edits — with the Properties list and the render disagreeing about the result. Every connection now gets a stable position, editor and engine order edges identically, and new connections land on top of the stack.
  • The Merge/Shape Merge input list now reads like a layer stack. The top row in Properties is what renders on top, and the wire fan on the node's input pill matches (top slot = top of composite). Sequence-style inputs (Shape Morph steps, Merge Text, instance sources) still read first-to-last. Reorder drags also track the pointer accurately over long distances.
  • Ease In/Out boxes no longer lock up for group selections. Selecting a run of keyframes that included the first or last key in a curve greyed out the In (or Out) influence box for the whole group. It now stays editable whenever any selected key has that segment; keys without it (the very first/last) are simply skipped.
  • The playhead grab handle is back in the Keys and Graph timeline views. The triangle at the top of the playhead line was only visible in the Layers view: the Keys (dope sheet) view drew it underneath the opaque frame ruler, and the Graph editor never drew one at all. Both views now show the same draggable triangle as the Layers view.
  • Published values no longer freeze the whole viewer with a false "circular dependency" error. A Value Source node that wasn't wired to anything — parked in the graph mid-edit — could still register a cross-layer dependency; combined with a publish flowing the other way, the engine refused to render every frame of the comp. Layer ordering is now computed only from consumers that actually evaluate, so stranded nodes and param publishes can never manufacture a cycle. Genuinely circular wiring is still rejected — and instead of a silently black canvas, the viewer now shows a banner naming the layers in the loop (e.g. "Box" → "Controls" → "Box") in plain language, and the node graph highlights the exact nodes whose references close it with a red border and a CYCLE badge.
  • Editing or animating a published value updates its consumers again. Nodes downstream of a Value Source cached their results without accounting for the published value itself, so dragging a published Constant (or keyframing it) left consumers replaying a stale frame forever. Published values now flow into the cache keys, exactly like Layer Source references.
  • Publishing, unpublishing, renaming, or re-pointing a published value now refreshes the frame. These edits live on the composition rather than any layer, and the frame cache never noticed them — the viewer kept showing the pre-change image until an unrelated edit forced a re-render.
  • Renaming a published value updates every consumer automatically. Value Source and layer-parenting consumers used to keep the old name and silently go dead; the rename now travels to all of them in a single undo step.
  • A missing publish no longer snaps consumers to zero. If a Value Source's publish name can't be resolved (renamed or deleted publish), the driven parameters now keep their stored values instead of collapsing to 0 — previously position jumped to comp centre and scale collapsed, making content vanish.
  • Fixed the Published panel corrupting parameter publishes on rename. Editing a param publish via the pencil button created a broken duplicate entry that resolved to nothing. The edit now renames correctly, and broken entries already saved in project files are cleaned up automatically on load.
  • Published textures no longer crop, shift, or crash when layer canvases differ. Reading a published texture from a layer with a different internal canvas size performed a raw copy that cropped the image — or triggered a GPU validation error. Mismatched sizes are now resampled.
  • Render and project-sync failures now show a dismissible banner in the viewer (and log to the status console) instead of failing silently to the developer console.

0.2.4 — July 21, 2026

Fixed

  • Image-sequence import no longer falls apart when a frame can't be read. Importing a numbered run (e.g. Blender's 0001.exr, 0002.exr, …) used to silently fall back to individual still images if the first frame's header couldn't be read at that moment — which is exactly what happens with cloud-placeholder files (OneDrive/Dropbox online-only), antivirus holds, or transient locks, especially on Windows. The sequence is now detected from the directory scan alone; dimensions are filled in on first render. And if File → Import Image Sequence can't import the picked file, it now says why in an error dialog instead of silently doing nothing.
  • DWAA/DWAB-compressed EXRs now decode. EXRs saved with the DWAA or DWAB codec (Blender's smallest-file option) previously rendered as fully transparent frames — the built-in decoder doesn't support those compressions. They now decode through the bundled ffmpeg automatically, at full float precision. Applies to single EXRs and sequences alike.
  • "New Project" now fully clears the previous project. Asset folders — and other editor-side organization like color tags, guides, and node groups — no longer carry over from the previously open project when creating a new one (or when opening a project file that doesn't have them).
  • Faster renders when content sits far outside the frame. Layers whose content extends way off-canvas (oversized guide shapes, elements pushed aside) no longer force giant hidden render canvases: the engine now proves which regions can never reach the final frame and skips them — up to ~9x faster per frame on affected scenes, with pixel-identical output. Scenes where the off-canvas content later re-enters the frame render exactly as before. (CADMIUM_DISABLE_OVERSCAN_CAP=1 restores the previous behavior.)
  • Much faster renders for layers with far-off-center content. Layers whose elements travel a long way from the frame (arms sweeping in from off-screen, parked passes that slide through) previously rendered on a single giant canvas stretched to reach them. The engine now renders each node into a small, precisely-placed window instead — 4-5x faster per frame on affected real-world projects, with no change to how anything is authored. Layers using nodes outside the supported set automatically keep the previous behavior. (CADMIUM_DISABLE_OVERSCAN_OFFSET=1 restores the old canvases wholesale.)
  • Blur edges no longer depend on hidden canvas size. Clamp-edge blurs used to smear whatever happened to sit at the edge of the layer's internal render canvas — an invisible implementation detail that could change the picture when the canvas resized. Blur edges are now derived from the content itself, so results are stable and slightly more correct near the frame edges on affected layers. (CADMIUM_DISABLE_CLAMP_BLUR_FIX=1 restores the old behavior.)

0.2.3 — July 20, 2026

Added

  • More shortcuts, all rebindable. Timeline: Enter renames the active layer, Delete removes selected layers, Alt+↑/Alt+↓ move the active layer in the stack, Cmd+Shift+D splits the selected layer(s) at the playhead (AE convention — previously button-only), and Cmd+Shift+R reverses the stacking order of the selection. Assets: Enter renames and Delete removes the selection panel-wide. These now live in the same customizable command system as everything else, so each can be rebound in Preferences → Shortcuts.
  • Customizable keyboard shortcuts. Preferences (Cmd+,) gains a Shortcuts tab listing every command by area with its current binding: click one, press the new combination, done. Search, per-shortcut reset, and Reset All included. If the keys are already in use in the same context you choose — Take Over (moves the shortcut, the other command loses it), Keep Both (context rules decide which runs), or Cancel. Customizations are per machine; menu actions (Save, Open…) and structural keys (Escape, Undo) stay fixed for now.
  • Delete removes the selected layer(s) from the viewer. With a layer selected in the viewport (Select mode), Delete/Backspace now deletes it — same as the timeline's delete button, one undo step. Edit mode keeps its meaning (deleting shape points), as does the Pen tool.
  • Cmd/Ctrl+D duplicates compositions from the Assets panel. Select one or more comps in Assets and duplicate them in place — the copies are selected afterward, and a multi-comp duplicate is a single undo step.

Fixed

  • Alt+↑/↓ moves the active layer in the stack again. The layer-reorder shortcut was unreachable — an arrow-key guard in the layer list swallowed Alt+arrows before the reorder code could run.
  • Dope sheet: Cmd+V no longer double-pastes. With a keyframe clicked and both clipboards populated, pasting could apply the selected-keyframe paste AND the param-row paste in one keystroke. Keyframe selection now takes precedence; param-row copy/paste applies only when no individual keyframes are selected.
  • The I eyedropper shortcut works. Documented as "activate the eyedropper while a color picker is open", it was wired to plumbing that no longer existed and did nothing. With a color picker open, I now arms that picker's eyedropper — same as clicking its eyedropper button: click the viewport to sample, Esc cancels.
  • E enters edit mode from the transform the viewport selected. Clicking a shape in the viewport selects its Transform — pressing E then did nothing until you manually clicked the shape node. E now walks upstream from the selected node to the shape or text feeding it (per branch, so the right shape is targeted when several are merged) and opens the editor on it.
  • V reliably returns to plain selection. Pressing V now also cancels the active tool — previously it set the viewport mode but left the Pan tool (H) engaged, so the viewport stayed in pan mode.
  • Colorize keeps transparency. Putting Colorize after anything with transparent regions — a matte, a shape on an empty canvas — flooded the whole frame with the ramp's darkest color, destroying the transparency. The output now carries the source's alpha through (output alpha = source alpha × ramp alpha), and brightness is read from the true un-premultiplied colors, so soft matte edges no longer read darker than they should. The same applies to the colorField output when Colorize re-maps another color field. To colorize the full canvas from a transparent source on purpose, Merge it over a Solid first.
  • Math node: the B input now drives Snap's step. With the operation set to Snap, wiring a value into B did nothing — the node only read the Step param, which has no port. A connected B now sets the snap step from upstream; the Step param still applies when B is unwired.

0.2.2 — July 15, 2026

Added

  • Missing-file badges and relinking in the Assets panel. Assets whose file can't be found on disk now show a red Missing badge (re-checked whenever the app window regains focus). Right-click any asset for a new context menu: Relink / Replace File… points the asset at a new file and every node that referenced the old one follows automatically — one undo step, metadata re-read from the new file. Rename and Remove round out the menu.
  • Composition duration in seconds. The Duration field in Composition Settings now has a Frames / Seconds switch — type 30 seconds instead of computing frame counts in your head. The helper text shows the equivalent in the other unit, the choice is remembered, and changing the frame rate preserves whichever unit you're working in (in seconds mode, a 30-second comp stays 30 seconds when you switch fps).

Fixed

  • Windows: imported assets show their filename, not the full C:\… path. Asset display names were derived by splitting on forward slashes only, so Windows paths never split. All name and sequence-grouping logic now handles both separator styles — image-sequence detection on Windows gets this fix too. Note: assets imported before this fix keep the name they were saved with — rename the row (double-click or Return) or re-import to clean them up; newly imported assets are unaffected.
  • Assets panel: the "In Use" tag now covers every file type. It only checked Image and Video Source nodes, so audio, SVG, image-sequence, and LUT assets never showed as in use. The check now matches any node that references the file, so new source types are covered automatically.
  • Shared project folders: source nodes find their files on the new machine. Opening a shared project resolved assets in the Assets panel but left every Image/Video/Vector Source node pointing at the previous machine's absolute paths — forcing a re-drag of each node from the panel. Node paths now follow their asset's resolved location, files inside the project folder (like generated/ AI outputs) re-anchor automatically, and relative paths saved on Windows resolve on Mac and vice versa.
  • SVG import: wordmarks and outlined text from Affinity Designer render correctly. Contours that return exactly to their start point without an explicit closepath (Z) — how Affinity exports letter outlines — were imported as open paths, so letters lost their fill and counters rendered filled instead of cut out. They now import as closed, filling and subtracting the way every other SVG renderer treats them.

0.2.1 — July 10, 2026

Added

  • Text Animate: overshoot and bounce, drawn on the progress curve. The In/Out progress params are now open-range — keyframe past 1 (or below 0) in the graph editor and the letters travel past their resting position and settle back; opacity and blur stay clamped so nothing flashes. A new Stagger Mode on the Timing group chooses how characters share that curve: Time (the default for new Text Animate nodes) delays the full curve per character by Stagger Frames, so a multi-bounce landing drawn once on the curve plays for every character at its own landing; Progress slices the 0→1 range per character as before, with out-of-range values moving the whole line together. Existing projects keep their current look, and the progress rows show a small "(overshoot)" tag whenever the value at the playhead is outside 0–1.

0.2.0 — July 9, 2026

Added

  • Variable font support. Selecting a variable font on a Text node exposes its design axes — Weight, Width, Slant, Optical Size, and any custom axes — as regular keyframeable sliders in the Typography section. Axes are continuous: keyframe Weight from 100 to 900 and the glyphs, spacing, and kerning all interpolate smoothly. Values clamp to the font's declared ranges, switching between variable fonts carries matching axes over, and static fonts keep the familiar named-weight dropdown.
  • Fonts that travel with the project. New projects scaffold a fonts/ subfolder, and any font files in it load automatically when the project opens — alongside system fonts. Hand the project folder to a teammate and it renders with the same typefaces, nothing to install. (Existing projects: just create a fonts/ folder next to the .caddis file.)
  • Format Number node (Text category) — turns any scalar into display text: fixed decimals, zero-padding (0042), thousands separators, prefix/suffix. Wire Time → Format Number → Text for frame counters, timers, percentage readouts, and price tags without leaving the graph.
  • Rich text: style part of a paragraph. Select characters while editing text in the viewport and a style bar appears — change the weight, size, or color of just the selection, or Reset it back to the base style. Mixed sizes grow their line to fit, styles stick to the text as you keep typing and editing, per-character colors survive Text Animate reveals and blurs, and everything flows through Text to Shape for further graph work. Range colors are fill colors: a stroked (outlined) text keeps its outline color on recolored words, in both Draw Text and the shape pipeline. Weight styling works on static families and variable-font Weight axes alike.
  • OpenType typography controls. A new OpenType section on the Text node exposes the font's real typographic machinery as toggles: kerning, standard and discretionary ligatures, contextual and stylistic alternates, small caps, fractions, oldstyle figures, tabular figures (fixed-width digits — the fix for jittery counters and timers), and stylistic sets 1–8. The panel lists only the features the current font declares, so it stays honest and compact.
  • Case transform. A Case control (Typography section) renders text as UPPERCASE, lowercase, or Title Case without touching what you typed — applied before shaping, so ligatures and kerning see the final letters, and editing, per-range styles, and Text Animate keep working on the original text.
  • Baseline shift and small caps for a selection. The viewport style bar gains two controls: shift just the selected characters up or down (ink-only — line spacing is unaffected), or flip them to the font's small-caps forms (the toggle only appears for fonts that provide them). Both stick to the text through edits like the other per-range styles.
  • The style bar understands base vs. override. With the whole text selected, weight and size changes write the Text node's own params — the Properties controls (including a variable font's Weight axis) stay live instead of being masked by a full-length override. And on variable fonts the bar's weight control becomes a numeric field driving the weight axis, replacing the nine fixed names. Bar numbers scrub like Properties fields: drag to adjust (Shift = coarse, Alt = fine, Escape reverts the drag), click to type. Color follows the same rule: a whole-text pick writes Draw Text's Fill Color — the painter stays the painter — while a sub-range pick colors just those characters.
  • Character styles are visible — and keyframeable. The Text node's Details tab lists every styled range (the affected text, swatch, and overridden properties) with a one-click Clear, and Draw Text calls out when upstream character colors override parts of its fill — no more silently dead parameters. A keyframe diamond on the Character Styles section hold-keyframes the whole style set, the same stepped model as keyframed text content: style-bar edits then land on the keyframe at the playhead. (Smooth per-glyph animation remains Text Animate's job.) Undo granularity inside a text edit session matches the rest of the app: each style action is one undo step, a scrub or color-picker session is one step, and typing coalesces into one step per burst — Cmd+Z peels changes off action by action instead of reverting the whole session.
  • Per-axis point scale — a new Scale X/Y target on Point Attributes. Alongside the uniform Scale target, Scale X/Y writes an independent X/Y scale per point: set a linked (unlinkable) X/Y value, or drive per-axis strength from a vector field. Clone to Points stretches instances along their local axes, and Draw Points renders per-axis-scaled points as ellipses. Existing uniform scale setups are unchanged.
  • Drag multiple assets at once. Dragging a row that's part of a multi-selection now carries the whole selection: drop on a folder to file everything (compositions included), or drop on the viewer/timeline to create a layer per asset — as a single undo step.
  • Importing one frame of an image sequence now asks. When a single imported image matches a numbered run on disk, a dialog offers Import Sequence or Import Single Image instead of silently pulling in the whole sequence. Multi-file drops still auto-collapse, and File → Import Image Sequence remains the explicit path.
  • Double-click anywhere on a layer's row to rename it — previously only the name text itself responded, so near-miss clicks did nothing. Return still works.
  • Draw Instances node (Render category) — Clone to Points for pixels. Stamp any rendered texture at every point in a Points set: a whole effect chain (an animated shape with its glow and shadow intact), an image, or another layer's output via Layer Source. Instances read the same per-point attributes cloning already uses — rotation, scale, opacity, color tint — and render as a single instanced GPU draw, so point order is stacking order. Connect several sources and they form a palette; Selection Mode picks per point: Sequential, Random (seeded), or Point Index (the index written on each point). Built-in cell fitting turns mixed-aspect images into uniform tiles — Fit or Fill into a cell size, with rounded corners — so an animated image grid is just images + Draw Instances + a Grid of moving points.
  • Staggered copies — per-instance time offsets on Draw Instances. Each instance can play the source at its own delay: Sequential cascades by a fixed step, Random shuffles the delays, Attribute reads a per-point value. One animated source becomes a wave of copies, each a few frames behind the last. Offsets sample into a bounded set of time slots (Max Time Samples), and the result is a pure function of the timeline — scrub anywhere, play, or export and it always renders the same. For heavy chains, drop a File Cache upstream and offset frames read from disk; the node shows the same "may be slow" hint as the other time nodes when that would help.
  • Clone Index node (Utility category) — a for-loop over copies. Place it in the chain feeding a Draw Instances and the chain re-evaluates once per instance, with Index / Count / Normalized / Random outputs describing which copy is being built. Wire Index through a Math node into a shape's radius and five copies render at five sizes; use Normalized for smooth ramps across the set, or the seeded Random for stable per-copy variation. Capped by a Max Clones control, and it composes with time offsets — each copy gets its own index and its own delay.
  • Index is now a first-class target on Point Attributes and Shape Attributes. Randomizing or ramping which variant each point shows used to mean writing a custom attribute by its internal name — now it's just target: Index, driven by a field, a value, or the index ramp. Clone to Points and Draw Instances read it directly in their Point Index selection mode (renamed from "Attribute" for clarity), and Draw Instances' time offsets read the same attribute by default — one written index can drive variant choice and stagger together.
  • Glow grows up: real falloff, real reach, and an isolated output. Radius now means what it says — the distance the glow visibly travels — and a new Falloff control picks the shape by look, not by number: Soft (the classic single-blur glow), Natural (bright core with a natural halo), or Long Tail (a tight core with long atmospheric tails). Under the hood the layered falloffs stack blur octaves at doubling radii with the core always at full strength, so pushing Radius makes a small shape bloom out across the whole frame instead of washing away. A linked Stretch X/Y pair bends the glow directional for anamorphic streaks, and a new Output option renders the glow by itself, no source underneath: grade it, distort it, stamp it with Draw Instances, or composite it back on your own terms. Existing projects keep their exact look (they load as Soft).
  • Preferences, with an Appearance tab. A new app settings dialog (Cmd+, on macOS under Caddis → Settings…, Ctrl+, on Windows under Edit → Preferences…) — the home for app-wide options going forward. First up, the most-requested readability controls: Text Size (up to 140%) scales the type throughout the interface while panels, controls, and the viewport keep their size; and Text Contrast: High brightens labels and secondary text throughout the app. Both apply live and persist per machine. The default look is unchanged.
  • Caches now scale to your machine. Every big frame cache used fixed one-size-fits-all budgets — a 192 GB studio machine got the same few gigabytes as a 16 GB laptop. Now the engine's frame cache uses a quarter of system memory (2–24 GB), RAM preview and the playback cache each scale to a quarter of memory (4–16 GB), and the GPU texture cache scales to the graphics hardware: an eighth of memory on Apple Silicon (where GPU memory IS system memory), and half of dedicated video memory on Windows graphics cards (a 24 GB card gets a 12 GB cache; a 4 GB card keeps a safe 2 GB). Big machines hold far more of the timeline and node graph hot; smaller machines get safer budgets than before. The env overrides (CADMIUM_FRAME_CACHE_GB, CADMIUM_GPU_CACHE_MB) still win.
  • Cut connections with a swipe. Hold Alt and drag across any wires in the node graph — a dashed cut line follows the cursor and every connection it crosses is deleted on release, as a single undo step. No more clicking each wire individually.
  • Deleting a node no longer breaks the chain. Delete a Blur sitting between an Image Source and the Output and the two reconnect automatically — including whole runs of deleted nodes, fan-outs to several consumers, and only when the port types actually match. On by default; the heal toggle lives in the node-graph toolbar next to auto-wire, and delete + heal is one undo step.
  • The cache bar now shows resolution, and manual quality means it. Cached frames in the timeline ruler are colored by the resolution they were rendered at — green for Full, light blue for 1/2, gold for 1/4, matching the gizmo color family; RAM preview bars follow the same coding — and the viewer's quality dropdown wears the same color as a live tier readout. Auto-quality behavior explains itself at a glance. And picking a manual quality now evicts cached frames from other tiers, so switching to Full shows Full immediately instead of replaying a stale lower-resolution cache.

Fixed

  • Running out of GPU memory no longer crashes Caddis. On graphics cards with less memory (4 GB class), a heavy scene could exhaust the GPU and kill the engine outright — the app became unusable. Three fixes: GPU errors now recover instead of crashing (caches are cleared and a warning appears in the status console — lower the viewer quality if it repeats); the engine's texture cache now has a real memory budget (2 GB by default, tunable via CADMIUM_GPU_CACHE_MB) instead of growing without bound on deep graphs; and the Node Preview panel got its own cache — it was silently forcing the entire graph to re-render and re-allocate on every frame whenever it was open alongside the main viewer.
  • Tab now walks through all Properties fields. Tabbing previously dead-ended when it reached a curve widget (Remap curves, Levels curves, index curves), which sat in the tab order but ignored the Tab key.
  • Playback no longer flickers between resolutions. Two compounding bugs. Auto quality kept re-trying a resolution tier it had just measured as too slow — heavy scenes (a very large Glow, say) oscillated 1/2 ↔ 1/4 forever. Upshifts are now predictive: the next tier is only attempted when its estimated cost actually fits the frame budget, and a failed attempt backs off exponentially before retrying. And the playback frame cache kept frames at whatever quality they were first rendered — mixed-resolution frames replayed as flicker on every loop pass even with a steady quality indicator. Cached frames now remember their tier and quietly re-render when it doesn't match. Auto quality also resumes from the tier it last settled on instead of re-paying the discovery cost on every play.

0.1.29 — July 3, 2026

Added

  • Compositions now live in the project panel's tree. Instead of a separate list pinned to the bottom of the panel, each composition appears as a row at the top of the same tree as your assets — file them into folders, color-tag them, and find them by search. Click a composition to switch to it, double-click for settings. The New Composition button now sits in the tree's toolbar and doubles as a drop target: drop an asset onto it to create a composition sized to that asset.
  • Shape Deform and Point Deform can weight X and Y independently. The Amplitude control is now a linked X/Y pair — unlink it to push a deformation harder along one axis than the other in XY mode. The single-axis and normal/tangent modes still show one value. Existing projects keep their current amplitude.
  • Node Preview — a second viewport. A new panel (View → Panels → Node Preview) shows the currently selected node's output, so you can inspect a single node against the full composition side by side. It updates as you scrub and edit. Image-producing nodes preview directly; value/geometry nodes that only become pixels downstream show as transparent.
  • Workspaces — save and switch between panel layouts. A workspace switcher in the top bar lets you save the current arrangement as a named workspace and jump between them. Update writes changes back into the active workspace (a dot flags unsaved layout changes), and you can rename or delete your own workspaces. A built-in Default is always available and can be reset to the shipped layout.
  • Panels can now be closed and reopened. Hover a panel's tab for a × to close it — its space goes to the neighboring panel. Reopen any panel from View → Panels, which also brings tabbed-away panels forward; a reopened panel returns to the slot it was closed from. View → Panels → Reset Layout restores the default arrangement, and layouts with closed panels survive restarts.
  • The project name now shows at the top of the window (Mac), centered in the title strip, with a dot marking unsaved changes — previously the title was set but never visible anywhere in the app.
  • Copy / Paste in the keyframe right-click menu. Right-clicking selected keyframes (dope sheet or graph editor) now offers Copy and Paste at Playhead alongside the interpolation options — the pasted keys land with the earliest on the playhead, keeping their relative offsets.

Fixed

  • The editor no longer fails to load from a panel layout it doesn't recognize. A saved layout that referenced a panel not present in the current build (e.g. after a downgrade) would crash the whole editor on launch. Unknown panels are now dropped from the layout on load, and the layout falls back to the default if nothing usable remains.
  • The app shuts down cleanly on system termination signals, rather than leaving its background rendering process orphaned — which could interfere with the next launch.
  • Unsaved-changes tracking works again. Edits were never being registered as unsaved: the title bar didn't show the unsaved dot, auto-save never ran, and quitting with unsaved work didn't warn. All three are restored.
  • Nodes added from the node-graph toolbar now keep their placement. The position of a toolbar-created node was written under a malformed key, so the node could snap to a default location instead of where it was dropped.
  • Text box gizmo follows a driven Box Size. When a Text node's box size was wired from another node, the edit-mode gizmo ignored the driven value and drew from the raw parameter instead.
  • Editable Shape "Auto" close state resolves correctly. The tri-state closed control's Auto setting always resolved to open; it now defers to the drawn sub-path's own closed state as intended.
  • Panel resizing works again around the Assistant panel. Dragging a divider on any split containing the Assistant panel silently failed (the resize math crashed mid-drag because the panel had no registered minimum size). Resizing is also hardened so a layout carrying an unrecognized panel can never break dragging again.
  • Keyboard copy/paste of keyframes now works on Windows. The Edit menu's shortcut registration was swallowing Ctrl+C / Ctrl+V before the timeline ever saw them, so copying keyframes with the keyboard silently did nothing on Windows. The Edit → Copy / Paste menu items now also operate on selected keyframes (they previously only handled text).
  • Keyframes can now be placed at — and beyond — the end of a clip. Dragging keyframes in the dope sheet stopped one frame short of the composition's end, so an animation could never be made to finish exactly on the clip boundary. Keyframes can now be dragged onto or past the end; values hold after the last keyframe, and any keys beyond the boundary are preserved if the clip is later extended.
  • Noise octaves now add detail instead of restructuring the pattern. Each octave used to be averaged into the total, which dimmed the base layer every time an octave was added — going from 1 to 2 octaves visibly changed the large shapes instead of refining them, and raising Roughness washed out the base as well. Octaves now stack on top of an unchanged base layer, and Roughness only controls the strength of the added detail. Octaves is also properly fractional now — values like 2.5 blend the last octave in halfway (previously only whole numbers had any effect), so animating octaves ramps detail in smoothly. Existing projects using more than 1 octave will render with slightly stronger base shapes; single-octave noise is unchanged.
  • Invert no longer looks too bright. The Invert node now inverts in display space by default, so the result is the expected visual complement — a mid-gray stays mid-gray and a light tone becomes the equally-dark tone, instead of brightening. A new Space control lets you switch back to the old scene-linear inversion for physically-based compositing. Existing projects update to the new default automatically.

0.1.28 — July 1, 2026

Fixed

  • Audio now works on Windows. The app couldn't locate its bundled ffprobe helper when inspecting audio, so importing an audio file — or exporting any composition that contains audio — failed with a "program not found" error. Windows now finds the bundled tool correctly. (Mac was unaffected, as was video-only work on Windows.)

0.1.27 — July 1, 2026

Fixed

  • The app no longer fails to start on PCs with certain GPU drivers. The engine used to probe every graphics backend at startup (including OpenGL), and on some machines a broken or outdated graphics driver would crash it during that probe — leaving the app unable to open at all. It now uses only the native backend (Direct3D 12 on Windows, Metal on Mac), which resolves the crash.

0.1.26 — June 29, 2026

Added

  • Shader node — write, paste, or AI-generate custom GPU shaders — a new node that runs a fragment shader you author in WGSL, acting as a generator, filter, or compositor depending on how many image inputs you give it. Edit the code in a resizable Properties editor with line numbers and live compile-error markers; declare image inputs and tweakable params with a one-line pragma — or the + input / + param buttons — and they become node ports and keyframeable sliders/colors. A dedicated Assistant panel writes the shader from a plain-language description ("a VHS glitch with chromatic aberration"), exposes the knobs you ask for, and auto-fixes compile errors — using either Caddis AI (token-metered, choose from several models) or your own Claude/OpenAI key (called directly from your machine — the key never leaves it).
  • Paste Shadertoy shaders (GLSL) — switch the Shader node's format to GLSL and paste a Shadertoy-style mainImage shader. Procedural effects, noise, fractals, and raymarching all work — they render, animate, and match Shadertoy's appearance, with params exposed as node knobs. The Assistant can write or adapt them too. (Single-pass procedural shaders; image-channel inputs and multipass are not yet supported.)

Fixed

  • The app always opens a window now — even when the graphics engine can't start. On some machines a graphics-driver crash during startup left the app running in the background with no window at all. The window now opens first and shows a clear error (with a crash report you can send) if the engine fails to initialize, instead of vanishing silently. (Seen on Windows with certain GPU drivers; updating the graphics driver resolves the underlying crash.)

0.1.25 — June 28, 2026

Added

  • Transform multiple layers as a group — select 2+ layers and the viewport shows one bounding box that moves, scales, and rotates all of them together about the group center. Arrow keys also nudge the whole selection. Works on any layers — a Transform2D is auto-inserted where one is missing.
  • Layer parenting + Null objects — a Parent column in the timeline lets a layer inherit another layer's (or a Null's) transform, so moving/animating the parent moves its children while each keeps its own offset. Parent chains work — an arm parented to an elbow parented to a shoulder inherits the whole chain. Moving a parent's anchor point re-pivots only that layer; its children hold their place. Multi-select to parent several layers at once. A New Null toolbar button adds a transform-only layer to parent a group to. Unparenting keeps the layer in place. (assumes default Pixels position mode; nulls are moved via Properties/arrow keys.)
  • Text Animate — scalar offset fields + independent X/Y amplitude — the Offset Field input now accepts a scalar field as well as a vector field. A scalar field drives the per-glyph position offset on both axes from one sample (black = at rest, white = fully offset — same "black = none" convention as the Scale/Rotation/Opacity fields), and the field amplitude is now a vec2 (X/Y) instead of a single value — so you can zero one axis to push letters along the other (e.g. a noise/gradient scalar field driving only horizontal letter drift), or scale X and Y independently. Vector fields still drive the two axes separately. Existing projects are unchanged. Beta-user requested.
  • Keep a matte source layer visible — a layer used as a track matte is normally hidden from the final composite (it only supplies the matte). An eye toggle on the matte source layer's own row in the timeline lets it also render while still acting as the matte — like turning the matte layer's eye back on in After Effects (e.g. text matted by a rectangle you still want to see). Off by default (unchanged behavior); per-layer.

Changed

  • Composition Settings & Export dialogs — input pass — the text/number fields no longer show Chromium's yellow autofill highlight or the native number-spinner arrows, and now use a quiet accent focus border consistent with the rest of the app (shared .dialog-input style + a reusable NumberField). The "Motion Blur" heading uses the standard engraved section-label style.
  • Composition Settings — resolution — Width and Height can be aspect-locked via a chain toggle (same icon as the Transform2D scale link); editing one dimension scales the other. The resolution presets are expanded and grouped — HD / UHD (720p, HD, 1440p, 4K UHD, 4K DCI), Vertical / Social (9:16, 4:5, 1:1), and Film (2.39:1).
  • Export — format picker — the eleven export formats are now split into a Type selector (Video / Image Sequence / Animated / Audio) that narrows a second Format list, instead of one long flat menu.

Fixed

  • Cmd/Ctrl +/− (and trackpad/Wacom pinch) no longer zoom the whole app — these used to scale the entire UI like a web page, and once engaged they broke the node-graph wheel (it scrolled instead of zooming). Browser/page zoom is now blocked at the source (menu zoom roles removed, pinch/visual zoom locked, ctrl+ wheel suppressed); the zoom hotkeys instead zoom the panel under the cursor (node graph, viewer, and all three timeline modes) as if you'd scrolled there. Wacom zoom gestures now zoom the panel under the cursor too, instead of the app.
  • Generators & spatial effects no longer drift from unrelated nodes — Noise, Random, Voronoi, Gradient, Mosaic and UV Remap used to change appearance when a sibling branch in the same layer grew past the comp (e.g. raising Point Grid spacing feeding CloneToPoints, merged in alongside): the bigger branch enlarges the layer's internal render canvas (overscan), and these nodes were sizing their pattern/warp against that canvas rather than the composition — so the result stretched (anisotropically, in whichever axis grew) and shifted. They now anchor and scale to the composition region, so the output depends only on the node's own params — identical regardless of a sibling's overscan or the render quality (Draft/Preview/Full), and unchanged for existing projects (no-op when overscan is 1). (DistanceField, DrawAscii and Tile were already comp-correct.)
  • Composition Settings is now truly modal — clicking outside the dialog (or a drag that starts in a field and releases outside it) no longer closes it and discards your edits. It stays open until you choose Save or Cancel; Escape cancels. The Export dialog got the same treatment.
  • "Check for Updates" now shows what happened — the manual check used to surface nothing for the common outcomes, so it looked like it did nothing. It now reports every state — Checking…, You're up to date, Update available with live download progress, or an error — and the "Update ready" pill has a solid background so it's no longer easy to miss.
  • Dope sheet names stay aligned to their tracks — the parameter-name list (left) and the keyframe tracks (right) now scroll together in the dope sheet, so a param's name always lines up with its row.
  • Ease/interpolation menus flip up near the screen bottom — the right-click ease presets and interpolation menus (keyframe tracks and graph editor) now measure themselves and open upward when there isn't room below, instead of being clipped.
  • Point Grid rows/columns are whole numbers — the Grid node's Rows and Columns fields now snap to integers (minimum 1) when typed or dragged, instead of accepting fractional values.
  • Opacity sliders clamp to 0–1 — typing or dragging an opacity value can no longer push it above 1 or below 0. Field-driven opacity (via an input port) is unaffected and may still use any range.

0.1.24 — June 25, 2026

Added

  • Shape fills can follow the shape — new DrawShape Fill UV control: set it to Local and a connected fill_source (gradient, image, field, or shader) moves, rotates, and scales with the shape instead of staying pinned to the comp. The default stays Canvas (comp-fixed, the prior behavior); Local is opt-in. The same option is available for stroke_source via Stroke UV. Works for every source type.
  • Text Animate glyph anchor (Baseline / Center) — a Glyph Anchor control sets where every letter rotates and scales from: the baseline (default) or the letter's vertical centre (e.g. scale each character from its middle). Selecting a Text Animate node now also shows a per-glyph gizmo in the viewport — a dashed box around each letter with a dot at its pivot — so you can see and choose the anchor; the dots read the engine's exact pivots, so they match the render.
  • Shape Morph supports Expose Channels — the "E" button on Shape Morph now reveals Mix and Offset input ports, so you can drive the morph from another node (a Time node, a Math expression, a field, etc.) instead of only keyframing it. The driven param shows a link indicator in Properties.
  • Drive per-letter text rotation from a field — Text Animate has a new Rotation Field input. Feed it a Gradient/Noise/any field to rotate each glyph by the field value (a C4D Plain-Effector–style per-character effector), with a Field Rotation Amplitude control for the degree range.
  • Transform2D anchor-point picker — a 9-point grid in Properties snaps the transform anchor (rotation/scale pivot) to the content's corners, edges, or centre in one click. Picking a preset keeps the content visually in place (position auto-compensates), exactly like dragging the anchor handle in the viewport. (Distinct from the "Pin To" picker, which positions the layer relative to the comp frame.)
  • Anchor handle snapping — dragging the anchor crosshair in the viewport now snaps to the content's corners / edges / centre (the same 9 points the picker offers); the crosshair turns yellow while latched. Hold Cmd/Ctrl to drag freely.
  • Rotate shape elements as a unit — Shape Attributes' Rotation target gains a Rotate Geometry toggle that bakes the rotation into the shape. Paths sharing a glyphIndex (e.g. the bowl and counter of "o"/"a") rotate together, so TextToShape → Shape Attributes now actually rotates whole letters.

Changed

  • Text Animate starts as a clean pass-through — the In animation now has an In Animation toggle (like Out), off by default, so a freshly-added node applies no entry offset/fade and the field-driven effector workflow (rotation/ scale/offset fields) works immediately with amplitudes front-and-centre. The Timing params (Target/Stagger/Order/Easing) — which only drive the staggered reveal — are hidden when both In and Out are off. Existing projects keep their In animation running.
  • Transform2D "Anchored" position mode renamed to "Pinned" (and its "Position Anchor" edge picker to "Pin To") — clearer that it positions the layer against a comp-frame edge, separate from the Anchor Point rotation/scale pivot. Display rename only; existing projects are unaffected.

Fixed

  • Thicken on a closed shape makes a proper donut — thickening a circle (or any closed path) now cuts the inner hole as intended instead of filling a solid disc. The outer and inner contours are tessellated together so the hole knocks out.
  • Text Animate scale now resizes glyphs — per-glyph scale (the In/Out Scale and the Scale Field) previously moved glyphs toward a point instead of resizing them. Glyphs now scale for real. The Scale Field is also now strength-based like the Rotation Field — field 0 = no change, 1 = Field Scale Target (default 0 = shrink away) — so the same gradient drives rotation and scale coherently (black = no effect, white = full).
  • Per-glyph rotation keeps the baseline — rotating characters used to pivot around each glyph's bounding-box centre, so the baseline drifted letter to letter. Rotation (and scale) now pivot on a baseline-anchored point, so the baseline stays put.

0.1.23 — June 23, 2026

Added

  • Windows is here — Caddis now runs on Windows 10/11 (x64), signed and installable, with the full node/compositing engine, video import/export, and AI features at parity with macOS.

Changed

  • Device-limit screen links to your account — when a license hits its 2-device limit, the screen now has a "Manage your devices" button that opens your account page so you can deactivate a device yourself — handy for freeing a slot (e.g. removing an old Mac) when activating Windows.

Fixed

  • Dither no longer crashes on large compositions — applying Dither to a high-resolution comp could exceed an internal GPU buffer limit and crash the engine mid-render. Large comps now render through Dither cleanly.

0.1.22 — June 13, 2026

Added

  • Motion tracking — a full planar + point tracking suite. Planar Track analyses a flat surface (a screen, a sign) and bakes its four corners per frame; Corner Pin warps any layer onto those corners for screen replacement. Track once, scrub instantly, and nudge a corner mid-clip to lay keyframed corrections on top of the bake (the Mocha "adjust track" model). Track Points tracks many features or blobs with stable IDs and feeds them straight into the Points pipeline (DrawPoints / PointTrail / CloneToPoints). All classical, pure-Rust, no cloud round-trip. The Track controls sit at the top of the Properties panel with a coverage bar and a single-frame stepper.
  • AI video nodesGenerate Video (text-to-video, or image-to-video from a first frame, with an optional last frame for transitions), Generate Video Depth (temporally-consistent depth video), Remove Video Background (whole-clip AI matting to transparency), and Segment Video (text-prompted rotoscoping — name an object, get a tracked matte). Curated models per function with a fast/cheap pick plus premium tiers; cost scales with duration and is shown live before you spend. Generations run in the background and play back like any video clip.
  • Point Trail now outputs points and fades — a points output for dotted trails (via Draw Points), a Smooth mode that fits bezier curves through the samples, a tail Fade with an adjustable falloff, and Inherit Color to carry each source point's color down its trail.

Changed

  • Point Trail is now fully deterministic — the trail at any frame is a pure function of the timeline, so scrubbing anywhere, looping, or exporting from any start frame all produce the exact same trail. Sequential playback stays fast (incremental), and the old background "Building trails" pre-roll pass is gone — it's no longer needed.

Fixed

  • Point Trail no longer glitches on stutters or cache clears — trails were rebuilt from whatever happened to be evaluated, so a dropped frame or a cache clear could restart or shorten them. They now rebuild correctly from the source motion every time.

0.1.21 — June 11, 2026

Added

  • File Cache can now cache geometry, not just renders — connect a points or shape source (a particle sim, a heavy procedural chain) straight into File Cache and bake: positions, IDs, and all per-point/per-path attributes are stored on disk per frame (compressed, typically 100–1000× smaller than caching the rendered pixels) and the upstream chain — including stateful simulations — is skipped entirely on playback. Downstream Draw nodes stay fully editable against the baked geometry. The node's ports switch type on connection, the same way Transform2D does.

Changed

  • File Cache frames are ~5–20× smaller on disk — texture payloads are now byte-shuffled and zstd-compressed instead of raw float pixels (a 300-frame bake that used to take 37 GB lands in the low single-digit GBs for typical content, less for flat/graphic frames). Old uncompressed caches still read fine; re-bake to shrink them. Smaller files also mean proportionally faster cache playback.
  • Cached frames are kept hot in memory — File Cache reads now land in an in-memory LRU (512 MB for textures, 128 MB for geometry), so scrubbing back and forth or looping over cached frames stops re-reading and re-decompressing from disk every single evaluation.

Fixed

  • Baked File Cache frames no longer rescale on playback — a texture bake whose content extended past comp bounds (e.g. caching Draw Points after a point sim) was stored at the layer's overscan canvas size, but on read the pruned upstream chain made the engine size a smaller canvas and stretch the baked frame into it: the whole frame visibly shrank. Cache files now record their bake-time canvas, reads place content at 1:1 comp pixels (never a raw stretch), and a serving File Cache anchors the canvas size to the baked extent. Re-bake existing texture caches to pick up the recorded canvas; geometry caches are unaffected.
  • Selecting a File Cache node no longer disables it — clicking the node (e.g. to watch its bake status) made the Details-panel inspection pull the entire upstream chain back into evaluation on every render, silently erasing the cache's performance win. Inspection now respects cache hits.
  • Draw ASCII respects upstream transforms — a video or image scaled down through a Transform2D into Draw ASCII showed a blown-up center crop instead of the full picture: the overscan computation treated Draw ASCII as a pure generator and never followed its image input, so the source was cropped at comp bounds before the scale-down. The image branch is now walked like any other chain, and the ASCII grid is comp-aligned (cells are sized and anchored to the comp, continuing into the overscan margin) so cell size no longer drifts when the canvas grows.
  • Closed the remaining overscan-crop gaps — the same "secondary input gets cropped at comp bounds" bug fixed for Draw Shape / Draw Text fill sources in 0.1.20 also affected every other per-pixel map input the overscan walk skipped. Now walked: Merge's second-and-later inputs, UV Remap's UV map, Blur's blur map, Lens Blur's depth map, Vector Motion Blur's flow input, Draw Glass's background and distortion inputs, and all four Channel Combine inputs. (Lens Blur's bokeh kernel is intentionally excluded — it's a self-contained lookup, not canvas-aligned.)
  • Video sources now contribute to overscan after project reload — chain walks read video dimensions from a cache that was only populated when the editor probed a video on import. Probed video metadata is now mirrored into that cache on every render, so a reopened project gets correct overscan for video layers without re-importing.
  • Image Sample's "Comp" coordinate space is origin-centered — with coordinateSpace = Comp, samples were interpreted as top-left-origin comp pixels while every field consumer passes origin-centered coordinates ((0,0) = comp center, the project-wide convention). All samples landed shifted by half the comp, clamping the left/top half to the raster edge. "Comp" now recenters correctly; "Local" (the default) was always correct.
  • Engine no longer crashes on large field readbacks — wiring Image Sample or Optical Flow fields (e.g. Draw Field streamlines) in a comp whose canvas grew large (big comp × overscan) hard-crashed the engine: the raster→field CPU readback allocated a staging buffer sized to the full canvas, and wgpu treats exceeding its 256MB buffer limit as a fatal validation error. Field readbacks are now budgeted at 128MB — oversized canvases are downsampled on the GPU before readback (fields are smooth; consumers map coordinates through the raster's own dimensions, so this is visually transparent).

0.1.20 — June 9, 2026

Added

  • OpenEXR sequence export — export a comp as a numbered .exr sequence (Export → preset EXR Sequence). Frames are written as 16-bit half-float, linear scene-referred, ZIP-compressed RGBA — the engine's native working space, with no sRGB baked in — so they drop straight into Nuke / Resolve / comp pipelines. Complements the existing EXR/HDR input support.

Fixed

  • Long video exports no longer time out — exports that took longer than ~10 minutes failed with "Engine request timeout" regardless of how well they were going. The export timeout is now a keep-alive watchdog: it resets on every per-frame progress event, so an export can run for any duration as long as frames keep rendering, and only fails on a genuine stall (no progress).
  • Scaled-down fill/stroke sources are no longer cropped — feeding a large image or video through a Transform2D that scales it down into a Draw Shape or Draw Text fill (or stroke) source showed only the center crop, as if the scale hadn't applied. The overscan computation now follows the fill/stroke-source branches (like it already does for Mix and Matte inputs), so the full source is available before the scale-down — the whole image shows, scaled to fit.

0.1.19 — June 8, 2026

Fixed

  • Large / custom resolutions no longer crash the engine — comps wider or taller than 8192 px (the old GPU texture cap) hard-crashed the render engine. Caddis now requests the GPU's real maximum (16384 on Apple Silicon) and clamps render targets to it, so a high-resolution comp renders cleanly instead of crashing. Comps beyond the hardware limit render at the capped size (and slightly lower effective DPR) rather than failing. Custom resolutions in Composition Settings now accept up to 16384 per side.

0.1.18 — June 8, 2026

Added

  • Custom curve colors in the graph editor — click the color swatch next to a curve in the graph editor's parameter list to recolor it from the same palette used for layer/asset tags (or "Default" to restore the automatic color). The choice is saved with the project and is undoable. Default colors also now match the palette (X = red, Y = green, Z = blue), and a position split via Separate Dimensions keeps its X/Y curves red/green instead of falling back to yellow.
  • Alt-click a parameter name to reset it to default — in the Properties panel, hold Alt (Option) and click a parameter's label to snap it back to the node's factory default. Works for numbers, vectors, colors, toggles, dropdowns, text, fonts, and file paths. Handy when you've experimented a value into a mess and just want the original back. (Driven parameters — fed by an edge — aren't resettable this way, since their value comes from the source.)
  • F fits whatever you're pointing at — press F to fit the panel under the cursor: the Viewer fits the canvas to the frame (same as its "Fit" zoom), the Node Graph frames all nodes, and the Timeline's Graph Editor fits the visible keyframes in both axes (time and value). Each panel fits independently, so it always does the right thing for where you are.

Changed

  • Opening a project now always confirms before replacing the current one — double-clicking a .caddis in Finder (or picking one from Open Recent) used to swap out your open project with no prompt whenever there were no unsaved changes. Now it asks first: Save / Don't Save / Cancel if you have unsaved changes, or a simple "Open …? This will replace the project currently open" otherwise. (No prompt on the launch screen, where there's nothing to replace.)
  • Audio Source can import video files — its file picker now offers an "Audio & Video" filter, so you can point an Audio Source node straight at a video (MP4, MOV, etc.) to use just its audio, without needing the video itself. (The engine already extracted audio from video containers — this removes the picker restriction that hid them.)

Fixed

  • Gizmos and motion paths now render outside the comp bounds — the motion- path overlay (the curve + draggable position keyframes) and the shape-drawing previews (rectangle/circle/polygon/pen/text) were clipped at the canvas edge, so an object keyframed off-screen had no visible, editable path. They now draw into the full viewport like the other gizmos (matching the bounding-box, shape, and gradient gizmos), so you can see and adjust handles beyond the comp frame.
  • Timeline zooms out to fit long comps — the timeline zoom was floored at 1 px/frame, so after increasing a composition's duration (e.g. to several thousand frames) you couldn't zoom out far enough to see the whole thing. The zoom-out floor now adapts to the comp duration and the timeline's width, and a Fit button next to the zoom controls snaps the view to the full comp in one click. The ruler also thins out its ticks when zoomed way out.
  • Field Math "Power" no longer freezes the app — switching a Field Math node's blend to Power while it fed a Draw Field node could hang the engine (the canvas would stop updating and you'd have to restart). Fields are evaluated over positive and negative values, and raising a negative number to a fractional power (or zero to a negative power) produces "not a number"/infinity, which then drove the field visualizer into a near-infinite drawing loop. Power now does the math in a way that always yields a finite, direction-preserving result, and the field renderer can no longer be sent off to infinity. Other Field Math modes were unaffected.

0.1.17 — June 5, 2026

Added

  • Numeric ease influence in the graph editor — with keyframe(s) selected, the graph editor footer shows In % / Out % boxes to set the easing influence (handle reach along time) numerically, like Resolve. Adjusting the value changes the handle's time-extent while preserving its shape; typing on a linear keyframe converts it to an ease. In is disabled on the first keyframe, Out on the last.
  • Missing-font warning — when a project uses a font that isn't installed on your machine, the text silently rendered in a fallback before with no indication. Now an amber FONT badge appears on the node and a ⚠ marker on the font row in Properties, with a tooltip showing which font is missing and what it's being rendered with. (Fonts still aren't embedded in the project file — only the name is saved — so this flags the substitution rather than preventing it.)

Changed

  • Shift constrains bezier handle dragging — holding Shift while dragging a keyframe's ease handle now locks it to the dominant axis (horizontal-only or vertical-only), matching the existing Shift axis-lock for keyframe dragging.
  • Pen tool previews the actual curve — when the previous point has a bezier handle, the dashed rubber-band line to the next point now shows the curve that will be created instead of a straight line. (After a corner point it still previews straight, as before.)

Fixed

  • Sign-in no longer hangs when the server can't be reached — if the request for a magic link failed at the network level (fetch failed), the sign-in screen got stuck on "Sending…" forever with no error and no email. It now shows "Could not reach the server. Check your connection and try again." with a Try again button, so a transient blip is recoverable instead of a dead end.
  • Marquee select works in the speed graph — dragging a selection box in the graph editor's Speed mode now selects keyframes (it only worked in Value mode before). The box hit-tests against each keyframe's velocity, matching where the markers are drawn.
  • Export now matches the composition's frame rate — exports were locked to 30fps regardless of the comp setting (e.g. a 60fps comp exported at 30). The export dialog was reading a non-existent fps field and silently defaulting to 30; it now derives the rate from the comp like the rest of the app.
  • Node search now highlights the keyboard selection — in the node-graph add-node menu, arrowing through search results didn't visually highlight the selected item (because the menu opens under the cursor, which counted as "hovering" and suppressed the highlight). The highlight now follows the arrow keys and the mouse consistently.
  • Audio files are selectable from the Audio Source node — its file picker was falling back to an images-only filter, so audio files showed greyed out in the open dialog. It now offers the audio formats (mp3, wav, aac, flac, ogg, aiff, m4a), matching the Assets panel.
  • Font picker no longer gets cut off near the bottom of the screen — the font-family dropdown (Text, DrawAscii, etc.) now opens upward when there isn't room below and caps its height to the available space, so it's never clipped on smaller windows.
  • The launch and sign-in screens can be dragged again — they had no titlebar, so on macOS there was nothing to grab to move the window. They now have a draggable titlebar strip (matching the editor's), with the traffic lights in it.
  • The "Update ready" pill no longer overlaps the top bar — it's positioned below the toolbar so it's fully visible and clickable, including on smaller screens. The launch screen is also now responsive: it reserves room for the pill, centers when there's space, and scrolls instead of clipping when the window is short.

Removed


0.1.16 — June 5, 2026

Added

  • Shape Morph node — interpolate ("morph") between two or more shapes with a single keyframeable mix (0 = first input, 1 = second, 2 = third, … with fractional values blending the bracketing pair). Inputs don't need matching vertex counts — each is resampled to a common point count automatically. An Auto Align pass (on by default) matches winding direction and picks the lowest-travel start offset so the morph deforms in place instead of swirling through itself; Offset nudges the correspondence for fine-tuning or spin.

Changed

  • The "Update ready" pill is now fully clickable — clicking anywhere on it (not just the word "Restart") restarts and installs the update. It also gets a subtle accent outline so it's easier to notice.

0.1.15 — June 3, 2026

Added

  • Trackpad pan & pinch-zoom in the Viewer and Node Graph — a two-finger swipe now pans the canvas and pinch zooms it (cursor-centered) in both the viewer and the node graph. Mouse behavior is unchanged: the scroll wheel still zooms at the same speed, and Cmd/Ctrl+wheel remains an explicit zoom.
  • Trackpad gestures in the timeline (Keys & Graph) — a two-finger swipe now slides the timeline (and pans the value axis in the Graph Editor) instead of zooming, and pinch zooms the time axis. Previously both gestures zoomed. Mouse wheel still zooms as before.
  • Maximize a panel with ` — press the backtick key to expand the panel under your cursor to fill the dock area (the top toolbar and status console stay put); press again to restore. Especially handy for the node graph. The panel keeps its state — zoom, scroll, selection — across maximize/restore.

Changed

  • Repurposed the ` key — it now maximizes the hovered panel (see Added) instead of toggling the developer performance HUD. The HUD remains available via its toolbar button in dev builds.

0.1.14 — May 31, 2026

Added

  • Anonymous usage analytics (opt-out) — Caddis now collects anonymous, aggregate product analytics (which features and nodes get used) to guide development. No session recording, no personal data beyond your account email, and no third-party scripts run in the app. Turn it off any time via Caddis → Share Anonymous Usage Data.

Fixed

  • Easing presets on multiple keyframes — applying an easing preset to two or more selected keyframes now eases every segment correctly. Previously only the first keyframe of a selection became a bezier; the trailing keyframe stayed linear, so its incoming handle was discarded and the resulting curve was wrong.

0.1.13 — May 30, 2026

Added

  • Linked nodes — paste with relative references, AE-style. Cmd+C on a node, Cmd+Shift+V in any graph (same or different comp) creates a linked instance: at eval time the engine mirrors the source's params / bypassed / expose-channels, while local edges and ports stay independent so the same Glow plugs into different upstream content in each layer. NodeBox grows a cyan pill below its bottom- right with a chain glyph ("Go to source") and a broken-chain glyph ("Unlink — snapshot source values into this node"). Source resolution walks the link chain with cycle detection (max 8 hops); when the source is missing the local values act as the snapshot fallback. NodeInspector renders source params (keyframe-resolved at the current frame) as read-only when the link resolves.
  • Published nodes drawer — right-click any node body → "Publish as" registers it in the comp's publishedNodes registry. The bottom-left "Published" panel gains a Nodes section listing every published node across the project, each draggable into any graph (same or different comp) to drop a linked instance at the cursor.
  • Publish any param as a ValueSource target — right-click a Properties param row → "Publish as". The engine pre-resolves param publishes (keyframe-aware) into the same resolver used by port publishes, so consumers don't care whether the source is a port or a param. ValueSource auto-infers its output port type from the source param's ParamValue type (scalar / vec2 / vec3 / color / string; boolean flows as Scalar 0/1). Amber 3px bar marks the published row, matching the existing port-publish accent.
  • Cross-comp layer copy/paste with Cmd+C / Cmd+V — copies the layer's full graph + node positions + groups + token links + color tag in a single undo entry. Cross-layer matte refs survive when both layers are copied together; otherwise they're cleared.
  • Tapered stroke on DrawShape. Four scalar params (taperStart, taperEnd, taperStartLength, taperEndLength) shape the stroke width along each path — AE-style. Defaults are uniform; touch them for leaves, calligraphy, comet tails, etc. Per-vertex strokeWidth attribute on the upstream shape (write it with ShapeAttributes Custom target) overrides the params for full programmatic control. All four params expose as scalar inputs for animation.
  • Dashed stroke expose channels on DrawShape — dash_in, gap_in, dashOffset_in. Animate dashOffset for marching-ants / running- stitch effects.
  • String as a first-class port type, with a new String source node (keyframeable text, Hold-interpolated), a MergeText node that concatenates strings with a separator (same multi-input pattern as MergeShapes / MergePoints), and a content_in port on Text that overrides the literal text param. The Text node's own text param is keyframeable too — animate captions, lyric typography, or AI-prompt assembly directly.
  • MergePoints node — concatenates point streams with conservative attribute merging (attribute only survives when present on every input with the same type, so common attrs like opacity / scale don't get zero-poisoned). Always emits a sourceIndex per-point attribute for downstream routing.
  • PathDelete node — sub-path-level filter, companion to PointDelete at the vertex level.
  • Thicken node — wraps an open path as a closed shape with caps; closed paths emit donut topology.
  • Per-path delay convention for animated traversal. TrimPath and ShapeAlongPath both accept a delayAttribute param that reads a per-path scalar attribute and staggers timing per path — set it via ShapeAttributes Index/Custom on a multi-path shape for "branch growing" effects.
  • Expose-channel inputs surfaced across the node graph. Many nodes had the engine plumbing already but were missing from the E-button allowlist (or had no helper yet). New / restored toggles on: Voronoi, Vortex, RadialForce, PointsField, Checkerboard, Stripes, Remap, Threshold, ColorCorrect, DrawAscii, Crop, plus all nine Effects nodes (Mosaic, Posterize, Vignette, EdgeDetect, Sharpen, Grain, ChromaticAberration, Glow, DropShadow). New expose channels on Dither (levels, cycle), VectorMotionBlur (strength, phase), Oscillate (offset), PointScatter (count, seed), PointsAlongPath (always-on count_in).
  • Grid pattern-aware expose ports. Each Grid pattern (Grid / Radial / Hex / Sunflower / Jittered / Concentric) now exposes only the channels it actually consumes, instead of showing the same static four. Switching pattern swaps ports + prunes orphan edges.
  • Debug node actually inspects data. ScalarField shows source label + center value + min/max/mean from a sparse sample grid. VectorField shows magnitude + direction angle + magnitude range. ColorField shows a center swatch + alpha. Previously every field just read "Scalar Field" / "Vector Field" / "No input" — now you can see the values. String / Audio / Text cases that previously fell through to "No input" also render properly.
  • FileCache Properties redesigned. New Location row (full cache path + Reveal in Finder button), Status, Bake, Progress, and Last rows all use the standard param-row grid. The status badge ("…F BAKED" / "EMPTY") now sits as a pill above the node header instead of crowding the title.
  • DrawGlass NO-BG badge sits above the node header (same pill style as the new FileCache badge) so it doesn't overlap the E / bypass buttons.
  • Open Source Licenses screen (Help → Open Source Licenses…) + bundled GPLv2 / LGPLv2.1 / FFmpeg LICENSE.md / THIRD-PARTY.md texts for the bundled FFmpeg. Build provenance and a corresponding- source pointer ship with every release.
  • Drag-to-scrub on numeric parameter inputs. Click and drag horizontally on any numeric parameter in the Properties panel to adjust the value. Modifier keys scale the per-pixel increment: Shift ×10, Ctrl/Cmd ×0.1, Alt ×0.01. A small "sensitivity ladder" tooltip floats above the input showing all four levels with the active one highlighted in amber. Click without dragging still focuses the field and selects its contents for typing. Escape mid-drag reverts. The whole drag coalesces into a single undo entry.
  • Arithmetic expressions in numeric parameter inputs. Type expressions like 200 / 3, (1920 - 100) * 0.5, or 360 / 8 into any numeric parameter and they resolve to a number on commit (Enter / Tab / blur). Supports + - * / % and parentheses with standard precedence, plus unary + -.
  • Create a composition from a dropped asset. Dragging a video, image, vector, or audio asset onto the Compositions section of the Assets panel creates a new comp sized + timed to match the source media, with the asset already wired up as a layer. Video comps inherit the source's exact pixel dimensions, frame rate (NTSC-aware: 23.976 → 24000/1001), and duration. Image / vector comps inherit pixel dimensions and default to a 5-second duration at 30 fps.
  • Frame rate mismatch warning on VideoSource nodes. When a video clip's frame rate doesn't match the active comp's frame rate, the Properties panel shows a small amber warning above the node's params so it's obvious that playback may judder.

Changed

  • Performance HUD is dev-only. The viewport perf dashboard (toolbar button, overlay, backtick shortcut) is gated by import.meta.env.DEV so production builds tree-shake the dashboard out entirely. The underlying usePerfStore still tracks frame metrics for non-UI consumers.
  • EditableShape gizmo: AE-style multi-point selection. Click a point to select it (unchanged), Cmd/Ctrl-click another point to add it to the selection (was already implemented but undocumented), and double-click a point to select every point on the same sub-path. The corner ↔ smooth toggle that used to live on double- click stays accessible via Alt+click, matching the pen-tool convert affordance. Cmd+A still selects every point across every sub-path, so the new gesture sits between "one point" and "all points" for letter outlines or multi-loop shapes.
  • EditableShape: forgiving bezier-handle hit target. The 3px tangent dots now have a 9px invisible click ring, so grabbing a handle no longer requires pixel-perfect aim. When a handle sits on top of the path (common for shallow curves) it also takes priority over the segment-insert preview — previously the white "add point here" dot would steal the click and drop a new vertex on top of the handle.
  • EditableShape: marquee select. Drag in empty space inside the viewport to lasso every point inside the rect. Shift- or Cmd-drag extends the existing selection instead of replacing it. A plain click (no drag) still deselects, matching the old behavior. Middle-mouse and right-click pass through the gizmo, so the Viewer's pan + context-menu shortcuts keep working in edit mode.
  • Graph editor: forgiving tangent handle hit target. The 3px bezier tangent dots in the timeline graph editor now have the same 9px invisible click ring as the EditableShape gizmo. No more chasing a single pixel to ease a curve.
  • Graph editor: unified scope filter (Node / Layer / Comp). The old two-toggle header (Layers: Active/All + Params: Single/ Node/All) is gone — replaced with a single Node / Layer / Comp segmented control matching the dope sheet. Node lists only animated params on the selected node(s), Layer lists every animated param on the active layer, Comp lists every animated param across the whole composition. Scope state persists per timeline mode so a wide graph view can sit alongside a node- focused dope sheet.
  • Graph editor: focused selection + auto-frame. Clicking a param row now focuses that row — the previous selection drops away (no more reaching for the tiny eye bubble to remove it). Clicking the same row again toggles it off. Shift- or Cmd/Ctrl-click extends the selection so you can build a multi-curve view. After every change the value viewport vertically re-frames to fit every visible curve (1.2x padding), so the F-hotkey is now optional rather than required. Multi-curve views frame to the widest range.
  • Graph editor: every visible curve is editable. Curves no longer dim to 40% when they aren't the active param — everything in the multi-select renders at full opacity, every curve gets tangent handles for its selected bezier keyframes, and marquee sweeps across every visible curve at once. So you can Shift-click position + scale, marquee their keyframes, hit an ease preset, and the easing lands on both at the same time. The active row still gets a slightly thicker stroke so the panel-highlighted curve stays easy to find.
  • Graph editor: Shift now locks to the dominant drag axis. Previously Shift always zeroed the value delta (frame-only). Now a mostly-horizontal drag locks value (frame-only), a mostly-vertical drag locks frame (value-only) — so you can hold Shift to nudge a keyframe's value without worrying about it slipping a frame sideways. Decision is made from raw pixel deltas, so it tracks the cursor's actual direction.
  • Graph editor: drag readout + Shift snap to whole number. A small floating tooltip follows the cursor while you drag a keyframe, showing the target frame and value live. When Shift locks the drag vertically, the value snaps to the nearest integer (pivoted on the anchor keyframe so multi-select drags preserve relative offsets) — so coaxing a position from 1 → 8 or 100 → 250 lands on clean numbers without typing.
  • Graph editor: AE-style selection bounding box. Marquee or Cmd-click two-or-more keyframes (across one curve or many) and a thin dashed accent box appears around them. Drag the left or right edge to scale time around the opposite frame; drag the top or bottom edge to scale values around the opposite value. Multi-component params (Position vec2, Scale vec2, Color vec3/4) scale every visible component in lockstep, not just the first axis. Each scale is one undo step. Hidden for single-key selections (the keyframe itself is already a draggable handle).
  • Transform2D: Separate Dimensions. A compact split / merge icon button sits in the vec2 row next to the X/Y link toggle on Position / Scale / Anchor. Clicking it spawns a MakeVector pre-seeded with the current value, wires it to the matching *_in port, and migrates any existing vec2 keyframes into per-axis scalar keyframes so prior animation survives the toggle. From there, X and Y become independent — keyframe a quick horizontal bounce without disturbing vertical motion, ease them on different curves in the graph editor, etc. Clicking the same button while separated pulls the per-axis keyframes back into one vec2 param (interpolation degrades to linear on merge since the two axes can disagree). The MakeVector node stays in the graph. Each toggle is one undo step.
  • Gizmo writes + reads follow the wire. When T2D's position_in / scale_in / rotation_in / anchor_in is driven by an upstream MakeVector or vec2 Constant, both the drag-write and the gizmo's at-rest baseline now resolve to that upstream node — drags write to MakeVector x/y (instead of T2D's silently-overridden local params), and the box stays locked to the rendered position the whole time (it used to snap to (0, 0) the instant a drag started because the local position param read 0).
  • Gizmo stays anchored to a downstream T2D. Selecting a MakeVector that drives a T2D's spatial port keeps the bounding-box gizmo visible — so you can keyframe X / Y on the MakeVector while still nudging the layer spatially with the box. Previously the gizmo disappeared the moment the selection moved off the T2D itself.
  • Motion path overlay reflects driven motion. When position_in is wired (e.g. via Separate Dimensions), the motion path's shape is sampled per-frame from MakeVector's X + Y so the rendered visual exactly matches what the engine outputs (no fake spatial bezier overlay). Selecting either the T2D or the driving MakeVector keeps the motion path visible — the overlay walks forward one hop to find the downstream T2D. The keyframe diamonds toggle filled/hollow to show curved vs linear motion. Alt-click a keyframe toggles bezier interpolation on both MakeVector axes at that frame, so curves come from real per-axis scalar interpolation (fine-tune the bezier handles in the graph editor on each axis). Drag a keyframe to reposition writes to MakeVector x/y. Spatial 2D tangent handles aren't shown in driven mode — they don't apply to independent per-axis interpolation.
  • Default Text alignment is now Center (was Left). Text-tool box- text drag still forces Left for paragraph layout — point-text / graph-spawned Text starts centered, which is what most motion-design use cases want.
  • TextAnimate got a polish pass: In Offset X / In Offset Y collapsed into a single In Offset vec2 (matches the offset_in vec2 expose port); same for outOffset. The Out section is gated behind a new Out Animation boolean toggle, hiding the six follow-up rows by default so the panel reads cleaner. seed only shows when order = Random. Engine reads the new vec2 first and falls back to the old scalar params for unmigrated projects.
  • Font / Weight / Style rows in the Properties panel now have the 16px keyframe-diamond gutter, so their labels line up with the rest of the rows.
  • Drag sensitivity for [0,1]-ish params. Params whose names contain opacity, progress, mix, evolution, contrast, strength, threshold, anchor, or scale (case-insensitive substring — catches fillOpacity, strokeOpacity, fromOpacity, outFromScale, etc.) drag at step=0.01 instead of 0.1. A 100- pixel drag now traverses the full [0,1] range cleanly.
  • NodeBox title truncates with ellipsis above 18 uppercase chars so long names like "Chromatic Aberration" don't bleed into the E button. Full label still shows on hover.
  • Properties panel: the keyframe diamond (and driven-by link icon) now lives in a fixed-width column at the start of each param row instead of trailing the value. Diamonds stack vertically across the panel regardless of label or value width, making it easy to scan which params are keyframed. Applies to scalar / vec2 / vec3 / color / boolean / string / path rows, the Levels widget, and the Gradient / Curve ramp section headers.
  • Node context menu: reorganized the ~110 nodes into 14 categories oriented around source / operation pairs instead of the old action- heavy buckets. New categories are Source, Shapes, Shape Ops, Points, Point Ops, Text, Fields, Effects, Color, Composite, Render, Math, Time, Utility. The 28-node "Effects" mega-bucket is gone — image effects now split into Effects (filters), Color (grading), and Composite (multi-input). Per-node docs in docs/nodes/ were updated to match. New docs added for AudioSource, MergeShapes, and Note (which previously had no docs).

Fixed

  • Node bypass passthrough is now type-safe for multi-output and shape-typed nodes. The old logic walked a hardcoded input-port allowlist (in / bg / fg / source / a) and always wrote to out, with a ColorSplit special case that wrote transparent to its r/g/ b/a outputs — which is why bypassing ColorSplit (and similar shape nodes like ShapeDeform / ResampleShape / SubdivideShape) blanked everything downstream. New behavior walks declared input ports, finds the first connected input whose upstream type matches a declared output type, and fans the value out to every matching output port. Type-conversion nodes (ShapeToPoints, BreakVector, DrawShape) fall back to a transparent texture as before.
  • Cross-comp link cache coherence. diff_project now marks comps containing linked-node consumers dirty when their source layer changes. Without this, edits to a linked Glow's source didn't bust the consumer comp's frame cache and pre-edit pixels lingered.
  • Graph editor: keyframe + tangent drags are now one undo step. Both drag flows used temporal.pause() / resume() to wrap their mutations, which suppresses tracking entirely instead of batching it — so the drag's edits never entered history, and cmd+z after "ease → drag a value" reverted the ease and swallowed the drag in a single step. Now snapshots before pausing and pushUndoEntry on pointer-up, so each drag is its own history entry.
  • Curves: untouched curve no longer darkens footage. A half-texel- offset in the 256-bin LUT sampling biased bilinear interpolation toward the lower bin, compounding ~0.001 per stage across master + per-channel. Identity curve is now true identity at every stage.
  • ColorCombine: overscan walk no longer dead-ends. The chain walk follows PRIMARY_INPUT_PORTS to step backward through the graph, but ColorCombine's inputs (r_in / g_in / b_in / a_in) weren't in the list. Upstream content larger than comp was cropped at canvas bounds before reaching ColorCombine. Per-node-type primary port lookup added so the walk continues correctly.
  • Crop → Tile bounds. Crop was treated as a passthrough in the content-bounds walker, so Tile saw the full upstream image and tiled past the crop region. Crop is now a content-bounds terminal that intersects upstream bounds with its rect. Keying also added to the same walker's passthrough list (chains like Crop → Keying → Tile now resolve correctly).
  • Crop gizmo drag silently failed when clicking the central edge handle — pointer-capture routed events to an element with no move/up listeners. Move/up handlers lifted to the outer SVG group so any captured pointer event reaches the drag handler.
  • Dot node pre-pass passthrough. Chains like Noise → Dot → PointDeform.field_in lost the ScalarField during the pre-pass (AABB / overscan path), and the field consumer fell back to its default behavior. Added a Dot | Debug passthrough arm to the pre-pass for every CPU-side NodeOutput variant.
  • FileCache now actually writes to the project's cache folder. The initial loadProject ran before setProjectFile populated projectDirPath, so the engine stored None for project_dir and FileCache fell back to the OS temp dir. Subsequent syncs only fired on project-store changes, so opening a saved project and baking-without-edits never updated it. Fixed by injecting the resolved path before sending and subscribing to projectDirPath in the sync hook.
  • FileCache Reveal works on an empty cache. shell.openPath returned an error when the leaf directory didn't exist; reveal now mkdir -p's the path first.
  • ResampleShape bezier output no longer overshoots at endpoints — switched to centripetal Catmull-Rom (Yuksel et al. 2009) for smooth-but-non-overshooting tangents on multi-anchor paths.
  • Export dialog: "Half" resolution no longer fails — the engine renders at its configured quality level and ffmpeg now scales the piped frames to the requested output dimensions, so input and output sizes can differ.
  • Export dialog: "Work Area" range now actually maps to the timeline work area (previously silently fell back to full duration because of a tuple vs. {start, end} shape mismatch).
  • Export dialog: custom frame range / width / height / CRF / GOP inputs can now be fully cleared while editing without snapping back to 0, and no longer show Chromium's yellow autofill highlight.

Removed


0.1.12 — May 23, 2026

First version with a maintained changelog. Prior releases (0.1.0 – 0.1.11) are listed on the GitHub releases page but were not logged in detail.

Highlights since 0.1.0:

  • Auto-update via electron-updater + GitHub Releases
  • Sentry crash reporting with symbolicated source maps
  • Code-signed + notarized arm64 builds via CI
  • Stable version-less download URL for marketing/welcome emails
  • macOS file associations for .caddis and .cadrecipe with custom icons