1030 lines
32 KiB
Odin
1030 lines
32 KiB
Odin
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)
|
||
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 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 0–1 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 0–1 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
|
||
}
|