use super::frames::adaptive_samples; use super::opts::{LayerTiming, Through}; use super::{ Arc, BlendMode, Buf, Comp, EvalCtx, Image, IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, Layer, LayerSource, Mat3, MatteKind, ParallelIterator, Quality, Region, Renderer, Sampling, Tick, WarpOpts, cache, color, composite_warp_changed, masks, source_size, styles, vec2, }; /// Blend policy for placing a layer, including optional output-depth quantization. struct PlacementBlend { mode: BlendMode, opacity: f32, quantize: Option, } /// A motion-blur shutter: the comp's, or for a collapsed precomp's layers the outermost /// containing comp's (they are drawn into its frames). #[derive(Clone, Copy)] struct Shutter { /// Shutter angle and phase as fractions of a frame. angle: f64, phase: f64, /// Frame duration (seconds). frame: f64, /// Samples Per Frame and Adaptive Sample Limit. samples: u32, limit: u32, } impl Shutter { /// Seconds from the frame time to sub-sample `i` of `n`, spread over the open shutter. fn offset(&self, i: usize, n: usize) -> f64 { let f = if n > 1 { i as f64 / (n - 1) as f64 } else { 0.0 }; (self.phase + self.angle * f) * self.frame } } impl<'a> Renderer<'a> { /// The layer with its styles: exterior passes and body (cached per pass). pub(crate) fn styled(&self, ctx: &EvalCtx, layer: &Layer, mut timing: Option<&mut LayerTiming>) -> Option { let (content, ckey) = self.content_buf_timed(ctx, layer, timing.as_deref_mut())?; if !styles::active(ctx, layer) { return Some(StyledLayer { passes: vec![], body: content.clone(), content, key: ckey, ckey, plain: true }); } let modes = styles::pass_modes(ctx, layer); let key = ckey.map(|k| cache::styles_key(ctx, layer, k)); if let (Some(c), Some(k)) = (self.cache, key) && let Some(body) = c.get(k) { let passes: Option> = modes.iter().enumerate().map(|(i, m)| c.get(cache::derive(k, i as u64 + 1)).map(|b| (b, *m))).collect(); if let Some(passes) = passes { return Some(StyledLayer { passes, body, content, key, ckey, plain: false }); } } let t0 = web_time::Instant::now(); let size = source_size(self.project, layer); let layer_size = if size.0 == 0 { [ctx.comp.width as f64, ctx.comp.height as f64] } else { [size.0 as f64, size.1 as f64] }; let st = styles::render(ctx, layer, &content, layer_size); if let Some(t) = timing { t.cached = false; t.effects.push(("layerStyles".into(), t0.elapsed().as_secs_f64() * 1e3)); } let mut body = Arc::new(st.body); let mut passes: Vec<_> = st.passes.into_iter().map(|(b, m)| (Arc::new(b), m)).collect(); // The buffers the cache keeps (another worker's, if it kept them first). if let (Some(c), Some(k)) = (self.cache_to_keep(), key) { body = c.insert(k, body); for (i, (b, _)) in passes.iter_mut().enumerate() { *b = c.insert(cache::derive(k, i as u64 + 1), b.clone()); } } Some(StyledLayer { passes, body, content, key, ckey, plain: false }) } /// The styled layer's buffers in the blending space. pub(crate) fn styled_to_blend(&self, st: StyledLayer) -> StyledLayer { if self.pipe.to_blend().is_none() { return st; } let body = self.to_blend(st.body, st.key); let content = if st.plain { body.clone() } else { self.to_blend(st.content, st.ckey) }; let passes = st.passes.into_iter().enumerate().map(|(i, (b, m))| (self.to_blend(b, st.key.map(|k| cache::derive(k, i as u64 + 1))), m)).collect(); StyledLayer { passes, body, content, ..st } } pub(crate) fn sampling(&self, layer: &Layer) -> effectcraft_raster::Sampling { if self.quality(layer) == Quality::Draft { // Draft quality: no interpolation (nearest neighbour), as After Effects' Draft. effectcraft_raster::Sampling::Nearest } else if self.opts.draft { effectcraft_raster::Sampling::Bilinear } else if layer.switches.sampling == Sampling::Bicubic { effectcraft_raster::Sampling::Bicubic } else { effectcraft_raster::Sampling::Bilinear } } /// The region of interest of a top-level 2D frame: (x, y) offset of the output in output /// pixels and its size. `None` renders the whole comp (no ROI, or a nested comp). pub(crate) fn roi_offset(&self) -> Option<(f64, f64, u32, u32)> { let r = self.opts.roi.filter(|_| self.top)?; let s = self.opts.scale; if r[2] <= 0.0 || r[3] <= 0.0 { return None; } Some(((r[0] * s).round(), (r[1] * s).round(), ((r[2] * s).round() as u32).max(1), ((r[3] * s).round() as u32).max(1))) } /// The region of interest's offset in output pixels for 3D projection (0 without one). pub(crate) fn out_offset(&self) -> (f64, f64) { self.roi_offset().map_or((0.0, 0.0), |(x, y, _, _)| (x, y)) } /// Scaled comp pixel → output pixel: the region of interest's offset. fn out_matrix(&self) -> Mat3 { match self.roi_offset() { Some((x, y, _, _)) => Mat3::translate(vec2(-x, -y)), None => Mat3::IDENTITY, } } /// Buffer pixel → output pixel matrix for the layer at the context time. fn buf_matrix(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf) -> Mat3 { let (l2c, _) = ctx.layer_to_comp(layer); self.place_matrix(self.outer.map_or(l2c, |o| o * l2c), buf) } /// Buffer pixel → output pixel matrix for a layer → (top-level) comp matrix. fn place_matrix(&self, l2c: Mat3, buf: &Buf) -> Mat3 { let s = self.opts.scale; self.out_matrix() * Mat3::scale(vec2(s, s)) * l2c * Mat3::scale(vec2(1.0 / buf.scale, 1.0 / buf.scale)) * Mat3::translate(vec2(-buf.offset[0], -buf.offset[1])) } /// The collapsed precomp layers this comp is drawn through, outermost first. fn links(&self) -> impl Iterator> { self.through.iter().map_while(Option::as_ref) } /// The shutter of a layer's motion blur: its comp's, or for a collapsed precomp's layers the /// outermost containing comp's. fn shutter(&self, ctx: &EvalCtx) -> Shutter { let c: &Comp = match self.links().next() { Some(l) => l.comp, None => ctx.comp, }; Shutter { angle: c.shutter_angle / 360.0, phase: c.shutter_phase / 360.0, frame: c.frame_duration().seconds(), samples: c.motion_blur_samples, limit: c.motion_blur_adaptive_limit, } } /// Whether a layer moves within the shutter: its own motion blur, or that of a collapsed /// precomp layer it is drawn through. fn blurred(&self, ctx: &EvalCtx, layer: &Layer) -> bool { self.mb_on(ctx, layer) || (!self.through_lost && self.links().any(|l| l.blur)) } /// The collapsed precomps' matrix (nested comp → the frame's comp) and the nested comp's time /// `dt` seconds into the outermost comp's shutter: each precomp layer moves if it is motion /// blurred, and times map down through each one (stretch, time remapping). fn outer_at(&self, ctx: &EvalCtx, dt: f64) -> (Option, Tick) { let shift = Tick::from_seconds_f64(dt); let mut outer: Option = None; let mut t: Option = None; for l in self.links() { let at = EvalCtx { comp_id: l.comp_id, comp: l.comp, time: t.unwrap_or(l.time + shift), ..*ctx }; let (m, _) = if l.blur { at.layer_to_comp(l.layer) } else { EvalCtx { time: l.time, ..at }.layer_to_comp(l.layer) }; outer = Some(outer.map_or(m, |o| o * m)); t = Some(at.nested_time(l.layer)); } (outer, t.unwrap_or(ctx.time + shift)) } /// [`Self::buf_matrix`] for the motion-blur sub-sample `dt` seconds into the shutter: the /// layer moves if it is motion blurred, the collapsed precomps above it if theirs are. fn buf_matrix_at(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf, dt: f64) -> Mat3 { if self.through_lost || self.links().next().is_none() { return self.buf_matrix(&ctx.at(ctx.time + Tick::from_seconds_f64(dt)), layer, buf); } let (outer, t) = self.outer_at(ctx, dt); let lctx = if self.mb_on(ctx, layer) { ctx.at(t) } else { *ctx }; let (l2c, _) = lctx.layer_to_comp(layer); self.place_matrix(outer.map_or(l2c, |o| o * l2c), buf) } /// Shape and text content that animates within the shutter (paths, Trim Paths, text /// animators…) of a motion-blurred layer: the times its source is drawn at and averaged, /// Samples Per Frame of them, as After Effects does for shape layers. `None` when the content /// holds still (compared through its cache key, which leaves the transform out). pub(crate) fn content_times(&self, ctx: &EvalCtx, layer: &Layer) -> Option> { if !matches!(layer.source, LayerSource::Shape | LayerSource::Text) || !self.mb_on(ctx, layer) { return None; } let sh = self.shutter(ctx); if sh.angle <= 0.0 { return None; } let n = if self.opts.draft { 4 } else { (sh.samples as usize).clamp(2, 64) }; let times: Vec = (0..n).map(|i| self.outer_at(ctx, sh.offset(i, n)).1).collect(); let key = |t: Tick| cache::layer_key(&ctx.at(t), layer, 1.0, false, false); let k0 = key(*times.first()?)?; let moves = [times.get(n / 2), times.last()].into_iter().flatten().any(|t| key(*t) != Some(k0)); moves.then_some(times) } /// The layer's source drawn at each of `times` and averaged (premultiplied) on the union of /// their bounds. Content that also changes its raster scale within the shutter, or would need /// an outsized buffer, is drawn once. pub(crate) fn blurred_source(&self, ctx: &EvalCtx, layer: &Layer, times: &[Tick]) -> Option { let bufs: Vec = times.iter().filter_map(|t| self.source(&ctx.at(*t), layer)).collect(); let scale = bufs.first()?.scale; // Pixel = layer point × scale + offset: with the largest offset every buffer lands at // x + (off - its offset) ≥ 0. let off = [0, 1].map(|i| bufs.iter().map(|b| b.offset[i]).fold(f64::MIN, f64::max)); let w = bufs.iter().map(|b| off[0] - b.offset[0] + b.img.width as f64).fold(0.0, f64::max).ceil(); let h = bufs.iter().map(|b| off[1] - b.offset[1] + b.img.height as f64).fold(0.0, f64::max).ceil(); if bufs.iter().any(|b| (b.scale - scale).abs() > 1e-9) || !(w * h).is_finite() || w * h > (1u64 << 26) as f64 { return self.source(ctx, layer); } let mut acc = Image::new(w as u32, h as u32); let k = 1.0 / bufs.len() as f32; for b in &bufs { let m = Mat3::translate(vec2(off[0] - b.offset[0], off[1] - b.offset[1])); effectcraft_raster::accumulate_warp(&mut acc, &b.img, &m, effectcraft_raster::Sampling::Bilinear, k); } Some(Buf { img: acc, offset: off, scale }) } /// A processed-layer cache key that also covers content motion blur (the averaged source /// depends on the content at every sub-sample). pub(crate) fn content_key(&self, ctx: &EvalCtx, layer: &Layer, key: Option) -> Option { let k = key?; let Some(times) = self.content_times(ctx, layer) else { return Some(k) }; times.iter().try_fold(cache::derive(k, 0x6d62_6c72), |d, t| Some(cache::derive(d, cache::layer_key(&ctx.at(*t), layer, 1.0, false, false)?))) } /// Whether a layer is motion blurred: the render options, the comp's switch and the layer's /// switch (limited by the precomp layers above when switches affect nested comps). Layer /// motion, mask motion blur, effects that read the shutter and Advanced 3D all ask this. pub(crate) fn mb_on(&self, ctx: &EvalCtx, layer: &Layer) -> bool { self.opts.motion_blur && ctx.comp.enable_motion_blur && self.layer_motion_blur(layer) } /// What a layer's mask motion blur may do in this render (see [`masks::MaskBlur`]). pub(crate) fn mask_blur(&self, ctx: &EvalCtx, layer: &Layer) -> masks::MaskBlur { masks::MaskBlur { allowed: self.opts.motion_blur && ctx.comp.enable_motion_blur && self.inherited.is_none_or(|(_, mb)| mb), layer: self.layer_motion_blur(layer), samples: if self.opts.draft { 4 } else { ctx.comp.motion_blur_samples as usize }, } } /// Motion-blur sub-samples for a layer buffer (1 = no motion blur). As in After Effects, a 2D /// layer gets about one sample per output pixel its corners travel during the shutter, /// at least Samples Per Frame and at most the comp's Adaptive Sample Limit; a layer that /// does not move within the shutter is drawn once. fn mb_samples(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf) -> usize { if !self.blurred(ctx, layer) { return 1; } let travel = self.shutter_travel(ctx, layer, buf); if travel < 1e-3 { return 1; } if self.opts.draft { return 4; } let sh = self.shutter(ctx); adaptive_samples(travel, sh.samples, sh.limit) } /// Largest distance (output pixels) a corner of the layer buffer moves during the shutter, /// measured along the path at a few points. fn shutter_travel(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf) -> f64 { let sh = self.shutter(ctx); let (w, h) = (buf.img.width as f64, buf.img.height as f64); let corners = [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h), (w * 0.5, h * 0.5)]; const PROBES: usize = 8; let mut prev: Option> = None; let mut travel = vec![0.0f64; corners.len()]; for i in 0..PROBES { let m = self.buf_matrix_at(ctx, layer, buf, sh.offset(i, PROBES)); let pts: Vec<_> = corners.iter().map(|&(x, y)| m.apply(vec2(x, y))).collect(); if let Some(p) = &prev { for (k, (a, b)) in p.iter().zip(&pts).enumerate() { travel[k] += (b.x - a.x).hypot(b.y - a.y); } } prev = Some(pts); } travel.into_iter().fold(0.0, f64::max) } /// Canvas pixels that compositing `buf` for `layer` can change: the transformed buffer's /// bounds plus the resampling footprint. Stencil/silhouette modes, motion blur and /// perspective fall back to the whole canvas. /// /// A quantised buffer placed pixel-exactly (integer translation, Normal, 100%) over a blank /// canvas leaves exactly its own (quantised) pixels: nothing to re-quantise. fn footprint(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf, opacity: f32, blank: bool) -> Region { if layer.blend_mode.is_stencil() || self.mb_samples(ctx, layer, buf) > 1 { return Region::Full; } let m = self.buf_matrix(ctx, layer, buf); if !m.is_affine() { return Region::Full; } let matte = layer.track_matte.is_some_and(|tm| tm.layer != layer.id && ctx.comp.layer(tm.layer).is_some()); let k = &m.0; let integral = |v: f64| v == v.round(); if blank && layer.blend_mode == BlendMode::Normal && opacity == 1.0 && !matte && !layer.preserve_transparency && self.pipe.to_blend().is_none() && k[0][0] == 1.0 && k[1][1] == 1.0 && k[0][1] == 0.0 && k[1][0] == 0.0 && integral(k[0][2]) && integral(k[1][2]) { return Region::Empty; } let (w, h) = (buf.img.width as f64, buf.img.height as f64); let c = [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)].map(|(x, y)| m.apply(vec2(x, y))); let (x0, x1) = (c.iter().map(|p| p.x).fold(f64::INFINITY, f64::min), c.iter().map(|p| p.x).fold(f64::NEG_INFINITY, f64::max)); let (y0, y1) = (c.iter().map(|p| p.y).fold(f64::INFINITY, f64::min), c.iter().map(|p| p.y).fold(f64::NEG_INFINITY, f64::max)); color::PxRect::covering(x0, y0, x1, y1, 3.0).map_or(Region::Full, Region::Rect) } /// How a processed layer buffer lands in the output: one buffer → output matrix, or one per /// motion-blur sub-sample (averaged), with the layer's sampling filter and dissolve seed. pub fn placement(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf) -> Placement { let samples = self.mb_samples(ctx, layer, buf); let (sampling, seed) = (self.sampling(layer), layer.id.0 as u32); if samples <= 1 { return Placement { matrices: vec![self.buf_matrix(ctx, layer, buf)], sampling, seed }; } let sh = self.shutter(ctx); let matrices = (0..samples).map(|i| self.buf_matrix_at(ctx, layer, buf, sh.offset(i, samples))).collect(); Placement { matrices, sampling, seed } } /// Draw a processed layer into `target` (transform, motion blur) with `mode`/`opacity`. pub(crate) fn place(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf, target: &mut Image, mode: BlendMode, opacity: f32) { self.place_changed(ctx, layer, buf, target, PlacementBlend { mode, opacity, quantize: None }); } /// [`Self::place`], returning the target pixels it changed when the warp tracked them (then /// also quantised to `quantize` levels; see [`composite_warp_changed`]). fn place_changed(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf, target: &mut Image, blend: PlacementBlend) -> Option<[u32; 4]> { let PlacementBlend { mode, opacity, quantize } = blend; let pl = self.placement(ctx, layer, buf); let opts = WarpOpts { sampling: pl.sampling, opacity, mode, seed: pl.seed, clip: None }; if let [m] = pl.matrices.as_slice() { return composite_warp_changed(target, &buf.img, m, &opts, quantize); } // Sub-samples sum straight into one buffer (each warp is row-parallel). let mut acc = Image::new(target.width, target.height); let k = 1.0 / pl.matrices.len() as f32; for m in &pl.matrices { effectcraft_raster::accumulate_warp(&mut acc, &buf.img, m, opts.sampling, k); } target.blend_from(&acc, mode, opacity, pl.seed); None } /// Draw a 2D layer into `canvas`; returns the region it may have changed. pub(crate) fn draw_layer(&self, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image, blank: bool) -> Region { let opacity = ctx.opacity(layer) as f32 * self.opacity_mul; if self.quality(layer) == Quality::Wireframe && !layer.switches.adjustment { self.draw_wireframe(ctx, layer, canvas); return Region::Full; } if opacity <= 0.0 && !layer.switches.adjustment { return Region::Empty; } if layer.switches.adjustment { self.draw_adjustment(ctx, layer, canvas, opacity); return Region::Full; } let mut timing = self.profile.map(|_| LayerTiming { depth: self.depth, layer: layer.name.clone(), ..Default::default() }); let t0 = web_time::Instant::now(); let Some(st) = self.styled(ctx, layer, timing.as_mut()) else { return Region::Empty }; let st = self.styled_to_blend(st); let t1 = web_time::Instant::now(); let changed = if st.plain { // Pixels the warp tracks are quantised as it blends them (nothing left to do); the // footprint is still needed when it doesn't, or when no pixel needs quantising. let footprint = self.footprint(ctx, layer, &st.body, opacity, blank); let quantize = if footprint == Region::Empty { None } else { self.pipe.levels }; match self.composite_layer(ctx, layer, &st.body, canvas, opacity, quantize) { Some(_) if quantize.is_some() => Region::Empty, drawn => narrow(footprint, drawn), } } else { self.composite_styled(ctx, layer, &st, canvas, opacity); Region::Full }; if let (Some(prof), Some(mut timing)) = (self.profile, timing) { timing.process_ms = (t1 - t0).as_secs_f64() * 1e3; timing.composite_ms = t1.elapsed().as_secs_f64() * 1e3; if let Ok(mut v) = prof.lock() { v.push(timing); } } changed } /// Composite a styled layer: exterior passes with their own modes, then the body with the /// layer's mode; the layer's opacity fades the whole stack. Knockout clears what is below the /// layer's content; switched-off R/G/B channels keep the values below. fn composite_styled(&self, ctx: &EvalCtx, layer: &Layer, st: &StyledLayer, canvas: &mut Image, opacity: f32) { let bl = styles::blending(ctx, layer); let matte = layer.track_matte.is_some_and(|tm| tm.layer != layer.id && ctx.comp.layer(tm.layer).is_some()); if matte || layer.preserve_transparency { let mut iso = Image::new(canvas.width, canvas.height); for (b, m) in &st.passes { self.place(ctx, layer, b, &mut iso, *m, 1.0); } self.place(ctx, layer, &st.body, &mut iso, BlendMode::Normal, 1.0); self.composite_iso(ctx, layer, iso, canvas, opacity); return; } let mut tmp = canvas.clone(); if bl.knockout > 0 { let mut k = Image::new(canvas.width, canvas.height); self.place(ctx, layer, &st.content, &mut k, BlendMode::Normal, 1.0); tmp.data.par_iter_mut().zip(k.data.par_iter()).for_each(|(p, q)| { let keep = 1.0 - q[3].clamp(0.0, 1.0); for c in p.iter_mut() { *c *= keep; } }); } for (b, m) in &st.passes { self.place(ctx, layer, b, &mut tmp, *m, 1.0); } self.place(ctx, layer, &st.body, &mut tmp, layer.blend_mode, 1.0); let ch = bl.channels; canvas.data.par_iter_mut().zip(tmp.data.par_iter()).for_each(|(c, t)| { for i in 0..4 { let v = if i < 3 && !ch[i] { c[i] } else { t[i] }; c[i] += (v - c[i]) * opacity; } }); } /// Composite a plain layer; returns the canvas pixels it changed when they are known (then /// also quantised to `quantize` levels). fn composite_layer(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf, canvas: &mut Image, opacity: f32, quantize: Option) -> Option<[u32; 4]> { let matte = layer.track_matte.is_some_and(|tm| tm.layer != layer.id && ctx.comp.layer(tm.layer).is_some()); if !matte && !layer.preserve_transparency { return self.place_changed(ctx, layer, buf, canvas, PlacementBlend { mode: layer.blend_mode, opacity, quantize }); } // Render in isolation, then matte / preserve transparency, then blend. let mut iso = Image::new(canvas.width, canvas.height); self.place(ctx, layer, buf, &mut iso, BlendMode::Normal, 1.0); self.composite_iso(ctx, layer, iso, canvas, opacity); None } /// Apply the track matte / preserve transparency to an isolated layer render, then blend. pub(crate) fn composite_iso(&self, ctx: &EvalCtx, layer: &Layer, iso: Image, canvas: &mut Image, opacity: f32) { self.composite_iso_with(ctx, layer, iso, canvas, opacity, None); } /// [`Self::composite_iso`] with the track matte's pixels already drawn (`matte`, e.g. a 3D /// matte seen through the camera); otherwise the matte layer is placed in 2D. pub(crate) fn composite_iso_with(&self, ctx: &EvalCtx, layer: &Layer, mut iso: Image, canvas: &mut Image, opacity: f32, matte_img: Option) { self.apply_track_matte(ctx, layer, &mut iso, matte_img); if layer.preserve_transparency { iso.data.par_iter_mut().zip(canvas.data.par_iter()).for_each(|(p, q)| { for c in p.iter_mut() { *c *= q[3]; } }); } canvas.blend_from(&iso, layer.blend_mode, opacity, layer.id.0 as u32); } /// Multiply `img` (comp-sized) by the layer's track matte, if it has one; `matte_img` as in /// [`Self::composite_iso_with`]. fn apply_track_matte(&self, ctx: &EvalCtx, layer: &Layer, img: &mut Image, matte_img: Option) { let matte = layer.track_matte.and_then(|tm| ctx.comp.layer(tm.layer).filter(|m| m.id != layer.id).map(|m| (m, tm.kind))); if let Some((m, kind)) = matte { let mimg = match matte_img { Some(i) if i.width == img.width && i.height == img.height => i, _ => { let mut mimg = Image::new(img.width, img.height); if m.is_active_at(ctx.time) && let Some(mb) = self.layer_buf(ctx, m) { let mo = ctx.opacity(m) as f32; self.place(ctx, m, &mb, &mut mimg, BlendMode::Normal, mo); } mimg } }; img.data.par_iter_mut().zip(mimg.data.par_iter()).for_each(|(p, q)| { let k = match kind { MatteKind::Alpha => q[3], MatteKind::AlphaInverted => 1.0 - q[3], MatteKind::Luma => effectcraft_color::luminance(q[0], q[1], q[2]), MatteKind::LumaInverted => 1.0 - effectcraft_color::luminance(q[0], q[1], q[2]), } .clamp(0.0, 1.0); for c in p.iter_mut() { *c *= k; } }); } } /// Adjustment layer: effects applied to the comp below, limited to the layer's footprint /// and its track matte. fn draw_adjustment(&self, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image, opacity: f32) { let Some(mut foot) = self.source(ctx, layer) else { return }; let _ = masks::apply(ctx, layer, &mut foot, self.mask_blur(ctx, layer)); let mut matte = Image::new(canvas.width, canvas.height); self.place(ctx, layer, &foot, &mut matte, BlendMode::Normal, 1.0); self.apply_track_matte(ctx, layer, &mut matte, None); let o = self.roi_offset().map(|(x, y, _, _)| [-x, -y]).unwrap_or([0.0; 2]); let mut below = Buf { img: canvas.clone(), offset: o, scale: self.opts.scale }; // Effects run in the working space, not the linear blending space. if let Some(c) = self.pipe.from_blend() { color::convert(&mut below.img, &c); } let mut adjusted = self.apply_effects(ctx, layer, below, true); // An effect that changed the frame (Motion Tile's Output size): back onto the canvas. if let Some((dx, dy)) = effectcraft_effects::adjustment_crop(adjusted.offset, o, (adjusted.img.width, adjusted.img.height), (canvas.width, canvas.height)) { adjusted.img = adjusted.img.crop(dx, dy, canvas.width, canvas.height); } if let Some(c) = self.pipe.to_blend() { color::convert(&mut adjusted.img, &c); } if adjusted.img.width != canvas.width || adjusted.img.height != canvas.height { return; } canvas.data.par_iter_mut().zip(adjusted.img.data.par_iter()).zip(matte.data.par_iter()).for_each(|((c, a), m)| { let k = m[3] * opacity; for i in 0..4 { c[i] += (a[i] - c[i]) * k; } }); } } /// A layer ready to composite (see [`Renderer::styled_layer`]). pub struct StyledLayer { /// Exterior style passes (Drop Shadow, Outer Glow) with their blend modes. pub passes: Vec<(Arc, BlendMode)>, pub body: Arc, /// Pre-style pixels (knockout shape). pub content: Arc, /// Styled cache key (the content key when plain). pub(crate) key: Option, /// Content cache key. ckey: Option, /// No layer styles: composite `body` the usual way. pub plain: bool, } /// Where a layer buffer lands (see [`Renderer::placement`]). #[derive(Clone, Debug)] pub struct Placement { /// Buffer pixel → output pixel; several = motion-blur sub-samples (equal weights). pub matrices: Vec, pub sampling: effectcraft_raster::Sampling, /// Dissolve noise seed. pub seed: u32, } /// The part of `footprint` a warp actually changed (`drawn`, see [`composite_warp_changed`]): /// pixels it left alone are still quantised. fn narrow(footprint: Region, drawn: Option<[u32; 4]>) -> Region { let Some([x0, y0, x1, y1]) = drawn else { return footprint }; if x0 >= x1 || y0 >= y1 { return Region::Empty; } let r = color::PxRect { x0, y0, x1, y1 }; match footprint { Region::Empty => Region::Empty, Region::Full => Region::Rect(r), Region::Rect(f) => { let q = color::PxRect { x0: r.x0.max(f.x0), y0: r.y0.max(f.y0), x1: r.x1.min(f.x1), y1: r.y1.min(f.y1) }; if q.x0 >= q.x1 || q.y0 >= q.y1 { Region::Empty } else { Region::Rect(q) } } } }