Three buffer pixels per layout unit was judged on device and kept, but the cost of it is real and it is quadratic: nine times the pixels to compose, diffuse and blit. A phone that cannot keep up with that is better off drawing four than dropping frames, so two of the rungs are offered. Not as a number. Nobody outside render.js has any reason to choose buffer pixels per layout unit; what a person can actually tell is whether the screen looks right and whether the phone in their hand keeps up. So the row reads INTENSITY, LOW and DEFAULT, and the mechanism stays where it belongs. 1 is deliberately not on it. At 1 the coarse half of the ruling ladder stops being reachable -- a screen needs a period and a half of the block to read as a pattern, and POSTER and BILLBOARD get stepped back down -- so it is a different picture rather than a cheaper one. It remains a constant for anyone who wants the 1984 look from the console. DENSITY becomes a live binding with setDensity(), which re-cuts the backing stores and clears the sprite cache by calling layout() -- the same path a rotation already takes every day, so nothing else in the drawing code learns a thing. The screenshot harness gets it as a getter rather than the copy it was taking at boot, which would have reported whatever the value happened to be when the page loaded. The sheet is re-ordered while it is open. Theme, blocks and intensity are three settings about how the picture is printed, and reading them with the hints and the sound in between made them look like three unrelated ones.
- commit
- e5d8739b3e9acee8c181f6a9c67c44b109139317
- parent
- aa31fefd515fba22dc0203571210f3fd4e31298a
- author
- Dami <dami.etoile@gmail.com>
| M | README.md | 9 | ++++++++ |
| M | mockup/ | 33 | +++++++++++++++++++----------- |
| M | mockup/ | 31 | +++++++++++++++++++++++++++- |
| M | mockup/ | 7 | +++++-- |
| M | mockup/ | 33 | ++++++++++++++++++++++++++++-- |
| M | mockup/ | 8 | ++++++- |
| M | mockup/ | 2 | +- |
| M | mockup/ | 2 | +- |
@@ -107,6 +107,15 @@ visible square and landing as grain. `DENSITY` in `render.js` composes the same
107107 grid — three times the dots per inch, with the layout, the type and the touch targets exactly where108108 they were. Set it to `1` for the 1984 look.109109 110+Two of its rungs are a setting, because the cost is real: the buffer grows with the square, and a111+phone that cannot keep up with nine times the pixels is better off drawing four than dropping112+frames. INTENSITY in the settings sheet offers LOW and DEFAULT — 2 and 3 — and says it in those113+terms, because nobody outside `render.js` has any reason to choose a number of buffer pixels per114+layout unit; what they can tell is whether the screen looks right and whether the phone keeps up.115+1 is not offered: at 1 the coarse half of the ruling ladder stops being reachable, so it is a116+different picture rather than a cheaper one. Changing it re-cuts every buffer and clears the sprite117+cache, which is `layout()` — the same path a rotation takes.118+110119 It is a transform, not a rewrite. Nothing that measures anything changes units: the scene canvas is111120 scaled and every drawing call arrives through it, so the several dozen hard-coded offsets in the112121 layout code keep meaning what they meant. Only the handful of places that touch a canvas's real@@ -109,23 +109,17 @@
109109 <div id="gate" class="zone"><span class="val beat">TAP TO BEGIN</span></div>110110 111111 <div id="sheet">112- <!-- Filled by game.js from THEMES, so adding a theme is a one-object112+ <!-- The three that change how the picture looks come first and together:113+ what it is printed in, what the blocks are printed with, and how114+ finely. Reading them apart, with the hints and the sound in between,115+ made three settings about one thing look like three unrelated ones.116+117+ Filled by game.js from THEMES, so adding a theme is a one-object113118 edit and never a markup one. -->114119 <div class="group">115120 <div class="grouplbl">THEME</div>116121 <div class="row" id="themeRow"></div>117122 </div>118- <div class="group">119- <div class="grouplbl">HINTS</div>120- <div class="row" id="hintRow"></div>121- </div>122- <div class="group">123- <div class="grouplbl">SOUND</div>124- <div class="row">125- <button data-toggle="sound">TONES</button>126- <button data-toggle="haptics">HAPTICS</button>127- </div>128- </div>129123 <!-- A pull-down rather than a row of buttons: eight shapes plus AUTO130124 wrapped into three ragged lines and read as a wall. Built by game.js131125 out of real elements, not a <select> -- a native menu is the one@@ -139,6 +133,21 @@
139133 <div class="menulist" id="shadeList" role="listbox"></div>140134 </div>141135 </div>136+ <div class="group">137+ <div class="grouplbl">INTENSITY</div>138+ <div class="row" id="densityRow"></div>139+ </div>140+ <div class="group">141+ <div class="grouplbl">HINTS</div>142+ <div class="row" id="hintRow"></div>143+ </div>144+ <div class="group">145+ <div class="grouplbl">SOUND</div>146+ <div class="row">147+ <button data-toggle="sound">TONES</button>148+ <button data-toggle="haptics">HAPTICS</button>149+ </div>150+ </div>142151 <div class="credit" id="credit"></div>143152 <button id="btnClose">CLOSE</button>144153 </div>@@ -24,7 +24,7 @@ import {
2424 import {2525 view, LY, fx, layout, layoutBoard, setTheme, requestRender, consumeDirty,2626 renderFrame, toVirtual, atlasGeometry, familyRowRect, atlasContentHeight,27- PERIOD, clearSprites,27+ PERIOD, clearSprites, setDensity, DENSITIES,2828 } from './render.js';2929 import { THEMES } from './theme.js';3030 import { PATTERNS } from './shading.js';@@ -481,6 +481,32 @@ function closeMenu() {
481481 $('shadeTitle').setAttribute('aria-expanded', 'false');482482 }483483 484+/* How finely the picture is composed -- DENSITY, said in the only terms anyone485+ * outside this file has any reason to care about.486+ *487+ * Nobody wants to choose a number of buffer pixels per layout unit. What they488+ * can tell is whether the screen looks the way it should and whether the phone489+ * in their hand keeps up, so the control names the look and not the mechanism,490+ * and offers the two rungs where the answer to that can differ. It sits with491+ * the theme and the blocks because it is the third thing about how the picture492+ * is printed, and reading it apart from them made one setting look like three.493+ *494+ * A row, like the hints: two choices, and they are ordered. */495+(function buildDensityRow() {496+ const row = $('densityRow');497+ for (const { n, label } of DENSITIES) {498+ const b = document.createElement('button');499+ b.textContent = label;500+ b.dataset.density = n;501+ b.onclick = () => {502+ save.density = n;503+ setDensity(n); // re-cuts the buffers and the sprite cache504+ persist(); syncSheet(); placeDom(); requestRender();505+ };506+ row.appendChild(b);507+ }508+})();509+484510 document.querySelectorAll('[data-toggle]').forEach(b => {485511 b.onclick = () => {486512 save[b.dataset.toggle] = !save[b.dataset.toggle];@@ -495,6 +521,9 @@ function syncSheet() {
495521 b.classList.toggle('armed', b.dataset.shading === save.shading));496522 document.querySelectorAll('[data-hint]').forEach(b =>497523 b.classList.toggle('armed', b.dataset.hint === save.hints));524+ // The dataset is a string and the save is a number, so it is compared as one.525+ document.querySelectorAll('[data-density]').forEach(b =>526+ b.classList.toggle('armed', +b.dataset.density === save.density));498527 $('shadeTitle').textContent =499528 save.shading === 'auto' ? 'AUTO' : (PATTERNS[save.shading] || PATTERNS.atkinson).label;500529 document.querySelectorAll('[data-toggle]').forEach(b =>@@ -5,7 +5,7 @@ import { save, requestPersistence } from './store.js';
55 import { FAMILIES } from './families.js';66 import {77 setTheme, setComboOverride, resolveCombo, lookFor, requestRender, renderFrame, DENSITY,8- view, LY, atlasGeometry, familyRowRect,8+ setDensity, view, LY, atlasGeometry, familyRowRect,99 } from './render.js';1010 import { depthForRung, levelProgress, puzzleFor } from './progress.js';1111 import { THEMES, themeById, comboFor } from './theme.js';@@ -37,8 +37,11 @@ window.klotski = {
3737 walkOptimal, openFamily,3838 atlasGeometry, familyRowRect,3939 depthForRung, levelProgress, puzzleFor,40- THEMES, comboFor, combo, setTheme, lookFor, placeDom, requestRender, renderFrame, DENSITY,40+ THEMES, comboFor, combo, setTheme, lookFor, placeDom, requestRender, renderFrame, setDensity,4141 backdrops: GROUND_NAMES, shading: PATTERN_NAMES, rulings: RULING_NAMES,42+ // A getter, not a copy: it is a setting now, and a number read once at boot43+ // would tell the screenshot harness whatever it happened to be then.44+ get DENSITY() { return DENSITY; },4245 };4346 4447 setTheme(save.palette);@@ -84,10 +84,39 @@ export const PERIOD = 8; // slides are quantised to this
8484 * times the buffer pixels is three times the room, so the whole ladder is8585 * reachable for the first time.8686 *87- * 1 is the 1984 look, should it ever be wanted back. */88-export const DENSITY = 3;87+ * 1 is the 1984 look, should it ever be wanted back.88+ *89+ * Two of the rungs are a setting, because the cost is real: the buffer grows90+ * with the square, and a phone that cannot keep up with nine times the pixels91+ * is better off drawing four than dropping frames. 3 is the standard and 2 is92+ * the way out; 1 is not offered, because at 1 the coarse half of the ruling93+ * ladder stops being reachable and the picture is a different one, not a94+ * cheaper one. */95+/* The rungs offered, in order, and what each is called where somebody actually96+ * has to choose one -- nobody outside this file has any reason to pick a number97+ * of buffer pixels per layout unit. The settings row is built from this, so a98+ * third rung would be a one-object edit and never a markup one. */99+export const DENSITIES = [100+ { n: 2, label: 'LOW' },101+ { n: 3, label: 'DEFAULT' },102+];103+/** Anything not offered lands on the standard. */104+export const cleanDensity = n => (DENSITIES.some(d => d.n === n) ? n : 3);105+106+export let DENSITY = cleanDensity(save.density);89107 const dpx = n => Math.round(n * DENSITY);90108 109+/* Changing it re-cuts every pixel the app owns: the backing stores are sized in110+ * buffer pixels and the sprite cache is baked at the old ones. layout() does111+ * both, and is the same path a rotation takes, so there is nothing here that a112+ * resize does not already do every day. */113+export function setDensity(n) {114+ const d = cleanDensity(n);115+ if (d === DENSITY) return;116+ DENSITY = d;117+ layout();118+}119+91120 /* ------------------------------------------------------------------ canvas */92121 const cvs = document.getElementById('board'); // the dithered picture93122 const octx = cvs.getContext('2d');@@ -23,6 +23,7 @@ const DEFAULTS = () => ({
2323 haptics: true,2424 shading: 'auto', // seeded per rung unless pinned to one screen shape2525 hints: 'full', // full | best | off -- how much of the solver shows26+ density: 3, // buffer pixels per layout unit -- 2 is the low rung2627 fams: {}, // id -> { rung, solved, deepest, best: {rung: steps}, ms }2728 totals: { solved: 0, steps: 0, optimal: 0, ms: 0, xp: 0 },2829 days: { first: null, last: null, streak: 0, best: 0 },@@ -40,7 +41,7 @@ function migrate(raw) {
4041 if (!p) return out;4142 // `ordered` is carried across a migration too, so that the conversion below4243 // sees it whichever path the save arrived by.43- for (const k of ['palette', 'sound', 'haptics', 'shading', 'hints', 'blockStyle', 'ordered']) {44+ for (const k of ['palette', 'sound', 'haptics', 'shading', 'hints', 'density', 'blockStyle', 'ordered']) {4445 if (p[k] !== undefined) out[k] = p[k];4546 }4647 if (raw.schema === 2 && p.solved) {@@ -99,6 +100,11 @@ if (save.blockStyle !== undefined) {
99100 }100101 delete save.blockDensity;101102 103+/* The picture's density is a number the renderer sizes its buffers with, and a104+ * save is the one input to it nobody validates. A stale or hand-edited value105+ * lands on the standard rather than on a canvas of some arbitrary size. */106+if (save.density !== 2) save.density = 3;107+102108 export function persist() {103109 save.counter = (save.counter || 0) + 1;104110 try {@@ -143,7 +143,7 @@ button.ghost { border-color: transparent; }
143143 pointer-events: auto;144144 }145145 #sheet.open { display: flex; }146-/* On a short screen the six stacked groups are taller than the screen is. Two146+/* On a short screen the stacked groups are taller than the screen is. Two147147 columns rather than a scroll: a settings sheet you have to scroll to reach148148 CLOSE is a dialog that has stopped being a dialog. The credit and the button149149 take a line of their own, which is where they were anyway. */