Skip to content

Commit 2f47a44

Browse files
Support color gradients with interpolation
1 parent 01c0131 commit 2f47a44

4 files changed

Lines changed: 389 additions & 79 deletions

File tree

public/app.mjs

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ function getDefaultPalette(alphabetSize) {
229229
);
230230
}
231231

232-
function colorFor(value, config, channels) {
232+
function colorFor(value, config, channels, x = 0, y = 0, gridWidth = 1, gridHeight = 1) {
233233
const visualValue = channels && channels.length > 1 ? channels[1] : value;
234234
const alphabet = config.alphabet_sortie || config.alphabet_entree || [0, 1];
235235
let index = alphabet.findIndex((item) => String(item) === String(visualValue));
@@ -238,19 +238,52 @@ function colorFor(value, config, channels) {
238238
// Check if gradient mode is enabled
239239
const gradient = config.rendu?.gradient;
240240
if (gradient && gradient.mode === "gradient") {
241-
return interpolateGradientColor(index, alphabet.length, gradient);
241+
return interpolateGradientColor(index, alphabet.length, gradient, x, y, gridWidth, gridHeight);
242242
}
243243

244244
// Fall back to discrete palette
245245
const palette = config.rendu.palette || getDefaultPalette(alphabet.length);
246246
return palette[index % palette.length] || palette[0];
247247
}
248248

249-
function interpolateGradientColor(stateIndex, stateCount, gradientConfig) {
250-
const t = stateCount > 1 ? stateIndex / (stateCount - 1) : 0;
249+
function positionToParameter(x, y, gridWidth, gridHeight, direction = "haut-bas") {
250+
switch (direction) {
251+
case "gauche-droite":
252+
return gridWidth > 1 ? x / (gridWidth - 1) : 0;
253+
case "diagonal":
254+
return gridWidth > 1 || gridHeight > 1 ? (x / (gridWidth - 1 || 1) + y / (gridHeight - 1 || 1)) / 2 : 0;
255+
case "radial": {
256+
const cx = (gridWidth - 1) / 2;
257+
const cy = (gridHeight - 1) / 2;
258+
const maxDist = Math.sqrt(cx * cx + cy * cy) || 1;
259+
const dist = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2);
260+
return Math.min(1, dist / maxDist);
261+
}
262+
case "haut-bas":
263+
default:
264+
return gridHeight > 1 ? y / (gridHeight - 1) : 0;
265+
}
266+
}
267+
268+
function interpolateGradientColor(stateIndex, stateCount, gradientConfig, x = 0, y = 0, gridWidth = 1, gridHeight = 1) {
251269
const anchors = gradientConfig.ancres || {};
252270
const method = gradientConfig.methode || "lineaire";
271+
const appliquer = gradientConfig.appliquer || "etat";
272+
const direction = gradientConfig.direction || "haut-bas";
273+
const influenceEtat = (gradientConfig.influence_etat ?? 50) / 100;
274+
275+
// Calculate parameters based on mode
276+
let t_state = stateCount > 1 ? stateIndex / (stateCount - 1) : 0;
277+
let t_position = positionToParameter(x, y, gridWidth, gridHeight, direction);
278+
let t = t_state;
279+
280+
if (appliquer === "position") {
281+
t = t_position;
282+
} else if (appliquer === "combine") {
283+
t = t_state * influenceEtat + t_position * (1 - influenceEtat);
284+
}
253285

286+
// Apply interpolation based on method
254287
if (method === "lineaire" && stateCount >= 2) {
255288
const c0 = anchors["0"] || "#0066ff";
256289
const c1 = anchors["1"] || "#ff6b35";
@@ -268,17 +301,36 @@ function interpolateGradientColor(stateIndex, stateCount, gradientConfig) {
268301
}
269302

270303
if (method === "bilinear" && stateCount >= 4) {
304+
let u, v;
305+
if (appliquer === "position") {
306+
// For position-based, map position to 2D space
307+
u = gridWidth > 1 ? x / (gridWidth - 1) : 0;
308+
v = gridHeight > 1 ? y / (gridHeight - 1) : 0;
309+
} else if (appliquer === "combine") {
310+
// For combined, blend state and position for each axis
311+
const u_state = (stateIndex % 2);
312+
const v_state = Math.floor(stateIndex / 2) % 2;
313+
u = u_state * influenceEtat + (gridWidth > 1 ? x / (gridWidth - 1) : 0) * (1 - influenceEtat);
314+
v = v_state * influenceEtat + (gridHeight > 1 ? y / (gridHeight - 1) : 0) * (1 - influenceEtat);
315+
} else {
316+
// State-based: use state index to determine grid position
317+
u = (stateIndex % 2);
318+
v = Math.floor(stateIndex / 2) % 2;
319+
}
271320
const c0 = anchors["0"] || "#0066ff";
272321
const c1 = anchors["1"] || "#00aaff";
273322
const c2 = anchors["2"] || "#00ff41";
274323
const c3 = anchors["3"] || "#ff6b35";
275-
const u = t * 2;
276-
const v = t * 2;
277-
return bilinearColor(u < 1 ? u : 2 - u, v < 1 ? v : 2 - v, c0, c1, c2, c3);
324+
return bilinearColor(u, v, c0, c1, c2, c3);
278325
}
279326

280-
// IDW fallback
327+
// IDW fallback for 5+ states
281328
const colors = Object.values(anchors);
329+
if (appliquer === "position") {
330+
t = t_position;
331+
} else if (appliquer === "combine") {
332+
t = t_state * influenceEtat + t_position * (1 - influenceEtat);
333+
}
282334
return idwColor(t, stateCount, colors);
283335
}
284336

@@ -429,13 +481,15 @@ function render() {
429481
const cell = Number(configuration.rendu.taille_cellule || 5);
430482
ctx.fillStyle = configuration.rendu.fond || "#05070d";
431483
ctx.fillRect(0, 0, canvas.width, canvas.height);
484+
const gridHeight = lignes.length;
485+
const gridWidth = lignes.length > 0 ? lignes[0].length : 1;
432486
lignes.forEach((line, y) => {
433487
line.forEach((value, x) => {
434488
const channels = sorties?.[y]?.[x];
435489
const visualValue = channels && channels.length > 1 ? channels[1] : value;
436490
const bgValue = configuration.alphabet_sortie?.[0] ?? configuration.alphabet_entree[0];
437491
if (!configuration.rendu.afficher_zero && String(visualValue) === String(bgValue)) return;
438-
ctx.fillStyle = colorFor(value, configuration, channels);
492+
ctx.fillStyle = colorFor(value, configuration, channels, x, y, gridWidth, gridHeight);
439493
ctx.fillRect(x * cell, y * cell, cell, cell);
440494
});
441495
});
@@ -476,9 +530,42 @@ function activateInitialMode(mode) {
476530
controls.initialMode.value = mode;
477531
}
478532

533+
function ensureGradientConfig(config) {
534+
if (!config.rendu) {
535+
config.rendu = {};
536+
}
537+
if (!config.rendu.gradient) {
538+
const alphabet = config.alphabet_sortie || config.alphabet_entree || [0, 1];
539+
const stateCount = alphabet.length;
540+
const defaultPalette = getDefaultPalette(stateCount);
541+
const defaultMethod = stateCount === 2 ? "lineaire" : stateCount === 3 ? "barycentric" : stateCount >= 4 ? "bilinear" : "idw";
542+
543+
config.rendu.gradient = {
544+
mode: "discret",
545+
appliquer: "etat",
546+
direction: "haut-bas",
547+
influence_etat: 50,
548+
methode: defaultMethod,
549+
nombre_etats: stateCount,
550+
ancres: {},
551+
};
552+
553+
for (let i = 0; i < stateCount; i++) {
554+
config.rendu.gradient.ancres[String(i)] = defaultPalette[i] || "#808080";
555+
}
556+
} else {
557+
// Ensure all new fields exist in existing gradient configs
558+
if (!config.rendu.gradient.appliquer) config.rendu.gradient.appliquer = "etat";
559+
if (!config.rendu.gradient.direction) config.rendu.gradient.direction = "haut-bas";
560+
if (config.rendu.gradient.influence_etat === undefined) config.rendu.gradient.influence_etat = 50;
561+
}
562+
return config;
563+
}
564+
479565
function applyConfig(config, { source = "Configuration" } = {}) {
480566
try {
481567
const normalized = window.AutomaginariumCore.ensureRenderableConfiguration(config);
568+
ensureGradientConfig(normalized);
482569
const validation = validateConfig(normalized);
483570
showStatus(validation);
484571
if (!validation.valid) {

0 commit comments

Comments
 (0)