Files
mixbox_flutter/lib/mixbox.dart
Robert Felker d199344d5d init
2026-02-10 17:07:13 +01:00

423 lines
12 KiB
Dart

import 'dart:typed_data';
import 'dart:math' as math;
/// MIXBOX 2.0 - Mélange de couleurs physiquement réaliste
/// (c) 2022 Secret Weapons. Tous droits réservés.
/// License: Creative Commons Attribution-NonCommercial 4.0
/// Auteurs: Sarka Sochorova et Ondrej Jamriska
///
/// USAGE BASIQUE:
/// int colorMix = Mixbox.lerp(color1, color2, 0.5);
///
/// MÉLANGE MULTI-COULEURS:
/// var z1 = Mixbox.rgbToLatent(color1);
/// var z2 = Mixbox.rgbToLatent(color2);
/// var z3 = Mixbox.rgbToLatent(color3);
///
/// var zMix = List.generate(Mixbox.latentSize, (i) =>
/// 0.3 * z1[i] + 0.6 * z2[i] + 0.1 * z3[i]
/// );
///
/// int colorMix = Mixbox.latentToRgb(zMix);
///
/// COULEURS PIGMENTS:
/// Cadmium Yellow: (254, 236, 0)
/// Hansa Yellow: (252, 211, 0)
/// Cadmium Orange: (255, 105, 0)
/// Cadmium Red: (255, 39, 2)
/// Quinacridone Magenta: (128, 2, 46)
/// Cobalt Violet: (78, 0, 66)
/// Ultramarine Blue: (25, 0, 89)
/// Cobalt Blue: (0, 33, 133)
/// Phthalo Blue: (13, 27, 68)
/// Phthalo Green: (0, 60, 50)
/// Permanent Green: (7, 109, 22)
/// Sap Green: (107, 148, 4)
/// Burnt Sienna: (123, 72, 0)
class Mixbox {
static const int latentSize = 7;
static late Uint8List _lut;
static bool _initialized = false;
static bool get isInitialized => _initialized;
/// Initialise la LUT
/// Appelez cette méthode une fois au démarrage de l'app
static void initialize(Uint8List lutData) {
if (_initialized) return;
_lut = lutData;
_initialized = true;
}
/// Charge la LUT depuis un fichier asset
/// Exemple: await Mixbox.initializeFromAsset('assets/mixbox_lut.dat');
static Future<void> initializeFromAsset(String assetPath) async {
// À implémenter selon votre loader d'assets Flutter
// import 'package:flutter/services.dart';
// final data = await rootBundle.load(assetPath);
// initialize(data.buffer.asUint8List());
}
/// Mélange linéaire entre deux couleurs (format int ARGB 32-bit)
///
/// [color1] et [color2]: Couleurs au format 0xAARRGGBB
/// [t]: Facteur de mélange [0.0-1.0], 0.0 = color1, 1.0 = color2
///
/// Retourne: Couleur mélangée au format 0xAARRGGBB
static int lerp(int color1, int color2, double t) {
final latent1 = rgbToLatent(color1);
final latent2 = rgbToLatent(color2);
final latentMix = List<double>.generate(latentSize, (i) => (1.0 - t) * latent1[i] + t * latent2[i]);
final alpha1 = (color1 >> 24) & 0xFF;
final alpha2 = (color2 >> 24) & 0xFF;
final alphaMix = _clamp0255(((1.0 - t) * alpha1 + t * alpha2).round());
return (alphaMix << 24) | (latentToRgb(latentMix) & 0xFFFFFF);
}
/// Mélange entre deux couleurs au format liste [r, g, b] ou [r, g, b, a]
///
/// Valeurs: 0-255 pour chaque composante
static List<int> lerpList(List<int> color1, List<int> color2, double t) {
final latent1 = rgbToLatentFromList(color1);
final latent2 = rgbToLatentFromList(color2);
final latentMix = List<double>.generate(latentSize, (i) => (1.0 - t) * latent1[i] + t * latent2[i]);
final colorMix = latentToRgb(latentMix);
if (color1.length == 3 && color2.length == 3) {
return [(colorMix >> 16) & 0xFF, (colorMix >> 8) & 0xFF, colorMix & 0xFF];
}
final alpha1 = color1.length > 3 ? color1[3] : 255;
final alpha2 = color2.length > 3 ? color2[3] : 255;
final alphaMix = _clamp0255(((1.0 - t) * alpha1 + t * alpha2).round());
return [(colorMix >> 16) & 0xFF, (colorMix >> 8) & 0xFF, colorMix & 0xFF, alphaMix];
}
/// Mélange entre deux couleurs au format float [0.0-1.0]
///
/// Format: [r, g, b] ou [r, g, b, a] avec valeurs 0.0-1.0
static List<double> lerpFloat(List<double> color1, List<double> color2, double t) {
final latent1 = floatRgbToLatent(color1[0], color1[1], color1[2]);
final latent2 = floatRgbToLatent(color2[0], color2[1], color2[2]);
final latentMix = List<double>.generate(latentSize, (i) => (1.0 - t) * latent1[i] + t * latent2[i]);
final colorMix = latentToFloatRgb(latentMix);
if (color1.length == 3 && color2.length == 3) {
return colorMix;
}
final alpha1 = color1.length > 3 ? color1[3] : 1.0;
final alpha2 = color2.length > 3 ? color2[3] : 1.0;
final alphaMix = (1.0 - t) * alpha1 + t * alpha2;
return [colorMix[0], colorMix[1], colorMix[2], alphaMix];
}
/// Mélange entre deux couleurs RGB linéaires [0.0-1.0]
///
/// Pour couleurs en espace linéaire (non-sRGB)
static List<double> lerpLinearFloat(List<double> color1, List<double> color2, double t) {
final latent1 = linearFloatRgbToLatent(color1[0], color1[1], color1[2]);
final latent2 = linearFloatRgbToLatent(color2[0], color2[1], color2[2]);
final latentMix = List<double>.generate(latentSize, (i) => (1.0 - t) * latent1[i] + t * latent2[i]);
final colorMix = latentToLinearFloatRgb(latentMix);
if (color1.length == 3 && color2.length == 3) {
return colorMix;
}
final alpha1 = color1.length > 3 ? color1[3] : 1.0;
final alpha2 = color2.length > 3 ? color2[3] : 1.0;
final alphaMix = (1.0 - t) * alpha1 + t * alpha2;
return [colorMix[0], colorMix[1], colorMix[2], alphaMix];
}
/// Conversion RGB (0-255) → espace latent
static List<double> rgbToLatentFromInts(int r, int g, int b) {
return floatRgbToLatent(r / 255.0, g / 255.0, b / 255.0);
}
/// Conversion RGB liste → espace latent
static List<double> rgbToLatentFromList(List<int> rgb) {
return rgbToLatentFromInts(rgb[0], rgb[1], rgb[2]);
}
/// Conversion RGB int (0xAARRGGBB) → espace latent
static List<double> rgbToLatent(int color) {
return rgbToLatentFromInts((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF);
}
/// Conversion espace latent → RGB int (0xAARRGGBB)
static int latentToRgb(List<double> latent) {
final rgb = _evalPolynomial(latent[0], latent[1], latent[2], latent[3]);
return 0xFF000000 |
((_clamp01(rgb[0] + latent[4]) * 255.0).round() << 16) |
((_clamp01(rgb[1] + latent[5]) * 255.0).round() << 8) |
((_clamp01(rgb[2] + latent[6]) * 255.0).round());
}
/// Conversion RGB float (0.0-1.0) → espace latent
///
/// C'est le cœur de l'algorithme Mixbox
static List<double> floatRgbToLatent(double r, double g, double b) {
r = _clamp01(r);
g = _clamp01(g);
b = _clamp01(b);
// Interpolation trilinéaire dans la LUT 64x64x64
final x = r * 63.0;
final y = g * 63.0;
final z = b * 63.0;
final ix = x.floor();
final iy = y.floor();
final iz = z.floor();
final tx = x - ix;
final ty = y - iy;
final tz = z - iz;
final xyz = (ix + iy * 64 + iz * 64 * 64) & 0x3FFFF;
double c0 = 0.0;
double c1 = 0.0;
double c2 = 0.0;
// Interpolation aux 8 sommets du cube
double w;
w = (1.0 - tx) * (1.0 - ty) * (1.0 - tz);
c0 += w * (_lut[xyz + 192] & 0xFF);
c1 += w * (_lut[xyz + 262336] & 0xFF);
c2 += w * (_lut[xyz + 524480] & 0xFF);
w = tx * (1.0 - ty) * (1.0 - tz);
c0 += w * (_lut[xyz + 193] & 0xFF);
c1 += w * (_lut[xyz + 262337] & 0xFF);
c2 += w * (_lut[xyz + 524481] & 0xFF);
w = (1.0 - tx) * ty * (1.0 - tz);
c0 += w * (_lut[xyz + 256] & 0xFF);
c1 += w * (_lut[xyz + 262400] & 0xFF);
c2 += w * (_lut[xyz + 524544] & 0xFF);
w = tx * ty * (1.0 - tz);
c0 += w * (_lut[xyz + 257] & 0xFF);
c1 += w * (_lut[xyz + 262401] & 0xFF);
c2 += w * (_lut[xyz + 524545] & 0xFF);
w = (1.0 - tx) * (1.0 - ty) * tz;
c0 += w * (_lut[xyz + 4288] & 0xFF);
c1 += w * (_lut[xyz + 266432] & 0xFF);
c2 += w * (_lut[xyz + 528576] & 0xFF);
w = tx * (1.0 - ty) * tz;
c0 += w * (_lut[xyz + 4289] & 0xFF);
c1 += w * (_lut[xyz + 266433] & 0xFF);
c2 += w * (_lut[xyz + 528577] & 0xFF);
w = (1.0 - tx) * ty * tz;
c0 += w * (_lut[xyz + 4352] & 0xFF);
c1 += w * (_lut[xyz + 266496] & 0xFF);
c2 += w * (_lut[xyz + 528640] & 0xFF);
w = tx * ty * tz;
c0 += w * (_lut[xyz + 4353] & 0xFF);
c1 += w * (_lut[xyz + 266497] & 0xFF);
c2 += w * (_lut[xyz + 528641] & 0xFF);
c0 /= 255.0;
c1 /= 255.0;
c2 /= 255.0;
final c3 = 1.0 - (c0 + c1 + c2);
// Calcul du résidu via polynôme
final rgb = _evalPolynomial(c0, c1, c2, c3);
return [c0, c1, c2, c3, r - rgb[0], g - rgb[1], b - rgb[2]];
}
/// Conversion RGB float liste → espace latent
static List<double> floatRgbToLatentFromList(List<double> rgb) {
return floatRgbToLatent(rgb[0], rgb[1], rgb[2]);
}
/// Conversion espace latent → RGB float (0.0-1.0)
static List<double> latentToFloatRgb(List<double> latent) {
final rgb = _evalPolynomial(latent[0], latent[1], latent[2], latent[3]);
return [_clamp01(rgb[0] + latent[4]), _clamp01(rgb[1] + latent[5]), _clamp01(rgb[2] + latent[6])];
}
/// Conversion RGB linéaire → espace latent
static List<double> linearFloatRgbToLatent(double r, double g, double b) {
return floatRgbToLatent(_linearToSrgb(r), _linearToSrgb(g), _linearToSrgb(b));
}
/// Conversion RGB linéaire liste → espace latent
static List<double> linearFloatRgbToLatentFromList(List<double> rgb) {
return linearFloatRgbToLatent(rgb[0], rgb[1], rgb[2]);
}
/// Conversion espace latent → RGB linéaire
static List<double> latentToLinearFloatRgb(List<double> latent) {
final rgb = latentToFloatRgb(latent);
return [_srgbToLinear(rgb[0]), _srgbToLinear(rgb[1]), _srgbToLinear(rgb[2])];
}
/// Évaluation du polynôme avec coefficients optimisés
///
/// Représente le mélange physique des pigments
static List<double> _evalPolynomial(double c0, double c1, double c2, double c3) {
double r = 0.0;
double g = 0.0;
double b = 0.0;
// Pré-calcul des termes
final c00 = c0 * c0;
final c11 = c1 * c1;
final c22 = c2 * c2;
final c33 = c3 * c3;
final c01 = c0 * c1;
final c02 = c0 * c2;
final c12 = c1 * c2;
double w;
// Termes cubiques purs
w = c0 * c00;
r += 0.07717053 * w;
g += 0.02826978 * w;
b += 0.24832992 * w;
w = c1 * c11;
r += 0.95912302 * w;
g += 0.80256528 * w;
b += 0.03561839 * w;
w = c2 * c22;
r += 0.74683774 * w;
g += 0.04868586 * w;
b += 0.00000000 * w;
w = c3 * c33;
r += 0.99518138 * w;
g += 0.99978149 * w;
b += 0.99704802 * w;
// Termes quadratiques-linéaires (c0)
w = c00 * c1;
r += 0.04819146 * w;
g += 0.83363781 * w;
b += 0.32515377 * w;
w = c01 * c1;
r += -0.68146950 * w;
g += 1.46107803 * w;
b += 1.06980936 * w;
w = c00 * c2;
r += 0.27058419 * w;
g += -0.15324870 * w;
b += 1.98735057 * w;
w = c02 * c2;
r += 0.80478189 * w;
g += 0.67093710 * w;
b += 0.18424500 * w;
w = c00 * c3;
r += -0.35031003 * w;
g += 1.37855826 * w;
b += 3.68865000 * w;
w = c0 * c33;
r += 1.05128046 * w;
g += 1.97815239 * w;
b += 2.82989073 * w;
// Termes quadratiques-linéaires (c1)
w = c11 * c2;
r += 3.21607125 * w;
g += 0.81270228 * w;
b += 1.03384539 * w;
w = c1 * c22;
r += 2.78893374 * w;
g += 0.41565549 * w;
b += -0.04487295 * w;
w = c11 * c3;
r += 3.02162577 * w;
g += 2.55374103 * w;
b += 0.32766114 * w;
w = c1 * c33;
r += 2.95124691 * w;
g += 2.81201112 * w;
b += 1.17578442 * w;
// Termes quadratiques-linéaires (c2)
w = c22 * c3;
r += 2.82677043 * w;
g += 0.79933038 * w;
b += 1.81715262 * w;
w = c2 * c33;
r += 2.99691099 * w;
g += 1.22593053 * w;
b += 1.80653661 * w;
// Termes linéaires mixtes
w = c01 * c2;
r += 1.87394106 * w;
g += 2.05027182 * w;
b += -0.29835996 * w;
w = c01 * c3;
r += 2.56609566 * w;
g += 7.03428198 * w;
b += 0.62575374 * w;
w = c02 * c3;
r += 4.08329484 * w;
g += -1.40408358 * w;
b += 2.14995522 * w;
w = c12 * c3;
r += 6.00078678 * w;
g += 2.55552042 * w;
b += 1.90739502 * w;
return [r, g, b];
}
// Utilitaires
static double _clamp01(double x) {
return x < 0.0 ? 0.0 : (x > 1.0 ? 1.0 : x);
}
static int _clamp0255(int x) {
return x < 0 ? 0 : (x > 255 ? 255 : x);
}
static double _srgbToLinear(double x) {
return (x >= 0.04045) ? math.pow((x + 0.055) / 1.055, 2.4).toDouble() : x / 12.92;
}
static double _linearToSrgb(double x) {
return (x >= 0.0031308) ? 1.055 * math.pow(x, 1.0 / 2.4).toDouble() - 0.055 : 12.92 * x;
}
}