@rickyzhangca/fpsr 0.0.1
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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/abort-CrgARaZd.js +43 -0
- package/dist/abort-CrgARaZd.js.map +1 -0
- package/dist/assets-Dy4fqZBV.d.ts +85 -0
- package/dist/assets-Dy4fqZBV.d.ts.map +1 -0
- package/dist/canvas.d.ts +98 -0
- package/dist/canvas.d.ts.map +1 -0
- package/dist/canvas.js +3 -0
- package/dist/execute-CRZV5zw1.js +963 -0
- package/dist/execute-CRZV5zw1.js.map +1 -0
- package/dist/host-DdX4g1YM.d.ts +34 -0
- package/dist/host-DdX4g1YM.d.ts.map +1 -0
- package/dist/index.d.ts +463 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1776 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +16 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +101 -0
- package/dist/node.js.map +1 -0
- package/dist/planner.d.ts +418 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +96 -0
- package/dist/planner.js.map +1 -0
- package/dist/profile-CTN1Q5SI.d.ts +365 -0
- package/dist/profile-CTN1Q5SI.d.ts.map +1 -0
- package/dist/profile-DR4i6HDp.js +610 -0
- package/dist/profile-DR4i6HDp.js.map +1 -0
- package/dist/render-db-BFQIspDB.d.ts +426 -0
- package/dist/render-db-BFQIspDB.d.ts.map +1 -0
- package/dist/render-db.d.ts +2 -0
- package/dist/render-db.js +1 -0
- package/dist/tiled-draw-list-Dc-5Ledi.js +3990 -0
- package/dist/tiled-draw-list-Dc-5Ledi.js.map +1 -0
- package/dist/types-CHgZBSra.d.ts +105 -0
- package/dist/types-CHgZBSra.d.ts.map +1 -0
- package/dist/upgrade-planner-CNjnMHPJ.d.ts +385 -0
- package/dist/upgrade-planner-CNjnMHPJ.d.ts.map +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,963 @@
|
|
|
1
|
+
import { S as RENDER_LAYERS, o as TRAIN_CHAIN_JOINT_RADIUS, p as entityInfoSilhouettePadPx } from "./profile-DR4i6HDp.js";
|
|
2
|
+
//#region src/text-font.ts
|
|
3
|
+
/** CSS / canvas font-family registered from pipeline DejaVuSans.ttf. */
|
|
4
|
+
const FPSR_TEXT_FONT_FAMILY = "fpsr-dejavu";
|
|
5
|
+
/** Fallback stack when the bundled TTF is unavailable. */
|
|
6
|
+
const FPSR_TEXT_FONT_FALLBACK = "DejaVu Sans, sans-serif";
|
|
7
|
+
function fpsrTextFontCss(sizePx) {
|
|
8
|
+
return `${sizePx}px ${FPSR_TEXT_FONT_FAMILY}, ${FPSR_TEXT_FONT_FALLBACK}`;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/checkerboard.ts
|
|
12
|
+
const TILE_LIGHT = "#1a1a1a";
|
|
13
|
+
const TILE_DARK = "#252525";
|
|
14
|
+
/** Fill `width`×`height` with an axis-aligned checkerboard; one cell = `pixelsPerTile` px. */
|
|
15
|
+
function drawTileCheckerboard(ctx, width, height, pixelsPerTile, tileOffsetX = 0, tileOffsetY = 0) {
|
|
16
|
+
const ppt = Math.max(1, pixelsPerTile);
|
|
17
|
+
const columns = Math.ceil(width / ppt);
|
|
18
|
+
const rows = Math.ceil(height / ppt);
|
|
19
|
+
for (let cellY = 0; cellY < rows; cellY++) {
|
|
20
|
+
const y = cellY * ppt;
|
|
21
|
+
for (let cellX = 0; cellX < columns; cellX++) {
|
|
22
|
+
const x = cellX * ppt;
|
|
23
|
+
ctx.fillStyle = (cellX + tileOffsetX + cellY + tileOffsetY) % 2 === 0 ? TILE_DARK : TILE_LIGHT;
|
|
24
|
+
ctx.fillRect(x, y, Math.min(ppt, width - x), Math.min(ppt, height - y));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Paint an image on top of a tile-aligned checkerboard (for external reference PNGs). */
|
|
29
|
+
function blitWithTileCheckerboard(canvas, image, width, height, pixelsPerTile) {
|
|
30
|
+
canvas.width = width;
|
|
31
|
+
canvas.height = height;
|
|
32
|
+
const ctx = canvas.getContext("2d");
|
|
33
|
+
if (!ctx) return;
|
|
34
|
+
drawTileCheckerboard(ctx, width, height, pixelsPerTile);
|
|
35
|
+
ctx.imageSmoothingEnabled = false;
|
|
36
|
+
ctx.drawImage(image, 0, 0, width, height);
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/coordinate-overlay.ts
|
|
40
|
+
const GRID_STROKE = "rgba(255, 255, 255, 0.28)";
|
|
41
|
+
const LABEL_FILL = "rgba(255, 255, 255, 0.85)";
|
|
42
|
+
const LABEL_SHADOW = "rgba(0, 0, 0, 0.75)";
|
|
43
|
+
/** How often to place a cell label based on pixels-per-tile. */
|
|
44
|
+
function labelStride(pixelsPerTile) {
|
|
45
|
+
if (pixelsPerTile < 12) return 4;
|
|
46
|
+
if (pixelsPerTile < 24) return 2;
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Draw map-space tile grid lines and coordinate labels over a rendered preview.
|
|
51
|
+
* Canvas (0,0) corresponds to `tileFrame.minX/minY`.
|
|
52
|
+
*/
|
|
53
|
+
function drawCoordinateOverlay(ctx, tileFrame, pixelsPerTile, width, height, outputTileFrame = tileFrame) {
|
|
54
|
+
const { minX, minY, maxX, maxY } = tileFrame;
|
|
55
|
+
const cols = maxX - minX;
|
|
56
|
+
const rows = maxY - minY;
|
|
57
|
+
if (cols <= 0 || rows <= 0 || pixelsPerTile <= 0) return;
|
|
58
|
+
ctx.save();
|
|
59
|
+
ctx.strokeStyle = GRID_STROKE;
|
|
60
|
+
ctx.lineWidth = 1;
|
|
61
|
+
ctx.beginPath();
|
|
62
|
+
for (let i = 0; i <= cols; i++) {
|
|
63
|
+
const x = i * pixelsPerTile + .5;
|
|
64
|
+
ctx.moveTo(x, 0);
|
|
65
|
+
ctx.lineTo(x, height);
|
|
66
|
+
}
|
|
67
|
+
for (let j = 0; j <= rows; j++) {
|
|
68
|
+
const y = j * pixelsPerTile + .5;
|
|
69
|
+
ctx.moveTo(0, y);
|
|
70
|
+
ctx.lineTo(width, y);
|
|
71
|
+
}
|
|
72
|
+
ctx.stroke();
|
|
73
|
+
const stride = labelStride(pixelsPerTile);
|
|
74
|
+
ctx.font = `${Math.max(8, Math.min(12, Math.floor(pixelsPerTile * .35)))}px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace`;
|
|
75
|
+
ctx.textBaseline = "top";
|
|
76
|
+
ctx.textAlign = "left";
|
|
77
|
+
const pad = Math.max(1, Math.floor(pixelsPerTile * .06));
|
|
78
|
+
const columnStart = ((outputTileFrame.minX - minX) % stride + stride) % stride;
|
|
79
|
+
const rowStart = ((outputTileFrame.minY - minY) % stride + stride) % stride;
|
|
80
|
+
for (let j = rowStart; j < rows; j += stride) for (let i = columnStart; i < cols; i += stride) {
|
|
81
|
+
const label = `${minX + i},${minY + j}`;
|
|
82
|
+
const x = i * pixelsPerTile + pad;
|
|
83
|
+
const y = j * pixelsPerTile + pad;
|
|
84
|
+
ctx.fillStyle = LABEL_SHADOW;
|
|
85
|
+
ctx.fillText(label, x + 1, y + 1);
|
|
86
|
+
ctx.fillStyle = LABEL_FILL;
|
|
87
|
+
ctx.fillText(label, x, y);
|
|
88
|
+
}
|
|
89
|
+
ctx.restore();
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/space-background.ts
|
|
93
|
+
const SPACE_FILL = "#000000";
|
|
94
|
+
const DEFAULT_STAR_SEED = 1374772855;
|
|
95
|
+
/** Incommensurate grids overlap into a starfield without a visible repeated lattice. */
|
|
96
|
+
const STAR_LAYERS = [
|
|
97
|
+
{
|
|
98
|
+
cellSize: 17,
|
|
99
|
+
occupancy: .06,
|
|
100
|
+
brightness: [.24, .5],
|
|
101
|
+
largeChance: 0,
|
|
102
|
+
salt: 11
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
cellSize: 37,
|
|
106
|
+
occupancy: .13,
|
|
107
|
+
brightness: [.42, .76],
|
|
108
|
+
largeChance: .04,
|
|
109
|
+
salt: 29
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
cellSize: 79,
|
|
113
|
+
occupancy: .28,
|
|
114
|
+
brightness: [.68, 1],
|
|
115
|
+
largeChance: .36,
|
|
116
|
+
salt: 47
|
|
117
|
+
}
|
|
118
|
+
];
|
|
119
|
+
/**
|
|
120
|
+
* Factorio `platform_backdrop` placement for Nauvis, expressed relative to
|
|
121
|
+
* screen center with radius 600 on a ~1920×1080 reference viewport:
|
|
122
|
+
* `position = {-680, 601}`, `radius = 600`.
|
|
123
|
+
*/
|
|
124
|
+
const PLANET_OFFSET_X_OVER_RADIUS = -680 / 600;
|
|
125
|
+
const PLANET_OFFSET_Y_OVER_RADIUS = 601 / 600;
|
|
126
|
+
/** Planet radius as a fraction of the shorter canvas edge. */
|
|
127
|
+
const PLANET_RADIUS_FRACTION = .5;
|
|
128
|
+
/** Floor so tiny previews still get a readable bottom-left peek. */
|
|
129
|
+
const PLANET_MIN_RADIUS_PX = 140;
|
|
130
|
+
function packedWidth$2(frame) {
|
|
131
|
+
return frame.pw ?? frame.w;
|
|
132
|
+
}
|
|
133
|
+
function packedHeight$2(frame) {
|
|
134
|
+
return frame.ph ?? frame.h;
|
|
135
|
+
}
|
|
136
|
+
/** Deterministic 0..1 hash from integer cell coordinates. */
|
|
137
|
+
const cellHash = (cellX, cellY, salt) => {
|
|
138
|
+
let h = Math.imul(cellX, 374761393) ^ Math.imul(cellY, 668265263) ^ Math.imul(salt, 1442695041);
|
|
139
|
+
h = Math.imul(h ^ h >>> 13, 1274126177);
|
|
140
|
+
return ((h ^ h >>> 16) >>> 0) / 4294967296;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Draw the space-platform planet peeking from the bottom-left, matching
|
|
144
|
+
* Factorio's backdrop offset ratios scaled to the canvas.
|
|
145
|
+
*/
|
|
146
|
+
function drawSpacePlanet(ctx, width, height, planet, viewport) {
|
|
147
|
+
if (width <= 0 || height <= 0) return;
|
|
148
|
+
const { frame, image } = planet;
|
|
149
|
+
if (frame.sw <= 0 || frame.sh <= 0 || frame.w <= 0 || frame.h <= 0) return;
|
|
150
|
+
const fullWidth = viewport?.fullWidth ?? width;
|
|
151
|
+
const fullHeight = viewport?.fullHeight ?? height;
|
|
152
|
+
const radius = Math.max(PLANET_MIN_RADIUS_PX, PLANET_RADIUS_FRACTION * Math.min(fullWidth, fullHeight));
|
|
153
|
+
const cx = fullWidth / 2 + PLANET_OFFSET_X_OVER_RADIUS * radius - (viewport?.x ?? 0);
|
|
154
|
+
const cy = fullHeight / 2 + PLANET_OFFSET_Y_OVER_RADIUS * radius - (viewport?.y ?? 0);
|
|
155
|
+
const scale = radius * 2 / Math.max(frame.sw, frame.sh);
|
|
156
|
+
const previousSmoothing = ctx.imageSmoothingEnabled;
|
|
157
|
+
ctx.imageSmoothingEnabled = true;
|
|
158
|
+
ctx.drawImage(image, frame.x, frame.y, packedWidth$2(frame), packedHeight$2(frame), cx - radius + frame.ox * scale, cy - radius + frame.oy * scale, frame.w * scale, frame.h * scale);
|
|
159
|
+
ctx.imageSmoothingEnabled = previousSmoothing;
|
|
160
|
+
}
|
|
161
|
+
function smoothstep(value) {
|
|
162
|
+
return value * value * (3 - 2 * value);
|
|
163
|
+
}
|
|
164
|
+
function lerp(from, to, amount) {
|
|
165
|
+
return from + (to - from) * amount;
|
|
166
|
+
}
|
|
167
|
+
/** Smooth low-frequency value noise used only to vary local star density. */
|
|
168
|
+
function densityNoise(x, y, seed) {
|
|
169
|
+
const cellX = Math.floor(x);
|
|
170
|
+
const cellY = Math.floor(y);
|
|
171
|
+
const tx = smoothstep(x - cellX);
|
|
172
|
+
const ty = smoothstep(y - cellY);
|
|
173
|
+
return lerp(lerp(cellHash(cellX, cellY, seed), cellHash(cellX + 1, cellY, seed), tx), lerp(cellHash(cellX, cellY + 1, seed), cellHash(cellX + 1, cellY + 1, seed), tx), ty);
|
|
174
|
+
}
|
|
175
|
+
function starDensity(x, y, seed) {
|
|
176
|
+
const broad = densityNoise(x / 310, y / 310, seed ^ 1831565813);
|
|
177
|
+
const detail = densityNoise(x / 137, y / 137, seed ^ 461845907);
|
|
178
|
+
return .36 + (broad * .68 + detail * .32) * .88;
|
|
179
|
+
}
|
|
180
|
+
function drawStar(ctx, width, height, x, y, size, color, sparkle) {
|
|
181
|
+
const left = Math.max(0, x);
|
|
182
|
+
const top = Math.max(0, y);
|
|
183
|
+
const clippedWidth = Math.min(width, x + size) - left;
|
|
184
|
+
const clippedHeight = Math.min(height, y + size) - top;
|
|
185
|
+
if (clippedWidth <= 0 || clippedHeight <= 0) return;
|
|
186
|
+
ctx.fillStyle = color;
|
|
187
|
+
ctx.fillRect(left, top, clippedWidth, clippedHeight);
|
|
188
|
+
if (!sparkle) return;
|
|
189
|
+
ctx.fillRect(x - 1, y, 4, 1);
|
|
190
|
+
ctx.fillRect(x, y - 1, 1, 4);
|
|
191
|
+
}
|
|
192
|
+
/** Fill `width`×`height` with a layered deterministic starfield. */
|
|
193
|
+
function drawSpaceBackground(ctx, width, height, options) {
|
|
194
|
+
if (width <= 0 || height <= 0) return;
|
|
195
|
+
ctx.fillStyle = SPACE_FILL;
|
|
196
|
+
ctx.fillRect(0, 0, width, height);
|
|
197
|
+
const seed = options?.seed == null ? DEFAULT_STAR_SEED : options.seed >>> 0;
|
|
198
|
+
const viewportX = options?.viewport?.x ?? 0;
|
|
199
|
+
const viewportY = options?.viewport?.y ?? 0;
|
|
200
|
+
for (const layer of STAR_LAYERS) {
|
|
201
|
+
const firstCellX = Math.max(0, Math.floor(viewportX / layer.cellSize) - 1);
|
|
202
|
+
const firstCellY = Math.max(0, Math.floor(viewportY / layer.cellSize) - 1);
|
|
203
|
+
const lastCellX = Math.ceil((viewportX + width) / layer.cellSize);
|
|
204
|
+
const lastCellY = Math.ceil((viewportY + height) / layer.cellSize);
|
|
205
|
+
const layerSeed = seed ^ Math.imul(layer.salt, 2654435761);
|
|
206
|
+
for (let cellY = firstCellY; cellY < lastCellY; cellY++) for (let cellX = firstCellX; cellX < lastCellX; cellX++) {
|
|
207
|
+
const xUnit = cellHash(cellX, cellY, layerSeed ^ 2);
|
|
208
|
+
const yUnit = cellHash(cellX, cellY, layerSeed ^ 3);
|
|
209
|
+
const globalX = cellX * layer.cellSize + Math.floor(xUnit * layer.cellSize);
|
|
210
|
+
const globalY = cellY * layer.cellSize + Math.floor(yUnit * layer.cellSize);
|
|
211
|
+
const density = starDensity(globalX, globalY, seed);
|
|
212
|
+
if (cellHash(cellX, cellY, layerSeed ^ 1) > layer.occupancy * density) continue;
|
|
213
|
+
const brightness = lerp(layer.brightness[0], layer.brightness[1], cellHash(cellX, cellY, layerSeed ^ 4));
|
|
214
|
+
const warmth = cellHash(cellX, cellY, layerSeed ^ 5) - .5;
|
|
215
|
+
const red = Math.round(Math.min(1, brightness * (1 + warmth * .12)) * 255);
|
|
216
|
+
const green = Math.round(brightness * (1 - Math.abs(warmth) * .035) * 255);
|
|
217
|
+
const blue = Math.round(Math.min(1, brightness * (1 - warmth * .16)) * 255);
|
|
218
|
+
const size = cellHash(cellX, cellY, layerSeed ^ 6) < layer.largeChance ? 2 : 1;
|
|
219
|
+
const sparkle = size === 2 && cellHash(cellX, cellY, layerSeed ^ 7) > .86;
|
|
220
|
+
drawStar(ctx, width, height, globalX - viewportX, globalY - viewportY, size, `rgb(${red},${green},${blue})`, sparkle);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (options?.planet) drawSpacePlanet(ctx, width, height, options.planet, options.viewport);
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/terrain-background.ts
|
|
227
|
+
const UINT32_RANGE = 4294967296;
|
|
228
|
+
const VARIANT_SALT = 2654435769;
|
|
229
|
+
const PATCH_SIZE_SALT = 2246822507;
|
|
230
|
+
/** Keep enough smaller patches in uniform terrain to break up large repeated motifs. */
|
|
231
|
+
const MAX_LARGEST_PATCH_PROBABILITY = .4;
|
|
232
|
+
const MAX_INTERMEDIATE_PATCH_PROBABILITY = .7;
|
|
233
|
+
function packedWidth$1(frame) {
|
|
234
|
+
return frame.pw ?? frame.w;
|
|
235
|
+
}
|
|
236
|
+
function packedHeight$1(frame) {
|
|
237
|
+
return frame.ph ?? frame.h;
|
|
238
|
+
}
|
|
239
|
+
/** Stable unsigned hash for an absolute terrain-patch cell and background salt. */
|
|
240
|
+
function terrainCellHash(cellX, cellY, salt) {
|
|
241
|
+
let hash = Math.imul(cellX | 0, 374761393) ^ Math.imul(cellY | 0, 668265263) ^ Math.imul(salt | 0, 1597334677) | 0;
|
|
242
|
+
hash = Math.imul(hash ^ hash >>> 13, 1274126177);
|
|
243
|
+
return (hash ^ hash >>> 16) >>> 0;
|
|
244
|
+
}
|
|
245
|
+
/** Derive a fallback salt for pre-seed render databases without coupling terrain types. */
|
|
246
|
+
function terrainSeed(background) {
|
|
247
|
+
if (Number.isFinite(background.seed)) return (background.seed ?? 0) >>> 0;
|
|
248
|
+
let seed = 2166136261;
|
|
249
|
+
for (const patch of [background, ...background.patches ?? []]) {
|
|
250
|
+
seed = Math.imul(seed ^ Math.floor(patch.patchSize), 16777619) >>> 0;
|
|
251
|
+
for (const frame of patch.frames) seed = Math.imul(seed ^ frame + 1 >>> 0, 16777619) >>> 0;
|
|
252
|
+
}
|
|
253
|
+
return seed >>> 0;
|
|
254
|
+
}
|
|
255
|
+
function greatestCommonDivisor(a, b) {
|
|
256
|
+
let left = Math.abs(a);
|
|
257
|
+
let right = Math.abs(b);
|
|
258
|
+
while (right !== 0) {
|
|
259
|
+
const remainder = left % right;
|
|
260
|
+
left = right;
|
|
261
|
+
right = remainder;
|
|
262
|
+
}
|
|
263
|
+
return left;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Split four-or-more variants into two seed-permuted pools on a checkerboard.
|
|
267
|
+
* Direct neighbours cannot select the exact same authored frame, while selection
|
|
268
|
+
* within each pool remains hashed and weighted.
|
|
269
|
+
*/
|
|
270
|
+
function variantCandidates(count, cellX, cellY, seed) {
|
|
271
|
+
if (count < 4) return Array.from({ length: count }, (_, index) => index);
|
|
272
|
+
let stride = (seed >>> 8) % count | 1;
|
|
273
|
+
while (greatestCommonDivisor(stride, count) !== 1) stride += 2;
|
|
274
|
+
const offset = (seed >>> 16) % count;
|
|
275
|
+
const parity = cellX + cellY & 1;
|
|
276
|
+
const candidates = [];
|
|
277
|
+
for (let logical = parity; logical < count; logical += 2) candidates.push((logical * stride + offset) % count);
|
|
278
|
+
return candidates;
|
|
279
|
+
}
|
|
280
|
+
function selectVariant(cellX, cellY, patch, seed) {
|
|
281
|
+
const candidates = variantCandidates(patch.frames.length, cellX, cellY, seed);
|
|
282
|
+
const unit = terrainCellHash(cellX, cellY, seed ^ VARIANT_SALT) / UINT32_RANGE;
|
|
283
|
+
const { weights } = patch;
|
|
284
|
+
if (!weights || weights.length !== patch.frames.length) return candidates[Math.min(candidates.length - 1, Math.floor(unit * candidates.length))] ?? 0;
|
|
285
|
+
let total = 0;
|
|
286
|
+
for (const index of candidates) {
|
|
287
|
+
const weight = weights[index] ?? 0;
|
|
288
|
+
if (Number.isFinite(weight) && weight > 0) total += weight;
|
|
289
|
+
}
|
|
290
|
+
if (total <= 0) return candidates[Math.min(candidates.length - 1, Math.floor(unit * candidates.length))] ?? 0;
|
|
291
|
+
let target = unit * total;
|
|
292
|
+
for (const index of candidates) {
|
|
293
|
+
const weight = weights[index] ?? 0;
|
|
294
|
+
if (Number.isFinite(weight) && weight > 0) {
|
|
295
|
+
target -= weight;
|
|
296
|
+
if (target < 0) return index;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return candidates[candidates.length - 1] ?? 0;
|
|
300
|
+
}
|
|
301
|
+
function terrainPatchSets(background) {
|
|
302
|
+
const unique = /* @__PURE__ */ new Map();
|
|
303
|
+
for (const patch of [background, ...background.patches ?? []]) {
|
|
304
|
+
const patchSize = Math.floor(patch.patchSize);
|
|
305
|
+
if (patchSize > 0 && patch.frames.length > 0 && !unique.has(patchSize)) unique.set(patchSize, {
|
|
306
|
+
patchSize,
|
|
307
|
+
frames: patch.frames,
|
|
308
|
+
...patch.weights ? { weights: patch.weights } : {},
|
|
309
|
+
...patch.probability != null ? { probability: patch.probability } : {}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return [...unique.values()].sort((left, right) => right.patchSize - left.patchSize);
|
|
313
|
+
}
|
|
314
|
+
function patchProbability(patch, index, count) {
|
|
315
|
+
if (index === count - 1) return 1;
|
|
316
|
+
const authored = Number.isFinite(patch.probability) ? Math.max(0, Math.min(1, patch.probability ?? 1)) : 1;
|
|
317
|
+
return Math.min(authored, index === 0 ? MAX_LARGEST_PATCH_PROBABILITY : MAX_INTERMEDIATE_PATCH_PROBABILITY);
|
|
318
|
+
}
|
|
319
|
+
function rgba$1(color) {
|
|
320
|
+
const [r, g, b, a] = color;
|
|
321
|
+
return `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Paint an infinite terrain plane anchored to absolute map coordinates.
|
|
325
|
+
* Only complete, untransformed Factorio patches are drawn. Larger regions may
|
|
326
|
+
* be subdivided into complete smaller authored patches, so every destination is
|
|
327
|
+
* covered exactly once and every texture edge keeps its intended orientation.
|
|
328
|
+
*/
|
|
329
|
+
function drawTerrainBackground(ctx, width, height, options) {
|
|
330
|
+
if (width <= 0 || height <= 0) return;
|
|
331
|
+
const { background, fallbackColor, frames, images, pixelsPerTile, tileFrame } = options;
|
|
332
|
+
ctx.fillStyle = rgba$1(background?.color ?? fallbackColor);
|
|
333
|
+
ctx.fillRect(0, 0, width, height);
|
|
334
|
+
if (!background) return;
|
|
335
|
+
const patchSets = terrainPatchSets(background);
|
|
336
|
+
const largestPatch = patchSets[0];
|
|
337
|
+
if (!largestPatch) return;
|
|
338
|
+
const seed = terrainSeed(background);
|
|
339
|
+
const previousSmoothing = ctx.imageSmoothingEnabled;
|
|
340
|
+
ctx.imageSmoothingEnabled = false;
|
|
341
|
+
const drawPatch = (worldX, worldY, patch) => {
|
|
342
|
+
const variantIndex = selectVariant(Math.floor(worldX / patch.patchSize), Math.floor(worldY / patch.patchSize), patch, seed ^ Math.imul(patch.patchSize, PATCH_SIZE_SALT));
|
|
343
|
+
const frameId = patch.frames[variantIndex];
|
|
344
|
+
const frame = frameId == null ? void 0 : frames[frameId];
|
|
345
|
+
const image = frame ? images[frame.a] : void 0;
|
|
346
|
+
if (!frame || !image || frame.sw <= 0 || frame.sh <= 0) return;
|
|
347
|
+
const patchLeft = Math.round((worldX - tileFrame.minX) * pixelsPerTile);
|
|
348
|
+
const patchTop = Math.round((worldY - tileFrame.minY) * pixelsPerTile);
|
|
349
|
+
const patchRight = Math.round((worldX + patch.patchSize - tileFrame.minX) * pixelsPerTile);
|
|
350
|
+
const patchBottom = Math.round((worldY + patch.patchSize - tileFrame.minY) * pixelsPerTile);
|
|
351
|
+
const scaleX = (patchRight - patchLeft) / frame.sw;
|
|
352
|
+
const scaleY = (patchBottom - patchTop) / frame.sh;
|
|
353
|
+
ctx.drawImage(image, frame.x, frame.y, packedWidth$1(frame), packedHeight$1(frame), patchLeft + frame.ox * scaleX, patchTop + frame.oy * scaleY, frame.w * scaleX, frame.h * scaleY);
|
|
354
|
+
};
|
|
355
|
+
const drawRegion = (worldX, worldY, regionSize, patchIndex) => {
|
|
356
|
+
const patch = patchSets[patchIndex];
|
|
357
|
+
if (!patch) return;
|
|
358
|
+
if (patch.patchSize < regionSize) {
|
|
359
|
+
const step = patch.patchSize;
|
|
360
|
+
for (let offsetY = 0; offsetY < regionSize; offsetY += step) for (let offsetX = 0; offsetX < regionSize; offsetX += step) drawRegion(worldX + offsetX, worldY + offsetY, step, patchIndex);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const cellX = Math.floor(worldX / regionSize);
|
|
364
|
+
const cellY = Math.floor(worldY / regionSize);
|
|
365
|
+
const probability = patchProbability(patch, patchIndex, patchSets.length);
|
|
366
|
+
const usePatch = probability >= 1 || terrainCellHash(cellX, cellY, seed ^ Math.imul(regionSize, PATCH_SIZE_SALT)) / UINT32_RANGE < probability;
|
|
367
|
+
const nextPatch = patchSets[patchIndex + 1];
|
|
368
|
+
if (usePatch || !nextPatch || regionSize % nextPatch.patchSize !== 0) {
|
|
369
|
+
drawPatch(worldX, worldY, patch);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const step = nextPatch.patchSize;
|
|
373
|
+
for (let offsetY = 0; offsetY < regionSize; offsetY += step) for (let offsetX = 0; offsetX < regionSize; offsetX += step) drawRegion(worldX + offsetX, worldY + offsetY, step, patchIndex + 1);
|
|
374
|
+
};
|
|
375
|
+
const patchSize = largestPatch.patchSize;
|
|
376
|
+
const firstCellX = Math.floor(tileFrame.minX / patchSize);
|
|
377
|
+
const firstCellY = Math.floor(tileFrame.minY / patchSize);
|
|
378
|
+
const lastCellX = Math.ceil(tileFrame.maxX / patchSize);
|
|
379
|
+
const lastCellY = Math.ceil(tileFrame.maxY / patchSize);
|
|
380
|
+
for (let cellY = firstCellY; cellY < lastCellY; cellY++) for (let cellX = firstCellX; cellX < lastCellX; cellX++) drawRegion(cellX * patchSize, cellY * patchSize, patchSize, 0);
|
|
381
|
+
ctx.imageSmoothingEnabled = previousSmoothing;
|
|
382
|
+
}
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/canvas2d/util.ts
|
|
385
|
+
function packedWidth(frame) {
|
|
386
|
+
return frame.pw ?? frame.w;
|
|
387
|
+
}
|
|
388
|
+
function packedHeight(frame) {
|
|
389
|
+
return frame.ph ?? frame.h;
|
|
390
|
+
}
|
|
391
|
+
function rgba([r, g, b, a]) {
|
|
392
|
+
return `rgba(${r * 255}, ${g * 255}, ${b * 255}, ${a})`;
|
|
393
|
+
}
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/canvas2d/draw-icon.ts
|
|
396
|
+
const ENTITY_INFO_BASE_ICON_PX = 32;
|
|
397
|
+
/** Scale the foreground icon inside the pin so it sits above the chevron. */
|
|
398
|
+
const REQUEST_PIN_ICON_SCALE = .7;
|
|
399
|
+
/** Shift the pin icon upward (fraction of command size) into the pin body. */
|
|
400
|
+
const REQUEST_PIN_ICON_Y_SHIFT = .1;
|
|
401
|
+
/**
|
|
402
|
+
* Trimmed atlas width still includes soft alpha fringe. Opaque chrome (a>20) is
|
|
403
|
+
* ~44 px of the 48 px trim — scale to that so layout gaps match the visible edge.
|
|
404
|
+
*/
|
|
405
|
+
const REQUEST_PIN_OPAQUE_WIDTH_RATIO = 44 / 48;
|
|
406
|
+
function drawIcon(ctx, cmd, frame, image, frames, images, iconImage, silhouetteImage, ox, oy, ppt) {
|
|
407
|
+
const size = cmd.size * ppt;
|
|
408
|
+
const cx = (cmd.x + ox) * ppt;
|
|
409
|
+
const cy = (cmd.y + oy) * ppt;
|
|
410
|
+
const isRequestPin = cmd.backingStyle === "request-pin";
|
|
411
|
+
const left = cx - size / 2;
|
|
412
|
+
const top = cy - size / 2;
|
|
413
|
+
const backingFrame = cmd.backingFrame == null ? void 0 : frames[cmd.backingFrame];
|
|
414
|
+
const backingImage = backingFrame ? images[backingFrame.a] : void 0;
|
|
415
|
+
if (backingFrame && backingImage) {
|
|
416
|
+
const backingScale = size / (isRequestPin ? Math.max(1, backingFrame.w * REQUEST_PIN_OPAQUE_WIDTH_RATIO) : ENTITY_INFO_BASE_ICON_PX);
|
|
417
|
+
const backingWidth = backingFrame.sw * backingScale;
|
|
418
|
+
const backingHeight = backingFrame.sh * backingScale;
|
|
419
|
+
const backingLeft = cx - backingWidth / 2;
|
|
420
|
+
const backingTop = cy - backingHeight / 2;
|
|
421
|
+
const previousAlpha = ctx.globalAlpha;
|
|
422
|
+
if (!isRequestPin) ctx.globalAlpha = previousAlpha * .16;
|
|
423
|
+
ctx.drawImage(backingImage, backingFrame.x, backingFrame.y, packedWidth(backingFrame), packedHeight(backingFrame), backingLeft + backingFrame.ox * backingScale, backingTop + backingFrame.oy * backingScale, backingFrame.w * backingScale, backingFrame.h * backingScale);
|
|
424
|
+
ctx.globalAlpha = previousAlpha;
|
|
425
|
+
} else if (cmd.backing) {
|
|
426
|
+
ctx.fillStyle = "rgba(0,0,0,0.6)";
|
|
427
|
+
const radius = size * .15;
|
|
428
|
+
if (typeof ctx.roundRect === "function") {
|
|
429
|
+
ctx.beginPath();
|
|
430
|
+
ctx.roundRect(left, top, size, size, radius);
|
|
431
|
+
ctx.fill();
|
|
432
|
+
} else ctx.fillRect(left, top, size, size);
|
|
433
|
+
}
|
|
434
|
+
const drawSize = isRequestPin ? size * REQUEST_PIN_ICON_SCALE : size;
|
|
435
|
+
const drawCx = cx;
|
|
436
|
+
const drawCy = isRequestPin ? cy - size * REQUEST_PIN_ICON_Y_SHIFT : cy;
|
|
437
|
+
const drawLeft = drawCx - drawSize / 2;
|
|
438
|
+
const drawTop = drawCy - drawSize / 2;
|
|
439
|
+
const iconScaleX = frame.sw === 0 ? 0 : drawSize / frame.sw;
|
|
440
|
+
const iconScaleY = frame.sh === 0 ? 0 : drawSize / frame.sh;
|
|
441
|
+
const iconLeft = drawLeft + frame.ox * iconScaleX;
|
|
442
|
+
const iconTop = drawTop + frame.oy * iconScaleY;
|
|
443
|
+
const iconWidth = frame.w * iconScaleX;
|
|
444
|
+
const iconHeight = frame.h * iconScaleY;
|
|
445
|
+
const hasEntityInfoBacking = !isRequestPin && (backingFrame != null || cmd.backing === true);
|
|
446
|
+
const needsEntityInfoSilhouette = !isRequestPin && (hasEntityInfoBacking || cmd.silhouette === true);
|
|
447
|
+
const drawIconLayer = (sourceImage, sourceX, sourceY, sourceW, sourceH, destLeft, destTop, destWidth, destHeight) => {
|
|
448
|
+
if (cmd.rotation != null && cmd.rotation !== 0) {
|
|
449
|
+
ctx.save();
|
|
450
|
+
ctx.translate(drawCx, drawCy);
|
|
451
|
+
ctx.rotate(cmd.rotation * Math.PI / 180);
|
|
452
|
+
ctx.drawImage(sourceImage, sourceX, sourceY, sourceW, sourceH, destLeft - drawCx, destTop - drawCy, destWidth, destHeight);
|
|
453
|
+
ctx.restore();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
ctx.drawImage(sourceImage, sourceX, sourceY, sourceW, sourceH, destLeft, destTop, destWidth, destHeight);
|
|
457
|
+
};
|
|
458
|
+
if (needsEntityInfoSilhouette && iconImage && silhouetteImage) {
|
|
459
|
+
const pad = entityInfoSilhouettePadPx();
|
|
460
|
+
const sourcePad = Math.max(1, Math.round(pad * (packedWidth(frame) / frame.w)));
|
|
461
|
+
const padX = pad * iconScaleX;
|
|
462
|
+
const padY = pad * iconScaleY;
|
|
463
|
+
drawIconLayer(silhouetteImage, 0, 0, packedWidth(frame) + 2 * sourcePad, packedHeight(frame) + 2 * sourcePad, iconLeft - padX, iconTop - padY, iconWidth + 2 * padX, iconHeight + 2 * padY);
|
|
464
|
+
drawIconLayer(iconImage, 0, 0, packedWidth(frame), packedHeight(frame), iconLeft, iconTop, iconWidth, iconHeight);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
drawIconLayer(iconImage ?? image, iconImage ? 0 : frame.x, iconImage ? 0 : frame.y, packedWidth(frame), packedHeight(frame), iconLeft, iconTop, iconWidth, iconHeight);
|
|
468
|
+
}
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region src/canvas2d/draw-snap-grid.ts
|
|
471
|
+
/** Factorio snap-rectangle green. */
|
|
472
|
+
const SNAP_GRID_STROKE = "#3cff00";
|
|
473
|
+
const SNAP_GRID_STROKE_OUTER = "#0a3d00";
|
|
474
|
+
/** Dash pattern in tiles at 32 ppt. */
|
|
475
|
+
const SNAP_DASH_AT_32PPT = [10, 6];
|
|
476
|
+
/**
|
|
477
|
+
* Draw the blueprint snap-to-grid rectangle as a dashed perimeter along the
|
|
478
|
+
* four edges (no corner L-brackets).
|
|
479
|
+
*/
|
|
480
|
+
function drawSnapGrid(ctx, cmd, ox, oy, ppt) {
|
|
481
|
+
const { x, y, w, h } = cmd;
|
|
482
|
+
if (!(w > 0) || !(h > 0)) return;
|
|
483
|
+
drawSnapGridStroke(ctx, x, y, w, h, ox, oy, ppt);
|
|
484
|
+
}
|
|
485
|
+
function drawSnapGridStroke(ctx, x, y, w, h, ox, oy, ppt) {
|
|
486
|
+
const px = (x + ox) * ppt;
|
|
487
|
+
const py = (y + oy) * ppt;
|
|
488
|
+
const pw = w * ppt;
|
|
489
|
+
const ph = h * ppt;
|
|
490
|
+
const lineWidth = Math.max(2, 3 * ppt / 32);
|
|
491
|
+
const dash = SNAP_DASH_AT_32PPT.map((v) => v * ppt / 32);
|
|
492
|
+
ctx.save();
|
|
493
|
+
if (typeof ctx.setLineDash === "function") ctx.setLineDash(dash);
|
|
494
|
+
ctx.lineCap = "butt";
|
|
495
|
+
ctx.lineWidth = lineWidth + 2;
|
|
496
|
+
ctx.strokeStyle = SNAP_GRID_STROKE_OUTER;
|
|
497
|
+
strokeRectCompat(ctx, px + .5, py + .5, pw - 1, ph - 1);
|
|
498
|
+
ctx.lineWidth = lineWidth;
|
|
499
|
+
ctx.strokeStyle = SNAP_GRID_STROKE;
|
|
500
|
+
strokeRectCompat(ctx, px + .5, py + .5, pw - 1, ph - 1);
|
|
501
|
+
if (typeof ctx.setLineDash === "function") ctx.setLineDash([]);
|
|
502
|
+
ctx.restore();
|
|
503
|
+
}
|
|
504
|
+
function strokeRectCompat(ctx, x, y, w, h) {
|
|
505
|
+
if (typeof ctx.strokeRect === "function") {
|
|
506
|
+
ctx.strokeRect(x, y, w, h);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
ctx.beginPath();
|
|
510
|
+
ctx.rect(x, y, w, h);
|
|
511
|
+
ctx.stroke();
|
|
512
|
+
}
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region src/canvas2d/draw-sprite.ts
|
|
515
|
+
const PIXEL_SNAPPED_RAIL_LAYERS = /* @__PURE__ */ new Set([
|
|
516
|
+
RENDER_LAYERS["rail-stone-path-lower"],
|
|
517
|
+
RENDER_LAYERS["rail-stone-path"],
|
|
518
|
+
RENDER_LAYERS["rail-tie"],
|
|
519
|
+
RENDER_LAYERS["rail-screw"],
|
|
520
|
+
RENDER_LAYERS["rail-metal"],
|
|
521
|
+
RENDER_LAYERS["elevated-rail-stone-path-lower"],
|
|
522
|
+
RENDER_LAYERS["elevated-rail-stone-path"],
|
|
523
|
+
RENDER_LAYERS["elevated-rail-tie"],
|
|
524
|
+
RENDER_LAYERS["elevated-rail-screw"],
|
|
525
|
+
RENDER_LAYERS["elevated-rail-metal"]
|
|
526
|
+
]);
|
|
527
|
+
function drawRect(ctx, cmd, ox, oy, ppt) {
|
|
528
|
+
ctx.fillStyle = rgba(cmd.color);
|
|
529
|
+
ctx.fillRect((cmd.x + ox) * ppt, (cmd.y + oy) * ppt, cmd.w * ppt, cmd.h * ppt);
|
|
530
|
+
}
|
|
531
|
+
function atlasSourceRect(frame, src) {
|
|
532
|
+
const packedW = packedWidth(frame);
|
|
533
|
+
const packedH = packedHeight(frame);
|
|
534
|
+
const atlasScaleX = frame.w === 0 ? 0 : packedW / frame.w;
|
|
535
|
+
const atlasScaleY = frame.h === 0 ? 0 : packedH / frame.h;
|
|
536
|
+
const relX = src.x - frame.ox;
|
|
537
|
+
const relY = src.y - frame.oy;
|
|
538
|
+
return {
|
|
539
|
+
x: frame.x + relX * atlasScaleX,
|
|
540
|
+
y: frame.y + relY * atlasScaleY,
|
|
541
|
+
w: src.w * atlasScaleX,
|
|
542
|
+
h: src.h * atlasScaleY
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function drawSprite(ctx, cmd, frame, image, ox, oy, ppt, createCanvas) {
|
|
546
|
+
let dx = (cmd.x + ox) * ppt;
|
|
547
|
+
let dy = (cmd.y + oy) * ppt;
|
|
548
|
+
let dw = cmd.w * ppt;
|
|
549
|
+
let dh = cmd.h * ppt;
|
|
550
|
+
if (PIXEL_SNAPPED_RAIL_LAYERS.has(cmd.layer) && (cmd.rotation ?? 0) === 0) {
|
|
551
|
+
const right = Math.round((cmd.x + cmd.w + ox) * ppt);
|
|
552
|
+
const bottom = Math.round((cmd.y + cmd.h + oy) * ppt);
|
|
553
|
+
dx = Math.round(dx);
|
|
554
|
+
dy = Math.round(dy);
|
|
555
|
+
dw = right - dx;
|
|
556
|
+
dh = bottom - dy;
|
|
557
|
+
}
|
|
558
|
+
const src = cmd.src ?? {
|
|
559
|
+
x: 0,
|
|
560
|
+
y: 0,
|
|
561
|
+
w: frame.sw,
|
|
562
|
+
h: frame.sh
|
|
563
|
+
};
|
|
564
|
+
const scaleX = src.w === 0 ? 0 : dw / src.w;
|
|
565
|
+
const scaleY = src.h === 0 ? 0 : dh / src.h;
|
|
566
|
+
const atlasSrc = cmd.src ? atlasSourceRect(frame, src) : {
|
|
567
|
+
x: frame.x,
|
|
568
|
+
y: frame.y,
|
|
569
|
+
w: packedWidth(frame),
|
|
570
|
+
h: packedHeight(frame)
|
|
571
|
+
};
|
|
572
|
+
const trimmedDx = cmd.src ? dx : dx + frame.ox * scaleX;
|
|
573
|
+
const trimmedDy = cmd.src ? dy : dy + frame.oy * scaleY;
|
|
574
|
+
const trimmedDw = cmd.src ? dw : frame.w * scaleX;
|
|
575
|
+
const trimmedDh = cmd.src ? dh : frame.h * scaleY;
|
|
576
|
+
const prevAlpha = ctx.globalAlpha;
|
|
577
|
+
if (cmd.shadow) ctx.globalAlpha = prevAlpha * .5;
|
|
578
|
+
const flipX = cmd.flipX === true;
|
|
579
|
+
const flipY = cmd.flipY === true;
|
|
580
|
+
const rotation = cmd.rotation ?? 0;
|
|
581
|
+
const hasClip = cmd.clip != null;
|
|
582
|
+
const tint = cmd.tint;
|
|
583
|
+
const blit = (target, destDx, destDy, destDw, destDh) => {
|
|
584
|
+
target.drawImage(image, atlasSrc.x, atlasSrc.y, atlasSrc.w, atlasSrc.h, destDx, destDy, destDw, destDh);
|
|
585
|
+
};
|
|
586
|
+
/** Multiply-tint the sprite via an offscreen canvas, then draw the result. */
|
|
587
|
+
const tintedSource = () => {
|
|
588
|
+
if (!tint || !createCanvas) return null;
|
|
589
|
+
const tw = Math.max(1, Math.ceil(trimmedDw));
|
|
590
|
+
const th = Math.max(1, Math.ceil(trimmedDh));
|
|
591
|
+
const scratch = createCanvas(tw, th);
|
|
592
|
+
scratch.width = tw;
|
|
593
|
+
scratch.height = th;
|
|
594
|
+
const sctx = scratch.getContext("2d");
|
|
595
|
+
if (!sctx) return null;
|
|
596
|
+
sctx.imageSmoothingEnabled = false;
|
|
597
|
+
blit(sctx, 0, 0, tw, th);
|
|
598
|
+
sctx.globalCompositeOperation = "multiply";
|
|
599
|
+
sctx.fillStyle = rgba(tint);
|
|
600
|
+
sctx.fillRect(0, 0, tw, th);
|
|
601
|
+
sctx.globalCompositeOperation = "destination-in";
|
|
602
|
+
blit(sctx, 0, 0, tw, th);
|
|
603
|
+
sctx.globalCompositeOperation = "source-over";
|
|
604
|
+
return {
|
|
605
|
+
image: scratch,
|
|
606
|
+
w: tw,
|
|
607
|
+
h: th
|
|
608
|
+
};
|
|
609
|
+
};
|
|
610
|
+
const tinted = tintedSource();
|
|
611
|
+
if (hasClip) {
|
|
612
|
+
ctx.save();
|
|
613
|
+
ctx.beginPath();
|
|
614
|
+
ctx.rect((cmd.clip.x + ox) * ppt, (cmd.clip.y + oy) * ppt, cmd.clip.w * ppt, cmd.clip.h * ppt);
|
|
615
|
+
ctx.clip();
|
|
616
|
+
}
|
|
617
|
+
if (flipX || flipY || rotation !== 0) {
|
|
618
|
+
const ucx = dx + dw / 2;
|
|
619
|
+
const ucy = dy + dh / 2;
|
|
620
|
+
ctx.save();
|
|
621
|
+
ctx.translate(ucx, ucy);
|
|
622
|
+
if (rotation !== 0) ctx.rotate(rotation * Math.PI / 180);
|
|
623
|
+
ctx.scale(flipX ? -1 : 1, flipY ? -1 : 1);
|
|
624
|
+
if (tinted) ctx.drawImage(tinted.image, 0, 0, tinted.w, tinted.h, trimmedDx - ucx, trimmedDy - ucy, trimmedDw, trimmedDh);
|
|
625
|
+
else blit(ctx, trimmedDx - ucx, trimmedDy - ucy, trimmedDw, trimmedDh);
|
|
626
|
+
ctx.restore();
|
|
627
|
+
} else if (tinted) ctx.drawImage(tinted.image, 0, 0, tinted.w, tinted.h, trimmedDx, trimmedDy, trimmedDw, trimmedDh);
|
|
628
|
+
else blit(ctx, trimmedDx, trimmedDy, trimmedDw, trimmedDh);
|
|
629
|
+
const seamBleed = cmd.seamBleed;
|
|
630
|
+
if (seamBleed && !hasClip && !tinted && !flipX && !flipY && rotation === 0) {
|
|
631
|
+
const bleed = (clipX, clipY, clipW, clipH, sx, sy) => {
|
|
632
|
+
ctx.save();
|
|
633
|
+
ctx.beginPath();
|
|
634
|
+
ctx.rect(clipX, clipY, clipW, clipH);
|
|
635
|
+
ctx.clip();
|
|
636
|
+
blit(ctx, trimmedDx + sx, trimmedDy + sy, trimmedDw, trimmedDh);
|
|
637
|
+
ctx.restore();
|
|
638
|
+
};
|
|
639
|
+
const seamBand = 2;
|
|
640
|
+
if (seamBleed.top) bleed(dx, dy - seamBand, dw, seamBand, 0, -2);
|
|
641
|
+
if (seamBleed.right) bleed(dx + dw, dy, seamBand, dh, seamBand, 0);
|
|
642
|
+
if (seamBleed.bottom) bleed(dx, dy + dh, dw, seamBand, 0, seamBand);
|
|
643
|
+
if (seamBleed.left) bleed(dx - seamBand, dy, seamBand, dh, -2, 0);
|
|
644
|
+
}
|
|
645
|
+
if (hasClip) ctx.restore();
|
|
646
|
+
if (cmd.shadow) ctx.globalAlpha = prevAlpha;
|
|
647
|
+
}
|
|
648
|
+
function shadowSpriteBounds(cmd, frame, ox, oy, ppt, outputWidth, outputHeight) {
|
|
649
|
+
const dx = (cmd.x + ox) * ppt;
|
|
650
|
+
const dy = (cmd.y + oy) * ppt;
|
|
651
|
+
const dw = cmd.w * ppt;
|
|
652
|
+
const dh = cmd.h * ppt;
|
|
653
|
+
const scaleX = frame.sw === 0 ? 0 : dw / frame.sw;
|
|
654
|
+
const scaleY = frame.sh === 0 ? 0 : dh / frame.sh;
|
|
655
|
+
const left = dx + frame.ox * scaleX;
|
|
656
|
+
const top = dy + frame.oy * scaleY;
|
|
657
|
+
const right = left + frame.w * scaleX;
|
|
658
|
+
const bottom = top + frame.h * scaleY;
|
|
659
|
+
const centerX = dx + dw / 2;
|
|
660
|
+
const centerY = dy + dh / 2;
|
|
661
|
+
const radians = (cmd.rotation ?? 0) * Math.PI / 180;
|
|
662
|
+
const cos = Math.cos(radians);
|
|
663
|
+
const sin = Math.sin(radians);
|
|
664
|
+
const flipX = cmd.flipX === true ? -1 : 1;
|
|
665
|
+
const flipY = cmd.flipY === true ? -1 : 1;
|
|
666
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
667
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
668
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
669
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
670
|
+
for (const [x, y] of [
|
|
671
|
+
[left, top],
|
|
672
|
+
[right, top],
|
|
673
|
+
[right, bottom],
|
|
674
|
+
[left, bottom]
|
|
675
|
+
]) {
|
|
676
|
+
const localX = (x - centerX) * flipX;
|
|
677
|
+
const localY = (y - centerY) * flipY;
|
|
678
|
+
const transformedX = centerX + localX * cos - localY * sin;
|
|
679
|
+
const transformedY = centerY + localX * sin + localY * cos;
|
|
680
|
+
minX = Math.min(minX, transformedX);
|
|
681
|
+
minY = Math.min(minY, transformedY);
|
|
682
|
+
maxX = Math.max(maxX, transformedX);
|
|
683
|
+
maxY = Math.max(maxY, transformedY);
|
|
684
|
+
}
|
|
685
|
+
minX = Math.floor(minX) - 1;
|
|
686
|
+
minY = Math.floor(minY) - 1;
|
|
687
|
+
maxX = Math.ceil(maxX) + 1;
|
|
688
|
+
maxY = Math.ceil(maxY) + 1;
|
|
689
|
+
if (cmd.clip) {
|
|
690
|
+
const clipLeft = (cmd.clip.x + ox) * ppt;
|
|
691
|
+
const clipTop = (cmd.clip.y + oy) * ppt;
|
|
692
|
+
const clipRight = clipLeft + cmd.clip.w * ppt;
|
|
693
|
+
const clipBottom = clipTop + cmd.clip.h * ppt;
|
|
694
|
+
minX = Math.max(minX, clipLeft);
|
|
695
|
+
minY = Math.max(minY, clipTop);
|
|
696
|
+
maxX = Math.min(maxX, clipRight);
|
|
697
|
+
maxY = Math.min(maxY, clipBottom);
|
|
698
|
+
}
|
|
699
|
+
minX = Math.max(0, minX);
|
|
700
|
+
minY = Math.max(0, minY);
|
|
701
|
+
maxX = Math.min(outputWidth, maxX);
|
|
702
|
+
maxY = Math.min(outputHeight, maxY);
|
|
703
|
+
return maxX > minX && maxY > minY ? {
|
|
704
|
+
minX,
|
|
705
|
+
minY,
|
|
706
|
+
maxX,
|
|
707
|
+
maxY
|
|
708
|
+
} : null;
|
|
709
|
+
}
|
|
710
|
+
//#endregion
|
|
711
|
+
//#region src/canvas2d/draw-text.ts
|
|
712
|
+
function drawText(ctx, cmd, ox, oy, ppt) {
|
|
713
|
+
const sizePx = Math.max(1, cmd.size * ppt);
|
|
714
|
+
ctx.save();
|
|
715
|
+
ctx.font = fpsrTextFontCss(sizePx);
|
|
716
|
+
ctx.textAlign = cmd.align ?? "left";
|
|
717
|
+
ctx.textBaseline = cmd.baseline ?? "top";
|
|
718
|
+
ctx.fillStyle = rgba(cmd.color);
|
|
719
|
+
ctx.fillText(cmd.text, (cmd.x + ox) * ppt, (cmd.y + oy) * ppt);
|
|
720
|
+
ctx.restore();
|
|
721
|
+
}
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/canvas2d/draw-wire.ts
|
|
724
|
+
const WIRE_COLORS = {
|
|
725
|
+
copper: "#cf7c00",
|
|
726
|
+
red: "#c83718",
|
|
727
|
+
green: "#588c38"
|
|
728
|
+
};
|
|
729
|
+
/** Stroke width in px at 32 ppt (FBE uses 1.5; we go slightly thinner). */
|
|
730
|
+
const WIRE_WIDTH_AT_32PPT = 1;
|
|
731
|
+
const WIRE_ALPHA = .72;
|
|
732
|
+
/** Factorio rolling-stock coupling overlay (procedural; no dedicated sprite).
|
|
733
|
+
* In-game measured color `#658024` (olive). Chart constants like
|
|
734
|
+
* `green_wire_color` / `train_preview_path_outline_color` are pure `#00ff00`
|
|
735
|
+
* and do not match this overlay; `vehicle_wagon_connection_color` is map-only red. */
|
|
736
|
+
const TRAIN_CHAIN_COLOR = "#658024";
|
|
737
|
+
/** Stroke width in px at 32 ppt (in-game coupling overlay ≈ 3 px). */
|
|
738
|
+
const TRAIN_CHAIN_WIDTH_AT_32PPT = 3;
|
|
739
|
+
const TRAIN_CHAIN_ALPHA = .95;
|
|
740
|
+
function drawWire(ctx, cmd, ox, oy, ppt) {
|
|
741
|
+
const x1 = (cmd.x1 + ox) * ppt;
|
|
742
|
+
const y1 = (cmd.y1 + oy) * ppt;
|
|
743
|
+
const x2 = (cmd.x2 + ox) * ppt;
|
|
744
|
+
const y2 = (cmd.y2 + oy) * ppt;
|
|
745
|
+
const dist = Math.hypot(x2 - x1, y2 - y1);
|
|
746
|
+
const mx = (x1 + x2) / 2;
|
|
747
|
+
const my = (y1 + y2) / 2 + .15 * dist;
|
|
748
|
+
const prevAlpha = ctx.globalAlpha;
|
|
749
|
+
ctx.beginPath();
|
|
750
|
+
ctx.strokeStyle = WIRE_COLORS[cmd.wire];
|
|
751
|
+
ctx.lineWidth = WIRE_WIDTH_AT_32PPT * ppt / 32;
|
|
752
|
+
ctx.lineCap = "round";
|
|
753
|
+
ctx.globalAlpha = prevAlpha * WIRE_ALPHA;
|
|
754
|
+
ctx.moveTo(x1, y1);
|
|
755
|
+
ctx.quadraticCurveTo(mx, my, x2, y2);
|
|
756
|
+
ctx.stroke();
|
|
757
|
+
ctx.globalAlpha = prevAlpha;
|
|
758
|
+
}
|
|
759
|
+
function drawTrainChain(ctx, cmd, ox, oy, ppt) {
|
|
760
|
+
const prevAlpha = ctx.globalAlpha;
|
|
761
|
+
ctx.strokeStyle = TRAIN_CHAIN_COLOR;
|
|
762
|
+
ctx.lineWidth = TRAIN_CHAIN_WIDTH_AT_32PPT * ppt / 32;
|
|
763
|
+
ctx.lineCap = "round";
|
|
764
|
+
ctx.globalAlpha = prevAlpha * TRAIN_CHAIN_ALPHA;
|
|
765
|
+
for (const s of cmd.segments) {
|
|
766
|
+
ctx.beginPath();
|
|
767
|
+
ctx.moveTo((s.x1 + ox) * ppt, (s.y1 + oy) * ppt);
|
|
768
|
+
ctx.lineTo((s.x2 + ox) * ppt, (s.y2 + oy) * ppt);
|
|
769
|
+
ctx.stroke();
|
|
770
|
+
}
|
|
771
|
+
const r = TRAIN_CHAIN_JOINT_RADIUS * ppt;
|
|
772
|
+
for (const j of cmd.joints) {
|
|
773
|
+
ctx.beginPath();
|
|
774
|
+
ctx.arc((j.x + ox) * ppt, (j.y + oy) * ppt, r, 0, Math.PI * 2);
|
|
775
|
+
ctx.stroke();
|
|
776
|
+
}
|
|
777
|
+
ctx.globalAlpha = prevAlpha;
|
|
778
|
+
}
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/canvas2d/execute.ts
|
|
781
|
+
const TERRAIN_FALLBACK_COLOR = [
|
|
782
|
+
141 / 255,
|
|
783
|
+
104 / 255,
|
|
784
|
+
60 / 255,
|
|
785
|
+
1
|
|
786
|
+
];
|
|
787
|
+
/**
|
|
788
|
+
* Execute a sorted draw list against a Canvas2D context.
|
|
789
|
+
* `images[i]` is the atlas image for atlas index i.
|
|
790
|
+
*/
|
|
791
|
+
function executeDrawList(ctx, list, images, opts) {
|
|
792
|
+
const ppt = opts.pixelsPerTile;
|
|
793
|
+
const pad = opts.padTiles ?? 0;
|
|
794
|
+
const { frames } = opts;
|
|
795
|
+
const { minX, minY, maxX, maxY } = list.bounds;
|
|
796
|
+
const frame = opts.tileFrame ?? {
|
|
797
|
+
minX: Math.floor(minX) - pad,
|
|
798
|
+
minY: Math.floor(minY) - pad,
|
|
799
|
+
maxX: Math.ceil(maxX) + pad,
|
|
800
|
+
maxY: Math.ceil(maxY) + pad
|
|
801
|
+
};
|
|
802
|
+
const ox = -frame.minX;
|
|
803
|
+
const oy = -frame.minY;
|
|
804
|
+
const width = Math.max(0, (frame.maxX - frame.minX) * ppt);
|
|
805
|
+
const height = Math.max(0, (frame.maxY - frame.minY) * ppt);
|
|
806
|
+
const shadowTileSize = Math.max(1, Math.floor(opts.shadowTileSize ?? 1024));
|
|
807
|
+
const stats = opts.stats;
|
|
808
|
+
if (stats) {
|
|
809
|
+
stats.shadowRuns = 0;
|
|
810
|
+
stats.shadowTiles = 0;
|
|
811
|
+
stats.shadowCompositedPixels = 0;
|
|
812
|
+
stats.shadowPeakScratchPixels = 0;
|
|
813
|
+
}
|
|
814
|
+
ctx.imageSmoothingEnabled = false;
|
|
815
|
+
if (opts.showSpace) {
|
|
816
|
+
const planetFrameId = opts.spaceBackground?.planetFrame;
|
|
817
|
+
const planetFrame = planetFrameId != null ? opts.frames[planetFrameId] : void 0;
|
|
818
|
+
const planetImage = planetFrame ? images[planetFrame.a] : void 0;
|
|
819
|
+
drawSpaceBackground(ctx, width, height, {
|
|
820
|
+
seed: planetFrameId,
|
|
821
|
+
viewport: opts.outputTileFrame ? {
|
|
822
|
+
x: (frame.minX - opts.outputTileFrame.minX) * ppt,
|
|
823
|
+
y: (frame.minY - opts.outputTileFrame.minY) * ppt,
|
|
824
|
+
fullWidth: (opts.outputTileFrame.maxX - opts.outputTileFrame.minX) * ppt,
|
|
825
|
+
fullHeight: (opts.outputTileFrame.maxY - opts.outputTileFrame.minY) * ppt
|
|
826
|
+
} : void 0,
|
|
827
|
+
planet: planetFrame && planetImage ? {
|
|
828
|
+
frame: planetFrame,
|
|
829
|
+
image: planetImage
|
|
830
|
+
} : void 0
|
|
831
|
+
});
|
|
832
|
+
} else if (opts.terrainBackground) drawTerrainBackground(ctx, width, height, {
|
|
833
|
+
tileFrame: frame,
|
|
834
|
+
pixelsPerTile: ppt,
|
|
835
|
+
frames: opts.frames,
|
|
836
|
+
images,
|
|
837
|
+
background: opts.terrainBackground,
|
|
838
|
+
fallbackColor: opts.terrainBackground.color ?? TERRAIN_FALLBACK_COLOR
|
|
839
|
+
});
|
|
840
|
+
else if (opts.showCheckerboard) drawTileCheckerboard(ctx, width, height, ppt, frame.minX, frame.minY);
|
|
841
|
+
else if (opts.background) {
|
|
842
|
+
ctx.fillStyle = rgba(opts.background);
|
|
843
|
+
ctx.fillRect(0, 0, width, height);
|
|
844
|
+
}
|
|
845
|
+
let shadowCanvas;
|
|
846
|
+
let shadowCtx = null;
|
|
847
|
+
const renderShadowRun = (commands) => {
|
|
848
|
+
if (!opts.createCanvas || commands.length === 0 || width <= 0 || height <= 0) return;
|
|
849
|
+
const tileWidth = Math.min(shadowTileSize, Math.ceil(width));
|
|
850
|
+
const tileHeight = Math.min(shadowTileSize, Math.ceil(height));
|
|
851
|
+
if (!shadowCanvas) {
|
|
852
|
+
shadowCanvas = opts.createCanvas(tileWidth, tileHeight);
|
|
853
|
+
shadowCanvas.width = tileWidth;
|
|
854
|
+
shadowCanvas.height = tileHeight;
|
|
855
|
+
shadowCtx = shadowCanvas.getContext("2d");
|
|
856
|
+
if (shadowCtx) shadowCtx.imageSmoothingEnabled = false;
|
|
857
|
+
}
|
|
858
|
+
if (!shadowCtx || !shadowCanvas) return;
|
|
859
|
+
const columns = Math.ceil(width / shadowTileSize);
|
|
860
|
+
const bins = /* @__PURE__ */ new Map();
|
|
861
|
+
for (const command of commands) {
|
|
862
|
+
const spriteFrame = frames[command.frame];
|
|
863
|
+
const image = spriteFrame ? images[spriteFrame.a] : void 0;
|
|
864
|
+
if (!spriteFrame || !image) continue;
|
|
865
|
+
const bounds = shadowSpriteBounds(command, spriteFrame, ox, oy, ppt, width, height);
|
|
866
|
+
if (!bounds) continue;
|
|
867
|
+
const minTileX = Math.floor(bounds.minX / shadowTileSize);
|
|
868
|
+
const minTileY = Math.floor(bounds.minY / shadowTileSize);
|
|
869
|
+
const maxTileX = Math.floor((bounds.maxX - 1) / shadowTileSize);
|
|
870
|
+
const maxTileY = Math.floor((bounds.maxY - 1) / shadowTileSize);
|
|
871
|
+
for (let tileY = minTileY; tileY <= maxTileY; tileY++) for (let tileX = minTileX; tileX <= maxTileX; tileX++) {
|
|
872
|
+
const key = tileY * columns + tileX;
|
|
873
|
+
const bin = bins.get(key);
|
|
874
|
+
if (bin) bin.push(command);
|
|
875
|
+
else bins.set(key, [command]);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (stats) {
|
|
879
|
+
stats.shadowRuns++;
|
|
880
|
+
stats.shadowPeakScratchPixels = Math.max(stats.shadowPeakScratchPixels, shadowCanvas.width * shadowCanvas.height);
|
|
881
|
+
}
|
|
882
|
+
const prevAlpha = ctx.globalAlpha;
|
|
883
|
+
for (const [key, tileCommands] of [...bins].sort((a, b) => a[0] - b[0])) {
|
|
884
|
+
const tileX = key % columns;
|
|
885
|
+
const tileY = Math.floor(key / columns);
|
|
886
|
+
const outputX = tileX * shadowTileSize;
|
|
887
|
+
const outputY = tileY * shadowTileSize;
|
|
888
|
+
const outputTileWidth = Math.min(shadowTileSize, width - outputX);
|
|
889
|
+
const outputTileHeight = Math.min(shadowTileSize, height - outputY);
|
|
890
|
+
shadowCtx.clearRect(0, 0, shadowCanvas.width, shadowCanvas.height);
|
|
891
|
+
shadowCtx.save();
|
|
892
|
+
shadowCtx.translate(-outputX, -outputY);
|
|
893
|
+
for (const command of tileCommands) {
|
|
894
|
+
const spriteFrame = frames[command.frame];
|
|
895
|
+
const image = spriteFrame ? images[spriteFrame.a] : void 0;
|
|
896
|
+
if (!spriteFrame || !image) continue;
|
|
897
|
+
drawSprite(shadowCtx, {
|
|
898
|
+
...command,
|
|
899
|
+
shadow: false
|
|
900
|
+
}, spriteFrame, image, ox, oy, ppt, opts.createCanvas);
|
|
901
|
+
}
|
|
902
|
+
shadowCtx.restore();
|
|
903
|
+
ctx.globalAlpha = prevAlpha * .5;
|
|
904
|
+
ctx.drawImage(shadowCanvas, 0, 0, outputTileWidth, outputTileHeight, outputX, outputY, outputTileWidth, outputTileHeight);
|
|
905
|
+
if (stats) {
|
|
906
|
+
stats.shadowTiles++;
|
|
907
|
+
stats.shadowCompositedPixels += outputTileWidth * outputTileHeight;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
ctx.globalAlpha = prevAlpha;
|
|
911
|
+
};
|
|
912
|
+
for (let commandIndex = 0; commandIndex < list.commands.length; commandIndex++) {
|
|
913
|
+
const cmd = list.commands[commandIndex];
|
|
914
|
+
if (cmd.kind === "sprite" && cmd.shadow && opts.createCanvas) {
|
|
915
|
+
const shadowRun = [];
|
|
916
|
+
while (commandIndex < list.commands.length) {
|
|
917
|
+
const candidate = list.commands[commandIndex];
|
|
918
|
+
if (candidate?.kind !== "sprite" || !candidate.shadow) break;
|
|
919
|
+
shadowRun.push(candidate);
|
|
920
|
+
commandIndex++;
|
|
921
|
+
}
|
|
922
|
+
commandIndex--;
|
|
923
|
+
renderShadowRun(shadowRun);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
switch (cmd.kind) {
|
|
927
|
+
case "rect":
|
|
928
|
+
drawRect(ctx, cmd, ox, oy, ppt);
|
|
929
|
+
break;
|
|
930
|
+
case "sprite": {
|
|
931
|
+
const frame = frames[cmd.frame];
|
|
932
|
+
const image = frame ? images[frame.a] : void 0;
|
|
933
|
+
if (!frame || !image) break;
|
|
934
|
+
drawSprite(ctx, cmd, frame, image, ox, oy, ppt, opts.createCanvas);
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
case "wire":
|
|
938
|
+
drawWire(ctx, cmd, ox, oy, ppt);
|
|
939
|
+
break;
|
|
940
|
+
case "train-chain":
|
|
941
|
+
drawTrainChain(ctx, cmd, ox, oy, ppt);
|
|
942
|
+
break;
|
|
943
|
+
case "snap-grid":
|
|
944
|
+
drawSnapGrid(ctx, cmd, ox, oy, ppt);
|
|
945
|
+
break;
|
|
946
|
+
case "text":
|
|
947
|
+
drawText(ctx, cmd, ox, oy, ppt);
|
|
948
|
+
break;
|
|
949
|
+
case "icon": {
|
|
950
|
+
const frame = frames[cmd.frame];
|
|
951
|
+
const image = frame ? images[frame.a] : void 0;
|
|
952
|
+
if (!frame || !image) break;
|
|
953
|
+
drawIcon(ctx, cmd, frame, image, frames, images, opts.iconImages?.get(cmd.frame), opts.silhouetteImages?.get(cmd.frame), ox, oy, ppt);
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (opts.showCoordinates) drawCoordinateOverlay(ctx, frame, ppt, width, height, opts.outputTileFrame ?? frame);
|
|
959
|
+
}
|
|
960
|
+
//#endregion
|
|
961
|
+
export { drawCoordinateOverlay as a, FPSR_TEXT_FONT_FALLBACK as c, drawSpacePlanet as i, FPSR_TEXT_FONT_FAMILY as l, drawTerrainBackground as n, blitWithTileCheckerboard as o, drawSpaceBackground as r, drawTileCheckerboard as s, executeDrawList as t, fpsrTextFontCss as u };
|
|
962
|
+
|
|
963
|
+
//# sourceMappingURL=execute-CRZV5zw1.js.map
|