This commit is contained in:
2026-01-08 17:55:50 -06:00
parent dec26f6b41
commit ce479299eb
4 changed files with 838 additions and 1 deletions

View File

@@ -203,6 +203,8 @@ film2d.render = function(params, backend) {
commands.push({cmd: 'draw_text', drawable: batch.drawable}) commands.push({cmd: 'draw_text', drawable: batch.drawable})
else if (batch.type == 'texture_ref') else if (batch.type == 'texture_ref')
commands.push({cmd: 'draw_texture_ref', drawable: batch.drawable}) 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'}) 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 { } else {
if (current) { if (current) {
batches.push(current) batches.push(current)

View File

@@ -36,6 +36,7 @@ var _crt_frag = null
var _accumulator_frag = null var _accumulator_frag = null
var _text_sdf_frag = null var _text_sdf_frag = null
var _text_msdf_frag = null var _text_msdf_frag = null
var _shape2d_frag = null
// Pipelines // Pipelines
var _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 return true
} }
@@ -582,6 +595,7 @@ function _create_pipelines() {
}] }]
} }
}) })
}
// Accumulator pipeline // Accumulator pipeline
if (_blit_vert && _accumulator_frag) { if (_blit_vert && _accumulator_frag) {
_pipelines.accumulator = new gpu_mod.graphics_pipeline(_gpu, { _pipelines.accumulator = new gpu_mod.graphics_pipeline(_gpu, {
@@ -604,7 +618,43 @@ function _create_pipelines() {
color_targets: [{format: _swapchain_format, blend: {enabled: false}}] 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) pending_draws.push(cmd)
break break
case 'draw_shape':
pending_draws.push(cmd)
break
case 'blit': case 'blit':
// Flush pending draws first // Flush pending draws first
if (current_pass && pending_draws.length > 0) { 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 pre-rendered effect texture
_render_texture_ref(cmd_buffer, pass, draw.drawable, camera, target) _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) 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) { function _render_text(cmd_buffer, pass, drawable, camera, target) {
// Get font - support mode tag: 'bitmap', 'sdf', 'msdf' // Get font - support mode tag: 'bitmap', 'sdf', 'msdf'
var font_path = drawable.font var font_path = drawable.font

View 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
View 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