@plasius/gpu-shared 0.1.19 → 1.0.0

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.
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  loadGltfModel,
8
8
  resolveShowcaseAssetUrl
9
- } from "./chunk-UKCJ2AWJ.js";
9
+ } from "./chunk-KGKLNL4X.js";
10
10
  import "./chunk-2GM64LB6.js";
11
11
  import "./chunk-DGUM43GV.js";
12
12
 
@@ -764,6 +764,13 @@ var LEGACY_HARBOR_LAYOUT = Object.freeze([
764
764
  })
765
765
  ]);
766
766
  var SHOWCASE_ENVIRONMENT_LAYOUT = Object.freeze([
767
+ Object.freeze({
768
+ assetKey: "shoreline",
769
+ position: Object.freeze({ x: 1.8, y: -0.04, z: 0.48 }),
770
+ rotationY: -0.03,
771
+ scale: 1.02,
772
+ accent: 0.03
773
+ }),
767
774
  Object.freeze({
768
775
  assetKey: "harbor-dock",
769
776
  position: Object.freeze({ x: -4.6, y: 0.16, z: 0.7 }),
@@ -794,6 +801,18 @@ var HARBOR_TORCHES = Object.freeze([
794
801
  Object.freeze({ x: -8.6, y: 2.48, z: -0.72, glow: 1 }),
795
802
  Object.freeze({ x: -10.4, y: 1.28, z: 0.82, glow: 0.92 })
796
803
  ]);
804
+ var SHORELINE_FOAM_ANCHORS = Object.freeze([
805
+ Object.freeze({ x: -7.8, z: 3, length: 1.25, angle: -0.12 }),
806
+ Object.freeze({ x: -6.3, z: 2.72, length: 0.92, angle: 0.08 }),
807
+ Object.freeze({ x: -4.9, z: 3.16, length: 1.08, angle: -0.2 }),
808
+ Object.freeze({ x: -3.2, z: 2.42, length: 0.76, angle: 0.16 }),
809
+ Object.freeze({ x: -1.4, z: 2.82, length: 1.18, angle: -0.04 }),
810
+ Object.freeze({ x: 0.4, z: 3.08, length: 0.88, angle: 0.14 }),
811
+ Object.freeze({ x: 2.1, z: 2.56, length: 1.34, angle: -0.18 }),
812
+ Object.freeze({ x: 3.8, z: 3, length: 0.94, angle: 0.1 }),
813
+ Object.freeze({ x: 5.5, z: 2.72, length: 1.12, angle: -0.08 }),
814
+ Object.freeze({ x: 7, z: 3.22, length: 0.72, angle: 0.18 })
815
+ ]);
797
816
  var FLAG_LAYOUT = Object.freeze({
798
817
  origin: Object.freeze({ x: -3.5, y: 5.9, z: 2.4 }),
799
818
  width: 4.8,
@@ -835,31 +854,31 @@ function injectStyles() {
835
854
  box-sizing: border-box;
836
855
  }
837
856
  .plasius-demo {
838
- width: min(1560px, calc(100vw - 32px));
839
- margin: 0 auto;
840
- padding: 28px 0 40px;
841
- display: grid;
842
- gap: 20px;
843
- }
844
- .plasius-demo__hero,
845
- .plasius-demo__layout {
846
- display: grid;
847
- gap: 20px;
848
- }
849
- .plasius-demo__hero {
850
- grid-template-columns: minmax(0, 1.5fr) minmax(320px, 0.85fr);
851
- align-items: start;
857
+ position: relative;
858
+ width: 100%;
859
+ min-height: 100dvh;
860
+ overflow: hidden;
852
861
  }
853
862
  .plasius-panel {
854
863
  border: 1px solid var(--plasius-border);
855
- border-radius: 24px;
864
+ border-radius: 8px;
856
865
  background: var(--plasius-panel);
857
866
  box-shadow: var(--plasius-shadow);
858
867
  backdrop-filter: blur(12px);
859
868
  }
860
869
  .plasius-demo__hero-card,
861
870
  .plasius-demo__status {
862
- padding: 20px 22px;
871
+ position: absolute;
872
+ z-index: 3;
873
+ padding: 10px 12px;
874
+ }
875
+ .plasius-demo__hero-card {
876
+ display: none;
877
+ }
878
+ .plasius-demo__status {
879
+ left: 16px;
880
+ bottom: 84px;
881
+ max-width: min(360px, calc(100vw - 32px));
863
882
  }
864
883
  .plasius-demo__eyebrow {
865
884
  margin: 0 0 8px;
@@ -882,31 +901,36 @@ function injectStyles() {
882
901
  .plasius-demo__status-badge {
883
902
  width: fit-content;
884
903
  margin: 0;
885
- padding: 8px 12px;
886
- border-radius: 999px;
904
+ padding: 6px 9px;
905
+ border-radius: 6px;
887
906
  background: rgba(243, 177, 106, 0.14);
888
907
  color: var(--plasius-accent);
889
908
  font-weight: 700;
909
+ font-size: 12px;
890
910
  }
891
911
  .plasius-demo__status-text {
892
912
  margin: 10px 0 0;
893
913
  color: var(--plasius-muted);
894
- line-height: 1.6;
895
- }
896
- .plasius-demo__layout {
897
- grid-template-columns: minmax(0, 1.45fr) minmax(320px, 0.68fr);
898
- align-items: start;
914
+ font-size: 12px;
915
+ line-height: 1.45;
899
916
  }
900
917
  .plasius-demo__canvas-panel {
901
- padding: 18px;
902
- position: relative;
918
+ position: absolute;
919
+ inset: 0;
920
+ padding: 0;
921
+ border: 0;
922
+ border-radius: 0;
923
+ background: transparent;
924
+ box-shadow: none;
925
+ backdrop-filter: none;
903
926
  }
904
927
  .plasius-demo__canvas {
905
928
  width: 100%;
906
- aspect-ratio: 16 / 9;
929
+ height: 100%;
930
+ min-height: 100dvh;
907
931
  display: block;
908
- border-radius: 20px;
909
- border: 1px solid rgba(159, 185, 223, 0.12);
932
+ border: 0;
933
+ border-radius: 0;
910
934
  background: linear-gradient(180deg, #071220 0%, #132440 42%, #10344b 42%, #05111d 100%);
911
935
  }
912
936
  .${CAPTURE_CLASS} .plasius-demo {
@@ -919,6 +943,8 @@ function injectStyles() {
919
943
  .${CAPTURE_CLASS} .plasius-demo__toolbar,
920
944
  .${CAPTURE_CLASS} .plasius-demo__legend,
921
945
  .${CAPTURE_CLASS} .plasius-demo__sidebar,
946
+ .${CAPTURE_CLASS} .plasius-demo__diagnostics,
947
+ .${CAPTURE_CLASS} .plasius-demo__status,
922
948
  .${CAPTURE_CLASS} .plasius-demo__footer {
923
949
  display: none;
924
950
  }
@@ -945,12 +971,14 @@ function injectStyles() {
945
971
  }
946
972
  .plasius-demo__toolbar {
947
973
  position: absolute;
948
- top: 26px;
949
- left: 26px;
974
+ top: 84px;
975
+ left: 16px;
976
+ z-index: 4;
950
977
  display: flex;
951
- gap: 12px;
978
+ gap: 8px;
952
979
  flex-wrap: wrap;
953
980
  align-items: center;
981
+ max-width: min(560px, calc(100vw - 32px));
954
982
  }
955
983
  .plasius-demo button,
956
984
  .plasius-demo label,
@@ -962,22 +990,63 @@ function injectStyles() {
962
990
  .plasius-demo .plasius-toggle,
963
991
  .plasius-demo select {
964
992
  border: 1px solid rgba(159, 185, 223, 0.18);
965
- border-radius: 999px;
993
+ border-radius: 6px;
966
994
  background: rgba(9, 20, 34, 0.84);
967
995
  color: var(--plasius-ink);
968
- padding: 10px 14px;
996
+ padding: 8px 10px;
969
997
  }
970
998
  .plasius-toggle {
971
999
  display: inline-flex;
972
1000
  align-items: center;
973
1001
  gap: 8px;
974
1002
  }
1003
+ .plasius-demo__diagnostics {
1004
+ position: absolute;
1005
+ right: 16px;
1006
+ bottom: 84px;
1007
+ z-index: 4;
1008
+ max-width: min(420px, calc(100vw - 32px));
1009
+ color: var(--plasius-ink);
1010
+ font-family: "JetBrains Mono", monospace;
1011
+ font-size: 12px;
1012
+ }
1013
+ .plasius-demo__diagnostics summary {
1014
+ width: fit-content;
1015
+ margin-left: auto;
1016
+ border: 1px solid rgba(159, 185, 223, 0.18);
1017
+ border-radius: 6px;
1018
+ padding: 8px 10px;
1019
+ background: rgba(9, 20, 34, 0.84);
1020
+ cursor: pointer;
1021
+ list-style: none;
1022
+ }
1023
+ .plasius-demo__diagnostics summary::-webkit-details-marker {
1024
+ display: none;
1025
+ }
1026
+ .plasius-demo__diagnostics[open] {
1027
+ width: min(420px, calc(100vw - 32px));
1028
+ }
1029
+ .plasius-demo__diagnostics[open] summary {
1030
+ margin-bottom: 8px;
1031
+ background: rgba(243, 177, 106, 0.14);
1032
+ color: var(--plasius-accent);
1033
+ }
975
1034
  .plasius-demo__sidebar {
976
1035
  display: grid;
977
- gap: 18px;
1036
+ gap: 8px;
1037
+ max-height: min(58vh, 520px);
1038
+ overflow: auto;
978
1039
  }
979
1040
  .plasius-demo__card {
980
- padding: 18px;
1041
+ padding: 10px;
1042
+ }
1043
+ .plasius-demo__card h2 {
1044
+ margin: 0;
1045
+ color: rgba(226, 236, 255, 0.72);
1046
+ font-family: "JetBrains Mono", monospace;
1047
+ font-size: 11px;
1048
+ letter-spacing: 0.08em;
1049
+ text-transform: uppercase;
981
1050
  }
982
1051
  .plasius-demo__metrics,
983
1052
  .plasius-demo__metrics li {
@@ -986,27 +1055,19 @@ function injectStyles() {
986
1055
  list-style: none;
987
1056
  }
988
1057
  .plasius-demo__metrics {
989
- margin-top: 12px;
1058
+ margin-top: 8px;
990
1059
  display: grid;
991
- gap: 8px;
1060
+ gap: 5px;
992
1061
  color: var(--plasius-muted);
993
- line-height: 1.55;
1062
+ font-size: 12px;
1063
+ line-height: 1.35;
994
1064
  }
995
1065
  .plasius-demo__metrics li {
996
1066
  border-top: 1px solid rgba(21, 32, 40, 0.08);
997
- padding-top: 8px;
1067
+ padding-top: 5px;
998
1068
  }
999
1069
  .plasius-demo__legend {
1000
- position: absolute;
1001
- right: 24px;
1002
- bottom: 24px;
1003
- padding: 10px 14px;
1004
- border-radius: 16px;
1005
- background: rgba(9, 20, 34, 0.82);
1006
- border: 1px solid rgba(159, 185, 223, 0.16);
1007
- color: var(--plasius-muted);
1008
- font-size: 12px;
1009
- line-height: 1.45;
1070
+ display: none;
1010
1071
  }
1011
1072
  .plasius-demo__legend strong {
1012
1073
  display: block;
@@ -1014,15 +1075,62 @@ function injectStyles() {
1014
1075
  margin-bottom: 4px;
1015
1076
  }
1016
1077
  .plasius-demo__footer {
1017
- margin-top: 4px;
1018
- color: rgba(226, 236, 255, 0.68);
1019
- font-size: 13px;
1020
- line-height: 1.6;
1078
+ display: none;
1021
1079
  }
1022
1080
  @media (max-width: 1200px) {
1023
- .plasius-demo__hero,
1024
- .plasius-demo__layout {
1025
- grid-template-columns: 1fr;
1081
+ .plasius-demo__toolbar {
1082
+ top: 92px;
1083
+ }
1084
+ }
1085
+ @media (max-width: 640px) {
1086
+ .plasius-demo__status {
1087
+ left: 10px;
1088
+ bottom: 10px;
1089
+ max-width: calc(100vw - 126px);
1090
+ padding: 6px 8px;
1091
+ }
1092
+ .plasius-demo__status-text {
1093
+ display: none;
1094
+ }
1095
+ .plasius-demo__status-badge {
1096
+ max-width: calc(100vw - 142px);
1097
+ overflow: hidden;
1098
+ text-overflow: ellipsis;
1099
+ white-space: nowrap;
1100
+ }
1101
+ .plasius-demo__toolbar {
1102
+ top: 10px;
1103
+ left: 10px;
1104
+ right: 10px;
1105
+ max-width: calc(100vw - 20px);
1106
+ flex-wrap: nowrap;
1107
+ overflow-x: auto;
1108
+ padding-bottom: 4px;
1109
+ scrollbar-width: none;
1110
+ }
1111
+ .plasius-demo__toolbar::-webkit-scrollbar {
1112
+ display: none;
1113
+ }
1114
+ .plasius-demo button,
1115
+ .plasius-demo .plasius-toggle,
1116
+ .plasius-demo select {
1117
+ padding: 7px 8px;
1118
+ font-size: 12px;
1119
+ white-space: nowrap;
1120
+ }
1121
+ .plasius-demo__diagnostics {
1122
+ right: 10px;
1123
+ bottom: 10px;
1124
+ }
1125
+ .plasius-demo__diagnostics[open] {
1126
+ bottom: 56px;
1127
+ left: 10px;
1128
+ right: 10px;
1129
+ width: auto;
1130
+ max-width: none;
1131
+ }
1132
+ .plasius-demo__diagnostics[open] .plasius-demo__sidebar {
1133
+ max-height: min(42vh, 340px);
1026
1134
  }
1027
1135
  }
1028
1136
  `;
@@ -1364,11 +1472,12 @@ function buildTrianglesFromMesh(mesh, transform, colorOverride, camera, viewport
1364
1472
  }
1365
1473
  }
1366
1474
  async function loadShowcaseAssetCatalog() {
1367
- const [brigantine, cutter, lighthouse, harborDock] = await Promise.all([
1475
+ const [brigantine, cutter, lighthouse, harborDock, shoreline] = await Promise.all([
1368
1476
  loadGltfModel(resolveShowcaseAssetUrl("brigantine")),
1369
1477
  loadGltfModel(resolveShowcaseAssetUrl("cutter")),
1370
1478
  loadGltfModel(resolveShowcaseAssetUrl("lighthouse")),
1371
- loadGltfModel(resolveShowcaseAssetUrl("harbor-dock"))
1479
+ loadGltfModel(resolveShowcaseAssetUrl("harbor-dock")),
1480
+ loadGltfModel(resolveShowcaseAssetUrl("shoreline"))
1372
1481
  ]);
1373
1482
  return Object.freeze({
1374
1483
  primaryShipKey: "brigantine",
@@ -1378,7 +1487,8 @@ async function loadShowcaseAssetCatalog() {
1378
1487
  }),
1379
1488
  environment: Object.freeze({
1380
1489
  lighthouse,
1381
- "harbor-dock": harborDock
1490
+ "harbor-dock": harborDock,
1491
+ shoreline
1382
1492
  })
1383
1493
  });
1384
1494
  }
@@ -1504,24 +1614,27 @@ function buildDemoDom(root, options) {
1504
1614
  ${t(gpuSharedTranslationKeys.legendCollisions)}
1505
1615
  </div>
1506
1616
  </section>
1507
- <aside class="plasius-demo__sidebar">
1508
- <section class="plasius-panel plasius-demo__card">
1509
- <h2>${t(gpuSharedTranslationKeys.sceneState)}</h2>
1510
- <ul id="sceneMetrics" class="plasius-demo__metrics"></ul>
1511
- </section>
1512
- <section class="plasius-panel plasius-demo__card">
1513
- <h2>${t(gpuSharedTranslationKeys.qualityBudgets)}</h2>
1514
- <ul id="qualityMetrics" class="plasius-demo__metrics"></ul>
1515
- </section>
1516
- <section class="plasius-panel plasius-demo__card">
1517
- <h2>${t(gpuSharedTranslationKeys.debugTelemetry)}</h2>
1518
- <ul id="debugMetrics" class="plasius-demo__metrics"></ul>
1519
- </section>
1520
- <section class="plasius-panel plasius-demo__card">
1521
- <h2>${t(gpuSharedTranslationKeys.notes)}</h2>
1522
- <ul id="sceneNotes" class="plasius-demo__metrics"></ul>
1523
- </section>
1524
- </aside>
1617
+ <details class="plasius-demo__diagnostics">
1618
+ <summary>${t(gpuSharedTranslationKeys.debugTelemetry)}</summary>
1619
+ <aside class="plasius-demo__sidebar">
1620
+ <section class="plasius-panel plasius-demo__card">
1621
+ <h2>${t(gpuSharedTranslationKeys.sceneState)}</h2>
1622
+ <ul id="sceneMetrics" class="plasius-demo__metrics"></ul>
1623
+ </section>
1624
+ <section class="plasius-panel plasius-demo__card">
1625
+ <h2>${t(gpuSharedTranslationKeys.qualityBudgets)}</h2>
1626
+ <ul id="qualityMetrics" class="plasius-demo__metrics"></ul>
1627
+ </section>
1628
+ <section class="plasius-panel plasius-demo__card">
1629
+ <h2>${t(gpuSharedTranslationKeys.debugTelemetry)}</h2>
1630
+ <ul id="debugMetrics" class="plasius-demo__metrics"></ul>
1631
+ </section>
1632
+ <section class="plasius-panel plasius-demo__card">
1633
+ <h2>${t(gpuSharedTranslationKeys.notes)}</h2>
1634
+ <ul id="sceneNotes" class="plasius-demo__metrics"></ul>
1635
+ </section>
1636
+ </aside>
1637
+ </details>
1525
1638
  </section>
1526
1639
  <p class="plasius-demo__footer">
1527
1640
  This visual example is shared across the GPU packages to keep manual validation fast and consistent.
@@ -1885,11 +1998,11 @@ function advanceShowcaseClothSimulationState(clothState, options = {}) {
1885
1998
  1 + Math.sin(gustPhase * 0.74) * 0.18
1886
1999
  )
1887
2000
  );
1888
- const windStrength = (1.6 + broadMotion * 1.25 + wrinkleLayers * 0.12) * flagMotion * (0.44 + u * 1.14);
2001
+ const windStrength = (0.94 + broadMotion * 0.82 + wrinkleLayers * 0.08) * flagMotion * (0.36 + u * 0.92);
1889
2002
  const wrinkleForce = vec3(
1890
- Math.sin(wrinklePhase) * 0.22 * wrinkleMotion * flagMotion,
1891
- Math.cos(wrinklePhase * 0.7) * 0.08 * wrinkleMotion,
1892
- Math.cos(wrinklePhase) * 0.14 * broadMotion * flagMotion
2003
+ Math.sin(wrinklePhase) * 0.12 * wrinkleMotion * flagMotion,
2004
+ Math.cos(wrinklePhase * 0.7) * 0.045 * wrinkleMotion,
2005
+ Math.cos(wrinklePhase) * 0.08 * broadMotion * flagMotion
1893
2006
  );
1894
2007
  const acceleration = addVec3(
1895
2008
  vec3(0, -0.48 - u * 0.08, 0),
@@ -1946,25 +2059,25 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
1946
2059
  ambientMist: "rgba(41, 63, 97, 0.16)",
1947
2060
  reflectionStrength: lightingSnapshot.currentLevel.config.reflectionStrength,
1948
2061
  shadowAccent: lightingSnapshot.currentLevel.config.shadowStrength,
1949
- waveAmplitude: 0.94,
2062
+ waveAmplitude: 0.82,
1950
2063
  waveDirection: { x: 0.88, z: 0.28 },
1951
- wavePhaseSpeed: 0.88,
1952
- wakeStrength: 0.31,
1953
- wakeLength: 18,
1954
- collisionRippleStrength: 0.42,
1955
- waterNear: { r: 0.08, g: 0.23, b: 0.33 },
1956
- waterFar: { r: 0.18, g: 0.35, b: 0.49 },
2064
+ wavePhaseSpeed: 0.74,
2065
+ wakeStrength: 0.24,
2066
+ wakeLength: 17,
2067
+ collisionRippleStrength: 0.22,
2068
+ waterNear: { r: 0.05, g: 0.2, b: 0.3 },
2069
+ waterFar: { r: 0.13, g: 0.31, b: 0.45 },
1957
2070
  harborWall: { r: 0.26, g: 0.24, b: 0.28 },
1958
2071
  harborDeck: { r: 0.33, g: 0.22, b: 0.16 },
1959
2072
  harborTower: { r: 0.23, g: 0.24, b: 0.29 },
1960
- flagColor: { r: 0.66, g: 0.16, b: 0.13 },
1961
- flagMotion: 0.92,
2073
+ flagColor: { r: 0.54, g: 0.13, b: 0.11 },
2074
+ flagMotion: 0.58,
1962
2075
  lanternCore: { r: 0.98, g: 0.8, b: 0.48 },
1963
2076
  lanternGlow: { r: 1, g: 0.56, b: 0.2 },
1964
2077
  lanternReflectionStrength: 0.42,
1965
2078
  torchCore: { r: 0.99, g: 0.72, b: 0.36 },
1966
2079
  torchGlow: { r: 0.98, g: 0.38, b: 0.15 },
1967
- collisionFlash: "rgba(255, 212, 168, 0.16)"
2080
+ collisionFlash: "rgba(255, 212, 168, 0.08)"
1968
2081
  };
1969
2082
  return {
1970
2083
  skyTop: typeof customVisuals.skyTop === "string" ? customVisuals.skyTop : defaults.skyTop,
@@ -2021,6 +2134,11 @@ function buildClothSurface(model, state, meshDetail, visuals, clothFeatures) {
2021
2134
  representation: clothPresentation.representation,
2022
2135
  continuity: clothPresentation.continuity,
2023
2136
  color: visuals.flagColor,
2137
+ material: Object.freeze({
2138
+ weaveAlpha: clothPresentation.band === "near" ? 0.22 : 0.12,
2139
+ foldAlpha: clothPresentation.band === "near" ? 0.3 : 0.18,
2140
+ edgeHighlightAlpha: clothPresentation.band === "near" ? 0.42 : 0.28
2141
+ }),
2024
2142
  positions: clothState.positions.map((point) => vec3(point.x, point.y, point.z)),
2025
2143
  indices: clothState.indices,
2026
2144
  grid: { rows: clothState.rows, cols: clothState.cols }
@@ -2104,7 +2222,7 @@ function buildWaterMotionEffects(state) {
2104
2222
  impulse.z
2105
2223
  ),
2106
2224
  radius,
2107
- opacity: clamp(impulse.life * 0.28, 0.08, 0.3)
2225
+ opacity: clamp(impulse.life * 0.13, 0.035, 0.15)
2108
2226
  });
2109
2227
  });
2110
2228
  for (const ship of state.ships) {
@@ -2117,7 +2235,7 @@ function buildWaterMotionEffects(state) {
2117
2235
  const lateral = vec3(-direction.z, 0, direction.x);
2118
2236
  const points = [];
2119
2237
  for (let sampleIndex = 0; sampleIndex < 6; sampleIndex += 1) {
2120
- const along = 1 + sampleIndex * 1.45;
2238
+ const along = 1 + sampleIndex * 1.55;
2121
2239
  const lateralOffset = Math.sin(state.time * 1.2 + sampleIndex * 0.8 + readVisualNumber(ship.wanderPhase, 0)) * 0.12;
2122
2240
  const worldPoint = addVec3(
2123
2241
  ship.position,
@@ -2130,13 +2248,14 @@ function buildWaterMotionEffects(state) {
2130
2248
  sampleWave(state, worldPoint.x, worldPoint.z, state.time) * 0.24 + 0.04,
2131
2249
  worldPoint.z
2132
2250
  ),
2133
- width: 0.34 + sampleIndex * 0.13
2251
+ width: 0.3 + sampleIndex * 0.11,
2252
+ foam: clamp(0.28 - sampleIndex * 0.028 + speed * 0.025, 0.1, 0.34)
2134
2253
  })
2135
2254
  );
2136
2255
  }
2137
2256
  wakeTrails.push(
2138
2257
  Object.freeze({
2139
- opacity: clamp(0.18 + speed * 0.09, 0.22, 0.46),
2258
+ opacity: clamp(0.1 + speed * 0.048, 0.12, 0.24),
2140
2259
  points: Object.freeze(points)
2141
2260
  })
2142
2261
  );
@@ -2146,6 +2265,27 @@ function buildWaterMotionEffects(state) {
2146
2265
  rippleRings: Object.freeze(rippleRings)
2147
2266
  });
2148
2267
  }
2268
+ function buildShorelineFoamSegments(state) {
2269
+ return Object.freeze(
2270
+ SHORELINE_FOAM_ANCHORS.map((anchor, index) => {
2271
+ const pulse = 0.5 + Math.sin(state.time * 0.84 + index * 1.17) * 0.5;
2272
+ const drift = Math.sin(state.time * 0.38 + index * 0.61) * 0.1;
2273
+ const direction = normalizeVec3(vec3(Math.cos(anchor.angle), 0, Math.sin(anchor.angle)));
2274
+ const center = vec3(
2275
+ anchor.x + direction.x * drift,
2276
+ sampleWave(state, anchor.x, anchor.z, state.time) * 0.12 - 0.02,
2277
+ anchor.z + direction.z * drift
2278
+ );
2279
+ return Object.freeze({
2280
+ center,
2281
+ direction,
2282
+ length: anchor.length * (0.78 + pulse * 0.34),
2283
+ width: 0.16 + pulse * 0.12,
2284
+ opacity: 0.07 + pulse * 0.12
2285
+ });
2286
+ })
2287
+ );
2288
+ }
2149
2289
  function buildWaterBands(state, fluidDetail, visuals, fluidFeatures) {
2150
2290
  const resolvedFluidFeatures = normalizeFluidFeatureAdapters(fluidFeatures);
2151
2291
  const fluidPlan = resolvedFluidFeatures.createPlan({
@@ -2168,9 +2308,9 @@ function buildWaterBands(state, fluidDetail, visuals, fluidFeatures) {
2168
2308
  const representation = fluidPlan.representations.find((entry) => entry.band === bandSpec.band) ?? fluidPlan.representations[0];
2169
2309
  const continuity = resolvedFluidFeatures.createContinuityEnvelope({ fluidBodyId: "harbor" });
2170
2310
  const bandContinuity = resolveFluidBandContinuity(continuity, bandSpec.band);
2171
- const bandResolution = bandSpec.band === "near" ? fluidDetail.nearResolution : bandSpec.band === "mid" ? fluidDetail.midResolution : bandSpec.band === "far" ? 5 : 3;
2172
- const cols = Math.max(4, bandResolution * 2);
2173
- const rows = Math.max(4, bandResolution + 2);
2311
+ const bandResolution = bandSpec.band === "near" ? Math.ceil(fluidDetail.nearResolution * 1.28) : bandSpec.band === "mid" ? Math.ceil(fluidDetail.midResolution * 1.2) : bandSpec.band === "far" ? 5 : 3;
2312
+ const cols = Math.max(4, bandResolution * (bandSpec.band === "near" ? 3 : 2));
2313
+ const rows = Math.max(4, bandResolution + (bandSpec.band === "near" ? 5 : 2));
2174
2314
  const positions = [];
2175
2315
  const indices = [];
2176
2316
  const originX = -bandSpec.width * 0.5;
@@ -2181,7 +2321,9 @@ function buildWaterBands(state, fluidDetail, visuals, fluidFeatures) {
2181
2321
  const v = row / (rows - 1);
2182
2322
  const x = originX + bandSpec.width * u;
2183
2323
  const z = originZ + bandSpec.depth * v;
2184
- const y = bandSpec.y + sampleWave(state, x, z, state.time) * bandContinuity.amplitudeFloor * (bandSpec.band === "near" ? 0.9 : bandSpec.band === "mid" ? 0.55 : 0.3);
2324
+ const baseHeight = bandSpec.y + sampleWave(state, x, z, state.time) * bandContinuity.amplitudeFloor * (bandSpec.band === "near" ? 0.9 : bandSpec.band === "mid" ? 0.55 : 0.3);
2325
+ const detailHeight = bandSpec.band === "near" ? Math.sin(x * 1.25 + z * 0.42 - state.time * 2.4) * 0.035 : 0;
2326
+ const y = baseHeight + detailHeight;
2185
2327
  positions.push(vec3(x, y, z));
2186
2328
  }
2187
2329
  }
@@ -2210,7 +2352,12 @@ function buildWaterBands(state, fluidDetail, visuals, fluidFeatures) {
2210
2352
  r: mix(visuals.waterFar.r, 0.76, 0.2),
2211
2353
  g: mix(visuals.waterFar.g, 0.78, 0.2),
2212
2354
  b: mix(visuals.waterFar.b, 0.82, 0.2)
2213
- }
2355
+ },
2356
+ material: Object.freeze({
2357
+ highlightAlpha: bandSpec.band === "near" ? 0.2 : bandSpec.band === "mid" ? 0.13 : 0.07,
2358
+ foamAlpha: bandSpec.band === "near" ? 0.28 : bandSpec.band === "mid" ? 0.14 : 0.05,
2359
+ microRippleScale: bandSpec.band === "near" ? 1 : bandSpec.band === "mid" ? 0.58 : 0.28
2360
+ })
2214
2361
  });
2215
2362
  }
2216
2363
  return { fluidPlan, bandMeshes };
@@ -2286,15 +2433,15 @@ function createSceneState(options, featureAdapters) {
2286
2433
  {
2287
2434
  id: "northwind",
2288
2435
  modelKey: "brigantine",
2289
- position: vec3(-5.2, 0, 7.2),
2290
- velocity: vec3(2.35, 0, -1.08),
2291
- rotationY: 0.58,
2292
- angularVelocity: 0.09,
2436
+ position: vec3(-7.8, 0, 11.2),
2437
+ velocity: vec3(1.08, 0, -0.18),
2438
+ rotationY: 1.38,
2439
+ angularVelocity: 0.025,
2293
2440
  tint: { r: 0.62, g: 0.39, b: 0.23 },
2294
2441
  massScale: 1.42,
2295
- cruiseSpeed: 2.25,
2296
- throttleResponse: 0.46,
2297
- rudderResponse: 0.54,
2442
+ cruiseSpeed: 1.22,
2443
+ throttleResponse: 0.36,
2444
+ rudderResponse: 0.4,
2298
2445
  wanderPhase: 0.35,
2299
2446
  lanterns: CUTTER_LANTERNS,
2300
2447
  lanternStrength: 1.06,
@@ -2303,15 +2450,15 @@ function createSceneState(options, featureAdapters) {
2303
2450
  {
2304
2451
  id: "tidecaller",
2305
2452
  modelKey: "cutter",
2306
- position: vec3(4.8, 0, 4.4),
2307
- velocity: vec3(-2.15, 0, 1.74),
2308
- rotationY: -2.48,
2309
- angularVelocity: -0.2,
2453
+ position: vec3(6.8, 0, 5.4),
2454
+ velocity: vec3(-0.82, 0, 0.14),
2455
+ rotationY: -1.34,
2456
+ angularVelocity: -0.035,
2310
2457
  tint: { r: 0.58, g: 0.24, b: 0.16 },
2311
2458
  massScale: 0.84,
2312
- cruiseSpeed: 2.68,
2313
- throttleResponse: 0.7,
2314
- rudderResponse: 0.78,
2459
+ cruiseSpeed: 1.36,
2460
+ throttleResponse: 0.52,
2461
+ rudderResponse: 0.58,
2315
2462
  wanderPhase: 1.6,
2316
2463
  lanterns: SHIP_LANTERNS,
2317
2464
  lanternStrength: 1.18,
@@ -2610,9 +2757,10 @@ function renderShipRigging(ctx, ship, camera, viewport) {
2610
2757
  }
2611
2758
  function renderClothAccent(ctx, cloth, camera, viewport) {
2612
2759
  const projected = cloth.positions.map((point) => projectPoint(point, camera, viewport));
2613
- ctx.strokeStyle = "rgba(255, 241, 226, 0.92)";
2614
- ctx.lineWidth = 1.7;
2615
- for (let row = 0; row < cloth.grid.rows; row += Math.max(1, Math.floor(cloth.grid.rows / 5))) {
2760
+ const material = cloth.material ?? {};
2761
+ ctx.strokeStyle = `rgba(255, 241, 226, ${material.foldAlpha ?? 0.32})`;
2762
+ ctx.lineWidth = 1.8;
2763
+ for (let row = 0; row < cloth.grid.rows; row += Math.max(1, Math.floor(cloth.grid.rows / 6))) {
2616
2764
  ctx.beginPath();
2617
2765
  let started = false;
2618
2766
  for (let column = 0; column < cloth.grid.cols; column += 1) {
@@ -2631,6 +2779,27 @@ function renderClothAccent(ctx, cloth, camera, viewport) {
2631
2779
  ctx.stroke();
2632
2780
  }
2633
2781
  }
2782
+ ctx.strokeStyle = `rgba(255, 228, 204, ${material.weaveAlpha ?? 0.22})`;
2783
+ ctx.lineWidth = 0.85;
2784
+ for (let column = 1; column < cloth.grid.cols - 1; column += Math.max(1, Math.floor(cloth.grid.cols / 8))) {
2785
+ ctx.beginPath();
2786
+ let started = false;
2787
+ for (let row = 0; row < cloth.grid.rows; row += 1) {
2788
+ const point = projected[row * cloth.grid.cols + column];
2789
+ if (!point) {
2790
+ continue;
2791
+ }
2792
+ if (!started) {
2793
+ ctx.moveTo(point.x, point.y);
2794
+ started = true;
2795
+ } else {
2796
+ ctx.lineTo(point.x, point.y);
2797
+ }
2798
+ }
2799
+ if (started) {
2800
+ ctx.stroke();
2801
+ }
2802
+ }
2634
2803
  const borderIndices = [
2635
2804
  0,
2636
2805
  cloth.grid.cols - 1,
@@ -2638,6 +2807,25 @@ function renderClothAccent(ctx, cloth, camera, viewport) {
2638
2807
  (cloth.grid.rows - 1) * cloth.grid.cols
2639
2808
  ];
2640
2809
  ctx.fillStyle = colorToRgba(cloth.color, 0.95);
2810
+ ctx.strokeStyle = `rgba(255, 246, 236, ${material.edgeHighlightAlpha ?? 0.5})`;
2811
+ ctx.lineWidth = 1.4;
2812
+ ctx.beginPath();
2813
+ let borderStarted = false;
2814
+ for (let column = 0; column < cloth.grid.cols; column += 1) {
2815
+ const point = projected[column];
2816
+ if (!point) {
2817
+ continue;
2818
+ }
2819
+ if (!borderStarted) {
2820
+ ctx.moveTo(point.x, point.y);
2821
+ borderStarted = true;
2822
+ } else {
2823
+ ctx.lineTo(point.x, point.y);
2824
+ }
2825
+ }
2826
+ if (borderStarted) {
2827
+ ctx.stroke();
2828
+ }
2641
2829
  for (const index of borderIndices) {
2642
2830
  const point = projected[index];
2643
2831
  if (!point) {
@@ -2653,14 +2841,22 @@ function renderWaterHighlights(ctx, waterBands, camera, viewport) {
2653
2841
  if (band.band === "horizon") {
2654
2842
  continue;
2655
2843
  }
2656
- const interval = band.band === "near" ? 2 : 3;
2657
- const alpha = band.band === "near" ? 0.22 : 0.14;
2844
+ const interval = band.band === "near" ? 4 : 5;
2845
+ const alpha = band.material?.highlightAlpha ?? (band.band === "near" ? 0.22 : 0.14);
2658
2846
  ctx.strokeStyle = `rgba(232, 247, 255, ${alpha})`;
2659
- ctx.lineWidth = band.band === "near" ? 1.3 : 0.9;
2847
+ ctx.lineWidth = band.band === "near" ? 0.9 : 0.65;
2660
2848
  for (let row = interval; row < band.rows - 1; row += interval) {
2661
- ctx.beginPath();
2662
2849
  let started = false;
2663
- for (let column = 0; column < band.cols; column += 1) {
2850
+ ctx.beginPath();
2851
+ for (let column = 0; column < band.cols; column += band.band === "near" ? 2 : 3) {
2852
+ if (pseudoRandom(row * 47 + column * 13) < 0.18) {
2853
+ if (started) {
2854
+ ctx.stroke();
2855
+ ctx.beginPath();
2856
+ started = false;
2857
+ }
2858
+ continue;
2859
+ }
2664
2860
  const point = projectPoint(
2665
2861
  band.positions[row * band.cols + column],
2666
2862
  camera,
@@ -2680,7 +2876,55 @@ function renderWaterHighlights(ctx, waterBands, camera, viewport) {
2680
2876
  ctx.stroke();
2681
2877
  }
2682
2878
  }
2879
+ if (band.band === "near") {
2880
+ ctx.fillStyle = `rgba(236, 249, 255, ${(band.material?.foamAlpha ?? 0.28) * 0.72})`;
2881
+ for (let column = 3; column < band.cols - 3; column += 10) {
2882
+ const point = projectPoint(
2883
+ band.positions[Math.floor(band.rows * 0.42) * band.cols + column],
2884
+ camera,
2885
+ viewport
2886
+ );
2887
+ if (!point) {
2888
+ continue;
2889
+ }
2890
+ ctx.beginPath();
2891
+ ctx.ellipse(point.x, point.y, 1.8, 0.75, -0.2, 0, Math.PI * 2);
2892
+ ctx.fill();
2893
+ }
2894
+ }
2895
+ }
2896
+ }
2897
+ function renderShorelineFoamSegments(ctx, segments, camera, viewport) {
2898
+ ctx.save();
2899
+ ctx.globalCompositeOperation = "screen";
2900
+ ctx.lineCap = "round";
2901
+ ctx.lineJoin = "round";
2902
+ for (const segment of segments) {
2903
+ const half = scaleVec3(segment.direction, segment.length * 0.5);
2904
+ const start = projectPoint(subVec3(segment.center, half), camera, viewport);
2905
+ const end = projectPoint(addVec3(segment.center, half), camera, viewport);
2906
+ const center = projectPoint(segment.center, camera, viewport);
2907
+ if (!start || !end || !center) {
2908
+ continue;
2909
+ }
2910
+ const depthScale = clamp(140 / Math.max(12, center.depth), 3, 10);
2911
+ ctx.strokeStyle = `rgba(232, 242, 238, ${segment.opacity})`;
2912
+ ctx.lineWidth = clamp(segment.width * depthScale, 0.8, 2.8);
2913
+ ctx.beginPath();
2914
+ ctx.moveTo(start.x, start.y);
2915
+ ctx.quadraticCurveTo(
2916
+ center.x,
2917
+ center.y + Math.sin(segment.center.x * 1.7) * 2.4,
2918
+ end.x,
2919
+ end.y
2920
+ );
2921
+ ctx.stroke();
2922
+ ctx.fillStyle = `rgba(248, 251, 246, ${segment.opacity * 0.68})`;
2923
+ ctx.beginPath();
2924
+ ctx.ellipse(center.x, center.y, depthScale * 0.18, depthScale * 0.08, -0.2, 0, Math.PI * 2);
2925
+ ctx.fill();
2683
2926
  }
2927
+ ctx.restore();
2684
2928
  }
2685
2929
  function readPhysicsNumber(physics, key, fallback) {
2686
2930
  const value = physics?.[key];
@@ -2712,14 +2956,17 @@ function getShipInverseInertia(ship, shipModel) {
2712
2956
  return 1 / Math.max(1, inertia);
2713
2957
  }
2714
2958
  function spawnSpray(state, point, intensity) {
2715
- const count = state.fluidDetail.getSnapshot().currentLevel.config.splashCount;
2959
+ const count = Math.max(
2960
+ 3,
2961
+ Math.ceil(state.fluidDetail.getSnapshot().currentLevel.config.splashCount * 0.32)
2962
+ );
2716
2963
  for (let index = 0; index < count; index += 1) {
2717
2964
  const angle = index / count * Math.PI * 2;
2718
- const speed = 0.9 + Math.random() * intensity * 0.45;
2965
+ const speed = 0.46 + Math.random() * intensity * 0.24;
2719
2966
  state.sprays.push({
2720
2967
  position: vec3(point.x, point.y, point.z),
2721
- velocity: vec3(Math.cos(angle) * speed * 0.35, 1.1 + Math.random() * 0.8, Math.sin(angle) * speed * 0.25),
2722
- life: 1.2 + Math.random() * 0.4
2968
+ velocity: vec3(Math.cos(angle) * speed * 0.24, 0.46 + Math.random() * 0.34, Math.sin(angle) * speed * 0.18),
2969
+ life: 0.72 + Math.random() * 0.22
2723
2970
  });
2724
2971
  }
2725
2972
  }
@@ -2734,7 +2981,7 @@ function resolveShipRoute(ship, state, radius) {
2734
2981
  }
2735
2982
  const wander = Math.sin(state.time * 0.22 + readVisualNumber(ship.wanderPhase, 0));
2736
2983
  const crossCurrent = Math.cos(state.time * 0.31 + readVisualNumber(ship.wanderPhase, 0));
2737
- const laneCenter = ship.id === "northwind" ? 10.2 + wander * 2.1 + crossCurrent * 0.6 : 7 + wander * 3.3 - crossCurrent * 1.1;
2984
+ const laneCenter = ship.id === "northwind" ? 11.6 + wander * 0.82 + crossCurrent * 0.24 : 5.4 + wander * 0.94 - crossCurrent * 0.32;
2738
2985
  const targetX = ship.routeDirection > 0 ? HARBOR_BOUNDS.maxX - radius * 1.7 : HARBOR_BOUNDS.minX + radius * 1.7;
2739
2986
  return vec3(targetX, 0, clamp(laneCenter, HARBOR_BOUNDS.minZ + 1.8, HARBOR_BOUNDS.maxZ - 1.8));
2740
2987
  }
@@ -2747,7 +2994,7 @@ function updateShipMotion(state, ship, dt, shipModel) {
2747
2994
  const angularDamping = readPhysicsNumber(physics, "angularDamping", 0.08);
2748
2995
  const throttleResponse = readVisualNumber(ship.throttleResponse, 0.58);
2749
2996
  const rudderResponse = readVisualNumber(ship.rudderResponse, 0.62);
2750
- const cruiseSpeed = readVisualNumber(ship.cruiseSpeed, 2.4);
2997
+ const cruiseSpeed = readVisualNumber(ship.cruiseSpeed, 1.25);
2751
2998
  ship.collisionCooldown = Math.max(0, readVisualNumber(ship.collisionCooldown, 0) - dt);
2752
2999
  const forward = directionFromYaw(ship.rotationY);
2753
3000
  const lateral = perpendicularOnWater(forward);
@@ -2842,7 +3089,7 @@ function resolveShipCollision(state, a, b, shipModelA, shipModelB) {
2842
3089
  b.position = addVec3(b.position, scaleVec3(correction, invMassB));
2843
3090
  const relativeVelocity = subVec3(b.velocity, a.velocity);
2844
3091
  const velocityAlongNormal = dotVec3(relativeVelocity, normal);
2845
- const restitution = (readPhysicsNumber(shipModelA.physics, "restitution", 0.22) + readPhysicsNumber(shipModelB.physics, "restitution", 0.22)) / 2 * 0.88;
3092
+ const restitution = (readPhysicsNumber(shipModelA.physics, "restitution", 0.22) + readPhysicsNumber(shipModelB.physics, "restitution", 0.22)) / 2 * 0.42;
2846
3093
  if (velocityAlongNormal < 0) {
2847
3094
  const impulseMagnitude = -(1 + restitution) * velocityAlongNormal / Math.max(1e-4, invMassSum);
2848
3095
  const impulse = scaleVec3(normal, impulseMagnitude);
@@ -2860,27 +3107,27 @@ function resolveShipCollision(state, a, b, shipModelA, shipModelB) {
2860
3107
  a.angularVelocity -= tangentSpeed * radiusA * getShipInverseInertia(a, shipModelA) * 0.2 + impulseMagnitude * 24e-5;
2861
3108
  b.angularVelocity += tangentSpeed * radiusB * getShipInverseInertia(b, shipModelB) * 0.2 + impulseMagnitude * 24e-5;
2862
3109
  const impactSpeed = Math.abs(velocityAlongNormal);
2863
- if (impactSpeed > 0.18 && Math.max(readVisualNumber(a.collisionCooldown, 0), readVisualNumber(b.collisionCooldown, 0)) <= 0) {
3110
+ if (impactSpeed > 0.36 && Math.max(readVisualNumber(a.collisionCooldown, 0), readVisualNumber(b.collisionCooldown, 0)) <= 0) {
2864
3111
  const contactPoint = vec3(
2865
3112
  (a.position.x + b.position.x) * 0.5,
2866
3113
  (a.position.y + b.position.y) * 0.5 + 0.14,
2867
3114
  (a.position.z + b.position.z) * 0.5
2868
3115
  );
2869
- spawnSpray(state, contactPoint, impactSpeed * 2.4 + penetration * 8);
3116
+ spawnSpray(state, contactPoint, impactSpeed * 0.9 + penetration * 2.4);
2870
3117
  state.waveImpulses.push({
2871
3118
  x: contactPoint.x,
2872
3119
  z: contactPoint.z,
2873
- strength: clamp(0.24 + impactSpeed * 0.46 + penetration * 0.9, 0.2, 1.7),
2874
- radius: 0.9 + penetration * 1.4,
3120
+ strength: clamp(0.1 + impactSpeed * 0.18 + penetration * 0.28, 0.08, 0.52),
3121
+ radius: 0.72 + penetration * 0.72,
2875
3122
  life: 1
2876
3123
  });
2877
3124
  state.collisionCount += 1;
2878
3125
  state.collisionFlash = Math.max(
2879
3126
  state.collisionFlash,
2880
- clamp(impactSpeed * 0.55 + penetration * 1.8, 0.16, 1)
3127
+ clamp(impactSpeed * 0.14 + penetration * 0.32, 0.04, 0.24)
2881
3128
  );
2882
- a.collisionCooldown = 0.2;
2883
- b.collisionCooldown = 0.2;
3129
+ a.collisionCooldown = 0.72;
3130
+ b.collisionCooldown = 0.72;
2884
3131
  }
2885
3132
  }
2886
3133
  state.contactCount += 1;
@@ -2903,7 +3150,7 @@ function updateShips(state, dt, shipModel) {
2903
3150
  collided = resolveShipCollision(state, shipA, shipB, shipModelA, shipModelB) || collided;
2904
3151
  }
2905
3152
  }
2906
- state.collisionFlash = collided ? Math.max(0.12, state.collisionFlash) : Math.max(0, state.collisionFlash - dt * 1.3);
3153
+ state.collisionFlash = collided ? Math.max(0.04, state.collisionFlash) : Math.max(0, state.collisionFlash - dt * 1.7);
2907
3154
  }
2908
3155
  function updateWaveImpulses(state, dt) {
2909
3156
  state.waveImpulses = state.waveImpulses.map((impulse) => ({
@@ -3265,7 +3512,8 @@ function renderWaterMotionEffects(ctx, effects, camera, viewport) {
3265
3512
  for (const wake of effects.wakeTrails) {
3266
3513
  const projected = wake.points.map((point) => ({
3267
3514
  projected: projectPoint(point.center, camera, viewport),
3268
- width: point.width
3515
+ width: point.width,
3516
+ foam: point.foam
3269
3517
  })).filter((entry) => entry.projected);
3270
3518
  if (projected.length < 2) {
3271
3519
  continue;
@@ -3273,8 +3521,8 @@ function renderWaterMotionEffects(ctx, effects, camera, viewport) {
3273
3521
  const averageDepth = projected.reduce((total, entry) => total + entry.projected.depth, 0) / projected.length;
3274
3522
  const averageWidth = projected.reduce((total, entry) => total + entry.width, 0) / projected.length;
3275
3523
  const baseWidth = clamp(averageWidth / Math.max(0.25, averageDepth) * 180, 1.6, 5.4);
3276
- ctx.strokeStyle = `rgba(146, 194, 236, ${wake.opacity * 0.52})`;
3277
- ctx.lineWidth = baseWidth * 1.9;
3524
+ ctx.strokeStyle = `rgba(146, 194, 236, ${wake.opacity * 0.34})`;
3525
+ ctx.lineWidth = baseWidth * 1.45;
3278
3526
  ctx.lineCap = "round";
3279
3527
  ctx.lineJoin = "round";
3280
3528
  ctx.beginPath();
@@ -3283,8 +3531,8 @@ function renderWaterMotionEffects(ctx, effects, camera, viewport) {
3283
3531
  ctx.lineTo(projected[index].projected.x, projected[index].projected.y);
3284
3532
  }
3285
3533
  ctx.stroke();
3286
- ctx.strokeStyle = `rgba(234, 247, 255, ${wake.opacity})`;
3287
- ctx.lineWidth = baseWidth;
3534
+ ctx.strokeStyle = `rgba(234, 247, 255, ${wake.opacity * 0.72})`;
3535
+ ctx.lineWidth = baseWidth * 0.72;
3288
3536
  ctx.lineCap = "round";
3289
3537
  ctx.lineJoin = "round";
3290
3538
  ctx.beginPath();
@@ -3294,13 +3542,14 @@ function renderWaterMotionEffects(ctx, effects, camera, viewport) {
3294
3542
  }
3295
3543
  ctx.stroke();
3296
3544
  for (const entry of projected.slice(1, 5)) {
3297
- ctx.fillStyle = `rgba(239, 248, 255, ${wake.opacity * 0.76})`;
3545
+ const foam = entry.foam ?? 0.3;
3546
+ ctx.fillStyle = `rgba(239, 248, 255, ${wake.opacity * foam * 0.92})`;
3298
3547
  ctx.beginPath();
3299
3548
  ctx.ellipse(
3300
3549
  entry.projected.x,
3301
3550
  entry.projected.y,
3302
- baseWidth * 0.72,
3303
- baseWidth * 0.44,
3551
+ baseWidth * 0.54,
3552
+ baseWidth * 0.28,
3304
3553
  0,
3305
3554
  0,
3306
3555
  Math.PI * 2
@@ -3318,13 +3567,22 @@ function renderWaterMotionEffects(ctx, effects, camera, viewport) {
3318
3567
  const radiusX = Math.hypot(xAxis.x - center.x, xAxis.y - center.y);
3319
3568
  const radiusY = Math.hypot(zAxis.x - center.x, zAxis.y - center.y);
3320
3569
  ctx.strokeStyle = `rgba(216, 235, 255, ${ring.opacity})`;
3321
- ctx.lineWidth = clamp((radiusX + radiusY) * 0.02, 1, 3.1);
3322
- ctx.beginPath();
3323
- ctx.ellipse(center.x, center.y, radiusX, radiusY, 0, 0, Math.PI * 2);
3324
- ctx.stroke();
3570
+ ctx.lineWidth = clamp((radiusX + radiusY) * 0.014, 0.65, 1.8);
3571
+ for (let segment = 0; segment < 5; segment += 1) {
3572
+ if (pseudoRandom(segment * 31 + radiusX * 0.7 + radiusY * 0.3) < 0.32) {
3573
+ continue;
3574
+ }
3575
+ const startAngle = segment * 1.22 + stateTimePhase(center.x, center.y) * 0.04;
3576
+ ctx.beginPath();
3577
+ ctx.ellipse(center.x, center.y, radiusX, radiusY, 0, startAngle, startAngle + 0.48);
3578
+ ctx.stroke();
3579
+ }
3325
3580
  }
3326
3581
  ctx.restore();
3327
3582
  }
3583
+ function stateTimePhase(x, y) {
3584
+ return Math.sin(x * 0.013 + y * 0.017);
3585
+ }
3328
3586
  function renderScene(ctx, canvas, state, shipModel, dom, lightingFeatures, fluidFeatures, clothFeatures) {
3329
3587
  const viewport = { width: canvas.width, height: canvas.height };
3330
3588
  const camera = buildCamera(state, canvas);
@@ -3392,6 +3650,7 @@ function renderScene(ctx, canvas, state, shipModel, dom, lightingFeatures, fluid
3392
3650
  }
3393
3651
  }
3394
3652
  const waterMotionEffects = buildWaterMotionEffects(state);
3653
+ const shorelineFoamSegments = buildShorelineFoamSegments(state);
3395
3654
  const lightSources = collectSceneLightSources(state, visuals);
3396
3655
  pushHarborGeometry(camera, viewport, sceneTriangles, state);
3397
3656
  const cloth = buildClothSurface(
@@ -3463,6 +3722,7 @@ function renderScene(ctx, canvas, state, shipModel, dom, lightingFeatures, fluid
3463
3722
  }
3464
3723
  renderWaterMotionEffects(ctx, waterMotionEffects, camera, viewport);
3465
3724
  renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
3725
+ renderShorelineFoamSegments(ctx, shorelineFoamSegments, camera, viewport);
3466
3726
  drawTriangles(
3467
3727
  ctx,
3468
3728
  sceneTriangles,
@@ -3559,7 +3819,7 @@ function updateSceneState(state, dt, shipModel, featureAdapters) {
3559
3819
  advanceShowcaseClothSimulationState(clothState, {
3560
3820
  dt,
3561
3821
  time: state.time,
3562
- flagMotion: readVisualNumber(state.demoVisuals?.flagMotion, 0.92),
3822
+ flagMotion: readVisualNumber(state.demoVisuals?.flagMotion, 0.58),
3563
3823
  waveInfluence: sampleWave(state, FLAG_LAYOUT.origin.x + FLAG_LAYOUT.width * 0.55, FLAG_LAYOUT.origin.z + FLAG_LAYOUT.width * 0.48, state.time)
3564
3824
  });
3565
3825
  updatePhysicsSnapshot(state, shipModel, featureAdapters.physics);
@@ -3776,6 +4036,8 @@ function updatePhysicsSnapshot(state, shipModel, physicsFeatures) {
3776
4036
  }
3777
4037
  export {
3778
4038
  advanceShowcaseClothSimulationState as __testOnlyAdvanceShowcaseClothSimulationState,
4039
+ buildClothSurface as __testOnlyBuildClothSurface,
4040
+ buildShorelineFoamSegments as __testOnlyBuildShorelineFoamSegments,
3779
4041
  buildWaterBands as __testOnlyBuildWaterBands,
3780
4042
  buildWaterMotionEffects as __testOnlyBuildWaterMotionEffects,
3781
4043
  collectSceneLightSources as __testOnlyCollectSceneLightSources,
@@ -3783,4 +4045,4 @@ export {
3783
4045
  mountGpuShowcase,
3784
4046
  showcaseFocusModes
3785
4047
  };
3786
- //# sourceMappingURL=showcase-runtime-OH3H6ZW2.js.map
4048
+ //# sourceMappingURL=showcase-runtime-INRAPCXW.js.map