cardkit-anim 0.1.0__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.
- cardkit_anim-0.1.0/LICENSE +21 -0
- cardkit_anim-0.1.0/PKG-INFO +332 -0
- cardkit_anim-0.1.0/README.md +307 -0
- cardkit_anim-0.1.0/pyproject.toml +32 -0
- cardkit_anim-0.1.0/setup.cfg +4 -0
- cardkit_anim-0.1.0/src/cardkit/__init__.py +72 -0
- cardkit_anim-0.1.0/src/cardkit/context.py +39 -0
- cardkit_anim-0.1.0/src/cardkit/export.py +67 -0
- cardkit_anim-0.1.0/src/cardkit/primitives.py +72 -0
- cardkit_anim-0.1.0/src/cardkit/render.py +107 -0
- cardkit_anim-0.1.0/src/cardkit/scene.py +263 -0
- cardkit_anim-0.1.0/src/cardkit/shapes.py +67 -0
- cardkit_anim-0.1.0/src/cardkit/svg.py +212 -0
- cardkit_anim-0.1.0/src/cardkit/text.py +50 -0
- cardkit_anim-0.1.0/src/cardkit/three.py +146 -0
- cardkit_anim-0.1.0/src/cardkit/timing.py +169 -0
- cardkit_anim-0.1.0/src/cardkit_anim.egg-info/PKG-INFO +332 -0
- cardkit_anim-0.1.0/src/cardkit_anim.egg-info/SOURCES.txt +20 -0
- cardkit_anim-0.1.0/src/cardkit_anim.egg-info/dependency_links.txt +1 -0
- cardkit_anim-0.1.0/src/cardkit_anim.egg-info/requires.txt +9 -0
- cardkit_anim-0.1.0/src/cardkit_anim.egg-info/top_level.txt +1 -0
- cardkit_anim-0.1.0/tests/test_cardkit.py +182 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Carlo Alberto Barbano
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cardkit-anim
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Write an animated scene once in Python and export it as GIF, WebP, MP4 and a looping, script-free animated SVG that look identical.
|
|
5
|
+
Author: Carlo Alberto Barbano
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/carloalbertobarbano/cardkit
|
|
8
|
+
Project-URL: Source, https://github.com/carloalbertobarbano/cardkit
|
|
9
|
+
Project-URL: Issues, https://github.com/carloalbertobarbano/cardkit/issues
|
|
10
|
+
Keywords: animation,svg,smil,matplotlib,gif,graphical abstract,scientific figures
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: numpy
|
|
18
|
+
Requires-Dist: matplotlib>=3.5
|
|
19
|
+
Requires-Dist: pillow>=9.1
|
|
20
|
+
Provides-Extra: video
|
|
21
|
+
Requires-Dist: imageio-ffmpeg; extra == "video"
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest; extra == "test"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# cardkit
|
|
27
|
+
|
|
28
|
+
**Write an animation once in Python. Get a GIF, a WebP, an MP4 and an animated SVG, and they all look the same.**
|
|
29
|
+
|
|
30
|
+
cardkit is a small scene graph built on matplotlib. You describe a card as a list of shapes, lines, text and images.
|
|
31
|
+
Any property of any element can be a function of time. cardkit then renders the scene in two ways:
|
|
32
|
+
|
|
33
|
+
* **raster**: matplotlib draws each frame, which becomes a GIF, an animated WebP, an MP4 or a PNG poster;
|
|
34
|
+
* **vector**: a native SVG whose animation uses SMIL `<animate>` tags, built by sampling the same functions.
|
|
35
|
+
It loops forever, needs no JavaScript and no fonts, and plays inside a plain `<img>` tag (GitHub READMEs, websites, slides).
|
|
36
|
+
|
|
37
|
+
All text is converted to outlines, mathtext included (`"$\hat{y} = \sum_i w_i \varphi_i(x)$"`), so the SVG matches the
|
|
38
|
+
frames pixel for pixel on any machine.
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<img src="https://raw.githubusercontent.com/carloalbertobarbano/cardkit/main/docs/tour_animated.svg" width="48%" alt="cardkit tour (animated SVG)">
|
|
42
|
+
<img src="https://raw.githubusercontent.com/carloalbertobarbano/cardkit/main/docs/tour.gif" width="48%" alt="cardkit tour (GIF)">
|
|
43
|
+
</p>
|
|
44
|
+
<p align="center"><sub>The same scene (<a href="https://github.com/carloalbertobarbano/cardkit/blob/main/examples/tour.py">examples/tour.py</a>): animated SVG on the left (101 kB), GIF on the right.</sub></p>
|
|
45
|
+
|
|
46
|
+
It was built for scientific graphical abstracts, website cards and social-media videos, where you want real plots,
|
|
47
|
+
real numbers and typeset maths, plus an SVG that stays sharp.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install cardkit-anim # numpy, matplotlib and pillow are the only dependencies
|
|
55
|
+
pip install "cardkit-anim[video]" # + imageio-ffmpeg, for MP4 export if ffmpeg is not installed
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The package on PyPI is `cardkit-anim`; the module you import is `cardkit` (`import cardkit as ck`).
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import numpy as np
|
|
64
|
+
import cardkit as ck
|
|
65
|
+
from cardkit import Txt, Path, Poly, Circ, ramp
|
|
66
|
+
|
|
67
|
+
INK, ACCENT, MUTED = "#1d2433", "#0f9d8a", "#8a93a3"
|
|
68
|
+
|
|
69
|
+
scene = ck.Scene(duration=5, size=(9, 9), background="#ffffff")
|
|
70
|
+
|
|
71
|
+
# a title that fades in between t = 0.2 s and t = 0.8 s
|
|
72
|
+
scene.add(Txt("Hello, cardkit", (0.6, 7.9), size=26, color=INK, weight="bold", op=ramp(0.2, 0.8)))
|
|
73
|
+
|
|
74
|
+
# a sine wave that draws itself from left to right
|
|
75
|
+
x = np.linspace(0.8, 8.2, 120)
|
|
76
|
+
scene.add(Path(np.c_[x, 5.2 + 0.7 * np.sin(2.2 * x)], color=ACCENT, lw=3, draw=ramp(1.0, 2.6)))
|
|
77
|
+
|
|
78
|
+
# bars that grow one after the other: each shape morphs from a flat to a full rectangle
|
|
79
|
+
for k, h in enumerate([1.2, 2.0, 1.6, 2.6, 3.1]):
|
|
80
|
+
x0, t0 = 1.0 + 1.5 * k, 2.4 + 0.2 * k
|
|
81
|
+
flat, full = ck.rect(x0, 0.9, x0 + 1.0, 0.91), ck.rect(x0, 0.9, x0 + 1.0, 0.9 + h)
|
|
82
|
+
scene.add(Poly(ck.tween(flat, full, t0, t0 + 0.8), fill=ACCENT, op=0.35 + 0.13 * k))
|
|
83
|
+
|
|
84
|
+
# a dot that pulses forever
|
|
85
|
+
scene.add(Circ((8.2, 7.75), r=ck.pulse(1.2, 0.10, 0.18), fill=ACCENT))
|
|
86
|
+
|
|
87
|
+
scene.save_all("out/quickstart", "gif", "svg", poster=4.5)
|
|
88
|
+
# -> out/quickstart.gif, out/quickstart_animated.svg, out/quickstart_poster.png
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
<p align="center"><img src="https://raw.githubusercontent.com/carloalbertobarbano/cardkit/main/docs/quickstart_animated.svg" width="300" alt="quick start result"></p>
|
|
92
|
+
|
|
93
|
+
To preview while you work, run `scene.frame(2.0)`, which returns a PIL image (it shows inline in Jupyter), or
|
|
94
|
+
`scene.storyboard("board.png")` for a contact sheet of the whole animation:
|
|
95
|
+
|
|
96
|
+
<p align="center"><img src="https://raw.githubusercontent.com/carloalbertobarbano/cardkit/main/docs/tour_storyboard.png" width="640" alt="storyboard"></p>
|
|
97
|
+
|
|
98
|
+
## Gallery
|
|
99
|
+
|
|
100
|
+
<p align="center">
|
|
101
|
+
<img src="https://raw.githubusercontent.com/carloalbertobarbano/cardkit/main/docs/network_animated.svg" width="32%" alt="neural network example">
|
|
102
|
+
<img src="https://raw.githubusercontent.com/carloalbertobarbano/cardkit/main/docs/three_d_animated.svg" width="32%" alt="3-D example">
|
|
103
|
+
<img src="https://raw.githubusercontent.com/carloalbertobarbano/cardkit/main/docs/brain_animated.svg" width="32%" alt="nilearn example">
|
|
104
|
+
</p>
|
|
105
|
+
|
|
106
|
+
| example | shows |
|
|
107
|
+
|---|---|
|
|
108
|
+
| [`network.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/network.py) | an MLP: forward-pass activation waves, then training, with about 80 edges whose width and colour follow their weights; softmax bars, a loss curve, counters |
|
|
109
|
+
| [`three_d.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/three_d.py) | `cardkit.three`: a connectivity matrix as a lit 3-D slab sending shaded tubes to recolouring label cubes, then a growing 3-D bar chart |
|
|
110
|
+
| [`brain.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/brain.py) | nilearn glass brains whose activation follows a BOLD curve as it draws, then a connectome with a node-strength bar chart |
|
|
111
|
+
|
|
112
|
+
Every image above is the animated SVG itself, with no GIF involved. Run `python docs/make_docs.py` to rebuild all the demos.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## The model in five ideas
|
|
117
|
+
|
|
118
|
+
### 1. Coordinates are slide units
|
|
119
|
+
|
|
120
|
+
A scene is `size=(width, height)` units, with **y pointing up** and `(0, 0)` at the bottom-left corner. The default card
|
|
121
|
+
is 9 × 9. Font sizes and line widths are in **points**, and 1 unit = 43.2 pt, so a 9-unit card is 388.8 pt
|
|
122
|
+
(5.4 in) wide. This keeps type the same size relative to the card at any output resolution. `width_px` (default
|
|
123
|
+
120 px per unit, so 1080 px for 9 units) only sets the resolution of the raster outputs.
|
|
124
|
+
|
|
125
|
+
For other aspect ratios, change `size` (and optionally `origin`, the slide coordinates of the bottom-left corner):
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
ck.Scene(25, size=(9, 11.25), origin=(0, -0.85)) # 4:5 portrait: the 9x9 card stays at 0..9, extra room below and above
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 2. Anything can be a function of time
|
|
132
|
+
|
|
133
|
+
Every attribute takes either a constant or a function `t -> value`: positions, point lists, colours, radii, opacity,
|
|
134
|
+
line widths, the fraction of a line that is drawn, even the text string.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
Circ(c=lambda t: (1 + t, 4.5), r=0.2, fill="#e17d38") # moves right at 1 unit / s
|
|
138
|
+
Txt(lambda t: f"{int(100 * t)} %", (4.5, 4.5), 30, ha="center") # a counter
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The helpers in [Timing helpers](#timing-helpers) cover the common cases without lambdas.
|
|
142
|
+
|
|
143
|
+
### 3. Five primitives
|
|
144
|
+
|
|
145
|
+
| primitive | what | main arguments |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `Poly(pts, fill=None, stroke=None, lw=0)` | closed polygon | `pts`: (N, 2) vertices |
|
|
148
|
+
| `Path(pts, color, lw=1.5, draw=1.0, dash=None, cap="round")` | open polyline | `draw`: fraction drawn, 0..1; `dash`: (on, off) in units |
|
|
149
|
+
| `Circ(c, r, fill=None, stroke=None, lw=0)` | circle | centre and radius in units |
|
|
150
|
+
| `Txt(s, xy, size=12, color="#000", ha="left", weight=, style=, family=)` | text as outlines | `xy` on the baseline; `ha`: left / center / right |
|
|
151
|
+
| `Img(arr, extent, interp="bilinear")` | raster image | `arr`: (H, W, 3 or 4) array; `extent`: (x0, x1, y0, y1) |
|
|
152
|
+
|
|
153
|
+
They all also take `op` (opacity, 0..1), `z` (drawing order: higher is on top) and `group` (see below). If you prefer
|
|
154
|
+
longer names, `ck.Polygon`, `ck.Line`, `ck.Circle`, `ck.Text` and `ck.Image` are the same classes.
|
|
155
|
+
|
|
156
|
+
### 4. Groups fade as a whole
|
|
157
|
+
|
|
158
|
+
Put the elements of one step of your story in a **group** with an opacity **envelope**. The group fades as a single
|
|
159
|
+
layer, so overlapping translucent shapes never show seams, and the SVG only samples a group while it is visible, which
|
|
160
|
+
keeps files small.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
with scene.group("step1", ck.fade(0.0, 6.4)): # visible from 0 to 6.4 s, fading in and out
|
|
164
|
+
scene.add(Txt("Step one", (0.5, 8), 22))
|
|
165
|
+
scene.add(Path(curve, "#0f9d8a", 3, draw=ramp(0.5, 2.0)))
|
|
166
|
+
|
|
167
|
+
with scene.group("step2", (6.4, 12.0)): # a (t0, t1) tuple is shorthand for fade(t0, t1)
|
|
168
|
+
...
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Elements added outside any group are in the base group, which is always visible.
|
|
172
|
+
|
|
173
|
+
### 5. Author in scene time, then add an intro and pauses
|
|
174
|
+
|
|
175
|
+
You time every element in **scene time**. The scene can be played back differently from how you wrote it:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
scene = ck.Scene(
|
|
179
|
+
duration=25, # the story runs from scene time 0 to 25
|
|
180
|
+
intro=3.0, # ...plus an intro authored at negative times, -3 .. 0
|
|
181
|
+
holds={6.05: 0.9, 12.0: 0.9}, # freeze at these scene times for 0.9 s (reading pauses)
|
|
182
|
+
)
|
|
183
|
+
scene.duration # 29.8: the actual loop length (display time)
|
|
184
|
+
scene.to_display(6.4) # scene time -> display time
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Adding an intro or a pause never forces you to re-time the rest. The display-time functions (`frame`, `storyboard`,
|
|
188
|
+
`check_text`, `save(..., at=)`) take display time, and `frame_at(tau)` and `poster=` take scene time.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Timing helpers
|
|
193
|
+
|
|
194
|
+
They all return `Fn` objects: functions of time that support arithmetic, such as `ramp(1, 2) * 0.5` or
|
|
195
|
+
`1 - ramp(3, 3.5)`, or `fade(0, 6) * pulse(2, 0.6, 1)`.
|
|
196
|
+
|
|
197
|
+
| helper | gives |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `ramp(t0, t1)` | 0 → 1 between t0 and t1, smoothstep-eased (`eased=False` for linear) |
|
|
200
|
+
| `fade(t0, t1=None, d=0.35)` | fades in after t0 and out before t1, over `d` s (or `(d_in, d_out)`) |
|
|
201
|
+
| `tween(a, b, t0, t1)` | interpolates numbers, arrays (point lists, so shapes morph) or colours |
|
|
202
|
+
| `keyframes({t: value, ...})` | piecewise interpolation through several keys (numbers, arrays, colours) |
|
|
203
|
+
| `along(pts, t0, t1)` | a position travelling along a polyline: `Circ(along(path, 1, 3), 0.1)` |
|
|
204
|
+
| `pulse(period, lo, hi)` | smooth periodic oscillation |
|
|
205
|
+
| `every(t0, period, n=None)` | integer step counter (0, 1, 2, ...), handy to cycle through examples |
|
|
206
|
+
| `fn(callable)` | wraps your own `t -> value` function to get the arithmetic |
|
|
207
|
+
| `seg(t, a, b)`, `ease(x)`, `lin(t, a, b)` | the raw scalar functions |
|
|
208
|
+
|
|
209
|
+
> **The lambda-in-a-loop trap.** `for k in range(5): Circ(..., op=lambda t: seg(t, k, k + 1))` gives every circle
|
|
210
|
+
> the *last* `k`. Use the helpers (`op=ramp(k, k + 1)`), which capture values correctly, or bind with `lambda t, k=k: ...`.
|
|
211
|
+
|
|
212
|
+
## Shapes and colours
|
|
213
|
+
|
|
214
|
+
`rect(x0, y0, x1, y1)`, `rounded_rect(x0, y0, x1, y1, r)`, `bezier(p0, c1, c2, p1, n)` (2-D or 3-D),
|
|
215
|
+
`arc(c, r, a0, a1)`, `ngon(c, r, k)` and `arrow_head(tip, direction)` return point arrays.
|
|
216
|
+
`mix(c1, c2, k)`, `shade(c, k)` and `lighten(c, k)` blend colours, and `colorize(c, fn)` applies one of them to a
|
|
217
|
+
colour that is itself a function of time. `text_width(s, size)` measures a label, for example to size a chip around it.
|
|
218
|
+
|
|
219
|
+
## Exporting
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
scene.save("card.svg") # animated SVG (SMIL)
|
|
223
|
+
scene.save("card_still.svg", at=12.0) # static SVG snapshot at display time 12 s
|
|
224
|
+
scene.save("poster.png", at=23.6) # one frame
|
|
225
|
+
scene.save("card.gif") # renders every frame, then encodes
|
|
226
|
+
scene.save_all("out/card", "gif", "webp", "mp4", "svg", poster=23.6) # renders the frames once for all formats
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
| format | notes |
|
|
230
|
+
|---|---|
|
|
231
|
+
| **SVG** | `save_svg(path, at=None, dt=0.1, simplify=True)`. Attributes are sampled every `dt` s; runs of identical samples are dropped (about half the size, same animation). Plays in `<img>`. |
|
|
232
|
+
| **GIF** | Pillow only, no ffmpeg. One shared 256-colour palette without dithering (crisp flat colours), `gif_fps=15`, `gif_width=720`. |
|
|
233
|
+
| **WebP** | Full resolution, `webp_quality=90`. |
|
|
234
|
+
| **MP4** | H.264 / yuv420p / faststart, `crf=16`: plays on LinkedIn, X, Slack and Keynote. Needs ffmpeg (`$FFMPEG`, `PATH`, or `pip install imageio-ffmpeg`). |
|
|
235
|
+
| **PNG** | `frame(t)` / `frame_at(tau)` return PIL images. |
|
|
236
|
+
|
|
237
|
+
Frames are rendered in parallel with all cores but one (`workers=1` renders serially). Use `fps=` (default 30) and
|
|
238
|
+
`width_px=` to change the frame rate and resolution.
|
|
239
|
+
|
|
240
|
+
## Quality checks
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
scene.check_text(scene.to_display(6.0))
|
|
244
|
+
# [check t=7.80] 14 texts, 1 issues
|
|
245
|
+
# OVERLAP 'FC matrix' <-> 'subjects per dataset'
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`check_text(t, margin=0.25)` lists visible texts that overlap each other or come closer than `margin` units to the edge.
|
|
249
|
+
It is cheap: run it at a few key instants after every layout change.
|
|
250
|
+
|
|
251
|
+
## 3-D
|
|
252
|
+
|
|
253
|
+
`cardkit.three` builds 3-D-looking objects from plain `Poly` faces, seen through a fixed oblique camera with Lambert
|
|
254
|
+
lighting. Because the camera never moves, hidden faces are removed and faces are ordered once, when the scene is built.
|
|
255
|
+
The objects can still grow, move and change colour over time, and the SVG stays fully vector.
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
from cardkit.three import Camera, box, slab, tube
|
|
259
|
+
|
|
260
|
+
cam = Camera(origin=(1.0, 2.0)) # world axes: X right, D into the screen, Z up
|
|
261
|
+
scene.add(box(cam, (0, 0, 0), (0.6, 0.6, ramp(1, 2) * 2.5), "#0f9d8a")) # a bar that grows to 2.5 units
|
|
262
|
+
scene.add(slab(cam, (0, 0, 0), matrix, 0.3, colors=lambda v: ck.mix("#fff", "#0f9d8a", v))) # a matrix lying flat
|
|
263
|
+
scene.add(tube(cam, ck.bezier(p0, c1, c2, p1), 0.04, "#6c5ce7", t0=1.0, dur=1.0)) # a shaded arrow that grows
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`Camera.project(P)` maps (N, 3) world points to the slide, `Camera.lit(color, normal)` shades a colour, and
|
|
267
|
+
`Camera.depth(P)` gives a value to use as `z` when separate objects overlap (closer objects get higher `z`).
|
|
268
|
+
|
|
269
|
+
## Using real figures
|
|
270
|
+
|
|
271
|
+
`Img` takes any array, so you can render a matplotlib or nilearn figure to an RGBA array once and place it in the card:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
fig = plt.figure(figsize=(6, 2.4), dpi=200)
|
|
275
|
+
plotting.plot_glass_brain(stat_img, display_mode="lzr", figure=fig, annotate=False)
|
|
276
|
+
fig.canvas.draw(); arr = np.asarray(fig.canvas.buffer_rgba()) / 255; plt.close(fig)
|
|
277
|
+
scene.add(Img(arr, (0.5, 8.5, 3.6, 6.8), op=ramp(0.3, 0.9)))
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
To animate *inside* a rendered figure, render it in two states and fade between them.
|
|
281
|
+
[`brain.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/brain.py) renders an empty glass brain and the same brain with the activation, then turns the
|
|
282
|
+
difference into an exact overlay whose opacity follows a signal. The outline never moves; only the activation fades.
|
|
283
|
+
Render both states from the same figure layout (for example with `display.add_overlay`), so that they line up.
|
|
284
|
+
|
|
285
|
+
Inside the SVG, images are embedded as palette PNGs (128 colours) to keep files small, so smooth gradients can show
|
|
286
|
+
some banding there. Mathtext follows matplotlib's `rcParams` (for example `plt.rcParams["mathtext.fontset"] = "cm"`),
|
|
287
|
+
and so does the default font family.
|
|
288
|
+
|
|
289
|
+
## Limitations (SVG only; the raster backend has none of these)
|
|
290
|
+
|
|
291
|
+
* `Txt.size` and `Img.arr` cannot be animated. Position, colour, opacity and the string itself can.
|
|
292
|
+
* A `Path` with `dash` cannot also animate `draw`: the SVG shows the dashes and cardkit warns you.
|
|
293
|
+
* Shapes whose number of vertices changes over time switch between shapes discretely instead of morphing.
|
|
294
|
+
* SVG time is sampled every `dt` (0.1 s by default): very fast motion may need a smaller `dt`.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Reference
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
Scene(duration, size=(9, 9), background="#ffffff", *, origin=(0, 0), width_px=None, fps=30,
|
|
302
|
+
intro=0.0, holds=None, hold=0.9, title="")
|
|
303
|
+
.add(*prims) add primitives or lists of primitives; returns them
|
|
304
|
+
.group(name, envelope=None) register a group; also a context manager
|
|
305
|
+
.duration loop length in display seconds
|
|
306
|
+
.to_display(tau) scene time -> display time
|
|
307
|
+
.frame(t, width_px=None, isolate=True) PIL image at display time t
|
|
308
|
+
.frame_at(tau) PIL image at scene time tau
|
|
309
|
+
.storyboard(path=None, times=None, n=8, cols=4, width=360)
|
|
310
|
+
.check_text(t, margin=0.25, min_alpha=0.2)
|
|
311
|
+
.save(path, at=None, ...) by extension: .svg .png .jpg .gif .webp .mp4
|
|
312
|
+
.save_svg(path, at=None, dt=0.1, simplify=True, width=None)
|
|
313
|
+
.save_all(stem, *formats, poster=None, fps=None, width_px=None, workers=None,
|
|
314
|
+
gif_fps=15, gif_width=720, webp_quality=90, crf=16)
|
|
315
|
+
.render_frames(folder, fps=None, width_px=None, workers=None)
|
|
316
|
+
Timeline(duration, intro=0.0, holds=None, hold=0.9) .warp(T) .to_display(tau) .display_duration
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Examples and tests
|
|
320
|
+
|
|
321
|
+
* [`examples/quickstart.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/quickstart.py): the quick start above.
|
|
322
|
+
* [`examples/tour.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/tour.py): intro, groups, holds, motion along a path, keyframed colour, a counter,
|
|
323
|
+
mathtext, an image, a progress indicator, the storyboard and the text check.
|
|
324
|
+
* [`examples/network.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/network.py), [`examples/three_d.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/three_d.py) and
|
|
325
|
+
[`examples/brain.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/brain.py): the gallery (the last one needs `pip install nilearn`).
|
|
326
|
+
* `python docs/make_docs.py` rebuilds every demo image in `docs/`.
|
|
327
|
+
* `python -m unittest discover tests`. The tests include a SMIL evaluator that checks the compressed SVG animates exactly
|
|
328
|
+
like the uncompressed one.
|
|
329
|
+
|
|
330
|
+
## License
|
|
331
|
+
|
|
332
|
+
MIT, see [LICENSE](https://github.com/carloalbertobarbano/cardkit/blob/main/LICENSE).
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# cardkit
|
|
2
|
+
|
|
3
|
+
**Write an animation once in Python. Get a GIF, a WebP, an MP4 and an animated SVG, and they all look the same.**
|
|
4
|
+
|
|
5
|
+
cardkit is a small scene graph built on matplotlib. You describe a card as a list of shapes, lines, text and images.
|
|
6
|
+
Any property of any element can be a function of time. cardkit then renders the scene in two ways:
|
|
7
|
+
|
|
8
|
+
* **raster**: matplotlib draws each frame, which becomes a GIF, an animated WebP, an MP4 or a PNG poster;
|
|
9
|
+
* **vector**: a native SVG whose animation uses SMIL `<animate>` tags, built by sampling the same functions.
|
|
10
|
+
It loops forever, needs no JavaScript and no fonts, and plays inside a plain `<img>` tag (GitHub READMEs, websites, slides).
|
|
11
|
+
|
|
12
|
+
All text is converted to outlines, mathtext included (`"$\hat{y} = \sum_i w_i \varphi_i(x)$"`), so the SVG matches the
|
|
13
|
+
frames pixel for pixel on any machine.
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<img src="https://raw.githubusercontent.com/carloalbertobarbano/cardkit/main/docs/tour_animated.svg" width="48%" alt="cardkit tour (animated SVG)">
|
|
17
|
+
<img src="https://raw.githubusercontent.com/carloalbertobarbano/cardkit/main/docs/tour.gif" width="48%" alt="cardkit tour (GIF)">
|
|
18
|
+
</p>
|
|
19
|
+
<p align="center"><sub>The same scene (<a href="https://github.com/carloalbertobarbano/cardkit/blob/main/examples/tour.py">examples/tour.py</a>): animated SVG on the left (101 kB), GIF on the right.</sub></p>
|
|
20
|
+
|
|
21
|
+
It was built for scientific graphical abstracts, website cards and social-media videos, where you want real plots,
|
|
22
|
+
real numbers and typeset maths, plus an SVG that stays sharp.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install cardkit-anim # numpy, matplotlib and pillow are the only dependencies
|
|
30
|
+
pip install "cardkit-anim[video]" # + imageio-ffmpeg, for MP4 export if ffmpeg is not installed
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The package on PyPI is `cardkit-anim`; the module you import is `cardkit` (`import cardkit as ck`).
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import numpy as np
|
|
39
|
+
import cardkit as ck
|
|
40
|
+
from cardkit import Txt, Path, Poly, Circ, ramp
|
|
41
|
+
|
|
42
|
+
INK, ACCENT, MUTED = "#1d2433", "#0f9d8a", "#8a93a3"
|
|
43
|
+
|
|
44
|
+
scene = ck.Scene(duration=5, size=(9, 9), background="#ffffff")
|
|
45
|
+
|
|
46
|
+
# a title that fades in between t = 0.2 s and t = 0.8 s
|
|
47
|
+
scene.add(Txt("Hello, cardkit", (0.6, 7.9), size=26, color=INK, weight="bold", op=ramp(0.2, 0.8)))
|
|
48
|
+
|
|
49
|
+
# a sine wave that draws itself from left to right
|
|
50
|
+
x = np.linspace(0.8, 8.2, 120)
|
|
51
|
+
scene.add(Path(np.c_[x, 5.2 + 0.7 * np.sin(2.2 * x)], color=ACCENT, lw=3, draw=ramp(1.0, 2.6)))
|
|
52
|
+
|
|
53
|
+
# bars that grow one after the other: each shape morphs from a flat to a full rectangle
|
|
54
|
+
for k, h in enumerate([1.2, 2.0, 1.6, 2.6, 3.1]):
|
|
55
|
+
x0, t0 = 1.0 + 1.5 * k, 2.4 + 0.2 * k
|
|
56
|
+
flat, full = ck.rect(x0, 0.9, x0 + 1.0, 0.91), ck.rect(x0, 0.9, x0 + 1.0, 0.9 + h)
|
|
57
|
+
scene.add(Poly(ck.tween(flat, full, t0, t0 + 0.8), fill=ACCENT, op=0.35 + 0.13 * k))
|
|
58
|
+
|
|
59
|
+
# a dot that pulses forever
|
|
60
|
+
scene.add(Circ((8.2, 7.75), r=ck.pulse(1.2, 0.10, 0.18), fill=ACCENT))
|
|
61
|
+
|
|
62
|
+
scene.save_all("out/quickstart", "gif", "svg", poster=4.5)
|
|
63
|
+
# -> out/quickstart.gif, out/quickstart_animated.svg, out/quickstart_poster.png
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
<p align="center"><img src="https://raw.githubusercontent.com/carloalbertobarbano/cardkit/main/docs/quickstart_animated.svg" width="300" alt="quick start result"></p>
|
|
67
|
+
|
|
68
|
+
To preview while you work, run `scene.frame(2.0)`, which returns a PIL image (it shows inline in Jupyter), or
|
|
69
|
+
`scene.storyboard("board.png")` for a contact sheet of the whole animation:
|
|
70
|
+
|
|
71
|
+
<p align="center"><img src="https://raw.githubusercontent.com/carloalbertobarbano/cardkit/main/docs/tour_storyboard.png" width="640" alt="storyboard"></p>
|
|
72
|
+
|
|
73
|
+
## Gallery
|
|
74
|
+
|
|
75
|
+
<p align="center">
|
|
76
|
+
<img src="https://raw.githubusercontent.com/carloalbertobarbano/cardkit/main/docs/network_animated.svg" width="32%" alt="neural network example">
|
|
77
|
+
<img src="https://raw.githubusercontent.com/carloalbertobarbano/cardkit/main/docs/three_d_animated.svg" width="32%" alt="3-D example">
|
|
78
|
+
<img src="https://raw.githubusercontent.com/carloalbertobarbano/cardkit/main/docs/brain_animated.svg" width="32%" alt="nilearn example">
|
|
79
|
+
</p>
|
|
80
|
+
|
|
81
|
+
| example | shows |
|
|
82
|
+
|---|---|
|
|
83
|
+
| [`network.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/network.py) | an MLP: forward-pass activation waves, then training, with about 80 edges whose width and colour follow their weights; softmax bars, a loss curve, counters |
|
|
84
|
+
| [`three_d.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/three_d.py) | `cardkit.three`: a connectivity matrix as a lit 3-D slab sending shaded tubes to recolouring label cubes, then a growing 3-D bar chart |
|
|
85
|
+
| [`brain.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/brain.py) | nilearn glass brains whose activation follows a BOLD curve as it draws, then a connectome with a node-strength bar chart |
|
|
86
|
+
|
|
87
|
+
Every image above is the animated SVG itself, with no GIF involved. Run `python docs/make_docs.py` to rebuild all the demos.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## The model in five ideas
|
|
92
|
+
|
|
93
|
+
### 1. Coordinates are slide units
|
|
94
|
+
|
|
95
|
+
A scene is `size=(width, height)` units, with **y pointing up** and `(0, 0)` at the bottom-left corner. The default card
|
|
96
|
+
is 9 × 9. Font sizes and line widths are in **points**, and 1 unit = 43.2 pt, so a 9-unit card is 388.8 pt
|
|
97
|
+
(5.4 in) wide. This keeps type the same size relative to the card at any output resolution. `width_px` (default
|
|
98
|
+
120 px per unit, so 1080 px for 9 units) only sets the resolution of the raster outputs.
|
|
99
|
+
|
|
100
|
+
For other aspect ratios, change `size` (and optionally `origin`, the slide coordinates of the bottom-left corner):
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
ck.Scene(25, size=(9, 11.25), origin=(0, -0.85)) # 4:5 portrait: the 9x9 card stays at 0..9, extra room below and above
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 2. Anything can be a function of time
|
|
107
|
+
|
|
108
|
+
Every attribute takes either a constant or a function `t -> value`: positions, point lists, colours, radii, opacity,
|
|
109
|
+
line widths, the fraction of a line that is drawn, even the text string.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
Circ(c=lambda t: (1 + t, 4.5), r=0.2, fill="#e17d38") # moves right at 1 unit / s
|
|
113
|
+
Txt(lambda t: f"{int(100 * t)} %", (4.5, 4.5), 30, ha="center") # a counter
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The helpers in [Timing helpers](#timing-helpers) cover the common cases without lambdas.
|
|
117
|
+
|
|
118
|
+
### 3. Five primitives
|
|
119
|
+
|
|
120
|
+
| primitive | what | main arguments |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `Poly(pts, fill=None, stroke=None, lw=0)` | closed polygon | `pts`: (N, 2) vertices |
|
|
123
|
+
| `Path(pts, color, lw=1.5, draw=1.0, dash=None, cap="round")` | open polyline | `draw`: fraction drawn, 0..1; `dash`: (on, off) in units |
|
|
124
|
+
| `Circ(c, r, fill=None, stroke=None, lw=0)` | circle | centre and radius in units |
|
|
125
|
+
| `Txt(s, xy, size=12, color="#000", ha="left", weight=, style=, family=)` | text as outlines | `xy` on the baseline; `ha`: left / center / right |
|
|
126
|
+
| `Img(arr, extent, interp="bilinear")` | raster image | `arr`: (H, W, 3 or 4) array; `extent`: (x0, x1, y0, y1) |
|
|
127
|
+
|
|
128
|
+
They all also take `op` (opacity, 0..1), `z` (drawing order: higher is on top) and `group` (see below). If you prefer
|
|
129
|
+
longer names, `ck.Polygon`, `ck.Line`, `ck.Circle`, `ck.Text` and `ck.Image` are the same classes.
|
|
130
|
+
|
|
131
|
+
### 4. Groups fade as a whole
|
|
132
|
+
|
|
133
|
+
Put the elements of one step of your story in a **group** with an opacity **envelope**. The group fades as a single
|
|
134
|
+
layer, so overlapping translucent shapes never show seams, and the SVG only samples a group while it is visible, which
|
|
135
|
+
keeps files small.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
with scene.group("step1", ck.fade(0.0, 6.4)): # visible from 0 to 6.4 s, fading in and out
|
|
139
|
+
scene.add(Txt("Step one", (0.5, 8), 22))
|
|
140
|
+
scene.add(Path(curve, "#0f9d8a", 3, draw=ramp(0.5, 2.0)))
|
|
141
|
+
|
|
142
|
+
with scene.group("step2", (6.4, 12.0)): # a (t0, t1) tuple is shorthand for fade(t0, t1)
|
|
143
|
+
...
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Elements added outside any group are in the base group, which is always visible.
|
|
147
|
+
|
|
148
|
+
### 5. Author in scene time, then add an intro and pauses
|
|
149
|
+
|
|
150
|
+
You time every element in **scene time**. The scene can be played back differently from how you wrote it:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
scene = ck.Scene(
|
|
154
|
+
duration=25, # the story runs from scene time 0 to 25
|
|
155
|
+
intro=3.0, # ...plus an intro authored at negative times, -3 .. 0
|
|
156
|
+
holds={6.05: 0.9, 12.0: 0.9}, # freeze at these scene times for 0.9 s (reading pauses)
|
|
157
|
+
)
|
|
158
|
+
scene.duration # 29.8: the actual loop length (display time)
|
|
159
|
+
scene.to_display(6.4) # scene time -> display time
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Adding an intro or a pause never forces you to re-time the rest. The display-time functions (`frame`, `storyboard`,
|
|
163
|
+
`check_text`, `save(..., at=)`) take display time, and `frame_at(tau)` and `poster=` take scene time.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Timing helpers
|
|
168
|
+
|
|
169
|
+
They all return `Fn` objects: functions of time that support arithmetic, such as `ramp(1, 2) * 0.5` or
|
|
170
|
+
`1 - ramp(3, 3.5)`, or `fade(0, 6) * pulse(2, 0.6, 1)`.
|
|
171
|
+
|
|
172
|
+
| helper | gives |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `ramp(t0, t1)` | 0 → 1 between t0 and t1, smoothstep-eased (`eased=False` for linear) |
|
|
175
|
+
| `fade(t0, t1=None, d=0.35)` | fades in after t0 and out before t1, over `d` s (or `(d_in, d_out)`) |
|
|
176
|
+
| `tween(a, b, t0, t1)` | interpolates numbers, arrays (point lists, so shapes morph) or colours |
|
|
177
|
+
| `keyframes({t: value, ...})` | piecewise interpolation through several keys (numbers, arrays, colours) |
|
|
178
|
+
| `along(pts, t0, t1)` | a position travelling along a polyline: `Circ(along(path, 1, 3), 0.1)` |
|
|
179
|
+
| `pulse(period, lo, hi)` | smooth periodic oscillation |
|
|
180
|
+
| `every(t0, period, n=None)` | integer step counter (0, 1, 2, ...), handy to cycle through examples |
|
|
181
|
+
| `fn(callable)` | wraps your own `t -> value` function to get the arithmetic |
|
|
182
|
+
| `seg(t, a, b)`, `ease(x)`, `lin(t, a, b)` | the raw scalar functions |
|
|
183
|
+
|
|
184
|
+
> **The lambda-in-a-loop trap.** `for k in range(5): Circ(..., op=lambda t: seg(t, k, k + 1))` gives every circle
|
|
185
|
+
> the *last* `k`. Use the helpers (`op=ramp(k, k + 1)`), which capture values correctly, or bind with `lambda t, k=k: ...`.
|
|
186
|
+
|
|
187
|
+
## Shapes and colours
|
|
188
|
+
|
|
189
|
+
`rect(x0, y0, x1, y1)`, `rounded_rect(x0, y0, x1, y1, r)`, `bezier(p0, c1, c2, p1, n)` (2-D or 3-D),
|
|
190
|
+
`arc(c, r, a0, a1)`, `ngon(c, r, k)` and `arrow_head(tip, direction)` return point arrays.
|
|
191
|
+
`mix(c1, c2, k)`, `shade(c, k)` and `lighten(c, k)` blend colours, and `colorize(c, fn)` applies one of them to a
|
|
192
|
+
colour that is itself a function of time. `text_width(s, size)` measures a label, for example to size a chip around it.
|
|
193
|
+
|
|
194
|
+
## Exporting
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
scene.save("card.svg") # animated SVG (SMIL)
|
|
198
|
+
scene.save("card_still.svg", at=12.0) # static SVG snapshot at display time 12 s
|
|
199
|
+
scene.save("poster.png", at=23.6) # one frame
|
|
200
|
+
scene.save("card.gif") # renders every frame, then encodes
|
|
201
|
+
scene.save_all("out/card", "gif", "webp", "mp4", "svg", poster=23.6) # renders the frames once for all formats
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
| format | notes |
|
|
205
|
+
|---|---|
|
|
206
|
+
| **SVG** | `save_svg(path, at=None, dt=0.1, simplify=True)`. Attributes are sampled every `dt` s; runs of identical samples are dropped (about half the size, same animation). Plays in `<img>`. |
|
|
207
|
+
| **GIF** | Pillow only, no ffmpeg. One shared 256-colour palette without dithering (crisp flat colours), `gif_fps=15`, `gif_width=720`. |
|
|
208
|
+
| **WebP** | Full resolution, `webp_quality=90`. |
|
|
209
|
+
| **MP4** | H.264 / yuv420p / faststart, `crf=16`: plays on LinkedIn, X, Slack and Keynote. Needs ffmpeg (`$FFMPEG`, `PATH`, or `pip install imageio-ffmpeg`). |
|
|
210
|
+
| **PNG** | `frame(t)` / `frame_at(tau)` return PIL images. |
|
|
211
|
+
|
|
212
|
+
Frames are rendered in parallel with all cores but one (`workers=1` renders serially). Use `fps=` (default 30) and
|
|
213
|
+
`width_px=` to change the frame rate and resolution.
|
|
214
|
+
|
|
215
|
+
## Quality checks
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
scene.check_text(scene.to_display(6.0))
|
|
219
|
+
# [check t=7.80] 14 texts, 1 issues
|
|
220
|
+
# OVERLAP 'FC matrix' <-> 'subjects per dataset'
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`check_text(t, margin=0.25)` lists visible texts that overlap each other or come closer than `margin` units to the edge.
|
|
224
|
+
It is cheap: run it at a few key instants after every layout change.
|
|
225
|
+
|
|
226
|
+
## 3-D
|
|
227
|
+
|
|
228
|
+
`cardkit.three` builds 3-D-looking objects from plain `Poly` faces, seen through a fixed oblique camera with Lambert
|
|
229
|
+
lighting. Because the camera never moves, hidden faces are removed and faces are ordered once, when the scene is built.
|
|
230
|
+
The objects can still grow, move and change colour over time, and the SVG stays fully vector.
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from cardkit.three import Camera, box, slab, tube
|
|
234
|
+
|
|
235
|
+
cam = Camera(origin=(1.0, 2.0)) # world axes: X right, D into the screen, Z up
|
|
236
|
+
scene.add(box(cam, (0, 0, 0), (0.6, 0.6, ramp(1, 2) * 2.5), "#0f9d8a")) # a bar that grows to 2.5 units
|
|
237
|
+
scene.add(slab(cam, (0, 0, 0), matrix, 0.3, colors=lambda v: ck.mix("#fff", "#0f9d8a", v))) # a matrix lying flat
|
|
238
|
+
scene.add(tube(cam, ck.bezier(p0, c1, c2, p1), 0.04, "#6c5ce7", t0=1.0, dur=1.0)) # a shaded arrow that grows
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`Camera.project(P)` maps (N, 3) world points to the slide, `Camera.lit(color, normal)` shades a colour, and
|
|
242
|
+
`Camera.depth(P)` gives a value to use as `z` when separate objects overlap (closer objects get higher `z`).
|
|
243
|
+
|
|
244
|
+
## Using real figures
|
|
245
|
+
|
|
246
|
+
`Img` takes any array, so you can render a matplotlib or nilearn figure to an RGBA array once and place it in the card:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
fig = plt.figure(figsize=(6, 2.4), dpi=200)
|
|
250
|
+
plotting.plot_glass_brain(stat_img, display_mode="lzr", figure=fig, annotate=False)
|
|
251
|
+
fig.canvas.draw(); arr = np.asarray(fig.canvas.buffer_rgba()) / 255; plt.close(fig)
|
|
252
|
+
scene.add(Img(arr, (0.5, 8.5, 3.6, 6.8), op=ramp(0.3, 0.9)))
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
To animate *inside* a rendered figure, render it in two states and fade between them.
|
|
256
|
+
[`brain.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/brain.py) renders an empty glass brain and the same brain with the activation, then turns the
|
|
257
|
+
difference into an exact overlay whose opacity follows a signal. The outline never moves; only the activation fades.
|
|
258
|
+
Render both states from the same figure layout (for example with `display.add_overlay`), so that they line up.
|
|
259
|
+
|
|
260
|
+
Inside the SVG, images are embedded as palette PNGs (128 colours) to keep files small, so smooth gradients can show
|
|
261
|
+
some banding there. Mathtext follows matplotlib's `rcParams` (for example `plt.rcParams["mathtext.fontset"] = "cm"`),
|
|
262
|
+
and so does the default font family.
|
|
263
|
+
|
|
264
|
+
## Limitations (SVG only; the raster backend has none of these)
|
|
265
|
+
|
|
266
|
+
* `Txt.size` and `Img.arr` cannot be animated. Position, colour, opacity and the string itself can.
|
|
267
|
+
* A `Path` with `dash` cannot also animate `draw`: the SVG shows the dashes and cardkit warns you.
|
|
268
|
+
* Shapes whose number of vertices changes over time switch between shapes discretely instead of morphing.
|
|
269
|
+
* SVG time is sampled every `dt` (0.1 s by default): very fast motion may need a smaller `dt`.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Reference
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
Scene(duration, size=(9, 9), background="#ffffff", *, origin=(0, 0), width_px=None, fps=30,
|
|
277
|
+
intro=0.0, holds=None, hold=0.9, title="")
|
|
278
|
+
.add(*prims) add primitives or lists of primitives; returns them
|
|
279
|
+
.group(name, envelope=None) register a group; also a context manager
|
|
280
|
+
.duration loop length in display seconds
|
|
281
|
+
.to_display(tau) scene time -> display time
|
|
282
|
+
.frame(t, width_px=None, isolate=True) PIL image at display time t
|
|
283
|
+
.frame_at(tau) PIL image at scene time tau
|
|
284
|
+
.storyboard(path=None, times=None, n=8, cols=4, width=360)
|
|
285
|
+
.check_text(t, margin=0.25, min_alpha=0.2)
|
|
286
|
+
.save(path, at=None, ...) by extension: .svg .png .jpg .gif .webp .mp4
|
|
287
|
+
.save_svg(path, at=None, dt=0.1, simplify=True, width=None)
|
|
288
|
+
.save_all(stem, *formats, poster=None, fps=None, width_px=None, workers=None,
|
|
289
|
+
gif_fps=15, gif_width=720, webp_quality=90, crf=16)
|
|
290
|
+
.render_frames(folder, fps=None, width_px=None, workers=None)
|
|
291
|
+
Timeline(duration, intro=0.0, holds=None, hold=0.9) .warp(T) .to_display(tau) .display_duration
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Examples and tests
|
|
295
|
+
|
|
296
|
+
* [`examples/quickstart.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/quickstart.py): the quick start above.
|
|
297
|
+
* [`examples/tour.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/tour.py): intro, groups, holds, motion along a path, keyframed colour, a counter,
|
|
298
|
+
mathtext, an image, a progress indicator, the storyboard and the text check.
|
|
299
|
+
* [`examples/network.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/network.py), [`examples/three_d.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/three_d.py) and
|
|
300
|
+
[`examples/brain.py`](https://github.com/carloalbertobarbano/cardkit/blob/main/examples/brain.py): the gallery (the last one needs `pip install nilearn`).
|
|
301
|
+
* `python docs/make_docs.py` rebuilds every demo image in `docs/`.
|
|
302
|
+
* `python -m unittest discover tests`. The tests include a SMIL evaluator that checks the compressed SVG animates exactly
|
|
303
|
+
like the uncompressed one.
|
|
304
|
+
|
|
305
|
+
## License
|
|
306
|
+
|
|
307
|
+
MIT, see [LICENSE](https://github.com/carloalbertobarbano/cardkit/blob/main/LICENSE).
|