anvilsign in

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
117117 const probe = el?.querySelector<HTMLElement>('.wall-probe');
118118 if (!el || !middle || !probe) return;
119119
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+
120127 const read = () => {
121128 const box = probe.getBoundingClientRect();
122129 if (box.width <= 0) return;
123130 // 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.
124132 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.
130133 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.
132136 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;
135138 const lap = parseFloat(style.getPropertyValue('--wall-lap')) || 1;
136139
140+ const from = `${middle.clientWidth}x${middle.clientHeight}/${ratio}/${angle}/${turn}/${lap}`;
141+ if (from === last) return;
142+ last = from;
143+
137144 // Everything about the shape is linear in the stack, so it is asked for
138145 // once at a stack of one and the answer scaled to the middle there is.
139146 const unit = rhombus(STACKS_PER_SIDE, ratio, angle, turn, lap);
⋯ 2 unchanged lines
142149 Math.min(middle.clientWidth / unit.box.w, middle.clientHeight / unit.box.h),
143150 );
144151 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 });
151153 };
152154
153155 read();
154156 // 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.
156159 const observer = new ResizeObserver(read);
157160 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.
164166 const styled = new MutationObserver(read);
165- styled.observe(el, { attributes: true, attributeFilter: ['style', 'class'] });
167+ styled.observe(el, { attributes: true, attributeFilter: ['style'] });
166168 return () => {
167169 observer.disconnect();
168170 styled.disconnect();
⋯ 5 unchanged lines