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.
// 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'];
