///|
/// 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 not(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 not(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 {
Group |> SVGNode::new |> Scene::new
}
///|
/// 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.view_box = updated.view_box
node.preserve_aspect_ratio = updated.preserve_aspect_ratio
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 not(current_bbox.is_empty()) {
let transformed_bbox = transform.apply_bbox(current_bbox)
dirty_bbox = dirty_bbox.union(transformed_bbox)
}
// Include previous bounds if available
if node.prev_bounds is Some(prev) {
dirty_bbox = dirty_bbox.union(prev)
}
}
// 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,
)
let resources = RenderResources::empty()
let base_transform = match self.root.view_box {
Some(vb) =>
vb.get_transform(
ctx.width.to_double(),
ctx.height.to_double(),
self.root.preserve_aspect_ratio,
)
None => Transform::identity()
}
// Render nodes, update bounds, and clear dirty flags
render_and_update_dirty(self.root, base_transform, clipped_ctx, resources)
self.clear_dirty()
dirty_region
}
///|
fn render_and_update_dirty(
node : SVGNode,
parent_transform : Transform,
ctx : RenderContext,
resources : RenderResources,
) -> Unit {
let transform = parent_transform.multiply(node.transform)
// Update previous bounds and clear dirty
let current_bbox = get_shape_bounds(node.shape)
if not(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, resources, true, Color::black())
// 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 in 1.. 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 in 1.. BoundingBox {
let transform = parent_transform.multiply(node.transform)
let mut bbox = get_shape_bounds(node.shape)
if not(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
}
///|
/// Compute bounds without applying the node's own transform
fn compute_bounds_without_self_transform(
node : SVGNode,
parent_transform : Transform,
) -> BoundingBox {
let transform = parent_transform
let mut bbox = get_shape_bounds(node.shape)
if not(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~, ..) =>
if width <= 0.0 || height <= 0.0 {
BoundingBox::empty()
} else {
BoundingBox::from_rect(x, y, width, height)
}
Circle(cx~, cy~, r~) =>
if r <= 0.0 {
BoundingBox::empty()
} else {
BoundingBox::from_rect(cx - r, cy - r, r * 2.0, r * 2.0)
}
Ellipse(cx~, cy~, rx~, ry~) =>
if rx <= 0.0 || ry <= 0.0 {
BoundingBox::empty()
} else {
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)
}
Image(x~, y~, width~, height~, ..) =>
if width <= 0.0 || height <= 0.0 {
BoundingBox::empty()
} else {
BoundingBox::from_rect(x, y, 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
/// Font callback: (codepoint, font_size) -> (path_commands, advance_width)
text_to_paths : ((Int, Double) -> (Array[PathCommand], Double))?
}
///|
priv struct RenderResources {
clips : ClipPathRegistry
masks : MaskRegistry
patterns : PatternRegistry
gradients : GradientRegistry
markers : MarkerRegistry
}
///|
fn RenderResources::empty() -> RenderResources {
{
clips: ClipPathRegistry::new(),
masks: MaskRegistry::new(),
patterns: PatternRegistry::new(),
gradients: GradientRegistry::new(),
markers: MarkerRegistry::new(),
}
}
///|
/// Render a parsed SVG document with registered resources
pub fn SVGDocument::render(self : SVGDocument, ctx : RenderContext) -> Unit {
let resources = {
clips: self.clips,
masks: self.masks,
patterns: self.patterns,
gradients: self.gradients,
markers: self.markers,
}
match self.root.view_box {
Some(vb) => {
let viewbox_transform = vb.get_transform(
ctx.width.to_double(),
ctx.height.to_double(),
self.root.preserve_aspect_ratio,
)
render_node(
self.root,
viewbox_transform,
ctx,
resources,
true,
Color::black(),
)
}
None =>
render_node(
self.root,
Transform::identity(),
ctx,
resources,
true,
Color::black(),
)
}
}
///|
/// Render the scene to a pixel setter
pub fn Scene::render(self : Scene, ctx : RenderContext) -> Unit {
let resources = RenderResources::empty()
let base_transform = match self.root.view_box {
Some(vb) =>
vb.get_transform(
ctx.width.to_double(),
ctx.height.to_double(),
self.root.preserve_aspect_ratio,
)
None => Transform::identity()
}
render_node(self.root, base_transform, ctx, resources, true, Color::black())
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()
let resources = RenderResources::empty()
render_node(self.root, camera_transform, ctx, resources, true, Color::black())
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,
)
let resources = RenderResources::empty()
render_node(
self.root,
viewbox_transform,
ctx,
resources,
true,
Color::black(),
)
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)
let resources = RenderResources::empty()
render_node(self.root, combined, ctx, resources, true, Color::black())
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 intersect_clip(a : ClipRect, b : ClipRect) -> ClipRect? {
let x0 = if a.x > b.x { a.x } else { b.x }
let y0 = if a.y > b.y { a.y } else { b.y }
let x1 = if a.x + a.width < b.x + b.width {
a.x + a.width
} else {
b.x + b.width
}
let y1 = if a.y + a.height < b.y + b.height {
a.y + a.height
} else {
b.y + b.height
}
let w = x1 - x0
let h = y1 - y0
if w <= 0 || h <= 0 {
None
} else {
Some(ClipRect::new(x0, y0, w, h))
}
}
///|
fn render_node(
node : SVGNode,
parent_transform : Transform,
ctx : RenderContext,
resources : RenderResources,
allow_mask : Bool,
parent_color : Color,
) -> Unit {
// Skip invisible nodes
if node.opacity <= 0.0 {
return
}
let node_color = match node.color {
Some(c) => c
None => parent_color
}
let base_transform = parent_transform.multiply(node.transform)
let transform = match
(node.view_box, node.viewport_width, node.viewport_height) {
(Some(vb), Some(w), Some(h)) =>
base_transform.multiply(
vb.get_transform(w, h, node.preserve_aspect_ratio),
)
_ => base_transform
}
// Apply clip path if present
let ctx_for_node = match node.clip_path_id {
Some(id) =>
match resources.clips.get(id) {
Some(clip) => {
let bbox_local = match clip.units {
ClipPathUnits::ObjectBoundingBox =>
compute_bounds_without_self_transform(node, Transform::identity())
ClipPathUnits::UserSpaceOnUse => BoundingBox::empty()
}
apply_clip_path(ctx, clip, transform, bbox_local)
}
None => ctx
}
None => ctx
}
let ctx_for_node = match
(node.clip_overflow, node.viewport_width, node.viewport_height) {
(true, Some(vw), Some(vh)) => {
let viewport_bbox = BoundingBox::from_rect(0.0, 0.0, vw, vh)
let world_bbox = base_transform.apply_bbox(viewport_bbox)
let mut clip_x = floor_to_int(world_bbox.min_x)
let mut clip_y = floor_to_int(world_bbox.min_y)
let mut clip_w = ceil_to_int(world_bbox.max_x - world_bbox.min_x)
let mut clip_h = ceil_to_int(world_bbox.max_y - world_bbox.min_y)
if clip_x < 0 {
clip_w = clip_w + clip_x
clip_x = 0
}
if clip_y < 0 {
clip_h = clip_h + clip_y
clip_y = 0
}
if clip_x + clip_w > ctx_for_node.width {
clip_w = ctx_for_node.width - clip_x
}
if clip_y + clip_h > ctx_for_node.height {
clip_h = ctx_for_node.height - clip_y
}
if clip_w <= 0 || clip_h <= 0 {
return
}
let viewport_clip = ClipRect::new(clip_x, clip_y, clip_w, clip_h)
let merged = match ctx_for_node.clip {
Some(existing) => intersect_clip(existing, viewport_clip)
None => Some(viewport_clip)
}
match merged {
Some(clip) => {
let setter = ctx_for_node.setter.with_clip(clip)
{ ..ctx_for_node, setter, clip: Some(clip) }
}
None => return
}
}
_ => ctx_for_node
}
// Apply mask if present
if allow_mask {
match node.mask_id {
Some(id) =>
match resources.masks.get(id) {
Some(mask) => {
render_masked_node(
node, parent_transform, ctx_for_node, resources, mask, node_color,
)
return
}
None => ()
}
None => ()
}
}
// Early culling: check if node's bounding box is visible
let shape_bbox = get_shape_bounds(node.shape)
if not(shape_bbox.is_empty()) {
let transformed_bbox = transform.apply_bbox(shape_bbox)
if not(is_visible(transformed_bbox, ctx_for_node)) {
// 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_for_node, resources, true, node_color)
}
return
}
}
// Render shape
match node.shape {
Rect(x~, y~, width~, height~, rx~, ry~) =>
render_rect(
x, y, width, height, rx, ry, node, transform, ctx_for_node, resources, node_color,
)
Circle(cx~, cy~, r~) =>
render_circle(
cx, cy, r, node, transform, ctx_for_node, resources, node_color,
)
Ellipse(cx~, cy~, rx~, ry~) =>
render_ellipse(
cx, cy, rx, ry, node, transform, ctx_for_node, resources, node_color,
)
Line(x1~, y1~, x2~, y2~) =>
render_line(
x1, y1, x2, y2, node, transform, ctx_for_node, resources, node_color,
)
Polyline(points~) =>
render_polyline(
points, node, transform, ctx_for_node, resources, node_color,
)
Polygon(points~) =>
render_polygon(
points, node, transform, ctx_for_node, node_color, resources,
)
Path(commands~) =>
render_path(
commands, node, transform, ctx_for_node, node_color, resources,
)
Text(x~, y~, text~, font_size~) =>
render_text(
x, y, text, font_size, node, transform, ctx_for_node, node_color, resources,
)
Image(x~, y~, width~, height~, href~) =>
render_image(
x,
y,
width,
height,
href,
transform,
ctx_for_node,
node.preserve_aspect_ratio,
node.preserve_aspect_ratio_is_set,
)
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_for_node, resources, true, node_color)
}
}
///|
fn apply_clip_path(
ctx : RenderContext,
clip : ClipPath,
element_transform : Transform,
bbox_local : BoundingBox,
) -> RenderContext {
let base_transform = match clip.units {
ClipPathUnits::UserSpaceOnUse => element_transform
ClipPathUnits::ObjectBoundingBox => {
if bbox_local.is_empty() {
return ctx
}
let translate = Transform::translate(bbox_local.min_x, bbox_local.min_y)
let scale = Transform::scale(bbox_local.width(), bbox_local.height())
element_transform.multiply(translate.multiply(scale))
}
}
let combined = base_transform.multiply(clip.transform)
let clip = ClipPath::{
id: clip.id,
shape: clip.shape,
transform: combined,
clip_rule: clip.clip_rule,
units: clip.units,
}
let inner = ctx.setter
let setter : PixelSetter = {
set: fn(x, y, color) {
let px = x.to_double() + 0.5
let py = y.to_double() + 0.5
if clip.contains(px, py) {
inner.pixel(x, y, color)
}
},
}
{ ..ctx, setter, }
}
///|
fn render_masked_node(
node : SVGNode,
parent_transform : Transform,
ctx : RenderContext,
resources : RenderResources,
mask : Mask,
parent_color : Color,
) -> Unit {
let bbox = compute_bounds(node, parent_transform)
if bbox.is_empty() {
return
}
let bbox_local = compute_bounds_without_self_transform(
node,
Transform::identity(),
)
let element_transform = parent_transform.multiply(node.transform)
let min_x = floor_to_int(bbox.min_x)
let min_y = floor_to_int(bbox.min_y)
let max_x = ceil_to_int(bbox.max_x)
let max_y = ceil_to_int(bbox.max_y)
let width = max_x - min_x
let height = max_y - min_y
if width <= 0 || height <= 0 {
return
}
let image = Image::new(width, height)
let image_ctx = make_image_context(image, width, height, ctx.flatness)
let offset = Transform::translate(-min_x.to_double(), -min_y.to_double())
let image_parent = offset.multiply(parent_transform)
render_node(node, image_parent, image_ctx, resources, false, parent_color)
let mask_image = Image::new(width, height)
let mask_ctx = make_image_context(mask_image, width, height, ctx.flatness)
let mask_bounds_local = mask.get_mask_bounds(bbox_local)
let mask_bounds = element_transform.apply_bbox(mask_bounds_local)
let mut clip_min_x = floor_to_int(mask_bounds.min_x) - min_x
let mut clip_min_y = floor_to_int(mask_bounds.min_y) - min_y
let mut clip_max_x = ceil_to_int(mask_bounds.max_x) - min_x
let mut clip_max_y = ceil_to_int(mask_bounds.max_y) - min_y
if clip_min_x < 0 {
clip_min_x = 0
}
if clip_min_y < 0 {
clip_min_y = 0
}
if clip_max_x > width {
clip_max_x = width
}
if clip_max_y > height {
clip_max_y = height
}
let clip_w = clip_max_x - clip_min_x
let clip_h = clip_max_y - clip_min_y
let mask_ctx = if clip_w > 0 && clip_h > 0 {
let clip = ClipRect::new(clip_min_x, clip_min_y, clip_w, clip_h)
{ ..mask_ctx, setter: mask_ctx.setter.with_clip(clip), clip: Some(clip) }
} else {
mask_ctx
}
let mask_parent = match mask.mask_content_units {
MaskUnits::ObjectBoundingBox => {
let scale = Transform::scale(bbox_local.width(), bbox_local.height())
let translate = Transform::translate(bbox_local.min_x, bbox_local.min_y)
let object_transform = element_transform.multiply(
translate.multiply(scale),
)
offset.multiply(object_transform)
}
MaskUnits::UserSpaceOnUse => offset.multiply(element_transform)
}
for content_node in mask.content {
render_node(
content_node, mask_parent, mask_ctx, resources, false, parent_color,
)
}
let masked = apply_mask_to_image(image, mask_image, mask.mask_type)
blit_image_to_context(masked, ctx, min_x, min_y)
}
///|
fn make_image_context(
image : Image,
width : Int,
height : Int,
flatness : Double,
) -> RenderContext {
let setter : PixelSetter = {
set: (x, y, color) => image.set_pixel(x, y, color),
}
{ setter, width, height, flatness, clip: None, text_to_paths: None }
}
///|
fn blit_image_to_context(
image : Image,
ctx : RenderContext,
offset_x : Int,
offset_y : Int,
) -> Unit {
let setter = match ctx.clip {
Some(clip) => ctx.setter.with_clip(clip)
None => ctx.setter
}
for y in 0.. 0 {
setter.pixel(x + offset_x, y + offset_y, color)
}
}
}
}
///|
fn floor_to_int(value : Double) -> Int {
value.floor().to_int()
}
///|
fn ceil_to_int(value : Double) -> Int {
let i = value.to_int()
if value > i.to_double() {
i + 1
} else {
i
}
}
///|
/// 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 in 1..= 0 && result[j].z_index > current.z_index {
result[j + 1] = result[j]
j = j - 1
}
result[j + 1] = current
}
result
}
///|
priv enum ResolvedPaint {
None
SolidColor(Color)
LinearGrad(LinearGradient)
RadialGrad(RadialGradient)
Pattern(Pattern)
}
///|
fn resolve_paint_fallback(
fallback : PaintFallback,
node_color : Color,
) -> ResolvedPaint {
match fallback {
PaintFallback::NoPaint => ResolvedPaint::None
PaintFallback::SolidColor(color) => ResolvedPaint::SolidColor(color)
PaintFallback::CurrentColor => ResolvedPaint::SolidColor(node_color)
}
}
///|
fn pattern_is_valid(pattern : Pattern) -> Bool {
if pattern.width <= 0.0 || pattern.height <= 0.0 {
return false
}
if not(pattern.transform.is_invertible()) {
return false
}
pattern.content.length() > 0
}
///|
fn resolve_paint_for_render(
paint : Paint,
node_color : Color,
resources : RenderResources,
) -> ResolvedPaint {
match paint {
None => ResolvedPaint::None
SolidColor(color) => ResolvedPaint::SolidColor(color)
LinearGrad(grad) =>
if grad.stops.length() == 0 {
ResolvedPaint::None
} else {
ResolvedPaint::LinearGrad(grad)
}
RadialGrad(grad) =>
if grad.stops.length() == 0 {
ResolvedPaint::None
} else {
ResolvedPaint::RadialGrad(grad)
}
CurrentColor => ResolvedPaint::SolidColor(node_color)
PaintServerRef(id, fallback) =>
match resources.gradients.get(id) {
Some(Gradient::Linear(grad)) =>
if not(grad.transform.is_invertible()) {
resolve_paint_fallback(fallback, node_color)
} else if grad.stops.length() == 0 {
ResolvedPaint::None
} else {
ResolvedPaint::LinearGrad(grad)
}
Some(Gradient::Radial(grad)) =>
if not(grad.transform.is_invertible()) {
resolve_paint_fallback(fallback, node_color)
} else if grad.stops.length() == 0 {
ResolvedPaint::None
} else {
ResolvedPaint::RadialGrad(grad)
}
None =>
match resources.patterns.get(id) {
Some(pattern) =>
if pattern_is_valid(pattern) {
ResolvedPaint::Pattern(pattern)
} else {
resolve_paint_fallback(fallback, node_color)
}
None => resolve_paint_fallback(fallback, node_color)
}
}
}
}
///|
fn render_paint_order(
order : PaintOrder,
draw_fill : () -> Unit,
draw_stroke : () -> Unit,
draw_markers : () -> Unit,
) -> Unit {
for item in order.order {
match item {
Fill => draw_fill()
Stroke => draw_stroke()
Markers => draw_markers()
}
}
}
///|
fn render_pattern_fill(
shape : Shape,
transform : Transform,
ctx : RenderContext,
pattern : Pattern,
opacity : Double,
) -> Unit {
let bbox_local = get_shape_bounds(shape)
if bbox_local.is_empty() {
return
}
let bbox_world = transform.apply_bbox(bbox_local)
let mut ix = floor_to_int(bbox_world.min_x)
let mut iy = floor_to_int(bbox_world.min_y)
let mut iw = ceil_to_int(bbox_world.max_x - bbox_world.min_x)
let mut ih = ceil_to_int(bbox_world.max_y - bbox_world.min_y)
if iw <= 0 || ih <= 0 {
return
}
if ix < 0 {
iw = iw + ix
ix = 0
}
if iy < 0 {
ih = ih + iy
iy = 0
}
if ix + iw > ctx.width {
iw = ctx.width - ix
}
if iy + ih > ctx.height {
ih = ctx.height - iy
}
if iw <= 0 || ih <= 0 {
return
}
let inv = transform.inverse()
let pattern_inv = pattern.transform.inverse()
for y in iy..<(iy + ih) {
for x in ix..<(ix + iw) {
let wx = x.to_double() + 0.5
let wy = y.to_double() + 0.5
let (lx, ly) = inv.apply(wx, wy)
if hit_test_shape(lx, ly, shape) {
let (px, py) = pattern_inv.apply(lx, ly)
if pattern.get_color_at(px, py, bbox_local) is Some(color) {
ctx.setter.pixel(x, y, apply_opacity(color, opacity))
}
}
}
}
}
///|
fn build_substring_local(s : String, start : Int, end : Int) -> String {
let buf = StringBuilder::new()
let mut i = start
let len = s.length()
while i < end && i < len {
buf.write_char(Int::unsafe_to_char(s[i].to_int()))
i = i + 1
}
buf.to_string()
}
///|
fn string_ends_with_local(s : String, suffix : String) -> Bool {
if suffix.length() > s.length() {
return false
}
let start = s.length() - suffix.length()
for i in 0.. String {
let mut start = 0
let mut end = s.length()
while start < end {
let c = Int::unsafe_to_char(s[start].to_int())
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
start = start + 1
} else {
break
}
}
while end > start {
let c = Int::unsafe_to_char(s[end - 1].to_int())
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
end = end - 1
} else {
break
}
}
build_substring_local(s, start, end)
}
///|
fn is_space_char(c : Char) -> Bool {
c == ' ' || c == '\t' || c == '\n' || c == '\r'
}
///|
fn char_at(s : String, i : Int) -> Char {
Int::unsafe_to_char(s[i].to_int())
}
///|
fn parse_simple_number(s : String) -> Double {
let s = trim_string_local(s)
if s.length() == 0 {
return 0.0
}
let mut result = 0.0
let mut sign = 1.0
let mut i = 0
let len = s.length()
let first = char_at(s, i)
if first == '-' {
sign = -1.0
i = i + 1
} else if first == '+' {
i = i + 1
}
while i < len {
let c = char_at(s, i)
if c >= '0' && c <= '9' {
result = result * 10.0 + (c.to_int() - 48).to_double()
i = i + 1
} else {
break
}
}
if i < len && char_at(s, i) == '.' {
i = i + 1
let mut frac = 0.1
while i < len {
let c = char_at(s, i)
if c >= '0' && c <= '9' {
result = result + (c.to_int() - 48).to_double() * frac
frac = frac * 0.1
i = i + 1
} else {
break
}
}
}
sign * result
}
///|
fn parse_length_simple(s : String) -> Double {
let s = trim_string_local(s)
let cleaned = if string_ends_with_local(s, "px") {
build_substring_local(s, 0, s.length() - 2)
} else if string_ends_with_local(s, "em") || string_ends_with_local(s, "pt") {
build_substring_local(s, 0, s.length() - 2)
} else if string_ends_with_local(s, "%") {
build_substring_local(s, 0, s.length() - 1)
} else {
s
}
parse_simple_number(cleaned)
}
///|
fn replace_viewport_units(value : String, vw : Double, vh : Double) -> String {
let len = value.length()
let buf = StringBuilder::new()
let mut i = 0
while i < len {
let c = char_at(value, i)
let is_number_start = (c >= '0' && c <= '9') ||
c == '.' ||
c == '-' ||
c == '+'
if is_number_start {
let start = i
i = i + 1
while i < len {
let nc = char_at(value, i)
if (nc >= '0' && nc <= '9') || nc == '.' {
i = i + 1
} else {
break
}
}
let num_str = build_substring_local(value, start, i)
if i + 1 < len && char_at(value, i) == 'v' {
let unit = char_at(value, i + 1)
if unit == 'w' || unit == 'h' {
let num = parse_simple_number(num_str)
let val = if unit == 'w' {
num * vw / 100.0
} else {
num * vh / 100.0
}
buf.write_string(val.to_string())
i = i + 2
continue
}
}
buf.write_string(num_str)
continue
}
buf.write_char(c)
i = i + 1
}
buf.to_string()
}
///|
fn match_at(s : String, idx : Int, pat : String) -> Bool {
if idx + pat.length() > s.length() {
return false
}
for i in 0.. String? {
let len = svg.length()
let mut end = len
for i in 0..' {
end = i
break
}
}
let mut i = 0
while i < end {
if match_at(svg, i, name) {
let mut j = i + name.length()
while j < end && is_space_char(char_at(svg, j)) {
j = j + 1
}
if j < end && char_at(svg, j) == '=' {
j = j + 1
while j < end && is_space_char(char_at(svg, j)) {
j = j + 1
}
if j >= end {
return None
}
let quote = char_at(svg, j)
if quote == '"' || quote == '\'' {
j = j + 1
let start = j
while j < end && char_at(svg, j) != quote {
j = j + 1
}
if j > start {
return Some(build_substring_local(svg, start, j))
}
return None
} else {
let start = j
while j < end &&
(char_at(svg, j) |> is_space_char |> not) &&
char_at(svg, j) != '>' {
j = j + 1
}
if j > start {
return Some(build_substring_local(svg, start, j))
}
return None
}
}
}
i = i + 1
}
None
}
///|
fn extract_svg_size(svg : String) -> (Double, Double) {
let width = match find_attr_value(svg, "width") {
Some(v) => parse_length_simple(v)
None => 0.0
}
let height = match find_attr_value(svg, "height") {
Some(v) => parse_length_simple(v)
None => 0.0
}
(width, height)
}
///|
fn render_image(
x : Double,
y : Double,
width : Double,
height : Double,
href : String,
transform : Transform,
ctx : RenderContext,
preserve_aspect_ratio : PreserveAspectRatio,
preserve_aspect_ratio_is_set : Bool,
) -> Unit {
if width <= 0.0 || height <= 0.0 {
return
}
if href.length() == 0 {
return
}
let (x0, y0) = transform.apply(x, y)
let (x1, y1) = transform.apply(x + width, y + height)
let min_x = min(x0, x1)
let min_y = min(y0, y1)
let max_x = max(x0, x1)
let max_y = max(y0, y1)
let ix = floor_to_int(min_x)
let iy = floor_to_int(min_y)
let iw = ceil_to_int(max_x - min_x)
let ih = ceil_to_int(max_y - min_y)
if iw <= 0 || ih <= 0 {
return
}
let processed = replace_viewport_units(href, iw.to_double(), ih.to_double())
match parse_svg_document(processed) {
Some(doc) => {
if preserve_aspect_ratio_is_set {
doc.root.preserve_aspect_ratio = preserve_aspect_ratio
}
if doc.root.view_box is None {
let (svg_w, svg_h) = extract_svg_size(processed)
if svg_w > 0.0 && svg_h > 0.0 {
doc.root.view_box = Some(ViewBox::{
min_x: 0.0,
min_y: 0.0,
width: svg_w,
height: svg_h,
})
}
}
let image = Image::new(iw, ih)
let setter : PixelSetter = {
set: (px, py, color) => image.set_pixel(px, py, color),
}
let image_ctx = RenderContext::new(setter, iw, ih)
doc.render(image_ctx)
blit_image_to_context(image, ctx, ix, iy)
}
None => ()
}
}
///|
fn render_rect(
x : Double,
y : Double,
width : Double,
height : Double,
rx : Double,
ry : Double,
node : SVGNode,
transform : Transform,
ctx : RenderContext,
resources : RenderResources,
node_color : Color,
) -> Unit {
if width <= 0.0 || height <= 0.0 {
return
}
// Transform corners
let (x0, y0) = transform.apply(x, y)
let (x1, y1) = transform.apply(x + width, y + height)
let min_x = min(x0, x1)
let min_y = min(y0, y1)
let max_x = max(x0, x1)
let max_y = max(y0, y1)
let ix = floor_to_int(min_x)
let iy = floor_to_int(min_y)
let iw = ceil_to_int(max_x - min_x)
let ih = ceil_to_int(max_y - min_y)
if iw <= 0 || ih <= 0 {
return
}
let (sx, sy) = transform.get_scale()
let irx = (rx * sx).to_int()
let iry = (ry * sy).to_int()
fn draw_fill() -> Unit {
match resolve_paint_for_render(node.fill, node_color, resources) {
ResolvedPaint::SolidColor(color) =>
if node.fill_opacity > 0.0 {
let fill_color = apply_opacity(
color,
node.fill_opacity * node.opacity,
)
if irx > 0 || iry > 0 {
raster_rounded_rect_fill(
ix,
iy,
iw,
ih,
irx,
iry,
fill_color,
ctx.setter,
)
} else {
raster_rect_fill(ix, iy, iw, ih, fill_color, ctx.setter)
}
}
ResolvedPaint::LinearGrad(grad) =>
if node.fill_opacity > 0.0 {
raster_rect_gradient(
ix,
iy,
iw,
ih,
grad,
node.fill_opacity * node.opacity,
ctx.setter,
)
}
ResolvedPaint::RadialGrad(grad) =>
if node.fill_opacity > 0.0 {
raster_rect_radial_gradient(
ix,
iy,
iw,
ih,
grad,
node.fill_opacity * node.opacity,
ctx.setter,
)
}
ResolvedPaint::Pattern(pattern) =>
if node.fill_opacity > 0.0 {
render_pattern_fill(
node.shape,
transform,
ctx,
pattern,
node.fill_opacity * node.opacity,
)
}
ResolvedPaint::None => ()
}
}
fn draw_stroke() -> Unit {
match resolve_paint_for_render(node.stroke.paint, node_color, resources) {
ResolvedPaint::SolidColor(color) =>
if node.stroke_opacity > 0.0 && node.stroke.width > 0.0 {
let stroke_color = apply_opacity(
color,
node.stroke_opacity * node.opacity,
)
let stroke_width = node.stroke.width * (sx + sy) / 2.0
let stroke_px = if stroke_width > 0.0 {
ceil_to_int(stroke_width)
} else {
0
}
if stroke_px > 0 {
if irx > 0 || iry > 0 {
raster_rounded_rect_stroke_thick(
ix,
iy,
iw,
ih,
irx,
iry,
stroke_px,
stroke_color,
ctx.setter,
)
} else {
raster_rect_stroke_thick(
ix,
iy,
iw,
ih,
stroke_px,
stroke_color,
ctx.setter,
)
}
}
}
_ => ()
}
}
render_paint_order(node.paint_order, draw_fill, draw_stroke, () => ())
}
///|
fn render_circle(
cx : Double,
cy : Double,
r : Double,
node : SVGNode,
transform : Transform,
ctx : RenderContext,
resources : RenderResources,
node_color : Color,
) -> Unit {
if r <= 0.0 {
return
}
let (tx, ty) = transform.apply(cx, cy)
let (sx, sy) = transform.get_scale()
let tr = (r * (sx + sy) / 2.0).to_int()
if tr <= 0 {
return
}
let icx = tx.to_int()
let icy = ty.to_int()
fn draw_fill() -> Unit {
match resolve_paint_for_render(node.fill, node_color, resources) {
ResolvedPaint::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)
}
ResolvedPaint::RadialGrad(grad) =>
if node.fill_opacity > 0.0 {
raster_circle_radial_gradient(
icx,
icy,
tr,
grad,
node.fill_opacity * node.opacity,
ctx.setter,
)
}
ResolvedPaint::Pattern(pattern) =>
if node.fill_opacity > 0.0 {
render_pattern_fill(
node.shape,
transform,
ctx,
pattern,
node.fill_opacity * node.opacity,
)
}
_ => ()
}
}
fn draw_stroke() -> Unit {
match resolve_paint_for_render(node.stroke.paint, node_color, resources) {
ResolvedPaint::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)
}
_ => ()
}
}
render_paint_order(node.paint_order, draw_fill, draw_stroke, () => ())
}
///|
fn render_ellipse(
cx : Double,
cy : Double,
rx : Double,
ry : Double,
node : SVGNode,
transform : Transform,
ctx : RenderContext,
resources : RenderResources,
node_color : Color,
) -> Unit {
if rx <= 0.0 || ry <= 0.0 {
return
}
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()
if trx <= 0 || try_ <= 0 {
return
}
let icx = tx.to_int()
let icy = ty.to_int()
fn draw_fill() -> Unit {
match resolve_paint_for_render(node.fill, node_color, resources) {
ResolvedPaint::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)
}
ResolvedPaint::RadialGrad(grad) =>
if node.fill_opacity > 0.0 {
raster_ellipse_radial_gradient(
icx,
icy,
trx,
try_,
grad,
node.fill_opacity * node.opacity,
ctx.setter,
)
}
ResolvedPaint::Pattern(pattern) =>
if node.fill_opacity > 0.0 {
render_pattern_fill(
node.shape,
transform,
ctx,
pattern,
node.fill_opacity * node.opacity,
)
}
_ => ()
}
}
fn draw_stroke() -> Unit {
match resolve_paint_for_render(node.stroke.paint, node_color, resources) {
ResolvedPaint::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)
}
_ => ()
}
}
render_paint_order(node.paint_order, draw_fill, draw_stroke, () => ())
}
///|
fn render_line(
x1 : Double,
y1 : Double,
x2 : Double,
y2 : Double,
node : SVGNode,
transform : Transform,
ctx : RenderContext,
resources : RenderResources,
node_color : Color,
) -> Unit {
let (tx1, ty1) = transform.apply(x1, y1)
let (tx2, ty2) = transform.apply(x2, y2)
fn draw_stroke() -> Unit {
match resolve_paint_for_render(node.stroke.paint, node_color, resources) {
ResolvedPaint::SolidColor(color) =>
if node.stroke_opacity > 0.0 && node.stroke.width > 0.0 {
let stroke_color = apply_opacity(
color,
node.stroke_opacity * node.opacity,
)
let stroke_px = node.stroke.width.to_int()
// 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(),
stroke_px,
stroke_color,
ctx.setter,
)
draw_line_caps(
tx1.to_int(),
ty1.to_int(),
tx2.to_int(),
ty2.to_int(),
stroke_px,
node.stroke.linecap,
stroke_color,
ctx.setter,
)
}
}
}
_ => ()
}
}
fn draw_markers() -> Unit {
render_markers_for_points(
[(x1, y1), (x2, y2)],
node,
transform,
ctx,
resources,
node_color,
)
}
render_paint_order(node.paint_order, () => (), draw_stroke, draw_markers)
}
///|
fn draw_line_caps(
x0 : Int,
y0 : Int,
x1 : Int,
y1 : Int,
width : Int,
linecap : LineCap,
color : Color,
setter : PixelSetter,
) -> Unit {
if width <= 1 {
return
}
match linecap {
Butt => ()
Round => {
let half_w = width / 2
raster_circle_fill(x0, y0, half_w, color, setter)
raster_circle_fill(x1, y1, half_w, color, setter)
}
Square => {
let dx = (x1 - x0).to_double()
let dy = (y1 - y0).to_double()
let len = (dx * dx + dy * dy).sqrt()
if len < 0.001 {
let half_w = width / 2
raster_circle_fill(x0, y0, half_w, color, setter)
return
}
let ux = dx / len
let uy = dy / len
let half_w = width / 2
let ex = (ux * half_w.to_double()).to_int()
let ey = (uy * half_w.to_double()).to_int()
raster_thick_line(x0 - ex, y0 - ey, x0, y0, width, color, setter)
raster_thick_line(x1, y1, x1 + ex, y1 + ey, width, color, setter)
}
}
}
///|
fn marker_transform_with_ref_ratio(
marker : Marker,
x : Double,
y : Double,
angle : Double,
scale : Double,
ref_ratio : Double,
) -> Transform {
let orient_angle = match marker.orient {
Auto => angle
AutoStartReverse => angle + 3.14159265358979323846
Angle(a) => degrees_to_radians(a)
}
let content_transform = match marker.view_box {
Some(vb) => {
let view_t = vb.get_transform(
marker.marker_width,
marker.marker_height,
marker.preserve_aspect_ratio,
)
let (ref_px, ref_py) = view_t.apply(marker.ref_x, marker.ref_y)
let ref_t = Transform::translate(-ref_px * ref_ratio, -ref_py * ref_ratio)
ref_t.multiply(view_t)
}
None =>
Transform::translate(-marker.ref_x * ref_ratio, -marker.ref_y * ref_ratio)
}
let t1 = Transform::translate(x, y)
let r = Transform::rotate(orient_angle)
let s = Transform::scale(scale, scale)
t1.multiply(r).multiply(s).multiply(content_transform)
}
///|
fn render_marker_instance(
marker : Marker,
x : Double,
y : Double,
angle : Double,
node : SVGNode,
transform : Transform,
ctx : RenderContext,
resources : RenderResources,
node_color : Color,
) -> Unit {
let (marker_scale, ref_ratio) = match marker.marker_units {
StrokeWidth => {
let scale = node.stroke.width
if scale > 0.0 {
(scale, 1.0)
} else {
(0.0, 1.0)
}
}
UserSpaceOnUse_ => (1.0, 1.0)
}
let marker_transform = marker_transform_with_ref_ratio(
marker, x, y, angle, marker_scale, ref_ratio,
)
let viewport_transform = marker_viewport_transform(
marker, x, y, angle, marker_scale, ref_ratio,
)
let parent_transform = transform.multiply(marker_transform)
let mut ctx_for_marker = ctx
if marker.clip_overflow &&
marker.marker_width > 0.0 &&
marker.marker_height > 0.0 {
let viewport_bbox = BoundingBox::from_rect(
0.0,
0.0,
marker.marker_width,
marker.marker_height,
)
let clip_transform = transform.multiply(viewport_transform)
let clip_shape = Rect(
x=viewport_bbox.min_x,
y=viewport_bbox.min_y,
width=viewport_bbox.width(),
height=viewport_bbox.height(),
rx=0.0,
ry=0.0,
)
let clip = ClipPath::with_transform(
"marker-viewport", clip_shape, clip_transform,
)
ctx_for_marker = apply_clip_path(
ctx_for_marker,
clip,
Transform::identity(),
BoundingBox::empty(),
)
}
render_node(
marker.content,
parent_transform,
ctx_for_marker,
resources,
true,
node_color,
)
}
///|
fn marker_viewport_transform(
marker : Marker,
x : Double,
y : Double,
angle : Double,
scale : Double,
ref_ratio : Double,
) -> Transform {
let orient_angle = match marker.orient {
Auto => angle
AutoStartReverse => angle + 3.14159265358979323846
Angle(a) => degrees_to_radians(a)
}
let (ref_px, ref_py) = match marker.view_box {
Some(vb) => {
let view_t = vb.get_transform(
marker.marker_width,
marker.marker_height,
marker.preserve_aspect_ratio,
)
view_t.apply(marker.ref_x, marker.ref_y)
}
None => (marker.ref_x, marker.ref_y)
}
let ref_t = Transform::translate(-ref_px * ref_ratio, -ref_py * ref_ratio)
let t1 = Transform::translate(x, y)
let r = Transform::rotate(orient_angle)
let s = Transform::scale(scale, scale)
t1.multiply(r).multiply(s).multiply(ref_t)
}
///|
fn render_markers_for_points(
points : Array[(Double, Double)],
node : SVGNode,
transform : Transform,
ctx : RenderContext,
resources : RenderResources,
node_color : Color,
) -> Unit {
if points.length() < 2 {
return
}
if node.marker_start is None &&
node.marker_mid is None &&
node.marker_end is None {
return
}
let (sx, sy) = transform.get_scale()
let skew = transform.a * transform.c + transform.b * transform.d
let use_transformed = (sx - sy).abs() > 0.0001 || skew.abs() > 0.0001
let angle_points = if use_transformed {
points.map(p => transform.apply(p.0, p.1))
} else {
points
}
let line = MarkedLine::new(angle_points)
let inv = transform.inverse()
fn to_local_angle(angle : Double) -> Double {
let dx = @math.cos(angle)
let dy = @math.sin(angle)
let lx = inv.a * dx + inv.c * dy
let ly = inv.b * dx + inv.d * dy
@math.atan2(ly, lx)
}
let last = points.length() - 1
for i in 0..
match resources.markers.get(id) {
Some(marker) => {
let (x, y) = points[i]
let angle = if use_transformed {
to_local_angle(line.get_angle_at(i))
} else {
line.get_angle_at(i)
}
render_marker_instance(
marker, x, y, angle, node, transform, ctx, resources, node_color,
)
}
None => ()
}
None => ()
}
}
}
///|
fn is_closed_polyline_double(points : Array[(Double, Double)]) -> Bool {
if points.length() < 2 {
false
} else {
let last = points.length() - 1
let (x0, y0) = points[0]
let (x1, y1) = points[last]
x0 == x1 && y0 == y1
}
}
///|
fn render_markers_for_polylines(
polylines : Array[Array[(Double, Double)]],
node : SVGNode,
transform : Transform,
ctx : RenderContext,
resources : RenderResources,
node_color : Color,
) -> Unit {
if node.marker_start is None &&
node.marker_mid is None &&
node.marker_end is None {
return
}
if polylines.length() == 0 {
return
}
// Find first/last non-empty polyline
let mut first_idx = -1
let mut last_idx = -1
for i in 0..= 2 {
if first_idx < 0 {
first_idx = i
}
last_idx = i
}
}
if first_idx < 0 || last_idx < 0 {
return
}
let eps = 0.0001
let (sx, sy) = transform.get_scale()
let skew = transform.a * transform.c + transform.b * transform.d
let use_transformed = (sx - sy).abs() > eps || skew.abs() > eps
let inv = transform.inverse()
fn angle_from(dx : Double, dy : Double) -> Double {
if use_transformed {
let lx = inv.a * dx + inv.c * dy
let ly = inv.b * dx + inv.d * dy
@math.atan2(ly, lx)
} else {
@math.atan2(dy, dx)
}
}
fn normalize(dx : Double, dy : Double) -> (Double, Double, Double) {
let len = (dx * dx + dy * dy).sqrt()
if len < eps {
(0.0, 0.0, len)
} else {
(dx / len, dy / len, len)
}
}
fn angle_start(points : Array[(Double, Double)]) -> Double {
if points.length() < 2 {
return 0.0
}
for i in 1..= eps {
return angle_from(dx, dy)
}
}
0.0
}
fn angle_end(points : Array[(Double, Double)]) -> Double {
if points.length() < 2 {
return 0.0
}
for i = points.length() - 1; i > 0; i = i - 1 {
let dx = points[i].0 - points[i - 1].0
let dy = points[i].1 - points[i - 1].1
let (_, _, l) = normalize(dx, dy)
if l >= eps {
return angle_from(dx, dy)
}
}
0.0
}
fn angle_mid(
points : Array[(Double, Double)],
index : Int,
closed : Bool,
has_duplicate : Bool,
) -> Double {
let len = points.length()
if len < 2 {
return 0.0
}
if not(closed) && index == 0 {
for i in 1..= eps {
return angle_from(dx, dy)
}
}
return 0.0
}
if not(closed) && index >= len - 1 {
for i = len - 1; i > 0; i = i - 1 {
let dx = points[i].0 - points[i - 1].0
let dy = points[i].1 - points[i - 1].1
let (_, _, l) = normalize(dx, dy)
if l >= eps {
return angle_from(dx, dy)
}
}
return 0.0
}
let prev = if has_duplicate && closed {
if index == 0 || index == len - 1 {
if len > 1 {
len - 2
} else {
0
}
} else {
index - 1
}
} else if index == 0 {
len - 1
} else {
index - 1
}
let next = if has_duplicate && closed {
if index == len - 1 {
if len > 1 {
1
} else {
0
}
} else if index + 1 == len {
0
} else {
index + 1
}
} else if index + 1 == len {
0
} else {
index + 1
}
let dx1 = points[index].0 - points[prev].0
let dy1 = points[index].1 - points[prev].1
let dx2 = points[next].0 - points[index].0
let dy2 = points[next].1 - points[index].1
let (ux1, uy1, l1) = normalize(dx1, dy1)
let (ux2, uy2, l2) = normalize(dx2, dy2)
if l1 < eps && l2 < eps {
return 0.0
}
if l1 < eps {
return angle_from(dx2, dy2)
}
if l2 < eps {
return angle_from(dx1, dy1)
}
let sx = ux1 + ux2
let sy = uy1 + uy2
if sx * sx + sy * sy < eps * eps {
angle_from(dx2, dy2)
} else {
angle_from(sx, sy)
}
}
for poly_idx in 0.. transform.apply(p.0, p.1))
} else {
points
}
let closed = is_closed_polyline_double(points)
let has_duplicate = closed
let last = points.length() - 1
for i in 0..
match resources.markers.get(id) {
Some(marker) => {
let (x, y) = points[i]
let angle = if i == 0 {
if closed {
angle_mid(angle_points, i, closed, has_duplicate)
} else {
angle_start(angle_points)
}
} else if i == last {
if closed {
angle_mid(angle_points, i, closed, has_duplicate)
} else {
angle_end(angle_points)
}
} else {
angle_mid(angle_points, i, closed, has_duplicate)
}
render_marker_instance(
marker, x, y, angle, node, transform, ctx, resources, node_color,
)
}
None => ()
}
None => ()
}
}
}
}
///|
priv struct MarkerPoint {
x : Double
y : Double
mut has_in : Bool
mut in_dx : Double
mut in_dy : Double
mut has_out : Bool
mut out_dx : Double
mut out_dy : Double
}
///|
fn MarkerPoint::new(x : Double, y : Double) -> MarkerPoint {
{
x,
y,
has_in: false,
in_dx: 0.0,
in_dy: 0.0,
has_out: false,
out_dx: 0.0,
out_dy: 0.0,
}
}
///|
priv struct MarkerSubpath {
points : Array[MarkerPoint]
}
///|
fn arc_tangent_at(
theta : Double,
cos_phi : Double,
sin_phi : Double,
rx : Double,
ry : Double,
) -> (Double, Double) {
let sin_t = @math.sin(theta)
let cos_t = @math.cos(theta)
let dx = -cos_phi * rx * sin_t - sin_phi * ry * cos_t
let dy = -sin_phi * rx * sin_t + cos_phi * ry * cos_t
(dx, dy)
}
///|
fn angle_between_vec(
ux : Double,
uy : Double,
vx : Double,
vy : Double,
) -> Double {
let dot = ux * vx + uy * vy
let len_u = (ux * ux + uy * uy).sqrt()
let len_v = (vx * vx + vy * vy).sqrt()
let len_prod = len_u * len_v
if len_prod == 0.0 {
return 0.0
}
let mut cos_angle = dot / len_prod
if cos_angle > 1.0 {
cos_angle = 1.0
}
if cos_angle < -1.0 {
cos_angle = -1.0
}
let angle = @math.acos(cos_angle)
let cross = ux * vy - uy * vx
if cross < 0.0 {
-angle
} else {
angle
}
}
///|
fn compute_arc_tangents(
x1 : Double,
y1 : Double,
rx_in : Double,
ry_in : Double,
rotation_degrees : Double,
large_arc : Bool,
sweep : Bool,
x2 : Double,
y2 : Double,
) -> (Double, Double, Double, Double) {
let dx_line = x2 - x1
let dy_line = y2 - y1
if (x1 == x2 && y1 == y2) || rx_in == 0.0 || ry_in == 0.0 {
return (dx_line, dy_line, dx_line, dy_line)
}
let mut rx = if rx_in < 0.0 { -rx_in } else { rx_in }
let mut ry = if ry_in < 0.0 { -ry_in } else { ry_in }
let phi = degrees_to_radians(rotation_degrees)
let cos_phi = @math.cos(phi)
let sin_phi = @math.sin(phi)
let dx = (x1 - x2) / 2.0
let dy = (y1 - y2) / 2.0
let x1p = cos_phi * dx + sin_phi * dy
let y1p = -sin_phi * dx + cos_phi * dy
let lambda = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry)
if lambda > 1.0 {
let sqrt_lambda = lambda.sqrt()
rx = rx * sqrt_lambda
ry = ry * sqrt_lambda
}
let rx2 = rx * rx
let ry2 = ry * ry
let x1p2 = x1p * x1p
let y1p2 = y1p * y1p
let sq = (rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2) / (rx2 * y1p2 + ry2 * x1p2)
let sq_abs = if sq < 0.0 { 0.0 } else { sq }
let coef = sq_abs.sqrt() * (if large_arc == sweep { -1.0 } else { 1.0 })
let cxp = coef * rx * y1p / ry
let cyp = -coef * ry * x1p / rx
let theta1 = angle_between_vec(1.0, 0.0, (x1p - cxp) / rx, (y1p - cyp) / ry)
let mut dtheta = angle_between_vec(
(x1p - cxp) / rx,
(y1p - cyp) / ry,
(-x1p - cxp) / rx,
(-y1p - cyp) / ry,
)
let pi = 3.14159265358979323846
if not(sweep) && dtheta > 0.0 {
dtheta = dtheta - 2.0 * pi
} else if sweep && dtheta < 0.0 {
dtheta = dtheta + 2.0 * pi
}
let theta2 = theta1 + dtheta
let sign = if dtheta < 0.0 { -1.0 } else { 1.0 }
let (sx, sy) = arc_tangent_at(theta1, cos_phi, sin_phi, rx, ry)
let (ex, ey) = arc_tangent_at(theta2, cos_phi, sin_phi, rx, ry)
(sx * sign, sy * sign, ex * sign, ey * sign)
}
///|
fn build_marker_subpaths(commands : Array[PathCommand]) -> Array[MarkerSubpath] {
let subpaths : Array[MarkerSubpath] = []
let mut points : Array[MarkerPoint] = []
let mut closed = false
let mut cur_x = 0.0
let mut cur_y = 0.0
let mut start_x = 0.0
let mut start_y = 0.0
let mut last_ctrl_x = 0.0
let mut last_ctrl_y = 0.0
let mut last_cmd_was_curve = false
let mut last_cmd_was_quad = false
let eps = 0.0001
fn finish_subpath(
subpaths : Array[MarkerSubpath],
points : Array[MarkerPoint],
closed : Bool,
) -> Unit {
if points.length() == 0 {
return
}
let out_points = points
if closed {
let first = out_points[0]
let dup = MarkerPoint::new(first.x, first.y)
if first.has_in {
dup.has_in = true
dup.in_dx = first.in_dx
dup.in_dy = first.in_dy
}
out_points.push(dup)
}
subpaths.push(MarkerSubpath::{ points: out_points })
}
fn record_out(
points : Array[MarkerPoint],
idx : Int,
dx : Double,
dy : Double,
eps : Double,
) -> Unit {
let len = (dx * dx + dy * dy).sqrt()
if len >= eps {
points[idx].has_out = true
points[idx].out_dx = dx
points[idx].out_dy = dy
}
}
fn record_in(
points : Array[MarkerPoint],
idx : Int,
dx : Double,
dy : Double,
eps : Double,
) -> Unit {
let len = (dx * dx + dy * dy).sqrt()
if len >= eps {
points[idx].has_in = true
points[idx].in_dx = dx
points[idx].in_dy = dy
}
}
fn add_segment(
points : Array[MarkerPoint],
cur_x : Double,
cur_y : Double,
end_x : Double,
end_y : Double,
out_dx : Double,
out_dy : Double,
in_dx : Double,
in_dy : Double,
eps : Double,
) -> Unit {
if points.length() == 0 {
points.push(MarkerPoint::new(cur_x, cur_y))
}
let last_idx = points.length() - 1
record_out(points, last_idx, out_dx, out_dy, eps)
let end_point = MarkerPoint::new(end_x, end_y)
let in_len = (in_dx * in_dx + in_dy * in_dy).sqrt()
if in_len >= eps {
end_point.has_in = true
end_point.in_dx = in_dx
end_point.in_dy = in_dy
}
points.push(end_point)
}
for cmd in commands {
match cmd {
MoveTo(x, y) => {
finish_subpath(subpaths, points, closed)
points = [MarkerPoint::new(x, y)]
closed = false
cur_x = x
cur_y = y
start_x = x
start_y = y
last_cmd_was_curve = false
last_cmd_was_quad = false
}
MoveToRel(dx, dy) => {
finish_subpath(subpaths, points, closed)
let x = cur_x + dx
let y = cur_y + dy
points = [MarkerPoint::new(x, y)]
closed = false
cur_x = x
cur_y = y
start_x = x
start_y = y
last_cmd_was_curve = false
last_cmd_was_quad = false
}
LineTo(x, y) => {
let dx = x - cur_x
let dy = y - cur_y
add_segment(points, cur_x, cur_y, x, y, dx, dy, dx, dy, eps)
cur_x = x
cur_y = y
last_cmd_was_curve = false
last_cmd_was_quad = false
}
LineToRel(dx, dy) => {
let x = cur_x + dx
let y = cur_y + dy
add_segment(points, cur_x, cur_y, x, y, dx, dy, dx, dy, eps)
cur_x = x
cur_y = y
last_cmd_was_curve = false
last_cmd_was_quad = false
}
HorizontalLineTo(x) => {
let dx = x - cur_x
add_segment(points, cur_x, cur_y, x, cur_y, dx, 0.0, dx, 0.0, eps)
cur_x = x
last_cmd_was_curve = false
last_cmd_was_quad = false
}
HorizontalLineToRel(dx) => {
let x = cur_x + dx
add_segment(points, cur_x, cur_y, x, cur_y, dx, 0.0, dx, 0.0, eps)
cur_x = x
last_cmd_was_curve = false
last_cmd_was_quad = false
}
VerticalLineTo(y) => {
let dy = y - cur_y
add_segment(points, cur_x, cur_y, cur_x, y, 0.0, dy, 0.0, dy, eps)
cur_y = y
last_cmd_was_curve = false
last_cmd_was_quad = false
}
VerticalLineToRel(dy) => {
let y = cur_y + dy
add_segment(points, cur_x, cur_y, cur_x, y, 0.0, dy, 0.0, dy, eps)
cur_y = y
last_cmd_was_curve = false
last_cmd_was_quad = false
}
CurveTo(x1, y1, x2, y2, x, y) => {
let mut sx = x1 - cur_x
let mut sy = y1 - cur_y
if (sx * sx + sy * sy).sqrt() < eps {
sx = x2 - cur_x
sy = y2 - cur_y
}
if (sx * sx + sy * sy).sqrt() < eps {
sx = x - cur_x
sy = y - cur_y
}
let mut ex = x - x2
let mut ey = y - y2
if (ex * ex + ey * ey).sqrt() < eps {
ex = x - x1
ey = y - y1
}
if (ex * ex + ey * ey).sqrt() < eps {
ex = x - cur_x
ey = y - cur_y
}
add_segment(points, cur_x, cur_y, x, y, sx, sy, ex, ey, eps)
last_ctrl_x = x2
last_ctrl_y = y2
cur_x = x
cur_y = y
last_cmd_was_curve = true
last_cmd_was_quad = false
}
CurveToRel(dx1, dy1, dx2, dy2, dx, dy) => {
let x1 = cur_x + dx1
let y1 = cur_y + dy1
let x2 = cur_x + dx2
let y2 = cur_y + dy2
let x = cur_x + dx
let y = cur_y + dy
let mut sx = x1 - cur_x
let mut sy = y1 - cur_y
if (sx * sx + sy * sy).sqrt() < eps {
sx = x2 - cur_x
sy = y2 - cur_y
}
if (sx * sx + sy * sy).sqrt() < eps {
sx = x - cur_x
sy = y - cur_y
}
let mut ex = x - x2
let mut ey = y - y2
if (ex * ex + ey * ey).sqrt() < eps {
ex = x - x1
ey = y - y1
}
if (ex * ex + ey * ey).sqrt() < eps {
ex = x - cur_x
ey = y - cur_y
}
add_segment(points, cur_x, cur_y, x, y, sx, sy, ex, ey, eps)
last_ctrl_x = x2
last_ctrl_y = y2
cur_x = x
cur_y = y
last_cmd_was_curve = true
last_cmd_was_quad = false
}
SmoothCurveTo(x2, y2, x, y) => {
let x1 = if last_cmd_was_curve {
2.0 * cur_x - last_ctrl_x
} else {
cur_x
}
let y1 = if last_cmd_was_curve {
2.0 * cur_y - last_ctrl_y
} else {
cur_y
}
let mut sx = x1 - cur_x
let mut sy = y1 - cur_y
if (sx * sx + sy * sy).sqrt() < eps {
sx = x2 - cur_x
sy = y2 - cur_y
}
if (sx * sx + sy * sy).sqrt() < eps {
sx = x - cur_x
sy = y - cur_y
}
let mut ex = x - x2
let mut ey = y - y2
if (ex * ex + ey * ey).sqrt() < eps {
ex = x - x1
ey = y - y1
}
if (ex * ex + ey * ey).sqrt() < eps {
ex = x - cur_x
ey = y - cur_y
}
add_segment(points, cur_x, cur_y, x, y, sx, sy, ex, ey, eps)
last_ctrl_x = x2
last_ctrl_y = y2
cur_x = x
cur_y = y
last_cmd_was_curve = true
last_cmd_was_quad = false
}
SmoothCurveToRel(dx2, dy2, dx, dy) => {
let x1 = if last_cmd_was_curve {
2.0 * cur_x - last_ctrl_x
} else {
cur_x
}
let y1 = if last_cmd_was_curve {
2.0 * cur_y - last_ctrl_y
} else {
cur_y
}
let x2 = cur_x + dx2
let y2 = cur_y + dy2
let x = cur_x + dx
let y = cur_y + dy
let mut sx = x1 - cur_x
let mut sy = y1 - cur_y
if (sx * sx + sy * sy).sqrt() < eps {
sx = x2 - cur_x
sy = y2 - cur_y
}
if (sx * sx + sy * sy).sqrt() < eps {
sx = x - cur_x
sy = y - cur_y
}
let mut ex = x - x2
let mut ey = y - y2
if (ex * ex + ey * ey).sqrt() < eps {
ex = x - x1
ey = y - y1
}
if (ex * ex + ey * ey).sqrt() < eps {
ex = x - cur_x
ey = y - cur_y
}
add_segment(points, cur_x, cur_y, x, y, sx, sy, ex, ey, eps)
last_ctrl_x = x2
last_ctrl_y = y2
cur_x = x
cur_y = y
last_cmd_was_curve = true
last_cmd_was_quad = false
}
QuadraticCurveTo(x1, y1, x, y) => {
let mut sx = x1 - cur_x
let mut sy = y1 - cur_y
if (sx * sx + sy * sy).sqrt() < eps {
sx = x - cur_x
sy = y - cur_y
}
let mut ex = x - x1
let mut ey = y - y1
if (ex * ex + ey * ey).sqrt() < eps {
ex = x - cur_x
ey = y - cur_y
}
add_segment(points, cur_x, cur_y, x, y, sx, sy, ex, ey, eps)
last_ctrl_x = x1
last_ctrl_y = y1
cur_x = x
cur_y = y
last_cmd_was_curve = false
last_cmd_was_quad = true
}
QuadraticCurveToRel(dx1, dy1, dx, dy) => {
let x1 = cur_x + dx1
let y1 = cur_y + dy1
let x = cur_x + dx
let y = cur_y + dy
let mut sx = x1 - cur_x
let mut sy = y1 - cur_y
if (sx * sx + sy * sy).sqrt() < eps {
sx = x - cur_x
sy = y - cur_y
}
let mut ex = x - x1
let mut ey = y - y1
if (ex * ex + ey * ey).sqrt() < eps {
ex = x - cur_x
ey = y - cur_y
}
add_segment(points, cur_x, cur_y, x, y, sx, sy, ex, ey, eps)
last_ctrl_x = x1
last_ctrl_y = y1
cur_x = x
cur_y = y
last_cmd_was_curve = false
last_cmd_was_quad = true
}
SmoothQuadraticCurveTo(x, y) => {
let x1 = if last_cmd_was_quad {
2.0 * cur_x - last_ctrl_x
} else {
cur_x
}
let y1 = if last_cmd_was_quad {
2.0 * cur_y - last_ctrl_y
} else {
cur_y
}
let mut sx = x1 - cur_x
let mut sy = y1 - cur_y
if (sx * sx + sy * sy).sqrt() < eps {
sx = x - cur_x
sy = y - cur_y
}
let mut ex = x - x1
let mut ey = y - y1
if (ex * ex + ey * ey).sqrt() < eps {
ex = x - cur_x
ey = y - cur_y
}
add_segment(points, cur_x, cur_y, x, y, sx, sy, ex, ey, eps)
last_ctrl_x = x1
last_ctrl_y = y1
cur_x = x
cur_y = y
last_cmd_was_curve = false
last_cmd_was_quad = true
}
SmoothQuadraticCurveToRel(dx, dy) => {
let x1 = if last_cmd_was_quad {
2.0 * cur_x - last_ctrl_x
} else {
cur_x
}
let y1 = if last_cmd_was_quad {
2.0 * cur_y - last_ctrl_y
} else {
cur_y
}
let x = cur_x + dx
let y = cur_y + dy
let mut sx = x1 - cur_x
let mut sy = y1 - cur_y
if (sx * sx + sy * sy).sqrt() < eps {
sx = x - cur_x
sy = y - cur_y
}
let mut ex = x - x1
let mut ey = y - y1
if (ex * ex + ey * ey).sqrt() < eps {
ex = x - cur_x
ey = y - cur_y
}
add_segment(points, cur_x, cur_y, x, y, sx, sy, ex, ey, eps)
last_ctrl_x = x1
last_ctrl_y = y1
cur_x = x
cur_y = y
last_cmd_was_curve = false
last_cmd_was_quad = true
}
ArcTo(rx, ry, rotation, large_arc, sweep, x, y) => {
let (sx, sy, ex, ey) = compute_arc_tangents(
cur_x, cur_y, rx, ry, rotation, large_arc, sweep, x, y,
)
add_segment(points, cur_x, cur_y, x, y, sx, sy, ex, ey, eps)
cur_x = x
cur_y = y
last_cmd_was_curve = false
last_cmd_was_quad = false
}
ArcToRel(rx, ry, rotation, large_arc, sweep, dx, dy) => {
let x = cur_x + dx
let y = cur_y + dy
let (sx, sy, ex, ey) = compute_arc_tangents(
cur_x, cur_y, rx, ry, rotation, large_arc, sweep, x, y,
)
add_segment(points, cur_x, cur_y, x, y, sx, sy, ex, ey, eps)
cur_x = x
cur_y = y
last_cmd_was_curve = false
last_cmd_was_quad = false
}
ClosePath => {
if points.length() > 0 {
let dx = start_x - cur_x
let dy = start_y - cur_y
let last_idx = points.length() - 1
record_out(points, last_idx, dx, dy, eps)
record_in(points, 0, dx, dy, eps)
cur_x = start_x
cur_y = start_y
closed = true
}
last_cmd_was_curve = false
last_cmd_was_quad = false
}
}
}
finish_subpath(subpaths, points, closed)
subpaths
}
///|
fn render_markers_for_path_commands(
commands : Array[PathCommand],
node : SVGNode,
transform : Transform,
ctx : RenderContext,
resources : RenderResources,
node_color : Color,
) -> Unit {
if node.marker_start is None &&
node.marker_mid is None &&
node.marker_end is None {
return
}
let subpaths = build_marker_subpaths(commands)
if subpaths.length() == 0 {
return
}
let eps = 0.0001
let (sx, sy) = transform.get_scale()
let skew = transform.a * transform.c + transform.b * transform.d
let use_transformed = (sx - sy).abs() > eps || skew.abs() > eps
let inv = transform.inverse()
fn to_local_angle(angle : Double) -> Double {
let dx = @math.cos(angle)
let dy = @math.sin(angle)
let lx = inv.a * dx + inv.c * dy
let ly = inv.b * dx + inv.d * dy
@math.atan2(ly, lx)
}
fn apply_vec(
dx : Double,
dy : Double,
t : Transform,
use_t : Bool,
) -> (Double, Double) {
if use_t {
(t.a * dx + t.c * dy, t.b * dx + t.d * dy)
} else {
(dx, dy)
}
}
fn normalize(
dx : Double,
dy : Double,
eps : Double,
) -> (Double, Double, Double) {
let len = (dx * dx + dy * dy).sqrt()
if len < eps {
(0.0, 0.0, len)
} else {
(dx / len, dy / len, len)
}
}
fn angle_from_vec(
dx : Double,
dy : Double,
use_t : Bool,
t : Transform,
) -> Double {
let (vx, vy) = apply_vec(dx, dy, t, use_t)
let ang = @math.atan2(vy, vx)
if use_t {
to_local_angle(ang)
} else {
ang
}
}
fn angle_mid_vec(
in_dx : Double,
in_dy : Double,
out_dx : Double,
out_dy : Double,
eps : Double,
) -> Double {
let (ux1, uy1, l1) = normalize(in_dx, in_dy, eps)
let (ux2, uy2, l2) = normalize(out_dx, out_dy, eps)
if l1 < eps && l2 < eps {
0.0
} else if l1 < eps {
@math.atan2(out_dy, out_dx)
} else if l2 < eps {
@math.atan2(in_dy, in_dx)
} else {
let sx = ux1 + ux2
let sy = uy1 + uy2
if sx * sx + sy * sy < eps * eps {
@math.atan2(out_dx, -out_dy)
} else {
@math.atan2(sy, sx)
}
}
}
let sub_last = subpaths.length() - 1
for sub_idx in 0..
match resources.markers.get(id) {
Some(marker) => {
let p = sub.points[i]
let angle = if i == 0 {
if p.has_in && p.has_out {
let (in_dx, in_dy) = apply_vec(
p.in_dx,
p.in_dy,
transform,
use_transformed,
)
let (out_dx, out_dy) = apply_vec(
p.out_dx,
p.out_dy,
transform,
use_transformed,
)
let ang = angle_mid_vec(in_dx, in_dy, out_dx, out_dy, eps)
if use_transformed {
to_local_angle(ang)
} else {
ang
}
} else if p.has_out {
angle_from_vec(p.out_dx, p.out_dy, use_transformed, transform)
} else if p.has_in {
angle_from_vec(p.in_dx, p.in_dy, use_transformed, transform)
} else {
0.0
}
} else if i == last {
if p.has_in {
angle_from_vec(p.in_dx, p.in_dy, use_transformed, transform)
} else if p.has_out {
angle_from_vec(p.out_dx, p.out_dy, use_transformed, transform)
} else {
0.0
}
} else if p.has_in && p.has_out {
let (in_dx, in_dy) = apply_vec(
p.in_dx,
p.in_dy,
transform,
use_transformed,
)
let (out_dx, out_dy) = apply_vec(
p.out_dx,
p.out_dy,
transform,
use_transformed,
)
let ang = angle_mid_vec(in_dx, in_dy, out_dx, out_dy, eps)
if use_transformed {
to_local_angle(ang)
} else {
ang
}
} else if p.has_in {
angle_from_vec(p.in_dx, p.in_dy, use_transformed, transform)
} else if p.has_out {
angle_from_vec(p.out_dx, p.out_dy, use_transformed, transform)
} else {
0.0
}
render_marker_instance(
marker,
p.x,
p.y,
angle,
node,
transform,
ctx,
resources,
node_color,
)
}
None => ()
}
None => ()
}
}
}
}
///|
fn is_closed_polyline(points : Array[(Int, Int)]) -> Bool {
if points.length() < 2 {
false
} else {
let last = points.length() - 1
let (x0, y0) = points[0]
let (x1, y1) = points[last]
x0 == x1 && y0 == y1
}
}
///|
fn polygon_area_int(points : Array[(Int, Int)]) -> Int {
if points.length() < 3 {
return 0
}
let mut area = 0
let last = points.length() - 1
for i in 0.. Unit {
if stroke_px <= 1 || points.length() < 2 {
return
}
if closed {
let mut count = points.length()
if is_closed_polyline(points) {
count = count - 1
}
if count < 2 {
return
}
for i in 0.. Unit {
draw_line_cap(p0, p1, width, linecap, color, setter, true)
}
///|
fn draw_line_cap_end(
p0 : (Int, Int),
p1 : (Int, Int),
width : Int,
linecap : LineCap,
color : Color,
setter : PixelSetter,
) -> Unit {
draw_line_cap(p0, p1, width, linecap, color, setter, false)
}
///|
fn draw_line_cap(
p0 : (Int, Int),
p1 : (Int, Int),
width : Int,
linecap : LineCap,
color : Color,
setter : PixelSetter,
is_start : Bool,
) -> Unit {
if width <= 1 {
return
}
match linecap {
Butt => ()
Round => {
let (x, y) = if is_start { p0 } else { p1 }
raster_circle_fill(x, y, width / 2, color, setter)
}
Square => {
let (x0, y0) = p0
let (x1, y1) = p1
let dx = (x1 - x0).to_double()
let dy = (y1 - y0).to_double()
let len = (dx * dx + dy * dy).sqrt()
if len < 0.001 {
raster_circle_fill(x0, y0, width / 2, color, setter)
return
}
let ux = dx / len
let uy = dy / len
let half_w = width / 2
let ex = (ux * half_w.to_double()).to_int()
let ey = (uy * half_w.to_double()).to_int()
if is_start {
raster_thick_line(x0 - ex, y0 - ey, x0, y0, width, color, setter)
} else {
raster_thick_line(x1, y1, x1 + ex, y1 + ey, width, color, setter)
}
}
}
}
///|
fn draw_line_join(
p0 : (Int, Int),
p1 : (Int, Int),
p2 : (Int, Int),
width : Int,
linejoin : LineJoin,
miterlimit : Double,
color : Color,
setter : PixelSetter,
) -> Unit {
if width <= 1 {
return
}
let (x0, y0) = p0
let (x1, y1) = p1
let (x2, y2) = p2
let dx0 = (x1 - x0).to_double()
let dy0 = (y1 - y0).to_double()
let dx1 = (x2 - x1).to_double()
let dy1 = (y2 - y1).to_double()
let len0 = (dx0 * dx0 + dy0 * dy0).sqrt()
let len1 = (dx1 * dx1 + dy1 * dy1).sqrt()
if len0 < 0.001 || len1 < 0.001 {
return
}
let ux0 = dx0 / len0
let uy0 = dy0 / len0
let ux1 = dx1 / len1
let uy1 = dy1 / len1
let cross = ux0 * uy1 - uy0 * ux1
if cross.abs() < 0.0001 {
return
}
let side = select_join_side(x1, y1, ux0, uy0, ux1, uy1, width)
match linejoin {
Round => raster_circle_fill(x1, y1, width / 2, color, setter)
Bevel =>
draw_bevel_join(x1, y1, ux0, uy0, ux1, uy1, side, width, color, setter)
Miter =>
draw_miter_join(
x1, y1, ux0, uy0, ux1, uy1, side, width, miterlimit, color, setter,
)
}
}
///|
fn select_join_side(
x1 : Int,
y1 : Int,
ux0 : Double,
uy0 : Double,
ux1 : Double,
uy1 : Double,
width : Int,
) -> Double {
let half = width.to_double() / 2.0
let side_a = 1.0
let side_b = -1.0
let len_a = compute_miter_len_for_side(
x1, y1, ux0, uy0, ux1, uy1, side_a, half,
)
let len_b = compute_miter_len_for_side(
x1, y1, ux0, uy0, ux1, uy1, side_b, half,
)
if len_a < 0.0 && len_b < 0.0 {
side_a
} else if len_a < 0.0 {
side_b
} else if len_b < 0.0 {
side_a
} else if len_a >= len_b {
side_a
} else {
side_b
}
}
///|
fn compute_miter_len_for_side(
x1 : Int,
y1 : Int,
ux0 : Double,
uy0 : Double,
ux1 : Double,
uy1 : Double,
side : Double,
half : Double,
) -> Double {
let ox0 = x1.to_double() + side * -uy0 * half
let oy0 = y1.to_double() + side * ux0 * half
let ox1 = x1.to_double() + side * -uy1 * half
let oy1 = y1.to_double() + side * ux1 * half
let denom = ux0 * uy1 - uy0 * ux1
if denom.abs() < 0.0001 {
return -1.0
}
let t = ((ox1 - ox0) * uy1 - (oy1 - oy0) * ux1) / denom
let ix = ox0 + t * ux0
let iy = oy0 + t * uy0
let dx = ix - x1.to_double()
let dy = iy - y1.to_double()
(dx * dx + dy * dy).sqrt() / half
}
///|
fn draw_bevel_join(
x1 : Int,
y1 : Int,
ux0 : Double,
uy0 : Double,
ux1 : Double,
uy1 : Double,
side : Double,
width : Int,
color : Color,
setter : PixelSetter,
) -> Unit {
let half = width.to_double() / 2.0
let ox0 = x1.to_double() + side * -uy0 * half
let oy0 = y1.to_double() + side * ux0 * half
let ox1 = x1.to_double() + side * -uy1 * half
let oy1 = y1.to_double() + side * ux1 * half
let points : Array[(Int, Int)] = [
(ox0.to_int(), oy0.to_int()),
(ox1.to_int(), oy1.to_int()),
(x1, y1),
]
raster_polygon_fill(points, color, setter)
}
///|
fn draw_miter_join(
x1 : Int,
y1 : Int,
ux0 : Double,
uy0 : Double,
ux1 : Double,
uy1 : Double,
side : Double,
width : Int,
miterlimit : Double,
color : Color,
setter : PixelSetter,
) -> Unit {
let half = width.to_double() / 2.0
let ox0 = x1.to_double() + side * -uy0 * half
let oy0 = y1.to_double() + side * ux0 * half
let ox1 = x1.to_double() + side * -uy1 * half
let oy1 = y1.to_double() + side * ux1 * half
// Intersection of offset lines
let denom = ux0 * uy1 - uy0 * ux1
if denom.abs() < 0.0001 {
draw_bevel_join(x1, y1, ux0, uy0, ux1, uy1, side, width, color, setter)
return
}
let px = ox0
let py = oy0
let qx = ox1
let qy = oy1
let t = ((qx - px) * uy1 - (qy - py) * ux1) / denom
let ix = px + t * ux0
let iy = py + t * uy0
let dx = ix - x1.to_double()
let dy = iy - y1.to_double()
let miter_len = (dx * dx + dy * dy).sqrt() / half
if miter_len > miterlimit {
draw_bevel_join(x1, y1, ux0, uy0, ux1, uy1, side, width, color, setter)
return
}
let points : Array[(Int, Int)] = [
(ox0.to_int(), oy0.to_int()),
(ix.to_int(), iy.to_int()),
(ox1.to_int(), oy1.to_int()),
]
raster_polygon_fill(points, color, setter)
}
///|
fn render_polyline(
points : Array[(Double, Double)],
node : SVGNode,
transform : Transform,
ctx : RenderContext,
resources : RenderResources,
node_color : Color,
) -> Unit {
if points.length() < 2 {
return
}
let transformed = points.map(fn(p) {
let (x, y) = transform.apply(p.0, p.1)
(x.to_int(), y.to_int())
})
let (sx, sy) = transform.get_scale()
let stroke_px = if node.stroke.width > 0.0 {
ceil_to_int(node.stroke.width * (sx + sy) / 2.0)
} else {
0
}
fn draw_stroke() -> Unit {
match resolve_paint_for_render(node.stroke.paint, node_color, resources) {
ResolvedPaint::SolidColor(color) =>
if node.stroke_opacity > 0.0 && stroke_px > 0 {
let stroke_color = apply_opacity(
color,
node.stroke_opacity * node.opacity,
)
if stroke_px <= 1 {
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)
}
} else {
for i = 0; i < transformed.length() - 1; i = i + 1 {
let (x1, y1) = transformed[i]
let (x2, y2) = transformed[i + 1]
raster_thick_line(
x1,
y1,
x2,
y2,
stroke_px,
stroke_color,
ctx.setter,
)
}
draw_polyline_joins_caps(
transformed,
stroke_px,
node.stroke.linecap,
node.stroke.linejoin,
node.stroke.miterlimit,
stroke_color,
ctx.setter,
false,
)
}
}
_ => ()
}
}
fn draw_markers() -> Unit {
render_markers_for_polylines(
[points],
node,
transform,
ctx,
resources,
node_color,
)
}
render_paint_order(node.paint_order, () => (), draw_stroke, draw_markers)
}
///|
fn render_polygon(
points : Array[(Double, Double)],
node : SVGNode,
transform : Transform,
ctx : RenderContext,
node_color : Color,
resources : RenderResources,
) -> Unit {
if points.length() < 3 {
return
}
let transformed = points.map(fn(p) {
let (x, y) = transform.apply(p.0, p.1)
(x.to_int(), y.to_int())
})
let (sx, sy) = transform.get_scale()
let stroke_px = if node.stroke.width > 0.0 {
ceil_to_int(node.stroke.width * (sx + sy) / 2.0)
} else {
0
}
fn draw_fill() -> Unit {
match resolve_paint_for_render(node.fill, node_color, resources) {
ResolvedPaint::SolidColor(color) =>
if node.fill_opacity > 0.0 {
let fill_color = apply_opacity(
color,
node.fill_opacity * node.opacity,
)
raster_polygon_fill_rule(
transformed,
fill_color,
node.fill_rule,
ctx.setter,
)
}
ResolvedPaint::Pattern(pattern) =>
if node.fill_opacity > 0.0 {
render_pattern_fill(
node.shape,
transform,
ctx,
pattern,
node.fill_opacity * node.opacity,
)
}
_ => ()
}
}
fn draw_stroke() -> Unit {
match resolve_paint_for_render(node.stroke.paint, node_color, resources) {
ResolvedPaint::SolidColor(color) =>
if node.stroke_opacity > 0.0 && stroke_px > 0 {
let stroke_color = apply_opacity(
color,
node.stroke_opacity * node.opacity,
)
if stroke_px <= 1 {
raster_polygon_stroke(transformed, stroke_color, ctx.setter)
} else {
let n = transformed.length()
for i in 0.. ()
}
}
fn draw_markers() -> Unit {
render_markers_for_polylines(
[points],
node,
transform,
ctx,
resources,
node_color,
)
}
render_paint_order(node.paint_order, draw_fill, draw_stroke, draw_markers)
}
///|
fn render_path(
commands : Array[PathCommand],
node : SVGNode,
transform : Transform,
ctx : RenderContext,
node_color : Color,
resources : RenderResources,
) -> Unit {
// Convert to polylines with transform
let polylines = path_to_polylines(commands, ctx.flatness)
let transformed_polylines : Array[Array[(Int, Int)]] = []
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())
})
transformed_polylines.push(transformed)
}
fn draw_fill() -> Unit {
match resolve_paint_for_render(node.fill, node_color, resources) {
ResolvedPaint::SolidColor(color) =>
if node.fill_opacity > 0.0 {
let fill_color = apply_opacity(
color,
node.fill_opacity * node.opacity,
)
let fill_polylines : Array[Array[(Int, Int)]] = []
for polyline in transformed_polylines {
if polyline.length() >= 3 {
let area = polygon_area_int(polyline)
if area == 0 {
continue
}
fill_polylines.push(polyline)
}
}
if fill_polylines.length() > 0 {
raster_polygons_fill_rule(
fill_polylines,
fill_color,
node.fill_rule,
ctx.setter,
)
}
}
ResolvedPaint::Pattern(pattern) =>
if node.fill_opacity > 0.0 {
render_pattern_fill(
node.shape,
transform,
ctx,
pattern,
node.fill_opacity * node.opacity,
)
}
_ => ()
}
}
fn draw_stroke() -> Unit {
let (sx, sy) = transform.get_scale()
let stroke_px = if node.stroke.width > 0.0 {
ceil_to_int(node.stroke.width * (sx + sy) / 2.0)
} else {
0
}
for polyline in transformed_polylines {
match resolve_paint_for_render(node.stroke.paint, node_color, resources) {
ResolvedPaint::SolidColor(color) =>
if node.stroke_opacity > 0.0 && stroke_px > 0 {
let stroke_color = apply_opacity(
color,
node.stroke_opacity * node.opacity,
)
if stroke_px <= 1 {
raster_polyline(polyline, stroke_color, ctx.setter)
} else {
for i = 0; i < polyline.length() - 1; i = i + 1 {
let (x1, y1) = polyline[i]
let (x2, y2) = polyline[i + 1]
raster_thick_line(
x1,
y1,
x2,
y2,
stroke_px,
stroke_color,
ctx.setter,
)
}
let closed = is_closed_polyline(polyline)
draw_polyline_joins_caps(
polyline,
stroke_px,
node.stroke.linecap,
node.stroke.linejoin,
node.stroke.miterlimit,
stroke_color,
ctx.setter,
closed,
)
}
}
_ => ()
}
}
}
fn draw_markers() -> Unit {
render_markers_for_path_commands(
commands, node, transform, ctx, resources, node_color,
)
}
render_paint_order(node.paint_order, draw_fill, draw_stroke, draw_markers)
}
///|
fn render_text(
x : Double,
y : Double,
text : String,
font_size : Double,
node : SVGNode,
transform : Transform,
ctx : RenderContext,
node_color : Color,
resources : RenderResources,
) -> 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
let fill_paint = resolve_paint_for_render(node.fill, node_color, resources)
match ctx.text_to_paths {
Some(to_paths) => {
// Use font callback: render each character as path commands
let fill_color : Color? = match fill_paint {
ResolvedPaint::SolidColor(color) =>
if node.fill_opacity > 0.0 {
Some(apply_opacity(color, node.fill_opacity * node.opacity))
} else {
None
}
_ => None
}
let mut cursor_x = tx
for i in 0..
// Fallback: bitmap font
match fill_paint {
ResolvedPaint::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,
)
}
ResolvedPaint::Pattern(pattern) =>
if node.fill_opacity > 0.0 {
render_pattern_fill(
node.shape,
transform,
ctx,
pattern,
node.fill_opacity * node.opacity,
)
}
_ => ()
}
}
}
///|
/// Translate path commands by (dx, dy) offset
fn translate_path_commands(
commands : Array[PathCommand],
dx : Double,
dy : Double,
) -> Array[PathCommand] {
let result : Array[PathCommand] = []
for cmd in commands {
let translated : PathCommand = match cmd {
MoveTo(x, y) => MoveTo(x + dx, y + dy)
LineTo(x, y) => LineTo(x + dx, y + dy)
QuadraticCurveTo(cx, cy, x, y) =>
QuadraticCurveTo(cx + dx, cy + dy, x + dx, y + dy)
CurveTo(cx1, cy1, cx2, cy2, x, y) =>
CurveTo(cx1 + dx, cy1 + dy, cx2 + dx, cy2 + dy, x + dx, y + dy)
ClosePath => ClosePath
other => other
}
result.push(translated)
}
result
}
///|
/// 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, text_to_paths: 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),
text_to_paths: None,
}
}
///|
/// Create a RenderContext with font callback for text rendering
pub fn RenderContext::with_font(
setter : PixelSetter,
width : Int,
height : Int,
text_to_paths : (Int, Double) -> (Array[PathCommand], Double),
) -> RenderContext {
{
setter,
width,
height,
flatness: 0.5,
clip: None,
text_to_paths: Some(text_to_paths),
}
}
///|
/// 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),
text_to_paths: None,
}
}