Socket
Socket
Sign inDemoInstall

simple-drawing-board

Package Overview
Dependencies
0
Maintainers
1
Versions
21
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 2.1.1 to 3.0.0

__htmls__/basic.html

5

.eslintrc.js
module.exports = {
env: {
es6: true,
node: true,
jasmine: true,
browser: true
},
parserOptions: {
sourceType: "module"
ecmaVersion: 2020,
sourceType: 'module',
},

@@ -9,0 +12,0 @@ extends: [

751

dist/simple-drawing-board.js
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.SimpleDrawingBoard = factory());
}(this, (function () { 'use strict';
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.SimpleDrawingBoard = {}));
}(this, (function (exports) { 'use strict';
/**
* touchデバイス or NOT
*
* @return {Boolean}
* isTouchデバイス
*/
function isTouch() {
return "ontouchstart" in window.document;
}
/**
* 透過の背景の場合、消すモードの処理が微妙に変わるので、
* それをチェックしたい
*
* @param {String} color
* 色
*/
function isTransparent(color) {
color = color.replace(/\s/g, "");
if (color === "transparent") {
return true;
}
const isRgbaOrHlsaTransparent = color.split(",")[3] === "0)";
if (isRgbaOrHlsaTransparent) {
return true;
}
return false;
}
/**
* ctx.drawImageできるのは3つ
*
* @param {HTMLElement} el
* チェックする要素
* @return {Boolean}
* 描画できる要素かどうか
*
*/
function isDrawableEl(el) {
const isDrawable =
["img", "canvas", "video"].indexOf(el.tagName.toLowerCase()) !== -1;
return isDrawable;
}
/**
* Minimal event interface
* Minimul EventEmitter implementation
* See `https://gist.github.com/leader22/3ab8416ce41883ae1ccd`
*
*/
class Eve {

@@ -104,376 +57,253 @@ constructor() {

}
removeAllListeners() {
this._events = {};
}
}
/**
* Stack Data Structure
*
* History for undo/redo Structure(mutable)
* See `https://gist.github.com/leader22/9fbed07106d652ef40fda702da4f39c4`
*
*/
class Stack {
constructor() {
this._items = [];
class History {
constructor(initialValue = null) {
this._past = [];
this._present = initialValue;
this._future = [];
}
get(i) {
return this._items[i];
get value() {
return this._present;
}
push(item) {
this._items.push(item);
undo() {
if (this._past.length === 0) return;
const previous = this._past.pop();
this._future.unshift(this._present);
this._present = previous;
}
pop() {
if (this._items.length > 0) {
return this._items.pop();
}
return null;
redo() {
if (this._future.length === 0) return;
const next = this._future.shift();
this._past.push(this._present);
this._present = next;
}
shift() {
if (this._items.length > 0) {
return this._items.shift();
}
return null;
save(newPresent) {
if (this._present === newPresent) return;
this._past.push(this._present);
this._future.length = 0;
this._present = newPresent;
}
clear() {
this._items.length = 0;
this._past.length = 0;
this._future.length = 0;
}
}
size() {
return this._items.length;
function isTouch() {
return "ontouchstart" in window.document;
}
// expect HTML elements from CanvasImageSource
function isDrawableElement($el) {
if ($el instanceof HTMLImageElement) return true;
if ($el instanceof SVGImageElement) return true;
if ($el instanceof HTMLCanvasElement) return true;
if ($el instanceof HTMLVideoElement) return true;
return false;
}
function isBase64DataURL(url) {
if (typeof url !== "string") return false;
if (!url.startsWith("data:image/")) return false;
return true;
}
async function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onerror = reject;
img.onload = () => resolve(img);
img.src = src;
});
}
function getMidInputCoords(old, coords) {
return {
x: (old.x + coords.x) >> 1,
y: (old.y + coords.y) >> 1,
};
}
function getInputCoords(ev, $el) {
let x, y;
if (isTouch()) {
x = ev.touches[0].pageX;
y = ev.touches[0].pageY;
} else {
x = ev.pageX;
y = ev.pageY;
}
// check this every time for real-time resizing
const elBCRect = $el.getBoundingClientRect();
// need to consider scrolled positions
const elRect = {
left: elBCRect.left + window.pageXOffset,
top: elBCRect.top + window.pageYOffset,
};
// if canvas has styled
const elScale = {
x: $el.width / elBCRect.width,
y: $el.height / elBCRect.height,
};
return {
x: (x - elRect.left) * elScale.x,
y: (y - elRect.top) * elScale.y,
};
}
class SimpleDrawingBoard {
constructor(el, options) {
if (!(el instanceof HTMLCanvasElement)) {
throw new Error("Pass canvas element as first argument.");
}
constructor($el) {
this._$el = $el;
this._ctx = this._$el.getContext("2d");
this.ev = new Eve();
this.el = el;
this.ctx = el.getContext("2d");
// handwriting fashion ;D
this._ctx.lineCap = this._ctx.lineJoin = "round";
// trueの時だけstrokeされる
this._isDrawing = 0;
// 描画用のタイマー
// for canvas operation
this._isDrawMode = true;
// for drawing
this._isDrawing = false;
this._timer = null;
// 座標情報
this._coords = {
old: { x: 0, y: 0 },
oldMid: { x: 0, y: 0 },
current: { x: 0, y: 0 }
current: { x: 0, y: 0 },
};
this._settings = {
lineColor: "#aaa",
lineSize: 5,
boardColor: "transparent",
historyDepth: 10,
isTransparent: 1,
isDrawMode: 1
};
// 描画履歴
this._history = {
// undo
prev: new Stack(),
// redo
next: new Stack()
};
this._initBoard(options);
}
this._ev = new Eve();
this._history = new History(this.toDataURL());
/**
* 線の太さを設定する
*
* @param {Number} size
* 太さ(1以下は全て1とする)
*
*/
setLineSize(size) {
this.ctx.lineWidth = size | 0 || 1;
return this;
this._bindEvents();
this._drawFrame();
}
/**
* 線の色を設定する
*
* @param {String} color
* 色
*
*/
setLineColor(color) {
this.ctx.strokeStyle = color;
return this;
get canvas() {
return this._$el;
}
/**
* 単一の色で塗りつぶす
*
* @param {String} color
* 色
*
*/
fill(color) {
this._saveHistory();
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
this.ctx.fillStyle = color;
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
return this;
get observer() {
return this._ev;
}
/**
* ボードをクリアする
* 実際は、背景色で塗りつぶす
*
*/
clear() {
const settings = this._settings;
this._saveHistory();
// 透明なときは一手間
if (settings.isTransparent) {
const oldGCO = this.ctx.globalCompositeOperation;
this.ctx.globalCompositeOperation = "destination-out";
this.fill(this._settings.boardColor);
this.ctx.globalCompositeOperation = oldGCO;
}
// 違うならそのまま
else {
this.fill(this._settings.boardColor);
}
return this;
get mode() {
return this._isDrawMode ? "draw" : "erase";
}
/**
* 書くモードと消すモードをスイッチ
*
*/
toggleMode() {
const settings = this._settings;
// 消す
if (settings.isDrawMode) {
this.setLineColor(settings.boardColor);
if (settings.isTransparent) {
this.ctx.globalCompositeOperation = "destination-out";
}
settings.isDrawMode = 0;
}
// 書く
else {
this.setLineColor(settings.lineColor);
if (settings.isTransparent) {
this.ctx.globalCompositeOperation = "source-over";
}
settings.isDrawMode = 1;
}
this.ev.trigger("toggleMode", settings.isDrawMode);
return this;
setLineSize(size) {
this._ctx.lineWidth = size | 0 || 1;
}
/**
* 現在のボードをbase64文字列で取得
*
* @return {String}
* base64文字列
*
*/
getImg() {
return this.ctx.canvas.toDataURL("image/png");
setLineColor(color) {
this._ctx.strokeStyle = color;
}
/**
* 現在のボードをなんかしら復元
*
* @param {String|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} src
* 画像URLか、drawImageできる要素
* @param {Boolean} isOverlay
* 上に重ねるならtrue
* @param {Boolean} isSkipSaveHistory
* 履歴保存をスキップするならtrue(デフォルトfalse)
*
*/
setImg(src, isOverlay, isSkipSaveHistory) {
isOverlay = isOverlay || false;
isSkipSaveHistory = isSkipSaveHistory || false;
if (!isSkipSaveHistory) {
this._saveHistory();
}
fill(color) {
const ctx = this._ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = color;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// imgUrl
if (typeof src === "string") {
this._setImgByImgSrc(src, isOverlay);
}
// img, video, canvas element
else {
this._setImgByDrawableEl(src, isOverlay);
}
return this;
this._saveHistory();
}
/**
* 履歴を戻す
*
*/
undo() {
this._restoreFromHistory(false);
clear() {
const ctx = this._ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
this._saveHistory();
}
/**
* 履歴を進める
*
*/
redo() {
this._restoreFromHistory(true);
toggleMode() {
this._ctx.globalCompositeOperation = this._isDrawMode
? "destination-out"
: "source-over";
this._isDrawMode = !this._isDrawMode;
}
/**
* 後始末
*
*/
dispose() {
this._unbindEvents();
cancelAnimationFrame(this._timer);
this._timer = null;
this._history.prev.clear();
this._history.next.clear();
this.ev.trigger("dispose");
toDataURL({ type, quality } = {}) {
return this._ctx.canvas.toDataURL(type, quality);
}
/**
* ボードを初期化する
*
* @param {Object} options
* 初期化オプション
* `Const.settings`参照
*
*/
_initBoard(options) {
const settings = this._settings;
fillImageByElement($el) {
if (!isDrawableElement($el))
throw new TypeError("Passed element is not a drawable!");
if (options) {
for (const p in options) {
settings[p] = options[p];
}
}
const ctx = this._ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage($el, 0, 0, ctx.canvas.width, ctx.canvas.height);
// 透過な時は消すモードで一手間必要になる
if (isTransparent(settings.boardColor)) {
settings.boardColor = "rgba(0,0,0,1)";
settings.isTransparent = 1;
}
// 初期は書くモード
settings.isDrawMode = 1;
this.ctx.lineCap = this.ctx.lineJoin = "round";
this.setLineSize(settings.lineSize);
this.setLineColor(settings.lineColor);
this._bindEvents();
this._draw();
this._saveHistory();
}
_bindEvents() {
const events = isTouch()
? ["touchstart", "touchmove", "touchend", "touchcancel", "gesturestart"]
: ["mousedown", "mousemove", "mouseup", "mouseout"];
async fillImageByDataURL(src) {
if (!isBase64DataURL(src))
throw new TypeError("Passed src is not a base64 data URL!");
for (let i = 0, l = events.length; i < l; i++) {
this.el.addEventListener(events[i], this, false);
}
}
const img = await loadImage(src);
_unbindEvents() {
const events = isTouch()
? ["touchstart", "touchmove", "touchend", "touchcancel", "gesturestart"]
: ["mousedown", "mousemove", "mouseup", "mouseout"];
const ctx = this._ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height);
for (let i = 0, l = events.length; i < l; i++) {
this.el.removeEventListener(events[i], this, false);
}
this._saveHistory();
}
/**
* 実際の描画処理
* 別のイベントで集めた座標情報を元に、描画するだけ
*
*/
_draw() {
// さっきと同じ場所なら書かなくていい
const isSameCoords =
this._coords.old.x === this._coords.current.x &&
this._coords.old.y === this._coords.current.y;
async undo() {
this._history.undo();
const base64 = this._history.value;
if (!isBase64DataURL(base64)) return;
if (this._isDrawing) {
const currentMid = this._getMidInputCoords(this._coords.current);
this.ctx.beginPath();
this.ctx.moveTo(currentMid.x, currentMid.y);
this.ctx.quadraticCurveTo(
this._coords.old.x,
this._coords.old.y,
this._coords.oldMid.x,
this._coords.oldMid.y
);
this.ctx.stroke();
const img = await loadImage(base64);
this._coords.old = this._coords.current;
this._coords.oldMid = currentMid;
if (!isSameCoords) this.ev.trigger("draw", this._coords.current);
}
this._timer = requestAnimationFrame(this._draw.bind(this));
const ctx = this._ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height);
}
/**
* 描画しはじめの処理
*
*/
_onInputDown(ev) {
this._saveHistory();
this._isDrawing = 1;
async redo() {
this._history.redo();
const base64 = this._history.value;
if (!isBase64DataURL(base64)) return;
const coords = this._getInputCoords(ev);
this._coords.current = this._coords.old = coords;
this._coords.oldMid = this._getMidInputCoords(coords);
const img = await loadImage(base64);
this.ev.trigger("drawBegin", this._coords.current);
const ctx = this._ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height);
}
/**
* 描画してる間の処理
*
*/
_onInputMove(ev) {
this._coords.current = this._getInputCoords(ev);
}
destroy() {
this._unbindEvents();
/**
* 描画しおわりの処理
*
*/
_onInputUp() {
this._isDrawing = 0;
this.ev.trigger("drawEnd", this._coords.current);
}
this._ev.removeAllListeners();
this._history.clear();
_onInputCancel() {
if (this._isDrawing) {
this.ev.trigger("drawEnd", this._coords.current);
}
this._isDrawing = 0;
cancelAnimationFrame(this._timer);
this._timer = null;
}
/**
* いわゆるhandleEvent
*
* @param {Object} ev
* イベント
*
*/
handleEvent(ev) {

@@ -504,167 +334,100 @@ ev.preventDefault();

/**
* 座標の取得
*
* @param {Object} ev
* イベント
* @return {Object}
* x, y座標
*
*/
_getInputCoords(ev) {
let x, y;
if (isTouch()) {
x = ev.touches[0].pageX;
y = ev.touches[0].pageY;
} else {
x = ev.pageX;
y = ev.pageY;
_bindEvents() {
const events = isTouch()
? ["touchstart", "touchmove", "touchend", "touchcancel", "gesturestart"]
: ["mousedown", "mousemove", "mouseup", "mouseout"];
for (const ev of events) {
this._$el.addEventListener(ev, this, false);
}
}
_unbindEvents() {
const events = isTouch()
? ["touchstart", "touchmove", "touchend", "touchcancel", "gesturestart"]
: ["mousedown", "mousemove", "mouseup", "mouseout"];
// いつリサイズされてもよいようリアルタイムに
const elBCRect = this.el.getBoundingClientRect();
for (const ev of events) {
this._$el.removeEventListener(ev, this, false);
}
}
// スクロールされた状態でリロードすると、位置ズレするので加味する
const elRect = {
left: elBCRect.left + window.pageXOffset,
top: elBCRect.top + window.pageYOffset
};
// canvasのstyle指定に対応する
const elScale = {
x: this.el.width / elBCRect.width,
y: this.el.height / elBCRect.height
};
_drawFrame() {
this._timer = requestAnimationFrame(() => this._drawFrame());
return {
x: (x - elRect.left) * elScale.x,
y: (y - elRect.top) * elScale.y
};
}
if (!this._isDrawing) return;
/**
* 座標の取得
*
* @param {Object} coords
* 元のx, y座標
* @return {Object}
* 変換されたx, y座標
*
*/
_getMidInputCoords(coords) {
return {
x: (this._coords.old.x + coords.x) >> 1,
y: (this._coords.old.y + coords.y) >> 1
};
}
const isSameCoords =
this._coords.old.x === this._coords.current.x &&
this._coords.old.y === this._coords.current.y;
/**
* 現在のボードを画像URLから復元
*
* @param {String} src
* 画像URL
* @param {Boolean} isOverlay
* 現在のボードを消さずに復元するならtrue
*
*/
_setImgByImgSrc(src, isOverlay) {
const ctx = this.ctx;
const oldGCO = ctx.globalCompositeOperation;
const img = new Image();
const currentMid = getMidInputCoords(
this._coords.old,
this._coords.current
);
const ctx = this._ctx;
img.onload = function() {
ctx.globalCompositeOperation = "source-over";
isOverlay || ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.globalCompositeOperation = oldGCO;
};
ctx.beginPath();
ctx.moveTo(currentMid.x, currentMid.y);
ctx.quadraticCurveTo(
this._coords.old.x,
this._coords.old.y,
this._coords.oldMid.x,
this._coords.oldMid.y
);
ctx.stroke();
img.src = src;
this._coords.old = this._coords.current;
this._coords.oldMid = currentMid;
if (!isSameCoords) this._ev.trigger("draw", this._coords.current);
}
/**
* 現在のボードを特定の要素から復元
*
* @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} el
* drawImageできる要素
* @param {Boolean} isOverlay
* 現在のボードを消さずに復元するならtrue
*
*/
_setImgByDrawableEl(el, isOverlay) {
if (!isDrawableEl(el)) {
return;
}
_onInputDown(ev) {
this._isDrawing = true;
const ctx = this.ctx;
const oldGCO = ctx.globalCompositeOperation;
const coords = getInputCoords(ev, this._$el);
this._coords.current = this._coords.old = coords;
this._coords.oldMid = getMidInputCoords(this._coords.old, coords);
ctx.globalCompositeOperation = "source-over";
isOverlay || ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(el, 0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.globalCompositeOperation = oldGCO;
this._ev.trigger("drawBegin", this._coords.current);
}
/**
* 履歴に現在のボードを保存する
*
*/
_saveHistory() {
const history = this._history;
_onInputMove(ev) {
this._coords.current = getInputCoords(ev, this._$el);
}
// 最後の履歴と同じ結果なら保存しない
const curImg = this.getImg();
const lastImg = history.prev.get(history.prev.size() - 1);
if (lastImg && curImg === lastImg) {
return;
}
_onInputUp() {
this._ev.trigger("drawEnd", this._coords.current);
this._saveHistory();
// 履歴には限度がある
while (history.prev.size() >= this._settings.historyDepth) {
// 古い履歴から消していく
history.prev.shift();
}
// 普通にセーブ
history.prev.push(curImg);
// redo用履歴はクリアする
history.next.clear();
this.ev.trigger("save", curImg);
this._isDrawing = false;
}
/**
* 履歴から復元する
*
* @param {Boolean} goForth
* 戻す or やり直すで、やり直すならtrue
*
*/
_restoreFromHistory(goForth) {
const history = this._history;
let pushKey = "next";
let popKey = "prev";
if (goForth) {
// redoのときはnextからpopし、prevにpushする
pushKey = "prev";
popKey = "next";
_onInputCancel() {
if (this._isDrawing) {
this._ev.trigger("drawEnd", this._coords.current);
this._saveHistory();
}
const item = history[popKey].pop();
if (item == null) {
return;
}
// 最後の履歴と同じ結果なら保存しない
const curImg = this.getImg();
const lastImg = history.next.get(history.next.size() - 1);
if (!lastImg || lastImg != curImg) {
history[pushKey].push(curImg);
}
this._isDrawing = false;
}
// この操作は履歴を保存しない
this.setImg(item, false, true);
_saveHistory() {
this._history.save(this.toDataURL());
this._ev.trigger("save", this._history.value);
}
}
return SimpleDrawingBoard;
function create($el) {
if (!($el instanceof HTMLCanvasElement))
throw new TypeError("HTMLCanvasElement must be passed as first argument!");
const sdb = new SimpleDrawingBoard($el);
return sdb;
}
exports.create = create;
Object.defineProperty(exports, '__esModule', { value: true });
})));

@@ -1,1 +0,1 @@

!function(t,s){"object"==typeof exports&&"undefined"!=typeof module?module.exports=s():"function"==typeof define&&define.amd?define(s):(t=t||self).SimpleDrawingBoard=s()}(this,(function(){"use strict";function t(){return"ontouchstart"in window.document}class s{constructor(){this._events={}}on(t,s){const e=this._events;t in e||(e[t]=[]),e[t].push(s)}off(t,s){const e=this._events;if(!(t in e))return;s||(e[t]=[]);const i=e[t].indexOf(s);i>=0&&e[t].splice(i,1)}trigger(t,s){const e=this._events;if(t in e)for(let i=0;i<e[t].length;i++){const o=e[t][i];o.handleEvent?o.handleEvent.call(this,s):o.call(this,s)}}}class e{constructor(){this._items=[]}get(t){return this._items[t]}push(t){this._items.push(t)}pop(){return this._items.length>0?this._items.pop():null}shift(){return this._items.length>0?this._items.shift():null}clear(){this._items.length=0}size(){return this._items.length}}return class{constructor(t,i){if(!(t instanceof HTMLCanvasElement))throw new Error("Pass canvas element as first argument.");this.ev=new s,this.el=t,this.ctx=t.getContext("2d"),this._isDrawing=0,this._timer=null,this._coords={old:{x:0,y:0},oldMid:{x:0,y:0},current:{x:0,y:0}},this._settings={lineColor:"#aaa",lineSize:5,boardColor:"transparent",historyDepth:10,isTransparent:1,isDrawMode:1},this._history={prev:new e,next:new e},this._initBoard(i)}setLineSize(t){return this.ctx.lineWidth=0|t||1,this}setLineColor(t){return this.ctx.strokeStyle=t,this}fill(t){return this._saveHistory(),this.ctx.clearRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height),this.ctx.fillStyle=t,this.ctx.fillRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height),this}clear(){const t=this._settings;if(this._saveHistory(),t.isTransparent){const t=this.ctx.globalCompositeOperation;this.ctx.globalCompositeOperation="destination-out",this.fill(this._settings.boardColor),this.ctx.globalCompositeOperation=t}else this.fill(this._settings.boardColor);return this}toggleMode(){const t=this._settings;return t.isDrawMode?(this.setLineColor(t.boardColor),t.isTransparent&&(this.ctx.globalCompositeOperation="destination-out"),t.isDrawMode=0):(this.setLineColor(t.lineColor),t.isTransparent&&(this.ctx.globalCompositeOperation="source-over"),t.isDrawMode=1),this.ev.trigger("toggleMode",t.isDrawMode),this}getImg(){return this.ctx.canvas.toDataURL("image/png")}setImg(t,s,e){return s=s||!1,(e=e||!1)||this._saveHistory(),"string"==typeof t?this._setImgByImgSrc(t,s):this._setImgByDrawableEl(t,s),this}undo(){this._restoreFromHistory(!1)}redo(){this._restoreFromHistory(!0)}dispose(){this._unbindEvents(),cancelAnimationFrame(this._timer),this._timer=null,this._history.prev.clear(),this._history.next.clear(),this.ev.trigger("dispose")}_initBoard(t){const s=this._settings;if(t)for(const e in t)s[e]=t[e];var e;("transparent"===(e=(e=s.boardColor).replace(/\s/g,""))||"0)"===e.split(",")[3])&&(s.boardColor="rgba(0,0,0,1)",s.isTransparent=1),s.isDrawMode=1,this.ctx.lineCap=this.ctx.lineJoin="round",this.setLineSize(s.lineSize),this.setLineColor(s.lineColor),this._bindEvents(),this._draw()}_bindEvents(){const s=t()?["touchstart","touchmove","touchend","touchcancel","gesturestart"]:["mousedown","mousemove","mouseup","mouseout"];for(let t=0,e=s.length;t<e;t++)this.el.addEventListener(s[t],this,!1)}_unbindEvents(){const s=t()?["touchstart","touchmove","touchend","touchcancel","gesturestart"]:["mousedown","mousemove","mouseup","mouseout"];for(let t=0,e=s.length;t<e;t++)this.el.removeEventListener(s[t],this,!1)}_draw(){const t=this._coords.old.x===this._coords.current.x&&this._coords.old.y===this._coords.current.y;if(this._isDrawing){const s=this._getMidInputCoords(this._coords.current);this.ctx.beginPath(),this.ctx.moveTo(s.x,s.y),this.ctx.quadraticCurveTo(this._coords.old.x,this._coords.old.y,this._coords.oldMid.x,this._coords.oldMid.y),this.ctx.stroke(),this._coords.old=this._coords.current,this._coords.oldMid=s,t||this.ev.trigger("draw",this._coords.current)}this._timer=requestAnimationFrame(this._draw.bind(this))}_onInputDown(t){this._saveHistory(),this._isDrawing=1;const s=this._getInputCoords(t);this._coords.current=this._coords.old=s,this._coords.oldMid=this._getMidInputCoords(s),this.ev.trigger("drawBegin",this._coords.current)}_onInputMove(t){this._coords.current=this._getInputCoords(t)}_onInputUp(){this._isDrawing=0,this.ev.trigger("drawEnd",this._coords.current)}_onInputCancel(){this._isDrawing&&this.ev.trigger("drawEnd",this._coords.current),this._isDrawing=0}handleEvent(t){switch(t.preventDefault(),t.stopPropagation(),t.type){case"mousedown":case"touchstart":this._onInputDown(t);break;case"mousemove":case"touchmove":this._onInputMove(t);break;case"mouseup":case"touchend":this._onInputUp();break;case"mouseout":case"touchcancel":case"gesturestart":this._onInputCancel()}}_getInputCoords(s){let e,i;t()?(e=s.touches[0].pageX,i=s.touches[0].pageY):(e=s.pageX,i=s.pageY);const o=this.el.getBoundingClientRect(),r=o.left+window.pageXOffset,n=o.top+window.pageYOffset;return{x:(e-r)*(this.el.width/o.width),y:(i-n)*(this.el.height/o.height)}}_getMidInputCoords(t){return{x:this._coords.old.x+t.x>>1,y:this._coords.old.y+t.y>>1}}_setImgByImgSrc(t,s){const e=this.ctx,i=e.globalCompositeOperation,o=new Image;o.onload=function(){e.globalCompositeOperation="source-over",s||e.clearRect(0,0,e.canvas.width,e.canvas.height),e.drawImage(o,0,0,e.canvas.width,e.canvas.height),e.globalCompositeOperation=i},o.src=t}_setImgByDrawableEl(t,s){if(!function(t){return-1!==["img","canvas","video"].indexOf(t.tagName.toLowerCase())}(t))return;const e=this.ctx,i=e.globalCompositeOperation;e.globalCompositeOperation="source-over",s||e.clearRect(0,0,e.canvas.width,e.canvas.height),e.drawImage(t,0,0,e.canvas.width,e.canvas.height),e.globalCompositeOperation=i}_saveHistory(){const t=this._history,s=this.getImg(),e=t.prev.get(t.prev.size()-1);if(!e||s!==e){for(;t.prev.size()>=this._settings.historyDepth;)t.prev.shift();t.prev.push(s),t.next.clear(),this.ev.trigger("save",s)}}_restoreFromHistory(t){const s=this._history;let e="next",i="prev";t&&(e="prev",i="next");const o=s[i].pop();if(null==o)return;const r=this.getImg(),n=s.next.get(s.next.size()-1);n&&n==r||s[e].push(r),this.setImg(o,!1,!0)}}}));
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).SimpleDrawingBoard={})}(this,(function(t){"use strict";class e{constructor(){this._events={}}on(t,e){const s=this._events;t in s||(s[t]=[]),s[t].push(e)}off(t,e){const s=this._events;if(!(t in s))return;e||(s[t]=[]);const i=s[t].indexOf(e);i>=0&&s[t].splice(i,1)}trigger(t,e){const s=this._events;if(t in s)for(let i=0;i<s[t].length;i++){const n=s[t][i];n.handleEvent?n.handleEvent.call(this,e):n.call(this,e)}}removeAllListeners(){this._events={}}}class s{constructor(t=null){this._past=[],this._present=t,this._future=[]}get value(){return this._present}undo(){if(0===this._past.length)return;const t=this._past.pop();this._future.unshift(this._present),this._present=t}redo(){if(0===this._future.length)return;const t=this._future.shift();this._past.push(this._present),this._present=t}save(t){this._present!==t&&(this._past.push(this._present),this._future.length=0,this._present=t)}clear(){this._past.length=0,this._future.length=0}}function i(){return"ontouchstart"in window.document}function n(t){return"string"==typeof t&&!!t.startsWith("data:image/")}async function o(t){return new Promise((e,s)=>{const i=new Image;i.onerror=s,i.onload=()=>e(i),i.src=t})}function r(t,e){return{x:t.x+e.x>>1,y:t.y+e.y>>1}}function a(t,e){let s,n;i()?(s=t.touches[0].pageX,n=t.touches[0].pageY):(s=t.pageX,n=t.pageY);const o=e.getBoundingClientRect(),r=o.left+window.pageXOffset,a=o.top+window.pageYOffset;return{x:(s-r)*(e.width/o.width),y:(n-a)*(e.height/o.height)}}class h{constructor(t){this._$el=t,this._ctx=this._$el.getContext("2d"),this._ctx.lineCap=this._ctx.lineJoin="round",this._isDrawMode=!0,this._isDrawing=!1,this._timer=null,this._coords={old:{x:0,y:0},oldMid:{x:0,y:0},current:{x:0,y:0}},this._ev=new e,this._history=new s(this.toDataURL()),this._bindEvents(),this._drawFrame()}get canvas(){return this._$el}get observer(){return this._ev}get mode(){return this._isDrawMode?"draw":"erase"}setLineSize(t){this._ctx.lineWidth=0|t||1}setLineColor(t){this._ctx.strokeStyle=t}fill(t){const e=this._ctx;e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillStyle=t,e.fillRect(0,0,e.canvas.width,e.canvas.height),this._saveHistory()}clear(){const t=this._ctx;t.clearRect(0,0,t.canvas.width,t.canvas.height),this._saveHistory()}toggleMode(){this._ctx.globalCompositeOperation=this._isDrawMode?"destination-out":"source-over",this._isDrawMode=!this._isDrawMode}toDataURL({type:t,quality:e}={}){return this._ctx.canvas.toDataURL(t,e)}fillImageByElement(t){if(!function(t){return t instanceof HTMLImageElement||(t instanceof SVGImageElement||(t instanceof HTMLCanvasElement||t instanceof HTMLVideoElement))}(t))throw new TypeError("Passed element is not a drawable!");const e=this._ctx;e.clearRect(0,0,e.canvas.width,e.canvas.height),e.drawImage(t,0,0,e.canvas.width,e.canvas.height),this._saveHistory()}async fillImageByDataURL(t){if(!n(t))throw new TypeError("Passed src is not a base64 data URL!");const e=await o(t),s=this._ctx;s.clearRect(0,0,s.canvas.width,s.canvas.height),s.drawImage(e,0,0,s.canvas.width,s.canvas.height),this._saveHistory()}async undo(){this._history.undo();const t=this._history.value;if(!n(t))return;const e=await o(t),s=this._ctx;s.clearRect(0,0,s.canvas.width,s.canvas.height),s.drawImage(e,0,0,s.canvas.width,s.canvas.height)}async redo(){this._history.redo();const t=this._history.value;if(!n(t))return;const e=await o(t),s=this._ctx;s.clearRect(0,0,s.canvas.width,s.canvas.height),s.drawImage(e,0,0,s.canvas.width,s.canvas.height)}destroy(){this._unbindEvents(),this._ev.removeAllListeners(),this._history.clear(),cancelAnimationFrame(this._timer),this._timer=null}handleEvent(t){switch(t.preventDefault(),t.stopPropagation(),t.type){case"mousedown":case"touchstart":this._onInputDown(t);break;case"mousemove":case"touchmove":this._onInputMove(t);break;case"mouseup":case"touchend":this._onInputUp();break;case"mouseout":case"touchcancel":case"gesturestart":this._onInputCancel()}}_bindEvents(){const t=i()?["touchstart","touchmove","touchend","touchcancel","gesturestart"]:["mousedown","mousemove","mouseup","mouseout"];for(const e of t)this._$el.addEventListener(e,this,!1)}_unbindEvents(){const t=i()?["touchstart","touchmove","touchend","touchcancel","gesturestart"]:["mousedown","mousemove","mouseup","mouseout"];for(const e of t)this._$el.removeEventListener(e,this,!1)}_drawFrame(){if(this._timer=requestAnimationFrame(()=>this._drawFrame()),!this._isDrawing)return;const t=this._coords.old.x===this._coords.current.x&&this._coords.old.y===this._coords.current.y,e=r(this._coords.old,this._coords.current),s=this._ctx;s.beginPath(),s.moveTo(e.x,e.y),s.quadraticCurveTo(this._coords.old.x,this._coords.old.y,this._coords.oldMid.x,this._coords.oldMid.y),s.stroke(),this._coords.old=this._coords.current,this._coords.oldMid=e,t||this._ev.trigger("draw",this._coords.current)}_onInputDown(t){this._isDrawing=!0;const e=a(t,this._$el);this._coords.current=this._coords.old=e,this._coords.oldMid=r(this._coords.old,e),this._ev.trigger("drawBegin",this._coords.current)}_onInputMove(t){this._coords.current=a(t,this._$el)}_onInputUp(){this._ev.trigger("drawEnd",this._coords.current),this._saveHistory(),this._isDrawing=!1}_onInputCancel(){this._isDrawing&&(this._ev.trigger("drawEnd",this._coords.current),this._saveHistory()),this._isDrawing=!1}_saveHistory(){this._history.save(this.toDataURL()),this._ev.trigger("save",this._history.value)}}t.create=function(t){if(!(t instanceof HTMLCanvasElement))throw new TypeError("HTMLCanvasElement must be passed as first argument!");return new h(t)},Object.defineProperty(t,"__esModule",{value:!0})}));
{
"name": "simple-drawing-board",
"version": "2.1.1",
"version": "3.0.0",
"description": "Just simple minimal canvas drawing lib.",
"main": "./dist/simple-drawing-board.js",
"types": "./index.d.ts",
"scripts": {
"test": "karma start",
"test:watch": "karma start --no-single-run --auto-watch --no-browsers",
"lint": "eslint ./src/**/*",

@@ -31,2 +34,8 @@ "dev": "rollup -c --watch",

"eslint-plugin-prettier": "^3.1.2",
"jasmine": "^3.5.0",
"karma": "^4.4.1",
"karma-chrome-launcher": "^3.1.0",
"karma-jasmine": "^3.1.1",
"karma-mocha-reporter": "^2.2.5",
"karma-rollup-preprocessor": "^7.0.5",
"prettier": "^2.0.2",

@@ -33,0 +42,0 @@ "rollup": "^2.3.2",

@@ -6,5 +6,12 @@ # simple-drawing-board.js

- 0 dependencies
- Mobile browser, IE11 compatibility
- Only 4.4KB(gzip)
- Modern browser compatibility
- Under 500 lines of code
> For `v2.x` users
> See https://github.com/leader22/simple-drawing-board.js/tree/v2.1.1
> For `v1.x` users
> See https://github.com/leader22/simple-drawing-board.js/tree/v1.4.1
## Install

@@ -22,2 +29,5 @@ ```sh

## How to use
Prepare your `canvas` element.
```html

@@ -27,17 +37,16 @@ <canvas id="canvas" width="500" height="300"></canvas>

Then create drawing board.
```javascript
const sdb = new SimpleDrawingBoard(document.getElementById('canvas'));
import { create } from "simple-drawing-board.js";
// w/ options
const sdb = new SimpleDrawingBoard(document.getElementById('canvas'), {
lineColor: '#000',
lineSize: 5,
boardColor: 'transparent',
historyDepth: 10
});
const sdb = create(document.getElementById("canvas"));
```
## APIs
### setLineSize
```javascript
See also [type definitions](./index.d.ts).
### setLineSize()
```js
sdb.setLineSize(10);

@@ -48,79 +57,90 @@ sdb.setLineSize(0); // to be 1

### setLineColor
```javascript
sdb.setLineColor('#0094c8');
sdb.setLineColor('red');
sdb.setLineColor('#0f0');
### setLineColor()
```js
sdb.setLineColor("#0094c8");
sdb.setLineColor("red");
sdb.setLineColor("#0f0");
```
### fill
```javascript
sdb.fill('#000');
### fill()
```js
sdb.fill("#000");
sdb.fill("orange");
```
### clear
```javascript
sdb.clear(); // fill with default boardColor
### clear()
```js
sdb.clear();
```
### toggleMode
```javascript
### toggleMode()
```js
// switch DRAW <=> ERASE
sdb.toggleMode(); // default is DRAW, so now mode is ERASE
sdb.mode; // "draw"
sdb.toggleMode();
sdb.mode; // "erase"
```
### getImg
```javascript
sdb.getImg(); // 'data:image/png;base64,xxxxxx....'
### toDataURL()
```js
sdb.toDataURL(); // "data:image/png;base64,xxxxxx...."
sdb.toDataURL({ type: "image/jpeg" }); // "data:image/jpeg;base64,xxxxxx...."
sdb.toDataURL({ type: "image/jpeg", quality: 0.3 }); // compression quality
```
### setImg
```javascript
sdb.setImg('data:image/png;base64,xxxxxx....'); // replace
sdb.setImg('data:image/png;base64,xxxxxx....', true); // overlay
### fillImageByElement()
```js
sdb.fillImageByElement(document.getElementById("img"));
```
### undo
```javascript
sdb.undo(); // go back history
### async fillImageByDataURL()
```js
await sdb.fillImageByDataURL("data:image/png;base64,xxxxxx....");
```
### redo
```javascript
sdb.redo(); // go forward history
### async undo()
```js
await sdb.undo();
```
### dispose
```javascript
sdb.dispose(); // remove all events and clear history
### async redo()
```js
await sdb.redo();
```
### destroy()
```js
sdb.destroy();
```
## Events
Available events are below.
```javascript
sdb.ev.on('toggleMode', function(isDrawMode) {
if (isDrawMode) {
console.log('Draw mode.');
} else {
console.log('Erase mode.');
}
});
Events are available via `observer` property.
sdb.ev.on('dispose', function() {
console.log('Do something on dispose.');
### drawBegin
```js
sdb.observer.on("drawBegin", (coords) => {
console.log(coords.x, coords.y);
});
```
sdb.ev.on('drawBegin', function(coords) {
### draw
```js
sdb.observer.on("draw", (coords) => {
console.log(coords.x, coords.y);
});
sdb.ev.on('draw', function(coords) {
```
### drawEnd
```js
sdb.observer.on("drawEnd", (coords) => {
console.log(coords.x, coords.y);
});
sdb.ev.on('drawEnd', function(coords) {
console.log(coords.x, coords.y);
});
```
sdb.ev.on('save', function(curImg) {
console.log(curImg); // 'data:image/png;base64,xxxxxx....'
### save
```js
sdb.observer.on("save", (curImg) => {
console.log(curImg); // "data:image/png;base64,xxxxxx...."
});

@@ -127,0 +147,0 @@ ```

import { terser } from "rollup-plugin-terser";
const config = {
input: "./src/main.js",
input: "./src/index.js",
output: {
file: "./dist/simple-drawing-board.js",
format: "umd",
name: "SimpleDrawingBoard"
}
name: "SimpleDrawingBoard",
},
};

@@ -11,0 +11,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc