Skip to content

Commit b2a39dc

Browse files
authored
bug: the Utility Library radar stops where the map name bar starts (#918)
1 parent ed6ceb5 commit b2a39dc

3 files changed

Lines changed: 140 additions & 10 deletions

File tree

‎components/utility/UtilityMapPreviewTile.vue‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Lock } from "lucide-vue-next";
44
import FiveStackToolTip from "~/components/FiveStackToolTip.vue";
55
import { useRadarProjection } from "~/composables/useRadarProjection";
66
import {
7+
drawnSquare,
78
morphFromRect,
89
restingRect,
910
} from "~/composables/useUtilityMapHandoff";
@@ -121,8 +122,13 @@ function shrinkFrom(from: UtilityMapRect) {
121122
animation.addEventListener("cancel", settle);
122123
}
123124
125+
function rect() {
126+
const box = restingRect(radar.value);
127+
return box ? drawnSquare(box) : null;
128+
}
129+
124130
defineExpose({
125-
rect: () => restingRect(radar.value),
131+
rect,
126132
src: radarSrc,
127133
shrinkFrom,
128134
});
@@ -141,7 +147,7 @@ defineExpose({
141147
v-if="radarSrc"
142148
ref="radar"
143149
:viewBox="`0 0 ${CANVAS} ${CANVAS}`"
144-
class="absolute inset-[7%] h-[86%] w-[86%] lg:inset-x-0 lg:bottom-auto lg:top-[3%] lg:mx-auto lg:h-[90%] lg:w-auto"
150+
class="absolute inset-[7%] h-[86%] w-[86%] lg:inset-x-0 lg:bottom-auto lg:top-[5%] lg:mx-auto lg:h-[calc(95%-2.7rem)] lg:w-auto lg:max-w-[90%]"
145151
aria-hidden="true"
146152
>
147153
<image
@@ -167,8 +173,8 @@ defineExpose({
167173
</svg>
168174
</span>
169175

170-
<!-- Along the foot of the cell on desktop, over the radar's empty lower
171-
margin, so the radar keeps the cell's whole height. -->
176+
<!-- Along the foot of the cell on desktop, 2.7rem tall there (pt-4, pb-2
177+
and one 1.2rem line of text): the radar above stops at its top. -->
172178
<span
173179
class="flex items-center gap-2 border-t border-white/[0.06] px-2.5 py-2 lg:absolute lg:inset-x-0 lg:bottom-0 lg:border-t-0 lg:bg-gradient-to-t lg:from-zinc-950/90 lg:via-zinc-950/60 lg:to-transparent lg:pt-4"
174180
>

‎composables/useUtilityMapHandoff.ts‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,25 +83,42 @@ export function restingRect(element: Element | null | undefined) {
8383
}
8484

8585
/**
86-
* Plays an element from where its twin was to where it is. Both are the same
87-
* square radar, so it is one uniform scale about the element's own corner.
86+
* Where a radar is drawn in its box. Its square viewBox is letterboxed into a
87+
* box that is not square, so the radar is the square centred in it -- a tile
88+
* in a two-row index is taller than it is wide.
89+
*/
90+
export function drawnSquare(box: UtilityMapRect): UtilityMapRect {
91+
const side = Math.min(box.width, box.height);
92+
return {
93+
left: box.left + (box.width - side) / 2,
94+
top: box.top + (box.height - side) / 2,
95+
width: side,
96+
height: side,
97+
};
98+
}
99+
100+
/**
101+
* Plays an element from where its twin was to where it is. Both draw the same
102+
* square radar, so it is one uniform scale about that square's corner.
88103
*/
89104
export function morphFromRect(
90105
element: Element | null | undefined,
91106
from: UtilityMapRect,
92107
) {
93-
const to = element?.getBoundingClientRect();
94-
if (!element || !to || !to.width) {
108+
const box = element?.getBoundingClientRect();
109+
if (!element || !box || !box.width) {
95110
return null;
96111
}
112+
const to = drawnSquare(box);
113+
const transformOrigin = `${to.left - box.left}px ${to.top - box.top}px`;
97114
const scale = from.width / to.width;
98115
return element.animate(
99116
[
100117
{
101-
transformOrigin: "0 0",
118+
transformOrigin,
102119
transform: `translate(${from.left - to.left}px, ${from.top - to.top}px) scale(${scale})`,
103120
},
104-
{ transformOrigin: "0 0", transform: "none" },
121+
{ transformOrigin, transform: "none" },
105122
],
106123
{ duration: 420, easing: "cubic-bezier(0.16, 1, 0.3, 1)" },
107124
);
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// @vitest-environment node
2+
import { describe, expect, it, vi } from "vitest";
3+
import {
4+
drawnSquare,
5+
morphFromRect,
6+
type UtilityMapRect,
7+
} from "~/composables/useUtilityMapHandoff";
8+
9+
const board: UtilityMapRect = { left: 500, top: 100, width: 800, height: 800 };
10+
11+
// A two-row index on desktop: the cell is taller than it is wide, the radar's
12+
// box is capped at 90% of its width, and the square radar is drawn centred in
13+
// it.
14+
const tallTile: UtilityMapRect = {
15+
left: 100,
16+
top: 200,
17+
width: 232,
18+
height: 330,
19+
};
20+
21+
function element(box: UtilityMapRect) {
22+
const animate = vi.fn();
23+
return {
24+
animate,
25+
getBoundingClientRect: () => box,
26+
} as unknown as Element & { animate: ReturnType<typeof vi.fn> };
27+
}
28+
29+
// Where the element's own point (x, y) sits on screen in a keyframe.
30+
function placed(
31+
frame: { transformOrigin: string; transform: string },
32+
box: UtilityMapRect,
33+
) {
34+
const [originX, originY] = frame.transformOrigin.split(" ").map(parseFloat);
35+
const [, x, y, scale] = frame.transform
36+
.match(/translate\((-?[\d.]+)px, (-?[\d.]+)px\) scale\(([\d.]+)\)/)!
37+
.map(Number);
38+
return (pointX: number, pointY: number) => ({
39+
left: box.left + originX + x + scale * (pointX - originX),
40+
top: box.top + originY + y + scale * (pointY - originY),
41+
});
42+
}
43+
44+
function expectAt(
45+
point: { left: number; top: number },
46+
expected: { left: number; top: number },
47+
) {
48+
expect(point.left).toBeCloseTo(expected.left, 6);
49+
expect(point.top).toBeCloseTo(expected.top, 6);
50+
}
51+
52+
describe("drawnSquare", () => {
53+
it("is the box itself when the box is square", () => {
54+
expect(drawnSquare(board)).toEqual(board);
55+
});
56+
57+
it("is the square centred in a box taller than it is wide", () => {
58+
expect(drawnSquare(tallTile)).toEqual({
59+
left: 100,
60+
top: 249,
61+
width: 232,
62+
height: 232,
63+
});
64+
});
65+
66+
it("is the square centred in a box wider than it is tall", () => {
67+
const wide = { left: 0, top: 0, width: 300, height: 200 };
68+
69+
expect(drawnSquare(wide)).toEqual({
70+
left: 50,
71+
top: 0,
72+
width: 200,
73+
height: 200,
74+
});
75+
});
76+
});
77+
78+
describe("morphFromRect", () => {
79+
it("starts a tile's drawn radar exactly on the board it shrinks out of", () => {
80+
const tile = element(tallTile);
81+
82+
morphFromRect(tile, board);
83+
84+
const [start, end] = tile.animate.mock.calls[0][0];
85+
const at = placed(start, tallTile);
86+
expectAt(at(0, 49), { left: board.left, top: board.top });
87+
expectAt(at(232, 281), {
88+
left: board.left + board.width,
89+
top: board.top + board.height,
90+
});
91+
expect(end.transform).toBe("none");
92+
});
93+
94+
it("grows a square board out of a square tile corner to corner", () => {
95+
const viewport = element(board);
96+
const from = drawnSquare(tallTile);
97+
98+
morphFromRect(viewport, from);
99+
100+
const at = placed(viewport.animate.mock.calls[0][0][0], board);
101+
expectAt(at(0, 0), { left: from.left, top: from.top });
102+
expectAt(at(800, 800), {
103+
left: from.left + from.width,
104+
top: from.top + from.height,
105+
});
106+
});
107+
});

0 commit comments

Comments
 (0)