0xfc3d126d…1282sent to0x499f4943…3001·#15,306,064·view on Etherscan
Firmament```javascript
"it rests among the stars / gathered up in the furthest firmament";
let
canvas, // p5js canvas.
state; // Keep all shared state in one place.
//-[ setup 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.frameRate(10);
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.transactionHash == properties.transactionHash &&
state.editionId == properties.editionId
) {
state.square = properties.square;
p.windowResized();
return;
}
// Pull bits from the hash to seed the RNG.
p.randomSeed("0x" +
properties.transactionHash.substr(2 + 8 * (properties.editionId % 8), 8));
// Intialize the state.
state = {
...properties,
animate: properties.square == 1,
progress: 0,
};
setStyling();
placeForegroundStars();
drawConstellation();
placeBackgroundStars();
addDust();
describe();
// Resize the window and turn looping on or off as needed.
p.windowResized();
state.animate ? p.loop() : (p.noLoop(), p.redraw());
};
p.windowResized = _ => {
if (!state) return;
const minDim = p.min(p.windowWidth, p.windowHeight) * .8 / state.square | 0;
if (minDim === p.width) return;
p.resizeCanvas(minDim, minDim);
state.resize = true;
};
p.keyTyped = _ => {
if (!state || state.square != 1) return;
if (p.key == "A") {
if (state.animate) {
state.animate = false;
p.noLoop();
p.redraw();
} else {
state.animate = true;
state.progress = 0;
p.loop();
}
}
if (p.key == "B") {
state.simple = !state.simple;
}
if (p.key == "C") {
state.clear = !state.clear;
}
};
const
setStyling = _ => {
// Determine star size.
state.chibi = p.random() < .06;
state.dashed = p.random() < .06;
state.sparse = p.random() < .18;
const gap = state.chibi ? .7 : .5;
state.drawLine = !state.dashed ?
line => p.line(...line.slice(0, 4)) :
line => {
const
dashes = p.max(1, line[6] / 3 | 0),
scale = dashes - gap;
for (let i = 0; i < dashes; i++) {
p.line(
p.lerp(line[0], line[2], i / scale),
p.lerp(line[1], line[3], i / scale),
p.lerp(line[0], line[2], (i + 1 - gap) / scale),
p.lerp(line[1], line[3], (i + 1 - gap) / scale)
);
}
};
let color = p.random();
state.color =
color < .12 ? [224, 42, 32, "Fire"] :
color < .24 ? [96, 128, 247, "Water"] :
color < .36 ? [148, 164, 196, "Air"] :
[239, 239, 153, "Earth"];
},
placeForegroundStars = _ => {
state.stars = [];
const mainSizer = _ => state.chibi ? p.random(1.3, 1.8) : 1;
// Place 2 to 4 stars somewhat uniformly.
const
preset = p.random(2, 5) | 0,
angleOffset = p.random(p.TWO_PI / preset);
for (let i = 0; i < preset; i++) {
const
dist = p.random(14, 20),
angle = angleOffset + (i * p.TWO_PI + p.random(p.PI)) / preset,
position = [dist * p.cos(angle), dist * p.sin(angle)];
let star = makeStar(position, .75, mainSizer());
state.stars.push(star);
}
// Place the remaining stars randomly.
const starCount = 4 + p.abs(p.randomGaussian(1, 3)) | 0;
for (let i = preset; i < starCount; i++) {
const position = getOpening(state.stars);
if (position) {
let star = makeStar(position, .75, mainSizer());
state.stars.push(star);
}
}
},
drawConstellation = _ => {
state.lines = [];
const
connectableStars = [...state.stars],
lineCount = state.stars.length * 1.3 + p.randomGaussian(.8, 2.3) | 0;
while (state.lines.length < lineCount && connectableStars.length > 1) {
const
index1 = p.random(connectableStars.length) | 0,
index2 = p.random(connectableStars.length) | 0;
// Make sure different stars were selected.
if (index1 === index2) {
if (p.random() < .25) {
// Random chance to remove this star from consideration.
connectableStars.splice(index1, 1);
}
continue;
}
const
star1 = connectableStars[index1],
star2 = connectableStars[index2],
xdiff = star2[0] - star1[0],
ydiff = star2[1] - star1[1];
// Check if the stars are too close.
if (xdiff * xdiff + ydiff * ydiff < 25) {
if (p.random() < .33) {
// Random chance to remove a star from consideration.
const
dist1 = star1[0] * star1[0] + star1[1] * star1[1],
dist2 = star2[0] * star2[0] + star2[1] * star2[1],
removeIndex = p.random(dist1 + dist2) < dist1 ? index1 : index2;
connectableStars.splice(removeIndex, 1);
}
continue;
}
// Check if this line would cover a star.
let covering = false;
for (let star of state.stars) {
if (star !== star1 && star !== star2 &&
isBetween(star1, star2, star, xdiff, ydiff))
{
covering = star;
break;
}
}
if (covering) {
let i = connectableStars.indexOf(covering);
if (i >= 0 && p.random() < .2) {
// Random chance to remove a star from consideration.
const
dist1 = star1[0] * star1[0] + star1[1] * star1[1],
dist2 = star2[0] * star2[0] + star2[1] * star2[1],
removeIndex = p.random(dist1 + dist2) < dist1 ? index1 : index2;
connectableStars.splice(removeIndex, 1);
}
continue;
}
// Check if this line conflicts with another line.
let
intersections = 0,
duplicate = false;
for (let line of state.lines) {
// Check if these lines have the same endpoints.
if (line[4] === star1 && line[5] === star2 ||
line[4] === star2 && line[5] === star1)
{
duplicate = true;
break;
}
// Check if these lines intersect.
if (intersects(star1, star2, line)) {
intersections++;
}
}
if (duplicate) {
if (p.random() < .4) {
// Random chance to remove a star from consideration.
const
dist1 = star1[0] * star1[0] + star1[1] * star1[1],
dist2 = star2[0] * star2[0] + star2[1] * star2[1],
removeIndex = p.random(dist1 + dist2) < dist1 ? index1 : index2;
connectableStars.splice(removeIndex, 1);
}
continue;
}
// The more intersections, the more likely this line will not be used.
if (p.random() >= (1 / (intersections * lineCount / 2 + 1))) continue;
if (intersections) state.intersections = true;
state.lines.push(lineBetween(star1, star2, xdiff, ydiff));
}
},
placeBackgroundStars = _ => {
const totalStarCount =
(state.sparse ? 20 : 35) + p.abs(p.randomGaussian(0, 15)) | 0;
for (let i = state.stars.length; i < totalStarCount; i++) {
const position = getOpening(state.stars, 22);
if (position) {
// Check that this new star isn't on a constellation line.
let covered = false;
for (let line of state.lines) {
if (isOn(line, position, 2.5 + 1.5 * state.chibi)) {
covered = true;
break;
}
}
if (!covered) {
state.stars.push(makeStar(position, .15, p.random(.4, 1)));
}
}
}
},
addDust = _ => {
state.dust = [];
const dustCount =
(state.sparse ? 80 : 350) + p.abs(p.randomGaussian(0, 100)) | 0;
for (let i = 0; i < dustCount; i++) {
const mote = makeStar(
getOpening([], p.random(18, 24)),
.05, p.random(.2, .6) * p.random(.25, 1)
);
// Dust motes are dimmer than foreground and background stars.
mote[3] *= p.random(.4, .7);
state.dust.push(mote);
}
},
describe = _ => {
let cycle = false;
const
groups = new Map(),
getId = group => group.id || getId(group.parent),
getRoot = group => group.parent && getRoot(group.parent) || group,
lockId = group => group.id || (group.id = lockId(group.parent));
// Figure out how many groups of stars/lines there are.
// In graph theory I guess these are "components"?
// IDK it's been at least 15 years since I took a data structure class.
for (let i = 0; i < state.lines.length; i++) {
const
line = state.lines[i],
group1 = groups.get(line[4]),
group2 = groups.get(line[5]);
if (!group1) {
if (!group2) {
// Don't laugh at my disjoint-set data structure please.
const newGroup = {id: i + 1};
groups.set(line[4], newGroup);
groups.set(line[5], newGroup);
} else {
groups.set(line[4], group2);
}
} else {
// group1 exists.
if (!group2) {
groups.set(line[5], group1);
} else {
if (getId(group1) !== getId(group2)) {
// The worst possible way to union sets, probably.
// There's definite O(n^2) energy, but n is small, so...
const root = getRoot(group2);
delete root.id;
root.parent = group1;
} else {
// The group has run back into itself, so we have a cycle.
cycle = true;
}
}
}
}
// Collect the group IDs from all lines.
const groupIds = new Map();
for (let group of groups.values()) {
groupIds.set(lockId(group), true);
}
const lineTraits = [];
if (state.intersections) lineTraits.push("Star-crossed");
if (groupIds.size > 1) lineTraits.push("Fragmented");
if (!cycle && state.lines.length > 1) lineTraits.push("Simple");
if (state.lines.length == 1) lineTraits.push("Minimalist");
if (!state.lines.length) lineTraits.push("Missing");
// Put an "and" between the last two traits.
if (lineTraits.length > 1) {
const last = lineTraits.pop()
lineTraits.push(lineTraits.pop() + " and " + last);
}
const
figureDescription =
lineTraits.join(", ") +
(lineTraits.length ? " " : "") +
(state.dashed ? "Asterism" : "Constellation"),
starDescription =
(state.sparse ? "Sparse" : "Crowded") +
" and " +
(state.chibi ? "Luminous" : "Twinkling"),
metadata = {
"Figure": figureDescription,
"Stars": starDescription,
"Element": state.color[3],
"transactionHash": state.transactionHash,
};
console.log("metadata:", metadata);
};
//-[ draw functions ]---------------------------------------------------------//
p.draw = _ => {
if (!state) return;
// When not animating, fast forward to a frame where everything is drawn.
const frame = state.animate ? state.progress : 1000;
// Advance a frame unless this is a resize.
if (!state.resize) {
state.progress++;
} else {
state.resize = false;
}
// The canvas is 25 by 25, origin in the middle, rotated by time of day-ish.
p.resetMatrix();
p.scale(p.width / 50);
p.translate(25, 25);
if (state.animate) {
p.rotate(((new Date()).getTime() % 86164100) / 86164100 * p.TWO_PI);
}
p.noStroke();
p.blendMode(p.BLEND);
p.background(16);
p.blendMode(p.SCREEN);
// Draw elements from back to front. Stardust first.
if (!state.simple) for (let i = 0; i < state.dust.length; i++) {
const dust = state.dust[i];
p.fill(
p.constrain((dust[3] + (state.animate ? p.random(-10, 10) : 0)) *
p.constrain((frame - i / 40) / p.random(10, 14), 0, 1), 0, 255)
);
dust[dust.length - 1]();
}
// Then stars.
for (let i = 0; i < state.stars.length; i++) {
const star = state.stars[i];
p.fill(
p.constrain((star[3] + (state.animate ? p.random(-20, 10) : 0)) *
p.constrain((frame - i / 2) / 8, 0, 1), 0, 255)
);
star[star.length - 1]();
}
// Then the asterism.
p.strokeCap(p.ROUND);
p.strokeWeight(state.chibi ? .66 : .4);
if (!state.clear) for (let i = 0; i < state.lines.length; i++) {
const line = state.lines[i];
let color = state.color.slice(0, 3);
let flicker = !state.animate ? 1 :
p.random(.93, 1) *
p.random(.93, 1) *
p.constrain(
(frame - 6 - .6 * state.lines.length - i / 5 + p.random(2)) / 5, 0, 1
);
for (let i = 0; i < color.length; i++) {
color[i] *= flicker;
}
p.stroke(...color);
state.drawLine(line);
}
};
//-[ helper functions ]-------------------------------------------------------//
const
getOpening = (stars, distance = 18) => {
let collision, tries = 0;
do {
collision = false;
tries++;
let point;
// Random point in a circle with radius 20, using rejection sampling.
do {
point = [p.random(-distance, distance), p.random(-distance, distance)];
} while (point[0] * point[0] + point[1] * point[1] > distance * distance);
for (let star of stars) {
let xdiff = star[0] - point[0], ydiff = star[1] - point[1];
if (xdiff * xdiff + ydiff * ydiff < 2 * star[2] * star[2]) {
collision = true;
break;
}
}
if (!collision) return point;
} while (tries < 100);
},
makeStar = (position, burstPercent, scale) => {
const
size = (scale || 1) * p.random(.6, .9),
margin = state.chibi ? 1 : .6,
brightness = p.random(240, 255);
if (p.random() < burstPercent) {
const
blunt = p.random(.01, .07),
fatness = p.random(.3, .6),
sides = p.max(3, p.randomGaussian(5.5, .25) | 0),
rotation = p.random(p.PI / sides),
angle = p.TWO_PI / sides;
// IDK.
const
maxInner = p.sin(p.HALF_PI - angle / 2),
halfCutSquared = (1 - p.cos(p.PI * (1 - 1 / sides))) / 2,
minInner = p.sqrt(1 - halfCutSquared),
inner = minInner + (maxInner - minInner) * fatness;
return [...position, size * 1.5 + margin, brightness, _ => {
p.push();
p.translate(...position);
p.rotate(rotation);
p.scale(size);
p.beginShape();
for (let s = 0; s < sides; s++) {
p.vertex(p.cos(angle * (s - blunt)), p.sin(angle * (s - blunt)));
p.vertex(p.cos(angle * (s + blunt)), p.sin(angle * (s + blunt)));
p.vertex(
inner * p.cos(angle * (s + .5)), inner * p.sin(angle * (s + .5))
);
}
p.endShape(p.CLOSE);
p.pop();
}];
} else {
return [...position, size + margin, brightness, _ => {
p.ellipse(...position, size, size);
}];
}
},
lineBetween = (star1, star2, xdiff, ydiff) => {
const
distance = p.sqrt(xdiff * xdiff + ydiff * ydiff),
xOff = xdiff / distance,
yOff = ydiff / distance,
s1Dist = star1[2],
s2Dist = star2[2];
return [
star1[0] + s1Dist * xOff,
star1[1] + s1Dist * yOff,
star2[0] - s2Dist * xOff,
star2[1] - s2Dist * yOff,
star1,
star2,
distance,
];
},
distanceSquared = (a, b) => {
let
x = a[0] - b[0],
y = a[1] - b[1];
return x * x + y * y;
},
isBetween = (star1, star2, covered, xdiff, ydiff, threshold = 2.5) => {
let lengthSquared = xdiff * xdiff + ydiff * ydiff;
let perpDistance =
p.abs(ydiff * (covered[0] - star1[0]) - xdiff * (covered[1] - star1[1])) /
p.sqrt(lengthSquared);
return (
perpDistance < threshold &&
distanceSquared(star1, covered) < lengthSquared &&
distanceSquared(star2, covered) < lengthSquared
)
},
// Same as `isBetween` except that it takes a line instead of endpoints.
isOn = (line, point, threshold = 2.5) =>
isBetween(
[line[0], line[1]],
[line[2], line[3]],
point,
line[2] - line[0],
line[3] - line[1],
threshold
),
clockwise = (star1, star2, star3) =>
(star3[1] - star1[1]) * (star2[0] - star1[0]) <
(star2[1] - star1[1]) * (star3[0] - star1[0]),
intersects = (star1, star2, line) =>
clockwise(star1, line, line.slice(2, 4)) !=
clockwise(star2, line, line.slice(2, 4)) &&
clockwise(star1, star2, line) !=
clockwise(star1, star2, line.slice(2, 4));
```