NMS Freighter Floor Planner

Plan your own Freighter layout

by Devilin Pixy

HTML

<div class="main">
  <header>
    <h1>No Man's Sky - Freighter Floor Planner</h1>
    <div>
      <button class="emptyButton" onclick="emptyCanvas()">Empty Plan</button>
      <button class="defaultButton" onclick="defaultCanvas()">Default Plan</button>
      <button class="clearButton" onclick="clearCanvas()">Clear Plan</button>
    </div>
  </header>
  <div class="main_body">
    <aside>
      <label>Part Selector</label>
      <div class="parts">
        <img id="parts-source" crossorigin />
        <div class="part-selection"></div>
      </div>
      <label>Layer</label>
      <div>
        <ul class="layers">
          <li><button onclick="setLayer(2)" class="layer" tile-layer="2">Top</button></li>
          <li><button onclick="setLayer(1)" class="layer" tile-layer="1">Middle</button></li>
          <li><button onclick="setLayer(0)" class="layer" tile-layer="0">Bottom</button></li>
        </ul>
      </div>
      <label>Import / Export</label>
      <div>
        <ul class="fileImpExp">
          <li><button onclick="document.getElementById('importJSONFile').click();">Import JSON</button>
            <input id="importJSONFile" type="file" accept=".json" style="display:none;"></li>
          <li><button onclick="exportJSON()">Export JSON</button></li>
          <li><button onclick="exportPNG()">Export Image</button></li>
        </ul>
      </div>
    </aside>
    <div class="main_right">
      <label class="instructions">
        <b>Click</b> to paint.
        <b>Shift+Click</b> to remove.
      </label>
      <!-- The main canvas -->
      <canvas width="672" height="704"></canvas>
      <!-- UI for layers -->
      <div class="footer">
        (Note: Freighter has 15 floors) --- Made by: Devilin Pixy© --- (updated for Endurance 3.94)
      </div>
    </div>
  </div>
</div>

CSS

* {
  box-sizing: border-box;
}

html {
  height: 100%;
}

body {
  font-family: 'Roboto', sans-serif;
	padding: 1em;
	background: linear-gradient(180deg, #666 0%, #222 100%);
	display: flex;
	justify-content: space-around;
	color: #222;
}

.main {
	background: #ccc;
	box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.5);
	padding-bottom: 16px;
}
.main header {
  display: flex;
	justify-content: space-between;
	align-items: center;
	border-bottom: 1px solid #000;
	margin-bottom: 1em;
	padding: 16px;
}
.main header h1 {
	margin: 0;
}
.main aside {
	padding-left: 16px;
	padding-right: 16px;
}
.main_body {
	display: flex;
}
.main_right {
	padding-right: 16px;
}

.instructions {
	font-size: 1em;
	color: #222;
	text-align: center;
	margin: 0;
}

label {
  background-color: #00ffff16;
  color: #222;
  text-align: center;
	font-weight: bold;
	display: block;
}

canvas {
  background-color: #888;
}

.parts {
	position: relative;
}
.part-selection {
	position: absolute;
	outline: 3px solid cyan;
	left: 0;
	top: 0;
	width: 32px;
	height: 32px;
}

.layers {
	list-style-type: none;
	margin: 0;
	padding: 0;
}
.layers button {
	appearance: none;
	font-family: inherit;
  font-size: 1em;
  color: #eee;
	outline: #000;
	background: #666;
  border-radius: 8px;
	padding: 4px;
  margin: 0 0 4px 0;
	display: block;
	width: 100%;
	text-align: center;
	cursor: pointer;
}
.layers button.active {
	font-weight: bold;
	color: #00ffff;
}
.layers button:hover {
  background-color: #aaa;
}

.clearButton, .defaultButton, .emptyButton {
	appearance: none;
	text-decoration: underline;
	background: transparent;
  font-size: 1em;
  font-weight: bold;
	color: #b00;
	border: 0;
	outline: 0;
	cursor: pointer;
}

.clearButton:hover, .defaultButton:hover, .emptyButton:hover {
  color: #ff0000;
}

.fileImpExp {
  list-style-type: none;
	margin: 0;
	padding: 0;
}

.fileImpExp button, .fileImpExp input {
	appearance: none;
	font-family: inherit;
  font-size: 1em;
	outline: #000;
	background-color: #eee;
 ...

JavaScript

var canvas = document.querySelector("canvas");
var tilesetContainer = document.querySelector(".parts");
var tilesetSelection = document.querySelector(".part-selection");
var tilesetImage = document.querySelector("#parts-source");

var selection = [0, 0]; //Part picked

var isMouseDown = false;
var currentLayer = 0;
var layers = [{},{},{}];

//Select tile from the plan
tilesetContainer.addEventListener("mousedown", (event) => {
	selection = getCoords(event);
  tilesetSelection.style.left = selection[0] * 32 + "px";
  tilesetSelection.style.top = selection[1] * 32 + "px";
});

//Handler for placing new tiles
function addTile(mouseEvent) {
  var clicked = getCoords(event);
  var key = clicked[0] + "-" + clicked[1];

  if (mouseEvent.shiftKey) {
		delete layers[currentLayer][key];
	} else {
		layers[currentLayer][key] = [selection[0], selection[1]];
	}
	draw();
}

//Bind mouse events for painting (or removing) tiles on click/drag
canvas.addEventListener("mousedown", () => {
  isMouseDown = true;
});
canvas.addEventListener("mouseup", () => {
  isMouseDown = false;
});
canvas.addEventListener("mouseleave", () => {
  isMouseDown = false;
});
canvas.addEventListener("mousedown", addTile);
canvas.addEventListener("mousemove", (event) => {
  if (isMouseDown) {
  	addTile(event);
  }
});

//Utility for getting coordinates of mouse click
function getCoords(e) {
  const { x, y } = e.target.getBoundingClientRect();
  const mouseX = e.clientX - x;
  const mouseY = e.clientY - y;
  return [Math.floor(mouseX / 32), Math.floor(mouseY / 32)];
}

//Reset state to empty
function clearCanvas() {
  layers = [{}, {}, {}];
  draw();
	setLayer(0);
}

function defaultCanvas() {
	//Default Freighter Floor Plan
	var defaultState =...