diff --git a/index.html b/index.html index 52b3e7cd..3779d96d 100644 --- a/index.html +++ b/index.html @@ -7801,7 +7801,7 @@ - + @@ -7820,7 +7820,7 @@ - + diff --git a/modules/dynamic/editors/cultures-editor.js b/modules/dynamic/editors/cultures-editor.js index 13611263..07b58cae 100644 --- a/modules/dynamic/editors/cultures-editor.js +++ b/modules/dynamic/editors/cultures-editor.js @@ -293,7 +293,7 @@ function getShapeOptions(selectShape, selected) { return ``; } -function cultureHighlightOn(event) { +const cultureHighlightOn = debounce(event => { const cultureId = Number(event.id || event.target.dataset.id); if (!layerIsOn("toggleCultures")) return; @@ -312,7 +312,7 @@ function cultureHighlightOn(event) { .transition(animate) .attr("r", 8) .attr("stroke", "#d0240f"); -} +}, 200); function cultureHighlightOff(event) { const cultureId = Number(event.id || event.target.dataset.id); diff --git a/modules/dynamic/editors/religions-editor.js b/modules/dynamic/editors/religions-editor.js index 833fe6df..381212d6 100644 --- a/modules/dynamic/editors/religions-editor.js +++ b/modules/dynamic/editors/religions-editor.js @@ -258,7 +258,7 @@ function getTypeOptions(type) { return options; } -function religionHighlightOn(event) { +const religionHighlightOn = debounce(event => { const religionId = Number(event.id || event.target.dataset.id); const $el = $body.querySelector(`div[data-id='${religionId}']`); if ($el) $el.classList.add("active"); @@ -280,7 +280,7 @@ function religionHighlightOn(event) { .attr("r", 8) .attr("stroke-width", 2) .attr("stroke", "#c13119"); -} +}, 200); function religionHighlightOff(event) { const religionId = Number(event.id || event.target.dataset.id); diff --git a/modules/dynamic/hierarchy-tree.js b/modules/dynamic/hierarchy-tree.js index 1dc94f3f..5a420eb5 100644 --- a/modules/dynamic/hierarchy-tree.js +++ b/modules/dynamic/hierarchy-tree.js @@ -7,6 +7,9 @@ const MARGINS = {top: 10, right: 10, bottom: -5, left: 10}; const handleZoom = () => viewbox.attr("transform", d3.event.transform); const zoom = d3.zoom().scaleExtent([0.2, 1.5]).on("zoom", handleZoom); +// store old root for transitions +let oldRoot; + // define svg elements const svg = d3.select("#hierarchyTree > svg").call(zoom); const viewbox = svg.select("g#hierarchyTree_viewbox"); @@ -159,10 +162,21 @@ function insertHtml() { function addListeners() {} function getRoot() { - return d3 + const root = d3 .stratify() .id(d => d.i) .parentId(d => d.origins[0])(validElements); + + oldRoot = root; + return root; +} + +function getLinkKey(d) { + return `${d.source.id}-${d.target.id}`; +} + +function getNodeKey(d) { + return d.id; } function getLinkPath(d) { @@ -211,14 +225,13 @@ const getSortIndex = node => { function renderTree(root, treeLayout) { treeLayout(root.sort((a, b) => getSortIndex(a) - getSortIndex(b))); - primaryLinks.selectAll("path").data(root.links()).enter().append("path").attr("d", getLinkPath); - secondaryLinks.selectAll("path").data(getSecondaryLinks(root)).enter().append("path").attr("d", getLinkPath); + primaryLinks.selectAll("path").data(root.links(), getLinkKey).join("path").attr("d", getLinkPath); + secondaryLinks.selectAll("path").data(getSecondaryLinks(root), getLinkKey).join("path").attr("d", getLinkPath); const node = nodes .selectAll("g") - .data(root.descendants()) - .enter() - .append("g") + .data(root.descendants(), getNodeKey) + .join("g") .attr("data-id", d => d.data.i) .attr("stroke", "#333") .attr("transform", d => `translate(${d.x}, ${d.y})`) @@ -236,27 +249,86 @@ function renderTree(root, treeLayout) { node.append("text").text(d => d.data.code || ""); } -function rerenderTree() { - nodes.selectAll("*").remove(); - primaryLinks.selectAll("*").remove(); - secondaryLinks.selectAll("*").remove(); +function mapCoords(newRoot, prevRoot) { + newRoot.x = prevRoot.x; + newRoot.y = prevRoot.y; + for (const node of newRoot.descendants()) { + const prevNode = prevRoot.descendants().find(n => n.data.i === node.data.i); + if (prevNode) { + node.x = prevNode.x; + node.y = prevNode.y; + } + } +} + +function updateTree() { + const prevRoot = oldRoot; const root = getRoot(); + mapCoords(root, prevRoot); + + const linksUpdateDuration = 50; + const moveDuration = 1000; + + // old layout: update links at old nodes positions + const linkEnter = enter => + enter + .append("path") + .attr("d", getLinkPath) + .attr("opacity", 0) + .call(enter => enter.transition().duration(linksUpdateDuration).attr("opacity", 1)); + + const linkUpdate = update => + update.call(update => update.transition().duration(linksUpdateDuration).attr("d", getLinkPath)); + + const linkExit = exit => + exit.call(exit => exit.transition().duration(linksUpdateDuration).attr("opacity", 0).remove()); + + primaryLinks.selectAll("path").data(root.links(), getLinkKey).join(linkEnter, linkUpdate, linkExit); + secondaryLinks.selectAll("path").data(getSecondaryLinks(root), getLinkKey).join(linkEnter, linkUpdate, linkExit); + + // new layout: move nodes with links to new positions const treeWidth = root.leaves().length * 50; const treeHeight = root.height * 50; const w = treeWidth - MARGINS.left - MARGINS.right; const h = treeHeight + 30 - MARGINS.top - MARGINS.bottom; - const treeLayout = d3.tree().size([w, h]); - renderTree(root, treeLayout); + const treeLayout = d3.tree().size([w, h]); + treeLayout(root.sort((a, b) => getSortIndex(a) - getSortIndex(b))); + + primaryLinks + .selectAll("path") + .data(root.links(), getLinkKey) + .transition() + .duration(moveDuration) + .delay(linksUpdateDuration) + .attr("d", getLinkPath); + + secondaryLinks + .selectAll("path") + .data(getSecondaryLinks(root), getLinkKey) + .transition() + .duration(moveDuration) + .delay(linksUpdateDuration) + .attr("d", getLinkPath); + + nodes + .selectAll("g") + .data(root.descendants(), getNodeKey) + .transition() + .delay(linksUpdateDuration) + .duration(moveDuration) + .attr("transform", d => `translate(${d.x},${d.y})`); } function selectElement(d) { const dataElement = d.data; + const node = nodes.select(`g[data-id="${d.id}"]`); nodes.selectAll("g").style("outline", "none"); - this.style.outline = "1px solid #c13119"; + node.style("outline", "1px solid #c13119"); + byId("hierarchyTree_selected").style.display = "block"; byId("hierarchyTree_infoLine").style.display = "none"; @@ -266,7 +338,8 @@ function selectElement(d) { byId("hierarchyTree_selectedCode").onchange = function () { if (this.value.length > 3) return tip("Abbreviation must be 3 characters or less", false, "error", 3000); if (!this.value.length) return tip("Abbreviation cannot be empty", false, "error", 3000); - nodes.select(`g[data-id="${d.id}"] > text`).text(this.value); + + node.select("text").text(this.value); dataElement.code = this.value; }; @@ -288,7 +361,7 @@ function selectElement(d) { const filtered = dataElement.origins.filter(elementOrigin => elementOrigin !== origin); dataElement.origins = filtered.length ? filtered : [0]; target.remove(); - rerenderTree(); + updateTree(); }; }; @@ -324,6 +397,7 @@ function selectElement(d) { `; }); + byId("hierarchyTree_originSelector").innerHTML = /*html*/ `
${selectableElementsHtml.join("")} @@ -347,7 +421,7 @@ function selectElement(d) { dataElement.origins = [primary, ...secondary]; - rerenderTree(); + updateTree(); createOriginButtons(); }, Cancel: () => { @@ -365,6 +439,8 @@ function selectElement(d) { } function handleNoteEnter(d) { + if (d.depth === 0) return; + this.classList.add("selected"); onNodeEnter(d); @@ -404,6 +480,7 @@ function dragToReorigin(from) { if (element.origins[0] === 0) element.origins = []; element.origins.push(newOrigin); - rerenderTree(); + selectElement(from); + updateTree(); }); } diff --git a/modules/religions-generator.js b/modules/religions-generator.js index a213f728..b7b3464c 100644 --- a/modules/religions-generator.js +++ b/modules/religions-generator.js @@ -418,7 +418,7 @@ window.Religions = (function () { const folk = isFolkBased && religions.find(r => r.culture === culture && r.type === "Folk"); if (folk && expansion === "culture" && folk.name.slice(0, 3) !== "Old") folk.name = "Old " + folk.name; - const origins = folk ? [folk.i] : getReligionsInRadius({x, y, r: 30, max: 2}); + const origins = folk ? [folk.i] : getReligionsInRadius({x, y, r: 150 / count, max: 2}); const expansionism = rand(3, 8); const baseColor = religions[culture]?.color || states[state]?.color || getRandomColor(); const color = getMixedColor(baseColor, 0.3, 0); @@ -451,7 +451,7 @@ window.Religions = (function () { if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion const culture = cells.culture[center]; - const origins = getReligionsInRadius({x, y, r: 75, max: rand(0, 4)}); + const origins = getReligionsInRadius({x, y, r: 300 / count, max: rand(0, 4)}); const deity = getDeityName(culture); const name = getCultName(form, center); diff --git a/modules/ui/editors.js b/modules/ui/editors.js index 248099af..3cf31e5c 100644 --- a/modules/ui/editors.js +++ b/modules/ui/editors.js @@ -1174,18 +1174,18 @@ function refreshAllEditors() { // dynamically loaded editors async function editStates() { if (customization) return; - const Editor = await import("../dynamic/editors/states-editor.js?v=08062022"); + const Editor = await import("../dynamic/editors/states-editor.js?v=12062022"); Editor.open(); } async function editCultures() { if (customization) return; - const Editor = await import("../dynamic/editors/cultures-editor.js?v=08062022"); + const Editor = await import("../dynamic/editors/cultures-editor.js?v=12062022"); Editor.open(); } async function editReligions() { if (customization) return; - const Editor = await import("../dynamic/editors/religions-editor.js?v=080620222"); + const Editor = await import("../dynamic/editors/religions-editor.js?v=12062022"); Editor.open(); } diff --git a/versioning.js b/versioning.js index 7bb4172f..21d29bf7 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.86.05"; // generator version, update each time +const version = "1.86.06"; // generator version, update each time { document.title += " v" + version;