examples
schedule-hell
schedule-hell-backbone
src
···
156
156
checksum = "a8ab6b55fe97976e46f91ddbed8d147d966475dc29b2032757ba47e02376fbc3"
157
157
158
158
[[package]]
159
159
+
name = "atomic-waker"
160
160
+
version = "1.1.2"
161
161
+
source = "registry+https://github.com/rust-lang/crates.io-index"
162
162
+
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
163
163
+
164
164
+
[[package]]
159
165
name = "atomic_float"
160
166
version = "0.1.0"
161
167
source = "registry+https://github.com/rust-lang/crates.io-index"
···
183
189
version = "1.5.0"
184
190
source = "registry+https://github.com/rust-lang/crates.io-index"
185
191
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
192
192
+
193
193
+
[[package]]
194
194
+
name = "axum"
195
195
+
version = "0.8.6"
196
196
+
source = "registry+https://github.com/rust-lang/crates.io-index"
197
197
+
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
198
198
+
dependencies = [
199
199
+
"axum-core",
200
200
+
"bytes 1.10.1",
201
201
+
"form_urlencoded",
202
202
+
"futures-util",
203
203
+
"http",
204
204
+
"http-body",
205
205
+
"http-body-util",
206
206
+
"hyper",
207
207
+
"hyper-util",
208
208
+
"itoa",
209
209
+
"matchit",
210
210
+
"memchr",
211
211
+
"mime",
212
212
+
"percent-encoding",
213
213
+
"pin-project-lite",
214
214
+
"serde_core",
215
215
+
"serde_json",
216
216
+
"serde_path_to_error",
217
217
+
"serde_urlencoded",
218
218
+
"sync_wrapper",
219
219
+
"tokio",
220
220
+
"tower",
221
221
+
"tower-layer",
222
222
+
"tower-service",
223
223
+
"tracing",
224
224
+
]
225
225
+
226
226
+
[[package]]
227
227
+
name = "axum-core"
228
228
+
version = "0.5.5"
229
229
+
source = "registry+https://github.com/rust-lang/crates.io-index"
230
230
+
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
231
231
+
dependencies = [
232
232
+
"bytes 1.10.1",
233
233
+
"futures-core",
234
234
+
"http",
235
235
+
"http-body",
236
236
+
"http-body-util",
237
237
+
"mime",
238
238
+
"pin-project-lite",
239
239
+
"sync_wrapper",
240
240
+
"tower-layer",
241
241
+
"tower-service",
242
242
+
"tracing",
243
243
+
]
186
244
187
245
[[package]]
188
246
name = "backtrace"
···
2484
2542
]
2485
2543
2486
2544
[[package]]
2545
2545
+
name = "http-body"
2546
2546
+
version = "1.0.1"
2547
2547
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2548
2548
+
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
2549
2549
+
dependencies = [
2550
2550
+
"bytes 1.10.1",
2551
2551
+
"http",
2552
2552
+
]
2553
2553
+
2554
2554
+
[[package]]
2555
2555
+
name = "http-body-util"
2556
2556
+
version = "0.1.3"
2557
2557
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2558
2558
+
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
2559
2559
+
dependencies = [
2560
2560
+
"bytes 1.10.1",
2561
2561
+
"futures-core",
2562
2562
+
"http",
2563
2563
+
"http-body",
2564
2564
+
"pin-project-lite",
2565
2565
+
]
2566
2566
+
2567
2567
+
[[package]]
2487
2568
name = "httparse"
2488
2569
version = "1.10.1"
2489
2570
source = "registry+https://github.com/rust-lang/crates.io-index"
2490
2571
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
2491
2572
2492
2573
[[package]]
2574
2574
+
name = "httpdate"
2575
2575
+
version = "1.0.3"
2576
2576
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2577
2577
+
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
2578
2578
+
2579
2579
+
[[package]]
2580
2580
+
name = "hyper"
2581
2581
+
version = "1.7.0"
2582
2582
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2583
2583
+
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
2584
2584
+
dependencies = [
2585
2585
+
"atomic-waker",
2586
2586
+
"bytes 1.10.1",
2587
2587
+
"futures-channel",
2588
2588
+
"futures-core",
2589
2589
+
"http",
2590
2590
+
"http-body",
2591
2591
+
"httparse",
2592
2592
+
"httpdate",
2593
2593
+
"itoa",
2594
2594
+
"pin-project-lite",
2595
2595
+
"pin-utils",
2596
2596
+
"smallvec",
2597
2597
+
"tokio",
2598
2598
+
]
2599
2599
+
2600
2600
+
[[package]]
2601
2601
+
name = "hyper-util"
2602
2602
+
version = "0.1.17"
2603
2603
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2604
2604
+
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
2605
2605
+
dependencies = [
2606
2606
+
"bytes 1.10.1",
2607
2607
+
"futures-core",
2608
2608
+
"http",
2609
2609
+
"http-body",
2610
2610
+
"hyper",
2611
2611
+
"pin-project-lite",
2612
2612
+
"tokio",
2613
2613
+
"tower-service",
2614
2614
+
]
2615
2615
+
2616
2616
+
[[package]]
2493
2617
name = "iana-time-zone"
2494
2618
version = "0.1.64"
2495
2619
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3049
3173
]
3050
3174
3051
3175
[[package]]
3176
3176
+
name = "matchit"
3177
3177
+
version = "0.8.4"
3178
3178
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3179
3179
+
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
3180
3180
+
3181
3181
+
[[package]]
3052
3182
name = "matrixmultiply"
3053
3183
version = "0.3.10"
3054
3184
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3129
3259
"quote",
3130
3260
"syn 2.0.108",
3131
3261
]
3262
3262
+
3263
3263
+
[[package]]
3264
3264
+
name = "mime"
3265
3265
+
version = "0.3.17"
3266
3266
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3267
3267
+
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
3132
3268
3133
3269
[[package]]
3134
3270
name = "minimal-lexical"
···
4140
4276
"pico-args",
4141
4277
"rand 0.9.2",
4142
4278
"shapemaker",
4279
4279
+
"tokio",
4143
4280
]
4144
4281
4145
4282
[[package]]
···
4326
4463
]
4327
4464
4328
4465
[[package]]
4466
4466
+
name = "serde_path_to_error"
4467
4467
+
version = "0.1.20"
4468
4468
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4469
4469
+
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
4470
4470
+
dependencies = [
4471
4471
+
"itoa",
4472
4472
+
"serde",
4473
4473
+
"serde_core",
4474
4474
+
]
4475
4475
+
4476
4476
+
[[package]]
4329
4477
name = "serde_spanned"
4330
4478
version = "0.6.9"
4331
4479
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4341
4489
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
4342
4490
dependencies = [
4343
4491
"serde_core",
4492
4492
+
]
4493
4493
+
4494
4494
+
[[package]]
4495
4495
+
name = "serde_urlencoded"
4496
4496
+
version = "0.7.1"
4497
4497
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4498
4498
+
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
4499
4499
+
dependencies = [
4500
4500
+
"form_urlencoded",
4501
4501
+
"itoa",
4502
4502
+
"ryu",
4503
4503
+
"serde",
4344
4504
]
4345
4505
4346
4506
[[package]]
···
4392
4552
version = "1.2.2"
4393
4553
dependencies = [
4394
4554
"anyhow",
4555
4555
+
"axum",
4395
4556
"backtrace",
4396
4557
"cargo",
4397
4558
"chrono",
···
4691
4852
]
4692
4853
4693
4854
[[package]]
4855
4855
+
name = "sync_wrapper"
4856
4856
+
version = "1.0.2"
4857
4857
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4858
4858
+
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
4859
4859
+
4860
4860
+
[[package]]
4694
4861
name = "synstructure"
4695
4862
version = "0.13.2"
4696
4863
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4887
5054
"mio 1.1.0",
4888
5055
"pin-project-lite",
4889
5056
"signal-hook-registry",
5057
5057
+
"socket2",
4890
5058
"tokio-macros",
4891
5059
"windows-sys 0.61.2",
4892
5060
]
···
4989
5157
version = "1.0.4"
4990
5158
source = "registry+https://github.com/rust-lang/crates.io-index"
4991
5159
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
5160
5160
+
5161
5161
+
[[package]]
5162
5162
+
name = "tower"
5163
5163
+
version = "0.5.2"
5164
5164
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5165
5165
+
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
5166
5166
+
dependencies = [
5167
5167
+
"futures-core",
5168
5168
+
"futures-util",
5169
5169
+
"pin-project-lite",
5170
5170
+
"sync_wrapper",
5171
5171
+
"tokio",
5172
5172
+
"tower-layer",
5173
5173
+
"tower-service",
5174
5174
+
"tracing",
5175
5175
+
]
5176
5176
+
5177
5177
+
[[package]]
5178
5178
+
name = "tower-layer"
5179
5179
+
version = "0.3.3"
5180
5180
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5181
5181
+
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
5182
5182
+
5183
5183
+
[[package]]
5184
5184
+
name = "tower-service"
5185
5185
+
version = "0.3.3"
5186
5186
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5187
5187
+
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
4992
5188
4993
5189
[[package]]
4994
5190
name = "tracing"
···
30
30
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
31
31
32
32
[features]
33
33
-
default = ["cli", "vst", "mp4"]
33
33
+
default = ["cli", "vst", "mp4", "video-server"]
34
34
vst = [
35
35
"cli",
36
36
"rand/thread_rng",
···
49
49
]
50
50
web = ["dep:wasm-bindgen", "dep:web-sys"]
51
51
mp4 = ["dep:env_logger"]
52
52
+
video-server = ["dep:axum"]
52
53
53
54
[dependencies]
54
55
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug.git", features = [
···
105
106
serde = { version = "1.0.228", features = ["derive"] }
106
107
url = "2.5.7"
107
108
tungstenite = { version = "0.28.0", optional = true }
109
109
+
axum = { version = "0.8.6", optional = true, features = ["json"] }
108
110
109
111
110
112
[dev-dependencies]
···
43
43
// canvas.render_to_svg_file(&format!("framedump-{}.svg", ctx.frame))?;
44
44
// Ok(())
45
45
// })
46
46
-
.render("schedule-hell-backbone.mp4")
46
46
+
.encode("schedule-hell-backbone.mp4")
47
47
.unwrap();
48
48
}
49
49
···
9
9
pico-args = { version = "0.5.0", features = ["combined-flags", "eq-separator"] }
10
10
rand = "0.9.0"
11
11
shapemaker = { path = "../..", features = ["mp4"] }
12
12
+
tokio = "1.48.0"
···
21
21
}
22
22
}
23
23
24
24
-
pub fn main() -> Result<()> {
24
24
+
#[tokio::main]
25
25
+
pub async fn main() -> Result<()> {
25
26
let mut canvas = Canvas::new(vec![]);
26
27
27
28
canvas.set_grid_size(16, 9);
···
253
254
Ok(())
254
255
});
255
256
256
256
-
video.render(
257
257
-
args.free_from_str()
258
258
-
.unwrap_or(String::from("schedule-hell.mp4")),
259
259
-
)?;
257
257
+
if args.contains("--serve") {
258
258
+
video.serve("localhost:8000").await;
259
259
+
} else {
260
260
+
video.encode(
261
261
+
args.free_from_str()
262
262
+
.unwrap_or(String::from("schedule-hell.mp4")),
263
263
+
)?;
264
264
+
}
260
265
261
266
Ok(())
262
267
}
···
4
4
5
5
/// Arguments: animation progress (from 0.0 to 1.0), canvas, current ms
6
6
pub type AnimationUpdateFunction =
7
7
-
dyn Fn(f32, &mut Canvas, usize) -> anyhow::Result<()>;
7
7
+
dyn Fn(f32, &mut Canvas, usize) -> anyhow::Result<()> + Send + Sync;
8
8
9
9
/// An animation that only manipulates a single layer. The layer's render cache is automatically flushed at the end. See `AnimationUpdateFunction` for more information.
10
10
pub type LayerAnimationUpdateFunction =
11
11
-
dyn Fn(f32, &mut Layer, usize) -> anyhow::Result<()>;
11
11
+
dyn Fn(f32, &mut Layer, usize) -> anyhow::Result<()> + Send + Sync;
12
12
13
13
pub struct Animation {
14
14
pub name: String,
···
1
1
use super::animation::{AnimationUpdateFunction, LayerAnimationUpdateFunction};
2
2
-
use super::engine::{LaterHook, LaterRenderFunction};
2
2
+
use super::hooks::{LaterHook, LaterRenderFunction};
3
3
use super::Animation;
4
4
use crate::synchronization::audio::{Note, StemAtInstant};
5
5
use crate::synchronization::sync::SyncData;
···
1
1
-
use super::{context::Context, engine::milliseconds_to_timestamp, Video};
2
2
-
use crate::rendering::stringify_svg;
3
3
-
use crate::{Canvas, SVGRenderable};
1
1
+
use super::{hooks::milliseconds_to_timestamp, Video};
2
2
+
use crate::Canvas;
4
3
use anyhow::Result;
5
5
-
use indicatif::ProgressIterator;
6
4
use measure_time::debug_time;
7
5
use std::fs::File;
8
8
-
use std::io::{Read, Write};
9
9
-
use std::sync::mpsc::{Sender, SyncSender};
6
6
+
use std::io::Write;
10
7
use std::thread;
11
8
use std::time::Duration;
12
9
use std::{fs::create_dir_all, path::PathBuf};
···
53
50
.spawn()?)
54
51
}
55
52
56
56
-
pub fn render_frames(
57
57
-
&self,
58
58
-
output: SyncSender<(Duration, String)>,
59
59
-
) -> Result<usize> {
60
60
-
debug_time!("render_frames");
61
61
-
let mut written_frames_count: usize = 0;
62
62
-
let mut context = Context {
63
63
-
frame: 0,
64
64
-
beat: 0,
65
65
-
beat_fractional: 0.0,
66
66
-
timestamp: "00:00:00.000".to_string(),
67
67
-
ms: 0,
68
68
-
bpm: self.syncdata.bpm,
69
69
-
syncdata: &self.syncdata,
70
70
-
extra: AdditionalContext::default(),
71
71
-
later_hooks: vec![],
72
72
-
audiofile: self.audiofile.clone(),
73
73
-
duration_override: self.duration_override,
74
74
-
};
75
75
-
76
76
-
let mut canvas = self.initial_canvas.clone();
77
77
-
78
78
-
let mut previous_rendered_beat = 0;
79
79
-
let mut previous_rendered_frame = 0;
80
80
-
81
81
-
let render_ms_range = self.start_rendering_at + 0..self.duration_ms();
82
82
-
83
83
-
self.progress_bar.set_length(render_ms_range.len() as u64);
84
84
-
85
85
-
for _ in render_ms_range {
86
86
-
context.ms += 1_usize;
87
87
-
context.timestamp = milliseconds_to_timestamp(context.ms).to_string();
88
88
-
context.beat_fractional =
89
89
-
(context.bpm * context.ms) as f32 / (1000.0 * 60.0);
90
90
-
context.beat = context.beat_fractional as usize;
91
91
-
context.frame = self.fps * context.ms / 1000;
92
92
-
93
93
-
if context.marker() != "" {
94
94
-
self.progress_bar.println(format!(
95
95
-
"{}: marker {}",
96
96
-
context.timestamp,
97
97
-
context.marker()
98
98
-
));
99
99
-
}
100
100
-
101
101
-
if context.marker().starts_with(':') {
102
102
-
let marker_text = context.marker();
103
103
-
let commandline = marker_text.trim_start_matches(':').to_string();
104
104
-
105
105
-
for command in &self.commands {
106
106
-
if commandline.starts_with(&command.name) {
107
107
-
let args = commandline
108
108
-
.trim_start_matches(&command.name)
109
109
-
.trim()
110
110
-
.to_string();
111
111
-
(command.action)(args, &mut canvas, &mut context)?;
112
112
-
}
113
113
-
}
114
114
-
}
115
115
-
116
116
-
// Render later hooks first, so that for example animations that aren't finished yet get overwritten by next frame's hook, if the next frames touches the same object
117
117
-
// This is way better to cancel early animations such as fading out an object that appears on every note of a stem, if the next note is too close for the fade-out to finish.
118
118
-
119
119
-
let mut later_hooks_to_delete: Vec<usize> = vec![];
120
120
-
121
121
-
for (i, hook) in context.later_hooks.iter().enumerate() {
122
122
-
if (hook.when)(&canvas, &context, previous_rendered_beat) {
123
123
-
(hook.render_function)(&mut canvas, context.ms)?;
124
124
-
if hook.once {
125
125
-
later_hooks_to_delete.push(i);
126
126
-
}
127
127
-
} else if !hook.once {
128
128
-
later_hooks_to_delete.push(i);
129
129
-
}
130
130
-
}
131
131
-
132
132
-
for i in later_hooks_to_delete {
133
133
-
if i < context.later_hooks.len() {
134
134
-
context.later_hooks.remove(i);
135
135
-
}
136
136
-
}
137
137
-
138
138
-
for hook in &self.hooks {
139
139
-
if (hook.when)(
140
140
-
&canvas,
141
141
-
&context,
142
142
-
previous_rendered_beat,
143
143
-
previous_rendered_frame,
144
144
-
) {
145
145
-
(hook.render_function)(&mut canvas, &mut context)?;
146
146
-
}
147
147
-
}
148
148
-
149
149
-
if context.frame != previous_rendered_frame {
150
150
-
output.send((
151
151
-
Duration::from_millis(context.ms as _),
152
152
-
stringify_svg(canvas.render_to_svg(
153
153
-
canvas.colormap.clone(),
154
154
-
canvas.cell_size,
155
155
-
canvas.object_sizes,
156
156
-
"",
157
157
-
)?),
158
158
-
))?;
159
159
-
160
160
-
written_frames_count += 1;
161
161
-
162
162
-
previous_rendered_beat = context.beat;
163
163
-
previous_rendered_frame = context.frame;
164
164
-
}
165
165
-
}
166
166
-
167
167
-
output.send((Duration::from_millis(context.ms as _), "".to_string()))?;
168
168
-
169
169
-
Ok(written_frames_count)
170
170
-
}
171
171
-
172
172
-
pub fn render(&mut self, output_file: impl Into<PathBuf>) -> Result<()> {
173
173
-
debug_time!("render");
53
53
+
pub fn encode(&mut self, output_file: impl Into<PathBuf>) -> Result<()> {
54
54
+
debug_time!("encode");
174
55
175
56
let output_file: PathBuf = output_file.into();
176
57
···
220
101
encoder.stdin.take().unwrap().flush().unwrap();
221
102
});
222
103
223
223
-
self.render_frames(tx)?;
104
104
+
self.render_all_frames(tx)?;
224
105
225
106
encoder_thread.join().expect("Encoder thread panicked");
226
107
···
1
1
-
use super::animation::LayerAnimationUpdateFunction;
2
2
-
use super::context::Context;
3
3
-
use crate::synchronization::audio::MusicalDurationUnit;
4
4
-
use crate::synchronization::midi::MidiSynchronizer;
5
5
-
use crate::synchronization::sync::{SyncData, Syncable};
6
6
-
use crate::ui::{self, setup_progress_bar, Log as _};
7
7
-
use crate::{Canvas, ColoredObject};
8
8
-
use anyhow::Result;
9
9
-
use chrono::{DateTime, NaiveDateTime};
10
10
-
use indicatif::ProgressBar;
11
11
-
use measure_time::debug_time;
12
12
-
use std::{fmt::Formatter, panic, path::PathBuf};
13
13
-
14
14
-
pub type BeatNumber = usize;
15
15
-
pub type FrameNumber = usize;
16
16
-
pub type Millisecond = usize;
17
17
-
18
18
-
pub type RenderFunction<C> =
19
19
-
dyn Fn(&mut Canvas, &mut Context<C>) -> anyhow::Result<()>;
20
20
-
21
21
-
pub type CommandAction<C> =
22
22
-
dyn Fn(String, &mut Canvas, &mut Context<C>) -> anyhow::Result<()>;
23
23
-
24
24
-
/// Arguments: canvas, context, previous rendered beat, previous rendered frame
25
25
-
pub type HookCondition<C> =
26
26
-
dyn Fn(&Canvas, &Context<C>, BeatNumber, FrameNumber) -> bool;
27
27
-
28
28
-
/// Arguments: canvas, context, current milliseconds timestamp
29
29
-
pub type LaterRenderFunction =
30
30
-
dyn Fn(&mut Canvas, Millisecond) -> anyhow::Result<()>;
31
31
-
32
32
-
/// Arguments: canvas, context, previous rendered beat
33
33
-
pub type LaterHookCondition<C> = dyn Fn(&Canvas, &Context<C>, BeatNumber) -> bool;
34
34
-
35
35
-
pub struct Video<C> {
36
36
-
pub fps: usize,
37
37
-
pub initial_canvas: Canvas,
38
38
-
pub hooks: Vec<Hook<C>>,
39
39
-
pub commands: Vec<Box<Command<C>>>,
40
40
-
pub frames: Vec<Canvas>,
41
41
-
pub frames_output_directory: &'static str,
42
42
-
pub syncdata: SyncData,
43
43
-
pub audiofile: PathBuf,
44
44
-
pub resolution: u32,
45
45
-
pub duration_override: Option<usize>,
46
46
-
pub start_rendering_at: usize,
47
47
-
pub progress_bar: indicatif::ProgressBar,
48
48
-
}
49
49
-
50
50
-
pub struct Hook<C> {
51
51
-
pub when: Box<HookCondition<C>>,
52
52
-
pub render_function: Box<RenderFunction<C>>,
53
53
-
}
54
54
-
55
55
-
pub struct LaterHook<C> {
56
56
-
pub when: Box<LaterHookCondition<C>>,
57
57
-
pub render_function: Box<LaterRenderFunction>,
58
58
-
/// Whether the hook should be run only once
59
59
-
pub once: bool,
60
60
-
}
61
61
-
62
62
-
impl<C> std::fmt::Debug for Hook<C> {
63
63
-
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
64
64
-
f.debug_struct("Hook")
65
65
-
.field("when", &"Box<HookCondition>")
66
66
-
.field("render_function", &"Box<RenderFunction>")
67
67
-
.finish()
68
68
-
}
69
69
-
}
70
70
-
71
71
-
pub struct Command<C> {
72
72
-
pub name: String,
73
73
-
pub action: Box<CommandAction<C>>,
74
74
-
}
75
75
-
76
76
-
impl<C> std::fmt::Debug for Command<C> {
77
77
-
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
78
78
-
f.debug_struct("Command")
79
79
-
.field("name", &self.name)
80
80
-
.field("action", &"Box<CommandAction>")
81
81
-
.finish()
82
82
-
}
83
83
-
}
84
84
-
85
85
-
impl<AdditionalContext: Default> Default for Video<AdditionalContext> {
86
86
-
fn default() -> Self {
87
87
-
Self::new(Canvas::new(vec!["root"]))
88
88
-
}
89
89
-
}
90
90
-
91
91
-
impl<AdditionalContext: Default> Video<AdditionalContext> {
92
92
-
pub fn new(canvas: Canvas) -> Self {
93
93
-
Self {
94
94
-
fps: 30,
95
95
-
initial_canvas: canvas,
96
96
-
hooks: vec![],
97
97
-
commands: vec![],
98
98
-
frames: vec![],
99
99
-
frames_output_directory: "frames/",
100
100
-
resolution: 1920,
101
101
-
syncdata: SyncData::default(),
102
102
-
audiofile: PathBuf::new(),
103
103
-
duration_override: None,
104
104
-
start_rendering_at: 0,
105
105
-
progress_bar: setup_progress_bar(0, ""),
106
106
-
}
107
107
-
}
108
108
-
109
109
-
pub fn sync_audio_with(self, sync_data_path: &str) -> Self {
110
110
-
debug_time!("sync_audio_with");
111
111
-
if sync_data_path.ends_with(".mid") || sync_data_path.ends_with(".midi") {
112
112
-
let loader = MidiSynchronizer::new(sync_data_path);
113
113
-
let syncdata = loader.load(Some(&self.progress_bar));
114
114
-
self.progress_bar.finish();
115
115
-
self.progress_bar.log(
116
116
-
"Loaded",
117
117
-
&format!(
118
118
-
"{} notes from {sync_data_path}",
119
119
-
syncdata
120
120
-
.stems
121
121
-
.values()
122
122
-
.map(|v| v.notes.len())
123
123
-
.sum::<usize>(),
124
124
-
),
125
125
-
);
126
126
-
return Self { syncdata, ..self };
127
127
-
}
128
128
-
129
129
-
panic!("Unsupported sync data format");
130
130
-
}
131
131
-
132
132
-
pub fn with_hook(self, hook: Hook<AdditionalContext>) -> Self {
133
133
-
let mut hooks = self.hooks;
134
134
-
hooks.push(hook);
135
135
-
Self { hooks, ..self }
136
136
-
}
137
137
-
138
138
-
pub fn init(
139
139
-
self,
140
140
-
render_function: &'static RenderFunction<AdditionalContext>,
141
141
-
) -> Self {
142
142
-
self.with_hook(Hook {
143
143
-
when: Box::new(move |_, context, _, _| context.frame == 0),
144
144
-
render_function: Box::new(render_function),
145
145
-
})
146
146
-
}
147
147
-
148
148
-
// TODO The &'static requirement might be possibly liftable, see https://users.rust-lang.org/t/how-to-store-functions-in-structs/58089
149
149
-
pub fn on(
150
150
-
self,
151
151
-
marker_text: &'static str,
152
152
-
render_function: &'static RenderFunction<AdditionalContext>,
153
153
-
) -> Self {
154
154
-
self.with_hook(Hook {
155
155
-
when: Box::new(move |_, context, _, _| {
156
156
-
context.marker() == marker_text
157
157
-
}),
158
158
-
render_function: Box::new(render_function),
159
159
-
})
160
160
-
}
161
161
-
162
162
-
pub fn each_beat(
163
163
-
self,
164
164
-
render_function: &'static RenderFunction<AdditionalContext>,
165
165
-
) -> Self {
166
166
-
self.with_hook(Hook {
167
167
-
when: Box::new(
168
168
-
move |_,
169
169
-
context,
170
170
-
previous_rendered_beat,
171
171
-
previous_rendered_frame| {
172
172
-
previous_rendered_frame != context.frame
173
173
-
&& (context.ms == 0
174
174
-
|| previous_rendered_beat != context.beat)
175
175
-
},
176
176
-
),
177
177
-
render_function: Box::new(render_function),
178
178
-
})
179
179
-
}
180
180
-
181
181
-
pub fn every(
182
182
-
self,
183
183
-
amount: f32,
184
184
-
unit: MusicalDurationUnit,
185
185
-
render_function: &'static RenderFunction<AdditionalContext>,
186
186
-
) -> Self {
187
187
-
let beats = match unit {
188
188
-
MusicalDurationUnit::Beats => amount,
189
189
-
MusicalDurationUnit::Halfs => amount / 2.0,
190
190
-
MusicalDurationUnit::Quarters => amount / 4.0,
191
191
-
MusicalDurationUnit::Eighths => amount / 8.0,
192
192
-
MusicalDurationUnit::Sixteenths => amount / 16.0,
193
193
-
MusicalDurationUnit::Thirds => amount / 3.0,
194
194
-
};
195
195
-
196
196
-
self.with_hook(Hook {
197
197
-
when: Box::new(move |_, context, _, _| {
198
198
-
context.beat_fractional % beats < 0.01
199
199
-
}),
200
200
-
render_function: Box::new(render_function),
201
201
-
})
202
202
-
}
203
203
-
204
204
-
pub fn each_frame(
205
205
-
self,
206
206
-
render_function: &'static RenderFunction<AdditionalContext>,
207
207
-
) -> Self {
208
208
-
self.each_n_frame(1, render_function)
209
209
-
}
210
210
-
211
211
-
pub fn each_n_frame(
212
212
-
self,
213
213
-
n: usize,
214
214
-
render_function: &'static RenderFunction<AdditionalContext>,
215
215
-
) -> Self {
216
216
-
self.with_hook(Hook {
217
217
-
when: Box::new(move |_, context, _, previous_rendered_frame| {
218
218
-
context.frame != previous_rendered_frame && context.frame % n == 0
219
219
-
}),
220
220
-
render_function: Box::new(render_function),
221
221
-
})
222
222
-
}
223
223
-
224
224
-
/// threshold is a value between 0 and 1: current amplitude / max amplitude of stem
225
225
-
pub fn on_stem(
226
226
-
self,
227
227
-
stem_name: &'static str,
228
228
-
threshold: f32,
229
229
-
above_amplitude: &'static RenderFunction<AdditionalContext>,
230
230
-
below_amplitude: &'static RenderFunction<AdditionalContext>,
231
231
-
) -> Self {
232
232
-
self.with_hook(Hook {
233
233
-
when: Box::new(move |_, context, _, _| {
234
234
-
context.stem(stem_name).amplitude_relative() > threshold
235
235
-
}),
236
236
-
render_function: Box::new(above_amplitude),
237
237
-
})
238
238
-
.with_hook(Hook {
239
239
-
when: Box::new(move |_, context, _, _| {
240
240
-
context.stem(stem_name).amplitude_relative() <= threshold
241
241
-
}),
242
242
-
render_function: Box::new(below_amplitude),
243
243
-
})
244
244
-
}
245
245
-
246
246
-
/// Triggers when a note starts on one of the stems in the comma-separated list of stem names `stems`.
247
247
-
pub fn on_note(
248
248
-
self,
249
249
-
stems: &'static str,
250
250
-
render_function: &'static RenderFunction<AdditionalContext>,
251
251
-
) -> Self {
252
252
-
self.with_hook(Hook {
253
253
-
when: Box::new(move |_, ctx, _, _| {
254
254
-
stems
255
255
-
.split(',')
256
256
-
.map(|stem_name| ctx.stem(stem_name.trim()))
257
257
-
.any(|stem| stem.notes.iter().any(|note| note.is_on()))
258
258
-
}),
259
259
-
render_function: Box::new(render_function),
260
260
-
})
261
261
-
}
262
262
-
263
263
-
/// Triggers when a note stops on one of the stems in the comma-separated list of stem names `stems`.
264
264
-
pub fn on_note_end(
265
265
-
self,
266
266
-
stems: &'static str,
267
267
-
render_function: &'static RenderFunction<AdditionalContext>,
268
268
-
) -> Self {
269
269
-
self.with_hook(Hook {
270
270
-
when: Box::new(move |_, ctx, _, _| {
271
271
-
stems
272
272
-
.split(',')
273
273
-
.map(|n| ctx.stem(n.trim()))
274
274
-
.any(|stem| stem.notes.iter().any(|note| note.is_off()))
275
275
-
}),
276
276
-
render_function: Box::new(render_function),
277
277
-
})
278
278
-
}
279
279
-
280
280
-
// Adds an object using object_creation on note start and removes it on note end
281
281
-
pub fn with_note(
282
282
-
self,
283
283
-
stems: &'static str,
284
284
-
cutoff_amplitude: f32,
285
285
-
layer_name: &'static str,
286
286
-
object_name: &'static str,
287
287
-
create_object: &'static dyn Fn(
288
288
-
&Canvas,
289
289
-
&mut Context<AdditionalContext>,
290
290
-
) -> Result<ColoredObject>,
291
291
-
) -> Self {
292
292
-
self.with_hook(Hook {
293
293
-
when: Box::new(move |_, ctx, _, _| {
294
294
-
stems.split(',').any(|stem_name| {
295
295
-
ctx.stem(stem_name).notes.iter().any(|note| note.is_on())
296
296
-
})
297
297
-
}),
298
298
-
render_function: Box::new(move |canvas, ctx| {
299
299
-
let object = create_object(canvas, ctx)?;
300
300
-
canvas.layer(layer_name).set(object_name, object);
301
301
-
Ok(())
302
302
-
}),
303
303
-
})
304
304
-
.with_hook(Hook {
305
305
-
when: Box::new(move |_, ctx, _, _| {
306
306
-
stems.split(',').any(|stem_name| {
307
307
-
ctx.stem(stem_name).amplitude_relative() < cutoff_amplitude
308
308
-
|| ctx
309
309
-
.stem(stem_name)
310
310
-
.notes
311
311
-
.iter()
312
312
-
.any(|note| note.is_off())
313
313
-
})
314
314
-
}),
315
315
-
render_function: Box::new(move |canvas, _| {
316
316
-
canvas.remove_object(object_name);
317
317
-
Ok(())
318
318
-
}),
319
319
-
})
320
320
-
}
321
321
-
322
322
-
pub fn at_frame(
323
323
-
self,
324
324
-
frame: usize,
325
325
-
render_function: &'static RenderFunction<AdditionalContext>,
326
326
-
) -> Self {
327
327
-
self.with_hook(Hook {
328
328
-
when: Box::new(move |_, context, _, _| context.frame == frame),
329
329
-
render_function: Box::new(render_function),
330
330
-
})
331
331
-
}
332
332
-
333
333
-
pub fn when_remaining(
334
334
-
self,
335
335
-
seconds: usize,
336
336
-
render_function: &'static RenderFunction<AdditionalContext>,
337
337
-
) -> Self {
338
338
-
self.with_hook(Hook {
339
339
-
when: Box::new(move |_, ctx, _, _| {
340
340
-
ctx.ms >= ctx.duration_ms().max(seconds * 1000) - seconds * 1000
341
341
-
}),
342
342
-
render_function: Box::new(render_function),
343
343
-
})
344
344
-
}
345
345
-
346
346
-
pub fn at_timestamp(
347
347
-
self,
348
348
-
timestamp: &'static str,
349
349
-
render_function: &'static RenderFunction<AdditionalContext>,
350
350
-
) -> Self {
351
351
-
let hook = Hook {
352
352
-
when: Box::new(move |_, context, _, previous_rendered_frame| {
353
353
-
if previous_rendered_frame == context.frame {
354
354
-
return false;
355
355
-
}
356
356
-
let (precision, criteria_time): (&str, NaiveDateTime) =
357
357
-
if let Ok(criteria_time_parsed) =
358
358
-
NaiveDateTime::parse_from_str(timestamp, "%H:%M:%S%.3f")
359
359
-
{
360
360
-
("milliseconds", criteria_time_parsed)
361
361
-
} else if let Ok(criteria_time_parsed) =
362
362
-
NaiveDateTime::parse_from_str(timestamp, "%M:%S%.3f")
363
363
-
{
364
364
-
("milliseconds", criteria_time_parsed)
365
365
-
} else if let Ok(criteria_time_parsed) =
366
366
-
NaiveDateTime::parse_from_str(timestamp, "%S%.3f")
367
367
-
{
368
368
-
("milliseconds", criteria_time_parsed)
369
369
-
} else if let Ok(criteria_time_parsed) =
370
370
-
NaiveDateTime::parse_from_str(timestamp, "%S")
371
371
-
{
372
372
-
("seconds", criteria_time_parsed)
373
373
-
} else if let Ok(criteria_time_parsed) =
374
374
-
NaiveDateTime::parse_from_str(timestamp, "%M:%S")
375
375
-
{
376
376
-
("seconds", criteria_time_parsed)
377
377
-
} else if let Ok(criteria_time_parsed) =
378
378
-
NaiveDateTime::parse_from_str(timestamp, "%H:%M:%S")
379
379
-
{
380
380
-
("seconds", criteria_time_parsed)
381
381
-
} else {
382
382
-
panic!("Unhandled timestamp format: {}", timestamp);
383
383
-
};
384
384
-
match precision {
385
385
-
"milliseconds" => {
386
386
-
let current_time: NaiveDateTime =
387
387
-
NaiveDateTime::parse_from_str(
388
388
-
timestamp,
389
389
-
"%H:%M:%S%.3f",
390
390
-
)
391
391
-
.unwrap();
392
392
-
current_time == criteria_time
393
393
-
}
394
394
-
"seconds" => {
395
395
-
let current_time: NaiveDateTime =
396
396
-
NaiveDateTime::parse_from_str(timestamp, "%H:%M:%S")
397
397
-
.unwrap();
398
398
-
current_time == criteria_time
399
399
-
}
400
400
-
_ => panic!("Unknown precision"),
401
401
-
}
402
402
-
}),
403
403
-
render_function: Box::new(render_function),
404
404
-
};
405
405
-
self.with_hook(hook)
406
406
-
}
407
407
-
408
408
-
pub fn command(
409
409
-
self,
410
410
-
command_name: &'static str,
411
411
-
action: &'static CommandAction<AdditionalContext>,
412
412
-
) -> Self {
413
413
-
let mut commands = self.commands;
414
414
-
commands.push(Box::new(Command {
415
415
-
name: command_name.to_string(),
416
416
-
action: Box::new(action),
417
417
-
}));
418
418
-
Self { commands, ..self }
419
419
-
}
420
420
-
421
421
-
pub fn bind_amplitude(
422
422
-
self,
423
423
-
layer: &'static str,
424
424
-
stem: &'static str,
425
425
-
update: &'static LayerAnimationUpdateFunction,
426
426
-
) -> Self {
427
427
-
self.with_hook(Hook {
428
428
-
when: Box::new(move |_, _, _, _| true),
429
429
-
render_function: Box::new(move |canvas, context| {
430
430
-
let amplitude = context.stem(stem).amplitude_relative();
431
431
-
update(amplitude, canvas.layer(layer), context.ms)?;
432
432
-
canvas.layer(layer).flush();
433
433
-
Ok(())
434
434
-
}),
435
435
-
})
436
436
-
}
437
437
-
438
438
-
pub fn total_frames(&self) -> usize {
439
439
-
self.fps * (self.duration_ms() + self.start_rendering_at) / 1000
440
440
-
}
441
441
-
442
442
-
pub fn duration_ms(&self) -> usize {
443
443
-
if let Some(duration_override) = self.duration_override {
444
444
-
return duration_override;
445
445
-
}
446
446
-
447
447
-
self.syncdata
448
448
-
.stems
449
449
-
.values()
450
450
-
.map(|stem| stem.duration_ms)
451
451
-
.max()
452
452
-
.expect("No audio sync data provided. Use .sync_audio_with() to load a MIDI file, or provide a duration override.")
453
453
-
}
454
454
-
455
455
-
pub fn setup_progress_bar(&self) -> ProgressBar {
456
456
-
ui::setup_progress_bar(self.total_frames() as u64, "Rendering")
457
457
-
}
458
458
-
}
459
459
-
460
460
-
pub fn milliseconds_to_timestamp(ms: usize) -> String {
461
461
-
format!(
462
462
-
"{}",
463
463
-
DateTime::from_timestamp_millis(ms as i64)
464
464
-
.unwrap()
465
465
-
.format("%H:%M:%S%.3f")
466
466
-
)
467
467
-
}
1
1
+
use super::{context::Context, hooks::milliseconds_to_timestamp, Video};
2
2
+
use crate::rendering::stringify_svg;
3
3
+
use crate::SVGRenderable;
4
4
+
use anyhow::Result;
5
5
+
use measure_time::debug_time;
6
6
+
use std::sync::mpsc::SyncSender;
7
7
+
use std::time::Duration;
8
8
+
9
9
+
impl<AdditionalContext: Default> Video<AdditionalContext> {
10
10
+
pub fn render(
11
11
+
&self,
12
12
+
output: SyncSender<(Duration, String)>,
13
13
+
controller: impl Fn(usize) -> EngineControl,
14
14
+
) -> Result<usize> {
15
15
+
debug_time!("render");
16
16
+
17
17
+
let mut rendered_frames_count: usize = 0;
18
18
+
let mut context = Context {
19
19
+
frame: 0,
20
20
+
beat: 0,
21
21
+
beat_fractional: 0.0,
22
22
+
timestamp: "00:00:00.000".to_string(),
23
23
+
ms: 0,
24
24
+
bpm: self.syncdata.bpm,
25
25
+
syncdata: &self.syncdata,
26
26
+
extra: AdditionalContext::default(),
27
27
+
later_hooks: vec![],
28
28
+
audiofile: self.audiofile.clone(),
29
29
+
duration_override: self.duration_override,
30
30
+
};
31
31
+
32
32
+
let mut canvas = self.initial_canvas.clone();
33
33
+
34
34
+
let mut previous_rendered_beat = 0;
35
35
+
let mut previous_rendered_frame = 0;
36
36
+
37
37
+
let render_ms_range = self.start_rendering_at + 0..self.duration_ms();
38
38
+
39
39
+
self.progress_bar.set_length(render_ms_range.len() as u64);
40
40
+
41
41
+
for _ in render_ms_range {
42
42
+
context.ms += 1_usize;
43
43
+
context.timestamp = milliseconds_to_timestamp(context.ms).to_string();
44
44
+
context.beat_fractional =
45
45
+
(context.bpm * context.ms) as f32 / (1000.0 * 60.0);
46
46
+
context.beat = context.beat_fractional as usize;
47
47
+
context.frame = self.fps * context.ms / 1000;
48
48
+
49
49
+
let control = controller(context.frame);
50
50
+
51
51
+
if control.stop_rendering_beforehand() {
52
52
+
println!(
53
53
+
"Stopping rendering as requested before frame {}",
54
54
+
context.frame
55
55
+
);
56
56
+
break;
57
57
+
}
58
58
+
59
59
+
if context.marker() != "" {
60
60
+
self.progress_bar.println(format!(
61
61
+
"{}: marker {}",
62
62
+
context.timestamp,
63
63
+
context.marker()
64
64
+
));
65
65
+
}
66
66
+
67
67
+
if context.marker().starts_with(':') {
68
68
+
let marker_text = context.marker();
69
69
+
let commandline = marker_text.trim_start_matches(':').to_string();
70
70
+
71
71
+
for command in &self.commands {
72
72
+
if commandline.starts_with(&command.name) {
73
73
+
let args = commandline
74
74
+
.trim_start_matches(&command.name)
75
75
+
.trim()
76
76
+
.to_string();
77
77
+
(command.action)(args, &mut canvas, &mut context)?;
78
78
+
}
79
79
+
}
80
80
+
}
81
81
+
82
82
+
// Render later hooks first, so that for example animations that aren't finished yet get overwritten by next frame's hook, if the next frames touches the same object
83
83
+
// This is way better to cancel early animations such as fading out an object that appears on every note of a stem, if the next note is too close for the fade-out to finish.
84
84
+
85
85
+
let mut later_hooks_to_delete: Vec<usize> = vec![];
86
86
+
87
87
+
for (i, hook) in context.later_hooks.iter().enumerate() {
88
88
+
if (hook.when)(&canvas, &context, previous_rendered_beat) {
89
89
+
(hook.render_function)(&mut canvas, context.ms)?;
90
90
+
if hook.once {
91
91
+
later_hooks_to_delete.push(i);
92
92
+
}
93
93
+
} else if !hook.once {
94
94
+
later_hooks_to_delete.push(i);
95
95
+
}
96
96
+
}
97
97
+
98
98
+
for i in later_hooks_to_delete {
99
99
+
if i < context.later_hooks.len() {
100
100
+
context.later_hooks.remove(i);
101
101
+
}
102
102
+
}
103
103
+
104
104
+
for hook in &self.hooks {
105
105
+
if (hook.when)(
106
106
+
&canvas,
107
107
+
&context,
108
108
+
previous_rendered_beat,
109
109
+
previous_rendered_frame,
110
110
+
) {
111
111
+
(hook.render_function)(&mut canvas, &mut context)?;
112
112
+
}
113
113
+
}
114
114
+
115
115
+
if control.render_this_one()
116
116
+
&& context.frame != previous_rendered_frame
117
117
+
{
118
118
+
output.send((
119
119
+
Duration::from_millis(context.ms as _),
120
120
+
stringify_svg(canvas.render_to_svg(
121
121
+
canvas.colormap.clone(),
122
122
+
canvas.cell_size,
123
123
+
canvas.object_sizes,
124
124
+
"",
125
125
+
)?),
126
126
+
))?;
127
127
+
128
128
+
rendered_frames_count += 1;
129
129
+
130
130
+
previous_rendered_beat = context.beat;
131
131
+
previous_rendered_frame = context.frame;
132
132
+
}
133
133
+
134
134
+
if control.stop_rendering_afterwards() {
135
135
+
println!(
136
136
+
"Stopping rendering as requested after frame {}",
137
137
+
context.frame
138
138
+
);
139
139
+
break;
140
140
+
}
141
141
+
}
142
142
+
143
143
+
output.send((Duration::from_millis(context.ms as _), "".to_string()))?;
144
144
+
145
145
+
println!("Rendered {rendered_frames_count} frames");
146
146
+
Ok(rendered_frames_count)
147
147
+
}
148
148
+
149
149
+
pub fn render_single_frame(
150
150
+
&self,
151
151
+
frame_no: usize,
152
152
+
) -> Result<(Duration, String)> {
153
153
+
let (tx, rx) = std::sync::mpsc::sync_channel::<(Duration, String)>(2);
154
154
+
155
155
+
self.render(tx, |n| {
156
156
+
if n == frame_no {
157
157
+
println!("Rendering frame #{n}");
158
158
+
EngineControl::Finish
159
159
+
} else if n < frame_no {
160
160
+
EngineControl::Skip
161
161
+
} else {
162
162
+
EngineControl::Stop
163
163
+
}
164
164
+
})?;
165
165
+
166
166
+
println!("Waiting for rendered frame...");
167
167
+
for (timecode, svg) in rx.iter() {
168
168
+
if svg.is_empty() {
169
169
+
continue;
170
170
+
}
171
171
+
172
172
+
return Ok((timecode, svg));
173
173
+
}
174
174
+
175
175
+
return Err(anyhow::format_err!(
176
176
+
"Renderer did not output any non-empty frames"
177
177
+
));
178
178
+
}
179
179
+
180
180
+
pub fn render_all_frames(
181
181
+
&self,
182
182
+
output: SyncSender<(Duration, String)>,
183
183
+
) -> Result<usize> {
184
184
+
self.render(output, |_| EngineControl::Render)
185
185
+
}
186
186
+
}
187
187
+
188
188
+
/// Tells the rendering engine what to do with a frame
189
189
+
pub enum EngineControl {
190
190
+
/// Skip to the next frame, don't render this one
191
191
+
Skip,
192
192
+
/// Render this frame as usual
193
193
+
Render,
194
194
+
/// Render this frame and stop rendering afterwards
195
195
+
Finish,
196
196
+
/// Don't render this frame and stop rendering
197
197
+
Stop,
198
198
+
}
199
199
+
200
200
+
impl EngineControl {
201
201
+
pub fn render_this_one(&self) -> bool {
202
202
+
match self {
203
203
+
EngineControl::Render | EngineControl::Finish => true,
204
204
+
EngineControl::Skip | EngineControl::Stop => false,
205
205
+
}
206
206
+
}
207
207
+
208
208
+
pub fn stop_rendering_beforehand(&self) -> bool {
209
209
+
match self {
210
210
+
EngineControl::Stop => true,
211
211
+
_ => false,
212
212
+
}
213
213
+
}
214
214
+
215
215
+
pub fn stop_rendering_afterwards(&self) -> bool {
216
216
+
match self {
217
217
+
EngineControl::Finish => true,
218
218
+
_ => false,
219
219
+
}
220
220
+
}
221
221
+
}
···
1
1
+
use super::animation::LayerAnimationUpdateFunction;
2
2
+
use super::context::Context;
3
3
+
use crate::synchronization::audio::MusicalDurationUnit;
4
4
+
use crate::synchronization::midi::MidiSynchronizer;
5
5
+
use crate::synchronization::sync::{SyncData, Syncable};
6
6
+
use crate::ui::{self, setup_progress_bar, Log as _};
7
7
+
use crate::{Canvas, ColoredObject, Object};
8
8
+
use anyhow::Result;
9
9
+
use chrono::{DateTime, NaiveDateTime};
10
10
+
use indicatif::ProgressBar;
11
11
+
use measure_time::debug_time;
12
12
+
use std::{fmt::Formatter, panic, path::PathBuf};
13
13
+
14
14
+
pub type BeatNumber = usize;
15
15
+
pub type FrameNumber = usize;
16
16
+
pub type Millisecond = usize;
17
17
+
18
18
+
pub type RenderFunction<C> =
19
19
+
dyn Fn(&mut Canvas, &mut Context<C>) -> anyhow::Result<()> + Send + Sync;
20
20
+
21
21
+
pub type CommandAction<C> = dyn Fn(String, &mut Canvas, &mut Context<C>) -> anyhow::Result<()>
22
22
+
+ Send
23
23
+
+ Sync;
24
24
+
25
25
+
/// Arguments: canvas, context, previous rendered beat, previous rendered frame
26
26
+
pub type HookCondition<C> =
27
27
+
dyn Fn(&Canvas, &Context<C>, BeatNumber, FrameNumber) -> bool + Send + Sync;
28
28
+
29
29
+
/// Arguments: canvas, context, current milliseconds timestamp
30
30
+
pub type LaterRenderFunction =
31
31
+
dyn Fn(&mut Canvas, Millisecond) -> anyhow::Result<()> + Send + Sync;
32
32
+
33
33
+
/// Arguments: canvas, context, previous rendered beat
34
34
+
pub type LaterHookCondition<C> =
35
35
+
dyn Fn(&Canvas, &Context<C>, BeatNumber) -> bool + Send + Sync;
36
36
+
37
37
+
pub struct Video<C> {
38
38
+
pub fps: usize,
39
39
+
pub initial_canvas: Canvas,
40
40
+
pub hooks: Vec<Hook<C>>,
41
41
+
pub commands: Vec<Box<Command<C>>>,
42
42
+
pub frames: Vec<Canvas>,
43
43
+
pub frames_output_directory: &'static str,
44
44
+
pub syncdata: SyncData,
45
45
+
pub audiofile: PathBuf,
46
46
+
pub resolution: u32,
47
47
+
pub duration_override: Option<usize>,
48
48
+
pub start_rendering_at: usize,
49
49
+
pub progress_bar: indicatif::ProgressBar,
50
50
+
}
51
51
+
52
52
+
pub struct Hook<C> {
53
53
+
pub when: Box<HookCondition<C>>,
54
54
+
pub render_function: Box<RenderFunction<C>>,
55
55
+
}
56
56
+
57
57
+
pub struct LaterHook<C> {
58
58
+
pub when: Box<LaterHookCondition<C>>,
59
59
+
pub render_function: Box<LaterRenderFunction>,
60
60
+
/// Whether the hook should be run only once
61
61
+
pub once: bool,
62
62
+
}
63
63
+
64
64
+
impl<C> std::fmt::Debug for Hook<C> {
65
65
+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
66
66
+
f.debug_struct("Hook")
67
67
+
.field("when", &"Box<HookCondition>")
68
68
+
.field("render_function", &"Box<RenderFunction>")
69
69
+
.finish()
70
70
+
}
71
71
+
}
72
72
+
73
73
+
pub struct Command<C> {
74
74
+
pub name: String,
75
75
+
pub action: Box<CommandAction<C>>,
76
76
+
}
77
77
+
78
78
+
impl<C> std::fmt::Debug for Command<C> {
79
79
+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
80
80
+
f.debug_struct("Command")
81
81
+
.field("name", &self.name)
82
82
+
.field("action", &"Box<CommandAction>")
83
83
+
.finish()
84
84
+
}
85
85
+
}
86
86
+
87
87
+
impl<AdditionalContext: Default> Default for Video<AdditionalContext> {
88
88
+
fn default() -> Self {
89
89
+
Self::new(Canvas::new(vec!["root"]))
90
90
+
}
91
91
+
}
92
92
+
93
93
+
impl<AdditionalContext: Default> Video<AdditionalContext> {
94
94
+
pub fn new(canvas: Canvas) -> Self {
95
95
+
Self {
96
96
+
fps: 30,
97
97
+
initial_canvas: canvas,
98
98
+
hooks: vec![],
99
99
+
commands: vec![],
100
100
+
frames: vec![],
101
101
+
frames_output_directory: "frames/",
102
102
+
resolution: 1920,
103
103
+
syncdata: SyncData::default(),
104
104
+
audiofile: PathBuf::new(),
105
105
+
duration_override: None,
106
106
+
start_rendering_at: 0,
107
107
+
progress_bar: setup_progress_bar(0, ""),
108
108
+
}
109
109
+
}
110
110
+
111
111
+
pub fn sync_audio_with(self, sync_data_path: &str) -> Self {
112
112
+
debug_time!("sync_audio_with");
113
113
+
if sync_data_path.ends_with(".mid") || sync_data_path.ends_with(".midi") {
114
114
+
let loader = MidiSynchronizer::new(sync_data_path);
115
115
+
let syncdata = loader.load(Some(&self.progress_bar));
116
116
+
self.progress_bar.finish();
117
117
+
self.progress_bar.log(
118
118
+
"Loaded",
119
119
+
&format!(
120
120
+
"{} notes from {sync_data_path}",
121
121
+
syncdata
122
122
+
.stems
123
123
+
.values()
124
124
+
.map(|v| v.notes.len())
125
125
+
.sum::<usize>(),
126
126
+
),
127
127
+
);
128
128
+
return Self { syncdata, ..self };
129
129
+
}
130
130
+
131
131
+
panic!("Unsupported sync data format");
132
132
+
}
133
133
+
134
134
+
pub fn with_hook(self, hook: Hook<AdditionalContext>) -> Self {
135
135
+
let mut hooks = self.hooks;
136
136
+
hooks.push(hook);
137
137
+
Self { hooks, ..self }
138
138
+
}
139
139
+
140
140
+
pub fn init(
141
141
+
self,
142
142
+
render_function: &'static RenderFunction<AdditionalContext>,
143
143
+
) -> Self {
144
144
+
self.with_hook(Hook {
145
145
+
when: Box::new(move |_, context, _, _| context.frame == 0),
146
146
+
render_function: Box::new(render_function),
147
147
+
})
148
148
+
}
149
149
+
150
150
+
// TODO The &'static requirement might be possibly liftable, see https://users.rust-lang.org/t/how-to-store-functions-in-structs/58089
151
151
+
pub fn on(
152
152
+
self,
153
153
+
marker_text: &'static str,
154
154
+
render_function: &'static RenderFunction<AdditionalContext>,
155
155
+
) -> Self {
156
156
+
self.with_hook(Hook {
157
157
+
when: Box::new(move |_, context, _, _| {
158
158
+
context.marker() == marker_text
159
159
+
}),
160
160
+
render_function: Box::new(render_function),
161
161
+
})
162
162
+
}
163
163
+
164
164
+
pub fn each_beat(
165
165
+
self,
166
166
+
render_function: &'static RenderFunction<AdditionalContext>,
167
167
+
) -> Self {
168
168
+
self.with_hook(Hook {
169
169
+
when: Box::new(
170
170
+
move |_,
171
171
+
context,
172
172
+
previous_rendered_beat,
173
173
+
previous_rendered_frame| {
174
174
+
previous_rendered_frame != context.frame
175
175
+
&& (context.ms == 0
176
176
+
|| previous_rendered_beat != context.beat)
177
177
+
},
178
178
+
),
179
179
+
render_function: Box::new(render_function),
180
180
+
})
181
181
+
}
182
182
+
183
183
+
pub fn every(
184
184
+
self,
185
185
+
amount: f32,
186
186
+
unit: MusicalDurationUnit,
187
187
+
render_function: &'static RenderFunction<AdditionalContext>,
188
188
+
) -> Self {
189
189
+
let beats = match unit {
190
190
+
MusicalDurationUnit::Beats => amount,
191
191
+
MusicalDurationUnit::Halfs => amount / 2.0,
192
192
+
MusicalDurationUnit::Quarters => amount / 4.0,
193
193
+
MusicalDurationUnit::Eighths => amount / 8.0,
194
194
+
MusicalDurationUnit::Sixteenths => amount / 16.0,
195
195
+
MusicalDurationUnit::Thirds => amount / 3.0,
196
196
+
};
197
197
+
198
198
+
self.with_hook(Hook {
199
199
+
when: Box::new(move |_, context, _, _| {
200
200
+
context.beat_fractional % beats < 0.01
201
201
+
}),
202
202
+
render_function: Box::new(render_function),
203
203
+
})
204
204
+
}
205
205
+
206
206
+
pub fn each_frame(
207
207
+
self,
208
208
+
render_function: &'static RenderFunction<AdditionalContext>,
209
209
+
) -> Self {
210
210
+
self.each_n_frame(1, render_function)
211
211
+
}
212
212
+
213
213
+
pub fn each_n_frame(
214
214
+
self,
215
215
+
n: usize,
216
216
+
render_function: &'static RenderFunction<AdditionalContext>,
217
217
+
) -> Self {
218
218
+
self.with_hook(Hook {
219
219
+
when: Box::new(move |_, context, _, previous_rendered_frame| {
220
220
+
context.frame != previous_rendered_frame && context.frame % n == 0
221
221
+
}),
222
222
+
render_function: Box::new(render_function),
223
223
+
})
224
224
+
}
225
225
+
226
226
+
/// threshold is a value between 0 and 1: current amplitude / max amplitude of stem
227
227
+
pub fn on_stem(
228
228
+
self,
229
229
+
stem_name: &'static str,
230
230
+
threshold: f32,
231
231
+
above_amplitude: &'static RenderFunction<AdditionalContext>,
232
232
+
below_amplitude: &'static RenderFunction<AdditionalContext>,
233
233
+
) -> Self {
234
234
+
self.with_hook(Hook {
235
235
+
when: Box::new(move |_, context, _, _| {
236
236
+
context.stem(stem_name).amplitude_relative() > threshold
237
237
+
}),
238
238
+
render_function: Box::new(above_amplitude),
239
239
+
})
240
240
+
.with_hook(Hook {
241
241
+
when: Box::new(move |_, context, _, _| {
242
242
+
context.stem(stem_name).amplitude_relative() <= threshold
243
243
+
}),
244
244
+
render_function: Box::new(below_amplitude),
245
245
+
})
246
246
+
}
247
247
+
248
248
+
/// Triggers when a note starts on one of the stems in the comma-separated list of stem names `stems`.
249
249
+
pub fn on_note(
250
250
+
self,
251
251
+
stems: &'static str,
252
252
+
render_function: &'static RenderFunction<AdditionalContext>,
253
253
+
) -> Self {
254
254
+
self.with_hook(Hook {
255
255
+
when: Box::new(move |_, ctx, _, _| {
256
256
+
stems
257
257
+
.split(',')
258
258
+
.map(|stem_name| ctx.stem(stem_name.trim()))
259
259
+
.any(|stem| stem.notes.iter().any(|note| note.is_on()))
260
260
+
}),
261
261
+
render_function: Box::new(render_function),
262
262
+
})
263
263
+
}
264
264
+
265
265
+
/// Triggers when a note stops on one of the stems in the comma-separated list of stem names `stems`.
266
266
+
pub fn on_note_end(
267
267
+
self,
268
268
+
stems: &'static str,
269
269
+
render_function: &'static RenderFunction<AdditionalContext>,
270
270
+
) -> Self {
271
271
+
self.with_hook(Hook {
272
272
+
when: Box::new(move |_, ctx, _, _| {
273
273
+
stems
274
274
+
.split(',')
275
275
+
.map(|n| ctx.stem(n.trim()))
276
276
+
.any(|stem| stem.notes.iter().any(|note| note.is_off()))
277
277
+
}),
278
278
+
render_function: Box::new(render_function),
279
279
+
})
280
280
+
}
281
281
+
282
282
+
// Adds an object using object_creation on note start and removes it on note end
283
283
+
pub fn with_note<ObjectCreator>(
284
284
+
self,
285
285
+
stems: &'static str,
286
286
+
cutoff_amplitude: f32,
287
287
+
layer_name: &'static str,
288
288
+
object_name: &'static str,
289
289
+
create_object: &'static ObjectCreator,
290
290
+
) -> Self
291
291
+
where
292
292
+
ObjectCreator: Fn(&Canvas, &mut Context<AdditionalContext>) -> Result<ColoredObject>
293
293
+
+ Send
294
294
+
+ Sync,
295
295
+
{
296
296
+
self.with_hook(Hook {
297
297
+
when: Box::new(move |_, ctx, _, _| {
298
298
+
stems.split(',').any(|stem_name| {
299
299
+
ctx.stem(stem_name).notes.iter().any(|note| note.is_on())
300
300
+
})
301
301
+
}),
302
302
+
render_function: Box::new(move |canvas, ctx| {
303
303
+
let object = create_object(canvas, ctx)?;
304
304
+
canvas.layer(layer_name).set(object_name, object);
305
305
+
Ok(())
306
306
+
}),
307
307
+
})
308
308
+
.with_hook(Hook {
309
309
+
when: Box::new(move |_, ctx, _, _| {
310
310
+
stems.split(',').any(|stem_name| {
311
311
+
ctx.stem(stem_name).amplitude_relative() < cutoff_amplitude
312
312
+
|| ctx
313
313
+
.stem(stem_name)
314
314
+
.notes
315
315
+
.iter()
316
316
+
.any(|note| note.is_off())
317
317
+
})
318
318
+
}),
319
319
+
render_function: Box::new(move |canvas, _| {
320
320
+
canvas.remove_object(object_name);
321
321
+
Ok(())
322
322
+
}),
323
323
+
})
324
324
+
}
325
325
+
326
326
+
pub fn at_frame(
327
327
+
self,
328
328
+
frame: usize,
329
329
+
render_function: &'static RenderFunction<AdditionalContext>,
330
330
+
) -> Self {
331
331
+
self.with_hook(Hook {
332
332
+
when: Box::new(move |_, context, _, _| context.frame == frame),
333
333
+
render_function: Box::new(render_function),
334
334
+
})
335
335
+
}
336
336
+
337
337
+
pub fn when_remaining(
338
338
+
self,
339
339
+
seconds: usize,
340
340
+
render_function: &'static RenderFunction<AdditionalContext>,
341
341
+
) -> Self {
342
342
+
self.with_hook(Hook {
343
343
+
when: Box::new(move |_, ctx, _, _| {
344
344
+
ctx.ms >= ctx.duration_ms().max(seconds * 1000) - seconds * 1000
345
345
+
}),
346
346
+
render_function: Box::new(render_function),
347
347
+
})
348
348
+
}
349
349
+
350
350
+
pub fn at_timestamp(
351
351
+
self,
352
352
+
timestamp: &'static str,
353
353
+
render_function: &'static RenderFunction<AdditionalContext>,
354
354
+
) -> Self {
355
355
+
let hook = Hook {
356
356
+
when: Box::new(move |_, context, _, previous_rendered_frame| {
357
357
+
if previous_rendered_frame == context.frame {
358
358
+
return false;
359
359
+
}
360
360
+
let (precision, criteria_time): (&str, NaiveDateTime) =
361
361
+
if let Ok(criteria_time_parsed) =
362
362
+
NaiveDateTime::parse_from_str(timestamp, "%H:%M:%S%.3f")
363
363
+
{
364
364
+
("milliseconds", criteria_time_parsed)
365
365
+
} else if let Ok(criteria_time_parsed) =
366
366
+
NaiveDateTime::parse_from_str(timestamp, "%M:%S%.3f")
367
367
+
{
368
368
+
("milliseconds", criteria_time_parsed)
369
369
+
} else if let Ok(criteria_time_parsed) =
370
370
+
NaiveDateTime::parse_from_str(timestamp, "%S%.3f")
371
371
+
{
372
372
+
("milliseconds", criteria_time_parsed)
373
373
+
} else if let Ok(criteria_time_parsed) =
374
374
+
NaiveDateTime::parse_from_str(timestamp, "%S")
375
375
+
{
376
376
+
("seconds", criteria_time_parsed)
377
377
+
} else if let Ok(criteria_time_parsed) =
378
378
+
NaiveDateTime::parse_from_str(timestamp, "%M:%S")
379
379
+
{
380
380
+
("seconds", criteria_time_parsed)
381
381
+
} else if let Ok(criteria_time_parsed) =
382
382
+
NaiveDateTime::parse_from_str(timestamp, "%H:%M:%S")
383
383
+
{
384
384
+
("seconds", criteria_time_parsed)
385
385
+
} else {
386
386
+
panic!("Unhandled timestamp format: {}", timestamp);
387
387
+
};
388
388
+
match precision {
389
389
+
"milliseconds" => {
390
390
+
let current_time: NaiveDateTime =
391
391
+
NaiveDateTime::parse_from_str(
392
392
+
timestamp,
393
393
+
"%H:%M:%S%.3f",
394
394
+
)
395
395
+
.unwrap();
396
396
+
current_time == criteria_time
397
397
+
}
398
398
+
"seconds" => {
399
399
+
let current_time: NaiveDateTime =
400
400
+
NaiveDateTime::parse_from_str(timestamp, "%H:%M:%S")
401
401
+
.unwrap();
402
402
+
current_time == criteria_time
403
403
+
}
404
404
+
_ => panic!("Unknown precision"),
405
405
+
}
406
406
+
}),
407
407
+
render_function: Box::new(render_function),
408
408
+
};
409
409
+
self.with_hook(hook)
410
410
+
}
411
411
+
412
412
+
pub fn command(
413
413
+
self,
414
414
+
command_name: &'static str,
415
415
+
action: &'static CommandAction<AdditionalContext>,
416
416
+
) -> Self {
417
417
+
let mut commands = self.commands;
418
418
+
commands.push(Box::new(Command {
419
419
+
name: command_name.to_string(),
420
420
+
action: Box::new(action),
421
421
+
}));
422
422
+
Self { commands, ..self }
423
423
+
}
424
424
+
425
425
+
pub fn bind_amplitude(
426
426
+
self,
427
427
+
layer: &'static str,
428
428
+
stem: &'static str,
429
429
+
update: &'static LayerAnimationUpdateFunction,
430
430
+
) -> Self {
431
431
+
self.with_hook(Hook {
432
432
+
when: Box::new(move |_, _, _, _| true),
433
433
+
render_function: Box::new(move |canvas, context| {
434
434
+
let amplitude = context.stem(stem).amplitude_relative();
435
435
+
update(amplitude, canvas.layer(layer), context.ms)?;
436
436
+
canvas.layer(layer).flush();
437
437
+
Ok(())
438
438
+
}),
439
439
+
})
440
440
+
}
441
441
+
442
442
+
pub fn total_frames(&self) -> usize {
443
443
+
self.fps * (self.duration_ms() + self.start_rendering_at) / 1000
444
444
+
}
445
445
+
446
446
+
pub fn duration_ms(&self) -> usize {
447
447
+
if let Some(duration_override) = self.duration_override {
448
448
+
return duration_override;
449
449
+
}
450
450
+
451
451
+
self.syncdata
452
452
+
.stems
453
453
+
.values()
454
454
+
.map(|stem| stem.duration_ms)
455
455
+
.max()
456
456
+
.expect("No audio sync data provided. Use .sync_audio_with() to load a MIDI file, or provide a duration override.")
457
457
+
}
458
458
+
459
459
+
pub fn setup_progress_bar(&self) -> ProgressBar {
460
460
+
ui::setup_progress_bar(self.total_frames() as u64, "Rendering")
461
461
+
}
462
462
+
}
463
463
+
464
464
+
pub fn milliseconds_to_timestamp(ms: usize) -> String {
465
465
+
format!(
466
466
+
"{}",
467
467
+
DateTime::from_timestamp_millis(ms as i64)
468
468
+
.unwrap()
469
469
+
.format("%H:%M:%S%.3f")
470
470
+
)
471
471
+
}
···
1
1
pub mod animation;
2
2
pub mod context;
3
3
pub mod engine;
4
4
+
pub mod hooks;
4
5
5
6
#[cfg(feature = "mp4")]
6
7
pub mod encoding;
7
8
9
9
+
#[cfg(feature = "video-server")]
10
10
+
pub mod server;
11
11
+
8
12
pub use animation::Animation;
9
9
-
pub use engine::Video;
13
13
+
pub use hooks::Video;
···
1
1
+
<!DOCTYPE html>
2
2
+
<html lang="en">
3
3
+
<head>
4
4
+
<meta charset="UTF-8" />
5
5
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
+
<title>Shapemaker preview</title>
7
7
+
<script type="importmap">
8
8
+
{
9
9
+
"imports": {
10
10
+
"debounce": "https://unpkg.com/throttle-debounce@5.0.2/esm/index.js"
11
11
+
}
12
12
+
}
13
13
+
</script>
14
14
+
</head>
15
15
+
<body>
16
16
+
<div id="frame_monitor"></div>
17
17
+
<div class="controls">
18
18
+
<button style="font-family: monospace" id="play_pause">|></button>
19
19
+
<input type="range" value="0" id="requested_frame" min="1" max="300" />
20
20
+
<code id="requested_frame_number"></code>
21
21
+
</div>
22
22
+
<script type="module">
23
23
+
import { debounce } from "debounce"
24
24
+
25
25
+
const FPS =
26
26
+
parseInt(new URLSearchParams(window.location.search).get("fps")) || 30
27
27
+
const cache = new Map()
28
28
+
29
29
+
let playLoop
30
30
+
play_pause.onclick = () => {
31
31
+
if (playLoop) {
32
32
+
clearInterval(playLoop)
33
33
+
playLoop = null
34
34
+
play_pause.innerText = "|>"
35
35
+
} else {
36
36
+
play_pause.innerText = "||"
37
37
+
playLoop = setInterval(() => {
38
38
+
requested_frame.value = requested_frame.valueAsNumber + 1
39
39
+
render(requested_frame.valueAsNumber)
40
40
+
}, 1000 / FPS)
41
41
+
}
42
42
+
}
43
43
+
44
44
+
requested_frame.oninput = debounce(10, ({ target }) => {
45
45
+
render(target.valueAsNumber)
46
46
+
})
47
47
+
48
48
+
function render(frameNo) {
49
49
+
requested_frame_number.innerText = `(${frameNo})`
50
50
+
51
51
+
if (cache.has(frameNo)) {
52
52
+
requested_frame_number.innerText = frameNo
53
53
+
requested_frame_number.style.color = "magenta"
54
54
+
frame_monitor.innerHTML = cache.get(frameNo)
55
55
+
frame_monitor.style.color = "initial"
56
56
+
frame_monitor.style.opacity = "1"
57
57
+
return
58
58
+
}
59
59
+
60
60
+
frame_monitor.style.opacity = "0.5"
61
61
+
62
62
+
const start = performance.now()
63
63
+
64
64
+
fetch(`/frame/${frameNo}.svg`)
65
65
+
.then((response) =>
66
66
+
response.text().then((text) => ({
67
67
+
renderTime: Math.round(performance.now() - start),
68
68
+
ok: response.ok,
69
69
+
text,
70
70
+
}))
71
71
+
)
72
72
+
.then(({ ok, text, renderTime }) => {
73
73
+
if (ok) cache.set(frameNo, text)
74
74
+
75
75
+
if (frameNo !== requested_frame.valueAsNumber) return
76
76
+
77
77
+
requested_frame_number.innerText = `${frameNo} (in ${renderTime}ms)`
78
78
+
requested_frame_number.style.color = "initial"
79
79
+
frame_monitor.style.opacity = "1"
80
80
+
81
81
+
if (ok) {
82
82
+
frame_monitor.innerHTML = text
83
83
+
frame_monitor.style.color = "initial"
84
84
+
} else {
85
85
+
frame_monitor.innerText = text
86
86
+
frame_monitor.style.color = "red"
87
87
+
}
88
88
+
})
89
89
+
}
90
90
+
</script>
91
91
+
<style>
92
92
+
#frame_monitor {
93
93
+
width: 100vw;
94
94
+
height: calc(9 / 16 * 100vw);
95
95
+
96
96
+
svg {
97
97
+
width: 100%;
98
98
+
height: 100%;
99
99
+
}
100
100
+
}
101
101
+
102
102
+
#requested_frame_number {
103
103
+
width: 20ch;
104
104
+
}
105
105
+
106
106
+
.controls {
107
107
+
display: flex;
108
108
+
align-items: center;
109
109
+
justify-content: center;
110
110
+
gap: 1em;
111
111
+
margin-top: 3rem;
112
112
+
}
113
113
+
114
114
+
body {
115
115
+
margin: 0;
116
116
+
}
117
117
+
</style>
118
118
+
</body>
119
119
+
</html>
···
1
1
+
use crate::Video;
2
2
+
use axum::{extract::Path, response::Html, routing, Router};
3
3
+
use std::sync::Arc;
4
4
+
5
5
+
pub struct VideoServer {
6
6
+
pub router: Router,
7
7
+
}
8
8
+
9
9
+
const PREVIEW_HTML: &str = include_str!("preview.html");
10
10
+
11
11
+
impl VideoServer {
12
12
+
pub fn new<C: 'static + Default>(video: Arc<Video<C>>) -> Self {
13
13
+
video.progress_bar.finish();
14
14
+
15
15
+
let router = Router::new()
16
16
+
.route("/", routing::get(async || Html(PREVIEW_HTML)))
17
17
+
.route("/frame/{number_dot_svg}",
18
18
+
routing::get(async move |Path(number_dot_svg): Path<String>| {
19
19
+
let number: usize = number_dot_svg
20
20
+
.strip_suffix(".svg")
21
21
+
.expect("Expecting /frame/{number}.svg, didn't find .svg at the end")
22
22
+
.parse()
23
23
+
.expect("Expecting /frame/{number}.svg, couldn't parse {number} to an integer");
24
24
+
25
25
+
println!("");
26
26
+
println!("Frame number requested: {number}");
27
27
+
28
28
+
match video.render_single_frame(number) {
29
29
+
Ok((timecode, svg)) => svg.replace(
30
30
+
"</svg>",
31
31
+
&format!(r#"<meta name="shapemaker:timecode" content="{timecode:?}" /></svg>"#)
32
32
+
),
33
33
+
Err(err) => format!("{err:?}"),
34
34
+
}
35
35
+
}),
36
36
+
);
37
37
+
38
38
+
Self { router }
39
39
+
}
40
40
+
41
41
+
pub async fn start(self, address: &str) {
42
42
+
axum::serve(
43
43
+
tokio::net::TcpListener::bind(address).await.unwrap(),
44
44
+
self.router,
45
45
+
)
46
46
+
.await
47
47
+
.unwrap();
48
48
+
}
49
49
+
}
50
50
+
51
51
+
impl<C: 'static + Default> Video<C> {
52
52
+
pub async fn serve(self, address: &str) {
53
53
+
VideoServer::new(Arc::new(self)).start(address).await;
54
54
+
}
55
55
+
}