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 =...