block by nitaku d79632a53187f8e92b15

Freehand drawing

Full Screen

A simple freehand drawing application, based on Bostock’s Line Drawing gist.

Use your stylus, fingers or mouse to draw. The color of the line can be changed by interacting with the color palette, and the canvas can be cleared by clicking the trash in the upper-right corner of the UI.

The application uses two stacked SVG elements, one for the UI and one for the canvas. This is used to disable drawing when interacting with UI elements.

Unlike Bostock’s example, this application maintains a DOM-independent object to store all the drawing’s data (just look at the JavaScript console each time you complete a line).

Colors are from Colorbrewer’s Dark2 palette.

index.js

// Generated by CoffeeScript 1.4.0
(function() {
  var SWATCH_D, active_color, active_line, canvas, drag, drawing_data, lines_layer, palette, redraw, render_line, swatches, trash_btn, ui;

  SWATCH_D = 22;

  render_line = d3.svg.line().x(function(d) {
    return d[0];
  }).y(function(d) {
    return d[1];
  }).interpolate('basis');

  drawing_data = {
    lines: [
      {
        color: "#1b9e77",
        points: [[451, 448], [447, 447], [442, 445], [430, 440], [417, 433], [411, 429], [405, 424], [398, 420], [385, 410], [372, 398], [366, 392], [360, 385], [354, 378], [348, 370], [343, 362], [338, 354], [333, 346], [329, 337], [326, 328], [322, 318], [319, 309], [316, 299], [314, 289], [313, 279], [312, 269], [312, 259], [314, 239], [316, 228], [317, 218], [319, 209], [322, 199], [326, 190], [330, 181], [334, 172], [339, 164], [344, 156], [350, 149], [355, 143], [362, 136], [368, 131], [375, 125], [383, 120], [390, 115], [398, 111], [406, 107], [423, 99], [431, 96], [440, 93], [449, 90], [458, 87], [467, 86], [495, 83], [505, 83], [514, 83], [524, 84], [534, 85], [543, 87], [552, 89], [566, 95], [578, 101], [589, 106], [598, 112], [607, 118], [615, 125], [622, 131], [629, 138], [635, 146], [641, 153], [652, 169], [661, 185], [665, 194], [671, 210], [674, 218], [676, 226], [677, 234], [678, 241], [679, 249], [678, 264], [677, 271], [676, 279], [674, 286], [671, 293], [664, 307], [660, 313], [655, 320], [650, 326], [638, 338], [631, 344], [624, 349], [609, 359], [593, 367], [585, 370], [567, 375], [558, 378], [548, 379], [529, 382], [511, 383], [493, 382], [484, 381], [475, 379], [450, 371], [443, 367], [435, 363], [428, 358], [422, 353], [416, 347], [406, 335], [398, 321], [395, 314], [393, 307], [389, 292], [388, 277], [389, 261], [393, 239], [398, 226], [408, 207], [412, 202], [417, 197], [422, 192], [427, 188], [438, 181], [449, 176], [462, 172], [475, 169], [494, 168], [508, 168], [521, 170], [539, 176], [551, 182], [556, 185], [561, 188], [570, 196], [576, 205], [583, 220], [585, 224], [586, 231], [587, 243], [587, 255], [586, 260], [583, 270], [581, 275], [578, 279], [573, 285], [567, 290], [557, 297], [546, 302], [530, 307], [519, 309], [507, 309], [491, 307], [480, 304], [471, 299], [467, 296], [463, 294], [456, 287], [449, 276], [448, 272], [447, 268], [446, 260], [447, 252], [452, 240], [456, 232], [462, 226], [469, 220], [476, 217], [485, 214], [498, 211], [506, 211], [514, 213], [525, 218], [531, 222], [534, 225], [536, 227], [540, 233], [543, 239], [544, 245], [544, 253], [542, 257], [537, 263], [535, 265], [529, 269], [524, 271], [521, 271], [519, 271], [517, 272], [511, 271], [507, 270], [502, 267], [501, 265], [497, 261], [496, 259], [494, 257], [491, 245], [492, 242], [493, 239], [497, 237]]
      }
    ]
  };

  active_line = null;

  active_color = "#333333";

  canvas = d3.select('#canvas');

  lines_layer = canvas.append('g');

  ui = d3.select('#ui');

  palette = ui.append('g').attr({
    transform: "translate(" + (4 + SWATCH_D / 2) + "," + (4 + SWATCH_D / 2) + ")"
  });

  swatches = palette.selectAll('swatch').data(["#333333", "#ffffff", "#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e", "#e6ab02", "#a6761d", "#666666"]);

  trash_btn = ui.append('text').html('').attr({
    "class": 'btn',
    dy: '0.35em',
    transform: 'translate(940,20)'
  }).on('click', function() {
    drawing_data.lines = [];
    return redraw();
  });

  swatches.enter().append('circle').attr({
    "class": 'swatch',
    cx: function(d, i) {
      return i * (SWATCH_D + 4) / 2;
    },
    cy: function(d, i) {
      if (i % 2) {
        return SWATCH_D;
      } else {
        return 0;
      }
    },
    r: SWATCH_D / 2,
    fill: function(d) {
      return d;
    }
  }).on('click', function(d) {
    active_color = d;
    swatches.classed('active', false);
    return d3.select(this).classed('active', true);
  });

  swatches.each(function(d) {
    if (d === active_color) {
      return d3.select(this).classed('active', true);
    }
  });

  drag = d3.behavior.drag();

  drag.on('dragstart', function() {
    active_line = {
      points: [],
      color: active_color
    };
    drawing_data.lines.push(active_line);
    return redraw(active_line);
  });

  drag.on('drag', function() {
    active_line.points.push(d3.mouse(this));
    return redraw(active_line);
  });

  drag.on('dragend', function() {
    if (active_line.points.length === 0) {
      drawing_data.lines.pop();
    }
    active_line = null;
    return console.log(drawing_data);
  });

  canvas.call(drag);

  redraw = function(specific_line) {
    var lines;
    lines = lines_layer.selectAll('.line').data(drawing_data.lines);
    lines.enter().append('path').attr({
      "class": 'line',
      stroke: function(d) {
        return d.color;
      }
    }).each(function(d) {
      return d.elem = d3.select(this);
    });
    if (specific_line != null) {
      specific_line.elem.attr({
        d: function(d) {
          return render_line(d.points);
        }
      });
    } else {
      lines.attr({
        d: function(d) {
          return render_line(d.points);
        }
      });
    }
    return lines.exit().remove();
  };

  redraw();

}).call(this);

index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Freehand drawing</title>
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
    <link rel="stylesheet" href="index.css">
    <script src="//d3js.org/d3.v3.min.js"></script>
  </head>
  <body>
    <svg id="canvas" width="960px" height="500px"></svg>
    <svg id="ui" width="960px" height="500px"></svg>
    <script src="index.js"></script>
  </body>
</html>

index.coffee

SWATCH_D = 22

render_line = d3.svg.line()
  .x((d) ->  d[0])
  .y((d) ->  d[1])
  .interpolate('basis')
  
drawing_data = {
  lines: [{
    color: "#1b9e77",
    points: [[451,448],[447,447],[442,445],[430,440],[417,433],[411,429],[405,424],[398,420],[385,410],[372,398],[366,392],[360,385],[354,378],[348,370],[343,362],[338,354],[333,346],[329,337],[326,328],[322,318],[319,309],[316,299],[314,289],[313,279],[312,269],[312,259],[314,239],[316,228],[317,218],[319,209],[322,199],[326,190],[330,181],[334,172],[339,164],[344,156],[350,149],[355,143],[362,136],[368,131],[375,125],[383,120],[390,115],[398,111],[406,107],[423,99],[431,96],[440,93],[449,90],[458,87],[467,86],[495,83],[505,83],[514,83],[524,84],[534,85],[543,87],[552,89],[566,95],[578,101],[589,106],[598,112],[607,118],[615,125],[622,131],[629,138],[635,146],[641,153],[652,169],[661,185],[665,194],[671,210],[674,218],[676,226],[677,234],[678,241],[679,249],[678,264],[677,271],[676,279],[674,286],[671,293],[664,307],[660,313],[655,320],[650,326],[638,338],[631,344],[624,349],[609,359],[593,367],[585,370],[567,375],[558,378],[548,379],[529,382],[511,383],[493,382],[484,381],[475,379],[450,371],[443,367],[435,363],[428,358],[422,353],[416,347],[406,335],[398,321],[395,314],[393,307],[389,292],[388,277],[389,261],[393,239],[398,226],[408,207],[412,202],[417,197],[422,192],[427,188],[438,181],[449,176],[462,172],[475,169],[494,168],[508,168],[521,170],[539,176],[551,182],[556,185],[561,188],[570,196],[576,205],[583,220],[585,224],[586,231],[587,243],[587,255],[586,260],[583,270],[581,275],[578,279],[573,285],[567,290],[557,297],[546,302],[530,307],[519,309],[507,309],[491,307],[480,304],[471,299],[467,296],[463,294],[456,287],[449,276],[448,272],[447,268],[446,260],[447,252],[452,240],[456,232],[462,226],[469,220],[476,217],[485,214],[498,211],[506,211],[514,213],[525,218],[531,222],[534,225],[536,227],[540,233],[543,239],[544,245],[544,253],[542,257],[537,263],[535,265],[529,269],[524,271],[521,271],[519,271],[517,272],[511,271],[507,270],[502,267],[501,265],[497,261],[496,259],[494,257],[491,245],[492,242],[493,239],[497,237]]
  }]
}

active_line = null
active_color = "#333333"

canvas = d3.select('#canvas')
lines_layer = canvas.append('g')

ui = d3.select('#ui')

palette = ui.append('g')
  .attr
    transform: "translate(#{4+SWATCH_D/2},#{4+SWATCH_D/2})"
swatches = palette.selectAll('swatch')
  .data(["#333333","#ffffff","#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"]) # "black", white + colorbrewer Dark2

trash_btn = ui.append('text')
  .html('&#xf1f8;')
  .attr
    class: 'btn'
    dy: '0.35em'
    transform: 'translate(940,20)'
  .on 'click', () ->
    drawing_data.lines = []
    redraw()

swatches.enter().append('circle')
  .attr
    class: 'swatch'
    cx: (d,i) -> i*(SWATCH_D+4)/2
    cy: (d,i) -> if i%2 then SWATCH_D else 0
    r: SWATCH_D/2
    fill: (d) -> d
  .on 'click', (d) ->
    active_color = d
    swatches.classed('active', false)
    d3.select(this).classed('active', true)

swatches.each (d) ->
  if d is active_color
    d3.select(this).classed('active', true)

# line drawing

drag = d3.behavior.drag()

drag.on 'dragstart', () ->
  active_line = {
    points: [],
    color: active_color
  }
  drawing_data.lines.push active_line
  
  redraw(active_line)

drag.on 'drag', () ->
  active_line.points.push(d3.mouse(this))

  redraw(active_line)
      
drag.on 'dragend', () ->
  # remove active line if empty (e.g., generated by single click)
  if active_line.points.length is 0
    drawing_data.lines.pop()
    
  active_line = null
  console.log drawing_data
  
canvas.call drag

# redraw all the lines, or a specific one if given
redraw = (specific_line) ->
  lines = lines_layer.selectAll('.line')
    .data(drawing_data.lines)
    
  lines.enter().append('path')
    .attr
      class: 'line'
      stroke: (d) -> d.color
    .each (d) -> d.elem = d3.select(this)
      
  if specific_line?
    specific_line.elem
      .attr
        d: (d) -> render_line(d.points)
  else
    lines
      .attr
        d: (d) -> render_line(d.points)
        
  lines.exit().remove()

# first redraw, to load initial data
redraw()

index.css

#canvas {
  background: oldlace;
}
#ui {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
}
.swatch {
  pointer-events: all;
}
.swatch.active {
  stroke-width: 2px;
  stroke: black;
}
.swatch {
  cursor: pointer;
}
.btn {
  pointer-events: all;
  
  font-family: FontAwesome;
  fill: #333;
  font-size: 32px;
  text-anchor: middle;
  
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}
.btn:hover {
  fill: black;
  cursor: pointer;
}

.line {
  fill: none;
  stroke-width: 2px;
  stroke-linejoin: round;
  stroke-linecap: round;
}