1+ /*
2+ Copyright (C) 2025 Alexander Emanuelsson (alexemanuelol)
3+
4+ This program is free software: you can redistribute it and/or modify
5+ it under the terms of the GNU General Public License as published by
6+ the Free Software Foundation, either version 3 of the License, or
7+ (at your option) any later version.
8+
9+ This program is distributed in the hope that it will be useful,
10+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+ GNU General Public License for more details.
13+
14+ You should have received a copy of the GNU General Public License
15+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+ https://github.com/alexemanuelol/rustplusplus
18+
19+ */
20+
21+ import { createCanvas , loadImage , Canvas } from '@napi-rs/canvas' ;
22+ import * as fs from 'fs' ;
23+ import * as path from 'path' ;
24+
25+ const TEMPLATES_PATH = 'src/resources/images/icon_templates/' ;
26+ const OUTPUT_PATH_NOTE_MARKERS = 'src/resources/images/note_markers/' ;
27+ const OUTPUT_PATH_MARKERS = 'src/resources/images/markers/' ;
28+
29+ const OTHER_COLORS = {
30+ Active : '#9CDB2E' ,
31+ Inactive : '#DA6816' ,
32+ Black : '#000000'
33+ }
34+
35+ const COLORS = {
36+ Yellow : '#D0D356' ,
37+ Blue : '#3074CA' ,
38+ Green : '#76A738' ,
39+ Red : '#BB3837' ,
40+ Purple : '#B15ABE' ,
41+ Cyan : '#07E8BE'
42+ }
43+
44+ const ICONS = {
45+ Pin : 'icon-pin-bg.png' ,
46+ Dollar : 'icon-dollar.png' ,
47+ Home : 'icon-home.png' ,
48+ Parachute : 'icon-parachute.png' ,
49+ Scope : 'icon-scope.png' ,
50+ Shield : 'icon-shield.png' ,
51+ Skull : 'icon-skull.png' ,
52+ Sleep : 'icon-sleep.png' ,
53+ Zzz : 'icon-zzz.png' ,
54+ Gun : 'icon-gun.png' ,
55+ Rock : 'icon-rock.png' ,
56+ Loot : 'icon-loot.png'
57+ }
58+
59+ async function generateIcon ( basePath : string , maskPath : string , color : string , iconPath : string | null = null ) : Promise < Buffer > {
60+ const baseImage = await loadImage ( basePath ) ;
61+ const maskImage = await loadImage ( maskPath ) ;
62+ const iconImage = iconPath ? await loadImage ( iconPath ) : null ;
63+
64+ const canvas = createCanvas ( baseImage . width , baseImage . height ) ;
65+ const ctx = canvas . getContext ( '2d' ) ;
66+
67+ /* Draw base image */
68+ ctx . drawImage ( baseImage , 0 , 0 ) ;
69+
70+ /* Apply color to base */
71+ ctx . globalCompositeOperation = 'source-atop' ;
72+ ctx . fillStyle = color ;
73+ ctx . fillRect ( 0 , 0 , baseImage . width , baseImage . height ) ;
74+ ctx . globalCompositeOperation = 'source-over' ;
75+
76+ /* Draw mask image */
77+ ctx . drawImage ( maskImage , 0 , 0 ) ;
78+
79+ if ( iconImage ) {
80+ const iconCanvas = createCanvas ( iconImage . width , iconImage . height ) ;
81+ const iconCtx = iconCanvas . getContext ( '2d' ) ;
82+
83+ /* Draw icon iamge */
84+ iconCtx . drawImage ( iconImage , 0 , 0 ) ;
85+
86+ /* Apply color to icon */
87+ iconCtx . globalCompositeOperation = 'source-atop' ;
88+ iconCtx . fillStyle = color ;
89+ iconCtx . fillRect ( 0 , 0 , iconImage . width , iconImage . height ) ;
90+ iconCtx . globalCompositeOperation = 'source-over' ;
91+
92+ /* Center the icon on the base */
93+ const x = ( baseImage . width - iconImage . width ) / 2 ;
94+ const y = ( baseImage . height - iconImage . height ) / 2 ;
95+
96+ /* Draw icon on base */
97+ ctx . drawImage ( iconCanvas as Canvas , x , y ) ;
98+ }
99+
100+ return canvas . toBuffer ( 'image/png' ) ;
101+ }
102+
103+ async function generateOther ( color : string , iconPath : string | null = null ) {
104+ const baseImage = await loadImage ( path . join ( TEMPLATES_PATH , 'icon-bg.png' ) ) ;
105+ const maskImage = await loadImage ( path . join ( TEMPLATES_PATH , 'icon-bg.png' ) ) ;
106+ const iconImage = iconPath ? await loadImage ( iconPath ) : null ;
107+
108+ const canvas = createCanvas ( baseImage . width , baseImage . height ) ;
109+ const ctx = canvas . getContext ( '2d' ) ;
110+
111+ /* Draw base image */
112+ ctx . drawImage ( baseImage , 0 , 0 ) ;
113+
114+ /* Apply color to base */
115+ ctx . globalCompositeOperation = 'source-atop' ;
116+ ctx . fillStyle = OTHER_COLORS . Black ;
117+ ctx . fillRect ( 0 , 0 , baseImage . width , baseImage . height ) ;
118+ ctx . globalCompositeOperation = 'source-over' ;
119+
120+ const borderSize = 15 ;
121+ const innerWidth = baseImage . width - borderSize * 2 ;
122+ const innerHeight = baseImage . height - borderSize * 2 ;
123+
124+ /* Position so it stays centered */
125+ const x = borderSize ;
126+ const y = borderSize ;
127+
128+ ctx . save ( ) ;
129+ ctx . beginPath ( ) ;
130+ ctx . arc ( baseImage . width / 2 , baseImage . height / 2 , innerWidth / 2 , 0 , Math . PI * 2 ) ;
131+ ctx . clip ( ) ;
132+
133+ /* Draw the inner image scaled down */
134+ ctx . drawImage ( maskImage , x , y , innerWidth , innerHeight ) ;
135+
136+ /* Apply color to the mask */
137+ ctx . globalCompositeOperation = 'source-atop' ;
138+ ctx . fillStyle = color ;
139+ ctx . fillRect ( x , y , innerWidth , innerHeight ) ;
140+ ctx . globalCompositeOperation = 'source-over' ;
141+
142+ ctx . restore ( ) ;
143+
144+ if ( iconImage ) {
145+ const maxIconWidth = baseImage . width * 0.5 ;
146+ const maxIconHeight = baseImage . height * 0.5 ;
147+
148+ /* Scale icon while preserving aspect ratio */
149+ let iconWidth = iconImage . width ;
150+ let iconHeight = iconImage . height ;
151+
152+ const widthRatio = maxIconWidth / iconWidth ;
153+ const heightRatio = maxIconHeight / iconHeight ;
154+ const scale = Math . min ( widthRatio , heightRatio , 1 ) ; // don't upscale
155+
156+ iconWidth *= scale ;
157+ iconHeight *= scale ;
158+
159+ /* Center the icon */
160+ const x = ( baseImage . width - iconWidth ) / 2 ;
161+ const y = ( baseImage . height - iconHeight ) / 2 ;
162+
163+ ctx . drawImage ( iconImage , x , y , iconWidth , iconHeight ) ;
164+ }
165+
166+ return canvas . toBuffer ( 'image/png' ) ;
167+ }
168+
169+ ( async ( ) => {
170+ for ( const [ colorName , colorValue ] of Object . entries ( COLORS ) ) {
171+ for ( const [ iconName , iconFile ] of Object . entries ( ICONS ) ) {
172+ for ( const item of [ '' , '-leader' ] ) {
173+ const base = path . join ( TEMPLATES_PATH , iconName === 'Pin' ? 'icon-pin-bg.png' : 'icon-bg.png' ) ;
174+ const mask = path . join ( TEMPLATES_PATH ,
175+ iconName === 'Pin' ? `icon-pin-mask${ item } .png` : `icon-bg-mask${ item } .png` ) ;
176+ const icon = iconName === 'Pin' ? null : path . join ( TEMPLATES_PATH , iconFile ) ;
177+
178+ const buffer = await generateIcon ( base , mask , colorValue , icon ) ;
179+
180+ const fileName = `${ iconName } ${ colorName } ${ item } .png` ;
181+ fs . writeFileSync ( path . join ( OUTPUT_PATH_NOTE_MARKERS , fileName ) , buffer ) ;
182+ }
183+ }
184+ }
185+
186+ let buffer = await generateOther ( OTHER_COLORS . Active , null ) ;
187+ fs . writeFileSync ( path . join ( OUTPUT_PATH_MARKERS , `player.png` ) , buffer ) ;
188+
189+ buffer = await generateOther ( OTHER_COLORS . Active , path . join ( TEMPLATES_PATH , 'icon-store.png' ) ) ;
190+ fs . writeFileSync ( path . join ( OUTPUT_PATH_MARKERS , `vending_machine_active.png` ) , buffer ) ;
191+
192+ buffer = await generateOther ( OTHER_COLORS . Inactive , path . join ( TEMPLATES_PATH , 'icon-store.png' ) ) ;
193+ fs . writeFileSync ( path . join ( OUTPUT_PATH_MARKERS , `vending_machine_inactive.png` ) , buffer ) ;
194+ } ) ( ) ;
0 commit comments