thegates/app/shaders/spinning_border.gdshader

152 lines
5.9 KiB
Text

shader_type canvas_item;
render_mode unshaded, blend_mix;
// Border thickness in UV units (0..0.5). 0.02 ≈ 2% of the smallest side.
uniform float border_thickness : hint_range(0.0, 0.5) = 0.02;
// Soft edge for anti-aliasing around the inner border.
uniform float edge_smoothness : hint_range(0.0, 0.05) = 0.002;
// Per-corner radius in UV units (0..0.5): x=top-left, y=top-right, z=bottom-right, w=bottom-left
uniform vec4 corner_radius = vec4(0.0);
// Animation speed (units per second around the perimeter).
uniform float speed : hint_range(-10.0, 10.0) = 0.5;
// Direction toggle: true = clockwise, false = counter-clockwise.
uniform bool clockwise = true;
// Provide a GradientTexture1D here (Texture2D) to control colors.
uniform sampler2D gradient : source_color, filter_linear, repeat_enable;
vec3 hsv2rgb(vec3 c) {
vec3 p = abs(fract(c.xxx + vec3(0.0, 0.6666667, 0.3333333)) * 6.0 - 3.0);
vec3 rgb = clamp(p - 1.0, 0.0, 1.0);
return c.z * mix(vec3(1.0), rgb, c.y);
}
void fragment() {
vec2 uv = UV;
// Estimate the item size in pixels using derivatives of UV
vec2 duv_dx = dFdx(uv);
vec2 duv_dy = dFdy(uv);
float grad_u = length(vec2(duv_dx.x, duv_dy.x));
float grad_v = length(vec2(duv_dx.y, duv_dy.y));
float width_px = 1.0 / max(grad_u, 1e-6);
float height_px = 1.0 / max(grad_v, 1e-6);
vec2 size_px = vec2(width_px, height_px);
vec2 p = uv * size_px; // position in pixels
// Decide radii source: pixel radii (if provided) or UV radii scaled to pixels
float base_scale = min(size_px.x, size_px.y);
vec4 r_src_px = corner_radius * base_scale;
// Clamp radii to reasonable bounds
float r_tl = clamp(r_src_px.x, 0.0, 0.5 * base_scale);
float r_tr = clamp(r_src_px.y, 0.0, 0.5 * base_scale);
float r_br = clamp(r_src_px.z, 0.0, 0.5 * base_scale);
float r_bl = clamp(r_src_px.w, 0.0, 0.5 * base_scale);
// Compute distance from the outer rounded-rectangle border, positive inside, negative outside
float d; // signed distance to outer border, positive inside
// Also compute a perimeter parameter s along the border for color animation
float s = 0.0;
// Precompute straight lengths and arc lengths (in UV units)
float L0 = max(0.0, size_px.x - (r_tl + r_tr)); // top straight length
float L1 = max(0.0, size_px.y - (r_tr + r_br)); // right straight length
float L2 = max(0.0, size_px.x - (r_br + r_bl)); // bottom straight length
float L3 = max(0.0, size_px.y - (r_bl + r_tl)); // left straight length
float A0 = 0.5 * PI * r_tl; // TL arc length
float A1 = 0.5 * PI * r_tr; // TR arc length
float A2 = 0.5 * PI * r_br; // BR arc length
float A3 = 0.5 * PI * r_bl; // BL arc length
float perim = L0 + L1 + L2 + L3 + A0 + A1 + A2 + A3;
perim = max(perim, 1e-6);
// Determine region and compute signed distance and perimeter position
if (p.x < r_tl && p.y < r_tl) {
// Top-left corner
vec2 c = vec2(r_tl, r_tl);
vec2 v = p - c;
float lenv = length(v);
d = r_tl - lenv;
float theta = atan(v.y, v.x);
if (theta < 0.0) theta += 2.0 * PI;
float a = clamp(theta - PI, 0.0, 0.5 * PI);
s = L0 + A1 + L1 + A2 + L2 + A3 + L3 + r_tl * a;
} else if (p.y < r_tl && p.x >= r_tl && p.x <= size_px.x - r_tr) {
// Top edge (left -> right)
d = p.y;
s = (p.x - r_tl);
} else if (p.x > size_px.x - r_tr && p.y < r_tr) {
// Top-right corner
vec2 c = vec2(size_px.x - r_tr, r_tr);
vec2 v = p - c;
float lenv = length(v);
d = r_tr - lenv;
float theta = atan(v.y, v.x); // [-pi,pi]
// Map to [-pi/2, 0] for this quadrant
float a = clamp(theta + 0.5 * PI, 0.0, 0.5 * PI);
s = L0 + r_tr * a;
} else if (p.x > size_px.x - r_tr && p.y >= r_tr && p.y <= size_px.y - r_br) {
// Right edge (top -> bottom)
d = size_px.x - p.x;
s = L0 + A1 + (p.y - r_tr);
} else if (p.x > size_px.x - r_br && p.y > size_px.y - r_br) {
// Bottom-right corner
vec2 c = vec2(size_px.x - r_br, size_px.y - r_br);
vec2 v = p - c;
float lenv = length(v);
d = r_br - lenv;
float theta = atan(v.y, v.x); // [0, pi/2] in this quadrant for inside
float a = clamp(theta - 0.0, 0.0, 0.5 * PI);
s = L0 + A1 + L1 + r_br * a;
} else if (p.y > size_px.y - r_br && p.x >= r_bl && p.x <= size_px.x - r_br) {
// Bottom edge (right -> left)
d = size_px.y - p.y;
s = L0 + A1 + L1 + A2 + (size_px.x - r_br - p.x);
} else if (p.x < r_bl && p.y > size_px.y - r_bl) {
// Bottom-left corner
vec2 c = vec2(r_bl, size_px.y - r_bl);
vec2 v = p - c;
float lenv = length(v);
d = r_bl - lenv;
float theta = atan(v.y, v.x);
float a = clamp(theta - 0.5 * PI, 0.0, 0.5 * PI);
s = L0 + A1 + L1 + A2 + L2 + r_bl * a;
} else if (p.x < r_bl && p.y >= r_tl && p.y <= size_px.y - r_bl) {
// Left edge (bottom -> top)
d = p.x;
s = L0 + A1 + L1 + A2 + L2 + A3 + (size_px.y - r_bl - p.y);
} else {
// Center area away from border; distance = min distance to any edge or corner (inside)
float d_top = p.y;
float d_right = size_px.x - p.x;
float d_bottom = size_px.y - p.y;
float d_left = p.x;
d = min(min(d_top, d_right), min(d_bottom, d_left));
// s fallback: project to nearest straight edge assuming top for simplicity
s = clamp(p.x - r_tl, 0.0, L0);
}
// Convert thickness and smoothing to pixels from UV-based uniforms
float base_scale2 = base_scale;
float thickness_px = border_thickness * base_scale2;
float smooth_px = edge_smoothness * base_scale2;
float aa = max(smooth_px, fwidth(d));
// Safety inset so the outer edge sits slightly inside the rounded rect to prevent corner clipping
float inset_px = max(aa, 0.75);
// Border mask band: [inset_px, inset_px + thickness_px] (grows inward only)
float mask = smoothstep(inset_px - aa, inset_px + aa, d) - smoothstep(inset_px + thickness_px - aa, inset_px + thickness_px + aa, d);
mask = clamp(mask, 0.0, 1.0);
// Perimeter coordinate t in [0,1), increasing clockwise, wrapping
float t = s / perim;
float dir = clockwise ? 1.0 : -1.0;
t = fract(t + TIME * speed * dir);
vec4 col = texture(gradient, vec2(t, 0.5));
COLOR = vec4(col.rgb, col.a * mask);
}