Initial commit
This commit is contained in:
commit
a6272848f9
379 changed files with 74829 additions and 0 deletions
21
build/packages/paint/LICENSE.txt
Normal file
21
build/packages/paint/LICENSE.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Eli Adelhult
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
34
build/packages/paint/README.md
Normal file
34
build/packages/paint/README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Paint
|
||||
**Make 2D drawings, animations, and games using Gleam and the HTML Canvas!**
|
||||
|
||||
[](https://hex.pm/packages/paint)
|
||||
[](https://hexdocs.pm/paint/)
|
||||
|
||||
Paint is a tiny embedded domain specific language (inspired by [Gloss](https://hackage.haskell.org/package/gloss)).
|
||||
Make pictures out of basic shapes then style, transform, and combine them using the provided functions.
|
||||
|
||||

|
||||
|
||||
```gleam
|
||||
import paint as p
|
||||
import paint/canvas
|
||||
|
||||
fn main() {
|
||||
let my_picture = p.combine([
|
||||
p.circle(30.0),
|
||||
p.circle(20.0) |> p.fill(p.colour_rgb(0, 200, 200)),
|
||||
p.rectangle(50.0, 30.0) |> p.rotate(p.angle_deg(30.0)),
|
||||
p.text("Hello world", 10) |> p.translate_y(-35.0),
|
||||
])
|
||||
|
||||
canvas.display(fn(_: canvas.Config) { my_picture }, "#canvas_id")
|
||||
}
|
||||
```
|
||||
|
||||
**Want to learn more? Read the [docs](https://hexdocs.pm/paint) or browse the [visual examples](https://adelhult.github.io/paint/).**
|
||||
|
||||
## Logo
|
||||
Lucy is borrowed from the [Gleam branding page](https://gleam.run/branding/) and the brush is made by [Delapouite (game icons)](https://game-icons.net/1x1/delapouite/paint-brush.html).
|
||||
|
||||
## Changelog
|
||||
API additions and breaking changes can be found in the file `CHANGELOG.md`.
|
||||
23
build/packages/paint/gleam.toml
Normal file
23
build/packages/paint/gleam.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
name = "paint"
|
||||
version = "1.0.0"
|
||||
target = "javascript"
|
||||
|
||||
# Fill out these fields if you intend to generate HTML documentation or publish
|
||||
# your project to the Hex package manager.
|
||||
#
|
||||
description = "Make 2D drawings, animations, and games (HTML Canvas)"
|
||||
licences = ["MIT"]
|
||||
repository = { type = "github", user = "adelhult", repo = "paint" }
|
||||
links = [
|
||||
{ title = "Visual examples", href = "https://adelhult.github.io/paint/" },
|
||||
]
|
||||
dev-dependencies = { gleeunit = ">= 1.6.1 and < 2.0.0" }
|
||||
#
|
||||
# For a full reference of all the available options, you can have a look at
|
||||
# https://gleam.run/writing-gleam/gleam-toml/.
|
||||
|
||||
|
||||
[dependencies]
|
||||
gleam_stdlib = ">= 0.58.0 and < 2.0.0"
|
||||
gleam_community_colour = ">= 2.0.0 and < 3.0.0"
|
||||
gleam_json = ">= 3.0.2 and < 4.0.0"
|
||||
271
build/packages/paint/src/impl_canvas_bindings.mjs
Normal file
271
build/packages/paint/src/impl_canvas_bindings.mjs
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
import { Ok, Error } from "./gleam.mjs";
|
||||
|
||||
class PaintCanvas extends HTMLElement {
|
||||
// Open an issue if you are in need of any other attributes :)
|
||||
static observedAttributes = ["width", "height", "style", "picture"];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// Create a canvas
|
||||
this.canvas = document.createElement("canvas");
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
`;
|
||||
this.shadow = this.attachShadow({ mode: "open" });
|
||||
this.shadow.appendChild(style);
|
||||
this.shadow.appendChild(this.canvas);
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, _oldValue, newValue) {
|
||||
if (name === "picture") {
|
||||
this.picture = newValue;
|
||||
return;
|
||||
} else if (name === "width") {
|
||||
this.width = newValue;
|
||||
} else if (name === "height") {
|
||||
this.height = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
drawPicture() {
|
||||
if (!this.pictureString) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ctx.reset();
|
||||
const display =
|
||||
window.PAINT_STATE[
|
||||
"display_on_rendering_context_with_default_drawing_state"
|
||||
];
|
||||
|
||||
display(this.pictureString, this.ctx);
|
||||
}
|
||||
|
||||
set picture(value) {
|
||||
this.pictureString = value;
|
||||
this.drawPicture();
|
||||
}
|
||||
|
||||
set width(value) {
|
||||
this.canvas.width = value;
|
||||
this.drawPicture();
|
||||
}
|
||||
|
||||
set height(value) {
|
||||
this.canvas.height = value;
|
||||
this.drawPicture();
|
||||
}
|
||||
|
||||
get width() {
|
||||
return this.canvas.width;
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this.canvas.height;
|
||||
}
|
||||
}
|
||||
|
||||
export function define_web_component() {
|
||||
window.customElements.define("paint-canvas", PaintCanvas);
|
||||
}
|
||||
|
||||
export function get_rendering_context(selector) {
|
||||
// TODO: Handle the case where the canvas element is not found.
|
||||
return document.querySelector(selector).getContext("2d");
|
||||
}
|
||||
|
||||
export function setup_request_animation_frame(callback) {
|
||||
window.requestAnimationFrame((time) => {
|
||||
callback(time);
|
||||
});
|
||||
}
|
||||
|
||||
export function setup_input_handler(event_name, callback) {
|
||||
window.addEventListener(event_name, callback);
|
||||
}
|
||||
|
||||
export function get_key_code(event) {
|
||||
return event.keyCode;
|
||||
}
|
||||
|
||||
export function set_global(state, id) {
|
||||
if (typeof window.PAINT_STATE == "undefined") {
|
||||
window.PAINT_STATE = {};
|
||||
}
|
||||
window.PAINT_STATE[id] = state;
|
||||
}
|
||||
|
||||
export function get_global(id) {
|
||||
if (!window.PAINT_STATE) {
|
||||
return new Error(undefined);
|
||||
}
|
||||
if (!(id in window.PAINT_STATE)) {
|
||||
return new Error(undefined);
|
||||
}
|
||||
return new Ok(window.PAINT_STATE[id]);
|
||||
}
|
||||
|
||||
export function get_width(ctx) {
|
||||
return ctx.canvas.clientWidth;
|
||||
}
|
||||
|
||||
export function get_height(ctx) {
|
||||
return ctx.canvas.clientHeight;
|
||||
}
|
||||
|
||||
// Based on https://stackoverflow.com/questions/17130395/real-mouse-position-in-canvas
|
||||
export function mouse_pos(ctx, event) {
|
||||
// Calculate the scaling of the canvas vs its content
|
||||
const rect = ctx.canvas.getBoundingClientRect();
|
||||
const scaleX = ctx.canvas.width / rect.width;
|
||||
const scaleY = ctx.canvas.height / rect.height;
|
||||
|
||||
return [
|
||||
(event.clientX - rect.left) * scaleX,
|
||||
(event.clientY - rect.top) * scaleY,
|
||||
];
|
||||
}
|
||||
|
||||
// if check_pressed is true, the function will return true if the button was pressed
|
||||
// if check_pressed is false, the function will return true if the button was released
|
||||
export function check_mouse_button(
|
||||
event,
|
||||
previous_event,
|
||||
button_index,
|
||||
check_pressed,
|
||||
) {
|
||||
let previous_buttons = previous_event?.buttons ?? 0;
|
||||
let current_buttons = event.buttons;
|
||||
|
||||
// ~001 &&
|
||||
// 011
|
||||
// -----
|
||||
// 010 found the newly pressed!
|
||||
//
|
||||
// 011 &&
|
||||
// ~001
|
||||
// -----
|
||||
// 010 found the newly released!
|
||||
if (check_pressed) {
|
||||
previous_buttons = ~previous_buttons;
|
||||
} else {
|
||||
current_buttons = ~current_buttons;
|
||||
}
|
||||
|
||||
let button = previous_buttons & current_buttons & (1 << button_index);
|
||||
return !!button;
|
||||
}
|
||||
|
||||
export function reset(ctx) {
|
||||
ctx.reset();
|
||||
}
|
||||
|
||||
export function arc(ctx, radius, start, end, fill, stroke) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, radius, start, end);
|
||||
if (fill) {
|
||||
ctx.fill();
|
||||
}
|
||||
if (stroke) {
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
export function polygon(ctx, points, closed, fill, stroke) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 0);
|
||||
let started = false;
|
||||
for (const point of points) {
|
||||
let x = point[0];
|
||||
let y = point[1];
|
||||
if (started) {
|
||||
ctx.lineTo(x, y);
|
||||
} else {
|
||||
ctx.moveTo(x, y);
|
||||
started = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (closed) {
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
if (fill && closed) {
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
if (stroke) {
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
export function text(ctx, text, style) {
|
||||
ctx.font = style;
|
||||
ctx.fillText(text, 0, 0);
|
||||
}
|
||||
|
||||
export function save(ctx) {
|
||||
ctx.save();
|
||||
}
|
||||
|
||||
export function restore(ctx) {
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
export function set_fill_colour(ctx, css_colour) {
|
||||
ctx.fillStyle = css_colour;
|
||||
}
|
||||
|
||||
export function set_stroke_color(ctx, css_color) {
|
||||
ctx.strokeStyle = css_color;
|
||||
}
|
||||
|
||||
export function set_line_width(ctx, width) {
|
||||
ctx.lineWidth = width;
|
||||
}
|
||||
|
||||
export function translate(ctx, x, y) {
|
||||
ctx.translate(x, y);
|
||||
}
|
||||
|
||||
export function scale(ctx, x, y) {
|
||||
ctx.scale(x, y);
|
||||
}
|
||||
|
||||
export function rotate(ctx, radians) {
|
||||
ctx.rotate(radians);
|
||||
}
|
||||
|
||||
export function reset_transform(ctx) {
|
||||
ctx.resetTransform();
|
||||
}
|
||||
|
||||
export function draw_image(ctx, image, width_px, height_px) {
|
||||
ctx.drawImage(image, 0, 0, width_px, height_px);
|
||||
}
|
||||
|
||||
export function image_from_query(selector) {
|
||||
return document.querySelector(selector);
|
||||
}
|
||||
|
||||
export function image_from_src(src) {
|
||||
const image = new Image();
|
||||
image.src = src;
|
||||
return image;
|
||||
}
|
||||
|
||||
export function on_image_load(image, callback) {
|
||||
if (image.complete) {
|
||||
callback();
|
||||
} else {
|
||||
image.addEventListener("load", callback);
|
||||
}
|
||||
}
|
||||
|
||||
export function set_image_smoothing_enabled(ctx, value) {
|
||||
ctx.imageSmoothingEnabled = value;
|
||||
}
|
||||
3
build/packages/paint/src/numbers_ffi.mjs
Normal file
3
build/packages/paint/src/numbers_ffi.mjs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function pi() {
|
||||
return Math.PI;
|
||||
}
|
||||
201
build/packages/paint/src/paint.gleam
Normal file
201
build/packages/paint/src/paint.gleam
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
//// This module contains the main `Picture` type as well as the
|
||||
//// function you can use to construct, modify and combine pictures.
|
||||
|
||||
import gleam/result
|
||||
import gleam_community/colour
|
||||
import paint/internal/types as internal_implementation
|
||||
|
||||
/// A 2D picture. This is the type which this entire library revolves around.
|
||||
///
|
||||
///> [!NOTE]
|
||||
///> Unless you intend to author a new backend you should **consider this type opaque and never use any of its constructors**.
|
||||
///> Instead, make use of the many utility functions defined in this module (`circle`, `combine`, `fill`, etc.)
|
||||
pub type Picture =
|
||||
internal_implementation.Picture
|
||||
|
||||
/// A reference to an image (i.e. a texture), not to be confused with the `Picture` type.
|
||||
/// To create an image, see the image functions in the `canvas` back-end.
|
||||
pub type Image =
|
||||
internal_implementation.Image
|
||||
|
||||
/// An angle in clock-wise direction.
|
||||
/// See: `angle_rad` and `angle_deg`.
|
||||
pub type Angle =
|
||||
internal_implementation.Angle
|
||||
|
||||
/// Create an angle expressed in radians
|
||||
pub fn angle_rad(radians: Float) -> Angle {
|
||||
internal_implementation.Radians(radians)
|
||||
}
|
||||
|
||||
/// Create an angle expressed in degrees
|
||||
pub fn angle_deg(degrees: Float) -> Angle {
|
||||
internal_implementation.Radians(degrees *. pi() /. 180.0)
|
||||
}
|
||||
|
||||
/// A rexport of the Colour type from [gleam_community/colour](https://hexdocs.pm/gleam_community_colour/).
|
||||
/// Paint also includes the functions `colour_hex` and `colour_rgb` to
|
||||
/// easily construct Colours, but feel free to import the `gleam_community/colour` module
|
||||
/// and use the many utility that are provided from there.
|
||||
pub type Colour =
|
||||
colour.Colour
|
||||
|
||||
/// A utility around [colour.from_rgb_hex_string](https://hexdocs.pm/gleam_community_colour/gleam_community/colour.html#from_rgb_hex_string)
|
||||
/// (from `gleam_community/colour`) that **panics** on an invalid hex code.
|
||||
pub fn colour_hex(string: String) -> Colour {
|
||||
result.lazy_unwrap(colour.from_rgb_hex_string(string), fn() {
|
||||
panic as "Failed to parse hex code"
|
||||
})
|
||||
}
|
||||
|
||||
/// A utility around [colour.from_rgb255](https://hexdocs.pm/gleam_community_colour/gleam_community/colour.html#from_rgb255)
|
||||
/// (from `gleam_community/colour`) that **panics** if the values are outside of the allowed range.
|
||||
pub fn colour_rgb(red: Int, green: Int, blue: Int) -> Colour {
|
||||
result.lazy_unwrap(colour.from_rgb255(red, green, blue), fn() {
|
||||
panic as "The value was not inside of the valid range [0-255]"
|
||||
})
|
||||
}
|
||||
|
||||
pub type Vec2 =
|
||||
#(Float, Float)
|
||||
|
||||
/// A blank picture
|
||||
pub fn blank() -> Picture {
|
||||
internal_implementation.Blank
|
||||
}
|
||||
|
||||
/// A circle with some given radius
|
||||
pub fn circle(radius: Float) -> Picture {
|
||||
internal_implementation.Arc(
|
||||
radius,
|
||||
start: internal_implementation.Radians(0.0),
|
||||
end: internal_implementation.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 {
|
||||
internal_implementation.Arc(radius, start: start, end: end)
|
||||
}
|
||||
|
||||
/// A polygon consisting of a list of 2d points
|
||||
pub fn polygon(points: List(#(Float, Float))) -> Picture {
|
||||
internal_implementation.Polygon(points, True)
|
||||
}
|
||||
|
||||
/// Lines (same as a polygon but not a closed shape)
|
||||
pub fn lines(points: List(#(Float, Float))) -> Picture {
|
||||
internal_implementation.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)
|
||||
}
|
||||
|
||||
/// Draw an image such as a PNG, JPEG or an SVG. See the `canvas` back-end for more details on how to load images.
|
||||
pub fn image(image: Image, width_px width_px, height_px height_px) -> Picture {
|
||||
// TODO: add a function that allows us to draw only part of an image, flip, and if we want smooth scaling or not
|
||||
internal_implementation.ImageRef(image, width_px:, height_px:)
|
||||
}
|
||||
|
||||
/// Set image scaling to be smooth (this is the default behaviour)
|
||||
pub fn image_scaling_smooth(picture: Picture) -> Picture {
|
||||
internal_implementation.ImageScalingBehaviour(
|
||||
picture,
|
||||
internal_implementation.ScalingSmooth,
|
||||
)
|
||||
}
|
||||
|
||||
/// Disable smooth image scaling, suitable for pixel art.
|
||||
pub fn image_scaling_pixelated(picture: Picture) -> Picture {
|
||||
internal_implementation.ImageScalingBehaviour(
|
||||
picture,
|
||||
internal_implementation.ScalingPixelated,
|
||||
)
|
||||
}
|
||||
|
||||
/// Text with some given font size
|
||||
pub fn text(text: String, px font_size: Int) -> Picture {
|
||||
internal_implementation.Text(
|
||||
text,
|
||||
style: internal_implementation.FontProperties(font_size, "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 {
|
||||
internal_implementation.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 {
|
||||
internal_implementation.Scale(picture, #(factor, 1.0))
|
||||
}
|
||||
|
||||
/// Scale the picture in the vertical direction
|
||||
pub fn scale_y(picture: Picture, factor: Float) -> Picture {
|
||||
internal_implementation.Scale(picture, #(1.0, factor))
|
||||
}
|
||||
|
||||
/// Scale the picture uniformly in horizontal and vertical direction
|
||||
pub fn scale_uniform(picture: Picture, factor: Float) -> Picture {
|
||||
internal_implementation.Scale(picture, #(factor, factor))
|
||||
}
|
||||
|
||||
/// Rotate the picture in a clock-wise direction
|
||||
pub fn rotate(picture: Picture, angle: Angle) -> Picture {
|
||||
internal_implementation.Rotate(picture, angle)
|
||||
}
|
||||
|
||||
/// Fill a picture with some given colour, see `Colour`.
|
||||
pub fn fill(picture: Picture, colour: Colour) -> Picture {
|
||||
internal_implementation.Fill(picture, colour)
|
||||
}
|
||||
|
||||
/// Set a solid stroke with some given colour and width
|
||||
pub fn stroke(picture: Picture, colour: Colour, width width: Float) -> Picture {
|
||||
internal_implementation.Stroke(
|
||||
picture,
|
||||
internal_implementation.SolidStroke(colour, width),
|
||||
)
|
||||
}
|
||||
|
||||
/// Remove the stroke of the given picture
|
||||
pub fn stroke_none(picture: Picture) -> Picture {
|
||||
internal_implementation.Stroke(picture, internal_implementation.NoStroke)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
internal_implementation.Combine(pictures)
|
||||
}
|
||||
|
||||
// Internal utility function to get Pi π
|
||||
@external(erlang, "math", "pi")
|
||||
@external(javascript, "./numbers_ffi.mjs", "pi")
|
||||
fn pi() -> Float {
|
||||
3.1415926
|
||||
}
|
||||
461
build/packages/paint/src/paint/canvas.gleam
Normal file
461
build/packages/paint/src/paint/canvas.gleam
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
//// A HTML canvas backend that can be used for displaying
|
||||
//// your `Picture`s. There are three different ways of doing so:
|
||||
//// - `display` (provide a picture and a CSS selector to some canvas element)
|
||||
//// - `define_web_component` (an alternative to `display` using custom web components, useful if you are using a web framework like Lustre)
|
||||
//// - `interact` (allows you to make animations and interactive programs)
|
||||
|
||||
import gleam/int
|
||||
import gleam/option.{type Option, None, Some}
|
||||
import gleam_community/colour
|
||||
import paint.{translate_xy}
|
||||
import paint/encode
|
||||
import paint/event.{type Event}
|
||||
import paint/internal/impl_canvas
|
||||
import paint/internal/types.{
|
||||
type Image, type Picture, Arc, Blank, Combine, Fill, FontProperties, Image,
|
||||
NoStroke, Polygon, Radians, Rotate, Scale, SolidStroke, Stroke, Text,
|
||||
Translate,
|
||||
}
|
||||
|
||||
/// The configuration of the "canvas"
|
||||
pub type Config {
|
||||
Config(width: Float, height: Float)
|
||||
}
|
||||
|
||||
/// Create a reference to an image using a CSS query selector. For example:
|
||||
/// ```
|
||||
/// fn kitten() {
|
||||
/// canvas.image_from_query("#kitten")
|
||||
/// }
|
||||
/// // In the HTML file:
|
||||
/// // <img
|
||||
/// // style="display: none"
|
||||
/// // src="https://upload.wikimedia.org/wikipedia/commons/4/4d/Cat_November_2010-1a.jpg"
|
||||
/// // id="kitten"
|
||||
/// // />
|
||||
/// ```
|
||||
///
|
||||
/// > [!WARNING]
|
||||
/// > **Important**: Make sure the image has loaded before trying to draw a pictures referencing it.
|
||||
/// > You can do this using `canvas.wait_until_loaded` function.
|
||||
pub fn image_from_query(selector: String) -> Image {
|
||||
let id = "image-selector-" <> selector
|
||||
case impl_canvas.get_global(id) {
|
||||
// Re-use the cached image if we can
|
||||
Ok(_) -> {
|
||||
Nil
|
||||
}
|
||||
Error(Nil) -> {
|
||||
let image = impl_canvas.image_from_query(selector)
|
||||
impl_canvas.set_global(image, id)
|
||||
}
|
||||
}
|
||||
Image(id)
|
||||
}
|
||||
|
||||
/// Create a reference to an image using a source path.
|
||||
/// ```
|
||||
/// fn my_logo_image() {
|
||||
/// canvas.image_from_src("./priv/static/logo.svg")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// > [!WARNING]
|
||||
/// > **Important**: Make sure the image has loaded before trying to draw a pictures referencing it.
|
||||
/// > You can do this using `canvas.wait_until_loaded` function.
|
||||
pub fn image_from_src(src: String) -> Image {
|
||||
let id = "image-src-" <> src
|
||||
case impl_canvas.get_global(id) {
|
||||
// Re-use the cached image if we can
|
||||
Ok(_) -> {
|
||||
Nil
|
||||
}
|
||||
Error(Nil) -> {
|
||||
let image = impl_canvas.image_from_src(src)
|
||||
impl_canvas.set_global(image, id)
|
||||
}
|
||||
}
|
||||
Image(id)
|
||||
}
|
||||
|
||||
/// Wait until a list of images have all been loaded, for example:
|
||||
/// ```
|
||||
/// fn lucy() {
|
||||
/// canvas.image_from_query("#lucy")
|
||||
/// }
|
||||
///
|
||||
/// fn cat() {
|
||||
/// canvas.image_from_src("./path/to/kitten.png")
|
||||
/// }
|
||||
///
|
||||
/// pub fn main() {
|
||||
/// use <- canvas.wait_until_loaded([lucy(), kitten()])
|
||||
/// // It is now safe to draw Pictures containing the images lucy and kitten :)
|
||||
/// }
|
||||
/// ```
|
||||
pub fn wait_until_loaded(images: List(Image), on_loaded: fn() -> Nil) -> Nil {
|
||||
case images {
|
||||
[] -> on_loaded()
|
||||
[image, ..rest] -> {
|
||||
let Image(id:) = image
|
||||
let assert Ok(js_image) = impl_canvas.get_global(id)
|
||||
impl_canvas.on_image_load(js_image, fn() {
|
||||
wait_until_loaded(rest, on_loaded)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Display a picture on a HTML canvas element
|
||||
/// (specified by some [CSS Selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors)).
|
||||
/// ```
|
||||
/// canvas.display(fn (_: canvas.Config) { circle(50.0) }, "#mycanvas")
|
||||
/// ```
|
||||
pub fn display(init: fn(Config) -> Picture, selector: String) {
|
||||
let ctx = impl_canvas.get_rendering_context(selector)
|
||||
impl_canvas.reset(ctx)
|
||||
let picture =
|
||||
init(Config(impl_canvas.get_width(ctx), impl_canvas.get_height(ctx)))
|
||||
display_on_rendering_context(picture, ctx, default_drawing_state)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
const default_drawing_state = DrawingState(fill: False, stroke: True)
|
||||
|
||||
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, colour) -> {
|
||||
impl_canvas.save(ctx)
|
||||
impl_canvas.set_fill_colour(ctx, colour.to_css_rgba_string(colour))
|
||||
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, colour.to_css_rgba_string(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
types.ImageRef(Image(id:), width_px:, height_px:) -> {
|
||||
// TODO: log an error if this fails?
|
||||
let assert Ok(image) = impl_canvas.get_global(id)
|
||||
impl_canvas.draw_image(ctx, image, width_px, height_px)
|
||||
}
|
||||
types.ImageScalingBehaviour(p, behaviour) -> {
|
||||
impl_canvas.save(ctx)
|
||||
impl_canvas.set_image_smoothing_enabled(ctx, case behaviour {
|
||||
types.ScalingPixelated -> False
|
||||
types.ScalingSmooth -> True
|
||||
})
|
||||
display_on_rendering_context(p, ctx, state)
|
||||
impl_canvas.restore(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Animations, interactive applications and tiny games can be built using the
|
||||
/// `interact` function. It roughly follows the [Elm architecture](https://guide.elm-lang.org/architecture/).
|
||||
/// Here is a short example:
|
||||
/// ```
|
||||
/// type State =
|
||||
/// Int
|
||||
///
|
||||
/// fn init(_: canvas.Config) -> State {
|
||||
/// 0
|
||||
/// }
|
||||
///
|
||||
/// fn update(state: State, event: event.Event) -> State {
|
||||
/// case event {
|
||||
/// event.Tick(_) -> state + 1
|
||||
/// _ -> state
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: State) -> Picture {
|
||||
/// paint.circle(int.to_float(state))
|
||||
/// }
|
||||
///
|
||||
/// fn main() {
|
||||
/// interact(init, update, view, "#mycanvas")
|
||||
/// }
|
||||
/// ```
|
||||
pub fn interact(
|
||||
init: fn(Config) -> state,
|
||||
update: fn(state, Event) -> state,
|
||||
view: fn(state) -> Picture,
|
||||
selector: String,
|
||||
) {
|
||||
let ctx = impl_canvas.get_rendering_context(selector)
|
||||
let initial_state =
|
||||
init(Config(impl_canvas.get_width(ctx), impl_canvas.get_height(ctx)))
|
||||
|
||||
impl_canvas.set_global(initial_state, selector)
|
||||
|
||||
// Handle keyboard input
|
||||
let create_key_handler = fn(event_name, constructor) {
|
||||
impl_canvas.setup_input_handler(
|
||||
event_name,
|
||||
fn(event: impl_canvas.KeyboardEvent) {
|
||||
let key = parse_key_code(impl_canvas.get_key_code(event))
|
||||
case key {
|
||||
Some(key) -> {
|
||||
let assert Ok(old_state) = impl_canvas.get_global(selector)
|
||||
let new_state = update(old_state, constructor(key))
|
||||
impl_canvas.set_global(new_state, selector)
|
||||
}
|
||||
None -> Nil
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
create_key_handler("keydown", event.KeyboardPressed)
|
||||
create_key_handler("keyup", event.KeyboardRelased)
|
||||
|
||||
// Handle mouse movement
|
||||
impl_canvas.setup_input_handler(
|
||||
"mousemove",
|
||||
fn(event: impl_canvas.MouseEvent) {
|
||||
let #(x, y) = impl_canvas.mouse_pos(ctx, event)
|
||||
let assert Ok(old_state) = impl_canvas.get_global(selector)
|
||||
let new_state = update(old_state, event.MouseMoved(x, y))
|
||||
impl_canvas.set_global(new_state, selector)
|
||||
Nil
|
||||
},
|
||||
)
|
||||
|
||||
// Handle mouse buttons
|
||||
let create_mouse_button_handler = fn(event_name, constructor, check_pressed) {
|
||||
impl_canvas.setup_input_handler(
|
||||
event_name,
|
||||
fn(event: impl_canvas.MouseEvent) {
|
||||
// Read the previous state of the mouse
|
||||
let previous_event_id = "PAINT_PREVIOUS_MOUSE_INPUT_FOR_" <> selector
|
||||
let previous_event = impl_canvas.get_global(previous_event_id)
|
||||
// Save this state
|
||||
impl_canvas.set_global(event, previous_event_id)
|
||||
|
||||
// A utility to check which buttons was just pressed/released
|
||||
let check_button = fn(i) {
|
||||
impl_canvas.check_mouse_button(
|
||||
event,
|
||||
previous_event,
|
||||
i,
|
||||
check_pressed,
|
||||
)
|
||||
}
|
||||
|
||||
let trigger_update = fn(button) {
|
||||
let assert Ok(old_state) = impl_canvas.get_global(selector)
|
||||
let new_state = update(old_state, constructor(button))
|
||||
impl_canvas.set_global(new_state, selector)
|
||||
}
|
||||
|
||||
// Note: it is rather rare, but it seems that multiple buttons
|
||||
// can be pressed in the very same MouseEvent, so we may need to
|
||||
// trigger multiple events at once.
|
||||
case check_button(0) {
|
||||
True -> trigger_update(event.MouseButtonLeft)
|
||||
False -> Nil
|
||||
}
|
||||
case check_button(1) {
|
||||
True -> trigger_update(event.MouseButtonRight)
|
||||
False -> Nil
|
||||
}
|
||||
case check_button(2) {
|
||||
True -> trigger_update(event.MouseButtonMiddle)
|
||||
False -> Nil
|
||||
}
|
||||
|
||||
Nil
|
||||
},
|
||||
)
|
||||
}
|
||||
create_mouse_button_handler("mousedown", event.MousePressed, True)
|
||||
create_mouse_button_handler("mouseup", event.MouseReleased, False)
|
||||
|
||||
impl_canvas.setup_request_animation_frame(get_tick_func(
|
||||
ctx,
|
||||
view,
|
||||
update,
|
||||
selector,
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_key_code(key_code: Int) -> Option(event.Key) {
|
||||
case key_code {
|
||||
32 -> Some(event.KeySpace)
|
||||
37 -> Some(event.KeyLeftArrow)
|
||||
38 -> Some(event.KeyUpArrow)
|
||||
39 -> Some(event.KeyRightArrow)
|
||||
40 -> Some(event.KeyDownArrow)
|
||||
87 -> Some(event.KeyW)
|
||||
65 -> Some(event.KeyA)
|
||||
83 -> Some(event.KeyS)
|
||||
68 -> Some(event.KeyD)
|
||||
90 -> Some(event.KeyZ)
|
||||
88 -> Some(event.KeyX)
|
||||
67 -> Some(event.KeyC)
|
||||
18 -> Some(event.KeyEnter)
|
||||
27 -> Some(event.KeyEscape)
|
||||
8 -> Some(event.KeyBackspace)
|
||||
_ -> None
|
||||
}
|
||||
}
|
||||
|
||||
// Gleam does not have recursive let bindings, so I need
|
||||
// to do this workaround...
|
||||
fn get_tick_func(ctx, view, update, selector) {
|
||||
fn(time) {
|
||||
let assert Ok(current_state) = impl_canvas.get_global(selector)
|
||||
|
||||
// Trigger a tick event before drawing
|
||||
let new_state = update(current_state, event.Tick(time))
|
||||
impl_canvas.set_global(new_state, selector)
|
||||
|
||||
// Create the picture
|
||||
let picture = view(new_state)
|
||||
|
||||
// Render the picture on the canvas
|
||||
impl_canvas.reset(ctx)
|
||||
display_on_rendering_context(picture, ctx, default_drawing_state)
|
||||
impl_canvas.setup_request_animation_frame(
|
||||
// call myself
|
||||
get_tick_func(ctx, view, update, selector),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// If you are using [Lustre](https://github.com/lustre-labs/lustre) or some other framework to build
|
||||
/// your web application you may prefer to use the [web components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) API
|
||||
/// and the `define_web_component` function.
|
||||
/// ```
|
||||
/// // Call this function once to register a custom HTML element <paint-canvas>
|
||||
/// canvas.define_web_component()
|
||||
/// // You can then display your picture by setting the "picture"
|
||||
/// // property or attribute on the element.
|
||||
///
|
||||
/// // In Lustre it would look something like this:
|
||||
/// fn canvas(picture: paint.Picture, attributes: List(attribute.Attribute(a))) {
|
||||
/// element.element(
|
||||
/// "paint-canvas",
|
||||
/// [attribute.attribute("picture", encode.to_string(picture)), ..attributes],
|
||||
/// [],
|
||||
/// )
|
||||
///}
|
||||
/// ```
|
||||
/// A more detailed example for using this API can be found in the `demos/with_lustre` directory.
|
||||
pub fn define_web_component() -> Nil {
|
||||
impl_canvas.define_web_component()
|
||||
// somewhat of an ugly hack, but the setter for the web component will need to call this
|
||||
// when the picture property changes. Therefore we
|
||||
// bind this function to the window object so we can access it from the JS side of things.
|
||||
//
|
||||
// However, we should be careful of changing this. It is not part of the public API but it seems like
|
||||
// [Tiramisu](https://hexdocs.pm/tiramisu/index.html) might makes use of it.
|
||||
impl_canvas.set_global(
|
||||
fn(encoded_picture, ctx) {
|
||||
let assert Ok(picture) = encode.from_string(encoded_picture)
|
||||
as "Invalid picture provided to web component"
|
||||
display_on_rendering_context(picture, ctx, default_drawing_state)
|
||||
},
|
||||
"display_on_rendering_context_with_default_drawing_state",
|
||||
)
|
||||
}
|
||||
|
||||
/// Utility to set the origin in the center of the canvas
|
||||
pub fn center(picture: Picture) -> fn(Config) -> Picture {
|
||||
fn(config) {
|
||||
let Config(width, height) = config
|
||||
picture |> translate_xy(width *. 0.5, height *. 0.5)
|
||||
}
|
||||
}
|
||||
260
build/packages/paint/src/paint/encode.gleam
Normal file
260
build/packages/paint/src/paint/encode.gleam
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
import gleam/dynamic/decode.{type Decoder}
|
||||
import gleam/json.{type Json}
|
||||
import gleam_community/colour
|
||||
import paint.{type Picture}
|
||||
import paint/internal/types.{
|
||||
type Angle, type FontProperties, type StrokeProperties, FontProperties,
|
||||
NoStroke, Radians, SolidStroke,
|
||||
}
|
||||
|
||||
/// Serialize a `Picture` to a string.
|
||||
///
|
||||
/// Note, serializing an `Image` texture will only store an ID referencing the image. This means that if you deserialize a Picture containing
|
||||
/// references to images, you are responsible for making sure all images are loaded before drawing the picture.
|
||||
/// More advanced APIs to support use cases such as these are planned for a future release.
|
||||
///
|
||||
/// Also, if you wish to store the serialized data, remember that the library currently makes no stability guarantee that
|
||||
/// the data can be deserialized by *future* versions of the library.
|
||||
pub fn to_string(picture: Picture) -> String {
|
||||
let version = "paint:unstable"
|
||||
json.object([
|
||||
#("version", json.string(version)),
|
||||
#("picture", picture_to_json(picture)),
|
||||
])
|
||||
|> json.to_string
|
||||
}
|
||||
|
||||
/// Attempt to deserialize a `Picture`
|
||||
pub fn from_string(string: String) {
|
||||
let decoder = {
|
||||
use picture <- decode.field("picture", decode_picture())
|
||||
decode.success(picture)
|
||||
}
|
||||
json.parse(string, decoder)
|
||||
}
|
||||
|
||||
fn decode_angle() {
|
||||
use radians <- decode.field("radians", decode.float)
|
||||
decode.success(Radians(radians))
|
||||
}
|
||||
|
||||
fn decode_picture() -> Decoder(Picture) {
|
||||
use <- decode.recursive
|
||||
use ty <- decode.field("type", decode.string)
|
||||
|
||||
case ty {
|
||||
"arc" -> {
|
||||
use radius <- decode.field("radius", decode.float)
|
||||
use start <- decode.field("start", decode_angle())
|
||||
use end <- decode.field("end", decode_angle())
|
||||
decode.success(types.Arc(radius, start:, end:))
|
||||
}
|
||||
"blank" -> decode.success(types.Blank)
|
||||
"combine" -> {
|
||||
use pictures <- decode.field(
|
||||
"pictures",
|
||||
decode.list(of: decode_picture()),
|
||||
)
|
||||
decode.success(types.Combine(pictures))
|
||||
}
|
||||
"fill" -> {
|
||||
use picture <- decode.field("picture", decode_picture())
|
||||
use colour <- decode.field("colour", colour.decoder())
|
||||
decode.success(types.Fill(picture, colour))
|
||||
}
|
||||
"polygon" -> {
|
||||
use points <- decode.field("points", decode.list(of: decode_vec2()))
|
||||
use closed <- decode.field("closed", decode.bool)
|
||||
decode.success(types.Polygon(points, closed))
|
||||
}
|
||||
"rotate" -> {
|
||||
use angle <- decode.field("angle", decode_angle())
|
||||
use picture <- decode.field("picture", decode_picture())
|
||||
decode.success(types.Rotate(picture, angle))
|
||||
}
|
||||
"scale" -> {
|
||||
use x <- decode.field("x", decode.float)
|
||||
use y <- decode.field("y", decode.float)
|
||||
use picture <- decode.field("picture", decode_picture())
|
||||
decode.success(types.Scale(picture, #(x, y)))
|
||||
}
|
||||
"stroke" -> {
|
||||
use stroke <- decode.field("stroke", decode_stroke())
|
||||
use picture <- decode.field("picture", decode_picture())
|
||||
decode.success(types.Stroke(picture, stroke))
|
||||
}
|
||||
"text" -> {
|
||||
use text <- decode.field("text", decode.string)
|
||||
use style <- decode.field("style", decode_font())
|
||||
decode.success(types.Text(text, style))
|
||||
}
|
||||
"translate" -> {
|
||||
use x <- decode.field("x", decode.float)
|
||||
use y <- decode.field("y", decode.float)
|
||||
use picture <- decode.field("picture", decode_picture())
|
||||
decode.success(types.Translate(picture, #(x, y)))
|
||||
}
|
||||
"image" -> {
|
||||
use id <- decode.field("id", decode.string)
|
||||
use width_px <- decode.field("width_px", decode.int)
|
||||
use height_px <- decode.field("height_px", decode.int)
|
||||
decode.success(types.ImageRef(types.Image(id:), width_px:, height_px:))
|
||||
}
|
||||
"image_scaling_behaviour" -> {
|
||||
use behaviour <- decode.field("behaviour", decode.string)
|
||||
use picture <- decode.field("picture", decode_picture())
|
||||
case behaviour {
|
||||
"smooth" ->
|
||||
decode.success(types.ImageScalingBehaviour(
|
||||
picture,
|
||||
types.ScalingSmooth,
|
||||
))
|
||||
"pixelated" ->
|
||||
decode.success(types.ImageScalingBehaviour(
|
||||
picture,
|
||||
types.ScalingPixelated,
|
||||
))
|
||||
_ -> decode.failure(types.Blank, "Picture")
|
||||
}
|
||||
}
|
||||
_ -> decode.failure(types.Blank, "Picture")
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_font() -> Decoder(FontProperties) {
|
||||
use size_px <- decode.field("sizePx", decode.int)
|
||||
use font_family <- decode.field("fontFamily", decode.string)
|
||||
decode.success(FontProperties(size_px:, font_family:))
|
||||
}
|
||||
|
||||
fn decode_stroke() -> Decoder(StrokeProperties) {
|
||||
use stroke_type <- decode.field("type", decode.string)
|
||||
case stroke_type {
|
||||
"noStroke" -> decode.success(NoStroke)
|
||||
"solidStroke" -> {
|
||||
use colour <- decode.field("colour", colour.decoder())
|
||||
use thickness <- decode.field("thickness", decode.float)
|
||||
decode.success(SolidStroke(colour, thickness))
|
||||
}
|
||||
_ -> decode.failure(NoStroke, "StrokeProperties")
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_vec2() -> Decoder(#(Float, Float)) {
|
||||
use x <- decode.field("x", decode.float)
|
||||
use y <- decode.field("y", decode.float)
|
||||
decode.success(#(x, y))
|
||||
}
|
||||
|
||||
fn picture_to_json(picture: Picture) -> Json {
|
||||
case picture {
|
||||
types.Arc(radius:, start:, end:) ->
|
||||
json.object([
|
||||
#("type", json.string("arc")),
|
||||
#("radius", json.float(radius)),
|
||||
#("start", angle_to_json(start)),
|
||||
#("end", angle_to_json(end)),
|
||||
])
|
||||
types.Blank -> json.object([#("type", json.string("blank"))])
|
||||
types.Combine(from) ->
|
||||
json.object([
|
||||
#("type", json.string("combine")),
|
||||
#("pictures", json.array(from:, of: picture_to_json)),
|
||||
])
|
||||
types.Fill(picture, colour) ->
|
||||
json.object([
|
||||
#("type", json.string("fill")),
|
||||
#("colour", colour.encode(colour)),
|
||||
#("picture", picture_to_json(picture)),
|
||||
])
|
||||
types.Polygon(points, closed:) ->
|
||||
json.object([
|
||||
#("type", json.string("polygon")),
|
||||
#(
|
||||
"points",
|
||||
json.array(from: points, of: fn(point) {
|
||||
let #(x, y) = point
|
||||
json.object([#("x", json.float(x)), #("y", json.float(y))])
|
||||
}),
|
||||
),
|
||||
#("closed", json.bool(closed)),
|
||||
])
|
||||
types.Rotate(picture, angle) ->
|
||||
json.object([
|
||||
#("type", json.string("rotate")),
|
||||
#("angle", angle_to_json(angle)),
|
||||
#("picture", picture_to_json(picture)),
|
||||
])
|
||||
types.Scale(picture, #(x, y)) ->
|
||||
json.object([
|
||||
#("type", json.string("scale")),
|
||||
#("x", json.float(x)),
|
||||
#("y", json.float(y)),
|
||||
#("picture", picture_to_json(picture)),
|
||||
])
|
||||
types.Stroke(picture, stroke) ->
|
||||
json.object([
|
||||
#("type", json.string("stroke")),
|
||||
#("stroke", stroke_to_json(stroke)),
|
||||
#("picture", picture_to_json(picture)),
|
||||
])
|
||||
types.Text(text:, style:) ->
|
||||
json.object([
|
||||
#("type", json.string("text")),
|
||||
#("text", json.string(text)),
|
||||
#("style", font_to_json(style)),
|
||||
])
|
||||
types.Translate(picture, #(x, y)) ->
|
||||
json.object([
|
||||
#("type", json.string("translate")),
|
||||
#("x", json.float(x)),
|
||||
#("y", json.float(y)),
|
||||
#("picture", picture_to_json(picture)),
|
||||
])
|
||||
types.ImageRef(types.Image(id:), width_px:, height_px:) -> {
|
||||
json.object([
|
||||
#("type", json.string("image")),
|
||||
#("id", json.string(id)),
|
||||
#("width_px", json.int(width_px)),
|
||||
#("height_px", json.int(height_px)),
|
||||
])
|
||||
}
|
||||
types.ImageScalingBehaviour(picture, behaviour) ->
|
||||
json.object([
|
||||
#("type", json.string("image_scaling_behaviour")),
|
||||
#(
|
||||
"behaviour",
|
||||
json.string(case behaviour {
|
||||
types.ScalingPixelated -> "pixelated"
|
||||
types.ScalingSmooth -> "smooth"
|
||||
}),
|
||||
),
|
||||
#("picture", picture_to_json(picture)),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
fn font_to_json(font: FontProperties) -> Json {
|
||||
let FontProperties(size_px:, font_family:) = font
|
||||
json.object([
|
||||
#("sizePx", json.int(size_px)),
|
||||
#("fontFamily", json.string(font_family)),
|
||||
])
|
||||
}
|
||||
|
||||
fn stroke_to_json(stroke: StrokeProperties) -> Json {
|
||||
case stroke {
|
||||
NoStroke -> json.object([#("type", json.string("noStroke"))])
|
||||
SolidStroke(colour, thickness) ->
|
||||
json.object([
|
||||
#("type", json.string("solidStroke")),
|
||||
#("colour", colour.encode(colour)),
|
||||
#("thickness", json.float(thickness)),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
fn angle_to_json(angle: Angle) -> Json {
|
||||
let Radians(rad) = angle
|
||||
json.object([#("radians", json.float(rad))])
|
||||
}
|
||||
50
build/packages/paint/src/paint/event.gleam
Normal file
50
build/packages/paint/src/paint/event.gleam
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
//// This module contains events that can be triggered when
|
||||
//// building interactive applications.
|
||||
////
|
||||
//// See `paint/canvas` and the `canvas.interact` function for a
|
||||
//// practical example of how this is used.
|
||||
|
||||
pub type Event {
|
||||
/// Triggered before drawing. Contains the number of milliseconds elapsed.
|
||||
Tick(Float)
|
||||
/// Triggered when a key is pressed
|
||||
KeyboardPressed(Key)
|
||||
/// Triggered when a key is released
|
||||
KeyboardRelased(Key)
|
||||
/// Triggered when the mouse is moved. Contains
|
||||
/// the `x` and `y` value for the mouse position.
|
||||
MouseMoved(Float, Float)
|
||||
/// Triggered when a mouse button is pressed
|
||||
MousePressed(MouseButton)
|
||||
/// Triggered when a mouse button is released.
|
||||
///
|
||||
/// Note, on the web you might encounter issues where the
|
||||
/// release event for the right mouse button is not triggered
|
||||
/// because of the context menu.
|
||||
MouseReleased(MouseButton)
|
||||
}
|
||||
|
||||
pub type Key {
|
||||
KeyLeftArrow
|
||||
KeyRightArrow
|
||||
KeyUpArrow
|
||||
KeyDownArrow
|
||||
KeySpace
|
||||
KeyW
|
||||
KeyA
|
||||
KeyS
|
||||
KeyD
|
||||
KeyZ
|
||||
KeyX
|
||||
KeyC
|
||||
KeyEnter
|
||||
KeyEscape
|
||||
KeyBackspace
|
||||
}
|
||||
|
||||
pub type MouseButton {
|
||||
MouseButtonLeft
|
||||
MouseButtonRight
|
||||
/// The scroll wheel button
|
||||
MouseButtonMiddle
|
||||
}
|
||||
118
build/packages/paint/src/paint/internal/impl_canvas.gleam
Normal file
118
build/packages/paint/src/paint/internal/impl_canvas.gleam
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
pub type RenderingContext2D
|
||||
|
||||
@external(javascript, "./../../impl_canvas_bindings.mjs", "define_web_component")
|
||||
pub fn define_web_component() -> Nil
|
||||
|
||||
// TODO: forward the timestamp from the callback
|
||||
@external(javascript, "./../../impl_canvas_bindings.mjs", "setup_request_animation_frame")
|
||||
pub fn setup_request_animation_frame(callback: fn(Float) -> Nil) -> Nil
|
||||
|
||||
@external(javascript, "./../../impl_canvas_bindings.mjs", "get_rendering_context")
|
||||
pub fn get_rendering_context(selector: String) -> RenderingContext2D
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "setup_input_handler")
|
||||
pub fn setup_input_handler(event: String, callback: fn(event) -> Nil) -> Nil
|
||||
|
||||
pub type KeyboardEvent
|
||||
|
||||
@external(javascript, "./../../impl_canvas_bindings.mjs", "get_key_code")
|
||||
pub fn get_key_code(event: KeyboardEvent) -> Int
|
||||
|
||||
pub type MouseEvent
|
||||
|
||||
@external(javascript, "./../../impl_canvas_bindings.mjs", "mouse_pos")
|
||||
pub fn mouse_pos(ctx: RenderingContext2D, event: MouseEvent) -> #(Float, Float)
|
||||
|
||||
@external(javascript, "./../../impl_canvas_bindings.mjs", "check_mouse_button")
|
||||
pub fn check_mouse_button(
|
||||
event: MouseEvent,
|
||||
previous_event: Result(MouseEvent, Nil),
|
||||
button_index: Int,
|
||||
check_pressed check_pressed: Bool,
|
||||
) -> Bool
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "get_width")
|
||||
pub fn get_width(ctx: RenderingContext2D) -> Float
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "get_height")
|
||||
pub fn get_height(ctx: RenderingContext2D) -> Float
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "set_global")
|
||||
pub fn set_global(state: state, id: String) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "get_global")
|
||||
pub fn get_global(id: String) -> Result(state, Nil)
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "reset")
|
||||
pub fn reset(ctx: RenderingContext2D) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "save")
|
||||
pub fn save(ctx: RenderingContext2D) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "restore")
|
||||
pub fn restore(ctx: RenderingContext2D) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "translate")
|
||||
pub fn translate(ctx: RenderingContext2D, x: Float, y: Float) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "scale")
|
||||
pub fn scale(ctx: RenderingContext2D, x: Float, y: Float) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "rotate")
|
||||
pub fn rotate(ctx: RenderingContext2D, radians: Float) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "reset_transform")
|
||||
pub fn reset_transform(ctx: RenderingContext2D) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "set_fill_colour")
|
||||
pub fn set_fill_colour(ctx: RenderingContext2D, css_colour: String) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "set_stroke_color")
|
||||
pub fn set_stroke_color(ctx: RenderingContext2D, css_color: String) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "set_line_width")
|
||||
pub fn set_line_width(ctx: RenderingContext2D, width: Float) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "set_image_smoothing_enabled")
|
||||
pub fn set_image_smoothing_enabled(ctx: RenderingContext2D, value: Bool) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "arc")
|
||||
pub fn arc(
|
||||
ctx: RenderingContext2D,
|
||||
radius: Float,
|
||||
start: Float,
|
||||
end: Float,
|
||||
fill: Bool,
|
||||
stroke: Bool,
|
||||
) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "polygon")
|
||||
pub fn polygon(
|
||||
ctx: RenderingContext2D,
|
||||
points: List(#(Float, Float)),
|
||||
closed: Bool,
|
||||
fill: Bool,
|
||||
stroke: Bool,
|
||||
) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "text")
|
||||
pub fn text(ctx: RenderingContext2D, text: String, style: String) -> Nil
|
||||
|
||||
pub type JsImage
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "draw_image")
|
||||
pub fn draw_image(
|
||||
ctx: RenderingContext2D,
|
||||
image: JsImage,
|
||||
width_px: Int,
|
||||
height_px: Int,
|
||||
) -> Nil
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "image_from_query")
|
||||
pub fn image_from_query(selector: String) -> JsImage
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "image_from_src")
|
||||
pub fn image_from_src(src: String) -> JsImage
|
||||
|
||||
@external(javascript, "../../impl_canvas_bindings.mjs", "on_image_load")
|
||||
pub fn on_image_load(image: JsImage, callback: fn() -> Nil) -> Nil
|
||||
48
build/packages/paint/src/paint/internal/types.gleam
Normal file
48
build/packages/paint/src/paint/internal/types.gleam
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import gleam_community/colour.{type Colour}
|
||||
|
||||
pub type Picture {
|
||||
// Shapes
|
||||
Blank
|
||||
Polygon(List(Vec2), closed: Bool)
|
||||
Arc(radius: Float, start: Angle, end: Angle)
|
||||
Text(text: String, style: FontProperties)
|
||||
ImageRef(Image, width_px: Int, height_px: Int)
|
||||
// Styling
|
||||
// TODO: font
|
||||
Fill(Picture, Colour)
|
||||
Stroke(Picture, StrokeProperties)
|
||||
ImageScalingBehaviour(Picture, ImageScalingBehaviour)
|
||||
// Transform
|
||||
Translate(Picture, Vec2)
|
||||
Scale(Picture, Vec2)
|
||||
Rotate(Picture, Angle)
|
||||
// Combine
|
||||
Combine(List(Picture))
|
||||
}
|
||||
|
||||
// The ID for an image
|
||||
// Invariant: the image object is assumed to already be created and stored somewhere (like the PAINT_STATE for the canvas backend)
|
||||
pub type Image {
|
||||
Image(id: String)
|
||||
}
|
||||
|
||||
pub type ImageScalingBehaviour {
|
||||
ScalingSmooth
|
||||
ScalingPixelated
|
||||
}
|
||||
|
||||
pub type StrokeProperties {
|
||||
NoStroke
|
||||
SolidStroke(Colour, Float)
|
||||
}
|
||||
|
||||
pub type FontProperties {
|
||||
FontProperties(size_px: Int, font_family: String)
|
||||
}
|
||||
|
||||
pub type Angle {
|
||||
Radians(Float)
|
||||
}
|
||||
|
||||
pub type Vec2 =
|
||||
#(Float, Float)
|
||||
Loading…
Add table
Add a link
Reference in a new issue