2017-12-22 17:22:08 +00:00
|
|
|
$(document).ready(function() {
|
|
|
|
var bitsyData = {};
|
|
|
|
|
|
|
|
var palette = {
|
2017-12-24 00:47:00 +00:00
|
|
|
id: 0,
|
2017-12-22 17:22:08 +00:00
|
|
|
background: {
|
|
|
|
red: 62,
|
|
|
|
green: 43,
|
|
|
|
blue: 32,
|
|
|
|
},
|
|
|
|
tile: {
|
|
|
|
red: 208,
|
|
|
|
green: 112,
|
|
|
|
blue: 56,
|
|
|
|
},
|
|
|
|
sprite: {
|
|
|
|
red: 229,
|
|
|
|
green: 92,
|
|
|
|
blue: 68,
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-12-23 00:57:52 +00:00
|
|
|
var room = [];
|
|
|
|
|
|
|
|
var tiles = [];
|
|
|
|
|
2017-12-22 17:22:08 +00:00
|
|
|
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) {
|
2017-12-24 18:14:16 +00:00
|
|
|
return sum + n;
|
2017-12-22 17:22:08 +00:00
|
|
|
}, 0);
|
|
|
|
}
|
|
|
|
|
2017-12-23 16:34:16 +00:00
|
|
|
function zeroPad(input, desiredLength) {
|
|
|
|
while (input.length < desiredLength) {
|
|
|
|
input = "0" + input;
|
|
|
|
}
|
|
|
|
|
|
|
|
return input;
|
|
|
|
}
|
|
|
|
|
2017-12-22 17:22:08 +00:00
|
|
|
function colourToHex(colour) {
|
2017-12-23 16:34:16 +00:00
|
|
|
return '#' + zeroPad(Number(colour.red ).toString(16), 2)
|
|
|
|
+ zeroPad(Number(colour.green).toString(16), 2)
|
|
|
|
+ zeroPad(Number(colour.blue ).toString(16), 2);
|
2017-12-22 17:22:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function hexToColour(hex) {
|
2017-12-24 18:14:16 +00:00
|
|
|
var rgb = hex.match(/[\da-f]{2}/gi);
|
2017-12-22 17:22:08 +00:00
|
|
|
|
|
|
|
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) {
|
2017-12-23 00:57:52 +00:00
|
|
|
colourOptions[name].name = name;
|
2017-12-22 17:22:08 +00:00
|
|
|
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] = {
|
2017-12-24 00:47:00 +00:00
|
|
|
id: n,
|
2017-12-22 17:22:08 +00:00
|
|
|
background: colours[0],
|
|
|
|
tile: colours[1],
|
|
|
|
sprite: colours[2],
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-12-23 00:57:52 +00:00
|
|
|
// get tiles
|
2017-12-24 18:14:16 +00:00
|
|
|
|
2017-12-23 00:57:52 +00:00
|
|
|
bitsyData.tiles = {};
|
|
|
|
|
2017-12-24 18:12:46 +00:00
|
|
|
// 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)
|
|
|
|
};
|
|
|
|
|
2017-12-24 18:14:16 +00:00
|
|
|
// todo: handle animated tiles properly instead of discarding the second animation frame
|
2017-12-23 00:57:52 +00:00
|
|
|
var tiles = input.match(/TIL (.*)\n([01]{8}\n){8}/g);
|
|
|
|
|
2017-12-24 18:14:16 +00:00
|
|
|
_.each(tiles, function(tile, i) {
|
|
|
|
var name = tile.match(/TIL .*/)[0].replace('TIL ', '');
|
|
|
|
|
|
|
|
tile = tile.replace(/TIL .*\n/, '');
|
2017-12-23 00:57:52 +00:00
|
|
|
|
2017-12-24 18:14:16 +00:00
|
|
|
var bitmap = tile.match(/[01]/g);
|
2017-12-23 00:57:52 +00:00
|
|
|
|
2017-12-24 18:14:16 +00:00
|
|
|
bitsyData.tiles[name] = {
|
|
|
|
name: name,
|
|
|
|
bitmap: _.chunk(bitmap, 8)
|
|
|
|
};
|
|
|
|
});
|
2017-12-23 00:57:52 +00:00
|
|
|
|
2017-12-22 17:22:08 +00:00
|
|
|
// set palette to first imported palette and redraw
|
2017-12-24 00:47:00 +00:00
|
|
|
palette = _.first(_.sortBy(bitsyData.palettes, 'id'));
|
2017-12-22 17:22:08 +00:00
|
|
|
|
|
|
|
renderResult();
|
|
|
|
|
|
|
|
// update palette picker
|
|
|
|
$('tr.palette').remove();
|
|
|
|
|
|
|
|
_.each(bitsyData.palettes, function(palette, name) {
|
|
|
|
$('#palette tbody').append(
|
|
|
|
'<tr class="palette">'
|
2017-12-24 00:47:00 +00:00
|
|
|
+ '<td>'
|
|
|
|
+ '<input type="radio" name="palette" id="palette-' + name + '">'
|
|
|
|
+ '<input type="hidden" name="id" value="' + palette.id + '">'
|
|
|
|
+ '</td>'
|
2017-12-22 19:45:33 +00:00
|
|
|
+ '<td><label for="palette-' + name + '">' + name + '</label></td>'
|
2017-12-22 17:22:08 +00:00
|
|
|
+ '<td><input type="color" name="background" value="' + colourToHex(palette.background) + '"></td>'
|
|
|
|
+ '<td><input type="color" name="tile" value="' + colourToHex(palette.tile) + '"></td>'
|
2017-12-22 19:45:33 +00:00
|
|
|
+ '<td><input type="color" name="sprite" value="' + colourToHex(palette.sprite) + '" disabled></td>'
|
2017-12-22 17:22:08 +00:00
|
|
|
+ '</tr>'
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
$('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) {
|
2017-12-23 00:57:52 +00:00
|
|
|
var imageData = result.getContext('2d').getImageData(0, 0, 128, 128);
|
|
|
|
var rawData = imageData.data;
|
|
|
|
var monochrome = [];
|
2017-12-22 17:22:08 +00:00
|
|
|
|
2017-12-24 18:14:16 +00:00
|
|
|
var brightnessAdjustment = parseFloat($('#brightness').val());
|
2017-12-22 17:22:08 +00:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
2017-12-24 18:14:16 +00:00
|
|
|
if (targetColour.name === "background") { // ?! why is this reversed?
|
|
|
|
monochrome.push(1);
|
2017-12-23 00:57:52 +00:00
|
|
|
} else { // tile
|
2017-12-24 18:14:16 +00:00
|
|
|
monochrome.push(0)
|
2017-12-23 00:57:52 +00:00
|
|
|
}
|
|
|
|
|
2017-12-22 17:22:08 +00:00
|
|
|
rawData[i ] = targetColour.red;
|
|
|
|
rawData[i + 1] = targetColour.green;
|
|
|
|
rawData[i + 2] = targetColour.blue;
|
2017-12-23 00:57:52 +00:00
|
|
|
rawData[i + 3] = 255;
|
2017-12-22 17:22:08 +00:00
|
|
|
}
|
|
|
|
|
2017-12-23 00:57:52 +00:00
|
|
|
// 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
|
|
|
|
|
2017-12-23 20:20:55 +00:00
|
|
|
room = [];
|
|
|
|
|
2017-12-23 00:57:52 +00:00
|
|
|
_.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
|
|
|
|
|
2017-12-24 18:14:16 +00:00
|
|
|
if (parseInt(pixel) === 0) {
|
|
|
|
var pixelColour = palette.background;
|
2017-12-23 00:57:52 +00:00
|
|
|
} else {
|
2017-12-24 18:14:16 +00:00
|
|
|
var pixelColour = palette.tile;
|
2017-12-23 00:57:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
rawData[position ] = pixelColour.red;
|
|
|
|
rawData[position + 1] = pixelColour.green;
|
|
|
|
rawData[position + 2] = pixelColour.blue;
|
|
|
|
rawData[position + 3] = 255;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2017-12-23 22:33:31 +00:00
|
|
|
|
2017-12-23 00:57:52 +00:00
|
|
|
document.getElementById('room-output').getContext('2d').putImageData(imageData, 0, 0);
|
2017-12-22 17:22:08 +00:00
|
|
|
});
|
2017-12-23 00:57:52 +00:00
|
|
|
}, 30);
|
2017-12-22 17:22:08 +00:00
|
|
|
|
|
|
|
$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);
|
|
|
|
|
2017-12-22 19:45:33 +00:00
|
|
|
handleBitsyGameData();
|
|
|
|
|
2017-12-22 17:22:08 +00:00
|
|
|
$('#imageUpload').on('change', function () {
|
|
|
|
readFile(this);
|
|
|
|
});
|
|
|
|
|
2017-12-24 00:47:00 +00:00
|
|
|
// these inputs get added and removed from the DOM so the event handler needs to be on the document
|
2017-12-23 22:33:31 +00:00
|
|
|
$(document).on('change', '#palette input', function() {
|
2017-12-24 20:36:39 +00:00
|
|
|
var id = parseInt($(this).closest('.palette').find('input[name="id"]').val());
|
|
|
|
|
2017-12-22 17:22:08 +00:00
|
|
|
// if this is a colour input, update the palette
|
|
|
|
if ($(this).attr('type') === 'color') {
|
2017-12-24 20:36:39 +00:00
|
|
|
if (id === palette.id) {
|
|
|
|
palette[$(this).attr('name')] = hexToColour($(this).val());
|
|
|
|
}
|
2017-12-22 17:22:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// if this is a radio button, pick this palette
|
2017-12-22 19:45:33 +00:00
|
|
|
if ($(this).attr('type') === 'radio') {
|
2017-12-24 20:36:39 +00:00
|
|
|
palette.id = id;
|
2017-12-24 00:47:00 +00:00
|
|
|
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
|
2017-12-22 19:45:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
renderResult();
|
2017-12-22 17:22:08 +00:00
|
|
|
});
|
2017-12-24 00:47:00 +00:00
|
|
|
|
|
|
|
$('#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;
|
|
|
|
|
2017-12-24 12:09:54 +00:00
|
|
|
var newRoomName = $('#roomName').val();
|
|
|
|
// remove invalid chars? what's invalid? newlines? are those possible?
|
|
|
|
|
2017-12-24 00:47:00 +00:00
|
|
|
var newRoom = "ROOM " + newRoomId + "\n";
|
|
|
|
|
|
|
|
_.each(room, function(row) {
|
|
|
|
newRoom += _.toString(row) + "\n";
|
|
|
|
});
|
|
|
|
|
2017-12-24 12:09:54 +00:00
|
|
|
if (newRoomName) {
|
|
|
|
newRoom += "NAME " + newRoomName + "\n";
|
|
|
|
}
|
|
|
|
|
2017-12-24 00:47:00 +00:00
|
|
|
newRoom += "PAL " + palette.id + "\n";
|
|
|
|
|
|
|
|
// write
|
|
|
|
$('textarea').val(newGameData.replace(/(ROOM .*\n(.*\n)*PAL .*)/g, '$1\n\n' + newRoom));
|
|
|
|
});
|
2017-12-24 18:14:16 +00:00
|
|
|
});
|