mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 18:36:44 -06:00
Text layers can follow a path (start offset, flip, alignment; edited in place; canvas, PNG, SVG, PDF; SVG textPath imports as real text on a path). A new pattern paint tiles a source layer in grid or hex rows with scale, spacing and alignment, live as the source changes (SVG pattern, PDF vector tiling pattern).
67 lines
3.0 KiB
Rust
67 lines
3.0 KiB
Rust
//! Pattern fills: the source layer drawn into one repeating cell at the resolution it shows at,
|
||
//! repeated by a tiling shader.
|
||
|
||
use std::cell::Cell;
|
||
use std::sync::Arc;
|
||
|
||
use figcraft_color::{PatternCell, PatternPaint};
|
||
use figcraft_doc::{Document, NodeId};
|
||
use figcraft_geom::Affine;
|
||
use tiny_skia::{FilterQuality, Pixmap, SpreadMode, Transform};
|
||
|
||
use crate::draw::{Ctx, draw_node};
|
||
use crate::paint::Prepared;
|
||
|
||
/// The largest side of a cell's pixmap; a cell showing larger is drawn coarser.
|
||
const MAX_CELL_PX: f64 = 2048.0;
|
||
/// How deep patterns nest (a source filled with a pattern); deeper ones, and a source filled
|
||
/// with its own pattern, draw nothing.
|
||
pub const MAX_DEPTH: u32 = 2;
|
||
|
||
thread_local! {
|
||
static DEPTH: Cell<u32> = const { Cell::new(0) };
|
||
}
|
||
|
||
/// Run `f` one pattern level deeper; `None` past [`MAX_DEPTH`].
|
||
pub fn nested<T>(f: impl FnOnce() -> Option<T>) -> Option<T> {
|
||
let d = DEPTH.with(Cell::get);
|
||
if d >= MAX_DEPTH {
|
||
return None;
|
||
}
|
||
DEPTH.with(|c| c.set(d + 1));
|
||
let out = f();
|
||
DEPTH.with(|c| c.set(d));
|
||
out
|
||
}
|
||
|
||
/// Pattern `pt` on a `w`×`h` box drawn into one cell at about `scale` device pixels per unit and
|
||
/// `time` seconds into animated GIFs:
|
||
/// the pixmap, the cell and the pixmap's pixels per unit on each axis (whole pixels per cell, so
|
||
/// the repeat lines up exactly).
|
||
pub fn cell_pixmap(doc: &Document, pt: &PatternPaint, w: f64, h: f64, scale: f64, time: f64) -> Option<(Pixmap, PatternCell, (f64, f64))> {
|
||
let src = doc.node(NodeId(pt.source)).filter(|n| !n.is_page())?;
|
||
let cell = pt.cell(src.width, src.height, w, h)?;
|
||
let scale = if scale.is_finite() && scale > 0.0 { scale } else { 1.0 };
|
||
let k = scale.min(MAX_CELL_PX / cell.size.0.max(cell.size.1));
|
||
let (pw, ph) = ((cell.size.0 * k).ceil().clamp(1.0, MAX_CELL_PX), (cell.size.1 * k).ceil().clamp(1.0, MAX_CELL_PX));
|
||
let (kx, ky) = (pw / cell.size.0, ph / cell.size.1);
|
||
let mut pix = Pixmap::new(pw as u32, ph as u32)?;
|
||
let ctx = Ctx { doc, scale: kx.max(ky) * cell.scale, outline_mode: false, outlined: &[], skip: &[], lod: false, under: None, time };
|
||
let unrotated = src.transform.inverse();
|
||
nested(|| {
|
||
for (tx, ty) in &cell.tiles {
|
||
let xf = Affine::scale_non_uniform(kx, ky) * Affine::translate((*tx, *ty)) * Affine::scale(cell.scale) * unrotated;
|
||
draw_node(&ctx, &mut pix, src.id, xf, 0);
|
||
}
|
||
Some(())
|
||
})?;
|
||
Some((pix, cell, (kx, ky)))
|
||
}
|
||
|
||
/// The shader for pattern `pt` on a `w`×`h` box shown at `scale` device pixels per unit.
|
||
pub fn prepare(doc: &Document, pt: &PatternPaint, w: f64, h: f64, scale: f64, time: f64, opacity: f32) -> Option<Prepared> {
|
||
let (pix, cell, (kx, ky)) = cell_pixmap(doc, pt, w, h, scale, time)?;
|
||
let transform = Transform::from_row((1.0 / kx) as f32, 0.0, 0.0, (1.0 / ky) as f32, cell.origin.0 as f32, cell.origin.1 as f32);
|
||
Some(Prepared::Pattern { pixmap: Arc::new(pix), mode: SpreadMode::Repeat, quality: FilterQuality::Bilinear, opacity, transform })
|
||
}
|