繁体   English   中英

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

[英]“Undo” canvas transformations for writing text

使用画布应用转换时,生成的文本也会(显然)被转换。 有没有办法防止影响文本的某些转换,例如反射?

例如,我设置了一个全局变换矩阵,因此 Y 轴指向上方,X 轴指向右侧,并且(0, 0)点位于屏幕的中心(您对数学坐标系的期望) )。

但是,这也会使文本颠倒。

 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>

除了手动重置变换矩阵之外,是否有一种“智能”方法可以使文本处于“正确”方向?

我的解决方案是旋转画布,然后绘制文本。

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
});

希望有帮助:)

 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>

为了建立 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));

如果对于您的实际应用程序,您将在绘制非文本对象和绘制文本之间来回切换,并且不想记住来回翻转画布,则上述内容很有用。 (在当前示例中这不是一个大问题,因为您先绘制三角形然后绘制所有文本,因此您只需要翻转一次。但如果您想使用更复杂的其他应用程序,那可能会很麻烦。)在上面的例子中,我用自定义方法替换了 fillText 方法,该方法翻转画布,绘制文本,然后再次翻转回来,这样您就不必每次要绘制文本时都手动执行此操作。

结果:

在此处输入图片说明

如果您不喜欢覆盖默认的fillText ,那么显然您可以创建一个具有新名称的方法; 这样你也可以避免创建虚拟上下文,只需在你的自定义方法中使用this.fillText

编辑:上述方法也适用于任意缩放和平移。 scale(1, -1)只是在 x 轴上反射画布:在此转换之后,位于 (x, y) 的点现在将位于 (x, -y)。 无论平移和缩放如何,都是如此。 如果您希望文本无论缩放如何都保持恒定大小,那么您只需要通过缩放来缩放字体大小。 例如:

 <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>

编辑:根据 Blindman67 的建议修改文本缩放方法。 还通过使相机运动渐进来改进演示。

我会采用一种方法,在没有实际像素的情况下存储绘图的“状态”,并定义一个可以在任何时候呈现此状态的draw方法。

您必须实现自己的scale并为您的积分translate方法,但我认为这最终是值得的。

所以,在子弹中:

  • 存储“要绘制的东西”列表(带标签的点)
  • 公开scaletranslate修改这些“事物”的方法
  • 公开一个draw这些“东西”的方法

例如,我创建了一个名为Figure的类,它显示了这些功能的 1.0 实现。 我创建了一个引用画布的新实例。 然后我通过传递一个xy和一个label向它添加点。 scaletransform更新这些点的xy属性。 通过点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>

注意:使用输入作为其工作原理的示例。 我选择“提交”规模的变化并立即翻译,所以订单很重要! 您可能想要按全屏以同时查看画布和输入。

替代方案

  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