@adatechnology/game-fluids 0.1.8 → 0.2.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.
@@ -23,17 +23,28 @@ var GameFluids = (() => {
23
23
  // src/index.ts
24
24
  var index_exports = {};
25
25
  __export(index_exports, {
26
+ FILM_PROFILES: () => FILM_PROFILES,
26
27
  ITEM_USES: () => ITEM_USES,
27
28
  LIQUIDS: () => LIQUIDS,
29
+ PADRAO_LAYOUT: () => PADRAO_LAYOUT,
28
30
  STAIN_SAMPLES: () => SAMPLES,
31
+ WORKSHOP_LAYOUT: () => WORKSHOP_LAYOUT,
29
32
  ballistic: () => ballistic,
33
+ createCpuSimulator: () => createCpuSimulator,
34
+ createFilm: () => createFilm,
35
+ createFilmLcg: () => createFilmLcg,
30
36
  createFluids: () => createFluids,
37
+ createFoldedLayout: () => createFoldedLayout,
31
38
  createItemUse: () => createItemUse,
32
39
  describeEvent: () => describeEvent,
40
+ filmProfileOf: () => filmProfileOf,
41
+ hashCell: () => hashCell,
42
+ layoutFromSurfaces: () => layoutFromSurfaces,
33
43
  locales: () => locales,
34
44
  resolveStrings: () => resolveStrings,
35
45
  sampleStream: () => sampleStream,
36
- stainDirection: () => stainDirection
46
+ stainDirection: () => stainDirection,
47
+ valueNoise: () => valueNoise
37
48
  });
38
49
 
39
50
  // src/data/liquids.json
@@ -45,7 +56,7 @@ var GameFluids = (() => {
45
56
  stainColor: 7309206,
46
57
  viscosity: 1,
47
58
  density: 1e3,
48
- dryTime: 600,
59
+ dryTime: 180,
49
60
  gloss: 0.9,
50
61
  drink: {
51
62
  thirst: 100,
@@ -223,9 +234,9 @@ var GameFluids = (() => {
223
234
  stainColor: 7232298,
224
235
  viscosity: 0.6,
225
236
  density: 740,
226
- dryTime: 120,
237
+ dryTime: 60,
227
238
  gloss: 0.95,
228
- leavesMark: true,
239
+ leavesMark: false,
229
240
  flammable: true,
230
241
  drink: {
231
242
  thirst: 0,
@@ -306,10 +317,10 @@ var GameFluids = (() => {
306
317
  id: "blood",
307
318
  color: 7997964,
308
319
  opacity: 0.92,
309
- stainColor: 4850950,
320
+ stainColor: 3874068,
310
321
  viscosity: 4,
311
322
  density: 1060,
312
- dryTime: 1200,
323
+ dryTime: 600,
313
324
  gloss: 0.75,
314
325
  leavesMark: true,
315
326
  drink: {
@@ -322,6 +333,46 @@ var GameFluids = (() => {
322
333
  "organic"
323
334
  ]
324
335
  },
336
+ {
337
+ id: "old_blood",
338
+ color: 7085078,
339
+ opacity: 0.92,
340
+ stainColor: 2758158,
341
+ viscosity: 4,
342
+ density: 1060,
343
+ dryTime: 360,
344
+ gloss: 0.7,
345
+ leavesMark: true,
346
+ drink: {
347
+ thirst: 10,
348
+ effects: [
349
+ "disgust"
350
+ ]
351
+ },
352
+ tags: [
353
+ "organic"
354
+ ]
355
+ },
356
+ {
357
+ id: "bile",
358
+ color: 9083434,
359
+ opacity: 0.9,
360
+ stainColor: 4080160,
361
+ viscosity: 6,
362
+ density: 1020,
363
+ dryTime: 900,
364
+ gloss: 0.75,
365
+ leavesMark: true,
366
+ drink: {
367
+ thirst: 0,
368
+ effects: [
369
+ "disgust"
370
+ ]
371
+ },
372
+ tags: [
373
+ "organic"
374
+ ]
375
+ },
325
376
  {
326
377
  id: "sap",
327
378
  color: 13146940,
@@ -564,6 +615,8 @@ var GameFluids = (() => {
564
615
  motor_oil: "motor oil",
565
616
  cooking_oil: "cooking oil",
566
617
  blood: "blood",
618
+ old_blood: "old blood",
619
+ bile: "bile",
567
620
  sap: "sap",
568
621
  acid: "acid",
569
622
  paint: "paint",
@@ -604,6 +657,8 @@ var GameFluids = (() => {
604
657
  motor_oil: "\xF3leo de motor",
605
658
  cooking_oil: "\xF3leo de cozinha",
606
659
  blood: "sangue",
660
+ old_blood: "sangue antigo",
661
+ bile: "bile",
607
662
  sap: "seiva",
608
663
  acid: "\xE1cido",
609
664
  paint: "tinta",
@@ -1115,7 +1170,7 @@ var GameFluids = (() => {
1115
1170
  }
1116
1171
  if (victim) stains.delete(victim.id);
1117
1172
  };
1118
- const deposit = (liquid, hit, liters, velocity) => {
1173
+ const deposit = (liquid, hit, liters, velocity, tint) => {
1119
1174
  const def = liquids.get(liquid);
1120
1175
  if (!def || liters <= 0) return;
1121
1176
  let s = stainAt(hit.surface, liquid, hit.point);
@@ -1123,6 +1178,7 @@ var GameFluids = (() => {
1123
1178
  const basis2 = { normal: s.normal, t1: s.tangent, t2: cross(s.normal, s.tangent) };
1124
1179
  nudgeCenter(s, hit.point, clamp(liters / (s.liters + liters) * 0.5, 0, 0.5), basis2);
1125
1180
  addInflow(s, liters, velocity);
1181
+ if (tint) s.tint = { ...tint };
1126
1182
  return;
1127
1183
  }
1128
1184
  while (stains.size >= maxStains) evictStain();
@@ -1130,6 +1186,7 @@ var GameFluids = (() => {
1130
1186
  const basis = hit.surface === GROUND_ID ? makeBasis(hit.normal) : surface;
1131
1187
  s = createStain(`stain_${++counters.stain}`, hit.surface, def, basis, hit.point, liters, rng);
1132
1188
  if (velocity) addInflow(s, 0, velocity);
1189
+ if (tint) s.tint = { ...tint };
1133
1190
  s.sinceInflow = 0;
1134
1191
  stains.set(s.id, s);
1135
1192
  emit({ type: "stain", stain: s.id, surface: hit.surface, liquid, point: copy3(hit.point) });
@@ -1322,7 +1379,8 @@ var GameFluids = (() => {
1322
1379
  tangent: copy3(s.tangent),
1323
1380
  flow: s.flow ? copy3(s.flow) : void 0,
1324
1381
  mark: s.mark,
1325
- age: s.age
1382
+ age: s.age,
1383
+ tint: s.tint ? { ...s.tint } : void 0
1326
1384
  });
1327
1385
  const world = {
1328
1386
  liquids,
@@ -1507,14 +1565,14 @@ var GameFluids = (() => {
1507
1565
  surfaces.delete(id);
1508
1566
  for (const s of [...stains.values()]) if (s.surface === id) stains.delete(s.id);
1509
1567
  },
1510
- splash(liquid, point, liters, velocity) {
1568
+ splash(liquid, point, liters, velocity, options2) {
1511
1569
  if (!liquids.has(liquid) || !(liters > 0)) return;
1512
1570
  const v = velocity ?? [0, -0.1, 0];
1513
1571
  const origin = add(point, [0, 0.02, 0]);
1514
1572
  const hit = trace(landingSurfaces(), groundAt, origin, v, gravity);
1515
1573
  if (!hit) return;
1516
1574
  emit({ type: "splash", liquid, point: copy3(hit.point), intensity: clamp(0.3 + liters * 2, 0, 1) });
1517
- deposit(liquid, hit, liters, length(v) > 0.3 ? hit.velocity : null);
1575
+ deposit(liquid, hit, liters, length(v) > 0.3 ? hit.velocity : null, options2?.tint);
1518
1576
  },
1519
1577
  clearStains(point, radius) {
1520
1578
  for (const s of [...stains.values()]) {
@@ -2202,6 +2260,974 @@ var GameFluids = (() => {
2202
2260
  };
2203
2261
  return player;
2204
2262
  }
2263
+
2264
+ // src/film/deposit.ts
2265
+ var VISIBLE_THICKNESS = 0.035;
2266
+ function depositCell(context, rowIn, columnIn, amount) {
2267
+ const { field } = context;
2268
+ const row = Math.round(rowIn);
2269
+ const column = Math.round(columnIn);
2270
+ if (row < 0 || column < 0 || row >= field.height || column >= field.width || !(amount > 0)) return;
2271
+ const index = row * field.width + column;
2272
+ field.thickness[index] += amount;
2273
+ if (amount > 0.02) field.dryness[index] *= 0.15;
2274
+ field.dampness[index] = 1;
2275
+ }
2276
+ function wetCellCount(field) {
2277
+ let cells = 0;
2278
+ for (let index = 0; index < field.thickness.length; index++) if (field.thickness[index] > VISIBLE_THICKNESS) cells++;
2279
+ return cells;
2280
+ }
2281
+ function depositBlob(context, centerRow, centerColumn, along, across, directionColumn, directionRow, amount, roughness) {
2282
+ const random = context.random;
2283
+ const phaseA = random() * 6.28;
2284
+ const phaseB = random() * 6.28;
2285
+ const phaseC = random() * 6.28;
2286
+ const phaseD = random() * 6.28;
2287
+ const reach = Math.ceil(Math.max(along, across) * (1 + roughness) + 1.5);
2288
+ for (let row = Math.floor(centerRow - reach); row <= centerRow + reach; row++) {
2289
+ for (let column = Math.floor(centerColumn - reach); column <= centerColumn + reach; column++) {
2290
+ const dx = column + 0.5 - centerColumn;
2291
+ const dy = row + 0.5 - centerRow;
2292
+ const alongDistance = dx * directionColumn + dy * directionRow;
2293
+ const acrossDistance = -dx * directionRow + dy * directionColumn;
2294
+ const angle = Math.atan2(acrossDistance / across, alongDistance / along);
2295
+ const edge = 1 + roughness * (0.45 * Math.sin(3 * angle + phaseA) + 0.3 * Math.sin(5 * angle + phaseB) + 0.18 * Math.sin(8 * angle + phaseC) + 0.12 * Math.sin(13 * angle + phaseD));
2296
+ const squared = (alongDistance / along * (alongDistance / along) + acrossDistance / across * (acrossDistance / across)) / (edge * edge);
2297
+ if (squared < 1) depositCell(context, row, column, amount * Math.sqrt(1 - squared));
2298
+ }
2299
+ }
2300
+ }
2301
+ function depositStroke(context, fromRow, fromColumn, toRow, toColumn, startWidth, endWidth, amount) {
2302
+ const { field } = context;
2303
+ const length2 = Math.hypot(toRow - fromRow, toColumn - fromColumn);
2304
+ const samples = Math.max(2, Math.ceil(length2 * 2.5));
2305
+ for (let sample = 0; sample <= samples; sample++) {
2306
+ const progress = sample / samples;
2307
+ const row = fromRow + (toRow - fromRow) * progress;
2308
+ const column = fromColumn + (toColumn - fromColumn) * progress;
2309
+ const width = startWidth + (endWidth - startWidth) * progress;
2310
+ const reach = Math.ceil(width + 1);
2311
+ for (let cellRow = Math.floor(row - reach); cellRow <= row + reach; cellRow++) {
2312
+ for (let cellColumn = Math.floor(column - reach); cellColumn <= column + reach; cellColumn++) {
2313
+ if (cellRow < 0 || cellColumn < 0 || cellRow >= field.height || cellColumn >= field.width) continue;
2314
+ const distance2 = Math.hypot(cellRow + 0.5 - row, cellColumn + 0.5 - column);
2315
+ if (distance2 >= width) continue;
2316
+ const index = cellRow * field.width + cellColumn;
2317
+ const target = amount * (1 - distance2 / width) * 0.5;
2318
+ if (field.thickness[index] < target) depositCell(context, cellRow, cellColumn, target - field.thickness[index]);
2319
+ }
2320
+ }
2321
+ }
2322
+ }
2323
+ function depositSplat(context, row, column, size, directionColumn, directionRow, sinAngleIn, amount) {
2324
+ const random = context.random;
2325
+ const spikes = context.profile().spikes;
2326
+ const sinAngle = Math.max(0.17, Math.min(1, sinAngleIn));
2327
+ const stretch = Math.min(5.5, 1 / sinAngle);
2328
+ const along = size * Math.pow(stretch, 0.75);
2329
+ const across = size / Math.pow(stretch, 0.25);
2330
+ const frontRow = row + directionRow * along * 0.25;
2331
+ const frontColumn = column + directionColumn * along * 0.25;
2332
+ depositBlob(context, frontRow, frontColumn, along, across, directionColumn, directionRow, amount, 0.1 + 0.12 * (1 - sinAngle) + (size < 1.4 ? 0 : 0.06));
2333
+ const sideColumn = -directionRow;
2334
+ const sideRow = directionColumn;
2335
+ const spikeCount = Math.round(Math.min(18, (size < 1.2 ? 1 : 4) + size * 1.6 + 5 * (1 - sinAngle)) * spikes);
2336
+ for (let spike = 0; spike < spikeCount; spike++) {
2337
+ const spread = Math.PI * (0.25 + 0.75 * sinAngle * sinAngle);
2338
+ let theta = (random() * 2 - 1) * spread;
2339
+ if (sinAngle > 0.9) theta = random() * 6.283;
2340
+ const cosine = Math.cos(theta);
2341
+ const sine = Math.sin(theta);
2342
+ const dx = cosine * directionColumn + sine * sideColumn;
2343
+ const dy = cosine * directionRow + sine * sideRow;
2344
+ const length2 = size * (0.25 + random() * 0.55) * (1 + 2.6 * (1 - sinAngle) * Math.max(0, cosine)) * (sinAngle > 0.9 ? 0.6 : 1) * (0.5 + 0.5 * spikes);
2345
+ const startRow = frontRow + (along * cosine * directionRow + across * sine * sideRow) * 0.75;
2346
+ const startColumn = frontColumn + (along * cosine * directionColumn + across * sine * sideColumn) * 0.75;
2347
+ const endRow = startRow + dy * length2;
2348
+ const endColumn = startColumn + dx * length2;
2349
+ depositStroke(context, startRow, startColumn, endRow, endColumn, Math.max(0.55, size * 0.22), 0.42, amount * 0.75);
2350
+ if (random() < 0.5) {
2351
+ const gap = 1 + random() * 2.2;
2352
+ depositBlob(context, endRow + dy * gap, endColumn + dx * gap, 0.55 + random() * 0.7, 0.5 + random() * 0.4, dx, dy, amount * 0.55, 0.08);
2353
+ }
2354
+ }
2355
+ const satelliteCount = Math.round((1 + size * 0.9) * (1.4 - sinAngle * 0.6) * (0.5 + 0.5 * spikes));
2356
+ for (let satellite = 0; satellite < satelliteCount; satellite++) {
2357
+ const cone = sinAngle > 0.9 ? Math.PI : 0.18 + 0.6 * sinAngle;
2358
+ const theta = (random() * 2 - 1) * cone;
2359
+ const cosine = Math.cos(theta);
2360
+ const sine = Math.sin(theta);
2361
+ const dx = cosine * directionColumn + sine * sideColumn;
2362
+ const dy = cosine * directionRow + sine * sideRow;
2363
+ const distance2 = along + size * (0.8 + random() * 3.2);
2364
+ const radius = 0.45 + random() * Math.min(1.2, size * 0.22);
2365
+ const satelliteRow = frontRow + dy * distance2;
2366
+ const satelliteColumn = frontColumn + dx * distance2;
2367
+ depositBlob(context, satelliteRow, satelliteColumn, radius * Math.sqrt(stretch), radius, dx, dy, amount * 0.6, 0.08);
2368
+ if (sinAngle < 0.8) depositStroke(context, satelliteRow + dy * radius * stretch * 0.7, satelliteColumn + dx * radius * stretch * 0.7, satelliteRow + dy * radius * stretch * 1.8, satelliteColumn + dx * radius * stretch * 1.8, 0.5, 0.3, amount * 0.35);
2369
+ }
2370
+ }
2371
+
2372
+ // src/film/drops.ts
2373
+ var GRAVITY2 = 9.8;
2374
+ var AIR_DRAG = 0.25;
2375
+ var MIN_SIZE_CELLS = 0.6;
2376
+ function createDropPool(layout, context) {
2377
+ const capacity = layout.rules.dropCapacity;
2378
+ let cursor = 0;
2379
+ const state = {
2380
+ capacity,
2381
+ get launched() {
2382
+ return cursor;
2383
+ },
2384
+ active: new Uint8Array(capacity),
2385
+ position: new Float64Array(capacity * 3),
2386
+ velocity: new Float64Array(capacity * 3),
2387
+ radius: new Float64Array(capacity)
2388
+ };
2389
+ const { active, position, velocity, radius } = state;
2390
+ const cellMillimetres = layout.cellSize * 1e3;
2391
+ const previous = [0, 0, 0];
2392
+ const point = [0, 0, 0];
2393
+ const speed = [0, 0, 0];
2394
+ function land(region, dropRadius) {
2395
+ const profile = context.profile();
2396
+ const impact = layout.impact(region, point, speed);
2397
+ const [alongColumn, alongRow] = impact.along;
2398
+ const travel = Math.hypot(alongColumn, alongRow) || 1e-6;
2399
+ const sinAngle = Math.abs(impact.normalSpeed) / Math.hypot(travel, impact.normalSpeed);
2400
+ const volume = 4 / 3 * Math.PI * dropRadius * dropRadius * dropRadius / (cellMillimetres * cellMillimetres) * (dropRadius > 3 ? 7 : 4) * profile.volume;
2401
+ const totalSpeed = Math.sqrt(speed[0] * speed[0] + speed[1] * speed[1] + speed[2] * speed[2]);
2402
+ const size = Math.max(MIN_SIZE_CELLS, dropRadius * 2 / cellMillimetres * (1 + 0.2 * totalSpeed));
2403
+ const amount = Math.min(profile.maxAmount, Math.max(0.25, volume / (Math.PI * size * size * 1.6 / (sinAngle < 0.3 ? 0.6 : 1))));
2404
+ if (profile.landsAsBlob) {
2405
+ depositBlob(context, impact.row, impact.column, size * 1.3, size * 1.1, alongColumn / travel, alongRow / travel, amount, 0.2);
2406
+ return;
2407
+ }
2408
+ depositSplat(context, impact.row, impact.column, size, alongColumn / travel, alongRow / travel, sinAngle, amount);
2409
+ }
2410
+ return {
2411
+ state,
2412
+ launch(origin, direction, dropRadius) {
2413
+ const slot = cursor++ % capacity;
2414
+ const offset = slot * 3;
2415
+ active[slot] = 1;
2416
+ position[offset] = origin[0];
2417
+ position[offset + 1] = origin[1];
2418
+ position[offset + 2] = origin[2];
2419
+ velocity[offset] = direction[0];
2420
+ velocity[offset + 1] = direction[1];
2421
+ velocity[offset + 2] = direction[2];
2422
+ radius[slot] = dropRadius;
2423
+ },
2424
+ update(dt) {
2425
+ for (let slot = 0; slot < capacity; slot++) {
2426
+ if (!active[slot]) continue;
2427
+ const offset = slot * 3;
2428
+ previous[0] = position[offset];
2429
+ previous[1] = position[offset + 1];
2430
+ previous[2] = position[offset + 2];
2431
+ velocity[offset + 1] -= GRAVITY2 * dt;
2432
+ const drag = 1 - dt * AIR_DRAG;
2433
+ velocity[offset] *= drag;
2434
+ velocity[offset + 1] *= drag;
2435
+ velocity[offset + 2] *= drag;
2436
+ point[0] = position[offset] + velocity[offset] * dt;
2437
+ point[1] = position[offset + 1] + velocity[offset + 1] * dt;
2438
+ point[2] = position[offset + 2] + velocity[offset + 2] * dt;
2439
+ const region = layout.collide(previous, point);
2440
+ position[offset] = point[0];
2441
+ position[offset + 1] = point[1];
2442
+ position[offset + 2] = point[2];
2443
+ if (region >= 0) {
2444
+ if (layout.contains(point)) {
2445
+ speed[0] = velocity[offset];
2446
+ speed[1] = velocity[offset + 1];
2447
+ speed[2] = velocity[offset + 2];
2448
+ land(region, radius[slot]);
2449
+ }
2450
+ active[slot] = 0;
2451
+ continue;
2452
+ }
2453
+ if (layout.escaped(point)) active[slot] = 0;
2454
+ }
2455
+ },
2456
+ clear() {
2457
+ active.fill(0);
2458
+ }
2459
+ };
2460
+ }
2461
+
2462
+ // src/film/simulate.ts
2463
+ var RAIN_SPREAD = 0.5;
2464
+ var BOUNDS_MARGIN = 8;
2465
+ function createCpuSimulator(field, layout, profileOf, options) {
2466
+ const { width, height, thickness, dryness, dampness } = field;
2467
+ const { bed, pin: pinning, vertical } = layout;
2468
+ const delta = new Float32Array(width * height);
2469
+ const bounds = { firstRow: 0, lastRow: -1, firstColumn: 0, lastColumn: -1 };
2470
+ let accumulated = 0;
2471
+ function refreshBounds() {
2472
+ let firstRow = height;
2473
+ let lastRow = -1;
2474
+ let firstColumn = width;
2475
+ let lastColumn = -1;
2476
+ for (let row = 0; row < height; row++) {
2477
+ const offset = row * width;
2478
+ let isWet = false;
2479
+ for (let column = 0; column < width; column++) {
2480
+ if (thickness[offset + column] > 0) {
2481
+ isWet = true;
2482
+ if (column < firstColumn) firstColumn = column;
2483
+ if (column > lastColumn) lastColumn = column;
2484
+ }
2485
+ }
2486
+ if (isWet) {
2487
+ if (row < firstRow) firstRow = row;
2488
+ lastRow = row;
2489
+ }
2490
+ }
2491
+ if (lastRow < 0) {
2492
+ bounds.firstRow = 0;
2493
+ bounds.lastRow = -1;
2494
+ bounds.firstColumn = 0;
2495
+ bounds.lastColumn = -1;
2496
+ return;
2497
+ }
2498
+ bounds.firstRow = Math.max(0, firstRow - BOUNDS_MARGIN);
2499
+ bounds.lastRow = Math.min(height - 1, lastRow + BOUNDS_MARGIN);
2500
+ bounds.firstColumn = Math.max(0, firstColumn - BOUNDS_MARGIN);
2501
+ bounds.lastColumn = Math.min(width - 1, lastColumn + BOUNDS_MARGIN);
2502
+ }
2503
+ const mobility = (index) => {
2504
+ const dry = dryness[index];
2505
+ return dry < 0.25 ? 1 : dry > 0.6 ? 0 : 1 - (dry - 0.25) / 0.35;
2506
+ };
2507
+ function flowStep() {
2508
+ const { firstRow, lastRow, firstColumn, lastColumn } = bounds;
2509
+ if (lastRow < 0) return;
2510
+ const profile = profileOf();
2511
+ const flow = profile.flow * (1 + RAIN_SPREAD * options.weather().rain);
2512
+ const referenceSquared = profile.referenceThickness * profile.referenceThickness;
2513
+ const slopeCap = profile.slopeCap;
2514
+ const pinLevel = profile.pin;
2515
+ const front = profile.front;
2516
+ for (let row = firstRow; row <= lastRow; row++) {
2517
+ const offset = row * width;
2518
+ for (let column = firstColumn; column <= lastColumn; column++) delta[offset + column] = 0;
2519
+ }
2520
+ for (let row = firstRow; row <= lastRow; row++) {
2521
+ const offset = row * width;
2522
+ for (let column = firstColumn; column <= lastColumn; column++) {
2523
+ const index = offset + column;
2524
+ const own = thickness[index];
2525
+ for (let edge = 0; edge < 2; edge++) {
2526
+ let neighbour;
2527
+ if (edge === 0) {
2528
+ if (column >= lastColumn) continue;
2529
+ neighbour = index + 1;
2530
+ } else {
2531
+ if (row >= lastRow) continue;
2532
+ neighbour = index + width;
2533
+ }
2534
+ const other = thickness[neighbour];
2535
+ if (own < 5e-3 && other < 5e-3) continue;
2536
+ let bedDrop = bed[index] - bed[neighbour];
2537
+ if (bedDrop > slopeCap) bedDrop = slopeCap;
2538
+ else if (bedDrop < -slopeCap) bedDrop = -slopeCap;
2539
+ let head = bedDrop + own - other;
2540
+ let source = index;
2541
+ let destination = neighbour;
2542
+ if (head < 0) {
2543
+ head = -head;
2544
+ source = neighbour;
2545
+ destination = index;
2546
+ }
2547
+ const sourceThickness = thickness[source];
2548
+ const verticalFactor = vertical[source];
2549
+ const pinThreshold = pinLevel * pinning[source] * (verticalFactor < 1 ? 0.5 : 1);
2550
+ if (sourceThickness <= pinThreshold) continue;
2551
+ if (thickness[destination] < 0.03 && sourceThickness < front * pinning[source] * verticalFactor) continue;
2552
+ const mobile = mobility(source);
2553
+ if (mobile <= 0) continue;
2554
+ let moved = flow * mobile * (sourceThickness * sourceThickness / (sourceThickness * sourceThickness + referenceSquared)) * head * (verticalFactor < 1 ? edge === 0 ? 0.22 : 1 : 4);
2555
+ const limit = (sourceThickness - pinThreshold) * 0.24;
2556
+ if (moved > limit) moved = limit;
2557
+ delta[source] -= moved;
2558
+ delta[destination] += moved;
2559
+ }
2560
+ }
2561
+ }
2562
+ for (let row = firstRow; row <= lastRow; row++) {
2563
+ const offset = row * width;
2564
+ for (let column = firstColumn; column <= lastColumn; column++) {
2565
+ const index = offset + column;
2566
+ let next = thickness[index] + delta[index];
2567
+ if (next < 1e-4) next = 0;
2568
+ else dampness[index] = 1;
2569
+ thickness[index] = next;
2570
+ }
2571
+ }
2572
+ }
2573
+ function age(dryingSeconds) {
2574
+ const { firstRow, lastRow, firstColumn, lastColumn } = bounds;
2575
+ if (lastRow < 0) return;
2576
+ const profile = profileOf();
2577
+ const rainScale = 1 - options.weather().rain;
2578
+ const seconds = dryingSeconds * rainScale;
2579
+ for (let row = firstRow; row <= lastRow; row++) {
2580
+ const offset = row * width;
2581
+ for (let column = firstColumn; column <= lastColumn; column++) {
2582
+ const index = offset + column;
2583
+ const own = thickness[index];
2584
+ if (own > 0) {
2585
+ const thin = 1 - Math.min(1, own / 1.6);
2586
+ if (profile.dryTime > 0 && dryness[index] < 1) dryness[index] = Math.min(1, dryness[index] + seconds / profile.dryTime * (0.45 + 1.7 * thin));
2587
+ if (profile.evaporation > 0) {
2588
+ const next = own - profile.evaporation * seconds * (0.5 + 1.5 * thin);
2589
+ thickness[index] = next > 4e-3 ? next : 0;
2590
+ }
2591
+ } else if (dampness[index] > 0) {
2592
+ dampness[index] = Math.max(0, dampness[index] - seconds / (profile.evaporation > 0 ? 25 : 1e9));
2593
+ }
2594
+ }
2595
+ }
2596
+ }
2597
+ return {
2598
+ bounds,
2599
+ refreshBounds,
2600
+ advance({ dt, dryingSeconds }) {
2601
+ accumulated += dt;
2602
+ let steps = 0;
2603
+ refreshBounds();
2604
+ while (accumulated >= 1 / options.stepRate && steps < options.maxSteps) {
2605
+ flowStep();
2606
+ accumulated -= 1 / options.stepRate;
2607
+ steps++;
2608
+ }
2609
+ if (steps === options.maxSteps) accumulated = 0;
2610
+ age(dryingSeconds);
2611
+ return steps;
2612
+ },
2613
+ resetClock() {
2614
+ accumulated = 0;
2615
+ }
2616
+ };
2617
+ }
2618
+
2619
+ // src/film/sources.ts
2620
+ var range = (random, from, to) => from + random() * (to - from);
2621
+ function updateDrip(drip, bead, layout, random, launch, dt) {
2622
+ if (!drip) {
2623
+ bead.visible = false;
2624
+ return;
2625
+ }
2626
+ drip.elapsed += dt;
2627
+ const swell = Math.min(1, drip.elapsed / drip.every);
2628
+ const radius = drip.radius * (0.35 + 0.65 * swell);
2629
+ const isWorkshop = layout.rules.dripStyle === "workshop";
2630
+ bead.visible = true;
2631
+ bead.swell = swell;
2632
+ bead.radius = radius;
2633
+ bead.position[0] = drip.position[0];
2634
+ bead.position[1] = drip.position[1] + (isWorkshop ? -radius * 12e-4 * (1 + swell * 0.7) : -radius * 12e-4);
2635
+ bead.position[2] = drip.position[2];
2636
+ if (drip.elapsed < drip.every) return;
2637
+ const origin = [bead.position[0], bead.position[1] + -radius * 1e-3, bead.position[2]];
2638
+ if (isWorkshop) {
2639
+ const sideX2 = range(random, -0.025, 0.025);
2640
+ const sideZ2 = range(random, -0.025, 0.025);
2641
+ launch(origin, [sideX2, -0.2, sideZ2], drip.radius * range(random, 0.85, 1.15));
2642
+ drip.elapsed = 0;
2643
+ drip.every = drip.base * range(random, 0.75, 1.25);
2644
+ return;
2645
+ }
2646
+ const sideX = (random() - 0.5) * 0.05;
2647
+ const sideZ = (random() - 0.5) * 0.05;
2648
+ launch(origin, [sideX, -0.2, sideZ], drip.radius * (0.85 + random() * 0.3));
2649
+ drip.elapsed = 0;
2650
+ drip.every = drip.base * (0.75 + random() * 0.5);
2651
+ }
2652
+ function updateSources(sources, context, dt) {
2653
+ const random = context.random;
2654
+ for (const source of sources) {
2655
+ source.elapsed += dt;
2656
+ if (source.elapsed < 0 || source.elapsed > source.duration) continue;
2657
+ const pulse = 0.6 + 0.4 * Math.max(0, Math.sin(source.elapsed * 5.2));
2658
+ const amount = source.rate * dt * pulse;
2659
+ for (let part = 0; part < 3; part++) {
2660
+ const angle = random() * 6.28;
2661
+ const distance2 = random() * source.radius;
2662
+ depositCell(context, source.row + Math.sin(angle) * distance2, source.column + Math.cos(angle) * distance2, amount / 3);
2663
+ }
2664
+ }
2665
+ }
2666
+ function updateDrags(drags, context, layout, launch, dt) {
2667
+ const random = context.random;
2668
+ for (const drag of drags) {
2669
+ if (drag.done) continue;
2670
+ drag.elapsed += dt;
2671
+ const progress = Math.min(1, drag.elapsed / drag.duration);
2672
+ const here = drag.path(progress);
2673
+ const ahead = drag.path(Math.min(1, progress + 0.01));
2674
+ let directionColumn = ahead.column - here.column;
2675
+ let directionRow = ahead.row - here.row;
2676
+ const length2 = Math.hypot(directionColumn, directionRow) || 1;
2677
+ directionColumn /= length2;
2678
+ directionRow /= length2;
2679
+ const sideColumn = -directionRow;
2680
+ const sideRow = directionColumn;
2681
+ const load = Math.pow(1 - progress, 1.3);
2682
+ for (let offset = -drag.width; offset <= drag.width; offset += 0.5) {
2683
+ const across = offset / drag.width;
2684
+ const stripe = drag.stripes[Math.round((offset + drag.width) * 2)];
2685
+ const edge = 1 - across * across;
2686
+ const amount = drag.amount * load * (0.25 + 0.75 * stripe) * edge;
2687
+ if (load < 0.45 && random() > load * 2.2) continue;
2688
+ depositCell(context, Math.round(here.row + sideRow * offset), Math.round(here.column + sideColumn * offset), amount * dt * 30);
2689
+ }
2690
+ if (drag.hand && random() < dt * 14 * load + 0.02) {
2691
+ const world = layout.cellToWorld(here.row, here.column);
2692
+ const origin = [world[0] + (random() - 0.5) * 0.06, drag.hand.height + random() * 0.3, world[2] + (random() - 0.5) * 0.06];
2693
+ const velocity = [directionColumn * drag.hand.speed, 0 * drag.hand.speed, directionRow * drag.hand.speed];
2694
+ velocity[0] += (random() - 0.5) * 0.3;
2695
+ velocity[1] += 0.2;
2696
+ velocity[2] += (random() - 0.5) * 0.3;
2697
+ launch(origin, velocity, 2 + random() * 2.2);
2698
+ }
2699
+ if (progress >= 1) drag.done = true;
2700
+ }
2701
+ }
2702
+
2703
+ // src/film/film.ts
2704
+ var DEFAULT_STEP_RATE = 180;
2705
+ var DEFAULT_MAX_STEPS = 6;
2706
+ function createFilm(options) {
2707
+ const { layout, random } = options;
2708
+ const cellCount = layout.width * layout.height;
2709
+ const field = {
2710
+ width: layout.width,
2711
+ height: layout.height,
2712
+ thickness: new Float32Array(cellCount),
2713
+ dryness: new Float32Array(cellCount),
2714
+ dampness: new Float32Array(cellCount)
2715
+ };
2716
+ let weatherState = { rain: 0 };
2717
+ let currentProfile = options.profile;
2718
+ const context = { field, random, profile: () => currentProfile };
2719
+ const drops = createDropPool(layout, context);
2720
+ const createSimulator = options.simulator ?? createCpuSimulator;
2721
+ const simulator = createSimulator(field, layout, () => currentProfile, {
2722
+ stepRate: options.stepRate ?? DEFAULT_STEP_RATE,
2723
+ maxSteps: options.maxSteps ?? DEFAULT_MAX_STEPS,
2724
+ weather: () => weatherState
2725
+ });
2726
+ const film = {
2727
+ layout,
2728
+ field,
2729
+ drops: drops.state,
2730
+ bead: { visible: false, position: [0, 0, 0], radius: 0, swell: 0 },
2731
+ simulator,
2732
+ get profile() {
2733
+ return currentProfile;
2734
+ },
2735
+ set profile(profile) {
2736
+ currentProfile = profile;
2737
+ },
2738
+ drip: void 0,
2739
+ sources: [],
2740
+ drags: [],
2741
+ launch: (position, velocity, radius) => drops.launch(position, velocity, radius),
2742
+ splat: (row, column, size, directionColumn, directionRow, sinAngle, amount) => depositSplat(context, row, column, size, directionColumn, directionRow, sinAngle, amount),
2743
+ blob: (row, column, along, across, directionColumn, directionRow, amount, roughness) => depositBlob(context, row, column, along, across, directionColumn, directionRow, amount, roughness),
2744
+ stroke: (fromRow, fromColumn, toRow, toColumn, startWidth, endWidth, amount) => depositStroke(context, fromRow, fromColumn, toRow, toColumn, startWidth, endWidth, amount),
2745
+ deposit: (row, column, amount) => depositCell(context, row, column, amount),
2746
+ update({ dt, dryingSeconds }) {
2747
+ drops.update(dt);
2748
+ updateDrip(film.drip, film.bead, layout, random, drops.launch, dt);
2749
+ updateSources(film.sources, context, dt);
2750
+ updateDrags(film.drags, context, layout, drops.launch, dt);
2751
+ simulator.advance({ dt, dryingSeconds });
2752
+ },
2753
+ setWeather(weather) {
2754
+ weatherState = { rain: Math.min(1, Math.max(0, weather.rain ?? weatherState.rain)) };
2755
+ },
2756
+ weather: () => ({ ...weatherState }),
2757
+ clear() {
2758
+ field.thickness.fill(0);
2759
+ field.dryness.fill(0);
2760
+ field.dampness.fill(0);
2761
+ drops.clear();
2762
+ film.drip = void 0;
2763
+ film.bead.visible = false;
2764
+ film.sources.length = 0;
2765
+ film.drags.length = 0;
2766
+ simulator.refreshBounds();
2767
+ },
2768
+ wetCells: () => wetCellCount(field)
2769
+ };
2770
+ return film;
2771
+ }
2772
+
2773
+ // src/film/random.ts
2774
+ function createFilmLcg(seed) {
2775
+ const lcg = (() => {
2776
+ lcg.seed = lcg.seed * 1664525 + 1013904223 >>> 0;
2777
+ return lcg.seed / 4294967296;
2778
+ });
2779
+ lcg.seed = seed >>> 0;
2780
+ return lcg;
2781
+ }
2782
+
2783
+ // src/film/noise.ts
2784
+ function hashCell(x, y) {
2785
+ let value = x * 374761393 + y * 668265263 | 0;
2786
+ value = (value ^ value >>> 13) * 1274126177 | 0;
2787
+ return ((value ^ value >>> 16) >>> 0) / 4294967296;
2788
+ }
2789
+ function valueNoise(x, y) {
2790
+ const xi = Math.floor(x);
2791
+ const yi = Math.floor(y);
2792
+ const xf = x - xi;
2793
+ const yf = y - yi;
2794
+ const u = xf * xf * (3 - 2 * xf);
2795
+ const v = yf * yf * (3 - 2 * yf);
2796
+ const a = hashCell(xi, yi);
2797
+ const b = hashCell(xi + 1, yi);
2798
+ const c = hashCell(xi, yi + 1);
2799
+ const d = hashCell(xi + 1, yi + 1);
2800
+ return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
2801
+ }
2802
+
2803
+ // src/film/layout.ts
2804
+ var REGION_NAMES = ["wall", "sidewalk", "curb", "road"];
2805
+ var dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
2806
+ function impactOf(region, position, velocity, cellSize) {
2807
+ const local = [position[0] - region.origin[0], position[1] - region.origin[1], position[2] - region.origin[2]];
2808
+ return {
2809
+ row: region.firstRow + dot3(local, region.rowAxis) / cellSize,
2810
+ column: dot3(local, region.columnAxis) / cellSize,
2811
+ along: [dot3(velocity, region.columnAxis), dot3(velocity, region.rowAxis)],
2812
+ normalSpeed: -dot3(velocity, region.normal)
2813
+ };
2814
+ }
2815
+ function cellToWorldOf(regions, cellSize, row, column) {
2816
+ let region = regions[regions.length - 1];
2817
+ for (const candidate of regions) {
2818
+ if (row < candidate.firstRow + candidate.rowCount) {
2819
+ region = candidate;
2820
+ break;
2821
+ }
2822
+ }
2823
+ const across = column * cellSize;
2824
+ const down = (row - region.firstRow) * cellSize;
2825
+ return [
2826
+ region.origin[0] + region.columnAxis[0] * across + region.rowAxis[0] * down,
2827
+ region.origin[1] + region.columnAxis[1] * across + region.rowAxis[1] * down,
2828
+ region.origin[2] + region.columnAxis[2] * across + region.rowAxis[2] * down
2829
+ ];
2830
+ }
2831
+ function createFoldedLayout(params) {
2832
+ const { columns, cellSize, originX, wallRows, sidewalkRows, curbRows, roadRows, wallTopY, sidewalkY, roadY, wallZ, rules } = params;
2833
+ const curbStep = params.curbStep ?? (sidewalkY - roadY) / curbRows;
2834
+ const height = wallRows + sidewalkRows + curbRows + roadRows;
2835
+ const cellCount = columns * height;
2836
+ const bed = new Float32Array(cellCount);
2837
+ const pin = new Float32Array(cellCount);
2838
+ const vertical = new Float32Array(cellCount);
2839
+ const sidewalkEnd = wallRows + sidewalkRows;
2840
+ const curbEnd = sidewalkEnd + curbRows;
2841
+ const regionOfRow = (row) => row < wallRows ? 0 : row < sidewalkEnd ? 1 : row < curbEnd ? 2 : 3;
2842
+ for (let row = 0; row < height; row++) {
2843
+ for (let column = 0; column < columns; column++) {
2844
+ const index = row * columns + column;
2845
+ const region = regionOfRow(row);
2846
+ const x = originX + (column + 0.5) * cellSize;
2847
+ let bedHeight;
2848
+ let roughness = 0.2;
2849
+ if (region === 0) {
2850
+ bedHeight = (wallTopY - (row + 0.5) * cellSize) * 1e3;
2851
+ roughness = 0.3;
2852
+ } else if (region === 1) {
2853
+ const z = (row - wallRows + 0.5) * cellSize;
2854
+ bedHeight = sidewalkY * 1e3 - z * 6;
2855
+ const jointX = Math.abs(((x - originX) % 0.5 + 0.5) % 0.5 - 0.25);
2856
+ const jointZ = Math.abs((z % 0.5 + 0.5) % 0.5 - 0.25);
2857
+ if (jointX > 0.25 - cellSize * 0.6 || jointZ > 0.25 - cellSize * 0.6) bedHeight -= 0.9;
2858
+ roughness = 0.18;
2859
+ } else if (region === 2) {
2860
+ bedHeight = sidewalkY * 1e3 - (row - sidewalkEnd + 0.5) * curbStep * 1e3;
2861
+ } else {
2862
+ const z = (row - curbEnd + 0.5) * cellSize;
2863
+ bedHeight = roadY * 1e3 + z * 14 - (x - params.roadSlopeAnchorX) * 5;
2864
+ roughness = 0.55;
2865
+ }
2866
+ const noise = valueNoise(column * 0.35, row * 0.35) * 0.6 + valueNoise(column * 1.3, row * 1.3) * 0.4;
2867
+ bed[index] = bedHeight + (noise - 0.5) * roughness * 2;
2868
+ vertical[index] = region === 0 || region === 2 ? 0.95 : 1;
2869
+ pin[index] = 0.62 + 0.76 * (valueNoise(column * 0.21 + 40, row * 0.21) * 0.5 + hashCell(column, row) * 0.5) * (region === 3 ? 1.25 : 1);
2870
+ }
2871
+ }
2872
+ const curbZ = wallZ + sidewalkRows * cellSize;
2873
+ const roadEndZ = curbZ + roadRows * cellSize;
2874
+ const right = [1, 0, 0];
2875
+ const regions = [
2876
+ { id: REGION_NAMES[0], firstRow: 0, rowCount: wallRows, origin: [originX, wallTopY, wallZ], columnAxis: right, rowAxis: [0, -1, 0], normal: [0, 0, 1], isVertical: true },
2877
+ { id: REGION_NAMES[1], firstRow: wallRows, rowCount: sidewalkRows, origin: [originX, sidewalkY, wallZ], columnAxis: right, rowAxis: [0, 0, 1], normal: [0, 1, 0], isVertical: false },
2878
+ { id: REGION_NAMES[2], firstRow: sidewalkEnd, rowCount: curbRows, origin: [originX, sidewalkY, curbZ], columnAxis: right, rowAxis: [0, -1, 0], normal: [0, 0, 1], isVertical: true },
2879
+ { id: REGION_NAMES[3], firstRow: curbEnd, rowCount: roadRows, origin: [originX, roadY, curbZ], columnAxis: right, rowAxis: [0, 0, 1], normal: [0, 1, 0], isVertical: false }
2880
+ ];
2881
+ const maxX = originX + columns * cellSize;
2882
+ return {
2883
+ id: params.id,
2884
+ width: columns,
2885
+ height,
2886
+ cellSize,
2887
+ bed,
2888
+ pin,
2889
+ vertical,
2890
+ regions,
2891
+ rules,
2892
+ cellToWorld: (row, column) => cellToWorldOf(regions, cellSize, row, column),
2893
+ collide(previous, position) {
2894
+ if (position[2] <= wallZ && position[1] > sidewalkY) {
2895
+ position[2] = wallZ;
2896
+ return 0;
2897
+ }
2898
+ if (position[2] < curbZ && (!rules.guardBehindWall || position[2] >= wallZ) && position[1] <= sidewalkY) {
2899
+ position[1] = sidewalkY;
2900
+ return 1;
2901
+ }
2902
+ if (rules.curbCatch && position[2] >= curbZ && previous[2] < curbZ && position[1] < sidewalkY) {
2903
+ position[2] = curbZ;
2904
+ return 2;
2905
+ }
2906
+ if (position[2] >= curbZ && position[1] <= roadY) {
2907
+ position[1] = roadY;
2908
+ return 3;
2909
+ }
2910
+ return -1;
2911
+ },
2912
+ impact: (region, position, velocity) => impactOf(regions[region], position, velocity, cellSize),
2913
+ contains: (position) => position[0] > originX && position[0] < maxX && (!rules.clipRoadEnd || position[2] < roadEndZ),
2914
+ escaped(position) {
2915
+ if (position[1] < -1) return true;
2916
+ const box = rules.escapeBox;
2917
+ return box !== void 0 && (position[0] < box.minX || position[0] > box.maxX || position[2] > box.maxZ);
2918
+ }
2919
+ };
2920
+ }
2921
+ var PADRAO_CELL = 0.0125;
2922
+ var PADRAO_WALL_Z = 0;
2923
+ var PADRAO_LAYOUT = createFoldedLayout({
2924
+ id: "padrao",
2925
+ columns: 256,
2926
+ cellSize: PADRAO_CELL,
2927
+ originX: -1.6,
2928
+ wallRows: 144,
2929
+ sidewalkRows: 128,
2930
+ curbRows: 12,
2931
+ roadRows: 112,
2932
+ wallTopY: 1.95,
2933
+ sidewalkY: 0.15,
2934
+ roadY: 0,
2935
+ wallZ: PADRAO_WALL_Z,
2936
+ curbStep: PADRAO_CELL,
2937
+ roadSlopeAnchorX: 0,
2938
+ rules: {
2939
+ dropCapacity: 600,
2940
+ curbCatch: true,
2941
+ guardBehindWall: true,
2942
+ clipRoadEnd: true,
2943
+ escapeBox: { minX: -3, maxX: 3, maxZ: PADRAO_WALL_Z + 128 * PADRAO_CELL + 112 * PADRAO_CELL + 1 },
2944
+ dripStyle: "padrao"
2945
+ }
2946
+ });
2947
+ var WORKSHOP_CELL = 0.0125;
2948
+ var WORKSHOP_LAYOUT = createFoldedLayout({
2949
+ id: "workshop",
2950
+ columns: 160,
2951
+ cellSize: WORKSHOP_CELL,
2952
+ originX: -2.2,
2953
+ wallRows: 160,
2954
+ sidewalkRows: 128,
2955
+ curbRows: 9,
2956
+ roadRows: 151,
2957
+ wallTopY: 2.12,
2958
+ sidewalkY: 0.1235,
2959
+ roadY: 9e-3,
2960
+ wallZ: -4.497,
2961
+ roadSlopeAnchorX: -1.2,
2962
+ rules: { dropCapacity: 400, curbCatch: false, guardBehindWall: false, clipRoadEnd: false, dripStyle: "workshop" }
2963
+ });
2964
+
2965
+ // src/film/surface-layout.ts
2966
+ var DEFAULT_COLUMNS = 160;
2967
+ var DEFAULT_ROWS = 160;
2968
+ var DEFAULT_CELL = 0.0125;
2969
+ var DEFAULT_WALL_ROWS = 48;
2970
+ var DEFAULT_CURB_ROWS = 12;
2971
+ var RULES = { dropCapacity: 160, curbCatch: false, guardBehindWall: false, clipRoadEnd: false, dripStyle: "padrao" };
2972
+ var along3 = (origin, axis, distance2) => add(origin, scale(axis, distance2));
2973
+ var reject = (vector, axis) => sub(vector, scale(axis, dot(vector, axis)));
2974
+ function layoutFromSurfaces(options) {
2975
+ const columns = options.columns ?? DEFAULT_COLUMNS;
2976
+ const totalRows = options.rows ?? DEFAULT_ROWS;
2977
+ const cell = options.cellSize ?? DEFAULT_CELL;
2978
+ const floorBasis = makeBasis(options.floor.normal, options.floor.tangent);
2979
+ const floorNormal = floorBasis.normal;
2980
+ const projected = sub(options.center, scale(floorNormal, dot(sub(options.center, options.floor.point), floorNormal)));
2981
+ const wallNormal = options.wall ? normalize(reject(options.wall.normal, floorNormal), floorBasis.t2) : void 0;
2982
+ const awayAxis = wallNormal ?? floorBasis.t2;
2983
+ const columnAxis = wallNormal ? normalize(cross(floorNormal, wallNormal), floorBasis.t1) : floorBasis.t1;
2984
+ const wallRows = options.wall ? Math.min(totalRows - 1, options.wallRows ?? DEFAULT_WALL_ROWS) : 0;
2985
+ let fold = projected;
2986
+ if (options.wall && wallNormal) {
2987
+ const toWall = dot(sub(projected, options.wall.point), wallNormal) / (dot(awayAxis, wallNormal) || 1);
2988
+ fold = sub(projected, scale(awayAxis, toWall));
2989
+ } else {
2990
+ fold = along3(projected, awayAxis, -(totalRows * cell) / 2);
2991
+ }
2992
+ const lateral = dot(sub(projected, fold), columnAxis) - columns * cell / 2;
2993
+ const base = along3(fold, columnAxis, lateral);
2994
+ const strips = [];
2995
+ const roughness = 0.18 + 0.4 * options.floor.porosity;
2996
+ if (options.wall && wallNormal) {
2997
+ strips.push({ id: "wall", rows: wallRows, origin: along3(base, floorNormal, wallRows * cell), rowAxis: scale(floorNormal, -1), normal: wallNormal, isVertical: true, roughness: 0.3 });
2998
+ }
2999
+ const lowerRows = totalRows - wallRows;
3000
+ const dropMetres = options.curb ? Math.max(0, dot(sub(options.floor.point, options.curb.point), floorNormal)) : 0;
3001
+ const curbRows = options.curb ? Math.min(options.curbRows ?? DEFAULT_CURB_ROWS, Math.max(1, Math.ceil(dropMetres / cell)), lowerRows - 2) : 0;
3002
+ const floorRows = options.curb ? Math.max(1, Math.floor((lowerRows - curbRows) / 2)) : lowerRows;
3003
+ strips.push({ id: "floor", rows: floorRows, origin: base, rowAxis: awayAxis, normal: floorNormal, isVertical: false, roughness });
3004
+ if (options.curb) {
3005
+ const edge = along3(base, awayAxis, floorRows * cell);
3006
+ strips.push({ id: "curb", rows: curbRows, origin: edge, rowAxis: scale(floorNormal, -1), normal: awayAxis, isVertical: true, roughness: 0.2 });
3007
+ const lowerOrigin = along3(edge, floorNormal, -curbRows * cell);
3008
+ strips.push({ id: "low", rows: lowerRows - floorRows - curbRows, origin: lowerOrigin, rowAxis: awayAxis, normal: floorNormal, isVertical: false, roughness: 0.55 });
3009
+ }
3010
+ const regions = [];
3011
+ let firstRow = 0;
3012
+ for (const strip of strips) {
3013
+ regions.push({ id: strip.id, firstRow, rowCount: strip.rows, origin: strip.origin, columnAxis, rowAxis: strip.rowAxis, normal: strip.normal, isVertical: strip.isVertical });
3014
+ firstRow += strip.rows;
3015
+ }
3016
+ const height = firstRow;
3017
+ const bed = new Float32Array(columns * height);
3018
+ const pin = new Float32Array(columns * height);
3019
+ const vertical = new Float32Array(columns * height);
3020
+ regions.forEach((region, regionIndex) => {
3021
+ const strip = strips[regionIndex];
3022
+ for (let row = 0; row < region.rowCount; row++) {
3023
+ for (let column = 0; column < columns; column++) {
3024
+ const index = (region.firstRow + row) * columns + column;
3025
+ const centre = add(add(region.origin, scale(columnAxis, (column + 0.5) * cell)), scale(region.rowAxis, (row + 0.5) * cell));
3026
+ const noise = valueNoise(column * 0.35, (region.firstRow + row) * 0.35) * 0.6 + valueNoise(column * 1.3, (region.firstRow + row) * 1.3) * 0.4;
3027
+ bed[index] = centre[1] * 1e3 + (noise - 0.5) * strip.roughness * 2;
3028
+ vertical[index] = region.isVertical ? 0.95 : 1;
3029
+ pin[index] = 0.62 + 0.76 * (valueNoise(column * 0.21 + 40, (region.firstRow + row) * 0.21) * 0.5 + valueNoise(column * 0.9, row * 0.9) * 0.5);
3030
+ }
3031
+ }
3032
+ });
3033
+ const lowestY = Math.min(...regions.map((region) => region.origin[1] - (region.rowAxis[1] > 0 ? 0 : region.rowCount * cell * Math.abs(region.rowAxis[1])))) - 1;
3034
+ const width = columns * cell;
3035
+ const cellToWorld = (row, column) => {
3036
+ const region = regions.find((candidate) => row < candidate.firstRow + candidate.rowCount) ?? regions[regions.length - 1];
3037
+ return add(add(region.origin, scale(region.columnAxis, column * cell)), scale(region.rowAxis, (row - region.firstRow) * cell));
3038
+ };
3039
+ return {
3040
+ id: "surfaces",
3041
+ width: columns,
3042
+ height,
3043
+ cellSize: cell,
3044
+ bed,
3045
+ pin,
3046
+ vertical,
3047
+ regions,
3048
+ rules: RULES,
3049
+ cellToWorld,
3050
+ collide(previous, position) {
3051
+ for (let index = 0; index < regions.length; index++) {
3052
+ const region = regions[index];
3053
+ const before = dot(sub(previous, region.origin), region.normal);
3054
+ const after = dot(sub(position, region.origin), region.normal);
3055
+ if (before < 0 || after > 0) continue;
3056
+ const local = sub(position, region.origin);
3057
+ const sideways = dot(local, region.columnAxis);
3058
+ const downward = dot(local, region.rowAxis);
3059
+ if (sideways < 0 || sideways >= width || downward < 0 || downward >= region.rowCount * cell) continue;
3060
+ position[0] -= region.normal[0] * after;
3061
+ position[1] -= region.normal[1] * after;
3062
+ position[2] -= region.normal[2] * after;
3063
+ return index;
3064
+ }
3065
+ return -1;
3066
+ },
3067
+ impact(regionIndex, position, velocity) {
3068
+ const region = regions[regionIndex];
3069
+ const local = sub(position, region.origin);
3070
+ return {
3071
+ row: region.firstRow + dot(local, region.rowAxis) / cell,
3072
+ column: dot(local, region.columnAxis) / cell,
3073
+ along: [dot(velocity, region.columnAxis), dot(velocity, region.rowAxis)],
3074
+ normalSpeed: -dot(velocity, region.normal)
3075
+ };
3076
+ },
3077
+ contains: () => true,
3078
+ escaped: (position) => position[1] < lowestY
3079
+ };
3080
+ }
3081
+
3082
+ // src/data/film-profiles.json
3083
+ var film_profiles_default = [
3084
+ {
3085
+ id: "blood",
3086
+ look: "blood",
3087
+ flow: 0.05,
3088
+ pin: 0.33,
3089
+ front: 0.58,
3090
+ referenceThickness: 0.6,
3091
+ slopeCap: 1.1,
3092
+ dryTime: 600,
3093
+ evaporation: 0,
3094
+ opacityRate: 3.2,
3095
+ reflectance: 0.045,
3096
+ normalScale: 16,
3097
+ thin: 7997966,
3098
+ thick: 2753028,
3099
+ dry: 3874068,
3100
+ dryThin: 6960678,
3101
+ dropColor: 7997965,
3102
+ dropOpacity: 1,
3103
+ volume: 1,
3104
+ spikes: 1,
3105
+ landsAsBlob: false,
3106
+ maxAmount: 2.6,
3107
+ sources: {
3108
+ padrao: "F46",
3109
+ oficina: "W583"
3110
+ }
3111
+ },
3112
+ {
3113
+ id: "old_blood",
3114
+ look: "blood",
3115
+ flow: 0.032,
3116
+ pin: 0.42,
3117
+ front: 0.7,
3118
+ referenceThickness: 0.7,
3119
+ slopeCap: 1,
3120
+ dryTime: 360,
3121
+ evaporation: 0,
3122
+ opacityRate: 3.8,
3123
+ reflectance: 0.03,
3124
+ normalScale: 14,
3125
+ thin: 7085078,
3126
+ thick: 2888717,
3127
+ dry: 2758158,
3128
+ dryThin: 4860444,
3129
+ dropColor: 4854286,
3130
+ dropOpacity: 1,
3131
+ volume: 0.6,
3132
+ spikes: 1,
3133
+ landsAsBlob: false,
3134
+ maxAmount: 2.6,
3135
+ sources: {
3136
+ padrao: "F48"
3137
+ }
3138
+ },
3139
+ {
3140
+ id: "bile",
3141
+ look: "bile",
3142
+ flow: 0.02,
3143
+ pin: 0.8,
3144
+ front: 1.2,
3145
+ referenceThickness: 1.2,
3146
+ slopeCap: 0.9,
3147
+ dryTime: 900,
3148
+ evaporation: 0,
3149
+ opacityRate: 7,
3150
+ reflectance: 0.04,
3151
+ normalScale: 12,
3152
+ thin: 11121724,
3153
+ thick: 7174171,
3154
+ dry: 4080160,
3155
+ dryThin: 5659180,
3156
+ dropColor: 9083434,
3157
+ dropOpacity: 1,
3158
+ volume: 1.3,
3159
+ spikes: 0.3,
3160
+ landsAsBlob: false,
3161
+ maxAmount: 4,
3162
+ sources: {
3163
+ padrao: "F50",
3164
+ oficina: "W584 (spikes 0.3 W578, cap 4 W691)"
3165
+ }
3166
+ },
3167
+ {
3168
+ id: "motor_oil",
3169
+ look: "oil",
3170
+ flow: 0.03,
3171
+ pin: 0.12,
3172
+ front: 0.22,
3173
+ referenceThickness: 0.5,
3174
+ slopeCap: 1,
3175
+ dryTime: 0,
3176
+ evaporation: 0,
3177
+ opacityRate: 4.5,
3178
+ reflectance: 0.06,
3179
+ normalScale: 18,
3180
+ thin: 1381135,
3181
+ thick: 263173,
3182
+ dry: 263173,
3183
+ dryThin: 1381135,
3184
+ dropColor: 657932,
3185
+ dropOpacity: 1,
3186
+ volume: 1,
3187
+ spikes: 1,
3188
+ landsAsBlob: true,
3189
+ maxAmount: 2.6,
3190
+ sources: {
3191
+ padrao: "F52",
3192
+ oficina: "W585 (blob W691)"
3193
+ }
3194
+ },
3195
+ {
3196
+ id: "fuel",
3197
+ look: "fuel",
3198
+ flow: 0.2,
3199
+ pin: 0.035,
3200
+ front: 0.07,
3201
+ referenceThickness: 0.2,
3202
+ slopeCap: 1.2,
3203
+ dryTime: 0,
3204
+ evaporation: 0.02,
3205
+ opacityRate: 0.25,
3206
+ reflectance: 0.04,
3207
+ normalScale: 0,
3208
+ thin: 10129520,
3209
+ thick: 7234630,
3210
+ dry: 0,
3211
+ dryThin: 0,
3212
+ dropColor: 12103824,
3213
+ dropOpacity: 0.45,
3214
+ volume: 1.4,
3215
+ spikes: 1,
3216
+ landsAsBlob: true,
3217
+ maxAmount: 2.6,
3218
+ sources: {
3219
+ padrao: "F54",
3220
+ oficina: "W586 (blob W691)"
3221
+ }
3222
+ }
3223
+ ];
3224
+
3225
+ // src/film/profiles.ts
3226
+ var FILM_PROFILES = film_profiles_default;
3227
+ function filmProfileOf(id, overrides = {}) {
3228
+ const profile = FILM_PROFILES.find((entry) => entry.id === id);
3229
+ return profile ? { ...profile, ...overrides } : void 0;
3230
+ }
2205
3231
  return __toCommonJS(index_exports);
2206
3232
  })();
2207
3233
  //# sourceMappingURL=index.global.js.map