Current section

Files

Jump to
paint src paint.gleam
Raw

src/paint.gleam

import gleam/int
import gleam/option.{type Option, None, Some}
import gleam_community/maths/elementary.{pi}
import impl_canvas
/// A 2D picture
pub opaque type Picture {
// Shapes
Blank
Polygon(List(Vec2), closed: Bool)
Arc(radius: Float, start: Angle, end: Angle)
Text(text: String, style: FontProperties)
// TODO: Bitmap images
// Styling
Fill(Picture, Color)
Stroke(Picture, StrokeProperties)
// Transform
Translate(Picture, Vec2)
Scale(Picture, Vec2)
Rotate(Picture, Angle)
// Combine
Combine(List(Picture))
}
/// Options for strokes. Either no stroke or
/// a stroke with some given color and line width.
pub type StrokeProperties {
NoStroke
SolidStroke(Color, Float)
}
/// Internal type used to decouple the font styling
/// from the back-end.
type FontProperties {
FontProperties(size_px: Int, font_family: String)
}
/// An angle in clock-wise direction.
/// See: `angle_rad` and `angle_deg`.
pub opaque type Angle {
Radians(Float)
}
/// Create an angle expressed in radians
pub fn angle_rad(radians: Float) -> Angle {
Radians(radians)
}
/// Create an angle expressed in degrees
pub fn angle_deg(degrees: Float) -> Angle {
Radians(degrees *. pi() /. 180.0)
}
/// A color. See: `color_rgb`.
pub opaque type Color {
Rgb(Int, Int, Int)
}
/// Given three Ints [0-255], create a color
pub fn color_rgb(red: Int, green: Int, blue: Int) -> Color {
Rgb(red, green, blue)
// TODO: ensure the values are within the allowed range
}
/// Convert a hexadecimal string to a color, for example:
/// "#eee", "#efefef" and "abc123"
// pub fn color_hex(hex: String) -> Result(Color, Nil) {
// todo
// }
pub type Vec2 =
#(Float, Float)
/// A blank image
pub fn blank() -> Picture {
Blank
}
/// A circle with some given radius
pub fn circle(radius: Float) -> Picture {
Arc(radius, start: Radians(0.0), end: Radians(2.0 *. pi()))
}
/// An arc with some radius going from some
/// starting angle to some other angle in clock-wise direction
pub fn arc(radius: Float, start: Angle, end: Angle) -> Picture {
Arc(radius, start: start, end: end)
}
/// A polygon consisting of a list of 2d points
pub fn polygon(points: List(#(Float, Float))) -> Picture {
Polygon(points, True)
}
/// Lines (same as a polygon but not a closed shape)
pub fn lines(points: List(#(Float, Float))) -> Picture {
Polygon(points, False)
}
/// A rectangle with some given width and height
pub fn rectangle(width: Float, height: Float) -> Picture {
polygon([#(0.0, 0.0), #(width, 0.0), #(width, height), #(0.0, height)])
}
/// A square
pub fn square(length: Float) -> Picture {
rectangle(length, length)
}
/// Text with some given font size
pub fn text(text: String, font_size_px: Int) -> Picture {
Text(text, style: FontProperties(font_size_px, "sans-serif"))
// TODO: expose more styling options (font and text alignment)
}
/// Translate a picture in horizontal and vertical direction
pub fn translate_xy(picture: Picture, x: Float, y: Float) -> Picture {
Translate(picture, #(x, y))
}
/// Translate a picture in the horizontal direction
pub fn translate_x(picture: Picture, x: Float) -> Picture {
translate_xy(picture, x, 0.0)
}
/// Translate a picture in the vertical direction
pub fn translate_y(picture: Picture, y: Float) -> Picture {
translate_xy(picture, 0.0, y)
}
/// Scale the picture in the horizontal direction
pub fn scale_x(picture: Picture, factor: Float) -> Picture {
Scale(picture, #(factor, 1.0))
}
/// Scale the picture in the vertical direction
pub fn scale_y(picture: Picture, factor: Float) -> Picture {
Scale(picture, #(1.0, factor))
}
/// Scale the picture uniformly in horizontal and vertical direction
pub fn scale_uniform(picture: Picture, factor: Float) -> Picture {
Scale(picture, #(factor, factor))
}
/// Rotate the picture in a clock-wise direction
pub fn rotate(picture: Picture, angle: Angle) -> Picture {
Rotate(picture, angle)
}
/// Fill a picture with some given color, see `Color`.
pub fn fill(picture: Picture, color: Color) -> Picture {
Fill(picture, color)
}
/// Set properties for the stroke. See `StrokeProperties`
pub fn stroke(picture: Picture, stroke_properties: StrokeProperties) -> Picture {
Stroke(picture, stroke_properties)
}
/// Concatenate two pictures
pub fn concat(picture: Picture, another_picture: Picture) -> Picture {
combine([picture, another_picture])
}
/// Combine multiple pictures into one
pub fn combine(pictures: List(Picture)) -> Picture {
Combine(pictures)
}
// HTML Canvas API
/// The configuration of the "canvas"
pub type CanvasConfig {
CanvasConfig(width: Float, height: Float)
}
/// A list of events
pub type Event {
/// Triggered before drawing. Contains the number of milliseconds elapsed.
Tick(Float)
/// Triggered when a key is pressed
KeyDown(Key)
/// Triggered when a key is released
KeyUp(Key)
// TODO: add more events
}
pub type Key {
LeftArrow
RightArrow
UpArrow
DownArrow
Space
// TODO: add more keys
}
/// Make animations and games and display them on the given HTML canvas.
/// Follows the same architecture as Elm/Lustre.
/// The type variable "state" can be anything
/// you want. If you are only making a stateless animation, use `Nil`.
/// Note: this function may only be called once the page has loaded and the
/// document and window objects are available.
pub fn interact_on_canvas(
init: fn(CanvasConfig) -> state,
update: fn(state, Event) -> state,
view: fn(state) -> Picture,
id: String,
) {
let ctx = impl_canvas.get_rendering_context(id)
let initial_state =
init(CanvasConfig(impl_canvas.get_width(ctx), impl_canvas.get_height(ctx)))
impl_canvas.store_state(initial_state, id)
let create_key_handler = fn(event_name, constructor) {
impl_canvas.setup_key_handler(event_name, fn(key_code) {
let key = parse_key_code(key_code)
case key {
Some(key) -> {
let new_state = update(impl_canvas.get_state(id), constructor(key))
impl_canvas.store_state(new_state, id)
}
None -> Nil
}
})
}
create_key_handler("keydown", KeyDown)
create_key_handler("keyup", KeyUp)
// TODO: Support more events and mouse input
impl_canvas.setup_request_animation_frame(get_tick_func(ctx, view, update, id))
}
fn parse_key_code(key_code: Int) -> Option(Key) {
case key_code {
32 -> Some(Space)
37 -> Some(LeftArrow)
38 -> Some(UpArrow)
39 -> Some(RightArrow)
40 -> Some(DownArrow)
_ -> None
}
}
// Gleam does n ot have recursive let bindings, so I need
// to do this workaround...
fn get_tick_func(ctx, view, update, id) {
fn(time) {
let current_state = impl_canvas.get_state(id)
// Trigger a tick event before drawing
let new_state = update(current_state, Tick(time))
impl_canvas.store_state(new_state, id)
// Create the picture
let picture = view(new_state)
// Render the picture on the canvas
impl_canvas.reset(ctx)
display_on_rendering_context(
picture,
ctx,
DrawingState(fill: False, stroke: True),
)
impl_canvas.setup_request_animation_frame(
// call myself
get_tick_func(ctx, view, update, id),
)
}
}
/// Display a picture on a HTML canvas element
/// Note: this function may only be called once the page has loaded and the
/// document objects is available.
pub fn display_on_canvas(init: fn(CanvasConfig) -> Picture, id: String) {
let ctx = impl_canvas.get_rendering_context(id)
impl_canvas.reset(ctx)
let picture =
init(CanvasConfig(impl_canvas.get_width(ctx), impl_canvas.get_height(ctx)))
display_on_rendering_context(
picture,
ctx,
DrawingState(fill: False, stroke: True),
)
}
/// Additional state used when drawing
/// (note that the fill and stroke color as well as the stroke width
/// is stored inside of the context)
type DrawingState {
DrawingState(fill: Bool, stroke: Bool)
}
fn display_on_rendering_context(
picture: Picture,
ctx: impl_canvas.RenderingContext2D,
state: DrawingState,
) {
case picture {
Blank -> Nil
Text(text, properties) -> {
let FontProperties(size_px, font_family) = properties
impl_canvas.save(ctx)
impl_canvas.text(
ctx,
text,
int.to_string(size_px) <> "px " <> font_family,
)
impl_canvas.restore(ctx)
}
Polygon(points, closed) -> {
impl_canvas.polygon(ctx, points, closed, state.fill, state.stroke)
}
Arc(radius, start, end) -> {
let Radians(start_radians) = start
let Radians(end_radians) = end
impl_canvas.arc(
ctx,
radius,
start_radians,
end_radians,
state.fill,
state.stroke,
)
}
Fill(p, color) -> {
impl_canvas.save(ctx)
impl_canvas.set_fill_color(ctx, color_to_css(color))
display_on_rendering_context(p, ctx, DrawingState(..state, fill: True))
impl_canvas.restore(ctx)
}
Stroke(p, stroke) -> {
case stroke {
NoStroke ->
display_on_rendering_context(
p,
ctx,
DrawingState(..state, stroke: False),
)
SolidStroke(color, width) -> {
impl_canvas.save(ctx)
impl_canvas.set_stroke_color(ctx, color_to_css(color))
impl_canvas.set_line_width(ctx, width)
display_on_rendering_context(
p,
ctx,
DrawingState(..state, stroke: True),
)
impl_canvas.restore(ctx)
}
}
}
Translate(p, vec) -> {
let #(x, y) = vec
impl_canvas.save(ctx)
impl_canvas.translate(ctx, x, y)
display_on_rendering_context(p, ctx, state)
impl_canvas.restore(ctx)
}
Scale(p, vec) -> {
let #(x, y) = vec
impl_canvas.save(ctx)
impl_canvas.scale(ctx, x, y)
display_on_rendering_context(p, ctx, state)
impl_canvas.restore(ctx)
}
Rotate(p, angle) -> {
let Radians(rad) = angle
impl_canvas.save(ctx)
impl_canvas.rotate(ctx, rad)
display_on_rendering_context(p, ctx, state)
impl_canvas.restore(ctx)
}
Combine(pictures) -> {
case pictures {
[] -> Nil
[p, ..ps] -> {
display_on_rendering_context(p, ctx, state)
display_on_rendering_context(Combine(ps), ctx, state)
}
}
}
}
}
/// Utility function that is useful for cases where you
/// are no interested in the canvas configuration. For example,
/// ```
/// display_on_canvas(just(circle(30.0)), "my_canvas")
/// // instead of...
/// display_on_canvas(fn(_config) { circle(30.0) }, "my_canvas")
/// ```
pub fn just(picture: Picture) -> fn(a) -> Picture {
fn(_config) { picture }
}
/// Utility to set the origin in the center of the canvas
pub fn center(picture: Picture) -> fn(CanvasConfig) -> Picture {
fn(config) {
let CanvasConfig(width, height) = config
picture |> translate_xy(width *. 0.5, height *. 0.5)
}
}
fn color_to_css(color: Color) -> String {
let Rgb(r, g, b) = color
"rgb("
<> int.to_string(r)
<> ", "
<> int.to_string(g)
<> ", "
<> int.to_string(b)
<> ")"
}