From 30ab1217a81d197ad1c711265c60acec0bc05a7b Mon Sep 17 00:00:00 2001 From: Rayzeq Date: Fri, 25 Jun 2021 00:15:45 +0200 Subject: [PATCH] Add states labels + Replace canvas by svg --- modules/save.js | 1 + modules/ui/3d.js | 254 +++++++++++++++++++++++++++-------------------- 2 files changed, 149 insertions(+), 106 deletions(-) diff --git a/modules/save.js b/modules/save.js index 4066532e..138928e1 100644 --- a/modules/save.js +++ b/modules/save.js @@ -85,6 +85,7 @@ async function getMapURL(type, options=[]) { if (isFirefox && type === "mesh") clone.select("#oceanPattern").remove(); if (options.includes("globe")) clone.select("#scaleBar").remove(); if (options.includes("noLabels")) { + clone.select("#labels #states").remove(); clone.select("#labels #burgLabels").remove(); clone.select("#icons #burgIcons").remove(); } diff --git a/modules/ui/3d.js b/modules/ui/3d.js index 2ca03d3a..c80a63cc 100644 --- a/modules/ui/3d.js +++ b/modules/ui/3d.js @@ -12,7 +12,8 @@ const options = {scale: 50, lightness: .7, shadow: .5, sun: {x: 100, y: 600, z: let Renderer, scene, camera, controls, animationFrame, material, texture, geometry, mesh, ambientLight, spotLight, waterPlane, waterMaterial, waterMesh, objexporter; -let drawCtx = document.createElement('canvas').getContext('2d'); +const drawSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg"); +document.body.appendChild(drawSVG); let textMeshs = [], iconMeshs = []; // initiate 3d scene @@ -44,17 +45,7 @@ const stop = function() { material.dispose(); if (waterPlane) waterPlane.dispose(); if (waterMaterial) waterMaterial.dispose(); - for (const mesh of textMeshs) { - mesh.material.map.dispose(); - mesh.material.dispose(); - mesh.geometry.dispose(); - scene.remove(mesh); - } - for (const mesh of iconMeshs) { - mesh.material.dispose(); - mesh.geometry.dispose(); - scene.remove(mesh); - } + deleteLabels(); Renderer.renderLists.dispose(); // is it required? Renderer.dispose(); @@ -84,7 +75,6 @@ const setScale = function(scale) { } for (const mesh of iconMeshs) { mesh.position.y = getMeshHeight(findGridCell(mesh.base_x, mesh.base_y)); - } geometry.vertices.forEach((v, i) => v.z = getMeshHeight(i)); geometry.verticesNeedUpdate = true; @@ -125,8 +115,10 @@ const toggleSky = function() { } const toggleLabels = function() { + if (options.labels3d) deleteLabels(); else createLabels(); + options.labels3d = !options.labels3d; - redraw(); + update(); } const setColors = function(sky, water) { @@ -198,32 +190,149 @@ async function newMesh(canvas) { return true; } -function createTextMesh(text, font, size, color) { - drawCtx.clearRect(0, 0, drawCtx.canvas.width, drawCtx.canvas.height); - drawCtx.font = "50px " + font; +function svg2base64(svg) { + const str_xml = new XMLSerializer().serializeToString(svg); + return 'data:image/svg+xml;base64,' + btoa(str_xml); +} - drawCtx.canvas.width = drawCtx.measureText(text).width; - drawCtx.canvas.height = 50 + 5; - drawCtx.font = "50px " + font; +function svg2mesh(svg, sx=1, sy=1) { + svg.removeAttribute("viewBox"); + const bbox = svg.getBBox(); + svg.setAttribute("viewBox", [bbox.x, bbox.y, bbox.width, bbox.height].join(" ")); + svg.setAttribute("width", bbox.width); + svg.setAttribute("height", bbox.height); - drawCtx.fillStyle = color; - drawCtx.fillText(text, 0, 50); - - // canvas contents will be used for a texture - const text_texture = new THREE.TextureLoader().load(drawCtx.canvas.toDataURL()); - text_texture.minFilter = THREE.LinearFilter - text_texture.needsUpdate = true; - - const text_material = new THREE.MeshBasicMaterial({map: text_texture/*, side:THREE.DoubleSide*/, depthWrite: false}); - text_material.transparent = true; + const texture = new THREE.TextureLoader().load(svg2base64(svg)); + texture.minFilter = THREE.LinearFilter; // remove `texture has been resized` warning - const text_mesh = new THREE.Mesh( - new THREE.PlaneGeometry(drawCtx.canvas.width*(size/100), drawCtx.canvas.height*(size/100)), - text_material + const material = new THREE.MeshBasicMaterial({map: texture, side:THREE.DoubleSide, depthWrite: false}); + material.transparent = true; + + const mesh = new THREE.Mesh( + new THREE.PlaneGeometry(1, 1), + material ); - text_mesh.renderOrder = 1; + mesh.scale.x = bbox.width * (sx / 100); + mesh.scale.y = bbox.height * (sy / 100); + mesh.renderOrder = 1; - return text_mesh + return mesh; +} + +function createStateText(font, size, color, label) { + drawSVG.innerHTML = ""; + drawSVG.appendChild(label.cloneNode(true)); + drawSVG.children[0].innerHTML = ``; + drawSVG.children[0].appendChild(svg.select(label.childNodes[0].href.baseVal).node().cloneNode(true)); // href of path in defs + drawSVG.children[1].setAttribute("transform", `scale(${5} ${5})`) + drawSVG.children[1].setAttribute('font-family', font); + drawSVG.children[1].setAttribute('font-size', size); + drawSVG.children[1].setAttribute('fill', color); + + const mesh = svg2mesh(drawSVG, 20, 20); + mesh.rotation.set(THREE.Math.degToRad(-90), 0, 0); + + return mesh; +} + +function createBurgText(text, font, size, color) { + drawSVG.innerHTML = `${text} +`; + return svg2mesh(drawSVG, 7*2, 7*2); +} + +function get3dCoords(base_x, base_y) { + const x = base_x - svg.attr("width")/2; + const y = getMeshHeight(findGridCell(base_x, base_y)); // work better than getMeshHeight(burg.cell) but I don't know why + const z = base_y - svg.attr("height")/2; + return [x, y, z]; +} + +function createLabels() { + // Burg labels + const cities = svg.select("#viewbox #labels #burgLabels #cities"); + const towns = svg.select('#viewbox #labels #burgLabels #towns'); + const cities_icons = svg.select('#viewbox #icons #burgIcons #cities'); + const towns_icons = svg.select('#viewbox #icons #burgIcons #towns'); + + for (const burg of pack.burgs) { + const [x, y, z] = get3dCoords(burg.x, burg.y) + + if(layerIsOn("toggleLabels")) { + if (burg.capital) { + var text_mesh = createBurgText(burg.name, cities.attr('font-family'), cities.attr('font-size'), cities.attr('fill')); // cities.attr('font-size') + } else { + var text_mesh = createBurgText(burg.name, towns.attr('font-family'), towns.attr('font-size'), towns.attr('fill')); // towns.attr('font-size') + } + + if (burg.capital) { + text_mesh.position.set(x, y + 15, z); + text_mesh.base_height = 15; + text_mesh.animate = function () { + this.rotation.copy(camera.rotation); + } + } else { + text_mesh.position.set(x, y + 5, z); + text_mesh.base_height = 5; + text_mesh.animate = function () { + if(this.position.distanceTo(camera.position) > 200) { + this.visible = false; + } else { + this.visible = true; + this.rotation.copy(camera.rotation); + } + } + } + + text_mesh.base_x = burg.x; + text_mesh.base_y = burg.y; + + textMeshs.push(text_mesh); + scene.add(text_mesh); + } + + // Icon + if(layerIsOn("toggleIcons")) { + const icon_material = new THREE.MeshBasicMaterial({color: burg.capital ? cities_icons.attr('fill') : towns_icons.attr('fill')}); + const icon_mesh = new THREE.Mesh( + new THREE.SphereGeometry((burg.capital ? cities_icons.attr("size") : towns_icons.attr("size")) * 2, 16, 16), + icon_material + ); + icon_mesh.position.set(x, y, z) + icon_mesh.base_x = burg.x + icon_mesh.base_y = burg.y + + iconMeshs.push(icon_mesh); + scene.add(icon_mesh); + } + } + + // State labels + const state_labels = svg.select("#viewbox #labels #states") + for (const label of state_labels.node().children) { + const text_mesh = createStateText(state_labels.attr("font-family"), state_labels.attr("font-size"), state_labels.attr("fill"), label); + const id = label.id.match(/\d+$/); + const pos = pack.states[id].pole + const [x, y, z] = get3dCoords(pos[0], pos[1]) + text_mesh.position.set(x, y + 25, z); + textMeshs.push(text_mesh) + scene.add(text_mesh); + } +} + +function deleteLabels() { + for (const mesh of textMeshs) { + mesh.material.map.dispose(); + mesh.material.dispose(); + mesh.geometry.dispose(); + scene.remove(mesh); + } + + for (const mesh of iconMeshs) { + mesh.material.dispose(); + mesh.geometry.dispose(); + scene.remove(mesh); + } } // create a mesh from pixel data @@ -255,79 +364,12 @@ async function createMesh(width, height, segmentsX, segmentsY) { mesh.receiveShadow = true; scene.add(mesh); - for (const mesh of textMeshs) { - mesh.material.map.dispose(); - mesh.material.dispose(); - mesh.geometry.dispose(); - scene.remove(mesh); - } - textMeshs = [] - - for (const mesh of iconMeshs) { - mesh.material.dispose(); - mesh.geometry.dispose(); - scene.remove(mesh); - } - iconMeshs = [] + deleteLabels(); + textMeshs = []; + iconMeshs = []; if (options.labels3d) { - const cities = svg.select("#viewbox #labels #burgLabels #cities"); - const towns = svg.select('#viewbox #labels #burgLabels #towns'); - const cities_icons = svg.select('#viewbox #icons #burgIcons #cities'); - const towns_icons = svg.select('#viewbox #icons #burgIcons #towns'); - - for (const burg of pack.burgs) { - const x = burg.x - svg.attr("width")/2; - const y = getMeshHeight(findGridCell(burg.x, burg.y)); // work better than getMeshHeight(burg.cell) but I don't know why - const z = burg.y - svg.attr("height")/2; - - if(layerIsOn("toggleLabels")) { - if (burg.capital) { - var text_mesh = createTextMesh(burg.name, cities.attr('font-family'), 25, cities.attr('fill')); // cities.data('size') - } else { - var text_mesh = createTextMesh(burg.name, towns.attr('font-family'), 7, towns.attr('fill')); // towns.data('size') - } - - if (burg.capital) { - text_mesh.position.set(x, y + 15, z); - text_mesh.base_height = 15 - text_mesh.animate = function () { - this.rotation.copy(camera.rotation); - } - } else { - text_mesh.position.set(x, y + 5, z); - text_mesh.base_height = 5 - text_mesh.animate = function () { - if(this.position.distanceTo(camera.position) > 200) { - this.visible = false; - } else { - this.visible = true; - this.rotation.copy(camera.rotation); - } - } - } - text_mesh.base_x = burg.x - text_mesh.base_y = burg.y - - textMeshs.push(text_mesh); - scene.add(text_mesh); - } - - // Icon - if(layerIsOn("toggleIcons")) { - const icon_material = new THREE.MeshBasicMaterial({color: burg.capital ? cities_icons.attr('fill') : towns_icons.attr('fill')}); - const icon_mesh = new THREE.Mesh( - new THREE.SphereGeometry((burg.capital ? cities_icons.attr("size") : towns_icons.attr("size")) * 2, 16, 16), - icon_material - ); - icon_mesh.position.set(x, y, z) - icon_mesh.base_x = burg.x - icon_mesh.base_y = burg.y - - iconMeshs.push(icon_mesh); - scene.add(icon_mesh); - } - } + createLabels(); } }