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

时间:2017-07-14 17:32:14

标签: javascript jquery canvas html5-canvas multi-touch

我很欣赏这不是一个严格的代码问题 - 但我还没有达到这一点 - 让我解释一下......

我要求用户能够(如简单的手绘线条)绘制到大图像上 - 并且能够在iPad上进行缩放,平移和缩放。

这让我有点疯狂。我看了很多库,代码示例,产品等,似乎没有什么能满足这个要求,即绘制(一键式)WITH(多点触控)捏,缩放,平移。很多paint.net,签名捕获等,但没有任何支持多点触摸位。

我试图调整各种库来实现我想要的东西(例如将sketch.js的旧版本与hammer.js结合起来),但说实话,我一直在努力。我怀疑在一天结束的时候我必须自己写一些东西,然后使用像hammer.js这样的手势(顺便说一下)来做手势。

无论如何,以防有人在那里遇到了一个可能符合我需要的图书馆,或者可以指出我正确的方向,我会很感激。

随意给我一个难以自行编码的时间; - )

1 个答案:

答案 0 :(得分:1)

自定义触控。

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

您需要通过文档正文中的CSS规则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){
                    m.x = t[j].x;
                    m.y = 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>