mirror of
https://github.com/Solido/mixbox.git
synced 2026-03-19 14:39:34 +01:00
423 lines
12 KiB
Dart
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;
|
|
}
|
|
}
|