xxxxxxxxxx
208
/**
* Made by Sébastien Raynaud - Twitter @seb_raynaud
*
* Inspired by The Coding Train (Daniel Shiffman): https://youtu.be/Ggxt06qSAe4
* Color palette by Polyducks: https://lospec.com/palette-list/1bit-monitor-glow
*
* Adapted with continuous values (instead of only 0 or 1)
* by trilinear interpolation of an initial rule
*
* "pattern": group of 3 cells from the previous generation, e.g. (0, 1, 1)
* each pattern can be seen a coordinates in a 3D space (x, y, z)
* the value of the new cell can be seen as the value of the point at the pattern's coordinates in 3D space
* the 8 patterns of a rule set can be seen as the vertices of a cube of unit length in 3D space
* the values at these vertices are enough information to perform trilinear interpolation on the whole volume of the cube
* as a consequence, we can determine the value at any point inside the cube, e.g. at (0.5, 0.03, 0.655)
* in the cellular automata this translates to being able to determine the value of a new cell based on ancestors that have any decimal value between 0 and 1
*
* for every rule, the middle cell of the first row value is initialized at 0
* and the increases progressively up to 1 using an ease-in-ou-expo easing function
* the easing function is required to correctly see the behavior for values extremely close to 0 and to 1 (e.g. 0.999999)
*
* References:
* - Cellular automaton: https://mathworld.wolfram.com/ElementaryCellularAutomaton.html
* - Trilinear interpolation: https://paulbourke.net/miscellaneous/interpolation/
*/
/**
* Update the first group of constants as you wish and click the play icon on the top left corner
*/
const WIDTH = 400;
const HEIGHT = 400;
const INITIAL_RULE_BASE_10 = 0; // a number between 0 and 255, not used if using specific rules
const SPECIFIC_RULES = [18, 77, 161]; // Some interesting rules because showing all 256 rules is quite long. Other interesting rules: 30, 33, 50, 62, 113, 129, 165, 169, 182, 193, 201, 222, 225
const SHOULD_USE_SPECIFIC_RULES = false; // use a list of given rules instead of looping on all rules
const RESOLUTION = 10;
const FRAME_RATE = 24;
const BACKGROUND_COLOR = [34, 35, 35]; // [red, green, blue]
const CELL_COLOR = [240, 246, 240];
const MIDDLE_CELL_INITIAL_VALUE = 0;
const MIDDLE_CELL_VALUE_EASING_RATIO = 105;
const MIDDLE_CELL_SPAN = 0; // how many cells left and right to the middle cell also have the middleCellValue on the first row
const SHOULD_CONSTRAIN_BETWEEN_0_AND_1 = true;
const DURATION_PER_RULE_SECONDS = 4;
const SHOULD_SAVE_AS_GIF = false;
const WIDTH_CELLS = Math.floor(WIDTH / RESOLUTION);
const OFFSET_RATIO = 3;
const OFFSET_CELLS = Math.ceil(WIDTH_CELLS * OFFSET_RATIO);
const TOTAL_CELLS = WIDTH_CELLS + OFFSET_CELLS * 2;
const DURATION_PER_RULE_FRAMES = DURATION_PER_RULE_SECONDS * FRAME_RATE;
const GIF_DURATION_SECONDS = SPECIFIC_RULES.length * DURATION_PER_RULE_SECONDS;
let rows = [];
let middleCellValue = MIDDLE_CELL_INITIAL_VALUE;
let ruleBase10 = SHOULD_USE_SPECIFIC_RULES
? SPECIFIC_RULES[0]
: INITIAL_RULE_BASE_10;
function setup() {
createCanvas(WIDTH, HEIGHT);
frameRate(FRAME_RATE);
if (SHOULD_SAVE_AS_GIF) {
const durationSeconds = saveGif(
"cellular_automata_continuous_values",
GIF_DURATION_SECONDS
);
}
}
function draw() {
generateRows();
render();
updateParameters();
if (frameCount > 0 && frameCount % DURATION_PER_RULE_FRAMES === 0) {
ruleBase10 = SHOULD_USE_SPECIFIC_RULES
? SPECIFIC_RULES[SPECIFIC_RULES.indexOf(ruleBase10) + 1] ||
SPECIFIC_RULES[0]
: (ruleBase10 + 1) % 256;
console.log("next rule:", ruleBase10);
middleCellValue = MIDDLE_CELL_INITIAL_VALUE;
}
}
function updateParameters() {
// easing function: ease-in-out-expo
const t = (frameCount % DURATION_PER_RULE_FRAMES) / DURATION_PER_RULE_FRAMES;
if (t === 0 || t === 1) {
middleCellValue = t;
}
if (t < 0.5) {
middleCellValue =
0.5 *
pow(
2,
MIDDLE_CELL_VALUE_EASING_RATIO * t - MIDDLE_CELL_VALUE_EASING_RATIO / 2
);
} else {
middleCellValue =
1 -
0.5 *
pow(
2,
-MIDDLE_CELL_VALUE_EASING_RATIO * t +
MIDDLE_CELL_VALUE_EASING_RATIO / 2
);
}
}
/**
* @parameters {number} ruleBase10 - the rule in base 10, e.g. 73
* @returns a function that can interpolate the value from any (x,y,z) decimals, base on the rule
*/
function getTrilinearInterpolationFunction(ruleBase10) {
// ruleArrayBase2 is something like [1, 0, 0, 1, 0, 0, 1] for rule 73
// which are the values of the next generations for ancestors (0,0,0), (0,0,1), ...
const ruleArrayBase2 = ruleBase10
.toString(2)
.padStart(8, "0")
.split("")
.reverse()
.map((item) => +item);
const getValueFromAncestorsByTrilinearInterpolation = ({ x, y, z }) => {
// get all elements of the sum described in "Trilinear Interpolation" of a unit cube by Paul Bourke, then add them
// https://paulbourke.net/miscellaneous/interpolation
const value = ruleArrayBase2
.map((value, index) => {
const xProduct = index % 8 >= 4 ? x : 1 - x;
const yProduct = index % 4 >= 2 ? y : 1 - y;
const zProduct = index % 2 >= 1 ? z : 1 - z;
return value * xProduct * yProduct * zProduct;
})
.reduce((acc, current) => acc + current, 0);
if (value < 0 || value > 1) {
console.log("ruleBase10", ruleBase10);
}
return SHOULD_CONSTRAIN_BETWEEN_0_AND_1 ? constrain(value, 0, 1) : value;
};
return getValueFromAncestorsByTrilinearInterpolation;
}
function generateRows() {
const getValueFromAncestorsByTrilinearInterpolation = getTrilinearInterpolationFunction(
ruleBase10
);
const firstRow = Array.from(Array(TOTAL_CELLS)).map((_, i) => {
if (
i >= floor(TOTAL_CELLS / 2) - MIDDLE_CELL_SPAN &&
i <= floor(TOTAL_CELLS / 2) + MIDDLE_CELL_SPAN
) {
return middleCellValue;
}
return 0;
});
rows = [firstRow];
const totalRows = ceil(HEIGHT / RESOLUTION);
for (let i = 1; i < totalRows; i++) {
const previousRow = rows[i - 1];
if (!previousRow) {
return;
}
const newRow = Array.from(Array(TOTAL_CELLS)).map((_, i) => {
const left = previousRow[i - 1] || 0;
const middle = previousRow[i];
const right = previousRow[i + 1] || 0;
return getValueFromAncestorsByTrilinearInterpolation({
x: left,
y: middle,
z: right,
});
});
rows.push(newRow);
}
}
function render() {
background(BACKGROUND_COLOR);
const isEvenNumberOfCellsPerRow = WIDTH_CELLS % 2 === 0;
if (isEvenNumberOfCellsPerRow) {
translate(-floor(RESOLUTION / 2), 0);
}
rows.forEach((row, i) => {
row.forEach((cell, actualJIndex) => {
const isOffScreen =
actualJIndex < OFFSET_CELLS ||
actualJIndex > WIDTH_CELLS + OFFSET_CELLS;
if (isOffScreen) {
return;
}
const j = actualJIndex - OFFSET_CELLS;
const opacity = cell * 255;
fill(CELL_COLOR, opacity);
strokeWeight(0);
square(j * RESOLUTION, i * RESOLUTION, RESOLUTION);
});
});
}