lots of stuff

This commit is contained in:
Max Bradbury 2020-11-05 18:48:22 +00:00
parent 0cb4f3af8d
commit 03588c4c55
13 changed files with 350 additions and 24 deletions

View File

@ -13,6 +13,8 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
"base64" = "^0.12.3" "base64" = "^0.12.3"
"bitsy-parser" = "^0.72.3" "bitsy-parser" = "^0.72.3"
"dither" = "1.3.9"
"image" = "^0.23.7" "image" = "^0.23.7"
"json" = "^0.12.4"
"lazy_static" = "^1.4.0" "lazy_static" = "^1.4.0"
"wasm-bindgen" = "=0.2.64" # newer versions are bugged... "wasm-bindgen" = "=0.2.64" # newer versions are bugged...

8
TODO.md Normal file
View File

@ -0,0 +1,8 @@
# todo
* preview
* dithering
* palette selection
* palette dropdown
* unit test
* if uploaded image is exactly 128×128, *don't* crop

View File

@ -4,4 +4,4 @@
rm -rf dist rm -rf dist
mkdir dist mkdir dist
cp -r README.md LICENSE index.html script.js background.png pkg includes dist cp -r README.md LICENSE index.html script.js background.png pkg includes dist
butler push dist ruin/image-to-bitsy:html # butler push dist ruin/pixsy:html

View File

@ -58,7 +58,6 @@ input {
} }
img { img {
max-height: 12em;
max-width: 100%; max-width: 100%;
margin: 0; margin: 0;
} }
@ -73,6 +72,10 @@ label {
font-weight: bold; font-weight: bold;
} }
select {
width: 100%;
}
textarea { textarea {
height: 15em; height: 15em;
padding: 0.5em; padding: 0.5em;
@ -94,6 +97,12 @@ textarea {
margin-top: 0; margin-top: 0;
} }
.half {
display: inline-block;
text-align: left;
width: 50%;
}
.image-container { .image-container {
height: 46vh; height: 46vh;
text-align: left; text-align: left;
@ -114,3 +123,10 @@ textarea {
#crop canvas { #crop canvas {
height: 32vh; height: 32vh;
} }
#preview {
width: 256px;
height: 256px;
image-rendering: pixelated;
image-rendering: crisp-edges;
}

View File

@ -24,34 +24,63 @@ html(lang="en-gb")
button.normal.pagination.next#load load an existing bitsy game button.normal.pagination.next#load load an existing bitsy game
.page.game-data .page.game-data
h2 game data h2 game data
input#game(type="file" autocomplete="off") input#game(type="file" autocomplete="off")
br br
textarea#game-data( textarea#game-data(
placeholder="Paste your game data here or use the file chooser button above" placeholder="Paste your game data here or use the file chooser button above"
autocomplete="off" autocomplete="off"
) )
button.pagination.prev previous button.pagination.prev previous
button.pagination.next#game-data-next(disabled=true) next button.pagination.next#game-data-next(disabled=true) next
.page.image .page.image
h2 image h2 image
.image-container .image-container
input#image(type="file" accept="image/*") input#image(type="file" accept="image/*")
#crop #crop
button.pagination.prev previous button.pagination.prev previous
button.pagination.next next button.pagination.next#image-next(disabled=true) next
.page.extras .page.room
h2 tiles h2 room
label
| tile name (optional) table
input#prefix(type="text" placeholder="e.g. 'forest'" autocomplete="off") tbody
tr
td(style="width: 60%")
img#preview(alt="preview")
br
label
| brightness
input#brightness(type="range" min=-64 max=64 value=0)
td
label
| palette
select#palette
label
input#dither(type="checkbox")
| dither
br
label
| name (optional)
input#room-name(type="text" placeholder="e.g. 'bedroom'" autocomplete="off")
button.pagination.prev#back-to-image previous button.pagination.prev#back-to-image previous
button.pagination.next#import next button.pagination.next#room-next next
.page.download .page.download
h2 download h2 download
textarea#output(autocomplete="off") textarea#output(autocomplete="off")
br br
button#download download button#download download
button.pagination.prev#add add another image button.pagination.prev#add add another image
button.pagination.start#reset start again button.pagination.start#reset start again
script(type="module") script(type="module")

View File

@ -1,14 +1,17 @@
import init, { import init, {
add_room, add_room,
get_palettes,
get_preview,
load_image, load_image,
load_game, load_game,
load_default_game, load_default_game,
output, output,
set_dither,
set_room_name, set_room_name,
} from './pkg/pixsy.js'; } from './pkg/pixsy.js';
if (typeof WebAssembly !== "object") { if (typeof WebAssembly !== "object") {
document.getElementById("start").innerText = "Sorry - your browser does not support WebAssembly"; window.location = "./old/"
} }
// stolen from https://ourcodeworld.com/articles/read/189/how-to-create-a-file-and-generate-a-download-with-javascript-in-the-browser-without-a-server // stolen from https://ourcodeworld.com/articles/read/189/how-to-create-a-file-and-generate-a-download-with-javascript-in-the-browser-without-a-server
@ -61,10 +64,14 @@ async function run() {
const buttonBackToImage = el("back-to-image"); const buttonBackToImage = el("back-to-image");
const buttonDownload = el("download"); const buttonDownload = el("download");
const buttonGameDataProceed = el("game-data-next"); const buttonGameDataProceed = el("game-data-next");
const buttonImportGame = el("import"); const buttonImageProceed = el("image-next");
const buttonRoomProceed = el("room-next");
const buttonLoadGame = el("load"); const buttonLoadGame = el("load");
const buttonNewGame = el("new"); const buttonNewGame = el("new");
const buttonReset = el("reset"); const buttonReset = el("reset");
const checkboxDither = el("dither");
const inputRoomName = el("room-name");
const selectPalette = el("palette");
const textareaGameDataInput = el("game-data"); const textareaGameDataInput = el("game-data");
const textareaGameDataOutput = el("output"); const textareaGameDataOutput = el("output");
@ -110,9 +117,25 @@ async function run() {
}, "text"); }, "text");
}); });
function setPaletteDropdown() {
let palettes = JSON.parse(get_palettes());
selectPalette.innerHTML = "";
for (let palette of palettes) {
let option = document.createElement("option");
option.value = palette.id;
option.innerText = palette.name;
selectPalette.appendChild(option);
}
}
function checkGameData() { function checkGameData() {
if (textareaGameDataInput.value.length > 0) { if (textareaGameDataInput.value.length > 0) {
buttonGameDataProceed.removeAttribute("disabled"); buttonGameDataProceed.removeAttribute("disabled");
setPaletteDropdown();
} else { } else {
buttonGameDataProceed.setAttribute("disabled", "disabled"); buttonGameDataProceed.setAttribute("disabled", "disabled");
} }
@ -127,19 +150,40 @@ async function run() {
if ( ! cropperRendered) { if ( ! cropperRendered) {
cropper.render("#crop"); cropper.render("#crop");
cropperRendered = true; cropperRendered = true;
buttonImageProceed.removeAttribute("disabled");
} }
cropper.loadImage(e.target.result); cropper.loadImage(e.target.result);
}, "image"); }, "image");
}); });
function addTiles() { function loadPreview() {
console.log(add_tiles()); el("preview").setAttribute("src", get_preview());
}
function handleImage() {
console.log(load_image(cropper.getCroppedImage()));
loadPreview();
}
buttonImageProceed.addEventListener("click", handleImage);
buttonImageProceed.addEventListener("touchend", handleImage);
checkboxDither.addEventListener("change", () => {
set_dither(checkboxDither.checked);
});
inputRoomName.addEventListener("change", () => {
set_room_name(inputRoomName.value);
});
function addRoom() {
console.log(add_room());
textareaGameDataOutput.value = output(); textareaGameDataOutput.value = output();
} }
buttonImportGame.addEventListener("click", addTiles); buttonRoomProceed.addEventListener("click", addRoom);
buttonImportGame.addEventListener("touchend", addTiles); buttonRoomProceed.addEventListener("touchend", addRoom);
function handleDownload() { function handleDownload() {
download("output.bitsy", textareaGameDataOutput.value); download("output.bitsy", textareaGameDataOutput.value);

View File

@ -5,6 +5,7 @@ use image::{GenericImageView, Pixel, DynamicImage};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::sync::Mutex; use std::sync::Mutex;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use image::imageops::dither;
const SD: u32 = 8; const SD: u32 = 8;
@ -13,6 +14,7 @@ struct State {
image: Option<DynamicImage>, image: Option<DynamicImage>,
room_name: Option<String>, room_name: Option<String>,
palette: Option<String>, palette: Option<String>,
dither: bool,
} }
lazy_static! { lazy_static! {
@ -22,6 +24,7 @@ lazy_static! {
image: None, image: None,
room_name: None, room_name: None,
palette: None, palette: None,
dither: true,
} }
); );
} }
@ -38,6 +41,7 @@ fn tile_name(prefix: &Option<String>, index: &u32) -> Option<String> {
pub fn load_default_game() { pub fn load_default_game() {
let mut state = STATE.lock().unwrap(); let mut state = STATE.lock().unwrap();
state.game = Some(bitsy_parser::mock::game_default()); state.game = Some(bitsy_parser::mock::game_default());
state.palette = Some(bitsy_parser::mock::game_default().palette_ids()[0].clone())
} }
#[wasm_bindgen] #[wasm_bindgen]
@ -47,11 +51,14 @@ pub fn load_game(game_data: String) -> String {
match result { match result {
Ok(game) => { Ok(game) => {
state.game = Some(game); let palette_id = game.palette_ids()[0].clone();
state.game = Some(game);
state.palette = Some(palette_id);
"".to_string() "".to_string()
}, },
_ => { _ => {
state.game = None; state.game = None;
state.palette = None;
format!("{}", result.err().unwrap()) format!("{}", result.err().unwrap())
} }
} }
@ -84,6 +91,22 @@ pub fn load_image(image_base64: String) -> String {
}.to_string() }.to_string()
} }
#[wasm_bindgen]
pub fn set_dither(dither: bool) {
let mut state = STATE.lock().unwrap();
state.dither = dither;
}
#[wasm_bindgen]
pub fn set_palette(palette_id: String) {
let mut state = STATE.lock().unwrap();
match palette_id.is_empty() {
true => { state.palette = None },
false => { state.palette = Some(palette_id) },
}
}
#[wasm_bindgen] #[wasm_bindgen]
pub fn set_room_name(room_name: String) { pub fn set_room_name(room_name: String) {
let mut state = STATE.lock().unwrap(); let mut state = STATE.lock().unwrap();
@ -95,12 +118,52 @@ pub fn set_room_name(room_name: String) {
} }
#[wasm_bindgen] #[wasm_bindgen]
pub fn set_palette(palette_id: String) { pub fn get_palettes() -> String {
let mut state = STATE.lock().unwrap(); let state = STATE.lock().unwrap();
match palette_id.is_empty() { let mut palette_objects = json::JsonValue::new_array();
true => { state.palette = None },
false => { state.palette = Some(palette_id) }, for palette in &state.game.as_ref().unwrap().palettes {
let mut object = json::JsonValue::new_object();
object.insert("id", palette.id.clone()).unwrap();
object.insert(
"name",
palette.name.clone().unwrap_or(
format!("Palette {}", palette.id))
).unwrap();
palette_objects.push(object).unwrap();
}
json::stringify(palette_objects)
}
fn image_to_base64(image: &DynamicImage) -> String {
let mut bytes: Vec<u8> = Vec::new();
image.write_to(&mut bytes, image::ImageOutputFormat::Png).unwrap();
format!("data:image/png;base64,{}", base64::encode(&bytes))
}
fn render_preview(image: &DynamicImage) -> DynamicImage {
let image = image.clone();
let image = image.grayscale();
// todo dither
// todo convert to palette colours
image
}
#[wasm_bindgen]
pub fn get_preview() -> String {
let state = STATE.lock().unwrap();
match &state.image.is_some() {
true => image_to_base64(&render_preview(state.image.as_ref().unwrap())),
false => "".to_string(),
} }
} }
@ -174,13 +237,38 @@ pub fn output() -> String {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{add_tiles, load_image, load_default_game, output}; use crate::{add_room, load_image, load_default_game, output, get_preview};
#[test]
fn image_to_base64() {
let image = image::load_from_memory(include_bytes!("test-resources/test.png")).unwrap();
let output = crate::image_to_base64(&image);
let expected = include_str!("test-resources/test.png.base64").trim();
assert_eq!(output, expected);
}
#[test]
fn get_palettes() {
load_default_game();
let output = crate::get_palettes();
let expected = "[{\"id\":\"0\",\"name\":\"blueprint\"}]";
assert_eq!(output, expected);
}
#[test]
fn render_preview() {
load_default_game();
load_image(include_str!("test-resources/colour_input.png.base64").trim().to_string());
let output = get_preview();
let expected = include_str!("test-resources/colour_input.png.base64.greyscale").trim();
assert_eq!(output, expected);
}
#[test] #[test]
fn example() { fn example() {
load_default_game(); load_default_game();
load_image(include_str!("test-resources/test.png.base64").to_string()); load_image(include_str!("test-resources/test.png.base64").to_string());
add_tiles(); add_room();
assert_eq!(output(), include_str!("test-resources/expected.bitsy")); assert_eq!(output(), include_str!("test-resources/expected.bitsy"));
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,136 @@
Write your game's title here
# BITSY VERSION 7.2
! ROOM_FORMAT 1
PAL 0
NAME blueprint
0,82,204
128,159,255
255,255,255
ROOM 0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,a,a,a,a,a,a,a,a,a,a,a,a,a,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,a,a,a,a,a,a,a,a,a,a,a,a,a,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
NAME example room
PAL 0
TIL a
11111111
10000001
10000001
10011001
10011001
10000001
10000001
11111111
NAME block
TIL 2
00011000
00111000
00011000
00011000
00011000
00011000
00011000
00111100
TIL 3
00111100
01100110
01100110
00001100
00011000
00110000
01100000
01111110
TIL 4
00111100
01100110
01100110
00001100
00001100
01100110
01100110
00111100
SPR A
00011000
00011000
00011000
00111100
01111110
10111101
00100100
00100100
POS 0 4,4
SPR a
00000000
00000000
01010001
01110001
01110010
01111100
00111100
00100100
NAME cat
DLG 0
POS 0 8,12
ITM 0
00000000
00000000
00000000
00111100
01100100
00100100
00011000
00000000
NAME tea
DLG 1
ITM 1
00000000
00111100
00100100
00111100
00010000
00011000
00010000
00011000
NAME key
DLG 2
DLG 0
I'm a cat
NAME cat dialog
DLG 1
You found a nice warm cup of tea
NAME tea dialog
DLG 2
A key! {wvy}What does it open?{wvy}
NAME key dialog
VAR a
42

BIN
src/test-resources/test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

View File

@ -0,0 +1 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAQCAYAAACm53kpAAAAvElEQVR4nO2QQQrDMBAD6/8/upVSBMqClab2IWAP2M1qtgmovcFrgNYa7i96lWdO8nKi7oz6HkcBvWUo3FgKXo7PQpmTvJzy2XNiWgGEM/HM6fmaz54TpwLwiBvhjVnPhDPxzLny5Gpn1FceVcCoJ7/sOKcCKlC4sRS8O87EMyf55Ejy1dU58YgCerm46+ucOArA79/oI/U1ykXy1QntXHlSd9wluHX+52LsAnB2ATjLsgvA2QXgLMvyBXwAjqzoAQg4VfAAAAAASUVORK5CYII=