react-svg-image-board
Version:
A react component. can drawing svg element to image board.
703 lines (612 loc) • 23.6 kB
JavaScript
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