diff --git a/index.css b/index.css index 891b6ede..90be984f 100644 --- a/index.css +++ b/index.css @@ -584,6 +584,13 @@ button.active { padding: 4px 0; } +#viewMode > button { + padding: .35em; + margin: .2em .3em; + float: left; + width: 30.7%; +} + fieldset { border: 1px solid #5d4651; } @@ -604,8 +611,9 @@ fieldset { color: grey; } -.tabcontent li:hover { - background-color: #a8879d; +.tabcontent li:hover, +.tabcontent button:hover { + box-shadow: 0 0 2px 2px #5d465117; } #optionsContainer span { @@ -1282,6 +1290,23 @@ div.states span.inactive:hover { cursor: pointer; } +#diplomacySelect { + position: absolute; + background-color: #ffffff; + border: 1px solid #1891ff; + width: 23%; + left: 70.5%; +} + +#diplomacySelect > div { + width: 100%; +} + +#diplomacySelect > div:hover { + background-color: #1891ff; + color: #ffffff; +} + #burgsFooterPopulation { border: 0; width: 50px; diff --git a/index.html b/index.html index ac4c045e..37ec6898 100644 --- a/index.html +++ b/index.html @@ -18,7 +18,7 @@ - - - - - + + + + + @@ -899,7 +899,7 @@
Azgaar's
Fantasy Map Generator
-
v. 1.1
+
v. 1.2

LOADING...

@@ -931,6 +931,7 @@ + @@ -965,6 +966,13 @@
  • Rulers
  • Scale Bar
  • + +
    +

    View mode:

    + + + +
    @@ -1745,9 +1753,9 @@ - + - PNG resolution + PNG/JPEG size @@ -1859,7 +1867,7 @@ - +
    @@ -1920,7 +1928,8 @@
    .map
    .svg
    -
    .png
    +
    .png
    +
    .jpeg
    .json
    storage
    @@ -2608,8 +2617,6 @@
    - - + + @@ -3296,7 +3317,7 @@ - + diff --git a/main.js b/main.js index 41f43a1f..3e786c5d 100644 --- a/main.js +++ b/main.js @@ -7,7 +7,7 @@ // See also https://github.com/Azgaar/Fantasy-Map-Generator/issues/153 "use strict"; -const version = "1.11"; // generator version +const version = "1.2"; // generator version document.title += " v" + version; // if map version is not stored, clear localStorage and show a message @@ -315,7 +315,7 @@ function applyDefaultBiomesSystem() { } function showWelcomeMessage() { - const post = link("https://www.reddit.com/r/FantasyMapGenerator/comments/daf6g2/update_new_version_is_published_v_11", "Main changes:"); // announcement on Reddit + const post = link("https://www.reddit.com/r/FantasyMapGenerator/comments/daf6g2/update_new_version_is_published_v_12", "Main changes:"); // announcement on Reddit const changelog = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "previous version"); const reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit community"); const discord = link("https://discordapp.com/invite/X7E84HU", "Discord server"); @@ -325,15 +325,8 @@ function showWelcomeMessage() { This version is compatible with ${changelog}, loaded .map files will be auto-updated.

    Join our ${reddit} and ${discord} to ask questions, share maps, discuss the Generator, report bugs and propose new features.

    @@ -1686,6 +1679,8 @@ const regenerateMap = debounce(function() { resetZoom(1000); generate(); restoreLayers(); + const canvas3d = document.getElementById("canvas3d"); + if (canvas3d) update3dPreview(canvas3d); if ($("#worldConfigurator").is(":visible")) editWorld(); }, 500); diff --git a/modules/save-and-load.js b/modules/save-and-load.js index d88b897b..68b4fc67 100644 --- a/modules/save-and-load.js +++ b/modules/save-and-load.js @@ -11,7 +11,7 @@ async function saveSVG() { document.body.appendChild(link); link.click(); - tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check`, true, "warning", 5000); + tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check`, true, "success", 5000); console.timeEnd("saveSVG"); } @@ -38,7 +38,7 @@ async function savePNG() { window.setTimeout(function() { canvas.remove(); window.URL.revokeObjectURL(link.href); - tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check`, true, "warning", 5000); + tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check`, true, "success", 5000); }, 1000); }); } @@ -46,6 +46,33 @@ async function savePNG() { console.timeEnd("savePNG"); } +// download map as JPEG +async function saveJPEG() { + console.time("saveJPEG"); + const url = await getMapURL("png"); + + const canvas = document.createElement("canvas"); + canvas.width = svgWidth * pngResolutionInput.value; + canvas.height = svgHeight * pngResolutionInput.value; + const img = new Image(); + img.src = url; + + img.onload = async function() { + canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height); + const quality = Math.min(rn(1 - pngResolutionInput.value / 20, 2), .92); + const URL = await canvas.toDataURL("image/jpeg", quality); + const link = document.createElement("a"); + link.download = getFileName() + ".jpeg"; + link.href = URL; + document.body.appendChild(link); + link.click(); + tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000); + window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000); + } + + console.timeEnd("saveJPEG"); +} + // parse map svg to object url async function getMapURL(type) { const cloneEl = document.getElementById("map").cloneNode(true); // clone svg @@ -59,7 +86,7 @@ async function getMapURL(type) { if (type === "mesh") clone.attr("width", graphWidth).attr("height", graphHeight); if (type !== "png") clone.select("#viewbox").attr("transform", null); // reset transform to show whole map if (type === "svg") removeUnusedElements(clone); - if (type === "mesh") updateMeshCells(clone); + if (customization && type === "mesh") updateMeshCells(clone); inlineStyle(clone); const fontStyle = await GFontToDataURI(getFontsToLoad()); // load non-standard fonts @@ -777,8 +804,8 @@ function parseLoadedData(data) { if (!markers.selectAll("*").size()) {addMarkers(); turnButtonOn("toggleMarkers");} // 1.0 add fogging layer (state focus) - let fogging = viewbox.insert("g", "#ruler").attr("id", "fogging-cont").attr("mask", "url(#fog)") - .append("g").attr("id", "fogging").attr("display", "none"); + fogging = viewbox.insert("g", "#ruler").attr("id", "fogging-cont").attr("mask", "url(#fog)") + .append("g").attr("id", "fogging").style("display", "none"); fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%"); defs.append("mask").attr("id", "fog").append("rect").attr("x", 0).attr("y", 0).attr("width", "100%") .attr("height", "100%").attr("fill", "white"); @@ -873,11 +900,14 @@ function parseLoadedData(data) { // v 1.11 replaced "display" attribute by "display" style viewbox.selectAll("g").each(function() { - if (this.hasAttribute("display")) this.removeAttribute("display"); - fogging.style("display", "none"); - prec.style("display", "none"); - ruler.style("display", "none"); + if (this.hasAttribute("display")) { + this.removeAttribute("display"); + this.style.display = "none"; + } }); + + // v 1.11 had an issue with fogging being displayed on load + unfog(); } }() diff --git a/modules/ui/3d.js b/modules/ui/3d.js new file mode 100644 index 00000000..523ee75c --- /dev/null +++ b/modules/ui/3d.js @@ -0,0 +1,226 @@ +"use strict"; +let threeD = {}; // master object for 3d scane and parameters +let threeDscale = 50; // 3d scene scale + +// start 3d view and heightmap edit preview +async function start3d(canvas) { + const loaded = await loadTHREE(); + if (!loaded) {tip("Cannot load 3d library", false, "error", 4000); return false}; + + threeD.scene = new THREE.Scene(); + //threeD.scene.background = new THREE.Color(0x53679f); + threeD.camera = new THREE.PerspectiveCamera(70, canvas.width / canvas.height, 0.1, 2000); + threeD.camera.position.x = 0; + threeD.camera.position.z = 350; + threeD.camera.position.y = 285; + threeD.Renderer = new THREE.WebGLRenderer({canvas, antialias: true, preserveDrawingBuffer: true}); + threeD.controls = await OrbitControls(threeD.camera, threeD.Renderer.domElement); + threeD.controls.minDistance = 10; threeD.controls.maxDistance = 1000; + threeD.controls.maxPolarAngle = Math.PI/2; + threeD.controls.keys = {}; + + threeD.Renderer.setSize(canvas.width, canvas.height); + add3dMesh(graphWidth, graphHeight, grid.cellsX, grid.cellsY); + + const ambientLight = new THREE.AmbientLight(0xcccccc, .7); + threeD.scene.add(ambientLight); + const spotLight = new THREE.SpotLight(0xcccccc, .8, 2000, .7, 0, 0); + spotLight.position.set(100, 600, 1000); + spotLight.castShadow = true; + threeD.scene.add(spotLight); + //threeD.scene.add(new THREE.SpotLightHelper(spotLight)); + + threeD.controls.addEventListener("change", render); + return true; +} + +// create a mesh from pixel data +async function add3dMesh(width, height, segmentsX, segmentsY) { + const geometry = new THREE.PlaneGeometry(width, height, segmentsX-1, segmentsY-1); + + // generateTexture + //threeD.material = new THREE.MeshBasicMaterial(); + //const texture = new THREE.CanvasTexture(generateTexture(grid.cells.h, grid.cellsX, grid.cellsY)); + //threeD.material.map = texture; + + const url = await getMapURL("mesh"); + threeD.material = new THREE.MeshLambertMaterial(); + const texture = new THREE.TextureLoader().load(url, render); + texture.needsUpdate = true; + threeD.material.map = texture; + + geometry.vertices.forEach((v, i) => v.z = getMeshHeight(i)); + geometry.computeVertexNormals(); // added + threeD.Renderer.shadowMap.enabled = true; // added + threeD.mesh = new THREE.Mesh(geometry, threeD.material); + threeD.mesh.rotation.x = -Math.PI / 2; + threeD.mesh.castShadow = true; + threeD.mesh.receiveShadow = true; + threeD.scene.add(threeD.mesh); +} + +function getMeshHeight(i) { + const h = grid.cells.h[i]; + return h < 20 ? 0 : (h - 18) / 82 * threeDscale; +} + +function generateTexture(data, width, height) { + let context, image, imageData; + const vector3 = new THREE.Vector3(0, 0, 0); + const sun = new THREE.Vector3(1, 1, 1); + sun.normalize(); + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + context = canvas.getContext('2d'); + context.fillStyle = '#000'; + context.fillRect(0, 0, width, height); + + image = context.getImageData(0, 0, canvas.width, canvas.height); + imageData = image.data; + + for (let i = 0, j = 0; i < imageData.length; i += 4, j ++) { + vector3.x = data[j - 2] - data[j + 2]; + vector3.y = 2; + vector3.z = data[j - width * 2] - data[j + width * 2]; + vector3.normalize(); + + const shade = vector3.dot(sun); + // initial: r 96 + shade * 128, g 32 + shade * 96, b shade * 96; + const clr = (shade * 255) * (.5 + data[j] * .007); // new: black and white + imageData[i] = imageData[i + 1] = imageData[i + 2] = clr; + } + context.putImageData(image, 0, 0); + + const canvasScaled = document.createElement('canvas'); + canvasScaled.width = width * 4; + canvasScaled.height = height * 4; + context = canvasScaled.getContext('2d'); + context.scale(4, 4); + context.drawImage(canvas, 0, 0); + + image = context.getImageData(0, 0, canvasScaled.width, canvasScaled.height); + imageData = image.data; + + for (let i = 0; i < imageData.length; i += 4) { + const v = ~~(Math.random() * 5); + imageData[i] += v; + imageData[i + 1] += v; + imageData[i + 2] += v; + } + context.putImageData(image, 0, 0); + return canvasScaled; +} + +function loadTHREE() { + if (window.THREE) return Promise.resolve(true); + + return new Promise(resolve => { + const script = document.createElement('script'); + script.src = "libs/three.min.js" + document.head.append(script); + script.onload = () => resolve(true); + script.onerror = () => resolve(false); + }); +} + +function OrbitControls(camera, domElement) { + if (THREE.OrbitControls) new THREE.OrbitControls(camera, domElement); + + return new Promise(resolve => { + const script = document.createElement('script'); + script.src = "libs/orbitControls.min.js" + document.head.append(script); + script.onload = () => resolve(new THREE.OrbitControls(camera, domElement)); + script.onerror = () => resolve(false); + }); +} + +function update3dPreview(canvas) { + threeD.scene.remove(threeD.mesh); + threeD.Renderer.setSize(canvas.width, canvas.height); + if (canvas.dataset.type === "viewGlobe") addGlobe3dMesh(); + else add3dMesh(graphWidth, graphHeight, grid.cellsX, grid.cellsY); + render(); +} + +async function update3d() { + const url = await getMapURL("mesh"); + threeD.material.map = new THREE.TextureLoader().load(url, render); +} + +function stop3d() { + if (!threeD.controls || !threeD.Renderer) return; + threeD.controls.dispose(); + threeD.Renderer.dispose() + cancelAnimationFrame(threeD.animationFrame); + threeD = {}; +} + +async function startGlobe(canvas) { + const loaded = await loadTHREE(); + if (!loaded) {tip("Cannot load 3d library", false, "error", 4000); return false}; + + threeD.scene = new THREE.Scene(); + threeD.scene.background = new THREE.TextureLoader().load("https://i0.wp.com/azgaar.files.wordpress.com/2019/10/stars.png", render); + threeD.Renderer = new THREE.WebGLRenderer({canvas, antialias: true, preserveDrawingBuffer: true}); + threeD.Renderer.setSize(canvas.width, canvas.height); + + threeD.camera = new THREE.PerspectiveCamera(45, canvas.width / canvas.height, 0.1, 1000).translateZ(5); + threeD.controls = await OrbitControls(threeD.camera, threeD.Renderer.domElement); + threeD.controls.minDistance = 1.8; threeD.controls.maxDistance = 10; + threeD.controls.autoRotate = true; + threeD.controls.keys = {}; + + const ambientLight = new THREE.AmbientLight(0xcccccc, .9); + threeD.scene.add(ambientLight); + const spotLight = new THREE.SpotLight(0xcccccc, .6, 200, .7, .1, 0); + spotLight.position.set(700, 300, 200); + spotLight.castShadow = false; + threeD.scene.add(spotLight); + //threeD.scene.add(new THREE.SpotLightHelper(spotLight)); + + addGlobe3dMesh(); + threeD.controls.addEventListener("change", render); + threeD.animationFrame = requestAnimationFrame(animate); + return true; +} + +// create globe mesh just from svg +async function addGlobe3dMesh() { + threeD.material = new THREE.MeshLambertMaterial(); + const url = await getMapURL("mesh"); + threeD.material.map = new THREE.TextureLoader().load(url, render); + threeD.mesh = new THREE.Mesh(new THREE.SphereBufferGeometry(1, 64, 64), threeD.material); + threeD.scene.add(threeD.mesh); +} + +// render 3d scene and camera, do only on controls change +function render() { + threeD.Renderer.render(threeD.scene, threeD.camera); +} + +// animate 3d scene and camera +function animate() { + threeD.animationFrame = requestAnimationFrame(animate); + threeD.controls.update(); + threeD.Renderer.render(threeD.scene, threeD.camera); +} + +function toggleRotation() { + const rotate = threeD.controls.autoRotate = !threeD.controls.autoRotate; + rotate ? requestAnimationFrame(animate) : cancelAnimationFrame(threeD.animationFrame); +} + +// download screenshot +async function saveScreenshot() { + const URL = threeD.Renderer.domElement.toDataURL("image/jpeg"); + const link = document.createElement("a"); + link.download = getFileName() + ".jpeg"; + link.href = URL; + document.body.appendChild(link); + link.click(); + tip(`Screenshot is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000); + window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000); +} \ No newline at end of file diff --git a/modules/ui/3dpreview.js b/modules/ui/3dpreview.js deleted file mode 100644 index 27dfb4eb..00000000 --- a/modules/ui/3dpreview.js +++ /dev/null @@ -1,91 +0,0 @@ -"use strict"; - -// Define variables - these make it easy to work with from the console -let _3dpreviewScale = 70; -let _3dpreviewCamera = null; -let _3dpreviewScene = null; -let _3dpreviewRenderer = null; -let _3danimationFrame = null; -let _3dmaterial = null; -let _3dmesh = null; - -// Create a mesh from pixel data -async function addMesh(width, height, segmentsX, segmentsY) { - const _3dgeometry = new THREE.PlaneGeometry(width, height, segmentsX-1, segmentsY-1); - const _3dmaterial = new THREE.MeshBasicMaterial({wireframe: false}); - const url = await getMapURL("mesh"); - _3dmaterial.map = new THREE.TextureLoader().load(url); - _3dgeometry.vertices.forEach((v, i) => v.z = getMeshHeight(i)); - _3dmesh = new THREE.Mesh(_3dgeometry, _3dmaterial); - _3dmesh.rotation.x = -Math.PI / 2; - _3dpreviewScene.add(_3dmesh); -} - -function getMeshHeight(i) { - const h = grid.cells.h[i]; - return h < 20 ? 0 : (h - 18) / 82 * _3dpreviewScale; -} - -// Function to render scene and camera -function render() { - _3danimationFrame = requestAnimationFrame(render); - _3dpreviewRenderer.render(_3dpreviewScene, _3dpreviewCamera); -} - -async function start3dpreview(canvas) { - const loaded = await loadTHREE(); - if (!loaded) { - tip("Cannot load 3d library", false, "error", 4000); - return false; - }; - _3dpreviewScene = new THREE.Scene(); - _3dpreviewCamera = new THREE.PerspectiveCamera(70, canvas.width / canvas.height, 0.1, 100000); - _3dpreviewCamera.position.x = 0; - _3dpreviewCamera.position.z = 350; - _3dpreviewCamera.position.y = 285; - _3dpreviewRenderer = new THREE.WebGLRenderer({canvas}); - OrbitControls(_3dpreviewCamera, _3dpreviewRenderer.domElement); - _3dpreviewRenderer.setSize(canvas.width, canvas.height); - addMesh(graphWidth, graphHeight, grid.cellsX, grid.cellsY); - _3danimationFrame = requestAnimationFrame(render); - return true; -} - -function loadTHREE() { - if (window.THREE) return Promise.resolve(true); - - return new Promise(resolve => { - const script = document.createElement('script'); - script.src = "libs/three.min.js" - document.head.append(script); - script.onload = () => resolve(true); - script.onerror = () => resolve(false); - }); -} - -function OrbitControls(camera, domElement) { - if (THREE.OrbitControls) { - new THREE.OrbitControls(camera, domElement); - return; - } - - const script = document.createElement('script'); - script.src = "libs/orbitControls.min.js" - document.head.append(script); - script.onload = () => new THREE.OrbitControls(camera, domElement); -} - -function update3dpreview(canvas) { - _3dpreviewScene.remove(_3dmesh); - _3dpreviewRenderer.setSize(canvas.width, canvas.height); - addMesh(graphWidth, graphHeight, grid.cellsX, grid.cellsY); -} - -function stop3dpreview() { - cancelAnimationFrame(_3danimationFrame); - _3danimationFrame = null; - _3dmesh = undefined; - _3dmaterial = undefined; - _3dpreviewScene = null; - _3dpreviewRenderer = null; -} \ No newline at end of file diff --git a/modules/ui/diplomacy-editor.js b/modules/ui/diplomacy-editor.js index 182d22f0..f4264c3f 100644 --- a/modules/ui/diplomacy-editor.js +++ b/modules/ui/diplomacy-editor.js @@ -39,6 +39,7 @@ function editDiplomacy() { document.getElementById("diplomacyMatrix").addEventListener("click", showRelationsMatrix); document.getElementById("diplomacyHistory").addEventListener("click", showRelationsHistory); document.getElementById("diplomacyExport").addEventListener("click", downloadDiplomacyData); + document.getElementById("diplomacySelect").addEventListener("click", diplomacyChangeRelations); function refreshDiplomacyEditor() { diplomacyEditorAddLines(); @@ -52,6 +53,11 @@ function editDiplomacy() { const sel = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i; const selName = states[sel].fullName; + // move select drop-down back to initial place + const select = document.getElementById("diplomacySelect"); + body.parentNode.insertBefore(select, body); + select.style.display = "none"; + let lines = `
    ${selName}
    `; @@ -62,11 +68,15 @@ function editDiplomacy() { const index = statuses.indexOf(relation); const color = colors[index]; const tip = s.fullName + description[index] + selName; + const tipSelect = `${tip}. Click to see relations to ${s.name}`; + const tipChange = `${tip}. Click to change relations to ${selName}`; lines += `
    -
    ${s.fullName}
    - - +
    ${s.fullName}
    + + + +
    `; } body.innerHTML = lines; @@ -75,8 +85,7 @@ function editDiplomacy() { body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev))); body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev))); body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick)); - body.querySelectorAll("div > select.diplomacyRelations").forEach(el => el.addEventListener("click", ev => ev.stopPropagation())); - body.querySelectorAll("div > select.diplomacyRelations").forEach(el => el.addEventListener("change", diplomacyChangeRelations)); + body.querySelectorAll(".changeRelations").forEach(el => el.addEventListener("click", toggleDiplomacySelect)); applySorting(diplomacyHeader); $("#diplomacyEditor").dialog(); @@ -113,12 +122,6 @@ function editDiplomacy() { }); } - function getRelations(relations) { - let options = ""; - statuses.forEach(s => options += ``); - return options; - } - function showStateRelations() { const selectedLine = body.querySelector("div.Self"); const sel = selectedLine ? +selectedLine.dataset.id : pack.states.find(s => s.i && !s.removed).i; @@ -156,18 +159,33 @@ function editDiplomacy() { refreshDiplomacyEditor(); } - function diplomacyChangeRelations() { + function toggleDiplomacySelect(event) { + event.stopPropagation(); + const select = document.getElementById("diplomacySelect"); + const show = select.style.display === "none"; + if (!show) {select.style.display = "none"; return;} + event.target.parentNode.insertBefore(select, event.target); + select.style.display = "block"; + } + + function diplomacyChangeRelations(event) { + event.stopPropagation(); + const select = document.getElementById("diplomacySelect"); + select.style.display = "none"; + + const subject = +event.target.parentElement.parentElement.dataset.id; + const rel = event.target.innerHTML; + body.parentNode.insertBefore(select, body); + const states = pack.states, chronicle = states[0].diplomacy; const selectedLine = body.querySelector("div.Self"); const object = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i; if (!object) return; const objectName = states[object].name; // object of relations change - - const subject = +this.parentNode.dataset.id; const subjectName = states[subject].name; // subject of relations change - actor const oldRel = states[subject].diplomacy[object]; - const rel = this.value; + if (rel === oldRel) return; states[subject].diplomacy[object] = rel; states[object].diplomacy[subject] = rel === "Vassal" ? "Suzerain" : rel === "Suzerain" ? "Vassal" : rel; diff --git a/modules/ui/general.js b/modules/ui/general.js index e1b08f2b..ec9b6f8b 100644 --- a/modules/ui/general.js +++ b/modules/ui/general.js @@ -279,13 +279,14 @@ document.addEventListener("keydown", event => { // Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys document.addEventListener("keyup", event => { - const active = document.activeElement.tagName; + if (!window.closeDialogs) return; // not all modules are loaded + const canvas3d = document.getElementById("canvas3d"); // check if 3d mode is active + const active = canvas3d ? null : document.activeElement.tagName; if (active === "INPUT" || active === "SELECT" || active === "TEXTAREA") return; // don't trigger if user inputs a text if (active === "DIV" && document.activeElement.contentEditable === "true") return; // don't trigger if user inputs a text event.stopPropagation(); const key = event.keyCode, ctrl = event.ctrlKey, shift = event.shiftKey, meta = event.metaKey; - const tdMode = document.getElementById("_3dpreview"); if (key === 112) showInfo(); // "F1" to show info else if (key === 113) regeneratePrompt(); // "F2" for new map @@ -296,7 +297,12 @@ document.addEventListener("keyup", event => { else if (key === 27) {closeDialogs(); hideOptions();} // Escape to close all dialogs else if (key === 46) removeElementOnKey(); // "Delete" to remove the selected element + else if (key === 83 && canvas3d) saveScreenshot(); // "S" to save a screenshot + else if (key === 82 && canvas3d) toggleRotation(); // "R" to toggle 3d rotation + else if (key === 85 && canvas3d && customization !== 1) update3d(); // "U" to update 3d view + else if (ctrl && key === 80) savePNG(); // Ctrl + "P" to save as PNG + else if (ctrl && key === 71) saveJPEG(); // Ctrl + "J" to save as JPEG else if (ctrl && key === 83) saveSVG(); // Ctrl + "S" to save as SVG else if (ctrl && key === 77) saveMap(); // Ctrl + "M" to save MAP file else if (ctrl && key === 71) saveGeoJSON(); // Ctrl + "G" to save as GeoJSON @@ -357,10 +363,10 @@ document.addEventListener("keyup", event => { else if (key === 187) toggleRulers(); // Equal (=) to toggle Rulers else if (key === 189) toggleScaleBar(); // Minus (-) to toggle Scale bar - else if (key === 37 && !tdMode) zoom.translateBy(svg, 10, 0); // Left to scroll map left - else if (key === 39 && !tdMode) zoom.translateBy(svg, -10, 0); // Right to scroll map right - else if (key === 38 && !tdMode) zoom.translateBy(svg, 0, 10); // Up to scroll map up - else if (key === 40 && !tdMode) zoom.translateBy(svg, 0, -10); // Up to scroll map up + else if (key === 37) zoom.translateBy(svg, 10, 0); // Left to scroll map left + else if (key === 39) zoom.translateBy(svg, -10, 0); // Right to scroll map right + else if (key === 38) zoom.translateBy(svg, 0, 10); // Up to scroll map up + else if (key === 40) zoom.translateBy(svg, 0, -10); // Up to scroll map up else if (key === 107 || key === 109) pressNumpadSign(key); // Numpad Plus/Minus to zoom map or change brush size else if (key === 48 || key === 96) resetZoom(1000); // 0 to reset zoom else if (key === 49 || key === 97) zoom.scaleTo(svg, 1); // 1 to zoom to 1 diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index c9c1d402..5465ab6d 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -127,7 +127,7 @@ function editHeightmap() { restartHistory(); if (document.getElementById("preview")) document.getElementById("preview").remove(); - if (document.getElementById("_3dpreview")) toggleHeightmap3dView(); + if (document.getElementById("canvas3d")) toggleHeightmap3dView(); const mode = heightmapEditMode.innerHTML; @@ -415,7 +415,7 @@ function editHeightmap() { if (!noStat) { updateStatistics(); if (document.getElementById("preview")) drawHeightmapPreview(); // update heightmap preview if opened - if (document.getElementById("_3dpreview")) update3dpreview(_3dpreview); // update 3d heightmap preview if opened + if (document.getElementById("canvas3d")) update3dPreview(canvas3d); // update 3d heightmap preview if opened } } @@ -430,7 +430,7 @@ function editHeightmap() { updateStatistics(); if (document.getElementById("preview")) drawHeightmapPreview(); // update heightmap preview if opened - if (document.getElementById("_3dpreview")) update3dpreview(_3dpreview); // update 3d heightmap preview if opened + if (document.getElementById("canvas3d")) update3dPreview(canvas3d); // update 3d heightmap preview if opened } // restart edits from 1st step @@ -871,7 +871,7 @@ function editHeightmap() { updateStatistics(); mockHeightmap(); if (document.getElementById("preview")) drawHeightmapPreview(); // update heightmap preview if opened - if (document.getElementById("_3dpreview")) update3dpreview(_3dpreview); // update 3d heightmap preview if opened + if (document.getElementById("canvas3d")) update3dPreview(canvas3d); // update 3d heightmap preview if opened } function downloadTemplate() { @@ -1195,38 +1195,38 @@ function editHeightmap() { // 3D previewer async function toggleHeightmap3dView() { - if (document.getElementById("_3dpreview")) { - $("#_3dpreviewEditor").dialog("close"); + if (document.getElementById("canvas3d")) { + $("#preview3d").dialog("close"); return; } const canvas = document.createElement("canvas"); - canvas.id = "_3dpreview"; + canvas.id = "canvas3d"; canvas.style.display = "block"; - canvas.width = parseFloat(_3dpreviewEditor.style.width) || graphWidth / 3; + canvas.width = parseFloat(preview3d.style.width) || graphWidth / 3; canvas.height = canvas.width / (graphWidth / graphHeight); - const started = await start3dpreview(canvas); + const started = await start3d(canvas); if (!started) return; - document.getElementById("_3dpreviewEditor").appendChild(canvas); + document.getElementById("preview3d").appendChild(canvas); canvas.onmouseenter = () => { - canvas.dataset.hovered ? tip("") : tip("Left mouse to change angle, middle mouse or mousewheel to zoom, right mouse to pan"); - canvas.dataset.hovered = 1; + +canvas.dataset.hovered > 2 ? tip("") : tip("Left mouse to change angle, middle mouse or mousewheel to zoom, right mouse to pan. R to toggle rotation"); + canvas.dataset.hovered = (+canvas.dataset.hovered|0) + 1; }; - $("#_3dpreviewEditor").dialog({ + $("#preview3d").dialog({ title: "3D Preview", resizable: true, position: {my: "left bottom", at: "left+10 bottom-20", of: "svg"}, - resizeStop: resize3dpreview, close: close3dPreview + resizeStop: resize3d, close: close3dPreview }); - function resize3dpreview() { - canvas.width = parseFloat(_3dpreviewEditor.style.width); - canvas.height = parseFloat(_3dpreviewEditor.style.height) - 2; - update3dpreview(canvas); + function resize3d() { + canvas.width = parseFloat(preview3d.style.width); + canvas.height = parseFloat(preview3d.style.height) - 2; + update3dPreview(canvas); } function close3dPreview() { - stop3dpreview(); + stop3d(); canvas.remove(); } } diff --git a/modules/ui/layers.js b/modules/ui/layers.js index 753d40f9..05088b07 100644 --- a/modules/ui/layers.js +++ b/modules/ui/layers.js @@ -33,7 +33,8 @@ function getDefaultPresets() { "religions": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleReligions", "toggleRivers", "toggleRoutes", "toggleScaleBar"], "provinces": ["toggleBorders", "toggleIcons", "toggleProvinces", "toggleRivers", "toggleScaleBar"], "biomes": ["toggleBiomes", "toggleRivers", "toggleScaleBar"], - "heightmap": ["toggleHeight", "toggleRivers", "toggleScaleBar"], + "heightmap": ["toggleHeight", "toggleRivers"], + "physical": ["toggleCoordinates", "toggleHeight", "toggleRivers", "toggleScaleBar"], "poi": ["toggleBorders", "toggleHeight", "toggleIcons", "toggleMarkers", "toggleRivers", "toggleRoutes", "toggleScaleBar"], "landmass": ["toggleScaleBar"] } @@ -70,6 +71,7 @@ function changePreset(preset) { const isDefault = getDefaultPresets()[preset]; removePresetButton.style.display = isDefault ? "none" : "inline-block"; savePresetButton.style.display = "none"; + if (document.getElementById("canvas3d")) setTimeout(update3d, 300); } function savePreset() { @@ -113,6 +115,11 @@ function getCurrentPreset() { savePresetButton.style.display = "inline-block"; } +// update 3d view is layer is toggled +document.getElementById("mapLayers").addEventListener("click", () => { + if (document.getElementById("canvas3d")) setTimeout(update3d, 300); +}); + function toggleHeight(event) { if (!terrs.selectAll("*").size()) { turnButtonOn("toggleHeight"); diff --git a/modules/ui/options.js b/modules/ui/options.js index 7e3922c0..2d940857 100644 --- a/modules/ui/options.js +++ b/modules/ui/options.js @@ -348,6 +348,7 @@ document.getElementById("sticked").addEventListener("click", function(event) { else if (id === "saveMap") saveMap(); else if (id === "saveSVG") saveSVG(); else if (id === "savePNG") savePNG(); + else if (id === "saveJPEG") saveJPEG(); else if (id === "saveGeo") saveGeoJSON(); else if (id === "saveDropbox") saveDropbox(); if (id === "quickSave" || id === "saveMap" || id === "saveSVG" || id === "savePNG" || id === "saveGeo" || id === "saveDropbox") toggleSavePane(); @@ -423,3 +424,46 @@ document.getElementById("mapToLoad").addEventListener("change", function() { closeDialogs(); uploadMap(fileToLoad); }); + +// View mode +viewMode.addEventListener("click", changeViewMode); +function changeViewMode(event) { + if (event.target.tagName !== "BUTTON") return; + const button = event.target; + + enterStandardView(); + if (button.classList.contains("pressed")) { + button.classList.remove("pressed"); + viewStandard.classList.add("pressed"); + } else { + viewMode.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed")); + button.classList.add("pressed"); + if (button.id !== "viewStandard") enter3dView(button.id); + } +} + +function enterStandardView() { + if (!document.getElementById("canvas3d")) return; + document.getElementById("canvas3d").remove(); + stop3d(); +} + +async function enter3dView(type) { + const canvas = document.createElement("canvas"); + canvas.id = "canvas3d"; + canvas.style.display = "block"; + canvas.width = svgWidth; + canvas.height = svgHeight; + canvas.style.position = "absolute"; + canvas.style.display = "none"; + canvas.dataset.type = type; + const started = type === "viewGlobe" ? await startGlobe(canvas) : await start3d(canvas); + if (!started) return; + canvas.style.display = "block"; + document.body.insertBefore(canvas, optionsContainer); + canvas.onmouseenter = () => { + const help = "Left mouse to change angle, middle mouse / mousewheel to zoom, right mouse to pan.\r\nR to toggle rotation. U to update. S to get a screenshot"; + +canvas.dataset.hovered > 2 ? tip("") : tip(help); + canvas.dataset.hovered = (+canvas.dataset.hovered|0) + 1; + }; +} \ No newline at end of file