繁体   English   中英

在HTML5 Canvas上绘制,支持多点触控缩放,平移和缩放

[英]Drawing on HTML5 Canvas with support for multitouch pinch, pan, and zoom

我很欣赏这并不是严格的代码问题-但我还没有完全理解-让我解释一下...

我需要使用户能够绘制(作为简单的手绘线)到大图像上,并且能够缩放,平移和捏合(在iPad上)。

这让我有些疯狂。 我看过这么多的库,代码示例,产品等,似乎没有什么可以满足此要求的,即使用(多点触摸)捏,缩放,平移来绘制(一键触摸)。 很多paint.net,签名捕获等,但是没有任何东西支持多点触控位。

我试图改编各种库以实现我想要的功能(例如,将旧版本的sketch.jsHammer.js结合使用),但是说实话,我一直在努力。 我确实怀疑我将不得不在一天结束时编写自己的脚本,并使用Hammer.js(顺便说一句)之类的手势。

无论如何,以防万一有人遇到了一个可能适合我的需求或可以指出正确方向的图书馆,我们将不胜感激。

随意给我一个避免自己编写代码的艰辛;-)

自定义触摸。

该示例显示了使用标准浏览器触摸事件的自定义一键绘制和2点缩放比例,旋转,平移。

您需要通过CSS规则touch-action: none;来阻止标准手势touch-action: none; 放在文档正文上,否则将无法正常工作。

指针

初始化的指针对象

const pointer = setupPointingDevice(canvas);

处理触摸。 使用pointer.count查看有多少触摸,第一个触摸点可以用作pointer.xpointer.y 触摸点数组可以作为指针访问pointer.points[touchNumber]

视图

是底部的用于处理视图的对象。 它只是一个2D矩阵,具有一些其他功能来处理收缩。 view.setPinch(point,point)以2点为基准开始捏合。 然后view.movePinch(point,point)进行更新

该视图用于在显示画布上绘制drawing画布。 要获取世界(绘图坐标),您需要将触摸屏坐标(画布像素)转换为转换后的绘图。 使用view.toWorld(pointer.points[0]); 以获得收缩图形的坐标。

要设置主画布转换,请使用view.apply();

不完美

人类趋于草率,触摸缩放的界面需要稍微延迟绘制时间,因为可能不会立即发生2次捏合触摸。 当检测到单点触摸时,应用程序开始记录绘图点。 如果几帧之后没有第二次触摸,则它将锁定为绘图模式。 没有触摸事件丢失。

如果第二触摸发生在第一触摸的几帧之内,则假定正在使用捏合动作。 该应用程序转储以前的所有绘图点,并将模式设置为收缩。

当应用程序处于绘制或捏合模式时,它们将被锁定,直到未检测到触摸为止。 这是为了防止由于草率接触而引起的不良行为。

演示版

该演示仅作为示例。

注意此功能不适用于非触摸设备。 我抛出错误是找不到触摸。

注意我仅完成了最基本的代理检测。 Android,iPhone,iPad以及任何报告多点触控的东西。

注意捏事件通常会导致两点拖到一处。 本示例无法正确处理此类事件。 当捏合手势变为单一触摸并旋转和缩放时,您应该切换到平移模式。

  const U = undefined; const doFor = (count, callback) => {var i = 0; while (i < count && callback(i ++) !== true ); }; const drawModeDelay = 8; // number of frames to delay drawing just incase the pinch touch is // slow on the second finger const worldPoint = {x : 0, y : 0}; // worldf point is in the coordinates system of the drawing const ctx = canvas.getContext("2d"); var drawMode = false; // true while drawing var pinchMode = false; // true while pinching var startup = true; // will call init when true // the drawing image const drawing = document.createElement("canvas"); const W = drawing.width = 512; const H = drawing.height = 512; const dCtx = drawing.getContext("2d"); dCtx.fillStyle = "white"; dCtx.fillRect(0,0,W,H); // pointer is the interface to the touch const pointer = setupPointingDevice(canvas); ctx.font = "16px arial."; if(pointer === undefined){ ctx.font = "16px arial."; ctx.fillText("Did not detect pointing device. Demo terminated.", 20,20); throw new Error("App Error : No touch found"); } // drawing functions and data const drawnPoints = []; // array of draw points function drawOnDrawing(){ // draw all points on drawingPoint array dCtx.fillStyle = "black"; while(drawnPoints.length > 0){ const point = drawnPoints.shift(); dCtx.beginPath(); dCtx.arc(point.x,point.y,8,0,Math.PI * 2); dCtx.fill(); dCtx.stroke(); } } // called once at start function init(){ startup = false; view.setContext(ctx); } // standard vars var w = canvas.width; var h = canvas.height; var cw = w / 2; // center var ch = h / 2; var globalTime; // main update function function update(timer){ if(startup){ init() }; globalTime = timer; ctx.setTransform(1,0,0,1,0,0); // reset transform ctx.globalAlpha = 1; // reset alpha ctx.globalCompositeOperation = "source-over"; if(w !== innerWidth || h !== innerHeight){ cw = (w = canvas.width = innerWidth) / 2; ch = (h = canvas.height = innerHeight) / 2; } // clear main canvas and draw the draw image with shadows and make it look nice ctx.clearRect(0,0,w,h); view.apply(); ctx.fillStyle = "black"; ctx.globalAlpha = 0.4; ctx.fillRect(5,H,W-5,5) ctx.fillRect(W,5,5,H); ctx.globalAlpha = 1; ctx.drawImage(drawing,0,0); ctx.setTransform(1,0,0,1,0,0); // handle touch. // If single point then draw if((pointer.count === 1 || drawMode) && ! pinchMode){ if(pointer.count === 0){ drawMode = false; drawOnDrawing(); }else{ view.toWorld(pointer,worldPoint); drawnPoints.push({x : worldPoint.x, y : worldPoint.y}) if(drawMode){ drawOnDrawing(); }else if(drawnPoints.length > drawModeDelay){ drawMode = true; } } // if two point then pinch. }else if(pointer.count === 2 || pinchMode){ drawnPoints.length = 0; // dump any draw points if(pointer.count === 0){ pinchMode = false; }else if(!pinchMode && pointer.count === 2){ pinchMode = true; view.setPinch(pointer.points[0],pointer.points[1]); }else{ view.movePinch(pointer.points[0],pointer.points[1]); } }else{ pinchMode = false; drawMode = false; } requestAnimationFrame(update); } requestAnimationFrame(update); function touch(element){ const touch = { points : [], x : 0, y : 0, //isTouch : true, // use to determine the IO type. count : 0, w : 0, rx : 0, ry : 0, } var m = touch; var t = touch.points; function newTouch () { for(var j = 0; j < m.pCount; j ++) { if (t[j].id === -1) { return t[j] } } } function getTouch(id) { for(var j = 0; j < m.pCount; j ++) { if (t[j].id === id) { return t[j] } } } function setTouch(touchPoint,point,start,down){ if(touchPoint === undefined){ return } if(start) { touchPoint.oy = point.pageX; touchPoint.ox = point.pageY; touchPoint.id = point.identifier; } else { touchPoint.ox = touchPoint.x; touchPoint.oy = touchPoint.y; } touchPoint.x = point.pageX; touchPoint.y = point.pageY; touchPoint.down = down; if(!down) { touchPoint.id = -1 } } function mouseEmulator(){ var tCount = 0; for(var j = 0; j < m.pCount; j ++){ if(t[j].id !== -1){ if(tCount === 0){ mx = t[j].x; my = t[j].y; } tCount += 1; } } m.count= tCount; } function touchEvent(e){ var i, p; p = e.changedTouches; if (e.type === "touchstart") { for (i = 0; i < p.length; i ++) { setTouch(newTouch(), p[i], true, true) } } else if (e.type === "touchmove") { for (i = 0; i < p.length; i ++) { setTouch(getTouch(p[i].identifier), p[i], false, true) } } else if (e.type === "touchend") { for (i = 0; i < p.length; i ++) { setTouch(getTouch(p[i].identifier), p[i], false, false) } } mouseEmulator(); e.preventDefault(); return false; } touch.pCount = navigator.maxTouchPoints; element = element === undefined ? document : element; doFor(navigator.maxTouchPoints, () => touch.points.push({x : 0, y : 0, dx : 0, dy : 0, down : false, id : -1})); ["touchstart","touchmove","touchend"].forEach(name => element.addEventListener(name, touchEvent) ); return touch; } function setupPointingDevice(element){ if(navigator.maxTouchPoints === undefined){ if(navigator.appVersion.indexOf("Android") > -1 || navigator.appVersion.indexOf("iPhone") > -1 || navigator.appVersion.indexOf("iPad") > -1 ){ navigator.maxTouchPoints = 5; } } if(navigator.maxTouchPoints > 0){ return touch(element); }else{ //return mouse(); // does not take an element defaults to the page. } } const view = (()=>{ const matrix = [1,0,0,1,0,0]; // current view transform const invMatrix = [1,0,0,1,0,0]; // current inverse view transform var m = matrix; // alias var im = invMatrix; // alias var scale = 1; // current scale var rotate = 0; var maxScale = 1; const pinch1 = {x :0, y : 0}; // holds the pinch origin used to pan zoom and rotate with two touch points const pinch1R = {x :0, y : 0}; var pinchDist = 0; var pinchScale = 1; var pinchAngle = 0; var pinchStartAngle = 0; const workPoint1 = {x :0, y : 0}; const workPoint2 = {x :0, y : 0}; const wp1 = workPoint1; // alias const wp2 = workPoint2; // alias var ctx; const pos = {x : 0,y : 0}; // current position of origin var dirty = true; const API = { canvasDefault () { ctx.setTransform(1, 0, 0, 1, 0, 0) }, apply(){ if(dirty){ this.update() } ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]) }, reset() { scale = 1; rotate = 0; pos.x = 0; pos.y = 0; dirty = true; }, matrix, invMatrix, update () { dirty = false; m[3] = m[0] = Math.cos(rotate) * scale; m[2] = -(m[1] = Math.sin(rotate) * scale); m[4] = pos.x; m[5] = pos.y; this.invScale = 1 / scale; var cross = m[0] * m[3] - m[1] * m[2]; im[0] = m[3] / cross; im[1] = -m[1] / cross; im[2] = -m[2] / cross; im[3] = m[0] / cross; }, toWorld (from,point = {}) { // convert screen to world coords var xx, yy; if (dirty) { this.update() } xx = from.x - m[4]; yy = from.y - m[5]; point.x = xx * im[0] + yy * im[2]; point.y = xx * im[1] + yy * im[3]; return point; }, toScreen (from,point = {}) { // convert world coords to screen coords if (dirty) { this.update() } point.x = from.x * m[0] + from.y * m[2] + m[4]; point.y = from.x * m[1] + from.y * m[3] + m[5]; return point; }, setPinch(p1,p2){ // for pinch zoom rotate pan set start of pinch screen coords if (dirty) { this.update() } pinch1.x = p1.x; pinch1.y = p1.y; var x = (p2.x - pinch1.x); var y = (p2.y - pinch1.y); pinchDist = Math.sqrt(x * x + y * y); pinchStartAngle = Math.atan2(y, x); pinchScale = scale; pinchAngle = rotate; this.toWorld(pinch1, pinch1R) }, movePinch(p1,p2,dontRotate){ if (dirty) { this.update() } var x = (p2.x - p1.x); var y = (p2.y - p1.y); var pDist = Math.sqrt(x * x + y * y); scale = pinchScale * (pDist / pinchDist); if(!dontRotate){ var ang = Math.atan2(y, x); rotate = pinchAngle + (ang - pinchStartAngle); } this.update(); pos.x = p1.x - pinch1R.x * m[0] - pinch1R.y * m[2]; pos.y = p1.y - pinch1R.x * m[1] - pinch1R.y * m[3]; dirty = true; }, setContext (context) {ctx = context; dirty = true }, }; return API; })(); 
 canvas { position : absolute; top : 0px; left : 0px; z-index: 2; } body { background:#bbb; touch-action: none; } 
 <canvas id="canvas"></canvas> 

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM