UNPKG

@cropper/element-image

Version:
972 lines (966 loc) 37.9 kB
const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined'; const WINDOW = IS_BROWSER ? window : {}; IS_BROWSER ? 'ontouchstart' in WINDOW.document.documentElement : false; const NAMESPACE = 'cropper'; const CROPPER_CANVAS = `${NAMESPACE}-canvas`; const CROPPER_IMAGE = `${NAMESPACE}-image`; const CROPPER_SELECTION = `${NAMESPACE}-selection`; const ACTION_MOVE = 'move'; const ACTION_SCALE = 'scale'; const ACTION_ROTATE = 'rotate'; const ACTION_TRANSFORM = 'transform'; const ACTION_NONE = 'none'; const EVENT_ERROR = 'error'; const EVENT_LOAD = 'load'; // Custom events const EVENT_ACTION = 'action'; const EVENT_ACTION_END = 'actionend'; const EVENT_ACTION_START = 'actionstart'; const EVENT_TRANSFORM = 'transform'; /** * 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 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; } /** * 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'; } 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); }); } /** * Add once event listener to the event target. * @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 once(target, types, listener, options) { on(target, types, listener, Object.assign(Object.assign({}, options), { once: true })); } 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 REGEXP_ANGLE_UNIT = /deg|g?rad|turn$/i; /** * Convert an angle to a radian number. * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/angle} * @param {number|string} angle The angle to convert. * @returns {number} Returns the radian number. */ function toAngleInRadian(angle) { const value = parseFloat(angle) || 0; if (value !== 0) { const [unit = 'rad'] = String(angle).match(REGEXP_ANGLE_UNIT) || []; switch (unit.toLowerCase()) { case 'deg': return (value / 360) * (Math.PI * 2); case 'grad': return (value / 400) * (Math.PI * 2); case 'turn': return value * (Math.PI * 2); } } return value; } /** * Multiply multiple matrices. * @param {Array} matrix The first matrix. * @param {Array} args The rest matrices. * @returns {Array} Returns the result matrix. */ function multiplyMatrices(matrix, ...args) { if (args.length === 0) { return matrix; } const [a1, b1, c1, d1, e1, f1] = matrix; const [a2, b2, c2, d2, e2, f2] = args[0]; // ┌ a1 c1 e1 ┐ ┌ a2 c2 e2 ┐ // │ b1 d1 f1 │ × │ b2 d2 f2 │ // └ 0 0 1 ┘ └ 0 0 1 ┘ matrix = [ a1 * a2 + c1 * b2 /* + e1 * 0 */, b1 * a2 + d1 * b2 /* + f1 * 0 */, a1 * c2 + c1 * d2 /* + e1 * 0 */, b1 * c2 + d1 * d2 /* + f1 * 0 */, a1 * e2 + c1 * f2 + e1 /* * 1 */, b1 * e2 + d1 * f2 + f1 /* * 1 */, ]; return multiplyMatrices(matrix, ...args.slice(1)); } 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:inline-block}img{display:block;height:100%;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}`; const canvasCache = new WeakMap(); const NATIVE_ATTRIBUTES = [ 'alt', 'crossorigin', 'decoding', 'importance', 'loading', 'referrerpolicy', 'sizes', 'src', 'srcset', ]; class CropperImage extends CropperElement { constructor() { super(...arguments); this.$matrix = [1, 0, 0, 1, 0, 0]; this.$onLoad = null; this.$onCanvasAction = null; this.$onCanvasActionEnd = null; this.$onCanvasActionStart = null; this.$actionStartTarget = null; this.$style = style; this.$image = new Image(); this.initialCenterSize = 'contain'; this.rotatable = false; this.scalable = false; this.skewable = false; this.slottable = false; this.translatable = false; } set $canvas(element) { canvasCache.set(this, element); } get $canvas() { return canvasCache.get(this); } static get observedAttributes() { return super.observedAttributes.concat(NATIVE_ATTRIBUTES, [ 'initial-center-size', 'rotatable', 'scalable', 'skewable', 'translatable', ]); } attributeChangedCallback(name, oldValue, newValue) { if (Object.is(newValue, oldValue)) { return; } super.attributeChangedCallback(name, oldValue, newValue); // Inherits the native attributes if (NATIVE_ATTRIBUTES.includes(name)) { this.$image.setAttribute(name, newValue); } } $propertyChangedCallback(name, oldValue, newValue) { if (Object.is(newValue, oldValue)) { return; } super.$propertyChangedCallback(name, oldValue, newValue); switch (name) { case 'initialCenterSize': this.$nextTick(() => { this.$center(newValue); }); break; } } connectedCallback() { super.connectedCallback(); const { $image } = this; const $canvas = this.closest(this.$getTagNameOf(CROPPER_CANVAS)); if ($canvas) { this.$canvas = $canvas; this.$setStyles({ // Make it a block element to avoid side effects (#1074). display: 'block', position: 'absolute', }); this.$onCanvasActionStart = (event) => { var _a, _b; this.$actionStartTarget = (_b = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.relatedEvent) === null || _b === void 0 ? void 0 : _b.target; }; this.$onCanvasActionEnd = () => { this.$actionStartTarget = null; }; this.$onCanvasAction = this.$handleAction.bind(this); on($canvas, EVENT_ACTION_START, this.$onCanvasActionStart); on($canvas, EVENT_ACTION_END, this.$onCanvasActionEnd); on($canvas, EVENT_ACTION, this.$onCanvasAction); } this.$onLoad = this.$handleLoad.bind(this); on($image, EVENT_LOAD, this.$onLoad); this.$getShadowRoot().appendChild($image); } disconnectedCallback() { const { $image, $canvas } = this; if ($canvas) { if (this.$onCanvasActionStart) { off($canvas, EVENT_ACTION_START, this.$onCanvasActionStart); this.$onCanvasActionStart = null; } if (this.$onCanvasActionEnd) { off($canvas, EVENT_ACTION_END, this.$onCanvasActionEnd); this.$onCanvasActionEnd = null; } if (this.$onCanvasAction) { off($canvas, EVENT_ACTION, this.$onCanvasAction); this.$onCanvasAction = null; } } if ($image && this.$onLoad) { off($image, EVENT_LOAD, this.$onLoad); this.$onLoad = null; } this.$getShadowRoot().removeChild($image); super.disconnectedCallback(); } $handleLoad() { const { $image } = this; this.$setStyles({ width: $image.naturalWidth, height: $image.naturalHeight, }); if (this.$canvas) { this.$center(this.initialCenterSize); } } $handleAction(event) { if (this.hidden || !(this.rotatable || this.scalable || this.translatable)) { return; } const { $canvas } = this; const { detail } = event; if (detail) { const { relatedEvent } = detail; let { action } = detail; if (action === ACTION_TRANSFORM && (!this.rotatable || !this.scalable)) { if (this.rotatable) { action = ACTION_ROTATE; } else if (this.scalable) { action = ACTION_SCALE; } else { action = ACTION_NONE; } } switch (action) { case ACTION_MOVE: if (this.translatable) { let $selection = null; if (relatedEvent) { $selection = relatedEvent.target.closest(this.$getTagNameOf(CROPPER_SELECTION)); } if (!$selection) { $selection = $canvas.querySelector(this.$getTagNameOf(CROPPER_SELECTION)); } if ($selection && $selection.multiple && !$selection.active) { $selection = $canvas.querySelector(`${this.$getTagNameOf(CROPPER_SELECTION)}[active]`); } if (!$selection || $selection.hidden || !$selection.movable || $selection.dynamic || !(this.$actionStartTarget && $selection.contains(this.$actionStartTarget))) { this.$move(detail.endX - detail.startX, detail.endY - detail.startY); } } break; case ACTION_ROTATE: if (this.rotatable) { if (relatedEvent) { const { x, y } = this.getBoundingClientRect(); this.$rotate(detail.rotate, relatedEvent.clientX - x, relatedEvent.clientY - y); } else { this.$rotate(detail.rotate); } } break; case ACTION_SCALE: if (this.scalable) { if (relatedEvent) { const $selection = relatedEvent.target.closest(this.$getTagNameOf(CROPPER_SELECTION)); if (!$selection || !$selection.zoomable || ($selection.zoomable && $selection.dynamic)) { const { x, y } = this.getBoundingClientRect(); this.$zoom(detail.scale, relatedEvent.clientX - x, relatedEvent.clientY - y); } } else { this.$zoom(detail.scale); } } break; case ACTION_TRANSFORM: if (this.rotatable && this.scalable) { const { rotate } = detail; let { scale } = detail; if (scale < 0) { scale = 1 / (1 - scale); } else { scale += 1; } const cos = Math.cos(rotate); const sin = Math.sin(rotate); const [scaleX, skewY, skewX, scaleY] = [ cos * scale, sin * scale, -sin * scale, cos * scale, ]; if (relatedEvent) { const clientRect = this.getBoundingClientRect(); const x = relatedEvent.clientX - clientRect.x; const y = relatedEvent.clientY - clientRect.y; const [a, b, c, d] = this.$matrix; const originX = clientRect.width / 2; const originY = clientRect.height / 2; const moveX = x - originX; const moveY = y - originY; const translateX = ((moveX * d) - (c * moveY)) / ((a * d) - (c * b)); const translateY = ((moveY * a) - (b * moveX)) / ((a * d) - (c * b)); /** * Equals to * this.$rotate(rotate, x, y); * this.$scale(scale, x, y); */ this.$transform(scaleX, skewY, skewX, scaleY, translateX * (1 - scaleX) + translateY * skewX, translateY * (1 - scaleY) + translateX * skewY); } else { /** * Equals to * this.$rotate(rotate); * this.$scale(scale); */ this.$transform(scaleX, skewY, skewX, scaleY, 0, 0); } } break; } } } /** * Defers the callback to execute after successfully loading the image. * @param {Function} [callback] The callback to execute after successfully loading the image. * @returns {Promise} Returns a promise that resolves to the image element. */ $ready(callback) { const { $image } = this; const promise = new Promise((resolve, reject) => { const error = new Error('Failed to load the image source'); if ($image.complete) { if ($image.naturalWidth > 0 && $image.naturalHeight > 0) { resolve($image); } else { reject(error); } } else { const onLoad = () => { // eslint-disable-next-line @typescript-eslint/no-use-before-define off($image, EVENT_ERROR, onError); resolve($image); }; const onError = () => { off($image, EVENT_LOAD, onLoad); reject(error); }; once($image, EVENT_LOAD, onLoad); once($image, EVENT_ERROR, onError); } }); if (isFunction(callback)) { promise.then((image) => { callback(image); return image; }); } return promise; } /** * Aligns the image to the center of its parent element. * @param {string} [size] The size of the image. * @returns {CropperImage} Returns `this` for chaining. */ $center(size) { const { parentElement } = this; if (!parentElement) { return this; } const container = parentElement.getBoundingClientRect(); const containerWidth = container.width; const containerHeight = container.height; const { x, y, width, height, } = this.getBoundingClientRect(); const startX = x + (width / 2); const startY = y + (height / 2); const endX = container.x + (containerWidth / 2); const endY = container.y + (containerHeight / 2); this.$move(endX - startX, endY - startY); if (size && (width !== containerWidth || height !== containerHeight)) { const scaleX = containerWidth / width; const scaleY = containerHeight / height; switch (size) { case 'cover': this.$scale(Math.max(scaleX, scaleY)); break; case 'contain': this.$scale(Math.min(scaleX, scaleY)); break; } } return this; } /** * Moves the image. * @param {number} x The moving distance in the horizontal direction. * @param {number} [y] The moving distance in the vertical direction. * @returns {CropperImage} Returns `this` for chaining. */ $move(x, y = x) { if (this.translatable && isNumber(x) && isNumber(y)) { const [a, b, c, d] = this.$matrix; const e = ((x * d) - (c * y)) / ((a * d) - (c * b)); const f = ((y * a) - (b * x)) / ((a * d) - (c * b)); this.$translate(e, f); } return this; } /** * Moves the image to a specific position. * @param {number} x The new position in the horizontal direction. * @param {number} [y] The new position in the vertical direction. * @returns {CropperImage} Returns `this` for chaining. */ $moveTo(x, y = x) { if (this.translatable && isNumber(x) && isNumber(y)) { const [a, b, c, d] = this.$matrix; const e = ((x * d) - (c * y)) / ((a * d) - (c * b)); const f = ((y * a) - (b * x)) / ((a * d) - (c * b)); this.$setTransform(a, b, c, d, e, f); } return this; } /** * Rotates the image. * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/rotate} * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/rotate} * @param {number|string} angle The rotation angle (in radians). * @param {number} [x] The rotation origin in the horizontal, defaults to the center of the image. * @param {number} [y] The rotation origin in the vertical, defaults to the center of the image. * @returns {CropperImage} Returns `this` for chaining. */ $rotate(angle, x, y) { if (this.rotatable) { const radian = toAngleInRadian(angle); const cos = Math.cos(radian); const sin = Math.sin(radian); const [scaleX, skewY, skewX, scaleY] = [cos, sin, -sin, cos]; if (isNumber(x) && isNumber(y)) { const [a, b, c, d] = this.$matrix; const { width, height } = this.getBoundingClientRect(); const originX = width / 2; const originY = height / 2; const moveX = x - originX; const moveY = y - originY; const translateX = ((moveX * d) - (c * moveY)) / ((a * d) - (c * b)); const translateY = ((moveY * a) - (b * moveX)) / ((a * d) - (c * b)); /** * Equals to * this.$translate(translateX, translateX); * this.$rotate(angle); * this.$translate(-translateX, -translateX); */ this.$transform(scaleX, skewY, skewX, scaleY, translateX * (1 - scaleX) - translateY * skewX, translateY * (1 - scaleY) - translateX * skewY); } else { this.$transform(scaleX, skewY, skewX, scaleY, 0, 0); } } return this; } /** * Zooms the image. * @param {number} scale The zoom factor. Positive numbers for zooming in, and negative numbers for zooming out. * @param {number} [x] The zoom origin in the horizontal, defaults to the center of the image. * @param {number} [y] The zoom origin in the vertical, defaults to the center of the image. * @returns {CropperImage} Returns `this` for chaining. */ $zoom(scale, x, y) { if (!this.scalable || scale === 0) { return this; } if (scale < 0) { scale = 1 / (1 - scale); } else { scale += 1; } if (isNumber(x) && isNumber(y)) { const [a, b, c, d] = this.$matrix; const { width, height } = this.getBoundingClientRect(); const originX = width / 2; const originY = height / 2; const moveX = x - originX; const moveY = y - originY; const translateX = ((moveX * d) - (c * moveY)) / ((a * d) - (c * b)); const translateY = ((moveY * a) - (b * moveX)) / ((a * d) - (c * b)); /** * Equals to * this.$translate(translateX, translateX); * this.$scale(scale); * this.$translate(-translateX, -translateX); */ this.$transform(scale, 0, 0, scale, translateX * (1 - scale), translateY * (1 - scale)); } else { this.$scale(scale); } return this; } /** * Scales the image. * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/scale} * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/scale} * @param {number} x The scaling factor in the horizontal direction. * @param {number} [y] The scaling factor in the vertical direction. * @returns {CropperImage} Returns `this` for chaining. */ $scale(x, y = x) { if (this.scalable) { this.$transform(x, 0, 0, y, 0, 0); } return this; } /** * Skews the image. * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/skew} * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/transform} * @param {number|string} x The skewing angle in the horizontal direction. * @param {number|string} [y] The skewing angle in the vertical direction. * @returns {CropperImage} Returns `this` for chaining. */ $skew(x, y = 0) { if (this.skewable) { const radianX = toAngleInRadian(x); const radianY = toAngleInRadian(y); this.$transform(1, Math.tan(radianY), Math.tan(radianX), 1, 0, 0); } return this; } /** * Translates the image. * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/translate} * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/translate} * @param {number} x The translating distance in the horizontal direction. * @param {number} [y] The translating distance in the vertical direction. * @returns {CropperImage} Returns `this` for chaining. */ $translate(x, y = x) { if (this.translatable && isNumber(x) && isNumber(y)) { this.$transform(1, 0, 0, 1, x, y); } return this; } /** * Transforms the image. * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix} * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/transform} * @param {number} a The scaling factor in the horizontal direction. * @param {number} b The skewing angle in the vertical direction. * @param {number} c The skewing angle in the horizontal direction. * @param {number} d The scaling factor in the vertical direction. * @param {number} e The translating distance in the horizontal direction. * @param {number} f The translating distance in the vertical direction. * @returns {CropperImage} Returns `this` for chaining. */ $transform(a, b, c, d, e, f) { if (isNumber(a) && isNumber(b) && isNumber(c) && isNumber(d) && isNumber(e) && isNumber(f)) { return this.$setTransform(multiplyMatrices(this.$matrix, [a, b, c, d, e, f])); } return this; } /** * Resets (overrides) the current transform to the specific identity matrix. * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setTransform} * @param {number|Array} a The scaling factor in the horizontal direction. * @param {number} b The skewing angle in the vertical direction. * @param {number} c The skewing angle in the horizontal direction. * @param {number} d The scaling factor in the vertical direction. * @param {number} e The translating distance in the horizontal direction. * @param {number} f The translating distance in the vertical direction. * @returns {CropperImage} Returns `this` for chaining. */ $setTransform(a, b, c, d, e, f) { if (this.rotatable || this.scalable || this.skewable || this.translatable) { if (Array.isArray(a)) { [a, b, c, d, e, f] = a; } if (isNumber(a) && isNumber(b) && isNumber(c) && isNumber(d) && isNumber(e) && isNumber(f)) { const oldMatrix = [...this.$matrix]; const newMatrix = [a, b, c, d, e, f]; if (this.$emit(EVENT_TRANSFORM, { matrix: newMatrix, oldMatrix, }) === false) { return this; } this.$matrix = newMatrix; this.style.transform = `matrix(${newMatrix.join(', ')})`; } } return this; } /** * Retrieves the current transformation matrix being applied to the element. * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getTransform} * @returns {Array} Returns the readonly transformation matrix. */ $getTransform() { return this.$matrix.slice(); } /** * Resets the current transform to the initial identity matrix. * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/resetTransform} * @returns {CropperImage} Returns `this` for chaining. */ $resetTransform() { return this.$setTransform([1, 0, 0, 1, 0, 0]); } } CropperImage.$name = CROPPER_IMAGE; CropperImage.$version = '2.0.0'; export { CropperImage as default };