// Uber-shader SDF de Aura. Todas las primitivas son instancias de un quad
// unitario (triangle strip de 4 vértices) leídas desde storage buffers.
// Una por lote, con desplazamiento dinámico. `matrix` = (a, b, c, d) y `offset` = (tx, ty, factor de escala, 0) de
// la transformación afín del lote; sin transformación es la identidad.
struct Globals {
viewport: vec2<f32>,
padding: vec2<f32>,
matrix: vec4<f32>,
offset: vec4<f32>,
}
@group(0) @binding(0) var<uniform> globals: Globals;
struct Quad {
bounds: vec4<f32>,
clip: vec4<f32>,
background: vec4<f32>,
border_color: vec4<f32>,
corner_radii: vec4<f32>,
border_widths: vec4<f32>,
gradient_end: vec4<f32>,
gradient: vec4<f32>,
}
struct Shadow {
bounds: vec4<f32>,
clip: vec4<f32>,
color: vec4<f32>,
corner_radii: vec4<f32>,
blur: f32,
}
struct Underline {
bounds: vec4<f32>,
clip: vec4<f32>,
color: vec4<f32>,
thickness: f32,
wavy: f32,
}
struct Blur {
bounds: vec4<f32>,
clip: vec4<f32>,
corner_radii: vec4<f32>,
sigma: f32,
}
struct Sprite {
bounds: vec4<f32>,
clip: vec4<f32>,
uv: vec4<f32>,
color: vec4<f32>,
}
@group(1) @binding(0) var<storage, read> quads: array<Quad>;
@group(1) @binding(0) var<storage, read> shadows: array<Shadow>;
@group(1) @binding(0) var<storage, read> underlines: array<Underline>;
@group(1) @binding(0) var<storage, read> sprites: array<Sprite>;
@group(1) @binding(0) var<storage, read> blurs: array<Blur>;
@group(2) @binding(0) var atlas_texture: texture_2d<f32>;
@group(2) @binding(1) var atlas_sampler: sampler;
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) logical: vec2<f32>,
@location(1) @interpolate(flat) instance: u32,
@location(2) uv: vec2<f32>,
}
fn quad_vertex(index: u32, bounds: vec4<f32>, instance: u32, padding: f32) -> VertexOutput {
let corner = vec2<f32>(f32(index & 1u), f32(index >> 1u));
let origin = bounds.xy - vec2<f32>(padding);
let extent = bounds.zw + vec2<f32>(padding * 2.0);
let logical = origin + corner * extent;
// `logical` queda en el espacio local (para el SDF y el recorte); solo la posición en pantalla se transforma.
let screen = vec2<f32>(
globals.matrix.x * logical.x + globals.matrix.z * logical.y + globals.offset.x,
globals.matrix.y * logical.x + globals.matrix.w * logical.y + globals.offset.y,
);
let ndc = screen / globals.viewport * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0);
var out: VertexOutput;
out.position = vec4<f32>(ndc, 0.0, 1.0);
out.logical = logical;
out.instance = instance;
out.uv = corner;
return out;
}
// 1 dentro del recorte (ContentMask), 0 fuera.
fn clip_alpha(position: vec2<f32>, clip: vec4<f32>) -> f32 {
let inside = step(clip.xy, position) * step(position, clip.xy + clip.zw);
return inside.x * inside.y;
}
// Distancia con signo a un rectángulo redondeado; radios en orden tl, tr, br, bl.
fn rounded_rect_distance(point: vec2<f32>, half_size: vec2<f32>, radii: vec4<f32>) -> f32 {
let side = select(radii.xw, radii.yz, point.x > 0.0);
let radius = select(side.x, side.y, point.y > 0.0);
let q = abs(point) - half_size + vec2<f32>(radius);
return min(max(q.x, q.y), 0.0) + length(max(q, vec2<f32>(0.0))) - radius;
}
fn premultiply(color: vec4<f32>) -> vec4<f32> {
return vec4<f32>(color.rgb * color.a, color.a);
}
// ---------- Quads ----------
@vertex
fn vs_quad(@builtin(vertex_index) vertex: u32, @builtin(instance_index) instance: u32) -> VertexOutput {
return quad_vertex(vertex, quads[instance].bounds, instance, 1.0);
}
@fragment
fn fs_quad(in: VertexOutput) -> @location(0) vec4<f32> {
let quad = quads[in.instance];
let half_size = quad.bounds.zw * 0.5;
let center = quad.bounds.xy + half_size;
// Las distancias son locales: con la escala del lote el borde sigue medio píxel de suave en pantalla.
let outer = rounded_rect_distance(in.logical - center, half_size, quad.corner_radii) * globals.offset.z;
let coverage = clamp(0.5 - outer, 0.0, 1.0) * clip_alpha(in.logical, quad.clip);
// widths: top, right, bottom, left
let widths = quad.border_widths;
let inner_half = half_size - vec2<f32>(widths.w + widths.y, widths.x + widths.z) * 0.5;
let inner_center = center + vec2<f32>(widths.w - widths.y, widths.x - widths.z) * 0.5;
let inner_radii = max(quad.corner_radii - vec4<f32>(max(max(widths.x, widths.y), max(widths.z, widths.w))), vec4<f32>(0.0));
let inner = rounded_rect_distance(in.logical - inner_center, inner_half, inner_radii) * globals.offset.z;
let has_border = step(0.001, widths.x + widths.y + widths.z + widths.w);
let border_mix = clamp(0.5 + inner, 0.0, 1.0) * has_border;
var fill = premultiply(quad.background);
if quad.gradient.z > 0.5 {
// Largo del degradado como en CSS: la proyección de la caja sobre la dirección.
let direction = quad.gradient.xy;
let length = abs(quad.bounds.z * direction.x) + abs(quad.bounds.w * direction.y);
let t = clamp(0.5 + dot(in.logical - center, direction) / max(length, 0.001), 0.0, 1.0);
fill = mix(fill, premultiply(quad.gradient_end), t);
}
let border = premultiply(quad.border_color);
return mix(fill, border, border_mix) * coverage;
}
// ---------- Sombras ----------
@vertex
fn vs_shadow(@builtin(vertex_index) vertex: u32, @builtin(instance_index) instance: u32) -> VertexOutput {
return quad_vertex(vertex, shadows[instance].bounds, instance, shadows[instance].blur * 3.0);
}
// Aproximación de erf (Abramowitz-Stegun) para la integral gaussiana.
fn erf_approx(value: f32) -> f32 {
let x = abs(value);
let t = 1.0 / (1.0 + 0.3275911 * x);
let poly = t * (0.254829592 + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
return sign(value) * (1.0 - poly * exp(-x * x));
}
@fragment
fn fs_shadow(in: VertexOutput) -> @location(0) vec4<f32> {
let shadow = shadows[in.instance];
let half_size = shadow.bounds.zw * 0.5;
let center = shadow.bounds.xy + half_size;
let distance = rounded_rect_distance(in.logical - center, half_size, shadow.corner_radii);
let sigma = max(shadow.blur * 0.5, 0.001);
let falloff = 0.5 * (1.0 - erf_approx(distance / (sigma * 1.41421356)));
let alpha = shadow.color.a * falloff * clip_alpha(in.logical, shadow.clip);
return vec4<f32>(shadow.color.rgb * alpha, alpha);
}
// ---------- Subrayados ----------
@vertex
fn vs_underline(@builtin(vertex_index) vertex: u32, @builtin(instance_index) instance: u32) -> VertexOutput {
return quad_vertex(vertex, underlines[instance].bounds, instance, 0.0);
}
@fragment
fn fs_underline(in: VertexOutput) -> @location(0) vec4<f32> {
let underline = underlines[in.instance];
let local = in.logical - underline.bounds.xy;
let center_y = underline.bounds.w * 0.5;
let wave = sin(local.x * 1.2) * underline.bounds.w * 0.25 * underline.wavy;
let distance = (abs(local.y - center_y - wave) - underline.thickness * 0.5) * globals.offset.z;
let alpha = clamp(0.5 - distance, 0.0, 1.0) * underline.color.a * clip_alpha(in.logical, underline.clip);
return vec4<f32>(underline.color.rgb * alpha, alpha);
}
// ---------- Sprites (glifos monocromáticos e imágenes a color) ----------
@vertex
fn vs_sprite(@builtin(vertex_index) vertex: u32, @builtin(instance_index) instance: u32) -> VertexOutput {
var out = quad_vertex(vertex, sprites[instance].bounds, instance, 0.0);
let sprite = sprites[instance];
out.uv = mix(sprite.uv.xy, sprite.uv.zw, out.uv);
return out;
}
@fragment
fn fs_monochrome(in: VertexOutput) -> @location(0) vec4<f32> {
let sprite = sprites[in.instance];
let mask = textureSample(atlas_texture, atlas_sampler, in.uv).r;
let alpha = mask * sprite.color.a * clip_alpha(in.logical, sprite.clip);
return vec4<f32>(sprite.color.rgb * alpha, alpha);
}
@fragment
fn fs_polychrome(in: VertexOutput) -> @location(0) vec4<f32> {
let sprite = sprites[in.instance];
let texel = textureSample(atlas_texture, atlas_sampler, in.uv);
return texel * sprite.color.a * clip_alpha(in.logical, sprite.clip);
}
// ---------- Desenfoque: pega la textura ya difuminada dentro de un rectángulo redondeado ----------
@vertex
fn vs_blur(@builtin(vertex_index) vertex: u32, @builtin(instance_index) instance: u32) -> VertexOutput {
return quad_vertex(vertex, blurs[instance].bounds, instance, 1.0);
}
@fragment
fn fs_blur(in: VertexOutput) -> @location(0) vec4<f32> {
let blur = blurs[in.instance];
let half_size = blur.bounds.zw * 0.5;
let center = blur.bounds.xy + half_size;
let distance = rounded_rect_distance(in.logical - center, half_size, blur.corner_radii);
let coverage = clamp(0.5 - distance, 0.0, 1.0) * clip_alpha(in.logical, blur.clip);
// `logical` ya está en px lógicos y la textura difuminada cubre toda la ventana.
let color = textureSampleLevel(atlas_texture, atlas_sampler, in.logical / globals.viewport, 0.0);
return vec4<f32>(color.rgb * coverage, coverage);
}