-
-
Notifications
You must be signed in to change notification settings - Fork 183
/
Copy pathDrawingTurtle.gd
344 lines (282 loc) · 9.25 KB
/
DrawingTurtle.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
# Draws geometric shapes by moving forward and turning. Inspired by the LOGO
# language.
#
# Most function calls record vertices and update the turtle's state but don't
# draw directly.
#
# To draw the shapes, see [play_draw_animation]. Uses tween animation to animate
# the motion of the turtle and drawing.
#
# Snaps drawing coordinates to the nearest pixel.
class_name DrawingTurtle
extends Node2D
signal turtle_finished
var draw_speed := 400.0
var turn_speed_degrees := 260.0
# Increases the animation playback speed.
var speed_multiplier := 1.0
var _points := []
var _polygons := []
var _current_offset := Vector2.ZERO
# Keeps a list of commands the user registered. This allows us to animate the
# turtle afterwards.
var _command_stack := []
# Stores commands until closing a polygon, to insert commands to move the
# camera.
var _temp_command_stack = []
var _tween := Tween.new()
onready var turn_degrees = rotation_degrees
onready var _pivot := $Pivot as Node2D
onready var _sprite := $Pivot/Sprite as Sprite
onready var _shadow := $Pivot/Shadow as Sprite
onready var _canvas := $Canvas as Node2D
onready var _camera := $Camera2D as Camera2D
func _ready() -> void:
add_child(_tween)
# Allows to have a camera follow the turtle when using it in practices,
# inside the GDQuestBoy.
if get_parent() is Viewport:
_camera.set_as_toplevel(true)
_camera.position = global_position
_camera.make_current()
# Virtually moves the turtle and records a new vertex.
func move_forward(distance: float) -> void:
var previous_point := Vector2.ZERO
if _points.empty():
_points.append(previous_point)
else:
previous_point = _points[-1]
var new_point := previous_point + Vector2.RIGHT.rotated(deg2rad(turn_degrees)) * distance
new_point = new_point.snapped(Vector2.ONE)
_points.append(new_point)
_temp_command_stack.append(
{command = "move_to", target = new_point + position + _current_offset}
)
func turn_right(angle_degrees: float) -> void:
turn_degrees = round(turn_degrees + angle_degrees)
_temp_command_stack.append({command = "turn", angle = round(angle_degrees)})
func turn_left(angle_degrees: float) -> void:
turn_degrees = round(turn_degrees - angle_degrees)
_temp_command_stack.append({command = "turn", angle = round(-angle_degrees)})
# Completes the current polygon's drawing and virtually jumps the turtle to a
# new start position.
func jump(x: float, y: float) -> void:
var last_point := Vector2.ZERO if _points.empty() else _points[-1]
_close_polygon()
_points.append(Vector2.ZERO)
_current_offset += Vector2(x, y) + last_point
_command_stack.append({command = "jump", offset = Vector2(x, y)})
# Resets the turtle's state. Use it when testing a student's assignments to
# reset the object between runs.
func reset() -> void:
_command_stack.clear()
stop_animation()
_animate_jump(0)
rotation_degrees = 0.0
turn_degrees = 0.0
_pivot.rotation_degrees = 0.0
_pivot.position = Vector2.ZERO
_camera.position = Vector2.ZERO
_points.clear()
_polygons.clear()
for child in _canvas.get_children():
child.queue_free()
_current_offset = Vector2.ZERO
_pivot.position = Vector2.ZERO
# Returns a copy of the polygons the turtle will draw.
func get_polygons() -> Array:
return _polygons.duplicate()
func stop_animation() -> void:
_tween.remove_all()
for line in _canvas.get_children():
line.stop()
# Queues all tweens required to animate the turtle drawing all the shapes and
# starts the tween animation.
func play_draw_animation() -> void:
_close_polygon()
# We queue all tweens at once, based on commands: moving the turtle, turning
# it, drawing lines...
var tween_start_time := 0.0
var turtle_position := position
var turtle_rotation_degrees := rotation_degrees
for command in _command_stack:
var duration := 1.0
match command.command:
"set_position":
turtle_position = command.target
_pivot.position = command.target
"move_camera":
# The callback never gets called if it has a delay of 0 seconds.
if is_equal_approx(tween_start_time, 0.0):
_move_camera(command.target)
else:
_tween.interpolate_callback(
self, tween_start_time, "_move_camera", command.target
)
"move_to":
duration = turtle_position.distance_to(command.target) / draw_speed / speed_multiplier
_tween.interpolate_property(
_pivot,
"position",
turtle_position - position,
command.target - position,
duration,
Tween.TRANS_LINEAR,
Tween.EASE_IN,
tween_start_time
)
var line := DrawingLine2D.new(
turtle_position - position,
command.target - position,
duration,
tween_start_time
)
_canvas.add_child(line)
turtle_position = command.target
tween_start_time += duration
"turn":
duration = abs(command.angle) / turn_speed_degrees / speed_multiplier
var target_angle: float = round(turtle_rotation_degrees + command.angle)
_tween.interpolate_property(
_pivot,
"rotation_degrees",
turtle_rotation_degrees,
target_angle,
duration,
Tween.TRANS_LINEAR,
Tween.EASE_IN,
tween_start_time
)
turtle_rotation_degrees = target_angle
tween_start_time += duration
"jump":
duration = 0.5 / speed_multiplier
_tween.interpolate_property(
_pivot,
"position",
turtle_position,
turtle_position + command.offset,
duration,
Tween.TRANS_LINEAR,
Tween.EASE_IN,
tween_start_time
)
_tween.interpolate_method(
self,
"_animate_jump",
0.0,
1.0,
duration,
Tween.TRANS_LINEAR,
Tween.EASE_IN,
tween_start_time
)
turtle_position += command.offset
tween_start_time += duration
_tween.start()
for line in _canvas.get_children():
line.start()
# Returns the total bounding rectangle enclosing all the turtle's drawn
# polygons.
func get_rect() -> Rect2:
var bounds := Rect2()
for polygon in _polygons:
var rect: Rect2 = polygon.get_rect()
rect.position += polygon.position
bounds = bounds.merge(rect)
return bounds
func get_command_stack() -> Array:
return _command_stack.duplicate()
# Animates the turtle's height and shadow scale when jumping. Tween the progress
# value from 0 to 1.
func _animate_jump(progress: float) -> void:
var parabola := -pow(2.0 * progress - 1.0, 2.0) + 1.0
_sprite.position.y = -parabola * 100.0
var shadow_scale := (1.0 - parabola + 1.0) / 2.0
_shadow.scale = shadow_scale * Vector2.ONE
func _close_polygon() -> void:
if _points.empty():
return
var polygon := Polygon.new()
# We want to test shapes being drawn at the correct position using the
# position property. It works differently from jump() which offsets the
# turtle from its position.
polygon.position = position + _current_offset
polygon.points = PoolVector2Array(_points)
_polygons.append(polygon)
_points.clear()
if not position.is_equal_approx(Vector2.ZERO):
_command_stack.append({command = "set_position", target = position})
# We can't know exactly when and where to move the camera until completing a
# shape, as we want to center the camera on the shape.
_command_stack.append({command = "move_camera", target = polygon.get_center()})
for command in _temp_command_stack:
_command_stack.append(command)
_temp_command_stack.clear()
func _move_camera(target_global_position: Vector2) -> void:
_camera.position = target_global_position
# Polygon that can animate drawing its line.
class Polygon:
extends Node2D
var points := PoolVector2Array() setget , get_points
# Returns the local bounds of the polygon. That is to say, it only takes the
# point into account in local space, but not the polygon's `position`.
func get_rect() -> Rect2:
var top_left := Vector2.ZERO
var bottom_right := Vector2.ZERO
for p in points:
if p.x > bottom_right.x:
bottom_right.x = p.x
elif p.x < top_left.x:
top_left.x = p.x
if p.y > bottom_right.y:
bottom_right.y = p.y
elif p.y < top_left.y:
top_left.y = p.y
return Rect2(top_left, bottom_right - top_left)
func get_positioned_rect() -> Rect2:
var rect := get_rect()
rect.position += position
return rect
func get_center() -> Vector2:
var rect := get_rect()
return (rect.position + rect.end) / 2.0 + position
func get_global_center() -> Vector2:
var rect := get_rect()
return (rect.position + rect.end) / 2.0 + global_position
func get_points() -> PoolVector2Array:
return points
func is_empty():
return points.empty() or points == PoolVector2Array([Vector2.ZERO])
class DrawingLine2D:
extends Line2D
const LabelScene := preload("DrawingTurtleLabel.tscn")
const LINE_THICKNESS := 4.0
const DEFAULT_COLOR := Color.white
var _tween := Tween.new()
func _init(start: Vector2, end: Vector2, duration: float, start_time: float) -> void:
add_child(_tween)
width = LINE_THICKNESS
default_color = DEFAULT_COLOR
points = PoolVector2Array([start, start])
_tween.interpolate_callback(self, start_time, "_spawn_label")
_tween.interpolate_method(
self,
"_animate_drawing",
start,
end,
duration,
Tween.TRANS_LINEAR,
Tween.EASE_IN,
start_time
)
func start() -> void:
_tween.start()
func stop() -> void:
_tween.stop_all()
func _animate_drawing(point: Vector2) -> void:
points[-1] = point
func _spawn_label() -> void:
var label := LabelScene.instance() as PanelContainer
label.rect_position = points[0] - label.rect_size / 2
add_child(label)