File indexing completed on 2024-06-16 04:38:29

0001 #!/usr/bin/node --trace-uncaught
0002 
0003 const util = require('util');
0004 
0005 Object.prototype.lp = function(name, value) {
0006         Object.defineProperty(this, name, { value: value });
0007         return value;
0008 }
0009 
0010 Number.prototype.toStdFloat = function(precision=7){
0011         return this.toPrecision(precision).replace(/([^\.])0+$/, '$1');
0012 }
0013 
0014 Number.prototype.toCFloat = function(precision=7){
0015         return this.toStdFloat(precision) + (precision <= 7 ? 'f' : '');
0016 }
0017 
0018 class vec {
0019         constructor(x, y, z, w) {
0020                 let a = [];
0021                 for(const i in arguments) {
0022                         const v = arguments[i];
0023                         if(v instanceof Array)
0024                                 a = a.concat(v);
0025                         else if(v instanceof vec)
0026                                 a = a.concat(v.elem);
0027                         else if(typeof(v) == 'number')
0028                                 a.push(v);
0029                 }
0030                 this.elem = a.slice(0, 4);
0031                 this.ch = ['x', 'y', 'z', 'w'].slice(0, this.elem.length);
0032                 // dynamic property array getter - inst.xyz, inst.xyzz, ... will get
0033                 const rgb = { x:'r', y:'g', z:'b', w:'a'};
0034                 for(let i = 0, pr = ['']; i < this.elem.length; i++) {
0035                         for(let j = 0, n = pr.length; j < n; j++) {
0036                                 const pre = pr.shift();
0037                                 for(let k = 0; k < this.elem.length; k++) {
0038                                         const key = pre + this.ch[k];
0039                                         pr.push(key);
0040                                         if(pre)
0041                                                 Object.defineProperty(this, key, { get: () => this.val(key) });
0042                                         else
0043                                                 this[key] = this.elem[k];
0044                                         Object.defineProperty(this, key.replace(/./g, (m) => rgb[m]), { get: () => this.val(key) });
0045                                 }
0046                         }
0047                 }
0048         }
0049 
0050         val(name) { return name.length > 1 ? name.split('').map(v => this[v]) : this[name]; }
0051 
0052         get size() { return this.elem.length; }
0053 
0054         get line() { return { a:-this.y, b:this.x, c:0 }; }
0055 
0056         get mag() { return this.lp('mag', Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2))); }
0057 
0058         debugDump(name, comment, precision=7) {
0059                 if(comment)
0060                         console.log(`// ${comment}`);
0061                 const p = precision ? (val) => val.toFixed(precision).replace(/^-(0\.0+)$/, '$1') : (val) => val;
0062                 console.log(`${name}\t${this.elem.map(v => p(v)).join('\t')}`);
0063         }
0064 
0065         angle(other) {
0066                 const l1 = this.line;
0067                 const l2 = other.line;
0068                 const m1 = -l1.a / l1.b;
0069                 const m2 = -l2.a / l2.b;
0070                 return Math.atan((m1 - m2) / (1. + m1 * m2));
0071         }
0072 
0073         dot(other) {
0074                 let res = 0;
0075                 for(let i = 0; i < this.size; i++) {
0076                         const ch = String.fromCharCode(120 + i);
0077                         res += this[ch] * other[ch];
0078                 }
0079                 return res;
0080                 // return this.mag * other.mag * Math.cos(this.angle(other));
0081                 // return this.x * other.x + this.y * other.y;
0082         }
0083 
0084         mul(other) { return new vec(this.elem.map(v => v * other)); }
0085         div(other) { return new vec(this.elem.map(v => v / other)); }
0086 
0087         /**
0088          * intersection point of line p1-p2 and line p3-p4
0089          */
0090         static intersection(p1, p2, p3, p4) {
0091                 const x12 = p1.x - p2.x;
0092                 const x34 = p3.x - p4.x;
0093                 const y12 = p1.y - p2.y;
0094                 const y34 = p3.y - p4.y;
0095                 const d = x12 * y34 - y12 * x34;
0096                 const xy12 = p1.x * p2.y - p1.y * p2.x;
0097                 const xy34 = p3.x * p4.y - p3.y * p4.x;
0098                 const ix = (xy12 * x34 - x12 * xy34) / d;
0099                 const iy = (xy12 * y34 - y12 * xy34) / d;
0100                 return new vec(ix, iy);
0101         }
0102 
0103         /**
0104          * line through two points [ax + by + c = 0]
0105          */
0106         static line(x1, y1, x2, y2) {
0107                 return {
0108                         a: y1 - y2,
0109                         b: x2 - x1,
0110                         c: x1 * y2 - x2 * y1,
0111                 };
0112         }
0113 }
0114 
0115 class mat {
0116         constructor(n_or_array) {
0117                 if(arguments.length > 1) {
0118                         this.elem = Array.from(arguments);
0119                 } else if(n_or_array instanceof Array) {
0120                         this.elem = n_or_array;
0121                 } else if(typeof(n_or_array) == 'number'){
0122                         this.elem = Array.from(new Array(n_or_array * n_or_array))
0123                                 .map((v, i) => i % n_or_array == parseInt(i / n_or_array) ? 1. : 0.);
0124                 } else {
0125                         throw `mat() should be constructed by numbers list, numbers array or mat size`;
0126                 }
0127         }
0128 
0129         get length() { return this.elem.length; }
0130         get size() { return this.lp('size', Math.sqrt(this.length)); }
0131 
0132         array2D(precision) {
0133                 let s = [];
0134                 const p = precision ? (val) => parseFloat(val.toPrecision(precision)) : (val) => val;
0135                 for(let y = 0; y < this.size; y++) {
0136                         s[y] = [];
0137                         for(let x = 0; x < this.size; x++)
0138                                 s[y].push(p(this.elem[y * this.size + x]));
0139                 }
0140                 return s;
0141         }
0142         debugDump(name, comment, precision=7) {
0143                 let s = [];
0144                 const p = precision ? (val) => val.toFixed(precision).replace(/^-(0\.0+)$/, '$1') : (val) => val;
0145                 for(let y = 0; y < this.size; y++) {
0146                         s[y] = [];
0147                         for(let x = 0; x < this.size; x++)
0148                                 s[y].push(p(this.elem[y * this.size + x]));
0149                         s[y] = s[y].join('\t');
0150                 }
0151                 if(comment)
0152                         console.log(`// ${comment}`);
0153                 console.log(`${name}\t${s.join('\n\t')}`);
0154         }
0155         glDump(name, comment, precision=7) {
0156                 let s = [];
0157                 for(let y = 0; y < this.size; y++)
0158                         for(let x = 0; x < this.size; x++)
0159                                 s.push((!x ? '\n\t' : '') + this.elem[y * this.size + x].toStdFloat(precision));
0160                 console.log(`mat${this.size} ${name} = mat${this.size}(${s.join(', ')});`);
0161         }
0162         cFloat(name, comment, precision=7) {
0163                 let s = [];
0164                 for(let y = 0; y < this.size; y++)
0165                         for(let x = 0; x < this.size; x++)
0166                                 s.push(this.elem[y + x * this.size].toCFloat(precision));
0167                 return { name:name, comment:comment, mat:s };
0168         }
0169 
0170         mul(other) {
0171                 return typeof(other) == 'number' ? this.mulScalar(other) :
0172                           (other instanceof vec ? this.mulVec(other) : this.mulMat(other));
0173         }
0174         mulVec(other) {
0175                 if(other.size != this.size)
0176                         throw `mat${this.size} and vec${other.size} must have same size`;
0177                 let res = other.elem.map(() => 0.);
0178                 for(let r = 0; r < this.size; r++) {
0179                         const ro = r * this.size;
0180                         for(let c = 0; c < other.size; c++)
0181                                 res[r] += other.elem[c] * this.elem[ro + c];
0182                 }
0183                 return new vec(res);
0184         }
0185         mulMat(other) {
0186                 if(other.size != this.size)
0187                         throw `mat${this.size} and mat${other.size} must have same size`;
0188                 let res = [];
0189                 for(let r = 0; r < this.size; r++) {
0190                         for(let c = 0; c < this.size; c++) {
0191                                 let v = 0.;
0192                                 for(let k = 0; k < this.size; k++)
0193                                         v += this.elem[r * this.size + k] * other.elem[k * this.size + c];
0194                                 res.push(v);
0195                         }
0196                 }
0197                 return new mat(res);
0198         }
0199         mulScalar(other) {
0200                 return new mat(this.elem.map((v, i) => v * other));
0201         }
0202 
0203         trans() {
0204                 return new mat(this.elem.map((v, i) => this.elem[parseInt(i / this.size) + (i % this.size) * this.size]));
0205         }
0206 
0207         sum(other) {
0208                 if(other.size != this.size)
0209                         throw `mat${this.size} and mat${other.size} must have same size`;
0210                 return new mat(this.elem.map((v, i) => v + other.elem[i]));
0211         }
0212 
0213         det() {
0214                 let el = this.elem.map(v => v);
0215                 for(let i = 0; i < this.size - 1; i++) {
0216                         for(let j = i + 1; j < this.size; j++) {
0217                                 const mul = el[j * this.size + i] / el[i * this.size + i];
0218                                 if(!mul || isNaN(mul)) continue;
0219                                 for(let k = i; k < this.size; k++)
0220                                         el[j * this.size + k] -= mul * el[i * this.size + k];
0221                         }
0222                 }
0223                 let d = 1.;
0224                 for(let i = 0; i < this.size; i++)
0225                         d *= el[this.size * i + i];
0226                 return d;
0227         }
0228 
0229         inv() {
0230                 let m1 = [];
0231                 for(let i = 0; i < this.size; i++) {
0232                         let s = this.elem.slice(i * this.size, (i + 1) * this.size);
0233                         for(let k = 0; k < this.size; k++)
0234                                 s.push(i == k ? 1. : 0.);
0235                         m1.push(s);
0236                 }
0237                 mat.solveLinearEquation(m1);
0238                 for(let i = 0; i < this.size; i++)
0239                         m1[i] = m1[i].slice(this.size);
0240                 return new mat(m1.flat());
0241         }
0242 
0243         static solveLinearEquation(sys) {
0244                 const len = Math.min(sys.length, sys[0].length - 1);
0245                 for(let j = 0; j < len; j++) {
0246                         // normalize diagonal
0247                         for(let i = 0, n = sys[j][j]; i < sys[j].length; i++)
0248                                 sys[j][i] /= n;
0249                         // zero above
0250                         for(let jj = j - 1; jj >= 0; jj--) {
0251                                 for(let i = j, n = sys[jj][j]; i < sys[j].length; i++)
0252                                         sys[jj][i] -= n * sys[j][i];
0253                         }
0254                         // zero under
0255                         for(let jj = j + 1; jj < sys.length; jj++) {
0256                                 for(let i = j, n = sys[jj][j]; i < sys[j].length; i++)
0257                                         sys[jj][i] -= n * sys[j][i];
0258                         }
0259                 }
0260                 return sys.map(el => el[el.length - 1]);
0261         }
0262 }
0263 
0264 class ColorCoefficients {
0265         // NOTE: fullRange is ignored... matrix is always full range
0266         constructor(Kr, Kg, Kb, fullRange) {
0267                 this.Kr = Kr;
0268                 this.Kg = Kg;
0269                 this.Kb = Kb;
0270                 this.fullRange = fullRange;
0271         }
0272 
0273         mat3_RGB_RGB() {
0274                 return new mat(
0275                         1., 0., 0.,
0276                         0., 1., 0.,
0277                         0., 0., 1.
0278                 );
0279         }
0280 
0281         // Y'CbCr (full range) and Y'PbPr
0282         mat3_YUV_RGB() {
0283                 /*
0284                         Rec. 709 calls for luma range 0...219, offset +16 at the interface. Cb and Cr are
0285                         scaled individually to range ±112, an excursion 224/219 of luma, offset +128 at
0286                         the interface. Codes 0 and 255 are prohibited.
0287                 */
0288 
0289                 return new mat(
0290                         1.0,    0.0,                                                                                    2.0 * (1.0 - this.Kr),
0291                         1.0,    -2.0 * (1.0 - this.Kb) * (this.Kb / this.Kg),   -2.0 * (1.0 - this.Kr) * (this.Kr / this.Kg),
0292                         1.0,    2.0 * (1.0 - this.Kb),                                                  0.0
0293                 );
0294 
0295                 /*
0296                 const Ymax = this.fullRange ? 255.0 / 256.0 : (16.0 + 219.0) / 254.0;
0297                 const Cmax = this.fullRange ? 255.0 / 256.0 : (16.0 + 224.0) / 254.0;
0298                 const Umax = (Cmax - this.Kb) / 2.0; // (B'max - Y'(B'max)) / 2.0
0299                 const Vmax = (Cmax - this.Kr) / 2.0; // (R'max - Y'(R'max)) / 2.0
0300 
0301 //              const R = (255 / 219) * (Y - 16)                                                                                                                                                        + (255 / 224) * 2 * (1. - this.Kr)                       * (Cr - 128);
0302 //              const G = (255 / 219) * (Y - 16)        - (255 / 224) * 2 * (1. - this.Kb) * (this.Kb / this.Kg) * (Cb - 128)   - (255 / 224) * 2 * (1. - this.Kr) * (this.Kr / this.Kg) * (Cr - 128);
0303 //              const B = (255 / 219) * (Y - 16)        + (255 / 224) * 2 * (1. - this.Kb)                       * (Cb - 128);
0304 
0305                 return new mat(
0306                         1., 0., (1. - this.Kr) / Vmax,
0307                         1., -this.Kb * (1. - this.Kb) / (Umax * this.Kg), -this.Kr * (1. - this.Kr) / (Vmax * this.Kg),
0308                         1., (1. - this.Kb) / Umax, 0.
0309                 );
0310                 */
0311         }
0312 }
0313 
0314 class ColorPrimaries {
0315         /**
0316          * Params are CIE xy (not normalized) chromacity diagram coordinates - ISO 23001-8-18 (7.1) Color primaries
0317          */
0318         constructor(Rx, Ry, Gx, Gy, Bx, By, Wx, Wy) {
0319                 // ColourPrimaries indicates the chromaticity coordinates of the source colour primaries
0320                 // in terms of the CIE 1931 definition of x and y as specified by ISO 11664-1.
0321                 this.Rx = Rx;
0322                 this.Ry = Ry;
0323                 this.Rz = 1 - Rx - Ry;
0324                 this.Gx = Gx;
0325                 this.Gy = Gy;
0326                 this.Gz = 1 - Gx - Gy;
0327                 this.Bx = Bx;
0328                 this.By = By;
0329                 this.Bz = 1 - Bx - By;
0330 
0331                 // CIE chromaticity coordinates (Wx, Wy) represent white point of standardized illuminant of a perfectly
0332                 // reflecting (or transmitting) diffuser. The CIE chromaticity coordinates are given for the 2 degree field
0333                 // of view (1931). The color swatches represent the hue and RGB of white point, calculated with luminance
0334                 // Y=0.54 and the standard observer, assuming correct sRGB display calibration.
0335                 // We can use Bradford Matrix to convert XYZ to different (Wx, Wy)
0336                 this.Wx = Wx;
0337                 this.Wy = Wy;
0338                 this.Wz = 1 - Wx - Wy;
0339         }
0340 
0341         /**
0342          * Calculate and return RGB Luminance coefficients
0343          */
0344         get rgbYratios() {
0345                 // https://stackoverflow.com/questions/53952959/why-were-the-constants-in-yuv-rgb-chosen-to-be-the-values-they-are/64392724#64392724
0346                 // Digital Video and HDTV - Algorithms and Interfaces - C. Poynton (2003) WW - Luminance coefficients - pg.256
0347                 return this.lp('rgbYratios', new vec(mat.solveLinearEquation([
0348                         // Ra, Ga, Ba, W
0349                         [ this.Rx, this.Gx, this.Bx, this.Wx / this.Wy ],
0350                         [ this.Ry, this.Gy, this.By, this.Wy / this.Wy ],
0351                         [ this.Rz, this.Gz, this.Bz, this.Wz / this.Wy ],
0352                 ])));
0353         }
0354 
0355         /**
0356          * mix of multiple CIE (x, y) chromaticities with Y luminance
0357          */
0358         mix(x1, y1, Y1, ...args) {
0359                 if(arguments.length % 3)
0360                         throw 'Wrong number of components... need multiple CIE xyY coordinates';
0361 
0362                 /*
0363                         https://en.wikipedia.org/wiki/CIE_1931_color_space
0364 
0365                         The CIE XYZ color space was deliberately designed so that the Y parameter is a measure
0366                         of the luminance of a color. The chromaticity is then specified by the two derived parameters x and y,
0367                         two of the three normalized values being functions of all three tristimulus values X, Y, and Z:
0368 
0369                         x = X / (X + Y + Z)
0370                         y = Y / (X + Y + Z)
0371                         z = Z / (X + Y + Z) = 1 - x - y
0372 
0373                         The X and Z tristimulus values can be calculated back from the chromaticity values x and y and the Y tristimulus value:
0374 
0375                         X = Y * x / y
0376                         Z = Y * z / y = Y * (1 - x - y) / y
0377 
0378                         When two or more colors are additively mixed, the x and y chromaticity coordinates of the resulting color (xMix, yMix)
0379                         may be calculated from the chromaticities of the mixture components (x1,y1; x2,y2; ...; xn,yn) and their corresponding
0380                         luminances (L1, L2, ..., Ln) with the following formulas:
0381 
0382                         xMix = (x1/y1*L1 + x2/y2*L2 + x3/y3*L3 + ...) / (L1/y1 + L2/y2 + L3/y3 + ...)
0383                         yMix = (L1 + L2 + L3 + ...) / (L1/y1 + L2/y2 + L3/y3 + ...)
0384                         zMix = 1 - xMix - yMix; // derived from below
0385                         ... Ln == Yn
0386 
0387                         These formulas can be derived from the previously presented definitions of x and y chromaticity coordinates by taking
0388                         advantage of the fact that the tristimulus values X, Y, and Z of the individual mixture components are directly additive.
0389 
0390                         In place of the luminance values (L1, L2, etc.) one can alternatively use any other photometric quantity that is directly
0391                         proportional to the tristimulus value Y (naturally meaning that Y itself can also be used as well).
0392 
0393                         As already mentioned, when two colors are mixed, the resulting color (xMix, yMix) will lie on the straight line segment
0394                         that connects these colors on the CIE xy chromaticity diagram. To calculate the mixing ratio of the component colors (x1, y1)
0395                         and (x2, y2) that results in a certain (xMix, yMix) on this line segment, one can use the formula:
0396 
0397                         L1 / L2 = (y1 * (x2 - xMix)) / (y2 * (xMix - x1)) = (y1 * (y2 - yMix)) / (y2 * (yMix - y1))
0398 
0399                         Note that, in accordance with the remarks concerning the formulas for xMix and yMix, the mixing ratio L1/L2 may well be
0400                         expressed in terms of photometric quantities other than luminance.
0401                 */
0402 
0403                 let div = 0, xMix = 0, yMix = 0;
0404                 for(let i = 0; i < arguments.length; i += 3) {
0405                         const x = arguments[i + 0];
0406                         const y = arguments[i + 1];
0407                         const Y = arguments[i + 2];
0408                         div += Y / y;
0409                         xMix += Y * x / y;
0410                         yMix += Y;
0411                 }
0412                 xMix /= div;
0413                 yMix /= div;
0414 
0415                 return new vec(xMix, yMix, 1. - xMix - yMix);
0416         }
0417 
0418         mat3_YUV_RGB() {
0419                 // YUV -> RGB coefficients
0420                 const rat = this.rgbYratios;
0421                 const Kr = rat.r * this.Ry;
0422                 const Kg = rat.g * this.Gy;
0423                 const Kb = rat.b * this.By;
0424 
0425                 const sum = parseFloat((Kr + Kg + Kb).toFixed(6));
0426                 if(sum !== 1.0)
0427                         console.warn(`${Kr} + ${Kg} + ${Kb} != 1.0 (${sum})`);
0428 
0429                 return new ColorCoefficients(Kr, Kg, Kb, false).mat3_YUV_RGB();
0430         }
0431 
0432         /**
0433          * Get luminance Y at CIE (x, y) chromaticity
0434          */
0435         get_xy_Y(pt) {
0436                 const rat = this.rgbYratios;
0437                 const Rv = new vec(this.Rx, this.Ry, rat.r * this.Ry);
0438                 const Gv = new vec(this.Gx, this.Gy, rat.g * this.Gy);
0439                 const Bv = new vec(this.Bx, this.By, rat.b * this.By);
0440 
0441                 // luminance between p1 and p2 at position x - x must be at line p1-p2
0442                 const lumaAtCIExy = (p1, p2, xMix) => {
0443                         const a = p1.y * (p2.x - xMix);
0444                         const b = p2.y * (xMix - p1.x);
0445 
0446                         return p1.z * a / (a + b) + p2.z * b / (a + b);
0447 
0448                         if(Math.abs(a) <= 1e-5) // xMix is at p2
0449                                 return p2.z;
0450                         if(Math.abs(b) <= 1e-5) // xMix is at p1
0451                                 return p1.z;
0452                         return p2.z * a / b + p1.z * b / a;
0453                 };
0454 
0455                 // p - intersection point between R-G and B-(x, y)
0456                 let ip = vec.intersection(Rv, Gv, Bv, pt);
0457                 if(Number.isNaN(ip.x)) // can't make line between Bv and (x, y) cause they're same point
0458                         return Bv.z;
0459                 // luminance at R-G intersection point
0460                 ip = new vec(ip.xy, lumaAtCIExy(Rv, Gv, ip.x));
0461                 // luminance at (x, y)
0462                 return lumaAtCIExy(ip, Bv, pt.x);
0463         }
0464 
0465         // approximation at best - they are not linear transforms
0466         mat4_xyY_RGB() {
0467                 return this.mat4_RGB_xyY().inv();
0468         }
0469         mat4_RGB_xyY() {
0470                 const rat = this.rgbYratios;
0471                 const cx = new vec(mat.solveLinearEquation([
0472                         [ 1, 1, 1, 1, this.Wx ],
0473                         [ 1, 0, 0, 1, this.Rx ],
0474                         [ 0, 1, 0, 1, this.Gx ],
0475                         [ 0, 0, 1, 1, this.Bx ],
0476                 ]));
0477                 const cy = new vec(mat.solveLinearEquation([
0478                         [ 1, 1, 1, 1, this.Wy ],
0479                         [ 1, 0, 0, 1, this.Ry ],
0480                         [ 0, 1, 0, 1, this.Gy ],
0481                         [ 0, 0, 1, 1, this.By ],
0482                 ]));
0483                 return new mat(
0484                         cx.r, cx.g, cx.b, cx.a,
0485                         cy.r, cy.g, cy.b, cy.a,
0486                         rat.r * this.Ry, rat.g * this.Gy, rat.b * this.By, 0.,
0487                         0., 0., 0., 1.
0488                 );
0489         }
0490 
0491         mat3_XYZ_RGB() { return this.mat3_RGB_XYZ().inv(); }
0492         mat3_RGB_XYZ() {
0493                 const rat = this.rgbYratios;
0494                 return new mat(
0495                         rat.r * this.Rx, rat.g * this.Gx, rat.b * this.Bx,
0496                         rat.r * this.Ry, rat.g * this.Gy, rat.b * this.By,
0497                         rat.r * this.Rz, rat.g * this.Gz, rat.b * this.Bz
0498                 );
0499         }
0500 }
0501 
0502 class ColorTransfer {
0503         constructor(funcs, values) {
0504                 /*
0505                         ISO 23001-8-18 (7.2) Transfer characteristics
0506                         TransferCharacteristics indicates the opto-electronic transfer characteristic of the source picture,
0507                         as specified in Table 3, as a function of a linear optical intensity input Lc with a nominal real-valued
0508                         range, of 0 to 1. For interpretation of entries in Table 3 that are expressed in terms of multiple curve
0509                         segments parameterized by the variable α over a region bounded by the variable β or by the variables
0510                         β and γ, the values of α and β are defined to be the positive constants necessary for the curve segments
0511                         that meet at the value β to have continuity of both value and slope at the value β. The value of γ, when
0512                         applicable, is defined to be the positive constant necessary for the associated curve segments to meet at
0513                         the value γ.
0514                         For example, for TransferCharacteristics equal to 1, 6, 14, or 15, β has the value 0.018053968510807, and
0515                         α has the value 1 + 5.5 * β = 1.099296826809442.
0516                 */
0517 
0518                 this.funcs = funcs.map((v, i) => {
0519                         for(const val in values)
0520                                 v = String(v).replace(new RegExp(`(?<=\\W|^)${val}(?=\\W|$)`, 'g'), `(${values[val]})`);
0521                         return this.funcToTree(v);
0522                 });
0523         }
0524 
0525         funcString(tree, fMap, pre, post, first) {
0526                 if(typeof(tree) === 'number')
0527                         return fMap.number(tree);
0528                 if(typeof(tree) === 'string')
0529                         return this.isOperator(tree) && !first ? ` ${tree} ` : tree;
0530                 if(tree.func)
0531                         return fMap[tree.func].apply(this, tree.args.map(v => this.funcString(v, fMap)));
0532 
0533                 let str = '';
0534                 for(let i = 0; i < tree.length; i++)
0535                         str += this.funcString(tree[i], fMap, tree[i - 1], tree[i + 1], i < 1);
0536                 return tree.length > 1 && ((pre && pre != '+') || post == '*' || post == '/') ? `(${str})` : str;
0537         }
0538 
0539         funcGLSL(tree) {
0540                 return 'return ' + this.funcString(tree, {
0541                         number: v => v.toStdFloat(7),
0542                         pow: (b, e) => `pow(${b}, ${e})`,
0543                         log: v => `(log(${v}) / log(10.0))`
0544                 }) + ';\n';
0545         }
0546 
0547         treeReplaceVar(tree, name, val) {
0548                 if(Array.isArray(tree)) {
0549                         for(let i = 0; i < tree.length; i++)
0550                                 tree[i] = this.treeReplaceVar(tree[i], name, val);
0551                         return tree;
0552                 }
0553                 if(tree.func) {
0554                         tree.args = this.treeReplaceVar(tree.args, name, val);
0555                         return tree;
0556                 }
0557                 return tree === name ? val : tree;
0558         }
0559 
0560         treeContains(tree, name) {
0561                 if(Array.isArray(tree)) {
0562                         for(let i = 0; i < tree.length; i++) {
0563                                 if(this.treeContains(tree[i], name))
0564                                         return true;
0565                         }
0566                         return false;
0567                 } else if(tree.func) {
0568                         return this.treeContains(tree.args, name);
0569                 }
0570 
0571                 return tree === name;
0572         }
0573 
0574         funcExtract(tree, name) {
0575 //              const ui = v => util.inspect(v, false, null, false);
0576 //              console.log(`***** funcExtract(${name})`, ui(tree));
0577                 tree = JSON.parse(JSON.stringify(tree)); // deep clone
0578                 let right = [];
0579                 const funcInv = {
0580                         pow: (l, r) => [ l[0], { func:'pow', args:[r, this.treeSimplify([1.0, '/', l[1]])] } ],
0581                         log: (l, r) => [ l[0], { func:'pow', args:[10, this.treeSimplify(r)] } ],
0582                 };
0583 
0584                 for(;;) {
0585                         if(tree.length == 1 && tree[0] === name)
0586                                 break;
0587 
0588                         let namePos = -1;
0589                         for(let i = 0; i < tree.length; i++) {
0590                                 if(this.treeContains(tree[i], name)) {
0591                                         namePos = i;
0592                                         break;
0593                                 }
0594                         }
0595 
0596                         if(namePos < 0) {
0597                                 console.warn(`WARNING: variable '${name}' is not in equation.`);
0598                                 return [0.0];
0599                         }
0600 
0601                         if(namePos == 0) {
0602                                 if(tree.length > 1) {
0603                                         let oper = tree[1];
0604                                         switch(oper) {
0605                                         case '-':
0606                                                 right = this.treeSimplify(right, true);
0607                                                 right = this.treeSimplify([right, '+', this.treeSimplify(tree.splice(2, tree.length - 2))], true);
0608                                                 break;
0609                                         case '+':
0610                                                 right = this.treeSimplify(right, true);
0611                                                 right = this.treeSimplify([right, '+', this.treeNegate(tree.splice(2, tree.length - 2))], true);
0612                                                 break;
0613                                         case '*':
0614                                                 right = this.treeSimplify(right, true);
0615                                                 right = this.treeSimplify([right, '/', this.treeSimplify(tree.splice(2, tree.length - 2))], true);
0616                                                 break;
0617                                         case '/':
0618                                                 right = this.treeSimplify(right, true);
0619                                                 right = this.treeSimplify([right, '*', this.treeSimplify(tree.splice(2, tree.length - 2))], true);
0620                                                 break;
0621                                         default:
0622                                                 oper = null;
0623                                                 break;
0624                                         }
0625                                         if(oper) {
0626                                                 tree.splice(1, 1);
0627                                                 tree = this.treeSimplify(tree, true);
0628 //                                              console.log(`post${oper}`, ui(tree), '==', ui(right));
0629                                         } else {
0630                                                 throw `Unexpected operator ${tree[1]}`;
0631                                         }
0632                                 }
0633                                 if(tree[0] === name)
0634                                         break;
0635                                 if(tree[0].func) {
0636                                         const f = tree[0].func;
0637 //                                      console.log('INV', tree[0], ' == ', ui(right));
0638                                         const r = funcInv[f](tree[0].args, right);
0639                                         tree[0] = r[0];
0640                                         tree = this.treeSimplify(tree, true);
0641                                         right = [r[1]];
0642 //                                      console.log(`inv-${f}`, ui(tree), '==', ui(right));
0643                                 }
0644                         } else {
0645                                 let oper = tree[namePos - 1];
0646                                 switch(oper) {
0647                                 case '-':
0648                                         if(namePos == 1) {
0649                                                 right = this.treeNegate(right, true);
0650                                         } else {
0651                                                 right = this.treeSimplify(right, true);
0652                                                 right = this.treeSimplify([right, '+', this.treeSimplify(tree.splice(0, namePos - 1))], true);
0653                                         }
0654                                         break;
0655                                 case '+':
0656                                         right = this.treeSimplify(right, true);
0657                                         right = this.treeSimplify([right, '+', this.treeNegate(tree.splice(0, namePos - 1))], true);
0658                                         break;
0659                                 case '*':
0660                                         right = this.treeSimplify(right, true);
0661                                         right = this.treeSimplify([right, '/', this.treeSimplify(tree.splice(0, namePos - 1))], true);
0662                                         break;
0663                                 case '/':
0664                                         right = this.treeSimplify(right, true);
0665                                         right = this.treeSimplify([this.treeSimplify(tree.splice(0, namePos - 1)), '/', right], true);
0666                                         break;
0667                                 default:
0668                                         oper = null;
0669                                         break;
0670                                 }
0671                                 if(oper) {
0672                                         tree.splice(0, 1);
0673                                         tree = this.treeSimplify(tree, true);
0674 //                                      console.log(`pre${oper}`, ui(tree), '==', ui(right));
0675                                 }
0676                         }
0677                 }
0678 
0679                 right = this.treeSimplify(right);
0680 //              console.log('done:', ui(tree), '==', ui(right));
0681 
0682                 return right;
0683         }
0684 
0685         isOperator(val) { return ['*', '/', '+', '-'].includes(val); }
0686 
0687         funcParams(str) {
0688                 let params = [];
0689 
0690                 while(str && str[0] != ')') {
0691                         let r = this.funcProc(str);
0692                         str = r[1];
0693                         if(r[0])
0694                                 params.push(r[0]);
0695                         if(r[2] == ')')
0696                                 break;
0697                 }
0698 
0699                 return [params, str];
0700         }
0701 
0702         treeNegate(tree, toplevel) {
0703                 if(typeof(tree) === 'number')
0704                         return -tree;
0705                 if(tree[0] == '-') {
0706                         tree.splice(0, 1);
0707                         return this.treeSimplify(tree, toplevel);
0708                 }
0709                 return ['-', tree];
0710         }
0711 
0712         treeSimplify(tree, toplevel) {
0713                 if(!Array.isArray(tree))
0714                         return tree;
0715                 tree = tree.map(v => typeof(v) == 'string' && v.match(/^[0-9\.]+$/) ? parseFloat(v) : v);
0716 //              tree = tree.map(v => Array.isArray(v) ? this.treeSimplify(v) : v);
0717 
0718                 while((Array.isArray(tree[0]) && !tree[0].length) || tree[0] === '+')
0719                         tree.shift();
0720 
0721                 for(let i = 0; i < tree.length; i++) {
0722                         if((i == 0 || this.isOperator(tree[i - 1])) && tree[i] === '-') {
0723                                 tree.splice(i, 1);
0724                                 tree[i] = this.treeNegate(tree[i]);
0725                         } else if(i && Array.isArray(tree[i]) && tree[i][0] === '-') {
0726                                 if(tree[i - 1] === '-') {
0727                                         tree[i - 1] = '+';
0728                                         tree[i].splice(0, 1);
0729                                         tree[i] = this.treeSimplify(tree[i]);
0730                                 } else if(tree[i - 1] === '+') {
0731                                         tree[i - 1] = '-';
0732                                         tree[i].splice(0, 1);
0733                                         tree[i] = this.treeSimplify(tree[i]);
0734                                 }
0735                         }
0736                 }
0737 
0738                 const replaceConst = (tree, toplevel) => {
0739                         ['*', '/', '+', '-'].forEach(oper => {
0740                                 for(let i = 1; i < tree.length; i++) {
0741                                         if(tree[i] !== oper || typeof(tree[i - 1]) !== 'number' || typeof(tree[i + 1]) !== 'number')
0742                                                 continue;
0743                                         switch(oper) {
0744                                         case '*': tree[i - 1] *= tree[i + 1]; break;
0745                                         case '/': tree[i - 1] /= tree[i + 1]; break;
0746                                         case '+': tree[i - 1] += tree[i + 1]; break;
0747                                         case '-': tree[i - 1] -= tree[i + 1]; break;
0748                                         }
0749                                         tree.splice(i, 2);
0750                                         i = 1;
0751                                 }
0752                         });
0753                         return toplevel || tree.length != 1 ? tree : tree[0];
0754                 };
0755 
0756                 ['*', '/', '+', '-'].forEach(oper => {
0757                         for(;;) {
0758                                 let found = false;
0759                                 for(let i = 1; i < tree.length - 1;) {
0760                                         if(tree[i] !== oper) {
0761                                                 i++;
0762                                                 continue;
0763                                         }
0764                                         found = true;
0765                                         tree.splice(i - 1, 0, replaceConst(tree.splice(i - 1, 3)));
0766                                 }
0767                                 if(!found)
0768                                         break;
0769                         }
0770                 });
0771 
0772                 tree = replaceConst(tree, toplevel);
0773 
0774                 while(tree && (!toplevel || Array.isArray(tree[0])) && tree.length == 1)
0775                         tree = tree[0];
0776 
0777                 return tree;
0778         }
0779 
0780         funcProc(str) {
0781                 let tree = [];
0782                 let part = '';
0783                 let ch = null;
0784                 while(str) {
0785                         ch = str[0]; str = str.substr(1);
0786                         if(ch == '(') {
0787                                 if(part) {
0788                                         let r = this.funcParams(str);
0789                                         str = r[1];
0790                                         tree.push({func:part, args:r[0]});
0791                                         part = '';
0792                                 } else {
0793                                         let r = this.funcProc(str);
0794                                         str = r[1];
0795                                         tree.push(r[0]);
0796                                 }
0797                         } else if(ch == ')' || ch == ',') {
0798                                 break;
0799                         } else if(this.isOperator(ch)) {
0800                                 if(part)
0801                                         tree.push(part);
0802                                 part = '';
0803                                 tree.push(ch);
0804                         } else {
0805                                 part += ch;
0806                         }
0807                 }
0808                 if(part)
0809                         tree.push(part);
0810                 return [this.treeSimplify(tree), str, ch];
0811         }
0812 
0813         funcToTree(str) {
0814                 return this.funcProc(str.replace(/=(.+)$/, '-($1)').replace(/\s+/g, ''))[0];
0815         }
0816 
0817         treeCalc(tree) {
0818                 if(Array.isArray(tree)) {
0819                         tree.forEach((v, i) => tree[i] = this.treeCalc(v));
0820                         if(tree[0] == '-') {
0821                                 tree.splice(0, 1);
0822                                 tree[0] = -tree[0];
0823                         }
0824 
0825                         for(let i = 1; i < tree.length; i++) {
0826                                 if(!this.isOperator(tree[i]) || typeof(tree[i - 1]) !== 'number' || typeof(tree[i + 1]) !== 'number')
0827                                         throw 'houston we have a problem';
0828                                 switch(tree[i]) {
0829                                 case '*': tree[i - 1] *= tree[i + 1]; break;
0830                                 case '/': tree[i - 1] /= tree[i + 1]; break;
0831                                 case '+': tree[i - 1] += tree[i + 1]; break;
0832                                 case '-': tree[i - 1] -= tree[i + 1]; break;
0833                                 }
0834                                 tree.splice(i, 2);
0835                                 i = 1;
0836                         }
0837 
0838                         return tree.length == 1 ? tree[0] : tree;
0839                 }
0840                 if(tree.func) {
0841                         const func = {
0842                                 pow: (b, e) => Math.pow(b, e),
0843                                 log: v => Math.log(v)
0844                         };
0845                         return func[tree.func].apply(null, tree.args.map(v => this.treeCalc(v)));
0846                 }
0847                 return tree;
0848         }
0849 
0850         glslShader(comment, precision=7) {
0851                 let res = { func:'', funcInv:'', comment:comment };
0852 
0853                 for(let i = this.funcs.length - 1; i >= 0; i -= 2) {
0854                         if(i) {
0855                                 const l1 = this.treeCalc(this.funcs[i - 1]).toStdFloat(7);
0856                                 res.func += `if(vLin < ${l1}) `;
0857                                 const l2 = this.treeCalc(this.treeReplaceVar(this.funcExtract(this.funcs[i], 'vExp'), 'vLin', this.funcs[i - 1])).toStdFloat(7);
0858                                 res.funcInv += `if(vExp < ${l2}) `;
0859                         }
0860                         res.func += this.funcGLSL(this.funcExtract(this.funcs[i], 'vExp'));
0861                         res.funcInv += this.funcGLSL(this.funcExtract(this.funcs[i], 'vLin'));
0862                 }
0863                 res.func = res.func.trim();
0864                 res.funcInv = res.funcInv.trim();
0865 
0866 //              res.debug = this.funcs;
0867 //              const ui = v => util.inspect(v, false, null, false);
0868 //              console.log(ui(res));
0869 
0870                 return res;
0871         }
0872 }
0873 
0874 const sRGB = new ColorPrimaries(0.640, 0.330, 0.300, 0.600, 0.150, 0.060, 0.31271, 0.32902);
0875 
0876 if(0){
0877         const n = -1/3;
0878         let eq = [
0879                 [ Math.pow(sRGB.Wx / sRGB.Wy, n), sRGB.Wx, sRGB.Wy, 1.0 ],
0880                 [ Math.pow(sRGB.Rx / sRGB.Ry, n), sRGB.Rx, sRGB.Ry, 0.21263682167732387 ],
0881                 [ Math.pow(sRGB.Gx / sRGB.Gy, n), sRGB.Gx, sRGB.Gy, 0.7151829818412506 ],
0882                 [ Math.pow(sRGB.Bx / sRGB.By, n), sRGB.Bx, sRGB.By, 0.07218019648142547 ], // test value
0883         ];
0884         console.log('xy max Luma', eq);
0885         mat.solveLinearEquation(eq);
0886         console.log('xy max Luma', eq); // bad - not linear
0887         process.exit();
0888 
0889 }
0890 
0891 if(0){
0892         const dump = (name, arr) => {
0893                 let s = [];
0894                 const p = (val) => parseFloat(val.toFixed(3).replace(/^-(0\.0+)$/, '$1')) || '.';
0895                 for(let y = 0; y < arr.length; y++) {
0896                         s[y] = [];
0897                         for(let x = 0; x < arr[y].length; x++)
0898                                 s[y].push(p(arr[y][x]));
0899                         s[y] = s[y].join('\t');
0900                 }
0901                 console.log(`${name}\t${s.join('\n\t')}`);
0902         };
0903 
0904         const dist = (v1, v2) => Math.sqrt(Math.pow(v1.x - v2.x, 2) + Math.pow(v1.y - v2.y, 2));
0905         const RGB_XYZ = sRGB.mat3_RGB_XYZ();
0906         const Rv = new vec(sRGB.Rx, sRGB.Ry, RGB_XYZ.mul(new vec(1, 0, 0)).y);
0907         const Gv = new vec(sRGB.Gx, sRGB.Gy, RGB_XYZ.mul(new vec(0, 1, 0)).y);
0908         const Bv = new vec(sRGB.Bx, sRGB.By, RGB_XYZ.mul(new vec(0, 0, 1)).y);
0909         const Wv = new vec(sRGB.Wx, sRGB.Wy, RGB_XYZ.mul(new vec(1, 1, 1)).y);
0910 
0911         const xyY = (r,g,b) => {
0912                 const v = RGB_XYZ.mul(new vec(1, 1, 0));
0913                 const sum = v.elem.reduce((a, v) => a + v, 0);
0914                 return new vec(v.x/sum, v.y/sum, v.y);
0915         };
0916 
0917         const RGv = xyY(1, 1, 0);
0918         const RBv = xyY(1, 0, 1);
0919         const GBv = xyY(0, 1, 1);
0920 
0921         const fromRGB = (r, g, b) => {
0922                 const rat = sRGB.rgbYratios;
0923                 return r * rat.r * sRGB.Ry
0924                         + g * rat.g * sRGB.Gy
0925                         + b * rat.b * sRGB.By;
0926         };
0927 
0928         console.log();
0929         console.log('red Y', fromRGB(1, 0, 0), Rv.z,  sRGB.get_xy_Y(new vec(Rv.x, Rv.y)));
0930         console.log('rg  Y', fromRGB(1, 1, 0), RGv.z, sRGB.get_xy_Y(new vec(RGv.x, RGv.y)));
0931         console.log('grn Y', fromRGB(0, 1, 0), Gv.z,  sRGB.get_xy_Y(new vec(Gv.x, Gv.y)));
0932         console.log('gb  Y', fromRGB(1, 1, 0), GBv.z, sRGB.get_xy_Y(new vec(GBv.x, GBv.y)));
0933         console.log('blu Y', fromRGB(0, 0, 1), Bv.z,  sRGB.get_xy_Y(new vec(Bv.x, Bv.y)));
0934         console.log('rb  Y', fromRGB(1, 1, 0), RBv.z, sRGB.get_xy_Y(new vec(RBv.x, RBv.y)));
0935         console.log('wht Y', fromRGB(1, 1, 1), Wv.z,  sRGB.get_xy_Y(new vec(Wv.x, Wv.y)));
0936         process.exit();
0937 }
0938 
0939 if(0) { // test CIE diagram GLSL
0940         console.log(`GLfloat pR[]{ ${sRGB.Rx.toCFloat()}, ${sRGB.Ry.toCFloat()}, ${(sRGB.Ry * sRGB.rgbYratios.r).toCFloat()} };`);
0941         console.log(`GLfloat pG[]{ ${sRGB.Gx.toCFloat()}, ${sRGB.Gy.toCFloat()}, ${(sRGB.Gy * sRGB.rgbYratios.g).toCFloat()} };`);
0942         console.log(`GLfloat pB[]{ ${sRGB.Bx.toCFloat()}, ${sRGB.By.toCFloat()}, ${(sRGB.By * sRGB.rgbYratios.b).toCFloat()} };`);
0943         console.log(`GLfloat pW[]{ ${sRGB.Wx.toCFloat()}, ${sRGB.Wy.toCFloat()}, 1.0f };`);
0944         console.log(`GLfloat csXYZ_RGB[]{ ${sRGB.mat3_XYZ_RGB().cFloat('XYZ_sRGB', 'XYZ -> sRGB colorspace').mat.join(', ')} };`);
0945         console.log(`GLfloat csxyY_RGB[]{ ${sRGB.mat4_xyY_RGB().cFloat('xyY_sRGB', 'xyY -> sRGB colorspace').mat.join(', ')} };`);
0946         console.log(`GLfloat triangle[]{ ${sRGB.Rx}f, ${sRGB.Ry}f, ${sRGB.Gx}f, ${sRGB.Gy}f, ${sRGB.Bx}f, ${sRGB.By}f, ${sRGB.Wx}f, ${sRGB.Wy}f };`)
0947 }
0948 
0949 if(0) {
0950         const RGB_XYZ = sRGB.mat3_RGB_XYZ();
0951         const XYZ_RGB = sRGB.mat3_XYZ_RGB();
0952         const RGB_xyY = sRGB.mat4_RGB_xyY();
0953         const xyY_RGB = sRGB.mat4_xyY_RGB();
0954 
0955         if(0) {
0956                 RGB_XYZ.mul(new vec(1, 0, 0)).debugDump(`red   XYZ`);
0957                 RGB_XYZ.mul(new vec(0, 1, 0)).debugDump(`green XYZ`);
0958                 RGB_XYZ.mul(new vec(0, 0, 1)).debugDump(`blue  XYZ`);
0959                 RGB_XYZ.mul(new vec(1, 1, 1)).debugDump(`white XYZ`);
0960                 RGB_XYZ.mul(new vec(0, 0, 0)).debugDump(`black XYZ`);
0961 
0962                 XYZ_RGB.mul(RGB_XYZ.mul(new vec(1, 0, 0))).debugDump(`\nred   RGB`);
0963                 XYZ_RGB.mul(RGB_XYZ.mul(new vec(0, 1, 0))).debugDump(`green RGB`);
0964                 XYZ_RGB.mul(RGB_XYZ.mul(new vec(0, 0, 1))).debugDump(`blue  RGB`);
0965                 XYZ_RGB.mul(RGB_XYZ.mul(new vec(1, 1, 1))).debugDump(`white RGB`);
0966                 XYZ_RGB.mul(RGB_XYZ.mul(new vec(0, 0, 0))).debugDump(`black RGB`);
0967 
0968                 RGB_xyY.mul(new vec(1, 0, 0, 1)).debugDump(`\nred   xyY`);
0969                 RGB_xyY.mul(new vec(0, 1, 0, 1)).debugDump(`green xyY`);
0970                 RGB_xyY.mul(new vec(0, 0, 1, 1)).debugDump(`blue  xyY`);
0971                 RGB_xyY.mul(new vec(1, 1, 1, 1)).debugDump(`white xyY`);
0972                 RGB_xyY.mul(new vec(0, 0, 0, 1)).debugDump(`black xyY`);
0973 
0974                 xyY_RGB.mul(RGB_xyY.mul(new vec(1, 0, 0, 1))).debugDump(`\nred   RGB`);
0975                 xyY_RGB.mul(RGB_xyY.mul(new vec(0, 1, 0, 1))).debugDump(`green RGB`);
0976                 xyY_RGB.mul(RGB_xyY.mul(new vec(0, 0, 1, 1))).debugDump(`blue  RGB`);
0977                 xyY_RGB.mul(RGB_xyY.mul(new vec(1, 1, 1, 1))).debugDump(`white RGB`);
0978                 xyY_RGB.mul(RGB_xyY.mul(new vec(0, 0, 0, 1))).debugDump(`black RGB`);
0979         }
0980 
0981         if(1) {
0982                 xyY_RGB.mul(new vec(sRGB.Rx, sRGB.Ry, sRGB.get_xy_Y(new vec(sRGB.Rx, sRGB.Ry)), 1)).debugDump(`\nred   RGB`);
0983                 xyY_RGB.mul(new vec(sRGB.Gx, sRGB.Gy, sRGB.get_xy_Y(new vec(sRGB.Gx, sRGB.Gy)), 1)).debugDump(`green RGB`);
0984                 xyY_RGB.mul(new vec(sRGB.Bx, sRGB.By, sRGB.get_xy_Y(new vec(sRGB.Bx, sRGB.By)), 1)).debugDump(`blue  RGB`);
0985                 xyY_RGB.mul(new vec(sRGB.Wx, sRGB.Wy, sRGB.get_xy_Y(new vec(sRGB.Wx, sRGB.Wy)), 1)).debugDump(`white RGB`);
0986         }
0987 
0988         if(0) {
0989                 const rXYZ = RGB_XYZ.mul(new vec(1, 0, 0)); //.debugDump(`red   XYZ`);
0990                 const gXYZ = RGB_XYZ.mul(new vec(0, 1, 0)); //.debugDump(`green XYZ`);
0991                 const bXYZ = RGB_XYZ.mul(new vec(0, 0, 1)); //.debugDump(`blue  XYZ`);
0992                 const wXYZ = RGB_XYZ.mul(new vec(1, 1, 1)); //.debugDump(`white XYZ`);
0993 
0994                 let eq = [
0995                         [ sRGB.Wx, sRGB.Wy, sRGB.Wz, wXYZ.y ],
0996                         [ sRGB.Rx, sRGB.Ry, sRGB.Rz, rXYZ.y ],
0997                         [ sRGB.Gx, sRGB.Gy, sRGB.Gz, gXYZ.y ],
0998                         [ sRGB.Bx, sRGB.By, sRGB.Bz, bXYZ.y ], // test value
0999                 ];
1000                 console.log('xy max Luma', eq);
1001                 mat.solveLinearEquation(eq);
1002                 console.log('xy max Luma', eq); // bad - not linear
1003                 process.exit();
1004         }
1005 
1006         if(0) {
1007                 const x1 = sRGB.Bx;
1008                 const y1 = sRGB.By;
1009                 const x2 = sRGB.Wx;
1010                 const y2 = sRGB.Wy;
1011                 const Y1 = sRGB.get_xy_Y(new vec(x1, y1));
1012                 const Y2 = sRGB.get_xy_Y(new vec(x2, y2));
1013                 for(let r = 0.0; r <= 1.0001; r += .1) {
1014                         const L1 = r * Y1;
1015                         const L2 = (1. - r) * Y2;
1016                         const xMix = (x1/y1*L1 + x2/y2*L2) / (L1/y1 + L2/y2);
1017                         const yMix = (L1 + L2) / (L1/y1 + L2/y2);
1018                         const Ymix = sRGB.get_xy_Y(new vec(xMix, yMix));
1019 
1020                         xyY_RGB.mul(new vec(xMix, yMix, Ymix, 1)).debugDump(`x:${xMix.toFixed(2)} y:${yMix.toFixed(2)} Y:${Ymix.toFixed(2)} ${r.toFixed(2)} RGB`);
1021 
1022                         const XYZ = new vec(Ymix * xMix / yMix, Ymix, Ymix * (1 - xMix - yMix) / yMix);
1023                         XYZ_RGB.mul(XYZ).debugDump(`\t\t     TEST RGB`);
1024                         XYZ.debugDump(`\t\t     TEST XYZ`);
1025                         console.log(`\t\t     TEST   L1: ${L1.toFixed(4)}\t    L2: ${L2.toFixed(4)}\t     x: ${xMix.toFixed(4)}\t     y: ${yMix.toFixed(4)}`);
1026                 }
1027                 console.log('whole range:');
1028                 for(let x = 0.; x <= 1.0; x += .05)
1029                         console.log(x, xyY_RGB.mul(new vec(x, x, 1, 1)).xyz);
1030         }
1031         process.exit();
1032 }
1033 
1034 
1035 
1036 // Header
1037 console.log('#include <QMap>\n#include <QVector>\n#include <QOpenGLFunctions>\n\nnamespace SubtitleComposer {\n')
1038 
1039 // Colorspace conversion from ISO IEC 23001-8:2018 (7.1) data
1040 {
1041         const csm = {
1042                 1: new ColorPrimaries(0.640, 0.330, 0.300, 0.600, 0.150, 0.060, 0.31271, 0.32902).mat3_YUV_RGB().cFloat('AVCOL_PRI_BT709',
1043                         'ITU-R BT.709-5; ITU-R BT.1361; IEC 61966-2-1 sRGB or sYCC; IEC 61966-2-4; SMPTE-RP-177:1993b'),
1044                 2: { comment: 'UNSPECIFIED - Image characteristics are unknown or are determined by the application' },
1045                 4: new ColorPrimaries(0.67, 0.33, 0.21, 0.71, 0.14, 0.08, 0.31006, 0.31616).mat3_YUV_RGB().cFloat('AVCOL_PRI_BT470M',
1046                         'ITU-R BT.470-6m; US-NTSC-1953; USFCCT-47:2003-73.682a'),
1047                 5: new ColorPrimaries(0.64, 0.33, 0.29, 0.60, 0.15, 0.06, 0.31271, 0.32902).mat3_YUV_RGB().cFloat('AVCOL_PRI_BT470BG',
1048                         'ITU-R BT.470-6bg; ITU-R BT.601-6 625; ITU-R BT.1358 625; ITU-R BT.1700 625 PAL/SECAM'),
1049                 6: new ColorPrimaries(0.630, 0.340, 0.310, 0.595, 0.155, 0.070, 0.31271, 0.32902).mat3_YUV_RGB().cFloat('AVCOL_PRI_SMPTE170M',
1050                         'ITU-R BT.601-6 525; ITU-R BT.1358 525; ITU-R BT.1700 NTSC; SMPTE-170M:2004'),
1051                 7: new ColorPrimaries(0.630, 0.340, 0.310, 0.595, 0.155, 0.070, 0.31271, 0.32902).mat3_YUV_RGB().cFloat('AVCOL_PRI_SMPTE240M',
1052                         'SMPTE-240M:1999'),
1053                 8: new ColorPrimaries(0.681, 0.319, 0.243, 0.692, 0.145, 0.049, 0.31006, 0.31616).mat3_YUV_RGB().cFloat('AVCOL_PRI_FILM',
1054                         'Generic film (color filters using CIE SI C)'),
1055                 9: new ColorPrimaries(0.708, 0.292, 0.170, 0.797, 0.131, 0.046, 0.31271, 0.32902).mat3_YUV_RGB().cFloat('AVCOL_PRI_BT2020',
1056                         'Rec. ITU-R BT.2020'),
1057                 10: new ColorPrimaries(1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0/3.0, 1.0/3.0).mat3_YUV_RGB().cFloat('AVCOL_PRI_SMPTE428',
1058                         'SMPTE-ST-428-1 (CIE 1931 XYZ as in ISO 11664-1)'),
1059                 11: new ColorPrimaries(0.680, 0.320, 0.265, 0.690, 0.150, 0.060, 0.314, 0.351).mat3_YUV_RGB().cFloat('AVCOL_PRI_SMPTE431',
1060                         'SMPTE-RP-431-2:2011'),
1061                 12: new ColorPrimaries(0.680, 0.320, 0.265, 0.690, 0.150, 0.060, 0.31271, 0.32902).mat3_YUV_RGB().cFloat('AVCOL_PRI_SMPTE432',
1062                         'SMPTE-EG-432-1:2010'),
1063                 22: new ColorPrimaries(0.630, 0.340, 0.295, 0.605, 0.155, 0.077, 0.31271, 0.32902).mat3_YUV_RGB().cFloat('AVCOL_PRI_EBU3213',
1064                         'EBU-3213-E:1975'),
1065         };
1066         console.log('// from ISO IEC 23001-8:2018 (7.1) data')
1067         console.log('const static QMap<int, QVector<GLfloat>> _csm{');
1068         const n = Object.keys(csm).reduce((ac, v) => Math.max(ac, v), 0);
1069         for(let i = 0; i <= n; i++) {
1070                 const e = csm[i];
1071                 if(e && e.mat)
1072                         console.log(`\t// ${i} - ${e.comment}\n\t{ ${i}, QVector<GLfloat>{ ${e.mat.join(', ')} }},`);
1073                 else
1074                         console.log(`\t// ${i} - ${e ? e.comment : 'RESERVED - For future use by ISO/IEC'}`);
1075         }
1076         console.log('};\n');
1077 }
1078 
1079 // Transfer characteristics inverse from ISO IEC 23001-8:2018 (7.2) data
1080 {
1081         const ctf = {
1082                 1: new ColorTransfer(['vExp = α * pow(vLin, γ) - (α - 1)', 'β', 'vExp = lm * vLin'],
1083                                                          {'α': '1 + 5.5 * β', 'β': 0.018053968510807, 'γ': 0.45, 'lm': 4.5}).glslShader(
1084                            'ITU-R BT.709-5; ITU-R BT.1361'),
1085                 2: { comment: 'Unspecified - Image characteristics are unknown or are determined by the application' },
1086                 // Assumed display gamma 2.2 - very similar to BT.709
1087                 4: new ColorTransfer(['vExp = α * pow(vLin, γ) - (α - 1)', 'β', 'vExp = lm * vLin'],
1088                                                          {'α': '1 + 5.5 * β', 'β': 0.018053968510807, 'γ': 1 / 2.2, 'lm': 4.5}).glslShader(
1089                            'ITU-R BT.470-6m; US-NTSC-1953; USFCCT-47:2003-73.682a; ITU-R BT.1700:2007 625 PAL/SECAM'),
1090                 // Assumed display gamma 2.8 - very similar to sRGB
1091                 5: new ColorTransfer(['vExp = α * pow(vLin, γ) - (α - 1)', 'β', 'vExp = lm * vLin'],
1092                                                          {'α': 1.055, 'β': 0.0031308, 'γ': 1 / 2.8, 'lm': 12.92}).glslShader(
1093                            'ITU-R BT.1700:2007 625 PAL/SECAM; ITU-R BT.470-6bg'),
1094                 6: new ColorTransfer(['vExp = α * pow(vLin, γ) - (α - 1)', 'β', 'vExp = lm * vLin'],
1095                                                          {'α': '1 + 5.5 * β', 'β': 0.018053968510807, 'γ': 0.45, 'lm': 4.5}).glslShader(
1096                            'ITU-R BT.601-6 525/625; ITU-R BT.1358 525/625; ITU-R BT.1700 NTSC; SMPTE-170M:2004'),
1097                 7: new ColorTransfer(['vExp = α * pow(vLin, γ) - (α - 1)', 'β', 'vExp = lm * vLin'],
1098                                                          {'α': 1.1115, 'β': 0.0228, 'γ': 0.45, 'lm': 4.0}).glslShader(
1099                            'SMPTE-240M:1999'),
1100                 8: new ColorTransfer(['vExp = vLin'], {}).glslShader(
1101                            'Linear transfer characteristics'),
1102                 9: new ColorTransfer(['vExp = 1.0 + log(vLin) / 2', 0.01, 'vExp = 0.0'], {}).glslShader(
1103                            'Logarithmic transfer characteristic (100:1 range)'),
1104                 10: new ColorTransfer(['vExp = 1.0 + log(vLin) / 2.5', Math.sqrt(10) / 1000, 'vExp = 0.0'], {}).glslShader(
1105                            'Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range)'),
1106                 11: new ColorTransfer(['vExp = α * pow(vLin, γ) - (α - 1)', 'β', 'vExp = lm * vLin', '-β', 'vExp = -α * pow(-vLin, γ) + (α - 1)'],
1107                                                          {'α': '1 + 5.5 * β', 'β': 0.018053968510807, 'γ': 0.45, 'lm': 4.5}).glslShader(
1108                            'IEC 61966-2-4'),
1109                 12: new ColorTransfer(['vExp = α * pow(vLin, ex) - (α - 1)', 'β', 'vExp = lm * vLin', '-γ', 'vExp = -(α * pow(-4 * vLin, ex) - (α - 1)) / 4'],
1110                                                          {'α': '1 + 5.5 * β', 'β': 0.018053968510807, 'γ': 0.004, 'ex': 0.45, 'lm': 4.5}).glslShader(
1111                            'ITU-R BT.1361'),
1112                 13: new ColorTransfer(['vExp = α * pow(vLin, 1.0 / γ) - (α - 1)', 'β', 'vExp = lm * vLin'],
1113                                                          {'α': 1.055, 'β': 0.0031308, 'γ': 2.4, 'lm': 12.92}).glslShader(
1114                            'IEC 61966-2-1 sRGB/sYCC'),
1115                 14: new ColorTransfer(['vExp = α * pow(vLin, γ) - (α - 1)', 'β', 'vExp = lm * vLin'],
1116                                                          {'α': '1 + 5.5 * β', 'β': 0.018053968510807, 'γ': 0.45, 'lm': 4.5}).glslShader(
1117                            'ITU-R BT.2020 (10-bit system)'),
1118                 15: new ColorTransfer(['vExp = α * pow(vLin, γ) - (α - 1)', 'β', 'vExp = lm * vLin'],
1119                                                          {'α': '1 + 5.5 * β', 'β': 0.018053968510807, 'γ': 0.45, 'lm': 4.5}).glslShader(
1120                            'ITU-R BT.2020 (12-bit system)'),
1121                 16: new ColorTransfer(['vExp = pow((c1 + c2 * pow(vLin, n)) / (1 + c3 * pow(vLin, n)), m)'],
1122                                                          {'c1': 'c3 - c2 + 1', 'c2': '2413 / 128', 'c3': '2392 / 128', 'm': '2523 / 32', 'n': '653 / 4096'}).glslShader(
1123                            'SMPTE-ST-2084 (for TV 10, 12, 14, and 16-bit systems)'),
1124                 17: new ColorTransfer(['vExp = pow(48 * vLin / 52.37, 1 / 2.6)'],
1125                                                          {}).glslShader(
1126                                 'SMPTE-ST-428-1'),
1127         };
1128         console.log('// from ISO IEC 23001-8:2018 (7.2) data')
1129         console.log('const static QMap<int, QString> _ctf{');
1130         const n = Object.keys(ctf).reduce((ac, v) => Math.max(ac, v), 0);
1131         for(let i = 0; i <= n; i++) {
1132                 const e = ctf[i];
1133                 if(e && e.func)
1134                         console.log(`\t// ${i} - ${e.comment}\n\t{ ${i}, QStringLiteral("${e.func.replaceAll('\n', '"\n\t\t\t"')}") },`);
1135                 else
1136                         console.log(`\t// ${i} - ${e ? e.comment : 'RESERVED - For future use by ISO/IEC'}`);
1137         }
1138         console.log('};');
1139         console.log('const static QMap<int, QString> _ctfi{');
1140         for(let i = 0; i <= n; i++) {
1141                 const e = ctf[i];
1142                 if(e && e.funcInv)
1143                         console.log(`\t// ${i} - ${e.comment}\n\t{ ${i}, QStringLiteral("${e.funcInv.replaceAll('\n', '"\n\t\t\t"')}") },`);
1144                 else
1145                         console.log(`\t// ${i} - ${e ? e.comment : 'RESERVED - For future use by ISO/IEC'}`);
1146         }
1147         console.log('};');
1148 }
1149 
1150 // Colorspace conversion and coefficients from ISO IEC 23001-8:2018 (7.3) data
1151 {
1152         const csc = {
1153                 0: new ColorCoefficients(0.0, 0.0, 0.0, true).mat3_RGB_RGB().cFloat('AVCOL_SPC_RGB',
1154                         'The identity matrix (RGB/XYZ); IEC 61966-2-1 sRGB; SMPTE-ST-428-1; ITU-R BT.709-5'),
1155                 1: new ColorCoefficients(0.2126, 0.7152, 0.0722, false).mat3_YUV_RGB().cFloat('AVCOL_SPC_BT709',
1156                         'ITU-R BT.709-5; ITU-R BT.1361; IEC 61966-2-1/4 sYCC/xvYCC709; SMPTE-RP-177:1993b'),
1157                 2: { comment: 'UNSPECIFIED - Image characteristics are unknown or are determined by the application' },
1158                 4: new ColorCoefficients(0.30, 0.59, 0.111, false).mat3_YUV_RGB().cFloat('AVCOL_SPC_FCC',
1159                         'USFCCT-47:2003-73.682a'),
1160                 5: new ColorCoefficients(0.299, 0.587, 0.114, false).mat3_YUV_RGB().cFloat('AVCOL_SPC_BT470BG',
1161                         'ITU-R BT.470-6bg; ITU-R BT.601-6 625; ITU-R BT.1358 625; ITU-R BT.1700 625 PAL/SECAM; IEC 61966-2-4 xvYCC601'),
1162                 6: new ColorCoefficients(0.299, 0.587, 0.114, false).mat3_YUV_RGB().cFloat('AVCOL_SPC_SMPTE170M',
1163                         'ITU-R BT.601-6 525; ITU-R BT.1358 525; ITU-R BT.1700 NTSC; SMPTE-170M:2004'),
1164                 7: new ColorCoefficients(0.212, 0.701, 0.087, false).mat3_YUV_RGB().cFloat('AVCOL_SPC_SMPTE240M',
1165                         'SMPTE-240M:1999'),
1166                 8: new ColorCoefficients(0.0, 0.0, 0.0, true).mat3_RGB_RGB().cFloat('AVCOL_SPC_YCGCO',
1167                         'ITU-T SG16; Dirac/VC-2 and H.264 FRext'),
1168                 9: new ColorCoefficients(0.2627, 0.678, 0.0593, false).mat3_YUV_RGB().cFloat('AVCOL_SPC_BT2020_NCL',
1169                         'ITU-R BT.2020 (non-constant luminance)'),
1170                 10: new ColorCoefficients(0.2627, 0.678, 0.0593, false).mat3_YUV_RGB().cFloat('AVCOL_SPC_BT2020_CL',
1171                         'ITU-R BT.2020 (constant luminance)'),
1172                 11: new ColorCoefficients(0.0, 0.0, 0.0, true).mat3_RGB_RGB().cFloat('AVCOL_SPC_SMPTE2085',
1173                         'SMPTE-ST-2085:2015'),
1174                 // 12-14 are not part of ISO
1175                 12: { comment: 'TODO: AVCOL_SPC_CHROMA_DERIVED_NCL defined in FFmpeg?' },
1176                 13: { comment: 'TODO: AVCOL_SPC_CHROMA_DERIVED_CL defined in FFmpeg?' },
1177                 14: { comment: 'TODO: AVCOL_SPC_ICTCP defined in FFmpeg?' },
1178         };
1179         console.log('// from ISO IEC 23001-8:2018 (7.3) data')
1180         console.log('const static QMap<int, QVector<GLfloat>> _csc{');
1181         const n = Object.keys(csc).reduce((ac, v) => Math.max(ac, v), 0);
1182         for(let i = 0; i <= n; i++) {
1183                 const e = csc[i];
1184                 if(e && e.mat)
1185                         console.log(`\t// ${i} - ${e.comment}\n\t{ ${i}, QVector<GLfloat>{ ${e.mat.join(', ')} }},`);
1186                 else
1187                         console.log(`\t// ${i} - ${e ? e.comment : 'RESERVED - For future use by ISO/IEC'}`);
1188         }
1189         console.log('};\n');
1190 }
1191 
1192 // Footer
1193 console.log('\n}');
1194