initial
This commit is contained in:
commit
48706c35f9
|
@ -0,0 +1,5 @@
|
||||||
|
/target
|
||||||
|
/index.html
|
||||||
|
/Cargo.lock
|
||||||
|
/style.css
|
||||||
|
/examples/
|
|
@ -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',
|
||||||
|
]
|
|
@ -0,0 +1,5 @@
|
||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
pug index.pug
|
||||||
|
lessc style.less style.css
|
||||||
|
wasm-pack build --target web
|
|
@ -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
|
|
@ -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();
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue