@@ -18,6 +18,208 @@ document.addEventListener('DOMContentLoaded', () => {
1818 } ) ;
1919} ) ;
2020
21+ document . addEventListener ( 'DOMContentLoaded' , ( ) => {
22+ const sections = document . querySelectorAll ( '.section' ) ;
23+ const palette = [ '#f8fafc' , '#c7d2fe' , '#93c5fd' , '#a3e635' , '#facc15' ] ;
24+ const pixelSize = 8 ;
25+ const maxPixels = 140 ;
26+
27+ sections . forEach ( ( section ) => {
28+ const canvas = document . createElement ( 'canvas' ) ;
29+ canvas . className = 'pixel-trail-canvas' ;
30+ canvas . setAttribute ( 'aria-hidden' , 'true' ) ;
31+ section . prepend ( canvas ) ;
32+
33+ const ctx = canvas . getContext ( '2d' ) ;
34+ if ( ! ctx ) return ;
35+
36+ const pixels = [ ] ;
37+ let animationFrame = null ;
38+ let dpr = Math . min ( window . devicePixelRatio || 1 , 2 ) ;
39+ let lastSpawnAt = 0 ;
40+
41+ function resizeCanvas ( ) {
42+ const bounds = section . getBoundingClientRect ( ) ;
43+ dpr = Math . min ( window . devicePixelRatio || 1 , 2 ) ;
44+ canvas . width = Math . floor ( bounds . width * dpr ) ;
45+ canvas . height = Math . floor ( bounds . height * dpr ) ;
46+ ctx . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 ) ;
47+ ctx . scale ( dpr , dpr ) ;
48+ }
49+
50+ function spawnPixel ( clientX , clientY ) {
51+ const bounds = section . getBoundingClientRect ( ) ;
52+ const x = clientX - bounds . left ;
53+ const y = clientY - bounds . top ;
54+
55+ for ( let i = 0 ; i < 3 ; i ++ ) {
56+ pixels . push ( {
57+ x : x + ( Math . random ( ) - 0.5 ) * 18 ,
58+ y : y + ( Math . random ( ) - 0.5 ) * 18 ,
59+ vx : ( Math . random ( ) - 0.5 ) * 1.4 ,
60+ vy : ( Math . random ( ) - 0.5 ) * 1.4 - 0.3 ,
61+ life : 1 ,
62+ fade : 0.018 + Math . random ( ) * 0.02 ,
63+ size : pixelSize + Math . floor ( Math . random ( ) * 2 ) * 4 ,
64+ color : palette [ Math . floor ( Math . random ( ) * palette . length ) ]
65+ } ) ;
66+ }
67+
68+ if ( pixels . length > maxPixels ) {
69+ pixels . splice ( 0 , pixels . length - maxPixels ) ;
70+ }
71+
72+ if ( ! animationFrame ) {
73+ animationFrame = requestAnimationFrame ( renderPixels ) ;
74+ }
75+ }
76+
77+ function renderPixels ( ) {
78+ const width = canvas . width / dpr ;
79+ const height = canvas . height / dpr ;
80+ ctx . clearRect ( 0 , 0 , width , height ) ;
81+
82+ for ( let i = pixels . length - 1 ; i >= 0 ; i -- ) {
83+ const pixel = pixels [ i ] ;
84+ pixel . x += pixel . vx ;
85+ pixel . y += pixel . vy ;
86+ pixel . vy += 0.01 ;
87+ pixel . life -= pixel . fade ;
88+
89+ if ( pixel . life <= 0 ) {
90+ pixels . splice ( i , 1 ) ;
91+ continue ;
92+ }
93+
94+ ctx . globalAlpha = pixel . life ;
95+ ctx . fillStyle = pixel . color ;
96+ ctx . fillRect (
97+ Math . round ( pixel . x / pixelSize ) * pixelSize ,
98+ Math . round ( pixel . y / pixelSize ) * pixelSize ,
99+ pixel . size ,
100+ pixel . size
101+ ) ;
102+ }
103+
104+ ctx . globalAlpha = 1 ;
105+
106+ if ( pixels . length > 0 ) {
107+ animationFrame = requestAnimationFrame ( renderPixels ) ;
108+ } else {
109+ animationFrame = null ;
110+ }
111+ }
112+
113+ function handlePointer ( clientX , clientY ) {
114+ const now = performance . now ( ) ;
115+ if ( now - lastSpawnAt < 22 ) return ;
116+ lastSpawnAt = now ;
117+ spawnPixel ( clientX , clientY ) ;
118+ }
119+
120+ section . addEventListener ( 'mousemove' , ( event ) => {
121+ handlePointer ( event . clientX , event . clientY ) ;
122+ } ) ;
123+
124+ section . addEventListener ( 'touchmove' , ( event ) => {
125+ const touch = event . touches [ 0 ] ;
126+ if ( ! touch ) return ;
127+ handlePointer ( touch . clientX , touch . clientY ) ;
128+ } , { passive : true } ) ;
129+
130+ window . addEventListener ( 'resize' , resizeCanvas ) ;
131+ resizeCanvas ( ) ;
132+ } ) ;
133+ } ) ;
134+
135+ document . addEventListener ( 'DOMContentLoaded' , ( ) => {
136+ const mainSection = document . getElementById ( 'main' ) ;
137+ if ( ! mainSection ) return ;
138+
139+ const canvas = document . createElement ( 'canvas' ) ;
140+ canvas . className = 'pixel-rain-canvas' ;
141+ canvas . setAttribute ( 'aria-hidden' , 'true' ) ;
142+ mainSection . prepend ( canvas ) ;
143+
144+ const ctx = canvas . getContext ( '2d' ) ;
145+ if ( ! ctx ) return ;
146+
147+ const drops = [ ] ;
148+ const dropCount = 52 ;
149+ const palette = [ '#c7d2fe' , '#93c5fd' , '#f8fafc' , '#a3e635' ] ;
150+ let dpr = Math . min ( window . devicePixelRatio || 1 , 2 ) ;
151+ let width = 0 ;
152+ let height = 0 ;
153+
154+ function resizeCanvas ( ) {
155+ const bounds = mainSection . getBoundingClientRect ( ) ;
156+ dpr = Math . min ( window . devicePixelRatio || 1 , 2 ) ;
157+ width = bounds . width ;
158+ height = bounds . height ;
159+ canvas . width = Math . floor ( width * dpr ) ;
160+ canvas . height = Math . floor ( height * dpr ) ;
161+ ctx . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 ) ;
162+ ctx . scale ( dpr , dpr ) ;
163+ }
164+
165+ function resetDrop ( drop , initial = false ) {
166+ const startBand = Math . max ( width , height ) * 0.35 ;
167+ drop . x = initial ? Math . random ( ) * width : ( Math . random ( ) * ( width + startBand ) ) - startBand ;
168+ drop . y = initial ? Math . random ( ) * height : - Math . random ( ) * ( height * 0.45 + 80 ) ;
169+ drop . vx = 0.9 + Math . random ( ) * 1.4 ;
170+ drop . vy = 1.3 + Math . random ( ) * 1.9 ;
171+ drop . length = 10 + Math . floor ( Math . random ( ) * 4 ) * 4 ;
172+ drop . size = 4 + Math . floor ( Math . random ( ) * 2 ) * 4 ;
173+ drop . alpha = 0.15 + Math . random ( ) * 0.22 ;
174+ drop . color = palette [ Math . floor ( Math . random ( ) * palette . length ) ] ;
175+ }
176+
177+ function initDrops ( ) {
178+ drops . length = 0 ;
179+ for ( let i = 0 ; i < dropCount ; i ++ ) {
180+ const drop = { } ;
181+ resetDrop ( drop , true ) ;
182+ drops . push ( drop ) ;
183+ }
184+ }
185+
186+ function renderRain ( ) {
187+ ctx . clearRect ( 0 , 0 , width , height ) ;
188+
189+ for ( const drop of drops ) {
190+ drop . x += drop . vx ;
191+ drop . y += drop . vy ;
192+
193+ if ( drop . y - drop . length > height || drop . x - drop . length > width ) {
194+ resetDrop ( drop , false ) ;
195+ }
196+
197+ ctx . globalAlpha = drop . alpha ;
198+ ctx . fillStyle = drop . color ;
199+
200+ for ( let segment = 0 ; segment < 3 ; segment ++ ) {
201+ const segX = Math . round ( ( drop . x - segment * drop . length ) / 8 ) * 8 ;
202+ const segY = Math . round ( ( drop . y - segment * drop . length ) / 8 ) * 8 ;
203+ const segAlpha = drop . alpha * ( 1 - segment * 0.28 ) ;
204+ ctx . globalAlpha = segAlpha ;
205+ ctx . fillRect ( segX , segY , drop . size , drop . size ) ;
206+ }
207+ }
208+
209+ ctx . globalAlpha = 1 ;
210+ requestAnimationFrame ( renderRain ) ;
211+ }
212+
213+ window . addEventListener ( 'resize' , ( ) => {
214+ resizeCanvas ( ) ;
215+ initDrops ( ) ;
216+ } ) ;
217+
218+ resizeCanvas ( ) ;
219+ initDrops ( ) ;
220+ requestAnimationFrame ( renderRain ) ;
221+ } ) ;
222+
21223document . addEventListener ( 'DOMContentLoaded' , ( ) => {
22224 const sections = document . querySelectorAll ( '.section' ) ;
23225 let currentSection = 'main' ;
@@ -196,5 +398,3 @@ document.getElementById('menu-toggle').addEventListener('click', function () {
196398 const menu = document . getElementById ( 'mobile-menu' ) ;
197399 menu . classList . toggle ( 'hidden' ) ;
198400} ) ;
199-
200-
0 commit comments