diff --git a/includes/script.js b/includes/script.js index 10ee0fc..78323ad 100644 --- a/includes/script.js +++ b/includes/script.js @@ -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,15 +143,16 @@ $(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() { diff --git a/includes/style.css b/includes/style.css index 327cd4f..e80ec04 100644 --- a/includes/style.css +++ b/includes/style.css @@ -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; diff --git a/includes/style.less b/includes/style.less index c4dcc32..4ba3cf8 100644 --- a/includes/style.less +++ b/includes/style.less @@ -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 { diff --git a/index.html b/index.html index 5fae3f4..a9f25f6 100644 --- a/index.html +++ b/index.html @@ -5229,4 +5229,4 @@ DLG ITM_0 You found a nice warm cup of tea VAR a -42

image

palette

preview

output

\ No newline at end of file +42

image

palette

preview




output

\ No newline at end of file diff --git a/index.pug b/index.pug index cd121c1..c18cb54 100644 --- a/index.pug +++ b/index.pug @@ -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 diff --git a/readme.md b/readme.md index 76027d2..5a727ae 100644 --- a/readme.md +++ b/readme.md @@ -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.