shape2d
This commit is contained in:
@@ -203,6 +203,8 @@ film2d.render = function(params, backend) {
|
||||
commands.push({cmd: 'draw_text', drawable: batch.drawable})
|
||||
else if (batch.type == 'texture_ref')
|
||||
commands.push({cmd: 'draw_texture_ref', drawable: batch.drawable})
|
||||
else if (batch.type == 'shape')
|
||||
commands.push({cmd: 'draw_shape', drawable: batch.drawable})
|
||||
}
|
||||
|
||||
commands.push({cmd: 'end_render'})
|
||||
@@ -301,6 +303,13 @@ function _batch_drawables(drawables) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (d.type == 'shape') {
|
||||
// Shapes are rendered individually (each has unique SDF params)
|
||||
if (current) {
|
||||
batches.push(current)
|
||||
current = null
|
||||
}
|
||||
batches.push({type: 'shape', drawable: d})
|
||||
} else {
|
||||
if (current) {
|
||||
batches.push(current)
|
||||
|
||||
204
sdl_gpu.cm
204
sdl_gpu.cm
@@ -36,6 +36,7 @@ var _crt_frag = null
|
||||
var _accumulator_frag = null
|
||||
var _text_sdf_frag = null
|
||||
var _text_msdf_frag = null
|
||||
var _shape2d_frag = null
|
||||
|
||||
// Pipelines
|
||||
var _pipelines = {}
|
||||
@@ -259,6 +260,18 @@ function _load_shaders() {
|
||||
})
|
||||
}
|
||||
|
||||
var shape2d_frag_code = io.slurp("shaders/msl/shape2d.frag.msl")
|
||||
if (shape2d_frag_code) {
|
||||
_shape2d_frag = new gpu_mod.shader(_gpu, {
|
||||
code: shape2d_frag_code,
|
||||
stage: "fragment",
|
||||
format: "msl",
|
||||
entrypoint: "fragment_main",
|
||||
num_uniform_buffers: 1,
|
||||
num_samplers: 0
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -582,6 +595,7 @@ function _create_pipelines() {
|
||||
}]
|
||||
}
|
||||
})
|
||||
}
|
||||
// Accumulator pipeline
|
||||
if (_blit_vert && _accumulator_frag) {
|
||||
_pipelines.accumulator = new gpu_mod.graphics_pipeline(_gpu, {
|
||||
@@ -604,7 +618,43 @@ function _create_pipelines() {
|
||||
color_targets: [{format: _swapchain_format, blend: {enabled: false}}]
|
||||
}
|
||||
})
|
||||
} }
|
||||
}
|
||||
|
||||
// Shape2D pipeline
|
||||
if (_sprite_vert && _shape2d_frag) {
|
||||
_pipelines.shape2d = new gpu_mod.graphics_pipeline(_gpu, {
|
||||
vertex: _sprite_vert,
|
||||
fragment: _shape2d_frag,
|
||||
primitive: "triangle",
|
||||
cull: "none",
|
||||
face: "counter_clockwise",
|
||||
fill: "fill",
|
||||
vertex_buffer_descriptions: [{
|
||||
slot: 0,
|
||||
pitch: 32,
|
||||
input_rate: "vertex"
|
||||
}],
|
||||
vertex_attributes: [
|
||||
{location: 0, buffer_slot: 0, format: "float2", offset: 0},
|
||||
{location: 1, buffer_slot: 0, format: "float2", offset: 8},
|
||||
{location: 2, buffer_slot: 0, format: "float4", offset: 16}
|
||||
],
|
||||
target: {
|
||||
color_targets: [{
|
||||
format: _swapchain_format,
|
||||
blend: {
|
||||
enabled: true,
|
||||
src_rgb: "src_alpha",
|
||||
dst_rgb: "one_minus_src_alpha",
|
||||
op_rgb: "add",
|
||||
src_alpha: "one",
|
||||
dst_alpha: "one_minus_src_alpha",
|
||||
op_alpha: "add"
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -1155,6 +1205,10 @@ function _execute_commands(commands, window_size) {
|
||||
pending_draws.push(cmd)
|
||||
break
|
||||
|
||||
case 'draw_shape':
|
||||
pending_draws.push(cmd)
|
||||
break
|
||||
|
||||
case 'blit':
|
||||
// Flush pending draws first
|
||||
if (current_pass && pending_draws.length > 0) {
|
||||
@@ -1357,6 +1411,13 @@ function _flush_draws(cmd_buffer, pass, draws, camera, target) {
|
||||
|
||||
// Render pre-rendered effect texture
|
||||
_render_texture_ref(cmd_buffer, pass, draw.drawable, camera, target)
|
||||
} else if (draw.cmd == 'draw_shape') {
|
||||
// Flush current batch
|
||||
if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target)
|
||||
current_batch = null
|
||||
|
||||
// Render shape immediately
|
||||
_render_shape(cmd_buffer, pass, draw.drawable, camera, target)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1472,6 +1533,147 @@ function _render_texture_ref(cmd_buffer, pass, drawable, camera, target) {
|
||||
pass.draw_indexed(geom.index_count, 1, 0, 0, 0)
|
||||
}
|
||||
|
||||
function _render_shape(cmd_buffer, pass, drawable, camera, target) {
|
||||
if (!_pipelines.shape2d) return
|
||||
|
||||
var pos = drawable.pos || {x: 0, y: 0}
|
||||
var w = drawable.width || 100
|
||||
var h = drawable.height || 100
|
||||
var ax = drawable.anchor_x != null ? drawable.anchor_x : 0.5
|
||||
var ay = drawable.anchor_y != null ? drawable.anchor_y : 0.5
|
||||
|
||||
// Calculate padding required for stroke and feather
|
||||
var stroke_thickness = drawable.stroke_thickness || 0
|
||||
var feather = drawable.feather != null ? drawable.feather : 0.5
|
||||
|
||||
// Resolve stroke alignment (0=inside, 0.5=center, 1=outside)
|
||||
var stroke_aligns = {inside: 0, center: 0.5, outside: 1}
|
||||
var sa = drawable.stroke_align in stroke_aligns ? stroke_aligns[drawable.stroke_align] : 0.5
|
||||
|
||||
var pad = feather
|
||||
if (stroke_thickness > 0) {
|
||||
if (sa > 0.75) pad += stroke_thickness // Outside
|
||||
else if (sa > 0.25) pad += stroke_thickness * 0.5 // Center
|
||||
// Inside adds 0
|
||||
}
|
||||
|
||||
// Expand quad by padding
|
||||
var x = pos.x - w * ax - pad
|
||||
var y = pos.y - h * ay - pad
|
||||
var qw = w + pad * 2
|
||||
var qh = h + pad * 2
|
||||
|
||||
// Expand UVs to match padding (p calculation depends on this)
|
||||
// logical size is w, h. padded size is qw, qh.
|
||||
// 0..1 maps to w, h.
|
||||
// We need UVs such that (uv - 0.5) * w spans the padded area logic.
|
||||
// u=0 -> -0.5 * w = left edge of shape
|
||||
// target left edge is -pad relative to shape left edge.
|
||||
// So we need uv such that (uv - 0.5) * w = -w/2 - pad
|
||||
// uv * w - w/2 = -w/2 - pad
|
||||
// uv * w = -pad => uv = -pad / w
|
||||
var u0 = -pad / w
|
||||
var v0 = -pad / h
|
||||
var u1 = 1.0 + pad / w
|
||||
var v1 = 1.0 + pad / h
|
||||
|
||||
var fill = drawable.fill || {r: 1, g: 1, b: 1, a: 1}
|
||||
var opacity = drawable.opacity != null ? drawable.opacity : 1
|
||||
|
||||
// Vertex data: pos(2) + uv(2) + color(4) = 8 floats = 32 bytes per vertex
|
||||
var vertex_data = new blob_mod(4 * 32)
|
||||
var index_data = new blob_mod(6 * 2)
|
||||
|
||||
// v0: bottom-left
|
||||
vertex_data.wf(x); vertex_data.wf(y)
|
||||
vertex_data.wf(u0); vertex_data.wf(v1)
|
||||
vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1)
|
||||
|
||||
// v1: bottom-right
|
||||
vertex_data.wf(x + qw); vertex_data.wf(y)
|
||||
vertex_data.wf(u1); vertex_data.wf(v1)
|
||||
vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1)
|
||||
|
||||
// v2: top-right
|
||||
vertex_data.wf(x + qw); vertex_data.wf(y + qh)
|
||||
vertex_data.wf(u1); vertex_data.wf(v0)
|
||||
vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1)
|
||||
|
||||
// v3: top-left
|
||||
vertex_data.wf(x); vertex_data.wf(y + qh)
|
||||
vertex_data.wf(u0); vertex_data.wf(v0)
|
||||
vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1)
|
||||
|
||||
// Indices
|
||||
index_data.w16(0); index_data.w16(1); index_data.w16(2)
|
||||
index_data.w16(0); index_data.w16(2); index_data.w16(3)
|
||||
|
||||
// Upload geometry
|
||||
var vb_size = 4 * 32
|
||||
var ib_size = 6 * 2
|
||||
|
||||
var vb = new gpu_mod.buffer(_gpu, {size: vb_size, vertex: true})
|
||||
var ib = new gpu_mod.buffer(_gpu, {size: ib_size, index: true})
|
||||
|
||||
var vb_transfer = new gpu_mod.transfer_buffer(_gpu, {size: vb_size, usage: "upload"})
|
||||
var ib_transfer = new gpu_mod.transfer_buffer(_gpu, {size: ib_size, usage: "upload"})
|
||||
|
||||
vb_transfer.copy_blob(_gpu, stone(vertex_data))
|
||||
ib_transfer.copy_blob(_gpu, stone(index_data))
|
||||
|
||||
var copy_cmd = _gpu.acquire_cmd_buffer()
|
||||
var copy = copy_cmd.copy_pass()
|
||||
copy.upload_to_buffer({transfer_buffer: vb_transfer, offset: 0}, {buffer: vb, offset: 0, size: vb_size}, false)
|
||||
copy.upload_to_buffer({transfer_buffer: ib_transfer, offset: 0}, {buffer: ib, offset: 0, size: ib_size}, false)
|
||||
copy.end()
|
||||
copy_cmd.submit()
|
||||
|
||||
// Build camera matrix
|
||||
var proj = _build_camera_matrix(camera, target.width, target.height)
|
||||
|
||||
// Build shape uniforms - must match ShapeParams struct in shader
|
||||
// Total size: 112 bytes
|
||||
var stroke = drawable.stroke || {r: 0, g: 0, b: 0, a: 0}
|
||||
var shape_types = {rect: 0, circle: 1, ellipse: 2, pill: 3}
|
||||
var shape_type = shape_types[drawable.shape_type] || 0
|
||||
|
||||
var u_data = new blob_mod(112)
|
||||
u_data.wf(fill.r); u_data.wf(fill.g); u_data.wf(fill.b); u_data.wf(fill.a) // fill_color (16)
|
||||
u_data.wf(stroke.r); u_data.wf(stroke.g); u_data.wf(stroke.b); u_data.wf(stroke.a) // stroke_color (16)
|
||||
u_data.wf(w); u_data.wf(h) // size (8) - PASS LOGICAL SIZE
|
||||
u_data.wf(drawable.radius || 0) // radius (4)
|
||||
u_data.wf(feather) // feather (4)
|
||||
u_data.wf(stroke_thickness) // stroke_thickness (4)
|
||||
u_data.wf(sa) // stroke_align (4)
|
||||
u_data.w32(shape_type) // shape_type (4)
|
||||
u_data.w32(0) // corner_style (4)
|
||||
u_data.wf(drawable.dash_len || 0) // dash_len (4)
|
||||
u_data.wf(drawable.gap_len || 0) // gap_len (4)
|
||||
u_data.wf(drawable.dash_offset || 0) // dash_offset (4)
|
||||
u_data.w32(0) // cap_type (4)
|
||||
|
||||
// uv_transform (16)
|
||||
if (drawable.uv && drawable.uv.scale) {
|
||||
u_data.wf(drawable.uv.scale.x); u_data.wf(drawable.uv.scale.y)
|
||||
u_data.wf(drawable.uv.offset.x); u_data.wf(drawable.uv.offset.y)
|
||||
} else {
|
||||
u_data.wf(1); u_data.wf(1); u_data.wf(0); u_data.wf(0)
|
||||
}
|
||||
|
||||
u_data.wf(drawable.uv && drawable.uv.rotate ? drawable.uv.rotate : 0) // uv_rotate (4)
|
||||
u_data.w32(0) // has_texture (4)
|
||||
u_data.wf(opacity) // opacity (4)
|
||||
u_data.wf(0) // _pad (4)
|
||||
|
||||
pass.bind_pipeline(_pipelines.shape2d)
|
||||
pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}])
|
||||
pass.bind_index_buffer({buffer: ib, offset: 0}, 16)
|
||||
pass.bind_fragment_samplers(0, [{texture: _white_texture, sampler: _sampler_linear}])
|
||||
cmd_buffer.push_vertex_uniform_data(0, proj)
|
||||
cmd_buffer.push_fragment_uniform_data(0, stone(u_data))
|
||||
pass.draw_indexed(6, 1, 0, 0, 0)
|
||||
}
|
||||
|
||||
function _render_text(cmd_buffer, pass, drawable, camera, target) {
|
||||
// Get font - support mode tag: 'bitmap', 'sdf', 'msdf'
|
||||
var font_path = drawable.font
|
||||
|
||||
472
shaders/msl/shape2d.frag.msl
Normal file
472
shaders/msl/shape2d.frag.msl
Normal file
@@ -0,0 +1,472 @@
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
#define M_PI_F 3.14159265358979323846f
|
||||
|
||||
struct VertexOut {
|
||||
float4 position [[position]];
|
||||
float2 uv;
|
||||
float4 color;
|
||||
};
|
||||
|
||||
struct ShapeParams {
|
||||
float4 fill_color;
|
||||
float4 stroke_color;
|
||||
float2 size;
|
||||
float radius;
|
||||
float feather;
|
||||
float stroke_thickness;
|
||||
float stroke_align;
|
||||
int shape_type; // 0=rect (rounded via radius), 1=circle, 2=ellipse, 3=pill
|
||||
int corner_style;
|
||||
float dash_len;
|
||||
float gap_len;
|
||||
float dash_offset;
|
||||
int cap_type; // 0=butt, 1=square-ish, 2=round-ish
|
||||
float4 uv_transform;
|
||||
float uv_rotate;
|
||||
int has_texture;
|
||||
float opacity;
|
||||
float _pad;
|
||||
};
|
||||
|
||||
// -------- SDFs --------
|
||||
|
||||
float sdf_rounded_rect(float2 p, float2 b, float r) {
|
||||
float2 q = abs(p) - b + r;
|
||||
return min(max(q.x, q.y), 0.0f) + length(max(q, 0.0f)) - r;
|
||||
}
|
||||
|
||||
float sdf_circle(float2 p, float r) {
|
||||
return length(p) - r;
|
||||
}
|
||||
|
||||
float sdf_ellipse(float2 p, float2 ab) {
|
||||
float2 pn = p / ab;
|
||||
float d = length(pn) - 1.0f;
|
||||
return d * min(ab.x, ab.y);
|
||||
}
|
||||
|
||||
float sdf_pill(float2 p, float2 b) {
|
||||
float r = min(b.x, b.y);
|
||||
float2 q = abs(p) - float2(b.x - r, b.y - r);
|
||||
return length(max(q, 0.0f)) + min(max(q.x, q.y), 0.0f) - r;
|
||||
}
|
||||
|
||||
float compute_sdf(float2 p, float2 half_size, int shape_type, float radius) {
|
||||
if (shape_type == 0) {
|
||||
float r = min(radius, min(half_size.x, half_size.y));
|
||||
return sdf_rounded_rect(p, half_size, r);
|
||||
}
|
||||
else if (shape_type == 1) {
|
||||
float r = min(half_size.x, half_size.y);
|
||||
return sdf_circle(p, r);
|
||||
}
|
||||
else if (shape_type == 2) {
|
||||
return sdf_ellipse(p, half_size);
|
||||
}
|
||||
else {
|
||||
return sdf_pill(p, half_size);
|
||||
}
|
||||
}
|
||||
|
||||
// -------- Helpers --------
|
||||
|
||||
float wrap_angle_0_2pi(float a) {
|
||||
float w = fmod(a, 2.0f * M_PI_F);
|
||||
if (w < 0.0f) w += 2.0f * M_PI_F;
|
||||
return w;
|
||||
}
|
||||
|
||||
// Project arbitrary p to ellipse boundary (stable and cheap)
|
||||
// ab are ellipse radii (half_size.x, half_size.y)
|
||||
float2 ellipse_project_boundary(float2 p, float2 ab) {
|
||||
float2 safe_ab = max(ab, float2(1e-6f));
|
||||
float2 pn = p / safe_ab;
|
||||
float len_pn = length(pn);
|
||||
if (len_pn < 1e-6f) return float2(ab.x, 0.0f);
|
||||
float2 dir = pn / len_pn;
|
||||
return dir * ab;
|
||||
}
|
||||
|
||||
// Numeric arc length from 0..theta for ellipse with radii a,b using midpoint rule.
|
||||
float ellipse_arclen_0_theta(float a, float b, float theta) {
|
||||
const int N = 12;
|
||||
float t = 0.0f;
|
||||
float dt = theta / float(N);
|
||||
float sum = 0.0f;
|
||||
for (int i = 0; i < N; i++) {
|
||||
float tm = t + 0.5f * dt;
|
||||
float s = sin(tm);
|
||||
float c = cos(tm);
|
||||
float ds = sqrt(a * a * s * s + b * b * c * c);
|
||||
sum += ds * dt;
|
||||
t += dt;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
float ellipse_perimeter_ramanujan(float a, float b) {
|
||||
float apb = a + b;
|
||||
float h = (a - b) * (a - b) / max(apb * apb, 1e-6f);
|
||||
return M_PI_F * apb * (1.0f + (3.0f * h) / (10.0f + sqrt(max(4.0f - 3.0f * h, 1e-6f))));
|
||||
}
|
||||
|
||||
// Rounded-rect boundary projection (for stable dash phase across stroke width)
|
||||
float2 rounded_rect_project_boundary(float2 p, float2 half_size, float r) {
|
||||
float2 inner = max(half_size - r, float2(0.0f));
|
||||
float2 ap = abs(p);
|
||||
|
||||
if (ap.x > inner.x && ap.y > inner.y && r > 0.0f) {
|
||||
float sx = (p.x < 0.0f) ? -1.0f : 1.0f;
|
||||
float sy = (p.y < 0.0f) ? -1.0f : 1.0f;
|
||||
float2 c = float2(inner.x * sx, inner.y * sy);
|
||||
float2 v = p - c;
|
||||
float lv = length(v);
|
||||
if (lv < 1e-6f) return c + float2(r, 0.0f);
|
||||
return c + v * (r / lv);
|
||||
}
|
||||
|
||||
float dx = half_size.x - ap.x;
|
||||
float dy = half_size.y - ap.y;
|
||||
|
||||
if (dx < dy) {
|
||||
float sx = (p.x < 0.0f) ? -1.0f : 1.0f;
|
||||
return float2(sx * half_size.x, clamp(p.y, -inner.y, inner.y));
|
||||
}
|
||||
float sy = (p.y < 0.0f) ? -1.0f : 1.0f;
|
||||
return float2(clamp(p.x, -inner.x, inner.x), sy * half_size.y);
|
||||
}
|
||||
|
||||
// Rounded-rect perimeter distance along boundary, starting at +X mid, CCW.
|
||||
float rounded_rect_dist_along(float2 pb, float2 half_size, float r) {
|
||||
float2 inner = max(half_size - r, float2(0.0f));
|
||||
float ix = inner.x;
|
||||
float iy = inner.y;
|
||||
|
||||
float seg_right_up = iy;
|
||||
float seg_arc = 0.5f * M_PI_F * r;
|
||||
float seg_top = 2.0f * ix;
|
||||
float seg_left = 2.0f * iy;
|
||||
float seg_bottom = 2.0f * ix;
|
||||
|
||||
float ax = abs(pb.x);
|
||||
float ay = abs(pb.y);
|
||||
|
||||
// Right edge
|
||||
if (pb.x > 0.0f && ay <= iy + 1e-5f) {
|
||||
if (pb.y >= 0.0f) return pb.y;
|
||||
float tail = seg_right_up + seg_arc + seg_top + seg_arc + seg_left + seg_arc + seg_bottom + seg_arc;
|
||||
return tail + (pb.y + iy);
|
||||
}
|
||||
|
||||
// Top edge
|
||||
if (pb.y > 0.0f && ax <= ix + 1e-5f) {
|
||||
float s0 = seg_right_up + seg_arc;
|
||||
return s0 + (ix - pb.x);
|
||||
}
|
||||
|
||||
// Left edge
|
||||
if (pb.x < 0.0f && ay <= iy + 1e-5f) {
|
||||
float s0 = seg_right_up + seg_arc + seg_top + seg_arc;
|
||||
return s0 + (iy - pb.y);
|
||||
}
|
||||
|
||||
// Bottom edge
|
||||
if (pb.y < 0.0f && ax <= ix + 1e-5f) {
|
||||
float s0 = seg_right_up + seg_arc + seg_top + seg_arc + seg_left + seg_arc;
|
||||
return s0 + (pb.x + ix);
|
||||
}
|
||||
|
||||
// Corners
|
||||
if (pb.x > 0.0f && pb.y > 0.0f) {
|
||||
float2 c = float2(ix, iy);
|
||||
float2 v = pb - c;
|
||||
float a = wrap_angle_0_2pi(atan2(v.y, v.x));
|
||||
float local = clamp(a, 0.0f, 0.5f * M_PI_F);
|
||||
return seg_right_up + r * local;
|
||||
}
|
||||
if (pb.x < 0.0f && pb.y > 0.0f) {
|
||||
float2 c = float2(-ix, iy);
|
||||
float2 v = pb - c;
|
||||
float a = wrap_angle_0_2pi(atan2(v.y, v.x));
|
||||
float local = clamp(M_PI_F - a, 0.0f, 0.5f * M_PI_F);
|
||||
float s0 = seg_right_up + seg_arc + seg_top;
|
||||
return s0 + r * local;
|
||||
}
|
||||
if (pb.x < 0.0f && pb.y < 0.0f) {
|
||||
float2 c = float2(-ix, -iy);
|
||||
float2 v = pb - c;
|
||||
float a = wrap_angle_0_2pi(atan2(v.y, v.x));
|
||||
float local = clamp(a - M_PI_F, 0.0f, 0.5f * M_PI_F);
|
||||
float s0 = seg_right_up + seg_arc + seg_top + seg_arc + seg_left;
|
||||
return s0 + r * local;
|
||||
}
|
||||
|
||||
// Bottom-right
|
||||
{
|
||||
float2 c = float2(ix, -iy);
|
||||
float2 v = pb - c;
|
||||
float a = wrap_angle_0_2pi(atan2(v.y, v.x));
|
||||
float local = 0.0f;
|
||||
if (a >= 1.5f * M_PI_F) local = clamp(a - 1.5f * M_PI_F, 0.0f, 0.5f * M_PI_F);
|
||||
float s0 = seg_right_up + seg_arc + seg_top + seg_arc + seg_left + seg_arc + seg_bottom;
|
||||
return s0 + r * local;
|
||||
}
|
||||
}
|
||||
|
||||
// -------- Pill helpers --------
|
||||
|
||||
float pill_perimeter(float2 half_size) {
|
||||
float r = min(half_size.x, half_size.y);
|
||||
float major = max(half_size.x, half_size.y);
|
||||
float straight = max(major - r, 0.0f);
|
||||
return 4.0f * straight + 2.0f * M_PI_F * r;
|
||||
}
|
||||
|
||||
float2 pill_project_boundary(float2 p, float2 half_size) {
|
||||
float r = min(half_size.x, half_size.y);
|
||||
bool horiz = (half_size.x >= half_size.y);
|
||||
if (horiz) {
|
||||
float cx = max(half_size.x - r, 0.0f);
|
||||
float2 c = float2((p.x >= 0.0f ? cx : -cx), 0.0f);
|
||||
float2 v = p - c;
|
||||
float lv = length(v);
|
||||
if (lv < 1e-6f) return c + float2(r, 0.0f);
|
||||
return c + v * (r / lv);
|
||||
}
|
||||
float cy = max(half_size.y - r, 0.0f);
|
||||
float2 c = float2(0.0f, (p.y >= 0.0f ? cy : -cy));
|
||||
float2 v = p - c;
|
||||
float lv = length(v);
|
||||
if (lv < 1e-6f) return c + float2(r, 0.0f);
|
||||
return c + v * (r / lv);
|
||||
}
|
||||
|
||||
float pill_dist_along(float2 pb, float2 half_size) {
|
||||
float r = min(half_size.x, half_size.y);
|
||||
bool horiz = (half_size.x >= half_size.y);
|
||||
|
||||
if (horiz) {
|
||||
float cx = max(half_size.x - r, 0.0f);
|
||||
|
||||
if (pb.x > cx - 1e-5f) {
|
||||
float2 c = float2(cx, 0.0f);
|
||||
float2 v = pb - c;
|
||||
float a = wrap_angle_0_2pi(atan2(v.y, v.x));
|
||||
float local = clamp(a, 0.0f, M_PI_F);
|
||||
return r * local;
|
||||
}
|
||||
|
||||
if (abs(pb.y - r) <= 1e-3f && abs(pb.x) <= cx + 1e-3f) {
|
||||
float s0 = M_PI_F * r;
|
||||
return s0 + (cx - pb.x);
|
||||
}
|
||||
|
||||
if (pb.x < -cx + 1e-5f) {
|
||||
float2 c = float2(-cx, 0.0f);
|
||||
float2 v = pb - c;
|
||||
float a = wrap_angle_0_2pi(atan2(v.y, v.x));
|
||||
float local = clamp(a - M_PI_F, 0.0f, M_PI_F);
|
||||
float s0 = M_PI_F * r + 2.0f * cx;
|
||||
return s0 + r * local;
|
||||
}
|
||||
|
||||
float s0 = M_PI_F * r + 2.0f * cx + M_PI_F * r;
|
||||
return s0 + (pb.x + cx);
|
||||
}
|
||||
|
||||
float cy = max(half_size.y - r, 0.0f);
|
||||
|
||||
if (pb.y > cy - 1e-5f) {
|
||||
float2 c = float2(0.0f, cy);
|
||||
float2 v = pb - c;
|
||||
float a = wrap_angle_0_2pi(atan2(v.y, v.x));
|
||||
float local = clamp(a, 0.0f, M_PI_F);
|
||||
return r * local;
|
||||
}
|
||||
|
||||
if (abs(pb.x + r) <= 1e-3f && abs(pb.y) <= cy + 1e-3f) {
|
||||
float s0 = M_PI_F * r;
|
||||
return s0 + (cy - pb.y);
|
||||
}
|
||||
|
||||
if (pb.y < -cy + 1e-5f) {
|
||||
float2 c = float2(0.0f, -cy);
|
||||
float2 v = pb - c;
|
||||
float a = wrap_angle_0_2pi(atan2(v.y, v.x));
|
||||
float local = clamp(a - M_PI_F, 0.0f, M_PI_F);
|
||||
float s0 = M_PI_F * r + 2.0f * cy;
|
||||
return s0 + r * local;
|
||||
}
|
||||
|
||||
float s0 = M_PI_F * r + 2.0f * cy + M_PI_F * r;
|
||||
return s0 + (pb.y + cy);
|
||||
}
|
||||
|
||||
// -------- Perimeter mapping for dashes --------
|
||||
|
||||
float perimeter_dist_along(float2 p, float2 half_size, int shape_type, float radius) {
|
||||
if (shape_type == 1) {
|
||||
float r = min(half_size.x, half_size.y);
|
||||
float2 pb = (length(p) < 1e-6f) ? float2(r, 0.0f) : normalize(p) * r;
|
||||
float theta = wrap_angle_0_2pi(atan2(pb.y, pb.x));
|
||||
return r * theta;
|
||||
}
|
||||
|
||||
if (shape_type == 2) {
|
||||
float a = max(half_size.x, 1e-6f);
|
||||
float b = max(half_size.y, 1e-6f);
|
||||
float2 pb = ellipse_project_boundary(p, float2(a, b));
|
||||
float theta = wrap_angle_0_2pi(atan2(pb.y / b, pb.x / a));
|
||||
return ellipse_arclen_0_theta(a, b, theta);
|
||||
}
|
||||
|
||||
if (shape_type == 3) {
|
||||
float2 pb = pill_project_boundary(p, half_size);
|
||||
return pill_dist_along(pb, half_size);
|
||||
}
|
||||
|
||||
float r = min(radius, min(half_size.x, half_size.y));
|
||||
float2 pb = rounded_rect_project_boundary(p, half_size, r);
|
||||
return rounded_rect_dist_along(pb, half_size, r);
|
||||
}
|
||||
|
||||
float perimeter_total(float2 half_size, int shape_type, float radius) {
|
||||
if (shape_type == 1) {
|
||||
float r = min(half_size.x, half_size.y);
|
||||
return 2.0f * M_PI_F * r;
|
||||
}
|
||||
if (shape_type == 2) {
|
||||
float a = max(half_size.x, 1e-6f);
|
||||
float b = max(half_size.y, 1e-6f);
|
||||
return ellipse_perimeter_ramanujan(a, b);
|
||||
}
|
||||
if (shape_type == 3) {
|
||||
return pill_perimeter(half_size);
|
||||
}
|
||||
float r = min(radius, min(half_size.x, half_size.y));
|
||||
float2 inner = max(half_size - r, float2(0.0f));
|
||||
return 4.0f * (inner.x + inner.y) + 2.0f * M_PI_F * r;
|
||||
}
|
||||
|
||||
float dash_mask_perimeter(float dist_along, float perimeter,
|
||||
float dash_len, float gap_len, float dash_offset,
|
||||
int cap_type, float aa) {
|
||||
if (dash_len <= 0.0f) return 1.0f;
|
||||
|
||||
float pattern_len = max(dash_len + gap_len, 1e-6f);
|
||||
float s = dist_along + dash_offset;
|
||||
|
||||
float pwrap = max(perimeter, 1e-6f);
|
||||
float s_wrap = fmod(s, pwrap);
|
||||
if (s_wrap < 0.0f) s_wrap += pwrap;
|
||||
|
||||
float pos = fmod(s_wrap, pattern_len);
|
||||
if (pos < 0.0f) pos += pattern_len;
|
||||
|
||||
if (cap_type == 0) {
|
||||
return pos < dash_len ? 1.0f : 0.0f;
|
||||
}
|
||||
|
||||
if (cap_type == 1) {
|
||||
float edge = dash_len;
|
||||
return 1.0f - smoothstep(edge - aa, edge + aa, pos);
|
||||
}
|
||||
|
||||
float half_dash = dash_len * 0.5f;
|
||||
float center = half_dash;
|
||||
float d = abs(pos - center);
|
||||
return smoothstep(half_dash + aa, half_dash - aa, d);
|
||||
}
|
||||
|
||||
// -------- Fragment --------
|
||||
|
||||
fragment float4 fragment_main(VertexOut in [[stage_in]],
|
||||
constant ShapeParams& params [[buffer(0)]],
|
||||
texture2d<float> fill_texture [[texture(0)]],
|
||||
sampler smp [[sampler(0)]]) {
|
||||
float2 uv = in.uv;
|
||||
|
||||
float2 p = (uv - 0.5f) * params.size;
|
||||
float2 half_size = params.size * 0.5f;
|
||||
|
||||
float d = compute_sdf(p, half_size, params.shape_type, params.radius);
|
||||
|
||||
float aa = max(params.feather, fwidth(d));
|
||||
|
||||
float fill_alpha = 0.0f;
|
||||
float stroke_alpha = 0.0f;
|
||||
|
||||
if (params.stroke_thickness > 0.0f) {
|
||||
float inner_offset, outer_offset;
|
||||
|
||||
if (params.stroke_align < 0.25f) {
|
||||
inner_offset = -params.stroke_thickness;
|
||||
outer_offset = 0.0f;
|
||||
}
|
||||
else if (params.stroke_align > 0.75f) {
|
||||
inner_offset = 0.0f;
|
||||
outer_offset = params.stroke_thickness;
|
||||
}
|
||||
else {
|
||||
inner_offset = -params.stroke_thickness * 0.5f;
|
||||
outer_offset = params.stroke_thickness * 0.5f;
|
||||
}
|
||||
|
||||
float fill_d = d - inner_offset;
|
||||
fill_alpha = 1.0f - smoothstep(-aa, aa, fill_d);
|
||||
|
||||
float start = smoothstep(-aa, aa, d - inner_offset);
|
||||
float end = 1.0f - smoothstep(-aa, aa, d - outer_offset);
|
||||
float in_stroke = start * end;
|
||||
|
||||
float perim = perimeter_total(half_size, params.shape_type, params.radius);
|
||||
float dist_along = perimeter_dist_along(p, half_size, params.shape_type, params.radius);
|
||||
float dash = dash_mask_perimeter(dist_along, perim,
|
||||
params.dash_len, params.gap_len, params.dash_offset,
|
||||
params.cap_type, aa);
|
||||
|
||||
stroke_alpha = in_stroke * dash;
|
||||
}
|
||||
else {
|
||||
fill_alpha = 1.0f - smoothstep(-aa, aa, d);
|
||||
}
|
||||
|
||||
float4 fill_col = params.fill_color;
|
||||
if (params.has_texture > 0) {
|
||||
float2 tex_uv = uv;
|
||||
tex_uv = tex_uv * params.uv_transform.xy + params.uv_transform.zw;
|
||||
|
||||
if (abs(params.uv_rotate) > 0.001f) {
|
||||
float2 center = float2(0.5f, 0.5f);
|
||||
tex_uv -= center;
|
||||
float c = cos(params.uv_rotate);
|
||||
float s = sin(params.uv_rotate);
|
||||
tex_uv = float2(tex_uv.x * c - tex_uv.y * s, tex_uv.x * s + tex_uv.y * c);
|
||||
tex_uv += center;
|
||||
}
|
||||
|
||||
float4 tex_col = fill_texture.sample(smp, tex_uv);
|
||||
fill_col *= tex_col;
|
||||
}
|
||||
|
||||
float4 fill_out = fill_col;
|
||||
fill_out.a *= fill_alpha;
|
||||
|
||||
float4 stroke_out = params.stroke_color;
|
||||
stroke_out.a *= stroke_alpha;
|
||||
|
||||
float sa = stroke_out.a;
|
||||
float fa = fill_out.a;
|
||||
|
||||
float3 rgb_premul = stroke_out.rgb * sa + fill_out.rgb * fa * (1.0f - sa);
|
||||
float a = sa + fa * (1.0f - sa);
|
||||
|
||||
float4 result = (a > 1e-6f) ? float4(rgb_premul / a, a) : float4(0.0f);
|
||||
|
||||
result *= in.color;
|
||||
result.a *= params.opacity;
|
||||
|
||||
return result;
|
||||
}
|
||||
154
shape2d.cm
Normal file
154
shape2d.cm
Normal file
@@ -0,0 +1,154 @@
|
||||
var film2d = use('film2d')
|
||||
|
||||
var shape_proto = {
|
||||
type: 'shape',
|
||||
|
||||
set_pos: function(x, y) {
|
||||
this.pos.x = x
|
||||
this.pos.y = y
|
||||
return this
|
||||
},
|
||||
|
||||
set_groups: function(groups) {
|
||||
var old_groups = this.groups
|
||||
this.groups = groups
|
||||
film2d.reindex(this._id, old_groups, groups)
|
||||
return this
|
||||
},
|
||||
|
||||
add_group: function(group) {
|
||||
if (this.groups.indexOf(group) < 0) {
|
||||
this.groups.push(group)
|
||||
film2d.index_group(this._id, group)
|
||||
}
|
||||
return this
|
||||
},
|
||||
|
||||
remove_group: function(group) {
|
||||
var idx = this.groups.indexOf(group)
|
||||
if (idx >= 0) {
|
||||
this.groups.splice(idx, 1)
|
||||
film2d.unindex_group(this._id, group)
|
||||
}
|
||||
return this
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
film2d.unregister(this._id)
|
||||
}
|
||||
}
|
||||
|
||||
var defaults = {
|
||||
type: 'shape',
|
||||
shape_type: 'rect', // 'rect', 'circle', 'ellipse', 'pill'
|
||||
|
||||
// routing
|
||||
plane: 'default',
|
||||
layer: 0,
|
||||
groups: [],
|
||||
visible: true,
|
||||
|
||||
// transform
|
||||
pos: {x: 0, y: 0},
|
||||
anchor_x: 0.5,
|
||||
anchor_y: 0.5,
|
||||
rotation: 0,
|
||||
|
||||
// geometry
|
||||
width: 100,
|
||||
height: 100,
|
||||
|
||||
// corner / radius controls (rect/pill)
|
||||
radius: 0,
|
||||
corner_style: 'round', // 'round', 'bevel', 'scoop', 'notch', 'square'
|
||||
|
||||
// edge / coverage
|
||||
feather: 0,
|
||||
stroke_thickness: 0,
|
||||
stroke_align: 'center', // 'inside', 'center', 'outside'
|
||||
|
||||
// dashed stroke
|
||||
dash_len: 0,
|
||||
gap_len: 0,
|
||||
dash_offset: 0,
|
||||
cap: 'butt', // 'butt', 'square', 'round'
|
||||
join: 'miter', // 'miter', 'bevel', 'round'
|
||||
miter_limit: 4,
|
||||
|
||||
// fill/stroke colors
|
||||
fill: {r: 1, g: 1, b: 1, a: 1},
|
||||
stroke: {r: 0, g: 0, b: 0, a: 0},
|
||||
|
||||
// blending
|
||||
blend: 'alpha', // 'alpha', 'add', 'mul', 'screen'
|
||||
opacity: 1,
|
||||
|
||||
// texture fill
|
||||
fill_tex: null,
|
||||
uv: {
|
||||
space: 'local', // 'local', 'screen'
|
||||
scale: {x: 1, y: 1},
|
||||
offset: {x: 0, y: 0},
|
||||
rotate: 0
|
||||
},
|
||||
sampler: null
|
||||
}
|
||||
|
||||
function make_shape(shape_type, props) {
|
||||
var data = {}
|
||||
for (var k in defaults) {
|
||||
var v = defaults[k]
|
||||
if (is_object(v) && !is_array(v)) {
|
||||
data[k] = {}
|
||||
for (var kk in v) data[k][kk] = v[kk]
|
||||
} else {
|
||||
data[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
data.shape_type = shape_type
|
||||
|
||||
// Apply user props (deep merge for objects)
|
||||
for (var k in props) {
|
||||
var v = props[k]
|
||||
if (is_object(v) && !is_array(v) && is_object(data[k])) {
|
||||
for (var kk in v) data[k][kk] = v[kk]
|
||||
} else {
|
||||
data[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure groups is array
|
||||
if (!data.groups) data.groups = []
|
||||
if (is_text(data.groups)) data.groups = [data.groups]
|
||||
|
||||
var s = meme(shape_proto, data)
|
||||
film2d.register(s)
|
||||
return s
|
||||
}
|
||||
|
||||
var shape2d = {
|
||||
rect: function(props) {
|
||||
return make_shape('rect', props)
|
||||
},
|
||||
|
||||
circle: function(props) {
|
||||
// For circle, width == height == diameter
|
||||
var p = props || {}
|
||||
if (p.radius != null && p.width == null) {
|
||||
p.width = p.radius * 2
|
||||
p.height = p.radius * 2
|
||||
}
|
||||
return make_shape('circle', p)
|
||||
},
|
||||
|
||||
ellipse: function(props) {
|
||||
return make_shape('ellipse', props)
|
||||
},
|
||||
|
||||
pill: function(props) {
|
||||
return make_shape('pill', props)
|
||||
}
|
||||
}
|
||||
|
||||
return shape2d
|
||||
Reference in New Issue
Block a user