16 Commits

Author SHA1 Message Date
4d40fd6e08 wip 2020-11-28 14:51:43 +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
21eb632d22 add old version 2020-11-07 19:30:20 +00:00
12 changed files with 421 additions and 175 deletions

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "pixsy" name = "pixsy"
version = "0.72.7" version = "0.72.8"
description = "convert images to Bitsy rooms" description = "convert images to Bitsy rooms"
authors = ["Max Bradbury <max@tinybird.info>"] authors = ["Max Bradbury <max@tinybird.info>"]
edition = "2018" edition = "2018"

View File

@@ -1,3 +1,57 @@
# pixsy # 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,6 +0,0 @@
# todo
* if image is exactly 128×128, *don't* crop
* tile reuse
* noise reduction (remove lonely pixels)
* implement Atkinson and Bayer dithering options

View File

@@ -1,7 +1,6 @@
#! /usr/bin/env bash #! /usr/bin/env bash
./build.sh
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 pkg includes old dist
# butler push dist ruin/pixsy:html 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

@@ -2,6 +2,7 @@
@page-background: #968eb5; @page-background: #968eb5;
@accent: #ec6d7d; @accent: #ec6d7d;
@text: #464256; @text: #464256;
@ok: #caec6d;
* { * {
box-sizing: border-box; box-sizing: border-box;
@@ -131,3 +132,13 @@ textarea {
image-rendering: pixelated; image-rendering: pixelated;
image-rendering: crisp-edges; image-rendering: crisp-edges;
} }
.message {
background-color: @ok;
padding: 1em;
text-align: left;
&#game-data-errors {
background-color: @accent;
}
}

View File

@@ -4,8 +4,8 @@ html(lang="en-gb")
meta(charset="utf-8") meta(charset="utf-8")
title pixsy title pixsy
link(rel="stylesheet" href="includes/style.css") link(rel="stylesheet" href="includes/style.css")
link(rel="stylesheet" href="includes/cropper.css") link(rel="stylesheet" href="includes/croppie.css")
script(src="includes/cropper.min.js") script(src="includes/croppie.min.js")
body body
header header
h1 h1
@@ -14,7 +14,7 @@ html(lang="en-gb")
p. p.
convert images to Bitsy rooms convert images to Bitsy rooms
| |
#[a(href="./old/") old version] #[a(href="http://tinybird.info/image-to-bitsy/old/" target="_blank") old version]
| |
#[a(href="mailto:max@tinybird.info") email] #[a(href="mailto:max@tinybird.info") email]
| |
@@ -34,6 +34,9 @@ html(lang="en-gb")
autocomplete="off" autocomplete="off"
) )
p#game-data-result.message(style="display: none;")
p#game-data-errors.message(style="display: none;")
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 .page.image#page-image
@@ -41,7 +44,7 @@ html(lang="en-gb")
.image-container .image-container
input#image(type="file" accept="image/*") input#image(type="file" accept="image/*")
#crop #crop(style="display: none;")
button.pagination.prev previous button.pagination.prev previous
button.pagination.next#image-next(disabled=true) next button.pagination.next#image-next(disabled=true) next
@@ -69,9 +72,9 @@ html(lang="en-gb")
#new-palette(style="display: none;") #new-palette(style="display: none;")
.half .half
input#colour-background(type="color" value="#2f4ac9") input#colour-background(type="color" value="#000000")
.half .half
input#colour-foreground(type="color" value="#8798fe") input#colour-foreground(type="color" value="#ffffff")
label label
input#dither(type="checkbox" checked=true) input#dither(type="checkbox" checked=true)

View File

@@ -12,10 +12,6 @@ import init, {
set_room_name, set_room_name,
} from './pkg/pixsy.js'; } 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 // 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) { function download(filename, text) {
let element = document.createElement('a'); let element = document.createElement('a');
@@ -60,6 +56,10 @@ function readFile(input, callback, type = "text") {
} }
async function run() { async function run() {
if (typeof WebAssembly !== "object") {
window.location = "http://tinybird.info/image-to-bitsy/old/"
}
await init(); await init();
const buttonAddImage = el("add"); const buttonAddImage = el("add");
@@ -72,17 +72,23 @@ async function run() {
const buttonNewGame = el("new"); const buttonNewGame = el("new");
const buttonReset = el("reset"); const buttonReset = el("reset");
const checkboxDither = el("dither"); const checkboxDither = el("dither");
const divCroppie = el("crop");
const divNewPalette = el("new-palette"); const divNewPalette = el("new-palette");
const inputBrightness = el("brightness"); const inputBrightness = el("brightness");
const inputColourBackground = el("colour-background"); const inputColourBackground = el("colour-background");
const inputColourForeground = el("colour-foreground"); const inputColourForeground = el("colour-foreground");
const inputRoomName = el("room-name"); const inputRoomName = el("room-name");
const paragraphGameResult = el("game-data-result");
const paragraphGameErrors = el("game-data-errors");
const selectPalette = el("palette"); const selectPalette = el("palette");
const textareaGameDataInput = el("game-data"); const textareaGameDataInput = el("game-data");
const textareaGameDataOutput = el("output"); const textareaGameDataOutput = el("output");
const cropper = new Cropper({ width: 192, height: 192 }); const croppie = new Croppie(divCroppie, {
let cropperRendered = false; viewport: {width: 128, height: 128, type: 'square'},
boundary: {width: 256, height: 256},
enableZoom: true,
});
// hide all pages except start page // hide all pages except start page
for (let page of document.getElementsByClassName('page')) { for (let page of document.getElementsByClassName('page')) {
@@ -95,6 +101,10 @@ async function run() {
pageButton.addEventListener('touchend', pagination); 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() { function new_game() {
load_default_game(); load_default_game();
textareaGameDataInput.value = output(); textareaGameDataInput.value = output();
@@ -118,7 +128,6 @@ async function run() {
el("game").addEventListener("change", function() { el("game").addEventListener("change", function() {
readFile(this, function (e) { readFile(this, function (e) {
textareaGameDataInput.value = e.target.result; textareaGameDataInput.value = e.target.result;
console.log(load_game(e.target.result));
checkGameData(); checkGameData();
}, "text"); }, "text");
}); });
@@ -145,27 +154,46 @@ async function run() {
} }
function checkGameData() { function checkGameData() {
if (textareaGameDataInput.value.length > 0) { paragraphGameResult.style.display = "none";
paragraphGameErrors.style.display = "none";
let game_data = textareaGameDataInput.value;
if (game_data.length === 0) {
buttonGameDataProceed.setAttribute("disabled", "disabled");
return;
}
let result = load_game(game_data);
console.debug(result);
let parts = result.split(". Errors: ");
result = parts[0];
let errors = parts[1];
paragraphGameResult.innerText = result;
paragraphGameResult.style.display = "block";
if (errors) {
paragraphGameErrors.innerText = errors;
paragraphGameErrors.style.display = "block";
}
if (result.startsWith("Error")) {
buttonGameDataProceed.setAttribute("disabled", "disabled");
} else {
buttonGameDataProceed.removeAttribute("disabled"); buttonGameDataProceed.removeAttribute("disabled");
setPaletteDropdown(); setPaletteDropdown();
} else {
buttonGameDataProceed.setAttribute("disabled", "disabled");
} }
} }
textareaGameDataInput.addEventListener("change", checkGameData); textareaGameDataInput.addEventListener("change", checkGameData);
textareaGameDataInput.addEventListener("keyup", checkGameData);
checkGameData(); checkGameData();
el('image').addEventListener('change', function () { el('image').addEventListener('change', function () {
readFile(this, function (e) { readFile(this, function (e) {
if ( ! cropperRendered) { croppie.bind({url: e.target.result, zoom: 0});
cropper.render("#crop"); divCroppie.style.display = "block";
cropperRendered = true;
buttonImageProceed.removeAttribute("disabled"); buttonImageProceed.removeAttribute("disabled");
}
cropper.loadImage(e.target.result);
}, "image"); }, "image");
}); });
@@ -174,8 +202,18 @@ async function run() {
} }
function handleImage() { function handleImage() {
console.log("Loading image: " + load_image(cropper.getCroppedImage())); 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(); loadPreview();
});
} }
buttonImageProceed.addEventListener("click", handleImage); buttonImageProceed.addEventListener("click", handleImage);
@@ -247,8 +285,8 @@ async function run() {
inputRoomName.value = ""; inputRoomName.value = "";
selectPalette.innerHTML = ""; selectPalette.innerHTML = "";
divNewPalette.style.display = "none"; divNewPalette.style.display = "none";
inputColourBackground.value = "#2f4ac9"; inputColourBackground.value = "#000000";
inputColourForeground.value = "#8798fe"; inputColourForeground.value = "#ffffff";
checkboxDither.checked = true; checkboxDither.checked = true;
} }

View File

@@ -76,16 +76,21 @@ pub fn load_game(game_data: String) -> String {
let result = Game::from(game_data); let result = Game::from(game_data);
match result { match result {
Ok((game, _errs)) => { Ok((game, errors)) => {
let palette_id = game.palette_ids()[0].clone(); let palette_id = game.palette_ids()[0].clone();
let game_name = game.name.clone();
let errors: Vec<String> = errors.iter().map(|err| format!("{}", err)).collect();
state.game = Some(game); state.game = Some(game);
state.palette = SelectedPalette::Existing { id: palette_id }; state.palette = SelectedPalette::Existing { id: palette_id };
format!("Loaded game")
}, format!("Loaded game: {}. Errors: {}", game_name, errors.join(", "))
}
_ => { _ => {
state.game = None; state.game = None;
state.palette = SelectedPalette::None; state.palette = SelectedPalette::None;
format!("{}", result.err().unwrap())
format!("Error: {}", result.err().unwrap())
} }
} }
} }
@@ -167,6 +172,7 @@ pub fn get_palettes() -> String {
let mut palette_objects = json::JsonValue::new_array(); let mut palette_objects = json::JsonValue::new_array();
if state.game.is_some() {
for palette in &state.game.as_ref().unwrap().palettes { for palette in &state.game.as_ref().unwrap().palettes {
let mut object = json::JsonValue::new_object(); let mut object = json::JsonValue::new_object();
@@ -180,6 +186,7 @@ pub fn get_palettes() -> String {
palette_objects.push(object).unwrap(); palette_objects.push(object).unwrap();
} }
}
json::stringify(palette_objects) json::stringify(palette_objects)
} }
@@ -392,4 +399,16 @@ mod test {
// todo what? why are extraneous pixels appearing in the output tiles? // todo what? why are extraneous pixels appearing in the output tiles?
assert_eq!(output(), include_str!("test-resources/expected.bitsy")); assert_eq!(output(), include_str!("test-resources/expected.bitsy"));
} }
#[test]
fn palettes() {
load_default_game();
assert_eq!(crate::get_palettes(), "[{\"id\":\"0\",\"name\":\"blueprint\"}]");
}
#[test]
fn no_palettes() {
assert_eq!(crate::get_palettes(), "[]");
}
} }