"map editor"
Bootstrap 3.0.0 Snippet by evarevirus

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

Related: See More


Questions / Comments: