UNPKG

@nutui/nutui-react-taro

Version:

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

429 lines (428 loc) 17.5 kB
import { _ as __rest, a as __awaiter } from "./tslib.es6-iWu3F_1J.js"; import React__default, { useState, useMemo, useEffect, useCallback } from "react"; import Taro, { useReady, createSelectorQuery } from "@tarojs/taro"; import classNames from "classnames"; import { Canvas } from "@tarojs/components"; import { g as getWindowInfo } from "./get-system-info-D27xNglo.js"; import { B as Button } from "./button.taro-CixO32Hw.js"; import { u as useConfig } from "./configprovider.taro-DpK4IiCE.js"; import { C as ComponentDefaults } from "./typings-DV9RBfhj.js"; import { u as useTouch } from "./use-touch-BeiLHDO9.js"; import { p as preventDefault, c as clamp } from "./index-DeNfBlua.js"; const defaultProps = Object.assign(Object.assign({}, ComponentDefaults), { maxZoom: 3, space: 10, toolbar: [ React__default.createElement(Button, { type: "danger", key: "cancel" }, "Cancel"), React__default.createElement(Button, { key: "reset" }, "Reset"), React__default.createElement(Button, { key: "rotate" }, "Rotate"), React__default.createElement(Button, { type: "success", key: "confirm" }, "Confirm") ], toolbarPosition: "bottom", editText: "Edit", sizeType: ["original", "compressed"], sourceType: ["album", "camera"], shape: "square" }); const classPrefix = `nut-avatar-cropper`; const AvatarCropper = (props) => { const { locale } = useConfig(); defaultProps.toolbar = [ React__default.createElement(Button, { type: "danger", key: "cancel" }, locale.cancel), React__default.createElement(Button, { key: "reset" }, locale.reset), React__default.createElement(Button, { key: "rotate" }, locale.avatarCropper.rotate), React__default.createElement(Button, { type: "success", key: "confirm" }, locale.confirm) ]; defaultProps.editText = locale.edit; const _a = Object.assign(Object.assign({}, defaultProps), props), { children, toolbar, maxZoom, space, toolbarPosition, editText, sizeType, sourceType, shape, className, style, onConfirm, onCancel } = _a, rest = __rest(_a, ["children", "toolbar", "maxZoom", "space", "toolbarPosition", "editText", "sizeType", "sourceType", "shape", "className", "style", "onConfirm", "onCancel"]); const cls = classNames(classPrefix, "taro", className, shape === "round" && "round"); const toolbarPositionCls = classNames(`${classPrefix}-popup-toolbar`, toolbarPosition); const [visible, setVisible] = useState(false); const [moving, setMoving] = useState(false); const [zooming, setZooming] = useState(false); const systemInfo = getWindowInfo(); const showAlipayCanvas2D = useMemo(() => { return Taro.getEnv() === "ALIPAY" && parseInt(Taro.SDKVersion.replace(/\./g, "")) >= 270; }, []); const showPixelRatio = Taro.getEnv() === "WEB" || showAlipayCanvas2D; const pixelRatio = showPixelRatio ? systemInfo.pixelRatio : 1; const [state, setState] = useState({ defScale: 1, scale: 1, angle: 0, moveX: 0, moveY: 0, displayWidth: systemInfo.windowWidth * pixelRatio, displayHeight: systemInfo.windowHeight * pixelRatio, cropperWidth: systemInfo.windowWidth * pixelRatio - space * pixelRatio * 2, cropperHeight: systemInfo.windowWidth * pixelRatio - space * pixelRatio * 2 }); const defDrawImage = { src: "", // 规定要使用的图像 x: 0, // 在画布上x的坐标位置 y: 0, // 在画布上y的坐标位置 width: 0, // 要使用的图像的宽度 height: 0 // 要使用的图像的高度 }; const [drawImage, setDrawImg] = useState(Object.assign({}, defDrawImage)); const [canvasAll, setCanvasAll] = useState({ canvasId: `canvas-${Date.now()}`, cropperCanvas: null, cropperCanvasContext: null }); useReady(() => { if (showAlipayCanvas2D) { const { canvasId } = canvasAll; createSelectorQuery().select(`#${canvasId}`).node(({ node: canvas }) => { canvas.width = state.displayWidth; canvas.height = state.displayHeight; setCanvasAll(Object.assign(Object.assign({}, canvasAll), { cropperCanvas: canvas })); }).exec(); } }); useEffect(() => { setCanvasAll(Object.assign(Object.assign({}, canvasAll), { cropperCanvasContext: Taro.createCanvasContext(canvasAll.canvasId) })); }, []); const touch = useTouch(); const highlightStyle = useMemo(() => { const width = `${state.cropperWidth / pixelRatio}px`; const height = width; return { width, height, borderRadius: shape === "round" ? "50%" : "" }; }, [pixelRatio, state.cropperWidth]); const isAngle = useMemo(() => { return state.angle === 90 || state.angle === 270; }, [state.angle]); const canvasStyle = useMemo(() => { return { width: `${state.displayWidth / pixelRatio}px`, height: `${state.displayHeight / pixelRatio}px` }; }, [pixelRatio, state.displayHeight, state.displayWidth]); const maxMoveX = useMemo(() => { const { displayWidth, scale, cropperWidth } = state; if (isAngle) { return Math.max(0, (drawImage.height * scale - cropperWidth) / 2); } return Math.max(0, (displayWidth * scale - cropperWidth) / 2); }, [drawImage.height, isAngle, state]); const maxMoveY = useMemo(() => { const { displayWidth, scale, cropperWidth } = state; if (isAngle) { return Math.max(0, (displayWidth * scale - cropperWidth) / 2); } return Math.max(0, (drawImage.height * scale - cropperWidth) / 2); }, [drawImage.height, isAngle, state]); const dataURLToImage = (dataURL) => { return new Promise((resolve) => { const img = new Image(); img.onload = () => resolve(img); img.src = dataURL; }); }; const dataURLToCanvasImage = (canvas, dataURL) => { return new Promise((resolve) => { const img = canvas.createImage(); img.onload = () => resolve(img); img.src = dataURL; }); }; const canvas2dDraw = useCallback((ctx) => { const { src, width, height, x, y } = drawImage; if (!ctx || !src) return; const { moveX, moveY, scale, angle, displayWidth, displayHeight, cropperWidth } = state; ctx.clearRect(0, 0, displayWidth, displayHeight); ctx.fillStyle = "#666"; ctx.fillRect(0, 0, displayWidth, displayHeight); ctx.fillStyle = "#000"; ctx.fillRect(space * pixelRatio, (displayHeight - cropperWidth) / 2, cropperWidth, cropperWidth); ctx.translate(displayWidth / 2 + moveX, displayHeight / 2 + moveY); ctx.rotate(Math.PI / 180 * angle); ctx.scale(scale, scale); ctx.drawImage(src, x, y, width, height); }, [drawImage, state]); const webDraw = useCallback(() => { const canvasDom = document.getElementById(canvasAll.canvasId); let canvas = canvasDom; if ((canvasDom === null || canvasDom === void 0 ? void 0 : canvasDom.tagName) !== "CANVAS") { canvas = canvasDom === null || canvasDom === void 0 ? void 0 : canvasDom.getElementsByTagName("canvas")[0]; } if (!canvas) return; canvas.width = state.displayWidth; canvas.height = state.displayHeight; const ctx = canvas.getContext("2d"); canvas2dDraw(ctx); }, [canvas2dDraw]); const alipayDraw = useCallback(() => { const ctx = canvasAll.cropperCanvas.getContext("2d"); ctx && ctx.resetTransform(); canvas2dDraw(ctx); }, [canvas2dDraw, canvasAll.cropperCanvas]); const draw = useCallback(() => { if (Taro.getEnv() === "WEB") { webDraw(); return; } if (showAlipayCanvas2D) { alipayDraw(); return; } const { src, width, height, x, y } = drawImage; const { moveX, moveY, scale, angle, displayWidth, displayHeight, cropperWidth } = state; const { cropperCanvasContext } = canvasAll; const ctx = cropperCanvasContext; if (!ctx) return; ctx.clearRect(0, 0, displayWidth, displayHeight); ctx.fillStyle = "#666"; ctx.setFillStyle("#666"); ctx.fillRect(0, 0, displayWidth, displayHeight); ctx.stroke(); ctx.fill(); ctx.fillStyle = "#000"; ctx.setFillStyle("#000"); ctx.fillRect(space, (displayHeight - cropperWidth) / 2, cropperWidth, cropperWidth); ctx.stroke(); ctx.fill(); ctx.translate(displayWidth / 2 + moveX, displayHeight / 2 + moveY); ctx.rotate(Math.PI / 180 * angle); ctx.scale(scale, scale); ctx.drawImage(src, x, y, width, height); ctx.draw(); }, [drawImage, state.scale, state.angle, state.moveX, state.moveY]); 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 setDrawImgInfo = (image) => __awaiter(void 0, void 0, void 0, function* () { const { displayWidth, cropperWidth } = state; const copyDrawImg = Object.assign({}, defDrawImage); const { width: imgWidth, height: imgHeight } = image; copyDrawImg.src = image.path; if (Taro.getEnv() === "WEB") { copyDrawImg.src = yield dataURLToImage(image.path); } if (showAlipayCanvas2D) { copyDrawImg.src = yield dataURLToCanvasImage(canvasAll.cropperCanvas, image.path); } const isPortrait = imgHeight > imgWidth; const rate = isPortrait ? imgWidth / imgHeight : imgHeight / imgWidth; copyDrawImg.width = displayWidth; copyDrawImg.height = isPortrait ? displayWidth / rate : displayWidth * rate; copyDrawImg.x = -copyDrawImg.width / 2; copyDrawImg.y = -copyDrawImg.height / 2; setDrawImg(copyDrawImg); const scale = cropperWidth / (isPortrait ? copyDrawImg.width : copyDrawImg.height); setState(Object.assign(Object.assign({}, state), { defScale: scale })); resetScale(scale); }); const chooseImage = () => { Taro.chooseImage({ count: 1, // 可以指定是原图还是压缩图,默认二者都有 sizeType, sourceType, success: (res) => { const { tempFiles } = res; !!tempFiles.length && imageChange(tempFiles[0]); } }); }; const imageChange = (file) => __awaiter(void 0, void 0, void 0, function* () { Taro.getImageInfo({ src: file.path }).then((res) => { setVisible(true); setDrawImgInfo(res); }); }); 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(); isEmit && onCancel && onCancel(); }; const confirmWEB = () => { const { cropperWidth, displayHeight } = state; const canvasDom = document.getElementById(canvasAll.canvasId); let canvas = canvasDom; if ((canvasDom === null || canvasDom === void 0 ? void 0 : canvasDom.tagName) !== "CANVAS") { canvas = canvasDom === null || canvasDom === void 0 ? void 0 : canvasDom.getElementsByTagName("canvas")[0]; } const width = cropperWidth; const height = cropperWidth; const croppedCanvas = document.createElement("canvas"); const croppedCtx = croppedCanvas.getContext("2d"); croppedCanvas.width = width; croppedCanvas.height = height; canvas && croppedCtx.drawImage(canvas, space * pixelRatio, (displayHeight - cropperWidth) / 2, width, height, 0, 0, width, height); const imageDataURL = croppedCanvas.toDataURL("image/png"); onConfirm && onConfirm(imageDataURL); cancel(false); }; const confirmALIPAY = () => { const { cropperWidth, displayHeight } = state; const { cropperCanvas } = canvasAll; Taro.canvasToTempFilePath({ canvas: cropperCanvas, x: props.space, y: (displayHeight - cropperWidth) / 2, width: cropperWidth, height: cropperWidth, destWidth: cropperWidth, destHeight: cropperWidth, success: (res) => __awaiter(void 0, void 0, void 0, function* () { const filePath = res.tempFilePath; onConfirm && onConfirm(filePath); cancel(false); }) }); }; const confirm = () => { if (Taro.getEnv() === "WEB") { confirmWEB(); return; } if (showAlipayCanvas2D) { confirmALIPAY(); return; } const { cropperWidth, displayHeight } = state; const { canvasId } = canvasAll; Taro.canvasToTempFilePath({ canvasId, x: props.space, y: (displayHeight - cropperWidth) / 2, width: cropperWidth, height: cropperWidth, destWidth: cropperWidth * systemInfo.pixelRatio, destHeight: cropperWidth * systemInfo.pixelRatio, success: (res) => __awaiter(void 0, void 0, void 0, function* () { const filePath = res.tempFilePath; onConfirm && onConfirm(filePath); 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 = () => { const { canvasId } = canvasAll; return React__default.createElement( "div", { className: `${classPrefix}-popup`, style: { display: visible ? "block" : "none" } }, React__default.createElement(Canvas, { id: canvasId, "canvas-id": canvasId, type: showAlipayCanvas2D ? "2d" : void 0, style: canvasStyle, 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("div", { className: "nut-avatar-cropper-edit-text", onClick: chooseImage }, editText) ), CropperPopup() ); }; AvatarCropper.displayName = "NutAvatarCropper"; export { AvatarCropper as A };