collin/mahjong · 7f8bbe3e
Stop the square recomputing itself in a circle
Collin Richards · 2026-08-21 21:31 UTC · 7f8bbe3ee9d0bb358ff8b8dc81ae8080944ab6a8 · parent 08bf9a7c · browse files
modifiedsrc/ui/WallRing.tsx+24 −22
| ⋯ 116 unchanged lines | |||
| 117 | 117 | const probe = el?.querySelector<HTMLElement>('.wall-probe'); | |
| 118 | 118 | if (!el || !middle || !probe) return; | |
| 119 | 119 | ||
| 120 | + | // Everything the square is worked out *from*. Both observers below can be | |
| 121 | + | // set off by this effect's own output — the probe is sized by the `--ws` it | |
| 122 | + | // writes, and the ring's style attribute is where it writes it — so the | |
| 123 | + | // guard is not a nicety: without it, one recompute schedules the next and | |
| 124 | + | // the tab stops answering. Same inputs, nothing to do. | |
| 125 | + | let last = ''; | |
| 126 | + | ||
| 120 | 127 | const read = () => { | |
| 121 | 128 | const box = probe.getBoundingClientRect(); | |
| 122 | 129 | if (box.width <= 0) return; | |
| 123 | 130 | // What shape a stack is, is CSS's business — read it, don't restate it. | |
| 131 | + | // Scale-free, so this is the same number however big the square came out. | |
| 124 | 132 | const ratio = box.height / box.width; | |
| 125 | - | // Stood on its corner, a square needs its *diagonal* where it used to | |
| 126 | - | // need its side — so the room it has to be built in shrinks by however | |
| 127 | - | // much the turn costs. Square on that is 1 and nothing changes; at 45° it | |
| 128 | - | // is √2 and the square comes out about seven tenths the size, which is | |
| 129 | - | // what keeps its arms on the table rather than off the edge of it. | |
| 130 | 133 | const style = getComputedStyle(el); | |
| 131 | - | const turn = parseFloat(style.getPropertyValue('--wall-turn')) || 0; | |
| 134 | + | // The corner it turns, how far round it is set, and how far each wall | |
| 135 | + | // runs past its corner as a multiple of what that corner needs. | |
| 132 | 136 | const angle = parseFloat(style.getPropertyValue('--wall-angle')) || 90; | |
| 133 | - | // A multiple of the overrun the corner actually needs. One is the tightest | |
| 134 | - | // join there can be; more is a longer overlap, and there is no less. | |
| 137 | + | const turn = parseFloat(style.getPropertyValue('--wall-turn')) || 0; | |
| 135 | 138 | const lap = parseFloat(style.getPropertyValue('--wall-lap')) || 1; | |
| 136 | 139 | ||
| 140 | + | const from = `${middle.clientWidth}x${middle.clientHeight}/${ratio}/${angle}/${turn}/${lap}`; | |
| 141 | + | if (from === last) return; | |
| 142 | + | last = from; | |
| 143 | + | ||
| 137 | 144 | // Everything about the shape is linear in the stack, so it is asked for | |
| 138 | 145 | // once at a stack of one and the answer scaled to the middle there is. | |
| 139 | 146 | const unit = rhombus(STACKS_PER_SIDE, ratio, angle, turn, lap); | |
| ⋯ 2 unchanged lines | |||
| 142 | 149 | Math.min(middle.clientWidth / unit.box.w, middle.clientHeight / unit.box.h), | |
| 143 | 150 | ); | |
| 144 | 151 | const shape = rhombus(STACKS_PER_SIDE * s, ratio * s, angle, turn, lap); | |
| 145 | - | const opening = { w: shape.open.w, h: shape.open.h }; | |
| 146 | - | setSquare((was) => | |
| 147 | - | was.cell === s && was.opening?.w === opening.w && was.opening?.h === opening.h | |
| 148 | - | ? was | |
| 149 | - | : { opening, cell: s, shape }, | |
| 150 | - | ); | |
| 152 | + | setSquare({ opening: { w: shape.open.w, h: shape.open.h }, cell: s, shape }); | |
| 151 | 153 | }; | |
| 152 | 154 | ||
| 153 | 155 | read(); | |
| 154 | 156 | // The middle, not the square: the square's own size is this effect's doing, | |
| 155 | - | // and watching it would be watching itself. | |
| 157 | + | // and watching it would be watching itself. The probe is not watched for | |
| 158 | + | // the same reason — it is `--ws` wearing a box, and `--ws` is the answer. | |
| 156 | 159 | const observer = new ResizeObserver(read); | |
| 157 | 160 | observer.observe(middle); | |
| 158 | - | observer.observe(probe); | |
| 159 | - | // The shape is three numbers on the element — the corner it turns, how far | |
| 160 | - | // round it is set, how far the walls lap — and changing any of them changes | |
| 161 | - | // nothing that has a size, so neither of the observers above would ever | |
| 162 | - | // hear about it. `/wall.html` sets them on the ring itself, so that is what | |
| 163 | - | // is watched. | |
| 161 | + | // The shape is three numbers on the element and none of them changes | |
| 162 | + | // anything that has a size, so the observer above would never hear about | |
| 163 | + | // one. `/wall.html` sets them on the ring itself, so that is what is | |
| 164 | + | // watched — and this effect writes to that same attribute, which is what | |
| 165 | + | // `last` is there to absorb. | |
| 164 | 166 | const styled = new MutationObserver(read); | |
| 165 | - | styled.observe(el, { attributes: true, attributeFilter: ['style', 'class'] }); | |
| 167 | + | styled.observe(el, { attributes: true, attributeFilter: ['style'] }); | |
| 166 | 168 | return () => { | |
| 167 | 169 | observer.disconnect(); | |
| 168 | 170 | styled.disconnect(); | |
| ⋯ 5 unchanged lines | |||