Gradient Part 2 - Linear gradients (#2279)
1
Cargo.lock
generated
@ -2741,6 +2741,7 @@ dependencies = [
|
||||
"fontdb",
|
||||
"image",
|
||||
"indexmap 2.0.0",
|
||||
"kurbo",
|
||||
"log",
|
||||
"miniz_oxide",
|
||||
"once_cell",
|
||||
|
@ -731,6 +731,7 @@ const TYPE_ORDER: &[&str] = &[
|
||||
"relative",
|
||||
"fraction",
|
||||
"color",
|
||||
"gradient",
|
||||
"datetime",
|
||||
"duration",
|
||||
"str",
|
||||
|
@ -1169,6 +1169,26 @@ impl<'a> CompletionContext<'a> {
|
||||
"cmyk(${c}, ${m}, ${y}, ${k})",
|
||||
"A custom CMYK color.",
|
||||
);
|
||||
self.snippet_completion(
|
||||
"oklab()",
|
||||
"oklab(${l}, ${a}, ${b}, ${alpha})",
|
||||
"A custom Oklab color.",
|
||||
);
|
||||
self.snippet_completion(
|
||||
"color.linear-rgb()",
|
||||
"color.linear-rgb(${r}, ${g}, ${b}, ${a})",
|
||||
"A custom linear RGBA color.",
|
||||
);
|
||||
self.snippet_completion(
|
||||
"color.hsv()",
|
||||
"color.hsv(${h}, ${s}, ${v}, ${a})",
|
||||
"A custom HSVA color.",
|
||||
);
|
||||
self.snippet_completion(
|
||||
"color.hsl()",
|
||||
"color.hsl(${h}, ${s}, ${l}, ${a})",
|
||||
"A custom HSLA color.",
|
||||
);
|
||||
self.scope_completions(false, |value| value.ty() == *ty);
|
||||
} else if *ty == Type::of::<Func>() {
|
||||
self.snippet_completion(
|
||||
|
@ -100,7 +100,7 @@ impl Layout for ColumnsElem {
|
||||
// case, the frame is first created with zero height and then
|
||||
// resized.
|
||||
let height = if regions.expand.y { region.y } else { Abs::zero() };
|
||||
let mut output = Frame::new(Size::new(regions.size.x, height));
|
||||
let mut output = Frame::hard(Size::new(regions.size.x, height));
|
||||
let mut cursor = Abs::zero();
|
||||
|
||||
for _ in 0..columns {
|
||||
|
@ -164,6 +164,7 @@ impl Layout for BoxElem {
|
||||
|
||||
// Apply metadata.
|
||||
frame.meta(styles, false);
|
||||
frame.set_kind(FrameKind::Hard);
|
||||
|
||||
Ok(Fragment::frame(frame))
|
||||
}
|
||||
@ -440,6 +441,7 @@ impl Layout for BlockElem {
|
||||
|
||||
// Apply metadata.
|
||||
for frame in &mut frames {
|
||||
frame.set_kind(FrameKind::Hard);
|
||||
frame.meta(styles, false);
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ impl Layout for FlowElem {
|
||||
let layoutable = child.with::<dyn Layout>().unwrap();
|
||||
layouter.layout_single(vt, layoutable, styles)?;
|
||||
} else if child.is::<MetaElem>() {
|
||||
let mut frame = Frame::new(Size::zero());
|
||||
let mut frame = Frame::soft(Size::zero());
|
||||
frame.meta(styles, true);
|
||||
layouter.items.push(FlowItem::Frame {
|
||||
frame,
|
||||
@ -484,7 +484,7 @@ impl<'a> FlowLayouter<'a> {
|
||||
size.y = self.initial.y;
|
||||
}
|
||||
|
||||
let mut output = Frame::new(size);
|
||||
let mut output = Frame::soft(size);
|
||||
let mut ruler = FixedAlign::Start;
|
||||
let mut float_top_offset = Abs::zero();
|
||||
let mut offset = float_top_height;
|
||||
|
@ -563,7 +563,7 @@ impl<'a> GridLayouter<'a> {
|
||||
height: Abs,
|
||||
y: usize,
|
||||
) -> SourceResult<Frame> {
|
||||
let mut output = Frame::new(Size::new(self.width, height));
|
||||
let mut output = Frame::soft(Size::new(self.width, height));
|
||||
let mut pos = Point::zero();
|
||||
|
||||
for (x, &rcol) in self.rcols.iter().enumerate() {
|
||||
@ -593,7 +593,7 @@ impl<'a> GridLayouter<'a> {
|
||||
// Prepare frames.
|
||||
let mut outputs: Vec<_> = heights
|
||||
.iter()
|
||||
.map(|&h| Frame::new(Size::new(self.width, h)))
|
||||
.map(|&h| Frame::soft(Size::new(self.width, h)))
|
||||
.collect();
|
||||
|
||||
// Prepare regions.
|
||||
@ -647,7 +647,7 @@ impl<'a> GridLayouter<'a> {
|
||||
}
|
||||
|
||||
// The frame for the region.
|
||||
let mut output = Frame::new(size);
|
||||
let mut output = Frame::soft(size);
|
||||
let mut pos = Point::zero();
|
||||
let mut rrows = vec![];
|
||||
|
||||
|
@ -381,7 +381,7 @@ impl PageElem {
|
||||
if extend_to.is_some_and(|p| p.matches(page_counter.physical().get())) {
|
||||
// Insert empty page after the current pages.
|
||||
let size = area.map(Abs::is_finite).select(area, Size::zero());
|
||||
frames.push(Frame::new(size));
|
||||
frames.push(Frame::hard(size));
|
||||
}
|
||||
|
||||
let fill = self.fill(styles);
|
||||
|
@ -714,7 +714,7 @@ fn prepare<'a>(
|
||||
}
|
||||
}
|
||||
Segment::Meta => {
|
||||
let mut frame = Frame::new(Size::zero());
|
||||
let mut frame = Frame::soft(Size::zero());
|
||||
frame.meta(styles, true);
|
||||
items.push(Item::Meta(frame));
|
||||
}
|
||||
@ -1521,7 +1521,7 @@ fn commit(
|
||||
}
|
||||
|
||||
let size = Size::new(width, top + bottom);
|
||||
let mut output = Frame::new(size);
|
||||
let mut output = Frame::soft(size);
|
||||
output.set_baseline(top);
|
||||
|
||||
// Construct the line's frame.
|
||||
|
@ -54,7 +54,7 @@ impl Layout for RepeatElem {
|
||||
bail!(self.span(), "repeat with no size restrictions");
|
||||
}
|
||||
|
||||
let mut frame = Frame::new(size);
|
||||
let mut frame = Frame::soft(size);
|
||||
if piece.has_baseline() {
|
||||
frame.set_baseline(piece.baseline());
|
||||
}
|
||||
|
@ -254,7 +254,7 @@ impl<'a> StackLayouter<'a> {
|
||||
size.set(self.axis, full);
|
||||
}
|
||||
|
||||
let mut output = Frame::new(size);
|
||||
let mut output = Frame::hard(size);
|
||||
let mut cursor = Abs::zero();
|
||||
let mut ruler: FixedAlign = self.dir.start().into();
|
||||
|
||||
|
@ -88,7 +88,7 @@ impl LayoutMath for AccentElem {
|
||||
let base_ascent = base.ascent();
|
||||
let baseline = base_pos.y + base.ascent();
|
||||
|
||||
let mut frame = Frame::new(size);
|
||||
let mut frame = Frame::soft(size);
|
||||
frame.set_baseline(baseline);
|
||||
frame.push_frame(accent_pos, accent);
|
||||
frame.push_frame(base_pos, base.into_frame());
|
||||
|
@ -106,7 +106,7 @@ impl LayoutMath for PrimesElem {
|
||||
// Custom amount of primes
|
||||
let prime = ctx.layout_fragment(&TextElem::packed('′'))?.into_frame();
|
||||
let width = prime.width() * (count + 1) as f64 / 2.0;
|
||||
let mut frame = Frame::new(Size::new(width, prime.height()));
|
||||
let mut frame = Frame::soft(Size::new(width, prime.height()));
|
||||
frame.set_baseline(prime.ascent());
|
||||
|
||||
for i in 0..count {
|
||||
@ -260,7 +260,7 @@ fn layout_attachments(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut frame = Frame::new(Size::new(
|
||||
let mut frame = Frame::soft(Size::new(
|
||||
pre_width_max + base_width + post_max_width + scaled!(ctx, space_after_script),
|
||||
ascent + descent,
|
||||
));
|
||||
@ -331,7 +331,7 @@ fn attach_top_and_bottom(
|
||||
let base_pos = Point::new((width - base.width()) / 2.0, base_offset);
|
||||
let delta = base.italics_correction() / 2.0;
|
||||
|
||||
let mut frame = Frame::new(Size::new(width, height));
|
||||
let mut frame = Frame::soft(Size::new(width, height));
|
||||
frame.set_baseline(base_pos.y + base.ascent());
|
||||
frame.push_frame(base_pos, base.into_frame());
|
||||
|
||||
|
@ -171,7 +171,7 @@ fn draw_cancel_line(
|
||||
let start = Axes::new(-mid.x, mid.y).zip_map(scales, |l, s| l * s);
|
||||
let delta = Axes::new(width, -height).zip_map(scales, |l, s| l * s);
|
||||
|
||||
let mut frame = Frame::new(body_size);
|
||||
let mut frame = Frame::soft(body_size);
|
||||
frame.push(
|
||||
start.to_point(),
|
||||
FrameItem::Shape(Geometry::Line(delta.to_point()).stroked(stroke), span),
|
||||
|
@ -110,7 +110,7 @@ fn layout(
|
||||
let denom_pos = Point::new((width - denom.width()) / 2.0, height - denom.height());
|
||||
let baseline = line_pos.y + axis;
|
||||
|
||||
let mut frame = Frame::new(size);
|
||||
let mut frame = Frame::soft(size);
|
||||
frame.set_baseline(baseline);
|
||||
frame.push_frame(num_pos, num);
|
||||
frame.push_frame(denom_pos, denom);
|
||||
|
@ -148,7 +148,7 @@ impl MathFragment {
|
||||
Self::Glyph(glyph) => glyph.into_frame(),
|
||||
Self::Variant(variant) => variant.frame,
|
||||
Self::Frame(fragment) => fragment.frame,
|
||||
_ => Frame::new(self.size()),
|
||||
_ => Frame::soft(self.size()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -309,7 +309,7 @@ impl GlyphFragment {
|
||||
}],
|
||||
};
|
||||
let size = Size::new(self.width, self.ascent + self.descent);
|
||||
let mut frame = Frame::new(size);
|
||||
let mut frame = Frame::soft(size);
|
||||
frame.set_baseline(self.ascent);
|
||||
frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item));
|
||||
frame.meta_iter(self.meta);
|
||||
|
@ -396,7 +396,7 @@ fn layout_mat_body(
|
||||
let ncols = rows.first().map_or(0, |row| row.len());
|
||||
let nrows = rows.len();
|
||||
if ncols == 0 || nrows == 0 {
|
||||
return Ok(Frame::new(Size::zero()));
|
||||
return Ok(Frame::soft(Size::zero()));
|
||||
}
|
||||
|
||||
// Before the full matrix body can be laid out, the
|
||||
@ -431,7 +431,7 @@ fn layout_mat_body(
|
||||
heights.iter().map(|&(a, b)| a + b).sum::<Abs>() + gap.y * (nrows - 1) as f64;
|
||||
|
||||
// Width starts at zero because it can't be calculated until later
|
||||
let mut frame = Frame::new(Size::new(Abs::zero(), total_height));
|
||||
let mut frame = Frame::soft(Size::new(Abs::zero(), total_height));
|
||||
|
||||
let mut x = Abs::zero();
|
||||
|
||||
|
@ -109,7 +109,7 @@ fn layout(
|
||||
let line_pos = Point::new(radicand_x, radicand_y - gap - (thickness / 2.0));
|
||||
let radicand_pos = Point::new(radicand_x, radicand_y);
|
||||
|
||||
let mut frame = Frame::new(size);
|
||||
let mut frame = Frame::soft(size);
|
||||
frame.set_baseline(ascent);
|
||||
|
||||
if let Some(index) = index {
|
||||
|
@ -156,7 +156,7 @@ impl MathRow {
|
||||
}
|
||||
|
||||
let AlignmentResult { points, width } = alignments(&rows);
|
||||
let mut frame = Frame::new(Size::zero());
|
||||
let mut frame = Frame::soft(Size::zero());
|
||||
|
||||
for (i, row) in rows.into_iter().enumerate() {
|
||||
let sub = row.into_line_frame(&points, align);
|
||||
@ -179,7 +179,7 @@ impl MathRow {
|
||||
|
||||
fn into_line_frame(self, points: &[Abs], align: FixedAlign) -> Frame {
|
||||
let ascent = self.ascent();
|
||||
let mut frame = Frame::new(Size::new(Abs::zero(), ascent + self.descent()));
|
||||
let mut frame = Frame::soft(Size::new(Abs::zero(), ascent + self.descent()));
|
||||
frame.set_baseline(ascent);
|
||||
|
||||
let mut next_x = {
|
||||
|
@ -161,7 +161,7 @@ fn assemble(
|
||||
baseline = full / 2.0 + axis;
|
||||
}
|
||||
|
||||
let mut frame = Frame::new(size);
|
||||
let mut frame = Frame::soft(size);
|
||||
let mut offset = Abs::zero();
|
||||
frame.set_baseline(baseline);
|
||||
frame.meta_iter(base.meta);
|
||||
|
@ -89,7 +89,7 @@ fn layout_underoverline(
|
||||
let size = Size::new(width, height);
|
||||
|
||||
let content_class = content.class().unwrap_or(MathClass::Normal);
|
||||
let mut frame = Frame::new(size);
|
||||
let mut frame = Frame::soft(size);
|
||||
frame.set_baseline(baseline);
|
||||
frame.push_frame(content_pos, content.into_frame());
|
||||
frame.push(
|
||||
@ -295,7 +295,7 @@ pub(super) fn stack(
|
||||
.collect();
|
||||
|
||||
let mut y = Abs::zero();
|
||||
let mut frame = Frame::new(Size::new(
|
||||
let mut frame = Frame::soft(Size::new(
|
||||
width,
|
||||
rows.iter().map(|row| row.height()).sum::<Abs>()
|
||||
+ rows.len().saturating_sub(1) as f64 * gap,
|
||||
|
@ -16,6 +16,7 @@ pub use self::shift::*;
|
||||
|
||||
use rustybuzz::Tag;
|
||||
use ttf_parser::Rect;
|
||||
use typst::diag::{bail, error, SourceResult};
|
||||
use typst::font::{Font, FontStretch, FontStyle, FontWeight, VerticalFontMetric};
|
||||
|
||||
use crate::layout::ParElem;
|
||||
@ -169,13 +170,23 @@ pub struct TextElem {
|
||||
#[default(Abs::pt(11.0))]
|
||||
pub size: TextSize,
|
||||
|
||||
/// The glyph fill color.
|
||||
/// The glyph fill paint.
|
||||
///
|
||||
/// ```example
|
||||
/// #set text(fill: red)
|
||||
/// This text is red.
|
||||
/// ```
|
||||
#[parse(args.named_or_find("fill")?)]
|
||||
#[parse({
|
||||
let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?;
|
||||
if let Some(paint) = &paint {
|
||||
// TODO: Implement gradients on text.
|
||||
if matches!(paint.v, Paint::Gradient(_)) {
|
||||
bail!(error!(paint.span, "text fill must be a solid color")
|
||||
.with_hint("gradients on text will be supported soon"));
|
||||
}
|
||||
}
|
||||
paint.map(|paint| paint.v)
|
||||
})]
|
||||
#[default(Color::BLACK.into())]
|
||||
pub fill: Paint,
|
||||
|
||||
|
@ -228,7 +228,7 @@ impl<'a> ShapedText<'a> {
|
||||
let size = Size::new(self.width, top + bottom);
|
||||
|
||||
let mut offset = Abs::zero();
|
||||
let mut frame = Frame::new(size);
|
||||
let mut frame = Frame::soft(size);
|
||||
frame.set_baseline(top);
|
||||
|
||||
let shift = TextElem::baseline_in(self.styles);
|
||||
|
@ -206,7 +206,7 @@ impl Layout for ImageElem {
|
||||
// First, place the image in a frame of exactly its size and then resize
|
||||
// the frame to the target size, center aligning the image in the
|
||||
// process.
|
||||
let mut frame = Frame::new(fitted);
|
||||
let mut frame = Frame::soft(fitted);
|
||||
frame.push(Point::zero(), FrameItem::Image(image, fitted, self.span()));
|
||||
frame.resize(target, Axes::splat(FixedAlign::Center));
|
||||
|
||||
|
@ -75,7 +75,7 @@ impl Layout for LineElem {
|
||||
let size = start.max(start + delta).max(Size::zero());
|
||||
let target = regions.expand.select(regions.size, size);
|
||||
|
||||
let mut frame = Frame::new(target);
|
||||
let mut frame = Frame::soft(target);
|
||||
let shape = Geometry::Line(delta.to_point()).stroked(stroke);
|
||||
frame.push(start.to_point(), FrameItem::Shape(shape, self.span()));
|
||||
Ok(Fragment::frame(frame))
|
||||
|
@ -18,6 +18,7 @@ use crate::prelude::*;
|
||||
pub(super) fn define(global: &mut Scope) {
|
||||
global.category("visualize");
|
||||
global.define_type::<Color>();
|
||||
global.define_type::<Gradient>();
|
||||
global.define_type::<Stroke>();
|
||||
global.define_elem::<ImageElem>();
|
||||
global.define_elem::<LineElem>();
|
||||
|
@ -82,7 +82,7 @@ impl Layout for PathElem {
|
||||
|
||||
let mut size = Size::zero();
|
||||
if points.is_empty() {
|
||||
return Ok(Fragment::frame(Frame::new(size)));
|
||||
return Ok(Fragment::frame(Frame::soft(size)));
|
||||
}
|
||||
|
||||
// Only create a path if there are more than zero points.
|
||||
@ -138,7 +138,7 @@ impl Layout for PathElem {
|
||||
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
|
||||
};
|
||||
|
||||
let mut frame = Frame::new(size);
|
||||
let mut frame = Frame::soft(size);
|
||||
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
|
||||
frame.push(Point::zero(), FrameItem::Shape(shape, self.span()));
|
||||
|
||||
|
@ -130,7 +130,7 @@ impl Layout for PolygonElem {
|
||||
.collect();
|
||||
|
||||
let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size();
|
||||
let mut frame = Frame::new(size);
|
||||
let mut frame = Frame::hard(size);
|
||||
|
||||
// Only create a path if there are more than zero points.
|
||||
if points.is_empty() {
|
||||
|
@ -492,7 +492,7 @@ fn layout(
|
||||
if kind.is_quadratic() {
|
||||
size = Size::splat(size.min_by_side());
|
||||
}
|
||||
frame = Frame::new(size);
|
||||
frame = Frame::soft(size);
|
||||
}
|
||||
|
||||
// Prepare stroke.
|
||||
|
@ -27,6 +27,7 @@ flate2 = "1"
|
||||
fontdb = { version = "0.14", default-features = false }
|
||||
image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] }
|
||||
indexmap = { version = "2", features = ["serde"] }
|
||||
kurbo = "0.9"
|
||||
log = "0.4"
|
||||
miniz_oxide = "0.7"
|
||||
once_cell = "1"
|
||||
|
@ -42,6 +42,8 @@ pub struct Frame {
|
||||
baseline: Option<Abs>,
|
||||
/// The items composing this layout.
|
||||
items: Arc<Vec<(Point, FrameItem)>>,
|
||||
/// The hardness of this frame.
|
||||
kind: FrameKind,
|
||||
}
|
||||
|
||||
/// Constructor, accessors and setters.
|
||||
@ -50,9 +52,40 @@ impl Frame {
|
||||
///
|
||||
/// Panics the size is not finite.
|
||||
#[track_caller]
|
||||
pub fn new(size: Size) -> Self {
|
||||
pub fn new(size: Size, kind: FrameKind) -> Self {
|
||||
assert!(size.is_finite());
|
||||
Self { size, baseline: None, items: Arc::new(vec![]) }
|
||||
Self {
|
||||
size,
|
||||
baseline: None,
|
||||
items: Arc::new(vec![]),
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new, empty soft frame.
|
||||
///
|
||||
/// Panics the size is not finite.
|
||||
#[track_caller]
|
||||
pub fn soft(size: Size) -> Self {
|
||||
Self::new(size, FrameKind::Soft)
|
||||
}
|
||||
|
||||
/// Create a new, empty hard frame.
|
||||
///
|
||||
/// Panics if the size is not finite.
|
||||
#[track_caller]
|
||||
pub fn hard(size: Size) -> Self {
|
||||
Self::new(size, FrameKind::Hard)
|
||||
}
|
||||
|
||||
/// Sets the frame's hardness.
|
||||
pub fn set_kind(&mut self, kind: FrameKind) {
|
||||
self.kind = kind;
|
||||
}
|
||||
|
||||
/// Whether the frame is hard or soft.
|
||||
pub fn kind(&self) -> FrameKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
/// Whether the frame contains no items.
|
||||
@ -185,7 +218,8 @@ impl Frame {
|
||||
|
||||
/// Whether the given frame should be inlined.
|
||||
fn should_inline(&self, frame: &Frame) -> bool {
|
||||
self.items.is_empty() || frame.items.len() <= 5
|
||||
// We do not inline big frames and hard frames.
|
||||
frame.kind().is_soft() && (self.items.is_empty() || frame.items.len() <= 5)
|
||||
}
|
||||
|
||||
/// Inline a frame at the given layer.
|
||||
@ -329,7 +363,7 @@ impl Frame {
|
||||
where
|
||||
F: FnOnce(&mut GroupItem),
|
||||
{
|
||||
let mut wrapper = Frame::new(self.size);
|
||||
let mut wrapper = Frame::soft(self.size);
|
||||
wrapper.baseline = self.baseline;
|
||||
let mut group = GroupItem::new(std::mem::take(self));
|
||||
f(&mut group);
|
||||
@ -407,6 +441,35 @@ impl Debug for Frame {
|
||||
}
|
||||
}
|
||||
|
||||
/// The hardness of a frame.
|
||||
///
|
||||
/// This corresponds to whether or not the frame is considered to be the
|
||||
/// innermost parent of its contents. This is used to determine the coordinate
|
||||
/// reference system for gradients.
|
||||
#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum FrameKind {
|
||||
/// A container which follows its parent's size.
|
||||
///
|
||||
/// Soft frames are the default since they do not impact the layout of
|
||||
/// a gradient set on one of its children.
|
||||
#[default]
|
||||
Soft,
|
||||
/// A container which uses its own size.
|
||||
Hard,
|
||||
}
|
||||
|
||||
impl FrameKind {
|
||||
/// Returns `true` if the frame is soft.
|
||||
pub fn is_soft(self) -> bool {
|
||||
matches!(self, Self::Soft)
|
||||
}
|
||||
|
||||
/// Returns `true` if the frame is hard.
|
||||
pub fn is_hard(self) -> bool {
|
||||
matches!(self, Self::Hard)
|
||||
}
|
||||
}
|
||||
|
||||
/// The building block frames are composed of.
|
||||
#[derive(Clone, Hash)]
|
||||
pub enum FrameItem {
|
||||
|
@ -183,6 +183,12 @@ impl<T: IntoValue> IntoResult for SourceResult<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoValue> IntoValue for fn() -> T {
|
||||
fn into_value(self) -> Value {
|
||||
self().into_value()
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to cast a Typst [`Value`] into a Rust type.
|
||||
///
|
||||
/// See also: [`Reflect`].
|
||||
|
@ -120,6 +120,14 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
}
|
||||
.into_value(),
|
||||
|
||||
(Gradient(gradient), Length(thickness))
|
||||
| (Length(thickness), Gradient(gradient)) => Stroke {
|
||||
paint: Smart::Custom(gradient.into()),
|
||||
thickness: Smart::Custom(thickness),
|
||||
..Stroke::default()
|
||||
}
|
||||
.into_value(),
|
||||
|
||||
(Duration(a), Duration(b)) => Duration(a + b),
|
||||
(Datetime(a), Duration(b)) => Datetime(a + b),
|
||||
(Duration(a), Datetime(b)) => Datetime(b + a),
|
||||
|
@ -18,7 +18,7 @@ use super::{
|
||||
};
|
||||
use crate::diag::StrResult;
|
||||
use crate::eval::Datetime;
|
||||
use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel};
|
||||
use crate::geom::{Abs, Angle, Color, Em, Fr, Gradient, Length, Ratio, Rel};
|
||||
use crate::model::{Label, Styles};
|
||||
use crate::syntax::{ast, Span};
|
||||
|
||||
@ -48,6 +48,8 @@ pub enum Value {
|
||||
Fraction(Fr),
|
||||
/// A color value: `#f79143ff`.
|
||||
Color(Color),
|
||||
/// A gradient value: `gradient.linear(...)`.
|
||||
Gradient(Gradient),
|
||||
/// A symbol: `arrow.l`.
|
||||
Symbol(Symbol),
|
||||
/// A version.
|
||||
@ -123,6 +125,7 @@ impl Value {
|
||||
Self::Relative(_) => Type::of::<Rel<Length>>(),
|
||||
Self::Fraction(_) => Type::of::<Fr>(),
|
||||
Self::Color(_) => Type::of::<Color>(),
|
||||
Self::Gradient(_) => Type::of::<Gradient>(),
|
||||
Self::Symbol(_) => Type::of::<Symbol>(),
|
||||
Self::Version(_) => Type::of::<Version>(),
|
||||
Self::Str(_) => Type::of::<Str>(),
|
||||
@ -235,6 +238,7 @@ impl Debug for Value {
|
||||
Self::Relative(v) => Debug::fmt(v, f),
|
||||
Self::Fraction(v) => Debug::fmt(v, f),
|
||||
Self::Color(v) => Debug::fmt(v, f),
|
||||
Self::Gradient(v) => Debug::fmt(v, f),
|
||||
Self::Symbol(v) => Debug::fmt(v, f),
|
||||
Self::Version(v) => Debug::fmt(v, f),
|
||||
Self::Str(v) => Debug::fmt(v, f),
|
||||
@ -283,6 +287,7 @@ impl Hash for Value {
|
||||
Self::Relative(v) => v.hash(state),
|
||||
Self::Fraction(v) => v.hash(state),
|
||||
Self::Color(v) => v.hash(state),
|
||||
Self::Gradient(v) => v.hash(state),
|
||||
Self::Symbol(v) => v.hash(state),
|
||||
Self::Version(v) => v.hash(state),
|
||||
Self::Str(v) => v.hash(state),
|
||||
@ -588,6 +593,7 @@ primitive! { Rel<Length>: "relative length",
|
||||
}
|
||||
primitive! { Fr: "fraction", Fraction }
|
||||
primitive! { Color: "color", Color }
|
||||
primitive! { Gradient: "gradient", Gradient }
|
||||
primitive! { Symbol: "symbol", Symbol }
|
||||
primitive! { Version: "version", Version }
|
||||
primitive! {
|
||||
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use pdf_writer::types::DeviceNSubtype;
|
||||
use pdf_writer::{writers, Dict, Filter, Name, PdfWriter, Ref};
|
||||
|
||||
use super::page::PageContext;
|
||||
use super::page::{PageContext, Transforms};
|
||||
use super::RefExt;
|
||||
use crate::export::pdf::deflate;
|
||||
use crate::geom::{Color, ColorSpace, Paint};
|
||||
@ -302,120 +302,192 @@ impl ColorEncode for ColorSpace {
|
||||
}
|
||||
|
||||
/// Encodes a paint into either a fill or stroke color.
|
||||
pub trait PaintEncode {
|
||||
pub(super) trait PaintEncode {
|
||||
/// Set the paint as the fill color.
|
||||
fn set_as_fill(&self, page_context: &mut PageContext);
|
||||
fn set_as_fill(&self, ctx: &mut PageContext, transforms: Transforms);
|
||||
|
||||
/// Set the paint as the stroke color.
|
||||
fn set_as_stroke(&self, page_context: &mut PageContext);
|
||||
fn set_as_stroke(&self, ctx: &mut PageContext, transforms: Transforms);
|
||||
}
|
||||
|
||||
impl PaintEncode for Paint {
|
||||
fn set_as_fill(&self, ctx: &mut PageContext) {
|
||||
let Paint::Solid(color) = self;
|
||||
match color {
|
||||
fn set_as_fill(&self, ctx: &mut PageContext, transforms: Transforms) {
|
||||
match self {
|
||||
Self::Solid(c) => c.set_as_fill(ctx, transforms),
|
||||
Self::Gradient(gradient) => gradient.set_as_fill(ctx, transforms),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_as_stroke(&self, ctx: &mut PageContext, transforms: Transforms) {
|
||||
match self {
|
||||
Self::Solid(c) => c.set_as_stroke(ctx, transforms),
|
||||
Self::Gradient(gradient) => gradient.set_as_stroke(ctx, transforms),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PaintEncode for Color {
|
||||
fn set_as_fill(&self, ctx: &mut PageContext, _: Transforms) {
|
||||
match self {
|
||||
Color::Luma(_) => {
|
||||
ctx.parent.colors.d65_gray(&mut ctx.parent.alloc);
|
||||
ctx.set_fill_color_space(D65_GRAY);
|
||||
|
||||
let [l, _, _, _] = ColorSpace::D65Gray.encode(*color);
|
||||
let [l, _, _, _] = ColorSpace::D65Gray.encode(*self);
|
||||
ctx.content.set_fill_color([l]);
|
||||
}
|
||||
Color::Oklab(_) => {
|
||||
ctx.parent.colors.oklab(&mut ctx.parent.alloc);
|
||||
ctx.set_fill_color_space(OKLAB);
|
||||
|
||||
let [l, a, b, _] = ColorSpace::Oklab.encode(*color);
|
||||
let [l, a, b, _] = ColorSpace::Oklab.encode(*self);
|
||||
ctx.content.set_fill_color([l, a, b]);
|
||||
}
|
||||
Color::LinearRgb(_) => {
|
||||
ctx.parent.colors.linear_rgb();
|
||||
ctx.set_fill_color_space(LINEAR_SRGB);
|
||||
|
||||
let [r, g, b, _] = ColorSpace::LinearRgb.encode(*color);
|
||||
let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self);
|
||||
ctx.content.set_fill_color([r, g, b]);
|
||||
}
|
||||
Color::Rgba(_) => {
|
||||
ctx.parent.colors.srgb(&mut ctx.parent.alloc);
|
||||
ctx.set_fill_color_space(SRGB);
|
||||
|
||||
let [r, g, b, _] = ColorSpace::Srgb.encode(*color);
|
||||
let [r, g, b, _] = ColorSpace::Srgb.encode(*self);
|
||||
ctx.content.set_fill_color([r, g, b]);
|
||||
}
|
||||
Color::Cmyk(_) => {
|
||||
ctx.reset_fill_color_space();
|
||||
|
||||
let [c, m, y, k] = ColorSpace::Cmyk.encode(*color);
|
||||
let [c, m, y, k] = ColorSpace::Cmyk.encode(*self);
|
||||
ctx.content.set_fill_cmyk(c, m, y, k);
|
||||
}
|
||||
Color::Hsl(_) => {
|
||||
ctx.parent.colors.hsl(&mut ctx.parent.alloc);
|
||||
ctx.set_fill_color_space(HSL);
|
||||
|
||||
let [h, s, l, _] = ColorSpace::Hsl.encode(*color);
|
||||
let [h, s, l, _] = ColorSpace::Hsl.encode(*self);
|
||||
ctx.content.set_fill_color([h, s, l]);
|
||||
}
|
||||
Color::Hsv(_) => {
|
||||
ctx.parent.colors.hsv(&mut ctx.parent.alloc);
|
||||
ctx.set_fill_color_space(HSV);
|
||||
|
||||
let [h, s, v, _] = ColorSpace::Hsv.encode(*color);
|
||||
let [h, s, v, _] = ColorSpace::Hsv.encode(*self);
|
||||
ctx.content.set_fill_color([h, s, v]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_as_stroke(&self, ctx: &mut PageContext) {
|
||||
let Paint::Solid(color) = self;
|
||||
match color {
|
||||
fn set_as_stroke(&self, ctx: &mut PageContext, _: Transforms) {
|
||||
match self {
|
||||
Color::Luma(_) => {
|
||||
ctx.parent.colors.d65_gray(&mut ctx.parent.alloc);
|
||||
ctx.set_stroke_color_space(D65_GRAY);
|
||||
|
||||
let [l, _, _, _] = ColorSpace::D65Gray.encode(*color);
|
||||
let [l, _, _, _] = ColorSpace::D65Gray.encode(*self);
|
||||
ctx.content.set_stroke_color([l]);
|
||||
}
|
||||
Color::Oklab(_) => {
|
||||
ctx.parent.colors.oklab(&mut ctx.parent.alloc);
|
||||
ctx.set_stroke_color_space(OKLAB);
|
||||
|
||||
let [l, a, b, _] = ColorSpace::Oklab.encode(*color);
|
||||
let [l, a, b, _] = ColorSpace::Oklab.encode(*self);
|
||||
ctx.content.set_stroke_color([l, a, b]);
|
||||
}
|
||||
Color::LinearRgb(_) => {
|
||||
ctx.parent.colors.linear_rgb();
|
||||
ctx.set_stroke_color_space(LINEAR_SRGB);
|
||||
|
||||
let [r, g, b, _] = ColorSpace::LinearRgb.encode(*color);
|
||||
let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self);
|
||||
ctx.content.set_stroke_color([r, g, b]);
|
||||
}
|
||||
Color::Rgba(_) => {
|
||||
ctx.parent.colors.srgb(&mut ctx.parent.alloc);
|
||||
ctx.set_stroke_color_space(SRGB);
|
||||
|
||||
let [r, g, b, _] = ColorSpace::Srgb.encode(*color);
|
||||
let [r, g, b, _] = ColorSpace::Srgb.encode(*self);
|
||||
ctx.content.set_stroke_color([r, g, b]);
|
||||
}
|
||||
Color::Cmyk(_) => {
|
||||
ctx.reset_stroke_color_space();
|
||||
|
||||
let [c, m, y, k] = ColorSpace::Cmyk.encode(*color);
|
||||
let [c, m, y, k] = ColorSpace::Cmyk.encode(*self);
|
||||
ctx.content.set_stroke_cmyk(c, m, y, k);
|
||||
}
|
||||
Color::Hsl(_) => {
|
||||
ctx.parent.colors.hsl(&mut ctx.parent.alloc);
|
||||
ctx.set_stroke_color_space(HSL);
|
||||
|
||||
let [h, s, l, _] = ColorSpace::Hsl.encode(*color);
|
||||
let [h, s, l, _] = ColorSpace::Hsl.encode(*self);
|
||||
ctx.content.set_stroke_color([h, s, l]);
|
||||
}
|
||||
Color::Hsv(_) => {
|
||||
ctx.parent.colors.hsv(&mut ctx.parent.alloc);
|
||||
ctx.set_stroke_color_space(HSV);
|
||||
|
||||
let [h, s, v, _] = ColorSpace::Hsv.encode(*color);
|
||||
let [h, s, v, _] = ColorSpace::Hsv.encode(*self);
|
||||
ctx.content.set_stroke_color([h, s, v]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extra color space functions.
|
||||
pub(super) trait ColorSpaceExt {
|
||||
/// Returns the range of the color space.
|
||||
fn range(self) -> [f32; 6];
|
||||
|
||||
/// Converts a color to the color space.
|
||||
fn convert<U: QuantizedColor>(self, color: Color) -> [U; 3];
|
||||
}
|
||||
|
||||
impl ColorSpaceExt for ColorSpace {
|
||||
fn range(self) -> [f32; 6] {
|
||||
[0.0, 1.0, 0.0, 1.0, 0.0, 1.0]
|
||||
}
|
||||
|
||||
fn convert<U: QuantizedColor>(self, color: Color) -> [U; 3] {
|
||||
let range = self.range();
|
||||
let [x, y, z, _] = color.to_space(self).to_vec4();
|
||||
|
||||
// We need to add 0.4 to y and z for Oklab
|
||||
// This is because DeviceN color spaces in PDF can **only** be in
|
||||
// the range 0..1 and some readers enforce that.
|
||||
// The oklab color space is in the range -0.4..0.4
|
||||
// Also map the angle range of HSV/HSL to 0..1 instead of 0..360
|
||||
let [x, y, z] = match self {
|
||||
Self::Oklab => [x, y + 0.4, z + 0.4],
|
||||
Self::Hsv | Self::Hsl => [x / 360.0, y, z],
|
||||
_ => [x, y, z],
|
||||
};
|
||||
|
||||
[
|
||||
U::quantize(x, [range[0], range[1]]),
|
||||
U::quantize(y, [range[2], range[3]]),
|
||||
U::quantize(z, [range[4], range[5]]),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Quantizes a color component to a specific type.
|
||||
pub(super) trait QuantizedColor {
|
||||
fn quantize(color: f32, range: [f32; 2]) -> Self;
|
||||
}
|
||||
|
||||
impl QuantizedColor for u16 {
|
||||
fn quantize(color: f32, range: [f32; 2]) -> Self {
|
||||
let value = (color - range[0]) / (range[1] - range[0]);
|
||||
(value.max(0.0).min(1.0) * Self::MAX as f32)
|
||||
.round()
|
||||
.max(0.0)
|
||||
.min(Self::MAX as f32) as Self
|
||||
}
|
||||
}
|
||||
|
||||
impl QuantizedColor for f32 {
|
||||
fn quantize(color: f32, [min, max]: [f32; 2]) -> Self {
|
||||
color.clamp(min, max)
|
||||
}
|
||||
}
|
||||
|
275
crates/typst/src/export/pdf/gradient.rs
Normal file
@ -0,0 +1,275 @@
|
||||
use ecow::{eco_format, EcoString};
|
||||
use pdf_writer::types::FunctionShadingType;
|
||||
use pdf_writer::{types::ColorSpaceOperand, Name};
|
||||
use pdf_writer::{Finish, Ref};
|
||||
|
||||
use super::color::{ColorSpaceExt, PaintEncode};
|
||||
use super::page::{PageContext, Transforms};
|
||||
use super::{AbsExt, PdfContext, RefExt};
|
||||
use crate::geom::{
|
||||
Abs, Color, ColorSpace, Gradient, Numeric, Quadrant, Ratio, Relative, Transform,
|
||||
};
|
||||
|
||||
/// A unique-transform-aspect-ratio combination that will be encoded into the
|
||||
/// PDF.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct PdfGradient {
|
||||
/// The transform to apply to the gradient.
|
||||
pub transform: Transform,
|
||||
/// The aspect ratio of the gradient.
|
||||
/// Required for aspect ratio correction.
|
||||
pub aspect_ratio: Ratio,
|
||||
/// The gradient.
|
||||
pub gradient: Gradient,
|
||||
}
|
||||
|
||||
/// Writes the actual gradients (shading patterns) to the PDF.
|
||||
/// This is performed once after writing all pages.
|
||||
pub fn write_gradients(ctx: &mut PdfContext) {
|
||||
for PdfGradient { transform, aspect_ratio, gradient } in
|
||||
ctx.gradient_map.items().cloned().collect::<Vec<_>>()
|
||||
{
|
||||
let shading = ctx.alloc.bump();
|
||||
ctx.gradient_refs.push(shading);
|
||||
|
||||
let mut shading_pattern = match &gradient {
|
||||
Gradient::Linear(linear) => {
|
||||
let shading_function = shading_function(ctx, &gradient);
|
||||
let mut shading_pattern = ctx.writer.shading_pattern(shading);
|
||||
let mut shading = shading_pattern.function_shading();
|
||||
shading.shading_type(FunctionShadingType::Axial);
|
||||
|
||||
ctx.colors
|
||||
.write(gradient.space(), shading.color_space(), &mut ctx.alloc);
|
||||
|
||||
let angle = Gradient::correct_aspect_ratio(linear.angle, aspect_ratio);
|
||||
let (sin, cos) = (angle.sin(), angle.cos());
|
||||
let length = sin.abs() + cos.abs();
|
||||
|
||||
shading
|
||||
.anti_alias(gradient.anti_alias())
|
||||
.function(shading_function)
|
||||
.coords([0.0, 0.0, length as f32, 0.0])
|
||||
.extend([true; 2]);
|
||||
|
||||
shading.finish();
|
||||
|
||||
shading_pattern
|
||||
}
|
||||
};
|
||||
|
||||
shading_pattern.matrix(transform_to_array(transform));
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes an expotential or stitched function that expresses the gradient.
|
||||
fn shading_function(ctx: &mut PdfContext, gradient: &Gradient) -> Ref {
|
||||
let function = ctx.alloc.bump();
|
||||
let mut functions = vec![];
|
||||
let mut bounds = vec![];
|
||||
let mut encode = vec![];
|
||||
|
||||
// Create the individual gradient functions for each pair of stops.
|
||||
for window in gradient.stops_ref().windows(2) {
|
||||
let (first, second) = (window[0], window[1]);
|
||||
|
||||
// Skip stops with the same position.
|
||||
if first.1.get() == second.1.get() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the color space is HSL or HSV, and we cross the 0°/360° boundary,
|
||||
// we need to create two separate stops.
|
||||
if gradient.space() == ColorSpace::Hsl || gradient.space() == ColorSpace::Hsv {
|
||||
let t1 = first.1.get() as f32;
|
||||
let t2 = second.1.get() as f32;
|
||||
let [h1, s1, x1, _] = first.0.to_space(gradient.space()).to_vec4();
|
||||
let [h2, s2, x2, _] = second.0.to_space(gradient.space()).to_vec4();
|
||||
|
||||
// Compute the intermediary stop at 360°.
|
||||
if (h1 - h2).abs() > 180.0 {
|
||||
let h1 = if h1 < h2 { h1 + 360.0 } else { h1 };
|
||||
let h2 = if h2 < h1 { h2 + 360.0 } else { h2 };
|
||||
|
||||
// We compute where the crossing happens between zero and one
|
||||
let t = (360.0 - h1) / (h2 - h1);
|
||||
// We then map it back to the original range.
|
||||
let t_prime = t * (t2 - t1) + t1;
|
||||
|
||||
// If the crossing happens between the two stops,
|
||||
// we need to create an extra stop.
|
||||
if t_prime <= t2 && t_prime >= t1 {
|
||||
bounds.push(t_prime);
|
||||
bounds.push(t_prime);
|
||||
bounds.push(t2);
|
||||
encode.extend([0.0, 1.0]);
|
||||
encode.extend([0.0, 1.0]);
|
||||
encode.extend([0.0, 1.0]);
|
||||
|
||||
// These need to be individual function to encode 360.0 correctly.
|
||||
let func1 = ctx.alloc.bump();
|
||||
ctx.writer
|
||||
.exponential_function(func1)
|
||||
.range(gradient.space().range())
|
||||
.c0(gradient.space().convert(first.0))
|
||||
.c1([1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t])
|
||||
.domain([0.0, 1.0])
|
||||
.n(1.0);
|
||||
|
||||
let func2 = ctx.alloc.bump();
|
||||
ctx.writer
|
||||
.exponential_function(func2)
|
||||
.range(gradient.space().range())
|
||||
.c0([1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t])
|
||||
.c1([0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t])
|
||||
.domain([0.0, 1.0])
|
||||
.n(1.0);
|
||||
|
||||
let func3 = ctx.alloc.bump();
|
||||
ctx.writer
|
||||
.exponential_function(func3)
|
||||
.range(gradient.space().range())
|
||||
.c0([0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t])
|
||||
.c1(gradient.space().convert(second.0))
|
||||
.domain([0.0, 1.0])
|
||||
.n(1.0);
|
||||
|
||||
functions.push(func1);
|
||||
functions.push(func2);
|
||||
functions.push(func3);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bounds.push(second.1.get() as f32);
|
||||
functions.push(single_gradient(ctx, first.0, second.0, gradient.space()));
|
||||
encode.extend([0.0, 1.0]);
|
||||
}
|
||||
|
||||
// Special case for gradients with only two stops.
|
||||
if functions.len() == 1 {
|
||||
return functions[0];
|
||||
}
|
||||
|
||||
// Remove the last bound, since it's not needed for the stitching function.
|
||||
bounds.pop();
|
||||
|
||||
// Create the stitching function.
|
||||
ctx.writer
|
||||
.stitching_function(function)
|
||||
.domain([0.0, 1.0])
|
||||
.range(gradient.space().range())
|
||||
.functions(functions)
|
||||
.bounds(bounds)
|
||||
.encode(encode);
|
||||
|
||||
function
|
||||
}
|
||||
|
||||
/// Writes an expontential function that expresses a single segment (between two
|
||||
/// stops) of a gradient.
|
||||
fn single_gradient(
|
||||
ctx: &mut PdfContext,
|
||||
first_color: Color,
|
||||
second_color: Color,
|
||||
color_space: ColorSpace,
|
||||
) -> Ref {
|
||||
let reference = ctx.alloc.bump();
|
||||
|
||||
ctx.writer
|
||||
.exponential_function(reference)
|
||||
.range(color_space.range())
|
||||
.c0(color_space.convert(first_color))
|
||||
.c1(color_space.convert(second_color))
|
||||
.domain([0.0, 1.0])
|
||||
.n(1.0);
|
||||
|
||||
reference
|
||||
}
|
||||
|
||||
impl PaintEncode for Gradient {
|
||||
fn set_as_fill(&self, ctx: &mut PageContext, transforms: Transforms) {
|
||||
ctx.reset_fill_color_space();
|
||||
|
||||
let id = register_gradient(ctx, self, transforms);
|
||||
let name = Name(id.as_bytes());
|
||||
|
||||
ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern);
|
||||
ctx.content.set_fill_pattern(None, name);
|
||||
}
|
||||
|
||||
fn set_as_stroke(&self, ctx: &mut PageContext, transforms: Transforms) {
|
||||
ctx.reset_stroke_color_space();
|
||||
|
||||
let id = register_gradient(ctx, self, transforms);
|
||||
let name = Name(id.as_bytes());
|
||||
|
||||
ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern);
|
||||
ctx.content.set_stroke_pattern(None, name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Deduplicates a gradient to a named PDF resource.
|
||||
fn register_gradient(
|
||||
ctx: &mut PageContext,
|
||||
gradient: &Gradient,
|
||||
mut transforms: Transforms,
|
||||
) -> EcoString {
|
||||
// Edge cases for strokes.
|
||||
if transforms.size.x.is_zero() {
|
||||
transforms.size.x = Abs::pt(1.0);
|
||||
}
|
||||
|
||||
if transforms.size.y.is_zero() {
|
||||
transforms.size.y = Abs::pt(1.0);
|
||||
}
|
||||
|
||||
let size = match gradient.unwrap_relative(false) {
|
||||
Relative::Self_ => transforms.size,
|
||||
Relative::Parent => transforms.container_size,
|
||||
};
|
||||
|
||||
let (offset_x, offset_y) = match gradient.angle().quadrant() {
|
||||
Quadrant::First => (Abs::zero(), Abs::zero()),
|
||||
Quadrant::Second => (size.x, Abs::zero()),
|
||||
Quadrant::Third => (size.x, size.y),
|
||||
Quadrant::Fourth => (Abs::zero(), size.y),
|
||||
};
|
||||
|
||||
let transform = match gradient.unwrap_relative(false) {
|
||||
Relative::Self_ => transforms.transform,
|
||||
Relative::Parent => transforms.container_transform,
|
||||
};
|
||||
|
||||
let pdf_gradient = PdfGradient {
|
||||
aspect_ratio: size.aspect_ratio(),
|
||||
transform: transform
|
||||
.pre_concat(Transform::translate(offset_x, offset_y))
|
||||
.pre_concat(Transform::scale(
|
||||
Ratio::new(size.x.to_pt()),
|
||||
Ratio::new(size.y.to_pt()),
|
||||
))
|
||||
.pre_concat(Transform::rotate(Gradient::correct_aspect_ratio(
|
||||
gradient.angle(),
|
||||
size.aspect_ratio(),
|
||||
))),
|
||||
gradient: gradient.clone(),
|
||||
};
|
||||
|
||||
let index = ctx.parent.gradient_map.insert(pdf_gradient);
|
||||
eco_format!("Gr{}", index)
|
||||
}
|
||||
|
||||
/// Convert to an array of floats.
|
||||
fn transform_to_array(ts: Transform) -> [f32; 6] {
|
||||
[
|
||||
ts.sx.get() as f32,
|
||||
ts.ky.get() as f32,
|
||||
ts.kx.get() as f32,
|
||||
ts.sy.get() as f32,
|
||||
ts.tx.to_f32(),
|
||||
ts.ty.to_f32(),
|
||||
]
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
mod color;
|
||||
mod extg;
|
||||
mod font;
|
||||
mod gradient;
|
||||
mod image;
|
||||
mod outline;
|
||||
mod page;
|
||||
@ -21,6 +22,7 @@ use pdf_writer::writers::PageLabel;
|
||||
use pdf_writer::{Finish, Name, PdfWriter, Ref, TextStr};
|
||||
use xmp_writer::{LangId, RenditionClass, XmpWriter};
|
||||
|
||||
use self::gradient::PdfGradient;
|
||||
use self::page::Page;
|
||||
use crate::doc::{Document, Lang};
|
||||
use crate::font::Font;
|
||||
@ -39,6 +41,7 @@ pub fn pdf(document: &Document) -> Vec<u8> {
|
||||
page::construct_pages(&mut ctx, &document.pages);
|
||||
font::write_fonts(&mut ctx);
|
||||
image::write_images(&mut ctx);
|
||||
gradient::write_gradients(&mut ctx);
|
||||
extg::write_external_graphics_states(&mut ctx);
|
||||
page::write_page_tree(&mut ctx);
|
||||
write_catalog(&mut ctx);
|
||||
@ -57,10 +60,12 @@ pub struct PdfContext<'a> {
|
||||
page_tree_ref: Ref,
|
||||
font_refs: Vec<Ref>,
|
||||
image_refs: Vec<Ref>,
|
||||
gradient_refs: Vec<Ref>,
|
||||
ext_gs_refs: Vec<Ref>,
|
||||
page_refs: Vec<Ref>,
|
||||
font_map: Remapper<Font>,
|
||||
image_map: Remapper<Image>,
|
||||
gradient_map: Remapper<PdfGradient>,
|
||||
ext_gs_map: Remapper<ExternalGraphicsState>,
|
||||
/// For each font a mapping from used glyphs to their text representation.
|
||||
/// May contain multiple chars in case of ligatures or similar things. The
|
||||
@ -88,9 +93,11 @@ impl<'a> PdfContext<'a> {
|
||||
page_refs: vec![],
|
||||
font_refs: vec![],
|
||||
image_refs: vec![],
|
||||
gradient_refs: vec![],
|
||||
ext_gs_refs: vec![],
|
||||
font_map: Remapper::new(),
|
||||
image_map: Remapper::new(),
|
||||
gradient_map: Remapper::new(),
|
||||
ext_gs_map: Remapper::new(),
|
||||
glyph_sets: HashMap::new(),
|
||||
languages: HashMap::new(),
|
||||
@ -254,13 +261,13 @@ where
|
||||
Self { to_pdf: HashMap::new(), to_items: vec![] }
|
||||
}
|
||||
|
||||
fn insert(&mut self, item: T) {
|
||||
fn insert(&mut self, item: T) -> usize {
|
||||
let to_layout = &mut self.to_items;
|
||||
self.to_pdf.entry(item.clone()).or_insert_with(|| {
|
||||
*self.to_pdf.entry(item.clone()).or_insert_with(|| {
|
||||
let pdf_index = to_layout.len();
|
||||
to_layout.push(item);
|
||||
pdf_index
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn map(&self, item: &T) -> usize {
|
||||
|
@ -39,7 +39,7 @@ pub fn construct_page(ctx: &mut PdfContext, frame: &Frame) {
|
||||
label: None,
|
||||
uses_opacities: false,
|
||||
content: Content::new(),
|
||||
state: State::default(),
|
||||
state: State::new(frame.size()),
|
||||
saves: vec![],
|
||||
bottom: 0.0,
|
||||
links: vec![],
|
||||
@ -105,6 +105,14 @@ pub fn write_page_tree(ctx: &mut PdfContext) {
|
||||
|
||||
images.finish();
|
||||
|
||||
let mut patterns = resources.patterns();
|
||||
for (gradient_ref, gr) in ctx.gradient_map.pdf_indices(&ctx.gradient_refs) {
|
||||
let name = eco_format!("Gr{}", gr);
|
||||
patterns.pair(Name(name.as_bytes()), gradient_ref);
|
||||
}
|
||||
|
||||
patterns.finish();
|
||||
|
||||
let mut ext_gs_states = resources.ext_g_states();
|
||||
for (gs_ref, gs) in ctx.ext_gs_map.pdf_indices(&ctx.ext_gs_refs) {
|
||||
let name = eco_format!("Gs{}", gs);
|
||||
@ -211,9 +219,14 @@ pub struct PageContext<'a, 'b> {
|
||||
|
||||
/// A simulated graphics state used to deduplicate graphics state changes and
|
||||
/// keep track of the current transformation matrix for link annotations.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct State {
|
||||
/// The transform of the current item.
|
||||
transform: Transform,
|
||||
/// The transform of first hard frame in the hierarchy.
|
||||
container_transform: Transform,
|
||||
/// The size of the first hard frame in the hierarchy.
|
||||
size: Size,
|
||||
font: Option<(Font, Abs)>,
|
||||
fill: Option<Paint>,
|
||||
fill_space: Option<Name<'static>>,
|
||||
@ -222,6 +235,46 @@ struct State {
|
||||
stroke_space: Option<Name<'static>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new, clean state for a given page `size`.
|
||||
pub fn new(size: Size) -> Self {
|
||||
Self {
|
||||
transform: Transform::identity(),
|
||||
container_transform: Transform::identity(),
|
||||
size,
|
||||
font: None,
|
||||
fill: None,
|
||||
fill_space: None,
|
||||
external_graphics_state: None,
|
||||
stroke: None,
|
||||
stroke_space: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the [`Transforms`] structure for the current item.
|
||||
pub fn transforms(&self, size: Size, pos: Point) -> Transforms {
|
||||
Transforms {
|
||||
transform: self.transform.pre_concat(Transform::translate(pos.x, pos.y)),
|
||||
container_transform: self.container_transform,
|
||||
container_size: self.size,
|
||||
size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Subset of the state used to calculate the transform of gradients and patterns.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct Transforms {
|
||||
/// The transform of the current item.
|
||||
pub transform: Transform,
|
||||
/// The transform of first hard frame in the hierarchy.
|
||||
pub container_transform: Transform,
|
||||
/// The size of the first hard frame in the hierarchy.
|
||||
pub container_size: Size,
|
||||
/// The size of the item.
|
||||
pub size: Size,
|
||||
}
|
||||
|
||||
impl PageContext<'_, '_> {
|
||||
fn save_state(&mut self) {
|
||||
self.saves.push(self.state.clone());
|
||||
@ -249,13 +302,21 @@ impl PageContext<'_, '_> {
|
||||
fn set_opacities(&mut self, stroke: Option<&FixedStroke>, fill: Option<&Paint>) {
|
||||
let stroke_opacity = stroke
|
||||
.map(|stroke| {
|
||||
let Paint::Solid(color) = stroke.paint;
|
||||
let color = match &stroke.paint {
|
||||
Paint::Solid(color) => *color,
|
||||
Paint::Gradient(_) => return 255,
|
||||
};
|
||||
|
||||
color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
|
||||
})
|
||||
.unwrap_or(255);
|
||||
let fill_opacity = fill
|
||||
.map(|paint| {
|
||||
let Paint::Solid(color) = paint;
|
||||
let color = match paint {
|
||||
Paint::Solid(color) => *color,
|
||||
Paint::Gradient(_) => return 255,
|
||||
};
|
||||
|
||||
color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
|
||||
})
|
||||
.unwrap_or(255);
|
||||
@ -278,6 +339,11 @@ impl PageContext<'_, '_> {
|
||||
]);
|
||||
}
|
||||
|
||||
fn group_transform(&mut self, transform: Transform) {
|
||||
self.state.container_transform =
|
||||
self.state.container_transform.pre_concat(transform);
|
||||
}
|
||||
|
||||
fn set_font(&mut self, font: &Font, size: Abs) {
|
||||
if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) {
|
||||
self.parent.font_map.insert(font.clone());
|
||||
@ -287,9 +353,15 @@ impl PageContext<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_fill(&mut self, fill: &Paint) {
|
||||
if self.state.fill.as_ref() != Some(fill) {
|
||||
fill.set_as_fill(self);
|
||||
fn size(&mut self, size: Size) {
|
||||
self.state.size = size;
|
||||
}
|
||||
|
||||
fn set_fill(&mut self, fill: &Paint, transforms: Transforms) {
|
||||
if self.state.fill.as_ref() != Some(fill)
|
||||
|| matches!(self.state.fill, Some(Paint::Gradient(_)))
|
||||
{
|
||||
fill.set_as_fill(self, transforms);
|
||||
self.state.fill = Some(fill.clone());
|
||||
}
|
||||
}
|
||||
@ -305,8 +377,13 @@ impl PageContext<'_, '_> {
|
||||
self.state.fill_space = None;
|
||||
}
|
||||
|
||||
fn set_stroke(&mut self, stroke: &FixedStroke) {
|
||||
if self.state.stroke.as_ref() != Some(stroke) {
|
||||
fn set_stroke(&mut self, stroke: &FixedStroke, transforms: Transforms) {
|
||||
if self.state.stroke.as_ref() != Some(stroke)
|
||||
|| matches!(
|
||||
self.state.stroke.as_ref().map(|s| &s.paint),
|
||||
Some(Paint::Gradient(_))
|
||||
)
|
||||
{
|
||||
let FixedStroke {
|
||||
paint,
|
||||
thickness,
|
||||
@ -316,7 +393,7 @@ impl PageContext<'_, '_> {
|
||||
miter_limit,
|
||||
} = stroke;
|
||||
|
||||
paint.set_as_stroke(self);
|
||||
paint.set_as_stroke(self, transforms);
|
||||
|
||||
self.content.set_line_width(thickness.to_f32());
|
||||
if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) {
|
||||
@ -359,10 +436,11 @@ fn write_frame(ctx: &mut PageContext, frame: &Frame) {
|
||||
for &(pos, ref item) in frame.items() {
|
||||
let x = pos.x.to_f32();
|
||||
let y = pos.y.to_f32();
|
||||
|
||||
match item {
|
||||
FrameItem::Group(group) => write_group(ctx, pos, group),
|
||||
FrameItem::Text(text) => write_text(ctx, x, y, text),
|
||||
FrameItem::Shape(shape, _) => write_shape(ctx, x, y, shape),
|
||||
FrameItem::Text(text) => write_text(ctx, pos, text),
|
||||
FrameItem::Shape(shape, _) => write_shape(ctx, pos, shape),
|
||||
FrameItem::Image(image, size, _) => write_image(ctx, x, y, image, *size),
|
||||
FrameItem::Meta(meta, size) => match meta {
|
||||
Meta::Link(dest) => write_link(ctx, pos, dest, *size),
|
||||
@ -382,6 +460,11 @@ fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) {
|
||||
ctx.save_state();
|
||||
ctx.transform(translation.pre_concat(group.transform));
|
||||
|
||||
if group.frame.kind().is_hard() {
|
||||
ctx.group_transform(translation.pre_concat(group.transform));
|
||||
ctx.size(group.frame.size());
|
||||
}
|
||||
|
||||
if group.clips {
|
||||
let size = group.frame.size();
|
||||
let w = size.x.to_f32();
|
||||
@ -399,7 +482,10 @@ fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) {
|
||||
}
|
||||
|
||||
/// Encode a text run into the content stream.
|
||||
fn write_text(ctx: &mut PageContext, x: f32, y: f32, text: &TextItem) {
|
||||
fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
|
||||
let x = pos.x.to_f32();
|
||||
let y = pos.y.to_f32();
|
||||
|
||||
*ctx.parent.languages.entry(text.lang).or_insert(0) += text.glyphs.len();
|
||||
|
||||
let glyph_set = ctx.parent.glyph_sets.entry(text.font.clone()).or_default();
|
||||
@ -408,7 +494,7 @@ fn write_text(ctx: &mut PageContext, x: f32, y: f32, text: &TextItem) {
|
||||
glyph_set.entry(g.id).or_insert_with(|| segment.into());
|
||||
}
|
||||
|
||||
ctx.set_fill(&text.fill);
|
||||
ctx.set_fill(&text.fill, ctx.state.transforms(Size::zero(), pos));
|
||||
ctx.set_font(&text.font, text.size);
|
||||
ctx.set_opacities(None, Some(&text.fill));
|
||||
ctx.content.begin_text();
|
||||
@ -456,7 +542,10 @@ fn write_text(ctx: &mut PageContext, x: f32, y: f32, text: &TextItem) {
|
||||
}
|
||||
|
||||
/// Encode a geometrical shape into the content stream.
|
||||
fn write_shape(ctx: &mut PageContext, x: f32, y: f32, shape: &Shape) {
|
||||
fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) {
|
||||
let x = pos.x.to_f32();
|
||||
let y = pos.y.to_f32();
|
||||
|
||||
let stroke = shape.stroke.as_ref().and_then(|stroke| {
|
||||
if stroke.thickness.to_f32() > 0.0 {
|
||||
Some(stroke)
|
||||
@ -470,11 +559,11 @@ fn write_shape(ctx: &mut PageContext, x: f32, y: f32, shape: &Shape) {
|
||||
}
|
||||
|
||||
if let Some(fill) = &shape.fill {
|
||||
ctx.set_fill(fill);
|
||||
ctx.set_fill(fill, ctx.state.transforms(shape.geometry.bbox_size(), pos));
|
||||
}
|
||||
|
||||
if let Some(stroke) = stroke {
|
||||
ctx.set_stroke(stroke);
|
||||
ctx.set_stroke(stroke, ctx.state.transforms(shape.geometry.bbox_size(), pos));
|
||||
}
|
||||
|
||||
ctx.set_opacities(stroke, shape.fill.as_ref());
|
||||
|
@ -11,11 +11,11 @@ use tiny_skia as sk;
|
||||
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||
use usvg::{NodeExt, TreeParsing};
|
||||
|
||||
use crate::doc::{Frame, FrameItem, GroupItem, Meta, TextItem};
|
||||
use crate::doc::{Frame, FrameItem, FrameKind, GroupItem, Meta, TextItem};
|
||||
use crate::font::Font;
|
||||
use crate::geom::{
|
||||
self, Abs, Color, FixedStroke, Geometry, LineCap, LineJoin, Paint, PathItem, Shape,
|
||||
Size, Transform,
|
||||
self, Abs, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, Paint,
|
||||
PathItem, Point, Ratio, Relative, Shape, Size, Transform,
|
||||
};
|
||||
use crate::image::{Image, ImageKind, RasterFormat};
|
||||
|
||||
@ -32,7 +32,7 @@ pub fn render(frame: &Frame, pixel_per_pt: f32, fill: Color) -> sk::Pixmap {
|
||||
canvas.fill(fill.into());
|
||||
|
||||
let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt);
|
||||
render_frame(&mut canvas, ts, None, frame);
|
||||
render_frame(&mut canvas, State::new(size, ts, pixel_per_pt), frame);
|
||||
|
||||
canvas
|
||||
}
|
||||
@ -78,30 +78,84 @@ pub fn render_merged(
|
||||
canvas
|
||||
}
|
||||
|
||||
/// Render a frame into the canvas.
|
||||
fn render_frame(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: Option<&sk::Mask>,
|
||||
frame: &Frame,
|
||||
) {
|
||||
for (pos, item) in frame.items() {
|
||||
let x = pos.x.to_f32();
|
||||
let y = pos.y.to_f32();
|
||||
let ts = ts.pre_translate(x, y);
|
||||
/// Additional metadata carried through the rendering process.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct State<'a> {
|
||||
/// The transform of the current item.
|
||||
transform: sk::Transform,
|
||||
/// The transform of the first hard frame in the hierarchy.
|
||||
container_transform: sk::Transform,
|
||||
/// The mask of the current item.
|
||||
mask: Option<&'a sk::Mask>,
|
||||
/// The pixel per point ratio.
|
||||
pixel_per_pt: f32,
|
||||
/// The size of the first hard frame in the hierarchy.
|
||||
size: Size,
|
||||
}
|
||||
|
||||
impl<'a> State<'a> {
|
||||
fn new(size: Size, transform: sk::Transform, pixel_per_pt: f32) -> Self {
|
||||
Self {
|
||||
size,
|
||||
transform,
|
||||
container_transform: transform,
|
||||
pixel_per_pt,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre translate the current item's transform.
|
||||
fn pre_translate(self, pos: Point) -> Self {
|
||||
Self {
|
||||
transform: self.transform.pre_translate(pos.x.to_f32(), pos.y.to_f32()),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre concat the current item's transform.
|
||||
fn pre_concat(self, transform: sk::Transform) -> Self {
|
||||
Self {
|
||||
transform: self.transform.pre_concat(transform),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the current mask.
|
||||
fn with_mask(self, mask: Option<&sk::Mask>) -> State<'_> {
|
||||
// Ensure that we're using the parent's mask if we don't have one.
|
||||
if mask.is_some() {
|
||||
State { mask, ..self }
|
||||
} else {
|
||||
State { mask: None, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the size of the first hard frame in the hierarchy.
|
||||
fn with_size(self, size: Size) -> Self {
|
||||
Self { size, ..self }
|
||||
}
|
||||
|
||||
/// Pre concat the container's transform.
|
||||
fn pre_concat_container(self, container_transform: sk::Transform) -> Self {
|
||||
Self { container_transform, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a frame into the canvas.
|
||||
fn render_frame(canvas: &mut sk::Pixmap, state: State, frame: &Frame) {
|
||||
for (pos, item) in frame.items() {
|
||||
match item {
|
||||
FrameItem::Group(group) => {
|
||||
render_group(canvas, ts, mask, group);
|
||||
render_group(canvas, state.pre_translate(*pos), group);
|
||||
}
|
||||
FrameItem::Text(text) => {
|
||||
render_text(canvas, ts, mask, text);
|
||||
render_text(canvas, state.pre_translate(*pos), text);
|
||||
}
|
||||
FrameItem::Shape(shape, _) => {
|
||||
render_shape(canvas, ts, mask, shape);
|
||||
render_shape(canvas, state.pre_translate(*pos), shape);
|
||||
}
|
||||
FrameItem::Image(image, size, _) => {
|
||||
render_image(canvas, ts, mask, image, *size);
|
||||
render_image(canvas, state.pre_translate(*pos), image, *size);
|
||||
}
|
||||
FrameItem::Meta(meta, _) => match meta {
|
||||
Meta::Link(_) => {}
|
||||
@ -115,23 +169,24 @@ fn render_frame(
|
||||
}
|
||||
|
||||
/// Render a group frame with optional transform and clipping into the canvas.
|
||||
fn render_group(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: Option<&sk::Mask>,
|
||||
group: &GroupItem,
|
||||
) {
|
||||
let ts = ts.pre_concat(group.transform.into());
|
||||
fn render_group(canvas: &mut sk::Pixmap, state: State, group: &GroupItem) {
|
||||
let state = match group.frame.kind() {
|
||||
FrameKind::Soft => state.pre_concat(group.transform.into()),
|
||||
FrameKind::Hard => state
|
||||
.pre_concat(group.transform.into())
|
||||
.pre_concat_container(group.transform.into())
|
||||
.with_size(group.frame.size()),
|
||||
};
|
||||
|
||||
let mut mask = mask;
|
||||
let mut mask = state.mask;
|
||||
let storage;
|
||||
if group.clips {
|
||||
let size = group.frame.size();
|
||||
let size: geom::Axes<Abs> = group.frame.size();
|
||||
let w = size.x.to_f32();
|
||||
let h = size.y.to_f32();
|
||||
if let Some(path) = sk::Rect::from_xywh(0.0, 0.0, w, h)
|
||||
.map(sk::PathBuilder::from_rect)
|
||||
.and_then(|path| path.transform(ts))
|
||||
.and_then(|path| path.transform(state.transform))
|
||||
{
|
||||
if let Some(mask) = mask {
|
||||
let mut mask = mask.clone();
|
||||
@ -164,25 +219,20 @@ fn render_group(
|
||||
}
|
||||
}
|
||||
|
||||
render_frame(canvas, ts, mask, &group.frame);
|
||||
render_frame(canvas, state.with_mask(mask), &group.frame);
|
||||
}
|
||||
|
||||
/// Render a text run into the canvas.
|
||||
fn render_text(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: Option<&sk::Mask>,
|
||||
text: &TextItem,
|
||||
) {
|
||||
fn render_text(canvas: &mut sk::Pixmap, state: State, text: &TextItem) {
|
||||
let mut x = 0.0;
|
||||
for glyph in &text.glyphs {
|
||||
let id = GlyphId(glyph.id);
|
||||
let offset = x + glyph.x_offset.at(text.size).to_f32();
|
||||
let ts = ts.pre_translate(offset, 0.0);
|
||||
let state = state.pre_translate(Point::new(Abs::raw(offset as _), Abs::raw(0.0)));
|
||||
|
||||
render_svg_glyph(canvas, ts, mask, text, id)
|
||||
.or_else(|| render_bitmap_glyph(canvas, ts, mask, text, id))
|
||||
.or_else(|| render_outline_glyph(canvas, ts, mask, text, id));
|
||||
render_svg_glyph(canvas, state, text, id)
|
||||
.or_else(|| render_bitmap_glyph(canvas, state, text, id))
|
||||
.or_else(|| render_outline_glyph(canvas, state, text, id));
|
||||
|
||||
x += glyph.x_advance.at(text.size).to_f32();
|
||||
}
|
||||
@ -191,11 +241,11 @@ fn render_text(
|
||||
/// Render an SVG glyph into the canvas.
|
||||
fn render_svg_glyph(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: Option<&sk::Mask>,
|
||||
state: State,
|
||||
text: &TextItem,
|
||||
id: GlyphId,
|
||||
) -> Option<()> {
|
||||
let ts = &state.transform;
|
||||
let mut data = text.font.ttf().glyph_svg_image(id)?;
|
||||
|
||||
// Decompress SVGZ.
|
||||
@ -269,7 +319,7 @@ fn render_svg_glyph(
|
||||
pixmap.as_ref(),
|
||||
&sk::PixmapPaint::default(),
|
||||
sk::Transform::identity(),
|
||||
mask,
|
||||
state.mask,
|
||||
);
|
||||
|
||||
Some(())
|
||||
@ -278,11 +328,11 @@ fn render_svg_glyph(
|
||||
/// Render a bitmap glyph into the canvas.
|
||||
fn render_bitmap_glyph(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: Option<&sk::Mask>,
|
||||
state: State,
|
||||
text: &TextItem,
|
||||
id: GlyphId,
|
||||
) -> Option<()> {
|
||||
let ts = state.transform;
|
||||
let size = text.size.to_f32();
|
||||
let ppem = size * ts.sy;
|
||||
let raster = text.font.ttf().glyph_raster_image(id, ppem as u16)?;
|
||||
@ -298,18 +348,22 @@ fn render_bitmap_glyph(
|
||||
let w = (image.width() as f64 / image.height() as f64) * h;
|
||||
let dx = (raster.x as f32) / (image.width() as f32) * size;
|
||||
let dy = (raster.y as f32) / (image.height() as f32) * size;
|
||||
let ts = ts.pre_translate(dx, -size - dy);
|
||||
render_image(canvas, ts, mask, &image, Size::new(w, h))
|
||||
render_image(
|
||||
canvas,
|
||||
state.pre_translate(Point::new(Abs::raw(dx as _), Abs::raw((-size - dy) as _))),
|
||||
&image,
|
||||
Size::new(w, h),
|
||||
)
|
||||
}
|
||||
|
||||
/// Render an outline glyph into the canvas. This is the "normal" case.
|
||||
fn render_outline_glyph(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: Option<&sk::Mask>,
|
||||
state: State,
|
||||
text: &TextItem,
|
||||
id: GlyphId,
|
||||
) -> Option<()> {
|
||||
let ts = &state.transform;
|
||||
let ppem = text.size.to_f32() * ts.sy;
|
||||
|
||||
// Render a glyph directly as a path. This only happens when the fast glyph
|
||||
@ -322,14 +376,17 @@ fn render_outline_glyph(
|
||||
builder.0.finish()?
|
||||
};
|
||||
|
||||
let paint = (&text.fill).into();
|
||||
// TODO: Implement gradients on text.
|
||||
let mut pixmap = None;
|
||||
let paint = to_sk_paint(&text.fill, state, Size::zero(), None, &mut pixmap);
|
||||
|
||||
let rule = sk::FillRule::default();
|
||||
|
||||
// Flip vertically because font design coordinate
|
||||
// system is Y-up.
|
||||
let scale = text.size.to_f32() / text.font.units_per_em() as f32;
|
||||
let ts = ts.pre_scale(scale, -scale);
|
||||
canvas.fill_path(&path, &paint, rule, ts, mask);
|
||||
canvas.fill_path(&path, &paint, rule, ts, state.mask);
|
||||
return Some(());
|
||||
}
|
||||
|
||||
@ -357,11 +414,11 @@ fn render_outline_glyph(
|
||||
|
||||
// If we have a clip mask we first render to a pixmap that we then blend
|
||||
// with our canvas
|
||||
if mask.is_some() {
|
||||
if state.mask.is_some() {
|
||||
let mw = bitmap.width;
|
||||
let mh = bitmap.height;
|
||||
|
||||
let Paint::Solid(color) = text.fill;
|
||||
let color = text.fill.unwrap_solid();
|
||||
let color = sk::ColorU8::from(color);
|
||||
|
||||
// Pad the pixmap with 1 pixel in each dimension so that we do
|
||||
@ -391,7 +448,7 @@ fn render_outline_glyph(
|
||||
pixmap.as_ref(),
|
||||
&sk::PixmapPaint::default(),
|
||||
sk::Transform::identity(),
|
||||
mask,
|
||||
state.mask,
|
||||
);
|
||||
} else {
|
||||
let cw = canvas.width() as i32;
|
||||
@ -406,7 +463,7 @@ fn render_outline_glyph(
|
||||
let bottom = top + mh;
|
||||
|
||||
// Premultiply the text color.
|
||||
let Paint::Solid(color) = text.fill;
|
||||
let Paint::Solid(color) = text.fill else { todo!() };
|
||||
let color = bytemuck::cast(sk::ColorU8::from(color).premultiply());
|
||||
|
||||
// Blend the glyph bitmap with the existing pixels on the canvas.
|
||||
@ -435,12 +492,8 @@ fn render_outline_glyph(
|
||||
}
|
||||
|
||||
/// Render a geometrical shape into the canvas.
|
||||
fn render_shape(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: Option<&sk::Mask>,
|
||||
shape: &Shape,
|
||||
) -> Option<()> {
|
||||
fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option<()> {
|
||||
let ts = state.transform;
|
||||
let path = match shape.geometry {
|
||||
Geometry::Line(target) => {
|
||||
let mut builder = sk::PathBuilder::new();
|
||||
@ -457,13 +510,16 @@ fn render_shape(
|
||||
};
|
||||
|
||||
if let Some(fill) = &shape.fill {
|
||||
let mut paint: sk::Paint = fill.into();
|
||||
let mut pixmap = None;
|
||||
let mut paint: sk::Paint =
|
||||
to_sk_paint(fill, state, shape.geometry.bbox_size(), None, &mut pixmap);
|
||||
|
||||
if matches!(shape.geometry, Geometry::Rect(_)) {
|
||||
paint.anti_alias = false;
|
||||
}
|
||||
|
||||
let rule = sk::FillRule::default();
|
||||
canvas.fill_path(&path, &paint, rule, ts, mask);
|
||||
canvas.fill_path(&path, &paint, rule, ts, state.mask);
|
||||
}
|
||||
|
||||
if let Some(FixedStroke {
|
||||
@ -490,7 +546,11 @@ fn render_shape(
|
||||
|
||||
sk::StrokeDash::new(dash_array, pattern.phase.to_f32())
|
||||
});
|
||||
let paint = paint.into();
|
||||
|
||||
let mut pixmap = None;
|
||||
let paint =
|
||||
to_sk_paint(paint, state, shape.geometry.bbox_size(), None, &mut pixmap);
|
||||
|
||||
let stroke = sk::Stroke {
|
||||
width,
|
||||
line_cap: line_cap.into(),
|
||||
@ -498,7 +558,7 @@ fn render_shape(
|
||||
dash,
|
||||
miter_limit: miter_limit.get() as f32,
|
||||
};
|
||||
canvas.stroke_path(&path, &paint, &stroke, ts, mask);
|
||||
canvas.stroke_path(&path, &paint, &stroke, ts, state.mask);
|
||||
}
|
||||
}
|
||||
|
||||
@ -537,11 +597,11 @@ fn convert_path(path: &geom::Path) -> Option<sk::Path> {
|
||||
/// Render a raster or SVG image into the canvas.
|
||||
fn render_image(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: Option<&sk::Mask>,
|
||||
state: State,
|
||||
image: &Image,
|
||||
size: Size,
|
||||
) -> Option<()> {
|
||||
let ts = state.transform;
|
||||
let view_width = size.x.to_f32();
|
||||
let view_height = size.y.to_f32();
|
||||
|
||||
@ -576,7 +636,7 @@ fn render_image(
|
||||
};
|
||||
|
||||
let rect = sk::Rect::from_xywh(0.0, 0.0, view_width, view_height)?;
|
||||
canvas.fill_rect(rect, &paint, ts, mask);
|
||||
canvas.fill_rect(rect, &paint, ts, state.mask);
|
||||
|
||||
Some(())
|
||||
}
|
||||
@ -626,16 +686,93 @@ impl From<Transform> for sk::Transform {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Paint> for sk::Paint<'static> {
|
||||
fn from(paint: &Paint) -> Self {
|
||||
let mut sk_paint = sk::Paint::default();
|
||||
let Paint::Solid(color) = *paint;
|
||||
sk_paint.set_color(color.into());
|
||||
sk_paint.anti_alias = true;
|
||||
sk_paint
|
||||
impl From<sk::Transform> for Transform {
|
||||
fn from(value: sk::Transform) -> Self {
|
||||
let sk::Transform { sx, ky, kx, sy, tx, ty } = value;
|
||||
Self {
|
||||
sx: Ratio::new(sx as _),
|
||||
ky: Ratio::new(ky as _),
|
||||
kx: Ratio::new(kx as _),
|
||||
sy: Ratio::new(sy as _),
|
||||
tx: Abs::raw(tx as _),
|
||||
ty: Abs::raw(ty as _),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms a [`Paint`] into a [`sk::Paint`].
|
||||
/// Applying the necessary transform, if the paint is a gradient.
|
||||
fn to_sk_paint<'a>(
|
||||
paint: &Paint,
|
||||
state: State,
|
||||
item_size: Size,
|
||||
fill_transform: Option<sk::Transform>,
|
||||
pixmap: &'a mut Option<Arc<sk::Pixmap>>,
|
||||
) -> sk::Paint<'a> {
|
||||
/// Actual sampling of the gradient, cached for performance.
|
||||
#[comemo::memoize]
|
||||
fn cached(gradient: &Gradient, width: u32, height: u32) -> Arc<sk::Pixmap> {
|
||||
let mut pixmap = sk::Pixmap::new(width.max(1), height.max(1)).unwrap();
|
||||
for x in 0..width {
|
||||
for y in 0..height {
|
||||
let color: sk::Color = gradient
|
||||
.sample_at((x as f32, y as f32), (width as f32, height as f32))
|
||||
.into();
|
||||
|
||||
pixmap.pixels_mut()[(y * width + x) as usize] =
|
||||
color.premultiply().to_color_u8();
|
||||
}
|
||||
}
|
||||
|
||||
Arc::new(pixmap)
|
||||
}
|
||||
|
||||
let mut sk_paint: sk::Paint<'_> = sk::Paint::default();
|
||||
match paint {
|
||||
Paint::Solid(color) => {
|
||||
sk_paint.set_color((*color).into());
|
||||
sk_paint.anti_alias = true;
|
||||
}
|
||||
Paint::Gradient(gradient) => {
|
||||
let container_size = match gradient.unwrap_relative(false) {
|
||||
Relative::Self_ => item_size,
|
||||
Relative::Parent => state.size,
|
||||
};
|
||||
|
||||
let fill_transform =
|
||||
fill_transform.unwrap_or_else(|| match gradient.unwrap_relative(false) {
|
||||
Relative::Self_ => sk::Transform::identity(),
|
||||
Relative::Parent => state
|
||||
.container_transform
|
||||
.post_concat(state.transform.invert().unwrap()),
|
||||
});
|
||||
let width = (container_size.x.to_f32() * state.pixel_per_pt).ceil() as u32;
|
||||
let height = (container_size.y.to_f32() * state.pixel_per_pt).ceil() as u32;
|
||||
|
||||
*pixmap = Some(cached(
|
||||
gradient,
|
||||
width.max(state.pixel_per_pt.ceil() as u32),
|
||||
height.max(state.pixel_per_pt.ceil() as u32),
|
||||
));
|
||||
|
||||
// We can use FilterQuality::Nearest here because we're
|
||||
// rendering to a pixmap that is already at native resolution.
|
||||
sk_paint.shader = sk::Pattern::new(
|
||||
pixmap.as_ref().unwrap().as_ref().as_ref(),
|
||||
sk::SpreadMode::Pad,
|
||||
sk::FilterQuality::Nearest,
|
||||
1.0,
|
||||
fill_transform
|
||||
.pre_scale(1.0 / state.pixel_per_pt, 1.0 / state.pixel_per_pt),
|
||||
);
|
||||
|
||||
sk_paint.anti_alias = gradient.anti_alias();
|
||||
}
|
||||
}
|
||||
|
||||
sk_paint
|
||||
}
|
||||
|
||||
impl From<Color> for sk::Color {
|
||||
fn from(color: Color) -> Self {
|
||||
let [r, g, b, a] = color.to_rgba().to_vec4_u8();
|
||||
|
@ -7,11 +7,11 @@ use ecow::{eco_format, EcoString};
|
||||
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||
use xmlwriter::XmlWriter;
|
||||
|
||||
use crate::doc::{Frame, FrameItem, GroupItem, TextItem};
|
||||
use crate::doc::{Frame, FrameItem, FrameKind, GroupItem, TextItem};
|
||||
use crate::font::Font;
|
||||
use crate::geom::{
|
||||
Abs, Angle, Axes, Color, FixedStroke, Geometry, LineCap, LineJoin, Paint, PathItem,
|
||||
Ratio, Shape, Size, Transform,
|
||||
Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, Paint,
|
||||
PathItem, Point, Quadrant, Ratio, RatioOrAngle, Relative, Shape, Size, Transform,
|
||||
};
|
||||
use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat};
|
||||
use crate::util::hash128;
|
||||
@ -21,7 +21,9 @@ use crate::util::hash128;
|
||||
pub fn svg(frame: &Frame) -> String {
|
||||
let mut renderer = SVGRenderer::new();
|
||||
renderer.write_header(frame.size());
|
||||
renderer.render_frame(frame, Transform::identity());
|
||||
|
||||
let state = State::new(frame.size(), Transform::identity());
|
||||
renderer.render_frame(state, Transform::identity(), frame);
|
||||
renderer.finalize()
|
||||
}
|
||||
|
||||
@ -40,7 +42,9 @@ pub fn svg_merged(frames: &[Frame], padding: Abs) -> String {
|
||||
|
||||
let [x, mut y] = [padding; 2];
|
||||
for frame in frames {
|
||||
renderer.render_frame(frame, Transform::translate(x, y));
|
||||
let ts = Transform::translate(x, y);
|
||||
let state = State::new(frame.size(), ts);
|
||||
renderer.render_frame(state, ts, frame);
|
||||
y += frame.height() + padding;
|
||||
}
|
||||
|
||||
@ -58,6 +62,87 @@ struct SVGRenderer {
|
||||
/// attribute of the group. The clip path is in the format of `M x y L x y C
|
||||
/// x1 y1 x2 y2 x y Z`.
|
||||
clip_paths: Deduplicator<EcoString>,
|
||||
/// Deduplicated gradients with transform matrices. They use a reference
|
||||
/// (`href`) to a "source" gradient instead of being defined inline.
|
||||
/// This saves a lot of space since gradients are often reused but with
|
||||
/// different transforms. Therefore this allows us to reuse the same gradient
|
||||
/// multiple times.
|
||||
gradient_refs: Deduplicator<GradientRef>,
|
||||
/// These are the actual gradients being written in the SVG file.
|
||||
/// These gradients are deduplicated because they do not contain the transform
|
||||
/// matrix, allowing them to be reused across multiple invocations.
|
||||
///
|
||||
/// The `Ratio` is the aspect ratio of the gradient, this is used to correct
|
||||
/// the angle of the gradient.
|
||||
gradients: Deduplicator<(Gradient, Ratio)>,
|
||||
}
|
||||
|
||||
/// Contextual information for rendering.
|
||||
#[derive(Clone, Copy)]
|
||||
struct State {
|
||||
/// The transform of the current item.
|
||||
transform: Transform,
|
||||
/// The size of the first hard frame in the hierarchy.
|
||||
size: Size,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn new(size: Size, transform: Transform) -> Self {
|
||||
Self { size, transform }
|
||||
}
|
||||
|
||||
/// Pre translate the current item's transform.
|
||||
fn pre_translate(self, pos: Point) -> Self {
|
||||
self.pre_concat(Transform::translate(pos.x, pos.y))
|
||||
}
|
||||
|
||||
/// Pre concat the current item's transform.
|
||||
fn pre_concat(self, transform: Transform) -> Self {
|
||||
Self {
|
||||
transform: self.transform.pre_concat(transform),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the size of the first hard frame in the hierarchy.
|
||||
fn with_size(self, size: Size) -> Self {
|
||||
Self { size, ..self }
|
||||
}
|
||||
|
||||
/// Sets the current item's transform.
|
||||
fn with_transform(self, transform: Transform) -> Self {
|
||||
Self { transform, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference to a deduplicated gradient, with a transform matrix.
|
||||
///
|
||||
/// Allows gradients to be reused across multiple invocations,
|
||||
/// simply by changing the transform matrix.
|
||||
#[derive(Hash)]
|
||||
struct GradientRef {
|
||||
/// The ID of the deduplicated gradient
|
||||
id: Id,
|
||||
/// The gradient kind (used to determine the SVG element to use)
|
||||
/// but without needing to clone the entire gradient.
|
||||
kind: GradientKind,
|
||||
/// The transform matrix to apply to the gradient.
|
||||
transform: Transform,
|
||||
}
|
||||
|
||||
/// The kind of linear gradient.
|
||||
#[derive(Hash, Clone, Copy, PartialEq, Eq)]
|
||||
enum GradientKind {
|
||||
/// A linear gradient.
|
||||
Linear,
|
||||
}
|
||||
|
||||
impl From<&Gradient> for GradientKind {
|
||||
fn from(value: &Gradient) -> Self {
|
||||
match value {
|
||||
Gradient::Linear { .. } => GradientKind::Linear,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a glyph to be rendered.
|
||||
@ -79,6 +164,8 @@ impl SVGRenderer {
|
||||
xml: XmlWriter::new(xmlwriter::Options::default()),
|
||||
glyphs: Deduplicator::new('g'),
|
||||
clip_paths: Deduplicator::new('c'),
|
||||
gradient_refs: Deduplicator::new('g'),
|
||||
gradients: Deduplicator::new('f'),
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,13 +187,18 @@ impl SVGRenderer {
|
||||
}
|
||||
|
||||
/// Render a frame with the given transform.
|
||||
fn render_frame(&mut self, frame: &Frame, ts: Transform) {
|
||||
fn render_frame(&mut self, state: State, ts: Transform, frame: &Frame) {
|
||||
self.xml.start_element("g");
|
||||
if !ts.is_identity() {
|
||||
self.xml.write_attribute("transform", &SvgMatrix(ts));
|
||||
}
|
||||
|
||||
for (pos, item) in frame.items() {
|
||||
// File size optimization
|
||||
if matches!(item, FrameItem::Meta(_, _)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let x = pos.x.to_pt();
|
||||
let y = pos.y.to_pt();
|
||||
self.xml.start_element("g");
|
||||
@ -114,11 +206,17 @@ impl SVGRenderer {
|
||||
.write_attribute_fmt("transform", format_args!("translate({x} {y})"));
|
||||
|
||||
match item {
|
||||
FrameItem::Group(group) => self.render_group(group),
|
||||
FrameItem::Text(text) => self.render_text(text),
|
||||
FrameItem::Shape(shape, _) => self.render_shape(shape),
|
||||
FrameItem::Group(group) => {
|
||||
self.render_group(state.pre_translate(*pos), group)
|
||||
}
|
||||
FrameItem::Text(text) => {
|
||||
self.render_text(state.pre_translate(*pos), text)
|
||||
}
|
||||
FrameItem::Shape(shape, _) => {
|
||||
self.render_shape(state.pre_translate(*pos), shape)
|
||||
}
|
||||
FrameItem::Image(image, size, _) => self.render_image(image, size),
|
||||
FrameItem::Meta(_, _) => {}
|
||||
FrameItem::Meta(_, _) => unreachable!(),
|
||||
};
|
||||
|
||||
self.xml.end_element();
|
||||
@ -129,7 +227,14 @@ impl SVGRenderer {
|
||||
|
||||
/// Render a group. If the group has `clips` set to true, a clip path will
|
||||
/// be created.
|
||||
fn render_group(&mut self, group: &GroupItem) {
|
||||
fn render_group(&mut self, state: State, group: &GroupItem) {
|
||||
let state = match group.frame.kind() {
|
||||
FrameKind::Soft => state.pre_concat(group.transform),
|
||||
FrameKind::Hard => {
|
||||
state.with_transform(group.transform).with_size(group.frame.size())
|
||||
}
|
||||
};
|
||||
|
||||
self.xml.start_element("g");
|
||||
self.xml.write_attribute("class", "typst-group");
|
||||
|
||||
@ -146,14 +251,15 @@ impl SVGRenderer {
|
||||
self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})"));
|
||||
}
|
||||
|
||||
self.render_frame(&group.frame, group.transform);
|
||||
self.render_frame(state, group.transform, &group.frame);
|
||||
self.xml.end_element();
|
||||
}
|
||||
|
||||
/// Render a text item. The text is rendered as a group of glyphs. We will
|
||||
/// try to render the text as SVG first, then bitmap, then outline. If none
|
||||
/// of them works, we will skip the text.
|
||||
fn render_text(&mut self, text: &TextItem) {
|
||||
// TODO: implement gradient on text.
|
||||
fn render_text(&mut self, _state: State, text: &TextItem) {
|
||||
let scale: f64 = text.size.to_pt() / text.font.units_per_em();
|
||||
let inv_scale: f64 = text.font.units_per_em() / text.size.to_pt();
|
||||
|
||||
@ -270,25 +376,33 @@ impl SVGRenderer {
|
||||
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
|
||||
self.xml
|
||||
.write_attribute_fmt("x", format_args!("{}", x_offset * inv_scale));
|
||||
self.write_fill(&text.fill);
|
||||
self.write_fill(&text.fill, Size::zero(), Transform::identity());
|
||||
self.xml.end_element();
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Render a shape element.
|
||||
fn render_shape(&mut self, shape: &Shape) {
|
||||
fn render_shape(&mut self, state: State, shape: &Shape) {
|
||||
self.xml.start_element("path");
|
||||
self.xml.write_attribute("class", "typst-shape");
|
||||
|
||||
if let Some(paint) = &shape.fill {
|
||||
self.write_fill(paint);
|
||||
self.write_fill(
|
||||
paint,
|
||||
self.shape_fill_size(state, paint, shape),
|
||||
self.shape_paint_transform(state, paint, shape),
|
||||
);
|
||||
} else {
|
||||
self.xml.write_attribute("fill", "none");
|
||||
}
|
||||
|
||||
if let Some(stroke) = &shape.stroke {
|
||||
self.write_stroke(stroke);
|
||||
self.write_stroke(
|
||||
stroke,
|
||||
self.shape_fill_size(state, &stroke.paint, shape),
|
||||
self.shape_paint_transform(state, &stroke.paint, shape),
|
||||
);
|
||||
}
|
||||
|
||||
let path = convert_geometry_to_path(&shape.geometry);
|
||||
@ -296,16 +410,114 @@ impl SVGRenderer {
|
||||
self.xml.end_element();
|
||||
}
|
||||
|
||||
/// Calculate the transform of the shape's fill or stroke.
|
||||
fn shape_paint_transform(
|
||||
&self,
|
||||
state: State,
|
||||
paint: &Paint,
|
||||
shape: &Shape,
|
||||
) -> Transform {
|
||||
let mut shape_size = shape.geometry.bbox_size();
|
||||
// Edge cases for strokes.
|
||||
if shape_size.x.to_pt() == 0.0 {
|
||||
shape_size.x = Abs::pt(1.0);
|
||||
}
|
||||
|
||||
if shape_size.y.to_pt() == 0.0 {
|
||||
shape_size.y = Abs::pt(1.0);
|
||||
}
|
||||
|
||||
if let Paint::Gradient(gradient) = paint {
|
||||
match gradient.unwrap_relative(false) {
|
||||
Relative::Self_ => Transform::scale(
|
||||
Ratio::new(shape_size.x.to_pt()),
|
||||
Ratio::new(shape_size.y.to_pt()),
|
||||
),
|
||||
Relative::Parent => Transform::scale(
|
||||
Ratio::new(state.size.x.to_pt()),
|
||||
Ratio::new(state.size.y.to_pt()),
|
||||
)
|
||||
.post_concat(state.transform.invert().unwrap()),
|
||||
}
|
||||
} else {
|
||||
Transform::identity()
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the size of the shape's fill.
|
||||
fn shape_fill_size(&self, state: State, paint: &Paint, shape: &Shape) -> Size {
|
||||
let mut shape_size = shape.geometry.bbox_size();
|
||||
// Edge cases for strokes.
|
||||
if shape_size.x.to_pt() == 0.0 {
|
||||
shape_size.x = Abs::pt(1.0);
|
||||
}
|
||||
|
||||
if shape_size.y.to_pt() == 0.0 {
|
||||
shape_size.y = Abs::pt(1.0);
|
||||
}
|
||||
|
||||
if let Paint::Gradient(gradient) = paint {
|
||||
match gradient.unwrap_relative(false) {
|
||||
Relative::Self_ => shape_size,
|
||||
Relative::Parent => state.size,
|
||||
}
|
||||
} else {
|
||||
shape_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a fill attribute.
|
||||
fn write_fill(&mut self, fill: &Paint) {
|
||||
let Paint::Solid(color) = fill;
|
||||
self.xml.write_attribute("fill", &color.encode());
|
||||
fn write_fill(&mut self, fill: &Paint, size: Size, ts: Transform) {
|
||||
match fill {
|
||||
Paint::Solid(color) => self.xml.write_attribute("fill", &color.encode()),
|
||||
Paint::Gradient(gradient) => {
|
||||
let id = self.push_gradient(gradient, size, ts);
|
||||
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes a gradient to the list of gradients to write SVG file.
|
||||
///
|
||||
/// If the gradient is already present, returns the id of the existing
|
||||
/// gradient. Otherwise, inserts the gradient and returns the id of the
|
||||
/// inserted gradient. If the transform of the gradient is the identify
|
||||
/// matrix, the returned ID will be the ID of the "source" gradient,
|
||||
/// this is a file size optimization.
|
||||
fn push_gradient(&mut self, gradient: &Gradient, size: Size, ts: Transform) -> Id {
|
||||
let gradient_id = self
|
||||
.gradients
|
||||
.insert_with(hash128(&(gradient, size.aspect_ratio())), || {
|
||||
(gradient.clone(), size.aspect_ratio())
|
||||
});
|
||||
|
||||
if ts.is_identity() {
|
||||
return gradient_id;
|
||||
}
|
||||
|
||||
self.gradient_refs
|
||||
.insert_with(hash128(&(gradient_id, ts)), || GradientRef {
|
||||
id: gradient_id,
|
||||
kind: gradient.into(),
|
||||
transform: ts,
|
||||
})
|
||||
}
|
||||
|
||||
/// Write a stroke attribute.
|
||||
fn write_stroke(&mut self, stroke: &FixedStroke) {
|
||||
let Paint::Solid(color) = stroke.paint;
|
||||
self.xml.write_attribute("stroke", &color.encode());
|
||||
fn write_stroke(
|
||||
&mut self,
|
||||
stroke: &FixedStroke,
|
||||
size: Size,
|
||||
fill_transform: Transform,
|
||||
) {
|
||||
match &stroke.paint {
|
||||
Paint::Solid(color) => self.xml.write_attribute("stroke", &color.encode()),
|
||||
Paint::Gradient(gradient) => {
|
||||
let id = self.push_gradient(gradient, size, fill_transform);
|
||||
self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})"));
|
||||
}
|
||||
}
|
||||
|
||||
self.xml.write_attribute("stroke-width", &stroke.thickness.to_pt());
|
||||
self.xml.write_attribute(
|
||||
"stroke-linecap",
|
||||
@ -354,11 +566,17 @@ impl SVGRenderer {
|
||||
fn finalize(mut self) -> String {
|
||||
self.write_glyph_defs();
|
||||
self.write_clip_path_defs();
|
||||
self.write_gradients();
|
||||
self.write_gradient_refs();
|
||||
self.xml.end_document()
|
||||
}
|
||||
|
||||
/// Build the glyph definitions.
|
||||
fn write_glyph_defs(&mut self) {
|
||||
if self.glyphs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.xml.start_element("defs");
|
||||
self.xml.write_attribute("id", "glyph");
|
||||
|
||||
@ -394,6 +612,10 @@ impl SVGRenderer {
|
||||
|
||||
/// Build the clip path definitions.
|
||||
fn write_clip_path_defs(&mut self) {
|
||||
if self.clip_paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.xml.start_element("defs");
|
||||
self.xml.write_attribute("id", "clip-path");
|
||||
|
||||
@ -408,6 +630,103 @@ impl SVGRenderer {
|
||||
|
||||
self.xml.end_element();
|
||||
}
|
||||
|
||||
/// Write the raw gradients (without transform) to the SVG file.
|
||||
fn write_gradients(&mut self) {
|
||||
if self.gradients.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.xml.start_element("defs");
|
||||
self.xml.write_attribute("id", "gradients");
|
||||
|
||||
for (id, (gradient, ratio)) in self.gradients.iter() {
|
||||
match &gradient {
|
||||
Gradient::Linear(linear) => {
|
||||
self.xml.start_element("linearGradient");
|
||||
self.xml.write_attribute("id", &id);
|
||||
self.xml.write_attribute("spreadMethod", "pad");
|
||||
self.xml.write_attribute("gradientUnits", "userSpaceOnUse");
|
||||
|
||||
let angle = Gradient::correct_aspect_ratio(linear.angle, *ratio);
|
||||
let (sin, cos) = (angle.sin(), angle.cos());
|
||||
let length = sin.abs() + cos.abs();
|
||||
let (x1, y1, x2, y2) = match angle.quadrant() {
|
||||
Quadrant::First => (0.0, 0.0, cos * length, sin * length),
|
||||
Quadrant::Second => (1.0, 0.0, cos * length + 1.0, sin * length),
|
||||
Quadrant::Third => {
|
||||
(1.0, 1.0, cos * length + 1.0, sin * length + 1.0)
|
||||
}
|
||||
Quadrant::Fourth => (0.0, 1.0, cos * length, sin * length + 1.0),
|
||||
};
|
||||
|
||||
self.xml.write_attribute("x1", &x1);
|
||||
self.xml.write_attribute("y1", &y1);
|
||||
self.xml.write_attribute("x2", &x2);
|
||||
self.xml.write_attribute("y2", &y2);
|
||||
|
||||
for window in linear.stops.windows(2) {
|
||||
let (_, start_t) = window[0];
|
||||
let (_, end_t) = window[1];
|
||||
|
||||
// Generate (256 / len) stops between the two stops.
|
||||
// This is a workaround for a bug in many readers:
|
||||
// They tend to just ignore the color space of the gradient.
|
||||
// The goal is to have smooth gradients but not to balloon the file size
|
||||
// too much if there are already a lot of stops as in most presets.
|
||||
let len = (256 / linear.stops.len() as u32).max(1);
|
||||
for i in 0..len {
|
||||
let t0 = i as f64 / (len - 1) as f64;
|
||||
let t = start_t + (end_t - start_t) * t0;
|
||||
let c = gradient.sample(RatioOrAngle::Ratio(t));
|
||||
|
||||
self.xml.start_element("stop");
|
||||
self.xml.write_attribute_fmt("offset", format_args!("{t:?}"));
|
||||
self.xml.write_attribute("stop-color", &c.to_hex());
|
||||
self.xml.end_element();
|
||||
}
|
||||
}
|
||||
|
||||
self.xml.end_element();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.xml.end_element()
|
||||
}
|
||||
|
||||
fn write_gradient_refs(&mut self) {
|
||||
if self.gradient_refs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.xml.start_element("defs");
|
||||
self.xml.write_attribute("id", "gradient-refs");
|
||||
for (id, gradient_ref) in self.gradient_refs.iter() {
|
||||
match gradient_ref.kind {
|
||||
GradientKind::Linear => {
|
||||
self.xml.start_element("linearGradient");
|
||||
self.xml.write_attribute(
|
||||
"gradientTransform",
|
||||
&SvgMatrix(gradient_ref.transform),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.xml.write_attribute("id", &id);
|
||||
|
||||
// Writing the href attribute to the "reference" gradient.
|
||||
self.xml
|
||||
.write_attribute_fmt("href", format_args!("#{}", gradient_ref.id));
|
||||
|
||||
// Also writing the xlink:href attribute for compatibility.
|
||||
self.xml
|
||||
.write_attribute_fmt("xlink:href", format_args!("#{}", gradient_ref.id));
|
||||
self.xml.end_element();
|
||||
}
|
||||
|
||||
self.xml.end_element();
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an outline glyph to an SVG path.
|
||||
@ -557,7 +876,7 @@ fn convert_image_to_base64_url(image: &Image) -> EcoString {
|
||||
#[derive(Debug, Clone)]
|
||||
struct Deduplicator<T> {
|
||||
kind: char,
|
||||
vec: Vec<T>,
|
||||
vec: Vec<(u128, T)>,
|
||||
present: HashMap<u128, Id>,
|
||||
}
|
||||
|
||||
@ -576,24 +895,32 @@ impl<T> Deduplicator<T> {
|
||||
{
|
||||
*self.present.entry(hash).or_insert_with(|| {
|
||||
let index = self.vec.len();
|
||||
self.vec.push(f());
|
||||
Id(self.kind, index)
|
||||
self.vec.push((hash, f()));
|
||||
Id(self.kind, hash, index)
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate over the the elements alongside their ids.
|
||||
fn iter(&self) -> impl Iterator<Item = (Id, &T)> {
|
||||
self.vec.iter().enumerate().map(|(i, v)| (Id(self.kind, i), v))
|
||||
self.vec
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (id, v))| (Id(self.kind, *id, i), v))
|
||||
}
|
||||
|
||||
/// Returns true if the deduplicator is empty.
|
||||
fn is_empty(&self) -> bool {
|
||||
self.vec.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifies a `<def>`.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
struct Id(char, usize);
|
||||
struct Id(char, u128, usize);
|
||||
|
||||
impl Display for Id {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}{}", self.0, self.1)
|
||||
write!(f, "{}{:0X}", self.0, self.1)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,6 +70,28 @@ impl Angle {
|
||||
pub fn tan(self) -> f64 {
|
||||
self.to_rad().tan()
|
||||
}
|
||||
|
||||
/// Get the quadrant of the Cartesian plane that this angle lies in.
|
||||
///
|
||||
/// The angle is automatically normalized to the range `0deg..=360deg`.
|
||||
///
|
||||
/// The quadrants are defined as follows:
|
||||
/// - First: `0deg..=90deg` (top-right)
|
||||
/// - Second: `90deg..=180deg` (top-left)
|
||||
/// - Third: `180deg..=270deg` (bottom-left)
|
||||
/// - Fourth: `270deg..=360deg` (bottom-right)
|
||||
pub fn quadrant(self) -> Quadrant {
|
||||
let angle = self.to_deg().rem_euclid(360.0);
|
||||
if angle <= 90.0 {
|
||||
Quadrant::First
|
||||
} else if angle <= 180.0 {
|
||||
Quadrant::Second
|
||||
} else if angle <= 270.0 {
|
||||
Quadrant::Third
|
||||
} else {
|
||||
Quadrant::Fourth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
@ -192,6 +214,19 @@ impl Debug for AngleUnit {
|
||||
}
|
||||
}
|
||||
|
||||
/// A quadrant of the Cartesian plane.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub enum Quadrant {
|
||||
/// The first quadrant, containing positive x and y values.
|
||||
First,
|
||||
/// The second quadrant, containing negative x and positive y values.
|
||||
Second,
|
||||
/// The third quadrant, containing negative x and y values.
|
||||
Third,
|
||||
/// The fourth quadrant, containing positive x and negative y values.
|
||||
Fourth,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -1,21 +1,25 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use ecow::{eco_format, EcoString, EcoVec};
|
||||
use once_cell::sync::Lazy;
|
||||
use palette::encoding::{self, Linear};
|
||||
use palette::{Darken, Desaturate, FromColor, Lighten, RgbHue, Saturate, ShiftHue};
|
||||
|
||||
use super::*;
|
||||
use crate::diag::{bail, error, At, SourceResult};
|
||||
use crate::eval::{cast, Args, Array, Str};
|
||||
use crate::eval::{cast, Args, Array, IntoValue, Module, Scope, Str};
|
||||
use crate::syntax::{Span, Spanned};
|
||||
|
||||
// Type aliases for `palette` internal types in f32.
|
||||
type Oklab = palette::oklab::Oklaba<f32>;
|
||||
type LinearRgba = palette::rgb::Rgba<Linear<encoding::Srgb>, f32>;
|
||||
type Rgba = palette::rgb::Rgba<encoding::Srgb, f32>;
|
||||
type Hsl = palette::hsl::Hsla<encoding::Srgb, f32>;
|
||||
type Hsv = palette::hsv::Hsva<encoding::Srgb, f32>;
|
||||
type Luma = palette::luma::Luma<encoding::Srgb, f32>;
|
||||
pub type Oklab = palette::oklab::Oklaba<f32>;
|
||||
pub type LinearRgba = palette::rgb::Rgba<Linear<encoding::Srgb>, f32>;
|
||||
pub type Rgba = palette::rgb::Rgba<encoding::Srgb, f32>;
|
||||
pub type Hsl = palette::hsl::Hsla<encoding::Srgb, f32>;
|
||||
pub type Hsv = palette::hsv::Hsva<encoding::Srgb, f32>;
|
||||
pub type Luma = palette::luma::Luma<encoding::Srgb, f32>;
|
||||
|
||||
/// Equivalent of [`std::f32::EPSILON`] but for hue angles.
|
||||
const ANGLE_EPSILON: f32 = 1e-5;
|
||||
|
||||
/// A color in a specific color space.
|
||||
///
|
||||
@ -42,6 +46,144 @@ type Luma = palette::luma::Luma<encoding::Srgb, f32>;
|
||||
/// #rect(fill: aqua)
|
||||
/// #rect(fill: color.aqua)
|
||||
/// ```
|
||||
///
|
||||
/// ## Color maps
|
||||
/// Typst also includes a number of preset color maps. In the following section,
|
||||
/// the list of available color maps is given, along with a sample of each gradient
|
||||
/// and relevant comments. Most of these color maps are chosen to be color blind
|
||||
/// friendly.
|
||||
///
|
||||
/// ### Turbo
|
||||
/// The [`turbo`]($color.map.turbo) gradient is a rainbow-like gradient that is
|
||||
/// perceptually uniform. You can learn more about the turbo color map on
|
||||
/// Google's [blog post](https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html).
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(width: 100%, height: 20pt, fill: gradient.linear(..color.map.turbo))
|
||||
/// ```
|
||||
///
|
||||
/// ### Cividis
|
||||
/// The [`cividis`]($color.map.cividis) gradient is a blue to gray to yellow
|
||||
/// gradient. You can learn more about the Cividis color map on the
|
||||
/// Berkley Institute for Data Science's [blog post](https://bids.github.io/colormap/).
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(width: 100%, height: 20pt, fill: gradient.linear(..color.map.cividis))
|
||||
/// ```
|
||||
///
|
||||
/// ### Rainbow
|
||||
/// The [`rainbow`]($color.map.rainbow) gradient cycles through the full color
|
||||
/// spectrum. This color map is best used by setting the interpolation color
|
||||
/// space to [HSL]($color.hsl).
|
||||
///
|
||||
/// **Attention:** The rainbow gradient is _not suitable_ for data visualization
|
||||
/// because it is not perceptually uniform, so the differences between values
|
||||
/// become unclear to your readers. It should only be used for decorative
|
||||
/// purposes.
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(
|
||||
/// width: 100%,
|
||||
/// height: 20pt,
|
||||
/// fill: gradient.linear(..color.map.rainbow, space: color.hsl)
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// ### Spectral
|
||||
/// The [`spectral`]($color.map.spectral) gradient is a red to yellow to blue
|
||||
/// gradient. Spectral does not take any parameters.
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(width: 100%, height: 20pt, fill: gradient.linear(..color.map.spectral))
|
||||
/// ```
|
||||
///
|
||||
/// ### Viridis
|
||||
/// The [`viridis`]($color.map.viridis) gradient is a purple to teal to yellow
|
||||
/// gradient. Viridis does not take any parameters.
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(width: 100%, height: 20pt, fill: gradient.linear(..color.map.viridis))
|
||||
/// ```
|
||||
///
|
||||
/// ### Inferno
|
||||
/// The [`inferno`]($color.map.inferno) gradient is a black to red to yellow
|
||||
/// gradient. Inferno does not take any parameters.
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(width: 100%, height: 20pt, fill: gradient.linear(..color.map.inferno))
|
||||
/// ```
|
||||
///
|
||||
/// ### Magma
|
||||
/// The [`magma`]($color.map.magma) gradient is a black to purple to yellow
|
||||
/// gradient. Magma does not take any parameters.
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(width: 100%, height: 20pt, fill: gradient.linear(..color.map.magma))
|
||||
/// ```
|
||||
///
|
||||
/// ### Plasma
|
||||
/// The [`plasma`]($color.map.plasma) gradient is a purple to pink to yellow
|
||||
/// gradient. Plasma does not take any parameters.
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(width: 100%, height: 20pt, fill: gradient.linear(..color.map.plasma))
|
||||
/// ```
|
||||
///
|
||||
/// ### Rocket
|
||||
/// The [`rocket`]($color.map.rocket) gradient is a black to red to white
|
||||
/// gradient. Rocket does not take any parameters.
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(width: 100%, height: 20pt, fill: gradient.linear(..color.map.rocket))
|
||||
/// ```
|
||||
///
|
||||
/// ### Mako
|
||||
/// The [`mako`]($color.map.mako) gradient is a black to teal to yellow gradient
|
||||
///. Mako does not take any parameters.
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(width: 100%, height: 20pt, fill: gradient.linear(..color.map.mako))
|
||||
/// ```
|
||||
///
|
||||
/// ### Vlag
|
||||
/// The [`vlag`]($color.map.vlag) gradient is a light blue to white to red
|
||||
/// gradient. Vlag does not take any parameters.
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(width: 100%, height: 20pt, fill: gradient.linear(..color.map.vlag))
|
||||
/// ```
|
||||
///
|
||||
/// ### Icefire
|
||||
/// The [`icefire`]($color.map.icefire) gradient is a light teal to black to
|
||||
/// yellow gradient. Icefire does not take any parameters.
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(width: 100%, height: 20pt, fill: gradient.linear(..color.map.icefire))
|
||||
/// ```
|
||||
///
|
||||
/// ### Flare
|
||||
/// The [`flare`]($color.map.flare) gradient is an orange to purple gradient that
|
||||
/// is perceptually uniform. Flare does not take any parameters.
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(width: 100%, height: 20pt, fill: gradient.linear(..color.map.flare))
|
||||
/// ```
|
||||
///
|
||||
/// ### Crest
|
||||
/// The [`crest`]($color.map.crest) gradient is a blue to white to red gradient .
|
||||
///Crest does not take any parameters.
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(width: 100%, height: 20pt, fill: gradient.linear(..color.map.crest))
|
||||
/// ```
|
||||
///
|
||||
/// ### On other presets
|
||||
/// [Jet](https://jakevdp.github.io/blog/2014/10/16/how-bad-is-your-colormap/)
|
||||
/// is not color blind friendly and should not be used for data visualization,
|
||||
/// which is why it is not included in Typst. Other popular presets are not
|
||||
/// neccesarily under a free licence, which is why we could not include them.
|
||||
///
|
||||
/// Feel free to use or create a package with other presets useful to you!
|
||||
#[ty(scope)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Color {
|
||||
@ -61,8 +203,58 @@ pub enum Color {
|
||||
Hsv(Hsv),
|
||||
}
|
||||
|
||||
impl From<Luma> for Color {
|
||||
fn from(c: Luma) -> Self {
|
||||
Self::Luma(c)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Oklab> for Color {
|
||||
fn from(c: Oklab) -> Self {
|
||||
Self::Oklab(c)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> for Color {
|
||||
fn from(c: Rgba) -> Self {
|
||||
Self::Rgba(c)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LinearRgba> for Color {
|
||||
fn from(c: LinearRgba) -> Self {
|
||||
Self::LinearRgb(c)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cmyk> for Color {
|
||||
fn from(c: Cmyk) -> Self {
|
||||
Self::Cmyk(c)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsl> for Color {
|
||||
fn from(c: Hsl) -> Self {
|
||||
Self::Hsl(c)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsv> for Color {
|
||||
fn from(c: Hsv) -> Self {
|
||||
Self::Hsv(c)
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl Color {
|
||||
/// The module of preset color maps.
|
||||
pub const MAP: fn() -> Module = || {
|
||||
// Lazy to avoid re-allocating.
|
||||
static MODULE: Lazy<Module> = Lazy::new(map);
|
||||
|
||||
MODULE.clone()
|
||||
};
|
||||
|
||||
pub const BLACK: Self = Self::Luma(Luma::new(0.0));
|
||||
pub const GRAY: Self = Self::Luma(Luma::new(0.6666666));
|
||||
pub const WHITE: Self = Self::Luma(Luma::new(1.0));
|
||||
@ -573,14 +765,18 @@ impl Color {
|
||||
Self::Hsl(c) => {
|
||||
if alpha {
|
||||
array![
|
||||
Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
|
||||
Angle::deg(
|
||||
c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON) as _
|
||||
),
|
||||
Ratio::new(c.saturation as _),
|
||||
Ratio::new(c.lightness as _),
|
||||
Ratio::new(c.alpha as _),
|
||||
]
|
||||
} else {
|
||||
array![
|
||||
Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
|
||||
Angle::deg(
|
||||
c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON) as _
|
||||
),
|
||||
Ratio::new(c.saturation as _),
|
||||
Ratio::new(c.lightness as _),
|
||||
]
|
||||
@ -589,14 +785,18 @@ impl Color {
|
||||
Self::Hsv(c) => {
|
||||
if alpha {
|
||||
array![
|
||||
Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
|
||||
Angle::deg(
|
||||
c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON) as _
|
||||
),
|
||||
Ratio::new(c.saturation as _),
|
||||
Ratio::new(c.value as _),
|
||||
Ratio::new(c.alpha as _),
|
||||
]
|
||||
} else {
|
||||
array![
|
||||
Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
|
||||
Angle::deg(
|
||||
c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON) as _
|
||||
),
|
||||
Ratio::new(c.saturation as _),
|
||||
Ratio::new(c.value as _),
|
||||
]
|
||||
@ -805,11 +1005,21 @@ impl Color {
|
||||
#[named]
|
||||
#[default(ColorSpace::Oklab)]
|
||||
space: ColorSpace,
|
||||
) -> StrResult<Color> {
|
||||
Self::mix_iter(colors, space)
|
||||
}
|
||||
}
|
||||
|
||||
impl Color {
|
||||
/// Same as [`Color::mix`], but takes an iterator instead of a vector.
|
||||
pub fn mix_iter(
|
||||
colors: impl IntoIterator<Item = WeightedColor>,
|
||||
space: ColorSpace,
|
||||
) -> StrResult<Color> {
|
||||
let mut total = 0.0;
|
||||
let mut acc = [0.0; 4];
|
||||
|
||||
for WeightedColor { color, weight } in colors.into_iter() {
|
||||
for WeightedColor { color, weight } in colors {
|
||||
let weight = weight as f32;
|
||||
let v = color.to_space(space).to_vec4();
|
||||
acc[0] += weight * v[0];
|
||||
@ -840,9 +1050,7 @@ impl Color {
|
||||
ColorSpace::D65Gray => Color::Luma(Luma::new(m[0])),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Color {
|
||||
/// Construct a new RGBA color from 8-bit values.
|
||||
pub fn from_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self::Rgba(Rgba::new(
|
||||
@ -864,6 +1072,7 @@ impl Color {
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the alpha channel of the color, if it has one.
|
||||
pub fn alpha(&self) -> Option<f32> {
|
||||
match self {
|
||||
Color::Luma(_) | Color::Cmyk(_) => None,
|
||||
@ -875,6 +1084,7 @@ impl Color {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the alpha channel of the color, if it has one.
|
||||
pub fn with_alpha(mut self, alpha: f32) -> Self {
|
||||
match &mut self {
|
||||
Color::Luma(_) | Color::Cmyk(_) => {}
|
||||
@ -888,6 +1098,7 @@ impl Color {
|
||||
self
|
||||
}
|
||||
|
||||
/// Converts the color to a vec of four floats.
|
||||
pub fn to_vec4(&self) -> [f32; 4] {
|
||||
match self {
|
||||
Color::Luma(c) => [c.luma; 4],
|
||||
@ -896,14 +1107,17 @@ impl Color {
|
||||
Color::LinearRgb(c) => [c.red, c.green, c.blue, c.alpha],
|
||||
Color::Cmyk(c) => [c.c, c.m, c.y, c.k],
|
||||
Color::Hsl(c) => [
|
||||
c.hue.into_degrees().rem_euclid(360.0),
|
||||
c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON),
|
||||
c.saturation,
|
||||
c.lightness,
|
||||
c.alpha,
|
||||
],
|
||||
Color::Hsv(c) => {
|
||||
[c.hue.into_degrees().rem_euclid(360.0), c.saturation, c.value, c.alpha]
|
||||
}
|
||||
Color::Hsv(c) => [
|
||||
c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON),
|
||||
c.saturation,
|
||||
c.value,
|
||||
c.alpha,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@ -1069,7 +1283,9 @@ impl Debug for Color {
|
||||
write!(
|
||||
f,
|
||||
"color.hsl({:?}, {:?}, {:?})",
|
||||
Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
|
||||
Angle::deg(
|
||||
c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON) as _
|
||||
),
|
||||
Ratio::new(c.saturation as _),
|
||||
Ratio::new(c.lightness as _),
|
||||
)
|
||||
@ -1077,7 +1293,9 @@ impl Debug for Color {
|
||||
write!(
|
||||
f,
|
||||
"color.hsl({:?}, {:?}, {:?}, {:?})",
|
||||
Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
|
||||
Angle::deg(
|
||||
c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON) as _
|
||||
),
|
||||
Ratio::new(c.saturation as _),
|
||||
Ratio::new(c.lightness as _),
|
||||
Ratio::new(c.alpha as _),
|
||||
@ -1089,7 +1307,9 @@ impl Debug for Color {
|
||||
write!(
|
||||
f,
|
||||
"color.hsv({:?}, {:?}, {:?})",
|
||||
Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
|
||||
Angle::deg(
|
||||
c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON) as _
|
||||
),
|
||||
Ratio::new(c.saturation as _),
|
||||
Ratio::new(c.value as _),
|
||||
)
|
||||
@ -1097,7 +1317,9 @@ impl Debug for Color {
|
||||
write!(
|
||||
f,
|
||||
"color.hsv({:?}, {:?}, {:?}, {:?})",
|
||||
Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
|
||||
Angle::deg(
|
||||
c.hue.into_degrees().rem_euclid(360.0 + ANGLE_EPSILON) as _
|
||||
),
|
||||
Ratio::new(c.saturation as _),
|
||||
Ratio::new(c.value as _),
|
||||
Ratio::new(c.alpha as _),
|
||||
@ -1389,6 +1611,55 @@ cast! {
|
||||
},
|
||||
}
|
||||
|
||||
/// A module with all preset color maps.
|
||||
fn map() -> Module {
|
||||
let mut scope = Scope::new();
|
||||
scope.define("turbo", turbo());
|
||||
scope.define("cividis", cividis());
|
||||
scope.define("rainbow", rainbow());
|
||||
scope.define("spectral", spectral());
|
||||
scope.define("viridis", viridis());
|
||||
scope.define("inferno", inferno());
|
||||
scope.define("magma", magma());
|
||||
scope.define("plasma", plasma());
|
||||
scope.define("rocket", rocket());
|
||||
scope.define("mako", mako());
|
||||
scope.define("vlag", vlag());
|
||||
scope.define("icefire", icefire());
|
||||
scope.define("flare", flare());
|
||||
scope.define("crest", crest());
|
||||
Module::new("map", scope)
|
||||
}
|
||||
|
||||
/// Defines a tradient preset as a series of colors expressed as u32s.
|
||||
macro_rules! preset {
|
||||
($name:ident; $($colors:literal),* $(,)*) => {
|
||||
fn $name() -> Array {
|
||||
Array::from(
|
||||
[$(Color::from_u32($colors)),*]
|
||||
.iter()
|
||||
.map(|c| c.into_value())
|
||||
.collect::<EcoVec<_>>()
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
preset!(turbo; 0x23171bff, 0x271a28ff, 0x2b1c33ff, 0x2f1e3fff, 0x32204aff, 0x362354ff, 0x39255fff, 0x3b2768ff, 0x3e2a72ff, 0x402c7bff, 0x422f83ff, 0x44318bff, 0x453493ff, 0x46369bff, 0x4839a2ff, 0x493ca8ff, 0x493eafff, 0x4a41b5ff, 0x4a44bbff, 0x4b46c0ff, 0x4b49c5ff, 0x4b4ccaff, 0x4b4ecfff, 0x4b51d3ff, 0x4a54d7ff, 0x4a56dbff, 0x4959deff, 0x495ce2ff, 0x485fe5ff, 0x4761e7ff, 0x4664eaff, 0x4567ecff, 0x446aeeff, 0x446df0ff, 0x426ff2ff, 0x4172f3ff, 0x4075f5ff, 0x3f78f6ff, 0x3e7af7ff, 0x3d7df7ff, 0x3c80f8ff, 0x3a83f9ff, 0x3985f9ff, 0x3888f9ff, 0x378bf9ff, 0x368df9ff, 0x3590f8ff, 0x3393f8ff, 0x3295f7ff, 0x3198f7ff, 0x309bf6ff, 0x2f9df5ff, 0x2ea0f4ff, 0x2da2f3ff, 0x2ca5f1ff, 0x2ba7f0ff, 0x2aaaefff, 0x2aacedff, 0x29afecff, 0x28b1eaff, 0x28b4e8ff, 0x27b6e6ff, 0x27b8e5ff, 0x26bbe3ff, 0x26bde1ff, 0x26bfdfff, 0x25c1dcff, 0x25c3daff, 0x25c6d8ff, 0x25c8d6ff, 0x25cad3ff, 0x25ccd1ff, 0x25cecfff, 0x26d0ccff, 0x26d2caff, 0x26d4c8ff, 0x27d6c5ff, 0x27d8c3ff, 0x28d9c0ff, 0x29dbbeff, 0x29ddbbff, 0x2adfb8ff, 0x2be0b6ff, 0x2ce2b3ff, 0x2de3b1ff, 0x2ee5aeff, 0x30e6acff, 0x31e8a9ff, 0x32e9a6ff, 0x34eba4ff, 0x35eca1ff, 0x37ed9fff, 0x39ef9cff, 0x3af09aff, 0x3cf197ff, 0x3ef295ff, 0x40f392ff, 0x42f490ff, 0x44f58dff, 0x46f68bff, 0x48f788ff, 0x4af786ff, 0x4df884ff, 0x4ff981ff, 0x51fa7fff, 0x54fa7dff, 0x56fb7aff, 0x59fb78ff, 0x5cfc76ff, 0x5efc74ff, 0x61fd71ff, 0x64fd6fff, 0x66fd6dff, 0x69fd6bff, 0x6cfd69ff, 0x6ffe67ff, 0x72fe65ff, 0x75fe63ff, 0x78fe61ff, 0x7bfe5fff, 0x7efd5dff, 0x81fd5cff, 0x84fd5aff, 0x87fd58ff, 0x8afc56ff, 0x8dfc55ff, 0x90fb53ff, 0x93fb51ff, 0x96fa50ff, 0x99fa4eff, 0x9cf94dff, 0x9ff84bff, 0xa2f84aff, 0xa6f748ff, 0xa9f647ff, 0xacf546ff, 0xaff444ff, 0xb2f343ff, 0xb5f242ff, 0xb8f141ff, 0xbbf03fff, 0xbeef3eff, 0xc1ed3dff, 0xc3ec3cff, 0xc6eb3bff, 0xc9e93aff, 0xcce839ff, 0xcfe738ff, 0xd1e537ff, 0xd4e336ff, 0xd7e235ff, 0xd9e034ff, 0xdcdf33ff, 0xdedd32ff, 0xe0db32ff, 0xe3d931ff, 0xe5d730ff, 0xe7d52fff, 0xe9d42fff, 0xecd22eff, 0xeed02dff, 0xf0ce2cff, 0xf1cb2cff, 0xf3c92bff, 0xf5c72bff, 0xf7c52aff, 0xf8c329ff, 0xfac029ff, 0xfbbe28ff, 0xfdbc28ff, 0xfeb927ff, 0xffb727ff, 0xffb526ff, 0xffb226ff, 0xffb025ff, 0xffad25ff, 0xffab24ff, 0xffa824ff, 0xffa623ff, 0xffa323ff, 0xffa022ff, 0xff9e22ff, 0xff9b21ff, 0xff9921ff, 0xff9621ff, 0xff9320ff, 0xff9020ff, 0xff8e1fff, 0xff8b1fff, 0xff881eff, 0xff851eff, 0xff831dff, 0xff801dff, 0xff7d1dff, 0xff7a1cff, 0xff781cff, 0xff751bff, 0xff721bff, 0xff6f1aff, 0xfd6c1aff, 0xfc6a19ff, 0xfa6719ff, 0xf96418ff, 0xf76118ff, 0xf65f18ff, 0xf45c17ff, 0xf25916ff, 0xf05716ff, 0xee5415ff, 0xec5115ff, 0xea4f14ff, 0xe84c14ff, 0xe64913ff, 0xe44713ff, 0xe24412ff, 0xdf4212ff, 0xdd3f11ff, 0xda3d10ff, 0xd83a10ff, 0xd5380fff, 0xd3360fff, 0xd0330eff, 0xce310dff, 0xcb2f0dff, 0xc92d0cff, 0xc62a0bff, 0xc3280bff, 0xc1260aff, 0xbe2409ff, 0xbb2309ff, 0xb92108ff, 0xb61f07ff, 0xb41d07ff, 0xb11b06ff, 0xaf1a05ff, 0xac1805ff, 0xaa1704ff, 0xa81604ff, 0xa51403ff, 0xa31302ff, 0xa11202ff, 0x9f1101ff, 0x9d1000ff, 0x9b0f00ff, 0x9a0e00ff, 0x980e00ff, 0x960d00ff, 0x950c00ff, 0x940c00ff, 0x930c00ff, 0x920c00ff, 0x910b00ff, 0x910c00ff, 0x900c00ff, 0x900c00ff, 0x900c00ff);
|
||||
preset!(cividis; 0x002051ff, 0x002153ff, 0x002255ff, 0x002356ff, 0x002358ff, 0x002459ff, 0x00255aff, 0x00255cff, 0x00265dff, 0x00275eff, 0x00275fff, 0x002860ff, 0x002961ff, 0x002962ff, 0x002a63ff, 0x002b64ff, 0x012b65ff, 0x022c65ff, 0x032d66ff, 0x042d67ff, 0x052e67ff, 0x052f68ff, 0x063069ff, 0x073069ff, 0x08316aff, 0x09326aff, 0x0b326aff, 0x0c336bff, 0x0d346bff, 0x0e346bff, 0x0f356cff, 0x10366cff, 0x12376cff, 0x13376dff, 0x14386dff, 0x15396dff, 0x17396dff, 0x183a6dff, 0x193b6dff, 0x1a3b6dff, 0x1c3c6eff, 0x1d3d6eff, 0x1e3e6eff, 0x203e6eff, 0x213f6eff, 0x23406eff, 0x24406eff, 0x25416eff, 0x27426eff, 0x28436eff, 0x29436eff, 0x2b446eff, 0x2c456eff, 0x2e456eff, 0x2f466eff, 0x30476eff, 0x32486eff, 0x33486eff, 0x34496eff, 0x364a6eff, 0x374a6eff, 0x394b6eff, 0x3a4c6eff, 0x3b4d6eff, 0x3d4d6eff, 0x3e4e6eff, 0x3f4f6eff, 0x414f6eff, 0x42506eff, 0x43516dff, 0x44526dff, 0x46526dff, 0x47536dff, 0x48546dff, 0x4a546dff, 0x4b556dff, 0x4c566dff, 0x4d576dff, 0x4e576eff, 0x50586eff, 0x51596eff, 0x52596eff, 0x535a6eff, 0x545b6eff, 0x565c6eff, 0x575c6eff, 0x585d6eff, 0x595e6eff, 0x5a5e6eff, 0x5b5f6eff, 0x5c606eff, 0x5d616eff, 0x5e616eff, 0x60626eff, 0x61636fff, 0x62646fff, 0x63646fff, 0x64656fff, 0x65666fff, 0x66666fff, 0x67676fff, 0x686870ff, 0x696970ff, 0x6a6970ff, 0x6b6a70ff, 0x6c6b70ff, 0x6d6c70ff, 0x6d6c71ff, 0x6e6d71ff, 0x6f6e71ff, 0x706f71ff, 0x716f71ff, 0x727071ff, 0x737172ff, 0x747172ff, 0x757272ff, 0x767372ff, 0x767472ff, 0x777473ff, 0x787573ff, 0x797673ff, 0x7a7773ff, 0x7b7774ff, 0x7b7874ff, 0x7c7974ff, 0x7d7a74ff, 0x7e7a74ff, 0x7f7b75ff, 0x807c75ff, 0x807d75ff, 0x817d75ff, 0x827e75ff, 0x837f76ff, 0x848076ff, 0x858076ff, 0x858176ff, 0x868276ff, 0x878376ff, 0x888477ff, 0x898477ff, 0x898577ff, 0x8a8677ff, 0x8b8777ff, 0x8c8777ff, 0x8d8877ff, 0x8e8978ff, 0x8e8a78ff, 0x8f8a78ff, 0x908b78ff, 0x918c78ff, 0x928d78ff, 0x938e78ff, 0x938e78ff, 0x948f78ff, 0x959078ff, 0x969178ff, 0x979278ff, 0x989278ff, 0x999378ff, 0x9a9478ff, 0x9b9578ff, 0x9b9678ff, 0x9c9678ff, 0x9d9778ff, 0x9e9878ff, 0x9f9978ff, 0xa09a78ff, 0xa19a78ff, 0xa29b78ff, 0xa39c78ff, 0xa49d78ff, 0xa59e77ff, 0xa69e77ff, 0xa79f77ff, 0xa8a077ff, 0xa9a177ff, 0xaaa276ff, 0xaba376ff, 0xaca376ff, 0xada476ff, 0xaea575ff, 0xafa675ff, 0xb0a775ff, 0xb2a874ff, 0xb3a874ff, 0xb4a974ff, 0xb5aa73ff, 0xb6ab73ff, 0xb7ac72ff, 0xb8ad72ff, 0xbaae72ff, 0xbbae71ff, 0xbcaf71ff, 0xbdb070ff, 0xbeb170ff, 0xbfb26fff, 0xc1b36fff, 0xc2b46eff, 0xc3b56dff, 0xc4b56dff, 0xc5b66cff, 0xc7b76cff, 0xc8b86bff, 0xc9b96aff, 0xcaba6aff, 0xccbb69ff, 0xcdbc68ff, 0xcebc68ff, 0xcfbd67ff, 0xd1be66ff, 0xd2bf66ff, 0xd3c065ff, 0xd4c164ff, 0xd6c263ff, 0xd7c363ff, 0xd8c462ff, 0xd9c561ff, 0xdbc660ff, 0xdcc660ff, 0xddc75fff, 0xdec85eff, 0xe0c95dff, 0xe1ca5cff, 0xe2cb5cff, 0xe3cc5bff, 0xe4cd5aff, 0xe6ce59ff, 0xe7cf58ff, 0xe8d058ff, 0xe9d157ff, 0xead256ff, 0xebd355ff, 0xecd454ff, 0xedd453ff, 0xeed553ff, 0xf0d652ff, 0xf1d751ff, 0xf1d850ff, 0xf2d950ff, 0xf3da4fff, 0xf4db4eff, 0xf5dc4dff, 0xf6dd4dff, 0xf7de4cff, 0xf8df4bff, 0xf8e04bff, 0xf9e14aff, 0xfae249ff, 0xfae349ff, 0xfbe448ff, 0xfbe548ff, 0xfce647ff, 0xfce746ff, 0xfde846ff, 0xfde946ff, 0xfdea45ff);
|
||||
preset!(rainbow; 0x7c4bbbff, 0x7f4bbcff, 0x824bbdff, 0x854abeff, 0x884abeff, 0x8b4abfff, 0x8e49bfff, 0x9149c0ff, 0x9449c0ff, 0x9748c0ff, 0x9a48c1ff, 0x9e48c1ff, 0xa148c1ff, 0xa447c1ff, 0xa747c1ff, 0xaa47c0ff, 0xad47c0ff, 0xb046c0ff, 0xb446bfff, 0xb746bfff, 0xba46beff, 0xbd46beff, 0xc046bdff, 0xc346bcff, 0xc646bbff, 0xc946baff, 0xcc46b9ff, 0xcf46b8ff, 0xd246b7ff, 0xd446b5ff, 0xd747b4ff, 0xda47b3ff, 0xdd47b1ff, 0xdf47b0ff, 0xe248aeff, 0xe448acff, 0xe748abff, 0xe949a9ff, 0xec49a7ff, 0xee4aa5ff, 0xf04ba3ff, 0xf34ba1ff, 0xf54c9fff, 0xf74c9dff, 0xf94d9bff, 0xfb4e98ff, 0xfd4f96ff, 0xfe5094ff, 0xff5191ff, 0xff528fff, 0xff538dff, 0xff548aff, 0xff5588ff, 0xff5685ff, 0xff5783ff, 0xff5880ff, 0xff5a7eff, 0xff5b7bff, 0xff5c79ff, 0xff5e76ff, 0xff5f74ff, 0xff6171ff, 0xff626fff, 0xff646cff, 0xff666aff, 0xff6767ff, 0xff6965ff, 0xff6b63ff, 0xff6d60ff, 0xff6e5eff, 0xff705cff, 0xff7259ff, 0xff7457ff, 0xff7655ff, 0xff7853ff, 0xff7a51ff, 0xff7c4fff, 0xff7f4dff, 0xff814bff, 0xff8349ff, 0xff8547ff, 0xff8745ff, 0xff8a44ff, 0xff8c42ff, 0xff8e40ff, 0xff913fff, 0xff933eff, 0xff953cff, 0xff983bff, 0xfd9a3aff, 0xfb9c39ff, 0xfa9f38ff, 0xf8a137ff, 0xf6a436ff, 0xf4a636ff, 0xf2a935ff, 0xf0ab35ff, 0xeeae34ff, 0xecb034ff, 0xeab234ff, 0xe8b534ff, 0xe6b734ff, 0xe4ba34ff, 0xe1bc34ff, 0xdfbf35ff, 0xddc135ff, 0xdbc336ff, 0xd9c636ff, 0xd6c837ff, 0xd4ca38ff, 0xd2cd39ff, 0xd0cf3aff, 0xcdd13bff, 0xcbd33dff, 0xc9d63eff, 0xc7d840ff, 0xc5da41ff, 0xc3dc43ff, 0xc1de45ff, 0xbfe047ff, 0xbde249ff, 0xbbe44bff, 0xb9e64dff, 0xb7e84fff, 0xb5ea52ff, 0xb3ec54ff, 0xb2ed57ff, 0xb0ef59ff, 0xadf05aff, 0xaaf15aff, 0xa6f159ff, 0xa2f259ff, 0x9ff259ff, 0x9bf358ff, 0x97f358ff, 0x94f459ff, 0x90f459ff, 0x8df559ff, 0x89f559ff, 0x85f65aff, 0x82f65bff, 0x7ff65bff, 0x7ef75cff, 0x7cf75dff, 0x7bf75eff, 0x7af75fff, 0x79f760ff, 0x78f762ff, 0x77f763ff, 0x76f764ff, 0x75f766ff, 0x74f768ff, 0x73f769ff, 0x72f76bff, 0x71f76dff, 0x70f76fff, 0x6ff671ff, 0x6ef673ff, 0x6df675ff, 0x6df577ff, 0x6cf579ff, 0x6bf47cff, 0x6af37eff, 0x69f380ff, 0x68f283ff, 0x67f185ff, 0x66f188ff, 0x66f08aff, 0x65ef8dff, 0x64ee8fff, 0x63ed92ff, 0x62ec94ff, 0x62eb97ff, 0x61ea9aff, 0x60e89cff, 0x5fe79fff, 0x5fe6a1ff, 0x5ee4a4ff, 0x5de3a7ff, 0x5ce2a9ff, 0x5ce0acff, 0x5bdfafff, 0x5addb1ff, 0x5adbb4ff, 0x59dab6ff, 0x58d8b9ff, 0x58d6bbff, 0x57d5beff, 0x56d3c0ff, 0x56d1c2ff, 0x55cfc5ff, 0x54cdc7ff, 0x54cbc9ff, 0x53c9cbff, 0x52c7cdff, 0x52c5cfff, 0x51c3d1ff, 0x51c1d3ff, 0x50bfd5ff, 0x50bdd7ff, 0x4fbbd9ff, 0x4eb9daff, 0x4eb6dcff, 0x4db4ddff, 0x4db2dfff, 0x4cb0e0ff, 0x4caee2ff, 0x4babe3ff, 0x4ba9e4ff, 0x4aa7e5ff, 0x4aa4e6ff, 0x49a2e7ff, 0x49a0e8ff, 0x489ee8ff, 0x489be9ff, 0x4799e9ff, 0x4797eaff, 0x4694eaff, 0x4692eaff, 0x4690ebff, 0x458eebff, 0x478bebff, 0x4889ebff, 0x4a87eaff, 0x4c85eaff, 0x4e82eaff, 0x5080e9ff, 0x527ee9ff, 0x537ce8ff, 0x557ae7ff, 0x5778e7ff, 0x5975e6ff, 0x5b73e5ff, 0x5c71e4ff, 0x5e6fe3ff, 0x606de1ff, 0x626be0ff, 0x6369dfff, 0x6567ddff, 0x6765dcff, 0x6864daff, 0x6a62d9ff, 0x6b60d7ff, 0x6d5ed5ff, 0x6e5cd3ff, 0x705bd1ff, 0x7159cfff, 0x7357cdff, 0x7456cbff, 0x7554c9ff, 0x7652c7ff, 0x7751c5ff, 0x794fc2ff, 0x7a4ec0ff, 0x7b4dbeff, 0x7c4bbbff);
|
||||
preset!(spectral; 0x9e0142ff, 0xd53e4fff, 0xf46d43ff, 0xfdae61ff, 0xfee08bff, 0xffffbfff, 0xe6f598ff, 0xabdda4ff, 0x66c2a5ff, 0x3288bdff, 0x5e4fa2ff);
|
||||
preset!(viridis; 0x440154ff, 0x482777ff, 0x3f4a8aff, 0x31678eff, 0x26838fff, 0x1f9d8aff, 0x6cce5aff, 0xb6de2bff, 0xfee825ff);
|
||||
preset!(inferno; 0x000004ff, 0x170b3aff, 0x420a68ff, 0x6b176eff, 0x932667ff, 0xbb3654ff, 0xdd513aff, 0xf3771aff, 0xfca50aff, 0xf6d644ff, 0xfcffa4ff);
|
||||
preset!(magma; 0x000004ff, 0x140e37ff, 0x3b0f70ff, 0x641a80ff, 0x8c2981ff, 0xb63679ff, 0xde4968ff, 0xf66f5cff, 0xfe9f6dff, 0xfece91ff, 0xfcfdbfff);
|
||||
preset!(plasma; 0x0d0887ff, 0x42039dff, 0x6a00a8ff, 0x900da3ff, 0xb12a90ff, 0xcb4678ff, 0xe16462ff, 0xf1834bff, 0xfca636ff, 0xfccd25ff, 0xf0f921ff);
|
||||
preset!(rocket; 0x3051aff, 0x4051aff, 0x5061bff, 0x6071cff, 0x7071dff, 0x8081eff, 0xa091fff, 0xb0920ff, 0xd0a21ff, 0xe0b22ff, 0x100b23ff, 0x110c24ff, 0x130d25ff, 0x140e26ff, 0x160e27ff, 0x170f28ff, 0x180f29ff, 0x1a102aff, 0x1b112bff, 0x1d112cff, 0x1e122dff, 0x20122eff, 0x211330ff, 0x221331ff, 0x241432ff, 0x251433ff, 0x271534ff, 0x281535ff, 0x2a1636ff, 0x2b1637ff, 0x2d1738ff, 0x2e1739ff, 0x30173aff, 0x31183bff, 0x33183cff, 0x34193dff, 0x35193eff, 0x37193fff, 0x381a40ff, 0x3a1a41ff, 0x3c1a42ff, 0x3d1a42ff, 0x3f1b43ff, 0x401b44ff, 0x421b45ff, 0x431c46ff, 0x451c47ff, 0x461c48ff, 0x481c48ff, 0x491d49ff, 0x4b1d4aff, 0x4c1d4bff, 0x4e1d4bff, 0x501d4cff, 0x511e4dff, 0x531e4dff, 0x541e4eff, 0x561e4fff, 0x581e4fff, 0x591e50ff, 0x5b1e51ff, 0x5c1e51ff, 0x5e1f52ff, 0x601f52ff, 0x611f53ff, 0x631f53ff, 0x641f54ff, 0x661f54ff, 0x681f55ff, 0x691f55ff, 0x6b1f56ff, 0x6d1f56ff, 0x6e1f57ff, 0x701f57ff, 0x711f57ff, 0x731f58ff, 0x751f58ff, 0x761f58ff, 0x781f59ff, 0x7a1f59ff, 0x7b1f59ff, 0x7d1f5aff, 0x7f1e5aff, 0x811e5aff, 0x821e5aff, 0x841e5aff, 0x861e5bff, 0x871e5bff, 0x891e5bff, 0x8b1d5bff, 0x8c1d5bff, 0x8e1d5bff, 0x901d5bff, 0x921c5bff, 0x931c5bff, 0x951c5bff, 0x971c5bff, 0x981b5bff, 0x9a1b5bff, 0x9c1b5bff, 0x9e1a5bff, 0x9f1a5bff, 0xa11a5bff, 0xa3195bff, 0xa4195bff, 0xa6195aff, 0xa8185aff, 0xaa185aff, 0xab185aff, 0xad1759ff, 0xaf1759ff, 0xb01759ff, 0xb21758ff, 0xb41658ff, 0xb51657ff, 0xb71657ff, 0xb91657ff, 0xba1656ff, 0xbc1656ff, 0xbd1655ff, 0xbf1654ff, 0xc11754ff, 0xc21753ff, 0xc41753ff, 0xc51852ff, 0xc71951ff, 0xc81951ff, 0xca1a50ff, 0xcb1b4fff, 0xcd1c4eff, 0xce1d4eff, 0xcf1e4dff, 0xd11f4cff, 0xd2204cff, 0xd3214bff, 0xd5224aff, 0xd62449ff, 0xd72549ff, 0xd82748ff, 0xd92847ff, 0xdb2946ff, 0xdc2b46ff, 0xdd2c45ff, 0xde2e44ff, 0xdf2f44ff, 0xe03143ff, 0xe13342ff, 0xe23442ff, 0xe33641ff, 0xe43841ff, 0xe53940ff, 0xe63b40ff, 0xe73d3fff, 0xe83f3fff, 0xe8403eff, 0xe9423eff, 0xea443eff, 0xeb463eff, 0xeb483eff, 0xec4a3eff, 0xec4c3eff, 0xed4e3eff, 0xed503eff, 0xee523fff, 0xee543fff, 0xef5640ff, 0xef5840ff, 0xef5a41ff, 0xf05c42ff, 0xf05e42ff, 0xf06043ff, 0xf16244ff, 0xf16445ff, 0xf16646ff, 0xf26747ff, 0xf26948ff, 0xf26b49ff, 0xf26d4bff, 0xf26f4cff, 0xf3714dff, 0xf3734eff, 0xf37450ff, 0xf37651ff, 0xf37852ff, 0xf47a54ff, 0xf47c55ff, 0xf47d57ff, 0xf47f58ff, 0xf4815aff, 0xf4835bff, 0xf4845dff, 0xf4865eff, 0xf58860ff, 0xf58a61ff, 0xf58b63ff, 0xf58d64ff, 0xf58f66ff, 0xf59067ff, 0xf59269ff, 0xf5946bff, 0xf5966cff, 0xf5976eff, 0xf59970ff, 0xf69b71ff, 0xf69c73ff, 0xf69e75ff, 0xf6a077ff, 0xf6a178ff, 0xf6a37aff, 0xf6a47cff, 0xf6a67eff, 0xf6a880ff, 0xf6a981ff, 0xf6ab83ff, 0xf6ad85ff, 0xf6ae87ff, 0xf6b089ff, 0xf6b18bff, 0xf6b38dff, 0xf6b48fff, 0xf6b691ff, 0xf6b893ff, 0xf6b995ff, 0xf6bb97ff, 0xf6bc99ff, 0xf6be9bff, 0xf6bf9dff, 0xf6c19fff, 0xf7c2a2ff, 0xf7c4a4ff, 0xf7c6a6ff, 0xf7c7a8ff, 0xf7c9aaff, 0xf7caacff, 0xf7ccafff, 0xf7cdb1ff, 0xf7cfb3ff, 0xf7d0b5ff, 0xf8d1b8ff, 0xf8d3baff, 0xf8d4bcff, 0xf8d6beff, 0xf8d7c0ff, 0xf8d9c3ff, 0xf8dac5ff, 0xf8dcc7ff, 0xf9ddc9ff, 0xf9dfcbff, 0xf9e0cdff, 0xf9e2d0ff, 0xf9e3d2ff, 0xf9e5d4ff, 0xfae6d6ff, 0xfae8d8ff, 0xfae9daff, 0xfaebddff);
|
||||
preset!(mako; 0xb0405ff, 0xd0406ff, 0xe0508ff, 0xf0609ff, 0x10060aff, 0x11070cff, 0x12080dff, 0x13090fff, 0x140910ff, 0x150a12ff, 0x160b13ff, 0x170c15ff, 0x180d16ff, 0x190e18ff, 0x1a0e19ff, 0x1b0f1aff, 0x1c101cff, 0x1d111dff, 0x1e111fff, 0x1f1220ff, 0x201322ff, 0x211423ff, 0x221425ff, 0x231526ff, 0x241628ff, 0x251729ff, 0x26172bff, 0x27182dff, 0x28192eff, 0x291930ff, 0x291a31ff, 0x2a1b33ff, 0x2b1c35ff, 0x2c1c36ff, 0x2d1d38ff, 0x2e1e39ff, 0x2e1e3bff, 0x2f1f3dff, 0x30203eff, 0x312140ff, 0x312142ff, 0x322243ff, 0x332345ff, 0x342447ff, 0x342548ff, 0x35254aff, 0x35264cff, 0x36274dff, 0x37284fff, 0x372851ff, 0x382953ff, 0x382a54ff, 0x392b56ff, 0x3a2c58ff, 0x3a2c59ff, 0x3b2d5bff, 0x3b2e5dff, 0x3b2f5fff, 0x3c3060ff, 0x3c3162ff, 0x3d3164ff, 0x3d3266ff, 0x3e3367ff, 0x3e3469ff, 0x3e356bff, 0x3f366dff, 0x3f366fff, 0x3f3770ff, 0x403872ff, 0x403974ff, 0x403a76ff, 0x403b78ff, 0x403c79ff, 0x413d7bff, 0x413e7dff, 0x413e7fff, 0x413f80ff, 0x414082ff, 0x414184ff, 0x414285ff, 0x414387ff, 0x414488ff, 0x40468aff, 0x40478bff, 0x40488dff, 0x40498eff, 0x3f4a8fff, 0x3f4b90ff, 0x3f4c92ff, 0x3e4d93ff, 0x3e4f94ff, 0x3e5095ff, 0x3d5195ff, 0x3d5296ff, 0x3c5397ff, 0x3c5598ff, 0x3b5698ff, 0x3b5799ff, 0x3b589aff, 0x3a599aff, 0x3a5b9bff, 0x3a5c9bff, 0x395d9cff, 0x395e9cff, 0x385f9cff, 0x38619dff, 0x38629dff, 0x38639dff, 0x37649eff, 0x37659eff, 0x37669eff, 0x37689fff, 0x36699fff, 0x366a9fff, 0x366b9fff, 0x366ca0ff, 0x366da0ff, 0x366fa0ff, 0x3670a0ff, 0x3671a0ff, 0x3572a1ff, 0x3573a1ff, 0x3574a1ff, 0x3575a1ff, 0x3576a2ff, 0x3578a2ff, 0x3579a2ff, 0x357aa2ff, 0x357ba3ff, 0x357ca3ff, 0x357da3ff, 0x357ea4ff, 0x347fa4ff, 0x3480a4ff, 0x3482a4ff, 0x3483a5ff, 0x3484a5ff, 0x3485a5ff, 0x3486a5ff, 0x3487a6ff, 0x3488a6ff, 0x3489a6ff, 0x348ba6ff, 0x348ca7ff, 0x348da7ff, 0x348ea7ff, 0x348fa7ff, 0x3490a8ff, 0x3491a8ff, 0x3492a8ff, 0x3493a8ff, 0x3495a9ff, 0x3496a9ff, 0x3497a9ff, 0x3498a9ff, 0x3499aaff, 0x349aaaff, 0x359baaff, 0x359caaff, 0x359eaaff, 0x359fabff, 0x35a0abff, 0x35a1abff, 0x36a2abff, 0x36a3abff, 0x36a4abff, 0x37a5acff, 0x37a6acff, 0x37a8acff, 0x38a9acff, 0x38aaacff, 0x39abacff, 0x39acacff, 0x3aadacff, 0x3aaeadff, 0x3bafadff, 0x3cb1adff, 0x3cb2adff, 0x3db3adff, 0x3eb4adff, 0x3fb5adff, 0x3fb6adff, 0x40b7adff, 0x41b8adff, 0x42b9adff, 0x43baadff, 0x44bcadff, 0x45bdadff, 0x46beadff, 0x47bfadff, 0x48c0adff, 0x49c1adff, 0x4bc2adff, 0x4cc3adff, 0x4dc4adff, 0x4fc5adff, 0x50c6adff, 0x52c7adff, 0x53c9adff, 0x55caadff, 0x57cbadff, 0x59ccadff, 0x5bcdadff, 0x5ecdadff, 0x60ceacff, 0x62cfacff, 0x65d0adff, 0x68d1adff, 0x6ad2adff, 0x6dd3adff, 0x70d4adff, 0x73d4adff, 0x76d5aeff, 0x79d6aeff, 0x7cd6afff, 0x7fd7afff, 0x82d8b0ff, 0x85d9b1ff, 0x88d9b1ff, 0x8bdab2ff, 0x8edbb3ff, 0x91dbb4ff, 0x94dcb5ff, 0x96ddb5ff, 0x99ddb6ff, 0x9cdeb7ff, 0x9edfb8ff, 0xa1dfb9ff, 0xa4e0bbff, 0xa6e1bcff, 0xa9e1bdff, 0xabe2beff, 0xaee3c0ff, 0xb0e4c1ff, 0xb2e4c2ff, 0xb5e5c4ff, 0xb7e6c5ff, 0xb9e6c7ff, 0xbbe7c8ff, 0xbee8caff, 0xc0e9ccff, 0xc2e9cdff, 0xc4eacfff, 0xc6ebd1ff, 0xc8ecd2ff, 0xcaedd4ff, 0xccedd6ff, 0xceeed7ff, 0xd0efd9ff, 0xd2f0dbff, 0xd4f1dcff, 0xd6f1deff, 0xd8f2e0ff, 0xdaf3e1ff, 0xdcf4e3ff, 0xdef5e5ff);
|
||||
preset!(vlag; 0x2369bdff, 0x266abdff, 0x296cbcff, 0x2c6dbcff, 0x2f6ebcff, 0x316fbcff, 0x3470bcff, 0x3671bcff, 0x3972bcff, 0x3b73bcff, 0x3d74bcff, 0x3f75bcff, 0x4276bcff, 0x4477bcff, 0x4678bcff, 0x4879bcff, 0x4a7bbcff, 0x4c7cbcff, 0x4e7dbcff, 0x507ebcff, 0x517fbcff, 0x5380bcff, 0x5581bcff, 0x5782bcff, 0x5983bdff, 0x5b84bdff, 0x5c85bdff, 0x5e86bdff, 0x6087bdff, 0x6288bdff, 0x6489beff, 0x658abeff, 0x678bbeff, 0x698cbeff, 0x6a8dbfff, 0x6c8ebfff, 0x6e90bfff, 0x6f91bfff, 0x7192c0ff, 0x7393c0ff, 0x7594c0ff, 0x7695c1ff, 0x7896c1ff, 0x7997c1ff, 0x7b98c2ff, 0x7d99c2ff, 0x7e9ac2ff, 0x809bc3ff, 0x829cc3ff, 0x839dc4ff, 0x859ec4ff, 0x87a0c4ff, 0x88a1c5ff, 0x8aa2c5ff, 0x8ba3c6ff, 0x8da4c6ff, 0x8fa5c7ff, 0x90a6c7ff, 0x92a7c8ff, 0x93a8c8ff, 0x95a9c8ff, 0x97abc9ff, 0x98acc9ff, 0x9aadcaff, 0x9baecbff, 0x9dafcbff, 0x9fb0ccff, 0xa0b1ccff, 0xa2b2cdff, 0xa3b4cdff, 0xa5b5ceff, 0xa7b6ceff, 0xa8b7cfff, 0xaab8d0ff, 0xabb9d0ff, 0xadbbd1ff, 0xafbcd1ff, 0xb0bdd2ff, 0xb2bed3ff, 0xb3bfd3ff, 0xb5c0d4ff, 0xb7c2d5ff, 0xb8c3d5ff, 0xbac4d6ff, 0xbbc5d7ff, 0xbdc6d7ff, 0xbfc8d8ff, 0xc0c9d9ff, 0xc2cadaff, 0xc3cbdaff, 0xc5cddbff, 0xc7cedcff, 0xc8cfddff, 0xcad0ddff, 0xcbd1deff, 0xcdd3dfff, 0xcfd4e0ff, 0xd0d5e0ff, 0xd2d7e1ff, 0xd4d8e2ff, 0xd5d9e3ff, 0xd7dae4ff, 0xd9dce5ff, 0xdadde5ff, 0xdcdee6ff, 0xdde0e7ff, 0xdfe1e8ff, 0xe1e2e9ff, 0xe2e3eaff, 0xe4e5ebff, 0xe6e6ecff, 0xe7e7ecff, 0xe9e9edff, 0xebeaeeff, 0xecebefff, 0xeeedf0ff, 0xefeef1ff, 0xf1eff2ff, 0xf2f0f2ff, 0xf3f1f3ff, 0xf5f2f4ff, 0xf6f3f4ff, 0xf7f4f4ff, 0xf8f4f5ff, 0xf9f5f5ff, 0xf9f5f5ff, 0xfaf5f5ff, 0xfaf5f5ff, 0xfaf5f4ff, 0xfaf5f4ff, 0xfaf4f3ff, 0xfaf3f3ff, 0xfaf3f2ff, 0xfaf2f1ff, 0xfaf0efff, 0xf9efeeff, 0xf9eeedff, 0xf8edebff, 0xf7ebeaff, 0xf7eae8ff, 0xf6e8e7ff, 0xf5e7e5ff, 0xf5e5e4ff, 0xf4e3e2ff, 0xf3e2e0ff, 0xf2e0dfff, 0xf2dfddff, 0xf1dddbff, 0xf0dbdaff, 0xefdad8ff, 0xefd8d6ff, 0xeed7d5ff, 0xedd5d3ff, 0xecd3d2ff, 0xecd2d0ff, 0xebd0ceff, 0xeacfcdff, 0xeacdcbff, 0xe9cbc9ff, 0xe8cac8ff, 0xe7c8c6ff, 0xe7c7c5ff, 0xe6c5c3ff, 0xe5c3c1ff, 0xe5c2c0ff, 0xe4c0beff, 0xe3bfbdff, 0xe3bdbbff, 0xe2bcb9ff, 0xe1bab8ff, 0xe1b9b6ff, 0xe0b7b5ff, 0xdfb5b3ff, 0xdfb4b2ff, 0xdeb2b0ff, 0xdeb1aeff, 0xddafadff, 0xdcaeabff, 0xdcacaaff, 0xdbaba8ff, 0xdaa9a7ff, 0xdaa8a5ff, 0xd9a6a4ff, 0xd9a5a2ff, 0xd8a3a0ff, 0xd7a29fff, 0xd7a09dff, 0xd69f9cff, 0xd59d9aff, 0xd59c99ff, 0xd49a97ff, 0xd49896ff, 0xd39794ff, 0xd29593ff, 0xd29491ff, 0xd19290ff, 0xd1918eff, 0xd08f8dff, 0xcf8e8bff, 0xcf8c8aff, 0xce8b88ff, 0xcd8987ff, 0xcd8885ff, 0xcc8784ff, 0xcc8582ff, 0xcb8481ff, 0xca827fff, 0xca817eff, 0xc97f7dff, 0xc87e7bff, 0xc87c7aff, 0xc77b78ff, 0xc77977ff, 0xc67875ff, 0xc57674ff, 0xc57572ff, 0xc47371ff, 0xc3726fff, 0xc3706eff, 0xc26f6dff, 0xc16d6bff, 0xc16c6aff, 0xc06a68ff, 0xc06967ff, 0xbf6765ff, 0xbe6664ff, 0xbe6463ff, 0xbd6361ff, 0xbc6160ff, 0xbc605eff, 0xbb5e5dff, 0xba5d5cff, 0xb95b5aff, 0xb95a59ff, 0xb85857ff, 0xb75756ff, 0xb75555ff, 0xb65453ff, 0xb55252ff, 0xb55151ff, 0xb44f4fff, 0xb34d4eff, 0xb24c4cff, 0xb24a4bff, 0xb1494aff, 0xb04748ff, 0xaf4647ff, 0xaf4446ff, 0xae4244ff, 0xad4143ff, 0xac3f42ff, 0xac3e40ff, 0xab3c3fff, 0xaa3a3eff, 0xa9393cff, 0xa9373bff);
|
||||
preset!(icefire; 0xbde7dbff, 0xbae5daff, 0xb7e3d9ff, 0xb4e1d9ff, 0xb2dfd8ff, 0xafddd7ff, 0xacdbd7ff, 0xa9d9d6ff, 0xa7d7d5ff, 0xa4d5d5ff, 0xa1d3d4ff, 0x9ed1d3ff, 0x9bcfd3ff, 0x98cdd2ff, 0x95cbd2ff, 0x93cad1ff, 0x90c8d1ff, 0x8dc6d0ff, 0x8ac4d0ff, 0x87c2cfff, 0x84c1cfff, 0x81bfcfff, 0x7ebdceff, 0x7bbbceff, 0x78b9ceff, 0x75b8ceff, 0x72b6ceff, 0x6eb4cdff, 0x6bb2cdff, 0x68b0cdff, 0x65afcdff, 0x63adcdff, 0x60abcdff, 0x5da9cdff, 0x5aa7cdff, 0x58a5cdff, 0x55a3cdff, 0x53a2cdff, 0x50a0cdff, 0x4e9ecdff, 0x4c9ccdff, 0x499aceff, 0x4798ceff, 0x4596ceff, 0x4394ceff, 0x4192ceff, 0x3f90ceff, 0x3e8ecfff, 0x3c8ccfff, 0x3a89cfff, 0x3987cfff, 0x3885d0ff, 0x3783d0ff, 0x3781d0ff, 0x377fd0ff, 0x377cd0ff, 0x377ad0ff, 0x3878cfff, 0x3975cfff, 0x3a73ceff, 0x3b71cdff, 0x3d6eccff, 0x3e6ccbff, 0x3f69c9ff, 0x4167c7ff, 0x4265c5ff, 0x4363c3ff, 0x4560c1ff, 0x465ebeff, 0x475cbcff, 0x475ab9ff, 0x4858b6ff, 0x4956b3ff, 0x4954b0ff, 0x4952adff, 0x4a50a9ff, 0x4a4fa5ff, 0x494da1ff, 0x494c9eff, 0x494a9aff, 0x484996ff, 0x474792ff, 0x47468eff, 0x46458aff, 0x454386ff, 0x444282ff, 0x43417fff, 0x42407bff, 0x413e77ff, 0x3f3d74ff, 0x3e3c70ff, 0x3d3b6dff, 0x3c3a69ff, 0x3b3866ff, 0x393763ff, 0x38365fff, 0x37355cff, 0x363459ff, 0x343356ff, 0x333153ff, 0x323050ff, 0x312f4dff, 0x302e4aff, 0x2e2d48ff, 0x2d2c45ff, 0x2c2b42ff, 0x2b2a40ff, 0x2a293dff, 0x29283bff, 0x282739ff, 0x272636ff, 0x262534ff, 0x252532ff, 0x242430ff, 0x24232eff, 0x23222dff, 0x22222bff, 0x222129ff, 0x212028ff, 0x212026ff, 0x202025ff, 0x201f24ff, 0x1f1f23ff, 0x1f1f21ff, 0x1f1e21ff, 0x1f1e20ff, 0x1f1e1fff, 0x1f1e1eff, 0x1f1e1eff, 0x201e1eff, 0x211e1eff, 0x221e1eff, 0x231e1eff, 0x251e1fff, 0x261e1fff, 0x271e1fff, 0x291e20ff, 0x2a1e20ff, 0x2c1e21ff, 0x2d1f21ff, 0x2f1f22ff, 0x311f23ff, 0x332023ff, 0x352024ff, 0x372025ff, 0x392126ff, 0x3b2127ff, 0x3d2228ff, 0x3f2228ff, 0x412329ff, 0x43232aff, 0x46242bff, 0x48242cff, 0x4a252eff, 0x4d252fff, 0x4f2630ff, 0x522731ff, 0x542732ff, 0x572833ff, 0x5a2834ff, 0x5c2935ff, 0x5f2936ff, 0x622937ff, 0x642a38ff, 0x672a39ff, 0x6a2b3aff, 0x6d2b3bff, 0x702b3cff, 0x722c3dff, 0x752c3eff, 0x782c3fff, 0x7b2d40ff, 0x7e2d40ff, 0x812d41ff, 0x842d42ff, 0x872d42ff, 0x8a2e43ff, 0x8d2e43ff, 0x902e44ff, 0x932e44ff, 0x962e44ff, 0x992e44ff, 0x9c2f45ff, 0x9f2f44ff, 0xa22f44ff, 0xa52f44ff, 0xa83044ff, 0xab3043ff, 0xae3143ff, 0xb13242ff, 0xb33341ff, 0xb63441ff, 0xb93540ff, 0xbb363fff, 0xbe373eff, 0xc0393dff, 0xc33a3cff, 0xc53c3cff, 0xc73d3bff, 0xc93f3aff, 0xcc4139ff, 0xce4338ff, 0xd04537ff, 0xd24737ff, 0xd34936ff, 0xd54b35ff, 0xd74e35ff, 0xd95034ff, 0xda5334ff, 0xdc5534ff, 0xde5733ff, 0xdf5a33ff, 0xe15c33ff, 0xe25f33ff, 0xe36233ff, 0xe56433ff, 0xe66734ff, 0xe76a34ff, 0xe86d35ff, 0xe96f36ff, 0xea7238ff, 0xeb753aff, 0xec783bff, 0xed7b3eff, 0xed7e40ff, 0xee8142ff, 0xef8445ff, 0xef8748ff, 0xf0894bff, 0xf18c4eff, 0xf18f51ff, 0xf29255ff, 0xf29558ff, 0xf3985bff, 0xf39a5fff, 0xf49d63ff, 0xf5a066ff, 0xf5a36aff, 0xf6a56dff, 0xf6a871ff, 0xf7ab75ff, 0xf7ae79ff, 0xf8b07cff, 0xf8b380ff, 0xf9b684ff, 0xfab887ff, 0xfabb8bff, 0xfbbe8fff, 0xfbc192ff, 0xfcc396ff, 0xfcc69aff, 0xfdc99eff, 0xfdcca1ff, 0xfecea5ff, 0xfed1a9ff, 0xffd4acff);
|
||||
preset!(flare; 0xedb081ff, 0xedaf80ff, 0xedae7fff, 0xedad7fff, 0xedac7eff, 0xedab7eff, 0xecaa7dff, 0xeca97cff, 0xeca87cff, 0xeca77bff, 0xeca67bff, 0xeca57aff, 0xeca479ff, 0xeca379ff, 0xeca278ff, 0xeca178ff, 0xeca077ff, 0xec9f76ff, 0xeb9e76ff, 0xeb9d75ff, 0xeb9c75ff, 0xeb9b74ff, 0xeb9a73ff, 0xeb9973ff, 0xeb9972ff, 0xeb9872ff, 0xeb9771ff, 0xea9671ff, 0xea9570ff, 0xea946fff, 0xea936fff, 0xea926eff, 0xea916eff, 0xea906dff, 0xea8f6cff, 0xea8e6cff, 0xe98d6bff, 0xe98c6bff, 0xe98b6aff, 0xe98a6aff, 0xe98969ff, 0xe98868ff, 0xe98768ff, 0xe98667ff, 0xe88567ff, 0xe88466ff, 0xe88366ff, 0xe88265ff, 0xe88165ff, 0xe88064ff, 0xe87f64ff, 0xe77e63ff, 0xe77d63ff, 0xe77c63ff, 0xe77b62ff, 0xe77a62ff, 0xe67961ff, 0xe67861ff, 0xe67760ff, 0xe67660ff, 0xe67560ff, 0xe5745fff, 0xe5735fff, 0xe5725fff, 0xe5715eff, 0xe5705eff, 0xe46f5eff, 0xe46e5eff, 0xe46d5dff, 0xe46c5dff, 0xe36b5dff, 0xe36a5dff, 0xe3695dff, 0xe3685cff, 0xe2675cff, 0xe2665cff, 0xe2655cff, 0xe1645cff, 0xe1635cff, 0xe1625cff, 0xe0615cff, 0xe0605cff, 0xe05f5cff, 0xdf5f5cff, 0xdf5e5cff, 0xde5d5cff, 0xde5c5cff, 0xde5b5cff, 0xdd5a5cff, 0xdd595cff, 0xdc585cff, 0xdc575cff, 0xdb565dff, 0xdb565dff, 0xda555dff, 0xda545dff, 0xd9535dff, 0xd9525eff, 0xd8525eff, 0xd7515eff, 0xd7505eff, 0xd64f5fff, 0xd64f5fff, 0xd54e5fff, 0xd44d60ff, 0xd44c60ff, 0xd34c60ff, 0xd24b60ff, 0xd24a61ff, 0xd14a61ff, 0xd04962ff, 0xd04962ff, 0xcf4862ff, 0xce4763ff, 0xcd4763ff, 0xcc4663ff, 0xcc4664ff, 0xcb4564ff, 0xca4564ff, 0xc94465ff, 0xc84465ff, 0xc84365ff, 0xc74366ff, 0xc64366ff, 0xc54266ff, 0xc44267ff, 0xc34167ff, 0xc24167ff, 0xc14168ff, 0xc14068ff, 0xc04068ff, 0xbf4069ff, 0xbe3f69ff, 0xbd3f69ff, 0xbc3f69ff, 0xbb3f6aff, 0xba3e6aff, 0xb93e6aff, 0xb83e6bff, 0xb73d6bff, 0xb63d6bff, 0xb53d6bff, 0xb43d6bff, 0xb33c6cff, 0xb23c6cff, 0xb13c6cff, 0xb13c6cff, 0xb03b6dff, 0xaf3b6dff, 0xae3b6dff, 0xad3b6dff, 0xac3a6dff, 0xab3a6dff, 0xaa3a6eff, 0xa93a6eff, 0xa8396eff, 0xa7396eff, 0xa6396eff, 0xa5396eff, 0xa4386fff, 0xa3386fff, 0xa2386fff, 0xa1386fff, 0xa1376fff, 0xa0376fff, 0x9f376fff, 0x9e3770ff, 0x9d3670ff, 0x9c3670ff, 0x9b3670ff, 0x9a3670ff, 0x993570ff, 0x983570ff, 0x973570ff, 0x963570ff, 0x953470ff, 0x943470ff, 0x943471ff, 0x933471ff, 0x923371ff, 0x913371ff, 0x903371ff, 0x8f3371ff, 0x8e3271ff, 0x8d3271ff, 0x8c3271ff, 0x8b3271ff, 0x8a3171ff, 0x893171ff, 0x883171ff, 0x873171ff, 0x873171ff, 0x863071ff, 0x853071ff, 0x843071ff, 0x833070ff, 0x822f70ff, 0x812f70ff, 0x802f70ff, 0x7f2f70ff, 0x7e2f70ff, 0x7d2e70ff, 0x7c2e70ff, 0x7b2e70ff, 0x7a2e70ff, 0x792e6fff, 0x782e6fff, 0x772d6fff, 0x762d6fff, 0x752d6fff, 0x752d6fff, 0x742d6eff, 0x732c6eff, 0x722c6eff, 0x712c6eff, 0x702c6eff, 0x6f2c6dff, 0x6e2c6dff, 0x6d2b6dff, 0x6c2b6dff, 0x6b2b6cff, 0x6a2b6cff, 0x692b6cff, 0x682a6cff, 0x672a6bff, 0x662a6bff, 0x652a6bff, 0x642a6aff, 0x642a6aff, 0x63296aff, 0x62296aff, 0x612969ff, 0x602969ff, 0x5f2969ff, 0x5e2868ff, 0x5d2868ff, 0x5c2868ff, 0x5b2867ff, 0x5a2767ff, 0x592767ff, 0x582766ff, 0x582766ff, 0x572766ff, 0x562666ff, 0x552665ff, 0x542665ff, 0x532665ff, 0x522564ff, 0x512564ff, 0x502564ff, 0x4f2463ff, 0x4f2463ff, 0x4e2463ff, 0x4d2463ff, 0x4c2362ff, 0x4b2362ff);
|
||||
preset!(crest; 0xa5cd90ff, 0xa4cc90ff, 0xa3cc91ff, 0xa2cb91ff, 0xa0cb91ff, 0x9fca91ff, 0x9eca91ff, 0x9dc991ff, 0x9cc891ff, 0x9bc891ff, 0x9ac791ff, 0x99c791ff, 0x98c691ff, 0x96c691ff, 0x95c591ff, 0x94c591ff, 0x93c491ff, 0x92c491ff, 0x91c391ff, 0x90c391ff, 0x8fc291ff, 0x8ec291ff, 0x8dc191ff, 0x8bc191ff, 0x8ac091ff, 0x89bf91ff, 0x88bf91ff, 0x87be91ff, 0x86be91ff, 0x85bd91ff, 0x84bd91ff, 0x82bc91ff, 0x81bc91ff, 0x80bb91ff, 0x7fbb91ff, 0x7eba91ff, 0x7dba91ff, 0x7cb991ff, 0x7bb991ff, 0x79b891ff, 0x78b891ff, 0x77b791ff, 0x76b791ff, 0x75b690ff, 0x74b690ff, 0x73b590ff, 0x72b490ff, 0x71b490ff, 0x70b390ff, 0x6fb390ff, 0x6eb290ff, 0x6db290ff, 0x6cb190ff, 0x6bb190ff, 0x6ab090ff, 0x69b090ff, 0x68af90ff, 0x67ae90ff, 0x66ae90ff, 0x65ad90ff, 0x64ad90ff, 0x63ac90ff, 0x62ac90ff, 0x62ab90ff, 0x61aa90ff, 0x60aa90ff, 0x5fa990ff, 0x5ea990ff, 0x5da890ff, 0x5ca890ff, 0x5ba790ff, 0x5ba690ff, 0x5aa690ff, 0x59a590ff, 0x58a590ff, 0x57a490ff, 0x57a490ff, 0x56a390ff, 0x55a290ff, 0x54a290ff, 0x53a190ff, 0x53a190ff, 0x52a090ff, 0x519f90ff, 0x509f90ff, 0x509e90ff, 0x4f9e90ff, 0x4e9d90ff, 0x4e9d90ff, 0x4d9c90ff, 0x4c9b90ff, 0x4b9b90ff, 0x4b9a8fff, 0x4a9a8fff, 0x49998fff, 0x49988fff, 0x48988fff, 0x47978fff, 0x47978fff, 0x46968fff, 0x45958fff, 0x45958fff, 0x44948fff, 0x43948fff, 0x43938fff, 0x42928fff, 0x41928fff, 0x41918fff, 0x40918fff, 0x40908eff, 0x3f8f8eff, 0x3e8f8eff, 0x3e8e8eff, 0x3d8e8eff, 0x3c8d8eff, 0x3c8c8eff, 0x3b8c8eff, 0x3a8b8eff, 0x3a8b8eff, 0x398a8eff, 0x388a8eff, 0x38898eff, 0x37888eff, 0x37888dff, 0x36878dff, 0x35878dff, 0x35868dff, 0x34858dff, 0x33858dff, 0x33848dff, 0x32848dff, 0x31838dff, 0x31828dff, 0x30828dff, 0x2f818dff, 0x2f818dff, 0x2e808dff, 0x2d808cff, 0x2d7f8cff, 0x2c7e8cff, 0x2c7e8cff, 0x2b7d8cff, 0x2a7d8cff, 0x2a7c8cff, 0x297b8cff, 0x287b8cff, 0x287a8cff, 0x277a8cff, 0x27798cff, 0x26788cff, 0x25788cff, 0x25778cff, 0x24778bff, 0x24768bff, 0x23758bff, 0x23758bff, 0x22748bff, 0x22748bff, 0x21738bff, 0x21728bff, 0x20728bff, 0x20718bff, 0x20718bff, 0x1f708bff, 0x1f6f8aff, 0x1e6f8aff, 0x1e6e8aff, 0x1e6d8aff, 0x1e6d8aff, 0x1d6c8aff, 0x1d6c8aff, 0x1d6b8aff, 0x1d6a8aff, 0x1d6a8aff, 0x1c6989ff, 0x1c6889ff, 0x1c6889ff, 0x1c6789ff, 0x1c6689ff, 0x1c6689ff, 0x1c6589ff, 0x1c6488ff, 0x1c6488ff, 0x1c6388ff, 0x1d6388ff, 0x1d6288ff, 0x1d6188ff, 0x1d6187ff, 0x1d6087ff, 0x1d5f87ff, 0x1d5f87ff, 0x1e5e87ff, 0x1e5d86ff, 0x1e5d86ff, 0x1e5c86ff, 0x1e5b86ff, 0x1f5b86ff, 0x1f5a85ff, 0x1f5985ff, 0x1f5985ff, 0x205885ff, 0x205784ff, 0x205784ff, 0x205684ff, 0x215584ff, 0x215583ff, 0x215483ff, 0x225383ff, 0x225283ff, 0x225282ff, 0x225182ff, 0x235082ff, 0x235081ff, 0x234f81ff, 0x244e81ff, 0x244e80ff, 0x244d80ff, 0x254c80ff, 0x254c7fff, 0x254b7fff, 0x254a7fff, 0x26497eff, 0x26497eff, 0x26487eff, 0x27477dff, 0x27477dff, 0x27467cff, 0x27457cff, 0x28457cff, 0x28447bff, 0x28437bff, 0x28427aff, 0x29427aff, 0x29417aff, 0x294079ff, 0x294079ff, 0x2a3f78ff, 0x2a3e78ff, 0x2a3d78ff, 0x2a3d77ff, 0x2a3c77ff, 0x2a3b76ff, 0x2b3b76ff, 0x2b3a76ff, 0x2b3975ff, 0x2b3875ff, 0x2b3875ff, 0x2b3774ff, 0x2b3674ff, 0x2c3574ff, 0x2c3573ff, 0x2c3473ff, 0x2c3373ff, 0x2c3272ff, 0x2c3172ff, 0x2c3172ff);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
778
crates/typst/src/geom/gradient.rs
Normal file
@ -0,0 +1,778 @@
|
||||
use std::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
use std::f64::{EPSILON, NEG_INFINITY};
|
||||
use std::fmt::{self, Debug, Write};
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
|
||||
use typst_macros::{cast, func, scope, ty, Cast};
|
||||
use typst_syntax::{Span, Spanned};
|
||||
|
||||
use super::color::{Hsl, Hsv};
|
||||
use super::*;
|
||||
use crate::diag::{bail, error, SourceResult};
|
||||
use crate::eval::{array, Args, Array, Func, IntoValue};
|
||||
use crate::geom::{ColorSpace, Smart};
|
||||
|
||||
/// A color gradient.
|
||||
///
|
||||
/// Typst supports linear gradients through the
|
||||
/// [`gradient.linear` function]($gradient.linear). Radial and conic gradients
|
||||
/// will be available soon.
|
||||
///
|
||||
/// See the [tracking issue](https://github.com/typst/typst/issues/2282) for
|
||||
/// more details on the progress of gradient implementation.
|
||||
///
|
||||
/// ## Stops
|
||||
/// A gradient is composed of a series of stops. Each of these stops has a color
|
||||
/// and an offset. The offset is a [ratio]($ratio) between `{0%}` and `{100%}`
|
||||
/// that determines how far along the gradient the stop is located. The stop's
|
||||
/// color is the color of the gradient at that position. You can choose to omit
|
||||
/// the offsets when defining a gradient. In this case, Typst will space all
|
||||
/// stops evenly.
|
||||
///
|
||||
/// ## Usage
|
||||
/// Gradients can be used for the following purposes:
|
||||
/// - As fills to paint the interior of a shape:
|
||||
/// `{rect(fill: gradient.linear(..))}`
|
||||
/// - As strokes to paint the outline of a shape:
|
||||
/// `{rect(stroke: 1pt + gradient.linear(..))}`
|
||||
/// - As color maps you can [sample]($gradient.sample) from:
|
||||
/// `{gradient.linear(..).sample(0.5)}`
|
||||
///
|
||||
/// Gradients are not currently supported on text.
|
||||
///
|
||||
/// ## Relativeness
|
||||
/// The location of the `{0%}` and `{100%}` stops is dependant on the dimensions
|
||||
/// of a container. This container can either be the shape they are painted on,
|
||||
/// or to the closest container ancestor. This is controlled by the `relative`
|
||||
/// argument of a gradient constructor. By default, gradients are relative to
|
||||
/// the shape they are painted on.
|
||||
///
|
||||
/// Typst determines the ancestor container as follows:
|
||||
/// - For shapes that are placed at the root/top level of the document, the
|
||||
/// closest ancestor is the page itself.
|
||||
/// - For other shapes, the ancestor is the innermost [`block`]($block) or
|
||||
/// [`box`]($box) that contains the shape. This includes the boxes and blocks
|
||||
/// that are implicitly created by show rules and elements. For example, a
|
||||
/// [`rotate`]($rotate) will not affect the parent of a gradient, but a
|
||||
/// [`grid`]($grid) will.
|
||||
///
|
||||
/// ## Color spaces and interpolation
|
||||
/// Gradients can be interpolated in any color space. By default, gradients are
|
||||
/// interpolated in the [Oklab]($color.oklab) color space, which is a
|
||||
/// [perceptually uniform](https://programmingdesignsystems.com/color/perceptually-uniform-color-spaces/index.html)
|
||||
/// color space. This means that the gradient will be perceived as having a
|
||||
/// smooth progression of colors. This is particularly useful for data
|
||||
/// visualization.
|
||||
///
|
||||
/// However, you can choose to interpolate the gradient in any supported color
|
||||
/// space you want, but beware that some color spaces are not suitable for
|
||||
/// perceptually interpolating between colors. Consult the table below when
|
||||
/// choosing an interpolation space.
|
||||
///
|
||||
/// | Color space | Perceptually uniform? |
|
||||
/// | ------------------------------- |:----------------------|
|
||||
/// | [Oklab]($color.oklab) | *Yes* |
|
||||
/// | [sRGB]($color.rgb) | *No* |
|
||||
/// | [linear-RGB]($color.linear-rgb) | *Yes* |
|
||||
/// | [CMYK]($color.cmyk) | *No* |
|
||||
/// | [Grayscale]($color.luma) | *Yes* |
|
||||
/// | [HSL]($color.hsl) | *No* |
|
||||
/// | [HSV]($color.hsv) | *No* |
|
||||
///
|
||||
/// ```example
|
||||
/// #set text(fill: white)
|
||||
/// #set block(spacing: 0pt)
|
||||
///
|
||||
/// #let spaces = (
|
||||
/// ("Oklab", color.oklab),
|
||||
/// ("sRGB", color.rgb),
|
||||
/// ("linear-RGB", color.linear-rgb),
|
||||
/// ("CMYK", color.cmyk),
|
||||
/// ("Grayscale", color.luma),
|
||||
/// ("HSL", color.hsl),
|
||||
/// ("HSV", color.hsv),
|
||||
/// )
|
||||
///
|
||||
/// #for (name, space) in spaces {
|
||||
/// block(
|
||||
/// width: 100%,
|
||||
/// height: 10pt,
|
||||
/// fill: gradient.linear(
|
||||
/// red,
|
||||
/// blue,
|
||||
/// space: space
|
||||
/// ),
|
||||
/// name
|
||||
/// )
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Direction
|
||||
/// Some gradients are sensitive to direction. For example, a linear gradient
|
||||
/// has an angle that determines the its direction. Typst uses a clockwise
|
||||
/// angle, with 0° being from left-to-right, 90° from top-to-bottom, 180° from
|
||||
/// right-to-left, and 270° from bottom-to-top.
|
||||
///
|
||||
/// ```example
|
||||
/// #set block(spacing: 0pt)
|
||||
/// #stack(
|
||||
/// dir: ltr,
|
||||
/// square(size: 50pt, fill: gradient.linear(red, blue, angle: 0deg)),
|
||||
/// square(size: 50pt, fill: gradient.linear(red, blue, angle: 90deg)),
|
||||
/// square(size: 50pt, fill: gradient.linear(red, blue, angle: 180deg)),
|
||||
/// square(size: 50pt, fill: gradient.linear(red, blue, angle: 270deg)),
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// ## Note on compatibility
|
||||
/// Gradients in [{`rotate`}]($rotate) blocks may not be rendered correctly by
|
||||
/// [PDF.js](https://mozilla.github.io/pdf.js/), the PDF reader bundled with
|
||||
/// Firefox. This is due to an issue in PDF.js, you can find the issue as reported
|
||||
/// on [their GitHub](https://github.com/mozilla/pdf.js/issues/17065).
|
||||
///
|
||||
/// ## Presets
|
||||
///
|
||||
/// You can find the full list of presets in the documentation of [`color`]($color),
|
||||
/// below is an overview of them. Note that not all presets are suitable for data
|
||||
/// visualization and full details and relevant sources can be found in the
|
||||
/// documentation of [`color`]($color).
|
||||
///
|
||||
/// ```example
|
||||
/// #set text(fill: white, size: 18pt)
|
||||
/// #set text(top-edge: "bounds", bottom-edge: "bounds")
|
||||
/// #let presets = (
|
||||
/// ("turbo", color.map.turbo),
|
||||
/// ("cividis", color.map.cividis),
|
||||
/// ("rainbow", color.map.rainbow),
|
||||
/// ("spectral", color.map.spectral),
|
||||
/// ("viridis", color.map.viridis),
|
||||
/// ("inferno", color.map.inferno),
|
||||
/// ("magma", color.map.magma),
|
||||
/// ("plasma", color.map.plasma),
|
||||
/// ("rocket", color.map.rocket),
|
||||
/// ("mako", color.map.mako),
|
||||
/// ("vlag", color.map.vlag),
|
||||
/// ("icefire", color.map.icefire),
|
||||
/// ("flare", color.map.flare),
|
||||
/// ("crest", color.map.crest),
|
||||
/// )
|
||||
///
|
||||
/// #stack(
|
||||
/// spacing: 3pt,
|
||||
/// ..presets.map(((name, preset)) => block(
|
||||
/// width: 100%,
|
||||
/// height: 20pt,
|
||||
/// fill: gradient.linear(..preset),
|
||||
/// align(center + horizon, smallcaps(name)),
|
||||
/// ))
|
||||
/// )
|
||||
/// ```
|
||||
#[ty(scope)]
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Gradient {
|
||||
Linear(Arc<LinearGradient>),
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl Gradient {
|
||||
/// Creates a new linear gradient.
|
||||
#[func(title = "Linear Gradient")]
|
||||
pub fn linear(
|
||||
/// The args of this function.
|
||||
args: Args,
|
||||
/// The call site of this function.
|
||||
span: Span,
|
||||
/// The color [stops](#stops) of the gradient.
|
||||
#[variadic]
|
||||
stops: Vec<Spanned<Stop>>,
|
||||
/// The color space in which to interpolate the gradient.
|
||||
///
|
||||
/// Defaults to a perceptually uniform color space called
|
||||
/// [Oklab]($color.oklab).
|
||||
#[named]
|
||||
#[default(ColorSpace::Oklab)]
|
||||
space: ColorSpace,
|
||||
/// The [relative placement](#relativeness) of the gradient.
|
||||
///
|
||||
/// For an element placed at the root/top level of the document, the parent
|
||||
/// is the page itself. For other elements, the parent is the innermost block,
|
||||
/// box, column, grid, or stack that contains the element.
|
||||
#[named]
|
||||
#[default(Smart::Auto)]
|
||||
relative: Smart<Relative>,
|
||||
/// The direction of the gradient.
|
||||
#[external]
|
||||
#[default(Dir::LTR)]
|
||||
dir: Dir,
|
||||
/// The angle of the gradient.
|
||||
#[external]
|
||||
angle: Angle,
|
||||
) -> SourceResult<Gradient> {
|
||||
let mut args = args;
|
||||
if stops.len() < 2 {
|
||||
bail!(error!(span, "a gradient must have at least two stops")
|
||||
.with_hint("try filling the shape with a single color instead"));
|
||||
}
|
||||
|
||||
let angle = if let Some(angle) = args.named::<Angle>("angle")? {
|
||||
angle
|
||||
} else if let Some(dir) = args.named::<Dir>("dir")? {
|
||||
match dir {
|
||||
Dir::LTR => Angle::rad(0.0),
|
||||
Dir::RTL => Angle::rad(PI),
|
||||
Dir::TTB => Angle::rad(FRAC_PI_2),
|
||||
Dir::BTT => Angle::rad(3.0 * FRAC_PI_2),
|
||||
}
|
||||
} else {
|
||||
Angle::rad(0.0)
|
||||
};
|
||||
|
||||
Ok(Self::Linear(Arc::new(LinearGradient {
|
||||
stops: process_stops(&stops)?,
|
||||
angle,
|
||||
space,
|
||||
relative,
|
||||
anti_alias: true,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Returns the stops of this gradient.
|
||||
#[func]
|
||||
pub fn stops(&self) -> Vec<Stop> {
|
||||
match self {
|
||||
Self::Linear(linear) => linear
|
||||
.stops
|
||||
.iter()
|
||||
.map(|(color, offset)| Stop { color: *color, offset: Some(*offset) })
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the mixing space of this gradient.
|
||||
#[func]
|
||||
pub fn space(&self) -> ColorSpace {
|
||||
match self {
|
||||
Self::Linear(linear) => linear.space,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the relative placement of this gradient.
|
||||
#[func]
|
||||
pub fn relative(&self) -> Smart<Relative> {
|
||||
match self {
|
||||
Self::Linear(linear) => linear.relative,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the angle of this gradient.
|
||||
#[func]
|
||||
pub fn angle(&self) -> Angle {
|
||||
match self {
|
||||
Self::Linear(linear) => linear.angle,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the kind of this gradient.
|
||||
#[func]
|
||||
pub fn kind(&self) -> Func {
|
||||
match self {
|
||||
Self::Linear(_) => Self::linear_data().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample the gradient at a given position.
|
||||
///
|
||||
/// The position is either a position along the gradient (a [ratio]($ratio)
|
||||
/// between `{0%}` and `{100%}`) or an [angle]($angle). Any value outside
|
||||
/// of this range will be clamped.
|
||||
///
|
||||
/// _The angle will be used for conic gradients once they are available._
|
||||
#[func]
|
||||
pub fn sample(
|
||||
&self,
|
||||
/// The position at which to sample the gradient.
|
||||
t: RatioOrAngle,
|
||||
) -> Color {
|
||||
let value: f64 = t.to_ratio().get();
|
||||
|
||||
match self {
|
||||
Self::Linear(linear) => sample_stops(&linear.stops, linear.space, value),
|
||||
}
|
||||
}
|
||||
|
||||
/// Samples the gradient at the given positions.
|
||||
///
|
||||
/// The position is either a position along the gradient (a [ratio]($ratio)
|
||||
/// between `{0%}` and `{100%}`) or an [angle]($angle). Any value outside
|
||||
/// of this range will be clamped.
|
||||
///
|
||||
/// _The angle will be used for conic gradients once they are available._
|
||||
#[func]
|
||||
pub fn samples(
|
||||
&self,
|
||||
/// The positions at which to sample the gradient.
|
||||
#[variadic]
|
||||
ts: Vec<RatioOrAngle>,
|
||||
) -> Array {
|
||||
ts.into_iter().map(|t| self.sample(t).into_value()).collect()
|
||||
}
|
||||
|
||||
/// Creates a sharp version of this gradient.
|
||||
///
|
||||
/// _Sharp gradients_ have discreet jumps between colors, instead of a
|
||||
/// smooth transition. They are particularly useful for creating color
|
||||
/// lists for a preset gradient.
|
||||
///
|
||||
/// ```example
|
||||
/// #let grad = gradient.linear(..color.map.rainbow)
|
||||
/// #rect(width: 100%, height: 20pt, fill: grad)
|
||||
/// #rect(width: 100%, height: 20pt, fill: grad.sharp(5))
|
||||
/// ```
|
||||
#[func]
|
||||
pub fn sharp(
|
||||
&self,
|
||||
/// The number of stops in the gradient.
|
||||
steps: Spanned<usize>,
|
||||
/// How much to smooth the gradient.
|
||||
#[named]
|
||||
#[default(Spanned::new(Ratio::zero(), Span::detached()))]
|
||||
smoothness: Spanned<Ratio>,
|
||||
) -> SourceResult<Gradient> {
|
||||
if steps.v < 2 {
|
||||
bail!(steps.span, "sharp gradients must have at least two stops");
|
||||
}
|
||||
|
||||
if smoothness.v.get() < 0.0 || smoothness.v.get() > 1.0 {
|
||||
bail!(smoothness.span, "smoothness must be between 0 and 1");
|
||||
}
|
||||
|
||||
let n = steps.v;
|
||||
let smoothness = smoothness.v.get();
|
||||
let colors = (0..n)
|
||||
.flat_map(|i| {
|
||||
let c = self
|
||||
.sample(RatioOrAngle::Ratio(Ratio::new(i as f64 / (n - 1) as f64)));
|
||||
|
||||
[c, c]
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut positions = Vec::with_capacity(n * 2);
|
||||
let index_to_progress = |i| i as f64 * 1.0 / n as f64;
|
||||
|
||||
let progress = smoothness * 1.0 / (4.0 * n as f64);
|
||||
for i in 0..n {
|
||||
let mut j = 2 * i;
|
||||
positions.push(index_to_progress(i));
|
||||
if j > 0 {
|
||||
positions[j] += progress;
|
||||
}
|
||||
|
||||
j += 1;
|
||||
positions.push(index_to_progress(i + 1));
|
||||
if j < colors.len() - 1 {
|
||||
positions[j] -= progress;
|
||||
}
|
||||
}
|
||||
|
||||
let mut stops = colors
|
||||
.into_iter()
|
||||
.zip(positions)
|
||||
.map(|(c, p)| (c, Ratio::new(p)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
stops.dedup();
|
||||
|
||||
Ok(match self {
|
||||
Self::Linear(linear) => Self::Linear(Arc::new(LinearGradient {
|
||||
stops,
|
||||
angle: linear.angle,
|
||||
space: linear.space,
|
||||
relative: linear.relative,
|
||||
anti_alias: false,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
/// Repeats this gradient a given number of times, optionally mirroring it
|
||||
/// at each repetition.
|
||||
#[func]
|
||||
pub fn repeat(
|
||||
&self,
|
||||
/// The number of times to repeat the gradient.
|
||||
repetitions: Spanned<usize>,
|
||||
/// Whether to mirror the gradient at each repetition.
|
||||
#[named]
|
||||
#[default(false)]
|
||||
mirror: bool,
|
||||
) -> SourceResult<Gradient> {
|
||||
if repetitions.v == 0 {
|
||||
bail!(repetitions.span, "must repeat at least once");
|
||||
}
|
||||
|
||||
let n = repetitions.v;
|
||||
let mut stops = std::iter::repeat(self.stops_ref())
|
||||
.take(n)
|
||||
.enumerate()
|
||||
.flat_map(|(i, stops)| {
|
||||
let mut stops = stops
|
||||
.iter()
|
||||
.map(move |&(color, offset)| {
|
||||
let t = i as f64 / n as f64;
|
||||
let r = offset.get();
|
||||
if i % 2 == 1 && mirror {
|
||||
(color, Ratio::new(t + (1.0 - r) / n as f64))
|
||||
} else {
|
||||
(color, Ratio::new(t + r / n as f64))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if i % 2 == 1 && mirror {
|
||||
stops.reverse();
|
||||
}
|
||||
|
||||
stops
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
stops.dedup();
|
||||
|
||||
Ok(match self {
|
||||
Self::Linear(grad) => Self::Linear(Arc::new(LinearGradient {
|
||||
stops,
|
||||
angle: grad.angle,
|
||||
space: grad.space,
|
||||
relative: grad.relative,
|
||||
anti_alias: true,
|
||||
})),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Gradient {
|
||||
/// Returns a reference to the stops of this gradient.
|
||||
pub fn stops_ref(&self) -> &[(Color, Ratio)] {
|
||||
match self {
|
||||
Gradient::Linear(linear) => &linear.stops,
|
||||
}
|
||||
}
|
||||
|
||||
/// Samples the gradient at a given position, in the given container.
|
||||
/// Handles the aspect ratio and angle directly.
|
||||
pub fn sample_at(&self, (x, y): (f32, f32), (width, height): (f32, f32)) -> Color {
|
||||
let t = match self {
|
||||
Self::Linear(linear) => {
|
||||
// Normalize the coordinates.
|
||||
let (mut x, mut y) = (x / width, y / height);
|
||||
|
||||
// Handle the direction of the gradient.
|
||||
let angle = linear.angle.to_rad().rem_euclid(TAU);
|
||||
|
||||
// Aspect ratio correction.
|
||||
let angle = (angle.tan() * height as f64).atan2(width as f64);
|
||||
let angle = match linear.angle.quadrant() {
|
||||
Quadrant::First => angle,
|
||||
Quadrant::Second => angle + PI,
|
||||
Quadrant::Third => angle + PI,
|
||||
Quadrant::Fourth => angle + TAU,
|
||||
};
|
||||
|
||||
let (sin, cos) = angle.sin_cos();
|
||||
|
||||
let length = sin.abs() + cos.abs();
|
||||
if angle > FRAC_PI_2 && angle < 3.0 * FRAC_PI_2 {
|
||||
x = 1.0 - x;
|
||||
}
|
||||
|
||||
if angle > PI {
|
||||
y = 1.0 - y;
|
||||
}
|
||||
|
||||
(x as f64 * cos.abs() + y as f64 * sin.abs()) / length
|
||||
}
|
||||
};
|
||||
|
||||
self.sample(RatioOrAngle::Ratio(Ratio::new(t)))
|
||||
}
|
||||
|
||||
/// Does this gradient need to be anti-aliased?
|
||||
pub fn anti_alias(&self) -> bool {
|
||||
match self {
|
||||
Self::Linear(linear) => linear.anti_alias,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the relative placement of this gradient, handling
|
||||
/// the special case of `auto`.
|
||||
pub fn unwrap_relative(&self, on_text: bool) -> Relative {
|
||||
self.relative().unwrap_or_else(|| {
|
||||
if on_text {
|
||||
Relative::Parent
|
||||
} else {
|
||||
Relative::Self_
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Corrects this angle for the aspect ratio of a gradient.
|
||||
///
|
||||
/// This is used specifically for gradients.
|
||||
pub fn correct_aspect_ratio(angle: Angle, aspect_ratio: Ratio) -> Angle {
|
||||
let rad = (angle.to_rad().rem_euclid(TAU).tan() / aspect_ratio.get()).atan();
|
||||
let rad = match angle.quadrant() {
|
||||
Quadrant::First => rad,
|
||||
Quadrant::Second => rad + PI,
|
||||
Quadrant::Third => rad + PI,
|
||||
Quadrant::Fourth => rad + TAU,
|
||||
};
|
||||
Angle::rad(rad.rem_euclid(TAU))
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Gradient {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Linear(linear) => linear.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A gradient that interpolates between two colors along an axis.
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
pub struct LinearGradient {
|
||||
/// The color stops of this gradient.
|
||||
pub stops: Vec<(Color, Ratio)>,
|
||||
/// The direction of this gradient.
|
||||
pub angle: Angle,
|
||||
/// The color space in which to interpolate the gradient.
|
||||
pub space: ColorSpace,
|
||||
/// The relative placement of the gradient.
|
||||
pub relative: Smart<Relative>,
|
||||
/// Whether to anti-alias the gradient (used for sharp gradients).
|
||||
pub anti_alias: bool,
|
||||
}
|
||||
|
||||
impl Debug for LinearGradient {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("gradient.linear(")?;
|
||||
|
||||
let angle = self.angle.to_rad().rem_euclid(TAU);
|
||||
if angle.abs() < EPSILON {
|
||||
// Default value, do nothing
|
||||
} else if (angle - FRAC_PI_2).abs() < EPSILON {
|
||||
f.write_str("dir: rtl, ")?;
|
||||
} else if (angle - PI).abs() < EPSILON {
|
||||
f.write_str("dir: ttb, ")?;
|
||||
} else if (angle - 3.0 * FRAC_PI_2).abs() < EPSILON {
|
||||
f.write_str("dir: btt, ")?;
|
||||
} else {
|
||||
write!(f, "angle: {:?}, ", self.angle)?;
|
||||
}
|
||||
|
||||
if self.space != ColorSpace::Oklab {
|
||||
write!(f, "space: {:?}, ", self.space.into_value())?;
|
||||
}
|
||||
|
||||
if self.relative.is_custom() {
|
||||
write!(f, "relative: {:?}, ", self.relative.into_value())?;
|
||||
}
|
||||
|
||||
for (i, (color, offset)) in self.stops.iter().enumerate() {
|
||||
write!(f, "({color:?}, {offset:?})")?;
|
||||
|
||||
if i != self.stops.len() - 1 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
}
|
||||
|
||||
f.write_char(')')
|
||||
}
|
||||
}
|
||||
|
||||
/// What is the gradient relative to.
|
||||
#[derive(Cast, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Relative {
|
||||
/// The gradient is relative to itself (its own bounding box).
|
||||
Self_,
|
||||
/// The gradient is relative to its parent (the parent's bounding box).
|
||||
Parent,
|
||||
}
|
||||
|
||||
/// A color stop.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Stop {
|
||||
/// The color for this stop.
|
||||
pub color: Color,
|
||||
/// The offset of the stop along the gradient.
|
||||
pub offset: Option<Ratio>,
|
||||
}
|
||||
|
||||
impl Stop {
|
||||
/// Create a new stop from a `color` and an `offset`.
|
||||
pub fn new(color: Color, offset: Ratio) -> Self {
|
||||
Self { color, offset: Some(offset) }
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
Stop,
|
||||
self => if let Some(offset) = self.offset {
|
||||
array![self.color.into_value(), offset].into_value()
|
||||
} else {
|
||||
self.color.into_value()
|
||||
},
|
||||
color: Color => Self { color, offset: None },
|
||||
array: Array => {
|
||||
let mut iter = array.into_iter();
|
||||
match (iter.next(), iter.next(), iter.next()) {
|
||||
(Some(a), Some(b), None) => Self {
|
||||
color: a.cast()?,
|
||||
offset: Some(b.cast()?)
|
||||
},
|
||||
_ => Err("a color stop must contain exactly two entries")?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A ratio or an angle.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum RatioOrAngle {
|
||||
Ratio(Ratio),
|
||||
Angle(Angle),
|
||||
}
|
||||
|
||||
impl RatioOrAngle {
|
||||
pub fn to_ratio(self) -> Ratio {
|
||||
match self {
|
||||
Self::Ratio(ratio) => ratio,
|
||||
Self::Angle(angle) => Ratio::new(angle.to_rad().rem_euclid(TAU) / TAU),
|
||||
}
|
||||
.clamp(Ratio::zero(), Ratio::one())
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
RatioOrAngle,
|
||||
self => match self {
|
||||
Self::Ratio(ratio) => ratio.into_value(),
|
||||
Self::Angle(angle) => angle.into_value(),
|
||||
},
|
||||
ratio: Ratio => Self::Ratio(ratio),
|
||||
angle: Angle => Self::Angle(angle),
|
||||
}
|
||||
|
||||
/// Pre-processes the stops, checking that they are valid and computing the
|
||||
/// offsets if necessary.
|
||||
///
|
||||
/// Returns an error if the stops are invalid.
|
||||
///
|
||||
/// This is split into its own function because it is used by all of the
|
||||
/// different gradient types.
|
||||
#[comemo::memoize]
|
||||
fn process_stops(stops: &[Spanned<Stop>]) -> SourceResult<Vec<(Color, Ratio)>> {
|
||||
let has_offset = stops.iter().any(|stop| stop.v.offset.is_some());
|
||||
if has_offset {
|
||||
let mut last_stop = NEG_INFINITY;
|
||||
for Spanned { v: stop, span } in stops.iter() {
|
||||
let Some(stop) = stop.offset else {
|
||||
bail!(error!(
|
||||
*span,
|
||||
"either all stops must have an offset or none of them can"
|
||||
)
|
||||
.with_hint("try adding an offset to all stops"));
|
||||
};
|
||||
|
||||
if stop.get() < last_stop {
|
||||
bail!(*span, "offsets must be in strictly monotonic order");
|
||||
}
|
||||
|
||||
last_stop = stop.get();
|
||||
}
|
||||
|
||||
let out = stops
|
||||
.iter()
|
||||
.map(|Spanned { v: Stop { color, offset }, span }| {
|
||||
if offset.unwrap().get() > 1.0 || offset.unwrap().get() < 0.0 {
|
||||
bail!(*span, "offset must be between 0 and 1");
|
||||
}
|
||||
Ok((*color, offset.unwrap()))
|
||||
})
|
||||
.collect::<SourceResult<Vec<_>>>()?;
|
||||
|
||||
if out[0].1 != Ratio::zero() {
|
||||
bail!(error!(stops[0].span, "first stop must have an offset of 0%")
|
||||
.with_hint("try setting this stop to `0%`"));
|
||||
}
|
||||
|
||||
if out[out.len() - 1].1 != Ratio::one() {
|
||||
bail!(error!(stops[0].span, "last stop must have an offset of 100%")
|
||||
.with_hint("try setting this stop to `100%`"));
|
||||
}
|
||||
|
||||
return Ok(out);
|
||||
}
|
||||
|
||||
Ok(stops
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, stop)| {
|
||||
let offset = i as f64 / (stops.len() - 1) as f64;
|
||||
(stop.v.color, Ratio::new(offset))
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Sample the stops at a given position.
|
||||
fn sample_stops(stops: &[(Color, Ratio)], mixing_space: ColorSpace, t: f64) -> Color {
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
let mut low = 0;
|
||||
let mut high = stops.len();
|
||||
|
||||
while low < high {
|
||||
let mid = (low + high) / 2;
|
||||
if stops[mid].1.get() < t {
|
||||
low = mid + 1;
|
||||
} else {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
|
||||
if low == 0 {
|
||||
low = 1;
|
||||
}
|
||||
let (col_0, pos_0) = stops[low - 1];
|
||||
let (col_1, pos_1) = stops[low];
|
||||
let t = (t - pos_0.get()) / (pos_1.get() - pos_0.get());
|
||||
|
||||
let out = Color::mix_iter(
|
||||
[WeightedColor::new(col_0, 1.0 - t), WeightedColor::new(col_1, t)],
|
||||
mixing_space,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Special case for handling multi-turn hue interpolation.
|
||||
if mixing_space == ColorSpace::Hsl || mixing_space == ColorSpace::Hsv {
|
||||
let hue_0 = col_0.to_space(mixing_space).to_vec4()[0];
|
||||
let hue_1 = col_1.to_space(mixing_space).to_vec4()[0];
|
||||
|
||||
// Check if we need to interpolate over the 360° boundary.
|
||||
if (hue_0 - hue_1).abs() > 180.0 {
|
||||
let hue_0 = if hue_0 < hue_1 { hue_0 + 360.0 } else { hue_0 };
|
||||
let hue_1 = if hue_1 < hue_0 { hue_1 + 360.0 } else { hue_1 };
|
||||
|
||||
let hue = (hue_0 * (1.0 - t as f32) + hue_1 * t as f32).rem_euclid(360.0);
|
||||
|
||||
if mixing_space == ColorSpace::Hsl {
|
||||
let [_, saturation, lightness, alpha] = out.to_hsl().to_vec4();
|
||||
return Color::Hsl(Hsl::new(hue, saturation, lightness, alpha));
|
||||
} else if mixing_space == ColorSpace::Hsv {
|
||||
let [_, saturation, value, alpha] = out.to_hsv().to_vec4();
|
||||
return Color::Hsv(Hsv::new(hue, saturation, value, alpha));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
@ -12,6 +12,7 @@ mod dir;
|
||||
mod ellipse;
|
||||
mod em;
|
||||
mod fr;
|
||||
mod gradient;
|
||||
mod length;
|
||||
mod paint;
|
||||
mod path;
|
||||
@ -29,7 +30,7 @@ mod transform;
|
||||
|
||||
pub use self::abs::{Abs, AbsUnit};
|
||||
pub use self::align::{Align, FixedAlign, HAlign, VAlign};
|
||||
pub use self::angle::{Angle, AngleUnit};
|
||||
pub use self::angle::{Angle, AngleUnit, Quadrant};
|
||||
pub use self::axes::{Axes, Axis};
|
||||
pub use self::color::{Color, ColorSpace, WeightedColor};
|
||||
pub use self::corners::{Corner, Corners};
|
||||
@ -37,6 +38,7 @@ pub use self::dir::Dir;
|
||||
pub use self::ellipse::ellipse;
|
||||
pub use self::em::Em;
|
||||
pub use self::fr::Fr;
|
||||
pub use self::gradient::{Gradient, LinearGradient, RatioOrAngle, Relative};
|
||||
pub use self::length::Length;
|
||||
pub use self::paint::Paint;
|
||||
pub use self::path::{Path, PathItem};
|
||||
|
@ -5,6 +5,19 @@ use super::*;
|
||||
pub enum Paint {
|
||||
/// A solid color.
|
||||
Solid(Color),
|
||||
/// A gradient.
|
||||
Gradient(Gradient),
|
||||
}
|
||||
|
||||
impl Paint {
|
||||
/// Temporary method to unwrap a solid color used for text rendering.
|
||||
pub fn unwrap_solid(&self) -> Color {
|
||||
// TODO: Implement gradients on text.
|
||||
match self {
|
||||
Self::Solid(color) => *color,
|
||||
Self::Gradient(_) => panic!("expected solid color"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Color>> From<T> for Paint {
|
||||
@ -13,10 +26,17 @@ impl<T: Into<Color>> From<T> for Paint {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Gradient> for Paint {
|
||||
fn from(gradient: Gradient) -> Self {
|
||||
Self::Gradient(gradient)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Paint {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Solid(color) => color.fmt(f),
|
||||
Self::Gradient(gradient) => gradient.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -25,6 +45,8 @@ cast! {
|
||||
Paint,
|
||||
self => match self {
|
||||
Self::Solid(color) => Value::Color(color),
|
||||
Self::Gradient(gradient) => Value::Gradient(gradient),
|
||||
},
|
||||
color: Color => Self::Solid(color),
|
||||
gradient: Gradient => Self::Gradient(gradient),
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
use kurbo::Shape;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A bezier path.
|
||||
@ -51,4 +53,50 @@ impl Path {
|
||||
pub fn close_path(&mut self) {
|
||||
self.0.push(PathItem::ClosePath);
|
||||
}
|
||||
|
||||
/// Computes the size of bounding box of this path.
|
||||
pub fn bbox_size(&self) -> Size {
|
||||
let mut min_x = Abs::inf();
|
||||
let mut min_y = Abs::inf();
|
||||
let mut max_x = -Abs::inf();
|
||||
let mut max_y = -Abs::inf();
|
||||
|
||||
let mut cursor = Point::zero();
|
||||
for item in self.0.iter() {
|
||||
match item {
|
||||
PathItem::MoveTo(to) => {
|
||||
min_x = min_x.min(cursor.x);
|
||||
min_y = min_y.min(cursor.y);
|
||||
max_x = max_x.max(cursor.x);
|
||||
max_y = max_y.max(cursor.y);
|
||||
cursor = *to;
|
||||
}
|
||||
PathItem::LineTo(to) => {
|
||||
min_x = min_x.min(cursor.x);
|
||||
min_y = min_y.min(cursor.y);
|
||||
max_x = max_x.max(cursor.x);
|
||||
max_y = max_y.max(cursor.y);
|
||||
cursor = *to;
|
||||
}
|
||||
PathItem::CubicTo(c0, c1, end) => {
|
||||
let cubic = kurbo::CubicBez::new(
|
||||
kurbo::Point::new(cursor.x.to_pt(), cursor.y.to_pt()),
|
||||
kurbo::Point::new(c0.x.to_pt(), c0.y.to_pt()),
|
||||
kurbo::Point::new(c1.x.to_pt(), c1.y.to_pt()),
|
||||
kurbo::Point::new(end.x.to_pt(), end.y.to_pt()),
|
||||
);
|
||||
|
||||
let bbox = cubic.bounding_box();
|
||||
min_x = min_x.min(Abs::pt(bbox.x0)).min(Abs::pt(bbox.x1));
|
||||
min_y = min_y.min(Abs::pt(bbox.y0)).min(Abs::pt(bbox.y1));
|
||||
max_x = max_x.max(Abs::pt(bbox.x0)).max(Abs::pt(bbox.x1));
|
||||
max_y = max_y.max(Abs::pt(bbox.y0)).max(Abs::pt(bbox.y1));
|
||||
cursor = *end;
|
||||
}
|
||||
PathItem::ClosePath => (),
|
||||
}
|
||||
}
|
||||
|
||||
Size::new(max_x - min_x, max_y - min_y)
|
||||
}
|
||||
}
|
||||
|
@ -32,4 +32,13 @@ impl Geometry {
|
||||
pub fn stroked(self, stroke: FixedStroke) -> Shape {
|
||||
Shape { geometry: self, fill: None, stroke: Some(stroke) }
|
||||
}
|
||||
|
||||
/// The bounding box of the geometry.
|
||||
pub fn bbox_size(&self) -> Size {
|
||||
match self {
|
||||
Self::Line(line) => Size::new(line.x, line.y),
|
||||
Self::Rect(s) => *s,
|
||||
Self::Path(p) => p.bbox_size(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,11 @@ impl Size {
|
||||
pub fn to_point(self) -> Point {
|
||||
Point::new(self.x, self.y)
|
||||
}
|
||||
|
||||
/// Converts to a ratio of width to height.
|
||||
pub fn aspect_ratio(self) -> Ratio {
|
||||
Ratio::new(self.x / self.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl Numeric for Size {
|
||||
|
@ -68,6 +68,55 @@ impl Transform {
|
||||
pub fn post_concat(self, next: Self) -> Self {
|
||||
next.pre_concat(self)
|
||||
}
|
||||
|
||||
/// Inverts the transformation.
|
||||
///
|
||||
/// Returns `None` if the determinant of the matrix is zero.
|
||||
pub fn invert(self) -> Option<Self> {
|
||||
// Allow the trivial case to be inlined.
|
||||
if self.is_identity() {
|
||||
return Some(self);
|
||||
}
|
||||
|
||||
// Fast path for scale-translate-only transforms.
|
||||
if self.kx.is_zero() && self.ky.is_zero() {
|
||||
if self.sx.is_zero() || self.sy.is_zero() {
|
||||
return Some(Self::translate(-self.tx, -self.ty));
|
||||
}
|
||||
|
||||
let inv_x = 1.0 / self.sx;
|
||||
let inv_y = 1.0 / self.sy;
|
||||
return Some(Self {
|
||||
sx: Ratio::new(inv_x),
|
||||
ky: Ratio::zero(),
|
||||
kx: Ratio::zero(),
|
||||
sy: Ratio::new(inv_y),
|
||||
tx: -self.tx * inv_x,
|
||||
ty: -self.ty * inv_y,
|
||||
});
|
||||
}
|
||||
|
||||
let det = self.sx * self.sy - self.kx * self.ky;
|
||||
if det.get() < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let inv_det = 1.0 / det;
|
||||
Some(Self {
|
||||
sx: (self.sy * inv_det),
|
||||
ky: (-self.ky * inv_det),
|
||||
kx: (-self.kx * inv_det),
|
||||
sy: (self.sx * inv_det),
|
||||
tx: Abs::pt(
|
||||
(self.kx.get() * self.ty.to_pt() - self.sy.get() * self.tx.to_pt())
|
||||
* inv_det,
|
||||
),
|
||||
ty: Abs::pt(
|
||||
(self.ky.get() * self.tx.to_pt() - self.sx.get() * self.ty.to_pt())
|
||||
* inv_det,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Transform {
|
||||
|
BIN
tests/ref/compiler/repr-color-gradient.png
Normal file
After Width: | Height: | Size: 100 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 63 KiB |
BIN
tests/ref/visualize/gradient-dir.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
tests/ref/visualize/gradient-presets.png
Normal file
After Width: | Height: | Size: 307 KiB |
BIN
tests/ref/visualize/gradient-relative.png
Normal file
After Width: | Height: | Size: 474 KiB |
BIN
tests/ref/visualize/gradient-repeat.png
Normal file
After Width: | Height: | Size: 156 KiB |
BIN
tests/ref/visualize/gradient-sharp.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
tests/ref/visualize/gradient-stroke.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
tests/ref/visualize/gradient-transform.png
Normal file
After Width: | Height: | Size: 85 KiB |
@ -132,6 +132,39 @@
|
||||
#test-repr(oklab(luma(40)).components(), (27.68%, 0.0, 0.0, 100%))
|
||||
#test-repr(oklab(rgb(1, 2, 3)).components(), (8.23%, -0.004, -0.007, 100%))
|
||||
|
||||
---
|
||||
// Test gradient functions.
|
||||
#test(gradient.linear(red, green, blue).kind(), gradient.linear)
|
||||
#test(gradient.linear(red, green, blue).stops(), ((red, 0%), (green, 50%), (blue, 100%)))
|
||||
#test(gradient.linear(red, green, blue, space: rgb).sample(0%), red)
|
||||
#test(gradient.linear(red, green, blue, space: rgb).sample(25%), rgb("#97873b"))
|
||||
#test(gradient.linear(red, green, blue, space: rgb).sample(50%), green)
|
||||
#test(gradient.linear(red, green, blue, space: rgb).sample(75%), rgb("#17a08c"))
|
||||
#test(gradient.linear(red, green, blue, space: rgb).sample(100%), blue)
|
||||
#test(gradient.linear(red, green, space: rgb).space(), rgb)
|
||||
#test(gradient.linear(red, green, space: oklab).space(), oklab)
|
||||
#test(gradient.linear(red, green, space: cmyk).space(), cmyk)
|
||||
#test(gradient.linear(red, green, space: luma).space(), luma)
|
||||
#test(gradient.linear(red, green, space: color.linear-rgb).space(), color.linear-rgb)
|
||||
#test(gradient.linear(red, green, space: color.hsl).space(), color.hsl)
|
||||
#test(gradient.linear(red, green, space: color.hsv).space(), color.hsv)
|
||||
#test(gradient.linear(red, green, relative: "self").relative(), "self")
|
||||
#test(gradient.linear(red, green, relative: "parent").relative(), "parent")
|
||||
#test(gradient.linear(red, green).relative(), auto)
|
||||
#test(gradient.linear(red, green).angle(), 0deg)
|
||||
#test(gradient.linear(red, green, dir: ltr).angle(), 0deg)
|
||||
#test(gradient.linear(red, green, dir: rtl).angle(), 180deg)
|
||||
#test(gradient.linear(red, green, dir: ttb).angle(), 90deg)
|
||||
#test(gradient.linear(red, green, dir: btt).angle(), 270deg)
|
||||
#test(
|
||||
gradient.linear(red, green, blue).repeat(2).stops(),
|
||||
((red, 0%), (green, 25%), (blue, 50%), (red, 50%), (green, 75%), (blue, 100%))
|
||||
)
|
||||
#test(
|
||||
gradient.linear(red, green, blue).repeat(2, mirror: true).stops(),
|
||||
((red, 0%), (green, 25%), (blue, 50%), (green, 75%), (red, 100%))
|
||||
)
|
||||
|
||||
---
|
||||
// Test alignment methods.
|
||||
#test(start.axis(), "horizontal")
|
||||
|
22
tests/typ/compiler/repr-color-gradient.typ
Normal file
@ -0,0 +1,22 @@
|
||||
// Test representation of values in the document.
|
||||
|
||||
---
|
||||
// Colors
|
||||
#set page(width: 400pt)
|
||||
#set text(0.8em)
|
||||
#blue \
|
||||
#color.linear-rgb(blue) \
|
||||
#oklab(blue) \
|
||||
#cmyk(blue) \
|
||||
#color.hsl(blue) \
|
||||
#color.hsv(blue) \
|
||||
#luma(blue)
|
||||
|
||||
---
|
||||
// Gradients
|
||||
#set page(width: 400pt)
|
||||
#set text(0.8em)
|
||||
#gradient.linear(blue, red) \
|
||||
#gradient.linear(blue, red, dir: ttb) \
|
||||
#gradient.linear(blue, red, angle: 45deg, relative: "self") \
|
||||
#gradient.linear(blue, red, angle: 45deg, space: rgb)
|
@ -47,13 +47,3 @@
|
||||
#int \
|
||||
#type("hi") \
|
||||
#type((a: 1))
|
||||
|
||||
---
|
||||
#set text(0.8em)
|
||||
#blue \
|
||||
#color.linear-rgb(blue) \
|
||||
#oklab(blue) \
|
||||
#cmyk(blue) \
|
||||
#color.hsl(blue) \
|
||||
#color.hsv(blue) \
|
||||
#luma(blue)
|
||||
|
@ -34,5 +34,5 @@
|
||||
#table()
|
||||
|
||||
---
|
||||
// Error: 14-19 expected color, none, array, or function, found string
|
||||
// Error: 14-19 expected color, gradient, none, array, or function, found string
|
||||
#table(fill: "hey")
|
||||
|
13
tests/typ/visualize/gradient-dir.typ
Normal file
@ -0,0 +1,13 @@
|
||||
// Test gradients with direction.
|
||||
|
||||
---
|
||||
#set page(width: 900pt)
|
||||
#for i in range(0, 360, step: 15){
|
||||
box(
|
||||
height: 100pt,
|
||||
width: 100pt,
|
||||
fill: gradient.linear(angle: i * 1deg, (red, 0%), (blue, 100%)),
|
||||
align(center + horizon)[Angle: #i degrees],
|
||||
)
|
||||
h(30pt)
|
||||
}
|
33
tests/typ/visualize/gradient-presets.typ
Normal file
@ -0,0 +1,33 @@
|
||||
// Test all gradient presets.
|
||||
|
||||
---
|
||||
#set page(width: 200pt, height: auto, margin: 0pt)
|
||||
#set text(fill: white, size: 18pt)
|
||||
#set text(top-edge: "bounds", bottom-edge: "bounds")
|
||||
|
||||
#let presets = (
|
||||
("turbo", color.map.turbo),
|
||||
("cividis", color.map.cividis),
|
||||
("rainbow", color.map.rainbow),
|
||||
("spectral", color.map.spectral),
|
||||
("viridis", color.map.viridis),
|
||||
("inferno", color.map.inferno),
|
||||
("magma", color.map.magma),
|
||||
("plasma", color.map.plasma),
|
||||
("rocket", color.map.rocket),
|
||||
("mako", color.map.mako),
|
||||
("vlag", color.map.vlag),
|
||||
("icefire", color.map.icefire),
|
||||
("flare", color.map.flare),
|
||||
("crest", color.map.crest),
|
||||
)
|
||||
|
||||
#stack(
|
||||
spacing: 3pt,
|
||||
..presets.map(((name, preset)) => block(
|
||||
width: 100%,
|
||||
height: 20pt,
|
||||
fill: gradient.linear(..preset),
|
||||
align(center + horizon, smallcaps(name)),
|
||||
))
|
||||
)
|
30
tests/typ/visualize/gradient-relative.typ
Normal file
@ -0,0 +1,30 @@
|
||||
// Test whether `relative: "parent"` works correctly.
|
||||
|
||||
|
||||
---
|
||||
// The image should look as if there is a single gradient that is being used for
|
||||
// both the page and the rectangles.
|
||||
#let grad = gradient.linear(red, blue, green, purple, relative: "parent");
|
||||
#let my-rect = rect(width: 50%, height: 50%, fill: grad)
|
||||
#set page(
|
||||
height: 200pt,
|
||||
width: 200pt,
|
||||
fill: grad,
|
||||
background: place(top + left, my-rect),
|
||||
)
|
||||
#place(top + right, my-rect)
|
||||
#place(bottom + center, rotate(45deg, my-rect))
|
||||
|
||||
---
|
||||
// The image should look as if there are multiple gradients, one for each
|
||||
// rectangle.
|
||||
#let grad = gradient.linear(red, blue, green, purple, relative: "self");
|
||||
#let my-rect = rect(width: 50%, height: 50%, fill: grad)
|
||||
#set page(
|
||||
height: 200pt,
|
||||
width: 200pt,
|
||||
fill: grad,
|
||||
background: place(top + left, my-rect),
|
||||
)
|
||||
#place(top + right, my-rect)
|
||||
#place(bottom + center, rotate(45deg, my-rect))
|
36
tests/typ/visualize/gradient-repeat.typ
Normal file
@ -0,0 +1,36 @@
|
||||
// Test repeated gradients.
|
||||
|
||||
---
|
||||
#rect(
|
||||
height: 40pt,
|
||||
width: 100%,
|
||||
fill: gradient.linear(..color.map.inferno).repeat(2, mirror: true)
|
||||
)
|
||||
|
||||
---
|
||||
#rect(
|
||||
height: 40pt,
|
||||
width: 100%,
|
||||
fill: gradient.linear(..color.map.rainbow).repeat(2, mirror: true),
|
||||
)
|
||||
|
||||
---
|
||||
#rect(
|
||||
height: 40pt,
|
||||
width: 100%,
|
||||
fill: gradient.linear(..color.map.rainbow).repeat(5, mirror: true)
|
||||
)
|
||||
|
||||
---
|
||||
#rect(
|
||||
height: 40pt,
|
||||
width: 100%,
|
||||
fill: gradient.linear(..color.map.rainbow).sharp(10).repeat(5, mirror: false)
|
||||
)
|
||||
|
||||
---
|
||||
#rect(
|
||||
height: 40pt,
|
||||
width: 100%,
|
||||
fill: gradient.linear(..color.map.rainbow).sharp(10).repeat(5, mirror: true)
|
||||
)
|
4
tests/typ/visualize/gradient-sharp.typ
Normal file
@ -0,0 +1,4 @@
|
||||
// Test sharp gradients.
|
||||
|
||||
---
|
||||
#square(size: 100pt, fill: gradient.linear(..color.map.rainbow).sharp(10))
|
12
tests/typ/visualize/gradient-stroke.typ
Normal file
@ -0,0 +1,12 @@
|
||||
// Test gradients on strokes.
|
||||
|
||||
---
|
||||
#set page(width: 100pt, height: 100pt)
|
||||
#align(center + horizon, square(size: 50pt, fill: black, stroke: 5pt + gradient.linear(red, blue)))
|
||||
|
||||
---
|
||||
// Test gradient on lines
|
||||
#set page(width: 100pt, height: 100pt)
|
||||
#line(length: 100%, stroke: 1pt + gradient.linear(red, blue))
|
||||
#line(length: 100%, angle: 10deg, stroke: 1pt + gradient.linear(red, blue))
|
||||
#line(length: 100%, angle: 10deg, stroke: 1pt + gradient.linear(red, blue, relative: "parent"))
|
7
tests/typ/visualize/gradient-text.typ
Normal file
@ -0,0 +1,7 @@
|
||||
// Test that gradient fills on text don't work (for now).
|
||||
// Ref: false
|
||||
|
||||
---
|
||||
// Hint: 17-43 gradients on text will be supported soon
|
||||
// Error: 17-43 text fill must be a solid color
|
||||
#set text(fill: gradient.linear(red, blue))
|
12
tests/typ/visualize/gradient-transform.typ
Normal file
@ -0,0 +1,12 @@
|
||||
// Test whether gradients work well when they are contained within a transform.
|
||||
|
||||
---
|
||||
#let grad = gradient.linear(red, blue, green, purple, relative: "parent");
|
||||
#let my-rect = rect(width: 50pt, height: 50pt, fill: grad)
|
||||
#set page(
|
||||
height: 200pt,
|
||||
width: 200pt,
|
||||
)
|
||||
#place(top + right, scale(x: 200%, y: 130%, my-rect))
|
||||
#place(bottom + center, rotate(45deg, my-rect))
|
||||
#place(horizon + center, scale(x: 200%, y: 130%, rotate(45deg, my-rect)))
|