Skip to content

Instantly share code, notes, and snippets.

@scriptype
Last active May 23, 2017 01:07
Show Gist options
  • Save scriptype/b93329cb3b7ed0734607916a1c103c6a to your computer and use it in GitHub Desktop.
Save scriptype/b93329cb3b7ed0734607916a1c103c6a to your computer and use it in GitHub Desktop.
Elementary Cellular Automata
<!-- - - - - - - - - - - - - - - - -
(☞゚ヮ゚)☞ DOM as a database ☜(゚ヮ゚☜)
- - - - - - - - - - - - - - - - -->
<ul id="db">
<li id="explanations">
<ol>
<li id="rule-number">
<p>Takes a value between 0 and 255.</p>
<p>Try these: 30, 90, 110, 184, 225.</p>
<p>It's applied for each cell in the generated image. Learn more about elementary cellular automata <a href="https://en.wikipedia.org/wiki/Elementary_cellular_automaton" target="_blank">here</a>.</p>
<p>Also check out <a href="http://atlas.wolfram.com/01/01/" target="_blank">Wolfram Atlas</a> for some other interesting rules.</p>
</li>
<li id="initial-method">
<p>Rule for the first row of cells.</p>
<p>Since all the other cells will depend on the state of the first row, changing this rule will have a notable effect on the generated image.</p>
<p>Some rules goes well with some particular initialization rules. e.g. <a href="https://en.wikipedia.org/wiki/Rule_184">Rule184</a> is most interesting with random initialization.</p>
</li>
</ol>
</li>
</ul>
/*
* Info Popup component
*/
function InfoTemplate(content) {
return `
<div class="info-popup-container">
<span class="info-popup-icon">?</span>
<div class="info-popup">
${content}
</div>
</div>
`
}
/*
* Settings Form component
*/
function settingsTemplate(state) {
function propIfExists(key, value) {
return typeof value !== 'undefined' ?
`${key}="${value}"` : ''
}
var fields = Object.keys(state).reduce((acc, key) => {
var field = state[key]
return acc + `
<div class="field field--${field.type}">
<label class="field-label" for="${key}">
${field.label}
</label>
${ field.type == 'select' ? `
<select id="${key}" name="${key}">
${ field.options.reduce((a, opt) => a + `
<option
${ field.value == opt ? 'selected' : '' }
value="${opt}">
${opt}
</option>
`, '') }
</select>
` : `
<input
type="${field.type}"
id="${key}"
name="${key}"
${ propIfExists('value', field.value) }
${ propIfExists('min', field.min) }
${ propIfExists('max', field.max) }
${ propIfExists('required', field.required) } />
` }
${ field.explanation ?
InfoTemplate(field.explanation) :
'' }
</div>
`
}, '')
var footer = `
<div class="footer">
<input
class="btn"
type="submit"
value="Apply Changes" />
</div>
`
return fields + footer
}
/*
* Return the coressponding value if the penEnv
* matches any. penEnv comes from:
* https://codepen.io/pavlovsk/pen/jmXOmo
*/
function envValue(values) {
return values[window.penEnv] || values.actual
}
/*
* Informations about some fields in the settings
*/
var Explanations = {
ruleNumber:
db.querySelector('#explanations')
.querySelector('#rule-number')
.innerHTML,
initialMethod:
db.querySelector('#explanations')
.querySelector('#initial-method')
.innerHTML
}
/*
* Fields used in settings form. These values
* will be updated whenever the form is submitted.
*/
var UIState = {
cellSize: {
label: 'Cell Size (px)',
type: 'number',
value: envValue({
picked: 8,
featured: 20,
thumbnail: 8,
actual: 1
}),
min: 1,
max: 999,
required: true
},
rowLength: {
label: 'Cells in a row',
type: 'number',
value: envValue({
picked: 50,
featured: 60,
thumbnail: 50,
actual: 480
}),
min: 1,
max: 99999,
required: true
},
steps: {
label: 'Steps',
type: 'number',
value: envValue({
picked: 60,
featured: 31,
thumbnail: 60,
actual: 640
}),
min: 1,
max: 99999,
required: true
},
ruleNumber: {
label: 'Rule Number',
type: 'number',
explanation: Explanations.ruleNumber,
value: envValue({
picked: 110,
featured: 30,
thumbnail: 110,
actual: 110
}),
min: 0,
max: 255,
required: true
},
initial: {
label: 'Initialization method',
type: 'select',
value: envValue({
picked: 'random',
featured: 'middle',
thumbnail: 'random',
actual: 'last'
}),
options: ['random', 'first', 'middle', 'last'],
explanation: Explanations.initialMethod,
required: true
},
bgColor: {
label: 'Color for 0\'s',
type: 'color',
value: '#000000',
required: true
},
fgColor: {
label: 'Color for 1\'s',
type: 'color',
value: '#ffffff',
required: true
}
}
/*
* Retrieve the ui state variable with the key.
*/
function ui(key) {
return UIState[key].value
}
/*
* Generate a new row of cells based on the old cells
* and the given rule.
*/
function generateCells(cells, rule) {
return new Array(cells.length)
.join(' ')
.split(' ')
.map((cell, i) => {
var prev = cells[i - 1] || 0
var curr = cells[i] || 0
var next = cells[i + 1] || 0
return rule(+prev, +curr, +next, i, cells.length)
})
}
/*
* Get a number 0-7 (inclusive) from the 3 bits
* e.g.: (1, 0, 1) => 5
*/
function getStateInt(prev, curr, next) {
return parseInt([prev, curr, next].join(''), 2)
}
/*
* Returns the binary representation of the
* ruleNumber, in reversed order. This allows
* making a direct lookup using a base-8 ([0-7])
* integer representing the state. For example,
* the bit for the state '101' is located at the
* index 5, which translates to 101 in binary.
*
* e.g.: 110 => [0, 1, 1, 1, 0, 1, 1, 0]
* | | | | | | | |
* 000 | 010 | 100 | 110 |
* 001 011 101 111
*
* or: 3 => [1, 1, 0, 0, 0, 0, 0, 0]
*/
function getRule(ruleNumber) {
return ('0'.repeat(8) + ruleNumber.toString(2))
.slice(-8)
.split('')
.reverse()
.map(Number)
}
var Rules = {
random(density) {
return _ => +(Math.random() <= density)
},
first(prev, curr, next, index, total) {
return +(index == 0)
},
middle(prev, curr, next, index, total) {
return +(index == Math.floor(total / 2))
},
last(prev, curr, next, index, total) {
return +(index == total - 1)
},
/*
* Finds the current state (prev, curr, next) of
* an individual cell inside the rule, and apply
* the found bit to the cell.
*
* Called in every turn of automata loop for each
* cell to transform the cell into a new state.
*
* State is represented as a base-8 integer [0-7]
* and used as index inside the rule.
*/
number(ruleNumber) {
return (prev, curr, next) => {
var state = getStateInt(prev, curr, next)
var rule = getRule(ruleNumber)
return rule[state]
}
}
/*
90(prev, curr, next) {
return prev ^ next
}
*/
}
var settingsForm = document.createElement('form')
settingsForm.classList.add('settings-form')
settingsForm.innerHTML = settingsTemplate(UIState)
settingsForm.addEventListener('submit', e => {
e.preventDefault()
var formData = new FormData(settingsForm)
Object.keys(UIState).forEach(field => {
var value = formData.get(field)
if (UIState[field].type == 'number') {
value = +value
}
UIState[field].value = value
})
reset()
})
document.body.appendChild(settingsForm)
/*
* Variables that make up app state.
*/
var canvas = null,
canvasWidth = null,
canvasHeight = null,
ctx = null,
render = null,
currentRow = null,
cells = null
/*
* Start everything from scratch with the current states.
*/
function reset() {
if (!canvas) {
canvas = document.createElement('canvas')
document.body.appendChild(canvas)
}
canvasWidth = ui('cellSize') * ui('rowLength')
canvasHeight = ui('cellSize') * ui('steps')
canvas.width = canvasWidth
canvas.height = canvasHeight
ctx = canvas.getContext('2d')
currentRow = 0
cells = []
cancelAnimationFrame(render)
render = requestAnimationFrame(draw)
}
/*
* Animation frame
*/
function draw() {
if (currentRow >= ui('steps')) {
return
}
// Generate initial row
if (currentRow == 0) {
var initialMethod = ui('initial')
var initial = Rules[initialMethod]
if (initialMethod == 'random') {
initial = initial(.5)
}
cells = generateCells(
new Array(ui('rowLength')),
initial
)
// Generate all the remaining cells
} else {
cells = generateCells(
cells,
Rules.number(ui('ruleNumber'))
)
}
// Draw the cells
var styles = [ ui('bgColor'), ui('fgColor') ]
var cellSize = ui('cellSize')
cells.forEach((cell, index) => {
ctx.fillStyle = styles[cell]
ctx.fillRect(
cellSize * index,
cellSize * currentRow,
cellSize,
cellSize
)
})
currentRow++
render = requestAnimationFrame(draw)
}
/*
* Kick off first render.
*/
reset()
<script src="https://codepen.io/pavlovsk/pen/jmXOmo"></script>
html {
height: 100%;
}
body {
display: flex;
min-height: 100%;
font: 100 normal 16px/1.6 georgia, times, serif;
background: #111;
}
#db {
display: none;
}
canvas {
display: block;
margin: 0 auto;
align-self: flex-start;
}
.btn {
font-size: inherit;
width: 140px;
}
.info-popup-container {
display: inline-block;
position: relative;
.info-popup-icon {
display: inline-block;
width: 16px;
height: 16px;
font: bold 12px/16px helvetica, sans-serif;
text-align: center;
border-radius: 50%;
background: #2a292c;
color: #aaa9ac;
}
.info-popup {
display: none;
position: absolute;
left: 50%;
transform: translateX(-50%);
z-index: 1;
width: 300px;
padding: 1.5em 2em;
font-size: 1em;
background: #0a090c;
color: #e0e6ea;
border: 1px solid #000;
box-shadow: inset 0 0 1px 0 #444;
p:not(:last-child) {
margin-bottom: 1em;
}
a {
color: #2066fa;
}
}
&:hover {
&::after {
content: "";
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 400%;
height: 100%;
}
.info-popup-icon {
background: #fff;
color: #000;
}
.info-popup {
display: block;
}
}
}
.settings-form {
display: flex;
flex-direction: column;
padding-top: 2em;
padding-left: 2.618em;
min-width: 300px;
background: #000;
color: #f0f0f0;
border-right: 5px solid #343436;
.footer { }
.field {
font-size: 1em;
padding-bottom: .5em;
&-label {
display: inline-block;
width: 180px;
&::after {
content: ":"
}
}
input {
background: none;
color: inherit;
border: none;
font: inherit;
vertical-align: middle;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment