///|
/// SVG Scene Graph Management
/// Optimized for games and animations with dirty tracking and transform caching
///|
/// Scene represents a renderable SVG scene with optimization support
pub(all) struct Scene {
root : SVGNode
mut dirty : Bool
}
///|
/// Create a new scene from a root SVGNode
pub fn Scene::new(root : SVGNode) -> Scene {
{ root, dirty: true }
}
///|
/// Animation manager for coordinating multiple tweens
pub(all) struct AnimationManager {
tweens : Array[Tween]
}
///|
pub fn AnimationManager::new() -> AnimationManager {
{ tweens: [] }
}
///|
/// Add a new tween animation
pub fn AnimationManager::add(self : AnimationManager, tween : Tween) -> Unit {
self.tweens.push(tween)
}
///|
/// Create and add a simple translate animation
pub fn AnimationManager::animate_translate(
self : AnimationManager,
target_id : String,
x : Double,
y : Double,
duration : Double,
easing : Easing,
) -> Unit {
self.add(Tween::new(target_id, Translate(x, y), duration, easing))
}
///|
/// Create and add a simple opacity animation
pub fn AnimationManager::animate_opacity(
self : AnimationManager,
target_id : String,
opacity : Double,
duration : Double,
easing : Easing,
) -> Unit {
self.add(Tween::new(target_id, Opacity(opacity), duration, easing))
}
///|
/// Create and add a simple scale animation
pub fn AnimationManager::animate_scale(
self : AnimationManager,
target_id : String,
scale : Double,
duration : Double,
easing : Easing,
) -> Unit {
self.add(Tween::new(target_id, ScaleUniform(scale), duration, easing))
}
///|
/// Update all animations with delta time
/// Returns true if any animations are still running
pub fn AnimationManager::update(
self : AnimationManager,
dt : Double,
scene : Scene,
) -> Bool {
let mut any_running = false
// Update each tween
for tween in self.tweens {
if !tween.is_complete() {
match scene.find_node(tween.target_id) {
Some(node) => if tween.update(dt, node) { any_running = true }
None => () // Node not found, skip
}
}
}
// Mark scene dirty if any animations ran
if any_running {
scene.mark_dirty()
}
any_running
}
///|
/// Remove completed animations
pub fn AnimationManager::cleanup(self : AnimationManager) -> Unit {
// Remove completed tweens (iterate backwards to avoid index issues)
let mut i = self.tweens.length() - 1
while i >= 0 {
if self.tweens[i].is_complete() {
let _ = self.tweens.remove(i)
}
i = i - 1
}
}
///|
/// Check if any animations are running
pub fn AnimationManager::is_animating(self : AnimationManager) -> Bool {
for tween in self.tweens {
if !tween.is_complete() {
return true
}
}
false
}
///|
/// Clear all animations
pub fn AnimationManager::clear(self : AnimationManager) -> Unit {
self.tweens.clear()
}
///|
/// Create an empty scene with a group root
pub fn Scene::empty() -> Scene {
Scene::new(SVGNode::new(Group))
}
///|
/// Mark the scene as dirty (needs re-render)
pub fn Scene::mark_dirty(self : Scene) -> Unit {
self.dirty = true
}
///|
/// Check if scene needs re-render
pub fn Scene::is_dirty(self : Scene) -> Bool {
self.dirty
}
///|
/// Clear the dirty flag
pub fn Scene::clear_dirty(self : Scene) -> Unit {
self.dirty = false
}
///|
/// Get the root node
pub fn Scene::get_root(self : Scene) -> SVGNode {
self.root
}
///|
/// Update a node by ID (returns true if found and updated)
pub fn Scene::update_node(
self : Scene,
id : String,
updater : (SVGNode) -> SVGNode,
) -> Bool {
let found = update_node_recursive(self.root, id, updater)
if found {
self.mark_dirty()
}
found
}
///|
fn update_node_recursive(
node : SVGNode,
id : String,
updater : (SVGNode) -> SVGNode,
) -> Bool {
if node.id == id {
let updated = updater(node)
// Copy updated fields to node (mutation)
node.shape = updated.shape
node.transform = updated.transform
node.fill = updated.fill
node.fill_rule = updated.fill_rule
node.fill_opacity = updated.fill_opacity
node.stroke = updated.stroke
node.stroke_opacity = updated.stroke_opacity
node.opacity = updated.opacity
return true
}
for child in node.children {
if update_node_recursive(child, id, updater) {
return true
}
}
false
}
///|
/// Find a node by ID
pub fn Scene::find_node(self : Scene, id : String) -> SVGNode? {
find_node_recursive(self.root, id)
}
///|
fn find_node_recursive(node : SVGNode, id : String) -> SVGNode? {
if node.id == id {
return Some(node)
}
for child in node.children {
match find_node_recursive(child, id) {
Some(found) => return Some(found)
None => continue
}
}
None
}
///|
/// Add a child to a node by parent ID
pub fn Scene::add_child(
self : Scene,
parent_id : String,
child : SVGNode,
) -> Bool {
let found = add_child_recursive(self.root, parent_id, child)
if found {
self.mark_dirty()
}
found
}
///|
fn add_child_recursive(
node : SVGNode,
parent_id : String,
child : SVGNode,
) -> Bool {
if node.id == parent_id {
node.children.push(child)
return true
}
for c in node.children {
if add_child_recursive(c, parent_id, child) {
return true
}
}
false
}
///|
/// Remove a node by ID
pub fn Scene::remove_node(self : Scene, id : String) -> Bool {
let found = remove_node_recursive(self.root, id)
if found {
self.mark_dirty()
}
found
}
///|
fn remove_node_recursive(node : SVGNode, id : String) -> Bool {
let mut found_idx : Int? = None
for i, child in node.children {
if child.id == id {
found_idx = Some(i)
break
}
}
match found_idx {
Some(idx) => {
let _ = node.children.remove(idx)
true
}
None => {
for child in node.children {
if remove_node_recursive(child, id) {
return true
}
}
false
}
}
}
///|
/// Compute the bounding box of the entire scene
pub fn Scene::get_bounds(self : Scene) -> BoundingBox {
compute_bounds(self.root, Transform::identity())
}
///|
/// Compute the dirty region (union of all dirty node bounds)
pub fn Scene::get_dirty_region(self : Scene) -> BoundingBox {
compute_dirty_region(self.root, Transform::identity())
}
///|
fn compute_dirty_region(
node : SVGNode,
parent_transform : Transform,
) -> BoundingBox {
let transform = parent_transform.multiply(node.transform)
let mut dirty_bbox = BoundingBox::empty()
// If this node is dirty, include its current and previous bounds
if node.node_dirty {
let current_bbox = get_shape_bounds(node.shape)
if !current_bbox.is_empty() {
let transformed_bbox = transform.apply_bbox(current_bbox)
dirty_bbox = dirty_bbox.union(transformed_bbox)
}
// Include previous bounds if available
match node.prev_bounds {
Some(prev) => dirty_bbox = dirty_bbox.union(prev)
None => ()
}
}
// Recurse to children
for child in node.children {
let child_dirty = compute_dirty_region(child, transform)
dirty_bbox = dirty_bbox.union(child_dirty)
}
dirty_bbox
}
///|
/// Render only nodes that intersect with the dirty region
pub fn Scene::render_dirty(self : Scene, ctx : RenderContext) -> BoundingBox {
let dirty_region = self.get_dirty_region()
if dirty_region.is_empty() {
// Nothing dirty
return dirty_region
}
// Create a clipped render context
let clip = ClipRect::new(
dirty_region.min_x.to_int(),
dirty_region.min_y.to_int(),
(dirty_region.max_x - dirty_region.min_x).to_int(),
(dirty_region.max_y - dirty_region.min_y).to_int(),
)
let clipped_ctx = RenderContext::with_clip(
ctx.setter,
ctx.width,
ctx.height,
clip,
)
// Render nodes, update bounds, and clear dirty flags
render_and_update_dirty(self.root, Transform::identity(), clipped_ctx)
self.clear_dirty()
dirty_region
}
///|
fn render_and_update_dirty(
node : SVGNode,
parent_transform : Transform,
ctx : RenderContext,
) -> Unit {
let transform = parent_transform.multiply(node.transform)
// Update previous bounds and clear dirty
let current_bbox = get_shape_bounds(node.shape)
if !current_bbox.is_empty() {
node.prev_bounds = Some(transform.apply_bbox(current_bbox))
}
node.node_dirty = false
// Render the node (existing render_node logic will handle clipping)
render_node(node, parent_transform, ctx)
// Note: render_node already recurses into children
}
///|
/// Mark a node and its ancestors as dirty
pub fn Scene::mark_node_dirty(self : Scene, id : String) -> Bool {
match self.find_node(id) {
Some(node) => {
node.mark_dirty()
self.mark_dirty()
true
}
None => false
}
}
///|
/// Clear all dirty flags in the scene
pub fn Scene::clear_all_dirty(self : Scene) -> Unit {
clear_dirty_recursive(self.root)
self.clear_dirty()
}
///|
fn clear_dirty_recursive(node : SVGNode) -> Unit {
node.node_dirty = false
for child in node.children {
clear_dirty_recursive(child)
}
}
///|
/// Set z_index of a node by ID
pub fn Scene::set_z_index(self : Scene, id : String, z_index : Int) -> Bool {
match self.find_node(id) {
Some(node) => {
node.z_index = z_index
self.mark_dirty()
true
}
None => false
}
}
///|
/// Bring a node to front (set z_index higher than all siblings)
pub fn Scene::bring_to_front(self : Scene, id : String) -> Bool {
match find_parent_and_node(self.root, id) {
Some((parent, node)) => {
let max_z = get_max_z_index(parent.children)
node.z_index = max_z + 1
self.mark_dirty()
true
}
None => false
}
}
///|
/// Send a node to back (set z_index lower than all siblings)
pub fn Scene::send_to_back(self : Scene, id : String) -> Bool {
match find_parent_and_node(self.root, id) {
Some((parent, node)) => {
let min_z = get_min_z_index(parent.children)
node.z_index = min_z - 1
self.mark_dirty()
true
}
None => false
}
}
///|
fn find_parent_and_node(node : SVGNode, id : String) -> (SVGNode, SVGNode)? {
for child in node.children {
if child.id == id {
return Some((node, child))
}
match find_parent_and_node(child, id) {
Some(result) => return Some(result)
None => continue
}
}
None
}
///|
fn get_max_z_index(nodes : Array[SVGNode]) -> Int {
if nodes.is_empty() {
return 0
}
let mut max_z = nodes[0].z_index
for i = 1; i < nodes.length(); i = i + 1 {
if nodes[i].z_index > max_z {
max_z = nodes[i].z_index
}
}
max_z
}
///|
fn get_min_z_index(nodes : Array[SVGNode]) -> Int {
if nodes.is_empty() {
return 0
}
let mut min_z = nodes[0].z_index
for i = 1; i < nodes.length(); i = i + 1 {
if nodes[i].z_index < min_z {
min_z = nodes[i].z_index
}
}
min_z
}
///|
fn compute_bounds(node : SVGNode, parent_transform : Transform) -> BoundingBox {
let transform = parent_transform.multiply(node.transform)
let mut bbox = get_shape_bounds(node.shape)
if !bbox.is_empty() {
bbox = transform.apply_bbox(bbox)
}
for child in node.children {
let child_bbox = compute_bounds(child, transform)
bbox = bbox.union(child_bbox)
}
bbox
}
///|
fn get_shape_bounds(shape : Shape) -> BoundingBox {
match shape {
Rect(x~, y~, width~, height~, ..) =>
BoundingBox::from_rect(x, y, width, height)
Circle(cx~, cy~, r~) =>
BoundingBox::from_rect(cx - r, cy - r, r * 2.0, r * 2.0)
Ellipse(cx~, cy~, rx~, ry~) =>
BoundingBox::from_rect(cx - rx, cy - ry, rx * 2.0, ry * 2.0)
Line(x1~, y1~, x2~, y2~) => {
let mut bbox = BoundingBox::empty()
bbox = bbox.expand_by_point(x1, y1)
bbox = bbox.expand_by_point(x2, y2)
bbox
}
Polyline(points~) | Polygon(points~) => {
let mut bbox = BoundingBox::empty()
for p in points {
bbox = bbox.expand_by_point(p.0, p.1)
}
bbox
}
Path(commands~) => path_bbox(commands)
Text(x~, y~, text~, font_size~) => {
// Approximate text bounds using character count
// Each character is approximately 0.6 * font_size wide
let char_width = font_size * 0.6
let width = char_width * text.length().to_double()
let height = font_size
// y is baseline, so text extends upward
BoundingBox::from_rect(x, y - height, width, height)
}
Group => BoundingBox::empty()
}
}
///|
/// Render context for drawing
pub(all) struct RenderContext {
setter : PixelSetter
width : Int
height : Int
flatness : Double // For path flattening
clip : ClipRect? // Optional clipping rectangle for culling
}
///|
/// Render the scene to a pixel setter
pub fn Scene::render(self : Scene, ctx : RenderContext) -> Unit {
render_node(self.root, Transform::identity(), ctx)
self.clear_dirty()
}
///|
/// Render the scene with a camera transform
pub fn Scene::render_with_camera(
self : Scene,
ctx : RenderContext,
camera : Camera,
) -> Unit {
let camera_transform = camera.get_transform()
render_node(self.root, camera_transform, ctx)
self.clear_dirty()
}
///|
/// Render the scene with viewBox coordinate mapping
pub fn Scene::render_with_viewbox(
self : Scene,
ctx : RenderContext,
viewbox : ViewBox,
preserve_aspect_ratio : PreserveAspectRatio,
) -> Unit {
let viewbox_transform = viewbox.get_transform(
ctx.width.to_double(),
ctx.height.to_double(),
preserve_aspect_ratio,
)
render_node(self.root, viewbox_transform, ctx)
self.clear_dirty()
}
///|
/// Render the scene with both viewBox and camera
pub fn Scene::render_with_viewbox_and_camera(
self : Scene,
ctx : RenderContext,
viewbox : ViewBox,
preserve_aspect_ratio : PreserveAspectRatio,
camera : Camera,
) -> Unit {
let viewbox_transform = viewbox.get_transform(
ctx.width.to_double(),
ctx.height.to_double(),
preserve_aspect_ratio,
)
let camera_transform = camera.get_transform()
// Apply viewBox first, then camera
let combined = viewbox_transform.multiply(camera_transform)
render_node(self.root, combined, ctx)
self.clear_dirty()
}
///|
/// Check if a bounding box is visible within the render context
fn is_visible(bbox : BoundingBox, ctx : RenderContext) -> Bool {
match ctx.clip {
Some(clip) => bbox.intersects(clip.to_bbox())
None => {
// Default: check against render context bounds
let ctx_bbox = BoundingBox::from_rect(
0.0,
0.0,
ctx.width.to_double(),
ctx.height.to_double(),
)
bbox.intersects(ctx_bbox)
}
}
}
///|
fn render_node(
node : SVGNode,
parent_transform : Transform,
ctx : RenderContext,
) -> Unit {
// Skip invisible nodes
if node.opacity <= 0.0 {
return
}
let transform = parent_transform.multiply(node.transform)
// Early culling: check if node's bounding box is visible
let shape_bbox = get_shape_bounds(node.shape)
if !shape_bbox.is_empty() {
let transformed_bbox = transform.apply_bbox(shape_bbox)
if !is_visible(transformed_bbox, ctx) {
// Node is outside visible area, but still need to check children
// (they might have different transforms that make them visible)
for child in node.children {
render_node(child, transform, ctx)
}
return
}
}
// Render shape
match node.shape {
Rect(x~, y~, width~, height~, rx~, ry~) =>
render_rect(x, y, width, height, rx, ry, node, transform, ctx)
Circle(cx~, cy~, r~) => render_circle(cx, cy, r, node, transform, ctx)
Ellipse(cx~, cy~, rx~, ry~) =>
render_ellipse(cx, cy, rx, ry, node, transform, ctx)
Line(x1~, y1~, x2~, y2~) =>
render_line(x1, y1, x2, y2, node, transform, ctx)
Polyline(points~) => render_polyline(points, node, transform, ctx)
Polygon(points~) => render_polygon(points, node, transform, ctx)
Path(commands~) => render_path(commands, node, transform, ctx)
Text(x~, y~, text~, font_size~) =>
render_text(x, y, text, font_size, node, transform, ctx)
Group => ()
}
// Render children sorted by z_index (lower z_index first = back to front)
let sorted_children = sort_by_z_index(node.children)
for child in sorted_children {
render_node(child, transform, ctx)
}
}
///|
/// Sort nodes by z_index (ascending order: lower z_index drawn first)
fn sort_by_z_index(nodes : Array[SVGNode]) -> Array[SVGNode] {
if nodes.length() <= 1 {
return nodes
}
// Simple insertion sort (good for small arrays, stable)
let result = nodes.copy()
for i = 1; i < result.length(); i = i + 1 {
let current = result[i]
let mut j = i - 1
while j >= 0 && result[j].z_index > current.z_index {
result[j + 1] = result[j]
j = j - 1
}
result[j + 1] = current
}
result
}
///|
fn render_rect(
x : Double,
y : Double,
width : Double,
height : Double,
rx : Double,
ry : Double,
node : SVGNode,
transform : Transform,
ctx : RenderContext,
) -> Unit {
// Transform corners
let (x0, y0) = transform.apply(x, y)
let (x1, y1) = transform.apply(x + width, y + height)
let ix = x0.to_int()
let iy = y0.to_int()
let iw = (x1 - x0).to_int()
let ih = (y1 - y0).to_int()
let irx = rx.to_int()
let iry = ry.to_int()
// Fill
match node.fill {
SolidColor(color) =>
if node.fill_opacity > 0.0 {
let fill_color = apply_opacity(color, node.fill_opacity * node.opacity)
if irx > 0 || iry > 0 {
// Rounded rect fill approximated as regular rect (simplified)
raster_rect_fill(ix, iy, iw, ih, fill_color, ctx.setter)
} else {
raster_rect_fill(ix, iy, iw, ih, fill_color, ctx.setter)
}
}
LinearGrad(grad) =>
if node.fill_opacity > 0.0 {
raster_rect_gradient(
ix,
iy,
iw,
ih,
grad,
node.fill_opacity * node.opacity,
ctx.setter,
)
}
RadialGrad(grad) =>
if node.fill_opacity > 0.0 {
raster_rect_radial_gradient(
ix,
iy,
iw,
ih,
grad,
node.fill_opacity * node.opacity,
ctx.setter,
)
}
None => ()
}
// Stroke
match node.stroke.paint {
SolidColor(color) =>
if node.stroke_opacity > 0.0 && node.stroke.width > 0.0 {
let stroke_color = apply_opacity(
color,
node.stroke_opacity * node.opacity,
)
if irx > 0 || iry > 0 {
raster_rounded_rect_stroke(
ix,
iy,
iw,
ih,
irx,
iry,
stroke_color,
ctx.setter,
)
} else {
raster_rect_stroke(ix, iy, iw, ih, stroke_color, ctx.setter)
}
}
LinearGrad(_) => () // TODO: gradient stroke not yet supported
RadialGrad(_) => () // TODO: gradient stroke not yet supported
None => ()
}
}
///|
fn render_circle(
cx : Double,
cy : Double,
r : Double,
node : SVGNode,
transform : Transform,
ctx : RenderContext,
) -> Unit {
let (tx, ty) = transform.apply(cx, cy)
let (sx, sy) = transform.get_scale()
let tr = (r * (sx + sy) / 2.0).to_int()
let icx = tx.to_int()
let icy = ty.to_int()
// Fill
match node.fill {
SolidColor(color) =>
if node.fill_opacity > 0.0 {
let fill_color = apply_opacity(color, node.fill_opacity * node.opacity)
raster_circle_fill(icx, icy, tr, fill_color, ctx.setter)
}
LinearGrad(_) => () // TODO: gradient fill for circles not yet supported
RadialGrad(grad) =>
if node.fill_opacity > 0.0 {
raster_circle_radial_gradient(
icx,
icy,
tr,
grad,
node.fill_opacity * node.opacity,
ctx.setter,
)
}
None => ()
}
// Stroke
match node.stroke.paint {
SolidColor(color) =>
if node.stroke_opacity > 0.0 && node.stroke.width > 0.0 {
let stroke_color = apply_opacity(
color,
node.stroke_opacity * node.opacity,
)
raster_circle_stroke(icx, icy, tr, stroke_color, ctx.setter)
}
LinearGrad(_) => () // TODO: gradient stroke not yet supported
RadialGrad(_) => () // TODO: gradient stroke not yet supported
None => ()
}
}
///|
fn render_ellipse(
cx : Double,
cy : Double,
rx : Double,
ry : Double,
node : SVGNode,
transform : Transform,
ctx : RenderContext,
) -> Unit {
let (tx, ty) = transform.apply(cx, cy)
let (sx, sy) = transform.get_scale()
let trx = (rx * sx).to_int()
let try_ = (ry * sy).to_int()
let icx = tx.to_int()
let icy = ty.to_int()
// Fill
match node.fill {
SolidColor(color) =>
if node.fill_opacity > 0.0 {
let fill_color = apply_opacity(color, node.fill_opacity * node.opacity)
raster_ellipse_fill(icx, icy, trx, try_, fill_color, ctx.setter)
}
LinearGrad(_) => () // TODO: gradient fill for ellipses not yet supported
RadialGrad(grad) =>
if node.fill_opacity > 0.0 {
raster_ellipse_radial_gradient(
icx,
icy,
trx,
try_,
grad,
node.fill_opacity * node.opacity,
ctx.setter,
)
}
None => ()
}
// Stroke
match node.stroke.paint {
SolidColor(color) =>
if node.stroke_opacity > 0.0 && node.stroke.width > 0.0 {
let stroke_color = apply_opacity(
color,
node.stroke_opacity * node.opacity,
)
raster_ellipse_stroke(icx, icy, trx, try_, stroke_color, ctx.setter)
}
LinearGrad(_) => () // TODO: gradient stroke not yet supported
RadialGrad(_) => () // TODO: gradient stroke not yet supported
None => ()
}
}
///|
fn render_line(
x1 : Double,
y1 : Double,
x2 : Double,
y2 : Double,
node : SVGNode,
transform : Transform,
ctx : RenderContext,
) -> Unit {
let (tx1, ty1) = transform.apply(x1, y1)
let (tx2, ty2) = transform.apply(x2, y2)
match node.stroke.paint {
SolidColor(color) =>
if node.stroke_opacity > 0.0 && node.stroke.width > 0.0 {
let stroke_color = apply_opacity(
color,
node.stroke_opacity * node.opacity,
)
// Check for dash pattern
match node.stroke.dasharray {
Some(dasharray) =>
raster_line_dashed(
tx1.to_int(),
ty1.to_int(),
tx2.to_int(),
ty2.to_int(),
stroke_color,
dasharray,
node.stroke.dashoffset,
ctx.setter,
)
None =>
if node.stroke.width <= 1.0 {
raster_line(
tx1.to_int(),
ty1.to_int(),
tx2.to_int(),
ty2.to_int(),
stroke_color,
ctx.setter,
)
} else {
raster_thick_line(
tx1.to_int(),
ty1.to_int(),
tx2.to_int(),
ty2.to_int(),
node.stroke.width.to_int(),
stroke_color,
ctx.setter,
)
}
}
}
LinearGrad(_) => () // TODO: gradient stroke not yet supported
RadialGrad(_) => () // TODO: gradient stroke not yet supported
None => ()
}
}
///|
fn render_polyline(
points : Array[(Double, Double)],
node : SVGNode,
transform : Transform,
ctx : RenderContext,
) -> Unit {
let transformed = points.map(fn(p) {
let (x, y) = transform.apply(p.0, p.1)
(x.to_int(), y.to_int())
})
match node.stroke.paint {
SolidColor(color) =>
if node.stroke_opacity > 0.0 && node.stroke.width > 0.0 {
let stroke_color = apply_opacity(
color,
node.stroke_opacity * node.opacity,
)
match node.stroke.dasharray {
Some(dasharray) =>
raster_polyline_dashed(
transformed,
stroke_color,
dasharray,
node.stroke.dashoffset,
ctx.setter,
)
None => raster_polyline(transformed, stroke_color, ctx.setter)
}
}
LinearGrad(_) => () // TODO: gradient stroke not yet supported
RadialGrad(_) => () // TODO: gradient stroke not yet supported
None => ()
}
}
///|
fn render_polygon(
points : Array[(Double, Double)],
node : SVGNode,
transform : Transform,
ctx : RenderContext,
) -> Unit {
let transformed = points.map(fn(p) {
let (x, y) = transform.apply(p.0, p.1)
(x.to_int(), y.to_int())
})
// Fill
match node.fill {
SolidColor(color) =>
if node.fill_opacity > 0.0 {
let fill_color = apply_opacity(color, node.fill_opacity * node.opacity)
raster_polygon_fill(transformed, fill_color, ctx.setter)
}
LinearGrad(_) => () // TODO: gradient fill for polygons not yet supported
RadialGrad(_) => () // TODO: gradient fill for polygons not yet supported
None => ()
}
// Stroke
match node.stroke.paint {
SolidColor(color) =>
if node.stroke_opacity > 0.0 && node.stroke.width > 0.0 {
let stroke_color = apply_opacity(
color,
node.stroke_opacity * node.opacity,
)
raster_polygon_stroke(transformed, stroke_color, ctx.setter)
}
LinearGrad(_) => () // TODO: gradient stroke not yet supported
RadialGrad(_) => () // TODO: gradient stroke not yet supported
None => ()
}
}
///|
fn render_path(
commands : Array[PathCommand],
node : SVGNode,
transform : Transform,
ctx : RenderContext,
) -> Unit {
// Convert to polylines with transform
let polylines = path_to_polylines(commands, ctx.flatness)
for polyline in polylines {
let transformed = polyline.map(fn(p) {
let (x, y) = transform.apply(p.0, p.1)
(x.to_int(), y.to_int())
})
// Fill
match node.fill {
SolidColor(color) =>
if node.fill_opacity > 0.0 && transformed.length() >= 3 {
let fill_color = apply_opacity(
color,
node.fill_opacity * node.opacity,
)
raster_polygon_fill(transformed, fill_color, ctx.setter)
}
LinearGrad(_) => () // TODO: gradient fill for paths not yet supported
RadialGrad(_) => () // TODO: gradient fill for paths not yet supported
None => ()
}
// Stroke
match node.stroke.paint {
SolidColor(color) =>
if node.stroke_opacity > 0.0 && node.stroke.width > 0.0 {
let stroke_color = apply_opacity(
color,
node.stroke_opacity * node.opacity,
)
raster_polyline(transformed, stroke_color, ctx.setter)
}
LinearGrad(_) => () // TODO: gradient stroke not yet supported
RadialGrad(_) => () // TODO: gradient stroke not yet supported
None => ()
}
}
}
///|
fn render_text(
x : Double,
y : Double,
text : String,
font_size : Double,
node : SVGNode,
transform : Transform,
ctx : RenderContext,
) -> Unit {
let (tx, ty) = transform.apply(x, y)
let (sx, _) = transform.get_scale()
let scaled_size = (font_size * sx).to_int()
// Fill color for text
match node.fill {
SolidColor(color) =>
if node.fill_opacity > 0.0 {
let fill_color = apply_opacity(color, node.fill_opacity * node.opacity)
raster_text(
tx.to_int(),
ty.to_int(),
text,
scaled_size,
fill_color,
ctx.setter,
)
}
LinearGrad(_) => () // TODO: gradient fill for text not yet supported
RadialGrad(_) => () // TODO: gradient fill for text not yet supported
None => ()
}
}
///|
/// Apply opacity to a color
fn apply_opacity(color : Color, opacity : Double) -> Color {
if opacity >= 1.0 {
color
} else if opacity <= 0.0 {
Color::transparent()
} else {
{
r: color.r,
g: color.g,
b: color.b,
a: (color.a.to_double() * opacity).to_int(),
}
}
}
///|
/// Helper: Create a rectangle node
pub fn rect(
id : String,
x : Double,
y : Double,
width : Double,
height : Double,
) -> SVGNode {
let node = SVGNode::new(Rect(x~, y~, width~, height~, rx=0.0, ry=0.0))
node.id = id
node
}
///|
/// Helper: Create a circle node
pub fn circle(id : String, cx : Double, cy : Double, r : Double) -> SVGNode {
let node = SVGNode::new(Circle(cx~, cy~, r~))
node.id = id
node
}
///|
/// Helper: Create a line node
pub fn line(
id : String,
x1 : Double,
y1 : Double,
x2 : Double,
y2 : Double,
) -> SVGNode {
let node = SVGNode::new(Line(x1~, y1~, x2~, y2~))
node.id = id
node.fill = None // Lines have no fill by default
node
}
///|
/// Helper: Create a path node from path data string
pub fn path(id : String, d : String) -> SVGNode {
let commands = parse_path(d)
let node = SVGNode::new(Path(commands~))
node.id = id
node
}
///|
/// Helper: Create a text node
pub fn text(
id : String,
x : Double,
y : Double,
content : String,
font_size : Double,
) -> SVGNode {
let node = SVGNode::new(Text(x~, y~, text=content, font_size~))
node.id = id
node
}
///|
/// Helper: Create a group node
pub fn group(id : String, children : Array[SVGNode]) -> SVGNode {
let node = SVGNode::new(Group)
node.id = id
for child in children {
node.children.push(child)
}
node
}
///|
/// Create a RenderContext with default settings
pub fn RenderContext::new(
setter : PixelSetter,
width : Int,
height : Int,
) -> RenderContext {
{ setter, width, height, flatness: 0.5, clip: None }
}
///|
/// Create a RenderContext with clipping
pub fn RenderContext::with_clip(
setter : PixelSetter,
width : Int,
height : Int,
clip : ClipRect,
) -> RenderContext {
{ setter, width, height, flatness: 0.5, clip: Some(clip) }
}
///|
/// Create a RenderContext for a camera
pub fn RenderContext::for_camera(
setter : PixelSetter,
camera : Camera,
) -> RenderContext {
let clip = ClipRect::from_size(camera.viewport_width, camera.viewport_height)
{
setter,
width: camera.viewport_width,
height: camera.viewport_height,
flatness: 0.5,
clip: Some(clip),
}
}