Files
levlib/draw/shapes.odin
Zachary Levy 0d424cbd6e Texture Rendering (#9)
Co-authored-by: Zachary Levy <zachary@sunforge.is>
Reviewed-on: #9
2026-04-22 00:05:08 +00:00

1168 lines
36 KiB
Odin
Raw Permalink 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)
}
@(private = "file")
prepare_sdf_primitive_textured :: proc(
layer: ^Layer,
prim: Primitive,
texture_id: Texture_Id,
sampler: Sampler_Preset,
) {
offset := u32(len(GLOB.tmp_primitives))
append(&GLOB.tmp_primitives, prim)
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
append_or_extend_sub_batch(scissor, layer, .SDF, offset, 1, texture_id, sampler)
}
// ----- 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)
defer delete(vertices, 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)
defer delete(vertices, 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)
defer delete(vertices, 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)
defer delete(vertices, 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)
defer delete(vertices, 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)
defer delete(vertices, 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 the visual center of a center-parametrized shape after applying
// Convention B origin semantics: `center` is where the origin-point lands in
// world space; the visual center is offset by -origin and then rotated around
// the landing point.
// visual_center = center + R(θ) · (-origin)
// When θ=0: visual_center = center - origin (pure positioning shift).
// When origin={0,0}: visual_center = center (no change).
@(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)
return(
center +
{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.
//
// Origin semantics:
// `origin` is a local offset from the rect's top-left corner that selects both the positioning
// anchor and the rotation pivot. `rect.x, rect.y` specifies where that anchor point lands in
// world space. When `origin = {0, 0}` (default), `rect.x, rect.y` is the top-left corner.
// When `origin = center_of_rectangle(rect)`, `rect.x, rect.y` is the visual center.
// Rotation always occurs around the anchor point.
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.
// Origin semantics: see `rectangle`.
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.
// Origin semantics: see `rectangle`.
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 - origin.x
center_y := rect.y + half_height - origin.y
if needs_transform(origin, rotation) {
rotation_radians = math.to_radians(rotation)
transform := build_pivot_rotation({rect.x + origin.x, rect.y + origin.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.
// Origin semantics: see `rectangle`.
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 - origin.x
center_y := rect.y + half_height - origin.y
if needs_transform(origin, rotation) {
rotation_radians = math.to_radians(rotation)
transform := build_pivot_rotation({rect.x + origin.x, rect.y + origin.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 rectangle with a texture fill via SDF. Supports rounded corners via `roundness`,
// rotation, and analytical anti-aliasing on the shape silhouette.
// Origin semantics: see `rectangle`.
rectangle_texture :: proc(
layer: ^Layer,
rect: Rectangle,
id: Texture_Id,
tint: Color = WHITE,
uv_rect: Rectangle = {0, 0, 1, 1},
sampler: Sampler_Preset = .Linear_Clamp,
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_texture_corners(
layer,
rect,
{cr, cr, cr, cr},
id,
tint,
uv_rect,
sampler,
origin,
rotation,
soft_px,
)
}
// Draw a rectangle with a texture fill and per-corner rounding radii via SDF.
// Origin semantics: see `rectangle`.
rectangle_texture_corners :: proc(
layer: ^Layer,
rect: Rectangle,
radii: [4]f32,
id: Texture_Id,
tint: Color = WHITE,
uv_rect: Rectangle = {0, 0, 1, 1},
sampler: Sampler_Preset = .Linear_Clamp,
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 - origin.x
center_y := rect.y + half_height - origin.y
if needs_transform(origin, rotation) {
rotation_radians = math.to_radians(rotation)
transform := build_pivot_rotation({rect.x + origin.x, rect.y + origin.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 = tint,
kind_flags = pack_kind_flags(.RRect, {.Textured}),
rotation = rotation_radians,
uv_rect = {uv_rect.x, uv_rect.y, uv_rect.width, uv_rect.height},
}
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_textured(layer, prim, id, sampler)
}
// Draw a filled circle via SDF.
//
// Origin semantics (Convention B):
// `origin` is a local offset from the shape's center that selects both the positioning anchor
// and the rotation pivot. The `center` parameter specifies where that anchor point lands in
// world space. When `origin = {0, 0}` (default), `center` is the visual center.
// When `origin = {r, 0}`, the point `r` pixels to the right of the shape center lands at
// `center`, shifting the shape left by `r`.
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.
// Origin semantics: see `circle`.
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.
// Origin semantics: see `circle`.
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.
// Origin semantics: see `circle`.
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.
// Origin semantics: see `circle`.
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.
// Origin semantics: see `circle`.
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
}