use bytemuck::{Pod, Zeroable};
use teksilo_canvas::render_frame::PaintData;
use teksilo_canvas::{DecorationRect, GlyphQuad, PathEntry, ShadowQuad, ShapeQuad, Transform2D};
#[inline]
fn srgb_to_linear(c: f32) -> f32 {
if c <= 0.04045 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
#[inline]
pub fn srgb_to_linear_rgba(c: [f32; 4]) -> [f32; 4] {
[
srgb_to_linear(c[0]),
srgb_to_linear(c[1]),
srgb_to_linear(c[2]),
c[3],
]
}
pub const QUAD_FLAG_COLOR_GLYPH: u32 = 1;
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub struct QuadVertex {
pub position: [f32; 2],
pub tex_coord: [f32; 2],
pub color: [f32; 4],
pub flags: u32,
pub _pad: u32,
}
const GLYPH_SNAP_AXIS_EPS: f32 = 1e-4;
const GLYPH_SNAP_SIZE_EPS: f32 = 1.0 / 16.0;
#[inline]
fn apply_affine(p: [f32; 2], t: &Transform2D) -> [f32; 2] {
let [a, b, c, d, tx, ty] = t.m;
[a * p[0] + c * p[1] + tx, b * p[0] + d * p[1] + ty]
}
impl QuadVertex {
pub fn from_glyph_quad_transformed(
quad: &GlyphQuad,
scale_factor: f32,
atlas_width: u32,
atlas_height: u32,
transform: &Transform2D,
) -> [QuadVertex; 4] {
let [x, y, w, h] = quad.screen;
let [ax, ay, aw, ah] = quad.atlas;
let sx = x * scale_factor;
let sy = y * scale_factor;
let sw = w * scale_factor;
let sh = h * scale_factor;
let aw_f = atlas_width.max(1) as f32;
let ah_f = atlas_height.max(1) as f32;
let u0 = ax / aw_f;
let v0 = ay / ah_f;
let u1 = (ax + aw) / aw_f;
let v1 = (ay + ah) / ah_f;
let [a, b, c, d, _, _] = transform.m;
let axis_aligned = b.abs() < GLYPH_SNAP_AXIS_EPS && c.abs() < GLYPH_SNAP_AXIS_EPS;
let one_to_one = axis_aligned
&& (a * sw - aw).abs() < GLYPH_SNAP_SIZE_EPS
&& (d * sh - ah).abs() < GLYPH_SNAP_SIZE_EPS;
let positions: [[f32; 2]; 4] = if one_to_one {
let [ox, oy] = apply_affine([sx, sy], transform);
let ox = ox.round();
let oy = oy.round();
[[ox, oy], [ox + aw, oy], [ox + aw, oy + ah], [ox, oy + ah]]
} else {
[
apply_affine([sx, sy], transform),
apply_affine([sx + sw, sy], transform),
apply_affine([sx + sw, sy + sh], transform),
apply_affine([sx, sy + sh], transform),
]
};
let flags = if quad.is_color {
QUAD_FLAG_COLOR_GLYPH
} else {
0
};
let color = srgb_to_linear_rgba(quad.color);
[
QuadVertex {
position: positions[0],
tex_coord: [u0, v0],
color,
flags,
_pad: 0,
},
QuadVertex {
position: positions[1],
tex_coord: [u1, v0],
color,
flags,
_pad: 0,
},
QuadVertex {
position: positions[2],
tex_coord: [u1, v1],
color,
flags,
_pad: 0,
},
QuadVertex {
position: positions[3],
tex_coord: [u0, v1],
color,
flags,
_pad: 0,
},
]
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub struct RectVertex {
pub position: [f32; 2],
pub color: [f32; 4],
}
impl RectVertex {
pub fn from_decoration(rect: &DecorationRect, scale_factor: f32) -> [RectVertex; 4] {
let [x, y, w, h] = rect.rect;
let sx = x * scale_factor;
let sy = y * scale_factor;
let sw = w * scale_factor;
let sh = h * scale_factor;
[
RectVertex {
position: [sx, sy],
color: srgb_to_linear_rgba(rect.color),
},
RectVertex {
position: [sx + sw, sy],
color: srgb_to_linear_rgba(rect.color),
},
RectVertex {
position: [sx + sw, sy + sh],
color: srgb_to_linear_rgba(rect.color),
},
RectVertex {
position: [sx, sy + sh],
color: srgb_to_linear_rgba(rect.color),
},
]
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub struct SdfVertex {
pub position: [f32; 2],
pub local_uv: [f32; 2],
pub color: [f32; 4],
pub corner_radii: [f32; 4],
pub shape_params: [f32; 4],
pub gradient_geo: [f32; 4],
pub gradient_color0: [f32; 4],
pub gradient_color1: [f32; 4],
pub gradient_color2: [f32; 4],
pub gradient_color3: [f32; 4],
pub gradient_offsets: [f32; 4],
}
impl SdfVertex {
pub fn from_shape_quad(shape: &ShapeQuad, scale_factor: f32) -> [SdfVertex; 4] {
Self::shape_quad_verts(shape, scale_factor, shape.stroke_width * scale_factor)
}
pub fn from_shape_quad_cosmetic(
shape: &ShapeQuad,
scale_factor: f32,
zoom: f32,
) -> [SdfVertex; 4] {
let zoom = zoom.max(1e-3);
Self::shape_quad_verts(
shape,
scale_factor,
shape.stroke_width * scale_factor / zoom,
)
}
fn shape_quad_verts(shape: &ShapeQuad, scale_factor: f32, stroke_px: f32) -> [SdfVertex; 4] {
let [x, y, w, h] = shape.screen;
let sx = x * scale_factor;
let sy = y * scale_factor;
let sw = w * scale_factor;
let sh = h * scale_factor;
let (paint_type, gradient_geo, colors, offsets) =
encode_paint_data(&shape.paint_data, w, h);
let stroke = stroke_px;
let pad = stroke * 0.5 + 1.0;
let u_pad = if sw > 0.0 { pad / sw } else { 0.0 };
let v_pad = if sh > 0.0 { pad / sh } else { 0.0 };
let params = [sw, sh, stroke, paint_type as f32];
let scaled_corner_radii = [
shape.corner_radii[0] * scale_factor,
shape.corner_radii[1] * scale_factor,
shape.corner_radii[2] * scale_factor,
shape.corner_radii[3] * scale_factor,
];
let base = SdfVertex {
position: [0.0, 0.0],
local_uv: [0.0, 0.0],
color: srgb_to_linear_rgba(shape.color),
corner_radii: scaled_corner_radii,
shape_params: params,
gradient_geo,
gradient_color0: srgb_to_linear_rgba(colors[0]),
gradient_color1: srgb_to_linear_rgba(colors[1]),
gradient_color2: srgb_to_linear_rgba(colors[2]),
gradient_color3: srgb_to_linear_rgba(colors[3]),
gradient_offsets: offsets,
};
[
SdfVertex {
position: [sx - pad, sy - pad],
local_uv: [-u_pad, -v_pad],
..base
},
SdfVertex {
position: [sx + sw + pad, sy - pad],
local_uv: [1.0 + u_pad, -v_pad],
..base
},
SdfVertex {
position: [sx + sw + pad, sy + sh + pad],
local_uv: [1.0 + u_pad, 1.0 + v_pad],
..base
},
SdfVertex {
position: [sx - pad, sy + sh + pad],
local_uv: [-u_pad, 1.0 + v_pad],
..base
},
]
}
}
pub(crate) fn encode_paint_data(
paint_data: &PaintData,
width: f32,
height: f32,
) -> (u32, [f32; 4], [[f32; 4]; 4], [f32; 4]) {
let zero_colors = [[0.0; 4]; 4];
let zero_offsets = [0.0; 4];
match paint_data {
PaintData::Solid => (0, [0.0; 4], zero_colors, zero_offsets),
PaintData::LinearGradient { start, end, stops } => {
let geo = [
start[0] / width,
start[1] / height,
end[0] / width,
end[1] / height,
];
let (colors, offsets) = encode_stops(stops);
(1, geo, colors, offsets)
}
PaintData::RadialGradient {
center,
radius,
stops,
} => {
let aspect = height / width.max(0.0001);
let geo = [
center[0] / width,
center[1] / height,
*radius / width,
aspect,
];
let (colors, offsets) = encode_stops(stops);
(2, geo, colors, offsets)
}
PaintData::ConicGradient {
center,
start_angle,
stops,
} => {
let geo = [center[0] / width, center[1] / height, *start_angle, 0.0];
let (colors, offsets) = encode_stops(stops);
(3, geo, colors, offsets)
}
}
}
pub(crate) fn encode_stops(stops: &[teksilo_canvas::GradientStop]) -> ([[f32; 4]; 4], [f32; 4]) {
let mut colors = [[0.0f32; 4]; 4];
let mut offsets = [0.0f32; 4];
for (i, stop) in stops.iter().take(4).enumerate() {
colors[i] = stop.color.to_array();
offsets[i] = stop.offset;
}
if !stops.is_empty() {
let last_idx = stops.len().min(4) - 1;
for i in stops.len()..4 {
colors[i] = colors[last_idx];
offsets[i] = offsets[last_idx];
}
}
(colors, offsets)
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub struct PathGradientVertex {
pub position: [f32; 2],
pub tex_coord: [f32; 2],
pub local_uv: [f32; 2],
pub paint_type: u32,
pub _pad: u32,
pub gradient_geo: [f32; 4],
pub gradient_color0: [f32; 4],
pub gradient_color1: [f32; 4],
pub gradient_color2: [f32; 4],
pub gradient_color3: [f32; 4],
pub gradient_offsets: [f32; 4],
}
impl PathGradientVertex {
pub(crate) fn from_path_entry(
entry: &PathEntry,
region: &crate::path_atlas::AtlasRegion,
scale_factor: f32,
atlas_width: u32,
atlas_height: u32,
opacity: f32,
transform: &Transform2D,
) -> [PathGradientVertex; 4] {
let [bx, by, bw, bh] = entry.bounds;
let sx = bx * scale_factor;
let sy = by * scale_factor;
let sw = bw * scale_factor;
let sh = bh * scale_factor;
let aw = atlas_width.max(1) as f32;
let ah = atlas_height.max(1) as f32;
let u0 = region.x as f32 / aw;
let v0 = region.y as f32 / ah;
let u1 = (region.x + region.w) as f32 / aw;
let v1 = (region.y + region.h) as f32 / ah;
let (paint_type, gradient_geo, raw_colors, gradient_offsets) =
encode_paint_data(&entry.paint_data, bw, bh);
let colors: [[f32; 4]; 4] = std::array::from_fn(|i| {
let mut c = srgb_to_linear_rgba(raw_colors[i]);
c[3] *= opacity;
c
});
let positions = [
apply_affine([sx, sy], transform),
apply_affine([sx + sw, sy], transform),
apply_affine([sx + sw, sy + sh], transform),
apply_affine([sx, sy + sh], transform),
];
let tex_coords = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
let local_uvs: [[f32; 2]; 4] = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
std::array::from_fn(|i| PathGradientVertex {
position: positions[i],
tex_coord: tex_coords[i],
local_uv: local_uvs[i],
paint_type,
_pad: 0,
gradient_geo,
gradient_color0: colors[0],
gradient_color1: colors[1],
gradient_color2: colors[2],
gradient_color3: colors[3],
gradient_offsets,
})
}
}
pub const QUAD_INDICES: [u32; 6] = [0, 1, 2, 0, 2, 3];
pub fn generate_quad_indices(count: usize) -> Vec<u32> {
let mut indices = Vec::with_capacity(count * 6);
for i in 0..count {
let base = (i * 4) as u32;
for &offset in &QUAD_INDICES {
indices.push(base + offset);
}
}
indices
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub struct ShadowVertex {
pub position: [f32; 2],
pub local_uv: [f32; 2],
pub shadow_color: [f32; 4],
pub corner_radii: [f32; 4],
pub shadow_params: [f32; 4],
pub shape_offset: [f32; 4],
}
impl ShadowVertex {
pub fn from_shadow_quad(shadow: &ShadowQuad, scale_factor: f32) -> [ShadowVertex; 4] {
let [x, y, w, h] = shadow.screen;
let sx = x * scale_factor;
let sy = y * scale_factor;
let sw = w * scale_factor;
let sh = h * scale_factor;
let [sr_x, sr_y, sr_w, sr_h] = shadow.shape_rect;
let shape_w = sr_w * scale_factor;
let shape_h = sr_h * scale_factor;
let shadow_cx = sx + sw * 0.5;
let shadow_cy = sy + sh * 0.5;
let shape_cx = (sr_x + sr_w * 0.5) * scale_factor;
let shape_cy = (sr_y + sr_h * 0.5) * scale_factor;
let offset_x = shape_cx - shadow_cx;
let offset_y = shape_cy - shadow_cy;
let params = [
shape_w,
shape_h,
shadow.blur_radius * scale_factor,
shadow.spread * scale_factor,
];
let offset = [offset_x, offset_y, 0.0, 0.0];
let scaled_corner_radii = [
shadow.corner_radii[0] * scale_factor,
shadow.corner_radii[1] * scale_factor,
shadow.corner_radii[2] * scale_factor,
shadow.corner_radii[3] * scale_factor,
];
[
ShadowVertex {
position: [sx, sy],
local_uv: [0.0, 0.0],
shadow_color: srgb_to_linear_rgba(shadow.color),
corner_radii: scaled_corner_radii,
shadow_params: params,
shape_offset: offset,
},
ShadowVertex {
position: [sx + sw, sy],
local_uv: [1.0, 0.0],
shadow_color: srgb_to_linear_rgba(shadow.color),
corner_radii: scaled_corner_radii,
shadow_params: params,
shape_offset: offset,
},
ShadowVertex {
position: [sx + sw, sy + sh],
local_uv: [1.0, 1.0],
shadow_color: srgb_to_linear_rgba(shadow.color),
corner_radii: scaled_corner_radii,
shadow_params: params,
shape_offset: offset,
},
ShadowVertex {
position: [sx, sy + sh],
local_uv: [0.0, 1.0],
shadow_color: srgb_to_linear_rgba(shadow.color),
corner_radii: scaled_corner_radii,
shadow_params: params,
shape_offset: offset,
},
]
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub struct AnimQuadVertex {
pub position: [f32; 2],
pub uv: [f32; 2],
pub slot: u32,
pub _pad: u32,
}
impl AnimQuadVertex {
pub fn from_animated_quad(
draw: &teksilo_canvas::AnimatedQuadDraw,
scale_factor: f32,
) -> [AnimQuadVertex; 4] {
let [x, y, w, h] = draw.screen;
let sx = x * scale_factor;
let sy = y * scale_factor;
let sw = w * scale_factor;
let sh = h * scale_factor;
[
AnimQuadVertex {
position: [sx, sy],
uv: [0.0, 0.0],
slot: draw.slot,
_pad: 0,
},
AnimQuadVertex {
position: [sx + sw, sy],
uv: [1.0, 0.0],
slot: draw.slot,
_pad: 0,
},
AnimQuadVertex {
position: [sx + sw, sy + sh],
uv: [1.0, 1.0],
slot: draw.slot,
_pad: 0,
},
AnimQuadVertex {
position: [sx, sy + sh],
uv: [0.0, 1.0],
slot: draw.slot,
_pad: 0,
},
]
}
}
#[cfg(test)]
mod tests {
use super::*;
use teksilo_canvas::{DecorationKind, GradientStop, PaintData, ShapeKind, StrokeSpace};
use teksilo_tokens::Color;
fn glyph(screen: [f32; 4], atlas: [f32; 4], is_color: bool) -> GlyphQuad {
GlyphQuad {
screen,
atlas,
color: [1.0, 1.0, 1.0, 1.0],
is_color,
}
}
fn assert_pos_near(actual: [f32; 2], expected: [f32; 2]) {
assert!(
(actual[0] - expected[0]).abs() < 1e-3 && (actual[1] - expected[1]).abs() < 1e-3,
"position {actual:?} != expected {expected:?}"
);
}
#[test]
fn glyph_quad_to_vertices() {
let quad = glyph([10.0, 20.0, 30.0, 40.0], [0.0, 0.0, 64.0, 64.0], false);
let verts =
QuadVertex::from_glyph_quad_transformed(&quad, 1.0, 256, 256, &Transform2D::IDENTITY);
assert_eq!(verts.len(), 4);
assert_eq!(verts[0].position, [10.0, 20.0]);
assert_eq!(verts[1].position, [40.0, 20.0]); assert_eq!(verts[2].position, [40.0, 60.0]); assert_eq!(verts[0].tex_coord, [0.0, 0.0]);
assert_eq!(verts[2].tex_coord, [0.25, 0.25]);
}
#[test]
fn scale_factor_applied_to_glyph_coords() {
let quad = glyph([10.0, 20.0, 30.0, 40.0], [0.0, 0.0, 128.0, 128.0], false);
let verts =
QuadVertex::from_glyph_quad_transformed(&quad, 2.0, 256, 256, &Transform2D::IDENTITY);
assert_eq!(verts[0].position, [20.0, 40.0]);
assert_eq!(verts[1].position, [80.0, 40.0]);
}
#[test]
fn glyph_snap_identity_fractional_origin() {
let quad = glyph([10.3, 20.7, 30.0, 40.0], [0.0, 0.0, 30.0, 40.0], false);
let verts =
QuadVertex::from_glyph_quad_transformed(&quad, 1.0, 256, 256, &Transform2D::IDENTITY);
assert_eq!(verts[0].position, [10.0, 21.0]);
assert_eq!(verts[1].position, [40.0, 21.0]);
assert_eq!(verts[2].position, [40.0, 61.0]);
assert_eq!(verts[3].position, [10.0, 61.0]);
}
#[test]
fn glyph_snap_hidpi_scale_factor() {
let quad = glyph([5.7, 8.3, 16.0, 16.0], [0.0, 0.0, 32.0, 32.0], false);
let verts =
QuadVertex::from_glyph_quad_transformed(&quad, 2.0, 256, 256, &Transform2D::IDENTITY);
assert_eq!(verts[0].position, [11.0, 17.0]);
assert_eq!(verts[2].position, [43.0, 49.0]);
}
#[test]
fn glyph_snap_fractional_dpi() {
let quad = glyph([4.2, 7.8, 20.0, 20.0], [0.0, 0.0, 25.0, 25.0], false);
let verts =
QuadVertex::from_glyph_quad_transformed(&quad, 1.25, 256, 256, &Transform2D::IDENTITY);
assert_eq!(verts[0].position, [5.0, 10.0]);
assert_eq!(verts[2].position, [30.0, 35.0]);
}
#[test]
fn glyph_snap_exact_bucket_zoom() {
let quad = glyph([4.0, 8.0, 20.0, 20.0], [0.0, 0.0, 50.0, 50.0], false);
let zoom = Transform2D {
m: [1.25, 0.0, 0.0, 1.25, 3.3, 7.8],
};
let verts = QuadVertex::from_glyph_quad_transformed(&quad, 2.0, 256, 256, &zoom);
assert_eq!(verts[0].position, [13.0, 28.0]);
assert_eq!(verts[2].position, [63.0, 78.0]);
}
#[test]
fn glyph_no_snap_mid_bucket_residual() {
let quad = glyph([4.0, 8.0, 20.0, 20.0], [0.0, 0.0, 50.0, 50.0], false);
let zoom = Transform2D {
m: [1.1, 0.0, 0.0, 1.1, 3.3, 7.8],
};
let verts = QuadVertex::from_glyph_quad_transformed(&quad, 2.0, 256, 256, &zoom);
assert_pos_near(verts[0].position, [1.1 * 8.0 + 3.3, 1.1 * 16.0 + 7.8]);
assert_pos_near(verts[2].position, [1.1 * 48.0 + 3.3, 1.1 * 56.0 + 7.8]);
}
#[test]
fn glyph_no_snap_rotation() {
let quad = glyph([10.0, 20.0, 30.0, 40.0], [0.0, 0.0, 30.0, 40.0], false);
let (s, c) = (0.1_f32.sin(), 0.1_f32.cos());
let rot = Transform2D {
m: [c, s, -s, c, 0.0, 0.0],
};
let verts = QuadVertex::from_glyph_quad_transformed(&quad, 1.0, 256, 256, &rot);
assert_pos_near(
verts[0].position,
[c * 10.0 - s * 20.0, s * 10.0 + c * 20.0],
);
assert_pos_near(
verts[2].position,
[c * 40.0 - s * 60.0, s * 40.0 + c * 60.0],
);
}
#[test]
fn glyph_snap_translation_only_transform() {
let quad = glyph([10.3, 20.0, 30.0, 40.0], [0.0, 0.0, 30.0, 40.0], false);
let pan = Transform2D {
m: [1.0, 0.0, 0.0, 1.0, 5.7, 3.2],
};
let verts = QuadVertex::from_glyph_quad_transformed(&quad, 1.0, 256, 256, &pan);
assert_eq!(verts[0].position, [16.0, 23.0]);
assert_eq!(verts[2].position, [46.0, 63.0]);
}
#[test]
fn glyph_snap_color_emoji_flag_preserved() {
let quad = glyph([10.3, 20.7, 30.0, 40.0], [0.0, 0.0, 30.0, 40.0], true);
let verts =
QuadVertex::from_glyph_quad_transformed(&quad, 1.0, 256, 256, &Transform2D::IDENTITY);
assert_eq!(verts[0].position, [10.0, 21.0]);
for v in &verts {
assert_eq!(v.flags, QUAD_FLAG_COLOR_GLYPH);
}
}
#[test]
fn glyph_uvs_independent_of_snapping() {
let quad = glyph([10.3, 20.7, 30.0, 40.0], [16.0, 32.0, 30.0, 40.0], false);
let snapped =
QuadVertex::from_glyph_quad_transformed(&quad, 1.0, 256, 256, &Transform2D::IDENTITY);
let residual = Transform2D {
m: [1.1, 0.0, 0.0, 1.1, 0.0, 0.0],
};
let unsnapped = QuadVertex::from_glyph_quad_transformed(&quad, 1.0, 256, 256, &residual);
for (a, b) in snapped.iter().zip(unsnapped.iter()) {
assert_eq!(a.tex_coord, b.tex_coord);
}
assert_eq!(snapped[0].tex_coord, [16.0 / 256.0, 32.0 / 256.0]);
assert_eq!(snapped[2].tex_coord, [46.0 / 256.0, 72.0 / 256.0]);
}
#[test]
fn decoration_rect_to_vertices() {
let rect = DecorationRect {
rect: [0.0, 0.0, 100.0, 2.0],
color: [1.0, 0.0, 0.0, 1.0],
kind: DecorationKind::FocusRing,
};
let verts = RectVertex::from_decoration(&rect, 1.0);
assert_eq!(verts.len(), 4);
assert_eq!(verts[0].position, [0.0, 0.0]);
assert_eq!(verts[2].position, [100.0, 2.0]);
}
#[test]
fn shape_quad_to_sdf_vertices() {
let shape = ShapeQuad {
screen: [0.0, 0.0, 100.0, 40.0],
color: [0.0, 0.5, 0.0, 1.0],
shape: ShapeKind::RoundedRect,
stroke_width: 0.0,
stroke_space: StrokeSpace::Logical,
corner_radii: [6.0, 6.0, 6.0, 6.0],
paint_data: PaintData::Solid,
};
let verts = SdfVertex::from_shape_quad(&shape, 1.0);
assert_eq!(verts.len(), 4);
assert_eq!(verts[0].corner_radii, [6.0, 6.0, 6.0, 6.0]);
assert_eq!(verts[0].position, [-1.0, -1.0]);
assert_eq!(verts[2].position, [101.0, 41.0]);
assert!((verts[0].local_uv[0] - (-0.01)).abs() < 1e-5);
assert!((verts[0].local_uv[1] - (-0.025)).abs() < 1e-5);
assert!((verts[2].local_uv[0] - 1.01).abs() < 1e-5);
assert!((verts[2].local_uv[1] - 1.025).abs() < 1e-5);
}
#[test]
fn sdf_scale_factor() {
let shape = ShapeQuad {
screen: [10.0, 10.0, 100.0, 40.0],
color: [0.0, 0.0, 0.0, 1.0],
shape: ShapeKind::RoundedRect,
stroke_width: 2.0,
stroke_space: StrokeSpace::Logical,
corner_radii: [4.0; 4],
paint_data: PaintData::Solid,
};
let verts = SdfVertex::from_shape_quad(&shape, 2.0);
assert_eq!(verts[0].position, [17.0, 17.0]);
assert_eq!(verts[0].shape_params[2], 4.0); assert_eq!(verts[0].corner_radii, [8.0; 4]);
}
#[test]
fn sdf_circle_stays_circle_on_hidpi() {
let shape = ShapeQuad {
screen: [0.0, 0.0, 19.0, 19.0],
color: [0.0, 0.0, 0.0, 1.0],
shape: ShapeKind::RoundedRect,
stroke_width: 0.0,
stroke_space: StrokeSpace::Logical,
corner_radii: [9.5; 4],
paint_data: PaintData::Solid,
};
let verts = SdfVertex::from_shape_quad(&shape, 2.0);
assert_eq!(verts[0].shape_params[0], 38.0);
assert_eq!(verts[0].shape_params[1], 38.0);
assert_eq!(verts[0].corner_radii, [19.0; 4]);
}
#[test]
fn cosmetic_shape_stroke_param_is_inverse_zoom() {
let shape = ShapeQuad {
screen: [0.0, 0.0, 100.0, 100.0],
color: [0.0, 0.0, 0.0, 1.0],
shape: ShapeKind::RoundedRect,
stroke_width: 2.0,
stroke_space: StrokeSpace::Device,
corner_radii: [10.0; 4],
paint_data: PaintData::Solid,
};
let sf = 2.0;
let logical = SdfVertex::from_shape_quad(&shape, sf);
let z1 = SdfVertex::from_shape_quad_cosmetic(&shape, sf, 1.0);
let z2 = SdfVertex::from_shape_quad_cosmetic(&shape, sf, 2.0);
assert!((z1[0].shape_params[2] - logical[0].shape_params[2]).abs() < 1e-4);
assert!((z1[0].shape_params[2] - 4.0).abs() < 1e-4);
assert!((z2[0].shape_params[2] - 2.0).abs() < 1e-4);
assert_eq!(z1[0].shape_params[0], z2[0].shape_params[0]);
assert_eq!(z2[0].shape_params[0], 200.0);
}
#[test]
fn sdf_linear_gradient_encoding() {
let shape = ShapeQuad {
screen: [0.0, 0.0, 100.0, 50.0],
color: [1.0, 1.0, 1.0, 1.0],
shape: ShapeKind::RoundedRect,
stroke_width: 0.0,
stroke_space: StrokeSpace::Logical,
corner_radii: [0.0; 4],
paint_data: PaintData::LinearGradient {
start: [0.0, 0.0],
end: [100.0, 0.0],
stops: vec![
GradientStop {
offset: 0.0,
color: Color::RED,
},
GradientStop {
offset: 1.0,
color: Color::BLUE,
},
],
},
};
let verts = SdfVertex::from_shape_quad(&shape, 1.0);
assert!((verts[0].shape_params[3] - 1.0).abs() < 0.01);
assert!((verts[0].gradient_geo[0]).abs() < 0.01);
assert!((verts[0].gradient_geo[2] - 1.0).abs() < 0.01);
assert!((verts[0].gradient_color0[0] - 1.0).abs() < 0.01);
assert!((verts[0].gradient_offsets[0]).abs() < 0.01);
assert!((verts[0].gradient_offsets[1] - 1.0).abs() < 0.01);
}
#[test]
fn linear_gradient_endpoints_are_rect_local_not_absolute() {
let shape = ShapeQuad {
screen: [50.0, 100.0, 200.0, 200.0],
color: [1.0, 1.0, 1.0, 1.0],
shape: ShapeKind::RoundedRect,
stroke_width: 0.0,
stroke_space: StrokeSpace::Logical,
corner_radii: [0.0; 4],
paint_data: PaintData::LinearGradient {
start: [0.0, 0.0],
end: [0.0, 200.0],
stops: vec![
GradientStop {
offset: 0.0,
color: Color::new(0.0, 0.0, 0.0, 0.0),
},
GradientStop {
offset: 1.0,
color: Color::BLACK,
},
],
},
};
let verts = SdfVertex::from_shape_quad(&shape, 1.0);
assert!((verts[0].gradient_geo[0]).abs() < 1e-5, "start_uv.x");
assert!((verts[0].gradient_geo[1]).abs() < 1e-5, "start_uv.y");
assert!((verts[0].gradient_geo[2]).abs() < 1e-5, "end_uv.x");
assert!((verts[0].gradient_geo[3] - 1.0).abs() < 1e-5, "end_uv.y");
}
fn rasterize_for_test(entry: &PathEntry, atlas_size: u32) -> crate::path_atlas::AtlasRegion {
let mut atlas = crate::path_atlas::PathAtlas::new(atlas_size, atlas_size);
atlas.begin_frame();
atlas
.lookup_or_rasterize(
&entry.path,
&entry.stroke_style,
entry.fill_rule,
entry.bounds,
1.0,
1.0,
)
.expect("test path rasterizes")
}
fn gradient_path_entry(bounds_rect: teksilo_canvas::Rect, paint_data: PaintData) -> PathEntry {
use teksilo_canvas::{FillRule, StrokeStyle};
PathEntry {
path: teksilo_canvas::Path::rect(bounds_rect),
color: [1.0, 1.0, 1.0, 1.0],
stroke_style: StrokeStyle::solid(0.0),
fill_rule: FillRule::Winding,
bounds: bounds_rect.to_array(),
paint_data,
}
}
#[test]
fn path_gradient_linear_encoding() {
let bounds_rect = teksilo_canvas::Rect::new(0.0, 0.0, 100.0, 50.0);
let entry = gradient_path_entry(
bounds_rect,
PaintData::LinearGradient {
start: [0.0, 0.0],
end: [100.0, 0.0],
stops: vec![
GradientStop {
offset: 0.0,
color: Color::RED,
},
GradientStop {
offset: 1.0,
color: Color::BLUE,
},
],
},
);
let region = rasterize_for_test(&entry, 256);
let verts = PathGradientVertex::from_path_entry(
&entry,
®ion,
1.0,
256,
256,
1.0,
&Transform2D::IDENTITY,
);
assert_eq!(verts[0].paint_type, 1);
assert!((verts[0].gradient_geo[0]).abs() < 0.01);
assert!((verts[0].gradient_geo[2] - 1.0).abs() < 0.01);
assert!((verts[0].gradient_color0[0] - 1.0).abs() < 0.01);
assert!((verts[0].gradient_color0[1]).abs() < 0.01);
assert!((verts[0].gradient_offsets[0]).abs() < 0.01);
assert!((verts[0].gradient_offsets[1] - 1.0).abs() < 0.01);
assert_eq!(verts[0].local_uv, [0.0, 0.0]);
assert_eq!(verts[1].local_uv, [1.0, 0.0]);
assert_eq!(verts[2].local_uv, [1.0, 1.0]);
assert_eq!(verts[3].local_uv, [0.0, 1.0]);
}
#[test]
fn path_gradient_endpoints_are_rect_local_not_absolute() {
let bounds_rect = teksilo_canvas::Rect::new(50.0, 100.0, 200.0, 200.0);
let entry = gradient_path_entry(
bounds_rect,
PaintData::LinearGradient {
start: [0.0, 0.0],
end: [0.0, 200.0],
stops: vec![
GradientStop {
offset: 0.0,
color: Color::new(0.0, 0.0, 0.0, 0.0),
},
GradientStop {
offset: 1.0,
color: Color::BLACK,
},
],
},
);
let region = rasterize_for_test(&entry, 512);
let verts = PathGradientVertex::from_path_entry(
&entry,
®ion,
1.0,
512,
512,
1.0,
&Transform2D::IDENTITY,
);
assert!((verts[0].gradient_geo[0]).abs() < 1e-5, "start_uv.x");
assert!((verts[0].gradient_geo[1]).abs() < 1e-5, "start_uv.y");
assert!((verts[0].gradient_geo[2]).abs() < 1e-5, "end_uv.x");
assert!((verts[0].gradient_geo[3] - 1.0).abs() < 1e-5, "end_uv.y");
}
#[test]
fn path_gradient_opacity_folds_into_every_stop_alpha() {
let bounds_rect = teksilo_canvas::Rect::new(0.0, 0.0, 40.0, 20.0);
let entry = gradient_path_entry(
bounds_rect,
PaintData::LinearGradient {
start: [0.0, 0.0],
end: [40.0, 0.0],
stops: vec![
GradientStop {
offset: 0.0,
color: Color::RED,
},
GradientStop {
offset: 1.0,
color: Color::BLUE,
},
],
},
);
let region = rasterize_for_test(&entry, 128);
let full = PathGradientVertex::from_path_entry(
&entry,
®ion,
1.0,
128,
128,
1.0,
&Transform2D::IDENTITY,
);
let half = PathGradientVertex::from_path_entry(
&entry,
®ion,
1.0,
128,
128,
0.5,
&Transform2D::IDENTITY,
);
assert!((full[0].gradient_color0[3] - 1.0).abs() < 1e-5);
assert!((half[0].gradient_color0[3] - 0.5).abs() < 1e-5);
assert!((half[0].gradient_color1[3] - 0.5).abs() < 1e-5);
}
#[test]
fn generate_indices_for_multiple_quads() {
let indices = generate_quad_indices(2);
assert_eq!(indices.len(), 12);
assert_eq!(&indices[0..6], &[0, 1, 2, 0, 2, 3]);
assert_eq!(&indices[6..12], &[4, 5, 6, 4, 6, 7]);
}
#[test]
fn shadow_quad_to_vertices() {
let shadow = ShadowQuad {
screen: [0.0, 0.0, 120.0, 60.0],
color: [0.0, 0.0, 0.0, 0.3],
corner_radii: [6.0; 4],
shape_rect: [10.0, 8.0, 100.0, 40.0],
blur_radius: 4.0,
spread: 0.0,
};
let verts = ShadowVertex::from_shadow_quad(&shadow, 1.0);
assert_eq!(verts.len(), 4);
assert_eq!(verts[0].position, [0.0, 0.0]);
assert_eq!(verts[2].position, [120.0, 60.0]);
assert_eq!(verts[0].shadow_params[2], 4.0); assert_eq!(verts[0].corner_radii, [6.0; 4]);
}
#[test]
fn shadow_scale_factor() {
let shadow = ShadowQuad {
screen: [10.0, 10.0, 120.0, 60.0],
color: [0.0, 0.0, 0.0, 0.3],
corner_radii: [6.0; 4],
shape_rect: [20.0, 18.0, 100.0, 40.0],
blur_radius: 4.0,
spread: 2.0,
};
let verts = ShadowVertex::from_shadow_quad(&shadow, 2.0);
assert_eq!(verts[0].position, [20.0, 20.0]);
assert_eq!(verts[0].shadow_params[0], 200.0); assert_eq!(verts[0].shadow_params[2], 8.0); assert_eq!(verts[0].shadow_params[3], 4.0); assert_eq!(verts[0].corner_radii, [12.0; 4]);
}
}