import Manager, { DEFAULT_UPDATE_INTERVAL } from '../manager.js';
import Thing from './thing.js';
import WebVideo from './webvideo.js';
export const FULLSCREEN_PADDING = 5;
export const KEYBOARD_NAVIGATION_DOM_ELEMENT_STYLE =
'position: absolute; width: 1px; height: 1px; top: -10px; overflow: hidden;';
export const HIDDEN_KEYBOARD_NAVIGATION_DOM_ELEMENT_STYLE =
KEYBOARD_NAVIGATION_DOM_ELEMENT_STYLE + 'display: none;';
export const HIDDEN_KEYBOARD_NAVIGATION_DOM_ELEMENT_ID = id => `${id}focusbutton`;
/**
* @type {Object.<string, GraphicsManager>}
* @private
*/
export let GraphicsInstances = {};
/**
* @type {Array.<any>}
* @example
* if (pressedKeys.indexOf(Keyboard.SPACE) > -1) {
* alert('you are pressing space!');
* }
*/
export let pressedKeys = [];
let graphicsInstanceID = 0;
/**
* Class for interacting with Graphics.
* @class
*/
class GraphicsManager extends Manager {
elementPool = [];
elementPoolSize = 0;
accessibleDOMElements = [];
/**
* The ratio of physical pixels to CSS pixels for the current device.
* This allows the canvas to be scaled for higher resolution drawing.
* For example, a devicePixelRatio of 2 indicates that the device will use
* 2 physical pixels to draw a single css pixel.
* https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio
* @private
* @type {number}
*/
devicePixelRatio = Math.ceil(window.devicePixelRatio) ?? 1;
/**
* Used to record when a resort is necessary as a result of adding an element with
* an invalidated sort. Sorting will be performed on next redraw when _sortInvalidated
* is true.
* @type {boolean}
* @private
*/
_sortInvalidated = false;
/**
* Set up an instance of the graphics library.
* @constructor
* @param {Object} options - Options, primarily .canvas, the selector
* string for the canvas.
* If multiple are returned, we'll take the first one.
* If none is passed, we'll look for any canvas
* tag on the page.
*/
constructor(options = {}) {
super(options);
this.resetAllState();
this.setCurrentCanvas(options.canvas);
this.onError = options.onError || undefined;
this.fullscreenMode = false;
this.fpsInterval = 1000 / DEFAULT_UPDATE_INTERVAL;
this.lastDrawTime = Date.now();
/**
* Whether the user is using the keyboard to navigate the page.
* This is used to toggle whether the hidden DOM elements used for keyboard
* navigation should show up.
* @private
* @type {boolean}
*/
this.userNavigatingWithKeyboard = false;
this.addEventListeners();
this.shouldUpdate = options.shouldUpdate ?? true;
GraphicsInstances[graphicsInstanceID++] = this;
}
onKeyDown = e => {
const index = pressedKeys.indexOf(e.keyCode);
if (index === -1) {
pressedKeys.push(e.keyCode);
}
if (e.key === 'Tab') {
for (let i = 0; i < this.elementPoolSize; i++) {
const elem = this.elementPool[i];
if (!elem._hasAccessibleDOMElement) {
this.createAccessibleDOMElement(elem);
}
}
this.userNavigatingWithKeyboard = true;
this.showKeyboardNavigationDOMElements();
}
this.keyDownCallback?.(e);
return true;
};
onKeyUp = e => {
const index = pressedKeys.indexOf(e.keyCode);
if (index !== -1) {
pressedKeys.splice(index, 1);
}
this.keyUpCallback?.(e);
};
onResize = e => {
// https://developer.mozilla.org/en-US/docs/Web/Events/resize
// Throttle the resize event handler since it fires at such a rapid rate
// Only respond to the resize event if there's not already a response queued up
if (!this._resizeTimeout) {
this._resizeTimeout = setTimeout(() => {
this._resizeTimeout = null;
this.fullscreenMode && this.setFullscreen?.();
}, DEFAULT_UPDATE_INTERVAL);
}
};
onOrientationChange = e => {
this.deviceOrientationCallback?.(e);
};
onDeviceMotion = e => {
this.deviceMotionCallback?.(e);
};
/**
* Add all handlers to the window for triggering functions on the instance.
*/
addEventListeners() {
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
window.addEventListener('resize', this.onResize);
/** MOBILE DEVICE EVENTS ****/
if (window.DeviceOrientationEvent) {
window.addEventListener('orientationchange', this.onOrientationChange);
}
if (window.DeviceMotionEvent) {
window.addEventListener('devicemotion', this.onDeviceMotion);
}
}
/**
* Remove all handlers from the window and clean up any memory.
*/
cleanup() {
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
window.removeEventListener('resize', this.onResize);
window.removeEventListener('orientationchange', this.onOrientationChange);
window.removeEventListener('devicemotion', this.onDeviceMotion);
}
configure(options = {}) {
this.onError = options.onError || undefined;
}
/**
* Get all living elements.
* @global
* @returns {Array.<Thing>}
*/
getElements() {
return this.elementPool.filter(element => element.alive);
}
/**
* Add an element to the graphics instance.
* @example
* let circle = new Circle(20);
* add(circle);
*
* @global
* @param {Thing} elem - A subclass of Thing to be added to the graphics instance.
*/
add(elem) {
elem.alive = true;
this.elementPool[this.elementPoolSize++] = elem;
if (elem._sortInvalidated) {
this._sortInvalidated = true;
}
}
/**
* Creates a hidden DOM element that can be navigated with a screen reader.
* @private
* @param {Thing} elem
*/
createAccessibleDOMElement(elem) {
const button = document.createElement('button');
// https://webaim.org/techniques/css/invisiblecontent/
button.style = this.userNavigatingWithKeyboard
? KEYBOARD_NAVIGATION_DOM_ELEMENT_STYLE
: HIDDEN_KEYBOARD_NAVIGATION_DOM_ELEMENT_STYLE;
button.id = HIDDEN_KEYBOARD_NAVIGATION_DOM_ELEMENT_ID(elem._id);
button.onfocus = () => {
elem.focus();
button.textContent = elem.describe?.() ?? 'An unknown graphics element';
};
button.onblur = () => {
elem.unfocus();
};
button.onkeydown = e => {
if (e.code === 'Space' && !e.repeat) {
const event = new Event('mousedown');
event.getX = () => elem.x;
event.getY = () => elem.y;
this.mouseDownCallback?.(event);
}
};
button.onkeyup = e => {
if (e.code === 'Space') {
const event = new Event('mouseup');
event.getX = () => elem.x;
event.getY = () => elem.y;
this.mouseUpCallback?.(event);
}
};
document.body.appendChild(button);
this.accessibleDOMElements.push(button);
elem._hasAccessibleDOMElement = true;
}
/**
* Exits keyboard navigation mode.
* @private
*/
exitKeyboardNavigation() {
this.userNavigatingWithKeyboard = false;
this.hideKeyboardNavigationDOMElements();
}
/**
* Makes DOM elements designed to be navigated with keyboard visible so they can be tabbed.
* @private
*/
showKeyboardNavigationDOMElements() {
this.accessibleDOMElements.forEach(
element => (element.style = KEYBOARD_NAVIGATION_DOM_ELEMENT_STYLE)
);
}
/**
* Makes DOM elements designed to be navigated with keyboard invisible.
* This is to make sure they don't accidentally appear and affect layout if they are not needed.
* @private
*/
hideKeyboardNavigationDOMElements() {
this.accessibleDOMElements.forEach(
element => (element.style = HIDDEN_KEYBOARD_NAVIGATION_DOM_ELEMENT_STYLE)
);
}
/**
* Record a click.
* This will cause all timers to be postponed until a click event happens.
* @deprecated
* @global
*/
waitForClick() {
this.clickCount++;
}
/**
* Assign a function as a callback for click (mouse down, mouse up) events.
* @example
* mouseClickMethod(e => {
* alert('You just clicked at ' + e.getX() + ', ' + e.getY());
* });
* @global
* @param {function} fn - A callback to be triggered on click events.
*/
mouseClickMethod(fn) {
this.clickCallback = this.withErrorHandler(fn);
}
/**
* Assign a function as a callback for mouse move events.
* @example
* mouseMoveMethod(e => {
* alert('You moved your mouse to ' + e.getX() + ', ' + e.getY());
* });
* @global
* @param {function} fn - A callback to be triggered on mouse move events.
*/
mouseMoveMethod(fn) {
this.moveCallback = this.withErrorHandler(fn);
}
/**
* Assign a function as a callback for mouse down events.
* @example
* mouseDownMethod(e => {
* alert('You depressed your mouse button at ' + e.getX() + ', ' + e.getY());
* });
* @global
* @param {function} fn - A callback to be triggered on mouse down.
*/
mouseDownMethod(fn) {
this.mouseDownCallback = this.withErrorHandler(fn);
}
/**
* Assign a function as a callback for mouse up events.
* @example
* mouseUpMethod(e => {
* alert('You lifted your mouse button at ' + e.getX() + ', ' + e.getY());
* });
* @global
* @param {function} fn - A callback to be triggered on mouse up events.
*/
mouseUpMethod(fn) {
this.mouseUpCallback = this.withErrorHandler(fn);
}
/**
* Assign a function as a callback for drag events.
* @example
* mouseDragMethod(e => {
* alert('You dragged your mouse to' + e.getX() + ', ' + e.getY());
* });
* @global
* @param {function} fn - A callback to be triggered on drag events.
*/
mouseDragMethod(fn) {
this.dragCallback = this.withErrorHandler(fn);
}
/**
* Assign a function as a callback for keydown events.
* @example
* keyDownMethod(e => {
* if (e.keyCode === Keyboard.letter('A')) {
* alert('You just pushed the a key!');
* }
* })
* @global
* @param {function} fn - A callback to be triggered on keydown events.
*/
keyDownMethod(fn) {
this.keyDownCallback = this.withErrorHandler(fn);
}
/**
* Assign a function as a callback for key up events.
* @example
* keyUpMethod(e => {
* if (e.keyCode === Keyboard.letter('A')) {
* alert('You just lifted the a key!');
* }
* })
* @global
* @param {function} fn - A callback to be triggered on key up events.
*/
keyUpMethod(fn) {
this.keyUpCallback = this.withErrorHandler(fn);
}
/**
* Assign a function as a callback for device orientation events.
* @global
* @param {function} fn - A callback to be triggered on device orientation
* events.
*/
deviceOrientationMethod(fn) {
this.deviceOrientationCallback = this.withErrorHandler(fn);
}
/**
* Assign a function as a callback for device motion events.
* @global
* @param {function} fn - A callback to be triggered device motion events.
*/
deviceMotionMethod(fn) {
this.deviceMotionCallback = this.withErrorHandler(fn);
}
/**
* Check if a key is currently pressed
* @example
* if (isKeyPressed(Keyboard.letter('a'))) {
* alert('Youre currently pressing A!');
* }
* @global
* @param {integer} keyCode - Key code of key being checked.
* @returns {boolean} Whether or not that key is being pressed.
*/
isKeyPressed(keyCode) {
return pressedKeys.indexOf(keyCode) !== -1;
}
/**
* Get the width of the entire graphics canvas.
* @example
* if (getWidth() > 200) {
* alert('The canvas is wider than 200 pixels!');
* }
* @global
* @returns {float} The width of the canvas.
*/
getWidth() {
const canvas = this.getCanvas();
return parseFloat(canvas.getAttribute('width') / this.devicePixelRatio);
}
/**
* Get the height of the entire graphics canvas.
* @example
* if (getHeight() > 200) {
* alert('The canvas is taller than 200 pixels!');
* }
* @global
* @returns {float} The height of the canvas.
*/
getHeight() {
const canvas = this.getCanvas();
return parseFloat(canvas.getAttribute('height') / this.devicePixelRatio);
}
/**
* Stop all timers.
* @global
*/
stopAllTimers() {
for (let i = 1; i < 99999; i++) {
window.clearInterval(i);
}
super.stopAllTimers();
this.setMainTimer();
}
/**
* Create a new timer.
* {@link Manager#setTimer}
* @global
* @param {function} fn - Function to be called at intervals.
* @param {integer} time - Time interval to call function `fn`
* @param {dictionary} data - Any data associated with the timer.
* @param {string} name - Name of this timer.
*/
setTimer(fn, time, data, name) {
if (arguments.length < 2) {
throw new Error(
'2 parameters required for `' +
'setTimer`, ' +
arguments.length +
' found. You must ' +
'provide a callback function and ' +
'a number representing the time delay ' +
'to `setTimer`.'
);
}
if (typeof fn !== 'function') {
throw new TypeError(
'Invalid callback function. ' +
'Make sure you are passing an actual function to ' +
'`setTimer`.'
);
}
if (typeof time !== 'number' || !isFinite(time)) {
throw new TypeError(
'Invalid value for time delay. ' +
'Make sure you are passing a finite number to ' +
'`setTimer` for the delay.'
);
}
if (this.waitingForClick()) {
this.delayedTimers.push({
fn: fn,
time: time,
data: data,
clicks: this.clickCount,
name: name,
});
} else {
return super.setTimer(this.withErrorHandler(fn), time, data, name ?? fn.name);
}
}
/**
* Set the background color of the canvas.
* @global
* @param {Color} color - The desired color of the canvas.
*/
setBackgroundColor(color) {
this.backgroundColor = color;
}
/**
* Clear everything from the canvas.
* @private
*/
clear(context) {
var ctx = context || this.getContext();
ctx.clearRect(0, 0, this.getWidth(), this.getHeight());
}
/**
* Get an element at a specific point.
* If several elements are present at the position, return the one put there first.
* @example
* let circle = new Circle(20);
* circle.setPosition(100, 100);
* add(circle);
*
* getElementAt(100, 100) === circle;
* @global
* @param {number} x - The x coordinate of a point to get element at.
* @param {number} y - The y coordinate of a point to get element at.
* @returns {Thing|null} The object at the point (x, y), if there is one (else null).
*/
getElementAt(x, y) {
for (let i = this.elementPool.length; i--; ) {
if (this.elementPool[i].alive && this.elementPool[i].containsPoint(x, y)) {
return this.elementPool[i];
}
}
return null;
}
/**
* Get all elements at a specific point.
* @example
* let circle = new Circle(20);
* circle.setPosition(100, 100);
* add(circle);
*
* let rectangle = new Rectangle(30, 30);
* rectangle.setPosition(80, 80);
* add(rectangle);
*
* getElementsAt(100, 100)[1] === rectangle;
* @global
* @param {number} x - The x coordinate of a point to get element at.
* @param {number} y - The y coordinate of a point to get element at.
* @returns {Array.<Thing>} The objects at the point (x, y).
*/
getElementsAt(x, y) {
return this.elementPool.filter(e => {
return e.alive && e.containsPoint(x, y);
});
}
/**
* Check if an element exists with the given paramenters.
* @global
* @param {object} params - Dictionary of parameters for the object.
* Includes x, y, heigh, width, color, radius, label and type.
* @returns {boolean}
*/
elementExistsWithParameters(params) {
for (let i = this.elementPool.length; i--; ) {
const elem = this.elementPool[i];
const checkedParams = Object.entries(params).map(([name, value]) => {
return value === elem[name];
});
if (elem.alive && checkedParams.every(param => param)) {
return true;
}
}
return false;
}
/**
* Remove all elements from the canvas.
* @example
* add(new Circle(10));
* add(new Rectangle(30, 30));
* removeAll();
*
* @global
*/
removeAll() {
this.stopAllVideo();
this.elementPool = [];
this.elementPoolSize = 0;
this.accessibleDOMElements.forEach(node => node.remove());
this.accessibleDOMElements = [];
}
/**
* Remove a specific element from the canvas.
* @global
* @param {Thing} elem - The element to be removed from the canvas.
*/
remove(elem) {
if (!(elem instanceof Thing)) {
return;
}
if (elem instanceof WebVideo) {
elem.stop();
}
elem.alive = false;
// mark the element as having invalidated sort, so in the case that it's
// add()ed later, a re-sort will happen and trigger an update in the pool size
elem._sortInvalidated = true;
if (elem._hasAccessibleDOMElement) {
const focusButtonID = HIDDEN_KEYBOARD_NAVIGATION_DOM_ELEMENT_ID(elem._id);
document.getElementById(focusButtonID)?.remove();
elem._hasAccessibleDOMElement = false;
}
}
/**
* Resizes the canvas, creating a temporary canvas to prevent flickering and
* perform size adjustments based on the devices's devicePixelRatio.
* @private
* @param {number} w
* @param {number} h
*/
_resize(w, h) {
w = Math.floor(w);
h = Math.floor(h);
const canvas = this.getCanvas();
// prevent flickering effect by saving the canvas and immediately drawing back.
// this will be cleared in redraw(), but it prevents a jarring
// flickering effect.
const temporaryCanvas = document.createElement('canvas');
temporaryCanvas.width = canvas.width;
temporaryCanvas.height = canvas.height;
temporaryCanvas.style.width = `${canvas.width / this.devicePixelRatio}px`;
temporaryCanvas.style.height = `${canvas.height / this.devicePixelRatio}px`;
const temporaryContext = temporaryCanvas.getContext('2d');
temporaryContext.drawImage(canvas, 0, 0);
canvas.width = w * this.devicePixelRatio;
canvas.height = h * this.devicePixelRatio;
canvas.style.width = `${w}px`;
canvas.style.height = `${h}px`;
const context = this.getContext();
context.drawImage(temporaryCanvas, 0, 0);
context.scale(this.devicePixelRatio, this.devicePixelRatio);
temporaryCanvas.remove();
}
/**
* Set the size of the canvas.
* @global
* @param {number} w - Desired width of the canvas.
* @param {number} h - Desired height of the canvas.
*/
setSize(w, h) {
this.fullscreenMode = false;
this._resize(w, h);
}
/**
* Set the canvas to take up the entire parent element
* @global
*/
setFullscreen() {
this.fullscreenMode = true; // when this is true, canvas will resize with parent
const canvas = this.getCanvas();
const width = canvas.parentElement.offsetWidth - FULLSCREEN_PADDING;
const height = canvas.parentElement.offsetHeight - FULLSCREEN_PADDING;
this._resize(width, height);
}
/**
* Resets all the timers to time 0.
* @global
*/
resetAllTimers() {
for (var cur in this.timers) {
clearInterval(this.timers[cur]);
}
}
/**
* Stop all video elements.
* @private
*/
stopAllVideo() {
for (var i = this.elementPool.length; i--; ) {
if (this.elementPool[i] instanceof WebVideo) {
this.elementPool[i].stop();
}
}
}
/**
* Resets the graphics instance to a clean slate.
* @private
*/
resetAllState() {
this.backgroundColor = null;
this.removeAll();
this.clickCallback = null;
this.moveCallback = null;
this.mouseDownCallback = null;
this.mouseUpCallback = null;
this.dragCallback = null;
this.keyDownCallback = null;
this.keyUpCallback = null;
this.deviceOrientationCallback = null;
this.deviceMotionCallback = null;
// A fast hash from timer key to timer interval #
this.timers = {};
// A useful list to store information about all timers.
this.timersList = [];
this.clickCount = 0;
this.delayedTimers = [];
this.fullscreenMode = false;
}
/**
* Reset all timers to 0 and clear timers and canvas.
* @private
*/
fullReset() {
this.stopAllVideo();
this.resetAllTimers();
this.resetAllState();
this.setMainTimer();
}
/**
* Return if the graphics canvas exists.
* @private
* @returns {boolean} Whether or not the canvas exists.
*/
canvasExists() {
return this.getCanvas() !== null;
}
/**
* Return the current canvas we are using. If there is no
* canvas on the page this will return null.
* @returns {HTMLCanvasElement} The current canvas.
*/
getCanvas() {
return this.currentCanvas;
}
/**
* Set the current canvas we are working with. If no canvas
* tag matches the selectorv then we will just have the current
* canvas set to null.
* @param {string} canvasSelector - String representing canvas class or ID.
* Selected with jQuery.
*/
setCurrentCanvas(canvasSelector) {
let currentCanvas;
if (canvasSelector) {
currentCanvas = document.querySelector(canvasSelector);
} else {
currentCanvas = document.getElementsByTagName('canvas')[0];
}
if (!currentCanvas) {
currentCanvas = document.createElement('canvas');
currentCanvas.width = 400;
currentCanvas.height = 400;
document.body.appendChild(currentCanvas);
}
this.currentCanvas = currentCanvas;
this.setSize(currentCanvas.width, currentCanvas.height);
// On changing the canvas reset the state.
this.fullReset();
this.setup();
}
/**
* Draw the background color for the current object.
* @private
*/
drawBackground() {
if (this.backgroundColor) {
var context = this.getContext();
context.fillStyle = this.backgroundColor;
context.beginPath();
context.rect(0, 0, this.getWidth(), this.getHeight());
context.closePath();
context.fill();
}
}
/**
* Return the 2D graphics context for this graphics
* object, or null if none exists.
* @returns {CanvasRenderingContext2D} The 2D graphics context.
*/
getContext() {
return this.getCanvas()?.getContext?.('2d');
}
/**
* Return the RGBA value of the pixel at the x, y coordinate.
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
* @returns {Array<number>} pixel - the [r, g, b, a] values for the pixel.
*/
getPixel(x, y) {
const context = this.getContext();
x *= this.devicePixelRatio;
y *= this.devicePixelRatio;
const pixelData = context.getImageData(x, y, 1, 1).data;
const index = 0;
return [
pixelData[index + 0],
pixelData[index + 1],
pixelData[index + 2],
pixelData[index + 3],
];
}
/**
* Sort the element pool, putting all elements with .alive=false at the end and
* all elements with lower layer before elements with higher layer.
* @private
*/
sortElementPool() {
this.elementPool.sort((a, b) => b.alive - a.alive || a.layer - b.layer);
let lastAliveElementIndex = -1;
for (let i = this.elementPool.length - 1; i >= 0; i--) {
if (this.elementPool[i].alive) {
lastAliveElementIndex = i;
break;
}
}
this.elementPoolSize = lastAliveElementIndex + 1;
this._sortInvalidated = false;
}
/**
* Redraw this graphics canvas.
* @private
*/
redraw() {
this.clear();
this.drawBackground();
let elem;
let sortPool = this._sortInvalidated;
for (let i = 0; i < this.elementPoolSize; i++) {
elem = this.elementPool[i];
// the pool needs to be resorted if:
// - the graphics manager has an invalid sort (as a result of adding a new element),
// - if an element has an invalid sort (as a result of having its layer changed),
// - or if an element has been removed, which will be true if .alive is false and it
// is within the elementPool < elementPoolSize
sortPool = sortPool || elem._sortInvalidated || !elem.alive;
// mark the element as having valid sort, even though it has not yet been sorted.
// it will be sorted immediately after in sortElementPool
elem._sortInvalidated = false;
}
if (sortPool) {
this.sortElementPool();
}
const context = this.getContext();
for (let i = 0; i < this.elementPoolSize; i++) {
elem = this.elementPool[i];
elem.draw(context);
}
}
/**
* Set the main timer for graphics.
* @private
*/
setMainTimer() {
this.shouldUpdate = true;
this.update();
}
/**
* The main update loop for the Graphics manager.
* @private
*/
update() {
if (this.shouldUpdate) {
requestAnimationFrame(this.update.bind(this));
}
this.now = Date.now();
const elapsed = this.now - this.lastDrawTime;
if (elapsed > this.fpsInterval) {
this.lastDrawTime = this.now - (elapsed % this.fpsInterval);
this.redraw();
}
}
/**
* Whether the graphics instance is waiting for a click.
* @returns {boolean} Whether or not the instance is waiting for a click.
*/
waitingForClick() {
return this.clickCount !== 0;
}
/**
* Whether the selected canvas already has an instance associated.
* @private
*/
canvasHasInstance(canvas) {
let instance;
for (let i = 0; i < allGraphicsInstances.length; i++) {
instance = allGraphicsInstances[i];
if (instance.instanceId !== this.instanceId && instance.getCanvas() === canvas) {
return instance.instanceId;
}
}
return null;
}
/**
* Set up the graphics instance to prepare for interaction
* @private
*/
setup() {
var drawingCanvas = this.getCanvas();
drawingCanvas.onclick = e => {
if (this.waitingForClick()) {
this.clickCount--;
for (var i = 0; i < this.delayedTimers.length; i++) {
var timer = this.delayedTimers[i];
timer.clicks--;
if (timer.clicks === 0) {
this.setTimer(this.withErrorHandler(timer.fn), timer.time, timer.data);
}
}
return;
}
if (this.clickCallback) {
this.clickCallback(e);
}
};
var mouseDown = false;
drawingCanvas.onmousemove = this.withErrorHandler(e => {
if (this.userNavigatingWithKeyboard) {
this.exitKeyboardNavigation();
}
if (this.moveCallback) {
this.moveCallback(e);
}
if (mouseDown && this.dragCallback) {
this.dragCallback(e);
}
});
drawingCanvas.onmousedown = e => {
if (this.userNavigatingWithKeyboard) {
this.exitKeyboardNavigation();
}
mouseDown = true;
if (this.mouseDownCallback) {
this.mouseDownCallback(e);
}
};
drawingCanvas.onmouseup = e => {
if (this.userNavigatingWithKeyboard) {
this.exitKeyboardNavigation();
}
mouseDown = false;
if (this.mouseUpCallback) {
this.mouseUpCallback(e);
}
};
drawingCanvas.ontouchmove = e => {
if (this.userNavigatingWithKeyboard) {
this.exitKeyboardNavigation();
}
e.preventDefault();
if (this.dragCallback) {
this.dragCallback(e);
} else if (this.moveCallback) {
this.moveCallback(e);
}
};
drawingCanvas.ontouchstart = e => {
if (this.userNavigatingWithKeyboard) {
this.exitKeyboardNavigation();
}
e.preventDefault();
if (this.mouseDownCallback) {
this.mouseDownCallback(e);
} else if (this.clickCallback) {
this.clickCallback(e);
}
if (this.waitingForClick()) {
this.clickCount--;
for (var i = 0; i < this.delayedTimers.length; i++) {
var timer = this.delayedTimers[i];
timer.clicks--;
if (timer.clicks === 0) {
this.setTimer(timer.fn, timer.time, timer.data);
}
}
return;
}
};
drawingCanvas.ontouchend = e => {
if (this.userNavigatingWithKeyboard) {
this.exitKeyboardNavigation();
}
e.preventDefault();
if (this.mouseUpCallback) {
this.mouseUpCallback(e);
}
};
}
}
/* Mouse and Touch Event Helpers */
const calculateCoordinates = e => {
const canvas = e.target;
const rect = canvas.getBoundingClientRect();
return {
x: Math.round(e.clientX - rect.left),
y: Math.round(e.clientY - rect.top),
};
};
MouseEvent.prototype.getX = function () {
return calculateCoordinates(this).x;
};
MouseEvent.prototype.getY = function () {
return calculateCoordinates(this).y;
};
if (typeof TouchEvent !== 'undefined') {
TouchEvent.prototype.getX = function () {
return (this.touches.length && calculateCoordinates(this.touches[0]).x) || null;
};
TouchEvent.prototype.getY = function () {
return (this.touches.length && calculateCoordinates(this.touches[0]).y) || null;
};
}
export default GraphicsManager;