diff --git a/icons.css b/icons.css index 61b3d935..6259bfe2 100644 --- a/icons.css +++ b/icons.css @@ -209,3 +209,4 @@ .icon-smooth:before {font-weight: bold; content: '∼'; } .icon-disrupt:before {font-weight: bold; content: '෴'; } .icon-if:before {font-style: italic; font-weight: bold; content: 'if'; } +.icon-arc:before {font-weight: bold; font-size: 1.2em; content: '⌒'; } diff --git a/index.css b/index.css index e52267a2..2f2cfaa6 100644 --- a/index.css +++ b/index.css @@ -47,7 +47,7 @@ button, select, a { } #cults { - stroke-width: 2; + stroke-width: 4; mask: url(#shape); mask-mode: alpha; pointer-events: none; @@ -75,6 +75,7 @@ button, select, a { #regions { stroke-width: 0; + stroke: none; fill-rule: evenodd; stroke-linejoin: round; mask: url(#shape); @@ -152,55 +153,61 @@ button, select, a { font-size: 9px; } -.editTrigger { - display: none; - position: relative; - width: 60px; +#labelEditor div { + display: inline-block; } -.editTrigger[type="number"] { - width: 44px; - height: 14px; -} - -.editTrigger[type="range"] { - width: 132px; +#labelEditor span { cursor: pointer; } -#editGroupSelect { +#labelGroupSelect { width: 146px; + height: 20px; } -#editGroupInput { +#labelGroupInput { display: none; - width: 161px; + width: 142px; } -#editText { +#labelText { width: 160px; } -#editFontSelect { +#labelFontSelect { width: 129px; } -#editFontInput { +#labelFontInput { width: 125px; } -input[type="color"].editColor { +#textPath { + stroke: #3e3e4b; + stroke-width: .5; + fill: none; +} + +#textPathControl { + stroke: #3e3e4b; + stroke-width: .5; + fill: #ffff00; + cursor: row-resize; +} + +div > input[type="color"].editColor { height: 18px; width: 46px; padding: 0; cursor: pointer; } -input[type="range"].editRange { +div > input[type="range"].editRange { width: 80px; } -input[type="number"].editNumber { +div > input[type="number"].editNumber { width: 44px; } @@ -234,13 +241,13 @@ input[type="number"].editNumber { #labels { text-anchor: middle; - dominant-baseline: alphabetic; + dominant-baseline: central; text-shadow: 0 0 4px white; cursor: pointer; } -#countries { - dominant-baseline: central; +#burgLabels { + dominant-baseline: alphabetic; } #routeLength { diff --git a/index.html b/index.html index b0c0c789..29e19765 100644 --- a/index.html +++ b/index.html @@ -30,10 +30,9 @@ - - - + + @@ -71,6 +70,13 @@ + + + + + + + @@ -188,18 +194,18 @@

Displayed layers. Drag to move, click to toggle

    -
  • Ocean
  • -
  • Heightmap
  • +
  • Ocean
  • +
  • Heightmap
  • Grid
  • Overlay
  • Cultures
  • Routes
  • Rivers
  • -
  • Countries
  • +
  • Countries
  • Borders
  • -
  • Relief
  • -
  • Labels
  • -
  • Icons
  • +
  • Relief
  • +
  • Labels
  • +
  • Icons
@@ -239,12 +245,13 @@

Ensure Overlay layer is active (see Layout tab)

Overlay type:
-
Size: - 5 +
Size: + 10

Elements: @@ -285,6 +292,7 @@
Radius: Stroke:
+

Opacity: 1 @@ -299,6 +307,7 @@ + @@ -315,7 +324,7 @@
Label groups: - +
@@ -452,7 +461,7 @@ Precipitation - + 15 @@ -551,7 +560,7 @@
- +
+ - + @@ -1144,5 +1171,5 @@
- + diff --git a/libs/d3-hexbin.v0.2.min.js b/libs/d3-hexbin.v0.2.min.js deleted file mode 100644 index f15da735..00000000 --- a/libs/d3-hexbin.v0.2.min.js +++ /dev/null @@ -1,2 +0,0 @@ -// https://github.com/d3/d3-hexbin Version 0.2.2. Copyright 2017 Mike Bostock. -!function(n,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(n.d3=n.d3||{})}(this,function(n){"use strict";function t(n){return n[0]}function r(n){return n[1]}var e=Math.PI/3,u=[0,e,2*e,3*e,4*e,5*e],o=function(){function n(n){var t,r={},e=[],u=n.length;for(t=0;t1){var v=i-h,M=h+(ig*g+m*m&&(h=M+(1&s?1:-1)/2,s=x)}var y=h+"-"+s,j=r[y];j?j.push(o):(e.push(j=r[y]=[o]),j.x=(h+(1&s)/2)*a,j.y=s*f)}return e}function o(n){var t=0,r=0;return u.map(function(e){var u=Math.sin(e)*n,o=-Math.cos(e)*n,i=u-t,a=o-r;return t=u,r=o,[i,a]})}var i,a,f,c=0,s=0,h=1,l=1,d=t,p=r;return n.hexagon=function(n){return"m"+o(null==n?i:+n).join("l")+"z"},n.centers=function(){for(var n=[],t=Math.round(s/f),r=Math.round(c/a),e=t*f;e 0.9) {templateInput.value = "Volcano";} - else if (rnd > 0.7) {templateInput.value = "High Island";} - else if (rnd > 0.5) {templateInput.value = "Low Island";} + if (rnd > 0.95) {templateInput.value = "Volcano";} + else if (rnd > 0.75) {templateInput.value = "High Island";} + else if (rnd > 0.55) {templateInput.value = "Low Island";} else if (rnd > 0.35) {templateInput.value = "Continents";} - else if (rnd > 0.05) {templateInput.value = "Archipelago";} - else if (rnd > 0.03) {templateInput.value = "Mainland";} + else if (rnd > 0.15) {templateInput.value = "Archipelago";} + else if (rnd > 0.10) {templateInput.value = "Mainland";} else if (rnd > 0.01) {templateInput.value = "Peninsulas";} else {templateInput.value = "Atoll";} } @@ -1658,6 +1653,10 @@ function fantasyMap() { if (pp === "burgIcons" || pp === "burgLabels") { tip('Use dragging for fine-tuning only, to move burg to a different cell use "Relocate" button'); } + if (pp === "labels") { + // also transform curve control circle + debug.select("circle").attr("transform", transform); + } }); d3.event.on("end", function() { @@ -2222,6 +2221,385 @@ function fantasyMap() { console.timeEnd('addLakes'); } + function editLabel() { + if (customization) return; + + unselect(); + closeDialogs("#labelEditor, .stable"); + elSelected = d3.select(this).call(d3.drag().on("start", elementDrag)).classed("draggable", true); + + // update group parameters + let group = d3.select(this.parentNode); + updateGroupOptions(); + labelGroupSelect.value = group.attr("id"); + labelFontSelect.value = fonts.indexOf(group.attr("data-font")); + labelSize.value = group.attr("data-size"); + labelColor.value = toHEX(group.attr("fill")); + labelOpacity.value = group.attr("opacity"); + labelText.value = elSelected.text(); + const tr = parseTransform(elSelected.attr("transform")); + labelAngle.value = tr[2]; + labelAngleValue.innerHTML = Math.abs(+tr[2]) + "°"; + + $("#labelEditor").dialog({ + title: "Edit Label: " + labelText.value, + minHeight: 30, width: "auto", maxWidth: 275, resizable: false, + position: {my: "center top+10", at: "bottom", of: this}, + close: unselect + }); + + if (modules.editLabel) return; + modules.editLabel = true; + + loadDefaultFonts(); + + function updateGroupOptions() { + labelGroupSelect.innerHTML = ""; + labels.selectAll("g:not(#burgLabels)").each(function(d) { + if (this.parentNode.id === "burgLabels") return; + let id = d3.select(this).attr("id"); + let opt = document.createElement("option"); + opt.value = opt.innerHTML = id; + labelGroupSelect.add(opt); + }); + } + + $("#labelGroupButton").click(function() { + $("#labelEditor > button").not(this).toggle(); + $("#labelGroupButtons").toggle(); + }); + + // on group change + document.getElementById("labelGroupSelect").addEventListener("change", function() { + document.getElementById(this.value).appendChild(elSelected.remove().node()); + }); + + // toggle inputs to declare a new group + document.getElementById("labelGroupNew").addEventListener("click", function() { + if ($("#labelGroupInput").css("display") === "none") { + $("#labelGroupInput").css("display", "inline-block"); + $("#labelGroupSelect").css("display", "none"); + labelGroupInput.focus(); + } else { + $("#labelGroupSelect").css("display", "inline-block"); + $("#labelGroupInput").css("display", "none"); + } + }); + + // toggle inputs to select a group + document.getElementById("labelExternalFont").addEventListener("click", function() { + if ($("#labelFontInput").css("display") === "none") { + $("#labelFontInput").css("display", "inline-block"); + $("#labelFontSelect").css("display", "none"); + labelFontInput.focus(); + } else { + $("#labelFontSelect").css("display", "inline-block"); + $("#labelFontInput").css("display", "none"); + } + }); + + // on new group creation + document.getElementById("labelGroupInput").addEventListener("change", function() { + if (!this.value) { + tip("Please provide a valid group name"); + return; + } + let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, ""); + if (Number.isFinite(+group.charAt(0))) group = "g" + group; + // if el with this id exists, add size to id + while (labels.selectAll("#"+group).size()) {group += "_new";} + createNewLabelGroup(group); + }); + + function createNewLabelGroup(g) { + let group = elSelected.node().parentNode.cloneNode(false); + let groupNew = labels.append(f => group).attr("id", g); + groupNew.append(f => elSelected.remove().node()); + updateGroupOptions(); + $("#labelGroupSelect, #labelGroupInput").toggle(); + labelGroupInput.value = ""; + labelGroupSelect.value = g; + updateLabelGroups(); + } + + // remove label group on click + document.getElementById("labelGroupRemove").addEventListener("click", function() { + let group = d3.select(elSelected.node().parentNode); + let id = group.attr("id"); + let count = group.selectAll("text").size(); + // remove group with < 2 label without ask + if (count < 2) { + removeAllLabelsInGroup(id); + $("#labelEditor").dialog("close"); + return; + } + alertMessage.innerHTML = "Are you sure you want to remove all labels (" + count + ") of that group?"; + $("#alert").dialog({resizable: false, title: "Remove label group", + buttons: { + Remove: function() { + $(this).dialog("close"); + removeAllLabelsInGroup(id); + $("#labelEditor").dialog("close"); + }, + Cancel: function() {$(this).dialog("close");} + } + }); + }); + + $("#labelTextButton").click(function() { + $("#labelEditor > button").not(this).toggle(); + $("#labelTextButtons").toggle(); + }); + + // on label text change + document.getElementById("labelText").addEventListener("input", function() { + if (!this.value) { + tip("Name should not be blank, set opacity to 0 to hide label or click remove button to delete"); + return; + } + // change Label text + if (elSelected.select("textPath").size()) elSelected.select("textPath").text(this.value); + else elSelected.text(this.value); + $("div[aria-describedby='labelEditor'] .ui-dialog-title").text("Edit Label: " + this.value); + // check if label is a country name + let id = elSelected.attr("id") || ""; + if (id.includes("regionLabel")) { + let state = +elSelected.attr("id").slice(11); + states[state].name = this.value; + } + }); + + // generate a random country name + document.getElementById("labelTextRandom").addEventListener("click", function() { + let name = elSelected.text(); + let id = elSelected.attr("id") || ""; + if (id.includes("regionLabel")) { + // label is a country name + let state = +elSelected.attr("id").slice(11); + name = generateStateName(state.i); + states[state].name = name; + } else { + // label is not a country name, get culture closest to BBox centre + let c = elSelected.node().getBBox(); + let closest = cultureTree.find((c.x + c.width / 2), (c.y + c.height / 2)); + let culture = cultureTree.data().indexOf(closest) || 0; + name = generateName(culture); + } + labelText.value = name; + $("div[aria-describedby='labelEditor'] .ui-dialog-title").text("Edit Label: " + name); + // change Label text + if (elSelected.select("textPath").size()) elSelected.select("textPath").text(name); + else elSelected.text(name); + }); + + $("#labelFontButton").click(function() { + $("#labelEditor > button").not(this).toggle(); + $("#labelFontButtons").toggle(); + }); + + // on label font change + document.getElementById("labelFontSelect").addEventListener("change", function() { + let group = elSelected.node().parentNode; + let font = fonts[this.value].split(':')[0].replace(/\+/g, " "); + group.setAttribute("font-family", font) + group.setAttribute("data-font", fonts[this.value]); + }); + + // on adding custom font + document.getElementById("labelFontInput").addEventListener("change", function() { + fetchFonts(this.value).then(fetched => { + if (!fetched) return; + labelExternalFont.click(); + labelFontInput.value = ""; + if (fetched === 1) $("#labelFontSelect").val(fonts.length - 1).change(); + }); + }); + + function fetchFonts(url) { + return new Promise((resolve, reject) => { + if (url === "") { + tip("Use a direct link to any @font-face declaration or just font name to fetch from Google Fonts"); + return; + } + if (url.indexOf("http") === -1) { + url = url.replace(url.charAt(0), url.charAt(0).toUpperCase()).split(" ").join("+"); + url = "https://fonts.googleapis.com/css?family=" + url; + } + const fetched = addFonts(url).then(fetched => { + if (fetched === undefined) { + tip("Cannot fetch font for this value!"); + return; + } + if (fetched === 0) { + tip("Already in the fonts list!"); + return; + } + updateFontOptions(); + if (fetched === 1) { + tip("Font " + fonts[fonts.length - 1] + " is fetched"); + } else if (fetched > 1) { + tip(fetched + " fonts are added to the list"); + } + resolve(fetched); + }); + }) + } + + function addFonts(url) { + $("head").append(''); + return fetch(url) + .then(resp => resp.text()) + .then(text => { + let s = document.createElement('style'); + s.innerHTML = text; + document.head.appendChild(s); + let styleSheet = Array.prototype.filter.call( + document.styleSheets, + sS => sS.ownerNode === s)[0]; + let FontRule = rule => { + let family = rule.style.getPropertyValue('font-family'); + let font = family.replace(/['"]+/g, '').replace(/ /g, "+"); + let weight = rule.style.getPropertyValue('font-weight'); + if (weight !== "400") font += ":" + weight; + if (fonts.indexOf(font) == -1) {fonts.push(font); fetched++}; + }; + let fetched = 0; + for (let r of styleSheet.cssRules) {FontRule(r);} + document.head.removeChild(s); + return fetched; + }) + .catch(function() {return}); + } + + // on label size input + document.getElementById("labelSize").addEventListener("input", function() { + let group = elSelected.node().parentNode; + let size = +this.value; + group.setAttribute("data-size", size); + group.setAttribute("font-size", rn((size + (size / scale)) / 2, 2)) + }); + + $("#labelStyleButton").click(function() { + $("#labelEditor > button").not(this).toggle(); + $("#labelStyleButtons").toggle(); + }); + + // on label fill color input + document.getElementById("labelColor").addEventListener("input", function() { + let group = elSelected.node().parentNode; + group.setAttribute("fill", this.value); + }); + + // on label opacity input + document.getElementById("labelOpacity").addEventListener("input", function() { + let group = elSelected.node().parentNode; + group.setAttribute("opacity", this.value); + }); + + $("#labelAngleButton").click(function() { + $("#labelEditor > button").not(this).toggle(); + $("#labelAngleButtons").toggle(); + }); + + // on label angle input + document.getElementById("labelAngle").addEventListener("input", function() { + const tr = parseTransform(elSelected.attr("transform")); + labelAngleValue.innerHTML = Math.abs(+this.value) + "°"; + const c = elSelected.node().getBBox(); + const angle = +this.value; + const transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)} ${(c.y+c.height/2)})`; + elSelected.attr("transform", transform); + }); + + // display control points to curve label (place on path) + document.getElementById("labelCurve").addEventListener("click", function() { + let c = elSelected.node().getBBox(); + let cx = c.x + c.width / 2, cy = c.y + c.height / 2; + + if (!elSelected.select("textPath").size()) { + let id = elSelected.attr("id"); + let pathId = "#textPath_" + id; + let path = `M${cx-c.width},${cy} q${c.width},0 ${c.width * 2},0`; + let text = elSelected.text(), x = elSelected.attr("x"), y = elSelected.attr("y"); + elSelected.text(null).attr("data-x", x).attr("data-y", y).attr("x", null).attr("y", null); + defs.append("path").attr("id", "textPath_" + id).attr("d", path); + elSelected.append("textPath").attr("href", pathId).attr("startOffset", "50%").text(text); + } + + if (!debug.select("circle").size()) { + debug.append("circle").attr("id", "textPathControl").attr("r", 1.6) + .attr("cx", cx).attr("cy", cy).attr("transform", elSelected.attr("transform") || null) + .call(d3.drag().on("start", textPathControlDrag)); + } + }); + + // drag textPath controle point to curve the label + function textPathControlDrag() { + let textPath = defs.select("#textPath_" + elSelected.attr("id")); + let path = textPath.attr("d").split(" "); + let M = path[0].split(","); + let q = path[1].split(","); // +q[1] to get qy - the only changeble value + let y = d3.event.y; + + d3.event.on("drag", function() { + let dy = d3.event.y - y; + let total = +q[1] + dy * 8; + d3.select(this).attr("cy", d3.event.y); + textPath.attr("d", `${M[0]},${+M[1] - dy} ${q[0]},${total} ${path[2]}`); + }); + } + + // cancel label curvature + document.getElementById("labelCurveCancel").addEventListener("click", function() { + if (!elSelected.select("textPath").size()) return; + let text = elSelected.text(), x = elSelected.attr("data-x"), y = elSelected.attr("data-y"); + elSelected.text(); + elSelected.attr("x", x).attr("y", y).attr("data-x", null).attr("data-y", null).text(text); + defs.select("#textPath_" + elSelected.attr("id")).remove(); + debug.select("circle").remove(); + }); + + // copy label on click + document.getElementById("labelCopy").addEventListener("click", function() { + let group = d3.select(elSelected.node().parentNode); + copy = group.append(f => elSelected.node().cloneNode(true)); + let number = 0, id = 0; + do {id = group.attr("id") + "Label" + number; number++;} while (group.select("#"+id).size()) + copy.attr("id", id).attr("class", null).on("click", editLabel); + let shift = +group.attr("font-size") + 1; + if (copy.select("textPath").size()) { + let path = defs.select("#textPath_" + elSelected.attr("id")).attr("d"); + let textPath = defs.append("path").attr("id", "textPath_" + id); + copy.select("textPath").attr("href", "#textPath_" + id); + let pathArray = path.split(" "); + let x = +pathArray[0].split(",")[0].slice(1); + let y = +pathArray[0].split(",")[1]; + textPath.attr("d", `M${x-shift},${y-shift} ${pathArray[1]} ${pathArray[2]}`);shift + } else { + let x = +elSelected.attr("x") - shift; + let y = +elSelected.attr("y") - shift; + while (group.selectAll("text[x='" + x + "']").size()) {x -= shift; y -= shift;} + copy.attr("x", x).attr("y", y); + } + }); + + // remove label on click + document.getElementById("labelRemoveSingle").addEventListener("click", function() { + alertMessage.innerHTML = "Are you sure you want to remove the label?"; + $("#alert").dialog({resizable: false, title: "Remove label", + buttons: { + Remove: function() { + $(this).dialog("close"); + elSelected.remove(); + defs.select("#textPath_" + elSelected.attr("id")).remove(); + $("#labelEditor").dialog("close"); + }, + Cancel: function() {$(this).dialog("close");} + } + }); + }); + } + function editRiver() { if (customization) return; if (elSelected) { @@ -2404,7 +2782,7 @@ function fantasyMap() { amended.map(function(p) {addRiverPoint(p);}); }); - $("#riverAngle").change(function() { + $("#riverAngle").on("input", function() { const tr = parseTransform(elSelected.attr("transform")); riverAngleValue.innerHTML = Math.abs(+this.value) + "°"; var c = elSelected.node().getBBox(); @@ -2522,22 +2900,6 @@ function fantasyMap() { } - function unselect() { - if (elSelected) { - elSelected.call(d3.drag().on("drag", null)).attr("class", null); - debug.select(".controlPoints").remove(); - viewbox.style("cursor", "default"); - elSelected = null; - } - } - - function parseTransform(string) { - // [translateX,translateY,rotateDeg,rotateX,rotateY,scale] - if (!string) {return [0,0,0,0,0,1];} - var a = string.replace(/[a-z()]/g,"").replace(/[ ]/g,",").split(","); - return [a[0] || 0, a[1] || 0, a[2] || 0, a[3] || 0, a[4] || 0, a[5] || 1]; - } - function editRoute() { if (customization) {return;} if (elSelected) { @@ -2784,7 +3146,7 @@ function fantasyMap() { unselect(); closeDialogs("#iconEditor, .stable"); - elSelected = d3.select(this).call(d3.drag().on("start", elementDrag)); + elSelected = d3.select(this).call(d3.drag().on("start", elementDrag)).classed("draggable", true); // update group parameters const group = d3.select(this.parentNode); @@ -2920,7 +3282,7 @@ function fantasyMap() { unselect(); closeDialogs("#reliefEditor, .stable"); - elSelected = d3.select(this).raise().call(d3.drag().on("start", elementDrag)); + elSelected = d3.select(this).raise().call(d3.drag().on("start", elementDrag)).classed("draggable", true); const group = elSelected.node().parentNode.id; reliefGroup.value = group; @@ -3252,7 +3614,7 @@ function fantasyMap() { group.attr("opacity", +this.value); }); - $("#burgLabelAngle").change(function() { + $("#burgLabelAngle").on("input", function() { const id = +elSelected.attr("data-id"); const el = burgLabels.select("[data-id='"+ id +"']"); const tr = parseTransform(el.attr("transform")); @@ -3466,6 +3828,22 @@ function fantasyMap() { }); } + // clear elSelected variable + function unselect() { + if (!elSelected) return; + elSelected.call(d3.drag().on("drag", null)).attr("class", null); + debug.selectAll("*").remove(); + viewbox.style("cursor", "default"); + elSelected = null; + } + + // transform string to array [translateX,translateY,rotateDeg,rotateX,rotateY,scale] + function parseTransform(string) { + if (!string) {return [0,0,0,0,0,1];} + var a = string.replace(/[a-z()]/g,"").replace(/[ ]/g,",").split(","); + return [a[0] || 0, a[1] || 0, a[2] || 0, a[3] || 0, a[4] || 0, a[5] || 1]; + } + // generic function to move any burg to any group function moveBurgToGroup(id, g) { $("#burgLabels [data-id=" + id + "]").detach().appendTo($("#burgLabels > #"+g)); @@ -4435,13 +4813,17 @@ function fantasyMap() { edges.splice(rem, 1); } path += lineGen(edgesOrdered) + "Z "; - var edgesFormatted = []; - edgesOrdered.map(function(e) {edgesFormatted.push([+e.scX, +e.scY])}); - array[array.length] = edgesFormatted; + array[array.length] = edgesOrdered.map(function(e) {return [+e.scX, +e.scY];}); } var color = states[region].color; - regions.append("path").attr("d", round(path, 1)).attr("fill", color).attr("stroke", "none").attr("class", "region"+region); + regions.append("path").attr("d", round(path, 1)).attr("fill", color).attr("class", "region"+region); array.sort(function(a, b){return b.length - a.length;}); + let capital = states[region].capital; + // add capital cell as a hole + if (!isNaN(capital)) { + const capitalCell = manors[capital].cell; + array.push(polygons[capitalCell]); + } var name = states[region].name; var c = polylabel(array, 1.0); // pole of inaccessibility labels.select("#countries").append("text").attr("id", "regionLabel"+region).attr("x", rn(c[0])).attr("y", rn(c[1])).text(name).on("click", editLabel); @@ -4475,7 +4857,7 @@ function fantasyMap() { path += lineGen(edgesOrdered); } var color = states[region].color; - regions.append("path").attr("d", round(path, 1)).attr("fill", "none").attr("stroke", color).attr("stroke-width", 3).attr("class", "region"+region); + regions.append("path").attr("d", round(path, 1)).attr("fill", "none").attr("stroke", color).attr("stroke-width", 5).attr("class", "region"+region); } function drawBorders(edges, type) { @@ -4789,9 +5171,11 @@ function fantasyMap() { if (overlay.selectAll("*").size() === 0) { var type = styleOverlayType.value; var size = +styleOverlaySize.value; - if (type === "hex") { - var hexbin = d3.hexbin().radius(size).size([svgWidth, svgHeight]); - overlay.append("path").attr("d", round(hexbin.mesh(), 0)); + if (type === "pointyHex" || type === "flatHex") { + let points = getHexGridPoints(size, type); + let hex = "m" + getHex(size, type).slice(0, 4).join("l"); + let d = points.map(function(p) {return "M" + p + hex;}).join(""); + overlay.append("path").attr("d", d); } else if (type === "square") { var x = d3.range(size, svgWidth, size); var y = d3.range(size, svgHeight, size); @@ -4814,6 +5198,35 @@ function fantasyMap() { } } + function getHex(radius, type) { + let x0 = 0, y0 = 0; + let s = type === "pointyHex" ? 0 : Math.PI / -6; + let thirdPi = Math.PI / 3; + let angles = [s, s + thirdPi, s + 2 * thirdPi, s + 3 * thirdPi, s + 4 * thirdPi, s + 5 * thirdPi]; + return angles.map(function(angle) { + var x1 = Math.sin(angle) * radius, + y1 = -Math.cos(angle) * radius, + dx = x1 - x0, + dy = y1 - y0; + x0 = x1, y0 = y1; + return [dx, dy]; + }); + } + + function getHexGridPoints(size, type) { + let points = []; + const rt3 = Math.sqrt(3); + const off = type === "pointyHex" ? rt3 * size / 2 : size * 3 / 2; + const ySpace = type === "pointyHex" ? size * 3 / 2 : rt3 * size / 2; + const xSpace = type === "pointyHex" ? rt3 * size : size * 3; + for (let y = 0, l = 0; y < graphHeight; y += ySpace, l++) { + for (let x = l % 2 ? 0 : off; x < graphWidth; x += xSpace) { + points.push([x, y]); + } + } + return points; + } + // clean data to get rid of redundand info function cleanData() { console.time("cleanData"); @@ -5143,7 +5556,9 @@ function fantasyMap() { .attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC") .attr("font-size", 18).attr("data-size", 18); } - group.append("text").attr("x", x).attr("y", y).text(name).on("click", editLabel); + let number = 0, id = 0; + do {id = group.attr("id") + "Label" + number; number++;} while (group.select("#"+id).size()) + group.append("text").attr("id", id).attr("x", x).attr("y", y).text(name).on("click", editLabel); if (d3.event.shiftKey === false) { $("#addLabel").removeClass("pressed"); @@ -5383,37 +5798,6 @@ function fantasyMap() { s.ruralPopulation = rn(cellsPop, 1); } - function editLabel() { - if (customization) {return;} - closeDialogs("#labelEditor, .stable"); - elSelected = d3.select(this); - elSelected.call(d3.drag().on("start", elementDrag)).classed("draggable", true); - - var group = d3.select(this.parentNode); - updateGroupOptions(); - editGroupSelect.value = group.attr("id"); - editFontSelect.value = fonts.indexOf(group.attr("data-font")); - editSize.value = group.attr("data-size"); - editColor.value = toHEX(group.attr("fill")); - editOpacity.value = group.attr("opacity"); - editText.value = elSelected.text(); - const tr = parseTransform(elSelected.attr("transform")); - editAngle.value = tr[2]; - editAngleValue.innerHTML = Math.abs(+tr[2]) + "°"; - - $("#labelEditor").dialog({ - title: "Edit Label: " + editText.value, - minHeight: 30, width: "auto", maxWidth: 275, resizable: false, - position: {my: "center top+10", at: "bottom", of: this}, - close: unselect - }); - - if (modules.editLabel) {return;} - modules.editLabel = true; - - loadDefaultFonts(); - } - function changeSelectedOnClick() { const point = d3.mouse(this); const index = diagram.find(point[0], point[1]).index; @@ -5459,266 +5843,17 @@ function fantasyMap() { // Update font list for Label and Burg Editors function updateFontOptions() { - editFontSelect.innerHTML = ""; + labelFontSelect.innerHTML = ""; for (let i=0; i < fonts.length; i++) { const opt = document.createElement('option'); opt.value = i; const font = fonts[i].split(':')[0].replace(/\+/g, " "); opt.style.fontFamily = opt.innerHTML = font; - editFontSelect.add(opt); + labelFontSelect.add(opt); } - burgSelectDefaultFont.innerHTML = editFontSelect.innerHTML; + burgSelectDefaultFont.innerHTML = labelFontSelect.innerHTML; } - $("#labelEditor .editButton, #labelEditor .editButtonS").click(function() { - var group = d3.select(elSelected.node().parentNode); - if (this.id == "editRemoveSingle") { - alertMessage.innerHTML = "Are you sure you want to remove the label?"; - $("#alert").dialog({resizable: false, title: "Remove label", - buttons: { - Remove: function() { - $(this).dialog("close"); - elSelected.remove(); - $("#labelEditor").dialog("close"); - }, - Cancel: function() {$(this).dialog("close");} - } - }) - return; - } - if (this.id == "editGroupRemove") { - var count = group.selectAll("text").size(); - if (count < 2) { - group.remove(); - updateLabelGroups(); - $("#labelEditor").dialog("close"); - return; - } - var message = "Are you sure you want to remove all labels (" + count + ") of that group?"; - alertMessage.innerHTML = message; - $("#alert").dialog({resizable: false, title: "Remove labels", - buttons: { - Remove: function() { - $(this).dialog("close"); - group.remove(); - updateLabelGroups(); - $("#labelEditor").dialog("close"); - }, - Cancel: function() {$(this).dialog("close");} - } - }) - return; - } - if (this.id == "editCopy") { - var shift = +group.attr("font-size") + 1; - var xn = +elSelected.attr("x") - shift; - var yn = +elSelected.attr("y") - shift; - while (group.selectAll("text[x='" + xn + "']").size() > 0) {xn -= shift; yn -= shift;} - group.append("text").attr("x", xn).attr("y", yn).text(elSelected.text()) - .attr("transform", elSelected.attr("transform")).on("click", editLabel); - return; - } - if (this.id == "editGroupNew") { - if ($("#editGroupInput").css("display") === "none") { - $("#editGroupInput").css("display", "inline-block"); - $("#editGroupSelect").css("display", "none"); - editGroupInput.focus(); - } else { - $("#editGroupSelect").css("display", "inline-block"); - $("#editGroupInput").css("display", "none"); - } - return; - } - if (this.id == "editExternalFont") { - if ($("#editFontInput").css("display") === "none") { - $("#editFontInput").css("display", "inline-block"); - $("#editFontSelect").css("display", "none"); - editFontInput.focus(); - } else { - $("#editFontSelect").css("display", "inline-block"); - $("#editFontInput").css("display", "none"); - } - return; - } - if (this.id === "editTextRandom") { - var name; - // check if label is country name - if (group.attr("id") === "countries") { - var state = $.grep(states, function(e) {return (e.name === editText.value);})[0]; - name = generateStateName(state.i); - state.name = name; - } else { - // if not, get culture closest to BBox centre - var c = elSelected.node().getBBox(); - var closest = cultureTree.find((c.x + c.width / 2), (c.y + c.height / 2)); - var culture = cultureTree.data().indexOf(closest) || 0; - name = generateName(culture); - } - editText.value = name; - elSelected.text(name); - $("div[aria-describedby='labelEditor'] .ui-dialog-title").text("Edit Label: " + name); - return; - } - $("#labelEditor .editButton").toggle(); - if (this.id == "editGroupButton") { - if ($("#editGroupInput").css("display") !== "none") {$("#editGroupSelect").css("display", "inline-block");} - if ($("#editGroupRemove").css("display") === "none") { - $("#editGroupRemove, #editGroupNew").css("display", "inline-block"); - } else { - $("#editGroupInput, #editGroupRemove, #editGroupNew").css("display", "none"); - } - } - if (this.id == "editFontButton") {$("#editSizeIcon, #editFontSelect, #editSize").toggle();} - if (this.id == "editStyleButton") {$("#editOpacityIcon, #editOpacity, #editShadowIcon, #editShadow").toggle();} - if (this.id == "editAngleButton") {$("#editAngleValue").toggle();} - if (this.id == "editTextButton") {$("#editTextRandom").toggle();} - $(this).show().next().toggle(); - }); - - function updateGroupOptions() { - editGroupSelect.innerHTML = ""; - labels.selectAll("g").each(function(d) { - const id = d3.select(this).attr("id"); - if (id === "burgLabels") return; - if (id === "capitals") return; - if (id === "towns") return; - var opt = document.createElement("option"); - opt.value = opt.innerHTML = id; - editGroupSelect.add(opt); - }); - } - - // on editAngle change - $("#editAngle").on("input", function() { - const tr = parseTransform(elSelected.attr("transform")); - editAngleValue.innerHTML = Math.abs(+this.value) + "°"; - const c = elSelected.node().getBBox(); - const angle = +this.value; - const transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)} ${(c.y+c.height/2)})`; - elSelected.attr("transform", transform); - }); - - $("#editFontInput").change(function() { - fetchFonts(this.value).then(fetched => { - if (!fetched) return; - editExternalFont.click(); - editFontInput.value = ""; - if (fetched === 1) $("#editFontSelect").val(fonts.length - 1).change(); - }); - }); - - function fetchFonts(url) { - return new Promise((resolve, reject) => { - if (url === "") { - tip("Use a direct link to any @font-face declaration or just font name to fetch from Google Fonts"); - return; - } - if (url.indexOf("http") === -1) { - url = url.replace(url.charAt(0), url.charAt(0).toUpperCase()).split(" ").join("+"); - url = "https://fonts.googleapis.com/css?family=" + url; - } - const fetched = addFonts(url).then(fetched => { - if (fetched === undefined) { - tip("Cannot fetch font for this value!"); - return; - } - if (fetched === 0) { - tip("Already in the fonts list!"); - return; - } - updateFontOptions(); - if (fetched === 1) { - tip("Font " + fonts[fonts.length - 1] + " is fetched"); - } else if (fetched > 1) { - tip(fetched + " fonts are added to the list"); - } - resolve(fetched); - }); - }) - } - - function addFonts(url) { - $("head").append(''); - return fetch(url) - .then(resp => resp.text()) - .then(text => { - let s = document.createElement('style'); - s.innerHTML = text; - document.head.appendChild(s); - let styleSheet = Array.prototype.filter.call( - document.styleSheets, - sS => sS.ownerNode === s)[0]; - let FontRule = rule => { - let family = rule.style.getPropertyValue('font-family'); - let font = family.replace(/['"]+/g, '').replace(/ /g, "+"); - let weight = rule.style.getPropertyValue('font-weight'); - if (weight !== "400") font += ":" + weight; - if (fonts.indexOf(font) == -1) {fonts.push(font); fetched++}; - }; - let fetched = 0; - for (let r of styleSheet.cssRules) {FontRule(r);} - document.head.removeChild(s); - return fetched; - }) - .catch(function() {return}); - } - - // on name input - $("#editText").on("input", function() { - $(this).attr("title", $(this).val()); - elSelected.text(editText.value); // change Label text - }); - - // on any Editor input change - $("#labelEditor .editTrigger").change(function() { - if (!elSelected) return; - // check if Group was changed - var group = d3.select(elSelected.node().parentNode); - var groupOld = group.attr("id"); - var groupNew = editGroupSelect.value; - var id = elSelected.attr("id") || ""; - // check if label is a country name - if (id.includes("regionLabel")) { - var state = +elSelected.attr("id").slice(11); - states[state].name = editText.value; - } - if (editGroupInput.value !== "") { - groupNew = editGroupInput.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, ""); - if (Number.isFinite(+groupNew.charAt(0))) groupNew = "g" + groupNew; - const size = svg.selectAll("#"+groupNew).size(); - if (size) groupNew += size; // if el with this id exists, add size to id; - } - if (groupOld !== groupNew) { - var removed = elSelected.remove(); - if (labels.select("#"+groupNew).size() > 0) { - group = labels.select("#"+groupNew); - editFontSelect.value = fonts.indexOf(group.attr("data-font")); - editSize.value = group.attr("data-size"); - editColor.value = toHEX(group.attr("fill")); - editOpacity.value = group.attr("opacity"); - } else { - if (group.selectAll("text").size() === 0) {group.remove();} - group = labels.append("g").attr("id", groupNew); - updateGroupOptions(); - $("#editGroupSelect, #editGroupInput").toggle(); - editGroupInput.value = ""; - } - group.append(function() {return removed.node();}); - editGroupSelect.value = group.attr("id"); - updateLabelGroups(); - } - // update Group attributes - var size = +editSize.value; - group.attr("data-size", size) - .attr("font-size", rn((size + (size / scale)) / 2, 2)) - .attr("fill", editColor.title) - .attr("opacity", editOpacity.value); - if (editFontSelect.value !== "") { - const font = fonts[editFontSelect.value].split(':')[0].replace(/\+/g, " "); - group.attr("font-family", font).attr("data-font", fonts[editFontSelect.value]); - } - }); - // convert RGB color string to HEX without # function toHEX(rgb){ if (rgb.charAt(0) === "#") {return rgb;} @@ -6323,66 +6458,20 @@ function fantasyMap() { console.timeEnd("loadMap"); } - // Poisson-disc sampling for a points - // Source: bl.ocks.org/mbostock/99049112373e12709381; Based on https://www.jasondavies.com/poisson-disc - function poissonDiscSampler(width, height, radius) { - var k = 5, // maximum number of points before rejection - radius2 = radius * radius, - R = 3 * radius2, - cellSize = radius * Math.SQRT1_2, - gridWidth = Math.ceil(width / cellSize), - gridHeight = Math.ceil(height / cellSize), - grid = new Array(gridWidth * gridHeight), - queue = [], - queueSize = 0, - sampleSize = 0; - return function() { - if (!sampleSize) return sample(Math.random() * width, Math.random() * height); - // Pick a random existing sample and remove it from the queue - while (queueSize) { - var i = Math.random() * queueSize | 0, - s = queue[i]; - // Make a new candidate between [radius, 2 * radius] from the existing sample. - for (let j = 0; j < k; ++j) { - var a = 2 * Math.PI * Math.random(), - r = Math.sqrt(Math.random() * R + radius2), - x = s[0] + r * Math.cos(a), - y = s[1] + r * Math.sin(a); - // Reject candidates that are outside the allowed extent, or closer than 2 * radius to any existing sample - if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) return sample(x, y); - } - queue[i] = queue[--queueSize]; - queue.length = queueSize; + // get square grid with some jirrering + function getJitteredGrid(spacing) { + const radius = spacing / 2; // square radius + const jittering = radius * 0.9; // max deviation + const jitter = function() {return Math.random() * 2 * jittering - jittering;} + let points = []; + for (let y = radius; y < graphHeight; y += spacing) { + for (let x = radius; x < graphWidth; x += spacing) { + let xj = rn(x + jitter(), 2); + let yj = rn(y + jitter(), 2); + points.push([xj, yj]); } - }; - function far(x, y) { - var i = x / cellSize | 0, - j = y / cellSize | 0, - i0 = Math.max(i - 2, 0), - j0 = Math.max(j - 2, 0), - i1 = Math.min(i + 3, gridWidth), - j1 = Math.min(j + 3, gridHeight); - for (j = j0; j < j1; ++j) { - var o = j * gridWidth; - for (i = i0; i < i1; ++i) { - if (s = grid[o + i]) { - var s, - dx = s[0] - x, - dy = s[1] - y; - if (dx * dx + dy * dy < radius2) return false; - } - } - } - return true; - } - function sample(x, y) { - var s = [x, y]; - queue.push(s); - grid[gridWidth * (y / cellSize | 0) + (x / cellSize | 0)] = s; - ++sampleSize; - ++queueSize; - return s; } + return points; } // Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys @@ -6684,7 +6773,7 @@ function fantasyMap() { if (isNaN(c)) return; moveBurgToGroup(c, "towns"); }); - labels.select("#countries").selectAll("text").remove(); + removeAllLabelsInGroup("countries"); regions.selectAll("path").remove(); states = []; states.push({i: 0, color: "neutral", capital: "neutral", name: "Neutrals"}); @@ -7438,7 +7527,7 @@ function fantasyMap() { if (i === 0) width = "4px"; var clr = color(1-i/100); var style = "background-color: " + clr + "; width: " + width; - div.append("div").attr("data-color", i/100).attr("style", style); + div.append("div").attr("data-color", i).attr("style", style); } div.selectAll("*").on("touchmove mousemove", showHeight).on("click", assignHeight); } @@ -7488,8 +7577,8 @@ function fantasyMap() { colors.push([r, g, b]); }); var cmap = MMCQ.quantize(colors, count); + heights = new Uint8Array(points.length); polygons.map(function(i, d) { - cells[d].height = undefined; var nearest = cmap.nearest(colors[d]); var rgb = "rgb(" + nearest[0] + ", " + nearest[1] + ", " + nearest[2] + ")"; var hex = toHEX(rgb); @@ -7523,7 +7612,7 @@ function fantasyMap() { if (el.attr("data-height")) { var height = el.attr("data-height"); $("#colorScheme div[data-color='" + height + "']").addClass("hoveredColor"); - $("#colorsSelectValue").text(rn(height * 100)); + $("#colorsSelectValue").text(height); } var color = "#" + d3.select(this).attr("id"); landmass.selectAll("path").classed("selectedCell", 0); @@ -7532,8 +7621,8 @@ function fantasyMap() { } function showHeight() { - var el = d3.select(this); - var height = rn(el.attr("data-color") * 100); + let el = d3.select(this); + let height = el.attr("data-color"); $("#colorsSelectValue").text(height); $("#colorScheme .hoveredColor").removeClass("hoveredColor"); el.classed("hoveredColor", 1); @@ -7542,7 +7631,7 @@ function fantasyMap() { function assignHeight() { var sel = $(".selectedColor")[0]; var height = +d3.select(this).attr("data-color"); - var rgb = color(1-height); + var rgb = color(1 - height / 100); var hex = toHEX(rgb); sel.style.backgroundColor = rgb; sel.setAttribute("data-height", height); @@ -7550,8 +7639,8 @@ function fantasyMap() { sel.id = hex.substr(1); landmass.selectAll(".selectedCell").each(function() { d3.select(this).attr("fill", hex).attr("stroke", hex); - var i = +d3.select(this).attr("data-i"); - cells[i].height = height; + let i = +d3.select(this).attr("data-i"); + heights[i] = height; }); var parent = sel.parentNode; if (parent.id === "colorsUnassigned") { @@ -7583,11 +7672,10 @@ function fantasyMap() { $("#landmass > path, .color-div").remove(); $("#colorsAssigned").fadeIn(); $("#colorsUnassigned").fadeOut(); - var heights = []; - polygons.map(function(i, d) { + polygons.forEach(function(i, d) { var x = rn(i.data[0]), y = rn(i.data[1]); - if (y == svgHeight) {y--;} - if (x == svgWidth) {x--;} + if (y == svgHeight) y--; + if (x == svgWidth) x--; var p = (x + y * svgWidth) * 4; var r = data[p], g = data[p + 1], b = data[p + 2]; var lab = d3.lab("rgb(" + r + ", " + g + ", " + b + ")"); @@ -7596,18 +7684,16 @@ function fantasyMap() { } else { var normalized = rn(normalize(lab.l, 0, 100), 2); } - heights.push(normalized); var rgb = color(1 - normalized); var hex = toHEX(rgb); - cells[d].height = normalized * 100; + heights[d] = normalized * 100; landmass.append("path").attr("d", "M" + i.join("L") + "Z").attr("data-i", d).attr("fill", hex).attr("stroke", hex); }); - heights.sort(function(a, b) {return a - b;}); - var unique = [...new Set(heights)]; - unique.map(function(i) { - var rgb = color(1 - i); + let unique = [...new Set(heights)].sort(); + unique.forEach(function(h) { + var rgb = color(1 - h / 100); var hex = toHEX(rgb); - $("#colorsAssigned").append('
'); + $("#colorsAssigned").append('
'); }); $(".color-div").click(selectColor); } @@ -7634,7 +7720,7 @@ function fantasyMap() { // Clear the map function undraw() { viewbox.selectAll("path, circle, line, text, use, #ruler > g").remove(); - svg.select("#shape").remove(); + defs.selectAll("*").remove(); landmass.select("rect").remove(); cells = [], land = [], riversData = [], manors = [], states = [], features = [], queue = []; } @@ -8722,14 +8808,26 @@ function fantasyMap() { function redrawRegions() { regions.selectAll("*").remove(); borders.selectAll("path").remove(); - labels.select("#countries").selectAll("text").remove(); + removeAllLabelsInGroup("countries"); drawRegions(); } + // remove all labels in group including textPaths + function removeAllLabelsInGroup(group) { + labels.select("#"+group).selectAll("text").each(function() { + defs.select("#textPath_" + this.id).remove(); + this.remove(); + }); + if (group !== "countries") { + labels.select("#"+group).remove(); + updateLabelGroups(); + } + } + // restore keeped region / burgs / cultures data on edit heightmap completion function restoreRegions() { borders.selectAll("path").remove(); - labels.select("#countries").selectAll("text").remove(); + removeAllLabelsInGroup("countries"); manors.map(function(m) { const cell = diagram.find(m.x, m.y).index; if (cells[cell].height < 20) { @@ -8989,7 +9087,7 @@ function fantasyMap() { styleOceanPattern.checked = true; styleOceanLayers.checked = true; - labels.attr("opacity", 1); + labels.attr("opacity", 1).attr("stroke", "#3a3a3a").attr("stroke-width", 0); let size = rn(8 - regionsInput.value / 20); if (size < 3) size = 3; burgLabels.select("#capitals").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", size).attr("data-size", size); @@ -9042,8 +9140,10 @@ function fantasyMap() { if (sel === "overlay") $("#styleOverlay").css("display", "block"); if (sel === "labels") { - $("#styleFill, #styleFontSize").css("display", "inline-block"); + $("#styleFill, #styleStroke, #styleStrokeWidth, #styleFontSize").css("display", "inline-block"); styleFillInput.value = styleFillOutput.value = el.select("g").attr("fill") || "#3e3e4b"; + styleStrokeInput.value = styleStrokeOutput.value = el.select("g").attr("stroke") || "#3a3a3a"; + styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 0; $("#styleLabelGroups").css("display", "inline-block"); updateLabelGroups(); } @@ -9134,7 +9234,7 @@ function fantasyMap() { if (!$("#toggleOverlay").hasClass("buttonoff")) toggleOverlay(); }); - $("#styleOverlaySize").on("input", function() { + $("#styleOverlaySize").on("change", function() { styleOverlaySizeOutput.value = this.value; overlay.selectAll("*").remove(); if (!$("#toggleOverlay").hasClass("buttonoff")) toggleOverlay();