Compare commits
90 Commits
72dbff8d5e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c444cb35d | |||
| a2c92b1e12 | |||
| 57b841ac3d | |||
| 3bbf51e5f8 | |||
| 7af54d938b | |||
| f224fe1e27 | |||
| e6fda7266e | |||
| f223e51195 | |||
| d72a4df55e | |||
| 0f23b878b2 | |||
| 23b99b8ea7 | |||
| c8b6c772ab | |||
| 52728e601b | |||
| 4bd6286cf0 | |||
| d53244a884 | |||
| f6678f28a9 | |||
| b157007ff3 | |||
| 704f710047 | |||
| 5c4259d222 | |||
| 47a9bf5c79 | |||
| d100473542 | |||
| d0946d3ab5 | |||
| 5ee1e908f1 | |||
| 5788baa0b8 | |||
| f135e184e4 | |||
| 3e7d6eeaa5 | |||
| f6308110be | |||
| 2d73963aa0 | |||
| 6f8e00130c | |||
| b478b1e3ee | |||
| 055928eb7b | |||
| b3690c4dd7 | |||
| da04534fd9 | |||
| 3d1129c613 | |||
| 3b851975d0 | |||
| afc626bae0 | |||
| 6e43249d64 | |||
| 458604cd1a | |||
| bb0b970281 | |||
| f62202cb74 | |||
| 745b18dfc0 | |||
| e5a87f854e | |||
| c2787db422 | |||
| 7d274bb3c2 | |||
| fbe40fb866 | |||
| 7ff67e51f8 | |||
| 80df4a9a6a | |||
| e76ff57053 | |||
| 0ef2d2acd9 | |||
| f9b0f6b6db | |||
| 8c92b05b74 | |||
| 512f386c25 | |||
| 63cb971aca | |||
| 03ce88e015 | |||
| 8a8b861e5d | |||
| c1c4fca1db | |||
| e5da032236 | |||
| d04eaf892b | |||
| daf9b21096 | |||
| 34053a1327 | |||
| 21eb632d22 | |||
| 9eb090924a | |||
| 9261c41cd8 | |||
| 6a58e8c003 | |||
| aea19fcd6c | |||
| db3cdf42ff | |||
| 31d7ff52ca | |||
| 45ef41c366 | |||
| 3a8e349aac | |||
| 080d0853eb | |||
| 0150a5ca33 | |||
| ec44370d7e | |||
| 013e1e1c8c | |||
| 37cde1713d | |||
| 0780bc8267 | |||
| fd5f141f19 | |||
| 5916c4af17 | |||
| 69a3b482be | |||
| 4bd896f05a | |||
| 03588c4c55 | |||
| 0cb4f3af8d | |||
| 1b3505929b | |||
| 6b679c3bbc | |||
| ca93a00eda | |||
| 4c958373ba | |||
| 65257b3886 | |||
| 196ec06c88 | |||
| 4dbcd6ac66 | |||
| 8a8743de93 | |||
| bf17c08909 |
17
.gitattributes
vendored
17
.gitattributes
vendored
@@ -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
13
.gitignore
vendored
@@ -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
|
||||
|
||||
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "pixsy"
|
||||
version = "0.710.0"
|
||||
description = "convert images to Bitsy rooms"
|
||||
authors = ["Max Bradbury <max@tinybird.info>"]
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
repository = "https://tinybird.dev/max/pixsy"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
base64 = "^0.12.3"
|
||||
bitsy-parser = "^0.710.0"
|
||||
image = "^0.23.7"
|
||||
json = "^0.12.4"
|
||||
lazy_static = "^1.4.0"
|
||||
wasm-bindgen = "^0.2.78"
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal 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
57
README.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
1
includes/croppie.min.js
vendored
Normal file
1
includes/croppie.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,144 +1,138 @@
|
||||
@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%;
|
||||
|
||||
&.half {
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
|
||||
160
index.pug
160
index.pug
@@ -1,96 +1,118 @@
|
||||
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
|
||||
p.
|
||||
#[a(href="https://github.com/synth-ruiner/bitsy-image-to-room") about]
|
||||
version 0.710.0
|
||||
|
|
||||
#[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="https://twitter.com/synth_ruiner") twitter]
|
||||
.pages
|
||||
.page#start
|
||||
p.
|
||||
#[b pixsy] is a tool for #[a(href="https://bitsy.org/") Bitsy Game Maker]
|
||||
that allows you to generate a room from an image and add it to your game.
|
||||
p.
|
||||
this version is tested to be compatible with Bitsy version 7.10 and earlier.
|
||||
later versions may also work fine, but make sure you have a backup of your game data.
|
||||
p.
|
||||
#[b pixsy] does not currently work via the Itch desktop program.
|
||||
if pixsy does not work for you, please try the
|
||||
#[a(href="http://tinybird.info/image-to-bitsy/old/") old version] instead.
|
||||
p.
|
||||
if your image is already the correct size for a bitsy room (128×128),
|
||||
simply leave the zoom slider at the default setting.
|
||||
you can draw your room in a pixel-art program and import it here.
|
||||
p.
|
||||
full instructions can be found on the
|
||||
#[a(href="https://ruin.itch.io/pixsy/") itch.io page] -
|
||||
scroll down to "how to use".
|
||||
|
||||
.flex-container
|
||||
#game-data.section
|
||||
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
|
||||
p.
|
||||
your game data is available from the #[i game data] window in bitsy,
|
||||
under the #[i tools] dropdown.
|
||||
|
||||
p
|
||||
input.game-data(type="file")
|
||||
p paste or upload your game data (or html) here
|
||||
p (maybe make a backup first)
|
||||
input#game(type="file" accept=".bitsy,.txt" autocomplete="off")
|
||||
br
|
||||
|
||||
#image.section
|
||||
textarea#game-data(
|
||||
placeholder="Paste your game data here or use the file chooser button above"
|
||||
autocomplete="off"
|
||||
)
|
||||
|
||||
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")
|
||||
| dither
|
||||
p (approximates a greyscale effect)
|
||||
br
|
||||
|
||||
label(for="brightness") brightness
|
||||
button.pagination.prev#back-to-image previous
|
||||
button.pagination.next#room-next add room
|
||||
.page.download
|
||||
h2 done!
|
||||
|
||||
//- to do
|
||||
input#dithering(type="checkbox")
|
||||
label(for="dithering") dithering
|
||||
p#added
|
||||
|
||||
//- to do
|
||||
input#smoothing(type="checkbox")
|
||||
label(for="smoothing") smoothing
|
||||
textarea#output(autocomplete="off")
|
||||
|
||||
#output.section
|
||||
h2 output
|
||||
button#clipboard.half copy to clipboard
|
||||
button#download.half download
|
||||
|
||||
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
|
||||
|
||||
input#roomName(type="text", placeholder="room name")
|
||||
|
||||
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
|
||||
|
||||
250
old/includes/croppie.css
Normal file
250
old/includes/croppie.css
Normal 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: '↻';
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
BIN
old/includes/snowy-owls.png
Normal file
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
144
old/includes/style.less
Normal 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;
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
289
script.js
Normal file
289
script.js
Normal file
@@ -0,0 +1,289 @@
|
||||
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 copyToClipboard() {
|
||||
const button = el("clipboard");
|
||||
|
||||
el("output").select();
|
||||
document.execCommand("copy");
|
||||
button.innerText = "copied!";
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerText = "copy to clipboard";
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
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 buttonCopyToClipboard = el("clipboard")
|
||||
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;
|
||||
checkGameData();
|
||||
}, "text");
|
||||
});
|
||||
|
||||
function setPaletteDropdown() {
|
||||
const palettes = JSON.parse(get_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() {
|
||||
let result = load_game(textareaGameDataInput.value)
|
||||
|
||||
if (result === "Loaded game") {
|
||||
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);
|
||||
}
|
||||
|
||||
buttonCopyToClipboard.addEventListener("click", copyToClipboard);
|
||||
buttonCopyToClipboard.addEventListener("touchend", copyToClipboard);
|
||||
|
||||
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
74
src/colour_map.rs
Normal 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];
|
||||
}
|
||||
}
|
||||
393
src/lib.rs
Normal file
393
src/lib.rs
Normal file
@@ -0,0 +1,393 @@
|
||||
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: false,
|
||||
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_rgba8();
|
||||
|
||||
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".into();
|
||||
}
|
||||
|
||||
match &state.palette {
|
||||
SelectedPalette::None => { return "No palette selected".into(); },
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let mut game = state.game.clone().unwrap();
|
||||
|
||||
if state.image.is_none() {
|
||||
return "No image loaded".into();
|
||||
}
|
||||
|
||||
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.blueprint").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"));
|
||||
}
|
||||
}
|
||||
BIN
src/test-resources/colour_input.png
Normal file
BIN
src/test-resources/colour_input.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
1
src/test-resources/colour_input.png.base64
Normal file
1
src/test-resources/colour_input.png.base64
Normal file
File diff suppressed because one or more lines are too long
1
src/test-resources/colour_input.png.base64.blueprint
Normal file
1
src/test-resources/colour_input.png.base64.blueprint
Normal file
@@ -0,0 +1 @@
|
||||
data:image/png;base64,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
|
||||
159
src/test-resources/expected.bitsy
Normal file
159
src/test-resources/expected.bitsy
Normal file
@@ -0,0 +1,159 @@
|
||||
Write your game's title here
|
||||
|
||||
# BITSY VERSION 7.5
|
||||
|
||||
! 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
BIN
src/test-resources/test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 523 B |
1
src/test-resources/test.png.base64
Normal file
1
src/test-resources/test.png.base64
Normal file
@@ -0,0 +1 @@
|
||||
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABHNCSVQICAgIfAhkiAAAAcJJREFUeJzt19FqgzAAQFEz+v+/nD1t2NLWsQgW7jlP2ogVvUYdc865LRhj/C7/7Gr/29678cfDeNxmdZznbtv2vwu2H39cPtr26H/mnNuc8259ZZzXvs7a0dHJdjE+022/sjKFrzh6jKyO89ppM8AZrgqw7C6AMcbbk3s0vmL1BfLZewDHPmIGcOdfZ5z1GbjymfbqEI7u5nf7EMvfjG3bzJVhH/EI4DoCiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHEfQM+SHkFb+/YEwAAAABJRU5ErkJggg==
|
||||
Reference in New Issue
Block a user