449 lines
13 kB
1
defmodule BlogWeb.RainbowLive do
2
use BlogWeb, :live_view
3
require Logger
4
5
@rainbow_colors [
6
"#FF0000", # Red
7
"#FF7F00", # Orange
8
"#FFFF00", # Yellow
9
"#00FF00", # Green
10
"#0000FF", # Blue
11
"#4B0082", # Indigo
12
"#9400D3" # Violet
13
]
14
@frame_interval 50 # 50ms between frames
15
@max_radius 300
16
@animation_steps 60
17
18
# Add DVD animation configuration
19
@dvd_speed 2
20
@viewport_width 600
21
@viewport_height 400
22
@logo_width 100
23
@logo_height 50
24
25
# Add these module attributes
26
@particle_count 12 # Number of particles per explosion
27
@particle_lifetime 80 # How long particles live
28
29
# Add new module attributes
30
@exhaust_particle_lifetime 40
31
@exhaust_emit_interval 2 # Emit particles every N frames
32
@exhaust_drift_speed 1.5
33
34
# Add these module attributes
35
@max_fractal_depth 7
36
@fractal_draw_interval 100 # How many frames between adding new points
37
38
def mount(_params, _session, socket) do
39
if connected?(socket) do
40
Process.send_after(self(), :animate, @frame_interval)
41
end
42
43
{:ok,
44
assign(socket,
45
rainbows: [], # List of rainbow states
46
letters: [], # Add letters state back
47
particles: [], # Add particles state
48
dvd_pos: %{
49
x: Enum.random(-300..300),
50
y: Enum.random(-200..200),
51
dx: @dvd_speed,
52
dy: @dvd_speed,
53
hue: 0
54
},
55
meta_attrs: [
56
%{name: "title", content: "Type shit and hear sounds and see wild shit or whatever"},
57
%{name: "description", content: "Bobby got high and made it so it looks wild when you press keys, theres web audio too but its broken."},
58
%{property: "og:title", content: "Type shit and hear sounds and see wild shit or whatever"},
59
%{property: "og:description", content: "Bobby got high and made it so it looks wild when you press keys, theres web audio too but its broken."},
60
%{property: "og:type", content: "website"}
61
],
62
page_title: "lol start typing and see what happens",
63
mouse_pos: %{x: 0, y: 0},
64
exhaust_particles: [],
65
frame_count: 0, # For controlling particle emission rate
66
colors_inverted: false,
67
fractal_depth: 0,
68
fractal_points: initial_fractal_points()
69
)}
70
end
71
72
def handle_event("keydown", %{"key" => key}, socket) when byte_size(key) == 1 do
73
# Create position for all animations
74
pos_x = Enum.random(-100..100)
75
pos_y = Enum.random(-50..50)
76
77
# Create a new rainbow
78
new_rainbow = %{
79
id: System.unique_integer([:positive]),
80
frame: 0,
81
x: pos_x,
82
y: pos_y
83
}
84
85
# Create particles for spiral explosion
86
new_particles = for i <- 1..@particle_count do
87
angle = i * (2 * :math.pi / @particle_count)
88
%{
89
id: System.unique_integer([:positive]),
90
frame: 0,
91
x: pos_x,
92
y: pos_y,
93
dx: :math.cos(angle) * 3,
94
dy: :math.sin(angle) * 3,
95
hue: Enum.random(0..360),
96
size: Enum.random(5..15),
97
rotation: angle * 180 / :math.pi
98
}
99
end
100
101
# Create a new letter with enhanced animation
102
new_letter = %{
103
id: System.unique_integer([:positive]),
104
char: key,
105
frame: 0,
106
x: pos_x,
107
y: pos_y,
108
size: 200, # Bigger initial size
109
rotation_speed: Enum.random(-5..5),
110
rotation: 0,
111
scale: 0.1 # Start small and grow
112
}
113
114
{:noreply,
115
assign(socket,
116
rainbows: [new_rainbow | socket.assigns.rainbows],
117
letters: [new_letter | socket.assigns.letters],
118
particles: new_particles ++ (socket.assigns[:particles] || []),
119
mouse_pos: %{x: 0, y: 0},
120
exhaust_particles: [],
121
frame_count: 0
122
)}
123
end
124
125
def handle_event("keydown", _key, socket), do: {:noreply, socket}
126
127
def handle_event("mousemove", %{"offsetX" => x, "offsetY" => y}, socket) do
128
# Convert screen coordinates to SVG viewBox coordinates
129
svg_x = (x / socket.assigns.window_width * 600) - 300
130
svg_y = (y / socket.assigns.window_height * 400) - 200
131
132
{:noreply, assign(socket, mouse_pos: %{x: svg_x, y: svg_y})}
133
end
134
135
def handle_event("click", _params, socket) do
136
{:noreply, assign(socket, colors_inverted: !socket.assigns.colors_inverted)}
137
end
138
139
def handle_info(:animate, socket) do
140
Process.send_after(self(), :animate, @frame_interval)
141
142
# Update DVD position
143
dvd_pos = update_dvd_position(socket.assigns.dvd_pos)
144
145
# Update rainbows
146
updated_rainbows = socket.assigns.rainbows
147
|> Enum.map(fn rainbow ->
148
%{rainbow | frame: rainbow.frame + 1}
149
end)
150
|> Enum.reject(fn rainbow ->
151
rainbow.frame >= @animation_steps
152
end)
153
154
# Update letters with growth and rotation
155
updated_letters = socket.assigns.letters
156
|> Enum.map(fn letter ->
157
new_scale = min(1.0, letter.scale + 0.05) # Grow to full size
158
%{letter |
159
frame: letter.frame + 1,
160
rotation: letter.rotation + letter.rotation_speed,
161
scale: new_scale
162
}
163
end)
164
|> Enum.reject(fn letter ->
165
letter.frame >= @animation_steps
166
end)
167
168
# Update particles
169
updated_particles = socket.assigns.particles
170
|> Enum.map(fn particle ->
171
%{particle |
172
frame: particle.frame + 1,
173
x: particle.x + particle.dx,
174
y: particle.y + particle.dy,
175
rotation: particle.rotation + 5
176
}
177
end)
178
|> Enum.reject(fn particle ->
179
particle.frame >= @particle_lifetime
180
end)
181
182
# Create new exhaust particles periodically
183
{new_exhaust, frame_count} = if rem(socket.assigns.frame_count, @exhaust_emit_interval) == 0 do
184
new_particles = for _i <- 1..3 do
185
%{
186
x: socket.assigns.mouse_pos.x,
187
y: socket.assigns.mouse_pos.y,
188
dx: :rand.normal() * @exhaust_drift_speed,
189
dy: :rand.normal() * @exhaust_drift_speed,
190
size: Enum.random(3..8),
191
frame: 0,
192
hue: Enum.random(200..240) # Blue-ish colors
193
}
194
end
195
{new_particles, socket.assigns.frame_count + 1}
196
else
197
{[], socket.assigns.frame_count + 1}
198
end
199
200
# Update existing exhaust particles
201
updated_exhaust = (socket.assigns.exhaust_particles ++ new_exhaust)
202
|> Enum.map(fn particle ->
203
%{particle |
204
frame: particle.frame + 1,
205
x: particle.x + particle.dx,
206
y: particle.y + particle.dy,
207
size: particle.size * 1.02 # Slowly grow
208
}
209
end)
210
|> Enum.reject(fn particle ->
211
particle.frame >= @exhaust_particle_lifetime
212
end)
213
214
# Update fractal every @fractal_draw_interval frames
215
{new_depth, new_points} = if rem(socket.assigns.frame_count, @fractal_draw_interval) == 0
216
and socket.assigns.fractal_depth < @max_fractal_depth do
217
{
218
socket.assigns.fractal_depth + 1,
219
generate_next_fractal_points(socket.assigns.fractal_points)
220
}
221
else
222
{socket.assigns.fractal_depth, socket.assigns.fractal_points}
223
end
224
225
{:noreply, assign(socket,
226
rainbows: updated_rainbows,
227
letters: updated_letters,
228
particles: updated_particles,
229
dvd_pos: dvd_pos,
230
exhaust_particles: updated_exhaust,
231
frame_count: frame_count,
232
fractal_depth: new_depth,
233
fractal_points: new_points
234
)}
235
end
236
237
# Add DVD position update logic
238
defp update_dvd_position(pos) do
239
new_x = pos.x + pos.dx
240
new_y = pos.y + pos.dy
241
242
{dx, new_hue} = if new_x <= -(@viewport_width/2) + @logo_width or new_x >= (@viewport_width/2) - @logo_width do
243
{-pos.dx, rem(pos.hue + 60, 360)}
244
else
245
{pos.dx, pos.hue}
246
end
247
248
{dy, final_hue} = if new_y <= -(@viewport_height/2) + @logo_height or new_y >= (@viewport_height/2) - @logo_height do
249
{-pos.dy, rem(new_hue + 60, 360)}
250
else
251
{pos.dy, new_hue}
252
end
253
254
%{
255
x: new_x,
256
y: new_y,
257
dx: dx,
258
dy: dy,
259
hue: final_hue
260
}
261
end
262
263
defp calculate_arcs(frame) do
264
progress = frame / @animation_steps
265
266
@rainbow_colors
267
|> Enum.with_index()
268
|> Enum.map(fn {color, index} ->
269
radius = @max_radius - (index * 40)
270
arc_progress = min(1.0, progress * 1.2 - (index * 0.1))
271
272
if arc_progress > 0 do
273
generate_arc_path(radius, arc_progress, color)
274
end
275
end)
276
|> Enum.reject(&is_nil/1)
277
end
278
279
defp generate_arc_path(radius, progress, color) do
280
end_angle = :math.pi * progress
281
end_x = radius * :math.cos(end_angle)
282
end_y = radius * :math.sin(end_angle)
283
284
path = "M #{radius} 0 A #{radius} #{radius} 0 0 1 #{end_x} #{end_y}"
285
286
%{
287
path: path,
288
color: color,
289
stroke_width: 20
290
}
291
end
292
293
defp initial_fractal_points do
294
# Start with the outer triangle
295
[
296
{0, -150}, # Top
297
{-130, 75}, # Bottom left
298
{130, 75} # Bottom right
299
]
300
end
301
302
defp generate_next_fractal_points(points) do
303
new_points = for i <- 0..(length(points) - 2), j <- (i + 1)..(length(points) - 1) do
304
{x1, y1} = Enum.at(points, i)
305
{x2, y2} = Enum.at(points, j)
306
{
307
(x1 + x2) / 2,
308
(y1 + y2) / 2
309
}
310
end
311
312
points ++ new_points
313
end
314
315
def render(assigns) do
316
~H"""
317
<div class="flex justify-center items-center min-h-screen bg-gray-900"
318
id="rainbow-container"
319
phx-window-keydown="keydown"
320
phx-mousemove="mousemove"
321
phx-click="click"
322
phx-hook="WindowSize">
323
<svg width="100%" height="100vh" viewBox="-300 -200 600 400"
324
style={"filter: #{if @colors_inverted, do: "invert(1)", else: "none"}"}>
325
<%!-- Exhaust particles --%>
326
<%= for particle <- @exhaust_particles do %>
327
<circle
328
cx={particle.x}
329
cy={particle.y}
330
r={particle.size}
331
fill={"hsla(#{particle.hue}, 70%, 50%, #{calculate_exhaust_opacity(particle.frame)})"}
332
filter="url(#blur)"
333
/>
334
<% end %>
335
336
<%!-- Add blur filter for smoother particles --%>
337
<defs>
338
<filter id="blur">
339
<feGaussianBlur stdDeviation="2" />
340
</filter>
341
</defs>
342
343
<%!-- DVD Logo --%>
344
<g transform={"translate(#{@dvd_pos.x}, #{@dvd_pos.y})"}>
345
<path
346
d="M-50,-25 h100 v50 h-100 z"
347
fill={"hsl(#{@dvd_pos.hue}, 100%, 70%)"}
348
style="transform-origin: center; transform: scale(0.8);"
349
>
350
<animate
351
attributeName="opacity"
352
values="0.8;1;0.8"
353
dur="2s"
354
repeatCount="indefinite"
355
/>
356
</path>
357
<text
358
x="0"
359
y="0"
360
text-anchor="middle"
361
dominant-baseline="middle"
362
fill="white"
363
font-family="Arial Black"
364
font-size="30"
365
style="font-weight: bold;"
366
>DVD</text>
367
</g>
368
369
<%!-- Particles --%>
370
<%= for particle <- @particles do %>
371
<g transform={"translate(#{particle.x}, #{particle.y}) rotate(#{particle.rotation})"}>
372
<path
373
d="M0,-#{particle.size} L#{particle.size/2},#{particle.size} L-#{particle.size/2},#{particle.size} Z"
374
fill={"hsl(#{particle.hue}, 100%, 70%)"}
375
opacity={calculate_particle_opacity(particle.frame)}
376
/>
377
</g>
378
<% end %>
379
380
<%!-- Rainbows --%>
381
<%= for rainbow <- @rainbows do %>
382
<g transform={"translate(#{rainbow.x}, #{rainbow.y})"}>
383
<%= for arc <- calculate_arcs(rainbow.frame) do %>
384
<path
385
d={arc.path}
386
stroke={arc.color}
387
stroke-width={arc.stroke_width}
388
fill="none"
389
/>
390
<% end %>
391
</g>
392
<% end %>
393
394
<%!-- Letters --%>
395
<%= for letter <- @letters do %>
396
<text
397
x={letter.x}
398
y={letter.y}
399
font-size={letter.size}
400
fill="white"
401
text-anchor="middle"
402
dominant-baseline="middle"
403
opacity={calculate_letter_opacity(letter.frame)}
404
transform={"rotate(#{letter.rotation}, #{letter.x}, #{letter.y}) scale(#{letter.scale})"}
405
>
406
<%= letter.char %>
407
</text>
408
<% end %>
409
410
<%!-- Sierpinski Fractal --%>
411
<%= for {x, y} <- @fractal_points do %>
412
<circle
413
cx={x}
414
cy={y}
415
r="1"
416
fill={"hsla(#{rem(@fractal_depth * 60, 360)}, 70%, 70%, 0.3)"}
417
/>
418
<% end %>
419
</svg>
420
421
<style>
422
@keyframes shift {
423
from {
424
filter: url(#noise) hue-rotate(0deg) <%= if @colors_inverted, do: "invert(1)" %>;
425
opacity: 0.3;
426
}
427
to {
428
filter: url(#noise) hue-rotate(360deg) <%= if @colors_inverted, do: "invert(1)" %>;
429
opacity: 0.4;
430
}
431
}
432
</style>
433
</div>
434
"""
435
end
436
437
defp calculate_letter_opacity(frame) do
438
1 - (frame / @animation_steps)
439
end
440
441
defp calculate_particle_opacity(frame) do
442
1 - (frame / @particle_lifetime)
443
end
444
445
defp calculate_exhaust_opacity(frame) do
446
opacity = 1 - (frame / @exhaust_particle_lifetime)
447
opacity * 0.6 # Make them semi-transparent
448
end
449
end
450