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 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.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 lerpList(List color1, List color2, double t) { final latent1 = rgbToLatentFromList(color1); final latent2 = rgbToLatentFromList(color2); final latentMix = List.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 lerpFloat(List color1, List color2, double t) { final latent1 = floatRgbToLatent(color1[0], color1[1], color1[2]); final latent2 = floatRgbToLatent(color2[0], color2[1], color2[2]); final latentMix = List.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 lerpLinearFloat(List color1, List color2, double t) { final latent1 = linearFloatRgbToLatent(color1[0], color1[1], color1[2]); final latent2 = linearFloatRgbToLatent(color2[0], color2[1], color2[2]); final latentMix = List.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 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 rgbToLatentFromList(List rgb) { return rgbToLatentFromInts(rgb[0], rgb[1], rgb[2]); } /// Conversion RGB int (0xAARRGGBB) → espace latent static List rgbToLatent(int color) { return rgbToLatentFromInts((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF); } /// Conversion espace latent → RGB int (0xAARRGGBB) static int latentToRgb(List 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 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 floatRgbToLatentFromList(List rgb) { return floatRgbToLatent(rgb[0], rgb[1], rgb[2]); } /// Conversion espace latent → RGB float (0.0-1.0) static List latentToFloatRgb(List 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 linearFloatRgbToLatent(double r, double g, double b) { return floatRgbToLatent(_linearToSrgb(r), _linearToSrgb(g), _linearToSrgb(b)); } /// Conversion RGB linéaire liste → espace latent static List linearFloatRgbToLatentFromList(List rgb) { return linearFloatRgbToLatent(rgb[0], rgb[1], rgb[2]); } /// Conversion espace latent → RGB linéaire static List latentToLinearFloatRgb(List 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 _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; } }