tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1// Copyright 2024 the Velato Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use super::Composition;
5use super::model::{Content, Draw, Geometry, GroupTransform, Layer, Shape, fixed};
6use kurbo::{
7 Affine, BezPath, CubicBez, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Point, QuadBez,
8 Rect,
9};
10use peniko::{Fill, Mix};
11use std::mem::swap;
12use std::ops::Range;
13
14pub trait RenderSink {
15 fn push_layer(
16 &mut self,
17 blend: impl Into<peniko::BlendMode>,
18 alpha: f32,
19 transform: Affine,
20 shape: &impl kurbo::Shape,
21 );
22
23 fn push_clip_layer(&mut self, transform: Affine, shape: &impl kurbo::Shape);
24
25 fn pop_layer(&mut self);
26
27 fn draw(
28 &mut self,
29 stroke: Option<&fixed::Stroke>,
30 transform: Affine,
31 brush: &fixed::Brush,
32 shape: &impl kurbo::Shape,
33 );
34}
35
36/// Renders a composition into a scene.
37#[derive(Debug, Default)]
38pub struct Renderer {
39 batch: Batch,
40 mask_elements: Vec<PathEl>,
41}
42
43impl Renderer {
44 /// Creates a new renderer.
45 pub fn new() -> Self {
46 Self::default()
47 }
48
49 /// Renders and appends the animation at a given frame to the provided scene.
50 pub fn append(
51 &mut self,
52 animation: &Composition,
53 frame: f64,
54 transform: Affine,
55 alpha: f64,
56 scene: &mut impl RenderSink,
57 ) {
58 self.batch.clear();
59 scene.push_clip_layer(
60 transform,
61 &Rect::new(0.0, 0.0, animation.width as _, animation.height as _),
62 );
63 for layer in animation.layers.iter().rev() {
64 if layer.is_mask {
65 continue;
66 }
67 self.render_layer(
68 animation,
69 &animation.layers,
70 layer,
71 transform,
72 alpha,
73 frame,
74 scene,
75 );
76 }
77 scene.pop_layer();
78 }
79
80 #[expect(clippy::too_many_arguments, reason = "Deferred")]
81 fn render_layer(
82 &mut self,
83 animation: &Composition,
84 layer_set: &[Layer],
85 layer: &Layer,
86 transform: Affine,
87 alpha: f64,
88 frame: f64,
89 scene: &mut impl RenderSink,
90 ) {
91 if !layer.frames.contains(&frame) {
92 return;
93 }
94 let parent_transform = transform;
95 let transform = self.compute_transform(layer_set, layer, parent_transform, frame);
96 let full_rect = Rect::new(0.0, 0.0, animation.width as f64, animation.height as f64);
97 if let Some((mode, mask_index)) = layer.mask_layer {
98 // todo: re-enable masking when it is more understood (and/or if
99 // it's currently supported in vello?) Extra layer to
100 // isolate blending for the mask
101 scene.push_layer(Mix::Normal, 1.0, parent_transform, &full_rect);
102 if let Some(mask) = layer_set.get(mask_index) {
103 self.render_layer(
104 animation,
105 layer_set,
106 mask,
107 parent_transform,
108 alpha,
109 frame,
110 scene,
111 );
112 }
113 scene.push_layer(mode, 1.0, parent_transform, &full_rect);
114 }
115 let alpha = alpha * layer.opacity.evaluate(frame) / 100.0;
116 for mask in &layer.masks {
117 mask.geometry.evaluate(frame, &mut self.mask_elements);
118 scene.push_clip_layer(transform, &self.mask_elements.as_slice());
119 self.mask_elements.clear();
120 }
121 match &layer.content {
122 Content::None => {}
123 Content::Instance {
124 name,
125 time_remap: _,
126 } => {
127 // TODO: Use time_remap
128 // let frame = time_remap
129 // .as_ref()
130 // .map(|tm| tm.evaluate(frame))
131 // .unwrap_or(frame);
132 if let Some(asset_layers) = animation.assets.get(name) {
133 let frame = frame / layer.stretch;
134 let frame_delta = -layer.start_frame / layer.stretch;
135 for asset_layer in asset_layers.iter().rev() {
136 if asset_layer.is_mask {
137 continue;
138 }
139 self.render_layer(
140 animation,
141 asset_layers,
142 asset_layer,
143 transform,
144 alpha,
145 frame + frame_delta,
146 scene,
147 );
148 }
149 }
150 }
151 Content::Shape(shapes) => {
152 self.render_shapes(shapes, transform, alpha, frame);
153 self.batch.render(scene);
154 self.batch.clear();
155 }
156 }
157 for _ in 0..layer.masks.len() + (layer.mask_layer.is_some() as usize * 2) {
158 scene.pop_layer();
159 }
160 }
161
162 fn render_shapes(&mut self, shapes: &[Shape], transform: Affine, alpha: f64, frame: f64) {
163 // Keep track of our local top of the geometry stack. Any subsequent
164 // draws are bounded by this.
165 let geometry_start = self.batch.geometries.len();
166 // Also keep track of top of draw stack for repeater evaluation.
167 let draw_start = self.batch.draws.len();
168 // Top to bottom, collect geometries and draws.
169 for shape in shapes {
170 match shape {
171 Shape::Group(shapes, group_transform) => {
172 let (group_transform, group_alpha) =
173 if let Some(GroupTransform { transform, opacity }) = group_transform {
174 (
175 transform.evaluate(frame).into_owned(),
176 opacity.evaluate(frame) / 100.0,
177 )
178 } else {
179 (Affine::IDENTITY, 1.0)
180 };
181 self.render_shapes(
182 shapes,
183 transform * group_transform,
184 alpha * group_alpha,
185 frame,
186 );
187 }
188 Shape::Geometry(geometry) => {
189 self.batch.push_geometry(geometry, transform, frame);
190 }
191 Shape::Draw(draw) => {
192 self.batch.push_draw(draw, alpha, geometry_start, frame);
193 }
194 Shape::Repeater(repeater) => {
195 let repeater = repeater.evaluate(frame);
196 self.batch
197 .repeat(repeater.as_ref(), geometry_start, draw_start);
198 }
199 Shape::Trim(trim) => {
200 let trim = trim.evaluate(frame);
201 self.batch.apply_trim(trim.as_ref(), geometry_start);
202 }
203 }
204 }
205 }
206
207 /// Computes the transform for a single layer. This currently chases the
208 /// full transform chain each time. If it becomes a bottleneck, we can
209 /// implement caching.
210 fn compute_transform(
211 &self,
212 layer_set: &[Layer],
213 layer: &Layer,
214 global_transform: Affine,
215 frame: f64,
216 ) -> Affine {
217 let mut transform = layer.transform.evaluate(frame).into_owned();
218 let mut parent_index = layer.parent;
219 let mut count = 0_usize;
220 while let Some(index) = parent_index {
221 // We don't check for cycles at import time, so this heuristic
222 // prevents infinite loops.
223 if count >= layer_set.len() {
224 break;
225 }
226 if let Some(parent) = layer_set.get(index) {
227 parent_index = parent.parent;
228 transform = parent.transform.evaluate(frame).into_owned() * transform;
229 count += 1;
230 } else {
231 break;
232 }
233 }
234 global_transform * transform
235 }
236}
237
238#[derive(Clone, Debug)]
239struct DrawData {
240 stroke: Option<fixed::Stroke>,
241 brush: fixed::Brush,
242 alpha: f64,
243 /// Range into `ShapeBatch::geometries`
244 geometry: Range<usize>,
245}
246
247impl DrawData {
248 fn new(draw: &Draw, alpha: f64, geometry: Range<usize>, frame: f64) -> Self {
249 Self {
250 stroke: draw
251 .stroke
252 .as_ref()
253 .map(|stroke| stroke.evaluate(frame).into_owned()),
254 brush: draw.brush.evaluate(1.0, frame).into_owned(),
255 alpha: alpha * draw.opacity.evaluate(frame) / 100.0,
256 geometry,
257 }
258 }
259}
260
261#[derive(Clone, Debug)]
262struct GeometryData {
263 /// Range into `ShapeBatch::elements`
264 elements: Range<usize>,
265 transform: Affine,
266}
267
268#[derive(Debug, Default)]
269struct Batch {
270 elements: Vec<PathEl>,
271 geometries: Vec<GeometryData>,
272 draws: Vec<DrawData>,
273 repeat_geometries: Vec<GeometryData>,
274 repeat_draws: Vec<DrawData>,
275 /// Length of geometries at time of most recent draw. This is
276 /// used to prevent merging into already used geometries.
277 drawn_geometry: usize,
278 trim_elements: Vec<PathEl>,
279}
280
281impl Batch {
282 fn push_geometry(&mut self, geometry: &Geometry, transform: Affine, frame: f64) {
283 // Merge with the previous geometry if possible. There are two
284 // conditions:
285 // 1. The previous geometry has not yet been referenced by a draw
286 // 2. The geometries have the same transform
287 if self.drawn_geometry < self.geometries.len()
288 && self.geometries.last().map(|last| last.transform) == Some(transform)
289 {
290 geometry.evaluate(frame, &mut self.elements);
291 self.geometries.last_mut().unwrap().elements.end = self.elements.len();
292 } else {
293 let start = self.elements.len();
294 geometry.evaluate(frame, &mut self.elements);
295 let end = self.elements.len();
296 self.geometries.push(GeometryData {
297 elements: start..end,
298 transform,
299 });
300 }
301 }
302
303 fn push_draw(&mut self, draw: &Draw, alpha: f64, geometry_start: usize, frame: f64) {
304 self.draws.push(DrawData::new(
305 draw,
306 alpha,
307 geometry_start..self.geometries.len(),
308 frame,
309 ));
310 self.drawn_geometry = self.geometries.len();
311 }
312
313 fn repeat(&mut self, repeater: &fixed::Repeater, geometry_start: usize, draw_start: usize) {
314 // First move the relevant ranges of geometries and draws into side
315 // buffers
316 self.repeat_geometries
317 .extend(self.geometries.drain(geometry_start..));
318 self.repeat_draws.extend(self.draws.drain(draw_start..));
319 // Next, repeat the geometries and apply the offset transform
320 for geometry in self.repeat_geometries.iter() {
321 for i in 0..repeater.copies {
322 let transform = repeater.transform(i);
323 let mut geometry = geometry.clone();
324 geometry.transform *= transform;
325 self.geometries.push(geometry);
326 }
327 }
328 // Finally, repeat the draws, taking into account opacity and the
329 // modified newly repeated geometry ranges
330 let start_alpha = repeater.start_opacity / 100.0;
331 let end_alpha = repeater.end_opacity / 100.0;
332 let delta_alpha = if repeater.copies > 1 {
333 // See note in Skottie: AE does not cover the full opacity range
334 (end_alpha - start_alpha) / repeater.copies as f64
335 } else {
336 0.0
337 };
338 for i in 0..repeater.copies {
339 let alpha = start_alpha + delta_alpha * i as f64;
340 if alpha <= 0.0 {
341 continue;
342 }
343 for mut draw in self.repeat_draws.iter().cloned() {
344 draw.alpha *= alpha;
345 let count = draw.geometry.end - draw.geometry.start;
346 draw.geometry.start =
347 geometry_start + (draw.geometry.start - geometry_start) * repeater.copies;
348 draw.geometry.end = draw.geometry.start + count * repeater.copies;
349 self.draws.push(draw);
350 }
351 }
352 // Clear the side buffers
353 self.repeat_geometries.clear();
354 self.repeat_draws.clear();
355 // Prevent merging until new geometries are pushed
356 self.drawn_geometry = self.geometries.len();
357 }
358
359 fn apply_trim(&mut self, trim: &fixed::Trim, geometry_start: usize) {
360 let Some((first, second)) = trim.normalized() else {
361 for geometry in &mut self.geometries[geometry_start..] {
362 geometry.elements = 0..0;
363 }
364 return;
365 };
366
367 if first.0 <= 1e-9 && first.1 >= 1.0 - 1e-9 && second.is_none() {
368 return;
369 }
370
371 for geometry in &self.geometries[..geometry_start] {
372 self.trim_elements
373 .extend(self.elements[geometry.elements.clone()].iter().cloned());
374 }
375
376 const ACCURACY: f64 = 0.1;
377
378 for geometry in &mut self.geometries[geometry_start..] {
379 let path: BezPath = self.elements[geometry.elements.clone()]
380 .iter()
381 .cloned()
382 .collect();
383 let segs: Vec<PathSeg> = path.segments().collect();
384 let total_length: f64 = segs.iter().map(|s| s.arclen(ACCURACY)).sum();
385
386 if total_length <= 0.0 || segs.is_empty() {
387 let pos = self.trim_elements.len();
388 geometry.elements = pos..pos;
389 continue;
390 }
391
392 let trim_ranges = [Some(first), second];
393 let mut trimmed = BezPath::new();
394 for range in trim_ranges.into_iter().flatten() {
395 let (norm_start, norm_end) = range;
396 let start_len = norm_start * total_length;
397 let end_len = norm_end * total_length;
398 let mut current_len = 0.0;
399 let mut need_move = true;
400
401 for seg in &segs {
402 let seg_len = seg.arclen(ACCURACY);
403 let seg_end = current_len + seg_len;
404
405 if seg_end < start_len - 1e-9 {
406 current_len = seg_end;
407 continue;
408 }
409 if current_len > end_len + 1e-9 {
410 break;
411 }
412
413 let t_start = if current_len < start_len {
414 seg.inv_arclen(start_len - current_len, ACCURACY)
415 .clamp(0.0, 1.0)
416 } else {
417 0.0
418 };
419 let t_end = if seg_end > end_len {
420 seg.inv_arclen(end_len - current_len, ACCURACY)
421 .clamp(0.0, 1.0)
422 } else {
423 1.0
424 };
425
426 if t_end > t_start + 1e-9 {
427 let sub = seg.subsegment(t_start..t_end);
428 if need_move {
429 trimmed.move_to(sub.start());
430 need_move = false;
431 }
432 match sub {
433 PathSeg::Line(l) => trimmed.line_to(l.p1),
434 PathSeg::Quad(q) => trimmed.quad_to(q.p1, q.p2),
435 PathSeg::Cubic(c) => trimmed.curve_to(c.p1, c.p2, c.p3),
436 }
437 }
438 current_len = seg_end;
439 }
440 }
441
442 let new_start = self.trim_elements.len();
443 self.trim_elements
444 .extend(trimmed.elements().iter().cloned());
445 geometry.elements = new_start..self.trim_elements.len();
446 }
447
448 let mut offset = 0;
449 for geometry in &mut self.geometries[..geometry_start] {
450 let len = geometry.elements.len();
451 geometry.elements = offset..offset + len;
452 offset += len;
453 }
454
455 swap(&mut self.elements, &mut self.trim_elements);
456 self.trim_elements.clear();
457 }
458
459 fn render(&self, scene: &mut impl RenderSink) {
460 // Process all draws in reverse
461 for draw in self.draws.iter().rev() {
462 // Some nastiness to avoid cloning the brush if unnecessary
463 let modified_brush = if draw.alpha != 1.0 {
464 Some(draw.brush.clone().multiply_alpha(draw.alpha as _))
465 } else {
466 None
467 };
468 let brush = modified_brush.as_ref().unwrap_or(&draw.brush);
469 for geometry in self.geometries[draw.geometry.clone()].iter() {
470 let path = &self.elements[geometry.elements.clone()];
471 let transform = geometry.transform;
472 scene.draw(draw.stroke.as_ref(), transform, brush, &path);
473 }
474 }
475 }
476
477 fn clear(&mut self) {
478 self.elements.clear();
479 self.geometries.clear();
480 self.draws.clear();
481 self.repeat_geometries.clear();
482 self.repeat_draws.clear();
483 self.drawn_geometry = 0;
484 self.trim_elements.clear();
485 }
486}