# kinemo > Python library for explanatory animations (math, algorithms, engineering, data), with a native core in Rust. One form per concept, errors that come with fixes, and machine-readable tools (`check`, `inspect`, `snap`). Version 0.9.0 · IR 1.0.0. Generated by `python -m kinemo.docs.llms`; do not edit by hand. ## Rules 1. Scene code runs once and produces a timeline. The frame at instant t is a pure function of t. 2. Objects are values. Creating an object does not put it in the scene; it enters with `s.add()` (instant) or with a verb (`k.draw`, `k.write`, …). 3. What takes time goes through `s.play` (blocks the cursor) or `s.start` (does not block). What is instant is a direct call: `s.add`, `s.remove`, `obj.set`, `x.set`. 4. A state change is `obj.to(...)`. Components may expose named transitions (`row.swap(i, j)`), always sugar for a `.to()`. 5. Position comes from constraints (`.place`, `k.Row`), not from coordinates. Coordinates exist, but they are the rare case. 6. Passing a signal or a lambda creates a reactive binding. There are no updaters. 7. Two reads, two names: `x.now` reads the value at the cursor during construction; `x()` is a tracked read inside lambdas, `k.computed` and `.map`. ## Recommended loop for agents 1. Read this `llms.txt` (or `kinemo docs ` for the symbols you will use). 2. Write the scene. 3. `kinemo check scene.py --json --strict`. If there are errors, apply the fixes (or `--fix`) and repeat. 4. `kinemo inspect scene.py --at --json` at the key instants (end of each `play`) to confirm positions. 5. Optional: `kinemo snap scene.py --at 0,2.5,end` and a visual review. 6. `kinemo render scene.py`. ## Conventions - `import kinemo as k`; a scene is `@k.scene def name(s: k.Scene): ...`. - Frame of 16 × 9 units, origin at the center, y up; `s.frame.safe` is the safe area. - Every verb and every `.to()` lasts 1 s with `k.ease.smooth`; they accept `duration=`, `ease=` and `delay=`. - One form per concept: this reference shows only the canonical form of each thing. - Functions used in `.map`, lambdas and `ax.plot` use `k` blocks (`k.sin`, `k.where`, `k.min`), never `math.*`, `if` on signals or the built-in `min()`/`max()`. - The `k.simulate` step and `.on` handlers are plain Python: `if`, `min()`, `math.*` are fine there (only lambdas, `.map` and `k.computed` are traced). - Layout-derived props, read-only and reactive: `obj.left`, `obj.right`, `obj.top`, `obj.bottom`, `obj.width`, `obj.height`, `obj.center` (in the parent) and `obj.world.position`, `obj.world.center` (global). Edge points: `obj.edge("right")`, for example `k.Arrow(start=a.edge("right"), end=b.edge("left"))` follows both objects. - Text parts: `txt["world"]` (first occurrence), `txt.find_all("a")`, `txt.chars[3:7]`, `txt.words[1]`, `txt.lines[0]`; in formulas, `eq["name"]` (from `\id{name}{...}`) or `eq["c^2"]`. Each part is an object: `s.play(txt["world"].to(color=k.YELLOW))`. - Containers: `row[i]` is the order at the cursor (already accounting for swaps scheduled earlier); `row.to(children=[...])` reorders freely; `swap`, `insert` and `pop` are shortcuts for it. - `ax.plot` curves are clipped to the visible range of the axes (x and y); choose `y=` to cover the values that matter. - For bars that grow from one side, position by constraint: `k.Rect(w=p * 6, h=0.4).place(inside=track, align="left")`. - Every prop accepts a value, a signal or a lambda (`k.Val[T]`), including in constructors: `k.Polygon.regular(n)` with a signal `n` changes the number of sides. - Anchors for `at=`, `align=`, `edge()` and `k.grow(from_=)`: `center`, `top`, `bottom`, `left`, `right`, `top-left`, `top-right`, `bottom-left`, `bottom-right`. `place(inside=box, align="bottom", pad=0.1)`. - `dash=(12, 10)`: dash and gap in pixels at 1080p resolution (like `stroke_width`). - Geometry on edges: `tri.sides` (at the cursor) and `k.Square.on(side, outward=True)`. - Lint fixes point to objects by variable name (`curve.label`); use `name=` to name objects created without a direct assignment. - `k.Math` accepts mathematical LaTeX (fractions, roots, sums, integrals, matrices, `\left…\right`, accents, Greek letters, `\text{}`); document-level commands (`\section`, `tabular`, TikZ) give K0801. ## API Each symbol: signature, summary and one canonical example (all start with `import kinemo as k` and pass `kinemo check --strict`). Details: `kinemo docs `. ### Scene #### `@k.scene(size="1080p", fps=60, background=None, seed=0, tail=0.5, theme=None, camera="2d", params=None, name=None)` Turns a function `def name(s: k.Scene)` into a scene. The body runs exactly once, in the build phase, and produces a timeline; the frame at time t is a pure function of t. The decorator arguments (size, fps, background, seed, tail, theme, parameters) take precedence over `kinemo.toml`. See also: `s.play`, `k.Text`, `k.Int`. ```python @k.scene(size="1080p", fps=60, tail=0.5) def hello(s: k.Scene): title = k.Text("Hello, kinemo").place(at="center") s.play(k.write(title)) s.play(title.to(color=k.BLUE, scale=1.5)) s.wait(1) ``` #### `s.play(*anims, duration=None, ease=None, at=None) -> TimeSpan` Schedules animations at the cursor and advances the cursor to their end. Multiple arguments run in parallel (`s.play(a, b)` is by definition `s.play(k.par(a, b))`). `duration=` sets the total duration of the group, rescaling its contents. See also: `s.start`, `k.par`, `s.wait`. ```python @k.scene def steps(s: k.Scene): a = k.Circle(r=0.8).place(at="center") b = k.Square(1.4).place(right_of=a, gap=0.6) s.play(k.draw(a), k.draw(b), duration=2) s.play(a.to(color=k.RED)) ``` #### `s.start(*anims, duration=None, ease=None, at=None) -> TimeSpan` Schedules animations at the cursor without moving it: this is how to run something in the background (clocks, simulations, continuous motion) while the script continues. Returns a `TimeSpan`; `h.done` is an event at the end of the animation. See also: `s.play`, `s.wait_for`, `k.simulate`. ```python @k.scene def background(s: k.Scene): dot = k.Dot(r=0.2, x=-4) title = k.Text("In parallel").place(at="top", margin=0.8) s.add(dot) s.start(dot.to(x=4), duration=3) s.play(k.write(title)) s.wait(2) ``` #### `s.wait(d=1.0) -> TimeSpan` Advances the cursor by `d` seconds (default 1). It is the script's pause: nothing is scheduled, but whatever was started with `s.start` keeps running. See also: `s.play`, `s.mark`. ```python @k.scene def pause(s: k.Scene): title = k.Text("Think for a moment").place(at="center") s.play(k.write(title)) s.wait(1.5) s.play(k.fade_out(title)) ``` #### `s.wait_for(event, count=1, timeout=None) -> EventInfo` Moves the main cursor to the time of an event (the `count`-th firing after the cursor) and returns the `EventInfo`. It only sees what has already been scheduled, so the usual pattern is `s.start(...)` followed by `s.wait_for(...)`. `timeout=` is required when the source has no guaranteed end. See also: `k.when`, `event.on`, `s.start`. ```python @k.scene def wait_for_it(s: k.Scene): dot = k.Dot(r=0.2, x=-5) title = k.Text("Moving...").place(at="top", margin=0.8) s.add(dot) h = s.start(dot.to(x=5), duration=3) s.play(k.write(title)) s.wait_for(h.done) s.play(k.fade_out(dot, title)) ``` #### `s.add(*objs)` Puts objects in the scene instantly, at the cursor. Creating an object does not put it in the scene: it enters with `s.add` or with an entrance verb (`k.draw`, `k.write`, `k.fade_in`, `k.grow`). See also: `s.remove`, `k.draw`, `k.fade_in`. ```python @k.scene def instant(s: k.Scene): box = k.Rect(w=3, h=1.5).place(at="center") label = k.Text("Ready").place(inside=box) s.add(box, label) s.wait(1) s.play(box.to(color=k.GREEN)) ``` #### `s.remove(*objs)` Takes objects out of the scene instantly, at the cursor. After that the object does not accept `.to()` (error K0102) until it enters again with an entrance verb. See also: `s.add`, `k.fade_out`, `k.shrink`. ```python @k.scene def cut(s: k.Scene): a = k.Text("Before").place(at="center") b = k.Text("After").place(at="center") s.add(a) s.wait(1) s.remove(a) s.add(b) s.wait(1) ``` #### `s.mark(name=None, slide=False) -> float` Creates a time anchor at the cursor without moving it and returns the time. Named marks are stored in `s.marks[name]` (useful with `at=`); `slide=True` defines a slide break for `kinemo render --format slides`. See also: `s.start`, `s.voice`. ```python @k.scene def presentation(s: k.Scene): title = k.Text("Part 1").place(at="center") s.play(k.write(title)) s.mark("part2", slide=True) s.play(title.to(text="Part 2")) s.wait(1) ``` #### `with s.during(*anims, duration=None, ease=None, revert=None):` `with` block that applies state changes on entry and reverts them, animated, on exit. The revert uses the same duration and easing; `revert="instant"`, `revert=0.3` or `revert=k.ease.out` change that. Only accepts reversible animations (`.to`, `k.indicate`). See also: `obj.to`, `k.indicate`, `group.swap`. ```python @k.scene def highlight(s: k.Scene): a = k.Circle(r=0.6) b = k.Circle(r=0.6) row = k.Row(a, b, gap=1).place(at="center") s.play(k.draw(row)) with s.during(a.to(color=k.YELLOW), b.to(color=k.YELLOW), duration=0.3): s.play(row.swap(0, 1)) s.wait(0.5) ``` #### `with s.tempo(factor, to=None):` `with` block that multiplies the speed of everything inside: `s.tempo(4)` divides durations and waits by 4. `s.tempo(1, to=8)` speeds up progressively over the block. Nested tempos multiply; `k.time` is not affected. See also: `s.during`, `k.stagger`. ```python @k.scene def speed_up(s: k.Scene): dots = [k.Dot(r=0.2) for _ in range(6)] row = k.Row(*dots, gap=0.6).place(at="center") s.add(row) with s.tempo(1, to=4): for d in dots: s.play(k.indicate(d), duration=0.5) ``` #### `with s.voice(narration, *, voice=None, gain=1.0):` Narrated `with` block: takes text (TTS from the provider in `kinemo.toml`) or an audio file, and lasts at least as long as the audio. Words marked `[word]{name}` become `s.marks[name]` at the moment they are spoken. Without a TTS provider, the voice becomes silence with an estimated duration and lint W1401 warns. See also: `s.mark`, `k.sound`. ```python @k.scene def narrated(s: k.Scene): tri = k.Triangle.right(3, 4, scale=0.6).place(at="center") with s.voice("Every right [triangle]{tri} hides a relation."): # kinemo: allow W1401 s.play(k.draw(tri)) s.start(k.indicate(tri), at=s.marks["tri"]) ``` #### `k.time # read-only signal` Global scene time in seconds, as a read-only signal. It is the right way to express clocks and continuous motion, because it advances linearly and is never eased. Works at module level, in contexts and in components. See also: `k.signal`, `x.map`, `k.integrate`. ```python @k.scene def clock(s: k.Scene): hand = k.Line(start=(0, 0), end=(0, 2), rotate=-k.time * 90) label = k.Text(lambda: f"{k.time():.1f} s").place(at="top", margin=0.8) s.add(hand, label) s.wait(4) ``` ### Object state #### `obj.to(*, duration=None, ease=None, delay=0.0, blend="replace", place=None, unpin=False, **props) -> Animation` Animated state change: interpolates each prop from its value at the cursor to the target. Accepts any public prop (`x`, `y`, `scale`, `rotate`, `color`, `opacity`...) plus `duration=`, `ease=`, `delay=`, `blend="add"`, and `unpin=True` to release a `.place` constraint where the animation starts (to switch constraints instead, use `obj.to_place(...)`). Colors interpolate in OKLab. See also: `obj.set`, `anim.with_`, `s.during`. ```python @k.scene def state(s: k.Scene): square = k.Square(1.5).place(at="center") s.play(k.draw(square)) s.play(square.to(color=k.RED, rotate=45, scale=1.5), duration=1.5) s.wait(0.5) ``` #### `obj.set(**props) -> Node` Instant change at the cursor. Passing a signal or a lambda creates a reactive binding from the cursor on (there are no updaters); `obj.unbind("x")` removes the binding. See also: `obj.to`, `obj.unbind`, `k.signal`. ```python @k.scene def binding(s: k.Scene): leader = k.Dot(r=0.25, x=-4, y=1) follower = k.Dot(r=0.15, fill=k.RED) s.add(leader, follower) follower.set(x=leader.x, y=leader.y - 2) s.play(leader.to(x=4), duration=2) ``` #### `obj.unbind(*names)` Removes reactive bindings (all props when no name is given), keeping the current value. Required before animating a bound prop (error K0401). See also: `obj.set`, `obj.unpin`. ```python @k.scene def unbind(s: k.Scene): a = k.Dot(r=0.25, x=-3) b = k.Dot(r=0.25, x=-3, y=-1, fill=k.RED) s.add(a, b) b.set(x=a.x) s.play(a.to(x=3)) b.unbind("x") s.play(b.to(x=-3)) ``` #### `obj.copy(frozen=False) -> Node` Creates a new identity with the same props. By default the copy follows the original's reactive bindings; `frozen=True` copies only the values at the cursor. Use it to show the same visual in two places (an object has a single parent). See also: `k.Group`, `k.reparent`. ```python @k.scene def copy(s: k.Scene): original = k.Circle(r=0.8, fill=k.TEAL, fill_opacity=0.5).place(at="left", margin=3) s.play(k.draw(original)) twin = original.copy(frozen=True).place(at="right", margin=3) s.play(k.fade_in(twin)) ``` #### `k.reparent(obj, new_parent) -> Animation` Moves an object to another group at the scheduled time, keeping its world position (inside a container, it takes its place in the flow). This is how you change parents: an object has exactly one (error K0103). See also: `obj.copy`, `k.Group`, `k.Row`. ```python @k.scene def change_group(s: k.Scene): dot = k.Dot(r=0.25) left = k.Row(dot, k.Square(0.8), gap=0.3).place(at="left", margin=2) right = k.Row(k.Square(0.8), gap=0.3).place(at="right", margin=2) s.add(left, right) s.play(k.reparent(dot, right)) s.wait(0.5) ``` ### Verbs #### `k.draw(*objs, duration=None, ease=None, delay=0.0) -> Animation` Entrance verb: traces the outline and then fills it. The object enters the scene at the start of the animation. If the object is a component with `enter()`, the verb uses that implementation. See also: `k.write`, `k.fade_in`, `k.grow`. ```python @k.scene def drawing(s: k.Scene): circle = k.Circle(r=1.5, fill=k.BLUE, fill_opacity=0.4).place(at="center") s.play(k.draw(circle)) s.wait(0.5) ``` #### `k.write(*objs, duration=None, ease=None, delay=0.0) -> Animation` Entrance verb: writes the text character by character. Other objects passed to `k.write` are drawn as with `k.draw`. See also: `k.Text`, `k.draw`, `k.fade_in`. ```python @k.scene def writing(s: k.Scene): title = k.Text("Hello, kinemo", size=0.8).place(at="center") s.play(k.write(title), duration=1.5) s.wait(0.5) ``` #### `k.fade_in(*objs, shift=None, duration=None, ease=None, delay=0.0) -> Animation` Entrance verb: opacity from 0 to 1. Accepts multiple objects; `shift=(dx, dy)` makes each object arrive from an offset of `-shift` to its final position. See also: `k.fade_out`, `k.draw`. ```python @k.scene def appear(s: k.Scene): a = k.Square(1.2).place(at="center") label = k.Text("square").place(below=a, gap=0.3) s.play(k.fade_in(a, label, shift=(0, 0.5))) s.wait(0.5) ``` #### `k.fade_out(*objs, shift=None, duration=None, ease=None, delay=0.0) -> Animation` Exit verb: opacity from 1 to 0, and then the objects leave the scene. Accepts multiple objects and `shift=` to exit with an offset. See also: `k.fade_in`, `k.shrink`, `s.remove`. ```python @k.scene def vanish(s: k.Scene): title = k.Text("See you soon").place(at="center") s.play(k.write(title)) s.play(k.fade_out(title, shift=(0, 0.5))) ``` #### `k.grow(obj, from_="center", *, duration=None, ease=None, delay=0.0) -> Animation` Entrance verb: scales up from the center (default) or from a side (`from_="bottom"`, `"left"`, `"top-left"`...). It is the natural entrance for bars and boxes. See also: `k.shrink`, `k.Bar`, `k.stagger`. ```python @k.scene def grow(s: k.Scene): bar = k.Bar(6, label=True).place(at="center") s.play(k.grow(bar, from_="bottom")) s.wait(0.5) ``` #### `k.shrink(obj, to="center", *, duration=None, ease=None, delay=0.0) -> Animation` Exit verb: the inverse of `k.grow`. Shrinks toward the center (default) or toward a side (`to="bottom"`) and takes the object out of the scene. See also: `k.grow`, `k.fade_out`. ```python @k.scene def shrink(s: k.Scene): box = k.RoundedRect(w=3, h=2).place(at="center") s.play(k.draw(box)) s.play(k.shrink(box, to="bottom")) ``` #### `k.indicate(obj, color=k.YELLOW, scale=1.2, *, duration=None, ease=None, delay=0.0) -> Animation` Emphasis: temporarily tints and pulses the object; the final state equals the initial one. It is reversible, so it also works inside `s.during`. See also: `k.flash`, `k.squash`, `s.during`. ```python @k.scene def emphasis(s: k.Scene): word = k.Text("important", size=0.8).place(at="center") s.play(k.write(word)) s.play(k.indicate(word, color=k.YELLOW, scale=1.3)) s.wait(0.5) ``` #### `k.flash(obj, color=k.YELLOW, *, duration=None, ease=None, delay=0.0) -> Animation` Emphasis: a ring of light that expands from the object's edge and fades away. It does not change the object. See also: `k.indicate`, `k.when`. ```python @k.scene def pulse(s: k.Scene): dot = k.Dot(r=0.3).place(at="center") s.add(dot) s.play(k.flash(dot, color=k.YELLOW)) s.wait(0.5) ``` #### `k.squash(obj, amount=0.3, *, duration=None, ease=None, delay=0.0) -> Animation` Emphasis: an elastic squash against the object's base; `amount=` controls the intensity. The state returns to the initial one. Good for impacts. See also: `k.indicate`, `k.simulate`. ```python @k.scene def impact(s: k.Scene): ball = k.Circle(r=0.6, fill=k.ORANGE, fill_opacity=1).place(at="center") s.add(ball) s.play(k.squash(ball, amount=0.4)) s.wait(0.5) ``` #### `k.follow(obj, path, *, rotate=False, duration=None, ease=None, delay=0.0) -> Animation` Motion: the object travels along a path (the outline of another object or a list of points, in world coordinates). `rotate=True` aligns the object with the tangent. See also: `k.trace`, `k.Path`. ```python @k.scene def orbit(s: k.Scene): track = k.Circle(r=2.5, stroke=k.GRAY).place(at="center") planet = k.Dot(r=0.2, fill=k.BLUE) s.add(track, planet) s.play(k.follow(planet, track), duration=3, ease=k.ease.linear) ``` #### `k.sound(path, gain=1.0) -> Animation` Audio: plays a sound file at the moment it is scheduled (zero duration in the script). `gain=` adjusts the volume. For narration, use `s.voice`. See also: `s.voice`. ```python @k.scene def click(s: k.Scene): button = k.RoundedRect(w=2, h=0.8).place(at="center") s.play(k.draw(button)) s.play(k.sound("click.wav", gain=0.8), k.indicate(button)) ``` #### `k.morph(a, b, *, match=None, duration=None, ease=None, delay=0.0) -> Animation` Swap: `a` leaves, `b` enters and the matching parts travel between them (identical characters and tokens slide; the rest fades out and in). `match={part_a: part_b}` forces matches. With nothing in common, it does a warp + crossfade and emits W0801. See also: `k.Code`, `k.write`, `k.fade_out`. ```python @k.scene def swap(s: k.Scene): a = k.Text("a + b = c", size=0.8).place(at="center") b = k.Text("c = a + b", size=0.8).place(at="center") s.play(k.write(a)) s.play(k.morph(a, b)) s.wait(0.5) ``` ### Composition #### `k.seq(*anims) -> Animation` Composes animations in sequence: each one starts when the previous one ends. The result is an `Animation` like any other and can be rescaled with `duration=`. See also: `k.par`, `k.stagger`, `s.play`. ```python @k.scene def sequence(s: k.Scene): box = k.Square(2).place(at="center") label = k.Text("box").place(inside=box) s.play(k.seq(k.draw(box), k.write(label)), duration=2) s.wait(0.5) ``` #### `k.par(*anims) -> Animation` Composes animations in parallel: they all start together and the group lasts as long as the longest one. `s.play(a, b)` is defined as `s.play(k.par(a, b))`; use `k.par` when the parallel group is part of another composition. See also: `k.seq`, `k.stagger`, `s.play`. ```python @k.scene def parallel(s: k.Scene): a = k.Circle(r=0.7).place(at="left", margin=3) b = k.Circle(r=0.7).place(at="right", margin=3) title = k.Text("Together").place(at="top", margin=0.8) s.play(k.seq(k.par(k.draw(a), k.draw(b)), k.write(title))) s.wait(0.5) ``` #### `k.stagger(anims, lag=0.1, order="sequence") -> Animation` Staggers a list of animations: each one starts `lag` seconds after the previous one. `order=` changes the order: `"sequence"` (default), `"reverse"`, `"center"` or `"random(seed)"`. See also: `k.seq`, `k.par`, `k.grow`. ```python @k.scene def cascade(s: k.Scene): bars = [k.Bar(v) for v in [3, 5, 2, 6, 4]] row = k.Row(*bars, gap=0.3, align="bottom").place(at="center") s.play(k.stagger([k.grow(b, from_="bottom") for b in row], lag=0.1)) s.wait(0.5) ``` #### `@k.clip` Decorator that turns a function `def name(s: k.Scene, ...)` into a reusable sequence with its own cursor. Calling the clip returns an `Animation`, which composes with `s.play`, `k.seq` and `k.par` and can be rescaled with `duration=`. See also: `k.seq`, `s.play`, `k.Component`. ```python @k.clip def present(s: k.Scene, obj: k.Node) -> None: s.play(k.draw(obj)) s.play(k.indicate(obj)) @k.scene def with_clip(s: k.Scene): a = k.Circle(r=0.8).place(at="left", margin=3) b = k.Square(1.6).place(at="right", margin=3) s.play(present(a), present(b)) ``` #### `anim.with_(*, duration=None, ease=None, delay=None) -> Animation` Returns a copy of the animation with a different duration, easing or delay. Animations are immutable values: the original does not change. See also: `k.ease`, `obj.to`. ```python @k.scene def variation(s: k.Scene): dot = k.Dot(r=0.25, x=-4) s.add(dot) move = dot.to(x=4) s.play(move.with_(duration=2, ease=k.ease.out_back, delay=0.2)) ``` #### `k.ease.linear | k.ease.smooth | k.ease.in_ | k.ease.out | k.ease.in_out | k.ease.out_back | k.ease.out_elastic | k.ease.spring(stiffness=100.0, damping=10.0) | k.ease.steps(n) | k.ease.custom(fn, samples=256) | k.ease.reverse(e)` Easing curves. Every verb and every `.to()` uses `k.ease.smooth` (cubic in-out) by default. Also: `linear`, `in_`, `out`, `in_out`, `out_back`, `out_elastic`, `spring(stiffness, damping)`, `steps(n)` and `custom(fn)` for any f: [0, 1] → ℝ. See also: `anim.with_`, `s.play`. ```python @k.scene def easing(s: k.Scene): a = k.Dot(r=0.2, x=-4, y=1) b = k.Dot(r=0.2, x=-4, y=-1) s.add(a, b) s.play(a.to(x=4, ease=k.ease.linear), b.to(x=4, ease=k.ease.out_elastic), duration=2) ``` ### Objects #### `k.Circle(r=1.0, **props)` Circle of radius `r`, centered on its position. Like every shape, it accepts the style props `color` (shorthand for stroke and fill), `fill`, `fill_opacity`, `stroke`, `stroke_width`, `dash` and the transform props. See also: `k.Dot`, `k.Ellipse`, `k.Arc`. ```python @k.scene def circle(s: k.Scene): c = k.Circle(r=1.2, stroke=k.BLUE, fill=k.BLUE, fill_opacity=0.3).place(at="center") s.play(k.draw(c)) s.play(c.to(r=2)) ``` #### `k.Dot(r=0.08, **props)` Filled dot (default radius 0.08), with no stroke. It is the marker used in charts and for points that follow curves. See also: `k.Circle`, `curve.point_at`. ```python @k.scene def dots(s: k.Scene): a = k.Dot(r=0.15, fill=k.RED).place(at="center") label = k.Text("P").place(above=a, gap=0.2) s.play(k.fade_in(a, label)) s.wait(0.5) ``` #### `k.Ellipse(w=2.0, h=1.0, **props)` Ellipse of width `w` and height `h`, centered on its position. See also: `k.Circle`. ```python @k.scene def ellipse(s: k.Scene): e = k.Ellipse(w=4, h=2, stroke=k.PURPLE).place(at="center") s.play(k.draw(e)) s.play(e.to(w=2, h=3)) ``` #### `k.Rect(w=2.0, h=1.0, **props)` Rectangle of width `w` and height `h`, centered on its position; `radius=` rounds the corners (for rounded corners prefer `k.RoundedRect`). See also: `k.RoundedRect`, `k.Square`. ```python @k.scene def rectangle(s: k.Scene): r = k.Rect(w=4, h=2, fill=k.GREEN, fill_opacity=0.3).place(at="center") s.play(k.draw(r)) s.play(r.to(w=6)) ``` #### `k.RoundedRect(w=2.0, h=1.0, radius=0.15, **props)` Rectangle with rounded corners (`radius=0.15` by default). Good for boxes and cards with text inside. See also: `k.Rect`, `obj.place`. ```python @k.scene def card(s: k.Scene): box = k.RoundedRect(w=4, h=2.4, radius=0.3).place(at="center") title = k.Text("Card").place(inside=box, align="top", pad=0.3) s.play(k.draw(box), k.write(title)) s.wait(0.5) ``` #### `k.Square(side=1.0, **props)` Square with side `side`, centered on its position. It is a `k.Rect` with `w == h`. See also: `k.Rect`. ```python @k.scene def square(s: k.Scene): sq = k.Square(2, fill=k.YELLOW, fill_opacity=0.4).place(at="center") s.play(k.draw(sq)) s.play(sq.to(rotate=90)) ``` #### `k.Polygon(*points, **props)` Polygon from its vertices (`k.Polygon((0, 0), (2, 0), (1, 1))`), centered on its bounding box. `k.Polygon.regular(n, r=)` creates the regular n-sided polygon. See also: `k.Triangle`, `k.Path`. ```python @k.scene def hexagon(s: k.Scene): hex_ = k.Polygon.regular(6, r=1.5).place(at="center") s.play(k.draw(hex_)) s.play(hex_.to(rotate=30)) ``` #### `k.Triangle(*points, **props)` Triangle from its three vertices (no arguments: equilateral with radius 1). `k.Triangle.right(a, b, scale=)` creates the right triangle with legs `a` and `b`. See also: `k.Polygon`. ```python @k.scene def triangle(s: k.Scene): tri = k.Triangle.right(3, 4, scale=0.6).place(at="center") s.play(k.draw(tri)) s.wait(0.5) ``` #### `k.Line(start=None, end=None, *, length=None, **props)` Segment from `start` to `end` (local coordinates), or centered with `length=`. The endpoints accept signals, which makes the line follow other objects. See also: `k.Arrow`, `k.Path`. ```python @k.scene def line(s: k.Scene): floor = k.Line(length=10).place(at="bottom", margin=1.5) ray = k.Line(start=(0, 0), end=(2, 1), stroke=k.BLUE) s.play(k.draw(floor), k.draw(ray)) s.wait(0.5) ``` #### `k.Arrow(start=None, end=None, **props)` Arrow from `start` to `end` with a tip of size `tip`. The endpoints accept signals. See also: `k.Line`. ```python @k.scene def arrow(s: k.Scene): a = k.Text("A").place(at="left", margin=3) b = k.Text("B").place(at="right", margin=3) link = k.Arrow(start=(-3.5, 0), end=(3.5, 0), stroke=k.YELLOW, fill=k.YELLOW) s.add(a, b) s.play(k.draw(link)) ``` #### `k.Arc(r=1.0, start_angle=0.0, angle=90.0, **props)` Circular arc of radius `r`, starting at `start_angle` and sweeping `angle` degrees (counterclockwise). See also: `k.Circle`. ```python @k.scene def arc(s: k.Scene): sweep = k.Arc(r=1.5, start_angle=0, angle=270, stroke=k.ORANGE).place(at="center") s.play(k.draw(sweep)) s.play(sweep.to(angle=360)) ``` #### `k.Path(d="", *, closed=False, **props)` Path from SVG commands (`d="M 0 0 L 1 1"`) or a polyline from a list of points; `closed=True` closes the outline. See also: `k.Polygon`, `k.follow`. ```python @k.scene def path(s: k.Scene): zigzag = k.Path([(-3, 0), (-1, 1), (1, -1), (3, 0)], stroke=k.TEAL).place(at="center") s.play(k.draw(zigzag), duration=1.5) s.wait(0.5) ``` #### `k.union(a, b, **style) -> Path` Boolean operations between shapes: `k.union(a, b)`, `k.intersect(a, b)` and `k.subtract(a, b)` return a new `k.Path` computed from the outlines at the cursor (with the style of `a`, unless another one is passed). See also: `k.Path`, `k.Circle`. ```python @k.scene def moon(s: k.Scene): disk = k.Circle(1.5, fill=k.YELLOW, fill_opacity=1, stroke_width=0) shadow = k.Circle(1.3, x=0.8) crescent = k.subtract(disk, shadow) s.play(k.draw(crescent)) ``` #### `k.Bar(value=0.0, *, label=False, width=0.6, unit=0.4, color=None, **props)` A value shown as a bar that grows from its base, with an optional label (`label=True`). `bar.value` is a signal: animating it changes the height and the label. Used in algorithms and charts. See also: `k.grow`, `k.Row`, `group.swap`. ```python @k.scene def bar(s: k.Scene): b = k.Bar(3, label=True).place(at="center") s.play(k.grow(b, from_="bottom")) s.play(b.to(value=7)) s.wait(0.5) ``` #### `k.Group(*children, **props)` Groups objects: transforms compose and opacity multiplies. An object has exactly one parent. Groups are iterable and indexable (`g[0]`), with indices at the cursor. See also: `k.Row`, `k.Stack`, `k.Component`. ```python @k.scene def group(s: k.Scene): sun = k.Circle(r=0.6, fill=k.YELLOW, fill_opacity=1) ray = k.Line(start=(0.8, 0), end=(1.4, 0), stroke=k.YELLOW) icon = k.Group(sun, ray).place(at="center") s.play(k.draw(icon)) s.play(icon.to(scale=1.5, rotate=90)) ``` #### `k.Image(path, width=None, height=None, **props)` Raster image (PNG or JPEG) drawn by the renderer, centered on its position. Without a size it is 3 units tall; with `width=` or `height=` the other side follows the file's aspect ratio (props `w` and `h`, animatable). Relative paths start from the folder of the scene file. Accepts `scale`, `rotate` and `opacity`; morphs and layout treat it as its rectangle, and the exported SVG embeds the file. See also: `k.SVG`, `k.morph`. ```python @k.scene def image(s: k.Scene): photo = k.Image("photo.png", height=4) s.play(k.fade_in(photo), photo.to(scale=0.8, rotate=-5)) ``` #### `k.SVG(source, height=3.0, **props)` Imports an SVG illustration (a file or inline markup): each shape becomes a `k.Path` with the SVG's fill, stroke and stroke width, and each `` becomes a `k.Group`. The drawing is scaled to be `height` units tall and centered on the position. Elements with an `id` are addressable: `svg["#motor"]` returns that object, which animates like any other; `svg.ids` lists the ids. See also: `k.Path`, `k.Image`, `k.Group`. ```python @k.scene def illustration(s: k.Scene): machine = k.SVG("motor.svg", height=3) s.play(k.draw(machine)) s.play(machine["#polia"].to(fill=k.RED), k.indicate(machine["#motor"])) ``` #### `k.Brace(target, direction="down", label=None, gap=0.1, *, depth=0.25, color=None, label_gap=0.12, **props)` Curly brace (`}`) along one side of an object's box: `direction=` "down", "up", "left" or "right", `gap` units away from it, with the tip pointing outward. It is recomputed every frame from the target's layout, so it follows the object when it moves or changes size. `label=` (text or an object) sits beyond the tip, as `brace.label`; the brace itself is `brace.shape`. See also: `k.Rect`, `obj.place`. ```python @k.scene def brace(s: k.Scene): bar = k.Rect(w=3, h=0.6, fill=k.BLUE, fill_opacity=0.8) s.play(k.draw(bar), k.draw(k.Brace(bar, "down", label="width"))) s.play(bar.to(w=6)) ``` #### `k.Points(xy=None, radius=0.02, color=None, *, x=None, y=None, **props)` Thousands of points in a single object, batch-drawn in the core. `xy` is a numpy array `(n, 2)`, a list of `(x, y)` or columns (`x=`, `y=`). `radius` and `color` accept one value, one per point, or a function of the symbolic point `p` (`p.x`, `p.y`, `p.index`, `p.t`), traced and vectorized in Rust. Never create thousands of `k.Dot` in a loop (W0901). See also: `k.VectorField`, `k.StreamLines`, `k.python`. ```python import numpy as np import kinemo as k @k.scene def cloud(s: k.Scene): xy = np.random.default_rng(1).uniform((-6, -3), (6, 3), size=(5000, 2)) pts = k.Points(xy, radius=0.03, color=lambda t, p: k.mix(k.BLUE, k.RED, (p.x + 6) / 12)) s.play(k.draw(pts), duration=2) ``` #### `k.VectorField(fn, density=30, *, length=0.8, x_range=None, y_range=None, color=None, **props)` Arrows of a field `fn(x, y) -> (vx, vy)` on a grid with `density` columns across the width of the region (`x_range`, `y_range`). Size and color (theme accent → secondary) follow the magnitude; `length` scales the arrows. `fn` is traced once and may use `k.time`. See also: `k.StreamLines`, `k.Points`. ```python @k.scene def rotation(s: k.Scene): field = k.VectorField(lambda x, y: (-y, x), density=24) s.play(k.draw(field), duration=2) ``` #### `k.StreamLines(field, seeds=200, *, step=0.05, steps=60, progress=1.0, tail=1.0, fade=0.0, x_range=None, y_range=None, color=None, **props)` Streamlines of a field (a `k.VectorField` or a function), integrated with RK4 in the core from `seeds` (a count or points). `progress` draws the lines over time; `tail` is the visible fraction behind the head and `fade` the opacity of the tail. See also: `k.VectorField`. ```python @k.scene def stream(s: k.Scene): field = k.VectorField(lambda x, y: (-y, x), density=24) lines = k.StreamLines(field, seeds=150, progress=0.0, tail=0.5) s.add(field) s.play(k.fade_in(lines), lines.to(progress=1), duration=3) ``` ### Text #### `k.Text(text="", *, size=None, width=None, align="left", **props)` Text with minimal inline markup (`**bold**`, `*italic*`, `` `code` ``). `size=` sets the size, `width=` wraps lines, `align=` aligns. With a lambda the text is reactive: `k.Text(lambda: f"{x():.1f} kWh")`. See also: `k.write`, `k.signal`. ```python @k.scene def text(s: k.Scene): x = k.signal(0.0) title = k.Text("**Solar** energy", size=0.7).place(at="top", margin=0.8) value = k.Text(lambda: f"{x():.1f} kWh").place(at="center") s.play(k.write(title), k.fade_in(value)) s.play(x.to(12), duration=2) ``` #### `k.Math(tex, *, size=0.6, display=True, engine="builtin", **props)` Formula in LaTeX syntax, typeset by the built-in engine (no TeX installation needed). `\id{name}{...}` names a subexpression (`eq["name"]`); any subexpression is found through the syntax tree (`eq["c^2"]` ≡ `eq["c^{2}"]`). `k.morph` between equations matches names first, then identical TeX subtrees. Unsupported command: K0801. See also: `k.morph`, `k.Text`, `k.write`. ```python @k.scene def formula(s: k.Scene): eq = k.Math(r"\id{lhs}{a^2 + b^2} = c^2").place(at="center") s.play(k.write(eq)) s.play(eq["lhs"].to(color=k.YELLOW)) s.play(eq["c^2"].to(color=k.GREEN)) ``` #### `k.Code(src, lang="python", *, theme="auto", line_numbers=False, size=0.32, **props)` Code with syntax highlighting (tree-sitter) and stable tokens: `lang=`, `line_numbers=True`, `size=`, `theme="auto"` (follows the scene background). `code.highlight` highlights lines and `k.morph` between two versions animates the diff: unchanged lines slide, inserted lines enter, removed lines leave. See also: `code.highlight`, `k.morph`, `k.Text`. ```python SRC = """ def add(a, b): return a + b """ @k.scene def code_block(s: k.Scene): code = k.Code(SRC, lang="python", line_numbers=True).place(at="center") s.play(k.write(code)) s.wait(0.5) ``` #### `code.highlight(lines=None, *, duration=None, ease=None) -> Animation` Named transition: dims every line except `lines` (numbered from 1); `code.highlight(None)` removes the highlight. Equivalent to `code.to(highlight=lines, highlight_amount=1)`. See also: `k.Code`. ```python SRC = """ x = 1 y = 2 print(x + y) """ @k.scene def code_highlight(s: k.Scene): code = k.Code(SRC, lang="python").place(at="center") s.add(code) s.play(code.highlight(lines=[3])) s.play(code.highlight(None)) ``` ### Layout #### `obj.place(at=, above=, below=, left_of=, right_of=, inside=, gap=, margin=, align=, pad=, clamp=False, weak=False)` Declares where the object sits, relative to the frame (`at="top"`, `margin=`) or to another object (`above=`, `below=`, `left_of=`, `right_of=`, `inside=` with `pad=`), with `gap=` (accepts a signal) and `align=`. The relation keeps holding while the objects move. Returns the object itself, so it chains with the constructor. See also: `obj.to_place`, `obj.unpin`, `k.Row`, `obj.to`. ```python @k.scene def positions(s: k.Scene): box = k.Square(2).place(at="center") title = k.Text("Title").place(at="top", margin=0.6) label = k.Text("box", size=0.4).place(below=box, gap=0.3) s.add(box, title, label) s.play(box.to(scale=1.5)) ``` #### `obj.to_place(at=, above=, below=, left_of=, right_of=, inside=, gap=, margin=, align=, pad=, clamp=False, weak=False, duration=None, ease=None, delay=0.0)` Animated change of placement: the `.to` of `.place`. Takes the same keywords as `.place(...)` plus `duration=`, `ease=` and `delay=`, and moves the object from its current placement to the new one. The new constraint keeps holding afterwards. See also: `obj.place`, `obj.unpin`, `obj.to`. ```python @k.scene def replace(s: k.Scene): tri = k.Triangle().place(at="center") title = k.Text("title", size=0.5).place(above=tri, gap=0.4) s.add(tri, title) s.play(title.to_place(right_of=tri, gap=0.4)) s.play(tri.to_place(at="left", margin=2)) # the title follows ``` #### `obj.unpin()` Releases the position constraint at the cursor: the object keeps its current position and is free to animate `x`/`y`. Inside an animation, `obj.to(x=..., unpin=True)` does the same where the animation starts. To switch constraints instead, use `obj.to_place(...)`. See also: `obj.place`, `obj.to_place`, `obj.to`. ```python @k.scene def release(s: k.Scene): tri = k.Triangle().place(at="center") title = k.Text("free").place(above=tri, gap=0.4) s.add(tri, title) s.play(title.to(x=3, unpin=True)) title.unpin() # the instant form, at the cursor ``` #### `k.Row(*children, gap=0.25, align="center", **props)` Container that lays its children out in a row (flexbox), with `gap=` and `align=` (`"center"`, `"top"`, `"bottom"`). Changing children with the container's transitions (`row.swap`, `row.insert`, `row.pop`) animates the reflow. See also: `k.Column`, `k.Grid`, `group.swap`. ```python @k.scene def row(s: k.Scene): items = [k.Square(0.8) for _ in range(4)] r = k.Row(*items, gap=0.4).place(at="center") s.play(k.draw(r)) s.play(r.swap(0, 3)) ``` #### `k.Column(*children, gap=0.25, align="center", **props)` Container that stacks its children in a column, with `gap=` and `align=` (`"center"`, `"left"`, `"right"`). See also: `k.Row`, `k.Grid`. ```python @k.scene def column(s: k.Scene): title = k.Text("Steps", size=0.7) steps = [k.Text(t, size=0.45) for t in ("1. measure", "2. compare", "3. decide")] col = k.Column(title, *steps, gap=0.3, align="left").place(at="center") s.play(k.write(col)) s.wait(0.5) ``` #### `k.Grid(*children, cols=3, gap=0.25, **props)` Grid container with `cols=` columns and `gap=`. Combine with `.fit(s.frame.safe)` to scale it down until it fits the frame. See also: `k.Row`, `group.fit`. ```python @k.scene def grid(s: k.Scene): cards = [k.RoundedRect(w=1.6, h=1) for _ in range(6)] g = k.Grid(*cards, cols=3, gap=0.3).place(at="center") s.play(k.stagger([k.draw(c) for c in cards], lag=0.1)) s.wait(0.5) ``` #### `k.Stack(*children, align="center", **props)` Container that overlays its children, centered (or aligned by `align=`). Good for icons over backgrounds. See also: `k.Group`, `k.Row`. ```python @k.scene def stack(s: k.Scene): background = k.Circle(r=1.2, fill=k.BLUE, fill_opacity=0.3) icon = k.Text("42", size=0.9) badge = k.Stack(background, icon).place(at="center") s.play(k.fade_in(badge)) s.wait(0.5) ``` #### `group.fit(area, margin=0.0) -> Group` Scales the group once, at the cursor, until it fits an area (usually `s.frame.safe`, the frame's safe area), with an optional `margin=`. See also: `k.Grid`. ```python @k.scene def fit(s: k.Scene): cards = [k.RoundedRect(w=3, h=2) for _ in range(12)] grid = k.Grid(*cards, cols=4, gap=0.4).place(at="center").fit(s.frame.safe) s.play(k.draw(grid)) s.wait(0.5) ``` #### `group.swap(i, j, **kw) -> Animation` Named transition: swaps the places of two children and the container animates the reflow. Equivalent to `group.to(children=[...])` with `i` and `j` swapped. See also: `group.insert`, `group.pop`, `k.Row`. ```python @k.scene def swap(s: k.Scene): bars = [k.Bar(v, label=True) for v in [4, 1, 3]] row = k.Row(*bars, gap=0.3, align="bottom").place(at="center") s.add(row) s.play(row.swap(0, 1), duration=0.6) s.play(row.swap(1, 2), duration=0.6) ``` #### `group.insert(i, obj, **kw) -> Animation` Named transition: inserts a child at position `i`; it enters together with the reflow. Equivalent to `group.to(children=[...])`. See also: `group.pop`, `group.swap`. ```python @k.scene def insert(s: k.Scene): row = k.Row(k.Square(0.8), k.Square(0.8), gap=0.4).place(at="center") s.add(row) s.play(row.insert(1, k.Circle(r=0.4, fill=k.RED, fill_opacity=1))) s.wait(0.5) ``` #### `group.pop(i=-1, **kw) -> Animation` Named transition: removes the child at position `i` (default: the last one); it exits together with the reflow. Equivalent to `group.to(children=[...])`. See also: `group.insert`, `group.swap`. ```python @k.scene def remove(s: k.Scene): row = k.Row(*[k.Square(0.8) for _ in range(4)], gap=0.4).place(at="center") s.add(row) s.play(row.pop(0)) s.wait(0.5) ``` ### Charts #### `k.PolarAxes(r=(0, 1), *, radius=3.0, spokes=12, labels=True, **props)` Polar axes (rings and spokes): `r=(0, r_max, step)`, `radius=` in units, `spokes=`. `pa.plot(lambda a: r(a))` draws `r = f(θ)`; `pa.point(r, θ)` gives the world position. See also: `k.Axes`, `ax.parametric`. ```python def petals(a: float) -> float: return abs(k.cos(3 * a)) @k.scene def rose(s: k.Scene): pa = k.PolarAxes(r=(0, 1, 0.25), radius=3).place(at="center") curve = pa.plot(petals, color=k.PINK) s.add(pa) s.play(k.draw(curve), duration=2) ``` #### `ax.parametric(fx, fy, *, t=(0.0, 6.283185307179586), samples=300, color=None, **style) -> ParametricPlot` Parametric curve `(fx(t), fy(t))` for `t=(start, end)`; clipped to the visible ranges. Siblings: `ax.bars(xs, heights, width=)` (bars in data units) and `ax.scatter(xs, ys)`. See also: `ax.plot`, `k.Axes`. ```python def fx(t: float) -> float: return k.sin(2 * t) def fy(t: float) -> float: return k.sin(3 * t) @k.scene def loop(s: k.Scene): ax = k.Axes(x=(-1.5, 1.5), y=(-1.5, 1.5), width=5, height=5).place(at="center") curve = ax.parametric(fx, fy, color=k.TEAL) s.add(ax) s.play(k.draw(curve), duration=2) ``` #### `k.Axes(x=(0, 10), y=(0, 5), *, labels=None, grid=False, width=8.0, height=4.5, tick_labels=True, **props)` Cartesian axes with ticks, labels and an optional grid: `x=(min, max, step)`, `y=(min, max)`, `labels=("x", "y")`, `width=`/`height=` in units. The visible ranges are signals: `ax.zoom_to(...)` animates them and everything on the axes follows. See also: `ax.plot`, `ax.zoom_to`, `k.NumberLine`. ```python @k.scene def axes(s: k.Scene): ax = k.Axes(x=(0, 10, 2), y=(0, 5, 1), labels=("t", "v"), grid=True).place(at="center") s.play(k.draw(ax)) s.wait(0.5) ``` #### `k.NumberLine(x=(0, 10, 1), *, width=10.0, **props)` A horizontal number line: a `k.Axes` with only the x axis. Use `nl.point(x, 0)` to position markers. See also: `k.Axes`, `ax.point`. ```python @k.scene def number_line(s: k.Scene): nl = k.NumberLine(x=(-3, 3, 1), width=10).place(at="center") x = k.signal(-2.0) dot = k.Dot(r=0.15, fill=k.RED).place(at=nl.point(x, 0)) s.play(k.draw(nl), k.fade_in(dot)) s.play(x.to(2), duration=2) ``` #### `ax.plot(fn, *, until=None, from_=None, domain=None, color=None, label=None, samples=160, **style) -> Plot` Draws the curve `y = fn(x)` on the axes, with adaptive sampling. `fn` uses `k` functions (`k.sin`, `k.max`...), so the same function works for floats and signals. `until=`/`from_=` accept signals: the curve grows as the signal moves. `label=` puts a label at the end of the curve. See also: `curve.point_at`, `ax.area`, `k.sin`. ```python def wave(x: float) -> float: return 2 + 1.5 * k.sin(x) @k.scene def wave_plot(s: k.Scene): ax = k.Axes(x=(0, 10, 2), y=(0, 4, 1)).place(at="center") t = k.signal(0.0) ax.plot(wave, until=t, color=k.YELLOW, label="wave") s.play(k.draw(ax)) s.play(t.to(10), duration=3, ease=k.ease.linear) ``` #### `ax.area(f, *, between=None, domain=None, until=None, samples=200, **style) -> Area` Filled region under a curve (down to the x axis) or between two curves (`between=`). Accepts a curve from `ax.plot` or a function, plus style (`fill=`, `fill_opacity=`). See also: `ax.plot`. ```python def f(x: float) -> float: return 0.1 * x * x @k.scene def area(s: k.Scene): ax = k.Axes(x=(0, 6, 1), y=(0, 4, 1)).place(at="center") curve = ax.plot(f, color=k.BLUE) ax.area(curve, domain=(1, 5), fill=k.BLUE, fill_opacity=0.3) s.play(k.draw(ax)) s.wait(0.5) ``` #### `ax.vline(at, *, style="solid", **props) -> Line` Vertical line on the axes at `at=` (accepts a signal: the line moves with it); `style="dashed"` makes it dashed. See also: `ax.hline`, `ax.plot`. ```python @k.scene def marker(s: k.Scene): ax = k.Axes(x=(0, 24, 6), y=(0, 5)).place(at="center") hour = k.signal(6.0) ax.vline(at=hour, style="dashed", stroke=k.YELLOW) s.play(k.draw(ax)) s.play(hour.to(18), duration=2) ``` #### `ax.hline(at, *, style="solid", **props) -> Line` Horizontal line on the axes at `at=` (accepts a signal); `style="dashed"` makes it dashed. Good for limits and targets. See also: `ax.vline`. ```python @k.scene def limit(s: k.Scene): ax = k.Axes(x=(0, 10, 2), y=(0, 5, 1)).place(at="center") ax.hline(at=4, style="dashed", stroke=k.RED) s.play(k.draw(ax)) s.wait(0.5) ``` #### `ax.scatter(xs, ys, *, radius=0.06, **props) -> Group[Dot]` Points `(xs[i], ys[i])` on the axes, as a group of `k.Dot`. Accepts lists, numpy arrays and Arrow columns (polars, pandas, pyarrow). See also: `ax.plot`, `k.Dot`. ```python @k.scene def scatter(s: k.Scene): ax = k.Axes(x=(0, 5, 1), y=(0, 5, 1)).place(at="center") pts = ax.scatter([1, 2, 3, 4], [1.5, 2.2, 3.1, 3.8], radius=0.1, fill=k.TEAL) s.play(k.draw(ax)) s.play(k.indicate(pts)) ``` #### `ax.zoom_to(*, x=None, y=None, **kw) -> Animation` Named transition: animates the visible ranges of the axes (`x=(a, b)`, `y=(c, d)`). Curves, ticks and points follow. Equivalent to `ax.to(x_range=..., y_range=...)`. See also: `k.Axes`, `ax.plot`. ```python @k.scene def zoom(s: k.Scene): ax = k.Axes(x=(-4, 4, 1), y=(-1, 9, 1)).place(at="center") ax.plot(lambda x: x * x, color=k.YELLOW) s.play(k.draw(ax)) s.play(ax.zoom_to(x=(0, 2), y=(0, 4)), duration=1.5) ``` #### `ax.point(x, y) -> Expr[Vec]` Data point `(x, y)` in world coordinates, reactive when `x` or `y` are signals. It is the target of `place(at=...)` for markers on the axes. See also: `curve.point_at`, `obj.place`. ```python @k.scene def point(s: k.Scene): ax = k.Axes(x=(0, 10, 2), y=(0, 10, 2)).place(at="center") pin = k.Dot(r=0.12, fill=k.RED).place(at=ax.point(3, 9)) label = k.Text("(3, 9)", size=0.35).place(right_of=pin, gap=0.2) s.play(k.draw(ax)) s.play(k.fade_in(pin, label)) ``` #### `curve.point_at(x) -> Expr[Vec]` World position of the curve at `x`; reactive when `x` is a signal. With `place(at=...)` it makes a dot slide along the curve. `curve.value_at(x)` gives the y value. See also: `curve.tangent_at`, `curve.slope_at`, `ax.point`. ```python def f(x: float) -> float: return 0.15 * x**3 - 0.9 * x + 1.5 @k.scene def slide(s: k.Scene): ax = k.Axes(x=(-3, 3, 1), y=(-1, 5, 1)).place(at="center") curve = ax.plot(f, color=k.YELLOW) x = k.signal(-2.0) dot = k.Dot(r=0.1, fill=k.RED).place(at=curve.point_at(x)) s.play(k.draw(ax), k.fade_in(dot)) s.play(x.to(2.5), duration=3) ``` #### `curve.tangent_at(x, length=2.0, **style) -> Line` Tangent segment `length` units long, centered on the curve at `x`, reactive when `x` is a signal. It is added to the axes directly; accepts style (`stroke=`). See also: `curve.slope_at`, `curve.point_at`. ```python @k.scene def tangent(s: k.Scene): ax = k.Axes(x=(-3, 3, 1), y=(-1, 9, 1)).place(at="center") curve = ax.plot(lambda x: x * x, color=k.YELLOW) x = k.signal(-2.0) curve.tangent_at(x, length=3, stroke=k.RED) s.play(k.draw(ax)) s.play(x.to(2), duration=3) ``` #### `curve.slope_at(x) -> Expr[float]` Numerical derivative of the curve at `x`, reactive when `x` is a signal. Use it inside lambdas with a tracked read: `curve.slope_at(x)()`. See also: `curve.tangent_at`. ```python @k.scene def slope(s: k.Scene): ax = k.Axes(x=(-3, 3, 1), y=(-1, 9, 1)).place(at="center") curve = ax.plot(lambda x: x * x, color=k.YELLOW) x = k.signal(-2.0) label = k.Text(lambda: f"f'({x():.1f}) = {curve.slope_at(x)():.1f}").place(at="top", margin=0.5) s.play(k.draw(ax), k.fade_in(label)) s.play(x.to(2), duration=3) ``` #### `k.BarChart(data, x, y, *, key=None, width=8.0, height=4.5, color=None, labels=True, grid=False, bar_ratio=0.7, label_size=0.28, **props)` Bar chart from a table: `x=` is the category column, `y=` the value column and `key=` identifies each bar. Accepts Arrow sources without copying (polars, pandas, pyarrow, duckdb), dicts of lists and lists of dicts. `chart.to(data=df2)` animates the change: bars grow, move, enter and leave by key; the value axis follows. See also: `k.LineChart`, `k.Table`, `k.Bar`. ```python @k.scene def energy(s: k.Scene): chart = k.BarChart({"country": ["PT", "ES"], "gwh": [50, 260]}, x="country", y="gwh").place(at="center") s.play(k.draw(chart)) s.play(chart.to(data={"country": ["ES", "PT", "FR"], "gwh": [300, 80, 540]}), duration=2) ``` #### `k.LineChart(data, x, y, *, x_range=None, y_range=None, width=8.0, height=4.5, colors=None, dots=False, legend=True, **props)` A `k.Axes` with one line per `y=` column (one or several), connecting the table's points in `x=` order. `chart.to(data=df2)` morphs the lines point by point; the axis ranges stay fixed (use `y_range=` to cover all the data). See also: `k.Axes`, `k.BarChart`. ```python @k.scene def lines(s: k.Scene): chart = k.LineChart({"h": [0, 12, 24], "kw": [0, 6, 0]}, x="h", y="kw", y_range=(0, 8)).place(at="center") s.play(k.draw(chart)) s.play(chart.to(data={"h": [0, 6, 12, 18, 24], "kw": [0, 3, 7, 3, 0]}), duration=2) ``` #### `k.Table(data, columns=None, *, size=0.32, header_color=None, rule=True, **props)` Table of `k.Text` with a highlighted header; `columns=` selects and orders the columns. `table.to(data=df2)` updates the cells: changed texts flash with the new value, new rows appear and removed ones leave. `table.cells[r][c]` are the cells. See also: `k.BarChart`, `k.Text`. ```python @k.scene def table(s: k.Scene): t = k.Table({"country": ["PT", "ES"], "gwh": [50, 260]}).place(at="center") s.play(k.fade_in(t)) s.play(t.to(data={"country": ["PT", "ES", "FR"], "gwh": [80, 260, 1200]})) ``` ### Reactive #### `k.signal(initial, *, lerp="linear", kind=None) -> Signal` A value with a timeline. `x.set(v)` changes it at the cursor, `s.play(x.to(v))` animates it, `x.now` reads the value at the cursor (build phase) and `x()` does a tracked read inside lambdas. Passing the signal to a prop creates a reactive binding. `lerp=None` switches in a single step. Every object prop is a signal with the same API. See also: `k.computed`, `x.map`, `obj.set`. ```python @k.scene def signal_demo(s: k.Scene): r = k.signal(0.5) c = k.Circle(r=r).place(at="center") label = k.Text(lambda: f"r = {r():.2f}").place(at="top", margin=0.8) s.add(c, label) s.play(r.to(2), duration=2) r.set(1) s.wait(0.5) ``` #### `k.computed(fn) -> Expr` Derived value with several dependencies: `k.computed(lambda: f(a(), b()))`. The function is traced to native code (error K0310 if it is not traceable); derived values are read-only (animate the sources). For a single signal, use `x.map(fn)`. See also: `x.map`, `k.signal`, `k.python`. ```python @k.scene def derived(s: k.Scene): w = k.signal(2.0) h = k.signal(1.0) area = k.computed(lambda: w() * h()) box = k.Rect(w=w, h=h).place(at="center") label = k.Text(lambda: f"area = {area():.1f}").place(at="top", margin=0.8) s.add(box, label) s.play(w.to(4), h.to(2), duration=2) ``` #### `x.map(fn) -> Expr` Applies a function to a signal: `hour.map(solar_curve)`. The function is traced to native code, so use `k` functions (`k.sin`, `k.where`...), not `math` or `if`; for opaque Python, `x.map(k.python(fn))`. See also: `k.computed`, `k.python`, `k.time`. ```python def solar_curve(h: float) -> float: return k.max(0, 6 * k.sin(k.pi * (h - 6) / 12)) @k.scene def mapping(s: k.Scene): hour = k.time.map(lambda t: t * 4) radius = hour.map(solar_curve) * 0.2 + 0.1 sun = k.Circle(r=radius, fill=k.YELLOW, fill_opacity=1).place(at="center") s.add(sun) s.wait(5) ``` #### `k.list(items=None) -> ListSignal` List signal. Plain Python lists are not tracked; `k.list([...])` records `append`, `insert`, `pop` and `swap` at the cursor, and lambdas that read it with `items()` follow the changes. See also: `k.signal`. ```python @k.scene def queue_demo(s: k.Scene): queue = k.list(["ann", "bea"]) label = k.Text(lambda: f"queue: {queue()}").place(at="center") s.add(label) s.wait(1) queue.append("cal") s.wait(1) ``` #### `k.python(fn, vectorized=False) -> PythonFn[Any, Any]` Marks an opaque Python function (external libraries, `math`, untraceable logic). It is precomputed in the resolve phase, one call per frame, and the render only reads the table. `vectorized=True` receives the whole timeline as a numpy array. Prefer `k` functions: they run natively at no extra cost. See also: `x.map`, `k.computed`. ```python import math import kinemo as k def opaque(t: float) -> float: return 1 + 0.5 * math.sin(t) ** 2 @k.scene def explicit_cost(s: k.Scene): c = k.Circle(r=k.time.map(k.python(opaque))).place(at="center") s.add(c) s.wait(3) ``` #### `k.lerp.linear | k.lerp.round | k.lerp.step | k.lerp.step_start | k.lerp.pointwise` Interpolation modes of a signal, passed as `k.signal(..., lerp=)`: `linear` (default), `round` (integers), `step` (switches at the end), `step_start` (switches at the start) and `pointwise` (lists of points, point by point). See also: `k.signal`. ```python @k.scene def counter(s: k.Scene): n = k.signal(0, lerp=k.lerp.round) label = k.Text(lambda: f"{n():.0f} steps", size=0.8).place(at="center") s.add(label) s.play(n.to(10), duration=2) ``` ### Native blocks #### `k.sin(x)` Native math functions: `k.sin`, `k.cos`, `k.tan`, `k.exp`, `k.log`, `k.sqrt` and `k.atan2(y, x)`. Use them instead of `math.*`, which is not traceable (error K0305/K0310). Like every `k` function, it accepts floats, signals and numpy arrays: the same function works for `ax.plot` (floats) and for `.map` (traced, native). See also: `k.pi`, `x.map`, `k.time`. ```python @k.scene def oscillator(s: k.Scene): dot = k.Dot(r=0.2, x=k.cos(k.time * 2) * 3, y=k.sin(k.time * 2) * 2) s.add(dot) s.wait(4) ``` #### `k.floor(x)` Native rounding down (`k.floor`) and up (`k.ceil`). They replace `int()` and `math.floor`, which are not traceable. Like every `k` function, it accepts floats, signals and numpy arrays: the same function works for `ax.plot` (floats) and for `.map` (traced, native). See also: `k.time`, `k.clamp`. ```python @k.scene def counter(s: k.Scene): hour = k.time * 2 clock = k.Text(lambda: f"{k.floor(hour()):02.0f}:00", size=0.9).place(at="center") s.add(clock) s.wait(4) ``` #### `k.min(*args)` Native minimum and maximum of two or more values (`k.min(a, b, c)`). They replace the built-in `min()`/`max()`, which are not traceable. Like every `k` function, it accepts floats, signals and numpy arrays: the same function works for `ax.plot` (floats) and for `.map` (traced, native). See also: `k.clamp`, `k.where`. ```python @k.scene def capped(s: k.Scene): hour = k.time.map(lambda t: k.min(t * 6, 24)) label = k.Text(lambda: f"{hour():.0f} h", size=0.9).place(at="center") s.add(label) s.wait(5) ``` #### `k.clamp(x, lo, hi)` Clamps a value to the range `[lo, hi]`, natively. Like every `k` function, it accepts floats, signals and numpy arrays: the same function works for `ax.plot` (floats) and for `.map` (traced, native). See also: `k.min`, `k.smoothstep`. ```python @k.scene def limit(s: k.Scene): x = k.signal(-6.0) dot = k.Dot(r=0.2, x=k.clamp(x, -3, 3)) s.add(dot) s.play(x.to(6), duration=3) ``` #### `k.where(cond, a, b)` The traceable `if`: `a` where `cond` holds, otherwise `b`. Combine conditions with `&`, `|` and `~` (not with `and`/`or`). Like every `k` function, it accepts floats, signals and numpy arrays: the same function works for `ax.plot` (floats) and for `.map` (traced, native). See also: `k.piecewise`, `k.when`. ```python def tariff(h: float) -> float: return k.where((h >= 18) & (h < 21), 1.8, 0.6) @k.scene def peak(s: k.Scene): hour = k.time * 6 label = k.Text(lambda: f"{hour():.0f} h: $ {tariff(hour()):.2f}").place(at="center") s.add(label) s.wait(4) ``` #### `k.piecewise(*cases, default=0.0)` Piecewise function: `k.piecewise((cond1, v1), (cond2, v2), default=v)`; the first true condition wins. Like every `k` function, it accepts floats, signals and numpy arrays: the same function works for `ax.plot` (floats) and for `.map` (traced, native). See also: `k.where`. ```python def band(v: float) -> k.Color: return k.piecewise((v < 1, k.GREEN), (v < 2, k.YELLOW), default=k.RED) @k.scene def traffic_light(s: k.Scene): v = k.signal(0.0) lamp = k.Circle(r=1, fill=v.map(band), fill_opacity=1).place(at="center") s.add(lamp) s.play(v.to(3), duration=3) ``` #### `k.spline(x, xs, ys)` Smooth table interpolation (natural cubic spline): `k.spline(x, xs, ys)`. Same inputs as `k.interp`; outside the range it holds the endpoint value. Like every `k` function, it accepts floats, signals and numpy arrays: the same function works for `ax.plot` (floats) and for `.map` (traced, native). See also: `k.interp`, `k.smoothstep`. ```python @k.scene def smooth(s: k.Scene): x = k.signal(0.0) height = k.spline(x, [0, 1, 2, 3], [0, 2, 1, 2]) dot = k.Dot(r=0.15, x=x * 2 - 3, y=height - 1) s.add(dot) s.play(x.to(3), duration=2) ``` #### `k.interp(x, xs, ys)` Linear table interpolation: `k.interp(x, xs, ys)`. `xs`/`ys` can be lists, numpy arrays or Arrow columns (polars, pandas, pyarrow), without copying. Like every `k` function, it accepts floats, signals and numpy arrays: the same function works for `ax.plot` (floats) and for `.map` (traced, native). See also: `ax.plot`, `k.mix`. ```python HOURS = [0, 6, 12, 18, 24] KW = [0.5, 0.8, 1.2, 2.5, 0.6] @k.scene def load(s: k.Scene): hour = k.time * 4 label = k.Text(lambda: f"{k.interp(hour(), HOURS, KW):.2f} kW").place(at="center") s.add(label) s.wait(5) ``` #### `k.mix(a, b, t)` Linear mix `a + (b - a) * t` of numbers, vectors or colors (colors in OKLab, with no gray in the middle). Like every `k` function, it accepts floats, signals and numpy arrays: the same function works for `ax.plot` (floats) and for `.map` (traced, native). See also: `k.interp`, `k.smoothstep`. ```python @k.scene def gradient(s: k.Scene): t = k.signal(0.0) box = k.Square(2, fill=k.mix(k.RED, k.GREEN, t), fill_opacity=1).place(at="center") s.add(box) s.play(t.to(1), duration=2) ``` #### `k.smoothstep(e0, e1, x)` Smooth transition from 0 to 1 as `x` goes from `e0` to `e1` (cubic Hermite). Like every `k` function, it accepts floats, signals and numpy arrays: the same function works for `ax.plot` (floats) and for `.map` (traced, native). See also: `k.clamp`, `k.mix`. ```python @k.scene def fade_glow(s: k.Scene): glow = k.smoothstep(1, 3, k.time) c = k.Circle(r=1.5, fill=k.YELLOW, fill_opacity=glow).place(at="center") s.add(c) s.wait(4) ``` #### `k.noise(x, seed=0) -> Expr[float]` Smooth, deterministic noise in [-1, 1] (`seed=` changes the sequence). Good for jitter and organic motion. See also: `k.sin`, `k.time`. ```python @k.scene def jitter(s: k.Scene): leaf = k.Ellipse(w=1.2, h=0.6, fill=k.GREEN, fill_opacity=1) leaf.set(x=k.noise(k.time, seed=1) * 2, y=k.noise(k.time, seed=2)) s.add(leaf) s.wait(4) ``` #### `k.vec(x, y)` 2D vector from two values (numbers or signals). With signals, the vector is reactive: use it in point props such as a line's `start=`/`end=`. See also: `k.sin`, `k.Line`. ```python @k.scene def clock_hand(s: k.Scene): angle = k.time * 2 hand = k.Line(start=(0, 0), end=k.vec(k.cos(angle) * 2, k.sin(angle) * 2)) s.add(hand) s.wait(4) ``` #### `k.pi = 3.14159` Constants `k.pi`, `k.tau` (2π) and `k.e`, for use inside traced functions. See also: `k.sin`. ```python @k.scene def orbit(s: k.Scene): angle = k.time * k.tau / 4 dot = k.Dot(r=0.2, x=k.cos(angle) * 2, y=k.sin(angle) * 2) s.add(dot) s.wait(4) ``` ### Stateful systems #### `k.when(cond, action, *, once=False, rearm=None) -> Effect` Edge-triggered effect: when `cond` goes from false to true, it fires an animation or emits an event. It does not move the cursor. Without `rearm=`, the condition must become false again before it fires again; `once=True` fires only the first time. See also: `event.on`, `s.wait_for`, `k.integrate`. ```python @k.scene def trigger(s: k.Scene): x = k.signal(-4.0) dot = k.Dot(r=0.25, x=x) s.add(dot) k.when(x >= 2, k.flash(dot, color=k.YELLOW)) s.play(x.to(4), duration=3, ease=k.ease.linear) ``` #### `k.integrate(expr, d=None, *, initial=0.0, clamp=None) -> Expr[float]` Integral of an expression with respect to the change in `d=` (default `k.time`), starting at the cursor, with `initial=` and `clamp=(lo, hi)`. It is precomputed during resolve; the result is a read-only signal. A clock used as `d=` must advance linearly. See also: `k.when`, `k.time`, `k.Component`. ```python @k.scene def tank(s: k.Scene): flow = k.signal(0.5) level = k.integrate(flow, initial=0.0, clamp=(0, 3)) water = k.Rect(w=2, h=level + 0.01, fill=k.BLUE, fill_opacity=0.8).place(at="center") s.add(water) s.wait(2) s.play(flow.to(-1), duration=1) s.wait(2) ``` #### `k.simulate(step, state, dt=0.00416667, until=None) -> Simulation` Fixed-step simulation: `step(state, dt) -> state`, run in Python during resolve. The state is a subclass of `k.State` (float, bool or pair fields with defaults, plus `k.Event[P]` events); states are values, so the step returns `st.replace(field=...)` and emits events with `st.event.emit(payload)` — an immediate effect at the simulated instant, not part of the returned value (event without payload: `bounce: k.Event`, `st.bounce.emit()`). The step is plain Python (`if`, `min`, `math` all work). Each field becomes a signal (`sim.y`) and each event a source (`sim.bounce`). Start it with `s.start(sim)`; `sim.done` fires at the end (`until=`). See also: `event.on`, `s.wait_for`, `k.Event`. ```python class Fall(k.State): y: float = 3.0 v: float = 0.0 def step(st: Fall, dt: float) -> Fall: return st.replace(y=st.y + st.v * dt, v=st.v - 9.8 * dt) @k.scene def fall(s: k.Scene): sim = k.simulate(step, Fall(), dt=1 / 240, until=1) s.add(k.Circle(r=0.3, y=sim.y)) s.start(sim) s.wait_for(sim.done) ``` #### `k.trace(point, length=2.0, **style) -> Trail` Trail: the last `length` seconds of the path of a moving point (usually `obj.world.position`), drawn as a stroke. It is sampled from the timeline, so rendering stays pure. See also: `k.follow`, `k.time`. ```python @k.scene def trail(s: k.Scene): dot = k.Dot(r=0.15, x=k.cos(k.time * 2) * 3, y=k.sin(k.time * 3) * 2) trail = k.trace(dot.world.position, length=1.5, stroke=k.TEAL) s.add(trail, dot) s.wait(4) ``` ### Events #### `@event.on · @event.on(once=True)` Decorator that reacts to every firing of an event: `@source.on` registers `def handler(s, e)`, run during resolve with its own `s` whose cursor starts at `e.time` (the main cursor does not change). `.on(once=True)` reacts only to the first one. Objects created in the handler must leave the scene (lint W0701). See also: `s.wait_for`, `k.when`, `k.EventInfo`. ```python @k.scene def reaction(s: k.Scene): box = k.Square(1.5).place(at="center") h = s.play(k.draw(box)) @h.done.on def notify(s: k.Scene, e: k.EventInfo) -> None: note = k.Text("done").place(above=box, gap=0.3) s.play(k.fade_in(note)) s.play(k.fade_out(note)) ``` #### `e.time · e.data · e.count · e.value(sig)` One firing of an event, received by `.on` handlers and returned by `s.wait_for`: `e.time` (instant), `e.data` (typed payload), `e.count` (n-th firing) and `e.value(sig)` (value of any signal at the instant of the event). See also: `event.on`, `s.wait_for`. ```python @k.scene def instant(s: k.Scene): x = k.signal(0.0) dot = k.Dot(r=0.2, x=x) s.add(dot) h = s.start(x.to(4), duration=2) e = s.wait_for(h.done) label = k.Text(f"x = {e.value(x):.0f} at t = {e.time:.0f} s").place(at="top", margin=0.8) s.play(k.write(label)) ``` ### Components #### `class Name(k.Component)` The only kind of reusable object: a subclass with props (`k.Prop[T]`, inputs), static fields (`name: T = default`), outs (`k.Out[T]`, outputs as signals) and events (`k.Event`), plus a `build()` that runs once and returns the visual. Parts stored on `self.*` are addressable from outside. Verbs use `enter()`/`exit()`/`indicate()` when the component defines them. See also: `k.Prop`, `k.Out`, `k.Event`, `k.clip`. ```python class Tag(k.Component): text: str = "kinemo" def build(self) -> k.Node: self.box = k.RoundedRect(w=3, h=1) self.label = k.Text(self.text).place(inside=self.box) return k.Group(self.box, self.label) @k.scene def component(s: k.Scene): tag = Tag(text="Hello").place(at="center") s.play(k.draw(tag)) s.play(tag.box.to(color=k.BLUE)) ``` #### `name: k.Prop[T] = default` Declares a reactive component prop: `power: k.Prop[float] = k.prop(0.0)`. Inside the component it is always a signal (`Signal[float]`), even if the author passed a constant; from outside, it accepts a value, signal or lambda and animates with `comp.to(power=...)`. The default via `k.prop` is what passes Pyright strict. See also: `k.Component`, `k.prop`, `k.field`. ```python class Gauge(k.Component): value: k.Prop[float] = k.prop(0.0) def build(self) -> k.Node: return k.Rect(w=0.6, h=self.value + 0.01, fill=k.GREEN, fill_opacity=1) @k.scene def gauge(s: k.Scene): m = Gauge(value=1.0).place(at="center") s.add(m) s.play(m.to(value=3)) ``` #### `name: k.Out[T]` Declares a continuous component output: `soc: k.Out[float]`. It must be assigned in `build()` (error K0602) and is read from outside as a read-only signal (`bat.soc`). See also: `k.Component`, `k.integrate`. ```python class Counter(k.Component): total: k.Out[float] def build(self) -> k.Node: self.total = k.integrate(0.5) return k.Text(lambda: f"{self.total():.1f}") @k.scene def output(s: k.Scene): c = Counter().place(at="center") bar = k.Rect(w=0.5, h=c.total + 0.01).place(below=c, gap=0.3) s.add(c, bar) s.wait(3) ``` #### `name: k.Event · name: k.Event[Payload]` Declares an event on a component or a `k.State`: `full: k.Event` or, with a typed payload, `bounce: k.Event[Impact]`. Fire it with `k.when(cond, self.full)` or `self.full.emit()`; react with `@obj.full.on` or `s.wait_for(obj.full)`. String-based events do not exist. See also: `event.on`, `k.when`, `s.wait_for`. ```python class Alarm(k.Component): rang: k.Event def build(self) -> k.Node: k.when(k.time >= 2, self.rang) return k.Circle(r=0.5, fill=k.RED, fill_opacity=1) @k.scene def event(s: k.Scene): alarm = Alarm().place(at="center") s.add(alarm) s.wait_for(alarm.rang, timeout=5) s.play(k.flash(alarm)) ``` #### `k.field(default=None, *, range=None, choices=None)` Validated default for a static component field: `k.field(0.2, range=(0, 1))` or `k.field("a", choices=[...])`. Out-of-range values are an error at construction; static fields reject signals (K0601). See also: `k.prop`, `k.Component`. ```python class Ring(k.Component): thickness: float = k.field(0.2, range=(0.05, 1)) def build(self) -> k.Node: return k.Circle(r=1, stroke_width=self.thickness * 20) @k.scene def ring(s: k.Scene): r = Ring(thickness=0.4).place(at="center") s.play(k.draw(r)) ``` #### `k.prop(default=None, *, range=None)` Validated default for a reactive prop: `k.prop(0.0, range=(-5, 5))`. Constants are validated at construction; for reactive values, lint W0602 samples the timeline. See also: `k.Prop`, `k.field`. ```python class Needle(k.Component): angle: k.Prop[float] = k.prop(0.0, range=(0, 360)) def build(self) -> k.Node: return k.Line(start=(0, 0), end=(2, 0), rotate=self.angle) @k.scene def needle(s: k.Scene): p = Needle(angle=30) s.add(p) s.play(p.to(angle=150)) ``` #### `k.context(name, default=None) -> Context` Declares a context (at module level): a value that many components need (clock, scale, unit), supplied with `k.provide` instead of passed down prop by prop. The theme is a built-in context. See also: `k.provide`, `k.from_context`. ```python Scale = k.context("scale", default=1.0) class Point(k.Component): r: k.Prop[float] = k.from_context(Scale) def build(self) -> k.Node: return k.Dot(r=self.r * 0.2) @k.scene def context(s: k.Scene): with k.provide(Scale, 2.0): p = Point().place(at="center") s.play(k.fade_in(p)) ``` #### `with k.provide(ctx, value):` `with` block that supplies a context value to the components constructed inside it. It is resolved at construction (lexical scope of the build phase); passing the prop explicitly always wins over the context. See also: `k.context`, `k.from_context`. Example: same as `k.context`. #### `k.from_context(ctx)` Prop or field default that reads a context: `time: k.Prop[float] = k.from_context(Clock)`. Without `k.provide`, the context's `default=` applies. See also: `k.context`, `k.provide`. Example: same as `k.context`. ### Parameters #### `k.Int(lo, hi, default=None)` Integer scene parameter in the range `[lo, hi]`: `k.Int(3, 12, default=5)`. Parameters reach the scene function as signals: in video they take the `default` (or `--param name=value` on the CLI) and in interactive output they become controls. Reading a parameter with `.now` freezes it at build time (lint W1301). See also: `k.Float`, `k.scene`. ```python @k.scene(params={"n": k.Int(1, 10, default=3)}) def count(s: k.Scene, n: k.Signal[int]): label = k.Text(lambda: f"n = {n():.0f}", size=0.9).place(at="center") s.play(k.write(label)) ``` #### `k.Float(lo, hi, default=None)` Real-valued scene parameter in the range `[lo, hi]`: `k.Float(0.5, 2, default=1)`. Parameters reach the scene function as signals: in video they take the `default` (or `--param name=value` on the CLI) and in interactive output they become controls. Reading a parameter with `.now` freezes it at build time (lint W1301). See also: `k.Int`, `k.scene`. ```python @k.scene(params={"r": k.Float(0.5, 3, default=1.5)}) def radius(s: k.Scene, r: k.Signal[float]): c = k.Circle(r=r).place(at="center") s.play(k.draw(c)) ``` #### `k.Bool(default=False)` Boolean scene parameter: `k.Bool(default=True)`. Parameters reach the scene function as signals: in video they take the `default` (or `--param name=value` on the CLI) and in interactive output they become controls. Reading a parameter with `.now` freezes it at build time (lint W1301). See also: `k.Choice`, `k.scene`. ```python @k.scene(params={"grid": k.Bool(default=True)}) def with_grid(s: k.Scene, grid: k.Signal[bool]): ax = k.Axes(x=(0, 5, 1), y=(0, 3, 1)).place(at="center") tag = k.Text("grid on", visible=grid).place(above=ax, gap=0.3) s.play(k.draw(ax), k.fade_in(tag)) ``` #### `k.Choice(options, default=None)` Scene parameter with fixed options: `k.Choice([k.BLUE, k.RED])` (the default is the first option, or `default=`). Parameters reach the scene function as signals: in video they take the `default` (or `--param name=value` on the CLI) and in interactive output they become controls. Reading a parameter with `.now` freezes it at build time (lint W1301). See also: `k.Int`, `k.scene`. ```python @k.scene(params={"color": k.Choice([k.BLUE, k.RED], default=k.RED)}) def choice(s: k.Scene, color: k.Signal[k.Color]): sq = k.Square(2, fill=color, fill_opacity=0.6).place(at="center") s.play(k.draw(sq)) ``` #### `k.Str(default="")` Text scene parameter: `k.Str(default="...")`. (`k.Text` is the text object; one name per concept.) Parameters reach the scene function as signals: in video they take the `default` (or `--param name=value` on the CLI) and in interactive output they become controls. Reading a parameter with `.now` freezes it at build time (lint W1301). See also: `k.Text`, `k.scene`. ```python @k.scene(params={"heading": k.Str(default="Hello")}) def greeting(s: k.Scene, heading: k.Signal[str]): title = k.Text(heading, size=0.9).place(at="center") s.play(k.write(title)) ``` ### Output #### `k.movie(scenes, transitions=(), *, name="movie") -> Movie` Composes several scenes into a single output, with one transition per join (`k.cut` when omitted). `kinemo render` renders the movie as a single video. See also: `k.crossfade`, `k.cut`, `k.morph_cut`. ```python @k.scene def intro(s: k.Scene): s.play(k.write(k.Text("Chapter 1").place(at="center"))) @k.scene def body(s: k.Scene): s.play(k.draw(k.Circle(r=1.5).place(at="center"))) movie = k.movie([intro, body], transitions=[k.crossfade(0.5)]) ``` #### `k.cut.duration` Hard-cut transition between two scenes of a `k.movie` (the default). See also: `k.movie`, `k.crossfade`. ```python @k.scene def before(s: k.Scene): s.play(k.write(k.Text("Before").place(at="center"))) @k.scene def after(s: k.Scene): s.play(k.write(k.Text("After").place(at="center"))) movie = k.movie([before, after], transitions=[k.cut]) ``` #### `k.crossfade(duration=0.5) -> Transition` Transition in which the end of one scene dissolves into the start of the next, over `duration` seconds. See also: `k.movie`, `k.cut`, `k.morph_cut`. Example: same as `k.movie`. #### `k.morph_cut(duration=0.5) -> Transition` Transition in which objects with the same `key=` in neighboring scenes travel across the cut. See also: `k.movie`, `k.crossfade`. ```python @k.scene def one(s: k.Scene): s.play(k.draw(k.Circle(r=1, key="sun").place(at="left", margin=3))) @k.scene def two(s: k.Scene): s.play(k.draw(k.Circle(r=1, key="sun").place(at="right", margin=3))) movie = k.movie([one, two], transitions=[k.morph_cut(0.8)]) ``` ### Theme and colors #### `k.theme.bg | k.theme.fg | k.theme.accent | k.theme.muted | k.theme.secondary` Tokens of the current theme: `k.theme.bg`, `fg`, `accent`, `muted`, `secondary`. They are resolved when the scene is built, so switching the theme changes the whole scene without touching the code. `k.RED`, `k.BLUE`... are the fixed palette. See also: `k.themes`, `k.BLUE`. ```python @k.scene(theme=k.themes.blueprint) def tokens(s: k.Scene): box = k.RoundedRect(w=4, h=2, stroke=k.theme.accent).place(at="center") note = k.Text("blueprint theme", fill=k.theme.secondary).place(below=box, gap=0.3) s.play(k.draw(box), k.write(note)) ``` #### `k.themes.dark | k.themes.light | k.themes.blueprint` Built-in themes: `k.themes.dark` (default), `k.themes.light` and `k.themes.blueprint`. Choose one with `@k.scene(theme=...)` or in `kinemo.toml`; `theme.with_(accent=k.PINK)` creates a variation. See also: `k.theme`. ```python @k.scene(theme=k.themes.light.with_(accent=k.PINK)) def light(s: k.Scene): dot = k.Circle(r=1, fill=k.theme.accent, fill_opacity=1).place(at="center") s.play(k.grow(dot)) ``` #### `k.BLUE | k.RED | k.GREEN | k.YELLOW | k.ORANGE | k.PURPLE | k.PINK | k.TEAL | k.WHITE | k.BLACK | k.GRAY | k.TRANSPARENT` Fixed palette: `k.BLUE`, `k.RED`, `k.GREEN`, `k.YELLOW`, `k.ORANGE`, `k.PURPLE`, `k.PINK`, `k.TEAL`, `k.WHITE`, `k.BLACK`, `k.GRAY` and `k.TRANSPARENT`. Colors interpolate in OKLab. For theme-dependent colors use `k.theme.*`. See also: `k.rgb`, `k.theme`, `k.mix`. ```python @k.scene def palette(s: k.Scene): colors = [k.BLUE, k.RED, k.GREEN, k.YELLOW, k.PURPLE] dots = [k.Circle(r=0.5, fill=c, fill_opacity=1, stroke=c) for c in colors] row = k.Row(*dots, gap=0.4).place(at="center") s.play(k.stagger([k.grow(d) for d in dots], lag=0.1)) ``` #### `k.rgb(r, g, b, a=1.0) -> Color` Color from components from 0 to 1 (`a=` is the opacity). Outside the palette and the theme, this is the way to define a color. See also: `k.BLUE`, `k.mix`. ```python @k.scene def custom_color(s: k.Scene): amber = k.rgb(1.0, 0.55, 0.1) sq = k.Square(2, fill=amber, fill_opacity=1).place(at="center") s.play(k.draw(sq)) ``` ## Diagnostics Every problem has a stable code, a location, an instant and, when possible, a fix in code (`kinemo check --fix` applies it). `kinemo explain ` gives the long explanation. | Range | Area | | --- | --- | | K01xx | Object lifecycle | | K02xx / W02xx | Animations, conflicts, interpolation | | K03xx / W03xx | Reactive system | | K04xx | Layout and constraints | | K05xx | Resolution (loops, convergence) | | K06xx / W06xx | Components | | K07xx / W07xx | Events | | K08xx / W08xx | Text and math | | W09xx | Performance | | W10xx | Visual legibility | | K11xx | Names from other libraries (Manim) | | K12xx | Data and interoperability (Arrow, arrays) | | W13xx | Parameters and export | | W14xx | Audio and voice | Codes: - `K0001` Python exception during build - `K0101` object not in the scene · `K0102` object already removed · `K0103` object with two parents · `K0104` outside scene construction · `K0105` invalid argument · `K0106` unknown prop - `K0201` two animations on the same prop · `K0202` invalid duration · `K0203` not an animation · `K0204` non-reversible animation in during · `K0205` type without interpolation - `K0301` tracked read outside a reactive context · `K0302` x.now inside a lambda · `K0303` derived values are read-only · `K0304` signal used as a boolean · `K0305` signal converted to a number · `K0306` write inside a derived value · `K0310` untraceable function - `K0401` axis held by a constraint · `K0402` constraint cycle · `K0403` conflicting constraints · `K0404` unknown anchor - `K0501` event loop - `K0601` signal in a static field · `K0602` unassigned out - `K0702` event did not happen before the timeout · `K0703` event already happened - `K0801` unsupported LaTeX command - `K1101` Manim name · `K1102` Manim `.animate` · `K1103` Manim `self.play` · `K1104` Manim ValueTracker · `K1105` Manim updater · `K1106` Manim direction constant - `K1201` data without Arrow · `K1202` missing column · `K1203` column with the wrong type · `K1204` duplicate key - `W0110` play with at= - `W0310` lambda in a loop · `W0311` captured Python list · `W0312` eased clock - `W0701` handler object never removed - `W0801` morph without matches - `W0901` too many individual objects - `W1001` outside the safe area · `W1002` text over text · `W1003` low contrast · `W1004` small text · `W1005` invisible object · `W1006` visual noise · `W1007` static scene - `W1301` parameter read with .now · `W1302` k.python depending on a parameter - `W1401` voice without a TTS provider ## Manim → kinemo Manim names are recognized and answered with the equivalent form (K11xx). | Manim | kinemo | | --- | --- | | `class S(Scene): def construct(self)` | `@k.scene def s(s: k.Scene)` | | `self.play(Create(x))` | `s.play(k.draw(x))` | | `self.play(Write(t))` | `s.play(k.write(t))` | | `self.add(x)` / `self.remove(x)` | `s.add(x)` / `s.remove(x)` | | `FadeIn` / `FadeOut` | `k.fade_in` / `k.fade_out` | | `GrowFromCenter(x)` | `k.grow(x)` | | `ShrinkToCenter(x)` | `k.shrink(x)` | | `Indicate(x)` / `Flash(x)` | `k.indicate(x)` / `k.flash(x)` | | `MoveAlongPath(x, path)` | `k.follow(x, path)` | | `Transform(a, b)` / `ReplacementTransform` | `k.morph(a, b)` | | `TransformMatchingTex(a, b)` | `k.morph(a, b)` (matching by TeX is the default) | | `x.animate.shift(UP)` | `s.play(x.to(y=x.y.now + 1))` | | `x.animate.set_color(RED)` | `s.play(x.to(color=k.RED))` | | `x.next_to(y, DOWN)` | `x.place(below=y)` | | `x.to_edge(UP)` / `x.to_corner(UL)` | `x.place(at="top")` / `x.place(at="top-left")` | | `VGroup(a, b).arrange(RIGHT)` | `k.Row(a, b)` | | `VGroup(a, b)` | `k.Group(a, b)` | | `ValueTracker(0)` | `k.signal(0)` | | `x.add_updater(f)` | `x.set(prop=signal_or_lambda)` | | `always_redraw(lambda: ...)` | Reactive props via lambda | | `AnimationGroup(a, b, lag_ratio=r)` / `LaggedStart` | `k.stagger([a, b], lag=...)` | | `Succession(a, b)` | `k.seq(a, b)` | | `self.wait()` | `s.wait()` | | `Tex(r"...")` / `Text("...")` | `k.Text("...")` | | `MathTex(r"...")` | `k.Math(r"...")` | | `Code(...)` | `k.Code(src, lang="python")` | | `Axes(...).plot(f)` | `k.Axes(...).plot(f)` | | `NumberLine(...)` | `k.NumberLine(...)` |