38 Commits

Author SHA1 Message Date
0324f5f389 wip wtf 2020-11-15 16:54:25 +00:00
47a9bf5c79 explain 2020-11-09 17:48:26 +00:00
d100473542 try to check for webassembly in a different place 2020-11-09 11:55:11 +00:00
d0946d3ab5 readme; remove todo as it's now in the readme and on gitea 2020-11-09 11:54:45 +00:00
5ee1e908f1 link won't open in same iframe 2020-11-08 21:29:21 +00:00
5788baa0b8 new host for old version 2020-11-08 21:27:05 +00:00
f135e184e4 Merge branch 'Refactor3' 2020-11-08 21:19:10 +00:00
3e7d6eeaa5 include old version 2020-11-08 21:18:51 +00:00
f6308110be try again 2020-11-08 20:53:52 +00:00
2d73963aa0 black and white palette 2020-11-08 20:52:29 +00:00
6f8e00130c update deploy script 2020-11-08 20:50:55 +00:00
b478b1e3ee update deploy script 2020-11-08 20:50:42 +00:00
055928eb7b todo 2020-11-08 20:48:04 +00:00
b3690c4dd7 black and white palette 2020-11-08 20:46:58 +00:00
da04534fd9 fix crop shit 2020-11-08 20:45:10 +00:00
3d1129c613 log 2020-11-08 20:07:48 +00:00
3b851975d0 room stats 2020-11-08 20:07:42 +00:00
afc626bae0 undo skipping crop on 128² 2020-11-08 20:07:26 +00:00
6e43249d64 done 2020-11-08 20:06:08 +00:00
458604cd1a display number of tiles added 2020-11-08 17:36:24 +00:00
bb0b970281 bump 2020-11-08 17:36:11 +00:00
f62202cb74 style header differently 2020-11-08 17:36:00 +00:00
745b18dfc0 styling tweaks 2020-11-08 17:35:28 +00:00
e5a87f854e this would break things 2020-11-08 17:35:08 +00:00
c2787db422 better load_image errors 2020-11-08 17:34:48 +00:00
7d274bb3c2 this function doesn't return anything 2020-11-08 17:33:39 +00:00
fbe40fb866 dedupe palette function 2020-11-08 17:33:15 +00:00
7ff67e51f8 x,y instead of y,x 2020-11-08 16:59:04 +00:00
80df4a9a6a better tile names 2020-11-08 15:37:38 +00:00
e76ff57053 allow auto pagination from image to room 2020-11-08 15:32:18 +00:00
0ef2d2acd9 return image size so the client side can determine whether to crop or not 2020-11-08 15:31:57 +00:00
f9b0f6b6db name room and give tiles appropriate names too 2020-11-08 15:30:39 +00:00
8c92b05b74 generate the dang room 2020-11-08 14:54:49 +00:00
512f386c25 first attempt at creating room 2020-11-08 12:30:46 +00:00
63cb971aca whitespace 2020-11-08 11:23:44 +00:00
03ce88e015 descriptive button 2020-11-08 11:23:37 +00:00
8a8b861e5d todo 2020-11-08 11:23:17 +00:00
21eb632d22 add old version 2020-11-07 19:30:20 +00:00
15 changed files with 546 additions and 228 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "pixsy"
version = "0.72.5"
version = "0.72.7"
description = "convert images to Bitsy rooms"
authors = ["Max Bradbury <max@tinybird.info>"]
edition = "2018"
@@ -13,6 +13,7 @@ crate-type = ["cdylib"]
[dependencies]
base64 = "^0.12.3"
bitsy-parser = "^0.72.5"
dither = "^1.3.9"
image = "^0.23.7"
json = "^0.12.4"
lazy_static = "^1.4.0"

View File

@@ -1,3 +1,57 @@
# pixsy
convert images to rooms for use in Bitsy
a tool for [Bitsy Game Maker](http://bitsy.org).
upload any image and convert it into a room.
## credits
made by [Max Bradbury](http://tinybird.info/).
makes use of my own [bitsy parser](https://crates.io/crates/bitsy-parser) library.
uses the [Croppie](https://foliotek.github.io/Croppie/) image crop plugin
by [Foliotek](https://www.foliotek.com/)
uses [wasm-bindgen](https://crates.io/crates/wasm-bindgen) to automate WebAssembly bindings.
## thanks
to [Adam Le Doux](http://ledoux.io/) for creating the wonderful and inspiring Bitsy
to [Mark Wonnacott](https://kool.tools/) for their support, encouragement and inspiration
and to everyone in the bitsy community!
## contributing
forks and pull requests welcome!
### development prerequisites
* [rust/cargo](https://rustup.rs/)
* [pug](https://pugjs.org/)
* [less](http://lesscss.org/)
* a bash shell for the build script
## bugs
when importing images, some pixels have errors.
it seems to only happen for pixels surrounded at the top and left:
```
111 111
100 => 110
100 100
```
pixsy does not work in the Itch desktop program
because their bundled version of Chromium does not support WebAssembly.
## to do
* add alternative dithering options (Atkinson, Bayer 8×8)
* add a 'smoothing' (noise reduction?) stage to remove errant pixels
## could do
* reimplement tile reuse option
* add camera support so users can take a pic instead of uploading an image
* allow user to draw to canvas

View File

@@ -1,8 +0,0 @@
# todo
* tests
* if image is exactly 128×128, *don't* crop
* tile reuse
* implement Atkinson and Bayer dithering options
* if "create new bitsy game", add new room as room 0
* actually create room

View File

@@ -1,7 +1,6 @@
#! /usr/bin/env bash
./build.sh
rm -rf dist
mkdir dist
cp -r README.md LICENSE index.html script.js background.png pkg includes dist
# butler push dist ruin/pixsy:html
cp -r README.md LICENSE index.html script.js pkg includes old dist
butler push dist ruin/pixsy:html

View File

@@ -1,122 +0,0 @@
.slider[type='range'] {
-webkit-appearance: none;
margin: 9px 0;
width: 100%
}
.slider[type='range']:focus {
outline: 0
}
.slider[type='range']::-webkit-slider-runnable-track {
cursor: pointer;
height: 5px;
width: 100%;
background: #008ecc;
border: none
}
.slider[type='range']::-webkit-slider-thumb {
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 50%;
box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.12);
cursor: pointer;
height: 18px;
width: 18px;
-webkit-appearance: none;
margin-top: -6.5px
}
.slider[type='range']::-moz-range-track {
cursor: pointer;
height: 5px;
width: 100%;
background: #008ecc;
border: none
}
.slider[type='range']::-moz-range-thumb {
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 50%;
box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.12);
cursor: pointer;
height: 18px;
width: 18px
}
.slider[type='range']::-ms-track {
cursor: pointer;
height: 5px;
width: 100%;
background: transparent;
border-color: transparent;
border-width: 9px 0;
color: transparent
}
.slider[type='range']::-ms-fill-lower {
background: #008ecc;
border: none
}
.slider[type='range']::-ms-fill-upper {
background: #008ecc;
border: none
}
.slider[type='range']::-ms-thumb {
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 50%;
box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.12);
cursor: pointer;
height: 18px;
width: 18px;
margin-top: 0
}
.cropper {
display: inline-block
}
.cropper canvas {
border-radius: 3px
}
.cropper canvas:hover {
cursor: move
}
.cropper-tools {
margin-top: 15px;
text-align: center
}
.cropper-zoom {
display: inline-block
}
.cropper-zoom .slider {
margin: 0 10px;
width: 225px
}
.cropper-zoom .icon {
margin-top: 2px;
font-size: 18px
}
.cropper-zoom .icon:last-child {
font-size: 24px
}
.cropper .icon {
display: inline-block;
width: 1em;
height: 1em;
fill: rgba(0, 0, 0, .54);
vertical-align: middle;
}

File diff suppressed because one or more lines are too long

250
includes/croppie.css Normal file
View File

@@ -0,0 +1,250 @@
.croppie-container {
width: 100%;
height: 100%;
}
.croppie-container .cr-image {
z-index: -1;
position: absolute;
top: 0;
left: 0;
transform-origin: 0 0;
max-height: none;
max-width: none;
}
.croppie-container .cr-boundary {
position: relative;
overflow: hidden;
margin: 0 auto;
z-index: 1;
width: 100%;
height: 100%;
}
.croppie-container .cr-viewport,
.croppie-container .cr-resizer {
position: absolute;
border: 2px solid #fff;
margin: auto;
top: 0;
bottom: 0;
right: 0;
left: 0;
box-shadow: 0 0 2000px 2000px rgba(0, 0, 0, 0.5);
z-index: 0;
}
.croppie-container .cr-resizer {
z-index: 2;
box-shadow: none;
pointer-events: none;
}
.croppie-container .cr-resizer-vertical,
.croppie-container .cr-resizer-horisontal {
position: absolute;
pointer-events: all;
}
.croppie-container .cr-resizer-vertical::after,
.croppie-container .cr-resizer-horisontal::after {
display: block;
position: absolute;
box-sizing: border-box;
border: 1px solid black;
background: #fff;
width: 10px;
height: 10px;
content: '';
}
.croppie-container .cr-resizer-vertical {
bottom: -5px;
cursor: row-resize;
width: 100%;
height: 10px;
}
.croppie-container .cr-resizer-vertical::after {
left: 50%;
margin-left: -5px;
}
.croppie-container .cr-resizer-horisontal {
right: -5px;
cursor: col-resize;
width: 10px;
height: 100%;
}
.croppie-container .cr-resizer-horisontal::after {
top: 50%;
margin-top: -5px;
}
.croppie-container .cr-original-image {
display: none;
}
.croppie-container .cr-vp-circle {
border-radius: 50%;
}
.croppie-container .cr-overlay {
z-index: 1;
position: absolute;
cursor: move;
touch-action: none;
}
.croppie-container .cr-slider-wrap {
width: 75%;
margin: 15px auto;
text-align: center;
}
.croppie-result {
position: relative;
overflow: hidden;
}
.croppie-result img {
position: absolute;
}
.croppie-container .cr-image,
.croppie-container .cr-overlay,
.croppie-container .cr-viewport {
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
/*************************************/
/***** STYLING RANGE INPUT ***********/
/*************************************/
/*http://brennaobrien.com/blog/2014/05/style-input-type-range-in-every-browser.html */
/*************************************/
.cr-slider {
-webkit-appearance: none;
/*removes default webkit styles*/
/*border: 1px solid white; *//*fix for FF unable to apply focus style bug */
width: 300px;
/*required for proper track sizing in FF*/
max-width: 100%;
padding-top: 8px;
padding-bottom: 8px;
background-color: transparent;
}
.cr-slider::-webkit-slider-runnable-track {
width: 100%;
height: 3px;
background: rgba(0, 0, 0, 0.5);
border: 0;
border-radius: 3px;
}
.cr-slider::-webkit-slider-thumb {
-webkit-appearance: none;
border: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: #ddd;
margin-top: -6px;
}
.cr-slider:focus {
outline: none;
}
/*
.cr-slider:focus::-webkit-slider-runnable-track {
background: #ccc;
}
*/
.cr-slider::-moz-range-track {
width: 100%;
height: 3px;
background: rgba(0, 0, 0, 0.5);
border: 0;
border-radius: 3px;
}
.cr-slider::-moz-range-thumb {
border: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: #ddd;
margin-top: -6px;
}
/*hide the outline behind the border*/
.cr-slider:-moz-focusring {
outline: 1px solid white;
outline-offset: -1px;
}
.cr-slider::-ms-track {
width: 100%;
height: 5px;
background: transparent;
/*remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead */
border-color: transparent;/*leave room for the larger thumb to overflow with a transparent border */
border-width: 6px 0;
color: transparent;/*remove default tick marks*/
}
.cr-slider::-ms-fill-lower {
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
}
.cr-slider::-ms-fill-upper {
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
}
.cr-slider::-ms-thumb {
border: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: #ddd;
margin-top:1px;
}
.cr-slider:focus::-ms-fill-lower {
background: rgba(0, 0, 0, 0.5);
}
.cr-slider:focus::-ms-fill-upper {
background: rgba(0, 0, 0, 0.5);
}
/*******************************************/
/***********************************/
/* Rotation Tools */
/***********************************/
.cr-rotate-controls {
position: absolute;
bottom: 5px;
left: 5px;
z-index: 1;
}
.cr-rotate-controls button {
border: 0;
background: none;
}
.cr-rotate-controls i:before {
display: inline-block;
font-style: normal;
font-weight: 900;
font-size: 22px;
}
.cr-rotate-l i:before {
content: '↺';
}
.cr-rotate-r i:before {
content: '↻';
}

1
includes/croppie.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
@background: #fff4d9;
@text: #ec6d7d;
@accent: #a3c4ef;
@background: #57506a;
@page-background: #968eb5;
@accent: #ec6d7d;
@text: #464256;
* {
box-sizing: border-box;
@@ -36,6 +37,10 @@ button {
}
}
header * {
color: @accent;
}
h1 {
margin: 0;
}
@@ -112,8 +117,8 @@ textarea {
height: 80vmin;
width: 80vmin;
background-color: @background;
border: 2px solid @accent;
background-color: @page-background;
color: @text;
border-radius: 5vmin;
box-shadow: @accent 1vmin 1vmin;
padding: 5vmin;

View File

@@ -4,20 +4,21 @@ html(lang="en-gb")
meta(charset="utf-8")
title pixsy
link(rel="stylesheet" href="includes/style.css")
link(rel="stylesheet" href="includes/cropper.css")
script(src="includes/cropper.min.js")
link(rel="stylesheet" href="includes/croppie.css")
script(src="includes/croppie.min.js")
body
h1
| pixsy
//img(alt="pixsy" src="includes/pixsy.png")
p.
convert images to Bitsy rooms
|
#[a(href="./old/") old version]
|
#[a(href="mailto:max@tinybird.info") email]
|
#[a(href="https://twitter.com/synth_ruiner") twitter]
header
h1
| pixsy
//img(alt="pixsy" src="includes/pixsy.png")
p.
convert images to Bitsy rooms
|
#[a(href="http://tinybird.info/image-to-bitsy/old/" target="_blank") old version]
|
#[a(href="mailto:max@tinybird.info") email]
|
#[a(href="https://twitter.com/synth_ruiner") twitter]
.pages
.page#start
button.normal.pagination.next#new create a new bitsy game
@@ -35,16 +36,16 @@ html(lang="en-gb")
button.pagination.prev previous
button.pagination.next#game-data-next(disabled=true) next
.page.image
.page.image#page-image
h2 image
.image-container
input#image(type="file" accept="image/*")
#crop
#crop(style="display: none;")
button.pagination.prev previous
button.pagination.next#image-next(disabled=true) next
.page.room
.page.room#page-room
h2 room
table
@@ -68,18 +69,23 @@ html(lang="en-gb")
#new-palette(style="display: none;")
.half
input#colour-background(type="color" value="#2f4ac9")
input#colour-background(type="color" value="#000000")
.half
input#colour-foreground(type="color" value="#8798fe")
input#colour-foreground(type="color" value="#ffffff")
label
input#dither(type="checkbox" checked=true)
| dither
br
select#dither
option None
option(value="Atkinson" selected=true) Atkinson
option(value="Bayer4x4") Bayer 4×4
option(value="FloydSteinberg") Floyd-Steinberg
button.pagination.prev#back-to-image previous
button.pagination.next#room-next next
button.pagination.next#room-next add room
.page.download
p#added
h2 download
textarea#output(autocomplete="off")

View File

@@ -12,10 +12,6 @@ import init, {
set_room_name,
} from './pkg/pixsy.js';
if (typeof WebAssembly !== "object") {
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
function download(filename, text) {
let element = document.createElement('a');
@@ -60,6 +56,10 @@ function readFile(input, callback, type = "text") {
}
async function run() {
if (typeof WebAssembly !== "object") {
window.location = "http://tinybird.info/image-to-bitsy/old/"
}
await init();
const buttonAddImage = el("add");
@@ -72,6 +72,7 @@ async function run() {
const buttonNewGame = el("new");
const buttonReset = el("reset");
const checkboxDither = el("dither");
const divCroppie = el("crop");
const divNewPalette = el("new-palette");
const inputBrightness = el("brightness");
const inputColourBackground = el("colour-background");
@@ -81,8 +82,11 @@ async function run() {
const textareaGameDataInput = el("game-data");
const textareaGameDataOutput = el("output");
const cropper = new Cropper({ width: 192, height: 192 });
let cropperRendered = false;
const croppie = new Croppie(divCroppie, {
viewport: {width: 128, height: 128, type: 'square'},
boundary: {width: 256, height: 256},
enableZoom: true,
});
// hide all pages except start page
for (let page of document.getElementsByClassName('page')) {
@@ -95,8 +99,12 @@ async function run() {
pageButton.addEventListener('touchend', pagination);
}
// croppie needs to be on screen to work;
// halt pagination until we're finished gathering the results
buttonImageProceed.removeEventListener("click", pagination);
function new_game() {
console.debug(load_default_game());
load_default_game();
textareaGameDataInput.value = output();
checkGameData();
// we don't need to look at the default game data, so skip ahead to the image page
@@ -159,13 +167,9 @@ async function run() {
el('image').addEventListener('change', function () {
readFile(this, function (e) {
if ( ! cropperRendered) {
cropper.render("#crop");
cropperRendered = true;
buttonImageProceed.removeAttribute("disabled");
}
cropper.loadImage(e.target.result);
croppie.bind({url: e.target.result, zoom: 0});
divCroppie.style.display = "block";
buttonImageProceed.removeAttribute("disabled");
}, "image");
});
@@ -174,8 +178,18 @@ async function run() {
}
function handleImage() {
console.log(load_image(cropper.getCroppedImage()));
loadPreview();
croppie.result({
type: "base64",
size: "viewport",
format: "png",
}).then((result) => {
console.log("Loading image: " + load_image(result));
el("page-image").style.display = "none";
el("page-room" ).style.display = "block";
loadPreview();
});
}
buttonImageProceed.addEventListener("click", handleImage);
@@ -216,7 +230,7 @@ async function run() {
});
function addRoom() {
console.log(add_room());
el("added").innerText = add_room();
textareaGameDataOutput.value = output();
}
@@ -247,8 +261,8 @@ async function run() {
inputRoomName.value = "";
selectPalette.innerHTML = "";
divNewPalette.style.display = "none";
inputColourBackground.value = "#2f4ac9";
inputColourForeground.value = "#8798fe";
inputColourBackground.value = "#000000";
inputColourForeground.value = "#ffffff";
checkboxDither.checked = true;
}

View File

@@ -3,6 +3,7 @@
use bitsy_parser::game::Game;
use bitsy_parser::image::Image;
use bitsy_parser::tile::Tile;
use dither::prelude::Dither;
use image::{GenericImageView, Pixel, DynamicImage};
use image::imageops::ColorMap;
use image::imageops::FilterType::CatmullRom;
@@ -27,12 +28,18 @@ enum SelectedPalette {
}
}
enum DitherAlgorithm {
Atkinson,
Bayer4x4,
FloydSteinberg,
}
struct State {
game: Option<Game>,
image: Option<DynamicImage>,
room_name: Option<String>,
palette: SelectedPalette,
dither: bool,
dither: Option<DitherAlgorithm>,
brightness: i32,
}
@@ -43,15 +50,15 @@ lazy_static! {
image: None,
room_name: None,
palette: SelectedPalette::None,
dither: true,
dither: Some(DitherAlgorithm::Atkinson),
brightness: 0,
}
);
}
fn tile_name(prefix: &Option<String>, index: &u32) -> Option<String> {
fn tile_name(prefix: &Option<String>, x: &u32, y: &u32) -> Option<String> {
if let Some(prefix) = prefix {
Some(format!("{} {}", prefix, index))
Some(format!("{} ({},{})", prefix, x, y))
} else {
None
}
@@ -95,34 +102,46 @@ pub fn load_image(image_base64: String) -> String {
let mut state = STATE.lock().expect("Couldn't lock application state");
let image_base64: Vec<&str> = image_base64.split("base64,").collect();
if image_base64.len() < 2 {
return format!("Error: Badly-formatted base64: {}", image_base64.join(""));
}
let image_base64 = image_base64[1];
match base64::decode(image_base64) {
Ok(image) => {
match image::load_from_memory(image.as_ref()) {
Ok(image) => {
let size = format!("{}×{}", image.width(), image.height());
// todo get rid of magic numbers! what about Bitsy HD?
let image = image.resize(128, 128, CatmullRom);
state.image = Some(image);
"OK"
size
},
_ => {
state.image = None;
"Couldn't load image"
"Error: Couldn't load image".to_string()
}
}
},
_ => {
state.image = None;
"Couldn't decode image"
"Error: Couldn't decode image".to_string()
}
}.to_string()
}
}
#[wasm_bindgen]
pub fn set_dither(dither: bool) {
pub fn set_dither(dither: &str) {
let mut state = STATE.lock().unwrap();
state.dither = dither;
state.dither = match dither {
"Atkinson" => Some(DitherAlgorithm::Atkinson),
"Bayer4x4" => Some(DitherAlgorithm::Bayer4x4),
"FloydSteinberg" => Some(DitherAlgorithm::FloydSteinberg),
_ => None,
}
}
#[wasm_bindgen]
@@ -184,35 +203,69 @@ fn image_to_base64(image: &DynamicImage) -> String {
format!("data:image/png;base64,{}", base64::encode(&bytes))
}
fn palette_from(bg: &bitsy_parser::Colour, fg: &bitsy_parser::Colour) -> bitsy_parser::Palette {
bitsy_parser::Palette {
id: "0".to_string(),
name: None,
colours: vec![
bg.clone(), fg.clone(), bitsy_parser::Colour { red: 0, green: 0, blue: 0 }
],
}
}
fn render_preview(state: &State) -> DynamicImage {
let mut buffer = state.image.as_ref().unwrap().clone().into_rgba();
let palette = match &state.palette {
SelectedPalette::None => bitsy_parser::mock::game_default().palettes[0].clone(),
SelectedPalette::Existing { id } => state.game.as_ref().unwrap().get_palette(id).unwrap().clone(),
SelectedPalette::New { background, foreground } => bitsy_parser::Palette {
id: "0".to_string(),
name: None,
colours: vec![
background.clone(), foreground.clone(), bitsy_parser::Colour { red: 0, green: 0, blue: 0 }
],
SelectedPalette::None => {
bitsy_parser::mock::game_default().palettes[0].clone()
},
SelectedPalette::Existing { id } => {
state.game.as_ref().unwrap().get_palette(id).unwrap().clone()
},
SelectedPalette::New { background, foreground } => {
palette_from(background, foreground)
},
};
let colour_map = crate::ColourMap::from(&palette);
// adjust brightness
let mut buffer = image::imageops::brighten(&mut buffer, state.brightness);
let mut buffer = image::imageops::brighten(
&mut buffer,
state.brightness
);
if state.dither {
image::imageops::dither(&mut buffer, &colour_map);
if state.dither.is_some() {
// image::imageops::dither(&mut buffer, &colour_map);
// so, what needs doing?
// convert the buffer to the format required by the dither crate
let dither_img = dither::prelude::Img::new(buffer.iter(), 128).unwrap();
// run the dither according to user preference
// todo what is the quantise function supposed to be like?
// dither::ditherer::ATKINSON.dither(dither_img, colour_map);
// convert the dither format back to an ImageBuffer
// buffer = image::ImageBuffer::new(128, 128); //from_vec(128, 128, pixels).unwrap();
//
// for pixel in dither_img.iter() {
// // todo enumerate?
// buffer.put_pixel(0,0, image::Rgba::from([0,0,0,0]));
// }
} else {
// just do colour indexing
let indices = image::imageops::colorops::index_colors(&mut buffer, &colour_map);
let colour_indices = image::imageops::colorops::index_colors(
&mut buffer,
&colour_map
);
// todo get rid of magic numbers! what about Bitsy HD?
buffer = image::ImageBuffer::from_fn(128, 128, |x, y| -> image::Rgba<u8> {
let p = indices.get_pixel(x, y);
let p = colour_indices.get_pixel(x, y);
colour_map
.lookup(p.0[0] as usize)
@@ -242,54 +295,96 @@ pub fn add_room() -> String {
return "No game data loaded".to_string();
}
match &state.palette {
SelectedPalette::None => { return "No palette selected".to_string(); },
_ => {}
};
let mut game = state.game.clone().unwrap();
if state.image.is_none() {
return "No image loaded".to_string();
}
let width = state.image.as_ref().unwrap().width();
let height = state.image.as_ref().unwrap().height();
let palette_id = Some(match &state.palette {
SelectedPalette::None => bitsy_parser::mock::game_default().palettes[0].id.clone(),
SelectedPalette::Existing { id } => id.clone(),
SelectedPalette::New { background, foreground } => {
game.add_palette(palette_from(background, foreground))
},
});
let foreground = &game.palettes
.iter()
.find(|&palette| &palette.id == palette_id.as_ref().unwrap())
.unwrap().colours[1].clone();
let preview = render_preview(&state);
let width = 128;
let height = 128;
let columns = (width as f64 / SD as f64).floor() as u32;
let rows = (height as f64 / SD as f64).floor() as u32;
let mut tile_index = 1;
let mut tile_ids = Vec::new();
let initial_tile_count = game.tiles.len();
for column in 0..columns {
for row in 0..rows {
for row in 0..rows {
for column in 0..columns {
let mut pixels = Vec::with_capacity(64);
fn colour_match(a: &image::Rgb<u8>, b: &bitsy_parser::Colour) -> u8 {
if a[0] == b.red && a[1] == b.green && a[2] == b.blue { 1 } else { 0 }
};
for y in (row * SD)..((row + 1) * SD) {
for x in (column * SD)..((column + 1) * SD) {
let pixel = state.image.as_ref().unwrap().get_pixel(x, y).to_rgb();
let total = pixel[0] as u32 + pixel[1] as u32 + pixel[2] as u32;
// is each channel brighter than 128/255 on average?
pixels.push(if total >= 384 {1} else {0});
let pixel = preview.get_pixel(x, y).to_rgb();
pixels.push(colour_match(&pixel, foreground));
}
}
let tile = Tile {
// "0" will get overwritten to a new, safe tile ID
id: "0".to_string(),
name: tile_name(&state.room_name, &tile_index),
name: tile_name(&state.room_name, &column, &row),
wall: None,
animation_frames: vec![Image { pixels }],
colour_id: None
};
if !game.tiles.contains(&tile) {
game.add_tile(tile.clone());
let tile_id = if game.tiles.contains(&tile) {
game.tiles.iter().find(|&t| t == &tile).unwrap().id.clone()
} else {
game.add_tile(tile)
};
tile_index += 1;
}
tile_ids.push(tile_id);
}
}
game.add_room(bitsy_parser::Room {
id: "0".to_string(),
palette_id,
name: state.room_name.clone(),
tiles: tile_ids,
items: vec![],
exits: vec![],
endings: vec![],
walls: None
});
game.dedupe_tiles();
let new_tile_count = game.tiles.len();
state.game = Some(game.to_owned());
"OK".to_string()
format!(
"Added room \"{}\" with {} new tiles",
&state.room_name.as_ref().unwrap_or(&"untitled".to_string()),
new_tile_count - initial_tile_count
)
}
#[wasm_bindgen]
@@ -304,7 +399,7 @@ pub fn output() -> String {
#[cfg(test)]
mod test {
use crate::{add_room, load_image, load_default_game, output, get_preview};
use crate::{add_room, load_image, load_default_game, output, get_preview, set_room_name};
#[test]
fn image_to_base64() {
@@ -334,9 +429,10 @@ mod test {
#[test]
fn example() {
load_default_game();
load_image(include_str!("test-resources/test.png.base64").to_string());
add_room();
load_image(include_str!("test-resources/test.png.base64").trim().to_string());
set_room_name("test".to_string());
println!("add_room(): {}", add_room());
// todo what? why are extraneous pixels appearing in the output tiles?
assert_eq!(output(), include_str!("test-resources/expected.bitsy"));
}
}

View File

@@ -30,6 +30,26 @@ ROOM 0
NAME example room
PAL 0
ROOM 1
a,a,1,1,2,2,3,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
NAME test
PAL 0
TIL a
11111111
10000001
@@ -41,7 +61,7 @@ TIL a
11111111
NAME block
TIL 2
TIL 1
00011000
00111000
00011000
@@ -50,8 +70,9 @@ TIL 2
00011000
00011000
00111100
NAME test (2,0)
TIL 3
TIL 2
00111100
01100110
01100110
@@ -60,8 +81,9 @@ TIL 3
00110000
01100000
01111110
NAME test (4,0)
TIL 4
TIL 3
00111100
01100110
01100110
@@ -70,6 +92,7 @@ TIL 4
01100110
01100110
00111100
NAME test (6,0)
SPR A
00011000

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 B

After

Width:  |  Height:  |  Size: 523 B

View File

@@ -1 +1 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAQCAYAAACm53kpAAAAvElEQVR4nO2QQQrDMBAD6/8/upVSBMqClab2IWAP2M1qtgmovcFrgNYa7i96lWdO8nKi7oz6HkcBvWUo3FgKXo7PQpmTvJzy2XNiWgGEM/HM6fmaz54TpwLwiBvhjVnPhDPxzLny5Gpn1FceVcCoJ7/sOKcCKlC4sRS8O87EMyf55Ejy1dU58YgCerm46+ucOArA79/oI/U1ykXy1QntXHlSd9wluHX+52LsAnB2ATjLsgvA2QXgLMvyBXwAjqzoAQg4VfAAAAAASUVORK5CYII=
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABHNCSVQICAgIfAhkiAAAAcJJREFUeJzt19FqgzAAQFEz+v+/nD1t2NLWsQgW7jlP2ogVvUYdc865LRhj/C7/7Gr/29678cfDeNxmdZznbtv2vwu2H39cPtr26H/mnNuc8259ZZzXvs7a0dHJdjE+022/sjKFrzh6jKyO89ppM8AZrgqw7C6AMcbbk3s0vmL1BfLZewDHPmIGcOdfZ5z1GbjymfbqEI7u5nf7EMvfjG3bzJVhH/EI4DoCiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHEfQM+SHkFb+/YEwAAAABJRU5ErkJggg==