atlas/web/_js/time.js

278 lines
28 KiB
JavaScript
Raw Normal View History

/*
========================================================================
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 <roland@draemm.li>
Copyright (c) 2022 Place Atlas contributors
Licensed under the GNU Affero General Public License Version 3
https://place-atlas.stefanocoding.me/license.txt
========================================================================
*/
2022-04-16 14:51:51 +02:00
const variationsConfig = {
default: {
name: "r/place",
code: "",
default: 164,
drawablePeriods: [1, 166],
2022-04-24 09:51:41 +02:00
versions: [{ "timestamp": 1648818000, "url": ["./_img/canvas/place30ex/start.png"] }, { "timestamp": 1648819800, "url": ["./_img/canvas/place30/005.png", "./_img/canvas/place30/000_005.png"] }, { "timestamp": 1648821600, "url": ["./_img/canvas/place30/005.png", "./_img/canvas/place30/001_005.png"] }, { "timestamp": 1648823400, "url": ["./_img/canvas/place30/005.png", "./_img/canvas/place30/002_005.png"] }, { "timestamp": 1648825200, "url": ["./_img/canvas/place30/005.png", "./_img/canvas/place30/003_005.png"] }, { "timestamp": 1648827000, "url": ["./_img/canvas/place30/005.png", "./_img/canvas/place30/004_005.png"] }, { "timestamp": 1648828800, "url": "./_img/canvas/place30/005.png" }, { "timestamp": 1648830600, "url": ["./_img/canvas/place30/005.png", "./_img/canvas/place30/006_005.png"] }, { "timestamp": 1648832400, "url": ["./_img/canvas/place30/005.png", "./_img/canvas/place30/007_005.png"] }, { "timestamp": 1648834200, "url": ["./_img/canvas/place30/005.png", "./_img/canvas/place30/008_005.png"] }, { "timestamp": 1648836000, "url": ["./_img/canvas/place30/005.png", "./_img/canvas/place30/009_005.png"] }, { "timestamp": 1648837800, "url": ["./_img/canvas/place30/005.png", "./_img/canvas/place30/010_005.png"] }, { "timestamp": 1648839600, "url": ["./_img/canvas/place30/016.png", "./_img/canvas/place30/011_016.png"] }, { "timestamp": 1648841400, "url": ["./_img/canvas/place30/016.png", "./_img/canvas/place30/012_016.png"] }, { "timestamp": 1648843200, "url": ["./_img/canvas/place30/016.png", "./_img/canvas/place30/013_016.png"] }, { "timestamp": 1648845000, "url": ["./_img/canvas/place30/016.png", "./_img/canvas/place30/014_016.png"] }, { "timestamp": 1648846800, "url": ["./_img/canvas/place30/016.png", "./_img/canvas/place30/015_016.png"] }, { "timestamp": 1648848600, "url": "./_img/canvas/place30/016.png" }, { "timestamp": 1648850400, "url": ["./_img/canvas/place30/016.png", "./_img/canvas/place30/017_016.png"] }, { "timestamp": 1648852200, "url": ["./_img/canvas/place30/016.png", "./_img/canvas/place30/018_016.png"] }, { "timestamp": 1648854000, "url": ["./_img/canvas/place30/016.png", "./_img/canvas/place30/019_016.png"] }, { "timestamp": 1648855800, "url": ["./_img/canvas/place30/016.png", "./_img/canvas/place30/020_016.png"] }, { "timestamp": 1648857600, "url": ["./_img/canvas/place30/016.png", "./_img/canvas/place30/021_016.png"] }, { "timestamp": 1648859400, "url": ["./_img/canvas/place30/027.png", "./_img/canvas/place30/022_027.png"] }, { "timestamp": 1648861200, "url": ["./_img/canvas/place30/027.png", "./_img/canvas/place30/023_027.png"] }, { "timestamp": 1648863000, "url": ["./_img/canvas/place30/027.png", "./_img/canvas/place30/024_027.png"] }, { "timestamp": 1648864800, "url": ["./_img/canvas/place30/027.png", "./_img/canvas/place30/025_027.png"] }, { "timestamp": 1648866600, "url": ["./_img/canvas/place30/027.png", "./_img/canvas/place30/026_027.png"] }, { "timestamp": 1648868400, "url": "./_img/canvas/place30/027.png" }, { "timestamp": 1648870200, "url": ["./_img/canvas/place30/027.png", "./_img/canvas/place30/028_027.png"] }, { "timestamp": 1648872000, "url": ["./_img/canvas/place30/027.png", "./_img/canvas/place30/029_027.png"] }, { "timestamp": 1648873800, "url": ["./_img/canvas/place30/027.png", "./_img/canvas/place30/030_027.png"] }, { "timestamp": 1648875600, "url": ["./_img/canvas/place30/027.png", "./_img/canvas/place30/031_027.png"] }, { "timestamp": 1648877400, "url": ["./_img/canvas/place30/027.png", "./_img/canvas/place30/032_027.png"] }, { "timestamp": 1648879200, "url": ["./_img/canvas/place30/038.png", "./_img/canvas/place30/033_038.png"] }, { "timestamp": 1648881000, "url": ["./_img/canvas/place30/038.png", "./_img/canvas/place30/034_038.png"] }, { "timestamp": 1648882800, "url": ["./_img/canvas/place30/038.png", "./_img/canvas/place30/035_038.png"] }, { "timestamp": 1648884600, "url": ["./_img/canvas/place30/038.png", "./_img/canvas/place30/036_038.png"] }, { "timestamp": 1648886400, "url": ["./_img/canvas/place30/038.png", "./_img/canvas/place30/037_038.png"] }, { "timestamp": 16488
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 192 192" aria-hidden="true"><polygon points="154 0 154 38 39 38 39 192 0 192 0 0"/><polygon points="192 38 192 192 77 192 77 153 154 153 154 38"/><rect x="77" y="77" width="38" height="38"/></svg>'
2022-04-16 14:52:04 +02:00
},
tfc: {
name: "The Final Clean",
code: "T",
default: 0,
drawablePeriods: [0, 0],
2022-04-16 14:52:04 +02:00
versions: [
{
timestamp: "Final",
url: "./_img/canvas/tfc/final.png",
},
2022-04-24 09:51:41 +02:00
],
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 192 192"><defs><style>.a{fill-rule:evenodd;}</style></defs><path class="a" d="M69.79,83.55c-.47,.65-.59,1.35-.59,1.35-.26,1.47,.76,2.72,.92,3.12,2.84,7.1,4.49,13.93,3.97,16.39-.47,2.18-5.6,5.65-12.36,8.33-3.63,1.44-6.11,2.99-8.04,5.01-7.17,7.51-10.24,17.86-7.14,24.05,3.93,7.84,18.38,5.86,28.05-3.85,2.09-2.1,3.15-3.83,6.63-10.77,2.97-5.93,4.26-8.05,5.47-8.95,2.04-1.52,9.82,.1,17.41,3.64,1.71,.8,2.31,1.04,2.78,.98,0,0,.22-.05,.43-.14,1.31-.59,17.43-17,25.58-25.34-1.79,.09-3.57,.18-5.36,.28-2.84,2.63-5.68,5.27-8.52,7.9-10.85-10.85-21.7-21.71-32.55-32.56,1.73-1.8,3.46-3.6,5.18-5.4-.29-1.56-.57-3.12-.86-4.69-1.34,1.27-19.42,18.45-21.01,20.66Zm-10.45,44.57c2.5,0,4.53,2.03,4.53,4.53s-2.03,4.53-4.53,4.53-4.53-2.03-4.53-4.53,2.03-4.53,4.53-4.53Z"/><path class="f" d="M132.9,97.36c-.88,.22-7.88,1.92-9.91-1.04-1.11-1.62-1.05-4.71-.52-6.57,.74-2.59,.9-4.06,.25-4.73-.73-.76-2.03-.31-3.73-.18-3.4,.27-8.08-.86-9.6-3.16-2.77-4.21,4.48-13.03,2.31-14.69-.17-.13-.34-.16-.67-.22-4.24-.73-6.79,4.71-11.66,5.1-2.93,.24-6.21-1.39-7.72-4.02-1.11-1.94-1-3.96-.86-4.95h0s7.38-7.39,17.6-17.52c12.75,12.73,25.51,25.47,38.26,38.2l-13.75,13.79Z"/><polygon points="154 0 154 38 39 38 39 192 0 192 0 0"/><polygon points="192 38 192 192 77 192 77 153 154 153 154 38"/></svg>'
}
2022-04-16 14:51:51 +02:00
}
const codeReference = {}
const imageCache = {}
2022-04-16 14:51:51 +02:00
const variantsEl = document.getElementById("variants")
2022-04-16 14:26:31 +02:00
for (const variation in variationsConfig) {
codeReference[variationsConfig[variation].code] = variation
const optionEl = document.createElement('option')
optionEl.value = variation
optionEl.textContent = variationsConfig[variation].name
variantsEl.appendChild(optionEl)
2022-04-16 14:51:51 +02:00
}
const timelineSlider = document.getElementById("timeControlsSlider");
const tooltip = document.getElementById("timeControlsTooltip")
const image = document.getElementById("image");
let abortController = new AbortController()
let currentUpdateIndex = 0
let updateTimeout = setTimeout(null, 0)
let tooltipDelayHide = setTimeout(null, 0)
2022-04-05 20:17:39 +02:00
let currentVariation = "default"
2022-04-16 14:26:31 +02:00
const defaultPeriod = variationsConfig[currentVariation].default
const defaultVariation = currentVariation
let currentPeriod = defaultPeriod
window.currentPeriod = currentPeriod
window.currentVariation = currentVariation
2022-04-08 01:11:29 +02:00
2022-04-05 20:17:39 +02:00
// SETUP
timelineSlider.max = variationsConfig[currentVariation].versions.length - 1;
timelineSlider.value = currentPeriod;
2022-04-05 20:17:39 +02:00
timelineSlider.addEventListener("input", (event) => {
updateTooltip(parseInt(event.target.value), currentVariation)
clearTimeout(updateTimeout)
updateTimeout = setTimeout(() => {
updateTime(parseInt(timelineSlider.value), currentVariation)
setTimeout(() => {
2022-04-17 04:53:43 +02:00
if (timelineSlider.value != currentPeriod && abortController.signal.aborted) {
updateTime(parseInt(timelineSlider.value), currentVariation)
}
}, 50)
}, 25)
2022-04-16 14:51:51 +02:00
})
2022-04-05 20:17:39 +02:00
2022-04-16 14:51:51 +02:00
variantsEl.addEventListener("input", (event) => {
updateTime(-1, event.target.value)
2022-04-10 09:03:08 +02:00
})
2022-04-05 20:17:39 +02:00
const dispatchTimeUpdateEvent = (period = timelineSlider.value, atlas = atlas) => {
const timeUpdateEvent = new CustomEvent('timeupdate', {
detail: {
period: period,
atlas: atlas
}
});
document.dispatchEvent(timeUpdateEvent);
2022-04-10 11:11:34 +02:00
}
async function updateBackground(newPeriod = currentPeriod, newVariation = currentVariation) {
abortController.abort()
abortController = new AbortController()
currentUpdateIndex++
const myUpdateIndex = currentUpdateIndex
const variationConfig = variationsConfig[newVariation]
2022-04-24 09:51:41 +02:00
variantsEl.value = currentVariation
variantsEl.previousElementSibling.innerHTML = variationConfig.icon;
2022-04-24 09:51:41 +02:00
const configObject = variationConfig.versions[currentPeriod];
if (typeof configObject.url === "string") {
if (imageCache[configObject.url] === undefined) {
const fetchResult = await fetch(configObject.url, {
signal: abortController.signal
});
if (currentUpdateIndex !== myUpdateIndex) {
return [configObject, newPeriod, newVariation]
}
const imageBlob = await fetchResult.blob()
imageCache[configObject.url] = URL.createObjectURL(imageBlob)
}
image.src = imageCache[configObject.url]
} else {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
context.canvas.width = 2000
context.canvas.height = 2000
await Promise.all(configObject.url.map(async url => {
if (imageCache[url] === undefined) {
const fetchResult = await fetch(url, {
signal: abortController.signal
});
if (currentUpdateIndex !== myUpdateIndex) {
return
}
const imageBlob = await fetchResult.blob()
imageCache[url] = URL.createObjectURL(imageBlob)
}
}))
for await (const url of configObject.url) {
const imageLayer = new Image()
await new Promise(resolve => {
imageLayer.onload = () => {
context.drawImage(imageLayer, 0, 0)
resolve()
}
imageLayer.src = imageCache[url]
})
}
if (currentUpdateIndex !== myUpdateIndex) return [configObject, newPeriod, newVariation]
const blob = await new Promise(resolve => canvas.toBlob(resolve))
image.src = URL.createObjectURL(blob)
}
}
async function updateTime(newPeriod = currentPeriod, newVariation = currentVariation, forcePeriod = false) {
document.body.dataset.canvasLoading = ""
if (!variationsConfig[newVariation]) newVariation = defaultVariation
const variationConfig = variationsConfig[newVariation]
if (newPeriod < 0) newPeriod = 0
else if (newPeriod > variationConfig.versions.length - 1) newPeriod = variationConfig.versions.length - 1
currentPeriod = newPeriod
if (currentVariation !== newVariation) {
currentVariation = newVariation
timelineSlider.max = variationConfig.versions.length - 1;
if (!forcePeriod) {
currentPeriod = variationConfig.default;
newPeriod = currentPeriod
}
if (variationConfig.versions.length === 1) document.getElementById("bottomBar").classList.add('no-time-slider');
else document.getElementById("bottomBar").classList.remove('no-time-slider');
}
timelineSlider.value = currentPeriod
updateTooltip(newPeriod, newVariation)
await updateBackground(newPeriod, newVariation)
atlas = []
for (const atlasIndex in atlasAll) {
2022-04-23 15:29:22 +02:00
let chosenIndex
const validPeriods2 = Object.keys(atlasAll[atlasIndex].path)
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
}
}
if (chosenIndex !== undefined) break
}
if (chosenIndex === undefined) continue
2022-04-23 15:29:22 +02:00
const pathChosen = Object.values(atlasAll[atlasIndex].path)[chosenIndex]
const centerChosen = Object.values(atlasAll[atlasIndex].center)[chosenIndex]
if (pathChosen === undefined) continue
atlas.push({
...atlasAll[atlasIndex],
path: pathChosen,
center: centerChosen,
})
}
dispatchTimeUpdateEvent(newPeriod, atlas)
delete document.body.dataset.canvasLoading
tooltip.dataset.forceVisible = ""
clearTimeout(tooltipDelayHide)
tooltipDelayHide = setTimeout(() => {
delete tooltip.dataset.forceVisible
}, 1000)
}
function updateTooltip(newPeriod, newVariation) {
const configObject = variationsConfig[newVariation].versions[newPeriod]
// If timestap is a number return a UTC formatted date otherwise use exact timestap label
if (typeof configObject.timestamp === "number") tooltip.querySelector('div').textContent = new Date(configObject.timestamp * 1000).toUTCString()
else tooltip.querySelector('div').textContent = configObject.timestamp;
// Clamps position of tooltip to prevent from going off screen
const timelineSliderRect = timelineSlider.getBoundingClientRect();
let min = -timelineSliderRect.left+12;
let max = (window.innerWidth-tooltip.offsetWidth)-timelineSliderRect.left+4;
tooltip.style.left = Math.min(Math.max((timelineSlider.offsetWidth)*(timelineSlider.value)/(timelineSlider.max)-tooltip.offsetWidth/2, min), max) + "px";
2022-04-10 09:03:08 +02:00
}
tooltip.parentElement.addEventListener('mouseenter', () => updateTooltip(parseInt(timelineSlider.value), currentVariation))
2022-04-10 09:03:08 +02:00
window.addEventListener('resize', () => updateTooltip(parseInt(timelineSlider.value), currentVariation))
2022-04-10 09:03:08 +02:00
2022-04-16 14:51:51 +02:00
function isOnPeriod(start, end, variation, currentPeriod, currentVariation) {
return currentPeriod >= start && currentPeriod <= end && variation === currentVariation
2022-04-14 16:03:17 +02:00
}
function parsePeriod(periodString) {
let variation = defaultVariation
2022-04-14 16:03:17 +02:00
periodString = periodString + ""
if (periodString.split(':').length > 1) {
const split = periodString.split(':')
variation = codeReference[split[0]]
periodString = split[1]
}
2022-04-14 16:03:17 +02:00
if (periodString.search('-') + 1) {
let [start, end] = periodString.split('-').map(i => parseInt(i))
2022-04-16 14:51:51 +02:00
return [start, end, variation]
} else if (codeReference[periodString]) {
variation = codeReference[periodString]
const defaultPeriod = variationsConfig[variation].default
return [defaultPeriod, defaultPeriod, variation]
2022-04-14 16:03:17 +02:00
} else {
2022-04-16 14:26:31 +02:00
const periodNew = parseInt(periodString)
2022-04-16 14:51:51 +02:00
return [periodNew, periodNew, variation]
2022-04-14 16:03:17 +02:00
}
}
function formatPeriod(start, end, variation) {
2022-04-30 06:28:20 +02:00
let periodString, variationString
variationString = variationsConfig[variation].code
if (start === end) {
2022-04-30 06:28:20 +02:00
if (start === variationsConfig[variation].default && variation !== defaultVariation) {
periodString = ""
}
else periodString = start
}
else periodString = start + "-" + end
2022-04-30 06:28:20 +02:00
if (periodString && variationString) return variationsConfig[variation].code + ":" + periodString
if (variationString) return variationString
return periodString
}