gpt4 book ai didi

javascript - 在支持多点触控捏合、平移和缩放的 HTML5 Canvas 上绘图

转载 作者:行者123 更新时间:2023-12-04 01:43:44 28 4
gpt4 key购买 nike

我明白这不是严格意义上的代码问题 - 但我还没有完全做到这一点 - 让我解释一下......

我有一个要求,使用户能够在大图像上绘制(作为简单的徒手画线)- 并且能够缩放、平移和捏合(在 iPad 上)。

这让我有点抓狂。我查看了如此多的库、代码示例、产品等,似乎没有任何东西可以满足此要求,即通过(多点触摸)捏合、缩放、平移(单点触摸)绘图。很多 paint.net、签名捕获等,但没有任何东西支持多点触控位。

我尝试过调整各种库来实现我想要的(例如,将 sketch.js 的旧版本与 hammer.js 结合使用),但老实说,我一直在努力。我确实怀疑我将不得不在一天结束时自己编写并使用像 hammer.js(顺便说一句非常好)之类的东西来做手势。

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

请随意给我一个艰难的时间来避免自己编码 ;-)

最佳答案

自定义触摸。

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

您需要通过文档正文中的 CSS 规则 touch-action: none; 来阻止标准手势,否则它将不起作用。

指针

初始化的指针对象

const pointer = setupPointingDevice(canvas);

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

访问

查看

这是一个位于底部的对象,用于处理 View 。它只是一个二维矩阵,带有一些额外的功能来处理夹点。 view.setPinch(point,point) 以 2 个点作为引用开始捏合。然后 view.movePinch(point,point) 进行更新

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

要设置主 Canvas 变换,请使用 view.apply();

不完美

人类往往很草率,触摸缩放的界面需要稍微延迟绘制,因为捏合 Action 的 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>

关于javascript - 在支持多点触控捏合、平移和缩放的 HTML5 Canvas 上绘图,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45108732/

28 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com