How it works
Explore how we generate the physical matters collection by using on-chain information to create unique and beautiful art.
The Sketch
The following is a TypeScript sketch of the algorithm used to generate the physical matters collection. A sketch is a P5.js file that can be run in the browser to visualize the algorithm.
// ██╗ ██╗███████╗██╗ ██████╗ ██████╗ ███╗ ███╗███████╗ ████████╗ ██████╗
// ██║ ██║██╔════╝██║ ██╔════╝██╔═══██╗████╗ ████║██╔════╝ ╚══██╔══╝██╔═══██╗
// ██║ █╗ ██║█████╗ ██║ ██║ ██║ ██║██╔████╔██║█████╗ ██║ ██║ ██║
// ██║███╗██║██╔══╝ ██║ ██║ ██║ ██║██║╚██╔╝██║██╔══╝ ██║ ██║ ██║
// ╚███╔███╔╝███████╗███████╗╚██████╗╚██████╔╝██║ ╚═╝ ██║███████╗ ██║ ╚██████╔╝
// ╚══╝╚══╝ ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═════╝
//
// ██████╗ ██████╗ ██████╗ ██████╗ ████████╗ ██████╗ ██╗ ██████╗ ██████╗██╗ ██╗███████╗
// ██╔═══██╗ ██╔══██╗██╔═══██╗██╔═══██╗╚══██╔══╝ ██╔══██╗██║ ██╔═══██╗██╔════╝██║ ██╔╝██╔════╝
// ██║██╗██║ ██████╔╝██║ ██║██║ ██║ ██║ ██████╔╝██║ ██║ ██║██║ █████╔╝ ███████╗
// ██║██║██║ ██╔══██╗██║ ██║██║ ██║ ██║ ██╔══██╗██║ ██║ ██║██║ ██╔═██╗ ╚════██║
// ╚█║████╔╝ ██║ ██║╚██████╔╝╚██████╔╝ ██║ ██████╔╝███████╗╚██████╔╝╚██████╗██║ ██╗███████║
// ╚╝╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚══════╝
// */**/*//*//**/*/*////**/**///*/***/******/**//***//****/*////**/*//**/*/*///**/**///**//
import p5 from "p5";
type Metadata = {
name: string;
description: string;
tokenId: number;
image: string;
attributes: {
display_type?: string;
trait_type: string;
value: string | number;
}[];
hidden: {
trait_type: string;
value: string | string[];
}[];
};
type ColorScheme = {
fg: string[];
bg: string[];
};
type StyleRange = {
lengthRange: number[];
weightRange: number[];
angleOffsetRange: number[];
depthRange: number[];
};
type HashValues = {
style: string;
mirStyle: string;
colorPaletteName: string;
colorPalette: any;
fgColor: any;
length: number;
lengthMirrored: number;
weight: number;
weightMirrored: number;
depth: number;
depthMirrored: number;
form: number;
symmetrical: number;
startPoint: number[];
initialAngle: number;
angleOffset: number;
angleOffsetMirrored: number;
seed: number;
complexity: number;
mirRanges: StyleRange;
mainRanges: StyleRange;
};
const sketch = (
p: p5,
artHash: string,
tokenId: number,
width: number = 2000,
height: number = 2000,
updateMetadata?: (metadata: Metadata) => void
) => {
let id = tokenId;
let branchLayer: p5.Graphics;
let hashValues: HashValues = {} as HashValues;
let styleRanges: Record<string, StyleRange> = {};
const styleRarities = [40, 30, 20, 10]; // ['Pure', 'Delicate', 'Robust', 'Intricate']
const paletteRarities = [30, 25, 20, 15, 10]; // ['Earthen Core', 'Molten Flare', 'Liquid Mirage', 'Aether Drift', 'Monochrome Shades']
const colorSchemes: Record<string, ColorScheme> = {
"Earthen Core": {
fg: ["#ffe9a2", "#BA5A00", "#e2b565", "#DE774B", "#BD7521"],
bg: ["#0C542E", "#003310"],
},
"Molten Flare": {
fg: ["#FF3E00", "#8B0000", "#FF8118", "#1B1B1B", "#e12700"],
bg: ["#F7FEC5", "#D6FDF4"],
},
"Liquid Mirage": {
fg: ["#FFFCDE", "#48C9B0", "#E6E6FA", "#64E986", "#95D8DE"],
bg: ["#000057", "#000C34"],
},
"Aether Drift": {
fg: ["#DA70D6", "#A64DE6", "#8AD6FF", "#FF67AA", "#FF05E5"],
bg: ["#2C2C3F", "#0D0D0D"],
},
"Monochrome Shades": {
fg: ["#EEEEEE", "#DDDDDD", "#CCCCCC", "#BBBBBB", "#AAAAAA"],
bg: ["#000000", "#111111"],
},
};
p.setup = () => {
//update ranges so artwork always looks the same no matter the window size
updateRangesOnResize();
//init canvas and drawing layer
const canvas = p.createCanvas(width, height);
branchLayer = p.createGraphics(width, height);
p.noLoop();
drawRoots();
};
// *///**//*//*//***//*////*///*****//**/*/**//**//
//
// ███╗ ███╗ █████╗ ██████╗ ██╗ ██╗ █████╗ ███████╗██╗ ██╗
// ████╗ ████║██╔══██╗██╔══██╗ ██║ ██║██╔══██╗██╔════╝██║ ██║
// ██╔████╔██║███████║██████╔╝ ███████║███████║███████╗███████║
// ██║╚██╔╝██║██╔══██║██╔═══╝ ██╔══██║██╔══██║╚════██║██╔══██║
// ██║ ╚═╝ ██║██║ ██║██║ ██║ ██║██║ ██║███████║██║ ██║
// ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝
//
// *//*/**/*//*///**///*/***//**/*/*///**/**//***//*//**/*/*///*****///*/****//*///
function computeHashValues(hash: string) {
// mi = mapping index
const mi = {
style: [2, 4],
mirStyle: [4, 6],
palette: [6, 8],
fgColor: [8, 12],
length: [12, 16],
weight: [16, 20],
depth: [20, 24],
form: [24, 28],
symmetry: [28, 32],
angleOffset: [32, 36],
};
// get style of tree
hashValues.style = selectByRarity(
extract(hash, mi.style) * (100 / 256),
styleRarities,
["Pure", "Delicate", "Robust", "Intricate"]
);
hashValues.mirStyle = selectByRarity(
extract(hash, mi.mirStyle) * (100 / 256),
styleRarities,
["Pure", "Delicate", "Robust", "Intricate"]
);
// get main color palette
hashValues.colorPaletteName = selectByRarity(
extract(hash, mi.palette) * (100 / 256),
paletteRarities,
[
"Earthen Core",
"Molten Flare",
"Liquid Mirage",
"Aether Drift",
"Monochrome Shades",
]
);
hashValues.colorPalette = colorSchemes[hashValues.colorPaletteName];
// get foreground colors
let fgColorStart = getColorFromHash(
hash,
hashValues.colorPalette.fg,
mi.fgColor
);
hashValues.fgColor = {
start: fgColorStart,
end: getColorFromHash(
hash,
hashValues.colorPalette.fg.filter(
(color: string) => color !== fgColorStart
),
mi.palette
),
};
// get main and mirrored ranges for parameters based on style
hashValues.mainRanges = styleRanges[hashValues.style];
hashValues.mirRanges = styleRanges[hashValues.mirStyle];
hashValues.length = mapHashValue(
hash,
mi.length,
hashValues.mainRanges.lengthRange
);
hashValues.lengthMirrored = mapHashValue(
hash,
mi.length,
hashValues.mirRanges.lengthRange
);
hashValues.weight = mapHashValue(
hash,
mi.weight,
hashValues.mainRanges.weightRange
);
hashValues.weightMirrored = mapHashValue(
hash,
mi.weight,
hashValues.mirRanges.weightRange
);
hashValues.depth = Math.floor(
mapHashValue(hash, mi.depth, hashValues.mainRanges.depthRange)
);
hashValues.depthMirrored = Math.floor(
mapHashValue(hash, mi.depth, hashValues.mirRanges.depthRange)
);
hashValues.form = extract(hash, mi.form) % 3;
const asymmetrical = extract(hash, mi.symmetry) % 5;
if (asymmetrical == 0) hashValues.symmetrical = 1;
else hashValues.symmetrical = 0;
hashValues.startPoint = getStartPoint(hashValues.form);
hashValues.initialAngle = getInitialAngle(hashValues.form);
hashValues.angleOffset = mapHashValue(
hash,
mi.angleOffset,
hashValues.mainRanges.angleOffsetRange
);
hashValues.angleOffsetMirrored = mapHashValue(
hash,
mi.angleOffset,
hashValues.mirRanges.angleOffsetRange
);
hashValues.seed = parseInt(hash.slice(-16), 16) % 1000000;
hashValues.complexity = 0;
return hashValues;
}
//
// ███╗ ███╗ █████╗ ██╗███╗ ██╗ ██████╗ ██████╗ █████╗ ██╗ ██╗██╗███╗ ██╗ ██████╗
// ████╗ ████║██╔══██╗██║████╗ ██║ ██╔══██╗██╔══██╗██╔══██╗██║ ██║██║████╗ ██║██╔════╝
// ██╔████╔██║███████║██║██╔██╗ ██║ ██║ ██║██████╔╝███████║██║ █╗ ██║██║██╔██╗ ██║██║ ███╗
// ██║╚██╔╝██║██╔══██║██║██║╚██╗██║ ██║ ██║██╔══██╗██╔══██║██║███╗██║██║██║╚██╗██║██║ ██║
// ██║ ╚═╝ ██║██║ ██║██║██║ ╚████║ ██████╔╝██║ ██║██║ ██║╚███╔███╔╝██║██║ ╚████║╚██████╔╝
// ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝
// *///**/**//**/*/*///*//**//**/*/*///**/**///**//*//**/*/*//****/*//*//***///*****//*/****//****/*//***/**//**/*/*///*/**
function drawRoots() {
p.clear();
branchLayer.clear();
const hashValues = computeHashValues(artHash);
// make random() function reproducible
p.randomSeed(hashValues.seed);
//draw background
drawGradientBackground(hashValues.colorPalette.bg);
//draw foreground
drawBranches(hashValues);
p.image(branchLayer, 0, 0);
//generate metadata
if (updateMetadata) {
updateMetadata(generateMetadata(hashValues));
}
}
function drawBranches(hashValues: HashValues) {
// If Mirrored
if (hashValues.form === 2) {
// If not symmetrical
if (!hashValues.symmetrical) {
drawBranch(
branchLayer,
hashValues.startPoint,
hashValues.weight,
hashValues.lengthMirrored,
hashValues.initialAngle + Math.PI,
hashValues.angleOffsetMirrored,
0,
hashValues.depthMirrored,
hashValues.symmetrical
);
}
// if symmetrical
else {
drawBranch(
branchLayer,
hashValues.startPoint,
hashValues.weight,
hashValues.length,
hashValues.initialAngle + Math.PI,
hashValues.angleOffset,
0,
hashValues.depth,
hashValues.symmetrical
);
}
}
// draw main
drawBranch(
branchLayer,
hashValues.startPoint,
hashValues.weight,
hashValues.length,
hashValues.initialAngle,
hashValues.angleOffset,
0,
hashValues.depth,
hashValues.symmetrical
);
}
function drawBranch(
gfx: p5.Graphics,
startPoint: number[],
weight: number,
length: number,
angle: number,
angleOffset: number,
depthIndex: number,
maxDepth: number,
symmetrical: number
) {
if (length < 0.005 * height || depthIndex > maxDepth) return;
// increase complexity trait for every branch drawn
hashValues.complexity++;
const [x1, y1] = [
startPoint[0] + length * p.cos(angle),
startPoint[1] + length * p.sin(angle),
];
const endpoint = [x1, y1];
const interColor = p.lerpColor(
p.color(hashValues.fgColor.start),
p.color(hashValues.fgColor.end),
p.pow(p.map(depthIndex, 0, maxDepth, 0, 1), 2)
);
gfx.stroke(interColor);
gfx.strokeWeight(weight);
gfx.line(startPoint[0], startPoint[1], endpoint[0], endpoint[1]);
let newWeight, newLength;
if (!symmetrical) {
newWeight = weight * p.random(0.6, 0.8);
newLength = length * p.random(0.7, 0.9);
} else {
newWeight = weight * 0.7;
newLength = length * 0.8;
}
drawBranch(
gfx,
endpoint,
newWeight,
newLength,
angle + angleOffset,
angleOffset,
depthIndex + 1,
maxDepth,
symmetrical
);
drawBranch(
gfx,
endpoint,
newWeight,
newLength,
angle - angleOffset,
angleOffset,
depthIndex + 1,
maxDepth,
symmetrical
);
}
function drawGradientBackground(bg: p5.Color[]) {
p.noStroke();
for (let y = 0; y < height; y++) {
const interColor = p.lerpColor(
p.color(bg[0]),
p.color(bg[1]),
y / height
);
p.fill(interColor);
p.rect(0, y, width, 1);
}
}
// ██╗ ██╗███████╗██╗ ██████╗ ███████╗██████╗ ███████╗██╗ ██╗███╗ ██╗ ██████╗████████╗██╗ ██████╗ ███╗ ██╗███████╗
// ██║ ██║██╔════╝██║ ██╔══██╗██╔════╝██╔══██╗ ██╔════╝██║ ██║████╗ ██║██╔════╝╚══██╔══╝██║██╔═══██╗████╗ ██║██╔════╝
// ███████║█████╗ ██║ ██████╔╝█████╗ ██████╔╝ █████╗ ██║ ██║██╔██╗ ██║██║ ██║ ██║██║ ██║██╔██╗ ██║███████╗
// ██╔══██║██╔══╝ ██║ ██╔═══╝ ██╔══╝ ██╔══██╗ ██╔══╝ ██║ ██║██║╚██╗██║██║ ██║ ██║██║ ██║██║╚██╗██║╚════██║
// ██║ ██║███████╗███████╗██║ ███████╗██║ ██║ ██║ ╚██████╔╝██║ ╚████║╚██████╗ ██║ ██║╚██████╔╝██║ ╚████║███████║
// ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝
// *///**//*//*/****//*/**/*//**//**///*/***///**//*//**/*/*///*//**//**/*/*//*///*
function updateRangesOnResize() {
styleRanges = {
Pure: {
lengthRange: [0.09 * height, 0.1 * height],
weightRange: [0.008 * width, 0.0102 * width],
angleOffsetRange: [Math.PI / 5, Math.PI / 4],
depthRange: [6, 8],
},
Delicate: {
lengthRange: [0.1 * height, 0.11 * height],
weightRange: [0.004 * width, 0.007 * width],
angleOffsetRange: [Math.PI / 10, Math.PI / 8],
depthRange: [9, 12],
},
Robust: {
lengthRange: [0.1 * height, 0.11 * height],
weightRange: [0.02 * width, 0.035 * width],
angleOffsetRange: [Math.PI / 5, Math.PI / 4],
depthRange: [6, 10],
},
Intricate: {
lengthRange: [0.09 * height, 0.1 * height],
weightRange: [0.01 * width, 0.015 * width],
angleOffsetRange: [Math.PI / 6, Math.PI / 5],
depthRange: [16, 20],
},
};
}
function selectByRarity(value: number, rarities: number[], names: string[]) {
let cumulative = 0;
for (let i = 0; i < rarities.length; i++) {
cumulative += rarities[i];
if (value < cumulative) return names[i];
}
return "";
}
function mapHashValue(hash: string, index: number[], range: number[]) {
const hashSegment = hash.slice(index[0], index[1]);
const hashValue =
parseInt(hashSegment, 16) / (Math.pow(16, index[1] - index[0]) - 1);
return range[0] + (range[1] - range[0]) * hashValue;
}
function getStartPoint(direction: number) {
if (direction === 0) return [width / 2, 0];
if (direction === 1) return [width / 2, height];
return [width / 2, height / 2];
}
function getInitialAngle(direction: number) {
return direction === 1 ? -Math.PI / 2 : Math.PI / 2;
}
function getColorFromHash(hash: string, palette: string[], index: number[]) {
const paletteIndex = Math.floor(
p.map(extract(hash, index), 0, 65535, 0, palette.length - 1)
);
return palette[paletteIndex];
}
function extract(hash: string, index: number[]) {
return parseInt(hash.slice(index[0], index[1]), 16);
}
//
// ███╗ ███╗███████╗████████╗ █████╗ ██████╗ █████╗ ████████╗ █████╗
// ████╗ ████║██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██╔══██╗
// ██╔████╔██║█████╗ ██║ ███████║██║ ██║███████║ ██║ ███████║
// ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║██╔══██║ ██║ ██╔══██║
// ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║██████╔╝██║ ██║ ██║ ██║ ██║
// ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
//
function generateMetadata(hashValues: HashValues) {
const formName = getFormName(hashValues.form, hashValues.symmetrical);
const metadata = {
name: `The ${hashValues.style} ${formName} of ${hashValues.colorPaletteName}`,
description: `Root Blocks Curated presents: Layers of Reality – Physical Matters. This artwork is called "The ${hashValues.style} ${formName} of ${hashValues.colorPaletteName}"`,
tokenId: id,
image: `img/${id}.png`,
attributes: [
{
trait_type: "Drop Name",
value: "Physical Matters",
},
{
display_type: "number",
trait_type: "Length",
value: Math.trunc(
(roundToTwoDecimal(hashValues.length) / height) * 1000
),
},
{
display_type: "number",
trait_type: "Thickness",
value: Math.trunc(
(roundToTwoDecimal(hashValues.weight) / width) * 1000
),
},
{
display_type: "number",
trait_type: "Depth",
value: hashValues.depth,
},
{
trait_type: "Style",
value: hashValues.style,
},
{
trait_type: "Form",
value: formName,
},
{
trait_type: "Color Palette",
value: hashValues.colorPaletteName,
},
{
trait_type: "Symmetrical",
value: hashValues.symmetrical ? "True" : "False",
},
{
display_type: "number",
trait_type: "Complexity",
value: hashValues.complexity,
},
],
hidden: [
{
trait_type: "Foreground 01",
value: hashValues.fgColor.start,
},
{
trait_type: "Foreground 02",
value: hashValues.fgColor.end,
},
{
trait_type: "Background 01",
value: hashValues.colorPalette.bg[0],
},
{
trait_type: "Background 02",
value: hashValues.colorPalette.bg[1],
},
{
trait_type: "Magic Hash",
value: [
"0x506E676B64732764206D676B796C2C",
"0x404778786D4B6F786E6C6A206675206A667A636D2014",
"0x6B6C6867207A6D7720786F7A726E",
"0x687A207368666C797A20627570616C2E",
],
},
],
};
if (formName == "Divergence" || formName == "Reflection") {
metadata.attributes.push({
trait_type: "Mirrored Style",
value: hashValues.mirStyle,
});
}
return metadata;
}
function getFormName(form: number, symmetrical: number) {
if (form === 0) return "Roots";
if (form === 1) return "Tree";
return symmetrical ? "Reflection" : "Divergence";
}
function roundToTwoDecimal(num: number) {
return Math.round((num + Number.EPSILON) * 100) / 100;
}
};
export default sketch;