Initial draw package

This commit is contained in:
Zachary Levy
2026-04-14 16:16:50 -07:00
parent 59c600d630
commit c786147720
26 changed files with 4371 additions and 1 deletions

669
draw/shapes.odin Normal file
View File

@@ -0,0 +1,669 @@
package draw
import "core:math"
SMOOTH_CIRCLE_ERROR_RATE :: 0.1
// ----- Adaptive tessellation ----
auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
if radius <= 0 do return 4
phys_radius := radius * GLOB.dpi_scaling
acos_arg := clamp(2 * math.pow(1 - SMOOTH_CIRCLE_ERROR_RATE / phys_radius, 2) - 1, -1, 1)
th := math.acos(acos_arg)
if th <= 0 do return 4
full_circle_segs := int(math.ceil(2 * math.PI / th))
segs := int(f32(full_circle_segs) * arc_degrees / 360.0)
min_segs := max(int(math.ceil(f64(arc_degrees / 90.0))), 4)
return max(segs, min_segs)
}
// ----- Internal helpers ----
@(private = "file")
extrude_line :: proc(
start, end_pos: [2]f32,
thick: f32,
color: Color,
vertices: []Vertex,
offset: int,
) -> int {
direction := end_pos - start
dx := direction[0]
dy := direction[1]
length := math.sqrt(dx * dx + dy * dy)
if length < 0.0001 do return 0
scale := thick / (2 * length)
perpendicular := [2]f32{-dy * scale, dx * scale}
p0 := start + perpendicular
p1 := start - perpendicular
p2 := end_pos - perpendicular
p3 := end_pos + perpendicular
vertices[offset + 0] = sv(p0, color)
vertices[offset + 1] = sv(p1, color)
vertices[offset + 2] = sv(p2, color)
vertices[offset + 3] = sv(p0, color)
vertices[offset + 4] = sv(p2, color)
vertices[offset + 5] = sv(p3, color)
return 6
}
// Create a vertex for solid-color shape drawing (no texture, UV defaults to zero).
@(private = "file")
sv :: proc(pos: [2]f32, color: Color) -> Vertex {
return Vertex{position = pos, color = color}
}
@(private = "file")
emit_rect :: proc(x, y, w, h: f32, color: Color, vertices: []Vertex, offset: int) {
vertices[offset + 0] = sv({x, y}, color)
vertices[offset + 1] = sv({x + w, y}, color)
vertices[offset + 2] = sv({x + w, y + h}, color)
vertices[offset + 3] = sv({x, y}, color)
vertices[offset + 4] = sv({x + w, y + h}, color)
vertices[offset + 5] = sv({x, y + h}, color)
}
// ----- Drawing functions ----
pixel :: proc(layer: ^Layer, pos: [2]f32, color: Color) {
vertices: [6]Vertex
emit_rect(pos[0], pos[1], 1, 1, color, vertices[:], 0)
prepare_shape(layer, vertices[:])
}
rectangle :: proc(
layer: ^Layer,
rect: Rectangle,
color: Color,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
temp_allocator := context.temp_allocator,
) {
vertices := make([]Vertex, 6, temp_allocator)
if rotation == 0 {
emit_rect(rect.x, rect.y, rect.w, rect.h, color, vertices, 0)
} else {
rad := math.to_radians(rotation)
cos_rotation := math.cos(rad)
sin_rotation := math.sin(rad)
// Corners relative to origin
top_left := [2]f32{-origin[0], -origin[1]}
top_right := [2]f32{rect.w - origin[0], -origin[1]}
bottom_right := [2]f32{rect.w - origin[0], rect.h - origin[1]}
bottom_left := [2]f32{-origin[0], rect.h - origin[1]}
// Translation to final position
translate := [2]f32{rect.x + origin[0], rect.y + origin[1]}
// Rotate and translate each corner
tl :=
[2]f32 {
cos_rotation * top_left[0] - sin_rotation * top_left[1],
sin_rotation * top_left[0] + cos_rotation * top_left[1],
} +
translate
tr :=
[2]f32 {
cos_rotation * top_right[0] - sin_rotation * top_right[1],
sin_rotation * top_right[0] + cos_rotation * top_right[1],
} +
translate
br :=
[2]f32 {
cos_rotation * bottom_right[0] - sin_rotation * bottom_right[1],
sin_rotation * bottom_right[0] + cos_rotation * bottom_right[1],
} +
translate
bl :=
[2]f32 {
cos_rotation * bottom_left[0] - sin_rotation * bottom_left[1],
sin_rotation * bottom_left[0] + cos_rotation * bottom_left[1],
} +
translate
vertices[0] = sv(tl, color)
vertices[1] = sv(tr, color)
vertices[2] = sv(br, color)
vertices[3] = sv(tl, color)
vertices[4] = sv(br, color)
vertices[5] = sv(bl, color)
}
prepare_shape(layer, vertices)
}
rectangle_lines :: proc(
layer: ^Layer,
rect: Rectangle,
color: Color,
thick: f32 = 1,
temp_allocator := context.temp_allocator,
) {
vertices := make([]Vertex, 24, temp_allocator)
// Top edge
emit_rect(rect.x, rect.y, rect.w, thick, color, vertices, 0)
// Bottom edge
emit_rect(rect.x, rect.y + rect.h - thick, rect.w, thick, color, vertices, 6)
// Left edge
emit_rect(rect.x, rect.y + thick, thick, rect.h - thick * 2, color, vertices, 12)
// Right edge
emit_rect(rect.x + rect.w - thick, rect.y + thick, thick, rect.h - thick * 2, color, vertices, 18)
prepare_shape(layer, vertices)
}
rectangle_gradient :: proc(
layer: ^Layer,
rect: Rectangle,
top_left, top_right, bottom_left, bottom_right: Color,
temp_allocator := context.temp_allocator,
) {
vertices := make([]Vertex, 6, temp_allocator)
tl := [2]f32{rect.x, rect.y}
tr := [2]f32{rect.x + rect.w, rect.y}
br := [2]f32{rect.x + rect.w, rect.y + rect.h}
bl := [2]f32{rect.x, rect.y + rect.h}
vertices[0] = sv(tl, top_left)
vertices[1] = sv(tr, top_right)
vertices[2] = sv(br, bottom_right)
vertices[3] = sv(tl, top_left)
vertices[4] = sv(br, bottom_right)
vertices[5] = sv(bl, bottom_left)
prepare_shape(layer, vertices)
}
circle_sector :: proc(
layer: ^Layer,
center: [2]f32,
radius: f32,
start_angle, end_angle: f32,
color: Color,
segments: int = 0,
temp_allocator := context.temp_allocator,
) {
arc_length := abs(end_angle - start_angle)
segs := segments > 0 ? segments : auto_segments(radius, arc_length)
vertex_count := segs * 3
vertices := make([]Vertex, vertex_count, temp_allocator)
start_rad := math.to_radians(start_angle)
end_rad := math.to_radians(end_angle)
step_angle := (end_rad - start_rad) / f32(segs)
for i in 0 ..< segs {
current_angle := start_rad + step_angle * f32(i)
next_angle := start_rad + step_angle * f32(i + 1)
edge_current := center + [2]f32{math.cos(current_angle) * radius, math.sin(current_angle) * radius}
edge_next := center + [2]f32{math.cos(next_angle) * radius, math.sin(next_angle) * radius}
idx := i * 3
vertices[idx + 0] = sv(center, color)
vertices[idx + 1] = sv(edge_next, color)
vertices[idx + 2] = sv(edge_current, color)
}
prepare_shape(layer, vertices)
}
circle_gradient :: proc(
layer: ^Layer,
center: [2]f32,
radius: f32,
inner, outer: Color,
segments: int = 0,
temp_allocator := context.temp_allocator,
) {
segs := segments > 0 ? segments : auto_segments(radius, 360)
vertex_count := segs * 3
vertices := make([]Vertex, vertex_count, temp_allocator)
step_angle := math.TAU / f32(segs)
for i in 0 ..< segs {
current_angle := step_angle * f32(i)
next_angle := step_angle * f32(i + 1)
edge_current := center + [2]f32{math.cos(current_angle) * radius, math.sin(current_angle) * radius}
edge_next := center + [2]f32{math.cos(next_angle) * radius, math.sin(next_angle) * radius}
idx := i * 3
vertices[idx + 0] = sv(center, inner)
vertices[idx + 1] = sv(edge_next, outer)
vertices[idx + 2] = sv(edge_current, outer)
}
prepare_shape(layer, vertices)
}
triangle :: proc(layer: ^Layer, v1, v2, v3: [2]f32, color: Color) {
vertices := [3]Vertex{sv(v1, color), sv(v2, color), sv(v3, color)}
prepare_shape(layer, vertices[:])
}
triangle_lines :: proc(
layer: ^Layer,
v1, v2, v3: [2]f32,
color: Color,
thick: f32 = 1,
temp_allocator := context.temp_allocator,
) {
vertices := make([]Vertex, 18, temp_allocator)
write_offset := 0
write_offset += extrude_line(v1, v2, thick, color, vertices, write_offset)
write_offset += extrude_line(v2, v3, thick, color, vertices, write_offset)
write_offset += extrude_line(v3, v1, thick, color, vertices, write_offset)
if write_offset > 0 {
prepare_shape(layer, vertices[:write_offset])
}
}
triangle_fan :: proc(
layer: ^Layer,
points: [][2]f32,
color: Color,
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)
for i in 1 ..< len(points) - 1 {
idx := (i - 1) * 3
vertices[idx + 0] = sv(points[0], color)
vertices[idx + 1] = sv(points[i], color)
vertices[idx + 2] = sv(points[i + 1], color)
}
prepare_shape(layer, vertices)
}
triangle_strip :: proc(
layer: ^Layer,
points: [][2]f32,
color: Color,
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)
for i in 0 ..< triangle_count {
idx := i * 3
if i % 2 == 0 {
vertices[idx + 0] = sv(points[i], color)
vertices[idx + 1] = sv(points[i + 1], color)
vertices[idx + 2] = sv(points[i + 2], color)
} else {
vertices[idx + 0] = sv(points[i + 1], color)
vertices[idx + 1] = sv(points[i], color)
vertices[idx + 2] = sv(points[i + 2], color)
}
}
prepare_shape(layer, vertices)
}
// ----- SDF drawing functions ----
// Draw a rectangle with per-corner rounding radii via SDF.
rectangle_corners :: proc(
layer: ^Layer,
rect: Rectangle,
radii: [4]f32,
color: Color,
soft_px: f32 = 1.0,
) {
max_radius := min(rect.w, rect.h) * 0.5
tl := clamp(radii[0], 0, max_radius)
tr := clamp(radii[1], 0, max_radius)
br := clamp(radii[2], 0, max_radius)
bl := clamp(radii[3], 0, max_radius)
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {rect.x - pad, rect.y - pad, rect.x + rect.w + pad, rect.y + rect.h + pad},
color = color,
kind_flags = pack_kind_flags(.RRect, {}),
}
prim.params.rrect = RRect_Params {
half_size = {rect.w * 0.5 * dpi, rect.h * 0.5 * dpi},
radii = {tr * dpi, br * dpi, tl * dpi, bl * dpi},
soft_px = soft_px,
stroke_px = 0,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a stroked rectangle with per-corner rounding radii via SDF.
rectangle_corners_lines :: proc(
layer: ^Layer,
rect: Rectangle,
radii: [4]f32,
color: Color,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
max_radius := min(rect.w, rect.h) * 0.5
tl := clamp(radii[0], 0, max_radius)
tr := clamp(radii[1], 0, max_radius)
br := clamp(radii[2], 0, max_radius)
bl := clamp(radii[3], 0, max_radius)
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {rect.x - pad, rect.y - pad, rect.x + rect.w + pad, rect.y + rect.h + pad},
color = color,
kind_flags = pack_kind_flags(.RRect, {.Stroke}),
}
prim.params.rrect = RRect_Params {
half_size = {rect.w * 0.5 * dpi, rect.h * 0.5 * dpi},
radii = {tr * dpi, br * dpi, tl * dpi, bl * dpi},
soft_px = soft_px,
stroke_px = thick * dpi,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a rectangle with uniform corner rounding via SDF.
rectangle_rounded :: proc(
layer: ^Layer,
rect: Rectangle,
roundness: f32,
color: Color,
soft_px: f32 = 1.0,
) {
cr := min(rect.w, rect.h) * clamp(roundness, 0, 1) * 0.5
if cr < 1 {
rectangle(layer, rect, color)
return
}
rectangle_corners(layer, rect, {cr, cr, cr, cr}, color, soft_px)
}
// Draw a stroked rectangle with uniform corner rounding via SDF.
rectangle_rounded_lines :: proc(
layer: ^Layer,
rect: Rectangle,
roundness: f32,
color: Color,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
cr := min(rect.w, rect.h) * clamp(roundness, 0, 1) * 0.5
if cr < 1 {
rectangle_lines(layer, rect, color, thick)
return
}
rectangle_corners_lines(layer, rect, {cr, cr, cr, cr}, color, thick, soft_px)
}
// Draw a filled circle via SDF.
circle :: proc(layer: ^Layer, center: [2]f32, radius: f32, color: Color, soft_px: f32 = 1.0) {
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {center.x - radius - pad, center.y - radius - pad,
center.x + radius + pad, center.y + radius + pad},
color = color,
kind_flags = pack_kind_flags(.Circle, {}),
}
prim.params.circle = Circle_Params{radius = radius * dpi, soft_px = soft_px}
prepare_sdf_primitive(layer, prim)
}
// Draw a stroked circle via SDF.
circle_lines :: proc(
layer: ^Layer,
center: [2]f32,
radius: f32,
color: Color,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {center.x - radius - pad, center.y - radius - pad,
center.x + radius + pad, center.y + radius + pad},
color = color,
kind_flags = pack_kind_flags(.Circle, {.Stroke}),
}
prim.params.circle = Circle_Params{
radius = radius * dpi, soft_px = soft_px, stroke_px = thick * dpi,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a filled ellipse via SDF.
ellipse :: proc(
layer: ^Layer,
center: [2]f32,
radius_h, radius_v: f32,
color: Color,
soft_px: f32 = 1.0,
) {
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {center.x - radius_h - pad, center.y - radius_v - pad,
center.x + radius_h + pad, center.y + radius_v + pad},
color = color,
kind_flags = pack_kind_flags(.Ellipse, {}),
}
prim.params.ellipse = Ellipse_Params{radii = {radius_h * dpi, radius_v * dpi}, soft_px = soft_px}
prepare_sdf_primitive(layer, prim)
}
// Draw a stroked ellipse via SDF.
ellipse_lines :: proc(
layer: ^Layer,
center: [2]f32,
radius_h, radius_v: f32,
color: Color,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
// Extra 10% padding: iq's sdEllipse has precision degradation near the tips of highly
// eccentric ellipses, so the quad needs additional breathing room beyond the stroke width.
pad := (max(radius_h, radius_v) * 0.1 + thick * 0.5 + soft_px) / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {center.x - radius_h - pad, center.y - radius_v - pad,
center.x + radius_h + pad, center.y + radius_v + pad},
color = color,
kind_flags = pack_kind_flags(.Ellipse, {.Stroke}),
}
prim.params.ellipse = Ellipse_Params{
radii = {radius_h * dpi, radius_v * dpi}, soft_px = soft_px, stroke_px = thick * dpi,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a filled ring arc via SDF.
ring :: proc(
layer: ^Layer,
center: [2]f32,
inner_radius, outer_radius: f32,
start_angle, end_angle: f32,
color: Color,
soft_px: f32 = 1.0,
) {
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {center.x - outer_radius - pad, center.y - outer_radius - pad,
center.x + outer_radius + pad, center.y + outer_radius + pad},
color = color,
kind_flags = pack_kind_flags(.Ring_Arc, {}),
}
prim.params.ring_arc = Ring_Arc_Params {
inner_radius = inner_radius * dpi,
outer_radius = outer_radius * dpi,
start_rad = math.to_radians(start_angle),
end_rad = math.to_radians(end_angle),
soft_px = soft_px,
}
prepare_sdf_primitive(layer, prim)
}
// Draw stroked ring arc outlines via SDF.
ring_lines :: proc(
layer: ^Layer,
center: [2]f32,
inner_radius, outer_radius: f32,
start_angle, end_angle: f32,
color: Color,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
// Inner arc outline
ring(layer, center, max(0, inner_radius - thick * 0.5), inner_radius + thick * 0.5,
start_angle, end_angle, color, soft_px)
// Outer arc outline
ring(layer, center, max(0, outer_radius - thick * 0.5), outer_radius + thick * 0.5,
start_angle, end_angle, color, soft_px)
// Start cap
start_rad := math.to_radians(start_angle)
end_rad := math.to_radians(end_angle)
inner_start := center + {math.cos(start_rad) * inner_radius, math.sin(start_rad) * inner_radius}
outer_start := center + {math.cos(start_rad) * outer_radius, math.sin(start_rad) * outer_radius}
line(layer, inner_start, outer_start, color, thick, soft_px)
// End cap
inner_end := center + {math.cos(end_rad) * inner_radius, math.sin(end_rad) * inner_radius}
outer_end := center + {math.cos(end_rad) * outer_radius, math.sin(end_rad) * outer_radius}
line(layer, inner_end, outer_end, color, thick, soft_px)
}
// Draw a line segment via SDF.
line :: proc(
layer: ^Layer,
start, end_pos: [2]f32,
color: Color,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
cap := thick * 0.5 + soft_px / GLOB.dpi_scaling
min_x := min(start.x, end_pos.x) - cap
max_x := max(start.x, end_pos.x) + cap
min_y := min(start.y, end_pos.y) - cap
max_y := max(start.y, end_pos.y) + cap
dpi := GLOB.dpi_scaling
center := [2]f32{(min_x + max_x) * 0.5, (min_y + max_y) * 0.5}
local_a := (start - center) * dpi
local_b := (end_pos - center) * dpi
prim := Primitive {
bounds = {min_x, min_y, max_x, max_y},
color = color,
kind_flags = pack_kind_flags(.Segment, {}),
}
prim.params.segment = Segment_Params {
a = local_a,
b = local_b,
width = thick * dpi,
soft_px = soft_px,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a line strip via decomposed SDF segments.
line_strip :: proc(
layer: ^Layer,
points: [][2]f32,
color: Color,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
if len(points) < 2 do return
for i in 0 ..< len(points) - 1 {
line(layer, points[i], points[i + 1], color, thick, soft_px)
}
}
// Draw a filled regular polygon via SDF.
poly :: proc(
layer: ^Layer,
center: [2]f32,
sides: int,
radius: f32,
color: Color,
rotation: f32 = 0,
soft_px: f32 = 1.0,
) {
if sides < 3 do return
pad := soft_px / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {center.x - radius - pad, center.y - radius - pad,
center.x + radius + pad, center.y + radius + pad},
color = color,
kind_flags = pack_kind_flags(.NGon, {}),
}
prim.params.ngon = NGon_Params {
radius = radius * math.cos(math.PI / f32(sides)) * dpi,
rotation = math.to_radians(rotation),
sides = f32(sides),
soft_px = soft_px,
}
prepare_sdf_primitive(layer, prim)
}
// Draw a stroked regular polygon via SDF.
poly_lines :: proc(
layer: ^Layer,
center: [2]f32,
sides: int,
radius: f32,
color: Color,
rotation: f32 = 0,
thick: f32 = 1,
soft_px: f32 = 1.0,
) {
if sides < 3 do return
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
dpi := GLOB.dpi_scaling
prim := Primitive {
bounds = {center.x - radius - pad, center.y - radius - pad,
center.x + radius + pad, center.y + radius + pad},
color = color,
kind_flags = pack_kind_flags(.NGon, {.Stroke}),
}
prim.params.ngon = NGon_Params {
radius = radius * math.cos(math.PI / f32(sides)) * dpi,
rotation = math.to_radians(rotation),
sides = f32(sides),
soft_px = soft_px,
stroke_px = thick * dpi,
}
prepare_sdf_primitive(layer, prim)
}