codimate 0.1.4__tar.gz → 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codimate-0.1.4 → codimate-0.1.5}/Cargo.lock +1 -1
- {codimate-0.1.4 → codimate-0.1.5}/PKG-INFO +16 -7
- {codimate-0.1.4 → codimate-0.1.5}/README.md +9 -6
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-py/Cargo.toml +1 -1
- {codimate-0.1.4 → codimate-0.1.5}/pyproject.toml +11 -2
- {codimate-0.1.4 → codimate-0.1.5}/python/codimate/__init__.py +4 -2
- {codimate-0.1.4 → codimate-0.1.5}/python/codimate/layout.py +214 -0
- {codimate-0.1.4 → codimate-0.1.5}/python/codimate/trace.py +79 -9
- {codimate-0.1.4 → codimate-0.1.5}/Cargo.toml +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-animation/Cargo.toml +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-animation/src/lib.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-animation/tests/animation.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-animation/tests/parallel.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-animation/tests/playable.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-animation/tests/sequence.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-animation/tests/stagger.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/Cargo.toml +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/builder.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/easing.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/glyph.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/lib.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/manim_palette.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/path.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/scene/circle.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/scene/connection.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/scene/mod.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/scene/path.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/scene/primitive.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/scene/pulse.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/scene/rect.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/scene/text.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/scene/transform.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/src/value.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/tests/anchors.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/tests/animated.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/tests/circle.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/tests/connection.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/tests/easing.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/tests/manim_palette.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/tests/path.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/tests/ports.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/tests/pulse.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/tests/rect.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/tests/scene.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/tests/scene_effect_helpers.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-core/tests/tween.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-export/Cargo.toml +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-export/src/lib.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-export/tests/export.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-export/tests/png.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-fonts/Cargo.toml +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-fonts/build.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-fonts/fonts/DejaVuSansMono.ttf +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-fonts/fonts/DroidSansFallbackFull.ttf +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-fonts/fonts/NotoSansKhmer-Regular.ttf +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-fonts/src/lib.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-glyph/Cargo.toml +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-glyph/src/lib.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-layout/Cargo.toml +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-layout/src/lib.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-layout/tests/layout.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-math/Cargo.toml +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-math/src/lib.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-py/src/lib.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-reconcile/Cargo.toml +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-reconcile/src/lib.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-render/Cargo.toml +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-render/src/command.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-render/src/lib.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-render/src/raster.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-render/tests/raster.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/crates/codimate-render/tests/render.rs +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/python/codimate/explain.py +0 -0
- {codimate-0.1.4 → codimate-0.1.5}/python/codimate/scene.py +0 -0
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codimate
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
6
12
|
Classifier: Topic :: Multimedia :: Video
|
|
7
13
|
Classifier: Topic :: Education
|
|
8
14
|
Requires-Dist: imageio-ffmpeg>=0.4.9
|
|
@@ -15,6 +21,10 @@ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
|
15
21
|
[](https://pypi.org/project/codimate/)
|
|
16
22
|
[](https://pypi.org/project/codimate/)
|
|
17
23
|
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
*Two seconds of [`helical_solar_system/`](python/examples/helical_solar_system/)*
|
|
27
|
+
|
|
18
28
|
Turn a running algorithm into an explainer video.
|
|
19
29
|
|
|
20
30
|
You write your algorithm as normal Python and say what each step looks like.
|
|
@@ -23,14 +33,13 @@ Codimate works out the motion, the timing, and every frame.
|
|
|
23
33
|
```python
|
|
24
34
|
import codimate as cm
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
def bubble_sort(values):
|
|
36
|
+
def bubble_sort(values, emit):
|
|
28
37
|
for i in range(len(values)):
|
|
29
38
|
for j in range(len(values) - 1 - i):
|
|
30
|
-
|
|
39
|
+
emit("compare", items=[values[j], values[j + 1]])
|
|
31
40
|
if values[j] > values[j + 1]:
|
|
32
41
|
values[j], values[j + 1] = values[j + 1], values[j]
|
|
33
|
-
|
|
42
|
+
emit("swap", items=[values[j], values[j + 1]])
|
|
34
43
|
|
|
35
44
|
def bars(frame):
|
|
36
45
|
scene = cm.Scene()
|
|
@@ -45,7 +54,7 @@ def bars(frame):
|
|
|
45
54
|
return scene
|
|
46
55
|
|
|
47
56
|
cm.explain(
|
|
48
|
-
trace=bubble_sort
|
|
57
|
+
trace=cm.trace(bubble_sort, cm.items([3, 1, 4, 2])),
|
|
49
58
|
view=bars,
|
|
50
59
|
motion=[cm.Rule("*", position="lift_carry_drop", clearance=90)],
|
|
51
60
|
timing=cm.Timing(default=0.55, events={"swap": 0.9}),
|
|
@@ -335,7 +344,7 @@ with `python docs/build_site.py`.
|
|
|
335
344
|
authored, and the one decision you have to make.
|
|
336
345
|
4. [Reference](docs/reference.md) — every call and parameter, on one page.
|
|
337
346
|
|
|
338
|
-
Then [`python/examples/`](python/examples/),
|
|
347
|
+
Then [`python/examples/`](python/examples/), eleven worked examples with notes, and
|
|
339
348
|
[the decisions](docs/adr/) behind the design.
|
|
340
349
|
|
|
341
350
|
```bash
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
[](https://pypi.org/project/codimate/)
|
|
4
4
|
[](https://pypi.org/project/codimate/)
|
|
5
5
|
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
*Two seconds of [`helical_solar_system/`](python/examples/helical_solar_system/)*
|
|
9
|
+
|
|
6
10
|
Turn a running algorithm into an explainer video.
|
|
7
11
|
|
|
8
12
|
You write your algorithm as normal Python and say what each step looks like.
|
|
@@ -11,14 +15,13 @@ Codimate works out the motion, the timing, and every frame.
|
|
|
11
15
|
```python
|
|
12
16
|
import codimate as cm
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
def bubble_sort(values):
|
|
18
|
+
def bubble_sort(values, emit):
|
|
16
19
|
for i in range(len(values)):
|
|
17
20
|
for j in range(len(values) - 1 - i):
|
|
18
|
-
|
|
21
|
+
emit("compare", items=[values[j], values[j + 1]])
|
|
19
22
|
if values[j] > values[j + 1]:
|
|
20
23
|
values[j], values[j + 1] = values[j + 1], values[j]
|
|
21
|
-
|
|
24
|
+
emit("swap", items=[values[j], values[j + 1]])
|
|
22
25
|
|
|
23
26
|
def bars(frame):
|
|
24
27
|
scene = cm.Scene()
|
|
@@ -33,7 +36,7 @@ def bars(frame):
|
|
|
33
36
|
return scene
|
|
34
37
|
|
|
35
38
|
cm.explain(
|
|
36
|
-
trace=bubble_sort
|
|
39
|
+
trace=cm.trace(bubble_sort, cm.items([3, 1, 4, 2])),
|
|
37
40
|
view=bars,
|
|
38
41
|
motion=[cm.Rule("*", position="lift_carry_drop", clearance=90)],
|
|
39
42
|
timing=cm.Timing(default=0.55, events={"swap": 0.9}),
|
|
@@ -323,7 +326,7 @@ with `python docs/build_site.py`.
|
|
|
323
326
|
authored, and the one decision you have to make.
|
|
324
327
|
4. [Reference](docs/reference.md) — every call and parameter, on one page.
|
|
325
328
|
|
|
326
|
-
Then [`python/examples/`](python/examples/),
|
|
329
|
+
Then [`python/examples/`](python/examples/), eleven worked examples with notes, and
|
|
327
330
|
[the decisions](docs/adr/) behind the design.
|
|
328
331
|
|
|
329
332
|
```bash
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codimate"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.5"
|
|
8
8
|
description = "Turn a running algorithm into an explainer video"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -13,9 +13,19 @@ requires-python = ">=3.9"
|
|
|
13
13
|
# own — so `pip install codimate` is enough on a bare machine. `typst`, which
|
|
14
14
|
# only `scene.formula` needs, stays a documented prerequisite.
|
|
15
15
|
dependencies = ["imageio-ffmpeg>=0.4.9"]
|
|
16
|
+
# The per-version entries are not decoration: `requires-python` above is what
|
|
17
|
+
# pip enforces, but PyPI's sidebar and the shields.io badge both read these
|
|
18
|
+
# instead, and without them the badge reads "missing". One abi3-py39 wheel
|
|
19
|
+
# serves every version listed.
|
|
16
20
|
classifiers = [
|
|
17
21
|
"Programming Language :: Rust",
|
|
18
22
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
19
29
|
"Topic :: Multimedia :: Video",
|
|
20
30
|
"Topic :: Education",
|
|
21
31
|
]
|
|
@@ -41,7 +51,6 @@ select = ["E", "W", "F", "PLR0913"]
|
|
|
41
51
|
ignore = ["E741"]
|
|
42
52
|
|
|
43
53
|
[tool.ruff.lint.per-file-ignores]
|
|
44
|
-
"python/examples/attention/weights.py" = ["E501"]
|
|
45
54
|
|
|
46
55
|
[tool.ruff.lint.pylint]
|
|
47
56
|
max-args = 5
|
|
@@ -48,8 +48,8 @@ encoding — happens in Rust (ADR 0008).
|
|
|
48
48
|
from __future__ import annotations
|
|
49
49
|
|
|
50
50
|
from .explain import Explanation, Rule, Timing, ease, explain
|
|
51
|
-
from .layout import (Place, Slot, at, canvas, column, height, measure,
|
|
52
|
-
ngon, row, star, width)
|
|
51
|
+
from .layout import (Axes, Place, Slot, at, axes, canvas, column, height, measure,
|
|
52
|
+
measure_math, ngon, row, star, width)
|
|
53
53
|
from .scene import Group, Handle, Scene
|
|
54
54
|
from .trace import Event, Frame, Item, Trace, emit, items, trace
|
|
55
55
|
|
|
@@ -77,6 +77,8 @@ __all__ = [
|
|
|
77
77
|
"at",
|
|
78
78
|
"row",
|
|
79
79
|
"column",
|
|
80
|
+
"axes",
|
|
81
|
+
"Axes",
|
|
80
82
|
"ngon",
|
|
81
83
|
"star",
|
|
82
84
|
# putting it together
|
|
@@ -306,3 +306,217 @@ def _resolve(given, half, names, default=_UNSET):
|
|
|
306
306
|
if low is not None:
|
|
307
307
|
return float(low) + half
|
|
308
308
|
return float(high) - half
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# --------------------------------------------------------------------------
|
|
312
|
+
# Axes: a coordinate map, not a drawing (ADR 0016)
|
|
313
|
+
# --------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _nice_step(span: float, about: int) -> float:
|
|
317
|
+
"""A round step near ``span / about``, from the 1-2-5 sequence.
|
|
318
|
+
|
|
319
|
+
Round steps are not a nicety. A tick is a named shape, so the set of them
|
|
320
|
+
is an identity that survives from one Scene to the next; a step that
|
|
321
|
+
drifted with every frame would rename every tick every frame, and a still
|
|
322
|
+
picture would flicker. 1-2-5 holds steady until the range really changes.
|
|
323
|
+
"""
|
|
324
|
+
import math
|
|
325
|
+
|
|
326
|
+
if span <= 0:
|
|
327
|
+
raise ValueError(f"a range needs width, got {span}")
|
|
328
|
+
raw = span / max(int(about), 1)
|
|
329
|
+
power = 10.0 ** math.floor(math.log10(raw))
|
|
330
|
+
for nice in (1.0, 2.0, 5.0):
|
|
331
|
+
if raw <= nice * power:
|
|
332
|
+
return nice * power
|
|
333
|
+
return 10.0 * power
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _places(step: float) -> int:
|
|
337
|
+
"""How many decimals a label needs so that two ticks never read alike."""
|
|
338
|
+
import math
|
|
339
|
+
|
|
340
|
+
return max(0, -math.floor(math.log10(step) + 1e-9))
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@dataclass(frozen=True)
|
|
344
|
+
class Axes:
|
|
345
|
+
"""A map from your numbers to pixels. Built by :func:`axes`.
|
|
346
|
+
|
|
347
|
+
It draws nothing you did not ask it to and owns none of your names — see
|
|
348
|
+
:meth:`at`. Its own shapes (frame, ticks, labels) are drawn by
|
|
349
|
+
:meth:`draw`, under names you choose the prefix of.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
x0: float
|
|
353
|
+
x1: float
|
|
354
|
+
y0: float
|
|
355
|
+
y1: float
|
|
356
|
+
left: float
|
|
357
|
+
top: float
|
|
358
|
+
w: float
|
|
359
|
+
h: float
|
|
360
|
+
ink: str = "#8b96a8"
|
|
361
|
+
label: float = 20.0
|
|
362
|
+
layer: int = 0
|
|
363
|
+
|
|
364
|
+
def looks(self, ink: str = None, label: float = None,
|
|
365
|
+
layer: int = None) -> "Axes":
|
|
366
|
+
"""Restyle the frame, ticks and labels. Returns a new Axes.
|
|
367
|
+
|
|
368
|
+
plot = cm.axes(x=(0, 10), y=(0, 5)).looks(ink="#334", label=16)
|
|
369
|
+
|
|
370
|
+
A separate call rather than more arguments on :meth:`draw`, which the
|
|
371
|
+
shapes made the same choice about: no call in this library takes more
|
|
372
|
+
than five things.
|
|
373
|
+
"""
|
|
374
|
+
import dataclasses
|
|
375
|
+
|
|
376
|
+
return dataclasses.replace(
|
|
377
|
+
self,
|
|
378
|
+
ink=self.ink if ink is None else str(ink),
|
|
379
|
+
label=self.label if label is None else float(label),
|
|
380
|
+
layer=self.layer if layer is None else int(layer))
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def right(self) -> float:
|
|
384
|
+
return self.left + self.w
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def bottom(self) -> float:
|
|
388
|
+
return self.top + self.h
|
|
389
|
+
|
|
390
|
+
def at(self, x: float, y: float) -> tuple[float, float]:
|
|
391
|
+
"""One data point as a pixel pair, for any shape's ``at=``.
|
|
392
|
+
|
|
393
|
+
scene.circle("dot", r=8, at=plot.at(2.0, 4.0))
|
|
394
|
+
|
|
395
|
+
Handing back pixels rather than drawing is the whole design: what you
|
|
396
|
+
do with them is an ordinary shape with a name of yours, so it tweens,
|
|
397
|
+
`focus` frames it, and a motion Rule can be aimed at it.
|
|
398
|
+
"""
|
|
399
|
+
across = (float(x) - self.x0) / (self.x1 - self.x0)
|
|
400
|
+
up = (float(y) - self.y0) / (self.y1 - self.y0)
|
|
401
|
+
return (self.left + across * self.w, self.bottom - up * self.h)
|
|
402
|
+
|
|
403
|
+
def line(self, f, steps: int = 200, over=None) -> list:
|
|
404
|
+
"""``steps + 1`` points along ``y = f(x)``, as pixels.
|
|
405
|
+
|
|
406
|
+
scene.curve("f", plot.line(lambda x: x * x), w=4).fill("red")
|
|
407
|
+
|
|
408
|
+
``over=(lo, hi)`` samples part of the range instead of all of it.
|
|
409
|
+
|
|
410
|
+
An open `curve` is *drawn* rather than filled, so its colour is
|
|
411
|
+
``fill()`` and its thickness is ``w=`` — ``fill("none", edge=...)``
|
|
412
|
+
draws nothing at all.
|
|
413
|
+
|
|
414
|
+
Every sample is returned, including any that fall outside the box.
|
|
415
|
+
Dropping them would be prettier and is wrong: two curves with different
|
|
416
|
+
point counts do not interpolate (ADR 0010), so a curve that shed a
|
|
417
|
+
point as it left the frame would stop animating. Keep the count and
|
|
418
|
+
clamp the range with ``over=`` if you need it inside.
|
|
419
|
+
"""
|
|
420
|
+
lo, hi = (self.x0, self.x1) if over is None else (float(over[0]),
|
|
421
|
+
float(over[1]))
|
|
422
|
+
steps = max(int(steps), 1)
|
|
423
|
+
return [self.at(x, f(x))
|
|
424
|
+
for x in (lo + (hi - lo) * i / steps for i in range(steps + 1))]
|
|
425
|
+
|
|
426
|
+
def ticks(self, axis: str = "x", about: int = 6) -> list:
|
|
427
|
+
"""``(n, value, label)`` per tick, where ``n`` is the step multiple.
|
|
428
|
+
|
|
429
|
+
``n`` is what a tick is named after, not the value. A float that drifts
|
|
430
|
+
by one part in a billion is a different name, and a renamed shape
|
|
431
|
+
leaves and re-enters — which is a fade, on a picture that did not move.
|
|
432
|
+
An integer count of steps cannot drift.
|
|
433
|
+
"""
|
|
434
|
+
import math
|
|
435
|
+
|
|
436
|
+
lo, hi = (self.x0, self.x1) if axis == "x" else (self.y0, self.y1)
|
|
437
|
+
step = _nice_step(hi - lo, about)
|
|
438
|
+
digits = _places(step)
|
|
439
|
+
first = math.ceil(lo / step - 1e-9)
|
|
440
|
+
last = math.floor(hi / step + 1e-9)
|
|
441
|
+
out = []
|
|
442
|
+
for n in range(int(first), int(last) + 1):
|
|
443
|
+
value = n * step
|
|
444
|
+
label = f"{value:.{digits}f}"
|
|
445
|
+
out.append((n, value, "0" if label.lstrip("-").strip("0.") == ""
|
|
446
|
+
else label))
|
|
447
|
+
return out
|
|
448
|
+
|
|
449
|
+
def draw(self, scene, name="plot", *, about: int = 6,
|
|
450
|
+
grid: bool = False) -> "Axes":
|
|
451
|
+
"""Draw the frame, ticks and labels. Returns the Axes, for chaining.
|
|
452
|
+
|
|
453
|
+
plot = cm.axes(x=(-4, 4), y=(-2, 6)).draw(scene)
|
|
454
|
+
|
|
455
|
+
Every shape is named ``(name, ...)``, so two plots on one canvas do not
|
|
456
|
+
collide and you can restyle or omit any of them by drawing your own.
|
|
457
|
+
Colours and sizes come from :meth:`looks`.
|
|
458
|
+
"""
|
|
459
|
+
ink, layer = self.ink, self.layer
|
|
460
|
+
zero_y = self.y0 <= 0.0 <= self.y1
|
|
461
|
+
zero_x = self.x0 <= 0.0 <= self.x1
|
|
462
|
+
base = self.at(self.x0, 0.0)[1] if zero_y else self.bottom
|
|
463
|
+
spine = self.at(0.0, self.y0)[0] if zero_x else self.left
|
|
464
|
+
|
|
465
|
+
scene.line((name, "x-axis"), start=(self.left, base),
|
|
466
|
+
end=(self.right, base), w=1.6).fill(ink).on(layer=layer)
|
|
467
|
+
scene.line((name, "y-axis"), start=(spine, self.top),
|
|
468
|
+
end=(spine, self.bottom), w=1.6).fill(ink).on(layer=layer)
|
|
469
|
+
|
|
470
|
+
for axis, along in (("x", True), ("y", False)):
|
|
471
|
+
for n, value, words in self.ticks(axis, about):
|
|
472
|
+
if n == 0 and (zero_x if along else zero_y):
|
|
473
|
+
continue # the other axis already draws through it
|
|
474
|
+
x, y = (self.at(value, 0.0)[0], base) if along else \
|
|
475
|
+
(spine, self.at(0.0, value)[1])
|
|
476
|
+
if grid:
|
|
477
|
+
ends = ((x, self.top), (x, self.bottom)) if along else \
|
|
478
|
+
((self.left, y), (self.right, y))
|
|
479
|
+
scene.line((name, "grid", axis, n), start=ends[0],
|
|
480
|
+
end=ends[1], w=1.0).fill(ink) \
|
|
481
|
+
.on(layer=layer - 1, opacity=0.18)
|
|
482
|
+
mark = ((x, y - 5), (x, y + 5)) if along else \
|
|
483
|
+
((x - 5, y), (x + 5, y))
|
|
484
|
+
scene.line((name, "tick", axis, n), start=mark[0], end=mark[1],
|
|
485
|
+
w=1.6).fill(ink).on(layer=layer)
|
|
486
|
+
where = at(x=x, top=y + 10) if along else at(x=x - 18, y=y)
|
|
487
|
+
scene.text((name, "label", axis, n), words, size=self.label,
|
|
488
|
+
at=where).fill(ink).on(layer=layer)
|
|
489
|
+
return self
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def axes(*, x, y, at=None, size=None, within: "Slot | None" = None) -> Axes:
|
|
493
|
+
"""A coordinate map from your numbers to pixels.
|
|
494
|
+
|
|
495
|
+
plot = cm.axes(x=(-4, 4), y=(-2, 6), size=(760, 420)).draw(scene)
|
|
496
|
+
scene.curve("f", plot.line(lambda t: t * t), w=4).fill("orange")
|
|
497
|
+
scene.circle("dot", r=8, at=plot.at(t, t * t))
|
|
498
|
+
|
|
499
|
+
``x`` and ``y`` are ``(low, high)``. ``size`` is ``(w, h)`` in pixels, or
|
|
500
|
+
one number for a square; it defaults to most of the canvas. ``at`` places
|
|
501
|
+
the box's centre, and ``within=slot`` fits it to part of the canvas.
|
|
502
|
+
|
|
503
|
+
It returns points rather than drawing, so what you plot is a shape with a
|
|
504
|
+
name of yours — which is what lets it tween, be framed by `focus`, and be
|
|
505
|
+
aimed at by a motion Rule. See ADR 0016.
|
|
506
|
+
"""
|
|
507
|
+
box = _within(within)
|
|
508
|
+
w, h = _size(size)
|
|
509
|
+
if w is None:
|
|
510
|
+
w = box.w * 0.72
|
|
511
|
+
if h is None:
|
|
512
|
+
h = box.h * 0.66
|
|
513
|
+
place = at or Place()
|
|
514
|
+
cx = box.x if place.x is None else place.x
|
|
515
|
+
cy = _resolve((place.y, place.top, place.bottom), h / 2.0,
|
|
516
|
+
("y", "top", "bottom"), box.y)
|
|
517
|
+
x0, x1 = (float(v) for v in x)
|
|
518
|
+
y0, y1 = (float(v) for v in y)
|
|
519
|
+
if x0 == x1 or y0 == y1:
|
|
520
|
+
raise ValueError(f"a range needs width, got x={x!r} y={y!r}")
|
|
521
|
+
return Axes(x0=x0, x1=x1, y0=y0, y1=y1,
|
|
522
|
+
left=cx - w / 2.0, top=cy - h / 2.0, w=float(w), h=float(h))
|
|
@@ -99,15 +99,75 @@ def emit(name: str, **data: Any) -> None:
|
|
|
99
99
|
rec.events.append(Event(name=name, state=rec.snapshot(), data=data))
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
"""Turn a normal, state-mutating function into a Trace.
|
|
102
|
+
_MISSING = object()
|
|
104
103
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
|
|
105
|
+
def _emitter(rec: "_Recorder") -> Callable[..., None]:
|
|
106
|
+
"""The `emit` handed to an algorithm: record that something happened."""
|
|
107
|
+
|
|
108
|
+
def emit(name: str, **data: Any) -> None:
|
|
109
|
+
rec.events.append(Event(name=name, state=rec.snapshot(), data=data))
|
|
110
|
+
|
|
111
|
+
emit.__doc__ = globals()["emit"].__doc__
|
|
112
|
+
return emit
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def trace(fn=_MISSING, state=_MISSING, /, *, snapshot=None) -> "Trace":
|
|
116
|
+
"""Run `fn` over `state` and record what happened.
|
|
117
|
+
|
|
118
|
+
def bubble_sort(values, emit):
|
|
119
|
+
...
|
|
120
|
+
emit("compare", items=[a, b])
|
|
121
|
+
|
|
122
|
+
cm.explain(trace=cm.trace(bubble_sort, cm.items([3, 1, 4, 2])), ...)
|
|
123
|
+
|
|
124
|
+
Your function is handed the state and an `emit`. Call `emit` *after*
|
|
125
|
+
changing the state — Codimate snapshots the result for you.
|
|
126
|
+
|
|
127
|
+
`emit` is an argument rather than something ambient, so the answer to
|
|
128
|
+
"where did that come from?" is the line above it, and so the function
|
|
129
|
+
stays ordinary Python: `bubble_sort(values, print)` runs it and prints the
|
|
130
|
+
events.
|
|
131
|
+
|
|
132
|
+
`snapshot(state)` returns the part worth showing, if a deep copy of the
|
|
133
|
+
whole state is not what you want. Items keep their identity through the
|
|
134
|
+
default copy.
|
|
135
|
+
|
|
136
|
+
The older form — `@cm.trace()` on a function that calls the module-level
|
|
137
|
+
`cm.emit` — still works and warns. It goes in 0.2.
|
|
108
138
|
"""
|
|
139
|
+
if fn is _MISSING or state is _MISSING:
|
|
140
|
+
return _decorator(snapshot) if fn is _MISSING else _decorator(snapshot)(fn)
|
|
141
|
+
|
|
142
|
+
def capture():
|
|
143
|
+
return snapshot(state) if snapshot is not None else copy.deepcopy(state)
|
|
144
|
+
|
|
145
|
+
rec = _Recorder(snapshot=capture, events=[])
|
|
146
|
+
initial = capture()
|
|
147
|
+
|
|
148
|
+
# The ContextVar is still set, so a helper that has not been moved over
|
|
149
|
+
# yet and still calls `cm.emit` keeps working during the transition.
|
|
150
|
+
token = _recorder.set(rec)
|
|
151
|
+
try:
|
|
152
|
+
fn(state, _emitter(rec))
|
|
153
|
+
finally:
|
|
154
|
+
_recorder.reset(token)
|
|
155
|
+
|
|
156
|
+
return Trace(initial=initial, events=tuple(rec.events))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _decorator(snapshot):
|
|
160
|
+
"""The older `@cm.trace()`, kept working until 0.2."""
|
|
161
|
+
import warnings
|
|
162
|
+
|
|
163
|
+
def decorate(fn):
|
|
164
|
+
warnings.warn(
|
|
165
|
+
"@cm.trace() and the ambient cm.emit are removed in 0.2. Take `emit` "
|
|
166
|
+
"as an argument and call cm.trace(fn, state) instead:\n"
|
|
167
|
+
f" def {fn.__name__}(state, emit): ...\n"
|
|
168
|
+
f" cm.explain(trace=cm.trace({fn.__name__}, state), ...)",
|
|
169
|
+
DeprecationWarning, stacklevel=3)
|
|
109
170
|
|
|
110
|
-
def decorator(fn):
|
|
111
171
|
def run(*args, **kwargs) -> Trace:
|
|
112
172
|
def capture():
|
|
113
173
|
if snapshot is not None:
|
|
@@ -116,20 +176,30 @@ def trace(*, snapshot: Callable[..., Any] = None):
|
|
|
116
176
|
|
|
117
177
|
rec = _Recorder(snapshot=capture, events=[])
|
|
118
178
|
initial = capture()
|
|
119
|
-
|
|
120
179
|
token = _recorder.set(rec)
|
|
121
180
|
try:
|
|
122
181
|
fn(*args, **kwargs)
|
|
123
182
|
finally:
|
|
124
183
|
_recorder.reset(token)
|
|
125
|
-
|
|
126
184
|
return Trace(initial=initial, events=tuple(rec.events))
|
|
127
185
|
|
|
128
186
|
run.__name__ = fn.__name__
|
|
129
187
|
run.__doc__ = fn.__doc__
|
|
130
188
|
return run
|
|
131
189
|
|
|
132
|
-
return
|
|
190
|
+
return decorate
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def items(self, key: str = "items") -> list:
|
|
194
|
+
"""The things this event named, e.g. ``frame.items()``.
|
|
195
|
+
|
|
196
|
+
Use it when you emitted a list: ``cm.emit("compare", items=[a, b])``.
|
|
197
|
+
Empty for the opening moment.
|
|
198
|
+
"""
|
|
199
|
+
if self.event is None:
|
|
200
|
+
return []
|
|
201
|
+
return list(self.event.data.get(key, ()))
|
|
202
|
+
|
|
133
203
|
|
|
134
204
|
@dataclass(frozen=True)
|
|
135
205
|
class Frame:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|