ousia-sketch-v1 // Ousia sketch - ON-CHAIN build (upload to ScriptyStorage as "ousia-sketch-v1").
// currentID, hexData, canvasSize, showPunk, showSphere are INJECTED by the OusiaRenderer
// contract in a preceding <script>, so they must NOT be declared here: two top-level
// 'let' of the same name across <script> tags is a SyntaxError that breaks the animation.
// The standalone/test version (with those vars set to a sample punk) is in sketch.js.
let gaskets = [];
let spheres = [];
let baseSize = 800;
let scaleFactor;
let minRadius;
let seed
const epsilon = 0.1;
let index = 0;
let gasketBuffer;
let punkBuffer;
let punkImageData;
p5.RendererGL.prototype._drawPoints = function(points, buffer) {
if (!points.length || !this.curStrokeColor) {
return;
}
const gl = this.GL;
const shader = this._getImmediatePointShader();
this._setPointUniforms(shader);
this._bindBuffer(buffer, gl.ARRAY_BUFFER, this._vToNArray(points), Float32Array, gl.STATIC_DRAW);
shader.enableAttrib(shader.attributes.aPosition, 3);
this._applyColorBlend(this.curStrokeColor);
gl.drawArrays(gl.POINTS, 0, points.length);
shader.unbindShader();
};
function setup() {
scaleFactor = canvasSize / baseSize;
createCanvas(canvasSize, canvasSize, WEBGL);
pixelDensity(1);
angleMode(DEGREES);
ortho();
let pxNumber = pData.numPixels;
let colNumber = pData.numColors;
minRadius = getMinRadius(pxNumber, scaleFactor);
punkImageData = hexStringToUint8Array(hexData);
punkBuffer = createGraphics(canvasSize, canvasSize, WEBGL);
punkBuffer.ortho();
punkBuffer.angleMode(DEGREES);
drawPunkToBuffer(punkBuffer);
seed = currentID * colNumber * pxNumber;
generateGaskets(seed, minRadius, pxNumber, canvasSize)
assignColorsToCircles(gaskets, pData.colors);
createSpheresFromCircles(gaskets, seed, scaleFactor);
gasketBuffer = createGraphics(canvasSize, canvasSize, WEBGL);
gasketBuffer.ortho();
gasketBuffer.angleMode(DEGREES);
gasketBuffer.blendMode(ADD);
for (let gasket of gaskets) {
gasket.show(gasketBuffer);
}
}
function draw() {
background(0);
if (showPunk){
image(punkBuffer, -width/2, -height/2);
}
if (showSphere) {
blendMode(ADD);
image(gasketBuffer, -width/2, -height/2);
blendMode(BLEND);
}
for (let sphere of spheres) {
sphere.show();
}
}
function drawPunkToBuffer(pg) {
let imageData = punkImageData;
let originalSize = 24;
let basePixelSize = baseSize / originalSize;
let reductionFactor = 6;
let pixelSize = (basePixelSize * scaleFactor) / reductionFactor;
let offsetX = -pg.width / 2;
let offsetY = pg.height / 2 - (pixelSize * originalSize) - 10;
pg.noStroke();
for (let y = 0; y < originalSize; y++) {
for (let x = 0; x < originalSize; x++) {
let i = (y * originalSize + x) * 4;
if (i + 3 < imageData.length) {
let r = imageData[i];
let g = imageData[i + 1];
let b = imageData[i + 2];
let a = imageData[i + 3];
if (r === 0 && g === 0 && b === 0 && a === 0) {
continue;
}
pg.fill(r, g, b);
pg.rect(x * pixelSize + offsetX, y * pixelSize + offsetY, pixelSize, pixelSize);
}
}
}
}
//======================Circles==============================
class Circle {
constructor(bend, x, y, index) {
this.center = new Complex(x, y);
this.bend = bend;
this.radius = abs(1 / this.bend);
this.index = index;
this.color = color(0);
}
show(pg) {
pg.noStroke();
let r = pg.red(this.color);
let g = pg.green(this.color);
let b = pg.blue(this.color);
for (let i = 0.0; i < 1.0; i += 0.05) {
pg.fill(r, g, b, (1.2 - i) * 100 * i);
pg.circle(this.center.a, this.center.b, this.radius * 1.2 * i);
}
}
dist(other) {
return dist(this.center.a, this.center.b, other.center.a, other.center.b);
}
setColor(newColor) {
this.color = newColor;
}
}
//======================EndCircles===========================
//======================Spheres==============================
class Sphere {
constructor(a, b, radius, col, seed, scaleFactor) {
this.a = a;
this.b = b;
this.r = radius;
this.color = col;
this.seed = seed;
this.scaleFactor = scaleFactor;
this.rot = randomFromSeed(seed, 0, 360);
this.rotX = randomFromSeed(seed + 1, 1, 10);
let NSpossibleValues = [8, 10, 12, 14];
this.NSdotAmount =
NSpossibleValues[
Math.floor(randomFromSeed(seed + 2, 0, NSpossibleValues.length))
];
this.NSlinesAmount = Math.floor(randomFromSeed(seed + 3, 4, 10));
this.SSdotsAmount = Math.floor(randomFromSeed(seed + 4, 3, 5));
this.SSthetaFactor = Math.floor(randomFromSeed(seed + 5, 6, 8));
this.sphereKey = Math.floor(randomFromSeed(seed + 6, 0, 2));
this.angleAmplitude = Math.floor(randomFromSeed(seed + 7, 0, 360));
}
show() {
stroke(this.color);
this.strokeSphere();
push();
translate(this.a, this.b);
rotate(this.rot);
rotateX(millis() / 50 * this.rotX);
if (this.sphereKey == 0) {
this.showNormalSphere();
} else {
this.showSphericalSphere();
}
}
showSphericalSphere() {
beginShape(POINTS);
for (let theta = 0; theta < 360 / 2; theta += this.SSdotsAmount) {
let x = this.r * cos(theta);
let y = this.r * sin(theta) * sin(theta * this.SSthetaFactor);
let z = this.r * sin(theta) * cos(theta * this.SSthetaFactor);
vertex(x, y, z);
}
endShape();
pop();
}
showNormalSphere() {
beginShape(POINTS);
for (let phi = 0; phi < 190; phi += 190 / this.NSdotAmount) {
let angleOffset = map(phi, 0, 190, 0, this.angleAmplitude);
for (let theta = 0; theta < 360; theta += 360 / this.NSlinesAmount) {
let x = this.r * cos(phi);
let y = this.r * sin(phi) * sin(theta + angleOffset);
let z = this.r * sin(phi) * cos(theta + angleOffset);
vertex(x, y, z);
}
}
endShape();
pop();
}
strokeSphere() {
let weight;
if (this.r > 150) {
weight = 6;
} else if (this.r > 100) {
weight = 5;
} else if (this.r > 50) {
weight = 4;
} else if (this.r > 25) {
weight = 3;
} else {
weight = 2;
}
strokeWeight(constrain(weight * this.scaleFactor, 0.5, 10));
}
}
//======================EndSpheres===========================
//======================Functions============================
function randomFromSeed(seed, min, max) {
let x = Math.sin(seed) * 10000;
return min + (x - Math.floor(x)) * (max - min);
}
function getMinRadius(pxNumber, scaleFactor) {
if (pxNumber >= 250) {
return 8 * scaleFactor;
} else if (pxNumber >= 170 && pxNumber < 250) {
return 10 * scaleFactor;
} else if (pxNumber <= 169) {
return 12 * scaleFactor;
}
}
function getTotalCircles() {
return gaskets
.map((gasket) => gasket.allCircles.length)
.reduce((sum, count) => sum + count, 0);
}
function removeMinCircles(gaskets, count) {
let allCirclesWithInfo = gaskets.flatMap((gasket) =>
gasket.getCirclesWithInfo()
);
allCirclesWithInfo.sort((a, b) => a.circle.radius - b.circle.radius);
let allCirclesToRemove = [];
for (let i = 0; i < count; i++) {
let { index, gasket } = allCirclesWithInfo[i];
allCirclesToRemove.push({ index: index, gasket: gasket });
}
allCirclesToRemove.sort((a, b) => b.index - a.index);
allCirclesToRemove.forEach(({ index, gasket }) => {
gasket.removeCircle(index);
});
}
function createSpheresFromCircles(gaskets, baseSeed, scaleFactor) {
let counter = 0;
for (let gasket of gaskets) {
for (let circle of gasket.allCircles) {
let seed = baseSeed + counter;
let sphere = new Sphere(circle.center.a, circle.center.b, circle.radius, circle.color, seed, scaleFactor);
spheres.push(sphere);
counter++;
}
}
}
//======================EndFunctions=========================
//======================Functions2===========================
function hexStringToUint8Array(hex) {
if (hex.startsWith('0x')) {
hex = hex.slice(2);
}
let array = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
array[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return array;
}
let pData = extractData(hexData);
function assignColorsToCircles(gaskets, colors) {
let rgbColors = colors.filter(color => color !== '0,0,0').map(color => `rgb(${color})`);
let nonEmptyGaskets = gaskets.filter(gasket => gasket.allCircles.length > 0);
let colorIndex = 0;
for (let gasket of nonEmptyGaskets) {
let assignedColor = rgbColors[colorIndex % rgbColors.length];
for (let circle of gasket.allCircles) {
circle.setColor(assignedColor);
}
colorIndex++;
}
}
function extractData(hexData) {
let imageData = hexStringToUint8Array(hexData);
let colorCount = {};
let totalPixels = 0;
let originalSize = 24;
for (let y = 0; y < originalSize; y++) {
for (let x = 0; x < originalSize; x++) {
let i = (y * originalSize + x) * 4;
if (i + 3 < imageData.length) {
let r = imageData[i];
let g = imageData[i + 1];
let b = imageData[i + 2];
let a = imageData[i + 3];
if (r === 0 && g === 0 && b === 0 && a === 0) {
continue;
}
let colorKey = `${r},${g},${b}`;
if (colorCount[colorKey] === undefined) {
colorCount[colorKey] = 0;
}
colorCount[colorKey]++;
totalPixels++;
}
}
}
let sortedColors = Object.keys(colorCount).sort((a, b) => colorCount[b] - colorCount[a]);
return {
numColors: sortedColors.length,
numPixels: totalPixels,
colors: sortedColors
};
}
//======================EndFunctions2========================
//======================Gaskets==============================
function generateGaskets(seed, minRadius, pxNumber, canvasSize){
gaskets.push(new Gasket(seed, minRadius, 0, 0, canvasSize / 2));
while (getTotalCircles() < pxNumber) {
let allMaxCircles = [];
for (let gasket of gaskets) {
let circle = gasket.getMaxCircle();
allMaxCircles.push(circle.radius);
}
let maxCircle = Math.max(...allMaxCircles);
let maxCircleGasketIndex = allMaxCircles.indexOf(maxCircle);
let nextGP = gaskets[maxCircleGasketIndex].getMaxCircle();
let newGasket = new Gasket(
seed + gaskets.length,
minRadius,
nextGP.center.a,
nextGP.center.b,
nextGP.radius
);
gaskets.push(newGasket);
gaskets[maxCircleGasketIndex].allCircles.splice(
gaskets[maxCircleGasketIndex].maxIndex,
1
);
if (getTotalCircles() > pxNumber) {
let circlesToRemove = getTotalCircles() - pxNumber;
removeMinCircles(gaskets, circlesToRemove);
}
}
}
class Gasket {
constructor(seed, minRadius, x, y, r) {
this.allCircles = [];
this.queue = [];
this.seed = seed;
this.minRadius = minRadius;
let c1 = new Circle(-1 / r, x, y, index);
let r2 = randomFromSeed(seed, c1.radius / 8, c1.radius / 2);
let angle = randomFromSeed(seed + 1, 0, 360);
let vX = (c1.radius - r2) * cos(angle);
let vY = (c1.radius - r2) * sin(angle);
let c2 = new Circle(1 / r2, x + vX, y + vY, index);
let r3 = sqrt(vX * vX + vY * vY);
angle += 180;
vX = (c1.radius - r3) * cos(angle);
vY = (c1.radius - r3) * sin(angle);
let c3 = new Circle(1 / r3, x + vX, y + vY, index);
this.allCircles = [c1, c2, c3];
this.queue = [[c1, c2, c3]];
let len = -1;
while (this.allCircles.length !== len) {
len = this.allCircles.length;
this.nextGeneration();
}
this.allCircles.shift();
}
getMaxCircle() {
this.radii = this.allCircles.map((circle) => circle.radius);
this.maxRadius = Math.max(...this.radii);
this.maxIndex = this.radii.indexOf(this.maxRadius);
return this.allCircles[this.maxIndex];
}
getCirclesWithInfo() {
return this.allCircles.map((circle, index) => ({
circle,
index,
gasket: this,
}));
}
removeCircle(index) {
this.allCircles.splice(index, 1);
}
nextGeneration() {
let nextQueue = [];
for (let triplet of this.queue) {
let [c1, c2, c3] = triplet;
let k4 = descartes(c1, c2, c3);
let newCircles = complexDescartes(c1, c2, c3, k4);
for (let newCircle of newCircles) {
if (validate(newCircle, c1, c2, c3, this.allCircles, this.minRadius)) {
this.allCircles.push(newCircle);
let t1 = [c1, c2, newCircle];
let t2 = [c1, c3, newCircle];
let t3 = [c2, c3, newCircle];
nextQueue = nextQueue.concat([t1, t2, t3]);
}
}
}
this.queue = nextQueue;
}
show(pg) {
for (let i = 0; i < this.allCircles.length; i++) {
this.allCircles[i].show(pg);
}
}
}
function isTangent(c1, c2) {
let d = c1.dist(c2);
let r1 = c1.radius;
let r2 = c2.radius;
let a = abs(d - (r1 + r2)) < epsilon;
let b = abs(d - abs(r2 - r1)) < epsilon;
return a || b;
}
function validate(c4, c1, c2, c3, allCircles, minRadius) {
if (c4.radius < minRadius) return false;
for (let other of allCircles) {
let d = c4.dist(other);
let radiusDiff = abs(c4.radius - other.radius);
if (d < epsilon && radiusDiff < epsilon) {
return false;
}
}
if (!isTangent(c4, c1)) return false;
if (!isTangent(c4, c2)) return false;
if (!isTangent(c4, c3)) return false;
return true;
}
function complexDescartes(c1, c2, c3, k4) {
let k1 = c1.bend;
let k2 = c2.bend;
let k3 = c3.bend;
let z1 = c1.center;
let z2 = c2.center;
let z3 = c3.center;
let zk1 = z1.scale(k1);
let zk2 = z2.scale(k2);
let zk3 = z3.scale(k3);
let sum = zk1.add(zk2).add(zk3);
let root = zk1.mult(zk2).add(zk2.mult(zk3)).add(zk1.mult(zk3));
root = root.sqrt().scale(2);
let center1 = sum.add(root).scale(1 / k4[0]);
let center2 = sum.sub(root).scale(1 / k4[0]);
let center3 = sum.add(root).scale(1 / k4[1]);
let center4 = sum.sub(root).scale(1 / k4[1]);
return [
new Circle(k4[0], center1.a, center1.b),
new Circle(k4[0], center2.a, center2.b),
new Circle(k4[1], center3.a, center3.b),
new Circle(k4[1], center4.a, center4.b),
];
}
function descartes(c1, c2, c3) {
let k1 = c1.bend;
let k2 = c2.bend;
let k3 = c3.bend;
let sum = k1 + k2 + k3;
let product = abs(k1 * k2 + k2 * k3 + k1 * k3);
let root = 2 * sqrt(product);
return [sum + root, sum - root];
}
//============================EndGaskets==================================
//============================Complex=====================================
class Complex {
constructor(a, b) {
this.a = a;
this.b = b;
}
add(other) {
return new Complex(this.a + other.a, this.b + other.b);
}
sub(other) {
return new Complex(this.a - other.a, this.b - other.b);
}
scale(value) {
return new Complex(this.a * value, this.b * value);
}
mult(other) {
let a = this.a * other.a - this.b * other.b;
let b = this.a * other.b + other.a * this.b;
return new Complex(a, b);
}
sqrt() {
let m = sqrt(this.a * this.a + this.b * this.b);
let angle = atan2(this.b, this.a);
m = sqrt(m);
angle = angle / 2;
return new Complex(m * cos(angle), m * sin(angle));
}
}
//===========================EndComplex=====================================
↓ show full (16,032 bytes)