$(document).ready(function() { var bitsyData = {}; var palette = { id: 0, background: { red: 62, green: 43, blue: 32, }, tile: { red: 208, green: 112, blue: 56, }, sprite: { red: 229, green: 92, blue: 68, } }; var room = []; var tiles = []; var croptions = { url: 'https://i.imgur.com/ThQZ94v.jpg', viewport: {width: 128, height: 128, type: 'square'}, boundary: {width: 256, height: 256}, zoom: 0 } var $croppie = $('#croppie'); var croppie = $croppie.croppie(croptions); function colourDifference(colour1, colour2) { difference = {}; _.each(['red', 'green', 'blue'], function(key) { difference[key] = Math.abs(colour1[key] - colour2[key]); }); // sum rgb differences return _.reduce(difference, function(sum, n) { return sum + n; }, 0); } function zeroPad(input, desiredLength) { while (input.length < desiredLength) { input = "0" + input; } return input; } function colourToHex(colour) { return '#' + zeroPad(Number(colour.red ).toString(16), 2) + zeroPad(Number(colour.green).toString(16), 2) + zeroPad(Number(colour.blue ).toString(16), 2); } function hexToColour(hex) { var rgb = hex.match(/[\da-fA-F]{2}/g); return { red: parseInt(rgb[0], 16), green: parseInt(rgb[1], 16), blue: parseInt(rgb[2], 16), }; } function getClosestColour(initialColour, colourOptions) { // ditch sprite colour as we're not using it atm delete colourOptions.sprite; _.each(palette, function(colour, name) { colourOptions[name].name = name; colourOptions[name].difference = colourDifference(initialColour, colour); }); // lowest difference (closest) wins return _.first(_.sortBy(colourOptions, 'difference')); } function handleBitsyGameData() { bitsyData = {}; var input = $('#bitsy-data').val(); // get palettes var palettes = input.match(/PAL (.*)\s(NAME (.*)\s)?([0-9,]*[\s]){3}/g); bitsyData.palettes = {}; _.each(palettes, function(palette, n) { var thisPalette = {}; var name = ""; if (palette.match(/NAME (.+)\n/)) { name = palette.match(/NAME (.+)\n/)[0].replace('NAME ', ''); } else if (palette.match(/PAL (\d+)\n/)) { name = palette.match(/PAL (\d+)\n/)[0].replace("PAL", "palette"); } var colours = palette.match(/\d+,\d+,\d+/g); colours = _.map(colours, function(colour) { var rgb = colour.split(','); return {red: rgb[0], green: rgb[1], blue: rgb[2]}; }); bitsyData.palettes[name] = { id: n, background: colours[0], tile: colours[1], sprite: colours[2], } }); // get 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) }; var tiles = input.match(/TIL (.*)\n([01]{8}\n){8}/g); if (tiles.length > 0) { for (var i = 0; i < tiles.length; i++) { var name = tiles[i].match(/TIL .*/)[0].replace('TIL ', ''); tiles[i] = tiles[i].replace(/TIL .*\n/, ''); var bitmap = tiles[i].match(/[01]/g); bitsyData.tiles[name] = { name: name, bitmap: _.chunk(bitmap, 8) }; } } // set palette to first imported palette and redraw palette = _.first(_.sortBy(bitsyData.palettes, 'id')); renderResult(); // update palette picker $('tr.palette').remove(); _.each(bitsyData.palettes, function(palette, name) { $('#palette tbody').append( '' + '' + '' + '' + '' + '' + '' + '' + '' + '' ); }); $('tr.palette input[type="radio"]').eq(0).trigger('click'); } function readFile(input) { if (input.files && input.files[0]) { var reader = new FileReader(); reader.onload = function (e) { $croppie.croppie('bind', { url: e.target.result, zoom: 0 }); } reader.readAsDataURL(input.files[0]); } } var renderResult = _.debounce(function() { $croppie.croppie('result', { type: 'rawcanvas', size: 'viewport' }).then(function (result) { var imageData = result.getContext('2d').getImageData(0, 0, 128, 128); var rawData = imageData.data; var monochrome = []; brightnessAdjustment = parseFloat($('#brightness').val()); // for each pixel for (var i = 0; i < rawData.length; i += 4) { // this brightness adjustment is pretty crude but whatever var pixel = { red: Math.min(rawData[i ] + brightnessAdjustment, 255), green: Math.min(rawData[i + 1] + brightnessAdjustment, 255), blue: Math.min(rawData[i + 2] + brightnessAdjustment, 255), }; var targetColour = getClosestColour(pixel, palette); if (targetColour.name === "background") { monochrome.push(0); } else { // tile monochrome.push(1) } rawData[i ] = targetColour.red; rawData[i + 1] = targetColour.green; rawData[i + 2] = targetColour.blue; rawData[i + 3] = 255; } // split monochrome bitmap into equal chunks for easier x:y access monochrome = _.chunk(monochrome, 128); document.getElementById('preview').getContext('2d').putImageData(imageData, 0, 0); // tiled output room = []; _.times(16, function(tileY) { _.times(16, function(tileX) { // make pseudo-tile from monochrome bitmap var pseudoTile = []; _.times(8, function(y) { pseudoTile.push( _.slice(monochrome[(tileY * 8) + y], (tileX * 8), (tileX * 8) + 8) ); }) var tilesForMatch = bitsyData.tiles; _.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++; } }); }); }); var bestMatch = _.first(_.sortBy(tilesForMatch, 'match')); // if best match is under threshold // turn pseudo-tile into a real tile and add it to the tile data 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); position *= 4; // 4 values (rgba) per pixel if (parseInt(pixel) === 1) { // ?! wtf pixelColour = palette.background; } else { pixelColour = palette.tile; } 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); // make this not debounced but called every n milliseconds $('#brightness').on('change', renderResult); $('#brightness').on('dblclick', function() { $(this).val(0); renderResult(); }); $('label[for="brightness"]').on('click touchdown', function() { $('#brightness').trigger('dblclick'); }); $('#bitsy-data').on('change blur keyup', handleBitsyGameData); handleBitsyGameData(); $('#imageUpload').on('change', function () { readFile(this); }); // these inputs get added and removed from the DOM so the event handler needs to be on the document $(document).on('change', '#palette input', function() { // if this is a colour input, update the palette if ($(this).attr('type') === 'color') { palette[$(this).attr('name')] = hexToColour($(this).val()); } // if this is a radio button, pick this palette if ($(this).attr('type') === 'radio') { palette.id = parseInt( $(this).closest('.palette').find('input[name="id"]' ).val()); palette.background = hexToColour($(this).closest('.palette').find('input[name="background"]').val()); palette.tile = hexToColour($(this).closest('.palette').find('input[name="tile"]' ).val()); // sprite colour is not currently used } renderResult(); }); $('#save').on('click touchend', function() { var newGameData = $('textarea').val(); // need to import IDs so we don't give the new room a conflicting ID var roomNames = newGameData.match(/ROOM \d+/g); var newRoomId = parseInt(_.last(roomNames).replace(/[^\d]+/g, "")) + 1; var newRoomName = $('#roomName').val(); // remove invalid chars? what's invalid? newlines? are those possible? var newRoom = "ROOM " + newRoomId + "\n"; _.each(room, function(row) { newRoom += _.toString(row) + "\n"; }); if (newRoomName) { newRoom += "NAME " + newRoomName + "\n"; } newRoom += "PAL " + palette.id + "\n"; // write $('textarea').val(newGameData.replace(/(ROOM .*\n(.*\n)*PAL .*)/g, '$1\n\n' + newRoom)); }); });