Skip to content

Instantly share code, notes, and snippets.

@jcgregorio
Last active August 7, 2017 07:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jcgregorio/7d83bcd9670575ec6c1f9bd682f7b24a to your computer and use it in GitHub Desktop.
Save jcgregorio/7d83bcd9670575ec6c1f9bd682f7b24a to your computer and use it in GitHub Desktop.
L-Systems with Redux and StateReflector
(function () {
var dup = (o) => JSON.parse(JSON.stringify(o));
var $ = (id) => document.getElementById(id);
var canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d"),
width = canvas.width,
height = canvas.height;
var rules = {
"X": "F-[[X]+X]+F[+FX]-X",
"F": "FF",
"+": "+",
"-": "-",
"[": "[",
"]": "]",
}
var E = (s) => s ? (rules[s[0]] + E(s.substr(1))) : "";
var L = E(E(E(E(E("X")))));
function draw(x, y, len, angle) {
var p = { x: x, y: y, a: 3 };
var stack = [];
ctx.beginPath()
ctx.moveTo(p.x, p.y)
L.split("").forEach(function(ch) {
if (ch == "F") {
p.x += len*Math.sin(p.a);
p.y += len*Math.cos(p.a);
ctx.lineTo(p.x, p.y);
ctx.stroke();
} else if (ch == "-") {
p.a += angle;
} else if (ch == "+") {
p.a -= angle;
} else if (ch == "[") {
stack.push(dup(p));
} else if (ch == "]") {
p = stack.pop();
ctx.beginPath();
ctx.moveTo(p.x, p.y);
}
})
}
var defaultState = {
length: 7,
angle: 0.4,
};
var updateState = (state = defaultState, action) => {
if (action.type == "@@redux/INIT") {
} else if (action.type == "REPLACE_ALL") {
state = dup(action.value);
} else {
state = dup(state);
state[action.type] = action.value;
}
return state;
};
var store = Redux.createStore(updateState);
function render() {
var state = store.getState();
$('length').value = state.length;
$('angle').value = state.angle;
$('lengthDisplay').innerText = state.length;
$('angleDisplay').innerText = state.angle;
ctx.clearRect(0, 0, width, height);
draw(width/2, height, state.length, state.angle);
}
store.subscribe(render);
StateReflector.stateReflector(store, function(state) {
return {type: "REPLACE_ALL", value: state};
});
function dispatchFromEvent(id, event, xform) {
$(id).addEventListener(event, function(e) {
store.dispatch({
type: e.target.id,
value: xform(e),
});
});
}
dispatchFromEvent('length', 'input', (e) => +e.target.value);
dispatchFromEvent('angle', 'input', (e) => +e.target.value);
})();
// Copyright (c) 2017 The Chromium Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
this.StateReflector = this.StateReflector || {};
(function(sr) {
"use strict";
// A Promise that resolves when DOMContentLoaded has fired.
sr.DomReady = new Promise(function(resolve, reject) {
if (document.readyState != 'loading') {
// If readyState is already past loading then
// DOMContentLoaded has already fired, so just resolve.
resolve();
} else {
document.addEventListener('DOMContentLoaded', resolve);
}
});
// Namespace for utilities for working with URL query strings.
sr.query = {};
// fromObject takes an object and encodes it into a query string.
//
// The reverse of this function is toObject.
sr.query.fromObject = function(o) {
var ret = [];
Object.keys(o).sort().forEach(function(key) {
if (Array.isArray(o[key])) {
o[key].forEach(function(value) {
ret.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
})
} else if (typeof(o[key]) == 'object') {
ret.push(encodeURIComponent(key) + '=' + encodeURIComponent(sr.query.fromObject(o[key])));
} else {
ret.push(encodeURIComponent(key) + '=' + encodeURIComponent(o[key]));
}
});
return ret.join('&');
}
// toObject decodes a query string into an object
// using the 'target' as a source for hinting on the types
// of the values.
//
// "a=2&b=true"
//
// decodes to:
//
// {
// a: 2,
// b: true,
// }
//
// When given a target of:
//
// {
// a: 1.0,
// b: false,
// }
//
// Note that a target of {} would decode
// the same query string into:
//
// {
// a: "2",
// b: "true",
// }
//
// Only Number, String, Boolean, Object, and Array of String hints are supported.
sr.query.toObject = function(s, target) {
var target = target || {};
var ret = {};
var vars = s.split("&");
for (var i=0; i<vars.length; i++) {
var pair = vars[i].split("=", 2);
if (pair.length == 2) {
var key = decodeURIComponent(pair[0]);
var value = decodeURIComponent(pair[1]);
if (target.hasOwnProperty(key)) {
switch (typeof(target[key])) {
case 'boolean':
ret[key] = value=="true";
break;
case 'number':
ret[key] = Number(value);
break;
case 'object': // Arrays report as 'object' to typeof.
if (Array.isArray(target[key])) {
var r = ret[key] || [];
r.push(value);
ret[key] = r;
} else {
ret[key] = sr.query.toObject(value, target[key]);
}
break;
case 'string':
ret[key] = value;
break;
default:
ret[key] = value;
}
} else {
ret[key] = value;
}
}
}
return ret;
}
// Namespace for utilities for working with Objects.
sr.object = {};
// Returns true if a and b are equal, covers Boolean, Number, String and
// Arrays and Objects.
sr.object.equals = function(a, b) {
if (typeof(a) != typeof(b)) {
return false
}
var ta = typeof(a);
if (ta == 'string' || ta == 'boolean' || ta == 'number') {
return a === b
}
if (ta == 'object') {
if (Array.isArray(ta)) {
return JSON.stringify(a) == JSON.stringify(b)
} else {
return sr.query.fromObject(a) == sr.query.fromObject(b)
}
}
}
// Returns an object with only values that are in o that are different
// from d.
//
// Only works shallowly, i.e. only diffs on the attributes of
// o and d, and only for the types that sr.object.equals supports.
sr.object.getDelta = function (o, d) {
var ret = {};
Object.keys(o).forEach(function(key) {
if (!sr.object.equals(o[key], d[key])) {
ret[key] = o[key];
}
});
return ret;
};
// Returns a copy of object o with values from delta if they exist.
sr.object.applyDelta = function (delta, o) {
var ret = {};
Object.keys(o).forEach(function(key) {
if (delta.hasOwnProperty(key)) {
ret[key] = JSON.parse(JSON.stringify(delta[key]));
} else {
ret[key] = JSON.parse(JSON.stringify(o[key]));
}
});
return ret;
};
// Track the state of a page and reflect it to and from the URL.
//
// store - A Redux store.
// The state must be on Object and all the values in the Object
// must be Number, String, Boolean, Object, or Array of String.
// Doesn't handle NaN, null, or undefined.
//
// stateChange - A callback of the form function(state) that is called when
// state has been changed by a change in the URL, the return value
// should be appropriate for passing into store.dispatch();
sr.stateReflector = function(store, stateChange) {
var defaultState = store.getState();
var lastState = store.getState();
store.subscribe(function() {
var state = store.getState();
if (Object.keys(sr.object.getDelta(lastState, state)).length > 0) {
lastState = state;
var q = sr.query.fromObject(sr.object.getDelta(state, defaultState));
window.history.pushState(null, "", window.location.origin + window.location.pathname + "#" + q);
}
});
// stateFromURL should be called when the URL has changed, it updates the state.
var stateFromURL = function() {
var delta = sr.query.toObject(window.location.hash.slice(1), defaultState);
lastState = sr.object.applyDelta(delta, defaultState);
store.dispatch(stateChange(lastState));
}
sr.DomReady.then(stateFromURL);
// Every popstate event should also update the state.
window.addEventListener('popstate', stateFromURL);
}
})(this.StateReflector);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment