UNPKG

react-svg-image-board

Version:

A react component. can drawing svg element to image board.

703 lines (612 loc) 23.6 kB
import _pt from "prop-types"; function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } import * as React from 'react'; // import 'style/index.scss'; import './styles/index.scss'; // import { BoardMode, Shape, Rectangle, Size, Point } from '..'; export let BoardMode; (function (BoardMode) { BoardMode[BoardMode["ReadOnly"] = 0] = "ReadOnly"; BoardMode[BoardMode["Edit"] = 1] = "Edit"; })(BoardMode || (BoardMode = {})); export let ShapeTypeEnum; (function (ShapeTypeEnum) { ShapeTypeEnum[ShapeTypeEnum["Rectangle"] = 0] = "Rectangle"; ShapeTypeEnum[ShapeTypeEnum["Line"] = 1] = "Line"; ShapeTypeEnum[ShapeTypeEnum["Circle"] = 2] = "Circle"; ShapeTypeEnum[ShapeTypeEnum["Polyline"] = 3] = "Polyline"; ShapeTypeEnum[ShapeTypeEnum["polygon"] = 4] = "polygon"; })(ShapeTypeEnum || (ShapeTypeEnum = {})); ; export class SvgImageBoard extends React.Component { constructor(props) { super(props); _defineProperty(this, "_isMouseDown", false); _defineProperty(this, "_mouseDownPostion", { x: 0, y: 0 }); _defineProperty(this, "_lastMousePosition", { x: 0, y: 0 }); _defineProperty(this, "resize", e => { if (this.mainBoxRef.current) { const { imageSize } = this.state; this.setImageDefaultRatio(imageSize); } }); _defineProperty(this, "preLoadImage", imageUrl => { if (imageUrl) { const img = new Image(); img.onload = e => { const imageSize = { width: img.naturalWidth, height: img.naturalHeight }; this.setState({ imageUrl, imageSize }); this.setImageDefaultRatio(imageSize); }; img.src = imageUrl; } }); _defineProperty(this, "setImageDefaultRatio", imageSize => { if (!this.props.imageUrl || !this.mainBoxRef.current) return; const boxSize = { width: this.mainBoxRef.current?.clientWidth, height: this.mainBoxRef.current?.clientHeight }; // 设置初始图像比例 let scaleRatio = 1; let scalePoint = { x: 0, y: 0 }; if (boxSize.width / imageSize.width < boxSize.height / imageSize.height) { scaleRatio = boxSize.width / imageSize.width; scalePoint.y = (boxSize.height - imageSize.height * scaleRatio) / 2; } else { scaleRatio = boxSize.height / imageSize.height; scalePoint.x = (boxSize.width - imageSize.width * scaleRatio) / 2; } this.mapscaleControlRef.current?.setAttribute('transform', `matrix(${scaleRatio},0,0,${scaleRatio},${scalePoint.x},${scalePoint.y})`); }); _defineProperty(this, "onkeydown", e => { if (e.keyCode === 46) { // Delete rect e.stopImmediatePropagation(); const { mode, onDeletedShape } = this.props; const { currentEditIndex, shapes } = this.state; if (mode === BoardMode.Edit && currentEditIndex >= 0) { const removes = shapes.splice(currentEditIndex, 1); if (removes && removes.length > 0) { this.setState({ shapes: [...shapes], currentEditIndex: -1 }); onDeletedShape?.call(this, removes[0]); } } } }); _defineProperty(this, "mainMouseDown", evt => { // e.button 0 : left button 1 是滚轮按键 2是右键 const e = evt.nativeEvent; e.stopImmediatePropagation(); if (e.button !== 0) return; // 仅支持左键操作 this._mouseTarget = e.target; this._isMouseDown = true; const x = e.offsetX; const y = e.offsetY; this._mouseDownPostion = { x, y }; this._lastMousePosition = this._mouseDownPostion; }); _defineProperty(this, "mainMouseMove", evt => { if (!this._isMouseDown) return; const e = evt.nativeEvent; e.stopImmediatePropagation(); const { mode } = this.props; const x = e.offsetX; const y = e.offsetY; const translateX = x - this._lastMousePosition.x; const translateY = y - this._lastMousePosition.y; switch (mode) { case BoardMode.ReadOnly: // move backgroud let matrix = this.mapscaleControlRef.current?.getCTM(); matrix = matrix?.translate(translateX / matrix.a, translateY / matrix.d); // 矩阵偏移函数 this.ResetTransform(matrix); this._lastMousePosition = { x, y }; break; case BoardMode.Edit: if (this._mouseTarget && this._mouseTarget.tagName === 'rect') { // 移动矩形 const matrix = this.mapscaleControlRef.current?.getCTM(); const svgContainer = this._mouseTarget.parentNode; const nowX = parseFloat(svgContainer.getAttribute('x') || '0') + translateX / matrix.a; const nowY = parseFloat(svgContainer.getAttribute('y') || '0') + translateY / matrix.d; svgContainer.setAttribute('x', nowX.toString()); svgContainer.setAttribute('y', nowY.toString()); this._lastMousePosition = { x, y }; } else if (this._mouseTarget && this._mouseTarget.tagName === 'circle') { const matrix = this.mapscaleControlRef.current?.getCTM(); const svgContainer = this._mouseTarget.parentNode; const oldItem = { x: parseFloat(svgContainer.getAttribute('x') || '0'), y: parseFloat(svgContainer.getAttribute('y') || '0'), width: parseFloat(svgContainer.getAttribute('width') || '0'), height: parseFloat(svgContainer.getAttribute('height') || '0') }; let nowItem = {}; const className = this._mouseTarget.className.baseVal; // console.log('className', className); switch (className) { case 'left-top': nowItem = { x: oldItem.x + translateX / matrix.a, y: oldItem.y + translateY / matrix.d, width: oldItem.width - translateX / matrix.a, height: oldItem.height - translateY / matrix.d }; if (nowItem.x < oldItem.x + oldItem.width && nowItem.y < oldItem.y + oldItem.height) { svgContainer.setAttribute('x', nowItem.x); svgContainer.setAttribute('y', nowItem.y); svgContainer.setAttribute('width', nowItem.width); svgContainer.setAttribute('height', nowItem.height); } break; case 'left': nowItem = { x: oldItem.x + translateX / matrix.a, width: oldItem.width - translateX / matrix.a }; if (nowItem.x < oldItem.x + oldItem.width) { svgContainer.setAttribute('x', nowItem.x); svgContainer.setAttribute('width', nowItem.width); } break; case 'left-bottom': nowItem = { x: oldItem.x + translateX / matrix.a, width: oldItem.width - translateX / matrix.a, height: oldItem.height + translateY / matrix.d }; if (nowItem.x < oldItem.x + oldItem.width && nowItem.height > 0) { svgContainer.setAttribute('x', nowItem.x); svgContainer.setAttribute('width', nowItem.width); svgContainer.setAttribute('height', nowItem.height); } break; case 'top': nowItem = { y: oldItem.y + translateY / matrix.d, height: oldItem.height - translateY / matrix.d }; if (nowItem.height > 0) { svgContainer.setAttribute('y', nowItem.y); svgContainer.setAttribute('height', nowItem.height); } break; case 'bottom': nowItem = { height: oldItem.height + translateY / matrix.d }; if (nowItem.height > 0) { svgContainer.setAttribute('height', nowItem.height); } break; case 'right-top': nowItem = { y: oldItem.y + translateY / matrix.d, width: oldItem.width + translateX / matrix.a, height: oldItem.height - translateY / matrix.d }; if (nowItem.width > 0 && nowItem.height > 0) { svgContainer.setAttribute('y', nowItem.y); svgContainer.setAttribute('width', nowItem.width); svgContainer.setAttribute('height', nowItem.height); } break; case 'right': nowItem = { width: oldItem.width + translateX / matrix.a }; if (nowItem.width > 0) { svgContainer.setAttribute('width', nowItem.width); } break; case 'right-bottom': nowItem = { width: oldItem.width + translateX / matrix.a, height: oldItem.height + translateY / matrix.d }; if (nowItem.width > 0 && nowItem.height > 0) { svgContainer.setAttribute('width', nowItem.width); svgContainer.setAttribute('height', nowItem.height); } break; default: break; } this._lastMousePosition = { x, y }; } else { // 画虚线框 this.setDrawRect({ x: Math.min(this._mouseDownPostion.x, x), y: Math.min(this._mouseDownPostion.y, y), height: Math.abs(this._mouseDownPostion.y - y), width: Math.abs(this._mouseDownPostion.x - x) }); } break; default: break; } }); _defineProperty(this, "mainMouseUp", evt => { if (!this._isMouseDown) return; const e = evt.nativeEvent; e.stopImmediatePropagation(); if (e.button !== 0) return; // 仅支持左键操作 const { mode, onCreatedShape, onUpdatedShape, drawShapeType } = this.props; const { currentEditIndex, shapes } = this.state; const x = e.offsetX; const y = e.offsetY; switch (mode) { case BoardMode.ReadOnly: break; case BoardMode.Edit: if (Math.abs(x - this._mouseDownPostion.x) > 1 || Math.abs(y - this._mouseDownPostion.y) > 1) { // 鼠标有移动 // console.log('this._mouseTarget', this._mouseTarget); if (this._mouseTarget) { if (this._mouseTarget.tagName === 'image') { // 创建矩形 if (drawShapeType === ShapeTypeEnum.Rectangle) { if (Math.abs(x - this._mouseDownPostion.x) > 5 && Math.abs(y - this._mouseDownPostion.y) > 5) { // 创建尺寸大于5*5 let matrix = this.mapscaleControlRef.current?.getCTM(); let rectX = Math.min(this._mouseDownPostion.x, x); let rectY = Math.min(this._mouseDownPostion.y, y); rectX = (rectX - matrix.e) / matrix.a; rectY = (rectY - matrix.f) / matrix.d; let theWidth = Math.abs(this._mouseDownPostion.x - x) / matrix.a; let theHeight = Math.abs(this._mouseDownPostion.y - y) / matrix.d; const rect = { type: ShapeTypeEnum.Rectangle, shape: { x: rectX, y: rectY, width: theWidth, height: theHeight } }; shapes.push(rect); this.setState({ shapes: [...shapes], currentEditIndex: shapes.length - 1 }); onCreatedShape?.call(this, rect); } this.setDrawRect({ x: 0, y: 0, height: 0, width: 0 }); } } else if (this._mouseTarget.tagName === 'rect') { // 移动矩形 const matrix = this.mapscaleControlRef.current?.getCTM(); const translateX = x - this._lastMousePosition.x; const translateY = y - this._lastMousePosition.y; const svgContainer = this._mouseTarget.parentNode; const nowX = parseFloat(`${svgContainer.getAttribute('x')}`) + translateX / matrix.a; const nowY = parseFloat(`${svgContainer.getAttribute('y')}`) + translateY / matrix.d; const rect = shapes[currentEditIndex]; const shape = rect.shape; shape.x = nowX; shape.y = nowY; rect.shape = shape; this.setState({ shapes: [...shapes] }); onUpdatedShape?.call(this, rect); } else if (this._mouseTarget.tagName === 'circle') { // 矩形拉伸 const svgContainer = this._mouseTarget.parentNode; const rect = shapes[currentEditIndex]; rect.shape = { x: parseFloat(svgContainer.getAttribute('x')), y: parseFloat(svgContainer.getAttribute('y')), width: parseFloat(svgContainer.getAttribute('width')), height: parseFloat(svgContainer.getAttribute('height')) }; this.setState({ shapes: [...shapes] }); onUpdatedShape?.call(this, rect); } else if (this._mouseTarget.tagName === 'svg' && this._mouseTarget.className.baseVal === 'mainBox') { this.setDrawRect({ x: 0, y: 0, height: 0, width: 0 }); } } } break; default: break; } this._isMouseDown = false; }); _defineProperty(this, "mainMouseWheel", evt => { if (this._isMouseDown) return; const { scaleNum = 0.1 } = this.props; const e = evt.nativeEvent; e.stopImmediatePropagation(); let zoomScale = 1; // if (e.wheelDelta > 0) { if (e.deltaY < 0) { zoomScale += scaleNum; } else { zoomScale -= scaleNum; } // if (evt.target.id.indexOf("mainBox") != -1) { //鼠标在缩放控件外,主容器内 //|| evt.target.id == "tempImg" // scalePoint = null; // } this.setZoomScale(zoomScale, { 'x': e.offsetX, 'y': e.offsetY }); }); _defineProperty(this, "setZoomScale", (zoomScale, scalePoint) => { let matrix = this.mapscaleControlRef.current?.getCTM(); const { maxScale = 5, minScale = 0.1 } = this.props; if ((matrix.a >= maxScale || matrix.d >= maxScale) && zoomScale > 1) { // 超出或达到最大比例 return; } else if ((matrix.a <= minScale || matrix.d <= minScale) && zoomScale < 1) { // 超出或达到最小比例 return; } if ((matrix.a * zoomScale > maxScale || matrix.d * zoomScale > maxScale) && zoomScale > 1) { // 放大比例超过最大比例,则取最大比例 if (matrix.a > matrix.d) { zoomScale = maxScale / matrix.a; } else { zoomScale = maxScale / matrix.d; } } else if ((matrix.a * zoomScale < minScale || matrix.d * zoomScale < minScale) && zoomScale < 1) { // 缩小比例超过最小比例,则取最小比例 if (matrix.a < matrix.d) { zoomScale = minScale / matrix.a; } else { zoomScale = minScale / matrix.d; } } let offsetX = 0; // 偏移缩放后实际的偏移量 let offsetY = 0; if (scalePoint) { offsetX = scalePoint.x - matrix.e; // / matrix.a * matrix.a;//偏移缩放后实际的偏移量 offsetY = scalePoint.y - matrix.f; // / matrix.d * matrix.d; } else { offsetX = this.mainBoxRef.current.clientWidth * matrix.a / 2; // 缩放控件中心点X offsetY = this.mainBoxRef.current.clientHeight * matrix.d / 2; // 缩放控件中心点Y } const translateX = offsetX - offsetX * zoomScale; const translateY = offsetY - offsetY * zoomScale; matrix = matrix.translate(translateX / matrix.a, translateY / matrix.d); matrix = matrix.scale(zoomScale); this.ResetTransform(matrix); }); _defineProperty(this, "setOriginalScale", () => { this.mapscaleControlRef.current?.setAttribute('transform', 'matrix(1,0,0,1,0,0)'); }); _defineProperty(this, "ResetTransform", Matrix => { // console.info("last matrix(" + Matrix.a + "," + Matrix.b + "," + Matrix.c + "," + Matrix.d + "," + Matrix.e + "," + Matrix.f + ")"); this.mapscaleControlRef.current?.setAttribute('transform', 'matrix(' + Matrix.a + ',' + Matrix.b + ',' + Matrix.c + ',' + Matrix.d + ',' + Matrix.e + ',' + Matrix.f + ')'); }); _defineProperty(this, "setDrawRect", rect => { this.drawRectRef.current?.setAttribute('height', `${rect.height}`); this.drawRectRef.current?.setAttribute('width', `${rect.width}`); this.drawRectRef.current?.setAttribute('x', `${rect.x}`); this.drawRectRef.current?.setAttribute('y', `${rect.y}`); }); _defineProperty(this, "getData", () => { console.log('getData'); return this.state.shapes; }); this.state = { uploading: false, imageUrl: '', imageSize: { width: 100, height: 100 }, currentEditIndex: -1, shapes: props.initShapes || [] }; this.mainBoxRef = React.createRef(); this.mapscaleControlRef = React.createRef(); this.drawRectRef = React.createRef(); } componentDidMount() { this.props.onLoad?.call(this, this); const { imageUrl } = this.props; setTimeout(() => { this.preLoadImage(imageUrl); }, 100); window.addEventListener('resize', this.resize); } componentWillUnmount() { window.removeEventListener('resize', this.resize); } render() { const { mode, shapeClassName } = this.props; const { imageUrl, imageSize, currentEditIndex, shapes } = this.state; return imageUrl && /*#__PURE__*/React.createElement("div", { style: { height: '100%' }, onContextMenu: e => e.preventDefault(), onSelect: e => e.preventDefault() }, /*#__PURE__*/React.createElement("svg", { ref: this.mainBoxRef, className: "mainBox", onMouseDown: this.mainMouseDown, onMouseMove: this.mainMouseMove, onMouseUp: this.mainMouseUp, onWheel: this.mainMouseWheel, tabIndex: 0, onKeyDown: e => this.onkeydown(e.nativeEvent) }, /*#__PURE__*/React.createElement("g", { ref: this.mapscaleControlRef, transform: "matrix(1,0,0,1,0,0)" }, /*#__PURE__*/React.createElement("image", { href: imageUrl, width: imageSize.width, height: imageSize.height }), shapes && shapes.map((item, index) => { if (item.type === ShapeTypeEnum.Rectangle) { const rectInfo = item.shape; return /*#__PURE__*/React.createElement("svg", _extends({ key: index // id={`svg_rect_${index}`} , className: `shape ${shapeClassName} ${currentEditIndex === index ? 'active' : ''} ${mode === BoardMode.Edit ? 'edit' : ''}` // x={item.shape.x} y={item.shape.y} height={item.shape.height} width={item.shape.width} }, rectInfo, { onMouseDown: e => this.setState({ currentEditIndex: index }) }), /*#__PURE__*/React.createElement("rect", { x: "0", y: "0", height: "100%", width: "100%" }), mode === BoardMode.Edit && currentEditIndex === index && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("circle", { className: "left-top", r: "4", cx: "0", cy: "0" }), /*#__PURE__*/React.createElement("circle", { className: "left", r: "4", cx: "0", cy: "50%" }), /*#__PURE__*/React.createElement("circle", { className: "left-bottom", r: "4", cx: "0", cy: "100%" }), /*#__PURE__*/React.createElement("circle", { className: "top", r: "4", cx: "50%", cy: "0" }), /*#__PURE__*/React.createElement("circle", { className: "bottom", r: "4", cx: "50%", cy: "100%" }), /*#__PURE__*/React.createElement("circle", { className: "right-top", r: "4", cx: "100%", cy: "0" }), /*#__PURE__*/React.createElement("circle", { className: "right", r: "4", cx: "100%", cy: "50%" }), /*#__PURE__*/React.createElement("circle", { className: "right-bottom", r: "4", cx: "100%", cy: "100%" }))); } else { return /*#__PURE__*/React.createElement("div", { key: index }, " "); } })), /*#__PURE__*/React.createElement("rect", { ref: this.drawRectRef, strokeWidth: "1", strokeDasharray: "6,2", stroke: "#15f635", fill: "none" }))); } } _defineProperty(SvgImageBoard, "propTypes", { imageUrl: _pt.string.isRequired, mode: _pt.oneOf([0, 1]).isRequired, initShapes: _pt.arrayOf(_pt.shape({ key: _pt.any, type: _pt.oneOf([0, 1, 2, 3, 4]).isRequired, tag: _pt.any })), onLoad: _pt.func, onDeletedShape: _pt.func, onCreatedShape: _pt.func, onUpdatedShape: _pt.func, scaleNum: _pt.number, maxScale: _pt.number, minScale: _pt.number, shapeClassName: _pt.string, drawShapeType: _pt.oneOf([0, 1, 2, 3, 4]) }); _defineProperty(SvgImageBoard, "defaultProps", { scaleNum: 0.1, maxScale: 5, minScale: 0.1, shapeClassName: 'red', drawShapeType: ShapeTypeEnum.Rectangle }); export default SvgImageBoard; //# sourceMappingURL=index.js.map