Compare commits
66 Commits
72dbff8d5e
...
f135e184e4
Author | SHA1 | Date |
---|---|---|
Max Bradbury | f135e184e4 | |
Max Bradbury | 3e7d6eeaa5 | |
Max Bradbury | f6308110be | |
Max Bradbury | 2d73963aa0 | |
Max Bradbury | 6f8e00130c | |
Max Bradbury | b478b1e3ee | |
Max Bradbury | 055928eb7b | |
Max Bradbury | b3690c4dd7 | |
Max Bradbury | da04534fd9 | |
Max Bradbury | 3d1129c613 | |
Max Bradbury | 3b851975d0 | |
Max Bradbury | afc626bae0 | |
Max Bradbury | 6e43249d64 | |
Max Bradbury | 458604cd1a | |
Max Bradbury | bb0b970281 | |
Max Bradbury | f62202cb74 | |
Max Bradbury | 745b18dfc0 | |
Max Bradbury | e5a87f854e | |
Max Bradbury | c2787db422 | |
Max Bradbury | 7d274bb3c2 | |
Max Bradbury | fbe40fb866 | |
Max Bradbury | 7ff67e51f8 | |
Max Bradbury | 80df4a9a6a | |
Max Bradbury | e76ff57053 | |
Max Bradbury | 0ef2d2acd9 | |
Max Bradbury | f9b0f6b6db | |
Max Bradbury | 8c92b05b74 | |
Max Bradbury | 512f386c25 | |
Max Bradbury | 63cb971aca | |
Max Bradbury | 03ce88e015 | |
Max Bradbury | 8a8b861e5d | |
Max Bradbury | c1c4fca1db | |
Max Bradbury | e5da032236 | |
Max Bradbury | d04eaf892b | |
Max Bradbury | daf9b21096 | |
Max Bradbury | 34053a1327 | |
Max Bradbury | 21eb632d22 | |
Max Bradbury | 9eb090924a | |
Max Bradbury | 9261c41cd8 | |
Max Bradbury | 6a58e8c003 | |
Max Bradbury | aea19fcd6c | |
Max Bradbury | db3cdf42ff | |
Max Bradbury | 31d7ff52ca | |
Max Bradbury | 45ef41c366 | |
Max Bradbury | 3a8e349aac | |
Max Bradbury | 080d0853eb | |
Max Bradbury | 0150a5ca33 | |
Max Bradbury | ec44370d7e | |
Max Bradbury | 013e1e1c8c | |
Max Bradbury | 37cde1713d | |
Max Bradbury | 0780bc8267 | |
Max Bradbury | fd5f141f19 | |
Max Bradbury | 5916c4af17 | |
Max Bradbury | 69a3b482be | |
Max Bradbury | 4bd896f05a | |
Max Bradbury | 03588c4c55 | |
Max Bradbury | 0cb4f3af8d | |
Max Bradbury | 1b3505929b | |
Max Bradbury | 6b679c3bbc | |
Max Bradbury | ca93a00eda | |
Max Bradbury | 4c958373ba | |
Max Bradbury | 65257b3886 | |
Max Bradbury | 196ec06c88 | |
Max Bradbury | 4dbcd6ac66 | |
Max Bradbury | 8a8743de93 | |
Max Bradbury | bf17c08909 |
|
@ -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
|
|
@ -1,6 +1,3 @@
|
|||
node_modules
|
||||
package-lock.json
|
||||
|
||||
# Windows image file caches
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
@ -53,8 +50,10 @@ Temporary Items
|
|||
*.zip
|
||||
itch*
|
||||
|
||||
# node
|
||||
node_modules
|
||||
|
||||
# IDE stuff
|
||||
.idea
|
||||
.idea
|
||||
|
||||
/Cargo.lock
|
||||
/dist/
|
||||
/index.html
|
||||
/includes/style.css
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
[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"
|
||||
image = "^0.23.7"
|
||||
json = "^0.12.4"
|
||||
lazy_static = "^1.4.0"
|
||||
wasm-bindgen = "=0.2.64" # newer versions are bugged...
|
|
@ -0,0 +1,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.
|
|
@ -0,0 +1,6 @@
|
|||
# todo
|
||||
|
||||
* tile reuse
|
||||
* noise reduction (remove lonely pixels)
|
||||
* implement Atkinson and Bayer dithering options
|
||||
* fix weird problem with pixels flipping (see test::example)
|
|
@ -1 +0,0 @@
|
|||
theme: jekyll-theme-dinky
|
5
build.sh
5
build.sh
|
@ -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
|
||||
wasm-pack build --target web
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
#! /usr/bin/env bash
|
||||
|
||||
zip -r image-to-bitsy.zip readme.md index.html includes/
|
||||
butler push image-to-bitsy.zip ruin/image-to-bitsy:html
|
||||
rm -rf dist
|
||||
mkdir dist
|
||||
cp -r README.md LICENSE index.html script.js pkg includes old dist
|
||||
butler push dist ruin/pixsy:html
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,144 +1,133 @@
|
|||
@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
|
||||
@background: #57506a;
|
||||
@page-background: #968eb5;
|
||||
@accent: #ec6d7d;
|
||||
@text: #464256;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: "rubikregular", sans-serif;
|
||||
color: @text;
|
||||
margin: 0 auto 0.5em auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
html, body {
|
||||
background-color: @dark;
|
||||
color: @light;
|
||||
background-color: @background;
|
||||
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;
|
||||
}
|
||||
|
||||
canvas {
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
&[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
padding: 1em 0;
|
||||
width: 256px;
|
||||
h3 {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
input[type="text"], button {
|
||||
width: 14em;
|
||||
margin: 1em;
|
||||
padding: 0.25em;
|
||||
font-size: 1em;
|
||||
input {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
&[type="checkbox"] {
|
||||
width: auto;
|
||||
margin-right: 1em;
|
||||
position: relative;
|
||||
top: 0.25em;
|
||||
left: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0 auto;
|
||||
img {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
td, th {
|
||||
width: 50%;
|
||||
}
|
||||
p {
|
||||
font-size: 0.8em;
|
||||
margin: 0 auto 1em auto;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: left;
|
||||
}
|
||||
label {
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: right;
|
||||
}
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
.box256;
|
||||
|
||||
font-family: monospace;
|
||||
height: 15em;
|
||||
padding: 0.5em;
|
||||
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;
|
||||
}
|
||||
|
||||
.box256 {
|
||||
height: 256px;
|
||||
width: 256px;
|
||||
.page {
|
||||
height: 80vmin;
|
||||
width: 80vmin;
|
||||
|
||||
background-color: @page-background;
|
||||
color: @text;
|
||||
border-radius: 5vmin;
|
||||
box-shadow: @accent 1vmin 1vmin;
|
||||
padding: 5vmin;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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;
|
||||
#preview {
|
||||
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;
|
||||
height: 256px;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
|
138
index.pug
138
index.pug
|
@ -1,96 +1,96 @@
|
|||
doctype html
|
||||
html
|
||||
html(lang="en-gb")
|
||||
head
|
||||
meta(charset="utf-8")
|
||||
title image to bitsy
|
||||
|
||||
// lodash
|
||||
script(src="includes/lodash.min.js")
|
||||
|
||||
// jquery
|
||||
script(src="includes/jquery.min.js")
|
||||
|
||||
// croppie
|
||||
title pixsy
|
||||
link(rel="stylesheet" href="includes/style.css")
|
||||
link(rel="stylesheet" href="includes/croppie.css")
|
||||
script(src="includes/croppie.js")
|
||||
|
||||
// main stuff
|
||||
link(rel="stylesheet" type="text/css" href="includes/style.css")
|
||||
script(src="includes/script.js")
|
||||
script(src="includes/croppie.min.js")
|
||||
body
|
||||
header
|
||||
h1 image-to-bitsy
|
||||
p convert any image to a #[a(href="https://ledoux.itch.io/bitsy") bitsy] room
|
||||
h1
|
||||
| pixsy
|
||||
//img(alt="pixsy" src="includes/pixsy.png")
|
||||
p.
|
||||
#[a(href="https://github.com/synth-ruiner/bitsy-image-to-room") about]
|
||||
convert images to Bitsy rooms
|
||||
|
|
||||
#[a(href="./old/") 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]
|
||||
|
||||
.flex-container
|
||||
#game-data.section
|
||||
|
|
||||
#[a(href="https://twitter.com/synth_ruiner") twitter]
|
||||
.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
|
||||
|
||||
textarea#bitsy-data(placeholder="Bitsy data or html")
|
||||
include includes/default.bitsy
|
||||
input#game(type="file" autocomplete="off")
|
||||
br
|
||||
|
||||
p
|
||||
input.game-data(type="file")
|
||||
p paste or upload your game data (or html) here
|
||||
p (maybe make a backup first)
|
||||
textarea#game-data(
|
||||
placeholder="Paste your game data here or use the file chooser button above"
|
||||
autocomplete="off"
|
||||
)
|
||||
|
||||
#image.section
|
||||
button.pagination.prev previous
|
||||
button.pagination.next#game-data-next(disabled=true) next
|
||||
.page.image#page-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
|
||||
h2 palette
|
||||
table
|
||||
tbody
|
||||
tr
|
||||
td(style="width: 60%")
|
||||
img#preview(alt="preview")
|
||||
br
|
||||
|
||||
form#palettes
|
||||
table
|
||||
tbody
|
||||
label
|
||||
| brightness
|
||||
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
|
||||
h2 preview
|
||||
label
|
||||
| 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
|
||||
input#dither(type="checkbox" checked=true)
|
||||
| dither
|
||||
br
|
||||
|
||||
label(for="brightness") brightness
|
||||
button.pagination.prev#back-to-image previous
|
||||
button.pagination.next#room-next add room
|
||||
.page.download
|
||||
p#added
|
||||
|
||||
//- to do
|
||||
input#dithering(type="checkbox")
|
||||
label(for="dithering") dithering
|
||||
h2 download
|
||||
|
||||
//- to do
|
||||
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
|
||||
textarea#output(autocomplete="off")
|
||||
br
|
||||
|
||||
input#roomName(type="text", placeholder="room name")
|
||||
button#download download
|
||||
|
||||
button#save write to game data
|
||||
|
||||
//- to do
|
||||
|
||||
//-label favour broad strokes | favour details
|
||||
|
||||
br
|
||||
|
||||
//-button Download
|
||||
button.pagination.prev#add add another image
|
||||
button.pagination.start#reset start again
|
||||
script(type="module")
|
||||
include script.js
|
||||
|
|
|
@ -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: '↻';
|
||||
}
|
|
@ -100,7 +100,7 @@ $(document).ready(function() {
|
|||
}
|
||||
|
||||
function handleBitsyGameData() {
|
||||
let input = $bitsyData.val();
|
||||
let input = $('#bitsy-data').val();
|
||||
|
||||
if ( ! input) {
|
||||
return;
|
||||
|
@ -488,7 +488,7 @@ $(document).ready(function() {
|
|||
});
|
||||
|
||||
$('#save').on('click touchend', function() {
|
||||
let $textArea = $('textarea');
|
||||
$textArea = $('textarea');
|
||||
|
||||
let newGameData = $textArea.val();
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 86 KiB |
|
@ -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;
|
||||
}
|
14
package.json
14
package.json
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
||||
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');
|
||||
|
||||
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() {
|
||||
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();
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,395 @@
|
|||
#![feature(clamp)]
|
||||
|
||||
use bitsy_parser::game::Game;
|
||||
use bitsy_parser::image::Image;
|
||||
use bitsy_parser::tile::Tile;
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
game: Option<Game>,
|
||||
image: Option<DynamicImage>,
|
||||
room_name: Option<String>,
|
||||
palette: SelectedPalette,
|
||||
dither: bool,
|
||||
brightness: i32,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref STATE: Mutex<State> = Mutex::new(
|
||||
State {
|
||||
game: None,
|
||||
image: None,
|
||||
room_name: None,
|
||||
palette: SelectedPalette::None,
|
||||
dither: true,
|
||||
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: bool) {
|
||||
let mut state = STATE.lock().unwrap();
|
||||
state.dither = dither;
|
||||
}
|
||||
|
||||
#[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 {
|
||||
image::imageops::dither(&mut buffer, &colour_map);
|
||||
} else {
|
||||
// just do colour indexing
|
||||
let 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);
|
||||
|
||||
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
|
@ -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
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 523 B |
|
@ -0,0 +1 @@
|
|||
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABHNCSVQICAgIfAhkiAAAAcJJREFUeJzt19FqgzAAQFEz+v+/nD1t2NLWsQgW7jlP2ogVvUYdc865LRhj/C7/7Gr/29678cfDeNxmdZznbtv2vwu2H39cPtr26H/mnNuc8259ZZzXvs7a0dHJdjE+022/sjKFrzh6jKyO89ppM8AZrgqw7C6AMcbbk3s0vmL1BfLZewDHPmIGcOdfZ5z1GbjymfbqEI7u5nf7EMvfjG3bzJVhH/EI4DoCiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHEfQM+SHkFb+/YEwAAAABJRU5ErkJggg==
|
Loading…
Reference in New Issue