diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4b68387..a0b743e8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -121,6 +121,14 @@ Hereforth is an example of the structured entry data. The example has been expan ## Development +> [!IMPORTANT] +> +> Instead of here, consider directing your contributions to [The 2023 r/place Atlas](https://github.com/placeAtlas/atlas-2023). +> +> Updates from the latest Atlas would be backported occasionally for the benefit of the other instances (e.g. the 2017 Atlas and the 2022 Atlas). Furthermore, there is a great chance that what you want to do would also benefit the other instances. +> +> There may some cases where what you want to do only apply on this specific instance. In that case, you may proceed. When in doubt, don't hesitate to contact us. + Other than contributing to the Atlas data, code contributions are also accepted. Here are some information regarding some aspects on the repository. ### Web interface diff --git a/README.md b/README.md index 269231c5..7a3b9c68 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ -[![Entry count](https://img.shields.io/badge/dynamic/json?color=blue&label=entries&query=%24.length&url=https%3A%2F%2Fgithub.com%2FplaceAtlas%2Fatlas%2Fblob%2Fmaster%2Fweb%2Fatlas.json%3Fraw%3Dtrue)](https://2022.place-atlas.stefanocoding.me/) +[![Entry count](https://img.shields.io/badge/dynamic/json?color=blue&label=entries&query=%24.length&url=https%3A%2F%2Fgithub.com%2FplaceAtlas%2Fatlas-2022%2Fblob%2Fmaster%2Fweb%2Fatlas.json%3Fraw%3Dtrue)](https://2022.place-atlas.stefanocoding.me/) ![Commit activity](https://img.shields.io/github/commit-activity/w/placeAtlas/atlas-2022) -[![Netlify](https://img.shields.io/netlify/1e7291ce-0680-45ed-9843-47a32a992bbb?logo=netlify&logoColor=white)](https://app.netlify.com/sites/place-atlas/deploys) -[![License](https://img.shields.io/github/license/placeAtlas/atlas-2022)](https://github.com/placeAtlas/atlas-2022/blob/master/LICENSE) +[![Netlify](https://img.shields.io/netlify/1e7291ce-0680-45ed-9843-47a32a992bbb?logo=netlify&logoColor=white)](https://app.netlify.com/sites/place-atlas-2022/deploys) +[![License](https://img.shields.io/github/license/placeAtlas/atlas)](https://github.com/placeAtlas/atlas-2022/blob/master/LICENSE) [![Discord](https://img.shields.io/discord/960791635342524496?color=%235865F2&logo=discord&logoColor=white)](https://discord.gg/pJkm23b2nA) [![Subreddit subscribers](https://img.shields.io/reddit/subreddit-subscribers/placeAtlas2?color=%23FF4500&label=r%2FplaceAtlas2&logo=reddit&logoColor=white)](https://www.reddit.com/r/placeAtlas2/) [![Website](https://img.shields.io/static/v1?label=website&message=2022.place-atlas.stefanocoding.me&color=blue)](https://2022.place-atlas.stefanocoding.me/) # The 2022 r/place Atlas -The 2022 r/place Atlas is a project aiming to chart all the artworks created during the r/place April Fools event on Reddit in 2022. It is made with information to each artwork of the canvas provided by the community. +The 2022 r/place Atlas is an interactive map aiming to chart all the artworks created during the r/place April Fools event on Reddit in 2022. It is made with information to each artwork of the canvas provided by the community. -This project was established by Roland Rytz for the event in 2017, and further developed and maintained by the Place Atlas team and contributors. +This project was established by Roland Rytz for the event in 2017, and further developed and maintained by the [Place Atlas Initiative](https://place-atlas.stefanocoding.me) and [contributors](https://github.com/placeAtlas/atlas-2022/graphs/contributors). This project is licensed under the [GNU Affero General Public License v3.0](LICENSE). -You can check out the website by visiting [2022.place-atlas.stefanocoding.me](https://2022.place-atlas.stefanocoding.me/). If you want to keep distance from GitHub, you may visit [r/placeAtlas2](https://www.reddit.com/r/placeAtlas2/). +You can check out the website by visiting [2022.place-atlas.stefanocoding.me](https://2022.place-atlas.stefanocoding.me/). The subreddit, [r/placeAtlas2](https://www.reddit.com/r/placeAtlas2/), is available for communications via Reddit. ## Contributing @@ -22,15 +22,18 @@ This project is open source, and contributions are welcome. In fact, the Atlas r Please read [CONTRIBUTING.md](CONTRIBUTING.md) to learn how to submit a new entry, edit existing entries, or contribute to the development of the codebase. -[The r/placeAlas2 subreddit](https://reddit.com/r/placeAtlas2/) and [the Discord server](https://discord.gg/pJkm23b2nA) is also the place to submit all bug reports, feature requests, or questions. +[The r/placeAtlas2 subreddit](https://reddit.com/r/placeAtlas2/) and [the Discord server](https://discord.gg/pJkm23b2nA) is also the place to submit all bug reports, feature requests, or questions. ## Contributors +> [!NOTE] +> For more credits, including entry contributors, please see [the Credits sections on the About page](https://2022.place-atlas.stefanocoding.me/about#credits). + [![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat)](#contributors) -Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key))): +Many thanks to these wonderful people for contributing on the scope of this repository. ([emoji key](https://allcontributors.org/docs/en/emoji-key)). diff --git a/package-lock.json b/package-lock.json index 8628f6bf..a8bae615 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "parcel": "^2.8.3", "parcel-namer-rewrite": "^2.0.0-rc.3", "parcel-resolver-ignore": "^2.1.3", - "postcss": "^8.4.31" + "postcss": "^8.4.21" } }, "node_modules/@babel/code-frame": { @@ -1926,9 +1926,9 @@ } }, "node_modules/all-contributors-cli": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/all-contributors-cli/-/all-contributors-cli-6.26.1.tgz", - "integrity": "sha512-Ymgo3FJACRBEd1eE653FD1J/+uD0kqpUNYfr9zNC1Qby0LgbhDBzB3EF6uvkAbYpycStkk41J+0oo37Lc02yEw==", + "version": "6.24.0", + "resolved": "https://registry.npmjs.org/all-contributors-cli/-/all-contributors-cli-6.24.0.tgz", + "integrity": "sha512-7oSKr2PnqxsOotuSwciltcFTS1eVRdjR0cn99hbElfff7gRQBShVhsf/XBprY41sLcgqTk0l0MKgKv6QNgZdMg==", "dev": true, "dependencies": { "@babel/runtime": "^7.7.6", @@ -1947,9 +1947,6 @@ }, "engines": { "node": ">=4" - }, - "optionalDependencies": { - "prettier": "^2" } }, "node_modules/all-contributors-cli/node_modules/ansi-styles": { @@ -3762,16 +3759,10 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -3974,9 +3965,9 @@ } }, "node_modules/parcel-resolver-ignore": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/parcel-resolver-ignore/-/parcel-resolver-ignore-2.1.5.tgz", - "integrity": "sha512-/2zgQw3J/2YA7L6JXg4XKBWT/SXDZx+PfweWcCsllchNVwFvK7jDJhG6h+puy+e15Rm9A/ubuuHYwANQHVXp2A==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/parcel-resolver-ignore/-/parcel-resolver-ignore-2.1.3.tgz", + "integrity": "sha512-C8uLvR4o7SPRSsQ/Nylm1/PdsLwn/Z9bzCs66qT3XIebJC7ojaFFF3MDl/mie5audngjcFF8wzU0AoEQkZq2pA==", "dev": true, "engines": { "parcel": ">=2.0.0" @@ -4137,9 +4128,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "dev": true, "funding": [ { @@ -4149,14 +4140,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -4691,22 +4678,6 @@ "node": ">=12" } }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "optional": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/purgecss": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-5.0.0.tgz", @@ -6550,9 +6521,9 @@ "dev": true }, "all-contributors-cli": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/all-contributors-cli/-/all-contributors-cli-6.26.1.tgz", - "integrity": "sha512-Ymgo3FJACRBEd1eE653FD1J/+uD0kqpUNYfr9zNC1Qby0LgbhDBzB3EF6uvkAbYpycStkk41J+0oo37Lc02yEw==", + "version": "6.24.0", + "resolved": "https://registry.npmjs.org/all-contributors-cli/-/all-contributors-cli-6.24.0.tgz", + "integrity": "sha512-7oSKr2PnqxsOotuSwciltcFTS1eVRdjR0cn99hbElfff7gRQBShVhsf/XBprY41sLcgqTk0l0MKgKv6QNgZdMg==", "dev": true, "requires": { "@babel/runtime": "^7.7.6", @@ -6564,7 +6535,6 @@ "lodash": "^4.11.2", "node-fetch": "^2.6.0", "pify": "^5.0.0", - "prettier": "^2", "yargs": "^15.0.1" }, "dependencies": { @@ -7853,9 +7823,9 @@ "dev": true }, "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", "dev": true }, "node-addon-api": { @@ -8045,9 +8015,9 @@ } }, "parcel-resolver-ignore": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/parcel-resolver-ignore/-/parcel-resolver-ignore-2.1.5.tgz", - "integrity": "sha512-/2zgQw3J/2YA7L6JXg4XKBWT/SXDZx+PfweWcCsllchNVwFvK7jDJhG6h+puy+e15Rm9A/ubuuHYwANQHVXp2A==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/parcel-resolver-ignore/-/parcel-resolver-ignore-2.1.3.tgz", + "integrity": "sha512-C8uLvR4o7SPRSsQ/Nylm1/PdsLwn/Z9bzCs66qT3XIebJC7ojaFFF3MDl/mie5audngjcFF8wzU0AoEQkZq2pA==", "dev": true, "requires": {} }, @@ -8103,12 +8073,12 @@ "dev": true }, "postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "dev": true, "requires": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } @@ -8473,13 +8443,6 @@ "is-json": "^2.0.1" } }, - "prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "optional": true - }, "purgecss": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-5.0.0.tgz", diff --git a/package.json b/package.json index 83831d23..4c12cf83 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "parcel": "^2.8.3", "parcel-namer-rewrite": "^2.0.0-rc.3", "parcel-resolver-ignore": "^2.1.3", - "postcss": "^8.4.31", + "postcss": "^8.4.21", "all-contributors-cli": "^6.24.0" }, "parcel-namer-rewrite": { @@ -18,7 +18,8 @@ }, "parcelIgnore": [ "sw.js", - "_img/.+" + "_img/.+", + "atlas.json" ], "browserslist": [ ">= 0.5%", diff --git a/tools/ci/build-preview.sh b/tools/ci/build-preview.sh index 5977fb6c..7990c189 100644 --- a/tools/ci/build-preview.sh +++ b/tools/ci/build-preview.sh @@ -20,6 +20,5 @@ cp -r web/_img/ dist/ cp web/atlas.json dist/ cp web/*.txt dist/ cp web/_headers dist/ -cp web/_redirects dist/ cp web/favicon.ico dist/ cp web/sw.js dist/ \ No newline at end of file diff --git a/tools/ci/build-prod.sh b/tools/ci/build-prod.sh index 5f644df1..8d211d48 100644 --- a/tools/ci/build-prod.sh +++ b/tools/ci/build-prod.sh @@ -19,6 +19,5 @@ cp -r web/_img/ dist/ cp web/atlas.json dist/ cp web/*.txt dist/ cp web/_headers dist/ -cp web/_redirects dist/ cp web/favicon.ico dist/ cp web/sw.js dist/ \ No newline at end of file diff --git a/tools/unused/smallifier.html b/tools/unused/smallifier.html index ced5124b..cea9c1dc 100644 --- a/tools/unused/smallifier.html +++ b/tools/unused/smallifier.html @@ -1,8 +1,7 @@ - diff --git a/web/_css/style.css b/web/_css/style.css index ecb4c949..a07892da 100644 --- a/web/_css/style.css +++ b/web/_css/style.css @@ -1,7 +1,7 @@ /*! * The 2022 r/place Atlas * Copyright (c) 2017 Roland Rytz - * Copyright (c) 2022 Place Atlas contributors + * Copyright (c) 2022 Place Atlas Initiative and contributors * Licensed under AGPL-3.0 (https://2022.place-atlas.stefanocoding.me/license.txt) */ @@ -52,6 +52,14 @@ body[data-mode] { 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); @@ -258,12 +266,12 @@ body[data-init-done] .listTransitioning #innerContainer { } #variantControls { - flex: 1 0 auto; + flex: 2 0 auto; } #timeControls { position: relative; - flex: 6 0 300px; + flex: 12 0 300px; } #bottomBar.no-time-slider #timeControls { @@ -440,13 +448,25 @@ body:not([data-dev]) .show-only-on-dev { display: none !important } +#objectsList, +#offcanvasList, +#offcanvasDraw, +#closeObjectsListButton { + margin-top: var(--global-top-padding); +} + +.copyleft { + display: inline-block; + transform: rotateY(180deg); +} + /* about.html */ -#credits a { +#entry-contributors-wrapper a { text-decoration: none; } -#credits a:hover { +#entry-contributors-wrapper a:hover { text-decoration: underline; } diff --git a/web/_headers b/web/_headers index 881b275a..ed8d9f92 100644 --- a/web/_headers +++ b/web/_headers @@ -1,8 +1,21 @@ /* Access-Control-Allow-Origin: * - -/_img/canvas/*/*.png - cache-control: public, max-age=604800 + +# Hashed resources: 1 year and immutable + +/_js/* + Cache-Control: public, immutable, max-age=31536000 + +/_css/* + Cache-Control: public, immutable, max-age=31536000 + +# Canvas images: 1 year + +/_img/canvas/* + Cache-Control: public, max-age=31536000 /_img/canvas/*.png - cache-control: public, max-age=604800 + Cache-Control: public, max-age=31536000 + +/_img/canvas/*/*.png + Cache-Control: public, max-age=31536000 diff --git a/web/_js/about.js b/web/_js/about.js index 14ccdc65..95aead10 100644 --- a/web/_js/about.js +++ b/web/_js/about.js @@ -1,11 +1,11 @@ /*! * The 2022 r/place Atlas * Copyright (c) 2017 Roland Rytz - * Copyright (c) 2022 Place Atlas contributors + * Copyright (c) 2022 Place Atlas Initiative and contributors * Licensed under AGPL-3.0 (https://2022.place-atlas.stefanocoding.me/license.txt) */ -const contributorsEl = document.querySelector('#contributors-wrapper') +const contributorsEl = document.querySelector('#entry-contributors-wrapper') // const gitHubEl = document.createElement("i") @@ -34,6 +34,8 @@ fetch('all-authors.txt') userEl.href = 'https://reddit.com/user/' + contributor userEl.textContent = contributor } + userEl.target = '_blank' + userEl.rel = 'noreferrer' contributorsEl.appendChild(userEl) contributorsEl.appendChild(document.createTextNode(' ')) } diff --git a/web/_js/config.js b/web/_js/config.js index e9a58d9d..84e4c403 100644 --- a/web/_js/config.js +++ b/web/_js/config.js @@ -265,8 +265,8 @@ window.useNumericalId = useNumericalId console.info(`%cThe 2022 r/place Atlas %cCopyright (c) 2017 Roland Rytz -Copyright (c) 2022 Place Atlas contributors -Licensed under AGPL-3.0 (https://2022.place-atlas.stefanocoding.me/license.txt) +Copyright (c) 2022 Place Atlas Initiative and contributors +Licensed under AGPL-3.0 (https://2023.place-atlas.stefanocoding.me/license.txt) https://2022.place-atlas.stefanocoding.me/ https://discord.gg/pJkm23b2nA @@ -274,4 +274,4 @@ https://reddit.com/r/placeatlas2 https://github.com/placeAtlas/atlas-2022 To get the image of the canvas, use downloadCanvas(). -`, 'font-size: 150%; line-height: 150%', '') \ No newline at end of file +`, 'font-size: 150%; line-height: 150%', '') diff --git a/web/_js/main/atlas.js b/web/_js/main/atlas.js index 0b6bac56..79ffa963 100644 --- a/web/_js/main/atlas.js +++ b/web/_js/main/atlas.js @@ -1,7 +1,7 @@ /*! * The 2022 r/place Atlas * Copyright (c) 2017 Roland Rytz - * Copyright (c) 2022 Place Atlas contributors + * Copyright (c) 2022 Place Atlas Initiative and contributors * Licensed under AGPL-3.0 (https://2022.place-atlas.stefanocoding.me/license.txt) */ diff --git a/web/_js/main/draw.js b/web/_js/main/draw.js index 40aef13c..6fb6abcb 100644 --- a/web/_js/main/draw.js +++ b/web/_js/main/draw.js @@ -1,7 +1,7 @@ /*! * The 2022 r/place Atlas * Copyright (c) 2017 Roland Rytz - * Copyright (c) 2022 Place Atlas contributors + * Copyright (c) 2022 Place Atlas Initiative and contributors * Licensed under AGPL-3.0 (https://2022.place-atlas.stefanocoding.me/license.txt) */ @@ -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() { @@ -332,7 +338,7 @@ function initDraw() { if (exportArea.value > 40000) { exportArea.value = " " + miniJsonString } - + // Reddit let redditPostJsonString = " " + prettyJsonString.split("\n").join("\n ") @@ -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,16 +487,16 @@ 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++) { + for (const entry of Object.values(atlas)) { - const path = atlas[i].path + const path = entry.path backgroundContext.beginPath() @@ -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]) @@ -805,25 +815,25 @@ function initDraw() { const [,, hashX, hashY, hashZoom] = hash.split('/') setView( - (isNaN(hashX) || hashX === '') ? center[0] : Number(hashX), - (isNaN(hashY) || hashY === '') ? center[1] : Number(hashY), + (isNaN(hashX) || hashX === '') ? center[0] : Number(hashX), + (isNaN(hashY) || hashY === '') ? center[1] : Number(hashY), (isNaN(hashZoom) || hashZoom === '') ? 4 : Number(hashZoom) ) - + 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) }) } @@ -862,7 +872,7 @@ function initPeriodGroups() { const periodCopyEl = periodGroupEl.querySelector('.period-copy') const periodDuplicateEl = periodGroupEl.querySelector('.period-duplicate') const periodDeleteEl = periodGroupEl.querySelector('.period-delete') - + const periodVariationEl = periodGroupEl.querySelector('.period-variation') const periodStatusEl = periodGroupEl.querySelector('.period-status') @@ -926,19 +936,19 @@ function initPeriodGroups() { }) startPeriodViewEl.addEventListener('click', () => { updateTime(parseInt(startPeriodEl.value), getCurrentVariation()) - + // Set zoom view periodCenter = calculateCenter(path) setView(periodCenter[0], periodCenter[1], setZoomByPath(path)) }) - + function getCurrentVariation() { return periodVariationEl[periodVariationEl.selectedIndex].value } function startPeriodUpdate(value) { endPeriodListEl.innerHTML = '' - + // Update time only when value changes if (startPeriodEl.value !== timelineSlider.value) { timelineSlider.value = value @@ -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..f926182d 100644 --- a/web/_js/main/infoblock.js +++ b/web/_js/main/infoblock.js @@ -1,7 +1,7 @@ /*! * The 2022 r/place Atlas * Copyright (c) 2017 Roland Rytz - * Copyright (c) 2022 Place Atlas contributors + * Copyright (c) 2022 Place Atlas Initiative and contributors * Licensed under AGPL-3.0 (https://2022.place-atlas.stefanocoding.me/license.txt) */ @@ -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..6f2921f8 100644 --- a/web/_js/main/main.js +++ b/web/_js/main/main.js @@ -1,7 +1,7 @@ /*! * The 2022 r/place Atlas * Copyright (c) 2017 Roland Rytz - * Copyright (c) 2022 Place Atlas contributors + * Copyright (c) 2022 Place Atlas Initiative and contributors * Licensed under AGPL-3.0 (https://2022.place-atlas.stefanocoding.me/license.txt) */ @@ -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 @@ -102,13 +100,24 @@ async function init() { } } + // Experimental: TemplateManager support + // Add a .json file of TemplateManager on the "template" URL param. + // e.g. ?template=https://osu.place/e/osuplace2023.json + // CORS bypass is required (e.g. a proxy, CORS Anywhere). + if (params.get("template")) { + const [ templateDatas ] = await loadTemplateData(params.get("template")) + const templateLayers = await loadTemplateImages(templateDatas) + additionalLayers.push(...templateLayers) + updateAdditionalLayer(additionalLayers) + } + if (mode === "about") window.location.replace("./about.html") // 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 +157,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 +252,7 @@ async function init() { zoom = 1 zoomOrigin = [0, 0] scaleZoomOrigin = [0, 0] - updateLines() + renderLines() applyView() }) @@ -384,7 +381,6 @@ async function init() { } window.addEventListener("mousemove", e => { - // updateLines() mousemove(e.clientX, e.clientY) if (dragging) { e.preventDefault() @@ -417,7 +413,7 @@ async function init() { scaleZoomOrigin[0] += deltaX / zoom scaleZoomOrigin[1] += deltaY / zoom - updateLines() + renderLines() applyView() } @@ -460,7 +456,7 @@ async function init() { zoomOrigin[1] = scaleZoomOrigin[1] * zoom applyView() - updateLines() + renderLines() } window.addEventListener("mouseup", e => { @@ -478,7 +474,7 @@ async function init() { }) window.addEventListener("touchend", touchend) - function mouseup(x, y) { + function mouseup() { dragging = false updateHash() } @@ -486,7 +482,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 +502,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,22 +525,37 @@ function updateAtlasAll(atlas = atlasAll) { } entry.path = currentPath entry.center = currentCenter + newAtlas[entry.id] = entry } - return atlas + // console.log(newAtlas) + return newAtlas } -// Announcement system +// Notice system -const announcementEl = document.querySelector("#headerAnnouncement") -const announcementButton = announcementEl.querySelector('[role=button]') -const announcementText = announcementEl.querySelector('p').textContent.trim() +const noticeEl = document.querySelector("#headerNotice") +const noticeButton = noticeEl.querySelector('[role=button]') +const noticeText = noticeEl.querySelector('p').textContent.trim() -if (announcementText && announcementText !== window.localStorage.getItem('announcement-closed')) { - announcementButton.click() - document.querySelector('#objectsList').style.marginTop = '2.8rem' +const resizeGlobalTopPadding = () => { + document.body.style.setProperty("--global-top-padding", noticeEl.offsetHeight + 'px') } -announcementEl.querySelector('[role=button]').addEventListener('click', () => { - window.localStorage.setItem('announcement-closed', announcementText) - document.querySelector('#objectsList').style.marginTop = '0' +if (window.localStorage.getItem('announcement-closed')) { + window.localStorage.setItem('closed-notice', window.localStorage.getItem('announcement-closed')) + window.localStorage.removeItem('announcement-closed') +} + +if (noticeText && noticeText !== window.localStorage.getItem('closed-notice')) { + noticeButton.click() + setTimeout(() => { + document.body.style.setProperty("--global-top-padding", noticeEl.offsetHeight + 'px') + }, 500) + window.addEventListener('resize', resizeGlobalTopPadding) +} + +noticeEl.querySelector('[role=button]').addEventListener('click', () => { + window.localStorage.setItem('closed-notice', noticeText) + window.removeEventListener('resize', resizeGlobalTopPadding) + document.body.style.setProperty("--global-top-padding", null) }) diff --git a/web/_js/main/overlap.js b/web/_js/main/overlap.js index bb534ed5..5b5c7965 100644 --- a/web/_js/main/overlap.js +++ b/web/_js/main/overlap.js @@ -1,7 +1,7 @@ /*! * The 2022 r/place Atlas * Copyright (c) 2017 Roland Rytz - * Copyright (c) 2022 Place Atlas contributors + * Copyright (c) 2022 Place Atlas Initiative and contributors * Licensed under AGPL-3.0 (https://2022.place-atlas.stefanocoding.me/license.txt) */ @@ -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/stats.js b/web/_js/main/stats.js index 016ec4d3..ec276f84 100644 --- a/web/_js/main/stats.js +++ b/web/_js/main/stats.js @@ -1,7 +1,7 @@ /*! * The 2022 r/place Atlas * Copyright (c) 2017 Roland Rytz - * Copyright (c) 2022 Place Atlas contributors + * Copyright (c) 2022 Place Atlas Initiative and contributors * Licensed under AGPL-3.0 (https://2022.place-atlas.stefanocoding.me/license.txt) */ @@ -203,7 +203,7 @@ console.info("The " + topCount + " largest entries:") let outstring = "" for (let i = 0; i < topCount; i++) { - outstring += ((i + 1) + "|[" + atlas[atlas.length - i - 1].name + "](http://2022.place-atlas.stefanocoding.me/?id=" + atlas[atlas.length - i - 1].id + ")|" + ~~atlas[atlas.length - i - 1].area + "|" + Math.round(atlas[atlas.length - i - 1].area / 100) / 100 + "%\n") + outstring += ((i + 1) + "|[" + atlas[atlas.length - i - 1].name + "](http://2023.place-atlas.stefanocoding.me/?id=" + atlas[atlas.length - i - 1].id + ")|" + ~~atlas[atlas.length - i - 1].area + "|" + Math.round(atlas[atlas.length - i - 1].area / 100) / 100 + "%\n") } console.info(outstring) diff --git a/web/_js/main/time.js b/web/_js/main/time.js index 09e7cfd8..f67da264 100644 --- a/web/_js/main/time.js +++ b/web/_js/main/time.js @@ -1,7 +1,7 @@ /*! * The 2022 r/place Atlas * Copyright (c) 2017 Roland Rytz - * Copyright (c) 2022 Place Atlas contributors + * Copyright (c) 2022 Place Atlas Initiative and contributors * Licensed under AGPL-3.0 (https://2022.place-atlas.stefanocoding.me/license.txt) */ @@ -32,6 +32,12 @@ let currentPeriod = defaultPeriod window.currentVariation = currentVariation window.currentPeriod = currentPeriod +let atlasDisplay = {} +window.atlasDisplay = atlasDisplay + +const additionalLayers = [] +const additionalLayerCanvas = document.createElement('canvas') + // SETUP if (variationsConfig[currentVariation].versions.length === 1) bottomBar.classList.add('no-time-slider') @@ -70,7 +76,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 +85,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 +102,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 +116,67 @@ 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 (myAbortController.signal.aborted || newPeriod !== currentPeriod || newVariation !== currentVariation || currentUpdateIndex !== myUpdateIndex) { + return false } - + for (const imageLayer of layers) { context.drawImage(imageLayer, 0, 0) } + context.drawImage(additionalLayerCanvas, 0, 0) + + if (myAbortController.signal.aborted || newPeriod !== currentPeriod || newVariation !== currentVariation || currentUpdateIndex !== myUpdateIndex) { + return false + } + 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 +201,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 +219,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 +251,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 +321,7 @@ function parsePeriod(periodString) { function formatPeriod(targetStart, targetEnd, targetVariation, forUrl = false) { targetStart ??= currentPeriod - targetEnd ??= currentPeriod + targetEnd ??= targetStart targetVariation ??= currentVariation let periodString, variationString @@ -297,12 +345,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 +360,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 +374,63 @@ 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 ] + +} + +const updateAdditionalLayer = () => { + const layers = additionalLayers + const canvas = additionalLayerCanvas + const context = additionalLayerCanvas.getContext('2d') + canvas.width = 0 + canvas.height = 0 + + + for (const layer of layers) { + if (!layer.imageLayer) continue + canvas.width = Math.max(layer.x + layer.imageLayer.width, canvas.width) + canvas.height = Math.max(layer.y + layer.imageLayer.height, canvas.height) + } + + for (const layer of layers) { + if (!layer.imageLayer) continue + context.drawImage(layer.imageLayer, layer.x, layer.y) + console.log(layer.imageLayer) + } +} diff --git a/web/_js/main/view.js b/web/_js/main/view.js index 737f122e..6a7b18eb 100644 --- a/web/_js/main/view.js +++ b/web/_js/main/view.js @@ -1,7 +1,7 @@ /*! * The 2022 r/place Atlas * Copyright (c) 2017 Roland Rytz - * Copyright (c) 2022 Place Atlas contributors + * Copyright (c) 2022 Place Atlas Initiative and contributors * Licensed under AGPL-3.0 (https://2022.place-atlas.stefanocoding.me/license.txt) */ @@ -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 @@ -65,18 +69,18 @@ const moreEntriesObserver = new IntersectionObserver(entries => { moreEntriesObserver.observe(moreEntriesButton) -let defaultSort = sortInput.value +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 () { - 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,63 +131,59 @@ offcanvasList.addEventListener('hidden.bs.offcanvas', e => { wrapper.classList.remove('listTransitioning') updateHovering(e) applyView() - render() - updateLines() + renderHighlight() + renderLines() }) closeObjectsListButton.addEventListener("click", clearObjectsList) -bottomBar.addEventListener("mouseover", () => { - if (!fixed) 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") - 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 +260,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 +279,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 +299,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,100 +363,130 @@ 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 = () => { if (entriesList.contains(moreEntriesButton)) { entriesList.removeChild(moreEntriesButton) - } - - for (let i = entriesOffset; i < entriesOffset + entriesLimit; i++) { - - if (i >= atlasDisplay.length) break - - 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() - }) - - entriesList.appendChild(element) - } - - entriesOffset += entriesLimit - - if (atlasDisplay.length > entriesOffset) { - moreEntriesButton.innerHTML = "Show " + Math.min(entriesLimit, atlasDisplay.length - entriesOffset) + " more" + + 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() - } -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 +502,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 +529,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 @@ -591,12 +591,16 @@ function updateHovering(e, tapped) { 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 - - 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 +616,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 +641,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,11 +669,11 @@ function updateViewFromHash() { targetPeriod = defaultPeriod targetVariation = defaultVariation } - updateTime(targetPeriod, targetVariation, true) + await updateTime(targetPeriod, targetVariation) setView( - (isNaN(hashX) || hashX === '') ? undefined : Number(hashX), - (isNaN(hashY) || hashY === '') ? undefined : Number(hashY), + (isNaN(hashX) || hashX === '') ? undefined : Number(hashX), + (isNaN(hashY) || hashY === '') ? undefined : Number(hashY), (isNaN(hashZoom) || hashZoom === '') ? undefined : Number(hashZoom) ) @@ -678,13 +681,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 +700,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(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] - 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 +739,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 +753,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() @@ -790,7 +783,7 @@ function initGlobal() { } }) - document.addEventListener('timeupdate', event => { + document.addEventListener('timeupdate', () => { updateHash() }) } @@ -837,6 +830,84 @@ 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) + }) + + 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 + +} diff --git a/web/about.html b/web/about.html index 4a09440a..1690319c 100644 --- a/web/about.html +++ b/web/about.html @@ -1,7 +1,7 @@ @@ -10,8 +10,8 @@ About - The 2022 r/place Atlas - - + + @@ -23,18 +23,19 @@ - + - - + + + @@ -60,8 +61,8 @@ diff --git a/web/index.html b/web/index.html index 374331a0..0f847139 100644 --- a/web/index.html +++ b/web/index.html @@ -1,7 +1,7 @@ @@ -11,10 +11,10 @@ The 2022 r/place Atlas - - + + - + @@ -22,11 +22,10 @@ - + - @@ -34,22 +33,21 @@ - + - + - + -