js仿QQ拖拽刪除
原生js實現仿QQ拖拽刪除交互,無需任何依賴。
項目演示請看這裏,
gitHub請移步這裏。
由於源碼很長,所以貼到最下面了。
效果截圖如下:
核心思想呢,就是點擊圓點的時候全屏覆蓋個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個點的計算,需要數學方面的知識,原理如下圖所示 (圖片來源於網絡,懶得畫了,就這個意思反正):
剩下的就是函數的封裝及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: 1;} .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‘ })
(完)
js仿QQ拖拽刪除