This commit is contained in:
Max Bradbury 2020-07-18 21:47:04 +01:00
commit 48706c35f9
7 changed files with 310 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/target
/index.html
/Cargo.lock
/style.css
/examples/

26
Cargo.toml Normal file
View File

@ -0,0 +1,26 @@
[package]
name = "bitsy-tiles"
version = "0.1.0"
authors = ["Max Bradbury <max@tinybird.info>"]
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',
]

5
build.sh Executable file
View File

@ -0,0 +1,5 @@
#! /usr/bin/env bash
pug index.pug
lessc style.less style.css
wasm-pack build --target web

37
index.pug Normal file
View File

@ -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

86
script.js Normal file
View File

@ -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();

91
src/lib.rs Normal file
View File

@ -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<String> {
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);
}
}

60
style.less Normal file
View File

@ -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;
}