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