@call-me-sensei/toonlab 0.2.0 → 0.3.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.
Files changed (205) hide show
  1. package/AGENTS.md +127 -4
  2. package/ATTRIBUTION.md +41 -3
  3. package/README.md +234 -34
  4. package/docs/characters.md +144 -0
  5. package/docs/debug-panel.md +126 -0
  6. package/docs/docs.css +589 -0
  7. package/docs/environment.md +186 -0
  8. package/docs/getting-started.md +180 -0
  9. package/docs/index.html +14 -0
  10. package/docs/lab-architecture.md +100 -0
  11. package/docs/lighting.md +825 -0
  12. package/docs/main.jsx +739 -0
  13. package/docs/mcp.md +97 -0
  14. package/docs/post-processing.md +98 -0
  15. package/docs/settings-reference.md +1878 -0
  16. package/docs/shader-constants.md +77 -0
  17. package/docs/sky.md +182 -0
  18. package/docs/style-labs.md +309 -0
  19. package/docs/texture-lab.md +135 -0
  20. package/docs/toon-shading.md +183 -0
  21. package/docs/tsl-conventions.md +167 -0
  22. package/docs/vegetation-sky.md +275 -0
  23. package/docs/water.md +430 -0
  24. package/docs/weather.md +200 -0
  25. package/docs/world-scale.md +111 -0
  26. package/mcp/server.mjs +610 -0
  27. package/mcp/style-lab-tools.mjs +371 -0
  28. package/mcp/vite-plugin.mjs +174 -0
  29. package/mcp/workspace.mjs +397 -0
  30. package/package.json +64 -5
  31. package/src/ambientfx/INTEGRATION.md +164 -0
  32. package/src/ambientfx/ambientFxPresets.js +83 -0
  33. package/src/ambientfx/ambientFxSettings.js +368 -0
  34. package/src/ambientfx/emitters.js +169 -0
  35. package/src/ambientfx/index.js +5 -0
  36. package/src/ambientfx/particleBackbone.js +493 -0
  37. package/src/ambientfx/stylizedAmbientFx.js +450 -0
  38. package/src/assetlib/ambientcg.js +117 -0
  39. package/src/assetlib/assetRef.js +91 -0
  40. package/src/assetlib/importedEntry.js +40 -0
  41. package/src/assetlib/index.js +23 -0
  42. package/src/assetlib/kaykit.js +192 -0
  43. package/src/assetlib/kaykitStaticIndex.js +700 -0
  44. package/src/assetlib/loadImported.js +143 -0
  45. package/src/assetlib/opensource3d.js +113 -0
  46. package/src/assetlib/polyhaven.js +182 -0
  47. package/src/assetlib/polypizza.js +115 -0
  48. package/src/assetlib/smithsonian.js +214 -0
  49. package/src/assetlib/sources.js +279 -0
  50. package/src/assetlib/zip.js +58 -0
  51. package/src/biome/biomeGenerator.js +385 -0
  52. package/src/biome/biomeRuntime.js +299 -0
  53. package/src/biome/index.js +2 -0
  54. package/src/buildinggen/buildingAsset.js +44 -0
  55. package/src/buildinggen/buildingGrammar.js +311 -0
  56. package/src/buildinggen/buildingMesh.js +451 -0
  57. package/src/buildinggen/buildingPresets.js +46 -0
  58. package/src/buildinggen/buildingRecipe.js +100 -0
  59. package/src/buildinggen/buildingSettings.js +238 -0
  60. package/src/buildinggen/index.js +6 -0
  61. package/src/camera/cameraDirector.js +157 -0
  62. package/src/camera/cameraGenerator.js +367 -0
  63. package/src/camera/cameraRig.js +570 -0
  64. package/src/camera/cameraSettings.js +236 -0
  65. package/src/camera/index.js +7 -0
  66. package/src/catalog/builtinEntries.js +245 -0
  67. package/src/catalog/catalog.js +210 -0
  68. package/src/catalog/index.js +3 -0
  69. package/src/catalog/manifest.js +84 -0
  70. package/src/core/generation.js +529 -0
  71. package/src/environment/environmentRigs.js +21 -1
  72. package/src/environment/environmentSettings.js +4 -0
  73. package/src/environment/environmentSunShadowPass.js +8 -0
  74. package/src/fauna/INTEGRATION.md +174 -0
  75. package/src/fauna/boids.js +861 -0
  76. package/src/fauna/faunaBodies.js +492 -0
  77. package/src/fauna/faunaPresets.js +52 -0
  78. package/src/fauna/faunaSettings.js +525 -0
  79. package/src/fauna/index.js +5 -0
  80. package/src/fauna/stylizedFauna.js +395 -0
  81. package/src/game-feel/gameFeelGenerator.js +402 -0
  82. package/src/game-feel/gameFeelRuntime.js +549 -0
  83. package/src/game-feel/index.js +2 -0
  84. package/src/index.js +17 -6
  85. package/src/lighting/colorIntensity.js +177 -0
  86. package/src/lighting/index.js +161 -0
  87. package/src/lighting/lightDescriptors.js +249 -0
  88. package/src/lighting/lightingCapabilities.js +79 -0
  89. package/src/lighting/lightingDocuments.js +247 -0
  90. package/src/lighting/lightingFixtures.js +446 -0
  91. package/src/lighting/lightingGenerator.js +449 -0
  92. package/src/lighting/lightingPresets.js +319 -0
  93. package/src/lighting/lightingRuntime.js +723 -0
  94. package/src/lighting/lightingStyle.js +386 -0
  95. package/src/lighting/lightingSystem.js +774 -0
  96. package/src/lighting/unrealExport.js +186 -0
  97. package/src/lighting/utils.js +87 -0
  98. package/src/motion/index.js +5 -0
  99. package/src/motion/motionClip.js +441 -0
  100. package/src/motion/motionController.js +628 -0
  101. package/src/motion/motionDocuments.js +225 -0
  102. package/src/motion/motionGraph.js +307 -0
  103. package/src/motion/motionSettings.js +222 -0
  104. package/src/pathgen/index.js +7 -0
  105. package/src/pathgen/pathBridge.js +232 -0
  106. package/src/pathgen/pathPresets.js +35 -0
  107. package/src/pathgen/pathRibbon.js +410 -0
  108. package/src/pathgen/pathRouter.js +380 -0
  109. package/src/pathgen/pathSettings.js +335 -0
  110. package/src/pathgen/pathTextures.js +123 -0
  111. package/src/pathgen/stylizedPaths.js +453 -0
  112. package/src/post/index.js +1 -0
  113. package/src/post/postGenerator.js +177 -0
  114. package/src/post/postProcessing.js +41 -0
  115. package/src/propgen/generatorsWave1.js +379 -0
  116. package/src/propgen/generatorsWave2.js +462 -0
  117. package/src/propgen/index.js +5 -0
  118. package/src/propgen/propAsset.js +323 -0
  119. package/src/propgen/propParts.js +170 -0
  120. package/src/propgen/propPlacement.js +459 -0
  121. package/src/propgen/propPresets.js +82 -0
  122. package/src/propgen/propSettings.js +395 -0
  123. package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
  124. package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
  125. package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
  126. package/src/shaders-tsl/chunks/water-waves.js +90 -11
  127. package/src/shaders-tsl/environment.js +15 -1
  128. package/src/shaders-tsl/flower.js +279 -30
  129. package/src/shaders-tsl/grass.js +60 -33
  130. package/src/shaders-tsl/sky.js +125 -31
  131. package/src/shaders-tsl/tree-leaf.js +61 -25
  132. package/src/shaders-tsl/water-breaker.js +7 -4
  133. package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
  134. package/src/shaders-tsl/water.js +439 -49
  135. package/src/shaders-tsl/woody-surface.js +154 -0
  136. package/src/sky/sceneOverrideLayers.js +10 -0
  137. package/src/sky/skyQuality.js +26 -0
  138. package/src/sky/stylizedSky.js +753 -45
  139. package/src/soundscape/index.js +4 -0
  140. package/src/soundscape/soundscapeGenerator.js +179 -0
  141. package/src/soundscape/soundscapeRuntime.js +806 -0
  142. package/src/soundscape/soundscapeSettings.js +292 -0
  143. package/src/styles/index.js +13 -0
  144. package/src/styles/styleBundle.js +325 -0
  145. package/src/stylizedTerrain.js +32 -2
  146. package/src/stylizedWorld.js +423 -20
  147. package/src/texgen/evaluateTexture.js +675 -0
  148. package/src/texgen/index.js +60 -0
  149. package/src/texgen/noise2.js +210 -0
  150. package/src/texgen/textureAi.js +436 -0
  151. package/src/texgen/textureGenerators.js +516 -0
  152. package/src/texgen/texturePresets.js +490 -0
  153. package/src/texgen/textureSettings.js +342 -0
  154. package/src/texgen/textureThree.js +59 -0
  155. package/src/vegetation/flowerSpecies.js +15 -3
  156. package/src/vegetation/grassPalettes.js +153 -0
  157. package/src/vegetation/index.js +6 -0
  158. package/src/vegetation/stylizedBush.js +2 -0
  159. package/src/vegetation/stylizedFlower.js +82 -0
  160. package/src/vegetation/stylizedFlowers.js +48 -7
  161. package/src/vegetation/stylizedForest.js +29 -1
  162. package/src/vegetation/stylizedGrass.js +291 -56
  163. package/src/vegetation/stylizedTree.js +38 -2
  164. package/src/vegetation/stylizedTreeFoliage.js +2 -1
  165. package/src/vegetation/vegetationShaders.js +1110 -0
  166. package/src/vfxgen/INTEGRATION.md +145 -0
  167. package/src/vfxgen/core/burstBackbone.js +380 -0
  168. package/src/vfxgen/core/projectileCore.js +92 -0
  169. package/src/vfxgen/core/spriteShapes.js +98 -0
  170. package/src/vfxgen/core/trailRibbon.js +272 -0
  171. package/src/vfxgen/core/vfxRandom.js +29 -0
  172. package/src/vfxgen/effects/emitHelpers.js +37 -0
  173. package/src/vfxgen/effects/magicEffects.js +162 -0
  174. package/src/vfxgen/effects/movementEffects.js +87 -0
  175. package/src/vfxgen/effects/weaponEffects.js +118 -0
  176. package/src/vfxgen/index.js +18 -0
  177. package/src/vfxgen/moves/moveController.js +146 -0
  178. package/src/vfxgen/moves/moveLibrary.js +335 -0
  179. package/src/vfxgen/vfxPresets.js +98 -0
  180. package/src/vfxgen/vfxSettings.js +384 -0
  181. package/src/vfxgen/vfxSystem.js +449 -0
  182. package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
  183. package/src/villagegen/index.js +4 -0
  184. package/src/villagegen/stylizedVillage.js +490 -0
  185. package/src/villagegen/villageArchetypes.js +160 -0
  186. package/src/villagegen/villageNames.js +40 -0
  187. package/src/villagegen/villageSites.js +105 -0
  188. package/src/water/sceneOverrideLayers.js +23 -0
  189. package/src/water/water.js +5 -0
  190. package/src/water/waterBreakerSystem.js +15 -1
  191. package/src/water/waterCurrentField.js +447 -0
  192. package/src/water/waterMaterial.js +12 -0
  193. package/src/water/waterNearshorePhase.js +320 -0
  194. package/src/water/waterScenePasses.js +83 -30
  195. package/src/water/waterSettings.js +325 -13
  196. package/src/water/waterShoreMaterial.js +322 -0
  197. package/src/water/waterShoreStateField.js +605 -0
  198. package/src/water/waterSurface.js +797 -28
  199. package/src/weather/index.js +6 -0
  200. package/src/weather/weatherPrecipitation.js +221 -0
  201. package/src/weather/weatherPresets.js +258 -0
  202. package/src/weather/weatherSettings.js +269 -0
  203. package/src/weather/weatherSystem.js +871 -0
  204. package/src/worldMinimap.js +62 -0
  205. package/src/worldPresets.js +4 -1
@@ -0,0 +1,6 @@
1
+ // Weather barrel. Import from '@call-me-sensei/toonlab/weather'.
2
+ export * from './weatherSettings.js';
3
+ export * from './weatherPresets.js';
4
+ export * from './weatherPrecipitation.js';
5
+ export * from './weatherSystem.js';
6
+
@@ -0,0 +1,221 @@
1
+ // One GPU-looping precipitation draw for rain, snow, sleet, hail, and dust.
2
+ // Static seeds are uploaded once; all trajectories are derived from time in
3
+ // TSL, so weather intensity only changes the instance count and uniforms.
4
+
5
+ import * as THREE from 'three';
6
+ import {
7
+ abs,
8
+ attribute,
9
+ cameraProjectionMatrix,
10
+ cameraViewMatrix,
11
+ clamp,
12
+ Discard,
13
+ float,
14
+ Fn,
15
+ fract,
16
+ length,
17
+ max,
18
+ mix,
19
+ mod,
20
+ positionGeometry,
21
+ sin,
22
+ smoothstep,
23
+ step,
24
+ uniform,
25
+ uv,
26
+ varying,
27
+ vec2,
28
+ vec3,
29
+ vec4,
30
+ viewportSize,
31
+ } from 'three/tsl';
32
+ import { NodeMaterial } from 'three/webgpu';
33
+
34
+ import { createWeatherSettings } from './weatherSettings.js';
35
+
36
+ export const WEATHER_PRECIPITATION_KIND = Object.freeze({
37
+ rain: 0,
38
+ snow: 1,
39
+ sleet: 2,
40
+ hail: 3,
41
+ dust: 4,
42
+ none: 0,
43
+ });
44
+
45
+ function mulberry32(seed) {
46
+ let state = (Math.trunc(seed) || 1) >>> 0;
47
+ return () => {
48
+ state = (state + 0x6d2b79f5) >>> 0;
49
+ let value = state;
50
+ value = Math.imul(value ^ (value >>> 15), value | 1);
51
+ value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
52
+ return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
53
+ };
54
+ }
55
+
56
+ export function createWeatherPrecipitationMaterial(settings = {}) {
57
+ const normalized = createWeatherSettings({ precipitation: settings }).precipitation;
58
+ const u = {
59
+ uArea: uniform(new THREE.Vector2(normalized.areaSize * 0.5, normalized.areaSize * 0.5)),
60
+ uCenter: uniform(new THREE.Vector3()),
61
+ uColor: uniform(new THREE.Color().setRGB(...normalized.color, THREE.SRGBColorSpace)),
62
+ uFallHeight: uniform(normalized.fallHeight),
63
+ uKind: uniform(WEATHER_PRECIPITATION_KIND[normalized.type] ?? 0),
64
+ uOpacity: uniform(normalized.opacity),
65
+ uPixelRatio: uniform(1),
66
+ uPointScale: uniform(540),
67
+ uSize: uniform(normalized.size),
68
+ uSpeed: uniform(normalized.speed),
69
+ uStreakLength: uniform(normalized.streakLength),
70
+ uTime: uniform(0),
71
+ uWind: uniform(new THREE.Vector2()),
72
+ uWindStrength: uniform(0),
73
+ };
74
+
75
+ const material = new NodeMaterial();
76
+ material.name = 'WeatherPrecipitation';
77
+ material.transparent = true;
78
+ material.depthWrite = false;
79
+ material.fog = false;
80
+ material.lights = false;
81
+ material.side = THREE.DoubleSide;
82
+
83
+ const vFade = varying(float(), 'vWeatherFade');
84
+ const vKind = varying(float(), 'vWeatherKind');
85
+
86
+ material.vertexNode = Fn(() => {
87
+ const seed = attribute('aWeatherSeed', 'vec4');
88
+ const kind = u.uKind.toVar();
89
+ const isSnow = step(0.5, kind).mul(step(kind, 1.49)).toVar();
90
+ const isSleet = step(1.5, kind).mul(step(kind, 2.49)).toVar();
91
+ const isHail = step(2.5, kind).mul(step(kind, 3.49)).toVar();
92
+ const isDust = step(3.5, kind).toVar();
93
+ const fallSpeed = u.uSpeed.mul(seed.w.mul(0.45).add(0.78)).toVar();
94
+ const cycle = u.uFallHeight.div(max(fallSpeed, 0.01)).toVar();
95
+ const time = mod(u.uTime.add(seed.z.mul(cycle)), cycle).toVar();
96
+ const fallingY = u.uFallHeight.sub(fallSpeed.mul(time)).toVar();
97
+ const risingY = mod(seed.z.mul(u.uFallHeight).add(time.mul(fallSpeed).mul(0.35)), u.uFallHeight);
98
+ const y = mix(fallingY, risingY, isDust).toVar();
99
+
100
+ const wind = u.uWind.mul(u.uWindStrength).toVar();
101
+ const xz = seed.xy.mul(2).sub(1).mul(u.uArea).add(wind.mul(time)).toVar();
102
+ const swirlPhase = u.uTime.mul(mix(0.5, 2.2, seed.w)).add(seed.z.mul(41)).toVar();
103
+ const snowSway = vec2(sin(swirlPhase), sin(swirlPhase.mul(0.73).add(2.1)))
104
+ .mul(u.uSize.mul(5)).mul(isSnow.add(isDust.mul(1.7)));
105
+ xz.addAssign(snowSway);
106
+ xz.assign(mod(xz.add(u.uArea), u.uArea.mul(2)).sub(u.uArea));
107
+
108
+ const worldPosition = u.uCenter.add(vec3(xz.x, y, xz.y)).toVar();
109
+ const mvPosition = cameraViewMatrix.mul(vec4(worldPosition, 1)).toVar();
110
+ vFade.assign(smoothstep(0, 0.7, y)
111
+ .mul(smoothstep(u.uFallHeight.mul(0.82), u.uFallHeight, y).oneMinus()));
112
+ vKind.assign(kind);
113
+
114
+ const streakKind = clamp(isSleet.add(step(kind, 0.49)), 0, 1);
115
+ const visibleSize = mix(u.uSize, u.uStreakLength, streakKind).toVar();
116
+ const pixels = visibleSize.mul(u.uPointScale).mul(u.uPixelRatio)
117
+ .div(max(mvPosition.z.negate(), 0.5)).toVar();
118
+ const clipPosition = cameraProjectionMatrix.mul(mvPosition).toVar();
119
+ clipPosition.xy.addAssign(
120
+ positionGeometry.xy.mul(pixels).mul(float(2).div(viewportSize)).mul(clipPosition.w),
121
+ );
122
+ return clipPosition;
123
+ })();
124
+
125
+ material.fragmentNode = Fn(() => {
126
+ const point = uv().mul(2).sub(1).toVar();
127
+ const radius = length(point).toVar();
128
+ const bar = smoothstep(0.06, 0.2, abs(point.x)).oneMinus()
129
+ .mul(smoothstep(0.66, 1, abs(point.y)).oneMinus());
130
+ const circle = smoothstep(0.62, 1, radius).oneMinus();
131
+ const axisA = smoothstep(0.05, 0.16, abs(point.x)).oneMinus();
132
+ const axisB = smoothstep(0.05, 0.16, abs(point.y)).oneMinus();
133
+ const diagonal = smoothstep(0.04, 0.14, abs(abs(point.x).sub(abs(point.y)))).oneMinus();
134
+ const flake = clamp(axisA.add(axisB).add(diagonal).mul(circle), 0, 1);
135
+ const dust = smoothstep(0.25, 1, radius).oneMinus().mul(0.5);
136
+
137
+ const isSnow = step(0.5, vKind).mul(step(vKind, 1.49));
138
+ const isSleet = step(1.5, vKind).mul(step(vKind, 2.49));
139
+ const isHail = step(2.5, vKind).mul(step(vKind, 3.49));
140
+ const isDust = step(3.5, vKind);
141
+ let mask = bar.toVar();
142
+ mask.assign(mix(mask, flake, isSnow));
143
+ mask.assign(mix(mask, mix(bar, circle, 0.42), isSleet));
144
+ mask.assign(mix(mask, circle, isHail));
145
+ mask.assign(mix(mask, dust, isDust));
146
+ const alpha = mask.mul(u.uOpacity).mul(vFade).toVar();
147
+ Discard(alpha.lessThan(0.015));
148
+ return vec4(u.uColor, alpha);
149
+ })();
150
+
151
+ material.uniforms = u;
152
+ material.userData.isToonNodeMaterial = true;
153
+ return material;
154
+ }
155
+
156
+ export class WeatherPrecipitation extends THREE.Mesh {
157
+ constructor({ maxParticles = 8000, seed = 1, settings = {} } = {}) {
158
+ const normalized = createWeatherSettings({ precipitation: { ...settings, maxParticles } }).precipitation;
159
+ const capacity = Math.max(100, Math.round(normalized.maxParticles));
160
+ const quad = new THREE.PlaneGeometry(1, 1);
161
+ const geometry = new THREE.InstancedBufferGeometry();
162
+ geometry.index = quad.index;
163
+ geometry.setAttribute('position', quad.attributes.position);
164
+ geometry.setAttribute('uv', quad.attributes.uv);
165
+ const seeds = new Float32Array(capacity * 4);
166
+ const random = mulberry32(seed);
167
+ for (let index = 0; index < capacity * 4; index += 1) seeds[index] = random();
168
+ geometry.setAttribute('aWeatherSeed', new THREE.InstancedBufferAttribute(seeds, 4));
169
+ geometry.instanceCount = 0;
170
+ geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(), 1e6);
171
+ quad.dispose();
172
+
173
+ super(geometry, createWeatherPrecipitationMaterial(normalized));
174
+ this.name = 'WeatherPrecipitation';
175
+ this.capacity = capacity;
176
+ this.frustumCulled = false;
177
+ this.renderOrder = 50;
178
+ this.userData.environmentShaderExclude = true;
179
+ this.userData.waterExclude = true;
180
+ this.settings = normalized;
181
+ this.applySettings(normalized);
182
+ }
183
+
184
+ applySettings(settings = {}, wind = {}) {
185
+ const normalized = createWeatherSettings({ precipitation: { ...this.settings, ...settings } }).precipitation;
186
+ this.settings = normalized;
187
+ const u = this.material.uniforms;
188
+ u.uArea.value.setScalar(normalized.areaSize * 0.5);
189
+ u.uColor.value.setRGB(...normalized.color, THREE.SRGBColorSpace);
190
+ u.uFallHeight.value = normalized.fallHeight;
191
+ u.uKind.value = WEATHER_PRECIPITATION_KIND[normalized.type] ?? 0;
192
+ u.uOpacity.value = normalized.opacity * (0.35 + normalized.intensity * 0.65);
193
+ u.uSize.value = normalized.size;
194
+ u.uSpeed.value = normalized.speed;
195
+ u.uStreakLength.value = normalized.streakLength;
196
+ if (Array.isArray(wind.direction)) u.uWind.value.set(wind.direction[0], wind.direction[1]);
197
+ if (Number.isFinite(wind.strength)) u.uWindStrength.value = wind.strength;
198
+ this.geometry.instanceCount = normalized.type === 'none'
199
+ ? 0
200
+ : Math.min(this.capacity, Math.round(this.capacity * normalized.intensity));
201
+ this.visible = this.geometry.instanceCount > 0;
202
+ return this;
203
+ }
204
+
205
+ update(delta, { center, renderer } = {}) {
206
+ const u = this.material.uniforms;
207
+ u.uTime.value += Math.min(Math.max(Number(delta) || 0.016, 0), 0.1);
208
+ if (center) u.uCenter.value.copy(center);
209
+ if (renderer) {
210
+ u.uPixelRatio.value = renderer.getPixelRatio?.() ?? 1;
211
+ u.uPointScale.value = (renderer.domElement?.clientHeight || 540) * 0.9;
212
+ }
213
+ return this;
214
+ }
215
+
216
+ dispose() {
217
+ this.geometry.dispose();
218
+ this.material.dispose();
219
+ }
220
+ }
221
+
@@ -0,0 +1,258 @@
1
+ import {
2
+ createSettingsPresetDocument,
3
+ parsePresetDocument,
4
+ serializePresetDocument,
5
+ validateSettingsPresetDocument,
6
+ } from '../core/presetDocuments.js';
7
+ import {
8
+ createWeatherSettings,
9
+ mergeWeatherSettings,
10
+ } from './weatherSettings.js';
11
+
12
+ export const WEATHER_PRESET_DOCUMENT_TYPE = 'toonlab/weather-preset';
13
+ export const WEATHER_PRESET_SCHEMA_VERSION = 1;
14
+
15
+ const DEFINITIONS = new Map();
16
+
17
+ function registerBuiltIn(id, label, description, settings) {
18
+ DEFINITIONS.set(id, Object.freeze({ description, label, settings: createWeatherSettings(settings) }));
19
+ }
20
+
21
+ registerBuiltIn('clear', 'Clear', 'Bright sky, light breeze, and sparse cloud shadows.', {
22
+ atmosphere: { cloudCoverage: 0.08, cloudShadowStrength: 0.08 },
23
+ });
24
+
25
+ registerBuiltIn('call_me_sensei', 'Call Me Sensei', 'Studio-managed signature weather: vivid sky, painterly moving clouds, and a cohesive world breeze.', {
26
+ atmosphere: { cloudCoverage: 0.42, cloudShadowCoverage: 0.45, cloudShadowStrength: 0.35, skyDarkening: 0, skyDesaturation: 0 },
27
+ wind: { direction: [1, 0.3], gustFrequency: 0.42, gustSpeed: 1.2, speed: 1, strength: 0.18 },
28
+ });
29
+
30
+ registerBuiltIn('partlyCloudy', 'Partly Cloudy', 'Broken cloud cover with moving pools of sunlight.', {
31
+ atmosphere: { cloudCoverage: 0.48, cloudShadowCoverage: 0.48, cloudShadowStrength: 0.45, sunIntensity: 0.86 },
32
+ wind: { speed: 1.25, strength: 0.22 },
33
+ });
34
+
35
+ registerBuiltIn('cloudy', 'Cloudy', 'Cool, soft daylight under broad cloud cover.', {
36
+ atmosphere: { ambientIntensity: 1.05, cloudCoverage: 0.68, cloudShadowCoverage: 0.62, cloudShadowStrength: 0.55, skyDesaturation: 0.3, sunIntensity: 0.68 },
37
+ wind: { speed: 1.35, strength: 0.24 },
38
+ });
39
+
40
+ registerBuiltIn('overcast', 'Overcast', 'Flat, diffuse light with dense gray cloud and closer haze.', {
41
+ atmosphere: { ambientIntensity: 1.08, cloudCoverage: 0.92, cloudShadowStrength: 0, fogColor: [0.64, 0.7, 0.76], fogRangeScale: 1.55, skyDarkening: 0.18, skyDesaturation: 0.66, sunIntensity: 0.38 },
42
+ wind: { speed: 1.2, strength: 0.2 },
43
+ });
44
+
45
+ registerBuiltIn('haze', 'Haze', 'Warm suspended haze that softens long sightlines without precipitation.', {
46
+ atmosphere: { cloudCoverage: 0.22, fogColor: [0.82, 0.79, 0.7], fogRangeScale: 1.8, skyDesaturation: 0.25, skyTint: [1, 0.94, 0.82], sunIntensity: 0.82 },
47
+ surface: { wetness: 0 },
48
+ });
49
+
50
+ registerBuiltIn('mist', 'Mist', 'Low-contrast humid air with gentle cloud and almost no wind.', {
51
+ atmosphere: { ambientIntensity: 1.08, cloudCoverage: 0.55, cloudShadowStrength: 0.12, fogColor: [0.76, 0.84, 0.88], fogRangeScale: 2.4, skyDesaturation: 0.38, sunIntensity: 0.58 },
52
+ wind: { speed: 0.45, strength: 0.06 },
53
+ surface: { wetness: 0.18 },
54
+ });
55
+
56
+ registerBuiltIn('fog', 'Dense Fog', 'Dense visibility-limiting fog with muted sun and nearly still air.', {
57
+ atmosphere: { ambientIntensity: 1.12, cloudCoverage: 0.72, cloudShadowStrength: 0, fogColor: [0.7, 0.76, 0.78], fogRangeScale: 4.8, skyDarkening: 0.16, skyDesaturation: 0.72, sunIntensity: 0.22 },
58
+ wind: { speed: 0.25, strength: 0.03 },
59
+ surface: { wetness: 0.22 },
60
+ });
61
+
62
+ registerBuiltIn('windy', 'High Wind', 'Fast-moving broken clouds and strong gust response without precipitation.', {
63
+ atmosphere: { cloudCoverage: 0.5, cloudShadowStrength: 0.5, cloudSpeed: 2.4, sunIntensity: 0.82 },
64
+ wind: { gustFrequency: 0.72, gustSpeed: 3.4, speed: 3.4, strength: 0.72 },
65
+ surface: { waterWaveBoost: 0.28 },
66
+ });
67
+
68
+ registerBuiltIn('drizzle', 'Drizzle', 'Fine light rain under a cool, soft sky.', {
69
+ atmosphere: { cloudCoverage: 0.82, cloudShadowStrength: 0.18, fogColor: [0.63, 0.71, 0.78], fogRangeScale: 1.55, skyDesaturation: 0.52, sunIntensity: 0.48 },
70
+ precipitation: { intensity: 0.28, opacity: 0.34, speed: 10, streakLength: 0.2, type: 'rain' },
71
+ surface: { waterRippleRate: 2, wetness: 0.45 },
72
+ wind: { speed: 0.9, strength: 0.14 },
73
+ });
74
+
75
+ registerBuiltIn('rain', 'Rain', 'Steady rain with dim light, wet surfaces, and active water ripples.', {
76
+ atmosphere: { cloudCoverage: 0.94, cloudShadowStrength: 0, fogColor: [0.53, 0.62, 0.7], fogRangeScale: 2.05, skyDarkening: 0.24, skyDesaturation: 0.72, sunIntensity: 0.28 },
77
+ precipitation: { intensity: 0.68, opacity: 0.52, speed: 17, streakLength: 0.45, type: 'rain' },
78
+ surface: { waterRippleRate: 7, waterWaveBoost: 0.08, wetness: 0.8 },
79
+ wind: { speed: 1.5, strength: 0.28 },
80
+ });
81
+
82
+ registerBuiltIn('heavyRain', 'Heavy Rain', 'Dense wind-driven rain and rough water under a dark sky.', {
83
+ atmosphere: { ambientIntensity: 0.96, cloudCoverage: 1, cloudShadowStrength: 0, fogColor: [0.42, 0.5, 0.58], fogRangeScale: 2.7, skyDarkening: 0.38, skyDesaturation: 0.82, skyTint: [0.82, 0.9, 1], sunIntensity: 0.16 },
84
+ precipitation: { areaSize: 52, intensity: 1, opacity: 0.62, speed: 22, streakLength: 0.66, type: 'rain' },
85
+ surface: { waterRippleRate: 14, waterWaveBoost: 0.25, wetness: 1 },
86
+ wind: { gustFrequency: 0.8, gustSpeed: 3.2, speed: 2.8, strength: 0.58 },
87
+ });
88
+
89
+ registerBuiltIn('thunderstorm', 'Thunderstorm', 'Heavy rain, severe gusts, rough water, lightning flashes, and thunder events.', {
90
+ atmosphere: { ambientIntensity: 0.9, cloudCoverage: 1, cloudShadowStrength: 0, fogColor: [0.34, 0.4, 0.5], fogRangeScale: 3.2, skyDarkening: 0.56, skyDesaturation: 0.86, skyTint: [0.72, 0.8, 1], sunIntensity: 0.08 },
91
+ lightning: { enabled: true, intensity: 7, strikesPerMinute: 7 },
92
+ precipitation: { areaSize: 58, intensity: 1, opacity: 0.68, speed: 25, streakLength: 0.8, type: 'rain' },
93
+ surface: { waterRippleRate: 18, waterWaveBoost: 0.52, wetness: 1 },
94
+ wind: { gustFrequency: 1.1, gustSpeed: 4.5, speed: 4, strength: 0.9 },
95
+ });
96
+
97
+ registerBuiltIn('tropicalStorm', 'Tropical Storm', 'Warm torrential rain, extreme wind, dense cloud, and frequent lightning.', {
98
+ atmosphere: { ambientIntensity: 0.96, cloudCoverage: 1, cloudSpeed: 2.2, cloudShadowStrength: 0, fogColor: [0.38, 0.48, 0.5], fogRangeScale: 3.4, skyDarkening: 0.48, skyDesaturation: 0.72, skyTint: [0.72, 0.92, 0.9], sunIntensity: 0.1 },
99
+ lightning: { enabled: true, intensity: 6.5, strikesPerMinute: 5 },
100
+ precipitation: { areaSize: 64, intensity: 1, opacity: 0.7, speed: 27, streakLength: 0.9, type: 'rain' },
101
+ surface: { waterRippleRate: 20, waterWaveBoost: 0.7, wetness: 1 },
102
+ wind: { direction: [1, 0.8], gustFrequency: 1.3, gustSpeed: 5.2, speed: 5.5, strength: 1.25 },
103
+ });
104
+
105
+ registerBuiltIn('snow', 'Snow', 'Gentle snow with quiet wind, cool diffuse light, and early accumulation.', {
106
+ atmosphere: { ambientIntensity: 1.08, cloudCoverage: 0.78, cloudShadowStrength: 0.08, fogColor: [0.78, 0.84, 0.9], fogRangeScale: 1.7, skyDesaturation: 0.42, skyTint: [0.88, 0.94, 1], sunIntensity: 0.56 },
107
+ precipitation: { color: [0.94, 0.97, 1], intensity: 0.52, opacity: 0.82, size: 0.2, speed: 1.35, type: 'snow' },
108
+ surface: { snowCover: 0.45, wetness: 0.15 },
109
+ wind: { speed: 0.7, strength: 0.12 },
110
+ });
111
+
112
+ registerBuiltIn('heavySnow', 'Heavy Snow', 'Dense snowfall, reduced visibility, and deep accumulation.', {
113
+ atmosphere: { ambientIntensity: 1.04, cloudCoverage: 0.96, cloudShadowStrength: 0, fogColor: [0.72, 0.78, 0.84], fogRangeScale: 2.8, skyDarkening: 0.2, skyDesaturation: 0.72, skyTint: [0.84, 0.91, 1], sunIntensity: 0.3 },
114
+ precipitation: { areaSize: 52, color: [0.94, 0.97, 1], intensity: 0.95, opacity: 0.9, size: 0.24, speed: 1.8, type: 'snow' },
115
+ surface: { snowCover: 0.9, wetness: 0.2 },
116
+ wind: { speed: 1.3, strength: 0.28 },
117
+ });
118
+
119
+ registerBuiltIn('blizzard', 'Blizzard', 'Wind-driven snow, whiteout fog, severe gusts, and icy surfaces.', {
120
+ atmosphere: { ambientIntensity: 1.06, cloudCoverage: 1, cloudSpeed: 2.5, cloudShadowStrength: 0, fogColor: [0.78, 0.82, 0.84], fogRangeScale: 5.5, skyDarkening: 0.28, skyDesaturation: 0.9, skyTint: [0.82, 0.9, 1], sunIntensity: 0.14 },
121
+ precipitation: { areaSize: 62, color: [0.94, 0.97, 1], intensity: 1, opacity: 0.92, size: 0.22, speed: 3.2, type: 'snow' },
122
+ surface: { ice: 0.6, snowCover: 1, wetness: 0.08 },
123
+ wind: { direction: [1, 0.7], gustFrequency: 1.25, gustSpeed: 5, speed: 5.2, strength: 1.2 },
124
+ });
125
+
126
+ registerBuiltIn('sleet', 'Sleet', 'Mixed ice pellets and rain with sharp wind and slick surfaces.', {
127
+ atmosphere: { cloudCoverage: 0.98, cloudShadowStrength: 0, fogColor: [0.58, 0.66, 0.74], fogRangeScale: 2.5, skyDarkening: 0.3, skyDesaturation: 0.76, sunIntensity: 0.22 },
128
+ precipitation: { color: [0.82, 0.9, 0.98], intensity: 0.84, opacity: 0.7, size: 0.1, speed: 10, streakLength: 0.26, type: 'sleet' },
129
+ surface: { ice: 0.35, snowCover: 0.18, waterRippleRate: 8, wetness: 0.88 },
130
+ wind: { speed: 2.4, strength: 0.48 },
131
+ });
132
+
133
+ registerBuiltIn('freezingRain', 'Freezing Rain', 'Cold rain that produces heavy wetness and rapid surface icing.', {
134
+ atmosphere: { cloudCoverage: 1, cloudShadowStrength: 0, fogColor: [0.52, 0.61, 0.7], fogRangeScale: 2.4, skyDarkening: 0.32, skyDesaturation: 0.82, sunIntensity: 0.18 },
135
+ precipitation: { color: [0.76, 0.88, 1], intensity: 0.82, opacity: 0.62, speed: 19, streakLength: 0.54, type: 'rain' },
136
+ surface: { ice: 0.9, waterRippleRate: 9, wetness: 1 },
137
+ wind: { speed: 1.7, strength: 0.32 },
138
+ });
139
+
140
+ registerBuiltIn('hail', 'Hail', 'Fast ice pellets, hard impacts, storm light, and rough water.', {
141
+ atmosphere: { ambientIntensity: 0.96, cloudCoverage: 1, cloudShadowStrength: 0, fogColor: [0.44, 0.52, 0.6], fogRangeScale: 2.7, skyDarkening: 0.44, skyDesaturation: 0.82, sunIntensity: 0.12 },
142
+ lightning: { enabled: true, intensity: 5, strikesPerMinute: 3 },
143
+ precipitation: { color: [0.88, 0.94, 1], intensity: 0.9, opacity: 0.88, size: 0.16, speed: 18, type: 'hail' },
144
+ surface: { ice: 0.2, waterRippleRate: 16, waterWaveBoost: 0.28, wetness: 0.75 },
145
+ wind: { gustFrequency: 0.9, gustSpeed: 3.8, speed: 3.3, strength: 0.68 },
146
+ });
147
+
148
+ registerBuiltIn('dustStorm', 'Dust Storm', 'Airborne earth, hot haze, strong wind, and severely reduced visibility.', {
149
+ atmosphere: { ambientIntensity: 0.9, cloudCoverage: 0.72, cloudShadowStrength: 0.12, fogColor: [0.58, 0.42, 0.27], fogRangeScale: 4.3, skyDarkening: 0.36, skyDesaturation: 0.5, skyTint: [1, 0.7, 0.45], sunIntensity: 0.3 },
150
+ precipitation: { color: [0.68, 0.45, 0.24], intensity: 0.82, opacity: 0.34, size: 0.32, speed: 2.4, type: 'dust' },
151
+ wind: { direction: [1, 0.45], gustFrequency: 1, gustSpeed: 4.2, speed: 4.8, strength: 1.1 },
152
+ });
153
+
154
+ registerBuiltIn('sandstorm', 'Sandstorm', 'Extreme wind-driven sand and near-whiteout amber atmosphere.', {
155
+ atmosphere: { ambientIntensity: 0.88, cloudCoverage: 0.84, cloudShadowStrength: 0, fogColor: [0.65, 0.46, 0.25], fogRangeScale: 6.2, skyDarkening: 0.46, skyDesaturation: 0.62, skyTint: [1, 0.62, 0.3], sunIntensity: 0.16 },
156
+ precipitation: { areaSize: 64, color: [0.78, 0.54, 0.26], intensity: 1, opacity: 0.42, size: 0.38, speed: 3.2, type: 'dust' },
157
+ wind: { direction: [1, 0.65], gustFrequency: 1.4, gustSpeed: 5.8, speed: 6.4, strength: 1.55 },
158
+ });
159
+
160
+ const ALIASES = Object.freeze({
161
+ storm: 'thunderstorm',
162
+ snowy: 'snow',
163
+ rainy: 'rain',
164
+ cloudyDay: 'cloudy',
165
+ clearDay: 'clear',
166
+ });
167
+
168
+ export function normalizeWeatherPresetName(name) {
169
+ const requested = String(name ?? '').trim();
170
+ const id = ALIASES[requested] ?? requested;
171
+ return DEFINITIONS.has(id) ? id : 'call_me_sensei';
172
+ }
173
+
174
+ export function getWeatherPresetOptions() {
175
+ return Array.from(DEFINITIONS.entries()).map(([id, definition]) => ({
176
+ description: definition.description,
177
+ id,
178
+ label: definition.label,
179
+ }));
180
+ }
181
+
182
+ export function resolveWeatherPreset(name) {
183
+ const id = normalizeWeatherPresetName(name);
184
+ const definition = DEFINITIONS.get(id);
185
+ return {
186
+ description: definition.description,
187
+ id,
188
+ label: definition.label,
189
+ settings: createWeatherSettings(definition.settings),
190
+ };
191
+ }
192
+
193
+ export function registerWeatherPreset(name, definition = {}, { overwrite = false } = {}) {
194
+ const id = String(name ?? '').trim();
195
+ if (!id) throw new Error('Weather preset name is required.');
196
+ if (!overwrite && DEFINITIONS.has(id)) throw new Error(`Weather preset "${id}" already exists.`);
197
+ const source = definition && typeof definition === 'object' ? definition : {};
198
+ const settings = source.settings ?? source;
199
+ DEFINITIONS.set(id, Object.freeze({
200
+ description: String(source.description ?? ''),
201
+ label: String(source.label ?? id),
202
+ settings: createWeatherSettings(settings),
203
+ }));
204
+ return { description: String(source.description ?? ''), id, label: String(source.label ?? id) };
205
+ }
206
+
207
+ function migrateWeatherDocument(document) {
208
+ return {
209
+ ...document,
210
+ type: document.type ?? WEATHER_PRESET_DOCUMENT_TYPE,
211
+ version: Number(document.version ?? document.schemaVersion ?? WEATHER_PRESET_SCHEMA_VERSION),
212
+ };
213
+ }
214
+
215
+ export function validateWeatherPresetDocument(input) {
216
+ return validateSettingsPresetDocument(input, {
217
+ documentType: WEATHER_PRESET_DOCUMENT_TYPE,
218
+ migrateDocument: migrateWeatherDocument,
219
+ normalizeId: (id) => String(id ?? '').trim(),
220
+ sanitizeSettings: createWeatherSettings,
221
+ schemaVersion: WEATHER_PRESET_SCHEMA_VERSION,
222
+ });
223
+ }
224
+
225
+ export function parseWeatherPresetDocument(input) {
226
+ return parsePresetDocument(input, validateWeatherPresetDocument, { invalidJsonLabel: 'weather preset' });
227
+ }
228
+
229
+ export function createWeatherPresetDocument(id, definition = {}) {
230
+ return createSettingsPresetDocument(id, definition, {
231
+ collectSettings: (source) => createWeatherSettings(source.settings ?? source),
232
+ documentType: WEATHER_PRESET_DOCUMENT_TYPE,
233
+ schemaVersion: WEATHER_PRESET_SCHEMA_VERSION,
234
+ validateDocument: validateWeatherPresetDocument,
235
+ });
236
+ }
237
+
238
+ export function serializeWeatherPresetDocument(idOrDocument, definition = {}, pretty = true) {
239
+ return serializePresetDocument(idOrDocument, definition, {
240
+ argumentCount: arguments.length,
241
+ createDocument: createWeatherPresetDocument,
242
+ pretty,
243
+ });
244
+ }
245
+
246
+ export function registerWeatherPresetDocument(input, { overwrite = false } = {}) {
247
+ const result = parseWeatherPresetDocument(input);
248
+ if (!result.ok) return result;
249
+ const { id, ...definition } = result.value;
250
+ registerWeatherPreset(id, definition, { overwrite });
251
+ return result;
252
+ }
253
+
254
+ /** Resolves a preset and merges developer overrides over it. */
255
+ export function resolveWeatherSettings(preset = 'call_me_sensei', overrides = {}) {
256
+ return mergeWeatherSettings(resolveWeatherPreset(preset).settings, overrides);
257
+ }
258
+