To speed things up, I'm going to apply refactorings that we've already covered without much explanation, and if some graphs don't show any unique points that I want to make, I'll skip over them. When I first wrote the DSP series, I was trying to focus on the explanations for all of the signal processing techniques, and I got lazy about writing clean code for the graphs. This led to way more copy-and-pasting than there should have been, so a lot of the refactorings will be terribly repetitive. I don't want to show them all, and I'm sure you don't want to read about them all, so we'll stick to the highlights. With that explanation out of the way, let's pick up where we left off with the signal transforms post.
Annotations and Animation Frames
After applying previous refactorings, the code for the first graph, showing how changing an offset changes a signal, looks like this:
$(function() {
  var renderer = dsp_graph.initCanvas('#canvas-sine-offset', onClick);
  var stage = new PIXI.Container();
  dsp_graph.drawZeroAxis(stage);
  var t = 0;
  var amplitude = 2;
  var offset = 0;
  var freq = 1;
  var phase = 0;
  var baseSine = dsp_graph.createCurve(stage);
  dsp_graph.drawSine(baseSine, 0xeeeeee, amplitude, offset, freq, phase);
  var sinewave = dsp_graph.createCurve(stage);
  var baseText = new PIXI.Text('f(t) = sin(t)', { font: 'italic 14px Arial', fill: '#eeeeee' });
  baseText.x = 100;
  baseText.y = 125;
  stage.addChild(baseText);
  var sineText = new PIXI.Text('f(t) = sin(t) + ' + offset, { font: 'italic 14px Arial', fill: '#00bbdd' });
  sineText.x = 100;
  sineText.y = 20;
  stage.addChild(sineText);
  function onClick() {
    if (offset == 0) offset = 2;
    else if (offset == 2) offset = -2;
    else offset = 0;
    sineText.text = 'f(t) = sin(t) + ' + offset;
  }
  (function animate() {
    if (t > offset + 0.01) t -= 0.04;
    else if (t < offset - 0.01) t += 0.04;
    sinewave.clear();
    dsp_graph.drawSine(sinewave, 0x00bbdd, amplitude, t, freq, phase);
    renderer.render(stage);
    requestAnimationFrame( animate );
  }());
});    annotate: function(stage, text, color, origin) {
      var annotation = new PIXI.Text(text, { font: 'italic 14px Arial', fill: color });
      annotation.x = origin.x;
      annotation.y = origin.y;
      stage.addChild(annotation);
      return annotation;
    }  var baseText = dsp_graph.annotate(stage, 'f(t) = sin(t)', '#eeeeee', {x:100, y:125});
  var sineText = dsp_graph.annotate(stage, 'f(t) = sin(t) + ' + offset, '#00bbdd', {x:100, y:20});  function animate() {
    var update = true;
    if (t > offset + 0.01) t -= 0.04;
    else if (t < offset - 0.01) t += 0.04;
    else update = false;
    sinewave.clear();
    dsp_graph.drawSine(sinewave, 0x00bbdd, amplitude, t, freq, phase);
    renderer.render(stage);
    if (update) requestAnimationFrame( animate );
  }
  animate();  function onClick() {
    if (offset == 0) offset = 2;
    else if (offset == 2) offset = -2;
    else offset = 0;
    sineText.text = 'f(t) = sin(t) + ' + offset;
    animate();
  }These same changes can be applied to the other graph code in this post, and then they all look pretty good, so after committing the small addition we made to the API, we can move on to the next post that covers sampling.
Extracting Sampling Graph Primitives
In the post on DSP sampling, there are two graphing primitives that we can add to our API feature set. The first one is a primitive that draws little sample circles on a moving sine curve, and the second one is a primitive that draws an animated blip on a moving sine curve as it passes a certain point. These animation features can be seen in the following graph:
The code for the animation of this graph is as follows:
  (function animate() {
    t += 0.04;
    if (t > 20) t = 0;
    sinewave.clear();
    dsp_graph.drawSine(sinewave, 0x888888, amplitude, offset, freq, t, 0, 10);
    var y = amplitude*Math.sin((10*freq + t)/10.0*Math.PI);
    sinewave.lineStyle(2/26.0, 0xeeeeee, 1);
    sinewave.moveTo(10,0);
    sinewave.lineTo(10,y);
    // Draw the sampling blip
    sinewave.drawCircle(10,y,2/26.0);
    var sample_offset = (t/2 - Math.floor(t/2))*2;
    if (sample_offset <= 0.3) {
      y = amplitude*Math.sin((10*freq + t - sample_offset)/10.0*Math.PI);
      sinewave.drawCircle(10 - sample_offset,y,sample_offset);
    }
    dsp_graph.drawSine(sinewave, 0x00bbdd, amplitude, offset, freq, t, 10, 20);
    // Draw the sample circles
    for (var x = 2 - sample_offset; x <= 10; x+=2) {
      y = amplitude*Math.sin((x*freq + t)/10.0*Math.PI);
      sinewave.drawCircle(x, y, 4/26.0);
    }
    renderer.render(stage);
    requestAnimationFrame( animate );
  }());    drawSamples: function(sinewave, amplitude, freq, phase, start, end, step) {
      for (var x = start; x <= end; x+=step) {
        var y = amplitude*Math.sin((x*freq + phase)/UNIT_FREQ*Math.PI);
        sinewave.drawCircle(x, y, 4/TICK_STEP);
      }
    },
    drawSampleBlip: function(sinewave, amplitude, freq, phase, sample_offset, x) {
      var y = amplitude*Math.sin((x*freq + phase)/UNIT_FREQ*Math.PI);
      sinewave.drawCircle(x,y,2/TICK_STEP);
      if (sample_offset <= 0.3) {
        y = amplitude*Math.sin((x*freq + phase - sample_offset)/UNIT_FREQ*Math.PI);
        sinewave.drawCircle(x - sample_offset, y, sample_offset);
      }
    },  (function animate() {
    t += 0.04;
    if (t > 20) t = 0;
    sinewave.clear();
    dsp_graph.drawSine(sinewave, 0x888888, amplitude, offset, freq, t, 0, 10);
    var y = amplitude*Math.sin((10*freq + t)/10.0*Math.PI);
    sinewave.lineStyle(2/26.0, 0xeeeeee, 1);
    sinewave.moveTo(10,0);
    sinewave.lineTo(10,y);
    var sample_offset = (t/2 - Math.floor(t/2))*2;
    dsp_graph.drawSampleBlip(sinewave, amplitude, freq, t, sample_offset, 10);
    dsp_graph.drawSine(sinewave, 0x00bbdd, amplitude, offset, freq, t, 10, 20);
    dsp_graph.drawSamples(sinewave, amplitude, freq, t, 2 - sample_offset, 10, 2);
    renderer.render(stage);
    requestAnimationFrame( animate );
  }());Cleaning up the Averaging Post
At this point it should be clear that most of the refactoring just involves identifying functions that can be extracted into the API, and tweaking them a bit to work generally for a wider variety of graphs. We're looking for primitive graph features that we can stick in a commonly accessible place, and then use these primitives in different combinations to create the different animated graphs with less code. The graphs in the averaging post are no different. I even had started extracting functions in the code for this post when I first wrote it, but I didn't pull them all the way into the API module. I lazily left them in each graph's drawing code and then copied them from one graph to another along with all of the rest of the code. That was bad and probably cost me time in the long run, but we're going to fix it now. Here's the code for the first average graph as it stands after the refactorings up to this point:
$(function() {
  var renderer = dsp_graph.initCanvas('#canvas-gas-average', onClick);
  var stage = new PIXI.Container();
  var x_labels = ['9/1997','9/1999','9/2001','9/2003','9/2005','9/2007','9/2009','9/2011','9/2013']
  var y_labels = ['$1','$2','$3','$4','$5']
  dsp_graph.drawPositiveAxis(stage, x_labels, y_labels);
  function Mean(ary) {
    return ary.reduce(function(a, b) { return a + b; }) / ary.length;
  }
  var t = 0;
  var state = 0;
  var avg = Mean(gas_prices);
  var avg_gas_prices = gas_prices.slice();
  var graph = dsp_graph.createCurve(stage);
  graph.lineStyle(2.0/26.0, 0x888888, 1);
  drawPoints(graph, gas_prices);
  var avg_graph = dsp_graph.createCurve(stage);
  function onClick() {
    if (state == 0) state = 1;
    else state = 0;
    animate();
  }
  function drawPoints(graphics, ary) {
    var x = 0;
    var step = 20.0 / ary.length;
    graphics.moveTo(x, ary[0]*2);
    ary.forEach(function(y) {
      graphics.lineTo(x, y*2);
      graphics.moveTo(x, y*2);
      x += step;
    })
  }
  function animate() {
    if (state == 1) {
      t += 0.01;
      if (t < 1) {
        avg_gas_prices = gas_prices.map(function(price) { return price - t*(price - avg) })
      } else {
        avg_gas_prices = avg_gas_prices.fill(avg)
      }
    } else {
      if (t > 0) t = 0;
      avg_gas_prices = gas_prices.slice();
    }
    avg_graph.clear();
    avg_graph.lineStyle(2/26.0, 0x00bbdd, 1);
    drawPoints(avg_graph, avg_gas_prices);
    renderer.render(stage);
    console.log("animate()")
    if (state == 1 && t < 1.2) requestAnimationFrame( animate );
  }
  animate();
});    mean: function(ary) {
      return ary.reduce(function(a, b) { return a + b; }) / ary.length;
    },    drawYPoints: function(curve, color, points) {
      curve.lineStyle(2.0/TICK_STEP, color, 1);
      var x = 0;
      var step = 20.0 / points.length;
      curve.moveTo(x, points[0]*2);
      points.forEach(function(y) {
        curve.lineTo(x, y*2);
        curve.moveTo(x, y*2);
        x += step;
      })
    },$(function() {
  var renderer = dsp_graph.initCanvas('#canvas-gas-average', onClick);
  var stage = new PIXI.Container();
  var x_labels = ['9/1997','9/1999','9/2001','9/2003','9/2005','9/2007','9/2009','9/2011','9/2013']
  var y_labels = ['$1','$2','$3','$4','$5']
  dsp_graph.drawPositiveAxis(stage, x_labels, y_labels);
  var t = 0;
  var state = 0;
  var avg = dsp_graph.mean(gas_prices);
  var avg_gas_prices = gas_prices.slice();
  var graph = dsp_graph.createCurve(stage);
  dsp_graph.drawYPoints(graph, 0x888888, gas_prices);
  var avg_graph = dsp_graph.createCurve(stage);
  function onClick() {
    if (state == 0) state = 1;
    else state = 0;
    animate();
  }
  function animate() {
    if (state == 1) {
      t += 0.01;
      if (t < 1) {
        avg_gas_prices = gas_prices.map(function(price) { return price - t*(price - avg) })
      } else {
        avg_gas_prices = avg_gas_prices.fill(avg)
      }
    } else {
      if (t > 0) t = 0;
      avg_gas_prices = gas_prices.slice();
    }
    avg_graph.clear();
    dsp_graph.drawYPoints(avg_graph, 0x00bbdd, avg_gas_prices);
    renderer.render(stage);
    console.log("animate()")
    if (state == 1 && t < 1.2) requestAnimationFrame( animate );
  }
  animate();
});    filter: function(ary, taps) {
      if (ary.length < taps.length) {
        var gain = taps.slice(0, ary.length).reduce(function(a, b) { return a + b; })
        taps = taps.map(function(a) { return a / gain })
      }
      return ary.reduce(function(a, b, i) { return a + b*taps[i] }, 0)
    },
    genFilter: function(n) {
      var taps = []
      for (var i = 0.5-n/2.0; i < n/2.0; i+=1.0) {
        taps.push(Math.sin(Math.PI*i/26.0) / (Math.PI*i/26.0))
      }
      var gain = taps.reduce(function(a, b) { return a + b; })
      taps = taps.map(function(a) { return a / gain })
      return taps
    },The earlier and the better you refactor your code, the easier it is to write more code and make faster progress. I've learned those lessons over again while doing this series of posts, and I'm kicking myself for not doing most of these refactorings while I was writing the original DSP series. I would have had a much easier time of it, and I wouldn't have spent so much time copy-and-pasting and doing little tweaks to code that was very similar from one graph to the next. When I was in the middle of it, I didn't want to take the time to clean things up; I just wanted to get each graph done and move onto the next one. But I paid for it in a messy code base, and lots of wasted time trudging through hundreds of lines of repetitive code. Don't make the same mistake. Refactor early, and well.
No comments:
Post a Comment