├── free.png
├── icon.png
├── screenshots
├── demo.gif
├── desktop.png
├── mobile.png
├── animatedTiles.gif
├── demo-tilemaps.gif
├── flipTileOnX.gif
└── randomTileFrame.gif
├── importers
└── tiled.js
├── .travis.yml
├── itchIframe.html
├── updatePwa.js
├── manifest.webmanifest
├── .github
└── FUNDING.yml
├── sw.js
├── package.json
├── LICENSE
├── .gitignore
├── src
├── styles.css
└── tilemap-editor.js
├── README.md
└── index.html
/free.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blurymind/tilemap-editor/HEAD/free.png
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blurymind/tilemap-editor/HEAD/icon.png
--------------------------------------------------------------------------------
/screenshots/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blurymind/tilemap-editor/HEAD/screenshots/demo.gif
--------------------------------------------------------------------------------
/screenshots/desktop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blurymind/tilemap-editor/HEAD/screenshots/desktop.png
--------------------------------------------------------------------------------
/screenshots/mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blurymind/tilemap-editor/HEAD/screenshots/mobile.png
--------------------------------------------------------------------------------
/screenshots/animatedTiles.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blurymind/tilemap-editor/HEAD/screenshots/animatedTiles.gif
--------------------------------------------------------------------------------
/screenshots/demo-tilemaps.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blurymind/tilemap-editor/HEAD/screenshots/demo-tilemaps.gif
--------------------------------------------------------------------------------
/screenshots/flipTileOnX.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blurymind/tilemap-editor/HEAD/screenshots/flipTileOnX.gif
--------------------------------------------------------------------------------
/screenshots/randomTileFrame.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blurymind/tilemap-editor/HEAD/screenshots/randomTileFrame.gif
--------------------------------------------------------------------------------
/importers/tiled.js:
--------------------------------------------------------------------------------
1 | const importTiledJson = ()=>{
2 | console.log("TEST ======================")
3 | }
4 |
5 | export default importTiledJson;
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 14.17.4
4 | cache: yarn
5 |
6 | install:
7 | - yarn install
8 | script:
9 | - node updatePwa.js
10 |
11 | deploy:
12 | provider: pages
13 | skip_cleanup: true
14 | github-token: $GITHUB_TOKEN
15 | on:
16 | branch: main
--------------------------------------------------------------------------------
/itchIframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
77 |
78 |
144 |
145 |
146 |
147 |
148 |
149 | |
150 | r
151 |
152 | +
153 |
154 | -
155 |
156 |
157 |
158 |
159 | Tile size:
160 |
161 |
162 |
163 | Tileset loader:
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 | 👓️
174 |
175 |
176 | Symbols
177 |
178 | +
179 | -
180 |
181 |
182 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
-y-
249 |
-x-
250 |
251 |
252 |
253 |
254 |
255 |
+
256 |
-
257 |
📑
258 |
🎚️
259 |
278 |
279 |
280 |
281 |
284 | +
285 |
286 |
287 |
288 |
289 |
290 | `
291 | }
292 | const getEmptyLayer = (name="layer")=> ({tiles:{}, visible: true, name, animatedTiles: {}, opacity: 1});
293 | let tilesetImage, canvas, tilesetContainer, tilesetSelection, cropSize,
294 | confirmBtn, tilesetGridContainer,
295 | layersElement, resizingCanvas, mapTileHeight, mapTileWidth, tileDataSel,tileFrameSel,tileAnimSel,
296 | tilesetDataSel, mapsDataSel, objectParametersEditor;
297 |
298 | const el = {tileFrameCount:"", animStart:"", animEnd:"",renameTileFrameBtn:"",renameTileAnimBtn:"", animSpeed: "", animLoop:""};
299 | Object.keys(el).forEach(key=>{
300 | el[key] = () => document.getElementById(key);
301 | })
302 |
303 | let TILESET_ELEMENTS = [];
304 | let IMAGES = [{src:''}];
305 | let ZOOM = 1;
306 | let SIZE_OF_CROP = 32;
307 | let WIDTH = 0;
308 | let HEIGHT = 0;
309 | const TOOLS = {
310 | BRUSH: 0,
311 | ERASE: 1,
312 | PAN: 2,
313 | PICK: 3,
314 | RAND: 4,
315 | FILL: 5
316 | }
317 | let PREV_ACTIVE_TOOL = 0;
318 | let ACTIVE_TOOL = 0;
319 | let ACTIVE_MAP = "";
320 | let DISPLAY_SYMBOLS = false;
321 | let SHOW_GRID = false;
322 | const getEmptyMap = (name="map", mapWidth =20, mapHeight=20, tileSize = 32, gridColor="#00FFFF") =>
323 | ({layers: [getEmptyLayer("bottom"), getEmptyLayer("middle"), getEmptyLayer("top")], name,
324 | mapWidth, mapHeight, tileSize, width: mapWidth * SIZE_OF_CROP,height: mapHeight * SIZE_OF_CROP, gridColor });
325 |
326 | const getEmptyTilesetTag = (name, code, tiles ={}) =>({name,code,tiles});
327 |
328 | const getEmptyTileSet = ({
329 | src,
330 | name = "tileset",
331 | gridWidth,
332 | gridHeight,
333 | tileData = {},
334 | symbolStartIdx,
335 | tileSize = SIZE_OF_CROP,
336 | tags = {},
337 | frames = {},
338 | width,
339 | height,
340 | description = "n/a"
341 | }) => {
342 | return { src, name, gridWidth, gridHeight, tileCount: gridWidth * gridHeight, tileData, symbolStartIdx,tileSize, tags, frames, description, width, height}
343 | }
344 |
345 | const getSnappedPos = (pos) => (Math.round(pos / (SIZE_OF_CROP)) * (SIZE_OF_CROP));
346 | let selection = [];
347 | let currentLayer = 0;
348 | let isMouseDown = false;
349 | let maps = {};
350 | let tileSets = {};
351 |
352 | let apiTileSetLoaders = {};
353 | let selectedTileSetLoader = {};
354 | let apiTileMapExporters = {};
355 | let apiTileMapImporters = {};
356 | let apiOnUpdateCallback = () => {};
357 | let apiOnMouseUp = () => {};
358 |
359 | let editedEntity
360 |
361 | const getContext = () => canvas.getContext('2d');
362 |
363 | const setLayer = (newLayer) => {
364 | currentLayer = Number(newLayer);
365 |
366 | const oldActivedLayer = document.querySelector('.layer.active');
367 | if (oldActivedLayer) {
368 | oldActivedLayer.classList.remove('active');
369 | }
370 |
371 | document.querySelector(`.layer[tile-layer="${newLayer}"]`)?.classList.add('active');
372 | document.getElementById("activeLayerLabel").innerHTML = `
373 | Editing Layer: ${maps[ACTIVE_MAP].layers[newLayer]?.name}
374 |
375 |
Layer: ${maps[ACTIVE_MAP].layers[newLayer]?.name}
376 |
377 |
378 | Opacity
379 |
380 | ${maps[ACTIVE_MAP].layers[newLayer]?.opacity}
381 |
382 |
383 |
384 | `;
385 | document.getElementById("layerOpacitySlider").value = maps[ACTIVE_MAP].layers[newLayer]?.opacity;
386 | document.getElementById("layerOpacitySlider").addEventListener("change", e =>{
387 | addToUndoStack();
388 | document.getElementById("layerOpacitySliderValue").innerText = e.target.value;
389 | maps[ACTIVE_MAP].layers[currentLayer].opacity = Number(e.target.value);
390 | draw();
391 | updateLayers();
392 | })
393 | }
394 |
395 | const setLayerIsVisible = (layer, override = null) => {
396 | const layerNumber = Number(layer);
397 | maps[ACTIVE_MAP].layers[layerNumber].visible = override ?? !maps[ACTIVE_MAP].layers[layerNumber].visible;
398 | document
399 | .getElementById(`setLayerVisBtn-${layer}`)
400 | .innerHTML = maps[ACTIVE_MAP].layers[layerNumber].visible ? "👁️": "👓";
401 | draw();
402 | }
403 |
404 | const trashLayer = (layer) => {
405 | const layerNumber = Number(layer);
406 | maps[ACTIVE_MAP].layers.splice(layerNumber, 1);
407 | updateLayers();
408 | setLayer(maps[ACTIVE_MAP].layers.length - 1);
409 | draw();
410 | }
411 |
412 | const addLayer = () => {
413 | const newLayerName = prompt("Enter layer name", `Layer${maps[ACTIVE_MAP].layers.length + 1}`);
414 | if(newLayerName !== null) {
415 | maps[ACTIVE_MAP].layers.push(getEmptyLayer(newLayerName));
416 | updateLayers();
417 | }
418 | }
419 |
420 | const updateLayers = () => {
421 | layersElement.innerHTML = maps[ACTIVE_MAP].layers.map((layer, index)=>{
422 | return `
423 |
424 |
${layer.name} ${layer.opacity < 1 ? ` (${layer.opacity})` : ""}
425 |
426 |
1 ? "":`disabled="true"`}>🗑️
427 |
428 | `
429 | }).reverse().join("\n")
430 |
431 | maps[ACTIVE_MAP].layers.forEach((_,index)=>{
432 | document.getElementById(`selectLayerBtn-${index}`).addEventListener("click",e=>{
433 | setLayer(e.target.getAttribute("tile-layer"));
434 | addToUndoStack();
435 | })
436 | document.getElementById(`setLayerVisBtn-${index}`).addEventListener("click",e=>{
437 | setLayerIsVisible(e.target.getAttribute("vis-layer"))
438 | addToUndoStack();
439 | })
440 | document.getElementById(`trashLayerBtn-${index}`).addEventListener("click",e=>{
441 | trashLayer(e.target.getAttribute("trash-layer"))
442 | addToUndoStack();
443 | })
444 | setLayerIsVisible(index, true);
445 | })
446 | setLayer(currentLayer);
447 | }
448 |
449 | const getTileData = (x= null,y= null) =>{
450 | const tilesetTiles = tileSets[tilesetDataSel.value].tileData;
451 | let data;
452 | if(x === null && y === null){
453 | const {x: sx, y: sy} = selection[0];
454 | return tilesetTiles[`${sx}-${sy}`];
455 | } else {
456 | data = tilesetTiles[`${x}-${y}`]
457 | }
458 | return data;
459 | }
460 | const setTileData = (x = null,y = null,newData, key= "") =>{
461 | const tilesetTiles = tileSets[tilesetDataSel.value].tileData;
462 | if(x === null && y === null){
463 | const {x:sx, y:sy} = selection[0];
464 | tilesetTiles[`${sx}-${sy}`] = newData;
465 | }
466 | if(key !== ""){
467 | tilesetTiles[`${x}-${y}`][key] = newData;
468 | }else{
469 | tilesetTiles[`${x}-${y}`] = newData;
470 | }
471 | }
472 |
473 | const setActiveTool = (toolIdx) => {
474 | ACTIVE_TOOL = toolIdx;
475 | const actTool = document.getElementById("toolButtonsWrapper").querySelector(`input[id="tool${toolIdx}"]`);
476 | if (actTool) actTool.checked = true;
477 | document.getElementById("canvas_wrapper").setAttribute("isDraggable", ACTIVE_TOOL === TOOLS.PAN);
478 | draw();
479 | }
480 |
481 | let selectionSize = [1,1];
482 | const updateSelection = (autoSelectTool = true) => {
483 | if(!tileSets[tilesetDataSel.value]) return;
484 | const selected = selection[0];
485 | if(!selected) return;
486 | const {x, y} = selected;
487 | const {x: endX, y: endY} = selection[selection.length - 1];
488 | const selWidth = endX - x + 1;
489 | const selHeight = endY - y + 1;
490 | selectionSize = [selWidth, selHeight]
491 | console.log(tileSets[tilesetDataSel.value].tileSize)
492 | const tileSize = tileSets[tilesetDataSel.value].tileSize;
493 | tilesetSelection.style.left = `${x * tileSize * ZOOM}px`;
494 | tilesetSelection.style.top = `${y * tileSize * ZOOM}px`;
495 | tilesetSelection.style.width = `${selWidth * tileSize * ZOOM}px`;
496 | tilesetSelection.style.height = `${selHeight * tileSize * ZOOM}px`;
497 |
498 | // Autoselect tool upon selecting a tile
499 | if(autoSelectTool && ![TOOLS.BRUSH, TOOLS.RAND, TOOLS.FILL].includes(ACTIVE_TOOL)) setActiveTool(TOOLS.BRUSH);
500 |
501 | // show/hide param editor
502 | if(tileDataSel.value === "frames" && editedEntity) objectParametersEditor.classList.add('entity');
503 | else objectParametersEditor.classList.remove('entity');
504 | onUpdateState();
505 | }
506 |
507 | const randomLetters = new Array(10680).fill(1).map((_, i) => String.fromCharCode(165 + i));
508 |
509 | const shouldHideSymbols = () => SIZE_OF_CROP < 10 && ZOOM < 2;
510 | const updateTilesetGridContainer = () =>{
511 | const viewMode = tileDataSel.value;
512 | const tilesetData = tileSets[tilesetDataSel.value];
513 | if(!tilesetData) return;
514 |
515 | const {tileCount, gridWidth, tileData, tags} = tilesetData;
516 | // console.log("COUNT", tileCount)
517 | const hideSymbols = !DISPLAY_SYMBOLS || shouldHideSymbols();
518 | const canvas = document.getElementById("tilesetCanvas");
519 | const img = TILESET_ELEMENTS[tilesetDataSel.value];
520 | canvas.width = img.width * ZOOM;
521 | canvas.height = img.height * ZOOM;
522 | const ctx = canvas.getContext('2d');
523 | if (ZOOM !== 1){
524 | ctx.webkitImageSmoothingEnabled = false;
525 | ctx.mozImageSmoothingEnabled = false;
526 | ctx.msImageSmoothingEnabled = false;
527 | ctx.imageSmoothingEnabled = false;
528 | }
529 | ctx.drawImage(img,0,0,canvas.width ,canvas.height);
530 | // console.log("WIDTH EXCEEDS?", canvas.width % SIZE_OF_CROP)
531 | const tileSizeSeemsIncorrect = canvas.width % SIZE_OF_CROP !== 0;
532 | drawGrid(ctx.canvas.width, ctx.canvas.height, ctx,SIZE_OF_CROP * ZOOM, tileSizeSeemsIncorrect ? "red":"cyan");
533 | Array.from({length: tileCount}, (x, i) => i).map(tile=>{
534 | if (viewMode === "frames") {
535 | const frameData = getCurrentFrames();
536 | if(!frameData || Object.keys(frameData).length === 0) return;
537 |
538 | const {width, height, start, tiles,frameCount} = frameData;
539 | selection = [...tiles];
540 | ctx.lineWidth = 0.5;
541 | ctx.strokeStyle = "red";
542 | ctx.strokeRect(SIZE_OF_CROP * ZOOM * (start.x + width), SIZE_OF_CROP * ZOOM * start.y, SIZE_OF_CROP * ZOOM * (width * (frameCount - 1)), SIZE_OF_CROP * ZOOM * height);
543 | } else if (!hideSymbols) {
544 | const x = tile % gridWidth;
545 | const y = Math.floor(tile / gridWidth);
546 | const tileKey = `${x}-${y}`;
547 | const innerTile = viewMode === "" ?
548 | tileData[tileKey]?.tileSymbol :
549 | viewMode === "frames" ? tile :tags[viewMode]?.tiles[tileKey]?.mark || "-";
550 |
551 | ctx.fillStyle = 'white';
552 | ctx.font = '11px arial';
553 | ctx.shadowColor="black";
554 | ctx.shadowBlur=4;
555 | ctx.lineWidth=2;
556 | const posX = (x * SIZE_OF_CROP * ZOOM) + ((SIZE_OF_CROP * ZOOM) / 3);
557 | const posY = (y * SIZE_OF_CROP * ZOOM) + ((SIZE_OF_CROP * ZOOM) / 2);
558 | ctx.fillText(innerTile,posX,posY);
559 | }
560 | })
561 | }
562 |
563 | let tileSelectStart = null;
564 | const getSelectedTile = (event) => {
565 | const { x, y } = event.target.getBoundingClientRect();
566 | const tileSize = tileSets[tilesetDataSel.value].tileSize * ZOOM;
567 | const tx = Math.floor(Math.max(event.clientX - x, 0) / tileSize);
568 | const ty = Math.floor(Math.max(event.clientY - y, 0) / tileSize);
569 | // add start tile, add end tile, add all tiles inbetween
570 | const newSelection = [];
571 | if (tileSelectStart !== null){
572 | for (let ix = tileSelectStart.x; ix < tx + 1; ix++) {
573 | for (let iy = tileSelectStart.y; iy < ty + 1; iy++) {
574 | const data = getTileData(ix,iy);
575 | newSelection.push({...data, x:ix,y:iy})
576 | }
577 | }
578 | }
579 | if (newSelection.length > 0) return newSelection;
580 |
581 | const data = getTileData(tx, ty);
582 | return [{...data, x:tx,y:ty}];
583 | }
584 |
585 | const draw = (shouldDrawGrid = true) =>{
586 | const ctx = getContext();
587 | ctx.clearRect(0, 0, WIDTH, HEIGHT);
588 | ctx.canvas.width = WIDTH;
589 | ctx.canvas.height = HEIGHT;
590 | if(shouldDrawGrid && !SHOW_GRID)drawGrid(WIDTH, HEIGHT, ctx,SIZE_OF_CROP * ZOOM, maps[ACTIVE_MAP].gridColor);
591 | const shouldHideHud = shouldHideSymbols();
592 |
593 | maps[ACTIVE_MAP].layers.forEach((layer) => {
594 | if(!layer.visible) return;
595 | ctx.globalAlpha = layer.opacity;
596 | if (ZOOM !== 1){
597 | ctx.webkitImageSmoothingEnabled = false;
598 | ctx.mozImageSmoothingEnabled = false;
599 | ctx.msImageSmoothingEnabled = false;
600 | ctx.imageSmoothingEnabled = false;
601 | }
602 | //static tiles on this layer
603 | Object.keys(layer.tiles).forEach((key) => {
604 | const [positionX, positionY] = key.split('-').map(Number);
605 | const {x, y, tilesetIdx, isFlippedX} = layer.tiles[key];
606 | const tileSize = tileSets[tilesetIdx]?.tileSize || SIZE_OF_CROP;
607 |
608 | if(!(tilesetIdx in TILESET_ELEMENTS)) { //texture not found
609 | ctx.fillStyle = 'red';
610 | ctx.fillRect(positionX * SIZE_OF_CROP * ZOOM, positionY * SIZE_OF_CROP * ZOOM, SIZE_OF_CROP * ZOOM, SIZE_OF_CROP * ZOOM);
611 | return;
612 | }
613 | if(isFlippedX){
614 | ctx.save();//Special canvas crap to flip a slice, cause drawImage cant do it
615 | ctx.translate(ctx.canvas.width, 0);
616 | ctx.scale(-1, 1);
617 | ctx.drawImage(
618 | TILESET_ELEMENTS[tilesetIdx],
619 | x * tileSize,
620 | y * tileSize,
621 | tileSize,
622 | tileSize,
623 | ctx.canvas.width - (positionX * SIZE_OF_CROP * ZOOM) - SIZE_OF_CROP * ZOOM,
624 | positionY * SIZE_OF_CROP * ZOOM,
625 | SIZE_OF_CROP * ZOOM,
626 | SIZE_OF_CROP * ZOOM
627 | );
628 | ctx.restore();
629 | } else {
630 | ctx.drawImage(
631 | TILESET_ELEMENTS[tilesetIdx],
632 | x * tileSize,
633 | y * tileSize,
634 | tileSize,
635 | tileSize,
636 | positionX * SIZE_OF_CROP * ZOOM,
637 | positionY * SIZE_OF_CROP * ZOOM,
638 | SIZE_OF_CROP * ZOOM,
639 | SIZE_OF_CROP * ZOOM
640 | );
641 | }
642 | });
643 | // animated tiles
644 | Object.keys(layer.animatedTiles || {}).forEach((key) => {
645 | const [positionX, positionY] = key.split('-').map(Number);
646 | const {start, width, height, frameCount, isFlippedX} = layer.animatedTiles[key];
647 | const {x, y, tilesetIdx} = start;
648 | const tileSize = tileSets[tilesetIdx]?.tileSize || SIZE_OF_CROP;
649 |
650 | if(!(tilesetIdx in TILESET_ELEMENTS)) { //texture not found
651 | ctx.fillStyle = 'yellow';
652 | ctx.fillRect(positionX * SIZE_OF_CROP * ZOOM, positionY * SIZE_OF_CROP * ZOOM, SIZE_OF_CROP * ZOOM * width, SIZE_OF_CROP * ZOOM * height);
653 | ctx.fillStyle = 'blue';
654 | ctx.fillText("X",positionX * SIZE_OF_CROP * ZOOM + 5,positionY * SIZE_OF_CROP * ZOOM + 10);
655 | return;
656 | }
657 | const frameIndex = tileDataSel.value === "frames" || frameCount === 1 ? Math.round(Date.now()/120) % frameCount : 1; //30fps
658 |
659 | if(isFlippedX) {
660 | ctx.save();//Special canvas crap to flip a slice, cause drawImage cant do it
661 | ctx.translate(ctx.canvas.width, 0);
662 | ctx.scale(-1, 1);
663 |
664 | const positionXFlipped = ctx.canvas.width - (positionX * SIZE_OF_CROP * ZOOM) - SIZE_OF_CROP * ZOOM;
665 | if(shouldDrawGrid && !shouldHideHud) {
666 | ctx.beginPath();
667 | ctx.lineWidth = 1;
668 | ctx.strokeStyle = 'rgba(250,240,255, 0.7)';
669 | ctx.rect(positionXFlipped, positionY * SIZE_OF_CROP * ZOOM, SIZE_OF_CROP * ZOOM * width, SIZE_OF_CROP * ZOOM * height);
670 | ctx.stroke();
671 | }
672 | ctx.drawImage(
673 | TILESET_ELEMENTS[tilesetIdx],
674 | x * tileSize + (frameIndex * tileSize * width),
675 | y * tileSize,
676 | tileSize * width,// src width
677 | tileSize * height, // src height
678 | positionXFlipped,
679 | positionY * SIZE_OF_CROP * ZOOM, //target y
680 | SIZE_OF_CROP * ZOOM * width, // target width
681 | SIZE_OF_CROP * ZOOM * height // target height
682 | );
683 | if(shouldDrawGrid && !shouldHideHud) {
684 | ctx.fillStyle = 'white';
685 | ctx.fillText("🔛",positionXFlipped + 5,positionY * SIZE_OF_CROP * ZOOM + 10);
686 | }
687 | ctx.restore();
688 | }else {
689 | if(shouldDrawGrid && !shouldHideHud) {
690 | ctx.beginPath();
691 | ctx.lineWidth = 1;
692 | ctx.strokeStyle = 'rgba(250,240,255, 0.7)';
693 | ctx.rect(positionX * SIZE_OF_CROP * ZOOM, positionY * SIZE_OF_CROP * ZOOM, SIZE_OF_CROP * ZOOM * width, SIZE_OF_CROP * ZOOM * height);
694 | ctx.stroke();
695 | }
696 | ctx.drawImage(
697 | TILESET_ELEMENTS[tilesetIdx],
698 | x * tileSize + (frameIndex * tileSize * width),//src x
699 | y * tileSize,//src y
700 | tileSize * width,// src width
701 | tileSize * height, // src height
702 | positionX * SIZE_OF_CROP * ZOOM, //target x
703 | positionY * SIZE_OF_CROP * ZOOM, //target y
704 | SIZE_OF_CROP * ZOOM * width, // target width
705 | SIZE_OF_CROP * ZOOM * height // target height
706 | );
707 | if(shouldDrawGrid && !shouldHideHud) {
708 | ctx.fillStyle = 'white';
709 | ctx.fillText("⭕",positionX * SIZE_OF_CROP * ZOOM + 5,positionY * SIZE_OF_CROP * ZOOM + 10);
710 | }
711 | }
712 | })
713 | });
714 | if(SHOW_GRID)drawGrid(WIDTH, HEIGHT, ctx,SIZE_OF_CROP * ZOOM, maps[ACTIVE_MAP].gridColor);
715 | onUpdateState();
716 | }
717 |
718 | const setMouseIsTrue=(e)=> {
719 | if(e.button === 0) {
720 | isMouseDown = true;
721 | }
722 | else if(e.button === 1){
723 | PREV_ACTIVE_TOOL = ACTIVE_TOOL;
724 | setActiveTool(TOOLS.PAN)
725 | }
726 | }
727 |
728 | const setMouseIsFalse=(e)=> {
729 | if(e.button === 0) {
730 | isMouseDown = false;
731 | }
732 | else if(e.button === 1 && ACTIVE_TOOL === TOOLS.PAN){
733 | setActiveTool(PREV_ACTIVE_TOOL)
734 | }
735 | }
736 |
737 | const removeTile=(key) =>{
738 | delete maps[ACTIVE_MAP].layers[currentLayer].tiles[key];
739 | if (key in (maps[ACTIVE_MAP].layers[currentLayer].animatedTiles || {})) delete maps[ACTIVE_MAP].layers[currentLayer].animatedTiles[key];
740 | }
741 |
742 | const isFlippedOnX = () => document.getElementById("toggleFlipX").checked;
743 | const addSelectedTiles = (key, tiles) => {
744 | const [x, y] = key.split("-");
745 | const tilesPatch = tiles || selection; // tiles is opt override for selection for fancy things like random patch of tiles
746 | const {x: startX, y: startY} = tilesPatch[0];// add selection override
747 | const selWidth = selectionSize[0];
748 | const selHeight = selectionSize[1];
749 | maps[ACTIVE_MAP].layers[currentLayer].tiles[key] = tilesPatch[0];
750 | const isFlippedX = isFlippedOnX();
751 | for (let ix = 0; ix < selWidth; ix++) {
752 | for (let iy = 0; iy < selHeight; iy++) {
753 | const tileX = isFlippedX ? Number(x)-ix : Number(x)+ix;//placed in reverse when flipped on x
754 | const coordKey = `${tileX}-${Number(y)+iy}`;
755 | maps[ACTIVE_MAP].layers[currentLayer].tiles[coordKey] = {
756 | ...tilesPatch
757 | .find(tile => tile.x === startX + ix && tile.y === startY + iy),
758 | isFlippedX
759 | };
760 | }
761 | }
762 | }
763 | const getCurrentFrames = () => tileSets[tilesetDataSel.value]?.frames[tileFrameSel.value];
764 | const getSelectedFrameCount = () => getCurrentFrames()?.frameCount || 1;
765 | const shouldNotAddAnimatedTile = () => (tileDataSel.value !== "frames" && getSelectedFrameCount() !== 1) || Object.keys(tileSets[tilesetDataSel.value]?.frames).length === 0;
766 | const addTile = (key) => {
767 | if (shouldNotAddAnimatedTile()) {
768 | addSelectedTiles(key);
769 | } else {
770 | // if animated tile mode and has more than one frames, add/remove to animatedTiles
771 | if(!maps[ACTIVE_MAP].layers[currentLayer].animatedTiles) maps[ACTIVE_MAP].layers[currentLayer].animatedTiles = {};
772 | const isFlippedX = isFlippedOnX();
773 | const [x,y] = key.split("-");
774 | maps[ACTIVE_MAP].layers[currentLayer].animatedTiles[key] = {
775 | ...getCurrentFrames(),
776 | isFlippedX, layer: currentLayer,
777 | xPos: Number(x) * SIZE_OF_CROP, yPos: Number(y) * SIZE_OF_CROP
778 | };
779 | }
780 | }
781 |
782 | const addRandomTile = (key) =>{
783 | // TODO add probability for empty
784 | if (shouldNotAddAnimatedTile()) {
785 | maps[ACTIVE_MAP].layers[currentLayer].tiles[key] = selection[Math.floor(Math.random()*selection.length)];
786 | }else {
787 | // do the same, but add random from frames instead
788 | const tilesetTiles = tileSets[tilesetDataSel.value].tileData;
789 | const {frameCount, tiles, width} = getCurrentFrames();
790 | const randOffset = Math.floor(Math.random()*frameCount);
791 | const randXOffsetTiles = tiles.map(tile=>tilesetTiles[`${tile.x + randOffset * width}-${tile.y}`]);
792 | addSelectedTiles(key,randXOffsetTiles);
793 | }
794 |
795 | }
796 |
797 | const fillEmptyOrSameTiles = (key) => {
798 | const pickedTile = maps[ACTIVE_MAP].layers[currentLayer].tiles[key];
799 | Array.from({length: mapTileWidth * mapTileHeight}, (x, i) => i).map(tile=>{
800 | const x = tile % mapTileWidth;
801 | const y = Math.floor(tile / mapTileWidth);
802 | const coordKey = `${x}-${y}`;
803 | const filledTile = maps[ACTIVE_MAP].layers[currentLayer].tiles[coordKey];
804 |
805 | if(pickedTile && filledTile && filledTile.x === pickedTile.x && filledTile.y === pickedTile.y){
806 | maps[ACTIVE_MAP].layers[currentLayer].tiles[coordKey] = selection[0];// Replace all clicked on tiles with selected
807 | }
808 | else if(!pickedTile && !(coordKey in maps[ACTIVE_MAP].layers[currentLayer].tiles)) {
809 | maps[ACTIVE_MAP].layers[currentLayer].tiles[coordKey] = selection[0]; // when clicked on empty, replace all empty with selection
810 | }
811 | })
812 | }
813 |
814 | const selectMode = (mode = null) => {
815 | if (mode !== null) tileDataSel.value = mode;
816 | document.getElementById("tileFrameSelContainer").style.display = tileDataSel.value === "frames" ?
817 | "flex":"none"
818 | // tilesetContainer.style.top = tileDataSel.value === "frames" ? "45px" : "0";
819 | updateTilesetGridContainer();
820 | }
821 | const getTile =(key, allLayers = false)=> {
822 | const layers = maps[ACTIVE_MAP].layers;
823 | editedEntity = undefined;
824 | const clicked = allLayers ?
825 | [...layers].reverse().find((layer,index)=> {
826 | if(layer.animatedTiles && key in layer.animatedTiles) {
827 | setLayer(layers.length - index - 1);
828 | editedEntity = layer.animatedTiles[key];
829 | }
830 | if(key in layer.tiles){
831 | setLayer(layers.length - index - 1);
832 | return layer.tiles[key]
833 | }
834 | })?.tiles[key] //TODO this doesnt work on animatedTiles
835 | :
836 | layers[currentLayer].tiles[key];
837 |
838 | if (clicked && !editedEntity) {
839 | selection = [clicked];
840 |
841 | // console.log("clicked", clicked, "entity data",editedEntity)
842 | document.getElementById("toggleFlipX").checked = !!clicked?.isFlippedX;
843 | // TODO switch to different tileset if its from a different one
844 | // if(clicked.tilesetIdx !== tilesetDataSel.value) {
845 | // tilesetDataSel.value = clicked.tilesetIdx;
846 | // reloadTilesets();
847 | // updateTilesetGridContainer();
848 | // }
849 | selectMode("");
850 | updateSelection();
851 | return true;
852 | } else if (editedEntity){
853 | // console.log("Animated tile found", editedEntity)
854 | selection = editedEntity.tiles;
855 | document.getElementById("toggleFlipX").checked = editedEntity.isFlippedX;
856 | setLayer(editedEntity.layer);
857 | tileFrameSel.value = editedEntity.name;
858 | updateSelection();
859 | selectMode("frames");
860 | return true;
861 | }else {
862 | return false;
863 | }
864 | }
865 |
866 | const toggleTile=(event)=> {
867 | if(ACTIVE_TOOL === TOOLS.PAN || !maps[ACTIVE_MAP].layers[currentLayer].visible) return;
868 |
869 | const {x,y} = getSelectedTile(event)[0];
870 | const key = `${x}-${y}`;
871 |
872 | // console.log(event.button)
873 | if (event.shiftKey) {
874 | removeTile(key);
875 | } else if (event.ctrlKey || event.button === 2 || ACTIVE_TOOL === TOOLS.PICK) {
876 | const pickedTile = getTile(key, true);
877 | if(ACTIVE_TOOL === TOOLS.BRUSH && !pickedTile) setActiveTool(TOOLS.ERASE); //picking empty tile, sets tool to eraser
878 | else if(ACTIVE_TOOL === TOOLS.FILL || ACTIVE_TOOL === TOOLS.RAND) setActiveTool(TOOLS.BRUSH); //
879 | } else {
880 | if(ACTIVE_TOOL === TOOLS.BRUSH){
881 | addTile(key);// also works with animated
882 | } else if(ACTIVE_TOOL === TOOLS.ERASE) {
883 | removeTile(key);// also works with animated
884 | } else if (ACTIVE_TOOL === TOOLS.RAND){
885 | addRandomTile(key);
886 | } else if (ACTIVE_TOOL === TOOLS.FILL){
887 | fillEmptyOrSameTiles(key);
888 | }
889 | }
890 | draw();
891 | addToUndoStack();
892 | }
893 |
894 | const clearCanvas = () => {
895 | addToUndoStack();
896 | maps[ACTIVE_MAP].layers = [getEmptyLayer("bottom"), getEmptyLayer("middle"), getEmptyLayer("top")];
897 | setLayer(0);
898 | updateLayers();
899 | draw();
900 | addToUndoStack();
901 | }
902 |
903 | const downloadAsTextFile = (input, fileName = "tilemap-editor.json") =>{
904 | const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(typeof input === "string" ? input : JSON.stringify(input));
905 | const dlAnchorElem = document.getElementById('downloadAnchorElem');
906 | dlAnchorElem.setAttribute("href", dataStr );
907 | dlAnchorElem.setAttribute("download", fileName);
908 | dlAnchorElem.click();
909 | }
910 | const exportJson = () => {
911 | downloadAsTextFile({tileSets, maps});
912 | }
913 |
914 | const exportImage = () => {
915 | draw(false);
916 | const data = canvas.toDataURL();
917 | const image = new Image();
918 | image.src = data;
919 | image.crossOrigin = "anonymous";
920 | const w = window.open('');
921 | w.document.write(image.outerHTML);
922 | draw();
923 | }
924 |
925 | const getTilesAnalisis = (ctx, width, height, sizeOfTile) =>{
926 | const analizedTiles = {};
927 | let uuid = 0;
928 | for (let y = 0; y < height; y += sizeOfTile) {
929 | for (let x = 0; x < width; x += sizeOfTile) {
930 | // console.log(x, y);
931 | const tileData = ctx.getImageData(x, y, sizeOfTile, sizeOfTile);
932 | const index = tileData.data.toString();
933 | if (analizedTiles[index]) {
934 | analizedTiles[index].coords.push({ x: x, y: y });
935 | analizedTiles[index].times++;
936 | } else {
937 | analizedTiles[index] = {
938 | uuid: uuid++,
939 | coords: [{ x: x, y: y }],
940 | times: 1,
941 | tileData: tileData
942 | };
943 | }
944 | }
945 | }
946 | const uniqueTiles = Object.values(analizedTiles).length - 1;
947 | // console.log("TILES:", {analizedTiles, uniqueTiles})
948 | return {analizedTiles, uniqueTiles};
949 | }
950 | const drawAnaliticsReport = () => {
951 | const prevZoom = ZOOM;
952 | ZOOM = 1;// needed for correct eval
953 | updateZoom();
954 | draw(false);
955 | const {analizedTiles, uniqueTiles} = getTilesAnalisis(getContext(), WIDTH, HEIGHT, SIZE_OF_CROP);
956 | const data = canvas.toDataURL();
957 | const image = new Image();
958 | image.src = data;
959 | const ctx = getContext();
960 | ZOOM = prevZoom;
961 | updateZoom();
962 | draw(false);
963 | Object.values(analizedTiles).map((t) => {
964 | // Fill the heatmap
965 | t.coords.forEach((c, i) => {
966 | const fillStyle = `rgba(255, 0, 0, ${(1/t.times) - 0.35})`;
967 | ctx.fillStyle = fillStyle;
968 | ctx.fillRect(c.x * ZOOM, c.y * ZOOM, SIZE_OF_CROP * ZOOM, SIZE_OF_CROP * ZOOM);
969 | });
970 | })
971 | drawGrid(WIDTH, HEIGHT, ctx,SIZE_OF_CROP * ZOOM,'rgba(255,213,0,0.5)')
972 | ctx.fillStyle = 'white';
973 | ctx.font = 'bold 17px arial';
974 | ctx.shadowColor="black";
975 | ctx.shadowBlur=5;
976 | ctx.lineWidth=3;
977 | ctx.fillText(`Unique tiles: ${uniqueTiles}`,4,HEIGHT - 30);
978 | ctx.fillText(`Map size: ${mapTileWidth}x${mapTileHeight}`,4,HEIGHT - 10);
979 | }
980 | const exportUniqueTiles = () => {
981 | const ctx = getContext();
982 | const prevZoom = ZOOM;
983 | ZOOM = 1;// needed for correct eval
984 | updateZoom();
985 | draw(false);
986 | const {analizedTiles} = getTilesAnalisis(getContext(), WIDTH, HEIGHT, SIZE_OF_CROP);
987 | ctx.clearRect(0, 0, WIDTH, HEIGHT);
988 | const gridWidth = tilesetImage.width / SIZE_OF_CROP;
989 | Object.values(analizedTiles).map((t, i) => {
990 | const positionX = i % gridWidth;
991 | const positionY = Math.floor(i / gridWidth);
992 | const tileCanvas = document.createElement("canvas");
993 | tileCanvas.width = SIZE_OF_CROP;
994 | tileCanvas.height = SIZE_OF_CROP;
995 | const tileCtx = tileCanvas.getContext("2d");
996 | tileCtx.putImageData(t.tileData, 0, 0);
997 | ctx.drawImage(
998 | tileCanvas,
999 | 0,
1000 | 0,
1001 | SIZE_OF_CROP,
1002 | SIZE_OF_CROP,
1003 | positionX * SIZE_OF_CROP,
1004 | positionY * SIZE_OF_CROP,
1005 | SIZE_OF_CROP,
1006 | SIZE_OF_CROP
1007 | );
1008 | });
1009 | const data = canvas.toDataURL();
1010 | const image = new Image();
1011 | image.src = data;
1012 | image.crossOrigin = "anonymous";
1013 | const w = window.open('');
1014 | w.document.write(image.outerHTML);
1015 | ZOOM = prevZoom;
1016 | updateZoom();
1017 | draw();
1018 | }
1019 |
1020 | exports.getLayers = ()=> {
1021 | return maps[ACTIVE_MAP].layers;
1022 | }
1023 |
1024 | const renameCurrentTileSymbol = ()=>{
1025 | const {x, y, tileSymbol} = selection[0];
1026 | const newSymbol = window.prompt("Enter tile symbol", tileSymbol || "*");
1027 | if(newSymbol !== null) {
1028 | setTileData(x,y,newSymbol, "tileSymbol");
1029 | updateSelection();
1030 | updateTilesetGridContainer();
1031 | addToUndoStack();
1032 | }
1033 | }
1034 |
1035 | const getFlattenedData = () => {
1036 | const result = Object.entries(maps).map(([key, map])=>{
1037 | console.log({map})
1038 | const layers = map.layers;
1039 | const flattenedData = Array(layers.length).fill([]).map(()=>{
1040 | return Array(map.mapHeight).fill([]).map(row=>{
1041 | return Array(map.mapWidth).fill([]).map(column => ({
1042 | tile: null,
1043 | tileSymbol: " "// a space is an empty tile
1044 | }))
1045 | })
1046 | });
1047 | layers.forEach((layerObj,lrIndex) => {
1048 | Object.entries(layerObj.tiles).forEach(([key,tile])=>{
1049 | const [x,y] = key.split("-");
1050 | if(Number(y) < map.mapHeight && Number(x) < map.mapWidth) {
1051 | flattenedData[lrIndex][Number(y)][Number(x)] = {tile, tileSymbol: tile.tileSymbol || "*"};
1052 | }
1053 | })
1054 | });
1055 | return {map:key, tileSet: map.tileSet,flattenedData};
1056 | });
1057 | return result;
1058 | };
1059 | const getExportData = () => {
1060 | const exportData = {maps, tileSets, flattenedData: getFlattenedData(), activeMap: ACTIVE_MAP, downloadAsTextFile};
1061 | console.log("Exported ", exportData);
1062 | return exportData;
1063 | }
1064 |
1065 | const updateMapSize = (size) =>{
1066 | if(size?.mapWidth && size?.mapWidth > 1){
1067 | mapTileWidth = size?.mapWidth;
1068 | WIDTH = mapTileWidth * SIZE_OF_CROP * ZOOM;
1069 | maps[ACTIVE_MAP].mapWidth = mapTileWidth;
1070 | document.querySelector(".canvas_resizer[resizerdir='x']").style=`left:${WIDTH}px`;
1071 | document.querySelector(".canvas_resizer[resizerdir='x'] input").value = String(mapTileWidth);
1072 | document.getElementById("canvasWidthInp").value = String(mapTileWidth);
1073 | }
1074 | if(size?.mapHeight && size?.mapHeight > 1){
1075 | mapTileHeight = size?.mapHeight;
1076 | HEIGHT = mapTileHeight * SIZE_OF_CROP * ZOOM;
1077 | maps[ACTIVE_MAP].mapHeight = mapTileHeight;
1078 | document.querySelector(".canvas_resizer[resizerdir='y']").style=`top:${HEIGHT}px`;
1079 | document.querySelector(".canvas_resizer[resizerdir='y'] input").value = String(mapTileHeight);
1080 | document.getElementById("canvasHeightInp").value = String(mapTileHeight);
1081 | }
1082 | draw();
1083 | }
1084 |
1085 | const setActiveMap =(id) =>{
1086 | ACTIVE_MAP = id;
1087 | document.getElementById("gridColorSel").value = maps[ACTIVE_MAP].gridColor || "#00FFFF";
1088 | draw();
1089 | updateMapSize({mapWidth: maps[ACTIVE_MAP].mapWidth, mapHeight: maps[ACTIVE_MAP].mapHeight})
1090 | updateLayers();
1091 | }
1092 |
1093 |
1094 | let undoStepPosition = -1;
1095 | let undoStack = [];
1096 | const clearUndoStack = () => {
1097 | undoStack = [];
1098 | undoStepPosition = -1;
1099 | }
1100 | const getAppState = () => {
1101 | // TODO we need for tilesets to load - rapidly refreshing the browser may return empty tilesets object!
1102 | if(Object.keys(tileSets).length === 0 && tileSets.constructor === Object) return null;
1103 | return {
1104 | tileMapData: {tileSets, maps},
1105 | appState: {
1106 | undoStack,
1107 | undoStepPosition,
1108 | currentLayer,
1109 | PREV_ACTIVE_TOOL,
1110 | ACTIVE_TOOL,
1111 | ACTIVE_MAP,
1112 | SHOW_GRID,
1113 | selection
1114 | }
1115 | //Todo tileSize and the others
1116 | // undo stack is lost
1117 | };
1118 | }
1119 | const onUpdateState = () => {
1120 | apiOnUpdateCallback(getAppState())
1121 | }
1122 | const addToUndoStack = () => {
1123 | if(Object.keys(tileSets).length === 0 || Object.keys(maps).length === 0) return;
1124 | const oldState = undoStack.length > 0 ? JSON.stringify(
1125 | {
1126 | maps: undoStack[undoStepPosition].maps,
1127 | tileSets: undoStack[undoStepPosition].tileSets,
1128 | currentLayer:undoStack[undoStepPosition].currentLayer,
1129 | ACTIVE_MAP:undoStack[undoStepPosition].ACTIVE_MAP,
1130 | IMAGES:undoStack[undoStepPosition].IMAGES
1131 | }) : undefined;
1132 | const newState = JSON.stringify({maps,tileSets,currentLayer,ACTIVE_MAP,IMAGES});
1133 | if (newState === oldState) return; // prevent updating when no changes are present in the data!
1134 |
1135 | undoStepPosition += 1;
1136 | undoStack.length = undoStepPosition;
1137 | undoStack.push(JSON.parse(JSON.stringify({maps,tileSets, currentLayer, ACTIVE_MAP, IMAGES, undoStepPosition})));
1138 | // console.log("undo stack updated", undoStack, undoStepPosition)
1139 | }
1140 | const restoreFromUndoStackData = () => {
1141 | maps = decoupleReferenceFromObj(undoStack[undoStepPosition].maps);
1142 | const undoTileSets = decoupleReferenceFromObj(undoStack[undoStepPosition].tileSets);
1143 | const undoIMAGES = decoupleReferenceFromObj(undoStack[undoStepPosition].IMAGES);
1144 | if(JSON.stringify(IMAGES) !== JSON.stringify(undoIMAGES)){ // images needs to happen before tilesets
1145 | IMAGES = undoIMAGES;
1146 | reloadTilesets();
1147 | }
1148 | if(JSON.stringify(undoTileSets) !== JSON.stringify(tileSets)) { // done to prevent the below, which is expensive
1149 | tileSets = undoTileSets;
1150 | updateTilesetGridContainer();
1151 | }
1152 | tileSets = undoTileSets;
1153 | updateTilesetDataList();
1154 |
1155 | const undoLayer = decoupleReferenceFromObj(undoStack[undoStepPosition].currentLayer);
1156 | const undoActiveMap = decoupleReferenceFromObj(undoStack[undoStepPosition].ACTIVE_MAP);
1157 | if(undoActiveMap !== ACTIVE_MAP){
1158 | setActiveMap(undoActiveMap)
1159 | updateMaps();
1160 | }
1161 | updateLayers(); // needs to happen after active map is set and maps are updated
1162 | setLayer(undoLayer);
1163 | draw();
1164 | }
1165 | const undo = () => {
1166 | if (undoStepPosition === 0) return;
1167 | undoStepPosition -= 1;
1168 | restoreFromUndoStackData();
1169 | }
1170 | const redo = () => {
1171 | if (undoStepPosition === undoStack.length - 1) return;
1172 | undoStepPosition += 1;
1173 | restoreFromUndoStackData();
1174 | }
1175 | const zoomLevels = [0.25, 0.5, 1, 2, 3, 4];
1176 | let zoomIndex = 1
1177 | const updateZoom = () => {
1178 | tilesetImage.style = `transform: scale(${ZOOM});transform-origin: left top;image-rendering: auto;image-rendering: crisp-edges;image-rendering: pixelated;`;
1179 | tilesetContainer.style.width = `${tilesetImage.width * ZOOM}px`;
1180 | tilesetContainer.style.height = `${tilesetImage.height * ZOOM}px`;
1181 | document.getElementById("zoomLabel").innerText = `${ZOOM}x`;
1182 | updateTilesetGridContainer();
1183 | updateSelection(false);
1184 | updateMapSize({mapWidth: mapTileWidth, mapHeight: mapTileHeight});
1185 | WIDTH = mapTileWidth * SIZE_OF_CROP * ZOOM;// needed when setting zoom?
1186 | HEIGHT = mapTileHeight * SIZE_OF_CROP * ZOOM;
1187 | zoomIndex = zoomLevels.indexOf(ZOOM) === -1 ? 0: zoomLevels.indexOf(ZOOM);
1188 | }
1189 | const zoomIn = () => {
1190 | if(zoomIndex >= zoomLevels.length - 1) return;
1191 | zoomIndex += 1;
1192 | ZOOM = zoomLevels[zoomIndex];
1193 | updateZoom();
1194 | }
1195 | const zoomOut = () => {
1196 | if(zoomIndex === 0) return;
1197 | zoomIndex -= 1;
1198 | ZOOM = zoomLevels[zoomIndex];
1199 | updateZoom();
1200 | }
1201 |
1202 | const toggleSymbolsVisible = (override=null) => {
1203 | if(override === null) DISPLAY_SYMBOLS = !DISPLAY_SYMBOLS;
1204 | document.getElementById("setSymbolsVisBtn").innerHTML = DISPLAY_SYMBOLS ? "👁️": "👓";
1205 | updateTilesetGridContainer();
1206 | }
1207 |
1208 | const getCurrentAnimation = (getAnim) => tileSets[tilesetDataSel.value]?.frames[tileFrameSel.value]?.animations?.[getAnim || tileAnimSel.value];
1209 | const updateTilesetDataList = (populateFrames = false) => {
1210 | const populateWithOptions = (selectEl, options, newContent)=>{
1211 | if(!options) return;
1212 | const value = selectEl.value + "";
1213 | selectEl.innerHTML = newContent;
1214 | Object.keys(options).forEach(opt=>{
1215 | const newOption = document.createElement("option");
1216 | newOption.innerText = opt;
1217 | newOption.value = opt;
1218 | selectEl.appendChild(newOption)
1219 | })
1220 | if (value in options || (["","frames","animations"].includes(value) && !populateFrames)) selectEl.value = value;
1221 | }
1222 |
1223 | if (!populateFrames) populateWithOptions(tileDataSel, tileSets[tilesetDataSel.value]?.tags, `
Symbols (${tileSets[tilesetDataSel.value]?.tileCount || "?"}) Objects `);
1224 | else {
1225 | populateWithOptions(tileFrameSel, tileSets[tilesetDataSel.value]?.frames, '');
1226 | populateWithOptions(tileAnimSel, tileSets[tilesetDataSel.value]?.frames[tileFrameSel.value]?.animations, '')
1227 | }
1228 |
1229 | document.getElementById("tileFrameCount").value = getCurrentFrames()?.frameCount || 1;
1230 | const currentAnim = getCurrentAnimation();
1231 | el.animStart().max = el.tileFrameCount().value;
1232 | el.animEnd().max = el.tileFrameCount().value;
1233 | if(currentAnim){
1234 | console.log({currentAnim})
1235 | el.animStart().value = currentAnim.start || 1
1236 | el.animEnd().value = currentAnim.end || 1
1237 | el.animLoop().checked = currentAnim.loop || false
1238 | el.animSpeed().value = currentAnim.speed || 1
1239 | }
1240 | }
1241 |
1242 | const reevaluateTilesetsData = () =>{
1243 | let symbolStartIdx = 0;
1244 | Object.entries(tileSets).forEach(([key,old])=>{
1245 | const tileData = {};
1246 | // console.log("OLD DATA",old)
1247 | const tileSize = old.tileSize || SIZE_OF_CROP;
1248 | const gridWidth = Math.ceil(old.width / tileSize);
1249 | const gridHeight = Math.ceil(old.height / tileSize);
1250 | const tileCount = gridWidth * gridHeight;
1251 |
1252 | Array.from({length: tileCount}, (x, i) => i).map(tile=>{
1253 | const x = tile % gridWidth;
1254 | const y = Math.floor(tile / gridWidth);
1255 | const oldTileData = old?.[`${x}-${y}`]?.tileData;
1256 | const tileSymbol = randomLetters[Math.floor(symbolStartIdx + tile)];
1257 | tileData[`${x}-${y}`] = {
1258 | ...oldTileData, x, y, tilesetIdx: key, tileSymbol
1259 | }
1260 | tileSets[key] = {...old, tileSize, gridWidth, gridHeight, tileCount, symbolStartIdx, tileData};
1261 | })
1262 | if(key === 0){
1263 | // console.log({gridWidth,gridHeight,tileCount, tileSize})
1264 | }
1265 | symbolStartIdx += tileCount;
1266 |
1267 | })
1268 | // console.log("UPDATED TSETS", tileSets)
1269 | }
1270 | const setCropSize = (newSize) => {
1271 | if(newSize === SIZE_OF_CROP && cropSize.value === newSize) return;
1272 | tileSets[tilesetDataSel.value].tileSize = newSize;
1273 | IMAGES.forEach((ts,idx)=> {
1274 | if (ts.src === tilesetImage.src) IMAGES[idx].tileSize = newSize;
1275 | });
1276 | SIZE_OF_CROP = newSize;
1277 | cropSize.value = SIZE_OF_CROP;
1278 | document.getElementById("gridCropSize").value = SIZE_OF_CROP;
1279 | // console.log("NEW SIZE", tilesetDataSel.value,tileSets[tilesetDataSel.value], newSize,ACTIVE_MAP, maps)
1280 | updateZoom()
1281 | updateTilesetGridContainer();
1282 | // console.log(tileSets, IMAGES)
1283 | reevaluateTilesetsData()
1284 | updateTilesetDataList()
1285 | draw();
1286 | }
1287 |
1288 | // Note: only call this when tileset images have changed
1289 | const reloadTilesets = () =>{
1290 | TILESET_ELEMENTS = [];
1291 | tilesetDataSel.innerHTML = "";
1292 | // Use to prevent old data from erasure
1293 | const oldTilesets = {...tileSets};
1294 | tileSets = {};
1295 | let symbolStartIdx = 0;
1296 | // Generate tileset data for each of the loaded images
1297 | IMAGES.forEach((tsImage, idx)=>{
1298 | const newOpt = document.createElement("option");
1299 | newOpt.innerText = tsImage.name || `tileset ${idx}`;
1300 | newOpt.value = idx;
1301 | tilesetDataSel.appendChild(newOpt);
1302 | const tilesetImgElement = document.createElement("img");
1303 | tilesetImgElement.src = tsImage.src;
1304 | tilesetImgElement.crossOrigin = "Anonymous";
1305 | TILESET_ELEMENTS.push(tilesetImgElement);
1306 | })
1307 |
1308 | Promise.all(Array.from(TILESET_ELEMENTS).filter(img => !img.complete)
1309 | .map(img => new Promise(resolve => { img.onload = img.onerror = resolve; })))
1310 | .then(() => {
1311 | // console.log("TILESET ELEMENTS", TILESET_ELEMENTS)
1312 | TILESET_ELEMENTS.forEach((tsImage,idx) => {
1313 | const tileSize = tsImage.tileSize || SIZE_OF_CROP;
1314 | tileSets[idx] = getEmptyTileSet(
1315 | {
1316 | tags: oldTilesets[idx]?.tags, frames: oldTilesets[idx]?.frames, tileSize,
1317 | animations: oldTilesets[idx]?.animations,
1318 | src: tsImage.src, name: `tileset ${idx}`, width: tsImage.width, height: tsImage.height
1319 | }
1320 | );
1321 | })
1322 | // console.log("POPULATED", tileSets)
1323 | reevaluateTilesetsData();
1324 | tilesetImage.src = TILESET_ELEMENTS[0].src;
1325 | tilesetImage.crossOrigin = "Anonymous";
1326 | updateSelection(false);
1327 | updateTilesetGridContainer();
1328 | });
1329 | // finally current tileset loaded
1330 | tilesetImage.addEventListener('load', () => {
1331 | draw();
1332 | updateLayers();
1333 | if (selection.length === 0) selection = [getTileData(0, 0)];
1334 | updateSelection(false);
1335 | updateTilesetDataList();
1336 | updateTilesetDataList(true);
1337 | updateTilesetGridContainer();
1338 | document.getElementById("tilesetSrcLabel").innerHTML = `src:
${tilesetImage.src} `;
1339 | document.getElementById("tilesetSrcLabel").title = tilesetImage.src;
1340 | const tilesetExtraInfo = IMAGES.find(ts=>ts.src === tilesetImage.src);
1341 |
1342 | // console.log("CHANGED TILESET", tilesetExtraInfo, IMAGES)
1343 |
1344 | if(tilesetExtraInfo) {
1345 | if (tilesetExtraInfo.link) {
1346 | document.getElementById("tilesetHomeLink").innerHTML = `link:
${tilesetExtraInfo.link} `;
1347 | document.getElementById("tilesetHomeLink").title = tilesetExtraInfo.link;
1348 | } else {
1349 | document.getElementById("tilesetHomeLink").innerHTML = "";
1350 | }
1351 | if (tilesetExtraInfo.description) {
1352 | document.getElementById("tilesetDescriptionLabel").innerText = tilesetExtraInfo.description;
1353 | document.getElementById("tilesetDescriptionLabel").title = tilesetExtraInfo.description;
1354 | } else {
1355 | document.getElementById("tilesetDescriptionLabel").innerText = "";
1356 | }
1357 | if (tilesetExtraInfo.tileSize ) {
1358 | setCropSize(tilesetExtraInfo.tileSize);
1359 | }
1360 | }
1361 | setCropSize(tileSets[tilesetDataSel.value].tileSize);
1362 | updateZoom();
1363 | document.querySelector('.canvas_resizer[resizerdir="x"]').style = `left:${WIDTH}px;`;
1364 |
1365 | if (undoStepPosition === -1) addToUndoStack();//initial undo stack entry
1366 | });
1367 | }
1368 |
1369 | const updateMaps = ()=>{
1370 | mapsDataSel.innerHTML = "";
1371 | let lastMap = ACTIVE_MAP;
1372 | Object.keys(maps).forEach((key, idx)=>{
1373 | const newOpt = document.createElement("option");
1374 | newOpt.innerText = maps[key].name//`map ${idx}`;
1375 | newOpt.value = key;
1376 | mapsDataSel.appendChild(newOpt);
1377 | if (idx === Object.keys(maps).length - 1) lastMap = key;
1378 | });
1379 | mapsDataSel.value = lastMap;
1380 | setActiveMap(lastMap);
1381 | document.getElementById("removeMapBtn").disabled = Object.keys(maps).length === 1;
1382 | }
1383 | const loadData = (data) =>{
1384 | try {
1385 | clearUndoStack();
1386 | WIDTH = canvas.width * ZOOM;
1387 | HEIGHT = canvas.height * ZOOM;
1388 | selection = [{}];
1389 | ACTIVE_MAP = data ? Object.keys(data.maps)[0] : "Map_1";
1390 | maps = data ? {...data.maps} : {[ACTIVE_MAP]: getEmptyMap("Map 1", mapTileWidth, mapTileHeight)};
1391 | tileSets = data ? {...data.tileSets} : {};
1392 | reloadTilesets();
1393 | tilesetDataSel.value = "0";
1394 | cropSize.value = data ? tileSets[tilesetDataSel.value]?.tileSize || maps[ACTIVE_MAP].tileSize : SIZE_OF_CROP;
1395 | document.getElementById("gridCropSize").value = cropSize.value;
1396 | updateMaps();
1397 | updateMapSize({mapWidth: maps[ACTIVE_MAP].mapWidth, mapHeight: maps[ACTIVE_MAP].mapHeight})
1398 | }
1399 | catch(e){
1400 | console.error(e)
1401 | }
1402 | }
1403 |
1404 | // Create the tilemap-editor in the dom and its events
1405 | exports.init = (
1406 | attachToId,
1407 | {
1408 | tileMapData, // the main data
1409 | tileSize,
1410 | mapWidth,
1411 | mapHeight,
1412 | tileSetImages,
1413 | applyButtonText,
1414 | onApply,
1415 | tileSetLoaders,
1416 | tileMapExporters,
1417 | tileMapImporters,
1418 | onUpdate = () => {},
1419 | onMouseUp = null,
1420 | appState
1421 | }
1422 | ) => {
1423 | // Attach
1424 | const attachTo = document.getElementById(attachToId);
1425 | if(attachTo === null) return;
1426 |
1427 | apiTileSetLoaders = tileSetLoaders || {};
1428 | apiTileSetLoaders.base64 = {
1429 | name: "Fs (as base64)",
1430 | onSelectImage: (setSrc, file, base64) => {
1431 | setSrc(base64);
1432 | },
1433 | }
1434 | apiTileMapExporters = tileMapExporters;
1435 | apiTileMapExporters.exportAsImage = {
1436 | name: "Export Map as image",
1437 | transformer: exportImage
1438 | }
1439 | apiTileMapExporters.saveData = {
1440 | name: "Download Json file",
1441 | transformer: exportJson
1442 | }
1443 | apiTileMapExporters.analizeTilemap = {
1444 | name: "Analize tilemap",
1445 | transformer: drawAnaliticsReport
1446 | }
1447 | apiTileMapExporters.exportTilesFromMap = {
1448 | name: "Extract tileset from map",
1449 | transformer: exportUniqueTiles
1450 | }
1451 | apiTileMapImporters = tileMapImporters;
1452 | apiTileMapImporters.openData = {
1453 | name: "Open Json file",
1454 | onSelectFiles: (setData, files) => {
1455 | const readFile = new FileReader();
1456 | readFile.onload = (e) => {
1457 | const json = JSON.parse(e.target.result);
1458 | setData(json);
1459 | };
1460 | readFile.readAsText(files[0]);
1461 | },
1462 | acceptFile: "application/JSON"
1463 | }
1464 | apiOnUpdateCallback = onUpdate;
1465 |
1466 | if(onMouseUp){
1467 | apiOnMouseUp = onMouseUp;
1468 | document.getElementById('tileMapEditor').addEventListener('pointerup', function(){
1469 | apiOnMouseUp(getAppState(), apiTileMapExporters)
1470 | })
1471 | }
1472 |
1473 | const importedTilesetImages = (tileMapData?.tileSets && Object.values(tileMapData?.tileSets)) || tileSetImages;
1474 | IMAGES = importedTilesetImages;
1475 | SIZE_OF_CROP = importedTilesetImages?.[0]?.tileSize || tileSize || 32;//to the best of your ability, predict the init tileSize
1476 | mapTileWidth = mapWidth || 12;
1477 | mapTileHeight = mapHeight || 12;
1478 | const canvasWidth = mapTileWidth * tileSize * ZOOM;
1479 | const canvasHeight = mapTileHeight * tileSize * ZOOM;
1480 |
1481 | if (SIZE_OF_CROP < 12) ZOOM = 2;// Automatically start with zoom 2 when the tilesize is tiny
1482 | // Attach elements
1483 | attachTo.innerHTML = getHtml(canvasWidth, canvasHeight);
1484 | attachTo.className = "tilemap_editor_root";
1485 | tilesetImage = document.createElement('img');
1486 | cropSize = document.getElementById('cropSize');
1487 |
1488 | confirmBtn = document.getElementById("confirmBtn");
1489 | if(onApply){
1490 | confirmBtn.innerText = applyButtonText || "Ok";
1491 | } else {
1492 | confirmBtn.style.display = "none";
1493 | }
1494 | canvas = document.getElementById('mapCanvas');
1495 | tilesetContainer = document.querySelector('.tileset-container');
1496 | tilesetSelection = document.querySelector('.tileset-container-selection');
1497 | tilesetGridContainer = document.getElementById("tilesetGridContainer");
1498 | layersElement = document.getElementById("layers");
1499 | objectParametersEditor = document.getElementById("objectParametersEditor");
1500 |
1501 | tilesetContainer.addEventListener("contextmenu", e => {
1502 | e.preventDefault();
1503 | });
1504 |
1505 | tilesetContainer.addEventListener('pointerdown', (e) => {
1506 | tileSelectStart = getSelectedTile(e)[0];
1507 | });
1508 | tilesetContainer.addEventListener('pointermove', (e) => {
1509 | if(tileSelectStart !== null){
1510 | selection = getSelectedTile(e);
1511 | updateSelection();
1512 | }
1513 | });
1514 |
1515 | const setFramesToSelection = (objectName, animName = "") =>{
1516 | console.log({animName, objectName})
1517 | if(objectName === "" || typeof objectName !== "string") return;
1518 | tileSets[tilesetDataSel.value].frames[objectName] = {
1519 | ...(tileSets[tilesetDataSel.value].frames[objectName]||{}),
1520 | width: selectionSize[0], height:selectionSize[1], start: selection[0], tiles: selection,
1521 | name: objectName,
1522 | //To be set when placing tile
1523 | layer: undefined, isFlippedX: false, xPos: 0, yPos: 0//TODO free position
1524 | }
1525 | }
1526 | tilesetContainer.addEventListener('pointerup', (e) => {
1527 | setTimeout(()=>{
1528 | document.getElementById("tilesetDataDetails").open = false;
1529 | },100);
1530 |
1531 | selection = getSelectedTile(e);
1532 | updateSelection();
1533 | selection = getSelectedTile(e);
1534 | tileSelectStart = null;
1535 |
1536 | const viewMode = tileDataSel.value;
1537 | if(viewMode === "" && e.button === 2){
1538 | renameCurrentTileSymbol();
1539 | return;
1540 | }
1541 | if (e.button === 0) {
1542 | if(DISPLAY_SYMBOLS && viewMode !== "" && viewMode !== "frames"){
1543 | selection.forEach(selected=>{
1544 | addToUndoStack();
1545 | const {x, y} = selected;
1546 | const tileKey = `${x}-${y}`;
1547 | const tagTiles = tileSets[tilesetDataSel.value]?.tags[viewMode]?.tiles;
1548 | if (tagTiles){
1549 | if(tileKey in tagTiles) {
1550 | delete tagTiles[tileKey]
1551 | }else {
1552 | tagTiles[tileKey] = { mark: "O"};
1553 | }
1554 | }
1555 | });
1556 | } else if (viewMode === "frames") {
1557 | setFramesToSelection(tileFrameSel.value);
1558 | }
1559 | updateTilesetGridContainer();
1560 | }
1561 | });
1562 | tilesetContainer.addEventListener('dblclick', (e) => {
1563 | const viewMode = tileDataSel.value;
1564 | if(viewMode === "") {
1565 | renameCurrentTileSymbol();
1566 | }
1567 | });
1568 | document.getElementById("addLayerBtn").addEventListener("click",()=>{
1569 | addToUndoStack();
1570 | addLayer();
1571 | });
1572 | // Maps DATA callbacks
1573 | mapsDataSel = document.getElementById("mapsDataSel");
1574 | mapsDataSel.addEventListener("change", e=>{
1575 | addToUndoStack();
1576 | setActiveMap(e.target.value);
1577 | addToUndoStack();
1578 | })
1579 | document.getElementById("addMapBtn").addEventListener("click",()=>{
1580 | const suggestMapName = `Map ${Object.keys(maps).length + 1}`;
1581 | const result = window.prompt("Enter new map key...", suggestMapName);
1582 | if(result !== null) {
1583 | addToUndoStack();
1584 | const newMapKey = result.trim().replaceAll(" ","_") || suggestMapName;
1585 | if (newMapKey in maps){
1586 | alert("A map with this key already exists.")
1587 | return
1588 | }
1589 | maps[newMapKey] = getEmptyMap(result.trim());
1590 | addToUndoStack();
1591 | updateMaps();
1592 | }
1593 | })
1594 | document.getElementById("duplicateMapBtn").addEventListener("click",()=>{
1595 | const makeNewKey = (key) => {
1596 | const suggestedNew = `${key}_copy`;
1597 | if (suggestedNew in maps){
1598 | return makeNewKey(suggestedNew)
1599 | }
1600 | return suggestedNew;
1601 | }
1602 | addToUndoStack();
1603 | const newMapKey = makeNewKey(ACTIVE_MAP);
1604 | maps[newMapKey] = {...JSON.parse(JSON.stringify(maps[ACTIVE_MAP])), name: newMapKey};// todo prompt to ask for name
1605 | updateMaps();
1606 | addToUndoStack();
1607 | })
1608 | document.getElementById("removeMapBtn").addEventListener("click",()=>{
1609 | addToUndoStack();
1610 | delete maps[ACTIVE_MAP];
1611 | setActiveMap(Object.keys(maps)[0])
1612 | updateMaps();
1613 | addToUndoStack();
1614 | })
1615 | // Tileset DATA Callbacks //tileDataSel
1616 | tileDataSel = document.getElementById("tileDataSel");
1617 | tileDataSel.addEventListener("change",()=>{
1618 | selectMode();
1619 | })
1620 | document.getElementById("addTileTagBtn").addEventListener("click",()=>{
1621 | const result = window.prompt("Name your tag", "solid()");
1622 | if(result !== null){
1623 | if (result in tileSets[tilesetDataSel.value].tags) {
1624 | alert("Tag already exists");
1625 | return;
1626 | }
1627 | tileSets[tilesetDataSel.value].tags[result] = getEmptyTilesetTag(result, result);
1628 | updateTilesetDataList();
1629 | addToUndoStack();
1630 | }
1631 | });
1632 | document.getElementById("removeTileTagBtn").addEventListener("click",()=>{
1633 | if (tileDataSel.value && tileDataSel.value in tileSets[tilesetDataSel.value].tags) {
1634 | delete tileSets[tilesetDataSel.value].tags[tileDataSel.value];
1635 | updateTilesetDataList();
1636 | addToUndoStack();
1637 | }
1638 | });
1639 | // Tileset frames
1640 | tileFrameSel = document.getElementById("tileFrameSel");
1641 | tileFrameSel.addEventListener("change", e =>{
1642 | el.tileFrameCount().value = getCurrentFrames()?.frameCount || 1;
1643 | updateTilesetDataList(true);
1644 | updateTilesetGridContainer();
1645 | });
1646 | el.animStart().addEventListener("change", e =>{
1647 | getCurrentAnimation().start = Number(el.animStart().value);
1648 | });
1649 | el.animEnd().addEventListener("change", e =>{
1650 | getCurrentAnimation().end = Number(el.animEnd().value);
1651 | });
1652 | document.getElementById("addTileFrameBtn").addEventListener("click",()=>{
1653 | const result = window.prompt("Name your object", `obj${Object.keys(tileSets[tilesetDataSel.value]?.frames||{}).length}`);
1654 | if(result !== null){
1655 | if (result in tileSets[tilesetDataSel.value].frames) {
1656 | alert("Object already exists");
1657 | return;
1658 | }
1659 | tileSets[tilesetDataSel.value].frames[result] = {
1660 | frameCount: Number(el.tileFrameCount().value),
1661 | animations: {
1662 | a1: {
1663 | start: 1,
1664 | end: Number(el.tileFrameCount().value) || 1,//todo move in here
1665 | name: "a1",
1666 | loop: el.animLoop().checked,
1667 | speed: Number(el.animSpeed().value),
1668 | }
1669 | }
1670 | }
1671 | setFramesToSelection(result);
1672 | updateTilesetDataList(true);
1673 | tileFrameSel.value = result;
1674 | updateTilesetGridContainer();
1675 | }
1676 | });
1677 | document.getElementById("removeTileFrameBtn").addEventListener("click",()=>{
1678 | if (tileFrameSel.value && tileFrameSel.value in tileSets[tilesetDataSel.value].frames && confirm(`Are you sure you want to delete ${tileFrameSel.value}`)) {
1679 | delete tileSets[tilesetDataSel.value].frames[tileFrameSel.value];
1680 | updateTilesetDataList(true);
1681 | updateTilesetGridContainer();
1682 | }
1683 | });
1684 | const renameKeyInObjectForSelectElement = (selectElement, objectPath, typeLabel) =>{
1685 | const oldValue = selectElement.value;
1686 | const result = window.prompt("Rename your animation", `${oldValue}`);
1687 | if(result && result !== oldValue){
1688 | if (!objectPath) return;
1689 | if(result in objectPath){
1690 | alert(`${typeLabel} with the ${result} name already exists. Aborted`);
1691 | return;
1692 | }
1693 | if(result.length < 2) {
1694 | alert(`${typeLabel} name needs to be longer than one character. Aborted`); //so animations and objects never overlap with symbols
1695 | return;
1696 | }
1697 | Object.defineProperty(objectPath, result,
1698 | Object.getOwnPropertyDescriptor(objectPath, oldValue));
1699 | delete objectPath[oldValue];
1700 | updateTilesetDataList(true);
1701 | selectElement.value = result;
1702 | updateTilesetDataList(true);
1703 | }
1704 | }
1705 | el.renameTileFrameBtn().addEventListener("click", ()=>{ // could be a generic function
1706 | renameKeyInObjectForSelectElement(tileFrameSel, tileSets[tilesetDataSel.value]?.frames, "object");
1707 | });
1708 | el.tileFrameCount().addEventListener("change", e=>{
1709 | if(tileFrameSel.value === "") return;
1710 | getCurrentFrames().frameCount = Number(e.target.value);
1711 | updateTilesetGridContainer();
1712 | })
1713 |
1714 | // animations
1715 | tileAnimSel = document.getElementById("tileAnimSel");
1716 | tileAnimSel.addEventListener("change", e =>{//swap with tileAnimSel
1717 | console.log("anim select", e, tileAnimSel.value)
1718 | el.animStart().value = getCurrentAnimation()?.start || 1;
1719 | el.animEnd().value = getCurrentAnimation()?.end || 1;
1720 | el.animLoop().checked = getCurrentAnimation()?.loop || false;
1721 | el.animSpeed().value = getCurrentAnimation()?.speed || 1;
1722 | updateTilesetGridContainer();
1723 | });
1724 | document.getElementById("addTileAnimBtn").addEventListener("click",()=>{
1725 | const result = window.prompt("Name your animation", `anim${Object.keys(tileSets[tilesetDataSel.value]?.frames[tileFrameSel.value]?.animations || {}).length}`);
1726 | if(result !== null){
1727 | if(!tileSets[tilesetDataSel.value].frames[tileFrameSel.value]?.animations){
1728 | tileSets[tilesetDataSel.value].frames[tileFrameSel.value].animations = {}
1729 | }
1730 | if (result in tileSets[tilesetDataSel.value].frames[tileFrameSel.value]?.animations) {
1731 | alert("Animation already exists");
1732 | return;
1733 | }
1734 | tileSets[tilesetDataSel.value].frames[tileFrameSel.value].animations[result] = {
1735 | start: 1,
1736 | end: Number(el.tileFrameCount().value || 1),
1737 | loop: el.animLoop().checked,
1738 | speed: Number(el.animSpeed().value || 1),
1739 | name: result
1740 | }
1741 | // setFramesToSelection(tileFrameSel.value, result);
1742 | updateTilesetDataList(true);
1743 | tileAnimSel.value = result;
1744 | updateTilesetGridContainer();
1745 | }
1746 | });
1747 | document.getElementById("removeTileAnimBtn").addEventListener("click",()=>{
1748 | console.log("delete", tileAnimSel.value, tileSets[tilesetDataSel.value].frames[tileFrameSel.value].animations)
1749 | if (tileAnimSel.value && tileSets[tilesetDataSel.value].frames[tileFrameSel.value]?.animations
1750 | && tileAnimSel.value in tileSets[tilesetDataSel.value].frames[tileFrameSel.value]?.animations
1751 | && confirm(`Are you sure you want to delete ${tileAnimSel.value}`)
1752 | ) {
1753 | delete tileSets[tilesetDataSel.value].frames[tileFrameSel.value].animations[tileAnimSel.value];
1754 | updateTilesetDataList(true);
1755 | updateTilesetGridContainer();
1756 | }
1757 | });
1758 | el.renameTileAnimBtn().addEventListener("click", ()=>{
1759 | renameKeyInObjectForSelectElement(tileAnimSel, tileSets[tilesetDataSel.value]?.frames[tileFrameSel.value]?.animations, "animation");
1760 | });
1761 |
1762 | el.animLoop().addEventListener("change", ()=>{
1763 | getCurrentAnimation().loop = el.animLoop().checked;
1764 | })
1765 | el.animSpeed().addEventListener("change", e=>{
1766 | getCurrentAnimation().speed = el.animSpeed().value;
1767 | })
1768 | // Tileset SELECT callbacks
1769 | tilesetDataSel = document.getElementById("tilesetDataSel");
1770 | tilesetDataSel.addEventListener("change",e=>{
1771 | tilesetImage.src = TILESET_ELEMENTS[e.target.value].src;
1772 | tilesetImage.crossOrigin = "Anonymous";
1773 | updateTilesetDataList();
1774 | })
1775 | el.tileFrameCount().addEventListener("change",()=>{
1776 | el.animStart().max = el.tileFrameCount().value;
1777 | el.animEnd().max = el.tileFrameCount().value;
1778 | })
1779 |
1780 | const replaceSelectedTileSet = (src) => {
1781 | addToUndoStack();
1782 | IMAGES[Number(tilesetDataSel.value)].src = src;
1783 | reloadTilesets();
1784 | }
1785 | const addNewTileSet = (src) => {
1786 | console.log("add new tileset"+ src)
1787 | addToUndoStack();
1788 | IMAGES.push({src});
1789 | reloadTilesets();
1790 | }
1791 | exports.addNewTileSet = addNewTileSet;
1792 | // replace tileset
1793 | document.getElementById("tilesetReplaceInput").addEventListener("change",e=>{
1794 | toBase64(e.target.files[0]).then(base64Src=>{
1795 | if (selectedTileSetLoader.onSelectImage) {
1796 | selectedTileSetLoader.onSelectImage(replaceSelectedTileSet, e.target.files[0], base64Src);
1797 | }
1798 | })
1799 | })
1800 | document.getElementById("replaceTilesetBtn").addEventListener("click",()=>{
1801 | if (selectedTileSetLoader.onSelectImage) {
1802 | document.getElementById("tilesetReplaceInput").click();
1803 | }
1804 | if (selectedTileSetLoader.prompt) {
1805 | selectedTileSetLoader.prompt(replaceSelectedTileSet);
1806 | }
1807 | });
1808 | // add tileset
1809 | document.getElementById("tilesetReadInput").addEventListener("change",e=>{
1810 | toBase64(e.target.files[0]).then(base64Src=>{
1811 | if (selectedTileSetLoader.onSelectImage) {
1812 | selectedTileSetLoader.onSelectImage(addNewTileSet, e.target.files[0], base64Src)
1813 | }
1814 | })
1815 | })
1816 | // remove tileset
1817 | document.getElementById("addTilesetBtn").addEventListener("click",()=>{
1818 | if (selectedTileSetLoader.onSelectImage) {
1819 | document.getElementById("tilesetReadInput").click();
1820 | }
1821 | if (selectedTileSetLoader.prompt) {
1822 | selectedTileSetLoader.prompt(addNewTileSet);
1823 | }
1824 | });
1825 | const tileSetLoadersSel = document.getElementById("tileSetLoadersSel");
1826 | Object.entries(apiTileSetLoaders).forEach(([key,loader])=>{
1827 | const tsLoaderOption = document.createElement("option");
1828 | tsLoaderOption.value = key;
1829 | tsLoaderOption.innerText = loader.name;
1830 | tileSetLoadersSel.appendChild(tsLoaderOption);
1831 | // apiTileSetLoaders[key].load = () => tileSetLoaders
1832 | });
1833 |
1834 | tileSetLoadersSel.value = "base64";
1835 | selectedTileSetLoader = apiTileSetLoaders[tileSetLoadersSel.value];
1836 | tileSetLoadersSel.addEventListener("change", e=>{
1837 | selectedTileSetLoader = apiTileSetLoaders[e.target.value];
1838 | })
1839 | exports.tilesetLoaders = apiTileSetLoaders;
1840 |
1841 | const deleteTilesetWithIndex = (index, cb = null) => {
1842 | if(confirm(`Are you sure you want to delete this image?`)){
1843 | addToUndoStack();
1844 | IMAGES.splice(index,1);
1845 | reloadTilesets();
1846 | if(cb) cb()
1847 | }
1848 | }
1849 | exports.IMAGES = IMAGES;
1850 | exports.deleteTilesetWithIndex = deleteTilesetWithIndex;
1851 | document.getElementById("removeTilesetBtn").addEventListener("click",()=>{
1852 | //Remove current tileset
1853 | if (tilesetDataSel.value !== "0") {
1854 | deleteTilesetWithIndex(Number(tilesetDataSel.value));
1855 | }
1856 | });
1857 |
1858 | // Canvas callbacks
1859 | canvas.addEventListener('pointerdown', setMouseIsTrue);
1860 | canvas.addEventListener('pointerup', setMouseIsFalse);
1861 | canvas.addEventListener('pointerleave', setMouseIsFalse);
1862 | canvas.addEventListener('pointerdown', toggleTile);
1863 | canvas.addEventListener("contextmenu", e => e.preventDefault());
1864 | draggable({ onElement: canvas, element: document.getElementById("canvas_wrapper")});
1865 | canvas.addEventListener('pointermove', (e) => {
1866 | if (isMouseDown && ACTIVE_TOOL !== 2) toggleTile(e)
1867 | });
1868 | // Canvas Resizer ===================
1869 | document.getElementById("canvasWidthInp").addEventListener("change", e=>{
1870 | updateMapSize({mapWidth: Number(e.target.value)})
1871 | })
1872 | document.getElementById("canvasHeightInp").addEventListener("change", e=>{
1873 | updateMapSize({mapHeight: Number(e.target.value)})
1874 | })
1875 | // draggable({
1876 | // element: document.querySelector(".canvas_resizer[resizerdir='x']"),
1877 | // onElement: document.querySelector(".canvas_resizer[resizerdir='x'] span"),
1878 | // isDrag: true, limitY: true,
1879 | // onRelease: ({x}) => {
1880 | // const snappedX = getSnappedPos(x);
1881 | // console.log("SNAPPED GRID", x,snappedX)
1882 | // updateMapSize({mapWidth: snappedX })
1883 | // },
1884 | // });
1885 |
1886 | document.querySelector(".canvas_resizer[resizerdir='y'] input").addEventListener("change", e=>{
1887 | updateMapSize({mapHeight: Number(e.target.value)})
1888 | })
1889 | document.querySelector(".canvas_resizer[resizerdir='x'] input").addEventListener("change", e=>{
1890 | updateMapSize({mapWidth: Number(e.target.value) })
1891 | })
1892 | document.getElementById("toolButtonsWrapper").addEventListener("click",e=>{
1893 | console.log("ACTIVE_TOOL", e.target.value)
1894 | if(e.target.getAttribute("name") === "tool") setActiveTool(Number(e.target.value));
1895 | })
1896 | document.getElementById("gridCropSize").addEventListener('change', e=>{
1897 | setCropSize(Number(e.target.value));
1898 | })
1899 | cropSize.addEventListener('change', e=>{
1900 | setCropSize(Number(e.target.value));
1901 | })
1902 |
1903 | document.getElementById("clearCanvasBtn").addEventListener('click', clearCanvas);
1904 | if(onApply){
1905 | confirmBtn.addEventListener('click', () => onApply.onClick(getExportData()));
1906 | }
1907 |
1908 | document.getElementById("renameMapBtn").addEventListener("click",()=>{
1909 | const newName = window.prompt("Change map name:", maps[ACTIVE_MAP].name || "Map");
1910 | if(newName !== null && maps[ACTIVE_MAP].name !== newName){
1911 | if(Object.values(maps).map(map=>map.name).includes(newName)){
1912 | alert(`${newName} already exists`);
1913 | return
1914 | }
1915 | maps[ACTIVE_MAP].name = newName;
1916 | updateMaps();
1917 | }
1918 | })
1919 |
1920 | const fileMenuDropDown = document.getElementById("fileMenuDropDown");
1921 | const makeMenuItem = (name, value, description) =>{
1922 | const menuItem = document.createElement("span");
1923 | menuItem.className = "item";
1924 | menuItem.innerText = name;
1925 | menuItem.title = description || name;
1926 | menuItem.value = value;
1927 | fileMenuDropDown.appendChild(menuItem);
1928 | return menuItem;
1929 | }
1930 | Object.entries(tileMapExporters).forEach(([key, exporter])=>{
1931 | makeMenuItem(exporter.name, key,exporter.description).onclick = () => {
1932 | exporter.transformer(getExportData());
1933 | }
1934 | apiTileMapExporters[key].getData = () => exporter.transformer(getExportData());
1935 | })
1936 | exports.exporters = apiTileMapExporters;
1937 |
1938 | Object.entries(apiTileMapImporters).forEach(([key, importer])=>{
1939 | makeMenuItem(importer.name, key,importer.description).onclick = () => {
1940 | if(importer.onSelectFiles) {
1941 | const input = document.createElement("input");
1942 | input.type = "file";
1943 | input.id = `importerInput-${key}`;
1944 | if(importer.acceptFile) input.accept = importer.acceptFile;
1945 | input.style.display = "none";
1946 | input.addEventListener("change",e=> {
1947 | importer.onSelectFiles(loadData, e.target.files);
1948 | })
1949 | input.click();
1950 | }
1951 | }
1952 | // apiTileMapImporters[key].setData = (files) => importer.onSelectFiles(loadData, files);
1953 | })
1954 | document.getElementById("toggleFlipX").addEventListener("change",(e)=>{
1955 | document.getElementById("flipBrushIndicator").style.transform = e.target.checked ? "scale(-1, 1)": "scale(1, 1)"
1956 | })
1957 | document.addEventListener('keypress', e =>{
1958 | if(e.ctrlKey){
1959 | if(e.code === "KeyZ") undo();
1960 | if(e.code === "KeyY") redo();
1961 | }
1962 | })
1963 | document.getElementById("gridColorSel").addEventListener("change", e=>{
1964 | console.log("grid col",e.target.value)
1965 | maps[ACTIVE_MAP].gridColor = e.target.value;
1966 | draw();
1967 | })
1968 | document.getElementById("showGrid").addEventListener("change", e => {
1969 | SHOW_GRID = e.target.checked;
1970 | draw();
1971 | })
1972 |
1973 | document.getElementById("undoBtn").addEventListener("click", undo);
1974 | document.getElementById("redoBtn").addEventListener("click", redo);
1975 | document.getElementById("zoomIn").addEventListener("click", zoomIn);
1976 | document.getElementById("zoomOut").addEventListener("click", zoomOut);
1977 | document.getElementById("setSymbolsVisBtn").addEventListener("click", ()=>toggleSymbolsVisible())
1978 | // Scroll zoom in/out - use wheel instead of scroll event since theres no scrollbar on the map
1979 | canvas.addEventListener('wheel', e=> {
1980 | if (e.deltaY < 0) zoomIn();
1981 | else zoomOut();
1982 | });
1983 |
1984 | loadData(tileMapData)
1985 | if (appState) {
1986 | ACTIVE_MAP = appState.ACTIVE_MAP;
1987 | mapsDataSel.value = ACTIVE_MAP;
1988 | setActiveMap(appState.ACTIVE_MAP)
1989 | PREV_ACTIVE_TOOL = appState.PREV_ACTIVE_TOOL;
1990 | ACTIVE_TOOL = appState.ACTIVE_TOOL;
1991 | setActiveTool(appState.ACTIVE_TOOL)
1992 | setLayer(appState.currentLayer)
1993 | selection = appState.selection;
1994 | updateSelection(false);
1995 | SHOW_GRID = appState.SHOW_GRID;
1996 | }
1997 |
1998 | // Animated tiles when on frames mode
1999 | const animateTiles = () => {
2000 | if (tileDataSel.value === "frames") draw();
2001 | requestAnimationFrame(animateTiles);
2002 | }
2003 | requestAnimationFrame(animateTiles);
2004 | };
2005 |
2006 | exports.getState = () => {
2007 | return getAppState();
2008 | }
2009 |
2010 | exports.onUpdate = apiOnUpdateCallback;
2011 | exports.onMouseUp = apiOnMouseUp;
2012 |
2013 | exports.getTilesets = () => tileSets;
2014 | });
2015 |
--------------------------------------------------------------------------------