@nayan-ui/engine 0.0.0-stage → 1.0.0-beta.3

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 (189) hide show
  1. package/LICENSE +21 -0
  2. package/NayanEngine.podspec +28 -0
  3. package/README.md +210 -2
  4. package/lib/module/assets/AssetBytesCache.js +76 -0
  5. package/lib/module/assets/AssetCache.js +19 -0
  6. package/lib/module/assets/AssetLoader.js +52 -0
  7. package/lib/module/assets/CacheGeneration.js +7 -0
  8. package/lib/module/assets/IndexedDbCache.web.js +149 -0
  9. package/lib/module/assets/ProceduralCache.js +222 -0
  10. package/lib/module/assets/font.js +136 -0
  11. package/lib/module/assets/loft.js +142 -0
  12. package/lib/module/assets/model.js +74 -0
  13. package/lib/module/assets/modelling.js +254 -0
  14. package/lib/module/assets/shape.js +926 -0
  15. package/lib/module/assets/source.js +58 -0
  16. package/lib/module/assets/source.web.js +22 -0
  17. package/lib/module/assets/storage.js +4 -0
  18. package/lib/module/assets/storage.shared.js +36 -0
  19. package/lib/module/assets/storage.web.js +37 -0
  20. package/lib/module/assets/surface.js +47 -0
  21. package/lib/module/assets/texture.js +66 -0
  22. package/lib/module/drawing/DrawingCanvas.js +38 -0
  23. package/lib/module/drawing/DrawingCanvas.web.js +206 -0
  24. package/lib/module/drawing/DrawingDocument.js +256 -0
  25. package/lib/module/drawing/types.js +4 -0
  26. package/lib/module/globals.d.ts +12 -0
  27. package/lib/module/index.js +36 -0
  28. package/lib/module/input/Joystick.js +84 -0
  29. package/lib/module/input/Joystick.web.js +93 -0
  30. package/lib/module/input/keyboard.js +250 -0
  31. package/lib/module/media/audio-worklet.js +34 -0
  32. package/lib/module/media/audio.js +94 -0
  33. package/lib/module/media/haptics.js +82 -0
  34. package/lib/module/media/output.web.js +66 -0
  35. package/lib/module/native/BrowserEngine.js +551 -0
  36. package/lib/module/native/EngineBridge.js +4 -0
  37. package/lib/module/native/NativeNayanEngine.js +7 -0
  38. package/lib/module/native/NativeNayanEngine.web.js +62 -0
  39. package/lib/module/native/prepare.js +7 -0
  40. package/lib/module/native/prepare.web.js +17 -0
  41. package/lib/module/native/spec/NativeNayanEngine.js +17 -0
  42. package/lib/module/package.json +1 -0
  43. package/lib/module/render/GameView.js +249 -0
  44. package/lib/module/render/camera.js +34 -0
  45. package/lib/module/render/host.js +39 -0
  46. package/lib/module/render/host.web.js +104 -0
  47. package/lib/module/render/meshes.js +159 -0
  48. package/lib/module/render/quality.js +112 -0
  49. package/lib/module/render/responder.js +4 -0
  50. package/lib/module/render/responder.web.js +97 -0
  51. package/lib/module/render/touch.js +185 -0
  52. package/lib/module/types.js +30 -0
  53. package/lib/module/world/Scoreboard.js +143 -0
  54. package/lib/module/world/TransformBatch.js +126 -0
  55. package/lib/module/world/World.js +690 -0
  56. package/lib/module/world/desc.js +186 -0
  57. package/lib/module/world/effects.js +451 -0
  58. package/lib/module/world/quat.js +86 -0
  59. package/lib/module/world/types.js +4 -0
  60. package/lib/typescript/package.json +1 -0
  61. package/lib/typescript/src/assets/AssetBytesCache.d.ts +7 -0
  62. package/lib/typescript/src/assets/AssetCache.d.ts +9 -0
  63. package/lib/typescript/src/assets/AssetLoader.d.ts +11 -0
  64. package/lib/typescript/src/assets/CacheGeneration.d.ts +3 -0
  65. package/lib/typescript/src/assets/IndexedDbCache.web.d.ts +20 -0
  66. package/lib/typescript/src/assets/ProceduralCache.d.ts +33 -0
  67. package/lib/typescript/src/assets/font.d.ts +53 -0
  68. package/lib/typescript/src/assets/loft.d.ts +31 -0
  69. package/lib/typescript/src/assets/model.d.ts +48 -0
  70. package/lib/typescript/src/assets/modelling.d.ts +157 -0
  71. package/lib/typescript/src/assets/shape.d.ts +203 -0
  72. package/lib/typescript/src/assets/source.d.ts +10 -0
  73. package/lib/typescript/src/assets/source.web.d.ts +8 -0
  74. package/lib/typescript/src/assets/storage.d.ts +2 -0
  75. package/lib/typescript/src/assets/storage.shared.d.ts +7 -0
  76. package/lib/typescript/src/assets/storage.web.d.ts +7 -0
  77. package/lib/typescript/src/assets/surface.d.ts +17 -0
  78. package/lib/typescript/src/assets/texture.d.ts +30 -0
  79. package/lib/typescript/src/drawing/DrawingCanvas.d.ts +5 -0
  80. package/lib/typescript/src/drawing/DrawingCanvas.web.d.ts +4 -0
  81. package/lib/typescript/src/drawing/DrawingDocument.d.ts +210 -0
  82. package/lib/typescript/src/drawing/types.d.ts +12 -0
  83. package/lib/typescript/src/index.d.ts +42 -0
  84. package/lib/typescript/src/input/Joystick.d.ts +20 -0
  85. package/lib/typescript/src/input/Joystick.web.d.ts +13 -0
  86. package/lib/typescript/src/input/keyboard.d.ts +17 -0
  87. package/lib/typescript/src/media/audio.d.ts +46 -0
  88. package/lib/typescript/src/media/haptics.d.ts +22 -0
  89. package/lib/typescript/src/media/output.web.d.ts +4 -0
  90. package/lib/typescript/src/native/BrowserEngine.d.ts +8 -0
  91. package/lib/typescript/src/native/EngineBridge.d.ts +22 -0
  92. package/lib/typescript/src/native/NativeNayanEngine.d.ts +5 -0
  93. package/lib/typescript/src/native/NativeNayanEngine.web.d.ts +10 -0
  94. package/lib/typescript/src/native/prepare.d.ts +3 -0
  95. package/lib/typescript/src/native/prepare.web.d.ts +2 -0
  96. package/lib/typescript/src/native/spec/NativeNayanEngine.d.ts +182 -0
  97. package/lib/typescript/src/render/GameView.d.ts +95 -0
  98. package/lib/typescript/src/render/camera.d.ts +6 -0
  99. package/lib/typescript/src/render/host.d.ts +29 -0
  100. package/lib/typescript/src/render/host.web.d.ts +16 -0
  101. package/lib/typescript/src/render/meshes.d.ts +45 -0
  102. package/lib/typescript/src/render/quality.d.ts +46 -0
  103. package/lib/typescript/src/render/responder.d.ts +2 -0
  104. package/lib/typescript/src/render/responder.web.d.ts +40 -0
  105. package/lib/typescript/src/render/touch.d.ts +26 -0
  106. package/lib/typescript/src/types.d.ts +72 -0
  107. package/lib/typescript/src/world/Scoreboard.d.ts +65 -0
  108. package/lib/typescript/src/world/TransformBatch.d.ts +39 -0
  109. package/lib/typescript/src/world/World.d.ts +199 -0
  110. package/lib/typescript/src/world/desc.d.ts +17 -0
  111. package/lib/typescript/src/world/effects.d.ts +156 -0
  112. package/lib/typescript/src/world/quat.d.ts +31 -0
  113. package/lib/typescript/src/world/types.d.ts +233 -0
  114. package/lib/web/audio-worklet.js +29 -0
  115. package/lib/web/engine_core_bg.wasm +0 -0
  116. package/lib/web/index.js +7974 -0
  117. package/lib/web/types/src/assets/AssetBytesCache.d.ts +6 -0
  118. package/lib/web/types/src/assets/AssetCache.d.ts +8 -0
  119. package/lib/web/types/src/assets/AssetLoader.d.ts +10 -0
  120. package/lib/web/types/src/assets/CacheGeneration.d.ts +2 -0
  121. package/lib/web/types/src/assets/IndexedDbCache.d.ts +19 -0
  122. package/lib/web/types/src/assets/ProceduralCache.d.ts +32 -0
  123. package/lib/web/types/src/assets/font.d.ts +52 -0
  124. package/lib/web/types/src/assets/loft.d.ts +30 -0
  125. package/lib/web/types/src/assets/model.d.ts +47 -0
  126. package/lib/web/types/src/assets/modelling.d.ts +156 -0
  127. package/lib/web/types/src/assets/shape.d.ts +202 -0
  128. package/lib/web/types/src/assets/source.d.ts +7 -0
  129. package/lib/web/types/src/assets/storage.d.ts +6 -0
  130. package/lib/web/types/src/assets/storage.shared.d.ts +6 -0
  131. package/lib/web/types/src/assets/surface.d.ts +16 -0
  132. package/lib/web/types/src/assets/texture.d.ts +29 -0
  133. package/lib/web/types/src/drawing/DrawingCanvas.d.ts +3 -0
  134. package/lib/web/types/src/drawing/DrawingDocument.d.ts +209 -0
  135. package/lib/web/types/src/drawing/types.d.ts +11 -0
  136. package/lib/web/types/src/index.d.ts +41 -0
  137. package/lib/web/types/src/input/Joystick.d.ts +12 -0
  138. package/lib/web/types/src/input/keyboard.d.ts +16 -0
  139. package/lib/web/types/src/media/audio.d.ts +45 -0
  140. package/lib/web/types/src/media/haptics.d.ts +21 -0
  141. package/lib/web/types/src/media/output.d.ts +3 -0
  142. package/lib/web/types/src/native/BrowserEngine.d.ts +7 -0
  143. package/lib/web/types/src/native/EngineBridge.d.ts +21 -0
  144. package/lib/web/types/src/native/NativeNayanEngine.d.ts +9 -0
  145. package/lib/web/types/src/native/prepare.d.ts +1 -0
  146. package/lib/web/types/src/native/spec/NativeNayanEngine.d.ts +181 -0
  147. package/lib/web/types/src/render/GameView.d.ts +94 -0
  148. package/lib/web/types/src/render/camera.d.ts +5 -0
  149. package/lib/web/types/src/render/host.d.ts +15 -0
  150. package/lib/web/types/src/render/meshes.d.ts +44 -0
  151. package/lib/web/types/src/render/quality.d.ts +45 -0
  152. package/lib/web/types/src/render/responder.d.ts +39 -0
  153. package/lib/web/types/src/render/touch.d.ts +25 -0
  154. package/lib/web/types/src/types.d.ts +71 -0
  155. package/lib/web/types/src/world/Scoreboard.d.ts +64 -0
  156. package/lib/web/types/src/world/TransformBatch.d.ts +38 -0
  157. package/lib/web/types/src/world/World.d.ts +198 -0
  158. package/lib/web/types/src/world/desc.d.ts +16 -0
  159. package/lib/web/types/src/world/effects.d.ts +155 -0
  160. package/lib/web/types/src/world/quat.d.ts +30 -0
  161. package/lib/web/types/src/world/types.d.ts +232 -0
  162. package/native/android/CMakeLists.txt +34 -0
  163. package/native/android/NayanEngineSpec.h +15 -0
  164. package/native/android/build.gradle +26 -0
  165. package/native/android/codegen/CMakeLists.txt +3 -0
  166. package/native/android/proguard-rules.pro +2 -0
  167. package/native/android/src/main/java/com/nayanui/engine/EngineSurfaceView.java +234 -0
  168. package/native/android/src/main/java/com/nayanui/engine/EngineSurfaceViewManager.java +45 -0
  169. package/native/android/src/main/java/com/nayanui/engine/NayanEnginePackage.java +24 -0
  170. package/native/core/build/EngineCore.xcframework/Info.plist +43 -0
  171. package/native/core/build/EngineCore.xcframework/ios-arm64/libengine_core.a +0 -0
  172. package/native/core/build/EngineCore.xcframework/ios-arm64-simulator/libengine_core.a +0 -0
  173. package/native/core/build/android/arm64-v8a/libengine_core.a +0 -0
  174. package/native/core/build/android/armeabi-v7a/libengine_core.a +0 -0
  175. package/native/core/build/android/x86/libengine_core.a +0 -0
  176. package/native/core/build/android/x86_64/libengine_core.a +0 -0
  177. package/native/core/include/drawing-licenses.txt +58 -0
  178. package/native/core/include/engine_core.h +322 -0
  179. package/native/cpp/NayanEngineModule.cpp +827 -0
  180. package/native/cpp/NayanEngineModule.h +144 -0
  181. package/native/cpp/generated/NayanEngineSpecJSI.cpp +2 -0
  182. package/native/cpp/generated/NayanEngineSpecJSI.h +921 -0
  183. package/native/ios/NayanEngineProvider.h +5 -0
  184. package/native/ios/NayanEngineProvider.mm +21 -0
  185. package/native/ios/NayanEngineSurface.mm +223 -0
  186. package/native/scripts/check-package.mjs +39 -0
  187. package/package.json +162 -4
  188. package/react-native.config.js +18 -0
  189. package/src/native/spec/NativeNayanEngine.ts +226 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Niranjan Devasani
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "NayanEngine"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/ursnj/nayan-ui.git", :tag => "engine-v#{s.version}" }
15
+
16
+ s.source_files = "native/cpp/**/*.{h,cpp}", "native/ios/**/*.{h,m,mm}"
17
+ s.vendored_frameworks = "native/core/build/EngineCore.xcframework"
18
+ # The Rust core's audio (cpal / CoreAudio), haptics (Core Haptics) and renderer (wgpu on Metal) need
19
+ # these system frameworks.
20
+ s.frameworks = "AudioToolbox", "AVFAudio", "CoreAudio", "CoreHaptics", "Foundation", "Metal", "QuartzCore", "UIKit"
21
+
22
+ s.pod_target_xcconfig = {
23
+ "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/native/core/include\" \"$(PODS_TARGET_SRCROOT)/native/cpp\"",
24
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++20"
25
+ }
26
+
27
+ install_modules_dependencies(s)
28
+ end
package/README.md CHANGED
@@ -1,3 +1,211 @@
1
- # Temporary Holding Version
1
+ # @nayan-ui/engine
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A fast 3D game engine for **React** and **React Native**. One TypeScript API, one Rust core: rendering (wgpu), physics (Rapier), animation, particles, audio, haptics and 2D drawing.
4
+
5
+ **[Documentation](https://www.nayanui.com/engine)** · **[First game tutorial](https://www.nayanui.com/engine/first-game)** · **[API reference](https://www.nayanui.com/engine/api-reference)** · **[Guide for AI agents](https://www.nayanui.com/engine/agent-guide)**
6
+
7
+ | Platform | Renders with | Needs |
8
+ | ----------- | ------------------------------ | ------------------------------------------------ |
9
+ | iOS | Metal | A development or store build (not Expo Go) |
10
+ | Android | Vulkan or OpenGL ES | `minSdkVersion` 26, a development or store build |
11
+ | Web (React) | WebAssembly + WebGPU or WebGL2 | A modern browser over HTTPS or localhost |
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm install @nayan-ui/engine
17
+ ```
18
+
19
+ **React Native (Expo):** add the model extensions to `metro.config.js`, then make a development build (`npx expo run:ios` / `npx expo run:android`). Install `expo-asset` if you load bundled files with `require(...)`.
20
+
21
+ ```js
22
+ config.resolver.assetExts.push('glb', 'gltf');
23
+ ```
24
+
25
+ **React (web):** nothing else to configure. Call `await warmUpEngine()` before creating a world.
26
+
27
+ ## Quick start
28
+
29
+ A complete scene that works unchanged on both platforms:
30
+
31
+ ```tsx
32
+ import { useEffect, useState } from 'react';
33
+ import { type Camera, GameView, warmUpEngine, World } from '@nayan-ui/engine';
34
+
35
+ const camera: Camera = { eye: [0, 4, 8], target: [0, 0, 0], fov: 1 };
36
+
37
+ export function SpinningCube() {
38
+ const [world, setWorld] = useState<World | null>(null);
39
+
40
+ useEffect(() => {
41
+ let created: World | undefined;
42
+ let active = true;
43
+ void warmUpEngine().then(() => {
44
+ if (!active) return;
45
+ created = new World(100); // fixed entity capacity
46
+ created.spawn({ mesh: 'plane', scale: [20, 1, 20], color: [0.3, 0.6, 0.3], physics: 'fixed' });
47
+ const cube = created.spawn({ mesh: 'cube', position: [0, 3, 0], color: [0.2, 0.6, 1], physics: 'dynamic' });
48
+ created.animate(cube, { scale: 1.2 }, { duration: 0.6, repeat: 'forever', yoyo: true });
49
+ setWorld(created);
50
+ });
51
+ return () => {
52
+ active = false;
53
+ created?.dispose();
54
+ };
55
+ }, []);
56
+
57
+ if (!world) return null;
58
+ return (
59
+ <GameView
60
+ source={world}
61
+ camera={camera}
62
+ light={{ direction: [0.4, 0.8, 0.5], ambient: 0.35 }}
63
+ onUpdate={(dt) => world.update(dt)}
64
+ style={{ flex: 1 }}
65
+ />
66
+ );
67
+ }
68
+ ```
69
+
70
+ ## The five rules
71
+
72
+ 1. **Create** the world in an effect (after `await warmUpEngine()` on the web) and **dispose** it on unmount.
73
+ 2. **Step** it from one place: `world.update(dt)` inside `GameView`'s `onUpdate`.
74
+ 3. **Keep handles**: `spawn` returns an `Entity`; change it with `set`, remove it with `despawn`.
75
+ 4. **Batch per-frame moves** in `world.transforms`, then `flush()` once after `update`.
76
+ 5. **Don't track disposal yourself**: check `world.isDisposed`. Cleanup calls are safe no-ops after `dispose()`.
77
+
78
+ ## Common tasks
79
+
80
+ **Physics and collisions**
81
+
82
+ ```ts
83
+ const ball = world.spawn({ mesh: 'sphere', position: [0, 5, 0], physics: { type: 'dynamic', bounce: 0.6 } });
84
+ const coin = world.spawn({ mesh: 'cylinder', position: [2, 1, 0], physics: { type: 'fixed', sensor: true } });
85
+ world.impulse(ball, [0, 4, 0]);
86
+ world.forEachCollision((a, b, { started }) => {
87
+ if (started && (a === coin || b === coin)) world.despawn(coin);
88
+ });
89
+ ```
90
+
91
+ **Animation** (runs in Rust and returns a Promise)
92
+
93
+ ```ts
94
+ await world.animate(piece, { position: [2, 0, 3] }, { duration: 0.25, easing: 'back' });
95
+ world.animate(star, { scale: 1.2 }, { repeat: 'forever', yoyo: true });
96
+ ```
97
+
98
+ **Ready-made effects**
99
+
100
+ ```ts
101
+ import { effects } from '@nayan-ui/engine';
102
+
103
+ effects.confetti(world, {
104
+ position: [0, 2, 0],
105
+ colors: [
106
+ [1, 0.3, 0.3],
107
+ [1, 0.85, 0.2]
108
+ ],
109
+ size: 0.12,
110
+ speed: 6,
111
+ count: 40,
112
+ lifetime: 1.2,
113
+ gravity: -6
114
+ });
115
+ effects.dust(world, { position: [0, 0, 0], color: [0.8, 0.75, 0.65, 0.6], size: 0.3, speed: 1.5, count: 6, lifetime: 0.5 });
116
+ await effects.pop(world, piece, { scale: 1, color: [1, 0.3, 0.3] }); // swell, flash white, shrink away
117
+ world.despawn(piece);
118
+ ```
119
+
120
+ Also available: `sparkle`, `shatter`, `splash`, `sparks`, `explosion` and `flash`.
121
+
122
+ **Per-frame movement without allocations**
123
+
124
+ ```ts
125
+ import { type MutableQuat, quat } from '@nayan-ui/engine';
126
+
127
+ const turn: MutableQuat = [0, 0, 0, 1];
128
+ const at: [number, number, number] = [0, 0, 0];
129
+
130
+ function onUpdate(dt: number) {
131
+ world.update(dt);
132
+ world.position(player, at); // queries can fill an `out` array
133
+ quat.y(heading, turn);
134
+ world.transforms.transform(car, carX, 0, carZ, turn[0], turn[1], turn[2], turn[3]);
135
+ world.transforms.flush(); // one native call per frame
136
+ }
137
+ ```
138
+
139
+ **Score, lives and timers**
140
+
141
+ ```ts
142
+ world.scoreboard.handlers = { onScore: setScore, onTimeUp: () => endGame('lose') };
143
+ world.scoreboard.reset(3, 60); // 3 lives and a stopped 60 s countdown for the ready screen
144
+ world.scoreboard.resume(); // Start
145
+ world.scoreboard.add(10);
146
+ const timer = world.after(0.5, spawnWave); // game time: pauses with the game
147
+ world.cancelTimer(timer);
148
+ ```
149
+
150
+ **Input**
151
+
152
+ ```tsx
153
+ <GameView
154
+ source={world}
155
+ onTap={(x, y) => {
156
+ const hit = world.pick(x, y);
157
+ if (hit) world.animate(hit.entity, { scale: 1.2 }, { yoyo: true, repeat: 1 });
158
+ }}
159
+ onSwipe={(direction) => move(direction)}
160
+ onDrag={({ phase, x, y }) => drag(phase, x, y)}
161
+ />
162
+ ```
163
+
164
+ **Models, textures, text, sound and haptics**
165
+
166
+ ```ts
167
+ import { audio, haptics, loadFont, loadModel, loadTexture } from '@nayan-ui/engine';
168
+
169
+ const car = await loadModel(require('./car.glb'), { fit: 2 }); // or a URL string on the web
170
+ const crate = await loadTexture(require('./crate.png'));
171
+ const font = await loadFont(require('./Inter-Bold.ttf'));
172
+ world.spawn({ mesh: car, position: [0, 0, 0] });
173
+ world.spawn({ mesh: 'cube', texture: crate });
174
+ world.spawn({ text: '2048', font, position: [0, 2, 0], scale: 0.5 });
175
+
176
+ const sfx = audio.load({ coin: require('./coin.wav') });
177
+ sfx.play('coin');
178
+ haptics.impact(0.5);
179
+ audio.enabled = settings.sounds; // respect the player's preferences
180
+ haptics.enabled = settings.haptics;
181
+ ```
182
+
183
+ **Drawing**: a native paint canvas, also usable as a live texture inside games
184
+
185
+ ```tsx
186
+ import { DrawingCanvas, DrawingDocument } from '@nayan-ui/engine';
187
+
188
+ const paper = new DrawingDocument(512, 512);
189
+ paper.setBrush({ brushType: 'MARKER', color: '#e53935', width: 12, opacity: 1 });
190
+ <DrawingCanvas document={paper} mode="draw" />;
191
+ world.spawn({ mesh: 'plane', texture: paper.texture() }); // paint on a 3D object
192
+ ```
193
+
194
+ ## Learn more
195
+
196
+ | Topic | Guide |
197
+ | ------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
198
+ | Build a complete game step by step | [First game](https://www.nayanui.com/engine/first-game) |
199
+ | Copy-paste solutions | [Recipes](https://www.nayanui.com/engine/recipes) |
200
+ | Worlds, entities, rotations, disposal | [World & entities](https://www.nayanui.com/engine/world-entities) |
201
+ | Effects and particles | [Effects](https://www.nayanui.com/engine/particles) |
202
+ | Procedural meshes and modelling | [Geometry](https://www.nayanui.com/engine/procedural-geometry) |
203
+ | Drawing and painting | [Drawing](https://www.nayanui.com/engine/drawing) |
204
+ | Performance and caching | [Performance](https://www.nayanui.com/engine/cache-performance) |
205
+ | Coding agents | [Agent guide](https://www.nayanui.com/engine/agent-guide) · [engine.json](https://www.nayanui.com/engine.json) |
206
+
207
+ Ready-made games built on this engine: **[@nayan-ui/games](https://www.nayanui.com/games)**.
208
+
209
+ ## License
210
+
211
+ MIT © [Niranjan Devasani](https://github.com/ursnj)
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+
3
+ import { cacheGeneration } from "./CacheGeneration.js";
4
+ import { readPersistentAsset, removePersistentAsset, writePersistentAsset } from './storage';
5
+ // Decoded resources own their lifetime. Only temporary download bytes are evictable.
6
+ const BUDGET = 16 * 1024 * 1024;
7
+ const MAX_ENTRIES = 128;
8
+ const pending = new Map();
9
+ const retained = new Map();
10
+ let used = 0;
11
+
12
+ /** Share concurrent requests across loader variants; failed requests can always be retried. */
13
+ export function assetBytes(key, load) {
14
+ const bytes = retained.get(key);
15
+ if (bytes) {
16
+ retained.delete(key);
17
+ retained.set(key, bytes);
18
+ return Promise.resolve(bytes);
19
+ }
20
+ const existing = pending.get(key);
21
+ if (existing) return existing;
22
+ const epoch = cacheGeneration();
23
+ const request = Promise.resolve().then(async () => {
24
+ let saved = null;
25
+ try {
26
+ saved = await readPersistentAsset(key);
27
+ } catch {
28
+ /* Denied/corrupt storage is a miss. */
29
+ }
30
+ if (saved && epoch === cacheGeneration()) return saved;
31
+ const value = await load();
32
+ if (epoch === cacheGeneration()) {
33
+ // Await only the file write, never permission requests. Callers can rely on a completed load being saved.
34
+ try {
35
+ await writePersistentAsset(key, value);
36
+ } catch {
37
+ /* Keep existing records on quota/disk-full failures. */
38
+ }
39
+ }
40
+ return value;
41
+ }).then(value => {
42
+ if (epoch === cacheGeneration() && value.byteLength <= BUDGET) {
43
+ retained.set(key, value);
44
+ used += value.byteLength;
45
+ while (used > BUDGET || retained.size > MAX_ENTRIES) {
46
+ const oldest = retained.keys().next().value;
47
+ used -= retained.get(oldest).byteLength;
48
+ retained.delete(oldest);
49
+ }
50
+ }
51
+ return value;
52
+ }).finally(() => {
53
+ if (pending.get(key) === request) pending.delete(key);
54
+ });
55
+ pending.set(key, request);
56
+ return request;
57
+ }
58
+
59
+ /** A decoder rejected the payload: a later load must fetch fresh bytes, not replay bad data. */
60
+ export function forgetAssetBytes(key) {
61
+ if (key === undefined) return;
62
+ void removePersistentAsset(key).catch(() => {});
63
+ const bytes = retained.get(key);
64
+ if (bytes) {
65
+ used -= bytes.byteLength;
66
+ retained.delete(key);
67
+ }
68
+ }
69
+
70
+ /** Clearing disk must also prevent an earlier request from retaining its old bytes. */
71
+ export function clearAssetBytes() {
72
+ retained.clear();
73
+ pending.clear();
74
+ used = 0;
75
+ }
76
+ //# sourceMappingURL=AssetBytesCache.js.map
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+
3
+ import { clearAssetBytes } from "./AssetBytesCache.js";
4
+ import { advanceCacheGeneration } from "./CacheGeneration.js";
5
+ import { clearPersistentStorage } from './storage';
6
+
7
+ /**
8
+ * Explicitly removes the engine's saved geometry and original model/texture/font/audio files.
9
+ * Also clears temporary download bytes. Existing worlds and loaded handles remain usable;
10
+ * registrations are session resources and will be rebuilt/reloaded on a future app launch.
11
+ * Custom ProceduralStorage adapters belong to their host and are not cleared by this function.
12
+ * Rejects if storage cannot be cleared, allowing a settings screen to report the failure.
13
+ */
14
+ export async function clearAssetCache() {
15
+ advanceCacheGeneration();
16
+ clearAssetBytes();
17
+ await clearPersistentStorage();
18
+ }
19
+ //# sourceMappingURL=AssetCache.js.map
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+
3
+ import NativeEngine from '../native/NativeNayanEngine';
4
+ import { prepareEngine } from '../native/prepare';
5
+ import { forgetAssetBytes } from "./AssetBytesCache.js";
6
+ import { assetUri, loadAssetBytes } from './source';
7
+
8
+ /** Shared session handles; source-byte persistence stays in AssetBytesCache and Rust. */
9
+ export class AssetLoader {
10
+ loads = new Map();
11
+ constructor(name) {
12
+ this.name = name;
13
+ }
14
+ load(source, decode, variant = []) {
15
+ let uri;
16
+ try {
17
+ uri = assetUri(source);
18
+ if (!uri) throw new Error("couldn't resolve the asset source");
19
+ } catch (cause) {
20
+ return Promise.reject(this.error(String(source), cause));
21
+ }
22
+ // Tuple encoding prevents a URI containing separators from colliding with loader options.
23
+ const key = JSON.stringify([uri, ...variant]);
24
+ const existing = this.loads.get(key);
25
+ if (existing) return existing;
26
+ const pending = Promise.resolve().then(() => prepareEngine()).then(async () => {
27
+ if (!NativeEngine) throw new Error('the engine core is not linked into this build');
28
+ const {
29
+ bytes
30
+ } = await loadAssetBytes(source);
31
+ try {
32
+ return decode(NativeEngine, bytes);
33
+ } catch (cause) {
34
+ // Only rejected payloads invalidate saved bytes. Initialization/network failures don't.
35
+ forgetAssetBytes(uri);
36
+ throw cause;
37
+ }
38
+ }).catch(cause => {
39
+ this.loads.delete(key);
40
+ throw this.error(uri, cause);
41
+ });
42
+ this.loads.set(key, pending);
43
+ return pending;
44
+ }
45
+ error(source, cause) {
46
+ const message = cause instanceof Error ? cause.message : String(cause);
47
+ return new Error(`${this.name}: couldn't load ${source}: ${message}`, {
48
+ cause
49
+ });
50
+ }
51
+ }
52
+ //# sourceMappingURL=AssetLoader.js.map
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+
3
+ // Operations started before an explicit clear may finish for their caller, but cannot refill storage.
4
+ let generation = 0;
5
+ export const cacheGeneration = () => generation;
6
+ export const advanceCacheGeneration = () => ++generation;
7
+ //# sourceMappingURL=CacheGeneration.js.map
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+
3
+ // Browser I/O only. Rust owns cache keys, record validation, geometry and the warm-memory budget.
4
+ const DATABASE = 'nayan-engine-assets-v1';
5
+ function complete(transaction) {
6
+ return new Promise((resolve, reject) => {
7
+ transaction.oncomplete = () => resolve();
8
+ transaction.onabort = () => reject(transaction.error ?? new Error('Asset storage transaction aborted'));
9
+ transaction.onerror = () => reject(transaction.error ?? new Error('Asset storage transaction failed'));
10
+ });
11
+ }
12
+ function result(request) {
13
+ return new Promise((resolve, reject) => {
14
+ request.onsuccess = () => resolve(request.result);
15
+ request.onerror = () => reject(request.error ?? new Error('Asset storage request failed'));
16
+ });
17
+ }
18
+ export class IndexedDbCache {
19
+ tail = Promise.resolve();
20
+ pending = new Set();
21
+ generation = 0;
22
+ clearOperation = Promise.resolve();
23
+ constructor(database) {
24
+ this.database = database;
25
+ database.onversionchange = () => database.close();
26
+ }
27
+ static open() {
28
+ return new Promise((resolve, reject) => {
29
+ let settled = false;
30
+ let request;
31
+ try {
32
+ request = indexedDB.open(DATABASE, 1);
33
+ } catch (error) {
34
+ reject(error);
35
+ return;
36
+ }
37
+ request.onupgradeneeded = () => {
38
+ try {
39
+ const db = request.result;
40
+ db.createObjectStore('records');
41
+ db.createObjectStore('geometry').createIndex('writtenAt', 'writtenAt');
42
+ } catch {
43
+ request.transaction?.abort();
44
+ }
45
+ };
46
+ request.onsuccess = () => {
47
+ if (settled) {
48
+ request.result.close();
49
+ return;
50
+ }
51
+ settled = true;
52
+ resolve(new IndexedDbCache(request.result));
53
+ };
54
+ request.onerror = () => {
55
+ settled = true;
56
+ reject(request.error ?? new Error('Asset database unavailable'));
57
+ };
58
+ request.onblocked = () => {
59
+ settled = true;
60
+ reject(new Error('Another tab blocked the asset database upgrade'));
61
+ };
62
+ });
63
+ }
64
+ enqueue(operation) {
65
+ const task = this.tail.then(operation);
66
+ this.pending.add(task);
67
+ this.tail = task.catch(() => {}); // A failed write must not prevent a later clear/write.
68
+ void task.then(() => this.pending.delete(task), () => this.pending.delete(task));
69
+ return task;
70
+ }
71
+ /** The Uint8Array is an owned copy made by Rust, never a view into growable WASM memory. */
72
+ write(name, bytes, geometry) {
73
+ const epoch = this.generation;
74
+ return this.enqueue(async () => {
75
+ if (epoch !== this.generation) return;
76
+ const transaction = this.database.transaction(['records', 'geometry'], 'readwrite');
77
+ const saved = complete(transaction);
78
+ try {
79
+ transaction.objectStore('records').put(bytes, name);
80
+ if (geometry) transaction.objectStore('geometry').put({
81
+ bytes: bytes.byteLength,
82
+ writtenAt: Date.now()
83
+ }, name);
84
+ } catch (error) {
85
+ transaction.abort();
86
+ await saved.catch(() => {});
87
+ throw error;
88
+ }
89
+ await saved;
90
+ });
91
+ }
92
+ async read(name) {
93
+ await this.tail;
94
+ const transaction = this.database.transaction('records', 'readonly');
95
+ const finished = complete(transaction);
96
+ const value = await Promise.all([result(transaction.objectStore('records').get(name)), finished]);
97
+ const bytes = value[0];
98
+ return bytes instanceof Uint8Array ? bytes : undefined;
99
+ }
100
+ remove(name) {
101
+ return this.enqueue(async () => {
102
+ const transaction = this.database.transaction(['records', 'geometry'], 'readwrite');
103
+ const finished = complete(transaction);
104
+ transaction.objectStore('records').delete(name);
105
+ transaction.objectStore('geometry').delete(name);
106
+ await finished;
107
+ });
108
+ }
109
+ clear() {
110
+ this.generation++; // Cancel queued pre-clear writes; an active transaction finishes before clear.
111
+ this.clearOperation = this.enqueue(async () => {
112
+ const transaction = this.database.transaction(['records', 'geometry'], 'readwrite');
113
+ const finished = complete(transaction);
114
+ transaction.objectStore('records').clear();
115
+ transaction.objectStore('geometry').clear();
116
+ await finished;
117
+ });
118
+ return this.clearOperation;
119
+ }
120
+ flush() {
121
+ return Promise.all([...this.pending]).then(() => undefined);
122
+ }
123
+ cleared() {
124
+ return this.clearOperation;
125
+ }
126
+
127
+ /** Read recent geometry only. Large source files remain in IndexedDB until requested by a loader. */
128
+ warm(canFit, restore) {
129
+ const transaction = this.database.transaction(['records', 'geometry'], 'readonly');
130
+ const finished = complete(transaction);
131
+ const cursor = transaction.objectStore('geometry').index('writtenAt').openCursor(null, 'prev');
132
+ let scanned = 0;
133
+ cursor.onsuccess = () => {
134
+ const current = cursor.result;
135
+ if (!current || !canFit(0) || ++scanned > 4096) return;
136
+ const metadata = current.value;
137
+ if (typeof current.primaryKey === 'string' && metadata !== null && typeof metadata === 'object' && Number.isSafeInteger(metadata.bytes) && metadata.bytes > 0 && canFit(metadata.bytes)) {
138
+ const name = current.primaryKey;
139
+ const record = transaction.objectStore('records').get(name);
140
+ record.onsuccess = () => {
141
+ if (record.result instanceof Uint8Array && record.result.byteLength === metadata.bytes && canFit(record.result.byteLength)) restore(name, record.result);
142
+ };
143
+ }
144
+ current.continue();
145
+ };
146
+ return finished;
147
+ }
148
+ }
149
+ //# sourceMappingURL=IndexedDbCache.web.js.map