1
+
2
+ type PixelPosition = {
3
+ row : number ;
4
+ col : number ;
5
+ normalizedX : number ; // 0 to 1 from left to right
6
+ normalizedY : number ; // 0 to 1 from top to bottom
7
+ } ;
8
+
9
+ // --------------------------------------------------------------------------------
10
+
11
+ export function createPixel ( position : PixelPosition , emptyBias : string , emptyRatio : number , colors : readonly string [ ] ) : HTMLElement {
12
+ const pixel = document . createElement ( 'div' ) ;
13
+ pixel . className = 'pixel' ;
14
+
15
+ const isEmpty = shouldPixelBeEmpty ( position , emptyBias , emptyRatio ) ;
16
+
17
+ if ( isEmpty ) {
18
+ pixel . classList . add ( 'pixel-empty' ) ;
19
+ } else {
20
+ applyPixelStyling ( pixel , colors ) ;
21
+ }
22
+
23
+ return pixel ;
24
+ }
25
+
26
+ export function createPosition ( row : number , col : number , gridCols : number , gridRows : number ) : PixelPosition {
27
+ return {
28
+ row,
29
+ col,
30
+ normalizedX : col / ( gridCols - 1 ) ,
31
+ normalizedY : row / ( gridRows - 1 )
32
+ } ;
33
+ }
34
+
35
+ // --------------------------------------------------------------------------------
36
+
37
+ function shouldPixelBeEmpty ( position : PixelPosition , emptyBias : string , emptyRatio : number ) : boolean {
38
+ if ( emptyBias === 'right' ) {
39
+ return calculateRightBiasEmpty ( position , emptyRatio ) ;
40
+ } else if ( emptyBias === 'left' ) {
41
+ return calculateLeftBiasEmpty ( position , emptyRatio ) ;
42
+ }
43
+ return false ;
44
+ }
45
+
46
+ function calculateRightBiasEmpty ( position : PixelPosition , emptyRatio : number ) : boolean {
47
+ const organicBoundary = calculateOrganicBoundary ( position , emptyRatio ) ;
48
+
49
+ if ( position . normalizedX > organicBoundary ) {
50
+ return addGhostPixelChance ( ) ;
51
+ }
52
+
53
+ return calculateScatterChance ( position , organicBoundary ) ;
54
+ }
55
+
56
+ function calculateLeftBiasEmpty ( position : PixelPosition , emptyRatio : number ) : boolean {
57
+ const organicBoundary = calculateOrganicBoundary ( position , 1 - emptyRatio ) ;
58
+
59
+ if ( position . normalizedX < organicBoundary ) {
60
+ return addGhostPixelChance ( ) ;
61
+ }
62
+
63
+ const distanceFromEdge = Math . abs ( position . normalizedX - organicBoundary ) ;
64
+ const scatterChance = Math . pow ( 1 - distanceFromEdge / ( 1 - organicBoundary ) , 2 ) * 0.25 ;
65
+ const rowVariation = Math . sin ( position . normalizedY * Math . PI * 2 ) * 0.1 ;
66
+
67
+ return Math . random ( ) < ( scatterChance + rowVariation ) ;
68
+ }
69
+
70
+ function calculateOrganicBoundary ( position : PixelPosition , baseRatio : number ) : number {
71
+ const waveOffset = Math . sin ( position . normalizedY * Math . PI * 3 ) * 0.1 ;
72
+ const randomOffset = ( Math . random ( ) - 0.5 ) * 0.15 ;
73
+ return baseRatio + waveOffset + randomOffset ;
74
+ }
75
+
76
+ function calculateScatterChance ( position : PixelPosition , boundary : number ) : boolean {
77
+ const distanceFromEdge = Math . abs ( position . normalizedX - boundary ) ;
78
+ const scatterChance = Math . pow ( 1 - distanceFromEdge / boundary , 2 ) * 0.25 ;
79
+ const rowVariation = Math . sin ( position . normalizedY * Math . PI * 2 ) * 0.1 ;
80
+
81
+ return Math . random ( ) < ( scatterChance + rowVariation ) ;
82
+ }
83
+
84
+ function addGhostPixelChance ( ) : boolean {
85
+ return Math . random ( ) >= 0.3 ;
86
+ }
87
+
88
+ function applyPixelStyling ( pixel : HTMLElement , colors : readonly string [ ] ) : void {
89
+ const randomColor = colors [ Math . floor ( Math . random ( ) * colors . length ) ] ;
90
+ pixel . style . background = randomColor ;
91
+ }
0 commit comments