Files
levlib/draw/shapes.odin

1024 lines
31 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)
theta := math.acos(acos_arg)
if theta <= 0 do return 4
full_circle_segments := int(math.ceil(2 * math.PI / theta))
segments := int(f32(full_circle_segments) * arc_degrees / 360.0)
min_segments := max(int(math.ceil(f64(arc_degrees / 90.0))), 4)
return max(segments, min_segments)
}
// ----- Internal helpers ----
@(private = "file")
extrude_line :: proc(
start, end_pos: [2]f32,
thickness: f32,
color: Color,
vertices: []Vertex,
offset: int,
) -> int {
direction := end_pos - start
delta_x := direction[0]
delta_y := direction[1]
length := math.sqrt(delta_x * delta_x + delta_y * delta_y)
if length < 0.0001 do return 0
scale := thickness / (2 * length)
perpendicular := [2]f32{-delta_y * scale, delta_x * scale}
p0 := start + perpendicular
p1 := start - perpendicular
p2 := end_pos - perpendicular
p3 := end_pos + perpendicular
vertices[offset + 0] = solid_vertex(p0, color)
vertices[offset + 1] = solid_vertex(p1, color)
vertices[offset + 2] = solid_vertex(p2, color)
vertices[offset + 3] = solid_vertex(p0, color)
vertices[offset + 4] = solid_vertex(p2, color)
vertices[offset + 5] = solid_vertex(p3, color)
return 6
}
// Create a vertex for solid-color shape drawing (no texture, UV defaults to zero).
@(private = "file")
solid_vertex :: proc(position: [2]f32, color: Color) -> Vertex {
return Vertex{position = position, color = color}
}
@(private = "file")
emit_rectangle :: proc(x, y, width, height: f32, color: Color, vertices: []Vertex, offset: int) {
vertices[offset + 0] = solid_vertex({x, y}, color)
vertices[offset + 1] = solid_vertex({x + width, y}, color)
vertices[offset + 2] = solid_vertex({x + width, y + height}, color)
vertices[offset + 3] = solid_vertex({x, y}, color)
vertices[offset + 4] = solid_vertex({x + width, y + height}, color)
vertices[offset + 5] = solid_vertex({x, y + height}, color)
}
// ----- Drawing functions ----
pixel :: proc(layer: ^Layer, pos: [2]f32, color: Color) {
vertices: [6]Vertex
emit_rectangle(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)
corner_top_left := [2]f32{rect.x, rect.y}
corner_top_right := [2]f32{rect.x + rect.width, rect.y}
corner_bottom_right := [2]f32{rect.x + rect.width, rect.y + rect.height}
corner_bottom_left := [2]f32{rect.x, rect.y + rect.height}
vertices[0] = solid_vertex(corner_top_left, top_left)
vertices[1] = solid_vertex(corner_top_right, top_right)
vertices[2] = solid_vertex(corner_bottom_right, bottom_right)
vertices[3] = solid_vertex(corner_top_left, top_left)
vertices[4] = solid_vertex(corner_bottom_right, bottom_right)
vertices[5] = solid_vertex(corner_bottom_left, 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)
segment_count := segments > 0 ? segments : auto_segments(radius, arc_length)
vertex_count := segment_count * 3
vertices := make([]Vertex, vertex_count, temp_allocator)
start_radians := math.to_radians(start_angle)
end_radians := math.to_radians(end_angle)
step_angle := (end_radians - start_radians) / f32(segment_count)
if !needs_transform(origin, rotation) {
for i in 0 ..< segment_count {
current_angle := start_radians + step_angle * f32(i)
next_angle := start_radians + 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] = solid_vertex(center, color)
vertices[idx + 1] = solid_vertex(edge_next, color)
vertices[idx + 2] = solid_vertex(edge_current, color)
}
} else {
transform := build_pivot_rotation(center, origin, rotation)
center_local := [2]f32{0, 0}
for i in 0 ..< segment_count {
current_angle := start_radians + step_angle * f32(i)
next_angle := start_radians + 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] = solid_vertex(apply_transform(transform, center_local), color)
vertices[idx + 1] = solid_vertex(apply_transform(transform, edge_next), color)
vertices[idx + 2] = solid_vertex(apply_transform(transform, 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,
) {
segment_count := segments > 0 ? segments : auto_segments(radius, 360)
vertex_count := segment_count * 3
vertices := make([]Vertex, vertex_count, temp_allocator)
step_angle := math.TAU / f32(segment_count)
if !needs_transform(origin, rotation) {
for i in 0 ..< segment_count {
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] = solid_vertex(center, inner)
vertices[idx + 1] = solid_vertex(edge_next, outer)
vertices[idx + 2] = solid_vertex(edge_current, outer)
}
} else {
transform := build_pivot_rotation(center, origin, rotation)
center_local := [2]f32{0, 0}
for i in 0 ..< segment_count {
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] = solid_vertex(apply_transform(transform, center_local), inner)
vertices[idx + 1] = solid_vertex(apply_transform(transform, edge_next), outer)
vertices[idx + 2] = solid_vertex(apply_transform(transform, 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{solid_vertex(v1, color), solid_vertex(v2, color), solid_vertex(v3, color)}
prepare_shape(layer, vertices[:])
return
}
bounds_min := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
transform := build_pivot_rotation(bounds_min, origin, rotation)
local_v1 := v1 - bounds_min
local_v2 := v2 - bounds_min
local_v3 := v3 - bounds_min
vertices := [3]Vertex {
solid_vertex(apply_transform(transform, local_v1), color),
solid_vertex(apply_transform(transform, local_v2), color),
solid_vertex(apply_transform(transform, local_v3), color),
}
prepare_shape(layer, vertices[:])
}
triangle_lines :: proc(
layer: ^Layer,
v1, v2, v3: [2]f32,
color: Color,
thickness: 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, thickness, color, vertices, write_offset)
write_offset += extrude_line(v2, v3, thickness, color, vertices, write_offset)
write_offset += extrude_line(v3, v1, thickness, color, vertices, write_offset)
} else {
bounds_min := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
transform := build_pivot_rotation(bounds_min, origin, rotation)
transformed_v1 := apply_transform(transform, v1 - bounds_min)
transformed_v2 := apply_transform(transform, v2 - bounds_min)
transformed_v3 := apply_transform(transform, v3 - bounds_min)
write_offset += extrude_line(transformed_v1, transformed_v2, thickness, color, vertices, write_offset)
write_offset += extrude_line(transformed_v2, transformed_v3, thickness, color, vertices, write_offset)
write_offset += extrude_line(transformed_v3, transformed_v1, thickness, 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] = solid_vertex(points[0], color)
vertices[idx + 1] = solid_vertex(points[i], color)
vertices[idx + 2] = solid_vertex(points[i + 1], color)
}
} else {
bounds_min := [2]f32{max(f32), max(f32)}
for point in points {
bounds_min.x = min(bounds_min.x, point.x)
bounds_min.y = min(bounds_min.y, point.y)
}
transform := build_pivot_rotation(bounds_min, origin, rotation)
for i in 1 ..< len(points) - 1 {
idx := (i - 1) * 3
vertices[idx + 0] = solid_vertex(apply_transform(transform, points[0] - bounds_min), color)
vertices[idx + 1] = solid_vertex(apply_transform(transform, points[i] - bounds_min), color)
vertices[idx + 2] = solid_vertex(apply_transform(transform, points[i + 1] - bounds_min), 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] = solid_vertex(points[i], color)
vertices[idx + 1] = solid_vertex(points[i + 1], color)
vertices[idx + 2] = solid_vertex(points[i + 2], color)
} else {
vertices[idx + 0] = solid_vertex(points[i + 1], color)
vertices[idx + 1] = solid_vertex(points[i], color)
vertices[idx + 2] = solid_vertex(points[i + 2], color)
}
}
} else {
bounds_min := [2]f32{max(f32), max(f32)}
for point in points {
bounds_min.x = min(bounds_min.x, point.x)
bounds_min.y = min(bounds_min.y, point.y)
}
transform := build_pivot_rotation(bounds_min, origin, rotation)
for i in 0 ..< triangle_count {
idx := i * 3
if i % 2 == 0 {
vertices[idx + 0] = solid_vertex(apply_transform(transform, points[i] - bounds_min), color)
vertices[idx + 1] = solid_vertex(apply_transform(transform, points[i + 1] - bounds_min), color)
vertices[idx + 2] = solid_vertex(apply_transform(transform, points[i + 2] - bounds_min), color)
} else {
vertices[idx + 0] = solid_vertex(apply_transform(transform, points[i + 1] - bounds_min), color)
vertices[idx + 1] = solid_vertex(apply_transform(transform, points[i] - bounds_min), color)
vertices[idx + 2] = solid_vertex(apply_transform(transform, points[i + 2] - bounds_min), 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)
cos_angle, sin_angle := math.cos(theta), math.sin(theta)
// pivot = center + origin; new_center = pivot + R(θ) * (center - pivot)
return(
center +
origin +
{cos_angle * (-origin.x) - sin_angle * (-origin.y), sin_angle * (-origin.x) + cos_angle * (-origin.y)} \
)
}
// Compute the AABB half-extents of a rectangle with half-size (half_width, half_height) rotated by rotation_radians.
@(private = "file")
rotated_aabb_half_extents :: proc(half_width, half_height, rotation_radians: f32) -> [2]f32 {
cos_abs := abs(math.cos(rotation_radians))
sin_abs := abs(math.sin(rotation_radians))
return {half_width * cos_abs + half_height * sin_abs, half_width * sin_abs + half_height * cos_abs}
}
// 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.width, rect.height) * 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,
thickness: f32 = 1,
roundness: f32 = 0,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
cr := min(rect.width, rect.height) * clamp(roundness, 0, 1) * 0.5
rectangle_corners_lines(layer, rect, {cr, cr, cr, cr}, color, thickness, 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.width, rect.height) * 0.5
top_left := clamp(radii[0], 0, max_radius)
top_right := clamp(radii[1], 0, max_radius)
bottom_right := clamp(radii[2], 0, max_radius)
bottom_left := clamp(radii[3], 0, max_radius)
padding := soft_px / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
half_width := rect.width * 0.5
half_height := rect.height * 0.5
rotation_radians: f32 = 0
center_x := rect.x + half_width
center_y := rect.y + half_height
if needs_transform(origin, rotation) {
rotation_radians = math.to_radians(rotation)
transform := build_pivot_rotation({rect.x, rect.y}, origin, rotation)
new_center := apply_transform(transform, {half_width, half_height})
center_x = new_center.x
center_y = new_center.y
}
bounds_half_width, bounds_half_height := half_width, half_height
if rotation_radians != 0 {
expanded := rotated_aabb_half_extents(half_width, half_height, rotation_radians)
bounds_half_width = expanded.x
bounds_half_height = expanded.y
}
prim := Primitive {
bounds = {
center_x - bounds_half_width - padding,
center_y - bounds_half_height - padding,
center_x + bounds_half_width + padding,
center_y + bounds_half_height + padding,
},
color = color,
kind_flags = pack_kind_flags(.RRect, {}),
rotation = rotation_radians,
}
prim.params.rrect = RRect_Params {
half_size = {half_width * dpi_scale, half_height * dpi_scale},
radii = {
top_right * dpi_scale,
bottom_right * dpi_scale,
top_left * dpi_scale,
bottom_left * dpi_scale,
},
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,
thickness: f32 = 1,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
max_radius := min(rect.width, rect.height) * 0.5
top_left := clamp(radii[0], 0, max_radius)
top_right := clamp(radii[1], 0, max_radius)
bottom_right := clamp(radii[2], 0, max_radius)
bottom_left := clamp(radii[3], 0, max_radius)
padding := (thickness * 0.5 + soft_px) / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
half_width := rect.width * 0.5
half_height := rect.height * 0.5
rotation_radians: f32 = 0
center_x := rect.x + half_width
center_y := rect.y + half_height
if needs_transform(origin, rotation) {
rotation_radians = math.to_radians(rotation)
transform := build_pivot_rotation({rect.x, rect.y}, origin, rotation)
new_center := apply_transform(transform, {half_width, half_height})
center_x = new_center.x
center_y = new_center.y
}
bounds_half_width, bounds_half_height := half_width, half_height
if rotation_radians != 0 {
expanded := rotated_aabb_half_extents(half_width, half_height, rotation_radians)
bounds_half_width = expanded.x
bounds_half_height = expanded.y
}
prim := Primitive {
bounds = {
center_x - bounds_half_width - padding,
center_y - bounds_half_height - padding,
center_x + bounds_half_width + padding,
center_y + bounds_half_height + padding,
},
color = color,
kind_flags = pack_kind_flags(.RRect, {.Stroke}),
rotation = rotation_radians,
}
prim.params.rrect = RRect_Params {
half_size = {half_width * dpi_scale, half_height * dpi_scale},
radii = {
top_right * dpi_scale,
bottom_right * dpi_scale,
top_left * dpi_scale,
bottom_left * dpi_scale,
},
soft_px = soft_px,
stroke_px = thickness * dpi_scale,
}
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,
) {
padding := soft_px / GLOB.dpi_scaling
dpi_scale := 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 - padding,
actual_center.y - radius - padding,
actual_center.x + radius + padding,
actual_center.y + radius + padding,
},
color = color,
kind_flags = pack_kind_flags(.Circle, {}),
// rotation stays 0 — circle is rotationally symmetric
}
prim.params.circle = Circle_Params {
radius = radius * dpi_scale,
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,
thickness: f32 = 1,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
padding := (thickness * 0.5 + soft_px) / GLOB.dpi_scaling
dpi_scale := 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 - padding,
actual_center.y - radius - padding,
actual_center.x + radius + padding,
actual_center.y + radius + padding,
},
color = color,
kind_flags = pack_kind_flags(.Circle, {.Stroke}),
}
prim.params.circle = Circle_Params {
radius = radius * dpi_scale,
soft_px = soft_px,
stroke_px = thickness * dpi_scale,
}
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,
) {
padding := soft_px / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
actual_center := center
rotation_radians: f32 = 0
if needs_transform(origin, rotation) {
actual_center = compute_pivot_center(center, origin, rotation)
rotation_radians = math.to_radians(rotation)
}
// When rotated, expand the bounds AABB to enclose the rotated ellipse
bound_h, bound_v := radius_h, radius_v
if rotation_radians != 0 {
expanded := rotated_aabb_half_extents(radius_h, radius_v, rotation_radians)
bound_h = expanded.x
bound_v = expanded.y
}
prim := Primitive {
bounds = {
actual_center.x - bound_h - padding,
actual_center.y - bound_v - padding,
actual_center.x + bound_h + padding,
actual_center.y + bound_v + padding,
},
color = color,
kind_flags = pack_kind_flags(.Ellipse, {}),
rotation = rotation_radians,
}
prim.params.ellipse = Ellipse_Params {
radii = {radius_h * dpi_scale, radius_v * dpi_scale},
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,
thickness: 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 + thickness * 0.5
padding := (extra + soft_px) / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
actual_center := center
rotation_radians: f32 = 0
if needs_transform(origin, rotation) {
actual_center = compute_pivot_center(center, origin, rotation)
rotation_radians = math.to_radians(rotation)
}
bound_h, bound_v := radius_h, radius_v
if rotation_radians != 0 {
expanded := rotated_aabb_half_extents(radius_h, radius_v, rotation_radians)
bound_h = expanded.x
bound_v = expanded.y
}
prim := Primitive {
bounds = {
actual_center.x - bound_h - padding,
actual_center.y - bound_v - padding,
actual_center.x + bound_h + padding,
actual_center.y + bound_v + padding,
},
color = color,
kind_flags = pack_kind_flags(.Ellipse, {.Stroke}),
rotation = rotation_radians,
}
prim.params.ellipse = Ellipse_Params {
radii = {radius_h * dpi_scale, radius_v * dpi_scale},
soft_px = soft_px,
stroke_px = thickness * dpi_scale,
}
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,
) {
padding := soft_px / GLOB.dpi_scaling
dpi_scale := 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 - padding,
actual_center.y - outer_radius - padding,
actual_center.x + outer_radius + padding,
actual_center.y + outer_radius + padding,
},
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_scale,
outer_radius = outer_radius * dpi_scale,
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,
thickness: f32 = 1,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
// Compute effective angles and pivot-translated center up front
effective_start_angle := start_angle + rotation
effective_end_angle := 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 - thickness * 0.5),
inner_radius + thickness * 0.5,
effective_start_angle,
effective_end_angle,
color,
soft_px = soft_px,
)
// Outer arc outline
ring(
layer,
actual_center,
max(0, outer_radius - thickness * 0.5),
outer_radius + thickness * 0.5,
effective_start_angle,
effective_end_angle,
color,
soft_px = soft_px,
)
// Start cap
start_radians := math.to_radians(effective_start_angle)
end_radians := math.to_radians(effective_end_angle)
inner_start :=
actual_center + {math.cos(start_radians) * inner_radius, math.sin(start_radians) * inner_radius}
outer_start :=
actual_center + {math.cos(start_radians) * outer_radius, math.sin(start_radians) * outer_radius}
line(layer, inner_start, outer_start, color, thickness, soft_px)
// End cap
inner_end := actual_center + {math.cos(end_radians) * inner_radius, math.sin(end_radians) * inner_radius}
outer_end := actual_center + {math.cos(end_radians) * outer_radius, math.sin(end_radians) * outer_radius}
line(layer, inner_end, outer_end, color, thickness, soft_px)
}
// Draw a line segment via SDF.
line :: proc(layer: ^Layer, start, end_pos: [2]f32, color: Color, thickness: f32 = 1, soft_px: f32 = 1.0) {
cap_padding := thickness * 0.5 + soft_px / GLOB.dpi_scaling
min_x := min(start.x, end_pos.x) - cap_padding
max_x := max(start.x, end_pos.x) + cap_padding
min_y := min(start.y, end_pos.y) - cap_padding
max_y := max(start.y, end_pos.y) + cap_padding
dpi_scale := GLOB.dpi_scaling
center := [2]f32{(min_x + max_x) * 0.5, (min_y + max_y) * 0.5}
local_start := (start - center) * dpi_scale
local_end := (end_pos - center) * dpi_scale
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_start,
b = local_end,
width = thickness * dpi_scale,
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, thickness: 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, thickness, soft_px)
}
}
// Draw a filled regular polygon via SDF.
polygon :: 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
padding := soft_px / GLOB.dpi_scaling
dpi_scale := 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 - padding,
actual_center.y - radius - padding,
actual_center.x + radius + padding,
actual_center.y + radius + padding,
},
color = color,
kind_flags = pack_kind_flags(.NGon, {}),
}
prim.params.ngon = NGon_Params {
radius = radius * math.cos(math.PI / f32(sides)) * dpi_scale,
rotation = math.to_radians(rotation),
sides = f32(sides),
soft_px = soft_px,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a stroked regular polygon via SDF.
polygon_lines :: proc(
layer: ^Layer,
center: [2]f32,
sides: int,
radius: f32,
color: Color,
rotation: f32 = 0,
origin: [2]f32 = {0, 0},
thickness: f32 = 1,
soft_px: f32 = 1.0,
) {
if sides < 3 do return
padding := (thickness * 0.5 + soft_px) / GLOB.dpi_scaling
dpi_scale := 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 - padding,
actual_center.y - radius - padding,
actual_center.x + radius + padding,
actual_center.y + radius + padding,
},
color = color,
kind_flags = pack_kind_flags(.NGon, {.Stroke}),
}
prim.params.ngon = NGon_Params {
radius = radius * math.cos(math.PI / f32(sides)) * dpi_scale,
rotation = math.to_radians(rotation),
sides = f32(sides),
soft_px = soft_px,
stroke_px = thickness * dpi_scale,
}
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 rectangle's top-left) ---------------------------------------------
center_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> [2]f32 {
return {rectangle.width * 0.5, rectangle.height * 0.5}
}
top_left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> [2]f32 {
return {0, 0}
}
top_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> [2]f32 {
return {rectangle.width * 0.5, 0}
}
top_right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> [2]f32 {
return {rectangle.width, 0}
}
left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> [2]f32 {
return {0, rectangle.height * 0.5}
}
right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> [2]f32 {
return {rectangle.width, rectangle.height * 0.5}
}
bottom_left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> [2]f32 {
return {0, rectangle.height}
}
bottom_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> [2]f32 {
return {rectangle.width * 0.5, rectangle.height}
}
bottom_right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> [2]f32 {
return {rectangle.width, rectangle.height}
}
// ----- Triangle anchors (origin measured from AABB top-left) -----------------------------------------------------
center_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
bounds_min := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
return (v1 + v2 + v3) / 3 - bounds_min
}
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 {
min_x := min(v1.x, v2.x, v3.x)
max_x := max(v1.x, v2.x, v3.x)
return {(max_x - min_x) * 0.5, 0}
}
top_right_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
min_x := min(v1.x, v2.x, v3.x)
max_x := max(v1.x, v2.x, v3.x)
return {max_x - min_x, 0}
}
left_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
min_y := min(v1.y, v2.y, v3.y)
max_y := max(v1.y, v2.y, v3.y)
return {0, (max_y - min_y) * 0.5}
}
right_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
bounds_min := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
bounds_max := [2]f32{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return {bounds_max.x - bounds_min.x, (bounds_max.y - bounds_min.y) * 0.5}
}
bottom_left_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
min_y := min(v1.y, v2.y, v3.y)
max_y := max(v1.y, v2.y, v3.y)
return {0, max_y - min_y}
}
bottom_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
bounds_min := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
bounds_max := [2]f32{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return {(bounds_max.x - bounds_min.x) * 0.5, bounds_max.y - bounds_min.y}
}
bottom_right_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
bounds_min := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
bounds_max := [2]f32{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return bounds_max - bounds_min
}