Added improved non-clay text handling along with consistent origin and rotation API

This commit is contained in:
Zachary Levy
2026-04-19 18:28:42 -07:00
parent 30b72128b2
commit 7a21d6f253
12 changed files with 1157 additions and 388 deletions

View File

@@ -76,90 +76,6 @@ pixel :: proc(layer: ^Layer, pos: [2]f32, color: Color) {
prepare_shape(layer, vertices[:])
}
rectangle :: proc(
layer: ^Layer,
rect: Rectangle,
color: Color,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
temp_allocator := context.temp_allocator,
) {
vertices := make([]Vertex, 6, temp_allocator)
if rotation == 0 {
emit_rect(rect.x, rect.y, rect.w, rect.h, color, vertices, 0)
} else {
rad := math.to_radians(rotation)
cos_rotation := math.cos(rad)
sin_rotation := math.sin(rad)
// Corners relative to origin
top_left := [2]f32{-origin[0], -origin[1]}
top_right := [2]f32{rect.w - origin[0], -origin[1]}
bottom_right := [2]f32{rect.w - origin[0], rect.h - origin[1]}
bottom_left := [2]f32{-origin[0], rect.h - origin[1]}
// Translation to final position
translate := [2]f32{rect.x + origin[0], rect.y + origin[1]}
// Rotate and translate each corner
tl :=
[2]f32 {
cos_rotation * top_left[0] - sin_rotation * top_left[1],
sin_rotation * top_left[0] + cos_rotation * top_left[1],
} +
translate
tr :=
[2]f32 {
cos_rotation * top_right[0] - sin_rotation * top_right[1],
sin_rotation * top_right[0] + cos_rotation * top_right[1],
} +
translate
br :=
[2]f32 {
cos_rotation * bottom_right[0] - sin_rotation * bottom_right[1],
sin_rotation * bottom_right[0] + cos_rotation * bottom_right[1],
} +
translate
bl :=
[2]f32 {
cos_rotation * bottom_left[0] - sin_rotation * bottom_left[1],
sin_rotation * bottom_left[0] + cos_rotation * bottom_left[1],
} +
translate
vertices[0] = sv(tl, color)
vertices[1] = sv(tr, color)
vertices[2] = sv(br, color)
vertices[3] = sv(tl, color)
vertices[4] = sv(br, color)
vertices[5] = sv(bl, color)
}
prepare_shape(layer, vertices)
}
rectangle_lines :: proc(
layer: ^Layer,
rect: Rectangle,
color: Color,
thick: f32 = 1,
temp_allocator := context.temp_allocator,
) {
vertices := make([]Vertex, 24, temp_allocator)
// Top edge
emit_rect(rect.x, rect.y, rect.w, thick, color, vertices, 0)
// Bottom edge
emit_rect(rect.x, rect.y + rect.h - thick, rect.w, thick, color, vertices, 6)
// Left edge
emit_rect(rect.x, rect.y + thick, thick, rect.h - thick * 2, color, vertices, 12)
// Right edge
emit_rect(rect.x + rect.w - thick, rect.y + thick, thick, rect.h - thick * 2, color, vertices, 18)
prepare_shape(layer, vertices)
}
rectangle_gradient :: proc(
layer: ^Layer,
rect: Rectangle,
@@ -189,6 +105,8 @@ circle_sector :: proc(
radius: f32,
start_angle, end_angle: f32,
color: Color,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
segments: int = 0,
temp_allocator := context.temp_allocator,
) {
@@ -202,17 +120,34 @@ circle_sector :: proc(
end_rad := math.to_radians(end_angle)
step_angle := (end_rad - start_rad) / f32(segs)
for i in 0 ..< segs {
current_angle := start_rad + step_angle * f32(i)
next_angle := start_rad + step_angle * f32(i + 1)
if !needs_transform(origin, rotation) {
for i in 0 ..< segs {
current_angle := start_rad + step_angle * f32(i)
next_angle := start_rad + step_angle * f32(i + 1)
edge_current := center + [2]f32{math.cos(current_angle) * radius, math.sin(current_angle) * radius}
edge_next := center + [2]f32{math.cos(next_angle) * radius, math.sin(next_angle) * radius}
edge_current := center + [2]f32{math.cos(current_angle) * radius, math.sin(current_angle) * radius}
edge_next := center + [2]f32{math.cos(next_angle) * radius, math.sin(next_angle) * radius}
idx := i * 3
vertices[idx + 0] = sv(center, color)
vertices[idx + 1] = sv(edge_next, color)
vertices[idx + 2] = sv(edge_current, color)
idx := i * 3
vertices[idx + 0] = sv(center, color)
vertices[idx + 1] = sv(edge_next, color)
vertices[idx + 2] = sv(edge_current, color)
}
} else {
xform := build_pivot_rot(center, origin, rotation)
center_local := [2]f32{0, 0}
for i in 0 ..< segs {
current_angle := start_rad + step_angle * f32(i)
next_angle := start_rad + step_angle * f32(i + 1)
edge_current := [2]f32{math.cos(current_angle) * radius, math.sin(current_angle) * radius}
edge_next := [2]f32{math.cos(next_angle) * radius, math.sin(next_angle) * radius}
idx := i * 3
vertices[idx + 0] = sv(apply_transform(xform, center_local), color)
vertices[idx + 1] = sv(apply_transform(xform, edge_next), color)
vertices[idx + 2] = sv(apply_transform(xform, edge_current), color)
}
}
prepare_shape(layer, vertices)
@@ -223,6 +158,8 @@ circle_gradient :: proc(
center: [2]f32,
radius: f32,
inner, outer: Color,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
segments: int = 0,
temp_allocator := context.temp_allocator,
) {
@@ -233,24 +170,61 @@ circle_gradient :: proc(
step_angle := math.TAU / f32(segs)
for i in 0 ..< segs {
current_angle := step_angle * f32(i)
next_angle := step_angle * f32(i + 1)
if !needs_transform(origin, rotation) {
for i in 0 ..< segs {
current_angle := step_angle * f32(i)
next_angle := step_angle * f32(i + 1)
edge_current := center + [2]f32{math.cos(current_angle) * radius, math.sin(current_angle) * radius}
edge_next := center + [2]f32{math.cos(next_angle) * radius, math.sin(next_angle) * radius}
edge_current := center + [2]f32{math.cos(current_angle) * radius, math.sin(current_angle) * radius}
edge_next := center + [2]f32{math.cos(next_angle) * radius, math.sin(next_angle) * radius}
idx := i * 3
vertices[idx + 0] = sv(center, inner)
vertices[idx + 1] = sv(edge_next, outer)
vertices[idx + 2] = sv(edge_current, outer)
idx := i * 3
vertices[idx + 0] = sv(center, inner)
vertices[idx + 1] = sv(edge_next, outer)
vertices[idx + 2] = sv(edge_current, outer)
}
} else {
xform := build_pivot_rot(center, origin, rotation)
center_local := [2]f32{0, 0}
for i in 0 ..< segs {
current_angle := step_angle * f32(i)
next_angle := step_angle * f32(i + 1)
edge_current := [2]f32{math.cos(current_angle) * radius, math.sin(current_angle) * radius}
edge_next := [2]f32{math.cos(next_angle) * radius, math.sin(next_angle) * radius}
idx := i * 3
vertices[idx + 0] = sv(apply_transform(xform, center_local), inner)
vertices[idx + 1] = sv(apply_transform(xform, edge_next), outer)
vertices[idx + 2] = sv(apply_transform(xform, edge_current), outer)
}
}
prepare_shape(layer, vertices)
}
triangle :: proc(layer: ^Layer, v1, v2, v3: [2]f32, color: Color) {
vertices := [3]Vertex{sv(v1, color), sv(v2, color), sv(v3, color)}
triangle :: proc(
layer: ^Layer,
v1, v2, v3: [2]f32,
color: Color,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
) {
if !needs_transform(origin, rotation) {
vertices := [3]Vertex{sv(v1, color), sv(v2, color), sv(v3, color)}
prepare_shape(layer, vertices[:])
return
}
mn := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
xform := build_pivot_rot(mn, origin, rotation)
local_v1 := v1 - mn
local_v2 := v2 - mn
local_v3 := v3 - mn
vertices := [3]Vertex {
sv(apply_transform(xform, local_v1), color),
sv(apply_transform(xform, local_v2), color),
sv(apply_transform(xform, local_v3), color),
}
prepare_shape(layer, vertices[:])
}
@@ -259,13 +233,28 @@ triangle_lines :: proc(
v1, v2, v3: [2]f32,
color: Color,
thick: f32 = 1,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
temp_allocator := context.temp_allocator,
) {
vertices := make([]Vertex, 18, temp_allocator)
write_offset := 0
write_offset += extrude_line(v1, v2, thick, color, vertices, write_offset)
write_offset += extrude_line(v2, v3, thick, color, vertices, write_offset)
write_offset += extrude_line(v3, v1, thick, color, vertices, write_offset)
if !needs_transform(origin, rotation) {
write_offset += extrude_line(v1, v2, thick, color, vertices, write_offset)
write_offset += extrude_line(v2, v3, thick, color, vertices, write_offset)
write_offset += extrude_line(v3, v1, thick, color, vertices, write_offset)
} else {
mn := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
xform := build_pivot_rot(mn, origin, rotation)
tv1 := apply_transform(xform, v1 - mn)
tv2 := apply_transform(xform, v2 - mn)
tv3 := apply_transform(xform, v3 - mn)
write_offset += extrude_line(tv1, tv2, thick, color, vertices, write_offset)
write_offset += extrude_line(tv2, tv3, thick, color, vertices, write_offset)
write_offset += extrude_line(tv3, tv1, thick, color, vertices, write_offset)
}
if write_offset > 0 {
prepare_shape(layer, vertices[:write_offset])
}
@@ -275,6 +264,8 @@ triangle_fan :: proc(
layer: ^Layer,
points: [][2]f32,
color: Color,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
temp_allocator := context.temp_allocator,
) {
if len(points) < 3 do return
@@ -283,11 +274,26 @@ triangle_fan :: proc(
vertex_count := triangle_count * 3
vertices := make([]Vertex, vertex_count, temp_allocator)
for i in 1 ..< len(points) - 1 {
idx := (i - 1) * 3
vertices[idx + 0] = sv(points[0], color)
vertices[idx + 1] = sv(points[i], color)
vertices[idx + 2] = sv(points[i + 1], color)
if !needs_transform(origin, rotation) {
for i in 1 ..< len(points) - 1 {
idx := (i - 1) * 3
vertices[idx + 0] = sv(points[0], color)
vertices[idx + 1] = sv(points[i], color)
vertices[idx + 2] = sv(points[i + 1], color)
}
} else {
mn := [2]f32{max(f32), max(f32)}
for p in points {
mn.x = min(mn.x, p.x)
mn.y = min(mn.y, p.y)
}
xform := build_pivot_rot(mn, origin, rotation)
for i in 1 ..< len(points) - 1 {
idx := (i - 1) * 3
vertices[idx + 0] = sv(apply_transform(xform, points[0] - mn), color)
vertices[idx + 1] = sv(apply_transform(xform, points[i] - mn), color)
vertices[idx + 2] = sv(apply_transform(xform, points[i + 1] - mn), color)
}
}
prepare_shape(layer, vertices)
@@ -297,6 +303,8 @@ triangle_strip :: proc(
layer: ^Layer,
points: [][2]f32,
color: Color,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
temp_allocator := context.temp_allocator,
) {
if len(points) < 3 do return
@@ -305,16 +313,37 @@ triangle_strip :: proc(
vertex_count := triangle_count * 3
vertices := make([]Vertex, vertex_count, temp_allocator)
for i in 0 ..< triangle_count {
idx := i * 3
if i % 2 == 0 {
vertices[idx + 0] = sv(points[i], color)
vertices[idx + 1] = sv(points[i + 1], color)
vertices[idx + 2] = sv(points[i + 2], color)
} else {
vertices[idx + 0] = sv(points[i + 1], color)
vertices[idx + 1] = sv(points[i], color)
vertices[idx + 2] = sv(points[i + 2], color)
if !needs_transform(origin, rotation) {
for i in 0 ..< triangle_count {
idx := i * 3
if i % 2 == 0 {
vertices[idx + 0] = sv(points[i], color)
vertices[idx + 1] = sv(points[i + 1], color)
vertices[idx + 2] = sv(points[i + 2], color)
} else {
vertices[idx + 0] = sv(points[i + 1], color)
vertices[idx + 1] = sv(points[i], color)
vertices[idx + 2] = sv(points[i + 2], color)
}
}
} else {
mn := [2]f32{max(f32), max(f32)}
for p in points {
mn.x = min(mn.x, p.x)
mn.y = min(mn.y, p.y)
}
xform := build_pivot_rot(mn, origin, rotation)
for i in 0 ..< triangle_count {
idx := i * 3
if i % 2 == 0 {
vertices[idx + 0] = sv(apply_transform(xform, points[i] - mn), color)
vertices[idx + 1] = sv(apply_transform(xform, points[i + 1] - mn), color)
vertices[idx + 2] = sv(apply_transform(xform, points[i + 2] - mn), color)
} else {
vertices[idx + 0] = sv(apply_transform(xform, points[i + 1] - mn), color)
vertices[idx + 1] = sv(apply_transform(xform, points[i] - mn), color)
vertices[idx + 2] = sv(apply_transform(xform, points[i + 2] - mn), color)
}
}
}
@@ -323,8 +352,68 @@ triangle_strip :: proc(
// ----- SDF drawing functions ----
// Compute new center position after rotating a center-parametrized shape
// around a pivot point. The pivot is at (center + origin) in world space.
@(private = "file")
compute_pivot_center :: proc(center: [2]f32, origin: [2]f32, rotation_deg: f32) -> [2]f32 {
if origin == {0, 0} do return center
theta := math.to_radians(rotation_deg)
c, s := math.cos(theta), math.sin(theta)
// pivot = center + origin; new_center = pivot + R(θ) * (center - pivot)
return center + origin + {c * (-origin.x) - s * (-origin.y), s * (-origin.x) + c * (-origin.y)}
}
// Compute the AABB half-extents of a rectangle with half-size (hx, hy) rotated by rot_rad.
@(private = "file")
rotated_aabb_half :: proc(hx, hy, rot_rad: f32) -> [2]f32 {
c_r := abs(math.cos(rot_rad))
s_r := abs(math.sin(rot_rad))
return {hx * c_r + hy * s_r, hx * s_r + hy * c_r}
}
// Draw a filled rectangle via SDF (analytical anti-aliasing at all orientations).
// `roundness` is a 01 fraction controlling uniform corner rounding — 0 is sharp, 1 is fully rounded.
// For per-corner pixel-precise rounding, use `rectangle_corners` instead.
rectangle :: proc(
layer: ^Layer,
rect: Rectangle,
color: Color,
roundness: f32 = 0,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
cr := min(rect.w, rect.h) * clamp(roundness, 0, 1) * 0.5
rectangle_corners(layer, rect, {cr, cr, cr, cr}, color, origin, rotation, soft_px)
}
// Draw a stroked rectangle via SDF (analytical anti-aliasing at all orientations).
// `roundness` is a 01 fraction controlling uniform corner rounding — 0 is sharp, 1 is fully rounded.
// For per-corner pixel-precise rounding, use `rectangle_corners_lines` instead.
rectangle_lines :: proc(
layer: ^Layer,
rect: Rectangle,
color: Color,
thick: f32 = 1,
roundness: f32 = 0,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
cr := min(rect.w, rect.h) * clamp(roundness, 0, 1) * 0.5
rectangle_corners_lines(layer, rect, {cr, cr, cr, cr}, color, thick, origin, rotation, soft_px)
}
// Draw a rectangle with per-corner rounding radii via SDF.
rectangle_corners :: proc(layer: ^Layer, rect: Rectangle, radii: [4]f32, color: Color, soft_px: f32 = 1.0) {
rectangle_corners :: proc(
layer: ^Layer,
rect: Rectangle,
radii: [4]f32,
color: Color,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
max_radius := min(rect.w, rect.h) * 0.5
tl := clamp(radii[0], 0, max_radius)
tr := clamp(radii[1], 0, max_radius)
@@ -334,13 +423,35 @@ rectangle_corners :: proc(layer: ^Layer, rect: Rectangle, radii: [4]f32, color:
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
hx := rect.w * 0.5
hy := rect.h * 0.5
rot_rad: f32 = 0
center_x := rect.x + hx
center_y := rect.y + hy
if needs_transform(origin, rotation) {
rot_rad = math.to_radians(rotation)
xform := build_pivot_rot({rect.x, rect.y}, origin, rotation)
new_center := apply_transform(xform, {hx, hy})
center_x = new_center.x
center_y = new_center.y
}
bhx, bhy := hx, hy
if rot_rad != 0 {
expanded := rotated_aabb_half(hx, hy, rot_rad)
bhx = expanded.x
bhy = expanded.y
}
prim := Primitive {
bounds = {rect.x - pad, rect.y - pad, rect.x + rect.w + pad, rect.y + rect.h + pad},
bounds = {center_x - bhx - pad, center_y - bhy - pad, center_x + bhx + pad, center_y + bhy + pad},
color = color,
kind_flags = pack_kind_flags(.RRect, {}),
rotation = rot_rad,
}
prim.params.rrect = RRect_Params {
half_size = {rect.w * 0.5 * dpi, rect.h * 0.5 * dpi},
half_size = {hx * dpi, hy * dpi},
radii = {tr * dpi, br * dpi, tl * dpi, bl * dpi},
soft_px = soft_px,
stroke_px = 0,
@@ -355,6 +466,8 @@ rectangle_corners_lines :: proc(
radii: [4]f32,
color: Color,
thick: f32 = 1,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
max_radius := min(rect.w, rect.h) * 0.5
@@ -366,13 +479,35 @@ rectangle_corners_lines :: proc(
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
hx := rect.w * 0.5
hy := rect.h * 0.5
rot_rad: f32 = 0
center_x := rect.x + hx
center_y := rect.y + hy
if needs_transform(origin, rotation) {
rot_rad = math.to_radians(rotation)
xform := build_pivot_rot({rect.x, rect.y}, origin, rotation)
new_center := apply_transform(xform, {hx, hy})
center_x = new_center.x
center_y = new_center.y
}
bhx, bhy := hx, hy
if rot_rad != 0 {
expanded := rotated_aabb_half(hx, hy, rot_rad)
bhx = expanded.x
bhy = expanded.y
}
prim := Primitive {
bounds = {rect.x - pad, rect.y - pad, rect.x + rect.w + pad, rect.y + rect.h + pad},
bounds = {center_x - bhx - pad, center_y - bhy - pad, center_x + bhx + pad, center_y + bhy + pad},
color = color,
kind_flags = pack_kind_flags(.RRect, {.Stroke}),
rotation = rot_rad,
}
prim.params.rrect = RRect_Params {
half_size = {rect.w * 0.5 * dpi, rect.h * 0.5 * dpi},
half_size = {hx * dpi, hy * dpi},
radii = {tr * dpi, br * dpi, tl * dpi, bl * dpi},
soft_px = soft_px,
stroke_px = thick * dpi,
@@ -380,47 +515,34 @@ rectangle_corners_lines :: proc(
prepare_sdf_primitive(layer, prim)
}
// Draw a rectangle with uniform corner rounding via SDF.
rectangle_rounded :: proc(layer: ^Layer, rect: Rectangle, roundness: f32, color: Color, soft_px: f32 = 1.0) {
cr := min(rect.w, rect.h) * clamp(roundness, 0, 1) * 0.5
if cr < 1 {
rectangle(layer, rect, color)
return
}
rectangle_corners(layer, rect, {cr, cr, cr, cr}, color, soft_px)
}
// Draw a stroked rectangle with uniform corner rounding via SDF.
rectangle_rounded_lines :: proc(
// Draw a filled circle via SDF.
circle :: proc(
layer: ^Layer,
rect: Rectangle,
roundness: f32,
center: [2]f32,
radius: f32,
color: Color,
thick: f32 = 1,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
cr := min(rect.w, rect.h) * clamp(roundness, 0, 1) * 0.5
if cr < 1 {
rectangle_lines(layer, rect, color, thick)
return
}
rectangle_corners_lines(layer, rect, {cr, cr, cr, cr}, color, thick, soft_px)
}
// Draw a filled circle via SDF.
circle :: proc(layer: ^Layer, center: [2]f32, radius: f32, color: Color, soft_px: f32 = 1.0) {
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
actual_center := center
if origin != {0, 0} {
actual_center = compute_pivot_center(center, origin, rotation)
}
prim := Primitive {
bounds = {
center.x - radius - pad,
center.y - radius - pad,
center.x + radius + pad,
center.y + radius + pad,
actual_center.x - radius - pad,
actual_center.y - radius - pad,
actual_center.x + radius + pad,
actual_center.y + radius + pad,
},
color = color,
kind_flags = pack_kind_flags(.Circle, {}),
// rotation stays 0 — circle is rotationally symmetric
}
prim.params.circle = Circle_Params {
radius = radius * dpi,
@@ -436,17 +558,24 @@ circle_lines :: proc(
radius: f32,
color: Color,
thick: f32 = 1,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
actual_center := center
if origin != {0, 0} {
actual_center = compute_pivot_center(center, origin, rotation)
}
prim := Primitive {
bounds = {
center.x - radius - pad,
center.y - radius - pad,
center.x + radius + pad,
center.y + radius + pad,
actual_center.x - radius - pad,
actual_center.y - radius - pad,
actual_center.x + radius + pad,
actual_center.y + radius + pad,
},
color = color,
kind_flags = pack_kind_flags(.Circle, {.Stroke}),
@@ -460,19 +589,43 @@ circle_lines :: proc(
}
// Draw a filled ellipse via SDF.
ellipse :: proc(layer: ^Layer, center: [2]f32, radius_h, radius_v: f32, color: Color, soft_px: f32 = 1.0) {
ellipse :: proc(
layer: ^Layer,
center: [2]f32,
radius_h, radius_v: f32,
color: Color,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
actual_center := center
rot_rad: f32 = 0
if needs_transform(origin, rotation) {
actual_center = compute_pivot_center(center, origin, rotation)
rot_rad = math.to_radians(rotation)
}
// When rotated, expand the bounds AABB to enclose the rotated ellipse
bound_h, bound_v := radius_h, radius_v
if rot_rad != 0 {
expanded := rotated_aabb_half(radius_h, radius_v, rot_rad)
bound_h = expanded.x
bound_v = expanded.y
}
prim := Primitive {
bounds = {
center.x - radius_h - pad,
center.y - radius_v - pad,
center.x + radius_h + pad,
center.y + radius_v + pad,
actual_center.x - bound_h - pad,
actual_center.y - bound_v - pad,
actual_center.x + bound_h + pad,
actual_center.y + bound_v + pad,
},
color = color,
kind_flags = pack_kind_flags(.Ellipse, {}),
rotation = rot_rad,
}
prim.params.ellipse = Ellipse_Params {
radii = {radius_h * dpi, radius_v * dpi},
@@ -488,22 +641,40 @@ ellipse_lines :: proc(
radius_h, radius_v: f32,
color: Color,
thick: f32 = 1,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
// Extra 10% padding: iq's sdEllipse has precision degradation near the tips of highly
// eccentric ellipses, so the quad needs additional breathing room beyond the stroke width.
pad := (max(radius_h, radius_v) * 0.1 + thick * 0.5 + soft_px) / GLOB.dpi_scaling
extra := max(radius_h, radius_v) * 0.1 + thick * 0.5
pad := (extra + soft_px) / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
actual_center := center
rot_rad: f32 = 0
if needs_transform(origin, rotation) {
actual_center = compute_pivot_center(center, origin, rotation)
rot_rad = math.to_radians(rotation)
}
bound_h, bound_v := radius_h, radius_v
if rot_rad != 0 {
expanded := rotated_aabb_half(radius_h, radius_v, rot_rad)
bound_h = expanded.x
bound_v = expanded.y
}
prim := Primitive {
bounds = {
center.x - radius_h - pad,
center.y - radius_v - pad,
center.x + radius_h + pad,
center.y + radius_v + pad,
actual_center.x - bound_h - pad,
actual_center.y - bound_v - pad,
actual_center.x + bound_h + pad,
actual_center.y + bound_v + pad,
},
color = color,
kind_flags = pack_kind_flags(.Ellipse, {.Stroke}),
rotation = rot_rad,
}
prim.params.ellipse = Ellipse_Params {
radii = {radius_h * dpi, radius_v * dpi},
@@ -520,26 +691,36 @@ ring :: proc(
inner_radius, outer_radius: f32,
start_angle, end_angle: f32,
color: Color,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
actual_center := center
rotation_offset: f32 = 0
if needs_transform(origin, rotation) {
actual_center = compute_pivot_center(center, origin, rotation)
rotation_offset = math.to_radians(rotation)
}
prim := Primitive {
bounds = {
center.x - outer_radius - pad,
center.y - outer_radius - pad,
center.x + outer_radius + pad,
center.y + outer_radius + pad,
actual_center.x - outer_radius - pad,
actual_center.y - outer_radius - pad,
actual_center.x + outer_radius + pad,
actual_center.y + outer_radius + pad,
},
color = color,
kind_flags = pack_kind_flags(.Ring_Arc, {}),
// No shader rotation — arc rotation handled by offsetting start/end angles
}
prim.params.ring_arc = Ring_Arc_Params {
inner_radius = inner_radius * dpi,
outer_radius = outer_radius * dpi,
start_rad = math.to_radians(start_angle),
end_rad = math.to_radians(end_angle),
start_rad = math.to_radians(start_angle) + rotation_offset,
end_rad = math.to_radians(end_angle) + rotation_offset,
soft_px = soft_px,
}
prepare_sdf_primitive(layer, prim)
@@ -553,39 +734,50 @@ ring_lines :: proc(
start_angle, end_angle: f32,
color: Color,
thick: f32 = 1,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
// Inner arc outline
// Compute effective angles and pivot-translated center up front
eff_start := start_angle + rotation
eff_end := end_angle + rotation
actual_center := center
if needs_transform(origin, rotation) {
actual_center = compute_pivot_center(center, origin, rotation)
}
// Inner arc outline (pass already-transformed center; no further origin/rotation)
ring(
layer,
center,
actual_center,
max(0, inner_radius - thick * 0.5),
inner_radius + thick * 0.5,
start_angle,
end_angle,
eff_start,
eff_end,
color,
soft_px,
soft_px = soft_px,
)
// Outer arc outline
ring(
layer,
center,
actual_center,
max(0, outer_radius - thick * 0.5),
outer_radius + thick * 0.5,
start_angle,
end_angle,
eff_start,
eff_end,
color,
soft_px,
soft_px = soft_px,
)
// Start cap
start_rad := math.to_radians(start_angle)
end_rad := math.to_radians(end_angle)
inner_start := center + {math.cos(start_rad) * inner_radius, math.sin(start_rad) * inner_radius}
outer_start := center + {math.cos(start_rad) * outer_radius, math.sin(start_rad) * outer_radius}
start_rad := math.to_radians(eff_start)
end_rad := math.to_radians(eff_end)
inner_start := actual_center + {math.cos(start_rad) * inner_radius, math.sin(start_rad) * inner_radius}
outer_start := actual_center + {math.cos(start_rad) * outer_radius, math.sin(start_rad) * outer_radius}
line(layer, inner_start, outer_start, color, thick, soft_px)
// End cap
inner_end := center + {math.cos(end_rad) * inner_radius, math.sin(end_rad) * inner_radius}
outer_end := center + {math.cos(end_rad) * outer_radius, math.sin(end_rad) * outer_radius}
inner_end := actual_center + {math.cos(end_rad) * inner_radius, math.sin(end_rad) * inner_radius}
outer_end := actual_center + {math.cos(end_rad) * outer_radius, math.sin(end_rad) * outer_radius}
line(layer, inner_end, outer_end, color, thick, soft_px)
}
@@ -632,18 +824,24 @@ poly :: proc(
radius: f32,
color: Color,
rotation: f32 = 0,
origin: [2]f32 = {0, 0},
soft_px: f32 = 1.0,
) {
if sides < 3 do return
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
actual_center := center
if origin != {0, 0} && rotation != 0 {
actual_center = compute_pivot_center(center, origin, rotation)
}
prim := Primitive {
bounds = {
center.x - radius - pad,
center.y - radius - pad,
center.x + radius + pad,
center.y + radius + pad,
actual_center.x - radius - pad,
actual_center.y - radius - pad,
actual_center.x + radius + pad,
actual_center.y + radius + pad,
},
color = color,
kind_flags = pack_kind_flags(.NGon, {}),
@@ -665,6 +863,7 @@ poly_lines :: proc(
radius: f32,
color: Color,
rotation: f32 = 0,
origin: [2]f32 = {0, 0},
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
@@ -672,12 +871,17 @@ poly_lines :: proc(
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
actual_center := center
if origin != {0, 0} && rotation != 0 {
actual_center = compute_pivot_center(center, origin, rotation)
}
prim := Primitive {
bounds = {
center.x - radius - pad,
center.y - radius - pad,
center.x + radius + pad,
center.y + radius + pad,
actual_center.x - radius - pad,
actual_center.y - radius - pad,
actual_center.x + radius + pad,
actual_center.y + radius + pad,
},
color = color,
kind_flags = pack_kind_flags(.NGon, {.Stroke}),
@@ -691,3 +895,103 @@ poly_lines :: proc(
}
prepare_sdf_primitive(layer, prim)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Anchor helpers ----------------
// ---------------------------------------------------------------------------------------------------------------------
//
// Return [2]f32 pixel offsets for use as the `origin` parameter of draw calls.
// Composable with normal vector +/- arithmetic.
//
// Text anchor helpers are in text.odin (they depend on measure_text / SDL_ttf).
// ----- Rectangle anchors (origin measured from rect's top-left) --------------------------------------------------
center_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
return {r.w * 0.5, r.h * 0.5}
}
top_left_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
return {0, 0}
}
top_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
return {r.w * 0.5, 0}
}
top_right_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
return {r.w, 0}
}
left_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
return {0, r.h * 0.5}
}
right_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
return {r.w, r.h * 0.5}
}
bottom_left_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
return {0, r.h}
}
bottom_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
return {r.w * 0.5, r.h}
}
bottom_right_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
return {r.w, r.h}
}
// ----- Triangle anchors (origin measured from AABB top-left) -----------------------------------------------------
center_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
mn := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
return (v1 + v2 + v3) / 3 - mn
}
top_left_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
return {0, 0}
}
top_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
mn_x := min(v1.x, v2.x, v3.x)
mx_x := max(v1.x, v2.x, v3.x)
return {(mx_x - mn_x) * 0.5, 0}
}
top_right_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
mn_x := min(v1.x, v2.x, v3.x)
mx_x := max(v1.x, v2.x, v3.x)
return {mx_x - mn_x, 0}
}
left_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
mn_y := min(v1.y, v2.y, v3.y)
mx_y := max(v1.y, v2.y, v3.y)
return {0, (mx_y - mn_y) * 0.5}
}
right_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
mn := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
mx := [2]f32{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return {mx.x - mn.x, (mx.y - mn.y) * 0.5}
}
bottom_left_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
mn_y := min(v1.y, v2.y, v3.y)
mx_y := max(v1.y, v2.y, v3.y)
return {0, mx_y - mn_y}
}
bottom_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
mn := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
mx := [2]f32{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return {(mx.x - mn.x) * 0.5, mx.y - mn.y}
}
bottom_right_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
mn := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
mx := [2]f32{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return mx - mn
}