-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathAnimatedImage.gd
559 lines (442 loc) · 17.4 KB
/
AnimatedImage.gd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
extends AnimatedSprite2D
var base_animation_name : String = "default"
var compress := true
@export var pack_name : String = "":
get:
if not sprite_frames: return ""
return sprite_frames.pack_name
set(value):
if sprite_frames:
sprite_frames.pack_name = value
@export var active : bool = false:
get:
return active
set(on):
if active and on: return
if not active and not on: return
active = on
const max_speed : float = 10.0
const max_speed_fps : float = 30.0
var speed : float = 1.0 :
get:
return speed
set(value):
speed = value
_speeds[animation] = value
const percent_frames_for_skip = 0.02
const max_frame_skip = 10
var frame_skip = 10
var stretch := false
var mouse_controls := false
var paused : bool = false
var frame_counts : Dictionary
var current_frame : Dictionary
var _direction : float = 1.0
var _anim_fps : float = 0
var _requested_animation : bool = false # tracks if animation change has been requested
var _current_texture : Texture2D # reference to current texture
var _index : int
var _speeds : Dictionary # stores current speeds for each animation
var _tag_keys_pressed : Dictionary = {} # "key": bool where True is pressed
var _sprite : Sprite2D
var listening_for_tags := false
signal real_frame_changed(frame: int)
# Called when the node enters the scene tree for the first time.
func _ready():
#if not sprite_frames: return
_sprite = $NextImage
self.frame_changed.connect(_on_frame_changed)
var animations = sprite_frames.get_animation_names()
if animations.size() == 0:
print("No animations!")
if base_animation_name not in animations:
print("no default animation!")
if not sprite_frames.has_animation(base_animation_name) or sprite_frames.get_frame_count(base_animation_name) == 0:
print("no default or has no frames!")
# try to fix:
for anim_name in animations:
if sprite_frames.get_frame_count(anim_name) > 0:
print("recreating new base animation from ", anim_name)
sprite_frames.copy_frames(anim_name, base_animation_name)
break
# set up frame_counts and current_frame
var start_anim : String
for animation_name in animations:
_add_animation(animation_name)
if animation_name != base_animation_name:
start_anim = animation_name
# start with last animation
_init_animation(start_anim)
_change_animation(start_anim)
debug_info()
play(animation, _direction)
paused = false
# IMPORTANT: HACK: FIXME: the image of the animation sprite_frames is never actually shown,
# rather it is hidden behind the sprite NextImage, instead what we are interested in is
# the frame change. This means that per frame durations will not work, since the animation
# image may not match the sequence image. I need to write ImageFrames extension that handles
# this better, but currently this workaround works well enough.
func _on_frame_changed():
# update current frame
if not _requested_animation:
goto_frame(sequence().next(_direction))
# if seq.active_flags > 0 and seq.has_mapping():
# prints(current_frame[animation], seq.tags(current_frame[animation]))
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta):
if sprite_frames == null or frame_counts[animation] == 0: return
# slows things down but if images are all different sizes this can make them appear more similar
if is_playing() and stretch: # per frame
rescale()
# if Input.is_anything_pressed():
# handle_input()
func _mouse_control_speed() -> void:
var mpos := get_viewport().get_mouse_position()
var viewsize := get_viewport().get_visible_rect().size
var relative_dist = remap(mpos.x, 0, viewsize.x, -1.0, 1.0)
change_relative_speed_normalized(relative_dist)
get_viewport().set_input_as_handled()
#printt(viewsize.x, mpos.x, relative_dist)
func _unhandled_input(event : InputEvent):
if not active:
return
if mouse_controls:
# no mouse motion events handled here
if event is InputEventMouseMotion:
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
if Input.is_key_pressed(KEY_CTRL) or Input.is_key_pressed(KEY_ALT) or Input.is_key_pressed(KEY_SHIFT):
return
_mouse_control_speed()
return
if event is InputEventTargetedAction:
# note: do not check for pressed,
# as these events may have strength
# that changes throughout a "press"
#print(event.as_text())
match event.action:
"set_speed":
if valid_target(event.target):
set_speed(event.strength, event.target)
"set_flag":
var flag = int(event.target)
#printt("set_flag", flag)
if flag == 0:
print("Turn off flags")
sequence().active_flags = 0
elif sequence().has_mapping(flag):
prints("set_flag", sequence().mapping.flag_tag(flag))
sequence().active_flags = flag
if mouse_controls:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_RIGHT:
stop()
var w : float = float(get_viewport().get_visible_rect().size.x)
# NOTE: leave a small amount on each side the is always start and end of sequence
goto_frame(int(remap(get_viewport().get_mouse_position().x, 0.1 * w, 0.9 * w, 0, frame_counts[animation])))
#printt("jump to", get_viewport().get_mouse_position().x, frame)
# allow when playing or not:
if event.is_action_pressed("play_toggle", false, true):
if is_playing():
_pause()
else:
_resume()
if not is_playing():
# allow for frame skip
if event.is_action_pressed("skip_forward", true, true):
next_frame(1)
elif event.is_action_pressed("skip_backward", true, true):
next_frame(-1)
# other input requires scenes to be playing:
return
if mouse_controls:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
# no modifier keys pressed
if Input.is_key_pressed(KEY_CTRL) or Input.is_key_pressed(KEY_ALT) or Input.is_key_pressed(KEY_SHIFT):
return
_mouse_control_speed()
return # no other mouse events below
# handle tags:
# activate tags for all keys that were pressed while shift was held
if event is InputEventKey and is_instance_valid(sequence().mapping):
if event.keycode == KEY_SHIFT:
if event.pressed:
listening_for_tags = true
else:
listening_for_tags = false
# activate tags
var flags := 0
if _tag_keys_pressed.size() == 0:
# clear tags
prints("deactivating tags")
sequence().active_flags = 0
else:
for keycode in _tag_keys_pressed:
if _tag_keys_pressed[keycode]:
_tag_keys_pressed[keycode] = false
flags |= sequence().mapping.key_flag(keycode)
_tag_keys_pressed.clear()
prints("activating tags", sequence().mapping.flags_to_tags(flags))
sequence().active_flags = flags
return
elif event.shift_pressed: # shift is held
if event.pressed:
if sequence().mapping.key_exists(event.keycode):
_tag_keys_pressed[event.keycode] = true
prints("selecting tag", event.keycode)
get_viewport().set_input_as_handled()
return
# no shift modifier:
# repeatable actions:
if event.is_action_pressed("fast_forward", true, true): # allow echo
speed_scale = frame_skip * speed
play(animation, 1.0)
elif event.is_action_pressed("fast_backward", true, true): # allow echo
speed_scale = frame_skip * speed
play(animation, -1.0)
elif event.is_action_pressed("step_forward", true, true): # allow echo
next_frame(1)
elif event.is_action_pressed("step_backward", true, true): # allow echo
next_frame(-1)
# non-repeated actions:
elif event.is_action_pressed("skip_forward", false, true):
next_frame(frame_skip)
elif event.is_action_pressed("skip_backward", false, true):
next_frame(-frame_skip)
elif event.is_action_pressed("faster", false, true):
speed *= 1.05
print("speed:", speed)
speed_scale = speed
elif event.is_action_pressed("slower", false, true):
speed /= 1.05
print("speed:", speed)
speed_scale = speed
elif event.is_action_pressed("reverse", false, true):
reverse()
elif event.is_action_pressed("next_animation", false, true):
change_animation(next_animation(1))
elif event.is_action_pressed("speed_reset", false, true):
speed = 1.0
print("speed:", speed)
speed_scale = speed
elif event.is_action_pressed("random", false, true):
goto_frame(randi() % frame_counts[animation])
# on release
elif (event.is_action_released("fast_forward", true)
or event.is_action_released("fast_backward", true)):
# resume normal play
speed_scale = speed
if paused:
pause()
else:
play(animation, _direction)
func valid_target(index : int = -1) -> bool:
if index < 0 and not active: return false
if index >= 0 and index != _index : return false
return true
func change_animation_relative(direction : int, layer : int = -1) -> void:
if not valid_target(layer): return
change_animation(next_animation(direction))
func _init_animation(animation_name : String) -> void:
# ensure that values are there, and set as base animation if values missing
if animation_name in sprite_frames.sequences:
_anim_fps = sprite_frames.get_fps(animation_name)
if animation_name not in frame_counts:
frame_counts[animation_name] = sprite_frames.get_frame_count(animation_name)
else:
printt("Warning: init_animation:", animation_name, "not found, using base animation")
_anim_fps = sprite_frames.get_fps(base_animation_name)
if animation_name not in frame_counts:
frame_counts[animation_name] = sprite_frames.get_frame_count(base_animation_name)
if animation_name not in _speeds:
_speeds[animation_name] = 1.0
if animation_name not in current_frame:
current_frame[animation_name] = 0
assert(_anim_fps > 0)
frame_skip = mini(max_frame_skip, maxi(1, floor(frame_counts[animation_name] * percent_frames_for_skip))) # % of frames
speed = _speeds[animation_name]
speed_scale = speed
func _add_animation(animation_name : String) -> void:
if animation_name not in current_frame:
current_frame[animation_name] = 0
if animation_name not in _speeds:
_speeds[animation_name] = 1.0
# use base animation frame count as default (to support sequences)
var frame_count = sprite_frames.get_frame_count(base_animation_name)
if animation_name in sprite_frames.get_animation_names():
frame_count = sprite_frames.get_frame_count(animation_name)
if animation_name not in frame_counts or frame_counts[animation_name] <= 0:
frame_counts[animation_name] = frame_count
# always changes animation, regardless of current and state
func _change_animation(requested_animation : String) -> void:
printt(_index, "change animation to ", requested_animation, "_anim_fps:", _anim_fps, "frame_skip:", frame_skip, "speed:", speed)
if requested_animation not in sprite_frames.sequences:
printt(_index, "no animation found:", requested_animation)
return
# NOTE: when changing animations it signals frame_changed and sets the frame back to the start
_requested_animation = true # now that this is set, it won't update current_frame or signal real_frame_changed
animation = requested_animation
_requested_animation = false
print(animation, sprite_frames.sequences)
goto_frame(sequence().current_value())
_current_texture = sequence().get_frame_texture(sprite_frames, current_frame[animation])
_sprite.texture = _current_texture
if stretch: rescale()
func change_animation(requested_animation : String) -> void:
if not active: return
#printt(_index, "animated.change_animation", requested_animation)
if frame_counts[requested_animation] <= 0:
return
# always set these:
_init_animation(requested_animation)
if requested_animation == animation:
return
_change_animation(requested_animation)
func info() -> Dictionary:
var i : Dictionary
if sprite_frames:
i = sprite_frames.info()
return i
func debug_info():
print(info())
print("Animations: ", sprite_frames.get_animation_names())
# TODO: print("Sequences: ", get_valid_sequence_names())
print("Current sequence: ", animation)
print("Frame count: ", frame_counts[animation])
func next_animation(inc : int) -> StringName:
var anim_names = sprite_frames.get_valid_animation_names()
var i = anim_names.find(animation)
if i < 0: return sprite_frames.get_valid_animation_names()[0]
for anim in anim_names: # loop maximum of once
i = fposmod(i + inc, anim_names.size())
# never switch to base animation and
# skip animations with no frames
if anim_names[i] != base_animation_name and frame_counts[anim_names[i]] > 0:
return StringName(anim_names[i])
return animation
func get_frame_duration() -> float:
var relative_duration = sprite_frames.get_frame_duration(animation, current_frame[animation])
var absolute_duration = relative_duration / (sprite_frames.get_animation_speed(animation) * abs(get_playing_speed()))
return absolute_duration
#assert(_anim_fps > 0)
#return (1.0 / _anim_fps) / speed_scale
# seconds that the frame as been visible for
func get_frame_duration_passed() -> float:
return get_frame_duration() * frame_progress
func get_current_frame() -> Texture2D:
#return sprite_frames.get_frame_texture(animation, current_frame[animation])
return _current_texture
func get_texture(seq_name : String = "", index : int = -1) -> Texture2D:
if seq_name == "": seq_name = base_animation_name
# FIXME: this is silly, there should be a better call path to this
return sprite_frames.sequences[seq_name].get_frame_texture(sprite_frames, index)
func get_rect() -> Rect2:
var size = get_current_frame().get_size()
var pos = offset
if centered:
pos -= 0.5 * size
return Rect2(pos, size)
func set_frame_duration(duration_s : float) -> void:
assert(_anim_fps > 0)
var default_speed = 1.0 / _anim_fps
speed = default_speed / duration_s
speed_scale = speed
func next_frame(increment : int = _direction) -> void:
printt(_index, "next_frame", increment)
if increment > 0 and increment < 1:
increment = 1
elif increment < 0 and increment > -1:
increment = -1
#frame = floor(fposmod(frame + increment, frame_counts[animation]))
goto_frame(sequence().next(increment))
func goto_frame(f : int) -> void:
if current_frame[animation] != f:
current_frame[animation] = f
_current_texture = sequence().get_frame_texture(sprite_frames, current_frame[animation])
_sprite.texture = _current_texture
real_frame_changed.emit(current_frame[animation])
func sequence(sequence_name : String = "") -> AnimatedSequence:
if sequence_name == "":
sequence_name = animation
return sprite_frames.sequences[sequence_name]
# TODO: without underscore this overrides the existing pause and needs to be changed
func _pause():
if is_playing():
printt(_index, "pause", animation)
pause()
paused = true
func _resume():
if not is_playing():
printt(_index, "play", animation)
play(animation, _direction)
paused = false
func rescale():
var viewsize : Vector2 = get_viewport().get_visible_rect().size # Vector2(1920, 1080) # FIXME: get these from project settings? # get_viewport().sizes
if _current_texture:
var framesize := _current_texture.get_size()
var viewscale : float = min( float(viewsize.x) / float(framesize.x), float(viewsize.y) / float(framesize.y))
if not is_equal_approx(viewscale, scale.x):
scale = Vector2(viewscale, viewscale)
# bug in godot 4 requires offset adjustment?
offset = Vector2i( viewsize.x / viewscale, viewsize.y / viewscale ) * 0.5
offset = Vector2i( 1920 / viewscale, 1080 / viewscale ) * 0.5
#printt(viewsize, framesize, viewscale, scale, offset)
func skip_frame(direction : float = 0.0, layer : int = -1) -> void:
if not valid_target(layer): return
#printt(_index, "skip_frame", frame_skip, direction)
# default to skip 0.3sec or 1 of frames whatever is less, but allow for direction to modulate
if is_playing():
next_frame(direction * clampi(0.3 * _anim_fps * speed, 1, frame_skip))
else:
next_frame(sign(direction))
func change_relative_speed(relative_speed : float = 0.0, layer : int = -1) -> void:
if not valid_target(layer): return
if speed <= 2.0 and speed > 0.1:
speed *= 1.0 + (relative_speed / speed * 0.05)
elif speed > 2.0:
speed += relative_speed * speed * 0.1
else:
speed += relative_speed * 0.05
printt(_index, "change_relative_speed", relative_speed, speed, _anim_fps)
speed = clampf(speed, 0.0, max_speed)
speed_scale = speed
if speed > 0:
play(animation, _direction)
func remap_speed(normalized_speed : float) -> float:
# linear below 0.5, smooth step above
var s : float
if normalized_speed < 0.25:
s = remap(normalized_speed, 0.0, 0.25, 0.0, 1.0)
elif normalized_speed < 0.5:
s = remap(normalized_speed, 0.25, 0.5, 1.0, 2.0)
elif normalized_speed < 0.8:
s = smoothstep(0.5, 0.8, normalized_speed)
s = remap(s, 0.0, 1.0, 1.0, max_speed)
else:
s = smoothstep(0.8, 1.0, normalized_speed)
# map to max fps
s = remap(s, 0.0, 1.0, max_speed, max_speed_fps / _anim_fps)
return s
func set_speed(normalized_speed : float = 0.0, layer : int = -1) -> void:
if not valid_target(layer): return
speed = remap_speed(normalized_speed)
printt(_index, "set_speed", normalized_speed, speed)
speed_scale = speed
func change_relative_speed_normalized(normalized_speed : float = 0.0, layer : int = -1) -> void:
if not valid_target(layer): return
normalized_speed = clampf(normalized_speed, -1.0, 1.0)
var eased := ease(abs(normalized_speed), 2)
var _max_speed = max(5.0, 30.0 / _anim_fps)
speed = remap(eased, 0, 1.0, 0, _max_speed)
printt(_index, "change_normalized_speed", normalized_speed, eased, speed, _anim_fps)
speed_scale = speed
if normalized_speed < 0:
_direction = -1.0
play(animation, _direction)
else:
_direction = 1.0
play(animation, _direction)
func reverse(layer : int = -1) -> void:
if not valid_target(layer): return
_direction = -_direction
play(animation, _direction)