demo

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 11, 2026 License: MIT Imports: 5 Imported by: 0

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

func Downscale(src image.Image, factor int) image.Image

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

func Montage(cells []image.Image, cols, gap int, bg color.Color) *image.RGBA

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

func Ramp(bases []color.RGBA, steps int) color.Palette

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

func (Clip) Record

func (c Clip) Record(rec *record.Recorder) (int, error)

Record runs the clip, adding frames to rec, and reports how many it captured. It stops at the frame count, the step cap, or when the recorder reports itself done — whichever comes first.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL