block by enjalot beed3e8491e29ad1b1f8347516a65fc9

character interpolation

Full Screen

Playing with the low dimensional vector produced in Neill Campbell’s Font Project

Inspired by watching his talk.

I’m only using data for one character as this is just a prototype. I can imagine a lot of ways to enhance the experience of exploring the low dimensional space like using parallel coordinates

Built with blockbuilder.org

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/numeric/1.2.6/numeric.min.js"></script>
  <!-- example data -->
  <script src="PerformProjections.js"></script>
  <script src="char_1_i.js"></script>
  
  <style>
    body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
    canvas {
      position: absolute;
      left: 350px;
    }
    
    #heatmap {
      position: absolute;
    }
    #heatmap img {
      position: absolute;
    }
    #pointer {
      position: absolute;
    }
    #line-control {
      position: absolute;
      bottom: 0;
    }
    
  </style>
</head>

<body>
  <div id="heatmap">
    <img src="//vecg.cs.ucl.ac.uk/Projects/projects_fonts/font_project/data/heatMap_char_1_i.jpg">
    <svg id="pointer"></svg>
  </div>
  <canvas></canvas>
  <svg id="line-control"></svg>
  <script>
    var width = 300;
    var height = 300;
    // hardcoded for the lowercase i
    var namespace = CharFuncs.char_1_i;
    
    var cursorX = 150;
    var cursorY = 150;
    
    var canvas = d3.select("canvas").node()
    canvas.width = width;
    canvas.height = height;
    var ctx = canvas.getContext('2d')
    
    var predictionData = {};
    var metricInfo = {};
    var xPoints = {};
    var xLimits = {};
    
    predictionData = namespace.getPredictionMatrices();
		
		xPoints = namespace.getXPoints();
		xLimits = namespace.getXLimits();
    
    var NK = 16; // TODO: automate this.
    /*
    // calculate the bounds of each element in K
    var minK = d3.range(NK).map(function() { return Infinity });
    var maxK = d3.range(NK).map(function() { return -Infinity })
    d3.range(300).forEach(function(j) {
      d3.range(300).forEach(function(i) {
        var point = getPoint(i, j)
        var instance = [point];
        var K = getK(instance, predictionData)[0];
        K.forEach(function(k, index) {
          if(k < minK[index]) minK[index] = k;
          if(k > maxK[index]) maxK[index] = k;
        })
      })
    })
    */
    // hard-code the min and max array based on running above code
    // saves execution time, double for loop is a bit expensive
    /*
    minK = d3.range(NK).map(function() { return 0 })
    maxK = d3.range(NK).map(function() { return 2.9179 })
    console.log("min", minK)
    console.log("max", maxK)
    */
    function renderPointer() {
      var svg = d3.select("#pointer")
      svg.attr({
        width: width,
        height: height
      })
      
      var point = svg.append("circle").classed("point", true)
        .datum([cursorX, cursorY])
      
      var drag = d3.behavior.drag()
      .on("drag", function() {
        var mouse = d3.mouse(svg.node())
        point.datum(mouse)
        .attr({
          cx: function(d) { return d[0] },
          cy: function(d) { return d[1] },
        })
        // rerender
        var p = getPoint(mouse[0], mouse[1])
        var instance = [p];
        var K = getK(instance, predictionData);
        renderControlLine(K);
        renderChar(K)
        
      })
      
      point
        .attr({
          cx: function(d) { return d[0] },
          cy: function(d) { return d[1] },
          r: 10,
        })
        .style({
          stroke: "#111",
          fill: "orange",
          cursor: "pointer"
        }).call(drag)
      
    }
    renderPointer();
    
    // render parcoords-like interface for playing with each
    // element in the K vector and rerendering
    function renderControlLine(K) {
      var cWidth = 600;
      var cHeight = 120;
      var cMargin = 10;
      var svg = d3.select("#line-control")
      .attr({
        width: cWidth,
        height: cHeight
      })
      
      var xscale = d3.scale.ordinal()
        .domain(d3.range(NK))
        .rangeBands([0, cWidth], 0.5)
      
      var yscale = d3.scale.linear()
      .domain([0, 2.9179]) // determined by above calculations
      .range([cMargin, cHeight - cMargin])
      
      /*
      function getY(d,i) {
        var yscale = d3.scale.linear()
        .domain([minK[i], maxK[i]])
        .range([cMargin, cHeight - cMargin])
        return yscale(d)
      }
      */
      var line = d3.svg.line()
      .x(function(d,i) {
        return xscale(i)
      })
      .y(function(d,i) {
        return yscale(d)
      })
      
      svg.selectAll("line.axis")
        .data(d3.range(NK))
      .enter().append("line").classed("axis", true)
      .attr({
        x1: function(d) { return xscale(d) },
        x2: function(d) { return xscale(d) },
        y1: cMargin,
        y2: cHeight - cMargin
      }).style({
        stroke: "#111"
      })
      
      var path = svg.selectAll("path.pc")
        .data(K)
      path.enter().append("path").classed("pc", true)
      path.attr("d", line)
      path.style({
        fill: "none",
        stroke: "#111"
      })
      
      
      
      // K is an array with 1 element, which is a 16 element array
      // that we actually care about
      var controls = svg.selectAll("circle.control")
        .data(K[0])
      controls.enter().append("circle").classed("control", true)
      
      var drag = d3.behavior.drag()
      .on("drag", function(d,i) {
        var dis = d3.select(this)
        var datum = dis.datum()
        var y = yscale(datum) + d3.event.dy;
        
        datum = yscale.invert(y)
        dis.datum(datum)
        dis.attr("cy", function(d,i) { return yscale(d) })
        
        var data = controls.data();
        path.datum(data)
        path.attr("d", line)
        
        renderChar([data])
      })
      
      
      controls.attr({
        cx: function(d,i) { return xscale(i) },
        cy: function(d,i) { return yscale(d) },
        r: 6
      }).style({
        cursor: "pointer",
        stroke: "#111",
        fill: "orange"
      }).call(drag)
      
    }
    var point = getPoint(cursorX, cursorY)
    var instance = [point];
    var K = getK(instance, predictionData);
    renderControlLine(K);
    
    function getPoint(x,y) {
      // convert from "screen" space to data space
      // screen is 300x300 from heatmap
      var xP = (x - 5.0) / width;
      var yP = (y - 5.0) / height;

      xP = (xP*(xLimits.xMax - xLimits.xMin)) + xLimits.xMin;
      yP = (yP*(xLimits.yMax - xLimits.yMin)) + xLimits.yMin;
      return [xP, yP]
    }
    
    function renderFromPoint(cursorX, cursorY) {
      var point = getPoint(cursorX, cursorY)
      var instance = [point];
      var K = getK(instance, predictionData);
      renderChar(K)
    }
    function renderChar(K) {
      ctx.clearRect(0, 0, width, height)
      ctx.strokeStyle = "#111";
      ctx.fillStyle = "orange";
      drawOutlineFromManifoldLocation(K, predictionData, ctx)
      ctx.stroke()
      ctx.fill();
    }
    
    renderFromPoint(cursorX, cursorY)
    /*
      lots of code borrowed directly from: //vecg.cs.ucl.ac.uk/Projects/projects_fonts/font_project/fontManifoldBrowser.js
    */
    function drawOutlineFromManifoldLocation(K, predictionData, context) {
      var S = performPredictionWithK(K, predictionData);
      //console.log("prediction curve", S)
      var y = S.y;
      var cx = S.cx;
      var cy = S.cy;
      var scale = 50;//*0.1333;
      function scalePtsX(x) {
        return ((x-cx)*scale+150+0);
      }
      function scalePtsY(y) {
        return (-(y-cy)*scale+150+0);
      }

      var yOffsetIdx = predictionData.N * predictionData.NumOutlines;

      var NumOutlinesToDraw = predictionData.NumOutlines;
      console.log("num outlines", NumOutlinesToDraw)

      context.beginPath();
      for (var outlineIdx = 0; outlineIdx < NumOutlinesToDraw; outlineIdx++) {
        var xOffsetIdx = predictionData.N * outlineIdx;
        context.moveTo(scalePtsX(y[xOffsetIdx][0]), scalePtsY(y[yOffsetIdx+xOffsetIdx][0]));
        for (var i = 1; i < predictionData.N; i++ ) {
          context.lineTo(scalePtsX(y[xOffsetIdx+i][0]), scalePtsY(y[yOffsetIdx+xOffsetIdx+i][0]));
        }
        context.lineTo(scalePtsX(y[xOffsetIdx][0]), scalePtsY(y[yOffsetIdx+xOffsetIdx][0]));
      }
      context.closePath();
    }
  </script>
</body>

PerformProjections.js

// from Font Project by Neill Campbell
// http://vecg.cs.ucl.ac.uk/Projects/projects_fonts/font_project/PerformPrediction.js
var CharFuncs = CharFuncs || {};

function size(A) { return numeric.dim(A); }

function computeK(x, X, alpha, gamma) {
	m = numeric.dim(x)[0];
	d = numeric.dim(x)[1];
	n = numeric.dim(X)[0];
	dd = numeric.dim(X)[1];

	if (d !== dd) { throw Error(); }

	xx = numeric.dot(numeric.mul(x,x),numeric.rep([d,1], 1));
	XX = numeric.dot(numeric.rep([1,d], 1), numeric.transpose(numeric.mul(X,X)));

	D1 = numeric.dot(xx, numeric.rep([1,n],1));
	D2 = numeric.dot(numeric.rep([m,1],1), XX);
	D3 = numeric.dot(numeric.mul(x, -2), numeric.transpose(X));

	DD = numeric.add(numeric.add(D1, D2), D3);

	K = numeric.mul(alpha, numeric.exp(numeric.mul(-0.5*gamma, DD)));

	return K;
}

function computeKArd(x, X, alpha, gamma) {
	m = numeric.dim(x)[0];
	d = numeric.dim(x)[1];
	n = numeric.dim(X)[0];
	dd = numeric.dim(X)[1];

	if (d !== dd) { throw Error(); }

	sqrtGamma = numeric.sqrt(numeric.transpose(gamma));

	X = numeric.mul(X, numeric.dot(numeric.rep([n,1], 1), sqrtGamma));

	xx = numeric.dot(numeric.mul(x,x),numeric.rep([d,1], 1));
	XX = numeric.dot(numeric.rep([1,d], 1), numeric.transpose(numeric.mul(X,X)));

	D1 = numeric.dot(xx, numeric.rep([1,n],1));
	D2 = numeric.dot(numeric.rep([m,1],1), XX);
	D3 = numeric.dot(numeric.mul(x, -2), numeric.transpose(X));

	DD = numeric.add(numeric.add(D1, D2), D3);

	K = numeric.mul(alpha, numeric.exp(numeric.mul(-0.5, DD)));

	return K;
}

function getK(x, data) {
  return computeKArd(x, data.X, data.alpha, data.gamma)
}

function performPrediction(x, data) {
	var K_xx_X

	if (data.gamma instanceof Array) {

		K_xx_X = computeKArd(x, data.X, data.alpha, data.gamma);

		yy_mean = numeric.dot(numeric.dot(data.Kinv, K_xx_X), data.Y);
	}
	else
	{
		console.log('RBF');

		K_xx_X = computeK(x, data.X, data.alpha, data.gamma);

		yy_mean = numeric.dot(numeric.dot(K_xx_X, data.Kinv), data.Y);
		
	}

	yy_mean = numeric.add(numeric.mul(yy_mean, data.Y_std), data.Y_mean);

	return numeric.transpose(yy_mean);
}

function performPredictionWithK(K_xx_X, data) {
  var y = numeric.dot(numeric.dot(data.Kinv, K_xx_X), data.Y)
	y = numeric.add(numeric.mul(y, data.Y_std), data.Y_mean);

	var yOffsetIdx = data.N * data.NumOutlines;

	ux = numeric.getBlock(data.Y_mean, [0,0], [0,yOffsetIdx-1]);
	uy = numeric.getBlock(data.Y_mean, [0,yOffsetIdx], [0,yOffsetIdx*2-1]);

	xMin = numeric.inf(ux);
	xMax = numeric.sup(ux);
	yMin = numeric.inf(uy);
	yMax = numeric.sup(uy);

	cx = 0.5 * (xMin + xMax);
	cy = 0.5 * (yMin + yMax);

	var S = {
		y: numeric.transpose(y),
		xMin: xMin,
		xMax: xMax,
		yMin: yMin,
		yMax: yMax,
		cx: cx,
		cy: cy,
	}

	return S;
}
function performPredictionWithBB(x, data) {
	var K_xx_X

	if (data.gamma instanceof Array) {

		K_xx_X = computeKArd(x, data.X, data.alpha, data.gamma);

		y = numeric.dot(numeric.dot(data.Kinv, K_xx_X), data.Y);
	}
	else
	{
		K_xx_X = computeK(x, data.X, data.alpha, data.gamma);

		y = numeric.dot(numeric.dot(K_xx_X, data.Kinv), data.Y);
	}
  y = numeric.add(numeric.mul(y, data.Y_std), data.Y_mean);

	var yOffsetIdx = data.N * data.NumOutlines;

	ux = numeric.getBlock(data.Y_mean, [0,0], [0,yOffsetIdx-1]);
	uy = numeric.getBlock(data.Y_mean, [0,yOffsetIdx], [0,yOffsetIdx*2-1]);

	xMin = numeric.inf(ux);
	xMax = numeric.sup(ux);
	yMin = numeric.inf(uy);
	yMax = numeric.sup(uy);

	cx = 0.5 * (xMin + xMax);
	cy = 0.5 * (yMin + yMax);

	var S = {
		y: numeric.transpose(y),
		xMin: xMin,
		xMax: xMax,
		yMin: yMin,
		yMax: yMax,
		cx: cx,
		cy: cy,
	}

	return S;

}