clone

an offline sliding-block puzzle game in gleam and lustre, with an endless ladder of puzzles ranked by search

main
klot.ski / src / klotski / render.gleam
render.gleam 2.9K
1/// The picture, handed to the pixel pipeline.2///3/// This module is one function wide on purpose. Everything above it — which4/// screen, which ruling, which colour, where a thing goes — has already been5/// decided in Gleam and arrives as a `scene.Frame`. What happens below it is6/// the part the platform will not let a declarative view express: compose a7/// greyscale buffer, read every pixel of it back, error-diffuse or threshold8/// it, reverse the goal block's mark, reprint the tinted regions in colour,9/// paint the ground into the holes nobody drew over, and blit the carried10/// sprites and the type on top.11///12/// A virtual DOM cannot do that, and pretending it can is how a one-bit game13/// becomes a page of divs with a dot pattern behind it. So the canvas is a14/// managed side effect: Lustre owns the elements and the state, and this owns15/// the pixels inside them.16import klotski/scene.{type Frame}17import lustre/effect.{type Effect}1819@external(javascript, "./render_ffi.mjs", "measureBox")20fn measure_box_ffi() -> #(Int, Int, Int, Int)2122@external(javascript, "./render_ffi.mjs", "devicePixelRatio")23pub fn device_pixel_ratio() -> Float2425/// The screen, less anything the hardware has taken out of it.26///27/// The body carries the safe-area insets as padding, so the content box is the28/// honest figure — the window's own width is not, and centring against it is29/// what pushes the picture under a notch on a landscape phone.30pub fn safe_box() -> #(Int, Int, Int, Int) {31 measure_box_ffi()32}3334@external(javascript, "./render_ffi.mjs", "paint")35fn paint_ffi(frame: Frame) -> Nil3637/// Compose and threshold one frame.38pub fn paint(frame: Frame) -> Effect(message) {39 effect.from(fn(_dispatch) { paint_ffi(frame) })40}4142@external(javascript, "./render_ffi.mjs", "watchPointer")43fn watch_pointer_ffi(44 dispatch: fn(message) -> Nil,45 down: fn(Float, Float) -> message,46 move: fn(Float, Float) -> message,47 up: fn(Float, Float) -> message,48 cancel: message,49) -> Nil5051/// Pointer input, in the picture's own units.52///53/// It lives with the renderer because the renderer owns the coordinate space: a54/// tap has to be converted through the same upscale the picture was drawn at,55/// and asking the view layer to know that number would be two places holding one56/// fact. Capture is taken on the way down, so a drag that leaves the canvas is57/// still a drag rather than a released block.58pub fn watch_pointer(59 down: fn(Float, Float) -> message,60 move: fn(Float, Float) -> message,61 up: fn(Float, Float) -> message,62 cancel: message,63) -> Effect(message) {64 effect.from(fn(dispatch) {65 watch_pointer_ffi(dispatch, down, move, up, cancel)66 })67}6869@external(javascript, "./render_ffi.mjs", "clearSprites")70fn clear_sprites_ffi() -> Nil7172/// The sprite cache bakes in the palette and the screen, so a change to either73/// has to drop it.74pub fn clear_sprites() -> Effect(message) {75 effect.from(fn(_dispatch) { clear_sprites_ffi() })76}