@cropper/element-canvas
Version:
A custom canvas element for the Cropper.
906 lines (897 loc) • 36.9 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.CropperCanvas = factory());
})(this, (function () { 'use strict';
const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined';
const WINDOW = IS_BROWSER ? window : {};
const IS_TOUCH_DEVICE = IS_BROWSER ? 'ontouchstart' in WINDOW.document.documentElement : false;
const HAS_POINTER_EVENT = IS_BROWSER ? 'PointerEvent' in WINDOW : false;
const NAMESPACE = 'cropper';
const CROPPER_CANVAS = `${NAMESPACE}-canvas`;
const CROPPER_IMAGE = `${NAMESPACE}-image`;
const ACTION_SCALE = 'scale';
const ACTION_ROTATE = 'rotate';
const ACTION_TRANSFORM = 'transform';
const ACTION_NONE = 'none';
// Attributes
const ATTRIBUTE_ACTION = 'action';
// Native events
const EVENT_TOUCH_END = IS_TOUCH_DEVICE ? 'touchend touchcancel' : 'mouseup';
const EVENT_TOUCH_MOVE = IS_TOUCH_DEVICE ? 'touchmove' : 'mousemove';
const EVENT_TOUCH_START = IS_TOUCH_DEVICE ? 'touchstart' : 'mousedown';
const EVENT_POINTER_DOWN = HAS_POINTER_EVENT ? 'pointerdown' : EVENT_TOUCH_START;
const EVENT_POINTER_MOVE = HAS_POINTER_EVENT ? 'pointermove' : EVENT_TOUCH_MOVE;
const EVENT_POINTER_UP = HAS_POINTER_EVENT ? 'pointerup pointercancel' : EVENT_TOUCH_END;
const EVENT_WHEEL = 'wheel';
// Custom events
const EVENT_ACTION = 'action';
const EVENT_ACTION_END = 'actionend';
const EVENT_ACTION_MOVE = 'actionmove';
const EVENT_ACTION_START = 'actionstart';
/**
* Check if the given value is a string.
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if the given value is a string, else `false`.
*/
function isString(value) {
return typeof value === 'string';
}
/**
* Check if the given value is not a number.
*/
const isNaN = Number.isNaN || WINDOW.isNaN;
/**
* Check if the given value is a number.
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if the given value is a number, else `false`.
*/
function isNumber(value) {
return typeof value === 'number' && !isNaN(value);
}
/**
* Check if the given value is a positive number.
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if the given value is a positive number, else `false`.
*/
function isPositiveNumber(value) {
return isNumber(value) && value > 0 && value < Infinity;
}
/**
* Check if the given value is undefined.
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if the given value is undefined, else `false`.
*/
function isUndefined(value) {
return typeof value === 'undefined';
}
/**
* Check if the given value is an object.
* @param {*} value - The value to check.
* @returns {boolean} Returns `true` if the given value is an object, else `false`.
*/
function isObject(value) {
return typeof value === 'object' && value !== null;
}
const { hasOwnProperty } = Object.prototype;
/**
* Check if the given value is a plain object.
* @param {*} value - The value to check.
* @returns {boolean} Returns `true` if the given value is a plain object, else `false`.
*/
function isPlainObject(value) {
if (!isObject(value)) {
return false;
}
try {
const { constructor } = value;
const { prototype } = constructor;
return constructor && prototype && hasOwnProperty.call(prototype, 'isPrototypeOf');
}
catch (error) {
return false;
}
}
/**
* Check if the given value is a function.
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if the given value is a function, else `false`.
*/
function isFunction(value) {
return typeof value === 'function';
}
/**
* Check if the given node is an element.
* @param {*} node The node to check.
* @returns {boolean} Returns `true` if the given node is an element; otherwise, `false`.
*/
function isElement(node) {
return typeof node === 'object' && node !== null && node.nodeType === 1;
}
const REGEXP_CAMEL_CASE = /([a-z\d])([A-Z])/g;
/**
* Transform the given string from camelCase to kebab-case.
* @param {string} value The value to transform.
* @returns {string} Returns the transformed value.
*/
function toKebabCase(value) {
return String(value).replace(REGEXP_CAMEL_CASE, '$1-$2').toLowerCase();
}
const REGEXP_KEBAB_CASE = /-[A-z\d]/g;
/**
* Transform the given string from kebab-case to camelCase.
* @param {string} value The value to transform.
* @returns {string} Returns the transformed value.
*/
function toCamelCase(value) {
return value.replace(REGEXP_KEBAB_CASE, (substring) => substring.slice(1).toUpperCase());
}
const REGEXP_SPACES = /\s\s*/;
/**
* Remove event listener from the event target.
* {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener}
* @param {EventTarget} target The target of the event.
* @param {string} types The types of the event.
* @param {EventListenerOrEventListenerObject} listener The listener of the event.
* @param {EventListenerOptions} [options] The options specify characteristics about the event listener.
*/
function off(target, types, listener, options) {
types.trim().split(REGEXP_SPACES).forEach((type) => {
target.removeEventListener(type, listener, options);
});
}
/**
* Add event listener to the event target.
* {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener}
* @param {EventTarget} target The target of the event.
* @param {string} types The types of the event.
* @param {EventListenerOrEventListenerObject} listener The listener of the event.
* @param {AddEventListenerOptions} [options] The options specify characteristics about the event listener.
*/
function on(target, types, listener, options) {
types.trim().split(REGEXP_SPACES).forEach((type) => {
target.addEventListener(type, listener, options);
});
}
const defaultEventOptions = {
bubbles: true,
cancelable: true,
composed: true,
};
/**
* Dispatch event on the event target.
* {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent}
* @param {EventTarget} target The target of the event.
* @param {string} type The name of the event.
* @param {*} [detail] The data passed when initializing the event.
* @param {CustomEventInit} [options] The other event options.
* @returns {boolean} Returns the result value.
*/
function emit(target, type, detail, options) {
return target.dispatchEvent(new CustomEvent(type, Object.assign(Object.assign(Object.assign({}, defaultEventOptions), { detail }), options)));
}
const resolvedPromise = Promise.resolve();
/**
* Defers the callback to be executed after the next DOM update cycle.
* @param {*} [context] The `this` context.
* @param {Function} [callback] The callback to execute after the next DOM update cycle.
* @returns {Promise} A promise that resolves to nothing.
*/
function nextTick(context, callback) {
return callback
? resolvedPromise.then(context ? callback.bind(context) : callback)
: resolvedPromise;
}
const SIZE_ADJUSTMENT_TYPE_CONTAIN = 'contain';
const SIZE_ADJUSTMENT_TYPE_COVER = 'cover';
/**
* Get the max sizes in a rectangle under the given aspect ratio.
* @param {object} data The original sizes.
* @param {string} [type] The adjust type.
* @returns {object} Returns the result sizes.
*/
function getAdjustedSizes(data, type = SIZE_ADJUSTMENT_TYPE_CONTAIN) {
const { aspectRatio } = data;
let { width, height } = data;
const isValidWidth = isPositiveNumber(width);
const isValidHeight = isPositiveNumber(height);
if (isValidWidth && isValidHeight) {
const adjustedWidth = height * aspectRatio;
if ((type === SIZE_ADJUSTMENT_TYPE_CONTAIN && adjustedWidth > width)
|| (type === SIZE_ADJUSTMENT_TYPE_COVER && adjustedWidth < width)) {
height = width / aspectRatio;
}
else {
width = height * aspectRatio;
}
}
else if (isValidWidth) {
height = width / aspectRatio;
}
else if (isValidHeight) {
width = height * aspectRatio;
}
return {
width,
height,
};
}
var style$1 = `:host([hidden]){display:none!important}`;
const REGEXP_SUFFIX = /left|top|width|height/i;
const DEFAULT_SHADOW_ROOT_MODE = 'open';
const shadowRoots = new WeakMap();
const styleSheets = new WeakMap();
const tagNames = new Map();
const supportsAdoptedStyleSheets = WINDOW.document && Array.isArray(WINDOW.document.adoptedStyleSheets) && 'replaceSync' in WINDOW.CSSStyleSheet.prototype;
class CropperElement extends HTMLElement {
get $sharedStyle() {
return `${this.themeColor ? `:host{--theme-color: ${this.themeColor};}` : ''}${style$1}`;
}
constructor() {
var _a, _b;
super();
this.shadowRootMode = DEFAULT_SHADOW_ROOT_MODE;
this.slottable = true;
const name = (_b = (_a = Object.getPrototypeOf(this)) === null || _a === void 0 ? void 0 : _a.constructor) === null || _b === void 0 ? void 0 : _b.$name;
if (name) {
tagNames.set(name, this.tagName.toLowerCase());
}
}
static get observedAttributes() {
return [
'shadow-root-mode',
'slottable',
'theme-color',
];
}
// Convert attribute to property
attributeChangedCallback(name, oldValue, newValue) {
if (Object.is(newValue, oldValue)) {
return;
}
const propertyName = toCamelCase(name);
const oldPropertyValue = this[propertyName];
let newPropertyValue = newValue;
switch (typeof oldPropertyValue) {
case 'boolean':
newPropertyValue = newValue !== null && newValue !== 'false';
break;
case 'number':
newPropertyValue = Number(newValue);
break;
}
this[propertyName] = newPropertyValue;
switch (name) {
case 'theme-color': {
const styleSheet = styleSheets.get(this);
const styles = this.$sharedStyle;
if (styleSheet && styles) {
if (supportsAdoptedStyleSheets) {
styleSheet.replaceSync(styles);
}
else {
styleSheet.textContent = styles;
}
}
break;
}
}
}
// Convert property to attribute
$propertyChangedCallback(name, oldValue, newValue) {
if (Object.is(newValue, oldValue)) {
return;
}
name = toKebabCase(name);
switch (typeof newValue) {
case 'boolean':
if (newValue === true) {
if (!this.hasAttribute(name)) {
this.setAttribute(name, '');
}
}
else {
this.removeAttribute(name);
}
break;
case 'number':
if (isNaN(newValue)) {
newValue = '';
}
else {
newValue = String(newValue);
}
// Fall through
// case 'string':
// eslint-disable-next-line no-fallthrough
default:
if (newValue) {
if (this.getAttribute(name) !== newValue) {
this.setAttribute(name, newValue);
}
}
else {
this.removeAttribute(name);
}
}
}
connectedCallback() {
// Observe properties after observed attributes
Object.getPrototypeOf(this).constructor.observedAttributes.forEach((attribute) => {
const property = toCamelCase(attribute);
let value = this[property];
if (!isUndefined(value)) {
this.$propertyChangedCallback(property, undefined, value);
}
Object.defineProperty(this, property, {
enumerable: true,
configurable: true,
get() {
return value;
},
set(newValue) {
const oldValue = value;
value = newValue;
this.$propertyChangedCallback(property, oldValue, newValue);
},
});
});
const shadow = this.attachShadow({
mode: this.shadowRootMode || DEFAULT_SHADOW_ROOT_MODE,
});
if (!this.shadowRoot) {
shadowRoots.set(this, shadow);
}
styleSheets.set(this, this.$addStyles(this.$sharedStyle));
if (this.$style) {
this.$addStyles(this.$style);
}
if (this.$template) {
const template = document.createElement('template');
template.innerHTML = this.$template;
shadow.appendChild(template.content);
}
if (this.slottable) {
const slot = document.createElement('slot');
shadow.appendChild(slot);
}
}
disconnectedCallback() {
if (styleSheets.has(this)) {
styleSheets.delete(this);
}
if (shadowRoots.has(this)) {
shadowRoots.delete(this);
}
}
// eslint-disable-next-line class-methods-use-this
$getTagNameOf(name) {
var _a;
return (_a = tagNames.get(name)) !== null && _a !== void 0 ? _a : name;
}
$setStyles(properties) {
Object.keys(properties).forEach((property) => {
let value = properties[property];
if (isNumber(value)) {
if (value !== 0 && REGEXP_SUFFIX.test(property)) {
value = `${value}px`;
}
else {
value = String(value);
}
}
this.style[property] = value;
});
return this;
}
/**
* Outputs the shadow root of the element.
* @returns {ShadowRoot} Returns the shadow root.
*/
$getShadowRoot() {
return this.shadowRoot || shadowRoots.get(this);
}
/**
* Adds styles to the shadow root.
* @param {string} styles The styles to add.
* @returns {CSSStyleSheet|HTMLStyleElement} Returns the generated style sheet.
*/
$addStyles(styles) {
let styleSheet;
const shadow = this.$getShadowRoot();
if (supportsAdoptedStyleSheets) {
styleSheet = new CSSStyleSheet();
styleSheet.replaceSync(styles);
shadow.adoptedStyleSheets = shadow.adoptedStyleSheets.concat(styleSheet);
}
else {
styleSheet = document.createElement('style');
styleSheet.textContent = styles;
shadow.appendChild(styleSheet);
}
return styleSheet;
}
/**
* Dispatches an event at the element.
* @param {string} type The name of the event.
* @param {*} [detail] The data passed when initializing the event.
* @param {CustomEventInit} [options] The other event options.
* @returns {boolean} Returns the result value.
*/
$emit(type, detail, options) {
return emit(this, type, detail, options);
}
/**
* Defers the callback to be executed after the next DOM update cycle.
* @param {Function} [callback] The callback to execute after the next DOM update cycle.
* @returns {Promise} A promise that resolves to nothing.
*/
$nextTick(callback) {
return nextTick(this, callback);
}
/**
* Defines the constructor as a new custom element.
* {@link https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define}
* @param {string|object} [name] The element name.
* @param {object} [options] The element definition options.
*/
static $define(name, options) {
if (isObject(name)) {
options = name;
name = '';
}
if (!name) {
name = this.$name || this.name;
}
name = toKebabCase(name);
if (IS_BROWSER && WINDOW.customElements && !WINDOW.customElements.get(name)) {
customElements.define(name, this, options);
}
}
}
CropperElement.$version = '2.0.0';
var style = `:host{display:block;min-height:100px;min-width:200px;overflow:hidden;position:relative;touch-action:none;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}:host([background]){background-color:#fff;background-image:repeating-linear-gradient(45deg,#ccc 25%,transparent 0,transparent 75%,#ccc 0,#ccc),repeating-linear-gradient(45deg,#ccc 25%,transparent 0,transparent 75%,#ccc 0,#ccc);background-image:repeating-conic-gradient(#ccc 0 25%,#fff 0 50%);background-position:0 0,.5rem .5rem;background-size:1rem 1rem}:host([disabled]){pointer-events:none}:host([disabled]):after{bottom:0;content:"";cursor:not-allowed;display:block;left:0;pointer-events:none;position:absolute;right:0;top:0}`;
class CropperCanvas extends CropperElement {
constructor() {
super(...arguments);
this.$onPointerDown = null;
this.$onPointerMove = null;
this.$onPointerUp = null;
this.$onWheel = null;
this.$wheeling = false;
this.$pointers = new Map();
this.$style = style;
this.$action = ACTION_NONE;
this.background = false;
this.disabled = false;
this.scaleStep = 0.1;
this.themeColor = '#39f';
}
static get observedAttributes() {
return super.observedAttributes.concat([
'background',
'disabled',
'scale-step',
]);
}
connectedCallback() {
super.connectedCallback();
if (!this.disabled) {
this.$bind();
}
}
disconnectedCallback() {
if (!this.disabled) {
this.$unbind();
}
super.disconnectedCallback();
}
$propertyChangedCallback(name, oldValue, newValue) {
if (Object.is(newValue, oldValue)) {
return;
}
super.$propertyChangedCallback(name, oldValue, newValue);
switch (name) {
case 'disabled':
if (newValue) {
this.$unbind();
}
else {
this.$bind();
}
break;
}
}
$bind() {
if (!this.$onPointerDown) {
this.$onPointerDown = this.$handlePointerDown.bind(this);
on(this, EVENT_POINTER_DOWN, this.$onPointerDown);
}
if (!this.$onPointerMove) {
this.$onPointerMove = this.$handlePointerMove.bind(this);
on(this.ownerDocument, EVENT_POINTER_MOVE, this.$onPointerMove);
}
if (!this.$onPointerUp) {
this.$onPointerUp = this.$handlePointerUp.bind(this);
on(this.ownerDocument, EVENT_POINTER_UP, this.$onPointerUp);
}
if (!this.$onWheel) {
this.$onWheel = this.$handleWheel.bind(this);
on(this, EVENT_WHEEL, this.$onWheel, {
passive: false,
capture: true,
});
}
}
$unbind() {
if (this.$onPointerDown) {
off(this, EVENT_POINTER_DOWN, this.$onPointerDown);
this.$onPointerDown = null;
}
if (this.$onPointerMove) {
off(this.ownerDocument, EVENT_POINTER_MOVE, this.$onPointerMove);
this.$onPointerMove = null;
}
if (this.$onPointerUp) {
off(this.ownerDocument, EVENT_POINTER_UP, this.$onPointerUp);
this.$onPointerUp = null;
}
if (this.$onWheel) {
off(this, EVENT_WHEEL, this.$onWheel, {
capture: true,
});
this.$onWheel = null;
}
}
$handlePointerDown(event) {
const { buttons, button, type } = event;
if (this.disabled || (
// Handle pointer or mouse event, and ignore touch event
((type === 'pointerdown' && event.pointerType === 'mouse') || type === 'mousedown') && (
// No primary button (Usually the left button)
(isNumber(buttons) && buttons !== 1) || (isNumber(button) && button !== 0)
// Open context menu
|| event.ctrlKey))) {
return;
}
const { $pointers } = this;
let action = '';
if (event.changedTouches) {
Array.from(event.changedTouches).forEach(({ identifier, pageX, pageY, }) => {
$pointers.set(identifier, {
startX: pageX,
startY: pageY,
endX: pageX,
endY: pageY,
});
});
}
else {
const { pointerId = 0, pageX, pageY } = event;
$pointers.set(pointerId, {
startX: pageX,
startY: pageY,
endX: pageX,
endY: pageY,
});
}
if ($pointers.size > 1) {
action = ACTION_TRANSFORM;
}
else if (isElement(event.target)) {
action = event.target.action || event.target.getAttribute(ATTRIBUTE_ACTION) || '';
}
if (this.$emit(EVENT_ACTION_START, {
action,
relatedEvent: event,
}) === false) {
return;
}
// Prevent page zooming in the browsers for iOS.
event.preventDefault();
this.$action = action;
this.style.willChange = 'transform';
}
$handlePointerMove(event) {
const { $action, $pointers } = this;
if (this.disabled || $action === ACTION_NONE || $pointers.size === 0) {
return;
}
if (this.$emit(EVENT_ACTION_MOVE, {
action: $action,
relatedEvent: event,
}) === false) {
return;
}
// Prevent page scrolling.
event.preventDefault();
if (event.changedTouches) {
Array.from(event.changedTouches).forEach(({ identifier, pageX, pageY, }) => {
const pointer = $pointers.get(identifier);
if (pointer) {
Object.assign(pointer, {
endX: pageX,
endY: pageY,
});
}
});
}
else {
const { pointerId = 0, pageX, pageY } = event;
const pointer = $pointers.get(pointerId);
if (pointer) {
Object.assign(pointer, {
endX: pageX,
endY: pageY,
});
}
}
const detail = {
action: $action,
relatedEvent: event,
};
if ($action === ACTION_TRANSFORM) {
const pointers2 = new Map($pointers);
let maxRotateRate = 0;
let maxScaleRate = 0;
let rotate = 0;
let scale = 0;
let centerX = event.pageX;
let centerY = event.pageY;
$pointers.forEach((pointer, pointerId) => {
pointers2.delete(pointerId);
pointers2.forEach((pointer2) => {
let x1 = pointer2.startX - pointer.startX;
let y1 = pointer2.startY - pointer.startY;
let x2 = pointer2.endX - pointer.endX;
let y2 = pointer2.endY - pointer.endY;
let z1 = 0;
let z2 = 0;
let a1 = 0;
let a2 = 0;
if (x1 === 0) {
if (y1 < 0) {
a1 = Math.PI * 2;
}
else if (y1 > 0) {
a1 = Math.PI;
}
}
else if (x1 > 0) {
a1 = (Math.PI / 2) + Math.atan(y1 / x1);
}
else if (x1 < 0) {
a1 = (Math.PI * 1.5) + Math.atan(y1 / x1);
}
if (x2 === 0) {
if (y2 < 0) {
a2 = Math.PI * 2;
}
else if (y2 > 0) {
a2 = Math.PI;
}
}
else if (x2 > 0) {
a2 = (Math.PI / 2) + Math.atan(y2 / x2);
}
else if (x2 < 0) {
a2 = (Math.PI * 1.5) + Math.atan(y2 / x2);
}
if (a2 > 0 || a1 > 0) {
const rotateRate = a2 - a1;
const absRotateRate = Math.abs(rotateRate);
if (absRotateRate > maxRotateRate) {
maxRotateRate = absRotateRate;
rotate = rotateRate;
centerX = (pointer.startX + pointer2.startX) / 2;
centerY = (pointer.startY + pointer2.startY) / 2;
}
}
x1 = Math.abs(x1);
y1 = Math.abs(y1);
x2 = Math.abs(x2);
y2 = Math.abs(y2);
if (x1 > 0 && y1 > 0) {
z1 = Math.sqrt((x1 * x1) + (y1 * y1));
}
else if (x1 > 0) {
z1 = x1;
}
else if (y1 > 0) {
z1 = y1;
}
if (x2 > 0 && y2 > 0) {
z2 = Math.sqrt((x2 * x2) + (y2 * y2));
}
else if (x2 > 0) {
z2 = x2;
}
else if (y2 > 0) {
z2 = y2;
}
if (z1 > 0 && z2 > 0) {
const scaleRate = (z2 - z1) / z1;
const absScaleRate = Math.abs(scaleRate);
if (absScaleRate > maxScaleRate) {
maxScaleRate = absScaleRate;
scale = scaleRate;
centerX = (pointer.startX + pointer2.startX) / 2;
centerY = (pointer.startY + pointer2.startY) / 2;
}
}
});
});
const rotatable = maxRotateRate > 0;
const scalable = maxScaleRate > 0;
if (rotatable && scalable) {
detail.rotate = rotate;
detail.scale = scale;
detail.centerX = centerX;
detail.centerY = centerY;
}
else if (rotatable) {
detail.action = ACTION_ROTATE;
detail.rotate = rotate;
detail.centerX = centerX;
detail.centerY = centerY;
}
else if (scalable) {
detail.action = ACTION_SCALE;
detail.scale = scale;
detail.centerX = centerX;
detail.centerY = centerY;
}
else {
detail.action = ACTION_NONE;
}
}
else {
const [pointer] = Array.from($pointers.values());
Object.assign(detail, pointer);
}
// Override the starting coordinate
$pointers.forEach((pointer) => {
pointer.startX = pointer.endX;
pointer.startY = pointer.endY;
});
if (detail.action !== ACTION_NONE) {
this.$emit(EVENT_ACTION, detail, {
cancelable: false,
});
}
}
$handlePointerUp(event) {
const { $action, $pointers } = this;
if (this.disabled || $action === ACTION_NONE) {
return;
}
if (this.$emit(EVENT_ACTION_END, {
action: $action,
relatedEvent: event,
}) === false) {
return;
}
event.preventDefault();
if (event.changedTouches) {
Array.from(event.changedTouches).forEach(({ identifier, }) => {
$pointers.delete(identifier);
});
}
else {
const { pointerId = 0 } = event;
$pointers.delete(pointerId);
}
if ($pointers.size === 0) {
this.style.willChange = '';
this.$action = ACTION_NONE;
}
}
$handleWheel(event) {
if (this.disabled) {
return;
}
event.preventDefault();
// Limit wheel speed to prevent zoom too fast (#21)
if (this.$wheeling) {
return;
}
this.$wheeling = true;
// Debounce by 50ms
setTimeout(() => {
this.$wheeling = false;
}, 50);
const delta = event.deltaY > 0 ? -1 : 1;
const scale = delta * this.scaleStep;
this.$emit(EVENT_ACTION, {
action: ACTION_SCALE,
scale,
relatedEvent: event,
}, {
cancelable: false,
});
}
/**
* Changes the current action to a new one.
* @param {string} action The new action.
* @returns {CropperCanvas} Returns `this` for chaining.
*/
$setAction(action) {
if (isString(action)) {
this.$action = action;
}
return this;
}
/**
* Generates a real canvas element, with the image draw into if there is one.
* @param {object} [options] The available options.
* @param {number} [options.width] The width of the canvas.
* @param {number} [options.height] The height of the canvas.
* @param {Function} [options.beforeDraw] The function called before drawing the image onto the canvas.
* @returns {Promise} Returns a promise that resolves to the generated canvas element.
*/
$toCanvas(options) {
return new Promise((resolve, reject) => {
if (!this.isConnected) {
reject(new Error('The current element is not connected to the DOM.'));
return;
}
const canvas = document.createElement('canvas');
let width = this.offsetWidth;
let height = this.offsetHeight;
let scale = 1;
if (isPlainObject(options)
&& (isPositiveNumber(options.width) || isPositiveNumber(options.height))) {
({ width, height } = getAdjustedSizes({
aspectRatio: width / height,
width: options.width,
height: options.height,
}));
scale = width / this.offsetWidth;
}
canvas.width = width;
canvas.height = height;
const cropperImage = this.querySelector(this.$getTagNameOf(CROPPER_IMAGE));
if (!cropperImage) {
resolve(canvas);
return;
}
cropperImage.$ready().then((image) => {
const context = canvas.getContext('2d');
if (context) {
const [a, b, c, d, e, f] = cropperImage.$getTransform();
let newE = e;
let newF = f;
let destWidth = image.naturalWidth;
let destHeight = image.naturalHeight;
if (scale !== 1) {
newE *= scale;
newF *= scale;
destWidth *= scale;
destHeight *= scale;
}
const centerX = destWidth / 2;
const centerY = destHeight / 2;
context.fillStyle = 'transparent';
context.fillRect(0, 0, width, height);
if (isPlainObject(options) && isFunction(options.beforeDraw)) {
options.beforeDraw.call(this, context, canvas);
}
context.save();
// Move the transform origin to the center of the image.
// https://developer.mozilla.org/en-US/docs/Web/CSS/transform-origin
context.translate(centerX, centerY);
context.transform(a, b, c, d, newE, newF);
// Reset the transform origin to the top-left of the image.
context.translate(-centerX, -centerY);
context.drawImage(image, 0, 0, destWidth, destHeight);
context.restore();
}
resolve(canvas);
}).catch(reject);
});
}
}
CropperCanvas.$name = CROPPER_CANVAS;
CropperCanvas.$version = '2.0.0';
return CropperCanvas;
}));