Skip to content

Commit 01c0131

Browse files
Support color gradients
1 parent fc71413 commit 01c0131

6 files changed

Lines changed: 1007 additions & 0 deletions

File tree

public/app.mjs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ let liveApplyTimer = null;
7575
const featureModules = {
7676
genetic: null,
7777
perturb: null,
78+
gradient: null,
7879
};
7980

8081
async function ensureFeatureModule(name) {
@@ -103,6 +104,19 @@ async function ensureFeatureModule(name) {
103104
}
104105
return featureModules[name];
105106
}
107+
if (name === "gradient") {
108+
const { initializeGradientEditor } = await import("./features/gradient-editor.mjs");
109+
featureModules[name] = initializeGradientEditor({
110+
getConfig: () => state.config,
111+
setConfig: (config) => {
112+
state.config = config;
113+
render();
114+
},
115+
render,
116+
});
117+
featureModules[name].init();
118+
return featureModules[name];
119+
}
106120
return null;
107121
}
108122

@@ -121,6 +135,9 @@ function setActiveInspectorPanel(panelId) {
121135
if (panelId === "perturb-panel") {
122136
void ensureFeatureModule("perturb");
123137
}
138+
if (panelId === "gradients-panel") {
139+
void ensureFeatureModule("gradient");
140+
}
124141
}
125142

126143
function controlsToConfig() {
@@ -217,10 +234,131 @@ function colorFor(value, config, channels) {
217234
const alphabet = config.alphabet_sortie || config.alphabet_entree || [0, 1];
218235
let index = alphabet.findIndex((item) => String(item) === String(visualValue));
219236
if (index < 0) index = Math.abs(Number(visualValue) || Number(value) || 0) % alphabet.length;
237+
238+
// Check if gradient mode is enabled
239+
const gradient = config.rendu?.gradient;
240+
if (gradient && gradient.mode === "gradient") {
241+
return interpolateGradientColor(index, alphabet.length, gradient);
242+
}
243+
244+
// Fall back to discrete palette
220245
const palette = config.rendu.palette || getDefaultPalette(alphabet.length);
221246
return palette[index % palette.length] || palette[0];
222247
}
223248

249+
function interpolateGradientColor(stateIndex, stateCount, gradientConfig) {
250+
const t = stateCount > 1 ? stateIndex / (stateCount - 1) : 0;
251+
const anchors = gradientConfig.ancres || {};
252+
const method = gradientConfig.methode || "lineaire";
253+
254+
if (method === "lineaire" && stateCount >= 2) {
255+
const c0 = anchors["0"] || "#0066ff";
256+
const c1 = anchors["1"] || "#ff6b35";
257+
return lerpColor(t, c0, c1);
258+
}
259+
260+
if (method === "barycentric" && stateCount >= 3) {
261+
const c0 = anchors["0"] || "#0066ff";
262+
const c1 = anchors["1"] || "#00ff41";
263+
const c2 = anchors["2"] || "#ff6b35";
264+
const lambda0 = Math.max(0, 1 - t);
265+
const lambda1 = t < 0.5 ? t * 2 : (1 - t) * 2;
266+
const lambda2 = t;
267+
return barycentricColor(lambda0, lambda1, lambda2, c0, c1, c2);
268+
}
269+
270+
if (method === "bilinear" && stateCount >= 4) {
271+
const c0 = anchors["0"] || "#0066ff";
272+
const c1 = anchors["1"] || "#00aaff";
273+
const c2 = anchors["2"] || "#00ff41";
274+
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);
278+
}
279+
280+
// IDW fallback
281+
const colors = Object.values(anchors);
282+
return idwColor(t, stateCount, colors);
283+
}
284+
285+
function lerpColor(t, color1, color2) {
286+
const c1 = hexToRgb(color1);
287+
const c2 = hexToRgb(color2);
288+
const r = Math.round(c1.r + (c2.r - c1.r) * t);
289+
const g = Math.round(c1.g + (c2.g - c1.g) * t);
290+
const b = Math.round(c1.b + (c2.b - c1.b) * t);
291+
return `rgb(${r}, ${g}, ${b})`;
292+
}
293+
294+
function hexToRgb(hex) {
295+
const h = hex.replace("#", "");
296+
return {
297+
r: parseInt(h.substr(0, 2), 16),
298+
g: parseInt(h.substr(2, 2), 16),
299+
b: parseInt(h.substr(4, 2), 16),
300+
};
301+
}
302+
303+
function barycentricColor(l0, l1, l2, color1, color2, color3) {
304+
const c1 = hexToRgb(color1);
305+
const c2 = hexToRgb(color2);
306+
const c3 = hexToRgb(color3);
307+
const sum = l0 + l1 + l2 || 1;
308+
const n0 = l0 / sum;
309+
const n1 = l1 / sum;
310+
const n2 = l2 / sum;
311+
const r = Math.round(c1.r * n0 + c2.r * n1 + c3.r * n2);
312+
const g = Math.round(c1.g * n0 + c2.g * n1 + c3.g * n2);
313+
const b = Math.round(c1.b * n0 + c2.b * n1 + c3.b * n2);
314+
return `rgb(${r}, ${g}, ${b})`;
315+
}
316+
317+
function bilinearColor(u, v, color1, color2, color3, color4) {
318+
const uc = Math.max(0, Math.min(1, u));
319+
const vc = Math.max(0, Math.min(1, v));
320+
const c1 = hexToRgb(color1);
321+
const c2 = hexToRgb(color2);
322+
const c3 = hexToRgb(color3);
323+
const c4 = hexToRgb(color4);
324+
const bottom = {
325+
r: c1.r + (c2.r - c1.r) * uc,
326+
g: c1.g + (c2.g - c1.g) * uc,
327+
b: c1.b + (c2.b - c1.b) * uc,
328+
};
329+
const top = {
330+
r: c4.r + (c3.r - c4.r) * uc,
331+
g: c4.g + (c3.g - c4.g) * uc,
332+
b: c4.b + (c3.b - c4.b) * uc,
333+
};
334+
const r = Math.round(bottom.r + (top.r - bottom.r) * vc);
335+
const g = Math.round(bottom.g + (top.g - bottom.g) * vc);
336+
const b = Math.round(bottom.b + (top.b - bottom.b) * vc);
337+
return `rgb(${r}, ${g}, ${b})`;
338+
}
339+
340+
function idwColor(t, stateCount, colors) {
341+
if (colors.length === 0) return "#808080";
342+
if (colors.length === 1) return colors[0];
343+
344+
let weights = [];
345+
for (let i = 0; i < stateCount; i++) {
346+
const pointPos = stateCount > 1 ? i / (stateCount - 1) : 0.5;
347+
const diff = Math.abs(t - pointPos);
348+
if (diff < 0.001) {
349+
return colors[i] || "#808080";
350+
}
351+
weights.push(1 / (diff * diff));
352+
}
353+
354+
const sumWeights = weights.reduce((a, b) => a + b, 0);
355+
const rgbs = colors.map(hexToRgb);
356+
const r = Math.round(rgbs.reduce((sum, rgb, i) => sum + rgb.r * weights[i], 0) / sumWeights);
357+
const g = Math.round(rgbs.reduce((sum, rgb, i) => sum + rgb.g * weights[i], 0) / sumWeights);
358+
const b = Math.round(rgbs.reduce((sum, rgb, i) => sum + rgb.b * weights[i], 0) / sumWeights);
359+
return `rgb(${r}, ${g}, ${b})`;
360+
}
361+
224362
function renderTransitionSignals(config) {
225363
if (!transitionSignals) return;
226364
const entries = window.AutomaginariumCore.transitionSignalEntries(config, 6);
@@ -358,6 +496,9 @@ function applyConfig(config, { source = "Configuration" } = {}) {
358496
describe(normalized);
359497
updateRuleSpaceDisplay(normalized);
360498
updatePaletteEditor();
499+
if (featureModules.gradient && typeof featureModules.gradient.updateOnConfigChange === "function") {
500+
featureModules.gradient.updateOnConfigChange();
501+
}
361502
updateHudRule();
362503
render();
363504
setSyncState("ok", `${source} chargee`);

0 commit comments

Comments
 (0)