atlas/web/_js/main/view.js

914 lines
24 KiB
JavaScript

/*!
* The 2022 r/place Atlas
* Copyright (c) 2017 Roland Rytz <roland@draemm.li>
* Copyright (c) 2022 Place Atlas Initiative and contributors
* Licensed under AGPL-3.0 (https://2022.place-atlas.stefanocoding.me/license.txt)
*/
const linesCanvas = document.getElementById("linesCanvas")
const linesContext = linesCanvas.getContext("2d")
let hovered = []
let previousScaleZoomOrigin
let previousZoom
const backgroundCanvas = document.createElement("canvas")
backgroundCanvas.width = canvasSize.x
backgroundCanvas.height = canvasSize.y
const backgroundContext = backgroundCanvas.getContext("2d")
const wrapper = document.getElementById("wrapper")
const bottomBar = document.getElementById("bottomBar")
const showListButton = document.getElementById("showListButton")
const offcanvasList = document.getElementById("offcanvasList")
const bsOffcanvasList = new bootstrap.Offcanvas(offcanvasList)
const offcanvasDraw = document.getElementById("offcanvasDraw")
const bsOffcanvasDraw = new bootstrap.Offcanvas(offcanvasDraw)
const objectsContainer = document.getElementById("objectsList")
const closeObjectsListButton = document.getElementById("closeObjectsListButton")
const objectsListOverflowNotice = document.getElementById("objectsListOverflowNotice")
const searchInput = document.getElementById("searchList")
const sortInput = document.getElementById("sort")
const entriesList = document.getElementById("entriesList")
let entriesListShown = false
const drawButton = document.getElementById("drawLink")
const objectEditNav = document.createElement("a")
objectEditNav.className = "btn btn-outline-primary"
objectEditNav.id = "objectEditNav"
objectEditNav.textContent = "Edit"
let atlas = null
window.atlas = atlas
let atlasOrder = []
window.atlasOrder = atlasOrder
const entriesLimit = 50
let entriesOffset = 0
const moreEntriesButton = document.createElement("button")
moreEntriesButton.innerHTML = "Show " + entriesLimit + " more"
moreEntriesButton.type = "button"
moreEntriesButton.className = "btn btn-primary d-block mb-2 mx-auto"
moreEntriesButton.id = "moreEntriesButton"
let showMoreEntries = () => {}
const moreEntriesObserver = new IntersectionObserver(entries => {
for (const entry of entries) {
if (!entry.isIntersecting) continue
showMoreEntries()
break
}
})
moreEntriesObserver.observe(moreEntriesButton)
let defaultSort = sortInput.value
let lastPos = [0, 0]
let fixed = false; // Fix hovered items in place, so that clicking on links is possible
searchInput.addEventListener("input", function () {
updateAtlas()
})
sortInput.addEventListener("input", function () {
updateAtlas()
})
offcanvasDraw.addEventListener('show.bs.offcanvas', () => {
wrapper.classList.remove('listHidden')
wrapper.classList.add('listTransitioning')
applyView()
})
offcanvasDraw.addEventListener('shown.bs.offcanvas', () => {
wrapper.classList.remove('listTransitioning')
applyView()
})
offcanvasDraw.addEventListener('hide.bs.offcanvas', () => {
wrapper.classList.add('listHidden')
wrapper.classList.add('listTransitioning')
applyView()
})
offcanvasDraw.addEventListener('hidden.bs.offcanvas', () => {
wrapper.classList.remove('listTransitioning')
applyView()
})
offcanvasList.addEventListener('show.bs.offcanvas', () => {
wrapper.classList.remove('listHidden')
wrapper.classList.add('listTransitioning')
applyView()
})
offcanvasList.addEventListener('shown.bs.offcanvas', e => {
entriesListShown = true
wrapper.classList.remove('listTransitioning')
updateHovering(e)
applyView()
renderHighlight()
renderLines()
})
offcanvasList.addEventListener('hide.bs.offcanvas', () => {
wrapper.classList.add('listHidden')
wrapper.classList.add('listTransitioning')
applyView()
})
offcanvasList.addEventListener('hidden.bs.offcanvas', e => {
entriesListShown = false
wrapper.classList.remove('listTransitioning')
updateHovering(e)
applyView()
renderHighlight()
renderLines()
})
closeObjectsListButton.addEventListener("click", clearObjectsList)
function clearObjectsList() {
hovered = []
fixed = false
renderLines()
renderHighlight()
document.title = pageTitle
closeObjectsListButton.classList.add("d-none")
objectsListOverflowNotice.classList.add("d-none")
entriesList.classList.remove("disableHover")
objectsContainer.replaceChildren()
objectEditNav.remove()
updateHash(false)
}
function toggleFixed(e, tapped) {
if (!fixed && hovered.length === 0) {
entriesList.classList.remove("disableHover")
return
}
fixed = !fixed
if (!fixed) {
updateHovering(e, tapped)
renderHighlight()
}
entriesList.classList.add("disableHover")
objectsListOverflowNotice.classList.add("d-none")
}
window.addEventListener("dblClick", renderLines)
window.addEventListener("wheel", renderLines)
objectsContainer.addEventListener("scroll", () => {
renderLines()
})
window.addEventListener("resize", () => {
applyView()
renderHighlight()
renderLines()
})
async function renderLines() {
if (hovered.length === 0) {
linesContext.clearRect(0, 0, linesCanvas.width, linesCanvas.height)
return
}
// Line border
linesCanvas.width = linesCanvas.clientWidth
linesCanvas.height = linesCanvas.clientHeight
linesContext.lineCap = "round"
linesContext.lineWidth = Math.max(Math.min(zoom * 1.5, 16 * 1.5), 6)
linesContext.strokeStyle = "#222"
for (let i = 0; i < hovered.length; i++) {
const element = hovered[i].element
if (element.getBoundingClientRect().left !== 0) {
linesContext.beginPath()
// Align line based on which side the card is on
if ((element.getBoundingClientRect().left + element.clientWidth / 2) < (document.documentElement.clientWidth / 2)) {
linesContext.moveTo(
element.getBoundingClientRect().left + document.documentElement.scrollLeft + element.clientWidth - 5,
element.getBoundingClientRect().top + document.documentElement.scrollTop + 20
)
} else {
linesContext.moveTo(
element.getBoundingClientRect().left + document.documentElement.scrollLeft + 5,
element.getBoundingClientRect().top + document.documentElement.scrollTop + 20
)
}
linesContext.lineTo(
~~((hovered[i].center[0] - canvasOffset.x) * zoom) + innerContainer.offsetLeft,
~~((hovered[i].center[1] - canvasOffset.y) * zoom) + innerContainer.offsetTop
)
linesContext.stroke()
}
}
// Line body
linesContext.lineWidth = Math.max(Math.min(zoom, 16), 4)
linesContext.strokeStyle = "#FFFFFF"
for (let i = 0; i < hovered.length; i++) {
const element = hovered[i].element
if (element.getBoundingClientRect().left !== 0) {
linesContext.beginPath()
// Align line based on which side the card is on
if ((element.getBoundingClientRect().left + element.clientWidth / 2) < (document.documentElement.clientWidth / 2)) {
linesContext.moveTo(
element.getBoundingClientRect().left + document.documentElement.scrollLeft + element.clientWidth - 5,
element.getBoundingClientRect().top + document.documentElement.scrollTop + 20
)
} else {
linesContext.moveTo(
element.getBoundingClientRect().left + document.documentElement.scrollLeft + 5,
element.getBoundingClientRect().top + document.documentElement.scrollTop + 20
)
}
linesContext.lineTo(
~~((hovered[i].center[0] - canvasOffset.x) * zoom) + innerContainer.offsetLeft,
~~((hovered[i].center[1] - canvasOffset.y) * zoom) + innerContainer.offsetTop
)
linesContext.stroke()
}
}
}
function renderBackground(atlas) {
backgroundContext.clearRect(0, 0, highlightCanvas.width, highlightCanvas.height)
//backgroundCanvas.width = 1000 * zoom
//backgroundCanvas.height = 1000 * zoom
//backgroundContext.lineWidth = zoom
backgroundContext.fillStyle = "rgba(0, 0, 0, 0.6)"
backgroundContext.fillRect(0, 0, backgroundCanvas.width, backgroundCanvas.height)
for (const entry of Object.values(atlas)) {
const path = entry.path
backgroundContext.beginPath()
if (path[0]) {
//backgroundContext.moveTo(path[0][0]*zoom, path[0][1]*zoom)
backgroundContext.moveTo(path[0][0] - canvasOffset.x, path[0][1] - canvasOffset.y)
}
for (let p = 1; p < path.length; p++) {
//backgroundContext.lineTo(path[p][0]*zoom, path[p][1]*zoom)
backgroundContext.lineTo(path[p][0] - canvasOffset.x, path[p][1] - canvasOffset.y)
}
backgroundContext.closePath()
let bgStrokeStyle
switch (entry.diff) {
case "add":
bgStrokeStyle = "rgba(0, 255, 0, 1)"
backgroundContext.lineWidth = 2
break
case "edit":
bgStrokeStyle = "rgba(255, 255, 0, 1)"
backgroundContext.lineWidth = 2
break
case "delete":
bgStrokeStyle = "rgba(255, 0, 0, 1)"
backgroundContext.lineWidth = 2
break
default:
bgStrokeStyle = "rgba(255, 255, 255, 0.8)"
break
}
backgroundContext.strokeStyle = bgStrokeStyle
backgroundContext.stroke()
backgroundContext.lineWidth = 1
}
}
function filterAtlas(prevAtlas) {
const sort = sortInput.value || defaultSort
const search = searchInput?.value.toLowerCase()
let newAtlas = Object.assign({}, prevAtlas)
let newAtlasOrder = []
document.getElementById("atlasSize").innerHTML = ""
if (search) {
for (const [id, entry] of Object.entries(prevAtlas)) {
if (!(
entry.name.toLowerCase().includes(search.toLowerCase()) ||
entry.description?.toLowerCase().includes(search.toLowerCase()) ||
Object.values(entry.links).flat().some(str => str.toLowerCase().includes(search)) ||
id.toString() === search
)) delete newAtlas[id]
}
}
// document.getElementById("sort").value = sort
let sortFunction
switch (sort) {
case "shuffle":
sortFunction = () => Math.random() - 0.5
break
case "alphaAsc":
sortFunction = (a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())
break
case "alphaDesc":
sortFunction = (a, b) => b.name.toLowerCase().localeCompare(a.name.toLowerCase())
break
case "newest":
sortFunction = (a, b) => b._index - a._index
break
case "oldest":
sortFunction = (a, b) => a._index - b._index
break
case "area":
sortFunction = (a, b) => calcPolygonArea(b.path) - calcPolygonArea(a.path)
break
case "relevant":
sortFunction = (a, b) => {
if (a.name.toLowerCase().includes(filter) && b.name.toLowerCase().includes(filter)) {
return a.name.toLowerCase().indexOf(filter) - b.name.toLowerCase().indexOf(filter) || a.name.toLowerCase().localeCompare(b.name.toLowerCase())
} else if (a.name.toLowerCase().includes(filter)) {
return -1
} else if (b.name.toLowerCase().includes(filter)) {
return 1
} else {
return a.description.toLowerCase().indexOf(filter) - b.description.toLowerCase().indexOf(filter) || a.name.toLowerCase().localeCompare(b.name.toLowerCase())
}
}
break
}
newAtlasOrder = Object.keys(newAtlas)
if (sortFunction) {
newAtlasOrder = newAtlasOrder.sort((a, b) => sortFunction(prevAtlas[a], prevAtlas[b]))
}
// console.log(newAtlas, newAtlasOrder)
return [newAtlas, newAtlasOrder]
}
function updateAtlas() {
;[atlas, atlasOrder] = filterAtlas(atlasAll)
;[atlasDisplay, atlasOrder] = generateAtlasDisplay(atlas, atlasOrder, currentPeriod, currentVariation)
const atlasSizeEl = document.getElementById("atlasSize")
if (Object.keys(atlas).length === Object.keys(atlasAll).length) {
atlasSizeEl.innerHTML = Object.keys(atlasAll).length + " entries in total."
} else {
atlasSizeEl.innerHTML = "Found " + Object.keys(atlas).length + " entries."
}
atlasSizeEl.innerHTML += " Displaying " + Object.keys(atlasDisplay).length + " entries."
resetEntriesList()
renderBackground(atlasDisplay)
renderHighlight(atlasDisplay)
}
async function resetEntriesList() {
entriesOffset = 0
entriesList.replaceChildren()
entriesList.appendChild(moreEntriesButton)
moreEntriesButton.removeEventListener('click', showMoreEntries)
showMoreEntries = () => {
if (entriesList.contains(moreEntriesButton)) {
entriesList.removeChild(moreEntriesButton)
}
let entriesLeft = entriesLimit
let element
while (entriesLeft > 0 && atlasOrder.length > entriesOffset) {
if (atlasDisplay[atlasOrder[entriesOffset]]) {
// console.log(i, entriesLeft)
let entry = atlasDisplay[atlasOrder[entriesOffset]]
element = createInfoBlock(entry)
element.addEventListener("mouseenter", function () {
if (fixed || dragging) return
objectsContainer.replaceChildren()
previousScaleZoomOrigin ??= [...scaleZoomOrigin]
previousZoom ??= zoom
setView(entry.center[0], entry.center[1], calculateZoomFromPath(entry.path))
hovered = [entry]
renderHighlight()
hovered[0].element = this
renderLines()
})
element.addEventListener("click", e => {
fixed = true
previousScaleZoomOrigin ??= [...scaleZoomOrigin]
previousZoom ??= zoom
applyView()
})
element.addEventListener("mouseleave", () => {
if (fixed || dragging) return
scaleZoomOrigin = [...previousScaleZoomOrigin]
zoom = previousZoom
previousScaleZoomOrigin = undefined
previousZoom = undefined
applyView()
hovered = []
renderLines()
renderHighlight()
})
} else {
let entry = atlas[atlasOrder[entriesOffset]]
element = createInfoBlock(entry, 1)
element.addEventListener("click", async e => {
e.preventDefault()
const [nearestPeriod, nearestVariation] = getNearestPeriod(entry, currentPeriod, currentVariation)
await updateTime(nearestPeriod, nearestVariation, true)
entry = atlasDisplay[entry.id]
element = createInfoBlock(entry)
hovered = [{ ...entry, element }]
fixed = true
previousScaleZoomOrigin = undefined
previousZoom = undefined
const hash = formatHash(entry.id, nearestPeriod, nearestVariation, entry.center[0], entry.center[1], calculateZoomFromPath(entry.path))
location.hash = hash
})
}
entriesOffset += 1
entriesLeft -= 1
entriesList.appendChild(element)
}
if (atlasOrder.length > entriesOffset) {
moreEntriesButton.innerHTML = "Show " + Math.min(entriesLimit, atlasOrder.length - entriesOffset) + " more"
entriesList.appendChild(moreEntriesButton)
}
}
moreEntriesButton.addEventListener('click', showMoreEntries)
showMoreEntries()
}
async function renderHighlight() {
highlightContext.clearRect(0, 0, highlightCanvas.width, highlightCanvas.height)
//canvas.width = 1000*zoom
//canvas.height = 1000*zoom
highlightContext.globalCompositeOperation = "source-over"
highlightContext.clearRect(0, 0, highlightCanvas.width, highlightCanvas.height)
if (hovered.length > 0) {
container.style.cursor = "pointer"
} else {
container.style.cursor = "default"
}
for (let i = 0; i < hovered.length; i++) {
const path = hovered[i].path
highlightContext.beginPath()
if (path[0]) {
//context.moveTo(path[0][0]*zoom, path[0][1]*zoom)
highlightContext.moveTo(path[0][0] - canvasOffset.x, path[0][1] - canvasOffset.y)
}
for (let p = 1; p < path.length; p++) {
//context.lineTo(path[p][0]*zoom, path[p][1]*zoom)
highlightContext.lineTo(path[p][0] - canvasOffset.x, path[p][1] - canvasOffset.y)
}
highlightContext.closePath()
highlightContext.globalCompositeOperation = "source-over"
highlightContext.fillStyle = "rgba(0, 0, 0, 1)"
highlightContext.fill()
}
highlightContext.globalCompositeOperation = "source-out"
highlightContext.drawImage(backgroundCanvas, 0, 0)
for (let i = 0; i < hovered.length; i++) {
const path = hovered[i].path
highlightContext.beginPath()
if (path[0]) {
//context.moveTo(path[0][0]*zoom, path[0][1]*zoom)
highlightContext.moveTo(path[0][0] - canvasOffset.x, path[0][1] - canvasOffset.y)
}
for (let p = 1; p < path.length; p++) {
//context.lineTo(path[p][0]*zoom, path[p][1]*zoom)
highlightContext.lineTo(path[p][0] - canvasOffset.x, path[p][1] - canvasOffset.y)
}
highlightContext.closePath()
highlightContext.globalCompositeOperation = "source-over"
let hoverStrokeStyle
switch (hovered[i].diff) {
case "add":
hoverStrokeStyle = "rgba(0, 155, 0, 1)"
break
case "edit":
hoverStrokeStyle = "rgba(155, 155, 0, 1)"
break
default:
hoverStrokeStyle = "rgba(0, 0, 0, 1)"
break
}
highlightContext.strokeStyle = hoverStrokeStyle
//context.lineWidth = zoom
highlightContext.stroke()
}
}
function updateCoordsDisplay(e) {
const pos = [
(e.clientX - (container.clientWidth / 2 - innerContainer.clientWidth / 2 + zoomOrigin[0] + container.offsetLeft)) / zoom + canvasOffset.x,
(e.clientY - (container.clientHeight / 2 - innerContainer.clientHeight / 2 + zoomOrigin[1] + container.offsetTop)) / zoom + canvasOffset.y
]
const coordsEl = document.getElementById("coords_p")
// Displays coordinates as zero instead of NaN
if (isNaN(pos[0])) {
coordsEl.textContent = "0, 0"
} else {
coordsEl.textContent = Math.floor(pos[0]) + ", " + Math.floor(pos[1])
}
return pos
}
function updateHovering(e, tapped) {
if (dragging || (fixed && !tapped)) return
const pos = updateCoordsDisplay(e)
if (!(pos[0] <= canvasSize.x + canvasOffset.x + 200 && pos[0] >= canvasOffset.x - 200 && pos[1] <= canvasSize.y + canvasOffset.y + 200 && pos[1] >= canvasOffset.x - 200)) return
let newHovered = []
for (const entry of Object.values(atlasDisplay)) {
if (pointIsInPolygon(pos, entry.path)) newHovered.push(entry)
}
newHovered = newHovered.sort(function (a, b) {
return calcPolygonArea(a.path) - calcPolygonArea(b.path)
})
let changed = false
if (hovered.length === newHovered.length) {
for (let i = 0; i < hovered.length; i++) {
if (hovered[i].id !== newHovered[i].id) {
changed = true
break
}
}
} else {
changed = true
}
if (!changed) return
hovered = newHovered
objectsContainer.replaceChildren()
for (const entry of hovered) {
const element = createInfoBlock(entry)
objectsContainer.appendChild(element)
entry.element = element
}
if (hovered.length) {
document.getElementById("timeControlsSlider").blur()
closeObjectsListButton.classList.remove("d-none")
if ((objectsContainer.scrollHeight > objectsContainer.clientHeight) && !tapped) {
objectsListOverflowNotice.classList.remove("d-none")
} else {
objectsListOverflowNotice.classList.add("d-none")
}
} else {
closeObjectsListButton.classList.add("d-none")
objectsListOverflowNotice.classList.add("d-none")
entriesList.classList.remove("disableHover")
}
renderLines()
renderHighlight()
}
window.addEventListener("hashchange", updateViewFromHash)
async function updateViewFromHash() {
const hash = window.location.hash.substring(1); //Remove hash prefix
let [hashEntryId, hashPeriod, hashX, hashY, hashZoom] = hash.split('/')
// Handle zzz and 0.. prefix
let newId = hashEntryId.replace(/^zzz([a-z0-9]{8,})$/g, "$1").replace(/^0+/, '')
if (hashEntryId !== newId) {
hashEntryId = newId
const newLocation = new URL(window.location)
newLocation.hash = '#' + [newId, hashPeriod, hashX, hashY, hashZoom].join('/')
history.replaceState({}, "", newLocation)
}
let targetPeriod, targetVariation
if (hashPeriod) {
[targetPeriod, , targetVariation] = parsePeriod(hashPeriod)
} else {
targetPeriod = defaultPeriod
targetVariation = defaultVariation
}
await updateTime(targetPeriod, targetVariation)
setView(
(isNaN(hashX) || hashX === '') ? undefined : Number(hashX),
(isNaN(hashY) || hashY === '') ? undefined : Number(hashY),
(isNaN(hashZoom) || hashZoom === '') ? undefined : Number(hashZoom)
)
if (!hashEntryId) return
// Highlight entry from hash
const entry = atlasDisplay[hashEntryId]
if (!entry) return
document.title = entry.name + " on " + pageTitle
if ((!entry.diff || entry.diff !== "delete")) {
objectEditNav.href = "./?mode=draw&id=" + hashEntryId
objectEditNav.title = "Edit " + entry.name
if (!objectEditNav.isConnected) {
showListButton.parentElement.appendChild(objectEditNav)
}
} else if (entry.diff === "delete" && document.getElementById("objectEditNav")) {
objectEditNav.remove()
}
const infoElement = createInfoBlock(entry)
objectsContainer.replaceChildren()
objectsContainer.appendChild(infoElement)
setView(
(isNaN(hashX) || hashX === '') ? entry.center[0] : Number(hashX),
(isNaN(hashY) || hashY === '') ? entry.center[1] : Number(hashY),
(isNaN(hashZoom) || hashZoom === '') ? calculateZoomFromPath(entry.path) : Number(hashZoom)
)
closeObjectsListButton.classList.remove("d-none")
entriesList.classList.add("disableHover")
hovered = [{...entry, element: infoElement}]
renderBackground(atlasDisplay)
renderHighlight(atlasDisplay)
renderLines()
}
function calculateZoomFromPath(path) {
let zoom
let boundingBox = [canvasSize.x + canvasOffset.x, canvasOffset.x, canvasSize.y + canvasOffset.y, canvasOffset.y]
path?.forEach(([x, y]) => {
boundingBox[0] = Math.min(boundingBox[0], x)
boundingBox[1] = Math.max(boundingBox[1], x)
boundingBox[2] = Math.min(boundingBox[2], y)
boundingBox[3] = Math.max(boundingBox[3], y)
})
const boundingBoxSize = [boundingBox[1] - boundingBox[0], boundingBox[3] - boundingBox[2]]
const clientSize = [
Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0),
Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
]
zoom = Math.min(clientSize[0] / boundingBoxSize[0], clientSize[1] / boundingBoxSize[1])
zoom = Math.min(4, zoom/2)
return zoom
}
function initView() {
updateAtlas()
document.addEventListener('timeupdate', () => {
updateAtlas()
})
// parse linked atlas entry id from link hash
/*if (window.location.hash.substring(3)){
zoom = 4
applyView()
updateViewFromHash()
}*/
applyView()
renderLines()
}
function initExplore() {
window.updateHovering = updateHovering
window.renderHighlight = () => { }
function updateHovering(e, tapped) {
if (dragging || (fixed && !tapped)) return
updateCoordsDisplay(e)
}
renderBackground({})
applyView()
}
function initGlobal() {
container.addEventListener("mousemove", e => {
if (e.sourceCapabilities) {
if (!e.sourceCapabilities.firesTouchEvents) {
updateHovering(e)
}
} else {
updateHovering(e)
}
})
document.addEventListener('timeupdate', () => {
updateHash()
})
}
function initViewGlobal() {
container.addEventListener("mousedown", e => {
lastPos = [
e.clientX,
e.clientY
]
})
container.addEventListener("touchstart", e => {
if (e.touches.length === 1) {
lastPos = [
e.touches[0].clientX,
e.touches[0].clientY
]
}
}, { passive: true })
container.addEventListener("mouseup", e => {
if (Math.abs(lastPos[0] - e.clientX) + Math.abs(lastPos[1] - e.clientY) <= 4) {
toggleFixed(e)
}
})
container.addEventListener("touchend", e => {
e.preventDefault()
if (e.changedTouches.length !== 1) return
e = e.changedTouches[0]
if (Math.sqrt(Math.pow(lastPos[0] - e.clientX, 2) + Math.pow(lastPos[1] - e.clientY, 2)) < 10)
setTimeout(() => updateHovering(e, true), 0)
dragging = false
fixed = false
})
if (window.location.hash) { // both "/" and just "/#" will be an empty hash string
updateViewFromHash()
}
document.addEventListener('timeupdate', event => {
drawButton.href = "./?mode=draw" + formatHash(null, event.detail.period, event.detail.variation)
})
document.addEventListener("mouseleave", () => {
if (!fixed) clearObjectsList()
})
}
async function loadTemplateData(initUrl, datas, blacklistUrls, level = 0) {
datas ??= {}
blacklistUrls ??= new Set()
if (datas[initUrl] || blacklistUrls.has(initUrl)) return [ datas, blacklistUrls ]
datas[initUrl] = {}
try {
const data = await (await fetch(initUrl)).json()
datas[initUrl] = data
for (const blacklisted of data?.blacklist) {
blacklistUrls.add(blacklisted.url)
}
await Promise.all(data?.whitelist.map(async wl => {
const [ wlDatas, wlBlacklistUrls ] = await loadTemplateData(wl.url, datas, blacklistUrls, level + 1)
Object.assign(datas, wlDatas)
blacklistUrls.add(...wlBlacklistUrls)
}))
} catch (e) {}
return [ datas, [...blacklistUrls] ]
}
async function loadTemplateImages(datas) {
const templates = []
for (const data of Object.values(datas)) {
if (!data?.templates) continue
for (const template of data?.templates) {
templates.push(template)
}
}
await Promise.all(templates.map(async (template, i) => {
if (!template.sources) return
for (const source of template.sources) {
try {
const sourceResponse = await (await fetch(source)).blob()
template.blob = URL.createObjectURL(sourceResponse)
break
} catch (e) {}
}
delete template.sources
if (!template.blob) return
const imageLayer = new Image()
await new Promise(resolve => {
imageLayer.onload = () => {
template.imageLayer = imageLayer
delete template.blob
resolve()
}
imageLayer.onerror = () => {
delete template
resolve()
}
imageLayer.src = template.blob
})
}))
for (const layer of templates) {
if (!layer.imageLayer) delete layer
}
return templates
}