lots of stuff
This commit is contained in:
parent
0cb4f3af8d
commit
03588c4c55
|
@ -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...
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
# todo
|
||||
|
||||
* preview
|
||||
* dithering
|
||||
* palette selection
|
||||
* palette dropdown
|
||||
* unit test
|
||||
* if uploaded image is exactly 128×128, *don't* crop
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
41
index.pug
41
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
|
||||
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
|
||||
| tile name (optional)
|
||||
input#prefix(type="text" placeholder="e.g. 'forest'" autocomplete="off")
|
||||
| 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")
|
||||
|
|
56
script.js
56
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);
|
||||
|
|
102
src/lib.rs
102
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<DynamicImage>,
|
||||
room_name: Option<String>,
|
||||
palette: Option<String>,
|
||||
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<String>, index: &u32) -> Option<String> {
|
|||
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) => {
|
||||
let palette_id = game.palette_ids()[0].clone();
|
||||
state.game = Some(game);
|
||||
state.palette = Some(palette_id);
|
||||
"".to_string()
|
||||
},
|
||||
_ => {
|
||||
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<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)]
|
||||
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"));
|
||||
}
|
||||
|
|
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
|
@ -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
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 234 B |
|
@ -0,0 +1 @@
|
|||
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAQCAYAAACm53kpAAAAvElEQVR4nO2QQQrDMBAD6/8/upVSBMqClab2IWAP2M1qtgmovcFrgNYa7i96lWdO8nKi7oz6HkcBvWUo3FgKXo7PQpmTvJzy2XNiWgGEM/HM6fmaz54TpwLwiBvhjVnPhDPxzLny5Gpn1FceVcCoJ7/sOKcCKlC4sRS8O87EMyf55Ejy1dU58YgCerm46+ucOArA79/oI/U1ykXy1QntXHlSd9wluHX+52LsAnB2ATjLsgvA2QXgLMvyBXwAjqzoAQg4VfAAAAAASUVORK5CYII=
|
Loading…
Reference in New Issue