<link href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<!------ Include the above in your HEAD tag ---------->
<!DOCTYPE html><html lang='en' class=''>
<head><script src='//production-assets.codepen.io/assets/editor/live/console_runner-079c09a0e3b9ff743e39ee2d5637b9216b3545af0de366d4b9aad9dc87e26bfd.js'></script><script src='//production-assets.codepen.io/assets/editor/live/events_runner-73716630c22bbc8cff4bd0f07b135f00a0bdc5d14629260c3ec49e5606f98fdd.js'></script><script src='//production-assets.codepen.io/assets/editor/live/css_live_reload_init-2c0dc5167d60a5af3ee189d570b1835129687ea2a61bee3513dee3a50c115a77.js'></script><meta charset='UTF-8'><meta name="robots" content="noindex"><link rel="shortcut icon" type="image/x-icon" href="//production-assets.codepen.io/assets/favicon/favicon-8ea04875e70c4b0bb41da869e81236e54394d63638a1ef12fa558a4a835f1164.ico" /><link rel="mask-icon" type="" href="//production-assets.codepen.io/assets/favicon/logo-pin-f2d2b6d2c61838f7e76325261b7195c27224080bc099486ddd6dccb469b8e8e6.svg" color="#111" /><link rel="canonical" href="https://codepen.io/zerratar/pen/aLKqBV" />
<style class="cp-pen-styles">#style-4::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
background-color: rgba(0, 0, 0, 0.75);
}
#style-4::-webkit-scrollbar {
width: 10px;
background-color: rgba(0, 0, 0, 0.25);
}
#style-4::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.75);
}
* {
position: relative;
box-sizing: border-box;
}
h4 {
padding: 8px 5px;
margin: 0;
letter-spacing: 0.09em;
}
html, body {
font-family: Roboto, arial;
background-color: #26252b;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
overflow-y: hidden;
}
.app {
width: 100%;
height: 100%;
padding: 10px;
}
.menu {
margin-bottom: 5px;
}
.surface {
color: white;
font-size: 9pt;
padding: 5px 9px;
border-radius: 2px;
-webkit-box-shadow: 0px 0px 8px -1px rgba(0, 0, 0, 0.95);
-moz-box-shadow: 0px 0px 8px -1px rgba(0, 0, 0, 0.95);
box-shadow: 0px 0px 8px -1px rgba(0, 0, 0, 0.95);
border: 1px solid #5a5667;
border-bottom: 1px solid #535060;
background: #373540;
background: -moz-linear-gradient(top, #484551 0%, #423f4c 100%);
background: -webkit-linear-gradient(top, #484551 0%, #423f4c 100%);
background: linear-gradient(to bottom, #484551 0%, #423f4c 100%);
}
.surface[disabled='disabled'] {
background: rgba(185, 185, 185, 0.2);
box-shadow: none;
color: rgba(255, 255, 255, 0.3);
cursor: default;
user-select: none;
top: 0px;
}
.btn {
user-select: none;
display: inline-block;
text-align: center;
}
.btn:hover {
background: #323035;
cursor: pointer;
}
.btn:active {
-webkit-box-shadow: 0;
-moz-box-shadow: 0;
box-shadow: 0;
background: #312F34;
top: 1px;
}
.btn[disabled='disabled'] {
background: rgba(185, 185, 185, 0.2);
color: rgba(255, 255, 255, 0.3);
cursor: default;
user-select: none;
top: 0px;
}
.btn .fa {
margin-right: 5px;
}
.workspace {
display: table;
user-select: none;
width: 100%;
height: 100%;
}
.project {
min-width: 251px;
width: auto;
display: table-cell;
height: 100%;
}
.selector {
width: auto;
min-width: 230px;
display: table-cell;
height: 100%;
}
.scene {
display: table-cell;
min-width: 265px;
width: 100%;
height: 100%;
padding-left: 5px;
padding-right: 5px;
}
.editor {
top: 1px;
position: absolute;
}
.editor-container {
display: table;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.window-title-bar, .tile-tools, .project-tools, .editor-tools {
display: inline-block;
height: 32px;
}
.window-title-bar .btn, .tile-tools .btn, .project-tools .btn, .editor-tools .btn {
height: auto;
width: auto;
text-align: center;
}
.window-title-bar .btn i, .window-title-bar .btn .fa, .tile-tools .btn i, .tile-tools .btn .fa, .project-tools .btn i, .project-tools .btn .fa, .editor-tools .btn i, .editor-tools .btn .fa {
text-align: center;
width: 100%;
}
.window-title-bar .btn.active, .tile-tools .btn.active, .project-tools .btn.active, .editor-tools .btn.active {
background: #26252b;
top: 1px;
box-shadow: none;
}
.editor-border {
padding: 0;
display: table-row;
width: 100%;
height: 100%;
}
.tab-header {
user-select: none;
cursor: default;
font-size: 9pt;
margin-top: 10px;
display: block;
width: 100px;
text-align: center;
margin-left: auto;
margin-right: auto;
color: white;
padding: 5px 9px;
border-radius: 2px 2px 0px 0px;
-webkit-box-shadow: 0px 0px 10px -1px rgba(0, 0, 0, 0.75);
-moz-box-shadow: 0px 0px 10px -1px rgba(0, 0, 0, 0.75);
box-shadow: 0px 0px 10px -1px rgba(0, 0, 0, 0.75);
border: 1px solid #434146;
background: #373540;
background: -moz-linear-gradient(top, #373540 0%, #373540 100%);
background: -webkit-linear-gradient(top, #373540 0%, #373540 100%);
background: linear-gradient(to bottom, #373540 0%, #373540 100%);
}
.tile-list, .project-item-list {
background-color: #26252b;
border-radius: 2px;
border: 1px solid rgba(0, 0, 0, 0.5);
-webkit-box-shadow: inset 0px 0px 14px 0px rgba(0, 0, 0, 0.5);
-moz-box-shadow: inset 0px 0px 14px 0px rgba(0, 0, 0, 0.5);
box-shadow: inset 0px 0px 14px 0px rgba(0, 0, 0, 0.5);
display: inline-block;
width: 100%;
}
.inspector {
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
align-items: stretch;
width: 100%;
height: 100%;
}
.item-details {
display: inline-block;
height: 150px;
width: 100%;
order: 1;
}
.item-details .item-name {
position: absolute;
right: 0px;
top: 5px;
color: white;
padding: 2px 5px;
width: 180px;
border: 1px solid #35323f;
border-radius: 3px;
background-color: #484551;
-webkit-box-shadow: inset 0px 0px 15px -5px rgba(0, 0, 0, 0.56);
-moz-box-shadow: inset 0px 0px 15px -5px rgba(0, 0, 0, 0.56);
box-shadow: inset 0px 0px 15px -5px rgba(0, 0, 0, 0.56);
}
.item-details .item-name:focus {
outline: none;
background-color: #3e3b47;
border: 1px solid #2b2835;
}
.item-details .item-type-icon {
top: 4px;
font-size: 16pt;
}
.item-details .item-type-icon.fa-folder {
font-size: 17pt;
color: #f39c12;
}
.group-tools, .layer-tools {
display: inline-block;
top: -83px;
height: calc(100% - 100px);
width: 100%;
order: 2;
}
.group-tools .brush-size, .layer-tools .brush-size {
margin-top: -30px;
margin-bottom: 10px;
}
.tile-selector {
display: block;
width: 100%;
height: calc(100% - 22px);
top: 0px;
}
.tile-list {
height: 100%;
top: 33px;
left: 0px;
position: absolute;
overflow-y: auto;
}
.project-item-list {
height: calc(100% - 32px);
}
.tab {
height: calc(100% - 70px);
}
.newline {
padding: 5px;
}
.input-header {
font-family: arial;
display: block;
width: 100%;
letter-spacing: 0.09em;
padding-top: 5px;
padding-bottom: 5px;
font-weight: bold;
}
.input-row {
width: 100%;
display: block;
}
.input-row input[type="text"] {
text-align: center;
width: 42px;
margin-left: 5px;
}
.window {
position: fixed;
display: none;
top: 25%;
left: 50%;
margin-left: -200px;
width: 400px;
height: 280px;
}
.window .window-title-bar {
width: 100%;
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
}
.window .window-title-bar h4 {
display: inline-block;
padding-top: 6px;
font-family: arial;
font-size: 10pt;
}
.window .window-title-bar .btn {
float: right;
display: inline-block;
}
.window .window-body {
font-family: arial;
padding: 5px;
}
.window .window-actions {
width: 100%;
position: absolute;
left: 0px;
height: 48px;
background-color: #26252b;
border-top: 1px solid rgba(0, 0, 0, 0.25);
padding: 10px;
margin-top: 10px;
}
.window .window-actions .btn {
float: right;
margin-left: 5px;
}
ul {
list-style-type: none;
padding-top: 0;
padding-bottom: 0;
padding-left: 18px;
padding-right: 0px;
}
.project-item-tree {
padding-left: 10px;
user-select: none;
}
.project-item-tree li {
width: 100%;
font-size: 10pt;
padding-top: 2px;
padding-bottom: 2px;
margin-right: 0px;
cursor: default;
}
.project-item-tree li .selected {
background-color: rgba(52, 152, 219, 0.5);
}
.project-item-tree li .item-visibility {
position: absolute;
display: inline-block;
right: 20px;
top: 6px;
width: 16px;
height: 16px;
}
.project-item-tree li .item-visibility.visible:before {
content: "\f06e";
font-family: FontAwesome;
font-style: normal;
font-weight: normal;
}
.project-item-tree li .item-visibility.not-visible:before {
content: "\f070";
font-family: FontAwesome;
font-style: normal;
font-weight: normal;
}
.project-item-tree li .item-name {
padding: 4px 5px;
margin-right: 10px;
}
.project-item-tree li .item-name:before {
margin-right: 5px;
font-family: FontAwesome;
font-style: normal;
font-weight: normal;
}
.project-item-tree li .name-editor {
max-width: 150px;
margin-right: 25px;
}
.project-item-tree li div.layer:before {
content: "\f15b";
}
.project-item-tree li div.group:before {
content: "\f07b";
font-size: 12pt;
color: #f39c12;
}
input[type="text"] {
border-top: 0;
border-left: 0;
border-right: 0;
border-bottom: 2px solid white;
background-color: transparent;
color: white;
}
input[type="text"]:active, input[type="text"]:focus {
outline: none;
}
.window-tint {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.65);
display: none;
}
#input-map-data {
width: 100%;
height: 108px;
}
.toasty {
display: none;
position: fixed;
right: 0px;
bottom: 0px;
height: 150px;
}
#load-tilesets-window {
height: 120px;
}
#load-tilesets-window .progress-bar {
width: 370px;
height: 20px;
position: relative;
left: 0px;
background-color: rgba(0, 0, 0, 0.25);
}
#load-tilesets-window .progress-bar .progress-bar-value {
width: 0px;
height: 18px;
top: 1px;
left: 0px;
background-color: white;
}
.selectable-tile {
padding: 5px;
display: inline-block;
box-sizing: border-box;
border: 2px solid transparent;
}
.selectable-tile.iso {
height: 45px;
width: 62px;
}
.selectable-tile.iso:hover {
border: 2px solid rgba(0, 0, 0, 0.5);
}
.selectable-tile.top:hover {
border: 2px solid rgba(0, 255, 0, 0.35);
}
.selectable-tile.selected {
border: 2px solid red;
}
</style></head><body>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css"/>
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet"/>
<div class="app">
<div class="menu">
<div class="surface btn" id="btn-new" title="Create a new map" onclick="newMap()"><i class="fa fa-plus"></i><span>New map</span></div>
<div class="surface btn" id="btn-load" title="Load a map using mapdata" onclick="loadMap()"><i class="fa fa-folder-open"> </i><span>Load</span></div>
<div class="surface btn" id="btn-save" title="Save/Export this map" onclick="saveMap()"><i class="fa fa-save"></i><span>Save</span></div>
<div class="surface btn" id="btn-settings" title="Change editor or map preferences" onclick="showSettings()"><i class="fa fa-cog"></i><span>Settings</span></div>
<div class="surface btn" id="btn-info" title="Totally about this tool!" onclick="showAbout()"><i class="fa fa-info"></i><span>About</span></div>
</div>
<div class="workspace">
<div class="project">
<div class="tab-header">Map layers</div>
<div class="surface tab">
<div class="project-tools">
<div class="surface btn" id="btn-layer-group" title="Create a new group" onclick="createLayerGroup()"><i class="fa fa-folder"></i></div>
<div class="surface btn" id="btn-layer-add" title="Create a layer" onclick="createLayer()"><i class="fa fa-file-o"> </i></div>
<div class="surface btn req-layer" id="btn-layer-duplicate" title="Duplicate layer" onclick="duplicateLayer(this)" disabled="disabled"><i class="fa fa-files-o"></i></div>
<div class="surface btn req-layer" id="btn-layer-up" title="Move group or layer upwards" onclick="moveLayerUp(this)" disabled="disabled"><i class="fa fa-arrow-up"></i></div>
<div class="surface btn req-layer" id="btn-layer-down" title="Move group or layer downwards" onclick="moveLayerDown(this)" disabled="disabled"><i class="fa fa-arrow-down"> </i></div>
<div class="surface btn req-layer" id="btn-layer-remove" title="Remove group or layer" onclick="removeLayerOrGroup(this)" disabled="disabled"><i class="fa fa-trash-o"> </i></div>
</div>
<div class="project-item-list" id="style-4">
<ul class="project-item-tree"></ul>
</div>
</div>
</div>
<div class="scene">
<div class="tab-header">Map editor</div>
<div class="surface tab">
<div class="editor-container">
<div class="editor-tools">
<div class="surface btn active" id="btn-editor-cursor" title="Selector tool - select objects to edit their properties" onclick="selectEditorTool('cursor')"><i class="fa fa-mouse-pointer"></i></div>
<div class="surface btn" id="btn-editor-brush" title="(1) Brush tool - paint tiles" onclick="selectEditorTool('brush')"><i class="fa fa-paint-brush"></i></div>
<div class="surface btn" id="btn-editor-eraser" title="(2) Eraser tool - erase tile data" onclick="selectEditorTool('eraser')"><i class="fa fa-eraser"></i></div>
<div class="surface btn" id="btn-editor-move" title="(3) Drag tool - pan around the map editor, you can also hold down (alt)" onclick="selectEditorTool('move')"><i class="fa fa-arrows"></i></div>
<div class="surface btn" id="btn-editor-zout" title="(-) Zoom out" onclick="zoomOut()"><i class="fa fa-search-minus"></i></div>
<div class="surface btn" id="btn-editor-zin" title="(+) Zoom in" onclick="zoomIn()"><i class="fa fa-search-plus"></i></div>
</div>
<div class="surface editor-border">
<canvas class="editor"></canvas>
</div>
</div>
</div>
</div>
<div class="selector">
<div class="tab-header">Inspector</div>
<div class="surface tab inspector">
<div class="item-details"><i class="item-type-icon"></i>
<input class="item-name"/>
</div>
<div class="group-tools"></div>
<div class="layer-tools">
<div class="brush-size">
<div class="input-header">Brush settings</div>
<div class="input-row">
<label for="brush-size">Size:</label>
<input type="text" value="1" onchange="brushSizeChanged()" id="brush-size"/>
</div>
</div>
<div class="tile-selector">
<div class="tile-tools">
<div class="surface btn" id="btn-tiles-previous" onclick="previousTilePage()"><i class="fa fa-angle-left"></i></div>
<div class="surface btn" id="btn-tiles-next" onclick="nextTilePage()"><i class="fa fa-angle-right"></i></div>
</div>
<div class="tile-list" id="style-4"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="window-tint"> </div>
<div class="window surface" id="create-map-window">
<div class="window-title-bar">
<h4>New map</h4>
<div class="surface btn" onclick="cancelCreateMap()"><i class="fa fa-close"></i></div>
</div>
<div class="window-body">
<p>Warning: Creating a new map will discard your current progress!</p>
<div class="input-header">Select perspective</div>
<div class="input-row">
<input class="map-perspective" id="p-2d-default" name="map-perspective" type="radio" value="top" checked="checked"/>
<label for="p-2d-default">2D default</label>
</div>
<div class="input-row">
<input class="map-perspective" id="p-2d-isometric" name="map-perspective" value="iso" type="radio"/>
<label for="p-2d-isometric">2D isometric</label>
</div>
<div class="newline"></div>
<div class="input-header">Map/grid size</div>
<div class="input-row">
<label for="input-map-width">Width :</label>
<input type="text" placeholder="eg. 32" id="input-map-width"/>
</div>
<div class="input-row">
<label for="input-map-height">Height:</label>
<input type="text" placeholder="eg. 32" id="input-map-height"/>
</div>
</div>
<div class="window-actions">
<div class="surface btn" onclick="cancelCreateMap()">Cancel</div>
<div class="surface btn" onclick="createMap()">OK</div>
</div>
</div>
<div class="window surface" id="load-map-window">
<div class="window-title-bar">
<h4>Load map</h4>
<div class="surface btn" onclick="cancelLoadMap()"><i class="fa fa-close"></i></div>
</div>
<div class="window-body">
<p>Warning: Loading a map will discard your current progress!</p>
<div class="input-header">Paste your map data here (not implemented)</div>
<div class="input-row">
<textarea placeholder="Paste your map data here" id="input-map-data"></textarea>
</div>
</div>
<div class="window-actions">
<div class="surface btn" onclick="cancelLoadMap()">Cancel</div>
<div class="surface btn" onclick="openMap()">OK</div>
</div>
</div>
<div class="window surface" id="load-tilesets-window">
<div class="window-title-bar">
<h4>Loading tilesets...</h4>
</div>
<div class="window-body">
<p>Please wait while we load the tilesets </p>
<div class="progress-bar">
<div class="progress-bar-value"></div>
</div>
</div>
</div><img class="toasty" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/163870/Toasty!MK3.png"/>
<script src='//production-assets.codepen.io/assets/common/stopExecutionOnTimeout-b2a7b3fe212eaa732349046d8416e00a9dec26eb7fd347590fbced3ab38af52e.js'></script>
<script>/*
* By Zerratar, Karl Johansson
* 2017-10-09
* How to use:
* 1. Link to this pen and use it as javascript
* 2. add <canvas class="myCanvas"></canvas> to html
* 3. add the following js
*
* const draw = () => {
* // ctx.fillRect(...)
* };
*
* const update = () => {
* // logic here, called just before draw is called
* };
*
* const resize = () => {
* canvas.width = window.innerWidth-1;
* canvas.height = window.innerHeight-4;
* };
*
* setup(".myCanvas", draw, update, resize);
*
* Presto! Its all done! Now you have access to a quick way of drawing on the canvas
* Pssst: You can even use stuff as: Time.time, Time.deltaTime, Time.deltaTimeUnscaled, Time.timeScale, Time.frameCount, mouse.x, mouse.y
* Time.time: gets the total time elapsed in seconds since first frame
* Time.deltaTime: gets the time elapsed since last frame, this is multiplied by Time.timeScale
* Time.deltaTimeUnscaled: gets the time elapsed since last frame, uneffected by the Time.timeScale
* Time.timeScale: gets the timescale, 1 is default and is the normal speed
*
* There are alot of other hidden gems in this script, not covered by the tiny documentation above.
*/
var EPSILON = 0.000001;
let canvas = undefined;
let ctx = undefined;
let gl = undefined;
let isWebGl = false;
var gravity_multiplier = 0.5;
var gravity_base = -0.00982;
var gravity = gravity_base * gravity_multiplier;
var mouse = { x: 0, y: 0, leftButton: false, rightButton: false, middleButton: false };
var Time = { timeScale: 1, deltaTime: 0, deltaTimeUnscaled: 0, time: 0, frameCount: 0 };
var onDraw = undefined;
var onUpdate = undefined;
let ctxScaleY = 1.0;
let ctxScaleX = 1.0;
function setGravityMultiplier(val) {
gravity_multiplier = val;
gravity = gravity_base * gravity_multiplier;
}
function setup3d(canvasSelector, onDrawCallback, onUpdateCallback, onResizeCallback, onInitCallback) {
isWebGl = true;
canvas = document.querySelector(canvasSelector);
ctx = canvas.getContext("experimental-webgl");
gl = ctx; // alias
onDraw = onDrawCallback;
onUpdate = onUpdateCallback;
window.addEventListener("resize", onResizeCallback, false);
canvas.addEventListener("mousemove", mouseMove, false);
canvas.addEventListener("touchmove", touchMove, false);
canvas.addEventListener("touchstart", e => {
e.preventDefault();
mouse.leftButton = true;
}, false);
canvas.addEventListener("touchend", e => {
e.preventDefault();
mouse.leftButton = false;
}, false);
canvas.addEventListener("mousedown", mouseDown, false);
canvas.addEventListener("mouseup", mouseUp, false);
if(onResizeCallback) onResizeCallback();
if (onInitCallback) onInitCallback();
run(0);
}
function setup(canvasSelector, onDrawCallback, onUpdateCallback, onResizeCallback) {
canvas = document.querySelector(canvasSelector);
ctx = canvas.getContext("2d");
onDraw = onDrawCallback;
onUpdate = onUpdateCallback;
window.addEventListener("resize", onResizeCallback, false);
canvas.addEventListener("mousemove", mouseMove, false);
canvas.addEventListener("touchmove", touchMove, false);
canvas.addEventListener("touchstart", e => {
e.preventDefault();
mouse.leftButton = true;
}, false);
canvas.addEventListener("touchend", e => {
e.preventDefault();
mouse.leftButton = false;
}, false);
canvas.addEventListener("mousedown", mouseDown, false);
canvas.addEventListener("mouseup", mouseUp, false);
if(onResizeCallback) onResizeCallback();
run(0);
}
function drawEllipse(cx, cy, w, h){
ctx.beginPath();
let lx = cx - w/2,
rx = cx + w/2,
ty = cy - h/2,
by = cy + h/2;
// let kappa = 4 * ((Math.sqrt(2) - 1)/3)
let kappa = 0.551784;
var xkappa = kappa*w/2;
var ykappa = h*kappa/2;
ctx.moveTo(cx,ty);
ctx.bezierCurveTo(cx+xkappa,ty,rx,cy-ykappa,rx,cy);
ctx.bezierCurveTo(rx,cy+ykappa,cx+xkappa,by,cx,by);
ctx.bezierCurveTo(cx-xkappa,by,lx,cy+ykappa,lx,cy);
ctx.bezierCurveTo(lx,cy-ykappa,cx-xkappa,ty,cx,ty);
ctx.stroke();
ctx.fill();
}
function drawCircle(x, y, fillStyle, radius) {
radius = radius || 5;
ctx.beginPath();
ctx.fillStyle=fillStyle||"green";
ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
ctx.stroke();
ctx.fill();
}
function resetScale() {
ctxScaleX = 1;
ctxScaleY = 1;
}
Array.prototype.remove = function(from, to) {
var rest = this.slice((to || from) + 1 || this.length);
this.length = from < 0 ? this.length + from : from;
return this.push.apply(this, rest);
};
function rgb(r, g, b) {
return new Color(r, g, b);
}
function rgba(r, g, b, a) {
return new Color(r,g,b,a);
}
class Color {
constructor(r,g,b,a) {
this.r=r;
this.g=g;
this.b=b;
if (a === undefined && a !== 0) a = 1.0;
this.a=a;
}
static getWhite() {
return new Color(255,255,255,1);
}
static getBlack() {
return new Color(0,0,0,1);
}
darker(amount) {
return this.shade(-amount);
}
lighter(amount) {
return this.shade(amount);
}
lerp(to, amount) {
const lerpNum = (start, end, a) => parseInt(start + ((end-start) * a));
return new Color(
lerpNum(this.r, to.r, amount),
lerpNum(this.g, to.g, amount),
lerpNum(this.b, to.b, amount),
this.a
);
}
shade(percent) {
let r = parseInt(this.r * (100 + percent) / 100);
let g = parseInt(this.g * (100 + percent) / 100);
let b = parseInt(this.b * (100 + percent) / 100);
let c = new Color((r<255)?r:255, (g<255)?g:255,(b<255)?b:255,this.a);
return c;
}
rgba(alpha) {
alpha = alpha || this.a;
return `rgba(${this.r},${this.g},${this.b},${alpha})`;
}
rgb() {
return `rgb(${this.r},${this.g},${this.b})`;
}
}
class PixelUtilities {
static resizeNearestNeighbor(pixels, oldWidth, oldHeight, newWidth, newHeight) {
let tmp = new Array(newWidth * newHeight);
let xRatio = ((oldWidth<<16)/newWidth)+1;
let yRatio = ((oldHeight<<16)/newHeight)+1;
let x2, y2;
for (let y = 0; y < newHeight; ++y) {
for (let x = 0; x < newWidth; ++x) {
y2 = (y*yRatio)>>16;
x2 = (x*xRatio)>>16;
tmp[y*newWidth+x] = pixels[y2*oldWidth+x2];
}
}
return tmp;
}
}
class Shape {
constructor(points) {
this.points = points;
}
}
class Polygon extends Shape {
constructor(points) {
super(points);
}
static create(x, y, radius, npoints) {
let TWO_PI = Math.PI * 2;
let angle = TWO_PI / npoints;
let pts = [];
let startPoint = undefined;
for (let a = 0; a < TWO_PI; a += angle) {
let sx = x + Math.cos(a) * radius;
let sy = y + Math.sin(a) * radius;
let pt = new Point(sx, sy);
if (startPoint == undefined)
startPoint = pt.copy();
pts.push(pt);
}
pts.push(startPoint);
return new Polygon(pts);
}
isPointInside(p) {
let isInside = false;
let polygon = this.points;
let minX = polygon[0].x, maxX = polygon[0].x;
let minY = polygon[0].y, maxY = polygon[0].y;
for (let n = 1; n < polygon.length; n++) {
var q = polygon[n];
minX = Math.min(q.x, minX);
maxX = Math.max(q.x, maxX);
minY = Math.min(q.y, minY);
maxY = Math.max(q.y, maxY);
}
if (p.x < minX || p.x > maxX || p.y < minY || p.y > maxY) {
return false;
}
var i = 0, j = polygon.length - 1;
for (i, j; i < polygon.length; j = i++) {
if ( (polygon[i].y > p.y) != (polygon[j].y > p.y) &&
p.x < (polygon[j].x - polygon[i].x) * (p.y - polygon[i].y) / (polygon[j].y - polygon[i].y) + polygon[i].x ) {
isInside = !isInside;
}
}
return isInside;
}
draw(strokeStyle, fillStyle) {
ctx.save();
ctx.beginPath();
ctx.strokeStyle = strokeStyle || "red";
ctx.moveTo(this.points[0].x, this.points[0].y);
for (let i = 1; i < this.points.length; ++i) {
ctx.lineTo(this.points[i].x, this.points[i].y);
}
ctx.stroke();
if (fillStyle !== undefined) {
ctx.fillStyle = fillStyle;
ctx.fill();
}
ctx.closePath();
ctx.restore();
}
offset(xoffset, yoffset) {
for(let i = 0; i < this.points.length; i++) {
this.points[i] = new Point(this.points[i].x + xoffset, this.points[i].y + yoffset);
}
return this;
}
scale(scale) {
for(let i = 0; i < this.points.length; i++) {
this.points[i] = new Point(this.points[i].x * scale, this.points[i].y * scale);
}
}
moveTo(x, y, origin) {
// get bounds
// then move all points so the bounds LEFT,TOP is touching the x, y pos
origin = origin || new Point(0, 0);
let bb = this.getBoundingBox();
let dx = x - bb.min.x;
let dy = y - bb.min.y;
if (origin.x > 0) dx -= (bb.max.x - bb.min.x) * origin.x;
if (origin.y > 0) dy -= (bb.max.y - bb.min.y) * origin.y;
return this.offset(dx, dy);
}
getBoundingBox() {
let xMin = 999999, xMax = 0;
let yMin = 999999, yMax = 0;
for (let i = 0; i < this.points.length; ++i) {
xMin = Math.min(xMin, this.points[i].x);
yMin = Math.min(yMin, this.points[i].y);
xMax = Math.max(xMax, this.points[i].x);
yMax = Math.max(yMax, this.points[i].y);
}
let min = {x:xMin,y:yMin};
let max = {x:xMax,y:yMax};
return new BoundingBox(min, max);
}
getLines() {
let pts = [];
let lines=[];
let idx = 0;
this.points.forEach(x=>pts.push(x));
for(let i = 1; i < pts.length; i++) {
let line = new Line(pts[i-1], pts[i]);
line.index = idx++;
lines.push(line);
}
return lines;
}
}
class Rectangle {
constructor() {
this.left=0;
this.right=0;
this.top=0;
this.bottom=0;
}
}
class Line extends Shape {
constructor(p1, p2) {
super([p1, p2]);
this.start = p1;
this.stop = p2;
}
get normal() {
let dx = this.stop.x - this.start.x;
let dy = this.stop.y - this.start.y;
return new Line(new Point(-dy, dx), new Point(dy, -dx));
}
draw(strokeStyle, linedash, linewidth) {
this.drawLine(this, strokeStyle, linedash, linewidth);
}
drawNormal(strokeStyle) {
let bb = this.getBoundingBox();
let n = this.normal;
let posx = bb.min.x ;
let posy = bb.min.y ;
let x1 = posx + n.start.x;
let x2 = posx + n.stop.x;
let y1 = posy + n.start.y;
let y2 = posy + n.stop.y;
this.drawLine(
new Line(
new Point(x1, y1),
new Point(x2, y2)
),
strokeStyle);
}
drawLine(line, strokeStyle, linedash, linewidth) {
ctx.save();
ctx.beginPath();
if (linedash) ctx.setLineDash(linedash);
if (linewidth) ctx.lineWidth = linewidth;
ctx.strokeStyle = strokeStyle || "yellow";
ctx.moveTo(line.start.x, line.start.y);
ctx.lineTo(line.stop.x, line.stop.y);
ctx.stroke();
ctx.restore();
}
getBoundingBox() {
return new BoundingBox(
new Point(Math.min(this.start.x, this.stop.x), Math.min(this.start.y, this.stop.y)),
new Point(Math.max(this.start.x, this.stop.x), Math.max(this.start.y, this.stop.y))
);
}
getMidPoint() {
return new Point((this.start.x + this.stop.x) / 2,(this.start.y + this.stop.y)/2);
}
}
class Point {
constructor(x,y,index,intersectionPoint) {
this.x = x;
this.y = y;
this.intersectionPoint = intersectionPoint||false;
this.index = index||-1;
}
mul(scale) {
this.x *= scale;
this.y *= scale;
return this;
}
copy() {
return new Point(this.x, this.y, this.index, false);
}
}
class Vector3 {
constructor(x, y, z) {
this.x = x||0;
this.y = y||0;
this.z = z||0;
}
}
class Vector2 {
constructor(x, y) {
this.x = x||0;
this.y = y||0;
}
get length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
get sqrLength() {
return this.x * this.x + this.y * this.y;
}
lerp(v2, a) {
return new Vector2(
this.x + (v2.x - this.x) * a,
this.y + (v2.y - this.y) * a,
);
}
moveTowards(v2, a) {
if (a > 1.0) a = 1.0;
if (a < 0.0) a = 0.0;
return this.lerp(v2, a);
}
dot(v2) {
let m = this.mul(v2);
return m.x + m.y;
}
cross(v2) {
return this.x * v2.y - v2.x * this.y;
}
angleBetween(v2) {
let sin = this.x * v2.y - v2.x * this.y;
let cos = this.x * v2.y + v2.x * this.y;
return Math.atan2(sin, cos) * (180 / Math.PI);
}
normalize() {
let len = this.length;
return new Vector2(this.x / len, this.y / len);
}
direction(v2) {
let heading = v2.sub(this);
var distance = heading.length;
return heading.div(distance);
}
distance(v2) {
let diff = v2.sub(this);
return Math.sqrt(diff.x * diff.x + diff.y * diff.y);
}
add(v2) {
if (v2 instanceof Vector2) return new Vector2(this.x + v2.x, this.y + v2.y);
else return new Vector2(this.x + v2, this.y + v2);
}
sub(v2) {
if (v2 instanceof Vector2) return new Vector2(this.x - v2.x, this.y - v2.y);
else return new Vector2(this.x - v2, this.y - v2);
}
mul(v2) {
if (v2 instanceof Vector2) return new Vector2(this.x * v2.x, this.y * v2.y);
else return new Vector2(this.x * v2, this.y * v2);
}
div(v2) {
if (v2 instanceof Vector2) return new Vector2(this.x / v2.x, this.y / v2.y);
else return new Vector2(this.x / v2, this.y / v2);
}
}
class Intersection {
/**
* Calculate the cross product of two points.
* @param a first point
* @param b second point
* @return the value of the cross product
*/
static crossProduct(a, b) {
return a.x * b.y - b.x * a.y;
}
static doBoundingBoxesIntersect(a, b) {
return a.min.x <= b.max.x && a.max.x >= b.min.x && a.min.y <= b.max.y && a.max.y >= b.min.y;
}
/**
* Checks if a Point is on a line
* @param a line (interpreted as line, although given as line
* segment)
* @param b point
* @return <code>true</code> if point is on line, otherwise
* <code>false</code>
*/
static isPointOnLine(a, b) {
// Move the image, so that a.first is on (0|0)
let aTmp = new Line(new Point(0, 0), new Point(a.stop.x - a.start.x, a.stop.y - a.start.y));
let bTmp = new Point(b.x - a.start.x, b.y - a.start.y);
let r = this.crossProduct(aTmp.stop, bTmp);
return Math.abs(r) < EPSILON;
}
/**
* Checks if a point is right of a line. If the point is on the
* line, it is not right of the line.
* @param a line segment interpreted as a line
* @param b the point
* @return <code>true</code> if the point is right of the line,
* <code>false</code> otherwise
*/
static isPointRightOfLine(a, b) {
// Move the image, so that a.first is on (0|0)
let aTmp = new Line(new Point(0, 0), new Point(
a.stop.x - a.start.x, a.stop.y - a.start.y));
let bTmp = new Point(b.x - a.start.x, b.y - a.start.y);
return this.crossProduct(aTmp.stop, bTmp) < 0;
}
/**
* Check if line segment first touches or crosses the line that is
* defined by line segment second.
*
* @param first line segment interpreted as line
* @param second line segment
* @return <code>true</code> if line segment first touches or
* crosses line second,
* <code>false</code> otherwise.
*/
static lineSegmentTouchesOrCrossesLine(a, b) {
return this.isPointOnLine(a, b.start)
|| this.isPointOnLine(a, b.stop)
|| (this.isPointRightOfLine(a, b.start) ^ this.isPointRightOfLine(a, b.stop));
}
/**
* Check if line segments intersect
* @param a first line segment
* @param b second line segment
* @return <code>true</code> if lines do intersect,
* <code>false</code> otherwise
*/
static doLinesIntersect(a, b) {
let box1 = a.getBoundingBox();
let box2 = b.getBoundingBox();
return this.doBoundingBoxesIntersect(box1, box2)
&& this.lineSegmentTouchesOrCrossesLine(a, b)
&& this.lineSegmentTouchesOrCrossesLine(b, a);
}
static getIntersection(a, b) {
/* the intersection [(x1,y1), (x2, y2)]
it might be a line or a single point. If it is a line,
then x1 = x2 and y1 = y2. */
var x1, y1, x2, y2;
if (a.start.x == a.stop.x) {
// Case (A)
// As a is a perfect vertical line, it cannot be represented
// nicely in a mathematical way. But we directly know that
//
x1 = a.start.x;
x2 = x1;
if (b.start.x == b.stop.x) {
// Case (AA): all x are the same!
// Normalize
if(a.start.y > a.stop.y) {
a = {start: a.stop, stop: a.start};
}
if(b.start.y > b.stop.y) {
b = {start: b.stop, stop: b.start};
}
if(a.start.y > b.start.y) {
var tmp = a;
a = b;
b = tmp;
}
// Now we know that the y-value of a.start is the
// lowest of all 4 y values
// this means, we are either in case (AAA):
// a: x--------------x
// b: x---------------x
// or in case (AAB)
// a: x--------------x
// b: x-------x
// in both cases:
// get the relavant y intervall
y1 = b.start.y;
y2 = Math.min(a.stop.y, b.stop.y);
} else {
// Case (AB)
// we can mathematically represent line b as
// y = m*x + t <=> t = y - m*x
// m = (y1-y2)/(x1-x2)
var m, t;
m = (b.start.y - b.stop.y)/
(b.start.x - b.stop.x);
t = b.start.y - m*b.start.x;
y1 = m*x1 + t;
y2 = y1
}
} else if (b.start.x == b.stop.x) {
// Case (B)
// essentially the same as Case (AB), but with
// a and b switched
x1 = b.start.x;
x2 = x1;
var tmp = a;
a = b;
b = tmp;
var m, t;
m = (b.start.y - b.stop.y)/
(b.start.x - b.stop.x);
t = b.start.y - m*b.start.x;
y1 = m*x1 + t;
y2 = y1
} else {
// Case (C)
// Both lines can be represented mathematically
var ma, mb, ta, tb;
ma = (a.start.y - a.stop.y)/
(a.start.x - a.stop.x);
mb = (b.start.y - b.stop.y)/
(b.start.x - b.stop.x);
ta = a.start.y - ma*a.start.x;
tb = b.start.y - mb*b.start.x;
if (ma == mb) {
// Case (CA)
// both lines are in parallel. As we know that they
// intersect, the intersection could be a line
// when we rotated this, it would be the same situation
// as in case (AA)
// Normalize
if(a.start.x > a.stop.x) {
a = {start: a.stop, stop: a.start};
}
if(b.start.x > b.stop.x) {
b = {start: b.stop, stop: b.start};
}
if(a.start.x > b.start.x) {
var tmp = a;
a = b;
b = tmp;
}
// get the relavant x intervall
x1 = b.start.x;
x2 = Math.min(a.stop.x, b.stop.x);
y1 = ma*x1+ta;
y2 = ma*x2+ta;
} else {
// Case (CB): only a point as intersection:
// y = ma*x+ta
// y = mb*x+tb
// ma*x + ta = mb*x + tb
// (ma-mb)*x = tb - ta
// x = (tb - ta)/(ma-mb)
x1 = (tb-ta)/(ma-mb);
y1 = ma*x1+ta;
x2 = x1;
y2 = y1;
}
}
// return {start: {"x":x1, "y":y1}, stop: {"x":x2, "y":y2}};
let intersectionPoint = new Point(x1, y1);
intersectionPoint.intersectionPoint = true;
return intersectionPoint;
}
static anyLineStartsWith(pt, polyLines) {
return polyLines.filter(x => x.start.x === pt.x && x.start.y === pt.y).length > 0;
}
/**
* Slice a polygon by the provided intersection points
* @param poly the polygon to slice
* @param intersections the intersection points used for the slicing
* @return an array of polygons that is the result of the slice
*/
static slice(poly, intersections) {
// if (intersections.length <= 1) return [poly];
if (intersections.length == 2) return this.sliceSimplePolygonIntersection( poly, intersections);
if (intersections.length > 2) return this.sliceComplexPolygonIntersection(poly, intersections);
return [poly];
}
static sliceSimplePolygonIntersection(poly, intersections) {
let polyLines = poly.getLines();
let breakIndex = 0;
let polyPointsA = [];
let polyPointsB = [];
let polys = [];
let polyA = undefined;
let polyB = undefined;
let polyPoints = [];
// 1. insert cut points
// 2. then iterate all points to build new lines
let lidx = 0;
let ll = -1;
for(let x = 0; x < poly.points.length;++x) {
polyPoints.push(poly.points[x]);
if (x!==0&&this.anyLineStartsWith(poly.points[x], polyLines)) lidx++;
for (let int of intersections) {
if (int.index === lidx && lidx !== ll) {
polyPoints.push(int);
ll = lidx;
}
}
}
// rotate points until we start on an intersection
while(!polyPoints[0].intersectionPoint) {
let item = polyPoints.shift();
polyPoints.push(item);
}
let intersectionIndex = 0;
// iterate each poly point so we can build our points for polyA (top-piece)
// logic:
// start at first intersectionPoint, iterate until next intersectionPoint
// then add the start(first intersectionPoint) again. Presto, polygon done!
for (let x = 0; x < polyPoints.length; x++) {
if (polyPoints[x].intersectionPoint === true) {
if (intersectionIndex === 0) {
// our starting point! Add it and continue.
polyPointsA.push(polyPoints[x].copy());
} else {
// alright! We're at our second intersection. Add this point
polyPointsA.push(polyPoints[x].copy());
// and add the first point so we can close the polygon
polyPointsA.push(polyPointsA[0].copy());
// presto! Create the poly and be happy
polyA = new Polygon(polyPointsA);
polys.push(polyA);
break;
}
++intersectionIndex;
}
// while we still havnt found our second intersection, keep pushing those points
else if (intersectionIndex === 1) {
polyPointsA.push(polyPoints[x].copy());
}
if (polyA !== undefined) break; // we're done, so jump out!
}
// time to create the bottom-piece polygon
// logic:
// start at first intersectionPoint
// then skip all points until we find our next intersectionPoint, when we do. We take
// the rest of the points until we're at the starting point again. (basically, take the rest)
intersectionIndex = 0;
for (let x = 0; x < polyPoints.length; x++) {
if (polyPoints[x].intersectionPoint === true) {
if (intersectionIndex === 0) {
// our starting point! Add it and continue.
polyPointsB.push(polyPoints[x].copy());
} else {
// alright! We're at our second intersection. Add this point
polyPointsB.push(polyPoints[x].copy());
// this means that we can now add polys again :)
}
++intersectionIndex;
}
// after adding both intersection points, we are now ready to grab the rest of points!
else if (intersectionIndex === 2) {
polyPointsB.push(polyPoints[x].copy());
}
}
// finally, all points have been added to our bottom-piece.
// all we have to do now is close the poly by adding starting point
// and then lets create our polygon :)
polyPointsB.push(polyPointsB[0].copy());
polyB = new Polygon(polyPointsB);
polys.push(polyB);
return polys; // return the new set of polygons
}
static sliceComplexPolygonIntersection(poly, intersections) {
return [poly]; // not implemented, better just return same than breaking the shape.
// to solve this one we still want to add the intersection points into the polygon.
// .... todo
// possible solution for having more than 2 intersection points are to first do the slice on the first two points
// then do another slice between 2 and 3rd point.
// although its a bit slower than wanted. it is at least simple to follow
}
static check(a, b) {
let linesA = [];
let linesB = [];
let intersections = [];
if (a instanceof Polygon) linesA = this.getPolygonLines(a);
if (a instanceof Rectangle) linesA = this.getRectangleLines(a);
if (a instanceof Line) linesA = [a];
if (b instanceof Polygon) linesB = this.getPolygonLines(b);
if (b instanceof Rectangle) linesB = this.getRectangleLines(b);
if (b instanceof Line) linesB = [b];
for (let i = 0; i < linesA.length; i++) {
for (let j = 0; j < linesB.length; j++) {
if (this.doLinesIntersect(linesA[i], linesB[j])) {
let result = this.getIntersection(linesA[i], linesB[j]);
if (result !== undefined) {
result.index = linesA[i].index;
intersections.push(result);
}
}
}
}
return intersections;
}
static getPolygonLines(poly) {
return poly.getLines();
}
static getRectangleLines(rect) {
return [
new Line(new Point(rect.left, rect.top), new Point(rect.right, rect.top)),
new Line(new Point(rect.right, rect.top), new Point(rect.right, rect.bottom)),
new Line(new Point(rect.right, rect.bottom), new Point(rect.left, rect.bottom)),
new Line(new Point(rect.left, rect.bottom), new Point(rect.left, rect.top)),
];
}
}
class Physics {
static register(object) {
if (!this.objects) this.objects = [];
this.objects.push(object);
}
static unregister(object) {
// note: this would have been super bad if we had multiple threads..
if (!this.objects) this.objects = [];
var index = this.objects.indexOf(object);
if (index === -1) return;
this.objects.remove(index);
}
static update() {
if (!this.objects) this.objects = [];
for (let index = 0; index < this.objects.length; ++index) {
let obj = this.objects[index];
if (obj.rigidBody) this.updateRigidBody(obj.rigidBody);
else if (obj.collider) this.updateCollider(obj.collider);
}
}
static updateRigidBody(rigidBody) {
let isGrounded = false;
let velX = rigidBody.velocity.x * Time.deltaTime;
let velY = rigidBody.velocity.y * Time.deltaTime;
if (!isNaN(velX) && !rigidBody.constraints.freezePositionX) rigidBody.gameObject.position.x += velX;
if (!isNaN(velY) && !rigidBody.constraints.freezePositionY) rigidBody.gameObject.position.y += velY;
// let height = rigidBody.gameObject.collider.bounds.max.y;
let screenHeight = window.innerHeight / ctxScaleY;
for (let i = 0; i < this.objects.length; ++i) {
if (this.objects[i].rigidBody !== rigidBody) {
let obj = this.objects[i];
if (obj.collider && obj.collider.isTouching(rigidBody.gameObject.collider)) {
if (obj.isTrigger === true) {
rigidBody.gameObject.onTriggerEnter(obj.collider);
} else {
rigidBody.gameObject.onCollisionEnter(obj.collider);
if (rigidBody.ignoreCollisionPhysics) return;
// check which face of the boundingbox/collider that actually had a collision
// and then stop the velocity of that direction
// TODO: RayCast the sides to determine where the blockage is
// and stop the velocity in that direction
// NOTE: You can jump through objects that whould block your left or right if you jump towards it
if (obj.position.y + obj.offset.y >= (rigidBody.gameObject.position.y + rigidBody.gameObject.offset.y)) {
isGrounded = true;
rigidBody.velocity.y = 0;
rigidBody.gameObject.position.y = (obj.position.y - rigidBody.gameObject.collider.bounds.max.y) + 1;
} else if (obj.position.x + obj.offset.x >= rigidBody.gameObject.position.x) {
// collides to the right
// + rigidBody.gameObject.collider.bounds.max.x
rigidBody.gameObject.position.x -= velX;
rigidBody.velocity.x = 0;
}
else if (obj.position.x + obj.offset.x + obj.collider.bounds.max.x >=
rigidBody.gameObject.position.x) {
// collides to the right
// + rigidBody.gameObject.collider.bounds.max.x
rigidBody.gameObject.position.x -= velX;
rigidBody.velocity.x = 0;
}
}
}
}
}
if (!isGrounded) {
let fall = gravity * Time.deltaTime;
if (!isNaN(fall)) {
rigidBody.velocity.y -= fall;
}
}
else {
if (rigidBody.force.y != 0) {
rigidBody.velocity.y = rigidBody.force.y;
rigidBody.force.y = 0;
}
}
rigidBody.isGrounded = isGrounded;
}
static updateCollider(collider) {
// console.log("update collider");
}
}
class GameComponent {
constructor() {
this.gameObject = null;
this.isEnabled = true;
this.tag = '';
this.layer = '';
}
setGameObject(gameObject) {
this.gameObject = gameObject;
}
update() { }
draw() { }
}
class BoundingBox {
constructor(min = { x: 0, y: 0 }, max = { x: 0, y: 0 }) {
this.min = min;
this.max = max;
}
get delta() {
return {
x: this.max.x - this.min.x,
y: this.max.y - this.min.y
};
}
}
class Viewport {
constructor() {
this.x = 0;
this.y = 0;
this.width = window.innerWidth / ctxScaleX;
this.height = window.innerWidth / ctxScaleY;
}
move (xOffset, yOffset) {
this.x += xOffset;
this.y += yOffset;
}
reset() {
this.x = 0;
this.y = 0;
}
}
class Camera {
constructor() {
this.viewport = new Viewport();
}
static getMainCamera() {
if (!this.mainCamera) this.mainCamera = new Camera();
return this.mainCamera;
}
setViewport(x, y, w, h) {
this.viewport.x = x;
this.viewport.y = y;
if (w) this.viewport.width = w;
if (h) this.viewport.height = h;
}
}
class FollowTarget extends GameComponent {
constructor(target, xOffset, yOffset) {
super();
this.target=target;
this.xOffset=xOffset||0;
this.yOffset=yOffset||0;
}
draw() {
if (!this.isEnabled||!this.isVisible) return;
super.draw();
}
update() {
if (!this.isEnabled) return;
super.update();
if (this.target instanceof GameObject) {
this.gameObject.position.x = this.target.position.x + this.xOffset;
this.gameObject.position.y = this.target.position.y + this.yOffset;
}
}
}
class FadeOut extends GameComponent {
constructor() {
super();
this.isRunning = false;
this.value = 1.0;
this.duration = 5.0;
this.timer = this.duration;
this.destroyObjectOnCompletion = false;
}
start() {
this.value = this.gameObject.opacity;
this.timer = this.duration * this.gameObject.opacity;
this.isRunning = true;
}
stop() {
this.isRunning = false;
}
update() {
if (!this.isEnabled || !this.isRunning) return;
super.update();
this.timer -= Time.deltaTime / 1000;
this.value = 1-((this.duration - this.timer) / this.duration);
this.gameObject.opacity = this.value;
if (this.value <= 0.001 || this.timer <= 0) {
if (this.onComplete) this.onComplete(this.gameObject);
this.isRunning = false;
this.timer = 0;
this.gameObject.opacity = 0;
if (this.destroyObjectOnCompletion) {
this.gameObject.destroy();
}
}
}
draw() {
super.draw();
}
}
class Mask extends GameComponent {
constructor() {
super();
this.shape = undefined;
this.centerShape = false;
this.drawShape = false;
this.offset = {x:0,y:0};
}
draw() {
if (this.shape === undefined) return;
if (this.isEnabled !== true) return;
if (this.drawShape) {
this.shape.draw();
}
let path = new Path2D();
path.moveTo(this.shape.points[0].x, this.shape.points[0].y);
for (let i = 1; i < this.shape.points.length; ++i) {
path.lineTo(this.shape.points[i].x, this.shape.points[i].y);
}
path.closePath();
ctx.clip(path, "nonzero");
}
update() {
if (!this.isEnabled || !this.centerShape || !this.shape) return;
// console.log(this.gameObject.image.src);
let goPos = this.gameObject.position;
// TODO: move path to center of sprite
//let bb = this.shape.getBoundingBox();
this.shape.moveTo(goPos.x + this.offset.x, goPos.y + this.offset.y, new Point(0, 0));
}
}
class Collider extends GameComponent {
constructor() {
super();
this.isTrigger = false;
this.bounds = new BoundingBox();
this.layerMask = undefined;
}
isTouching(otherCollider) {
return false;
}
}
class PolygonCollider extends Collider {
constructor(shape) {
super();
this.isTrigger = false;
this.shape = shape;
this.bounds = new BoundingBox();
if (this.shape) {
this.bounds = shape.getBoundingBox();
}
}
isTouching(otherCollider) {
if (!this.shape) return;
if (this.gameObject && otherCollider && otherCollider.gameObject) {
if (this.layerMask && otherCollider.layer !== this.layerMask) {
return false;
}
if (this.bounds.max.x == 0) this.bounds = this.shape.getBoundingBox();
// first check if we are inside eachother's bounding box. otherwise theres no point of checking whether they touch
let aSize = { width: this.bounds.max.x, height : this.bounds.max.y };
let aPos = { x: this.gameObject.position.x + this.gameObject.offset.x, y: this.gameObject.position.y + this.gameObject.offset.y};
let bSize = { width: otherCollider.bounds.max.x, height: otherCollider.bounds.max.y };
let bPos = { x: otherCollider.gameObject.position.x + otherCollider.gameObject.offset.x, y: otherCollider.gameObject.position.y + otherCollider.gameObject.offset.y};
if(aPos.x + aSize.width >= bPos.x
&& aPos.x <= bPos.x + bSize.width
&& aPos.y + aSize.height >= bPos.y
&& aPos.y <= bPos.y + bSize.height) {
let pts = otherCollider.getPoints();
for(let pt of pts) {
if (this.shape.isPointInside(pt)) return true;
}
}
}
return false;
}
getPoints() {
return this.shape.points;
}
}
class BoxCollider extends Collider {
constructor() {
super();
this.isTrigger = false;
this.bounds = new BoundingBox();
}
isTouching(otherCollider) {
if (this.gameObject && otherCollider && otherCollider.gameObject) {
if (this.layerMask && otherCollider.layer !== this.layerMask) {
return false;
}
let aSize = { width: this.bounds.max.x, height : this.bounds.max.y };
let aPos = { x: this.gameObject.position.x + this.gameObject.offset.x, y: this.gameObject.position.y + this.gameObject.offset.y};
let bSize = { width: otherCollider.bounds.max.x, height: otherCollider.bounds.max.y };
let bPos = { x: otherCollider.gameObject.position.x + otherCollider.gameObject.offset.x, y: otherCollider.gameObject.position.y + otherCollider.gameObject.offset.y};
return aPos.x + aSize.width >= bPos.x
&& aPos.x <= bPos.x + bSize.width
&& aPos.y + aSize.height >= bPos.y
&& aPos.y <= bPos.y + bSize.height;
}
return false;
}
getPoints() {
return [
new Point(this.bounds.min.x, this.bounds.min.y), new Point(this.bounds.max.x, this.bounds.min.y),
new Point(this.bounds.max.x, this.bounds.min.y), new Point(this.bounds.max.x, this.bounds.max.y),
new Point(this.bounds.max.x, this.bounds.max.y), new Point(this.bounds.min.x, this.bounds.max.y),
new Point(this.bounds.min.x, this.bounds.max.y), new Point(this.bounds.min.x, this.bounds.min.y),
];
}
}
class BoxRenderer extends GameComponent {
constructor() {
super();
this.size = { width: 0, height: 0 };
}
draw() {
if (this.isEnabled !== true) return;
let camera = Camera.getMainCamera();
ctx.save();
ctx.beginPath();
ctx.rect(
camera.viewport.x + this.gameObject.position.x,
camera.viewport.y + this.gameObject.position.y,
this.size.width,
this.size.height);
ctx.strokeStyle = 'Red';
ctx.stroke();
ctx.closePath();
ctx.restore();
}
}
class RigidBody extends GameComponent {
constructor() {
super();
this.velocity = { x: 0, y: 0 };
this.force = { x: 0, y: 0 };
this.isStatic = false;
this.isGrounded = false;
this.constraints = { freezePositionX: false, freezePositionY: false };
this.ignoreCollisionPhysics = false;
}
}
class GameObject {
constructor() {
this.position = { x: 0, y: 0 };
this.rotation = 0;
this.opacity = 1;
this.isEnabled = true;
this.isVisible = true;
this.rigidBody = null;
this.collider = null;
this.renderer = null;
this.parent = null;
this.isDestroyed = false;
this.components = [];
this.children = [];
this.name = '';
this.tag = '';
this.layer = '';
this.offset = {x:0, y:0};
Physics.register(this);
}
// get localPosition() {
getLocalPosition() {
if (this.parent !== null) {
return {
x: this.position.x - this.parent.position.x,
y: this.position.y - this.parent.position.y
};
}
return {
x: this.position.x,
y: this.position.y
};
}
//set localPosition(value) {
setLocalPosition(x, y) {
if (this.parent !== null) {
this.position = {
x: this.parent.position.x + x,
y: this.parent.position.y + y
};
return;
}
this.position = {
x: value.x,
y: value.y
};
}
destroy() {
if (this.isDestroyed) return;
this.isDestroyed = true;
Physics.unregister(this);
if (this.parent) {
this.parent.removeChild(this);
}
}
addComponent(component) {
this.components.push(component);
component.setGameObject(this);
return component;
}
addChild(gameObject) {
this.children.push(gameObject);
gameObject.setParent(this);
return gameObject;
}
removeChild(gameObject) {
let index = this.children.indexOf(gameObject);
gameObject.setParent(null);
this.children.remove(index);
}
getComponent(name) {
for(let child of this.components) {
if (child.constructor.name == name) {
return child;
}
}
return undefined;
}
setParent(gameObject) {
this.parent = gameObject;
}
setRenderer(renderer) {
if (this.renderer) {
var index = this.components.indexOf(this.renderer);
this.components.remove(index);
}
this.renderer = renderer;
this.addComponent(this.renderer);
}
setRigidBody(rigidBody) {
if (this.rigidBody) {
var index = this.components.indexOf(this.rigidBody);
this.components.remove(index);
}
this.rigidBody = rigidBody;
this.addComponent(this.rigidBody);
}
setCollider(collider) {
if (this.collider) {
var index = this.components.indexOf(this.collider);
this.components.remove(index);
}
this.collider = collider;
this.addComponent(this.collider);
}
update() {
for(let i = 0; i < this.components.length; ++i) {
this.components[i].update();
}
for(let i = 0; i < this.children.length; ++i) {
this.children[i].update();
}
}
draw() {
for(let i = 0; i < this.components.length; ++i) {
this.components[i].draw();
}
for(let i = 0; i < this.children.length; ++i) {
this.children[i].draw();
}
}
onCollisionEnter(collider) { }
onTriggerEnter(collider) { }
}
class ParticleSystem extends GameObject {
constructor() {
super();
this.isLooping = false;
this.isEmitting = false;
this.startSize = 1;
this.startDelay = 0;
this.startSpeed = 5;
this.startColor = "red";
this.startLifetime = 1;
this.startTime = 0;
this.duration = 1.0;
this.timer = this.duration;
this.maxParticleCount = 10;
}
update() {
if (!this.isEnabled) return;
super.update();
// pass-1 update whether we should continue to emit particles
if (this.isEmitting) {
this.timer -= Time.deltaTime/1000;
if (this.timer <= 0) {
this.timer = this.isLooping ? this.duration : 0;
// if its still 0, then stop the emitting
this.isEmitting = this.timer > 0;
}
}
// if we are still emitting, keep adding those particles!
if (this.isEmitting) {
this.addParticle();
this.updateParticles();
}
}
addParticle() {
if (this.children.length >= this.maxParticleCount) return;
let velocity = new Point((Math.random()*0.5)-0.25, Math.random()*0.2);
let particle = new Particle(this.position.x, this.position.y, this.startLifetime, velocity, this.startColor, (Math.random() * 3)+1);
this.addChild(particle);
}
updateParticles() {
let toRemove = this.children.filter(x => x.lifetime <= 0 || x.position.y >= canvas.height);
toRemove.forEach(x => x.destroy());
}
destroyParticles() {
let toRemove = [];
this.children.forEach(x => toRemove.push(particle));
toRemove.forEach(x => x.destroy());
}
draw() {
if (!this.isEnabled) return;
super.draw();
}
start() {
this.destroyParticles();
this.isEmitting = true;
this.timer = this.duration;
this.startTime = Time.time;
}
stop() {
this.isEmitting = false;
}
get particleCount() {
return children.length;
}
}
class Particle extends GameObject {
constructor(startX, startY, life, velocity, color, size) {
super();
const rigidBody = new RigidBody();
rigidBody.velocity.x = velocity.x;
rigidBody.velocity.y = velocity.y;
this.size = size;
this.color = color;
this.lifetime = life;
this.setRigidBody(rigidBody);
this.position.x = startX;
this.position.y = startY;
}
update() {
if (!this.isEnabled) return;
super.update();
this.lifetime -= Time.deltaTime/1000;
}
draw() {
if (!this.isEnabled || !this.isVisible) return;
super.draw();
ctx.save();
// console.log("draw particle at: " + this.position.x + "," + this.position.y);
drawCircle(this.position.x, this.position.y, this.color, this.size);
ctx.restore();
}
}
class Animation {
constructor(name, interval = 150.0, animationFrames = [], playOnce = false, canInterrupt = true) {
this.name = name;
this.updateInterval = interval;
this.playOnce = playOnce;
this.interruptable = canInterrupt;
this.isPlaying = false;
this.updateTimer = 0.0;
this.frameIndex = 0;
this.frames = animationFrames;
}
update() {
if (!this.isPlaying) return;
if (this.frameIndex + 1 >= this.frames.length && this.playOnce) {
this.isPlaying = false;
this.frameIndex = 0;
return;
}
this.updateTimer += Time.deltaTime;
if (this.updateTimer >= this.updateInterval) {
this.updateTimer = 0.0;
let targetFrameIndex = (this.frameIndex + 1) % this.frames.length;
let frame = this.getCurrentFrame();
if (frame) {
if (!frame.continueWhen) {
this.frameIndex = targetFrameIndex;
return;
}
if (frame.continueWhen() === true) {
this.frameIndex = targetFrameIndex;
}
}
}
}
play() { this.isPlaying = true; }
stop() { this.isPlaying = false; }
addFrame(frame) { this.frames.push(frame); }
addFrames(framesToAdd) {
for (let i = 0; i < framesToAdd.length; ++i) {
this.addFrame(framesToAdd[i]);
}
}
getCurrentFrame() {
if (this.frames.length === 0) {
return null;
}
return this.frames[this.frameIndex];
}
getFrameAt(index) {
if (this.frames.length === 0 || index >= this.frames.length) {
return null;
}
return this.frames[index];
}
}
class AnimationFrame {
constructor(x, y, width, height, continueWhen) {
this.position = { x: x, y: y };
this.size = { width: width, height: height };
this.continueWhen = continueWhen;
}
}
class Button extends GameObject {
constructor() {
super();
this.states = [];
this.states["default"] = {callbacks: []};
this.states["hover"] = {callbacks: []};
this.states["active"] = {callbacks: []};
this.states["click"] = {callbacks: []};
this.state = "default";
this.borderOnInside = false;
this.width = 150;
this.height = 50;
this.text = "Button 1";
this.fontSize = 16;
this.fontColor = Color.getWhite();
this.font = "arial";
this.background = new Color(255, 0, 0);
this.border = new Color(0, 255, 0);
this.borderWidth = 1;
this.doubleBorder = false;
this.doubleBorderDistance = 5;
this.content = undefined; // appoint a gameobject and it will draw it as content :-)
this.contentScale = 1.0;
this.contentMargin = {top:0,left:0,right:0,bottom:0};
}
draw() {
if (!this.isEnabled || !this.isVisible) return
super.draw();
this.drawButtonBase();
this.drawContent();
this.drawText();
}
drawButtonBase() {
let fill = this.background;
let stroke = this.border;
switch (this.state) {
case "hover":
fill = fill.darker(22);
stroke = stroke.darker(22);
break;
case "active":
fill = fill.darker(44);
stroke = stroke.darker(44);
break;
}
const bw =ctx.lineWidth;
const fs =ctx.fillStyle;
const ss =ctx.strokeStyle;
ctx.save();
ctx.beginPath();
ctx.lineWidth = this.borderWidth;
ctx.fillStyle = fill.rgba();
ctx.strokeStyle = stroke.rgba();
ctx.rect(this.position.x, this.position.y, this.width, this.height);
ctx.fill();
if (this.borderOnInside === true) {
ctx.beginPath();
ctx.rect(this.position.x + this.borderWidth/2,
this.position.y + this.borderWidth/2,
this.width - (this.borderWidth),
this.height - (this.borderWidth));
}
ctx.stroke();
if(this.doubleBorder === true) {
const bd = this.doubleBorderDistance;
ctx.beginPath();
ctx.lineWidth = this.borderWidth;
ctx.fillStyle = fill.rgba();
ctx.strokeStyle = stroke.rgba();
ctx.rect(this.position.x + bd, this.position.y + bd, this.width - bd*2, this.height - bd*2);
ctx.fill();
ctx.stroke();
}
ctx.restore();
ctx.lineWidth = bw;
ctx.fillStyle = fs;
ctx.strokeStyle =ss;
}
drawContent() {
if (!this.content) return;
ctx.save();
let x = this.position.x + this.contentMargin.left;
let y = this.position.y + this.contentMargin.top;
ctx.translate(x, y);
ctx.scale(this.contentScale,this.contentScale);
this.content.draw();
ctx.restore();
}
drawText() {
if (!this.text || this.text.length === 0) return;
ctx.save();
ctx.font = this.fontSize + "px " + this.font;
const size = ctx.measureText(this.text);
ctx.fillStyle = this.fontColor.rgba();
ctx.fillText(this.text, this.position.x + (this.width / 2 - size.width/2), (this.position.y + this.fontSize) + (this.height/2 - this.fontSize/2));
ctx.restore();
}
update() {
if (!this.isEnabled) return;
super.update();
let oldState = this.state;
let click = false;
if (mouse.x >= this.position.x && mouse.x <= this.position.x + this.width &&
mouse.y >= this.position.y && mouse.y <= this.position.y + this.height) {
if (mouse.leftButton) {
if (this.state !== "active") {
this.state = "active";
}
} else {
if (this.state !== "hover") {
this.state = "hover";
click = oldState === "active";
}
}
} else if(this.state !== "default") {
this.state = "default"
}
if (oldState !== this.state) {
this.states[this.state].callbacks.forEach(x => x());
if (click) this.states["click"].callbacks.forEach(x => x());
}
}
on(state, callback) {
this.states[state].callbacks.push(callback);
}
}
class Sprite extends GameObject {
constructor(img) {
super();
this.image = img;
this.width= -1;
this.height=-1;
this.imageOffset = {x:0,y:0};
this.scale = {x:1,y:1};
this.origin = {x:0,y:0};
this.isTiledRepeat = false;
}
static fromUrl(src) {
const spriteImg = new Image();
spriteImg.src = src;
return new Sprite(spriteImg);
}
update() {
if (!this.isEnabled) return;
super.update();
}
draw() {
if (!ctx || !this.isVisible || !this.isEnabled || !this.image) return;
ctx.save();
super.draw();
// ctx.restore();
const w = this.image.width;
const h = this.image.height;
const cols = Math.floor(this.width / w) + 1;
const rows = Math.floor(this.height / h) + 1;
if (this.isTiledRepeat && !isNaN(cols) && !isNaN(rows) && cols > 0 && rows > 0 && cols < 1920 && rows < 1080) {
for(let x = 0; x < cols; x++) {
for(let y = 0; y < rows; y++) {
ctx.drawImage(this.image, x * w, y * h);
}
}
} else {
let camera = Camera.getMainCamera();
if (this.width <= 0) {
this.width = this.image.width;
this.height = this.image.height;
}
const scaledWidth = this.width * this.scale.x;
const scaledHeight = this.height * this.scale.y;
const bb = this.getBoundingBox();
const dx = this.origin.x > 0 ? -((bb.max.x - bb.min.x) * this.origin.x) : 0;
const dy = this.origin.y > 0 ? -((bb.max.y - bb.min.y) * this.origin.y) : 0;
const renderX = camera.viewport.x + this.position.x + this.offset.x + this.imageOffset.x + dx;
const renderY = camera.viewport.y + this.position.y + this.offset.y + this.imageOffset.y + dy;
// ctx.save();
ctx.globalAlpha = this.opacity;
if (this.rotation !== 0) {
ctx.translate(renderX-dx, renderY-dy);
ctx.rotate(this.rotation * Math.PI/180.0);
ctx.drawImage(this.image, 0, 0, this.image.width, this.image.height, dx, dy, scaledWidth, scaledHeight);
} else {
ctx.drawImage(
this.image,
0,
0,
this.image.width,
this.image.height,
renderX,
renderY,
scaledWidth,
scaledHeight);
}
}
ctx.restore(); // do restore again as we saved our context the first thing we did.
}
getBoundingBox() {
return new BoundingBox(
new Point(this.position.x, this.position.y),
new Point(this.position.x + (this.width * this.scale.x), this.position.y + (this.height * this.scale.y))
);
}
}
class AnimatedSprite extends GameObject {
constructor(spritesheet) {
super();
this.spritesheet = spritesheet;
this.animations = [];
this.currentAnimation = null;
this.flipHorizontal = false;
this.flipVertical = false;
}
addAnimation(animation) {
this.animations.push(animation);
}
playAnimation(key) {
if (this.currentAnimation) {
if (!this.currentAnimation.interruptable && this.currentAnimation.isPlaying) {
return;
}
this.currentAnimation.stop();
}
let targetAnimation = this.animations.find(x => x.name == key);
this.currentAnimation = targetAnimation;
this.currentAnimation.play();
}
update() {
if (!this.isEnabled) {
return;
}
if (this.currentAnimation) {
this.currentAnimation.update();
}
super.update();
}
draw() {
if (!ctx || !this.isVisible || !this.isEnabled || !this.currentAnimation)
return;
let frame = this.currentAnimation.getCurrentFrame();
if (!frame) return;
ctx.save();
super.draw();
if (this.collider) this.offset.x = (this.collider.bounds.max.x/2);
let camera = Camera.getMainCamera();
if (this.flipHorizontal) {
ctx.translate(camera.viewport.x + (this.position.x + this.offset.x + this.collider.bounds.max.x), this.position.y);
ctx.scale(-1, 1);
} else {
ctx.translate(this.position.x + ((-this.offset.x)+this.collider.bounds.max.x), this.position.y);
}
ctx.globalAlpha = this.opacity;
ctx.drawImage(
this.spritesheet,
frame.position.x,
frame.position.y,
frame.size.width,
frame.size.height,
this.flipHorizontal ? 0 : camera.viewport.x,
camera.viewport.y,
frame.size.width,
frame.size.height);
ctx.restore();
}
}
class Scene extends GameObject {
constructor() {
super();
}
update() {
super.update();
}
draw() {
super.draw();
}
}
function getMousePos(element, evt) {
var rect = element.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
function getTouchPos(canvas, evt) {
var rect = element.getBoundingClientRect();
return {
x: evt.touches[0].clientX - rect.left,
y: evt.touches[0].clientY - rect.top
};
}
function run(time) {
Time.deltaTime = (time - Time.time) * Time.timeScale;
Time.deltaTimeUnscaled = time - Time.time;
Time.time = time;
Physics.update();
if(onUpdate) onUpdate();
if(onDraw) onDraw();
Time.frameCount++;
window.requestAnimationFrame(run);
}
function clear(clearStyle) {
if (isWebGl) {
ctx.clearColor(0,0,0.8,1);
ctx.clear(gl.COLOR_BUFFER_BIT);
return;
}
if (clearStyle) {
ctx.fillStyle = clearStyle;
ctx.fillRect(0, 0, canvas.width, canvas.height);
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
function mouseMove(evt) {
let pos = getMousePos(canvas, evt);
mouse.x = pos.x;
mouse.y = pos.y;
}
function touchMove(evt) {
evt.preventDefault();
let pos = getTouchPos(canvas, evt);
mouse.x = pos.x;
mouse.y = pos.y;
}
function mouseDown(evt) {
if (evt.button === 0) mouse.leftButton = true;
if (evt.button === 1) mouse.rightButton = true;
if (evt.button === 3) mouse.middleButton = true;
}
function mouseUp(evt) {
if (evt.button === 0) mouse.leftButton = false;
if (evt.button === 1) mouse.rightButton = false;
if (evt.button === 3) mouse.middleButton = false;
}
</script>
<script >let selectedTreeElement = undefined;
let selectedTreeItem = undefined;
let inspectorPanel = document.querySelector(".selector");
let inspectorLayerTools = inspectorPanel.querySelector(".layer-tools");
let inspectorGroupTools = inspectorPanel.querySelector(".group-tools");
let inspectorTileSelector = inspectorPanel.querySelector(".tile-selector");
let inspectorBrushSize = inspectorLayerTools.querySelector(".brush-size");
let inputBrushSize = inspectorBrushSize.querySelector("#brush-size");
let tileListElement = inspectorPanel.querySelector(".tile-list");
let itemDetailElement = inspectorPanel.querySelector(".item-details");
let itemDetailTypeIconElement = itemDetailElement.querySelector(".item-type-icon");
let itemDetailNameInputElement = itemDetailElement.querySelector(".item-name");
let mapTreeElement = document.querySelector(".project-item-tree");
let container = document.querySelector(".editor-container");
let windowBackgroundTint = document.querySelector(".window-tint");
let createMapWindow = document.querySelector("#create-map-window");
let loadMapWindow = document.querySelector("#load-map-window");
let loadTilesetWindow = document.querySelector("#load-tilesets-window");
let loadTilesetWindowProgressBarValue = loadTilesetWindow.querySelector(".progress-bar-value");
let backToolName = undefined;
let activeTool = undefined;
let activeToolName = "cursor";
let tools = ["cursor", "brush", "eraser", "move", "fill"];
let isWindowOpen = false;
let isRenamingTreeItem = false;
let zoomIntensity = 0.1;
let brushSize = 1;
let maxBrushSize = 8;
let selectedTilePage = 0;
let selectedTileElement = undefined;
let selectedTile = undefined;
let tilePageCount = 1; // should be the same as tilesets.length
let tilesets = [];
let isLoadingTilesets = true;
let tilesetLoadCount = 0;
let tilesetLoadCompleteCount = 0;
let toastyPlayed = false;
let tilesetPagingForIsoEnabled = false;
class IsometricMapRenderer {
constructor() {
this.tileDepth=80;
this.renderIndex=0;
}
draw(map) {
this.renderIndex = 0;
if (!map) return;
this.drawGrid(map);
this.drawRecursive(map);
if (map.hoverVisible) {
this.drawBrush(map);
}
}
drawBrush(map) {
let mousePoint = this.screenToWorldPoint(map, mouse.x / ctxScaleX, mouse.y / ctxScaleY);
for(let y = 0; y < brushSize;++y) {if (window.CP.shouldStopExecution(2)){break;}
for (let x = 0; x < brushSize;++x) {if (window.CP.shouldStopExecution(1)){break;}
let renderPoint = this.getRenderPoint(mousePoint.x+x, mousePoint.y+y);
this.drawTileHover(renderPoint.x, renderPoint.y, map.tileWidth, map.tileHeight);
}
window.CP.exitedLoop(1);
}
window.CP.exitedLoop(2);
}
drawRecursive(map, currentLayer) {
if (typeof currentLayer == "undefined") {
for (let item of map.children) {if (window.CP.shouldStopExecution(3)){break;}
this.drawRecursive(map, item);
}
window.CP.exitedLoop(3);
} else {
if (currentLayer.visible) {
if (currentLayer instanceof MapLayerGroup) {
for (let item of currentLayer.children) {if (window.CP.shouldStopExecution(4)){break;}
this.drawRecursive(map, item);
}
window.CP.exitedLoop(4);
} else {
this.drawLayer(map, currentLayer);
}
}
this.renderIndex++;
}
}
drawGrid(map) {
let camera = Camera.getMainCamera();
let tileWidth = map.tileWidth;
let tileHeight = map.tileHeight;
let tile_half_width = tileWidth / 2;
let tile_half_height = tileHeight / 2;
for (let tileX = 0; tileX < map.width; ++tileX) {if (window.CP.shouldStopExecution(6)){break;}
for (let tileY = 0; tileY < map.height; ++tileY) {if (window.CP.shouldStopExecution(5)){break;}
let renderX = camera.viewport.x + (tileX - tileY) * tile_half_width;
let renderY = camera.viewport.y + (tileX + tileY) * tile_half_height;
this.drawGridTile(renderX, renderY, tileWidth, tileHeight);
}
window.CP.exitedLoop(5);
}
window.CP.exitedLoop(6);
}
drawLayer(map, layer) {
let camera = Camera.getMainCamera();
let tileWidth = map.tileWidth;
let tileHeight = map.tileHeight;
let tile_half_width = tileWidth / 2;
let tile_half_height = tileHeight / 2;
for (let tileX = 0; tileX < map.width; ++tileX) {if (window.CP.shouldStopExecution(8)){break;}
for (let tileY = 0; tileY < map.height; ++tileY) {if (window.CP.shouldStopExecution(7)){break;}
let renderX = camera.viewport.x + (tileX - tileY) * tile_half_width;
let renderY = camera.viewport.y + (tileX + tileY) * tile_half_height;
this.drawTile(map, layer.tileData[tileY * map.width + tileX], renderX, renderY-48-(this.renderIndex*12), tileWidth, tileHeight);
}
window.CP.exitedLoop(7);
}
window.CP.exitedLoop(8);
}
getRenderPoint(tileX, tileY) {
let camera = Camera.getMainCamera();
let tileWidth = map.tileWidth;
let tileHeight = map.tileHeight;
let tile_half_width = tileWidth / 2;
let tile_half_height = tileHeight / 2;
let renderX = camera.viewport.x + (tileX - tileY) * tile_half_width;
let renderY = camera.viewport.y + (tileX + tileY) * tile_half_height;
return {
x: renderX,
y: renderY
}
}
drawGridTile(x, y, width, height) {
this.drawTileGraphics(x, y, width, height, 'rgba(255,255,255,0.4)', 'rgba(25,34, 44,0.2)', [5], 1);
}
drawTile(map, tileData, x, y, width, height) {
if (!tileData || tileData.id === -1)
return;
let tileset = getTilesetById(tileData.tileset, map.type);
let tile = tileset.getTile(tileData.id);
if (!tile || !tile.src)
return;
// this.drawTileGraphics(x, y, width, height, 'rgba(255,255,255,0.4)', 'rgba(25,34, 44,0.2)', [5], 1);
// ctx.drawImage(tile.src, tile.x, tile.y, tile.width, tile.height, x, y, tile.width, tile.height);
let offsetY = this.tileDepth - height;
ctx.drawImage(tile.src, x, y+offsetY-(height/2));
}
drawTileHover(x, y, width, height){
this.drawTileGraphics(x, y, width, height, 'rgba(30,250,42,0.4)', 'rgba(30,250,42,0.1)', [], 1);
}
drawTileGraphics(x, y, width, height, strokeStyle, fillStyle, lineDash, lineWidth) {
ctx.beginPath();
ctx.setLineDash(lineDash);
ctx.strokeStyle = strokeStyle;
ctx.fillStyle = fillStyle;
ctx.lineWidth = lineWidth;
ctx.moveTo(x, y);
ctx.lineTo(x + width/2, y-height/2);
ctx.lineTo(x + width, y);
ctx.lineTo(x + width/2, y + height/2);
ctx.lineTo(x, y);
ctx.stroke();
ctx.fill();
}
worldToScreenPoint(map, x, y) {
let camera = Camera.getMainCamera();
let tileWidth = map.tileWidth;
let tileHeight = map.tileHeight;
let tile_half_width = tileWidth / 2;
let tile_half_height = tileHeight / 2;
let renderX = (x - y) * tile_half_width - camera.viewport.x;
let renderY = (x + y) * tile_half_height - camera.viewport.y;
return {
x: renderX,
y: renderY
}
}
screenToWorldPoint(map, x, y) {
let camera = Camera.getMainCamera();
let tile_height = map.tileHeight;
let tile_width = map.tileWidth;
let mouse_y = y - camera.viewport.y;
let mouse_x = x - camera.viewport.x;
return {
x: Math.floor((mouse_y / tile_height) + (mouse_x / tile_width)),
y: Math.floor((-mouse_x / tile_width) + (mouse_y / tile_height))+1
};
}
}
class MapRenderer {
draw(map) {
if (!map) return;
this.drawGrid(map);
this.drawRecursive(map);
if (map.hoverVisible) {
this.drawBrush(map);
}
}
drawBrush(map) {
let mousePoint = this.screenToWorldPoint(map, mouse.x / ctxScaleX, mouse.y / ctxScaleY);
for(let y = 0; y < brushSize;++y) {if (window.CP.shouldStopExecution(10)){break;}
for (let x = 0; x < brushSize;++x) {if (window.CP.shouldStopExecution(9)){break;}
let renderPoint = this.getRenderPoint(mousePoint.x+x, mousePoint.y+y);
this.drawTileHover(map, renderPoint.x, renderPoint.y);
}
window.CP.exitedLoop(9);
}
window.CP.exitedLoop(10);
}
drawRecursive(map, currentLayer) {
if (typeof currentLayer == "undefined") {
for (let item of map.children) {if (window.CP.shouldStopExecution(11)){break;}
this.drawRecursive(map, item);
}
window.CP.exitedLoop(11);
} else {
if (currentLayer.visible) {
if (currentLayer instanceof MapLayerGroup) {
for (let item of currentLayer.children) {if (window.CP.shouldStopExecution(12)){break;}2
this.drawRecursive(map, item);
}
window.CP.exitedLoop(12);
} else {
this.drawLayer(map, currentLayer);
}
}
}
}
drawGrid(map) {
let camera = Camera.getMainCamera();
for(let y = 0; y < map.height; ++y) {if (window.CP.shouldStopExecution(14)){break;}
for (let x = 0; x < map.width; ++x) {if (window.CP.shouldStopExecution(13)){break;}
let renderX = camera.viewport.x + (x * map.tileWidth);
let renderY = camera.viewport.y + (y * map.tileHeight);
this.drawGridTile(map, renderX, renderY);
}
window.CP.exitedLoop(13);
}
window.CP.exitedLoop(14);
}
drawLayer(map, layer) {
let camera = Camera.getMainCamera();
for(let y = 0; y < map.height; ++y) {if (window.CP.shouldStopExecution(16)){break;}
for (let x = 0; x < map.width; ++x) {if (window.CP.shouldStopExecution(15)){break;}
let renderX = camera.viewport.x + (x * map.tileWidth);
let renderY = camera.viewport.y + (y * map.tileHeight);
this.drawTile(layer.tileData[y * map.width + x], map, renderX, renderY);
}
window.CP.exitedLoop(15);
}
window.CP.exitedLoop(16);
}
getRenderPoint(x, y) {
let camera = Camera.getMainCamera();
return {
x: camera.viewport.x + (x * map.tileWidth),
y: camera.viewport.y + (y * map.tileHeight)
}
}
drawGridTile(map, x, y) {
this.drawTileGraphics(map, x, y, 'rgba(255,255,255,0.4)', 'rgba(25,34, 44,0.2)', [1], 1);
}
drawTile(tileData, map, x, y) {
if (!tileData || tileData.id === -1)
return;
let tileset = getTilesetById(tileData.tileset, map.type);
let tile = tileset.getTile(tileData.id);
ctx.drawImage(tileset.src, tile.x, tile.y, tile.width, tile.height, x, y, tile.width, tile.height);
// this.drawTileGraphics(map, x, y, 'rgba(255,255,255,0.4)', 'rgba(25,34, 44,0.2)', [1], 1);
}
drawTileHover(map, x, y) {
this.drawTileGraphics(map, x, y, 'rgba(30,250,42,0.4)', 'rgba(30,250,42,0.1)', [], 1);
}
drawTileGraphics(map, x, y, strokeStyle, fillStyle, lineDash, lineWidth) {
ctx.beginPath();
ctx.setLineDash(lineDash);
ctx.strokeStyle = strokeStyle;
ctx.fillStyle = fillStyle;
ctx.lineWidth = lineWidth;
ctx.rect(x, y, map.tileWidth, map.tileHeight);
ctx.fill();
ctx.stroke();
}
worldToScreenPoint(map, x, y) {
let camera = Camera.getMainCamera();
return {
x: (map.tileWidth * x) - camera.viewport.x,
y: (map.tileHeight * y) - camera.viewport.y
}
}
screenToWorldPoint(map, x, y) {
let camera = Camera.getMainCamera();
let mouse_y = y - camera.viewport.y;
let mouse_x = x - camera.viewport.x;
return {
x: Math.floor(mouse_x / map.tileWidth),
y: Math.floor(mouse_y / map.tileHeight)
};
}
}
class Map {
constructor(type, width, height, tileWidth, tileHeight, renderer) {
this.type = type;
this.width = width;
this.height = height;
this.tileWidth = tileWidth;
this.tileHeight = tileHeight;
this.children = []; // both layer and groups
this.renderer = renderer;
this.hoverVisible = false;
this.itemId = 0;
}
static createIso(width, height) {
resetScale();
Camera.getMainCamera()
.setViewport(canvas.width/2-48, canvas.height/2-((48*height)/2));
return new Map("iso", width, height, 96, 48, new IsometricMapRenderer());
}
static create(width, height) {
resetScale();
Camera.getMainCamera()
.setViewport(canvas.width/2-((32*width)/2), canvas.height/2-((32*height)/2));
return new Map("top", width, height, 32, 32, new MapRenderer());
}
draw() {
this.renderer.draw(this);
}
getHoverTile() {
return this.renderer.screenToWorldPoint(this, mouse.x/ ctxScaleX, mouse.y/ ctxScaleY);
}
clone(item) {
if (!item.type) { // check if the property: type exists, it should only exist on layers
alert("Groups cannot be cloned yet.");
return; // we can't clone groups right now
}
let newLayer = this.createLayer(item.parent);
newLayer.name = item.name;
newLayer.type = item.type;
return newLayer;
}
createLayer(parent) {
let layer = new MapLayer(this.type, ++this.itemId, "New layer", "normal", this.width, this.height);
layer.properties = {};
if (parent) {
layer.parent = parent;
parent.children.push(layer);
} else {
this.children.push(layer);
layer.parent = this;
}
return layer;
}
createGroup(parent) {
let group = new MapLayerGroup(++this.itemId, "New group");
if (parent) {
group.parent = parent;
parent.children.push(group);
} else {
this.children.push(group);
group.parent = this;
}
return group;
}
}
class MapLayerGroup {
constructor(id, name) {
this.name = name;
this.id = id;
this.children = [];
this.visible = true;
this.parent = undefined;
}
}
class MapLayerTile {
constructor(id, tileset) {
this.id = id;
this.tileset = tileset;
}
static empty() {
return new MapLayerTile(-1, -1);
}
}
class MapLayer {
constructor(mapType, id, name, layerType, width, height) {
this.name = name;
this.id = id;
this.type = layerType;
this.width = width;
this.height = height;
this.visible = true;
this.parent = undefined;
this.tileData = [];
for (let y = 0; y < height; ++y)
{if (window.CP.shouldStopExecution(18)){break;}for (let x = 0; x < width; ++x)
{if (window.CP.shouldStopExecution(17)){break;}this.tileData[y * width + x] = MapLayerTile.empty();
window.CP.exitedLoop(17);
}}
window.CP.exitedLoop(18);
this.properties = mapType === "iso"
? this.createIsoProperties()
: this.createStandardProperties();
}
createIsoProperties() {
return {
heightLevel: 0,
block: false,
};
}
createStandardProperties() {
return {
block: false
};
}
}
class FillTool {
constructor() {
this.stackSize = 16777216; //avoid possible overflow exception
this.stackptr = 0;
this.stack = [];
this.h = 0;
this.w = 0;
this.mouseWasDown=false;
}
update() {
if (isGroupSelected()) return;
if (!mouse.leftButton && this.mouseWasDown) {
this.mouseWasDown = false;
this.w = map.width;
this.h = map.height;
let camera = Camera.getMainCamera();
// todo:
// this.floodFill(
// selectedTreeItem, x, y,
// selectedTile.id,
// layer.tileData[this.getIndex(x, y)].id);
}
if (mouse.leftButton) {
this.mouseWasDown = true;
}
}
floodFill(layer, x, y, newTile, oldTile) {
if (newTile === oldTile) return;
this.emptyStack();
var x1, spanAbove, spanBelow, val, index;
if (this.push(x, y) === undefined) return;
while ((val = this.pop()) !== undefined) {if (window.CP.shouldStopExecution(21)){break;}
x1=val.x;
while (x1>=0&&layer.tileData[this.getIndex(x1, y)].id==oldTile){if (window.CP.shouldStopExecution(19)){break;}x1--;}
window.CP.exitedLoop(19);
x1++; spanAbove=spanBelow=0;
while(x1<this.w&&layer.tileData[this.getIndex(x1, y)].id==oldTile){if (window.CP.shouldStopExecution(20)){break;}
layer.tileData[this.getIndex(x1, y)].type = newTile;
if (!spanAbove&&y>0&&layer.tileData[this.getIndex(x1, y-1)].id==oldTile) {
if(this.push(x1, y-1) === undefined) return;
spanAbove=1;
} else if(spanAbove&&y>0&&layer.tileData[this.getIndex(x1, y-1)].id!=oldTile) {
spanAbove=0;
} else if(!spanBelow&&y>h-1&&layer.tileData[this.getIndex(x1, y+1)].id==oldTile) {
if(this.push(x1,y+1) === undefined) return;
spanBelow=1;
} else if(spanAbove&&y>0&&layer.tileData[this.getIndex(x1, y+1)].id!=oldTile) {
spanBelow=0;
}
x1++;
}
window.CP.exitedLoop(20);
}
window.CP.exitedLoop(21);
}
getIndex(x,y) {
return y * this.w + x;
}
pop() {
if (this.stackptr > 0) {
let p = this.stack[this.stackptr];
let x = p / this.h;
let y = p % this.h;
this.stackptr--;
return { x: x, y: y };
}
return undefined;
}
push(x, y) {
if (this.stackptr < this.stackSize - 1) {
this.stackptr++;
this.stack[this.stackptr] = this.h * x + y;
return true;
}
return undefined;
}
emptyStack() {
while(this.pop() !== undefined){if (window.CP.shouldStopExecution(22)){break;};}
window.CP.exitedLoop(22);
}
}
class MoveTool {
constructor() {
this.isDragging = false;
this.dragStartX = 0;
this.dragStartY = 0;
this.dragCameraStartY = 0;
this.dragCameraStartX = 0;
}
update() {
if (mouse.leftButton) {
let camera = Camera.getMainCamera();
if (!this.isDragging) {
this.isDragging = true;
this.dragStartX = mouse.x / ctxScaleX;
this.dragStartY = mouse.y / ctxScaleY;
this.dragCameraStartX = camera.viewport.x;
this.dragCameraStartY = camera.viewport.y;
} else {
camera.viewport.x = this.dragCameraStartX - (this.dragStartX - mouse.x / ctxScaleX);
camera.viewport.y = this.dragCameraStartY - (this.dragStartY - mouse.y / ctxScaleY);
}
} else {
this.isDragging = false;
}
}
}
class BrushTool {
constructor() {
}
update() {
if (isGroupSelected()||!selectedTile||!selectedTileElement) return;
if (mouse.leftButton) {
let layer = selectedTreeItem;
let brush = selectedTile;
let tileStart = map.getHoverTile();
this.paint(layer, tileStart, brush, brushSize);
}
// todo: add undo support?
}
paint(layer, position, brush, size) {
if (!brush) return;
if (position.x < 0 || position.y < 0 || position.x >= layer.width || position.y >= layer.height)
return;
for(let y = 0; y < size; ++y)
{if (window.CP.shouldStopExecution(24)){break;}for(let x = 0; x < size; ++x) {if (window.CP.shouldStopExecution(23)){break;}
let idx = (position.y+y) * layer.width + (position.x+x);
if (idx < layer.tileData.length) {
let tile = layer.tileData[idx];
tile.id = brush.id;
tile.tileset = brush.tileset;
}
}
window.CP.exitedLoop(23);
}
window.CP.exitedLoop(24);
}
}
class EraserTool {
constructor() {
}
update() {
if (isGroupSelected()||!selectedTile||!selectedTileElement) return;
if (mouse.leftButton) {
let layer = selectedTreeItem;
let tileStart = map.getHoverTile();
this.clear(layer, tileStart, brushSize);
}
// todo: add undo support?
}
clear(layer, position, size) {
if (position.x < 0 || position.y < 0 || position.x >= layer.width || position.y >= layer.height)
return;
for(let y = 0; y < size; ++y)
{if (window.CP.shouldStopExecution(26)){break;}for(let x = 0; x < size; ++x) {if (window.CP.shouldStopExecution(25)){break;}
let idx = (position.y+y) * layer.width + (position.x+x);
if (idx < layer.tileData.length) {
let tile = layer.tileData[idx];
tile.id = -1;
tile.tileset = -1;
}
}
window.CP.exitedLoop(26);
window.CP.exitedLoop(25);
}
}
}
class Tileset {
constructor(id, type, width, height, tileWidth, tileHeight, src) {
this.id = id;
this.type = type;
this.width = width; // amount of tiles per row
this.height = height;// amount of rows
this.tileWidth=tileWidth;
this.tileHeight = tileHeight;
this.tiles = []; // array of TilesetSource
this.src = src; // undefined if the source exists in the tiles:TilesetSource
}
getTile(id) {
for(let i = 0; i < this.tiles.length; ++i) {if (window.CP.shouldStopExecution(27)){break;}
if (this.tiles[i].id == id) return this.tiles[i];
}
window.CP.exitedLoop(27);
return undefined;
}
}
class TilesetSource {
constructor(id, x, y, width, height, src) {
this.id = id; // used for identifying which tile it is when drawing
this.src = src; // should only be defined if we have 1 tile per image
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
}
function getTilesetById(id, type) {
for(let i = 0; i < tilesets.length; ++i) {if (window.CP.shouldStopExecution(28)){break;}
if (tilesets[i].id == id && tilesets[i].type == type) return tilesets[i];
}
window.CP.exitedLoop(28);
return undefined;
}
const resize = () => {
canvas.style.left = "1px";
canvas.style.top = "33px";
canvas.width = container.clientWidth-2;
canvas.height = container.clientHeight-34;
};
let toasty = new Audio("https://www.dropbox.com/s/cql3setstbtz9r2/TOASTY%21.mp3?raw=1");
canvas = document.querySelector(".editor"); // needed in the createIso function. Otherwise its not necessary to assign it here
resize();
// let map = Map.create(8, 8);
let map = Map.createIso(8, 8);
createLayer(true);
window.addEventListener('mousewheel', evt => {
if (activeToolName !== "move") {
return;
}
let camera = Camera.getMainCamera();
let scaleChange = evt.wheelDelta/120;
var zoom = Math.exp(scaleChange*zoomIntensity);
ctxScaleX *= zoom;
ctxScaleY *= zoom;
});
itemDetailNameInputElement.addEventListener("input", e => {
if (selectedTreeItem && selectedTreeElement) {
selectedTreeItem.name = itemDetailNameInputElement.value;
selectedTreeElement.innerHTML = itemDetailNameInputElement.value;
}
}, false);
window.addEventListener("keydown", evt => {
if (isWindowOpen
||isRenamingTreeItem
||document.activeElement === itemDetailNameInputElement
||document.activeElement === inputBrushSize) return;
evt.stopPropagation();
evt.preventDefault();
if (evt.which === 0x31) selectEditorTool(tools[0]);
if (evt.which === 0x32) selectEditorTool(tools[1]);
if (evt.which === 0x33) selectEditorTool(tools[2]);
if (evt.which === 0x34) selectEditorTool(tools[3]);
if (evt.which === 0x6B || evt.which === 0xAB) zoomIn();
if (evt.which === 0x6D || evt.which === 0xAD) zoomOut();
if (evt.altKey) {
if (activeToolName !== "move") {
backToolName = activeToolName;
selectEditorTool("move");
}
}
}, false);
window.addEventListener("keyup", evt => {
if (isWindowOpen
||isRenamingTreeItem
||document.activeElement === itemDetailNameInputElement
||document.activeElement === inputBrushSize) return;
if (!evt.altKey && backToolName !== undefined) {
selectEditorTool(backToolName);
backToolName = undefined;
}
}, false);
const draw = () => {
// ctx.fillRect(...)
let hoverTile = {x:0, y:0};
clear("black");
ctx.scale(ctxScaleX, ctxScaleY);
if (map) {
map.hoverVisible = activeToolName !== undefined
&& (activeToolName === "eraser" || activeToolName === "brush");
map.draw();
hoverTile = map.getHoverTile();
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = "white";
ctx.strokeStyle = "white";
ctx.font = "14px Arial";
ctx.fillText(`${mouse.x}, ${mouse.y}`, 5, 17);
ctx.fillText(`${hoverTile.x}, ${hoverTile.y}`, 5, 30);
};
const update = () => {
// logic here, called just before draw is called
if (activeTool) {
activeTool.update();
}
};
loadTileSets();
setup(".editor", draw, update, resize);
/*
App and map io functions
*/
function newMap() {
showWindow(createMapWindow);
}
function loadMap() {
showWindow(loadMapWindow);
}
function saveMap() {}
function showSettings() {}
function showAbout() {
displayToasty();
}
function cancelCreateMap() {
closeWindow(createMapWindow);
}
function cancelLoadMap() {
closeWindow(loadMapWindow);
}
function openMap() {
closeWindow(loadMapWindow);
}
function createMap() {
closeWindow(createMapWindow);
let mapType = "top";
let mapWidth = parseInt(document.querySelector("#input-map-width").value || 32)
let mapHeight = parseInt(document.querySelector("#input-map-height").value || 32);
for(let radio of document.getElementsByClassName("map-perspective")) {if (window.CP.shouldStopExecution(29)){break;}
if (radio.checked) {
mapType = radio.value;
break;
}
}
window.CP.exitedLoop(29);
if (mapType === "iso") {
map = Map.createIso(mapWidth, mapHeight);
} else {
map = Map.create(mapWidth, mapHeight);
}
clearTreeItems();
createLayer(true);
buildTileSelectorPage(0);
}
function showWindow(hwnd) {
hwnd.style.display = "block";
windowBackgroundTint.style.display = "block";
isWindowOpen = true;
}
function closeWindow(hwnd) {
hwnd.style.display = "none";
windowBackgroundTint.style.display = "none";
isWindowOpen = false;
}
/*
Map layer functions
*/
function createLayerGroup() {
return createTreeItem("group", x => map.createGroup(x)) ;
}
function createLayer(skipRename) {
return createTreeItem("layer", x => map.createLayer(x),skipRename) ;
}
function createLayerFromItem(item) {
let l = createLayer(true);
l.item.name = item.name;
l.item.visible = item.visible;
l.item.properties = item.properties;
l.item.tileData = JSON.parse(JSON.stringify(item.tileData));
l.node.innerHTML = item.name;
}
function clearTreeItems() {
if (mapTreeElement) {
mapTreeElement.innerHTML = "";
}
}
function createTreeItem(type, factory, skipRename) {
if (!map) return;
if (isRenamingTreeItem) acceptRenameTreeItem();
var group;
var parent = mapTreeElement;
if (isGroupSelected()) {
group = factory(selectedTreeItem);
parent = selectedTreeElement.parentElement.querySelector(".children");
}
else {
group = factory();
}
let layerElementWrapper = document.createElement("li");
layerElementWrapper.classList.add(type);
layerElementWrapper.setAttribute("data-id", group.id);
let itemNameElement = document.createElement("div");
itemNameElement.classList.add("item-name");
itemNameElement.classList.add(type);
itemNameElement.innerHTML = group.name;
layerElementWrapper.appendChild(itemNameElement);
let itemVisibilityElement = document.createElement("i");
itemVisibilityElement.classList.add("item-visibility");
itemVisibilityElement.classList.add("visible");
itemVisibilityElement.addEventListener("click", e => {
group.visible = !group.visible;
if (group.visible) {
itemVisibilityElement.classList.remove("not-visible");
itemVisibilityElement.classList.add("visible");
} else {
itemVisibilityElement.classList.remove("visible");
itemVisibilityElement.classList.add("not-visible");
}
}, false);
layerElementWrapper.appendChild(itemVisibilityElement);
if (type === "group") {
let childlist = document.createElement("ul");
childlist.classList.add("children");
layerElementWrapper.appendChild(childlist);
}
itemNameElement.addEventListener("click", e => {
e.stopPropagation();
selectTreeItem(group, itemNameElement);
}, false);
itemNameElement.addEventListener("dblclick", e => {
e.stopPropagation();
showRenameTreeItem(group, itemNameElement);
}, false);
parent.appendChild(layerElementWrapper);
if (skipRename)
selectTreeItem(group, itemNameElement);
else showRenameTreeItem(group, itemNameElement);
return {
item: group,
node: itemNameElement
};
}
function showDeleteTreeItemAndChildren(item, elm) {
if (elm.classList.contains("group")) {
if (!confirm("Are you sure you want to delete this group and all its children?")) {
return;
}
}
else if (!confirm("Are you sure you want to delete this layer?")) {
return;
}
let index = item.parent.children.indexOf(item);
if (index === -1) {
alert("Error removing item!!");
return;
}
elm.parentElement.parentElement.removeChild(elm.parentElement);
item.parent.children.remove(index);
setLayerButtonState(false);
clearSelectionDetails();
hideInspector();
selectedTreeItem = undefined;
selectedTreeElement = undefined;
}
function selectTreeItem(item, elm) {
if (selectedTreeElement === elm) {
return;
}
if (selectedTreeElement !== undefined) {
acceptRenameTreeItem();
selectedTreeElement.classList.remove("selected");
}
elm.classList.add("selected");
selectedTreeElement = elm;
selectedTreeItem = item;
setLayerButtonState(true);
updateSelectionDetails();
showInspector();
}
function acceptRenameTreeItem() {
if (isRenamingTreeItem) {
isRenamingTreeItem=false;
let input = selectedTreeElement.querySelector("input");
if (input) {
selectedTreeItem.name = input.value;
selectedTreeElement.innerHTML = input.value;
}
}
}
function showRenameTreeItem(item, elm) {
if (isRenamingTreeItem && selectedTreeItem === elm) {
return;
} else if (isRenamingTreeItem) {
acceptRenameTreeItem();
}
selectTreeItem(item, elm);
isRenamingTreeItem = true;
let input = document.createElement("input");
input.classList.add("name-editor");
elm.innerHTML = "";
input.addEventListener("keydown", evt => {
if (evt.keyCode === 27) {
// cancel
isRenamingTreeItem=false;
elm.innerHTML = item.name;
return;
}
}, false);
input.addEventListener("keypress", evt=> {
if (evt.which === 13) {
// accept
isRenamingTreeItem=false;
item.name = input.value;
elm.innerHTML = item.name;
updateSelectionDetails();
return;
}
}, false);
input.value = item.name;
elm.appendChild(input);
input.select();
}
function isGroupSelected() {
return selectedTreeItem && selectedTreeElement.classList.contains("group");
}
function duplicateLayer(btn) {
if (btn.getAttribute("disabled")||isGroupSelected()) return;
if (isRenamingTreeItem) acceptRenameTreeItem();
createLayerFromItem(selectedTreeItem);
updateSelectionDetails();
}
function moveLayerUp(btn) {
if (btn.getAttribute("disabled")) return;
if (isRenamingTreeItem) acceptRenameTreeItem();
let p = selectedTreeItem.parent;
let i = p.children.indexOf(selectedTreeItem);
if (i === -1 || i === 0) return;
let listItem = selectedTreeElement.parentElement;
let list = listItem.parentElement;
let prev = listItem.previousSibling;
let old = list.removeChild(listItem);
list.insertBefore(old, prev);
let prev2 = p.children[i-1];
p.children[i] = prev2;
p.children[i-1] = selectedTreeItem;
}
function moveLayerDown(btn) {
if (btn.getAttribute("disabled")) return;
if (isRenamingTreeItem) acceptRenameTreeItem();
let p = selectedTreeItem.parent;
let i = p.children.indexOf(selectedTreeItem);
if (i === -1 || i === p.children.length - 1) return;
let listItem = selectedTreeElement.parentElement;
let list = listItem.parentElement;
let next = listItem.nextSibling;
let old = list.removeChild(next);
list.insertBefore(old, listItem);
let next2 = p.children[i+1];
p.children[i] = next2;
p.children[i+1] = selectedTreeItem;
}
function removeLayerOrGroup(btn) {
if (btn.getAttribute("disabled")||isRenamingTreeItem) return;
showDeleteTreeItemAndChildren(selectedTreeItem, selectedTreeElement);
}
function clearSelectionDetails() {
itemDetailTypeIconElement.className = "";
itemDetailNameInputElement.value = "";
hideInspectorTools();
}
function updateSelectionDetails() {
itemDetailNameInputElement.value = selectedTreeItem.name;
itemDetailTypeIconElement.className = "";
itemDetailTypeIconElement.classList.add("item-type-icon");
itemDetailTypeIconElement.classList.add("fa");
hideInspectorTools();
if (isGroupSelected()) {
itemDetailTypeIconElement.classList.add("fa-folder");
showGroupInspectorTools();
}
else {
itemDetailTypeIconElement.classList.add("fa-file");
updateLayerInspectorTools();
}
}
function hideInspectorTools() {
inspectorLayerTools.style.display = "none";
inspectorGroupTools.style.display = "none";
}
function showGroupInspectorTools() {
inspectorGroupTools.style.display = "block";
inspectorLayerTools.style.display = "none";
}
function updateLayerInspectorTools() {
inspectorGroupTools.style.display = "none";
inspectorLayerTools.style.display = "block";
if (activeToolName === "brush") {
inspectorTileSelector.style.display = "block";
} else {
inspectorTileSelector.style.display = "none";
}
if (activeToolName === "brush" || activeToolName === "eraser") {
inspectorBrushSize.style.display = "block";
} else {
inspectorBrushSize.style.display = "none";
}
}
function hideInspector() {
inspectorPanel.style.display = "none";
resize();
}
function showInspector() {
if (inspectorPanel.style.display !== "block") {
inspectorPanel.style.display = "block";
resize();
}
}
function setLayerButtonState(enabled) {
let elms = document.getElementsByClassName("req-layer");
for(let elm of elms) {if (window.CP.shouldStopExecution(30)){break;}
if (enabled) {
elm.removeAttribute("disabled");
} else {
elm.setAttribute("disabled","disabled");
}
}
window.CP.exitedLoop(30);
}
/*
Map editor functions
*/
function selectEditorTool(tool) {
document.querySelector("#btn-editor-" + activeToolName).classList.remove("active");
document.querySelector("#btn-editor-" + tool).classList.add("active");
activeToolName = tool;
switch(tool) {
case "cursor":
activeTool = undefined;
break;
case "brush":
activeTool = new BrushTool();
break;
case "eraser":
activeTool = new EraserTool();
break;
case "move":
activeTool = new MoveTool();
break;
case "fill":
activeTool = new FillTool();
break;
}
updateLayerInspectorTools();
}
function zoomOut() {
ctxScaleX-=zoomIntensity;
ctxScaleY-=zoomIntensity;
}
function zoomIn() {
ctxScaleX+=zoomIntensity;
ctxScaleY+=zoomIntensity;
}
function brushSizeChanged() {
if (!inputBrushSize) return;
brushSize = parseInt(inputBrushSize.value||"1");
if (brushSize > maxBrushSize) {
brushSize = maxBrushSize;
inputBrushSize.value = brushSize;
}
if (brushSize <= 0) {
brushSize = 1;
inputBrushSize.value = brushSize;
}
}
/*
Tile selector functions
*/
function previousTilePage() {
if (!tilesetPagingForIsoEnabled && map.type == "iso") return; // for now
selectedTilePage--;
if (selectedTilePage < 0) selectedTilePage = 0;
else {
buildTileSelectorPage(selectedTilePage);
}
}
function nextTilePage() {
if (!tilesetPagingForIsoEnabled && map.type == "iso") return; // for now
selectedTilePage++;
if (selectedTilePage >= tilePageCount) {
selectedTilePage = tilePageCount-1;
} else {
buildTileSelectorPage(selectedTilePage);
}
}
function buildTileSelectorPage(page) {
tileListElement.innerHTML = "";
if (map.type === "iso") {
// todo: fix me if we need "proper" isometric tilesets
// right now all our tilesets are 1 tile per image, so we will
// just iterate all iso-typed tilesets and grab those "one" tiles
// and create our tile-selector items
for(let ts of tilesets) {if (window.CP.shouldStopExecution(31)){break;}
if (ts.type === map.type) {
let tileData = ts.tiles[0];
tileData.src.setAttribute("data-tileset", ts.id);
tileData.src.setAttribute("data-type", ts.type);
tileData.src.setAttribute("data-tile-id", tileData.id);
tileData.src.classList.add("selectable-tile");
tileData.src.classList.add(map.type);
tileData.src.addEventListener("click", e=>tileClicked(e, ts, tileData, tileData.src), false);
tileListElement.appendChild(tileData.src);
}
}
window.CP.exitedLoop(31);
} else {
let tilesetIteration = 0;
for(let ts of tilesets) {if (window.CP.shouldStopExecution(33)){break;}
if (ts.type === map.type) {
if (tilesetIteration != page) {
tilesetIteration++;
continue;
}
for(let tileData of ts.tiles) {if (window.CP.shouldStopExecution(32)){break;}
let tile = document.createElement("div");
tile.setAttribute("data-tileset", ts.id);
tile.setAttribute("data-type", ts.type);
tile.setAttribute("data-tile-id", tileData.id);
tile.classList.add("selectable-tile");
tile.classList.add(map.type);
tile.addEventListener("click", e=>tileClicked(e, ts, tileData, tile), false);
tile.style.background =`url('${ts.src.src}') left -${tileData.x}px top -${tileData.y}px`;
tile.style.width = `${tileData.width}px`;
tile.style.height = `${tileData.height}px`;
tileListElement.appendChild(tile);
}
window.CP.exitedLoop(32);
return;
}
}
window.CP.exitedLoop(33);
}
}
function tileClicked(clickEvent, tileset, tiledata, elm) {
if (selectedTileElement) {
selectedTileElement.classList.remove("selected");
}
selectedTile = {tileset: tileset.id, id: tiledata.id};
elm.classList.add("selected");
selectedTileElement = elm;
}
function displayToasty() {
// play it once only, I'm pretty sure it wont be funny next time :P
if (toastyPlayed) return;
let toastyImg = document.querySelector(".toasty");
toastyPlayed = true;
toasty.play();
setTimeout(() => {
toastyImg.style.display ="block";
setTimeout(() => {
toastyImg.style.display = 'none';
}, 1500);
}, 150);
}
function showTilesetLoader() {
showWindow(loadTilesetWindow);
}
function hideTilesetLoader() {
closeWindow(loadTilesetWindow);
}
function updateTilesetLoader(current, total) {
loadTilesetWindowProgressBarValue.style.width = ((current/total) * 370) + "px";
}
function loadTileSets() {
isLoadingTilesets = true;
let queue = [];
tilePageCount = 5;
for (let i = 0; i < tilePageCount; ++i) {if (window.CP.shouldStopExecution(34)){break;}
queue.push({id:i,type:"top",w:32,h:32});
}
window.CP.exitedLoop(34);
for (let i = 0; i < 88; ++i) {if (window.CP.shouldStopExecution(35)){break;} // 88
queue.push({id:i,type:"iso",w:96,h:48});
}
window.CP.exitedLoop(35);
tilesetLoadCount = queue.length;
showTilesetLoader();
tilesetLoadCompleteCount = 0;
for (let i = 0; i < queue.length; ++i) {if (window.CP.shouldStopExecution(36)){break;}
loadTileSet(queue[i].id,queue[i].type,queue[i].w,queue[i].h, res => {
++tilesetLoadCompleteCount;
updateTilesetLoader(tilesetLoadCompleteCount, tilesetLoadCount);
if(tilesetLoadCompleteCount == queue.length) {
isLoadingTilesets = false;
hideTilesetLoader();
buildTileSelectorPage(0);
}
});
}
window.CP.exitedLoop(36);
if (queue.length == 0) {
isLoadingTilesets = false;
hideTilesetLoader();
}
}
function loadTileSet(i, type, tileWidth, tileHeight, completed) {
let baseUri = "https://s3-us-west-2.amazonaws.com/s.cdpn.io/163870/";
let tilesetSource = new Image();
tilesetSource.src = baseUri + `2d-${type}-${i}.png`;
if (tilesetSource.complete) {
let res = addTileSet(i, type, tilesetSource, tileWidth, tileHeight);
completed(res);
} else {
tilesetSource.addEventListener("load", e => {
let res = addTileSet(i, type, tilesetSource, tileWidth, tileHeight);
completed(res);
}, false);
}
}
function addTileSet(id, type, src, tileWidth, tileHeight) {
let isSingleTile = src.width/tileWidth<1.99&&src.height/tileHeight<1.99;
if(isSingleTile) {
// (id, x, y, width, height, src)
let tSrc = new TilesetSource(0, 0, 0, tileWidth, tileHeight, src);
let tSet = new Tileset(id, type, 1, 1, tileWidth, tileHeight);
tSet.tiles.push(tSrc);
tilesets.push(tSet);
return tSet;
} else {
let width = src.width/tileWidth;
let height = src.height/tileHeight;
// constructor(id, type, width, height, tileWidth, tileHeight, src)
let tSet = new Tileset(id, type, width, height, tileWidth, tileHeight, src);
for (let y = 0; y < height; ++y)
{if (window.CP.shouldStopExecution(38)){break;}for (let x = 0; x < width; ++x)
{if (window.CP.shouldStopExecution(37)){break;}tSet.tiles.push(new TilesetSource(y*tileWidth+x, x*tileWidth, y*tileHeight, tileWidth, tileHeight));}
window.CP.exitedLoop(37);
}
window.CP.exitedLoop(38);
tilesets.push(tSet);
return tSet;
}
}
//# sourceURL=pen.js
</script>
</body></html>