[英]“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
方法,但我认为这最终是值得的。
所以,在子弹中:
scale
和translate
修改这些“事物”的方法draw
这些“东西”的方法例如,我创建了一个名为Figure
的类,它显示了这些功能的 1.0 实现。 我创建了一个引用画布的新实例。 然后我通过传递一个x
, y
和一个label
向它添加点。 scale
和transform
更新这些点的x
和y
属性。 通过点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.