collect statistics for a short period

This commit is contained in:
Azgaar 2021-09-24 01:00:03 +03:00
parent 2a9edd2458
commit 5d714c8c17
9 changed files with 160 additions and 53 deletions

View file

@ -554,12 +554,6 @@ input[type="color"]::-webkit-color-swatch-wrapper {
width: 100%;
}
#optionsSeedGenerate:before {
content: "✓";
margin-left: -2px;
font-weight: bold;
}
#options input[type="color"] {
width: 2em;
padding: 1px;

View file

@ -235,7 +235,7 @@
<div id="collapsible">
<button id="optionsTrigger" data-t="tipOptionsTrigger" data-tip="Click to show the Menu. Shortcut: Tab" class="options glow" onclick="showOptions(event)" style="padding:.6em .45em"></button>
<button id="regenerate" data-t="tipRegenerate" data-tip="Click to generate a new map. Shortcut: F2" onclick="regeneratePrompt()" class="options" style="display: none"><t data-t="newMap">New Map!</t></button>
<button id="regenerate" data-t="tipRegenerate" data-tip="Click to generate a new map. Shortcut: F2" onclick="regeneratePrompt('drawer')" class="options" style="display: none"><t data-t="newMap">New Map!</t></button>
</div>
<div id="options" style="display:none">
@ -942,14 +942,13 @@
<tr data-tip="Map seed number. Seed produces the same map only if canvas size and options are the same">
<td>
<i data-tip="Click to generate a map for this seed" id="optionsSeedGenerate"></i>
<i data-tip="Show seed history to apply a previous seed" id="optionsMapHistory" class="icon-history"></i>
</td>
<td>Map seed</td>
<td>
<input id="optionsSeed" class="long" type="number" min=1 max=999999999 step=1>
</td>
<td>
<i data-tip="Show seed history to apply a previous seed" id="optionsMapHistory" class="icon-history"></i>
<i data-tip="Copy map seed as URL. It will produce the same map only if options are default or the same" id="optionsCopySeed" class="icon-docs"></i>
</td>
</tr>
@ -4304,6 +4303,7 @@
<script src="modules/fonts.js"></script>
<script src="modules/ui/layers.js"></script>
<script src="modules/ui/measurers.js"></script>
<script src="libs/umami.js"></script>
<script async defer src="https://unpkg.com/dropbox@10.8.0/dist/Dropbox-sdk.min.js"></script>
<script async defer src="modules/ui/general.js"></script>
@ -4354,6 +4354,5 @@
<script async defer src="libs/jquery.ui.touch-punch.min.js"></script>
<script async defer src="libs/pell.min.js"></script>
<script async defer src="libs/jszip.min.js"></script>
<script async defer data-website-id="4f6fd0ae-646a-4946-a9da-7aad63284e48" src="https://fmg-stats.herokuapp.com/umami.js"></script>
</body>
</html>

48
libs/umami.js Normal file
View file

@ -0,0 +1,48 @@
(window => {
const noTrack = window.localStorage.getItem("noTrack");
if (noTrack) return;
const {
screen: {width, height},
navigator: {language},
location: {hostname, pathname, search},
document: {referrer}
} = window;
const website = "4f6fd0ae-646a-4946-a9da-7aad63284e48";
const root = "https://fmg-stats.herokuapp.com";
const screen = `${width}x${height}`;
const url = `${pathname}${search}`;
const post = (url, data) => {
const req = new XMLHttpRequest();
req.open("POST", url, true);
req.setRequestHeader("Content-Type", "application/json");
req.send(JSON.stringify(data));
};
const collect = (type, params) => {
const payload = {website, hostname, screen, language, cache: false};
Object.keys(params).forEach(key => {
payload[key] = params[key];
});
post(`${root}/api/collect`, {type, payload});
};
// const addEvents = () => {
// document.querySelectorAll("[class*='umami--']").forEach(element => {
// element.className.split(" ").forEach(className => {
// if (/^umami--([a-z]+)--([\w]+[\w-]*)$/.test(className)) {
// const [, type, value] = className.split("--");
// const listener = () => collect("event", {event_type: type, event_value: value});
// element.addEventListener(type, listener, true);
// }
// });
// });
// };
// addEvents();
collect("pageview", {url, referrer});
window.track = (event_type = "reach", event_value = "") => collect("event", {event_type, event_value, url});
})(window);

12
main.js
View file

@ -652,11 +652,13 @@ function generate() {
INFO && console.groupEnd("Generated Map " + seed);
} catch (error) {
ERROR && console.error(error);
const parsedError = parseError(error);
track("error", parsedError);
clearMainTip();
alertMessage.innerHTML = `An error is occured on map generation. Please retry.
<br>If error is critical, clear the stored data and try again.
<p id="errorBox">${parseError(error)}</p>`;
<p id="errorBox">${parsedError}</p>`;
$("#alert").dialog({
resizable: false,
title: "Generation error",
@ -667,7 +669,7 @@ function generate() {
localStorage.setItem("version", version);
},
Regenerate: function () {
regenerateMap();
regenerateMap("generation error");
$(this).dialog("close");
},
Ignore: function () {
@ -1830,7 +1832,7 @@ function addZones(number = 1) {
// show map stats on generation complete
function showStatistics() {
const template = templateInput.value;
const template = templateInput.options[templateInput.selectedIndex].text;
const templateRandom = locked("template") ? "" : "(random)";
const stats = ` Seed: ${seed}
Canvas size: ${graphWidth}x${graphHeight}
@ -1848,9 +1850,10 @@ function showStatistics() {
mapId = Date.now(); // unique map id is it's creation date number
mapHistory.push({seed, width: graphWidth, height: graphHeight, template, created: mapId});
INFO && console.log(stats);
track("generate", `Template: ${template} ${templateRandom}. Points: ${pointsInput.dataset.cells}`);
}
const regenerateMap = debounce(function () {
const regenerateMap = debounce(function (source) {
WARN && console.warn("Generate new random map");
closeDialogs("#worldConfigurator, #options3d");
customization = 0;
@ -1860,6 +1863,7 @@ const regenerateMap = debounce(function () {
restoreLayers();
if (ThreeD.options.isOn) ThreeD.redraw();
if ($("#worldConfigurator").is(":visible")) editWorld();
track("regenerate", `from ${source}`);
}, 1000);
// clear the map

View file

@ -4,6 +4,7 @@
// download map as SVG
async function saveSVG() {
TIME && console.time("saveSVG");
track("export", "svg");
const url = await getMapURL("svg");
const link = document.createElement("a");
link.download = getFileName() + ".svg";
@ -17,6 +18,7 @@ async function saveSVG() {
// download map as PNG
async function savePNG() {
TIME && console.time("savePNG");
track("export", "png");
const url = await getMapURL("png");
const link = document.createElement("a");
@ -47,6 +49,7 @@ async function savePNG() {
// download map as JPEG
async function saveJPEG() {
TIME && console.time("saveJPEG");
track("export", "jpg");
const url = await getMapURL("png");
const canvas = document.createElement("canvas");
@ -264,7 +267,11 @@ async function getMapURL(type, options = {}) {
if (!cloneEl.getElementById("labels")) cloneEl.getElementById("textPaths")?.remove(); // removed unused textPaths
// add armies style
if (cloneEl.getElementById("armies")) cloneEl.insertAdjacentHTML("afterbegin", "<style>#armies text {stroke: none; fill: #fff; text-shadow: 0 0 4px #000; dominant-baseline: central; text-anchor: middle; font-family: Helvetica; fill-opacity: 1;}#armies text.regimentIcon {font-size: .8em;}</style>");
if (cloneEl.getElementById("armies"))
cloneEl.insertAdjacentHTML(
"afterbegin",
"<style>#armies text {stroke: none; fill: #fff; text-shadow: 0 0 4px #000; dominant-baseline: central; text-anchor: middle; font-family: Helvetica; fill-opacity: 1;}#armies text.regimentIcon {font-size: .8em;}</style>"
);
// add xlink: for href to support svg1.1
if (type === "svg") {
@ -372,6 +379,7 @@ function inlineStyle(clone) {
}
function saveGeoJSON_Cells() {
track("export", "getJSON cells");
const json = {type: "FeatureCollection", features: []};
const cells = pack.cells;
const getPopulation = i => {
@ -402,6 +410,7 @@ function saveGeoJSON_Cells() {
}
function saveGeoJSON_Routes() {
track("export", "getJSON routes");
const json = {type: "FeatureCollection", features: []};
routes.selectAll("g > path").each(function () {
@ -418,6 +427,7 @@ function saveGeoJSON_Routes() {
}
function saveGeoJSON_Rivers() {
track("export", "getJSON rivers");
const json = {type: "FeatureCollection", features: []};
rivers.selectAll("path").each(function () {
@ -440,6 +450,8 @@ function saveGeoJSON_Rivers() {
}
function saveGeoJSON_Markers() {
// TODO: rework for new markers
track("export", "getJSON markers");
const json = {type: "FeatureCollection", features: []};
markers.selectAll("use").each(function () {

View file

@ -13,6 +13,7 @@ function quickLoad() {
}
async function loadFromDropbox() {
track("load", `from dropbox`);
const mapPath = document.getElementById("loadFromDropboxSelect")?.value;
DEBUG && console.log("Loading map from Dropbox:", mapPath);
@ -68,6 +69,7 @@ function loadMapPrompt(blob) {
function loadLastSavedMap() {
WARN && console.warn("Load last saved map");
track("load", `from browser storage`);
try {
uploadMap(blob);
} catch (error) {
@ -78,6 +80,7 @@ function loadMapPrompt(blob) {
}
function loadMapFromURL(maplink, random) {
track("load", `from url`);
const URL = decodeURIComponent(maplink);
fetch(URL, {method: "GET", mode: "cors"})
@ -93,6 +96,7 @@ function loadMapFromURL(maplink, random) {
}
function showUploadErrorMessage(error, URL, random) {
track("error", `map load from url`);
ERROR && console.error(error);
alertMessage.innerHTML = `Cannot load map from the ${link(URL, "link provided")}.
${random ? `A new random map is generated. ` : ""}
@ -431,12 +435,27 @@ function parseLoadedData(data) {
// 1.0 adds a legend box
legend = svg.append("g").attr("id", "legend");
legend.attr("font-family", "Almendra SC").attr("font-size", 13).attr("data-size", 13).attr("data-x", 99).attr("data-y", 93).attr("stroke-width", 2.5).attr("stroke", "#812929").attr("stroke-dasharray", "0 4 10 4").attr("stroke-linecap", "round");
legend
.attr("font-family", "Almendra SC")
.attr("font-size", 13)
.attr("data-size", 13)
.attr("data-x", 99)
.attr("data-y", 93)
.attr("stroke-width", 2.5)
.attr("stroke", "#812929")
.attr("stroke-dasharray", "0 4 10 4")
.attr("stroke-linecap", "round");
// 1.0 separated drawBorders fron drawStates()
stateBorders = borders.append("g").attr("id", "stateBorders");
provinceBorders = borders.append("g").attr("id", "provinceBorders");
borders.attr("opacity", null).attr("stroke", null).attr("stroke-width", null).attr("stroke-dasharray", null).attr("stroke-linecap", null).attr("filter", null);
borders
.attr("opacity", null)
.attr("stroke", null)
.attr("stroke-width", null)
.attr("stroke-dasharray", null)
.attr("stroke-linecap", null)
.attr("filter", null);
stateBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt");
provinceBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 0.5).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt");
@ -976,7 +995,7 @@ function parseLoadedData(data) {
},
"New map": function () {
$(this).dialog("close");
regenerateMap();
regenerateMap("loading error");
},
Cancel: function () {
$(this).dialog("close");

View file

@ -16,12 +16,15 @@ function editHeightmap() {
width: "28em",
buttons: {
Erase: function () {
track("edit", "heightmap erase");
enterHeightmapEditMode("erase");
},
Keep: function () {
track("edit", "heightmap keep");
enterHeightmapEditMode("keep");
},
Risk: function () {
track("edit", "heightmap risk");
enterHeightmapEditMode("risk");
},
Cancel: function () {
@ -87,7 +90,16 @@ function editHeightmap() {
exitCustomization.style.bottom = svgHeight / 2 + "px";
exitCustomization.style.transform = "scale(2)";
exitCustomization.style.display = "block";
d3.select("#exitCustomization").transition().duration(1000).style("opacity", 1).transition().duration(2000).ease(d3.easeSinInOut).style("right", "10px").style("bottom", "10px").style("transform", "scale(1)");
d3.select("#exitCustomization")
.transition()
.duration(1000)
.style("opacity", 1)
.transition()
.duration(2000)
.ease(d3.easeSinInOut)
.style("right", "10px")
.style("bottom", "10px")
.style("transform", "scale(1)");
} else exitCustomization.style.display = "block";
openBrushesPanel();
@ -130,7 +142,8 @@ function editHeightmap() {
// Exit customization mode
function finalizeHeightmap() {
if (viewbox.select("#heights").selectAll("*").size() < 200) return tip("Insufficient land area! There should be at least 200 land cells to finalize the heightmap", null, "error");
if (viewbox.select("#heights").selectAll("*").size() < 200)
return tip("Insufficient land area! There should be at least 200 land cells to finalize the heightmap", null, "error");
if (document.getElementById("imageConverter").offsetParent) return tip("Please exit the Image Conversion mode first", null, "error");
delete window.edits; // remove global variable
@ -618,7 +631,10 @@ function editHeightmap() {
else if (brush === "brushLower") s.forEach(i => (h[i] = lim(h[i] - power)));
else if (brush === "brushDepress") s.forEach((i, d) => (h[i] = lim(h[i] - interpolate(d / Math.max(s.length - 1, 1)))));
else if (brush === "brushAlign") s.forEach(i => (h[i] = lim(h[start])));
else if (brush === "brushSmooth") s.forEach(i => (h[i] = rn((d3.mean(grid.cells.c[i].filter(i => (land ? h[i] >= 20 : 1)).map(c => h[c])) + h[i] * (10 - power) + 0.6) / (11 - power), 1)));
else if (brush === "brushSmooth")
s.forEach(
i => (h[i] = rn((d3.mean(grid.cells.c[i].filter(i => (land ? h[i] >= 20 : 1)).map(c => h[c])) + h[i] * (10 - power) + 0.6) / (11 - power), 1))
);
else if (brush === "brushDisrupt") s.forEach(i => (h[i] = h[i] < 15 ? h[i] : lim(h[i] + power / 1.6 - Math.random() * power)));
mockHeightmapSelection(s);
@ -688,6 +704,7 @@ function editHeightmap() {
function openTemplateEditor() {
if ($("#templateEditor").is(":visible")) return;
track("edit", "template editor");
const body = document.getElementById("templateBody");
$("#templateEditor").dialog({
@ -767,15 +784,29 @@ function editHeightmap() {
const TempY = `<span>y:<input class="templateY" data-tip="Placement range percentage along Y axis (minY-maxY)" value=${arg5 || "20-80"}></span>`;
const TempX = `<span>x:<input class="templateX" data-tip="Placement range percentage along X axis (minX-maxX)" value=${arg4 || "15-85"}></span>`;
const Height = `<span>h:<input class="templateHeight" data-tip="Blob maximum height, use hyphen to get a random number in range" value=${arg3 || "40-50"}></span>`;
const Height = `<span>h:<input class="templateHeight" data-tip="Blob maximum height, use hyphen to get a random number in range" value=${
arg3 || "40-50"
}></span>`;
const Count = `<span>n:<input class="templateCount" data-tip="Blobs to add, use hyphen to get a random number in range" value=${count || "1-2"}></span>`;
const blob = `${common}${TempY}${TempX}${Height}${Count}</div>`;
if (type === "Hill" || type === "Pit" || type === "Range" || type === "Trough") return blob;
if (type === "Strait") return `${common}<span>d:<select class="templateDist" data-tip="Strait direction"><option value="vertical" selected>vertical</option><option value="horizontal">horizontal</option></select></span><span>w:<input class="templateCount" data-tip="Strait width, use hyphen to get a random number in range" value=${count || "2-7"}></span></div>`;
if (type === "Add") return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Add value to height of all cells (negative values are allowed)" type="number" value=${count || -10} min=-100 max=100 step=1></span></div>`;
if (type === "Multiply") return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Multiply all cells Height by the value" type="number" value=${count || 1.1} min=0 max=10 step=.1></span></div>`;
if (type === "Smooth") return `${common}<span>f:<input class="templateCount" data-tip="Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc." type="number" min=1 max=10 value=${count || 2}></span></div>`;
if (type === "Strait")
return `${common}<span>d:<select class="templateDist" data-tip="Strait direction"><option value="vertical" selected>vertical</option><option value="horizontal">horizontal</option></select></span><span>w:<input class="templateCount" data-tip="Strait width, use hyphen to get a random number in range" value=${
count || "2-7"
}></span></div>`;
if (type === "Add")
return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Add value to height of all cells (negative values are allowed)" type="number" value=${
count || -10
} min=-100 max=100 step=1></span></div>`;
if (type === "Multiply")
return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Multiply all cells Height by the value" type="number" value=${
count || 1.1
} min=0 max=10 step=.1></span></div>`;
if (type === "Smooth")
return `${common}<span>f:<input class="templateCount" data-tip="Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc." type="number" min=1 max=10 value=${
count || 2
}></span></div>`;
}
function setRange(event) {
@ -910,6 +941,7 @@ function editHeightmap() {
function openImageConverter() {
if ($("#imageConverter").is(":visible")) return;
track("edit", "convert image");
imageToLoad.click();
closeDialogs("#imageConverter");
@ -1170,10 +1202,14 @@ function editHeightmap() {
}
function setConvertColorsNumber() {
prompt(`Please set maximum number of colors. <br>An actual number is usually lower and depends on color scheme`, {default: +convertColors.value, step: 1, min: 3, max: 255}, number => {
prompt(
`Please set maximum number of colors. <br>An actual number is usually lower and depends on color scheme`,
{default: +convertColors.value, step: 1, min: 3, max: 255},
number => {
convertColors.value = number;
heightsFromImage(number);
});
}
);
}
function setOverlayOpacity(v) {

View file

@ -26,7 +26,7 @@ function handleKeyup(event) {
const alt = altKey || key === "Alt";
if (key === "F1") showInfo();
else if (key === "F2") regeneratePrompt();
else if (key === "F2") regeneratePrompt("hotkey");
else if (key === "F6") quickSave();
else if (key === "F9") quickLoad();
else if (key === "TAB") toggleOptions(event);

View file

@ -13,6 +13,7 @@ if (localStorage.getItem("disable_click_arrow_tooltip")) {
// Show options pane on trigger click
function showOptions(event) {
track("click", "show options");
if (!localStorage.getItem("disable_click_arrow_tooltip")) {
clearMainTip();
localStorage.setItem("disable_click_arrow_tooltip", true);
@ -75,6 +76,7 @@ document
// show popup with a list of Patreon supportes (updated manually, to be replaced with API call)
function showSupporters() {
track("click", "show supporters");
const supporters = `Aaron Meyer,Ahmad Amerih,AstralJacks,aymeric,Billy Dean Goehring,Branndon Edwards,Chase Mayers,Curt Flood,cyninge,Dino Princip,
E.M. White,es,Fondue,Fritjof Olsson,Gatsu,Johan Fröberg,Jonathan Moore,Joseph Miranda,Kate,KC138,Luke Nelson,Markus Finster,Massimo Vella,Mikey,
Nathan Mitchell,Paavi1,Pat,Ryan Westcott,Sasquatch,Shawn Spencer,Sizz_TV,Timothée CALLET,UTG community,Vlad Tomash,Wil Sisney,William Merriott,
@ -160,7 +162,7 @@ optionsContent.addEventListener("change", function (event) {
const value = event.target.value;
if (id === "zoomExtentMin" || id === "zoomExtentMax") changeZoomExtent(value);
else if (id === "optionsSeed") generateMapWithSeed();
else if (id === "optionsSeed") generateMapWithSeed("seed change");
else if (id === "uiSizeInput" || id === "uiSizeOutput") changeUIsize(value);
if (id === "shapeRendering") viewbox.attr("shape-rendering", value);
else if (id === "yearInput") changeYear();
@ -170,7 +172,6 @@ optionsContent.addEventListener("change", function (event) {
optionsContent.addEventListener("click", function (event) {
const id = event.target.id;
if (id === "toggleFullscreen") toggleFullscreen();
else if (id === "optionsSeedGenerate") generateMapWithSeed();
else if (id === "optionsMapHistory") showSeedHistoryDialog();
else if (id === "optionsCopySeed") copyMapURL();
else if (id === "optionsEraRegenerate") regenerateEra();
@ -211,8 +212,8 @@ function changeMapSize() {
// just apply canvas size that was already set
function applyMapSize() {
const zoomMin = +zoomExtentMin.value,
zoomMax = +zoomExtentMax.value;
const zoomMin = +zoomExtentMin.value;
const zoomMax = +zoomExtentMax.value;
graphWidth = +mapWidthInput.value;
graphHeight = +mapHeightInput.value;
svgWidth = Math.min(graphWidth, window.innerWidth);
@ -280,12 +281,9 @@ function testSpeaker() {
speechSynthesis.speak(speaker);
}
function generateMapWithSeed() {
if (optionsSeed.value == seed) {
tip("The current map already has this seed", false, "error");
return;
}
regeneratePrompt();
function generateMapWithSeed(source) {
if (optionsSeed.value == seed) return tip("The current map already has this seed", false, "error");
regeneratePrompt(source);
}
function showSeedHistoryDialog() {
@ -316,7 +314,7 @@ function restoreSeed(id) {
mapHeightInput.value = mapHistory[id].height;
templateInput.value = mapHistory[id].template;
if (locked("template")) unlock("template");
regeneratePrompt();
regeneratePrompt("seed history");
}
function restoreDefaultZoomExtent() {
@ -656,23 +654,17 @@ function restoreDefaultOptions() {
// Sticked menu Options listeners
document.getElementById("sticked").addEventListener("click", function (event) {
const id = event.target.id;
if (id === "newMapButton") regeneratePrompt();
if (id === "newMapButton") regeneratePrompt("sticky button");
else if (id === "saveButton") showSavePane();
else if (id === "exportButton") showExportPane();
else if (id === "loadButton") showLoadPane();
else if (id === "zoomReset") resetZoom(1000);
});
function regeneratePrompt() {
if (customization) {
tip("New map cannot be generated when edit mode is active, please exit the mode and retry", false, "error");
return;
}
function regeneratePrompt(source) {
if (customization) return tip("New map cannot be generated when edit mode is active, please exit the mode and retry", false, "error");
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
if (workingTime < 5) {
regenerateMap();
return;
}
if (workingTime < 5) return regenerateMap(source);
alertMessage.innerHTML = `Are you sure you want to generate a new map?<br>
All unsaved changes made to the current map will be lost`;
@ -685,7 +677,7 @@ function regeneratePrompt() {
},
Generate: function () {
closeDialogs();
regenerateMap();
regenerateMap(source);
}
}
});
@ -792,6 +784,7 @@ function loadURL() {
// load map
document.getElementById("mapToLoad").addEventListener("change", function () {
track("load", `from local file`);
const fileToLoad = this.files[0];
this.value = "";
closeDialogs();
@ -811,6 +804,7 @@ function openSaveTiles() {
width: "23em",
buttons: {
Download: function () {
track("export", `tiles`);
status.innerHTML = "Preparing for download...";
setTimeout(() => (status.innerHTML = "Downloading. It may take some time."), 1000);
loading = setInterval(() => (status.innerHTML += "."), 1000);
@ -900,6 +894,7 @@ function enterStandardView() {
}
async function enter3dView(type) {
track("click", `3d mode: ${type}`);
const canvas = document.createElement("canvas");
canvas.id = "canvas3d";
canvas.dataset.type = type;