@tokens-studio/graph-engine
Version:
An execution engine to handle Token Studios generators and resolvers
137 lines • 5.03 kB
JavaScript
import { Black, Gray, White, toColor, toColorObject } from './lib/utils.js';
import { BooleanSchema, ColorSchema, NumberSchema } from '../../schemas/index.js';
import { Node } from '../../programmatic/node.js';
import { setToPrecision } from "../../utils/precision.js";
import Color from 'colorjs.io';
export default class NodeDefinition extends Node {
static title = 'Match Alpha';
static type = 'studio.tokens.color.matchAlpha';
static description = 'Finds the alpha value that, when used to blend the foreground and background colors, will result in the reference color.';
constructor(props) {
super(props);
this.addInput('foreground', {
type: {
...ColorSchema,
default: Black
}
});
this.addInput('background', {
type: {
...ColorSchema,
default: White
}
});
this.addInput('reference', {
type: {
...ColorSchema,
default: Gray
}
});
this.addInput('threshold', {
type: {
...NumberSchema,
default: 0.01
}
});
this.addInput('precision', {
type: {
...NumberSchema,
default: 0.01
}
});
this.addOutput('inRange', {
type: BooleanSchema
});
this.addOutput('color', {
type: ColorSchema
});
this.addOutput('alpha', {
type: NumberSchema
});
}
execute() {
const { foreground, background, reference, threshold, precision } = this.getAllInputs();
const bg = toColor(background);
const fg = toColor(foreground);
const ref = toColor(reference);
let alpha = Number.NaN;
let inRange = false;
// the matching is done per channel
// if the background and reference are "the same" (within precision), return zero
if (Math.abs(bg.r - ref.r) < precision &&
Math.abs(bg.g - ref.g) < precision &&
Math.abs(bg.b - ref.b) < precision) {
alpha = 0;
inRange = true;
}
else {
// find the matching alpha for each channel
const ar = validateAlpha((ref.r - bg.r) / (fg.r - bg.r));
const ag = validateAlpha((ref.g - bg.g) / (fg.g - bg.g));
const ab = validateAlpha((ref.b - bg.b) / (fg.b - bg.b));
// return the average of the alphas for all matched channels
if (!isNaN(ar) && !isNaN(ag) && !isNaN(ab)) {
if (Math.abs(ar - ar) < precision &&
Math.abs(ag - ag) < precision &&
Math.abs(ab - ab) < precision) {
alpha = (ar + ag + ab) / 3;
inRange = true;
}
}
else if (!isNaN(ar) && !isNaN(ag)) {
if (Math.abs(ar - ag) < precision) {
alpha = (ar + ag) / 2;
inRange = true;
}
}
else if (!isNaN(ag) && !isNaN(ab)) {
if (Math.abs(ag - ab) < precision) {
alpha = (ag + ab) / 2;
inRange = true;
}
}
else if (!isNaN(ar) && !isNaN(ab)) {
if (Math.abs(ar - ab) < precision) {
alpha = (ar + ab) / 2;
inRange = true;
}
}
else {
alpha = ar || ag || ab;
inRange = !isNaN(alpha);
}
}
// round the result to match the precision
alpha = setToPrecision(alpha, Math.max(0, -Math.floor(Math.log10(precision))));
// calculate the composite color, and if it's too far from the reference, return NaN
// deltaE() returns values typically ranging from 0 to 100, so I divide it by 100
// to compare normalized colors
const comp = blendColors(fg, bg, alpha);
if (comp.deltaE2000(ref) / 100 > threshold) {
alpha = Number.NaN;
inRange = false;
}
// if the resulting alpha is valid, assign it to the foreground color,
// which becomes the output color
if (inRange)
fg.alpha = alpha;
this.outputs.inRange.set(inRange);
this.outputs.color.set(toColorObject(fg));
this.outputs.alpha.set(alpha);
}
}
function validateAlpha(alpha) {
// set valid but out-of-range alphas to NaN
return !isNaN(alpha) && alpha >= 0 && alpha <= 1 ? alpha : Number.NaN;
}
function blendChannels(fg, bg, alpha) {
return fg * alpha + bg * (1 - alpha);
}
function blendColors(fg, bg, alpha) {
return new Color('srgb', [
blendChannels(fg.r, bg.r, alpha),
blendChannels(fg.g, bg.g, alpha),
blendChannels(fg.b, bg.b, alpha)
]);
}
//# sourceMappingURL=matchAlpha.js.map