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