Skip to content

Instantly share code, notes, and snippets.

@jcgregorio
Last active August 5, 2017 15:48
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/d3fae4dd7ebc3617cd669a855ae4ad9a to your computer and use it in GitHub Desktop.
Save jcgregorio/d3fae4dd7ebc3617cd669a855ae4ad9a to your computer and use it in GitHub Desktop.
D3 and L-Systems with Redux and StateReflector
(function () {
function E(s) { return s ? (rules[s[0]] + E(s.substr(1))) : "" }
rules = {
X: "F-[[X]+X]+F[+FX]-X",
F: "FF",
"+": "+",
"-": "-",
"[": "[",
"]": "]",
}
var start = {id: "0"};
var graph = {
"nodes": [start],
"links": [],
};
var n = 0;
var st = [start];
L = E(E(E(E(E("X"))))).split('').forEach(function(ch) {
if (ch == "[") {
n += 1;
var node = {id: ""+n};
st.push(node);
graph.nodes.push(node);
graph.links.push(
{
source: st[st.length-1].id,
target: st[st.length-2].id,
},
);
} else if (ch == "]") {
st.pop()
}
});
var canvas = document.querySelector("canvas"),
context = canvas.getContext("2d"),
width = canvas.width,
height = canvas.height;
var dup = (o) => JSON.parse(JSON.stringify(o));
var $ = (id) => document.getElementById(id);
var defaultState = {
linkStrength: 0.5,
linkDistance: 3,
chargeStrength: -1,
};
var linkForce = d3.forceLink().iterations(5).id(function(d) { return d.id; });
var charge = d3.forceManyBody();
var simulation = d3.forceSimulation()
.force("link", linkForce)
.force("charge", charge)
.force("center", d3.forceCenter(width / 2, height / 2));
var updateState = (state = defaultState, action) => {
if (action.type == "@@redux/INIT") {
} else if (action.type == "REPLACE_ALL") { // This is new.
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();
$('linkStrength').value = state.linkStrength;
$('linkDistance').value = state.linkDistance;
$('chargeStrength').value = state.chargeStrength;
$('linkStrengthDisplay').innerText = state.linkStrength;
$('linkDistanceDisplay').innerText = state.linkDistance;
$('chargeStrengthDisplay').innerText = state.chargeStrength;
linkForce.distance(state.linkDistance).strength(state.linkStrength);
charge.strength(state.chargeStrength);
simulation.alphaTarget(0.1).restart();
}
store.subscribe(render);
// This is new.
StateReflector.stateReflector(store, function(state) {
return {type: "REPLACE_ALL", value: state};
});
render();
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
d3.select(canvas)
.call(d3.drag()
.container(canvas)
.subject(dragsubject)
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function ticked() {
context.clearRect(0, 0, width, height);
context.beginPath();
graph.links.forEach(drawLink);
context.strokeStyle = "#343";
context.fillStyle = "#363";
context.stroke();
context.beginPath();
graph.nodes.forEach(drawNode);
context.fill();
}
function dragsubject() {
return simulation.find(d3.event.x, d3.event.y);
}
function dragstarted() {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d3.event.subject.fx = d3.event.subject.x;
d3.event.subject.fy = d3.event.subject.y;
}
function dragged() {
d3.event.subject.fx = d3.event.x;
d3.event.subject.fy = d3.event.y;
}
function dragended() {
if (!d3.event.active) simulation.alphaTarget(0);
d3.event.subject.fx = null;
d3.event.subject.fy = null;
}
function drawLink(d) {
context.moveTo(d.source.x, d.source.y);
context.lineTo(d.target.x, d.target.y);
}
function drawNode(d) {
context.moveTo(d.x + 3, d.y);
context.arc(d.x, d.y, 3, 0, 2 * Math.PI);
}
function dispatchFromEvent(id, event, xform) {
$(id).addEventListener(event, function(e) {
store.dispatch({
type: e.target.id,
value: xform(e),
});
});
}
dispatchFromEvent('linkDistance', 'input', (e) => +e.target.value);
dispatchFromEvent('linkStrength', 'input', (e) => Math.abs(+e.target.value));
dispatchFromEvent('chargeStrength', 'input', (e) => -Math.abs(+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