js仿QQ拖拽删除

时间:2023-03-09 03:53:10
js仿QQ拖拽删除

原生js实现仿QQ拖拽删除交互,无需任何依赖。

项目演示请看这里

gitHub请移步这里

由于源码很长,所以贴到最下面了。

效果截图如下:

js仿QQ拖拽删除js仿QQ拖拽删除js仿QQ拖拽删除

核心思想呢,就是点击圆点的时候全屏覆盖个canvas,在canvas上画出想要的效果。

原理:

1.点击圆点,生成全屏canvas,画出两个圆 , 在原地画圆,根据拉动距离变小, 在手指触控的位置,跟随手指画圆。

2.计算两圆位置,计算两圆的切点位置,通过两圆切断位置画贝塞尔曲线填充,模拟粘性。

3.判断距离是否超出最大距离,做出对应动作。

看似简单,但是做起来会有各种问题,

1. 移动端时, 对小球发生的touchstart事件,该事件对应的touchmove对象只能是该球,所以就会出现,touchmove时的事件处理。

2. 当touchmove的时候,我们的意愿是不滑动页面,只拖动小球,这就涉及到touchmove的事件阻止问题, 由于第一点原因,将touchmove事件指向了最外层元素,但是此时,如果是给 <body>的话,我们是没法阻止页面滚动的,详情参见google对此事件的说明。

3. 如何回收使用过的canvas,这里我们是每次声明一个canvas的时候都会给一个随机ID,来通过这个随机id回收,这样既保证了id不会重名,又保证了回收的顺利进行。

4. 当手指离开的时候,播放小球销毁动画,该动画会有持续时间,如果该动画在全屏canvas上做的话,就会在touchend后的 0.8s (销毁动画的持续时间) 内对屏幕操作是无效的,被canvas阻止, 所以此刻我们选择在销毁处,单独生成个小点的canvas(具体多大取决于你想让销毁动画蔓延多大)来执行销毁动画。

5. 画贝塞尔曲线填充,仿粘性 , 这里需要计算两个圆的共4个切点,通过四个切点,及中心点,来话二次贝塞尔曲线填充, 这4个点的计算,需要数学方面的知识,原理如下图所示 (图片来源于网络,懒得画了,就这个意思反正):

js仿QQ拖拽删除

剩下的就是函数的封装及canvas的使用了,如有疑问可以看看源码,并不复杂, 如有写的不对的地方欢迎批评指正。

<div id="body">
<div class="drag">
<div class="div1 dragdom" data-target="1">1</div>
</div>
<div class="drag">
<div class="div1 dragdom" data-target="2">36</div>
</div>
<div class="drag">
<div class="div1 dragdom" data-target="3">7</div>
</div>
<div class="drag">
<div class="div1 dragdom" data-target="4">15</div>
</div>
<div class="drag">
<div class="div1 dragdom" data-target="5">9</div>
</div>
<div class="drag">
<div class="div1 dragdom" data-target="6">14</div>
</div>
</div>
#body{overflow: scroll;height: 100vh;}
.drag{position: relative;padding:10px 30px;z-index:;}
.drag div{
width: 40px;
height: 40px;
color: #FFF;
text-align: center;
font-size: 20px;
line-height: 40px;
border-radius: 50%;
background-color: red;
}
.dragdom{
position: relative;
}
CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
// x: 起点X坐标, y: 起点Y坐标 w: 宽度, h: 高度: r: 圆角弧度,
if (w < 2 * r) {r = w / 2;}
if (h < 2 * r){ r = h / 2;}
this.beginPath();
this.moveTo(x+r, y);
this.arcTo(x+w, y, x+w, y+h, r);
this.arcTo(x+w, y+h, x, y+h, r);
this.arcTo(x, y+h, x, y, r);
this.arcTo(x, y, x+w, y, r);
this.closePath();
return this;
}
function Drag (params) {
// {
// dragId: dragId
// max: max,
// fillColor: ''
//}
var D = this;
this.drag = params.drag;
this.max = params.max || 70;
this.texts = this.drag.innerHTML;
this.domBody = document.getElementById('body');
this.device = /android|iphone|ipad|ipod|webos|iemobile|opear mini|linux/i.test(navigator.userAgent.toLowerCase()); this.eventName = {
start: this.device ? 'touchstart' : 'mousedown',
move: this.device ? 'touchmove' : 'mousemove',
end: this.device ? 'touchend' : 'mouseup',
} this.onDragStart = function () {
this.drag.style.visibility = 'hidden'
}
this.draged = false;
this.onDragEnd = function (d) {
if(!d) {
D.drag.style.visibility = 'visible'
}
}
this.onBeforeDelate = function () {}
this.onDelated = function () {}
this._r = D.drag.offsetWidth / 2;
this.point = {
r: D._r,
x: D.offset(D.drag).left + D._r,
y: D.offset(D.drag).top + D._r,
w: D.drag.offsetWidth,
h: D.drag.offsetHeight
}
this.current = {
x: 0,
y: 0,
r: D.point.r,
direction: 0,
canDelate: false,
fillColor: 'red',
coefficient: 0.3
}
this.fullCanvas = {
canvas: '',
ctx: '',
width: document.documentElement.clientWidth || document.documentElement.clientWidth,
height: document.documentElement.clientHeight || document.documentElement.clientHeight,
id: ''
} this.startEvent = function (){
var e = event;
D.point.x = D.offset(D.drag).left + D._r;
D.point.y = D.offset(D.drag).top + D._r ;
console.log(D.offset(D.drag).top)
var width = D.fullCanvas.width;
var height = D.fullCanvas.height;
var cssObj = {
'position': 'fixed',
'left': '0',
'top': '0',
'zIndex': '99'
}
var convasObj = D.createCanvas(width, height, cssObj);
D.domBody.appendChild(convasObj.canvas)
D.fullCanvas.canvas = document.getElementById(convasObj.id);
D.fullCanvas.ctx = D.fullCanvas.canvas.getContext('2d');
if(D.device) {
D.domBody.addEventListener(D.eventName.move, D.moveEvent)
D.drag.addEventListener(D.eventName.end, D.endEvent)
} else {
D.fullCanvas.canvas.addEventListener(D.eventName.move, D.moveEvent)
D.fullCanvas.canvas.addEventListener(D.eventName.end, D.endEvent)
}
}
this.moveEvent = function () {
var e = event;
e.preventDefault();
D.current.x = D.device ? e.touches[0].clientX : e.clientX;
D.current.y = D.device ? e.touches[0].clientY : e.clientY;
D.currentDirection = D.drawCercle(D.fullCanvas.ctx, D.fullCanvas.width, D.fullCanvas.height, D.current.fillColor, D.point.x, D.point.y, D.point.r, D.current.x, D.current.y, D.current.r, D.max)
if(!D.draged) {
D.draged = true;
D.onDragStart(D.drag)
}
}
this.endEvent = function (e) {
console.log(e.target)
if(D.device) {
D.drag.removeEventListener(D.eventName.move, D.moveEvent);
D.domBody.removeEventListener(D.eventName.move, D.moveEvent)
} else {
D.fullCanvas.canvas.removeEventListener(D.eventName.move, D.moveEvent);
}
D.draged = false;
if(D.currentDirection > D.max) {
isDelate = true;
D.current.canDelate = false;
D.disappear({
x: D.current.x,
y: D.current.y,
r: D.current.r
});
D.domBody.removeChild(D.fullCanvas.canvas);
} else {
D.bounce(D.fullCanvas.ctx, D.fullCanvas.width, D.fullCanvas.height, D.current.fillColor, D.point.x, D.point.y, D.point.r, D.current.x, D.current.y, D.current.r, D.max)
}
} this.drag.addEventListener(D.eventName.start, D.startEvent) }
Drag.prototype = {
offset: function (el) {
var rect, win,elem = el;
var rect = elem.getBoundingClientRect();
var win = elem.ownerDocument.defaultView;
return {
top: rect.top + win.pageYOffset,
left: rect.left + win.pageXOffset
};
},
tween: {
/*
* t: current time(当前时间);
* b: beginning value(初始值);
* c: change in value(变化量);
* d: duration(持续时间)。
*/
easeOut: function(t, b, c, d) {
return -c *(t /= d)*(t-2) + b;
}
},
getPoints: function (startX, startY, startR, endX, endY, endR, maxDirection) {
var D = this;
// 计算两圆距离
var currentDirection = Math.sqrt( (endX-startX) * (endX-startX) + (endY -startY) * (endY -startY) ) // 计算起始圆 半径
var m = (maxDirection - currentDirection) / maxDirection; var startR = startR * 0.3 + startR * 0.7 * (m > 0 ? m : 0);
// 计算起始圆切点坐标
var filletB = (endX - startX) / currentDirection ;
var filletA = (endY - startY) / currentDirection; var startPoint = {
center: {
x: startX,
y: startY,
r: startR
},
a: {
x: startX + filletA * startR,
y: startY - filletB * startR
},
b: {
x: startX - filletA * startR,
y: startY + filletB * startR
}
}
// 计算结束圆切点坐标 var endPoint = {
center: {
x: endX,
y: endY,
r: endR
},
a: {
x: endX + filletA * endR,
y: endY - filletB * endR
},
b: {
x: endX - filletA * endR,
y: endY + filletB * endR
}
}
if(Math.abs(currentDirection) > D.max) {
D.current.canDelate = true;
}
if(D.current.canDelate) {
startPoint = endPoint;
} var ctrolPoint = {
x: startX + (endX-startX)*0.5,
y: startY + (endY-startY)*0.5,
} return {
startPoint: startPoint,
endPoint: endPoint,
ctrolPoint: ctrolPoint,
currentDirection: currentDirection
}
} ,
drawCercle: function (ctx, ctxWidth, ctxHeight, fillColor,startX, startY, startR, endX, endY, endR, maxDirection) {
// param: ctx, ctxWidth, ctxHeight, fillColor,startX, startY, startR, endX, endY, endR, maxDirection
var D = this;
var points = D.getPoints(startX, startY, startR, endX, endY, endR, maxDirection);
var startPoint = points.startPoint;
var endPoint = points.endPoint; var ctrolPoint = points.ctrolPoint;
// 画起始圆
ctx.clearRect(0,0, ctxWidth, ctxHeight);
ctx.fillStyle = D.current.fillColor;
ctx.beginPath();
ctx.arc(startPoint.center.x,startPoint.center.y,startPoint.center.r,0 , 2*Math.PI);
ctx.closePath();
ctx.fill();
ctx.closePath();
// 画结束圆
ctx.beginPath();
ctx.arc(endPoint.center.x,endPoint.center.y,endPoint.center.r,0, 2*Math.PI);
ctx.closePath();
ctx.fill();
ctx.closePath();
// 画贝塞尔曲线填充
ctx.beginPath();
ctx.moveTo(startPoint.a.x, startPoint.a.y);
ctx.quadraticCurveTo(ctrolPoint.x, ctrolPoint.y, endPoint.a.x, endPoint.a.y);
ctx.lineTo(endPoint.b.x, endPoint.b.y);
ctx.quadraticCurveTo(ctrolPoint.x, ctrolPoint.y, startPoint.b.x, startPoint.b.y);
ctx.lineTo(startPoint.a.x, startPoint.a.y);
ctx.closePath();
ctx.fill();
// 画文字
ctx.save()
ctx.textBaseline = 'middle';
ctx.font="20px Arial";
ctx.fillStyle = '#FFF';
ctx.textAlign='center'
ctx.fillText(D.texts,endPoint.center.x, endPoint.center.y,D.point.w);
ctx.restore() return points.currentDirection
},
bounce: function (ctx, ctxWidth, ctxHeight, fillColor,startX, startY, startR, endX, endY, endR, maxDirection) {
var D = this;
// 只需要计算随鼠标动的小球的位置 给它做bounce运动就行了 var p = [{},{ // end
x: endX,
y: endY
},{ // bef
x: startX - (endX - startX)/2,
y: startY - (endY - startY)/2
},{ // start
x: startX,
y: startY
},
]
var i = 0
// 弹到圆心
if(!D.current.canDelate) {
var timer = setInterval(function (){
i++
D.drawCercle(ctx, ctxWidth, ctxHeight, fillColor,startX, startY, startR, p[i].x, p[i].y, endR, maxDirection);
if(i>= p.length-1) {
clearInterval(timer)
D.current.canDelate = false
D.domBody.removeChild(D.fullCanvas.canvas)
D.onDragEnd(false)
}
},80)
} else {
D.drawCercle(ctx, ctxWidth, ctxHeight, fillColor,startX, startY, startR, p[p.length-1].x, p[p.length-1].y, endR, maxDirection);
D.domBody.removeChild(D.fullCanvas.canvas)
D.current.canDelate = false
D.onDragEnd(false)
}
},
createCanvas: function (width, height, cssObj) {
console.log('createCanvas')
var id = 'canvas' + Math.round(Math.random() * 100000);
var canvas = document.createElement('canvas');
canvas.setAttribute('id', id);
canvas.setAttribute('width', width);
canvas.setAttribute('height', height);
for(var item in cssObj) {
canvas.style[item] = cssObj[item];
}
return {canvas: canvas, id: id};
},
disappear: function (pos) {
// pos = {x: x, y: y} 消失点的坐标
var D = this;
D.onDragEnd(true)
var screenWidth = document.documentElement.clientWidth || document.body.clientWidth;
var timer = null;
var width, height, i, PI = Math.PI;
width = height = D.current.r * 5; var cssObj = {
'position': 'fixed',
'left': pos.x - width / 2 + 'px',
'top': pos.y - height / 2 + 'px'
}
var canvasObj = this.createCanvas(width, height, cssObj);
console.log(canvasObj)
document.getElementsByTagName('body')[0].appendChild(canvasObj.canvas)
var canvas = document.getElementById(canvasObj.id);
var ctx = canvas.getContext('2d');
var dots = [];
var dotsLength = Math.round(Math.random() * 3 + 5);
var currentStep = 0 ,allSteps = 20;
for (i = 0 ; i < dotsLength; i ++) {
var r, x, y, a;
r = D.current.r;
x = Math.round(Math.random() * (width - r * 2)) + r;
y = Math.round(Math.random() * (height - r * 2)) + r;
a = r / allSteps;
var o = {
r: r,
x: x,
y: y,
a: a
}
dots.push(o);
} function disappear(currentStep ,allSteps) {
ctx.clearRect(0, 0, width,height);
// ctx.fillStyle = D.current.fillColor;
ctx.fillStyle = '#D4D4D4'; for(i = 0; i < dots.length; i ++) {
ctx.beginPath();
ctx.arc(
D.tween.easeOut(currentStep, width/2, dots[i].x - width/2, allSteps),
D.tween.easeOut(currentStep, height/2, dots[i].y - height/2, allSteps),
D.tween.easeOut(currentStep, dots[i].r, -dots[i].r , allSteps),
0, 2*PI);
ctx.closePath();
ctx.fill();
}
}
disappear(currentStep ,allSteps)
timer = setInterval(function (){
currentStep ++ ;
disappear(currentStep ,allSteps)
if(currentStep >= allSteps) {
clearInterval(timer)
console.log(canvas)
document.getElementsByTagName('body')[0].removeChild(canvas)
D.onDelated()
}
}, 40)
}
} function MoveDrag (params) {
// params = {
// doms: '', // 元素,可以是类名或者是id或者是直接获取到的元素// 本期只支持类名
// max: 70m
// fillColor: ''
// }
var M = this;
this.defaults = {
doms: '',
max: 70,
fillcolor: ''
}
this.defaults = Object.assign(this.defaults, params) var d = this.defaults.doms; if(typeof(d) == 'string') {
if(/^\./.test(d)) {
// 类名
this.defaults.doms = document.getElementsByClassName(d.replace(/^\./,''));
} else if (/^#/.test(d)){
// id名
var _dm = document.getElementById(d)
if(_dm) {
this.defaults.doms = [document.getElementById(d.replace(/^#/,''))];
} else {
this.defaults.doms = [];
}
} else {
// 标签名
this.defaults.doms = document.getElementsByTagName(d);
}
} else {
// 不知道是啥玩意儿
}
var obj = []; for (var i = 0; i < this.defaults.doms.length; i ++) {
var o = new Drag({
drag: M.defaults.doms[i],
max: M.defaults.max,
fillColor: M.defaults.fillColor
})
obj.push(o)
}
return obj; }
new MoveDrag({
doms: '.dragdom',
max: 90,
fillcolor: 'red'
})

(完)