Files
levlib/draw/shapes.odin

998 lines
28 KiB
Odin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package draw
import "core:math"
SMOOTH_CIRCLE_ERROR_RATE :: 0.1
// ----- Adaptive tessellation ----
auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
if radius <= 0 do return 4
phys_radius := radius * GLOB.dpi_scaling
acos_arg := clamp(2 * math.pow(1 - SMOOTH_CIRCLE_ERROR_RATE / phys_radius, 2) - 1, -1, 1)
th := math.acos(acos_arg)
if th <= 0 do return 4
full_circle_segs := int(math.ceil(2 * math.PI / th))
segs := int(f32(full_circle_segs) * arc_degrees / 360.0)
min_segs := max(int(math.ceil(f64(arc_degrees / 90.0))), 4)
return max(segs, min_segs)
}
// ----- Internal helpers ----
@(private = "file")
extrude_line :: proc(
start, end_pos: [2]f32,
thick: f32,
color: Color,
vertices: []Vertex,
offset: int,
) -> int {
direction := end_pos - start
dx := direction[0]
dy := direction[1]
length := math.sqrt(dx * dx + dy * dy)
if length < 0.0001 do return 0
scale := thick / (2 * length)
perpendicular := [2]f32{-dy * scale, dx * scale}
p0 := start + perpendicular
p1 := start - perpendicular
p2 := end_pos - perpendicular
p3 := end_pos + perpendicular
vertices[offset + 0] = sv(p0, color)
vertices[offset + 1] = sv(p1, color)
vertices[offset + 2] = sv(p2, color)
vertices[offset + 3] = sv(p0, color)
vertices[offset + 4] = sv(p2, color)
vertices[offset + 5] = sv(p3, color)
return 6
}
// Create a vertex for solid-color shape drawing (no texture, UV defaults to zero).
@(private = "file")
sv :: proc(pos: [2]f32, color: Color) -> Vertex {
return Vertex{position = pos, color = color}
}
@(private = "file")
emit_rect :: proc(x, y, w, h: f32, color: Color, vertices: []Vertex, offset: int) {
vertices[offset + 0] = sv({x, y}, color)
vertices[offset + 1] = sv({x + w, y}, color)
vertices[offset + 2] = sv({x + w, y + h}, color)
vertices[offset + 3] = sv({x, y}, color)
vertices[offset + 4] = sv({x + w, y + h}, color)
vertices[offset + 5] = sv({x, y + h}, color)
}
// ----- Drawing functions ----
pixel :: proc(layer: ^Layer, pos: [2]f32, color: Color) {
vertices: [6]Vertex
emit_rect(pos[0], pos[1], 1, 1, color, vertices[:], 0)
prepare_shape(layer, vertices[:])
}
rectangle_gradient :: proc(
layer: ^Layer,
rect: Rectangle,
top_left, top_right, bottom_left, bottom_right: Color,
temp_allocator := context.temp_allocator,
) {
vertices := make([]Vertex, 6, temp_allocator)
tl := [2]f32{rect.x, rect.y}
tr := [2]f32{rect.x + rect.w, rect.y}
br := [2]f32{rect.x + rect.w, rect.y + rect.h}
bl := [2]f32{rect.x, rect.y + rect.h}
vertices[0] = sv(tl, top_left)
vertices[1] = sv(tr, top_right)
vertices[2] = sv(br, bottom_right)
vertices[3] = sv(tl, top_left)
vertices[4] = sv(br, bottom_right)
vertices[5] = sv(bl, bottom_left)
prepare_shape(layer, vertices)
}
circle_sector :: proc(
layer: ^Layer,
center: [2]f32,
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,
) {
arc_length := abs(end_angle - start_angle)
segs := segments > 0 ? segments : auto_segments(radius, arc_length)
vertex_count := segs * 3
vertices := make([]Vertex, vertex_count, temp_allocator)
start_rad := math.to_radians(start_angle)
end_rad := math.to_radians(end_angle)
step_angle := (end_rad - start_rad) / f32(segs)
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}
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)
}
circle_gradient :: proc(
layer: ^Layer,
center: [2]f32,
radius: f32,
inner, outer: Color,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
segments: int = 0,
temp_allocator := context.temp_allocator,
) {
segs := segments > 0 ? segments : auto_segments(radius, 360)
vertex_count := segs * 3
vertices := make([]Vertex, vertex_count, temp_allocator)
step_angle := math.TAU / f32(segs)
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}
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,
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[:])
}
triangle_lines :: proc(
layer: ^Layer,
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
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])
}
}
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
triangle_count := len(points) - 2
vertex_count := triangle_count * 3
vertices := make([]Vertex, vertex_count, temp_allocator)
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)
}
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
triangle_count := len(points) - 2
vertex_count := triangle_count * 3
vertices := make([]Vertex, vertex_count, temp_allocator)
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)
}
}
}
prepare_shape(layer, vertices)
}
// ----- 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,
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)
br := clamp(radii[2], 0, max_radius)
bl := clamp(radii[3], 0, max_radius)
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 = {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 = {hx * dpi, hy * dpi},
radii = {tr * dpi, br * dpi, tl * dpi, bl * dpi},
soft_px = soft_px,
stroke_px = 0,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a stroked rectangle with per-corner rounding radii via SDF.
rectangle_corners_lines :: proc(
layer: ^Layer,
rect: Rectangle,
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
tl := clamp(radii[0], 0, max_radius)
tr := clamp(radii[1], 0, max_radius)
br := clamp(radii[2], 0, max_radius)
bl := clamp(radii[3], 0, max_radius)
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 = {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 = {hx * dpi, hy * dpi},
radii = {tr * dpi, br * dpi, tl * dpi, bl * dpi},
soft_px = soft_px,
stroke_px = thick * dpi,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a filled circle via SDF.
circle :: proc(
layer: ^Layer,
center: [2]f32,
radius: 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
if origin != {0, 0} {
actual_center = compute_pivot_center(center, origin, rotation)
}
prim := Primitive {
bounds = {
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,
soft_px = soft_px,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a stroked circle via SDF.
circle_lines :: proc(
layer: ^Layer,
center: [2]f32,
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 = {
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}),
}
prim.params.circle = Circle_Params {
radius = radius * dpi,
soft_px = soft_px,
stroke_px = thick * dpi,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a filled ellipse via SDF.
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 = {
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},
soft_px = soft_px,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a stroked ellipse via SDF.
ellipse_lines :: proc(
layer: ^Layer,
center: [2]f32,
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.
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 = {
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},
soft_px = soft_px,
stroke_px = thick * dpi,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a filled ring arc via SDF.
ring :: proc(
layer: ^Layer,
center: [2]f32,
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 = {
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) + rotation_offset,
end_rad = math.to_radians(end_angle) + rotation_offset,
soft_px = soft_px,
}
prepare_sdf_primitive(layer, prim)
}
// Draw stroked ring arc outlines via SDF.
ring_lines :: proc(
layer: ^Layer,
center: [2]f32,
inner_radius, outer_radius: f32,
start_angle, end_angle: f32,
color: Color,
thick: f32 = 1,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
// 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,
actual_center,
max(0, inner_radius - thick * 0.5),
inner_radius + thick * 0.5,
eff_start,
eff_end,
color,
soft_px = soft_px,
)
// Outer arc outline
ring(
layer,
actual_center,
max(0, outer_radius - thick * 0.5),
outer_radius + thick * 0.5,
eff_start,
eff_end,
color,
soft_px = soft_px,
)
// Start cap
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 := 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)
}
// Draw a line segment via SDF.
line :: proc(layer: ^Layer, start, end_pos: [2]f32, color: Color, thick: f32 = 1, soft_px: f32 = 1.0) {
cap := thick * 0.5 + soft_px / GLOB.dpi_scaling
min_x := min(start.x, end_pos.x) - cap
max_x := max(start.x, end_pos.x) + cap
min_y := min(start.y, end_pos.y) - cap
max_y := max(start.y, end_pos.y) + cap
dpi := GLOB.dpi_scaling
center := [2]f32{(min_x + max_x) * 0.5, (min_y + max_y) * 0.5}
local_a := (start - center) * dpi
local_b := (end_pos - center) * dpi
prim := Primitive {
bounds = {min_x, min_y, max_x, max_y},
color = color,
kind_flags = pack_kind_flags(.Segment, {}),
}
prim.params.segment = Segment_Params {
a = local_a,
b = local_b,
width = thick * dpi,
soft_px = soft_px,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a line strip via decomposed SDF segments.
line_strip :: proc(layer: ^Layer, points: [][2]f32, color: Color, thick: f32 = 1, soft_px: f32 = 1.0) {
if len(points) < 2 do return
for i in 0 ..< len(points) - 1 {
line(layer, points[i], points[i + 1], color, thick, soft_px)
}
}
// Draw a filled regular polygon via SDF.
poly :: proc(
layer: ^Layer,
center: [2]f32,
sides: int,
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 = {
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, {}),
}
prim.params.ngon = NGon_Params {
radius = radius * math.cos(math.PI / f32(sides)) * dpi,
rotation = math.to_radians(rotation),
sides = f32(sides),
soft_px = soft_px,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a stroked regular polygon via SDF.
poly_lines :: proc(
layer: ^Layer,
center: [2]f32,
sides: int,
radius: f32,
color: Color,
rotation: f32 = 0,
origin: [2]f32 = {0, 0},
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
if sides < 3 do return
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 = {
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}),
}
prim.params.ngon = NGon_Params {
radius = radius * math.cos(math.PI / f32(sides)) * dpi,
rotation = math.to_radians(rotation),
sides = f32(sides),
soft_px = soft_px,
stroke_px = thick * dpi,
}
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
}