spinning shader for special bookmarks

This commit is contained in:
Nordup 2025-08-16 17:18:01 +07:00
parent 74c3c4e65e
commit 25890062df
3 changed files with 178 additions and 32 deletions

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=11 format=3 uid="uid://82ca8so31njy"]
[gd_scene load_steps=13 format=3 uid="uid://82ca8so31njy"]
[ext_resource type="Script" path="res://scripts/ui/menu/bookmark_ui.gd" id="1_bpkqj"]
[ext_resource type="Resource" uid="uid://b1xvdym0qh6td" path="res://resources/gate_events.res" id="2_7i5yr"]
@ -7,24 +7,23 @@
[ext_resource type="StyleBox" uid="uid://bmxiecm3vkddl" path="res://assets/styles/panel_hover.stylebox" id="4_figib"]
[ext_resource type="LabelSettings" uid="uid://85ms8ndcmbn0" path="res://assets/styles/text_small.tres" id="4_xqjm8"]
[ext_resource type="Texture2D" uid="uid://6k1ia4pidwrq" path="res://assets/textures/empty_icon.svg" id="5_vwpfy"]
[ext_resource type="Shader" path="res://shaders/spinning_border.gdshader" id="6_16gpr"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1vnuq"]
bg_color = Color(0.423529, 0.235294, 0.933333, 1)
corner_radius_top_left = 25
corner_radius_top_right = 25
corner_radius_bottom_right = 25
corner_radius_bottom_left = 25
shadow_color = Color(0.0862745, 0.0901961, 0.117647, 0.784314)
shadow_size = 4
[sub_resource type="Gradient" id="Gradient_sffh2"]
offsets = PackedFloat32Array(0, 0.25, 0.5, 0.75, 1)
colors = PackedColorArray(1, 0.2, 0.4, 1, 1, 0.8, 0.2, 1, 0.2, 1, 0.6, 1, 0.2, 0.6, 1, 1, 1, 0.2, 0.4, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bktbh"]
bg_color = Color(0.32549, 0.14902, 0.8, 1)
corner_radius_top_left = 25
corner_radius_top_right = 25
corner_radius_bottom_right = 25
corner_radius_bottom_left = 25
shadow_color = Color(0.0862745, 0.0901961, 0.117647, 0.784314)
shadow_size = 4
[sub_resource type="GradientTexture1D" id="GradientTexture1D_yl185"]
gradient = SubResource("Gradient_sffh2")
[sub_resource type="ShaderMaterial" id="ShaderMaterial_i1368"]
shader = ExtResource("6_16gpr")
shader_parameter/border_thickness = 0.015
shader_parameter/edge_smoothness = 0.01
shader_parameter/corner_radius = Vector4(0.25, 0.25, 0.25, 0.25)
shader_parameter/speed = 1.5
shader_parameter/clockwise = false
shader_parameter/gradient = SubResource("GradientTexture1D_yl185")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_od0ga"]
corner_radius_top_left = 20
@ -32,8 +31,7 @@ corner_radius_top_right = 20
corner_radius_bottom_right = 20
corner_radius_bottom_left = 20
[node name="Bookmark" type="Control" node_paths=PackedStringArray("icon", "title", "button", "button_special")]
clip_children = 1
[node name="Bookmark" type="Control" node_paths=PackedStringArray("icon", "title", "button", "special_effect")]
custom_minimum_size = Vector2(180, 100)
layout_mode = 3
anchors_preset = 0
@ -45,7 +43,7 @@ ui_events = ExtResource("3_sp6jv")
icon = NodePath("Mask/Icon")
title = NodePath("Title")
button = NodePath("Button")
button_special = NodePath("ButtonSpecial")
special_effect = NodePath("SpecialEffect")
[node name="Button" type="Button" parent="."]
layout_mode = 1
@ -60,19 +58,16 @@ theme_override_styles/hover = ExtResource("4_figib")
theme_override_styles/pressed = ExtResource("4_figib")
theme_override_styles/normal = ExtResource("3_tb1mf")
[node name="ButtonSpecial" type="Button" parent="."]
visible = false
[node name="SpecialEffect" type="Panel" parent="."]
material = SubResource("ShaderMaterial_i1368")
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
focus_mode = 0
mouse_filter = 2
mouse_default_cursor_shape = 2
theme_override_styles/hover = SubResource("StyleBoxFlat_1vnuq")
theme_override_styles/pressed = SubResource("StyleBoxFlat_1vnuq")
theme_override_styles/normal = SubResource("StyleBoxFlat_bktbh")
[node name="Mask" type="Panel" parent="."]
clip_children = 1

View file

@ -6,7 +6,7 @@ class_name BookmarkUI
@export var icon: TextureRect
@export var title: Label
@export var button: Button
@export var button_special: Button
@export var special_effect: Panel
var url: String
var is_special: bool
@ -14,7 +14,6 @@ var is_special: bool
func _ready() -> void:
button.pressed.connect(on_pressed)
button_special.pressed.connect(on_pressed)
ui_events.onboarding_started.connect(update_button_type)
ui_events.onboarding_finished.connect(update_button_type)
@ -36,11 +35,9 @@ func fill(gate: Gate) -> void:
func update_button_type() -> void:
if ui_events.is_onboarding_started:
button.visible = true
button_special.visible = false
special_effect.visible = false
else:
button.visible = not is_special
button_special.visible = is_special
special_effect.visible = is_special
func on_pressed() -> void:

View file

@ -0,0 +1,154 @@
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);
}