简体   繁体   English

用于书写文本的“撤消”画布转换

[英]“Undo” canvas transformations for writing text

When applying a transformation with canvas, the resulting text is also (obviously) transformed.使用画布应用转换时,生成的文本也会(显然)被转换。 Is there a way to prevent certain transformations, such as reflection, of affecting text?有没有办法防止影响文本的某些转换,例如反射?

For example, I set a global transformation matrix so the Y-axis points upwards, X-axis to the right, and the (0, 0) point is in the center of the screen (what you'd expect of a mathematical coordinate system).例如,我设置了一个全局变换矩阵,因此 Y 轴指向上方,X 轴指向右侧,并且(0, 0)点位于屏幕的中心(您对数学坐标系的期望) )。

However, this also makes the text upside-down.但是,这也会使文本颠倒。

 const size = 200; const canvas = document.getElementsByTagName('canvas')[0] canvas.width = canvas.height = size; const ctx = canvas.getContext('2d'); ctx.setTransform(1, 0, 0, -1, size / 2, size / 2); const triangle = [ {x: -70, y: -70, label: 'A'}, {x: 70, y: -70, label: 'B'}, {x: 0, y: 70, label: 'C'}, ]; // draw lines ctx.beginPath(); ctx.strokeStyle = 'black'; ctx.moveTo(triangle[2].x, triangle[2].y); triangle.forEach(v => ctx.lineTo(vx, vy)); ctx.stroke(); ctx.closePath(); // draw labels ctx.textAlign = 'center'; ctx.font = '24px Arial'; triangle.forEach(v => ctx.fillText(v.label, vx, vy - 8));
 <canvas></canvas>

Is there a "smart" way to get the text in "correct" orientation, apart from manually resetting transformation matrices?除了手动重置变换矩阵之外,是否有一种“智能”方法可以使文本处于“正确”方向?

My solution is rotate the canvas and then draw the text.我的解决方案是旋转画布,然后绘制文本。

ctx.scale(1,-1); // rotate the canvas
triangle.forEach(v => {
ctx.fillText(v.label, v.x, -v.y + 25); // draw with a bit adapt position
});

Hope that helps :)希望有帮助:)

 const size = 200; const canvas = document.getElementsByTagName('canvas')[0] canvas.width = canvas.height = size; const ctx = canvas.getContext('2d'); ctx.setTransform(1, 0, 0, -1, size / 2, size / 2); const triangle = [ {x: -70, y: -70, label: 'A'}, {x: 70, y: -70, label: 'B'}, {x: 0, y: 70, label: 'C'}, ]; // draw lines ctx.beginPath(); ctx.strokeStyle = 'black'; ctx.moveTo(triangle[2].x, triangle[2].y); triangle.forEach(v => ctx.lineTo(vx, vy)); ctx.stroke(); ctx.closePath(); // draw labels ctx.textAlign = 'center'; ctx.font = '24px Arial'; ctx.scale(1,-1); triangle.forEach(v => { ctx.fillText(v.label, vx, -vy + 25); });
 <canvas></canvas>

To build off of Tai's answer, which is fantastic, you might want to consider the following:为了建立 Tai 的答案,这太棒了,您可能需要考虑以下几点:

 const size = 200; const canvas = document.getElementsByTagName('canvas')[0] canvas.width = canvas.height = size; const ctx = canvas.getContext('2d'); // Create a custom fillText funciton that flips the canvas, draws the text, and then flips it back ctx.fillText = function(text, x, y) { this.save(); // Save the current canvas state this.scale(1, -1); // Flip to draw the text this.fillText.dummyCtx.fillText.call(this, text, x, -y); // Draw the text, invert y to get coordinate right this.restore(); // Restore the initial canvas state } // Create a dummy canvas context to use as a source for the original fillText function ctx.fillText.dummyCtx = document.createElement('canvas').getContext('2d'); ctx.setTransform(1, 0, 0, -1, size / 2, size / 2); const triangle = [ {x: -70, y: -70, label: 'A'}, {x: 70, y: -70, label: 'B'}, {x: 0, y: 70, label: 'C'}, ]; // draw lines ctx.beginPath(); ctx.strokeStyle = 'black'; ctx.moveTo(triangle[2].x, triangle[2].y); triangle.forEach(v => ctx.lineTo(vx, vy)); ctx.stroke(); ctx.closePath(); // draw labels ctx.textAlign = 'center'; ctx.font = '24px Arial'; // For this particular example, multiplying x and y by small factors >1 offsets the labels from the triangle vertices triangle.forEach(v => ctx.fillText(v.label, 1.2*vx, 1.1*vy));

The above is useful if for your real application, you'll be going back and forth between drawing non-text objects and drawing text and don't want to have to remember to flip the canvas back and forth.如果对于您的实际应用程序,您将在绘制非文本对象和绘制文本之间来回切换,并且不想记住来回翻转画布,则上述内容很有用。 (It's not a huge problem in the current example, because you draw the triangle and then draw all the text, so you only need one flip. But if you have in mind a different application that's more complex, that could be an annoyance.) In the above example, I've replaced the fillText method with a custom method that flips the canvas, draws the text, and then flips it back again so that you don't have to do it manually every time you want to draw text. (在当前示例中这不是一个大问题,因为您先绘制三角形然后绘制所有文本,因此您只需要翻转一次。但如果您想使用更复杂的其他应用程序,那可能会很麻烦。)在上面的例子中,我用自定义方法替换了 fillText 方法,该方法翻转画布,绘制文本,然后再次翻转回来,这样您就不必每次要绘制文本时都手动执行此操作。

The result:结果:

在此处输入图片说明

If you don't like overriding the default fillText , then obviously you can just create a method with a new name;如果您不喜欢覆盖默认的fillText ,那么显然您可以创建一个具有新名称的方法; that way you could also avoid creating the dummy context and just use this.fillText within your custom method.这样你也可以避免创建虚拟上下文,只需在你的自定义方法中使用this.fillText

EDIT: The above approach also works with arbitrary zoom and translation.编辑:上述方法也适用于任意缩放和平移。 scale(1, -1) simply reflects the canvas over the x-axis: after this transformation, a point that was at (x, y) will now be at (x, -y). scale(1, -1)只是在 x 轴上反射画布:在此转换之后,位于 (x, y) 的点现在将位于 (x, -y)。 This is true regardless of translation and zoom.无论平移和缩放如何,都是如此。 If you want the text to remain a constant size regardless of zoom, then you just have to scale the font size by zoom.如果您希望文本无论缩放如何都保持恒定大小,那么您只需要通过缩放来缩放字体大小。 For example:例如:

 <html> <body> <canvas id='canvas'></canvas> </body> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); var framesPerSec = 100; var msBetweenFrames = 1000/framesPerSec; ctx.font = '12px Arial'; function getRandomCamera() { return {x: ((Math.random() > 0.5) ? -1 : 1) * Math.random()*5, y: ((Math.random() > 0.5) ? -1 : 1) * Math.random()*5+5, zoom: Math.random()*20+0.1, }; } var camera = getRandomCamera(); moveCamera(); function moveCamera() { var newCamera = getRandomCamera(); var transitionFrames = Math.random()*500+100; var animationTime = transitionFrames*msBetweenFrames; var cameraSteps = { x: (newCamera.x-camera.x)/transitionFrames, y: (newCamera.y-camera.y)/transitionFrames, zoom: (newCamera.zoom-camera.zoom)/transitionFrames }; for (var t=0; t<animationTime; t+=msBetweenFrames) { window.setTimeout(updateCanvas, t); } window.setTimeout(moveCamera, animationTime); function updateCanvas() { camera.x += cameraSteps.x; camera.y += cameraSteps.y; camera.zoom += cameraSteps.zoom; redrawCanvas(); } } ctx.drawText = function(text, x, y) { this.save(); this.transform(1 / camera.zoom, 0, 0, -1 / camera.zoom, x, y); this.fillText(text, 0, 0); this.restore(); } function redrawCanvas() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); ctx.translate(canvas.width / 2 - (camera.x * camera.zoom), canvas.height / 2 + (camera.y * camera.zoom)); ctx.scale(camera.zoom, -camera.zoom); for (var i = 0; i < 10; i++) { ctx.beginPath(); ctx.arc(5, i * 2, .5, 0, 2 * Math.PI); ctx.drawText(i, 7, i*2-0.5); ctx.fill(); } ctx.restore(); } </script> </html>

EDIT: Modified text scaling method based on suggestion by Blindman67.编辑:根据 Blindman67 的建议修改文本缩放方法。 Also improved demo by making camera motion gradual.还通过使相机运动渐进来改进演示。

I'd go with an approach that stores the "state" of your drawing without the actual pixels, and defines a draw method that can render this state at any point.我会采用一种方法,在没有实际像素的情况下存储绘图的“状态”,并定义一个可以在任何时候呈现此状态的draw方法。

You'll have to implement your own scale and translate methods for your points, but I think it's worth it in the end.您必须实现自己的scale并为您的积分translate方法,但我认为这最终是值得的。

So, in bullets:所以,在子弹中:

  • Store a list of "things to draw" (points with labels)存储“要绘制的东西”列表(带标签的点)
  • Expose scale and translate methods that modify these "things"公开scaletranslate修改这些“事物”的方法
  • Expose a draw method that renders these "things"公开一个draw这些“东西”的方法

As an example, I've created a class called Figure that shows a 1.0 implementation of these features.例如,我创建了一个名为Figure的类,它显示了这些功能的 1.0 实现。 I create a new instance that references a canvas.我创建了一个引用画布的新实例。 I then add points to it by passing an x , y and a label .然后我通过传递一个xy和一个label向它添加点。 scale and transform update these points' x and y properties. scaletransform更新这些点的xy属性。 draw loops through the points to a) draw the "dot", and b) draw the label.通过点draw循环以 a) 绘制“点”,和 b) 绘制标签。

 const Figure = function(canvas) { const ctx = canvas.getContext('2d'); const origin = { x: canvas.width / 2, y: canvas.height / 2 }; const shift = p => Object.assign(p, { x: origin.x + px, y: origin.y - py }); let points = []; this.addPoint = (x, y, label) => { points = points.concat({ x, y, label }); } this.translate = (tx, ty) => { points = points.map( p => Object.assign(p, { x: px + tx, y: py + ty }) ); }; this.scale = (sx, sy) => { points = points.map( p => Object.assign(p, { x: px * sx, y: py * sy }) ); }; this.draw = function() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.beginPath(); const sPoints = points.map(shift); sPoints.forEach(p => drawDot(ctx, 5, px, py)); sPoints.forEach(p => drawLabel(ctx, p.label, px + 5, py)); ctx.fill(); } } const init = () => { const canvas = document.getElementById('canvas'); const fig = new Figure(canvas); // Generate some test data for (let i = 0, labels = "ABCD"; i < labels.length; i += 1) { fig.addPoint(i * 3, (i + 1) * 10, labels[i]); } const sX = parseFloat(document.querySelector(".js-scaleX").value); const sY = parseFloat(document.querySelector(".js-scaleY").value); const tX = parseFloat(document.querySelector(".js-transX").value); const tY = parseFloat(document.querySelector(".js-transY").value); fig.scale(sX, sY); fig.translate(tX, tY); fig.draw(); } Array .from(document.querySelectorAll("input")) .forEach(el => el.addEventListener("change", init)); init(); // Utilities for drawing function drawDot(ctx, d, x, y) { ctx.arc(x, y, d / 2, 0, 2 * Math.PI); } function drawLabel(ctx, label, x, y) { ctx.fillText(label, x, y); }
 canvas { background: #efefef; margin: 1rem; } input { width: 50px; }
 <div> <p> Scales first, translates second (hard coded, can be changed) </p> <label>Scale x <input type="number" class="js-scaleX" value="1"></label> <label>Scale y <input type="number" class="js-scaleY" value="1"></label> <br/> <label>Translate x <input type="number" class="js-transX" value="0"></label> <label>translate y <input type="number" class="js-transY" value="0"></label> </div> <canvas id="canvas" width="250" height="250"></canvas>

Note: use the inputs for an example of how this works.注意:使用输入作为其工作原理的示例。 I've chosen to "commit" the changes from scale and translate immediately, so order matters!我选择“提交”规模的变化并立即翻译,所以订单很重要! You might want to press full-screen to get both canvas and inputs in to view.您可能想要按全屏以同时查看画布和输入。

Alternative solution替代方案

  var x = 100;
  var y = 100;
  var pixelRatio = 2;
  var transform = {"x": 0, "y": 0, "k": 1}

  context.save();
  context.setTransform(pixelRatio, 0.0, 0.0, pixelRatio, 0.0, 0.0);
  context.translate(transform.x, 0);
  context.scale(transform.k, 1);

  context.save();
  // get Transformed Point
  var context_transform = context.getTransform();
  var pt = context_transform.transformPoint({
        x: x,
        y: y
      });

  // Reset previous transforms
  context.setTransform(pixelRatio, 0.0, 0.0, pixelRatio, -pt.x, -pt.y);
  

  // draw with the values as usual
  context.textAlign = "left";
  context.font = "14px Arial";
  context.fillText("Hello", pt.x, pt.y);
  context.restore();

  context.restore();

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

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