From 03588c4c553b5f3732a34acaa6166cd2356f0779 Mon Sep 17 00:00:00 2001 From: Max Bradbury Date: Thu, 5 Nov 2020 18:48:22 +0000 Subject: [PATCH] lots of stuff --- Cargo.toml | 2 + TODO.md | 8 ++ deploy.sh | 2 +- includes/style.less | 18 ++- index.pug | 43 +++++- script.js | 56 +++++++- src/lib.rs | 106 ++++++++++++-- src/test-resources/colour_input.png | Bin 0 -> 13253 bytes src/test-resources/colour_input.png.base64 | 1 + .../colour_input.png.base64.greyscale | 1 + src/test-resources/expected.bitsy | 136 ++++++++++++++++++ src/test-resources/test.png | Bin 0 -> 234 bytes src/test-resources/test.png.base64 | 1 + 13 files changed, 350 insertions(+), 24 deletions(-) create mode 100644 TODO.md create mode 100644 src/test-resources/colour_input.png create mode 100644 src/test-resources/colour_input.png.base64 create mode 100644 src/test-resources/colour_input.png.base64.greyscale create mode 100644 src/test-resources/expected.bitsy create mode 100644 src/test-resources/test.png create mode 100644 src/test-resources/test.png.base64 diff --git a/Cargo.toml b/Cargo.toml index 7881897..caf9afa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,8 @@ crate-type = ["cdylib"] [dependencies] "base64" = "^0.12.3" "bitsy-parser" = "^0.72.3" +"dither" = "1.3.9" "image" = "^0.23.7" +"json" = "^0.12.4" "lazy_static" = "^1.4.0" "wasm-bindgen" = "=0.2.64" # newer versions are bugged... diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..18682bc --- /dev/null +++ b/TODO.md @@ -0,0 +1,8 @@ +# todo + +* preview +* dithering +* palette selection +* palette dropdown +* unit test +* if uploaded image is exactly 128×128, *don't* crop diff --git a/deploy.sh b/deploy.sh index e38a15e..b21127a 100644 --- a/deploy.sh +++ b/deploy.sh @@ -4,4 +4,4 @@ rm -rf dist mkdir 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 diff --git a/includes/style.less b/includes/style.less index 748f62b..b55b4be 100644 --- a/includes/style.less +++ b/includes/style.less @@ -58,7 +58,6 @@ input { } img { - max-height: 12em; max-width: 100%; margin: 0; } @@ -73,6 +72,10 @@ label { font-weight: bold; } +select { + width: 100%; +} + textarea { height: 15em; padding: 0.5em; @@ -94,6 +97,12 @@ textarea { margin-top: 0; } +.half { + display: inline-block; + text-align: left; + width: 50%; +} + .image-container { height: 46vh; text-align: left; @@ -114,3 +123,10 @@ textarea { #crop canvas { height: 32vh; } + +#preview { + width: 256px; + height: 256px; + image-rendering: pixelated; + image-rendering: crisp-edges; +} diff --git a/index.pug b/index.pug index ce491fd..ca35914 100644 --- a/index.pug +++ b/index.pug @@ -24,34 +24,63 @@ html(lang="en-gb") button.normal.pagination.next#load load an existing bitsy game .page.game-data h2 game data + input#game(type="file" autocomplete="off") br + textarea#game-data( placeholder="Paste your game data here or use the file chooser button above" autocomplete="off" ) + button.pagination.prev previous button.pagination.next#game-data-next(disabled=true) next .page.image h2 image + .image-container input#image(type="file" accept="image/*") #crop + button.pagination.prev previous - button.pagination.next next - .page.extras - h2 tiles - label - | tile name (optional) - input#prefix(type="text" placeholder="e.g. 'forest'" autocomplete="off") + button.pagination.next#image-next(disabled=true) next + .page.room + h2 room + + table + 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.next#import next + button.pagination.next#room-next next .page.download h2 download + textarea#output(autocomplete="off") br + button#download download + button.pagination.prev#add add another image button.pagination.start#reset start again script(type="module") diff --git a/script.js b/script.js index fa75634..80eacc6 100644 --- a/script.js +++ b/script.js @@ -1,14 +1,17 @@ import init, { add_room, + get_palettes, + get_preview, load_image, load_game, load_default_game, output, + set_dither, set_room_name, } from './pkg/pixsy.js'; 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 @@ -61,10 +64,14 @@ async function run() { const buttonBackToImage = el("back-to-image"); const buttonDownload = el("download"); 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 buttonNewGame = el("new"); const buttonReset = el("reset"); + const checkboxDither = el("dither"); + const inputRoomName = el("room-name"); + const selectPalette = el("palette"); const textareaGameDataInput = el("game-data"); const textareaGameDataOutput = el("output"); @@ -110,9 +117,25 @@ async function run() { }, "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() { if (textareaGameDataInput.value.length > 0) { buttonGameDataProceed.removeAttribute("disabled"); + setPaletteDropdown(); } else { buttonGameDataProceed.setAttribute("disabled", "disabled"); } @@ -127,19 +150,40 @@ async function run() { if ( ! cropperRendered) { cropper.render("#crop"); cropperRendered = true; + buttonImageProceed.removeAttribute("disabled"); } cropper.loadImage(e.target.result); }, "image"); }); - function addTiles() { - console.log(add_tiles()); + function loadPreview() { + 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(); } - buttonImportGame.addEventListener("click", addTiles); - buttonImportGame.addEventListener("touchend", addTiles); + buttonRoomProceed.addEventListener("click", addRoom); + buttonRoomProceed.addEventListener("touchend", addRoom); function handleDownload() { download("output.bitsy", textareaGameDataOutput.value); diff --git a/src/lib.rs b/src/lib.rs index f56f6d7..168e29b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ use image::{GenericImageView, Pixel, DynamicImage}; use lazy_static::lazy_static; use std::sync::Mutex; use wasm_bindgen::prelude::*; +use image::imageops::dither; const SD: u32 = 8; @@ -13,6 +14,7 @@ struct State { image: Option, room_name: Option, palette: Option, + dither: bool, } lazy_static! { @@ -22,6 +24,7 @@ lazy_static! { image: None, room_name: None, palette: None, + dither: true, } ); } @@ -38,6 +41,7 @@ fn tile_name(prefix: &Option, index: &u32) -> Option { pub fn load_default_game() { let mut state = STATE.lock().unwrap(); state.game = Some(bitsy_parser::mock::game_default()); + state.palette = Some(bitsy_parser::mock::game_default().palette_ids()[0].clone()) } #[wasm_bindgen] @@ -47,11 +51,14 @@ pub fn load_game(game_data: String) -> String { match result { 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() }, _ => { - state.game = None; + state.game = None; + state.palette = None; format!("{}", result.err().unwrap()) } } @@ -84,6 +91,22 @@ pub fn load_image(image_base64: String) -> 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] pub fn set_room_name(room_name: String) { let mut state = STATE.lock().unwrap(); @@ -95,12 +118,52 @@ pub fn set_room_name(room_name: String) { } #[wasm_bindgen] -pub fn set_palette(palette_id: String) { - let mut state = STATE.lock().unwrap(); +pub fn get_palettes() -> String { + let state = STATE.lock().unwrap(); - match palette_id.is_empty() { - true => { state.palette = None }, - false => { state.palette = Some(palette_id) }, + let mut palette_objects = json::JsonValue::new_array(); + + 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 = 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)] 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] fn example() { load_default_game(); load_image(include_str!("test-resources/test.png.base64").to_string()); - add_tiles(); + add_room(); assert_eq!(output(), include_str!("test-resources/expected.bitsy")); } diff --git a/src/test-resources/colour_input.png b/src/test-resources/colour_input.png new file mode 100644 index 0000000000000000000000000000000000000000..227c1a36f70ea0e550c2398b6a9704b321c58f1a GIT binary patch literal 13253 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 literal 0 HcmV?d00001 diff --git a/src/test-resources/test.png.base64 b/src/test-resources/test.png.base64 new file mode 100644 index 0000000..4c542ca --- /dev/null +++ b/src/test-resources/test.png.base64 @@ -0,0 +1 @@ +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAQCAYAAACm53kpAAAAvElEQVR4nO2QQQrDMBAD6/8/upVSBMqClab2IWAP2M1qtgmovcFrgNYa7i96lWdO8nKi7oz6HkcBvWUo3FgKXo7PQpmTvJzy2XNiWgGEM/HM6fmaz54TpwLwiBvhjVnPhDPxzLny5Gpn1FceVcCoJ7/sOKcCKlC4sRS8O87EMyf55Ejy1dU58YgCerm46+ucOArA79/oI/U1ykXy1QntXHlSd9wluHX+52LsAnB2ATjLsgvA2QXgLMvyBXwAjqzoAQg4VfAAAAAASUVORK5CYII=