svg-action
Version:
控制SVG拖动,缩放,旋转,转图片等
988 lines (847 loc) • 25.1 kB
JavaScript
let debug = false;
/**
* 给svg添加放大缩小的功能
*/
SvgAction.defaultOption = {
// 鼠标超过多少才移动
threshold: 15,
// 每次放大比例
deltaThreshold: 0.1,
// 可缩放倍数
range: { min: 0.4, max: 100 },
// 每次缩放比例
scale: 0.25,
// ctrl | shift + 鼠标滚轮每次移动的像素
stepSize: 75,
// 键盘或调用移动方法时默认步长
keyStepSize: 10,
// 滚轮缩放
mouseScale: true,
// 平移
mouseMove: true,
// 拖动
mouseDrag: true,
// 旋转
mouseRotate: true,
// 翻转
mouseFlip: true,
autoCenter: true,
};
let DEG_ELEMENT;
/**
* @param {Object} options
options: {
threshold {Number}: 指定鼠标拖动时起始阈值,默认:15
deltaThreshold {Number} 最小放大比例,默认:0.1
range {Object} {
min: 最小缩放倍数,默认:0.25
max: 最大缩放倍数,默认:100
}
scale {Number}: 每次缩放比例,默认:0.25
stepSize {Number}: ctrl | shift + 鼠标滚轮每次移动的像素 默认75
keyStepSize {Number}: 键盘或调用移动方法时默认步长,默认10
mouseControl {Element}: 设置鼠标控制节点,默认svg节点父级document
mouseScale {Boolean} 是否开启鼠标缩放,默认true
mouseMove {Boolean} 是否开启鼠标移动,按住Shift或Ctrl键 + 滚轮即可平移,默认true
mouseDrag {Boolean} 是否开启鼠标拖拽,默认true
mouseRotate {Boolean} 是否开启鼠标旋转,按住ctrl即可旋转,默认true
mouseFlip {Boolean} 是否开启鼠标翻转,按住Shift即可旋转,默认true
keyControl {Element} 设置键盘控制的节点,默认svg父级或document
}
*/
function SvgAction (svg, options) {
let self = this;
if (!svg) {
throw new Error('初始化失败,找不到SVG!');
}
self._svg = svg;
self.options = {
...SvgAction.defaultOption,
...options,
};
self.options.mouseControl = self.getControlElement();
self.options.keyControl = self.getKeyElement();
self._totalDelta = 0;
self._previouScale = 1; // 当前缩放大小
self._deg = 0; // 当前旋转角度
self._init();
}
SvgAction.prototype._init = function () {
let self = this;
self._initSvg();
// 添加css
generateCss();
self.buildEvent();
if (self.options.autoCenter){
if (self._svg.offsetParent) {
self.reset();
} else {
let observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
observer.unobserve(self._svg);
observer.disconnect();
self.reset();
}
});
observer.observe(self._svg);
}
}
}
SvgAction.prototype.buildEvent = function () {
if (this.eventState){
return;
}
this.eventState = true;
// 绑定鼠标事件
buildMoveEvent.call(this);
buildMousewheelEvent.call(this);
// 绑定键盘事件
buildKeyEvent.call(this);
}
function stopBubble(event) {
let e = event || window.event;
if (e.preventDefault) {
e.preventDefault();
}
// 一般用在鼠标或键盘事件上
if (e.stopPropagation) {
// W3C取消冒泡事件
e.stopPropagation();
} else {
// IE取消冒泡事件
window.event.cancelBubble = true;
}
};
function touchDebug(self, fast, last, cente){
if(!debug){
return;
}
let ns = 'http://www.w3.org/2000/svg'
let group = self._viewport.querySelector("#touchDebug"),
fastText, lastText, centeText;
if (group){
fastText = group.querySelector('#fastText');
lastText = group.querySelector('#lastText');
centeText = group.querySelector('#centeText');
} else {
group = document.createElementNS(ns, 'g');
fastText = document.createElementNS(ns,'text');
lastText = document.createElementNS(ns,'text');
centeText = document.createElementNS(ns, 'text');
group.id = 'touchDebug';
fastText.id = 'fastText';
lastText.id = 'lastText';
centeText.id = 'centeText';
fastText.innerHTML = '点1';
lastText.innerHTML = '点2';
centeText.innerHTML = '中';
group.appendChild(fastText);
group.appendChild(lastText);
group.appendChild(centeText);
self._viewport.appendChild(group);
}
// if (!cente){
// cente = getPointCentre(fast.x, fast.y, last.x, last.y);
// }
let f = transformPoint(self._svg, fast.x, fast.y);
let l = transformPoint(self._svg, last.x, last.y);
// let c = transformPoint(self._svg, cente.x, cente.y);
fastText.setAttribute('transform', `translate(${f.x}, ${f.y})`)
lastText.setAttribute('transform', `translate(${l.x}, ${l.y})`)
centeText.setAttribute('transform', `translate(${cente.x}, ${cente.y})`)
}
function transformPoint(svg, screenX, screenY) {
var p = svg.createSVGPoint()
p.x = screenX
p.y = screenY
return p.matrixTransform(svg.getScreenCTM().inverse())
}
/**
* 添加图形移动事件
*/
function buildMoveEvent () {
let self = this;
let ops = self.options;
let context = {};
// 添加事件
// 鼠标按下
let el = ops.mouseControl;
el.addEventListener('mousedown', handleMousedown, false);
el.addEventListener('touchstart', handleTouchstart, { passive: false });
function handleMousedown(event) { // 开启拖拽
// stopBubble(event);
context.start = toPoint(event); // 起始坐标
self._svg.className.baseVal = 'move-cursor-grabbing';
document.addEventListener('mousemove', handleMouseMove, false);
document.addEventListener('mouseup', handleEnd, false);
}
function handleTouchstart(event){
stopBubble(event);
context.start = toPoint(event); // 起始坐标
document.addEventListener('touchmove', handleMouseMove, { passive: false });
document.addEventListener('touchend', handleEnd, { passive: false });
if (event.touches.length >= 2){
let fast = toPoint(event.touches[0]);
let last = toPoint(event.touches[1]);
let cente = getPointCentre(fast.x, fast.y, last.x, last.y);
cente = transformPoint(self._svg, cente.x, cente.y),
context.doubleStart = {
fast,
last,
cente,
length: getDistance(fast.x, fast.y, last.x, last.y),
};
touchDebug(self, fast, last, cente);
}
}
function handleMouseMove(event){
stopBubble(event);
if (event.type == 'touchmove' && event.touches.length >= 2){
// 缩放
let fast = toPoint(event.touches[0]);
let last = toPoint(event.touches[1]);
let doubleStart = context.doubleStart;
touchDebug(self, fast, last, doubleStart.cente);
// 计算距离
let length = getDistance(fast.x, fast.y, last.x, last.y)
if (length > doubleStart.length){ // 放大
self._zoom(1, doubleStart.cente, 0.025);
} else { // 缩小
self._zoom(-1, doubleStart.cente, 0.025);
}
doubleStart.length = length;
return;
}
context.position = toPoint(event); // 当前鼠标坐标
context.delta = deltaPos(context.position, context.start); // 当前坐标到起始坐标的偏移
// 鼠标移动超过15px才移动图形,防止晃动
let fast = false;
if (!context.dragging && _length(context.delta) > ops.threshold) {
context.dragging = true;
fast = true;
}
if (!context.dragging){
return;
}
if (ops.mouseRotate && event.ctrlKey){
handleRotate(event, fast);
} else if (ops.mouseFlip && event.shiftKey) {
handleFlip(event, fast);
} else if (ops.mouseDrag){
handleMove(event, fast);
}
context.last = context.position; // 记录下最后一次移动的位置
}
// 处理拖拽
function handleMove (event) {
let lastPosition = context.last || context.start;
let delta = deltaPos(context.position, lastPosition);
self.move({
dx: delta.x,
dy: delta.y
});
}
/**
* 处理旋转
* 按住Ctrl 加鼠标
*/
function handleRotate(event, fast){
let degEl = degElement();
// 鼠标移动超过15px才移动图形,防止晃动
if (fast) {
document.body.appendChild(degEl);
degEl.show = true;
}
let lastPosition = context.last || context.start;
let delta = deltaPos(context.position, lastPosition);
let step = delta.x || delta.y;
let deg = step < 0 ? -1 : 1;
self.rotate(deg); // 没出触发旋转1度
degEl.style.top = event.clientY + 'px';
degEl.style.left = event.clientX + 20 + 'px';
degEl.innerText = self._deg + '°';
}
/**
* 处理翻转
* 按住Shift 加鼠标
*/
function handleFlip(event, fast){
if (!fast){
return;
}
if (context.delta.x >= ops.threshold){ // X 轴
self.flipX();
} else if (context.delta.x < ops.threshold){ //Y轴
self.flipY();
}
}
function handleEnd (event) {
self._svg.className.baseVal = 'move-cursor-grab';
context = {}; // 每一次移动结束,都清除上一次的结果
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleEnd);
document.removeEventListener('touchmove', handleMouseMove);
document.removeEventListener('touchend', handleEnd);
degElement().remove();
}
self._eventFn = {
handleMousedown,
handleTouchstart,
handleMouseMove,
handleEnd
}
}
//计算两点间距离
function getDistance (startX, startY, endX, endY) {
return Math.hypot(endX - startX, endY - startY);
};
function getPointCentre(startX, startY, endX, endY) {
let x = (startX - endX) / 2
let y = (startY - endY) / 2
return {
x: startX + (0 - x),
y: startY + (0 - y)
};
};
function degElement(){
if (DEG_ELEMENT){
return DEG_ELEMENT;
}
DEG_ELEMENT = document.createElement('span');
DEG_ELEMENT.style.position = 'fixed';
DEG_ELEMENT.style.userSelect = 'none';
return DEG_ELEMENT;
}
/**
* 图形缩放事件
* 滚轮事件
*/
function buildMousewheelEvent () {
let self = this;
let el = self.options.mouseControl;
el.addEventListener('mousewheel', handleMousewheel, false);
function handleMousewheel (event) {
stopBubble(event);
if (self.options.mouseMove && (event.ctrlKey || event.shiftKey)) {
handleWheelMove(event);
} else if (self.options.mouseScale) {
handleMouseScale(event);
}
}
self._eventFn.handleMousewheel = handleMousewheel;
// 平移
function handleWheelMove(event){
let factor = self.options.stepSize / 100;
let delta;
if (event.shiftKey) {
delta = { // 水平
dx: factor * event.deltaY,
dy: 0
};
} else { // 垂直
delta = {
dx: factor * event.deltaX,
dy: factor * event.deltaY
};
}
self.move(delta);
}
/**
* 缩放
*/
function handleMouseScale(event){
var elementRect = el.getBoundingClientRect();
var offset = {
x: event.clientX - elementRect.left,
y: event.clientY - elementRect.top
};
self.zoom(event.wheelDelta < 0 ? 'out' : 'in', offset);
}
}
/**
* 处理键盘事件
*/
function buildKeyEvent(){
let self = this;
self.options.keyControl.addEventListener('keydown', handleKeydown);
function handleKeydown (event) {
let key = getKey(event);
if (!key) {
return;
}
if (event.ctrlKey) {
handleRotateAndZoom(key);
} else if (event.shiftKey) {
handleFlip(key);
} else if (key == 'center') {
self.reset();
} else {
handleMove(key);
}
}
self._eventFn.handleKeydown = handleKeydown;
function handleMove(key){
self[key]();
// switch(key){
// case 'up':
// self.up();
// break;
// case 'down':
// self.down();
// break;
// case 'left':
// self.left();
// break;
// case 'right':
// self.right();
// break;
// }
}
function handleRotateAndZoom(key){
switch(key){
case 'up':
self.zoomIn();
break;
case 'left':
self.rotate();
break;
case 'down':
self.zoomOut();
break;
case 'right':
self.rotate(-45);
break;
}
}
function handleFlip(key){
switch (key) {
case 'up':
case 'down':
self.flipY();
break;
case 'left':
case 'right':
self.flipX();
break;
}
}
}
function getKey(event){
switch (event.key){
case 'w':
case 'ArrowUp':
return 'up';
case 's':
case 'ArrowDown':
return 'down';
case 'a':
case 'ArrowLeft':
return 'left';
case 'd':
case 'ArrowRight':
return 'right';
case '0':
case '`':
return 'center';
default: return null;
}
}
/**
* 初始化svg,第一个节点必须是g
* 如果不是就添加
*/
SvgAction.prototype._initSvg = function () {
let svg = this._svg;
svg.className.baseVal = 'move-cursor-grab';
// 第一个子节点必须是g
if (svg.tagName === 'g') {
this._viewport = svg;
this._svg = findSvg(this._viewport) || this._viewport;
function findSvg(node){
if (node.parentElement.tagName == 'svg'){
return node.parentElement;
}
return findSvg(node.parentElement);
}
} if (svg.childNodes.length === 1 && svg.firstElementChild.tagName === 'g') { // 判断第一个节点是否是g
this._viewport = svg.firstElementChild;
} else {
let g = document.createElementNS(svg.namespaceURI, 'g');
let childNodes = svg.childNodes;
for (;childNodes.length !== 0;) {
g.appendChild(childNodes[0]);
}
svg.appendChild(g);
this._viewport = g;
}
}
/**
* 放大
* @param {Number} i
*/
SvgAction.prototype.zoomIn = function (i) {
i = i || this.options.scale;
this._zoom(1, getCentre.call(this), i);
}
/**
* 缩小
* @param {Number} i
*/
SvgAction.prototype.zoomOut = function (i) {
i = i || this.options.scale;
this._zoom(-1, getCentre.call(this), i);
}
SvgAction.prototype.reset = function reset() {
// this.zoom('fit-viewport');
this.zoom('center');
};
/**
* 缩放
* @param {Number | String} delta in: 放大,out:缩小,center 居中,如果为数字,表示缩放倍数
* @param {Object} position {x,y}以哪个位置为中心, 默认中心
*/
SvgAction.prototype.zoom = function (delta, position) {
let self = this;
if (!delta) {
return round(self._viewport.getCTM().a, 1000);
}
var scale = self.options.scale;
if (delta === 'center') {
return self._fitViewport(delta);
} else if(delta === 'in') {
delta = 1;
} else if (delta === 'out') {
delta = -1;
} else if (typeof (delta) == 'number'){
scale = Math.abs(delta);
} else {
return round(self._viewport.getCTM().a, 1000);
}
self._totalDelta += delta;
if (Math.abs(self._totalDelta) > self.options.deltaThreshold) {
let i = self._zoom(delta, position, scale);
// reset
self._totalDelta = 0;
return i;
}
};
/**
* 缩放
* @param {Number} delta 正数或负数,正数表示放大,负数缩小
* @param {Object} position 偏移
* @param {Number} stepSize 缩放比例
* @returns
*/
SvgAction.prototype._zoom = function (delta, position, stepSize) {
let self = this;
var direction = delta > 0 ? 1 : -1;
var currentLinearZoomLevel = log10(self._previouScale);
var newLinearZoomLevel = Math.round(currentLinearZoomLevel / stepSize) * stepSize;
newLinearZoomLevel += stepSize * direction;
var newLogZoomLevel = Math.pow(10, newLinearZoomLevel);
setZoom.call(self, cap(self.options.range, newLogZoomLevel), position);
this._previouScale = round(self._viewport.getCTM().a, 1000);
return this._previouScale;
};
function setZoom (scale, center) {
var svg = this._svg,
viewport = this._viewport;
var matrix = svg.createSVGMatrix();
var point = svg.createSVGPoint();
var centerPoint,
originalPoint,
currentMatrix,
scaleMatrix,
newMatrix;
currentMatrix = viewport.getCTM();
var currentScale = currentMatrix.a;
if (center) {
point.x = center.x;
point.y = center.y;
centerPoint = point;
// revert applied viewport transformations
originalPoint = centerPoint.matrixTransform(currentMatrix.inverse());
// create scale matrix
scaleMatrix = matrix
.translate(originalPoint.x, originalPoint.y)
.scale(1 / currentScale * scale)
.translate(-originalPoint.x, -originalPoint.y);
newMatrix = currentMatrix.multiply(scaleMatrix);
} else {
newMatrix = matrix.scale(scale);
}
setCTM(this._viewport, {matrix: newMatrix});
return newMatrix;
};
SvgAction.prototype._fitViewport = function (center) {
let self = this;
let outer = {
height: self._svg.clientHeight,
width: self._svg.clientWidth
};
// let inner = {
// height: self._viewport.clientHeight,
// width: self._viewport.clientWidth,
// x: 0,
// y: 0
// };
let inner = self._viewport.getBBox();
let newScale, newViewbox;
// display the complete diagram without zooming in.
// instead of relying on internal zoom, we perform a
// hard reset on the canvas viewbox to realize this
//
// if diagram does not need to be zoomed in, we focus it around
// the diagram origin instead
if (inner.x >= 0 &&
inner.y >= 0 &&
inner.x + inner.width <= outer.width &&
inner.y + inner.height <= outer.height &&
!center) {
newViewbox = {
x: 0,
y: 0,
width: Math.max(inner.width + inner.x, outer.width),
height: Math.max(inner.height + inner.y, outer.height)
};
} else {
newScale = Math.min(1, outer.width / inner.width, outer.height / inner.height);
let offsetX = - inner.x * newScale;
let offsetY = - inner.y * newScale;
let x = outer.width / 2 - inner.width * newScale / 2;
let y = outer.height / 2 - inner.height * newScale / 2;
newViewbox = {
x: x >= 0 ? x + offsetX : offsetX,
y: y >= 0 ? y + offsetY : offsetY
};
}
let newMatrix = self._svg.createSVGMatrix()
.translate(newViewbox.x, newViewbox.y)
.scale(newScale);
setCTM(self._viewport, {matrix: newMatrix});
// 设置角度还原
self._deg = 0;
return self._previouScale = round(self._viewport.getCTM().a, 1000);
};
/**
* 平移
* @param {Object} delta {dx: 0, dy: 0}
*/
SvgAction.prototype.move = function (delta) {
let viewport = this._viewport;
var matrix = viewport.getCTM();
if (delta) {
delta.dx = delta.dx || delta.x || 0;
delta.dy = delta.dy || delta.y || 0;
delta = Object.assign({ dx: 0, dy: 0 }, delta);
// 使用浏览器的api计算偏移距离
matrix = this._svg.createSVGMatrix().translate(delta.dx, delta.dy).multiply(matrix);
setCTM(viewport, {matrix});
}
return { x: matrix.e, y: matrix.f };
}
/**
* 上移
* @param {Number} i 移动的像素点
*/
SvgAction.prototype.up = function (i) {
i = i || this.options.keyStepSize;
return this.move({dy: -i});
}
/**
* 下移
* @param {Number} i 移动的像素点
*/
SvgAction.prototype.down = function (i) {
i = i || this.options.keyStepSize;
return this.move({dy: i});
}
/**
* 左移
* @param {Number} i 移动的像素点
*/
SvgAction.prototype.left = function (i) {
i = i || this.options.keyStepSize;
return this.move({dx: -i});
}
/**
* 右移
* @param {Number} i 移动的像素点
*/
SvgAction.prototype.right = function (i) {
i = i || this.options.keyStepSize;
return this.move({dx: i});
}
/**
* 旋转
*/
SvgAction.prototype.rotate = function(angle = 45){
let bbox = this._viewport.getBBox();
setCTM(this._viewport, {
rotate: {
angle: angle,
x: bbox.width / 2 + bbox.x,
y: bbox.height / 2 + bbox.y
}
});
this._deg += angle;
}
/**
* 翻转
*/
SvgAction.prototype.flipX = function () {
let ctm = this._viewport.getCTM();
setCTM(this._viewport, { matrix: ctm.flipX() });
}
SvgAction.prototype.flipY = function () {
let ctm = this._viewport.getCTM();
setCTM(this._viewport, { matrix: ctm.flipY() });
}
SvgAction.prototype.getControlElement = function () {
return this.options.mouseControl || this._svg.parentElement || document;
}
SvgAction.prototype.getKeyElement = function () {
return this.options.keyControl || document;
}
SvgAction.prototype.toImage = function () {
let self = this;
let image = new Image();
let rect = self._svg.getBoundingClientRect();
image.width = rect.width;
image.height = rect.height;
image.src = 'data:image/svg+xml;utf8,' + unescape(self._svg.outerHTML);
return new Promise((resolve)=>{
image.onload = function () {
let canvas = document.createElement('canvas');
canvas.width = rect.width;
canvas.height = rect.height;
let context = canvas.getContext('2d');
context.drawImage(image, 0, 0, rect.width, rect.height);
resolve(canvas.toDataURL('image/png'));
}
});
}
SvgAction.prototype.save = function () {
this.toImage().then(url=>{
var a = document.createElement('a');
a.href = url;
a.download = "svg.png"; //设定下载名称
a.click(); //点击触发下载
})
}
/**
* 关闭事件
*/
SvgAction.prototype.disableEvent = function () {
// 清空事件
let self = this;
let ops = self.options;
let eventFn = self._eventFn;
let el = ops.mouseControl;
self.eventState = false;
el.removeEventListener('mousedown', eventFn.handleMousedown);
el.removeEventListener('touchstart', eventFn.handleTouchstart);
document.removeEventListener('mousemove', eventFn.handleMouseMove);
document.removeEventListener('mouseup', eventFn.handleEnd);
document.removeEventListener('touchmove', eventFn.handleMouseMove);
document.removeEventListener('touchend', eventFn.handleEnd);
el.removeEventListener('mousewheel', eventFn.handleMousewheel);
ops.keyControl.removeEventListener('keydown', eventFn.handleKeydown);
}
/**
* 获取图形原中心坐标
* @param {Element} node
*/
function getCentre (){
let rect = this._viewport.getBoundingClientRect();
let ctm = this._viewport.getCTM();
return {
x: ctm.e + rect.width / 2,
y: ctm.f + rect.height / 2,
}
}
/**
* 设置matrix
* @param {Object} node 节点
* @param {Object} m 平移或放大
*/
function setCTM(node, { matrix, rotate }) {
matrix = matrix || node.getCTM();
// var mstr = 'matrix(' + m.a + ',' + m.b + ',' + m.c + ',' + m.d + ',' + m.e + ',' + m.f + ')';
var mstr = `matrix(${matrix.a}, ${matrix.b}, ${matrix.c}, ${matrix.d}, ${matrix.e}, ${matrix.f})`;
if (rotate){
mstr += `, rotate(${rotate.angle}, ${rotate.x}, ${rotate.y})`;
}
node.setAttribute('transform', mstr);
}
function cap (range, scale) {
return Math.max(range.min, Math.min(range.max, scale));
}
function round (number, resolution) {
return Math.round(number * resolution) / resolution;
}
function log10 (x) {
return Math.log(x) / Math.log(10);
}
var sign = Math.sign || function (n) {
return n >= 0 ? 1 : -1;
};
/**
* 计算移动的长度
* @param {Object} point
*/
function length (point) {
return Math.sqrt(Math.pow(point.x, 2) + Math.pow(point.y, 2));
}
/**
* 计算移动了多少
* @param {Object} a
* @param {Object} b
*/
function deltaPos (a, b) {
return {
x: a.x - b.x,
y: a.y - b.y
};
}
/**
* 转换为坐标
* @param {Object} event
*/
function toPoint (event) {
if (event.pointers && event.pointers.length) {
event = event.pointers[0];
}
if (event.touches && event.touches.length) {
event = event.touches[0];
}
return event ? {
x: event.clientX,
y: event.clientY
} : null;
}
function _length (point) {
return Math.sqrt(Math.pow(point.x, 2) + Math.pow(point.y, 2));
}
function generateCss () {
let SvgAction = document.getElementById('SvgAction');
if (SvgAction) {
return;
}
let css = `
.move-cursor-grab{
cursor: grab;
cursor: -webkit-grab;
}
.move-cursor-grabbing{
cursor: grabbing;
cursor: -webkit-grabbing;
}
`;
let style = document.createElement('style');
style.id = 'SvgAction';
style.innerText = css;
document.head.appendChild(style);
}
window.SvgAction = SvgAction;
export default SvgAction;