feat(hierarchy tree): animate on update

This commit is contained in:
Azgaar 2022-06-12 22:21:34 +03:00
parent e3ea5cd479
commit 434907fa9e
7 changed files with 106 additions and 29 deletions

View file

@ -7801,7 +7801,7 @@
<script src="modules/cultures-generator.js?v=06062022"></script> <script src="modules/cultures-generator.js?v=06062022"></script>
<script src="modules/burgs-and-states.js?v=29052022"></script> <script src="modules/burgs-and-states.js?v=29052022"></script>
<script src="modules/routes-generator.js"></script> <script src="modules/routes-generator.js"></script>
<script src="modules/religions-generator.js?v=07062022"></script> <script src="modules/religions-generator.js?v=12062022"></script>
<script src="modules/military-generator.js"></script> <script src="modules/military-generator.js"></script>
<script src="modules/markers-generator.js"></script> <script src="modules/markers-generator.js"></script>
<script src="modules/coa-generator.js"></script> <script src="modules/coa-generator.js"></script>
@ -7820,7 +7820,7 @@
<script defer src="modules/relief-icons.js"></script> <script defer src="modules/relief-icons.js"></script>
<script defer src="modules/ui/style.js"></script> <script defer src="modules/ui/style.js"></script>
<script defer src="modules/ui/editors.js?v=080620222"></script> <script defer src="modules/ui/editors.js?v=12062022"></script>
<script defer src="modules/ui/tools.js?v=12062022"></script> <script defer src="modules/ui/tools.js?v=12062022"></script>
<script defer src="modules/ui/world-configurator.js?v=29052022"></script> <script defer src="modules/ui/world-configurator.js?v=29052022"></script>
<script defer src="modules/ui/heightmap-editor.js?v=29052020"></script> <script defer src="modules/ui/heightmap-editor.js?v=29052020"></script>

View file

@ -293,7 +293,7 @@ function getShapeOptions(selectShape, selected) {
return `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureEmblems hide">${options}</select>`; return `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureEmblems hide">${options}</select>`;
} }
function cultureHighlightOn(event) { const cultureHighlightOn = debounce(event => {
const cultureId = Number(event.id || event.target.dataset.id); const cultureId = Number(event.id || event.target.dataset.id);
if (!layerIsOn("toggleCultures")) return; if (!layerIsOn("toggleCultures")) return;
@ -312,7 +312,7 @@ function cultureHighlightOn(event) {
.transition(animate) .transition(animate)
.attr("r", 8) .attr("r", 8)
.attr("stroke", "#d0240f"); .attr("stroke", "#d0240f");
} }, 200);
function cultureHighlightOff(event) { function cultureHighlightOff(event) {
const cultureId = Number(event.id || event.target.dataset.id); const cultureId = Number(event.id || event.target.dataset.id);

View file

@ -258,7 +258,7 @@ function getTypeOptions(type) {
return options; return options;
} }
function religionHighlightOn(event) { const religionHighlightOn = debounce(event => {
const religionId = Number(event.id || event.target.dataset.id); const religionId = Number(event.id || event.target.dataset.id);
const $el = $body.querySelector(`div[data-id='${religionId}']`); const $el = $body.querySelector(`div[data-id='${religionId}']`);
if ($el) $el.classList.add("active"); if ($el) $el.classList.add("active");
@ -280,7 +280,7 @@ function religionHighlightOn(event) {
.attr("r", 8) .attr("r", 8)
.attr("stroke-width", 2) .attr("stroke-width", 2)
.attr("stroke", "#c13119"); .attr("stroke", "#c13119");
} }, 200);
function religionHighlightOff(event) { function religionHighlightOff(event) {
const religionId = Number(event.id || event.target.dataset.id); const religionId = Number(event.id || event.target.dataset.id);

View file

@ -7,6 +7,9 @@ const MARGINS = {top: 10, right: 10, bottom: -5, left: 10};
const handleZoom = () => viewbox.attr("transform", d3.event.transform); const handleZoom = () => viewbox.attr("transform", d3.event.transform);
const zoom = d3.zoom().scaleExtent([0.2, 1.5]).on("zoom", handleZoom); const zoom = d3.zoom().scaleExtent([0.2, 1.5]).on("zoom", handleZoom);
// store old root for transitions
let oldRoot;
// define svg elements // define svg elements
const svg = d3.select("#hierarchyTree > svg").call(zoom); const svg = d3.select("#hierarchyTree > svg").call(zoom);
const viewbox = svg.select("g#hierarchyTree_viewbox"); const viewbox = svg.select("g#hierarchyTree_viewbox");
@ -159,10 +162,21 @@ function insertHtml() {
function addListeners() {} function addListeners() {}
function getRoot() { function getRoot() {
return d3 const root = d3
.stratify() .stratify()
.id(d => d.i) .id(d => d.i)
.parentId(d => d.origins[0])(validElements); .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) { function getLinkPath(d) {
@ -211,14 +225,13 @@ const getSortIndex = node => {
function renderTree(root, treeLayout) { function renderTree(root, treeLayout) {
treeLayout(root.sort((a, b) => getSortIndex(a) - getSortIndex(b))); treeLayout(root.sort((a, b) => getSortIndex(a) - getSortIndex(b)));
primaryLinks.selectAll("path").data(root.links()).enter().append("path").attr("d", getLinkPath); primaryLinks.selectAll("path").data(root.links(), getLinkKey).join("path").attr("d", getLinkPath);
secondaryLinks.selectAll("path").data(getSecondaryLinks(root)).enter().append("path").attr("d", getLinkPath); secondaryLinks.selectAll("path").data(getSecondaryLinks(root), getLinkKey).join("path").attr("d", getLinkPath);
const node = nodes const node = nodes
.selectAll("g") .selectAll("g")
.data(root.descendants()) .data(root.descendants(), getNodeKey)
.enter() .join("g")
.append("g")
.attr("data-id", d => d.data.i) .attr("data-id", d => d.data.i)
.attr("stroke", "#333") .attr("stroke", "#333")
.attr("transform", d => `translate(${d.x}, ${d.y})`) .attr("transform", d => `translate(${d.x}, ${d.y})`)
@ -236,27 +249,86 @@ function renderTree(root, treeLayout) {
node.append("text").text(d => d.data.code || ""); node.append("text").text(d => d.data.code || "");
} }
function rerenderTree() { function mapCoords(newRoot, prevRoot) {
nodes.selectAll("*").remove(); newRoot.x = prevRoot.x;
primaryLinks.selectAll("*").remove(); newRoot.y = prevRoot.y;
secondaryLinks.selectAll("*").remove();
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(); 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 treeWidth = root.leaves().length * 50;
const treeHeight = root.height * 50; const treeHeight = root.height * 50;
const w = treeWidth - MARGINS.left - MARGINS.right; const w = treeWidth - MARGINS.left - MARGINS.right;
const h = treeHeight + 30 - MARGINS.top - MARGINS.bottom; 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) { function selectElement(d) {
const dataElement = d.data; const dataElement = d.data;
const node = nodes.select(`g[data-id="${d.id}"]`);
nodes.selectAll("g").style("outline", "none"); 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_selected").style.display = "block";
byId("hierarchyTree_infoLine").style.display = "none"; byId("hierarchyTree_infoLine").style.display = "none";
@ -266,7 +338,8 @@ function selectElement(d) {
byId("hierarchyTree_selectedCode").onchange = function () { 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 > 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); 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; dataElement.code = this.value;
}; };
@ -288,7 +361,7 @@ function selectElement(d) {
const filtered = dataElement.origins.filter(elementOrigin => elementOrigin !== origin); const filtered = dataElement.origins.filter(elementOrigin => elementOrigin !== origin);
dataElement.origins = filtered.length ? filtered : [0]; dataElement.origins = filtered.length ? filtered : [0];
target.remove(); target.remove();
rerenderTree(); updateTree();
}; };
}; };
@ -324,6 +397,7 @@ function selectElement(d) {
</div> </div>
`; `;
}); });
byId("hierarchyTree_originSelector").innerHTML = /*html*/ ` byId("hierarchyTree_originSelector").innerHTML = /*html*/ `
<form style="max-height: 35vh"> <form style="max-height: 35vh">
${selectableElementsHtml.join("")} ${selectableElementsHtml.join("")}
@ -347,7 +421,7 @@ function selectElement(d) {
dataElement.origins = [primary, ...secondary]; dataElement.origins = [primary, ...secondary];
rerenderTree(); updateTree();
createOriginButtons(); createOriginButtons();
}, },
Cancel: () => { Cancel: () => {
@ -365,6 +439,8 @@ function selectElement(d) {
} }
function handleNoteEnter(d) { function handleNoteEnter(d) {
if (d.depth === 0) return;
this.classList.add("selected"); this.classList.add("selected");
onNodeEnter(d); onNodeEnter(d);
@ -404,6 +480,7 @@ function dragToReorigin(from) {
if (element.origins[0] === 0) element.origins = []; if (element.origins[0] === 0) element.origins = [];
element.origins.push(newOrigin); element.origins.push(newOrigin);
rerenderTree(); selectElement(from);
updateTree();
}); });
} }

View file

@ -418,7 +418,7 @@ window.Religions = (function () {
const folk = isFolkBased && religions.find(r => r.culture === culture && r.type === "Folk"); 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; 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 expansionism = rand(3, 8);
const baseColor = religions[culture]?.color || states[state]?.color || getRandomColor(); const baseColor = religions[culture]?.color || states[state]?.color || getRandomColor();
const color = getMixedColor(baseColor, 0.3, 0); 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 if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion
const culture = cells.culture[center]; 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 deity = getDeityName(culture);
const name = getCultName(form, center); const name = getCultName(form, center);

View file

@ -1174,18 +1174,18 @@ function refreshAllEditors() {
// dynamically loaded editors // dynamically loaded editors
async function editStates() { async function editStates() {
if (customization) return; 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(); Editor.open();
} }
async function editCultures() { async function editCultures() {
if (customization) return; 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(); Editor.open();
} }
async function editReligions() { async function editReligions() {
if (customization) return; 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(); Editor.open();
} }

View file

@ -1,7 +1,7 @@
"use strict"; "use strict";
// version and caching control // 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; document.title += " v" + version;