use super::footage::NoFootage; use super::opts::RenderOpts; use super::{EvalCtx, Footage, FrameBlend, FrameRate, Image, ItemId, Layer, LayerSource, Project, Renderer, TICKS_PER_SECOND, Tick, shapes, text}; /// The layer's frame blending mode, if the comp's Enable Frame Blending switch is on. /// One field of an interlaced frame as a full frame: the field's lines (`parity` 0 = even / /// upper, 1 = odd / lower) kept, the other lines interpolated from their neighbours. pub fn field_frame(img: &Image, parity: usize) -> Image { let (w, h) = (img.width as usize, img.height as usize); let mut out = img.clone(); if h < 2 { return out; } for y in 0..h { if y % 2 == parity { continue; } let above = y.checked_sub(1); let below = (y + 1 < h).then_some(y + 1); for x in 0..w { let px = match (above, below) { (Some(a), Some(b)) => { let (p, q) = (img.data[a * w + x], img.data[b * w + x]); [(p[0] + q[0]) * 0.5, (p[1] + q[1]) * 0.5, (p[2] + q[2]) * 0.5, (p[3] + q[3]) * 0.5] } (Some(a), None) => img.data[a * w + x], (None, Some(b)) => img.data[b * w + x], (None, None) => img.data[y * w + x], }; out.data[y * w + x] = px; } } out } /// Interpret Footage ▸ Invert Alpha and Interpret As Linear Light on decoded (premultiplied) /// pixels; linear light is encoded in `space` ([`color::Pipe::linear_footage_space`]). pub fn interpret_pixels(img: &Image, f: &Footage, space: effectcraft_color::ColorSpace) -> Image { let linear = f.linear_light_applies(); if !f.invert_alpha && !linear { return img.clone(); } let mut out = img.clone(); for p in out.data.iter_mut() { let a = p[3]; let mut c = if a > 1e-6 { [p[0] / a, p[1] / a, p[2] / a] } else { [0.0; 3] }; if linear { // The file's values are linear light: encode them like the rest of the footage. c = c.map(|v| space.encode(v)); } let a = if f.invert_alpha { 1.0 - a } else { a }; *p = [c[0] * a, c[1] * a, c[2] * a, a]; } out } pub(crate) fn frame_blend_mode(ctx: &EvalCtx, layer: &Layer) -> FrameBlend { if ctx.comp.enable_frame_blending { layer.switches.frame_blend } else { FrameBlend::Off } } /// Position of source time `t` among frames at `rate`: the frame at or before `t` and the /// fraction (0..1) of the way to the next frame. Exact (integer tick arithmetic), so frames that /// line up exactly report a fraction of 0. pub fn frame_position(t: Tick, rate: FrameRate) -> (i64, f64) { let num = t.0 as i128 * rate.num as i128; let den = TICKS_PER_SECOND as i128 * rate.den as i128; (num.div_euclid(den) as i64, num.rem_euclid(den) as f64 / den as f64) } /// Blend two neighbouring frames `w` of the way from `a` to `b`: a cross-fade (Frame Mix) or a /// motion-compensated interpolation (Pixel Motion). pub fn blend_frames(mode: FrameBlend, a: &Image, b: &Image, w: f32) -> Image { match mode { FrameBlend::PixelMotion => effectcraft_raster::flow::interpolate(a, b, w), _ => effectcraft_raster::flow::mix(a, b, w), } } /// Convenience: render one frame of a comp with no footage source. /// Adaptive motion-blur sample count for a layer whose corners travel `travel` output pixels /// during the shutter: one sample per pixel, clamped to Samples Per Frame … Adaptive Sample Limit. pub fn adaptive_samples(travel: f64, per_frame: u32, limit: u32) -> usize { let lo = per_frame.clamp(2, 64) as f64; let hi = (limit.clamp(16, 256) as f64).max(lo); travel.ceil().clamp(lo, hi) as usize } /// A region of interest `[x, y, w, h]` lies within a `w`×`h` comp frame. pub fn roi_inside(r: [f64; 4], w: u32, h: u32) -> bool { r[0] >= 0.0 && r[1] >= 0.0 && r[0] + r[2] <= w as f64 + 1e-6 && r[1] + r[3] <= h as f64 + 1e-6 } /// The Extended Viewer's region of interest: the comp frame grown by `margin` × its size on every /// side (`margin` = 1 shows a pasteboard one comp wide around the frame), intersected with the /// `visible` comp-space rectangle `[x0, y0, x1, y1]` and snapped outward to whole 16 px steps from the frame edges /// so panning re-renders rarely. `None` when the visible area lies inside the frame. pub fn extended_region(w: u32, h: u32, visible: [f64; 4], margin: f64) -> Option<[f64; 4]> { let (w, h) = (w as f64, h as f64); let (mx, my) = (w * margin, h * margin); let snap_lo = |v: f64| (v / 16.0).floor() * 16.0; let snap_hi = |v: f64| (v / 16.0).ceil() * 16.0; let x0 = snap_lo(visible[0].max(-mx)).min(0.0); let y0 = snap_lo(visible[1].max(-my)).min(0.0); let x1 = w + snap_hi(visible[2].min(w + mx) - w).max(0.0); let y1 = h + snap_hi(visible[3].min(h + my) - h).max(0.0); if x0 >= 0.0 && y0 >= 0.0 && x1 <= w && y1 <= h { return None; } Some([x0, y0, x1 - x0, y1 - y0]) } /// The size of a `width` x `height` comp frame rendered at `scale` (both compositors, and the /// frame size Composition Settings shows for a Resolution). pub fn frame_size(width: u32, height: u32, scale: f64) -> (u32, u32) { (((width as f64 * scale).round() as u32).max(1), ((height as f64 * scale).round() as u32).max(1)) } pub fn render_frame(project: &Project, comp: ItemId, t: Tick, scale: f64) -> Image { Renderer::new(project, &NoFootage, RenderOpts { scale, ..Default::default() }).comp_frame(comp, t) } /// Layer-space bounds `[x0, y0, x1, y1]` of a layer's content at the context time (source size /// for solids/footage/precomps, glyph bounds for text, painted bounds for shapes). Used for viewer /// handles and hit testing. pub fn content_bounds(ctx: &EvalCtx, layer: &effectcraft_project::Layer) -> Option<[f64; 4]> { match &layer.source { LayerSource::Text => { let glyphs = text::glyph_paths(ctx, layer); let paths: Vec<_> = glyphs.into_iter().map(|g| g.0).collect(); effectcraft_path::bounds(&paths).map(|r| [r.x0, r.y0, r.x1, r.y1]) } LayerSource::Shape => layer.props.sub("contents").and_then(|c| shapes::content_bounds(ctx, layer, c)).map(|r| [r.x0, r.y0, r.x1, r.y1]), LayerSource::Camera | LayerSource::Light { .. } => None, _ => { let (w, h) = ctx.source_size(layer); (w > 0 && h > 0).then_some([0.0, 0.0, w as f64, h as f64]) } } } /// A mask/shape path as a kurbo path (for drawing overlays). pub fn kurbo_path(sp: &effectcraft_keyframe::ShapePath) -> kurbo::BezPath { effectcraft_path::to_kurbo(sp) }