Skip to content

Commit 9357478

Browse files
Add screen ruler tool (#129)
### Motivation - Provide a standalone, full-screen browser ruler for on-screen measurements using CSS centimetre units so designers can quickly check layout scale and spacing. - Show clear visual guides with large ticks every centimetre and smaller ticks every millimetre along the top and left viewport edges. - Report viewport size and mapping information to help understand how the browser maps CSS units to screen pixels. ### Description - Add `ruler.html` which creates fixed horizontal and vertical ruler elements, a corner label, and an on-page readout for viewport pixels, CSS centimetres, and device pixel ratio. - Implement styling and layout via CSS variables and rules (including `--ruler-size`, tick styles, and responsive adjustments) to place rulers on the top and left edges and style cm/mm ticks and labels. - Add JavaScript that measures CSS centimetres with `pixelsPerCentimetre()` by probing a `1cm` element, then uses `makeTick()` and `drawRuler()` to render millimetre and centimetre ticks and labels along each axis. - Recompute and redraw rulers on window resize (debounced), and update the on-page metrics using `window.innerWidth/innerHeight` and `window.devicePixelRatio`. - Add `ruler.docs.md` documenting the tool and its intent. ### Testing - Verified the generated `ruler.html` is well-formed HTML by running the quick parse check `python - <<'PY' ... HTMLParser ... PY`, which executed successfully (printed `ok`). ------ [Codex Task](https://chatgpt.com/codex/cloud/tasks/task_e_6a4e56e87c68832598c3479c5d403671)
1 parent 02f0823 commit 9357478

2 files changed

Lines changed: 283 additions & 0 deletions

File tree

ruler.docs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This full-screen browser ruler draws horizontal and vertical measuring guides along the top and left edges of the viewport. It uses JavaScript to read the current viewport dimensions and CSS centimetre measurements, then renders large labelled ticks every centimetre with smaller ticks at every millimetre. The page also reports the viewport size in pixels, the visible CSS centimetres, and the device pixel ratio so you can understand how the browser is mapping screen dimensions.

ruler.html

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Ruler</title>
8+
<link rel="stylesheet" href="styles.css">
9+
<style>
10+
:root {
11+
--ruler-size: 48px;
12+
--tick-colour: var(--tx);
13+
--tick-subtle: var(--tx-2);
14+
--ruler-bg: color-mix(in srgb, var(--bg) 86%, transparent);
15+
}
16+
17+
body {
18+
min-height: 100vh;
19+
overflow: hidden;
20+
}
21+
22+
.ruler-page {
23+
min-height: 100vh;
24+
padding: calc(var(--ruler-size) + 1.5rem) 1.5rem 1.5rem calc(var(--ruler-size) + 1.5rem);
25+
display: grid;
26+
place-items: center;
27+
background:
28+
radial-gradient(circle at top right, color-mix(in srgb, var(--accent) 16%, transparent), transparent 34rem),
29+
linear-gradient(135deg, var(--bg), var(--bg-2));
30+
}
31+
32+
.panel {
33+
width: min(680px, 100%);
34+
padding: clamp(1.25rem, 3vw, 2rem);
35+
z-index: 2;
36+
}
37+
38+
.panel h1 {
39+
margin-top: 0;
40+
}
41+
42+
.readout {
43+
display: grid;
44+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
45+
gap: 0.75rem;
46+
margin: 1.25rem 0;
47+
}
48+
49+
.metric {
50+
border: 1px solid var(--ui-2);
51+
border-radius: var(--radius-md);
52+
padding: 0.85rem;
53+
background: var(--bg);
54+
}
55+
56+
.metric strong {
57+
display: block;
58+
font-family: var(--font-heading);
59+
font-size: clamp(1.2rem, 3vw, 1.7rem);
60+
line-height: 1.2;
61+
}
62+
63+
.metric span {
64+
color: var(--tx-2);
65+
font-size: 0.9rem;
66+
}
67+
68+
.ruler {
69+
position: fixed;
70+
z-index: 10;
71+
background: var(--ruler-bg);
72+
backdrop-filter: blur(10px);
73+
box-shadow: var(--shadow-soft);
74+
user-select: none;
75+
pointer-events: none;
76+
}
77+
78+
.horizontal-ruler {
79+
top: 0;
80+
left: var(--ruler-size);
81+
right: 0;
82+
height: var(--ruler-size);
83+
border-bottom: 1px solid var(--ui-3);
84+
}
85+
86+
.vertical-ruler {
87+
top: var(--ruler-size);
88+
left: 0;
89+
bottom: 0;
90+
width: var(--ruler-size);
91+
border-right: 1px solid var(--ui-3);
92+
}
93+
94+
.corner {
95+
position: fixed;
96+
top: 0;
97+
left: 0;
98+
width: var(--ruler-size);
99+
height: var(--ruler-size);
100+
z-index: 11;
101+
display: grid;
102+
place-items: center;
103+
font-size: 0.7rem;
104+
font-weight: 700;
105+
background: var(--accent);
106+
color: var(--bg);
107+
border-right: 1px solid var(--ui-3);
108+
border-bottom: 1px solid var(--ui-3);
109+
}
110+
111+
.tick {
112+
position: absolute;
113+
background: var(--tick-colour);
114+
}
115+
116+
.horizontal-ruler .tick {
117+
top: 0;
118+
width: 1px;
119+
height: 32%;
120+
}
121+
122+
.vertical-ruler .tick {
123+
left: 0;
124+
width: 32%;
125+
height: 1px;
126+
}
127+
128+
.horizontal-ruler .tick.half {
129+
height: 52%;
130+
background: var(--tick-subtle);
131+
}
132+
133+
.vertical-ruler .tick.half {
134+
width: 52%;
135+
background: var(--tick-subtle);
136+
}
137+
138+
.horizontal-ruler .tick.cm {
139+
height: 100%;
140+
width: 2px;
141+
}
142+
143+
.vertical-ruler .tick.cm {
144+
width: 100%;
145+
height: 2px;
146+
}
147+
148+
.label {
149+
position: absolute;
150+
color: var(--tx);
151+
font-size: 0.72rem;
152+
font-weight: 700;
153+
line-height: 1;
154+
}
155+
156+
.horizontal-ruler .label {
157+
top: 28px;
158+
transform: translateX(4px);
159+
}
160+
161+
.vertical-ruler .label {
162+
left: 28px;
163+
transform: translateY(4px) rotate(90deg);
164+
transform-origin: left top;
165+
}
166+
167+
@media (max-width: 640px) {
168+
:root {
169+
--ruler-size: 40px;
170+
}
171+
172+
.ruler-page {
173+
padding: calc(var(--ruler-size) + 1rem) 1rem 1rem calc(var(--ruler-size) + 1rem);
174+
}
175+
}
176+
</style>
177+
</head>
178+
179+
<body>
180+
<div class="corner" aria-hidden="true">cm</div>
181+
<div id="horizontal-ruler" class="ruler horizontal-ruler" aria-hidden="true"></div>
182+
<div id="vertical-ruler" class="ruler vertical-ruler" aria-hidden="true"></div>
183+
184+
<main class="ruler-page">
185+
<section class="surface panel" aria-live="polite">
186+
<a class="site-link" href="https://tools.mathspp.com/" aria-label="Back to tools.mathspp.com">← tools.mathspp.com</a>
187+
<h1>Ruler</h1>
188+
<p class="lead">A full-screen browser ruler with millimetre ticks and centimetre labels along the top and left edges of the viewport.</p>
189+
<div class="readout">
190+
<div class="metric">
191+
<strong id="viewport-px"></strong>
192+
<span>viewport pixels</span>
193+
</div>
194+
<div class="metric">
195+
<strong id="viewport-cm"></strong>
196+
<span>CSS centimetres visible</span>
197+
</div>
198+
<div class="metric">
199+
<strong id="pixel-ratio"></strong>
200+
<span>device pixel ratio</span>
201+
</div>
202+
</div>
203+
<p>The browser maps CSS centimetres to screen pixels, so this is best for layout and on-screen measurement. Physical accuracy depends on your browser, display scaling, zoom level, and operating system settings.</p>
204+
</section>
205+
</main>
206+
207+
<script>
208+
(function () {
209+
const horizontalRuler = document.getElementById('horizontal-ruler');
210+
const verticalRuler = document.getElementById('vertical-ruler');
211+
const viewportPx = document.getElementById('viewport-px');
212+
const viewportCm = document.getElementById('viewport-cm');
213+
const pixelRatio = document.getElementById('pixel-ratio');
214+
215+
function pixelsPerCentimetre() {
216+
const probe = document.createElement('div');
217+
probe.style.position = 'absolute';
218+
probe.style.visibility = 'hidden';
219+
probe.style.width = '1cm';
220+
document.body.appendChild(probe);
221+
const px = probe.getBoundingClientRect().width;
222+
probe.remove();
223+
return px;
224+
}
225+
226+
function makeTick(axis, position, kind, label) {
227+
const tick = document.createElement('span');
228+
tick.className = `tick ${kind}`.trim();
229+
tick.style[axis === 'x' ? 'left' : 'top'] = `${position}px`;
230+
if (label) {
231+
const labelElement = document.createElement('span');
232+
labelElement.className = 'label';
233+
labelElement.textContent = label;
234+
labelElement.style[axis === 'x' ? 'left' : 'top'] = `${position}px`;
235+
return [tick, labelElement];
236+
}
237+
return [tick];
238+
}
239+
240+
function drawRuler(ruler, axis, length, pxPerCm) {
241+
const fragment = document.createDocumentFragment();
242+
const pxPerMm = pxPerCm / 10;
243+
const tickCount = Math.ceil(length / pxPerMm);
244+
245+
for (let mm = 0; mm <= tickCount; mm += 1) {
246+
const position = Math.round(mm * pxPerMm * 100) / 100;
247+
const isCentimetre = mm % 10 === 0;
248+
const isHalfCentimetre = mm % 5 === 0;
249+
const kind = isCentimetre ? 'cm' : (isHalfCentimetre ? 'half' : '');
250+
const label = isCentimetre ? String(mm / 10) : '';
251+
makeTick(axis, position, kind, label).forEach(element => fragment.appendChild(element));
252+
}
253+
254+
ruler.replaceChildren(fragment);
255+
}
256+
257+
function render() {
258+
const pxPerCm = pixelsPerCentimetre();
259+
const rulerSize = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--ruler-size')) || 48;
260+
const horizontalLength = Math.max(0, window.innerWidth - rulerSize);
261+
const verticalLength = Math.max(0, window.innerHeight - rulerSize);
262+
263+
drawRuler(horizontalRuler, 'x', horizontalLength, pxPerCm);
264+
drawRuler(verticalRuler, 'y', verticalLength, pxPerCm);
265+
266+
viewportPx.textContent = `${window.innerWidth} × ${window.innerHeight}`;
267+
viewportCm.textContent = `${(window.innerWidth / pxPerCm).toFixed(1)} × ${(window.innerHeight / pxPerCm).toFixed(1)}`;
268+
pixelRatio.textContent = `${window.devicePixelRatio || 1}×`;
269+
}
270+
271+
let resizeTimer;
272+
window.addEventListener('resize', () => {
273+
window.clearTimeout(resizeTimer);
274+
resizeTimer = window.setTimeout(render, 100);
275+
});
276+
277+
render();
278+
})();
279+
</script>
280+
</body>
281+
282+
</html>

0 commit comments

Comments
 (0)