Back to Safe Sand · Material studies
SOURCE / PINNED RELEASE

Made of little things.

Safe Sand · Material studies

Release
9101a9cd9fb4…
Author-recorded commit
75d98e1b8edb…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-7682e55a791

Archive hash verified: 6c8ddabeb31100e1…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/engine/presets.js
// Adapted from av/safe-sand, e27bafa. See THIRD_PARTY.md for provenance.
const lin = (hex) => {
  const n = parseInt(hex.slice(1), 16);
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((c) => Math.pow(c / 255, 2.2));
};

function palette(entries) {
  const total = entries.reduce((s, e) => s + e[1], 0);
  const out = new Float32Array(32);
  let acc = 0;
  for (let i = 0; i < 8; i++) {
    const e = entries[Math.min(i, entries.length - 1)];
    if (i < entries.length) acc += e[1] / total;
    const c = lin(e[0]);
    out.set([c[0], c[1], c[2], i < entries.length ? acc : 1], i * 4);
  }
  out[(entries.length - 1) * 4 + 3] = 1;
  return out;
}

export const PRESETS = {
  river: {
    name: 'River',
    swatch: '#8a7d6b',
    palette: palette([
      ['#9f978b', 28], ['#877c6c', 22], ['#6b665f', 14], ['#363330', 8],
      ['#cdc7bb', 10], ['#86694f', 7], ['#a8968a', 5], ['#5f625d', 6],
    ]),
    size: [0.19, 0.36, 0.02, 0.00025],
    size2: [0.7, 2.6, 0.32, 0.7],
    fric: [0.22, 0.75, 0.58, 1.0],
  },
  beach: {
    name: 'Beach',
    swatch: '#d8bf8f',
    palette: palette([
      ['#e3cfa4', 30], ['#cdb183', 24], ['#b8935f', 14], ['#f2e8d4', 12],
      ['#8f7454', 8], ['#3b342c', 5], ['#c98f6e', 4], ['#e9ddc7', 3],
    ]),
    size: [0.17, 0.3, 0.012, 0.00012],
    size2: [0.6, 1.8, 0.3, 0.6],
    fric: [0.2, 0.7, 0.55, 1.2],
  },
  desert: {
    name: 'Desert',
    swatch: '#c77d45',
    palette: palette([
      ['#c98f5c', 30], ['#b97c4c', 24], ['#d6a273', 18], ['#9e6640', 12],
      ['#dfc095', 8], ['#7a4a2e', 5], ['#e8d3b0', 3],
    ]),
    size: [0.13, 0.22, 0.0, 0.0],
    size2: [0.6, 1.0, 0.3, 0.5],
    fric: [0.25, 0.7, 0.56, 0.5],
  },
  volcanic: {
    name: 'Volcanic',
    swatch: '#2b2a2a',
    palette: palette([
      ['#2b2a29', 34], ['#1a1919', 24], ['#45423f', 16], ['#5a5048', 8],
      ['#3c4334', 4], ['#0d0d0d', 7], ['#7d756c', 4],
    ]),
    size: [0.2, 0.34, 0.02, 0.0003],
    size2: [0.7, 2.4, 0.32, 0.7],
    fric: [0.25, 0.75, 0.6, 1.6],
  },
  coral: {
    name: 'Coral',
    swatch: '#efe4d6',
    palette: palette([
      ['#e6ddcf', 32], ['#d8cab5', 24], ['#e9c9bd', 12], ['#c7b293', 13],
      ['#f4eee4', 9], ['#b58f80', 6], ['#857868', 4],
    ]),
    size: [0.22, 0.4, 0.03, 0.00025],
    size2: [0.6, 2.2, 0.35, 0.8],
    fric: [0.25, 0.75, 0.6, 0.8],
  },
};

// shape (0 grain, 1 sphere, 2 filing, 3 flake, 4 crystal), cohesion,
// edge restitution, air/rolling drag. These are tactile approximations.
Object.assign(PRESETS, {
  snow: {
    name: 'Powder snow', swatch: '#d9e9f3',
    palette: palette([['#f6fbff', 45], ['#dcebf4', 30], ['#c2d9e9', 20], ['#ffffff', 5]]),
    size: [0.23, 0.36, 0.012, 0], size2: [0.6, 1, 0.4, 0.65],
    fric: [0.28, 0.88, 0.76, 0.9], material: [0, 0.65, 0.03, 7],
    description: 'Soft, clinging grains. Push slow ridges into a fresh snowfall.',
    trait: 'Soft · cohesive', surface: 'slate',
  },
  glass: {
    name: 'Glass beads', swatch: '#79c8bd',
    palette: palette([['#70c5bb', 25], ['#b3e7df', 25], ['#4e9caa', 18], ['#e4f9ed', 20], ['#578795', 12]]),
    size: [0.37, 0.12, 0, 0], size2: [0.6, 1, 0.4, 0.5],
    fric: [0.035, 0.13, 0.13, 2.8], material: [1, 0, 0.7, 0.12],
    description: 'Smooth little spheres. Tilt the tray and watch them roll and rebound.',
    trait: 'Rolling · bouncy', surface: 'dark',
  },
  iron: {
    name: 'Iron filings', swatch: '#8e98a8',
    palette: palette([['#6f7987', 30], ['#3d4855', 25], ['#aeb6c1', 20], ['#232e3b', 15], ['#d0d5dc', 10]]),
    size: [0.3, 0.22, 0, 0], size2: [0.6, 1, 0.3, 0.5],
    fric: [0.23, 0.65, 0.52, 2.5], material: [2, 0.06, 0.15, 1.2],
    description: 'Bring a magnet close. Fine metallic needles gather around your touch.',
    trait: 'Magnetic · metallic', surface: 'light',
  },
  glitter: {
    name: 'Prism glitter', swatch: '#bd85ce',
    palette: palette([['#cc83cb', 22], ['#71c6ca', 22], ['#efd48d', 18], ['#9c95d9', 18], ['#e4b4d5', 12], ['#eef2eb', 8]]),
    size: [0.27, 0.15, 0, 0], size2: [0.6, 1, 0.3, 0.5],
    fric: [0.14, 0.48, 0.38, 7], material: [3, 0, 0.12, 5],
    description: 'Light-catching flakes. Stir a shifting field of pink, gold and teal.',
    trait: 'Faceted · shimmering', surface: 'dark',
  },
  salt: {
    name: 'Rock salt', swatch: '#efc6b5',
    palette: palette([['#f5e6db', 32], ['#e4b8a7', 25], ['#f2d4c6', 23], ['#bc8e7b', 10], ['#ffffff', 10]]),
    size: [0.34, 0.25, 0.025, 0], size2: [0.6, 1, 0.44, 0.62],
    fric: [0.35, 0.82, 0.68, 1.7], material: [4, 0.08, 0.22, 1],
    description: 'Chunky pink crystals. Rough edges settle into steep, crunchy piles.',
    trait: 'Angular · coarse', surface: 'slate',
  },
  kinetic: {
    name: 'Kinetic sand', swatch: '#bc8cde',
    palette: palette([['#b18bc7', 35], ['#9671b3', 25], ['#ceaadf', 25], ['#78548f', 15]]),
    size: [0.19, 0.2, 0, 0], size2: [0.6, 1, 0.3, 0.5],
    fric: [0.4, 0.94, 0.92, 0.25], material: [0, 1.5, 0.015, 9],
    description: 'Sculptable, sticky grains. Carve deep channels and leave raised edges.',
    trait: 'Sculptable · sticky', surface: 'light',
  },
});

const descriptions = {
  river: ['A mix of smooth minerals and tiny pebbles. Draw a path through the riverbed.', 'Mixed · pebbly'],
  beach: ['Warm quartz grains, a few shells and the quiet pleasure of drawing in sand.', 'Fine · golden'],
  desert: ['Fine, rounded grains. Tilt gently to build flowing copper-colored dunes.', 'Fine · flowing'],
  volcanic: ['Dark, rough grains with tiny mineral glints. Sculpt a volcanic shoreline.', 'Rough · mineral'],
  coral: ['Pale, irregular grains with soft pink shell fragments.', 'Coarse · shelly'],
};
for (const [key, [description, trait]] of Object.entries(descriptions)) {
  Object.assign(PRESETS[key], { description, trait, surface: key === 'coral' ? 'slate' : 'light', material: [0, 0, 0.08, 0] });
}

export const PRESET_ORDER = ['river', 'beach', 'desert', 'volcanic', 'coral', 'snow', 'glass', 'iron', 'glitter', 'salt', 'kinetic'];