allow for creating new tiles

(does not yet add to game data)
This commit is contained in:
synth-ruiner 2017-12-29 15:49:18 +00:00
parent e2d5d33ac7
commit 948fccb5a3
6 changed files with 152 additions and 68 deletions

View File

@ -1,6 +1,7 @@
$(document).ready(function() {
// todo define things like 16x16, 128x128 etc. as constants?
// also script debounce/throttle times
const animationTime = 400; // defined in bitsy.js
var bitsyData = {};
@ -27,6 +28,8 @@ $(document).ready(function() {
var tiles = [];
var tileMatchThreshold = 0;
var croptions = {
url: 'https://i.imgur.com/ThQZ94v.jpg',
viewport: {width: 128, height: 128, type: 'square'},
@ -87,6 +90,19 @@ $(document).ready(function() {
return _.first(_.sortBy(colourOptions, 'difference'));
}
function newTileName() {
var tileNames = _.map(bitsyData.tiles, 'name');
var i = 1; // start with 1 as 0 is an implicit tile
while (tileNames.indexOf(i.toString(36)) > -1) {
i++;
}
// base 36 = 0-9a-z
return i.toString(36);
}
function handleBitsyGameData() {
bitsyData = {};
@ -127,14 +143,15 @@ $(document).ready(function() {
// get tiles
bitsyData.tiles = {};
bitsyData.tiles = [];
// tile 0 (background colour only) is implicit in bitsy rather than being stored in the game data
// so, make our own version
bitsyData.tiles[0] = {
name: 0,
bitmap: _.chunk(_.split(_.repeat(0, 64), ''), 8)
};
bitsyData.tiles.push({
name: "0",
bitmap: _.chunk(_.times(64, _.constant(0)), 8),
new: false // this could also be used to stop it from being added to the game data, wooo
});
// todo: handle animated tiles properly instead of discarding the second animation frame
var tiles = input.match(/TIL (.*)\n([01]{8}\n){8}(>\n([01]{8}\n){8})?/g); // everything after > is an optional second animation frame
@ -144,9 +161,14 @@ $(document).ready(function() {
tile = tile.replace(/TIL .*\n/, '');
var bitmap = tile.match(/[01]/g);
var bitmap = _.map(tile.match(/[01]/g), _.toInteger);
var newTile = {name: name};
var newTile = {
name: name,
new: false
};
// todo make this agnostic? i.e. tile.frames = _.chunk(bitmap, 64)
if (bitmap.length === 64) { // normal tile
newTile.bitmap = _.chunk(bitmap, 8);
@ -155,7 +177,7 @@ $(document).ready(function() {
newTile.secondAnimationFrame = _.chunk(_.takeRight(bitmap, 64), 8);
}
bitsyData.tiles[name] = newTile;
bitsyData.tiles.push(newTile);
});
if (_.find(bitsyData.palettes, {'id': palette.id})) {
@ -172,7 +194,7 @@ $(document).ready(function() {
palette = _.first(_.sortBy(bitsyData.palettes, 'id'));
}
renderResult();
renderDebounced();
// update palette picker
$('tr.palette').remove();
@ -210,7 +232,7 @@ $(document).ready(function() {
}
}
var renderResult = _.debounce(function() {
function render() {
$croppie.croppie('result', {
type: 'rawcanvas',
size: 'viewport'
@ -232,10 +254,10 @@ $(document).ready(function() {
var targetColour = getClosestColour(pixel, palette);
if (targetColour.name === "background") { // ?! why is this reversed?
monochrome.push(1);
if (targetColour.name === "background") {
monochrome.push(0);
} else { // tile
monochrome.push(0)
monochrome.push(1)
}
rawData[i ] = targetColour.red;
@ -262,23 +284,16 @@ $(document).ready(function() {
pseudoTile.push(
_.slice(monochrome[(tileY * 8) + y], (tileX * 8), (tileX * 8) + 8)
);
})
});
var tilesForMatch = bitsyData.tiles;
_.each(tilesForMatch, function(tile) {
tile.match = 0;
// if we want to always create new tiles, don't bother trying to check matches
if (tileMatchThreshold < 64) {
_.each(tilesForMatch, function(tile) {
tile.match = 0;
_.each(tile.bitmap, function(row, y) {
_.each(row, function(pixel, x) {
if (parseInt(pixel) === parseInt(pseudoTile[y][x])) {
tile.match++;
}
});
});
if (tile.secondAnimationFrame) {
_.each(tile.secondAnimationFrame, function(row, y) {
_.each(tile.bitmap, function(row, y) {
_.each(row, function(pixel, x) {
if (parseInt(pixel) === parseInt(pseudoTile[y][x])) {
tile.match++;
@ -286,62 +301,100 @@ $(document).ready(function() {
});
});
tile.match /= 2;
}
});
if (tile.secondAnimationFrame) {
_.each(tile.secondAnimationFrame, function(row, y) {
_.each(row, function(pixel, x) {
if (parseInt(pixel) === parseInt(pseudoTile[y][x])) {
tile.match++;
}
});
});
var bestMatch = _.first(_.sortBy(tilesForMatch, 'match'));
tile.match /= 2;
}
});
}
// if best match is under threshold
// what if there are several equally good matches?
// find highest match amount and find all of them
var bestMatchAmount = _.last(_.sortBy(tilesForMatch, ['match'])).match;
var bestMatches = _.filter(tilesForMatch, {'match': bestMatchAmount});
// sort by name in ascending order
// earlier names are preferable
var bestMatch = _.first(_.sortBy(bestMatches, 'name'));
if (tileMatchThreshold === 64 || bestMatch.match <= tileMatchThreshold) {
// turn pseudo-tile into a real tile and add it to the tile data
room.push(bestMatch.name);
var name = newTileName();
bitsyData.tiles.push({
name: name,
bitmap: pseudoTile,
new: true
});
room.push(name);
// issue with this approach:
// what if a tile we add late in the loop is a better match for an earlier "good enough" match?
// this would also cause different results if the user were to add the same room several times
// we could keep iterating until the room no longer changes
} else {
room.push(bestMatch.name);
}
});
});
room = _.chunk(room, 16);
// write room to output
imageData = document.getElementById("room-output").getContext('2d').getImageData(0, 0, 128, 128);
rawData = imageData.data;
_.each(room, function(row, tileY) {
_.each(row, function(tileName, tileX) {
if (_.get(bitsyData, 'tiles.' + tileName + '.bitmap')) {
_.each(bitsyData.tiles[tileName].bitmap, function(row, y) {
_.each(row, function(pixel, x) {
var position = (((tileY * 8) + y) * 128) + ((tileX * 8) + x);
var tile = _.find(bitsyData.tiles, {'name' : tileName});
position *= 4; // 4 values (rgba) per pixel
_.each(tile.bitmap, function(row, y) {
_.each(row, function(pixel, x) {
var position = (((tileY * 8) + y) * 128) + ((tileX * 8) + x);
if (parseInt(pixel) === 0) {
var pixelColour = palette.background;
} else {
var pixelColour = palette.tile;
}
position *= 4; // 4 values (rgba) per pixel
rawData[position ] = pixelColour.red;
rawData[position + 1] = pixelColour.green;
rawData[position + 2] = pixelColour.blue;
rawData[position + 3] = 255;
});
var pixelColour = {};
switch(parseInt(pixel)) {
case 0: pixelColour = palette.background; break;
case 1: pixelColour = palette.tile; break;
default: console.log("error");
}
rawData[position ] = pixelColour.red;
rawData[position + 1] = pixelColour.green;
rawData[position + 2] = pixelColour.blue;
rawData[position + 3] = 255;
});
}
});
});
});
document.getElementById('room-output').getContext('2d').putImageData(imageData, 0, 0);
});
}, 30);
}
$croppie.on('update', renderResult);
var renderDebounced = _.debounce(render, 30);
// make this not debounced but called every n milliseconds
$('#brightness').on('change', renderResult);
$croppie.on('update', renderDebounced);
$('#brightness').on('change', renderDebounced);
$('#brightness').on('dblclick', function() {
$(this).val(0);
renderResult();
renderDebounced();
});
$('label[for="brightness"]').on('click touchdown', function() {
@ -375,7 +428,20 @@ $(document).ready(function() {
// sprite colour is not currently used
}
renderResult();
renderDebounced();
});
$(document).on('change', '#threshold', function() {
var newValue = parseInt($(this).val());
if (newValue < tileMatchThreshold) {
// set tiles back to default
bitsyData.tiles = _.filter(bitsyData.tiles, ['new', false]);
}
tileMatchThreshold = newValue;
renderDebounced();
});
$('#save').on('click touchend', function() {

View File

@ -74,10 +74,12 @@ input {
background-color: #d3cbd0;
color: #594a54;
}
#brightness,
#threshold {
#brightness {
width: 256px;
}
#threshold {
width: 150px;
}
#brightness + label,
#threshold + label {
margin: 0 auto;

View File

@ -95,13 +95,20 @@ textarea, input {
color: @dark;
}
#brightness {
width: 256px;
}
#threshold {
width: 150px;
}
#brightness, #threshold {
+ label{
.centre;
}
// todo make this match the croppie slider or vice versa
width: 256px;
}
#imageUpload {

View File

@ -5229,4 +5229,4 @@ DLG ITM_0
You found a nice warm cup of tea
VAR a
42</textarea></div><div class="section" id="image"><h2>image</h2><div id="croppie"></div><input id="imageUpload" type="file" accepts="image/*"></div><div class="section" id="palette"><h2>palette</h2><form id="palettes"><table><tbody></tbody></table></form></div><div class="section" id="crop"><h2>preview</h2><canvas id="preview" width="128" height="128"></canvas><input id="brightness" type="range" min="-255" max="255" value="0"><label for="brightness">brightness</label></div><div class="section" id="output"><h2>output</h2><canvas id="room-output" width="128" height="128"></canvas><input id="roomName" type="text" placeholder="room name"><button id="save">write to game data</button></div></body></html>
42</textarea></div><div class="section" id="image"><h2>image</h2><div id="croppie"></div><input id="imageUpload" type="file" accepts="image/*"></div><div class="section" id="palette"><h2>palette</h2><form id="palettes"><table><tbody></tbody></table></form></div><div class="section" id="crop"><h2>preview</h2><canvas id="preview" width="128" height="128"></canvas><input id="brightness" type="range" min="-255" max="255" value="0"><label for="brightness">brightness</label><br><br><label>never </label><input id="threshold" type="range" min="0" max="64" value="0"><label> always</label><br><label for="threshold">create new tiles</label></div><div class="section" id="output"><h2>output</h2><canvas id="room-output" width="128" height="128"></canvas><input id="roomName" type="text" placeholder="room name"><button id="save">write to game data</button></div></body></html>

View File

@ -58,6 +58,16 @@ html
label(for="brightness") brightness
br
br
label never
input#threshold(type="range" min=0 max=64 value=0)
label always
br
label(for="threshold") create new tiles
br
//- to do
input#dithering(type="checkbox")
label(for="dithering") dithering
@ -76,11 +86,6 @@ html
button#save write to game data
//- to do
input#threshold(type="range" min=0 max=64)
br
label(for="threshold")
span.left use existing tiles
span.right create new tiles
//-label favour broad strokes | favour details

View File

@ -42,7 +42,8 @@ if (src.match(/^(file|https)?:\/\/|^\/\//)) {
## to do
* allow user to save output as image, or tweet it :) *user can currently right-click -> Save As but the 128x128 size is not great*
* allow user to save output as image, or tweet it :)
* *user can currently right-click -> Save As but the 128x128 size is not great*
* create new tiles based on image
* only add unique new tiles
* don't write the 0 tile (implicit background-only tile)
@ -64,4 +65,7 @@ if (src.match(/^(file|https)?:\/\/|^\/\//)) {
* allow user to draw to canvas
* do a 'branching tree' approach to finding the closest tile? i.e. create a 1x1, 2x2, 4x4 version of each tile, so all the broadly darker tiles will sit under '0' and lighter tiles under '1', then tiles that are lighter at the top will sit under '1100', etc... I'm not sure how much more effective this will be or whatever it will give better/faster results but it's worth a try
* give heavier weighting to edge pixels when finding a matching tile? (thanks Mark)
* apply grid lines to preview?
* apply grid lines to preview
* optionally add inverted versions of existing tiles if they are a better match
* make new tiles out of fragments of existing tiles instead of directly copying from bitmap
* allow user to zoom out so the image is letterboxed/windowboxed/etc.