UNPKG

@nutui/nutui-react

Version:

京东风格的轻量级移动端 React 组件库,支持一套代码生成 H5 和小程序

306 lines (305 loc) 13.1 kB
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 };