我花了很长时间试图在画布元素上获得像素完美的直线。我尝试了许多针对其他 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 显示器上)”给出了以下结果: 但该解决方案仅适用于我的 1.25 dPR 显示器,并且仅在绘制“英寸”时有效。
选择“推荐绘制”(来自 Mozilla 网站的解决方案)可以使段落与所有显示器上的标尺很好地对齐,但会出现这种垂直线:
我尝试过将上下文翻译 0.5,我尝试过通过 dPR 进行翻译,我尝试过舍入我需要绘制的线的 x 位置,但似乎没有任何效果。有些线条很锐利,有些则不然。 (顺便说一句,我只在 Chrome 上进行了测试。)我已经尝试了在本评论末尾提到的 3 步过程,无论有没有四舍五入,有或没有翻译,仍然不起作用...
我没有像评论中提到的那样尝试过 getBoundingClientRect ,我不确定如何实现它,如果这可以是一个解决方案并且任何人都可以提供帮助,我将不胜感激。请随意修改 jsbin 沙盒测试中的代码。
像素操作 API 获得了像素完美。
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>