/* Copyright (c) 2010, Sage Software, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import declare from 'dojo/_base/declare';
import win from 'dojo/window';
import format from '../Format';
import View from '../View';
import getResource from '../I18n';
const resource = getResource('signature');
/**
* @class argos.Views.Signature
* @classdesc Signature View is a view tailored to present an HTML5 canvas that has signature-recording capabilities.
* It goes hand-in-hand with {@link SignatureField SignatureField}
* @extends argos.View
* @requires argos.Format
*/
const __class = declare('argos.Views.Signature', [View], /** @lends argos.Views.Signature# */{
// Localization
/**
* @property {String}
* Text shown in the top toolbar header
*/
titleText: resource.titleText,
/**
* @property {String}
* Text shown in the clear button
*/
clearCanvasText: resource.clearCanvasText,
/**
* @property {String}
* Text shown in the undo button
*/
undoText: resource.undoText,
// Templates
/**
* @property {Simplate}
* Simplate that defines the HTML Markup
*
* * `$` => Signature view instance
*
*/
widgetTemplate: new Simplate([
'<div id="{%= $.id %}" title="{%: $.titleText %}" class="panel {%= $.cls %}">',
'{%! $.canvasTemplate %}',
'<div class="buttons">',
'<button class="button" data-action="_undo"><span>{%: $.undoText %}</span></button>',
'<button class="button" data-action="clearValue"><span>{%: $.clearCanvasText %}</span></button>',
'</div>',
'<div>',
]),
/**
* @property {Simplate}
* Simplate that defines the canvas with a set width and height
*/
canvasTemplate: new Simplate([
'<canvas data-dojo-attach-point="signatureNode" width="{%: $.canvasNodeWidth %}" height="{%: $.canvasNodeHeight %}" data-dojo-attach-event="onmousedown:_penDown,onmousemove:_penMove,onmouseup:_penUp,ontouchstart:_penDown,ontouchmove:_penMove,ontouchend:_penUp"></canvas>',
]),
/**
* @property {HTMLElement}
* The dojo-attach-point for the canvas element
*/
signatureNode: null,
// View Properties
/**
* @property {String}
* The unique view id
*/
id: 'signature_edit',
/**
* @property {Boolean}
* Flag that may be used to control if the view is shown in configurable lists
*/
expose: false,
/**
* @property {Number[][]}
* Stores series of x,y coordinates in the format of: `[[0,0],[1,5]]`
*/
signature: [],
/**
* @property {Number[][]}
* Collection of the touchmove positions
*/
trace: [],
/**
* @property {Object}
* Stores where the last drawn point was
*/
lastpos: {
x: -1,
y: -1,
},
/**
* @cfg {Object}
* Stores the passed options for: `scale`, `lineWidth`, `penColor` and `drawColor`.
*/
config: {
scale: 1,
lineWidth: 3,
penColor: 'blue',
drawColor: 'red',
},
/**
* @property {Boolean}
* Flag for determining if the pen is in "down" state.
*/
isPenDown: false,
/**
* @property {Object}
* The stored 2d context of the canvas node
*/
context: null,
/**
* @property {Number[][]}
* Used to temporarily store the signature
*/
buffer: [],
/**
* @cfg {Number}
* Starting default width of canvas, will be re-sized when the view is shown.
*/
canvasNodeWidth: 360,
/**
* @cfg {Number}
* Starting default height of canvas, will be re-sized when the view is shown.
*/
canvasNodeHeight: 120,
show: function show(options) {
this.inherited(arguments);
if (options && options.lineWidth) {
this.config.lineWidth = options.lineWidth;
}
if (options && options.penColor) {
this.config.penColor = options.penColor;
}
if (options && options.drawColor) {
this.config.drawColor = options.drawColor;
}
this.signature = (options && options.signature) ? options.signature : [];
this._sizeCanvas();
this.context = this.signatureNode.getContext('2d');
$(window).on('resize', this.onResize.bind(this));
this.redraw(this.signature, this.signatureNode, this.config);
},
/**
* Returns the optimized signature array as a JSON string
* @return {String}
*/
getValues: function getValues() {
return JSON.stringify(this.optimizeSignature());
},
/**
* Sets the current value and draws it.
* @param {String} val JSON-stringified Number[][] of x-y coordinates
* @param initial Unused.
*/
setValue: function setValue(val/* , initial*/) {
this.signature = val ? JSON.parse(val) : [];
this.redraw(this.signature, this.signatureNode, this.config);
},
/**
* Clears the value and saves the buffer
*/
clearValue: function clearValue() {
this.buffer = this.signature;
this.setValue('', true);
},
/**
* Returns pointer pixel coordinates [x,y] relative to canvas object
* @param {Event} e
* @return Number[]
*/
_getCoords: function _getCoords(e) {
const offset = $(this.signatureNode).position();
return e.touches ? [
e.touches[0].pageX - offset.left,
e.touches[0].pageY - offset.top,
] : [
e.clientX - offset.left,
e.clientY - offset.top,
];
},
/**
* Handler for `ontouchstart`, records the starting point and sets the state to down
* @param {Event} e
*/
_penDown: function _penDown(e) {
this.isPenDown = true;
this.lastpos = this._getCoords(e);
this.context.lineWidth = this.config.lineWidth;
this.context.strokeStyle = this.config.drawColor;
e.preventDefault();
},
/**
* Handler for `ontouchmove`, draws the lines between the last postition and current position
* @param {Event} e
*/
_penMove: function _penMove(e) {
if (!this.isPenDown) {
return;
}
this.pos = this._getCoords(e);
e.preventDefault();
this.context.beginPath();
this.context.moveTo(this.lastpos[0], this.lastpos[1]);
this.context.lineTo(this.pos[0], this.pos[1]);
this.context.closePath();
this.context.stroke();
e.preventDefault();
this.lastpos = this.pos;
this.trace.push(this.pos);
},
/**
* Handler for `ontouchend`, saves the final signature and redraws the canvas
* @param e
*/
_penUp: function _penUp(e) {
e.preventDefault();
this.isPenDown = false;
if (this.trace.length) {
this.signature.push(this.trace);
}
this.trace = [];
this.context.strokeStyle = this.config.penColor;
this.redraw(this.signature, this.signatureNode, this.config);
},
/**
* Undoes the last pen down-to-pen up line by using the buffer
*/
_undo: function _undo() {
if (this.signature.length) {
this.buffer = this.signature.pop();
if (!this.signature.length) {
this.buffer = [this.buffer];
}
} else if (this.buffer.length) {
this.signature = this.buffer;
}
this.redraw(this.signature, this.signatureNode, this.config);
},
/**
* Sets the canvas width/height based on the size of the window/screen
*/
_sizeCanvas: function _sizeCanvas() {
this.canvasNodeWidth = Math.floor(win.getBox().w * 0.92);
this.canvasNodeHeight = Math.min(
Math.floor(this.canvasNodeWidth * 0.5),
win.getBox().h - $('.toolbar').get(0).offsetHeight
);
this.signatureNode.width = this.canvasNodeWidth;
this.signatureNode.height = this.canvasNodeHeight;
},
/**
* Calls {@link #_sizeCanvas _sizeCanvas} to size the canvas itself then it also scales the
* drawn signature accordingly to the ratio.
* @param e
*/
onResize: function onResize(/* e*/) {
const oldWidth = this.canvasNodeWidth;
const oldHeight = this.canvasNodeHeight;
this._sizeCanvas();
const newScale = Math.min(
this.canvasNodeWidth / oldWidth,
this.canvasNodeHeight / oldHeight
);
this.signature = this.rescale(newScale);
this.redraw(this.signature, this.signatureNode, this.config);
},
/**
* Calls {@link format#canvasDraw format.canvasDraw} which clears and draws the vectors
* @param {Number[][]} vector The x-y coordinates of the points
* @param {HTMLElement} canvas Canvas to be drawn to
* @param {Object} options Options to be passed to canvasDraw
*/
redraw: function redraw(vector, canvas, options) {
format.canvasDraw(vector, canvas, options);
},
/**
* Loops through the vector points in the signature and applies the given scale ratio
* @param {Number} scale Ratio in which to multiply the vector point
* @return {Number[][]} Rescaled signature array
*/
rescale: function rescale(scale) {
const rescaled = [];
for (let i = 0; i < this.signature.length; i++) {
rescaled.push([]);
for (let j = 0; j < this.signature[i].length; j++) {
rescaled[i].push([
this.signature[i][j][0] * scale,
this.signature[i][j][1] * scale,
]);
}
}
return rescaled;
},
/**
* Loops the signature calling optimize on each pen down-to-pen up segment
* @return {Number[][]} Optimized signature
*/
optimizeSignature: function optimizeSignature() {
const optimized = [];
for (let i = 0; i < this.signature.length; i++) {
optimized.push(this.optimize(this.signature[i]));
}
return optimized;
},
/**
* Attempts to remove points by comparing the x/y variation between the two and
* removing points within a certain threshold.
* @param {Number[]} vector Array of x,y coordinates to optimize
* @return {Number[]} Optimized array
*/
optimize: function optimize(vector) {
if (vector.length < 2) {
return vector;
}
const result = [];
const minA = 0.95;
const maxL = 15.0; // 15.0, 10.0 works well
let rootP = vector[0];
let lastP = vector[1];
let rootV = [lastP[0] - rootP[0], lastP[1] - rootP[1]];
let rootL = Math.sqrt(rootV[0] * rootV[0] + rootV[1] * rootV[1]);
for (let i = 2; i < vector.length; i++) {
const currentP = vector[i];
const currentV = [currentP[0] - rootP[0], currentP[1] - rootP[1]];
const currentL = Math.sqrt(currentV[0] * currentV[0] + currentV[1] * currentV[1]);
const dotProduct = (rootV[0] * currentV[0] + rootV[1] * currentV[1]) / (rootL * currentL);
if (dotProduct < minA || currentL > maxL) {
result.push(rootP);
rootP = lastP;
lastP = currentP;
rootV = [lastP[0] - rootP[0], lastP[1] - rootP[1]];
rootL = Math.sqrt(rootV[0] * rootV[0] + rootV[1] * rootV[1]);
} else {
lastP = currentP;
}
}
result.push(lastP);
return result;
},
});
export default __class;