Download scripts/modules/timeline.gd from geter62/monkeystudio: direct link, hf CLI and curl.
- Browser
- Download file 4.95 kB
-
https://huggingface.co/geter62/monkeystudio/resolve/main/scripts/modules/timeline.gd
- Command line
-
hf download hf://geter62/monkeystudio/scripts/modules/timeline.gd
-
curl -L -o timeline.gd https://huggingface.co/geter62/monkeystudio/resolve/main/scripts/modules/timeline.gd
4.95 kB
| extends Node | |
| ## | |
| ## TimelineModule - keyframe timeline for objects and bones (Features 81-100). | |
| ## | |
| ## Backed by a real Godot Animation resource + AnimationPlayer, so playback, | |
| ## interpolation and looping use engine-native code (fast, cached). The module | |
| ## exposes a touch-friendly API: scrub(), add_keyframe(), play(), pause(). | |
| ## | |
| @export var duration: float = 5.0 | |
| @export var fps: int = 30 | |
| var _host: Node = null | |
| var _player: AnimationPlayer = null | |
| var _anim: Animation = null | |
| var _playhead: float = 0.0 | |
| var _playing := false | |
| var _key_count := 0 | |
| func _activate(host: Node) -> void: | |
| _host = host | |
| _ensure_player() | |
| print("[Timeline] activated (%.1fs @ %d fps)" % [duration, fps]) | |
| func _deactivate() -> void: | |
| _playing = false | |
| _player = null | |
| _anim = null | |
| _host = null | |
| func _ensure_player() -> void: | |
| if _player != null and is_instance_valid(_player): | |
| return | |
| _anim = Animation.new() | |
| _anim.length = duration | |
| _anim.step = 1.0 / float(fps) | |
| _player = AnimationPlayer.new() | |
| _player.name = "TimelinePlayer" | |
| if _host != null: | |
| _host.add_child(_player) | |
| var lib := AnimationLibrary.new() | |
| lib.add_animation("main", _anim) | |
| _player.add_animation_library("", lib) | |
| _player.animation_finished.connect(_on_finished) | |
| func _process(delta: float) -> void: | |
| if not _playing or _player == null: | |
| return | |
| _playhead = _player.current_animation_position | |
| StudioBus.timeline_playhead.emit(_playhead) | |
| ## --- Playback --------------------------------------------------------------- | |
| func play(from: float = -1.0) -> void: | |
| _ensure_player() | |
| if from >= 0.0: | |
| _player.seek(from, true) | |
| _player.play("main") | |
| _playing = true | |
| StudioBus.playback_state_changed.emit(true) | |
| func pause() -> void: | |
| if _player != null: | |
| _player.pause() | |
| _playing = false | |
| StudioBus.playback_state_changed.emit(false) | |
| func stop() -> void: | |
| if _player != null: | |
| _player.stop() | |
| _playhead = 0.0 | |
| _playing = false | |
| StudioBus.playback_state_changed.emit(false) | |
| ## Jump the playhead to `time` seconds (scrubbing). | |
| func scrub(time: float) -> void: | |
| _ensure_player() | |
| time = clampf(time, 0.0, duration) | |
| _playhead = time | |
| _player.seek(time, true) | |
| StudioBus.timeline_playhead.emit(time) | |
| func is_playing() -> bool: | |
| return _playing | |
| func playhead() -> float: | |
| return _playhead | |
| ## --- Keyframes -------------------------------------------------------------- | |
| ## Add a keyframe for a `property` (NodePath-style) on an object at `time`. | |
| ## For bones use property like "bones/Head:position" (Skeleton3D pose tracks). | |
| func add_keyframe(target_node: Node3D, property: String, time: float, value: Variant) -> void: | |
| _ensure_player() | |
| time = clampf(time, 0.0, duration) | |
| var path := _player.get_path_to(target_node) | |
| if path == NodePath(): | |
| push_warning("Timeline: target is not under the player root") | |
| return | |
| var track := _find_or_create_track(String(path), property, value) | |
| var key_idx := _anim.track_insert_key(track, time, value, 0.0) | |
| if key_idx >= 0: | |
| _key_count += 1 | |
| StudioBus.keyframe_added.emit(String(path), property, time) | |
| StudioBus.scene_modified.emit() | |
| func _find_or_create_track(node_path: String, property: String, value: Variant) -> int: | |
| var full := NodePath(node_path + ":" + property) | |
| for i in range(_anim.get_track_count()): | |
| if _anim.track_get_path(i) == full: | |
| return i | |
| var type := Animation.TYPE_VALUE | |
| if value is Transform3D: | |
| type = Animation.TYPE_VALUE | |
| _anim.add_track(type) | |
| var idx := _anim.get_track_count() - 1 | |
| _anim.track_set_path(idx, full) | |
| if value is Vector3: | |
| _anim.track_set_interpolation_type(idx, Animation.INTERPOLATION_CUBIC) | |
| return idx | |
| ## Convenience: key every transform channel of a bone of a Skeleton3D. | |
| ## Godot stores bone poses as 3D transforms, so we key position/rotation/scale. | |
| func add_bone_keyframe(skeleton: Skeleton3D, bone_name: String, time: float) -> void: | |
| if skeleton == null: | |
| return | |
| var bone_idx := skeleton.find_bone(bone_name) | |
| if bone_idx < 0: | |
| push_warning("Timeline: bone '%s' not found" % bone_name) | |
| return | |
| var skin_path := "%s:%s" % [_player.get_path_to(skeleton), bone_name] | |
| var pose := skeleton.get_bone_pose(bone_idx) | |
| add_keyframe(skeleton, "bones/" + bone_name + "/position", time, pose.origin) | |
| # Store rotation as quaternion for clean interpolation. | |
| add_keyframe(skeleton, "bones/" + bone_name + "/rotation", time, pose.basis.get_rotation_quaternion()) | |
| add_keyframe(skeleton, "bones/" + bone_name + "/scale", time, pose.basis.get_scale()) | |
| func delete_keyframe(track: int, key: int) -> void: | |
| if track >= 0 and track < _anim.get_track_count(): | |
| _anim.track_remove_key(track, key) | |
| _key_count = maxi(0, _key_count - 1) | |
| func track_count() -> int: | |
| return _anim.get_track_count() if _anim != null else 0 | |
| func key_count() -> int: | |
| return _key_count | |
| func set_duration(seconds: float) -> void: | |
| duration = maxf(0.1, seconds) | |
| _ensure_player() | |
| _anim.length = duration | |
| func _on_finished() -> void: | |
| _playing = false | |
| StudioBus.playback_state_changed.emit(false) | |