Skip to content

Instantly share code, notes, and snippets.

@christianbriggs
Created August 9, 2016 21:23
Show Gist options
  • Save christianbriggs/124a06966170c0387b64ed1cbac6fe26 to your computer and use it in GitHub Desktop.
Save christianbriggs/124a06966170c0387b64ed1cbac6fe26 to your computer and use it in GitHub Desktop.
Freehand drawing
license: gpl-3.0

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.

forked from nitaku's block: Freehand drawing

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('')
.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()
#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;
}
<!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="http://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>
// 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('&#xf1f8;').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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment