157 lines
4.3 KiB
Vue
157 lines
4.3 KiB
Vue
<script lang="ts">
|
||
import { Vue, Component, toNative } from 'vue-facing-decorator';
|
||
|
||
interface PhotoMetadata {
|
||
id: string
|
||
owner_key: string // This will be filtered out for public requests
|
||
upload_time: string
|
||
original_photo: string
|
||
edited_photo: string
|
||
thumbnail: string
|
||
thumbnail_edited: string
|
||
exif: {[id: string]: string}
|
||
}
|
||
|
||
// Take in a string, use its hash to produce a number from 0 to 1
|
||
function detRandom(seed: string): number {
|
||
return Array.from(seed).reduce((acc, char) => (acc + char.charCodeAt(0) * 65535) % 22859, 0) / 22859
|
||
}
|
||
|
||
@Component({})
|
||
class Photos extends Vue {
|
||
photos: PhotoMetadata[]
|
||
photoRows: PhotoMetadata[][]
|
||
|
||
async created() {
|
||
this.photos = await (await fetch('https://p.aza.moe/photos')).json()
|
||
this.photos.sort((a, b) => (a.exif.DateTime < b.exif.DateTime ? 1 : -1))
|
||
|
||
let rowProbabilityTable = {
|
||
1: 0, 2: 0.3, 3: 0.5
|
||
}
|
||
|
||
// Generate photo rows: there is a 10% chance that a photo will be the only photo in its row
|
||
this.photoRows = []
|
||
let currentRow: PhotoMetadata[] = []
|
||
this.photos.forEach((p) => {
|
||
if (currentRow.length === 0) currentRow.push(p)
|
||
else if (currentRow.length >= 3) {
|
||
this.photoRows.push(currentRow)
|
||
currentRow = [ p ]
|
||
}
|
||
else {
|
||
const singleChance = detRandom(p.original_photo)
|
||
if (singleChance < rowProbabilityTable[currentRow.length]) {
|
||
this.photoRows.push(currentRow)
|
||
currentRow = [ p ]
|
||
} else currentRow.push(p)
|
||
}
|
||
})
|
||
if (currentRow.length > 0) this.photoRows.push(currentRow)
|
||
}
|
||
|
||
url(s: string): string {
|
||
s = s.replace('data/photos', 'static').replace('./', '')
|
||
return `https://p.aza.moe/${s}`
|
||
}
|
||
|
||
randomRotation(s: string): string {
|
||
const angle = (detRandom(s) * 20) - 10 // -10 to +10 degrees
|
||
return `rotate(${angle}deg)`
|
||
}
|
||
|
||
async clickPhoto(p: PhotoMetadata, e: MouseEvent) {
|
||
console.log("Clicked photo:", p.id)
|
||
const dom = e.currentTarget as HTMLDivElement
|
||
const photoEl = dom.querySelector('.photo-wrapper') as HTMLDivElement
|
||
|
||
photoEl.style.viewTransitionName = `photo-${p.id}`
|
||
|
||
const transition = document.startViewTransition(() => {
|
||
dom.classList.toggle('active')
|
||
document.getElementsByClassName('blur')[0].toggleAttribute('hidden')
|
||
})
|
||
|
||
await transition.finished
|
||
photoEl.style.viewTransitionName = ''
|
||
}
|
||
}
|
||
|
||
export default toNative(Photos)
|
||
</script>
|
||
|
||
<template>
|
||
<div class="title">
|
||
<div class="font-script-en bold">The Wandering Gallery</div>
|
||
<div class="subtitle <sm:hidden">想要把旅行中用相机拍到好看照片时的喜悦分享给照片里出现的路人,所以买了便携照片打印机、搭了这个网页!</div>
|
||
</div>
|
||
<div class="outer-grid">
|
||
<div v-for="row in photoRows" :key="row[0].id" flex justify-center :class="`grid-cols-${row.length}`">
|
||
<div v-for="p in row" :key="p.id" @click.capture="async e => await clickPhoto(p, e)"
|
||
class="img-container" cursor-pointer>
|
||
<img class="photo" w-full h-full object-contain opacity-0 :src="url(p.thumbnail_edited)" :alt="p.id"/>
|
||
<div class="photo-abs-container" absolute inset-0 flex justify-center items-center>
|
||
<div class="photo-wrapper" :style="{transform: randomRotation(p.id)}">
|
||
<img class="photo" w-full object-contain :src="url(p.thumbnail_edited)" :alt="p.id" />
|
||
</div>
|
||
</div>
|
||
<div flex w-full justify-center absolute position-top-none>
|
||
<img class="pin" src="/thumb%20tack%202%20plain.png" alt=""/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="blur" hidden pos-fixed inset-0 backdrop-blur-sm z-5></div>
|
||
</template>
|
||
|
||
<style scoped lang="sass">
|
||
@use "../css/colors"
|
||
@use "../css/responsive"
|
||
|
||
.blur
|
||
z-index: 2500
|
||
|
||
.title
|
||
margin-top: 8rem
|
||
margin-bottom: 6rem
|
||
|
||
.bold
|
||
font-size: 3em
|
||
|
||
.img-container
|
||
margin: -0.5rem
|
||
max-width: 50%
|
||
position: relative
|
||
|
||
.img-container.active
|
||
position: unset
|
||
|
||
img.pin
|
||
display: none
|
||
.photo-abs-container
|
||
position: fixed
|
||
z-index: 3000
|
||
|
||
.photo-wrapper
|
||
transform: rotate(0deg) !important
|
||
|
||
img.photo
|
||
z-index: 3001
|
||
|
||
img.photo
|
||
clip-path: inset(2.8% 1.8% 2.4%)
|
||
pointer-events: none
|
||
|
||
div.photo-abs-container
|
||
position: absolute
|
||
inset: 0
|
||
z-index: 1000
|
||
|
||
div.photo-wrapper
|
||
filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3))
|
||
|
||
img.pin
|
||
width: 40px
|
||
height: 40px
|
||
z-index: 2000
|
||
</style> |