1+ const MAX_PER_ROW = 20 ;
2+ const SPACING = 24 ;
3+ const SPEED = 6 ;
4+
5+ const TOTK_COLOR = [ 248 , 108 , 50 , 255 ] ;
6+ const BOTW_COLOR = [ 253 , 58 , 56 , 255 ] ;
7+ const EMPTY_COLOR = [ 0 , 0 , 0 , 200 ] ;
8+
9+ const canvas = document . getElementById ( "canvas" ) ;
10+ const ctx = canvas . getContext ( "2d" ) ;
11+
12+ const heartImg = new Image ( ) ;
13+ heartImg . src = "heart.png" ;
14+ heartImg . onload = render ;
15+
16+ const healSound = new Audio ( "increment_health.ogg" ) ;
17+ healSound . preload = "auto" ;
18+ healSound . load ( ) ;
19+
20+ let displayedValue = parseFloat ( document . getElementById ( "max" ) . value ) ;
21+ let targetValue = displayedValue ;
22+ let lastTime = 0 ;
23+ let scale = 0.5 ;
24+
25+ function createColoredHeart ( colorRGBA ) {
26+ const w = heartImg . width ;
27+ const h = heartImg . height ;
28+
29+ const temp = document . createElement ( "canvas" ) ;
30+ temp . width = w ;
31+ temp . height = h ;
32+ const tctx = temp . getContext ( "2d" ) ;
33+
34+ // fill solid color
35+ tctx . clearRect ( 0 , 0 , w , h ) ;
36+ tctx . fillStyle = `rgba(${ colorRGBA [ 0 ] } ,${ colorRGBA [ 1 ] } ,${ colorRGBA [ 2 ] } ,${ colorRGBA [ 3 ] / 255 } )` ;
37+ tctx . fillRect ( 0 , 0 , w , h ) ;
38+
39+ // use heart alpha as mask
40+ tctx . globalCompositeOperation = "destination-in" ;
41+ tctx . drawImage ( heartImg , 0 , 0 ) ;
42+ tctx . globalCompositeOperation = "source-over" ;
43+
44+ return temp ;
45+ }
46+
47+ function createEmptyHeart ( ) {
48+ const w = heartImg . width ;
49+ const h = heartImg . height ;
50+
51+ const temp = document . createElement ( "canvas" ) ;
52+ temp . width = w ;
53+ temp . height = h ;
54+ const tctx = temp . getContext ( "2d" ) ;
55+
56+ // base color
57+ tctx . clearRect ( 0 , 0 , w , h ) ;
58+ tctx . fillStyle = `rgba(${ EMPTY_COLOR [ 0 ] } ,${ EMPTY_COLOR [ 1 ] } ,${ EMPTY_COLOR [ 2 ] } ,${ EMPTY_COLOR [ 3 ] / 255 } )` ;
59+ tctx . fillRect ( 0 , 0 , w , h ) ;
60+
61+ // mask to heart shape
62+ tctx . globalCompositeOperation = "destination-in" ;
63+ tctx . drawImage ( heartImg , 0 , 0 ) ;
64+ tctx . globalCompositeOperation = "source-over" ;
65+
66+ return temp ;
67+ }
68+
69+ function createPartialHeart ( fraction , filledHeart , emptyHeart ) {
70+ const w = heartImg . width ;
71+ const h = heartImg . height ;
72+
73+ const temp = document . createElement ( "canvas" ) ;
74+ temp . width = w ;
75+ temp . height = h ;
76+ const tctx = temp . getContext ( "2d" ) ;
77+
78+ // draw empty heart first
79+ tctx . drawImage ( emptyHeart , 0 , 0 ) ;
80+
81+ // calculate arc angles
82+ const cx = w / 2 ;
83+ const cy = h / 2 ;
84+ const radius = Math . max ( w , h ) ; // big enough to cover entire heart
85+
86+ const startAngle = - Math . PI / 2 ;
87+ const endAngle = startAngle - ( Math . PI * 2 * fraction ) ;
88+
89+ // create angular clipping mask, kinda confusing but I got it down
90+ tctx . save ( ) ;
91+ tctx . beginPath ( ) ;
92+ tctx . moveTo ( cx , cy ) ;
93+ tctx . arc ( cx , cy , radius , startAngle , endAngle , true ) ;
94+ tctx . closePath ( ) ;
95+ tctx . clip ( ) ;
96+
97+ // draw filled portion inside clip
98+ tctx . drawImage ( filledHeart , 0 , 0 ) ;
99+ tctx . restore ( ) ;
100+
101+ return temp ;
102+ }
103+
104+ function drawHearts ( value ) {
105+ let max = parseInt ( document . getElementById ( "max" ) . value ) ;
106+ if ( max <= 0 || max > 40 ) { max = Math . ceil ( value ) ; }
107+
108+ value = Math . min ( value , max ) ;
109+
110+ const w = heartImg . width ;
111+ const h = heartImg . height ;
112+
113+ const full = Math . floor ( value ) ;
114+ const remainder = value - full ;
115+ const fraction = Math . max ( 0 , Math . min ( 1 , remainder ) ) ;
116+
117+ const style = document . getElementById ( "style" ) . value ;
118+ const filledHeart = createColoredHeart ( style == "0" ? TOTK_COLOR : BOTW_COLOR ) ;
119+ const emptyHeart = createEmptyHeart ( ) ;
120+
121+ const hearts = [ ] ;
122+
123+ // full hearts
124+ for ( let i = 0 ; i < full ; i ++ ) {
125+ hearts . push ( filledHeart ) ;
126+ }
127+
128+ // partial heart
129+ if ( fraction > 0 && hearts . length < max ) {
130+ hearts . push ( createPartialHeart ( fraction , filledHeart , emptyHeart ) ) ;
131+ }
132+
133+ // empty hearts
134+ while ( hearts . length < max ) {
135+ hearts . push ( emptyHeart ) ;
136+ }
137+
138+ // layout
139+ const cols = Math . min ( hearts . length , MAX_PER_ROW ) ;
140+ const rows = Math . ceil ( hearts . length / MAX_PER_ROW ) ;
141+
142+ canvas . width = ( cols * w + ( cols - 1 ) * SPACING ) * scale ;
143+ canvas . height = ( rows * h + ( rows - 1 ) * SPACING ) * scale ;
144+
145+ ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
146+
147+ // draw these hearts yo
148+ hearts . forEach ( ( heart , i ) => {
149+ const row = Math . floor ( i / MAX_PER_ROW ) ;
150+ const col = i % MAX_PER_ROW ;
151+ const x = col * ( w + SPACING ) * scale ;
152+ const y = row * ( h + SPACING ) * scale ;
153+ ctx . drawImage ( heart , x , y , heartImg . width * scale , heartImg . height * scale ) ;
154+ } ) ;
155+ }
156+
157+ function animate ( timestamp ) {
158+ if ( ! lastTime ) lastTime = timestamp ;
159+ const deltaTime = ( timestamp - lastTime ) / 1000 ; // seconds, fixed tickrate is so much easier to work with :pensive:
160+ lastTime = timestamp ;
161+
162+ const diff = targetValue - displayedValue ;
163+ const direction = Math . sign ( diff ) ;
164+ const step = SPEED * deltaTime * direction ;
165+
166+ let prevDisplayedValue = displayedValue ;
167+
168+ if ( Math . abs ( step ) >= Math . abs ( diff ) ) {
169+ displayedValue = targetValue ;
170+ } else {
171+ displayedValue += step ;
172+ }
173+
174+ if ( Math . floor ( prevDisplayedValue ) < Math . floor ( displayedValue ) ) {
175+ healSound . cloneNode ( ) . play ( ) ;
176+ }
177+
178+ drawHearts ( displayedValue ) ;
179+
180+ requestAnimationFrame ( animate ) ;
181+ }
182+
183+ function render ( ) {
184+ if ( ! heartImg . complete ) return ;
185+
186+ let current = parseFloat ( document . getElementById ( "current" ) . value ) ;
187+ let max = parseInt ( document . getElementById ( "max" ) . value ) ;
188+
189+ if ( current < 0 || current > 40 ) current = 0 ;
190+ if ( max <= 0 || max > 40 ) max = Math . ceil ( current ) ;
191+
192+ current = Math . min ( current , max ) ;
193+
194+ targetValue = current ;
195+ }
196+
197+ function updateScale ( ) {
198+ scale = parseFloat ( document . getElementById ( "scale" ) . value ) ;
199+ document . getElementById ( "display" ) . innerText = scale ;
200+ }
201+
202+ updateScale ( ) ;
203+ render ( ) ;
204+ requestAnimationFrame ( animate ) ;
0 commit comments