Memory material / 1
A deterministic integer model of a surface whose response changes with accumulated pressure. The reference rule is a starting point for artistic inquiry, not a physical material simulation.
Foundry Research / Published sourceHow to use it
Use replayMaterial(recipe) to evaluate the 48 × 32 grid. Each bounded press accumulates history; release resets displacement while retaining history. Compare materialEvidence(recipe) against the same events with memory set to zero. The browser runs this trusted reference implementation. Submitted instruments are never executed by this website.
Permanent link to this version ↗
Source SHA-256: cb0a50cb52d4b003aeb1db02b39d00542667ca9307609fbcc9d44676d0cc6607
Inspect the source
/**
* The Research reference material. This is trusted application code, not an
* interpreter for submitted programs. It has no I/O and imports no Foundry domain.
* Integer state and explicit events make a study replayable in the browser.
*/
export const MATERIAL_VERSION = 'memory-material/1';
export const MATERIAL_WIDTH = 48;
export const MATERIAL_HEIGHT = 32;
export const MATERIAL_MAX_EVENTS = 160;
export type MaterialEvent = {
kind: 'press' | 'release';
x: number;
y: number;
radius: number;
strength: number;
};
export type MaterialRecipe = {
version: typeof MATERIAL_VERSION;
memory: number;
events: MaterialEvent[];
};
export type MaterialState = {
displacement: number[];
history: number[];
};
export function emptyMaterial(): MaterialState {
return {
displacement: Array<number>(MATERIAL_WIDTH * MATERIAL_HEIGHT).fill(0),
history: Array<number>(MATERIAL_WIDTH * MATERIAL_HEIGHT).fill(0),
};
}
export function materialEvent(state: MaterialState, event: MaterialEvent, memory: number): MaterialState {
if (event.kind === 'release') {
return { displacement: state.displacement.map(() => 0), history: [...state.history] };
}
const displacement = [...state.displacement];
const history = [...state.history];
const radiusSquared = event.radius * event.radius;
for (let y = 0; y < MATERIAL_HEIGHT; y++) {
for (let x = 0; x < MATERIAL_WIDTH; x++) {
const distanceSquared = (x - event.x) ** 2 + (y - event.y) ** 2;
if (distanceSquared >= radiusSquared) continue;
const i = y * MATERIAL_WIDTH + x;
const force = Math.floor(event.strength * (radiusSquared - distanceSquared) / radiusSquared);
const remembered = Math.floor((state.history[i] ?? 0) * memory / 100);
// Repeated loading increases compliance. Past loading affects the NEXT load.
displacement[i] = Math.min(2000, (state.displacement[i] ?? 0) + Math.floor(force * (1000 + remembered) / 1000));
history[i] = Math.min(2000, (state.history[i] ?? 0) + force);
}
}
return { displacement, history };
}
export function replayMaterial(recipe: MaterialRecipe, through = recipe.events.length): MaterialState {
let state = emptyMaterial();
for (const event of recipe.events.slice(0, through)) state = materialEvent(state, event, recipe.memory);
return state;
}
export function materialEvidence(recipe: MaterialRecipe) {
const withMemory = replayMaterial(recipe);
const withoutMemory = replayMaterial({ ...recipe, memory: 0 });
const total = (values: number[]) => values.reduce((sum, value) => sum + value, 0);
return {
model_version: MATERIAL_VERSION,
event_count: recipe.events.length,
affected_cells: withMemory.history.filter((value) => value > 0).length,
displacement: total(withMemory.displacement),
control_displacement: total(withoutMemory.displacement),
response_difference: total(withMemory.displacement) - total(withoutMemory.displacement),
retained_history: total(withMemory.history),
units: 'model units; no claim about physical materials',
};
}
export const REFERENCE_RECIPE: MaterialRecipe = {
version: MATERIAL_VERSION,
memory: 85,
events: [
{ kind: 'press', x: 15, y: 14, radius: 10, strength: 460 },
{ kind: 'release', x: 0, y: 0, radius: 1, strength: 0 },
{ kind: 'press', x: 29, y: 19, radius: 11, strength: 540 },
{ kind: 'release', x: 0, y: 0, radius: 1, strength: 0 },
{ kind: 'press', x: 23, y: 16, radius: 12, strength: 600 },
],
};