merge(animations): Manim explainers (knob, XY pad, fan-out, feedback contrast) + index
This commit is contained in:
commit
d6226f3cf0
11 changed files with 756 additions and 0 deletions
BIN
assets/media/01-knob-mapping.mp4
Normal file
BIN
assets/media/01-knob-mapping.mp4
Normal file
Binary file not shown.
BIN
assets/media/02-xy-pad.mp4
Normal file
BIN
assets/media/02-xy-pad.mp4
Normal file
Binary file not shown.
BIN
assets/media/03-fanout.mp4
Normal file
BIN
assets/media/03-fanout.mp4
Normal file
Binary file not shown.
BIN
assets/media/04-feedback.mp4
Normal file
BIN
assets/media/04-feedback.mp4
Normal file
Binary file not shown.
59
assets/media/README.md
Normal file
59
assets/media/README.md
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Educational explainers (Manim)
|
||||||
|
|
||||||
|
3Blue1Brown-style animated explainers for Manifold's core ideas: how physical
|
||||||
|
controls become numbers, how one control fans out to many parameters, and how the
|
||||||
|
two feedback/training modes reshape a mapping. Rendered with Manim Community
|
||||||
|
Edition v0.19 (provided via `nix-shell -p manim`), 1920x1080 @ 60 fps.
|
||||||
|
|
||||||
|
British spelling throughout the on-screen copy (randomise, centre, behaviour).
|
||||||
|
|
||||||
|
## Index
|
||||||
|
|
||||||
|
| File | Duration | Shows | Interactive-demo candidate? | Where it slots in |
|
||||||
|
|------|----------|-------|------------------------------|-------------------|
|
||||||
|
| `01-knob-mapping.mp4` | ~21s | A knob rotates while a paired 0–1 number rises/falls in lockstep. The point: a control is just a readout of one number. | **Strong** — a live knob bound to a number is the simplest possible interactive widget. | Onboarding step 1 / the very first "what is a control" beat. Pairs with the `Slider`/`Knob` primitives. |
|
||||||
|
| `02-xy-pad.mp4` | ~20s | A 2-D pad projects to two independent 0–1 values A and B, each shown as a slider, as the dot moves. | **Strong** — a `JoyMap` driving two live readouts is already close to an in-app primitive demo. | Onboarding step 2. Directly mirrors the `JoyMap` primitive + two `Slider`s. |
|
||||||
|
| `03-fanout.mp4` | ~27s | The same 2-D control driving 3 → 4 → 5 outputs (N sliders one side, control the other), then the control type cycled across knob, fader, touchpad, 2-D joystick, 3-D joystick. | **Medium** — the N-slider fan-out is demoable live; the control-type carousel is better as video. | Onboarding step 3 — the "why a network in the middle" moment, just before the mode UI. Connects to the dimensionality story in the mode schemas. |
|
||||||
|
| `04-feedback.mp4` | ~53s | Training on the mapping surface (projected to 2-D) and how a verdict reshapes it. **Contrasts the two feedback modes:** geometric push-away (directed repulsion from the liked centroid) vs explore-and-place (randomise → audition → place → interpolate). Ends on a side-by-side and the recommendation. | **Medium/low** — concept-heavy; the explore-and-place loop could become a guided interactive walkthrough, but the contrast slide is best as video. | The learning-behaviour onboarding / the dock-panel `FEEDBACK_MODE` selector help. Coordinates with `docs/redesign/rl-feedback-design.md` (explore-and-place is the recommended default). |
|
||||||
|
|
||||||
|
`04-feedback.mp4` is `s4_feedback.py`'s three scenes (`GeometricDislike`,
|
||||||
|
`ExploreAndPlace`, `FeedbackContrast`) concatenated.
|
||||||
|
|
||||||
|
## Interactive-onboarding recommendation
|
||||||
|
|
||||||
|
Pieces 1 and 2 are the best candidates to become **interactive in-app onboarding
|
||||||
|
demos** rather than playback video — they map almost 1:1 onto existing playground
|
||||||
|
primitives (`Knob`/`Slider`, `JoyMap`). A first onboarding pass could embed live
|
||||||
|
versions of those two and keep 3 and 4 as explainer video, since 3's control
|
||||||
|
carousel and 4's mode contrast carry more narrative than a single widget can.
|
||||||
|
|
||||||
|
## Rebuilding
|
||||||
|
|
||||||
|
Sources live in `src/`. There is no system Manim; use Nix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd assets/media/src
|
||||||
|
nix-shell -p manim --run "manim -qh --disable_caching s1_knob_mapping.py KnobMapping"
|
||||||
|
nix-shell -p manim --run "manim -qh --disable_caching s2_xy_pad.py XYPad"
|
||||||
|
nix-shell -p manim --run "manim -qh --disable_caching s3_fanout.py Fanout"
|
||||||
|
nix-shell -p manim --run "manim -qh --disable_caching s4_feedback.py GeometricDislike ExploreAndPlace FeedbackContrast"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then stitch the feedback piece and copy the finals up one level:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
V=media/videos
|
||||||
|
printf "file '%s'\n" \
|
||||||
|
"$PWD/$V/s4_feedback/1080p60/GeometricDislike.mp4" \
|
||||||
|
"$PWD/$V/s4_feedback/1080p60/ExploreAndPlace.mp4" \
|
||||||
|
"$PWD/$V/s4_feedback/1080p60/FeedbackContrast.mp4" > concat_feedback.txt
|
||||||
|
ffmpeg -y -f concat -safe 0 -i concat_feedback.txt -c copy "$V/s4_feedback/1080p60/Feedback.mp4"
|
||||||
|
|
||||||
|
cp $V/s1_knob_mapping/1080p60/KnobMapping.mp4 ../01-knob-mapping.mp4
|
||||||
|
cp $V/s2_xy_pad/1080p60/XYPad.mp4 ../02-xy-pad.mp4
|
||||||
|
cp $V/s3_fanout/1080p60/Fanout.mp4 ../03-fanout.mp4
|
||||||
|
cp $V/s4_feedback/1080p60/Feedback.mp4 ../04-feedback.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
The `src/media/` render tree is gitignored (regenerable). Only the four committed
|
||||||
|
`.mp4`s and the sources are tracked. Use `-ql` for fast draft iteration.
|
||||||
4
assets/media/src/.gitignore
vendored
Normal file
4
assets/media/src/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Manim render work tree — regenerable, do not commit.
|
||||||
|
media/
|
||||||
|
concat_feedback.txt
|
||||||
|
__pycache__/
|
||||||
43
assets/media/src/plan.md
Normal file
43
assets/media/src/plan.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Manim Explainers — Plan (workstream G)
|
||||||
|
|
||||||
|
3Blue1Brown-style educational explainers for Manifold's core ideas: how physical
|
||||||
|
controls map to numbers, how a single control fans out to many parameters, and how
|
||||||
|
the two feedback/training modes reshape a mapping. British spelling in all on-screen copy.
|
||||||
|
|
||||||
|
## Shared visual language
|
||||||
|
|
||||||
|
- Palette: Classic 3B1B on `#1C1C1C`. Primary `#58C4DD` (control side), Green
|
||||||
|
`#83C167` (numbers/outputs), Yellow `#FFFF00` (accent / the moving value).
|
||||||
|
Dislike/push = warm red `#FF6B6B`. Anchors (placed sounds) = green dots.
|
||||||
|
- Monospace font throughout (`MONO`), per the skill (Pango kerning).
|
||||||
|
- Opacity layering: primary 1.0, context 0.4, structure (axes/grid) 0.15.
|
||||||
|
- Breathing room: `self.wait()` after every reveal.
|
||||||
|
|
||||||
|
## The five scripts
|
||||||
|
|
||||||
|
| # | File | Scene(s) | Teaches |
|
||||||
|
|---|------|----------|---------|
|
||||||
|
| 1 | `s1_knob_mapping.py` | `KnobMapping` | A knob is a 1:1 readout of one number in [0,1]. Rotation ⇄ value in lockstep. |
|
||||||
|
| 2 | `s2_xy_pad.py` | `XYPad` | A 2-D pad projects to two *independent* numbers A and B, shown as sliders. |
|
||||||
|
| 3 | `s3_fanout.py` | `Fanout` | The same 2-D control drives 3→4→5 outputs (N sliders), across control types. |
|
||||||
|
| 4 | `s4_feedback.py` | `GeometricDislike`, `ExploreAndPlace`, `FeedbackContrast` | The two feedback modes on a loss/mapping surface: geometric push-away vs explore-and-place (the recommended default). |
|
||||||
|
|
||||||
|
Each scene is independently renderable; scripts are stitched with ffmpeg per piece.
|
||||||
|
|
||||||
|
## Narrative arcs
|
||||||
|
|
||||||
|
1. **Knob = 1:1.** Misconception corrected: a control isn't "magic", it's a number.
|
||||||
|
Show the dial; bind a 0–1 readout; sweep both ways; they never disagree.
|
||||||
|
2. **XY → two numbers.** One gesture, two readings. Move the dot; A reads the
|
||||||
|
horizontal, B reads the vertical; they move independently. The pad is just two
|
||||||
|
knobs stacked at right angles.
|
||||||
|
3. **Fan-out.** The interesting bit: one control, many outputs. A small network
|
||||||
|
sits between the control and N sliders. Grow N (3→4→5) and cycle the control
|
||||||
|
type (knob, fader, touchpad, 2-D joystick, 3-D joystick) — the principle holds.
|
||||||
|
4. **Feedback.** The mapping is a landscape. Two ways to teach it:
|
||||||
|
- *Geometric dislike*: you have a liked region; a thumbs-down carves the current
|
||||||
|
point directionally **away** from the liked centroid (directed repulsion).
|
||||||
|
- *Explore and place* (DEFAULT): randomise the net → audition → "I like that" →
|
||||||
|
drop it in a corner → randomise again → place another → interpolate between
|
||||||
|
placed anchors. Positive-only; you never reason about "away from what".
|
||||||
|
End on a side-by-side contrast and the recommendation.
|
||||||
113
assets/media/src/s1_knob_mapping.py
Normal file
113
assets/media/src/s1_knob_mapping.py
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
"""Manifold explainer 1 — Knob = 1:1 mapping.
|
||||||
|
|
||||||
|
A knob rotates; a paired 0-1 number rises and falls in lockstep. The point: a
|
||||||
|
physical control is nothing more than a readout of one number.
|
||||||
|
|
||||||
|
Render:
|
||||||
|
manim -qh s1_knob_mapping.py KnobMapping
|
||||||
|
"""
|
||||||
|
|
||||||
|
from manim import *
|
||||||
|
|
||||||
|
BG = "#1C1C1C"
|
||||||
|
PRIMARY = "#58C4DD" # control side
|
||||||
|
NUMBER = "#83C167" # the number / value side
|
||||||
|
ACCENT = "#FFFF00" # the live value
|
||||||
|
DIM = 0.4
|
||||||
|
MONO = "Monospace"
|
||||||
|
|
||||||
|
|
||||||
|
def knob_group(value_tracker, radius=1.3):
|
||||||
|
"""A dial whose pointer angle tracks value in [0,1] over a 300 degree sweep."""
|
||||||
|
start_ang = -150 * DEGREES # bottom-left
|
||||||
|
sweep = 300 * DEGREES
|
||||||
|
|
||||||
|
face = Circle(radius=radius, color=PRIMARY, stroke_width=6)
|
||||||
|
face.set_fill(PRIMARY, opacity=0.06)
|
||||||
|
|
||||||
|
# the swept arc (the track the pointer travels along)
|
||||||
|
track = Arc(radius=radius + 0.18, start_angle=start_ang, angle=sweep,
|
||||||
|
color=PRIMARY, stroke_width=4).set_opacity(DIM)
|
||||||
|
|
||||||
|
def make_pointer():
|
||||||
|
ang = start_ang + value_tracker.get_value() * sweep
|
||||||
|
tip = np.array([np.cos(ang), np.sin(ang), 0]) * (radius - 0.18)
|
||||||
|
line = Line(ORIGIN, tip, color=ACCENT, stroke_width=8)
|
||||||
|
return line
|
||||||
|
|
||||||
|
pointer = always_redraw(make_pointer)
|
||||||
|
hub = Dot(ORIGIN, radius=0.08, color=ACCENT)
|
||||||
|
return VGroup(face, track), pointer, hub
|
||||||
|
|
||||||
|
|
||||||
|
class KnobMapping(Scene):
|
||||||
|
def construct(self):
|
||||||
|
self.camera.background_color = BG
|
||||||
|
|
||||||
|
title = Text("A knob is just a number", font_size=44, color=PRIMARY,
|
||||||
|
weight=BOLD, font=MONO)
|
||||||
|
self.play(Write(title), run_time=1.5)
|
||||||
|
self.wait(1.0)
|
||||||
|
self.play(title.animate.scale(0.62).to_edge(UP, buff=0.5), run_time=1.0)
|
||||||
|
self.wait(0.3)
|
||||||
|
|
||||||
|
val = ValueTracker(0.5)
|
||||||
|
|
||||||
|
dial, pointer, hub = knob_group(val)
|
||||||
|
dial.shift(LEFT * 3.2)
|
||||||
|
pointer.shift(LEFT * 3.2)
|
||||||
|
hub.shift(LEFT * 3.2)
|
||||||
|
|
||||||
|
knob_label = Text("control", font_size=24, color=PRIMARY, font=MONO)
|
||||||
|
knob_label.next_to(dial, DOWN, buff=0.4)
|
||||||
|
|
||||||
|
self.play(Create(dial), FadeIn(hub), run_time=1.2)
|
||||||
|
self.add(pointer)
|
||||||
|
self.play(FadeIn(knob_label), run_time=0.8)
|
||||||
|
self.wait(0.5)
|
||||||
|
|
||||||
|
# the number side: a vertical 0-1 bar + live readout
|
||||||
|
bar_h = 3.0
|
||||||
|
bar = Rectangle(width=0.5, height=bar_h, color=NUMBER, stroke_width=4)
|
||||||
|
bar.shift(RIGHT * 2.6)
|
||||||
|
bar_bottom = bar.get_bottom()
|
||||||
|
|
||||||
|
fill = always_redraw(lambda: Rectangle(
|
||||||
|
width=0.5, height=max(1e-3, val.get_value() * bar_h),
|
||||||
|
color=NUMBER, fill_color=NUMBER, fill_opacity=0.8, stroke_width=0,
|
||||||
|
).move_to(bar_bottom + UP * (val.get_value() * bar_h) / 2))
|
||||||
|
|
||||||
|
tick0 = Text("0", font_size=20, color=NUMBER, font=MONO).next_to(bar, DL, buff=0.15)
|
||||||
|
tick1 = Text("1", font_size=20, color=NUMBER, font=MONO).next_to(bar, UL, buff=0.15)
|
||||||
|
|
||||||
|
readout = always_redraw(lambda: Text(
|
||||||
|
f"{val.get_value():.2f}", font_size=40, color=ACCENT, font=MONO,
|
||||||
|
).next_to(bar, RIGHT, buff=0.5))
|
||||||
|
|
||||||
|
num_label = Text("value", font_size=24, color=NUMBER, font=MONO)
|
||||||
|
num_label.next_to(bar, DOWN, buff=0.55)
|
||||||
|
|
||||||
|
# the binding arrow
|
||||||
|
arrow = Arrow(dial.get_right() + RIGHT * 0.1, bar.get_left() + LEFT * 0.1,
|
||||||
|
color=WHITE, stroke_width=4, buff=0.2).set_opacity(DIM)
|
||||||
|
|
||||||
|
self.play(Create(bar), FadeIn(tick0), FadeIn(tick1), run_time=1.0)
|
||||||
|
self.add(fill, readout)
|
||||||
|
self.play(GrowArrow(arrow), FadeIn(num_label), run_time=1.0)
|
||||||
|
self.wait(1.0)
|
||||||
|
|
||||||
|
bind = Text("they move in lockstep", font_size=26, color=WHITE, font=MONO)
|
||||||
|
bind.to_edge(DOWN, buff=0.6)
|
||||||
|
self.play(FadeIn(bind), run_time=0.8)
|
||||||
|
self.wait(0.8)
|
||||||
|
|
||||||
|
# sweep up, down, and to extremes -- pointer and number never disagree
|
||||||
|
self.play(val.animate.set_value(1.0), run_time=2.0, rate_func=smooth)
|
||||||
|
self.wait(0.8)
|
||||||
|
self.play(val.animate.set_value(0.0), run_time=2.5, rate_func=smooth)
|
||||||
|
self.wait(0.8)
|
||||||
|
self.play(val.animate.set_value(0.73), run_time=1.5, rate_func=smooth)
|
||||||
|
self.wait(1.5)
|
||||||
|
|
||||||
|
self.play(FadeOut(Group(*self.mobjects)), run_time=0.6)
|
||||||
|
self.wait(0.3)
|
||||||
134
assets/media/src/s2_xy_pad.py
Normal file
134
assets/media/src/s2_xy_pad.py
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
"""Manifold explainer 2 — XY pad projects to two numbers.
|
||||||
|
|
||||||
|
A 2-D pad is two independent 0-1 readings. Moving the dot moves A (horizontal)
|
||||||
|
and B (vertical) independently, each shown as a slider.
|
||||||
|
|
||||||
|
Render:
|
||||||
|
manim -qh s2_xy_pad.py XYPad
|
||||||
|
"""
|
||||||
|
|
||||||
|
from manim import *
|
||||||
|
|
||||||
|
BG = "#1C1C1C"
|
||||||
|
PRIMARY = "#58C4DD" # pad / control
|
||||||
|
A_COL = "#FFFF00" # A axis (horizontal)
|
||||||
|
B_COL = "#83C167" # B axis (vertical)
|
||||||
|
DIM = 0.4
|
||||||
|
MONO = "Monospace"
|
||||||
|
|
||||||
|
|
||||||
|
def slider(value_tracker, length, colour, horizontal=True):
|
||||||
|
"""A track + a knob that tracks value in [0,1] along `length`."""
|
||||||
|
if horizontal:
|
||||||
|
track = Line(LEFT * length / 2, RIGHT * length / 2, color=colour,
|
||||||
|
stroke_width=5).set_opacity(DIM)
|
||||||
|
knob = always_redraw(lambda: Dot(
|
||||||
|
track.get_start() + RIGHT * value_tracker.get_value() * length,
|
||||||
|
radius=0.13, color=colour))
|
||||||
|
fill = always_redraw(lambda: Line(
|
||||||
|
track.get_start(),
|
||||||
|
track.get_start() + RIGHT * value_tracker.get_value() * length,
|
||||||
|
color=colour, stroke_width=5))
|
||||||
|
else:
|
||||||
|
track = Line(DOWN * length / 2, UP * length / 2, color=colour,
|
||||||
|
stroke_width=5).set_opacity(DIM)
|
||||||
|
knob = always_redraw(lambda: Dot(
|
||||||
|
track.get_start() + UP * value_tracker.get_value() * length,
|
||||||
|
radius=0.13, color=colour))
|
||||||
|
fill = always_redraw(lambda: Line(
|
||||||
|
track.get_start(),
|
||||||
|
track.get_start() + UP * value_tracker.get_value() * length,
|
||||||
|
color=colour, stroke_width=5))
|
||||||
|
return VGroup(track), fill, knob
|
||||||
|
|
||||||
|
|
||||||
|
class XYPad(Scene):
|
||||||
|
def construct(self):
|
||||||
|
self.camera.background_color = BG
|
||||||
|
|
||||||
|
title = Text("One pad, two numbers", font_size=44, color=PRIMARY,
|
||||||
|
weight=BOLD, font=MONO)
|
||||||
|
self.play(Write(title), run_time=1.5)
|
||||||
|
self.wait(1.0)
|
||||||
|
self.play(title.animate.scale(0.62).to_edge(UP, buff=0.5), run_time=1.0)
|
||||||
|
self.wait(0.3)
|
||||||
|
|
||||||
|
# the pad
|
||||||
|
side = 3.4
|
||||||
|
ax = ValueTracker(0.5)
|
||||||
|
bx = ValueTracker(0.5)
|
||||||
|
|
||||||
|
pad = Square(side_length=side, color=PRIMARY, stroke_width=5)
|
||||||
|
pad.set_fill(PRIMARY, opacity=0.05)
|
||||||
|
pad.shift(LEFT * 3.0)
|
||||||
|
bl = pad.get_corner(DL)
|
||||||
|
|
||||||
|
grid = VGroup()
|
||||||
|
for f in (0.25, 0.5, 0.75):
|
||||||
|
grid.add(Line(bl + RIGHT * f * side, bl + RIGHT * f * side + UP * side,
|
||||||
|
color=PRIMARY, stroke_width=1).set_opacity(0.15))
|
||||||
|
grid.add(Line(bl + UP * f * side, bl + UP * f * side + RIGHT * side,
|
||||||
|
color=PRIMARY, stroke_width=1).set_opacity(0.15))
|
||||||
|
|
||||||
|
dot = always_redraw(lambda: Dot(
|
||||||
|
bl + RIGHT * ax.get_value() * side + UP * bx.get_value() * side,
|
||||||
|
radius=0.13, color=WHITE))
|
||||||
|
|
||||||
|
# guide lines from the dot to each axis
|
||||||
|
vline = always_redraw(lambda: DashedLine(
|
||||||
|
bl + RIGHT * ax.get_value() * side,
|
||||||
|
bl + RIGHT * ax.get_value() * side + UP * bx.get_value() * side,
|
||||||
|
color=A_COL, stroke_width=2).set_opacity(DIM))
|
||||||
|
hline = always_redraw(lambda: DashedLine(
|
||||||
|
bl + UP * bx.get_value() * side,
|
||||||
|
bl + RIGHT * ax.get_value() * side + UP * bx.get_value() * side,
|
||||||
|
color=B_COL, stroke_width=2).set_opacity(DIM))
|
||||||
|
|
||||||
|
self.play(Create(pad), Create(grid), run_time=1.2)
|
||||||
|
self.add(dot)
|
||||||
|
pad_label = Text("XY pad", font_size=24, color=PRIMARY, font=MONO)
|
||||||
|
pad_label.next_to(pad, DOWN, buff=0.4)
|
||||||
|
self.play(FadeIn(pad_label), run_time=0.6)
|
||||||
|
self.wait(0.5)
|
||||||
|
self.add(vline, hline)
|
||||||
|
|
||||||
|
# A slider (horizontal) and B slider (vertical) on the right
|
||||||
|
a_track, a_fill, a_knob = slider(ax, 3.0, A_COL, horizontal=True)
|
||||||
|
a_group = VGroup(a_track, a_fill, a_knob)
|
||||||
|
a_group.shift(RIGHT * 3.0 + UP * 1.4)
|
||||||
|
a_lab = always_redraw(lambda: Text(
|
||||||
|
f"A = {ax.get_value():.2f}", font_size=28, color=A_COL, font=MONO
|
||||||
|
).next_to(a_track, UP, buff=0.3))
|
||||||
|
|
||||||
|
b_track, b_fill, b_knob = slider(bx, 3.0, B_COL, horizontal=False)
|
||||||
|
b_group = VGroup(b_track, b_fill, b_knob)
|
||||||
|
b_group.shift(RIGHT * 3.6 + DOWN * 1.4)
|
||||||
|
b_lab = always_redraw(lambda: Text(
|
||||||
|
f"B = {bx.get_value():.2f}", font_size=28, color=B_COL, font=MONO
|
||||||
|
).next_to(b_track, RIGHT, buff=0.4))
|
||||||
|
|
||||||
|
self.play(Create(a_track), Create(b_track), run_time=1.0)
|
||||||
|
self.add(a_fill, a_knob, a_lab, b_fill, b_knob, b_lab)
|
||||||
|
self.wait(0.5)
|
||||||
|
|
||||||
|
note = Text("A reads across, B reads up -- independently",
|
||||||
|
font_size=24, color=WHITE, font=MONO).to_edge(DOWN, buff=0.55)
|
||||||
|
self.play(FadeIn(note), run_time=0.8)
|
||||||
|
self.wait(1.0)
|
||||||
|
|
||||||
|
# move horizontally only: A changes, B holds
|
||||||
|
self.play(ax.animate.set_value(0.9), run_time=1.6, rate_func=smooth)
|
||||||
|
self.wait(0.6)
|
||||||
|
# move vertically only: B changes, A holds
|
||||||
|
self.play(bx.animate.set_value(0.85), run_time=1.6, rate_func=smooth)
|
||||||
|
self.wait(0.6)
|
||||||
|
# a diagonal sweep: both change together
|
||||||
|
self.play(ax.animate.set_value(0.15), bx.animate.set_value(0.2),
|
||||||
|
run_time=2.0, rate_func=smooth)
|
||||||
|
self.wait(0.6)
|
||||||
|
self.play(ax.animate.set_value(0.65), bx.animate.set_value(0.7),
|
||||||
|
run_time=1.6, rate_func=smooth)
|
||||||
|
self.wait(1.5)
|
||||||
|
|
||||||
|
self.play(FadeOut(Group(*self.mobjects)), run_time=0.6)
|
||||||
|
self.wait(0.3)
|
||||||
162
assets/media/src/s3_fanout.py
Normal file
162
assets/media/src/s3_fanout.py
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
"""Manifold explainer 3 — Dimensionality fan-out.
|
||||||
|
|
||||||
|
The same low-dimensional control drives N outputs (3, then 4, then 5), with a
|
||||||
|
small network in between. Shown for several control types: knob, fader, touchpad,
|
||||||
|
2-D joystick, 3-D joystick. Control on one side, N sliders on the other.
|
||||||
|
|
||||||
|
Render:
|
||||||
|
manim -qh s3_fanout.py Fanout
|
||||||
|
"""
|
||||||
|
|
||||||
|
from manim import *
|
||||||
|
|
||||||
|
BG = "#1C1C1C"
|
||||||
|
PRIMARY = "#58C4DD" # control
|
||||||
|
NET = "#FF6B6B" # the network in the middle
|
||||||
|
OUT = "#83C167" # outputs
|
||||||
|
ACCENT = "#FFFF00"
|
||||||
|
DIM = 0.4
|
||||||
|
MONO = "Monospace"
|
||||||
|
|
||||||
|
|
||||||
|
def mini_control(kind):
|
||||||
|
"""Return a small VGroup glyph + label for a control type, plus how many
|
||||||
|
numbers it emits (its dimensionality)."""
|
||||||
|
if kind == "knob":
|
||||||
|
face = Circle(radius=0.55, color=PRIMARY, stroke_width=5)
|
||||||
|
ptr = Line(ORIGIN, UP * 0.45, color=ACCENT, stroke_width=6).rotate(
|
||||||
|
-40 * DEGREES, about_point=ORIGIN)
|
||||||
|
g = VGroup(face, ptr, Dot(ORIGIN, radius=0.05, color=ACCENT))
|
||||||
|
return g, "knob", 1
|
||||||
|
if kind == "fader":
|
||||||
|
track = Line(DOWN * 0.6, UP * 0.6, color=PRIMARY, stroke_width=5)
|
||||||
|
cap = Rectangle(width=0.4, height=0.18, color=ACCENT, fill_color=ACCENT,
|
||||||
|
fill_opacity=1).move_to(UP * 0.15)
|
||||||
|
return VGroup(track, cap), "fader", 1
|
||||||
|
if kind == "touchpad":
|
||||||
|
sq = Square(side_length=1.1, color=PRIMARY, stroke_width=5)
|
||||||
|
sq.set_fill(PRIMARY, opacity=0.06)
|
||||||
|
d = Dot(sq.get_center() + RIGHT * 0.25 + UP * 0.18, radius=0.09, color=WHITE)
|
||||||
|
return VGroup(sq, d), "touchpad", 2
|
||||||
|
if kind == "joystick2d":
|
||||||
|
ring = Circle(radius=0.6, color=PRIMARY, stroke_width=5)
|
||||||
|
stick = Line(ORIGIN, RIGHT * 0.3 + UP * 0.35, color=ACCENT, stroke_width=7)
|
||||||
|
knob = Dot(RIGHT * 0.3 + UP * 0.35, radius=0.1, color=ACCENT)
|
||||||
|
return VGroup(ring, stick, knob), "2-D joystick", 2
|
||||||
|
if kind == "joystick3d":
|
||||||
|
ring = Circle(radius=0.6, color=PRIMARY, stroke_width=5)
|
||||||
|
stick = Line(ORIGIN, RIGHT * 0.28 + UP * 0.32, color=ACCENT, stroke_width=7)
|
||||||
|
knob = Dot(RIGHT * 0.28 + UP * 0.32, radius=0.1, color=ACCENT)
|
||||||
|
# a small twist arc to suggest the 3rd (rotational) axis
|
||||||
|
twist = Arc(radius=0.78, start_angle=20 * DEGREES, angle=120 * DEGREES,
|
||||||
|
color=ACCENT, stroke_width=3).set_opacity(0.7)
|
||||||
|
twist_tip = Triangle(color=ACCENT, fill_opacity=1).scale(0.06).move_to(
|
||||||
|
twist.get_end())
|
||||||
|
return VGroup(ring, stick, knob, twist, twist_tip), "3-D joystick", 3
|
||||||
|
raise ValueError(kind)
|
||||||
|
|
||||||
|
|
||||||
|
class Fanout(Scene):
|
||||||
|
def construct(self):
|
||||||
|
self.camera.background_color = BG
|
||||||
|
|
||||||
|
title = Text("One control, many outputs", font_size=44, color=PRIMARY,
|
||||||
|
weight=BOLD, font=MONO)
|
||||||
|
self.play(Write(title), run_time=1.5)
|
||||||
|
self.wait(1.0)
|
||||||
|
self.play(title.animate.scale(0.6).to_edge(UP, buff=0.4), run_time=1.0)
|
||||||
|
self.wait(0.3)
|
||||||
|
|
||||||
|
# --- Part A: grow N (3 -> 4 -> 5) with a fixed 2-D control ---
|
||||||
|
ctrl, ctrl_name, ndim = mini_control("touchpad")
|
||||||
|
ctrl.scale(1.3).shift(LEFT * 4.5)
|
||||||
|
ctrl_lab = Text(f"{ctrl_name} ({ndim} numbers)", font_size=22,
|
||||||
|
color=PRIMARY, font=MONO).next_to(ctrl, DOWN, buff=0.4)
|
||||||
|
|
||||||
|
net = RoundedRectangle(width=1.4, height=2.4, corner_radius=0.15,
|
||||||
|
color=NET, stroke_width=4)
|
||||||
|
net.set_fill(NET, opacity=0.08).shift(LEFT * 0.6)
|
||||||
|
net_lab = Text("network", font_size=20, color=NET, font=MONO).next_to(
|
||||||
|
net, DOWN, buff=0.3)
|
||||||
|
|
||||||
|
self.play(FadeIn(ctrl), FadeIn(ctrl_lab), run_time=1.0)
|
||||||
|
self.play(Create(net), FadeIn(net_lab), run_time=1.0)
|
||||||
|
in_arrow = Arrow(ctrl.get_right(), net.get_left(), color=WHITE,
|
||||||
|
stroke_width=4, buff=0.25).set_opacity(DIM)
|
||||||
|
self.play(GrowArrow(in_arrow), run_time=0.6)
|
||||||
|
self.wait(0.5)
|
||||||
|
|
||||||
|
def make_sliders(n):
|
||||||
|
grp = VGroup()
|
||||||
|
top = 1.9
|
||||||
|
gap = 3.8 / max(1, n - 1) if n > 1 else 0
|
||||||
|
for i in range(n):
|
||||||
|
y = top - i * gap if n > 1 else 0
|
||||||
|
t = Line(ORIGIN, RIGHT * 2.0, color=OUT, stroke_width=4).set_opacity(DIM)
|
||||||
|
t.move_to(RIGHT * 3.7 + UP * y)
|
||||||
|
frac = 0.5 + 0.4 * np.sin(i * 1.3 + n)
|
||||||
|
fill = Line(t.get_start(), t.get_start() + RIGHT * 2.0 * frac,
|
||||||
|
color=OUT, stroke_width=4)
|
||||||
|
knob = Dot(t.get_start() + RIGHT * 2.0 * frac, radius=0.1, color=OUT)
|
||||||
|
lab = Text(f"out {i+1}", font_size=16, color=OUT, font=MONO).next_to(
|
||||||
|
t, LEFT, buff=0.2)
|
||||||
|
grp.add(VGroup(t, fill, knob, lab))
|
||||||
|
return grp
|
||||||
|
|
||||||
|
counter = Text("3 outputs", font_size=26, color=OUT, font=MONO).to_edge(
|
||||||
|
DOWN, buff=0.55)
|
||||||
|
|
||||||
|
sliders = make_sliders(3)
|
||||||
|
fan = VGroup(*[Line(net.get_right(), s[0].get_left(), color=NET,
|
||||||
|
stroke_width=2).set_opacity(0.5) for s in sliders])
|
||||||
|
self.play(Create(sliders), Create(fan), FadeIn(counter), run_time=1.2)
|
||||||
|
self.wait(1.2)
|
||||||
|
|
||||||
|
for n in (4, 5):
|
||||||
|
new_sliders = make_sliders(n)
|
||||||
|
new_fan = VGroup(*[Line(net.get_right(), s[0].get_left(), color=NET,
|
||||||
|
stroke_width=2).set_opacity(0.5) for s in new_sliders])
|
||||||
|
new_counter = Text(f"{n} outputs", font_size=26, color=OUT,
|
||||||
|
font=MONO).to_edge(DOWN, buff=0.55)
|
||||||
|
self.play(
|
||||||
|
ReplacementTransform(sliders, new_sliders),
|
||||||
|
ReplacementTransform(fan, new_fan),
|
||||||
|
ReplacementTransform(counter, new_counter),
|
||||||
|
run_time=1.3)
|
||||||
|
sliders, fan, counter = new_sliders, new_fan, new_counter
|
||||||
|
self.wait(1.0)
|
||||||
|
|
||||||
|
self.wait(0.5)
|
||||||
|
# clear the output side + counter, keep the network, swap the control type
|
||||||
|
self.play(FadeOut(sliders), FadeOut(fan), FadeOut(counter),
|
||||||
|
FadeOut(ctrl), FadeOut(ctrl_lab), run_time=0.6)
|
||||||
|
|
||||||
|
# --- Part B: cycle control types into the same network -> 5 outputs ---
|
||||||
|
note = Text("...and any control feeds the same fan-out",
|
||||||
|
font_size=24, color=WHITE, font=MONO).to_edge(DOWN, buff=0.55)
|
||||||
|
self.play(FadeIn(note), run_time=0.6)
|
||||||
|
|
||||||
|
five = make_sliders(5)
|
||||||
|
five_fan = VGroup(*[Line(net.get_right(), s[0].get_left(), color=NET,
|
||||||
|
stroke_width=2).set_opacity(0.5) for s in five])
|
||||||
|
self.play(Create(five), Create(five_fan), run_time=0.8)
|
||||||
|
|
||||||
|
prev_ctrl = None
|
||||||
|
prev_lab = None
|
||||||
|
for kind in ("knob", "fader", "touchpad", "joystick2d", "joystick3d"):
|
||||||
|
g, name, nd = mini_control(kind)
|
||||||
|
g.scale(1.3).shift(LEFT * 4.5)
|
||||||
|
lab = Text(f"{name} ({nd} number{'s' if nd > 1 else ''})",
|
||||||
|
font_size=22, color=PRIMARY, font=MONO).next_to(
|
||||||
|
g, DOWN, buff=0.4)
|
||||||
|
if prev_ctrl is None:
|
||||||
|
self.play(FadeIn(g), FadeIn(lab), run_time=0.7)
|
||||||
|
else:
|
||||||
|
self.play(ReplacementTransform(prev_ctrl, g),
|
||||||
|
ReplacementTransform(prev_lab, lab), run_time=0.9)
|
||||||
|
prev_ctrl, prev_lab = g, lab
|
||||||
|
self.wait(0.9)
|
||||||
|
|
||||||
|
self.wait(1.2)
|
||||||
|
self.play(FadeOut(Group(*self.mobjects)), run_time=0.6)
|
||||||
|
self.wait(0.3)
|
||||||
241
assets/media/src/s4_feedback.py
Normal file
241
assets/media/src/s4_feedback.py
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
"""Manifold explainer 4 — Feedback / training, two modes contrasted.
|
||||||
|
|
||||||
|
Training reshapes the mapping, seen as a landscape (the loss / mapping surface
|
||||||
|
projected to 2-D). Manifold offers two feedback modes:
|
||||||
|
|
||||||
|
* GeometricDislike -- you have a liked region; a thumbs-down carves the current
|
||||||
|
point directionally AWAY from the liked centroid.
|
||||||
|
* ExploreAndPlace -- (RECOMMENDED DEFAULT) randomise the net, audition, "I like
|
||||||
|
that", drop it in a corner, randomise again, place another,
|
||||||
|
then interpolate between the placed anchors. Positive-only.
|
||||||
|
|
||||||
|
Coordinated with docs/redesign/rl-feedback-design.md.
|
||||||
|
|
||||||
|
Render:
|
||||||
|
manim -qh s4_feedback.py GeometricDislike ExploreAndPlace FeedbackContrast
|
||||||
|
"""
|
||||||
|
|
||||||
|
from manim import *
|
||||||
|
|
||||||
|
BG = "#1C1C1C"
|
||||||
|
PRIMARY = "#58C4DD"
|
||||||
|
LIKE = "#83C167" # liked / positive
|
||||||
|
DISLIKE = "#FF6B6B" # disliked / push-away
|
||||||
|
ACCENT = "#FFFF00"
|
||||||
|
ANCHOR = "#83C167"
|
||||||
|
MONO = "Monospace"
|
||||||
|
|
||||||
|
|
||||||
|
def control_pad(side=3.2, shift=ORIGIN, colour=PRIMARY):
|
||||||
|
"""A 2-D control space (the input plane the mapping lives over)."""
|
||||||
|
pad = Square(side_length=side, color=colour, stroke_width=4)
|
||||||
|
pad.set_fill(colour, opacity=0.05).move_to(shift)
|
||||||
|
grid = VGroup()
|
||||||
|
bl = pad.get_corner(DL)
|
||||||
|
for f in (0.25, 0.5, 0.75):
|
||||||
|
grid.add(Line(bl + RIGHT * f * side, bl + RIGHT * f * side + UP * side,
|
||||||
|
color=colour, stroke_width=1).set_opacity(0.15))
|
||||||
|
grid.add(Line(bl + UP * f * side, bl + UP * f * side + RIGHT * side,
|
||||||
|
color=colour, stroke_width=1).set_opacity(0.15))
|
||||||
|
return pad, grid
|
||||||
|
|
||||||
|
|
||||||
|
class GeometricDislike(Scene):
|
||||||
|
def construct(self):
|
||||||
|
self.camera.background_color = BG
|
||||||
|
title = Text("Geometric dislike", font_size=42, color=DISLIKE,
|
||||||
|
weight=BOLD, font=MONO).to_edge(UP, buff=0.4)
|
||||||
|
sub = Text("push the sound away from what you like", font_size=24,
|
||||||
|
color=WHITE, font=MONO).next_to(title, DOWN, buff=0.25)
|
||||||
|
self.play(Write(title), run_time=1.2)
|
||||||
|
self.play(FadeIn(sub), run_time=0.7)
|
||||||
|
self.wait(0.8)
|
||||||
|
|
||||||
|
pad, grid = control_pad(3.4, LEFT * 3.0 + DOWN * 0.3)
|
||||||
|
self.play(Create(pad), Create(grid), run_time=1.0)
|
||||||
|
|
||||||
|
# a few liked points + their centroid
|
||||||
|
liked_pts = [LEFT * 3.8 + DOWN * 0.9, LEFT * 2.5 + UP * 0.3,
|
||||||
|
LEFT * 3.2 + UP * 0.6]
|
||||||
|
likes = VGroup(*[Dot(p, radius=0.11, color=LIKE) for p in liked_pts])
|
||||||
|
like_lab = Text("liked region", font_size=20, color=LIKE, font=MONO)
|
||||||
|
like_lab.next_to(pad, DOWN, buff=0.3)
|
||||||
|
self.play(LaggedStart(*[GrowFromCenter(d) for d in likes], lag_ratio=0.3),
|
||||||
|
FadeIn(like_lab), run_time=1.2)
|
||||||
|
|
||||||
|
centroid_pos = np.mean(np.array(liked_pts), axis=0)
|
||||||
|
centroid = Dot(centroid_pos, radius=0.09, color=LIKE).set_opacity(0.6)
|
||||||
|
cring = Circle(radius=0.22, color=LIKE, stroke_width=2).move_to(
|
||||||
|
centroid_pos).set_opacity(0.6)
|
||||||
|
c_lab = Text("centre", font_size=16, color=LIKE, font=MONO).next_to(
|
||||||
|
cring, UP, buff=0.1)
|
||||||
|
self.play(FadeIn(centroid), Create(cring), FadeIn(c_lab), run_time=0.8)
|
||||||
|
self.wait(0.8)
|
||||||
|
|
||||||
|
# the current (heard) point + thumbs-down
|
||||||
|
cur = Dot(LEFT * 1.9 + DOWN * 1.0, radius=0.13, color=ACCENT)
|
||||||
|
cur_lab = Text("current sound", font_size=18, color=ACCENT, font=MONO).next_to(
|
||||||
|
cur, RIGHT, buff=0.15)
|
||||||
|
self.play(GrowFromCenter(cur), FadeIn(cur_lab), run_time=0.8)
|
||||||
|
self.wait(0.5)
|
||||||
|
|
||||||
|
down = Text("thumbs-down", font_size=26, color=DISLIKE, font=MONO).to_edge(
|
||||||
|
DOWN, buff=0.5)
|
||||||
|
self.play(FadeIn(down), Flash(cur, color=DISLIKE, line_length=0.3), run_time=0.9)
|
||||||
|
self.wait(0.4)
|
||||||
|
|
||||||
|
# the push vector: directed AWAY from the centroid (cur - centroid)
|
||||||
|
direction = cur.get_center() - centroid_pos
|
||||||
|
direction = direction / np.linalg.norm(direction)
|
||||||
|
target = cur.get_center() + direction * 1.5
|
||||||
|
push = Arrow(cur.get_center(), target, color=DISLIKE, stroke_width=6,
|
||||||
|
buff=0.0)
|
||||||
|
self.play(GrowArrow(push), run_time=1.0)
|
||||||
|
self.wait(0.4)
|
||||||
|
self.play(cur.animate.move_to(target),
|
||||||
|
cur_lab.animate.next_to(target, RIGHT, buff=0.15),
|
||||||
|
run_time=1.4, rate_func=smooth)
|
||||||
|
self.play(FadeOut(push), run_time=0.4)
|
||||||
|
self.wait(0.5)
|
||||||
|
|
||||||
|
note = Text("directed repulsion: away from the centre, not random",
|
||||||
|
font_size=22, color=WHITE, font=MONO).to_edge(DOWN, buff=0.5)
|
||||||
|
self.play(ReplacementTransform(down, note), run_time=0.7)
|
||||||
|
self.wait(1.8)
|
||||||
|
self.play(FadeOut(Group(*self.mobjects)), run_time=0.6)
|
||||||
|
self.wait(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
class ExploreAndPlace(Scene):
|
||||||
|
def construct(self):
|
||||||
|
self.camera.background_color = BG
|
||||||
|
title = Text("Explore and place", font_size=42, color=LIKE,
|
||||||
|
weight=BOLD, font=MONO).to_edge(UP, buff=0.4)
|
||||||
|
tag = Text("recommended default", font_size=18, color=ACCENT,
|
||||||
|
font=MONO).next_to(title, RIGHT, buff=0.4)
|
||||||
|
sub = Text("randomise -> audition -> place -> interpolate", font_size=24,
|
||||||
|
color=WHITE, font=MONO).next_to(title, DOWN, buff=0.25)
|
||||||
|
self.play(Write(title), FadeIn(tag), run_time=1.2)
|
||||||
|
self.play(FadeIn(sub), run_time=0.7)
|
||||||
|
self.wait(0.8)
|
||||||
|
|
||||||
|
pad, grid = control_pad(3.6, LEFT * 3.0 + DOWN * 0.3)
|
||||||
|
self.play(Create(pad), Create(grid), run_time=1.0)
|
||||||
|
|
||||||
|
# the scratchpad readout (audition meter) on the right
|
||||||
|
meter_lab = Text("audition", font_size=22, color=PRIMARY, font=MONO)
|
||||||
|
meter_lab.shift(RIGHT * 3.3 + UP * 2.0)
|
||||||
|
bar_bg = Rectangle(width=2.4, height=0.5, color=PRIMARY, stroke_width=3)
|
||||||
|
bar_bg.next_to(meter_lab, DOWN, buff=0.3)
|
||||||
|
self.play(FadeIn(meter_lab), Create(bar_bg), run_time=0.8)
|
||||||
|
|
||||||
|
def audition_bar(frac, colour=ACCENT):
|
||||||
|
return Rectangle(width=2.4 * frac, height=0.5, color=colour,
|
||||||
|
fill_color=colour, fill_opacity=0.8,
|
||||||
|
stroke_width=0).align_to(bar_bg, LEFT).set_y(bar_bg.get_y())
|
||||||
|
|
||||||
|
step = Text("", font_size=24, color=WHITE, font=MONO).to_edge(DOWN, buff=0.5)
|
||||||
|
self.add(step)
|
||||||
|
|
||||||
|
def set_step(txt, colour=WHITE):
|
||||||
|
new = Text(txt, font_size=24, color=colour, font=MONO).to_edge(DOWN, buff=0.5)
|
||||||
|
self.play(Transform(step, new), run_time=0.5)
|
||||||
|
|
||||||
|
anchors = VGroup()
|
||||||
|
corners = [LEFT * 4.1 + UP * 0.9, LEFT * 1.9 + DOWN * 1.4]
|
||||||
|
timbres = [0.78, 0.32]
|
||||||
|
|
||||||
|
for i, (corner, t) in enumerate(zip(corners, timbres)):
|
||||||
|
# randomise: meter jitters
|
||||||
|
set_step('"meh, randomise..."', PRIMARY)
|
||||||
|
jit = audition_bar(0.5)
|
||||||
|
self.add(jit)
|
||||||
|
for f in (0.85, 0.2, 0.6, t):
|
||||||
|
self.play(Transform(jit, audition_bar(f)), run_time=0.35)
|
||||||
|
self.wait(0.4)
|
||||||
|
set_step('"oh, I like that!"', LIKE)
|
||||||
|
self.play(Transform(jit, audition_bar(t, LIKE)),
|
||||||
|
Flash(bar_bg, color=LIKE, line_length=0.2), run_time=0.6)
|
||||||
|
self.wait(0.4)
|
||||||
|
# place it into a corner
|
||||||
|
set_step("place it in that corner", LIKE)
|
||||||
|
placed = Dot(bar_bg.get_center(), radius=0.13, color=ANCHOR)
|
||||||
|
self.add(placed)
|
||||||
|
self.play(placed.animate.move_to(corner), run_time=1.2, rate_func=smooth)
|
||||||
|
ring = Circle(radius=0.2, color=ANCHOR, stroke_width=2).move_to(corner)
|
||||||
|
self.play(Create(ring), run_time=0.4)
|
||||||
|
anchors.add(VGroup(placed, ring))
|
||||||
|
self.remove(jit)
|
||||||
|
self.wait(0.5)
|
||||||
|
|
||||||
|
# interpolate between the two placed anchors
|
||||||
|
set_step("interpolate between placed sounds", ACCENT)
|
||||||
|
a0 = corners[0]
|
||||||
|
a1 = corners[1]
|
||||||
|
morph = Dot(a0, radius=0.12, color=ACCENT)
|
||||||
|
line = DashedLine(a0, a1, color=ACCENT, stroke_width=2).set_opacity(0.4)
|
||||||
|
self.play(Create(line), GrowFromCenter(morph), run_time=0.7)
|
||||||
|
morph_bar = audition_bar(timbres[0], ACCENT)
|
||||||
|
self.add(morph_bar)
|
||||||
|
for f in (0.25, 0.5, 0.75, 1.0, 0.4):
|
||||||
|
pos = a0 + (a1 - a0) * f
|
||||||
|
val = timbres[0] + (timbres[1] - timbres[0]) * f
|
||||||
|
self.play(morph.animate.move_to(pos),
|
||||||
|
Transform(morph_bar, audition_bar(val, ACCENT)),
|
||||||
|
run_time=0.7)
|
||||||
|
self.wait(0.5)
|
||||||
|
set_step("a smooth morph -- and no concept of 'dislike'", LIKE)
|
||||||
|
self.wait(1.8)
|
||||||
|
self.play(FadeOut(Group(*self.mobjects)), run_time=0.6)
|
||||||
|
self.wait(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
class FeedbackContrast(Scene):
|
||||||
|
def construct(self):
|
||||||
|
self.camera.background_color = BG
|
||||||
|
title = Text("Two ways to teach a mapping", font_size=40, color=PRIMARY,
|
||||||
|
weight=BOLD, font=MONO).to_edge(UP, buff=0.4)
|
||||||
|
self.play(Write(title), run_time=1.2)
|
||||||
|
self.wait(0.6)
|
||||||
|
|
||||||
|
divider = DashedLine(UP * 2.3, DOWN * 3.0, color=WHITE,
|
||||||
|
stroke_width=2).set_opacity(0.25)
|
||||||
|
self.play(Create(divider), run_time=0.6)
|
||||||
|
|
||||||
|
# left: geometric dislike
|
||||||
|
l_head = Text("Geometric dislike", font_size=28, color=DISLIKE,
|
||||||
|
weight=BOLD, font=MONO).move_to(LEFT * 3.5 + UP * 1.7)
|
||||||
|
l_lines = VGroup(
|
||||||
|
Text("- you have a liked region", font_size=20, color=WHITE, font=MONO),
|
||||||
|
Text("- thumbs-down pushes the", font_size=20, color=WHITE, font=MONO),
|
||||||
|
Text(" sound AWAY from it", font_size=20, color=DISLIKE, font=MONO),
|
||||||
|
Text("- directed, audible repulsion", font_size=20, color=WHITE, font=MONO),
|
||||||
|
Text("- precision sculpting tool", font_size=20, color=WHITE, font=MONO),
|
||||||
|
).arrange(DOWN, aligned_edge=LEFT, buff=0.28).move_to(LEFT * 3.5 + DOWN * 0.3)
|
||||||
|
|
||||||
|
# right: explore and place
|
||||||
|
r_head = Text("Explore and place", font_size=28, color=LIKE,
|
||||||
|
weight=BOLD, font=MONO).move_to(RIGHT * 3.5 + UP * 1.7)
|
||||||
|
r_tag = Text("DEFAULT", font_size=16, color=ACCENT, font=MONO).next_to(
|
||||||
|
r_head, DOWN, buff=0.12)
|
||||||
|
r_lines = VGroup(
|
||||||
|
Text("- randomise, audition, place", font_size=20, color=WHITE, font=MONO),
|
||||||
|
Text("- positive-only: collect", font_size=20, color=LIKE, font=MONO),
|
||||||
|
Text(" sounds you like", font_size=20, color=LIKE, font=MONO),
|
||||||
|
Text("- interpolate between anchors", font_size=20, color=WHITE, font=MONO),
|
||||||
|
Text("- no 'away from what' to reason", font_size=20, color=WHITE, font=MONO),
|
||||||
|
).arrange(DOWN, aligned_edge=LEFT, buff=0.28).move_to(RIGHT * 3.5 + DOWN * 0.3)
|
||||||
|
|
||||||
|
self.play(FadeIn(l_head), FadeIn(r_head), FadeIn(r_tag), run_time=0.8)
|
||||||
|
self.play(LaggedStart(*[FadeIn(x) for x in l_lines], lag_ratio=0.25),
|
||||||
|
LaggedStart(*[FadeIn(x) for x in r_lines], lag_ratio=0.25),
|
||||||
|
run_time=2.0)
|
||||||
|
self.wait(1.5)
|
||||||
|
|
||||||
|
rec = Text("Default to explore-and-place; reach for dislike to sculpt.",
|
||||||
|
font_size=22, color=ACCENT, font=MONO).to_edge(DOWN, buff=0.45)
|
||||||
|
box = SurroundingRectangle(rec, color=ACCENT, buff=0.2, stroke_width=2)
|
||||||
|
self.play(FadeIn(rec), Create(box), run_time=1.0)
|
||||||
|
self.wait(2.5)
|
||||||
|
self.play(FadeOut(Group(*self.mobjects)), run_time=0.6)
|
||||||
|
self.wait(0.3)
|
||||||
Loading…
Reference in a new issue