Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Last active March 16, 2023 16:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattdesl/b118e3bf379289c6d2095e4d40169f4b to your computer and use it in GitHub Desktop.
Save mattdesl/b118e3bf379289c6d2095e4d40169f4b to your computer and use it in GitHub Desktop.
// Using mixbox latent vectors without
// actually pulling mixbox dependency into the code
// Useful in two ways:
// 1. To bring file size down (e.g. on-chain art)
// 2. (Legal gray area) To potentially create something that is not restricted by mixbox license. Not clear yet whether this would circumvent the need for a license.
// EDIT: After discussion with them, this would indeed invalidate their license and be considered infringement. Code exists for educational purpose only.
// note this file includes some licensed code (evalPolynomial) from mixbox's source
// so I have not put a license on this
const canvasSketch = require("canvas-sketch");
const Color = require("canvas-sketch-util/color");
const { clamp01, lerpArray } = require("canvas-sketch-util/math");
const distanceSq = require("euclidean-distance/squared");
const settings = {
dimensions: [1024, 1024],
};
// Predefined pigments for background, paint 0 and paint 1
const background = {
rgb: [242, 242, 242],
latent: [
0.02519031141868511, 0.01600075350311099, 0.013111126543025103,
0.9456978085351788, 0.001795255083438474, -0.00011933726800561484,
0.00006372186977998684,
],
};
const colorA = {
rgb: [0, 33, 133],
latent: [
0.8644521337946944, 0.004521337946943488, 0.029534703846936693,
0.10149182441142535, -0.05364788273574864, -0.012035752512736503,
0.0034582075508062804,
],
};
const colorB = {
rgb: [252, 211, 0],
latent: [
0.00392156862745098, 0.9343412714566796, 0.00392156862745098,
0.05781559128841851, 0.033365873962807324, 0.02504377559021409,
-0.057261741984619266,
],
};
const palette = [background, colorA, colorB];
const sketch = ({ canvasWidth, canvasHeight, scaleX, scaleY }) => {
const layers = [colorA, colorB].map((color) => {
const canvas0 = document.createElement("canvas");
const context0 = canvas0.getContext("2d");
canvas0.width = canvasWidth;
canvas0.height = canvasHeight;
return {
canvas: canvas0,
context: context0,
color,
};
});
return (props) => {
const { context, width, height } = props;
context.globalCompositeOperation = "source-over";
context.fillStyle = Color.parse(background.rgb).hex;
context.fillRect(0, 0, width, height);
const IS_MIX = true;
const paintAlpha = 0.75;
if (IS_MIX) {
const canvasWidth = props.canvasWidth;
const canvasHeight = props.canvasHeight;
const composite = context.getImageData(0, 0, canvasWidth, canvasHeight);
layers.forEach((layer, i) => {
layer.context.save();
layer.context.scale(scaleX, scaleY);
layer.context.clearRect(0, 0, width, height);
layer.context.globalCompositeOperation = "source-over";
drawLayer(
{
...props,
context: layer.context,
canvas: layer.canvas,
},
i,
"black"
);
layer.imageData = layer.context.getImageData(
0,
0,
canvasWidth,
canvasHeight
);
// blend each
for (let y = 0; y < canvasHeight; y++) {
for (let x = 0; x < canvasWidth; x++) {
const idx = x + y * canvasWidth;
// get current color in buffer
let srcRGB = rgbaAt(composite.data, idx);
// get latent for image from palette
const latent0 =
palette[
nearestIdx(
srcRGB.slice(0, 3),
palette.map((x) => x.rgb)
)
].latent;
// latent for this layer
const latent1 = layer.color.latent;
// use the drawing as an alpha mask
const alpha = rgbaAt(layer.imageData.data, idx)[3] / 0xff;
// KM mix
const newLatent = lerpArray(latent0, latent1, alpha * paintAlpha);
const newRGB = latentToRgb(newLatent);
// const newRGB = mixbox.lerp(srcRGB, top, alpha * 0.75);
composite.data[idx * 4 + 0] = newRGB[0];
composite.data[idx * 4 + 1] = newRGB[1];
composite.data[idx * 4 + 2] = newRGB[2];
composite.data[idx * 4 + 3] = 0xff;
}
}
layer.context.restore();
});
context.putImageData(composite, 0, 0);
} else {
drawLayer(props, 0, Color.parse(colorA.rgb).hex);
context.globalCompositeOperation = "multiply";
drawLayer(props, 1, Color.parse(colorB.rgb).hex);
context.globalCompositeOperation = "source-over";
}
};
function rgbaAt(pixels, i) {
return [
pixels[i * 4 + 0],
pixels[i * 4 + 1],
pixels[i * 4 + 2],
pixels[i * 4 + 3],
];
}
function drawLayer(props, i = 0, color) {
const { context, width, height } = props;
context.lineCap = "round";
const pad = width * 0.2;
context.lineWidth = pad;
context.strokeStyle = color;
if (i == 0) {
context.beginPath();
context.lineTo(pad, pad);
context.lineTo(width - pad, height - pad);
context.stroke();
} else {
context.beginPath();
context.lineTo(width - pad, pad);
context.lineTo(pad, height - pad);
context.stroke();
}
}
};
canvasSketch(sketch, settings);
function nearestIdx(color, palette) {
let dist = Infinity;
let idx = -1;
for (let i = 0; i < palette.length; i++) {
const b = palette[i];
const dstSq = distanceSq(color, b);
if (dstSq < dist) {
dist = dstSq;
idx = i;
}
}
return idx;
}
// prettier-ignore
function evalPolynomial(c0, c1, c2, c3) {
var r = 0.0;
var g = 0.0;
var b = 0.0;
var c00 = c0 * c0;
var c11 = c1 * c1;
var c22 = c2 * c2;
var c33 = c3 * c3;
var c01 = c0 * c1;
var c02 = c0 * c2;
var c12 = c1 * c2;
var w = 0.0;
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;
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;
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;
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;
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];
}
function latentToRgb(latent) {
var rgb = evalPolynomial(latent[0], latent[1], latent[2], latent[3]);
return [
(clamp01(rgb[0] + latent[4]) * 255.0 + 0.5) | 0,
(clamp01(rgb[1] + latent[5]) * 255.0 + 0.5) | 0,
(clamp01(rgb[2] + latent[6]) * 255.0 + 0.5) | 0,
];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment