Skip to content

Commit 01719cb

Browse files
committed
Add script to generate markers
1 parent 4132311 commit 01719cb

File tree

182 files changed

+195
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

182 files changed

+195
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"test": "tsc --noEmit -p .",
1212
"lint": "npx eslint . --ext .ts",
1313
"gen-env": "ts-node scripts/generate-env.ts",
14+
"gen-icons": "ts-node scripts/generate-icons.ts",
1415
"unusedPhrases": "npx ts-node scripts/findUnusedPhrases.ts",
1516
"find-eslint-comments": "grep -r --exclude-dir=node_modules 'eslint-disable' .",
1617
"force-update-master": "git fetch origin && git reset --hard origin/master"

scripts/generate-icons.ts

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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+
})();
2.51 KB
1.77 KB
831 Bytes
2.57 KB
2.58 KB
2.06 KB
1.77 KB
3.65 KB

0 commit comments

Comments
 (0)