kitten / moomin

An experimental JSX/component framework around Reprocessing for Reason

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.


Moomin

Check out CodeSandbox for a quick example

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

A more extensive example can also be found on Codesandbox.

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

Note: This is a section on Moomin only. If you're trying to understand how Reprocessing differs from Processing read their section on the matter.

  • 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.

Prop Type Description
fill colorT Changes the current fill colour
stroke colorT Changes the current stroke colour
strokeWidth int Changes the current stroke width
strokeLinecap strokeCapT Changes the current stroke cap style
x float Moves the drawing starting point horizontally
y float Moves the drawing starting point vertically
rotate float Rotates the canvas
scaleX float Scales the canvas horizontally
scaleY float Scales the canvas vertically
shearX float Shears the canvas horizontally
shearY float Shears 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.

Prop Type Description
width float The rectangle's width
height float The rectangle's height

<line>

Draws a line.

Note: 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.

Prop Type Description
x1 float Starting point's x coordinate
y1 float Starting point's y coordinate
x2 float Destination point's x coordinate
y2 float Destination point's y coordinate

<triangle>

Draws a triangle.

Note: 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.

Prop Type Description
x1 float 1st corner's x coordinate
y1 float 1st corner's y coordinate
x2 float 2nd corner's x coordinate
y2 float 2nd corner's y coordinate
x3 float 3rd corner's x coordinate
y3 float 3rd corner's y coordinate

<ellipse>

Draws an ellipse.

Note: 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.

Prop Type Description
cx float The ellipse's center x coordinate
cy float The ellipse's center y coordinate
rx float The ellipse's horizontal radius
ry float The ellipse's vertical radius

<circle>

Draws a circle.

Note: 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.

Prop Type Description
cx float The circle's center x coordinate
cy float The circle's center y coordinate
r float The circle's radius

<text>

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

More on how to load fonts in the Reprocessing docs.

Prop Type Description
font fontT Font used to draw the text
body string The 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.

More on how to load images in the Reprocessing docs.

Prop Type Description
image imageT The image that should be drawn
width float The width at which the image is drawn
height float The 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

Prop Type Description
width int The desired window / canvas width
height int The desired window / canvas height
background colorT The canvas' background colour
children array(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

About

An experimental JSX/component framework around Reprocessing for Reason


Languages

Language:OCaml 99.7%Language:C++ 0.3%