Skip to content

Commit

Permalink
Merge pull request #29 from jakubfiala/add-adaptive-stroke-flag
Browse files Browse the repository at this point in the history
Add adaptive stroke flag
  • Loading branch information
jakubfiala authored Jan 31, 2017
2 parents 8c77ef1 + 649d257 commit 3876f44
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 119 deletions.
4 changes: 2 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
"no-empty": ["error", { "allowEmptyCatch": true }],
"no-param-reassign": ["off"],
"no-underscore-dangle": ["off"],
"brace-style": ["error", "stroustrup"],
"brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
"comma-dangle": ["error", "never"],
"func-names": ["off"],
"consistent-return": ["off"],
"max-len": ["error", { "code": 100, "tabWidth": 2, "ignoreComments": true }],
"max-len": ["off"],
"padded-blocks": ["off"],
"global-require": ["off"],
"no-unused-expressions": ["off"],
Expand Down
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,22 @@ sketcher.weight = 20; //in pixels
```
+ change the color:
```js
sketcher.color = `#ff485e`; //just like CSS
sketcher.color = '#ff485e'; //just like CSS
```
+ toggle between modes:
```js
sketcher.mode = `erase`; // eraser tool
sketcher.mode = `fill`; // click to fill area
sketcher.mode = `draw`; // default
sketcher.mode = 'erase'; // eraser tool
sketcher.mode = 'fill'; // click to fill area
sketcher.mode = 'draw'; // default
```
+ toggle smoothing - having it on makes the drawings look much better, turning it off makes it feel a bit more responsive:
+ toggle smoothing - having it on makes the drawings look much better, turning it off makes it feel a bit more responsive. `true` by default.
```js
sketcher.smoothing = false;
```
+ toggle adaptive stroke, i.e. line width changing based on drawing speed for a more natural effect. `true` by default.
```js
sketcher.adaptiveStroke = false;
```
+ change the opacity:
```js
sketcher.opacity = 0.5; //number between 0-1
Expand All @@ -88,6 +92,13 @@ var dataURL = sketcher.toImage();
//then we can, for instance, open a new window with it
window.open(dataURL);
```
+ `dirty` event – do something when the canvas becomes dirty:
```js
// this also fires when you clear the canvas
// the dirty property is then false
// note that we attach the event to canvas
canvas.addEventListener('dirty', e => console.info(sketcher.dirty));
```

## Development
To obtain the dependencies, `cd` into the atrament directory and run `npm install`.
Expand Down
18 changes: 15 additions & 3 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,22 @@
font-family: sans-serif;
padding: 1em;
}

#clear {
display: none;
}
</style>
</head>
<body>
<h1>atrament.js</h1>
<form>
<button onclick="event.preventDefault(); atrament.clear();">clear</button><br>
<button id="clear" onclick="event.preventDefault(); atrament.clear();">clear</button><br>
<label>Thickness</label><br>
<input type="range" min="1" max="40" oninput="atrament.weight = parseFloat(event.target.value);" value="2" step="0.1" autocomplete="off"/><br>
<label>Smoothing</label><br>
<input type="checkbox" onchange="atrament.smoothing = event.target.checked;" checked autocomplete="off"><br>
<label>Adaptive stroke</label><br>
<input type="checkbox" onchange="atrament.adaptiveStroke = event.target.checked;" checked autocomplete="off"><br>
<label>Mode</label>
<select onchange="atrament.mode = event.target.value;">
<option value="draw" default>Draw</option>
Expand All @@ -60,10 +66,16 @@ <h1>atrament.js</h1>
<label>Opacity</label><br>
<input type="range" min="0" max="1" onchange="atrament.opacity = parseFloat(event.target.value);" value="1" step="0.05" autocomplete="off">
</form>
<canvas></canvas>
<canvas id="sketcher"></canvas>
<a href="https://github.com/jakubfiala/atrament.js"><img style="position: fixed; top: 0; right: 0; border: 0; z-index: 3;" src="https://camo.githubusercontent.com/38ef81f8aca64bb9a64448d0d70f1308ef5341ab/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f6461726b626c75655f3132313632312e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png"></a>
</body>
<script>
var atrament = atrament('canvas', window.innerWidth, window.innerHeight);
var canvas = document.getElementById('sketcher');
var atrament = atrament(canvas, window.innerWidth, window.innerHeight);

var clearButton = document.getElementById('clear');
canvas.addEventListener('dirty', function(e) {
clearButton.style.display = atrament.dirty ? 'inline-block' : 'none';
});
</script>
</html>
2 changes: 1 addition & 1 deletion dist/atrament.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/atrament.min.js.map

Large diffs are not rendered by default.

138 changes: 31 additions & 107 deletions src/atrament.js
Original file line number Diff line number Diff line change
@@ -1,90 +1,4 @@
// make a class for Point
class Point {
constructor(x, y) {
if (typeof x === 'undefined' || typeof y === 'undefined') {
throw new Error('not enough coordinates for Point.');
}

this._x = x;
this._y = y;
}

get x() {
return this._x;
}

get y() {
return this._y;
}

set x(x) {
this._x = x;
}

set y(y) {
this._y = y;
}

set(x, y) {
if (typeof x === 'undefined' || typeof y === 'undefined') {
throw new Error('not enough coordinates for Point.set');
}

this._x = x;
this._y = y;
}
}

// make a class for the mouse data
class Mouse extends Point {
constructor() {
super(0, 0);
this._down = false;
this._px = 0;
this._py = 0;
}

get down() {
return this._down;
}

set down(d) {
this._down = d;
}

get x() {
return this._x;
}

get y() {
return this._y;
}

set x(x) {
this._x = x;
}

set y(y) {
this._y = y;
}

get px() {
return this._px;
}

get py() {
return this._py;
}

set px(px) {
this._px = px;
}

set py(py) {
this._py = py;
}

}
import Mouse from './mouse.js';

class Atrament {
constructor(selector, width, height, color) {
Expand Down Expand Up @@ -154,7 +68,6 @@ class Atrament {
this.context.beginPath();
this.context.moveTo(this.mouse.px, this.mouse.py);
};

const mouseUp = () => {
this.mouse.down = false;
// stop drawing
Expand All @@ -169,7 +82,7 @@ class Atrament {
this.canvas.addEventListener('touchend', mouseUp);
this.canvas.addEventListener('touchmove', mouseMove);

// helper for destroying Atrament(removing event listeners)
// helper for destroying Atrament (removing event listeners)
this.destroy = () => {
this.clear();
this.canvas.removeEventListener('mousemove', mouseMove);
Expand Down Expand Up @@ -201,20 +114,19 @@ class Atrament {
this._targetThickness = 2;
this._weight = 2;
this._mode = 'draw';
this._adaptive = true;
}

static lineDistance(x1, y1, x2, y2) {
// calculate euclidean distance between(x1, y1) and(x2, y2)
// calculate euclidean distance between (x1, y1) and (x2, y2)
const xs = Math.pow(x2 - x1, 2);
const ys = Math.pow(y2 - y1, 2);

return Math.sqrt(xs + ys);
}

static hexToRgb(hexColor) {
// Since input type color provides hex and ImageData accepts RGB need to transform
const m = hexColor.match(/^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i);

return [
parseInt(m[1], 16),
parseInt(m[2], 16),
Expand Down Expand Up @@ -280,17 +192,23 @@ class Atrament {
// recalculate distance from previous point, this time relative to the smoothed coords
const dist = Atrament.lineDistance(mouse.x, mouse.y, mouse.px, mouse.py);

// calculate target thickness based on the new distance
this._targetThickness = (dist - 1) / (50 - 1) * (this._maxWeight - this._weight) + this._weight;
// approach the target gradually
if (this._thickness > this._targetThickness) {
this._thickness -= 0.5;
if (this._adaptive) {
// calculate target thickness based on the new distance
this._targetThickness = (dist - 1) / (50 - 1) * (this._maxWeight - this._weight) + this._weight;
// approach the target gradually
if (this._thickness > this._targetThickness) {
this._thickness -= 0.5;
}
else if (this._thickness < this._targetThickness) {
this._thickness += 0.5;
}
// set line width
context.lineWidth = this._thickness;
}
else if (this._thickness < this._targetThickness) {
this._thickness += 0.5;
else {
// line width is equal to default weight
context.lineWidth = this._weight;
}
// set line width
context.lineWidth = this._thickness;

// draw using quad interpolation
context.quadraticCurveTo(mouse.px, mouse.py, mouse.x, mouse.y);
Expand Down Expand Up @@ -322,6 +240,14 @@ class Atrament {
this._maxWeight = w + this.WEIGHT_SPREAD;
}

get adaptiveStroke() {
return this._adaptive;
}

set adaptiveStroke(s) {
this._adaptive = !!s;
}

get mode() {
return this._mode;
}
Expand Down Expand Up @@ -398,13 +324,12 @@ class Atrament {
fill() {
const mouse = this.mouse;
const context = this.context;
const startColor = Array.from(context.getImageData(mouse.x, mouse.y, 1, 1).data, 0);
const startColor = Array.prototype.slice.call(context.getImageData(mouse.x, mouse.y, 1, 1).data, 0); // converting to Array because Safari 9

if (!this._filling) {
this.canvas.style.cursor = 'progress';
this._filling = true;

setTimeout(() => this._floodFill(mouse.x, mouse.y, startColor), 100);
setTimeout(() => { this._floodFill(mouse.x, mouse.y, startColor); }, 100);
}
else {
this._fillStack.push([
Expand Down Expand Up @@ -432,7 +357,7 @@ class Atrament {
// check if we're trying to fill with the same colour, if so, stop
if (matchFillColor((startY * context.canvas.width + startX) * 4)) {
this._filling = false;
setTimeout(() => this.canvas.style.cursor = 'crosshair', 100);
setTimeout(() => { this.canvas.style.cursor = 'crosshair'; }, 100);
return;
}

Expand Down Expand Up @@ -492,10 +417,9 @@ class Atrament {
}
else {
this._filling = false;
setTimeout(() => this.canvas.style.cursor = 'crosshair', 100);
setTimeout(() => { this.canvas.style.cursor = 'crosshair'; }, 100);
}
}

}

// for people who like functional programming
Expand Down
81 changes: 81 additions & 0 deletions src/mouse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// make a class for Point
class Point {
constructor(x, y) {
this._x = x;
this._y = y;
}

get x() {
return this._x;
}

get y() {
return this._y;
}

set x(x) {
this._x = x;
}

set y(y) {
this._y = y;
}

set(x, y) {
this._x = x;
this._y = y;
}
}

// make a class for the mouse data
class Mouse extends Point {
constructor() {
super(0, 0);
this._down = false;
this._px = 0;
this._py = 0;
}

get down() {
return this._down;
}

set down(d) {
this._down = d;
}

get x() {
return this._x;
}

get y() {
return this._y;
}

set x(x) {
this._x = x;
}

set y(y) {
this._y = y;
}

get px() {
return this._px;
}

get py() {
return this._py;
}

set px(px) {
this._px = px;
}

set py(py) {
this._py = py;
}

}

export default Mouse;

0 comments on commit 3876f44

Please sign in to comment.