From 48706c35f9408bdac272608f2c6387c920a46cce Mon Sep 17 00:00:00 2001 From: Max Bradbury Date: Sat, 18 Jul 2020 21:47:04 +0100 Subject: [PATCH] initial --- .gitignore | 5 +++ Cargo.toml | 26 ++++++++++++++++ build.sh | 5 +++ index.pug | 37 ++++++++++++++++++++++ script.js | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ style.less | 60 +++++++++++++++++++++++++++++++++++ 7 files changed, 310 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100755 build.sh create mode 100644 index.pug create mode 100644 script.js create mode 100644 src/lib.rs create mode 100644 style.less diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c2df47 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +/index.html +/Cargo.lock +/style.css +/examples/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a599298 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "bitsy-tiles" +version = "0.1.0" +authors = ["Max Bradbury "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +"base64" = "^0.12.3" +"bitsy-parser" = "^0.71.1" +"image" = "^0.23.7" +"wasm-bindgen" = "^0.2.64" + +[dependencies.web-sys] +version = "^0.3.4" +features = [ + 'Document', + 'Element', + 'HtmlElement', + 'Node', + 'Window', +] diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..4b778c3 --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +#! /usr/bin/env bash + +pug index.pug +lessc style.less style.css +wasm-pack build --target web diff --git a/index.pug b/index.pug new file mode 100644 index 0000000..ed338d6 --- /dev/null +++ b/index.pug @@ -0,0 +1,37 @@ +doctype html +html(lang="en-gb") + head + meta(charset="utf-8") + title tiles to bitsy + link(rel="stylesheet" href="style.css") + body + h1 tiles to bitsy + p import 8x8 tile sheets into your Bitsy games + .pages + .page#start + button.normal.pagination.next#new create a new bitsy game + button.normal.pagination.next#load load an existing bitsy game + .page.game-data + h2 game data + input#game(type="file") + br + textarea#game-data(placeholder="Paste your game data here or use the file chooser button above") + button.pagination.prev previous + button.pagination.next#game-data-next next + .page.image + h2 image + .image-container + input#image(type="file") + img#preview + input#prefix(type="text" placeholder="tile name prefix, e.g. 'forest'") + button.pagination.prev previous + button.pagination.next#import next + .page.download + h2 download + textarea#output + br + button download + button.pagination.prev previous + button.pagination.start start again + script(type="module") + include script.js diff --git a/script.js b/script.js new file mode 100644 index 0000000..a6ca354 --- /dev/null +++ b/script.js @@ -0,0 +1,86 @@ +import init, {load_default_game_data, add_tiles} from './pkg/bitsy_tiles.js'; + +async function run() { + await init(); + + // hide all pages except start page + for (let page of document.getElementsByClassName('page')) { + page.style.display = "none"; + } + + document.getElementById("start").style.display = "block"; + + function pagination(e) { + const parent = e.target.parentNode; + + parent.style.display = "none"; + + if (e.target.classList.contains("next")) { + parent.nextSibling.style.display = "block"; + } else if (e.target.classList.contains("prev")) { + parent.previousSibling.style.display = "block"; + } else if (e.target.classList.contains("start")) { + document.getElementById("start").style.display = "block"; + } + } + + for (let pageButton of document.getElementsByClassName("pagination")) { + pageButton.addEventListener('click', pagination); + pageButton.addEventListener('touchend', pagination); + } + + function new_game() { + load_default_game_data(); + // we don't need to look at the default game data, so skip ahead to the image page + document.getElementById("game-data-next").click(); + } + + function clear_game() { + document.getElementById("game-data").innerHTML = ""; + } + + let new_game_button = document.getElementById("new"); + new_game_button.addEventListener("click", new_game); + new_game_button.addEventListener("touchend", new_game); + let load_game_button = document.getElementById("load"); + load_game_button.addEventListener("click", clear_game); + load_game_button.addEventListener("touchend", clear_game); + + // handle game data and image + + function readFile(input, callback, type = "text") { + if (input.files && input.files[0]) { + let reader = new FileReader(); + reader.onload = callback; + + if (type === "text") { + reader.readAsText(input.files[0]); + } else { + reader.readAsDataURL(input.files[0]); + } + } + } + + const preview = document.getElementById("preview"); + preview.style.display = "none"; + + document.getElementById('image').addEventListener('change', function (e) { + readFile(this, function (e) { + preview.src = e.target.result; + preview.style.display = "initial" + }, "image"); + }); + + function addTiles() { + let image = document.getElementById("preview").getAttribute("src"); + let gameData = document.getElementById("game-data").innerHTML; + let prefix = document.getElementById("prefix").value; + document.getElementById("output").innerHTML = add_tiles(gameData, image, prefix); + } + + const importButton = document.getElementById("import"); + importButton.addEventListener("click", addTiles); + importButton.addEventListener("touchend", addTiles); +} + +run(); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..590b94c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,91 @@ +use bitsy_parser::game::Game; +use bitsy_parser::image::Image; +use bitsy_parser::tile::Tile; +use image::{GenericImageView, Pixel}; +use wasm_bindgen::prelude::*; + +const SD: u32 = 8; + +#[wasm_bindgen] +pub fn load_default_game_data() { + let window = web_sys::window().expect("no global `window` exists"); + let document = window.document().expect("should have a document on window"); + let game_data_input = document.get_element_by_id("game-data").expect("no game data input"); + game_data_input.set_inner_html(&bitsy_parser::mock::game_default().to_string()); +} + +fn tile_name(prefix: &str, index: &u32) -> Option { + if prefix.len() > 0 { + Some(format!("{} {}", prefix, index)) + } else { + None + } +} + +/// image is a base64-encoded string +/// prefix will be ignored if empty +#[wasm_bindgen] +pub fn add_tiles(game_data: String, image: String, prefix: String) -> String { + let mut game = Game::from(game_data) + .expect("Couldn't parse game data"); + + let image: Vec<&str> = image.split("base64,").collect(); + let image = image[1]; + let image = base64::decode(image).unwrap(); + let image = image::load_from_memory(image.as_ref()) + .expect("Couldn't load image"); + + let width = image.width(); + let height = image.height(); + assert_eq!(width % 8, 0); + assert_eq!(height % 8, 0); + let columns = (width as f64 / 8 as f64).floor() as u32; + let rows = (height as f64 / 8 as f64).floor() as u32; + + // todo iterate over 8x8 tiles in image + + let mut tile_index = 1; + + for column in 0..columns { + for row in 0..rows { + let mut pixels = Vec::with_capacity(64); + + for x in (column * SD)..((column + 1) * SD) { + for y in (row * SD)..((row + 1) * SD) { + let pixel = image.get_pixel(x, y).to_rgb(); + let total: u32 = (pixel[0] as u32 + pixel[1] as u32 + pixel[2] as u32) as u32; + // is each channel brighter than 128/255 on average? + pixels.push(if total >= 384 {1} else {0}); + } + } + + game.add_tile(Tile { + /// "0" will get overwritten to a new, safe tile ID + id: "0".to_string(), + name: tile_name(&prefix, &tile_index), + wall: None, + animation_frames: vec![Image { pixels }], + colour_id: None + }); + + tile_index += 1; + } + } + + game.dedupe_tiles(); + game.to_string() +} + +#[cfg(test)] +mod test { + use crate::add_tiles; + + #[test] + fn example() { + let game_data = bitsy_parser::mock::game_default().to_string(); + let image = include_str!("test-resources/test.png.base64").to_string(); + let output = add_tiles(game_data, image, "".to_string()); + let expected = include_str!("test-resources/expected.bitsy"); + assert_eq!(output, expected); + } +} diff --git a/style.less b/style.less new file mode 100644 index 0000000..36e6a31 --- /dev/null +++ b/style.less @@ -0,0 +1,60 @@ +* { + box-sizing: border-box; + margin: 0 auto 0.5em auto; + text-align: center; +} + +html, body { + margin: 0; + padding: 0; + font-size: 3vmin; +} + +button { + padding: 1em; + white-space: nowrap; + width: 100%; + + &.pagination:not(.normal) { + position: absolute; + bottom: 5vmin; + width: auto; + + &.prev { + left: 5vmin; + } + + &.next, &.start { + right: 5vmin; + } + } +} + +input { + width: 100%; + text-align: left; +} + +img { + max-height: 12em; + max-width: 100%; +} + +textarea { + height: 20em; + padding: 0.5em; + text-align: left; + width: 100%; +} + +.image-container { + height: 38.5vmin; + text-align: left; +} + +.page { + width: 80vmin; + height: 80vmin; + padding: 5vmin; + position: relative; +}