203 lines
6.3 KiB
JavaScript
203 lines
6.3 KiB
JavaScript
const filterStyle = document.createElement('style')
|
|
document.head.appendChild(filterStyle)
|
|
const selected = new Set()
|
|
for (const checkbox of document.getElementsByClassName('filter-checkbox')) {
|
|
const className = `project-${checkbox.id.replace('filter-by-', '')}`
|
|
checkbox.addEventListener('change', e => {
|
|
if (checkbox.checked) {
|
|
selected.add(className)
|
|
unselect()
|
|
lastSelected = null
|
|
} else {
|
|
selected.delete(className)
|
|
}
|
|
if (selected.size === 0) {
|
|
filterStyle.textContent = ''
|
|
} else {
|
|
filterStyle.textContent = `.project{display:none}${Array.from(
|
|
selected,
|
|
className => '.' + className
|
|
).join('')}{display:block}`
|
|
}
|
|
})
|
|
}
|
|
|
|
for (const label of document.getElementsByClassName('filter-tag')) {
|
|
// Prevent selecting text when spam clicking button
|
|
// Why not CSS? This way, you can still select the text by other means, like
|
|
// with normal <button>s
|
|
label.addEventListener('mousedown', e => {
|
|
e.preventDefault()
|
|
})
|
|
}
|
|
|
|
function empty (elem) {
|
|
while (elem.firstChild) elem.removeChild(elem.firstChild)
|
|
}
|
|
|
|
const descElems = {
|
|
wrapper: document.getElementById('description-wrapper'),
|
|
title: document.getElementById('desc-title'),
|
|
link: document.getElementById('desc-link'),
|
|
tags: document.getElementById('desc-tags'),
|
|
text: document.getElementById('description')
|
|
}
|
|
function createTag (tagElem) {
|
|
const tag = document.createElement('div')
|
|
tag.className = 'desc-tag'
|
|
const icon = document.createElement('div')
|
|
icon.className = `desc-tag-icon base-tag ${[...tagElem.classList].find(cls =>
|
|
cls.startsWith('tag-')
|
|
)}`
|
|
const name = document.createElement('div')
|
|
name.className = 'desc-tag-name'
|
|
name.textContent = tagElem.title
|
|
tag.append(icon, name)
|
|
return tag
|
|
}
|
|
function setDescription (project) {
|
|
descElems.title.textContent = project.querySelector('.title').textContent
|
|
descElems.link.href = project.href
|
|
empty(descElems.tags)
|
|
for (const tag of project.getElementsByClassName('tag')) {
|
|
descElems.tags.append(createTag(tag))
|
|
}
|
|
empty(descElems.text)
|
|
for (const paragraph of project.dataset.desc.split(/\r?\n/)) {
|
|
const p = document.createElement('p')
|
|
p.textContent = paragraph
|
|
descElems.text.append(p)
|
|
}
|
|
}
|
|
function unselect () {
|
|
descElems.wrapper.style.display = 'none'
|
|
if (lastSelected) {
|
|
lastSelected.classList.remove('showing-desc')
|
|
lastSelected.ariaExpanded = 'false'
|
|
}
|
|
}
|
|
|
|
const projectsWrapper = document.getElementById('projects-wrapper')
|
|
let lastSelected = null
|
|
projectsWrapper.addEventListener('click', e => {
|
|
const project = e.target.closest('.project')
|
|
if (!project) {
|
|
return
|
|
}
|
|
const showInfoHidden =
|
|
window.getComputedStyle(project.querySelector('.show-info')).display ===
|
|
'none'
|
|
if (showInfoHidden || e.target.closest('.show-info')) {
|
|
e.preventDefault()
|
|
unselect()
|
|
if (lastSelected !== project) {
|
|
setDescription(project)
|
|
project.after(descElems.wrapper)
|
|
descElems.wrapper.style.display = null
|
|
lastSelected = project
|
|
project.classList.add('showing-desc')
|
|
project.ariaExpanded = 'true'
|
|
} else {
|
|
lastSelected = null
|
|
}
|
|
}
|
|
})
|
|
|
|
for (const project of document.getElementsByClassName('project')) {
|
|
project.ariaExpanded = 'false'
|
|
}
|
|
|
|
function easeInOutCubic (t) {
|
|
t *= 2
|
|
if (t < 1) return (t * t * t) / 2
|
|
t -= 2
|
|
return (t * t * t + 2) / 2
|
|
}
|
|
|
|
const BIRTHDAY = 1049933280000 // new Date('2003-04-09T17:08-07:00').getTime()
|
|
const MS_IN_YR = 1000 * 60 * 60 * 24 * 365.242199
|
|
function getAge (now = Date.now()) {
|
|
// Could be 15, but it gets imprecise at such a small scale, and the animation
|
|
// looks unlike the others
|
|
return ((now - BIRTHDAY) / MS_IN_YR).toFixed(13)
|
|
}
|
|
const ANIM_LENGTH = 500 // ms
|
|
const ALPHA = 0.8
|
|
|
|
const ageSpan = document.getElementById('age')
|
|
ageSpan.textContent = Math.floor((Date.now() - BIRTHDAY) / MS_IN_YR)
|
|
ageSpan.classList.add('age-clickable')
|
|
ageSpan.tabIndex = 0
|
|
ageSpan.title = 'Click to see me age in real time'
|
|
ageSpan.addEventListener(
|
|
'click',
|
|
e => {
|
|
const ageWrapper = document.createElement('code')
|
|
ageWrapper.classList.add('age')
|
|
ageWrapper.role = 'text'
|
|
const age = getAge()
|
|
ageWrapper.style.width = age.length + 'ch'
|
|
const decimal = age.indexOf('.')
|
|
const digits = new Array(age.length)
|
|
let sigfigs = Math.floor((Date.now() - BIRTHDAY) / 10000).toString().length
|
|
for (let i = 0; i < age.length; i++) {
|
|
const digit = document.createElement('span')
|
|
digits[i] = {
|
|
elem: digit,
|
|
exponent:
|
|
i === decimal ? null : i < decimal ? decimal - i - 1 : decimal - i
|
|
}
|
|
if (age[i] !== '.') {
|
|
if (sigfigs <= 0) {
|
|
digit.classList.add('insignificant')
|
|
digit.title = 'This digit is purely an estimation.'
|
|
}
|
|
sigfigs--
|
|
} else {
|
|
digit.textContent = '.'
|
|
digit.style.transform = `translate3d(${i}ch, 0, 0)`
|
|
}
|
|
ageWrapper.append(digit)
|
|
}
|
|
ageWrapper.append('\xa0') // nbsp
|
|
ageSpan.replaceWith(ageWrapper)
|
|
function display () {
|
|
const now = Date.now()
|
|
const age = getAge(now)
|
|
for (let i = 0; i < age.length; i++) {
|
|
const digit = digits[i]
|
|
if (digit.exponent !== null) {
|
|
const interval = 10 ** digit.exponent * MS_IN_YR
|
|
const animationTime = Math.min(interval, ANIM_LENGTH)
|
|
const time = (now - BIRTHDAY) % interval
|
|
if (digit.elem.textContent !== age[i]) {
|
|
digit.elem.textContent = age[i]
|
|
}
|
|
if (time < animationTime) {
|
|
const interp = easeInOutCubic(time / animationTime)
|
|
digit.elem.style.transform = `translate3d(${i}ch, ${
|
|
interp - 1
|
|
}em, 0)`
|
|
digit.elem.style.color = `rgba(255, 255, 255, ${interp * ALPHA})`
|
|
digit.elem.style.setProperty(
|
|
'--last',
|
|
`rgba(255, 255, 255, ${(1 - interp) * ALPHA})`
|
|
)
|
|
digit.elem.dataset.last = (+age[i] + 9) % 10
|
|
digit.wasStatic = false
|
|
} else if (!digit.wasStatic) {
|
|
digit.elem.style.transform = `translate3d(${i}ch, 0, 0)`
|
|
digit.elem.style.color = null
|
|
digit.elem.style.removeProperty('--last')
|
|
delete digit.elem.dataset.last
|
|
digit.wasStatic = true
|
|
}
|
|
}
|
|
}
|
|
window.requestAnimationFrame(display)
|
|
}
|
|
display()
|
|
},
|
|
{ once: true }
|
|
)
|