[英]“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.