nexus-2d 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.cjs +1674 -11
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +1674 -11
- package/dist/esm/index.js.map +1 -1
- package/dist/types/engine/PlatformController.d.ts +11 -0
- package/dist/types/engine/tiled.d.ts +19 -0
- package/dist/types/engine/tiledcache.d.ts +199 -0
- package/dist/types/engine/tiledprofiler.d.ts +30 -0
- package/package.json +2 -2
package/dist/cjs/index.cjs
CHANGED
|
@@ -59876,6 +59876,963 @@ class SpriteAtlas {
|
|
|
59876
59876
|
}
|
|
59877
59877
|
}
|
|
59878
59878
|
|
|
59879
|
+
// tilemap-profiler.js
|
|
59880
|
+
// Instruments tilemap rendering to expose the waste
|
|
59881
|
+
|
|
59882
|
+
class TilemapProfiler {
|
|
59883
|
+
constructor() {
|
|
59884
|
+
this.reset();
|
|
59885
|
+
}
|
|
59886
|
+
|
|
59887
|
+
reset() {
|
|
59888
|
+
this.frameCount = 0;
|
|
59889
|
+
this.totalPixelsTouched = 0;
|
|
59890
|
+
this.totalBlitCalls = 0;
|
|
59891
|
+
this.totalChunksDrawn = 0;
|
|
59892
|
+
this.cameraMovedFrames = 0;
|
|
59893
|
+
this.cameraStaticFrames = 0;
|
|
59894
|
+
this.lastCameraPos = { x: 0, y: 0 };
|
|
59895
|
+
}
|
|
59896
|
+
|
|
59897
|
+
startFrame(camera) {
|
|
59898
|
+
this.frameCount++;
|
|
59899
|
+
|
|
59900
|
+
// Track if camera moved
|
|
59901
|
+
const moved = camera.worldPosition.x !== this.lastCameraPos.x ||
|
|
59902
|
+
camera.worldPosition.y !== this.lastCameraPos.y;
|
|
59903
|
+
|
|
59904
|
+
if (moved) {
|
|
59905
|
+
this.cameraMovedFrames++;
|
|
59906
|
+
} else {
|
|
59907
|
+
this.cameraStaticFrames++;
|
|
59908
|
+
}
|
|
59909
|
+
|
|
59910
|
+
this.lastCameraPos.x = camera.worldPosition.x;
|
|
59911
|
+
this.lastCameraPos.y = camera.worldPosition.y;
|
|
59912
|
+
}
|
|
59913
|
+
|
|
59914
|
+
recordBlit(width, height) {
|
|
59915
|
+
this.totalBlitCalls++;
|
|
59916
|
+
this.totalPixelsTouched += width * height;
|
|
59917
|
+
}
|
|
59918
|
+
|
|
59919
|
+
recordChunk() {
|
|
59920
|
+
this.totalChunksDrawn++;
|
|
59921
|
+
}
|
|
59922
|
+
|
|
59923
|
+
getReport() {
|
|
59924
|
+
if (this.frameCount === 0) return "No frames recorded";
|
|
59925
|
+
|
|
59926
|
+
const avgPixelsPerFrame = this.totalPixelsTouched / this.frameCount;
|
|
59927
|
+
const avgBlitsPerFrame = this.totalBlitCalls / this.frameCount;
|
|
59928
|
+
const avgChunksPerFrame = this.totalChunksDrawn / this.frameCount;
|
|
59929
|
+
const wasteRatio = this.cameraStaticFrames / this.frameCount;
|
|
59930
|
+
|
|
59931
|
+
return {
|
|
59932
|
+
totalFrames: this.frameCount,
|
|
59933
|
+
cameraMovedFrames: this.cameraMovedFrames,
|
|
59934
|
+
cameraStaticFrames: this.cameraStaticFrames,
|
|
59935
|
+
wasteRatio: (wasteRatio * 100).toFixed(1) + '%',
|
|
59936
|
+
avgPixelsPerFrame: avgPixelsPerFrame.toFixed(0),
|
|
59937
|
+
avgBlitsPerFrame: avgBlitsPerFrame.toFixed(1),
|
|
59938
|
+
avgChunksPerFrame: avgChunksPerFrame.toFixed(1),
|
|
59939
|
+
totalPixelsTouched: this.totalPixelsTouched,
|
|
59940
|
+
wastedPixels: (avgPixelsPerFrame * this.cameraStaticFrames).toFixed(0),
|
|
59941
|
+
|
|
59942
|
+
// The smoking gun
|
|
59943
|
+
redundantWork: `${this.cameraStaticFrames} frames redrew ${avgPixelsPerFrame.toFixed(0)} pixels each for ZERO camera movement`
|
|
59944
|
+
};
|
|
59945
|
+
}
|
|
59946
|
+
|
|
59947
|
+
printReport() {
|
|
59948
|
+
const r = this.getReport();
|
|
59949
|
+
console.log('\n=== TILEMAP RENDER WASTE REPORT ===');
|
|
59950
|
+
console.log(`Total frames analyzed: ${r.totalFrames}`);
|
|
59951
|
+
console.log(`Camera moved: ${r.cameraMovedFrames} frames`);
|
|
59952
|
+
console.log(`Camera static: ${r.cameraStaticFrames} frames`);
|
|
59953
|
+
console.log(`Waste ratio: ${r.wasteRatio} of frames were redundant`);
|
|
59954
|
+
console.log(`\nPer-frame averages:`);
|
|
59955
|
+
console.log(` Pixels touched: ${r.avgPixelsPerFrame}`);
|
|
59956
|
+
console.log(` Blit calls: ${r.avgBlitsPerFrame}`);
|
|
59957
|
+
console.log(` Chunks drawn: ${r.avgChunksPerFrame}`);
|
|
59958
|
+
console.log(`\nTotal waste:`);
|
|
59959
|
+
console.log(` Pixels touched: ${r.totalPixelsTouched.toLocaleString()}`);
|
|
59960
|
+
console.log(` Wasted pixels (static frames): ${r.wastedPixels.toLocaleString()}`);
|
|
59961
|
+
console.log(`\n⚠️ ${r.redundantWork}`);
|
|
59962
|
+
console.log('=====================================\n');
|
|
59963
|
+
}
|
|
59964
|
+
}
|
|
59965
|
+
|
|
59966
|
+
// Singleton instance
|
|
59967
|
+
const tilemapProfiler = new TilemapProfiler();
|
|
59968
|
+
|
|
59969
|
+
CacheBuffer.prototype.markRegion = (...args) => { }; // trickc
|
|
59970
|
+
|
|
59971
|
+
|
|
59972
|
+
|
|
59973
|
+
class CameraTracker {
|
|
59974
|
+
constructor() {
|
|
59975
|
+
this.lastPosition = { x: 0, y: 0 };
|
|
59976
|
+
this.lastZoom = 1.0;
|
|
59977
|
+
this.currentPosition = { x: 0, y: 0 };
|
|
59978
|
+
this.currentZoom = 1.0;
|
|
59979
|
+
|
|
59980
|
+
// Movement stats
|
|
59981
|
+
this.totalFrames = 0;
|
|
59982
|
+
this.staticFrames = 0;
|
|
59983
|
+
this.smallMoveFrames = 0; // < threshold
|
|
59984
|
+
this.largeMoveFrames = 0; // >= threshold
|
|
59985
|
+
this.teleportFrames = 0; // way outside cache
|
|
59986
|
+
|
|
59987
|
+
// Thresholds (pixels)
|
|
59988
|
+
this.smallMoveThreshold = 32; // moves smaller than this are "small"
|
|
59989
|
+
this.teleportThreshold = 256; // moves larger than this are "teleports"
|
|
59990
|
+
}
|
|
59991
|
+
|
|
59992
|
+
// this at start of each frame
|
|
59993
|
+
update(camera) {
|
|
59994
|
+
this.totalFrames++;
|
|
59995
|
+
|
|
59996
|
+
this.currentPosition.x = camera.worldPosition.x;
|
|
59997
|
+
this.currentPosition.y = camera.worldPosition.y;
|
|
59998
|
+
this.currentZoom = camera.zoom || camera.worldZoom || 1.0;
|
|
59999
|
+
|
|
60000
|
+
const delta = this.getDelta();
|
|
60001
|
+
const distance = Math.sqrt(delta.x * delta.x + delta.y * delta.y);
|
|
60002
|
+
|
|
60003
|
+
// Classify movement
|
|
60004
|
+
if (delta.x === 0 && delta.y === 0 && delta.zoom === 0) {
|
|
60005
|
+
this.staticFrames++;
|
|
60006
|
+
} else if (distance < this.smallMoveThreshold) {
|
|
60007
|
+
this.smallMoveFrames++;
|
|
60008
|
+
} else if (distance < this.teleportThreshold) {
|
|
60009
|
+
this.largeMoveFrames++;
|
|
60010
|
+
} else {
|
|
60011
|
+
this.teleportFrames++;
|
|
60012
|
+
}
|
|
60013
|
+
}
|
|
60014
|
+
|
|
60015
|
+
// Get movement delta since last frame
|
|
60016
|
+
getDelta() {
|
|
60017
|
+
return {
|
|
60018
|
+
x: this.currentPosition.x - this.lastPosition.x,
|
|
60019
|
+
y: this.currentPosition.y - this.lastPosition.y,
|
|
60020
|
+
zoom: this.currentZoom - this.lastZoom,
|
|
60021
|
+
magnitude: Math.sqrt(
|
|
60022
|
+
Math.pow(this.currentPosition.x - this.lastPosition.x, 2) +
|
|
60023
|
+
Math.pow(this.currentPosition.y - this.lastPosition.y, 2)
|
|
60024
|
+
)
|
|
60025
|
+
};
|
|
60026
|
+
}
|
|
60027
|
+
|
|
60028
|
+
// Check if camera moved at all
|
|
60029
|
+
hasMoved() {
|
|
60030
|
+
const d = this.getDelta();
|
|
60031
|
+
return d.x !== 0 || d.y !== 0 || d.zoom !== 0;
|
|
60032
|
+
}
|
|
60033
|
+
|
|
60034
|
+
// Check if movement is small enough for incremental update
|
|
60035
|
+
isSmallMove() {
|
|
60036
|
+
return this.getDelta().magnitude < this.smallMoveThreshold;
|
|
60037
|
+
}
|
|
60038
|
+
|
|
60039
|
+
// Check if this is a teleport (need full redraw)
|
|
60040
|
+
isTeleport() {
|
|
60041
|
+
return this.getDelta().magnitude >= this.teleportThreshold;
|
|
60042
|
+
}
|
|
60043
|
+
|
|
60044
|
+
// Check if zoom changed
|
|
60045
|
+
zoomChanged() {
|
|
60046
|
+
return this.currentZoom !== this.lastZoom;
|
|
60047
|
+
}
|
|
60048
|
+
|
|
60049
|
+
// Call at end of frame to commit current position as "last"
|
|
60050
|
+
commit() {
|
|
60051
|
+
this.lastPosition.x = this.currentPosition.x;
|
|
60052
|
+
this.lastPosition.y = this.currentPosition.y;
|
|
60053
|
+
this.lastZoom = this.currentZoom;
|
|
60054
|
+
}
|
|
60055
|
+
|
|
60056
|
+
// Get movement direction (for determining which edge to update)
|
|
60057
|
+
getDirection() {
|
|
60058
|
+
const delta = this.getDelta();
|
|
60059
|
+
const absX = Math.abs(delta.x);
|
|
60060
|
+
const absY = Math.abs(delta.y);
|
|
60061
|
+
|
|
60062
|
+
// Primary direction is the larger component
|
|
60063
|
+
if (absX > absY) {
|
|
60064
|
+
return delta.x > 0 ? 'right' : 'left';
|
|
60065
|
+
} else if (absY > absX) {
|
|
60066
|
+
return delta.y > 0 ? 'down' : 'up';
|
|
60067
|
+
} else if (absX === absY && absX > 0) {
|
|
60068
|
+
// Diagonal - return both directions
|
|
60069
|
+
const h = delta.x > 0 ? 'right' : 'left';
|
|
60070
|
+
const v = delta.y > 0 ? 'down' : 'up';
|
|
60071
|
+
return `${v}-${h}`; // e.g., "down-right"
|
|
60072
|
+
}
|
|
60073
|
+
|
|
60074
|
+
return 'none';
|
|
60075
|
+
}
|
|
60076
|
+
|
|
60077
|
+
// Get stats about camera movement patterns
|
|
60078
|
+
getStats() {
|
|
60079
|
+
if (this.totalFrames === 0) return null;
|
|
60080
|
+
|
|
60081
|
+
return {
|
|
60082
|
+
totalFrames: this.totalFrames,
|
|
60083
|
+
staticFrames: this.staticFrames,
|
|
60084
|
+
smallMoveFrames: this.smallMoveFrames,
|
|
60085
|
+
largeMoveFrames: this.largeMoveFrames,
|
|
60086
|
+
teleportFrames: this.teleportFrames,
|
|
60087
|
+
staticRatio: (this.staticFrames / this.totalFrames * 100).toFixed(1) + '%',
|
|
60088
|
+
smallMoveRatio: (this.smallMoveFrames / this.totalFrames * 100).toFixed(1) + '%',
|
|
60089
|
+
largeMoveRatio: (this.largeMoveFrames / this.totalFrames * 100).toFixed(1) + '%',
|
|
60090
|
+
teleportRatio: (this.teleportFrames / this.totalFrames * 100).toFixed(1) + '%'
|
|
60091
|
+
};
|
|
60092
|
+
}
|
|
60093
|
+
|
|
60094
|
+
printStats() {
|
|
60095
|
+
const s = this.getStats();
|
|
60096
|
+
if (!s) {
|
|
60097
|
+
console.log('No camera movement data yet');
|
|
60098
|
+
return;
|
|
60099
|
+
}
|
|
60100
|
+
|
|
60101
|
+
console.log('\n=== CAMERA MOVEMENT STATS ===');
|
|
60102
|
+
console.log(`Total frames: ${s.totalFrames}`);
|
|
60103
|
+
console.log(`Static (no movement): ${s.staticFrames} (${s.staticRatio})`);
|
|
60104
|
+
console.log(`Small moves (<32px): ${s.smallMoveFrames} (${s.smallMoveRatio})`);
|
|
60105
|
+
console.log(`Large moves (32-256px): ${s.largeMoveFrames} (${s.largeMoveRatio})`);
|
|
60106
|
+
console.log(`Teleports (>256px): ${s.teleportFrames} (${s.teleportRatio})`);
|
|
60107
|
+
console.log('============================\n');
|
|
60108
|
+
}
|
|
60109
|
+
}
|
|
60110
|
+
|
|
60111
|
+
|
|
60112
|
+
class CacheUpdateStrategy {
|
|
60113
|
+
static determine(delta, cacheMargin) {
|
|
60114
|
+
const dist = delta.magnitude;
|
|
60115
|
+
|
|
60116
|
+
// Zoom change always requires full redraw
|
|
60117
|
+
if (delta.zoom !== 0) {
|
|
60118
|
+
return { type: 'FULL_REDRAW', reason: 'zoom changed' };
|
|
60119
|
+
}
|
|
60120
|
+
|
|
60121
|
+
// No movement - no update needed
|
|
60122
|
+
if (dist === 0) {
|
|
60123
|
+
return { type: 'NONE', reason: 'camera static' };
|
|
60124
|
+
}
|
|
60125
|
+
|
|
60126
|
+
// Teleport - full redraw
|
|
60127
|
+
if (dist > cacheMargin * 0.8) {
|
|
60128
|
+
return { type: 'FULL_REDRAW', reason: 'teleport (outside cache margin)' };
|
|
60129
|
+
}
|
|
60130
|
+
|
|
60131
|
+
// Small move - incremental update
|
|
60132
|
+
if (dist < cacheMargin * 0.5) {
|
|
60133
|
+
return {
|
|
60134
|
+
type: 'INCREMENTAL',
|
|
60135
|
+
reason: 'small move (within cache margin)',
|
|
60136
|
+
delta: delta
|
|
60137
|
+
};
|
|
60138
|
+
}
|
|
60139
|
+
|
|
60140
|
+
// Medium move - still incremental but closer to limit
|
|
60141
|
+
return {
|
|
60142
|
+
type: 'INCREMENTAL',
|
|
60143
|
+
reason: 'medium move',
|
|
60144
|
+
delta: delta
|
|
60145
|
+
};
|
|
60146
|
+
}
|
|
60147
|
+
}
|
|
60148
|
+
|
|
60149
|
+
// shifting
|
|
60150
|
+
|
|
60151
|
+
class CacheShifter {
|
|
60152
|
+
|
|
60153
|
+
// Shift buffer content left by dx pixels
|
|
60154
|
+
// This is called when camera moves RIGHT
|
|
60155
|
+
static shiftLeft(buffer, dx) {
|
|
60156
|
+
if (dx <= 0 || dx >= buffer.width) return;
|
|
60157
|
+
|
|
60158
|
+
const data = buffer.data;
|
|
60159
|
+
const width = buffer.width;
|
|
60160
|
+
const height = buffer.height;
|
|
60161
|
+
const shiftPixels = Math.floor(dx);
|
|
60162
|
+
|
|
60163
|
+
// Process row by row
|
|
60164
|
+
// Each row: copy pixels from [shiftPixels...width] to [0...width-shiftPixels]
|
|
60165
|
+
for (let y = 0; y < height; y++) {
|
|
60166
|
+
const rowStart = y * width * 4;
|
|
60167
|
+
const srcStart = rowStart + shiftPixels * 4;
|
|
60168
|
+
const copyWidth = (width - shiftPixels) * 4;
|
|
60169
|
+
|
|
60170
|
+
// Copy row content leftward
|
|
60171
|
+
data.copyWithin(rowStart, srcStart, srcStart + copyWidth);
|
|
60172
|
+
}
|
|
60173
|
+
}
|
|
60174
|
+
|
|
60175
|
+
// Shift buffer content right by dx pixels
|
|
60176
|
+
// Called when camera moves LEFT
|
|
60177
|
+
static shiftRight(buffer, dx) {
|
|
60178
|
+
if (dx <= 0 || dx >= buffer.width) return;
|
|
60179
|
+
|
|
60180
|
+
const data = buffer.data;
|
|
60181
|
+
const width = buffer.width;
|
|
60182
|
+
const height = buffer.height;
|
|
60183
|
+
const shiftPixels = Math.floor(dx);
|
|
60184
|
+
|
|
60185
|
+
// Process row by row, BACKWARDS to avoid overwriting
|
|
60186
|
+
for (let y = height - 1; y >= 0; y--) {
|
|
60187
|
+
const rowStart = y * width * 4;
|
|
60188
|
+
const dstStart = rowStart + shiftPixels * 4;
|
|
60189
|
+
const copyWidth = (width - shiftPixels) * 4;
|
|
60190
|
+
|
|
60191
|
+
// Copy row content rightward
|
|
60192
|
+
data.copyWithin(dstStart, rowStart, rowStart + copyWidth);
|
|
60193
|
+
}
|
|
60194
|
+
}
|
|
60195
|
+
|
|
60196
|
+
// Shift buffer content up by dy pixels
|
|
60197
|
+
// Called when camera moves DOWN
|
|
60198
|
+
static shiftUp(buffer, dy) {
|
|
60199
|
+
if (dy <= 0 || dy >= buffer.height) return;
|
|
60200
|
+
|
|
60201
|
+
const data = buffer.data;
|
|
60202
|
+
const width = buffer.width;
|
|
60203
|
+
const height = buffer.height;
|
|
60204
|
+
const shiftRows = Math.floor(dy);
|
|
60205
|
+
const rowBytes = width * 4;
|
|
60206
|
+
|
|
60207
|
+
// Copy rows upward
|
|
60208
|
+
// Move rows [shiftRows...height] to [0...height-shiftRows]
|
|
60209
|
+
const srcStart = shiftRows * rowBytes;
|
|
60210
|
+
const copyBytes = (height - shiftRows) * rowBytes;
|
|
60211
|
+
|
|
60212
|
+
data.copyWithin(0, srcStart, srcStart + copyBytes);
|
|
60213
|
+
}
|
|
60214
|
+
|
|
60215
|
+
// Shift buffer content down by dy pixels
|
|
60216
|
+
// Called when camera moves UP
|
|
60217
|
+
static shiftDown(buffer, dy) {
|
|
60218
|
+
if (dy <= 0 || dy >= buffer.height) return;
|
|
60219
|
+
|
|
60220
|
+
const data = buffer.data;
|
|
60221
|
+
const width = buffer.width;
|
|
60222
|
+
const height = buffer.height;
|
|
60223
|
+
const shiftRows = Math.floor(dy);
|
|
60224
|
+
const rowBytes = width * 4;
|
|
60225
|
+
|
|
60226
|
+
// Copy rows downward, process backwards to avoid overwrite
|
|
60227
|
+
const srcBytes = (height - shiftRows) * rowBytes;
|
|
60228
|
+
const dstStart = shiftRows * rowBytes;
|
|
60229
|
+
|
|
60230
|
+
data.copyWithin(dstStart, 0, srcBytes);
|
|
60231
|
+
}
|
|
60232
|
+
|
|
60233
|
+
// Combined shift for diagonal movement
|
|
60234
|
+
// Does horizontal first, then vertical
|
|
60235
|
+
static shift(buffer, dx, dy) {
|
|
60236
|
+
// Horizontal shift
|
|
60237
|
+
if (dx > 0) {
|
|
60238
|
+
this.shiftLeft(buffer, dx);
|
|
60239
|
+
} else if (dx < 0) {
|
|
60240
|
+
this.shiftRight(buffer, -dx);
|
|
60241
|
+
}
|
|
60242
|
+
|
|
60243
|
+
// Vertical shift
|
|
60244
|
+
if (dy > 0) {
|
|
60245
|
+
this.shiftUp(buffer, dy);
|
|
60246
|
+
} else if (dy < 0) {
|
|
60247
|
+
this.shiftDown(buffer, -dy);
|
|
60248
|
+
}
|
|
60249
|
+
}
|
|
60250
|
+
}
|
|
60251
|
+
|
|
60252
|
+
// ============= THE FIX =============
|
|
60253
|
+
// The key insight: we need to calculate world regions BEFORE updating worldX/worldY
|
|
60254
|
+
// because the regions represent what NEW content to draw, relative to the OLD cache position
|
|
60255
|
+
|
|
60256
|
+
class ShiftRegionCalculator {
|
|
60257
|
+
static calculateNewRegions(oldWorldX, oldWorldY, cacheWidth, cacheHeight, deltaX, deltaY) {
|
|
60258
|
+
const regions = [];
|
|
60259
|
+
|
|
60260
|
+
// deltaX/Y represent camera movement in world space
|
|
60261
|
+
// We calculate which regions of the NEW cache bounds need fresh rendering
|
|
60262
|
+
|
|
60263
|
+
if (deltaX > 0) {
|
|
60264
|
+
// Camera moved right → old cache was at oldWorldX, now at oldWorldX + deltaX
|
|
60265
|
+
// The NEW right edge shows world content that wasn't in the old cache
|
|
60266
|
+
// World coordinates: from (oldWorldX + cacheWidth) to (oldWorldX + cacheWidth + deltaX)
|
|
60267
|
+
regions.push({
|
|
60268
|
+
cache: {
|
|
60269
|
+
x: cacheWidth - Math.floor(deltaX),
|
|
60270
|
+
y: 0,
|
|
60271
|
+
width: Math.floor(deltaX),
|
|
60272
|
+
height: cacheHeight
|
|
60273
|
+
},
|
|
60274
|
+
world: {
|
|
60275
|
+
x: oldWorldX + cacheWidth, // Right edge of OLD cache
|
|
60276
|
+
y: oldWorldY,
|
|
60277
|
+
width: Math.floor(deltaX),
|
|
60278
|
+
height: cacheHeight
|
|
60279
|
+
},
|
|
60280
|
+
edge: 'right'
|
|
60281
|
+
});
|
|
60282
|
+
} else if (deltaX < 0) {
|
|
60283
|
+
// Camera moved left → new left edge needs rendering
|
|
60284
|
+
const absDx = Math.floor(-deltaX);
|
|
60285
|
+
regions.push({
|
|
60286
|
+
cache: {
|
|
60287
|
+
x: 0,
|
|
60288
|
+
y: 0,
|
|
60289
|
+
width: absDx,
|
|
60290
|
+
height: cacheHeight
|
|
60291
|
+
},
|
|
60292
|
+
world: {
|
|
60293
|
+
x: oldWorldX + deltaX, // New left edge in world space
|
|
60294
|
+
y: oldWorldY,
|
|
60295
|
+
width: absDx,
|
|
60296
|
+
height: cacheHeight
|
|
60297
|
+
},
|
|
60298
|
+
edge: 'left'
|
|
60299
|
+
});
|
|
60300
|
+
}
|
|
60301
|
+
|
|
60302
|
+
if (deltaY > 0) {
|
|
60303
|
+
// Camera moved down → new bottom edge
|
|
60304
|
+
const absDy = Math.floor(deltaY);
|
|
60305
|
+
regions.push({
|
|
60306
|
+
cache: {
|
|
60307
|
+
x: 0,
|
|
60308
|
+
y: cacheHeight - absDy,
|
|
60309
|
+
width: cacheWidth,
|
|
60310
|
+
height: absDy
|
|
60311
|
+
},
|
|
60312
|
+
world: {
|
|
60313
|
+
x: oldWorldX,
|
|
60314
|
+
y: oldWorldY + cacheHeight, // Bottom edge of OLD cache
|
|
60315
|
+
width: cacheWidth,
|
|
60316
|
+
height: absDy
|
|
60317
|
+
},
|
|
60318
|
+
edge: 'bottom'
|
|
60319
|
+
});
|
|
60320
|
+
} else if (deltaY < 0) {
|
|
60321
|
+
// Camera moved up → new top edge
|
|
60322
|
+
const absDy = Math.floor(-deltaY);
|
|
60323
|
+
regions.push({
|
|
60324
|
+
cache: {
|
|
60325
|
+
x: 0,
|
|
60326
|
+
y: 0,
|
|
60327
|
+
width: cacheWidth,
|
|
60328
|
+
height: absDy
|
|
60329
|
+
},
|
|
60330
|
+
world: {
|
|
60331
|
+
x: oldWorldX,
|
|
60332
|
+
y: oldWorldY + deltaY, // New top edge in world space
|
|
60333
|
+
width: cacheWidth,
|
|
60334
|
+
height: absDy
|
|
60335
|
+
},
|
|
60336
|
+
edge: 'top'
|
|
60337
|
+
});
|
|
60338
|
+
}
|
|
60339
|
+
|
|
60340
|
+
return regions;
|
|
60341
|
+
}
|
|
60342
|
+
}
|
|
60343
|
+
|
|
60344
|
+
|
|
60345
|
+
class FullMapPreRender {
|
|
60346
|
+
constructor(mapWidth, mapHeight) {
|
|
60347
|
+
this.mapWidth = mapWidth;
|
|
60348
|
+
this.mapHeight = mapHeight;
|
|
60349
|
+
this.buffer = new CacheBuffer(mapWidth, mapHeight);
|
|
60350
|
+
this.rendered = false;
|
|
60351
|
+
this.pixelsDrawn = 0;
|
|
60352
|
+
|
|
60353
|
+
console.log(`[FullMapPreRender] Created ${mapWidth}x${mapHeight} full-map buffer`);
|
|
60354
|
+
}
|
|
60355
|
+
|
|
60356
|
+
render(renderCallback) {
|
|
60357
|
+
if (this.rendered) return;
|
|
60358
|
+
|
|
60359
|
+
console.log(`[FullMapPreRender] Rendering full map...`);
|
|
60360
|
+
this.buffer.clear(0, 0, 0, 0);
|
|
60361
|
+
|
|
60362
|
+
renderCallback(this.buffer);
|
|
60363
|
+
this.pixelsDrawn = this.mapWidth * this.mapHeight;
|
|
60364
|
+
this.rendered = true;
|
|
60365
|
+
}
|
|
60366
|
+
|
|
60367
|
+
copyToDisplay(displayCanvas, camera) {
|
|
60368
|
+
const cameraWorldX = camera.worldPosition.x;
|
|
60369
|
+
const cameraWorldY = camera.worldPosition.y;
|
|
60370
|
+
|
|
60371
|
+
// offset within full map buffer where camera is looking
|
|
60372
|
+
const bufferOffsetX = cameraWorldX;
|
|
60373
|
+
const bufferOffsetY = cameraWorldY;
|
|
60374
|
+
|
|
60375
|
+
this._blitRegion(
|
|
60376
|
+
this.buffer,
|
|
60377
|
+
bufferOffsetX,
|
|
60378
|
+
bufferOffsetY,
|
|
60379
|
+
displayCanvas.width,
|
|
60380
|
+
displayCanvas.height,
|
|
60381
|
+
displayCanvas,
|
|
60382
|
+
0,
|
|
60383
|
+
0
|
|
60384
|
+
);
|
|
60385
|
+
}
|
|
60386
|
+
|
|
60387
|
+
_blitRegion(srcBuffer, srcX, srcY, width, height, dstCanvas, dstX, dstY) {
|
|
60388
|
+
const srcData = srcBuffer.data;
|
|
60389
|
+
const dstData = dstCanvas.data;
|
|
60390
|
+
const srcW = srcBuffer.width;
|
|
60391
|
+
const srcH = srcBuffer.height;
|
|
60392
|
+
const dstW = dstCanvas.width;
|
|
60393
|
+
const dstH = dstCanvas.height;
|
|
60394
|
+
|
|
60395
|
+
// Clamp source bounds
|
|
60396
|
+
const srcStartX = Math.max(0, Math.floor(srcX));
|
|
60397
|
+
const srcStartY = Math.max(0, Math.floor(srcY));
|
|
60398
|
+
const srcEndX = Math.min(srcW, Math.ceil(srcX + width));
|
|
60399
|
+
const srcEndY = Math.min(srcH, Math.ceil(srcY + height));
|
|
60400
|
+
|
|
60401
|
+
// Clamp destination bounds
|
|
60402
|
+
const dstStartX = Math.max(0, Math.floor(dstX));
|
|
60403
|
+
const dstStartY = Math.max(0, Math.floor(dstY));
|
|
60404
|
+
const dstEndX = Math.min(dstW, Math.floor(dstX + (srcEndX - srcStartX)));
|
|
60405
|
+
const dstEndY = Math.min(dstH, Math.floor(dstY + (srcEndY - srcStartY)));
|
|
60406
|
+
|
|
60407
|
+
const copyWidth = dstEndX - dstStartX;
|
|
60408
|
+
const copyHeight = dstEndY - dstStartY;
|
|
60409
|
+
|
|
60410
|
+
if (copyWidth <= 0 || copyHeight <= 0) return;
|
|
60411
|
+
|
|
60412
|
+
// Adjust source start if destination was clamped
|
|
60413
|
+
const srcOffsetX = dstStartX - Math.floor(dstX);
|
|
60414
|
+
const srcOffsetY = dstStartY - Math.floor(dstY);
|
|
60415
|
+
|
|
60416
|
+
// Fast row-by-row copy
|
|
60417
|
+
for (let y = 0; y < copyHeight; y++) {
|
|
60418
|
+
const srcRowStart = ((srcStartY + srcOffsetY + y) * srcW + srcStartX + srcOffsetX) * 4;
|
|
60419
|
+
const dstRowStart = ((dstStartY + y) * dstW + dstStartX) * 4;
|
|
60420
|
+
|
|
60421
|
+
dstData.set(
|
|
60422
|
+
srcData.subarray(srcRowStart, srcRowStart + copyWidth * 4),
|
|
60423
|
+
dstRowStart
|
|
60424
|
+
);
|
|
60425
|
+
}
|
|
60426
|
+
|
|
60427
|
+
dstCanvas.markRegion(dstStartX, dstStartY, copyWidth, copyHeight);
|
|
60428
|
+
}
|
|
60429
|
+
|
|
60430
|
+
getStats() {
|
|
60431
|
+
return {
|
|
60432
|
+
mapSize: `${this.mapWidth}x${this.mapHeight}`,
|
|
60433
|
+
rendered: this.rendered,
|
|
60434
|
+
pixelsDrawn: this.pixelsDrawn
|
|
60435
|
+
};
|
|
60436
|
+
}
|
|
60437
|
+
|
|
60438
|
+
printStats() {
|
|
60439
|
+
const s = this.getStats();
|
|
60440
|
+
console.log('FULL MAP PRE-RENDER STATS');
|
|
60441
|
+
console.log(`Map size: ${s.mapSize}`);
|
|
60442
|
+
console.log(`Rendered: ${s.rendered}`);
|
|
60443
|
+
console.log(`Pixels drawn: ${s.pixelsDrawn}`);
|
|
60444
|
+
console.log('=============================\n');
|
|
60445
|
+
}
|
|
60446
|
+
}
|
|
60447
|
+
|
|
60448
|
+
|
|
60449
|
+
class OffscreenTilemapCache {
|
|
60450
|
+
constructor(viewportWidth, viewportHeight, marginRatio = 0.25) {
|
|
60451
|
+
this.viewportWidth = viewportWidth;
|
|
60452
|
+
this.viewportHeight = viewportHeight;
|
|
60453
|
+
this.marginRatio = marginRatio;
|
|
60454
|
+
const marginX = Math.ceil(viewportWidth * marginRatio);
|
|
60455
|
+
const marginY = Math.ceil(viewportHeight * marginRatio);
|
|
60456
|
+
|
|
60457
|
+
this.cacheWidth = viewportWidth + marginX * 2;
|
|
60458
|
+
this.cacheHeight = viewportHeight + marginY * 2;
|
|
60459
|
+
this.marginPixels = { x: marginX, y: marginY };
|
|
60460
|
+
|
|
60461
|
+
this.buffer = new CacheBuffer(this.cacheWidth, this.cacheHeight);
|
|
60462
|
+
|
|
60463
|
+
this.worldX = 0;
|
|
60464
|
+
this.worldY = 0;
|
|
60465
|
+
|
|
60466
|
+
|
|
60467
|
+
this.dirty = true;
|
|
60468
|
+
|
|
60469
|
+
this.tracker = new CameraTracker();
|
|
60470
|
+
|
|
60471
|
+
this.fullRedraws = 0;
|
|
60472
|
+
this.incrementalUpdates = 0;
|
|
60473
|
+
this.copies = 0;
|
|
60474
|
+
this.skippedUpdates = 0;
|
|
60475
|
+
this.pixelsShifted = 0;
|
|
60476
|
+
this.pixelsDrawn = 0;
|
|
60477
|
+
|
|
60478
|
+
console.log(`[OffscreenCache] Created ${this.cacheWidth}x${this.cacheHeight} cache for ${viewportWidth}x${viewportHeight} viewport`);
|
|
60479
|
+
}
|
|
60480
|
+
|
|
60481
|
+
needsUpdate(camera) {
|
|
60482
|
+
this.tracker.update(camera);
|
|
60483
|
+
const delta = this.tracker.getDelta();
|
|
60484
|
+
|
|
60485
|
+
// Use margin size as threshold for determining update type
|
|
60486
|
+
const marginSize = Math.min(this.marginPixels.x, this.marginPixels.y);
|
|
60487
|
+
return CacheUpdateStrategy.determine(delta, marginSize);
|
|
60488
|
+
|
|
60489
|
+
}
|
|
60490
|
+
|
|
60491
|
+
incrementalUpdate(delta, renderCallback) {
|
|
60492
|
+
const dx = delta.x;
|
|
60493
|
+
const dy = delta.y;
|
|
60494
|
+
|
|
60495
|
+
// STEP 1: Calculate regions BEFORE updating worldX/worldY
|
|
60496
|
+
// This is the fix - we need the OLD cache position to know what's NEW
|
|
60497
|
+
const oldWorldX = this.worldX;
|
|
60498
|
+
const oldWorldY = this.worldY;
|
|
60499
|
+
|
|
60500
|
+
const newRegions = ShiftRegionCalculator.calculateNewRegions(
|
|
60501
|
+
oldWorldX,
|
|
60502
|
+
oldWorldY,
|
|
60503
|
+
this.cacheWidth,
|
|
60504
|
+
this.cacheHeight,
|
|
60505
|
+
dx,
|
|
60506
|
+
dy
|
|
60507
|
+
);
|
|
60508
|
+
|
|
60509
|
+
// STEP 2: Shift buffer content
|
|
60510
|
+
CacheShifter.shift(this.buffer, dx, dy);
|
|
60511
|
+
this.pixelsShifted += this.cacheWidth * this.cacheHeight;
|
|
60512
|
+
|
|
60513
|
+
// STEP 3: Render new regions (BEFORE updating world bounds)
|
|
60514
|
+
// Each region now has BOTH cache coords and world coords pre-calculated
|
|
60515
|
+
for (const region of newRegions) {
|
|
60516
|
+
this.pixelsDrawn += region.cache.width * region.cache.height;
|
|
60517
|
+
renderCallback(region.cache, region.world);
|
|
60518
|
+
}
|
|
60519
|
+
|
|
60520
|
+
// STEP 4: Update world bounds (NOW, after rendering)
|
|
60521
|
+
this.worldX += dx;
|
|
60522
|
+
this.worldY += dy;
|
|
60523
|
+
|
|
60524
|
+
this.incrementalUpdates++;
|
|
60525
|
+
}
|
|
60526
|
+
|
|
60527
|
+
commitFrame() {
|
|
60528
|
+
this.tracker.commit();
|
|
60529
|
+
}
|
|
60530
|
+
|
|
60531
|
+
invalidate() {
|
|
60532
|
+
this.dirty = true;
|
|
60533
|
+
}
|
|
60534
|
+
|
|
60535
|
+
setWorldRegion(worldX, worldY) {
|
|
60536
|
+
this.worldX = worldX;
|
|
60537
|
+
this.worldY = worldY;
|
|
60538
|
+
}
|
|
60539
|
+
|
|
60540
|
+
getWorldBounds() {
|
|
60541
|
+
return {
|
|
60542
|
+
left: this.worldX,
|
|
60543
|
+
top: this.worldY,
|
|
60544
|
+
right: this.worldX + this.cacheWidth,
|
|
60545
|
+
bottom: this.worldY + this.cacheHeight,
|
|
60546
|
+
width: this.cacheWidth,
|
|
60547
|
+
height: this.cacheHeight
|
|
60548
|
+
};
|
|
60549
|
+
}
|
|
60550
|
+
|
|
60551
|
+
isCameraInBounds(camera) {
|
|
60552
|
+
const bounds = this.getWorldBounds();
|
|
60553
|
+
const camX = camera.worldPosition.x;
|
|
60554
|
+
const camY = camera.worldPosition.y;
|
|
60555
|
+
|
|
60556
|
+
// Camera should stay within the inner viewport area, not near edges
|
|
60557
|
+
const safeLeft = bounds.left + this.marginPixels.x;
|
|
60558
|
+
const safeRight = bounds.right - this.marginPixels.x;
|
|
60559
|
+
const safeTop = bounds.top + this.marginPixels.y;
|
|
60560
|
+
const safeBottom = bounds.bottom - this.marginPixels.y;
|
|
60561
|
+
|
|
60562
|
+
return camX >= safeLeft && camX <= safeRight &&
|
|
60563
|
+
camY >= safeTop && camY <= safeBottom;
|
|
60564
|
+
}
|
|
60565
|
+
|
|
60566
|
+
clear() {
|
|
60567
|
+
this.buffer.clear(0, 0, 0, 0);
|
|
60568
|
+
}
|
|
60569
|
+
|
|
60570
|
+
copyToDisplay(displayCanvas, camera) {
|
|
60571
|
+
this.copies++;
|
|
60572
|
+
|
|
60573
|
+
|
|
60574
|
+
const cameraWorldX = camera.worldPosition.x;
|
|
60575
|
+
const cameraWorldY = camera.worldPosition.y;
|
|
60576
|
+
|
|
60577
|
+
// offset within cache where camera is looking
|
|
60578
|
+
const cacheOffsetX = cameraWorldX - this.worldX;
|
|
60579
|
+
const cacheOffsetY = cameraWorldY - this.worldY;
|
|
60580
|
+
|
|
60581
|
+
|
|
60582
|
+
this._blitRegion(
|
|
60583
|
+
this.buffer,
|
|
60584
|
+
cacheOffsetX,
|
|
60585
|
+
cacheOffsetY,
|
|
60586
|
+
this.viewportWidth,
|
|
60587
|
+
this.viewportHeight,
|
|
60588
|
+
displayCanvas,
|
|
60589
|
+
0,
|
|
60590
|
+
0
|
|
60591
|
+
);
|
|
60592
|
+
}
|
|
60593
|
+
|
|
60594
|
+
// Fast blit from cache to display - assumes opaque
|
|
60595
|
+
_blitRegion(srcBuffer, srcX, srcY, width, height, dstCanvas, dstX, dstY) {
|
|
60596
|
+
const srcData = srcBuffer.data;
|
|
60597
|
+
const dstData = dstCanvas.data;
|
|
60598
|
+
const srcW = srcBuffer.width;
|
|
60599
|
+
const srcH = srcBuffer.height;
|
|
60600
|
+
const dstW = dstCanvas.width;
|
|
60601
|
+
const dstH = dstCanvas.height;
|
|
60602
|
+
|
|
60603
|
+
// Clamp source bounds
|
|
60604
|
+
const srcStartX = Math.max(0, Math.floor(srcX));
|
|
60605
|
+
const srcStartY = Math.max(0, Math.floor(srcY));
|
|
60606
|
+
const srcEndX = Math.min(srcW, Math.ceil(srcX + width));
|
|
60607
|
+
const srcEndY = Math.min(srcH, Math.ceil(srcY + height));
|
|
60608
|
+
|
|
60609
|
+
// Clamp destination bounds
|
|
60610
|
+
const dstStartX = Math.max(0, Math.floor(dstX));
|
|
60611
|
+
const dstStartY = Math.max(0, Math.floor(dstY));
|
|
60612
|
+
const dstEndX = Math.min(dstW, Math.floor(dstX + (srcEndX - srcStartX)));
|
|
60613
|
+
const dstEndY = Math.min(dstH, Math.floor(dstY + (srcEndY - srcStartY)));
|
|
60614
|
+
|
|
60615
|
+
const copyWidth = dstEndX - dstStartX;
|
|
60616
|
+
const copyHeight = dstEndY - dstStartY;
|
|
60617
|
+
|
|
60618
|
+
if (copyWidth <= 0 || copyHeight <= 0) return;
|
|
60619
|
+
|
|
60620
|
+
// Adjust source start if destination was clamped
|
|
60621
|
+
const srcOffsetX = dstStartX - Math.floor(dstX);
|
|
60622
|
+
const srcOffsetY = dstStartY - Math.floor(dstY);
|
|
60623
|
+
|
|
60624
|
+
// Fast row-by-row copy
|
|
60625
|
+
for (let y = 0; y < copyHeight; y++) {
|
|
60626
|
+
const srcRowStart = ((srcStartY + srcOffsetY + y) * srcW + srcStartX + srcOffsetX) * 4;
|
|
60627
|
+
const dstRowStart = ((dstStartY + y) * dstW + dstStartX) * 4;
|
|
60628
|
+
|
|
60629
|
+
dstData.set(
|
|
60630
|
+
srcData.subarray(srcRowStart, srcRowStart + copyWidth * 4),
|
|
60631
|
+
dstRowStart
|
|
60632
|
+
);
|
|
60633
|
+
}
|
|
60634
|
+
|
|
60635
|
+
dstCanvas.markRegion(dstStartX, dstStartY, copyWidth, copyHeight);
|
|
60636
|
+
}
|
|
60637
|
+
|
|
60638
|
+
getStats() {
|
|
60639
|
+
return {
|
|
60640
|
+
cacheSize: `${this.cacheWidth}x${this.cacheHeight}`,
|
|
60641
|
+
viewportSize: `${this.viewportWidth}x${this.viewportHeight}`,
|
|
60642
|
+
marginRatio: this.marginRatio,
|
|
60643
|
+
fullRedraws: this.fullRedraws,
|
|
60644
|
+
copies: this.copies,
|
|
60645
|
+
worldRegion: `(${this.worldX}, ${this.worldY}) to (${this.worldX + this.cacheWidth}, ${this.worldY + this.cacheHeight})`
|
|
60646
|
+
};
|
|
60647
|
+
}
|
|
60648
|
+
|
|
60649
|
+
printStats() {
|
|
60650
|
+
const s = this.getStats();
|
|
60651
|
+
console.log('OFFSCREEN CACHE STATS');
|
|
60652
|
+
console.log(`Cache size: ${s.cacheSize}`);
|
|
60653
|
+
console.log(`Viewport size: ${s.viewportSize}`);
|
|
60654
|
+
console.log(`Margin: ${(this.marginRatio * 100).toFixed(0)}%`);
|
|
60655
|
+
console.log(`Full redraws: ${s.fullRedraws}`);
|
|
60656
|
+
console.log(`Cache copies: ${s.copies}`);
|
|
60657
|
+
console.log(`World region: ${s.worldRegion}`);
|
|
60658
|
+
console.log('=============================\n');
|
|
60659
|
+
}
|
|
60660
|
+
}
|
|
60661
|
+
|
|
60662
|
+
|
|
60663
|
+
// ============= STRATEGY 3: CHUNKED PRE-RENDER =============
|
|
60664
|
+
// Divide world into fixed-size chunks, pre-render all layers into each chunk,
|
|
60665
|
+
// then just copy visible chunks to display. No shifting, no artifacts.
|
|
60666
|
+
|
|
60667
|
+
class ChunkedPreRender {
|
|
60668
|
+
constructor(mapWidth, mapHeight, chunkSize = 256) {
|
|
60669
|
+
this.mapWidth = mapWidth;
|
|
60670
|
+
this.mapHeight = mapHeight;
|
|
60671
|
+
this.chunkSize = chunkSize;
|
|
60672
|
+
|
|
60673
|
+
// Calculate grid dimensions
|
|
60674
|
+
this.chunksX = Math.ceil(mapWidth / chunkSize);
|
|
60675
|
+
this.chunksY = Math.ceil(mapHeight / chunkSize);
|
|
60676
|
+
|
|
60677
|
+
// Store rendered chunk buffers in a 2D grid
|
|
60678
|
+
this.chunks = new Array(this.chunksY);
|
|
60679
|
+
for (let y = 0; y < this.chunksY; y++) {
|
|
60680
|
+
this.chunks[y] = new Array(this.chunksX);
|
|
60681
|
+
}
|
|
60682
|
+
|
|
60683
|
+
this.renderCallbacks = [];
|
|
60684
|
+
this.rendered = false;
|
|
60685
|
+
this.pixelsDrawn = 0;
|
|
60686
|
+
|
|
60687
|
+
console.log(`[ChunkedPreRender] Created ${this.chunksX}x${this.chunksY} chunk grid (${chunkSize}x${chunkSize}px chunks) for ${mapWidth}x${mapHeight} map`);
|
|
60688
|
+
}
|
|
60689
|
+
|
|
60690
|
+
// Register a callback that will render layers to a buffer
|
|
60691
|
+
// Called during initialization and when chunks need updating
|
|
60692
|
+
onRenderChunk(callback) {
|
|
60693
|
+
this.renderCallbacks.push(callback);
|
|
60694
|
+
}
|
|
60695
|
+
|
|
60696
|
+
// Pre-render all chunks (call once during init)
|
|
60697
|
+
renderAllChunks() {
|
|
60698
|
+
if (this.rendered) return;
|
|
60699
|
+
|
|
60700
|
+
console.log(`[ChunkedPreRender] Pre-rendering ${this.chunksX * this.chunksY} chunks...`);
|
|
60701
|
+
|
|
60702
|
+
for (let cy = 0; cy < this.chunksY; cy++) {
|
|
60703
|
+
for (let cx = 0; cx < this.chunksX; cx++) {
|
|
60704
|
+
const chunk = this._renderChunk(cx, cy);
|
|
60705
|
+
this.chunks[cy][cx] = chunk;
|
|
60706
|
+
this.pixelsDrawn += chunk.buffer.width * chunk.buffer.height;
|
|
60707
|
+
}
|
|
60708
|
+
}
|
|
60709
|
+
|
|
60710
|
+
this.rendered = true;
|
|
60711
|
+
console.log(`[ChunkedPreRender] Pre-render complete. Total pixels: ${this.pixelsDrawn}`);
|
|
60712
|
+
}
|
|
60713
|
+
|
|
60714
|
+
// Render a single chunk
|
|
60715
|
+
_renderChunk(chunkX, chunkY) {
|
|
60716
|
+
// Calculate world bounds for this chunk
|
|
60717
|
+
const worldX = chunkX * this.chunkSize;
|
|
60718
|
+
const worldY = chunkY * this.chunkSize;
|
|
60719
|
+
|
|
60720
|
+
// Calculate actual chunk size (may be smaller at map edges)
|
|
60721
|
+
const chunkWidth = Math.min(this.chunkSize, this.mapWidth - worldX);
|
|
60722
|
+
const chunkHeight = Math.min(this.chunkSize, this.mapHeight - worldY);
|
|
60723
|
+
|
|
60724
|
+
// Create buffer for this chunk
|
|
60725
|
+
const buffer = new CacheBuffer(chunkWidth, chunkHeight);
|
|
60726
|
+
buffer.clear(0, 0, 0, 0);
|
|
60727
|
+
|
|
60728
|
+
// Call all registered render callbacks with this chunk's world region
|
|
60729
|
+
for (const callback of this.renderCallbacks) {
|
|
60730
|
+
callback(buffer, {
|
|
60731
|
+
x: worldX,
|
|
60732
|
+
y: worldY,
|
|
60733
|
+
width: chunkWidth,
|
|
60734
|
+
height: chunkHeight
|
|
60735
|
+
});
|
|
60736
|
+
}
|
|
60737
|
+
|
|
60738
|
+
return {
|
|
60739
|
+
buffer,
|
|
60740
|
+
worldX,
|
|
60741
|
+
worldY,
|
|
60742
|
+
width: chunkWidth,
|
|
60743
|
+
height: chunkHeight
|
|
60744
|
+
};
|
|
60745
|
+
}
|
|
60746
|
+
|
|
60747
|
+
// Copy visible chunks to display canvas based on camera position
|
|
60748
|
+
copyToDisplay(displayCanvas, camera) {
|
|
60749
|
+
const cameraX = camera.worldPosition.x;
|
|
60750
|
+
const cameraY = camera.worldPosition.y;
|
|
60751
|
+
const viewWidth = displayCanvas.width;
|
|
60752
|
+
const viewHeight = displayCanvas.height;
|
|
60753
|
+
|
|
60754
|
+
// Calculate which chunks are visible
|
|
60755
|
+
// Camera position is top-left of viewport
|
|
60756
|
+
const minChunkX = Math.max(0, Math.floor(cameraX / this.chunkSize));
|
|
60757
|
+
const maxChunkX = Math.min(this.chunksX - 1, Math.floor((cameraX + viewWidth) / this.chunkSize));
|
|
60758
|
+
const minChunkY = Math.max(0, Math.floor(cameraY / this.chunkSize));
|
|
60759
|
+
const maxChunkY = Math.min(this.chunksY - 1, Math.floor((cameraY + viewHeight) / this.chunkSize));
|
|
60760
|
+
|
|
60761
|
+
// Blit each visible chunk to canvas
|
|
60762
|
+
for (let cy = minChunkY; cy <= maxChunkY; cy++) {
|
|
60763
|
+
for (let cx = minChunkX; cx <= maxChunkX; cx++) {
|
|
60764
|
+
const chunk = this.chunks[cy][cx];
|
|
60765
|
+
if (!chunk) continue;
|
|
60766
|
+
|
|
60767
|
+
// Calculate destination position on canvas
|
|
60768
|
+
// chunk.worldX is in world space, convert to screen space (relative to camera)
|
|
60769
|
+
const screenX = chunk.worldX - cameraX;
|
|
60770
|
+
const screenY = chunk.worldY - cameraY;
|
|
60771
|
+
|
|
60772
|
+
this._blitChunk(chunk.buffer, screenX, screenY, displayCanvas);
|
|
60773
|
+
}
|
|
60774
|
+
}
|
|
60775
|
+
}
|
|
60776
|
+
|
|
60777
|
+
// Blit a single chunk buffer to the display canvas
|
|
60778
|
+
_blitChunk(srcBuffer, dstX, dstY, dstCanvas) {
|
|
60779
|
+
const srcData = srcBuffer.data;
|
|
60780
|
+
const dstData = dstCanvas.data;
|
|
60781
|
+
const srcW = srcBuffer.width;
|
|
60782
|
+
const srcH = srcBuffer.height;
|
|
60783
|
+
const dstW = dstCanvas.width;
|
|
60784
|
+
const dstH = dstCanvas.height;
|
|
60785
|
+
|
|
60786
|
+
// Clamp destination bounds
|
|
60787
|
+
const dstStartX = Math.max(0, Math.floor(dstX));
|
|
60788
|
+
const dstStartY = Math.max(0, Math.floor(dstY));
|
|
60789
|
+
const dstEndX = Math.min(dstW, Math.floor(dstX + srcW));
|
|
60790
|
+
const dstEndY = Math.min(dstH, Math.floor(dstY + srcH));
|
|
60791
|
+
|
|
60792
|
+
if (dstStartX >= dstEndX || dstStartY >= dstEndY) return;
|
|
60793
|
+
|
|
60794
|
+
// Calculate corresponding source region (clipping)
|
|
60795
|
+
const srcStartX = Math.max(0, Math.floor(-dstX));
|
|
60796
|
+
const srcStartY = Math.max(0, Math.floor(-dstY));
|
|
60797
|
+
const copyWidth = dstEndX - dstStartX;
|
|
60798
|
+
const copyHeight = dstEndY - dstStartY;
|
|
60799
|
+
|
|
60800
|
+
// Fast row-by-row copy
|
|
60801
|
+
for (let y = 0; y < copyHeight; y++) {
|
|
60802
|
+
const srcRowStart = ((srcStartY + y) * srcW + srcStartX) * 4;
|
|
60803
|
+
const dstRowStart = ((dstStartY + y) * dstW + dstStartX) * 4;
|
|
60804
|
+
|
|
60805
|
+
dstData.set(
|
|
60806
|
+
srcData.subarray(srcRowStart, srcRowStart + copyWidth * 4),
|
|
60807
|
+
dstRowStart
|
|
60808
|
+
);
|
|
60809
|
+
}
|
|
60810
|
+
|
|
60811
|
+
dstCanvas.markRegion(dstStartX, dstStartY, copyWidth, copyHeight);
|
|
60812
|
+
}
|
|
60813
|
+
|
|
60814
|
+
getStats() {
|
|
60815
|
+
return {
|
|
60816
|
+
mapSize: `${this.mapWidth}x${this.mapHeight}`,
|
|
60817
|
+
chunkSize: this.chunkSize,
|
|
60818
|
+
chunkGrid: `${this.chunksX}x${this.chunksY}`,
|
|
60819
|
+
rendered: this.rendered,
|
|
60820
|
+
pixelsDrawn: this.pixelsDrawn
|
|
60821
|
+
};
|
|
60822
|
+
}
|
|
60823
|
+
|
|
60824
|
+
printStats() {
|
|
60825
|
+
const s = this.getStats();
|
|
60826
|
+
console.log('=== CHUNKED PRE-RENDER STATS ===');
|
|
60827
|
+
console.log(`Map size: ${s.mapSize}`);
|
|
60828
|
+
console.log(`Chunk size: ${s.chunkSize}`);
|
|
60829
|
+
console.log(`Chunk grid: ${s.chunkGrid}`);
|
|
60830
|
+
console.log(`Rendered: ${s.rendered}`);
|
|
60831
|
+
console.log(`Pixels drawn: ${s.pixelsDrawn}`);
|
|
60832
|
+
console.log('==================================\n');
|
|
60833
|
+
}
|
|
60834
|
+
}
|
|
60835
|
+
|
|
59879
60836
|
const tiledperf = globalPerf;
|
|
59880
60837
|
|
|
59881
60838
|
function reshapeArray(arr, width, height) {
|
|
@@ -60009,6 +60966,7 @@ class TileLayer extends BaseLayer {
|
|
|
60009
60966
|
}
|
|
60010
60967
|
|
|
60011
60968
|
renderChunkToBuffer(chunkX, chunkY, buffer) {
|
|
60969
|
+
tiledperf.start("renderChunkToBuffer");
|
|
60012
60970
|
const startX = chunkX * this.chunkSize;
|
|
60013
60971
|
const startY = chunkY * this.chunkSize;
|
|
60014
60972
|
const endX = Math.min(startX + this.chunkSize, this.layerwidth);
|
|
@@ -60046,9 +61004,11 @@ class TileLayer extends BaseLayer {
|
|
|
60046
61004
|
});
|
|
60047
61005
|
}
|
|
60048
61006
|
}
|
|
61007
|
+
tiledperf.end("renderChunkToBuffer");
|
|
60049
61008
|
}
|
|
60050
61009
|
|
|
60051
61010
|
blitBuffer(srcBuffer, dstCanvas, destX, destY) {
|
|
61011
|
+
tiledperf.start("blitBufferPixels");
|
|
60052
61012
|
const srcData = srcBuffer.data;
|
|
60053
61013
|
const dstData = dstCanvas.data;
|
|
60054
61014
|
const srcW = srcBuffer.width;
|
|
@@ -60061,7 +61021,10 @@ class TileLayer extends BaseLayer {
|
|
|
60061
61021
|
const endX = Math.min(dstW, destX + srcW);
|
|
60062
61022
|
const endY = Math.min(dstH, destY + srcH);
|
|
60063
61023
|
|
|
60064
|
-
if (startX >= endX || startY >= endY)
|
|
61024
|
+
if (startX >= endX || startY >= endY) {
|
|
61025
|
+
tiledperf.end("blitBufferPixels");
|
|
61026
|
+
return;
|
|
61027
|
+
}
|
|
60065
61028
|
|
|
60066
61029
|
if (this.assumeOpaque) {
|
|
60067
61030
|
for (let y = startY; y < endY; y++) {
|
|
@@ -60106,12 +61069,136 @@ class TileLayer extends BaseLayer {
|
|
|
60106
61069
|
}
|
|
60107
61070
|
}
|
|
60108
61071
|
}
|
|
60109
|
-
|
|
60110
|
-
dstCanvas.markRegion(startX, startY, endX - startX, endY - startY);
|
|
61072
|
+
|
|
61073
|
+
dstCanvas.markRegion(startX, startY, endX - startX, endY - startY);
|
|
61074
|
+
tiledperf.end("blitBufferPixels");
|
|
61075
|
+
}
|
|
61076
|
+
|
|
61077
|
+
|
|
61078
|
+
|
|
61079
|
+
|
|
61080
|
+
/**
|
|
61081
|
+
* Draw only a specific region of the tilemap
|
|
61082
|
+
* Used for incremental cache updates
|
|
61083
|
+
*
|
|
61084
|
+
* @param {CacheBuffer} canvas - buffer to draw to
|
|
61085
|
+
* @param {Object} camera - fake camera for coordinate conversion
|
|
61086
|
+
* @param {Object} region - {x, y, width, height} in cache coordinates
|
|
61087
|
+
* @param {Object} worldRegion - {x, y, width, height} in world coordinates
|
|
61088
|
+
*/
|
|
61089
|
+
|
|
61090
|
+
drawRegion(canvas, camera, region, worldRegion) {
|
|
61091
|
+
tiledperf.start("tilelayer_drawRegion");
|
|
61092
|
+
// Cache locals for faster access
|
|
61093
|
+
const chunks = this.chunkspertiles[0];
|
|
61094
|
+
const chunkSize = this.chunkSize;
|
|
61095
|
+
const tileW = this.tileWidth;
|
|
61096
|
+
const tileH = this.tileHeight;
|
|
61097
|
+
const layerWorldX = this.worldPosition.x;
|
|
61098
|
+
const layerWorldY = this.worldPosition.y;
|
|
61099
|
+
const regionX = worldRegion.x;
|
|
61100
|
+
const regionY = worldRegion.y;
|
|
61101
|
+
const regionW = worldRegion.width;
|
|
61102
|
+
const regionH = worldRegion.height;
|
|
61103
|
+
|
|
61104
|
+
// Calculate chunk bounds - inline to avoid function call
|
|
61105
|
+
const chunkPixelW = chunkSize * tileW;
|
|
61106
|
+
const chunkPixelH = chunkSize * tileH;
|
|
61107
|
+
|
|
61108
|
+
let startChunkX = Math.floor((regionX - layerWorldX) / chunkPixelW);
|
|
61109
|
+
let endChunkX = Math.ceil((regionX + regionW - layerWorldX) / chunkPixelW);
|
|
61110
|
+
let startChunkY = Math.floor((regionY - layerWorldY) / chunkPixelH);
|
|
61111
|
+
let endChunkY = Math.ceil((regionY + regionH - layerWorldY) / chunkPixelH);
|
|
61112
|
+
|
|
61113
|
+
// Clamp to valid chunk range
|
|
61114
|
+
if (startChunkX < 0) startChunkX = 0;
|
|
61115
|
+
if (startChunkY < 0) startChunkY = 0;
|
|
61116
|
+
const maxChunkX = chunks[0].length - 1;
|
|
61117
|
+
const maxChunkY = chunks.length - 1;
|
|
61118
|
+
if (endChunkX > maxChunkX) endChunkX = maxChunkX;
|
|
61119
|
+
if (endChunkY > maxChunkY) endChunkY = maxChunkY;
|
|
61120
|
+
|
|
61121
|
+
// Blit chunks - tight loop, no allocations
|
|
61122
|
+
for (let cy = startChunkY; cy <= endChunkY; cy++) {
|
|
61123
|
+
const chunkRow = this.chunkBuffers[cy];
|
|
61124
|
+
|
|
61125
|
+
for (let cx = startChunkX; cx <= endChunkX; cx++) {
|
|
61126
|
+
const chunkBuffer = chunkRow[cx];
|
|
61127
|
+
|
|
61128
|
+
// Calculate world position - inline
|
|
61129
|
+
const chunkWorldX = layerWorldX + cx * chunkPixelW;
|
|
61130
|
+
const chunkWorldY = layerWorldY + cy * chunkPixelH;
|
|
61131
|
+
|
|
61132
|
+
// Convert to cache coords using the proper worldToScreen method
|
|
61133
|
+
const screenPos = camera.worldToScreen(chunkWorldX, chunkWorldY);
|
|
61134
|
+
const destX = Math.floor(screenPos.x);
|
|
61135
|
+
const destY = Math.floor(screenPos.y);
|
|
61136
|
+
|
|
61137
|
+
// Quick bounds check - inline
|
|
61138
|
+
const chunkW = chunkBuffer.width;
|
|
61139
|
+
const chunkH = chunkBuffer.height;
|
|
61140
|
+
const regionLeft = region.x;
|
|
61141
|
+
const regionRight = region.x + region.width;
|
|
61142
|
+
const regionTop = region.y;
|
|
61143
|
+
const regionBottom = region.y + region.height;
|
|
61144
|
+
|
|
61145
|
+
if (destX + chunkW < regionLeft || destX > regionRight ||
|
|
61146
|
+
destY + chunkH < regionTop || destY > regionBottom) {
|
|
61147
|
+
continue;
|
|
61148
|
+
}
|
|
61149
|
+
|
|
61150
|
+
// Blit chunk
|
|
61151
|
+
this.blitBuffer(chunkBuffer, canvas, destX, destY);
|
|
61152
|
+
}
|
|
60111
61153
|
}
|
|
61154
|
+
tiledperf.end("tilelayer_drawRegion");
|
|
61155
|
+
}
|
|
61156
|
+
// drawRegion(canvas, camera, region, worldRegion) {
|
|
61157
|
+
// tiledperf.start("drawRegion");
|
|
61158
|
+
// // Calculate which chunks intersect this region
|
|
61159
|
+
// const startChunkX = Math.floor((worldRegion.x - this.worldPosition.x) / (this.chunkSize * this.tileWidth));
|
|
61160
|
+
// const endChunkX = Math.ceil((worldRegion.x + worldRegion.width - this.worldPosition.x) / (this.chunkSize * this.tileWidth));
|
|
61161
|
+
// const startChunkY = Math.floor((worldRegion.y - this.worldPosition.y) / (this.chunkSize * this.tileHeight));
|
|
61162
|
+
// const endChunkY = Math.ceil((worldRegion.y + worldRegion.height - this.worldPosition.y) / (this.chunkSize * this.tileHeight));
|
|
61163
|
+
|
|
61164
|
+
// const chunks = this.chunkspertiles[0];
|
|
61165
|
+
// const clampStartX = Math.max(0, startChunkX);
|
|
61166
|
+
// const clampEndX = Math.min(chunks[0].length - 1, endChunkX);
|
|
61167
|
+
// const clampStartY = Math.max(0, startChunkY);
|
|
61168
|
+
// const clampEndY = Math.min(chunks.length - 1, endChunkY);
|
|
61169
|
+
|
|
61170
|
+
// // Blit only the chunks that intersect the region
|
|
61171
|
+
// for (let cy = clampStartY; cy <= clampEndY; cy++) {
|
|
61172
|
+
// for (let cx = clampStartX; cx <= clampEndX; cx++) {
|
|
61173
|
+
// const chunkBuffer = this.chunkBuffers[cy][cx];
|
|
61174
|
+
// const chunkWorldX = this.worldPosition.x + cx * this.chunkSize * this.tileWidth;
|
|
61175
|
+
// const chunkWorldY = this.worldPosition.y + cy * this.chunkSize * this.tileHeight;
|
|
61176
|
+
|
|
61177
|
+
// // Convert world position to cache coordinates
|
|
61178
|
+
// const screenPos = camera.worldToScreen(chunkWorldX, chunkWorldY);
|
|
61179
|
+
// const destX = Math.floor(screenPos.x);
|
|
61180
|
+
// const destY = Math.floor(screenPos.y);
|
|
61181
|
+
|
|
61182
|
+
// // Only blit if chunk intersects region
|
|
61183
|
+
// if (destX + chunkBuffer.width >= region.x &&
|
|
61184
|
+
// destX < region.x + region.width &&
|
|
61185
|
+
// destY + chunkBuffer.height >= region.y &&
|
|
61186
|
+
// destY < region.y + region.height) {
|
|
61187
|
+
|
|
61188
|
+
// this.blitBuffer(chunkBuffer, canvas, destX, destY);
|
|
61189
|
+
// }
|
|
61190
|
+
// }
|
|
61191
|
+
// }
|
|
61192
|
+
// tiledperf.end("drawRegion");
|
|
61193
|
+
// }
|
|
60112
61194
|
|
|
60113
|
-
draw(canvas, camera) {
|
|
61195
|
+
draw(canvas, camera, region = null, worldRegion = null) {
|
|
61196
|
+
if (region && worldRegion) {
|
|
61197
|
+
this.drawRegion(canvas, camera, region, worldRegion);
|
|
61198
|
+
return;
|
|
61199
|
+
}
|
|
60114
61200
|
tiledperf.start("drawVisibleChunks");
|
|
61201
|
+
tilemapProfiler.startFrame(camera);
|
|
60115
61202
|
const visibleBounds = camera.getVisibleBounds();
|
|
60116
61203
|
const chunks = this.chunkspertiles[0];
|
|
60117
61204
|
|
|
@@ -60129,6 +61216,8 @@ class TileLayer extends BaseLayer {
|
|
|
60129
61216
|
const screenPos = camera.worldToScreen(worldX, worldY);
|
|
60130
61217
|
const destX = Math.floor(screenPos.x);
|
|
60131
61218
|
const destY = Math.floor(screenPos.y);
|
|
61219
|
+
tilemapProfiler.recordBlit(chunkBuffer.width, chunkBuffer.height);
|
|
61220
|
+
tilemapProfiler.recordChunk();
|
|
60132
61221
|
this.blitBuffer(chunkBuffer, canvas, destX, destY);
|
|
60133
61222
|
}
|
|
60134
61223
|
}
|
|
@@ -60188,7 +61277,66 @@ class ObjectLayer extends BaseLayer {
|
|
|
60188
61277
|
}
|
|
60189
61278
|
}
|
|
60190
61279
|
|
|
60191
|
-
|
|
61280
|
+
/**
|
|
61281
|
+
* Draw only objects that intersect the specified region
|
|
61282
|
+
* For incremental cache updates
|
|
61283
|
+
*/
|
|
61284
|
+
drawRegion(canvas, camera, cacheRegion, worldRegion) {
|
|
61285
|
+
// Iterate through precomputed render cache
|
|
61286
|
+
// Only draw objects that intersect worldRegion
|
|
61287
|
+
tiledperf.start("objectlayer_drawregion");
|
|
61288
|
+
|
|
61289
|
+
|
|
61290
|
+
const regionLeft = worldRegion.x;
|
|
61291
|
+
const regionRight = worldRegion.x + worldRegion.width;
|
|
61292
|
+
const regionTop = worldRegion.y;
|
|
61293
|
+
const regionBottom = worldRegion.y + worldRegion.height;
|
|
61294
|
+
|
|
61295
|
+
for (let i = 0; i < this.objectRenderCache.length; i++) {
|
|
61296
|
+
const cached = this.objectRenderCache[i];
|
|
61297
|
+
|
|
61298
|
+
// Quick AABB intersection test
|
|
61299
|
+
const objLeft = cached.worldX;
|
|
61300
|
+
const objRight = cached.worldX + cached.width;
|
|
61301
|
+
const objTop = cached.worldY;
|
|
61302
|
+
const objBottom = cached.worldY + cached.height;
|
|
61303
|
+
|
|
61304
|
+
// Skip if no intersection
|
|
61305
|
+
if (objRight < regionLeft || objLeft > regionRight ||
|
|
61306
|
+
objBottom < regionTop || objTop > regionBottom) {
|
|
61307
|
+
continue;
|
|
61308
|
+
}
|
|
61309
|
+
|
|
61310
|
+
// Object intersects region - draw it
|
|
61311
|
+
const screenPos = camera.worldToScreen(cached.worldX, cached.worldY);
|
|
61312
|
+
|
|
61313
|
+
const destRect = {
|
|
61314
|
+
x: Math.floor(screenPos.x),
|
|
61315
|
+
y: Math.floor(screenPos.y),
|
|
61316
|
+
width: Math.floor(cached.width * camera.zoom),
|
|
61317
|
+
height: Math.floor(cached.height * camera.zoom)
|
|
61318
|
+
};
|
|
61319
|
+
|
|
61320
|
+
// Draw to canvas
|
|
61321
|
+
drawAtlasRegionToCanvas(cached.image, cached.srcRect, canvas, destRect, {
|
|
61322
|
+
algorithm: "nn",
|
|
61323
|
+
camera: camera
|
|
61324
|
+
});
|
|
61325
|
+
}
|
|
61326
|
+
|
|
61327
|
+
tiledperf.end("objectlayer_drawregion");
|
|
61328
|
+
|
|
61329
|
+
}
|
|
61330
|
+
|
|
61331
|
+
|
|
61332
|
+
draw(canvas, camera, cacheRegion = null, worldRegion = null) {
|
|
61333
|
+
tiledperf.start("objectlayer_draw");
|
|
61334
|
+
|
|
61335
|
+
if (cacheRegion && worldRegion) {
|
|
61336
|
+
this.drawRegion(canvas, camera, cacheRegion, worldRegion);
|
|
61337
|
+
return;
|
|
61338
|
+
}
|
|
61339
|
+
|
|
60192
61340
|
for (const cached of this.objectRenderCache) {
|
|
60193
61341
|
const screenPos = camera.worldToScreen(cached.worldX, cached.worldY);
|
|
60194
61342
|
|
|
@@ -60209,6 +61357,8 @@ class ObjectLayer extends BaseLayer {
|
|
|
60209
61357
|
camera: camera
|
|
60210
61358
|
});
|
|
60211
61359
|
}
|
|
61360
|
+
tiledperf.end("objectlayer_draw");
|
|
61361
|
+
|
|
60212
61362
|
}
|
|
60213
61363
|
|
|
60214
61364
|
printDebug() {
|
|
@@ -60408,9 +61558,124 @@ class CollisionLayer extends BaseLayer {
|
|
|
60408
61558
|
return created;
|
|
60409
61559
|
}
|
|
60410
61560
|
|
|
60411
|
-
|
|
61561
|
+
drawRegion(canvas, camera, cacheRegion, worldRegion) {
|
|
61562
|
+
if (!this.debugCollisions) return;
|
|
61563
|
+
|
|
61564
|
+
const regionLeft = worldRegion.x;
|
|
61565
|
+
const regionRight = worldRegion.x + worldRegion.width;
|
|
61566
|
+
const regionTop = worldRegion.y;
|
|
61567
|
+
const regionBottom = worldRegion.y + worldRegion.height;
|
|
61568
|
+
|
|
61569
|
+
const getColorForBody = (body) => {
|
|
61570
|
+
switch ((body.type || '').toLowerCase()) {
|
|
61571
|
+
case 'static': return { r: 120, g: 120, b: 120, a: 220 };
|
|
61572
|
+
case 'kinematic': return { r: 0, g: 140, b: 255, a: 220 };
|
|
61573
|
+
case 'dynamic':
|
|
61574
|
+
default: return { r: 255, g: 60, b: 60, a: 220 };
|
|
61575
|
+
}
|
|
61576
|
+
};
|
|
61577
|
+
|
|
61578
|
+
// Inline AABB test - no function call overhead
|
|
61579
|
+
const intersectsRegion = (left, top, right, bottom) => {
|
|
61580
|
+
return !(right < regionLeft || left > regionRight ||
|
|
61581
|
+
bottom < regionTop || top > regionBottom);
|
|
61582
|
+
};
|
|
61583
|
+
|
|
61584
|
+
const strokeThickness = 1;
|
|
61585
|
+
|
|
61586
|
+
for (const [name, body] of this.bodies.entries()) {
|
|
61587
|
+
if (!body) continue;
|
|
61588
|
+
|
|
61589
|
+
const color = getColorForBody(body);
|
|
61590
|
+
const children = body.children || [];
|
|
61591
|
+
|
|
61592
|
+
if (!children.length) continue;
|
|
61593
|
+
|
|
61594
|
+
for (let s = 0; s < children.length; s++) {
|
|
61595
|
+
const shape = children[s];
|
|
61596
|
+
if (!shape || !shape.shapeType) continue;
|
|
61597
|
+
|
|
61598
|
+
const opts = shape.options || {};
|
|
61599
|
+
const anchor = shape.anchorOffset || { x: 0, y: 0 };
|
|
61600
|
+
const baseX = (opts.x || 0);
|
|
61601
|
+
const baseY = (opts.y || 0);
|
|
61602
|
+
const lx = baseX - anchor.x;
|
|
61603
|
+
const ly = baseY - anchor.y;
|
|
61604
|
+
const worldBaseX = (body.position && body.position.x !== undefined) ? body.position.x + lx : lx;
|
|
61605
|
+
const worldBaseY = (body.position && body.position.y !== undefined) ? body.position.y + ly : ly;
|
|
61606
|
+
|
|
61607
|
+
// Quick reject based on shape type
|
|
61608
|
+
let shouldDraw = false;
|
|
61609
|
+
|
|
61610
|
+
if (shape.shapeType === 'box') {
|
|
61611
|
+
const w = opts.width || 0;
|
|
61612
|
+
const h = opts.height || 0;
|
|
61613
|
+
const left = worldBaseX - w / 2;
|
|
61614
|
+
const top = worldBaseY - h / 2;
|
|
61615
|
+
shouldDraw = intersectsRegion(left, top, left + w, top + h);
|
|
61616
|
+
|
|
61617
|
+
if (shouldDraw) {
|
|
61618
|
+
const screenPos = camera.worldToScreen(left, top);
|
|
61619
|
+
ShapeDrawer.strokeRect(canvas, screenPos.x, screenPos.y, w, h, strokeThickness, color.r, color.g, color.b, color.a);
|
|
61620
|
+
}
|
|
61621
|
+
} else if (shape.shapeType === 'circle') {
|
|
61622
|
+
const rpx = opts.radius || 0;
|
|
61623
|
+
const left = worldBaseX - rpx;
|
|
61624
|
+
const top = worldBaseY - rpx;
|
|
61625
|
+
shouldDraw = intersectsRegion(left, top, worldBaseX + rpx, worldBaseY + rpx);
|
|
61626
|
+
|
|
61627
|
+
if (shouldDraw) {
|
|
61628
|
+
const screenPos = camera.worldToScreen(worldBaseX, worldBaseY);
|
|
61629
|
+
ShapeDrawer.strokeCircle(canvas, screenPos.x, screenPos.y, rpx, strokeThickness, color.r, color.g, color.b, color.a);
|
|
61630
|
+
}
|
|
61631
|
+
} else if (shape.shapeType === 'polygon' || shape.shapeType === 'poly') {
|
|
61632
|
+
const pts = opts.points || [];
|
|
61633
|
+
if (!pts.length) continue;
|
|
61634
|
+
|
|
61635
|
+
// Calculate AABB for polygon
|
|
61636
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
61637
|
+
for (let p = 0; p < pts.length; p++) {
|
|
61638
|
+
const wx = worldBaseX + pts[p].x;
|
|
61639
|
+
const wy = worldBaseY + pts[p].y;
|
|
61640
|
+
if (wx < minX) minX = wx;
|
|
61641
|
+
if (wy < minY) minY = wy;
|
|
61642
|
+
if (wx > maxX) maxX = wx;
|
|
61643
|
+
if (wy > maxY) maxY = wy;
|
|
61644
|
+
}
|
|
61645
|
+
|
|
61646
|
+
shouldDraw = intersectsRegion(minX, minY, maxX, maxY);
|
|
61647
|
+
|
|
61648
|
+
if (shouldDraw) {
|
|
61649
|
+
const worldPts = pts.map(p => ({ x: worldBaseX + p.x, y: worldBaseY + p.y }));
|
|
61650
|
+
const screenPts = worldPts.map(p => camera.worldToScreen(p.x, p.y));
|
|
61651
|
+
|
|
61652
|
+
if (shape.shapeType === 'polygon' || opts.closed === true) {
|
|
61653
|
+
PolygonDrawer.fillPolygon(canvas, screenPts, color.r, color.g, color.b, Math.floor(color.a * 0.2));
|
|
61654
|
+
PolygonDrawer.strokePolygon(canvas, screenPts, strokeThickness, color.r, color.g, color.b, color.a);
|
|
61655
|
+
} else {
|
|
61656
|
+
PolygonDrawer.strokePolygon(canvas, screenPts, strokeThickness, color.r, color.g, color.b, color.a);
|
|
61657
|
+
}
|
|
61658
|
+
}
|
|
61659
|
+
} else if (shape.shapeType === 'point') {
|
|
61660
|
+
shouldDraw = intersectsRegion(worldBaseX - 3, worldBaseY - 3, worldBaseX + 3, worldBaseY + 3);
|
|
61661
|
+
|
|
61662
|
+
if (shouldDraw) {
|
|
61663
|
+
const screenPos = camera.worldToScreen(worldBaseX, worldBaseY);
|
|
61664
|
+
ShapeDrawer.fillCircle(canvas, screenPos.x, screenPos.y, 3, color.r, color.g, color.b, color.a);
|
|
61665
|
+
}
|
|
61666
|
+
}
|
|
61667
|
+
}
|
|
61668
|
+
}
|
|
61669
|
+
}
|
|
61670
|
+
|
|
61671
|
+
draw(canvas, camera, cacheRegion = null, worldRegion = null) {
|
|
60412
61672
|
if (!this.debugCollisions) return;
|
|
60413
61673
|
|
|
61674
|
+
if (cacheRegion && worldRegion) {
|
|
61675
|
+
this.drawRegion(canvas, camera, cacheRegion, worldRegion);
|
|
61676
|
+
return;
|
|
61677
|
+
}
|
|
61678
|
+
|
|
60414
61679
|
const getColorForBody = (body) => {
|
|
60415
61680
|
switch ((body.type || '').toLowerCase()) {
|
|
60416
61681
|
case 'static': return { r: 120, g: 120, b: 120, a: 220 };
|
|
@@ -60625,6 +61890,19 @@ class Tiled extends Node {
|
|
|
60625
61890
|
this.opaque = opaque;
|
|
60626
61891
|
this.renderer = renderer;
|
|
60627
61892
|
|
|
61893
|
+
this.useCache = opts.useCache !== false; // default true
|
|
61894
|
+
this.cache = null;
|
|
61895
|
+
this.cacheInitialized = false;
|
|
61896
|
+
|
|
61897
|
+
// Full map pre-render strategy (Strategy 2)
|
|
61898
|
+
this.preRenderEntireMap = opts.preRenderEntireMap || false;
|
|
61899
|
+
this.fullMapBuffer = null;
|
|
61900
|
+
|
|
61901
|
+
// Chunked pre-render strategy (Strategy 3)
|
|
61902
|
+
this.useChunkedRender = opts.useChunkedRender || false;
|
|
61903
|
+
this.chunkedBuffer = null;
|
|
61904
|
+
this.chunkSize = opts.chunkSize || 256;
|
|
61905
|
+
|
|
60628
61906
|
if (path$1.extname(path) != ".json") {
|
|
60629
61907
|
throw new Error("expected a json map");
|
|
60630
61908
|
}
|
|
@@ -60645,6 +61923,20 @@ class Tiled extends Node {
|
|
|
60645
61923
|
tiledperf.start("processLayers");
|
|
60646
61924
|
this.processLayers(this.json.layers);
|
|
60647
61925
|
tiledperf.end("processLayers");
|
|
61926
|
+
|
|
61927
|
+
this._drawMethods = [
|
|
61928
|
+
function fallback(c, cam) {
|
|
61929
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
61930
|
+
this.layers[i].draw(c, cam);
|
|
61931
|
+
}
|
|
61932
|
+
}.bind(this),
|
|
61933
|
+
function fast(c, cam) {
|
|
61934
|
+
const strategy = this.cache.needsUpdate(cam);
|
|
61935
|
+
console.log("STRAT", strategy);
|
|
61936
|
+
this.cache.copyToDisplay(c, cam);
|
|
61937
|
+
}.bind(this)
|
|
61938
|
+
];
|
|
61939
|
+
|
|
60648
61940
|
}
|
|
60649
61941
|
|
|
60650
61942
|
processTilesets(tilesets) {
|
|
@@ -60665,12 +61957,191 @@ class Tiled extends Node {
|
|
|
60665
61957
|
|
|
60666
61958
|
|
|
60667
61959
|
this.collisionObjects = c;
|
|
60668
|
-
|
|
61960
|
+
|
|
61961
|
+
}
|
|
61962
|
+
|
|
61963
|
+
|
|
61964
|
+
initializeFullMapBuffer(camera) {
|
|
61965
|
+
if (!this.preRenderEntireMap) return;
|
|
61966
|
+
|
|
61967
|
+
console.log('[Tiled] Initializing full-map pre-render...');
|
|
61968
|
+
|
|
61969
|
+
// Calculate full map bounds in world space
|
|
61970
|
+
let mapWidth = this.json.width * this.json.tilewidth;
|
|
61971
|
+
let mapHeight = this.json.height * this.json.tileheight;
|
|
61972
|
+
|
|
61973
|
+
// Account for layer offsets
|
|
61974
|
+
for (const layer of this.layers) {
|
|
61975
|
+
if (layer.worldPosition) {
|
|
61976
|
+
const layerRightEdge = layer.worldPosition.x + (layer.layer?.width || 0) * this.json.tilewidth;
|
|
61977
|
+
const layerBottomEdge = layer.worldPosition.y + (layer.layer?.height || 0) * this.json.tileheight;
|
|
61978
|
+
mapWidth = Math.max(mapWidth, layerRightEdge);
|
|
61979
|
+
mapHeight = Math.max(mapHeight, layerBottomEdge);
|
|
61980
|
+
}
|
|
61981
|
+
}
|
|
61982
|
+
|
|
61983
|
+
this.fullMapBuffer = new FullMapPreRender(Math.ceil(mapWidth), Math.ceil(mapHeight));
|
|
61984
|
+
|
|
61985
|
+
// Render all layers to the full-map buffer
|
|
61986
|
+
this.fullMapBuffer.render((buffer) => {
|
|
61987
|
+
const fakeCamera = {
|
|
61988
|
+
worldPosition: { x: 0, y: 0 },
|
|
61989
|
+
zoom: 1,
|
|
61990
|
+
worldToScreen: (wx, wy) => ({ x: wx, y: wy }),
|
|
61991
|
+
getVisibleBounds: () => ({
|
|
61992
|
+
left: 0,
|
|
61993
|
+
top: 0,
|
|
61994
|
+
right: mapWidth,
|
|
61995
|
+
bottom: mapHeight,
|
|
61996
|
+
width: mapWidth,
|
|
61997
|
+
height: mapHeight
|
|
61998
|
+
}),
|
|
61999
|
+
cullingEnabled: true,
|
|
62000
|
+
frustum: { left: 0, right: mapWidth, top: 0, bottom: mapHeight }
|
|
62001
|
+
};
|
|
62002
|
+
|
|
62003
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
62004
|
+
this.layers[i].draw(buffer, fakeCamera);
|
|
62005
|
+
}
|
|
62006
|
+
});
|
|
62007
|
+
|
|
62008
|
+
console.log(`[Tiled] Full-map buffer rendered: ${mapWidth}x${mapHeight}`);
|
|
62009
|
+
}
|
|
62010
|
+
|
|
62011
|
+
initializeChunkedRender(camera) {
|
|
62012
|
+
if (!this.useChunkedRender) return;
|
|
62013
|
+
|
|
62014
|
+
console.log('[Tiled] Initializing chunked pre-render...');
|
|
62015
|
+
|
|
62016
|
+
// Calculate full map bounds in world space
|
|
62017
|
+
let mapWidth = this.json.width * this.json.tilewidth;
|
|
62018
|
+
let mapHeight = this.json.height * this.json.tileheight;
|
|
62019
|
+
|
|
62020
|
+
// Account for layer offsets
|
|
62021
|
+
for (const layer of this.layers) {
|
|
62022
|
+
if (layer.worldPosition) {
|
|
62023
|
+
const layerRightEdge = layer.worldPosition.x + (layer.layer?.width || 0) * this.json.tilewidth;
|
|
62024
|
+
const layerBottomEdge = layer.worldPosition.y + (layer.layer?.height || 0) * this.json.tileheight;
|
|
62025
|
+
mapWidth = Math.max(mapWidth, layerRightEdge);
|
|
62026
|
+
mapHeight = Math.max(mapHeight, layerBottomEdge);
|
|
62027
|
+
}
|
|
62028
|
+
}
|
|
62029
|
+
|
|
62030
|
+
this.chunkedBuffer = new ChunkedPreRender(Math.ceil(mapWidth), Math.ceil(mapHeight), this.chunkSize);
|
|
62031
|
+
|
|
62032
|
+
// Register render callback for all layers
|
|
62033
|
+
this.chunkedBuffer.onRenderChunk((buffer, region) => {
|
|
62034
|
+
const fakeCamera = {
|
|
62035
|
+
worldPosition: {
|
|
62036
|
+
x: region.x + region.width / 2,
|
|
62037
|
+
y: region.y + region.height / 2
|
|
62038
|
+
},
|
|
62039
|
+
zoom: 1,
|
|
62040
|
+
worldToScreen: (wx, wy) => ({
|
|
62041
|
+
x: wx - region.x,
|
|
62042
|
+
y: wy - region.y
|
|
62043
|
+
}),
|
|
62044
|
+
getVisibleBounds: () => ({
|
|
62045
|
+
left: region.x,
|
|
62046
|
+
top: region.y,
|
|
62047
|
+
right: region.x + region.width,
|
|
62048
|
+
bottom: region.y + region.height,
|
|
62049
|
+
width: region.width,
|
|
62050
|
+
height: region.height
|
|
62051
|
+
}),
|
|
62052
|
+
cullingEnabled: true,
|
|
62053
|
+
frustum: {
|
|
62054
|
+
left: region.x,
|
|
62055
|
+
right: region.x + region.width,
|
|
62056
|
+
top: region.y,
|
|
62057
|
+
bottom: region.y + region.height
|
|
62058
|
+
}
|
|
62059
|
+
};
|
|
62060
|
+
|
|
62061
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
62062
|
+
this.layers[i].draw(buffer, fakeCamera);
|
|
62063
|
+
}
|
|
62064
|
+
});
|
|
62065
|
+
|
|
62066
|
+
// Pre-render all chunks
|
|
62067
|
+
this.chunkedBuffer.renderAllChunks();
|
|
62068
|
+
console.log(`[Tiled] Chunked render initialized: ${mapWidth}x${mapHeight} into ${this.chunkSize}x${this.chunkSize} chunks`);
|
|
62069
|
+
}
|
|
62070
|
+
|
|
62071
|
+
initializeCache(viewportWidth, viewportHeight, camera) {
|
|
62072
|
+
if (!this.useCache) return;
|
|
62073
|
+
|
|
62074
|
+
console.log('[Tiled] Initializing offscreen cache...');
|
|
62075
|
+
|
|
62076
|
+
// Create cache
|
|
62077
|
+
this.cache = new OffscreenTilemapCache(viewportWidth, viewportHeight);
|
|
62078
|
+
|
|
62079
|
+
// Set cache to cover camera view with margin
|
|
62080
|
+
const bounds = camera.getVisibleBounds();
|
|
62081
|
+
|
|
62082
|
+
// Center cache on camera
|
|
62083
|
+
// this.cache.setWorldRegion(
|
|
62084
|
+
// camera.worldPosition.x - halfViewW - this.cache.marginPixels.x,
|
|
62085
|
+
// camera.worldPosition.y - halfViewH - this.cache.marginPixels.y
|
|
62086
|
+
// );
|
|
62087
|
+
this.cache.setWorldRegion(bounds.left, bounds.top);
|
|
62088
|
+
|
|
62089
|
+
|
|
62090
|
+
// Render all layers to cache
|
|
62091
|
+
this._renderToCache();
|
|
62092
|
+
|
|
62093
|
+
this.cacheInitialized = true;
|
|
62094
|
+
this.cache.fullRedraws++;
|
|
62095
|
+
|
|
62096
|
+
console.log('[Tiled] Cache initialized and rendered');
|
|
62097
|
+
}
|
|
62098
|
+
|
|
62099
|
+
|
|
62100
|
+
_renderToCache() {
|
|
62101
|
+
tiledperf.start("_renderToCache");
|
|
62102
|
+
if (!this.cache) {
|
|
62103
|
+
tiledperf.end("_renderToCache");
|
|
62104
|
+
return;
|
|
62105
|
+
}
|
|
62106
|
+
|
|
62107
|
+
|
|
62108
|
+
this.cache.clear();
|
|
62109
|
+
|
|
62110
|
+
const fakeCamera = {
|
|
62111
|
+
worldPosition: {
|
|
62112
|
+
x: this.cache.worldX + this.cache.cacheWidth / 2,
|
|
62113
|
+
y: this.cache.worldY + this.cache.cacheHeight / 2
|
|
62114
|
+
},
|
|
62115
|
+
zoom: 1,
|
|
62116
|
+
worldToScreen: (wx, wy) => {
|
|
62117
|
+
return {
|
|
62118
|
+
x: wx - this.cache.worldX,
|
|
62119
|
+
y: wy - this.cache.worldY
|
|
62120
|
+
};
|
|
62121
|
+
},
|
|
62122
|
+
getVisibleBounds: () => this.cache.getWorldBounds(),
|
|
62123
|
+
cullingEnabled: true,
|
|
62124
|
+
frustum: {
|
|
62125
|
+
left: this.cache.worldX,
|
|
62126
|
+
right: this.cache.worldX + this.cache.cacheWidth,
|
|
62127
|
+
top: this.cache.worldY,
|
|
62128
|
+
bottom: this.cache.worldY + this.cache.cacheHeight
|
|
62129
|
+
}
|
|
62130
|
+
};
|
|
62131
|
+
|
|
62132
|
+
// draw all layers to cache buffer
|
|
62133
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
62134
|
+
this.layers[i].draw(this.cache.buffer, fakeCamera);
|
|
62135
|
+
}
|
|
62136
|
+
tiledperf.end("_renderToCache");
|
|
60669
62137
|
}
|
|
62138
|
+
|
|
62139
|
+
|
|
62140
|
+
|
|
60670
62141
|
_ready() {
|
|
60671
62142
|
for (let i = 0; i < this.collisionObjects.length; i++)
|
|
60672
62143
|
this.parent.addChild(this.collisionObjects[i]);
|
|
60673
|
-
|
|
62144
|
+
console.log("added ", this.collisionObjects.length, " bodies");
|
|
60674
62145
|
}
|
|
60675
62146
|
processLayers(layers) {
|
|
60676
62147
|
for (let i = 0; i < layers.length; i++) {
|
|
@@ -60701,11 +62172,203 @@ class Tiled extends Node {
|
|
|
60701
62172
|
}
|
|
60702
62173
|
}
|
|
60703
62174
|
|
|
60704
|
-
|
|
60705
|
-
|
|
62175
|
+
|
|
62176
|
+
|
|
62177
|
+
_renderRegionToCache(cacheRegion, worldRegion) {
|
|
62178
|
+
tiledperf.start("_renderRegionToCache");
|
|
62179
|
+
if (!this.cache) {
|
|
62180
|
+
tiledperf.end("_renderRegionToCache");
|
|
62181
|
+
return;
|
|
62182
|
+
}
|
|
62183
|
+
|
|
62184
|
+
// Create fake camera for this region
|
|
62185
|
+
const fakeCamera = {
|
|
62186
|
+
worldPosition: {
|
|
62187
|
+
x: this.cache.worldX + this.cache.cacheWidth / 2,
|
|
62188
|
+
y: this.cache.worldY + this.cache.cacheHeight / 2
|
|
62189
|
+
},
|
|
62190
|
+
zoom: 1,
|
|
62191
|
+
worldToScreen: (wx, wy) => {
|
|
62192
|
+
return {
|
|
62193
|
+
x: wx - this.cache.worldX,
|
|
62194
|
+
y: wy - this.cache.worldY
|
|
62195
|
+
};
|
|
62196
|
+
},
|
|
62197
|
+
getVisibleBounds: () => ({
|
|
62198
|
+
left: worldRegion.x,
|
|
62199
|
+
top: worldRegion.y,
|
|
62200
|
+
right: worldRegion.x + worldRegion.width,
|
|
62201
|
+
bottom: worldRegion.y + worldRegion.height,
|
|
62202
|
+
width: worldRegion.width,
|
|
62203
|
+
height: worldRegion.height
|
|
62204
|
+
}),
|
|
62205
|
+
cullingEnabled: true,
|
|
62206
|
+
frustum: {
|
|
62207
|
+
left: worldRegion.x,
|
|
62208
|
+
right: worldRegion.x + worldRegion.width,
|
|
62209
|
+
top: worldRegion.y,
|
|
62210
|
+
bottom: worldRegion.y + worldRegion.height
|
|
62211
|
+
}
|
|
62212
|
+
};
|
|
62213
|
+
|
|
62214
|
+
// Draw each layer's region
|
|
60706
62215
|
for (let i = 0; i < this.layers.length; i++) {
|
|
60707
|
-
this.layers[i].draw(
|
|
62216
|
+
this.layers[i].draw(this.cache.buffer, fakeCamera, cacheRegion, worldRegion);
|
|
62217
|
+
}
|
|
62218
|
+
tiledperf.end("_renderRegionToCache");
|
|
62219
|
+
}
|
|
62220
|
+
|
|
62221
|
+
_draw(canvas, camera) {
|
|
62222
|
+
tiledperf.start("_draw");
|
|
62223
|
+
|
|
62224
|
+
// Strategy 3: Chunked pre-render
|
|
62225
|
+
if (this.useChunkedRender && !this.chunkedBuffer) {
|
|
62226
|
+
this.initializeChunkedRender(camera);
|
|
62227
|
+
}
|
|
62228
|
+
|
|
62229
|
+
if (this.useChunkedRender && this.chunkedBuffer) {
|
|
62230
|
+
tiledperf.start("chunkedBuffer_copy");
|
|
62231
|
+
this.chunkedBuffer.copyToDisplay(canvas, camera);
|
|
62232
|
+
tiledperf.end("chunkedBuffer_copy");
|
|
62233
|
+
tiledperf.end("_draw");
|
|
62234
|
+
return;
|
|
62235
|
+
}
|
|
62236
|
+
|
|
62237
|
+
// Strategy 2: Full-map pre-render
|
|
62238
|
+
if (this.preRenderEntireMap && !this.fullMapBuffer) {
|
|
62239
|
+
this.initializeFullMapBuffer(camera);
|
|
62240
|
+
}
|
|
62241
|
+
|
|
62242
|
+
if (this.preRenderEntireMap && this.fullMapBuffer) {
|
|
62243
|
+
tiledperf.start("fullMapBuffer_copy");
|
|
62244
|
+
this.fullMapBuffer.copyToDisplay(canvas, camera);
|
|
62245
|
+
tiledperf.end("fullMapBuffer_copy");
|
|
62246
|
+
tiledperf.end("_draw");
|
|
62247
|
+
return;
|
|
62248
|
+
}
|
|
62249
|
+
|
|
62250
|
+
// Strategy 1: Incremental offscreen cache
|
|
62251
|
+
// First frame: initialize
|
|
62252
|
+
if (this.useCache && !this.cacheInitialized) {
|
|
62253
|
+
this.initializeCache(canvas.width, canvas.height, camera);
|
|
62254
|
+
}
|
|
62255
|
+
|
|
62256
|
+
if (this.useCache && this.cache) {
|
|
62257
|
+
// Determine what kind of update we need
|
|
62258
|
+
const strategy = this.cache.needsUpdate(camera);
|
|
62259
|
+
|
|
62260
|
+
switch (strategy.type) {
|
|
62261
|
+
case 'NONE':
|
|
62262
|
+
tiledperf.start("cacheUpdate_NONE");
|
|
62263
|
+
// Camera didn't move - just copy from cache
|
|
62264
|
+
this.cache.skippedUpdates++;
|
|
62265
|
+
tiledperf.end("cacheUpdate_NONE");
|
|
62266
|
+
break;
|
|
62267
|
+
|
|
62268
|
+
case 'INCREMENTAL':
|
|
62269
|
+
tiledperf.start("cacheUpdate_INCREMENTAL");
|
|
62270
|
+
// Small move - incremental update (Chapter 4 will implement this)
|
|
62271
|
+
// For now, treat as full redraw
|
|
62272
|
+
// console.log(`[Tiled] Incremental update needed (${strategy.reason}), delta: ${strategy.delta.x.toFixed(1)}, ${strategy.delta.y.toFixed(1)}`);
|
|
62273
|
+
// this._fullCacheRedraw(camera);
|
|
62274
|
+
this.cache.incrementalUpdates++;
|
|
62275
|
+
this.cache.incrementalUpdate(
|
|
62276
|
+
strategy.delta,
|
|
62277
|
+
(cacheRegion, worldRegion) => {
|
|
62278
|
+
this._renderRegionToCache(cacheRegion, worldRegion);
|
|
62279
|
+
}
|
|
62280
|
+
);
|
|
62281
|
+
tiledperf.end("cacheUpdate_INCREMENTAL");
|
|
62282
|
+
break;
|
|
62283
|
+
|
|
62284
|
+
case 'FULL_REDRAW':
|
|
62285
|
+
tiledperf.start("cacheUpdate_FULL_REDRAW");
|
|
62286
|
+
// Teleport or zoom - need full redraw
|
|
62287
|
+
console.log(`[Tiled] Full redraw needed (${strategy.reason})`);
|
|
62288
|
+
this._fullCacheRedraw(camera);
|
|
62289
|
+
this.cache.fullRedraws++;
|
|
62290
|
+
tiledperf.end("cacheUpdate_FULL_REDRAW");
|
|
62291
|
+
break;
|
|
62292
|
+
}
|
|
62293
|
+
|
|
62294
|
+
// Always copy cache to display
|
|
62295
|
+
tiledperf.start("copyToDisplay");
|
|
62296
|
+
this.cache.copyToDisplay(canvas, camera);
|
|
62297
|
+
tiledperf.end("copyToDisplay");
|
|
62298
|
+
|
|
62299
|
+
// Commit frame
|
|
62300
|
+
this.cache.commitFrame();
|
|
62301
|
+
|
|
62302
|
+
} else {
|
|
62303
|
+
// Fallback: no cache
|
|
62304
|
+
tiledperf.start("directLayerDraw");
|
|
62305
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
62306
|
+
this.layers[i].draw(canvas, camera);
|
|
62307
|
+
}
|
|
62308
|
+
tiledperf.end("directLayerDraw");
|
|
60708
62309
|
}
|
|
62310
|
+
tiledperf.end("_draw");
|
|
62311
|
+
}
|
|
62312
|
+
|
|
62313
|
+
// Full cache redraw - recenter and render
|
|
62314
|
+
_fullCacheRedraw(camera) {
|
|
62315
|
+
tiledperf.start("_fullCacheRedraw");
|
|
62316
|
+
this.cache.viewportWidth / 2;
|
|
62317
|
+
this.cache.viewportHeight / 2;
|
|
62318
|
+
|
|
62319
|
+
const bounds = camera.getVisibleBounds();
|
|
62320
|
+
// Recenter cache on camera
|
|
62321
|
+
// this.cache.setWorldRegion(
|
|
62322
|
+
// camera.worldPosition.x - halfViewW - this.cache.marginPixels.x,
|
|
62323
|
+
// camera.worldPosition.y - halfViewH - this.cache.marginPixels.y
|
|
62324
|
+
// );
|
|
62325
|
+
|
|
62326
|
+
this.cache.setWorldRegion(bounds.left, bounds.top);
|
|
62327
|
+
|
|
62328
|
+
this._renderToCache();
|
|
62329
|
+
tiledperf.end("_fullCacheRedraw");
|
|
62330
|
+
}
|
|
62331
|
+
|
|
62332
|
+
|
|
62333
|
+
// _draw(canvas, camera) {
|
|
62334
|
+
// (this.useCache && !this.cacheInitialized) && (
|
|
62335
|
+
// this.initializeCache(canvas.width, canvas.height, camera),
|
|
62336
|
+
// this.cache.printStats(),
|
|
62337
|
+
// this._drawMethods[1] = function fast(c, cam) { this.cache.copyToDisplay(c, cam); }.bind(this) // update after init
|
|
62338
|
+
// );
|
|
62339
|
+
|
|
62340
|
+
// const useFast = +(this.useCache && !!this.cache);
|
|
62341
|
+
// this._drawMethods[useFast](canvas, camera);
|
|
62342
|
+
// }
|
|
62343
|
+
// _draw(canvas, camera) {
|
|
62344
|
+
// if (this.useCache && !this.cacheInitialized) {
|
|
62345
|
+
// this.initializeCache(canvas.width, canvas.height, camera);
|
|
62346
|
+
// this.cache.printStats()
|
|
62347
|
+
// }
|
|
62348
|
+
|
|
62349
|
+
// if (this.useCache && this.cache) {
|
|
62350
|
+
// // Fast path: copy from cache
|
|
62351
|
+
// this.cache.copyToDisplay(canvas, camera);
|
|
62352
|
+
// } else {
|
|
62353
|
+
// // Fallback: direct render (old way)
|
|
62354
|
+
// // polymorphic draw - each layer handles its own rendering
|
|
62355
|
+
// for (let i = 0; i < this.layers.length; i++) {
|
|
62356
|
+
// this.layers[i].draw(canvas, camera);
|
|
62357
|
+
// }
|
|
62358
|
+
// }
|
|
62359
|
+
|
|
62360
|
+
// }
|
|
62361
|
+
|
|
62362
|
+
needsCacheUpdate(camera) {
|
|
62363
|
+
if (!this.cache) return false;
|
|
62364
|
+
|
|
62365
|
+
const bounds = this.cache.getWorldBounds();
|
|
62366
|
+
const camX = camera.worldPosition.x;
|
|
62367
|
+
const camY = camera.worldPosition.y;
|
|
62368
|
+
|
|
62369
|
+
// Simple check: is camera center still inside cache bounds?
|
|
62370
|
+
return camX < bounds.left || camX > bounds.right ||
|
|
62371
|
+
camY < bounds.top || camY > bounds.bottom;
|
|
60709
62372
|
}
|
|
60710
62373
|
|
|
60711
62374
|
}
|