From 25890062dfcb4c4fd882a281354118205512ff18 Mon Sep 17 00:00:00 2001 From: Nordup Date: Sat, 16 Aug 2025 17:18:01 +0700 Subject: [PATCH] spinning shader for special bookmarks --- app/scenes/components/bookmark.tscn | 47 ++++---- app/scripts/ui/menu/bookmark_ui.gd | 9 +- app/shaders/spinning_border.gdshader | 154 +++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 32 deletions(-) create mode 100644 app/shaders/spinning_border.gdshader diff --git a/app/scenes/components/bookmark.tscn b/app/scenes/components/bookmark.tscn index 4bcef7c..a2f4faa 100644 --- a/app/scenes/components/bookmark.tscn +++ b/app/scenes/components/bookmark.tscn @@ -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 diff --git a/app/scripts/ui/menu/bookmark_ui.gd b/app/scripts/ui/menu/bookmark_ui.gd index 4374dbf..3264eaf 100644 --- a/app/scripts/ui/menu/bookmark_ui.gd +++ b/app/scripts/ui/menu/bookmark_ui.gd @@ -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: diff --git a/app/shaders/spinning_border.gdshader b/app/shaders/spinning_border.gdshader new file mode 100644 index 0000000..7ef12f3 --- /dev/null +++ b/app/shaders/spinning_border.gdshader @@ -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); +} + +