72 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
c1c4fca1db reset function 2020-11-08 10:09:04 +00:00
e5da032236 remove commented tests 2020-11-08 00:05:48 +00:00
d04eaf892b move name to top 2020-11-07 21:55:30 +00:00
daf9b21096 todos 2020-11-07 21:33:14 +00:00
34053a1327 add old version 2020-11-07 19:31:44 +00:00
21eb632d22 add old version 2020-11-07 19:30:20 +00:00
9eb090924a bump 2020-11-07 16:32:47 +00:00
9261c41cd8 tidyup 2020-11-07 16:31:09 +00:00
6a58e8c003 change the way palettes are handled; support custom palette 2020-11-07 16:30:50 +00:00
aea19fcd6c bump bitsy-parser version for hex colours 2020-11-07 16:27:58 +00:00
db3cdf42ff remove extraneous colour index stuff 2020-11-07 16:27:27 +00:00
31d7ff52ca done 2020-11-07 13:48:38 +00:00
45ef41c366 fix dithering and indexing for image 2020-11-07 13:48:00 +00:00
3a8e349aac feedback for loading game 2020-11-07 11:10:27 +00:00
080d0853eb bit more logging 2020-11-07 11:00:07 +00:00
0150a5ca33 a bit more range on brightness adjustment; top-align room options 2020-11-07 10:58:46 +00:00
ec44370d7e make cropper a bit bigger so we can scale down instead of having to scale up to 128×128 2020-11-07 10:57:01 +00:00
013e1e1c8c resize cropped image (plugin doesn't seem to allow specification of size) 2020-11-07 10:54:52 +00:00
37cde1713d remove unused imports; bump 2020-11-06 16:16:17 +00:00
0780bc8267 get actual palette; comment broken tests; fix for new bitsy_parser game result 2020-11-06 16:14:47 +00:00
fd5f141f19 implement colour mapping and dithering properly 2020-11-06 15:33:26 +00:00
5916c4af17 update to-do list 2020-11-06 12:50:31 +00:00
69a3b482be update palette on selection 2020-11-06 12:47:26 +00:00
4bd896f05a brightness adjustment; find closest palette colour; tests 2020-11-06 12:35:37 +00:00
03588c4c55 lots of stuff 2020-11-05 18:48:22 +00:00
0cb4f3af8d bigger pages; page caption 2020-11-05 09:52:45 +00:00
1b3505929b new crop tool 2020-11-05 09:52:14 +00:00
6b679c3bbc ignore output css 2020-11-04 16:19:51 +00:00
ca93a00eda "set palette" function 2020-11-04 16:18:38 +00:00
4c958373ba tweaks 2020-11-04 16:11:44 +00:00
65257b3886 readme 2020-11-04 15:46:44 +00:00
196ec06c88 ignore output html 2020-11-04 15:39:43 +00:00
4dbcd6ac66 update with new changes from tilesy 2020-11-04 15:38:43 +00:00
8a8743de93 exact version 2020-10-30 23:54:19 +00:00
bf17c08909 wip (wasm isn't compiling) 2020-10-30 17:50:03 +00:00
35 changed files with 1627 additions and 225 deletions

17
.gitattributes vendored
View File

@@ -1,17 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto
# Custom for Visual Studio
*.cs diff=csharp
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain

13
.gitignore vendored
View File

@@ -1,6 +1,3 @@
node_modules
package-lock.json
# Windows image file caches # Windows image file caches
Thumbs.db Thumbs.db
ehthumbs.db ehthumbs.db
@@ -53,8 +50,10 @@ Temporary Items
*.zip *.zip
itch* itch*
# node
node_modules
# IDE stuff # IDE stuff
.idea .idea
/Cargo.lock
/dist/
/index.html
/includes/style.css

20
Cargo.toml Normal file
View File

@@ -0,0 +1,20 @@
[package]
name = "pixsy"
version = "0.72.7"
description = "convert images to Bitsy rooms"
authors = ["Max Bradbury <max@tinybird.info>"]
edition = "2018"
license = "MIT"
repository = "https://tinybird.dev/max/image-to-bitsy"
[lib]
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"
wasm-bindgen = "=0.2.64" # newer versions are bugged...

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Max Bradbury
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

57
README.md Normal file
View File

@@ -0,0 +1,57 @@
# pixsy
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 +0,0 @@
theme: jekyll-theme-dinky

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env bash #! /usr/bin/env bash
pug index.pug index.html pug index.pug
lessc includes/style.less includes/style.css lessc includes/style.less includes/style.css
wasm-pack build --target web

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env bash #! /usr/bin/env bash
zip -r image-to-bitsy.zip readme.md index.html includes/ rm -rf dist
butler push image-to-bitsy.zip ruin/image-to-bitsy:html mkdir dist
cp -r README.md LICENSE index.html script.js pkg includes old dist
butler push dist ruin/pixsy:html

1
includes/croppie.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,144 +1,133 @@
@font-face { @background: #57506a;
font-family: 'rubikregular'; @page-background: #968eb5;
src: url('rubik-regular-webfont.woff2') format('woff2'), @accent: #ec6d7d;
url('rubik-regular-webfont.woff') format('woff'); @text: #464256;
font-weight: normal;
font-style: normal;
}
@light: #d3cbd0;
@dark: #594a54;
@accent: #f69f8f; // pink
* { * {
box-sizing: border-box; box-sizing: border-box;
font-family: "rubikregular", sans-serif; color: @text;
margin: 0 auto 0.5em auto;
text-align: center; text-align: center;
} }
html, body { html, body {
background-color: @dark; background-color: @background;
color: @light; font-size: 3vmin;
margin: 0;
padding: 0;
} }
a { button {
padding: 1em;
white-space: nowrap;
width: 100%;
&.pagination:not(.normal) {
position: absolute;
bottom: 5vmin;
width: auto;
&.prev {
left: 5vmin;
}
&.next, &.start {
right: 5vmin;
}
}
}
header * {
color: @accent; color: @accent;
} }
canvas { h1 {
image-rendering: -moz-crisp-edges;
image-rendering: pixelated;
}
input[type="color"] {
&[disabled] {
opacity: 0.5;
}
width: 2em;
height: 2em;
margin: 0; margin: 0;
padding: 0;
border: none;
background: none;
} }
input[type="file"] { h3 {
padding: 1em 0; font-size: 0.9em;
width: 256px;
} }
input[type="text"], button { input {
width: 14em; width: 100%;
margin: 1em; text-align: left;
padding: 0.25em;
font-size: 1em; &[type="checkbox"] {
width: auto;
margin-right: 1em;
position: relative;
top: 0.25em;
left: 0.25em;
}
} }
table { img {
margin: 0 auto; max-width: 100%;
margin: 0;
}
td, th { p {
width: 50%; font-size: 0.8em;
} margin: 0 auto 1em auto;
}
td { label {
text-align: left; font-size: 0.75em;
} font-weight: bold;
}
th { select {
text-align: right; width: 100%;
}
} }
textarea { textarea {
.box256; height: 15em;
padding: 0.5em;
font-family: monospace; text-align: left;
width: 100%;
} }
textarea, input { .background {
background-color: @background;
padding: 0.5em;
border-radius: 0.5em;
}
.checkboxes label {
margin-right: 1em;
}
.cropper-tools {
margin-top: 0;
}
.half {
display: inline-block;
text-align: left;
width: 50%;
}
.image-container {
height: 46vh;
text-align: left; text-align: left;
} }
.box256 { .page {
height: 256px; height: 80vmin;
width: 256px; width: 80vmin;
background-color: @page-background;
color: @text;
border-radius: 5vmin;
box-shadow: @accent 1vmin 1vmin;
padding: 5vmin;
position: relative;
} }
.centre { #preview {
margin: 0 auto;
}
.croppie-container {
height: auto;
}
.flex-container {
background-color: @light;
display: flex;
flex-flow: row wrap;
}
// make this just for desktop view?
// put sections in a single column on mobile/etc?
.section {
.centre;
background-color: @light;
color: @dark;
width: 256px; width: 256px;
padding-bottom: 1em; height: 256px;
} image-rendering: pixelated;
image-rendering: crisp-edges;
#brightness {
width: 256px;
}
#threshold {
width: 150px;
}
#brightness, #threshold {
+ label{
.centre;
}
// todo make this match the croppie slider or vice versa
}
#palettes {
.box256;
overflow-y: scroll;
}
#preview, #room-output {
.centre;
width: 256px;
}
#save {
margin-top: 0;
} }

141
index.pug
View File

@@ -1,96 +1,99 @@
doctype html doctype html
html html(lang="en-gb")
head head
meta(charset="utf-8") meta(charset="utf-8")
title image to bitsy title pixsy
link(rel="stylesheet" href="includes/style.css")
// lodash
script(src="includes/lodash.min.js")
// jquery
script(src="includes/jquery.min.js")
// croppie
link(rel="stylesheet" href="includes/croppie.css") link(rel="stylesheet" href="includes/croppie.css")
script(src="includes/croppie.js") script(src="includes/croppie.min.js")
// main stuff
link(rel="stylesheet" type="text/css" href="includes/style.css")
script(src="includes/script.js")
body body
header header
h1 image-to-bitsy h1
p convert any image to a #[a(href="https://ledoux.itch.io/bitsy") bitsy] room | pixsy
//img(alt="pixsy" src="includes/pixsy.png")
p. p.
#[a(href="https://github.com/synth-ruiner/bitsy-image-to-room") about] convert images to Bitsy rooms
|
#[a(href="http://tinybird.info/image-to-bitsy/old/" target="_blank") old version]
| |
please contact me if you have any issues:
#[a(href="https://twitter.com/synth_ruiner") twitter],
#[a(href="mailto:max@tinybird.info") email] #[a(href="mailto:max@tinybird.info") email]
|
.flex-container #[a(href="https://twitter.com/synth_ruiner") twitter]
#game-data.section .pages
.page#start
button.normal.pagination.next#new create a new bitsy game
button.normal.pagination.next#load load an existing bitsy game
.page.game-data
h2 game data h2 game data
textarea#bitsy-data(placeholder="Bitsy data or html") input#game(type="file" autocomplete="off")
include includes/default.bitsy br
p textarea#game-data(
input.game-data(type="file") placeholder="Paste your game data here or use the file chooser button above"
p paste or upload your game data (or html) here autocomplete="off"
p (maybe make a backup first) )
#image.section button.pagination.prev previous
button.pagination.next#game-data-next(disabled=true) next
.page.image#page-image
h2 image h2 image
#croppie .image-container
input#image(type="file" accept="image/*")
#crop(style="display: none;")
input#imageUpload(type="file" accepts="image/*") button.pagination.prev previous
button.pagination.next#image-next(disabled=true) next
.page.room#page-room
h2 room
#palette.section table
h2 palette tbody
tr
td(style="width: 60%")
img#preview(alt="preview")
br
form#palettes label
table | brightness
tbody input#brightness(type="range" min=-96 max=96 value=0)
td(style="vertical-align: top;")
label
| name (optional)
input#room-name(type="text" placeholder="e.g. 'bedroom'" autocomplete="off")
#crop.section label
h2 preview | palette
select#palette
canvas#preview(width=128, height=128) #new-palette(style="display: none;")
.half
input#colour-background(type="color" value="#000000")
.half
input#colour-foreground(type="color" value="#ffffff")
input#brightness(type="range" min=-255 max=255 value=0) label
| dither
select#dither
option None
option(value="Atkinson" selected=true) Atkinson
option(value="Bayer4x4") Bayer 4×4
option(value="FloydSteinberg") Floyd-Steinberg
label(for="brightness") brightness button.pagination.prev#back-to-image previous
button.pagination.next#room-next add room
.page.download
p#added
//- to do h2 download
input#dithering(type="checkbox")
label(for="dithering") dithering
//- to do textarea#output(autocomplete="off")
input#smoothing(type="checkbox")
label(for="smoothing") smoothing
#output.section
h2 output
canvas#room-output(width=128, height=128)
label#never never
input#threshold(type="range" min=0 max=64 value=64)
label#always always
br
label(for="threshold") create new tiles
br br
input#roomName(type="text", placeholder="room name") button#download download
button#save write to game data button.pagination.prev#add add another image
button.pagination.start#reset start again
//- to do script(type="module")
include script.js
//-label favour broad strokes | favour details
br
//-button Download

250
old/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: '↻';
}

View File

@@ -100,7 +100,7 @@ $(document).ready(function() {
} }
function handleBitsyGameData() { function handleBitsyGameData() {
let input = $bitsyData.val(); let input = $('#bitsy-data').val();
if ( ! input) { if ( ! input) {
return; return;
@@ -488,7 +488,7 @@ $(document).ready(function() {
}); });
$('#save').on('click touchend', function() { $('#save').on('click touchend', function() {
let $textArea = $('textarea'); $textArea = $('textarea');
let newGameData = $textArea.val(); let newGameData = $textArea.val();

BIN
old/includes/snowy-owls.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

144
old/includes/style.less Normal file
View File

@@ -0,0 +1,144 @@
@font-face {
font-family: 'rubikregular';
src: url('rubik-regular-webfont.woff2') format('woff2'),
url('rubik-regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@light: #d3cbd0;
@dark: #594a54;
@accent: #f69f8f; // pink
* {
box-sizing: border-box;
font-family: "rubikregular", sans-serif;
text-align: center;
}
html, body {
background-color: @dark;
color: @light;
}
a {
color: @accent;
}
canvas {
image-rendering: -moz-crisp-edges;
image-rendering: pixelated;
}
input[type="color"] {
&[disabled] {
opacity: 0.5;
}
width: 2em;
height: 2em;
margin: 0;
padding: 0;
border: none;
background: none;
}
input[type="file"] {
padding: 1em 0;
width: 256px;
}
input[type="text"], button {
width: 14em;
margin: 1em;
padding: 0.25em;
font-size: 1em;
}
table {
margin: 0 auto;
td, th {
width: 50%;
}
td {
text-align: left;
}
th {
text-align: right;
}
}
textarea {
.box256;
font-family: monospace;
}
textarea, input {
text-align: left;
}
.box256 {
height: 256px;
width: 256px;
}
.centre {
margin: 0 auto;
}
.croppie-container {
height: auto;
}
.flex-container {
background-color: @light;
display: flex;
flex-flow: row wrap;
}
// make this just for desktop view?
// put sections in a single column on mobile/etc?
.section {
.centre;
background-color: @light;
color: @dark;
width: 256px;
padding-bottom: 1em;
}
#brightness {
width: 256px;
}
#threshold {
width: 150px;
}
#brightness, #threshold {
+ label{
.centre;
}
// todo make this match the croppie slider or vice versa
}
#palettes {
.box256;
overflow-y: scroll;
}
#preview, #room-output {
.centre;
width: 256px;
}
#save {
margin-top: 0;
}

View File

@@ -1,14 +0,0 @@
{
"name": "bitsy-image-to-room",
"description": "Tool to convert images to Bitsy rooms. Can work as an offline web application",
"version": "0.0.1",
"devDependencies": {
"pug": "latest",
"pug-cli": "latest",
"less": "latest"
},
"dependencies": {
"croppie": "^2.5.1",
"jquery": "^3.5.0"
}
}

273
script.js Normal file
View File

@@ -0,0 +1,273 @@
import init, {
add_room,
get_palettes,
get_preview,
load_image,
load_game,
load_default_game,
output,
set_brightness,
set_dither,
set_palette,
set_room_name,
} from './pkg/pixsy.js';
// 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');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function el(id) {
return document.getElementById(id);
}
function pagination(e) {
const parent = e.target.parentNode;
parent.style.display = "none";
if (e.target.classList.contains("next")) {
parent.nextSibling.style.display = "block";
} else if (e.target.classList.contains("prev")) {
parent.previousSibling.style.display = "block";
} else if (e.target.classList.contains("start")) {
document.getElementById("start").style.display = "block";
}
}
function readFile(input, callback, type = "text") {
if (input.files && input.files[0]) {
let reader = new FileReader();
reader.onload = callback;
if (type === "text") {
reader.readAsText(input.files[0]);
} else {
reader.readAsDataURL(input.files[0]);
}
}
}
async function run() {
if (typeof WebAssembly !== "object") {
window.location = "http://tinybird.info/image-to-bitsy/old/"
}
await init();
const buttonAddImage = el("add");
const buttonBackToImage = el("back-to-image");
const buttonDownload = el("download");
const buttonGameDataProceed = el("game-data-next");
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 divCroppie = el("crop");
const divNewPalette = el("new-palette");
const inputBrightness = el("brightness");
const inputColourBackground = el("colour-background");
const inputColourForeground = el("colour-foreground");
const inputRoomName = el("room-name");
const selectPalette = el("palette");
const textareaGameDataInput = el("game-data");
const textareaGameDataOutput = el("output");
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')) {
page.style.display = "none";
}
el("start").style.display = "block";
for (let pageButton of document.getElementsByClassName("pagination")) {
pageButton.addEventListener('click', 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() {
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
buttonGameDataProceed.click();
}
function clear_game() {
textareaGameDataInput.value = "";
checkGameData();
}
buttonNewGame.addEventListener("click", new_game);
buttonNewGame.addEventListener("touchend", new_game);
buttonLoadGame.addEventListener("click", clear_game);
buttonLoadGame.addEventListener("touchend", clear_game);
// handle game data and image
el("game").addEventListener("change", function() {
readFile(this, function (e) {
textareaGameDataInput.value = e.target.result;
console.log(load_game(e.target.result));
checkGameData();
}, "text");
});
function setPaletteDropdown() {
const palettes = JSON.parse(get_palettes());
console.debug(palettes);
selectPalette.innerHTML = "";
palettes.push({
id: "NEW_PALETTE",
name: "new palette"
});
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");
}
}
textareaGameDataInput.addEventListener("change", checkGameData);
textareaGameDataInput.addEventListener("keyup", checkGameData);
checkGameData();
el('image').addEventListener('change', function () {
readFile(this, function (e) {
croppie.bind({url: e.target.result, zoom: 0});
divCroppie.style.display = "block";
buttonImageProceed.removeAttribute("disabled");
}, "image");
});
function loadPreview() {
el("preview").setAttribute("src", get_preview());
}
function handleImage() {
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);
buttonImageProceed.addEventListener("touchend", handleImage);
selectPalette.addEventListener("change", () => {
set_palette(selectPalette.value, inputColourBackground.value, inputColourForeground.value);
if (selectPalette.value === "NEW_PALETTE") {
divNewPalette.style.display = "block";
} else {
divNewPalette.style.display = "none";
}
loadPreview();
});
function updateCustomPalette() {
set_palette(selectPalette.value, inputColourBackground.value, inputColourForeground.value);
loadPreview();
}
inputColourForeground.addEventListener("change", updateCustomPalette);
inputColourBackground.addEventListener("change", updateCustomPalette);
checkboxDither.addEventListener("change", () => {
set_dither(checkboxDither.checked);
loadPreview();
});
inputRoomName.addEventListener("change", () => {
set_room_name(inputRoomName.value);
});
inputBrightness.addEventListener("input", () => {
set_brightness(inputBrightness.value);
loadPreview();
});
function addRoom() {
el("added").innerText = add_room();
textareaGameDataOutput.value = output();
}
buttonRoomProceed.addEventListener("click", addRoom);
buttonRoomProceed.addEventListener("touchend", addRoom);
function handleDownload() {
download("output.bitsy", textareaGameDataOutput.value);
}
buttonDownload.addEventListener("click", handleDownload);
buttonDownload.addEventListener("touchend", handleDownload);
function addImage() {
textareaGameDataInput.value = textareaGameDataOutput.value;
textareaGameDataOutput.value = "";
buttonBackToImage.click();
}
buttonAddImage.addEventListener("click", addImage);
buttonAddImage.addEventListener("touchend", addImage);
// would it be easier just to reload the page? lol
function reset() {
clear_game();
// todo clear file inputs
inputBrightness.value = 0;
inputRoomName.value = "";
selectPalette.innerHTML = "";
divNewPalette.style.display = "none";
inputColourBackground.value = "#000000";
inputColourForeground.value = "#ffffff";
checkboxDither.checked = true;
}
buttonReset.addEventListener("click", reset);
buttonReset.addEventListener("touchend", reset);
}
run();

74
src/colour_map.rs Normal file
View File

@@ -0,0 +1,74 @@
use image::Rgba;
#[derive(Clone, Copy)]
pub struct ColourMap {
background: Rgba<u8>,
foreground: Rgba<u8>,
}
impl ColourMap {
pub(crate) fn from(palette: &bitsy_parser::Palette) -> ColourMap {
let background = Rgba::from([
palette.colours[0].red,
palette.colours[0].green,
palette.colours[0].blue,
255,
]);
let foreground = Rgba::from([
palette.colours[1].red,
palette.colours[1].green,
palette.colours[1].blue,
255,
]);
ColourMap { background, foreground }
}
}
fn diff(a: &Rgba<u8>, b:&Rgba<u8>) -> u32 {
let diff_red = (a[0] as i16 - b[0] as i16).abs();
let diff_green= (a[1] as i16 - b[1] as i16).abs();
let diff_blue = (a[2] as i16 - b[2] as i16).abs();
(diff_red + diff_green + diff_blue) as u32
}
impl image::imageops::colorops::ColorMap for ColourMap {
type Color = Rgba<u8>;
#[inline(always)]
fn index_of(&self, color: &Self::Color) -> usize {
let diff_background = diff(color, &self.background);
let diff_foreground = diff(color, &self.foreground);
if diff_foreground <= diff_background { 1 } else { 0 }
}
#[inline(always)]
fn lookup(&self, idx: usize) -> Option<Self::Color> {
match idx {
0 => Some(self.background.into()),
1 => Some(self.foreground.into()),
_ => None,
}
}
/// Indicate NeuQuant implements `lookup`.
fn has_lookup(&self) -> bool {
true
}
#[inline(always)]
fn map_color(&self, color: &mut Self::Color) {
let closest = match self.index_of(color) {
1 => self.foreground,
_ => self.background,
};
color[0] = closest[0];
color[1] = closest[1];
color[2] = closest[2];
color[3] = closest[3];
}
}

438
src/lib.rs Normal file
View File

@@ -0,0 +1,438 @@
#![feature(clamp)]
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;
use lazy_static::lazy_static;
use std::sync::Mutex;
use wasm_bindgen::prelude::*;
mod colour_map;
use colour_map::ColourMap;
const SD: u32 = 8;
enum SelectedPalette {
None,
Existing {
id: String,
},
New {
background: bitsy_parser::Colour,
foreground: bitsy_parser::Colour,
}
}
enum DitherAlgorithm {
Atkinson,
Bayer4x4,
FloydSteinberg,
}
struct State {
game: Option<Game>,
image: Option<DynamicImage>,
room_name: Option<String>,
palette: SelectedPalette,
dither: Option<DitherAlgorithm>,
brightness: i32,
}
lazy_static! {
static ref STATE: Mutex<State> = Mutex::new(
State {
game: None,
image: None,
room_name: None,
palette: SelectedPalette::None,
dither: Some(DitherAlgorithm::Atkinson),
brightness: 0,
}
);
}
fn tile_name(prefix: &Option<String>, x: &u32, y: &u32) -> Option<String> {
if let Some(prefix) = prefix {
Some(format!("{} ({},{})", prefix, x, y))
} else {
None
}
}
#[wasm_bindgen]
pub fn load_default_game() {
let mut state = STATE.lock().unwrap();
state.game = Some(bitsy_parser::mock::game_default());
// yes, this will probably always just be "0", but to be safe…
state.palette = SelectedPalette::Existing {
id: bitsy_parser::mock::game_default().palette_ids()[0].clone()
}
}
#[wasm_bindgen]
pub fn load_game(game_data: String) -> String {
let mut state = STATE.lock().unwrap();
let result = Game::from(game_data);
match result {
Ok((game, _errs)) => {
let palette_id = game.palette_ids()[0].clone();
state.game = Some(game);
state.palette = SelectedPalette::Existing { id: palette_id };
format!("Loaded game")
},
_ => {
state.game = None;
state.palette = SelectedPalette::None;
format!("{}", result.err().unwrap())
}
}
}
#[wasm_bindgen]
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);
size
},
_ => {
state.image = None;
"Error: Couldn't load image".to_string()
}
}
},
_ => {
state.image = None;
"Error: Couldn't decode image".to_string()
}
}
}
#[wasm_bindgen]
pub fn set_dither(dither: &str) {
let mut state = STATE.lock().unwrap();
state.dither = match dither {
"Atkinson" => Some(DitherAlgorithm::Atkinson),
"Bayer4x4" => Some(DitherAlgorithm::Bayer4x4),
"FloydSteinberg" => Some(DitherAlgorithm::FloydSteinberg),
_ => None,
}
}
#[wasm_bindgen]
pub fn set_palette(palette_id: &str, background: String, foreground: String) {
let mut state = STATE.lock().unwrap();
state.palette = match palette_id {
"NEW_PALETTE" => SelectedPalette::New {
background: bitsy_parser::Colour::from_hex(&background).unwrap(),
foreground: bitsy_parser::Colour::from_hex(&foreground).unwrap(),
},
"" => SelectedPalette::None,
_ => SelectedPalette::Existing { id: palette_id.to_string() },
}
}
#[wasm_bindgen]
pub fn set_room_name(room_name: String) {
let mut state = STATE.lock().unwrap();
match room_name.is_empty() {
true => { state.room_name = None },
false => { state.room_name = Some(room_name) },
}
}
#[wasm_bindgen]
pub fn set_brightness(brightness: i32) {
let mut state = STATE.lock().unwrap();
state.brightness = brightness;
}
#[wasm_bindgen]
pub fn get_palettes() -> String {
let state = STATE.lock().unwrap();
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 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 } => {
palette_from(background, foreground)
},
};
let colour_map = crate::ColourMap::from(&palette);
// adjust brightness
let mut buffer = image::imageops::brighten(
&mut buffer,
state.brightness
);
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 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 = colour_indices.get_pixel(x, y);
colour_map
.lookup(p.0[0] as usize)
.expect("indexed colour out-of-range")
.into()
});
}
image::DynamicImage::ImageRgba8(buffer)
}
#[wasm_bindgen]
pub fn get_preview() -> String {
let state = STATE.lock().unwrap();
match &state.image.is_some() {
true => image_to_base64(&render_preview(&state)),
false => "".to_string(),
}
}
#[wasm_bindgen]
pub fn add_room() -> String {
let mut state = STATE.lock().expect("Couldn't lock application state");
if state.game.is_none() {
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 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_ids = Vec::new();
let initial_tile_count = game.tiles.len();
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 = 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, &column, &row),
wall: None,
animation_frames: vec![Image { pixels }],
colour_id: None
};
let tile_id = if game.tiles.contains(&tile) {
game.tiles.iter().find(|&t| t == &tile).unwrap().id.clone()
} else {
game.add_tile(tile)
};
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());
format!(
"Added room \"{}\" with {} new tiles",
&state.room_name.as_ref().unwrap_or(&"untitled".to_string()),
new_tile_count - initial_tile_count
)
}
#[wasm_bindgen]
pub fn output() -> String {
let state = STATE.lock().unwrap();
match &state.game {
Some(game) => game.to_string(),
None => "No game loaded".to_string(),
}
}
#[cfg(test)]
mod test {
use crate::{add_room, load_image, load_default_game, output, get_preview, set_room_name};
#[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").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"));
}
}

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,159 @@
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
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
10000001
10011001
10011001
10000001
10000001
11111111
NAME block
TIL 1
00011000
00111000
00011000
00011000
00011000
00011000
00011000
00111100
NAME test (2,0)
TIL 2
00111100
01100110
01100110
00001100
00011000
00110000
01100000
01111110
NAME test (4,0)
TIL 3
00111100
01100110
01100110
00001100
00001100
01100110
01100110
00111100
NAME test (6,0)
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: 523 B

View File

@@ -0,0 +1 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABHNCSVQICAgIfAhkiAAAAcJJREFUeJzt19FqgzAAQFEz+v+/nD1t2NLWsQgW7jlP2ogVvUYdc865LRhj/C7/7Gr/29678cfDeNxmdZznbtv2vwu2H39cPtr26H/mnNuc8259ZZzXvs7a0dHJdjE+022/sjKFrzh6jKyO89ppM8AZrgqw7C6AMcbbk3s0vmL1BfLZewDHPmIGcOdfZ5z1GbjymfbqEI7u5nf7EMvfjG3bzJVhH/EI4DoCiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHEfQM+SHkFb+/YEwAAAABJRU5ErkJggg==