Changelog
The SDK and the oxframe CLI are released together under one version. Before 1.0 a
breaking change bumps the minor version (0.19 → 0.20) and is listed with a before and after.
Since 0.20.0 every release also gets a video, made from these notes by the release-video Action.
0.20.2
Fixed
TitleCardandEndCardwrap a long title or second line inside the card, and the card grows to hold it. A long line used to run past the card's edges, which release videos with long notes showed.- Centered and end-aligned text with a
max_widthwas drawn beside its own box when it was narrower thanmax_width(a single short line, say). It now sits inside the box, so placing it andoxframe checksee where it really is.
0.20.1
Added
- Every issue in
report.jsonhasdocs: the link to its rule on https://oxframe.dev/docs/check-rules, with what the rule reports and how to fix it (Issue::docs). - The
showreelexample: scenes from other examples between a title and an end card, the reel on oxframe.dev.
Fixed
checkreportedtoo_fastat a cut between two scenes when the first node of each had the same id: node ids start again in every scene, and a move is now only a node added at the same place.
0.20.0
Parallel rendering.
Added
render --jobs 4renders a video in 2-second segments, several at once, each in its own process; the segments are joined without re-encoding and the soundtrack is added once. The default is one job per core, at most eight. A PNG sequence is split the same way.- The file does not depend on the number of jobs:
--jobs 1and--jobs 8write the same bytes. A 7-second 3D scene renders 2.7 times faster on 8 cores. oxframe_export::concatjoins segments encoded with the same settings.
Changed
- A video longer than two seconds now has a key frame every two seconds (where the segments meet), so files are a little larger than before.
0.19.0
3D scenes inside 2D ones, behind the 3d feature.
Added
Scene3dis a world of meshes:Mesh::cube(),Mesh::sphere(),Mesh::plane(),Mesh::cylinder(),Mesh::group()and models from a binary glTF (Mesh::glb_file)..color(),.metallic(),.roughness(),.emissive()and.opacity()set the material.scene.add(mesh)returns anObject3dwhose position, rotation, scale, color and opacity are signals, so.to()animates them;object.add(mesh)makes a child that moves with its parent.scene.camera()(position, target, field of view) andscene.light(Light::directional(..))orLight::point(..)are signals too; with no light, a default sunlight shades each face differently.View3d::new(&scene).size(640.0, 400.0)shows the scene as a 2D node: position, opacity, effects, the 2D camera and layout work as for any node.view.projector()maps a 3D point into the view, so a 2D label follows a corner of a model.- The renderer is a software rasterizer with a depth buffer and 2 × 2 supersampling: the same scene gives the same pixels in a render, in
check, in CI without a GPU and in the browser playground. DrawCtx::imagedraws a picture from a node of your own.- The
cube_3dexample.
0.18.0
Presentations from the same scenes.
Added
slide_break().awaitmarks a stop. A render plays straight through it;oxframe present(CLI 0.18.0) pauses there until the next key press. The last frame of every scene is a stop too.notes("…")adds speaker notes for the part that ends at the next stop.render --slides talk.pdfwrites a PDF with the frame of every stop, one per page, without ffmpeg.- The timeline sent to the editor lists the stops.
- The
slidesexample.
Fixed
- Rendering with
--subtitlesfromoxframe rendernow works: the CLI passes the flag to the project (CLI 0.18.0).
0.17.0
Matrices, vectors and tables.
Added
Matrix::new([[1, 2], [3, 4]])draws values in square brackets. Each cell is aTxtfromcell(r, c),row(r)orcolumn(c);row_frame,column_frameandcell_framegive hidden outlines to fade in..cell_width(px)leaves room for longer values.axes.vector(x, y)draws an arrow from the origin whose tip is two signals;v.to(x, y, seconds)moves it and.label("v")follows it.axes.apply_matrix(m, seconds)maps the plane through a 2 × 2 matrix: a transformed grid is drawn over the original, and plots, series, vectors and placed nodes move with it.axes.matrix()is theMat2signal;Mat2::aftermultiplies andMat2::detgives the determinant.Table::newtakes the column titles and.row(..)adds rows.insert_row,push_rowandremove_rowslide the other rows into place,sort_by_columnandsort_bymove every row to its sorted place, andtable.row(i)is aTableRowwith a background to highlight it.- Guide lesson 15, and the
matrix_multiply,linear_mapandtable_sortexamples.
0.16.0
Icons from Lucide, Tabler and Simple Icons, behind the icons feature.
Added
icon!("lucide:server")adds an icon by set and name:lucide:,tabler:(with-filledvariants) andsimple:for brand logos. Only the icons a project names are put into its binary. A name that does not exist fails to compile and lists the closest names..size(),.color()and.stroke_width()on the builder; theIconhandle hascolor()andstroke_width()as signals,Icon::pathsfordraw_on, andIcon::style(IconStyle::StrokeorIconStyle::Fill). The color defaults to the theme's text color.Icon::from_svgmakes the same node from a 24 by 24 SVG of your own.FlowDiagramputs an icon at the start of a card with.icon(name, icon!(..));flow.icon(name)returns its handle.- The
architectureexample.
Fixed
- Creating GPU renderers on several threads at once could crash inside the Vulkan driver. Renderers now share one Vulkan, Metal or DX12 instance; a GL-only machine still gets one per renderer.
0.15.0
Video clips in a scene, behind the video feature.
Added
Video::new().file("assets/demo.mp4")shows a clip as a node, sized with.width(pct(70))like an image, with.radius(24.0)corners, effects and the camera.demo.play(),demo.pause()anddemo.seek(2.0)control it;demo.playback_rate()anddemo.time()are signals, sodemo.playback_rate().to(0.25, 0.5)slows it down.- The frame shown is a function of the scene's time, so a render and a seek show the same frame. Frames are decoded with ffmpeg and cached; WebM (VP9) and ProRes 4444 keep their alpha.
- The clip's sound goes to the mixer while it plays at rate 1;
.muted(true)leaves it out. checkreportsvideo_unavailablewhen a clip cannot be opened.- The
videoexample.
Fixed
- The mixer read the first track of a file; for a video that is the picture. It now takes the first audio track.
0.14.0
Nodes of your own, behind the unstable-node-api feature.
Added
#[derive(Node)]on a struct that implementsDrawmakes it a node. Fields marked#[signal]get a setter on the builder (Gauge::new()) and a signal on the handle (GaugeHandle), sogauge.value().to(0.8, 1.0)animates them. Other fields are set once.Draw::drawpaints with the sameDrawCtxasCustom, andDraw::sizegives the box that layout,Camera::fitandcheckuse. The node gets position, opacity, effects, flex properties and theme colors like any built-in one, andchecknames it by its type.DrawCtx::arcstrokes or fills an arc, for gauges and rings.oxframe init --template node my-nodemakes a crate with a node, a demo scene and a reference test.- The
gaugeexample.
0.13.0
Charts: axes, number lines, function plots and bars, all following animated ranges.
Added
Axes::new()is a plotting box with a grid, axes, ticks and labels..x_range(-5.0..5.0)and.y_range(-2.0..2.0)set the data ranges,.width()and.height()the box,.no_grid()drops the grid.axes.x_range()is a signal ofInterval, soaxes.x_range().to(-10.0..10.0, 1.0)zooms; ticks stay on round steps and each label keeps its value while the range slides.axes.plot(|x| x.sin())draws a function as aPlot. It is sampled again every frame, finer where it bends, broken at jumps (no line across the gaps oftan) and cut at the box..stroke(Accent)and.width(4.0)style it,.draw_on()draws it, andplot.to_fn(|x| x.cos(), 1.0)morphs it into another function.axes.point_on(&plot, x)keeps a dot on the curve at a signalx.axes.c2p(x, y)gives the scene point of a data point, andaxes.place(node, x, y)keeps a node there.axes.bars([3.0, 7.5])draws bars with their values above them;bars.value(0).to(9.0, 1.0)grows one,.labels(["a", "b"])names them.axes.line_series(..)andaxes.scatter(..)draw data points.NumberLine::new(0.0..10.0)is a single axis with the same methods.- Guide lesson 14 (charts) and the
chartsexample.
Fixed
- A long
BulletListrow ran off both edges of the frame. Rows now wrap inside 70 % of the frame width, start right of the mark, and take the height they need. Short lists look as before. checkpointed at the component's own source for nodes a component adds. It now names the line of youraddcall.
0.12.1
Added
oxframe inspectlists each shader node's numeric parameters (amp,wetness, …) and the strength and speed of an effect, underparams.- Guide lessons 11 to 13 as examples: scenes and transitions, sound and voice, shaders and effects.
- The
oceanexample: the target scene of hybrid rendering.
Fixed
checkreporteddead_timewhile a shader parameter or an effect was animating, such as a growing wave or a wetting lens. Those changes now count as motion.
0.12.0
Shaders can read what is already drawn. A lens gets wet over the frame, a node ripples, and a scene gives way through rising water. All three run on the CPU, so check and a render without a GPU paint them, and the GPU renderer draws the same picture.
Added
shaders::lens_drops()puts drops on the lens over everything drawn before it, inside its box. Add it last and make it.fullscreen()..wetness()is a signal from 0 (dry) to 1, solens.wetness().to(1.0, 0.4)is a splash..seed()places the drops and.drop_size()scales them..input("scene", Input::frame())names what it reads;Input::backdrop()is the same outside a layer.Node::effectruns a shader on a node's own pixels, children included.shaders::ripple()sends rings out from the node's center:.strength()and.speed()are signals on the returnedEffect. The outline stays, so use it on text, a gradient, an image or water.Transition::shaderpaints a transition with a shader preset.shaders::water_wipe()raises a wavy waterline over the frame; below it is the next scene, rippled near the line..wave()sets the wave height.checkreportsinput_cycle(an error) when a shader would read pixels that are not finished:lens_dropsor an effect inside a group with opacity below 1, a blur, a clip, a blend, a shadow or another effect. The shader is skipped there. A misspelled input orlens_dropsparameter isparam_unknownwith the right name.inspect --graphshows effect passes with the shader that runs, andinspectnames the preset of each shader node.Svg::new().width()and.height()takepct(), read when the node is added. AShaderhandle converts toNode, soCamera::center_onworks on water.
Fixed
- Renderers kept every image they had drawn. A waves node paints a new image each frame, so a long render grew without bound. They now keep only the images of the current frame.
- A shader wider than the safe area (water to the edges) is no longer an
out_of_framewarning, andlens_dropsover text is notcovers_text.
Not in this version
Input::node, Input::image and .offscreen() from the roadmap are not built. A custom WGSL shade is still checked, not run. low_contrast does not read shader pixels yet.
Breaking
- A renderer of your own needs
render_list, which draws a part of the draw list. Before → after:
// before fn render(&mut self, graph: &SceneGraph, o: &RenderOptions) -> Image { draw(&graph.draw_list(), o) } // after fn render(&mut self, graph: &SceneGraph, o: &RenderOptions) -> Image { self.render_list(graph, &graph.draw_list(), o) } fn render_list(&mut self, _: &SceneGraph, items: &[DrawItem], o: &RenderOptions) -> Image { draw(items, o) }
- The draw list has three markers, ReadFrame, BeginEffect and EndEffect. A renderer skips them (Primitive::is_marker on docs.rs); the engine handles them.
- The pass and inspect node structs have a new
shaderfield.
0.11.1
Fixed
- The waves example stopped with "unreachable executed" in the browser playground. The timer that reports a slow shader read the clock, and the clock panics in WebAssembly. It is now skipped there, so the example plays. Native builds,
oxframe devand the CLI were not affected, so there is no new CLI release.
0.11.0
A scene can paint a wave as a node. The preset renders in a check with no GPU, and the same image is what the GPU draws.
Added
shaders::waves()is a box in the scene..amp(),.deep()and.foam()are signals, and a theme role such asAccentfollows the theme..uniform()sets a numeric field of your own shader.Shader::sourcekeeps WGSL in the scene.Shader::filereads it on every draw, so saving the file updates the picture without a rebuild. The text definesParamsandshade. oxframe addsShadeIn. A wrong name or type fails the check and lists the fields. WGSL that does not compile fails it too, with the line in your source.- A check paints shader layers at half resolution. An opaque shader over text is a warning. A pass over the draft budget is a warning.
0.10.0
A scene can wait for one word of a spoken line. Without a provider the length is a speech-rate estimate, so the playground and CI still run.
Added
Project::ttsspeaks through aTtsProvider. The key stays in the provider. A failed call falls back to the estimate and the scene still runs.voice("one two").start()starts the line..at_word()waits until that word. Awaiting the line waits until the end. A line is.priority(), so it ducks a soundtrack built with.duck_under_voice().- The recording is cached under
tts/next to the scene. Commit that folder to render in CI without a key. Without a provider, oxframe reads a committed cache and otherwise estimates the length. oxframe render --subtitles out.srtwrites the lines. A path ending in.vttwrites WebVTT.--no-audiostill writes them, and--rangeshifts the cues into that part of the video.
0.9.0
Sound effects mix with the soundtrack into one track. ffmpeg only encodes that mix, so the samples do not depend on the ffmpeg version.
Added
Audio::trackbuilds the soundtrack.Project::audio("music.mp3")still works..volume(),.fade_in(),.fade_out()and.master_fade()shape it..duck_under_voice()lowers it under a clip marked.priority().sound("click.wav")starts at the current frame..play()does not wait. Awaiting it waits until the clip ends..trim()plays a slice of the file, which is also how long the wait is.- The editor draws each effect on the timeline and plays the mix while you play or seek.
--no-audioleaves effects out.--rangekeeps only the effects inside that part of the video.
0.8.0
A frame's draw list is a pass graph, and a layer whose vectors did not change is reused (ROADMAP task 15, stage 2). No new scene API and no shaders.
Added
- Render passes in topological order: one vector pass when nothing opens a layer, then a composite pass for a group opacity, a blend, a blur, a shadow or a clip. Contents run before the composite that uses them.
- A static layer is cached as the pixmap from before
composite, so the next frame skips redrawing that layer. The root vector pass is not hashed, and a layer that moved or changed is drawn again. CPU bytes stay the reference. inspect --graphadds apassesarray (kind,node,name,items,depth,opacity,blend,filtered). Without the flag the JSON is unchanged.
0.7.0
A node can draw its own geometry, and a spray keeps a seeded simulation (ROADMAP task 15, stage 1). No shaders yet.
Added
Custom::new(|ctx, t| { ... }).ctx.line,ctx.circle,ctx.rectandctx.pathtake.fillor.stroke.ctx.rng(seed)does not advancerandom(), and the same seed draws the same shapes after a seek.Particles::new().seed(7).gravity(900.0).toward_camera()andburst(origin, count).await. Integration uses a fixed 1/120 s step and a checkpoint every 30 steps, so seeking back replays at most that many steps and lands on the same dots.toward_cameraonly scales the dots.oxframe checkreportsnondeterministicwhen painting the same frame twice does not match.- Example
spray: rain fromCustom, then a burst into the camera.
0.6.0
Scenes can blend into each other (ROADMAP task 5).
Added
transition(Transition::slide(Dir::Left)).awaitat the start of a scene. Alsofade,wipe,zoom_in,zoom_out,.duration,.ease,.overlap, andTransition::custom. The length is stored inmeta.jsonand can be dragged on the timeline.render --sceneleaves the incoming blend out.checksamples it andinspectreports kind and progress.- Example
transitions: a title card slides into an end card. - The browser presents a transition as one canvas frame. Both layers are drawn first, then blended.
0.5.0
Code edits itself (ROADMAP task 2). Txt still types its text out. SVG files become ordinary nodes (ROADMAP task 3).
Added
code.code().to(next, seconds)diffs the two sources. Tokens that stay slide to their new places, deleted tokens fade and collapse, new tokens fade in. Lines below the edit shift. Highlighting stays on the six languages and is taken from each side, not from a half-typed string.insert(line, col, text, seconds),remove(((line, col), (line, col)), seconds)andreplace(range, text, seconds). Lines are 1-based, columns are 0-based characters, and the range is half-open..flash()on that tween lights the inserted text for a moment.- A focus on lines that move rides with them and lands on their new numbers when the tween ends.
- Example
code_steps: a function written in a few edits. Svgloads a file (.src(include_bytes!(..))or.file(path)) and expands it into groups, paths and images..width/.heightkeep the aspect ratio.svg.find("id")is the element with that id,svg.paths()is every contour in paint order, and those paths morph throughdatalike a path written by hand. Filters, masks and clip paths are skipped with a warning.- Example
svg: a logo drawn on path by path, then one part recolored.
Breaking
code.code()is no longer aSignal<String>..get(),.set()and.bind()still work..to()is a diff, not a typewriter:
// before — `code()` was a Signal<String>, and `.to()` typed the text
let src: Signal<String> = code.code();
src.to(next, 1.0).await;
// after — `.to()` slides tokens; `.get()` / `.set()` / `.bind()` remain
code.code().to(next, 1.0).await;
code.insert(2, 0, " let y = x * 2;\n", 0.6).flash().await; 0.4.0
Added
Camera(ROADMAP task 1): a node that zooms, pans and turns the view of everything added to it. Signalsx/y(the point it looks at),zoom,rotation;center_on(node, s),fit(node, padding, s)(measures the node when called, so a growing layout fits at its current size) andreset(s). Follow a moving node withcam.x().bind(..). Nodes added to the view stay on screen (titles, captions, a HUD).- Zoomed content is drawn at the final scale on both renderers: text and outlines stay sharp at ×10, blurs and shadows scale with the zoom (golden frames
camera_zoom,camera_rotate_blur; GPU within ΔE 0.04). oxframe checkdoes not reportout_of_framefor nodes seen through a camera (framing a detail is the point);inspectmarks themin_camera.- Example
camera: a tour of a system diagram.
Changed
Signal::totakes anything that converts into the value: theme tokens (rect.fill().to(Accent, 0.3)), integers forf64,&strfor text.
Breaking
- An explicit
.into()in.to(..)is now ambiguous; drop it:
// before
title.text().to("Hello".into(), 1.0).await;
// after
title.text().to("Hello", 1.0).await; 0.3.0
Scene code without Rust plumbing (ROADMAP task 4): no Box::pin, no BoxFuture in a typical scene.
Added
stagger(delay, items, |x| animation): the same animation on many items, each startingdelayseconds after the previous one.all(..)takes aVec, an array or an iterator of animations of one kind as well as a tuple:all(cards.iter().map(|c| c.fade_in())).any(..): completes when the first animation does (returns its index); the others stop where they are.repeat(n, || animation)andloop_while(|| condition, || animation).- Node helpers, timed by the theme's motion tokens:
pop_in()(scale from zero with overshoot),slide_in(Side::Left)(slides in and fades in),draw_on()(outline drawn from start to end), next to the existingfade_in()/fade_out(). Tween::duration(s)changes a helper's length:title.fade_in().duration(0.3).- Clearer compile errors:
all(5),all(a, b)and astaggerclosure that ends in;explain how to fix them.
Breaking
sequence(delay, futures)takes anyVec, array or iterator of animations instead ofVec<BoxFuture>. Code that built the list with.collect()no longer infers its type; usestaggerinstead:
// before
sequence(0.15, cards.iter()
.map(|c| -> oxframe::BoxFuture { Box::pin(c.opacity().to(1.0, 0.5)) })
.collect()).await;
// after
stagger(0.15, &cards, |c| c.opacity().to(1.0, 0.5)).await;
Several steps per item: return an async move { … } block from the closure, no boxing.
all_vec(v)is deprecated:all(v)takes theVecdirectly.CodeExplainer::next()returnsimpl Futureinstead ofBoxFuture(awaiting it is unchanged).
0.2.6
The first published release after 0.2.1: it also contains everything listed under 0.2.2–0.2.5 below, which were not published separately.
No crate changes. Released together with oxframe-cli 0.2.6 (oxframe mcp, --json, and hints on the compile errors agents hit most often).
The scene API and the editor protocol are unchanged.
0.2.5
Added
- Crate
oxframe-components, re-exported from the prelude.TitleCard,ChapterMarker,BulletList,CodeExplainer,FlowDiagram,Callout,LowerThirdandEndCardare added like any other node. Each hasenter,nextandexit. They take color and type from the theme.LowerThirdandChapterMarkerpin to the safe area.FlowDiagramlays its own nodes out in layers (.down()stacks them). Node::from_id, so another crate can implementNodeBuilder.
The scene API and the editor protocol are unchanged.
0.2.4
Added
- Relative placement.
.next_to(other, Side::Bottom),.align_to(other, Edge::Top),.to_edge(Edge::Top)and.center_in(other)are recomputed every frame, so a node follows a neighbor that moves or resizes. The gap innext_tois the theme'sMdstep.to_edgepins to the safe area; a portrait frame leaves room for a vertical video's status bar and buttons. .width(pct(80))and.height(pct(50)): a fraction of the frame, or of the parent when that parent has a fixed size.view.safe_area()andview.is_portrait().Project::aspects([Landscape, Portrait, Square]).checklints every declared format.render --aspect 9:16exports one;--aspect allexports each, with the shape in the file name.
The scene API and the editor protocol are unchanged. A project that does not declare aspects renders and checks at its own size, as before.
0.2.3
Added
- Themes and design tokens.
Project::theme(Theme::dark().accent(...))(alsolight,midnight,paper,vivid) supplies colors, type roles (.display(),.h1(),.body(),.caption(),.code()), spacing (.padding(Md)), radii, shadows (.elevation(Md)) and motion (Fast/Normal/Slow,fade_in). A token is accepted wherever the concrete value is. Text with no fill uses the theme's text color..accent(color)keeps a 4.5:1 contrast.[theme]inoxframe.tomloverrides the compiled theme. RELEASE.md: before a release tag, publish the crates, then move oxframe-web's playground pin to that version. The docs version list already follows crates.io; the compile image cannot.
The scene API and the editor protocol are unchanged. Existing explicit colors are unchanged. The default theme is the previous dark background and white text.
0.2.2
Added
checkandinspectsubcommands on the project binary (whatoxframe checkandoxframe inspectrun).checksamples the scene on the CPU, writes a storyboard and a JSON report, and fails when a node stays outside the frame, text overlaps or overflows, or text contrast is below WCAG 4.5:1. Text that flashes too briefly and a long still with no soundtrack are reported too..name("title")on every node builder.oxframe checkandoxframe inspectuse it; without a name the node is labeled by theaddcall site.
0.2.1
Fixed
- The browser engine (
oxframewith thewasmfeature, used by the playground) did not compile in 0.2.0. CI now builds it.
0.2.0
Changed
- Crate layering.
oxframe-corenow ownsImage(RGBA pixels) and the newFrameSourcetrait — "give me frame *n*" — through which the editor (oxframe-preview),render/snapshotand the exporter get frames without knowing what draws them.oxframe-previewandoxframe-exportno longer depend on the 2D crate or the renderers; CI checks this (scripts/check-layering.sh). Scene code is unchanged and renders byte-identically. - The crate docs and README carry a development-status notice.
Breaking (only for code using the internal crates directly)
oxframe_preview::PreviewEnginenow extendsoxframe_core::FrameSource:fps,size,total_framesandrendermoved to that trait.
// before
impl PreviewEngine for MyEngine { fn fps(&self) -> u32 { 60 } /* … */ }
// after
impl FrameSource for MyEngine { fn fps(&self) -> u32 { 60 } /* … */ }
impl PreviewEngine for MyEngine { /* the editor-specific methods */ }
oxframe_render::Imageis a re-export ofoxframe_core::Image;Image::encode_pngstill exists.
0.1.0
First release: scenes as coroutines, signals and tweens, 2D nodes with flexbox layout, text and code, effects, CPU (tiny-skia) and GPU (vello) renderers, the editor protocol, video export through ffmpeg and the in-browser (wasm) engine.