【林大大哟年度推荐功能】
已在多项目中使用,最小改动实现完全通用!
【线上预览地址】lveRectangleJs
林大大将矩形框单独封装成类了噢~仓库地址为LveRectangleJs
1、功能描述
1.1、矩形框的绘制
1.2、矩形框的缩放
1.3、矩形框的拖动
1.4、矩形框的键盘移动方向
1.5、矩形框的截图及导出图片
编辑绘制核心逻辑:通过js监听mouse事件来实现矩形框的绘制,再通过区分点击的是边角还是其他位置来实现矩形框的缩放和拖动,并且在拖动和缩放时,都做了边界限制,当缩放或拖动 到边界时,就不能继续拉缩放拖动了。当然在相关开发时,还是需要你对一些常规的offsetLeft,offsetX等的dom属性了解哟~
截图核心逻辑:获取到矩形框相对坐标及宽高,通过canvas的drawImage来绘制截图,然后将截图导出成图片
2、效果图展示
3、矩形框原理讲解
3.0、矩形类构造器讲解
第一参数为 最外层大盒子dom
第二参数为 矩形框dom
矩形框,其实矩形框并不是凭空绘制出来的,只是先将其宽高置为0,并且定位到负无限处,所以在页面上看不到,在绘制时,通过控制矩形框的定位及宽高来进行展示,这也是我进行绘制/拖动/缩放的核心思想
第三参数为 自定义配置,如果没有配置,将使用默认设置,设置了的话,以用户设置为主
constructor(target = '', childTarget = '', options = {}) {
if (!target || typeof target !== 'string') {
throw new Error('请传入正确的参数')
}
const {
rect,
dot,
events
} = clone(true, defaultOptions, options) // clone为深拷贝的方法,在仓库utils里有
this.rect = rect
this.dot = dot
this.events = events
this.target = document.querySelector(target) // 绑定父级dom
this.childTarget = document.querySelector(childTarget) // 绑定矩形框dom
this.img = {
target: null,
width: 0,
height: 0,
scale: 1
};
this.origin = this.target.getBoundingClientRect()
this.supportEvent = ['draging', 'dragend']
this.customEvent = new Event() // 事件类也在仓库里有
this.defaultEvent() // 默认事件配置,这些都在仓库,可以自行观看
}
3.1、矩形类初始化
实例化写法:
const rect = new Rectangle('#out-box', '#rect', {
rect: {
border: {
width: 1
}
},
dot: {
width: 4
},
events: {
contextmenu: {
disabled: true
},
draggable: {
disabled: false
},
clip: {
disabled: false
}
}
})
<!-- 矩形类第三参数的默认参数 -->
/** 默认配置 */
const defaultOptions = {
/** 矩形框配置 */
rect: {
left: -9999, // 矩形框左上角x坐标
top: 0, // 矩形框左上角y坐标
width: 0, // 矩形框宽度
height: 0, // 矩形框高度
border: { // 矩形框的边框样式
color: '#fa9120',
width: 1
},
style: null
},
/** 矩形框周围小球配置 */
dot: {
width: 6, // 小球宽度
dotNum: 8 // 小球数量,4|8
},
/** 相关事件 */
events: {
contextmenu: { // 右键事件
disabled: false // 是否禁用
},
draggable: { // 拖动事件
disabled: false // 是否禁用
},
clip: { // 截图操作(7个快捷键功能)
/**
* 开始截图:alt + w
* 保存当前截图:ctrl + s
* 关闭当前截图:ctrl + q
* 键盘向左方向键操作
* 键盘向上方向键操作
* 键盘向右方向键操作
* 键盘向下方向键操作
*/
disabled: false // 是否禁用
}
}
}
3.2、绘制方法及监听事件
/** 清屏 */
rect.clearPaint()
/** 开始绘制 */
rect.startPaint()
/** 监听矩形框正在拖动/缩放/绘制 */
rect.on('draging', e => {
console.log('draging', e);
})
/** 监听矩形框操作完成 */
rect.on('dragend', e => {
console.log('dragend', e);
})
3.3、判断当前是拖动还是缩放讲解
当点击矩形框边角附近时,你肯定是想要缩放矩形框,当点击其他位置时,你肯定想要拖动矩形框。
在矩形框内部点击,就可以使用offset相关属性,你所点击的位置,就是offsetX,offsetY,这俩属性是相对于当前dom内部的点击位置的,而offsetWidth,offsetHeight是当前dom的宽高,difference是模糊距离,有时候你得设置一个点击的范围,只要点击点在那个范围内,就是缩放,反之亦然。当然这个范围你可以自己设置,我设置的是 矩形框border宽度的2倍,因为根据实践来看,这个范围是恰到好处的。
当点击的对象不是矩形框时,则代表当前的要对矩形框进行缩放,否则则是要对矩形框进行拖拽
let flag = 0 // 点击的是除边角以外的其他部分 代表着拖动操作
let left = e.offsetX
let top = e.offsetY
let width = e.target.offsetWidth
let height = e.target.offsetHeight
// 点击的是dot,非rect, 代表着拉伸操作
if (e.target !== rect) {
flag = 1
const parent = e.target.offsetParent.getBoundingClientRect()
const child = e.target.getBoundingClientRect()
left = child.x - parent.x
top = child.y - parent.y
width = e.target.offsetParent.offsetWidth
height = e.target.offsetParent.offsetHeight
}
const difference = this.get('dot.width') * 2; // 点击dot周边 get('dot.width') * 2 + px范围为拉伸,其他为拖动
当然,最好的方式就是有八个缩放点,除了这八个缩放点,其余位置都是被拖拽的地方,下面方法就是判断是哪个拉伸点的(这个也蛮好理解,不信你打开你的截图软件,是不是有8个能被缩放的小方块),除去这八个点以外,其他都是返回[-1, -1],以来标识拖动
const resultRange = {
x: 0,
y: 0
}
resultRange.x = 0 // 0 => left, 1 => middle, 2 => right, -1 => 点击的位置不能被拖动
resultRange.y = 0 // 0 => top, 1 => middle, 2 => bottom, -1 => 点击的位置不能被拖动
if (left < difference) { // 点击的位置为矩形左侧
resultRange.x = 0
} else if (left > (width / 2 - difference) && left < (width / 2 + difference)) { // 点击的位置为矩形中间 width/2 - this.get('dot.width') ~ width/2 + this.get('dot.width')
resultRange.x = 1
} else if (left < width && left > (width - difference)){ // 点击的位置为矩形右侧 width - 6px ~ width
resultRange.x = 2
} else {
resultRange.x = -1
}
if (top < difference) { // 点击的位置为矩形上侧
resultRange.y = 0
} else if (top > (height / 2 - difference) && top < (height / 2 + difference)) { // 点击的位置为矩形中间 height/2 - this.get('dot.width') ~ height/2 + this.get('dot.width')
resultRange.y = 1
} else if (top < height && top > (height - difference)){ // 点击的位置为矩形下侧 height - this.get('dot.width') ~ height
resultRange.y = 2
} else {
resultRange.y = -1
}
if (resultRange.x === -1 || resultRange.y === -1 || (resultRange.x === 1 && resultRange.y === 1)) {
return [-1, -1]
}
return [resultRange.x, resultRange.y] // 只会有八个位置能被准确返回,其余都是返回[-1, -1]
3.4、实现拖动和缩放的方法详解
拖动还是缩放?
当3.3知识点中返回的是[-1, -1]时,那就说明是拖动,拖动的话,只需要改变矩形框的left,top定位即可。
if (e.target !== this.childTarget && e.target.className.indexOf('dot') === -1) return; // 类名中包含被放行的dot除外
当返回值不是[-1, -1]时,例如[2, 2],那就说明是向右下角缩放,目前八个方向已经全部实现了噢~
this.childTarget.onmousedown = e => {
if (e.target !== this.childTarget && e.target.className.indexOf('dot') === -1) return; // 类名中包含被放行的dot除外
const flag = this.mousedownHandle(e);
let left = e.clientX;
let top = e.clientY;
const width = this.childTarget.offsetWidth;
const height = this.childTarget.offsetHeight;
const [dragX, dragY] = flag;
// 拖动
if (dragX === -1 && dragY === -1) {
left -= this.childTarget.offsetLeft; // 要保持之前矩形框的坐标值
top -= this.childTarget.offsetTop;
}
const child = e.target.getBoundingClientRect();
document.onmousemove = e => {
// 取消浏览器因回流导致的默认事件及冒泡事件
e.preventDefault();
if (e.stopPropagation) {
e.stopPropagation();
} else {
e.cancelable = true;
}
if (dragX === -1 && dragY === -1) {
this.setStyle(this.childTarget, 'cursor', 'move');
// 将最外层父边框宽度减去
const rightArea = this.target.offsetWidth - this.childTarget.offsetWidth - 2 * getComputedStyle(this.target).borderWidth.replace(/px/, ''); // 右边界
const bottomArea = this.target.offsetHeight - this.childTarget.offsetHeight - 2 * getComputedStyle(this.target).borderWidth.replace(/px/, ''); // 下边界
const leftArea = 0 // 左边界
const topArea = 0 // 上边界
this.rect.left = e.clientX - left > rightArea ? rightArea : (e.clientX - left< leftArea ? leftArea : e.clientX - left);
this.rect.top = e.clientY - top > bottomArea ? bottomArea : (e.clientY - top < topArea ? topArea : e.clientY - top);
this.childTarget.style.left = this.rect.left + 'px';
this.childTarget.style.top = this.rect.top + 'px';
} else if (dragX === 0 && dragY === 0) { // 左上角拉伸
this.rect.left = e.clientX > this.origin.x ? ((e.clientX > (left + width)) ? left + width - this.origin.x : e.clientX - this.origin.x) : 0;
this.rect.top = e.clientY > this.origin.y ? ((e.clientY > (top + height)) ? top + height - this.origin.y : e.clientY - this.origin.y) : 0;
this.rect.width = e.clientX > this.origin.x ? ((e.clientX > (left + width)) ? 0 : width + (left - e.clientX)) : width + (left - this.origin.x);
this.rect.height = e.clientY > this.origin.y ? ((e.clientY > (top + height)) ? 0 : height + (top - e.clientY)) : height + (top - this.origin.y);
this.childTarget.style.left = this.rect.left + 'px';
this.childTarget.style.top = this.rect.top + 'px';
this.childTarget.style.width = this.rect.width + 'px';
this.childTarget.style.height = this.rect.height + 'px';
} else if (dragX === 1 && dragY === 0) { // 中上拉伸
this.rect.top = e.clientY > this.origin.y ? ((e.clientY > (top + height)) ? top + height - this.origin.y : e.clientY - this.origin.y) : 0;
this.rect.height = e.clientY > this.origin.y ? ((e.clientY > (top + height)) ? 0 : height + (top - e.clientY)) : height + (top - this.origin.y);
this.childTarget.style.top = this.rect.top + 'px';
this.childTarget.style.height = this.rect.height + 'px';
} else if (dragX === 2 && dragY === 0) { // 右上角拉伸
this.rect.top = e.clientY > this.origin.y ? ((e.clientY > (top + height)) ? top + height - this.origin.y : e.clientY - this.origin.y) : 0;
this.rect.width = (e.clientX - left + width > this.target.offsetWidth - this.childTarget.offsetLeft ? this.target.offsetWidth - this.childTarget.offsetLeft : e.clientX - left + width);
this.rect.height = e.clientY > this.origin.y ? ((e.clientY > (top + height)) ? 0 : height + (top - e.clientY)) : height + (top - this.origin.y);
this.childTarget.style.top = this.rect.top + 'px';
this.childTarget.style.width = this.rect.width + 'px';
this.childTarget.style.height = this.rect.height + 'px';
} else if (dragX === 2 && dragY === 1) { // 右中拉伸
this.rect.width = (e.clientX - left + width > this.target.offsetWidth - this.childTarget.offsetLeft ? this.target.offsetWidth - this.childTarget.offsetLeft : e.clientX - left + width);
this.childTarget.style.width = this.rect.width + 'px';
}else if (dragX === 2 && dragY === 2) { // 右下角拉伸
this.rect.width = (e.clientX - left + width > this.target.offsetWidth - this.childTarget.offsetLeft ? this.target.offsetWidth - this.childTarget.offsetLeft : e.clientX - left + width);
this.rect.height = (e.clientY- top + height > this.target.offsetHeight - this.childTarget.offsetTop ? this.target.offsetHeight - this.childTarget.offsetTop : e.clientY- top + height);
this.childTarget.style.width = this.rect.width + 'px';
this.childTarget.style.height = this.rect.height + 'px';
} else if (dragX === 1 && dragY === 2) { // 中下拉伸
this.rect.height = (e.clientY- top + height > this.target.offsetHeight - this.childTarget.offsetTop ? this.target.offsetHeight - this.childTarget.offsetTop : e.clientY- top + height);
this.childTarget.style.height = this.rect.height + 'px';
} else if (dragX === 0 && dragY === 2) { // 左下角拉伸
this.rect.left = e.clientX > this.origin.x ? ((e.clientX > (left + width)) ? left + width - this.origin.x : e.clientX - this.origin.x) : 0;
this.rect.width = e.clientX > this.origin.x ? ((e.clientX > (left + width)) ? 0 : width + (left - e.clientX)) : width + (left - this.origin.x);
this.rect.height = (e.clientY- top + height > this.target.offsetHeight - this.childTarget.offsetTop ? this.target.offsetHeight - this.childTarget.offsetTop : e.clientY- top + height);
this.childTarget.style.left = this.rect.left + 'px';
this.childTarget.style.width = this.rect.width + 'px';
this.childTarget.style.height = this.rect.height + 'px';
} else if (dragX === 0 && dragY === 1) { // 左中拉伸
this.rect.left = e.clientX > this.origin.x ? ((e.clientX > (left + width)) ? left + width - this.origin.x : e.clientX - this.origin.x) : 0;
this.rect.width = e.clientX > this.origin.x ? ((e.clientX > (left + width)) ? 0 : width + (left - e.clientX)) : width + (left - this.origin.x);
this.childTarget.style.left = this.rect.left + 'px';
this.childTarget.style.width = this.rect.width + 'px';
}
this.customEvent.trigger('draging', this.getRect());
}
document.onmouseup = e => {
this.customEvent.trigger('dragend', this.getRect());
document.onmousemove = null;
document.onmouseup = null;
this.setStyle(this.childTarget, 'cursor', 'move');
}
}
4、矩形框快捷键及截图原理讲解
注意:当插入有图片,才会进行截图噢~这也是必然的,所以截图功能是本矩形框额外提供的功能!
首选是矩形框快捷键,这个毋庸置疑,监听键盘事件就好了,然后updateRect更新矩形框坐标,
截图呢,就是获取矩形框的xywh,然后调用canvas的drawImage绘制成图片,最后toDataURL('jpeg', 1)导出成图片,当然从canvas到图片有很多种方式,这个可以你自己来选择~
/** 默认事件执行 */
defaultEvent() {
// 右键是佛禁用
if (this.events.contextmenu.disabled) {
this.target.addEventListener('contextmenu', (e) => {
e.preventDefault();
})
}
// 截图功能提供
if (!this.events.clip.disabled) {
document.addEventListener('keydown', (e) => this.screensEvent(e, this))
}
}
/**
* 截图快捷键
* @param {*} e
*/
screensEvent(e, comp) {
// 截图
// 键入键盘ascll码时必须要先加在下面这行代码里,不要忘记了喔
if ((e.altKey || e.ctrlKey) && [81, 83, 87].includes(e.keyCode)) {
e.preventDefault();
let typeCode = ''
if (e.altKey && e.keyCode === 87) typeCode = 0 // 开始截图:alt + w
else if (e.ctrlKey && e.keyCode === 83) typeCode = 1 // 保存当前截图:ctrl + s
else if (e.ctrlKey && e.keyCode === 81) typeCode = 2 // 关闭当前截图:ctrl + q
if (typeCode || typeCode === 0) comp.clipPhoto(typeCode)
}
// 切图
// 键入键盘ascll码时必须要先加在下面这行代码里,不要忘记了喔
if ([37, 38, 39, 40].includes(e.keyCode)) {
// e.preventDefault(); // 这一行放开 就会把键盘原本事件覆盖掉
let typeCode = ''
if (e.keyCode === 37) typeCode = 0 // 键盘向左方向键操作
else if (e.keyCode === 38) typeCode = 1 // 键盘向上方向键操作
else if (e.keyCode === 39) typeCode = 2 // 键盘向右方向键操作
else if (e.keyCode === 40) typeCode = 3 // 键盘向下方向键操作
comp.cutPhoto(typeCode)
}
// if (e.ctrlKey) {
// this.mouseCtrl = true
// }
return false
}
/**
* 截图操作 0 => 开始截图 1 => 保存截图 2 => 退出截图
* @param {*} type
*/
clipPhoto(type) {
switch (type) {
case 0:
this.startClip();
break;
case 1:
this.saveClip();
break;
case 2:
this.closeClip();
break;
}
}
/** 开始截图 */
startClip() {
this.startPaint();
}
/** 保存截图 */
saveClip() {
// 有图片才允许保存截图
if (this.img) {
this.getClip()
}
}
/** 退出截图 */
closeClip() {
this.clearPaint();
}
/**
* 切图快捷键操作
* @param {*} type
*/
cutPhoto(type) {
switch (type) {
case 0:
this.cutFrameMove('left');
break
case 1:
this.cutFrameMove('top');
break
case 2:
this.cutFrameMove('right');
break
case 3:
this.cutFrameMove('bottom');
break
}
}
/**
* 切图框键盘控制移动(增加了移动边界限制,当抵达边界时,则无法再向外侧移动)
* @param {*} type
*/
cutFrameMove(type) {
if (this.childTarget) { // 只有聚焦状态的单框才能被移动, 附属移动边界限制
const leftArea = 0; // 左边界
const topArea = 0; // 上边界
// 将最外层父边框宽度减去
const rightArea = this.target.offsetWidth - this.childTarget.offsetWidth - 2 * getComputedStyle(this.target).borderWidth.replace(/px/, ''); // 右边界
const bottomArea = this.target.offsetHeight - this.childTarget.offsetHeight - 2 * getComputedStyle(this.target).borderWidth.replace(/px/, ''); // 下边界
if (type === 'left') {
this.rect.left = this.rect.left - 5 <= leftArea ? leftArea : this.rect.left - 5;
} else if (type === 'top') {
this.rect.top = this.rect.top - 5 <= topArea ? topArea : this.rect.top - 5;
} else if (type === 'right') {
this.rect.left = Number(this.rect.left) + 5 >= rightArea ? rightArea : Number(this.rect.left) + 5;
} else {
this.rect.top = Number(this.rect.top) + 5 >= bottomArea ? bottomArea : Number(this.rect.top) + 5;
}
this.updateRect();
}
}
/**
* 插入图片
* @param {*} url 图片链接
* @param {*} mode 图片模式
*/
insertImage(url, mode = 'standard') {
const image = new Image()
image.crossOrigin='anonymous'
image.src = url
this.img.target = image
image.onload = (e) => {
if (mode === 'standard') {
this.img.width = image.width;
this.img.height = image.height;
this.img.target.style.width = image.width + 'px'
this.img.target.style.height = image.height + 'px'
} else {
if (Object.prototype.toString.call(mode) === '[object Object]') {
this.img.width = mode.width;
this.img.height = mode.height;
this.img.target.style.width = mode.width + 'px'
this.img.target.style.height = mode.height + 'px'
}
}
}
if (!this.target.querySelector('img')) {
this.target.appendChild(this.img.target)
}
}
/**
* 获取截图canvas, 方便自己转化
* @returns canvas
*/
getClipCanvas() {
const canvas = document.createElement('canvas');
canvas.width = this.getRect().width
canvas.height = this.getRect().height
const context = canvas.getContext('2d');
context.drawImage(
this.img.target, // 规定要使用的图像、画布或视频。
this.getRect().left, this.getRect().top,
this.getRect().width, this.getRect().height,
0, 0,
canvas.width, canvas.height
)
return canvas
}
/**
* 获取截图
*/
getClip() {
const outputClip = this.getClipCanvas().toDataURL('jpeg', 1)
console.log(outputClip);
return outputClip
}
5、问题答疑?
1、在onmousedown里为啥要加上 if (e.target !== dom) return 这行代码呢?
this.target.onmousedown = e => {
if (e.target !== this.target) return;
}
因为我在多次写的时候发现呀,很多时候矩形框的下面就会有类似截图软件下的涂鸦功能,当你点击涂鸦按钮时,其实这个时候你也是点击dom的(可理解为事件穿透),但是我们写逻辑的时候不想要涂鸦按钮和dom一起绑定住想区分开,所以这个时候判断onmousedown的对象和dom是否一致,不一致的话,就不执行后续操作。
2、在onmousemove里为啥要加 e.preventDefault() ... 这段代码呢?
document.onmousemove = e => {
e.preventDefault()
if (e.stopPropagation) {
e.stopPropagation()
} else {
e.cancelable = true
}
}
注意哦,只在绑定对象为document才加这一段代码,其他的不加~
加上这段代码是因为在mousemove来回移动到当前矩形框时,会出现浏览器的黑色拒绝符号并会卡顿,导致操作不流畅,所以加上这段代码可以取消浏览器因回流导致的默认事件及冒泡事件
3、 矩形框周围的黑色半透明蒙层是如何实现的?
box-shadow: 0 0 0 1999px rgba(0, 0, 0, .4);
是通过这行样式实现的喔,其中1999px基本满足大部分屏幕的要求,当然也可以根据你的需求来设置哦,但是一定要切记,外层盒子要设置overflow,不然矩形框的阴影将会溢出去
overflow: hidden;
4、绘制矩形框时的边界范围限制?
其实可以看到,绘制时,矩形框的left和top都是已经确定好的,只是在mousemove时候改变宽高,但是宽高不能超过外层盒子的范围呀,所以就是在你mousemove时,将计算出来的宽高与父级offsetWidth,offsetHeight进行比较(记住为了更好看,我这里是将边框宽度也一起计算进来的喔,如果你不想计算的话,影响也不大~)(内部变量找不到的可以到上面源码里查看全部喔~)
// 宽高边界限制
const widthArea = e.clientX - this.origin.x > this.target.offsetWidth ? this.target.offsetWidth : e.clientX - this.origin.x
const heightArea = e.clientY - this.origin.y > this.target.offsetHeight ? this.target.offsetHeight : e.clientY - this.origin.y
this.childTarget.style.width = widthArea - left + 'px'
this.childTarget.style.height = heightArea - top + 'px'
5、拖动矩形框时的边界范围限制?
这里给出了上下左右四个边界范围限制,当移动left,top超过这个范围时,都将给予处理,使矩形框不会超过外层盒子的边界范围(内部变量找不到的可以到上面源码里查看全部喔~)
const rightArea = this.target.offsetWidth - this.childTarget.offsetWidth - 2 * getComputedStyle(this.target).borderWidth.replace(/px/, ''); // 右边界
const bottomArea = this.target.offsetHeight - this.childTarget.offsetHeight - 2 * getComputedStyle(this.target).borderWidth.replace(/px/, ''); // 下边界
const leftArea = 0 // 左边界
const topArea = 0 // 上边界
this.rect.left = e.clientX - left > rightArea ? rightArea : (e.clientX - left< leftArea ? leftArea : e.clientX - left);
this.rect.top = e.clientY - top > bottomArea ? bottomArea : (e.clientY - top < topArea ? topArea : e.clientY - top);
this.childTarget.style.left = this.rect.left + 'px';
this.childTarget.style.top = this.rect.top + 'px';
6、缩放矩形框时的边界范围限制?
缩放的时候,其实矩形框的left,top,width和height都可能需要改变,不过具体要看你缩放的是哪个边角,下面我代码里是缩放的右下角,所以只需要改变矩形框宽高即可。当宽高的长度等于或超过外层盒子的offsetWidth,offsetHeight减去矩形框的offsetLeft,offsetTop时,就说明已经到范围边界了,这个长度不能再被增加了。(内部变量找不到的可以到上面源码里查看全部喔~)
下述是往右下角方向拉伸噢~
this.rect.width = (e.clientX - left + width > this.target.offsetWidth - this.childTarget.offsetLeft ? this.target.offsetWidth - this.childTarget.offsetLeft : e.clientX - left + width);
this.rect.height = (e.clientY- top + height > this.target.offsetHeight - this.childTarget.offsetTop ? this.target.offsetHeight - this.childTarget.offsetTop : e.clientY- top + height);
this.childTarget.style.width = this.rect.width + 'px';
this.childTarget.style.height = this.rect.height + 'px';
---喜欢的请点赞收藏吧,欢迎评论---