Доброго времени на вашей стороне планеты.
Сегодня на хабре прямо день космических кораблей, столько интересных статей про последнюю битву в EVE Online, ну а я в свободное время я продолжаю делать свою двухмерную космическую игру и после длительного перерыва взялся за генератор кораблей. Пусть корабли и не такие шикарные как в EVE, зато свои.
Кому интересно как такой рендер на канве сделать, прошу под кат.
Вступление
Для начала нужно сказать для чего нужен такой генератор. Причин несколько:
- Спрайты кораблей нужны, а рисовать я толком не умею, но компьютер умеет
- Очень хотелось сделать в игре пилотам возможность создавать свои чертежи кораблей, без однотипного набора кораблей на рынке
- Я всегда смотрел на красивые концепты различной техники которые делают художники, и мне хотелось хотя бы чуть-чуть приблизится к такому уровню
- Ну и это просто интересно
Что должен уметь генератор:
- На основе JSON данных(основанных на vessel specification, о которой далее) генерить картинку корабля, на прозрачном фоне, для вставку в игру
- Генерить красивый рендер, который можно скачать, залить, и запостить на где-нибудь на форумах
- Выдавать конфиг корабля, который также можно скопипастить на форумы, и другой человек может поглядеть что там за дарвинский пепелац сорудили =)
Теперь поподробней о этой vessel specification, которая видна на рендере сверху. Технически она описывает как именно должны собираться корабли и из чего. С точки зрения бэкстори это что-то вроде ГОСТ'а, который придуман чтобы стандартизировать производство кораблей коммерческими компаниями. С точки зрения геймплея это попытка сделать какой-то общий дизайн кораблей, который не позволит создать тайловый редактор (когда по клеточкам рисуются корабли). Конечно общий дизайн это довольно условно, потому что редактирование конфига позволяет менять очень немало.
Из чего же составляются корабли по этому «ГОСТ'у»? Основой являются линии(line), линия это набор секций(section), которые располагаются параллельно друг другу. Секция — это компонент плюс два блока, которые скрепляют несколько секций в линию. Компонент же может быть много чем, это может быть просто скрепления, которые нужны только для увеличения прочности конструкции, не значительно увеличивающие её массу, или грузовым блоком, или блоком управления, или двигателем, или платформой для установки вооружения и так далее.
Думаю я уже достаточно рассказал о том, что такое генератор кораблей, давайте теперь посмотрим как же он собственно генерит эти рендеры.
Шаг 0 — Готовим холсты и краски
Для начала нам нужно настроить три канвы, #backCanvs, #mainCanvas и #topCanvas.
<canvas id="backCanvas" width="640" height="480"></canvas>
<canvas id="mainCanvas" width="640" height="480"></canvas>
<canvas id="topCanvas" width="640" height="480"></canvas>
Вот функция инициализации, в ней использован очень известный трюк, с установкой разрешения канвы, больше чем её визуальные размеры.
//Вызываем функцию инициализации слоев
var canvasSize = { width: Math.floor(window.innerWidth*3.5),
height: Math.floor(window.innerHeight*3.5) };
var cssSize = { width: window.innerWidth, height: window.innerHeight };
shipGen.layerInit(["#backCanvas", "#mainCanvas", "#topCanvas"], canvasSize, cssSize);
//Сама функция
shipGen.layerInit = function(canvases, canvasSize, cssSize) {
shipGen.config.canvasSize = canvasSize;
shipGen.config.cssSize = cssSize;
//Если слои есть, очищаем их и удаляем
for(var i in shipGen.layers) {
shipGen.layers[i].clearRect(0, 0, canvasSize.width, canvasSize.height)
}
shipGen.layers = [];
for(var i in canvases) {
shipGen.layers.push($(canvases[i]).attr("width", canvasSize.width)
.attr("height", canvasSize.height)
.css("width", cssSize.width)
.css("height", cssSize.height).get(0).getContext("2d"));
}
}
Шаг 0.5 — Готовим кисточки
Теперь нужно сделать второе, и самое важное — конфиги.
Как я уже говорил сам конфиг корабля записываем в JSON формате, который потом парсится в Javascript Object, но ещё есть настройки самой рисовалки:
lines: [], //Сюда помещаются распарсеный JSON-конфиг
config: { //Настройки для отрисовки
factor: 15, //Маштабирование
factorRandLight: 3, //Нужно для распределения лампочек
angle: 0, //Угол поворота корабля
canvasSize: {}, //Размеры канвы
designName: "", //Название
authorName: "" //Автор
},
counters: { //Различные счетчики, которые подсчитываются в процессе отрисовки или рядом
heightShip: 0, //Длина корабля
hull: 0, //Показатель прочности корпуса
linesCount: 0, //Сколько всего линий
sectionsCount: 0, //Сколько всего секций
totalMass: 0, //Суммарная масса корабля
},
data: { //Собираем данные по конкретным компонентам
engines: [],
blocks: [],
pipes: [],
cargos: []
}
Конфиг корабля мы задаем так:
try {
if(localStorage["config"]) {
shipGen.lines = JSON.parse(localStorage["config"]);
}
else {
shipGen.lines = JSON.parse($("#text").val());
}
}
catch(e) {
alert("Error parse config");
}
Привожу в пример небольшой конфг конфига корабля. Сначала описывается массив всех линий(в данном случае их три), для каждой линии указывается смещение следующей линии, и перечисляются секции, в данном случае их по две на линию.
[
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 10,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
}
]
Всё готово, начинаем писать.
Шаг 1 — Пишем звезды на первом холсте
Не только звезды, но и небольшую туманность. Это делается двумя функциями:
shipGen.render.nebula(Math.random(), canvasSize.width, canvasSize.height);
shipGen.render.stars(Math.random(), canvasSize.width, canvasSize.height);
Не вижу смысла приводить большие куски кода рисования фона, кто хочет может посмотреть в коде, а заголовок статьи всё-таки имеет подстроку «корабли», вот к ним и перейдем.
Но тем не менее у нас уже получилось что-то такое:
Шаг 2 — Пишем космический аппарат на втором холсте
Здесь уже интересней, нам нужно отрисовать в каждой линии, каждую секцию, и блоки скрепляющий секции. Основная функция выглядит так, всё с подробными комментариями:
shipGen.process = function() {
//Берем #mainCanvas
var ctx = shipGen.layers[1];
//Смещаем и поворачиваем контекст
ctx.translate(shipGen.config.canvasSize.width/2, shipGen.config.canvasSize.height/2);
ctx.rotate(shipGen.config.angle);
//Записываем количество линий
shipGen.counters.linesCount = shipGen.lines.length;
//Вспомогательная переменная, отмеряющая насколько мы сместились по X координате
//в процессе отрисовки линий
var lenShiftX = 0;
for (var i = 0; i < shipGen.counters.linesCount; i++) {
ctx.save();
//Берем данные о секциях
var sections = shipGen.lines[i].sections;
//Крутим цикл по каждому компоненту секции
for(var component in sections) {
//В процессе подсчитываем суммарное количество секций в кораблей
shipGen.counters.sectionsCount += 1
//Берем набор параметров компонента, добавляем к нему некоторые служебные данные и вызываем отрисовку компонента
shipGen.selectComponent(sections[component], {lenShiftX: lenShiftX, blockTopW: sections[component].blockW2, blockBottomW: sections[component].blockW1 } );
}
ctx.restore();
lenShiftX += shipGen.lines[i].nextLineX;
//Смещаем отрисовку вправо, для того чтобы рядом нарисовать следующую паралелльную линию
ctx.translate(shipGen.lines[i].nextLineX, 0);
}
}
Функцией selectComponent отрисовываем в нужном месте сначала первый блок, потом вызываем функцию рисования конкретного компонента, потом рисуем закрывающий блок:
...
shipGen.block(obj.blockW1, obj.blockH1);
...
shipGen.components[obj.name](obj);
...
shipGen.block(obj.blockW1, obj.blockH1);
...
Каждый компонент рисуется своей функцией, банальными moveTo/LineTo/rect/fill/stroke. Просто представляем как должен выглядеть компонент и последовательно вызываем функции по нужным координатам. Вот несколько примеров:
Pipe'ы и блоки:
Двигатели:
Грузовые блоки:
Ну и так далее, можно сделать много разных компонентов, что и нужно сделать, но пока для тестов нового формата кораблей достаточно и трех.
Финальный результат корабля, с конфигом:
[
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"2": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"3": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 40,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 10,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"2": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 10,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"2": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"3": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 40,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
}
]
Шаг 3 — Пишем красивый текст на третьем холсте
Остался предпоследний этап, нужно офоромить характеристики корабля в виде красивого текста. Ну хотя бы чуть-чуть красивого.
И делаем это очень легко, просто меняем размеры шрифта, и смещаемся постепенно вниз по Y координате. Как раз таки используем данные из конфига об авторе, названии дизайна и другие. Кстати об авторе и названии дизайна, я сделал пока их статичными, потому что они будут вытащены при интеграции с игрой, а пока пусть будут Unnamed и Unknown.
//Вызов рисования текста
shipGen.render.text();
//Сама функция
shipGen.render.text = function() {
var ctx = shipGen.layers[2];
//Номер дизайна
ctx.fillStyle = "#fff";
ctx.strokeStyle = "#fff";
ctx.font = "130pt Arial";
ctx.fillText("Vessel Design " + shipGen.config.number, 220, 220);
//Название формата
ctx.font = "50pt Arial";
ctx.fillText("Vessel specification of Tranquilla Community VSC-V3", 250, 320);
ctx.font = "40pt Arial";
//Название дизайна
ctx.fillText("Design name: " + shipGen.config.designName, 250, 420);
//Имя автора дизайна
ctx.fillText("Author name: " + shipGen.config.authorName, 250, 520);
//Где мы делали этот дизайн
ctx.fillText("Place: Tranq One IV Station 41 (1020; 1210)", 250, 620);
ctx.font = "35pt Arial";
//Основные характеристики
ctx.fillText("Mass: " + shipGen.counters.totalMass, 250, 750);
ctx.fillText("Hull: " + shipGen.counters.hull, 250, 800);
ctx.fillText("Lines count: " + shipGen.counters.linesCount , 250, 850);
ctx.fillText("Sections count: " + shipGen.counters.sectionsCount, 250, 900);
ctx.fillText("Block count: " + shipGen.data.blocks.length, 250, 950);
ctx.fillText("Pipe count: " + shipGen.data.pipes.length, 250, 1000);
var lineY = 1150;
//Характеристики компонентов
for(var i in shipGen.data) {
if(i == "blocks" || i == "pipes") continue;
ctx.fillText(i.toString() + ":", 250, lineY - 50);
for (var v in shipGen.data[i]) {
ctx.fillText(i.toString() + ": " + JSON.stringify(shipGen.data[i][v], "", 1).replace(/"/g, ''), 250, lineY);
lineY += 50;
}
lineY+=100;
}
}
Результат мы уже видели выше, привожу тоже самое для другого конфига.
[ {
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 40,
"blockH1": 0,
"blockW2": 20,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 25,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"2": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 30,
"blockH2": 40,
"width": 15,
"height": 40,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"3": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 40,
"blockH2": 90,
"width": 15,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 20,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 25,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"2": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"3": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 40,
"blockH2": 10,
"width": 15,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 40,
"blockH1": 0,
"blockW2": 20,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 25,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"2": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 30,
"blockH2": 40,
"width": 15,
"height": 40,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"3": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 40,
"blockH2": 90,
"width": 15,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 20,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 25,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"2": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 30,
"blockH2": 40,
"width": 15,
"height": 40,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"3": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 40,
"blockH2": 90,
"width": 15,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 20,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 25,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"2": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"3": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 40,
"blockH2": 10,
"width": 15,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 40,
"blockH1": 0,
"blockW2": 20,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 25,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"2": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 30,
"blockH2": 40,
"width": 15,
"height": 40,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"3": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 40,
"blockH2": 90,
"width": 15,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
}
]
Шаг 4 — Открываем выставку
Последний этап, подключаем FileSaver.js, и пишем такую простую сохранялку, объединяя все слои.
$("#save-render").click(function(){
var canvasMerge = $("<canvas width='"+canvasSize.width+"' height='"+canvasSize.height+"'></canvas>").get(0);
var ctxMerge = canvasMerge.getContext("2d");
ctxMerge.fillStyle = "#000";
ctxMerge.fillRect(0, 0, canvasSize.width, canvasSize.height);
ctxMerge.drawImage($("#backCanvas").get(0), 0, 0);
ctxMerge.drawImage($("#mainCanvas").get(0), 0, 0);
ctxMerge.drawImage($("#topCanvas").get(0), 0, 0);
canvasMerge.toBlob(function(blob) {
saveAs(blob, "render.png");
});
});
Заключение
Для тех кто не хочет мучатся с ручным редактированием конфигов, а просто хочет посмотреть на кораблики, я здесь привожу несколько конфигов, которые я делал в процессе рарзработки. Чтобы поглядеть на них нужно нажать внизу на ссылку «Edit config», вставить в выбранный конфиг, и нажать также внизу «Apply».
[
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"2": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"3": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"4": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"2": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"3": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 80,
"color": {"r":20,"g":20,"b":20},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 80,
"color": {"r":20,"g":20,"b":20},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"2": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"3": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"4": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
}
}
}
]
[
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"2": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"3": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 80,
"color": {"r":20,"g":20,"b":20},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 80,
"color": {"r":20,"g":20,"b":20},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"2": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"3": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"4": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"2": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"3": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 80,
"color": {"r":20,"g":20,"b":20},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 80,
"color": {"r":20,"g":20,"b":20},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"2": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"3": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"4": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"2": {
"name": "cargo",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"3": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 80,
"color": {"r":20,"g":20,"b":20},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 80,
"color": {"r":20,"g":20,"b":20},
"shift": 0,
"step": 35
}
}
}
]
[
{
"nextLineX": 100,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 100,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 10,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 100,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 10,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 10,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 8,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
}
]
[
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 20,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 25,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "cargo",
"blockW1": 40,
"blockH1": 10,
"blockW2": 50,
"blockH2": 60,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"2": {
"name": "cargo",
"blockW1": 40,
"blockH1": 10,
"blockW2": 50,
"blockH2": 60,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"3": {
"name": "cargo",
"blockW1": 40,
"blockH1": 10,
"blockW2": 50,
"blockH2": 60,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
},
"4": {
"name": "cargo",
"blockW1": 40,
"blockH1": 10,
"blockW2": 50,
"blockH2": 60,
"width": 35,
"height": 35,
"color": {
"r": 20,
"g": 20,
"b": 20
}
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 20,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 25,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 50,
"blockH2": 60,
"width": 15,
"height": 70,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"2": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"3": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 40,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 40,
"blockH2": 10,
"width": 15,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 40,
"blockH1": 0,
"blockW2": 20,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 25,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 50,
"blockH2": 60,
"width": 15,
"height": 70,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"2": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 30,
"blockH2": 40,
"width": 15,
"height": 40,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"3": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 40,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 40,
"blockH2": 90,
"width": 15,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 20,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 25,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 50,
"blockH2": 60,
"width": 15,
"height": 70,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"2": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 30,
"blockH2": 40,
"width": 15,
"height": 40,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"3": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 40,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 40,
"blockH2": 90,
"width": 15,
"height": 30,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
},
{
"nextLineX": 200,
"sections": {
"0": {
"name": "engine",
"blockW1": 0,
"blockH1": 0,
"blockW2": 20,
"blockH2": 10,
"width": 12,
"height": 8,
"color": {
"r": 25,
"g": 20,
"b": 20
},
"shift": 0,
"widthLeft": 8,
"widthRight": 8
},
"1": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 50,
"blockH2": 60,
"width": 15,
"height": 70,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"2": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"3": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 10,
"blockH2": 10,
"width": 15,
"height": 40,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
},
"4": {
"name": "pipe",
"blockW1": 40,
"blockH1": 10,
"blockW2": 40,
"blockH2": 10,
"width": 15,
"height": 80,
"color": {
"r": 20,
"g": 20,
"b": 20
},
"shift": 0,
"step": 35
}
}
}
]
Весь код доступен как всегда на гитхабе: github.com/MagistrAVSH/ship-gen/
Демо можно увидеть здесь: magistravsh.github.io/ship-gen/
В следующий раз надеюсь уже написать об интеграции редактора с игрой, думаю это будет интересно.
Fly safe!
Автор: Magistr_AVSH