UNPKG

@cropper/element-canvas

Version:
906 lines (897 loc) 36.9 kB
(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; }));