progress_bar_none_overload_3000 1.0.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,356 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProgressBarNone
4
+ class Gantt
5
+ # Color mode configurations
6
+ MODES = {
7
+ tufte: {
8
+ done: { palette: :ocean, char: "█", partial: "▓" },
9
+ wip: { palette: :sunset, char: "▓", partial: "▒" },
10
+ pending: { palette: :mono, char: "░", partial: "░" },
11
+ frame: false,
12
+ animated: false,
13
+ },
14
+ phase: {
15
+ palettes: [:ocean, :forest, :sunset, :crystal, :fire, :ice, :galaxy, :neon],
16
+ char: "█", partial: "▓",
17
+ frame: :rounded,
18
+ animated: false,
19
+ },
20
+ rainbow: {
21
+ char: "█", partial: "▓",
22
+ frame: :neon,
23
+ animated: true,
24
+ },
25
+ fire: {
26
+ done: { palette: :lava, char: "█", partial: "▓" },
27
+ wip: { palette: :lava, char: "▓", partial: "▒" },
28
+ pending: { palette: :mono, char: "░", partial: "░" },
29
+ frame: :bold,
30
+ animated: true,
31
+ },
32
+ matrix: {
33
+ done: { palette: :matrix, char: "█", partial: "▓" },
34
+ wip: { palette: :matrix, char: "▓", partial: "▒" },
35
+ pending: { palette: :hacker, char: "░", partial: "░" },
36
+ frame: :single,
37
+ animated: true,
38
+ },
39
+ neon: {
40
+ done: { palette: :synthwave, char: "█", partial: "▓" },
41
+ wip: { palette: :neon, char: "▓", partial: "▒" },
42
+ pending: { palette: :mono, char: "░", partial: "░" },
43
+ frame: :cyber,
44
+ animated: true,
45
+ },
46
+ }.freeze
47
+
48
+ STATUS_ICONS = {
49
+ done: "✓",
50
+ wip: "◆",
51
+ pending: "○",
52
+ }.freeze
53
+
54
+ attr_reader :tasks, :mode, :title, :width, :show_progress, :animated
55
+
56
+ def initialize(tasks, title: nil, mode: :tufte, width: 80, show_progress: true,
57
+ animated: nil, frame_style: nil, custom_palettes: nil)
58
+ @tasks = tasks.map { |t| normalize_task(t) }
59
+ @title = title
60
+ @mode = mode
61
+ @mode_config = MODES[mode] || MODES[:tufte]
62
+ @width = width
63
+ @show_progress = show_progress
64
+ @animated = animated.nil? ? @mode_config[:animated] : animated
65
+ @frame_style = frame_style || @mode_config[:frame]
66
+ @custom_palettes = custom_palettes
67
+ @max_time = @tasks.map { |t| t[:start] + t[:duration] }.max || 1
68
+ end
69
+
70
+ def render(frame_num: 0)
71
+ lines = []
72
+ lines.concat(render_title(frame_num))
73
+ lines << render_timeline_header
74
+ lines << render_separator
75
+ @tasks.each_with_index { |t, i| lines << render_task_row(t, i, frame_num) }
76
+ lines << render_separator
77
+ lines.concat(render_footer(frame_num)) if @show_progress
78
+ lines
79
+ end
80
+
81
+ def to_s(frame_num: 0)
82
+ render(frame_num: frame_num).join("\n")
83
+ end
84
+
85
+ def run(fps: 1)
86
+ print ANSI::HIDE_CURSOR
87
+ frame = 0
88
+ loop do
89
+ print "\e[2J\e[H"
90
+ puts to_s(frame_num: frame)
91
+ frame += 1
92
+ sleep(1.0 / fps)
93
+ end
94
+ rescue Interrupt
95
+ print ANSI::SHOW_CURSOR
96
+ end
97
+
98
+ private
99
+
100
+ def normalize_task(t)
101
+ {
102
+ name: t[:name] || "Untitled",
103
+ group: t[:group] || "",
104
+ start: t[:start] || 0,
105
+ duration: t[:duration] || 1,
106
+ status: t[:status] || :pending,
107
+ progress: t[:progress] || 0.0,
108
+ }
109
+ end
110
+
111
+ def render_title(frame_num)
112
+ return [] unless @title
113
+ if @mode == :rainbow
114
+ rainbow_title = @title.each_char.with_index.map { |c, i|
115
+ "#{ANSI.rainbow_cycle(i * 0.1, frame_num * 0.1, 1.0)}#{c}"
116
+ }.join + ANSI::RESET
117
+ [rainbow_title, ""]
118
+ elsif @frame_style
119
+ Frames.banner(@title, style: :double, palette: title_palette, animated: @animated, frame_num: frame_num) + [""]
120
+ else
121
+ color = ANSI.palette_color(title_palette, 0.5)
122
+ ["#{ANSI::BOLD}#{color}#{@title}#{ANSI::RESET}", ""]
123
+ end
124
+ end
125
+
126
+ def render_timeline_header
127
+ label_width = calc_label_width
128
+ bar_width = @width - label_width
129
+ header = "#{ANSI::DIM}#{"Phase".ljust(6)}#{"Task".ljust(label_width - 6)}#{ANSI::RESET}"
130
+
131
+ step = [(@max_time / 10.0).ceil, 1].max
132
+ markers = (0..@max_time).step(step * 2).map { |i| format("%-4s", "T#{i}") }.join
133
+ " #{header}#{ANSI::DIM}#{markers}#{ANSI::RESET}"
134
+ end
135
+
136
+ def render_separator
137
+ " #{ANSI::DIM}#{"─" * (@width - 4)}#{ANSI::RESET}"
138
+ end
139
+
140
+ def render_task_row(task, index, frame_num)
141
+ status = task[:status]
142
+ icon_color = status_color(status, 0.5, index, frame_num)
143
+ icon = "#{icon_color}#{STATUS_ICONS[status]}#{ANSI::RESET}"
144
+
145
+ name_style = status == :done ? ANSI::DIM : ""
146
+ group_color = ANSI.palette_color(:crystal, 0.3)
147
+
148
+ label = " #{group_color}#{task[:group].ljust(6)}#{ANSI::RESET}#{icon} #{name_style}#{task[:name].ljust(calc_label_width - 8)}#{ANSI::RESET}"
149
+
150
+ bar = render_bar(task, index, frame_num)
151
+ "#{label}#{bar}"
152
+ end
153
+
154
+ def render_bar(task, index, frame_num)
155
+ label_width = calc_label_width
156
+ bar_width = @width - label_width - 4
157
+ scale = bar_width.to_f / @max_time
158
+
159
+ start_pos = (task[:start] * scale).round
160
+ filled_total = (task[:duration] * scale).round
161
+ filled_done = (filled_total * task[:progress]).round
162
+
163
+ # Build bar as array of cells (each cell is a pre-formatted string)
164
+ cells = Array.new(bar_width) { " " }
165
+
166
+ filled_total.times do |i|
167
+ pos = start_pos + i
168
+ break if pos >= bar_width
169
+
170
+ is_done_portion = i < filled_done
171
+ char_progress = i.to_f / [filled_total, 1].max
172
+
173
+ color = if @mode == :rainbow
174
+ ANSI.rainbow_cycle(char_progress, frame_num * 0.1, 1.0)
175
+ elsif @mode == :fire && @animated
176
+ palette = ANSI::CRYSTAL_PALETTE[:lava]
177
+ c = palette[(char_progress * (palette.length - 1)).round]
178
+ ANSI.fire_flicker(c[0], c[1], c[2], frame_num * 0.1 + i * 0.05)
179
+ else
180
+ status_color(task[:status], char_progress, index, frame_num)
181
+ end
182
+
183
+ ch = if is_done_portion
184
+ bar_char(task[:status], :filled)
185
+ elsif task[:progress] > 0
186
+ bar_char(task[:status], :partial)
187
+ else
188
+ bar_char(task[:status], :filled)
189
+ end
190
+
191
+ cells[pos] = "#{color}#{ch}#{ANSI::RESET}"
192
+ end
193
+
194
+ # Matrix rain in empty space
195
+ if @mode == :matrix && @animated
196
+ cells.each_with_index do |cell, i|
197
+ if cell == " " && rand < 0.03
198
+ cells[i] = "#{ANSI::DIM}#{ANSI.palette_color(:matrix, rand)}#{["0", "1"].sample}#{ANSI::RESET}"
199
+ end
200
+ end
201
+ end
202
+
203
+ cells.join
204
+ end
205
+
206
+ def render_footer(frame_num)
207
+ done = @tasks.count { |t| t[:status] == :done }
208
+ total = @tasks.size
209
+ pct = total > 0 ? done.to_f / total : 0
210
+
211
+ renderer = Renderer.new(
212
+ style: :crystal,
213
+ width: [30, @width - 30].min,
214
+ palette: footer_palette,
215
+ )
216
+
217
+ state = { progress: pct, current: done, total: total }
218
+ lines = [""]
219
+ lines << " #{renderer.render_progress_bar(state)}"
220
+ lines << " #{ANSI::DIM}Updated: #{Time.now.strftime("%H:%M:%S")}#{ANSI::RESET}"
221
+ lines << ""
222
+ lines
223
+ end
224
+
225
+ def status_color(status, progress, index, frame_num)
226
+ case @mode
227
+ when :phase
228
+ palettes = MODES[:phase][:palettes]
229
+ palette_name = palettes[index % palettes.length]
230
+ ANSI.palette_color(palette_name, progress)
231
+ when :rainbow
232
+ ANSI.rainbow_cycle(progress, frame_num * 0.1, 1.0)
233
+ when :custom
234
+ palette_name = @custom_palettes&.dig(status) || :crystal
235
+ ANSI.palette_color(palette_name, progress)
236
+ else
237
+ config = @mode_config[status]
238
+ if config
239
+ ANSI.palette_color(config[:palette], progress)
240
+ else
241
+ ANSI.palette_color(:mono, progress)
242
+ end
243
+ end
244
+ end
245
+
246
+ def bar_char(status, type)
247
+ config = @mode_config[status]
248
+ return "█" unless config.is_a?(Hash)
249
+ type == :filled ? config[:char] : config[:partial]
250
+ end
251
+
252
+ def title_palette
253
+ case @mode
254
+ when :fire then :lava
255
+ when :matrix then :matrix
256
+ when :neon then :neon
257
+ when :rainbow then :rainbow
258
+ else :ocean
259
+ end
260
+ end
261
+
262
+ def footer_palette
263
+ case @mode
264
+ when :fire then :lava
265
+ when :matrix then :matrix
266
+ when :neon then :synthwave
267
+ when :rainbow then :rainbow
268
+ else :ocean
269
+ end
270
+ end
271
+
272
+ def calc_label_width
273
+ max_name = @tasks.map { |t| t[:name].length }.max || 10
274
+ max_group = @tasks.map { |t| t[:group].length }.max || 2
275
+ [max_name + max_group + 10, @width / 2].min
276
+ end
277
+
278
+ public
279
+
280
+ def render_svg(svg_width: 900, row_height: 25)
281
+ svg_height = 120 + @tasks.size * row_height + 60
282
+
283
+ status_palette = { done: :ocean, wip: :sunset, pending: :mono }
284
+ status_opacity = { done: 0.8, wip: 0.6, pending: 0.3 }
285
+
286
+ lines = []
287
+ lines << %(<svg xmlns="http://www.w3.org/2000/svg" width="#{svg_width}" height="#{svg_height}" font-family="'SF Mono', 'Menlo', 'Monaco', monospace" font-size="13">)
288
+ lines << %( <rect width="100%" height="100%" fill="#1a1a2e" rx="12"/>)
289
+ lines << %( <g fill="#e0e0e0">)
290
+
291
+ if @title
292
+ lines << %( <text x="#{svg_width / 2}" y="50" text-anchor="middle" fill="#{palette_rgb(:ocean, 0.5)}" font-size="18" font-weight="bold">#{escape_svg(@title)}</text>)
293
+ lines << %( <rect x="20" y="65" width="#{svg_width - 40}" height="1" fill="#333"/>)
294
+ end
295
+
296
+ y_start = @title ? 90 : 30
297
+ lines << %( <text x="30" y="#{y_start}" fill="#666" font-size="11">Phase</text>)
298
+ lines << %( <text x="90" y="#{y_start}" fill="#666" font-size="11">Task</text>)
299
+ lines << %( <rect x="20" y="#{y_start + 7}" width="#{svg_width - 40}" height="1" fill="#333"/>)
300
+
301
+ bar_left = 350
302
+ bar_right = svg_width - 30
303
+ bar_total = bar_right - bar_left
304
+ scale = bar_total.to_f / @max_time
305
+
306
+ @tasks.each_with_index do |task, i|
307
+ y = y_start + 20 + i * row_height
308
+ icon = STATUS_ICONS[task[:status]]
309
+ color = palette_rgb(status_palette[task[:status]], 0.5)
310
+ opacity = status_opacity[task[:status]]
311
+ name_fill = task[:status] == :done ? "#999" : "#ccc"
312
+
313
+ lines << %( <text x="30" y="#{y}" fill="#00bcd4" font-size="11">#{escape_svg(task[:group])}</text>)
314
+ lines << %( <text x="70" y="#{y}" fill="#{color}" font-size="11">#{icon}</text>)
315
+ lines << %( <text x="90" y="#{y}" fill="#{name_fill}" font-size="11">#{escape_svg(task[:name])}</text>)
316
+
317
+ rx = bar_left + (task[:start] * scale).round
318
+ rw = (task[:duration] * scale).round
319
+ lines << %( <rect x="#{rx}" y="#{y - 12}" width="#{rw}" height="16" fill="#{color}" opacity="#{opacity}" rx="2"/>)
320
+ end
321
+
322
+ done = @tasks.count { |t| t[:status] == :done }
323
+ pct = @tasks.size > 0 ? (done.to_f / @tasks.size * 100).round : 0
324
+ footer_y = y_start + 20 + @tasks.size * row_height + 20
325
+ filled_w = (pct / 100.0 * 300).round
326
+ lines << %( <rect x="20" y="#{footer_y - 15}" width="#{svg_width - 40}" height="1" fill="#333"/>)
327
+ lines << %( <text x="30" y="#{footer_y}" fill="#ccc" font-weight="bold" font-size="12">Progress:</text>)
328
+ lines << %( <rect x="120" y="#{footer_y - 12}" width="300" height="16" fill="#333" rx="4"/>)
329
+ lines << %( <rect x="120" y="#{footer_y - 12}" width="#{filled_w}" height="16" fill="#{palette_rgb(:ocean, 0.7)}" opacity="0.8" rx="4"/>)
330
+ lines << %( <text x="430" y="#{footer_y}" fill="#{palette_rgb(:ocean, 0.7)}" font-size="12">#{pct}% (#{done}/#{@tasks.size})</text>)
331
+
332
+ lines << %( </g>)
333
+ lines << %(</svg>)
334
+ lines.join("\n")
335
+ end
336
+
337
+ private
338
+
339
+ def palette_rgb(palette_name, progress)
340
+ p = ANSI::CRYSTAL_PALETTE[palette_name] || ANSI::CRYSTAL_PALETTE[:crystal]
341
+ scaled = progress * (p.length - 1)
342
+ i = scaled.floor
343
+ frac = scaled - i
344
+ c1 = p[i]
345
+ c2 = p[[i + 1, p.length - 1].min]
346
+ r = (c1[0] + (c2[0] - c1[0]) * frac).round
347
+ g = (c1[1] + (c2[1] - c1[1]) * frac).round
348
+ b = (c1[2] + (c2[2] - c1[2]) * frac).round
349
+ "#%02x%02x%02x" % [r, g, b]
350
+ end
351
+
352
+ def escape_svg(text)
353
+ text.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")
354
+ end
355
+ end
356
+ end
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module ProgressBarNone
6
+ # Kitty Graphics Protocol support for inline images in terminal
7
+ # Works with Ghostty, Kitty, WezTerm, and other compatible terminals
8
+ module Graphics
9
+ # Kitty Graphics Protocol escape sequences
10
+ # Format: <ESC>_G<control data>;<payload><ESC>\
11
+ KITTY_START = "\e_G"
12
+ KITTY_END = "\e\\"
13
+
14
+ # iTerm2 protocol (also supported by many terminals)
15
+ ITERM_START = "\e]1337;File="
16
+ ITERM_END = "\a"
17
+
18
+ class << self
19
+ # Check if terminal supports Kitty graphics protocol
20
+ def kitty_supported?
21
+ term = ENV["TERM"] || ""
22
+ term_program = ENV["TERM_PROGRAM"] || ""
23
+
24
+ # Known supporting terminals
25
+ term.include?("kitty") ||
26
+ term.include?("ghostty") ||
27
+ term_program.downcase.include?("kitty") ||
28
+ term_program.downcase.include?("ghostty") ||
29
+ term_program.downcase.include?("wezterm")
30
+ end
31
+
32
+ # Check if terminal supports iTerm2 graphics protocol
33
+ def iterm_supported?
34
+ term_program = ENV["TERM_PROGRAM"] || ""
35
+ lc_terminal = ENV["LC_TERMINAL"] || ""
36
+
37
+ term_program.include?("iTerm") ||
38
+ lc_terminal.include?("iTerm") ||
39
+ term_program.downcase.include?("wezterm")
40
+ end
41
+
42
+ # Display an image using the best available protocol
43
+ # @param path [String] Path to image file (PNG, GIF, JPEG, etc.)
44
+ # @param width [Integer, nil] Width in cells (nil = auto)
45
+ # @param height [Integer, nil] Height in cells (nil = auto)
46
+ # @param preserve_aspect [Boolean] Preserve aspect ratio
47
+ # @return [String] Escape sequence to display image
48
+ def display_image(path, width: nil, height: nil, preserve_aspect: true)
49
+ return "" unless File.exist?(path)
50
+
51
+ if kitty_supported?
52
+ kitty_display_image(path, width: width, height: height)
53
+ elsif iterm_supported?
54
+ iterm_display_image(path, width: width, height: height, preserve_aspect: preserve_aspect)
55
+ else
56
+ # Fallback: return empty or ASCII art placeholder
57
+ ascii_placeholder(width || 10, height || 3)
58
+ end
59
+ end
60
+
61
+ # Display image using Kitty graphics protocol
62
+ def kitty_display_image(path, width: nil, height: nil)
63
+ data = File.binread(path)
64
+ encoded = Base64.strict_encode64(data)
65
+
66
+ # Build control data
67
+ controls = []
68
+ controls << "a=T" # Action: transmit and display
69
+ controls << "f=100" # Format: PNG (auto-detect)
70
+ controls << "t=d" # Transmission: direct
71
+ controls << "c=#{width}" if width
72
+ controls << "r=#{height}" if height
73
+
74
+ # Chunk the data (max 4096 bytes per chunk)
75
+ chunks = encoded.scan(/.{1,4096}/)
76
+ result = ""
77
+
78
+ chunks.each_with_index do |chunk, i|
79
+ is_last = i == chunks.length - 1
80
+ ctrl = controls.dup
81
+ ctrl << (is_last ? "m=0" : "m=1")
82
+
83
+ result += "#{KITTY_START}#{ctrl.join(",")};#{chunk}#{KITTY_END}"
84
+ end
85
+
86
+ result
87
+ end
88
+
89
+ # Display image using iTerm2 protocol
90
+ def iterm_display_image(path, width: nil, height: nil, preserve_aspect: true)
91
+ data = File.binread(path)
92
+ encoded = Base64.strict_encode64(data)
93
+
94
+ # Build arguments
95
+ args = []
96
+ args << "inline=1"
97
+ args << "width=#{width}" if width
98
+ args << "height=#{height}" if height
99
+ args << "preserveAspectRatio=#{preserve_aspect ? 1 : 0}"
100
+
101
+ "#{ITERM_START}#{args.join(";")};#{encoded}#{ITERM_END}"
102
+ end
103
+
104
+ # ASCII art placeholder when graphics not supported
105
+ def ascii_placeholder(width, height)
106
+ top = "┌" + "─" * width + "┐\n"
107
+ middle = ("│" + " " * width + "│\n") * [height - 2, 1].max
108
+ bottom = "└" + "─" * width + "┘"
109
+ top + middle + bottom
110
+ end
111
+
112
+ # Generate inline ASCII art animations
113
+ # These work in ALL terminals!
114
+ def ascii_art(name, frame = 0)
115
+ case name
116
+ when :fire
117
+ fire_art(frame)
118
+ when :nyan
119
+ nyan_art(frame)
120
+ when :rocket
121
+ rocket_art(frame)
122
+ when :celebration
123
+ celebration_art(frame)
124
+ when :skull
125
+ skull_art(frame)
126
+ when :matrix
127
+ matrix_art(frame)
128
+ when :loading
129
+ loading_art(frame)
130
+ else
131
+ ""
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ def fire_art(frame)
138
+ # Animated fire ASCII art
139
+ flames = [
140
+ " ( ) ",
141
+ " ( ) ",
142
+ " ( () ) ",
143
+ "( (()) )",
144
+ " \\|/||\\|/ ",
145
+ " \\||||/ ",
146
+ " \\||/ ",
147
+ " || ",
148
+ ]
149
+
150
+ # Animate by shifting colors
151
+ colors = [
152
+ "\e[38;2;255;0;0m", # Red
153
+ "\e[38;2;255;100;0m", # Orange
154
+ "\e[38;2;255;200;0m", # Yellow
155
+ "\e[38;2;255;255;100m", # Light yellow
156
+ ]
157
+
158
+ result = ""
159
+ flames.each_with_index do |line, i|
160
+ color_idx = (frame + i) % colors.length
161
+ result += "#{colors[color_idx]}#{line}\e[0m\n"
162
+ end
163
+ result
164
+ end
165
+
166
+ def nyan_art(frame)
167
+ # Animated nyan cat
168
+ cat_frames = [
169
+ [
170
+ " ╭━━━━━━━╮ ",
171
+ " ╭┃ ▀ ω ▀ ┃╮ ",
172
+ "━━╯┃ ┃╰━",
173
+ " ╰━━━╮╭━━╯ ",
174
+ " ╰╯╰╯ ",
175
+ ],
176
+ [
177
+ " ╭━━━━━━━╮ ",
178
+ " ╭┃ ▀ ω ▀ ┃╮ ",
179
+ "━━╯┃ ┃╰━",
180
+ " ╰━━╮━╮━━╯ ",
181
+ " ╯ ╰ ",
182
+ ],
183
+ ]
184
+
185
+ rainbow = "\e[38;2;255;0;0m━\e[38;2;255;127;0m━\e[38;2;255;255;0m━\e[38;2;0;255;0m━\e[38;2;0;0;255m━\e[38;2;139;0;255m━\e[0m"
186
+ cat = cat_frames[frame % 2]
187
+
188
+ result = ""
189
+ cat.each { |line| result += "#{rainbow}#{line}\n" }
190
+ result
191
+ end
192
+
193
+ def rocket_art(frame)
194
+ rockets = [
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
+ colors = ["\e[38;2;255;100;0m", "\e[38;2;255;200;0m", "\e[38;2;255;255;255m"]
226
+ rocket = rockets[frame % 2]
227
+
228
+ result = ""
229
+ rocket.each_with_index do |line, i|
230
+ color = i < 6 ? "\e[38;2;200;200;200m" : colors[(frame + i) % colors.length]
231
+ result += "#{color}#{line}\e[0m\n"
232
+ end
233
+ result
234
+ end
235
+
236
+ def celebration_art(frame)
237
+ # Fireworks/confetti
238
+ patterns = [
239
+ " * . * . * ",
240
+ " . * * . ",
241
+ "* . * . *",
242
+ " . * . * . * ",
243
+ " * * * ",
244
+ ]
245
+
246
+ emojis = ["🎉", "🎊", "✨", "💫", "⭐", "🌟"]
247
+
248
+ result = ""
249
+ patterns.each_with_index do |pattern, i|
250
+ colored = pattern.gsub("*") do
251
+ color = ANSI.rainbow_cycle((frame + i) * 0.1, frame * 0.05, 2.0)
252
+ "#{color}#{emojis[(frame + i) % emojis.length]}\e[0m"
253
+ end
254
+ result += colored + "\n"
255
+ end
256
+ result
257
+ end
258
+
259
+ def skull_art(frame)
260
+ skull = [
261
+ " ___ ",
262
+ " / \\ ",
263
+ " | x x | ",
264
+ " | _ | ",
265
+ " | \\_/ | ",
266
+ " \\___/ ",
267
+ ]
268
+
269
+ eye_frames = ["x", "o", "O", "*"]
270
+ eye = eye_frames[frame % eye_frames.length]
271
+
272
+ result = ""
273
+ skull.each do |line|
274
+ colored_line = line.gsub("x", eye)
275
+ result += "\e[38;2;255;255;255m#{colored_line}\e[0m\n"
276
+ end
277
+ result
278
+ end
279
+
280
+ def matrix_art(frame)
281
+ width = 20
282
+ height = 5
283
+ chars = "ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ0123456789".chars
284
+
285
+ result = ""
286
+ height.times do |y|
287
+ line = ""
288
+ width.times do |x|
289
+ if rand < 0.3
290
+ brightness = rand(100..255)
291
+ char = chars.sample
292
+ line += "\e[38;2;0;#{brightness};0m#{char}\e[0m"
293
+ else
294
+ line += " "
295
+ end
296
+ end
297
+ result += line + "\n"
298
+ end
299
+ result
300
+ end
301
+
302
+ def loading_art(frame)
303
+ frames = [
304
+ "[ ● ]",
305
+ "[ ● ]",
306
+ "[ ● ]",
307
+ "[ ● ]",
308
+ "[ ● ]",
309
+ "[ ● ]",
310
+ ]
311
+ "\e[38;2;0;255;255m#{frames[frame % frames.length]}\e[0m"
312
+ end
313
+ end
314
+ end
315
+ end