Skip to content

Commit a9001cb

Browse files
committed
Add support for asymmetric padding
1 parent 956a3fc commit a9001cb

File tree

5 files changed

+83
-49
lines changed

5 files changed

+83
-49
lines changed

packages/tyria/dev/index.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
debug overlays
8989
</label>
9090
<button id="lionsarch">Lion's Arch</button>
91+
<button id="lionsarch2">Lion's Arch (padding)</button>
9192
<button id="ascalon">Ascalon</button>
9293
<button id="horn">Horn of Maguuma</button>
9394
<button id="cantha">Cantha</button>
@@ -101,9 +102,8 @@
101102
maxZoom: 7,
102103
minZoom: 1,
103104
zoomSnap: .5,
104-
padding: 80,
105105
bounds: [[0, 0], [81920, 114688]],
106-
// padding: { top: 16, bottom: 80, left: 16, right: 80 },
106+
padding: { top: 16, bottom: 80, left: 16, right: 80 },
107107
});
108108

109109
map.addLayer(new TileLayer({
@@ -143,6 +143,7 @@
143143
document.getElementById('debug').addEventListener('change', (e) => map.setDebug(e.target.checked));
144144

145145
document.getElementById('lionsarch').addEventListener('click', () => map.easeTo({ contain: [[48130, 30720], [50430, 32250]] }))
146+
document.getElementById('lionsarch2').addEventListener('click', () => map.easeTo({ contain: [[48130, 30720], [50430, 32250]], padding: { top: 16, right: 80, bottom: 80, left: 1000 } }))
146147
document.getElementById('ascalon').addEventListener('click', () => map.easeTo({ contain: [[56682, 24700], [64500, 35800]], zoom: 3 }))
147148
document.getElementById('horn').addEventListener('click', () => map.easeTo({ contain: [[19328, 19048], [27296, 24800]] }))
148149
document.getElementById('cantha').addEventListener('click', () => map.easeTo({ contain: [[20576, 97840], [39056, 106256]] }))

packages/tyria/src/Tyria.ts

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ImageManager } from './image-manager';
44
import { Layer, LayerHitTestContext, LayerPreloadContext, LayerRenderContext } from './layer';
55
import { TyriaMapOptions } from './options';
66
import { RenderQueue, RenderQueuePriority, RenderReason } from './render-queue';
7-
import { Bounds, Point, View, ViewOptions } from './types';
7+
import { Bounds, Padding, Point, View, ViewOptions } from './types';
88
import { add, clamp, easeInOutCubic, getPadding, multiply, subtract } from './util';
99

1010
export class Tyria extends TyriaEventTarget {
@@ -13,7 +13,8 @@ export class Tyria extends TyriaEventTarget {
1313

1414
view: Readonly<View> = {
1515
center: [0, 0],
16-
zoom: 1
16+
zoom: 1,
17+
padding: { top: 0, right: 0, bottom: 0, left: 0 },
1718
}
1819
layers: { id: number, layer: Layer }[] = [];
1920
debug = false
@@ -116,9 +117,11 @@ export class Tyria extends TyriaEventTarget {
116117
const dpr = window.devicePixelRatio || 1;
117118
const width = this.canvas.width / dpr;
118119
const height = this.canvas.height / dpr;
120+
const padding = this.view.padding;
121+
119122
const translate = this.project(this.view.center);
120-
const translateX = -translate[0] + (width / 2);
121-
const translateY = -translate[1] + (height / 2);
123+
const translateX = -translate[0] + (padding.left - padding.right + width) / 2;
124+
const translateY = -translate[1] + (padding.top - padding.bottom + height) / 2;
122125

123126
const transform = new DOMMatrix([dpr, 0, 0, dpr, translateX * dpr, translateY * dpr]);
124127

@@ -131,6 +134,7 @@ export class Tyria extends TyriaEventTarget {
131134
zoom: this.view.zoom,
132135
width,
133136
height,
137+
padding,
134138
area: this.#getViewportArea(this.view),
135139
dpr,
136140
debug: this.debug,
@@ -197,35 +201,35 @@ export class Tyria extends TyriaEventTarget {
197201
ctx.resetTransform();
198202

199203
// render padding
200-
if(this.options.padding && this.debugLastViewOptions?.contain) {
201-
ctx.fillStyle = '#673AB788';
202-
ctx.strokeStyle = '#673AB7';
203-
ctx.lineWidth = 2 * dpr;
204-
205-
const padding = getPadding(this.options.padding);
204+
ctx.fillStyle = '#673AB788';
205+
ctx.strokeStyle = '#673AB7';
206+
ctx.lineWidth = 2 * dpr;
206207

207-
if(padding.top) {
208-
ctx.fillRect(padding.left * dpr, 0, (width - padding.left - padding.right) * dpr, padding.top * dpr);
209-
}
210-
if(padding.bottom) {
211-
ctx.fillRect(padding.left * dpr, (height - padding.bottom) * dpr, (width - padding.left - padding.right) * dpr, height * dpr);
212-
}
213-
if(padding.left) {
214-
ctx.fillRect(0, 0, padding.left * dpr, height * dpr);
215-
}
216-
if(padding.right) {
217-
ctx.fillRect((width - padding.right) * dpr, 0, padding.right * dpr, height * dpr);
218-
}
219-
ctx.strokeRect(padding.left * dpr, padding.top * dpr, (width - padding.left - padding.right) * dpr, (height - padding.top - padding.bottom) * dpr);
208+
if(padding.top) {
209+
ctx.fillRect(padding.left * dpr, 0, (width - padding.left - padding.right) * dpr, padding.top * dpr);
210+
}
211+
if(padding.bottom) {
212+
ctx.fillRect(padding.left * dpr, (height - padding.bottom) * dpr, (width - padding.left - padding.right) * dpr, height * dpr);
213+
}
214+
if(padding.left) {
215+
ctx.fillRect(0, 0, padding.left * dpr, height * dpr);
220216
}
217+
if(padding.right) {
218+
ctx.fillRect((width - padding.right) * dpr, 0, padding.right * dpr, height * dpr);
219+
}
220+
ctx.strokeRect(padding.left * dpr, padding.top * dpr, (width - padding.left - padding.right) * dpr, (height - padding.top - padding.bottom) * dpr);
221221

222222
// render map center
223-
ctx.setTransform(dpr, 0, 0, dpr, dpr * width / 2, dpr * height / 2);
223+
ctx.setTransform(dpr, 0, 0, dpr, dpr * (padding.left - padding.right + width) / 2, dpr * (padding.top - padding.bottom + height) / 2);
224224
ctx.fillStyle = 'lime';
225225
ctx.fillRect(-4, -4, 8, 8);
226226
ctx.font = '12px monospace';
227227
ctx.textAlign = 'left';
228228
ctx.textBaseline = 'top';
229+
ctx.fillStyle = '#000';
230+
ctx.fillText(`px ${translate[0]}, ${translate[1]}`, 8 + 1, 0 + 1);
231+
ctx.fillText(`map ${this.view.center[0]}, ${this.view.center[1]}`, 8 + 1, 16 + 1);
232+
ctx.fillText(`zoom ${this.view.zoom}`, 8 + 1, 32 + 1);
229233
ctx.fillStyle = '#fff';
230234
ctx.fillText(`px ${translate[0]}, ${translate[1]}`, 8, 0);
231235
ctx.fillText(`map ${this.view.center[0]}, ${this.view.center[1]}`, 8, 16);
@@ -264,6 +268,9 @@ export class Tyria extends TyriaEventTarget {
264268
// get dpr to correctly calculate viewport size
265269
const dpr = window.devicePixelRatio ?? 1;
266270

271+
// get padding
272+
const padding = getPadding(view.padding ?? this.options.padding);
273+
267274
// make sure the area is completely visible in the viewport
268275
// TODO: handle passing contain + center?
269276
if(view.contain) {
@@ -272,7 +279,6 @@ export class Tyria extends TyriaEventTarget {
272279
const aspectRatio = size[0] / size[1];
273280

274281
// get size and aspect ratio of the viewport
275-
const padding = getPadding(this.options.padding);
276282
const viewportSizePx = [
277283
(this.canvas.width / dpr) - padding.left - padding.right,
278284
(this.canvas.height / dpr) - padding.top - padding.bottom,
@@ -293,7 +299,6 @@ export class Tyria extends TyriaEventTarget {
293299
}
294300

295301
// set center to the middle of the area
296-
// TODO: adjust for asymmetric padding
297302
center = add(view.contain[0], multiply(size, 0.5));
298303

299304
// make sure we are zooming out when zoom snapping
@@ -353,17 +358,21 @@ export class Tyria extends TyriaEventTarget {
353358
center = this.unproject(centerPx, zoom);
354359
}
355360

356-
return { center, zoom };
361+
return { center, zoom, padding };
357362
}
358363

359364
/** Gets the area visible in the viewport */
360-
#getViewportArea(view: View): Bounds {
365+
#getViewportArea(view: Readonly<View>): Bounds {
361366
const dpr = window.devicePixelRatio ?? 1;
362-
const viewportHalfSizePx: Point = [this.canvas.width / dpr / 2, this.canvas.height / dpr / 2];
367+
const padding = view.padding;
368+
const viewportHalfSizePx: Point = [
369+
(padding.left - padding.right + this.canvas.width / dpr) / 2,
370+
(padding.top - padding.bottom + this.canvas.height / dpr) / 2
371+
];
363372
const centerPx = this.project(view.center);
364373

365-
const topLeft = this.unproject(subtract(centerPx, viewportHalfSizePx));
366-
const bottomRight = this.unproject(add(centerPx, viewportHalfSizePx));
374+
const topLeft = this.unproject(subtract(subtract(centerPx, viewportHalfSizePx), 0));
375+
const bottomRight = this.unproject(add(add(centerPx, viewportHalfSizePx), [padding.right - padding.left, padding.bottom - padding.top]));
367376

368377
return [topLeft, bottomRight];
369378
}
@@ -399,7 +408,7 @@ export class Tyria extends TyriaEventTarget {
399408
const target = this.resolveView(view);
400409

401410
// if we are not moving, don't move
402-
if(target.zoom === start.zoom && target.center[0] === start.center[0] && target.center[1] === start.center[1]) {
411+
if(target.zoom === start.zoom && target.center[0] === start.center[0] && target.center[1] === start.center[1] && target.padding.top === start.padding.top && target.padding.right === start.padding.right && target.padding.bottom === start.padding.bottom && target.padding.left === start.padding.left) {
403412
return;
404413
}
405414

@@ -409,10 +418,10 @@ export class Tyria extends TyriaEventTarget {
409418
const startArea = this.#getViewportArea(start);
410419
const targetArea = this.#getViewportArea(target);
411420
const combinedArea: Bounds = [
412-
[Math.min(startArea[0][0], targetArea[0][0]), Math.min(startArea[0][1], targetArea[0][1])] as Point,
413-
[Math.max(startArea[1][0], targetArea[1][0]), Math.max(startArea[1][1], targetArea[1][1])] as Point
421+
[Math.min(startArea[0][0], targetArea[0][0]), Math.min(startArea[0][1], targetArea[0][1])],
422+
[Math.max(startArea[1][0], targetArea[1][0]), Math.max(startArea[1][1], targetArea[1][1])]
414423
];
415-
this.preload(this.resolveView({ contain: combinedArea }));
424+
this.preload({ contain: combinedArea, padding: 0 });
416425

417426
// calculate delta
418427
const deltaZoom = target.zoom - start.zoom;
@@ -439,8 +448,16 @@ export class Tyria extends TyriaEventTarget {
439448
// calculate center
440449
const center = add(start.center, multiply(deltaCenter, easedProgress * speedup));
441450

451+
// calculate padding
452+
const padding: Padding = {
453+
top: start.padding.top + (target.padding.top - start.padding.top) * easedProgress,
454+
right: start.padding.right + (target.padding.right - start.padding.right) * easedProgress,
455+
bottom: start.padding.bottom + (target.padding.bottom - start.padding.bottom) * easedProgress,
456+
left: start.padding.left + (target.padding.left - start.padding.left) * easedProgress,
457+
}
458+
442459
// set view to the calculated center and zoom
443-
this.view = { center, zoom };
460+
this.view = { center, zoom, padding };
444461

445462
if(progress === 1) {
446463
performance.mark('easeTo-end');
@@ -452,6 +469,7 @@ export class Tyria extends TyriaEventTarget {
452469

453470
if(duration === 0) {
454471
// if the duration of the transition is 0 we just call the end frame
472+
// TODO: why not just call `jumpTo(view)` at the start?
455473
frame(1);
456474
} else {
457475
// store current ease and queue frame
@@ -498,22 +516,26 @@ export class Tyria extends TyriaEventTarget {
498516
/** Convert a pixel in the canvas (for example offsetX/offsetY from an event) to the corresponding map coordinates at that point */
499517
canvasPixelToMapCoordinate([x, y]: Point) {
500518
const dpr = window.devicePixelRatio || 1;
519+
const padding = this.view.padding;
501520

502-
const halfWidth = this.canvas.width / dpr / 2;
503-
const halfHeight = this.canvas.height / dpr / 2;
521+
const viewportHalfSizePx: Point = [
522+
(padding.left - padding.right + this.canvas.width / dpr) / 2,
523+
(padding.top - padding.bottom + this.canvas.height / dpr) / 2
524+
];
504525

505-
const offset: Point = this.unproject([-x + halfWidth, -y + halfHeight]);
526+
const offset: Point = this.unproject([-x + viewportHalfSizePx[0], -y + viewportHalfSizePx[1]]);
506527

507528
return subtract(this.view.center, offset);
508529
}
509530

510531
/** Convert a map coordinate to canvas px */
511532
mapCoordinateToCanvasPixel(coordinate: Point) {
512533
const dpr = window.devicePixelRatio || 1;
534+
const padding = this.view.padding;
513535

514536
const viewportHalfSizePx: Point = [
515-
this.canvas.width / dpr / 2,
516-
this.canvas.height / dpr / 2
537+
(padding.left - padding.right + this.canvas.width / dpr) / 2,
538+
(padding.top - padding.bottom + this.canvas.height / dpr) / 2
517539
];
518540

519541
const pointPx = this.project(coordinate);
@@ -543,6 +565,7 @@ export class Tyria extends TyriaEventTarget {
543565
zoom: target.zoom,
544566
width: this.canvas.width / dpr,
545567
height: this.canvas.height / dpr,
568+
padding: target.padding,
546569
area: this.#getViewportArea(target),
547570
dpr: dpr,
548571
debug: this.debug,
@@ -612,6 +635,7 @@ export class Tyria extends TyriaEventTarget {
612635
zoom: this.view.zoom,
613636
width,
614637
height,
638+
padding: this.view.padding,
615639
area: this.#getViewportArea(this.view),
616640
dpr,
617641
debug: this.debug,

packages/tyria/src/layer.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ImageGetOptions } from "./image-manager";
22
import { RenderReason } from "./render-queue";
3-
import { Bounds, Point } from "./types";
3+
import { Bounds, Padding, Point } from "./types";
44
import { Tyria } from "./Tyria";
55

66
export interface Layer {
@@ -23,6 +23,9 @@ export interface MapState {
2323
/** height of the map in px */
2424
height: number,
2525

26+
/** padding of the map area */
27+
padding: Padding,
28+
2629
/** The visible area in the viewport in map coordinates */
2730
area: Bounds,
2831

packages/tyria/src/layers/TileLayer.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class TileLayer implements Layer {
2323
: new OffscreenCanvas(0, 0);
2424
}
2525

26-
getTiles({ state, project }) {
26+
getTiles({ state, project }: Pick<LayerRenderContext, 'state' | 'project'>) {
2727
// get the zoom level of tiles to use (prefer higher resolution)
2828
const zoom = Math.ceil(state.zoom);
2929

@@ -46,12 +46,12 @@ export class TileLayer implements Layer {
4646
const boundsBottomRight = project(this.options.bounds?.[1] ?? [0, 0]);
4747

4848
// get the top left position (px)
49-
const topLeftX = Math.max(center[0] - state.width / 2, boundsTopLeft[0]);
50-
const topLeftY = Math.max(center[1] - state.height / 2, boundsTopLeft[1]);
49+
const topLeftX = Math.max(center[0] - (state.padding.left - state.padding.right + state.width) / 2, boundsTopLeft[0]);
50+
const topLeftY = Math.max(center[1] - (state.padding.top - state.padding.bottom + state.height) / 2, boundsTopLeft[1]);
5151

5252
// get the top right position (px)
53-
const bottomRightX = Math.min(center[0] + state.width / 2, boundsBottomRight[0]) - 1;
54-
const bottomRightY = Math.min(center[1] + state.height / 2, boundsBottomRight[1]) - 1;
53+
const bottomRightX = Math.min(center[0] + (-state.padding.left + state.padding.right + state.width) / 2, boundsBottomRight[0]) - 1;
54+
const bottomRightY = Math.min(center[1] + (-state.padding.top + state.padding.bottom + state.height) / 2, boundsBottomRight[1]) - 1;
5555

5656
// convert px position to tiles
5757
const tileTopLeft: Point = [Math.floor(topLeftX / renderedTileSize), Math.floor(topLeftY / renderedTileSize)];

packages/tyria/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export type View = {
77

88
/** The zoom level of the map */
99
zoom: number;
10+
11+
/** The padding of the map */
12+
padding: Padding;
1013
}
1114

1215
export type ViewOptions = {
@@ -25,6 +28,9 @@ export type ViewOptions = {
2528
/** Makes sure the viewport is completely within this area. */
2629
cover?: Bounds;
2730

31+
/** Override map padding */
32+
padding?: Partial<Padding> | number;
33+
2834
/**
2935
* Modifies the center to align with device pixels so tiles stay sharp.
3036
* @defaultValue true

0 commit comments

Comments
 (0)