diff --git a/packages/tyria/dev/index.html b/packages/tyria/dev/index.html
index 203abce..33e1e97 100644
--- a/packages/tyria/dev/index.html
+++ b/packages/tyria/dev/index.html
@@ -88,6 +88,7 @@
debug overlays
+
@@ -101,9 +102,8 @@
maxZoom: 7,
minZoom: 1,
zoomSnap: .5,
- padding: 80,
bounds: [[0, 0], [81920, 114688]],
- // padding: { top: 16, bottom: 80, left: 16, right: 80 },
+ padding: { top: 16, bottom: 80, left: 16, right: 80 },
});
map.addLayer(new TileLayer({
@@ -143,6 +143,7 @@
document.getElementById('debug').addEventListener('change', (e) => map.setDebug(e.target.checked));
document.getElementById('lionsarch').addEventListener('click', () => map.easeTo({ contain: [[48130, 30720], [50430, 32250]] }))
+ document.getElementById('lionsarch2').addEventListener('click', () => map.easeTo({ contain: [[48130, 30720], [50430, 32250]], padding: { top: 16, right: 80, bottom: 80, left: 1000 } }))
document.getElementById('ascalon').addEventListener('click', () => map.easeTo({ contain: [[56682, 24700], [64500, 35800]], zoom: 3 }))
document.getElementById('horn').addEventListener('click', () => map.easeTo({ contain: [[19328, 19048], [27296, 24800]] }))
document.getElementById('cantha').addEventListener('click', () => map.easeTo({ contain: [[20576, 97840], [39056, 106256]] }))
diff --git a/packages/tyria/src/Tyria.ts b/packages/tyria/src/Tyria.ts
index 23e31bc..bb44c03 100644
--- a/packages/tyria/src/Tyria.ts
+++ b/packages/tyria/src/Tyria.ts
@@ -4,7 +4,7 @@ import { ImageManager } from './image-manager';
import { Layer, LayerHitTestContext, LayerPreloadContext, LayerRenderContext } from './layer';
import { TyriaMapOptions } from './options';
import { RenderQueue, RenderQueuePriority, RenderReason } from './render-queue';
-import { Bounds, Point, View, ViewOptions } from './types';
+import { Bounds, Padding, Point, View, ViewOptions } from './types';
import { add, clamp, easeInOutCubic, getPadding, multiply, subtract } from './util';
export class Tyria extends TyriaEventTarget {
@@ -13,7 +13,8 @@ export class Tyria extends TyriaEventTarget {
view: Readonly = {
center: [0, 0],
- zoom: 1
+ zoom: 1,
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
}
layers: { id: number, layer: Layer }[] = [];
debug = false
@@ -116,9 +117,11 @@ export class Tyria extends TyriaEventTarget {
const dpr = window.devicePixelRatio || 1;
const width = this.canvas.width / dpr;
const height = this.canvas.height / dpr;
+ const padding = this.view.padding;
+
const translate = this.project(this.view.center);
- const translateX = -translate[0] + (width / 2);
- const translateY = -translate[1] + (height / 2);
+ const translateX = -translate[0] + (padding.left - padding.right + width) / 2;
+ const translateY = -translate[1] + (padding.top - padding.bottom + height) / 2;
const transform = new DOMMatrix([dpr, 0, 0, dpr, translateX * dpr, translateY * dpr]);
@@ -131,6 +134,7 @@ export class Tyria extends TyriaEventTarget {
zoom: this.view.zoom,
width,
height,
+ padding,
area: this.#getViewportArea(this.view),
dpr,
debug: this.debug,
@@ -197,35 +201,35 @@ export class Tyria extends TyriaEventTarget {
ctx.resetTransform();
// render padding
- if(this.options.padding && this.debugLastViewOptions?.contain) {
- ctx.fillStyle = '#673AB788';
- ctx.strokeStyle = '#673AB7';
- ctx.lineWidth = 2 * dpr;
-
- const padding = getPadding(this.options.padding);
+ ctx.fillStyle = '#673AB788';
+ ctx.strokeStyle = '#673AB7';
+ ctx.lineWidth = 2 * dpr;
- if(padding.top) {
- ctx.fillRect(padding.left * dpr, 0, (width - padding.left - padding.right) * dpr, padding.top * dpr);
- }
- if(padding.bottom) {
- ctx.fillRect(padding.left * dpr, (height - padding.bottom) * dpr, (width - padding.left - padding.right) * dpr, height * dpr);
- }
- if(padding.left) {
- ctx.fillRect(0, 0, padding.left * dpr, height * dpr);
- }
- if(padding.right) {
- ctx.fillRect((width - padding.right) * dpr, 0, padding.right * dpr, height * dpr);
- }
- ctx.strokeRect(padding.left * dpr, padding.top * dpr, (width - padding.left - padding.right) * dpr, (height - padding.top - padding.bottom) * dpr);
+ if(padding.top) {
+ ctx.fillRect(padding.left * dpr, 0, (width - padding.left - padding.right) * dpr, padding.top * dpr);
+ }
+ if(padding.bottom) {
+ ctx.fillRect(padding.left * dpr, (height - padding.bottom) * dpr, (width - padding.left - padding.right) * dpr, height * dpr);
+ }
+ if(padding.left) {
+ ctx.fillRect(0, 0, padding.left * dpr, height * dpr);
}
+ if(padding.right) {
+ ctx.fillRect((width - padding.right) * dpr, 0, padding.right * dpr, height * dpr);
+ }
+ ctx.strokeRect(padding.left * dpr, padding.top * dpr, (width - padding.left - padding.right) * dpr, (height - padding.top - padding.bottom) * dpr);
// render map center
- ctx.setTransform(dpr, 0, 0, dpr, dpr * width / 2, dpr * height / 2);
+ ctx.setTransform(dpr, 0, 0, dpr, dpr * (padding.left - padding.right + width) / 2, dpr * (padding.top - padding.bottom + height) / 2);
ctx.fillStyle = 'lime';
ctx.fillRect(-4, -4, 8, 8);
ctx.font = '12px monospace';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
+ ctx.fillStyle = '#000';
+ ctx.fillText(`px ${translate[0]}, ${translate[1]}`, 8 + 1, 0 + 1);
+ ctx.fillText(`map ${this.view.center[0]}, ${this.view.center[1]}`, 8 + 1, 16 + 1);
+ ctx.fillText(`zoom ${this.view.zoom}`, 8 + 1, 32 + 1);
ctx.fillStyle = '#fff';
ctx.fillText(`px ${translate[0]}, ${translate[1]}`, 8, 0);
ctx.fillText(`map ${this.view.center[0]}, ${this.view.center[1]}`, 8, 16);
@@ -264,6 +268,9 @@ export class Tyria extends TyriaEventTarget {
// get dpr to correctly calculate viewport size
const dpr = window.devicePixelRatio ?? 1;
+ // get padding
+ const padding = getPadding(view.padding ?? this.options.padding);
+
// make sure the area is completely visible in the viewport
// TODO: handle passing contain + center?
if(view.contain) {
@@ -272,7 +279,6 @@ export class Tyria extends TyriaEventTarget {
const aspectRatio = size[0] / size[1];
// get size and aspect ratio of the viewport
- const padding = getPadding(this.options.padding);
const viewportSizePx = [
(this.canvas.width / dpr) - padding.left - padding.right,
(this.canvas.height / dpr) - padding.top - padding.bottom,
@@ -293,7 +299,6 @@ export class Tyria extends TyriaEventTarget {
}
// set center to the middle of the area
- // TODO: adjust for asymmetric padding
center = add(view.contain[0], multiply(size, 0.5));
// make sure we are zooming out when zoom snapping
@@ -353,17 +358,21 @@ export class Tyria extends TyriaEventTarget {
center = this.unproject(centerPx, zoom);
}
- return { center, zoom };
+ return { center, zoom, padding };
}
/** Gets the area visible in the viewport */
- #getViewportArea(view: View): Bounds {
+ #getViewportArea(view: Readonly): Bounds {
const dpr = window.devicePixelRatio ?? 1;
- const viewportHalfSizePx: Point = [this.canvas.width / dpr / 2, this.canvas.height / dpr / 2];
+ const padding = view.padding;
+ const viewportHalfSizePx: Point = [
+ (padding.left - padding.right + this.canvas.width / dpr) / 2,
+ (padding.top - padding.bottom + this.canvas.height / dpr) / 2
+ ];
const centerPx = this.project(view.center);
- const topLeft = this.unproject(subtract(centerPx, viewportHalfSizePx));
- const bottomRight = this.unproject(add(centerPx, viewportHalfSizePx));
+ const topLeft = this.unproject(subtract(subtract(centerPx, viewportHalfSizePx), 0));
+ const bottomRight = this.unproject(add(add(centerPx, viewportHalfSizePx), [padding.right - padding.left, padding.bottom - padding.top]));
return [topLeft, bottomRight];
}
@@ -399,7 +408,7 @@ export class Tyria extends TyriaEventTarget {
const target = this.resolveView(view);
// if we are not moving, don't move
- if(target.zoom === start.zoom && target.center[0] === start.center[0] && target.center[1] === start.center[1]) {
+ 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) {
return;
}
@@ -409,10 +418,10 @@ export class Tyria extends TyriaEventTarget {
const startArea = this.#getViewportArea(start);
const targetArea = this.#getViewportArea(target);
const combinedArea: Bounds = [
- [Math.min(startArea[0][0], targetArea[0][0]), Math.min(startArea[0][1], targetArea[0][1])] as Point,
- [Math.max(startArea[1][0], targetArea[1][0]), Math.max(startArea[1][1], targetArea[1][1])] as Point
+ [Math.min(startArea[0][0], targetArea[0][0]), Math.min(startArea[0][1], targetArea[0][1])],
+ [Math.max(startArea[1][0], targetArea[1][0]), Math.max(startArea[1][1], targetArea[1][1])]
];
- this.preload(this.resolveView({ contain: combinedArea }));
+ this.preload({ contain: combinedArea, padding: 0 });
// calculate delta
const deltaZoom = target.zoom - start.zoom;
@@ -439,8 +448,16 @@ export class Tyria extends TyriaEventTarget {
// calculate center
const center = add(start.center, multiply(deltaCenter, easedProgress * speedup));
+ // calculate padding
+ const padding: Padding = {
+ top: start.padding.top + (target.padding.top - start.padding.top) * easedProgress,
+ right: start.padding.right + (target.padding.right - start.padding.right) * easedProgress,
+ bottom: start.padding.bottom + (target.padding.bottom - start.padding.bottom) * easedProgress,
+ left: start.padding.left + (target.padding.left - start.padding.left) * easedProgress,
+ }
+
// set view to the calculated center and zoom
- this.view = { center, zoom };
+ this.view = { center, zoom, padding };
if(progress === 1) {
performance.mark('easeTo-end');
@@ -452,6 +469,7 @@ export class Tyria extends TyriaEventTarget {
if(duration === 0) {
// if the duration of the transition is 0 we just call the end frame
+ // TODO: why not just call `jumpTo(view)` at the start?
frame(1);
} else {
// store current ease and queue frame
@@ -498,11 +516,14 @@ export class Tyria extends TyriaEventTarget {
/** Convert a pixel in the canvas (for example offsetX/offsetY from an event) to the corresponding map coordinates at that point */
canvasPixelToMapCoordinate([x, y]: Point) {
const dpr = window.devicePixelRatio || 1;
+ const padding = this.view.padding;
- const halfWidth = this.canvas.width / dpr / 2;
- const halfHeight = this.canvas.height / dpr / 2;
+ const viewportHalfSizePx: Point = [
+ (padding.left - padding.right + this.canvas.width / dpr) / 2,
+ (padding.top - padding.bottom + this.canvas.height / dpr) / 2
+ ];
- const offset: Point = this.unproject([-x + halfWidth, -y + halfHeight]);
+ const offset: Point = this.unproject([-x + viewportHalfSizePx[0], -y + viewportHalfSizePx[1]]);
return subtract(this.view.center, offset);
}
@@ -510,10 +531,11 @@ export class Tyria extends TyriaEventTarget {
/** Convert a map coordinate to canvas px */
mapCoordinateToCanvasPixel(coordinate: Point) {
const dpr = window.devicePixelRatio || 1;
+ const padding = this.view.padding;
const viewportHalfSizePx: Point = [
- this.canvas.width / dpr / 2,
- this.canvas.height / dpr / 2
+ (padding.left - padding.right + this.canvas.width / dpr) / 2,
+ (padding.top - padding.bottom + this.canvas.height / dpr) / 2
];
const pointPx = this.project(coordinate);
@@ -543,6 +565,7 @@ export class Tyria extends TyriaEventTarget {
zoom: target.zoom,
width: this.canvas.width / dpr,
height: this.canvas.height / dpr,
+ padding: target.padding,
area: this.#getViewportArea(target),
dpr: dpr,
debug: this.debug,
@@ -612,6 +635,7 @@ export class Tyria extends TyriaEventTarget {
zoom: this.view.zoom,
width,
height,
+ padding: this.view.padding,
area: this.#getViewportArea(this.view),
dpr,
debug: this.debug,
diff --git a/packages/tyria/src/layer.ts b/packages/tyria/src/layer.ts
index 48f8b1b..eb39b4a 100644
--- a/packages/tyria/src/layer.ts
+++ b/packages/tyria/src/layer.ts
@@ -1,6 +1,6 @@
import { ImageGetOptions } from "./image-manager";
import { RenderReason } from "./render-queue";
-import { Bounds, Point } from "./types";
+import { Bounds, Padding, Point } from "./types";
import { Tyria } from "./Tyria";
export interface Layer {
@@ -23,6 +23,9 @@ export interface MapState {
/** height of the map in px */
height: number,
+ /** padding of the map area */
+ padding: Padding,
+
/** The visible area in the viewport in map coordinates */
area: Bounds,
diff --git a/packages/tyria/src/layers/TileLayer.ts b/packages/tyria/src/layers/TileLayer.ts
index bc86466..eddbe4e 100644
--- a/packages/tyria/src/layers/TileLayer.ts
+++ b/packages/tyria/src/layers/TileLayer.ts
@@ -23,7 +23,7 @@ export class TileLayer implements Layer {
: new OffscreenCanvas(0, 0);
}
- getTiles({ state, project }) {
+ getTiles({ state, project }: Pick) {
// get the zoom level of tiles to use (prefer higher resolution)
const zoom = Math.ceil(state.zoom);
@@ -46,12 +46,12 @@ export class TileLayer implements Layer {
const boundsBottomRight = project(this.options.bounds?.[1] ?? [0, 0]);
// get the top left position (px)
- const topLeftX = Math.max(center[0] - state.width / 2, boundsTopLeft[0]);
- const topLeftY = Math.max(center[1] - state.height / 2, boundsTopLeft[1]);
+ const topLeftX = Math.max(center[0] - (state.padding.left - state.padding.right + state.width) / 2, boundsTopLeft[0]);
+ const topLeftY = Math.max(center[1] - (state.padding.top - state.padding.bottom + state.height) / 2, boundsTopLeft[1]);
// get the top right position (px)
- const bottomRightX = Math.min(center[0] + state.width / 2, boundsBottomRight[0]) - 1;
- const bottomRightY = Math.min(center[1] + state.height / 2, boundsBottomRight[1]) - 1;
+ const bottomRightX = Math.min(center[0] + (-state.padding.left + state.padding.right + state.width) / 2, boundsBottomRight[0]) - 1;
+ const bottomRightY = Math.min(center[1] + (-state.padding.top + state.padding.bottom + state.height) / 2, boundsBottomRight[1]) - 1;
// convert px position to tiles
const tileTopLeft: Point = [Math.floor(topLeftX / renderedTileSize), Math.floor(topLeftY / renderedTileSize)];
diff --git a/packages/tyria/src/types.ts b/packages/tyria/src/types.ts
index 0ac0bd2..f09f66d 100644
--- a/packages/tyria/src/types.ts
+++ b/packages/tyria/src/types.ts
@@ -7,6 +7,9 @@ export type View = {
/** The zoom level of the map */
zoom: number;
+
+ /** The padding of the map */
+ padding: Padding;
}
export type ViewOptions = {
@@ -25,6 +28,9 @@ export type ViewOptions = {
/** Makes sure the viewport is completely within this area. */
cover?: Bounds;
+ /** Override map padding */
+ padding?: Partial | number;
+
/**
* Modifies the center to align with device pixels so tiles stay sharp.
* @defaultValue true