@nayan-ui/games 1.20.0 → 1.21.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 (552) hide show
  1. package/lib/module/games/arrows/Arrows.js +1 -1
  2. package/lib/module/games/arrows/ArrowsScene.js +1 -1
  3. package/lib/module/games/balloon-blaster/BalloonBlaster.js +1 -1
  4. package/lib/module/games/balloon-blaster/BalloonBlasterScene.js +1 -1
  5. package/lib/module/games/bike-racing/BikeMeshes.js +1 -1
  6. package/lib/module/games/bike-racing/BikeRacing.js +1 -1
  7. package/lib/module/games/bike-racing/BikeRacingConstants.js +1 -1
  8. package/lib/module/games/bike-racing/BikeRacingScene.js +1 -1
  9. package/lib/module/games/block-blast/BlockBlast.js +1 -1
  10. package/lib/module/games/block-blast/BlockBlastScene.js +1 -1
  11. package/lib/module/games/block-breaker/BlockBreaker.js +1 -1
  12. package/lib/module/games/block-breaker/BlockBreakerScene.js +1 -1
  13. package/lib/module/games/bluk/Bluk.js +1 -1
  14. package/lib/module/games/bluk/BlukScene.js +1 -1
  15. package/lib/module/games/bubble-shooter/BubbleShooter.js +1 -1
  16. package/lib/module/games/bubble-shooter/BubbleShooterScene.js +1 -1
  17. package/lib/module/games/candy-crush/CandyCrush.js +1 -1
  18. package/lib/module/games/candy-crush/CandyCrushScene.js +1 -1
  19. package/lib/module/games/car-racing/CarMeshes.js +1 -1
  20. package/lib/module/games/car-racing/CarRacing.js +1 -1
  21. package/lib/module/games/car-racing/CarRacingScene.js +1 -1
  22. package/lib/module/games/chameleon-run/ChameleonRun.js +1 -1
  23. package/lib/module/games/chameleon-run/ChameleonRunBackdrop.js +1 -1
  24. package/lib/module/games/chameleon-run/ChameleonRunMeshes.js +1 -1
  25. package/lib/module/games/checkers/Checkers.js +1 -1
  26. package/lib/module/games/checkers/CheckersScene.js +1 -1
  27. package/lib/module/games/color-switch/ColorSwitch.js +1 -1
  28. package/lib/module/games/color-switch/ColorSwitchScene.js +1 -1
  29. package/lib/module/games/colors-sort/ColorsSort.js +1 -1
  30. package/lib/module/games/colors-sort/ColorsSortScene.js +1 -1
  31. package/lib/module/games/connect-em-all/ConnectEmAll.js +1 -1
  32. package/lib/module/games/connect-em-all/ConnectEmAllScene.js +1 -1
  33. package/lib/module/games/crossy-road/CrossyRoad.js +1 -1
  34. package/lib/module/games/crossy-road/CrossyRoadMeshes.js +1 -1
  35. package/lib/module/games/crossy-road/CrossyRoadPerson.js +1 -1
  36. package/lib/module/games/dino-jump/DinoJump.js +1 -1
  37. package/lib/module/games/dino-jump/DinoJumpScene.js +1 -1
  38. package/lib/module/games/dots-and-boxes/DotsAndBoxes.js +1 -1
  39. package/lib/module/games/dots-and-boxes/DotsAndBoxesScene.js +1 -1
  40. package/lib/module/games/falling-sticks/FallingSticks.js +1 -1
  41. package/lib/module/games/falling-sticks/FallingSticksScene.js +1 -1
  42. package/lib/module/games/find-different-number/FindDifferentNumber.js +1 -1
  43. package/lib/module/games/find-different-number/FindDifferentNumberScene.js +1 -1
  44. package/lib/module/games/flappy-bird/FlappyBird.js +1 -1
  45. package/lib/module/games/flappy-bird/FlappyBirdScene.js +1 -1
  46. package/lib/module/games/fruit-merger/FruitMerger.js +1 -1
  47. package/lib/module/games/fruit-merger/FruitMergerScene.js +1 -1
  48. package/lib/module/games/fruit-ninja/FruitNinja.js +1 -1
  49. package/lib/module/games/fruit-ninja/FruitNinjaScene.js +1 -1
  50. package/lib/module/games/fruit-ninja/FruitNinjaWorld.js +1 -1
  51. package/lib/module/games/game-2048/Game2048.js +1 -1
  52. package/lib/module/games/game-2048/Game2048Scene.js +1 -1
  53. package/lib/module/games/knife-hit/KnifeHit.js +1 -1
  54. package/lib/module/games/knife-hit/KnifeHitScene.js +1 -1
  55. package/lib/module/games/ludo-king/LudoKing.js +1 -1
  56. package/lib/module/games/ludo-king/LudoKingScene.js +1 -1
  57. package/lib/module/games/maze-runner/MazeRunner.js +1 -1
  58. package/lib/module/games/maze-runner/MazeRunnerScene.js +1 -1
  59. package/lib/module/games/memory-match/MemoryMatch.js +1 -1
  60. package/lib/module/games/memory-match/MemoryMatchScene.js +1 -1
  61. package/lib/module/games/mine-sweeper/MineSweeper.js +1 -1
  62. package/lib/module/games/mine-sweeper/MineSweeperScene.js +1 -1
  63. package/lib/module/games/mob-control/MobControl.js +1 -1
  64. package/lib/module/games/mob-control/MobControlGame.js +1 -1
  65. package/lib/module/games/mob-control/MobControlMeshes.js +1 -1
  66. package/lib/module/games/mob-control/MobControlRenderer.js +1 -1
  67. package/lib/module/games/mob-control/MobControlRules.js +1 -1
  68. package/lib/module/games/mob-control/MobControlScene.js +1 -1
  69. package/lib/module/games/nuts-and-bolts/NutsAndBolts.js +1 -1
  70. package/lib/module/games/nuts-and-bolts/NutsAndBoltsScene.js +1 -1
  71. package/lib/module/games/pac-man/PacMan.js +1 -1
  72. package/lib/module/games/pac-man/PacManGame.js +1 -1
  73. package/lib/module/games/pac-man/PacManScene.js +1 -1
  74. package/lib/module/games/paper-io/PaperIo.js +1 -1
  75. package/lib/module/games/paper-io/PaperIoMeshes.js +1 -1
  76. package/lib/module/games/paper-io/PaperIoScene.js +1 -1
  77. package/lib/module/games/penguin-run/PenguinMeshes.js +1 -1
  78. package/lib/module/games/penguin-run/PenguinRun.js +1 -1
  79. package/lib/module/games/penguin-run/PenguinRunScene.js +1 -1
  80. package/lib/module/games/perfect-circle/PerfectCircle.js +1 -1
  81. package/lib/module/games/perfect-circle/PerfectCircleScene.js +1 -1
  82. package/lib/module/games/pipe-connect/PipeConnect.js +1 -1
  83. package/lib/module/games/pipe-connect/PipeConnectScene.js +1 -1
  84. package/lib/module/games/play-with-tua/PlayWithTua.js +1 -1
  85. package/lib/module/games/popit-fidget/PopitFidget.js +1 -1
  86. package/lib/module/games/popit-fidget/PopitFidgetScene.js +1 -1
  87. package/lib/module/games/reversi/Reversi.js +1 -1
  88. package/lib/module/games/reversi/ReversiScene.js +1 -1
  89. package/lib/module/games/search-grid/SearchGrid.js +1 -1
  90. package/lib/module/games/search-grid/SearchGridScene.js +1 -1
  91. package/lib/module/games/shared/BackdropCard.js +1 -1
  92. package/lib/module/games/shared/EngineBackdrop.js +1 -1
  93. package/lib/module/games/shared/EngineBlasts.js +1 -1
  94. package/lib/module/games/shared/EngineBlocks.js +1 -1
  95. package/lib/module/games/shared/EngineDice.js +1 -1
  96. package/lib/module/games/shared/EngineFruits.js +1 -1
  97. package/lib/module/games/shared/EngineGameLoader.js +1 -1
  98. package/lib/module/games/shared/EngineGameScreen.js +1 -1
  99. package/lib/module/games/shared/EngineGlyphs.js +1 -1
  100. package/lib/module/games/shared/EngineHud.js +1 -1
  101. package/lib/module/games/shared/EngineMeadow.js +1 -1
  102. package/lib/module/games/shared/EngineTouchSurface.js +1 -1
  103. package/lib/module/games/shared/GameActivity.js +1 -1
  104. package/lib/module/games/shared/GameLoading.js +1 -1
  105. package/lib/module/games/shared/GameLoadingTransition.js +1 -1
  106. package/lib/module/games/sliding-numbers/SlidingNumbers.js +1 -1
  107. package/lib/module/games/sliding-numbers/SlidingNumbersScene.js +1 -1
  108. package/lib/module/games/snake-3d/Snake3D.js +1 -1
  109. package/lib/module/games/snake-3d/Snake3DScene.js +1 -1
  110. package/lib/module/games/snakes-and-ladders/SnakesAndLadders.js +1 -1
  111. package/lib/module/games/snakes-and-ladders/SnakesAndLaddersScene.js +1 -1
  112. package/lib/module/games/space-fighter/SpaceFighter.js +1 -1
  113. package/lib/module/games/space-fighter/SpaceFighterScene.js +1 -1
  114. package/lib/module/games/spider-solitaire/SpiderSolitaire.js +1 -1
  115. package/lib/module/games/spider-solitaire/SpiderSolitaireScene.js +1 -1
  116. package/lib/module/games/stack-tower/StackTower.js +1 -1
  117. package/lib/module/games/stack-tower/StackTowerScene.js +1 -1
  118. package/lib/module/games/sudoku/Sudoku.js +1 -1
  119. package/lib/module/games/sudoku/SudokuScene.js +1 -1
  120. package/lib/module/games/tangram/Tangram.js +1 -1
  121. package/lib/module/games/tangram/TangramScene.js +1 -1
  122. package/lib/module/games/tank-1990/Tank1990.js +1 -1
  123. package/lib/module/games/tank-1990/Tank1990Scene.js +1 -1
  124. package/lib/module/games/tic-tac-toe/TicTacToe.js +1 -1
  125. package/lib/module/games/tic-tac-toe/TicTacToeScene.js +1 -1
  126. package/lib/module/games/tile-home/TileHome.js +1 -1
  127. package/lib/module/games/tile-home/TileHomeScene.js +1 -1
  128. package/lib/module/games/treasure-hunt/TreasureHunt.js +1 -1
  129. package/lib/module/games/treasure-hunt/TreasureHuntConstants.js +1 -1
  130. package/lib/module/games/treasure-hunt/TreasureHuntModel.js +1 -1
  131. package/lib/module/games/treasure-hunt/TreasureHuntScene.js +1 -1
  132. package/lib/module/games/whack-a-mole/WhackAMole.js +1 -1
  133. package/lib/module/games/whack-a-mole/WhackAMoleScene.js +1 -1
  134. package/lib/module/helpers/GameSettingsModal.js +1 -1
  135. package/lib/module/helpers/GameTheme.js +1 -1
  136. package/package.json +1 -2
  137. package/src/assets/fonts/Inter-SemiBold.ttf +0 -0
  138. package/src/assets/models/explorer/explorer.glb +0 -0
  139. package/src/assets/models/explorer/params.json +0 -51
  140. package/src/assets/models/explorer/source.json +0 -222
  141. package/src/assets/models/play-with-tua/play-with-tua.glb +0 -0
  142. package/src/assets/models/play-with-tua/sources.json +0 -141
  143. package/src/assets/sfx/balloon_pop.wav +0 -0
  144. package/src/assets/sfx/blocked.wav +0 -0
  145. package/src/assets/sfx/boing.wav +0 -0
  146. package/src/assets/sfx/bonus.wav +0 -0
  147. package/src/assets/sfx/boom.wav +0 -0
  148. package/src/assets/sfx/bump.wav +0 -0
  149. package/src/assets/sfx/cannon.wav +0 -0
  150. package/src/assets/sfx/capture.wav +0 -0
  151. package/src/assets/sfx/card_flip.wav +0 -0
  152. package/src/assets/sfx/card_place.wav +0 -0
  153. package/src/assets/sfx/catch.wav +0 -0
  154. package/src/assets/sfx/chime.wav +0 -0
  155. package/src/assets/sfx/chomp.wav +0 -0
  156. package/src/assets/sfx/chomp_alt.wav +0 -0
  157. package/src/assets/sfx/clack.wav +0 -0
  158. package/src/assets/sfx/clang.wav +0 -0
  159. package/src/assets/sfx/clash.wav +0 -0
  160. package/src/assets/sfx/click.wav +0 -0
  161. package/src/assets/sfx/coin.wav +0 -0
  162. package/src/assets/sfx/combo.wav +0 -0
  163. package/src/assets/sfx/crash.wav +0 -0
  164. package/src/assets/sfx/crunch.wav +0 -0
  165. package/src/assets/sfx/dance.wav +0 -0
  166. package/src/assets/sfx/deselect.wav +0 -0
  167. package/src/assets/sfx/dice_roll.wav +0 -0
  168. package/src/assets/sfx/die.wav +0 -0
  169. package/src/assets/sfx/draw.wav +0 -0
  170. package/src/assets/sfx/drop.wav +0 -0
  171. package/src/assets/sfx/eat.wav +0 -0
  172. package/src/assets/sfx/engine.wav +0 -0
  173. package/src/assets/sfx/error.wav +0 -0
  174. package/src/assets/sfx/flag.wav +0 -0
  175. package/src/assets/sfx/flap.wav +0 -0
  176. package/src/assets/sfx/flip_disc.wav +0 -0
  177. package/src/assets/sfx/footstep_run.wav +0 -0
  178. package/src/assets/sfx/footstep_walk.wav +0 -0
  179. package/src/assets/sfx/game_over.wav +0 -0
  180. package/src/assets/sfx/gate.wav +0 -0
  181. package/src/assets/sfx/hit.wav +0 -0
  182. package/src/assets/sfx/hop.wav +0 -0
  183. package/src/assets/sfx/hurt.wav +0 -0
  184. package/src/assets/sfx/invalid.wav +0 -0
  185. package/src/assets/sfx/jump.wav +0 -0
  186. package/src/assets/sfx/ladder.wav +0 -0
  187. package/src/assets/sfx/land.wav +0 -0
  188. package/src/assets/sfx/laugh.wav +0 -0
  189. package/src/assets/sfx/lose.wav +0 -0
  190. package/src/assets/sfx/merge.wav +0 -0
  191. package/src/assets/sfx/miss.wav +0 -0
  192. package/src/assets/sfx/nitro.wav +0 -0
  193. package/src/assets/sfx/pew.wav +0 -0
  194. package/src/assets/sfx/place.wav +0 -0
  195. package/src/assets/sfx/pling.wav +0 -0
  196. package/src/assets/sfx/point.wav +0 -0
  197. package/src/assets/sfx/pop.wav +0 -0
  198. package/src/assets/sfx/pop_soft.wav +0 -0
  199. package/src/assets/sfx/power_up.wav +0 -0
  200. package/src/assets/sfx/reveal.wav +0 -0
  201. package/src/assets/sfx/robot.wav +0 -0
  202. package/src/assets/sfx/rotate.wav +0 -0
  203. package/src/assets/sfx/run_steps.wav +0 -0
  204. package/src/assets/sfx/select.wav +0 -0
  205. package/src/assets/sfx/shuffle.wav +0 -0
  206. package/src/assets/sfx/six.wav +0 -0
  207. package/src/assets/sfx/slice.wav +0 -0
  208. package/src/assets/sfx/slide.wav +0 -0
  209. package/src/assets/sfx/snake.wav +0 -0
  210. package/src/assets/sfx/snap.wav +0 -0
  211. package/src/assets/sfx/splash.wav +0 -0
  212. package/src/assets/sfx/star.wav +0 -0
  213. package/src/assets/sfx/start.wav +0 -0
  214. package/src/assets/sfx/swoosh.wav +0 -0
  215. package/src/assets/sfx/tap.wav +0 -0
  216. package/src/assets/sfx/thud.wav +0 -0
  217. package/src/assets/sfx/thunk.wav +0 -0
  218. package/src/assets/sfx/tick.wav +0 -0
  219. package/src/assets/sfx/time_up.wav +0 -0
  220. package/src/assets/sfx/tock.wav +0 -0
  221. package/src/assets/sfx/walk_steps.wav +0 -0
  222. package/src/assets/sfx/warning.wav +0 -0
  223. package/src/assets/sfx/whack.wav +0 -0
  224. package/src/assets/sfx/whoosh.wav +0 -0
  225. package/src/assets/sfx/win.wav +0 -0
  226. package/src/assets/sfx/zap.wav +0 -0
  227. package/src/assets/textures/ad_bolt.png +0 -0
  228. package/src/assets/textures/ad_bubbles.png +0 -0
  229. package/src/assets/textures/ad_chevrons.png +0 -0
  230. package/src/assets/textures/asphalt.png +0 -0
  231. package/src/assets/textures/bark.jpg +0 -0
  232. package/src/assets/textures/explorer-atlas.png +0 -0
  233. package/src/assets/textures/facade.png +0 -0
  234. package/src/assets/textures/grass.png +0 -0
  235. package/src/assets/textures/leaves.png +0 -0
  236. package/src/assets/textures/limestone.jpg +0 -0
  237. package/src/assets/textures/meadow.jpg +0 -0
  238. package/src/assets/textures/nature-sources.json +0 -87
  239. package/src/assets/textures/pine.png +0 -0
  240. package/src/assets/textures/river.jpg +0 -0
  241. package/src/assets/textures/soft_glow.png +0 -0
  242. package/src/games/arrows/Arrows.tsx +0 -69
  243. package/src/games/arrows/ArrowsConstants.ts +0 -107
  244. package/src/games/arrows/ArrowsGame.ts +0 -735
  245. package/src/games/arrows/ArrowsScene.ts +0 -395
  246. package/src/games/balloon-blaster/BalloonBlaster.tsx +0 -75
  247. package/src/games/balloon-blaster/BalloonBlasterConstants.ts +0 -79
  248. package/src/games/balloon-blaster/BalloonBlasterGame.ts +0 -287
  249. package/src/games/balloon-blaster/BalloonBlasterPhysics.ts +0 -95
  250. package/src/games/balloon-blaster/BalloonBlasterScene.ts +0 -225
  251. package/src/games/bike-racing/BikeMeshes.ts +0 -273
  252. package/src/games/bike-racing/BikeRacing.tsx +0 -211
  253. package/src/games/bike-racing/BikeRacingConstants.ts +0 -103
  254. package/src/games/bike-racing/BikeRacingGame.ts +0 -557
  255. package/src/games/bike-racing/BikeRacingScene.ts +0 -119
  256. package/src/games/block-blast/BlockBlast.tsx +0 -73
  257. package/src/games/block-blast/BlockBlastConstants.ts +0 -179
  258. package/src/games/block-blast/BlockBlastGame.ts +0 -471
  259. package/src/games/block-blast/BlockBlastScene.ts +0 -156
  260. package/src/games/block-breaker/BlockBreaker.tsx +0 -79
  261. package/src/games/block-breaker/BlockBreakerConstants.ts +0 -139
  262. package/src/games/block-breaker/BlockBreakerGame.ts +0 -459
  263. package/src/games/block-breaker/BlockBreakerScene.ts +0 -257
  264. package/src/games/bluk/Bluk.tsx +0 -82
  265. package/src/games/bluk/BlukConstants.ts +0 -121
  266. package/src/games/bluk/BlukGame.ts +0 -323
  267. package/src/games/bluk/BlukRules.ts +0 -154
  268. package/src/games/bluk/BlukScene.ts +0 -514
  269. package/src/games/bubble-shooter/BubbleShooter.tsx +0 -76
  270. package/src/games/bubble-shooter/BubbleShooterConstants.ts +0 -103
  271. package/src/games/bubble-shooter/BubbleShooterGame.ts +0 -459
  272. package/src/games/bubble-shooter/BubbleShooterScene.ts +0 -158
  273. package/src/games/candy-crush/CandyCrush.tsx +0 -77
  274. package/src/games/candy-crush/CandyCrushConstants.ts +0 -94
  275. package/src/games/candy-crush/CandyCrushGame.ts +0 -477
  276. package/src/games/candy-crush/CandyCrushScene.ts +0 -209
  277. package/src/games/car-racing/CarMeshes.ts +0 -440
  278. package/src/games/car-racing/CarRacing.tsx +0 -211
  279. package/src/games/car-racing/CarRacingConstants.ts +0 -167
  280. package/src/games/car-racing/CarRacingFacade.ts +0 -40
  281. package/src/games/car-racing/CarRacingGame.ts +0 -539
  282. package/src/games/car-racing/CarRacingScene.ts +0 -1026
  283. package/src/games/car-racing/CarRacingScenery.ts +0 -265
  284. package/src/games/chameleon-run/ChameleonRun.tsx +0 -96
  285. package/src/games/chameleon-run/ChameleonRunBackdrop.ts +0 -186
  286. package/src/games/chameleon-run/ChameleonRunConstants.ts +0 -47
  287. package/src/games/chameleon-run/ChameleonRunGame.ts +0 -309
  288. package/src/games/chameleon-run/ChameleonRunMeshes.ts +0 -305
  289. package/src/games/chameleon-run/ChameleonRunRules.ts +0 -252
  290. package/src/games/chameleon-run/ChameleonRunScene.ts +0 -310
  291. package/src/games/checkers/Checkers.tsx +0 -83
  292. package/src/games/checkers/CheckersAI.ts +0 -255
  293. package/src/games/checkers/CheckersConstants.ts +0 -68
  294. package/src/games/checkers/CheckersGame.ts +0 -322
  295. package/src/games/checkers/CheckersRules.ts +0 -167
  296. package/src/games/checkers/CheckersScene.ts +0 -308
  297. package/src/games/color-switch/ColorSwitch.tsx +0 -75
  298. package/src/games/color-switch/ColorSwitchConstants.ts +0 -61
  299. package/src/games/color-switch/ColorSwitchGame.ts +0 -329
  300. package/src/games/color-switch/ColorSwitchScene.ts +0 -220
  301. package/src/games/colors-sort/ColorsSort.tsx +0 -82
  302. package/src/games/colors-sort/ColorsSortConstants.ts +0 -119
  303. package/src/games/colors-sort/ColorsSortGame.ts +0 -246
  304. package/src/games/colors-sort/ColorsSortScene.ts +0 -315
  305. package/src/games/connect-em-all/ConnectEmAll.tsx +0 -70
  306. package/src/games/connect-em-all/ConnectEmAllConstants.ts +0 -77
  307. package/src/games/connect-em-all/ConnectEmAllGame.ts +0 -401
  308. package/src/games/connect-em-all/ConnectEmAllScene.ts +0 -235
  309. package/src/games/crossy-road/CrossyRoad.tsx +0 -74
  310. package/src/games/crossy-road/CrossyRoadCamera.ts +0 -59
  311. package/src/games/crossy-road/CrossyRoadConstants.ts +0 -132
  312. package/src/games/crossy-road/CrossyRoadGame.ts +0 -293
  313. package/src/games/crossy-road/CrossyRoadMeshes.ts +0 -195
  314. package/src/games/crossy-road/CrossyRoadPerson.ts +0 -198
  315. package/src/games/crossy-road/CrossyRoadRules.ts +0 -99
  316. package/src/games/crossy-road/CrossyRoadScene.ts +0 -355
  317. package/src/games/dino-jump/DinoJump.tsx +0 -77
  318. package/src/games/dino-jump/DinoJumpConstants.ts +0 -80
  319. package/src/games/dino-jump/DinoJumpGame.ts +0 -344
  320. package/src/games/dino-jump/DinoJumpScene.ts +0 -686
  321. package/src/games/dots-and-boxes/DotsAndBoxes.tsx +0 -83
  322. package/src/games/dots-and-boxes/DotsAndBoxesConstants.ts +0 -77
  323. package/src/games/dots-and-boxes/DotsAndBoxesGame.ts +0 -271
  324. package/src/games/dots-and-boxes/DotsAndBoxesScene.ts +0 -249
  325. package/src/games/falling-sticks/FallingSticks.tsx +0 -72
  326. package/src/games/falling-sticks/FallingSticksConstants.ts +0 -62
  327. package/src/games/falling-sticks/FallingSticksGame.ts +0 -220
  328. package/src/games/falling-sticks/FallingSticksRules.ts +0 -145
  329. package/src/games/falling-sticks/FallingSticksScene.ts +0 -245
  330. package/src/games/find-different-number/FindDifferentNumber.tsx +0 -74
  331. package/src/games/find-different-number/FindDifferentNumberConstants.ts +0 -74
  332. package/src/games/find-different-number/FindDifferentNumberGame.ts +0 -174
  333. package/src/games/find-different-number/FindDifferentNumberRules.ts +0 -43
  334. package/src/games/find-different-number/FindDifferentNumberScene.ts +0 -193
  335. package/src/games/flappy-bird/FlappyBird.tsx +0 -73
  336. package/src/games/flappy-bird/FlappyBirdConstants.ts +0 -95
  337. package/src/games/flappy-bird/FlappyBirdGame.ts +0 -375
  338. package/src/games/flappy-bird/FlappyBirdScene.ts +0 -630
  339. package/src/games/fruit-merger/FruitMerger.tsx +0 -73
  340. package/src/games/fruit-merger/FruitMergerConstants.ts +0 -102
  341. package/src/games/fruit-merger/FruitMergerGame.ts +0 -285
  342. package/src/games/fruit-merger/FruitMergerRules.ts +0 -286
  343. package/src/games/fruit-merger/FruitMergerScene.ts +0 -498
  344. package/src/games/fruit-ninja/FruitNinja.tsx +0 -129
  345. package/src/games/fruit-ninja/FruitNinjaConstants.ts +0 -107
  346. package/src/games/fruit-ninja/FruitNinjaGame.ts +0 -391
  347. package/src/games/fruit-ninja/FruitNinjaScene.ts +0 -319
  348. package/src/games/fruit-ninja/FruitNinjaWorld.ts +0 -17
  349. package/src/games/game-2048/Game2048.tsx +0 -84
  350. package/src/games/game-2048/Game2048Constants.ts +0 -68
  351. package/src/games/game-2048/Game2048Game.ts +0 -181
  352. package/src/games/game-2048/Game2048Rules.ts +0 -101
  353. package/src/games/game-2048/Game2048Scene.ts +0 -206
  354. package/src/games/knife-hit/KnifeHit.tsx +0 -78
  355. package/src/games/knife-hit/KnifeHitConstants.ts +0 -89
  356. package/src/games/knife-hit/KnifeHitGame.ts +0 -264
  357. package/src/games/knife-hit/KnifeHitScene.ts +0 -360
  358. package/src/games/ludo-king/LudoKing.tsx +0 -86
  359. package/src/games/ludo-king/LudoKingConstants.ts +0 -212
  360. package/src/games/ludo-king/LudoKingGame.ts +0 -326
  361. package/src/games/ludo-king/LudoKingRules.ts +0 -143
  362. package/src/games/ludo-king/LudoKingScene.ts +0 -397
  363. package/src/games/maze-runner/MazeRunner.tsx +0 -88
  364. package/src/games/maze-runner/MazeRunnerConstants.ts +0 -65
  365. package/src/games/maze-runner/MazeRunnerGame.ts +0 -176
  366. package/src/games/maze-runner/MazeRunnerRules.ts +0 -161
  367. package/src/games/maze-runner/MazeRunnerScene.ts +0 -245
  368. package/src/games/memory-match/MemoryMatch.tsx +0 -77
  369. package/src/games/memory-match/MemoryMatchConstants.ts +0 -105
  370. package/src/games/memory-match/MemoryMatchGame.ts +0 -222
  371. package/src/games/memory-match/MemoryMatchRules.ts +0 -38
  372. package/src/games/memory-match/MemoryMatchScene.ts +0 -268
  373. package/src/games/mine-sweeper/MineSweeper.tsx +0 -77
  374. package/src/games/mine-sweeper/MineSweeperConstants.ts +0 -94
  375. package/src/games/mine-sweeper/MineSweeperGame.ts +0 -234
  376. package/src/games/mine-sweeper/MineSweeperRules.ts +0 -67
  377. package/src/games/mine-sweeper/MineSweeperScene.ts +0 -262
  378. package/src/games/mob-control/MobControl.tsx +0 -73
  379. package/src/games/mob-control/MobControlConstants.ts +0 -106
  380. package/src/games/mob-control/MobControlGame.ts +0 -176
  381. package/src/games/mob-control/MobControlMeshes.ts +0 -85
  382. package/src/games/mob-control/MobControlRenderer.ts +0 -108
  383. package/src/games/mob-control/MobControlRules.ts +0 -522
  384. package/src/games/mob-control/MobControlScene.ts +0 -399
  385. package/src/games/number-search/NumberSearch.tsx +0 -18
  386. package/src/games/number-search/NumberSearchNumbers.ts +0 -623
  387. package/src/games/nuts-and-bolts/NutsAndBolts.tsx +0 -85
  388. package/src/games/nuts-and-bolts/NutsAndBoltsConstants.ts +0 -75
  389. package/src/games/nuts-and-bolts/NutsAndBoltsGame.ts +0 -298
  390. package/src/games/nuts-and-bolts/NutsAndBoltsRules.ts +0 -474
  391. package/src/games/nuts-and-bolts/NutsAndBoltsScene.ts +0 -498
  392. package/src/games/pac-man/PacMan.tsx +0 -86
  393. package/src/games/pac-man/PacManConstants.ts +0 -169
  394. package/src/games/pac-man/PacManGame.ts +0 -406
  395. package/src/games/pac-man/PacManRules.ts +0 -255
  396. package/src/games/pac-man/PacManScene.ts +0 -387
  397. package/src/games/paper-io/PaperIo.tsx +0 -91
  398. package/src/games/paper-io/PaperIoConstants.ts +0 -86
  399. package/src/games/paper-io/PaperIoGame.ts +0 -193
  400. package/src/games/paper-io/PaperIoMeshes.ts +0 -233
  401. package/src/games/paper-io/PaperIoRules.ts +0 -315
  402. package/src/games/paper-io/PaperIoScene.ts +0 -310
  403. package/src/games/penguin-run/PenguinMeshes.ts +0 -269
  404. package/src/games/penguin-run/PenguinModel.ts +0 -712
  405. package/src/games/penguin-run/PenguinRun.tsx +0 -77
  406. package/src/games/penguin-run/PenguinRunConstants.ts +0 -69
  407. package/src/games/penguin-run/PenguinRunGame.ts +0 -406
  408. package/src/games/penguin-run/PenguinRunScene.ts +0 -949
  409. package/src/games/perfect-circle/PerfectCircle.tsx +0 -74
  410. package/src/games/perfect-circle/PerfectCircleConstants.ts +0 -52
  411. package/src/games/perfect-circle/PerfectCircleGame.ts +0 -153
  412. package/src/games/perfect-circle/PerfectCircleRules.ts +0 -111
  413. package/src/games/perfect-circle/PerfectCircleScene.ts +0 -176
  414. package/src/games/pipe-connect/PipeConnect.tsx +0 -69
  415. package/src/games/pipe-connect/PipeConnectConstants.ts +0 -101
  416. package/src/games/pipe-connect/PipeConnectGame.ts +0 -179
  417. package/src/games/pipe-connect/PipeConnectRules.ts +0 -326
  418. package/src/games/pipe-connect/PipeConnectScene.ts +0 -271
  419. package/src/games/play-with-tua/PlayWithTua.tsx +0 -123
  420. package/src/games/play-with-tua/PlayWithTuaConstants.ts +0 -70
  421. package/src/games/play-with-tua/PlayWithTuaGame.ts +0 -167
  422. package/src/games/play-with-tua/PlayWithTuaModel.ts +0 -42
  423. package/src/games/play-with-tua/PlayWithTuaScene.ts +0 -36
  424. package/src/games/popit-fidget/PopitFidget.tsx +0 -72
  425. package/src/games/popit-fidget/PopitFidgetConstants.ts +0 -71
  426. package/src/games/popit-fidget/PopitFidgetGame.ts +0 -148
  427. package/src/games/popit-fidget/PopitFidgetScene.ts +0 -242
  428. package/src/games/reversi/Reversi.tsx +0 -94
  429. package/src/games/reversi/ReversiConstants.ts +0 -67
  430. package/src/games/reversi/ReversiGame.ts +0 -266
  431. package/src/games/reversi/ReversiRules.ts +0 -176
  432. package/src/games/reversi/ReversiScene.ts +0 -240
  433. package/src/games/search-grid/SearchGrid.tsx +0 -92
  434. package/src/games/search-grid/SearchGridConstants.ts +0 -178
  435. package/src/games/search-grid/SearchGridGame.ts +0 -209
  436. package/src/games/search-grid/SearchGridScene.ts +0 -301
  437. package/src/games/shared/BackdropCard.tsx +0 -54
  438. package/src/games/shared/EngineBackdrop.ts +0 -300
  439. package/src/games/shared/EngineBlasts.ts +0 -136
  440. package/src/games/shared/EngineBlocks.ts +0 -91
  441. package/src/games/shared/EngineControlText.ts +0 -17
  442. package/src/games/shared/EngineDice.ts +0 -250
  443. package/src/games/shared/EngineFootprint.ts +0 -35
  444. package/src/games/shared/EngineFruits.ts +0 -1435
  445. package/src/games/shared/EngineGameLoader.tsx +0 -88
  446. package/src/games/shared/EngineGameScreen.tsx +0 -325
  447. package/src/games/shared/EngineGameWidth.ts +0 -3
  448. package/src/games/shared/EngineGameWidth.web.ts +0 -3
  449. package/src/games/shared/EngineGlyphs.ts +0 -178
  450. package/src/games/shared/EngineHud.ts +0 -815
  451. package/src/games/shared/EngineLoading.ts +0 -477
  452. package/src/games/shared/EngineMeadow.ts +0 -725
  453. package/src/games/shared/EngineMeshCache.ts +0 -15
  454. package/src/games/shared/EngineMeshes.ts +0 -10
  455. package/src/games/shared/EngineRacingChase.ts +0 -21
  456. package/src/games/shared/EngineRacingHandling.ts +0 -48
  457. package/src/games/shared/EngineReadyContext.ts +0 -4
  458. package/src/games/shared/EngineTilt.ts +0 -44
  459. package/src/games/shared/EngineTouchSurface.tsx +0 -103
  460. package/src/games/shared/EngineUtils.ts +0 -52
  461. package/src/games/shared/GameActivity.ts +0 -13
  462. package/src/games/shared/GameLoading.tsx +0 -202
  463. package/src/games/shared/GameLoadingTransition.tsx +0 -61
  464. package/src/games/shared/ProceduralCacheVersion.ts +0 -2
  465. package/src/games/shared/useEngineGame.ts +0 -240
  466. package/src/games/shared/useGameResource.ts +0 -21
  467. package/src/games/shared/useLiveState.ts +0 -58
  468. package/src/games/sliding-numbers/SlidingNumbers.tsx +0 -71
  469. package/src/games/sliding-numbers/SlidingNumbersConstants.ts +0 -65
  470. package/src/games/sliding-numbers/SlidingNumbersGame.ts +0 -192
  471. package/src/games/sliding-numbers/SlidingNumbersRules.ts +0 -79
  472. package/src/games/sliding-numbers/SlidingNumbersScene.ts +0 -193
  473. package/src/games/snake-3d/Snake3D.tsx +0 -76
  474. package/src/games/snake-3d/Snake3DConstants.ts +0 -61
  475. package/src/games/snake-3d/Snake3DGame.ts +0 -192
  476. package/src/games/snake-3d/Snake3DMotion.ts +0 -22
  477. package/src/games/snake-3d/Snake3DRules.ts +0 -116
  478. package/src/games/snake-3d/Snake3DScene.ts +0 -322
  479. package/src/games/snakes-and-ladders/SnakesAndLadders.tsx +0 -87
  480. package/src/games/snakes-and-ladders/SnakesAndLaddersConstants.ts +0 -136
  481. package/src/games/snakes-and-ladders/SnakesAndLaddersGame.ts +0 -310
  482. package/src/games/snakes-and-ladders/SnakesAndLaddersScene.ts +0 -515
  483. package/src/games/space-fighter/SpaceFighter.tsx +0 -74
  484. package/src/games/space-fighter/SpaceFighterConstants.ts +0 -86
  485. package/src/games/space-fighter/SpaceFighterGame.ts +0 -292
  486. package/src/games/space-fighter/SpaceFighterScene.ts +0 -550
  487. package/src/games/spider-solitaire/SpiderSolitaire.tsx +0 -80
  488. package/src/games/spider-solitaire/SpiderSolitaireConstants.ts +0 -107
  489. package/src/games/spider-solitaire/SpiderSolitaireGame.ts +0 -387
  490. package/src/games/spider-solitaire/SpiderSolitaireRules.ts +0 -123
  491. package/src/games/spider-solitaire/SpiderSolitaireScene.ts +0 -352
  492. package/src/games/stack-tower/StackTower.tsx +0 -76
  493. package/src/games/stack-tower/StackTowerConstants.ts +0 -74
  494. package/src/games/stack-tower/StackTowerGame.ts +0 -236
  495. package/src/games/stack-tower/StackTowerScene.ts +0 -186
  496. package/src/games/sudoku/Sudoku.tsx +0 -73
  497. package/src/games/sudoku/SudokuConstants.ts +0 -127
  498. package/src/games/sudoku/SudokuGame.ts +0 -250
  499. package/src/games/sudoku/SudokuRules.ts +0 -125
  500. package/src/games/sudoku/SudokuScene.ts +0 -304
  501. package/src/games/tangram/Tangram.tsx +0 -79
  502. package/src/games/tangram/TangramConstants.ts +0 -64
  503. package/src/games/tangram/TangramGame.ts +0 -356
  504. package/src/games/tangram/TangramRules.ts +0 -409
  505. package/src/games/tangram/TangramScene.ts +0 -203
  506. package/src/games/tank-1990/Tank1990.tsx +0 -78
  507. package/src/games/tank-1990/Tank1990Constants.ts +0 -81
  508. package/src/games/tank-1990/Tank1990Game.ts +0 -331
  509. package/src/games/tank-1990/Tank1990Rules.ts +0 -390
  510. package/src/games/tank-1990/Tank1990Scene.ts +0 -434
  511. package/src/games/tic-tac-toe/TicTacToe.tsx +0 -98
  512. package/src/games/tic-tac-toe/TicTacToeConstants.ts +0 -69
  513. package/src/games/tic-tac-toe/TicTacToeGame.ts +0 -200
  514. package/src/games/tic-tac-toe/TicTacToeRules.ts +0 -103
  515. package/src/games/tic-tac-toe/TicTacToeScene.ts +0 -245
  516. package/src/games/tile-home/TileHome.tsx +0 -92
  517. package/src/games/tile-home/TileHomeConstants.ts +0 -139
  518. package/src/games/tile-home/TileHomeGame.ts +0 -435
  519. package/src/games/tile-home/TileHomeScene.ts +0 -154
  520. package/src/games/treasure-hunt/TreasureHunt.tsx +0 -206
  521. package/src/games/treasure-hunt/TreasureHuntConstants.ts +0 -266
  522. package/src/games/treasure-hunt/TreasureHuntControls.ts +0 -280
  523. package/src/games/treasure-hunt/TreasureHuntGame.ts +0 -402
  524. package/src/games/treasure-hunt/TreasureHuntMeshes.ts +0 -422
  525. package/src/games/treasure-hunt/TreasureHuntModel.ts +0 -506
  526. package/src/games/treasure-hunt/TreasureHuntRules.ts +0 -1210
  527. package/src/games/treasure-hunt/TreasureHuntScene.ts +0 -586
  528. package/src/games/whack-a-mole/WhackAMole.tsx +0 -79
  529. package/src/games/whack-a-mole/WhackAMoleConstants.ts +0 -94
  530. package/src/games/whack-a-mole/WhackAMoleGame.ts +0 -233
  531. package/src/games/whack-a-mole/WhackAMoleScene.ts +0 -349
  532. package/src/games/word-search/WordSearch.tsx +0 -18
  533. package/src/games/word-search/WordSearchWords.ts +0 -1642
  534. package/src/helpers/GamePatternData.ts +0 -120
  535. package/src/helpers/GameSettingsModal.tsx +0 -384
  536. package/src/helpers/GameTheme.tsx +0 -37
  537. package/src/helpers/IconPaths.ts +0 -6
  538. package/src/helpers/index.ts +0 -2
  539. package/src/index.tsx +0 -60
  540. package/src/services/GameAssetLoader.ts +0 -111
  541. package/src/services/GameAssets.ts +0 -149
  542. package/src/services/GameLoaders.ts +0 -93
  543. package/src/services/GamesConstants.ts +0 -193
  544. package/src/services/GamesMetadata.ts +0 -711
  545. package/src/services/GamesService.ts +0 -116
  546. package/src/services/HapticsService.ts +0 -48
  547. package/src/services/ScoringService.ts +0 -19
  548. package/src/services/SfxSources.ts +0 -81
  549. package/src/services/SoundsService.ts +0 -582
  550. package/src/services/ThemeService.ts +0 -281
  551. package/src/services/UtilsService.ts +0 -134
  552. package/src/services/index.ts +0 -6
@@ -1,1435 +0,0 @@
1
- // Fruit models for the Nayan engine games (from the engine's example, games/shapes/fruits.ts).
2
- // Fruit built from the engine's custom shapes: lathed bodies with painted skins (blushes, streaks,
3
- // stripes, speckles) and dimpled peel, tube stems and bananas, folded serrated leaves, seeds; each
4
- // merged into one mesh. Every fruit also comes in two pieces for slicing, as in Fruit Ninja.
5
- //
6
- // Pieces share the whole fruit's origin and size. Each has its cut face toward +Z and its solid
7
- // toward -Z: spawn pieces[0] turned a quarter turn about Y (cut facing +X, so it's the left piece)
8
- // and pieces[1] turned the other way, where the fruit was, and push them apart.
9
- import {
10
- type Color,
11
- extrude,
12
- type Geometry,
13
- type GeometryPart,
14
- lathe,
15
- merge,
16
- type Model,
17
- Path,
18
- type Quat,
19
- recolor,
20
- tube,
21
- type Vec3
22
- } from '@nayan-ui/engine';
23
- import { cacheGameAsset, createGameMesh as createShape } from './EngineMeshes';
24
-
25
- export type FruitKind =
26
- | 'apple'
27
- | 'orange'
28
- | 'lemon'
29
- | 'pear'
30
- | 'strawberry'
31
- | 'watermelon'
32
- | 'banana'
33
- | 'cherries'
34
- | 'grapes'
35
- | 'kiwi'
36
- | 'mango'
37
- | 'peach'
38
- | 'pineapple'
39
- | 'pomegranate'
40
- | 'papaya';
41
- export type Fruit = {
42
- whole: Model;
43
- /** The left and right pieces after a slice. */
44
- pieces: [Model, Model];
45
- /** Color of its juice (for splashes). */
46
- juice: Color;
47
- /** Centre of the whole fruit's bounding box (stems and leaves included), in its own units. */
48
- center: Vec3;
49
- };
50
-
51
- /** The middle of a geometry's bounding box. */
52
- function boxCenter(g: Geometry): Vec3 {
53
- const lo = [Infinity, Infinity, Infinity];
54
- const hi = [-Infinity, -Infinity, -Infinity];
55
- for (let i = 0; i < g.vertices.length; i += 11) {
56
- for (let k = 0; k < 3; k++) {
57
- lo[k] = Math.min(lo[k]!, g.vertices[i + k]!);
58
- hi[k] = Math.max(hi[k]!, g.vertices[i + k]!);
59
- }
60
- }
61
- return [(lo[0]! + hi[0]!) / 2, (lo[1]! + hi[1]!) / 2, (lo[2]! + hi[2]!) / 2];
62
- }
63
-
64
- const TAU = Math.PI * 2;
65
-
66
- // ── Little helpers: noise, colors, rotations ─────────────────────────────
67
-
68
- const fract = (x: number) => x - Math.floor(x);
69
- const hash = (x: number, y: number) => fract(Math.sin(x * 127.1 + y * 311.7) * 43758.5453);
70
- const smooth = (t: number) => t * t * (3 - 2 * t);
71
- const clamp01 = (x: number) => Math.max(0, Math.min(1, x));
72
- const smoothstep = (a: number, b: number, x: number) => smooth(clamp01((x - a) / (b - a)));
73
-
74
- /** Value noise, 0..1, repeating every `cu` cells in u (so it wraps around a lathe without a seam). */
75
- function noise(u: number, v: number, cu: number, cv: number) {
76
- const x = u * cu;
77
- const y = v * cv;
78
- const ix = Math.floor(x);
79
- const iy = Math.floor(y);
80
- const fx = smooth(x - ix);
81
- const fy = smooth(y - iy);
82
- const h = (i: number, j: number) => hash(((i % cu) + cu) % cu, j);
83
- const top = h(ix, iy) + (h(ix + 1, iy) - h(ix, iy)) * fx;
84
- const bottom = h(ix, iy + 1) + (h(ix + 1, iy + 1) - h(ix, iy + 1)) * fx;
85
- return top + (bottom - top) * fy;
86
- }
87
-
88
- const mix = (a: Color, b: Color, t: number): Color => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
89
- const shade = (c: Color, k: number): Color => [c[0] * k, c[1] * k, c[2] * k];
90
-
91
- const turn = (axis: Vec3, angle: number): Quat => [
92
- axis[0] * Math.sin(angle / 2),
93
- axis[1] * Math.sin(angle / 2),
94
- axis[2] * Math.sin(angle / 2),
95
- Math.cos(angle / 2)
96
- ];
97
- function mul(a: Quat, b: Quat): Quat {
98
- const [ax, ay, az, aw] = a;
99
- const [bx, by, bz, bw] = b;
100
- return [
101
- aw * bx + ax * bw + ay * bz - az * by,
102
- aw * by - ax * bz + ay * bw + az * bx,
103
- aw * bz + ax * by - ay * bx + az * bw,
104
- aw * bw - ax * bx - ay * by - az * bz
105
- ];
106
- }
107
- /** The rotation taking +Y to direction `n` (unit). */
108
- function upTo(n: Vec3): Quat {
109
- const axis: Vec3 = [n[2], 0, -n[0]];
110
- const len = Math.hypot(axis[0], axis[2]);
111
- if (len < 1e-6) return n[1] > 0 ? [0, 0, 0, 1] : [1, 0, 0, 0];
112
- return turn([axis[0] / len, 0, axis[2] / len], Math.acos(Math.max(-1, Math.min(1, n[1]))));
113
- }
114
-
115
- /** A rotation about Y (pieces are turned so their cut faces +Z). */
116
- const yTurn = (angle: number) => turn([0, 1, 0], angle);
117
-
118
- /**
119
- * Bends geometry in place: every vertex moved by `f`, its normal carried along (by the inverse
120
- * transpose of f's derivative), so curves, leans and grooves still catch the light. For shapes a
121
- * lathe can't make alone: a mango's kidney curve, a peach's groove. Cut faces bend with the skin.
122
- */
123
- function warp(g: Geometry, f: (p: Vec3) => Vec3): Geometry {
124
- const v = g.vertices;
125
- const e = 1e-4;
126
- const sub = (a: Vec3, b: Vec3): Vec3 => [(a[0] - b[0]) / e, (a[1] - b[1]) / e, (a[2] - b[2]) / e];
127
- const cross = (a: Vec3, b: Vec3): Vec3 => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
128
- for (let i = 0; i < v.length; i += 11) {
129
- const [x, y, z] = [v[i]!, v[i + 1]!, v[i + 2]!];
130
- const q = f([x, y, z]);
131
- const [jx, jy, jz] = [sub(f([x + e, y, z]), q), sub(f([x, y + e, z]), q), sub(f([x, y, z + e]), q)];
132
- const [a, b, c] = [cross(jy, jz), cross(jz, jx), cross(jx, jy)];
133
- const [nx, ny, nz] = [v[i + 3]!, v[i + 4]!, v[i + 5]!];
134
- const n = [0, 1, 2].map((k) => nx * a[k]! + ny * b[k]! + nz * c[k]!);
135
- const len = Math.hypot(n[0]!, n[1]!, n[2]!);
136
- v.set(q, i);
137
- if (len > 1e-12) v.set([n[0]! / len, n[1]! / len, n[2]! / len], i + 3);
138
- }
139
- return g;
140
- }
141
-
142
- // ── Profiles ─────────────────────────────────────────────────────────────
143
-
144
- type Profile = [number, number][];
145
-
146
- /** A profile from r(t), t going 0 (top) to 1 (bottom), `height` tall. */
147
- /**
148
- * Mesh detail for the fruit being built (1 = full). Games that show fruit small (Tile Home's 72 tiles)
149
- * build lighter meshes: around and along each lathe, vertex counts scale by this.
150
- */
151
- let detail = 1;
152
- /** A segment / step count at the current detail (small counts are kept: seeds, grapes). */
153
- const seg = (n: number) => (n <= 8 ? n : Math.max(8, Math.round(n * detail)));
154
-
155
- function profile(height: number, r: (t: number) => number, steps = 48): Profile {
156
- steps = seg(steps);
157
- const out: Profile = [];
158
- for (let i = 0; i <= steps; i++) {
159
- const t = i / steps;
160
- out.push([Math.max(0, r(t)), height / 2 - t * height]);
161
- }
162
- return out;
163
- }
164
-
165
- /** Evenly spaced points along a profile, so painted details have vertices to land on. */
166
- function resample(p: Profile, n: number): Profile {
167
- n = seg(n);
168
- const lengths = [0];
169
- for (let i = 1; i < p.length; i++) lengths.push(lengths[i - 1]! + Math.hypot(p[i]![0] - p[i - 1]![0], p[i]![1] - p[i - 1]![1]));
170
- const total = lengths[lengths.length - 1]!;
171
- const out: Profile = [];
172
- let k = 0;
173
- for (let i = 0; i <= n; i++) {
174
- const at = (i / n) * total;
175
- while (k < p.length - 2 && lengths[k + 1]! < at) k++;
176
- const t = (at - lengths[k]!) / (lengths[k + 1]! - lengths[k]! || 1);
177
- out.push([p[k]![0] + (p[k + 1]![0] - p[k]![0]) * t, p[k]![1] + (p[k + 1]![1] - p[k]![1]) * t]);
178
- }
179
- return out;
180
- }
181
-
182
- /** A rounded profile: an ellipse when `power` is 2, blunter above (a barrel, a kiwi), `height` tall. */
183
- const rounded = (height: number, radius: number, power = 2, steps = 64) =>
184
- profile(height, (t) => radius * Math.pow(Math.max(0, 1 - Math.pow(Math.abs(2 * t - 1), power)), 1 / power), steps);
185
-
186
- /** A profile's radius at height `y` (for placing things on a cut face). */
187
- function radiusAt(p: Profile, y: number) {
188
- let best = 0;
189
- for (let i = 0; i < p.length - 1; i++) {
190
- const [r0, y0] = p[i]!;
191
- const [r1, y1] = p[i + 1]!;
192
- if ((y - y0) * (y - y1) <= 0 && y0 !== y1) best = Math.max(best, r0 + ((y - y0) / (y1 - y0)) * (r1 - r0));
193
- }
194
- return best;
195
- }
196
-
197
- /**
198
- * A dimpled profile from its highest point to its lowest, for a piece cut through the core: the cut
199
- * face of a profile that turns back is filled without inner points, so painted cores wouldn't show.
200
- */
201
- function undimpled(p: Profile): Profile {
202
- let [top, bottom] = [0, 0];
203
- p.forEach((q, i) => {
204
- if (q[1] > p[top]![1]) top = i;
205
- if (q[1] < p[bottom]![1]) bottom = i;
206
- });
207
- return p.slice(top, bottom + 1);
208
- }
209
-
210
- /** Scales a profile so its lathe is 1 unit across its largest side, centered on the origin. */
211
- function normalize(p: Profile): Profile {
212
- const top = Math.max(...p.map((q) => q[1]));
213
- const bottom = Math.min(...p.map((q) => q[1]));
214
- const width = 2 * Math.max(...p.map((q) => q[0]));
215
- const s = 1 / Math.max(width, top - bottom);
216
- const mid = (top + bottom) / 2;
217
- return p.map(([r, y]) => [r * s, (y - mid) * s]);
218
- }
219
-
220
- /** The point and outward normal of a lathe of `p` at `v` down the profile and `angle` around. */
221
- function onSurface(p: Profile, v: number, angle: number): [Vec3, Vec3] {
222
- const f = v * (p.length - 1);
223
- const i = Math.min(p.length - 2, Math.floor(f));
224
- const t = f - i;
225
- const [r0, y0] = p[i]!;
226
- const [r1, y1] = p[i + 1]!;
227
- const r = r0 + (r1 - r0) * t;
228
- const y = y0 + (y1 - y0) * t;
229
- const len = Math.hypot(r1 - r0, y1 - y0) || 1;
230
- const [nr, ny] = [(y0 - y1) / len, (r1 - r0) / len];
231
- const [c, s] = [Math.cos(angle), -Math.sin(angle)];
232
- return [
233
- [r * c, y, r * s],
234
- [nr * c, ny, nr * s]
235
- ];
236
- }
237
-
238
- // ── Parts: leaves, stems, seeds ──────────────────────────────────────────
239
-
240
- const LEAF_TOP: Color = [0.24, 0.56, 0.17];
241
- const LEAF_UNDER: Color = [0.19, 0.46, 0.14];
242
- const VEIN: Color = [0.55, 0.74, 0.36];
243
-
244
- /**
245
- * A leaf from its base at the origin to its tip at +X, `length` long: two serrated halves folded up
246
- * along a pale midrib (a V, like a real leaf), on a short stalk.
247
- */
248
- function leaf(length: number, width: number, { fold = 0.38, teeth = 14, stalk = 0.16 } = {}): Geometry {
249
- // One half's outline (y up): along the serrated edge to the tip, then back down the midrib.
250
- const half = (sign: 1 | -1) => {
251
- const pts: [number, number][] = [[0, 0]];
252
- const steps = teeth * 2;
253
- for (let i = 1; i < steps; i++) {
254
- const s = i / steps;
255
- const tooth = i % 2 ? 0.07 : 0; // little saw teeth along the edge
256
- const w = width * Math.pow(Math.sin(Math.PI * s), 0.75) * (1 - 0.25 * s) * (1 + tooth);
257
- pts.push([length * s, sign * w]);
258
- }
259
- pts.push([length, 0]);
260
- // Path coordinates are y down.
261
- return extrude(new Path().addPolygon(pts.map(([x, y]) => [x, -y] as [number, number])), { depth: 0.006 });
262
- };
263
- return merge([
264
- { geometry: half(1), rotation: turn([1, 0, 0], fold), color: LEAF_TOP },
265
- { geometry: half(-1), rotation: turn([1, 0, 0], -fold), color: LEAF_UNDER },
266
- {
267
- geometry: tube(
268
- [
269
- [0, 0, 0],
270
- [length * 0.5, 0.004, 0],
271
- [length * 0.96, 0, 0]
272
- ],
273
- { radius: (t) => 0.007 * (1 - t) + 0.002, sides: seg(6) }
274
- ),
275
- color: VEIN
276
- },
277
- {
278
- geometry: tube(
279
- [
280
- [-length * stalk, -0.01, 0],
281
- [0, 0, 0]
282
- ],
283
- { radius: 0.008, sides: seg(6) }
284
- ),
285
- color: [0.36, 0.42, 0.18]
286
- }
287
- ]);
288
- }
289
-
290
- /** A woody stem curving up from the origin, `height` tall, with a knobbly, paler cut end. */
291
- function stem(height: number, radius = 0.024, bend = 0.06): Geometry {
292
- return tube(
293
- [
294
- [0, -0.03, 0],
295
- [0.005, height * 0.4, 0],
296
- [bend * 0.5, height * 0.8, 0],
297
- [bend, height, 0]
298
- ],
299
- {
300
- radius: (t) => radius * (1.25 - 0.45 * t + (t > 0.9 ? 0.5 * (t - 0.9) * 10 : 0)),
301
- sides: seg(8),
302
- color: (p) => mix([0.33, 0.21, 0.1], [0.5, 0.42, 0.28], smoothstep(0.75, 1, p.v) + 0.15 * noise(p.u, p.v, 6, 10)),
303
- capColor: [0.62, 0.55, 0.4]
304
- }
305
- );
306
- }
307
-
308
- /** A seed: a small teardrop, 1 unit tall before scaling, pointing up. */
309
- const seed = (segments = 8) =>
310
- lathe(
311
- [
312
- [0, 0.5],
313
- [0.22, 0.32],
314
- [0.4, 0.02],
315
- [0.36, -0.28],
316
- [0.2, -0.45],
317
- [0, -0.5]
318
- ],
319
- { segments }
320
- );
321
-
322
- /** A flat, pointed star (a calyx or the scar where a stem was). */
323
- const calyx = (outer: number, inner: number, points: number, depth = 0.012) => extrude(new Path().addStar(0, 0, outer, inner, points), { depth });
324
-
325
- /** Faces a geometry made facing +Z (an extrusion) up (+Y) instead. */
326
- const FACE_UP = turn([1, 0, 0], -Math.PI / 2);
327
- /** Turns a piece cut across (cut face down, -Y, solid up) to the convention: cut face +Z, solid -Z. */
328
- const CUT_ACROSS = turn([1, 0, 0], -Math.PI / 2);
329
-
330
- // ── The fruit ────────────────────────────────────────────────────────────
331
-
332
- const SKIN_SEGMENTS = 72;
333
-
334
- type Skin = (p: { u: number; v: number }) => Color;
335
- /** A piece's painter: its u (around half a turn) or v (down half the profile) mapped onto the whole's. */
336
- const halfTurn =
337
- (skin: Skin): Skin =>
338
- (q) =>
339
- skin({ u: q.u * 0.5, v: q.v });
340
- const upperHalf =
341
- (skin: Skin): Skin =>
342
- (q) =>
343
- skin({ u: q.u, v: q.v * 0.5 });
344
- const upperDisplace = (d: (u: number, v: number) => number) => (u: number, v: number) => d(u, v * 0.5);
345
-
346
- function apple(): Fruit {
347
- // Drawn as a path (x = distance from the core, y down): dimpled at the top and bottom.
348
- const outline = Path.fromSVG('M0 0.1 C 0.12 -0.03 0.42 -0.06 0.5 0.3 C 0.55 0.62 0.36 0.9 0.14 0.93 Q 0.05 0.94 0 0.88');
349
- const p = normalize(
350
- resample(
351
- outline.toContours()[0]!.map(([x, y]) => [x, -y]),
352
- 64
353
- )
354
- );
355
- const RED: Color = [0.68, 0.04, 0.06];
356
- const DEEP: Color = [0.45, 0.02, 0.05];
357
- const GOLD: Color = [0.86, 0.66, 0.18];
358
- const skin = ({ u, v }: { u: number; v: number }): Color => {
359
- // Streaky red over a gold-green ground that shows near the stem and the bottom.
360
- const streak = noise(u, v, 90, 4);
361
- let c = mix(RED, DEEP, smoothstep(0.35, 0.9, streak) * 0.7);
362
- c = mix(c, GOLD, smoothstep(0.55, 0.8, noise(u, v, 7, 3)) * 0.35); // a sunny patch
363
- c = mix(c, [0.72, 0.68, 0.22], smoothstep(0.16, 0.02, v) * 0.75 + smoothstep(0.86, 0.99, v) * 0.6);
364
- // Lenticels: tiny pale dots.
365
- if (hash(Math.floor(u * 140), Math.floor(v * 70)) > 0.975) c = mix(c, [0.95, 0.85, 0.55], 0.55);
366
- return c;
367
- };
368
- const top = p[0]![1];
369
- const bottom = p[p.length - 1]![1];
370
- const body = lathe(p, { segments: seg(SKIN_SEGMENTS), color: skin });
371
- const whole = merge([
372
- { geometry: body },
373
- { geometry: stem(0.17, 0.022, 0.05), position: [0, top, 0] },
374
- { geometry: leaf(0.32, 0.075), position: [0.03, top + 0.11, 0], rotation: mul(turn([0, 1, 0], -0.5), turn([0, 0, 1], 0.55)) },
375
- { geometry: calyx(0.05, 0.015, 5), position: [0, bottom - 0.004, 0], rotation: turn([1, 0, 0], Math.PI / 2), color: [0.3, 0.2, 0.1] }
376
- ]);
377
- // Cut through the core: cream flesh, a faint core outline around the axis, two seeds.
378
- const flesh = (r: number, y: number): Color => {
379
- const core = Math.hypot(r / 0.2, y / 0.24);
380
- let c: Color = [0.97, 0.93, 0.74];
381
- c = mix(c, [0.92, 0.88, 0.62], smoothstep(1.05, 0.85, core)); // the core is a touch greener
382
- c = mix(c, [0.82, 0.76, 0.5], smoothstep(0.12, 0, Math.abs(core - 1)) * 0.6); // its outline
383
- return mix(c, [1, 0.97, 0.88], smoothstep(0.88, 1, r) * 0.6); // paler just under the skin
384
- };
385
- const pieces = (): [Model, Model] => {
386
- const half = merge([
387
- {
388
- geometry: lathe(undimpled(p), {
389
- segments: seg(SKIN_SEGMENTS),
390
- sweep: Math.PI,
391
- color: halfTurn(skin),
392
- cutColor: flesh,
393
- cutSteps: 24,
394
- capColor: [0.7, 0.62, 0.25]
395
- })
396
- },
397
- ...[-1, 1].map((side): GeometryPart => ({
398
- geometry: seed(),
399
- position: [side * 0.06, -0.02, 0.004],
400
- scale: [0.045, 0.075, 0.02],
401
- rotation: turn([0, 0, 1], side * 0.35),
402
- color: [0.3, 0.17, 0.08]
403
- }))
404
- ]);
405
- const piece = createShape(half, { center: false, shine: 0.3 });
406
- return [piece, piece];
407
- };
408
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.65 }), juice: [0.98, 0.94, 0.75] }, pieces);
409
- }
410
-
411
- /** Citrus segments on a face cut across: rind, white pith, wedges with pale membranes, a pithy core. */
412
- function citrusFace(rind: Color, pith: Color, pulp: Color, wedges: number, pithWidth: number) {
413
- return (r: number, angle: number): Color => {
414
- if (r > 1 - pithWidth * 0.45) return rind;
415
- if (r > 1 - pithWidth) return pith;
416
- if (r < 0.12) return mix(pith, pulp, smoothstep(0.08, 0.12, r));
417
- const w = TAU / wedges;
418
- const across = Math.abs(fract(angle / w) - 0.5) * w * r; // distance from the wedge's middle, along the arc
419
- const membrane = smoothstep(0.5 * w * r - 0.02, 0.5 * w * r - 0.006, across);
420
- // Juice sacs: slightly lighter and darker grains, longer toward the rim.
421
- const grain = noise(angle / TAU, r, wedges * 6, 9);
422
- return mix(shade(pulp, 0.92 + 0.16 * grain), pith, membrane * 0.85);
423
- };
424
- }
425
-
426
- /** Pitted peel: shallow pores pushed into the surface. */
427
- const peel = (depth: number, fine: number) => (u: number, v: number) =>
428
- -depth * Math.pow(noise(u, v, fine, fine * 0.55), 2) - depth * 0.5 * noise(u, v, fine * 2.5, fine * 1.4);
429
-
430
- function orange(): Fruit {
431
- const p = normalize(rounded(0.94, 0.5, 2, 64));
432
- const ORANGE: Color = [1, 0.47, 0.03];
433
- const skin = ({ u, v }: { u: number; v: number }): Color => {
434
- let c = shade(ORANGE, 0.9 + 0.14 * noise(u, v, 40, 22));
435
- c = mix(c, [0.82, 0.55, 0.12], smoothstep(0.05, 0, v) * 0.6); // green-brown around the stem
436
- return mix(c, [0.75, 0.35, 0.05], smoothstep(0.95, 1, v) * 0.7); // the navel
437
- };
438
- const displace = peel(0.006, 64);
439
- const top = p[0]![1];
440
- const whole = merge([
441
- { geometry: lathe(p, { segments: seg(120), color: skin, displace }) },
442
- { geometry: calyx(0.06, 0.025, 5, 0.01), position: [0, top - 0.002, 0], rotation: FACE_UP, color: [0.3, 0.42, 0.14] },
443
- {
444
- geometry: tube(
445
- [
446
- [0, top - 0.01, 0],
447
- [0, top + 0.035, 0]
448
- ],
449
- { radius: 0.014, sides: seg(6) }
450
- ),
451
- color: [0.42, 0.4, 0.2]
452
- }
453
- ]);
454
- const pieces = (): [Model, Model] => {
455
- // Cut across the middle: the top half, its cut face showing ten segments.
456
- const upper = p.slice(0, Math.floor(p.length / 2) + 1);
457
- const face = citrusFace(shade(ORANGE, 0.95), [0.99, 0.95, 0.84], [1, 0.58, 0.08], 10, 0.12);
458
- const piece = createShape(
459
- merge([
460
- {
461
- geometry: lathe(upper, { segments: seg(120), color: upperHalf(skin), displace: upperDisplace(displace), capColor: face, capRings: 24 }),
462
- rotation: CUT_ACROSS
463
- }
464
- ]),
465
- {
466
- center: false,
467
- shine: 0.25
468
- }
469
- );
470
- return [piece, piece];
471
- };
472
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.35 }), juice: [1, 0.6, 0.1] }, pieces);
473
- }
474
-
475
- function lemon(): Fruit {
476
- // Pointed at both ends; laid on its side.
477
- const p = normalize(profile(1, (t) => 0.34 * Math.pow(Math.sin(Math.PI * t), 0.75), 64));
478
- const YELLOW: Color = [1, 0.86, 0.12];
479
- const skin = ({ u, v }: { u: number; v: number }): Color => {
480
- const c = shade(YELLOW, 0.9 + 0.14 * noise(u, v, 36, 20));
481
- return mix(c, [0.62, 0.72, 0.15], (smoothstep(0.06, 0, v) + smoothstep(0.94, 1, v)) * 0.6); // greenish tips
482
- };
483
- const displace = peel(0.005, 56);
484
- const side = turn([0, 0, 1], Math.PI / 2);
485
- const whole = merge([{ geometry: lathe(p, { segments: seg(96), color: skin, displace }), rotation: side }]);
486
- const pieces = (): [Model, Model] => {
487
- const upper = p.slice(0, Math.floor(p.length / 2) + 1);
488
- const face = citrusFace(YELLOW, [0.99, 0.97, 0.86], [1, 0.93, 0.5], 9, 0.1);
489
- // Cut across the middle of its length (the cut face toward +Z, the rest along -Z, as the whole lies).
490
- const piece = createShape(
491
- merge([
492
- {
493
- geometry: lathe(upper, { segments: seg(96), color: upperHalf(skin), displace: upperDisplace(displace), capColor: face, capRings: 20 }),
494
- rotation: CUT_ACROSS
495
- }
496
- ]),
497
- {
498
- center: false,
499
- shine: 0.3
500
- }
501
- );
502
- return [piece, piece];
503
- };
504
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.4 }), juice: [1, 0.95, 0.55] }, pieces);
505
- }
506
-
507
- function pear(): Fruit {
508
- // Narrow neck, round bottom, a slight dimple where the stem goes in.
509
- const p = normalize(
510
- profile(1, (t) => Math.pow(Math.sin(Math.PI * t), 0.55) * (0.18 + 0.37 * Math.pow(t, 1.35)) * (1 - 0.15 * Math.exp(-t * 30)), 64)
511
- );
512
- const skin = ({ u, v }: { u: number; v: number }): Color => {
513
- let c = mix([0.62, 0.74, 0.2], [0.84, 0.8, 0.28], noise(u, v, 6, 4));
514
- const blush = Math.pow(Math.max(0, Math.cos((u - 0.2) * TAU)), 2) * smoothstep(0.3, 0.8, v);
515
- c = mix(c, [0.78, 0.28, 0.12], blush * 0.55); // a red cheek on the sunny side
516
- c = mix(c, [0.55, 0.42, 0.18], smoothstep(0.12, 0, v) * 0.6); // russet neck
517
- if (hash(Math.floor(u * 120), Math.floor(v * 64)) > 0.95) c = mix(c, [0.5, 0.38, 0.18], 0.65); // speckles
518
- return c;
519
- };
520
- const top = p[0]![1];
521
- const bottom = p[p.length - 1]![1];
522
- const whole = merge([
523
- { geometry: lathe(p, { segments: seg(SKIN_SEGMENTS), color: skin }) },
524
- { geometry: stem(0.2, 0.02, 0.07), position: [0, top, 0] },
525
- { geometry: calyx(0.035, 0.012, 5), position: [0, bottom - 0.003, 0], rotation: turn([1, 0, 0], Math.PI / 2), color: [0.32, 0.22, 0.12] }
526
- ]);
527
- // The core sits low in a pear, with its seeds.
528
- const flesh = (r: number, y: number): Color => {
529
- const core = Math.hypot(r / 0.17, (y + 0.15) / 0.17);
530
- let c: Color = [0.98, 0.96, 0.82];
531
- c = mix(c, [0.92, 0.9, 0.7], smoothstep(1.1, 0.8, core));
532
- return mix(c, [0.86, 0.82, 0.58], smoothstep(0.1, 0, Math.abs(core - 1)) * 0.6);
533
- };
534
- const pieces = (): [Model, Model] => {
535
- const half = merge([
536
- { geometry: lathe(p, { segments: seg(SKIN_SEGMENTS), sweep: Math.PI, color: halfTurn(skin), cutColor: flesh }) },
537
- ...[-1, 1].map((side): GeometryPart => ({
538
- geometry: seed(),
539
- position: [side * 0.05, -0.16, 0.004],
540
- scale: [0.035, 0.06, 0.018],
541
- rotation: turn([0, 0, 1], side * 0.3),
542
- color: [0.25, 0.14, 0.07]
543
- }))
544
- ]);
545
- const piece = createShape(half, { center: false, shine: 0.25 });
546
- return [piece, piece];
547
- };
548
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.45 }), juice: [0.98, 0.96, 0.8] }, pieces);
549
- }
550
-
551
- function strawberry(): Fruit {
552
- // Broad shoulders, a pointed tip.
553
- const p = normalize(profile(1, (t) => 0.5 * Math.pow(Math.sin(Math.PI * Math.min(1, t * 1.02)), 0.45) * (1 - 0.62 * Math.pow(t, 1.2)), 64));
554
- const skin = ({ v }: { v: number }): Color => {
555
- let c: Color = [0.82, 0.05, 0.1];
556
- c = mix(c, [0.95, 0.8, 0.62], smoothstep(0.1, 0.02, v) * 0.7); // pale under the leaves
557
- return mix(c, [0.95, 0.25, 0.12], smoothstep(0.8, 1, v) * 0.5); // a lighter, orange-red tip
558
- };
559
- // Seeds (achenes) spiral over the surface, every one at the golden angle from the last.
560
- const seeds = (sweep: number): GeometryPart[] => {
561
- const out: GeometryPart[] = [];
562
- const n = 80;
563
- for (let i = 0; i < n; i++) {
564
- const v = 0.14 + (0.8 * i) / n;
565
- const angle = fract(i * 0.381966) * TAU;
566
- if (angle > sweep) continue;
567
- const [pos, normal] = onSurface(p, v, angle);
568
- out.push({
569
- geometry: seed(6),
570
- position: [pos[0] - normal[0] * 0.003, pos[1] - normal[1] * 0.003, pos[2] - normal[2] * 0.003], // half sunk into the skin
571
- rotation: upTo(normal),
572
- scale: [0.022, 0.018, 0.03],
573
- color: [0.95, 0.8, 0.3]
574
- });
575
- }
576
- return out;
577
- };
578
- const top = p[0]![1];
579
- // Sepals: narrow pointed leaves fanned around the top, drooping over the shoulders.
580
- const sepals: GeometryPart[] = [];
581
- for (let i = 0; i < 8; i++) {
582
- const around = (i / 8) * TAU + 0.2;
583
- sepals.push({
584
- geometry: leaf(0.26 + 0.05 * Math.sin(i * 2.3), 0.05, { fold: 0.2, teeth: 4, stalk: 0 }),
585
- position: [0, top + 0.012, 0],
586
- rotation: mul(turn([0, 1, 0], around), turn([0, 0, 1], 0.08 - 0.1 * Math.cos(i * 1.7)))
587
- });
588
- }
589
- const crown: GeometryPart[] = [...sepals, { geometry: stem(0.12, 0.018, 0.02), position: [0, top - 0.02, 0], color: [0.55, 1.2, 0.5] }];
590
- const whole = merge([{ geometry: lathe(p, { segments: seg(SKIN_SEGMENTS), color: skin }) }, ...seeds(TAU), ...crown]);
591
- // Cut lengthwise: a pale heart fading to red at the edge.
592
- const flesh = (r: number, y: number): Color => {
593
- const c = mix([1, 0.85, 0.82], [0.88, 0.12, 0.16], smoothstep(0.35, 0.85, r));
594
- return mix(c, [0.98, 0.75, 0.72], smoothstep(0.12, 0, Math.abs(r - 0.32)) * 0.4 * smoothstep(-0.4, 0.2, y)); // faint vascular ring
595
- };
596
- const pieces = (): [Model, Model] => {
597
- const piece = createShape(
598
- merge([{ geometry: lathe(p, { segments: seg(SKIN_SEGMENTS), sweep: Math.PI, color: halfTurn(skin), cutColor: flesh }) }, ...seeds(Math.PI)]),
599
- {
600
- center: false,
601
- shine: 0.4
602
- }
603
- );
604
- return [piece, piece];
605
- };
606
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.55 }), juice: [0.95, 0.15, 0.2] }, pieces);
607
- }
608
-
609
- function watermelon(): Fruit {
610
- // Oblong: lathed along its length, then laid on its side.
611
- const p = normalize(rounded(1, 0.36, 2.2, 64));
612
- const DARK: Color = [0.07, 0.27, 0.08];
613
- const LIGHT: Color = [0.42, 0.62, 0.26];
614
- const skin = ({ u, v }: { u: number; v: number }): Color => {
615
- // Dark, jagged stripes running end to end over a pale ground.
616
- const wobble = (noise(u, v, 16, 10) - 0.5) * 0.9;
617
- const stripe = Math.sin((u * 16 + wobble) * TAU);
618
- let c = mix(LIGHT, DARK, smoothstep(-0.35, 0.1, stripe));
619
- c = shade(c, 0.9 + 0.15 * noise(u, v, 50, 30));
620
- // The field spot where it lay on the ground: a creamy yellow patch on one side.
621
- const spot = smoothstep(0.16, 0.05, Math.hypot((fract(u - 0.75 + 0.5) - 0.5) * 2.2, v - 0.5));
622
- return mix(c, [0.82, 0.76, 0.38], spot * 0.85);
623
- };
624
- const side = turn([0, 0, 1], Math.PI / 2);
625
- const whole = merge([{ geometry: lathe(p, { segments: seg(96), color: skin }), rotation: side }]);
626
- // Cut across the middle: dark rind, a pale band, white, then red flesh, deeper toward the middle.
627
- const face = (r: number): Color => {
628
- if (r > 0.965) return DARK;
629
- if (r > 0.92) return mix([0.62, 0.8, 0.45], [0.85, 0.93, 0.7], (0.965 - r) / 0.045);
630
- if (r > 0.86) return mix([0.97, 0.97, 0.85], [0.98, 0.55, 0.5], smoothstep(0.92, 0.86, r));
631
- return mix([0.98, 0.3, 0.32], [0.88, 0.1, 0.16], smoothstep(0.85, 0.2, r));
632
- };
633
- const pieces = (): [Model, Model] => {
634
- const upper = p.slice(0, Math.floor(p.length / 2) + 1);
635
- const r0 = upper[upper.length - 1]![0]; // the cut face's radius
636
- const blackSeeds: GeometryPart[] = [];
637
- for (let i = 0; i < 14; i++) {
638
- const a = (i / 14) * TAU + (i % 2) * 0.12;
639
- const rr = r0 * (0.5 + 0.1 * (i % 3));
640
- // On the cut face (y = 0, facing -Y in the lathe), pointing out from the middle.
641
- blackSeeds.push({
642
- geometry: seed(),
643
- position: [Math.cos(a) * rr, -0.004, -Math.sin(a) * rr],
644
- rotation: mul(turn([0, 1, 0], a + Math.PI / 2), turn([1, 0, 0], Math.PI / 2)), // lying flat, pointing outward
645
- scale: [0.03, 0.05, 0.012],
646
- color: [0.08, 0.05, 0.04]
647
- });
648
- }
649
- const half = merge([
650
- {
651
- geometry: merge([{ geometry: lathe(upper, { segments: seg(96), color: upperHalf(skin), capColor: face, capRings: 24 }) }, ...blackSeeds]),
652
- rotation: CUT_ACROSS
653
- }
654
- ]);
655
- const piece = createShape(half, { center: false, shine: 0.25 });
656
- return [piece, piece];
657
- };
658
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.35 }), juice: [0.95, 0.2, 0.25] }, pieces);
659
- }
660
-
661
- function banana(): Fruit {
662
- // A tapering, five-sided curve along X, centered, 1 unit from tip to stem.
663
- const raw: Vec3[] = [];
664
- for (let i = 0; i <= 12; i++) {
665
- const a = Math.PI * (0.12 + (0.76 * i) / 12);
666
- raw.push([-Math.cos(a) * 0.5, -Math.sin(a) * 0.3, 0]);
667
- }
668
- const xs = raw.map((q) => q[0]);
669
- const ys = raw.map((q) => q[1]);
670
- const [cx, cy] = [(Math.min(...xs) + Math.max(...xs)) / 2, (Math.min(...ys) + Math.max(...ys)) / 2];
671
- const scale = 0.92 / (Math.max(...xs) - Math.min(...xs));
672
- const arc: Vec3[] = raw.map(([x, y]) => [(x - cx) * scale, (y - cy) * scale + 0.06, 0]);
673
- const radius = (t: number) => 0.014 + 0.09 * Math.pow(Math.sin(Math.PI * Math.min(1, 0.03 + t * 0.97)), 0.55);
674
- const ridges = (around: number) => 1 + 0.06 * Math.cos(around * TAU * 5);
675
- const skin = ({ u, v }: { u: number; v: number }): Color => {
676
- let c: Color = [1, 0.84, 0.16];
677
- c = mix(c, [0.82, 0.76, 0.18], 0.4 * (0.5 + 0.5 * Math.cos(u * TAU * 5))); // darker in the grooves
678
- c = mix(c, [0.62, 0.72, 0.2], smoothstep(0.82, 1, v) * 0.6); // still green by the stem
679
- c = mix(c, [0.18, 0.12, 0.06], smoothstep(0.05, 0, v)); // the dark blossom tip
680
- // Sugar spots.
681
- const spot = noise(u, v, 18, 26);
682
- if (spot > 0.78) c = mix(c, [0.45, 0.28, 0.08], smoothstep(0.78, 0.88, spot) * 0.9);
683
- return c;
684
- };
685
- const flesh = (r: number): Color => (r > 0.9 ? [1, 0.84, 0.16] : r < 0.22 ? [0.86, 0.8, 0.58] : [0.99, 0.95, 0.78]);
686
- // The stalk: green-brown, a little square, with a darker cut end.
687
- const end = arc[arc.length - 1]!;
688
- const prev = arc[arc.length - 2]!;
689
- const dir = [end[0] - prev[0], end[1] - prev[1]];
690
- const len = Math.hypot(dir[0]!, dir[1]!);
691
- const stalkEnd: Vec3 = [end[0] + (dir[0]! / len) * 0.1, end[1] + (dir[1]! / len) * 0.1 + 0.015, 0];
692
- const stalk = tube([end, stalkEnd], {
693
- radius: (t) => 0.026 - 0.006 * t,
694
- shape: (around) => 1 + 0.12 * Math.cos(around * TAU * 4),
695
- sides: seg(12),
696
- color: [0.45, 0.48, 0.2],
697
- capColor: [0.3, 0.25, 0.12]
698
- });
699
- const body = (points: Vec3[], t0: number, t1: number) =>
700
- tube(points, {
701
- radius: (t) => radius(t0 + (t1 - t0) * t),
702
- shape: ridges,
703
- sides: seg(30),
704
- smoothness: 6,
705
- color: (p) => skin({ u: p.u, v: t0 + (t1 - t0) * p.v }),
706
- capColor: (r) => flesh(r)
707
- });
708
- const whole = merge([{ geometry: body(arc, 0, 1) }, { geometry: stalk }]);
709
- // Cut in the middle: the tip end on the left, the stalk end on the right, each turned so its cut faces +Z.
710
- const pieces = (): [Model, Model] => {
711
- const mid = Math.floor(arc.length / 2);
712
- const left = body(arc.slice(0, mid + 1), 0, mid / (arc.length - 1));
713
- const right = merge([{ geometry: body(arc.slice(mid), mid / (arc.length - 1), 1) }, { geometry: stalk }]);
714
- return [
715
- createShape(merge([{ geometry: left, rotation: turn([0, 1, 0], -Math.PI / 2) }]), { center: false, shine: 0.3 }),
716
- createShape(merge([{ geometry: right, rotation: turn([0, 1, 0], Math.PI / 2) }]), { center: false, shine: 0.3 })
717
- ];
718
- };
719
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.3 }), juice: [1, 0.95, 0.72] }, pieces);
720
- }
721
-
722
- function cherries(): Fruit {
723
- // Two glossy cherries, dimpled where the stems go in, hanging from stems joined at the top.
724
- const outline = Path.fromSVG('M0 0.12 C 0.1 0.01 0.46 -0.04 0.5 0.42 C 0.53 0.76 0.3 0.98 0 0.96');
725
- const p = normalize(
726
- resample(
727
- outline.toContours()[0]!.map(([x, y]) => [x, -y]),
728
- 40
729
- )
730
- );
731
- const skin = ({ u, v }: { u: number; v: number }): Color => {
732
- let c = mix([0.55, 0.02, 0.07], [0.26, 0.0, 0.04], smoothstep(0.3, 0.75, noise(u, v, 5, 3)) * 0.6 + smoothstep(0.5, 1, v) * 0.35);
733
- c = mix(c, [0.78, 0.12, 0.12], smoothstep(0.2, 0.05, v) * 0.4); // lighter around the stem
734
- return mix(c, [0.2, 0.02, 0.03], smoothstep(0.92, 1, v) * 0.7); // the dark blossom point
735
- };
736
- const top = p[0]![1];
737
- const S = 0.42;
738
- const at: Vec3[] = [
739
- [-0.2, -0.2, 0.02],
740
- [0.19, -0.28, -0.04]
741
- ];
742
- const join: Vec3 = [0.06, 0.55, 0];
743
- const stemFrom = (c: Vec3, bow: number) =>
744
- tube([[c[0], c[1] + top * S - 0.02, c[2]], [c[0] + bow * 0.4, c[1] + 0.38, c[2] * 0.5], [join[0] + bow * 0.15, join[1] - 0.12, 0], join], {
745
- radius: (t) => 0.012 + 0.003 * t,
746
- sides: seg(7),
747
- color: (q) => mix([0.4, 0.55, 0.16], [0.42, 0.32, 0.14], smoothstep(0.75, 1, q.v)),
748
- capColor: [0.35, 0.25, 0.12]
749
- });
750
- const stems: GeometryPart[] = [
751
- { geometry: stemFrom(at[0]!, -0.1) },
752
- { geometry: stemFrom(at[1]!, 0.08) },
753
- { geometry: leaf(0.3, 0.07), position: join, rotation: mul(turn([0, 1, 0], 0.6), turn([0, 0, 1], 0.4)) }
754
- ];
755
- const body = lathe(p, { segments: seg(40), color: skin });
756
- const whole = merge([...at.map((position): GeometryPart => ({ geometry: body, position, scale: S })), ...stems]);
757
- // Cut through: dark red flesh around a pale stone.
758
- const flesh = (r: number, y: number): Color => {
759
- const stone = Math.hypot(r / 0.42, (y + 0.02) / 0.32);
760
- if (stone < 1) return mix([0.93, 0.84, 0.66], [0.8, 0.66, 0.5], stone * stone);
761
- return mix([0.62, 0.04, 0.1], [0.42, 0.01, 0.06], smoothstep(1, 2.2, stone));
762
- };
763
- const pieces = (): [Model, Model] => {
764
- const half = lathe(undimpled(p), {
765
- segments: seg(40),
766
- sweep: Math.PI,
767
- color: halfTurn(skin),
768
- cutColor: flesh,
769
- cutSteps: 20,
770
- capColor: [0.5, 0.04, 0.08]
771
- });
772
- return [
773
- createShape(merge([{ geometry: half, position: [0, at[0]![1], 0], scale: S }]), { center: false, shine: 0.7 }),
774
- createShape(
775
- merge([
776
- { geometry: half, position: [0, at[1]![1], 0], scale: S },
777
- { geometry: merge(stems), position: [-at[1]![0], 0, 0], rotation: yTurn(Math.PI / 2) }
778
- ]),
779
- {
780
- center: false,
781
- shine: 0.7
782
- }
783
- )
784
- ];
785
- };
786
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.85 }), juice: [0.6, 0.03, 0.1] }, pieces);
787
- }
788
-
789
- function grapes(): Fruit {
790
- // A bunch: dusky purple grapes with a waxy bloom, packed in a hanging cone below a woody stalk.
791
- const p = normalize(rounded(1.12, 0.5, 2, 16));
792
- const skin = ({ u, v }: { u: number; v: number }): Color => {
793
- let c = mix([0.3, 0.07, 0.27], [0.17, 0.03, 0.17], smoothstep(0.2, 0.9, noise(u, v, 4, 3)));
794
- c = mix(c, [0.52, 0.46, 0.62], 0.3 * smoothstep(0.35, 0.8, noise(u, v, 9, 6))); // the powdery bloom
795
- return mix(c, [0.36, 0.14, 0.25], smoothstep(0.6, 0.1, v) * 0.3); // a touch redder on top
796
- };
797
- const grape = lathe(p, { segments: seg(24), color: skin });
798
- const half = lathe(p, {
799
- segments: seg(24),
800
- sweep: Math.PI,
801
- color: halfTurn(skin),
802
- cutColor: (r) => (r > 0.93 ? [0.3, 0.08, 0.25] : mix([0.86, 0.9, 0.62], [0.58, 0.68, 0.32], r * r)),
803
- cutSteps: 6
804
- });
805
- const R = 0.085; // a grape's radius
806
- // Rings of grapes, wide at the shoulders and narrowing to a single grape at the tip.
807
- const spots: { at: Vec3; tint: Color; turn: number }[] = [];
808
- const LAYERS = 8;
809
- for (let i = 0; i < LAYERS; i++) {
810
- const t = i / (LAYERS - 1);
811
- const y = 0.32 - t * 0.78;
812
- const ring = 0.26 * Math.pow(1 - t, 0.85) * (1 - 0.35 * Math.pow(1 - t, 6));
813
- for (const [radius, offset] of [
814
- [ring, 0],
815
- [ring * 0.45, 0.5]
816
- ] as const) {
817
- const count = radius < 0.03 ? (offset ? 0 : 1) : Math.max(1, Math.floor((TAU * radius) / (R * 1.85)));
818
- for (let k = 0; k < count; k++) {
819
- const a = ((k + offset + i * 0.37) / count) * TAU;
820
- const j = hash(i, k + offset * 10);
821
- spots.push({
822
- at: [Math.cos(a) * radius, y + (j - 0.5) * 0.04, Math.sin(a) * radius],
823
- tint: shade([1, 0.95 + 0.1 * j, 1], 0.85 + 0.3 * hash(k, i + 3)),
824
- turn: j * 2
825
- });
826
- }
827
- }
828
- }
829
- const scale: Vec3 = [R * 2, R * 2, R * 2];
830
- const placed = (s: (typeof spots)[number]): GeometryPart => ({
831
- geometry: grape,
832
- position: s.at,
833
- scale,
834
- color: s.tint,
835
- rotation: mul(turn([0, 0, 1], (s.turn - 1) * 0.5), yTurn(s.turn * 3))
836
- });
837
- // The stalk, a few branches into the bunch, a vine leaf and a curly tendril.
838
- const WOOD = (q: { v: number; u: number }): Color => mix([0.42, 0.35, 0.16], [0.32, 0.22, 0.1], noise(q.u, q.v, 4, 6));
839
- const stalk = tube(
840
- [
841
- [0.02, 0.62, 0],
842
- [0, 0.48, 0],
843
- [0.01, 0.3, 0],
844
- [0, 0.1, 0]
845
- ],
846
- { radius: (t) => 0.022 - 0.008 * t, sides: seg(8), color: WOOD, capColor: [0.55, 0.48, 0.3] }
847
- );
848
- const branches: GeometryPart[] = [0, 1, 2, 3].map((k) => {
849
- const a = (k / 4) * TAU + 0.4;
850
- return {
851
- geometry: tube(
852
- [
853
- [0, 0.42 - k * 0.04, 0],
854
- [Math.cos(a) * 0.12, 0.36 - k * 0.05, Math.sin(a) * 0.12],
855
- [Math.cos(a) * 0.2, 0.3 - k * 0.06, Math.sin(a) * 0.2]
856
- ],
857
- { radius: 0.009, sides: seg(6), color: WOOD }
858
- )
859
- };
860
- });
861
- const vine: [number, number][] = [];
862
- for (let i = 0; i < 160; i++) {
863
- const a = (i / 160) * TAU;
864
- // Five pointed lobes (deepest toward the stalk, at the bottom) with saw teeth.
865
- const lobes = 0.6 + 0.4 * Math.pow(Math.abs(Math.cos(2.5 * (a - Math.PI / 2))), 0.7);
866
- const notch = 1 - 0.55 * Math.exp(-Math.pow((a - Math.PI / 2) / 0.22, 2)); // y down: the stalk end is +y
867
- const r = 0.22 * lobes * notch * (1 + (i % 4 === 0 ? 0.06 : 0));
868
- vine.push([Math.cos(a) * r, Math.sin(a) * r - 0.05]);
869
- }
870
- const vineLeaf = recolor(extrude(new Path().addPolygon(vine), { depth: 0.008 }), ({ position }) => {
871
- const a = Math.atan2(position[1] + 0.05, position[0]);
872
- const vein = Math.min(...[0, 1, 2, 3, 4].map((k) => Math.abs(Math.sin(a - (Math.PI / 2 + (k * TAU) / 5)))));
873
- return mix([0.22, 0.48, 0.14], [0.55, 0.72, 0.3], smoothstep(0.08, 0, vein * Math.hypot(position[0], position[1] + 0.05) * 6) * 0.8);
874
- });
875
- const helix: Vec3[] = [];
876
- for (let i = 0; i <= 24; i++) helix.push([-0.04 - i * 0.008, 0.56 + 0.05 * Math.sin(i * 0.8), 0.05 * Math.cos(i * 0.8)]);
877
- const top: GeometryPart[] = [
878
- { geometry: stalk },
879
- ...branches,
880
- { geometry: vineLeaf, position: [0.17, 0.56, -0.06], rotation: mul(turn([0, 1, 0], -0.4), turn([1, 0, 0], -0.9)) },
881
- { geometry: tube(helix, { radius: 0.005, sides: seg(5), color: [0.45, 0.6, 0.2] }) }
882
- ];
883
- const whole = merge([...spots.map(placed), ...top]);
884
- // Sliced down the middle: the grapes on the cut halved (pale green inside), the rest to each side.
885
- const side = (sign: 1 | -1): GeometryPart[] => {
886
- const parts: GeometryPart[] = [];
887
- for (const s of spots) {
888
- if (Math.abs(s.at[0]) < R * 0.7)
889
- parts.push({ geometry: half, position: [0, s.at[1], s.at[2]], scale, color: s.tint, rotation: yTurn((sign * Math.PI) / 2) });
890
- else if (Math.sign(s.at[0]) === -sign) parts.push(placed(s));
891
- }
892
- return sign === 1 ? [...parts, ...top] : parts;
893
- };
894
- const pieces = (): [Model, Model] => {
895
- return [
896
- createShape(merge([{ geometry: merge(side(1)), rotation: yTurn(-Math.PI / 2) }]), { center: false, shine: 0.4 }),
897
- createShape(merge([{ geometry: merge(side(-1)), rotation: yTurn(Math.PI / 2) }]), { center: false, shine: 0.4 })
898
- ];
899
- };
900
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.4 }), juice: [0.75, 0.85, 0.45] }, pieces);
901
- }
902
-
903
- function kiwi(): Fruit {
904
- // A blunt oval with fuzzy brown skin, laid on its side.
905
- const p = normalize(rounded(1, 0.4, 2.4, 72));
906
- const skin = ({ u, v }: { u: number; v: number }): Color => {
907
- let c = mix([0.5, 0.35, 0.18], [0.36, 0.24, 0.11], noise(u, v, 12, 8));
908
- c = shade(c, 0.85 + 0.3 * hash(Math.floor(u * 200), Math.floor(v * 90))); // the fuzz
909
- return mix(c, [0.3, 0.2, 0.1], (smoothstep(0.05, 0, v) + smoothstep(0.95, 1, v)) * 0.6);
910
- };
911
- const displace = (u: number, v: number) => -0.004 * noise(u, v, 60, 30);
912
- const SIDE = turn([0, 0, 1], Math.PI / 2);
913
- // Cut across: bright green flesh streaked toward a creamy core, ringed with tiny black seeds.
914
- const face = (r: number, angle: number): Color => {
915
- if (r > 0.975) return [0.42, 0.3, 0.14];
916
- if (r > 0.94) return mix([0.55, 0.72, 0.2], [0.4, 0.58, 0.12], (r - 0.94) / 0.035);
917
- const ray = noise(angle / TAU, r, 70, 3); // fine streaks running out from the core
918
- if (r < 0.26) return mix([0.97, 0.96, 0.78], [0.86, 0.9, 0.55], smoothstep(0.18, 0.26, r) * 0.6 + ray * 0.1);
919
- const n = 56;
920
- const cell = Math.floor((angle / TAU) * n);
921
- const jitter = hash(cell, 7);
922
- const off = (Math.abs(fract((angle / TAU) * n) - 0.5) / n) * TAU * r; // arc distance from the seed's middle
923
- const mid = 0.34 + 0.06 * jitter;
924
- if (Math.hypot(off / 0.016, (r - mid) / 0.045) < 1) return [0.06, 0.05, 0.03];
925
- const c = mix([0.62, 0.86, 0.22], [0.44, 0.7, 0.08], smoothstep(0.3, 0.9, r));
926
- return mix(c, [0.85, 0.95, 0.5], ray * 0.25 * smoothstep(0.95, 0.4, r));
927
- };
928
- const whole = merge([{ geometry: lathe(p, { segments: seg(96), color: skin, displace }), rotation: SIDE }]);
929
- const pieces = (): [Model, Model] => {
930
- const upper = p.slice(0, Math.floor(p.length / 2) + 1);
931
- const piece = createShape(
932
- merge([
933
- {
934
- geometry: lathe(upper, { segments: seg(168), color: upperHalf(skin), displace: upperDisplace(displace), capColor: face, capRings: 48 }),
935
- rotation: CUT_ACROSS
936
- }
937
- ]),
938
- {
939
- center: false,
940
- shine: 0.45
941
- }
942
- );
943
- return [piece, piece];
944
- };
945
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.05 }), juice: [0.6, 0.85, 0.25] }, pieces);
946
- }
947
-
948
- /** A mango's bean curve and flattened sides, applied after lathing (so cut faces bend too). */
949
- const mangoShape = ([x, y, z]: Vec3): Vec3 => [x - 0.22 * y * y + 0.05 * y, y, z * 0.8];
950
-
951
- function mango(): Fruit {
952
- // An egg, broad at the shoulders, curved like a bean and flattened, blushing from green to red.
953
- const p = normalize(profile(1, (t) => 0.36 * Math.sqrt(Math.max(0, 1 - Math.pow(2 * t - 1, 2))) * (1.08 - 0.2 * t), 64));
954
- const skin = ({ u, v }: { u: number; v: number }): Color => {
955
- const sun = Math.max(0, Math.cos(u * TAU)) * smoothstep(0.85, 0.15, v) + 0.4 * noise(u, v, 5, 3) - 0.15; // the side facing the sun
956
- let c = mix([0.42, 0.6, 0.16], [0.96, 0.72, 0.15], smoothstep(0.95, 0.3, v) * 0.8 + 0.25 * noise(u, v, 4, 3));
957
- c = mix(c, [0.98, 0.5, 0.12], smoothstep(0.1, 0.5, sun) * 0.8);
958
- c = mix(c, [0.78, 0.12, 0.12], smoothstep(0.35, 0.85, sun) * 0.85);
959
- if (hash(Math.floor(u * 110), Math.floor(v * 60)) > 0.965) c = mix(c, [0.98, 0.9, 0.55], 0.5); // lenticels
960
- return c;
961
- };
962
- const top = p[0]![1];
963
- const stemAt = mangoShape([0, top, 0]);
964
- const stub: GeometryPart = { geometry: stem(0.05, 0.018, 0.01), position: [stemAt[0], stemAt[1] - 0.01, 0] };
965
- const whole = merge([{ geometry: warp(lathe(p, { segments: seg(SKIN_SEGMENTS), color: skin }), mangoShape) }, stub]);
966
- // Cut through the flat sides: deep yellow flesh around a pale, fibrous, flat stone.
967
- const flesh = (r: number, y: number): Color => {
968
- const stone = Math.hypot(r / 0.24, y / 0.34);
969
- if (stone < 1) return mix([0.98, 0.86, 0.5], [0.9, 0.72, 0.32], noise(r, y + 1, 3, 40) * 0.5 + stone * 0.4);
970
- const c = mix([1, 0.62, 0.08], [1, 0.76, 0.22], smoothstep(1, 2.6, stone));
971
- return mix(c, [0.62, 0.7, 0.2], smoothstep(0.93, 1, r) * 0.6);
972
- };
973
- const pieces = (): [Model, Model] => {
974
- const half = merge([
975
- {
976
- geometry: warp(lathe(p, { segments: seg(SKIN_SEGMENTS), sweep: Math.PI, color: halfTurn(skin), cutColor: flesh, cutSteps: 32 }), mangoShape)
977
- },
978
- stub
979
- ]);
980
- const piece = createShape(half, { center: false, shine: 0.4 });
981
- return [piece, piece];
982
- };
983
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.5 }), juice: [1, 0.7, 0.15] }, pieces);
984
- }
985
-
986
- /** A peach's groove (suture), down the side facing +Z, pinched toward the core. */
987
- const peachShape = ([x, y, z]: Vec3): Vec3 => {
988
- const k = 1 - 0.09 * Math.exp(-Math.pow(x / 0.08, 2)) * smoothstep(0, 0.3, z) * smoothstep(0.48, 0.15, Math.abs(y + 0.03));
989
- return [x, y, z * k];
990
- };
991
-
992
- function peach(): Fruit {
993
- // Round, with a dimpled top, a little point at the bottom, a groove down one side; velvety skin.
994
- const outline = Path.fromSVG('M0 0.08 C 0.14 -0.02 0.48 -0.02 0.5 0.42 C 0.52 0.78 0.3 0.96 0.04 0.98 L 0 1.0');
995
- const p = normalize(
996
- resample(
997
- outline.toContours()[0]!.map(([x, y]) => [x, -y]),
998
- 64
999
- )
1000
- );
1001
- const skin = ({ u, v }: { u: number; v: number }): Color => {
1002
- const blush = smoothstep(0.3, 0.8, noise(u, v, 5, 4) * 0.6 + Math.max(0, Math.cos((u - 0.75) * TAU)) * 0.6);
1003
- let c = mix([1, 0.74, 0.3], [1, 0.62, 0.22], noise(u, v, 10, 6));
1004
- c = mix(c, [0.92, 0.3, 0.2], blush * 0.85);
1005
- c = mix(c, [0.7, 0.1, 0.12], blush * smoothstep(0.6, 0.9, noise(u, v, 48, 28)) * 0.45); // freckled red
1006
- return mix(shade(c, 0.97 + 0.05 * hash(Math.floor(u * 220), Math.floor(v * 110))), [1, 0.85, 0.7], 0.06); // the fuzz
1007
- };
1008
- const top = p[0]![1];
1009
- const crown: GeometryPart[] = [
1010
- { geometry: stem(0.05, 0.02, 0.01), position: [0, top - 0.015, 0] },
1011
- {
1012
- geometry: leaf(0.4, 0.07, { teeth: 18, fold: 0.3 }),
1013
- position: [0.01, top + 0.02, 0],
1014
- rotation: mul(turn([0, 1, 0], 2.4), turn([0, 0, 1], 0.35))
1015
- }
1016
- ];
1017
- const whole = merge([{ geometry: warp(lathe(p, { segments: seg(SKIN_SEGMENTS), color: skin }), peachShape) }, ...crown]);
1018
- // Cut down the groove: golden flesh blushing red around a wrinkled brown stone.
1019
- const flesh = (r: number, y: number): Color => {
1020
- const stone = Math.hypot(r / 0.4, (y + 0.02) / 0.34);
1021
- if (stone < 1) return mix([0.55, 0.28, 0.14], [0.36, 0.17, 0.08], smoothstep(0.3, 0.7, noise(r * 3, y + 1, 7, 9)));
1022
- let c = mix([1, 0.74, 0.3], [1, 0.82, 0.42], smoothstep(1.2, 2.6, stone));
1023
- c = mix(c, [0.88, 0.3, 0.16], smoothstep(1.35, 1, stone) * 0.85); // red around the stone
1024
- return mix(c, [0.85, 0.3, 0.18], smoothstep(0.94, 1, r) * 0.7);
1025
- };
1026
- const pieces = (): [Model, Model] => {
1027
- const half = merge([
1028
- {
1029
- geometry: warp(
1030
- lathe(undimpled(p), {
1031
- segments: seg(SKIN_SEGMENTS),
1032
- sweep: Math.PI,
1033
- color: halfTurn(skin),
1034
- cutColor: flesh,
1035
- cutSteps: 32,
1036
- capColor: [0.95, 0.5, 0.2]
1037
- }),
1038
- peachShape
1039
- )
1040
- },
1041
- ...crown
1042
- ]);
1043
- const piece = createShape(half, { center: false, shine: 0.25 });
1044
- return [piece, piece];
1045
- };
1046
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.12 }), juice: [1, 0.75, 0.35] }, pieces);
1047
- }
1048
-
1049
- function pineapple(): Fruit {
1050
- // A barrel covered in diamond scales ("eyes") on two spirals, under a crown of spiky leaves.
1051
- const p = normalize(resample(rounded(1, 0.34, 2.6, 96), 110));
1052
- const AROUND = 13;
1053
- const DOWN = 7;
1054
- // Each scale's coordinates: which cell, and where in it (0..1 both ways).
1055
- const scale = (u: number, v: number) => {
1056
- const a = u * AROUND + v * DOWN;
1057
- const b = u * AROUND - v * DOWN;
1058
- return { fa: fract(a), fb: fract(b), cell: Math.floor(a) * 31 + Math.floor(b) };
1059
- };
1060
- const skin = ({ u, v }: { u: number; v: number }): Color => {
1061
- const { fa, fb, cell } = scale(u, v);
1062
- const edge = Math.max(Math.abs(fa - 0.5), Math.abs(fb - 0.5)) * 2; // 0 middle .. 1 groove
1063
- const ripe = smoothstep(0.95, 0.3, v) * 0.8 + 0.3 * hash(cell, 3);
1064
- let c = mix([0.42, 0.42, 0.12], [0.86, 0.58, 0.14], ripe);
1065
- c = mix(c, [0.64, 0.42, 0.14], smoothstep(0.45, 0.15, edge) * 0.8); // the eye's brown middle
1066
- // The spike at the top of each eye.
1067
- if (Math.hypot(fa - 0.62, fb - 0.62) < 0.1) c = [0.3, 0.2, 0.08];
1068
- return mix(c, [0.2, 0.18, 0.06], smoothstep(0.75, 1, edge) * 0.85); // dark grooves
1069
- };
1070
- const displace = (u: number, v: number) => {
1071
- const { fa, fb } = scale(u, v);
1072
- const edge = Math.max(Math.abs(fa - 0.5), Math.abs(fb - 0.5)) * 2;
1073
- return 0.018 * (Math.sqrt(Math.max(0, 1 - edge * edge)) - 1) * smoothstep(0, 0.06, v) * smoothstep(1, 0.94, v);
1074
- };
1075
- const top = p[0]![1];
1076
- // Crown leaves: flat, tapering, curling outward, in tiers (the inner ones tallest and most upright).
1077
- const flat = (around: number) => 1 / Math.sqrt(Math.cos(around * TAU) ** 2 + (Math.sin(around * TAU) / 0.16) ** 2);
1078
- const leaves: GeometryPart[] = [];
1079
- const tiers = [
1080
- { n: 7, tilt: 0.45, length: 0.62 },
1081
- { n: 9, tilt: 0.8, length: 0.55 },
1082
- { n: 11, tilt: 1.15, length: 0.42 },
1083
- { n: 9, tilt: 1.45, length: 0.3 }
1084
- ];
1085
- tiers.forEach(({ n, tilt, length }, tier) => {
1086
- for (let k = 0; k < n; k++) {
1087
- const around = ((k + tier * 0.5) / n) * TAU;
1088
- const L = length * (0.85 + 0.3 * hash(k, tier));
1089
- const pts: Vec3[] = [];
1090
- for (let s = 0; s <= 5; s++) {
1091
- const t = s / 5;
1092
- const lean = tilt + t * 0.5; // curling outward toward the tip
1093
- pts.push([Math.sin(lean) * L * t, Math.cos(lean) * L * t, 0]);
1094
- }
1095
- leaves.push({
1096
- geometry: tube(pts, {
1097
- radius: (t) => 0.045 * Math.pow(1 - t, 0.9) + 0.002,
1098
- shape: flat,
1099
- sides: seg(10),
1100
- smoothness: 4,
1101
- color: (q) => mix(mix([0.24, 0.42, 0.22], [0.36, 0.55, 0.3], q.v), [0.6, 0.55, 0.3], smoothstep(0.8, 1, q.v) * 0.7)
1102
- }),
1103
- position: [0, top + 0.01, 0],
1104
- rotation: yTurn(around)
1105
- });
1106
- }
1107
- });
1108
- const crown: GeometryPart = { geometry: merge(leaves) };
1109
- const whole = merge([{ geometry: lathe(p, { segments: seg(130), color: skin, displace }) }, crown]);
1110
- // Cut across: a ring of rind and eyes, juicy golden flesh with radial fibres, a paler woody core.
1111
- const face = (r: number, angle: number): Color => {
1112
- if (r > 0.95) return [0.55, 0.42, 0.14];
1113
- const fibre = noise(angle / TAU, r, 90, 4);
1114
- if (r > 0.86) return Math.abs(fract((angle / TAU) * AROUND * 2) - 0.5) < 0.12 && r > 0.89 ? [0.55, 0.35, 0.12] : [0.96, 0.84, 0.45];
1115
- if (r < 0.2) return mix([1, 0.92, 0.62], [0.98, 0.86, 0.5], fibre * 0.4);
1116
- return shade(mix([1, 0.78, 0.18], [1, 0.84, 0.3], smoothstep(0.8, 0.3, r)), 0.9 + 0.15 * fibre);
1117
- };
1118
- const pieces = (): [Model, Model] => {
1119
- const mid = Math.floor(p.length / 2);
1120
- const upper = p.slice(0, mid + 1);
1121
- const lower = p.slice(mid);
1122
- const upperSkin = (q: { u: number; v: number }) => skin({ u: q.u, v: q.v * 0.5 });
1123
- const lowerSkin = (q: { u: number; v: number }) => skin({ u: q.u, v: 0.5 + q.v * 0.5 });
1124
- const opts = { segments: seg(130), capColor: face, capRings: 30 };
1125
- return [
1126
- createShape(
1127
- merge([
1128
- {
1129
- geometry: merge([{ geometry: lathe(upper, { ...opts, color: upperSkin, displace: (u, v) => displace(u, v * 0.5) }) }, crown]),
1130
- rotation: CUT_ACROSS
1131
- }
1132
- ]),
1133
- { center: false, shine: 0.3 }
1134
- ),
1135
- createShape(
1136
- merge([
1137
- {
1138
- geometry: lathe(lower, { ...opts, color: lowerSkin, displace: (u, v) => displace(u, 0.5 + v * 0.5) }),
1139
- rotation: turn([1, 0, 0], Math.PI / 2)
1140
- }
1141
- ]),
1142
- { center: false, shine: 0.3 }
1143
- )
1144
- ];
1145
- };
1146
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.3 }), juice: [1, 0.85, 0.3] }, pieces);
1147
- }
1148
-
1149
- function pomegranate(): Fruit {
1150
- // A leathery red ball, slightly flattened and faceted, under a crown of pointed sepals.
1151
- const p = normalize(rounded(0.9, 0.5, 2, 64));
1152
- const skin = ({ u, v }: { u: number; v: number }): Color => {
1153
- let c = mix([0.72, 0.1, 0.1], [0.5, 0.04, 0.07], smoothstep(0.3, 0.8, noise(u, v, 6, 4)));
1154
- c = mix(c, [0.88, 0.5, 0.32], smoothstep(0.6, 0.85, noise(u + 0.3, v, 5, 3)) * 0.55); // pale, sun-faded patches
1155
- c = mix(c, [0.45, 0.06, 0.06], smoothstep(0.7, 0.95, noise(u, v, 60, 30)) * 0.4); // fine marks
1156
- return mix(c, [0.62, 0.3, 0.2], smoothstep(0.08, 0, v) * 0.6);
1157
- };
1158
- const displace = (u: number, v: number) => -0.008 * Math.abs(Math.sin(u * TAU * 3)) * Math.sin(Math.PI * v); // six soft facets
1159
- const top = p[0]![1];
1160
- const SEPAL = (q: { v: number }): Color => mix([0.6, 0.14, 0.12], [0.42, 0.22, 0.12], smoothstep(0.6, 1, q.v));
1161
- const crown: GeometryPart[] = [
1162
- {
1163
- geometry: tube(
1164
- [
1165
- [0, top - 0.04, 0],
1166
- [0, top + 0.06, 0]
1167
- ],
1168
- { radius: (t) => 0.07 + 0.015 * t, sides: seg(18), color: SEPAL, capColor: [0.35, 0.12, 0.08] }
1169
- )
1170
- }
1171
- ];
1172
- for (let k = 0; k < 6; k++) {
1173
- const a = (k / 6) * TAU;
1174
- crown.push({
1175
- geometry: tube(
1176
- [
1177
- [0.07, 0, 0],
1178
- [0.09, 0.05, 0],
1179
- [0.12, 0.1, 0]
1180
- ],
1181
- { radius: (t) => 0.025 * (1 - t) + 0.002, sides: seg(6), color: SEPAL }
1182
- ),
1183
- position: [0, top + 0.04, 0],
1184
- rotation: yTurn(a)
1185
- });
1186
- }
1187
- const whole = merge([{ geometry: lathe(p, { segments: seg(SKIN_SEGMENTS), color: skin, displace }) }, ...crown]);
1188
- // Cut across: pale membranes between chambers packed with glossy, ruby seeds (arils).
1189
- const face = (r: number): Color => (r > 0.95 ? [0.62, 0.08, 0.09] : r > 0.88 ? [0.98, 0.82, 0.68] : [0.96, 0.9, 0.72]);
1190
- const pieces = (): [Model, Model] => {
1191
- const upper = p.slice(0, Math.floor(p.length / 2) + 1);
1192
- const r0 = upper[upper.length - 1]![0];
1193
- const aril = lathe(rounded(1.2, 0.5, 2, 8), { segments: seg(8), color: [1, 1, 1] });
1194
- const arils: GeometryPart[] = [];
1195
- const GAP = 0.052;
1196
- for (let gy = -1; gy <= 1; gy += GAP / r0) {
1197
- for (let gx = -1; gx <= 1; gx += GAP / r0) {
1198
- const jx = gx + (hash(gx * 13, gy * 7) - 0.5) * 0.04;
1199
- const jy = gy + (hash(gy * 11, gx * 5) - 0.5) * 0.04 + ((Math.round(gx / (GAP / r0)) % 2) * GAP) / r0 / 2;
1200
- const r = Math.hypot(jx, jy);
1201
- const a = Math.atan2(jy, jx);
1202
- const membrane = Math.abs(Math.sin(a * 3 + 0.3)) * r; // six chambers
1203
- if (r > 0.84 || r < 0.1 || membrane < 0.05) continue;
1204
- const h = hash(jx * 31, jy * 17);
1205
- arils.push({
1206
- geometry: aril,
1207
- position: [jx * r0, -0.006, jy * r0],
1208
- rotation: mul(yTurn(h * TAU), turn([1, 0, 0], Math.PI / 2 + (h - 0.5) * 0.6)),
1209
- scale: [0.044, 0.05, 0.03],
1210
- color: mix([0.85, 0.06, 0.14], [0.62, 0.02, 0.1], h)
1211
- });
1212
- }
1213
- }
1214
- const half = merge([
1215
- {
1216
- geometry: merge([
1217
- {
1218
- geometry: lathe(upper, {
1219
- segments: seg(SKIN_SEGMENTS),
1220
- color: upperHalf(skin),
1221
- displace: upperDisplace(displace),
1222
- capColor: face,
1223
- capRings: 12
1224
- })
1225
- },
1226
- ...crown,
1227
- ...arils
1228
- ]),
1229
- rotation: CUT_ACROSS
1230
- }
1231
- ]);
1232
- const piece = createShape(half, { center: false, shine: 0.55 });
1233
- return [piece, piece];
1234
- };
1235
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.45 }), juice: [0.8, 0.05, 0.15] }, pieces);
1236
- }
1237
-
1238
- function papaya(): Fruit {
1239
- // Long, narrow at the stem end, green ripening to yellow-orange; laid on its side.
1240
- const p = normalize(profile(1, (t) => 0.25 * Math.pow(Math.max(0, 1 - Math.pow(Math.abs(2 * t - 1), 2.2)), 1 / 2.2) * (0.7 + 0.42 * t), 72));
1241
- const ribs = (u: number, v: number) => -0.006 * Math.pow(Math.abs(Math.cos(u * TAU * 2.5)), 3) * Math.sin(Math.PI * v); // five faint lobes
1242
- const skin = ({ u, v }: { u: number; v: number }): Color => {
1243
- const ripe = smoothstep(0.35, 0.8, v * 0.7 + noise(u, v, 5, 6) * 0.55);
1244
- let c = mix([0.3, 0.5, 0.14], [0.98, 0.7, 0.16], ripe);
1245
- c = mix(c, [0.95, 0.5, 0.12], ripe * smoothstep(0.55, 0.85, noise(u, v, 8, 10)) * 0.5);
1246
- if (hash(Math.floor(u * 140), Math.floor(v * 90)) > 0.97) c = mix(c, [0.25, 0.35, 0.1], 0.5); // speckles
1247
- return mix(c, [0.4, 0.5, 0.18], smoothstep(0.06, 0, v) * 0.6);
1248
- };
1249
- const SIDE = turn([0, 0, 1], -Math.PI / 2); // the stem end to +X
1250
- const top = p[0]![1];
1251
- const whole = merge([
1252
- {
1253
- geometry: merge([
1254
- { geometry: lathe(p, { segments: seg(SKIN_SEGMENTS), color: skin, displace: ribs }) },
1255
- { geometry: stem(0.04, 0.02, 0), position: [0, top - 0.01, 0], color: [1.3, 1.6, 0.9] }
1256
- ]),
1257
- rotation: SIDE
1258
- }
1259
- ]);
1260
- // Cut lengthwise: salmon-orange flesh around a glistening hollow full of black seeds.
1261
- const cavity = (y: number) => Math.max(0, 0.5 * Math.sin(Math.PI * smoothstep(0.25, -0.42, y)));
1262
- const flesh = (r: number, y: number): Color => {
1263
- const hollow = cavity(y);
1264
- if (r > 0.96) return [0.5, 0.6, 0.15];
1265
- if (r < hollow) return mix([0.5, 0.12, 0.07], [0.95, 0.42, 0.22], smoothstep(0.75, 1, r / hollow)); // the glistening hollow
1266
- return mix([1, 0.5, 0.26], [1, 0.62, 0.34], smoothstep(hollow, 0.9, r));
1267
- };
1268
- const pieces = (): [Model, Model] => {
1269
- const seeds: GeometryPart[] = [];
1270
- for (let i = 0; i < 90; i++) {
1271
- const y = 0.2 - 0.58 * hash(i, 1);
1272
- const reach = cavity(y) * radiusAt(p, y) * 0.85;
1273
- if (reach < 0.025) continue;
1274
- const x = (hash(i, 2) * 2 - 1) * reach;
1275
- seeds.push({
1276
- geometry: lathe(rounded(1, 0.5, 2, 8), { segments: seg(10) }),
1277
- position: [x, y, 0.004 + 0.01 * hash(i, 3)],
1278
- scale: 0.042,
1279
- color: [0.06, 0.05, 0.04]
1280
- });
1281
- }
1282
- const half = merge([
1283
- {
1284
- geometry: merge([
1285
- {
1286
- geometry: lathe(p, {
1287
- segments: seg(SKIN_SEGMENTS),
1288
- sweep: Math.PI,
1289
- color: halfTurn(skin),
1290
- displace: (u, v) => ribs(u * 0.5, v),
1291
- cutColor: flesh,
1292
- cutSteps: 24
1293
- })
1294
- },
1295
- ...seeds
1296
- ]),
1297
- rotation: SIDE
1298
- }
1299
- ]);
1300
- const piece = createShape(half, { center: false, shine: 0.45 });
1301
- return [piece, piece];
1302
- };
1303
- return withPieces({ center: boxCenter(whole), whole: createShape(whole, { center: false, shine: 0.4 }), juice: [1, 0.55, 0.3] }, pieces);
1304
- }
1305
-
1306
- const sliced = new WeakSet<Fruit>();
1307
- /** A fruit whose sliced pieces are only built if a game asks for them (Tile Home never does). */
1308
- function withPieces(fruit: Omit<Fruit, 'pieces'>, build: (() => [Model, Model]) | undefined): Fruit {
1309
- let pieces: [Model, Model] | undefined;
1310
- const level = detail;
1311
- const make = () => {
1312
- detail = level;
1313
- try {
1314
- return build!();
1315
- } finally {
1316
- detail = 1;
1317
- }
1318
- };
1319
- return Object.defineProperty(fruit, 'pieces', {
1320
- get: () => {
1321
- if (!pieces) {
1322
- pieces = make();
1323
- build = undefined; // release the builder's temporary geometry once both models are cached
1324
- sliced.add(fruit as Fruit);
1325
- }
1326
- return pieces;
1327
- },
1328
- enumerable: true
1329
- }) as Fruit;
1330
- }
1331
-
1332
- const BUILDERS: Record<FruitKind, () => Fruit> = {
1333
- apple,
1334
- orange,
1335
- lemon,
1336
- pear,
1337
- strawberry,
1338
- watermelon,
1339
- banana,
1340
- cherries,
1341
- grapes,
1342
- kiwi,
1343
- mango,
1344
- peach,
1345
- pineapple,
1346
- pomegranate,
1347
- papaya
1348
- };
1349
- const built = new Map<string, Fruit>();
1350
-
1351
- /**
1352
- * One fruit, built on first use (mesh ids last for the whole app). Needs the native core.
1353
- * `level` < 1 builds a lighter mesh for fruit shown small (0.35 still looks round at 60 points).
1354
- */
1355
- export function getFruit(kind: FruitKind, level = 1): Fruit {
1356
- const key = `${kind}@${level}`;
1357
- let fruit = built.get(key);
1358
- if (!fruit) {
1359
- let generated: Fruit | undefined;
1360
- const generate = () => {
1361
- detail = level;
1362
- try {
1363
- return BUILDERS[kind]();
1364
- } finally {
1365
- detail = 1;
1366
- }
1367
- };
1368
- const whole = cacheGameAsset(`fruit:${key}:whole`, () => {
1369
- generated = generate();
1370
- return { whole: generated.whole, center: generated.center, juice: generated.juice };
1371
- });
1372
- let pieces: [Model, Model] | undefined;
1373
- fruit = Object.defineProperty({ ...whole }, 'pieces', {
1374
- enumerable: true,
1375
- get: () => {
1376
- if (!pieces) {
1377
- pieces = cacheGameAsset(`fruit:${key}:pieces`, () => (generated ??= generate()).pieces);
1378
- generated = undefined;
1379
- sliced.add(fruit!);
1380
- }
1381
- return pieces;
1382
- }
1383
- }) as Fruit;
1384
- built.set(key, fruit);
1385
- }
1386
- return fruit;
1387
- }
1388
-
1389
- /** Whether `kind` is cached at `level`, including its sliced models when `withSlices` is true. */
1390
- export const isFruitBuilt = (kind: FruitKind, level = 1, withSlices = false) => {
1391
- const fruit = built.get(`${kind}@${level}`);
1392
- return fruit !== undefined && (!withSlices || sliced.has(fruit));
1393
- };
1394
-
1395
- /**
1396
- * Builds `kinds` one per frame, so the screen keeps animating while they're made, then calls `done`.
1397
- * Already built fruit cost nothing. Returns a cancel function.
1398
- */
1399
- export function loadFruits(kinds: readonly FruitKind[], done: () => void, level = 1): () => void {
1400
- const queue = kinds.filter((k) => !built.has(`${k}@${level}`));
1401
- let frame = 0;
1402
- let cancelled = false;
1403
- const step = () => {
1404
- if (cancelled) return;
1405
- const kind = queue.shift();
1406
- if (kind) getFruit(kind, level);
1407
- if (queue.length) frame = requestAnimationFrame(step);
1408
- else done();
1409
- };
1410
- frame = requestAnimationFrame(step);
1411
- return () => {
1412
- cancelled = true;
1413
- cancelAnimationFrame(frame);
1414
- };
1415
- }
1416
-
1417
- let fruits: Record<FruitKind, Fruit> | undefined;
1418
- /** Every fruit, built once (mesh ids last for the whole app). Needs the native core. */
1419
- export function getFruits(): Record<FruitKind, Fruit> {
1420
- if (!fruits) {
1421
- // Each fruit is built when a game first reads it.
1422
- const all = {} as Record<FruitKind, Fruit>;
1423
- for (const kind of Object.keys(BUILDERS) as FruitKind[]) Object.defineProperty(all, kind, { get: () => getFruit(kind), enumerable: true });
1424
- fruits = all;
1425
- }
1426
- return fruits;
1427
- }
1428
-
1429
- /** Whole bodies and cut pieces are separate jobs; board games never build unused slices. */
1430
- export function* warmAssets(kinds: readonly FruitKind[], level: number, pieces = false): Iterable<() => unknown> {
1431
- for (const kind of kinds) {
1432
- yield () => getFruit(kind, level).whole;
1433
- if (pieces) yield () => getFruit(kind, level).pieces;
1434
- }
1435
- }