11"use client" ;
22
3- import { useEffect , useState } from "react" ;
4- import { motion , useMotionValue , useTransform } from "framer-motion" ;
3+ import { useMemo } from "react" ;
4+ import { motion , useReducedMotion } from "framer-motion" ;
55
66const codeTags = [
77 "const" ,
@@ -15,80 +15,142 @@ const codeTags = [
1515 "=>" ,
1616] ;
1717
18+ /** Seeded RNG so positions/timings are stable */
19+ function mulberry32 ( seed : number ) {
20+ let t = seed >>> 0 ;
21+ return ( ) => {
22+ t += 0x6d2b79f5 ;
23+ let r = Math . imul ( t ^ ( t >>> 15 ) , 1 | t ) ;
24+ r ^= r + Math . imul ( r ^ ( r >>> 7 ) , 61 | r ) ;
25+ return ( ( r ^ ( r >>> 14 ) ) >>> 0 ) / 4294967296 ;
26+ } ;
27+ }
28+ type Pos = { top : number ; left : number } ;
29+ type CornerIndex = 0 | 1 | 2 | 3 ;
30+
31+ function placeAwayFromCenter ( rand : ( ) => number ) : Pos {
32+ let top = rand ( ) * 90 + 5 ;
33+ let left = rand ( ) * 90 + 5 ;
34+
35+ const inCenter = top > 30 && top < 70 && left > 30 && left < 70 ;
36+ if ( inCenter ) {
37+ const cornerIndex = Math . min (
38+ 3 ,
39+ Math . max ( 0 , Math . floor ( rand ( ) * 4 ) ) ,
40+ ) as CornerIndex ;
41+
42+ // Tuple of exactly 4 positions
43+ const ranges : readonly [ Pos , Pos , Pos , Pos ] = [
44+ { top : rand ( ) * 25 + 5 , left : rand ( ) * 25 + 5 } , // TL
45+ { top : rand ( ) * 25 + 5 , left : rand ( ) * 25 + 75 } , // TR
46+ { top : rand ( ) * 25 + 75 , left : rand ( ) * 25 + 5 } , // BL
47+ { top : rand ( ) * 25 + 75 , left : rand ( ) * 25 + 75 } , // BR
48+ ] as const ;
49+
50+ const chosen : Pos = ranges [ cornerIndex ] ; // always defined with tuple indexing
51+ top = chosen . top ;
52+ left = chosen . left ;
53+ }
54+
55+ return { top, left } ;
56+ }
1857export function FloatingCodeTags ( ) {
19- const [ windowSize , setWindowSize ] = useState ( { width : 1000 , height : 800 } ) ;
58+ const reduceMotion = useReducedMotion ( ) ;
2059
21- const mouseX = useMotionValue ( 0 ) ;
22- const mouseY = useMotionValue ( 0 ) ;
60+ const configs = useMemo ( ( ) => {
61+ return codeTags . map ( ( tag , i ) => {
62+ const seed =
63+ Array . from ( ( tag + i ) . toString ( ) ) . reduce (
64+ ( a , c ) => a + c . charCodeAt ( 0 ) ,
65+ 0 ,
66+ ) +
67+ i * 99991 ;
68+ const rnd = mulberry32 ( seed ) ;
2369
24- useEffect ( ( ) => {
25- if ( typeof window === "undefined" ) return ;
70+ const { top, left } = placeAwayFromCenter ( rnd ) ;
71+ const floatDuration = 10 + Math . floor ( rnd ( ) * 6 ) ; // 10–15s
72+ const floatDelay = rnd ( ) * 2 ; // 0–2s
73+ const floatAmplitude = 8 + rnd ( ) * 6 ; // 8–14px
2674
27- setWindowSize ( { width : window . innerWidth , height : window . innerHeight } ) ;
75+ // add a very subtle horizontal drift to avoid uniformity (no parallax)
76+ const drift = 4 + rnd ( ) * 4 ; // 4–8px
77+ const driftDuration = 12 + Math . floor ( rnd ( ) * 6 ) ; // 12–17s
78+ const driftDelay = rnd ( ) * 2 ;
2879
29- const handleMouseMove = ( e : MouseEvent ) => {
30- mouseX . set ( e . clientX ) ;
31- mouseY . set ( e . clientY ) ;
32- } ;
33- window . addEventListener ( "mousemove" , handleMouseMove ) ;
34- return ( ) => window . removeEventListener ( "mousemove" , handleMouseMove ) ;
35- } , [ mouseX , mouseY ] ) ;
80+ return {
81+ key : `${ tag } -${ i } ` ,
82+ tag,
83+ topPct : top ,
84+ leftPct : left ,
85+ floatDuration,
86+ floatDelay,
87+ floatAmplitude,
88+ drift,
89+ driftDuration,
90+ driftDelay,
91+ } ;
92+ } ) ;
93+ } , [ ] ) ;
3694
3795 return (
38- < div className = "pointer-events-none absolute inset-0 z-[1] overflow-hidden" >
39- { codeTags . map ( ( tag , i ) => {
40- const x = useTransform ( mouseX , [ 0 , windowSize . width ] , [ - 8 , 8 ] ) ;
41- const y = useTransform ( mouseY , [ 0 , windowSize . height ] , [ - 8 , 8 ] ) ;
42-
43- // Avoid placing tags in the center area where logo/text is
44- let top = Math . random ( ) * 90 + 5 ;
45- let left = Math . random ( ) * 90 + 5 ;
46-
47- // Keep tags away from center area (30%-70% both horizontally and vertically)
48- if ( top > 30 && top < 70 && left > 30 && left < 70 ) {
49- // Randomly place in corners instead
50- const corners = [
51- { top : Math . random ( ) * 25 + 5 , left : Math . random ( ) * 25 + 5 } , // top-left
52- { top : Math . random ( ) * 25 + 5 , left : Math . random ( ) * 25 + 75 } , // top-right
53- { top : Math . random ( ) * 25 + 75 , left : Math . random ( ) * 25 + 5 } , // bottom-left
54- { top : Math . random ( ) * 25 + 75 , left : Math . random ( ) * 25 + 75 } , // bottom-right
55- ] ;
56- const corner = corners [ Math . floor ( Math . random ( ) * corners . length ) ] ;
57- if ( corner ) {
58- top = corner . top ;
59- left = corner . left ;
60- }
61- }
62-
63- const delay = Math . random ( ) * 2 ;
64-
65- return (
96+ < div
97+ className = "pointer-events-none absolute inset-0 z-[1] overflow-hidden"
98+ aria-hidden
99+ >
100+ { configs . map (
101+ ( {
102+ key,
103+ tag,
104+ topPct,
105+ leftPct,
106+ floatDuration,
107+ floatDelay,
108+ floatAmplitude,
109+ drift,
110+ driftDuration,
111+ driftDelay,
112+ } ) => (
66113 < motion . span
67- key = { tag + i }
68- className = "absolute select-none font-mono text-xs text-white opacity-15 blur-[0.3px] lg:text-sm"
69- style = { {
70- top : `${ top } %` ,
71- left : `${ left } %` ,
72- x,
73- y,
74- } }
75- initial = { { opacity : 0 , scale : 0.6 } }
76- animate = { {
77- opacity : [ 0.1 , 0.2 , 0.1 ] ,
78- scale : [ 0.8 , 1 , 0.8 ] ,
79- y : [ "0%" , "-8%" , "0%" ] ,
80- } }
81- transition = { {
82- duration : 8 + Math . random ( ) * 4 ,
83- repeat : Infinity ,
84- delay,
85- ease : "easeInOut" ,
86- } }
114+ key = { key }
115+ className = "absolute select-none font-mono text-xs text-white/20 will-change-transform lg:text-sm"
116+ style = { { top : `${ topPct } %` , left : `${ leftPct } %` } }
117+ initial = { { opacity : 0 , scale : 0.95 } }
118+ animate = {
119+ reduceMotion
120+ ? { opacity : 0.2 , scale : 1 }
121+ : {
122+ opacity : 0.2 , // steady (no flicker)
123+ scale : 1 ,
124+ // gentle vertical bob + slow horizontal drift
125+ translateY : [ 0 , - floatAmplitude , 0 ] ,
126+ translateX : [ 0 , drift , 0 , - drift , 0 ] ,
127+ }
128+ }
129+ transition = {
130+ reduceMotion
131+ ? { opacity : { duration : 0.6 , ease : "easeOut" } }
132+ : {
133+ opacity : { duration : 0.8 , ease : "easeOut" } ,
134+ scale : { duration : 0.8 , ease : "easeOut" } ,
135+ translateY : {
136+ duration : floatDuration ,
137+ repeat : Infinity ,
138+ delay : floatDelay ,
139+ ease : "easeInOut" ,
140+ } ,
141+ translateX : {
142+ duration : driftDuration ,
143+ repeat : Infinity ,
144+ delay : driftDelay ,
145+ ease : "easeInOut" ,
146+ } ,
147+ }
148+ }
87149 >
88150 { tag }
89151 </ motion . span >
90- ) ;
91- } ) }
152+ ) ,
153+ ) }
92154 </ div >
93155 ) ;
94156}
0 commit comments