@tool extends "../base_sprite_resource_creator.gd" var _DEFAULT_ANIMATION_LIBRARY = "" # GLOBAL func create_animations(target_node: Node, player: AnimationPlayer, aseprite_files: Dictionary, options: Dictionary): var result = _import(target_node, player, aseprite_files, options) if result != result_code.SUCCESS: printerr(result_code.get_error_message(result)) func _import(target_node: Node, player: AnimationPlayer, aseprite_files: Dictionary, options: Dictionary): var source_file = aseprite_files.data_file var sprite_sheet = aseprite_files.sprite_sheet var data = _aseprite_file_exporter.load_json_content(source_file) if not data.is_ok: return data.code var content = data.content var context = {} if target_node is CanvasItem: target_node.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST else: target_node.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST _setup_texture(target_node, sprite_sheet, content, context, options.slice != "") var result = _configure_animations(target_node, player, content, context, options) if result != result_code.SUCCESS: return result return _cleanup_animations(target_node, player, content, options) func _load_texture(sprite_sheet: String) -> Texture2D: var texture = ResourceLoader.load(sprite_sheet, 'Image', ResourceLoader.CACHE_MODE_IGNORE) texture.take_over_path(sprite_sheet) return texture func _configure_animations(target_node: Node, player: AnimationPlayer, content: Dictionary, context: Dictionary, options: Dictionary): var frames = _aseprite.get_content_frames(content) var slice_rect = null if options.slice != "": options["slice_rect"] = _aseprite.get_slice_rect(content, options.slice) if not player.has_animation_library(_DEFAULT_ANIMATION_LIBRARY): player.add_animation_library(_DEFAULT_ANIMATION_LIBRARY, AnimationLibrary.new()) if content.meta.has("frameTags") and content.meta.frameTags.size() > 0: var result = result_code.SUCCESS for tag in content.meta.frameTags: var selected_frames = frames.slice(tag.from, tag.to + 1) result = _add_animation_frames(target_node, player, tag.name, selected_frames, context, options, tag.direction, int(tag.get("repeat", -1))) if result != result_code.SUCCESS: break return result else: return _add_animation_frames(target_node, player, "default", frames, context, options) func _add_animation_frames(target_node: Node, player: AnimationPlayer, anim_name: String, frames: Array, context: Dictionary, options: Dictionary, direction = 'forward', repeat = -1): var animation_name = anim_name var library_name = _DEFAULT_ANIMATION_LIBRARY var is_loopable = _config.is_default_animation_loop_enabled() var slice_rect = options.get("slice_rect") var is_importing_slice: bool = slice_rect != null var anim_tokens := anim_name.split("/") if anim_tokens.size() > 2: push_error("Invalid animation name: %s" % animation_name) return elif anim_tokens.size() == 2: library_name = anim_tokens[0] animation_name = anim_tokens[1] if not _validate_animation_name(animation_name): push_error("Invalid animation name: %s" % animation_name) return # Create library if doesn't exist if library_name != _DEFAULT_ANIMATION_LIBRARY and not player.has_animation_library(library_name): player.add_animation_library(library_name, AnimationLibrary.new()) # Check loop if animation_name.begins_with(_config.get_animation_loop_exception_prefix()): animation_name = animation_name.substr(_config.get_animation_loop_exception_prefix().length()) is_loopable = not is_loopable # Add library if not player.get_animation_library(library_name).has_animation(animation_name): player.get_animation_library(library_name).add_animation(animation_name, Animation.new()) var full_name = ( animation_name if library_name == "" else "%s/%s" % [library_name, animation_name] ) var animation = player.get_animation(full_name) _cleanup_tracks(target_node, player, animation) _create_meta_tracks(target_node, player, animation) var frame_track = _get_property_track_path(player, target_node, _get_frame_property(is_importing_slice)) var frame_track_index = _create_track(target_node, animation, frame_track) if direction == "reverse" or direction == "pingpong_reverse": frames.reverse() var animation_length = 0 var repetition = 1 if repeat != -1: is_loopable = false repetition = repeat for i in range(repetition): for frame in frames: var frame_key = _get_frame_key(target_node, frame, context, slice_rect) animation.track_insert_key(frame_track_index, animation_length, frame_key) animation_length += frame.duration / 1000 # Godot 4 has an Animation.LOOP_PINGPONG mode, however it does not # behave like in Aseprite, so I'm keeping the custom implementation if direction.begins_with("pingpong"): var working_frames = frames.duplicate() working_frames.remove_at(working_frames.size() - 1) if is_loopable or (repetition > 1 and i < repetition - 1): working_frames.remove_at(0) working_frames.reverse() for frame in working_frames: var frame_key = _get_frame_key(target_node, frame, context, slice_rect) animation.track_insert_key(frame_track_index, animation_length, frame_key) animation_length += frame.duration / 1000 # if keep_anim_length is enabled only adjust length if # - there aren't other tracks besides metas and frame # - the current animation is shorter than new one if not options.keep_anim_length or (animation.get_track_count() == (_get_meta_prop_names().size() + 1) or animation.length < animation_length): animation.length = animation_length animation.loop_mode = Animation.LOOP_LINEAR if is_loopable else Animation.LOOP_NONE return result_code.SUCCESS const _INVALID_TOKENS := ["/", ":", ",", "["] func _validate_animation_name(name: String) -> bool: return not _INVALID_TOKENS.any(func(token: String): return token in name) func _create_track(target_node: Node, animation: Animation, track: String): var track_index = animation.find_track(track, Animation.TYPE_VALUE) if track_index != -1: animation.remove_track(track_index) track_index = animation.add_track(Animation.TYPE_VALUE) animation.track_set_path(track_index, track) animation.track_set_interpolation_loop_wrap(track_index, false) animation.value_track_set_update_mode(track_index, Animation.UPDATE_DISCRETE) return track_index func _get_property_track_path(player: AnimationPlayer, target_node: Node, prop: String) -> String: var node_path = player.get_node(player.root_node).get_path_to(target_node) return "%s:%s" % [node_path, prop] func _cleanup_animations(target_node: Node, player: AnimationPlayer, content: Dictionary, options: Dictionary): if not (content.meta.has("frameTags") and content.meta.frameTags.size() > 0): return result_code.SUCCESS _remove_unused_animations(content, player) if options.get("cleanup_hide_unused_nodes", false): _hide_unused_nodes(target_node, player, content) return result_code.SUCCESS func _remove_unused_animations(content: Dictionary, player: AnimationPlayer): pass # FIXME it's not removing unused animations anymore. Sample impl bellow # var tags = ["RESET"] # for t in content.meta.frameTags: # var a = t.name # if a.begins_with(_config.get_animation_loop_exception_prefix()): # a = a.substr(_config.get_animation_loop_exception_prefix().length()) # tags.push_back(a) # var track = _get_frame_track_path(player, sprite) # for a in player.get_animation_list(): # if tags.has(a): # continue # # var animation = player.get_animation(a) # if animation.get_track_count() != 1: # var t = animation.find_track(track) # if t != -1: # animation.remove_track(t) # continue # # if animation.find_track(track) != -1: # player.remove_animation(a) func _hide_unused_nodes(target_node: Node, player: AnimationPlayer, content: Dictionary): var root_node := player.get_node(player.root_node) var all_animations := player.get_animation_list() var all_sprite_nodes := [] var animation_sprites := {} for a in all_animations: var animation := player.get_animation(a) var sprite_nodes := [] for track_idx in animation.get_track_count(): var raw_path := animation.track_get_path(track_idx) if raw_path.get_subname(0) == "visible": continue var path := _remove_properties_from_path(raw_path) var sprite_node := root_node.get_node(path) if !(sprite_node is Sprite2D || sprite_node is Sprite3D): continue if sprite_nodes.has(sprite_node): continue sprite_nodes.append(sprite_node) animation_sprites[animation] = sprite_nodes for sn in sprite_nodes: if all_sprite_nodes.has(sn): continue all_sprite_nodes.append(sn) for animation in animation_sprites: var sprite_nodes : Array = animation_sprites[animation] for node in all_sprite_nodes: if sprite_nodes.has(node): continue var visible_track = _get_property_track_path(player, node, "visible") if animation.find_track(visible_track, Animation.TYPE_VALUE) != -1: continue var visible_track_index = _create_track(node, animation, visible_track) animation.track_insert_key(visible_track_index, 0, false) func list_layers(file: String, only_visibles = false) -> Array: return _aseprite.list_layers(file, only_visibles) func list_slices(file: String) -> Array: return _aseprite.list_slices(file) func _remove_properties_from_path(path: NodePath) -> NodePath: var string_path := path as String if !(":" in string_path): return string_path as NodePath var property_path := path.get_concatenated_subnames() as String string_path = string_path.substr(0, string_path.length() - property_path.length() - 1) return string_path as NodePath func _create_meta_tracks(target_node: Node, player: AnimationPlayer, animation: Animation): for prop in _get_meta_prop_names(): var track = _get_property_track_path(player, target_node, prop) var track_index = _create_track(target_node, animation, track) animation.track_insert_key(track_index, 0, true if prop == "visible" else target_node.get(prop)) func _cleanup_tracks(target_node: Node, player: AnimationPlayer, animation: Animation): for track_key in ["texture", "hframes", "vframes", "region_rect", "frame"]: var track = _get_property_track_path(player, target_node, track_key) var track_index = animation.find_track(track, Animation.TYPE_VALUE) if track_index != -1: animation.remove_track(track_index) func _setup_texture(target_node: Node, sprite_sheet: String, content: Dictionary, context: Dictionary, is_importing_slice: bool): push_error("_setup_texture not implemented!") func _get_frame_property(is_importing_slice: bool) -> String: push_error("_get_frame_property not implemented!") return "" func _get_frame_key(target_node: Node, frame: Dictionary, context: Dictionary, slice_info: Variant): push_error("_get_frame_key not implemented!") func _get_meta_prop_names(): push_error("_get_meta_prop_names not implemented!")