three-fiber/app.js

155 lines
4.2 KiB
JavaScript
Raw Normal View History

2022-11-17 15:37:59 -05:00
//@ts-check
import ReactDOM from 'https://esm.sh/react-dom'
import React from 'https://esm.sh/react'
import * as RTF from 'https://esm.sh/@react-three/fiber'
import { useGLTF, useAnimations, PerspectiveCamera } from 'https://esm.sh/@react-three/drei'
import {html} from 'https://esm.sh/htm/react'
/**@typedef {[x:number, y:number, z:number]} V3 */
/**
* @typedef {{Type:"click", Payload:number}} ActionClick
* @typedef {{Type:"round"}} ActionRound
* @typedef {ActionClick | ActionRound} Action
*/
/**
* @typedef {{Done:boolean, Pos:V3}} StateGoal
*/
/**
* @typedef {{Round:number, Done:boolean, Goals:Array<StateGoal>}} State
*/
/** @typedef {(inState:State, inAction:Action)=>void} Reducer */
/** @typedef {{Initial:State, Reducer:React.Reducer<State, Action>, Consume:()=>GameBinding}} Game */
/** @typedef {[State:State, Dispatcher:React.Dispatch<Action>]} GameBinding */
/** @type Game */
const Game =
{
Initial:
{
Round:0,
Goals:[{Done:false, Pos:[0, 0, 0]}, {Done:true, Pos:[2, 0, 0]}],
Done:false
},
Reducer(inState, inAction)
{
/**@type {(inGoals:Array<StateGoal>)=>boolean} */
const allDone =(inGoals)=>
{
for(let i=0; i<inGoals.length; i++)
{
if(!clone.Goals[i].Done)
{
return false;
}
}
return true;
}
/** @type State */ const clone = {...inState};
switch(inAction.Type)
{
case "click":
{
/**@type StateGoal|undefined*/
const goal = clone.Goals[inAction.Payload];
if(goal)
{
goal.Done = !goal.Done;
}
clone.Done = allDone(clone.Goals);
break;
}
case "round":
{
clone.Round++;
clone.Goals = [];
clone.Done = false;
for(let i=0; i<1+Math.random()*10; i++)
{
/**@type StateGoal*/ const goal = {Done:false, Pos:[
(Math.random()-0.5)*5,
(Math.random()-0.5)*5,
(Math.random()-0.5)*5
]};
clone.Goals.push(goal)
}
}
}
return clone;
},
Consume()
{
return React.useReducer(Game.Reducer, Game.Initial);
}
};
/** @type {(props:{goal:StateGoal, handler:()=>void})=>JSX.Element}*/
function Model(props)
{
const { nodes, animations } = useGLTF("/bounce.gltf");
const Anim = useAnimations(animations);
React.useEffect(()=>
{
const action = Anim.actions.CubeAction;
props.goal.Done ? action.play() : action.stop();
}, [props.goal.Done]);
return html`
<group position=${props.goal.Pos} dispose=${null}>
<mesh
ref=${Anim.ref}
name="Cube"
castShadow
receiveShadow
geometry=${nodes.Cube.geometry}
material=${nodes.Cube.material}
onClick=${e=>
{
e.stopPropagation();
props.handler();
}
} />
</group>`
}
useGLTF.preload("/bounce.gltf");
function App()
{
const [State, Dispatch] = Game.Consume();
/** @type Array<JSX.Element> */
const children = State.Goals.map((g, i)=>
{
return React.createElement(Model,
{
goal:g,
handler:()=>Dispatch({Type:"click", Payload:i}),
key:i
});
});
return html`
<div>
<div>
<dl>
<dt>Round</dt>
<dd>${State.Round}</dd>
<dt>Done</dt>
<dd>${ State.Done ? "Done!" : "Not Done :("}</dd>
<dd><button disabled=${!State.Done} onClick=${()=>Dispatch({Type:"round"})}>Next</button></dd>
</dl>
</div>
<${RTF.Canvas}>
<pointLight position=${[10, 10, 10]} />
<${PerspectiveCamera} makeDefault fov=${100} position=${[0, 0, 10]} />
<${React.Suspense}>
${children}
<//>
<//>
</div>`;
}
ReactDOM.render(
React.createElement(App),
document.getElementById('root')
)