11import { useEffect , useRef } from 'react' ;
22
3+ type CanvasBounds = {
4+ width : number ;
5+ height : number ;
6+ } ;
7+
8+ type ResizeSubscription = {
9+ observe : ( target : HTMLElement ) => void ;
10+ disconnect : ( ) => void ;
11+ } ;
12+
313export type MeteorsBackgroundProps = {
414 gridSize ?: number ;
515 meteorCount ?: number ;
@@ -22,7 +32,7 @@ class Meteor {
2232
2333 constructor (
2434 private readonly gridSize : number ,
25- private readonly canvas : HTMLCanvasElement ,
35+ private readonly bounds : CanvasBounds ,
2636 ) {
2737 this . reset ( ) ;
2838 }
@@ -41,26 +51,26 @@ class Meteor {
4151 switch ( this . direction ) {
4252 case Direction . UP :
4353 this . x =
44- Math . floor ( getMiddlePosition ( this . canvas . width ) / this . gridSize ) *
54+ Math . floor ( getMiddlePosition ( this . bounds . width ) / this . gridSize ) *
4555 this . gridSize ;
46- this . y = this . canvas . height ;
56+ this . y = this . bounds . height ;
4757 break ;
4858 case Direction . RIGHT :
4959 this . x = 0 ;
5060 this . y =
51- Math . floor ( getMiddlePosition ( this . canvas . height ) / this . gridSize ) *
61+ Math . floor ( getMiddlePosition ( this . bounds . height ) / this . gridSize ) *
5262 this . gridSize ;
5363 break ;
5464 case Direction . DOWN :
5565 this . x =
56- Math . floor ( getMiddlePosition ( this . canvas . width ) / this . gridSize ) *
66+ Math . floor ( getMiddlePosition ( this . bounds . width ) / this . gridSize ) *
5767 this . gridSize ;
5868 this . y = 0 ;
5969 break ;
6070 case Direction . LEFT :
61- this . x = this . canvas . width ;
71+ this . x = this . bounds . width ;
6272 this . y =
63- Math . floor ( getMiddlePosition ( this . canvas . height ) / this . gridSize ) *
73+ Math . floor ( getMiddlePosition ( this . bounds . height ) / this . gridSize ) *
6474 this . gridSize ;
6575 break ;
6676 }
@@ -76,13 +86,13 @@ class Meteor {
7686 break ;
7787 case Direction . RIGHT :
7888 this . x += this . speed ;
79- if ( this . x > this . canvas . width ) {
89+ if ( this . x > this . bounds . width ) {
8090 this . reset ( ) ;
8191 }
8292 break ;
8393 case Direction . DOWN :
8494 this . y += this . speed ;
85- if ( this . y > this . canvas . height ) {
95+ if ( this . y > this . bounds . height ) {
8696 this . reset ( ) ;
8797 }
8898 break ;
@@ -154,38 +164,106 @@ export function MeteorsBackground({
154164 return ;
155165 }
156166
157- const setCanvasSize = ( ) => {
158- canvas . width = window . innerWidth ;
159- canvas . height = window . innerHeight * 0.65 ;
160- } ;
167+ const gridCanvas = document . createElement ( 'canvas' ) ;
168+ const gridContext = gridCanvas . getContext ( '2d' ) ;
161169
162- setCanvasSize ( ) ;
163- window . addEventListener ( 'resize' , setCanvasSize ) ;
170+ if ( ! gridContext ) {
171+ return ;
172+ }
173+
174+ const bounds : CanvasBounds = {
175+ width : 0 ,
176+ height : 0 ,
177+ } ;
164178
165179 const meteors = Array . from (
166180 { length : meteorCount } ,
167- ( ) => new Meteor ( gridSize , canvas ) ,
181+ ( ) => new Meteor ( gridSize , bounds ) ,
168182 ) ;
169183
184+ const drawGrid = ( devicePixelRatio : number ) => {
185+ if ( bounds . width === 0 || bounds . height === 0 ) {
186+ gridCanvas . width = 0 ;
187+ gridCanvas . height = 0 ;
188+ return ;
189+ }
190+
191+ gridCanvas . width = Math . max (
192+ 1 ,
193+ Math . round ( bounds . width * devicePixelRatio ) ,
194+ ) ;
195+ gridCanvas . height = Math . max (
196+ 1 ,
197+ Math . round ( bounds . height * devicePixelRatio ) ,
198+ ) ;
199+ gridContext . setTransform ( devicePixelRatio , 0 , 0 , devicePixelRatio , 0 , 0 ) ;
200+ gridContext . clearRect ( 0 , 0 , bounds . width , bounds . height ) ;
201+ gridContext . strokeStyle = 'rgba(128, 128, 128, 0.1)' ;
202+ gridContext . lineWidth = 1 ;
203+
204+ for ( let x = 0 ; x < bounds . width ; x += gridSize ) {
205+ gridContext . beginPath ( ) ;
206+ gridContext . moveTo ( x , 0 ) ;
207+ gridContext . lineTo ( x , bounds . height ) ;
208+ gridContext . stroke ( ) ;
209+ }
210+
211+ for ( let y = 0 ; y < bounds . height ; y += gridSize ) {
212+ gridContext . beginPath ( ) ;
213+ gridContext . moveTo ( 0 , y ) ;
214+ gridContext . lineTo ( bounds . width , y ) ;
215+ gridContext . stroke ( ) ;
216+ }
217+ } ;
218+
219+ const setCanvasSize = ( ) => {
220+ const { width, height } = canvas . getBoundingClientRect ( ) ;
221+
222+ if ( width === 0 || height === 0 ) {
223+ bounds . width = 0 ;
224+ bounds . height = 0 ;
225+ canvas . width = 0 ;
226+ canvas . height = 0 ;
227+ return ;
228+ }
229+
230+ bounds . width = Math . round ( width ) ;
231+ bounds . height = Math . round ( height ) ;
232+
233+ const devicePixelRatio = window . devicePixelRatio || 1 ;
234+
235+ canvas . width = Math . max ( 1 , Math . round ( bounds . width * devicePixelRatio ) ) ;
236+ canvas . height = Math . max ( 1 , Math . round ( bounds . height * devicePixelRatio ) ) ;
237+ context . setTransform ( devicePixelRatio , 0 , 0 , devicePixelRatio , 0 , 0 ) ;
238+ drawGrid ( devicePixelRatio ) ;
239+
240+ for ( const meteor of meteors ) {
241+ meteor . reset ( ) ;
242+ }
243+ } ;
244+
245+ const resizeSubscription : ResizeSubscription =
246+ typeof ResizeObserver !== 'undefined'
247+ ? new ResizeObserver ( setCanvasSize )
248+ : {
249+ observe : ( _target : HTMLElement ) => {
250+ window . addEventListener ( 'resize' , setCanvasSize ) ;
251+ } ,
252+ disconnect : ( ) => {
253+ window . removeEventListener ( 'resize' , setCanvasSize ) ;
254+ } ,
255+ } ;
256+
257+ resizeSubscription . observe ( canvas ) ;
258+ setCanvasSize ( ) ;
259+
170260 let animationFrameId = 0 ;
171261
172262 const animate = ( ) => {
173- context . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
174- context . strokeStyle = 'rgba(128, 128, 128, 0.1)' ;
175- context . lineWidth = 1 ;
176-
177- for ( let x = 0 ; x < canvas . width ; x += gridSize ) {
178- context . beginPath ( ) ;
179- context . moveTo ( x , 0 ) ;
180- context . lineTo ( x , canvas . height ) ;
181- context . stroke ( ) ;
182- }
263+ context . clearRect ( 0 , 0 , bounds . width , bounds . height ) ;
183264
184- for ( let y = 0 ; y < canvas . height ; y += gridSize ) {
185- context . beginPath ( ) ;
186- context . moveTo ( 0 , y ) ;
187- context . lineTo ( canvas . width , y ) ;
188- context . stroke ( ) ;
265+ if ( gridCanvas . width > 0 && gridCanvas . height > 0 ) {
266+ context . drawImage ( gridCanvas , 0 , 0 , bounds . width , bounds . height ) ;
189267 }
190268
191269 for ( const meteor of meteors ) {
@@ -200,7 +278,7 @@ export function MeteorsBackground({
200278
201279 return ( ) => {
202280 window . cancelAnimationFrame ( animationFrameId ) ;
203- window . removeEventListener ( 'resize' , setCanvasSize ) ;
281+ resizeSubscription . disconnect ( ) ;
204282 } ;
205283 } , [ gridSize , meteorCount ] ) ;
206284
0 commit comments