无法在 html 画布元素上获得锐利的线条

问题描述 投票:0回答:2

我花了很长时间试图在画布元素上获得像素完美的直线。我尝试了许多针对其他 stackoverflow 问题和 WebGL github 存储库上提出的问题提出的解决方案。我仍然不确定我错过了什么。我正在尝试绘制一把标尺(如文字处理标尺),其中带有代表当前缩进的三角形。该标尺下方的文本段落的左侧缩进应与标尺上三角形的位置相匹配。我已经成功地做到了这一点,但并非标尺上的所有垂直线都完全锐利,有些比其他的稍粗。 这是我针对此案例的测试沙箱。在我的屏幕(具有 1.25 devicePixelRatio)上工作得很好(就清晰度而言)的唯一解决方案在不同 dPR 的屏幕上却不起作用(就标尺大小以及段落与标尺的对齐而言)。对于我测试过的所有屏幕都有效的解决方案(关于标尺大小和段落与标尺的对齐)在我测试过的任何屏幕上都不起作用(关于线条清晰度)。有什么想法可以让垂直线变得完美锐利吗?我当然可以接受它们的不完美,但我相信当生成的图形 UI 界面像素完美时,最终会带来更好的用户体验。

我认为在这里发布 jsbin 示例中的所有代码没有用,但我基本上是在做 mozilla 网站上推荐的事情:

//options.rulerLength == 6.25, options.initialPadding == 35
options.canvasWidth = (options.rulerLength * 96 * window.devicePixelRatio) + (options.initialPadding*2);
canvas.style.width = options.canvasWidth + 'px';
canvas.style.height = options.canvasHeight + 'px';

canvas.width = options.canvasWidth * window.devicePixelRatio;
canvas.height = options.canvasHeight * window.devicePixelRatio;
context.scale(window.devicePixelRatio,window.devicePixelRatio);

在我最初的探索中,我天真地开始制作一把具有精确现实生活单位尺寸的标尺,直到研究发现这目前是不可能的(并且被认为对网络标准并不重要),所以我放弃了尝试让标尺匹配现实生活中的单位英寸或厘米(因为这个 UI 将成为 Google Docs 附加组件的一部分,我什至尝试测量 Google Docs 文档顶部的标尺,事实上它并不对应于实际的单位英寸)。所以我给了标题“CSS 英寸标尺”,我放弃了尝试真正的单位英寸标尺,并接受创建一个“CSS 单位英寸”标尺。但我现在的首要任务,除了保持段落和标尺同步之外,是获得像素清晰的线条......

上述测试中选择“英寸”和“更高绘制精度(在我的 1.25 dPR 显示器上)”给出了以下结果: image 但该解决方案仅适用于我的 1.25 dPR 显示器,并且仅在绘制“英寸”时有效。

选择“推荐绘制”(来自 Mozilla 网站的解决方案)可以使段落与所有显示器上的标尺很好地对齐,但会出现这种垂直线: image

我尝试过将上下文翻译 0.5,我尝试过通过 dPR 进行翻译,我尝试过舍入我需要绘制的线的 x 位置,但似乎没有任何效果。有些线条很锐利,有些则不然。 (顺便说一句,我只在 Chrome 上进行了测试。)我已经尝试了在本评论末尾提到的 3 步过程,无论有没有四舍五入,有或没有翻译,仍然不起作用...

我没有像评论中提到的那样尝试过 getBoundingClientRect ,我不确定如何实现它,如果这可以是一个解决方案并且任何人都可以提供帮助,我将不胜感激。请随意修改 jsbin 沙盒测试中的代码。

css html5-canvas pixel-perfect devicepixelratio
2个回答
1
投票
我坚信你无法获得清晰的线条,因为你是使用路径 API 来绘制它们的。 我仅通过使用

像素操作 API 获得了像素完美。


0
投票
所以四年后回到这个问题,我决定再次尝试。我开始为每条线创建特定的像素调整,直到看起来正确为止,然后查看调整模式。经过大量的试验和错误,我终于想出了一系列在我的屏幕上有效的调整序列,我必须在其他几个屏幕上进行测试,但这里是重复的调整模式,它在这个例子:

const pattern = [0.6, 0, 0.2, 0.4];
使用生成器函数,我可以创建一个连续序列,可以将其用作调整(至少对于英寸;对于厘米则必须进行更多测试):

const generateAdjustedSequence = (function* () { const pattern = [0.6, 0, 0.2, 0.4]; let index = 0; while (true) { yield pattern[index % pattern.length]; index++; } })();
该片段最好在整页上查看:)

let rulerOptions = { rulerLength: 6.25, //in inches convertToCM: false, canvasWidth: 800, //the canvasWidth will be calculated as a conversion of 6.25 inches to css pixels + initialPadding canvasHeight: 20, canvas: document.getElementById('previewRuler'), initialPadding: 35, lineWidth: 0.5, strokeStyle: '#000', font: 'bold 10px Arial', tabPos: 0 }; let inchLabel = 'Current tabulation position (1/4 inch increments):'; let cmLabel = 'Current tabulation position (1/2 cm increments):'; var getPixelRatioVals = function(convertToCM, rulerLength) { console.log(window.devicePixelRatio); let inchesToCM = 2.54, dpr = window.devicePixelRatio, // 1.5625 ppi = (96 * dpr) / 100, // 1.5 works great for a fullHD monitor with dPR 1.25 dpi = 96 * ppi, // 144 works great for my fullHD monitor with dPR 1.25 //for inches we will draw a line every eigth of an inch drawInterval = 0.125, dpiA = 96 * dpr; // 150 if (convertToCM) { ppi /= inchesToCM; dpi /= inchesToCM; rulerLength *= inchesToCM; //for centimeters we will draw a line every quarter centimeter drawInterval = 0.25; dpiA /= inchesToCM; } return { inchesToCM: inchesToCM, dpr: dpr, ppi: ppi, dpi: dpi, dpiA: dpiA, rulerLength: rulerLength, drawInterval: drawInterval }; }, triangleAt = function(x, options) { let context = options.context, pixelRatioVals = options.pixelRatioVals, initialPadding = options.initialPadding; let xPosA = x * pixelRatioVals.dpiA; context.lineWidth = 0.5; context.fillStyle = "#4285F4"; context.beginPath(); context.moveTo(initialPadding + xPosA - 6, 11); context.lineTo(initialPadding + xPosA + 6, 11); context.lineTo(initialPadding + xPosA, 18); context.closePath(); context.stroke(); context.fill(); }, drawRuler = function(userOptions) { if (typeof userOptions !== 'object') { alert('bad options data'); return false; } let options = jQuery.extend({}, { rulerLength: 6.25, //in inches convertToCM: false, canvasWidth: 800, canvasHeight: 20, canvas: document.getElementById('previewRuler'), initialPadding: 35, lineWidth: 0.5, strokeStyle: '#000', font: 'bold 10px Arial', tabPos: 0.25 }, userOptions); let context = options.canvas.getContext('2d'), pixelRatioVals = getPixelRatioVals(options.convertToCM, options.rulerLength), canvas = options.canvas; options.context = context; options.pixelRatioVals = pixelRatioVals; options.canvasWidth = (options.rulerLength * 96 * pixelRatioVals.dpr) + (options.initialPadding * 2); canvas.style.width = options.canvasWidth + 'px'; canvas.style.height = options.canvasHeight + 'px'; canvas.width = options.canvasWidth * pixelRatioVals.dpr; canvas.height = options.canvasHeight * pixelRatioVals.dpr; context.scale(pixelRatioVals.dpr, pixelRatioVals.dpr); context.lineWidth = options.lineWidth; context.strokeStyle = options.strokeStyle; context.font = options.font; context.beginPath(); context.moveTo(options.initialPadding, 1); context.lineTo(options.initialPadding + pixelRatioVals.rulerLength * pixelRatioVals.dpiA, 1); context.stroke(); let currentWholeNumber = 0; let offset = 2; //slight offset to center numbers const generateAdjustedSequence = (function*() { const pattern = [0.6, 0, 0.2, 0.4]; let index = 0; while (true) { yield pattern[index % pattern.length]; index++; } })(); for (let interval = 0; interval <= pixelRatioVals.rulerLength; interval += pixelRatioVals.drawInterval) { let xPosA = interval * pixelRatioVals.dpiA; let iteration = interval / 0.125; xPosA += generateAdjustedSequence.next().value; if (interval == Math.floor(interval) && interval > 0) { if (currentWholeNumber + 1 == 10) { offset += 4; } //compensate number centering when two digits context.fillText(++currentWholeNumber, options.initialPadding + xPosA - offset, 14); } else if (interval == Math.floor(interval) + 0.5) { context.beginPath(); context.moveTo(options.initialPadding + xPosA, 15); context.lineTo(options.initialPadding + xPosA, 5); context.closePath(); context.stroke(); } else { context.beginPath(); context.moveTo(options.initialPadding + xPosA, 10); context.lineTo(options.initialPadding + xPosA, 5); context.closePath(); context.stroke(); } } let xPosB = options.tabPos; if (options.convertToCM) { xPosB *= 2; } triangleAt(xPosB, options); }; switch (rulerOptions.convertToCM) { case true: jQuery('input#useCM').prop("checked", true); break; case false: jQuery('input#useInches').prop("checked", true); break; } jQuery('.controlgroup').controlgroup({ "direction": "vertical" }); jQuery('#tabPositionSlider').slider({ min: 0, max: rulerOptions.rulerLength, value: rulerOptions.tabPos, step: 0.25, slide: function(event, ui) { jQuery('#currentTabPosition').val(ui.value); rulerOptions.tabPos = rulerOptions.convertToCM ? parseFloat(ui.value / 2) : parseFloat(ui.value); drawRuler(rulerOptions); let pixelRatioVals = getPixelRatioVals(rulerOptions.convertToCM, rulerOptions.rulerLength); let paddingLeft = rulerOptions.initialPadding + (ui.value * pixelRatioVals.dpiA); jQuery("#dummyText").css({ "padding-left": paddingLeft + "px" }); } }); jQuery('label[for=currentTabPosition]').text((rulerOptions.convertToCM ? cmLabel : inchLabel)); jQuery('#currentTabPosition').val(rulerOptions.tabPos); jQuery('.controlgroup input').on('change', function() { if (this.checked) { let curSliderVal, pixelRatioVals, paddingLeft; switch (this.value) { case "useCM": rulerOptions.convertToCM = true; jQuery('label[for=currentTabPosition]').text(cmLabel); curSliderVal = jQuery("#tabPositionSlider").slider("value"); jQuery("#tabPositionSlider").slider("option", "max", rulerOptions.rulerLength * 2.54); jQuery("#tabPositionSlider").slider("option", "step", 0.5); jQuery("#tabPositionSlider").slider("value", curSliderVal * 2); //rulerLength *= inchesToCM jQuery("#currentTabPosition").val((curSliderVal * 2).toString()); drawRuler(rulerOptions); pixelRatioVals = getPixelRatioVals(rulerOptions.convertToCM, rulerOptions.rulerLength); paddingLeft = rulerOptions.initialPadding + (curSliderVal * 2 * pixelRatioVals.dpiA); jQuery("#dummyText").css({ "padding-left": paddingLeft + "px" }); break; case "useInches": rulerOptions.convertToCM = false; jQuery('label[for=currentTabPosition]').text(inchLabel); curSliderVal = jQuery("#tabPositionSlider").slider("value"); jQuery("#tabPositionSlider").slider("option", "max", rulerOptions.rulerLength); jQuery("#tabPositionSlider").slider("option", "step", 0.25); jQuery("#tabPositionSlider").slider("value", curSliderVal / 2); jQuery("#currentTabPosition").val((curSliderVal / 2).toString()); drawRuler(rulerOptions); pixelRatioVals = getPixelRatioVals(rulerOptions.convertToCM, rulerOptions.rulerLength); paddingLeft = rulerOptions.initialPadding + (curSliderVal / 2 * pixelRatioVals.dpiA); jQuery("#dummyText").css({ "padding-left": paddingLeft + "px" }); break; } } }); let bestWidth = rulerOptions.rulerLength * 96 * window.devicePixelRatio + (rulerOptions.initialPadding * 2); // * window.devicePixelRatio jQuery('#dummyText').css({ "width": bestWidth + "px" }); drawRuler(rulerOptions);
body {
  text-align: center;
  border: 1px solid green;
}

#dummyTextContainer {
  width: 100%;
}

#dummyText {
  text-align: justify;
  width: 800px;
  margin: 0px auto;
  box-sizing: border-box;
  padding-left: 35px;
  padding-right: 35px;
}

div.controlgroup {
  padding: 12px;
  border: 1px groove white;
  font-weight: bold;
  text-align: center;
  width: auto;
  margin: 12px;
}

canvas {
  border: 1px solid Red;
}

div#tabPositionSlider {
  width: 80%;
  margin: 0px auto;
}

span.ui-checkboxradio-icon {
  display: none;
}

.ui-controlgroup-vertical .ui-controlgroup-item {
  text-align: center;
}
<!DOCTYPE html>
<html>

<head>
  <meta name="description" content="css inch ruler">
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <link href="https://code.jquery.com/ui/1.12.1/themes/smoothness/jquery-ui.css" rel="stylesheet" type="text/css" />
</head>

<body>
  <h2 id="greeting">THE "CSS INCH" RULER (6.25 css inches or 15.875 css centimeters)</h2>
  <canvas id="previewRuler"></canvas>
  <div id="dummyTextContainer">
    <p id="dummyText">Friends, Romans, countrymen, lend me your ears; I come to bury Caesar, not to praise him. The evil that men do lives after them; The good is oft interred with their bones; So let it be with Caesar. The noble Brutus Hath told you Caesar was ambitious:
      If it were so, it was a grievous fault, And grievously hath Caesar answer’d it. Here, under leave of Brutus and the rest– For Brutus is an honourable man; So are they all, all honourable men– Come I to speak in Caesar’s funeral. He was my friend,
      faithful and just to me: But Brutus says he was ambitious; And Brutus is an honourable man. He hath brought many captives home to Rome Whose ransoms did the general coffers fill: Did this in Caesar seem ambitious? When that the poor have cried,
      Caesar hath wept: Ambition should be made of sterner stuff: Yet Brutus says he was ambitious; And Brutus is an honourable man. You all did see that on the Lupercal I thrice presented him a kingly crown, Which he did thrice refuse: was this ambition?
      Yet Brutus says he was ambitious; And, sure, he is an honourable man. I speak not to disprove what Brutus spoke, But here I am to speak what I do know. You all did love him once, not without cause: What cause withholds you then, to mourn for him?
      O judgment! thou art fled to brutish beasts, And men have lost their reason. Bear with me; My heart is in the coffin there with Caesar, And I must pause till it come back to me.</p>
  </div>
  <div id="controlsContainer">
    <div class="controlgroup">
      <label><input type="radio" value="useInches" name="unit" id="useInches">INCHES</label>
      <label><input type="radio" value="useCM" name="unit" id="useCM">CENTIMETERS</label>
    </div>
    <div id="tabPositionSlider"></div>
    <p>
      <label for="currentTabPosition">Current tabulation position (1/4 inch increments):</label>
      <input type="text" id="currentTabPosition" readonly style="border:0; color:#f6931f; background-color:transparent; font-weight:bold;">
    </p>

  </div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
  <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
</body>

</html>

© www.soinside.com 2019 - 2024. All rights reserved.