Skip to content

Instantly share code, notes, and snippets.

@stuart-warren
Forked from nitaku/README.md
Last active September 2, 2021 14:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stuart-warren/5d778299fcbcb9b1343aac9d77d0fc49 to your computer and use it in GitHub Desktop.
Save stuart-warren/5d778299fcbcb9b1343aac9d77d0fc49 to your computer and use it in GitHub Desktop.
Freehand drawing

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

https://bl.ocks.org/stuart-warren/raw/5d778299fcbcb9b1343aac9d77d0fc49/?raw=true

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.

SWATCH_D = 22
render_line = d3.svg.line()
.x((d) -> d[0])
.y((d) -> d[1])
.interpolate('basis')
drawing_data = {
lines: []
}
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: white;
}
#ui {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
.swatch {
pointer-events: all;
}
.swatch.active {
stroke-width: 3px;
stroke: black;
}
.swatch {
cursor: pointer;
stroke-width: 1px;
stroke: darkgrey;
}
.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: 10px;
stroke-linejoin: round;
stroke-linecap: round;
}
body {
background: oldlace;
/* Disables pull-to-refresh but allows overscroll glow effects. */
overscroll-behavior-y: contain;
}
<!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="1024px"></svg>
<svg id="ui" width="960px" height="1024px"></svg>
<script src="index.js"></script>
</body>
</html>
// Generated by CoffeeScript 2.5.1
(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: []
};
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', 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);
}
});
// line drawing
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() {
// remove active line if empty (e.g., generated by single click)
if (active_line.points.length === 0) {
drawing_data.lines.pop();
}
active_line = null;
return console.log(drawing_data);
});
canvas.call(drag);
// redraw all the lines, or a specific one if given
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();
};
// first redraw, to load initial data
redraw();
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment