Stage and Mount#
This is the part of oj that differs from the OneJS model rather than wrapping it. Everything else in the package is a filtered re-export or a thin helper.
mount()#
import { mount, View, Text } from "oj"
function Game() {
return (
<View style={{ flexGrow: 1 }}>
<Text text="Hello" />
</View>
)
}
mount(<Game />, { stage: { size: [960, 540], fit: "letterbox" } })mount(element, options?) renders your tree fitted to a stage. It takes two options, both optional: stage, described below, and theme, which applies the default theme before the first render and can be set to false if you are supplying your own.
Underneath, mount calls render against a root it sets up itself. That is the whole relationship between the two: render is the primitive, mount is render plus a stage and a runtime. In a Unity project mount starts that runtime itself, which is why the same source runs unchanged whether it came from play.onejs.com or from your own scene folder.
You still need a JSRunner and its PanelSettings in the scene. mount decides what happens inside the panel, not whether there is one.
The stage#
A stage is a logical size you lay out against once, plus a rule for what happens when the player's window is some other size.
mount(<Game />, { stage: { size: [960, 540], fit: "letterbox" } })size defaults to 960 by 540. fit takes four values:
fit | What happens |
|---|---|
letterbox | Scales to fit, preserving aspect. Bars fill the remainder. |
cover | Scales to fill, preserving aspect. Overflow is cropped. |
stretch | Scales both axes independently. Aspect is not preserved. |
fluid | No scaling. Your layout gets the real viewport and handles it. |
Pick letterbox for anything with a fixed playfield, and fluid for an interface that should reflow, which is most applications. fluid is the one to reach for if you came here from ordinary OneJS UI work and the idea of a fixed stage feels wrong for what you are building. It is.
Reading the layout#
useStage() returns the current layout and re-renders when the viewport changes.
import { useStage } from "oj"
function Hud() {
const stage = useStage()
return <Text text={`${stage.width} x ${stage.height}`} />
}Two helpers convert between spaces, which matters the moment you mix stage coordinates with anything measured in panel pixels:
import { toStage, fromStage, useStage } from "oj"
const stage = useStage()
const p = toStage(stage, e.localX, e.localY) // panel pixels to stage units
const q = fromStage(stage, p.x, p.y) // stage units back to panel pixelsOn a letterboxed stage the two differ by the size of the bars, so mixing them silently offsets everything by however much matting there is. If a pointer position is out by a constant amount, this is why.
The frame loop#
useFrame(callback, deps?) runs a callback once per frame with the delta in seconds.
import { useFrame } from "oj"
function Ball() {
const ref = useRef(null)
useFrame((dt) => {
y += velocity * dt
ref.current.style.top = y
})
return <View ref={ref} />
}It follows the useEffect dependency rule: the callback is re-registered when deps change, and an empty array registers once. Drive motion from dt rather than assuming a frame rate, since the same code runs on machines that do not agree about what a frame costs.
Write to element styles or refs from a frame callback, not to React state. Sixty state updates a second is sixty renders a second, and the reason useFrame exists at all is to give you a place to move things without paying for that.