Syncret ```
"the dream spins outward / slipping from our clenched hands";
let
canvas, // p5js canvas.
state; // Keep all shared state in one place.
const
body = document.body,
halfRoot3 = p.sqrt(.75),
//-[ Geometry functions ]---------------------------------------------------//
// v - point or vector - [x, y]
// l - line - [a, b, c] where ax + by = c
// Returns the line passing through points v1 and v2.
line = (v1, v2) => {
let
a = v2[1] - v1[1],
b = v1[0] - v2[0];
return [a, b, a * v1[0] + b * v1[1]];
},
// Adds v2 to v1 and returns the result.
// Modifies v1 in place for performance reasons.
add = (v1, v2) => (v1[0] += v2[0], v1[1] += v2[1], v1),
// Returns the difference between v1 and v2.
subtract = (v1, v2) => [v1[0] - v2[0], v1[1] - v2[1]],
// Creates a unit vector with angle a.
unit = a => [p.cos(a), p.sin(a)],
// Scales v by s.
// Modifies the vector in place for performance reasons.
scale = (v, s) => (v[0] *= s, v[1] *= s, v),
// Returns the length of v.
length = v => p.sqrt(v[0] * v[0] + v[1] * v[1]),
// Returns the unit vector from v1 toward v2.
direction = (v1, v2) => {
let v3 = subtract(v2, v1);
return scale(v3, 1 / length(v3));
},
// Returns the squared distance between v1 and v2.
distSquared = (v1, v2) => {
let [x, y] = subtract(v1, v2);
return x * x + y * y;
},
// Returns the line perpendicular to l that passes through point v.
perpendicular = (v, l) => [-l[1], l[0], -l[1] * v[0] + l[0] * v[1]],
// Returns the midpoint between v1 and v2.
midpoint = (v1, v2) => [(v1[0] + v2[0]) / 2, (v1[1] + v2[1]) / 2],
// Returns the line that bisects the segment between v1 and v2.
bisect = (v1, v2) => perpendicular(midpoint(v1, v2), line(v1, v2)),
// Returns the point where l1 and l2 intersect.
intersect = (l1, l2) => {
let det = l1[0] * l2[1] - l2[0] * l1[1];
// Ignore parallel case where determinate == 0.
return [
(l2[1] * l1[2] - l1[1] * l2[2]) / det,
(l1[0] * l2[2] - l2[0] * l1[2]) / det
];
},
// Returns the circumscribed circle for up to 3 given points.
// The return value is in the format [x, y, r^2].
circumscribe = vs => {
if (!vs.length) return [0, 0, 0];
if (vs.length == 1) return [vs[0][0], vs[0][1], 0];
if (vs.length == 2) {
return [...midpoint(...vs), distSquared(vs[0], vs[1]) / 4];
}
let center = intersect(bisect(vs[0], vs[1]), bisect(vs[1], vs[2]));
return [...center, distSquared(center, vs[0])];
},
// Returns the minimal enclosing circle for the given points.
// The return value is in the format [x, y, r^2].
enclose = (vs, pBounds) => {
if (pBounds.length == 3 || !vs.length) return circumscribe(pBounds);
let
tailCircle = enclose(vs.slice(1), pBounds),
v = vs[0],
inside = distSquared(tailCircle, v) < tailCircle[2];
return inside || pBounds.some(vB => vB[0] == v[0] && vB[1] == v[1]) ?
tailCircle : enclose(vs.slice(1), [v, ...pBounds]);
},
// Returns a point on a Bezier curve in one dimension.
bezier = (v0, v1, v2, v3, t) => {
let t2 = t * t;
let t3 = t2 * t;
return v0 * (-t3 + 3 * t2 - 3 * t + 1) +
v1 * (3 * t3 - 6 * t2 + 3 * t) +
v2 * 3 * (t2 - t3) +
v3 * t3;
},
// Returns a point on a Bezier curve in three dimensions.
bezier3d = (p0, p1, p2, p3, t) => [
bezier(p0[0], p1[0], p2[0], p3[0], t),
bezier(p0[1], p1[1], p2[1], p3[1], t),
bezier(p0[2], p1[2], p2[2], p3[2], t)
],
//-[ Randomness functions ]-------------------------------------------------//
gaussian = (sd = 1) => p.randomGaussian(0, sd),
agaussian = (sd = 1) => p.abs(gaussian(sd)),
randomBool = (t = .5) => p.random() < t,
// Allows the provided value to be glitched by using the replace function.
glitch = (name, value, replace, post) => {
if (!randomBool(state.glitch)) return value;
let replacement = replace();
if (replacement != value) {
state.glitches[name] = true; // Record the glitch.
post && post(); // Execute the post function if provided.
}
return replacement;
},
//-[ Shape functions ]------------------------------------------------------//
// Creates an array with length n. Lambda l is used to populate the values.
array = (n, l) => Array.from(Array(n), (_, i) => l(i)),
// Creates a rounded hexagon as a list of vertices.
hexagon = (node, roundRadius = .25, roundPoints = 5) => {
let [x, y, r, a] = node;
// Given max radius, adjust to min radius.
r *= 1 - roundRadius * (1 - halfRoot3);
return array(6, i => {
let vertexAngle = a + p.PI / 3 * i;
return array(roundPoints, j => {
let roundAngle = vertexAngle + p.PI / 3 * j / (roundPoints - 1);
return [
x + r * (1 - roundRadius) * p.cos(vertexAngle + p.PI / 6) +
r * roundRadius * p.cos(roundAngle),
y + r * (1 - roundRadius) * p.sin(vertexAngle + p.PI / 6) +
r * roundRadius * p.sin(roundAngle)
];
})
}).flat(); // Join the groups of points from each corner into one big array.
},
// Creates a circle as a 30-sided polygon.
circle = (node, points = 30) => array(points, i => [
node[0] + node[2] * p.cos(node[3] + p.PI / 6 + i / points * p.TWO_PI),
node[1] + node[2] * p.sin(node[3] + p.PI / 6 + i / points * p.TWO_PI)
]),
// Returns a copy of a node with the values slightly smeared.
smear = (node, distance) => {
let [x, y, r, a] = node;
return [
x + gaussian(distance / 2),
y + gaussian(distance / 2),
r + agaussian(distance),
a + gaussian(distance / r)
];
},
//-[ Tracing functions ]----------------------------------------------------//
// Reset the mutable parts of the global state used for tracing.
resetTraceState = _ => {
state.penPosition = state.center;
state.penVelocity = [0, 0];
state.wristPosition = [state.center[0], state.center[1] + state.handLength];
state.wristVelocity = [0, 0];
},
// Moves the pen one step toward the target point.
moveToward = point => {
// Figure out the wrist position and angle, compared to the ideals.
let
hand = subtract(state.penPosition, state.wristPosition),
targetHand = subtract(point, state.wristPosition),
diffRatio = (hand[0] * targetHand[0] + hand[1] * targetHand[1]) /
(length(hand) * length(targetHand)),
// Calculate the difference in angle between hand and targetHand.
angleDiff = p.acos(diffRatio);
// Every now and then, diffRatio likes to get too big and causes NaN values.
// When this happens, fall back to clamp values.
if (Number.isNaN(angleDiff)) angleDiff = (diffRatio < 0) * p.PI;
// Account for wrist rotation in the direction the pen tries to move.
let
path = subtract(point, state.penPosition),
pathAngle = p.atan2(path[1], path[0]),
// Deflect movement angle to simulate rotation at wrist.
moveAngle =
pathAngle - Math.sign(path[0]) * angleDiff * state.penDeflection / 2;
// Update the pen and wrist velocities.
state.penVelocity = add(
scale([...state.penVelocity], state.penSmoothing),
scale(unit(moveAngle), state.penStep * (1 - state.penSmoothing))
);
state.wristVelocity = add(
scale([...state.wristVelocity], state.wristSmoothing),
scale(
direction(state.wristPosition, [point[0], point[1] + state.handLength]),
state.wristStep * (1 - state.wristSmoothing)
)
);
// Update the pen and wrist positions.
add(state.wristPosition, state.wristVelocity);
state.penPosition = add([...state.penPosition], state.penVelocity);
return state.penPosition;
},
// Returns a point that is "nearly" the provided point.
nearly = point => [
point[0] + gaussian(state.penStep / 2),
point[1] + gaussian(state.penStep / 2)
],
// Traces the given polygon and returns the result.
tracePolygon = poly => {
let
points = [],
vertexIndex = 1,
vertex = nearly(poly[0]);
// Move to the first point in the polygon.
while (distSquared(state.penPosition, vertex) > state.thresholdSquared) {
moveToward(vertex);
}
// Trace to every successive point.
for (; vertexIndex < poly.length; vertexIndex++) {
vertex = nearly(poly[vertexIndex]);
let i = 0;
while (distSquared(state.penPosition, vertex) > state.thresholdSquared) {
points.push(moveToward(vertex));
}
}
// Wrap back around to the first point.
vertex = nearly(poly[0]);
while (distSquared(state.penPosition, vertex) > state.thresholdSquared) {
points.push(moveToward(vertex));
}
return points;
},
//-[ Drawing helper functions ]---------------------------------------------//
// Converts a polygon into a p5js shape instructions.
drawPolygon = (instance, polygon) => {
if (!polygon.length) return;
instance.beginShape();
polygon.map(vertex => instance.vertex(...vertex.slice(0, 2)));
instance.endShape();
},
// Converts Cartesian coordinates into canvas-relative polar coordinates.
// Used for computing colors and other styling information.
ra = v => {
const [x, y] = subtract(v, state.center);
return [
4.84 * (x * x + y * y) / (state.canvasSize * state.canvasSize),
p.atan2(y, x) - state.styleAngle
];
},
// Bypasses p5js to create a linear gradient fill.
fillLinear = (instance, x1, y1, x2, y2, c1, c2) => {
const gradient =
instance.drawingContext.createLinearGradient(x1, y1, x2, y2);
gradient.addColorStop(0, instance.color(c1).toString());
gradient.addColorStop(1, instance.color(c2).toString());
instance.fill("#DEADC0DE"); // Clear the cached fill value.
instance.drawingContext.fillStyle = gradient;
},
// Bypasses p5js to create a radial gradient fill.
fillRadial = (instance, x, y, radius, inner, outter) => {
const gradient =
instance.drawingContext.createRadialGradient(x, y, 0, x, y, radius);
gradient.addColorStop(0, instance.color(inner).toString());
gradient.addColorStop(1, instance.color(outter).toString());
instance.fill("#DEADC0DE"); // Clear the cached fill value.
instance.drawingContext.fillStyle = gradient;
},
//-[ Drawing function for "Nexus: Supreme" ]--------------------------------//
supreme = texture => {
// First draw the subtle background gradient.
p.background("#C5C5C2");
texture.clear();
p.noStroke();
texture.noStroke();
p.colorMode(p.RGB);
texture.colorMode(p.RGB);
fillRadial(
texture, ...state.center, state.canvasSize / 2, "#CACAC8", "#CECECC");
texture.circle(...state.center, state.canvasSize);
// Roll style-specific random values.
const
uniform = randomBool(.4),
// Precompute values for nodes with Gaussian distrobution.
cachedGaussians = state.nodes.map(node => gaussian()),
// Fetch a random value for a node.
// Based on the value of `uniform` value, this may or may not be the
// precomputed value from `cachedGaussians`.
nodeGaussian = uniform ? i => cachedGaussians[i] :
i => randomBool() ? gaussian() : cachedGaussians[i],
// 1 in 4 chance of highlighting the largest node in red.
popId = randomBool(.75) ? -1 :
state.nodes.reduce(
(a, node, i) => a[0] > node[2] ? a : [node[2], i], [0]
)[1],
// Precompute the node colors.
sides = state.nodes.map((node, i) => {
const
[r, a] = ra(node),
offset = 1 - r * p.sin(a);
return [
(offset + .45 * nodeGaussian(i)) | 0,
(offset + .45 * nodeGaussian(i)) | 0,
(offset + .45 * nodeGaussian(i)) | 0,
(offset + .45 * nodeGaussian(i)) | 0
];
}),
c = ["#000000", "#222222", "#F2F2F2", "#FFFFFF"],
// Alpha value for background colors.
// Backgrounds only have transparency when glitched.
bg = glitch("Revealed", "", _ => "AA");
// Glitch the dark coloring.
c[1] = glitch(
"Enlightened",
c[1],
a => (
a = ["22"],
a.splice(p.random(2) | 0, 0, (324 + p.random(35) | 0).toString(16).substr(1)),
a.splice(p.random(3) | 0, 0, (290 + p.random(52) | 0).toString(16).substr(1)),
"#" + a.join("")
)
);
// Record style specific metadata.
state.majorMetadata.Emptiness = popId < 0 ? "Dispersed" : "Concentrated";
state.majorMetadata.Polarity = uniform ? "Quiescent" : "Kinetic";
// Finally, draw the background circles.
state.nodes.map((node, i) => {
p.fill(c[sides[i][0] ? 2 : 0] + bg);
p.circle(node[0], node[1], node[2] * 8);
const texturePoly =
tracePolygon(circle(
smear([node[0], node[1], node[2] * 4, node[3]], state.smear * 1.5)
));
texture.fill(c[sides[i][0] ? 3 : 1] + bg);
drawPolygon(texture, texturePoly);
if (uniform) return;
texture.fill(sides[i][1] ? c[3] + "22" : c[1] + "44");
drawPolygon(texture, texturePoly);
});
// Draw the foreground hexagons.
state.hexagons.map((hexagon, i) => {
if (popId == i) return;
p.fill(c[sides[i][2] ? 0 : 2]);
drawPolygon(p, hexagon);
});
state.textugons.map((textugon, i) => {
if (popId == i) return;
texture.fill(c[sides[i][2] ? 1 : 3]);
drawPolygon(texture, textugon);
if (uniform) return;
texture.fill(sides[i][3] ? c[1] + "88" : c[3] + "55");
drawPolygon(texture, textugon);
});
// Put the cherry on top.
if (popId >= 0) {
p.fill("#F22222");
drawPolygon(p, state.hexagons[popId]);
texture.fill("#F22");
drawPolygon(texture, state.textugons[popId]);
}
},
//-[ Drawing function for "Nexus: Celestial" ]------------------------------//
celestial = texture => {
// Set up colors.
let
skyColors = ["#182834", "#122632"],
bgColors = [
[85, 215, 255],
[85, 102, 215],
[85, 215, 255],
[102, 85, 215],
];
// Glitch the colors.
glitch("Radiant", true, _ => {
skyColors =
skyColors.map(color => "#" + color.substr(5) + color.substr(1, 4));
bgColors =
bgColors.map(color => [color[2], color[1] * 1.1, color[0]]);
});
// Make the Bezier functions for computing each node's colors.
const
radialVector = scale(unit(state.styleAngle), state.canvasSize / 2),
mainBez =
t => bezier3d(bgColors[0], bgColors[1], [34, 17, 85], [34, 34, 34], t),
textureBez =
t => bezier3d(bgColors[2], bgColors[3], [34, 17, 85], [34, 34, 34], t),
hexagonBez =
t => bezier3d(
[255, 238, 187],
[238, 221, 204],
[238, 238, 255],
[205, 215, 245],
1.6 * t - .3
);
// Now that colors are set up, draw the background.
p.clear();
p.noStroke();
p.colorMode(p.RGB);
fillLinear(
p,
...add(scale([...radialVector], .6), state.center),
...subtract(state.center, radialVector),
"#111",
skyColors[0]
);
p.circle(...state.center, state.canvasSize);
// The background on the texture overlay is made by adding two gradients.
texture.clear();
texture.noStroke();
texture.colorMode(p.RGB);
fillLinear(
texture,
...add([...radialVector], state.center),
...subtract(state.center, radialVector),
"#0E0E0E",
skyColors[1]
);
texture.circle(...state.center, state.canvasSize);
texture.blendMode(p.ADD);
fillRadial(
texture, ...state.center, state.canvasSize / 2, "#000", "#060609");
texture.circle(...state.center, state.canvasSize);
texture.blendMode(p.BLEND);
// Add stars to the background based on node positions.
const
allStars = glitch("Galactic", false, _ => true),
starThreshold = state.smear * 2,
stars = state.nodes.map(node => {
const
[r, a] = ra(node),
starCount = (allStars || r * p.cos(a)) * p.random(30),
nodeStars = [];
for (let i = 0; i++ < starCount;) {
const
offset = node[2] * (5 + 3.5 * gaussian()),
angle = a + p.PI * gaussian(),
diameter = p.random(p.random(node[2])) / 4;
if (diameter > starThreshold || randomBool(.7)) {
nodeStars.push([
node[0] + offset * p.sin(angle),
node[1] + offset * p.cos(angle),
p.max(diameter, starThreshold)
]);
}
}
return nodeStars;
}).flat();
p.fill("#F2F2F2");
texture.fill("#FFF");
stars.map(star => {
p.circle(...star);
texture.circle(...smear(star, state.smear / 2).slice(0, 3));
});
const
hexagonColors = [],
showHexagons = glitch("Revealed", true, _ => false);
// Draw the background circles and precompute node colors.
state.nodes.map(node => {
const
[r, a] = ra(node),
distance = .5 + r / 2 * p.cos(a),
alpha = 25 - 18 * distance + 15 * (!showHexagons) - 5 * allStars;
hexagonColors.push(hexagonBez(distance));
if (alpha < 0) return;
const
textureNode =
smear([node[0], node[1], node[2] * 4, node[3]], state.smear * 2),
hexColor = mainBez(distance),
texColor = textureBez(distance);
fillRadial(
p,
...node.slice(0, 2),
node[2] * 4,
p.color(...hexColor, alpha),
p.color(...hexColor, alpha * .6)
);
p.circle(node[0], node[1], node[2] * 8);
fillRadial(
texture,
...textureNode.slice(0, 2),
textureNode[2],
p.color(...texColor, alpha),
p.color(...texColor, alpha * .6)
);
drawPolygon(texture, tracePolygon(circle(textureNode)));
});
// Finally, draw foreground hexagons.
const alpha = 60 + 5640 / state.nodes.length;
state.hexagons.map((hexagon, i) => {
if (randomBool(.1) == showHexagons) return;
const shine = alpha + 75 * !showHexagons * p.random();
p.fill(...hexagonColors[i], shine);
drawPolygon(p, hexagon);
texture.fill(...hexagonColors[i], shine + 3);
drawPolygon(texture, state.textugons[i]);
});
},
//-[ Drawing function for "Nexus: Energy" ]---------------------------------//
// Energy stones and their associated color values.
stones = [
["Ruby", 0, 6, 4],
["Garnet", 15, 8, 2.5],
["Topaz", 27, 6, 5],
["Diamond", 42, .5, 5],
["Peridot", 72, 5, 4],
["Emerald", 105, 6, 3],
["Turquoise", 165, 6, 6],
["Aquamarine", 189, 6, 3],
["Sapphire", 234, 6, 4],
["Amethyst", 267, 6, 4],
["Pearl", 294, .75, 6.5],
["Tourmaline", 315, 5, 5]
],
energy = texture => {
// Start with a black background.
p.background(0);
p.noStroke();
p.colorMode(p.HSL);
texture.background(0);
texture.noStroke();
texture.colorMode(p.HSL);
// Pick a stone and compute the base alpha value.
let
hue, sat, brightness,
color = p.random(stones.length) | 0,
alpha = .04 + p.random(.04) + 4 / state.nodes.length;
state.majorMetadata.Stone = stones[color][0];
// Glitch the alpha value.
alpha =
glitch("Severe", alpha, _ => alpha > .2 ? alpha : .4 + p.random(.3));
const
// Sets color values based on the selected stone.
setValues = _ => {
hue = stones[color][1] + gaussian(2);
sat = stones[color][2] + .3;
brightness = stones[color][3];
},
// Roll style-specific random values.
purity = 7 + gaussian(2),
multiply = glitch("Sinister", false, _ => true) ?
state.nodes.map(node => randomBool(.7 + .25 * ra(node)[0])) :
state.nodes.map(node => randomBool(.25 + .25 * ra(node)[0])),
hueShifts = state.nodes.map(node => purity * gaussian());
setValues();
// Draw the backgound gradient.
[p, texture].map(instance => {
fillRadial(
instance,
...state.center,
state.canvasSize / 2,
instance.color(hue, sat * 15, brightness + 5),
instance.color(hue, 100, 2 + (instance == texture))
);
instance.circle(...state.center, state.canvasSize);
});
// Draw the background circles.
state.nodes.map((node, i) => {
const
blend = multiply[i] ? p.MULTIPLY : p.ADD,
scaled = [node[0], node[1], node[2] * 15, node[3]];
p.blendMode(blend);
texture.blendMode(blend);
p.fill(
multiply[i] ? p.color(0, alpha * 2.5) :
p.color(hue + hueShifts[i], sat * 8, brightness * 2, alpha * 2)
);
texture.fill(multiply[i] ? p.color(0, alpha * 2.4) :
p.color(hue + hueShifts[i], sat * 7.5, brightness * 1.9, alpha * 2));
p.circle(...scaled.slice(0, 2), scaled[2] * 2);
drawPolygon(
texture, tracePolygon(circle(smear(scaled, state.smear * 3))));
});
// Draw the background hexagons.
state.nodes.map((node, i) => {
const
blend = multiply[i] ? p.MULTIPLY : p.ADD,
scaled = [node[0], node[1], node[2] * 5, node[3]];
p.blendMode(blend);
texture.blendMode(blend);
p.fill(
multiply[i] ? p.color(0, alpha * 3.5) :
p.color(hue + hueShifts[i], sat * 9, brightness * 4.5, alpha * 3)
);
texture.fill(
multiply[i] ? p.color(0, alpha * 3.4) :
p.color(hue + hueShifts[i], sat * 9, brightness * 4.75, alpha * 3.3)
);
drawPolygon(p, hexagon(scaled));
drawPolygon(texture, tracePolygon(hexagon(smear(scaled, state.smear))));
});
// Recolor the background; Bypass p5js since it has no "color" compositing.
p.drawingContext.globalCompositeOperation =
texture.drawingContext.globalCompositeOperation = "color";
const recolor = p.color(hue, sat * 14, brightness + 5, .35);
p.fill(recolor);
p.circle(...state.center, state.canvasSize);
texture.fill(recolor);
texture.circle(...state.center, state.canvasSize);
// Reset the cached blend mode.
p.blendMode(p.BLEND);
texture.blendMode(p.BLEND);
// Now that the background is complete, glitch the foreground color.
glitch(
"Eccentric",
color,
_ => color =
((color + p.round(gaussian(3))) % stones.length + stones.length) %
stones.length,
_ => {
setValues();
state.majorMetadata.Stone += ", " + stones[color][0];
}
);
// Draw the foreground hexagons.
state.hexagons.map((hexagon, i) => {
p.blendMode(multiply[i] ? p.MULTIPLY : p.ADD);
p.fill(
multiply[i] ? p.color(hue + hueShifts[i], 100, 5, alpha * 5) :
p.color(hue + hueShifts[i], sat * 10, brightness * 10 + 10, alpha * 6)
);
drawPolygon(p, hexagon);
});
state.textugons.map((textugon, i) => {
texture.blendMode(multiply[i] ? p.MULTIPLY : p.ADD);
texture.fill(
multiply[i] ? p.color(hue + hueShifts[i], 100, 5, alpha * 5) :
p.color(hue + hueShifts[i], sat * 9, brightness * 10 + 10, alpha * 6.5)
);
drawPolygon(texture, textugon);
});
// Recolor the foreground; Bypass p5js since it has no "color" compositing.
// Previous mode was MULTIPLY or ADD, and next mode will be BLEND, so no
// need for cache busting.
p.drawingContext.globalCompositeOperation =
texture.drawingContext.globalCompositeOperation = "color";
state.hexagons.map((hexagon, i) => {
p.fill(hue + hueShifts[i], sat * 9, brightness * 10, alpha * 3);
drawPolygon(p, hexagon);
});
state.textugons.map((textugon, i) => {
texture.fill(hue + hueShifts[i], sat * 7.5, brightness * 10, alpha * 2.7);
drawPolygon(texture, textugon);
});
// Set the blend mode back to the default any subsequent draws.
p.blendMode(p.BLEND);
texture.blendMode(p.BLEND);
};
//-[ p5js functions ]---------------------------------------------------------//
// Creates the p5js canvas and disables looping.
// The rest of the setup is done when the Immutables properties are received.
p.setup = _ => {
canvas = p.createCanvas(1,1);
canvas.style("display", "block");
p.noLoop();
},
// Responds to the properties provided by Immutables.
p.myCustomRedrawAccordingToNewPropsHandler = properties => {
if (!properties.transactionHash) return;
// Check if existing state already matches the new properties.
if (
state &&
state.hash == properties.transactionHash &&
state.edition == properties.editionId
) {
if (state.square != properties.square) {
state.square = properties.square;
p.windowResized();
}
// The state doesn't need updated, so our work here is done.
return;
}
// Pull 32 bits from the transaction hash to seed the RNG.
// Pick which bits to pull based on the edition ID to cover the off chance
// that someone uses a contract to mint multiple outputs at once.
const setupSeed =
properties.transactionHash.substr(2 + 8 * (properties.editionId % 8), 8);
p.randomSeed(+("0x" + setupSeed));
// Roll values needed for node generation.
const
layers = (.2 + agaussian(1.2)) | 0,
loschianNumber = randomBool() ? 4 : 7,
layerRatio = p.sqrt(loschianNumber),
layerRotate = loschianNumber == 4 ? 0 : p.atan2(halfRoot3, 2.5),
alignment = randomBool() ? 1 : randomBool() ? 0 : 4,
ratio = randomBool(.75) ? .01 : randomBool() ? .1 : .3,
stutter = !alignment ? .05 : .015,
nodeCount = 32 + p.max(8, p.ceil(224 + gaussian(160))),
nodes = [],
candidates = [], // A list of candidate parents for the next generated node.
isDense = randomBool(.2),
// Creates a new node.
newNode = parent => {
let
// Random chance of jumping to a new layer. This affects all the rest
// of the values.
layer = randomBool(.1) && layers ? p.random(layers + 1) | 0 : parent[4],
size =
parent[2] * 2 ** gaussian(ratio) * layerRatio ** (layer - parent[4]),
distance = layer != parent[4] ? 0 :
(size + parent[2]) * (1.15 + gaussian(stutter)),
angle = parent[3] + (p.PI * isDense) +
p.round(gaussian(1.5) % 6) * p.PI / 3 +
gaussian(alignment * p.PI / 96) + layerRotate * (parent[4] - layer);
let node = [
parent[0] + distance * p.cos(angle),
parent[1] + distance * p.sin(angle),
size,
angle,
layer
];
nodes.push(node);
return node;
};
// Generate starter nodes in every layer.
for (let i = layers + 1, layerSize = 20; i--;) {
let node = [0, 0, layerSize, i * layerRotate, i, 0];
nodes.push(node);
for (let j = 2 + randomBool(); j--;) candidates.push(node, node);
// Help smaller layers out by giving them more nodes.
for (let j = layers - i; j--;) newNode(node);
layerSize /= layerRatio;
}
// Grow randomly from current nodes to reach the target node count.
while (nodes.length < nodeCount) {
let
pIndex = p.random(candidates.length) | 0,
parent = candidates[pIndex];
candidates.splice(pIndex, 1);
let node = newNode(parent);
while (randomBool() || !candidates.length) candidates.push(node);
}
const
// Compute the canvas center and size based on node locations.
frame = enclose(nodes, []),
maxRadius = p.max(
nodes.map(node => p.sqrt(distSquared(node, frame)) + node[2] * 1.5)
),
// Roll the remaining values needed to prepare for drawing.
size = p.random(4) | 0,
handRatio = 2 ** (1.5 + size / 2),
handLength = maxRadius / handRatio,
style = p.random(),
styleId = (style < .875) + (style < .7125);
// Put everything where it can be referenced when drawing and redrawing.
state = {
square: properties.square,
edition: +properties.editionId,
hash: properties.transactionHash,
center: [frame[0], frame[1]],
canvasSize: maxRadius * 2.2,
styleAngle: p.random(p.TWO_PI),
smear: maxRadius / (handRatio * 256),
leftHanded: randomBool(.1),
handRatio: handRatio,
handLength: handLength,
penStep: handLength / 64,
wristStep: handLength / 384,
penSmoothing: .6,
wristSmoothing: .9,
penDeflection: .25,
// How close the pen has to get to a vertex before having "arrived".
thresholdSquared: handLength * handLength / 2048,
nodes: nodes,
hexagons: nodes.map(node => hexagon(node)),
glitch: p.random() * p.random() * .4,
glitches: {},
styleDraw: [supreme, celestial, energy][styleId],
};
// Add the metadata to state that is known at this point.
// It is split into major and minor metadata for ordering purposes.
state.majorMetadata = {
Nexus: ["Supreme", "Celestial", "Energy"][styleId],
Arcana: "" // Reserving the key index. Value is filled during drawing.
};
state.minorMetadata = {
Adherence: ["Extreme", "Varied", "Uniform"][(ratio < .1) + (ratio < .2)],
Growth: ["Rigid", "Loose", "", "", "Chaotic"][alignment],
Reach: ["Local", "Regional", "Continental", "Global"][size],
Focus: isDense ? "Inward" : "Outward",
Centuries: "" + (1 + (nodeCount - 1) / 100 | 0),
Degrees: "" + (layers + 1),
};
// Only include the Loschian Number in metadata if there are actually layers.
if (layers) state.minorMetadata["L\u00F6schian Number"] = "" + loschianNumber;
// Add final metadata here that should be last in the list of traits.
state.minorMetadata.Handed = state.leftHanded ? "Left" : "Right";
state.minorMetadata.transactionHash = properties.transactionHash;
// Consider glitching the trace variables now that the state is set up.
glitch("Revelous", true, _ => {
state.smear *= 3;
state.penSmoothing = .8;
state.penDeflection = .875;
// Since texture overlay hexagons are based on original nodes, this creates
// a nice discrepency between the nodes and their overlay.
state.nodes = nodes.map(node => smear(node, state.smear));
});
// Setup the trace state and trace the hexagons on the texture overlay.
resetTraceState();
state.textugons =
nodes.map(node => tracePolygon(hexagon(smear(node, state.smear))));
// Force a redraw and resize the window if needed.
p.windowResized();
};
p.windowResized = _ => {
if (!state) return;
const minDim = p.min(p.windowWidth, p.windowHeight) * .8 / state.square | 0;
p.resizeCanvas(minDim, minDim);
};
p.keyTyped = _ => {
if (!state || state.square != 1) return;
if (p.key == 'S' && !state.save) {
state.pixelDensity = p.pixelDensity();
state.save = true;
canvas.style("display", "none");
p.pixelDensity(1);
p.resizeCanvas(5000,5000);
} else if (p.key == 'Z') {
state.zoom = !state.zoom;
p.redraw();
}
}
p.draw = _ => {
if (!state) return;
// Pull 32 bits from the transaction hash to reseed the RNG.
// Reseed at the beginning of every draw to keep output consistent.
// See the comment on setupSeed to explain the use of the edition ID.
const drawSeed = state.hash.substr(2 + 8 * ((state.edition + 1) % 8), 8);
p.randomSeed(+("0x" + drawSeed));
// Set up the transformation matrix for centering the nodes.
const
zoom = state.zoom ? p.sqrt(2) : 1,
texture = p.createGraphics(p.width, p.width),
scale = p.width / state.canvasSize * zoom,
matrix = [
scale,
0,
0,
scale,
p.width / 2 - state.center[0] * scale,
p.width / 2 - state.center[1] * scale
];
p.applyMatrix(...matrix);
texture.applyMatrix(...matrix);
// Reset the tracing state before drawing to keep output consistent.
resetTraceState();
// Call the style-specific draw code.
state.styleDraw(texture);
// Compile glitches.
state.majorMetadata.Arcana =
Object.keys(state.glitches).sort().join(", ") || "Unknown";
const fullMetadata = {
...state.majorMetadata,
...state.minorMetadata,
}
// Report the metadata and log each trait to the console.
if (!state.metadataReported) {
console.log("metadata: ", fullMetadata);
Object.keys(fullMetadata)
.map(key => console.log(key, ":", fullMetadata[key]));
// Mark as completed so that metadata output can be skipped for redraws.
state.metadataReported = true;
}
// Create the "thread" texture mask.
const
threadSize = p.width / (state.handRatio * 54),
threadLimit = p.width / threadSize + 1,
mask = p.createGraphics(p.width, p.width);
mask.fill(0);
mask.noStroke();
// When zoom is enabled, zoom in just enough to have a full square image.
if (state.zoom) {
const offset = p.width / 2 * (1 - zoom);
mask.applyMatrix(zoom, 0, 0, zoom, offset, offset);
}
// Draw a grid of threads over the whole mask.
for (let x = 0; x < threadLimit; x++) {
for (let y = 0; y < threadLimit; y++) {
if (x % 2 == y % 2) continue;
const
w = threadSize * (1 + gaussian(.16)),
h = threadSize * (1 + gaussian(.16));
if (w < 0 || h < 0) continue;
mask.ellipse(threadSize * x, threadSize * y, w, h);
}
}
// Apply texture mask.
const textureImage = texture.get();
textureImage.mask(mask);
// Apply border masks.
const mainImage = p.get();
if (state.zoom) {
// Square mask.
const inset = threadSize * p.sqrt(2) / 8;
mask.clear();
mask.resetMatrix();
mask.rect(inset, inset, mask.width - inset * 2);
mainImage.mask(mask);
} else {
// Circle mask.
mask.clear();
mask.circle(mask.width / 2, mask.width / 2, mask.width * .99);
textureImage.mask(mask);
mask.clear();
mask.circle(mask.width / 2, mask.width / 2, mask.width * .99 - threadSize / 4);
mainImage.mask(mask);
}
// Copy the masked images to the main canvas.
p.clear();
p.resetMatrix();
p.image(mainImage, 0, 0);
p.image(textureImage, 0, 0);
// Clean up the extra canvases.
texture.remove();
mask.remove();
// If a save was initiated, write out the image and reset the canvas.
if (state.save) {
p.save("Syncret #" + state.edition + ".png");
p.pixelDensity(state.pixelDensity);
p.windowResized();
canvas.style("display", "block");
state.save = false;
}
}
```
↓ show full (34,363 bytes)