···
5
5
s *args:
6
6
just schedule-hell --resolution 480 {{args}}
7
7
8
8
+
backbone *args:
9
9
+
just schedule-hell --resolution 480 --marker "\"end first break\"" {{args}}
10
10
+
8
11
[working-directory: 'examples/schedule-hell']
9
12
schedule-hell *args:
10
13
cargo run -- {{args}}
···
6
6
[dependencies]
7
7
anyhow = "1.0.97"
8
8
itertools = "0.14.0"
9
9
+
notify-rust = "4.11.7"
9
10
pico-args = { version = "0.5.0", features = ["combined-flags", "eq-separator"] }
10
11
rand = "0.9.0"
11
12
serde-aux = "4.7.0"
···
1
1
mod scenes;
2
2
3
3
use anyhow::anyhow;
4
4
+
use itertools::Itertools;
4
5
use rand::{SeedableRng, rngs::SmallRng};
5
5
-
use shapemaker::{ui::Log, *};
6
6
+
use shapemaker::{ui::Log, video::engine::EngineControl, *};
6
7
use std::{fs, path::PathBuf, time::Duration};
7
8
8
9
pub struct State {
9
10
bass_pattern_at: Region,
10
11
kick_color: Color,
11
12
rng: SmallRng,
12
12
-
kick_counter: u32,
13
13
+
cranks: u32,
13
14
}
14
15
15
16
impl Default for State {
···
18
19
bass_pattern_at: Region::from_topleft(Point(1, 1), (2, 2)).unwrap(),
19
20
kick_color: Color::White,
20
21
rng: SmallRng::seed_from_u64(0),
21
21
-
kick_counter: 0,
22
22
+
cranks: 0,
22
23
}
23
24
}
24
25
}
25
26
26
27
#[tokio::main]
27
28
pub async fn main() {
28
28
-
let mut canvas = Canvas::with_layers(vec![]);
29
29
-
30
30
-
canvas.set_grid_size(16, 9);
31
31
-
canvas.colormap = ColorMapping {
32
32
-
black: "#000000".into(),
33
33
-
white: "#ffffff".into(),
34
34
-
red: "#cf0a2b".into(),
35
35
-
green: "#22e753".into(),
36
36
-
blue: "#2734e6".into(),
37
37
-
yellow: "#f8e21e".into(),
38
38
-
orange: "#f05811".into(),
39
39
-
purple: "#6a24ec".into(),
40
40
-
brown: "#a05634".into(),
41
41
-
pink: "#e92e76".into(),
42
42
-
gray: "#81a0a8".into(),
43
43
-
cyan: "#4fecec".into(),
44
44
-
};
29
29
+
let canvas = Canvas::new(16, 9);
45
30
46
31
let mut video = Video::<State>::new(canvas);
47
32
let mut args = pico_args::Arguments::from_env();
···
61
46
.map(Timestamp::from_seconds)
62
47
.unwrap_or_default();
63
48
49
49
+
video = video
50
50
+
// Sync inputs //
51
51
+
.sync_audio_with("schedule-hell.midi")
52
52
+
.sync_audio_with("schedule-hell.wav");
53
53
+
54
54
+
if let Ok(marker) = args.value_from_str::<_, String>("--marker") {
55
55
+
let marker_start = video
56
56
+
.syncdata
57
57
+
.markers
58
58
+
.iter()
59
59
+
.find_map(|(&ms, m)| if m == &marker { Some(ms) } else { None })
60
60
+
.expect("Marker not found");
61
61
+
62
62
+
let marker_end = video
63
63
+
.syncdata
64
64
+
.markers
65
65
+
.iter()
66
66
+
.filter(|&(&ms, _)| ms > marker_start)
67
67
+
.sorted_by_key(|&(&ms, _)| ms)
68
68
+
.find_map(|(&ms, m)| if m != &marker { Some(ms) } else { None });
69
69
+
70
70
+
video.start_rendering_at = Timestamp::from_ms(marker_start as _);
71
71
+
video.duration_override =
72
72
+
marker_end.map(|end| Duration::from_millis((end - marker_start) as _))
73
73
+
}
74
74
+
64
75
video.resolution = args.value_from_str("--resolution").ok().unwrap_or(480);
65
76
video.fps = args.value_from_str("--fps").ok().unwrap_or(30);
66
77
67
78
video.audiofile = PathBuf::from("schedule-hell.wav");
68
79
video = video
69
69
-
// Sync inputs //
70
70
-
.sync_audio_with("schedule-hell.midi")
71
71
-
.sync_audio_with("schedule-hell.wav")
72
80
// Scenes //
73
81
.with_scene(scenes::starry_sky())
74
82
.with_init_scene(scenes::intro())
75
83
.with_marked_scene(scenes::first_break())
76
76
-
.assign_scene_to("end of first break", "starry sky")
77
77
-
.assign_scene_to("second break", "intro")
84
84
+
.with_scene(scenes::backbone())
85
85
+
.assign_scene_to("end of first break", "intro")
86
86
+
.assign_scene_to("second break", "starry sky")
87
87
+
// "end first break" means "end of second break" lol
88
88
+
.assign_scene_to("end first break", "backbone")
78
89
// Credits //
79
90
.when_remaining(10, &|canvas, _| {
80
91
let world = canvas.world_region;
···
112
123
),
113
124
);
114
125
115
115
-
video.render_frame(frame_no, render_ahead).and_then(|svg| {
116
116
-
fs::write(destination, svg.to_string())
117
117
-
.map_err(|e| anyhow!("{e:?}"))
118
118
-
})
126
126
+
video
127
127
+
.render_frame(frame_no, render_ahead)
128
128
+
.and_then(|svg| {
129
129
+
fs::write(destination, svg.to_string())
130
130
+
.map_err(|e| anyhow!("{e:?}"))
131
131
+
})
132
132
+
.map(|_| Duration::default())
119
133
} else {
120
120
-
video.encode(destination).map(|_| ())
134
134
+
video.encode(destination)
121
135
};
122
136
123
137
match result {
···
1
1
+
use anyhow::Result;
2
2
+
use rand::{Rng, rngs::SmallRng, seq::IteratorRandom};
3
3
+
use shapemaker::*;
4
4
+
5
5
+
use crate::State;
6
6
+
7
7
+
pub fn backbone() -> Scene<State> {
8
8
+
Scene::<State>::new("backbone")
9
9
+
.init(&|canvas, ctx| {
10
10
+
canvas.clear();
11
11
+
12
12
+
canvas.colormap = ColorMapping {
13
13
+
black: "#000000".to_string(),
14
14
+
white: "#FFFFFF".to_string(),
15
15
+
purple: "#da40f5".to_string(),
16
16
+
..Default::default()
17
17
+
};
18
18
+
19
19
+
canvas.set_grid_size(16, 10);
20
20
+
canvas.set_background(Black);
21
21
+
canvas.object_sizes.dot_radius = 7.5;
22
22
+
23
23
+
iterate(&mut ctx.extra.rng, canvas)?;
24
24
+
Ok(())
25
25
+
})
26
26
+
.each_n_frame(3, &|canvas, ctx| {
27
27
+
canvas.clear();
28
28
+
iterate(&mut ctx.extra.rng, canvas)?;
29
29
+
Ok(())
30
30
+
})
31
31
+
.on_note("anchor kick", &|canvas, ctx| {
32
32
+
canvas.clear();
33
33
+
iterate(&mut ctx.extra.rng, canvas)?;
34
34
+
35
35
+
let world = canvas.world_region.clone();
36
36
+
let flickers = canvas.layer("flickers")?;
37
37
+
38
38
+
let point = world.iter().choose(&mut ctx.extra.rng).unwrap();
39
39
+
40
40
+
flickers.tag_objects("rotate", |id, _| {
41
41
+
id == &format!("crosses-SWNE-{point}")
42
42
+
|| id == &format!("crosses-NWSE-{point}")
43
43
+
});
44
44
+
45
45
+
ctx.animate(700, &move |t, canvas, _| {
46
46
+
canvas
47
47
+
.layer("flickers")?
48
48
+
.objects_with_tag("rotate")
49
49
+
.for_each(|(_, obj)| {
50
50
+
obj.recolor(Cyan);
51
51
+
obj.set_rotation(Angle::from_degrees(t * 45.0));
52
52
+
});
53
53
+
54
54
+
Ok(())
55
55
+
});
56
56
+
57
57
+
Ok(())
58
58
+
})
59
59
+
}
60
60
+
61
61
+
fn iterate(rng: &mut SmallRng, canvas: &mut Canvas) -> Result<()> {
62
62
+
// let mut rng = canvas.rng.clone();
63
63
+
let world = canvas.world_region.clone();
64
64
+
65
65
+
let grid_thickness = 2.0;
66
66
+
67
67
+
for point in
68
68
+
Region::from((world.topleft(), world.topright().translated(1, 1)))
69
69
+
{
70
70
+
canvas.root().set(
71
71
+
format!("grid-rows-{point}"),
72
72
+
Object::Line(
73
73
+
Point(point.0, world.topleft().1),
74
74
+
Point(point.0, world.bottomleft().1 + 1),
75
75
+
grid_thickness * 0.75,
76
76
+
)
77
77
+
.filled(White.translucent(0.05 + rng.random_range(0.0..0.3))),
78
78
+
);
79
79
+
}
80
80
+
81
81
+
for point in
82
82
+
Region::from((world.topleft(), world.bottomleft().translated(1, 1)))
83
83
+
{
84
84
+
canvas.root().set(
85
85
+
format!("grid-cols-{point}"),
86
86
+
Object::Line(
87
87
+
Point(world.topleft().0, point.1),
88
88
+
Point(world.bottomright().0 + 1, point.1),
89
89
+
grid_thickness * 0.75,
90
90
+
)
91
91
+
.filled(White.translucent(0.005 + rng.random_range(0.0..0.3))),
92
92
+
);
93
93
+
}
94
94
+
95
95
+
let occlusions = canvas.layer_or_empty("occlusions");
96
96
+
97
97
+
for point in world.enlarged(1, 1) {
98
98
+
occlusions.set(
99
99
+
format!("occlusion-{point}"),
100
100
+
Object::Dot(point).colored(Color::Black),
101
101
+
);
102
102
+
}
103
103
+
104
104
+
let flickers = canvas.layer_or_empty("flickers");
105
105
+
106
106
+
for point in world {
107
107
+
flickers.set(
108
108
+
format!("crosses-SWNE-{point}"),
109
109
+
Object::Line(point, point.translated(1, 1), grid_thickness)
110
110
+
.colored(Color::Purple)
111
111
+
.opacified(0.25 + rng.random_range(0.5..1.0)),
112
112
+
);
113
113
+
flickers.set(
114
114
+
format!("crosses-NWSE-{point}"),
115
115
+
Object::Line(
116
116
+
point.translated(0, 1),
117
117
+
point.translated(1, 0),
118
118
+
grid_thickness,
119
119
+
)
120
120
+
.colored(Color::Purple)
121
121
+
.opacified(0.25 + rng.random_range(0.5..1.0)),
122
122
+
);
123
123
+
}
124
124
+
125
125
+
let flickers_occlusions = canvas.layer_or_empty("flickers_occlusions");
126
126
+
flickers_occlusions.object_sizes.dot_radius = 10.0;
127
127
+
128
128
+
for point in world.enlarged(1, 1) {
129
129
+
flickers_occlusions.set(
130
130
+
format!("crosses-occlusions-{point}"),
131
131
+
Object::Dot(point).colored(Color::Black),
132
132
+
)
133
133
+
}
134
134
+
135
135
+
canvas.reorder_layers(vec![
136
136
+
"flickers_occlusions",
137
137
+
"flickers",
138
138
+
"occlusions",
139
139
+
"root",
140
140
+
]);
141
141
+
142
142
+
Ok(())
143
143
+
}
···
7
7
canvas.clear();
8
8
canvas.set_grid_size(16, 9);
9
9
canvas.set_background(Black);
10
10
+
canvas.colormap = ColorMapping {
11
11
+
black: "#000000".into(),
12
12
+
white: "#ffffff".into(),
13
13
+
red: "#cf0a2b".into(),
14
14
+
green: "#22e753".into(),
15
15
+
blue: "#2734e6".into(),
16
16
+
yellow: "#f8e21e".into(),
17
17
+
orange: "#f05811".into(),
18
18
+
purple: "#6a24ec".into(),
19
19
+
brown: "#a05634".into(),
20
20
+
pink: "#e92e76".into(),
21
21
+
gray: "#81a0a8".into(),
22
22
+
cyan: "#4fecec".into(),
23
23
+
};
10
24
11
25
let mut kicks = Layer::new("anchor kick");
12
26
···
25
39
.layer("anchor kick")?
26
40
.paint_all_objects(Fill::Translucent(ctx.extra.kick_color, 1.0));
27
41
28
28
-
ctx.animate_layer("anchor kick", 200, &|t, layer, _| {
29
29
-
layer.objects.values_mut().for_each(
42
42
+
ctx.animate(200, &|t, canvas, _| {
43
43
+
canvas.layer("anchor kick")?.objects.values_mut().for_each(
30
44
|ColoredObject { fill, .. }| {
31
45
*fill = fill.opacify(1.0 - t);
32
46
},
···
4
4
pub use first_break::first_break;
5
5
pub mod starry_sky;
6
6
pub use starry_sky::starry_sky;
7
7
+
pub mod backbone;
8
8
+
pub use backbone::backbone;
···
6
6
pub fn starry_sky() -> Scene<State> {
7
7
Scene::<State>::new("starry sky")
8
8
.init(&|canvas, ctx| {
9
9
-
ctx.extra.kick_counter = 0;
10
10
-
sky(ctx.extra.kick_counter, canvas)
9
9
+
ctx.extra.cranks = 0;
10
10
+
sky(ctx.extra.cranks, canvas)
11
11
})
12
12
-
.on_note("anchor kick", &|canvas, ctx| {
12
12
+
.on_note("brokenup", &|canvas, ctx| {
13
13
// Move spacecraft on each kick
14
14
-
ctx.extra.kick_counter += 1;
15
15
-
sky(ctx.extra.kick_counter, canvas)
14
14
+
ctx.extra.cranks += 1;
15
15
+
sky(ctx.extra.cranks, canvas)
16
16
})
17
17
.each_n_frame(3, &|canvas, ctx| {
18
18
// Keep spacecraft alive, by animating on threes
19
19
-
sky(ctx.extra.kick_counter, canvas)
19
19
+
sky(ctx.extra.cranks, canvas)
20
20
})
21
21
}
22
22
23
23
-
fn sky(kick_hits_count: u32, canvas: &mut Canvas) -> Result<()> {
24
24
-
// Make a full rotation every 32 kicks
25
25
-
let theta = Angle::from_ratio(kick_hits_count as f32, 32.0);
23
23
+
fn sky(cranks: u32, canvas: &mut Canvas) -> Result<()> {
24
24
+
let theta = Angle::from_ratio(cranks as f32, 72.0);
26
25
27
26
canvas.clear();
28
27
canvas.colormap = ColorMapping {