@plasius/gpu-shared 0.1.3 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -19,6 +19,43 @@ var __copyProps = (to, from, except, desc) => {
19
19
  };
20
20
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
21
 
22
+ // src/asset-url.js
23
+ function createInlineShowcaseAssetUrl() {
24
+ return new URL(INLINE_BRIGANTINE_GLTF_URL);
25
+ }
26
+ function getBrowserBaseUrl() {
27
+ if (typeof document !== "undefined" && typeof document.baseURI === "string" && document.baseURI.length > 0) {
28
+ return document.baseURI;
29
+ }
30
+ if (typeof window !== "undefined" && typeof window.location?.href === "string" && window.location.href.length > 0) {
31
+ return window.location.href;
32
+ }
33
+ return null;
34
+ }
35
+ function resolveShowcaseAssetUrl(baseUrl2 = import_meta.url) {
36
+ try {
37
+ return new URL("../assets/brigantine.gltf", baseUrl2);
38
+ } catch {
39
+ const browserBaseUrl = getBrowserBaseUrl();
40
+ if (browserBaseUrl) {
41
+ try {
42
+ const normalizedBaseUrl = new URL(baseUrl2, browserBaseUrl);
43
+ return new URL("../assets/brigantine.gltf", normalizedBaseUrl);
44
+ } catch {
45
+ return createInlineShowcaseAssetUrl();
46
+ }
47
+ }
48
+ return createInlineShowcaseAssetUrl();
49
+ }
50
+ }
51
+ var import_meta, INLINE_BRIGANTINE_GLTF_URL;
52
+ var init_asset_url = __esm({
53
+ "src/asset-url.js"() {
54
+ import_meta = {};
55
+ INLINE_BRIGANTINE_GLTF_URL = "data:application/json;base64,ewogICJhc3NldCI6IHsKICAgICJ2ZXJzaW9uIjogIjIuMCIsCiAgICAiZ2VuZXJhdG9yIjogIlBsYXNpdXMgZGVtbyBhc3NldCBnZW5lcmF0b3IiCiAgfSwKICAic2NlbmUiOiAwLAogICJzY2VuZXMiOiBbCiAgICB7CiAgICAgICJub2RlcyI6IFswXQogICAgfQogIF0sCiAgIm5vZGVzIjogWwogICAgewogICAgICAibWVzaCI6IDAsCiAgICAgICJuYW1lIjogImJyaWdhbnRpbmUiLAogICAgICAiZXh0cmFzIjogewogICAgICAgICJwaHlzaWNzIjogewogICAgICAgICAgInNoYXBlIjogImJveCIsCiAgICAgICAgICAiaGFsZkV4dGVudHMiOiBbMS4zNSwgMC45NSwgMy45XSwKICAgICAgICAgICJtYXNzIjogMzIwMCwKICAgICAgICAgICJyZXN0aXR1dGlvbiI6IDAuMjIsCiAgICAgICAgICAibGluZWFyRGFtcGluZyI6IDAuMDQsCiAgICAgICAgICAiYW5ndWxhckRhbXBpbmciOiAwLjA4LAogICAgICAgICAgIndhdGVybGluZSI6IDAuNDIKICAgICAgICB9CiAgICAgIH0KICAgIH0KICBdLAogICJtZXNoZXMiOiBbCiAgICB7CiAgICAgICJuYW1lIjogImJyaWdhbnRpbmUtaHVsbCIsCiAgICAgICJwcmltaXRpdmVzIjogWwogICAgICAgIHsKICAgICAgICAgICJhdHRyaWJ1dGVzIjogewogICAgICAgICAgICAiUE9TSVRJT04iOiAwCiAgICAgICAgICB9LAogICAgICAgICAgImluZGljZXMiOiAxLAogICAgICAgICAgIm1hdGVyaWFsIjogMAogICAgICAgIH0KICAgICAgXQogICAgfQogIF0sCiAgIm1hdGVyaWFscyI6IFsKICAgIHsKICAgICAgIm5hbWUiOiAicGFpbnRlZC1odWxsIiwKICAgICAgInBick1ldGFsbGljUm91Z2huZXNzIjogewogICAgICAgICJiYXNlQ29sb3JGYWN0b3IiOiBbMC41NiwgMC4zMywgMC4yMiwgMV0sCiAgICAgICAgIm1ldGFsbGljRmFjdG9yIjogMC4wOCwKICAgICAgICAicm91Z2huZXNzRmFjdG9yIjogMC45MgogICAgICB9CiAgICB9CiAgXSwKICAiYnVmZmVycyI6IFsKICAgIHsKICAgICAgInVyaSI6ICJkYXRhOmFwcGxpY2F0aW9uL29jdGV0LXN0cmVhbTtiYXNlNjQsbXBtWnZ3QUFBTC9OekV6QW1wbVpQd0FBQUwvTnpFekF6Y3lzdnpNenM3NHpNN08vemN5c1B6TXpzNzR6TTdPL0FBQ2d2ODNNVEwzTnpNdy9BQUNnUDgzTVRMM056TXcvQUFBQUFPeFJPTDR6TTROQUFBQUFBR1ptNWo0QUFIQkFNek56dnpNenN6NmFtUm5BTXpOelB6TXpzejZhbVJuQXpjeE12ejBLMXo3TnpFdy96Y3hNUHowSzF6N056RXcvQUFBQUFETXpjejltWm1hL0FBQUNBQU1BQUFBREFBRUFBZ0FFQUFVQUFnQUZBQU1BQkFBSEFBVUFCQUFHQUFjQUJRQUhBQVlBQUFBQkFBa0FBQUFKQUFnQUNBQUpBQXdBQWdBSUFBd0FBd0FNQUFrQUFnQU1BQW9BQXdBTEFBd0FBZ0FLQUFRQUF3QUZBQXNBQ2dBTUFBc0FBQUFJQUFJQUFRQURBQWtBQkFBS0FBWUFCUUFHQUFzQUFnQUtBQXNBQWdBTEFBTUEiLAogICAgICAiYnl0ZUxlbmd0aCI6IDI5NAogICAgfQogIF0sCiAgImJ1ZmZlclZpZXdzIjogWwogICAgewogICAgICAiYnVmZmVyIjogMCwKICAgICAgImJ5dGVPZmZzZXQiOiAwLAogICAgICAiYnl0ZUxlbmd0aCI6IDE1NiwKICAgICAgInRhcmdldCI6IDM0OTYyCiAgICB9LAogICAgewogICAgICAiYnVmZmVyIjogMCwKICAgICAgImJ5dGVPZmZzZXQiOiAxNTYsCiAgICAgICJieXRlTGVuZ3RoIjogMTM4LAogICAgICAidGFyZ2V0IjogMzQ5NjMKICAgIH0KICBdLAogICJhY2Nlc3NvcnMiOiBbCiAgICB7CiAgICAgICJidWZmZXJWaWV3IjogMCwKICAgICAgImJ5dGVPZmZzZXQiOiAwLAogICAgICAiY29tcG9uZW50VHlwZSI6IDUxMjYsCiAgICAgICJjb3VudCI6IDEzLAogICAgICAidHlwZSI6ICJWRUMzIiwKICAgICAgIm1pbiI6IFstMS4zNSwgLTAuNSwgLTMuMl0sCiAgICAgICJtYXgiOiBbMS4zNSwgMC45NSwgNC4xXQogICAgfSwKICAgIHsKICAgICAgImJ1ZmZlclZpZXciOiAxLAogICAgICAiYnl0ZU9mZnNldCI6IDAsCiAgICAgICJjb21wb25lbnRUeXBlIjogNTEyMywKICAgICAgImNvdW50IjogNjksCiAgICAgICJ0eXBlIjogIlNDQUxBUiIsCiAgICAgICJtYXgiOiBbMTJdLAogICAgICAibWluIjogWzBdCiAgICB9CiAgXQp9Cg==";
56
+ }
57
+ });
58
+
22
59
  // src/gltf-loader.js
23
60
  var gltf_loader_exports = {};
24
61
  __export(gltf_loader_exports, {
@@ -94,12 +131,39 @@ function computeBounds(positions) {
94
131
  }
95
132
  return { min, max };
96
133
  }
134
+ function resolveBrowserRequestBaseUrl() {
135
+ if (typeof document !== "undefined" && typeof document.baseURI === "string" && document.baseURI.length > 0) {
136
+ return document.baseURI;
137
+ }
138
+ if (typeof window !== "undefined" && typeof window.location?.href === "string" && window.location.href.length > 0) {
139
+ return window.location.href;
140
+ }
141
+ return null;
142
+ }
143
+ function resolveFetchBaseUrl(requestUrl, responseUrl) {
144
+ if (typeof responseUrl === "string" && responseUrl.length > 0) {
145
+ try {
146
+ return new URL(responseUrl);
147
+ } catch {
148
+ }
149
+ }
150
+ try {
151
+ return new URL(requestUrl);
152
+ } catch {
153
+ const browserBaseUrl = resolveBrowserRequestBaseUrl();
154
+ if (browserBaseUrl) {
155
+ return new URL(requestUrl, browserBaseUrl);
156
+ }
157
+ throw new Error(`Unable to resolve a stable base URL for glTF asset loading: ${String(requestUrl)}`);
158
+ }
159
+ }
97
160
  async function loadGltfModel(url) {
98
161
  const response = await fetch(url);
99
162
  if (!response.ok) {
100
163
  throw new Error(`Failed to load glTF asset: ${response.status} ${response.statusText}`);
101
164
  }
102
165
  const document2 = await response.json();
166
+ const baseUrl2 = resolveFetchBaseUrl(url, response.url);
103
167
  const buffers = await Promise.all(
104
168
  (document2.buffers ?? []).map(async (buffer) => {
105
169
  if (typeof buffer.uri !== "string") {
@@ -108,7 +172,7 @@ async function loadGltfModel(url) {
108
172
  if (buffer.uri.startsWith("data:")) {
109
173
  return decodeDataUri(buffer.uri);
110
174
  }
111
- const nested = await fetch(new URL(buffer.uri, url));
175
+ const nested = await fetch(new URL(buffer.uri, baseUrl2));
112
176
  if (!nested.ok) {
113
177
  throw new Error(`Failed to load glTF buffer: ${nested.status} ${nested.statusText}`);
114
178
  }
@@ -2452,10 +2516,10 @@ function getLightingProfile(name = defaultLightingProfile) {
2452
2516
  }
2453
2517
  return profile;
2454
2518
  }
2455
- var import_meta, __require, baseUrl, techniqueSpecs, lightingTechniques, lightingTechniqueNames, defaultLightingTechnique, profileSpecs, lightingProfiles, lightingProfileNames, defaultLightingProfile, lightingDistanceBands, lightingWorkerQueueClass, lightingDebugOwner, lightingImportanceLevels, lightingBandPolicySpecs, lightingWorkerSpecPresets, lightingWorkerDagSpecs, lightingWorkerManifests, defaultTechnique, lightingPreludeWgslUrl, lightingJobLabels, lightingJobs;
2519
+ var import_meta2, __require, baseUrl, techniqueSpecs, lightingTechniques, lightingTechniqueNames, defaultLightingTechnique, profileSpecs, lightingProfiles, lightingProfileNames, defaultLightingProfile, lightingDistanceBands, lightingWorkerQueueClass, lightingDebugOwner, lightingImportanceLevels, lightingBandPolicySpecs, lightingWorkerSpecPresets, lightingWorkerDagSpecs, lightingWorkerManifests, defaultTechnique, lightingPreludeWgslUrl, lightingJobLabels, lightingJobs;
2456
2520
  var init_dist3 = __esm({
2457
2521
  "node_modules/@plasius/gpu-lighting/dist/index.js"() {
2458
- import_meta = {};
2522
+ import_meta2 = {};
2459
2523
  __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2460
2524
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
2461
2525
  }) : x)(function(x) {
@@ -2463,8 +2527,8 @@ var init_dist3 = __esm({
2463
2527
  throw Error('Dynamic require of "' + x + '" is not supported');
2464
2528
  });
2465
2529
  baseUrl = (() => {
2466
- if (typeof import_meta.url !== "undefined") {
2467
- return new URL("./index.js", import_meta.url);
2530
+ if (typeof import_meta2.url !== "undefined") {
2531
+ return new URL("./index.js", import_meta2.url);
2468
2532
  }
2469
2533
  if (typeof __filename !== "undefined" && typeof __require !== "undefined") {
2470
2534
  const { pathToFileURL } = __require("url");
@@ -5091,7 +5155,7 @@ var init_dist5 = __esm({
5091
5155
  }
5092
5156
  });
5093
5157
 
5094
- // node_modules/@plasius/gpu-physics/dist/chunk-CNTXT5QJ.js
5158
+ // node_modules/@plasius/gpu-physics/dist/chunk-PFUNZLNF.js
5095
5159
  function assertIdentifier5(name, value) {
5096
5160
  if (typeof value !== "string" || value.trim().length === 0) {
5097
5161
  throw new Error(`${name} must be a non-empty string.`);
@@ -5240,7 +5304,7 @@ function getPhysicsWorkerManifest(profile = defaultPhysicsWorkerProfile) {
5240
5304
  return manifest;
5241
5305
  }
5242
5306
  function createPhysicsSimulationPlan(profile = defaultPhysicsWorkerProfile) {
5243
- const plan = physicsSimulationPlanSpecs[profile];
5307
+ const plan = physicsSimulationPlans[profile];
5244
5308
  if (!plan) {
5245
5309
  const available = physicsWorkerProfileNames.join(", ");
5246
5310
  throw new Error(
@@ -5304,9 +5368,9 @@ function createPhysicsWorldSnapshot(input) {
5304
5368
  metadata: normalizeMetadata(input.metadata)
5305
5369
  });
5306
5370
  }
5307
- var physicsDebugOwner, physicsWorkerQueueClasses, defaultPhysicsWorkerProfile, physicsSimulationStageOrder, physicsWorkerProfileSpecs, physicsWorkerDagSpecs, physicsSimulationPlanSpecs, physicsWorkerManifests, physicsWorkerProfileNames;
5308
- var init_chunk_CNTXT5QJ = __esm({
5309
- "node_modules/@plasius/gpu-physics/dist/chunk-CNTXT5QJ.js"() {
5371
+ var physicsDebugOwner, physicsWorkerQueueClasses, defaultPhysicsWorkerProfile, physicsSimulationStageOrder, physicsWorkerProfileSpecs, physicsWorkerDagSpecs, physicsSimulationPlans, physicsWorkerManifests, physicsWorkerProfileNames;
5372
+ var init_chunk_PFUNZLNF = __esm({
5373
+ "node_modules/@plasius/gpu-physics/dist/chunk-PFUNZLNF.js"() {
5310
5374
  physicsDebugOwner = "physics";
5311
5375
  physicsWorkerQueueClasses = Object.freeze({
5312
5376
  simulation: "simulation",
@@ -6052,7 +6116,7 @@ var init_chunk_CNTXT5QJ = __esm({
6052
6116
  contactVisuals: { priority: 1, dependencies: ["worldSnapshot"] }
6053
6117
  }
6054
6118
  };
6055
- physicsSimulationPlanSpecs = Object.freeze({
6119
+ physicsSimulationPlans = Object.freeze({
6056
6120
  gameplay: Object.freeze({
6057
6121
  description: "Gameplay simulation plan that hands off a stable post-commit world snapshot to visual preparation.",
6058
6122
  snapshotStageId: "worldSnapshot",
@@ -6268,7 +6332,7 @@ var init_chunk_CNTXT5QJ = __esm({
6268
6332
  // node_modules/@plasius/gpu-physics/dist/browser.js
6269
6333
  var init_browser = __esm({
6270
6334
  "node_modules/@plasius/gpu-physics/dist/browser.js"() {
6271
- init_chunk_CNTXT5QJ();
6335
+ init_chunk_PFUNZLNF();
6272
6336
  }
6273
6337
  });
6274
6338
 
@@ -6276,12 +6340,8 @@ var init_browser = __esm({
6276
6340
  var showcase_runtime_exports = {};
6277
6341
  __export(showcase_runtime_exports, {
6278
6342
  mountGpuShowcase: () => mountGpuShowcase,
6279
- resolveShowcaseAssetUrl: () => resolveShowcaseAssetUrl,
6280
6343
  showcaseFocusModes: () => showcaseFocusModes
6281
6344
  });
6282
- function resolveShowcaseAssetUrl(baseUrl2 = import_meta2.url) {
6283
- return new URL("../assets/brigantine.gltf", baseUrl2);
6284
- }
6285
6345
  function injectStyles() {
6286
6346
  if (document.getElementById(STYLE_ID)) {
6287
6347
  return;
@@ -6289,27 +6349,27 @@ function injectStyles() {
6289
6349
  const style = document.createElement("style");
6290
6350
  style.id = STYLE_ID;
6291
6351
  style.textContent = `
6292
- :root {
6293
- color-scheme: light;
6294
- --plasius-paper: #f4f7f8;
6295
- --plasius-ink: #152028;
6296
- --plasius-muted: #5c6f7b;
6297
- --plasius-accent: #8f5634;
6298
- --plasius-panel: rgba(255, 255, 255, 0.82);
6299
- --plasius-border: rgba(21, 32, 40, 0.12);
6300
- --plasius-shadow: 0 20px 48px rgba(15, 24, 31, 0.16);
6301
- }
6302
- * {
6303
- box-sizing: border-box;
6304
- }
6305
- body {
6352
+ .${ROOT_CLASS} {
6353
+ color-scheme: dark;
6354
+ --plasius-paper: #081321;
6355
+ --plasius-ink: #edf4ff;
6356
+ --plasius-muted: #b6c5dd;
6357
+ --plasius-accent: #f3b16a;
6358
+ --plasius-panel: rgba(8, 19, 33, 0.72);
6359
+ --plasius-border: rgba(159, 185, 223, 0.18);
6360
+ --plasius-shadow: 0 24px 56px rgba(1, 6, 14, 0.44);
6306
6361
  margin: 0;
6307
- min-height: 100vh;
6362
+ min-height: 100%;
6308
6363
  font-family: "Fraunces", "Iowan Old Style", serif;
6309
6364
  color: var(--plasius-ink);
6310
6365
  background:
6311
- radial-gradient(circle at top left, rgba(255, 247, 238, 0.92), transparent 34%),
6312
- linear-gradient(180deg, #f6f8fb 0%, #d2dee6 48%, #b6c4ce 100%);
6366
+ radial-gradient(circle at 18% 12%, rgba(73, 101, 170, 0.28), transparent 30%),
6367
+ radial-gradient(circle at 82% 18%, rgba(240, 188, 103, 0.08), transparent 18%),
6368
+ linear-gradient(180deg, #04101d 0%, #0b1930 42%, #081321 100%);
6369
+ }
6370
+ .${ROOT_CLASS},
6371
+ .${ROOT_CLASS} * {
6372
+ box-sizing: border-box;
6313
6373
  }
6314
6374
  .plasius-demo {
6315
6375
  width: min(1560px, calc(100vw - 32px));
@@ -6343,7 +6403,7 @@ function injectStyles() {
6343
6403
  text-transform: uppercase;
6344
6404
  letter-spacing: 0.18em;
6345
6405
  font-size: 12px;
6346
- color: rgba(21, 32, 40, 0.56);
6406
+ color: rgba(226, 236, 255, 0.58);
6347
6407
  }
6348
6408
  .plasius-demo h1,
6349
6409
  .plasius-demo h2,
@@ -6361,7 +6421,7 @@ function injectStyles() {
6361
6421
  margin: 0;
6362
6422
  padding: 8px 12px;
6363
6423
  border-radius: 999px;
6364
- background: rgba(143, 86, 52, 0.12);
6424
+ background: rgba(243, 177, 106, 0.14);
6365
6425
  color: var(--plasius-accent);
6366
6426
  font-weight: 700;
6367
6427
  }
@@ -6383,8 +6443,8 @@ function injectStyles() {
6383
6443
  aspect-ratio: 16 / 9;
6384
6444
  display: block;
6385
6445
  border-radius: 20px;
6386
- border: 1px solid rgba(21, 32, 40, 0.08);
6387
- background: linear-gradient(180deg, #dce8ef 0%, #a9bfd0 42%, #0f5168 42%, #092433 100%);
6446
+ border: 1px solid rgba(159, 185, 223, 0.12);
6447
+ background: linear-gradient(180deg, #071220 0%, #132440 42%, #10344b 42%, #05111d 100%);
6388
6448
  }
6389
6449
  .plasius-demo__toolbar {
6390
6450
  position: absolute;
@@ -6404,9 +6464,9 @@ function injectStyles() {
6404
6464
  .plasius-demo button,
6405
6465
  .plasius-demo .plasius-toggle,
6406
6466
  .plasius-demo select {
6407
- border: 1px solid rgba(21, 32, 40, 0.12);
6467
+ border: 1px solid rgba(159, 185, 223, 0.18);
6408
6468
  border-radius: 999px;
6409
- background: rgba(255, 255, 255, 0.84);
6469
+ background: rgba(9, 20, 34, 0.84);
6410
6470
  color: var(--plasius-ink);
6411
6471
  padding: 10px 14px;
6412
6472
  }
@@ -6445,8 +6505,8 @@ function injectStyles() {
6445
6505
  bottom: 24px;
6446
6506
  padding: 10px 14px;
6447
6507
  border-radius: 16px;
6448
- background: rgba(255, 255, 255, 0.84);
6449
- border: 1px solid rgba(21, 32, 40, 0.1);
6508
+ background: rgba(9, 20, 34, 0.82);
6509
+ border: 1px solid rgba(159, 185, 223, 0.16);
6450
6510
  color: var(--plasius-muted);
6451
6511
  font-size: 12px;
6452
6512
  line-height: 1.45;
@@ -6458,7 +6518,7 @@ function injectStyles() {
6458
6518
  }
6459
6519
  .plasius-demo__footer {
6460
6520
  margin-top: 4px;
6461
- color: rgba(21, 32, 40, 0.66);
6521
+ color: rgba(226, 236, 255, 0.68);
6462
6522
  font-size: 13px;
6463
6523
  line-height: 1.6;
6464
6524
  }
@@ -6477,6 +6537,10 @@ function clamp(value, min, max) {
6477
6537
  function mix(a, b, t) {
6478
6538
  return a + (b - a) * t;
6479
6539
  }
6540
+ function pseudoRandom(seed) {
6541
+ const value = Math.sin(seed * 12.9898 + seed * seed * 17e-4) * 43758.5453;
6542
+ return value - Math.floor(value);
6543
+ }
6480
6544
  function vec3(x = 0, y = 0, z = 0) {
6481
6545
  return { x, y, z };
6482
6546
  }
@@ -6510,6 +6574,12 @@ function reflectVec3(vector, normal) {
6510
6574
  const unitNormal = normalizeVec3(normal);
6511
6575
  return subVec3(vector, scaleVec3(unitNormal, 2 * dotVec3(vector, unitNormal)));
6512
6576
  }
6577
+ function directionFromYaw(yaw) {
6578
+ return normalizeVec3(vec3(Math.sin(yaw), 0, Math.cos(yaw)));
6579
+ }
6580
+ function perpendicularOnWater(direction) {
6581
+ return vec3(-direction.z, 0, direction.x);
6582
+ }
6513
6583
  function rotateY(point, angle) {
6514
6584
  const cosine = Math.cos(angle);
6515
6585
  const sine = Math.sin(angle);
@@ -6692,7 +6762,7 @@ function buildDemoDom(root, options) {
6692
6762
  <section class="plasius-panel plasius-demo__status">
6693
6763
  <p id="demoStatus" class="plasius-demo__status-badge">Booting 3D scene\u2026</p>
6694
6764
  <p id="demoDetails" class="plasius-demo__status-text">
6695
- Preparing GLTF assets, cloth and fluid continuity plans, and adaptive quality metadata.
6765
+ Preparing a moonlit harbor scene, GLTF hull data, cloth and fluid continuity plans, and adaptive quality metadata.
6696
6766
  </p>
6697
6767
  </section>
6698
6768
  </section>
@@ -6720,9 +6790,9 @@ function buildDemoDom(root, options) {
6720
6790
  </div>
6721
6791
  <div class="plasius-demo__legend">
6722
6792
  <strong>Scene</strong>
6723
- GLTF ships carry collision metadata.<br />
6724
- Flag cloth and ocean waves scale by distance band.<br />
6725
- Ray-traced shadow and reflection style is preserved near the camera.
6793
+ GLTF ships carry hull mass and damping metadata.<br />
6794
+ Lanterns and torches warm the moonlit harbor.<br />
6795
+ Mass-aware collisions stay authoritative near the camera.
6726
6796
  </div>
6727
6797
  </section>
6728
6798
  <aside class="plasius-demo__sidebar">
@@ -6803,6 +6873,7 @@ function buildSceneSnapshot(state, shipModel) {
6803
6873
  })
6804
6874
  )
6805
6875
  ),
6876
+ shipPhysics: shipModel?.physics ?? null,
6806
6877
  physics: Object.freeze({
6807
6878
  profile: state.physics.profile,
6808
6879
  plan: state.physics.plan,
@@ -6846,28 +6917,39 @@ function readVisualNumber(value, fallback) {
6846
6917
  function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {}) {
6847
6918
  const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
6848
6919
  const defaults = {
6849
- skyTop: premiumShadows ? "#f0f7fb" : "#e8f1f7",
6850
- skyMid: premiumShadows ? "#c7d9e5" : "#b9ceda",
6851
- skyBottom: premiumShadows ? "#84a7bd" : "#7b9bb0",
6852
- seaTop: premiumShadows ? "#235064" : "#264c5f",
6853
- seaMid: premiumShadows ? "#153e53" : "#173d4f",
6854
- seaBottom: "#0b2433",
6855
- sunCore: "rgba(255, 244, 210, 0.9)",
6920
+ skyTop: premiumShadows ? "#040c1a" : "#06101f",
6921
+ skyMid: premiumShadows ? "#11203b" : "#152643",
6922
+ skyBottom: premiumShadows ? "#2f4468" : "#364d73",
6923
+ duskGlow: premiumShadows ? "rgba(116, 142, 201, 0.26)" : "rgba(104, 128, 188, 0.22)",
6924
+ seaTop: premiumShadows ? "#102946" : "#153050",
6925
+ seaMid: premiumShadows ? "#0a1d33" : "#0d2138",
6926
+ seaBottom: "#04101d",
6927
+ moonCore: "rgba(241, 246, 255, 0.98)",
6928
+ moonHalo: "rgba(167, 191, 255, 0.24)",
6929
+ moonReflection: "rgba(192, 214, 255, 0.22)",
6930
+ starColor: "rgba(232, 239, 255, 0.82)",
6931
+ ambientMist: "rgba(41, 63, 97, 0.16)",
6856
6932
  reflectionStrength: lightingSnapshot.currentLevel.config.reflectionStrength,
6857
6933
  shadowAccent: lightingSnapshot.currentLevel.config.shadowStrength,
6858
- waveAmplitude: 1,
6859
- waveDirection: { x: 0.86, z: 0.34 },
6860
- wavePhaseSpeed: 1,
6861
- wakeStrength: 0.24,
6862
- wakeLength: 15,
6863
- collisionRippleStrength: 0.34,
6864
- waterNear: { r: 0.12, g: 0.36, b: 0.46 },
6865
- waterFar: { r: 0.28, g: 0.56, b: 0.68 },
6866
- harborWall: { r: 0.48, g: 0.4, b: 0.32 },
6867
- harborDeck: { r: 0.5, g: 0.34, b: 0.22 },
6868
- harborTower: { r: 0.34, g: 0.32, b: 0.36 },
6869
- flagColor: { r: 0.76, g: 0.24, b: 0.18 },
6870
- flagMotion: 1
6934
+ waveAmplitude: 0.94,
6935
+ waveDirection: { x: 0.88, z: 0.28 },
6936
+ wavePhaseSpeed: 0.88,
6937
+ wakeStrength: 0.31,
6938
+ wakeLength: 18,
6939
+ collisionRippleStrength: 0.42,
6940
+ waterNear: { r: 0.08, g: 0.23, b: 0.33 },
6941
+ waterFar: { r: 0.18, g: 0.35, b: 0.49 },
6942
+ harborWall: { r: 0.26, g: 0.24, b: 0.28 },
6943
+ harborDeck: { r: 0.33, g: 0.22, b: 0.16 },
6944
+ harborTower: { r: 0.23, g: 0.24, b: 0.29 },
6945
+ flagColor: { r: 0.66, g: 0.16, b: 0.13 },
6946
+ flagMotion: 0.92,
6947
+ lanternCore: { r: 0.98, g: 0.8, b: 0.48 },
6948
+ lanternGlow: { r: 1, g: 0.56, b: 0.2 },
6949
+ lanternReflectionStrength: 0.42,
6950
+ torchCore: { r: 0.99, g: 0.72, b: 0.36 },
6951
+ torchGlow: { r: 0.98, g: 0.38, b: 0.15 },
6952
+ collisionFlash: "rgba(255, 212, 168, 0.16)"
6871
6953
  };
6872
6954
  return {
6873
6955
  skyTop: typeof customVisuals.skyTop === "string" ? customVisuals.skyTop : defaults.skyTop,
@@ -6876,7 +6958,12 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
6876
6958
  seaTop: typeof customVisuals.seaTop === "string" ? customVisuals.seaTop : defaults.seaTop,
6877
6959
  seaMid: typeof customVisuals.seaMid === "string" ? customVisuals.seaMid : defaults.seaMid,
6878
6960
  seaBottom: typeof customVisuals.seaBottom === "string" ? customVisuals.seaBottom : defaults.seaBottom,
6879
- sunCore: typeof customVisuals.sunCore === "string" ? customVisuals.sunCore : defaults.sunCore,
6961
+ duskGlow: typeof customVisuals.duskGlow === "string" ? customVisuals.duskGlow : defaults.duskGlow,
6962
+ moonCore: typeof customVisuals.moonCore === "string" ? customVisuals.moonCore : typeof customVisuals.sunCore === "string" ? customVisuals.sunCore : defaults.moonCore,
6963
+ moonHalo: typeof customVisuals.moonHalo === "string" ? customVisuals.moonHalo : defaults.moonHalo,
6964
+ moonReflection: typeof customVisuals.moonReflection === "string" ? customVisuals.moonReflection : defaults.moonReflection,
6965
+ starColor: typeof customVisuals.starColor === "string" ? customVisuals.starColor : defaults.starColor,
6966
+ ambientMist: typeof customVisuals.ambientMist === "string" ? customVisuals.ambientMist : defaults.ambientMist,
6880
6967
  reflectionStrength: readVisualNumber(
6881
6968
  customVisuals.reflectionStrength,
6882
6969
  defaults.reflectionStrength
@@ -6897,7 +6984,16 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
6897
6984
  harborDeck: normalizeColorOverride(customVisuals.harborDeck, defaults.harborDeck),
6898
6985
  harborTower: normalizeColorOverride(customVisuals.harborTower, defaults.harborTower),
6899
6986
  flagColor: normalizeColorOverride(customVisuals.flagColor, defaults.flagColor),
6900
- flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion)
6987
+ flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion),
6988
+ lanternCore: normalizeColorOverride(customVisuals.lanternCore, defaults.lanternCore),
6989
+ lanternGlow: normalizeColorOverride(customVisuals.lanternGlow, defaults.lanternGlow),
6990
+ lanternReflectionStrength: readVisualNumber(
6991
+ customVisuals.lanternReflectionStrength,
6992
+ defaults.lanternReflectionStrength
6993
+ ),
6994
+ torchCore: normalizeColorOverride(customVisuals.torchCore, defaults.torchCore),
6995
+ torchGlow: normalizeColorOverride(customVisuals.torchGlow, defaults.torchGlow),
6996
+ collisionFlash: typeof customVisuals.collisionFlash === "string" ? customVisuals.collisionFlash : defaults.collisionFlash
6901
6997
  };
6902
6998
  }
6903
6999
  function buildClothSurface(model, state, meshDetail, visuals) {
@@ -7132,18 +7228,34 @@ function createSceneState(options) {
7132
7228
  {
7133
7229
  id: "northwind",
7134
7230
  position: vec3(-5.2, 0, 7.2),
7135
- velocity: vec3(2.1, 0, -1.6),
7136
- rotationY: 0.42,
7137
- angularVelocity: 0.18,
7138
- tint: { r: 0.62, g: 0.39, b: 0.23 }
7231
+ velocity: vec3(2.35, 0, -1.08),
7232
+ rotationY: 0.58,
7233
+ angularVelocity: 0.09,
7234
+ tint: { r: 0.62, g: 0.39, b: 0.23 },
7235
+ massScale: 1.42,
7236
+ cruiseSpeed: 2.25,
7237
+ throttleResponse: 0.46,
7238
+ rudderResponse: 0.54,
7239
+ wanderPhase: 0.35,
7240
+ lanterns: SHIP_LANTERNS,
7241
+ lanternStrength: 1.06,
7242
+ collisionRadiusScale: 1.04
7139
7243
  },
7140
7244
  {
7141
7245
  id: "tidecaller",
7142
7246
  position: vec3(4.8, 0, 4.4),
7143
- velocity: vec3(-1.85, 0, 1.25),
7144
- rotationY: -2.62,
7145
- angularVelocity: -0.14,
7146
- tint: { r: 0.48, g: 0.28, b: 0.19 }
7247
+ velocity: vec3(-2.15, 0, 1.74),
7248
+ rotationY: -2.48,
7249
+ angularVelocity: -0.2,
7250
+ tint: { r: 0.48, g: 0.28, b: 0.19 },
7251
+ massScale: 0.84,
7252
+ cruiseSpeed: 2.68,
7253
+ throttleResponse: 0.7,
7254
+ rudderResponse: 0.78,
7255
+ wanderPhase: 1.6,
7256
+ lanterns: SHIP_LANTERNS,
7257
+ lanternStrength: 1.18,
7258
+ collisionRadiusScale: 0.94
7147
7259
  }
7148
7260
  ],
7149
7261
  sprays: [],
@@ -7165,41 +7277,71 @@ function setListContent(element, values) {
7165
7277
  element.innerHTML = values.map((value) => `<li>${value}</li>`).join("");
7166
7278
  }
7167
7279
  function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength, visuals) {
7168
- const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
7169
7280
  const sky = ctx.createLinearGradient(0, 0, 0, canvas.height * 0.5);
7170
7281
  sky.addColorStop(0, visuals.skyTop);
7171
- sky.addColorStop(0.6, visuals.skyMid);
7282
+ sky.addColorStop(0.54, visuals.skyMid);
7172
7283
  sky.addColorStop(1, visuals.skyBottom);
7173
7284
  ctx.fillStyle = sky;
7174
7285
  ctx.fillRect(0, 0, canvas.width, canvas.height);
7286
+ for (let index = 0; index < 70; index += 1) {
7287
+ const x = pseudoRandom(index + 13) * canvas.width;
7288
+ const y = pseudoRandom(index * 7 + 5) * canvas.height * 0.42;
7289
+ const twinkle = 0.45 + Math.sin(state.time * 1.4 + index * 0.73) * 0.25;
7290
+ const radius = 0.6 + pseudoRandom(index * 11 + 2) * 1.9;
7291
+ ctx.fillStyle = visuals.starColor.replace(/[\d.]+\)$/u, `${clamp(twinkle, 0.16, 0.92)})`);
7292
+ ctx.beginPath();
7293
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
7294
+ ctx.fill();
7295
+ }
7296
+ const horizonGlow = ctx.createLinearGradient(0, canvas.height * 0.22, 0, canvas.height * 0.62);
7297
+ horizonGlow.addColorStop(0, "rgba(0, 0, 0, 0)");
7298
+ horizonGlow.addColorStop(1, visuals.duskGlow);
7299
+ ctx.fillStyle = horizonGlow;
7300
+ ctx.fillRect(0, canvas.height * 0.2, canvas.width, canvas.height * 0.45);
7175
7301
  const shoreline = ctx.createLinearGradient(0, canvas.height * 0.45, 0, canvas.height);
7176
7302
  shoreline.addColorStop(0, visuals.seaTop);
7177
7303
  shoreline.addColorStop(0.52, visuals.seaMid);
7178
7304
  shoreline.addColorStop(1, visuals.seaBottom);
7179
7305
  ctx.fillStyle = shoreline;
7180
7306
  ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
7181
- const sunX = mix(canvas.width * 0.16, canvas.width * 0.84, (Math.sin(state.time * 0.12) + 1) * 0.5);
7182
- const sunY = canvas.height * 0.14 + Math.cos(state.time * 0.12) * 22;
7183
- const sun = ctx.createRadialGradient(sunX, sunY, 10, sunX, sunY, 90);
7184
- sun.addColorStop(0, visuals.sunCore);
7185
- sun.addColorStop(1, "rgba(255, 244, 210, 0)");
7186
- ctx.fillStyle = sun;
7307
+ const moonX = canvas.width * 0.76 + Math.sin(state.time * 0.045) * 18;
7308
+ const moonY = canvas.height * 0.17 + Math.cos(state.time * 0.05) * 10;
7309
+ const moon = ctx.createRadialGradient(moonX, moonY, 14, moonX, moonY, 126);
7310
+ moon.addColorStop(0, visuals.moonCore);
7311
+ moon.addColorStop(0.46, visuals.moonHalo);
7312
+ moon.addColorStop(1, "rgba(167, 191, 255, 0)");
7313
+ ctx.fillStyle = moon;
7314
+ ctx.beginPath();
7315
+ ctx.arc(moonX, moonY, 94, 0, Math.PI * 2);
7316
+ ctx.fill();
7317
+ const moonCore = ctx.createRadialGradient(moonX, moonY, 4, moonX, moonY, 28);
7318
+ moonCore.addColorStop(0, "rgba(255, 255, 255, 0.98)");
7319
+ moonCore.addColorStop(1, visuals.moonCore);
7320
+ ctx.fillStyle = moonCore;
7187
7321
  ctx.beginPath();
7188
- ctx.arc(sunX, sunY, 90, 0, Math.PI * 2);
7322
+ ctx.arc(moonX, moonY, 24, 0, Math.PI * 2);
7189
7323
  ctx.fill();
7190
- const track = ctx.createLinearGradient(sunX, canvas.height * 0.46, sunX, canvas.height * 0.96);
7191
- track.addColorStop(0, `rgba(255, 243, 214, ${0.08 + reflectionStrength * 0.18})`);
7192
- track.addColorStop(0.42, `rgba(224, 242, 255, ${0.04 + reflectionStrength * 0.2})`);
7193
- track.addColorStop(1, "rgba(224, 242, 255, 0)");
7324
+ const track = ctx.createLinearGradient(moonX, canvas.height * 0.44, moonX, canvas.height * 0.98);
7325
+ track.addColorStop(0, visuals.moonReflection.replace(/[\d.]+\)$/u, `${0.08 + reflectionStrength * 0.12})`));
7326
+ track.addColorStop(0.42, visuals.moonReflection.replace(/[\d.]+\)$/u, `${0.04 + reflectionStrength * 0.18})`));
7327
+ track.addColorStop(1, "rgba(192, 214, 255, 0)");
7194
7328
  ctx.save();
7195
7329
  ctx.globalCompositeOperation = "screen";
7196
7330
  ctx.fillStyle = track;
7197
7331
  ctx.beginPath();
7198
- ctx.ellipse(sunX, canvas.height * 0.72, 46 + shadowStrength * 60, canvas.height * 0.26, 0, 0, Math.PI * 2);
7332
+ ctx.ellipse(moonX, canvas.height * 0.75, 38 + shadowStrength * 42, canvas.height * 0.24, 0, 0, Math.PI * 2);
7199
7333
  ctx.fill();
7200
7334
  ctx.restore();
7335
+ const mist = ctx.createLinearGradient(0, canvas.height * 0.5, 0, canvas.height);
7336
+ mist.addColorStop(0, "rgba(0, 0, 0, 0)");
7337
+ mist.addColorStop(1, visuals.ambientMist);
7338
+ ctx.fillStyle = mist;
7339
+ ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
7201
7340
  if (state.collisionFlash > 0.01) {
7202
- ctx.fillStyle = `rgba(255, 243, 228, ${state.collisionFlash * 0.14})`;
7341
+ ctx.fillStyle = visuals.collisionFlash.replace(
7342
+ /[\d.]+\)$/u,
7343
+ `${clamp(state.collisionFlash * 0.22, 0, 0.26)})`
7344
+ );
7203
7345
  ctx.fillRect(0, 0, canvas.width, canvas.height);
7204
7346
  }
7205
7347
  }
@@ -7298,7 +7440,7 @@ function pushHarborGeometry(camera, viewport, triangles, visuals) {
7298
7440
  }
7299
7441
  }
7300
7442
  function renderShipRigging(ctx, ship, camera, viewport) {
7301
- const transform = { position: ship.position, rotationY: ship.rotationY, scale: 1.1 };
7443
+ const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
7302
7444
  const mastBase = transformPoint(vec3(0, 0.38, -0.4), transform);
7303
7445
  const mastTop = transformPoint(vec3(0, 3.8, -0.2), transform);
7304
7446
  const aftBase = transformPoint(vec3(-0.25, 0.32, -1.9), transform);
@@ -7402,6 +7544,35 @@ function renderWaterHighlights(ctx, waterBands, camera, viewport) {
7402
7544
  }
7403
7545
  }
7404
7546
  }
7547
+ function readPhysicsNumber(physics, key, fallback) {
7548
+ const value = physics?.[key];
7549
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
7550
+ }
7551
+ function getShipMass(ship, shipModel) {
7552
+ const baseMass = readPhysicsNumber(shipModel.physics, "mass", 3200);
7553
+ return baseMass * readVisualNumber(ship.massScale, 1);
7554
+ }
7555
+ function getShipHalfExtents(ship, shipModel) {
7556
+ const physicsHalfExtents = Array.isArray(shipModel.physics.halfExtents) ? shipModel.physics.halfExtents : [1.35, 0.95, 3.9];
7557
+ const scale = SHIP_SCALE * readVisualNumber(ship.collisionRadiusScale, 1);
7558
+ return {
7559
+ x: physicsHalfExtents[0] * scale,
7560
+ y: physicsHalfExtents[1] * scale,
7561
+ z: physicsHalfExtents[2] * scale
7562
+ };
7563
+ }
7564
+ function getShipCollisionRadius(ship, shipModel) {
7565
+ const halfExtents = getShipHalfExtents(ship, shipModel);
7566
+ return Math.max(halfExtents.x * 1.08, halfExtents.z * 0.62);
7567
+ }
7568
+ function getShipInverseMass(ship, shipModel) {
7569
+ return 1 / Math.max(1, getShipMass(ship, shipModel));
7570
+ }
7571
+ function getShipInverseInertia(ship, shipModel) {
7572
+ const radius = getShipCollisionRadius(ship, shipModel);
7573
+ const inertia = getShipMass(ship, shipModel) * radius * radius * 0.72;
7574
+ return 1 / Math.max(1, inertia);
7575
+ }
7405
7576
  function spawnSpray(state, point, intensity) {
7406
7577
  const count = state.fluidDetail.getSnapshot().currentLevel.config.splashCount;
7407
7578
  for (let index = 0; index < count; index += 1) {
@@ -7414,55 +7585,182 @@ function spawnSpray(state, point, intensity) {
7414
7585
  });
7415
7586
  }
7416
7587
  }
7417
- function updateShips(state, dt, shipModel) {
7588
+ function resolveShipRoute(ship, state, radius) {
7589
+ if (typeof ship.routeDirection !== "number") {
7590
+ ship.routeDirection = ship.velocity.x >= 0 ? 1 : -1;
7591
+ }
7592
+ if (ship.position.x > HARBOR_BOUNDS.maxX - radius * 1.1) {
7593
+ ship.routeDirection = -1;
7594
+ } else if (ship.position.x < HARBOR_BOUNDS.minX + radius * 1.1) {
7595
+ ship.routeDirection = 1;
7596
+ }
7597
+ const wander = Math.sin(state.time * 0.22 + readVisualNumber(ship.wanderPhase, 0));
7598
+ const crossCurrent = Math.cos(state.time * 0.31 + readVisualNumber(ship.wanderPhase, 0));
7599
+ const laneCenter = ship.id === "northwind" ? 10.2 + wander * 2.1 + crossCurrent * 0.6 : 7 + wander * 3.3 - crossCurrent * 1.1;
7600
+ const targetX = ship.routeDirection > 0 ? HARBOR_BOUNDS.maxX - radius * 1.7 : HARBOR_BOUNDS.minX + radius * 1.7;
7601
+ return vec3(targetX, 0, clamp(laneCenter, HARBOR_BOUNDS.minZ + 1.8, HARBOR_BOUNDS.maxZ - 1.8));
7602
+ }
7603
+ function updateShipMotion(state, ship, dt, shipModel) {
7418
7604
  const physics = shipModel.physics;
7419
- const halfExtents = physics.halfExtents ?? [1.35, 0.95, 3.9];
7605
+ const massScale = Math.max(0.55, readVisualNumber(ship.massScale, 1));
7606
+ const radius = getShipCollisionRadius(ship, shipModel);
7607
+ const waterline = readPhysicsNumber(physics, "waterline", 0.42);
7608
+ const linearDamping = readPhysicsNumber(physics, "linearDamping", 0.04);
7609
+ const angularDamping = readPhysicsNumber(physics, "angularDamping", 0.08);
7610
+ const throttleResponse = readVisualNumber(ship.throttleResponse, 0.58);
7611
+ const rudderResponse = readVisualNumber(ship.rudderResponse, 0.62);
7612
+ const cruiseSpeed = readVisualNumber(ship.cruiseSpeed, 2.4);
7613
+ ship.collisionCooldown = Math.max(0, readVisualNumber(ship.collisionCooldown, 0) - dt);
7614
+ const forward = directionFromYaw(ship.rotationY);
7615
+ const lateral = perpendicularOnWater(forward);
7616
+ const routeTarget = resolveShipRoute(ship, state, radius);
7617
+ const desiredHeading = Math.atan2(routeTarget.x - ship.position.x, routeTarget.z - ship.position.z);
7618
+ const headingError = Math.atan2(
7619
+ Math.sin(desiredHeading - ship.rotationY),
7620
+ Math.cos(desiredHeading - ship.rotationY)
7621
+ );
7622
+ ship.angularVelocity += headingError * rudderResponse * dt * (1.18 / Math.sqrt(massScale)) + Math.sin(state.time * 0.9 + readVisualNumber(ship.wanderPhase, 0)) * dt * 0.04;
7623
+ const waveDirection = resolveWaveDirection(state);
7624
+ const forwardSpeed = dotVec3(ship.velocity, forward);
7625
+ const lateralSpeed = dotVec3(ship.velocity, lateral);
7626
+ const thrust = (cruiseSpeed - forwardSpeed) * throttleResponse;
7627
+ const currentDrift = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.016;
7628
+ const acceleration = addVec3(
7629
+ scaleVec3(forward, thrust),
7630
+ addVec3(
7631
+ scaleVec3(lateral, -lateralSpeed * (1.28 + rudderResponse * 0.4)),
7632
+ scaleVec3(waveDirection, currentDrift / Math.sqrt(massScale))
7633
+ )
7634
+ );
7635
+ ship.velocity = addVec3(ship.velocity, scaleVec3(acceleration, dt));
7636
+ ship.velocity = scaleVec3(
7637
+ ship.velocity,
7638
+ Math.max(0, 1 - linearDamping / Math.pow(massScale, 0.22) * dt)
7639
+ );
7640
+ ship.angularVelocity *= Math.max(
7641
+ 0,
7642
+ 1 - angularDamping / Math.pow(massScale, 0.15) * dt
7643
+ );
7644
+ ship.rotationY += ship.angularVelocity * dt;
7645
+ ship.position = addVec3(ship.position, scaleVec3(ship.velocity, dt));
7646
+ ship.position.y = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.24 + waterline;
7647
+ }
7648
+ function resolveBoundaryCollision(ship, state, shipModel) {
7649
+ const restitution = readPhysicsNumber(shipModel.physics, "restitution", 0.22) * 0.56;
7650
+ const radius = getShipCollisionRadius(ship, shipModel);
7651
+ const boundaries = [
7652
+ { axis: "x", min: HARBOR_BOUNDS.minX + radius, max: HARBOR_BOUNDS.maxX - radius, normalMin: vec3(1, 0, 0), normalMax: vec3(-1, 0, 0) },
7653
+ { axis: "z", min: HARBOR_BOUNDS.minZ + radius, max: HARBOR_BOUNDS.maxZ - radius, normalMin: vec3(0, 0, 1), normalMax: vec3(0, 0, -1) }
7654
+ ];
7655
+ for (const boundary of boundaries) {
7656
+ if (ship.position[boundary.axis] < boundary.min) {
7657
+ ship.position[boundary.axis] = boundary.min;
7658
+ const normal = boundary.normalMin;
7659
+ const speedIntoWall = dotVec3(ship.velocity, normal);
7660
+ if (speedIntoWall < 0) {
7661
+ ship.velocity = subVec3(
7662
+ ship.velocity,
7663
+ scaleVec3(normal, (1 + restitution) * speedIntoWall)
7664
+ );
7665
+ const tangent = vec3(-normal.z, 0, normal.x);
7666
+ const tangentSpeed = dotVec3(ship.velocity, tangent);
7667
+ ship.velocity = subVec3(ship.velocity, scaleVec3(tangent, tangentSpeed * 0.12));
7668
+ ship.angularVelocity += tangentSpeed * 4e-3;
7669
+ }
7670
+ } else if (ship.position[boundary.axis] > boundary.max) {
7671
+ ship.position[boundary.axis] = boundary.max;
7672
+ const normal = boundary.normalMax;
7673
+ const speedIntoWall = dotVec3(ship.velocity, normal);
7674
+ if (speedIntoWall < 0) {
7675
+ ship.velocity = subVec3(
7676
+ ship.velocity,
7677
+ scaleVec3(normal, (1 + restitution) * speedIntoWall)
7678
+ );
7679
+ const tangent = vec3(-normal.z, 0, normal.x);
7680
+ const tangentSpeed = dotVec3(ship.velocity, tangent);
7681
+ ship.velocity = subVec3(ship.velocity, scaleVec3(tangent, tangentSpeed * 0.12));
7682
+ ship.angularVelocity += tangentSpeed * 4e-3;
7683
+ }
7684
+ }
7685
+ }
7686
+ }
7687
+ function resolveShipCollision(state, a, b, shipModel) {
7688
+ const delta = subVec3(b.position, a.position);
7689
+ const radiusA = getShipCollisionRadius(a, shipModel);
7690
+ const radiusB = getShipCollisionRadius(b, shipModel);
7691
+ const distance = Math.hypot(delta.x, delta.z);
7692
+ const minDistance = radiusA + radiusB;
7693
+ if (distance >= minDistance) {
7694
+ return false;
7695
+ }
7696
+ const normal = distance > 1e-4 ? normalizeVec3(vec3(delta.x / distance, 0, delta.z / distance)) : normalizeVec3(vec3(Math.cos(state.time * 5.2), 0, Math.sin(state.time * 4.8)));
7697
+ const tangent = vec3(-normal.z, 0, normal.x);
7698
+ const penetration = minDistance - distance;
7699
+ const invMassA = getShipInverseMass(a, shipModel);
7700
+ const invMassB = getShipInverseMass(b, shipModel);
7701
+ const invMassSum = invMassA + invMassB;
7702
+ const correction = scaleVec3(normal, penetration / Math.max(1e-4, invMassSum) * 0.72);
7703
+ a.position = subVec3(a.position, scaleVec3(correction, invMassA));
7704
+ b.position = addVec3(b.position, scaleVec3(correction, invMassB));
7705
+ const relativeVelocity = subVec3(b.velocity, a.velocity);
7706
+ const velocityAlongNormal = dotVec3(relativeVelocity, normal);
7707
+ const restitution = readPhysicsNumber(shipModel.physics, "restitution", 0.22) * 0.88;
7708
+ if (velocityAlongNormal < 0) {
7709
+ const impulseMagnitude = -(1 + restitution) * velocityAlongNormal / Math.max(1e-4, invMassSum);
7710
+ const impulse = scaleVec3(normal, impulseMagnitude);
7711
+ a.velocity = subVec3(a.velocity, scaleVec3(impulse, invMassA));
7712
+ b.velocity = addVec3(b.velocity, scaleVec3(impulse, invMassB));
7713
+ const tangentSpeed = dotVec3(relativeVelocity, tangent);
7714
+ const frictionMagnitude = clamp(
7715
+ -tangentSpeed / Math.max(1e-4, invMassSum),
7716
+ -impulseMagnitude * 0.16,
7717
+ impulseMagnitude * 0.16
7718
+ );
7719
+ const frictionImpulse = scaleVec3(tangent, frictionMagnitude);
7720
+ a.velocity = subVec3(a.velocity, scaleVec3(frictionImpulse, invMassA));
7721
+ b.velocity = addVec3(b.velocity, scaleVec3(frictionImpulse, invMassB));
7722
+ a.angularVelocity -= tangentSpeed * radiusA * getShipInverseInertia(a, shipModel) * 0.2 + impulseMagnitude * 24e-5;
7723
+ b.angularVelocity += tangentSpeed * radiusB * getShipInverseInertia(b, shipModel) * 0.2 + impulseMagnitude * 24e-5;
7724
+ const impactSpeed = Math.abs(velocityAlongNormal);
7725
+ if (impactSpeed > 0.18 && Math.max(readVisualNumber(a.collisionCooldown, 0), readVisualNumber(b.collisionCooldown, 0)) <= 0) {
7726
+ const contactPoint = vec3(
7727
+ (a.position.x + b.position.x) * 0.5,
7728
+ (a.position.y + b.position.y) * 0.5 + 0.14,
7729
+ (a.position.z + b.position.z) * 0.5
7730
+ );
7731
+ spawnSpray(state, contactPoint, impactSpeed * 2.4 + penetration * 8);
7732
+ state.waveImpulses.push({
7733
+ x: contactPoint.x,
7734
+ z: contactPoint.z,
7735
+ strength: clamp(0.24 + impactSpeed * 0.46 + penetration * 0.9, 0.2, 1.7),
7736
+ radius: 0.9 + penetration * 1.4,
7737
+ life: 1
7738
+ });
7739
+ state.collisionCount += 1;
7740
+ state.collisionFlash = Math.max(
7741
+ state.collisionFlash,
7742
+ clamp(impactSpeed * 0.55 + penetration * 1.8, 0.16, 1)
7743
+ );
7744
+ a.collisionCooldown = 0.2;
7745
+ b.collisionCooldown = 0.2;
7746
+ }
7747
+ }
7748
+ state.contactCount += 1;
7749
+ return true;
7750
+ }
7751
+ function updateShips(state, dt, shipModel) {
7420
7752
  let collided = false;
7421
7753
  state.contactCount = 0;
7422
7754
  for (const ship of state.ships) {
7423
- ship.position = addVec3(ship.position, scaleVec3(ship.velocity, dt));
7424
- ship.rotationY += ship.angularVelocity * dt;
7425
- ship.velocity = scaleVec3(ship.velocity, 1 - (physics.linearDamping ?? 0.04) * dt);
7426
- ship.angularVelocity *= 1 - (physics.angularDamping ?? 0.08) * dt;
7427
- ship.position.y = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.22 + (physics.waterline ?? 0.42);
7428
- if (Math.abs(ship.position.x) > 10) {
7429
- ship.velocity.x *= -1;
7430
- ship.angularVelocity *= -1;
7431
- }
7432
- if (ship.position.z < 2 || ship.position.z > 16) {
7433
- ship.velocity.z *= -1;
7434
- ship.angularVelocity *= -1;
7435
- }
7755
+ updateShipMotion(state, ship, dt, shipModel);
7756
+ resolveBoundaryCollision(ship, state, shipModel);
7436
7757
  }
7437
- const [a, b] = state.ships;
7438
- const dx = b.position.x - a.position.x;
7439
- const dz = b.position.z - a.position.z;
7440
- const overlapX = Math.abs(dx) < halfExtents[0] * 1.7;
7441
- const overlapZ = Math.abs(dz) < halfExtents[2] * 0.8;
7442
- if (overlapX && overlapZ) {
7443
- const restitution = physics.restitution ?? 0.22;
7444
- const swapX = a.velocity.x;
7445
- const swapZ = a.velocity.z;
7446
- a.velocity.x = -b.velocity.x * (0.86 + restitution);
7447
- a.velocity.z = -b.velocity.z * (0.82 + restitution);
7448
- b.velocity.x = -swapX * (0.86 + restitution);
7449
- b.velocity.z = -swapZ * (0.82 + restitution);
7450
- a.angularVelocity += 0.55;
7451
- b.angularVelocity -= 0.55;
7452
- const contactPoint = vec3((a.position.x + b.position.x) * 0.5, (a.position.y + b.position.y) * 0.5 + 0.1, (a.position.z + b.position.z) * 0.5);
7453
- spawnSpray(state, contactPoint, Math.abs(dx) + Math.abs(dz));
7454
- state.waveImpulses.push({
7455
- x: contactPoint.x,
7456
- z: contactPoint.z,
7457
- strength: Math.min(1.4, 0.2 + (Math.abs(dx) + Math.abs(dz)) * 0.18),
7458
- radius: 0.8,
7459
- life: 1
7460
- });
7461
- state.collisionCount += 1;
7462
- state.contactCount = 1;
7463
- collided = true;
7758
+ for (let index = 0; index < state.ships.length; index += 1) {
7759
+ for (let otherIndex = index + 1; otherIndex < state.ships.length; otherIndex += 1) {
7760
+ collided = resolveShipCollision(state, state.ships[index], state.ships[otherIndex], shipModel) || collided;
7761
+ }
7464
7762
  }
7465
- state.collisionFlash = collided ? 1 : Math.max(0, state.collisionFlash - dt * 1.8);
7763
+ state.collisionFlash = collided ? Math.max(0.12, state.collisionFlash) : Math.max(0, state.collisionFlash - dt * 1.3);
7466
7764
  }
7467
7765
  function updateWaveImpulses(state, dt) {
7468
7766
  state.waveImpulses = state.waveImpulses.map((impulse) => ({
@@ -7575,7 +7873,7 @@ function renderFlagPole(ctx, camera, viewport) {
7575
7873
  function renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDir, shadowStrength) {
7576
7874
  const bounds = shipModel.bounds;
7577
7875
  const keelY = (shipModel.physics.waterline ?? 0.42) - 0.28;
7578
- const transform = { position: ship.position, rotationY: ship.rotationY, scale: 1.1 };
7876
+ const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
7579
7877
  const hullCorners = [
7580
7878
  vec3(bounds.min[0], keelY, bounds.min[2]),
7581
7879
  vec3(bounds.max[0], keelY, bounds.min[2]),
@@ -7601,6 +7899,97 @@ function renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength
7601
7899
  blur: 12 + shadowStrength * 20
7602
7900
  });
7603
7901
  }
7902
+ function renderGlowLight(ctx, point, camera, viewport, coreColor, glowColor, glowScale, reflectionStrength = 0, state = null) {
7903
+ const projected = projectPoint(point, camera, viewport);
7904
+ if (!projected) {
7905
+ return;
7906
+ }
7907
+ const radius = clamp(1 / projected.depth * 420 * glowScale, 4, 34);
7908
+ const halo = ctx.createRadialGradient(projected.x, projected.y, radius * 0.12, projected.x, projected.y, radius);
7909
+ halo.addColorStop(0, colorToRgba(coreColor, 0.98));
7910
+ halo.addColorStop(0.5, colorToRgba(glowColor, 0.42));
7911
+ halo.addColorStop(1, colorToRgba(glowColor, 0));
7912
+ ctx.save();
7913
+ ctx.globalCompositeOperation = "screen";
7914
+ ctx.fillStyle = halo;
7915
+ ctx.beginPath();
7916
+ ctx.arc(projected.x, projected.y, radius, 0, Math.PI * 2);
7917
+ ctx.fill();
7918
+ ctx.restore();
7919
+ ctx.fillStyle = colorToRgba(coreColor, 0.98);
7920
+ ctx.beginPath();
7921
+ ctx.arc(projected.x, projected.y, Math.max(1.2, radius * 0.16), 0, Math.PI * 2);
7922
+ ctx.fill();
7923
+ if (state && reflectionStrength > 0) {
7924
+ const waterline = sampleWave(state, point.x, point.z, state.time) * 0.22;
7925
+ const reflectedPoint = vec3(point.x, waterline - (point.y - waterline) * 0.58, point.z + 0.08);
7926
+ const reflected = projectPoint(reflectedPoint, camera, viewport);
7927
+ if (reflected) {
7928
+ const reflectionRadius = radius * 0.72;
7929
+ const glow = ctx.createRadialGradient(
7930
+ reflected.x,
7931
+ reflected.y,
7932
+ reflectionRadius * 0.1,
7933
+ reflected.x,
7934
+ reflected.y,
7935
+ reflectionRadius
7936
+ );
7937
+ glow.addColorStop(0, colorToRgba(coreColor, reflectionStrength * 0.34));
7938
+ glow.addColorStop(1, colorToRgba(glowColor, 0));
7939
+ ctx.save();
7940
+ ctx.globalCompositeOperation = "screen";
7941
+ ctx.fillStyle = glow;
7942
+ ctx.beginPath();
7943
+ ctx.ellipse(
7944
+ reflected.x,
7945
+ reflected.y,
7946
+ reflectionRadius * 0.34,
7947
+ reflectionRadius,
7948
+ 0,
7949
+ 0,
7950
+ Math.PI * 2
7951
+ );
7952
+ ctx.fill();
7953
+ ctx.restore();
7954
+ }
7955
+ }
7956
+ }
7957
+ function renderShipLanterns(ctx, ship, state, camera, viewport, visuals) {
7958
+ const lanterns = Array.isArray(ship.lanterns) ? ship.lanterns : SHIP_LANTERNS;
7959
+ const strength = readVisualNumber(ship.lanternStrength, 1);
7960
+ for (const lantern of lanterns) {
7961
+ const position = transformPoint(
7962
+ vec3(lantern.x, lantern.y, lantern.z),
7963
+ { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE }
7964
+ );
7965
+ renderGlowLight(
7966
+ ctx,
7967
+ position,
7968
+ camera,
7969
+ viewport,
7970
+ visuals.lanternCore,
7971
+ visuals.lanternGlow,
7972
+ lantern.glow * strength,
7973
+ visuals.lanternReflectionStrength,
7974
+ state
7975
+ );
7976
+ }
7977
+ }
7978
+ function renderHarborTorches(ctx, state, camera, viewport, visuals) {
7979
+ for (const torch of HARBOR_TORCHES) {
7980
+ renderGlowLight(
7981
+ ctx,
7982
+ vec3(torch.x, torch.y, torch.z),
7983
+ camera,
7984
+ viewport,
7985
+ visuals.torchCore,
7986
+ visuals.torchGlow,
7987
+ torch.glow,
7988
+ visuals.lanternReflectionStrength * 0.55,
7989
+ state
7990
+ );
7991
+ }
7992
+ }
7604
7993
  function renderScene(ctx, canvas, state, shipModel, dom) {
7605
7994
  const viewport = { width: canvas.width, height: canvas.height };
7606
7995
  const camera = buildCamera(state, canvas);
@@ -7610,7 +7999,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7610
7999
  importance: state.focus === "lighting" ? "critical" : "high"
7611
8000
  });
7612
8001
  const nearLighting = lightingPlan.bands.find((entry) => entry.band === "near") ?? lightingPlan.bands[0];
7613
- const lightDir = normalizeVec3(vec3(-0.45, 0.85, -0.24));
8002
+ const lightDir = normalizeVec3(vec3(-0.22, 0.94, -0.31));
7614
8003
  const lightingSnapshot = state.lightingDetail.getSnapshot();
7615
8004
  const visuals = resolveVisualConfig(
7616
8005
  nearLighting,
@@ -7684,7 +8073,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7684
8073
  for (const ship of state.ships) {
7685
8074
  buildTrianglesFromMesh(
7686
8075
  shipModel,
7687
- { position: ship.position, rotationY: ship.rotationY, scale: 1.1 },
8076
+ { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE },
7688
8077
  ship.tint,
7689
8078
  camera,
7690
8079
  viewport,
@@ -7698,10 +8087,12 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7698
8087
  renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
7699
8088
  drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, shadowStrength);
7700
8089
  renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
8090
+ renderHarborTorches(ctx, state, camera, viewport, visuals);
7701
8091
  renderFlagPole(ctx, camera, viewport);
7702
8092
  renderClothAccent(ctx, cloth, camera, viewport);
7703
8093
  for (const ship of state.ships) {
7704
8094
  renderShipRigging(ctx, ship, camera, viewport);
8095
+ renderShipLanterns(ctx, ship, state, camera, viewport, visuals);
7705
8096
  }
7706
8097
  renderSprays(ctx, state.sprays, camera, viewport);
7707
8098
  const debugSnapshot = state.debugSession.getSnapshot();
@@ -7713,8 +8104,10 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7713
8104
  const sceneMetrics = [
7714
8105
  `focus: ${state.focus}`,
7715
8106
  `ships: ${state.ships.length} active GLTF hulls`,
8107
+ `moonlight: cold overhead key + ${HARBOR_TORCHES.length + state.ships.length * SHIP_LANTERNS.length} warm deck and harbor lights`,
7716
8108
  `physics snapshot: ${state.physics.snapshot.stage} (${state.physics.snapshot.stability})`,
7717
- `physics contacts: ${state.physics.snapshot.contactCount ?? 0}`,
8109
+ `physics contacts: ${state.contactCount}`,
8110
+ `mass split: ${state.ships.map((ship) => `${ship.id} ${(getShipMass(ship, shipModel) / 1e3).toFixed(1)}t`).join(" \xB7 ")}`,
7718
8111
  `cloth band: ${cloth.band} -> ${cloth.representation.output}`,
7719
8112
  `fluid near band: ${water.bandMeshes[0].representation.output}`,
7720
8113
  `lighting profile: ${lightingPlan.profile} (${lightingDistanceBands.length} bands)`
@@ -7737,8 +8130,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7737
8130
  ];
7738
8131
  const sceneNotes = state.focus === "physics" ? [
7739
8132
  "Stable world snapshots are taken after the authoritative rigid-body commit and before visual follow-up work.",
7740
- "The ships collide on GLTF-derived hull volumes while cloth and fluid remain downstream visual consumers.",
7741
- "Near-field lighting keeps the ray-traced-primary shadow impression so the collision read stays crisp."
8133
+ "The ships collide with mass-weighted impulses and positional correction, so the heavier hull keeps more of its line.",
8134
+ "Moonlight keeps the overall read legible while lanterns and torches make collision moments easy to track against the water."
7742
8135
  ] : SCENE_NOTES;
7743
8136
  const custom = state.demoDescription ?? null;
7744
8137
  setListContent(
@@ -7755,7 +8148,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7755
8148
  );
7756
8149
  setListContent(dom.sceneNotes, Array.isArray(custom?.notes) ? custom.notes : sceneNotes);
7757
8150
  dom.status.textContent = typeof custom?.status === "string" ? custom.status : `3D scene live \xB7 ${state.lastDecision.metrics.fps.toFixed(1)} FPS`;
7758
- dom.details.textContent = typeof custom?.details === "string" ? custom.details : state.focus === "physics" ? `Stable world snapshots are emitted from ${state.physics.plan.snapshotStageId} after the authoritative solver; GLTF ships collide on ${shipModel.physics.shape ?? "box"} volumes while visual follow-up remains downstream.` : `GLTF ships are colliding with ${shipModel.physics.shape ?? "box"} physics volumes; cloth and fluid remain continuous while the governor pressure is ${state.lastDecision.pressureLevel}.`;
8151
+ dom.details.textContent = typeof custom?.details === "string" ? custom.details : state.focus === "physics" ? `Stable world snapshots are emitted from ${state.physics.plan.snapshotStageId} after the authoritative solver; the heavier hull now carries momentum through mass-aware collision impulses while cloth and fluid remain downstream.` : `Moonlit GLTF ships collide on ${shipModel.physics.shape ?? "box"} physics volumes; lantern reflections, cloth, and fluid remain continuous while the governor pressure is ${state.lastDecision.pressureLevel}.`;
7759
8152
  }
7760
8153
  function updateSceneState(state, dt, shipModel) {
7761
8154
  updateShips(state, dt, shipModel);
@@ -7774,7 +8167,9 @@ function syncTextState(state, shipModel) {
7774
8167
  y: Number(ship.position.y.toFixed(2)),
7775
8168
  z: Number(ship.position.z.toFixed(2)),
7776
8169
  vx: Number(ship.velocity.x.toFixed(2)),
7777
- vz: Number(ship.velocity.z.toFixed(2))
8170
+ vz: Number(ship.velocity.z.toFixed(2)),
8171
+ massKg: Math.round(getShipMass(ship, shipModel)),
8172
+ lanterns: Array.isArray(ship.lanterns) ? ship.lanterns.length : 0
7778
8173
  })),
7779
8174
  shipPhysics: shipModel.physics,
7780
8175
  sprays: state.sprays.length,
@@ -7802,6 +8197,7 @@ function syncTextState(state, shipModel) {
7802
8197
  async function mountGpuShowcase(options = {}) {
7803
8198
  injectStyles();
7804
8199
  const root = options.root ?? document.body;
8200
+ root.classList?.add?.(ROOT_CLASS);
7805
8201
  const previousMarkup = root.innerHTML;
7806
8202
  const previousRenderGameToText = window.render_game_to_text;
7807
8203
  const previousAdvanceTime = window.advanceTime;
@@ -7821,8 +8217,11 @@ async function mountGpuShowcase(options = {}) {
7821
8217
  state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
7822
8218
  syncTextState(state, shipModel);
7823
8219
  const ctx = dom.canvas.getContext("2d");
8220
+ if (!ctx) {
8221
+ throw new Error("2D canvas context is required for the shared showcase.");
8222
+ }
8223
+ let animationFrameId = 0;
7824
8224
  let destroyed = false;
7825
- let frameHandle = null;
7826
8225
  const renderFrame = (nowMs) => {
7827
8226
  if (destroyed) {
7828
8227
  return;
@@ -7843,7 +8242,7 @@ async function mountGpuShowcase(options = {}) {
7843
8242
  state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
7844
8243
  renderScene(ctx, dom.canvas, state, shipModel, dom);
7845
8244
  syncTextState(state, shipModel);
7846
- frameHandle = requestAnimationFrame(renderFrame);
8245
+ animationFrameId = requestAnimationFrame(renderFrame);
7847
8246
  };
7848
8247
  const handlePauseClick = () => {
7849
8248
  state.paused = !state.paused;
@@ -7862,34 +8261,43 @@ async function mountGpuShowcase(options = {}) {
7862
8261
  dom.pauseButton.addEventListener("click", handlePauseClick);
7863
8262
  dom.stressToggle.addEventListener("change", handleStressChange);
7864
8263
  dom.focusMode.addEventListener("change", handleFocusChange);
7865
- frameHandle = requestAnimationFrame(renderFrame);
8264
+ animationFrameId = requestAnimationFrame(renderFrame);
8265
+ const destroy = () => {
8266
+ if (destroyed) {
8267
+ return;
8268
+ }
8269
+ destroyed = true;
8270
+ if (animationFrameId) {
8271
+ cancelAnimationFrame(animationFrameId);
8272
+ }
8273
+ dom.pauseButton.removeEventListener("click", handlePauseClick);
8274
+ dom.stressToggle.removeEventListener("change", handleStressChange);
8275
+ dom.focusMode.removeEventListener("change", handleFocusChange);
8276
+ try {
8277
+ if (typeof options.destroyState === "function") {
8278
+ options.destroyState(state.packageState);
8279
+ }
8280
+ } finally {
8281
+ state.packageState = void 0;
8282
+ }
8283
+ root.classList?.remove?.(ROOT_CLASS);
8284
+ root.innerHTML = previousMarkup;
8285
+ if (typeof previousRenderGameToText === "function") {
8286
+ window.render_game_to_text = previousRenderGameToText;
8287
+ } else {
8288
+ delete window.render_game_to_text;
8289
+ }
8290
+ if (typeof previousAdvanceTime === "function") {
8291
+ window.advanceTime = previousAdvanceTime;
8292
+ } else {
8293
+ delete window.advanceTime;
8294
+ }
8295
+ };
7866
8296
  return {
7867
8297
  state,
7868
8298
  shipModel,
7869
8299
  canvas: dom.canvas,
7870
- destroy() {
7871
- if (destroyed) {
7872
- return;
7873
- }
7874
- destroyed = true;
7875
- if (frameHandle != null) {
7876
- cancelAnimationFrame(frameHandle);
7877
- }
7878
- dom.pauseButton.removeEventListener("click", handlePauseClick);
7879
- dom.stressToggle.removeEventListener("change", handleStressChange);
7880
- dom.focusMode.removeEventListener("change", handleFocusChange);
7881
- root.innerHTML = previousMarkup;
7882
- if (typeof previousRenderGameToText === "function") {
7883
- window.render_game_to_text = previousRenderGameToText;
7884
- } else {
7885
- delete window.render_game_to_text;
7886
- }
7887
- if (typeof previousAdvanceTime === "function") {
7888
- window.advanceTime = previousAdvanceTime;
7889
- } else {
7890
- delete window.advanceTime;
7891
- }
7892
- }
8300
+ destroy
7893
8301
  };
7894
8302
  }
7895
8303
  function updatePhysicsSnapshot(state, shipModel) {
@@ -7903,7 +8311,7 @@ function updatePhysicsSnapshot(state, shipModel) {
7903
8311
  animationInputRevision: state.frame,
7904
8312
  bodyCount: state.ships.length + 2,
7905
8313
  dynamicBodyCount: state.ships.length,
7906
- contactCount: state.collisionFlash > 0.02 ? 1 : 0,
8314
+ contactCount: state.contactCount,
7907
8315
  metadata: {
7908
8316
  collisionCount: state.collisionCount,
7909
8317
  contactCount: state.contactCount,
@@ -7912,7 +8320,7 @@ function updatePhysicsSnapshot(state, shipModel) {
7912
8320
  }
7913
8321
  });
7914
8322
  }
7915
- var import_meta2, STYLE_ID, DEFAULT_TITLE, DEFAULT_SUBTITLE, CAMERA_PRESETS, showcaseFocusModes, SCENE_NOTES, UNIT_BOX_MESH;
8323
+ var STYLE_ID, ROOT_CLASS, DEFAULT_TITLE, DEFAULT_SUBTITLE, SHIP_SCALE, HARBOR_BOUNDS, CAMERA_PRESETS, showcaseFocusModes, SCENE_NOTES, UNIT_BOX_MESH, SHIP_LANTERNS, HARBOR_TORCHES;
7916
8324
  var init_showcase_runtime = __esm({
7917
8325
  "src/showcase-runtime.js"() {
7918
8326
  init_dist();
@@ -7921,11 +8329,19 @@ var init_showcase_runtime = __esm({
7921
8329
  init_dist4();
7922
8330
  init_dist5();
7923
8331
  init_browser();
8332
+ init_asset_url();
7924
8333
  init_gltf_loader();
7925
- import_meta2 = {};
7926
8334
  STYLE_ID = "plasius-shared-3d-showcase-style";
8335
+ ROOT_CLASS = "plasius-showcase-root";
7927
8336
  DEFAULT_TITLE = "Flag by the Sea";
7928
8337
  DEFAULT_SUBTITLE = "Shared 3D validation scene using GLTF ships, cloth, fluid continuity, adaptive performance, and telemetry.";
8338
+ SHIP_SCALE = 1.1;
8339
+ HARBOR_BOUNDS = Object.freeze({
8340
+ minX: -11.2,
8341
+ maxX: 11.2,
8342
+ minZ: 1.8,
8343
+ maxZ: 17.2
8344
+ });
7929
8345
  CAMERA_PRESETS = Object.freeze({
7930
8346
  integrated: Object.freeze({ yaw: -0.55, pitch: 0.34, distance: 27, target: [0, 2.2, 0] }),
7931
8347
  lighting: Object.freeze({ yaw: -0.28, pitch: 0.28, distance: 23, target: [0, 2.8, 0] }),
@@ -7937,10 +8353,10 @@ var init_showcase_runtime = __esm({
7937
8353
  });
7938
8354
  showcaseFocusModes = Object.freeze(Object.keys(CAMERA_PRESETS));
7939
8355
  SCENE_NOTES = Object.freeze([
7940
- "Ships are loaded from a GLTF asset and carry physics metadata from node extras.",
7941
- "Near-field lighting uses the ray-traced-primary shadow and reflection path before stepping down by distance band.",
7942
- "Cloth and fluid continuity stay coherent across near, mid, far, and horizon bands.",
7943
- "Performance pressure reduces visual detail before authoritative collision motion is touched."
8356
+ "Ships are loaded from a GLTF asset and carry mass, damping, restitution, and hull extents from node extras.",
8357
+ "Moonlight sets the cold ambient read while deck lanterns and harbor torches provide warm local contrast.",
8358
+ "Cloth and fluid continuity stay coherent across near, mid, far, and horizon bands even in the darker night palette.",
8359
+ "Performance pressure reduces visual detail before mass-weighted authoritative collision motion is touched."
7944
8360
  ]);
7945
8361
  UNIT_BOX_MESH = Object.freeze({
7946
8362
  positions: Object.freeze([
@@ -8008,6 +8424,17 @@ var init_showcase_runtime = __esm({
8008
8424
  0
8009
8425
  ])
8010
8426
  });
8427
+ SHIP_LANTERNS = Object.freeze([
8428
+ Object.freeze({ x: 0.94, y: 1.54, z: 2.52, glow: 1 }),
8429
+ Object.freeze({ x: -0.9, y: 1.58, z: 2.44, glow: 0.92 }),
8430
+ Object.freeze({ x: 0.62, y: 1.42, z: -2.18, glow: 0.88 }),
8431
+ Object.freeze({ x: -0.58, y: 1.46, z: -2.04, glow: 0.84 })
8432
+ ]);
8433
+ HARBOR_TORCHES = Object.freeze([
8434
+ Object.freeze({ x: -5.2, y: 1.25, z: 1.36, glow: 1.1 }),
8435
+ Object.freeze({ x: -8.6, y: 2.48, z: -0.72, glow: 1 }),
8436
+ Object.freeze({ x: -10.4, y: 1.28, z: 0.82, glow: 0.92 })
8437
+ ]);
8011
8438
  }
8012
8439
  });
8013
8440
 
@@ -8016,11 +8443,11 @@ var index_exports = {};
8016
8443
  __export(index_exports, {
8017
8444
  loadGltfModel: () => loadGltfModel2,
8018
8445
  mountGpuShowcase: () => mountGpuShowcase2,
8019
- resolveShowcaseAssetUrl: () => resolveShowcaseAssetUrl2,
8446
+ resolveShowcaseAssetUrl: () => resolveShowcaseAssetUrl,
8020
8447
  showcaseFocusModes: () => showcaseFocusModes2
8021
8448
  });
8022
8449
  module.exports = __toCommonJS(index_exports);
8023
- var import_meta3 = {};
8450
+ init_asset_url();
8024
8451
  var showcaseFocusModes2 = Object.freeze([
8025
8452
  "integrated",
8026
8453
  "lighting",
@@ -8030,9 +8457,6 @@ var showcaseFocusModes2 = Object.freeze([
8030
8457
  "performance",
8031
8458
  "debug"
8032
8459
  ]);
8033
- function resolveShowcaseAssetUrl2(baseUrl2 = import_meta3.url) {
8034
- return new URL("../assets/brigantine.gltf", baseUrl2);
8035
- }
8036
8460
  async function loadGltfModel2(url) {
8037
8461
  const module2 = await Promise.resolve().then(() => (init_gltf_loader(), gltf_loader_exports));
8038
8462
  return module2.loadGltfModel(url);