@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.
- package/lib/module/games/arrows/Arrows.js +1 -1
- package/lib/module/games/arrows/ArrowsScene.js +1 -1
- package/lib/module/games/balloon-blaster/BalloonBlaster.js +1 -1
- package/lib/module/games/balloon-blaster/BalloonBlasterScene.js +1 -1
- package/lib/module/games/bike-racing/BikeMeshes.js +1 -1
- package/lib/module/games/bike-racing/BikeRacing.js +1 -1
- package/lib/module/games/bike-racing/BikeRacingConstants.js +1 -1
- package/lib/module/games/bike-racing/BikeRacingScene.js +1 -1
- package/lib/module/games/block-blast/BlockBlast.js +1 -1
- package/lib/module/games/block-blast/BlockBlastScene.js +1 -1
- package/lib/module/games/block-breaker/BlockBreaker.js +1 -1
- package/lib/module/games/block-breaker/BlockBreakerScene.js +1 -1
- package/lib/module/games/bluk/Bluk.js +1 -1
- package/lib/module/games/bluk/BlukScene.js +1 -1
- package/lib/module/games/bubble-shooter/BubbleShooter.js +1 -1
- package/lib/module/games/bubble-shooter/BubbleShooterScene.js +1 -1
- package/lib/module/games/candy-crush/CandyCrush.js +1 -1
- package/lib/module/games/candy-crush/CandyCrushScene.js +1 -1
- package/lib/module/games/car-racing/CarMeshes.js +1 -1
- package/lib/module/games/car-racing/CarRacing.js +1 -1
- package/lib/module/games/car-racing/CarRacingScene.js +1 -1
- package/lib/module/games/chameleon-run/ChameleonRun.js +1 -1
- package/lib/module/games/chameleon-run/ChameleonRunBackdrop.js +1 -1
- package/lib/module/games/chameleon-run/ChameleonRunMeshes.js +1 -1
- package/lib/module/games/checkers/Checkers.js +1 -1
- package/lib/module/games/checkers/CheckersScene.js +1 -1
- package/lib/module/games/color-switch/ColorSwitch.js +1 -1
- package/lib/module/games/color-switch/ColorSwitchScene.js +1 -1
- package/lib/module/games/colors-sort/ColorsSort.js +1 -1
- package/lib/module/games/colors-sort/ColorsSortScene.js +1 -1
- package/lib/module/games/connect-em-all/ConnectEmAll.js +1 -1
- package/lib/module/games/connect-em-all/ConnectEmAllScene.js +1 -1
- package/lib/module/games/crossy-road/CrossyRoad.js +1 -1
- package/lib/module/games/crossy-road/CrossyRoadMeshes.js +1 -1
- package/lib/module/games/crossy-road/CrossyRoadPerson.js +1 -1
- package/lib/module/games/dino-jump/DinoJump.js +1 -1
- package/lib/module/games/dino-jump/DinoJumpScene.js +1 -1
- package/lib/module/games/dots-and-boxes/DotsAndBoxes.js +1 -1
- package/lib/module/games/dots-and-boxes/DotsAndBoxesScene.js +1 -1
- package/lib/module/games/falling-sticks/FallingSticks.js +1 -1
- package/lib/module/games/falling-sticks/FallingSticksScene.js +1 -1
- package/lib/module/games/find-different-number/FindDifferentNumber.js +1 -1
- package/lib/module/games/find-different-number/FindDifferentNumberScene.js +1 -1
- package/lib/module/games/flappy-bird/FlappyBird.js +1 -1
- package/lib/module/games/flappy-bird/FlappyBirdScene.js +1 -1
- package/lib/module/games/fruit-merger/FruitMerger.js +1 -1
- package/lib/module/games/fruit-merger/FruitMergerScene.js +1 -1
- package/lib/module/games/fruit-ninja/FruitNinja.js +1 -1
- package/lib/module/games/fruit-ninja/FruitNinjaScene.js +1 -1
- package/lib/module/games/fruit-ninja/FruitNinjaWorld.js +1 -1
- package/lib/module/games/game-2048/Game2048.js +1 -1
- package/lib/module/games/game-2048/Game2048Scene.js +1 -1
- package/lib/module/games/knife-hit/KnifeHit.js +1 -1
- package/lib/module/games/knife-hit/KnifeHitScene.js +1 -1
- package/lib/module/games/ludo-king/LudoKing.js +1 -1
- package/lib/module/games/ludo-king/LudoKingScene.js +1 -1
- package/lib/module/games/maze-runner/MazeRunner.js +1 -1
- package/lib/module/games/maze-runner/MazeRunnerScene.js +1 -1
- package/lib/module/games/memory-match/MemoryMatch.js +1 -1
- package/lib/module/games/memory-match/MemoryMatchScene.js +1 -1
- package/lib/module/games/mine-sweeper/MineSweeper.js +1 -1
- package/lib/module/games/mine-sweeper/MineSweeperScene.js +1 -1
- package/lib/module/games/mob-control/MobControl.js +1 -1
- package/lib/module/games/mob-control/MobControlGame.js +1 -1
- package/lib/module/games/mob-control/MobControlMeshes.js +1 -1
- package/lib/module/games/mob-control/MobControlRenderer.js +1 -1
- package/lib/module/games/mob-control/MobControlRules.js +1 -1
- package/lib/module/games/mob-control/MobControlScene.js +1 -1
- package/lib/module/games/nuts-and-bolts/NutsAndBolts.js +1 -1
- package/lib/module/games/nuts-and-bolts/NutsAndBoltsScene.js +1 -1
- package/lib/module/games/pac-man/PacMan.js +1 -1
- package/lib/module/games/pac-man/PacManGame.js +1 -1
- package/lib/module/games/pac-man/PacManScene.js +1 -1
- package/lib/module/games/paper-io/PaperIo.js +1 -1
- package/lib/module/games/paper-io/PaperIoMeshes.js +1 -1
- package/lib/module/games/paper-io/PaperIoScene.js +1 -1
- package/lib/module/games/penguin-run/PenguinMeshes.js +1 -1
- package/lib/module/games/penguin-run/PenguinRun.js +1 -1
- package/lib/module/games/penguin-run/PenguinRunScene.js +1 -1
- package/lib/module/games/perfect-circle/PerfectCircle.js +1 -1
- package/lib/module/games/perfect-circle/PerfectCircleScene.js +1 -1
- package/lib/module/games/pipe-connect/PipeConnect.js +1 -1
- package/lib/module/games/pipe-connect/PipeConnectScene.js +1 -1
- package/lib/module/games/play-with-tua/PlayWithTua.js +1 -1
- package/lib/module/games/popit-fidget/PopitFidget.js +1 -1
- package/lib/module/games/popit-fidget/PopitFidgetScene.js +1 -1
- package/lib/module/games/reversi/Reversi.js +1 -1
- package/lib/module/games/reversi/ReversiScene.js +1 -1
- package/lib/module/games/search-grid/SearchGrid.js +1 -1
- package/lib/module/games/search-grid/SearchGridScene.js +1 -1
- package/lib/module/games/shared/BackdropCard.js +1 -1
- package/lib/module/games/shared/EngineBackdrop.js +1 -1
- package/lib/module/games/shared/EngineBlasts.js +1 -1
- package/lib/module/games/shared/EngineBlocks.js +1 -1
- package/lib/module/games/shared/EngineDice.js +1 -1
- package/lib/module/games/shared/EngineFruits.js +1 -1
- package/lib/module/games/shared/EngineGameLoader.js +1 -1
- package/lib/module/games/shared/EngineGameScreen.js +1 -1
- package/lib/module/games/shared/EngineGlyphs.js +1 -1
- package/lib/module/games/shared/EngineHud.js +1 -1
- package/lib/module/games/shared/EngineMeadow.js +1 -1
- package/lib/module/games/shared/EngineTouchSurface.js +1 -1
- package/lib/module/games/shared/GameActivity.js +1 -1
- package/lib/module/games/shared/GameLoading.js +1 -1
- package/lib/module/games/shared/GameLoadingTransition.js +1 -1
- package/lib/module/games/sliding-numbers/SlidingNumbers.js +1 -1
- package/lib/module/games/sliding-numbers/SlidingNumbersScene.js +1 -1
- package/lib/module/games/snake-3d/Snake3D.js +1 -1
- package/lib/module/games/snake-3d/Snake3DScene.js +1 -1
- package/lib/module/games/snakes-and-ladders/SnakesAndLadders.js +1 -1
- package/lib/module/games/snakes-and-ladders/SnakesAndLaddersScene.js +1 -1
- package/lib/module/games/space-fighter/SpaceFighter.js +1 -1
- package/lib/module/games/space-fighter/SpaceFighterScene.js +1 -1
- package/lib/module/games/spider-solitaire/SpiderSolitaire.js +1 -1
- package/lib/module/games/spider-solitaire/SpiderSolitaireScene.js +1 -1
- package/lib/module/games/stack-tower/StackTower.js +1 -1
- package/lib/module/games/stack-tower/StackTowerScene.js +1 -1
- package/lib/module/games/sudoku/Sudoku.js +1 -1
- package/lib/module/games/sudoku/SudokuScene.js +1 -1
- package/lib/module/games/tangram/Tangram.js +1 -1
- package/lib/module/games/tangram/TangramScene.js +1 -1
- package/lib/module/games/tank-1990/Tank1990.js +1 -1
- package/lib/module/games/tank-1990/Tank1990Scene.js +1 -1
- package/lib/module/games/tic-tac-toe/TicTacToe.js +1 -1
- package/lib/module/games/tic-tac-toe/TicTacToeScene.js +1 -1
- package/lib/module/games/tile-home/TileHome.js +1 -1
- package/lib/module/games/tile-home/TileHomeScene.js +1 -1
- package/lib/module/games/treasure-hunt/TreasureHunt.js +1 -1
- package/lib/module/games/treasure-hunt/TreasureHuntConstants.js +1 -1
- package/lib/module/games/treasure-hunt/TreasureHuntModel.js +1 -1
- package/lib/module/games/treasure-hunt/TreasureHuntScene.js +1 -1
- package/lib/module/games/whack-a-mole/WhackAMole.js +1 -1
- package/lib/module/games/whack-a-mole/WhackAMoleScene.js +1 -1
- package/lib/module/helpers/GameSettingsModal.js +1 -1
- package/lib/module/helpers/GameTheme.js +1 -1
- package/package.json +1 -2
- package/src/assets/fonts/Inter-SemiBold.ttf +0 -0
- package/src/assets/models/explorer/explorer.glb +0 -0
- package/src/assets/models/explorer/params.json +0 -51
- package/src/assets/models/explorer/source.json +0 -222
- package/src/assets/models/play-with-tua/play-with-tua.glb +0 -0
- package/src/assets/models/play-with-tua/sources.json +0 -141
- package/src/assets/sfx/balloon_pop.wav +0 -0
- package/src/assets/sfx/blocked.wav +0 -0
- package/src/assets/sfx/boing.wav +0 -0
- package/src/assets/sfx/bonus.wav +0 -0
- package/src/assets/sfx/boom.wav +0 -0
- package/src/assets/sfx/bump.wav +0 -0
- package/src/assets/sfx/cannon.wav +0 -0
- package/src/assets/sfx/capture.wav +0 -0
- package/src/assets/sfx/card_flip.wav +0 -0
- package/src/assets/sfx/card_place.wav +0 -0
- package/src/assets/sfx/catch.wav +0 -0
- package/src/assets/sfx/chime.wav +0 -0
- package/src/assets/sfx/chomp.wav +0 -0
- package/src/assets/sfx/chomp_alt.wav +0 -0
- package/src/assets/sfx/clack.wav +0 -0
- package/src/assets/sfx/clang.wav +0 -0
- package/src/assets/sfx/clash.wav +0 -0
- package/src/assets/sfx/click.wav +0 -0
- package/src/assets/sfx/coin.wav +0 -0
- package/src/assets/sfx/combo.wav +0 -0
- package/src/assets/sfx/crash.wav +0 -0
- package/src/assets/sfx/crunch.wav +0 -0
- package/src/assets/sfx/dance.wav +0 -0
- package/src/assets/sfx/deselect.wav +0 -0
- package/src/assets/sfx/dice_roll.wav +0 -0
- package/src/assets/sfx/die.wav +0 -0
- package/src/assets/sfx/draw.wav +0 -0
- package/src/assets/sfx/drop.wav +0 -0
- package/src/assets/sfx/eat.wav +0 -0
- package/src/assets/sfx/engine.wav +0 -0
- package/src/assets/sfx/error.wav +0 -0
- package/src/assets/sfx/flag.wav +0 -0
- package/src/assets/sfx/flap.wav +0 -0
- package/src/assets/sfx/flip_disc.wav +0 -0
- package/src/assets/sfx/footstep_run.wav +0 -0
- package/src/assets/sfx/footstep_walk.wav +0 -0
- package/src/assets/sfx/game_over.wav +0 -0
- package/src/assets/sfx/gate.wav +0 -0
- package/src/assets/sfx/hit.wav +0 -0
- package/src/assets/sfx/hop.wav +0 -0
- package/src/assets/sfx/hurt.wav +0 -0
- package/src/assets/sfx/invalid.wav +0 -0
- package/src/assets/sfx/jump.wav +0 -0
- package/src/assets/sfx/ladder.wav +0 -0
- package/src/assets/sfx/land.wav +0 -0
- package/src/assets/sfx/laugh.wav +0 -0
- package/src/assets/sfx/lose.wav +0 -0
- package/src/assets/sfx/merge.wav +0 -0
- package/src/assets/sfx/miss.wav +0 -0
- package/src/assets/sfx/nitro.wav +0 -0
- package/src/assets/sfx/pew.wav +0 -0
- package/src/assets/sfx/place.wav +0 -0
- package/src/assets/sfx/pling.wav +0 -0
- package/src/assets/sfx/point.wav +0 -0
- package/src/assets/sfx/pop.wav +0 -0
- package/src/assets/sfx/pop_soft.wav +0 -0
- package/src/assets/sfx/power_up.wav +0 -0
- package/src/assets/sfx/reveal.wav +0 -0
- package/src/assets/sfx/robot.wav +0 -0
- package/src/assets/sfx/rotate.wav +0 -0
- package/src/assets/sfx/run_steps.wav +0 -0
- package/src/assets/sfx/select.wav +0 -0
- package/src/assets/sfx/shuffle.wav +0 -0
- package/src/assets/sfx/six.wav +0 -0
- package/src/assets/sfx/slice.wav +0 -0
- package/src/assets/sfx/slide.wav +0 -0
- package/src/assets/sfx/snake.wav +0 -0
- package/src/assets/sfx/snap.wav +0 -0
- package/src/assets/sfx/splash.wav +0 -0
- package/src/assets/sfx/star.wav +0 -0
- package/src/assets/sfx/start.wav +0 -0
- package/src/assets/sfx/swoosh.wav +0 -0
- package/src/assets/sfx/tap.wav +0 -0
- package/src/assets/sfx/thud.wav +0 -0
- package/src/assets/sfx/thunk.wav +0 -0
- package/src/assets/sfx/tick.wav +0 -0
- package/src/assets/sfx/time_up.wav +0 -0
- package/src/assets/sfx/tock.wav +0 -0
- package/src/assets/sfx/walk_steps.wav +0 -0
- package/src/assets/sfx/warning.wav +0 -0
- package/src/assets/sfx/whack.wav +0 -0
- package/src/assets/sfx/whoosh.wav +0 -0
- package/src/assets/sfx/win.wav +0 -0
- package/src/assets/sfx/zap.wav +0 -0
- package/src/assets/textures/ad_bolt.png +0 -0
- package/src/assets/textures/ad_bubbles.png +0 -0
- package/src/assets/textures/ad_chevrons.png +0 -0
- package/src/assets/textures/asphalt.png +0 -0
- package/src/assets/textures/bark.jpg +0 -0
- package/src/assets/textures/explorer-atlas.png +0 -0
- package/src/assets/textures/facade.png +0 -0
- package/src/assets/textures/grass.png +0 -0
- package/src/assets/textures/leaves.png +0 -0
- package/src/assets/textures/limestone.jpg +0 -0
- package/src/assets/textures/meadow.jpg +0 -0
- package/src/assets/textures/nature-sources.json +0 -87
- package/src/assets/textures/pine.png +0 -0
- package/src/assets/textures/river.jpg +0 -0
- package/src/assets/textures/soft_glow.png +0 -0
- package/src/games/arrows/Arrows.tsx +0 -69
- package/src/games/arrows/ArrowsConstants.ts +0 -107
- package/src/games/arrows/ArrowsGame.ts +0 -735
- package/src/games/arrows/ArrowsScene.ts +0 -395
- package/src/games/balloon-blaster/BalloonBlaster.tsx +0 -75
- package/src/games/balloon-blaster/BalloonBlasterConstants.ts +0 -79
- package/src/games/balloon-blaster/BalloonBlasterGame.ts +0 -287
- package/src/games/balloon-blaster/BalloonBlasterPhysics.ts +0 -95
- package/src/games/balloon-blaster/BalloonBlasterScene.ts +0 -225
- package/src/games/bike-racing/BikeMeshes.ts +0 -273
- package/src/games/bike-racing/BikeRacing.tsx +0 -211
- package/src/games/bike-racing/BikeRacingConstants.ts +0 -103
- package/src/games/bike-racing/BikeRacingGame.ts +0 -557
- package/src/games/bike-racing/BikeRacingScene.ts +0 -119
- package/src/games/block-blast/BlockBlast.tsx +0 -73
- package/src/games/block-blast/BlockBlastConstants.ts +0 -179
- package/src/games/block-blast/BlockBlastGame.ts +0 -471
- package/src/games/block-blast/BlockBlastScene.ts +0 -156
- package/src/games/block-breaker/BlockBreaker.tsx +0 -79
- package/src/games/block-breaker/BlockBreakerConstants.ts +0 -139
- package/src/games/block-breaker/BlockBreakerGame.ts +0 -459
- package/src/games/block-breaker/BlockBreakerScene.ts +0 -257
- package/src/games/bluk/Bluk.tsx +0 -82
- package/src/games/bluk/BlukConstants.ts +0 -121
- package/src/games/bluk/BlukGame.ts +0 -323
- package/src/games/bluk/BlukRules.ts +0 -154
- package/src/games/bluk/BlukScene.ts +0 -514
- package/src/games/bubble-shooter/BubbleShooter.tsx +0 -76
- package/src/games/bubble-shooter/BubbleShooterConstants.ts +0 -103
- package/src/games/bubble-shooter/BubbleShooterGame.ts +0 -459
- package/src/games/bubble-shooter/BubbleShooterScene.ts +0 -158
- package/src/games/candy-crush/CandyCrush.tsx +0 -77
- package/src/games/candy-crush/CandyCrushConstants.ts +0 -94
- package/src/games/candy-crush/CandyCrushGame.ts +0 -477
- package/src/games/candy-crush/CandyCrushScene.ts +0 -209
- package/src/games/car-racing/CarMeshes.ts +0 -440
- package/src/games/car-racing/CarRacing.tsx +0 -211
- package/src/games/car-racing/CarRacingConstants.ts +0 -167
- package/src/games/car-racing/CarRacingFacade.ts +0 -40
- package/src/games/car-racing/CarRacingGame.ts +0 -539
- package/src/games/car-racing/CarRacingScene.ts +0 -1026
- package/src/games/car-racing/CarRacingScenery.ts +0 -265
- package/src/games/chameleon-run/ChameleonRun.tsx +0 -96
- package/src/games/chameleon-run/ChameleonRunBackdrop.ts +0 -186
- package/src/games/chameleon-run/ChameleonRunConstants.ts +0 -47
- package/src/games/chameleon-run/ChameleonRunGame.ts +0 -309
- package/src/games/chameleon-run/ChameleonRunMeshes.ts +0 -305
- package/src/games/chameleon-run/ChameleonRunRules.ts +0 -252
- package/src/games/chameleon-run/ChameleonRunScene.ts +0 -310
- package/src/games/checkers/Checkers.tsx +0 -83
- package/src/games/checkers/CheckersAI.ts +0 -255
- package/src/games/checkers/CheckersConstants.ts +0 -68
- package/src/games/checkers/CheckersGame.ts +0 -322
- package/src/games/checkers/CheckersRules.ts +0 -167
- package/src/games/checkers/CheckersScene.ts +0 -308
- package/src/games/color-switch/ColorSwitch.tsx +0 -75
- package/src/games/color-switch/ColorSwitchConstants.ts +0 -61
- package/src/games/color-switch/ColorSwitchGame.ts +0 -329
- package/src/games/color-switch/ColorSwitchScene.ts +0 -220
- package/src/games/colors-sort/ColorsSort.tsx +0 -82
- package/src/games/colors-sort/ColorsSortConstants.ts +0 -119
- package/src/games/colors-sort/ColorsSortGame.ts +0 -246
- package/src/games/colors-sort/ColorsSortScene.ts +0 -315
- package/src/games/connect-em-all/ConnectEmAll.tsx +0 -70
- package/src/games/connect-em-all/ConnectEmAllConstants.ts +0 -77
- package/src/games/connect-em-all/ConnectEmAllGame.ts +0 -401
- package/src/games/connect-em-all/ConnectEmAllScene.ts +0 -235
- package/src/games/crossy-road/CrossyRoad.tsx +0 -74
- package/src/games/crossy-road/CrossyRoadCamera.ts +0 -59
- package/src/games/crossy-road/CrossyRoadConstants.ts +0 -132
- package/src/games/crossy-road/CrossyRoadGame.ts +0 -293
- package/src/games/crossy-road/CrossyRoadMeshes.ts +0 -195
- package/src/games/crossy-road/CrossyRoadPerson.ts +0 -198
- package/src/games/crossy-road/CrossyRoadRules.ts +0 -99
- package/src/games/crossy-road/CrossyRoadScene.ts +0 -355
- package/src/games/dino-jump/DinoJump.tsx +0 -77
- package/src/games/dino-jump/DinoJumpConstants.ts +0 -80
- package/src/games/dino-jump/DinoJumpGame.ts +0 -344
- package/src/games/dino-jump/DinoJumpScene.ts +0 -686
- package/src/games/dots-and-boxes/DotsAndBoxes.tsx +0 -83
- package/src/games/dots-and-boxes/DotsAndBoxesConstants.ts +0 -77
- package/src/games/dots-and-boxes/DotsAndBoxesGame.ts +0 -271
- package/src/games/dots-and-boxes/DotsAndBoxesScene.ts +0 -249
- package/src/games/falling-sticks/FallingSticks.tsx +0 -72
- package/src/games/falling-sticks/FallingSticksConstants.ts +0 -62
- package/src/games/falling-sticks/FallingSticksGame.ts +0 -220
- package/src/games/falling-sticks/FallingSticksRules.ts +0 -145
- package/src/games/falling-sticks/FallingSticksScene.ts +0 -245
- package/src/games/find-different-number/FindDifferentNumber.tsx +0 -74
- package/src/games/find-different-number/FindDifferentNumberConstants.ts +0 -74
- package/src/games/find-different-number/FindDifferentNumberGame.ts +0 -174
- package/src/games/find-different-number/FindDifferentNumberRules.ts +0 -43
- package/src/games/find-different-number/FindDifferentNumberScene.ts +0 -193
- package/src/games/flappy-bird/FlappyBird.tsx +0 -73
- package/src/games/flappy-bird/FlappyBirdConstants.ts +0 -95
- package/src/games/flappy-bird/FlappyBirdGame.ts +0 -375
- package/src/games/flappy-bird/FlappyBirdScene.ts +0 -630
- package/src/games/fruit-merger/FruitMerger.tsx +0 -73
- package/src/games/fruit-merger/FruitMergerConstants.ts +0 -102
- package/src/games/fruit-merger/FruitMergerGame.ts +0 -285
- package/src/games/fruit-merger/FruitMergerRules.ts +0 -286
- package/src/games/fruit-merger/FruitMergerScene.ts +0 -498
- package/src/games/fruit-ninja/FruitNinja.tsx +0 -129
- package/src/games/fruit-ninja/FruitNinjaConstants.ts +0 -107
- package/src/games/fruit-ninja/FruitNinjaGame.ts +0 -391
- package/src/games/fruit-ninja/FruitNinjaScene.ts +0 -319
- package/src/games/fruit-ninja/FruitNinjaWorld.ts +0 -17
- package/src/games/game-2048/Game2048.tsx +0 -84
- package/src/games/game-2048/Game2048Constants.ts +0 -68
- package/src/games/game-2048/Game2048Game.ts +0 -181
- package/src/games/game-2048/Game2048Rules.ts +0 -101
- package/src/games/game-2048/Game2048Scene.ts +0 -206
- package/src/games/knife-hit/KnifeHit.tsx +0 -78
- package/src/games/knife-hit/KnifeHitConstants.ts +0 -89
- package/src/games/knife-hit/KnifeHitGame.ts +0 -264
- package/src/games/knife-hit/KnifeHitScene.ts +0 -360
- package/src/games/ludo-king/LudoKing.tsx +0 -86
- package/src/games/ludo-king/LudoKingConstants.ts +0 -212
- package/src/games/ludo-king/LudoKingGame.ts +0 -326
- package/src/games/ludo-king/LudoKingRules.ts +0 -143
- package/src/games/ludo-king/LudoKingScene.ts +0 -397
- package/src/games/maze-runner/MazeRunner.tsx +0 -88
- package/src/games/maze-runner/MazeRunnerConstants.ts +0 -65
- package/src/games/maze-runner/MazeRunnerGame.ts +0 -176
- package/src/games/maze-runner/MazeRunnerRules.ts +0 -161
- package/src/games/maze-runner/MazeRunnerScene.ts +0 -245
- package/src/games/memory-match/MemoryMatch.tsx +0 -77
- package/src/games/memory-match/MemoryMatchConstants.ts +0 -105
- package/src/games/memory-match/MemoryMatchGame.ts +0 -222
- package/src/games/memory-match/MemoryMatchRules.ts +0 -38
- package/src/games/memory-match/MemoryMatchScene.ts +0 -268
- package/src/games/mine-sweeper/MineSweeper.tsx +0 -77
- package/src/games/mine-sweeper/MineSweeperConstants.ts +0 -94
- package/src/games/mine-sweeper/MineSweeperGame.ts +0 -234
- package/src/games/mine-sweeper/MineSweeperRules.ts +0 -67
- package/src/games/mine-sweeper/MineSweeperScene.ts +0 -262
- package/src/games/mob-control/MobControl.tsx +0 -73
- package/src/games/mob-control/MobControlConstants.ts +0 -106
- package/src/games/mob-control/MobControlGame.ts +0 -176
- package/src/games/mob-control/MobControlMeshes.ts +0 -85
- package/src/games/mob-control/MobControlRenderer.ts +0 -108
- package/src/games/mob-control/MobControlRules.ts +0 -522
- package/src/games/mob-control/MobControlScene.ts +0 -399
- package/src/games/number-search/NumberSearch.tsx +0 -18
- package/src/games/number-search/NumberSearchNumbers.ts +0 -623
- package/src/games/nuts-and-bolts/NutsAndBolts.tsx +0 -85
- package/src/games/nuts-and-bolts/NutsAndBoltsConstants.ts +0 -75
- package/src/games/nuts-and-bolts/NutsAndBoltsGame.ts +0 -298
- package/src/games/nuts-and-bolts/NutsAndBoltsRules.ts +0 -474
- package/src/games/nuts-and-bolts/NutsAndBoltsScene.ts +0 -498
- package/src/games/pac-man/PacMan.tsx +0 -86
- package/src/games/pac-man/PacManConstants.ts +0 -169
- package/src/games/pac-man/PacManGame.ts +0 -406
- package/src/games/pac-man/PacManRules.ts +0 -255
- package/src/games/pac-man/PacManScene.ts +0 -387
- package/src/games/paper-io/PaperIo.tsx +0 -91
- package/src/games/paper-io/PaperIoConstants.ts +0 -86
- package/src/games/paper-io/PaperIoGame.ts +0 -193
- package/src/games/paper-io/PaperIoMeshes.ts +0 -233
- package/src/games/paper-io/PaperIoRules.ts +0 -315
- package/src/games/paper-io/PaperIoScene.ts +0 -310
- package/src/games/penguin-run/PenguinMeshes.ts +0 -269
- package/src/games/penguin-run/PenguinModel.ts +0 -712
- package/src/games/penguin-run/PenguinRun.tsx +0 -77
- package/src/games/penguin-run/PenguinRunConstants.ts +0 -69
- package/src/games/penguin-run/PenguinRunGame.ts +0 -406
- package/src/games/penguin-run/PenguinRunScene.ts +0 -949
- package/src/games/perfect-circle/PerfectCircle.tsx +0 -74
- package/src/games/perfect-circle/PerfectCircleConstants.ts +0 -52
- package/src/games/perfect-circle/PerfectCircleGame.ts +0 -153
- package/src/games/perfect-circle/PerfectCircleRules.ts +0 -111
- package/src/games/perfect-circle/PerfectCircleScene.ts +0 -176
- package/src/games/pipe-connect/PipeConnect.tsx +0 -69
- package/src/games/pipe-connect/PipeConnectConstants.ts +0 -101
- package/src/games/pipe-connect/PipeConnectGame.ts +0 -179
- package/src/games/pipe-connect/PipeConnectRules.ts +0 -326
- package/src/games/pipe-connect/PipeConnectScene.ts +0 -271
- package/src/games/play-with-tua/PlayWithTua.tsx +0 -123
- package/src/games/play-with-tua/PlayWithTuaConstants.ts +0 -70
- package/src/games/play-with-tua/PlayWithTuaGame.ts +0 -167
- package/src/games/play-with-tua/PlayWithTuaModel.ts +0 -42
- package/src/games/play-with-tua/PlayWithTuaScene.ts +0 -36
- package/src/games/popit-fidget/PopitFidget.tsx +0 -72
- package/src/games/popit-fidget/PopitFidgetConstants.ts +0 -71
- package/src/games/popit-fidget/PopitFidgetGame.ts +0 -148
- package/src/games/popit-fidget/PopitFidgetScene.ts +0 -242
- package/src/games/reversi/Reversi.tsx +0 -94
- package/src/games/reversi/ReversiConstants.ts +0 -67
- package/src/games/reversi/ReversiGame.ts +0 -266
- package/src/games/reversi/ReversiRules.ts +0 -176
- package/src/games/reversi/ReversiScene.ts +0 -240
- package/src/games/search-grid/SearchGrid.tsx +0 -92
- package/src/games/search-grid/SearchGridConstants.ts +0 -178
- package/src/games/search-grid/SearchGridGame.ts +0 -209
- package/src/games/search-grid/SearchGridScene.ts +0 -301
- package/src/games/shared/BackdropCard.tsx +0 -54
- package/src/games/shared/EngineBackdrop.ts +0 -300
- package/src/games/shared/EngineBlasts.ts +0 -136
- package/src/games/shared/EngineBlocks.ts +0 -91
- package/src/games/shared/EngineControlText.ts +0 -17
- package/src/games/shared/EngineDice.ts +0 -250
- package/src/games/shared/EngineFootprint.ts +0 -35
- package/src/games/shared/EngineFruits.ts +0 -1435
- package/src/games/shared/EngineGameLoader.tsx +0 -88
- package/src/games/shared/EngineGameScreen.tsx +0 -325
- package/src/games/shared/EngineGameWidth.ts +0 -3
- package/src/games/shared/EngineGameWidth.web.ts +0 -3
- package/src/games/shared/EngineGlyphs.ts +0 -178
- package/src/games/shared/EngineHud.ts +0 -815
- package/src/games/shared/EngineLoading.ts +0 -477
- package/src/games/shared/EngineMeadow.ts +0 -725
- package/src/games/shared/EngineMeshCache.ts +0 -15
- package/src/games/shared/EngineMeshes.ts +0 -10
- package/src/games/shared/EngineRacingChase.ts +0 -21
- package/src/games/shared/EngineRacingHandling.ts +0 -48
- package/src/games/shared/EngineReadyContext.ts +0 -4
- package/src/games/shared/EngineTilt.ts +0 -44
- package/src/games/shared/EngineTouchSurface.tsx +0 -103
- package/src/games/shared/EngineUtils.ts +0 -52
- package/src/games/shared/GameActivity.ts +0 -13
- package/src/games/shared/GameLoading.tsx +0 -202
- package/src/games/shared/GameLoadingTransition.tsx +0 -61
- package/src/games/shared/ProceduralCacheVersion.ts +0 -2
- package/src/games/shared/useEngineGame.ts +0 -240
- package/src/games/shared/useGameResource.ts +0 -21
- package/src/games/shared/useLiveState.ts +0 -58
- package/src/games/sliding-numbers/SlidingNumbers.tsx +0 -71
- package/src/games/sliding-numbers/SlidingNumbersConstants.ts +0 -65
- package/src/games/sliding-numbers/SlidingNumbersGame.ts +0 -192
- package/src/games/sliding-numbers/SlidingNumbersRules.ts +0 -79
- package/src/games/sliding-numbers/SlidingNumbersScene.ts +0 -193
- package/src/games/snake-3d/Snake3D.tsx +0 -76
- package/src/games/snake-3d/Snake3DConstants.ts +0 -61
- package/src/games/snake-3d/Snake3DGame.ts +0 -192
- package/src/games/snake-3d/Snake3DMotion.ts +0 -22
- package/src/games/snake-3d/Snake3DRules.ts +0 -116
- package/src/games/snake-3d/Snake3DScene.ts +0 -322
- package/src/games/snakes-and-ladders/SnakesAndLadders.tsx +0 -87
- package/src/games/snakes-and-ladders/SnakesAndLaddersConstants.ts +0 -136
- package/src/games/snakes-and-ladders/SnakesAndLaddersGame.ts +0 -310
- package/src/games/snakes-and-ladders/SnakesAndLaddersScene.ts +0 -515
- package/src/games/space-fighter/SpaceFighter.tsx +0 -74
- package/src/games/space-fighter/SpaceFighterConstants.ts +0 -86
- package/src/games/space-fighter/SpaceFighterGame.ts +0 -292
- package/src/games/space-fighter/SpaceFighterScene.ts +0 -550
- package/src/games/spider-solitaire/SpiderSolitaire.tsx +0 -80
- package/src/games/spider-solitaire/SpiderSolitaireConstants.ts +0 -107
- package/src/games/spider-solitaire/SpiderSolitaireGame.ts +0 -387
- package/src/games/spider-solitaire/SpiderSolitaireRules.ts +0 -123
- package/src/games/spider-solitaire/SpiderSolitaireScene.ts +0 -352
- package/src/games/stack-tower/StackTower.tsx +0 -76
- package/src/games/stack-tower/StackTowerConstants.ts +0 -74
- package/src/games/stack-tower/StackTowerGame.ts +0 -236
- package/src/games/stack-tower/StackTowerScene.ts +0 -186
- package/src/games/sudoku/Sudoku.tsx +0 -73
- package/src/games/sudoku/SudokuConstants.ts +0 -127
- package/src/games/sudoku/SudokuGame.ts +0 -250
- package/src/games/sudoku/SudokuRules.ts +0 -125
- package/src/games/sudoku/SudokuScene.ts +0 -304
- package/src/games/tangram/Tangram.tsx +0 -79
- package/src/games/tangram/TangramConstants.ts +0 -64
- package/src/games/tangram/TangramGame.ts +0 -356
- package/src/games/tangram/TangramRules.ts +0 -409
- package/src/games/tangram/TangramScene.ts +0 -203
- package/src/games/tank-1990/Tank1990.tsx +0 -78
- package/src/games/tank-1990/Tank1990Constants.ts +0 -81
- package/src/games/tank-1990/Tank1990Game.ts +0 -331
- package/src/games/tank-1990/Tank1990Rules.ts +0 -390
- package/src/games/tank-1990/Tank1990Scene.ts +0 -434
- package/src/games/tic-tac-toe/TicTacToe.tsx +0 -98
- package/src/games/tic-tac-toe/TicTacToeConstants.ts +0 -69
- package/src/games/tic-tac-toe/TicTacToeGame.ts +0 -200
- package/src/games/tic-tac-toe/TicTacToeRules.ts +0 -103
- package/src/games/tic-tac-toe/TicTacToeScene.ts +0 -245
- package/src/games/tile-home/TileHome.tsx +0 -92
- package/src/games/tile-home/TileHomeConstants.ts +0 -139
- package/src/games/tile-home/TileHomeGame.ts +0 -435
- package/src/games/tile-home/TileHomeScene.ts +0 -154
- package/src/games/treasure-hunt/TreasureHunt.tsx +0 -206
- package/src/games/treasure-hunt/TreasureHuntConstants.ts +0 -266
- package/src/games/treasure-hunt/TreasureHuntControls.ts +0 -280
- package/src/games/treasure-hunt/TreasureHuntGame.ts +0 -402
- package/src/games/treasure-hunt/TreasureHuntMeshes.ts +0 -422
- package/src/games/treasure-hunt/TreasureHuntModel.ts +0 -506
- package/src/games/treasure-hunt/TreasureHuntRules.ts +0 -1210
- package/src/games/treasure-hunt/TreasureHuntScene.ts +0 -586
- package/src/games/whack-a-mole/WhackAMole.tsx +0 -79
- package/src/games/whack-a-mole/WhackAMoleConstants.ts +0 -94
- package/src/games/whack-a-mole/WhackAMoleGame.ts +0 -233
- package/src/games/whack-a-mole/WhackAMoleScene.ts +0 -349
- package/src/games/word-search/WordSearch.tsx +0 -18
- package/src/games/word-search/WordSearchWords.ts +0 -1642
- package/src/helpers/GamePatternData.ts +0 -120
- package/src/helpers/GameSettingsModal.tsx +0 -384
- package/src/helpers/GameTheme.tsx +0 -37
- package/src/helpers/IconPaths.ts +0 -6
- package/src/helpers/index.ts +0 -2
- package/src/index.tsx +0 -60
- package/src/services/GameAssetLoader.ts +0 -111
- package/src/services/GameAssets.ts +0 -149
- package/src/services/GameLoaders.ts +0 -93
- package/src/services/GamesConstants.ts +0 -193
- package/src/services/GamesMetadata.ts +0 -711
- package/src/services/GamesService.ts +0 -116
- package/src/services/HapticsService.ts +0 -48
- package/src/services/ScoringService.ts +0 -19
- package/src/services/SfxSources.ts +0 -81
- package/src/services/SoundsService.ts +0 -582
- package/src/services/ThemeService.ts +0 -281
- package/src/services/UtilsService.ts +0 -134
- 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
|
-
}
|