simple-drawing-board
Advanced tools
Comparing version 2.1.1 to 3.0.0
module.exports = { | ||
env: { | ||
es6: true, | ||
node: true, | ||
jasmine: true, | ||
browser: true | ||
}, | ||
parserOptions: { | ||
sourceType: "module" | ||
ecmaVersion: 2020, | ||
sourceType: 'module', | ||
}, | ||
@@ -9,0 +12,0 @@ extends: [ |
(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", |
140
README.md
@@ -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 @@ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
164373
22
151
12
1143
1