-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.js
221 lines (190 loc) · 8.14 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// Reload the skin in the CSS variable
function reloadSkin() {
document.documentElement.style.setProperty('--skin', 'url(' + document.getElementById('texture').src + ')');
}
function applyURLValuesToInputs() {
const urlParams = new URLSearchParams(window.location.search);
// Attempt to apply URL parameter values to inputs
try {
// For each URL parameter, set the matching input value if it exists
urlParams.forEach((value, key) => {
const input = document.getElementById(key); // Find input by ID
if (input) {
input.value = value; // Set the input's value to the parameter value
} else {
console.warn(`Input with id "${key}" not found on the page.`);
}
});
} catch (error) {
console.error("An error occurred while applying URL values to inputs:", error);
}
}
function syncColorsToURL() {
const urlParams = new URLSearchParams(window.location.search);
// For each color input, set the URL parameter value
document.querySelectorAll('.color').forEach(input => {
urlParams.set(input.id, input.value);
});
clearTimeout(this.timeout);
// wait a little bit before updating the URL to prevent lag
this.timeout = setTimeout(() => {
// Update the URL with the new parameters
window.history.replaceState({}, '', `${location.pathname}?${urlParams}`);
}, 1000);
}
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
applyURLValuesToInputs();
reloadSkin();
generateOutfit();
document.getElementById('download').addEventListener('click', function(event) {
event.preventDefault();
var texture = document.getElementById('texture');
var link = document.createElement('a');
link.download = 'pietje.png';
link.href = texture.src;
link.click();
});
// Listen for changes in the color inputs
document.querySelectorAll('.color').forEach(function(input) {
input.addEventListener('input', function() {
// wait a little bit before generating the outfit to prevent lag, and a oneshot to prevent multiple calls
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
generateOutfit();
syncColorsToURL();
}
, 10);
});
// Place after the color input a button to reset the color back to the default value
var resetButton = document.createElement('button');
resetButton.textContent = 'Reset';
resetButton.classList.add('reset');
input.parentNode.insertBefore(resetButton, input.nextSibling);
resetButton.addEventListener('click', function(event) {
event.preventDefault();
input.value = input.defaultValue;
generateOutfit();
syncColorsToURL();
});
});
});
// Generate a image by layering the skin parts from the folder layers and save it to the texture element
function generateOutfit() {
// Define the parts and other variables
var skinParts = ['color_maillot', 'color_skin', 'color_hair', 'color_eyes', 'color_primary', 'color_secondary', 'color_shoes', 'top'];
var texture = document.getElementById('texture');
// Main canvas setup
var mainCanvas = document.createElement('canvas');
var ctx = mainCanvas.getContext('2d');
// Set canvas size
mainCanvas.width = 64;
mainCanvas.height = 64;
// Off-screen canvas for layering
var offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = mainCanvas.width;
offscreenCanvas.height = mainCanvas.height;
var offscreenCtx = offscreenCanvas.getContext('2d');
function drawLayeredImage(img, color, part) {
// Step 1: Clear the off-screen canvas
offscreenCtx.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
// Step 2: Draw the grayscale image on the off-screen canvas
offscreenCtx.globalCompositeOperation = 'source-over';
offscreenCtx.drawImage(img, 0, 0);
// Step 3: Apply color with multiply mode on the off-screen canvas
offscreenCtx.globalCompositeOperation = 'multiply';
// when part is color_maillot, or color_skin, clamp the color to a minimum value for better visibility
if (['color_maillot', 'color_skin'].includes(part)) {
color = clampColor(color);
} else if (part === 'color_hair') {
color = clampColor(color, 12);
}
offscreenCtx.fillStyle = color;
offscreenCtx.fillRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
// Step 4: Retain only the opaque parts of the original grayscale image
offscreenCtx.globalCompositeOperation = 'destination-in';
offscreenCtx.drawImage(img, 0, 0);
// Step 5: Draw the result from the off-screen canvas onto the main canvas
ctx.globalCompositeOperation = 'source-over';
ctx.drawImage(offscreenCanvas, 0, 0);
}
function loadImage(part) {
return new Promise((resolve, reject) => {
// Check if the image is already in localStorage
const cachedImage = localStorage.getItem(`imageCache_${part}`);
if (cachedImage) {
const img = new Image();
img.src = cachedImage; // Load image from Base64 string
resolve({ img, part });
} else {
// Load image from server and store in localStorage
const img = new Image();
img.src = 'layers/' + part + '.png';
img.onload = function () {
// Convert image to Base64 string and store in localStorage
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
try {
const dataURL = canvas.toDataURL(); // Convert to Base64
localStorage.setItem(`imageCache_${part}`, dataURL);
} catch (e) {
console.warn('LocalStorage quota exceeded, cannot cache image.', e);
}
resolve({ img, part });
};
img.onerror = function () {
console.error("Failed to load image:", img.src);
reject(img.src);
};
}
});
}
// Load all images, then draw them in sequence
Promise.all(skinParts.map(part => loadImage(part).then(({ img, part }) => {
// Get the color value
let color;
try {
color = document.getElementById(part).value;
} catch (error) {
color = '#ffffff'; // Default color if element not found
}
return { img, color, part };
})))
.then(images => {
// All images are loaded, now draw them in the specified order
images.forEach(({ img, color, part }) => {
drawLayeredImage(img, color, part);
});
// Update the texture after all layers are drawn
texture.src = mainCanvas.toDataURL();
reloadSkin();
})
.catch(error => {
console.error("An error occurred while loading images:", error);
});
}
// Clamp the color to a minimum value for better visibility
function clampColor(color, min = 7) {
// Convert hex color to RGB components
let r = parseInt(color.slice(1, 3), 16);
let g = parseInt(color.slice(3, 5), 16);
let b = parseInt(color.slice(5, 7), 16);
// limit the colors above 230 for better results
if (r > 230) {
r = 230;
}
if (g > 230) {
g = 230;
}
if (b > 230) {
b = 230;
}
if (r < min && g < min && b < min) {
return `#${min.toString(16).padStart(2, '0')}${min.toString(16).padStart(2, '0')}${min.toString(16).padStart(2, '0')}`;
}
// Convert back to hex format
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
}