Documentation
¶
Overview ¶
Package demo assembles documentation media from a headless simulation: it drives a run and captures frames into a record.Recorder (Clip), shrinks a frame to a sensible size (Downscale), tiles stills into a contact sheet (Montage), and builds the brightness ramps a clean GIF palette needs (Ramp).
It is display-free. A front-end that can draw a frame into a pixel buffer — its own software renderer, or a crucible canvas — generates its whole docs/demos set with no window and no display, which is how the family builds its media in CI.
What each clip shows — which levels, which staging, which inputs — stays in the game. This package only drives and assembles.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Downscale ¶ added in v1.0.0
Downscale shrinks src by an integer factor, averaging each factor×factor block of source pixels into one. A factor below two returns src unchanged.
This is the filter for a still that will be looked at: a montage cell, a screenshot, a documentation frame. It is deliberately not the one record.Recorder applies to captured frames, which point-samples instead — averaging mixes new colours that were never in the scene, and a frame-diffed GIF needs unchanged regions to quantise to byte-identical palette entries.
func Montage ¶
Montage tiles cells into a grid of the given column count, separated and bordered by gap pixels of bg. Cells are laid out left to right, top to bottom, each in a box the size of the largest cell, so a short final row stays aligned. It returns nil when there are no cells.
Games use it for contact sheets — a row of weapon viewmodels, a grid of generated levels — that read better together than as separate files.
func Ramp ¶
Ramp builds a GIF palette by fanning each base colour into steps brightness levels, from dim to full. A palette made of the scene's own colours quantises cleanly without dithering, which keeps a frame-diffed GIF small; pass the result to record.WithPalette.
The palette opens with opaque black so unlit pixels have an exact match. Fewer than one step, or no bases, yields just that black.
Example ¶
package main
import (
"fmt"
"image/color"
"github.com/danielriddell21/crucible/demo"
)
func main() {
// Two scene colours, each fanned into four brightness levels.
pal := demo.Ramp([]color.RGBA{
{R: 150, G: 110, B: 78, A: 255}, // wall
{R: 44, G: 36, B: 30, A: 255}, // floor
}, 4)
fmt.Println(len(pal), "entries")
// Pass it to the recorder: record.NewRecorder(0, 1, 0, record.WithPalette(pal))
}
Output: 9 entries
Types ¶
type Clip ¶
type Clip struct {
// Frames is how many frames to capture before stopping. Zero captures
// until MaxSteps is reached.
Frames int
// Every advances the simulation this many steps per captured frame, so a
// long run can be shown at a watchable pace. Zero and one both mean every
// step is captured.
Every int
// MaxSteps caps total simulation steps so a clip whose Ready never fires
// still terminates. Zero means no cap, in which case Frames must be set.
MaxSteps int
// Step advances the simulation by one step. It is called for every step,
// captured or not.
Step func(step int) error
// Ready gates capture: nothing is recorded until it first returns true, so
// a clip can open on the action rather than the walk up to it. Nil
// captures from the first step.
Ready func(step int) bool
// Frame renders the current state. It is called only for captured frames.
Frame func(step int) image.Image
// Stop ends the clip once it returns true, checked after each captured
// frame so the finishing state is included. A run of unknown length — a
// game that ends when it ends — uses this instead of a frame count. Nil
// runs on to Frames or MaxSteps.
Stop func(step int) bool
}
Clip drives a simulation and captures frames from it. The callbacks carry all the app-specific behaviour: Step advances the game, Frame renders it, and Ready decides when the interesting part has started.
Example ¶
package main
import (
"fmt"
"image"
"github.com/danielriddell21/crucible/demo"
"github.com/danielriddell21/crucible/record"
)
func main() {
// A stand-in for a game: a counter the "renderer" paints as a shade.
tick := 0
clip := demo.Clip{
Frames: 4,
Every: 3, // three simulation steps per captured frame
Step: func(int) error { tick++; return nil },
Frame: func(int) image.Image {
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
for i := 0; i < len(img.Pix); i += 4 {
img.Pix[i], img.Pix[i+3] = uint8(tick*10), 255
}
return img
},
}
rec := record.NewRecorder(12, 1, 0, record.WithFrameDiff())
captured, err := clip.Record(rec)
if err != nil {
panic(err)
}
fmt.Printf("%d frames from %d steps\n", captured, tick)
// rec.Save("docs/demos/clip.gif") writes the GIF.
}
Output: 4 frames from 10 steps