@nutui/nutui-react
Version:
京东风格的轻量级移动端 React 组件库,支持一套代码生成 H5 和小程序
306 lines (305 loc) • 13.1 kB
JavaScript
import { _ as __rest, a as __awaiter } from "./tslib.es6.js";
import React__default, { useRef, useState, useMemo, useCallback, useEffect } from "react";
import classNames from "classnames";
import Button__default from "./Button.js";
import { C as ComponentDefaults } from "./typings.js";
import { u as useTouch } from "./use-touch.js";
import { p as preventDefault, c as clamp } from "./index.js";
import { g as getRect } from "./use-client-rect.js";
import { u as useConfig } from "./configprovider.taro.js";
const defaultProps = Object.assign(Object.assign({}, ComponentDefaults), { maxZoom: 3, space: 10, toolbar: [
React__default.createElement(Button__default, { type: "danger", key: "cancel" }, "Cancel"),
React__default.createElement(Button__default, { key: "reset" }, "Reset"),
React__default.createElement(Button__default, { key: "rotate" }, "Rotate"),
React__default.createElement(Button__default, { type: "success", key: "confirm" }, "Confirm")
], toolbarPosition: "bottom", editText: "Edit", shape: "square" });
const classPrefix = `nut-avatar-cropper`;
const AvatarCropper = (props) => {
const { locale } = useConfig();
defaultProps.toolbar = [
React__default.createElement(Button__default, { type: "danger", key: "cancel" }, locale.cancel),
React__default.createElement(Button__default, { key: "reset" }, locale.reset),
React__default.createElement(Button__default, { key: "rotate" }, locale.avatarCropper.rotate),
React__default.createElement(Button__default, { type: "success", key: "confirm" }, locale.confirm)
];
const _a = Object.assign(Object.assign({}, defaultProps), props), { children, maxZoom, space, toolbar, toolbarPosition, editText, shape, className, style, onConfirm, onCancel } = _a, rest = __rest(_a, ["children", "maxZoom", "space", "toolbar", "toolbarPosition", "editText", "shape", "className", "style", "onConfirm", "onCancel"]);
const cls = classNames(classPrefix, className, shape === "round" && "round");
const toolbarPositionCls = classNames(`${classPrefix}-popup-toolbar`, toolbarPosition);
const inputImageRef = useRef(null);
const cropperPopupRef = useRef(null);
const canvasRef = useRef(null);
const [visible, setVisible] = useState(false);
const [moving, setMoving] = useState(false);
const [zooming, setZooming] = useState(false);
const [state, setState] = useState({
defScale: 1,
scale: 1,
angle: 0,
moveX: 0,
moveY: 0,
displayWidth: 0,
displayHeight: 0
});
const defDrawImage = {
img: new Image(),
// 规定要使用的图像
sx: 0,
// 开始剪切的 x 坐标位置
sy: 0,
// 开始剪切的 y 坐标位置
swidth: 0,
// 被剪切区域的宽度
sheight: 0,
// 被剪切区域的高度
x: 0,
// 在画布上x的坐标位置
y: 0,
// 在画布上y的坐标位置
width: 0,
// 要使用的图像的宽度
height: 0
// 要使用的图像的高度
};
const [drawImage, setDrawImg] = useState(Object.assign({}, defDrawImage));
const devicePixelRatio = window.devicePixelRatio || 1;
const touch = useTouch();
const highlightStyle = useMemo(() => {
const width = `${drawImage.swidth / devicePixelRatio}px`;
const height = width;
return {
width,
height,
borderRadius: shape === "round" ? "50%" : ""
};
}, [devicePixelRatio, drawImage.swidth]);
const isAngle = useMemo(() => {
return state.angle === 90 || state.angle === 270;
}, [state.angle]);
const maxMoveX = useMemo(() => {
const { swidth, height } = drawImage;
if (isAngle) {
return Math.max(0, (height * state.scale - swidth) / 2);
}
return Math.max(0, (state.displayWidth * state.scale - swidth) / 2);
}, [state.scale, state.displayWidth, drawImage, isAngle]);
const maxMoveY = useMemo(() => {
const { swidth, height } = drawImage;
if (isAngle) {
return Math.max(0, (state.displayWidth * state.scale - swidth) / 2);
}
return Math.max(0, (height * state.scale - swidth) / 2);
}, [state.scale, state.displayWidth, drawImage, isAngle]);
const fileToDataURL = (file) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = (e) => resolve(e.target.result);
reader.readAsDataURL(file);
});
};
const dataURLToImage = (dataURL) => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.src = dataURL;
});
};
const draw = useCallback(() => {
const { img, width, height, x, y, swidth } = drawImage;
const { moveX, moveY, scale, angle, displayWidth, displayHeight } = state;
const canvas = canvasRef.current;
if (!canvas)
return;
const ctx = canvas.getContext("2d");
canvas.width = displayWidth;
canvas.height = displayHeight;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#666";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#000";
ctx.fillRect(space * devicePixelRatio, (canvas.height - swidth) / 2, swidth, swidth);
ctx.translate(canvas.width / 2 + moveX, canvas.height / 2 + moveY);
ctx.rotate(Math.PI / 180 * angle);
ctx.scale(scale, scale);
ctx.drawImage(img, x, y, width, height);
}, [drawImage, state, devicePixelRatio, space]);
useEffect(() => {
if (Math.abs(state.moveX) > maxMoveX) {
setState(Object.assign(Object.assign({}, state), { moveX: maxMoveX }));
}
if (Math.abs(state.moveY) > maxMoveY) {
setState(Object.assign(Object.assign({}, state), { moveY: maxMoveY }));
}
draw();
}, [state, maxMoveX, maxMoveY, draw]);
const setDrawImgs = (image) => {
const rect = getRect(cropperPopupRef.current);
if (!rect)
return;
const { width: clientWidth, height: clientHeight } = rect;
const canvasWidth = state.displayWidth = clientWidth * devicePixelRatio;
const canvasHeight = state.displayHeight = clientHeight * devicePixelRatio;
const copyDrawImg = Object.assign({}, defDrawImage);
const { width: imgWidth, height: imgHeight } = image;
copyDrawImg.img = image;
const isPortrait = imgHeight > imgWidth;
const rate = isPortrait ? imgWidth / imgHeight : imgHeight / imgWidth;
copyDrawImg.width = canvasWidth;
copyDrawImg.height = isPortrait ? canvasWidth / rate : canvasWidth * rate;
copyDrawImg.x = -copyDrawImg.width / 2;
copyDrawImg.y = -copyDrawImg.height / 2;
copyDrawImg.swidth = canvasWidth - space * 2 * devicePixelRatio;
copyDrawImg.sheight = isPortrait ? copyDrawImg.swidth / rate : copyDrawImg.swidth * rate;
copyDrawImg.sx = space * devicePixelRatio;
copyDrawImg.sy = (canvasHeight - copyDrawImg.swidth) / 2;
setDrawImg(copyDrawImg);
const scale = copyDrawImg.swidth / (isPortrait ? copyDrawImg.width : copyDrawImg.height);
setState(Object.assign(Object.assign({}, state), { defScale: scale }));
resetScale(scale);
};
const inputImageChange = (event) => __awaiter(void 0, void 0, void 0, function* () {
setVisible(true);
const $el = event.target;
const { files } = $el;
if (!(files === null || files === void 0 ? void 0 : files.length))
return;
const base64 = yield fileToDataURL(files[0]);
const image = yield dataURLToImage(base64);
setDrawImgs(image);
});
const resetScale = (scale) => {
setState(Object.assign(Object.assign({}, state), { moveX: 0, moveY: 0, angle: 0, scale: scale || state.defScale, defScale: scale || state.defScale }));
};
const setScale = (scale) => {
scale = clamp(scale, 0.3, +maxZoom + 1);
if (scale !== state.scale) {
setState(Object.assign(Object.assign({}, state), { scale }));
}
};
const getDistance = (touches) => Math.sqrt(Math.pow(touches[0].clientX - touches[1].clientX, 2) + Math.pow(touches[0].clientY - touches[1].clientY, 2));
const [startMove, setStartMove] = useState({
startMoveX: 0,
startMoveY: 0,
startScale: 0,
startDistance: 0
});
const { startMoveX, startMoveY, startScale, startDistance } = startMove;
const onTouchStart = (event) => {
const { touches } = event;
const { offsetX } = touch;
touch.start(event);
const fingerNum = touches === null || touches === void 0 ? void 0 : touches.length;
setStartMove(Object.assign(Object.assign({}, startMove), { startMoveX: state.moveX, startMoveY: state.moveY }));
setMoving(fingerNum === 1);
setZooming(fingerNum === 2 && !offsetX.current);
if (fingerNum === 2 && !offsetX.current) {
setStartMove(Object.assign(Object.assign({}, startMove), { startScale: state.scale, startDistance: getDistance(event.touches) }));
}
};
const onTouchMove = (event) => {
const { touches } = event;
touch.move(event);
if (moving || zooming) {
preventDefault(event, true);
}
if (moving) {
const { deltaX, deltaY } = touch;
const moveX = deltaX.current * state.scale + startMoveX;
const moveY = deltaY.current * state.scale + startMoveY;
setState(Object.assign(Object.assign({}, state), { moveX: clamp(moveX, -maxMoveX, maxMoveX), moveY: clamp(moveY, -maxMoveY, maxMoveY) }));
}
if (zooming && touches.length === 2) {
const distance = getDistance(touches);
const scale = startScale * distance / startDistance;
setScale(scale);
}
};
const onTouchEnd = (event) => {
let stopPropagation = false;
if (moving || zooming) {
stopPropagation = !(moving && startMoveX === state.moveX && startMoveY === state.moveY);
if (!event.touches.length) {
if (zooming) {
setState(Object.assign(Object.assign({}, state), { moveX: clamp(state.moveX, -maxMoveX, maxMoveX), moveY: clamp(state.moveY, -maxMoveY, maxMoveY) }));
setZooming(false);
}
setMoving(false);
setStartMove(Object.assign(Object.assign({}, startMove), { startMoveX: 0, startMoveY: 0, startScale: state.defScale }));
if (state.scale < state.defScale) {
resetScale();
}
if (state.scale > maxZoom) {
setState(Object.assign(Object.assign({}, state), { scale: +maxZoom }));
}
}
}
preventDefault(event, stopPropagation);
touch.reset();
};
const reset = () => {
setState(Object.assign(Object.assign({}, state), { angle: 0 }));
};
const rotate = () => {
if (state.angle === 270) {
setState(Object.assign(Object.assign({}, state), { angle: 0 }));
return;
}
setState(Object.assign(Object.assign({}, state), { angle: state.angle + 90 }));
};
const cancel = (isEmit = true) => {
setVisible(false);
resetScale();
inputImageRef.current && (inputImageRef.current.value = "");
isEmit && onCancel && onCancel();
};
const confirm = () => {
const canvas = canvasRef.current;
const { sx, sy, swidth } = drawImage;
const width = swidth;
const height = swidth;
const croppedCanvas = document.createElement("canvas");
const croppedCtx = croppedCanvas.getContext("2d");
croppedCanvas.width = width;
croppedCanvas.height = height;
canvas && croppedCtx.drawImage(canvas, sx, sy, width, height, 0, 0, width, height);
const imageDataURL = croppedCanvas.toDataURL("image/png");
onConfirm && onConfirm(imageDataURL);
cancel(false);
};
const ToolBar = () => {
const actions = [cancel, reset, rotate, confirm];
return React__default.createElement("div", { className: `${classPrefix}-popup-toolbar-flex` }, actions.map((action, index) => React__default.createElement("div", { key: index, className: `${classPrefix}-popup-toolbar-item`, onClick: (_e) => action() }, toolbar[index])));
};
const CropperPopup = () => {
return React__default.createElement(
"div",
{ ref: cropperPopupRef, className: `${classPrefix}-popup`, style: { display: visible ? "block" : "none" } },
React__default.createElement("canvas", { ref: canvasRef, className: `${classPrefix}-popup-canvas` }),
React__default.createElement(
"div",
{ className: `${classPrefix}-popup-highlight`, onTouchStart, onTouchMove, onTouchEnd },
React__default.createElement("div", { className: "highlight", style: highlightStyle })
),
React__default.createElement(
"div",
{ className: toolbarPositionCls },
React__default.createElement(ToolBar, null)
)
);
};
return React__default.createElement(
React__default.Fragment,
null,
React__default.createElement(
"div",
Object.assign({ className: cls }, rest, { style }),
children,
React__default.createElement("input", { ref: inputImageRef, type: "file", accept: "image/*", className: `${classPrefix}-input`, onChange: (e) => inputImageChange(e), "aria-label": locale.avatarCropper.selectImage }),
React__default.createElement("div", { className: "nut-avatar-cropper-edit-text" }, editText)
),
CropperPopup()
);
};
AvatarCropper.displayName = "NutAvatarCropper";
export {
AvatarCropper as default
};