在旋转的CANVAS上绘制-第2部分 [英] Draw on rotated CANVAS - Part 2
问题描述
作为对此问题和答案的后续操作 ...我还有另一个问题要解决解决:
As a follow up to this question and answer...I have another issue to solve:
当我在画布上绘制然后应用诸如旋转之类的变换时,我想保留绘制的内容并继续绘制。
When I draw on a canvas and then apply some transformations like rotation, I would like to keep what was drawn and continue the drawing.
要对此进行测试,请使用鼠标绘制一些内容,然后单击旋转。
To test this, use the mouse to draw something and then click "rotate".
这是我正在尝试的方法,但是
This is what I'm trying, but the canvas gets erased.
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(链接的问题/答案中原始版本的更新版本) )
DEMO (updated version of original in linked question/answer)
https://jsfiddle.net/mgf8uz7s / 1 /
推荐答案
记录所有路径,使用画布缓冲区保持界面流畅
您有几个选项,具体取决于需求。
Record all paths, use canvas buffer to keep interface smooth
You have several options which will depend on what the requirements are.
-
屏幕外缓冲区用于保存渲染的行。渲染到屏幕外缓冲区,然后将缓冲区绘制到显示画布。这是最快的方法,但是您使用的是像素,因此,如果缩放,则会出现像素瑕疵,这将限制绘图区域的大小(仍然很大,但不是伪无限大),并严格限制可以提供的撤消次数到内存限制
Offscreen buffer/s to hold the rendered lines. Render to the offscreen buffer then draw the buffer to the display canvas. This is the quickest method but you are working with pixels, thus if you zoom you will get pixel artifacts and it will limit the size of the drawing area (still large but not pseudo infinite) and severely restrict the number of undos your can provide due to memory limits
绘制缓冲区时,基本上记录鼠标的移动和点击,然后在每次更新时重新渲染所有可见的路径显示器。这将使您可以缩放和旋转而没有像素伪像,为您提供尽可能大的绘制区域(在64位双精度的限制之内),并且奖励撤消一直返回到第一行。这种方法的问题在于它很快变得很慢(尽管您可以使用webGL提高渲染速度)
Buffer paths as they are draw, basicly recording mouse movements and clicks, then re-rendering all visible paths each time you update the display. This will let you zoom and rotate without pixel artifacts, give you a draw area as large as you like (within limit of 64bit doubles) and a bonus undo all the way back to the first line. The problem with this method is that it quickly becomes very slow (though you can improve rendering speed with webGL)
上述两种方法的组合方法。在绘制路径时记录它们,但也将它们渲染到屏幕外的画布上。使用屏幕外的画布更新显示并保持较高的刷新率。仅在需要时才重新渲染屏幕外的画布,即,在撤消或缩放时,在平移或旋转时不需要重新渲染。
A combination of the above two methods. Record the paths as they are drawn, but also render them to an offscreen canvas/s. Use the offscreen canvas to update the display and keep the refresh rate high. You only re-render the offscreen canvas when you need to, ie when you undo or if you zoom, you will not need to re-render when you pan or rotate.
Demo
我不会做完整的绘图包,所以这只是一个使用屏幕外缓冲区保存可见内容的示例路径。绘制的所有路径都记录在path数组中。当用户更改视图,平移,缩放,旋转时,路径会重新绘制到屏幕外的画布上以匹配新视图。
Demo
I am not going to do a full drawing package so this is just an example that uses an offscreen buffer to hold the visible paths. All paths that are drawn are recorded in a paths array. When the user changes the view, pan, zoom, rotate, the paths are redrawn to the offscreen canvas to match the new view.
有一些样板可以处理设置和鼠标,忽略了。因为有很多代码,而且时间很短,所以由于注释很短,您将不得不从中选择所需的内容。
There is some boilerplate to handle setup and mouse that can be ignored. As there is a lot of code and time is short you will have to pick out what you need from it as the comments are short.
这里有路径
路径对象。 视图
包含转换和相关功能。一些用于平移,缩放和旋转的功能。还有一个显示功能,可以渲染和处理所有鼠标和用户IO。通过按住鼠标修饰符ctrl,alt,shift
There is a paths
object for paths. view
holds the transform and associated functions. Some functions for pan, zoom, rotate. And a display function that renders and handles all mouse and user IO. The pan,zoom and scale controls are accessed via holding the mouse modifiers 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)
函数来查看对象。从世界坐标转换为屏幕坐标。 - 改进的旋转方法以设置绝对x轴方向。
- 添加了带有指示器的旋转反馈,以显示旋转原点
- 在帮助文本显示中显示比例。
- Added many more comments.
- Added
toScreen(x,y)
function to view object. Converts from world coordinates to screen coordinates. - Improved rotation method to set absolute x Axis direction.
- Added rotation feed back with indicators to show rotation origin and the current x Axis direction and a red line to indicate new x Axis direction if mouse button down.
- Showing scale in help text display.
这篇关于在旋转的CANVAS上绘制-第2部分的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!