diff --git a/src/js/core/buffer_maintainer.js b/src/js/core/buffer_maintainer.js index 1d506803..3eaf1f8a 100644 --- a/src/js/core/buffer_maintainer.js +++ b/src/js/core/buffer_maintainer.js @@ -167,4 +167,25 @@ export class BufferMaintainer { }); return canvas; } + + /** + * @param {object} param0 + * @param {string} param0.key + * @param {string} param0.subKey + * @returns {HTMLCanvasElement?} + * + */ + getForKeyOrNullNoUpdate({ key, subKey }) { + let parent = this.cache.get(key); + if (!parent) { + return null; + } + + // Now search for sub key + const cacheHit = parent.get(subKey); + if (cacheHit) { + return cacheHit.canvas; + } + return null; + } } diff --git a/src/js/core/config.js b/src/js/core/config.js index 78dd99e4..dcb29e77 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -55,6 +55,7 @@ export const globalConfig = { // Map mapChunkSize: 16, + chunkAggregateSize: 4, mapChunkOverviewMinZoom: 0.9, mapChunkWorldSize: null, // COMPUTED diff --git a/src/js/game/map.js b/src/js/game/map.js index a5ec8f21..67df7db3 100644 --- a/src/js/game/map.js +++ b/src/js/game/map.js @@ -3,6 +3,7 @@ import { Vector } from "../core/vector"; import { BasicSerializableObject, types } from "../savegame/serialization"; import { BaseItem } from "./base_item"; import { Entity } from "./entity"; +import { MapChunkAggregate } from "./map_chunk_aggregate"; import { MapChunkView } from "./map_chunk_view"; import { GameRoot } from "./root"; @@ -31,6 +32,11 @@ export class BaseMap extends BasicSerializableObject { * Mapping of 'X|Y' to chunk * @type {Map} */ this.chunksById = new Map(); + + /** + * Mapping of 'X|Y' to chunk aggregate + * @type {Map} */ + this.aggregatesById = new Map(); } /** @@ -55,6 +61,39 @@ export class BaseMap extends BasicSerializableObject { return null; } + /** + * Returns the chunk aggregate containing a given chunk + * @param {number} chunkX + * @param {number} chunkY + */ + getAggregateForChunk(chunkX, chunkY, createIfNotExistent = false) { + const aggX = Math.floor(chunkX / globalConfig.chunkAggregateSize); + const aggY = Math.floor(chunkY / globalConfig.chunkAggregateSize); + return this.getAggregate(aggX, aggY, createIfNotExistent); + } + + /** + * Returns the given chunk aggregate by index + * @param {number} aggX + * @param {number} aggY + */ + getAggregate(aggX, aggY, createIfNotExistent = false) { + const aggIdentifier = aggX + "|" + aggY; + let storedAggregate; + + if ((storedAggregate = this.aggregatesById.get(aggIdentifier))) { + return storedAggregate; + } + + if (createIfNotExistent) { + const instance = new MapChunkAggregate(this.root, aggX, aggY); + this.aggregatesById.set(aggIdentifier, instance); + return instance; + } + + return null; + } + /** * Gets or creates a new chunk if not existent for the given tile * @param {number} tileX diff --git a/src/js/game/map_chunk_aggregate.js b/src/js/game/map_chunk_aggregate.js new file mode 100644 index 00000000..67b4b45e --- /dev/null +++ b/src/js/game/map_chunk_aggregate.js @@ -0,0 +1,146 @@ +import { globalConfig } from "../core/config"; +import { DrawParameters } from "../core/draw_parameters"; +import { MapChunk } from "./map_chunk"; +import { GameRoot } from "./root"; +import { drawSpriteClipped } from "../core/draw_utils"; + +export const CHUNK_OVERLAY_RES = 3; + +export class MapChunkAggregate { + /** + * + * @param {GameRoot} root + * @param {number} x + * @param {number} y + */ + constructor(root, x, y) { + this.root = root; + this.x = x; + this.y = y; + + /** + * Whenever something changes, we increase this number - so we know we need to redraw + */ + this.renderIteration = 0; + this.dirty = false; + /** @type {Array} */ + this.dirtyList = new Array(globalConfig.chunkAggregateSize ** 2).fill(true); + this.markDirty(0, 0); + } + + /** + * Marks this chunk as dirty, rerendering all caches + * @param {number} chunkX + * @param {number} chunkY + */ + markDirty(chunkX, chunkY) { + const relX = chunkX % globalConfig.chunkAggregateSize; + const relY = chunkY % globalConfig.chunkAggregateSize; + this.dirtyList[relY * globalConfig.chunkAggregateSize + relX] = true; + if (this.dirty) { + return; + } + this.dirty = true; + ++this.renderIteration; + this.renderKey = this.x + "/" + this.y + "@" + this.renderIteration; + } + + /** + * + * @param {HTMLCanvasElement} canvas + * @param {CanvasRenderingContext2D} context + * @param {number} w + * @param {number} h + * @param {number} dpi + */ + generateOverlayBuffer(canvas, context, w, h, dpi) { + const prevKey = this.x + "/" + this.y + "@" + (this.renderIteration - 1); + const prevBuffer = this.root.buffers.getForKeyOrNullNoUpdate({ + key: "agg@" + this.root.currentLayer, + subKey: prevKey, + }); + + const overlaySize = globalConfig.mapChunkSize * CHUNK_OVERLAY_RES; + let onlyDirty = false; + if (prevBuffer) { + context.drawImage(prevBuffer, 0, 0); + onlyDirty = true; + } + + for (let x = 0; x < globalConfig.chunkAggregateSize; x++) { + for (let y = 0; y < globalConfig.chunkAggregateSize; y++) { + if (onlyDirty && !this.dirtyList[globalConfig.chunkAggregateSize * y + x]) continue; + this.root.map + .getChunk( + this.x * globalConfig.chunkAggregateSize + x, + this.y * globalConfig.chunkAggregateSize + y, + true + ) + .generateOverlayBuffer( + context, + overlaySize, + overlaySize, + x * overlaySize, + y * overlaySize + ); + } + } + + this.dirty = false; + this.dirtyList.fill(false); + } + + /** + * Overlay + * @param {DrawParameters} parameters + */ + drawOverlay(parameters) { + const aggregateOverlaySize = + globalConfig.mapChunkSize * globalConfig.chunkAggregateSize * CHUNK_OVERLAY_RES; + const sprite = this.root.buffers.getForKey({ + key: "agg@" + this.root.currentLayer, + subKey: this.renderKey, + w: aggregateOverlaySize, + h: aggregateOverlaySize, + dpi: 1, + redrawMethod: this.generateOverlayBuffer.bind(this), + }); + + const dims = globalConfig.mapChunkWorldSize * globalConfig.chunkAggregateSize; + const extrude = 0.05; + + // Draw chunk "pixel" art + parameters.context.imageSmoothingEnabled = false; + drawSpriteClipped({ + parameters, + sprite, + x: this.x * dims - extrude, + y: this.y * dims - extrude, + w: dims + 2 * extrude, + h: dims + 2 * extrude, + originalW: aggregateOverlaySize, + originalH: aggregateOverlaySize, + }); + + parameters.context.imageSmoothingEnabled = true; + const resourcesScale = this.root.app.settings.getAllSettings().mapResourcesScale; + + // Draw patch items + if (this.root.currentLayer === "regular" && resourcesScale > 0.05) { + const diameter = (70 / Math.pow(parameters.zoomLevel, 0.35)) * (0.2 + 2 * resourcesScale); + + for (let x = 0; x < globalConfig.chunkAggregateSize; x++) { + for (let y = 0; y < globalConfig.chunkAggregateSize; y++) { + this.root.map + .getChunk(this.x + x, this.y + y, true) + .drawOverlayPatches( + parameters, + this.x * dims + x * globalConfig.mapChunkSize * CHUNK_OVERLAY_RES, + this.y * dims + y * globalConfig.mapChunkSize * CHUNK_OVERLAY_RES, + diameter + ); + } + } + } + } +} diff --git a/src/js/game/map_chunk_view.js b/src/js/game/map_chunk_view.js index 131ce37b..947b7a9f 100644 --- a/src/js/game/map_chunk_view.js +++ b/src/js/game/map_chunk_view.js @@ -33,6 +33,7 @@ export class MapChunkView extends MapChunk { markDirty() { ++this.renderIteration; this.renderKey = this.x + "/" + this.y + "@" + this.renderIteration; + this.root.map.getAggregateForChunk(this.x, this.y, true).markDirty(this.x, this.y); } /** @@ -82,73 +83,41 @@ export class MapChunkView extends MapChunk { } /** - * Overlay * @param {DrawParameters} parameters + * @param {number} xoffs + * @param {number} yoffs + * @param {number} diameter */ - drawOverlay(parameters) { - const overlaySize = globalConfig.mapChunkSize * CHUNK_OVERLAY_RES; - const sprite = this.root.buffers.getForKey({ - key: "chunk@" + this.root.currentLayer, - subKey: this.renderKey, - w: overlaySize, - h: overlaySize, - dpi: 1, - redrawMethod: this.generateOverlayBuffer.bind(this), - }); - - const dims = globalConfig.mapChunkWorldSize; - const extrude = 0.05; - - // Draw chunk "pixel" art - parameters.context.imageSmoothingEnabled = false; - drawSpriteClipped({ - parameters, - sprite, - x: this.x * dims - extrude, - y: this.y * dims - extrude, - w: dims + 2 * extrude, - h: dims + 2 * extrude, - originalW: overlaySize, - originalH: overlaySize, - }); - - parameters.context.imageSmoothingEnabled = true; - const resourcesScale = this.root.app.settings.getAllSettings().mapResourcesScale; - - // Draw patch items - if (this.root.currentLayer === "regular" && resourcesScale > 0.05) { - const diameter = (70 / Math.pow(parameters.zoomLevel, 0.35)) * (0.2 + 2 * resourcesScale); - - for (let i = 0; i < this.patches.length; ++i) { - const patch = this.patches[i]; - if (patch.item.getItemType() === "shape") { - const destX = this.x * dims + patch.pos.x * globalConfig.tileSize; - const destY = this.y * dims + patch.pos.y * globalConfig.tileSize; - patch.item.drawItemCenteredClipped(destX, destY, parameters, diameter); - } + drawOverlayPatches(parameters, xoffs, yoffs, diameter) { + for (let i = 0; i < this.patches.length; ++i) { + const patch = this.patches[i]; + if (patch.item.getItemType() === "shape") { + const destX = xoffs + patch.pos.x * globalConfig.tileSize; + const destY = yoffs + patch.pos.y * globalConfig.tileSize; + patch.item.drawItemCenteredClipped(destX, destY, parameters, diameter); } } } /** * - * @param {HTMLCanvasElement} canvas * @param {CanvasRenderingContext2D} context * @param {number} w * @param {number} h - * @param {number} dpi + * @param {number=} xoffs + * @param {number=} yoffs */ - generateOverlayBuffer(canvas, context, w, h, dpi) { + generateOverlayBuffer(context, w, h, xoffs, yoffs) { context.fillStyle = this.containedEntities.length > 0 ? THEME.map.chunkOverview.filled : THEME.map.chunkOverview.empty; - context.fillRect(0, 0, w, h); + context.fillRect(xoffs, yoffs, w, h); if (this.root.app.settings.getAllSettings().displayChunkBorders) { context.fillStyle = THEME.map.chunkBorders; - context.fillRect(0, 0, w, 1); - context.fillRect(0, 1, 1, h); + context.fillRect(xoffs, yoffs, w, 1); + context.fillRect(xoffs, yoffs + 1, 1, h); } for (let x = 0; x < globalConfig.mapChunkSize; ++x) { @@ -174,8 +143,8 @@ export class MapChunkView extends MapChunk { if (lowerContent) { context.fillStyle = lowerContent.getBackgroundColorAsResource(); context.fillRect( - x * CHUNK_OVERLAY_RES, - y * CHUNK_OVERLAY_RES, + xoffs + x * CHUNK_OVERLAY_RES, + yoffs + y * CHUNK_OVERLAY_RES, CHUNK_OVERLAY_RES, CHUNK_OVERLAY_RES ); @@ -190,8 +159,8 @@ export class MapChunkView extends MapChunk { const isFilled = overlayMatrix[dx + dy * 3]; if (isFilled) { context.fillRect( - x * CHUNK_OVERLAY_RES + dx, - y * CHUNK_OVERLAY_RES + dy, + xoffs + x * CHUNK_OVERLAY_RES + dx, + yoffs + y * CHUNK_OVERLAY_RES + dy, 1, 1 ); @@ -206,8 +175,8 @@ export class MapChunkView extends MapChunk { data.rotationVariant ); context.fillRect( - x * CHUNK_OVERLAY_RES, - y * CHUNK_OVERLAY_RES, + xoffs + x * CHUNK_OVERLAY_RES, + yoffs + y * CHUNK_OVERLAY_RES, CHUNK_OVERLAY_RES, CHUNK_OVERLAY_RES ); @@ -220,8 +189,8 @@ export class MapChunkView extends MapChunk { if (lowerContent) { context.fillStyle = lowerContent.getBackgroundColorAsResource(); context.fillRect( - x * CHUNK_OVERLAY_RES, - y * CHUNK_OVERLAY_RES, + xoffs + x * CHUNK_OVERLAY_RES, + yoffs + y * CHUNK_OVERLAY_RES, CHUNK_OVERLAY_RES, CHUNK_OVERLAY_RES ); @@ -233,7 +202,7 @@ export class MapChunkView extends MapChunk { // Draw wires overlay context.fillStyle = THEME.map.wires.overlayColor; - context.fillRect(0, 0, w, h); + context.fillRect(xoffs, yoffs, w, h); for (let x = 0; x < globalConfig.mapChunkSize; ++x) { const wiresArray = this.wireContents[x]; @@ -244,8 +213,8 @@ export class MapChunkView extends MapChunk { } MapChunkView.drawSingleWiresOverviewTile({ context, - x: x * CHUNK_OVERLAY_RES, - y: y * CHUNK_OVERLAY_RES, + x: xoffs + x * CHUNK_OVERLAY_RES, + y: yoffs + y * CHUNK_OVERLAY_RES, entity: content, tileSizePixels: CHUNK_OVERLAY_RES, }); diff --git a/src/js/game/map_view.js b/src/js/game/map_view.js index f4372a51..c6078ea9 100644 --- a/src/js/game/map_view.js +++ b/src/js/game/map_view.js @@ -5,6 +5,7 @@ import { freeCanvas, makeOffscreenBuffer } from "../core/buffer_utils"; import { Entity } from "./entity"; import { THEME } from "./theme"; import { MapChunkView } from "./map_chunk_view"; +import { MapChunkAggregate } from "./map_chunk_aggregate"; /** * This is the view of the map, it extends the map which is the raw model and allows @@ -164,6 +165,40 @@ export class MapView extends BaseMap { } } + /** + * Calls a given method on all given chunks + * @param {DrawParameters} parameters + * @param {function} method + */ + drawVisibleAggregates(parameters, method) { + const cullRange = parameters.visibleRect.allScaled(1 / globalConfig.tileSize); + const top = cullRange.top(); + const right = cullRange.right(); + const bottom = cullRange.bottom(); + const left = cullRange.left(); + + const border = 0; + const minY = top - border; + const maxY = bottom + border; + const minX = left - border; + const maxX = right + border; + + const aggregateTiles = globalConfig.chunkAggregateSize * globalConfig.mapChunkSize; + const aggStartX = Math.floor(minX / aggregateTiles); + const aggStartY = Math.floor(minY / aggregateTiles); + + const aggEndX = Math.floor(maxX / aggregateTiles); + const aggEndY = Math.floor(maxY / aggregateTiles); + + // Render y from top down for proper blending + for (let aggX = aggStartX; aggX <= aggEndX; ++aggX) { + for (let aggY = aggStartY; aggY <= aggEndY; ++aggY) { + const aggregate = this.root.map.getAggregate(aggX, aggY, true); + method.call(aggregate, parameters); + } + } + } + /** * Draws the wires foreground * @param {DrawParameters} parameters @@ -177,7 +212,7 @@ export class MapView extends BaseMap { * @param {DrawParameters} parameters */ drawOverlay(parameters) { - this.drawVisibleChunks(parameters, MapChunkView.prototype.drawOverlay); + this.drawVisibleAggregates(parameters, MapChunkAggregate.prototype.drawOverlay); } /**