Major rework to draw rendering system. We are making a SDF first rendering system with tesselated stuff only as a fallback strategy for specific situations where SDF is particularly poorly suited Co-authored-by: Zachary Levy <zachary@sunforge.is> Reviewed-on: #17
777 lines
26 KiB
Odin
777 lines
26 KiB
Odin
package draw
|
||
|
||
import "core:math"
|
||
|
||
// ----- Internal helpers ----
|
||
|
||
// Internal
|
||
extrude_line :: proc(
|
||
start, end_pos: Vec2,
|
||
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 := Vec2{-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).
|
||
// Color is premultiplied: the tessellated fragment shader passes it through directly
|
||
// and the blend state is ONE, ONE_MINUS_SRC_ALPHA.
|
||
solid_vertex :: proc(position: Vec2, color: Color) -> Vertex {
|
||
return Vertex{position = position, color = premultiply_color(color)}
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
// Internal
|
||
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)
|
||
}
|
||
|
||
//Internal
|
||
//
|
||
// 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).
|
||
compute_pivot_center :: proc(center: Vec2, origin: Vec2, sin_angle, cos_angle: f32) -> Vec2 {
|
||
if origin == {0, 0} do return center
|
||
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 the given cos/sin.
|
||
rotated_aabb_half_extents :: proc(half_width, half_height, cos_angle, sin_angle: f32) -> [2]f32 {
|
||
cos_abs := abs(cos_angle)
|
||
sin_abs := abs(sin_angle)
|
||
return {half_width * cos_abs + half_height * sin_abs, half_width * sin_abs + half_height * cos_abs}
|
||
}
|
||
|
||
// Pack sin/cos into the Primitive.rotation_sc field as two f16 values.
|
||
pack_rotation_sc :: #force_inline proc(sin_angle, cos_angle: f32) -> u32 {
|
||
return pack_f16_pair(f16(sin_angle), f16(cos_angle))
|
||
}
|
||
|
||
|
||
// Internal
|
||
//
|
||
// Build an RRect Primitive with bounds, params, and rotation computed from rectangle geometry.
|
||
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
|
||
build_rrect_primitive :: proc(
|
||
rect: Rectangle,
|
||
radii: Rectangle_Radii,
|
||
origin: Vec2,
|
||
rotation: f32,
|
||
feather_px: f32,
|
||
) -> Primitive {
|
||
max_radius := min(rect.width, rect.height) * 0.5
|
||
clamped_top_left := clamp(radii.top_left, 0, max_radius)
|
||
clamped_top_right := clamp(radii.top_right, 0, max_radius)
|
||
clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius)
|
||
clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius)
|
||
|
||
half_feather := feather_px * 0.5
|
||
padding := half_feather / GLOB.dpi_scaling
|
||
dpi_scale := GLOB.dpi_scaling
|
||
|
||
half_width := rect.width * 0.5
|
||
half_height := rect.height * 0.5
|
||
center_x := rect.x + half_width - origin.x
|
||
center_y := rect.y + half_height - origin.y
|
||
sin_angle: f32 = 0
|
||
cos_angle: f32 = 1
|
||
has_rotation := false
|
||
|
||
if needs_transform(origin, rotation) {
|
||
rotation_radians := math.to_radians(rotation)
|
||
sin_angle, cos_angle = math.sincos(rotation_radians)
|
||
has_rotation = rotation != 0
|
||
transform := build_pivot_rotation_sc({rect.x + origin.x, rect.y + origin.y}, origin, cos_angle, sin_angle)
|
||
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 has_rotation {
|
||
expanded := rotated_aabb_half_extents(half_width, half_height, cos_angle, sin_angle)
|
||
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,
|
||
},
|
||
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
|
||
}
|
||
prim.params.rrect = RRect_Params {
|
||
half_size = {half_width * dpi_scale, half_height * dpi_scale},
|
||
radii = {
|
||
clamped_bottom_right * dpi_scale,
|
||
clamped_top_right * dpi_scale,
|
||
clamped_bottom_left * dpi_scale,
|
||
clamped_top_left * dpi_scale,
|
||
},
|
||
half_feather = half_feather,
|
||
}
|
||
return prim
|
||
}
|
||
|
||
// Internal
|
||
//
|
||
// Build an RRect Primitive for a circle (fully-rounded square RRect).
|
||
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
|
||
build_circle_primitive :: proc(
|
||
center: Vec2,
|
||
radius: f32,
|
||
origin: Vec2,
|
||
rotation: f32,
|
||
feather_px: f32,
|
||
) -> Primitive {
|
||
half_feather := feather_px * 0.5
|
||
padding := half_feather / GLOB.dpi_scaling
|
||
dpi_scale := GLOB.dpi_scaling
|
||
|
||
actual_center := center
|
||
if origin != {0, 0} {
|
||
sin_a, cos_a := math.sincos(math.to_radians(rotation))
|
||
actual_center = compute_pivot_center(center, origin, sin_a, cos_a)
|
||
}
|
||
|
||
prim := Primitive {
|
||
bounds = {
|
||
actual_center.x - radius - padding,
|
||
actual_center.y - radius - padding,
|
||
actual_center.x + radius + padding,
|
||
actual_center.y + radius + padding,
|
||
},
|
||
}
|
||
scaled_radius := radius * dpi_scale
|
||
prim.params.rrect = RRect_Params {
|
||
half_size = {scaled_radius, scaled_radius},
|
||
radii = {scaled_radius, scaled_radius, scaled_radius, scaled_radius},
|
||
half_feather = half_feather,
|
||
}
|
||
return prim
|
||
}
|
||
|
||
// Internal
|
||
//
|
||
// Build an Ellipse Primitive with bounds, params, and rotation computed from ellipse geometry.
|
||
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
|
||
build_ellipse_primitive :: proc(
|
||
center: Vec2,
|
||
radius_horizontal, radius_vertical: f32,
|
||
origin: Vec2,
|
||
rotation: f32,
|
||
feather_px: f32,
|
||
) -> Primitive {
|
||
half_feather := feather_px * 0.5
|
||
padding := half_feather / GLOB.dpi_scaling
|
||
dpi_scale := GLOB.dpi_scaling
|
||
|
||
actual_center := center
|
||
sin_angle: f32 = 0
|
||
cos_angle: f32 = 1
|
||
has_rotation := false
|
||
|
||
if needs_transform(origin, rotation) {
|
||
rotation_radians := math.to_radians(rotation)
|
||
sin_angle, cos_angle = math.sincos(rotation_radians)
|
||
actual_center = compute_pivot_center(center, origin, sin_angle, cos_angle)
|
||
has_rotation = rotation != 0
|
||
}
|
||
|
||
bound_horizontal, bound_vertical := radius_horizontal, radius_vertical
|
||
if has_rotation {
|
||
expanded := rotated_aabb_half_extents(radius_horizontal, radius_vertical, cos_angle, sin_angle)
|
||
bound_horizontal = expanded.x
|
||
bound_vertical = expanded.y
|
||
}
|
||
|
||
prim := Primitive {
|
||
bounds = {
|
||
actual_center.x - bound_horizontal - padding,
|
||
actual_center.y - bound_vertical - padding,
|
||
actual_center.x + bound_horizontal + padding,
|
||
actual_center.y + bound_vertical + padding,
|
||
},
|
||
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
|
||
}
|
||
prim.params.ellipse = Ellipse_Params {
|
||
radii = {radius_horizontal * dpi_scale, radius_vertical * dpi_scale},
|
||
half_feather = half_feather,
|
||
}
|
||
return prim
|
||
}
|
||
|
||
// Internal
|
||
//
|
||
// Build an NGon Primitive with bounds, params, and rotation computed from polygon geometry.
|
||
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
|
||
build_polygon_primitive :: proc(
|
||
center: Vec2,
|
||
sides: int,
|
||
radius: f32,
|
||
origin: Vec2,
|
||
rotation: f32,
|
||
feather_px: f32,
|
||
) -> Primitive {
|
||
half_feather := feather_px * 0.5
|
||
padding := half_feather / GLOB.dpi_scaling
|
||
dpi_scale := GLOB.dpi_scaling
|
||
|
||
actual_center := center
|
||
if origin != {0, 0} && rotation != 0 {
|
||
sin_a, cos_a := math.sincos(math.to_radians(rotation))
|
||
actual_center = compute_pivot_center(center, origin, sin_a, cos_a)
|
||
}
|
||
|
||
rotation_radians := math.to_radians(rotation)
|
||
sin_rot, cos_rot := math.sincos(rotation_radians)
|
||
|
||
prim := Primitive {
|
||
bounds = {
|
||
actual_center.x - radius - padding,
|
||
actual_center.y - radius - padding,
|
||
actual_center.x + radius + padding,
|
||
actual_center.y + radius + padding,
|
||
},
|
||
rotation_sc = rotation != 0 ? pack_rotation_sc(sin_rot, cos_rot) : 0,
|
||
}
|
||
prim.params.ngon = NGon_Params {
|
||
radius = radius * math.cos(math.PI / f32(sides)) * dpi_scale,
|
||
sides = f32(sides),
|
||
half_feather = half_feather,
|
||
}
|
||
return prim
|
||
}
|
||
|
||
// Internal
|
||
//
|
||
// Build a Ring_Arc Primitive with bounds and params computed from ring/arc geometry.
|
||
// Pre-computes the angular boundary normals on the CPU so the fragment shader needs
|
||
// no per-pixel sin/cos. The radial SDF uses max(inner-r, r-outer) which correctly
|
||
// handles pie slices (inner_radius = 0) and full rings.
|
||
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
|
||
build_ring_arc_primitive :: proc(
|
||
center: Vec2,
|
||
inner_radius, outer_radius: f32,
|
||
start_angle: f32,
|
||
end_angle: f32,
|
||
origin: Vec2,
|
||
rotation: f32,
|
||
feather_px: f32,
|
||
) -> (
|
||
Primitive,
|
||
Shape_Flags,
|
||
) {
|
||
half_feather := feather_px * 0.5
|
||
padding := half_feather / GLOB.dpi_scaling
|
||
dpi_scale := GLOB.dpi_scaling
|
||
|
||
actual_center := center
|
||
rotation_offset: f32 = 0
|
||
if needs_transform(origin, rotation) {
|
||
sin_a, cos_a := math.sincos(math.to_radians(rotation))
|
||
actual_center = compute_pivot_center(center, origin, sin_a, cos_a)
|
||
rotation_offset = math.to_radians(rotation)
|
||
}
|
||
|
||
start_rad := math.to_radians(start_angle) + rotation_offset
|
||
end_rad := math.to_radians(end_angle) + rotation_offset
|
||
|
||
// Normalize arc span to [0, 2π]
|
||
arc_span := end_rad - start_rad
|
||
if arc_span < 0 {
|
||
arc_span += 2 * math.PI
|
||
}
|
||
|
||
// Pre-compute edge normals and arc flags on CPU — no per-pixel trig needed.
|
||
// arc_flags: {} = full ring, {.Arc_Narrow} = span ≤ π (intersect), {.Arc_Wide} = span > π (union)
|
||
arc_flags: Shape_Flags = {}
|
||
normal_start: [2]f32 = {}
|
||
normal_end: [2]f32 = {}
|
||
|
||
if arc_span < 2 * math.PI - 0.001 {
|
||
sin_start, cos_start := math.sincos(start_rad)
|
||
sin_end, cos_end := math.sincos(end_rad)
|
||
normal_start = {sin_start, -cos_start}
|
||
normal_end = {-sin_end, cos_end}
|
||
arc_flags = arc_span <= math.PI ? {.Arc_Narrow} : {.Arc_Wide}
|
||
}
|
||
|
||
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,
|
||
},
|
||
}
|
||
prim.params.ring_arc = Ring_Arc_Params {
|
||
inner_radius = inner_radius * dpi_scale,
|
||
outer_radius = outer_radius * dpi_scale,
|
||
normal_start = normal_start,
|
||
normal_end = normal_end,
|
||
half_feather = half_feather,
|
||
}
|
||
return prim, arc_flags
|
||
}
|
||
|
||
// Apply gradient and outline effects to a primitive. Sets flags, uv.effects, and expands bounds.
|
||
// All parameters (outline_width) are in logical pixels, matching the rest of the public API.
|
||
// The helper converts to physical pixels for GPU packing internally.
|
||
@(private)
|
||
apply_shape_effects :: proc(
|
||
prim: ^Primitive,
|
||
kind: Shape_Kind,
|
||
gradient: Gradient,
|
||
outline_color: Color,
|
||
outline_width: f32,
|
||
extra_flags: Shape_Flags = {},
|
||
) {
|
||
flags: Shape_Flags = extra_flags
|
||
gradient_dir_sc: u32 = 0
|
||
|
||
switch g in gradient {
|
||
case Linear_Gradient:
|
||
flags += {.Gradient}
|
||
prim.uv.effects.gradient_color = g.end_color
|
||
rad := math.to_radians(g.angle)
|
||
sin_a, cos_a := math.sincos(rad)
|
||
gradient_dir_sc = pack_f16_pair(f16(cos_a), f16(sin_a))
|
||
case Radial_Gradient:
|
||
flags += {.Gradient, .Gradient_Radial}
|
||
prim.uv.effects.gradient_color = g.outer_color
|
||
case:
|
||
}
|
||
|
||
outline_packed: u32 = 0
|
||
if outline_width > 0 {
|
||
flags += {.Outline}
|
||
prim.uv.effects.outline_color = outline_color
|
||
outline_packed = pack_f16_pair(f16(outline_width * GLOB.dpi_scaling), 0)
|
||
// Expand bounds to contain the outline (bounds are in logical pixels)
|
||
prim.bounds[0] -= outline_width
|
||
prim.bounds[1] -= outline_width
|
||
prim.bounds[2] += outline_width
|
||
prim.bounds[3] += outline_width
|
||
}
|
||
|
||
// Set .Rotated flag if rotation_sc was populated by the build proc
|
||
if prim.rotation_sc != 0 {
|
||
flags += {.Rotated}
|
||
}
|
||
|
||
prim.uv.effects.gradient_dir_sc = gradient_dir_sc
|
||
prim.uv.effects.outline_packed = outline_packed
|
||
prim.flags = pack_kind_flags(kind, flags)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- SDF Rectangle procs -----------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
// Draw a filled rectangle via SDF with optional per-corner rounding radii.
|
||
// Use `uniform_radii(rect, roundness)` to compute uniform radii from a 0–1 fraction.
|
||
//
|
||
// 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.
|
||
// Rotation always occurs around the anchor point.
|
||
rectangle :: proc(
|
||
layer: ^Layer,
|
||
rect: Rectangle,
|
||
color: Color,
|
||
gradient: Gradient = nil,
|
||
outline_color: Color = {},
|
||
outline_width: f32 = 0,
|
||
radii: Rectangle_Radii = {},
|
||
origin: Vec2 = {},
|
||
rotation: f32 = 0,
|
||
feather_px: f32 = DFT_FEATHER_PX,
|
||
) {
|
||
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px)
|
||
prim.color = color
|
||
apply_shape_effects(&prim, .RRect, gradient, outline_color, outline_width)
|
||
prepare_sdf_primitive(layer, prim)
|
||
}
|
||
|
||
// Draw a rectangle with a texture fill via SDF with optional per-corner rounding radii.
|
||
// Texture and gradient/outline are mutually exclusive (they share the same storage in the
|
||
// primitive). To outline a textured rect, draw the texture first, then a stroke-only rect on top.
|
||
// Origin semantics: see `rectangle`.
|
||
rectangle_texture :: proc(
|
||
layer: ^Layer,
|
||
rect: Rectangle,
|
||
id: Texture_Id,
|
||
tint: Color = DFT_TINT,
|
||
uv_rect: Rectangle = DFT_UV_RECT,
|
||
sampler: Sampler_Preset = DFT_SAMPLER,
|
||
radii: Rectangle_Radii = {},
|
||
origin: Vec2 = {},
|
||
rotation: f32 = 0,
|
||
feather_px: f32 = DFT_FEATHER_PX,
|
||
) {
|
||
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px)
|
||
prim.color = tint
|
||
tex_flags: Shape_Flags = {.Textured}
|
||
if prim.rotation_sc != 0 {
|
||
tex_flags += {.Rotated}
|
||
}
|
||
prim.flags = pack_kind_flags(.RRect, tex_flags)
|
||
prim.uv.uv_rect = {uv_rect.x, uv_rect.y, uv_rect.width, uv_rect.height}
|
||
prepare_sdf_primitive_textured(layer, prim, id, sampler)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- SDF Circle procs (emit RRect primitives) ------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
// Draw a filled circle via SDF (emitted as a fully-rounded RRect).
|
||
//
|
||
// 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: Vec2,
|
||
radius: f32,
|
||
color: Color,
|
||
gradient: Gradient = nil,
|
||
outline_color: Color = {},
|
||
outline_width: f32 = 0,
|
||
origin: Vec2 = {},
|
||
rotation: f32 = 0,
|
||
feather_px: f32 = DFT_FEATHER_PX,
|
||
) {
|
||
prim := build_circle_primitive(center, radius, origin, rotation, feather_px)
|
||
prim.color = color
|
||
apply_shape_effects(&prim, .RRect, gradient, outline_color, outline_width)
|
||
prepare_sdf_primitive(layer, prim)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- SDF Ellipse procs (emit Ellipse primitives) ---
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
// Draw a filled ellipse via SDF.
|
||
// Origin semantics: see `circle`.
|
||
ellipse :: proc(
|
||
layer: ^Layer,
|
||
center: Vec2,
|
||
radius_horizontal, radius_vertical: f32,
|
||
color: Color,
|
||
gradient: Gradient = nil,
|
||
outline_color: Color = {},
|
||
outline_width: f32 = 0,
|
||
origin: Vec2 = {},
|
||
rotation: f32 = 0,
|
||
feather_px: f32 = DFT_FEATHER_PX,
|
||
) {
|
||
prim := build_ellipse_primitive(center, radius_horizontal, radius_vertical, origin, rotation, feather_px)
|
||
prim.color = color
|
||
apply_shape_effects(&prim, .Ellipse, gradient, outline_color, outline_width)
|
||
prepare_sdf_primitive(layer, prim)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- SDF Polygon procs (emit NGon primitives) ------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
// Draw a filled regular polygon via SDF.
|
||
// `sides` must be >= 3. The polygon is inscribed in a circle of the given `radius`.
|
||
// Origin semantics: see `circle`.
|
||
polygon :: proc(
|
||
layer: ^Layer,
|
||
center: Vec2,
|
||
sides: int,
|
||
radius: f32,
|
||
color: Color,
|
||
gradient: Gradient = nil,
|
||
outline_color: Color = {},
|
||
outline_width: f32 = 0,
|
||
origin: Vec2 = {},
|
||
rotation: f32 = 0,
|
||
feather_px: f32 = DFT_FEATHER_PX,
|
||
) {
|
||
if sides < 3 do return
|
||
|
||
prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_px)
|
||
prim.color = color
|
||
apply_shape_effects(&prim, .NGon, gradient, outline_color, outline_width)
|
||
prepare_sdf_primitive(layer, prim)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- SDF Ring / Arc procs (emit Ring_Arc primitives) ----
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
// Draw a ring, arc, or pie slice via SDF.
|
||
// Full ring by default. Pass start_angle/end_angle (degrees) for partial arcs.
|
||
// Use inner_radius = 0 for pie slices (sectors).
|
||
// Origin semantics: see `circle`.
|
||
ring :: proc(
|
||
layer: ^Layer,
|
||
center: Vec2,
|
||
inner_radius, outer_radius: f32,
|
||
color: Color,
|
||
gradient: Gradient = nil,
|
||
outline_color: Color = {},
|
||
outline_width: f32 = 0,
|
||
start_angle: f32 = 0,
|
||
end_angle: f32 = DFT_CIRC_END_ANGLE,
|
||
origin: Vec2 = {},
|
||
rotation: f32 = 0,
|
||
feather_px: f32 = DFT_FEATHER_PX,
|
||
) {
|
||
prim, arc_flags := build_ring_arc_primitive(
|
||
center,
|
||
inner_radius,
|
||
outer_radius,
|
||
start_angle,
|
||
end_angle,
|
||
origin,
|
||
rotation,
|
||
feather_px,
|
||
)
|
||
prim.color = color
|
||
apply_shape_effects(&prim, .Ring_Arc, gradient, outline_color, outline_width, arc_flags)
|
||
prepare_sdf_primitive(layer, prim)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- SDF Line procs (emit rotated RRect primitives) ----
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
// Draw a line segment via SDF (emitted as a rotated capsule-shaped RRect).
|
||
// Round caps are produced by setting corner radii equal to half the thickness.
|
||
line :: proc(
|
||
layer: ^Layer,
|
||
start_position, end_position: Vec2,
|
||
color: Color,
|
||
thickness: f32 = DFT_STROKE_THICKNESS,
|
||
outline_color: Color = {},
|
||
outline_width: f32 = 0,
|
||
feather_px: f32 = DFT_FEATHER_PX,
|
||
) {
|
||
delta_x := end_position.x - start_position.x
|
||
delta_y := end_position.y - start_position.y
|
||
seg_length := math.sqrt(delta_x * delta_x + delta_y * delta_y)
|
||
if seg_length < 0.0001 do return
|
||
rotation_radians := math.atan2(delta_y, delta_x)
|
||
sin_angle, cos_angle := math.sincos(rotation_radians)
|
||
|
||
center_x := (start_position.x + end_position.x) * 0.5
|
||
center_y := (start_position.y + end_position.y) * 0.5
|
||
|
||
half_length := seg_length * 0.5
|
||
half_thickness := thickness * 0.5
|
||
cap_radius := half_thickness
|
||
|
||
half_feather := feather_px * 0.5
|
||
padding := half_feather / GLOB.dpi_scaling
|
||
dpi_scale := GLOB.dpi_scaling
|
||
|
||
// Expand bounds for rotation
|
||
bounds_half := rotated_aabb_half_extents(half_length + cap_radius, half_thickness, cos_angle, sin_angle)
|
||
|
||
prim := Primitive {
|
||
bounds = {
|
||
center_x - bounds_half.x - padding,
|
||
center_y - bounds_half.y - padding,
|
||
center_x + bounds_half.x + padding,
|
||
center_y + bounds_half.y + padding,
|
||
},
|
||
color = color,
|
||
rotation_sc = pack_rotation_sc(sin_angle, cos_angle),
|
||
}
|
||
prim.params.rrect = RRect_Params {
|
||
half_size = {(half_length + cap_radius) * dpi_scale, half_thickness * dpi_scale},
|
||
radii = {
|
||
cap_radius * dpi_scale,
|
||
cap_radius * dpi_scale,
|
||
cap_radius * dpi_scale,
|
||
cap_radius * dpi_scale,
|
||
},
|
||
half_feather = half_feather,
|
||
}
|
||
apply_shape_effects(&prim, .RRect, nil, outline_color, outline_width)
|
||
prepare_sdf_primitive(layer, prim)
|
||
}
|
||
|
||
// Draw a line strip via decomposed SDF line segments.
|
||
line_strip :: proc(
|
||
layer: ^Layer,
|
||
points: []Vec2,
|
||
color: Color,
|
||
thickness: f32 = DFT_STROKE_THICKNESS,
|
||
outline_color: Color = {},
|
||
outline_width: f32 = 0,
|
||
feather_px: f32 = DFT_FEATHER_PX,
|
||
) {
|
||
if len(points) < 2 do return
|
||
for i in 0 ..< len(points) - 1 {
|
||
line(layer, points[i], points[i + 1], color, thickness, outline_color, outline_width, feather_px)
|
||
}
|
||
}
|
||
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- Helpers ----------------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
// Returns uniform radii (all corners the same) as a fraction of the shorter side.
|
||
// `roundness` is clamped to [0, 1]; 0 = sharp corners, 1 = fully rounded (stadium or circle).
|
||
uniform_radii :: #force_inline proc(rect: Rectangle, roundness: f32) -> Rectangle_Radii {
|
||
cr := min(rect.width, rect.height) * clamp(roundness, 0, 1) * 0.5
|
||
return {cr, cr, cr, cr}
|
||
}
|
||
|
||
// Return Vec2 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) -> Vec2 {
|
||
return {rectangle.width * 0.5, rectangle.height * 0.5}
|
||
}
|
||
|
||
top_left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||
return {0, 0}
|
||
}
|
||
|
||
top_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||
return {rectangle.width * 0.5, 0}
|
||
}
|
||
|
||
top_right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||
return {rectangle.width, 0}
|
||
}
|
||
|
||
left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||
return {0, rectangle.height * 0.5}
|
||
}
|
||
|
||
right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||
return {rectangle.width, rectangle.height * 0.5}
|
||
}
|
||
|
||
bottom_left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||
return {0, rectangle.height}
|
||
}
|
||
|
||
bottom_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||
return {rectangle.width * 0.5, rectangle.height}
|
||
}
|
||
|
||
bottom_right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
|
||
return {rectangle.width, rectangle.height}
|
||
}
|
||
|
||
// ----- Triangle anchors (origin measured from AABB top-left) -----------------------------------------------------
|
||
|
||
center_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
|
||
bounds_min := Vec2{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: Vec2) -> Vec2 {
|
||
return {0, 0}
|
||
}
|
||
|
||
top_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
|
||
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: Vec2) -> Vec2 {
|
||
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: Vec2) -> Vec2 {
|
||
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: Vec2) -> Vec2 {
|
||
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||
bounds_max := Vec2{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: Vec2) -> Vec2 {
|
||
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: Vec2) -> Vec2 {
|
||
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||
bounds_max := Vec2{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: Vec2) -> Vec2 {
|
||
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||
bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
|
||
return bounds_max - bounds_min
|
||
}
|