fantasy-cli 1.2.14 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gsd
4
+ module TUI
5
+ # Animations - Engine de animações para TUI
6
+ #
7
+ # Suporta:
8
+ # - Typing effect (texto aparecendo caractere por caractere)
9
+ # - Fade in/out
10
+ # - Pulse/glow effects
11
+ # - Progress bars animadas
12
+ # - Frame-based animations
13
+ class Animation
14
+ attr_reader :type, :duration, :frame, :running, :finished
15
+
16
+ def initialize(type:, duration: 1.0, &block)
17
+ @type = type
18
+ @duration = duration
19
+ @frame = 0
20
+ @running = false
21
+ @finished = false
22
+ @callback = block
23
+ @start_time = nil
24
+ @thread = nil
25
+ end
26
+
27
+ def start
28
+ return if @running
29
+
30
+ @running = true
31
+ @finished = false
32
+ @start_time = Time.now
33
+ @frame = 0
34
+
35
+ @thread = Thread.new do
36
+ loop do
37
+ elapsed = Time.now - @start_time
38
+
39
+ if elapsed >= @duration
40
+ @finished = true
41
+ @running = false
42
+ @callback&.call(:finished, self)
43
+ break
44
+ end
45
+
46
+ @frame += 1
47
+ @callback&.call(:frame, self)
48
+
49
+ sleep(1.0 / 60) # 60 FPS
50
+ end
51
+ end
52
+
53
+ self
54
+ end
55
+
56
+ def stop
57
+ @running = false
58
+ @thread&.kill
59
+ @thread = nil
60
+ end
61
+
62
+ # Progresso da animação (0.0 a 1.0)
63
+ def progress
64
+ return 1.0 if @finished
65
+ return 0.0 unless @start_time
66
+
67
+ elapsed = Time.now - @start_time
68
+ [elapsed / @duration, 1.0].min
69
+ end
70
+
71
+ # Easing functions
72
+ def ease_in_out
73
+ p = progress
74
+ p < 0.5 ? 2 * p * p : 1 - ((-2 * p + 2)**2 / 2)
75
+ end
76
+
77
+ def ease_out
78
+ p = progress
79
+ 1 - (1 - p)**2
80
+ end
81
+
82
+ def ease_in
83
+ p = progress
84
+ p * p
85
+ end
86
+
87
+ def bounce
88
+ p = progress
89
+ return 0.0 if p < 0.5
90
+ Math.sin(p * Math::PI * 4) * (1 - p) + p
91
+ end
92
+ end
93
+
94
+ # AnimationManager - Gerencia múltiplas animações
95
+ class AnimationManager
96
+ def initialize
97
+ @animations = []
98
+ @mutex = Mutex.new
99
+ end
100
+
101
+ def create(type:, duration: 1.0, &block)
102
+ anim = Animation.new(type: type, duration: duration, &block)
103
+ @mutex.synchronize { @animations << anim }
104
+ anim
105
+ end
106
+
107
+ def update
108
+ @mutex.synchronize do
109
+ @animations.delete_if { |a| a.finished && !a.running }
110
+ end
111
+ end
112
+
113
+ def stop_all
114
+ @mutex.synchronize do
115
+ @animations.each(&:stop)
116
+ @animations.clear
117
+ end
118
+ end
119
+
120
+ def running_count
121
+ @mutex.synchronize { @animations.count(&:running) }
122
+ end
123
+ end
124
+
125
+ # TypingEffect - Animação de digitação
126
+ class TypingEffect
127
+ attr_reader :text, :cursor, :finished
128
+
129
+ def initialize(text:, speed: 0.05, &block)
130
+ @text = text
131
+ @speed = speed
132
+ @cursor = 0
133
+ @finished = false
134
+ @callback = block
135
+ @thread = nil
136
+ end
137
+
138
+ def start
139
+ return if @thread&.alive?
140
+
141
+ @finished = false
142
+ @cursor = 0
143
+
144
+ @thread = Thread.new do
145
+ @text.each_char do |char|
146
+ sleep(@speed)
147
+ @cursor += 1
148
+ @callback&.call(self)
149
+ end
150
+ @finished = true
151
+ @callback&.call(self)
152
+ end
153
+
154
+ self
155
+ end
156
+
157
+ def stop
158
+ @thread&.kill
159
+ @thread = nil
160
+ end
161
+
162
+ # Texto visível até o cursor
163
+ def visible_text
164
+ @text[0...@cursor]
165
+ end
166
+
167
+ # Texto restante
168
+ def remaining_text
169
+ @text[@cursor..-1]
170
+ end
171
+ end
172
+
173
+ # SpinnerAnimation - Spinner melhorado
174
+ class SpinnerAnimation
175
+ FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].freeze
176
+
177
+ attr_reader :running
178
+
179
+ def initialize(message: '', speed: 0.08, &block)
180
+ @message = message
181
+ @speed = speed
182
+ @callback = block
183
+ @running = false
184
+ @thread = nil
185
+ @frame_index = 0
186
+ end
187
+
188
+ def start
189
+ return if @running
190
+
191
+ @running = true
192
+ @frame_index = 0
193
+
194
+ @thread = Thread.new do
195
+ while @running
196
+ frame = FRAMES[@frame_index % FRAMES.length]
197
+ @callback&.call(frame, @message)
198
+ @frame_index += 1
199
+ sleep(@speed)
200
+ end
201
+ end
202
+
203
+ self
204
+ end
205
+
206
+ def stop
207
+ @running = false
208
+ @thread&.join(0.5)
209
+ @thread = nil
210
+ end
211
+
212
+ def update_message(new_message)
213
+ @message = new_message
214
+ end
215
+
216
+ def render
217
+ frame = FRAMES[@frame_index % FRAMES.length]
218
+ "#{frame} #{@message}"
219
+ end
220
+ end
221
+
222
+ # PulseEffect - Efeito de pulsação
223
+ class PulseEffect
224
+ attr_reader :running
225
+
226
+ def initialize(cycles: 3, speed: 0.5, &block)
227
+ @cycles = cycles
228
+ @speed = speed
229
+ @callback = block
230
+ @running = false
231
+ @thread = nil
232
+ end
233
+
234
+ def start
235
+ return if @running
236
+
237
+ @running = true
238
+
239
+ @thread = Thread.new do
240
+ @cycles.times do |i|
241
+ break unless @running
242
+
243
+ # Intensidade varia de 0.0 a 1.0
244
+ intensity = Math.sin(i * Math::PI / @cycles)
245
+ @callback&.call(intensity, i)
246
+ sleep(@speed)
247
+ end
248
+ @running = false
249
+ end
250
+
251
+ self
252
+ end
253
+
254
+ def stop
255
+ @running = false
256
+ @thread&.kill
257
+ @thread = nil
258
+ end
259
+ end
260
+
261
+ # ProgressBar - Barra de progresso animada
262
+ class ProgressBar
263
+ attr_reader :value, :max, :width
264
+
265
+ def initialize(width: 40, max: 100, fill: '█', empty: '░')
266
+ @width = width
267
+ @max = max
268
+ @value = 0
269
+ @fill = fill
270
+ @empty = empty
271
+ @animation = nil
272
+ end
273
+
274
+ def update(value)
275
+ @value = [[value, 0].max, @max].min
276
+ end
277
+
278
+ def animate_to(target, duration: 1.0, &block)
279
+ start_val = @value
280
+ @animation&.stop
281
+
282
+ @animation = Animation.new(type: :progress, duration: duration) do |event, anim|
283
+ if event == :frame
284
+ @value = start_val + (target - start_val) * anim.ease_out
285
+ block&.call(self)
286
+ end
287
+ end
288
+
289
+ @animation.start
290
+ end
291
+
292
+ def render
293
+ filled = ((@value.to_f / @max) * @width).round
294
+ filled = [@width, filled].min
295
+
296
+ bar = @fill * filled + @empty * (@width - filled)
297
+ percentage = ((@value / @max.to_f) * 100).round
298
+
299
+ "[#{bar}] #{percentage}%"
300
+ end
301
+
302
+ def render_colored(theme: nil)
303
+ t = theme || Colors.theme
304
+ filled = ((@value.to_f / @max) * @width).round
305
+ filled = [@width, filled].min
306
+
307
+ filled_part = @fill * filled
308
+ empty_part = @empty * (@width - filled)
309
+ percentage = ((@value / @max.to_f) * 100).round
310
+
311
+ "#{t[:accent]}[#{filled_part}#{t[:dim]}#{empty_part}#{t[:accent]}] #{percentage}%#{Colors::RESET}"
312
+ end
313
+
314
+ def finish
315
+ @animation&.stop
316
+ @value = @max
317
+ end
318
+ end
319
+ end
320
+ end