This commit is contained in:
Azgaar 2019-10-23 00:35:31 +03:00
parent 93c0c64cbd
commit f6ec0251ec
11 changed files with 449 additions and 168 deletions

View file

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

View file

@ -18,7 +18,7 @@
<link rel="canonical" href="https://azgaar.github.io/Fantasy-Map-Generator/">
<style type="text/css">
body {margin: 0; font-size: 11px;}
body {margin: 0; font-size: 11px; overflow: hidden;}
#map {position: absolute;}
#initial {fill: none; stroke: black; pointer-events: none;}
#init-rose {animation: 20s infinite spin; opacity: .7; transform-origin: center;}
@ -34,11 +34,11 @@
#loading-text span:nth-child(3) {animation-delay: 2s;}
@keyframes blink {0% {opacity: 0;} 20% {opacity: 1;} 100% {opacity: .1;}}
</style>
<link rel="preload" href="index.css?version=1.1.09" as="style" onload="this.onload=null; this.rel='stylesheet'">
<link rel="preload" href="icons.css?version=1.1.09" as="style" onload="this.onload=null; this.rel='stylesheet'">
<link rel="preload" href="libs/jquery-ui.css" as="style" onload="this.onload=null; this.rel='stylesheet'">
<link rel="stylesheet" href="index.css?version=1.1.09">
<link rel="stylesheet" href="icons.css?version=1.1.09">
<link rel="preload" href="index.css?version=1.2" as="style">
<link rel="preload" href="icons.css?version=1.2" as="style">
<link rel="preload" href="libs/jquery-ui.css" as="style">
<link rel="stylesheet" href="index.css?version=1.2">
<link rel="stylesheet" href="icons.css?version=1.2">
<link rel="stylesheet" href="libs/jquery-ui.css">
</head>
@ -899,7 +899,7 @@
<div id="loading">
<div id="title_name">Azgaar's</div>
<div id="title">Fantasy Map Generator</div>
<div id="version">v. 1.1</div>
<div id="version">v. 1.2</div>
<p id="loading-text">LOADING<span>.</span><span>.</span><span>.</span></p>
</div>
@ -931,6 +931,7 @@
<option value="provinces">Provinces map</option>
<option value="biomes">Biomes map</option>
<option value="heightmap">Heightmap</option>
<option value="physical">Physical map</option>
<option value="poi">Places of interest</option>
<option value="landmass">Pure landmass</option>
<option hidden value="custom">Custom (not saved)</option>
@ -965,6 +966,13 @@
<li id="toggleRulers" data-tip="Rulers: click to toggle, drag to move, click on label to delete. Ctrl + click to edit layer style. Shortcut: = (equal)" class="buttonoff" onclick="toggleRulers(event)">Rulers</li>
<li id="toggleScaleBar" data-tip="Scale Bar: click to toggle, drag to move. Ctrl + click to edit style. Shortcut: - (minus)" onclick="toggleScaleBar(event)" class="solid">Scale Bar</li>
</ul>
<div id="viewMode" data-tip="Set view node">
<p>View mode:</p>
<button data-tip="Standard view mode that allows to edit the map" id="viewStandard" class="pressed">Standard</button>
<button data-tip="Map presentation in 3D scene. Works best for heightmap. Cannot be used for editing" id="view3D">3D scene</button>
<button data-tip="Project map on globe as if it represents whole world. Cannot be used for editing" id="viewGlobe">Globe</button>
</div>
</div>
<div id="styleContent" class="tabcontent">
@ -1745,9 +1753,9 @@
</td>
</tr>
<tr data-tip="Define relative size of a saved png image. Saving big images is slow and may cause a browser crash">
<tr data-tip="Define relative size of a saved png/jpeg image. Saving big images is slow and may cause a browser crash!">
<td></td>
<td>PNG resolution</td>
<td>PNG/JPEG size</td>
<td>
<input id="pngResolutionInput" data-stored="pngResolution" type="range" min=1 max=8 value=5>
</td>
@ -1859,7 +1867,7 @@
<button data-tip="Open template editor" id="applyTemplate" style="display: none">Template Editor</button>
<button data-tip="Open Image Converter" id="convertImage" style="display: none">Image Converter</button>
<button data-tip="Render heightmap data as a small monochrome image" id="heightmapPreview">Heightmap Preview</button>
<button data-tip="View heightmap data in 3D" id="heightmap3DView">3D</button>
<button data-tip="Preview heightmap in 3D scene" id="heightmap3DView">3D</button>
</div>
<div id="customizeOptions">
@ -1920,7 +1928,8 @@
<div id="saveDropdown">
<div id="saveMap" data-tip="Download the map as fully functional .map file to your machine. Shortcut: Ctrl + M">.map</div>
<div id="saveSVG" data-tip="Download the map as vector image (open in browser or Inkscape). Shortcut: Ctrl + S">.svg</div>
<div id="savePNG" data-tip="Download visible part of the map as .png image. Shortcut: Ctrl + P">.png</div>
<div id="savePNG" data-tip="Download visible part of the map as .png (lossless compressed) image. Shortcut: Ctrl + P">.png</div>
<div id="saveJPEG" data-tip="Download visible part of the map as .jpeg (lossy compressed) image. Shortcut: Ctrl + J">.jpeg</div>
<div id="saveGeo" data-tip="Download map data in GeoJSON format. Shortcut: Ctrl + G">.json</div>
<div id="quickSave" data-tip="Save map to browser storage. Shortcut: F6">storage</div>
<!-- <div id="saveDropbox" data-tip="Save fully functional .map file to Dropbox. Shortcut: Ctrl + B">Dropbox</div> -->
@ -2608,8 +2617,6 @@
</div>
<div id="_3dpreviewEditor" class="dialog stable" style="display: none; padding: 0px"></div>
<div id="statesEditor" class="dialog stable" style="display: none">
<div id="statesHeader" class="header">
<div style="left:1.4em" data-tip="Click to sort by state name" class="sortable alphabetically" data-sortby="name">State&nbsp;</div>
@ -2800,6 +2807,18 @@
<div style="left:12.4em" data-tip="Click to sort by diplomatical relations" class="sortable alphabetically" data-sortby="relations">Relations&nbsp;</div>
</div>
<div id="diplomacySelect">
<div data-tip="Ally means states formed a defensive pact and will protect each other in case of third party aggression">Ally</div>
<div data-tip="State is friendly to anouther state when they share some common interests">Friendly</div>
<div data-tip="Neutral means states relations are neither positive nor negative">Neutral</div>
<div data-tip="Suspicion means shate has a cautious distrust of another state">Suspicion</div>
<div data-tip="Enemies are states at war with each other">Enemy</div>
<div data-tip="Relations are unknown if states do not have enought information about each other">Unknown</div>
<div data-tip="Rivalry is a state of competing for dominance in the region">Rival</div>
<div data-tip="Vassal is a state having obligation to its suzerain">Vassal</div>
<div data-tip="Suzerain is a state having some control over its vassals">Suzerain</div>
</div>
<div id="diplomacyBodySection" class="table"></div>
<div id="diplomacyBottom" style="margin-top: .1em">
@ -3219,6 +3238,8 @@
</div>
</div>
<div id="preview3d" class="dialog stable" style="display: none; padding: 0px"></div>
<div id="alert" style="display: none" class="dialog">
<p id="alertMessage">Warning!</p>
</div>
@ -3296,7 +3317,7 @@
<script defer src="modules/ui/diplomacy-editor.js"></script>
<script defer src="modules/ui/zones-editor.js"></script>
<script defer src="modules/ui/editors.js"></script>
<script defer src="modules/ui/3dpreview.js"></script>
<script defer src="modules/ui/3d.js"></script>
<script defer src="libs/quantize.min.js"></script>
<script defer src="libs/jquery.ui.touch-punch.min.js"></script>
</body>

17
main.js
View file

@ -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 <i>.map</i> files will be auto-updated.
<ul>${post}
<li>Lake Editor</li>
<li>Coastline Editor</li>
<li>New lake groups (types)</li>
<li>Culture presets</li>
<li>Provinces, states and burgs charts</li>
<li>Editable religions tree</li>
<li>Data export in geojson format</li>
<li>Map quick save and quick load</li>
<li>Map loading from URL</li>
<li>3d scene</li>
<li>Globe view</li>
</ul>
<p>Join our ${reddit} and ${discord} to ask questions, share maps, discuss the Generator, report bugs and propose new features.</p>
@ -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);

View file

@ -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();
}
}()

226
modules/ui/3d.js Normal file
View file

@ -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);
}

View file

@ -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;
}

View file

@ -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 = `<div class="states Self" data-id=${sel}>
<div data-tip="List below shows relations to ${selName}" style="width: 100%">${selName}</div>
</div>`;
@ -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 += `<div class="states" data-id=${s.i} data-name="${s.fullName}" data-relations="${relation}">
<div data-tip="${tip}. Click to see relations to ${s.name}" style="width:12em">${s.fullName}</div>
<input data-tip="${tip}. Click to see relations to ${s.name}" class="stateColor" type="color" value="${color}" disabled>
<select data-tip="Click to change ${getAdjective(s.name)} relations to ${selName}" class="diplomacyRelations">${getRelations(relation)}</select>
<div data-tip="${tipSelect}" style="width:12em">${s.fullName}</div>
<svg data-tip="${tipChange}" width=".9em" height=".9em" style="margin-bottom:-1px" class="changeRelations">
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="zoneFill" style="pointer-events: none"></rect>
</svg>
<input data-tip="${tipChange}" class="changeRelations diplomacyRelations" value="${relation}" readonly/>
</div>`;
}
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 += `<option ${relations === s ? "selected" : ""} value="${s}">${s}</option>`);
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;

View file

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

View file

@ -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();
}
}

View file

@ -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");

View file

@ -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\n<b>R</b> to toggle rotation. <b>U</b> to update. <b>S</b> to get a screenshot";
+canvas.dataset.hovered > 2 ? tip("") : tip(help);
canvas.dataset.hovered = (+canvas.dataset.hovered|0) + 1;
};
}