1use bytemuck::{Pod, Zeroable};
5
6use teksilo_canvas::live_image::LiveImageQuad;
7use teksilo_canvas::render_frame::PaintData;
8use teksilo_canvas::{DecorationRect, GlyphQuad, PathEntry, ShadowQuad, ShapeQuad, Transform2D};
9
10#[inline]
18fn srgb_to_linear(c: f32) -> f32 {
19 if c <= 0.04045 {
20 c / 12.92
21 } else {
22 ((c + 0.055) / 1.055).powf(2.4)
23 }
24}
25
26#[inline]
29pub fn srgb_to_linear_rgba(c: [f32; 4]) -> [f32; 4] {
30 [
31 srgb_to_linear(c[0]),
32 srgb_to_linear(c[1]),
33 srgb_to_linear(c[2]),
34 c[3],
35 ]
36}
37
38pub const QUAD_FLAG_COLOR_GLYPH: u32 = 1;
41
42pub const QUAD_FLAG_OPAQUE: u32 = 2;
45
46pub const QUAD_FLAG_SWAP_RB: u32 = 4;
50
51#[repr(C)]
53#[derive(Debug, Clone, Copy, Pod, Zeroable)]
54pub struct QuadVertex {
55 pub position: [f32; 2],
56 pub tex_coord: [f32; 2],
57 pub color: [f32; 4],
58 pub flags: u32,
61 pub _pad: u32,
64}
65
66const GLYPH_SNAP_AXIS_EPS: f32 = 1e-4;
70
71const GLYPH_SNAP_SIZE_EPS: f32 = 1.0 / 16.0;
77
78#[inline]
82fn apply_affine(p: [f32; 2], t: &Transform2D) -> [f32; 2] {
83 let [a, b, c, d, tx, ty] = t.m;
84 [a * p[0] + c * p[1] + tx, b * p[0] + d * p[1] + ty]
85}
86
87pub(crate) fn live_quad_verts(
96 quad: &LiveImageQuad,
97 texture: (u32, u32),
98 scale_factor: f32,
99 transform: &Transform2D,
100 color: [f32; 4],
101 flags: u32,
102) -> [QuadVertex; 4] {
103 let (screen, uv) = (quad.screen, quad.uv);
104 let [x, y, w, h] = screen;
105 let (sx, sy, sw, sh) = (
106 x * scale_factor,
107 y * scale_factor,
108 w * scale_factor,
109 h * scale_factor,
110 );
111 let (tw, th) = (texture.0 as f32, texture.1 as f32);
112 let along = |from: [f32; 2], to: [f32; 2]| {
114 ((to[0] - from[0]) * tw).abs() + ((to[1] - from[1]) * th).abs()
115 };
116 let (across, down) = (along(uv[0], uv[1]), along(uv[0], uv[3]));
117 let [a, b, c, d, _, _] = transform.m;
118 let whole = |v: f32| (v - v.round()).abs() < GLYPH_SNAP_SIZE_EPS;
119 let one_to_one = quad.pixel_snap
120 && b.abs() < GLYPH_SNAP_AXIS_EPS
121 && c.abs() < GLYPH_SNAP_AXIS_EPS
122 && (a * sw - across).abs() < GLYPH_SNAP_SIZE_EPS
123 && (d * sh - down).abs() < GLYPH_SNAP_SIZE_EPS
124 && whole(across)
125 && whole(down);
126 let positions: [[f32; 2]; 4] = if one_to_one {
127 let [ox, oy] = apply_affine([sx, sy], transform);
128 let (ox, oy) = (ox.round(), oy.round());
129 let (cw, ch) = (across.round() * a.signum(), down.round() * d.signum());
130 [[ox, oy], [ox + cw, oy], [ox + cw, oy + ch], [ox, oy + ch]]
131 } else {
132 [
133 apply_affine([sx, sy], transform),
134 apply_affine([sx + sw, sy], transform),
135 apply_affine([sx + sw, sy + sh], transform),
136 apply_affine([sx, sy + sh], transform),
137 ]
138 };
139 std::array::from_fn(|i| QuadVertex {
140 position: positions[i],
141 tex_coord: uv[i],
142 color,
143 flags,
144 _pad: 0,
145 })
146}
147
148impl QuadVertex {
149 pub fn from_glyph_quad_transformed(
175 quad: &GlyphQuad,
176 scale_factor: f32,
177 atlas_width: u32,
178 atlas_height: u32,
179 transform: &Transform2D,
180 ) -> [QuadVertex; 4] {
181 let [x, y, w, h] = quad.screen;
182 let [ax, ay, aw, ah] = quad.atlas;
183 let sx = x * scale_factor;
184 let sy = y * scale_factor;
185 let sw = w * scale_factor;
186 let sh = h * scale_factor;
187
188 let aw_f = atlas_width.max(1) as f32;
190 let ah_f = atlas_height.max(1) as f32;
191 let u0 = ax / aw_f;
192 let v0 = ay / ah_f;
193 let u1 = (ax + aw) / aw_f;
194 let v1 = (ay + ah) / ah_f;
195
196 let [a, b, c, d, _, _] = transform.m;
197 let axis_aligned = b.abs() < GLYPH_SNAP_AXIS_EPS && c.abs() < GLYPH_SNAP_AXIS_EPS;
198 let one_to_one = axis_aligned
199 && (a * sw - aw).abs() < GLYPH_SNAP_SIZE_EPS
200 && (d * sh - ah).abs() < GLYPH_SNAP_SIZE_EPS;
201 let positions: [[f32; 2]; 4] = if one_to_one {
202 let [ox, oy] = apply_affine([sx, sy], transform);
203 let ox = ox.round();
204 let oy = oy.round();
205 [[ox, oy], [ox + aw, oy], [ox + aw, oy + ah], [ox, oy + ah]]
206 } else {
207 [
208 apply_affine([sx, sy], transform),
209 apply_affine([sx + sw, sy], transform),
210 apply_affine([sx + sw, sy + sh], transform),
211 apply_affine([sx, sy + sh], transform),
212 ]
213 };
214
215 let flags = if quad.is_color {
224 QUAD_FLAG_COLOR_GLYPH
225 } else {
226 0
227 };
228 let color = srgb_to_linear_rgba(quad.color);
229
230 [
231 QuadVertex {
232 position: positions[0],
233 tex_coord: [u0, v0],
234 color,
235 flags,
236 _pad: 0,
237 },
238 QuadVertex {
239 position: positions[1],
240 tex_coord: [u1, v0],
241 color,
242 flags,
243 _pad: 0,
244 },
245 QuadVertex {
246 position: positions[2],
247 tex_coord: [u1, v1],
248 color,
249 flags,
250 _pad: 0,
251 },
252 QuadVertex {
253 position: positions[3],
254 tex_coord: [u0, v1],
255 color,
256 flags,
257 _pad: 0,
258 },
259 ]
260 }
261}
262
263#[repr(C)]
265#[derive(Debug, Clone, Copy, Pod, Zeroable)]
266pub struct RectVertex {
267 pub position: [f32; 2],
268 pub color: [f32; 4],
269}
270
271impl RectVertex {
272 pub fn from_decoration(rect: &DecorationRect, scale_factor: f32) -> [RectVertex; 4] {
274 let [x, y, w, h] = rect.rect;
275 let sx = x * scale_factor;
276 let sy = y * scale_factor;
277 let sw = w * scale_factor;
278 let sh = h * scale_factor;
279
280 [
281 RectVertex {
282 position: [sx, sy],
283 color: srgb_to_linear_rgba(rect.color),
284 },
285 RectVertex {
286 position: [sx + sw, sy],
287 color: srgb_to_linear_rgba(rect.color),
288 },
289 RectVertex {
290 position: [sx + sw, sy + sh],
291 color: srgb_to_linear_rgba(rect.color),
292 },
293 RectVertex {
294 position: [sx, sy + sh],
295 color: srgb_to_linear_rgba(rect.color),
296 },
297 ]
298 }
299}
300
301#[repr(C)]
303#[derive(Debug, Clone, Copy, Pod, Zeroable)]
304pub struct SdfVertex {
305 pub position: [f32; 2],
306 pub local_uv: [f32; 2],
308 pub color: [f32; 4],
309 pub corner_radii: [f32; 4],
310 pub shape_params: [f32; 4],
313 pub gradient_geo: [f32; 4],
315 pub gradient_color0: [f32; 4],
317 pub gradient_color1: [f32; 4],
319 pub gradient_color2: [f32; 4],
321 pub gradient_color3: [f32; 4],
323 pub gradient_offsets: [f32; 4],
325}
326
327impl SdfVertex {
328 pub fn from_shape_quad(shape: &ShapeQuad, scale_factor: f32) -> [SdfVertex; 4] {
341 Self::shape_quad_verts(shape, scale_factor, shape.stroke_width * scale_factor)
342 }
343
344 pub fn from_shape_quad_cosmetic(
357 shape: &ShapeQuad,
358 scale_factor: f32,
359 zoom: f32,
360 ) -> [SdfVertex; 4] {
361 let zoom = zoom.max(1e-3);
362 Self::shape_quad_verts(
363 shape,
364 scale_factor,
365 shape.stroke_width * scale_factor / zoom,
366 )
367 }
368
369 fn shape_quad_verts(shape: &ShapeQuad, scale_factor: f32, stroke_px: f32) -> [SdfVertex; 4] {
374 let [x, y, w, h] = shape.screen;
375 let sx = x * scale_factor;
376 let sy = y * scale_factor;
377 let sw = w * scale_factor;
378 let sh = h * scale_factor;
379
380 let (paint_type, gradient_geo, colors, offsets) =
382 encode_paint_data(&shape.paint_data, [0.0, 0.0], w, h);
383
384 let stroke = stroke_px;
385 let pad = stroke * 0.5 + 1.0;
389 let u_pad = if sw > 0.0 { pad / sw } else { 0.0 };
390 let v_pad = if sh > 0.0 { pad / sh } else { 0.0 };
391
392 let params = [sw, sh, stroke, paint_type as f32];
393 let scaled_corner_radii = [
400 shape.corner_radii[0] * scale_factor,
401 shape.corner_radii[1] * scale_factor,
402 shape.corner_radii[2] * scale_factor,
403 shape.corner_radii[3] * scale_factor,
404 ];
405
406 let base = SdfVertex {
407 position: [0.0, 0.0],
408 local_uv: [0.0, 0.0],
409 color: srgb_to_linear_rgba(shape.color),
410 corner_radii: scaled_corner_radii,
411 shape_params: params,
412 gradient_geo,
413 gradient_color0: srgb_to_linear_rgba(colors[0]),
414 gradient_color1: srgb_to_linear_rgba(colors[1]),
415 gradient_color2: srgb_to_linear_rgba(colors[2]),
416 gradient_color3: srgb_to_linear_rgba(colors[3]),
417 gradient_offsets: offsets,
418 };
419
420 [
421 SdfVertex {
422 position: [sx - pad, sy - pad],
423 local_uv: [-u_pad, -v_pad],
424 ..base
425 },
426 SdfVertex {
427 position: [sx + sw + pad, sy - pad],
428 local_uv: [1.0 + u_pad, -v_pad],
429 ..base
430 },
431 SdfVertex {
432 position: [sx + sw + pad, sy + sh + pad],
433 local_uv: [1.0 + u_pad, 1.0 + v_pad],
434 ..base
435 },
436 SdfVertex {
437 position: [sx - pad, sy + sh + pad],
438 local_uv: [-u_pad, 1.0 + v_pad],
439 ..base
440 },
441 ]
442 }
443}
444
445pub(crate) fn encode_paint_data(
461 paint_data: &PaintData,
462 origin: [f32; 2],
463 width: f32,
464 height: f32,
465) -> (u32, [f32; 4], [[f32; 4]; 4], [f32; 4]) {
466 let zero_colors = [[0.0; 4]; 4];
467 let zero_offsets = [0.0; 4];
468 let u = |x: f32| (x - origin[0]) / width;
469 let v = |y: f32| (y - origin[1]) / height;
470
471 match paint_data {
472 PaintData::Solid => (0, [0.0; 4], zero_colors, zero_offsets),
473 PaintData::LinearGradient { start, end, stops } => {
474 let geo = [u(start[0]), v(start[1]), u(end[0]), v(end[1])];
476 let (colors, offsets) = encode_stops(stops);
477 (1, geo, colors, offsets)
478 }
479 PaintData::RadialGradient {
480 center,
481 radius,
482 stops,
483 } => {
484 let aspect = height / width.max(0.0001);
489 let geo = [u(center[0]), v(center[1]), *radius / width, aspect];
490 let (colors, offsets) = encode_stops(stops);
491 (2, geo, colors, offsets)
492 }
493 PaintData::ConicGradient {
494 center,
495 start_angle,
496 stops,
497 } => {
498 let geo = [u(center[0]), v(center[1]), *start_angle, 0.0];
499 let (colors, offsets) = encode_stops(stops);
500 (3, geo, colors, offsets)
501 }
502 }
503}
504
505pub(crate) fn encode_stops(stops: &[teksilo_canvas::GradientStop]) -> ([[f32; 4]; 4], [f32; 4]) {
508 let mut colors = [[0.0f32; 4]; 4];
509 let mut offsets = [0.0f32; 4];
510 for (i, stop) in stops.iter().take(4).enumerate() {
511 colors[i] = stop.color.to_array();
512 offsets[i] = stop.offset;
513 }
514 if !stops.is_empty() {
516 let last_idx = stops.len().min(4) - 1;
517 for i in stops.len()..4 {
518 colors[i] = colors[last_idx];
519 offsets[i] = offsets[last_idx];
520 }
521 }
522 (colors, offsets)
523}
524
525#[repr(C)]
541#[derive(Debug, Clone, Copy, Pod, Zeroable)]
542pub struct PathGradientVertex {
543 pub position: [f32; 2],
544 pub tex_coord: [f32; 2],
546 pub local_uv: [f32; 2],
548 pub paint_type: u32,
553 pub _pad: u32,
555 pub gradient_geo: [f32; 4],
559 pub gradient_color0: [f32; 4],
561 pub gradient_color1: [f32; 4],
563 pub gradient_color2: [f32; 4],
565 pub gradient_color3: [f32; 4],
567 pub gradient_offsets: [f32; 4],
569}
570
571impl PathGradientVertex {
572 pub(crate) fn from_path_entry(
586 entry: &PathEntry,
587 placement: &crate::path_atlas::PathPlacement,
588 scale_factor: f32,
589 atlas_width: u32,
590 atlas_height: u32,
591 opacity: f32,
592 transform: &Transform2D,
593 ) -> [PathGradientVertex; 4] {
594 let region = &placement.region;
597 let [sx, sy, sw, sh] = placement.device_rect;
598
599 let aw = atlas_width.max(1) as f32;
600 let ah = atlas_height.max(1) as f32;
601 let u0 = region.x as f32 / aw;
602 let v0 = region.y as f32 / ah;
603 let u1 = (region.x + region.w) as f32 / aw;
604 let v1 = (region.y + region.h) as f32 / ah;
605
606 let sf = scale_factor.max(1e-4);
612 let origin = [
613 (sx - entry.bounds[0] * scale_factor) / sf,
614 (sy - entry.bounds[1] * scale_factor) / sf,
615 ];
616 let (paint_type, gradient_geo, raw_colors, gradient_offsets) =
617 encode_paint_data(&entry.paint_data, origin, sw / sf, sh / sf);
618 let colors: [[f32; 4]; 4] = std::array::from_fn(|i| {
621 let mut c = srgb_to_linear_rgba(raw_colors[i]);
622 c[3] *= opacity;
623 c
624 });
625
626 let positions = [
627 apply_affine([sx, sy], transform),
628 apply_affine([sx + sw, sy], transform),
629 apply_affine([sx + sw, sy + sh], transform),
630 apply_affine([sx, sy + sh], transform),
631 ];
632 let tex_coords = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
633 let local_uvs: [[f32; 2]; 4] = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
634
635 std::array::from_fn(|i| PathGradientVertex {
636 position: positions[i],
637 tex_coord: tex_coords[i],
638 local_uv: local_uvs[i],
639 paint_type,
640 _pad: 0,
641 gradient_geo,
642 gradient_color0: colors[0],
643 gradient_color1: colors[1],
644 gradient_color2: colors[2],
645 gradient_color3: colors[3],
646 gradient_offsets,
647 })
648 }
649}
650
651pub const QUAD_INDICES: [u32; 6] = [0, 1, 2, 0, 2, 3];
659
660pub fn generate_quad_indices(count: usize) -> Vec<u32> {
662 let mut indices = Vec::with_capacity(count * 6);
663 for i in 0..count {
664 let base = (i * 4) as u32;
665 for &offset in &QUAD_INDICES {
666 indices.push(base + offset);
667 }
668 }
669 indices
670}
671
672#[repr(C)]
674#[derive(Debug, Clone, Copy, Pod, Zeroable)]
675pub struct ShadowVertex {
676 pub position: [f32; 2],
677 pub local_uv: [f32; 2],
679 pub shadow_color: [f32; 4],
680 pub corner_radii: [f32; 4],
681 pub shadow_params: [f32; 4],
683 pub shape_offset: [f32; 4],
685}
686
687impl ShadowVertex {
688 pub fn from_shadow_quad(shadow: &ShadowQuad, scale_factor: f32) -> [ShadowVertex; 4] {
690 let [x, y, w, h] = shadow.screen;
691 let sx = x * scale_factor;
692 let sy = y * scale_factor;
693 let sw = w * scale_factor;
694 let sh = h * scale_factor;
695
696 let [sr_x, sr_y, sr_w, sr_h] = shadow.shape_rect;
697 let shape_w = sr_w * scale_factor;
698 let shape_h = sr_h * scale_factor;
699
700 let shadow_cx = sx + sw * 0.5;
702 let shadow_cy = sy + sh * 0.5;
703 let shape_cx = (sr_x + sr_w * 0.5) * scale_factor;
704 let shape_cy = (sr_y + sr_h * 0.5) * scale_factor;
705 let offset_x = shape_cx - shadow_cx;
706 let offset_y = shape_cy - shadow_cy;
707
708 let params = [
709 shape_w,
710 shape_h,
711 shadow.blur_radius * scale_factor,
712 shadow.spread * scale_factor,
713 ];
714 let offset = [offset_x, offset_y, 0.0, 0.0];
715 let scaled_corner_radii = [
720 shadow.corner_radii[0] * scale_factor,
721 shadow.corner_radii[1] * scale_factor,
722 shadow.corner_radii[2] * scale_factor,
723 shadow.corner_radii[3] * scale_factor,
724 ];
725
726 [
727 ShadowVertex {
728 position: [sx, sy],
729 local_uv: [0.0, 0.0],
730 shadow_color: srgb_to_linear_rgba(shadow.color),
731 corner_radii: scaled_corner_radii,
732 shadow_params: params,
733 shape_offset: offset,
734 },
735 ShadowVertex {
736 position: [sx + sw, sy],
737 local_uv: [1.0, 0.0],
738 shadow_color: srgb_to_linear_rgba(shadow.color),
739 corner_radii: scaled_corner_radii,
740 shadow_params: params,
741 shape_offset: offset,
742 },
743 ShadowVertex {
744 position: [sx + sw, sy + sh],
745 local_uv: [1.0, 1.0],
746 shadow_color: srgb_to_linear_rgba(shadow.color),
747 corner_radii: scaled_corner_radii,
748 shadow_params: params,
749 shape_offset: offset,
750 },
751 ShadowVertex {
752 position: [sx, sy + sh],
753 local_uv: [0.0, 1.0],
754 shadow_color: srgb_to_linear_rgba(shadow.color),
755 corner_radii: scaled_corner_radii,
756 shadow_params: params,
757 shape_offset: offset,
758 },
759 ]
760 }
761}
762
763#[repr(C)]
771#[derive(Debug, Clone, Copy, Pod, Zeroable)]
772pub struct AnimQuadVertex {
773 pub position: [f32; 2],
775 pub uv: [f32; 2],
780 pub slot: u32,
784 pub _pad: u32,
787}
788
789impl AnimQuadVertex {
790 pub fn from_animated_quad(
791 draw: &teksilo_canvas::AnimatedQuadDraw,
792 scale_factor: f32,
793 ) -> [AnimQuadVertex; 4] {
794 let [x, y, w, h] = draw.screen;
795 let sx = x * scale_factor;
796 let sy = y * scale_factor;
797 let sw = w * scale_factor;
798 let sh = h * scale_factor;
799 [
800 AnimQuadVertex {
801 position: [sx, sy],
802 uv: [0.0, 0.0],
803 slot: draw.slot,
804 _pad: 0,
805 },
806 AnimQuadVertex {
807 position: [sx + sw, sy],
808 uv: [1.0, 0.0],
809 slot: draw.slot,
810 _pad: 0,
811 },
812 AnimQuadVertex {
813 position: [sx + sw, sy + sh],
814 uv: [1.0, 1.0],
815 slot: draw.slot,
816 _pad: 0,
817 },
818 AnimQuadVertex {
819 position: [sx, sy + sh],
820 uv: [0.0, 1.0],
821 slot: draw.slot,
822 _pad: 0,
823 },
824 ]
825 }
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831 use teksilo_canvas::{DecorationKind, GradientStop, PaintData, ShapeKind, StrokeSpace};
832 use teksilo_tokens::Color;
833
834 fn glyph(screen: [f32; 4], atlas: [f32; 4], is_color: bool) -> GlyphQuad {
836 GlyphQuad {
837 screen,
838 atlas,
839 color: [1.0, 1.0, 1.0, 1.0],
840 is_color,
841 }
842 }
843
844 fn assert_pos_near(actual: [f32; 2], expected: [f32; 2]) {
845 assert!(
846 (actual[0] - expected[0]).abs() < 1e-3 && (actual[1] - expected[1]).abs() < 1e-3,
847 "position {actual:?} != expected {expected:?}"
848 );
849 }
850
851 #[test]
852 fn glyph_quad_to_vertices() {
853 let quad = glyph([10.0, 20.0, 30.0, 40.0], [0.0, 0.0, 64.0, 64.0], false);
856 let verts =
857 QuadVertex::from_glyph_quad_transformed(&quad, 1.0, 256, 256, &Transform2D::IDENTITY);
858 assert_eq!(verts.len(), 4);
859 assert_eq!(verts[0].position, [10.0, 20.0]);
860 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]);
864 assert_eq!(verts[2].tex_coord, [0.25, 0.25]);
865 }
866
867 #[test]
868 fn scale_factor_applied_to_glyph_coords() {
869 let quad = glyph([10.0, 20.0, 30.0, 40.0], [0.0, 0.0, 128.0, 128.0], false);
871 let verts =
872 QuadVertex::from_glyph_quad_transformed(&quad, 2.0, 256, 256, &Transform2D::IDENTITY);
873 assert_eq!(verts[0].position, [20.0, 40.0]);
874 assert_eq!(verts[1].position, [80.0, 40.0]);
875 }
876
877 #[test]
878 fn glyph_snap_identity_fractional_origin() {
879 let quad = glyph([10.3, 20.7, 30.0, 40.0], [0.0, 0.0, 30.0, 40.0], false);
883 let verts =
884 QuadVertex::from_glyph_quad_transformed(&quad, 1.0, 256, 256, &Transform2D::IDENTITY);
885 assert_eq!(verts[0].position, [10.0, 21.0]);
886 assert_eq!(verts[1].position, [40.0, 21.0]);
887 assert_eq!(verts[2].position, [40.0, 61.0]);
888 assert_eq!(verts[3].position, [10.0, 61.0]);
889 }
890
891 #[test]
892 fn glyph_snap_hidpi_scale_factor() {
893 let quad = glyph([5.7, 8.3, 16.0, 16.0], [0.0, 0.0, 32.0, 32.0], false);
897 let verts =
898 QuadVertex::from_glyph_quad_transformed(&quad, 2.0, 256, 256, &Transform2D::IDENTITY);
899 assert_eq!(verts[0].position, [11.0, 17.0]);
900 assert_eq!(verts[2].position, [43.0, 49.0]);
901 }
902
903 #[test]
904 fn glyph_snap_fractional_dpi() {
905 let quad = glyph([4.2, 7.8, 20.0, 20.0], [0.0, 0.0, 25.0, 25.0], false);
909 let verts =
910 QuadVertex::from_glyph_quad_transformed(&quad, 1.25, 256, 256, &Transform2D::IDENTITY);
911 assert_eq!(verts[0].position, [5.0, 10.0]);
912 assert_eq!(verts[2].position, [30.0, 35.0]);
913 }
914
915 #[test]
916 fn glyph_snap_exact_bucket_zoom() {
917 let quad = glyph([4.0, 8.0, 20.0, 20.0], [0.0, 0.0, 50.0, 50.0], false);
923 let zoom = Transform2D {
924 m: [1.25, 0.0, 0.0, 1.25, 3.3, 7.8],
925 };
926 let verts = QuadVertex::from_glyph_quad_transformed(&quad, 2.0, 256, 256, &zoom);
927 assert_eq!(verts[0].position, [13.0, 28.0]);
929 assert_eq!(verts[2].position, [63.0, 78.0]);
930 }
931
932 #[test]
933 fn glyph_no_snap_mid_bucket_residual() {
934 let quad = glyph([4.0, 8.0, 20.0, 20.0], [0.0, 0.0, 50.0, 50.0], false);
938 let zoom = Transform2D {
939 m: [1.1, 0.0, 0.0, 1.1, 3.3, 7.8],
940 };
941 let verts = QuadVertex::from_glyph_quad_transformed(&quad, 2.0, 256, 256, &zoom);
942 assert_pos_near(verts[0].position, [1.1 * 8.0 + 3.3, 1.1 * 16.0 + 7.8]);
943 assert_pos_near(verts[2].position, [1.1 * 48.0 + 3.3, 1.1 * 56.0 + 7.8]);
944 }
945
946 #[test]
947 fn glyph_no_snap_rotation() {
948 let quad = glyph([10.0, 20.0, 30.0, 40.0], [0.0, 0.0, 30.0, 40.0], false);
950 let (s, c) = (0.1_f32.sin(), 0.1_f32.cos());
951 let rot = Transform2D {
952 m: [c, s, -s, c, 0.0, 0.0],
953 };
954 let verts = QuadVertex::from_glyph_quad_transformed(&quad, 1.0, 256, 256, &rot);
955 assert_pos_near(
956 verts[0].position,
957 [c * 10.0 - s * 20.0, s * 10.0 + c * 20.0],
958 );
959 assert_pos_near(
960 verts[2].position,
961 [c * 40.0 - s * 60.0, s * 40.0 + c * 60.0],
962 );
963 }
964
965 #[test]
966 fn glyph_snap_translation_only_transform() {
967 let quad = glyph([10.3, 20.0, 30.0, 40.0], [0.0, 0.0, 30.0, 40.0], false);
970 let pan = Transform2D {
971 m: [1.0, 0.0, 0.0, 1.0, 5.7, 3.2],
972 };
973 let verts = QuadVertex::from_glyph_quad_transformed(&quad, 1.0, 256, 256, &pan);
974 assert_eq!(verts[0].position, [16.0, 23.0]);
976 assert_eq!(verts[2].position, [46.0, 63.0]);
977 }
978
979 #[test]
980 fn glyph_snap_color_emoji_flag_preserved() {
981 let quad = glyph([10.3, 20.7, 30.0, 40.0], [0.0, 0.0, 30.0, 40.0], true);
982 let verts =
983 QuadVertex::from_glyph_quad_transformed(&quad, 1.0, 256, 256, &Transform2D::IDENTITY);
984 assert_eq!(verts[0].position, [10.0, 21.0]);
985 for v in &verts {
986 assert_eq!(v.flags, QUAD_FLAG_COLOR_GLYPH);
987 }
988 }
989
990 #[test]
991 fn glyph_uvs_independent_of_snapping() {
992 let quad = glyph([10.3, 20.7, 30.0, 40.0], [16.0, 32.0, 30.0, 40.0], false);
995 let snapped =
996 QuadVertex::from_glyph_quad_transformed(&quad, 1.0, 256, 256, &Transform2D::IDENTITY);
997 let residual = Transform2D {
998 m: [1.1, 0.0, 0.0, 1.1, 0.0, 0.0],
999 };
1000 let unsnapped = QuadVertex::from_glyph_quad_transformed(&quad, 1.0, 256, 256, &residual);
1001 for (a, b) in snapped.iter().zip(unsnapped.iter()) {
1002 assert_eq!(a.tex_coord, b.tex_coord);
1003 }
1004 assert_eq!(snapped[0].tex_coord, [16.0 / 256.0, 32.0 / 256.0]);
1005 assert_eq!(snapped[2].tex_coord, [46.0 / 256.0, 72.0 / 256.0]);
1006 }
1007
1008 #[test]
1009 fn decoration_rect_to_vertices() {
1010 let rect = DecorationRect {
1011 rect: [0.0, 0.0, 100.0, 2.0],
1012 color: [1.0, 0.0, 0.0, 1.0],
1013 kind: DecorationKind::FocusRing,
1014 };
1015 let verts = RectVertex::from_decoration(&rect, 1.0);
1016 assert_eq!(verts.len(), 4);
1017 assert_eq!(verts[0].position, [0.0, 0.0]);
1018 assert_eq!(verts[2].position, [100.0, 2.0]);
1019 }
1020
1021 #[test]
1022 fn shape_quad_to_sdf_vertices() {
1023 let shape = ShapeQuad {
1024 screen: [0.0, 0.0, 100.0, 40.0],
1025 color: [0.0, 0.5, 0.0, 1.0],
1026 shape: ShapeKind::RoundedRect,
1027 stroke_width: 0.0,
1028 stroke_space: StrokeSpace::Logical,
1029 corner_radii: [6.0, 6.0, 6.0, 6.0],
1030 paint_data: PaintData::Solid,
1031 };
1032 let verts = SdfVertex::from_shape_quad(&shape, 1.0);
1033 assert_eq!(verts.len(), 4);
1034 assert_eq!(verts[0].corner_radii, [6.0, 6.0, 6.0, 6.0]);
1035 assert_eq!(verts[0].position, [-1.0, -1.0]);
1038 assert_eq!(verts[2].position, [101.0, 41.0]);
1039 assert!((verts[0].local_uv[0] - (-0.01)).abs() < 1e-5);
1040 assert!((verts[0].local_uv[1] - (-0.025)).abs() < 1e-5);
1041 assert!((verts[2].local_uv[0] - 1.01).abs() < 1e-5);
1042 assert!((verts[2].local_uv[1] - 1.025).abs() < 1e-5);
1043 }
1044
1045 #[test]
1046 fn sdf_scale_factor() {
1047 let shape = ShapeQuad {
1048 screen: [10.0, 10.0, 100.0, 40.0],
1049 color: [0.0, 0.0, 0.0, 1.0],
1050 shape: ShapeKind::RoundedRect,
1051 stroke_width: 2.0,
1052 stroke_space: StrokeSpace::Logical,
1053 corner_radii: [4.0; 4],
1054 paint_data: PaintData::Solid,
1055 };
1056 let verts = SdfVertex::from_shape_quad(&shape, 2.0);
1057 assert_eq!(verts[0].position, [17.0, 17.0]);
1060 assert_eq!(verts[0].shape_params[2], 4.0); assert_eq!(verts[0].corner_radii, [8.0; 4]);
1065 }
1066
1067 #[test]
1068 fn sdf_circle_stays_circle_on_hidpi() {
1069 let shape = ShapeQuad {
1075 screen: [0.0, 0.0, 19.0, 19.0],
1076 color: [0.0, 0.0, 0.0, 1.0],
1077 shape: ShapeKind::RoundedRect,
1078 stroke_width: 0.0,
1079 stroke_space: StrokeSpace::Logical,
1080 corner_radii: [9.5; 4],
1081 paint_data: PaintData::Solid,
1082 };
1083 let verts = SdfVertex::from_shape_quad(&shape, 2.0);
1084 assert_eq!(verts[0].shape_params[0], 38.0);
1085 assert_eq!(verts[0].shape_params[1], 38.0);
1086 assert_eq!(verts[0].corner_radii, [19.0; 4]);
1087 }
1088
1089 #[test]
1090 fn cosmetic_shape_stroke_param_is_inverse_zoom() {
1091 let shape = ShapeQuad {
1096 screen: [0.0, 0.0, 100.0, 100.0],
1097 color: [0.0, 0.0, 0.0, 1.0],
1098 shape: ShapeKind::RoundedRect,
1099 stroke_width: 2.0,
1100 stroke_space: StrokeSpace::Device,
1101 corner_radii: [10.0; 4],
1102 paint_data: PaintData::Solid,
1103 };
1104 let sf = 2.0;
1105 let logical = SdfVertex::from_shape_quad(&shape, sf);
1106 let z1 = SdfVertex::from_shape_quad_cosmetic(&shape, sf, 1.0);
1107 let z2 = SdfVertex::from_shape_quad_cosmetic(&shape, sf, 2.0);
1108 assert!((z1[0].shape_params[2] - logical[0].shape_params[2]).abs() < 1e-4);
1110 assert!((z1[0].shape_params[2] - 4.0).abs() < 1e-4);
1111 assert!((z2[0].shape_params[2] - 2.0).abs() < 1e-4);
1113 assert_eq!(z1[0].shape_params[0], z2[0].shape_params[0]);
1116 assert_eq!(z2[0].shape_params[0], 200.0);
1117 }
1118
1119 #[test]
1120 fn sdf_linear_gradient_encoding() {
1121 let shape = ShapeQuad {
1122 screen: [0.0, 0.0, 100.0, 50.0],
1123 color: [1.0, 1.0, 1.0, 1.0],
1124 shape: ShapeKind::RoundedRect,
1125 stroke_width: 0.0,
1126 stroke_space: StrokeSpace::Logical,
1127 corner_radii: [0.0; 4],
1128 paint_data: PaintData::LinearGradient {
1129 start: [0.0, 0.0],
1130 end: [100.0, 0.0],
1131 stops: vec![
1132 GradientStop {
1133 offset: 0.0,
1134 color: Color::RED,
1135 },
1136 GradientStop {
1137 offset: 1.0,
1138 color: Color::BLUE,
1139 },
1140 ],
1141 },
1142 };
1143 let verts = SdfVertex::from_shape_quad(&shape, 1.0);
1144 assert!((verts[0].shape_params[3] - 1.0).abs() < 0.01);
1146 assert!((verts[0].gradient_geo[0]).abs() < 0.01);
1148 assert!((verts[0].gradient_geo[2] - 1.0).abs() < 0.01);
1149 assert!((verts[0].gradient_color0[0] - 1.0).abs() < 0.01);
1151 assert!((verts[0].gradient_offsets[0]).abs() < 0.01);
1153 assert!((verts[0].gradient_offsets[1] - 1.0).abs() < 0.01);
1154 }
1155
1156 #[test]
1157 fn linear_gradient_endpoints_are_rect_local_not_absolute() {
1158 let shape = ShapeQuad {
1167 screen: [50.0, 100.0, 200.0, 200.0],
1168 color: [1.0, 1.0, 1.0, 1.0],
1169 shape: ShapeKind::RoundedRect,
1170 stroke_width: 0.0,
1171 stroke_space: StrokeSpace::Logical,
1172 corner_radii: [0.0; 4],
1173 paint_data: PaintData::LinearGradient {
1174 start: [0.0, 0.0],
1175 end: [0.0, 200.0],
1176 stops: vec![
1177 GradientStop {
1178 offset: 0.0,
1179 color: Color::new(0.0, 0.0, 0.0, 0.0),
1180 },
1181 GradientStop {
1182 offset: 1.0,
1183 color: Color::BLACK,
1184 },
1185 ],
1186 },
1187 };
1188 let verts = SdfVertex::from_shape_quad(&shape, 1.0);
1189 assert!((verts[0].gradient_geo[0]).abs() < 1e-5, "start_uv.x");
1190 assert!((verts[0].gradient_geo[1]).abs() < 1e-5, "start_uv.y");
1191 assert!((verts[0].gradient_geo[2]).abs() < 1e-5, "end_uv.x");
1192 assert!((verts[0].gradient_geo[3] - 1.0).abs() < 1e-5, "end_uv.y");
1193 }
1194
1195 fn rasterize_for_test(entry: &PathEntry, atlas_size: u32) -> crate::path_atlas::PathPlacement {
1201 place_for_test(entry, atlas_size, 1.0, false)
1202 }
1203
1204 fn place_for_test(
1205 entry: &PathEntry,
1206 atlas_size: u32,
1207 scale_factor: f32,
1208 snap: bool,
1209 ) -> crate::path_atlas::PathPlacement {
1210 let mut atlas = crate::path_atlas::PathAtlas::new(atlas_size, atlas_size);
1211 atlas.begin_frame();
1212 atlas
1213 .lookup_or_rasterize(
1214 &entry.path,
1215 &entry.stroke_style,
1216 entry.fill_rule,
1217 entry.bounds,
1218 scale_factor,
1219 1.0,
1220 snap,
1221 )
1222 .expect("test path rasterizes")
1223 }
1224
1225 #[test]
1233 fn snapping_the_quad_does_not_move_the_gradient() {
1234 let bounds_rect = teksilo_canvas::Rect::new(1.5, 1.5, 13.0, 13.0);
1236 let entry = gradient_path_entry(
1237 bounds_rect,
1238 PaintData::LinearGradient {
1239 start: [0.0, 0.0],
1240 end: [13.0, 0.0],
1241 stops: vec![
1242 GradientStop {
1243 offset: 0.0,
1244 color: Color::RED,
1245 },
1246 GradientStop {
1247 offset: 1.0,
1248 color: Color::BLUE,
1249 },
1250 ],
1251 },
1252 );
1253
1254 for snap in [false, true] {
1255 let placement = place_for_test(&entry, 256, 1.0, snap);
1256 let verts = PathGradientVertex::from_path_entry(
1257 &entry,
1258 &placement,
1259 1.0,
1260 256,
1261 256,
1262 1.0,
1263 &Transform2D::IDENTITY,
1264 );
1265 let [qx, _, qw, _] = placement.device_rect;
1266 let start_x = qx + verts[0].gradient_geo[0] * qw;
1268 let end_x = qx + verts[0].gradient_geo[2] * qw;
1269 assert!(
1270 (start_x - 1.5).abs() < 0.01,
1271 "snap={snap}: gradient start must sit at the bounds origin, got {start_x}"
1272 );
1273 assert!(
1274 (end_x - 14.5).abs() < 0.01,
1275 "snap={snap}: gradient end must sit at the bounds' far edge, got {end_x}"
1276 );
1277 }
1278 }
1279
1280 fn gradient_path_entry(bounds_rect: teksilo_canvas::Rect, paint_data: PaintData) -> PathEntry {
1281 use teksilo_canvas::{FillRule, StrokeStyle};
1282 PathEntry {
1283 path: teksilo_canvas::Path::rect(bounds_rect),
1284 color: [1.0, 1.0, 1.0, 1.0],
1285 stroke_style: StrokeStyle::solid(0.0),
1286 fill_rule: FillRule::Winding,
1287 bounds: bounds_rect.to_array(),
1288 paint_data,
1289 }
1290 }
1291
1292 #[test]
1293 fn path_gradient_linear_encoding() {
1294 let bounds_rect = teksilo_canvas::Rect::new(0.0, 0.0, 100.0, 50.0);
1295 let entry = gradient_path_entry(
1296 bounds_rect,
1297 PaintData::LinearGradient {
1298 start: [0.0, 0.0],
1299 end: [100.0, 0.0],
1300 stops: vec![
1301 GradientStop {
1302 offset: 0.0,
1303 color: Color::RED,
1304 },
1305 GradientStop {
1306 offset: 1.0,
1307 color: Color::BLUE,
1308 },
1309 ],
1310 },
1311 );
1312 let placement = rasterize_for_test(&entry, 256);
1313
1314 let verts = PathGradientVertex::from_path_entry(
1315 &entry,
1316 &placement,
1317 1.0,
1318 256,
1319 256,
1320 1.0,
1321 &Transform2D::IDENTITY,
1322 );
1323
1324 assert_eq!(verts[0].paint_type, 1);
1326 assert!((verts[0].gradient_geo[0]).abs() < 0.01);
1328 assert!((verts[0].gradient_geo[2] - 1.0).abs() < 0.01);
1329 assert!((verts[0].gradient_color0[0] - 1.0).abs() < 0.01);
1331 assert!((verts[0].gradient_color0[1]).abs() < 0.01);
1332 assert!((verts[0].gradient_offsets[0]).abs() < 0.01);
1334 assert!((verts[0].gradient_offsets[1] - 1.0).abs() < 0.01);
1335 assert_eq!(verts[0].local_uv, [0.0, 0.0]);
1337 assert_eq!(verts[1].local_uv, [1.0, 0.0]);
1338 assert_eq!(verts[2].local_uv, [1.0, 1.0]);
1339 assert_eq!(verts[3].local_uv, [0.0, 1.0]);
1340 }
1341
1342 #[test]
1343 fn path_gradient_endpoints_are_rect_local_not_absolute() {
1344 let bounds_rect = teksilo_canvas::Rect::new(50.0, 100.0, 200.0, 200.0);
1350 let entry = gradient_path_entry(
1351 bounds_rect,
1352 PaintData::LinearGradient {
1353 start: [0.0, 0.0],
1354 end: [0.0, 200.0],
1355 stops: vec![
1356 GradientStop {
1357 offset: 0.0,
1358 color: Color::new(0.0, 0.0, 0.0, 0.0),
1359 },
1360 GradientStop {
1361 offset: 1.0,
1362 color: Color::BLACK,
1363 },
1364 ],
1365 },
1366 );
1367 let placement = rasterize_for_test(&entry, 512);
1368
1369 let verts = PathGradientVertex::from_path_entry(
1370 &entry,
1371 &placement,
1372 1.0,
1373 512,
1374 512,
1375 1.0,
1376 &Transform2D::IDENTITY,
1377 );
1378 assert!((verts[0].gradient_geo[0]).abs() < 1e-5, "start_uv.x");
1379 assert!((verts[0].gradient_geo[1]).abs() < 1e-5, "start_uv.y");
1380 assert!((verts[0].gradient_geo[2]).abs() < 1e-5, "end_uv.x");
1381 assert!((verts[0].gradient_geo[3] - 1.0).abs() < 1e-5, "end_uv.y");
1382 }
1383
1384 #[test]
1385 fn path_gradient_opacity_folds_into_every_stop_alpha() {
1386 let bounds_rect = teksilo_canvas::Rect::new(0.0, 0.0, 40.0, 20.0);
1392 let entry = gradient_path_entry(
1393 bounds_rect,
1394 PaintData::LinearGradient {
1395 start: [0.0, 0.0],
1396 end: [40.0, 0.0],
1397 stops: vec![
1398 GradientStop {
1399 offset: 0.0,
1400 color: Color::RED,
1401 },
1402 GradientStop {
1403 offset: 1.0,
1404 color: Color::BLUE,
1405 },
1406 ],
1407 },
1408 );
1409 let placement = rasterize_for_test(&entry, 128);
1410
1411 let full = PathGradientVertex::from_path_entry(
1412 &entry,
1413 &placement,
1414 1.0,
1415 128,
1416 128,
1417 1.0,
1418 &Transform2D::IDENTITY,
1419 );
1420 let half = PathGradientVertex::from_path_entry(
1421 &entry,
1422 &placement,
1423 1.0,
1424 128,
1425 128,
1426 0.5,
1427 &Transform2D::IDENTITY,
1428 );
1429
1430 assert!((full[0].gradient_color0[3] - 1.0).abs() < 1e-5);
1431 assert!((half[0].gradient_color0[3] - 0.5).abs() < 1e-5);
1432 assert!((half[0].gradient_color1[3] - 0.5).abs() < 1e-5);
1433 }
1434
1435 #[test]
1436 fn generate_indices_for_multiple_quads() {
1437 let indices = generate_quad_indices(2);
1438 assert_eq!(indices.len(), 12);
1439 assert_eq!(&indices[0..6], &[0, 1, 2, 0, 2, 3]);
1440 assert_eq!(&indices[6..12], &[4, 5, 6, 4, 6, 7]);
1441 }
1442
1443 #[test]
1444 fn shadow_quad_to_vertices() {
1445 let shadow = ShadowQuad {
1446 screen: [0.0, 0.0, 120.0, 60.0],
1447 color: [0.0, 0.0, 0.0, 0.3],
1448 corner_radii: [6.0; 4],
1449 shape_rect: [10.0, 8.0, 100.0, 40.0],
1450 blur_radius: 4.0,
1451 spread: 0.0,
1452 };
1453 let verts = ShadowVertex::from_shadow_quad(&shadow, 1.0);
1454 assert_eq!(verts.len(), 4);
1455 assert_eq!(verts[0].position, [0.0, 0.0]);
1456 assert_eq!(verts[2].position, [120.0, 60.0]);
1457 assert_eq!(verts[0].shadow_params[2], 4.0); assert_eq!(verts[0].corner_radii, [6.0; 4]);
1459 }
1460
1461 #[test]
1462 fn shadow_scale_factor() {
1463 let shadow = ShadowQuad {
1464 screen: [10.0, 10.0, 120.0, 60.0],
1465 color: [0.0, 0.0, 0.0, 0.3],
1466 corner_radii: [6.0; 4],
1467 shape_rect: [20.0, 18.0, 100.0, 40.0],
1468 blur_radius: 4.0,
1469 spread: 2.0,
1470 };
1471 let verts = ShadowVertex::from_shadow_quad(&shadow, 2.0);
1472 assert_eq!(verts[0].position, [20.0, 20.0]);
1473 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]);
1479 }
1480}