update game_menu

This commit was merged in pull request #40.
This commit is contained in:
2026-06-30 20:01:13 +00:00
parent 7db1df9ab6
commit 7be800398d
81 changed files with 8493 additions and 139 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bjowpx1ap4uxt"
path="res://.godot/imported/back.png-bddbf3418b746eb53bc79fd4d0989d41.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/PageFlip/Assets/back.png"
dest_files=["res://.godot/imported/back.png-bddbf3418b746eb53bc79fd4d0989d41.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cen51wqc15b14"
path="res://.godot/imported/blank_page.png-589b6dfce0585742cc1d305c391c9cd9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/PageFlip/Assets/blank_page.png"
dest_files=["res://.godot/imported/blank_page.png-589b6dfce0585742cc1d305c391c9cd9.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://bhitebdghyhua"
path="res://.godot/imported/book_impact.ogg-ae2f194921fcd1bc549a3ddb37fab4ba.oggvorbisstr"
[deps]
source_file="res://addons/PageFlip/Assets/book_impact.ogg"
dest_files=["res://.godot/imported/book_impact.ogg-ae2f194921fcd1bc549a3ddb37fab4ba.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dbdwbowx32d3v"
path="res://.godot/imported/front.png-76dff7071666496195badceef4e0ce67.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/PageFlip/Assets/front.png"
dest_files=["res://.godot/imported/front.png-76dff7071666496195badceef4e0ce67.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dt1tiecgw5rip"
path="res://.godot/imported/interior.png-d4b06daf084fdb8a90836ec57d2dd53f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/PageFlip/Assets/interior.png"
dest_files=["res://.godot/imported/interior.png-d4b06daf084fdb8a90836ec57d2dd53f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://bylfc3b5pmbij"
path="res://.godot/imported/page_flip.ogg-aabb6ae51ff161ae6afa52da11fe75de.oggvorbisstr"
[deps]
source_file="res://addons/PageFlip/Assets/page_flip.ogg"
dest_files=["res://.godot/imported/page_flip.ogg-aabb6ae51ff161ae6afa52da11fe75de.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -0,0 +1,49 @@
shader_type canvas_item;
uniform sampler2D front_texture : source_color, filter_linear_mipmap;
uniform sampler2D back_texture : source_color, filter_linear_mipmap;
uniform sampler2D spine_shadow_gradient : source_color, filter_linear, repeat_disable;
uniform float shadow_intensity : hint_range(0.0, 1.0) = 0.0;
uniform float max_shadow_spread : hint_range(0.0, 1.0) = 0.35;
uniform bool debug_mode = false;
uniform bool is_backward = false;
void fragment() {
vec2 uv_dx = dFdx(UV);
vec2 uv_dy = dFdy(UV);
float face_dir = uv_dx.x * uv_dy.y - uv_dx.y * uv_dy.x;
bool is_front = face_dir > 0.0;
vec4 base_color;
if (is_front) {
if (is_backward) {
// Let's try flipping Y just in case they meant upside down!
// And keep X normal, since UV goes 0 to 1.
base_color = texture(front_texture, UV);
} else {
base_color = texture(front_texture, UV);
}
} else {
if (is_backward) {
base_color = texture(back_texture, vec2(1.0 - UV.x, UV.y));
} else {
base_color = texture(back_texture, vec2(1.0 - UV.x, UV.y));
}
}
float current_spread = max_shadow_spread * (shadow_intensity + 0.001);
float sample_x = UV.x / current_spread;
float shadow_alpha = texture(spine_shadow_gradient, vec2(sample_x, 0.0)).a;
if (sample_x > 1.0) {
shadow_alpha = 0.0;
}
shadow_alpha *= shadow_intensity;
vec3 final_rgb = mix(base_color.rgb, vec3(0.0, 0.0, 0.0), shadow_alpha);
COLOR = vec4(final_rgb, base_color.a);
}

View File

@@ -0,0 +1 @@
uid://b57sdbj15qx37

View File

@@ -0,0 +1,9 @@
[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://ddw2ie8wnrnre"]
[ext_resource type="Shader" uid="uid://b57sdbj15qx37" path="res://addons/PageFlip/Assets/page_flip_final.gdshader" id="1_cl0rr"]
[resource]
shader = ExtResource("1_cl0rr")
shader_parameter/shadow_intensity = 0.0
shader_parameter/max_shadow_spread = 0.35
shader_parameter/debug_mode = false

View File

@@ -0,0 +1,8 @@
[gd_resource type="GradientTexture2D" format=3 uid="uid://rm7bporlv3cf"]
[sub_resource type="Gradient" id="Gradient_3i6i7"]
offsets = PackedFloat32Array(0, 0.2731183, 0.44731182, 1)
colors = PackedColorArray(0, 0, 0, 0.521, 0, 0, 0, 0.492, 0, 0, 0, 0.358, 0, 0, 0, 0)
[resource]
gradient = SubResource("Gradient_3i6i7")

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cit41jypw2sy1"
path="res://.godot/imported/spine.png-c2fa10bef4996b84a954651c908149a7.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/PageFlip/Assets/spine.png"
dest_files=["res://.godot/imported/spine.png-c2fa10bef4996b84a954651c908149a7.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

229
addons/PageFlip/book_api.gd Normal file
View File

@@ -0,0 +1,229 @@
@tool
class_name BookAPI
extends RefCounted
## Static utility class for the PageFlip2D system by Newold.
## Provides helper methods to configure the book and facilitate interaction
## from embedded scenes (UI, puzzles, maps).
##
## [b]How to use:[/b][br]
## This class is designed to be used statically. You do not need to instance it.
## Simply call [code]BookAPI.function_name()[/code] from anywhere in your project.
## It automatically tracks the active [PageFlip2D] instance.
## (When a PageFlip scene enters the tree, it registers itself as the current_book.)
# ==============================================================================
# ENUMS (Mirrored for easy access)
# ==============================================================================
enum JumpTarget {
## Jump to the closed state showing the Front Cover.
FRONT_COVER,
## Jump to the closed state showing the Back Cover.
BACK_COVER,
## Jump to a specific content page number.
CONTENT_PAGE
}
# Internal reference to the currently active book.
static var _current_book: PageFlip2D
# ==============================================================================
# SETUP & REFERENCES
# ==============================================================================
## Registers a book instance as the currently active one.
## Automatically called by [PageFlip2D] in its [method Node._ready].
static func set_current_book(book: PageFlip2D) -> void:
_current_book = book
## Returns the currently active book instance, or null if none is registered.
static func get_current_book() -> PageFlip2D:
return _current_book
# ==============================================================================
# NAVIGATION CONTROLS
# ==============================================================================
## Turns the page forward (to the next page/spread).
## Does nothing if the book is animating or at the end.
static func next_page() -> void:
if is_instance_valid(_current_book):
force_release_control(_current_book)
_current_book.next_page()
## Turns the page backward (to the previous page/spread).
## Does nothing if the book is animating or at the beginning.
static func prev_page() -> void:
if is_instance_valid(_current_book):
force_release_control(_current_book)
_current_book.prev_page()
## Forces the active book to close towards a specific cover.
## [param to_front_cover]: If true, closes to the Front Cover (Right to Left). If false, closes to Back Cover.
static func force_close_book(to_front_cover: bool) -> void:
if is_instance_valid(_current_book):
force_release_control(_current_book)
_current_book.force_close_book(to_front_cover)
## Navigates to a specific page number (1-based index).
## Acts as a wrapper for go_to_spread, calculating the correct index automatically.
## [b]ASYNC:[/b] Must be called with 'await' if animated is true.
## [param page_num]: The 1-based page number (1 = first texture in pages_paths).
## [param target]: Specifies if the target is a content page or a cover.
static func go_to_page(page_num: int = 1, target: JumpTarget = JumpTarget.CONTENT_PAGE, animated: bool = true) -> void:
var book = _current_book
if not is_instance_valid(book): return
var target_spread_idx: int = 0
match target:
JumpTarget.FRONT_COVER:
target_spread_idx = -1
JumpTarget.BACK_COVER:
target_spread_idx = book.total_spreads
JumpTarget.CONTENT_PAGE:
# Calculate spread index: 1-2 -> Spread 0, 3-4 -> Spread 1, etc.
var safe_page = max(1, page_num)
target_spread_idx = int(safe_page / 2.0)
target_spread_idx = clampi(target_spread_idx, 0, book.total_spreads - 1)
await go_to_spread(book, target_spread_idx, animated)
## Navigates to a specific spread index directly.
## [b]ASYNC:[/b] Must be called with 'await' if animated is true.
## - Animated: Fast-forwards through pages with dynamic speed.
## - Instant: Snaps to page and manually triggers the scene activation handshake.
static func go_to_spread(book: PageFlip2D, target_spread: int, animated: bool = true) -> void:
if not is_instance_valid(book): return
# Clamp target
var final_target = clampi(target_spread, -1, book.total_spreads)
var diff = final_target - book.current_spread
if diff == 0:
return
force_release_control(book)
if not animated:
# INSTANT TELEPORT
book.current_spread = final_target
book.call("_update_static_visuals_immediate")
book.call("_update_volume_visuals")
# Force check for interactive scenes after teleport
book.call("_check_scene_activation")
else:
# ANIMATED FAST-FORWARD
if book.is_animating: return # Don't interrupt an existing animation
var original_speed = book.anim_player.speed_scale
# --- DYNAMIC SPEED CALCULATION ---
var steps = abs(diff)
# Base speed: 1.5x -> Max speed: 10.0x
var dynamic_speed = remap(float(steps), 0.0, float(book.total_spreads), 1.5, 10.0)
book.anim_player.speed_scale = dynamic_speed
var going_forward = diff > 0
for i in range(steps):
if not is_instance_valid(book): break
if going_forward: book.next_page()
else: book.prev_page()
# Wait for the physical page turn to finish before starting the next one.
if book.anim_player.is_playing():
await book.anim_player.animation_finished
else:
await book.get_tree().process_frame
# RESTORE STATE
if is_instance_valid(book):
book.anim_player.speed_scale = original_speed
# ==============================================================================
# STATE & INTERACTION HELPERS
# ==============================================================================
## Returns true if the book (active or specific) is currently animating.
static func is_busy(book_instance: PageFlip2D = null) -> bool:
var book = book_instance if book_instance else _current_book
if not is_instance_valid(book): return false
return book.is_animating
## Locates the PageFlip2D controller ancestor from any node inside an interactive page.
static func find_book_controller(caller_node: Node) -> PageFlip2D:
var current = caller_node
while current:
if current is PageFlip2D:
return current
current = current.get_parent()
return null
## Safely locks or unlocks the book's ability to turn pages manually.
## WARNING: If locked, the interactive scene MUST be responsible for unlocking it later.
static func set_interaction_lock(book: PageFlip2D, locked: bool) -> void:
if not is_instance_valid(book): return
book.call("_pageflip_set_input_enabled", not locked)
## Forces the book to regain input control immediately.
## Useful as a failsafe if an interactive scene closes unexpectedly.
static func force_release_control(book: PageFlip2D) -> void:
if not is_instance_valid(book): return
book.call("_pageflip_set_input_enabled", true)
# ==============================================================================
# CONFIGURATION HELPERS
# ==============================================================================
## Configures the visual properties of a Book instance via a dictionary.
static func configure_visuals(book: PageFlip2D, data: Dictionary) -> void:
if not is_instance_valid(book): return
if "pages" in data:
book.pages_paths = data["pages"]
book.call("_prepare_book_content")
if "cover_front_out" in data: book.tex_cover_front_out = data["cover_front_out"]
if "cover_front_in" in data: book.tex_cover_front_in = data["cover_front_in"]
if "cover_back_in" in data: book.tex_cover_back_in = data["cover_back_in"]
if "cover_back_out" in data: book.tex_cover_back_out = data["cover_back_out"]
if "spine_col" in data: book.spine_color = data["spine_col"]
if "spine_width" in data:
book.spine_width = data["spine_width"]
book.call("_build_spine")
if "size" in data:
book.target_page_size = data["size"]
book.call("_apply_new_size")
book.call("_update_static_visuals_immediate")
book.call("_update_volume_visuals")
## Configures the physics simulation of the page turning effect.
static func configure_physics(book: PageFlip2D, data: Dictionary) -> void:
if not is_instance_valid(book) or not book.dynamic_poly: return
var rigger = book.dynamic_poly
for key in data.keys():
rigger.set(key, data[key])
if rigger.has_method("rebuild"):
rigger.rebuild(book.target_page_size)

View File

@@ -0,0 +1 @@
uid://c2dx1dksgeey6

1416
addons/PageFlip/page_flip.gd Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
uid://ctw83iqgqxjjt

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,587 @@
@tool
class_name DynamicPage
extends Polygon2D
# ==============================================================================
# ENUMS & CONSTANTS
# ==============================================================================
## Defines the material physical properties for the page simulation.
enum PagePreset {
CUSTOM,
DEFAULT,
STANDARD_PAPER,
HEAVY_GRIMOIRE,
LIGHT_MAGAZINE,
OLD_SCROLL,
RIGID_BOARD,
WET_CLOTH,
PLASTIC_SHEET,
METAL_PLATE
}
## Determines the direction of the page turn curve.
enum CurlMode {
STRAIGHT,
TOP_CORNER_FIRST,
BOTTOM_CORNER_FIRST
}
const SKELETON_NAME = "AutoSkeleton"
const SHADOW_NAME = "AutoShadow"
# ==============================================================================
# SIGNALS
# ==============================================================================
signal change_page_requested
signal end_animation
# ==============================================================================
# INTERNAL CACHE
# ==============================================================================
var _custom_cache: Dictionary = {}
# ==============================================================================
# CONFIGURATION
# ==============================================================================
@export_category("Rig Generator")
## Horizontal mesh subdivision. Higher values create smoother bends.
@export_range(1, 20) var subdivision_x: int = 8
## Vertical mesh subdivision. Higher values allow for better corner curling.
@export_range(1, 10) var subdivision_y: int = 5
## Generates the mesh, skeleton, weights and animations.
@export var rebuild_all: bool = false : set = _on_generate_pressed
@export_category("Animation Generator")
@export_group("Quick Presets")
## Select a material style to auto-configure physics.
@export var animation_preset: PagePreset = PagePreset.CUSTOM : set = _on_preset_changed
@export_group("Manual Configuration")
## The AnimationPlayer node to store the generated animations.
@export var anim_player: AnimationPlayer
## Duration of the page turn animation in seconds.
@export var anim_duration: float = 0.75
@export_subgroup("Paper Physics")
## Stiffness of the material. 0.5 = Rubber, 5.0 = Wood.
@export_range(0.5, 5.0, 0.1) var paper_stiffness: float = 2.0
## Bend angle during the lift phase (Negative = Tip drags down).
@export_range(-180.0, 180.0) var lift_bend: float = -10.0
## Bend angle during the landing phase (Negative = Tip floats up).
@export_range(-180.0, 180.0) var land_bend: float = -8.0
@export_subgroup("Curl Effect")
## Which part of the page initiates the movement.
@export var curl_mode: CurlMode = CurlMode.TOP_CORNER_FIRST
## Time delay for the trailing corner (0.0 = None, 1.0 = High lag).
@export_range(0.0, 1.0, 0.05) var curl_lag: float = 0.8
@export_subgroup("Shadow FX")
## Create a dynamic shadow behind the page (Background Shadow).
@export var enable_shadow: bool = false
## The gradient texture used for the back shadow.
@export var shadow_gradient: GradientTexture2D
@export_subgroup("Fine Timing")
## Normalized time (0.0 - 1.0) for the peak lift position.
@export_range(0.05, 0.45, 0.01) var timing_peak_lift: float = 0.15
## Normalized time (0.0 - 1.0) for the landing contact.
@export_range(0.55, 0.95, 0.01) var timing_peak_land: float = 0.85
@export var generate_anims_btn: bool = false : set = _on_anim_pressed
# ==============================================================================
# RUNTIME INITIALIZATION
# ==============================================================================
func _ready():
self.z_index = 10
if animation_preset == PagePreset.CUSTOM:
_save_state_to_cache()
func rebuild(current_page_size: Vector2 = Vector2.ZERO) -> void:
if not anim_player: return
_clean_previous_rig()
_create_rig_logic(current_page_size)
_generate_animations_logic()
# ==============================================================================
# PRESET LOGIC
# ==============================================================================
func _on_preset_changed(val):
if animation_preset == PagePreset.CUSTOM:
_save_state_to_cache()
animation_preset = val
if val == PagePreset.CUSTOM:
if not _custom_cache.is_empty():
_load_state_from_cache()
print("[PageRigger] Restored Custom Settings.")
notify_property_list_changed()
return
# Defaults
curl_mode = CurlMode.BOTTOM_CORNER_FIRST
curl_lag = 0.3
enable_shadow = false
anim_duration = 0.75
match val:
PagePreset.DEFAULT:
paper_stiffness = 2.0; lift_bend = -15.0; land_bend = 5.0
curl_mode = CurlMode.TOP_CORNER_FIRST; curl_lag = 0.5
anim_duration = 0.75; timing_peak_lift = 0.15; timing_peak_land = 0.85
PagePreset.STANDARD_PAPER:
paper_stiffness = 2.5; lift_bend = -12.0; land_bend = 3.0
curl_mode = CurlMode.TOP_CORNER_FIRST; curl_lag = 0.4
anim_duration = 0.65; timing_peak_lift = 0.12; timing_peak_land = 0.88
PagePreset.HEAVY_GRIMOIRE:
paper_stiffness = 4.0; lift_bend = -3.0; land_bend = 1.0
curl_mode = CurlMode.STRAIGHT; curl_lag = 0.1
anim_duration = 1.1; timing_peak_lift = 0.25; timing_peak_land = 0.75
PagePreset.LIGHT_MAGAZINE:
paper_stiffness = 0.8; lift_bend = -16.0; land_bend = 0.0
curl_mode = CurlMode.BOTTOM_CORNER_FIRST; curl_lag = 0.6
anim_duration = 0.8; timing_peak_lift = 0.10; timing_peak_land = 0.92
PagePreset.OLD_SCROLL:
paper_stiffness = 1.8; lift_bend = -20.0; land_bend = 8.0
curl_mode = CurlMode.TOP_CORNER_FIRST; curl_lag = 0.9
anim_duration = 0.9; timing_peak_lift = 0.18; timing_peak_land = 0.82
PagePreset.RIGID_BOARD:
paper_stiffness = 5.0; lift_bend = 0.0; land_bend = 0.0
curl_mode = CurlMode.STRAIGHT; curl_lag = 0.0
anim_duration = 0.8; timing_peak_lift = 0.2; timing_peak_land = 0.8
PagePreset.WET_CLOTH:
paper_stiffness = 0.3; lift_bend = -28.0; land_bend = -0.0
curl_mode = CurlMode.BOTTOM_CORNER_FIRST; curl_lag = 0.6
anim_duration = 1.3; timing_peak_lift = 0.2; timing_peak_land = 0.95
PagePreset.PLASTIC_SHEET:
paper_stiffness = 3.5; lift_bend = -12.0; land_bend = 12.0
curl_mode = CurlMode.TOP_CORNER_FIRST; curl_lag = 0.2
anim_duration = 0.5; timing_peak_lift = 0.08; timing_peak_land = 0.92
PagePreset.METAL_PLATE:
paper_stiffness = 5.0; lift_bend = 0.0; land_bend = 0.0
curl_mode = CurlMode.STRAIGHT; curl_lag = 0.0
anim_duration = 1.4; timing_peak_lift = 0.15; timing_peak_land = 0.98
notify_property_list_changed()
if anim_player and skeleton != NodePath(""):
_generate_animations_logic()
print("[PageRigger] Preset Applied: ", PagePreset.keys()[val])
func _save_state_to_cache():
_custom_cache = {
"stiffness": paper_stiffness, "lift": lift_bend, "land": land_bend,
"curl_m": curl_mode, "curl_l": curl_lag,
"t_lift": timing_peak_lift, "t_land": timing_peak_land,
"dur": anim_duration, "shadow": enable_shadow
}
func _load_state_from_cache():
paper_stiffness = _custom_cache.get("stiffness", 1.5)
lift_bend = _custom_cache.get("lift", -30.0)
land_bend = _custom_cache.get("land", -15.0)
curl_mode = _custom_cache.get("curl_m", CurlMode.BOTTOM_CORNER_FIRST)
curl_lag = _custom_cache.get("curl_l", 0.3)
timing_peak_lift = _custom_cache.get("t_lift", 0.15)
timing_peak_land = _custom_cache.get("t_land", 0.85)
anim_duration = _custom_cache.get("dur", 1.0)
enable_shadow = false # Force disable shadow
# ==============================================================================
# RIGGING LOGIC
# ==============================================================================
func _calculate_polygon_rect() -> Rect2:
if polygon.size() == 0:
return Rect2(0,0,0,0)
var min_v = polygon[0]
var max_v = polygon[0]
for v in polygon:
min_v.x = min(min_v.x, v.x)
min_v.y = min(min_v.y, v.y)
max_v.x = max(max_v.x, v.x)
max_v.y = max(max_v.y, v.y)
return Rect2(min_v, max_v - min_v)
func _create_rig_logic(current_page_size: Vector2 = Vector2.ZERO):
self.z_index = 10
var original_size = Vector2(512, 820)
var tex_size: Vector2 = Vector2.ZERO
if current_page_size != Vector2.ZERO:
tex_size = current_page_size
elif texture:
tex_size = texture.get_size()
if tex_size == Vector2.ZERO:
tex_size = original_size
var step_x = tex_size.x / subdivision_x
var step_y = tex_size.y / subdivision_y
# Prepare Gradient Texture for Background Shadow
var applied_shadow_tex: Texture2D = null
if not shadow_gradient:
shadow_gradient = preload("uid://rm7bporlv3cf")
var dup = shadow_gradient.duplicate()
if dup is GradientTexture2D:
dup.width = int(tex_size.x)
applied_shadow_tex = dup
# --- 0. BACKGROUND SHADOW ---
if enable_shadow:
var shadow = Polygon2D.new()
shadow.name = SHADOW_NAME
shadow.z_index = -1
if applied_shadow_tex:
shadow.texture = applied_shadow_tex
shadow.color = Color.WHITE
else:
shadow.color = Color(0, 0, 0, 0.5)
var margin = 4.0
shadow.polygon = PackedVector2Array([
Vector2(margin, margin), Vector2(tex_size.x - margin, margin),
Vector2(tex_size.x - margin, tex_size.y - margin), Vector2(margin, tex_size.y - margin)
])
shadow.uv = shadow.polygon
add_child(shadow)
# --- 1. MESH & UV ---
var new_uvs = PackedVector2Array()
for y in range(subdivision_y + 1):
for x in range(subdivision_x + 1):
new_uvs.append(Vector2(x * step_x, y * step_y))
self.uv = new_uvs
self.polygon = new_uvs
var new_polygons = []
var rc = subdivision_x + 1
for y in range(subdivision_y):
for x in range(subdivision_x):
var i = y * rc + x
new_polygons.append(PackedInt32Array([i, i + 1, i + rc + 1, i + rc]))
self.polygons = new_polygons
# --- 2. SKELETON ---
var sk = Skeleton2D.new()
sk.name = SKELETON_NAME
add_child(sk)
self.skeleton = NodePath(SKELETON_NAME)
# --- 3. BONES (Structured Hierarchy) ---
for y in range(subdivision_y + 1):
var parent_bone: Bone2D = null
for x in range(subdivision_x + 1):
var b = Bone2D.new()
b.name = "Bone_%d_%d" % [x, y]
b.set_autocalculate_length_and_angle(false)
b.set_length(step_x)
if x == 0:
# Root bone (Spine)
sk.add_child(b)
b.position = Vector2(0, y * step_y)
else:
# Child bone
if parent_bone:
parent_bone.add_child(b)
b.position = Vector2(step_x, 0)
b.set_rest(b.transform)
parent_bone = b
# --- 4. WEIGHTS (Gradient Skinning) ---
_apply_weights_to_polygon(self, sk)
queue_redraw()
notify_property_list_changed()
func _apply_weights_to_polygon(poly: Polygon2D, sk: Skeleton2D):
poly.clear_bones()
var bone_map = {}
for y in range(subdivision_y + 1):
for x in range(subdivision_x + 1):
var bname = "Bone_%d_%d" % [x, y]
var bone_node = sk.find_child(bname, true, false)
if bone_node:
bone_map[bname] = bone_node.get_path()
var vertex_count = poly.polygon.size()
var weights_per_bone = {}
for bpath in bone_map.values():
var w_arr = PackedFloat32Array()
w_arr.resize(vertex_count)
w_arr.fill(0.0)
weights_per_bone[bpath] = w_arr
var vert_idx = 0
for y in range(subdivision_y + 1):
for x in range(subdivision_x + 1):
var primary_name = "Bone_%d_%d" % [x, y]
var prev_name = "Bone_%d_%d" % [x - 1, y]
var next_name = "Bone_%d_%d" % [x + 1, y]
var primary_path = bone_map.get(primary_name)
var prev_path = bone_map.get(prev_name)
var next_path = bone_map.get(next_name)
var w_center = 0.7
var w_neighbor = 0.15
if primary_path: weights_per_bone[primary_path][vert_idx] += w_center
if prev_path: weights_per_bone[prev_path][vert_idx] += w_neighbor
else: weights_per_bone[primary_path][vert_idx] += w_neighbor
if next_path: weights_per_bone[next_path][vert_idx] += w_neighbor
else: weights_per_bone[primary_path][vert_idx] += w_neighbor
vert_idx += 1
for bpath in weights_per_bone:
poly.add_bone(bpath, weights_per_bone[bpath])
# ==============================================================================
# ANIMATION LOGIC
# ==============================================================================
func _generate_animations_logic():
var library: AnimationLibrary
if anim_player.has_animation_library(""): library = anim_player.get_animation_library("")
else: library = AnimationLibrary.new(); anim_player.add_animation_library("", library)
_create_single_anim(library, "turn_flexible_page", false, false)
_create_single_anim(library, "turn_rigid_page", true, false)
_create_single_anim(library, "turn_flexible_page_mirror", false, true)
_create_single_anim(library, "turn_rigid_page_mirror", true, true)
notify_property_list_changed()
func _create_single_anim(library: AnimationLibrary, anim_name: String, is_rigid: bool, is_mirror: bool):
var anim = Animation.new()
anim.length = anim_duration
anim.step = 0.01
if library.has_animation(anim_name): library.remove_animation(anim_name)
library.add_animation(anim_name, anim)
var sk_node = find_child(SKELETON_NAME, true, false)
if not sk_node: return
var my_path = anim_player.get_node(anim_player.root_node).get_path_to(self)
var visible_col = Color(1, 1, 1, 1)
var invisible_col = Color(1, 1, 1, 0)
var faded_col = Color(1, 1, 1, 0.6)
var t_mid = anim_duration * 0.5
var t_end_snap = anim_duration * 0.95
# ==========================================================================
# 1. VISIBILITY & Z-INDEX
# ==========================================================================
var z_track = anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(z_track, str(my_path) + ":z_index")
anim.track_set_interpolation_type(z_track, Animation.INTERPOLATION_NEAREST)
anim.track_insert_key(z_track, 0.0, 10)
anim.track_insert_key(z_track, anim_duration * 0.15, 25)
anim.track_insert_key(z_track, anim_duration * 0.65, 10)
# ==========================================================================
# 2. BACKGROUND SHADOW
# ==========================================================================
var shadow_node = find_child(SHADOW_NAME, true, false)
if shadow_node and enable_shadow:
var shadow_path = anim_player.get_node(anim_player.root_node).get_path_to(shadow_node)
var t_scale = anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(t_scale, str(shadow_path) + ":scale")
anim.track_set_interpolation_type(t_scale, Animation.INTERPOLATION_CUBIC)
var t_mod = anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(t_mod, str(shadow_path) + ":modulate")
var s_start = Vector2(-1.0, 1.0) if is_mirror else Vector2(1.0, 1.0)
var s_end = Vector2(1.0, 1.0) if is_mirror else Vector2(-1.0, 1.0)
anim.track_insert_key(t_scale, 0.0, s_start)
anim.track_insert_key(t_mod, 0.0, invisible_col)
anim.track_insert_key(t_mod, anim_duration * 0.1, visible_col)
anim.track_insert_key(t_scale, t_mid, Vector2(0.01, 1.0))
anim.track_insert_key(t_mod, t_mid, faded_col)
anim.track_insert_key(t_scale, t_end_snap, s_end)
anim.track_insert_key(t_scale, anim_duration, s_end)
anim.track_insert_key(t_mod, t_end_snap, invisible_col)
anim.track_insert_key(t_mod, anim_duration, invisible_col)
# ==========================================================================
# 3. BONE ANIMATION
# ==========================================================================
for y in range(subdivision_y + 1):
var row_factor = 1.0
if not is_rigid and curl_mode != CurlMode.STRAIGHT:
var y_ratio = float(y) / float(max(1, subdivision_y))
if curl_mode == CurlMode.TOP_CORNER_FIRST:
row_factor = lerp(1.0, 1.0 - curl_lag, y_ratio)
elif curl_mode == CurlMode.BOTTOM_CORNER_FIRST:
row_factor = lerp(1.0 - curl_lag, 1.0, y_ratio)
var time_offset = (1.0 - row_factor) * (anim_duration * 0.1)
for x in range(subdivision_x + 1):
var bone_name = "Bone_%d_%d" % [x, y]
var bone = sk_node.find_child(bone_name, true, false)
if not bone: continue
var t_idx = anim.add_track(Animation.TYPE_VALUE)
var path = anim_player.get_node(anim_player.root_node).get_path_to(bone)
anim.track_set_path(t_idx, str(path) + ":rotation_degrees")
anim.track_set_interpolation_type(t_idx, Animation.INTERPOLATION_CUBIC)
var deg_flat_right = 0.0; var deg_flat_left = -179.9; var deg_mid = -90.0
if is_rigid:
if x == 0:
var s = deg_flat_left if is_mirror else deg_flat_right
var e = deg_flat_right if is_mirror else deg_flat_left
anim.track_insert_key(t_idx, 0.0, s)
anim.track_insert_key(t_idx, anim_duration * 0.5, deg_mid)
anim.track_insert_key(t_idx, anim_duration, e)
else:
anim.track_insert_key(t_idx, 0.0, 0.0)
anim.track_insert_key(t_idx, anim_duration, 0.0)
else:
var x_ratio = float(x) / float(subdivision_x)
var influence = pow(x_ratio, paper_stiffness)
var t_lift = clamp((anim_duration * timing_peak_lift) + time_offset, 0.0, (anim_duration * 0.5) - 0.05)
var t_mid_bone = anim_duration * 0.5
var t_land = clamp((anim_duration * timing_peak_land) - time_offset, t_mid_bone + 0.05, anim_duration)
if x == 0:
# ROOT BONE
var s = deg_flat_left if is_mirror else deg_flat_right
var e = deg_flat_right if is_mirror else deg_flat_left
var sl = -15.0 * row_factor
if is_mirror: sl = deg_flat_left - sl
var slt = lerp(-90.0, -180.0, timing_peak_land)
if is_mirror: slt = lerp(-90.0, 0.0, timing_peak_land)
anim.track_insert_key(t_idx, 0.0, s)
anim.track_insert_key(t_idx, t_lift, sl)
anim.track_insert_key(t_idx, t_mid_bone, deg_mid)
anim.track_insert_key(t_idx, t_land, slt)
var t_settle = lerp(t_land, anim_duration, 0.7)
var s_settle = lerp(slt, e, 0.85)
anim.track_insert_key(t_idx, t_settle, s_settle)
anim.track_insert_key(t_idx, anim_duration, e)
else:
# SURFACE BONES (CURVED)
var bm = -1.0 if is_mirror else 1.0
anim.track_insert_key(t_idx, 0.0, 0.0)
anim.track_insert_key(t_idx, t_lift, lift_bend * row_factor * bm * influence)
# REMOVED FORCED 0.0 AT MIDPOINT FOR ORGANIC CURVE
var val_land = land_bend * row_factor * bm * influence
anim.track_insert_key(t_idx, t_land, val_land)
var t_settle = lerp(t_land, anim_duration, 0.75)
anim.track_insert_key(t_idx, t_settle, val_land * 0.15)
anim.track_insert_key(t_idx, anim_duration, 0.0)
# ==========================================================================
# 4. MAIN POLYGON SCALE ("SQUASH & STRETCH")
# ==========================================================================
if not is_rigid:
var t_main_scale = anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(t_main_scale, str(my_path) + ":scale")
anim.track_set_interpolation_type(t_main_scale, Animation.INTERPOLATION_LINEAR)
var squash_x = 0.88
anim.track_insert_key(t_main_scale, 0.0, Vector2(1, 1))
anim.track_insert_key(t_main_scale, t_mid, Vector2(1, 1))
anim.track_insert_key(t_main_scale, t_mid + (anim_duration * 0.1), Vector2(squash_x, 1))
anim.track_insert_key(t_main_scale, anim_duration * 0.85, Vector2(1, 1))
anim.track_insert_key(t_main_scale, anim_duration, Vector2(1, 1))
var method_track = anim.add_track(Animation.TYPE_METHOD)
anim.track_set_path(method_track, str(my_path))
anim.track_insert_key(method_track, anim_duration * 0.5, {"method": "_trigger_midpoint", "args": []})
anim.track_insert_key(method_track, anim_duration, {"method": "_trigger_end", "args": []})
# ==============================================================================
# HELPER FUNCTIONS
# ==============================================================================
func _trigger_midpoint(): emit_signal("change_page_requested")
func _trigger_end(): emit_signal("end_animation")
func _on_generate_pressed(val):
if val:
rebuild_all=false
_clean_previous_rig()
_create_rig_logic()
_generate_animations_logic()
func _on_anim_pressed(val):
if val:
generate_anims_btn=false
if anim_player and skeleton!=NodePath(""):
_generate_animations_logic()
func _clean_previous_rig():
for c in get_children():
remove_child(c)
c.free()
var old_shadow = find_child(SHADOW_NAME, true, false)
if old_shadow: old_shadow.free()
clear_bones()
skeleton = NodePath("")

View File

@@ -0,0 +1 @@
uid://ce5ib2f4qff24