Last active
March 16, 2023 16:32
-
-
Save mattdesl/b118e3bf379289c6d2095e4d40169f4b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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