0.1.0-alpha.1 • Published 6 years ago

moomin v0.1.0-alpha.1

Weekly downloads
1
License
MIT
Repository
github
Last release
6 years ago

Moomin

An experimental framework around Reprocessing for Reason, allowing games to be written with components and JSX.

NOTE: This is an experimental stage project. It's basic functionality is to provide components that can render SVG-like elements with Reprocessing. It's currently just a lean alternative API around Reprocessing that doesn't do much more than to organise state in components and to allow JSX instead of direct function calls.

Why?

Reprocessing is an excellent library for writing games with as little code as is possible. Out of sheer interest, I wanted to see whether it was feasible to structure games like you would structure a React app.

The more interesting implication of this is that someone familiar with React will be able to write cleaner games, if they have access to a component/element tree and an SVG-like JSX syntax.

On the other hand it does complicate Reprocessing's minimal and elegant API, and you might find it to add mental overhead.

How?

This library follows ReasonReact's structure and utilises Reason's built-in JSX transpilation to create elements of its own. Instead of creating elements and components for React to use, it instead has its own element structure.

Once elements in Moomin are created however, it follows the React intuition quite closely. Every element is rendered at 60 FPS (Not when state changes!) and preserves its state across renders.

When an element unmounts (or its key changes, like in React) its state is thrown away. The usual lifecycle rules of React apply and ReasonReact's API is adopted vaguely, but not closely.

Table of Contents

Installation

This package is distributed via npm which is bundled with node and should be installed as one of your project's dependencies:

# npm
npm install --save moomin
# yarn
yarn add moomin

After installing the package, don't forget to add it to your bsconfig.json and make sure to enable Reason's react-jsx: 2 mode:

{
  "name": "<your name>",
  "version": "0.1.0",
  "sources": ["src"],
  "bsc-flags": ["-bs-super-errors"],
  "bs-dependencies": [
+    "moomin"
  ],
+  "refmt": 3,
+  "reason": {
+    "react-jsx": 2
+  }
}

This package does not depend on Reprocessing directly as it vendors it for now.

  • Please make sure not to install and add it to your bsconfig.json.
  • Please make sure not to add ReasonReact as well, as its module names are taken up by Moomin.

Usage

Basic Example

Here's a simple example with a single component rendering two rectangles on screen:

open Moomin;

module Example = {
  let component = ReasonReact.statelessComponent("Example");

  let make = (~x, ~y, _children) => {
    ...component,
    render: self => {
      <>
        <rect x={x} y={y} width={50.} height={50.} fill={Colors.blue} />
        <rect x={x} y={y +. 100.} width={50.} height={50.} fill={Colors.blue} />
      </>
    }
  };
};

run(
  <Std.Setup width={200} height={200} background={Colors.white}>
    <Example x={25.} y={25.} />
    <Example x={125.} y={25.} />
  </Std.Setup>
);

As you can see here, the API closely follows ReasonReact's API, with the familiar component creation and JSX.

The render lifecycle in this example returns two <rect> elements inside a fragment. Similar to SVG we'd also be able to wrap them inside a <g>, which allows for some transformations.

Lastly the run function accepts some JSX elements and starts the Reprocessing render loop with all elements' lifecycles.

Stateful Example

Stateful components ("reducer components") follow the same practices.

module Square = {
  type state = {
    rotate: float
  };

  let component = ReasonReact.reducerComponent("Square");

  let make = (_children) => {
    ...component,
    initialState: _glEnv => { rotate: 0. },
    reducer: (_action: unit, state) => state,
    willUpdate: self => { rotate: self.state.rotate +. 0.1 },
    render: self => {
      <rect
        x={5.}
        y={5.}
        width={20.}
        height={20.}
        rotate={self.state.rotate}
        fill={Colors.black}
      />
    }
  };
};

You might notice that some lifecycles are different from ReasonReact's ones. Due to the fact that render cycles in Moomin occur at up to 60 times per second, this also means that some lifecycles from ReasonReact don't quite fit this usecase.

More on this in the next section, "Basics".

Basics

Components

Similarly to ReasonReact there's helper functions to create a component "template". In Moomin there's (currently) only two:

  • ReasonReact.statelessComponent
  • ReasonReact.reducerComponent.

These two functions accept a component name as their only argument. Unlike in ReasonReact these names must be unique and are not for debugging only, as they're also used for the reconciliation process.

The last two examples already illustrate how to create stateless and stateful components.

Lifecycles

Moomin's components have different lifecycle methods from ReasonReact's ones. They're similar but slightly different. To be specific there are:

  • initialState
  • willUpdate
  • render
  • didMount
  • didUpdate
  • willUnmount

Some of these might already imply their general use (more on that in the API section). All of these methods are called synchronously during rendering, so there's no special consideration being made for concurrency, hence willUnmount is just a simple lifecycle method for instance.

"DOM" Elements

Moomin also exposes a ReactDOMRe module. It obviously doesn't render to the DOM, but it has some elements that might remind you of SVG elements, namely:

  • <rect>
  • <image>
  • <line>
  • <triangle>
  • <ellipse>
  • <circle>
  • <text>
  • <g>

Not all SVG elements have been implemented and some (Looking at you <triangle>!) are not SVG elements at all.

Shortcomings / Plans

  • Complex SVG elements have not been implemented. <path> for instance
  • There's no element for tilemaps yet
  • There's no new color utilities for hex colours and more
  • Animations are a consideration, but efforts aren't complete yet (See moomin_animated.re)
  • Events are a consideration, but haven't been implemented yet
  • There's no components in Std for some standard timing or input events

API

Basic JSX

The basic use of components and JSX follows ReasonReact. Take a look at their docs on JSX for more information.

The entrypoint for your game will always be the run() function, which accepts a JSX element.

Base Elements Props

All "DOM" elements accept a common set of props. All their styles and transformations will cascade down the element tree, meaning that when <g> has some props applied, those props will also affect its children.

To summarise these common props affect the current element and its children, but never sibling elements or parents.

PropTypeDescription
fillcolorTChanges the current fill colour
strokecolorTChanges the current stroke colour
strokeWidthintChanges the current stroke width
strokeLinecapstrokeCapTChanges the current stroke cap style
xfloatMoves the drawing starting point horizontally
yfloatMoves the drawing starting point vertically
rotatefloatRotates the canvas
scaleXfloatScales the canvas horizontally
scaleYfloatScales the canvas vertically
shearXfloatShears the canvas horizontally
shearYfloatShears the canvas vertically

Elements

These are all "DOM" elements that can be drawn. Remember that all of the above base props apply to all of these elements.

All of these element's props are technically inside a single shared type, due to ReasonReact's transpilation limitations. They're also all optional, but mostly not passing a required prop will default to an appropriate value.

<g>

The g ("group") element accepts all base props but doesn't draw or render anything on its own. It also accepts any number of children, while all other elements don't accept any children.

If you don't need to pass any prop to <g> you can also just use a fragment (<>).

<rect>

Draws a rectangle.

PropTypeDescription
widthfloatThe rectangle's width
heightfloatThe rectangle's height

<rect>

Draws a rectangle.

PropTypeDescription
widthfloatThe rectangle's width
heightfloatThe rectangle's height

<line>

Draws a line. The points still stay relevant to the current coordinate system's translation. So keep in mind that x and y will still apply to this element and will move it.

PropTypeDescription
x1floatStarting point's x coordinate
y1floatStarting point's y coordinate
x2floatDestination point's x coordinate
y2floatDestination point's y coordinate

<triangle>

Draws a triangle. The points still stay relevant to the current coordinate system's translation. So keep in mind that x and y will still apply to this element and will move it.

PropTypeDescription
x1float1st corner's x coordinate
y1float1st corner's y coordinate
x2float2nd corner's x coordinate
y2float2nd corner's y coordinate
x3float3rd corner's x coordinate
y3float3rd corner's y coordinate

<ellipse>

Draws an ellipse. The points still stay relevant to the current coordinate system's translation. So keep in mind that x and y will still apply to this element and will move it.

PropTypeDescription
cxfloatThe ellipse's center x coordinate
cyfloatThe ellipse's center y coordinate
rxfloatThe ellipse's horizontal radius
ryfloatThe ellipse's vertical radius

<text>

Draws text. Note that it's not its children that's of type string, but instead it accepts a prop.

PropTypeDescription
fontfontTFont used to draw the text
bodystringThe string that will be drawn

<image>

Draws an image. Like in Reprocessing itself, if width and height are not passed, then the image's resolution is used.

PropTypeDescription
imageimageTThe image that should be drawn
widthfloatThe width at which the image is drawn
heightfloatThe height at which the image is drawn

Component Creation

When you create a component there's two helpers you can use:

  • statelessComponent
  • reducerComponent

Both accept a string as an argument, which should be your unique component name. An exception will be raised if the name is already in use.

The return value of these functions are component templates, which you can then spread into your make's return value.

Component & Element Types

selfT

Multiple component lifecycle methods receive self as an argument. The type of this argument is selfT. This is pretty similar (but more simplistic) to ReasonReact's self argument.

type selfT('state, 'action) = {
  send: 'action => unit,
  state: 'state,
  glEnv: Reprocessing.glEnvT
};

send is used to dispatch an action to the component's reducer, which will be queued up and run before the next rerender.

state is the current state of the mounted component.

glEnv is just Reprocessing's GL Env.

componentT

This is the type of every component.

type componentSpecT('state, 'action, 'initState) = {
  internal: internalT('state, 'action),
  initialState: Reprocessing.glEnvT => 'initState,
  willUpdate: selfT('state, 'action) => 'state,
  render: selfT('state, 'action) => elementT,
  didMount: selfT('state, 'action) => unit,
  didUpdate: selfT('state, 'action) => unit,
  willUnmount: selfT('state, 'action) => unit,
  reducer: ('action, 'state) => 'state
}

Component API

initialState

Reprocessing.glEnvT => 'initState

This method receives the Reprocessing GL Env and should return the initial state of the component.

It's only called before the component is mounted as an element.

willUpdate

selfT => 'state

This method is called before a (mounted) component is rerendered / redrawn. It can return new state that will be used during rendering.

render

selfT => elementT

The well known render method returns new elements. You can use ReasonReact.null if you don't wish to return and render any children. When you want to render multiple elements without having a wrapper element drawn, use fragments.

More information can be found in ReasonReact's JSX docs which all apply to Moomin as well.

didMount

selfT => unit

This method is called after render when the component has been mounted for the first time.

didUpdate

selfT => unit

This method is called after render when the (mounted) component has just rerendered.

willUnmount

selfT => unit

This method is called when a component is unmounting.

reducer

('action, 'state) => 'state

Unlike in ReasonReact the reducer methods in Moomin just return a new version of the state given an action. This is because there's no need to avoid rerenders since they're happening at 60 FPS anyway.

Standard Components

Moomin exposes an Std module with some "standard" utility components. Currently there's only one.

<Std.Setup>

This component should be used under all circumstances as it sets up the window / canvas size and redraws the background colour.

It should wrap all your other elements. See the example for more information

PropTypeDescription
widthintThe desired window / canvas width
heightintThe desired window / canvas height
backgroundcolorTThe canvas' background colour
childrenarray(elementT)Child elements (note that it accepts multiple ones)

Reprocessing APIs

Reprocessing's modules are exposed in the same way as they would be if you'd open Reprocessing.

You will find:

  • Utils
  • Constants
  • Draw
  • Env
  • Events

Apart from those Moomin comes with Colors as well, which adds some of its base colours and might have more utilities in the future.

LICENSE

MIT