package claudius
Install
dune-project
Dependency
Authors
Maintainers
Sources
sha256=9c6ef0f46bfebbad7124e0377cf11a5928b326dc70f78b7e1b45e537085480b8
sha512=5b25e6b97be4703a8e81cd6f3693d40d33a1eba0ec9dc588f885ea208848d4e1bee37adf294d92bed36302117c3e3a341fd6a9a922ca7ce038aa1d21acc706b5
doc/index.html
Claudius - A fantasy retro computer library.
Claudius started out trying to be a functional library that works like a fantasy console system like TIC-80 or PICO-8: A way to do some retro-style demo graphics programming but in OCaml rather than in LUA. In its current form, it doesn't do nearly as much as those fantasy consoles, instead just concentrating on enabling you to make graphical demos, and lacks things like audio, expressive input support, sprite editors and so forth. But if your goal is to enter something like Tiny Code Christmas or Genuary, then Claudius is designed for that use case.
Key concepts
As mentioned already, Claudius takes inspiration from existing fantasy console environments, whilst trying to encourage a more functional approach to working within such constraints. This section covers the basic concepts that Claudius relies on.
One of the main differences is that rather than work with a memory map layout of the virtual computer (e.g., see TIC-80's docs where it describes the memory layout it uses), Claudius works with a Claudius.Framebuffer.t
type, which is an ADT that manages the pixels in a palette based colour space. You can generate new framebuffers when you like, or recycle old ones. By not treating them as memory mapped as in TIC-80, we can encourage a more functional access approach (as described below).
The framebuffer
Claudius is based on the concept of a framebuffer that exists in a paletted space with a fixed number of colours, similar to how early home computers worked. Every frame (or tick, see below), you will be asked to return a framebuffer to be rendered to the screen. This is a nod to how fantasy-consoles like TIC-80 work, where they present a memory mapped framebuffer, but Claudius is trying to encourage more functional thinking, and so you are responsible for defining and returning framebuffers.
This mode of operation, whilst perhaps a little odd, does have some nice properties where you can write "shader" style code that operates per pixel, either creating a new framebuffer, or by mapping over an existing framebuffer.
Styles of working with Claudius
To support this there's three primary modes of working with Claudius for generating visual effects and demos:
- Pixel functional
- Screen functional
- Imperative
Pixel Functional
Often visual effects can be encoded as "pixel functional" - that is to draw the screen you just need to provide a function that takes the `x` and `y` coordinate of the pixel and then generates its value. A classic example of this would be a Mandelbrot Fractal. To encourage this, the most common way to generate a blank canvas in Claudius is:
let fb = Framebuffer.init (640, 480) (fun _x _y -> 0)
You can also modify an existing framebuffer by providing a shader style function. For example, this shader is used to fade out the previous frame:
let faded_fb = Framebuffer.map (fun pixel -> if pixel > 1 then (pixel - 1) else 0) fb
Done too much this can be expensive in memory allocations, and so there is also a `shader_inplace` variation that does an update on the provided framebuffer - this is less functional, but is sometimes a pragmatic compromise based on performance.
Screen Functional
Whilst pixel functional effects can be fun, they can also be quite limiting, and often you will want to build up bigger scenes to be rendered at once. For this we encourage a functional pipeline style of processing, which we refer to as "Screen Functional" - each frame is a function of time t. To support this Claudius has a primitives library, whereby you can render objects to a framebuffer:
let w, h = Screen.dimensions s in
let palsize = Palette.size (Screen.palette s) in
(* generate some points *)
List.init 42 (fun _ -> (Random.int w, Random.int h))
(* Convert those circle primitives in different colours *)
|> List.mapi (fun idx (x, y) -> Primitive.Circle ({x ; y } ; 3.0 ; idx mod palsize))
(* Draw to framebuffer *)
|> Framebuffer.render fb
More complicated examples for say a 3D-style pipeline you often end up writing pipelines that look like:
(* generate some points for the model *)
generate_model_points ()
(* Advance the model by time t *)
|> update_points t
(* Convert points to 2D *)
|> project_points
(* Convert to primatives *)
|> convert_to_primatives
(* Finally render to framebuffer *)
|> Framebuffer.render fb
You can see an example of this in practice in cladius-examples/day1.
Imperative
Finally, if you just want to get some shapes on screen, then all primatives can be directly rendered to a framebuffer like so:
Framebuffer.draw_line x0 y0 x1 y1 col buffer
Tick and Boot
Similar to both TIC-80, and embedded development systems like Arduino, Claudius programs have two main entry points, functions that you must provide to Claudius: an optional one called `boot` and a mandatory one called `tick`.
Boot
This function is called once, and at its minimum is used to set an initial screen state, though it can also be used by your code to initialise any other state that your program maintains. If you're happy with just a blank screen (using palette entry 0, see screen details below for more on this), then you don't need to provide a `boot` function.
Tick
the `tick` function is mandatory, and will be called once per frame redraw by Claudius. This will be where you either generate a new set of screen contents or you can modify the old screen contents and provide that back to Claudius. The tick function will be provided with a monotomically incrementing counter `t` that can be used to derive a particular frame update.
Screen modes
Claudius isn't as restrictive as a dedicated fantasy console, which typically offers one or a few dedicated modes (e.g., 240x180x16 for TIC-80), but rather you specify a screen as having a resolution and palette of your choosing. Currently palettes are only configured at start-of-day, and not yet modifiable whilst an effect is running, but the ability to have palettes of arbitrary sizes does offset this limitation somewhat, but is probably something that will be addressed in a future release.
Palettes
Palettes are defined as sets of 24bit red, green, blue values. You can thus create a 5 entry palette of black, red, green, blue, white by doing the following:
let p = Palette.of_list [0x000000 ; 0xFF0000; 0x00FF00; 0x0000FF ; 0xFFFFFF]
To assist with some common fancy palettes, there are some helper functions that will save you some code. For instance, you can create a 256 entry monochromatic palette like so:
let p = Palette.generate_mono_palette 256
Or a plasma colour palette like this:
let p = Palette.generate_plasma_palette 16
You can also turn a palette back into a list of integers, to say create a palette that has black and white and 14 plasma colours:
let = p Palette.generate_plasma_palette 14
|> Palette.to_list
|> List.concat [0x000000;0xFFFFFF]
|> Palette.of_list
Aside: because you can't change the palette once Claudius has started, this may feel very restrictive compared to TIC-80 and other fantasy console systems, and you'd be correct. However, because a palette in Claudius allows for more than just 16 colours, you can take a functional approach and compose a palette that is the super set of the various palettes you will need and then use offsets when rendering to access the approapriate "bank" of colours.
Screens
Once you have a colour palette defined, you can now create the screen mode you want to use for this program's lifetime:
let s = Screen.create 640 480 1 (Palette.generate_mono_palette 16)
The first two arguments are the width and height of the emulated screen mode, and final argument is the palette. The third argument is a scaling factor when displayed; if you're trying to work at resolutions like 320x200 (old-school VGA 256 colour), then things can get quite small on modern displays, so you might want to bump that up a bit, for example making it display at three times the size:
let s = Screen.create 320 200 3 (Palette.generate_plasma_palette 256)
Claudius will use this information to generate the initial framebuffer state if you don't provide a boot function, and it is used to generate and render the window Claudius will display on the screen.
Details
Pulling this together
Pixel Functional example
Here is a simple example of a pixel functional effect, that draws a plasma effect on the screen.
open Claudius
let tick t s _p _i =
let palsize = Palette.size (Screen.palette s) in
Framebuffer.init (Screen.dimensions s) (fun x y ->
let ft = (Float.of_int t) /. 10.
and fx = (Float.of_int x) /. 140.
and fy = (Float.of_int y) /. 140. in
let z = 10. +. (sin (ft /. 1000.) *. 5.)
and d = 10. +. (cos (ft /. 1000.) *. 5.) in
let fc = (sin (sin ((fx +. ft) /. z)) +. sin (sin ((fy +. ft) /. d))) *. Float.of_int(palsize / 2) in
let rc = ((int_of_float fc)) mod palsize in
if rc >= 0 then rc else (rc + palsize)
)
let () =
Palette.generate_plasma_palette 1024
|> Screen.create 640 480 1
|> Base.run "Plasma effect" None tick