From b6d39f7e3909dd8585bfe8bfd3c6167b19423add Mon Sep 17 00:00:00 2001 From: Hans5958 Date: Wed, 13 Sep 2023 19:16:08 +0700 Subject: [PATCH] Sync codebase Ref: https://github.com/placeAtlas/atlas-2023/commit/f65cafb8d1d74d1dbc2d724e36e3316ba3017891 --- web/_css/style.css | 8 + web/_js/main/draw.js | 68 ++-- web/_js/main/infoblock.js | 35 +- web/_js/main/main.js | 82 ++-- web/_js/main/overlap.js | 16 +- web/_js/main/time.js | 150 ++++++-- web/_js/main/view.js | 359 +++++++++--------- web/about.html | 2 + .../Put the image overrides in this directory | 0 web/index.html | 34 +- web/manifest.webmanifest | 6 +- web/sw.js | 2 +- 12 files changed, 424 insertions(+), 338 deletions(-) delete mode 100644 web/imageOverrides/Put the image overrides in this directory diff --git a/web/_css/style.css b/web/_css/style.css index ecb4c949..5ddf075d 100644 --- a/web/_css/style.css +++ b/web/_css/style.css @@ -52,6 +52,14 @@ .offcanvas { padding-left: env(safe-area-inset-left); } +.opacity-0 { + opacity: 0; +} + +.transition-opacity { + transition: opacity .15s linear; +} + @supports (backdrop-filter: blur()) or (-webkit-backdrop-filter: blur()) { .navbar, .offcanvas { -webkit-backdrop-filter: saturate(180%) blur(15px); diff --git a/web/_js/main/draw.js b/web/_js/main/draw.js index 40aef13c..dfb98277 100644 --- a/web/_js/main/draw.js +++ b/web/_js/main/draw.js @@ -69,7 +69,7 @@ const periodClipboard = { const drawBackButton = document.createElement("a") drawBackButton.className = "btn btn-outline-primary" drawBackButton.id = "drawBackButton" -drawBackButton.textContent = "Exit Draw Mode" +drawBackButton.textContent = "Exit Drawing" const baseInputAddon = document.createElement("span") baseInputAddon.className = "input-group-text" @@ -106,12 +106,13 @@ function initDraw() { showListButton.insertAdjacentHTML("afterend", '') showListButton.parentElement.appendChild(drawBackButton) showListButton.remove() - + drawButton.remove() + // Opens draw menu wrapper.classList.remove('listHidden') bsOffcanvasDraw.show() - window.render = render + window.renderHighlight = renderHighlight window.renderBackground = renderBackground window.updateHovering = updateHovering @@ -125,13 +126,18 @@ function initDraw() { let highlightUncharted = highlightUnchartedEl.checked - renderBackground(atlas) + window.updateAtlas = updateAtlas + + updateAtlas() + + document.addEventListener('timeupdate', () => { + updateAtlas() + }) + applyView() container.style.cursor = "crosshair" - render(path) - container.addEventListener("mousedown", e => { lastPos = [ e.clientX, @@ -168,7 +174,7 @@ function initDraw() { const coords = getCanvasCoords(e.clientX, e.clientY) path.push(coords) - render(path) + renderHighlight(path) undoHistory = [] redoButton.disabled = true @@ -185,13 +191,13 @@ function initDraw() { container.addEventListener("mousemove", e => { if (!dragging && drawing && path.length > 0) { const coords = getCanvasCoords(e.clientX, e.clientY) - render([...path, coords]) + renderHighlight([...path, coords]) } }) container.addEventListener("mouseout", function () { if (!dragging && drawing && path.length > 0) { - render(path) + renderHighlight(path) } }) @@ -228,19 +234,19 @@ function initDraw() { undoButton.addEventListener("click", e => { undo() const coords = getCanvasCoords(e.clientX, e.clientY) - render([...path, coords]) + renderHighlight([...path, coords]) }) redoButton.addEventListener("click", e => { redo() const coords = getCanvasCoords(e.clientX, e.clientY) - render([...path, coords]) + renderHighlight([...path, coords]) }) resetButton.addEventListener("click", e => { reset() const coords = getCanvasCoords(e.clientX, e.clientY) - render([...path, coords]) + renderHighlight([...path, coords]) }) resetButton.addEventListener("blur", function () { @@ -269,7 +275,7 @@ function initDraw() { highlightUnchartedEl.addEventListener("click", function () { highlightUncharted = this.checked - render(path) + renderHighlight(path) }) function generateExportObject() { @@ -385,13 +391,11 @@ function initDraw() { } githubPostButton.href = githubPostUrl - console.log(githubPostUrl) - exportModal.show() } function preview() { - let infoElement = createInfoBlock(generateExportObject(), true) + let infoElement = createInfoBlock(generateExportObject(), 2) objectsContainer.replaceChildren() objectsContainer.appendChild(infoElement) closeObjectsListButton.classList.remove("d-none") @@ -483,17 +487,17 @@ function initDraw() { closeObjectsListButton.classList.add("d-none") } - function renderBackground() { + function renderBackground(atlas) { backgroundContext.clearRect(0, 0, highlightCanvas.width, highlightCanvas.height) backgroundContext.fillStyle = "rgba(0, 0, 0, 1)" - //backgroundContext.fillRect(0, 0, canvas.width, canvas.height) + //backgroundContext.fillRect(0, 0, canvas.width, renderBackgroundcanvas.height) - for (let i = 0; i < atlas.length; i++) { - - const path = atlas[i].path + for (const entry of Object.values(atlas)) { + const path = entry.path + backgroundContext.beginPath() if (path[0]) { @@ -510,7 +514,7 @@ function initDraw() { } } - function render(path) { + function renderHighlight(path) { if (!Array.isArray(path)) return @@ -553,10 +557,16 @@ function initDraw() { updateCoordsDisplay(e) } + function updateAtlas() { + ;[atlas, atlasOrder] = filterAtlas(atlasAll) + ;[atlasDisplay, atlasOrder] = generateAtlasDisplay(atlas, atlasOrder, currentPeriod, currentVariation) + renderBackground(atlasDisplay) + renderHighlight(atlasDisplay) + } + const getEntry = id => { if (!id) return - const entries = atlasAll.filter(entry => entry.id.toString() === id.toString()) - if (entries.length === 1) return entries[0] + return atlasAll[id] } function addFieldButton(inputButton, inputGroup, array, index, name) { @@ -790,7 +800,7 @@ function initDraw() { } else { document.getElementById("offcanvasDrawLabel").textContent = "New Entry" - pathWithPeriods.push([formatPeriod(currentPeriod, currentPeriod, currentVariation), []]) + pathWithPeriods.push([formatPeriod(currentPeriod, null, currentVariation), []]) // Builds multi-input list addWebsiteFields("", 0, [0]) @@ -811,19 +821,19 @@ function initDraw() { ) document.addEventListener('timeupdate', () => { - renderBackground(atlas) + renderBackground(atlasDisplay) updatePeriodGroups() }) periodsAdd.addEventListener('click', () => { - pathWithPeriods.push([formatPeriod(currentPeriod, currentPeriod, currentVariation), []]) + pathWithPeriods.push([formatPeriod(currentPeriod, null, currentVariation), []]) initPeriodGroups() }) drawBackButton.href = "./" + formatHash(entry?.id) document.addEventListener('timeupdate', event => { - drawBackButton.href = "./" + formatHash(entry?.id, event.detail.period, event.detail.period, event.detail.variation) + drawBackButton.href = "./" + formatHash(entry?.id, event.detail.period, event.detail.variation) }) } @@ -1224,7 +1234,7 @@ function updatePeriodGroups() { function updatePath(newPath, newUndoHistory) { path = newPath || path if (path.length > 3) center = calculateCenter(path) - render(path) + renderHighlight(path) undoButton.disabled = path.length === 0; // Maybe make it undo the cancel action in the future undoHistory = newUndoHistory || [] redoButton.disabled = (!undoHistory.length) diff --git a/web/_js/main/infoblock.js b/web/_js/main/infoblock.js index c36d4b6b..2a642dca 100644 --- a/web/_js/main/infoblock.js +++ b/web/_js/main/infoblock.js @@ -31,7 +31,10 @@ function createInfoListItem(name, value) { return entryInfoListElement } -function createInfoBlock(entry, isPreview) { +// mode 0 = normal +// mode 1 = entry list but none on atlas +// mode 2 = preview +function createInfoBlock(entry, mode = 0) { const element = document.createElement("div") element.className = "card mb-2 overflow-hidden shadow" @@ -40,21 +43,31 @@ function createInfoBlock(entry, isPreview) { const linkElement = document.createElement("a") linkElement.className = "text-decoration-none d-flex justify-content-between text-body" - if (isPreview) linkElement.href = "#" - else { - linkElement.href = formatHash(entry.id, null, null, null, false, false, false) - linkElement.addEventListener('click', e => { + + let nearestPeriod = currentPeriod + let nearestVariation = currentVariation + if (!atlasDisplay[entry.id]) { + [nearestPeriod, nearestVariation] = getNearestPeriod(entry, currentPeriod, currentVariation) + } + + if (mode === 2) { + linkElement.href = "#" + } else { + const hash = formatHash(entry.id, nearestPeriod, nearestVariation, false, false, false) + linkElement.href = hash + if (mode === 0) linkElement.addEventListener('click', e => { e.preventDefault() - location.hash = formatHash(entry.id, null, null, null, false, false, false) + location.hash = hash window.dispatchEvent(new HashChangeEvent("hashchange")) }) } + const linkNameElement = document.createElement("span") linkNameElement.className = "flex-grow-1 text-break" linkNameElement.textContent = entry.name headerElement.appendChild(linkElement) linkElement.appendChild(linkNameElement) - linkElement.insertAdjacentHTML("beforeend", '') + linkElement.insertAdjacentHTML("beforeend", '') element.appendChild(headerElement) const bodyElement = document.createElement("div") @@ -92,7 +105,7 @@ function createInfoBlock(entry, isPreview) { } // Entry data submitted to preview does not include center or path - if (!isPreview) { + if (mode === 0) { const [x, y] = entry?.center listElement.appendChild(createInfoListItem("Position: ", `${Math.floor(x)}, ${Math.floor(y)}`)) @@ -170,11 +183,11 @@ function createInfoBlock(entry, isPreview) { element.appendChild(idElementContainer) // Adds edit button only if element is not deleted - if (!isPreview && (!entry.diff || entry.diff !== "delete")) { + if (mode < 2 && (!entry.diff || entry.diff !== "delete")) { const editElement = document.createElement("a") - editElement.textContent = "Edit" + editElement.innerHTML = ' Edit' editElement.className = "btn btn-sm btn-outline-primary" - editElement.href = "./?mode=draw&id=" + entry.id + formatHash(false) + editElement.href = "./?mode=draw&id=" + entry.id + formatHash(false, nearestPeriod, nearestVariation, false, false, false) editElement.title = "Edit " + entry.name idElementContainer.appendChild(editElement) } diff --git a/web/_js/main/main.js b/web/_js/main/main.js index a79789b9..b4aa2a4e 100644 --- a/web/_js/main/main.js +++ b/web/_js/main/main.js @@ -26,7 +26,7 @@ const maxZoom = 128 const minZoom = 0.125 let zoomOrigin = [0, 0] -let scaleZoomOrigin = [0, 0] +let scaleZoomOrigin = [canvasCenter.x, canvasCenter.y] let dragging = false let lastPosition = [0, 0] @@ -54,7 +54,7 @@ function applyView() { } function setView(targetX, targetY, targetZoom) { - + if (isNaN(targetX)) targetX = null if (isNaN(targetY)) targetY = null @@ -72,8 +72,6 @@ function updateHash(...args) { if (location.hash !== newLocation.hash) history.replaceState({}, "", newLocation) } -let atlas = null -window.atlas = atlas let atlasAll = null window.atlasAll = atlasAll @@ -106,9 +104,9 @@ async function init() { // For Reviewing Reddit Changes // const atlasRef = '../tools/temp-atlas.json' - const atlasRef = params.get('atlas') || './atlas.json' - const atlasResp = await fetch(atlasRef) - atlasAll = updateAtlasAll(await atlasResp.json()) + const atlasAllUrl = params.get('atlas') || './atlas.json' + atlasAll = generateAtlasAll(await (await fetch(atlasAllUrl)).json()) + // console.log(atlas, atlasOrder) const hash = window.location.hash.substring(1) const [, hashPeriod, hashX, hashY, hashZoom] = hash.split('/') @@ -148,45 +146,33 @@ async function init() { initExplore() } else if (mode.startsWith("diff")) { try { - const liveAtlasRef = params.get('liveatlas') || `https://${prodDomain}/atlas.json` - const liveAtlasResp = await fetch(liveAtlasRef) - let liveAtlas = await liveAtlasResp.json() - liveAtlas = updateAtlasAll(liveAtlas) + const liveAtlasUrl = params.get('liveatlas') || `https://${prodDomain}/atlas.json` + let liveAtlasAll = generateAtlasAll(await (await fetch(liveAtlasUrl)).json()) - const liveAtlasReduced = liveAtlas.reduce((atlas, entry) => { - delete entry._index - atlas[entry.id] = entry - return atlas - }, {}) // Mark added/edited entries - atlasAll = atlasAll.map(function (entry) { - delete entry._index - if (!liveAtlasReduced[entry.id]) { + for (const entry of Object.values(atlasAll)) { + if (!liveAtlasAll[entry.id]) { entry.diff = "add" - } else if (JSON.stringify(entry) !== JSON.stringify(liveAtlasReduced[entry.id])) { + } else { + if (JSON.stringify({ ...entry, _index: undefined }) === JSON.stringify({ ...liveAtlasAll[entry.id], _index: undefined })) continue entry.diff = "edit" } - return entry - }) - - // Mark removed entries - const atlasReduced = atlasAll.reduce((atlas, entry) => { - delete entry._index - atlas[entry.id] = entry - return atlas - }, {}) - const removedEntries = liveAtlas.filter(entry => !atlasReduced[entry.id]).map(entry => { - delete entry._index - entry.diff = "delete" - return entry - }) - atlasAll.push(...removedEntries) - - if (mode.includes("only")) { - atlasAll = atlasAll.filter(entry => entry.diff) } - atlas = generateAtlasForPeriod() + // Mark removed entries + for (const entry of Object.values(liveAtlasAll)) { + if (!atlasAll[entry.id]) { + entry.diff = "delete" + atlasAll[entry.id] = entry + } + } + + if (mode.includes('only')) { + for (const key of Object.keys(atlasAll)) { + if (atlasAll[key].diff) continue + delete atlasAll[key] + } + } } catch (error) { console.warn("Diff mode failed to load, reverting to normal view.", error) @@ -255,7 +241,7 @@ async function init() { zoom = 1 zoomOrigin = [0, 0] scaleZoomOrigin = [0, 0] - updateLines() + renderLines() applyView() }) @@ -384,7 +370,6 @@ async function init() { } window.addEventListener("mousemove", e => { - // updateLines() mousemove(e.clientX, e.clientY) if (dragging) { e.preventDefault() @@ -417,7 +402,7 @@ async function init() { scaleZoomOrigin[0] += deltaX / zoom scaleZoomOrigin[1] += deltaY / zoom - updateLines() + renderLines() applyView() } @@ -460,7 +445,7 @@ async function init() { zoomOrigin[1] = scaleZoomOrigin[1] * zoom applyView() - updateLines() + renderLines() } window.addEventListener("mouseup", e => { @@ -478,7 +463,7 @@ async function init() { }) window.addEventListener("touchend", touchend) - function mouseup(x, y) { + function mouseup() { dragging = false updateHash() } @@ -486,7 +471,7 @@ async function init() { function touchend(e) { if (e.touches.length === 0) { mouseup() - setTimeout(() => updateLines(), 0) + renderLines() dragging = false } else if (e.touches.length === 1) { @@ -506,7 +491,8 @@ async function init() { } -function updateAtlasAll(atlas = atlasAll) { +function generateAtlasAll(atlas = atlasAll) { + const newAtlas = {} for (const index in atlas) { const entry = atlas[index] entry._index = index @@ -528,8 +514,10 @@ function updateAtlasAll(atlas = atlasAll) { } entry.path = currentPath entry.center = currentCenter + newAtlas[entry.id] = entry } - return atlas + // console.log(newAtlas) + return newAtlas } // Announcement system diff --git a/web/_js/main/overlap.js b/web/_js/main/overlap.js index bb534ed5..60099edb 100644 --- a/web/_js/main/overlap.js +++ b/web/_js/main/overlap.js @@ -9,22 +9,16 @@ function initOverlap() { window.renderBackground = renderBackground + updateAtlas() + // const hovered = [] - resetEntriesList() - renderBackground(atlas) - render() - document.addEventListener('timeupdate', () => { - atlasDisplay = atlas.slice() - resetEntriesList() - renderBackground(atlasDisplay) - render() + updateAtlas() }) applyView() - render() - updateLines() + renderLines() if (window.location.hash) { updateViewFromHash() @@ -37,7 +31,7 @@ function initOverlap() { backgroundContext.fillStyle = "rgba(255, 255, 255, 1)" backgroundContext.fillRect(0, 0, highlightCanvas.width, highlightCanvas.height) - for (const entry of atlas) { + for (const entry of Object.values(atlas)) { const path = entry.path diff --git a/web/_js/main/time.js b/web/_js/main/time.js index 09e7cfd8..7af07693 100644 --- a/web/_js/main/time.js +++ b/web/_js/main/time.js @@ -32,6 +32,9 @@ let currentPeriod = defaultPeriod window.currentVariation = currentVariation window.currentPeriod = currentPeriod +let atlasDisplay = {} +window.atlasDisplay = atlasDisplay + // SETUP if (variationsConfig[currentVariation].versions.length === 1) bottomBar.classList.add('no-time-slider') @@ -70,7 +73,7 @@ const dispatchTimeUpdateEvent = (period = currentPeriod, variation = currentVari detail: { period: period, variation: variation, - periodString: formatPeriod(period, period, variation), + periodString: formatPeriod(period, null, variation), atlas: atlas } }) @@ -79,7 +82,7 @@ const dispatchTimeUpdateEvent = (period = currentPeriod, variation = currentVari async function updateBackground(newPeriod = currentPeriod, newVariation = currentVariation) { abortController.abort() - myAbortController = new AbortController() + const myAbortController = new AbortController() abortController = myAbortController currentUpdateIndex++ const myUpdateIndex = currentUpdateIndex @@ -96,7 +99,7 @@ async function updateBackground(newPeriod = currentPeriod, newVariation = curren variantsEl.parentElement.classList.remove('input-group') } - const configObject = variationConfig.versions[currentPeriod] + const configObject = variationConfig.versions[newPeriod] let layerUrls = [] let layers = [] @@ -110,39 +113,59 @@ async function updateBackground(newPeriod = currentPeriod, newVariation = curren layers.length = layerUrls.length await Promise.all(layerUrls.map(async (url, i) => { - const imageBlob = await (await fetch(url, { signal: myAbortController.signal })).blob() - const imageLayer = new Image() - await new Promise(resolve => { - imageLayer.onload = () => { - context.canvas.width = Math.max(imageLayer.width, context.canvas.width) - context.canvas.height = Math.max(imageLayer.height, context.canvas.height) - layers[i] = imageLayer - resolve() - } - imageLayer.src = URL.createObjectURL(imageBlob) - }) + try { + const imageBlob = await (await fetch(url, { signal: myAbortController.signal })).blob() + const imageLayer = new Image() + await new Promise(resolve => { + imageLayer.onload = () => { + context.canvas.width = Math.max(imageLayer.width, context.canvas.width) + context.canvas.height = Math.max(imageLayer.height, context.canvas.height) + layers[i] = imageLayer + resolve() + } + imageLayer.src = URL.createObjectURL(imageBlob) + }) + } catch (e) { + const aborted = myAbortController.signal.aborted + if (!aborted) throw e + } })) - if (myAbortController.signal.aborted || newPeriod !== currentPeriod || newVariation !== currentVariation) { - return + if (currentUpdateIndex !== myUpdateIndex) { + return false } for (const imageLayer of layers) { context.drawImage(imageLayer, 0, 0) } - - if (currentUpdateIndex !== myUpdateIndex) return [configObject, newPeriod, newVariation] const blob = await new Promise(resolve => canvas.toBlob(resolve)) canvasUrl = URL.createObjectURL(blob) image.src = canvasUrl + + return true + } +let loadingTimeout = setTimeout(() => {}, 0) + async function updateTime(newPeriod = currentPeriod, newVariation = currentVariation, forceLoad = false) { - if (newPeriod === currentPeriod && !forceLoad) { - return; + if (newPeriod === currentPeriod && newVariation === currentVariation && !forceLoad) { + return } document.body.dataset.canvasLoading = "" + const loadingEl = document.getElementById("loading") + const previouslyHidden = loadingEl.classList.contains("d-none") + + if (previouslyHidden) loadingEl.classList.add("opacity-0", "transition-opacity") + clearTimeout(loadingTimeout) + loadingTimeout = setTimeout(() => { + loadingEl.classList.remove("d-none") + if (previouslyHidden) setTimeout(() => { + loadingEl.classList.remove("opacity-0") + }, 0) + }, 2000) + // const oldPeriod = currentPeriod const oldVariation = currentVariation @@ -167,12 +190,16 @@ async function updateTime(newPeriod = currentPeriod, newVariation = currentVaria timelineSlider.value = currentPeriod updateTooltip(newPeriod, newVariation) - await updateBackground(newPeriod, newVariation) + const updateBackgroundResult = await updateBackground(newPeriod, newVariation) - atlas = generateAtlasForPeriod(newPeriod, newVariation) + if (!updateBackgroundResult) return dispatchTimeUpdateEvent(newPeriod, newVariation, atlas) delete document.body.dataset.canvasLoading + clearTimeout(loadingTimeout) + document.getElementById("loading").classList.add("d-none") + document.getElementById("loading").classList.remove("opacity-0", "opacity-100", "transition-opacity") + tooltip.dataset.forceVisible = "" clearTimeout(tooltipDelayHide) tooltipDelayHide = setTimeout(() => { @@ -181,24 +208,30 @@ async function updateTime(newPeriod = currentPeriod, newVariation = currentVaria } -function generateAtlasForPeriod(newPeriod = currentPeriod, newVariation = currentVariation) { +function generateAtlasDisplay(prevAtlas, prevAtlasOrder, newPeriod = currentPeriod, newVariation = currentVariation) { + + const newAtlas = {} + const newAtlasOrderDisplayed = [] + const newAtlasOrderNotDisplayed = [] + + for (const id of prevAtlasOrder) { + + newAtlasOrderNotDisplayed.push(id) + const entry = prevAtlas[id] - const atlas = [] - for (const entry of atlasAll) { let chosenIndex const validPeriods2 = Object.keys(entry.path) - for (const i in validPeriods2) { + periodCheck: for (const i in validPeriods2) { const validPeriods = validPeriods2[i].split(', ') for (const j in validPeriods) { const [start, end, variation] = parsePeriod(validPeriods[j]) if (isOnPeriod(start, end, variation, newPeriod, newVariation)) { chosenIndex = i - break + break periodCheck } } - if (chosenIndex !== undefined) break } if (chosenIndex === undefined) continue @@ -207,14 +240,18 @@ function generateAtlasForPeriod(newPeriod = currentPeriod, newVariation = curren if (pathChosen === undefined) continue - atlas.push({ + newAtlas[id] = { ...entry, path: pathChosen, center: centerChosen, - }) + } + + newAtlasOrderNotDisplayed.pop() + newAtlasOrderDisplayed.push(id) + } - return atlas + return [newAtlas, [...newAtlasOrderDisplayed, ...newAtlasOrderNotDisplayed]] } @@ -273,7 +310,7 @@ function parsePeriod(periodString) { function formatPeriod(targetStart, targetEnd, targetVariation, forUrl = false) { targetStart ??= currentPeriod - targetEnd ??= currentPeriod + targetEnd ??= targetStart targetVariation ??= currentVariation let periodString, variationString @@ -297,12 +334,11 @@ function setReferenceVal(reference, newValue) { else return reference ?? newValue } -function formatHash(targetEntry, targetPeriodStart, targetPeriodEnd, targetVariation, targetX, targetY, targetZoom) { +function formatHash(targetEntry, targetPeriod, targetVariation, targetX, targetY, targetZoom) { let hashData = window.location.hash.substring(1).split('/') targetEntry = setReferenceVal(targetEntry, hashData[0]) - targetPeriodStart = setReferenceVal(targetPeriodStart, currentPeriod) - targetPeriodEnd = setReferenceVal(targetPeriodEnd, currentPeriod) + targetPeriod = setReferenceVal(targetPeriod, currentPeriod) targetVariation = setReferenceVal(targetVariation, currentVariation) targetX = setReferenceVal(targetX, canvasCenter.x - scaleZoomOrigin[0]) targetY = setReferenceVal(targetY, canvasCenter.y - scaleZoomOrigin[1]) @@ -313,8 +349,8 @@ function formatHash(targetEntry, targetPeriodStart, targetPeriodEnd, targetVaria if (targetZoom) targetZoom = targetZoom.toFixed(3).replace(/\.?0+$/, '') const result = [targetEntry] - const targetPeriod = formatPeriod(targetPeriodStart, targetPeriodEnd, targetVariation, true) - result.push(targetPeriod, targetX, targetY, targetZoom) + const targetPeriodFormat = formatPeriod(targetPeriod, null, targetVariation, true) + result.push(targetPeriodFormat, targetX, targetY, targetZoom) if (!result.some(el => el || el === 0)) return '' return '#' + result.join('/').replace(/\/+$/, '') } @@ -327,4 +363,42 @@ function downloadCanvas() { document.body.appendChild(linkEl) linkEl.click() document.body.removeChild(linkEl) -} \ No newline at end of file +} + +function getNearestPeriod(entry, targetPeriod, targetVariation) { + + const pathKeys = Object.keys(entry.path) + + let nearestScore, nearestPeriod, nearestVariation, nearestKey + + function updateNearest(newScore, newPeriod, newVariation, newKey) { + if (newScore >= nearestScore) return + nearestScore = newScore + nearestPeriod = newPeriod + nearestVariation = newVariation + nearestKey = newKey + } + + checkPaths: for (const pathKey of pathKeys) { + const pathPeriods = pathKey.split(', ') + + checkPathPeriod: for (const j in pathPeriods) { + const [pathStart, pathEnd, pathVariation] = parsePeriod(pathPeriods[j]) + if (isOnPeriod(pathStart, pathEnd, pathVariation, targetPeriod, targetVariation)) { + updateNearest(0, targetPeriod, targetVariation) + break checkPaths + } else if (pathVariation !== targetVariation) { + updateNearest(Infinity, pathStart, pathVariation, pathKey) + continue checkPathPeriod + } else if (Math.abs(pathStart - targetPeriod) < Math.abs(pathEnd - targetPeriod)) { + updateNearest(Math.abs(pathStart - targetPeriod), pathStart, pathVariation, pathKey) + } else { + updateNearest(Math.abs(pathEnd - targetPeriod), pathStart, pathVariation, pathKey) + } + } + + } + + return [ nearestPeriod, nearestVariation, nearestKey ] + +} diff --git a/web/_js/main/view.js b/web/_js/main/view.js index 737f122e..ee9734b1 100644 --- a/web/_js/main/view.js +++ b/web/_js/main/view.js @@ -43,7 +43,11 @@ objectEditNav.className = "btn btn-outline-primary" objectEditNav.id = "objectEditNav" objectEditNav.textContent = "Edit" -let atlasDisplay +let atlas = null +window.atlas = atlas + +let atlasOrder = [] +window.atlasOrder = atlasOrder const entriesLimit = 50 let entriesOffset = 0 @@ -72,11 +76,11 @@ let lastPos = [0, 0] let fixed = false; // Fix hovered items in place, so that clicking on links is possible searchInput.addEventListener("input", function () { - resetEntriesList() + updateAtlas() }) sortInput.addEventListener("input", function () { - resetEntriesList() + updateAtlas() }) offcanvasDraw.addEventListener('show.bs.offcanvas', () => { @@ -112,8 +116,8 @@ offcanvasList.addEventListener('shown.bs.offcanvas', e => { wrapper.classList.remove('listTransitioning') updateHovering(e) applyView() - render() - updateLines() + renderHighlight() + renderLines() }) offcanvasList.addEventListener('hide.bs.offcanvas', () => { @@ -127,8 +131,8 @@ offcanvasList.addEventListener('hidden.bs.offcanvas', e => { wrapper.classList.remove('listTransitioning') updateHovering(e) applyView() - render() - updateLines() + renderHighlight() + renderLines() }) closeObjectsListButton.addEventListener("click", clearObjectsList) @@ -138,52 +142,52 @@ bottomBar.addEventListener("mouseover", () => { }) function clearObjectsList() { + hovered = [] + fixed = false + renderLines() + renderHighlight() + document.title = pageTitle closeObjectsListButton.classList.add("d-none") objectsListOverflowNotice.classList.add("d-none") entriesList.classList.remove("disableHover") - hovered = [] objectsContainer.replaceChildren() - updateLines() - fixed = false - render() objectEditNav.remove() updateHash(false) - document.title = pageTitle } function toggleFixed(e, tapped) { if (!fixed && hovered.length === 0) { entriesList.classList.remove("disableHover") - return 0 + return } fixed = !fixed if (!fixed) { updateHovering(e, tapped) - render() + renderHighlight() } entriesList.classList.add("disableHover") objectsListOverflowNotice.classList.add("d-none") } -window.addEventListener("resize", updateLines) -window.addEventListener("mousemove", updateLines) -window.addEventListener("dblClick", updateLines) -window.addEventListener("wheel", updateLines) +window.addEventListener("dblClick", renderLines) +window.addEventListener("wheel", renderLines) objectsContainer.addEventListener("scroll", () => { - updateLines() + renderLines() }) window.addEventListener("resize", () => { - applyView() - render() - updateLines() + renderHighlight() + renderLines() }) -function updateLines() { - +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 @@ -260,9 +264,9 @@ function renderBackground(atlas) { backgroundContext.fillStyle = "rgba(0, 0, 0, 0.6)" backgroundContext.fillRect(0, 0, backgroundCanvas.width, backgroundCanvas.height) - for (let i = 0; i < atlas.length; i++) { + for (const entry of Object.values(atlas)) { - const path = atlas[i].path + const path = entry.path backgroundContext.beginPath() @@ -279,7 +283,7 @@ function renderBackground(atlas) { backgroundContext.closePath() let bgStrokeStyle - switch (atlas[i].diff) { + switch (entry.diff) { case "add": bgStrokeStyle = "rgba(0, 255, 0, 1)" backgroundContext.lineWidth = 2 @@ -299,45 +303,39 @@ function renderBackground(atlas) { backgroundContext.strokeStyle = bgStrokeStyle backgroundContext.stroke() backgroundContext.lineWidth = 1 + + } + } -function buildObjectsList(filter, sort) { +function filterAtlas(prevAtlas) { - atlasDisplay = atlas.slice() + const sort = sortInput.value || defaultSort + const search = searchInput?.value.toLowerCase() + let newAtlas = Object.assign({}, prevAtlas) + let newAtlasOrder = [] - if (filter) { - atlasDisplay = atlas.filter(entry => { - return ( - entry.name.toLowerCase().includes(filter.toLowerCase()) - || entry.description?.toLowerCase().includes(filter.toLowerCase()) - || Object.values(entry.links).flat().some(str => str.toLowerCase().includes(filter)) - || entry.id.toString() === filter - ) - }) - document.getElementById("atlasSize").innerHTML = "Found " + atlasDisplay.length + " entries." - } else { - document.getElementById("atlasSize").innerHTML = "The Atlas contains " + atlasDisplay.length + " entries." + 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] + } } - renderBackground(atlasDisplay) - render() - - sort ||= defaultSort - document.getElementById("sort").value = sort - - //console.log(sort) + // document.getElementById("sort").value = sort let sortFunction - //console.log(sort) - switch (sort) { case "shuffle": - sortFunction = null - if (entriesOffset === 0) { - shuffle() - } + sortFunction = () => Math.random() - 0.5 break case "alphaAsc": sortFunction = (a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()) @@ -369,10 +367,37 @@ function buildObjectsList(filter, sort) { break } + newAtlasOrder = Object.keys(newAtlas) if (sortFunction) { - atlasDisplay.sort(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 = () => { @@ -380,89 +405,92 @@ function buildObjectsList(filter, sort) { entriesList.removeChild(moreEntriesButton) } - for (let i = entriesOffset; i < entriesOffset + entriesLimit; i++) { + let entriesLeft = entriesLimit + let element - if (i >= atlasDisplay.length) break + while (entriesLeft > 0 && atlasOrder.length > entriesOffset) { + + if (atlasDisplay[atlasOrder[entriesOffset]]) { + // console.log(i, entriesLeft) - const element = createInfoBlock(atlasDisplay[i]) - const entry = atlasDisplay[i] - - element.addEventListener("mouseenter", function () { - if (fixed || dragging) return - objectsContainer.replaceChildren() - - previousScaleZoomOrigin ??= [...scaleZoomOrigin] - previousZoom ??= zoom - setView(entry.center[0], entry.center[1], setZoomByPath(entry.path)) - - hovered = [entry] - render() - hovered[0].element = this - updateLines() - - }) - - element.addEventListener("click", e => { - toggleFixed(e) - if (!fixed) return - previousScaleZoomOrigin ??= [...scaleZoomOrigin] - previousZoom ??= zoom - applyView() - }) - - element.addEventListener("mouseleave", () => { - if (fixed || dragging) return - - scaleZoomOrigin = [...previousScaleZoomOrigin] - zoom = previousZoom - previousScaleZoomOrigin = undefined - previousZoom = undefined - applyView() - - hovered = [] - updateLines() - render() - }) + 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) } - entriesOffset += entriesLimit - - if (atlasDisplay.length > entriesOffset) { - moreEntriesButton.innerHTML = "Show " + Math.min(entriesLimit, atlasDisplay.length - entriesOffset) + " more" + if (atlasOrder.length > entriesOffset) { + moreEntriesButton.innerHTML = "Show " + Math.min(entriesLimit, atlasOrder.length - entriesOffset) + " more" entriesList.appendChild(moreEntriesButton) } } moreEntriesButton.addEventListener('click', showMoreEntries) showMoreEntries() - } -function shuffle() { - //console.log("shuffled atlas") - for (let i = atlasDisplay.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)) - const temp = atlasDisplay[i] - atlasDisplay[i] = atlasDisplay[j] - atlasDisplay[j] = temp - } -} - -function resetEntriesList() { - entriesOffset = 0 - entriesList.replaceChildren() - entriesList.appendChild(moreEntriesButton) - - const sort = sortInput.value || defaultSort - const search = searchInput?.value.toLowerCase() - - buildObjectsList(search, sort) -} - -async function render() { +async function renderHighlight() { highlightContext.clearRect(0, 0, highlightCanvas.width, highlightCanvas.height) @@ -478,10 +506,8 @@ async function render() { container.style.cursor = "default" } - for (let i = 0; i < hovered.length; i++) { - const path = hovered[i].path highlightContext.beginPath() @@ -507,28 +533,6 @@ async function render() { highlightContext.globalCompositeOperation = "source-out" highlightContext.drawImage(backgroundCanvas, 0, 0) - if (hovered.length === 1 && hovered[0].path.length && hovered[0].overrideImage) { - const undisputableHovered = hovered[0] - // Find the left-topmost point of all the paths - const entryPosition = getPositionOfEntry(undisputableHovered) - if (entryPosition) { - const [startX, startY] = entryPosition - const overrideImage = new Image() - const loadingPromise = new Promise((res, rej) => { - overrideImage.onerror = rej - overrideImage.onload = res - }) - overrideImage.src = "imageOverrides/" + undisputableHovered.overrideImage - try { - await loadingPromise - highlightContext.globalCompositeOperation = "source-over" - highlightContext.drawImage(overrideImage, startX, startY) - } catch (ex) { - console.error("Cannot override image.", ex) - } - } - } - for (let i = 0; i < hovered.length; i++) { const path = hovered[i].path @@ -592,11 +596,15 @@ function updateHovering(e, tapped) { 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 - const newHovered = [] - for (const entry of atlasDisplay) { + 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) { @@ -612,9 +620,7 @@ function updateHovering(e, tapped) { if (!changed) return - hovered = newHovered.sort(function (a, b) { - return calcPolygonArea(a.path) - calcPolygonArea(b.path) - }) + hovered = newHovered objectsContainer.replaceChildren() @@ -639,12 +645,13 @@ function updateHovering(e, tapped) { objectsListOverflowNotice.classList.add("d-none") entriesList.classList.remove("disableHover") } - render() + renderLines() + renderHighlight() } window.addEventListener("hashchange", updateViewFromHash) -function updateViewFromHash() { +async function updateViewFromHash() { const hash = window.location.hash.substring(1); //Remove hash prefix let [hashEntryId, hashPeriod, hashX, hashY, hashZoom] = hash.split('/') @@ -666,7 +673,7 @@ function updateViewFromHash() { targetPeriod = defaultPeriod targetVariation = defaultVariation } - updateTime(targetPeriod, targetVariation, true) + await updateTime(targetPeriod, targetVariation) setView( (isNaN(hashX) || hashX === '') ? undefined : Number(hashX), @@ -678,13 +685,8 @@ function updateViewFromHash() { // Highlight entry from hash - const entries = atlas.filter(e => { - return e.id.toString() === hashEntryId - }) - - if (entries.length !== 1) return - - const entry = entries[0] + const entry = atlasDisplay[hashEntryId] + if (!entry) return document.title = entry.name + " on " + pageTitle @@ -702,24 +704,24 @@ function updateViewFromHash() { objectsContainer.replaceChildren() objectsContainer.appendChild(infoElement) - renderBackground(atlas) setView( (isNaN(hashX) || hashX === '') ? entry.center[0] : Number(hashX), (isNaN(hashY) || hashY === '') ? entry.center[1] : Number(hashY), - (isNaN(hashZoom) || hashZoom === '') ? setZoomByPath(entry.path) : Number(hashZoom) + (isNaN(hashZoom) || hashZoom === '') ? calculateZoomFromPath(entry.path) : Number(hashZoom) ) closeObjectsListButton.classList.remove("d-none") entriesList.classList.add("disableHover") - hovered = [entry] - render() - hovered[0].element = infoElement - updateLines() + hovered = [{...entry, element: infoElement}] + renderBackground(atlasDisplay) + renderHighlight(atlasDisplay) + renderLines() } -function setZoomByPath(path) { +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) @@ -741,13 +743,10 @@ function setZoomByPath(path) { function initView() { - buildObjectsList(null, null) - renderBackground(atlas) - render() - + updateAtlas() + document.addEventListener('timeupdate', () => { - atlasDisplay = atlas.slice() - resetEntriesList() + updateAtlas() }) // parse linked atlas entry id from link hash @@ -758,22 +757,20 @@ function initView() { }*/ applyView() - render() - updateLines() + renderLines() } function initExplore() { - window.updateHovering = updateHovering - window.render = () => { } + window.renderHighlight = () => { } function updateHovering(e, tapped) { if (dragging || (fixed && !tapped)) return updateCoordsDisplay(e) } - renderBackground(atlas) + renderBackground({}) applyView() @@ -837,6 +834,6 @@ function initViewGlobal() { } document.addEventListener('timeupdate', event => { - drawButton.href = "./?mode=draw" + formatHash(null, event.detail.period, event.detail.period, event.detail.variation) + drawButton.href = "./?mode=draw" + formatHash(null, event.detail.period, event.detail.variation) }) } diff --git a/web/about.html b/web/about.html index 4a09440a..a382d7cd 100644 --- a/web/about.html +++ b/web/about.html @@ -35,6 +35,8 @@ + + diff --git a/web/imageOverrides/Put the image overrides in this directory b/web/imageOverrides/Put the image overrides in this directory deleted file mode 100644 index e69de29b..00000000 diff --git a/web/index.html b/web/index.html index 374331a0..5b1261bf 100644 --- a/web/index.html +++ b/web/index.html @@ -9,16 +9,16 @@ - The 2022 r/place Atlas - + The 2023 r/place Atlas + - + - - + + - - + + @@ -34,17 +34,17 @@ - + - + - + @@ -138,18 +138,19 @@