515 lines
15 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
@text_lifetime 40 # How many frames the text stays visible
18
19
# Add DVD animation configuration
20
@dvd_speed 2
21
@viewport_width 600
22
@viewport_height 400
23
@logo_width 100
24
@logo_height 50
25
26
# Add new animation configs
27
@bubble_count 15
28
@star_count 5
29
@spiral_arms 6
30
31
def mount(_params, _session, socket) do
32
if connected?(socket) do
33
Process.send_after(self(), :animate, @frame_interval)
34
end
35
36
{:ok,
37
assign(socket,
38
rainbows: [], # List of rainbow states
39
letters: [], # List to track letters and their positions/lifetimes
40
play_sound: false, # Add this to track when to play sound
41
# Add DVD logo state
42
dvd_pos: %{
43
x: Enum.random(0..@viewport_width),
44
y: Enum.random(0..@viewport_height),
45
dx: @dvd_speed,
46
dy: @dvd_speed,
47
hue: 0
48
},
49
# Add new animation states
50
bubbles: create_bubbles(),
51
stars: create_stars(),
52
spiral_rotation: 0,
53
spiral_arms: @spiral_arms,
54
meta_attrs: [
55
%{name: "title", content: "Rainbow Animation"},
56
%{name: "description", content: "Bobby is doinking around with art and computers. type on the page to see the rainbow animation change."},
57
%{property: "og:title", content: "Rainbow Animation shit started this lets see where it goes, it changes when you type on the page"},
58
%{property: "og:description", content: "Bobby is doinking around with art and computers. type on the page to see the rainbow animation change."},
59
%{property: "og:type", content: "website"}
60
],
61
page_title: "Rainbow Animation"
62
)}
63
end
64
65
def handle_event("keydown", %{"key" => key}, socket) when byte_size(key) == 1 do
66
# Create a new rainbow
67
new_rainbow = %{
68
id: System.unique_integer([:positive]),
69
frame: 0,
70
x: Enum.random(-100..100),
71
y: Enum.random(-50..50)
72
}
73
74
# Create a new letter with rotation properties
75
new_letter = %{
76
id: System.unique_integer([:positive]),
77
char: key,
78
frame: 0,
79
x: new_rainbow.x,
80
y: new_rainbow.y,
81
size: Enum.random(50..150),
82
rotation_speed: Enum.random(-10..10), # Degrees per frame
83
rotation: Enum.random(0..360) # Initial rotation
84
}
85
86
{:noreply,
87
assign(socket,
88
rainbows: [new_rainbow | socket.assigns.rainbows],
89
letters: [new_letter | socket.assigns.letters]
90
)}
91
end
92
93
def handle_event("keydown", _key, socket), do: {:noreply, socket}
94
95
def handle_info(:animate, socket) do
96
Process.send_after(self(), :animate, @frame_interval)
97
98
# Update DVD position and handle bouncing
99
dvd_pos = update_dvd_position(socket.assigns.dvd_pos)
100
101
# Track which rainbows are completing this frame
102
{completing, continuing} = socket.assigns.rainbows
103
|> Enum.map(fn rainbow ->
104
%{rainbow | frame: rainbow.frame + 1}
105
end)
106
|> Enum.split_with(fn rainbow ->
107
rainbow.frame >= @animation_steps
108
end)
109
110
# Set play_sound if any rainbows completed
111
should_play = length(completing) > 0
112
113
# Update letters with rotation
114
updated_letters = socket.assigns.letters
115
|> Enum.map(fn letter ->
116
%{letter |
117
frame: letter.frame + 1,
118
rotation: letter.rotation + letter.rotation_speed
119
}
120
end)
121
|> Enum.reject(fn letter ->
122
letter.frame >= @text_lifetime
123
end)
124
125
# Update new animations
126
updated_bubbles = update_bubbles(socket.assigns.bubbles)
127
updated_stars = update_stars(socket.assigns.stars)
128
updated_spiral = rem(socket.assigns.spiral_rotation + 2, 360)
129
130
{:noreply, assign(socket,
131
rainbows: continuing,
132
letters: updated_letters,
133
play_sound: should_play,
134
dvd_pos: dvd_pos,
135
bubbles: updated_bubbles,
136
stars: updated_stars,
137
spiral_rotation: updated_spiral
138
)}
139
end
140
141
defp update_dvd_position(pos) do
142
new_x = pos.x + pos.dx
143
new_y = pos.y + pos.dy
144
145
{dx, new_hue} = if new_x <= -(@viewport_width/2) + @logo_width or new_x >= (@viewport_width/2) - @logo_width do
146
{-pos.dx, rem(pos.hue + 60, 360)}
147
else
148
{pos.dx, pos.hue}
149
end
150
151
{dy, final_hue} = if new_y <= -(@viewport_height/2) + @logo_height or new_y >= (@viewport_height/2) - @logo_height do
152
{-pos.dy, rem(new_hue + 60, 360)}
153
else
154
{pos.dy, new_hue}
155
end
156
157
%{
158
x: new_x,
159
y: new_y,
160
dx: dx,
161
dy: dy,
162
hue: final_hue
163
}
164
end
165
166
defp calculate_arcs(frame) do
167
progress = frame / @animation_steps
168
169
@rainbow_colors
170
|> Enum.with_index()
171
|> Enum.map(fn {color, index} ->
172
radius = @max_radius - (index * 40)
173
arc_progress = min(1.0, progress * 1.2 - (index * 0.1))
174
175
if arc_progress > 0 do
176
generate_arc_path(radius, arc_progress, color)
177
end
178
end)
179
|> Enum.reject(&is_nil/1)
180
end
181
182
defp generate_arc_path(radius, progress, color) do
183
end_angle = :math.pi * progress
184
end_x = radius * :math.cos(end_angle)
185
end_y = radius * :math.sin(end_angle)
186
187
path = "M #{radius} 0 A #{radius} #{radius} 0 0 1 #{end_x} #{end_y}"
188
189
%{
190
path: path,
191
color: color,
192
stroke_width: 20
193
}
194
end
195
196
defp calculate_letter_opacity(frame) do
197
1 - (frame / @text_lifetime)
198
end
199
200
defp create_bubbles do
201
for _i <- 1..@bubble_count do
202
%{
203
x: Enum.random(-300..300),
204
y: Enum.random(-200..200),
205
size: Enum.random(10..30),
206
speed: Enum.random(1..3),
207
hue: Enum.random(0..360),
208
offset: Enum.random(0..100)
209
}
210
end
211
end
212
213
defp create_stars do
214
for _i <- 1..@star_count do
215
%{
216
x: -400,
217
y: Enum.random(-200..200),
218
angle: :math.atan2(Enum.random(-100..100), 400),
219
speed: Enum.random(5..10),
220
length: Enum.random(30..60),
221
hue: Enum.random(0..360),
222
active: true
223
}
224
end
225
end
226
227
defp update_bubbles(bubbles) do
228
Enum.map(bubbles, fn bubble ->
229
new_y = bubble.y - bubble.speed
230
y = if new_y < -250, do: 250, else: new_y
231
232
# Add subtle horizontal movement using sine wave
233
x_offset = :math.sin((bubble.offset + y) / 50) * 5
234
235
%{bubble |
236
y: y,
237
x: bubble.x + x_offset
238
}
239
end)
240
end
241
242
defp update_stars(stars) do
243
Enum.map(stars, fn star ->
244
if star.active do
245
new_x = star.x + :math.cos(star.angle) * star.speed
246
new_y = star.y + :math.sin(star.angle) * star.speed
247
248
if new_x > 400 do
249
# Reset star to start position with new random values
250
%{star |
251
x: -400,
252
y: Enum.random(-200..200),
253
angle: :math.atan2(Enum.random(-100..100), 400),
254
hue: Enum.random(0..360)
255
}
256
else
257
%{star | x: new_x, y: new_y}
258
end
259
else
260
star
261
end
262
end)
263
end
264
265
defp generate_spiral_path(rotation) do
266
points = for t <- 0..50 do
267
r = t * 2
268
angle = t * 0.5 + rotation * :math.pi / 180
269
x = r * :math.cos(angle)
270
y = r * :math.sin(angle)
271
"#{x},#{y}"
272
end
273
274
"M " <> Enum.join(points, " L ")
275
end
276
277
def render(assigns) do
278
~H"""
279
<div class="flex justify-center items-center min-h-screen" phx-window-keydown="keydown">
280
<%!-- Add SVG filters for noise --%>
281
<svg width="0" height="0">
282
<defs>
283
<filter id="noise">
284
<feTurbulence
285
type="fractalNoise"
286
baseFrequency="0.6"
287
numOctaves="3"
288
seed="1"
289
>
290
<animate
291
attributeName="seed"
292
from="1"
293
to="100"
294
dur="3s"
295
repeatCount="indefinite"
296
/>
297
</feTurbulence>
298
<feColorMatrix type="saturate" values="0"/>
299
<feBlend mode="multiply" in2="SourceGraphic"/>
300
</filter>
301
302
<%!-- Add shine effect for DVD logo --%>
303
<filter id="shine">
304
<feGaussianBlur in="SourceAlpha" stdDeviation="2" result="blur"/>
305
<feSpecularLighting in="blur" surfaceScale="5" specularConstant="1" specularExponent="20" result="spec">
306
<fePointLight x="-5000" y="-10000" z="20000"/>
307
</feSpecularLighting>
308
<feComposite in="SourceGraphic" in2="spec" operator="arithmetic" k1="0" k2="1" k3="1" k4="0"/>
309
</filter>
310
</defs>
311
</svg>
312
313
<%!-- Background with noise --%>
314
<div class="absolute inset-0 bg-gray-900 animate-noise">
315
<style>
316
@keyframes shift {
317
from {
318
filter: url(#noise) hue-rotate(0deg);
319
opacity: 0.3;
320
}
321
to {
322
filter: url(#noise) hue-rotate(360deg);
323
opacity: 0.4;
324
}
325
}
326
.animate-noise {
327
animation: shift 20s linear infinite;
328
background: linear-gradient(
329
45deg,
330
rgba(20, 20, 30, 0.9),
331
rgba(40, 40, 60, 0.9)
332
);
333
}
334
</style>
335
</div>
336
337
<%!-- Main content with DVD logo --%>
338
<svg width="100%" height="100vh" viewBox="-300 -200 600 400" style="position: relative; z-index: 1;">
339
<%!-- DVD Logo --%>
340
<g transform={"translate(#{@dvd_pos.x}, #{@dvd_pos.y})"}>
341
<path
342
d="M-50,-25 h100 v50 h-100 z M-30,-15 L-10,15 H10 L30,-15 H-30 Z M-20,0 h40 M-25,-10 h50"
343
fill={"hsl(#{@dvd_pos.hue}, 100%, 70%)"}
344
filter="url(#shine)"
345
style="transform-origin: center; transform: scale(0.8);"
346
>
347
<animate
348
attributeName="opacity"
349
values="0.8;1;0.8"
350
dur="2s"
351
repeatCount="indefinite"
352
/>
353
</path>
354
<text
355
x="0"
356
y="5"
357
text-anchor="middle"
358
fill="white"
359
font-family="Arial Black"
360
font-size="20"
361
filter="url(#shine)"
362
>DVD</text>
363
</g>
364
365
<%!-- Floating Bubbles --%>
366
<%= for bubble <- @bubbles do %>
367
<circle
368
cx={bubble.x}
369
cy={bubble.y}
370
r={bubble.size}
371
fill={"hsla(#{bubble.hue}, 100%, 70%, 0.6)"}
372
filter="url(#shine)"
373
>
374
<animate
375
attributeName="r"
376
values={"#{bubble.size};#{bubble.size * 1.2};#{bubble.size}"}
377
dur="2s"
378
repeatCount="indefinite"
379
begin={"#{bubble.offset}ms"}
380
/>
381
</circle>
382
<% end %>
383
384
<%!-- Shooting Stars --%>
385
<%= for star <- @stars do %>
386
<g transform={"translate(#{star.x}, #{star.y})"}>
387
<path
388
d={"M0,0 L#{star.length},0"}
389
stroke={"hsl(#{star.hue}, 100%, 70%)"}
390
stroke-width="2"
391
transform={"rotate(#{:math.atan2(:math.sin(star.angle), :math.cos(star.angle)) * 180 / :math.pi})"}
392
>
393
<animate
394
attributeName="stroke-width"
395
values="2;4;2"
396
dur="0.5s"
397
repeatCount="indefinite"
398
/>
399
</path>
400
</g>
401
<% end %>
402
403
<%!-- Pulsing Spiral --%>
404
<%= for i <- 0..(@spiral_arms - 1) do %>
405
<path
406
d={generate_spiral_path(i * 360 / @spiral_arms + @spiral_rotation)}
407
stroke={"hsl(#{i * 360 / @spiral_arms}, 100%, 70%)"}
408
stroke-width="2"
409
fill="none"
410
opacity="0.5"
411
>
412
<animate
413
attributeName="stroke-width"
414
values="2;4;2"
415
dur="1s"
416
repeatCount="indefinite"
417
begin={"#{i * 150}ms"}
418
/>
419
</path>
420
<% end %>
421
422
<%!-- Keep existing rainbows and letters --%>
423
<%= for rainbow <- @rainbows do %>
424
<g transform={"translate(#{rainbow.x}, #{rainbow.y})"}>
425
<%= for arc <- calculate_arcs(rainbow.frame) do %>
426
<path
427
d={arc.path}
428
stroke={arc.color}
429
stroke-width={arc.stroke_width}
430
fill="none"
431
/>
432
<% end %>
433
</g>
434
<% end %>
435
<%= for letter <- @letters do %>
436
<text
437
x={letter.x}
438
y={letter.y}
439
font-size={letter.size}
440
fill="white"
441
text-anchor="middle"
442
dominant-baseline="middle"
443
opacity={calculate_letter_opacity(letter.frame)}
444
transform={"rotate(#{letter.rotation}, #{letter.x}, #{letter.y})"}
445
>
446
<%= letter.char %>
447
</text>
448
<% end %>
449
</svg>
450
451
<%!-- Keep existing script and sound trigger --%>
452
<script>
453
let audioCtx;
454
455
// Initialize audio context on first user interaction
456
window.addEventListener('keydown', () => {
457
if (!audioCtx) {
458
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
459
}
460
});
461
462
// Function to play a pleasant chime sound
463
function playChime() {
464
if (!audioCtx) {
465
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
466
}
467
468
const oscillator = audioCtx.createOscillator();
469
const gainNode = audioCtx.createGain();
470
471
oscillator.connect(gainNode);
472
gainNode.connect(audioCtx.destination);
473
474
oscillator.frequency.setValueAtTime(523.25, audioCtx.currentTime);
475
oscillator.type = 'sine';
476
477
gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
478
gainNode.gain.linearRampToValueAtTime(0.3, audioCtx.currentTime + 0.01);
479
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
480
481
oscillator.start();
482
oscillator.stop(audioCtx.currentTime + 0.3);
483
}
484
485
// Watch for changes to play_sound
486
let lastPlaySound = false;
487
const observer = new MutationObserver(function(mutations) {
488
mutations.forEach(function(mutation) {
489
if (mutation.type === "attributes" && mutation.attributeName === "data-play-sound") {
490
const shouldPlay = mutation.target.getAttribute("data-play-sound") === "true";
491
if (shouldPlay && !lastPlaySound) {
492
playChime();
493
}
494
lastPlaySound = shouldPlay;
495
}
496
});
497
});
498
499
// Start observing when the element is available
500
document.addEventListener('DOMContentLoaded', () => {
501
const trigger = document.getElementById('sound-trigger');
502
if (trigger) {
503
observer.observe(trigger, {
504
attributes: true,
505
attributeFilter: ['data-play-sound']
506
});
507
}
508
});
509
</script>
510
511
<div id="sound-trigger" data-play-sound={@play_sound} phx-update="ignore"></div>
512
</div>
513
"""
514
end
515
end
516