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;