///|
fn apply_clip_path(
ctx : RenderState,
clip : ClipPath,
element_transform : Transform,
bbox_local : BoundingBox,
resources : RenderResources,
) -> RenderState {
let mask = build_clip_path_coverage(
clip,
element_transform,
bbox_local,
ctx.width,
ctx.height,
resources,
0,
)
if mask.width == 0 || mask.height == 0 {
let setter : ColorSink = { set: fn(_, _, _) { () } }
return { ..ctx, setter, }
}
let inner = ctx.setter
let setter : ColorSink = {
set: fn(x, y, color) {
let coverage = mask.get_device(x, y)
if coverage > 0 {
inner.pixel(
x,
y,
if coverage == 65535 {
color
} else {
apply_coverage(color, coverage)
},
)
}
},
}
{ ..ctx, setter, }
}
///|
fn clip_shape_contours(
shape : Shape,
transform : Transform,
) -> Array[Array[(Double, Double)]] {
let commands = match shape {
Rect(x~, y~, width~, height~, rx~, ry~) =>
if width > 0.0 && height > 0.0 {
rect_path_commands(x, y, width, height, rx, ry)
} else {
[]
}
Circle(cx~, cy~, r~) =>
if r > 0.0 {
ellipse_path_commands(cx, cy, r, r)
} else {
[]
}
Ellipse(cx~, cy~, rx~, ry~) =>
if rx > 0.0 && ry > 0.0 {
ellipse_path_commands(cx, cy, rx, ry)
} else {
[]
}
Polyline(points~) | Polygon(points~) => points_path_commands(points, true)
Path(commands~) => commands
Text(x~, y~, text~, font_size~) =>
rect_path_commands(
x,
y - font_size,
font_size * 0.6 * text.length().to_double(),
font_size,
0.0,
0.0,
)
Line(..) | Image(..) | Group => []
}
let contours : Array[Array[(Double, Double)]] = []
for polyline in path_to_polylines(commands, device_path_flatness(transform)) {
contours.push(polyline.map(fn(point) { transform.apply(point.0, point.1) }))
}
contours
}
///|
fn build_clip_node_coverage(
node : SVGNode,
parent_transform : Transform,
width : Int,
height : Int,
resources : RenderResources,
depth : Int,
) -> CoverageMask {
if depth >= 32 {
return { origin_x: 0, origin_y: 0, width: 0, height: 0, values: [] }
}
let base_transform = parent_transform.multiply(node.transform)
let transform = match
(node.view_box, node.viewport_width, node.viewport_height) {
(Some(vb), Some(vw), Some(vh)) =>
base_transform.multiply(
vb.get_transform(vw, vh, node.preserve_aspect_ratio),
)
_ => base_transform
}
let mut result = build_coverage_mask(
clip_shape_contours(node.shape, transform),
node.clip_rule,
width,
height,
)
for child in node.children {
result = union_coverage_masks(
result,
build_clip_node_coverage(
child,
transform,
width,
height,
resources,
depth + 1,
),
)
}
match node.clip_path_id {
Some(id) =>
match resources.clips.get(id) {
Some(nested) => {
let local_bounds = compute_bounds_without_self_transform(
node,
Transform::identity(),
)
result = intersect_coverage_masks(
result,
build_clip_path_coverage(
nested,
transform,
local_bounds,
width,
height,
resources,
depth + 1,
),
)
}
None => ()
}
None => ()
}
result
}
///|
fn build_clip_path_coverage(
clip : ClipPath,
element_transform : Transform,
bbox_local : BoundingBox,
width : Int,
height : Int,
resources : RenderResources,
depth : Int,
) -> CoverageMask {
let base_transform = match clip.units {
UserSpaceOnUse => element_transform
ObjectBoundingBox => {
if bbox_local.is_empty() {
return { origin_x: 0, origin_y: 0, width: 0, height: 0, values: [] }
}
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)
if clip.content.is_empty() {
return build_coverage_mask(
clip_shape_contours(clip.shape, combined),
clip.clip_rule,
width,
height,
)
}
let mut result : CoverageMask = {
origin_x: 0,
origin_y: 0,
width: 0,
height: 0,
values: [],
}
for child in clip.content {
result = union_coverage_masks(
result,
build_clip_node_coverage(
child,
combined,
width,
height,
resources,
depth + 1,
),
)
}
result
}
///|
fn render_masked_node(
node : SVGNode,
parent_transform : Transform,
ctx : RenderState,
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)
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)
let mask_bounds_local = mask.get_mask_bounds(bbox_local)
let mask_region_transform = offset.multiply(element_transform)
let mask_ctx = match
apply_transformed_rect_clip(
mask_ctx, mask_bounds_local, mask_region_transform,
) {
Some(clipped) => clipped
None => return
}
let mask_parent = match mask.mask_content_units {
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)
}
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,
parent : RenderState,
) -> RenderState {
let setter = make_image_compositing_setter(image)
{
setter,
width,
height,
flatness: parent.flatness,
clip: None,
text_to_paths: parent.text_to_paths,
image_resolver: parent.image_resolver,
target_image: Some(image),
blend_pixel: parent.blend_pixel,
diagnostics: parent.diagnostics,
}
}
///|
fn make_image_compositing_setter(image : Image) -> ColorSink {
{
set: fn(x, y, source) {
let backdrop = image.get_pixel(x, y)
image.set_pixel(x, y, blend_colors(backdrop, source))
},
}
}
///|
fn blit_image_to_context(
image : Image,
ctx : RenderState,
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
}
}
///|