Matrices, vectors and tables
Matrices with cells you can point at, vectors on axes, linear maps that bend the grid, and tables whose rows slide into place.
Loading live preview…
Source of matrix_multiply — edit in the playground
//! Matrix multiplication step by step: a row of A meets a column of B and
//! their dot product fills one cell of the result.
use oxframe::prelude::*;
#[oxframe::scene]
pub async fn multiply(view: View) {
view.add(
Txt::new()
.text("Multiplying matrices")
.h1()
.to_edge(Edge::Top),
);
let a_vals = [[1, 2], [3, 4]];
let b_vals = [[5, 6], [7, 8]];
let a = view.add(Matrix::new(a_vals).font_size(64.0).x(-560.0));
view.add(Txt::new().text("×").h1().x(-310.0));
let b = view.add(Matrix::new(b_vals).font_size(64.0).x(-60.0));
view.add(Txt::new().text("=").h1().x(190.0));
let c = view.add(
Matrix::new([["?", "?"], ["?", "?"]])
.font_size(64.0)
.cell_width(80.0)
.x(470.0),
);
for cell in (0..2).flat_map(|i| c.row(i)) {
cell.fill().set(Muted.into());
}
let sum = view.add(Txt::new().text("").h2().y(280.0));
let rows = [a.row_frame(0), a.row_frame(1)];
let cols = [b.column_frame(0), b.column_frame(1)];
wait_for(0.4).await;
for i in 0..2 {
for j in 0..2 {
all((
rows[i].opacity().to(1.0, 0.3),
cols[j].opacity().to(1.0, 0.3),
))
.await;
let terms: Vec<String> = (0..2)
.map(|k| format!("{}·{}", a_vals[i][k], b_vals[k][j]))
.collect();
let value: i32 = (0..2).map(|k| a_vals[i][k] * b_vals[k][j]).sum();
sum.text().set(format!("{} = {value}", terms.join(" + ")));
sum.opacity().set(0.0);
sum.fade_in().await;
let cell = c.cell(i, j);
cell.text().set(value.to_string());
cell.fill().set(Accent.into());
wait_for(0.4).await;
all((
rows[i].opacity().to(0.0, 0.3),
cols[j].opacity().to(0.0, 0.3),
cell.fill().to(Text, 0.3),
))
.await;
}
}
sum.fade_out().await;
wait_for(0.6).await;
}
pub fn project() -> Project {
Project::new("matrix_multiply").scene(multiply())
}Matrix
let a = view.add(Matrix::new([[1, 2], [3, 4]]).font_size(64.0));
let row = a.row_frame(0);
row.fade_in().await;
a.cell(0, 1).fill().to(Accent, 0.3).await; Matrix::new takes rows of numbers, &str or anything that prints, and
draws them in square brackets. Every cell is a Txt: a.cell(r, c), a.row(r) and a.column(c) give them, so .text().set(..) changes a value and .fill() colors it. row_frame, column_frame and cell_frame add a rounded outline, hidden until you fade
it in. .cell_width(px) keeps room for values that will be longer than the first ones,
such as a result filled in step by step; .font_size(px), .column_gap(px) and .row_gap(px) set the rest.
Vectors and linear maps
Loading live preview…
Source of linear_map — edit in the playground
//! A linear map on the plane: the basis vectors and the grid follow a
//! shear, then a rotation, while the matrix of the map is shown beside it.
use oxframe::prelude::*;
#[oxframe::scene]
pub async fn shear(view: View) {
let axes = view.add(
Axes::new()
.x_range(-4.0..4.0)
.y_range(-3.0..3.0)
.width(880.0)
.height(660.0)
.x(-180.0)
.y(30.0),
);
let i = axes.vector(1.0, 0.0);
i.label("i");
let j = axes.vector(0.0, 1.0).stroke(Success);
j.label("j");
let v = axes.vector(2.0, 1.0).stroke(Warning);
v.label("v");
let m = view.add(
Matrix::new([[1, 0], [0, 1]])
.font_size(56.0)
.cell_width(56.0)
.x(640.0)
.y(30.0),
);
wait_for(0.5).await;
// Shear: x picks up y.
m.cell(0, 1).text().set("1".into());
m.cell(0, 1).fill().set(Accent.into());
axes.apply_matrix([[1.0, 1.0], [0.0, 1.0]], 1.5).await;
wait_for(0.5).await;
// Then a quarter turn, after the shear: the product shows in the matrix.
let turn = Mat2([[0.0, -1.0], [1.0, 0.0]]);
let total = turn.after(&axes.matrix().get());
axes.apply_matrix(turn, 1.5).await;
for (r, row) in total.0.iter().enumerate() {
for (c, value) in row.iter().enumerate() {
m.cell(r, c).text().set(format!("{value}"));
}
}
wait_for(0.5).await;
// Back to the identity; the vector's tip is a signal of its own.
all((axes.matrix().to(Mat2::IDENTITY, 1.2), v.to(-1.0, 2.0, 1.2))).await;
for (r, row) in [[1, 0], [0, 1]].iter().enumerate() {
for (c, value) in row.iter().enumerate() {
m.cell(r, c).text().set(value.to_string());
m.cell(r, c).fill().set(Text.into());
}
}
wait_for(0.6).await;
}
pub fn project() -> Project {
Project::new("linear_map").scene(shear())
}let axes = view.add(Axes::new().x_range(-4.0..4.0).y_range(-3.0..3.0));
let v = axes.vector(2.0, 1.0);
v.label("v");
axes.apply_matrix([[1.0, 1.0], [0.0, 1.0]], 1.5).await; // a shear
v.to(-1.0, 2.0, 1.0).await;
axes.matrix().to(Mat2::IDENTITY, 1.0).await; axes.vector(x, y) draws an arrow from the origin. Its tip is two signals, v.x() and v.y(), and v.to(x, y, seconds) moves both; .label("v") keeps a label past the tip.
axes.apply_matrix(m, seconds) maps the plane through a 2 × 2 matrix, written row by
row, on top of what was applied before. A transformed grid is drawn over the original one, and
plots, series, vectors and nodes placed with axes.place move with it. The matrix
itself is axes.matrix(), a Mat2 signal that animates entry by entry; set it back
to Mat2::IDENTITY to undo. Mat2::after multiplies two matrices and Mat2::det gives the determinant. Ticks and labels stay where they were, and placed nodes
keep their own shape.
Table
Loading live preview…
Source of table_sort — edit in the playground
//! A table that changes on screen: a row slides in, the rows sort by a
//! column, and the slowest one leaves.
use oxframe::prelude::*;
#[oxframe::scene]
pub async fn ranking(view: View) {
view.add(
Txt::new()
.text("Frame time at 1080p")
.h1()
.to_edge(Edge::Top),
);
let table = view.add(
Table::new(["Renderer", "Backend", "Frame"])
.row(["skia", "CPU", "12 ms"])
.row(["vello", "GPU", "4 ms"])
.row(["cairo", "CPU", "18 ms"])
.row(["tiny-skia", "CPU", "9 ms"])
.font_size(44.0)
.row_height(92.0)
.widths([300.0, 220.0, 200.0])
.y(40.0),
);
wait_for(0.6).await;
table.insert_row(2, ["wgpu", "GPU", "3 ms"]).await;
wait_for(0.4).await;
table.sort_by_column(2, 0.8).await;
let fastest = table.row(0);
fastest.background().fade_in().await;
wait_for(0.4).await;
let last = table.len() - 1;
table.remove_row(last).await;
wait_for(0.8).await;
}
pub fn project() -> Project {
Project::new("table_sort").scene(ranking())
}let table = view.add(
Table::new(["Renderer", "Frame"])
.row(["skia", "12 ms"])
.row(["vello", "4 ms"]),
);
table.insert_row(1, ["wgpu", "3 ms"]).await;
table.sort_by_column(1, 0.8).await;
table.row(0).background().fade_in().await;
table.remove_row(2).await; Table::new takes the column titles and .row(..) adds rows. Columns fit
their widest value; .widths([..]) sets them when rows added later are wider, and .font_size(px) with .row_height(px) sizes the text.
The rows are a list you change while the scene runs. insert_row and push_row slide the rows below down and fade the new one in; remove_row fades one out and closes the gap. sort_by_column moves every
row to its sorted place, comparing cells that start with a number ("12 ms") as
numbers; sort_by takes your own comparison. table.row(i) is a TableRow in the current order, with its cells and a hidden background for highlights.