(click to reload)
Sketch showing several possible variations of a single output from a generative handwriting model. The model first generates a single handwriting sample, then works backwards to show variants further and further into the past.
Based on my fork of Mike Bostock's branched random walk and hardmaru's RNN Tutorial for Artists.
xxxxxxxxxx
<meta charset="utf-8">
<canvas width="960" height="500"></canvas>
<script src="https://d3js.org/d3.v4.min.js"></script>
<!-- Note: numjs is not quite in cdnjs: https://github.com/cdnjs/cdnjs/pull/10019 -->
<script language="javascript" type="text/javascript" src="numjs.js"></script>
<script language="javascript" type="text/javascript" src="weights.js"></script>
<script language="javascript" type="text/javascript" src="model.js"></script>
<script>
var canvas = document.querySelector("canvas"),
context = canvas.getContext("2d"),
width = canvas.width,
height = canvas.height,
length = 300,
tail_every = 20,
color = d3.scaleSequential(d3.interpolateRainbow).domain([0, length])
x0 = 0.1 * width,
y0 = 0.5 * height,
pen_up0 = false,
mainWalk = null;
var is_rendering = false;
var last_render_time = 0;
var cur_index;
var do_abort = false;
render();
var redraw_check_interval = 1000; // one second in milliseconds
var redraw_interval = 30 * 1000;
function check_redraw(elapsed) {
var so_far = d3.now() - last_render_time;
if(so_far > redraw_interval && !is_rendering) {
// console.log("YES: " + so_far);
render();
}
d3.timeout(check_redraw, redraw_check_interval);
}
d3.timeout(check_redraw, redraw_check_interval);
canvas.onclick = click_check;
function click_check() {
if(is_rendering) {
do_abort = true;
context.clearRect(0, 0, width, height);
}
else {
render();
}
}
function render() {
if(is_rendering) {
return;
}
last_render_time = d3.now();
is_rendering = true;
context.clearRect(0, 0, width, height);
d3.timeout(render2, 25);
}
function render2(elapsed) {
if(do_abort) {
do_abort = false;
is_rendering = false;
render();
return;
}
mainWalk = handwritingWalk([[x0, y0, pen_up0, null]], length);
context.clearRect(0, 0, width, height);
context.lineJoin = "round";
context.lineCap = "round";
context.lineWidth = 5;
context.strokeStyle = "black";
context.globalAlpha = 0.7;
renderWalk(mainWalk);
context.globalCompositeOperation = "multiply";
context.lineWidth = 2;
cur_index = mainWalk.length - tail_every;
d3.timeout(render3, 25);
}
function render3(elapsed) {
if(do_abort) {
do_abort = false;
is_rendering = false;
render();
return;
}
var branchHistory = mainWalk.slice(0, cur_index+1);
tail_length = mainWalk.length - cur_index;
tail_chunk = Math.floor(tail_length / 20);
for (var j = 0; j < 1; ++j) {
context.strokeStyle = color(cur_index);
branchCopy = branchHistory.slice();
for (var k = 0, m = 20; k < m; ++k) {
context.globalAlpha = (m - k - 1) / m;
var pieceWalk;
pieceWalk = handwritingWalk(branchCopy, tail_chunk);
var pieceEnd = pieceWalk[pieceWalk.length - 1];
renderWalk(pieceWalk);
branchCopy.push(pieceEnd)
}
context.globalAlpha = 1;
}
cur_index -= tail_every;
if(cur_index <= tail_every) {
is_rendering = false;
}
else {
d3.timeout(render3, 25);
}
}
function renderWalk(walk) {
var i, n = walk.length;
context.beginPath();
context.moveTo(walk[0][0], walk[0][1]);
for (i = 1; i < n; ++i) {
// check for pen_up
if(walk[i-1][2]) {
context.moveTo(walk[i][0], walk[i][1]);
}
else {
context.lineTo(walk[i][0], walk[i][1]);
}
}
context.stroke();
}
// This works similiar to randomWalk.
function handwritingWalk(history, n, temperature) {
var points = new Array(n), i, x, y, pen_up;
var initial_state, rnn_state, temp_state;
if (temperature === undefined) {
temperature = 0.25;
}
// initialize the scale factor for the model. Bigger -> large outputs
Model.set_scale_factor(16.0);
// TODO: this is probably unnecessary
// initialize pen's states to zero.
Model.zero_input(); // the pen's states
points[0] = history[history.length - 1];
[x, y, pen_up, initial_state] = points[0];
temp_state = null;
// plug in history
if(initial_state === null) {
// initialize the rnn's initial states to zero
rnn_state = Model.random_state();
}
else {
rnn_state = Model.copy_state(initial_state)
}
// idea: could init from points only. eg:
// for(i=0; i<history.length; i++) {
// rnn_state = Model.update( /* compute dx, dy, pen here */, rnn_state);
// }
for(i=1; i<n; i++) {
// get the parameters of the probability distribution (pdf) from hidden state
pdf = Model.get_pdf(rnn_state);
// sample the next pen's states from our probability distribution
next_step = Model.sample(pdf, temperature);
// update state
rnn_state = Model.update(next_step, rnn_state);
temp_state = Model.copy_state(rnn_state);
// save path
points[i] = [
x += next_step[0],
y += next_step[1],
next_step[2],
temp_state
];
}
return points;
}
</script>
https://d3js.org/d3.v4.min.js