11import React , { useMemo } from 'react' ;
2- import ReactDOMServer from 'react-dom/server' ;
3- import { DownloadSimple } from '@phosphor-icons/react' ;
42
5- const GenerativeArt = ( ) => {
6- const art = useMemo ( ( ) => {
7- const SVG_SIZE = 500 ;
8- const NUM_SHAPES = 25 ;
3+ const GenerativeArt = ( { seed = 'fezcodex' , className } ) => {
4+ // Sanitize seed for use in SVG IDs
5+ const safeId = useMemo ( ( ) => seed . replace ( / [ ^ a - z 0 - 9 ] / gi, '-' ) . toLowerCase ( ) , [ seed ] ) ;
6+
7+ const shapes = useMemo ( ( ) => {
8+ // Seeded RNG
9+ let h = 0xdeadbeef ;
10+ const safeSeed = seed || 'fezcodex' ;
11+ for ( let i = 0 ; i < safeSeed . length ; i ++ ) {
12+ h = Math . imul ( h ^ safeSeed . charCodeAt ( i ) , 2654435761 ) ;
13+ }
14+ const rng = ( ) => {
15+ h = Math . imul ( h ^ ( h >>> 16 ) , 2246822507 ) ;
16+ h = Math . imul ( h ^ ( h >>> 13 ) , 3266489909 ) ;
17+ return ( ( h ^= h >>> 16 ) >>> 0 ) / 4294967296 ;
18+ } ;
19+
20+ const hue = Math . floor ( rng ( ) * 360 ) ;
21+ const primaryColor = `hsl(${ hue } , 70%, 60%)` ;
22+ const secondaryColor = `hsl(${ ( hue + 180 ) % 360 } , 60%, 50%)` ;
23+ const accentColor = `hsl(${ ( hue + 90 ) % 360 } , 80%, 60%)` ;
24+
25+ const type = Math . floor ( rng ( ) * 3 ) ;
926 const shapes = [ ] ;
1027
11- const colors = [ '#FBBF24' , '#A7F3D0' , '#F87171' , '#60A5FA' , '#A78BFA' ] ;
28+ if ( type === 0 ) {
29+ // Bauhaus Grid
30+ const gridSize = 5 ;
31+ const cellSize = 100 / gridSize ;
32+ let count = 0 ;
1233
13- for ( let i = 0 ; i < NUM_SHAPES ; i ++ ) {
14- const x = Math . random ( ) * SVG_SIZE ;
15- const y = Math . random ( ) * SVG_SIZE ;
16- const width = 10 + Math . random ( ) * 150 ;
17- const height = 10 + Math . random ( ) * 150 ;
18- const fill = colors [ Math . floor ( Math . random ( ) * colors . length ) ] ;
19- const rotation = Math . random ( ) * 360 ;
20- const opacity = 0.5 + Math . random ( ) * 0.5 ;
34+ const addShape = ( x , y ) => {
35+ const shapeType = Math . floor ( rng ( ) * 4 ) ;
36+ const rotation = Math . floor ( rng ( ) * 4 ) * 90 ;
37+ const colorRoll = rng ( ) ;
38+ let color = primaryColor ;
39+ if ( colorRoll > 0.6 ) color = secondaryColor ;
40+ if ( colorRoll > 0.9 ) color = accentColor ;
41+ if ( colorRoll < 0.1 ) color = '#ffffff' ;
42+ shapes . push ( { mode : 'grid' , x : x * cellSize , y : y * cellSize , size : cellSize , shapeType, rotation, color, isOutline : rng ( ) > 0.6 } ) ;
43+ count ++ ;
44+ } ;
2145
22- shapes . push (
23- < rect
24- key = { i }
25- x = { x }
26- y = { y }
27- width = { width }
28- height = { height }
29- fill = { fill }
30- opacity = { opacity }
31- transform = { `rotate(${ rotation } ${ x + width / 2 } ${ y + height / 2 } )` }
32- /> ,
33- ) ;
46+ for ( let x = 0 ; x < gridSize ; x ++ ) {
47+ for ( let y = 0 ; y < gridSize ; y ++ ) {
48+ if ( rng ( ) > 0.5 ) addShape ( x , y ) ;
49+ }
50+ }
51+ // Guarantee at least 5 shapes
52+ if ( count < 5 ) {
53+ for ( let i = 0 ; i < 5 ; i ++ ) addShape ( Math . floor ( rng ( ) * gridSize ) , Math . floor ( rng ( ) * gridSize ) ) ;
54+ }
55+ } else if ( type === 1 ) {
56+ // Tech Circuit
57+ const count = 15 ;
58+ for ( let i = 0 ; i < count ; i ++ ) {
59+ const isVertical = rng ( ) > 0.5 ;
60+ const x = Math . floor ( rng ( ) * 10 ) * 10 ;
61+ const y = Math . floor ( rng ( ) * 10 ) * 10 ;
62+ const thickness = 0.5 + rng ( ) * 1.5 ;
63+ shapes . push ( {
64+ mode : 'tech' ,
65+ x, y, isVertical, length : 20 + rng ( ) * 60 , thickness,
66+ color : rng ( ) > 0.8 ? '#ffffff' : primaryColor ,
67+ opacity : 0.4 + rng ( ) * 0.6
68+ } ) ;
69+ if ( rng ( ) > 0.4 ) shapes . push ( { mode : 'node' , cx : x , cy : y , r : thickness * 2 , color : accentColor } ) ;
70+ }
71+ } else {
72+ // Geometric Flow
73+ for ( let i = 0 ; i < 8 ; i ++ ) {
74+ shapes . push ( {
75+ mode : 'flow' ,
76+ cx : rng ( ) * 100 , cy : rng ( ) * 100 ,
77+ r : 10 + rng ( ) * 40 ,
78+ color : i % 2 === 0 ? primaryColor : secondaryColor ,
79+ opacity : 0.3 + rng ( ) * 0.3
80+ } ) ;
81+ }
3482 }
3583
36- return (
37- < svg
38- width = "100%"
39- height = "100%"
40- viewBox = { `0 0 ${ SVG_SIZE } ${ SVG_SIZE } ` }
41- xmlns = "http://www.w3.org/2000/svg"
42- >
84+ return shapes ;
85+ } , [ seed ] ) ;
86+
87+ return (
88+ < div className = { `w-full h-full bg-neutral-950 overflow-hidden relative ${ className } ` } >
89+ < svg viewBox = "0 0 100 100" preserveAspectRatio = "xMidYMid slice" className = "w-full h-full" >
4390 < defs >
44- < clipPath id = "art-board ">
45- < rect width = { SVG_SIZE } height = { SVG_SIZE } />
46- </ clipPath >
91+ < pattern id = { `bg-grid- ${ safeId } ` } width = "20" height = "20" patternUnits = "userSpaceOnUse ">
92+ < circle cx = "1" cy = "1" r = "0.5" fill = "white" opacity = "0.05" />
93+ </ pattern >
4794 </ defs >
48- < rect width = { SVG_SIZE } height = { SVG_SIZE } fill = "#1F2937" />
49- < g clipPath = "url(#art-board)" > { shapes } </ g >
95+ < rect width = "100" height = "100" fill = { `url(#bg-grid-${ safeId } )` } />
96+ { shapes . map ( ( s , i ) => {
97+ if ( s . mode === 'grid' ) {
98+ const center = s . size / 2 ;
99+ const p = s . size * 0.1 ;
100+ const is = s . size - ( p * 2 ) ;
101+ return (
102+ < g key = { i } transform = { `translate(${ s . x } , ${ s . y } ) rotate(${ s . rotation } , ${ center } , ${ center } )` } >
103+ { s . shapeType === 0 && < rect x = { p } y = { p } width = { is } height = { is } fill = { s . isOutline ? 'none' : s . color } stroke = { s . color } strokeWidth = { s . isOutline ? 1.5 : 0 } opacity = "0.9" rx = "1" /> }
104+ { s . shapeType === 1 && < circle cx = { center } cy = { center } r = { is / 2 } fill = { s . isOutline ? 'none' : s . color } stroke = { s . color } strokeWidth = { s . isOutline ? 1.5 : 0 } opacity = "0.9" /> }
105+ { s . shapeType === 2 && < path d = { `M ${ p } ${ p } L ${ s . size - p } ${ p } A ${ is } ${ is } 0 0 1 ${ p } ${ s . size - p } Z` } fill = { s . color } opacity = "0.9" /> }
106+ { s . shapeType === 3 && < polygon points = { `${ p } ,${ s . size - p } ${ s . size / 2 } ,${ p } ${ s . size - p } ,${ s . size - p } ` } fill = { s . isOutline ? 'none' : s . color } stroke = { s . color } strokeWidth = { s . isOutline ? 1.5 : 0 } opacity = "0.9" /> }
107+ </ g >
108+ ) ;
109+ }
110+ if ( s . mode === 'tech' ) return < rect key = { i } x = { s . x } y = { s . y } width = { s . isVertical ? s . thickness : s . length } height = { s . isVertical ? s . length : s . thickness } fill = { s . color } opacity = { s . opacity } /> ;
111+ if ( s . mode === 'node' ) return < circle key = { i } cx = { s . cx } cy = { s . cy } r = { s . r } fill = { s . color } opacity = "0.8" /> ;
112+ if ( s . mode === 'flow' ) return < circle key = { i } cx = { s . cx } cy = { s . cy } r = { s . r } fill = { s . color } opacity = { s . opacity } style = { { mixBlendMode : 'screen' } } /> ;
113+ return null ;
114+ } ) }
50115 </ svg >
51- ) ;
52- } , [ ] ) ;
53-
54- const handleDownload = ( ) => {
55- const svgString = ReactDOMServer . renderToString ( art ) ;
56- const blob = new Blob ( [ svgString ] , { type : 'image/svg+xml' } ) ;
57- const url = URL . createObjectURL ( blob ) ;
58- const link = document . createElement ( 'a' ) ;
59- link . href = url ;
60- link . download = 'generative-art.svg' ;
61- document . body . appendChild ( link ) ;
62- link . click ( ) ;
63- document . body . removeChild ( link ) ;
64- URL . revokeObjectURL ( url ) ;
65- } ;
66-
67- return (
68- < div >
69- < div
70- style = { {
71- width : '100%' ,
72- height : '60vh' ,
73- border : '1px solid #374151' ,
74- borderRadius : '8px' ,
75- overflow : 'hidden' ,
76- } }
77- >
78- { art }
79- </ div >
80- < div className = "flex justify-center mt-4" >
81- < button
82- onClick = { handleDownload }
83- className = "flex items-center gap-2 text-lg font-arvo font-normal px-6 py-2 rounded-md border transition-colors duration-300 ease-in-out border-green-700 bg-green-800/50 text-white hover:bg-green-700/50"
84- >
85- < DownloadSimple size = { 24 } />
86- Download SVG
87- </ button >
88- </ div >
116+ < div className = "absolute inset-0 opacity-[0.15] pointer-events-none mix-blend-overlay" style = { { backgroundImage : `url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3'/%3E%3C/filter%3E%3Crect width='512' height='512' filter='url(%23n)'/%3E%3C/svg%3E")` } } />
89117 </ div >
90118 ) ;
91119} ;
92120
93- export default GenerativeArt ;
121+ export default GenerativeArt ;
0 commit comments