memoscan
← all memos

Memo 0xf0c36560…659b66 on Ethereum

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; } } ```
unknown sendersent to0x499f4943…3001·#14,119,488·view on Etherscan
``` "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; } } ```