color.ts
examples/fxedit/fx/nodes/color.ts · raw · open the example · all examples · site home
Exports: colorNodes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
// Color operations. Splat colors are sRGB-encoded values; above 1 is allowed
// (the ext encoding keeps HDR) and reads as glow.
import { TSL } from "@sparkjsdev/radix";
import type { NodeDef } from "../types.js";
import { IQ_PALETTE, cosinePalette } from "./iq.js";
const { hue, max, min, mix, saturation, vec3 } = TSL;
const THREE_COLOR = { name: "three.js ColorAdjustment", url: "https://github.com/mrdoob/three.js/blob/dev/src/nodes/display/ColorAdjustment.js", license: "MIT" };
// Matt Zucker, "Simple polynomial approximations of matplotlib colormaps"
// (shadertoy WlfXRN, CC0): degree-6 fits, t in [0, 1]
const ZUCKER = { name: "Matt Zucker, polynomial matplotlib colormaps", url: "https://www.shadertoy.com/view/WlfXRN", license: "CC0" };
const COLORMAPS: Record<string, number[][]> = {
magma: [[-0.002136485053939582, -0.000749655052795221, -0.005386127855323933], [0.2516605407371642, 0.6775232436837668, 2.494026599312351], [8.353717279216625, -3.577719514958484, 0.3144679030132573], [-27.66873308576866, 14.26473078096533, -13.64921318813922], [52.17613981234068, -27.94360607168351, 12.94416944238394], [-50.76852536473588, 29.04658282127291, 4.23415299384598], [18.65570506591883, -11.48977351997711, -5.601961508734096]],
inferno: [[0.0002189403691192265, 0.001651004631001012, -0.01948089843709184], [0.1065134194856116, 0.5639564367884091, 3.932712388889277], [11.60249308247187, -3.972853965665698, -15.9423941062914], [-41.70399613139459, 17.43639888205313, 44.35414519872813], [77.162935699427, -33.40235894210092, -81.80730925738993], [-71.31942824499214, 32.62606426397723, 73.20951985803202], [25.13112622477341, -12.24266895238567, -23.07032500287172]],
plasma: [[0.05873234392399702, 0.02333670892565664, 0.5433401826748754], [2.176514634195958, 0.2383834171260182, 0.7539604599784036], [-2.689460476458034, -7.455851135738909, 3.110799939717086], [6.130348345893603, 42.3461881477227, -28.51885465332158], [-11.10743619062271, -82.66631109428045, 60.13984767418263], [10.02306557647065, 71.41361770095349, -54.07218655560067], [-3.658713842777788, -22.93153465461149, 18.19190778539828]],
viridis: [[0.2777273272234177, 0.005407344544966578, 0.3340998053353061], [0.1050930431085774, 1.404613529898575, 1.384590162594685], [-0.3308618287255563, 0.214847559468213, 0.09509516302823659], [-4.634230498983486, -5.799100973351585, -19.33244095627987], [6.228269936347081, 14.17993336680509, 56.69055260068105], [4.776384997670288, -13.74514537774601, -65.35303263337234], [-5.435455855934631, 4.645852612178535, 26.3124352495832]],
};
export const colorNodes: NodeDef[] = [
{
id: "color.colormap",
label: "Colormap",
description: "A perceptual color ramp for a 0..1 number (magma, inferno, plasma, viridis)",
category: "color",
inputs: { t: { type: "float", default: 0.5, min: 0, max: 1 } },
options: { map: { default: "magma", choices: Object.keys(COLORMAPS) } },
outputs: { value: "color" },
source: ZUCKER,
build: ({ t }, ctx) => {
const c = COLORMAPS[String(ctx.option("map"))] ?? COLORMAPS.magma;
const x = t.clamp(0, 1).toVar();
let acc = vec3(...c[6]);
for (let i = 5; i >= 0; i -= 1) {
acc = vec3(...c[i]).add(x.mul(acc));
}
return { value: acc.max(0) };
},
},
{
id: "color.hsvAdjust",
label: "Hue / saturation / value",
description: "Turns the hue, scales saturation and brightness",
category: "color",
inputs: {
rgb: { type: "color", default: [1, 1, 1] },
hue: { type: "float", default: 0, min: -0.5, max: 0.5, label: "hue (turns)" },
saturation: { type: "float", default: 1, min: 0, max: 3 },
value: { type: "float", default: 1, min: 0, max: 4 },
},
outputs: { value: "color" },
source: THREE_COLOR,
build: ({ rgb, hue: turns, saturation: s, value }) => ({ value: saturation(hue(rgb, turns.mul(2 * Math.PI)), s).mul(value) }),
},
{
id: "color.cosinePalette",
label: "Cosine palette",
description: "A smooth color ramp from a number (Inigo Quilez)",
category: "color",
inputs: {
t: { type: "float", default: 0, min: 0, max: 1 },
a: { type: "color", default: [0.5, 0.5, 0.5] },
b: { type: "color", default: [0.5, 0.5, 0.5] },
c: { type: "color", default: [1, 1, 1] },
d: { type: "color", default: [0, 0.33, 0.67] },
},
outputs: { value: "color" },
source: IQ_PALETTE,
build: ({ t, a, b, c, d }) => ({ value: cosinePalette(t, a, b, c, d) }),
},
{
id: "color.tint",
label: "Tint",
description: "Blends a color toward another (intensity above 1 glows)",
category: "color",
inputs: {
rgb: { type: "color", default: [1, 1, 1] },
color: { type: "color", default: [1, 0.5, 0.2] },
intensity: { type: "float", default: 1, min: 0, max: 8 },
amount: { type: "float", default: 1, min: 0, max: 1 },
},
// mix(rgb, color * intensity, amount)
outputs: { value: "color" },
build: ({ rgb, color, intensity, amount }) => ({ value: mix(rgb, color.mul(intensity), amount) }),
},
{
id: "color.blend",
label: "Blend",
description: "Combines two colors like a paint layer (multiply, add, screen, …), faded in by amount",
category: "color",
inputs: {
base: { type: "color", default: [1, 1, 1] },
color: { type: "color", default: [1, 1, 1] },
amount: { type: "float", default: 1, min: 0, max: 1 },
},
options: { mode: { default: "multiply", choices: ["multiply", "add", "screen", "darken", "lighten", "replace"] } },
outputs: { value: "color" },
build: ({ base, color, amount }, ctx) => {
const mode = ctx.option("mode");
const layer = mode === "add" ? base.add(color)
: mode === "screen" ? vec3(1).sub(vec3(1).sub(base).mul(vec3(1).sub(color)))
: mode === "darken" ? min(base, color)
: mode === "lighten" ? max(base, color)
: mode === "replace" ? color
: base.mul(color);
return { value: mix(base, layer, amount) };
},
},
];