作为对此question and answer的跟进......我还有另外一个问题需要解决:
当我在画布上绘制然后应用一些变换(如旋转)时,我想保留绘制的内容并继续绘制。
要测试此项,请使用鼠标绘制内容,然后单击“旋转”。
这就是我正在尝试的,但画布会被删除。
JS
//main variables
canvas = document.createElement("canvas");
canvas.width = 500;
canvas.height = 300;
canvas.ctx = canvas.getContext("2d");
ctx = canvas.ctx;
canvas_aux = document.createElement("canvas");
canvas_aux.width = 500;
canvas_aux.height = 300;
canvas_aux.ctx = canvas.getContext("2d");
ctx_aux = canvas_aux.ctx;
function rotate()
{
ctx_aux.drawImage(canvas, 0, 0); //new line: save current drawing
timer += timerStep;
var cw = canvas.width / 2;
var ch = canvas.height / 2;
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset the transform so we can clear
ctx.clearRect(0, 0, canvas.width, canvas.height); // clear the canvas
createMatrix(cw, ch -50, scale, timer);
var m = matrix;
ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
//draw();
ctx.drawImage(canvas_aux, 0, 0); //new line: repaint current drawing
if(timer <= rotation )
{
requestAnimationFrame(rotate);
}
}
DEMO(链接问题/答案中原件的更新版本)
答案 0 :(得分:6)
您有几个选项取决于要求是什么。
屏幕外缓冲区用于保存渲染线条。渲染到屏幕外缓冲区,然后将缓冲区绘制到显示画布。这是最快的方法,但你正在使用像素,因此如果你缩放你会得到像素伪像,它将限制绘图区域的大小(仍然很大但不是伪无限)并严重限制你可以提供的undos数量内存限制
绘制缓冲路径,基本记录鼠标移动和点击,然后在每次更新显示时重新渲染所有可见路径。这将让您在没有像素伪影的情况下进行缩放和旋转,为您提供任意大小的绘制区域(在64位双精度范围内)和奖励撤消一直回到第一行。这种方法的问题在于它很快变得非常慢(尽管你可以通过webGL提高渲染速度)
以上两种方法的组合。在绘制路径时记录路径,但也将它们渲染到屏幕外的画布。使用屏幕外画布更新显示并保持较高的刷新率。您只需在需要时重新渲染屏幕外画布,即撤消或缩放时,平移或旋转时无需重新渲染。
我不会做一个完整的绘图包,所以这只是一个使用屏幕外缓冲区来保存可见路径的示例。绘制的所有路径都记录在路径数组中。当用户更改视图,平移,缩放,旋转时,路径将重新绘制到屏幕外画布以匹配新视图。
有一些样板来处理可以忽略的设置和鼠标。由于代码很多而且时间很短,因此评论很短,你必须从中挑选出你需要的东西。
路径有paths
个对象。 view
包含转换和相关函数。一些平移,缩放,旋转功能。还有一个显示功能,可以呈现和处理所有鼠标和用户IO。通过按住鼠标修改器ctrl,alt,shift
var drawing = createImage(100,100); // offscreen canvas for drawing paths
// the onResize is a callback used by the boilerplate code at the bottom of this snippet
// it is called whenever the display size has changed (including starting app). It is
// debounced by 100ms to prevent needless calls
var onResize = function(){
drawing.width = canvas.width;
drawing.height = canvas.height;
redrawBuffers = true; // flag that drawing buffers need redrawing
ctx.font = "18px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
view.pos.x = cw; // set origin at center of screen
view.pos.y = ch;
view.update();
}
const paths = []; // array of all recorded paths
const path = { // descriptor of a path object
addPoint(x,y){ // adds a point to the path
this.points.push({x,y});
},
draw(ctx){ // draws this path on context ctx
var i = 0;
ctx.beginPath();
ctx.moveTo(this.points[i].x,this.points[i++].y);
while(i < this.points.length){
ctx.lineTo(this.points[i].x,this.points[i++].y);
}
ctx.stroke();
}
}
// creates a new path and adds it to the array of paths.
// returns the new path
function addPath(){
var newPath;
newPath = Object.assign({points : []},path);
paths.push(newPath)
return newPath;
}
// draws all recorded paths onto context cts using the current view
function drawAll(ctx){
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,w,h);
var m = view.matrix;
ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
var i = 0;
for(i = 0; i < paths.length; i ++){
paths[i].draw(ctx);
}
}
// this controls the view
const view = {
matrix : [1,0,0,1,0,0], // current view transform
invMatrix : [1,0,0,1,0,0], // current inverse view transform
rotate : 0, // current x axis direction in radians
scale : 1, // current scale
pos : { // current position of origin
x : 0,
y : 0,
},
update(){ // call to update transforms
var xdx = Math.cos(this.rotate) * this.scale;
var xdy = Math.sin(this.rotate) * this.scale;
var m = this.matrix;
var im = this.invMatrix;
m[0] = xdx;
m[1] = xdy;
m[2] = -xdy;
m[3] = xdx;
m[4] = this.pos.x;
m[5] = this.pos.y;
// calculate the inverse transformation
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;
},
mouseToWorld(){ // conver screen to world coords
var xx, yy, m;
m = this.invMatrix;
xx = mouse.x - this.matrix[4];
yy = mouse.y - this.matrix[5];
mouse.xr = xx * m[0] + yy * m[2];
mouse.yr = xx * m[1] + yy * m[3];
},
toWorld(x,y,point = {}){ // convert screen to world coords
var xx, yy, m;
m = this.invMatrix;
xx = x - this.matrix[4];
yy = y - this.matrix[5];
point.x = xx * m[0] + yy * m[2];
point.y = xx * m[1] + yy * m[3];
return point;
},
toScreen(x,y,point = {}){ // convert world coords to coords
var m;
m = this.matrix;
point.x = x * m[0] + y * m[2] + m[4];
point.y = x * m[1] + y * m[3] + m[5];
return point;
},
clickOrigin : { // used to hold coords to deal with pan zoom and rotate
x : 0,
y : 0,
scale : 1,
},
dragging : false, // true is dragging
startDrag(){ // called to start a Orientation UI input such as rotate, pan and scale
if(!view.dragging){
view.dragging = true;
view.clickOrigin.x = mouse.xr;
view.clickOrigin.y = mouse.yr;
view.clickOrigin.screenX = mouse.x;
view.clickOrigin.screenY = mouse.y;
view.clickOrigin.scale = view.scale;
}
}
}
// functions to do pan zoom and scale
function panView(){ // pans the view
view.startDrag(); // set origins as referance point
view.pos.x -= (view.clickOrigin.screenX - mouse.x);
view.pos.y -= (view.clickOrigin.screenY - mouse.y);
view.update();
view.mouseToWorld(); // get the new mouse pos
view.clickOrigin.screenX = mouse.x; // save the new mouse coords
view.clickOrigin.screenY = mouse.y;
}
// scales the view
function scaleView(){
view.startDrag();
var y = view.clickOrigin.screenY - mouse.y;
if(y !== 0){
view.scale = view.clickOrigin.scale + (y/ch);
view.update();
}
}
// rotates the view by setting the x axis direction
function rotateView(){
view.startDrag();
workingCoord = view.toScreen(0,0,workingCoord); // get location of origin
var x = workingCoord.x - mouse.x;
var y = workingCoord.y - mouse.y;
var dist = Math.sqrt(x * x + y * y);
if(dist > 2 / view.scale){
view.rotate = Math.atan2(-y,-x);
view.update();
}
}
var currentPath; // Holds the currently drawn path
var redrawBuffers = false; // if true this indicates that all paths need to be redrawn
var workingCoord; // var to use as a coordinate
// main loop function called from requestAnimationFrame callback in boilerplate code
function display() {
var showTransform = false; // flags that view is being changed
// clear the canvas and set defaults
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
view.mouseToWorld(); // get the mouse world coords
// get the transform matrix
var m = view.matrix;
// show feedback
if(mouse.shift || mouse.alt || mouse.ctrl){
if(mouse.shift){
ctx.fillText("Click drag to pan",cw, 20);
}else if(mouse.ctrl){
ctx.fillText("Click drag to rotate",cw, 20);
}else{
ctx.fillText("Click drag to scale : " + view.scale.toFixed(4),cw, 20);
}
}else{
ctx.fillText("Click drag to draw.",cw, 20);
ctx.fillText("Hold [shift], [ctrl], or [alt] and use mouse to pan, rotate, scale",cw, 40);
}
if(mouse.buttonRaw === 1){ // when mouse is down
if(mouse.shift || mouse.alt || mouse.ctrl){ // pan zoom rotate
if(mouse.shift){
panView();
}else if(mouse.ctrl){
rotateView();
}else{
scaleView();
}
m = view.matrix;
showTransform = true;
redrawBuffers = true;
}else{ // or add a path
if(currentPath === undefined){
currentPath = addPath();
}
currentPath.addPoint(mouse.xr,mouse.yr)
}
}else{
// if there is a path then draw it onto the offscreen canvas and
// reset the path to undefined
if(currentPath !== undefined){
currentPath.draw(drawing.ctx);
currentPath = undefined;
}
view.dragging = false; // incase there is a pan/zoom/scale happening turn it off
}
if(showTransform){ // redraw all paths when pan rotate or zoom
redrawBuffers = false;
drawAll(drawing.ctx);
ctx.drawImage(drawing,0,0);
}else{ // draws the sceen when normal drawing mode.
if(redrawBuffers){
redrawBuffers = false;
drawAll(drawing.ctx);
}
ctx.drawImage(drawing,0,0);
ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
drawing.ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
// draw a cross hair.
if(mouse.buttonRaw === 0){
var invScale = 1 / view.scale; // get inverted scale
ctx.beginPath();
ctx.moveTo(mouse.xr - 10 * invScale,mouse.yr);
ctx.lineTo(mouse.xr + 10 * invScale,mouse.yr);
ctx.moveTo(mouse.xr ,mouse.yr - 10 * invScale);
ctx.lineTo(mouse.xr ,mouse.yr + 10 * invScale);
ctx.lineWidth = invScale;
ctx.stroke();
ctx.lineWidth = 1;
}
}
// draw a new path if being drawn
if(currentPath){
currentPath.draw(ctx);
}
// If rotating or about to rotate show feedback
if(mouse.ctrl){
ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
view.mouseToWorld(); // get the mouse world coords
ctx.strokeStyle = "black";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(0,0,3,0,Math.PI * 2);
ctx.moveTo(0,0);
ctx.lineTo(mouse.xr,mouse.yr);
ctx.stroke();
ctx.lineWidth = 1.5;
ctx.strokeStyle = "red";
ctx.beginPath();
ctx.arc(0,0,3,0,Math.PI * 2);
ctx.moveTo(0,0);
ctx.lineTo(mouse.xr,mouse.yr);
ctx.stroke();
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(200000 / view.scale,0);
ctx.stroke();
ctx.scale(1/ view.scale,1 / view.scale);
ctx.fillText("X axis",100 ,-10 );
}
}
/******************************************************************************/
// end of answer code
/******************************************************************************/
//Boiler plate from here down and can be ignored.
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
createCanvas = function () {
var c,
cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if(firstRun){
onResize();
firstRun = false;
}else{
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
w : 0,
alt : false,
shift : false,
ctrl : false,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
crashRecover : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
if (m.callbacks) {
m.callbacks.forEach(c => c(e));
}
e.preventDefault();
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === undefined) {
m.callbacks = [callback];
} else {
m.callbacks.push(callback);
}
}
}
m.start = function (element) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
m.element.removeEventListener("contextmenu", preventDefault);
m.element = m.callbacks = undefined;
m.active = false;
}
}
return mouse;
})();
function update(timer) { // Main update loop
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
setTimeout(function(){
resizeCanvas();
mouse.start(canvas, true);
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();
/** SimpleFullCanvasMouse.js end **/
// creates a blank image with 2d context
function createImage(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}
<强>更新强>
toScreen(x,y)
函数。从世界坐标转换为屏幕坐标。