Compare commits

..

No commits in common. "f135e184e4d8cafb89c8ad1e66539401f5d8d8fc" and "72dbff8d5eb84feb4e4f0ee23e90e8fb8b7647c3" have entirely different histories.

36 changed files with 229 additions and 1536 deletions

17
.gitattributes vendored Normal file
View File

@ -0,0 +1,17 @@
# 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

11
.gitignore vendored
View File

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

View File

@ -1,19 +0,0 @@
[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...

21
LICENSE
View File

@ -1,21 +0,0 @@
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.

View File

@ -1,3 +0,0 @@
# pixsy
convert images to rooms for use in Bitsy

View File

@ -1,6 +0,0 @@
# todo
* tile reuse
* noise reduction (remove lonely pixels)
* implement Atkinson and Bayer dithering options
* fix weird problem with pixels flipping (see test::example)

1
_config.yml Normal file
View File

@ -0,0 +1 @@
theme: jekyll-theme-dinky

View File

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

View File

@ -1,6 +1,4 @@
#! /usr/bin/env bash #!/usr/bin/env bash
rm -rf dist zip -r image-to-bitsy.zip readme.md index.html includes/
mkdir dist butler push image-to-bitsy.zip ruin/image-to-bitsy:html
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

View File

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

View File

@ -1,133 +1,144 @@
@background: #57506a; @font-face {
@page-background: #968eb5; font-family: 'rubikregular';
@accent: #ec6d7d; src: url('rubik-regular-webfont.woff2') format('woff2'),
@text: #464256; url('rubik-regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@light: #d3cbd0;
@dark: #594a54;
@accent: #f69f8f; // pink
* { * {
box-sizing: border-box; box-sizing: border-box;
color: @text; font-family: "rubikregular", sans-serif;
margin: 0 auto 0.5em auto;
text-align: center; text-align: center;
} }
html, body { html, body {
background-color: @background; background-color: @dark;
font-size: 3vmin; color: @light;
margin: 0;
padding: 0;
} }
button { a {
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;
} }
h1 { canvas {
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;
} }
h3 { input[type="file"] {
font-size: 0.9em; padding: 1em 0;
width: 256px;
} }
input { input[type="text"], button {
width: 100%; width: 14em;
text-align: left; margin: 1em;
padding: 0.25em;
font-size: 1em;
}
&[type="checkbox"] { table {
width: auto; margin: 0 auto;
margin-right: 1em;
position: relative; td, th {
top: 0.25em; width: 50%;
left: 0.25em; }
td {
text-align: left;
}
th {
text-align: right;
} }
} }
img {
max-width: 100%;
margin: 0;
}
p {
font-size: 0.8em;
margin: 0 auto 1em auto;
}
label {
font-size: 0.75em;
font-weight: bold;
}
select {
width: 100%;
}
textarea { textarea {
height: 15em; .box256;
padding: 0.5em;
font-family: monospace;
}
textarea, input {
text-align: left; text-align: left;
width: 100%;
} }
.background { .box256 {
background-color: @background; height: 256px;
padding: 0.5em; width: 256px;
border-radius: 0.5em;
} }
.checkboxes label { .centre {
margin-right: 1em; margin: 0 auto;
} }
.cropper-tools { .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; margin-top: 0;
} }
.half {
display: inline-block;
text-align: left;
width: 50%;
}
.image-container {
height: 46vh;
text-align: left;
}
.page {
height: 80vmin;
width: 80vmin;
background-color: @page-background;
color: @text;
border-radius: 5vmin;
box-shadow: @accent 1vmin 1vmin;
padding: 5vmin;
position: relative;
}
#preview {
width: 256px;
height: 256px;
image-rendering: pixelated;
image-rendering: crisp-edges;
}

138
index.pug
View File

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

View File

@ -1,250 +0,0 @@
.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: '↻';
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@ -1,144 +0,0 @@
@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 Normal file
View File

@ -0,0 +1,14 @@
{
"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
View File

@ -1,273 +0,0 @@
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();

View File

@ -1,74 +0,0 @@
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];
}
}

View File

@ -1,395 +0,0 @@
#![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.

Before

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

@ -1,159 +0,0 @@
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.

Before

Width:  |  Height:  |  Size: 523 B

View File

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