@cropper/element-canvas
Version:
A custom canvas element for the Cropper.
459 lines (452 loc) • 18.6 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@cropper/element'), require('@cropper/utils')) :
typeof define === 'function' && define.amd ? define(['@cropper/element', '@cropper/utils'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.CropperCanvas = factory(global.CropperElement, global.CropperUtils));
})(this, (function (CropperElement, utils) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var CropperElement__default = /*#__PURE__*/_interopDefaultLegacy(CropperElement);
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__default["default"] {
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 = utils.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);
utils.on(this, utils.EVENT_POINTER_DOWN, this.$onPointerDown);
}
if (!this.$onPointerMove) {
this.$onPointerMove = this.$handlePointerMove.bind(this);
utils.on(this.ownerDocument, utils.EVENT_POINTER_MOVE, this.$onPointerMove);
}
if (!this.$onPointerUp) {
this.$onPointerUp = this.$handlePointerUp.bind(this);
utils.on(this.ownerDocument, utils.EVENT_POINTER_UP, this.$onPointerUp);
}
if (!this.$onWheel) {
this.$onWheel = this.$handleWheel.bind(this);
utils.on(this, utils.EVENT_WHEEL, this.$onWheel, {
passive: false,
capture: true,
});
}
}
$unbind() {
if (this.$onPointerDown) {
utils.off(this, utils.EVENT_POINTER_DOWN, this.$onPointerDown);
this.$onPointerDown = null;
}
if (this.$onPointerMove) {
utils.off(this.ownerDocument, utils.EVENT_POINTER_MOVE, this.$onPointerMove);
this.$onPointerMove = null;
}
if (this.$onPointerUp) {
utils.off(this.ownerDocument, utils.EVENT_POINTER_UP, this.$onPointerUp);
this.$onPointerUp = null;
}
if (this.$onWheel) {
utils.off(this, utils.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)
(utils.isNumber(buttons) && buttons !== 1) || (utils.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 = utils.ACTION_TRANSFORM;
}
else if (utils.isElement(event.target)) {
action = event.target.action || event.target.getAttribute(utils.ATTRIBUTE_ACTION) || '';
}
if (this.$emit(utils.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 === utils.ACTION_NONE || $pointers.size === 0) {
return;
}
if (this.$emit(utils.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 === utils.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 = utils.ACTION_ROTATE;
detail.rotate = rotate;
detail.centerX = centerX;
detail.centerY = centerY;
}
else if (scalable) {
detail.action = utils.ACTION_SCALE;
detail.scale = scale;
detail.centerX = centerX;
detail.centerY = centerY;
}
else {
detail.action = utils.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 !== utils.ACTION_NONE) {
this.$emit(utils.EVENT_ACTION, detail, {
cancelable: false,
});
}
}
$handlePointerUp(event) {
const { $action, $pointers } = this;
if (this.disabled || $action === utils.ACTION_NONE) {
return;
}
if (this.$emit(utils.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 = utils.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(utils.EVENT_ACTION, {
action: utils.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 (utils.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 (utils.isPlainObject(options)
&& (utils.isPositiveNumber(options.width) || utils.isPositiveNumber(options.height))) {
({ width, height } = utils.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(utils.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 (utils.isPlainObject(options) && utils.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 = utils.CROPPER_CANVAS;
CropperCanvas.$version = '2.0.0';
return CropperCanvas;
}));