///|
/// SVG Layout Engine - Standalone Types
/// Independent of HTML/CSS layout system
///|
/// RGB Color (0-255 range)
pub(all) struct Color {
r : Int
g : Int
b : Int
a : Int // Alpha (0-255, 255 = opaque)
} derive(Debug)
///|
pub fn Color::rgb(r : Int, g : Int, b : Int) -> Color {
{ r, g, b, a: 255 }
}
///|
pub fn Color::rgba(r : Int, g : Int, b : Int, a : Int) -> Color {
{ r, g, b, a }
}
///|
pub fn Color::transparent() -> Color {
{ r: 0, g: 0, b: 0, a: 0 }
}
///|
pub fn Color::black() -> Color {
{ r: 0, g: 0, b: 0, a: 255 }
}
///|
pub fn Color::white() -> Color {
{ r: 255, g: 255, b: 255, a: 255 }
}
///|
pub fn Color::is_transparent(self : Color) -> Bool {
self.a == 0
}
///|
fn lerp(a : Double, b : Double, t : Double) -> Double {
a + (b - a) * t
}
///|
fn blend_colors(dst : Color, src : Color) -> Color {
PremulColor16::from_color(src)
.source_over(PremulColor16::from_color(dst))
.to_color()
}
///|
/// 2D affine transformation matrix
/// | a c e |
/// | b d f |
/// | 0 0 1 |
pub(all) struct Transform {
a : Double
b : Double
c : Double
d : Double
e : Double
f : Double
}
///|
/// ViewBox specification for SVG coordinate system mapping
pub(all) struct ViewBox {
min_x : Double
min_y : Double
width : Double
height : Double
}
///|
/// preserveAspectRatio alignment values
pub(all) enum Align {
None // No forced uniform scaling
XMinYMin
XMidYMin
XMaxYMin
XMinYMid
XMidYMid // Default
XMaxYMid
XMinYMax
XMidYMax
XMaxYMax
} derive(Debug, Eq)
///|
/// preserveAspectRatio meet/slice
pub(all) enum MeetOrSlice {
Meet // Scale to fit entirely (default)
Slice // Scale to cover entirely
} derive(Debug, Eq)
///|
/// Complete preserveAspectRatio setting
pub(all) struct PreserveAspectRatio {
align : Align
meet_or_slice : MeetOrSlice
}
///|
pub fn PreserveAspectRatio::default() -> PreserveAspectRatio {
{ align: XMidYMid, meet_or_slice: Meet }
}
///|
/// Calculate the transform matrix to map viewBox coordinates to viewport
/// viewport_width/height: the actual pixel dimensions of the SVG element
pub fn ViewBox::get_transform(
self : ViewBox,
viewport_width : Double,
viewport_height : Double,
preserve_aspect_ratio : PreserveAspectRatio,
) -> Transform {
// Handle zero-size viewBox
if self.width <= 0.0 || self.height <= 0.0 {
return Transform::identity()
}
// Calculate scale factors
let scale_x = viewport_width / self.width
let scale_y = viewport_height / self.height
// Determine actual scale based on preserveAspectRatio
let (sx, sy, tx, ty) = match preserve_aspect_ratio.align {
None =>
// No uniform scaling - stretch to fit
(scale_x, scale_y, -self.min_x * scale_x, -self.min_y * scale_y)
_ => {
// Uniform scaling
let scale = match preserve_aspect_ratio.meet_or_slice {
Meet => if scale_x < scale_y { scale_x } else { scale_y } // Fit entirely
Slice => if scale_x > scale_y { scale_x } else { scale_y } // Cover entirely
}
// Calculate translation based on alignment
let (align_x, align_y) = get_alignment_factors(
preserve_aspect_ratio.align,
)
// Extra space after scaling
let extra_x = viewport_width - self.width * scale
let extra_y = viewport_height - self.height * scale
let translate_x = -self.min_x * scale + extra_x * align_x
let translate_y = -self.min_y * scale + extra_y * align_y
(scale, scale, translate_x, translate_y)
}
}
// Combine: first translate by viewBox min, then scale
{ a: sx, b: 0.0, c: 0.0, d: sy, e: tx, f: ty }
}
///|
/// Get alignment factors (0.0 = min, 0.5 = mid, 1.0 = max)
fn get_alignment_factors(align : Align) -> (Double, Double) {
match align {
None => (0.0, 0.0)
XMinYMin => (0.0, 0.0)
XMidYMin => (0.5, 0.0)
XMaxYMin => (1.0, 0.0)
XMinYMid => (0.0, 0.5)
XMidYMid => (0.5, 0.5)
XMaxYMid => (1.0, 0.5)
XMinYMax => (0.0, 1.0)
XMidYMax => (0.5, 1.0)
XMaxYMax => (1.0, 1.0)
}
}
///|
/// SVG path commands (full SVG 1.1 spec)
pub(all) enum PathCommand {
// Absolute commands
MoveTo(Double, Double) // M x y
LineTo(Double, Double) // L x y
HorizontalLineTo(Double) // H x
VerticalLineTo(Double) // V y
CurveTo(Double, Double, Double, Double, Double, Double) // C x1 y1 x2 y2 x y
SmoothCurveTo(Double, Double, Double, Double) // S x2 y2 x y
QuadraticCurveTo(Double, Double, Double, Double) // Q x1 y1 x y
SmoothQuadraticCurveTo(Double, Double) // T x y
ArcTo(Double, Double, Double, Bool, Bool, Double, Double) // A rx ry rotation large-arc sweep x y
ClosePath // Z
// Relative commands
MoveToRel(Double, Double) // m dx dy
LineToRel(Double, Double) // l dx dy
HorizontalLineToRel(Double) // h dx
VerticalLineToRel(Double) // v dy
CurveToRel(Double, Double, Double, Double, Double, Double) // c dx1 dy1 dx2 dy2 dx dy
SmoothCurveToRel(Double, Double, Double, Double) // s dx2 dy2 dx dy
QuadraticCurveToRel(Double, Double, Double, Double) // q dx1 dy1 dx dy
SmoothQuadraticCurveToRel(Double, Double) // t dx dy
ArcToRel(Double, Double, Double, Bool, Bool, Double, Double) // a rx ry rotation large-arc sweep dx dy
} derive(Debug)
///|
/// SVG shape primitives
pub(all) enum Shape {
Rect(
x~ : Double,
y~ : Double,
width~ : Double,
height~ : Double,
rx~ : Double,
ry~ : Double
)
Circle(cx~ : Double, cy~ : Double, r~ : Double)
Ellipse(cx~ : Double, cy~ : Double, rx~ : Double, ry~ : Double)
Line(x1~ : Double, y1~ : Double, x2~ : Double, y2~ : Double)
Polyline(points~ : Array[(Double, Double)])
Polygon(points~ : Array[(Double, Double)])
Path(commands~ : Array[PathCommand])
Text(x~ : Double, y~ : Double, text~ : String, font_size~ : Double)
Image(
x~ : Double,
y~ : Double,
width~ : Double,
height~ : Double,
href~ : String
)
Group // Container for children
} derive(Debug)
///|
pub(all) enum ImageSampling {
Nearest
Bilinear
Bicubic
} derive(Debug, Eq)
///|
/// Gradient stop (position 0.0-1.0 and color)
pub(all) struct GradientStop {
offset : Double // 0.0 to 1.0
color : Color
}
///|
/// Linear gradient definition
pub(all) struct LinearGradient {
x1 : Double // Start point x (0.0-1.0 or absolute)
y1 : Double // Start point y
x2 : Double // End point x
y2 : Double // End point y
stops : Array[GradientStop]
spread_method : SpreadMethod
units : GradientUnits
transform : Transform
}
///|
pub fn LinearGradient::new(
x1 : Double,
y1 : Double,
x2 : Double,
y2 : Double,
stops : Array[GradientStop],
) -> LinearGradient {
{
x1,
y1,
x2,
y2,
stops,
spread_method: Pad,
units: ObjectBoundingBox,
transform: Transform::identity(),
}
}
///|
/// Interpolate color at position t (0.0-1.0)
fn LinearGradient::color_at(self : LinearGradient, t : Double) -> Color {
if self.stops.length() == 0 {
return Color::black()
}
if self.stops.length() == 1 {
return self.stops[0].color
}
// Apply spread method
let t = match self.spread_method {
Pad => if t < 0.0 { 0.0 } else if t > 1.0 { 1.0 } else { t }
Repeat => {
let t2 = t - t.floor()
if t2 < 0.0 {
t2 + 1.0
} else {
t2
}
}
Reflect => {
let t2 = t - t.floor()
let t2 = if t2 < 0.0 { t2 + 1.0 } else { t2 }
let cycle = t.floor().to_int() % 2
if cycle == 1 {
1.0 - t2
} else {
t2
}
}
}
interpolate_gradient_color(t, self.stops)
}
///|
/// How gradient extends beyond its bounds
pub(all) enum SpreadMethod {
Pad // Extend with end colors (default)
Repeat // Repeat pattern
Reflect // Mirror pattern
} derive(Debug, Eq)
///|
/// Gradient units coordinate space
pub(all) enum GradientUnits {
UserSpaceOnUse
ObjectBoundingBox
} derive(Debug, Eq)
///|
/// Paint style (fill or stroke)
pub(all) enum PaintFallback {
NoPaint
SolidColor(Color)
CurrentColor
}
///|
/// Paint style (fill or stroke)
pub(all) enum Paint {
None
SolidColor(Color)
LinearGrad(LinearGradient)
RadialGrad(RadialGradient)
CurrentColor
PaintServerRef(String, PaintFallback)
}
///|
/// Stroke properties
pub(all) struct StrokeStyle {
paint : Paint
width : Double
linecap : LineCap
linejoin : LineJoin
miterlimit : Double
dasharray : Array[Double]?
dashoffset : Double
non_scaling : Bool
}
///|
pub fn StrokeStyle::default() -> StrokeStyle {
{
paint: None,
width: 1.0,
linecap: Butt,
linejoin: Miter,
miterlimit: 4.0,
dasharray: None,
dashoffset: 0.0,
non_scaling: false,
}
}
///|
pub(all) enum LineCap {
Butt
Round
Square
} derive(Debug, Eq)
///|
pub(all) enum LineJoin {
Miter
Round
Bevel
} derive(Debug, Eq)
///|
/// Fill rule for paths and polygons
pub(all) enum FillRule {
NonZero // Default
EvenOdd
} derive(Debug, Eq)
///|
/// SVG paint-order components.
pub(all) enum PaintOrderItem {
Fill
Stroke
Markers
} derive(Debug, Eq)
///|
/// SVG paint-order specification.
pub(all) struct PaintOrder {
order : Array[PaintOrderItem]
} derive(Debug, Eq)
///|
pub fn PaintOrder::default() -> PaintOrder {
{ order: [Fill, Stroke, Markers] }
}
///|
/// SVG node (element in the scene graph)
pub(all) struct SVGNode {
mut id : String
mut shape : Shape
mut transform : Transform
mut view_box : ViewBox?
mut viewport_width : Double?
mut viewport_height : Double?
mut preserve_aspect_ratio : PreserveAspectRatio
priv mut preserve_aspect_ratio_is_set : Bool
mut image_sampling : ImageSampling
mut fill : Paint
priv mut fill_is_set : Bool
mut color : Color?
priv mut color_is_set : Bool
mut paint_order : PaintOrder
mut fill_rule : FillRule
mut clip_rule : FillRule
mut fill_opacity : Double
mut stroke : StrokeStyle
priv mut stroke_paint_is_set : Bool
priv mut stroke_width_is_set : Bool
mut stroke_opacity : Double
mut opacity : Double
mut blend_mode : BlendMode
mut isolation : Isolation
mut marker_start : String?
priv mut marker_start_is_set : Bool
mut marker_mid : String?
priv mut marker_mid_is_set : Bool
mut marker_end : String?
priv mut marker_end_is_set : Bool
priv filters : Array[Filter] // Filter effects to apply
priv mut filter_graph_id : String? // Reference to an SVG filter graph by ID
priv mut mask_id : String? // Reference to mask by ID
priv mut clip_path_id : String? // Reference to clip path by ID
mut clip_overflow : Bool
children : Array[SVGNode]
}
///|
pub fn SVGNode::new(shape : Shape) -> SVGNode {
{
id: "",
shape,
transform: Transform::identity(),
view_box: None,
viewport_width: None,
viewport_height: None,
preserve_aspect_ratio: PreserveAspectRatio::default(),
preserve_aspect_ratio_is_set: false,
image_sampling: Bilinear,
fill: SolidColor(Color::black()),
fill_is_set: false,
color: None,
color_is_set: false,
paint_order: PaintOrder::default(),
fill_rule: NonZero,
clip_rule: NonZero,
fill_opacity: 1.0,
stroke: StrokeStyle::default(),
stroke_paint_is_set: false,
stroke_width_is_set: false,
stroke_opacity: 1.0,
opacity: 1.0,
blend_mode: Normal,
isolation: Auto,
marker_start: None,
marker_start_is_set: false,
marker_mid: None,
marker_mid_is_set: false,
marker_end: None,
marker_end_is_set: false,
filters: [],
filter_graph_id: None,
mask_id: None,
clip_path_id: None,
clip_overflow: true,
children: [],
}
}
///|
/// Add a filter to the node
pub fn SVGNode::add_filter(self : SVGNode, filter : Filter) -> Unit {
self.filters.push(filter)
}
///|
/// Clear all filters from the node
pub fn SVGNode::clear_filters(self : SVGNode) -> Unit {
self.filters.clear()
}
///|
/// Set an SVG filter graph reference by ID.
pub fn SVGNode::set_filter_graph(self : SVGNode, filter_id : String) -> Unit {
self.filter_graph_id = Some(filter_id)
}
///|
pub fn SVGNode::clear_filter_graph(self : SVGNode) -> Unit {
self.filter_graph_id = None
}
///|
/// Set mask reference by ID
pub fn SVGNode::set_mask(self : SVGNode, mask_id : String) -> Unit {
self.mask_id = Some(mask_id)
}
///|
/// Clear mask reference
pub fn SVGNode::clear_mask(self : SVGNode) -> Unit {
self.mask_id = None
}
///|
/// Set clip path reference by ID
pub fn SVGNode::set_clip_path(self : SVGNode, clip_path_id : String) -> Unit {
self.clip_path_id = Some(clip_path_id)
}
///|
/// Clear clip path reference
pub fn SVGNode::clear_clip_path(self : SVGNode) -> Unit {
self.clip_path_id = None
}
///|
///|
/// Helper function for min of two doubles
fn min(a : Double, b : Double) -> Double {
if a < b {
a
} else {
b
}
}
///|
/// Helper function for max of two doubles
fn max(a : Double, b : Double) -> Double {
if a > b {
a
} else {
b
}
}
///|
/// Bounding box for a shape
pub(all) struct BoundingBox {
min_x : Double
min_y : Double
max_x : Double
max_y : Double
}
///|
pub fn BoundingBox::empty() -> BoundingBox {
{
min_x: @double.infinity,
min_y: @double.infinity,
max_x: @double.neg_infinity,
max_y: @double.neg_infinity,
}
}
///|
pub fn BoundingBox::from_rect(
x : Double,
y : Double,
w : Double,
h : Double,
) -> BoundingBox {
{ min_x: x, min_y: y, max_x: x + w, max_y: y + h }
}
///|
pub fn BoundingBox::width(self : BoundingBox) -> Double {
self.max_x - self.min_x
}
///|
pub fn BoundingBox::height(self : BoundingBox) -> Double {
self.max_y - self.min_y
}
///|
pub fn BoundingBox::is_empty(self : BoundingBox) -> Bool {
self.min_x > self.max_x || self.min_y > self.max_y
}
///|
pub fn BoundingBox::union(
self : BoundingBox,
other : BoundingBox,
) -> BoundingBox {
if self.is_empty() {
other
} else if other.is_empty() {
self
} else {
{
min_x: min(self.min_x, other.min_x),
min_y: min(self.min_y, other.min_y),
max_x: max(self.max_x, other.max_x),
max_y: max(self.max_y, other.max_y),
}
}
}
///|
pub fn BoundingBox::expand_by_point(
self : BoundingBox,
x : Double,
y : Double,
) -> BoundingBox {
{
min_x: min(self.min_x, x),
min_y: min(self.min_y, y),
max_x: max(self.max_x, x),
max_y: max(self.max_y, y),
}
}
///|
/// Check if two bounding boxes intersect
pub fn BoundingBox::intersects(self : BoundingBox, other : BoundingBox) -> Bool {
if self.is_empty() || other.is_empty() {
false
} else {
self.min_x <= other.max_x &&
self.max_x >= other.min_x &&
self.min_y <= other.max_y &&
self.max_y >= other.min_y
}
}
///|
/// Check if this bounding box contains a point
pub fn BoundingBox::contains_point(
self : BoundingBox,
x : Double,
y : Double,
) -> Bool {
x >= self.min_x && x <= self.max_x && y >= self.min_y && y <= self.max_y
}
///|
/// Clipping rectangle for camera/viewport
priv struct ClipRect {
x : Int
y : Int
width : Int
height : Int
}
///|
fn ClipRect::new(x : Int, y : Int, width : Int, height : Int) -> ClipRect {
{ x, y, width, height }
}
///|
/// Check if a point is inside the clip rect
fn ClipRect::contains(self : ClipRect, x : Int, y : Int) -> Bool {
x >= self.x &&
x < self.x + self.width &&
y >= self.y &&
y < self.y + self.height
}
///|
/// Convert to BoundingBox
fn ClipRect::to_bbox(self : ClipRect) -> BoundingBox {
{
min_x: self.x.to_double(),
min_y: self.y.to_double(),
max_x: (self.x + self.width).to_double(),
max_y: (self.y + self.height).to_double(),
}
}