/* ======================================================================== The 2022 /r/place Atlas An Atlas of Reddit's 2022 /r/place, with information to each artwork of the canvas provided by the community. Copyright (c) 2017 Roland Rytz Copyright (c) 2022 Place Atlas contributors Licensed under the GNU Affero General Public License Version 3 https://place-atlas.stefanocoding.me/license.txt ======================================================================== */ const linesCanvas = document.getElementById("linesCanvas"); const linesContext = linesCanvas.getContext("2d"); let hovered = []; let previousZoomOrigin = [0, 0]; let previousScaleZoomOrigin = [0, 0]; const backgroundCanvas = document.createElement("canvas"); backgroundCanvas.width = 2000; backgroundCanvas.height = 2000; const backgroundContext = backgroundCanvas.getContext("2d"); const wrapper = document.getElementById("wrapper"); const objectsContainer = document.getElementById("objectsList"); const closeObjectsListButton = document.getElementById("closeObjectsListButton"); const objectsListOverflowNotice = document.getElementById("objectsListOverflowNotice"); const filterInput = document.getElementById("searchList"); const entriesList = document.getElementById("entriesList"); const hideListButton = document.getElementById("hideListButton"); let entriesListShown = false; let sortedAtlas; 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"; moreEntriesButton.onclick = function () { buildObjectsList(null, null); renderBackground(); render(); }; let defaultSort = "shuffle"; document.getElementById("sort").value = defaultSort; let lastPos = [0, 0]; let fixed = false; // Fix hovered items in place, so that clicking on links is possible filterInput.addEventListener("input", function(e){ entriesOffset = 0; entriesList.innerHTML = ""; entriesList.appendChild(moreEntriesButton); if (this.value === "") { document.getElementById("relevantOption").disabled = true; sortedAtlas = atlas.concat(); buildObjectsList(null, null); } else { document.getElementById("relevantOption").disabled = false; buildObjectsList(this.value.toLowerCase(), "relevant"); } }); document.getElementById("sort").addEventListener("input", function (e) { if (this.value != "relevant") { defaultSort = this.value; } resetEntriesList(filterInput.value.toLowerCase(), this.value); }); var showDraw = document.getElementById('offcanvasDraw') showDraw.addEventListener('show.bs.offcanvas', function() { wrapper.classList.remove('listHidden'); wrapper.classList.add('listTransitioning'); applyView(); }) var shownDraw = document.getElementById('offcanvasDraw') shownDraw.addEventListener('shown.bs.offcanvas', function() { wrapper.classList.remove('listTransitioning'); applyView(); }) var hideDraw = document.getElementById('offcanvasDraw') hideDraw.addEventListener('hide.bs.offcanvas', function() { wrapper.classList.add('listHidden'); wrapper.classList.add('listTransitioning'); applyView(); }) var hiddenDraw = document.getElementById('offcanvasDraw') hiddenDraw.addEventListener('hidden.bs.offcanvas', function() { wrapper.classList.remove('listTransitioning'); applyView(); }) var showList = document.getElementById('offcanvasList') showList.addEventListener('show.bs.offcanvas', function(e) { wrapper.classList.remove('listHidden'); wrapper.classList.add('listTransitioning'); applyView(); }); var shownList = document.getElementById('offcanvasList') shownList.addEventListener('shown.bs.offcanvas', function(e) { entriesListShown = true; wrapper.classList.remove('listTransitioning'); updateHovering(e); applyView(); render(); updateLines(); }); var hideList = document.getElementById('offcanvasList') hideList.addEventListener('hide.bs.offcanvas', function(e) { wrapper.classList.add('listHidden'); wrapper.classList.add('listTransitioning'); applyView(); }); var hiddenList = document.getElementById('offcanvasList') hiddenList.addEventListener('hidden.bs.offcanvas', function(e) { entriesListShown = false; wrapper.classList.remove('listTransitioning'); updateHovering(e); applyView(); render(); updateLines(); }); closeObjectsListButton.addEventListener("click", function(){ hovered = []; objectsContainer.replaceChildren(); updateLines(); closeObjectsListButton.classList.add("d-none"); fixed = false; render(); }); function toggleFixed(e, tapped) { if (!fixed && hovered.length == 0) { return 0; } fixed = !fixed; if (!fixed) { updateHovering(e, tapped); render(); console.log("fixed"); } objectsListOverflowNotice.classList.add("d-none"); } window.addEventListener("resize", updateLines); window.addEventListener("mousemove", updateLines); window.addEventListener("dblClick", updateLines); window.addEventListener("wheel", updateLines); objectsContainer.addEventListener("scroll", function (e) { updateLines(); }); window.addEventListener("resize", function(e){ //console.log(document.documentElement.clientWidth, document.documentElement.clientHeight); let viewportWidth = document.documentElement.clientWidth; if (document.documentElement.clientWidth > 2000 && viewportWidth <= 2000) { entriesListShown = true; wrapper.classList.remove("listHidden"); } if (document.documentElement.clientWidth < 2000 && viewportWidth >= 2000) { entriesListShown = false; wrapper.classList.add("listHidden"); } updateHovering(e); viewportWidth = document.documentElement.clientWidth; applyView(); render(); updateLines(); }); function updateLines() { 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 = "#000000"; for (let i = 0; i < hovered.length; i++) { const element = hovered[i].element; if (element.getBoundingClientRect().left != 0) { linesContext.beginPath(); //linesContext.moveTo(element.offsetLeft + element.clientWidth - 10, element.offsetTop + 20); linesContext.moveTo( element.getBoundingClientRect().left + document.documentElement.scrollLeft + element.clientWidth / 2 , element.getBoundingClientRect().top + document.documentElement.scrollTop + 20 ); linesContext.lineTo( ~~(hovered[i].center[0] * zoom) + innerContainer.offsetLeft , ~~(hovered[i].center[1] * zoom) + innerContainer.offsetTop ); linesContext.stroke(); } } 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(); linesContext.moveTo( element.getBoundingClientRect().left + document.documentElement.scrollLeft + element.clientWidth / 2 , element.getBoundingClientRect().top + document.documentElement.scrollTop + 20 ); linesContext.lineTo( ~~(hovered[i].center[0] * zoom) + innerContainer.offsetLeft , ~~(hovered[i].center[1] * zoom) + innerContainer.offsetTop ); linesContext.stroke(); } } } function renderBackground(atlas) { backgroundContext.clearRect(0, 0, canvas.width, canvas.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 (let i = 0; i < atlas.length; i++) { const path = atlas[i].path; backgroundContext.beginPath(); if (path[0]) { //backgroundContext.moveTo(path[0][0]*zoom, path[0][1]*zoom); backgroundContext.moveTo(path[0][0], path[0][1]); } for (let p = 1; p < path.length; p++) { //backgroundContext.lineTo(path[p][0]*zoom, path[p][1]*zoom); backgroundContext.lineTo(path[p][0], path[p][1]); } backgroundContext.closePath(); let bgStrokeStyle; switch (atlas[i].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 buildObjectsList(filter = null, sort = null) { if (entriesList.contains(moreEntriesButton)) { entriesList.removeChild(moreEntriesButton); } if (!sortedAtlas) { sortedAtlas = atlas.concat(); document.getElementById("atlasSize").innerHTML = "The Atlas contains " + sortedAtlas.length + " entries."; } if (filter) { sortedAtlas = atlas.filter(function (value) { return ( value.name.toLowerCase().indexOf(filter) !== -1 || value.description.toLowerCase().indexOf(filter) !== -1 || value.subreddit && value.subreddit.toLowerCase().indexOf(filter) !== -1 || value.id === filter ); }); document.getElementById("atlasSize").innerHTML = "Found " + sortedAtlas.length + " entries."; } else { document.getElementById("atlasSize").innerHTML = "The Atlas contains " + sortedAtlas.length + " entries."; } if (sort === null) { sort = defaultSort; } renderBackground(sortedAtlas); render(); document.getElementById("sort").value = sort; //console.log(sort); let sortFunction; //console.log(sort); switch (sort) { case "shuffle": sortFunction = null; if (entriesOffset == 0) { shuffle(); } break; case "alphaAsc": sortFunction = function (a, b) { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); } break; case "alphaDesc": sortFunction = function (a, b) { return b.name.toLowerCase().localeCompare(a.name.toLowerCase()); } break; case "newest": sortFunction = function (a, b) { if (a.id > b.id) { return -1; } if (a.id < b.id) { return 1; } // a must be equal to b return 0; } break; case "oldest": sortFunction = function (a, b) { if (a.id < b.id) { return -1; } if (a.id > b.id) { return 1; } // a must be equal to b return 0; } break; case "area": sortFunction = function (a, b) { return calcPolygonArea(b.path) - calcPolygonArea(a.path); } break; case "relevant": sortFunction = function (a, b) { if (a.name.toLowerCase().indexOf(filter) !== -1 && b.name.toLowerCase().indexOf(filter) !== -1) { if (a.name.toLowerCase().indexOf(filter) < b.name.toLowerCase().indexOf(filter)) { return -1; } else if (a.name.toLowerCase().indexOf(filter) > b.name.toLowerCase().indexOf(filter)) { return 1; } else { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); } } else if (a.name.toLowerCase().indexOf(filter) !== -1) { return -1; } else if (b.name.toLowerCase().indexOf(filter) !== -1) { return 1; } else { if (a.description.toLowerCase().indexOf(filter) < b.description.toLowerCase().indexOf(filter)) { return -1; } else if (a.description.toLowerCase().indexOf(filter) > b.description.toLowerCase().indexOf(filter)) { return 1; } else { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); } } } break; } if (sortFunction) { sortedAtlas.sort(sortFunction); } for (let i = entriesOffset; i < entriesOffset + entriesLimit; i++) { if (i >= sortedAtlas.length) { break; } const element = createInfoBlock(sortedAtlas[i]); element.entry = sortedAtlas[i]; element.addEventListener("mouseenter", function (e) { if (!fixed && !dragging) { objectsContainer.replaceChildren(); previousZoomOrigin = [zoomOrigin[0], zoomOrigin[1]]; previousScaleZoomOrigin = [scaleZoomOrigin[0], scaleZoomOrigin[1]]; applyView(); zoomOrigin = [ innerContainer.clientWidth / 2 - this.entry.center[0] * zoom// + container.offsetLeft , innerContainer.clientHeight / 2 - this.entry.center[1] * zoom// + container.offsetTop ] scaleZoomOrigin = [ 2000 / 2 - this.entry.center[0] , 2000 / 2 - this.entry.center[1] ] //console.log(zoomOrigin); applyView(); hovered = [this.entry]; render(); hovered[0].element = this; updateLines(); } }); element.addEventListener("click", function (e) { toggleFixed(e); if (fixed) { previousZoomOrigin = [zoomOrigin[0], zoomOrigin[1]]; previousScaleZoomOrigin = [scaleZoomOrigin[0], scaleZoomOrigin[1]]; applyView(); } if (document.documentElement.clientWidth < 500) { objectsContainer.replaceChildren(); entriesListShown = false; wrapper.classList.add("listHidden"); zoom = 4; renderBackground(atlas); applyView(); updateHovering(e); zoomOrigin = [ innerContainer.clientWidth / 2 - this.entry.center[0] * zoom// + container.offsetLeft , innerContainer.clientHeight / 2 - this.entry.center[1] * zoom// + container.offsetTop ] scaleZoomOrigin = [ 2000 / 2 - this.entry.center[0] , 2000 / 2 - this.entry.center[1] ] previousZoomOrigin = [zoomOrigin[0], zoomOrigin[1]]; previousScaleZoomOrigin = [scaleZoomOrigin[0], scaleZoomOrigin[1]]; fixed = true; hovered = [this.entry]; hovered[0].element = this; applyView(); render(); updateLines(); } }); element.addEventListener("mouseleave", function (e) { if (!fixed && !dragging) { zoomOrigin = [previousScaleZoomOrigin[0] * zoom, previousScaleZoomOrigin[1] * zoom]; scaleZoomOrigin = [previousScaleZoomOrigin[0], previousScaleZoomOrigin[1]]; applyView(); hovered = []; updateLines(); render(); } }); entriesList.appendChild(element); } entriesOffset += entriesLimit; if (sortedAtlas.length > entriesOffset) { moreEntriesButton.innerHTML = "Show " + Math.min(entriesLimit, sortedAtlas.length - entriesOffset) + " more"; entriesList.appendChild(moreEntriesButton); } } function shuffle() { //console.log("shuffled atlas"); for (let i = sortedAtlas.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const temp = sortedAtlas[i]; sortedAtlas[i] = sortedAtlas[j]; sortedAtlas[j] = temp; } } function resetEntriesList() { entriesOffset = 0; entriesList.replaceChildren(); entriesList.appendChild(moreEntriesButton); buildObjectsList(filter = null, sort = null) } async function render() { context.clearRect(0, 0, canvas.width, canvas.height); //canvas.width = 1000*zoom; //canvas.height = 1000*zoom; context.globalCompositeOperation = "source-over"; context.clearRect(0, 0, canvas.width, canvas.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; context.beginPath(); if (path[0]) { //context.moveTo(path[0][0]*zoom, path[0][1]*zoom); context.moveTo(path[0][0], path[0][1]); } for (let p = 1; p < path.length; p++) { //context.lineTo(path[p][0]*zoom, path[p][1]*zoom); context.lineTo(path[p][0], path[p][1]); } context.closePath(); context.globalCompositeOperation = "source-over"; context.fillStyle = "rgba(0, 0, 0, 1)"; context.fill(); } context.globalCompositeOperation = "source-out"; context.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; context.globalCompositeOperation = "source-over"; context.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; context.beginPath(); if (path[0]) { //context.moveTo(path[0][0]*zoom, path[0][1]*zoom); context.moveTo(path[0][0], path[0][1]); } for (let p = 1; p < path.length; p++) { //context.lineTo(path[p][0]*zoom, path[p][1]*zoom); context.lineTo(path[p][0], path[p][1]); } context.closePath(); context.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; } context.strokeStyle = hoverStrokeStyle; //context.lineWidth = zoom; context.stroke(); } } function updateHovering(e, tapped) { if (!dragging && (!fixed || tapped)) { const pos = [ (e.clientX - (container.clientWidth / 2 - innerContainer.clientWidth / 2 + zoomOrigin[0] + container.offsetLeft)) / zoom , (e.clientY - (container.clientHeight / 2 - innerContainer.clientHeight / 2 + zoomOrigin[1] + container.offsetTop)) / zoom ]; const coords_p = document.getElementById("coords_p"); // Displays coordinates as zero instead of NaN if (isNaN(pos[0]) == true) { coords_p.innerText = "0, 0"; } else { coords_p.innerText = Math.ceil(pos[0]) + ", " + Math.ceil(pos[1]); } if (pos[0] <= 2200 && pos[0] >= -100 && pos[0] <= 2200 && pos[0] >= -100) { const newHovered = []; for (let i = 0; i < atlas.length; i++) { if (pointIsInPolygon(pos, atlas[i].path)) { newHovered.push(atlas[i]); } } 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) { hovered = newHovered.sort(function (a, b) { return calcPolygonArea(a.path) - calcPolygonArea(b.path); }); objectsContainer.replaceChildren(); for (const i in hovered) { const element = createInfoBlock(hovered[i]); objectsContainer.appendChild(element); hovered[i].element = element; } if (hovered.length > 0){ 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"); } render(); } } } } window.addEventListener("hashchange", highlightEntryFromUrl); function highlightEntryFromUrl() { const id = window.location.hash.substring(1); //Remove hash prefix const entries = atlas.filter(function (e) { return e.id === id; }); if (entries.length === 1) { const entry = entries[0]; document.title = entry.name + " on the 2022 r/place Atlas"; if ((!entry.diff || entry.diff !== "delete")) { if (document.getElementById("objectEditNav")) { document.getElementById("objectEditNav").href = "./?mode=draw&id=" + id; document.getElementById("objectEditNav").title = "Edit " + entry.name; } else { const objectEditNav = document.createElement("a"); objectEditNav.className = "btn btn-outline-primary"; objectEditNav.id = "objectEditNav"; objectEditNav.innerText = "Edit"; objectEditNav.href = "./?mode=draw&id=" + id; objectEditNav.title = "Edit " + entry.name; document.getElementById("showListButton").parentElement.appendChild(objectEditNav); } } else if (entry.diff == "delete" && document.getElementById("objectEditNav")) { document.getElementById("objectEditNav").remove(); } const infoElement = createInfoBlock(entry); objectsContainer.replaceChildren(); objectsContainer.appendChild(infoElement); //console.log(entry.center[0]); //console.log(entry.center[1]); zoom = 4; renderBackground(atlas); applyView(); zoomOrigin = [ innerContainer.clientWidth / 2 - entry.center[0] * zoom// + container.offsetLeft , innerContainer.clientHeight / 2 - entry.center[1] * zoom// + container.offsetTop ]; scaleZoomOrigin = [ 2000 / 2 - entry.center[0]// + container.offsetLeft , 2000 / 2 - entry.center[1]// + container.offsetTop ]; //console.log(zoomOrigin); applyView(); hovered = [entry]; render(); hovered[0].element = infoElement; closeObjectsListButton.classList.remove("d-none"); updateLines(); fixed = true; } } function initView() { buildObjectsList(null, null); renderBackground(atlas); render(); document.addEventListener('timeupdate', (event) => { sortedAtlas = atlas.concat() resetEntriesList(null, null) }) // parse linked atlas entry id from link hash /*if (window.location.hash.substring(3)){ zoom = 4; applyView(); highlightEntryFromUrl(); }*/ applyView(); render(); updateLines(); if (window.location.hash) { // both "/" and just "/#" will be an empty hash string highlightEntryFromUrl(); } } function initExplore() { window.updateHovering = updateHovering window.render = function () { } function updateHovering(e, tapped) { if (!dragging && (!fixed || tapped)) { const pos = [ (e.clientX - (container.clientWidth / 2 - innerContainer.clientWidth / 2 + zoomOrigin[0] + container.offsetLeft)) / zoom , (e.clientY - (container.clientHeight / 2 - innerContainer.clientHeight / 2 + zoomOrigin[1] + container.offsetTop)) / zoom ]; const coords_p = document.getElementById("coords_p"); coords_p.innerText = Math.ceil(pos[0]) + ", " + Math.ceil(pos[1]); } } renderBackground(atlas); applyView(); } function initGlobal() { container.addEventListener("mousemove", function (e) { if (e.sourceCapabilities) { if (!e.sourceCapabilities.firesTouchEvents) { updateHovering(e); } } else { updateHovering(e); } }); } function initViewGlobal() { container.addEventListener("mousedown", function (e) { lastPos = [ e.clientX , e.clientY ]; }); container.addEventListener("touchstart", function (e) { if (e.touches.length == 1) { lastPos = [ e.touches[0].clientX , e.touches[0].clientY ]; } }, { passive: true }); container.addEventListener("mouseup", function (e) { if (Math.abs(lastPos[0] - e.clientX) + Math.abs(lastPos[1] - e.clientY) <= 4) { toggleFixed(e); } }); container.addEventListener("touchend", function (e) { e.preventDefault() //console.log(e); //console.log(e.changedTouches[0].clientX); if (e.changedTouches.length == 1) { e = e.changedTouches[0]; //console.log(lastPos[0] - e.clientX); if (Math.abs(lastPos[0] - e.clientX) + Math.abs(lastPos[1] - e.clientY) <= 4) { //console.log("Foo!!"); dragging = false; fixed = false; setTimeout( function () { updateHovering(e, true); } , 10); } } }); }