Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/tyria/dev/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
debug overlays
</label>
<button id="lionsarch">Lion's Arch</button>
<button id="lionsarch2">Lion's Arch (padding)</button>
<button id="ascalon">Ascalon</button>
<button id="horn">Horn of Maguuma</button>
<button id="cantha">Cantha</button>
Expand All @@ -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({
Expand Down Expand Up @@ -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]] }))
Expand Down
106 changes: 65 additions & 41 deletions packages/tyria/src/Tyria.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -13,7 +13,8 @@ export class Tyria extends TyriaEventTarget {

view: Readonly<View> = {
center: [0, 0],
zoom: 1
zoom: 1,
padding: { top: 0, right: 0, bottom: 0, left: 0 },
}
layers: { id: number, layer: Layer }[] = [];
debug = false
Expand Down Expand Up @@ -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]);

Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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<View>): 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];
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
Expand All @@ -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');
Expand All @@ -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
Expand Down Expand Up @@ -498,22 +516,26 @@ 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);
}

/** 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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion packages/tyria/src/layer.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,

Expand Down
10 changes: 5 additions & 5 deletions packages/tyria/src/layers/TileLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class TileLayer implements Layer {
: new OffscreenCanvas(0, 0);
}

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

Expand All @@ -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)];
Expand Down
6 changes: 6 additions & 0 deletions packages/tyria/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -25,6 +28,9 @@ export type ViewOptions = {
/** Makes sure the viewport is completely within this area. */
cover?: Bounds;

/** Override map padding */
padding?: Partial<Padding> | number;

/**
* Modifies the center to align with device pixels so tiles stay sharp.
* @defaultValue true
Expand Down