Added draw package as renderer focused on mixed use layout / 2D / 3D scene applications #7
@@ -55,6 +55,11 @@
|
|||||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- hellope-shapes",
|
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- hellope-shapes",
|
||||||
"cwd": "$ZED_WORKTREE_ROOT",
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "Run draw hellope-text example",
|
||||||
|
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- hellope-text",
|
||||||
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
|
},
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
// ----- Other ------------------------
|
// ----- Other ------------------------
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|||||||
284
draw/draw.odin
284
draw/draw.odin
@@ -1,14 +1,15 @@
|
|||||||
package draw
|
package draw
|
||||||
|
|
||||||
import clay "../vendor/clay"
|
|
||||||
import "base:runtime"
|
import "base:runtime"
|
||||||
import "core:c"
|
import "core:c"
|
||||||
import "core:log"
|
import "core:log"
|
||||||
|
import "core:math"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import sdl "vendor:sdl3"
|
import sdl "vendor:sdl3"
|
||||||
import sdl_ttf "vendor:sdl3/ttf"
|
import sdl_ttf "vendor:sdl3/ttf"
|
||||||
|
|
||||||
|
import clay "../vendor/clay"
|
||||||
|
|
||||||
when ODIN_OS == .Darwin {
|
when ODIN_OS == .Darwin {
|
||||||
SHADER_TYPE :: sdl.GPUShaderFormat{.MSL}
|
SHADER_TYPE :: sdl.GPUShaderFormat{.MSL}
|
||||||
ENTRY_POINT :: "main0"
|
ENTRY_POINT :: "main0"
|
||||||
@@ -89,34 +90,35 @@ Scissor :: struct {
|
|||||||
GLOB: Global
|
GLOB: Global
|
||||||
|
|
||||||
Global :: struct {
|
Global :: struct {
|
||||||
odin_context: runtime.Context,
|
odin_context: runtime.Context,
|
||||||
pipeline_2d_base: Pipeline_2D_Base,
|
pipeline_2d_base: Pipeline_2D_Base,
|
||||||
text_cache: Text_Cache,
|
text_cache: Text_Cache,
|
||||||
layers: [dynamic]Layer,
|
layers: [dynamic]Layer,
|
||||||
scissors: [dynamic]Scissor,
|
scissors: [dynamic]Scissor,
|
||||||
tmp_shape_verts: [dynamic]Vertex,
|
tmp_shape_verts: [dynamic]Vertex,
|
||||||
tmp_text_verts: [dynamic]Vertex,
|
tmp_text_verts: [dynamic]Vertex,
|
||||||
tmp_text_indices: [dynamic]c.int,
|
tmp_text_indices: [dynamic]c.int,
|
||||||
tmp_text_batches: [dynamic]TextBatch,
|
tmp_text_batches: [dynamic]TextBatch,
|
||||||
tmp_primitives: [dynamic]Primitive,
|
tmp_primitives: [dynamic]Primitive,
|
||||||
tmp_sub_batches: [dynamic]Sub_Batch,
|
tmp_sub_batches: [dynamic]Sub_Batch,
|
||||||
clay_mem: [^]u8,
|
tmp_uncached_text: [dynamic]^sdl_ttf.Text, // Uncached TTF_Text objects to destroy after end()
|
||||||
msaa_texture: ^sdl.GPUTexture,
|
clay_mem: [^]u8,
|
||||||
curr_layer_index: uint,
|
msaa_texture: ^sdl.GPUTexture,
|
||||||
max_layers: int,
|
curr_layer_index: uint,
|
||||||
max_scissors: int,
|
max_layers: int,
|
||||||
max_shape_verts: int,
|
max_scissors: int,
|
||||||
max_text_verts: int,
|
max_shape_verts: int,
|
||||||
max_text_indices: int,
|
max_text_verts: int,
|
||||||
max_text_batches: int,
|
max_text_indices: int,
|
||||||
max_primitives: int,
|
max_text_batches: int,
|
||||||
max_sub_batches: int,
|
max_primitives: int,
|
||||||
dpi_scaling: f32,
|
max_sub_batches: int,
|
||||||
msaa_w: u32,
|
dpi_scaling: f32,
|
||||||
msaa_h: u32,
|
msaa_w: u32,
|
||||||
sample_count: sdl.GPUSampleCount,
|
msaa_h: u32,
|
||||||
clay_z_index: i16,
|
sample_count: sdl.GPUSampleCount,
|
||||||
cleared: bool,
|
clay_z_index: i16,
|
||||||
|
cleared: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
Init_Options :: struct {
|
Init_Options :: struct {
|
||||||
@@ -161,20 +163,21 @@ init :: proc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
GLOB = Global {
|
GLOB = Global {
|
||||||
layers = make([dynamic]Layer, 0, INITIAL_LAYER_SIZE, allocator = allocator),
|
layers = make([dynamic]Layer, 0, INITIAL_LAYER_SIZE, allocator = allocator),
|
||||||
scissors = make([dynamic]Scissor, 0, INITIAL_SCISSOR_SIZE, allocator = allocator),
|
scissors = make([dynamic]Scissor, 0, INITIAL_SCISSOR_SIZE, allocator = allocator),
|
||||||
tmp_shape_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
tmp_shape_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||||
tmp_text_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
tmp_text_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||||
tmp_text_indices = make([dynamic]c.int, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
tmp_text_indices = make([dynamic]c.int, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||||
tmp_text_batches = make([dynamic]TextBatch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
tmp_text_batches = make([dynamic]TextBatch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||||
tmp_primitives = make([dynamic]Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
tmp_primitives = make([dynamic]Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||||
tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||||
odin_context = odin_context,
|
tmp_uncached_text = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
|
||||||
dpi_scaling = sdl.GetWindowDisplayScale(window),
|
odin_context = odin_context,
|
||||||
clay_mem = make([^]u8, min_memory_size, allocator = allocator),
|
dpi_scaling = sdl.GetWindowDisplayScale(window),
|
||||||
sample_count = resolved_sample_count,
|
clay_mem = make([^]u8, min_memory_size, allocator = allocator),
|
||||||
pipeline_2d_base = pipeline,
|
sample_count = resolved_sample_count,
|
||||||
text_cache = text_cache,
|
pipeline_2d_base = pipeline,
|
||||||
|
text_cache = text_cache,
|
||||||
}
|
}
|
||||||
log.debug("Window DPI scaling:", GLOB.dpi_scaling)
|
log.debug("Window DPI scaling:", GLOB.dpi_scaling)
|
||||||
arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_mem)
|
arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_mem)
|
||||||
@@ -182,7 +185,7 @@ init :: proc(
|
|||||||
sdl.GetWindowSize(window, &window_width, &window_height)
|
sdl.GetWindowSize(window, &window_width, &window_height)
|
||||||
|
|
||||||
clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler})
|
clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler})
|
||||||
clay.SetMeasureTextFunction(measure_text, nil)
|
clay.SetMeasureTextFunction(measure_text_clay, nil)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +219,8 @@ destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
|
|||||||
delete(GLOB.tmp_text_batches)
|
delete(GLOB.tmp_text_batches)
|
||||||
delete(GLOB.tmp_primitives)
|
delete(GLOB.tmp_primitives)
|
||||||
delete(GLOB.tmp_sub_batches)
|
delete(GLOB.tmp_sub_batches)
|
||||||
|
for t in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(t)
|
||||||
|
delete(GLOB.tmp_uncached_text)
|
||||||
free(GLOB.clay_mem, allocator)
|
free(GLOB.clay_mem, allocator)
|
||||||
if GLOB.msaa_texture != nil {
|
if GLOB.msaa_texture != nil {
|
||||||
sdl.ReleaseGPUTexture(device, GLOB.msaa_texture)
|
sdl.ReleaseGPUTexture(device, GLOB.msaa_texture)
|
||||||
@@ -229,6 +234,9 @@ clear_global :: proc() {
|
|||||||
GLOB.curr_layer_index = 0
|
GLOB.curr_layer_index = 0
|
||||||
GLOB.clay_z_index = 0
|
GLOB.clay_z_index = 0
|
||||||
GLOB.cleared = false
|
GLOB.cleared = false
|
||||||
|
// Destroy uncached TTF_Text objects from the previous frame (after end() has submitted draw data)
|
||||||
|
for t in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(t)
|
||||||
|
clear(&GLOB.tmp_uncached_text)
|
||||||
clear(&GLOB.layers)
|
clear(&GLOB.layers)
|
||||||
clear(&GLOB.scissors)
|
clear(&GLOB.scissors)
|
||||||
clear(&GLOB.tmp_shape_verts)
|
clear(&GLOB.tmp_shape_verts)
|
||||||
@@ -244,7 +252,7 @@ clear_global :: proc() {
|
|||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
@(private = "file")
|
@(private = "file")
|
||||||
measure_text :: proc "c" (
|
measure_text_clay :: proc "c" (
|
||||||
text: clay.StringSlice,
|
text: clay.StringSlice,
|
||||||
config: ^clay.TextElementConfig,
|
config: ^clay.TextElementConfig,
|
||||||
user_data: rawptr,
|
user_data: rawptr,
|
||||||
@@ -384,6 +392,54 @@ prepare_text :: proc(layer: ^Layer, txt: Text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Submit a text element with a 2D affine transform applied to vertices.
|
||||||
|
// Used by the high-level `text` proc when rotation or a non-zero origin is specified.
|
||||||
|
// NOTE: xform must be in physical (DPI-scaled) pixel space — the caller pre-scales
|
||||||
|
// pos and origin by GLOB.dpi_scaling before building the transform.
|
||||||
|
prepare_text_transformed :: proc(layer: ^Layer, txt: Text, xform: Transform_2D) {
|
||||||
|
data := sdl_ttf.GetGPUTextDrawData(txt.ref)
|
||||||
|
if data == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
|
||||||
|
|
||||||
|
for data != nil {
|
||||||
|
vertex_start := u32(len(GLOB.tmp_text_verts))
|
||||||
|
index_start := u32(len(GLOB.tmp_text_indices))
|
||||||
|
|
||||||
|
for i in 0 ..< data.num_vertices {
|
||||||
|
pos := data.xy[i]
|
||||||
|
uv := data.uv[i]
|
||||||
|
// SDL_ttf gives glyph positions in physical pixels relative to text origin.
|
||||||
|
// The transform is already in physical-pixel space (caller pre-scaled),
|
||||||
|
// so we apply directly — no per-vertex DPI divide/multiply.
|
||||||
|
append(
|
||||||
|
&GLOB.tmp_text_verts,
|
||||||
|
Vertex{position = apply_transform(xform, {pos.x, -pos.y}), uv = {uv.x, uv.y}, color = txt.color},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
append(&GLOB.tmp_text_indices, ..data.indices[:data.num_indices])
|
||||||
|
|
||||||
|
batch_idx := u32(len(GLOB.tmp_text_batches))
|
||||||
|
append(
|
||||||
|
&GLOB.tmp_text_batches,
|
||||||
|
TextBatch {
|
||||||
|
atlas_texture = data.atlas_texture,
|
||||||
|
vertex_start = vertex_start,
|
||||||
|
vertex_count = u32(data.num_vertices),
|
||||||
|
index_start = index_start,
|
||||||
|
index_count = u32(data.num_indices),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
append_or_extend_sub_batch(scissor, layer, .Text, batch_idx, 1)
|
||||||
|
|
||||||
|
data = data.next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Append a new sub-batch or extend the last one if same kind and contiguous.
|
// Append a new sub-batch or extend the last one if same kind and contiguous.
|
||||||
@(private)
|
@(private)
|
||||||
append_or_extend_sub_batch :: proc(
|
append_or_extend_sub_batch :: proc(
|
||||||
@@ -466,26 +522,13 @@ prepare_clay_batch :: proc(
|
|||||||
render_data := render_command.renderData.text
|
render_data := render_command.renderData.text
|
||||||
txt := string(render_data.stringContents.chars[:render_data.stringContents.length])
|
txt := string(render_data.stringContents.chars[:render_data.stringContents.length])
|
||||||
c_text := strings.clone_to_cstring(txt, context.temp_allocator)
|
c_text := strings.clone_to_cstring(txt, context.temp_allocator)
|
||||||
sdl_text := GLOB.text_cache.cache[render_command.id]
|
// Clay's render_command.id is already hashed with the same Jenkins algorithm
|
||||||
|
// as text_cache_hash, so it shares the same keyspace.
|
||||||
if sdl_text == nil {
|
sdl_text := cache_get_or_update(
|
||||||
// Cache a SDL text object
|
render_command.id,
|
||||||
sdl_text = sdl_ttf.CreateText(
|
c_text,
|
||||||
GLOB.text_cache.engine,
|
get_font(render_data.fontId, render_data.fontSize),
|
||||||
get_font(render_data.fontId, render_data.fontSize),
|
)
|
||||||
c_text,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
if sdl_text == nil {
|
|
||||||
log.panicf("Failed to create SDL text for clay render command: %s", sdl.GetError())
|
|
||||||
}
|
|
||||||
GLOB.text_cache.cache[render_command.id] = sdl_text
|
|
||||||
} else {
|
|
||||||
if !sdl_ttf.SetTextString(sdl_text, c_text, 0) {
|
|
||||||
log.panicf("Failed to update SDL text string: %s", sdl.GetError())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, color_from_clay(render_data.textColor)})
|
prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, color_from_clay(render_data.textColor)})
|
||||||
case clay.RenderCommandType.Image:
|
case clay.RenderCommandType.Image:
|
||||||
case clay.RenderCommandType.ScissorStart:
|
case clay.RenderCommandType.ScissorStart:
|
||||||
@@ -749,3 +792,112 @@ destroy_buffer :: proc(device: ^sdl.GPUDevice, buffer: ^Buffer) {
|
|||||||
sdl.ReleaseGPUBuffer(device, buffer.gpu)
|
sdl.ReleaseGPUBuffer(device, buffer.gpu)
|
||||||
sdl.ReleaseGPUTransferBuffer(device, buffer.transfer)
|
sdl.ReleaseGPUTransferBuffer(device, buffer.transfer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Transform ------------------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 2x3 affine transform for 2D pivot-rotation.
|
||||||
|
// Used internally by rotation-aware drawing procs.
|
||||||
|
Transform_2D :: struct {
|
||||||
|
m00, m01: f32, // row 0: rotation/scale
|
||||||
|
m10, m11: f32, // row 1: rotation/scale
|
||||||
|
tx, ty: f32, // translation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a pivot-rotation transform.
|
||||||
|
//
|
||||||
|
// Semantics (raylib-style):
|
||||||
|
// The point whose local coordinates equal `origin` lands at `pos` in world space.
|
||||||
|
// The rest of the shape rotates around that pivot.
|
||||||
|
//
|
||||||
|
// Formula: p_world = pos + R(θ) · (p_local - origin)
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// pos – world-space position where the pivot lands.
|
||||||
|
// origin – pivot point in local space (measured from the shape's natural reference point).
|
||||||
|
// rotation_deg – rotation in degrees, counter-clockwise.
|
||||||
|
//
|
||||||
|
build_pivot_rot :: proc(pos: [2]f32, origin: [2]f32, rotation_deg: f32) -> Transform_2D {
|
||||||
|
rad := math.to_radians(rotation_deg)
|
||||||
|
c := math.cos(rad)
|
||||||
|
s := math.sin(rad)
|
||||||
|
return Transform_2D {
|
||||||
|
m00 = c,
|
||||||
|
m01 = -s,
|
||||||
|
m10 = s,
|
||||||
|
m11 = c,
|
||||||
|
tx = pos.x - (c * origin.x - s * origin.y),
|
||||||
|
ty = pos.y - (s * origin.x + c * origin.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the transform to a local-space point, producing a world-space point.
|
||||||
|
apply_transform :: #force_inline proc(t: Transform_2D, p: [2]f32) -> [2]f32 {
|
||||||
|
return {t.m00 * p.x + t.m01 * p.y + t.tx, t.m10 * p.x + t.m11 * p.y + t.ty}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast-path check callers use BEFORE building a transform.
|
||||||
|
// Returns true if either the origin is non-zero or rotation is non-zero,
|
||||||
|
// meaning a transform actually needs to be computed.
|
||||||
|
needs_transform :: #force_inline proc(origin: [2]f32, rotation: f32) -> bool {
|
||||||
|
return origin != {0, 0} || rotation != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Procedure Groups ------------------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
center_of :: proc {
|
||||||
|
center_of_rect,
|
||||||
|
center_of_triangle,
|
||||||
|
center_of_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
top_left_of :: proc {
|
||||||
|
top_left_of_rect,
|
||||||
|
top_left_of_triangle,
|
||||||
|
top_left_of_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
top_of :: proc {
|
||||||
|
top_of_rect,
|
||||||
|
top_of_triangle,
|
||||||
|
top_of_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
top_right_of :: proc {
|
||||||
|
top_right_of_rect,
|
||||||
|
top_right_of_triangle,
|
||||||
|
top_right_of_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
left_of :: proc {
|
||||||
|
left_of_rect,
|
||||||
|
left_of_triangle,
|
||||||
|
left_of_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
right_of :: proc {
|
||||||
|
right_of_rect,
|
||||||
|
right_of_triangle,
|
||||||
|
right_of_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_left_of :: proc {
|
||||||
|
bottom_left_of_rect,
|
||||||
|
bottom_left_of_triangle,
|
||||||
|
bottom_left_of_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_of :: proc {
|
||||||
|
bottom_of_rect,
|
||||||
|
bottom_of_triangle,
|
||||||
|
bottom_of_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_right_of :: proc {
|
||||||
|
bottom_right_of_rect,
|
||||||
|
bottom_right_of_triangle,
|
||||||
|
bottom_right_of_text,
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package examples
|
|||||||
|
|
||||||
import "../../draw"
|
import "../../draw"
|
||||||
import "../../vendor/clay"
|
import "../../vendor/clay"
|
||||||
import "core:c"
|
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import sdl "vendor:sdl3"
|
import sdl "vendor:sdl3"
|
||||||
import sdl_ttf "vendor:sdl3/ttf"
|
|
||||||
|
|
||||||
JETBRAINS_MONO_REGULAR_RAW :: #load("fonts/JetBrainsMono-Regular.ttf")
|
JETBRAINS_MONO_REGULAR_RAW :: #load("fonts/JetBrainsMono-Regular.ttf")
|
||||||
JETBRAINS_MONO_REGULAR: draw.Font_Id = max(draw.Font_Id) // Max so we crash if registration is forgotten
|
JETBRAINS_MONO_REGULAR: draw.Font_Id = max(draw.Font_Id) // Max so we crash if registration is forgotten
|
||||||
@@ -17,21 +15,24 @@ hellope_shapes :: proc() {
|
|||||||
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
|
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
|
||||||
if !draw.init(gpu, window) do os.exit(1)
|
if !draw.init(gpu, window) do os.exit(1)
|
||||||
|
|
||||||
|
spin_angle: f32 = 0
|
||||||
|
|
||||||
for {
|
for {
|
||||||
defer free_all(context.temp_allocator)
|
defer free_all(context.temp_allocator)
|
||||||
ev: sdl.Event
|
ev: sdl.Event
|
||||||
for sdl.PollEvent(&ev) {
|
for sdl.PollEvent(&ev) {
|
||||||
if ev.type == .QUIT do return
|
if ev.type == .QUIT do return
|
||||||
}
|
}
|
||||||
|
spin_angle += 1
|
||||||
base_layer := draw.begin({w = 500, h = 500})
|
base_layer := draw.begin({w = 500, h = 500})
|
||||||
|
|
||||||
// Background
|
// Background
|
||||||
draw.rectangle(base_layer, {0, 0, 500, 500}, {40, 40, 40, 255})
|
draw.rectangle(base_layer, {0, 0, 500, 500}, {40, 40, 40, 255})
|
||||||
|
|
||||||
// Shapes demo
|
// ----- Shapes without rotation (existing demo) -----
|
||||||
draw.rectangle(base_layer, {20, 20, 200, 120}, {80, 120, 200, 255})
|
draw.rectangle(base_layer, {20, 20, 200, 120}, {80, 120, 200, 255})
|
||||||
draw.rectangle_lines(base_layer, {20, 20, 200, 120}, draw.WHITE, thick = 2)
|
draw.rectangle_lines(base_layer, {20, 20, 200, 120}, draw.WHITE, thick = 2)
|
||||||
draw.rectangle_rounded(base_layer, {240, 20, 240, 120}, 0.3, {200, 80, 80, 255})
|
draw.rectangle(base_layer, {240, 20, 240, 120}, {200, 80, 80, 255}, roundness = 0.3)
|
||||||
draw.rectangle_gradient(
|
draw.rectangle_gradient(
|
||||||
base_layer,
|
base_layer,
|
||||||
{20, 160, 460, 60},
|
{20, 160, 460, 60},
|
||||||
@@ -41,15 +42,66 @@ hellope_shapes :: proc() {
|
|||||||
{255, 255, 0, 255},
|
{255, 255, 0, 255},
|
||||||
)
|
)
|
||||||
|
|
||||||
draw.circle(base_layer, {120, 320}, 60, {100, 200, 100, 255})
|
// ----- Rotation demos -----
|
||||||
draw.circle_lines(base_layer, {120, 320}, 60, draw.WHITE, thick = 2)
|
|
||||||
draw.circle_gradient(base_layer, {300, 320}, 60, {255, 200, 50, 255}, {200, 50, 50, 255})
|
|
||||||
draw.ring(base_layer, {430, 320}, 30, 55, 0, 270, {100, 100, 220, 255})
|
|
||||||
|
|
||||||
draw.triangle(base_layer, {60, 420}, {180, 480}, {20, 480}, {220, 180, 60, 255})
|
// Rectangle rotating around its center
|
||||||
draw.line(base_layer, {220, 420}, {460, 480}, {255, 255, 100, 255}, thick = 3)
|
rect := draw.Rectangle{100, 320, 80, 50}
|
||||||
draw.poly(base_layer, {350, 450}, 6, 40, {180, 100, 220, 255}, rotation = 30)
|
draw.rectangle(
|
||||||
draw.poly_lines(base_layer, {350, 450}, 6, 40, draw.WHITE, rotation = 30, thick = 2)
|
base_layer,
|
||||||
|
rect,
|
||||||
|
{100, 200, 100, 255},
|
||||||
|
origin = draw.center_of(rect),
|
||||||
|
rotation = spin_angle,
|
||||||
|
)
|
||||||
|
draw.rectangle_lines(
|
||||||
|
base_layer,
|
||||||
|
rect,
|
||||||
|
draw.WHITE,
|
||||||
|
thick = 2,
|
||||||
|
origin = draw.center_of(rect),
|
||||||
|
rotation = spin_angle,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rounded rectangle rotating around its center
|
||||||
|
rrect := draw.Rectangle{230, 300, 100, 80}
|
||||||
|
draw.rectangle(
|
||||||
|
base_layer,
|
||||||
|
rrect,
|
||||||
|
{200, 100, 200, 255},
|
||||||
|
roundness = 0.4,
|
||||||
|
origin = draw.center_of(rrect),
|
||||||
|
rotation = spin_angle,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ellipse rotating around its center (tilted ellipse)
|
||||||
|
draw.ellipse(base_layer, {410, 340}, 50, 30, {255, 200, 50, 255}, rotation = spin_angle)
|
||||||
|
|
||||||
|
// Circle orbiting a point (moon orbiting planet)
|
||||||
|
planet_pos := [2]f32{100, 450}
|
||||||
|
moon_pos := planet_pos + {0, -40}
|
||||||
|
draw.circle(base_layer, planet_pos, 8, {200, 200, 200, 255}) // planet (stationary)
|
||||||
|
draw.circle(base_layer, moon_pos, 5, {100, 150, 255, 255}, origin = {0, 40}, rotation = spin_angle) // moon orbiting
|
||||||
|
|
||||||
|
// Ring arc rotating in place
|
||||||
|
draw.ring(base_layer, {250, 450}, 15, 30, 0, 270, {100, 100, 220, 255}, rotation = spin_angle)
|
||||||
|
|
||||||
|
// Triangle rotating around its center
|
||||||
|
tv1 := [2]f32{350, 420}
|
||||||
|
tv2 := [2]f32{420, 480}
|
||||||
|
tv3 := [2]f32{340, 480}
|
||||||
|
draw.triangle(
|
||||||
|
base_layer,
|
||||||
|
tv1,
|
||||||
|
tv2,
|
||||||
|
tv3,
|
||||||
|
{220, 180, 60, 255},
|
||||||
|
origin = draw.center_of(tv1, tv2, tv3),
|
||||||
|
rotation = spin_angle,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Polygon rotating around its center (already had rotation; now with origin for orbit)
|
||||||
|
draw.poly(base_layer, {460, 450}, 6, 30, {180, 100, 220, 255}, rotation = spin_angle)
|
||||||
|
draw.poly_lines(base_layer, {460, 450}, 6, 30, draw.WHITE, rotation = spin_angle, thick = 2)
|
||||||
|
|
||||||
draw.end(gpu, window)
|
draw.end(gpu, window)
|
||||||
}
|
}
|
||||||
@@ -57,17 +109,14 @@ hellope_shapes :: proc() {
|
|||||||
|
|
||||||
hellope_text :: proc() {
|
hellope_text :: proc() {
|
||||||
if !sdl.Init({.VIDEO}) do os.exit(1)
|
if !sdl.Init({.VIDEO}) do os.exit(1)
|
||||||
window := sdl.CreateWindow("Hellope!", 500, 500, {.HIGH_PIXEL_DENSITY})
|
window := sdl.CreateWindow("Hellope!", 600, 600, {.HIGH_PIXEL_DENSITY})
|
||||||
gpu := sdl.CreateGPUDevice({.MSL}, true, nil)
|
gpu := sdl.CreateGPUDevice({.MSL}, true, nil)
|
||||||
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
|
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
|
||||||
if !draw.init(gpu, window) do os.exit(1)
|
if !draw.init(gpu, window) do os.exit(1)
|
||||||
JETBRAINS_MONO_REGULAR = draw.register_font(JETBRAINS_MONO_REGULAR_RAW)
|
JETBRAINS_MONO_REGULAR = draw.register_font(JETBRAINS_MONO_REGULAR_RAW)
|
||||||
|
|
||||||
FONT_SIZE :: u16(24)
|
FONT_SIZE :: u16(24)
|
||||||
TEXT_ID :: u32(1)
|
spin_angle: f32 = 0
|
||||||
|
|
||||||
font := draw.get_font(JETBRAINS_MONO_REGULAR, FONT_SIZE)
|
|
||||||
dpi := sdl.GetWindowDisplayScale(window)
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
defer free_all(context.temp_allocator)
|
defer free_all(context.temp_allocator)
|
||||||
@@ -75,28 +124,74 @@ hellope_text :: proc() {
|
|||||||
for sdl.PollEvent(&ev) {
|
for sdl.PollEvent(&ev) {
|
||||||
if ev.type == .QUIT do return
|
if ev.type == .QUIT do return
|
||||||
}
|
}
|
||||||
base_layer := draw.begin({w = 500, h = 500})
|
spin_angle += 0.5
|
||||||
|
base_layer := draw.begin({w = 600, h = 600})
|
||||||
|
|
||||||
// Grey background
|
// Grey background
|
||||||
draw.rectangle(base_layer, {0, 0, 500, 500}, {127, 127, 127, 255})
|
draw.rectangle(base_layer, {0, 0, 600, 600}, {127, 127, 127, 255})
|
||||||
|
|
||||||
// Measure and center text
|
// ----- Text API demos -----
|
||||||
tw, th: c.int
|
|
||||||
sdl_ttf.GetStringSize(font, "Hellope!", 0, &tw, &th)
|
|
||||||
text_w := f32(tw) / dpi
|
|
||||||
text_h := f32(th) / dpi
|
|
||||||
pos_x := (500.0 - text_w) / 2.0
|
|
||||||
pos_y := (500.0 - text_h) / 2.0
|
|
||||||
|
|
||||||
txt := draw.text(
|
// Cached text with id — TTF_Text reused across frames (good for text-heavy apps)
|
||||||
TEXT_ID,
|
draw.text(
|
||||||
|
base_layer,
|
||||||
"Hellope!",
|
"Hellope!",
|
||||||
{pos_x, pos_y},
|
{300, 80},
|
||||||
|
JETBRAINS_MONO_REGULAR,
|
||||||
|
FONT_SIZE,
|
||||||
color = draw.WHITE,
|
color = draw.WHITE,
|
||||||
font_id = JETBRAINS_MONO_REGULAR,
|
origin = draw.center_of("Hellope!", JETBRAINS_MONO_REGULAR, FONT_SIZE),
|
||||||
font_size = FONT_SIZE,
|
id = "hellope",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rotating sentence — verifies multi-word text rotation around center
|
||||||
|
draw.text(
|
||||||
|
base_layer,
|
||||||
|
"Hellope World!",
|
||||||
|
{300, 250},
|
||||||
|
JETBRAINS_MONO_REGULAR,
|
||||||
|
FONT_SIZE,
|
||||||
|
color = {255, 200, 50, 255},
|
||||||
|
origin = draw.center_of("Hellope World!", JETBRAINS_MONO_REGULAR, FONT_SIZE),
|
||||||
|
rotation = spin_angle,
|
||||||
|
id = "rotating_sentence",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Uncached text (no id) — created and destroyed each frame, simplest usage
|
||||||
|
draw.text(
|
||||||
|
base_layer,
|
||||||
|
"Top-left anchored",
|
||||||
|
{20, 450},
|
||||||
|
JETBRAINS_MONO_REGULAR,
|
||||||
|
FONT_SIZE,
|
||||||
|
color = draw.WHITE,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Measure text for manual layout
|
||||||
|
size := draw.measure_text("Measured!", JETBRAINS_MONO_REGULAR, FONT_SIZE)
|
||||||
|
draw.rectangle(base_layer, {300 - size.x / 2, 380, size.x, size.y}, {60, 60, 60, 200})
|
||||||
|
draw.text(
|
||||||
|
base_layer,
|
||||||
|
"Measured!",
|
||||||
|
{300, 380},
|
||||||
|
JETBRAINS_MONO_REGULAR,
|
||||||
|
FONT_SIZE,
|
||||||
|
color = draw.WHITE,
|
||||||
|
origin = draw.top_of("Measured!", JETBRAINS_MONO_REGULAR, FONT_SIZE),
|
||||||
|
id = "measured",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rotating text anchored at top-left (no origin offset) — spins around top-left corner
|
||||||
|
draw.text(
|
||||||
|
base_layer,
|
||||||
|
"Corner spin",
|
||||||
|
{150, 530},
|
||||||
|
JETBRAINS_MONO_REGULAR,
|
||||||
|
FONT_SIZE,
|
||||||
|
color = {100, 200, 255, 255},
|
||||||
|
rotation = spin_angle,
|
||||||
|
id = "corner_spin",
|
||||||
)
|
)
|
||||||
draw.prepare_text(base_layer, txt)
|
|
||||||
|
|
||||||
draw.end(gpu, window)
|
draw.end(gpu, window)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,8 @@ Primitive :: struct {
|
|||||||
bounds: [4]f32, // 0: min_x, min_y, max_x, max_y (world-space, pre-DPI)
|
bounds: [4]f32, // 0: min_x, min_y, max_x, max_y (world-space, pre-DPI)
|
||||||
color: Color, // 16: u8x4, unpacked in shader via unpackUnorm4x8
|
color: Color, // 16: u8x4, unpacked in shader via unpackUnorm4x8
|
||||||
kind_flags: u32, // 20: (kind as u32) | (flags as u32 << 8)
|
kind_flags: u32, // 20: (kind as u32) | (flags as u32 << 8)
|
||||||
_pad: [2]f32, // 24: alignment to vec4 boundary
|
rotation: f32, // 24: shader self-rotation in radians (used by RRect, Ellipse)
|
||||||
|
_pad: f32, // 28: alignment to vec4 boundary
|
||||||
params: Shape_Params, // 32: two vec4s of shape params
|
params: Shape_Params, // 32: two vec4s of shape params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,32 +24,41 @@ struct main0_in
|
|||||||
float4 f_params [[user(locn2)]];
|
float4 f_params [[user(locn2)]];
|
||||||
float4 f_params2 [[user(locn3)]];
|
float4 f_params2 [[user(locn3)]];
|
||||||
uint f_kind_flags [[user(locn4)]];
|
uint f_kind_flags [[user(locn4)]];
|
||||||
|
float f_rotation [[user(locn5), flat]];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static inline __attribute__((always_inline))
|
||||||
|
float2 apply_rotation(thread const float2& p, thread const float& angle)
|
||||||
|
{
|
||||||
|
float cr = cos(-angle);
|
||||||
|
float sr = sin(-angle);
|
||||||
|
return float2x2(float2(cr, sr), float2(-sr, cr)) * p;
|
||||||
|
}
|
||||||
|
|
||||||
static inline __attribute__((always_inline))
|
static inline __attribute__((always_inline))
|
||||||
float sdRoundedBox(thread const float2& p, thread const float2& b, thread float4& r)
|
float sdRoundedBox(thread const float2& p, thread const float2& b, thread float4& r)
|
||||||
{
|
{
|
||||||
float2 _56;
|
float2 _61;
|
||||||
if (p.x > 0.0)
|
if (p.x > 0.0)
|
||||||
{
|
{
|
||||||
_56 = r.xy;
|
_61 = r.xy;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_56 = r.zw;
|
_61 = r.zw;
|
||||||
}
|
}
|
||||||
r.x = _56.x;
|
r.x = _61.x;
|
||||||
r.y = _56.y;
|
r.y = _61.y;
|
||||||
float _73;
|
float _78;
|
||||||
if (p.y > 0.0)
|
if (p.y > 0.0)
|
||||||
{
|
{
|
||||||
_73 = r.x;
|
_78 = r.x;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_73 = r.y;
|
_78 = r.y;
|
||||||
}
|
}
|
||||||
r.x = _73;
|
r.x = _78;
|
||||||
float2 q = (abs(p) - b) + float2(r.x);
|
float2 q = (abs(p) - b) + float2(r.x);
|
||||||
return (fast::min(fast::max(q.x, q.y), 0.0) + length(fast::max(q, float2(0.0)))) - r.x;
|
return (fast::min(fast::max(q.x, q.y), 0.0) + length(fast::max(q, float2(0.0)))) - r.x;
|
||||||
}
|
}
|
||||||
@@ -142,16 +151,23 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
float4 r = float4(in.f_params.zw, in.f_params2.xy);
|
float4 r = float4(in.f_params.zw, in.f_params2.xy);
|
||||||
soft = fast::max(in.f_params2.z, 1.0);
|
soft = fast::max(in.f_params2.z, 1.0);
|
||||||
float stroke_px = in.f_params2.w;
|
float stroke_px = in.f_params2.w;
|
||||||
float2 param = in.f_local_or_uv;
|
float2 p_local = in.f_local_or_uv;
|
||||||
float2 param_1 = b;
|
if (in.f_rotation != 0.0)
|
||||||
float4 param_2 = r;
|
{
|
||||||
float _453 = sdRoundedBox(param, param_1, param_2);
|
float2 param = p_local;
|
||||||
d = _453;
|
float param_1 = in.f_rotation;
|
||||||
|
p_local = apply_rotation(param, param_1);
|
||||||
|
}
|
||||||
|
float2 param_2 = p_local;
|
||||||
|
float2 param_3 = b;
|
||||||
|
float4 param_4 = r;
|
||||||
|
float _491 = sdRoundedBox(param_2, param_3, param_4);
|
||||||
|
d = _491;
|
||||||
if ((flags & 1u) != 0u)
|
if ((flags & 1u) != 0u)
|
||||||
{
|
{
|
||||||
float param_3 = d;
|
float param_5 = d;
|
||||||
float param_4 = stroke_px;
|
float param_6 = stroke_px;
|
||||||
d = sdf_stroke(param_3, param_4);
|
d = sdf_stroke(param_5, param_6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -161,14 +177,14 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
float radius = in.f_params.x;
|
float radius = in.f_params.x;
|
||||||
soft = fast::max(in.f_params.y, 1.0);
|
soft = fast::max(in.f_params.y, 1.0);
|
||||||
float stroke_px_1 = in.f_params.z;
|
float stroke_px_1 = in.f_params.z;
|
||||||
float2 param_5 = in.f_local_or_uv;
|
float2 param_7 = in.f_local_or_uv;
|
||||||
float param_6 = radius;
|
float param_8 = radius;
|
||||||
d = sdCircle(param_5, param_6);
|
d = sdCircle(param_7, param_8);
|
||||||
if ((flags & 1u) != 0u)
|
if ((flags & 1u) != 0u)
|
||||||
{
|
{
|
||||||
float param_7 = d;
|
float param_9 = d;
|
||||||
float param_8 = stroke_px_1;
|
float param_10 = stroke_px_1;
|
||||||
d = sdf_stroke(param_7, param_8);
|
d = sdf_stroke(param_9, param_10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -178,15 +194,22 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
float2 ab = in.f_params.xy;
|
float2 ab = in.f_params.xy;
|
||||||
soft = fast::max(in.f_params.z, 1.0);
|
soft = fast::max(in.f_params.z, 1.0);
|
||||||
float stroke_px_2 = in.f_params.w;
|
float stroke_px_2 = in.f_params.w;
|
||||||
float2 param_9 = in.f_local_or_uv;
|
float2 p_local_1 = in.f_local_or_uv;
|
||||||
float2 param_10 = ab;
|
if (in.f_rotation != 0.0)
|
||||||
float _511 = sdEllipse(param_9, param_10);
|
{
|
||||||
d = _511;
|
float2 param_11 = p_local_1;
|
||||||
|
float param_12 = in.f_rotation;
|
||||||
|
p_local_1 = apply_rotation(param_11, param_12);
|
||||||
|
}
|
||||||
|
float2 param_13 = p_local_1;
|
||||||
|
float2 param_14 = ab;
|
||||||
|
float _560 = sdEllipse(param_13, param_14);
|
||||||
|
d = _560;
|
||||||
if ((flags & 1u) != 0u)
|
if ((flags & 1u) != 0u)
|
||||||
{
|
{
|
||||||
float param_11 = d;
|
float param_15 = d;
|
||||||
float param_12 = stroke_px_2;
|
float param_16 = stroke_px_2;
|
||||||
d = sdf_stroke(param_11, param_12);
|
d = sdf_stroke(param_15, param_16);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -197,10 +220,10 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
float2 b_1 = in.f_params.zw;
|
float2 b_1 = in.f_params.zw;
|
||||||
float width = in.f_params2.x;
|
float width = in.f_params2.x;
|
||||||
soft = fast::max(in.f_params2.y, 1.0);
|
soft = fast::max(in.f_params2.y, 1.0);
|
||||||
float2 param_13 = in.f_local_or_uv;
|
float2 param_17 = in.f_local_or_uv;
|
||||||
float2 param_14 = a;
|
float2 param_18 = a;
|
||||||
float2 param_15 = b_1;
|
float2 param_19 = b_1;
|
||||||
d = sdSegment(param_13, param_14, param_15) - (width * 0.5);
|
d = sdSegment(param_17, param_18, param_19) - (width * 0.5);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -218,26 +241,18 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
{
|
{
|
||||||
angle += 6.283185482025146484375;
|
angle += 6.283185482025146484375;
|
||||||
}
|
}
|
||||||
float ang_start = start_rad;
|
float ang_start = mod(start_rad, 6.283185482025146484375);
|
||||||
float ang_end = end_rad;
|
float ang_end = mod(end_rad, 6.283185482025146484375);
|
||||||
if (ang_start < 0.0)
|
float _654;
|
||||||
{
|
|
||||||
ang_start += 6.283185482025146484375;
|
|
||||||
}
|
|
||||||
if (ang_end < 0.0)
|
|
||||||
{
|
|
||||||
ang_end += 6.283185482025146484375;
|
|
||||||
}
|
|
||||||
float _615;
|
|
||||||
if (ang_end > ang_start)
|
if (ang_end > ang_start)
|
||||||
{
|
{
|
||||||
_615 = float((angle >= ang_start) && (angle <= ang_end));
|
_654 = float((angle >= ang_start) && (angle <= ang_end));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_615 = float((angle >= ang_start) || (angle <= ang_end));
|
_654 = float((angle >= ang_start) || (angle <= ang_end));
|
||||||
}
|
}
|
||||||
float in_arc = _615;
|
float in_arc = _654;
|
||||||
if (abs(ang_end - ang_start) >= 6.282185077667236328125)
|
if (abs(ang_end - ang_start) >= 6.282185077667236328125)
|
||||||
{
|
{
|
||||||
in_arc = 1.0;
|
in_arc = 1.0;
|
||||||
@@ -262,9 +277,9 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
d = (length(p) * cos(bn)) - radius_1;
|
d = (length(p) * cos(bn)) - radius_1;
|
||||||
if ((flags & 1u) != 0u)
|
if ((flags & 1u) != 0u)
|
||||||
{
|
{
|
||||||
float param_16 = d;
|
float param_20 = d;
|
||||||
float param_17 = stroke_px_3;
|
float param_21 = stroke_px_3;
|
||||||
d = sdf_stroke(param_16, param_17);
|
d = sdf_stroke(param_20, param_21);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,9 +287,9 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
float param_18 = d;
|
float param_22 = d;
|
||||||
float param_19 = soft;
|
float param_23 = soft;
|
||||||
float alpha = sdf_alpha(param_18, param_19);
|
float alpha = sdf_alpha(param_22, param_23);
|
||||||
out.out_color = float4(in.f_color.xyz, in.f_color.w * alpha);
|
out.out_color = float4(in.f_color.xyz, in.f_color.w * alpha);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -15,7 +15,8 @@ struct Primitive
|
|||||||
float4 bounds;
|
float4 bounds;
|
||||||
uint color;
|
uint color;
|
||||||
uint kind_flags;
|
uint kind_flags;
|
||||||
float2 _pad;
|
float rotation;
|
||||||
|
float _pad;
|
||||||
float4 params;
|
float4 params;
|
||||||
float4 params2;
|
float4 params2;
|
||||||
};
|
};
|
||||||
@@ -25,7 +26,8 @@ struct Primitive_1
|
|||||||
float4 bounds;
|
float4 bounds;
|
||||||
uint color;
|
uint color;
|
||||||
uint kind_flags;
|
uint kind_flags;
|
||||||
float2 _pad;
|
float rotation;
|
||||||
|
float _pad;
|
||||||
float4 params;
|
float4 params;
|
||||||
float4 params2;
|
float4 params2;
|
||||||
};
|
};
|
||||||
@@ -42,6 +44,7 @@ struct main0_out
|
|||||||
float4 f_params [[user(locn2)]];
|
float4 f_params [[user(locn2)]];
|
||||||
float4 f_params2 [[user(locn3)]];
|
float4 f_params2 [[user(locn3)]];
|
||||||
uint f_kind_flags [[user(locn4)]];
|
uint f_kind_flags [[user(locn4)]];
|
||||||
|
float f_rotation [[user(locn5)]];
|
||||||
float4 gl_Position [[position]];
|
float4 gl_Position [[position]];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,7 +55,7 @@ struct main0_in
|
|||||||
float4 v_color [[attribute(2)]];
|
float4 v_color [[attribute(2)]];
|
||||||
};
|
};
|
||||||
|
|
||||||
vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Primitives& _70 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
|
vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Primitives& _72 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
|
||||||
{
|
{
|
||||||
main0_out out = {};
|
main0_out out = {};
|
||||||
if (_12.mode == 0u)
|
if (_12.mode == 0u)
|
||||||
@@ -62,17 +65,19 @@ vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer
|
|||||||
out.f_params = float4(0.0);
|
out.f_params = float4(0.0);
|
||||||
out.f_params2 = float4(0.0);
|
out.f_params2 = float4(0.0);
|
||||||
out.f_kind_flags = 0u;
|
out.f_kind_flags = 0u;
|
||||||
|
out.f_rotation = 0.0;
|
||||||
out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0);
|
out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Primitive p;
|
Primitive p;
|
||||||
p.bounds = _70.primitives[int(gl_InstanceIndex)].bounds;
|
p.bounds = _72.primitives[int(gl_InstanceIndex)].bounds;
|
||||||
p.color = _70.primitives[int(gl_InstanceIndex)].color;
|
p.color = _72.primitives[int(gl_InstanceIndex)].color;
|
||||||
p.kind_flags = _70.primitives[int(gl_InstanceIndex)].kind_flags;
|
p.kind_flags = _72.primitives[int(gl_InstanceIndex)].kind_flags;
|
||||||
p._pad = _70.primitives[int(gl_InstanceIndex)]._pad;
|
p.rotation = _72.primitives[int(gl_InstanceIndex)].rotation;
|
||||||
p.params = _70.primitives[int(gl_InstanceIndex)].params;
|
p._pad = _72.primitives[int(gl_InstanceIndex)]._pad;
|
||||||
p.params2 = _70.primitives[int(gl_InstanceIndex)].params2;
|
p.params = _72.primitives[int(gl_InstanceIndex)].params;
|
||||||
|
p.params2 = _72.primitives[int(gl_InstanceIndex)].params2;
|
||||||
float2 corner = in.v_position;
|
float2 corner = in.v_position;
|
||||||
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
||||||
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
||||||
@@ -81,6 +86,7 @@ vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer
|
|||||||
out.f_params = p.params;
|
out.f_params = p.params;
|
||||||
out.f_params2 = p.params2;
|
out.f_params2 = p.params2;
|
||||||
out.f_kind_flags = p.kind_flags;
|
out.f_kind_flags = p.kind_flags;
|
||||||
|
out.f_rotation = p.rotation;
|
||||||
out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0);
|
out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
Binary file not shown.
@@ -6,6 +6,7 @@ layout(location = 1) in vec2 f_local_or_uv;
|
|||||||
layout(location = 2) in vec4 f_params;
|
layout(location = 2) in vec4 f_params;
|
||||||
layout(location = 3) in vec4 f_params2;
|
layout(location = 3) in vec4 f_params2;
|
||||||
layout(location = 4) flat in uint f_kind_flags;
|
layout(location = 4) flat in uint f_kind_flags;
|
||||||
|
layout(location = 5) flat in float f_rotation;
|
||||||
|
|
||||||
// --- Output ---
|
// --- Output ---
|
||||||
layout(location = 0) out vec4 out_color;
|
layout(location = 0) out vec4 out_color;
|
||||||
@@ -82,6 +83,15 @@ float sdf_stroke(float d, float stroke_width) {
|
|||||||
return abs(d) - stroke_width * 0.5;
|
return abs(d) - stroke_width * 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rotate a 2D point by the negative of the given angle (inverse rotation).
|
||||||
|
// Used to rotate the sampling frame opposite to the shape's rotation so that
|
||||||
|
// the SDF evaluates correctly for the rotated shape.
|
||||||
|
vec2 apply_rotation(vec2 p, float angle) {
|
||||||
|
float cr = cos(-angle);
|
||||||
|
float sr = sin(-angle);
|
||||||
|
return mat2(cr, sr, -sr, cr) * p;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// main
|
// main
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -113,11 +123,16 @@ void main() {
|
|||||||
soft = max(f_params2.z, 1.0);
|
soft = max(f_params2.z, 1.0);
|
||||||
float stroke_px = f_params2.w;
|
float stroke_px = f_params2.w;
|
||||||
|
|
||||||
d = sdRoundedBox(f_local_or_uv, b, r);
|
vec2 p_local = f_local_or_uv;
|
||||||
|
if (f_rotation != 0.0) {
|
||||||
|
p_local = apply_rotation(p_local, f_rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
d = sdRoundedBox(p_local, b, r);
|
||||||
if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px);
|
if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px);
|
||||||
}
|
}
|
||||||
else if (kind == 2u) {
|
else if (kind == 2u) {
|
||||||
// Circle
|
// Circle — rotationally symmetric, no rotation needed
|
||||||
float radius = f_params.x;
|
float radius = f_params.x;
|
||||||
soft = max(f_params.y, 1.0);
|
soft = max(f_params.y, 1.0);
|
||||||
float stroke_px = f_params.z;
|
float stroke_px = f_params.z;
|
||||||
@@ -131,11 +146,16 @@ void main() {
|
|||||||
soft = max(f_params.z, 1.0);
|
soft = max(f_params.z, 1.0);
|
||||||
float stroke_px = f_params.w;
|
float stroke_px = f_params.w;
|
||||||
|
|
||||||
d = sdEllipse(f_local_or_uv, ab);
|
vec2 p_local = f_local_or_uv;
|
||||||
|
if (f_rotation != 0.0) {
|
||||||
|
p_local = apply_rotation(p_local, f_rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
d = sdEllipse(p_local, ab);
|
||||||
if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px);
|
if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px);
|
||||||
}
|
}
|
||||||
else if (kind == 4u) {
|
else if (kind == 4u) {
|
||||||
// Segment (capsule line)
|
// Segment (capsule line) — no rotation (excluded)
|
||||||
vec2 a = f_params.xy; // already in local physical pixels
|
vec2 a = f_params.xy; // already in local physical pixels
|
||||||
vec2 b = f_params.zw;
|
vec2 b = f_params.zw;
|
||||||
float width = f_params2.x;
|
float width = f_params2.x;
|
||||||
@@ -144,7 +164,7 @@ void main() {
|
|||||||
d = sdSegment(f_local_or_uv, a, b) - width * 0.5;
|
d = sdSegment(f_local_or_uv, a, b) - width * 0.5;
|
||||||
}
|
}
|
||||||
else if (kind == 5u) {
|
else if (kind == 5u) {
|
||||||
// Ring / Arc
|
// Ring / Arc — rotation handled by CPU angle offset, no shader rotation
|
||||||
float inner = f_params.x;
|
float inner = f_params.x;
|
||||||
float outer = f_params.y;
|
float outer = f_params.y;
|
||||||
float start_rad = f_params.z;
|
float start_rad = f_params.z;
|
||||||
@@ -157,10 +177,8 @@ void main() {
|
|||||||
// Angular clip
|
// Angular clip
|
||||||
float angle = atan(f_local_or_uv.y, f_local_or_uv.x);
|
float angle = atan(f_local_or_uv.y, f_local_or_uv.x);
|
||||||
if (angle < 0.0) angle += 2.0 * PI;
|
if (angle < 0.0) angle += 2.0 * PI;
|
||||||
float ang_start = start_rad;
|
float ang_start = mod(start_rad, 2.0 * PI);
|
||||||
float ang_end = end_rad;
|
float ang_end = mod(end_rad, 2.0 * PI);
|
||||||
if (ang_start < 0.0) ang_start += 2.0 * PI;
|
|
||||||
if (ang_end < 0.0) ang_end += 2.0 * PI;
|
|
||||||
|
|
||||||
float in_arc = (ang_end > ang_start)
|
float in_arc = (ang_end > ang_start)
|
||||||
? ((angle >= ang_start && angle <= ang_end) ? 1.0 : 0.0) : ((angle >= ang_start || angle <= ang_end) ? 1.0 : 0.0);
|
? ((angle >= ang_start && angle <= ang_end) ? 1.0 : 0.0) : ((angle >= ang_start || angle <= ang_end) ? 1.0 : 0.0);
|
||||||
@@ -169,7 +187,7 @@ void main() {
|
|||||||
d = in_arc > 0.5 ? d_ring : 1e30;
|
d = in_arc > 0.5 ? d_ring : 1e30;
|
||||||
}
|
}
|
||||||
else if (kind == 6u) {
|
else if (kind == 6u) {
|
||||||
// Regular N-gon
|
// Regular N-gon — has its own rotation in params, no Primitive.rotation used
|
||||||
float radius = f_params.x;
|
float radius = f_params.x;
|
||||||
float rotation = f_params.y;
|
float rotation = f_params.y;
|
||||||
float sides = f_params.z;
|
float sides = f_params.z;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ layout(location = 1) out vec2 f_local_or_uv;
|
|||||||
layout(location = 2) out vec4 f_params;
|
layout(location = 2) out vec4 f_params;
|
||||||
layout(location = 3) out vec4 f_params2;
|
layout(location = 3) out vec4 f_params2;
|
||||||
layout(location = 4) flat out uint f_kind_flags;
|
layout(location = 4) flat out uint f_kind_flags;
|
||||||
|
layout(location = 5) flat out float f_rotation;
|
||||||
|
|
||||||
// ---------- Uniforms (single block — avoids spirv-cross reordering on Metal) ----------
|
// ---------- Uniforms (single block — avoids spirv-cross reordering on Metal) ----------
|
||||||
layout(set = 1, binding = 0) uniform Uniforms {
|
layout(set = 1, binding = 0) uniform Uniforms {
|
||||||
@@ -24,7 +25,8 @@ struct Primitive {
|
|||||||
vec4 bounds; // 0-15: min_x, min_y, max_x, max_y
|
vec4 bounds; // 0-15: min_x, min_y, max_x, max_y
|
||||||
uint color; // 16-19: packed u8x4 (unpack with unpackUnorm4x8)
|
uint color; // 16-19: packed u8x4 (unpack with unpackUnorm4x8)
|
||||||
uint kind_flags; // 20-23: kind | (flags << 8)
|
uint kind_flags; // 20-23: kind | (flags << 8)
|
||||||
vec2 _pad; // 24-31: padding
|
float rotation; // 24-27: shader self-rotation in radians
|
||||||
|
float _pad; // 28-31: alignment padding
|
||||||
vec4 params; // 32-47: shape params part 1
|
vec4 params; // 32-47: shape params part 1
|
||||||
vec4 params2; // 48-63: shape params part 2
|
vec4 params2; // 48-63: shape params part 2
|
||||||
};
|
};
|
||||||
@@ -42,6 +44,7 @@ void main() {
|
|||||||
f_params = vec4(0.0);
|
f_params = vec4(0.0);
|
||||||
f_params2 = vec4(0.0);
|
f_params2 = vec4(0.0);
|
||||||
f_kind_flags = 0u;
|
f_kind_flags = 0u;
|
||||||
|
f_rotation = 0.0;
|
||||||
|
|
||||||
gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0);
|
gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0);
|
||||||
} else {
|
} else {
|
||||||
@@ -57,6 +60,7 @@ void main() {
|
|||||||
f_params = p.params;
|
f_params = p.params;
|
||||||
f_params2 = p.params2;
|
f_params2 = p.params2;
|
||||||
f_kind_flags = p.kind_flags;
|
f_kind_flags = p.kind_flags;
|
||||||
|
f_rotation = p.rotation;
|
||||||
|
|
||||||
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
702
draw/shapes.odin
702
draw/shapes.odin
@@ -76,90 +76,6 @@ pixel :: proc(layer: ^Layer, pos: [2]f32, color: Color) {
|
|||||||
prepare_shape(layer, vertices[:])
|
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(
|
rectangle_gradient :: proc(
|
||||||
layer: ^Layer,
|
layer: ^Layer,
|
||||||
rect: Rectangle,
|
rect: Rectangle,
|
||||||
@@ -189,6 +105,8 @@ circle_sector :: proc(
|
|||||||
radius: f32,
|
radius: f32,
|
||||||
start_angle, end_angle: f32,
|
start_angle, end_angle: f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
|
origin: [2]f32 = {0, 0},
|
||||||
|
rotation: f32 = 0,
|
||||||
segments: int = 0,
|
segments: int = 0,
|
||||||
temp_allocator := context.temp_allocator,
|
temp_allocator := context.temp_allocator,
|
||||||
) {
|
) {
|
||||||
@@ -202,17 +120,34 @@ circle_sector :: proc(
|
|||||||
end_rad := math.to_radians(end_angle)
|
end_rad := math.to_radians(end_angle)
|
||||||
step_angle := (end_rad - start_rad) / f32(segs)
|
step_angle := (end_rad - start_rad) / f32(segs)
|
||||||
|
|
||||||
for i in 0 ..< segs {
|
if !needs_transform(origin, rotation) {
|
||||||
current_angle := start_rad + step_angle * f32(i)
|
for i in 0 ..< segs {
|
||||||
next_angle := start_rad + step_angle * f32(i + 1)
|
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_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}
|
edge_next := center + [2]f32{math.cos(next_angle) * radius, math.sin(next_angle) * radius}
|
||||||
|
|
||||||
idx := i * 3
|
idx := i * 3
|
||||||
vertices[idx + 0] = sv(center, color)
|
vertices[idx + 0] = sv(center, color)
|
||||||
vertices[idx + 1] = sv(edge_next, color)
|
vertices[idx + 1] = sv(edge_next, color)
|
||||||
vertices[idx + 2] = sv(edge_current, color)
|
vertices[idx + 2] = sv(edge_current, color)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
xform := build_pivot_rot(center, origin, rotation)
|
||||||
|
center_local := [2]f32{0, 0}
|
||||||
|
for i in 0 ..< segs {
|
||||||
|
current_angle := start_rad + step_angle * f32(i)
|
||||||
|
next_angle := start_rad + 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] = sv(apply_transform(xform, center_local), color)
|
||||||
|
vertices[idx + 1] = sv(apply_transform(xform, edge_next), color)
|
||||||
|
vertices[idx + 2] = sv(apply_transform(xform, edge_current), color)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prepare_shape(layer, vertices)
|
prepare_shape(layer, vertices)
|
||||||
@@ -223,6 +158,8 @@ circle_gradient :: proc(
|
|||||||
center: [2]f32,
|
center: [2]f32,
|
||||||
radius: f32,
|
radius: f32,
|
||||||
inner, outer: Color,
|
inner, outer: Color,
|
||||||
|
origin: [2]f32 = {0, 0},
|
||||||
|
rotation: f32 = 0,
|
||||||
segments: int = 0,
|
segments: int = 0,
|
||||||
temp_allocator := context.temp_allocator,
|
temp_allocator := context.temp_allocator,
|
||||||
) {
|
) {
|
||||||
@@ -233,24 +170,61 @@ circle_gradient :: proc(
|
|||||||
|
|
||||||
step_angle := math.TAU / f32(segs)
|
step_angle := math.TAU / f32(segs)
|
||||||
|
|
||||||
for i in 0 ..< segs {
|
if !needs_transform(origin, rotation) {
|
||||||
current_angle := step_angle * f32(i)
|
for i in 0 ..< segs {
|
||||||
next_angle := step_angle * f32(i + 1)
|
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_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}
|
edge_next := center + [2]f32{math.cos(next_angle) * radius, math.sin(next_angle) * radius}
|
||||||
|
|
||||||
idx := i * 3
|
idx := i * 3
|
||||||
vertices[idx + 0] = sv(center, inner)
|
vertices[idx + 0] = sv(center, inner)
|
||||||
vertices[idx + 1] = sv(edge_next, outer)
|
vertices[idx + 1] = sv(edge_next, outer)
|
||||||
vertices[idx + 2] = sv(edge_current, outer)
|
vertices[idx + 2] = sv(edge_current, outer)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
xform := build_pivot_rot(center, origin, rotation)
|
||||||
|
center_local := [2]f32{0, 0}
|
||||||
|
for i in 0 ..< segs {
|
||||||
|
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] = sv(apply_transform(xform, center_local), inner)
|
||||||
|
vertices[idx + 1] = sv(apply_transform(xform, edge_next), outer)
|
||||||
|
vertices[idx + 2] = sv(apply_transform(xform, edge_current), outer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prepare_shape(layer, vertices)
|
prepare_shape(layer, vertices)
|
||||||
}
|
}
|
||||||
|
|
||||||
triangle :: proc(layer: ^Layer, v1, v2, v3: [2]f32, color: Color) {
|
triangle :: proc(
|
||||||
vertices := [3]Vertex{sv(v1, color), sv(v2, color), sv(v3, color)}
|
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{sv(v1, color), sv(v2, color), sv(v3, color)}
|
||||||
|
prepare_shape(layer, vertices[:])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mn := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||||||
|
xform := build_pivot_rot(mn, origin, rotation)
|
||||||
|
local_v1 := v1 - mn
|
||||||
|
local_v2 := v2 - mn
|
||||||
|
local_v3 := v3 - mn
|
||||||
|
vertices := [3]Vertex {
|
||||||
|
sv(apply_transform(xform, local_v1), color),
|
||||||
|
sv(apply_transform(xform, local_v2), color),
|
||||||
|
sv(apply_transform(xform, local_v3), color),
|
||||||
|
}
|
||||||
prepare_shape(layer, vertices[:])
|
prepare_shape(layer, vertices[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,13 +233,28 @@ triangle_lines :: proc(
|
|||||||
v1, v2, v3: [2]f32,
|
v1, v2, v3: [2]f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
thick: f32 = 1,
|
thick: f32 = 1,
|
||||||
|
origin: [2]f32 = {0, 0},
|
||||||
|
rotation: f32 = 0,
|
||||||
temp_allocator := context.temp_allocator,
|
temp_allocator := context.temp_allocator,
|
||||||
) {
|
) {
|
||||||
vertices := make([]Vertex, 18, temp_allocator)
|
vertices := make([]Vertex, 18, temp_allocator)
|
||||||
write_offset := 0
|
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)
|
if !needs_transform(origin, rotation) {
|
||||||
write_offset += extrude_line(v3, v1, thick, color, vertices, write_offset)
|
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)
|
||||||
|
} else {
|
||||||
|
mn := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||||||
|
xform := build_pivot_rot(mn, origin, rotation)
|
||||||
|
tv1 := apply_transform(xform, v1 - mn)
|
||||||
|
tv2 := apply_transform(xform, v2 - mn)
|
||||||
|
tv3 := apply_transform(xform, v3 - mn)
|
||||||
|
write_offset += extrude_line(tv1, tv2, thick, color, vertices, write_offset)
|
||||||
|
write_offset += extrude_line(tv2, tv3, thick, color, vertices, write_offset)
|
||||||
|
write_offset += extrude_line(tv3, tv1, thick, color, vertices, write_offset)
|
||||||
|
}
|
||||||
|
|
||||||
if write_offset > 0 {
|
if write_offset > 0 {
|
||||||
prepare_shape(layer, vertices[:write_offset])
|
prepare_shape(layer, vertices[:write_offset])
|
||||||
}
|
}
|
||||||
@@ -275,6 +264,8 @@ triangle_fan :: proc(
|
|||||||
layer: ^Layer,
|
layer: ^Layer,
|
||||||
points: [][2]f32,
|
points: [][2]f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
|
origin: [2]f32 = {0, 0},
|
||||||
|
rotation: f32 = 0,
|
||||||
temp_allocator := context.temp_allocator,
|
temp_allocator := context.temp_allocator,
|
||||||
) {
|
) {
|
||||||
if len(points) < 3 do return
|
if len(points) < 3 do return
|
||||||
@@ -283,11 +274,26 @@ triangle_fan :: proc(
|
|||||||
vertex_count := triangle_count * 3
|
vertex_count := triangle_count * 3
|
||||||
vertices := make([]Vertex, vertex_count, temp_allocator)
|
vertices := make([]Vertex, vertex_count, temp_allocator)
|
||||||
|
|
||||||
for i in 1 ..< len(points) - 1 {
|
if !needs_transform(origin, rotation) {
|
||||||
idx := (i - 1) * 3
|
for i in 1 ..< len(points) - 1 {
|
||||||
vertices[idx + 0] = sv(points[0], color)
|
idx := (i - 1) * 3
|
||||||
vertices[idx + 1] = sv(points[i], color)
|
vertices[idx + 0] = sv(points[0], color)
|
||||||
vertices[idx + 2] = sv(points[i + 1], color)
|
vertices[idx + 1] = sv(points[i], color)
|
||||||
|
vertices[idx + 2] = sv(points[i + 1], color)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mn := [2]f32{max(f32), max(f32)}
|
||||||
|
for p in points {
|
||||||
|
mn.x = min(mn.x, p.x)
|
||||||
|
mn.y = min(mn.y, p.y)
|
||||||
|
}
|
||||||
|
xform := build_pivot_rot(mn, origin, rotation)
|
||||||
|
for i in 1 ..< len(points) - 1 {
|
||||||
|
idx := (i - 1) * 3
|
||||||
|
vertices[idx + 0] = sv(apply_transform(xform, points[0] - mn), color)
|
||||||
|
vertices[idx + 1] = sv(apply_transform(xform, points[i] - mn), color)
|
||||||
|
vertices[idx + 2] = sv(apply_transform(xform, points[i + 1] - mn), color)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prepare_shape(layer, vertices)
|
prepare_shape(layer, vertices)
|
||||||
@@ -297,6 +303,8 @@ triangle_strip :: proc(
|
|||||||
layer: ^Layer,
|
layer: ^Layer,
|
||||||
points: [][2]f32,
|
points: [][2]f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
|
origin: [2]f32 = {0, 0},
|
||||||
|
rotation: f32 = 0,
|
||||||
temp_allocator := context.temp_allocator,
|
temp_allocator := context.temp_allocator,
|
||||||
) {
|
) {
|
||||||
if len(points) < 3 do return
|
if len(points) < 3 do return
|
||||||
@@ -305,16 +313,37 @@ triangle_strip :: proc(
|
|||||||
vertex_count := triangle_count * 3
|
vertex_count := triangle_count * 3
|
||||||
vertices := make([]Vertex, vertex_count, temp_allocator)
|
vertices := make([]Vertex, vertex_count, temp_allocator)
|
||||||
|
|
||||||
for i in 0 ..< triangle_count {
|
if !needs_transform(origin, rotation) {
|
||||||
idx := i * 3
|
for i in 0 ..< triangle_count {
|
||||||
if i % 2 == 0 {
|
idx := i * 3
|
||||||
vertices[idx + 0] = sv(points[i], color)
|
if i % 2 == 0 {
|
||||||
vertices[idx + 1] = sv(points[i + 1], color)
|
vertices[idx + 0] = sv(points[i], color)
|
||||||
vertices[idx + 2] = sv(points[i + 2], color)
|
vertices[idx + 1] = sv(points[i + 1], color)
|
||||||
} else {
|
vertices[idx + 2] = sv(points[i + 2], color)
|
||||||
vertices[idx + 0] = sv(points[i + 1], color)
|
} else {
|
||||||
vertices[idx + 1] = sv(points[i], color)
|
vertices[idx + 0] = sv(points[i + 1], color)
|
||||||
vertices[idx + 2] = sv(points[i + 2], color)
|
vertices[idx + 1] = sv(points[i], color)
|
||||||
|
vertices[idx + 2] = sv(points[i + 2], color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mn := [2]f32{max(f32), max(f32)}
|
||||||
|
for p in points {
|
||||||
|
mn.x = min(mn.x, p.x)
|
||||||
|
mn.y = min(mn.y, p.y)
|
||||||
|
}
|
||||||
|
xform := build_pivot_rot(mn, origin, rotation)
|
||||||
|
for i in 0 ..< triangle_count {
|
||||||
|
idx := i * 3
|
||||||
|
if i % 2 == 0 {
|
||||||
|
vertices[idx + 0] = sv(apply_transform(xform, points[i] - mn), color)
|
||||||
|
vertices[idx + 1] = sv(apply_transform(xform, points[i + 1] - mn), color)
|
||||||
|
vertices[idx + 2] = sv(apply_transform(xform, points[i + 2] - mn), color)
|
||||||
|
} else {
|
||||||
|
vertices[idx + 0] = sv(apply_transform(xform, points[i + 1] - mn), color)
|
||||||
|
vertices[idx + 1] = sv(apply_transform(xform, points[i] - mn), color)
|
||||||
|
vertices[idx + 2] = sv(apply_transform(xform, points[i + 2] - mn), color)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,8 +352,68 @@ triangle_strip :: proc(
|
|||||||
|
|
||||||
// ----- SDF drawing functions ----
|
// ----- 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)
|
||||||
|
c, s := math.cos(theta), math.sin(theta)
|
||||||
|
// pivot = center + origin; new_center = pivot + R(θ) * (center - pivot)
|
||||||
|
return center + origin + {c * (-origin.x) - s * (-origin.y), s * (-origin.x) + c * (-origin.y)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the AABB half-extents of a rectangle with half-size (hx, hy) rotated by rot_rad.
|
||||||
|
@(private = "file")
|
||||||
|
rotated_aabb_half :: proc(hx, hy, rot_rad: f32) -> [2]f32 {
|
||||||
|
c_r := abs(math.cos(rot_rad))
|
||||||
|
s_r := abs(math.sin(rot_rad))
|
||||||
|
return {hx * c_r + hy * s_r, hx * s_r + hy * c_r}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.w, rect.h) * 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,
|
||||||
|
thick: f32 = 1,
|
||||||
|
roundness: f32 = 0,
|
||||||
|
origin: [2]f32 = {0, 0},
|
||||||
|
rotation: f32 = 0,
|
||||||
|
soft_px: f32 = 1.0,
|
||||||
|
) {
|
||||||
|
cr := min(rect.w, rect.h) * clamp(roundness, 0, 1) * 0.5
|
||||||
|
rectangle_corners_lines(layer, rect, {cr, cr, cr, cr}, color, thick, origin, rotation, soft_px)
|
||||||
|
}
|
||||||
|
|
||||||
// Draw a rectangle with per-corner rounding radii via SDF.
|
// 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) {
|
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.w, rect.h) * 0.5
|
max_radius := min(rect.w, rect.h) * 0.5
|
||||||
tl := clamp(radii[0], 0, max_radius)
|
tl := clamp(radii[0], 0, max_radius)
|
||||||
tr := clamp(radii[1], 0, max_radius)
|
tr := clamp(radii[1], 0, max_radius)
|
||||||
@@ -334,13 +423,35 @@ rectangle_corners :: proc(layer: ^Layer, rect: Rectangle, radii: [4]f32, color:
|
|||||||
pad := soft_px / GLOB.dpi_scaling
|
pad := soft_px / GLOB.dpi_scaling
|
||||||
dpi := GLOB.dpi_scaling
|
dpi := GLOB.dpi_scaling
|
||||||
|
|
||||||
|
hx := rect.w * 0.5
|
||||||
|
hy := rect.h * 0.5
|
||||||
|
rot_rad: f32 = 0
|
||||||
|
center_x := rect.x + hx
|
||||||
|
center_y := rect.y + hy
|
||||||
|
|
||||||
|
if needs_transform(origin, rotation) {
|
||||||
|
rot_rad = math.to_radians(rotation)
|
||||||
|
xform := build_pivot_rot({rect.x, rect.y}, origin, rotation)
|
||||||
|
new_center := apply_transform(xform, {hx, hy})
|
||||||
|
center_x = new_center.x
|
||||||
|
center_y = new_center.y
|
||||||
|
}
|
||||||
|
|
||||||
|
bhx, bhy := hx, hy
|
||||||
|
if rot_rad != 0 {
|
||||||
|
expanded := rotated_aabb_half(hx, hy, rot_rad)
|
||||||
|
bhx = expanded.x
|
||||||
|
bhy = expanded.y
|
||||||
|
}
|
||||||
|
|
||||||
prim := Primitive {
|
prim := Primitive {
|
||||||
bounds = {rect.x - pad, rect.y - pad, rect.x + rect.w + pad, rect.y + rect.h + pad},
|
bounds = {center_x - bhx - pad, center_y - bhy - pad, center_x + bhx + pad, center_y + bhy + pad},
|
||||||
color = color,
|
color = color,
|
||||||
kind_flags = pack_kind_flags(.RRect, {}),
|
kind_flags = pack_kind_flags(.RRect, {}),
|
||||||
|
rotation = rot_rad,
|
||||||
}
|
}
|
||||||
prim.params.rrect = RRect_Params {
|
prim.params.rrect = RRect_Params {
|
||||||
half_size = {rect.w * 0.5 * dpi, rect.h * 0.5 * dpi},
|
half_size = {hx * dpi, hy * dpi},
|
||||||
radii = {tr * dpi, br * dpi, tl * dpi, bl * dpi},
|
radii = {tr * dpi, br * dpi, tl * dpi, bl * dpi},
|
||||||
soft_px = soft_px,
|
soft_px = soft_px,
|
||||||
stroke_px = 0,
|
stroke_px = 0,
|
||||||
@@ -355,6 +466,8 @@ rectangle_corners_lines :: proc(
|
|||||||
radii: [4]f32,
|
radii: [4]f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
thick: f32 = 1,
|
thick: f32 = 1,
|
||||||
|
origin: [2]f32 = {0, 0},
|
||||||
|
rotation: f32 = 0,
|
||||||
soft_px: f32 = 1.0,
|
soft_px: f32 = 1.0,
|
||||||
) {
|
) {
|
||||||
max_radius := min(rect.w, rect.h) * 0.5
|
max_radius := min(rect.w, rect.h) * 0.5
|
||||||
@@ -366,13 +479,35 @@ rectangle_corners_lines :: proc(
|
|||||||
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
|
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
|
||||||
dpi := GLOB.dpi_scaling
|
dpi := GLOB.dpi_scaling
|
||||||
|
|
||||||
|
hx := rect.w * 0.5
|
||||||
|
hy := rect.h * 0.5
|
||||||
|
rot_rad: f32 = 0
|
||||||
|
center_x := rect.x + hx
|
||||||
|
center_y := rect.y + hy
|
||||||
|
|
||||||
|
if needs_transform(origin, rotation) {
|
||||||
|
rot_rad = math.to_radians(rotation)
|
||||||
|
xform := build_pivot_rot({rect.x, rect.y}, origin, rotation)
|
||||||
|
new_center := apply_transform(xform, {hx, hy})
|
||||||
|
center_x = new_center.x
|
||||||
|
center_y = new_center.y
|
||||||
|
}
|
||||||
|
|
||||||
|
bhx, bhy := hx, hy
|
||||||
|
if rot_rad != 0 {
|
||||||
|
expanded := rotated_aabb_half(hx, hy, rot_rad)
|
||||||
|
bhx = expanded.x
|
||||||
|
bhy = expanded.y
|
||||||
|
}
|
||||||
|
|
||||||
prim := Primitive {
|
prim := Primitive {
|
||||||
bounds = {rect.x - pad, rect.y - pad, rect.x + rect.w + pad, rect.y + rect.h + pad},
|
bounds = {center_x - bhx - pad, center_y - bhy - pad, center_x + bhx + pad, center_y + bhy + pad},
|
||||||
color = color,
|
color = color,
|
||||||
kind_flags = pack_kind_flags(.RRect, {.Stroke}),
|
kind_flags = pack_kind_flags(.RRect, {.Stroke}),
|
||||||
|
rotation = rot_rad,
|
||||||
}
|
}
|
||||||
prim.params.rrect = RRect_Params {
|
prim.params.rrect = RRect_Params {
|
||||||
half_size = {rect.w * 0.5 * dpi, rect.h * 0.5 * dpi},
|
half_size = {hx * dpi, hy * dpi},
|
||||||
radii = {tr * dpi, br * dpi, tl * dpi, bl * dpi},
|
radii = {tr * dpi, br * dpi, tl * dpi, bl * dpi},
|
||||||
soft_px = soft_px,
|
soft_px = soft_px,
|
||||||
stroke_px = thick * dpi,
|
stroke_px = thick * dpi,
|
||||||
@@ -380,47 +515,34 @@ rectangle_corners_lines :: proc(
|
|||||||
prepare_sdf_primitive(layer, prim)
|
prepare_sdf_primitive(layer, prim)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw a rectangle with uniform corner rounding via SDF.
|
// Draw a filled circle via SDF.
|
||||||
rectangle_rounded :: proc(layer: ^Layer, rect: Rectangle, roundness: f32, color: Color, soft_px: f32 = 1.0) {
|
circle :: proc(
|
||||||
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,
|
layer: ^Layer,
|
||||||
rect: Rectangle,
|
center: [2]f32,
|
||||||
roundness: f32,
|
radius: f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
thick: f32 = 1,
|
origin: [2]f32 = {0, 0},
|
||||||
|
rotation: f32 = 0,
|
||||||
soft_px: f32 = 1.0,
|
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
|
pad := soft_px / GLOB.dpi_scaling
|
||||||
dpi := GLOB.dpi_scaling
|
dpi := GLOB.dpi_scaling
|
||||||
|
|
||||||
|
actual_center := center
|
||||||
|
if origin != {0, 0} {
|
||||||
|
actual_center = compute_pivot_center(center, origin, rotation)
|
||||||
|
}
|
||||||
|
|
||||||
prim := Primitive {
|
prim := Primitive {
|
||||||
bounds = {
|
bounds = {
|
||||||
center.x - radius - pad,
|
actual_center.x - radius - pad,
|
||||||
center.y - radius - pad,
|
actual_center.y - radius - pad,
|
||||||
center.x + radius + pad,
|
actual_center.x + radius + pad,
|
||||||
center.y + radius + pad,
|
actual_center.y + radius + pad,
|
||||||
},
|
},
|
||||||
color = color,
|
color = color,
|
||||||
kind_flags = pack_kind_flags(.Circle, {}),
|
kind_flags = pack_kind_flags(.Circle, {}),
|
||||||
|
// rotation stays 0 — circle is rotationally symmetric
|
||||||
}
|
}
|
||||||
prim.params.circle = Circle_Params {
|
prim.params.circle = Circle_Params {
|
||||||
radius = radius * dpi,
|
radius = radius * dpi,
|
||||||
@@ -436,17 +558,24 @@ circle_lines :: proc(
|
|||||||
radius: f32,
|
radius: f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
thick: f32 = 1,
|
thick: f32 = 1,
|
||||||
|
origin: [2]f32 = {0, 0},
|
||||||
|
rotation: f32 = 0,
|
||||||
soft_px: f32 = 1.0,
|
soft_px: f32 = 1.0,
|
||||||
) {
|
) {
|
||||||
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
|
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
|
||||||
dpi := GLOB.dpi_scaling
|
dpi := GLOB.dpi_scaling
|
||||||
|
|
||||||
|
actual_center := center
|
||||||
|
if origin != {0, 0} {
|
||||||
|
actual_center = compute_pivot_center(center, origin, rotation)
|
||||||
|
}
|
||||||
|
|
||||||
prim := Primitive {
|
prim := Primitive {
|
||||||
bounds = {
|
bounds = {
|
||||||
center.x - radius - pad,
|
actual_center.x - radius - pad,
|
||||||
center.y - radius - pad,
|
actual_center.y - radius - pad,
|
||||||
center.x + radius + pad,
|
actual_center.x + radius + pad,
|
||||||
center.y + radius + pad,
|
actual_center.y + radius + pad,
|
||||||
},
|
},
|
||||||
color = color,
|
color = color,
|
||||||
kind_flags = pack_kind_flags(.Circle, {.Stroke}),
|
kind_flags = pack_kind_flags(.Circle, {.Stroke}),
|
||||||
@@ -460,19 +589,43 @@ circle_lines :: proc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw a filled ellipse via SDF.
|
// 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) {
|
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,
|
||||||
|
) {
|
||||||
pad := soft_px / GLOB.dpi_scaling
|
pad := soft_px / GLOB.dpi_scaling
|
||||||
dpi := GLOB.dpi_scaling
|
dpi := GLOB.dpi_scaling
|
||||||
|
|
||||||
|
actual_center := center
|
||||||
|
rot_rad: f32 = 0
|
||||||
|
if needs_transform(origin, rotation) {
|
||||||
|
actual_center = compute_pivot_center(center, origin, rotation)
|
||||||
|
rot_rad = math.to_radians(rotation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When rotated, expand the bounds AABB to enclose the rotated ellipse
|
||||||
|
bound_h, bound_v := radius_h, radius_v
|
||||||
|
if rot_rad != 0 {
|
||||||
|
expanded := rotated_aabb_half(radius_h, radius_v, rot_rad)
|
||||||
|
bound_h = expanded.x
|
||||||
|
bound_v = expanded.y
|
||||||
|
}
|
||||||
|
|
||||||
prim := Primitive {
|
prim := Primitive {
|
||||||
bounds = {
|
bounds = {
|
||||||
center.x - radius_h - pad,
|
actual_center.x - bound_h - pad,
|
||||||
center.y - radius_v - pad,
|
actual_center.y - bound_v - pad,
|
||||||
center.x + radius_h + pad,
|
actual_center.x + bound_h + pad,
|
||||||
center.y + radius_v + pad,
|
actual_center.y + bound_v + pad,
|
||||||
},
|
},
|
||||||
color = color,
|
color = color,
|
||||||
kind_flags = pack_kind_flags(.Ellipse, {}),
|
kind_flags = pack_kind_flags(.Ellipse, {}),
|
||||||
|
rotation = rot_rad,
|
||||||
}
|
}
|
||||||
prim.params.ellipse = Ellipse_Params {
|
prim.params.ellipse = Ellipse_Params {
|
||||||
radii = {radius_h * dpi, radius_v * dpi},
|
radii = {radius_h * dpi, radius_v * dpi},
|
||||||
@@ -488,22 +641,40 @@ ellipse_lines :: proc(
|
|||||||
radius_h, radius_v: f32,
|
radius_h, radius_v: f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
thick: f32 = 1,
|
thick: f32 = 1,
|
||||||
|
origin: [2]f32 = {0, 0},
|
||||||
|
rotation: f32 = 0,
|
||||||
soft_px: f32 = 1.0,
|
soft_px: f32 = 1.0,
|
||||||
) {
|
) {
|
||||||
// Extra 10% padding: iq's sdEllipse has precision degradation near the tips of highly
|
// 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.
|
// 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
|
extra := max(radius_h, radius_v) * 0.1 + thick * 0.5
|
||||||
|
pad := (extra + soft_px) / GLOB.dpi_scaling
|
||||||
dpi := GLOB.dpi_scaling
|
dpi := GLOB.dpi_scaling
|
||||||
|
|
||||||
|
actual_center := center
|
||||||
|
rot_rad: f32 = 0
|
||||||
|
if needs_transform(origin, rotation) {
|
||||||
|
actual_center = compute_pivot_center(center, origin, rotation)
|
||||||
|
rot_rad = math.to_radians(rotation)
|
||||||
|
}
|
||||||
|
|
||||||
|
bound_h, bound_v := radius_h, radius_v
|
||||||
|
if rot_rad != 0 {
|
||||||
|
expanded := rotated_aabb_half(radius_h, radius_v, rot_rad)
|
||||||
|
bound_h = expanded.x
|
||||||
|
bound_v = expanded.y
|
||||||
|
}
|
||||||
|
|
||||||
prim := Primitive {
|
prim := Primitive {
|
||||||
bounds = {
|
bounds = {
|
||||||
center.x - radius_h - pad,
|
actual_center.x - bound_h - pad,
|
||||||
center.y - radius_v - pad,
|
actual_center.y - bound_v - pad,
|
||||||
center.x + radius_h + pad,
|
actual_center.x + bound_h + pad,
|
||||||
center.y + radius_v + pad,
|
actual_center.y + bound_v + pad,
|
||||||
},
|
},
|
||||||
color = color,
|
color = color,
|
||||||
kind_flags = pack_kind_flags(.Ellipse, {.Stroke}),
|
kind_flags = pack_kind_flags(.Ellipse, {.Stroke}),
|
||||||
|
rotation = rot_rad,
|
||||||
}
|
}
|
||||||
prim.params.ellipse = Ellipse_Params {
|
prim.params.ellipse = Ellipse_Params {
|
||||||
radii = {radius_h * dpi, radius_v * dpi},
|
radii = {radius_h * dpi, radius_v * dpi},
|
||||||
@@ -520,26 +691,36 @@ ring :: proc(
|
|||||||
inner_radius, outer_radius: f32,
|
inner_radius, outer_radius: f32,
|
||||||
start_angle, end_angle: f32,
|
start_angle, end_angle: f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
|
origin: [2]f32 = {0, 0},
|
||||||
|
rotation: f32 = 0,
|
||||||
soft_px: f32 = 1.0,
|
soft_px: f32 = 1.0,
|
||||||
) {
|
) {
|
||||||
pad := soft_px / GLOB.dpi_scaling
|
pad := soft_px / GLOB.dpi_scaling
|
||||||
dpi := GLOB.dpi_scaling
|
dpi := 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 {
|
prim := Primitive {
|
||||||
bounds = {
|
bounds = {
|
||||||
center.x - outer_radius - pad,
|
actual_center.x - outer_radius - pad,
|
||||||
center.y - outer_radius - pad,
|
actual_center.y - outer_radius - pad,
|
||||||
center.x + outer_radius + pad,
|
actual_center.x + outer_radius + pad,
|
||||||
center.y + outer_radius + pad,
|
actual_center.y + outer_radius + pad,
|
||||||
},
|
},
|
||||||
color = color,
|
color = color,
|
||||||
kind_flags = pack_kind_flags(.Ring_Arc, {}),
|
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 {
|
prim.params.ring_arc = Ring_Arc_Params {
|
||||||
inner_radius = inner_radius * dpi,
|
inner_radius = inner_radius * dpi,
|
||||||
outer_radius = outer_radius * dpi,
|
outer_radius = outer_radius * dpi,
|
||||||
start_rad = math.to_radians(start_angle),
|
start_rad = math.to_radians(start_angle) + rotation_offset,
|
||||||
end_rad = math.to_radians(end_angle),
|
end_rad = math.to_radians(end_angle) + rotation_offset,
|
||||||
soft_px = soft_px,
|
soft_px = soft_px,
|
||||||
}
|
}
|
||||||
prepare_sdf_primitive(layer, prim)
|
prepare_sdf_primitive(layer, prim)
|
||||||
@@ -553,39 +734,50 @@ ring_lines :: proc(
|
|||||||
start_angle, end_angle: f32,
|
start_angle, end_angle: f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
thick: f32 = 1,
|
thick: f32 = 1,
|
||||||
|
origin: [2]f32 = {0, 0},
|
||||||
|
rotation: f32 = 0,
|
||||||
soft_px: f32 = 1.0,
|
soft_px: f32 = 1.0,
|
||||||
) {
|
) {
|
||||||
// Inner arc outline
|
// Compute effective angles and pivot-translated center up front
|
||||||
|
eff_start := start_angle + rotation
|
||||||
|
eff_end := 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(
|
ring(
|
||||||
layer,
|
layer,
|
||||||
center,
|
actual_center,
|
||||||
max(0, inner_radius - thick * 0.5),
|
max(0, inner_radius - thick * 0.5),
|
||||||
inner_radius + thick * 0.5,
|
inner_radius + thick * 0.5,
|
||||||
start_angle,
|
eff_start,
|
||||||
end_angle,
|
eff_end,
|
||||||
color,
|
color,
|
||||||
soft_px,
|
soft_px = soft_px,
|
||||||
)
|
)
|
||||||
// Outer arc outline
|
// Outer arc outline
|
||||||
ring(
|
ring(
|
||||||
layer,
|
layer,
|
||||||
center,
|
actual_center,
|
||||||
max(0, outer_radius - thick * 0.5),
|
max(0, outer_radius - thick * 0.5),
|
||||||
outer_radius + thick * 0.5,
|
outer_radius + thick * 0.5,
|
||||||
start_angle,
|
eff_start,
|
||||||
end_angle,
|
eff_end,
|
||||||
color,
|
color,
|
||||||
soft_px,
|
soft_px = soft_px,
|
||||||
)
|
)
|
||||||
// Start cap
|
// Start cap
|
||||||
start_rad := math.to_radians(start_angle)
|
start_rad := math.to_radians(eff_start)
|
||||||
end_rad := math.to_radians(end_angle)
|
end_rad := math.to_radians(eff_end)
|
||||||
inner_start := center + {math.cos(start_rad) * inner_radius, math.sin(start_rad) * inner_radius}
|
inner_start := actual_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}
|
outer_start := actual_center + {math.cos(start_rad) * outer_radius, math.sin(start_rad) * outer_radius}
|
||||||
line(layer, inner_start, outer_start, color, thick, soft_px)
|
line(layer, inner_start, outer_start, color, thick, soft_px)
|
||||||
// End cap
|
// End cap
|
||||||
inner_end := center + {math.cos(end_rad) * inner_radius, math.sin(end_rad) * inner_radius}
|
inner_end := actual_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}
|
outer_end := actual_center + {math.cos(end_rad) * outer_radius, math.sin(end_rad) * outer_radius}
|
||||||
line(layer, inner_end, outer_end, color, thick, soft_px)
|
line(layer, inner_end, outer_end, color, thick, soft_px)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,18 +824,24 @@ poly :: proc(
|
|||||||
radius: f32,
|
radius: f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
rotation: f32 = 0,
|
rotation: f32 = 0,
|
||||||
|
origin: [2]f32 = {0, 0},
|
||||||
soft_px: f32 = 1.0,
|
soft_px: f32 = 1.0,
|
||||||
) {
|
) {
|
||||||
if sides < 3 do return
|
if sides < 3 do return
|
||||||
pad := soft_px / GLOB.dpi_scaling
|
pad := soft_px / GLOB.dpi_scaling
|
||||||
dpi := GLOB.dpi_scaling
|
dpi := GLOB.dpi_scaling
|
||||||
|
|
||||||
|
actual_center := center
|
||||||
|
if origin != {0, 0} && rotation != 0 {
|
||||||
|
actual_center = compute_pivot_center(center, origin, rotation)
|
||||||
|
}
|
||||||
|
|
||||||
prim := Primitive {
|
prim := Primitive {
|
||||||
bounds = {
|
bounds = {
|
||||||
center.x - radius - pad,
|
actual_center.x - radius - pad,
|
||||||
center.y - radius - pad,
|
actual_center.y - radius - pad,
|
||||||
center.x + radius + pad,
|
actual_center.x + radius + pad,
|
||||||
center.y + radius + pad,
|
actual_center.y + radius + pad,
|
||||||
},
|
},
|
||||||
color = color,
|
color = color,
|
||||||
kind_flags = pack_kind_flags(.NGon, {}),
|
kind_flags = pack_kind_flags(.NGon, {}),
|
||||||
@@ -665,6 +863,7 @@ poly_lines :: proc(
|
|||||||
radius: f32,
|
radius: f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
rotation: f32 = 0,
|
rotation: f32 = 0,
|
||||||
|
origin: [2]f32 = {0, 0},
|
||||||
thick: f32 = 1,
|
thick: f32 = 1,
|
||||||
soft_px: f32 = 1.0,
|
soft_px: f32 = 1.0,
|
||||||
) {
|
) {
|
||||||
@@ -672,12 +871,17 @@ poly_lines :: proc(
|
|||||||
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
|
pad := (thick * 0.5 + soft_px) / GLOB.dpi_scaling
|
||||||
dpi := GLOB.dpi_scaling
|
dpi := GLOB.dpi_scaling
|
||||||
|
|
||||||
|
actual_center := center
|
||||||
|
if origin != {0, 0} && rotation != 0 {
|
||||||
|
actual_center = compute_pivot_center(center, origin, rotation)
|
||||||
|
}
|
||||||
|
|
||||||
prim := Primitive {
|
prim := Primitive {
|
||||||
bounds = {
|
bounds = {
|
||||||
center.x - radius - pad,
|
actual_center.x - radius - pad,
|
||||||
center.y - radius - pad,
|
actual_center.y - radius - pad,
|
||||||
center.x + radius + pad,
|
actual_center.x + radius + pad,
|
||||||
center.y + radius + pad,
|
actual_center.y + radius + pad,
|
||||||
},
|
},
|
||||||
color = color,
|
color = color,
|
||||||
kind_flags = pack_kind_flags(.NGon, {.Stroke}),
|
kind_flags = pack_kind_flags(.NGon, {.Stroke}),
|
||||||
@@ -691,3 +895,103 @@ poly_lines :: proc(
|
|||||||
}
|
}
|
||||||
prepare_sdf_primitive(layer, prim)
|
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 rect's top-left) --------------------------------------------------
|
||||||
|
|
||||||
|
center_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
|
||||||
|
return {r.w * 0.5, r.h * 0.5}
|
||||||
|
}
|
||||||
|
|
||||||
|
top_left_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
|
||||||
|
return {0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
top_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
|
||||||
|
return {r.w * 0.5, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
top_right_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
|
||||||
|
return {r.w, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
left_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
|
||||||
|
return {0, r.h * 0.5}
|
||||||
|
}
|
||||||
|
|
||||||
|
right_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
|
||||||
|
return {r.w, r.h * 0.5}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_left_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
|
||||||
|
return {0, r.h}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
|
||||||
|
return {r.w * 0.5, r.h}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_right_of_rect :: #force_inline proc(r: Rectangle) -> [2]f32 {
|
||||||
|
return {r.w, r.h}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Triangle anchors (origin measured from AABB top-left) -----------------------------------------------------
|
||||||
|
|
||||||
|
center_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
|
||||||
|
mn := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||||||
|
return (v1 + v2 + v3) / 3 - mn
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
mn_x := min(v1.x, v2.x, v3.x)
|
||||||
|
mx_x := max(v1.x, v2.x, v3.x)
|
||||||
|
return {(mx_x - mn_x) * 0.5, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
top_right_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
|
||||||
|
mn_x := min(v1.x, v2.x, v3.x)
|
||||||
|
mx_x := max(v1.x, v2.x, v3.x)
|
||||||
|
return {mx_x - mn_x, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
left_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
|
||||||
|
mn_y := min(v1.y, v2.y, v3.y)
|
||||||
|
mx_y := max(v1.y, v2.y, v3.y)
|
||||||
|
return {0, (mx_y - mn_y) * 0.5}
|
||||||
|
}
|
||||||
|
|
||||||
|
right_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
|
||||||
|
mn := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||||||
|
mx := [2]f32{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
|
||||||
|
return {mx.x - mn.x, (mx.y - mn.y) * 0.5}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_left_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
|
||||||
|
mn_y := min(v1.y, v2.y, v3.y)
|
||||||
|
mx_y := max(v1.y, v2.y, v3.y)
|
||||||
|
return {0, mx_y - mn_y}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
|
||||||
|
mn := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||||||
|
mx := [2]f32{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
|
||||||
|
return {(mx.x - mn.x) * 0.5, mx.y - mn.y}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_right_of_triangle :: #force_inline proc(v1, v2, v3: [2]f32) -> [2]f32 {
|
||||||
|
mn := [2]f32{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||||||
|
mx := [2]f32{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
|
||||||
|
return mx - mn
|
||||||
|
}
|
||||||
|
|||||||
201
draw/text.odin
201
draw/text.odin
@@ -1,6 +1,9 @@
|
|||||||
package draw
|
package draw
|
||||||
|
|
||||||
|
import "core:c"
|
||||||
|
import "core:hash"
|
||||||
import "core:log"
|
import "core:log"
|
||||||
|
import "core:strings"
|
||||||
import sdl "vendor:sdl3"
|
import sdl "vendor:sdl3"
|
||||||
import sdl_ttf "vendor:sdl3/ttf"
|
import sdl_ttf "vendor:sdl3/ttf"
|
||||||
|
|
||||||
@@ -71,32 +74,195 @@ Text :: struct {
|
|||||||
color: Color,
|
color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
text :: proc(
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
id: u32,
|
// ----- Text cache hashing ------------
|
||||||
txt: cstring,
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
pos: [2]f32,
|
|
||||||
font_id: Font_Id,
|
// Hash a string to a u32 cache key using the same Jenkins one-at-a-time algorithm as Clay.
|
||||||
font_size: u16 = 44,
|
// This means Clay element IDs and user-chosen string IDs share the same keyspace — same
|
||||||
color: Color = {0, 0, 0, 255},
|
// string produces the same cache key regardless of whether it came from Clay or user code.
|
||||||
) -> Text {
|
text_cache_hash :: #force_inline proc(s: string) -> u32 {
|
||||||
sdl_text := GLOB.text_cache.cache[id]
|
return hash.jenkins(transmute([]u8)s) + 1 // +1 reserves 0 as "no entry" (matches Clay convention)
|
||||||
if sdl_text == nil {
|
}
|
||||||
sdl_text = sdl_ttf.CreateText(GLOB.text_cache.engine, get_font(font_id, font_size), txt, 0)
|
|
||||||
|
// Shared cache lookup/create/update logic used by both the `text` proc and the Clay render path.
|
||||||
|
// Returns the cached (or newly created) TTF_Text pointer.
|
||||||
|
@(private)
|
||||||
|
cache_get_or_update :: proc(cache_id: u32, c_str: cstring, font: ^sdl_ttf.Font) -> ^sdl_ttf.Text {
|
||||||
|
existing, found := GLOB.text_cache.cache[cache_id]
|
||||||
|
if !found {
|
||||||
|
sdl_text := sdl_ttf.CreateText(GLOB.text_cache.engine, font, c_str, 0)
|
||||||
if sdl_text == nil {
|
if sdl_text == nil {
|
||||||
log.panicf("Failed to create SDL text: %s", sdl.GetError())
|
log.panicf("Failed to create SDL text: %s", sdl.GetError())
|
||||||
}
|
}
|
||||||
GLOB.text_cache.cache[id] = sdl_text
|
GLOB.text_cache.cache[cache_id] = sdl_text
|
||||||
|
return sdl_text
|
||||||
} else {
|
} else {
|
||||||
//TODO if IDs are always unique and never change the underlying text
|
if !sdl_ttf.SetTextString(existing, c_str, 0) {
|
||||||
// can get rid of this
|
|
||||||
if !sdl_ttf.SetTextString(sdl_text, txt, 0) {
|
|
||||||
log.panicf("Failed to update SDL text string: %s", sdl.GetError())
|
log.panicf("Failed to update SDL text string: %s", sdl.GetError())
|
||||||
}
|
}
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Text drawing ------------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Draw text at a position with optional rotation and origin.
|
||||||
|
//
|
||||||
|
// When `id` is nil (the default), the text is created and destroyed each frame — simple and
|
||||||
|
// leak-free, appropriate for HUDs and moderate UI (up to ~50 text elements per frame).
|
||||||
|
//
|
||||||
|
// When `id` is set, the TTF_Text object is cached across frames using a hash of the provided
|
||||||
|
// string (same algorithm as Clay's element IDs). This avoids per-frame HarfBuzz shaping and
|
||||||
|
// allocation, which matters for text-heavy apps (editors, terminals, chat). The user is
|
||||||
|
// responsible for choosing unique ID strings per logical text element and calling
|
||||||
|
// `clear_text_cache` or `clear_text_cache_entry` when cached entries are no longer needed.
|
||||||
|
//
|
||||||
|
// `origin` is in pixels from the text block's top-left corner (raylib convention).
|
||||||
|
// The point whose local coords equal `origin` lands at `pos` in world space.
|
||||||
|
// `rotation` is in degrees, counter-clockwise.
|
||||||
|
text :: proc(
|
||||||
|
layer: ^Layer,
|
||||||
|
str: string,
|
||||||
|
pos: [2]f32,
|
||||||
|
font_id: Font_Id,
|
||||||
|
font_size: u16 = 44,
|
||||||
|
color: Color = BLACK,
|
||||||
|
origin: [2]f32 = {0, 0},
|
||||||
|
rotation: f32 = 0,
|
||||||
|
id: Maybe(string) = nil,
|
||||||
|
temp_allocator := context.temp_allocator,
|
||||||
|
) {
|
||||||
|
c_str := strings.clone_to_cstring(str, temp_allocator)
|
||||||
|
|
||||||
|
sdl_text: ^sdl_ttf.Text
|
||||||
|
cached := false
|
||||||
|
|
||||||
|
if id_str, ok := id.?; ok {
|
||||||
|
cached = true
|
||||||
|
sdl_text = cache_get_or_update(text_cache_hash(id_str), c_str, get_font(font_id, font_size))
|
||||||
|
} else {
|
||||||
|
sdl_text = sdl_ttf.CreateText(GLOB.text_cache.engine, get_font(font_id, font_size), c_str, 0)
|
||||||
|
if sdl_text == nil {
|
||||||
|
log.panicf("Failed to create SDL text: %s", sdl.GetError())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Text{sdl_text, pos, color}
|
if needs_transform(origin, rotation) {
|
||||||
|
dpi := GLOB.dpi_scaling
|
||||||
|
xform := build_pivot_rot(pos * dpi, origin * dpi, rotation)
|
||||||
|
prepare_text_transformed(layer, Text{sdl_text, {0, 0}, color}, xform)
|
||||||
|
} else {
|
||||||
|
prepare_text(layer, Text{sdl_text, pos, color})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cached {
|
||||||
|
// Don't destroy now — the draw data (atlas texture, vertices) is still referenced
|
||||||
|
// by the batch buffers until end() submits to the GPU. Deferred to clear_global().
|
||||||
|
append(&GLOB.tmp_uncached_text, sdl_text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Public text measurement -------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Measure a string in logical pixels (pre-DPI-scaling) using the same font backend as the renderer.
|
||||||
|
measure_text :: proc(
|
||||||
|
str: string,
|
||||||
|
font_id: Font_Id,
|
||||||
|
font_size: u16 = 44,
|
||||||
|
allocator := context.temp_allocator,
|
||||||
|
) -> [2]f32 {
|
||||||
|
c_str := strings.clone_to_cstring(str, allocator)
|
||||||
|
w, h: c.int
|
||||||
|
if !sdl_ttf.GetStringSize(get_font(font_id, font_size), c_str, 0, &w, &h) {
|
||||||
|
log.panicf("Failed to measure text: %s", sdl.GetError())
|
||||||
|
}
|
||||||
|
return {f32(w) / GLOB.dpi_scaling, f32(h) / GLOB.dpi_scaling}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Text anchor helpers -----------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
center_of_text :: proc(str: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
|
||||||
|
size := measure_text(str, font_id, font_size)
|
||||||
|
return size * 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
top_left_of_text :: proc(str: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
|
||||||
|
return {0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
top_of_text :: proc(str: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
|
||||||
|
size := measure_text(str, font_id, font_size)
|
||||||
|
return {size.x * 0.5, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
top_right_of_text :: proc(str: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
|
||||||
|
size := measure_text(str, font_id, font_size)
|
||||||
|
return {size.x, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
left_of_text :: proc(str: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
|
||||||
|
size := measure_text(str, font_id, font_size)
|
||||||
|
return {0, size.y * 0.5}
|
||||||
|
}
|
||||||
|
|
||||||
|
right_of_text :: proc(str: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
|
||||||
|
size := measure_text(str, font_id, font_size)
|
||||||
|
return {size.x, size.y * 0.5}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_left_of_text :: proc(str: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
|
||||||
|
size := measure_text(str, font_id, font_size)
|
||||||
|
return {0, size.y}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_of_text :: proc(str: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
|
||||||
|
size := measure_text(str, font_id, font_size)
|
||||||
|
return {size.x * 0.5, size.y}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_right_of_text :: proc(str: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
|
||||||
|
size := measure_text(str, font_id, font_size)
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Cache management --------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Destroy all cached text objects. Call on scene transitions, view changes, or periodically
|
||||||
|
// in apps that produce many distinct cached text entries over time.
|
||||||
|
// After calling this, subsequent text draws with an `id` will re-create their cache entries.
|
||||||
|
clear_text_cache :: proc() {
|
||||||
|
for _, sdl_text in GLOB.text_cache.cache {
|
||||||
|
sdl_ttf.DestroyText(sdl_text)
|
||||||
|
}
|
||||||
|
clear(&GLOB.text_cache.cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy a specific cached text entry by its string id (same string used when drawing).
|
||||||
|
// Uses the same hash as Clay's element IDs, so this also works for clearing Clay text entries
|
||||||
|
// by passing the same string used in clay.ID("...").
|
||||||
|
// No-op if the id is not in the cache.
|
||||||
|
clear_text_cache_entry :: proc(id: string) {
|
||||||
|
key := text_cache_hash(id)
|
||||||
|
sdl_text, ok := GLOB.text_cache.cache[key]
|
||||||
|
if ok {
|
||||||
|
sdl_ttf.DestroyText(sdl_text)
|
||||||
|
delete_key(&GLOB.text_cache.cache, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Internal cache lifecycle ------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
@(private, require_results)
|
@(private, require_results)
|
||||||
init_text_cache :: proc(
|
init_text_cache :: proc(
|
||||||
device: ^sdl.GPUDevice,
|
device: ^sdl.GPUDevice,
|
||||||
@@ -132,6 +298,9 @@ destroy_text_cache :: proc() {
|
|||||||
for _, font in GLOB.text_cache.sdl_fonts {
|
for _, font in GLOB.text_cache.sdl_fonts {
|
||||||
sdl_ttf.CloseFont(font)
|
sdl_ttf.CloseFont(font)
|
||||||
}
|
}
|
||||||
|
for _, sdl_text in GLOB.text_cache.cache {
|
||||||
|
sdl_ttf.DestroyText(sdl_text)
|
||||||
|
}
|
||||||
delete(GLOB.text_cache.sdl_fonts)
|
delete(GLOB.text_cache.sdl_fonts)
|
||||||
delete(GLOB.text_cache.font_bytes)
|
delete(GLOB.text_cache.font_bytes)
|
||||||
delete(GLOB.text_cache.cache)
|
delete(GLOB.text_cache.cache)
|
||||||
|
|||||||
Reference in New Issue
Block a user