atlas/web/_js/draw.js

1122 lines
35 KiB
JavaScript
Raw Normal View History

/*
========================================================================
2022-04-28 12:41:19 +02:00
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
2022-04-09 03:23:55 +02:00
https://place-atlas.stefanocoding.me/license.txt
========================================================================
*/
2022-05-06 09:41:22 +02:00
const finishButton = document.getElementById("finishButton")
const resetButton = document.getElementById("resetButton")
const undoButton = document.getElementById("undoButton")
const redoButton = document.getElementById("redoButton")
const highlightUnchartedLabel = document.getElementById("highlightUnchartedLabel")
2022-04-14 08:41:13 +02:00
2022-05-06 09:41:22 +02:00
const drawControlsBody = document.getElementById("offcanvasDraw-drawControls")
const objectInfoBody = document.getElementById("offcanvasDraw-objectInfo")
const objectInfoForm = document.getElementById("objectInfo")
2022-05-06 09:41:22 +02:00
const hintText = document.getElementById("hint")
2022-04-14 08:41:13 +02:00
2022-05-06 09:41:22 +02:00
const periodsStatus = document.getElementById('periodsStatus')
const periodGroups = document.getElementById('periodGroups')
const periodGroupTemplate = document.getElementById('period-group').content.firstElementChild.cloneNode(true)
const periodsAdd = document.getElementById('periodsAdd')
2022-04-14 09:56:30 +02:00
2022-05-06 09:41:22 +02:00
const exportButton = document.getElementById("exportButton")
const cancelButton = document.getElementById("cancelButton")
2022-04-14 08:41:13 +02:00
2022-05-06 09:41:22 +02:00
const exportModal = new bootstrap.Modal(document.getElementById("exportModal"))
const exportModalElement = document.getElementById("exportModal")
2022-05-06 09:41:22 +02:00
const exportOverlay = document.getElementById("exportOverlay")
const exportCloseButton = document.getElementById("exportCloseButton")
const exportBackButton = document.getElementById("exportBackButton")
2022-05-06 09:41:22 +02:00
const nameField = document.getElementById("nameField")
const descriptionField = document.getElementById("descriptionField")
const websiteGroup = document.getElementById("websiteGroup")
const subredditGroup = document.getElementById("subredditGroup")
const discordGroup = document.getElementById("discordGroup")
const wikiGroup = document.getElementById("wikiGroup")
const exportArea = document.getElementById("exportString")
const subredditPattern = /^(?:(?:(?:(?:(?:https?:\/\/)?(?:(?:www|old|new|np)\.)?)?reddit\.com)?\/)?[rR]\/)?([A-Za-z0-9][A-Za-z0-9_]{2,20})(?:\/[^" ]*)*$/
const discordPattern = /^(?:(?:https?:\/\/)?(?:www\.)?(?:(?:discord)?\.?gg|discord(?:app)?\.com\/invite)\/)?([^\s/]+?)(?=\b)$/
2022-05-06 09:41:22 +02:00
let entryId = 0
let path = []
let center = [1000, 1000]
2022-04-14 08:41:13 +02:00
2022-05-06 09:41:22 +02:00
let websiteGroupElements = []
let subredditGroupElements = []
let discordGroupElements = []
let wikiGroupElements = []
2022-04-14 08:41:13 +02:00
2022-05-06 09:41:22 +02:00
let pathWithPeriods = []
let periodGroupElements = []
2022-05-06 09:41:22 +02:00
let disableDrawingOverride = false
let drawing = true
2022-04-14 08:41:13 +02:00
2022-05-06 09:41:22 +02:00
let undoHistory = []
const periodClipboard = {
"index": null,
"path": null
}
;[...document.querySelectorAll("#objectInfo textarea")].forEach(el => {
el.addEventListener("input", function () {
2022-05-06 09:41:22 +02:00
this.style.height = "auto"
this.style.height = (this.scrollHeight) + "px"
})
})
window.initDraw = initDraw
function initDraw() {
2022-04-09 11:27:14 +02:00
wrapper.classList.remove('listHidden')
2022-05-06 09:41:22 +02:00
var backButton = document.getElementById("showListButton")
backButton.insertAdjacentHTML("afterend", '<button class="btn btn-outline-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasDraw" aria-controls="offcanvasDraw">Menu</button><a id="drawBackButton" class="btn btn-outline-primary" href="./">Exit Draw Mode</a>')
backButton.remove()
2022-04-20 10:54:58 +02:00
2022-05-06 09:41:22 +02:00
var myOffcanvas = document.getElementById("offcanvasDraw")
var bsOffcanvas = new bootstrap.Offcanvas(myOffcanvas)
bsOffcanvas.show()
2022-04-20 10:54:58 +02:00
2022-04-09 11:27:14 +02:00
window.render = render
window.renderBackground = renderBackground
window.updateHovering = updateHovering
// let startPeriodField = document.getElementById('startPeriodField')
// let endPeriodField = document.getElementById('endPeriodField')
// let periodVisbilityInfo = document.getElementById('periodVisbilityInfo')
2022-05-06 09:41:22 +02:00
let rShiftPressed = false
let lShiftPressed = false
let shiftPressed = false
2022-05-06 09:41:22 +02:00
let highlightUncharted = true
2022-05-06 09:41:22 +02:00
renderBackground()
applyView()
2022-05-06 09:41:22 +02:00
container.style.cursor = "crosshair"
2022-05-06 09:41:22 +02:00
render(path)
container.addEventListener("mousedown", function (e) {
lastPos = [
e.clientX,
e.clientY
2022-05-06 09:41:22 +02:00
]
})
function getCanvasCoords(x, y) {
2022-05-06 09:41:22 +02:00
x = x - container.offsetLeft
y = y - container.offsetTop
const pos = [
~~((x - (container.clientWidth / 2 - innerContainer.clientWidth / 2 + zoomOrigin[0])) / zoom) + 0.5,
~~((y - (container.clientHeight / 2 - innerContainer.clientHeight / 2 + zoomOrigin[1])) / zoom) + 0.5
2022-05-06 09:41:22 +02:00
]
if (shiftPressed && path.length > 0) {
2022-05-06 09:41:22 +02:00
const previous = path[path.length - 1]
if (Math.abs(pos[1] - previous[1]) > Math.abs(pos[0] - previous[0])) {
2022-05-06 09:41:22 +02:00
pos[0] = previous[0]
} else {
2022-05-06 09:41:22 +02:00
pos[1] = previous[1]
}
}
2022-05-06 09:41:22 +02:00
return pos
}
container.addEventListener("mouseup", function (e) {
if (Math.abs(lastPos[0] - e.clientX) + Math.abs(lastPos[1] - e.clientY) <= 4 && drawing) {
2022-05-06 09:41:22 +02:00
const coords = getCanvasCoords(e.clientX, e.clientY)
2022-05-06 09:41:22 +02:00
path.push(coords)
render(path)
2022-05-06 09:41:22 +02:00
undoHistory = []
redoButton.disabled = true
undoButton.disabled = false
if (path.length >= 3) {
2022-05-06 09:41:22 +02:00
finishButton.disabled = false
}
updatePath()
}
2022-05-06 09:41:22 +02:00
})
window.addEventListener("mousemove", function (e) {
2022-04-09 11:27:14 +02:00
if (!dragging && drawing && path.length > 0) {
2022-04-09 11:27:14 +02:00
2022-05-06 09:41:22 +02:00
const coords = getCanvasCoords(e.clientX, e.clientY)
render([...path, coords])
}
2022-05-06 09:41:22 +02:00
})
2022-05-06 09:41:22 +02:00
window.addEventListener("keyup", function (e) {
if (e.key == "z" && e.ctrlKey) {
undo()
} else if (e.key == "y" && e.ctrlKey) {
2022-05-06 09:41:22 +02:00
redo()
} else if (e.key === "Shift") {
if (e.code === "ShiftRight") {
rShiftPressed = false
} else if (e.code === "ShiftLeft") {
2022-05-06 09:41:22 +02:00
lShiftPressed = false
}
2022-05-06 09:41:22 +02:00
shiftPressed = rShiftPressed || lShiftPressed
}
2022-05-06 09:41:22 +02:00
})
2022-05-06 09:41:22 +02:00
window.addEventListener("keydown", function (e) {
if (e.key === "Shift") {
if (e.code === "ShiftRight") {
2022-05-06 09:41:22 +02:00
rShiftPressed = true
} else if (e.code === "ShiftLeft") {
2022-05-06 09:41:22 +02:00
lShiftPressed = true
}
2022-05-06 09:41:22 +02:00
shiftPressed = rShiftPressed || lShiftPressed
}
2022-05-06 09:41:22 +02:00
})
finishButton.addEventListener("click", function () {
finish()
})
undoButton.addEventListener("click", function (e) {
undo()
const coords = getCanvasCoords(e.clientX, e.clientY)
render([...path, coords])
})
redoButton.addEventListener("click", function (e) {
redo()
const coords = getCanvasCoords(e.clientX, e.clientY)
render([...path, coords])
})
resetButton.addEventListener("click", function (e) {
reset()
const coords = getCanvasCoords(e.clientX, e.clientY)
render([...path, coords])
})
resetButton.addEventListener("blur", function () {
resetButton.textContent = "Reset"
resetButton.className = "btn btn-secondary"
})
cancelButton.addEventListener("click", function () {
back()
})
// Refocus on button when modal is closed
2022-05-06 09:41:22 +02:00
exportModalElement.addEventListener('hidden.bs.modal', function () {
exportButton.focus()
})
2022-05-06 09:41:22 +02:00
objectInfoForm.addEventListener('submit', function (e) {
2022-04-20 10:54:58 +02:00
e.preventDefault()
// Allows for html form validation with preview button
if (e.submitter && e.submitter.value == "Preview") {
preview()
} else {
exportJson()
}
2022-05-06 09:41:22 +02:00
})
2022-05-06 09:41:22 +02:00
document.getElementById("highlightUncharted").addEventListener("click", function () {
highlightUncharted = this.checked
render(path)
})
function generateExportObject() {
const exportObject = {
id: entryId,
name: nameField.value,
description: descriptionField.value,
links: {},
2022-04-14 16:03:17 +02:00
path: {},
center: {},
2022-05-06 09:41:22 +02:00
}
2022-04-30 06:36:00 +02:00
const pathWithPeriodsTemp = JSON.parse(JSON.stringify(pathWithPeriods))
2022-04-14 16:03:17 +02:00
for (let i = pathWithPeriodsTemp.length - 1; i > 0; i--) {
for (let j = 0; j < i; j++) {
if (JSON.stringify(pathWithPeriodsTemp[i][1]) === JSON.stringify(pathWithPeriodsTemp[j][1])) {
2022-05-06 09:41:22 +02:00
pathWithPeriodsTemp[j][0] = pathWithPeriodsTemp[i][0] + ', ' + pathWithPeriodsTemp[j][0]
pathWithPeriodsTemp.splice(i, 1)
break
2022-04-14 16:03:17 +02:00
}
}
2022-04-10 11:11:34 +02:00
}
2022-04-14 16:03:17 +02:00
pathWithPeriodsTemp.forEach(([key, value]) => {
// TODO: Compress periods on something like 0-13, 14.
2022-05-06 09:41:22 +02:00
exportObject.path[key] = value
exportObject.center[key] = calculateCenter(value)
2022-04-14 16:03:17 +02:00
})
2022-05-06 09:41:22 +02:00
const inputWebsite = websiteGroupElements.map(element => element.value.trim()).filter(element => element)
const inputSubreddit = subredditGroupElements.map(element => element.value.trim().match(subredditPattern)?.[1]).filter(element => element)
const inputDiscord = discordGroupElements.map(element => element.value.trim().match(discordPattern)?.[1]).filter(element => element)
const inputWiki = wikiGroupElements.map(element => element.value.trim().replace(/ /g, '_')).filter(element => element)
2022-05-06 09:41:22 +02:00
if (inputWebsite.length) exportObject.links.website = inputWebsite
if (inputSubreddit.length) exportObject.links.subreddit = inputSubreddit
if (inputDiscord.length) exportObject.links.discord = inputDiscord
if (inputWiki.length) exportObject.links.wiki = inputWiki
2022-05-06 09:41:22 +02:00
return exportObject
}
function exportJson() {
const exportObject = generateExportObject()
2022-05-06 09:41:22 +02:00
let jsonString = JSON.stringify(exportObject, null, "\t")
jsonString = jsonString.split("\n")
jsonString = jsonString.join("\n ")
jsonString = " " + jsonString
exportArea.value = jsonString
let directPostUrl = "https://www.reddit.com/r/placeAtlas2/submit?selftext=true&title=New%20Submission&text=" + encodeURIComponent(exportArea.value)
if (jsonString.length > 7493) {
2022-05-06 09:41:22 +02:00
directPostUrl = "https://www.reddit.com/r/placeAtlas2/submit?selftext=true&title=New%20Submission&text=" + encodeURIComponent(" " + JSON.stringify(exportObject))
}
2022-05-06 09:41:22 +02:00
document.getElementById("exportDirectPost").href = directPostUrl
2022-05-06 09:41:22 +02:00
if (entryId == 0) document.getElementById("redditFlair").textContent = "New Entry"
else document.getElementById("redditFlair").textContent = "Edit Entry"
2022-05-06 09:41:22 +02:00
exportModal.show()
}
function preview() {
2022-05-06 09:41:22 +02:00
let infoElement = createInfoBlock(generateExportObject(), true)
objectsContainer.replaceChildren()
objectsContainer.appendChild(infoElement)
closeObjectsListButton.classList.remove("d-none")
}
function undo() {
if (path.length > 0 && drawing) {
2022-05-06 09:41:22 +02:00
undoHistory.push(path.pop())
redoButton.disabled = false
2022-05-06 12:28:26 +02:00
updatePath(path, undoHistory)
}
}
function redo() {
if (undoHistory.length > 0 && drawing) {
2022-05-06 09:41:22 +02:00
path.push(undoHistory.pop())
undoButton.disabled = false
2022-05-06 12:28:26 +02:00
updatePath(path, undoHistory)
}
}
function finish() {
2022-04-14 09:56:30 +02:00
updatePath()
2022-05-06 09:41:22 +02:00
drawing = false
disableDrawingOverride = true
objectInfoBody.classList.remove("d-none")
drawControlsBody.classList.add("d-none")
;[...document.querySelectorAll("#objectInfo textarea")].forEach(el => {
if (el.value) el.style.height = (el.scrollHeight) + "px"
})
2022-04-14 08:41:13 +02:00
// if (isOnPeriod()) {
// periodVisbilityInfo.textContent = ""
// } else {
// periodVisbilityInfo.textContent = "Not visible during this period!"
// }
}
function reset() {
2022-04-24 09:51:41 +02:00
// Requires button to be pressed twice to confirm reset
if (resetButton.textContent == "Confirm Reset") {
2022-05-06 09:41:22 +02:00
resetButton.textContent = "Reset"
resetButton.className = "btn btn-secondary"
2022-04-24 09:51:41 +02:00
updatePath([])
2022-05-06 09:41:22 +02:00
undoHistory = []
drawing = true
disableDrawingOverride = false
objectInfoBody.classList.add("d-none")
drawControlsBody.classList.remove("d-none")
2022-04-24 09:51:41 +02:00
// Blanks input values
2022-05-06 09:41:22 +02:00
nameField.value = ""
descriptionField.value = ""
2022-04-24 09:51:41 +02:00
// Clears input array
2022-05-06 09:41:22 +02:00
websiteGroupElements = []
subredditGroupElements = []
discordGroupElements = []
wikiGroupElements = []
2022-04-24 09:51:41 +02:00
// Rebuilds multi-input list
2022-05-06 09:41:22 +02:00
websiteGroup.replaceChildren()
subredditGroup.replaceChildren()
discordGroup.replaceChildren()
wikiGroup.replaceChildren()
addWebsiteFields("", 0, [0])
addSubredditFields("", 0, [0])
addDiscordFields("", 0, [0])
addWikiFields("", 0, [0])
2022-04-26 01:56:23 +02:00
// Resets periods
pathWithPeriods = []
pathWithPeriods.push([defaultPeriod, []])
initPeriodGroups()
2022-04-24 09:51:41 +02:00
} else {
2022-05-06 09:41:22 +02:00
resetButton.textContent = "Confirm Reset"
resetButton.className = "btn btn-danger"
2022-04-24 09:51:41 +02:00
}
2022-05-06 09:41:22 +02:00
}
function back() {
2022-05-06 09:41:22 +02:00
drawing = true
disableDrawingOverride = false
2022-04-14 09:56:30 +02:00
updatePath()
2022-05-06 09:41:22 +02:00
objectInfoBody.classList.add("d-none")
drawControlsBody.classList.remove("d-none")
// Clears preview
2022-05-06 09:41:22 +02:00
objectsContainer.replaceChildren()
closeObjectsListButton.classList.add("d-none")
}
function renderBackground() {
2022-05-06 09:41:22 +02:00
backgroundContext.clearRect(0, 0, canvas.width, canvas.height)
2022-05-06 09:41:22 +02:00
backgroundContext.fillStyle = "rgba(0, 0, 0, 1)"
//backgroundContext.fillRect(0, 0, canvas.width, canvas.height)
for (let i = 0; i < atlas.length; i++) {
2022-05-06 09:41:22 +02:00
const path = atlas[i].path
2022-05-06 09:41:22 +02:00
backgroundContext.beginPath()
if (path[0]) {
2022-05-06 09:41:22 +02:00
backgroundContext.moveTo(path[0][0], path[0][1])
}
for (let p = 1; p < path.length; p++) {
2022-05-06 09:41:22 +02:00
backgroundContext.lineTo(path[p][0], path[p][1])
}
2022-05-06 09:41:22 +02:00
backgroundContext.closePath()
2022-05-06 09:41:22 +02:00
backgroundContext.fill()
}
}
function render(path) {
if (!Array.isArray(path)) return
2022-05-06 09:41:22 +02:00
context.globalCompositeOperation = "source-over"
context.clearRect(0, 0, canvas.width, canvas.height)
if (highlightUncharted) {
2022-05-06 09:41:22 +02:00
context.drawImage(backgroundCanvas, 0, 0)
context.fillStyle = "rgba(0, 0, 0, 0.4)"
} else {
2022-05-06 09:41:22 +02:00
context.fillStyle = "rgba(0, 0, 0, 0.6)"
}
2022-05-06 09:41:22 +02:00
context.fillRect(0, 0, canvas.width, canvas.height)
2022-05-06 09:41:22 +02:00
context.beginPath()
if (path[0]) {
2022-05-06 09:41:22 +02:00
context.moveTo(path[0][0], path[0][1])
}
for (let i = 1; i < path.length; i++) {
2022-05-06 09:41:22 +02:00
context.lineTo(path[i][0], path[i][1])
}
2022-05-06 09:41:22 +02:00
context.closePath()
2022-05-06 09:41:22 +02:00
context.strokeStyle = "rgba(255, 255, 255, 1)"
context.stroke()
2022-05-06 09:41:22 +02:00
context.globalCompositeOperation = "destination-out"
2022-05-06 09:41:22 +02:00
context.fillStyle = "rgba(0, 0, 0, 1)"
context.fill()
}
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
2022-05-06 09:41:22 +02:00
]
2022-05-06 09:41:22 +02:00
const coords_p = document.getElementById("coords_p")
2022-04-20 10:54:58 +02:00
// Displays coordinates as zero instead of NaN
if (isNaN(pos[0]) == true) {
2022-05-06 09:41:22 +02:00
coords_p.innerText = "0, 0"
2022-04-20 10:54:58 +02:00
} else {
2022-05-06 09:41:22 +02:00
coords_p.innerText = Math.ceil(pos[0]) + ", " + Math.ceil(pos[1])
2022-04-20 10:54:58 +02:00
}
2022-04-09 11:27:14 +02:00
}
}
const getEntry = id => {
const entries = atlasAll.filter(entry => entry.id === id)
if (entries.length === 1) return entries[0]
return {}
}
2022-04-16 14:26:31 +02:00
const params = new URLSearchParams(document.location.search)
function addFieldButton(inputButton, inputGroup, array, index, name) {
2022-05-06 09:41:22 +02:00
console.log("add button fired")
if (inputButton.title == "Remove " + name) {
2022-05-06 09:41:22 +02:00
console.log("add (now remove) button fired")
removeFieldButton(inputGroup, array, index)
return
}
2022-05-06 09:41:22 +02:00
inputButton.className = "btn btn-outline-secondary"
inputButton.title = "Remove " + name
inputButton.innerHTML = '<i class="bi bi-trash-fill" aria-hidden="true"></i>'
console.log(array)
if (name == "website") {
2022-05-06 09:41:22 +02:00
addWebsiteFields(null, array.length, array)
} else if (name == "subreddit") {
2022-05-06 09:41:22 +02:00
addSubredditFields(null, array.length, array)
} else if (name == "Discord invite") {
2022-05-06 09:41:22 +02:00
addDiscordFields(null, array.length, array)
} else if (name == "wiki page") {
2022-05-06 09:41:22 +02:00
addWikiFields(null, array.length, array)
}
}
function removeFieldButton(inputGroup, array, index) {
2022-05-06 09:41:22 +02:00
console.log("remove button fired")
delete array[index]
inputGroup.remove()
console.log(array)
}
function addWebsiteFields(link, index, array) {
2022-05-06 09:41:22 +02:00
const inputGroup = document.createElement("div")
inputGroup.className = "input-group"
websiteGroup.appendChild(inputGroup)
const inputField = document.createElement("input")
inputField.type = "url"
inputField.name = "url"
inputField.className = "form-control"
inputField.id = "websiteField" + index
inputField.placeholder = "https://example.com"
inputField.pattern = "https?://.*"
inputField.title = "Website URL using the http:// or https:// protocol"
inputField.setAttribute("aria-labelledby", "websiteLabel")
inputField.value = link
2022-05-06 09:41:22 +02:00
inputGroup.appendChild(inputField)
websiteGroupElements.push(inputField)
2022-05-06 09:41:22 +02:00
const inputButton = document.createElement("button")
inputButton.type = "button"
// If button is the last in the array give it the add button
if (array.length == index + 1) {
2022-05-06 09:41:22 +02:00
inputButton.className = "btn btn-secondary"
inputButton.title = "Add website"
inputButton.innerHTML = '<i class="bi bi-plus-lg" aria-hidden="true"></i>'
inputButton.addEventListener('click', () => addFieldButton(inputButton, inputGroup, websiteGroupElements, index, "website"))
} else {
2022-05-06 09:41:22 +02:00
inputButton.className = "btn btn-outline-secondary"
inputButton.title = "Remove website"
inputButton.innerHTML = '<i class="bi bi-trash-fill" aria-hidden="true"></i>'
inputButton.addEventListener('click', () => removeFieldButton(inputGroup, array, index))
}
2022-05-06 09:41:22 +02:00
inputGroup.appendChild(inputButton)
}
function addSubredditFields(link, index, array) {
2022-05-06 09:41:22 +02:00
const inputGroup = document.createElement("div")
inputGroup.className = "input-group"
subredditGroup.appendChild(inputGroup)
const inputAddon = document.createElement("span")
inputAddon.className = "input-group-text"
inputAddon.id = "subredditField" + index + "-addon"
inputAddon.textContent = "reddit.com/"
inputGroup.appendChild(inputAddon)
const inputField = document.createElement("input")
inputField.type = "text"
inputField.className = "form-control"
inputField.id = "subredditField" + index
inputField.placeholder = "r/example"
inputField.pattern = "^r\/[A-Za-z0-9][A-Za-z0-9_]{2,20}$"
inputField.title = "Subbredit in format of r/example"
inputField.minLength = "4"
inputField.maxLength = "23"
inputField.spellcheck = false
inputField.setAttribute("aria-labelledby", "subredditLabel")
inputField.setAttribute("aria-describedby", "subredditField" + index + "-addon")
if (link) {
2022-05-06 09:41:22 +02:00
inputField.value = "r/" + link
} else {
2022-05-06 09:41:22 +02:00
inputField.value = ""
}
2022-05-06 09:41:22 +02:00
inputGroup.appendChild(inputField)
subredditGroupElements.push(inputField)
2022-05-06 09:41:22 +02:00
const inputButton = document.createElement("button")
inputButton.type = "button"
// If button is the last in the array give it the add button
if (array.length == index + 1) {
2022-05-06 09:41:22 +02:00
inputButton.className = "btn btn-secondary"
inputButton.title = "Add subreddit"
inputButton.innerHTML = '<i class="bi bi-plus-lg" aria-hidden="true"></i>'
inputButton.addEventListener('click', () => addFieldButton(inputButton, inputGroup, subredditGroupElements, index, "subreddit"))
} else {
2022-05-06 09:41:22 +02:00
inputButton.className = "btn btn-outline-secondary"
inputButton.title = "Remove subreddit"
inputButton.innerHTML = '<i class="bi bi-trash-fill" aria-hidden="true"></i>'
inputButton.addEventListener('click', () => removeFieldButton(inputGroup, array, index))
}
inputField.addEventListener('paste', (event) => {
let paste = (event.clipboardData || window.clipboardData).getData('text')
paste = paste.trim().match(subredditPattern)?.[1]
if (paste) {
event.target.value = "r/" + paste
event.preventDefault()
}
})
2022-05-06 09:41:22 +02:00
inputGroup.appendChild(inputButton)
}
function addDiscordFields(link, index, array) {
2022-05-06 09:41:22 +02:00
const inputGroup = document.createElement("div")
inputGroup.className = "input-group"
discordGroup.appendChild(inputGroup)
const inputAddon = document.createElement("span")
inputAddon.className = "input-group-text"
inputAddon.id = "discordField" + index + "-addon"
inputAddon.textContent = "discord.gg/"
inputGroup.appendChild(inputAddon)
const inputField = document.createElement("input")
inputField.type = "text"
inputField.className = "form-control"
inputField.id = "discordField" + index
inputField.placeholder = "pJkm23b2nA"
inputField.spellcheck = false
inputField.setAttribute("aria-labelledby", "discordLabel")
inputField.setAttribute("aria-describedby", "discordField" + index + "-addon")
inputField.value = link
2022-05-06 09:41:22 +02:00
inputGroup.appendChild(inputField)
discordGroupElements.push(inputField)
2022-05-06 09:41:22 +02:00
const inputButton = document.createElement("button")
inputButton.type = "button"
// If button is the last in the array give it the add button
if (array.length == index + 1) {
2022-05-06 09:41:22 +02:00
inputButton.className = "btn btn-secondary"
inputButton.title = "Add Discord invite"
inputButton.innerHTML = '<i class="bi bi-plus-lg" aria-hidden="true"></i>'
inputButton.addEventListener('click', () => addFieldButton(inputButton, inputGroup, discordGroupElements, index, "Discord invite"))
} else {
2022-05-06 09:41:22 +02:00
inputButton.className = "btn btn-outline-secondary"
inputButton.title = "Remove Discord invite"
inputButton.innerHTML = '<i class="bi bi-trash-fill" aria-hidden="true"></i>'
inputButton.addEventListener('click', () => removeFieldButton(inputGroup, array, index))
}
inputField.addEventListener('paste', (event) => {
let paste = (event.clipboardData || window.clipboardData).getData('text')
paste = paste.trim().match(discordPattern)?.[1]
if (paste) {
event.target.value = paste
event.preventDefault()
}
})
2022-05-06 09:41:22 +02:00
inputGroup.appendChild(inputButton)
}
function addWikiFields(link, index, array) {
2022-05-06 09:41:22 +02:00
const inputGroup = document.createElement("div")
inputGroup.className = "input-group"
wikiGroup.appendChild(inputGroup)
const inputField = document.createElement("input")
inputField.type = "text"
inputField.className = "form-control"
inputField.id = "wikiField" + index
inputField.placeholder = "Page title"
inputField.spellcheck = false
inputField.setAttribute("aria-labelledby", "wikiLabel")
inputField.value = link
2022-05-06 09:41:22 +02:00
inputGroup.appendChild(inputField)
wikiGroupElements.push(inputField)
2022-05-06 09:41:22 +02:00
const inputButton = document.createElement("button")
inputButton.type = "button"
// If button is the last in the array give it the add button
if (array.length == index + 1) {
2022-05-06 09:41:22 +02:00
inputButton.className = "btn btn-secondary"
inputButton.title = "Add wiki page"
inputButton.innerHTML = '<i class="bi bi-plus-lg" aria-hidden="true"></i>'
inputButton.addEventListener('click', () => addFieldButton(inputButton, inputGroup, wikiGroupElements, index, "wiki page"))
} else {
2022-05-06 09:41:22 +02:00
inputButton.className = "btn btn-outline-secondary"
inputButton.title = "Remove wiki page"
inputButton.innerHTML = '<i class="bi bi-trash-fill" aria-hidden="true"></i>'
inputButton.addEventListener('click', () => removeFieldButton(inputGroup, array, index))
}
2022-05-06 09:41:22 +02:00
inputGroup.appendChild(inputButton)
}
if (params.has('id')) {
const entry = getEntry(params.get('id'))
nameField.value = entry.name
descriptionField.value = entry.description
if (entry.links.website.length) {
entry.links.website.forEach((link, index, array) => {
2022-05-06 09:41:22 +02:00
addWebsiteFields(link, index, array)
})
} else {
2022-05-06 09:41:22 +02:00
addWebsiteFields("", -1, entry.links.website)
}
if (entry.links.subreddit.length) {
entry.links.subreddit.forEach((link, index, array) => {
2022-05-06 09:41:22 +02:00
addSubredditFields(link, index, array)
})
} else {
2022-05-06 09:41:22 +02:00
addSubredditFields("", -1, entry.links.subreddit)
}
if (entry.links.discord.length) {
entry.links.discord.forEach((link, index, array) => {
2022-05-06 09:41:22 +02:00
addDiscordFields(link, index, array)
})
} else {
2022-05-06 09:41:22 +02:00
addDiscordFields("", -1, entry.links.discord)
}
if (entry.links.wiki.length) {
entry.links.wiki.forEach((link, index, array) => {
2022-05-06 09:41:22 +02:00
addWikiFields(link, index, array)
})
} else {
2022-05-06 09:41:22 +02:00
addWikiFields("", -1, entry.links.wiki)
}
2022-05-06 09:41:22 +02:00
redoButton.disabled = true
undoButton.disabled = false
entryId = params.get('id')
Object.entries(entry.path).forEach(([period, path]) => {
period.split(", ").forEach(period => {
pathWithPeriods.push([period, path])
})
})
2022-04-14 08:44:39 +02:00
} else {
2022-05-06 09:41:22 +02:00
document.getElementById("offcanvasDrawLabel").textContent = "New Entry"
2022-04-14 08:44:39 +02:00
pathWithPeriods.push([defaultPeriod, []])
2022-05-06 09:41:22 +02:00
addWebsiteFields("", 0, [0])
addSubredditFields("", 0, [0])
addDiscordFields("", 0, [0])
addWikiFields("", 0, [0])
}
2022-04-14 08:44:39 +02:00
initPeriodGroups()
2022-05-06 09:41:22 +02:00
zoom = 4
zoomOrigin = [
innerContainer.clientWidth / 2 - center[0] * zoom,// + container.offsetLeft
innerContainer.clientHeight / 2 - center[1] * zoom// + container.offsetTop
2022-05-06 09:41:22 +02:00
]
scaleZoomOrigin = [
2000 / 2 - center[0],// + container.offsetLeft
2000 / 2 - center[1]// + container.offsetTop
2022-05-06 09:41:22 +02:00
]
2022-05-06 09:41:22 +02:00
applyView()
2022-04-10 11:11:34 +02:00
document.addEventListener('timeupdate', (event) => {
renderBackground()
2022-04-14 08:41:13 +02:00
updatePeriodGroups()
})
2022-04-14 09:56:30 +02:00
periodsAdd.addEventListener('click', () => {
pathWithPeriods.push([defaultPeriod, []])
initPeriodGroups()
})
2022-04-14 08:41:13 +02:00
}
function calculateCenter(path) {
2022-04-23 15:29:22 +02:00
const result = polylabel(path)
return [Math.floor(result[0]) + 0.5, Math.floor(result[1]) + 0.5]
}
2022-04-14 08:41:13 +02:00
function initPeriodGroups() {
periodGroupElements = []
2022-05-06 09:41:22 +02:00
periodGroups.replaceChildren()
2022-04-14 08:41:13 +02:00
pathWithPeriods.forEach(([period, path], index) => {
2022-04-16 14:26:31 +02:00
const periodGroupEl = periodGroupTemplate.cloneNode(true)
2022-04-14 08:41:13 +02:00
periodGroupEl.id = "periodGroup" + index
2022-04-16 14:26:31 +02:00
const startPeriodEl = periodGroupEl.querySelector('.period-start')
const startPeriodListEl = periodGroupEl.querySelector('#periodStartList')
2022-04-16 14:26:31 +02:00
const endPeriodEl = periodGroupEl.querySelector('.period-end')
const endPeriodListEl = periodGroupEl.querySelector('#periodEndList')
2022-04-16 14:26:31 +02:00
const periodDeleteEl = periodGroupEl.querySelector('.period-delete')
const periodDuplicateEl = periodGroupEl.querySelector('.period-duplicate')
const periodVariationEl = periodGroupEl.querySelector('.period-variation')
const periodCopyEl = periodGroupEl.querySelector('.period-copy')
2022-04-29 05:46:11 +02:00
const periodStatusEl = periodGroupEl.querySelector('.period-status')
2022-04-14 08:41:13 +02:00
2022-04-16 14:26:31 +02:00
const [start, end, variation] = parsePeriod(period)
2022-04-14 08:41:13 +02:00
startPeriodEl.id = "periodStart" + index
startPeriodEl.previousElementSibling.htmlFor = startPeriodEl.id
startPeriodListEl.id = "periodStartList" + index
startPeriodEl.setAttribute("list", startPeriodListEl.id)
2022-04-14 08:41:13 +02:00
endPeriodEl.id = "periodEnd" + index
endPeriodEl.previousElementSibling.htmlFor = endPeriodEl.id
endPeriodListEl.id = "periodEndList" + index
endPeriodEl.setAttribute("list", endPeriodListEl.id)
periodVariationEl.id = "periodVariation" + index
periodCopyEl.id = "periodCopy" + index
2022-04-29 05:46:11 +02:00
periodStatusEl.id = "periodStatus" + index
2022-04-14 08:41:13 +02:00
startPeriodEl.min = variationsConfig[variation].drawablePeriods[0]
endPeriodEl.min = variationsConfig[variation].drawablePeriods[0]
startPeriodEl.max = variationsConfig[variation].drawablePeriods[1]
endPeriodEl.max = variationsConfig[variation].drawablePeriods[1]
startPeriodEl.value = start
endPeriodEl.value = end
// Adds tick marks to assit in preventing overlap
startPeriodListEl.innerHTML = '<option value="' + (end - 1) + '"></option>'
endPeriodListEl.innerHTML = '<option value="' + (start + 1) + '"></option>'
if (startPeriodEl.max == 0) periodGroupEl.classList.add('no-time-slider')
else periodGroupEl.classList.remove('no-time-slider')
2022-05-06 09:41:22 +02:00
if (pathWithPeriods.length === 1) periodDeleteEl.disabled = true
2022-04-14 08:41:13 +02:00
startPeriodEl.addEventListener('input', event => {
endPeriodListEl.innerHTML = '<option value="' + (parseInt(event.target.value) + 1) + '"></option>'
timelineSlider.value = parseInt(event.target.value)
updateTime(parseInt(event.target.value), variation)
2022-04-14 08:41:13 +02:00
})
endPeriodEl.addEventListener('input', event => {
startPeriodListEl.innerHTML = '<option value="' + (parseInt(event.target.value) - 1) + '"></option>'
timelineSlider.value = parseInt(event.target.value)
updateTime(parseInt(event.target.value), variation)
2022-04-14 08:41:13 +02:00
})
2022-04-14 09:56:30 +02:00
periodDeleteEl.addEventListener('click', () => {
if (pathWithPeriods.length === 1) return
pathWithPeriods.splice(index, 1)
2022-04-14 09:56:30 +02:00
initPeriodGroups()
})
periodDuplicateEl.addEventListener('click', () => {
pathWithPeriods.push([pathWithPeriods[index][0], [...pathWithPeriods[index][1]]])
initPeriodGroups()
})
periodVariationEl.addEventListener('input', event => {
2022-04-16 14:26:31 +02:00
const newVariation = event.target.value
const newVariationConfig = variationsConfig[newVariation]
startPeriodEl.min = newVariationConfig.drawablePeriods[0]
endPeriodEl.min = newVariationConfig.drawablePeriods[0]
startPeriodEl.max = newVariationConfig.drawablePeriods[1]
endPeriodEl.max = newVariationConfig.drawablePeriods[1]
2022-04-29 05:12:43 +02:00
startPeriodEl.value = newVariationConfig.default
endPeriodEl.value = newVariationConfig.default
2022-05-06 09:41:22 +02:00
periodVariationEl.previousElementSibling.innerHTML = newVariationConfig.icon
if (startPeriodEl.max == 0) periodGroupEl.classList.add('no-time-slider')
else periodGroupEl.classList.remove('no-time-slider')
2022-04-29 05:12:43 +02:00
pathWithPeriods[index][0] = `${newVariationConfig.code}:${newVariationConfig.default}`
updateTime(newVariationConfig.default, newVariation)
})
2022-04-14 08:41:13 +02:00
periodCopyEl.addEventListener("click", event => {
const index = parseInt(event.target.id.split('periodCopy')[1])
if (event.target.textContent === " Copy") {
2022-05-06 09:41:22 +02:00
event.target.className = "period-copy btn btn-primary btn-sm flex-fill"
event.target.innerHTML = '<i class="bi bi-clipboard-x" aria-hidden="true"></i> End'
periodClipboard.index = index
periodClipboard.path = [...pathWithPeriods[index][1]]
2022-05-06 09:41:22 +02:00
startPeriodEl.disabled = true
endPeriodEl.disabled = true
updatePeriodGroups()
} else if (event.target.textContent === " End") {
2022-05-06 09:41:22 +02:00
event.target.className = "period-copy btn btn-secondary btn-sm flex-fill"
event.target.innerHTML = '<i class="bi bi-clipboard" aria-hidden="true"></i> Copy'
periodClipboard.index = null
periodClipboard.path = null
2022-05-06 09:41:22 +02:00
startPeriodEl.disabled = false
endPeriodEl.disabled = false
updatePeriodGroups()
} else if (event.target.textContent === " Paste") {
pathWithPeriods[index][1] = [...periodClipboard.path]
if (pathWithPeriods.length > 2) console.log(pathWithPeriods[2])
initPeriodGroups()
}
})
2022-04-14 08:41:13 +02:00
periodGroups.appendChild(periodGroupEl)
2022-04-16 14:26:31 +02:00
for (const variation in variationsConfig) {
const optionEl = document.createElement('option')
optionEl.value = variation
optionEl.textContent = variationsConfig[variation].name
periodVariationEl.appendChild(optionEl)
}
periodVariationEl.value = variation
2022-05-06 09:41:22 +02:00
periodVariationEl.previousElementSibling.innerHTML = variationsConfig[variation].icon
2022-04-14 08:41:13 +02:00
periodGroupElements.push({
periodGroupEl,
startPeriodEl,
endPeriodEl,
periodVariationEl,
2022-04-29 05:46:11 +02:00
periodCopyEl,
periodStatusEl
2022-04-14 08:41:13 +02:00
})
})
2022-04-14 08:41:13 +02:00
updatePeriodGroups()
}
function updatePeriodGroups() {
let pathToActive = []
let lastActivePathIndex
let currentActivePathIndex
const currentActivePathIndexes = []
2022-04-14 08:41:13 +02:00
periodGroupElements.forEach((elements, index) => {
2022-04-16 14:26:31 +02:00
const {
2022-04-14 08:41:13 +02:00
periodGroupEl,
startPeriodEl,
endPeriodEl,
periodVariationEl,
2022-04-29 05:46:11 +02:00
periodCopyEl,
periodStatusEl
2022-04-14 08:41:13 +02:00
} = elements
if (periodGroupEl.dataset.active === "true") lastActivePathIndex = index
periodGroupEl.dataset.active = ""
2022-04-14 08:41:13 +02:00
if (isOnPeriod(
parseInt(startPeriodEl.value),
2022-04-14 08:41:13 +02:00
parseInt(endPeriodEl.value),
periodVariationEl.value,
currentPeriod,
currentVariation
2022-04-14 08:41:13 +02:00
)) {
pathToActive = pathWithPeriods[index][1]
currentActivePathIndex = index
currentActivePathIndexes.push(index)
periodGroupEl.dataset.active = "true"
2022-04-10 11:11:34 +02:00
}
2022-04-14 08:41:13 +02:00
pathWithPeriods[index][0] = formatPeriod(
parseInt(startPeriodEl.value),
2022-04-14 08:41:13 +02:00
parseInt(endPeriodEl.value),
periodVariationEl.value
2022-04-14 08:41:13 +02:00
)
if (periodClipboard.index !== null) {
if (index !== periodClipboard.index) {
periodCopyEl.innerHTML = '<i class="bi bi-clipboard-plus" aria-hidden="true"></i> Paste'
if (JSON.stringify(pathWithPeriods[index][1]) === JSON.stringify(periodClipboard.path)) {
periodCopyEl.innerHTML = '<i class="bi bi-clipboard-check" aria-hidden="true"></i> Paste'
periodCopyEl.disabled = true
} else {
periodCopyEl.innerHTML = '<i class="bi bi-clipboard-plus" aria-hidden="true"></i> Paste'
periodCopyEl.disabled = false
}
} else {
2022-05-06 09:41:22 +02:00
periodCopyEl.className = "period-copy btn btn-primary btn-sm flex-fill"
periodCopyEl.innerHTML = '<i class="bi bi-clipboard-x" aria-hidden="true"></i> End'
}
} else {
periodCopyEl.innerHTML = '<i class="bi bi-clipboard" aria-hidden="true"></i> Copy'
periodCopyEl.disabled = false
}
2022-04-10 11:11:34 +02:00
})
2022-04-14 09:56:30 +02:00
periodsStatus.textContent = ""
2022-04-14 08:41:13 +02:00
if (lastActivePathIndex !== undefined) {
if (lastActivePathIndex === currentActivePathIndex) {
// just update the path
2022-04-16 14:26:31 +02:00
const {
startPeriodEl,
endPeriodEl,
periodVariationEl
} = periodGroupElements[currentActivePathIndex]
2022-04-14 08:41:13 +02:00
pathWithPeriods[currentActivePathIndex] = [
formatPeriod(
parseInt(startPeriodEl.value),
parseInt(endPeriodEl.value),
periodVariationEl.value,
2022-04-14 08:41:13 +02:00
),
path
]
updatePath()
2022-04-14 08:41:13 +02:00
} else if (currentActivePathIndex === undefined) {
2022-04-14 09:56:30 +02:00
pathWithPeriods[lastActivePathIndex][1] = path
2022-04-14 08:41:13 +02:00
updatePath([])
} else {
// switch the path
2022-04-14 09:56:30 +02:00
pathWithPeriods[lastActivePathIndex][1] = path
updatePath(pathToActive)
2022-04-14 08:41:13 +02:00
}
} else {
updatePath(pathToActive)
}
drawing = disableDrawingOverride ? false : currentActivePathIndex !== undefined
}
2022-05-06 12:28:26 +02:00
function updatePath(newPath, newUndoHistory) {
path = newPath || path
if (path.length > 3) center = calculateCenter(path)
2022-04-14 08:41:13 +02:00
render(path)
undoButton.disabled = path.length == 0; // Maybe make it undo the cancel action in the future
2022-05-06 12:28:26 +02:00
undoHistory = newUndoHistory || []
redoButton.disabled = (!undoHistory.length)
updateErrors()
}
function updateErrors() {
if (path.length === 0) {
periodsStatus.textContent = "No paths available on this period!"
}
2022-05-06 09:41:22 +02:00
const { conflicts, insufficientPaths } = getErrors()
let errorCount = 0
2022-04-29 07:16:54 +02:00
// console.log(conflicts, invalidPaths, allErrors)
2022-04-29 05:46:11 +02:00
periodGroupElements.forEach((el, index) => {
const { periodStatusEl, periodGroupEl } = el
periodStatusEl.textContent = ""
periodStatusEl.classList.add("d-none")
if (conflicts[index] !== undefined) {
periodStatusEl.textContent += `Period conflicts with path${conflicts[index].length === 1 ? "" : "s"} ${conflicts[index].join(", ")}.\n`
}
if (insufficientPaths[index] !== undefined) {
periodStatusEl.textContent += `Insufficient paths. Got ${insufficientPaths[index]}, need at least 3.\n`
2022-04-29 05:46:11 +02:00
}
if (periodStatusEl.textContent !== "") {
periodStatusEl.classList.remove("d-none")
periodGroupEl.dataset.status = "error"
errorCount += 1
2022-04-29 05:46:11 +02:00
}
})
if (errorCount > 0) {
periodsStatus.textContent = `Problems detected. Please check the groups indicated by red.`
finishButton.disabled = true
} else {
periodsStatus.textContent = ``
finishButton.disabled = false
periodGroupElements.forEach((elements, index) => {
2022-04-16 14:26:31 +02:00
const { periodGroupEl } = elements
if (periodGroupEl.dataset.active === "true") periodGroupEl.dataset.status = "active"
else periodGroupEl.dataset.status = ""
})
}
}
function getConflicts() {
const conflicts = {}
for (let i = pathWithPeriods.length - 1; i > 0; i--) {
2022-04-16 14:26:31 +02:00
const [start1, end1, period1] = parsePeriod(pathWithPeriods[i][0])
for (let j = 0; j < i; j++) {
2022-04-16 14:26:31 +02:00
const [start2, end2, period2] = parsePeriod(pathWithPeriods[j][0])
if (period1 !== period2) continue
if (
(start2 <= start1 && start1 <= end2) ||
(start2 <= end1 && end1 <= end2) ||
(start1 <= start2 && start2 <= end1) ||
(start1 <= end2 && end2 <= end1)
) {
2022-05-06 09:41:22 +02:00
if (!conflicts[i]) conflicts[i] = []
if (!conflicts[j]) conflicts[j] = []
conflicts[i].push(j)
conflicts[j].push(i)
}
}
}
return conflicts
}
function getErrors() {
2022-04-16 14:26:31 +02:00
const conflicts = getConflicts()
const insufficientPaths = {}
pathWithPeriods.forEach(([period, path], i) => {
if (path.length < 3) insufficientPaths[i] = path.length
})
2022-04-29 07:14:17 +02:00
// console.info('conflicts', conflicts)
// console.info('invalid paths', invalidPaths)
return {
2022-05-06 09:41:22 +02:00
conflicts,
insufficientPaths,
}
2022-04-14 16:03:17 +02:00
}
// function compressPeriod(periodsString) {
// let periodStrings = periodsString.split(", ")
// let validPeriods = new Set()
// periodStrings.forEach(periodString => {
// let [start, end] = parsePeriod(periodString)
// for (var i = start; i <= end; i++) {
// validPeriods.add(i)
// }
// })
// validPeriods = [...validPeriods].sort()
// }