53 Commits

Author SHA1 Message Date
Copilot ea9c12eda5 Migrate remaining class/decorator Vue components to Composition API (#3)
* Plan: migrate class-based Vue components to Composition API

Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/061a81e2-3249-43c2-a205-af770343be40

Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>

* Migrate legacy class-based Vue components to Composition API

Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/061a81e2-3249-43c2-a205-af770343be40

Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>

* Fix BlogIndex tag key uniqueness after review

Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/061a81e2-3249-43c2-a205-af770343be40

Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>

* Apply validation feedback and finalize composition migration

Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/061a81e2-3249-43c2-a205-af770343be40

Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>

* Remove package-lock and validate project with Bun lockfile

Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/ee17e721-3915-4dfb-8ccc-cf9b0b715fb0

Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>

* Normalize bun.lockb file mode

Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/ee17e721-3915-4dfb-8ccc-cf9b0b715fb0

Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>
2026-04-24 04:44:58 +08:00
azalea f25f468c99 Update Photo.vue 2025-10-27 19:13:26 +08:00
azalea 495e106e4c Update Photo.vue 2025-10-27 19:02:38 +08:00
azalea e52ef61c8b [F] Fix link 2025-10-27 18:55:45 +08:00
azalea 51157cae08 [F] Fix build 2025-10-27 18:34:14 +08:00
azalea 9b5a613593 [F] Fix animation 2025-10-27 17:44:29 +08:00
azalea ab474204ce [+] Transition 2025-10-27 17:24:49 +08:00
azalea 6c776c8a2e f 2025-10-27 16:46:03 +08:00
azalea fafe05a792 [+] Routes 2025-10-27 15:42:13 +08:00
azalea c7303bd5fd [+] Styling 2025-10-27 15:15:48 +08:00
azalea 2f6272818d [+] Photo 2025-10-27 14:46:54 +08:00
azalea 58d0cb99b8 Unocss 2025-10-27 14:46:46 +08:00
azalea 84128c52a5 [F] Fix vue-tsc vue-facing-decorator toNative
https://github.com/facing-dev/vue-facing-decorator/issues/163
2025-10-24 18:22:40 +08:00
azalea 34b076244b [U] Update deps 2025-10-24 17:56:28 +08:00
azalea 07a5ba0f75 [U] Update deps 2025-10-24 17:56:07 +08:00
azalea 285fe5d2de [U] Update deps 2025-10-24 17:53:55 +08:00
azalea 13da185b18 Revert "[U] Upgrade deps"
This reverts commit 86fc1a365e.
2025-10-24 17:52:08 +08:00
azalea ba52340363 Revert "[U] Upgrade deps"
This reverts commit 2fbb36b830.
2025-10-24 17:52:05 +08:00
azalea b65fdc6f99 Update vite.yml 2025-10-24 17:48:20 +08:00
azalea 0fc709709b Update vite.yml 2025-10-24 17:47:09 +08:00
azalea 2fbb36b830 [U] Upgrade deps 2025-10-24 15:50:45 +08:00
azalea 0d42bfba2b [F] Fix http 2025-10-24 15:46:18 +08:00
azalea 86fc1a365e [U] Upgrade deps 2025-10-24 15:45:57 +08:00
azalea 2ff45e1068 [U] Migrate sass 2025-10-24 15:45:10 +08:00
azalea 835bccf288 [+] Sass migrate
https://github.com/sass/migrator/issues/211
2025-10-24 15:23:20 +08:00
azalea db1b257df4 [U] Yarn -> Bun 2025-10-24 15:21:43 +08:00
azalea 799f8515ec [U] Update title 2025-04-30 07:17:23 -04:00
azalea 22e15e6beb Update vite.yml 2024-12-17 07:48:15 +08:00
azalea 51682f81a8 Update vite.yml 2024-12-17 07:42:19 +08:00
azalea 6cd3484f7c [U] Update actions 2024-12-17 07:41:30 +08:00
azalea db5bcc66ec [U] Update title 2024-12-17 07:37:41 +08:00
azalea a4f6e21af8 [U] Fix image url 2024-08-06 01:28:53 -04:00
azalea 9339af97d5 [F] Fix build 2024-08-01 12:40:28 +08:00
azalea 26e19af0a3 [F] Fix import 2024-07-31 18:58:13 +08:00
azalea 7cc6a57b49 [U] Upgrade to vue-facing-decorator 2024-07-31 18:56:42 +08:00
azalea 4fbe9f156b [O] Use lxgw wenkai webfont 2024-07-31 18:56:20 +08:00
azalea ab6919ca28 [U] Upgrade packages 2024-07-31 18:55:48 +08:00
azalea 4f128a6fb8 [U] Upgrade to yarn 4.3.1 2024-07-31 18:55:02 +08:00
azalea 8b8c6db10f [+] Maiandra GD 2024-07-31 18:06:53 +08:00
Azalea Gui fd07dd04f7 [U] Update dependencies 2024-02-12 12:01:05 -05:00
Azalea Gui 848a8ddf91 [U] Update tg-blog 2024-02-12 11:57:09 -05:00
Azalea Gui 473a84b91e [U] Update tg-blog 2023-07-15 14:49:03 -04:00
Azalea Gui b68031238f [U] Update dependencies 2023-07-02 21:53:52 -04:00
Azalea Gui d1525096da [U] Yarn upgrade 2023-04-21 15:31:56 -04:00
Azalea Gui 83ba8b9f93 [U] yarn upgrade 2023-03-11 17:23:37 -05:00
Azalea Gui 56ca0ab655 [F] Fix v-for key 2023-03-09 17:20:11 -05:00
Azalea Gui d613ac62aa [S] Style markdown figure 2023-03-09 17:14:17 -05:00
Azalea Gui 6ebd786b7c [O] Allow images 2023-03-09 17:14:03 -05:00
Azalea Gui 747d725c43 [O] Convert blog to setup script 2023-03-09 15:37:09 -05:00
Azalea Gui 731c08dbcb [-] yarn install 2023-03-09 15:25:00 -05:00
Azalea Gui 0143b1edc9 [-] Remove color picker from router 2023-03-09 15:24:48 -05:00
Azalea Gui ac9bea5fd2 [-] Split color picker 2023-03-09 15:22:27 -05:00
Azalea Gui 58462de365 [!!!!!!!!!!!!!!!] Remove new-home 2023-03-09 15:06:03 -05:00
48 changed files with 1544 additions and 3307 deletions
+5 -20
View File
@@ -13,33 +13,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2.1.5
- name: Cache node modules
uses: actions/cache@v2
with:
path: ~/node_modules
key: cache-${{ hashFiles('**/package-lock.json') }}
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
- name: Install and build
run: |
yarn install
# pass --base if CNAME is not used
# npm run build -- --base=/${{ github.event.repository.name }}/
yarn build
bun install
bun run build
# Enable Vue Router history mode with 404.html hack for Github Pages
cd dist
ln -s index.html 404.html
- name: Deploy to github pages
uses: JamesIves/github-pages-deploy-action@4.1.0
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: gh-pages
folder: dist
- name: Publish deployed code
uses: actions/upload-artifact@v2
with:
name: published
path: dist
Binary file not shown.
+894
View File
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.
+23 -27
View File
@@ -8,35 +8,31 @@
"build": "vue-tsc --noEmit && vite build"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.3.0",
"@fortawesome/fontawesome-free": "^6.6.0",
"animsition": "^4.0.2",
"core-js": "^3.29.0",
"emoji-regex": "^10.2.1",
"linkify-urls": "^4.0.0",
"marked": "^4.2.12",
"meshline": "^3.1.6",
"moment": "^2.29.4",
"tg-blog": "^1.1.0",
"three": "^0.150.1",
"vue": "^3.2.47",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6",
"vue3-colorpicker": "^2.1.2"
"core-js": "^3.37.1",
"emoji-regex": "^10.3.0",
"linkify-urls": "^4.1.0",
"lxgw-wenkai-webfont": "^1.7.0",
"marked": "^4.3.0",
"moment": "^2.30.1",
"tg-blog": "^1.1.7",
"vue": "^3.4.35",
"vue-i18n": "^11.1.12",
"vue-router": "^4.4.0"
},
"devDependencies": {
"@types/jquery": "^3.5.16",
"@types/jqueryui": "^1.12.16",
"@types/marked": "^4.0.7",
"@types/node": "^18.14.5",
"@types/three": "^0.149.0",
"@vitejs/plugin-vue": "^4.0.0",
"eslint": "^8.35.0",
"sass": "^1.58.3",
"tslib": "^2.5.0",
"typescript": "^4.9.4",
"vite": "^4.1.4",
"vue-class-component": "^8.0.0-rc.1",
"vue-property-decorator": "^10.0.0-rc.3",
"vue-tsc": "^1.0.24"
"@types/jquery": "^3.5.30",
"@types/jqueryui": "^1.12.23",
"@types/marked": "^4.3.2",
"@types/node": "^24.9.1",
"@vitejs/plugin-vue": "^6.0.1",
"eslint": "^9.8.0",
"sass": "^1.77.8",
"tslib": "^2.6.3",
"typescript": "^5.5.4",
"unocss": "^66.5.4",
"vite": "^7.1.12",
"vue-tsc": "3.1.1"
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

+85 -80
View File
@@ -1,21 +1,22 @@
<template>
<div id="nav" class="fbox-v"
:class="(currentRoute) + ' ' + (menuOpen ? 'open' : '')"
v-if="currentRoute !== 'colorpicker'">
v-if="currentRoute !== 'colorpicker'">
<div id="menu" @click="showMenu"><i class="fas fa-bars"></i></div>
<div id="items" class="fbox-v">
<router-link class="router-link" ref="others" to="/others">{{ $t('nav.others') }}</router-link>
<router-link class="router-link" :ref="setNavRef('others')" to="/others">{{ $t('nav.others') }}</router-link>
<div class="dot">·</div>
<router-link class="router-link" ref="blog" to="/blog">{{ $t('nav.blog') }}</router-link>
<router-link class="router-link" :ref="setNavRef('photo')" to="/photo">{{ $t('nav.photo') }}</router-link>
<div class="dot">·</div>
<router-link class="router-link" ref="life" to="/life">{{ $t('nav.life') }}</router-link>
<router-link class="router-link" :ref="setNavRef('blog')" to="/blog">{{ $t('nav.blog') }}</router-link>
<div class="dot">·</div>
<router-link class="router-link" ref="about" to="/about">{{ $t('nav.about') }}</router-link>
<router-link class="router-link" :ref="setNavRef('life')" to="/life">{{ $t('nav.life') }}</router-link>
<div class="dot">·</div>
<router-link class="router-link" ref="home" to="/">
<svg focusable="false" data-prefix="fal" data-icon="house-night" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" class="svg-inline--fa fa-house-night fa-w-20"><path fill="currentColor" d="M112,224a111.5,111.5,0,0,0,87-41.45,20.51,20.51,0,0,0-19.75-33.08A59.2,59.2,0,0,1,138.84,39.85a20.3,20.3,0,0,0,10.07-21.27,20.26,20.26,0,0,0-16.47-16.7A136,136,0,0,0,112,0a112,112,0,0,0,0,224ZM97.78,33.27a91.21,91.21,0,0,0,54.47,147.9A80,80,0,1,1,97.78,33.27Zm97.15,35.51,39.72,16.56,16.56,39.72a5.33,5.33,0,0,0,9.55,0l16.56-39.72L317,68.78a5.33,5.33,0,0,0,0-9.54L277.32,42.68,260.76,3a5.33,5.33,0,0,0-9.55,0L234.65,42.68,194.93,59.24a5.34,5.34,0,0,0,0,9.54ZM157,379.24l-39.72-16.57L100.76,323a5.34,5.34,0,0,0-9.55,0L74.65,362.67,34.93,379.24a5.34,5.34,0,0,0,0,9.54l39.72,16.56,16.56,39.72a5.33,5.33,0,0,0,9.55,0l16.56-39.72L157,388.78a5.33,5.33,0,0,0,0-9.54Zm179-101.9v85.33A21.39,21.39,0,0,0,357.36,384h85.31A21.39,21.39,0,0,0,464,362.67V277.34A21.4,21.4,0,0,0,442.67,256H357.36A21.4,21.4,0,0,0,336,277.34ZM368,288H432v64H368Zm266.49,8L576,244.75V144a16,16,0,0,0-32,0v72.75L410.53,100a16,16,0,0,0-21.07,0l-224,196a16,16,0,0,0,21.07,24.09L224,287.28V464a48.05,48.05,0,0,0,48,48H528a48.06,48.06,0,0,0,48-48V287.28l37.46,32.78A16,16,0,0,0,634.53,296ZM544,464a16,16,0,0,1-16,16H272a16,16,0,0,1-16-16V264a15.94,15.94,0,0,0-.81-4L400,133.27l144,126Z" class=""></path></svg></router-link>
<router-link class="router-link" :ref="setNavRef('about')" to="/about">{{ $t('nav.about') }}</router-link>
<div class="dot">·</div>
<router-link class="router-link" :ref="setNavRef('home')" to="/">
<svg focusable="false" data-prefix="fal" data-icon="house-night" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" class="svg-inline--fa fa-house-night fa-w-20"><path fill="currentColor" d="M112,224a111.5,111.5,0,0,0,87-41.45,20.51,20.51,0,0,0-19.75-33.08A59.2,59.2,0,0,1,138.84,39.85a20.3,20.3,0,0,0,10.07-21.27,20.26,20.26,0,0,0-16.47-16.7A136,136,0,0,0,112,0a112,112,0,0,0,0,224ZM97.78,33.27a91.21,91.21,0,0,0,54.47,147.9A80,80,0,1,1,97.78,33.27Zm97.15,35.51,39.72,16.56,16.56,39.72a5.33,5.33,0,0,0,9.55,0l16.56-39.72L317,68.78a5.33,5.33,0,0,0,0-9.54L277.32,42.68,260.76,3a5.33,5.33,0,0,0-9.55,0L234.65,42.68,194.93,59.24a5.34,5.34,0,0,0,0,9.54ZM157,379.24l-39.72-16.57L100.76,323a5.34,5.34,0,0,0-9.55,0L74.65,362.67,34.93,379.24a5.34,5.34,0,0,0,0,9.54l39.72,16.56,16.56,39.72a5.33,5.33,0,0,0,9.55,0l16.56-39.72L157,388.78a5.33,5.33,0,0,0,0-9.54Zm179-101.9v85.33A21.39,21.39,0,0,0,357.36,384h85.31A21.39,21.39,0,0,0,464,362.67V277.34A21.4,21.4,0,0,0,442.67,256H357.36A21.4,21.4,0,0,0,336,277.34ZM368,288H432v64H368Zm266.49,8L576,244.75V144a16,16,0,0,0-32,0v72.75L410.53,100a16,16,0,0,0-21.07,0l-224,196a16,16,0,0,0,21.07,24.09L224,287.28V464a48.05,48.05,0,0,0,48,48H528a48.06,48.06,0,0,0,48-48V287.28l37.46,32.78A16,16,0,0,0,634.53,296ZM544,464a16,16,0,0,1-16,16H272a16,16,0,0,1-16-16V264a15.94,15.94,0,0,0-.81-4L400,133.27l144,126Z" class=""></path></svg></router-link>
</div>
<div id="nav-bookmark" ref="bookmark" :style="bookmarkCss"></div>
@@ -27,89 +28,93 @@
<router-view/>
</template>
<script lang="ts">
import {Options, Vue} from "vue-class-component";
<script setup lang="ts">
import {nextTick, onMounted, onUnmounted, ref, ComponentPublicInstance} from 'vue';
import router from "@/scripts/router";
import {RouteLocationNormalized} from "vue-router";
import {RouteLocationNormalized, RouteLocationRaw, useRoute} from "vue-router";
@Options({components: {}})
export default class App extends Vue
{
currentRoute = ''
currentLink: Element = null as never as Element
bookmarkCss = ""
bookmarkUpdateIntervalId!: number
lastTop = 0
const route = useRoute()
menuOpen = false
const currentRoute = ref('')
const bookmarkCss = ref('')
const lastTop = ref(0)
const menuOpen = ref(false)
const bookmarkUpdateIntervalId = ref<number | null>(null)
const removeAfterEach = ref<(() => void) | null>(null)
showMenu(): void
{
this.menuOpen = !this.menuOpen
const navRefs = ref<Record<string, Element | ComponentPublicInstance | null>>({})
// Auto close
if (this.menuOpen) setTimeout(() => this.menuOpen = false, 2000)
}
const showMenu = (): void => {
menuOpen.value = !menuOpen.value
updateBookmark(to: RouteLocationNormalized): void
{
// Update title
// Use next tick to handle router history correctly
// see: https://github.com/vuejs/vue-router/issues/914#issuecomment-384477609
this.$nextTick(() => {
if (to.name == 'Blog' && Object.keys(to.query).length != 0) return
document.title = to.meta.title ? `Hykilpikonna - ${to.meta.title}` : 'Hykilpikonna - Home';
})
console.log('AfterEach called', to)
this.currentRoute = ((to.meta?.navBookmark ?? to.name) as string).toLowerCase()
this.calculateBookmarkCss()
this.menuOpen = false
}
mounted(): void
{
console.log('Mounted called', this.$route)
router.afterEach(this.updateBookmark)
if (this.$route.name) this.currentRoute = ((this.$route.meta?.navBookmark ?? this.$route.name) as string).toLowerCase()
// Resize listener
window.addEventListener('resize', this.calculateBookmarkCss, true);
// Update every second
this.bookmarkUpdateIntervalId = window.setInterval(this.calculateBookmarkCss, 1000)
}
unmounted(): void
{
window.removeEventListener('resize', this.calculateBookmarkCss)
window.clearInterval(this.bookmarkUpdateIntervalId)
}
calculateBookmarkCss(): void
{
if (this.currentRoute in this.$refs)
this.currentLink = (this.$refs[this.currentRoute] as Vue).$el
else return
// https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect
let box = this.currentLink.getBoundingClientRect()
if (box.top == this.lastTop) return
this.lastTop = box.top
let h = box.bottom - box.top
let width = Math.round(h / 2) + 8
this.bookmarkCss = `top: ${box.top - 8}px;` +
`border-width: ${width}px 20px ${width}px 50px;`
}
if (menuOpen.value) setTimeout(() => menuOpen.value = false, 2000)
}
const setNavRef = (name: string) => (el: Element | ComponentPublicInstance | null) => {
navRefs.value[name] = el
}
const resolveNavElement = (target: Element | ComponentPublicInstance | null | undefined): Element | null => {
if (!target) return null
if (target instanceof Element) return target
return (target.$el as Element | undefined) ?? null
}
const calculateBookmarkCss = (): void => {
const currentLink = resolveNavElement(navRefs.value[currentRoute.value])
if (!currentLink) return
const box = currentLink.getBoundingClientRect()
if (box.top === lastTop.value) return
lastTop.value = box.top
const h = box.bottom - box.top
const width = Math.round(h / 2) + 8
bookmarkCss.value = `top: ${box.top - 8}px;` +
`border-width: ${width}px 20px ${width}px 50px;`
}
const getRouteBookmark = (to: RouteLocationNormalized): string => {
return ((to.meta?.navBookmark ?? to.name) as string).toLowerCase()
}
const updateBookmark = (to: RouteLocationNormalized): void => {
nextTick(() => {
if (to.name == 'Blog' && Object.keys(to.query).length != 0) return
document.title = to.meta.title ? `Aza - ${to.meta.title}` : 'Aza - Home';
})
console.log('AfterEach called', to)
currentRoute.value = getRouteBookmark(to)
calculateBookmarkCss()
menuOpen.value = false
}
onMounted(() => {
console.log('Mounted called', route)
removeAfterEach.value = router.afterEach(updateBookmark)
if (route.name) {
currentRoute.value = getRouteBookmark(route as unknown as RouteLocationNormalized)
}
window.addEventListener('resize', calculateBookmarkCss, true)
bookmarkUpdateIntervalId.value = window.setInterval(calculateBookmarkCss, 1000)
})
onUnmounted(() => {
removeAfterEach.value?.()
window.removeEventListener('resize', calculateBookmarkCss, true)
if (bookmarkUpdateIntervalId.value !== null) {
window.clearInterval(bookmarkUpdateIntervalId.value)
}
})
</script>
<style lang="sass">
@import "css/global"
@import "css/animations"
@use "css/global"
@use "css/animations"
#nav
position: fixed
-34
View File
@@ -1,34 +0,0 @@
import {Color} from "three";
/**
* Configurations
*/
export const config = {
// Field of vision and cutoff frustum for near and far
cam: {fov: 50, near: 1, far: 5000},
// Smooth movements (speeds are in terms of pixels per ms)
smooth: {mouseSpeed: 10 * window.devicePixelRatio},
// Cursor
cursor: {radius: 2, width: 0.3, color: new Color('#333')},
// Debug mode
debug: false,
// Edit mode
editMode: true,
// Editor config
editor: {zMin: 70, zMax: 90}
}
export const colors = {
sky: {
top: new Color('#3284ff'),
bottom: new Color('#ffffff'),
ground: new Color('#ffc87f'),
dirLight: new Color('#fff4e5')
},
}
-62
View File
@@ -1,62 +0,0 @@
import * as THREE from 'three'
/**
* Create a 3D box geometry made out of dashed lines
* @param width
* @param height
* @param depth
*/
export function box(width: number, height: number, depth: number): THREE.BufferGeometry
{
width = width * 0.5
height = height * 0.5
depth = depth * 0.5
const geometry = new THREE.BufferGeometry();
const position = [];
for (const x of [-1, 1])
for (const y of [-1, 1])
for (const z of [-1, 1])
{
const rx = x * width, ry = y * height, rz = z * depth
position.push(rx, ry, rz)
position.push(rx * -x, ry, rz)
position.push(rx, ry, rz)
position.push(rx, ry * -y, rz)
position.push(rx, ry, rz)
position.push(rx, ry, rz * -z)
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(position, 3))
return geometry
}
/**
* Create a 2D circle
* @param color
* @param z
* @param r
* @param hollow
*/
export function circle(color: THREE.Color | number | string, z: number, r: number): THREE.Mesh
{
const geometry = new THREE.CircleGeometry(r, 64)
const material = new THREE.MeshBasicMaterial({color})
const circle = new THREE.Mesh(geometry, material)
circle.position.z = z
return circle
}
/**
* Ignore depth https://stackoverflow.com/a/62818553/7346633
* @param obj
* @param material
*/
export function alwaysOnTop(obj: THREE.Object3D, material: THREE.Material): void
{
obj.renderOrder = 999
material.depthTest = false
material.depthWrite = false
obj.onBeforeRender = (r) => r.clearDepth()
}
-156
View File
@@ -1,156 +0,0 @@
import * as THREE from 'three'
import {Color} from 'three'
import * as helper from "@/animation/Helpers"
import {initMouseTracker} from "@/animation/Trackers"
import {addDirLight, addGround, addHemiLight, addSky} from "@/animation/Shaders"
import {config} from "@/animation/Config"
import Cursor from "@/animation/components/Cursor";
import IUpdatable from "@/animation/components/IUpdatable";
import Grid from "@/animation/components/Grid";
import Editor from "@/animation/components/Editor";
export let renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.PerspectiveCamera
export let editor: Editor
const clock = new THREE.Clock()
export const objects: { [id: string]: THREE.Object3D } = {}
const updatable: IUpdatable[] = []
// ////////////////////
// Three.js scene code
function init(): void
{
const geometryBox = helper.box(50, 50, 1000)
const lineSegments = new THREE.LineSegments(geometryBox, new THREE.LineDashedMaterial({
color: "#420000",
dashSize: 3,
gapSize: 1
}))
lineSegments.computeLineDistances()
updatable.push(new Cursor(scene, config.cursor, camera))
updatable.push(new Grid(), editor = new Editor())
objects.box = lineSegments
scene.add(lineSegments)
// scene.add(circle(0xffff00, 0, 5))
// scene.add(circle(0xff00ff, 1, 4))
// scene.add(circle(0x0000ff, 2, 3))
// scene.add(circle(0x00ffff, 3, 2))
// scene.add(circle(0xff0000, 4, 1))
}
// Buffer for smooth update
const smoothBuffer = {cam: {x: 0, y: 0}}
function pn(b: boolean): number
{
return b ? 1 : -1
}
/**
* Update frame
*
* @param dt delta time in ms
*/
function update(dt: number): void
{
// objects['cursor'].position.set(moused.x, moused.y, 150)
// smoothBuffer.cam.x = moused.x * config.mouseFactor
// smoothBuffer.cam.y = moused.y * config.mouseFactor
// smoothUpdate()
// const time = Date.now() * 0.001
// objects.box.rotation.x = 0.25 * time
// objects.box.rotation.y = 0.25 * time
function smoothUpdate(): void
{
// Pixels moved = speed * time
const delta = config.smooth.mouseSpeed * dt
// Current position
const cp = camera.position
// Target position
const tp = smoothBuffer.cam
if (Math.abs(cp.x - tp.x) > delta)
{
cp.x = cp.x + delta * pn(cp.x < tp.x)
} else
{
cp.x = tp.x
}
if (Math.abs(cp.y - tp.y) > delta)
{
cp.y = cp.y + delta * pn(cp.y < tp.y)
} else
{
cp.y = tp.y
}
}
}
// ///////////////////
// Three.js meta code
/**
* Start the three.js rendering engine
*
* @param id: Canvas element id
*/
export function start(id: string): void
{
scene = new THREE.Scene()
scene.background = new Color('#f9f2e0')
// Create camera
camera = new THREE.PerspectiveCamera(config.cam.fov, window.innerWidth / window.innerHeight,
config.cam.near, config.cam.far)
camera.position.set(0, 0, 200)
camera.lookAt(0, 0, 0)
// @ts-ignore Create WebGL Renderer
renderer = new THREE.WebGLRenderer({canvas: document.getElementById(id), antialias: true})
onWindowResize()
window.addEventListener('resize', onWindowResize)
addLights()
init()
initMouseTracker()
animate()
}
function addLights(): void
{
objects.hemiLight = addHemiLight(scene)
objects.dirLight = addDirLight(scene)
objects.ground = addGround(scene)
objects.sky = addSky(scene)
objects.sky.visible = false
objects.ground.visible = false
renderer.outputEncoding = THREE.sRGBEncoding
renderer.shadowMap.enabled = true
}
function onWindowResize()
{
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
}
function animate(): void
{
requestAnimationFrame(animate)
const dt = clock.getDelta()
update(dt)
for (const o of updatable) o.update(dt)
renderer.render(scene, camera)
}
-118
View File
@@ -1,118 +0,0 @@
import * as THREE from 'three'
import {DirectionalLight, HemisphereLight, Mesh} from 'three'
import {colors, config} from "@/animation/Config";
export const vertexShader = `
varying vec3 vWorldPosition;
void main() {
vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
vWorldPosition = worldPosition.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`;
export const fragmentShader = `
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform float offset;
uniform float exponent;
varying vec3 vWorldPosition;
void main() {
float h = normalize( vWorldPosition + offset ).y;
gl_FragColor = vec4( mix( bottomColor, topColor, max( pow( max( h , 0.0), exponent ), 0.0 ) ), 1.0 );
}`;
/**
* Create sky dome
* @param scene
*/
export function addSky(scene: THREE.Scene): Mesh
{
const uniforms = {
"topColor": { value: colors.sky.top },
"bottomColor": { value: colors.sky.bottom },
"offset": { value: 33 },
"exponent": { value: 0.6 }
}
const skyGeo = new THREE.SphereGeometry( 4000, 32, 15 );
const skyMat = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vertexShader,
fragmentShader: fragmentShader,
side: THREE.BackSide
});
const sky = new THREE.Mesh(skyGeo, skyMat)
scene.add(sky)
scene.fog = new THREE.Fog(colors.sky.bottom, 1, 5000)
return sky
}
/**
* Create ground
* @param scene
*/
export function addGround(scene: THREE.Scene): Mesh
{
const groundGeo = new THREE.PlaneGeometry(10000, 10000)
const groundMat = new THREE.MeshLambertMaterial({color: colors.sky.ground})
const ground = new THREE.Mesh(groundGeo, groundMat)
ground.position.y = -33
ground.rotation.x = -Math.PI / 2
ground.receiveShadow = true
scene.add(ground)
return ground
}
/**
* Add hemisphere light
* @param scene
*/
export function addHemiLight(scene: THREE.Scene): HemisphereLight
{
const hemiLight = new THREE.HemisphereLight(colors.sky.top, colors.sky.ground, 0.6)
hemiLight.position.set(0, 50, 0)
scene.add(hemiLight)
if (config.debug) scene.add(new THREE.HemisphereLightHelper(hemiLight, 10))
return hemiLight
}
/**
* Add directional light
* @param scene
*/
export function addDirLight(scene: THREE.Scene): DirectionalLight
{
// Directional light
const dirLight = new THREE.DirectionalLight(colors.sky.dirLight, 1)
dirLight.position.set(-1, 1.75, 1)
dirLight.position.multiplyScalar(30)
scene.add(dirLight)
dirLight.castShadow = true
dirLight.shadow.mapSize.width = 2048
dirLight.shadow.mapSize.height = 2048
const d = 50
dirLight.shadow.camera.left = -d
dirLight.shadow.camera.right = d
dirLight.shadow.camera.top = d
dirLight.shadow.camera.bottom = -d
dirLight.shadow.camera.far = 3500
dirLight.shadow.bias = -0.0001
if (config.debug) scene.add(new THREE.DirectionalLightHelper(dirLight, 10))
return dirLight
}
-41
View File
@@ -1,41 +0,0 @@
import * as THREE from 'three'
import {Vector3} from 'three'
import {camera} from "@/animation/Home";
export let mouse: MouseEvent
export const moused = {x: 0, y: 0, pos: new Vector3()}
/**
* Initialize mouse tracker
*/
export function initMouseTracker(): void
{
document.onmousemove = (e: MouseEvent) =>
{
mouse = e
moused.x = e.clientX / window.innerWidth * 2 - 1
moused.y = -(e.clientY / window.innerHeight * 2 - 1)
moused.pos = projectTo3D(moused.x, moused.y, 0)
}
}
/**
* Project to 3D position
* https://www.reddit.com/r/threejs/comments/eba9l3/3d_cursor_using_threejs_html_css/
* https://jsfiddle.net/atwfxdpd/10/
*
* @param screenX X position on 2D canvas
* @param screenY Y position on 2D canvas
* @param z Z position in 3D
*/
export function projectTo3D(screenX: number, screenY: number, z: number): Vector3
{
const vector = new THREE.Vector3(screenX, screenY, 0.5)
vector.unproject(camera)
const dir = vector.sub(camera.position).normalize()
const distance = (-camera.position.z + z) / dir.z
const pos = camera.position.clone().add(dir.multiplyScalar(distance))
// console.log('Dir:', dir)
// console.log('Pos:', pos)
return pos
}
-62
View File
@@ -1,62 +0,0 @@
import * as THREE from "three";
import {Color, Material} from "three";
import {MeshLineGeometry, MeshLineMaterial} from 'meshline';
import IUpdatable from "@/animation/components/IUpdatable";
import {moused} from "@/animation/Trackers";
import {alwaysOnTop, circle} from "@/animation/Helpers";
type CursorConfig = {radius: number, color: Color, width: number}
export default class Cursor implements IUpdatable
{
camera: THREE.Camera
circle: CursorCircle
dot: THREE.Mesh
constructor(scene: THREE.Scene, conf: CursorConfig, camera: THREE.Camera)
{
this.camera = camera
this.circle = new CursorCircle(conf, camera)
scene.add(this.circle)
this.dot = circle('#000', 0, 0.3)
alwaysOnTop(this.dot, this.dot.material as Material)
scene.add(this.dot)
this.circle.visible = false
}
update(dt: number): void
{
this.circle.update(dt)
this.dot.position.copy(moused.pos)
this.circle.position.copy(moused.pos)
}
}
export class CursorCircle extends THREE.Mesh implements IUpdatable
{
camera: THREE.Camera
constructor(conf: CursorConfig, camera: THREE.Camera)
{
// https://discourse.threejs.org/t/shift-vertices-of-circle-geometry-not-working/26664
const pts = new THREE.Path().absarc(0, 0, conf.radius, 0, Math.PI * 2, true).getPoints(90)
const geometry = new MeshLineGeometry()
geometry.setFromPoints(pts)
// MeshLine: https://stackoverflow.com/a/25759280/7346633
const material = new MeshLineMaterial({color: conf.color, lineWidth: conf.width,
resolution: new THREE.Vector2(window.innerWidth, window.innerHeight)})
super(geometry, material)
this.camera = camera
alwaysOnTop(this, material)
}
update(dt: number): void
{
return
}
}
-92
View File
@@ -1,92 +0,0 @@
import {Mesh, MeshBasicMaterial, Vector3} from "three";
import IUpdatable from "@/animation/components/IUpdatable";
import {circle} from "@/animation/Helpers";
import {scene} from "@/animation/Home";
import {moused, projectTo3D} from "@/animation/Trackers";
import {config} from "@/animation/Config";
import {minMax} from "@/scripts/utils";
type DisplayCircle = {x: number, y: number, z: number, radius: number, color: string}
export default class Editor implements IUpdatable
{
hand: Mesh
radius = 3
scale = 1
z = config.editor.zMax
data: DisplayCircle[] = []
circles: Mesh[] = []
constructor()
{
this.hand = circle('#ffffff', 0, this.radius)
scene.add(this.hand)
window.addEventListener('mousedown', (e) =>
{
console.log('clicked', e)
this.addCirc(this.radius * this.scale, this.hand.position, this.color)
})
window.addEventListener('wheel', (e) =>
{
let direction = (e.detail < 0 || e.deltaY > 0) ? 1 : -1;
// Shift to micro-adjust
if (e.shiftKey) direction /= 10
// Ctrl + Alt to shift the entire plane
if (e.altKey && e.ctrlKey)
{
// TODO
return
}
// Scroll to adjust z, alt + scroll to adjust radius
if (e.altKey)
{
this.scale -= direction / 10
this.hand.scale.set(this.scale, this.scale, this.scale)
}
else this.z = minMax(this.z - direction * 2, config.editor.zMin, config.editor.zMax)
}, false)
}
addCirc(radius: number, pos: Vector3, color: string): void
{
const data = {...pos, radius, color}
data.z -= config.editor.zMax
this.data.push(data)
this.displayCirc(data)
}
displayCirc(params: DisplayCircle): void
{
const circ = circle(params.color, 0, params.radius)
circ.position.x = params.x
circ.position.y = params.y
circ.position.z = params.z + config.editor.zMax
scene.add(circ)
this.circles.push(circ)
}
update(dt: number): void
{
const pos = projectTo3D(moused.x, moused.y, this.z)
this.hand.position.copy(pos)
return
}
get color(): string
{
return '#' + this.handMaterial.color.getHexString()
}
set color(value: string)
{
this.handMaterial.color.setStyle(value.substr(0, 7))
}
get handMaterial(): MeshBasicMaterial { return (this.hand.material as MeshBasicMaterial) }
}
-38
View File
@@ -1,38 +0,0 @@
import * as THREE from "three";
import {GridHelper} from "three";
import {scene} from "@/animation/Home";
import IUpdatable from "@/animation/components/IUpdatable";
export default class Grid implements IUpdatable
{
lines = []
grid: GridHelper[] = []
constructor()
{
const size = 100
const divisions = 20
let color = new THREE.Color('#7a0c0c')
let grid = new THREE.GridHelper(size, divisions, color, color)
grid.rotation.x = Math.PI / 2
grid.position.z = 90
this.grid.push(grid)
color = new THREE.Color('#a4a4a4')
grid = new THREE.GridHelper(size, divisions, color, color)
grid.rotation.x = Math.PI / 2
grid.position.z = 70
this.grid.push(grid)
this.grid.forEach(it => scene.add(it))
}
update(dt: number): void
{
// this.grid.rotation.x = 0.25 * Date.now() * 0.001
// console.log(this.grid.rotation.x)
// this.grid.position.z += dt * 10
// console.log(this.grid.position.z)
}
}
-4
View File
@@ -1,4 +0,0 @@
export default interface IUpdatable
{
update: (dt: number) => void
}
+16 -23
View File
@@ -1,6 +1,6 @@
<template>
<div class="index index-tags" v-if="mode === 'tags'">
<Tag v-for="t in meta.tags" :key="t" :tag-name="t[0]" direction="right"
<Tag v-for="t in meta.tags" :key="`${t[0]}-${t[1]}`" :tag-name="t[0]" direction="right"
@click="e => clickTag(e, t)">{{ t[0] }} ({{ t[1] }})</Tag>
</div>
<div class="index index-categories" v-else>
@@ -9,38 +9,31 @@
</div>
</template>
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
import {staticMeta} from '@/views/Blog.vue'
<script setup lang="ts">
import Tag from "@/components/Tag.vue";
import {Prop} from "vue-property-decorator";
import {pushQuery} from "@/scripts/router";
import {BlogMeta} from "@/scripts/models";
import {globals} from "@/scripts/global";
@Options({components: {Tag}})
export default class BlogIndexLinks extends Vue
{
@Prop({default: 'tags'}) mode: 'tags' | 'categories' = 'tags'
withDefaults(defineProps<{ mode?: 'tags' | 'categories' }>(), {
mode: 'tags'
})
meta: BlogMeta = staticMeta
const meta: BlogMeta = globals.staticMeta
clickCat(e: MouseEvent, cat: [string, number]): void
{
e.stopPropagation()
pushQuery({category: cat[0], tag: null})
}
const clickCat = (e: MouseEvent, cat: [string, number]): void => {
e.stopPropagation()
pushQuery({category: cat[0], tag: null})
}
clickTag(e: MouseEvent, tag: [string, number]): void
{
e.stopPropagation()
pushQuery({tag: tag[0], category: null})
}
const clickTag = (e: MouseEvent, tag: [string, number]): void => {
e.stopPropagation()
pushQuery({tag: tag[0], category: null})
}
</script>
<style lang="sass" scoped>
@import 'src/css/colors'
@use '../css/colors'
.index
*
@@ -56,7 +49,7 @@ export default class BlogIndexLinks extends Vue
.index-categories
font-size: 0.8em
color: $color-text-special
color: colors.$color-text-special
*
text-decoration: underline
+7 -8
View File
@@ -1,6 +1,6 @@
<template>
<div id="BlogPostPreview" class="card" :class="elClass">
<img class="title-image" :src="image" v-if="image && imageOnTop" alt="Title Image">
<img class="title-image" :src="p.meta.title_image" v-if="p.meta.title_image && imageOnTop" alt="Title Image">
<div id="titles" class="unselectable clickable" @click="clickTitle">
<div id="date">{{ date.format('YYYY-MM-DD') }}</div>
@@ -15,7 +15,7 @@
</div>
<div id="content">
<img class="title-image" :src="image" v-if="image && !imageOnTop" alt="Title Image">
<img class="title-image" :src="p.meta.title_image" v-if="p.meta.title_image && !imageOnTop" alt="Title Image">
<div id="text" class="markdown-content">
<Dynamic :template="content"></Dynamic>
</div>
@@ -106,13 +106,12 @@ const elClass = computed(() =>
return classes
})
const content = computed(() => marked(p.meta.content.replaceAll('\n', ' \n')))
const content = marked(p.meta.content.replaceAll('\n', ' \n').replaceAll("{src}", hosts.content))
const date = moment(p.meta.date)
const image = p.meta.title_image ? hosts.content + '/' + p.meta.title_image : null
</script>
<style lang="sass" scoped>
@import 'src/css/colors'
@use '../css/colors'
#BlogPostPreview
text-align: left
@@ -122,7 +121,7 @@ const image = p.meta.title_image ? hosts.content + '/' + p.meta.title_image : nu
#date
font-size: 0.7em
color: $color-text-light
color: colors.$color-text-light
> * + *, #content > * + *
padding-top: 10px
@@ -151,7 +150,7 @@ const image = p.meta.title_image ? hosts.content + '/' + p.meta.title_image : nu
#subtitle
font-size: 0.8em
color: $color-text-light
color: colors.$color-text-light
img
$margin: 10px
@@ -172,7 +171,7 @@ const image = p.meta.title_image ? hosts.content + '/' + p.meta.title_image : nu
#expand
font-size: 0.8em
padding-top: 10px
color: $color-text-light
color: colors.$color-text-light
// Put image on top
#BlogPostPreview.image-top
+14 -20
View File
@@ -7,30 +7,24 @@
</div>
</template>
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
import {Prop} from "vue-property-decorator";
<script setup lang="ts">
import {computed, onMounted} from 'vue'
import {$} from '@/scripts/constants';
@Options({components: {}})
export default class Collapse extends Vue
{
@Prop() title!: string
@Prop({default: false}) active = false
const props = withDefaults(defineProps<{ title: string, active?: boolean }>(), {
active: false
})
show = false
const displayTitle = computed((): string => decodeURIComponent(props.title))
get displayTitle(): string
{
return decodeURIComponent(this.title)
}
mounted(): void
{
$('.collapse').accordion({collapsible: true, header: 'h3', heightStyle: 'content',
active: this.active})
}
}
onMounted((): void => {
$('.collapse').accordion({
collapsible: true,
header: 'h3',
heightStyle: 'content',
active: props.active
})
})
</script>
<style lang="sass">
+15 -23
View File
@@ -7,41 +7,33 @@
</table>
</template>
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
import {Prop} from "vue-property-decorator";
<script setup lang="ts">
import {computed} from 'vue'
@Options({components: {}})
export default class MetaTable extends Vue
{
@Prop({required: true}) table!: {[id: string]: unknown}
const props = defineProps<{ table: {[id: string]: unknown} }>()
get filteredTable(): {[id: string]: unknown}
{
const t: {[id: string]: unknown} = {}
const filteredTable = computed((): {[id: string]: unknown} => {
const t: {[id: string]: unknown} = {}
Object.keys(this.table).forEach(k => {
// Ignore empty
if (!this.table[k]) return
Object.keys(props.table).forEach(k => {
if (!props.table[k]) return
// Convert to sentence case (https://stackoverflow.com/a/7225450/7346633)
let newK = k.replace(/([A-Z])/g, " $1")
newK = newK.charAt(0).toUpperCase() + newK.slice(1)
t[newK] = this.table[k]
})
let newK = k.replace(/([A-Z])/g, " $1")
newK = newK.charAt(0).toUpperCase() + newK.slice(1)
t[newK] = props.table[k]
})
return t
}
}
return t
})
</script>
<style lang="sass" scoped>
@import "src/css/colors"
@use "../css/colors"
.meta
td:first-child
text-align: right
color: $color-text-light
color: colors.$color-text-light
td:last-child
display: inline-block
+3 -3
View File
@@ -15,11 +15,11 @@ const props = defineProps({
</script>
<style lang="sass" scoped>
@import src/css/colors
@use "../css/colors"
$tag-height: 20px
$tag-color: $color-bg-6
$text-color: $color-text-light
$tag-color: colors.$color-bg-6
$text-color: colors.$color-text-light
$padding: calc($tag-height / 2)
$triangle-width: calc($tag-height / 2) * 0.8
+27 -31
View File
@@ -16,16 +16,15 @@
<div id="attachments" v-if="item.attachments.length !== 0">
<div class="label">Attachments</div>
<div class="content" v-for="a of item.attachments" :key="a.data.key">
<a :href="a.links['enclosure'].href">{{a.data.title}}</a>
<a :href="a.links['enclosure']?.href">{{a.data.title}}</a>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
import {Prop} from "vue-property-decorator";
<script setup lang="ts">
import {computed, onMounted} from 'vue'
import moment from "moment";
import MetaTable from "@/components/MetaTable.vue";
import {capitalize} from "@/scripts/utils";
@@ -33,34 +32,32 @@ import linkifyUrls from "linkify-urls";
import {$} from '@/scripts/constants';
import {ZoteroData, ZoteroItem} from "@/scripts/zotero";
@Options({components: {MetaTable}})
export default class ZoteroPublicationView extends Vue
{
@Prop({required: true}) item!: ZoteroItem
const props = defineProps<{ item: ZoteroItem }>()
get d(): ZoteroData { return this.item.data }
get date(): moment.Moment { return moment(this.item.meta.parsedDate) }
get authors(): string { return this.d.creators.map(it => it.firstName + ' ' + it.lastName).join(' & ') }
const d = computed((): ZoteroData => props.item.data)
const date = computed((): moment.Moment => moment(props.item.meta.parsedDate))
const authors = computed((): string => d.value.creators.map(it => it.firstName + ' ' + it.lastName).join(' & '))
get tableData(): {[id: string]: unknown}
{
const t: {[id: string]: unknown} = {...this.d}
t.creators = this.authors
delete t.key
delete t.version
delete t.title
delete t.abstractNote
if (t.itemType) t.itemType = capitalize(t.itemType as string)
if (t.url) t.url = linkifyUrls(t.url as string)
return t
}
const tableData = computed((): {[id: string]: unknown} => {
const t: {[id: string]: unknown} = {...d.value}
t.creators = authors.value
delete t.key
delete t.version
delete t.title
delete t.abstractNote
if (t.itemType) t.itemType = capitalize(t.itemType as string)
if (t.url) t.url = linkifyUrls(t.url as string)
return t
})
mounted(): void
{
$('.publication').accordion({collapsible: true, header: 'div.header', active: false,
heightStyle: "content"})
}
}
onMounted((): void => {
$('.publication').accordion({
collapsible: true,
header: 'div.header',
active: false,
heightStyle: 'content'
})
})
</script>
<style lang="sass" scoped>
@@ -77,6 +74,7 @@ export default class ZoteroPublicationView extends Vue
#details
padding-left: calc(1.6em + 6px)
font-size: 0.8em
> div
margin-bottom: 1em
@@ -87,8 +85,6 @@ export default class ZoteroPublicationView extends Vue
.label
font-weight: bold
font-size: 0.8em
.header
align-items: center
-285
View File
@@ -1,285 +0,0 @@
<template>
<div id="ColorPicker" ref="el" @keydown.esc="close">
<div id="title-colors" class="fbox-h">
<div class="text" @mousedown="windowDrag" @click.right.alt="close">Colors</div>
<div class="close fbox-vcenter" @click="close" v-if="showClose"><i class="fas fa-times"></i></div>
<input v-model="colorInput" spellcheck="false" @change="colorModel = colorInput">
</div>
<ColorPicker id="picker" :isWidget="true" pickerType="chrome" v-model:pureColor="colorModel"
:disableHistory="true" @pureColorChange="change" format="hex8"/>
<div id="palette">
<div class="row" v-for="(p, i) of palette" :key="i">
<div class="color" v-for="(c, j) in p" :key="j" :style="{'background-color': c ? c : '#333'}"
@click.exact="setPalette(i, j)"
@click.right.alt="(e) => altClickPalette(e, i, j)"
@click.right.exact="(e) => rightClickPalette(e, i, j)"
draggable="true" @dragstart="paletteDragStart(i, j)" @drop="(e) => dropPalette(e, i, j)"
@dragenter="(e) => paletteDragEnter(e, i, j)" @dragover="(e) => paletteDragOver(e, i, j)"/>
</div>
</div>
</div>
</template>
<script lang="ts">
import "vue3-colorpicker/style.css";
import {ColorPicker} from "vue3-colorpicker";
import {range} from "@/scripts/utils";
import {Emit, Model, Prop, Ref} from "vue-property-decorator";
import {Options, Vue} from "vue-class-component";
/**
* Mouse Usage:
* - Alt + Right-click the title to close
* - Left-click on a color block to add color to palette
* - Left-drag on a color block to move color
* - Right-click on a color block to select color
* - Alt + Right-click on a color block to remove color
*/
@Options({components: {ColorPicker}})
export default class MyColorPicker extends Vue
{
@Model('color') color!: string // Color prop in #ffffffff format
colorModel = '' // Color model in #ffffffff format
colorInput = '' // Color input in ffffff format
palette: string[][] = []
showClose = false
@Ref() el!: HTMLElement
@Prop({default: null}) initialPos?: {x: number, y: number}
/**
* Init
*/
created(): void
{
this.colorModel = this.color
this.colorInput = this.colorModel.substr(1, 6)
const storedPalette = localStorage.getItem('palette')
this.palette = !storedPalette ? range(6).map(_ => range(10).map(_ => '')) : JSON.parse(storedPalette);
this.storePalette()
}
mounted(): void
{
if (this.initialPos) this.setPos(this.initialPos.x, this.initialPos.y)
}
/**
* Color change
*/
@Emit('update:color')
change(color: string): string
{
this.colorInput = color.substr(1, 6)
return this.colorModel
}
/**
* Set window position
*/
setPos(x: number, y: number): void
{
this.el.style.left = x + 'px'
this.el.style.top = y + 'px'
}
/**
* Window dragging
*/
windowDrag(e: MouseEvent): void
{
e.preventDefault()
let lastX = e.clientX, lastY = e.clientY
const mousemove = (e: MouseEvent) =>
{
const dx = lastX - e.clientX, dy = lastY - e.clientY
lastX = e.clientX; lastY = e.clientY
this.setPos(this.el.offsetLeft - dx, this.el.offsetTop - dy)
}
const mouseup = () => {document.removeEventListener('mouseup', mouseup); document.removeEventListener('mousemove', mousemove)}
document.addEventListener('mouseup', mouseup)
document.addEventListener('mousemove', mousemove)
}
@Emit('updatePalette')
storePalette(): string[][]
{
localStorage.setItem('palette', JSON.stringify(this.palette))
return this.palette
}
@Emit()
close(e?: Event): void
{
if (e) e.preventDefault()
console.log('Color picker close')
}
/**
* Left click to override
*/
setPalette(i: number, j: number): void
{
this.palette[i][j] = this.colorModel
this.storePalette()
}
/**
* Right click to select
*/
rightClickPalette(e: Event, i: number, j: number): void
{
e.preventDefault()
if (!this.palette[i][j]) return
this.colorModel = this.palette[i][j]
this.change(this.colorModel)
}
/**
* Alt right click to remove
*/
altClickPalette(e: Event, i: number, j: number): void
{
e.preventDefault()
this.palette[i][j] = ''
this.storePalette()
}
dragging = {i: 0, j: 0}
paletteDragStart(i: number, j: number): void
{
this.dragging = {i, j}
}
dropPalette(e: DragEvent, i: number, j: number): void
{
// We can assume that toI != fromI
const fromI = this.dragging.i * 10 + this.dragging.j
const toI = i * 10 + j
const incr = toI > fromI ? 1 : -1
const currentColor = this.palette[this.dragging.i][this.dragging.j]
for (let index of range(fromI, toI))
{
const col = index % 10, row = Math.floor(index / 10)
const lastI = index + incr
const lastC = lastI % 10, lastR = Math.floor(lastI / 10)
console.log(lastR, lastC, 'TO', row, col)
this.palette[row][col] = this.palette[lastR][lastC]
}
this.palette[i][j] = currentColor
this.storePalette()
}
paletteDragEnter(e: DragEvent, i: number, j: number): void
{
// TODO: Drag preview
console.log('Drag enter')
console.log(e)
console.log('' + i + ' ' + j)
}
paletteDragOver(e: DragEvent, i: number, j: number): void
{
// Only allow drag if it's not dragging onto itself
if (!(i == this.dragging.i && j == this.dragging.j)) e.preventDefault()
}
}
</script>
<style lang="sass">
#ColorPicker
position: absolute
border-radius: 20px
overflow: hidden
$cp-color: #4d4d4d
$txt-color: #cbcbcb
background-color: $cp-color
box-shadow: 0 0 10px #00000026
color: $txt-color
#title-colors
margin: 8px 0
font-size: 1.2em
padding: 0 15px
div.close
font-size: 10px
margin-right: 10px
div.text
flex-grow: 1
text-align: left
user-select: none
input
background-color: lighten($cp-color, 6)
font-family: monospace
color: $txt-color
border: none
padding: 0 10px
width: 60px
text-align: center
border-radius: 8px
transition: all 0.25s ease
input:focus-visible
outline: none
background-color: lighten($cp-color, 10)
.vc-colorpicker
width: 300px
background-color: transparent
box-shadow: none
padding-bottom: 0
border-radius: 0
.vc-colorpicker--container
padding: 0 3px
.vc-chrome-colorPicker, .vc-chrome-colorPicker-body
background-color: transparent
.vc-display
display: none
.vc-saturation, .vc-saturation__white, .vc-saturation__black
border-radius: 5px
.vc-saturation
height: 200px
.vc-chrome-colorPicker-body
margin-left: 10px
margin-right: 10px
#palette
width: 300px
// Transparency texture
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)
background-repeat: repeat
.row
width: 100%
display: flex
.color:first-child
border-left: none
.row:first-child .color
border-top: none
.color
width: 30px
height: 30px
box-sizing: border-box
border-left: 1px solid $cp-color
border-top: 1px solid $cp-color
flex-grow: 1
</style>
-26
View File
@@ -1,26 +0,0 @@
<template>
<div id="Projects">
<MyColorPicker v-model:color="color"></MyColorPicker>
<button @click="log"></button>
</div>
</template>
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
import MyColorPicker from "@/components/color/ColorPicker.vue";
@Options({components: {MyColorPicker}})
export default class Projects extends Vue
{
color = '#ffffff'
log(): void
{
console.log(this.color)
}
}
</script>
<style lang="sass" scoped>
</style>
+6 -4
View File
@@ -1,7 +1,9 @@
@font-face
font-family: 'WenKai'
src: url("/fonts/LXGWWenKai-Regular.woff2") format('woff2'), url("/fonts/LXGWWenKai-Regular.woff") format('woff')
//@font-face
// font-family: 'WenKai'
// src: url("/fonts/LXGWWenKai-Regular.woff2") format('woff2'), url("/fonts/LXGWWenKai-Regular.woff") format('woff')
@import 'lxgw-wenkai-webfont/style.css'
.font-custom
font-family: "WenKai", var(--font-fallback)
//font-family: "WenKai", var(--font-fallback)
font-family: "LXGW WenKai", var(--font-fallback), serif
//line-height: 1.3em
+31 -9
View File
@@ -1,5 +1,5 @@
@import colors
@import font
@use "colors"
@use "font"
// Google Fonts
// TODO: Localize
@@ -119,7 +119,7 @@ body
-webkit-font-smoothing: antialiased
-moz-osx-font-smoothing: grayscale
text-align: center
color: $color-text-main
color: colors.$color-text-main
// Max width and center
max-width: 900px
@@ -170,30 +170,52 @@ html
text-justify: inter-word
a
color: $color-text-special
color: colors.$color-text-special
text-decoration: none
h1, h2
border-bottom: 1px solid $color-text-special
border-bottom: 1px solid colors.$color-text-special
font-size: 1.5em
margin-top: 1em
h1, h2
line-height: 1.3
margin-bottom: 0.25em
padding: 0
h2
font-size: 1.3em
h3
font-size: 1.2em
p
font-size: 0.9em
margin: 0.5em 0
font-size: 1em
margin: 0.7em 0
line-height: 1.6
p:last-child
margin-bottom: 0
li
font-size: 0.9em
font-size: 1em
margin-bottom: 0.25em
figure
margin: 0
img
width: calc(100% + 20px)
margin-left: -10px
margin-right: -10px
border-radius: 10px
object-fit: cover
max-height: 500px
caption
display: flex
width: 100%
justify-content: center
align-items: center
font-size: 0.8em
color: colors.$color-text-light
+2 -2
View File
@@ -1,4 +1,4 @@
@import "src/css/colors"
@use "../css/colors"
$width: 600px
@@ -14,7 +14,7 @@ $width: 600px
margin-bottom: 0
.subtitle
color: $color-text-light
color: colors.$color-text-light
// Phone layout
+1
View File
@@ -6,6 +6,7 @@ import '@fortawesome/fontawesome-free/css/all.min.css'
import Collapse from "@/components/Collapse.vue"
import BlogIndex from "@/components/BlogIndex.vue";
import Tag from "@/components/Tag.vue";
import 'virtual:uno.css'
const app = createApp(App).use(router).use(i18n)
.component('Collapse', Collapse)
+78
View File
@@ -0,0 +1,78 @@
from pathlib import Path
import re
import subprocess
import tempfile
def extract_and_migrate_sass(file_path):
"""Extracts <style lang="sass"> block from a Svelte file, runs sass-migrator on it, and replaces the original style block."""
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
# Regex pattern to match <style lang="sass">...</style>
style_pattern = re.compile(r'<style\s+lang\s*=\s*["\']sass["\']( scoped)*>(.*?)</style>', re.DOTALL)
# Extract all matching style blocks
matches = list(style_pattern.finditer(content))
if not matches:
print(f"No <style lang='sass'> block found in {file_path}")
return
updated_content = content
for match in matches:
original_style_block = match.group(0) # The full <style>...</style> block
scoped = match.group(1) # The scoped attribute if present
sass_content = match.group(2) # The content inside the <style> block
# Create a temporary file in the same directory as the .svelte file
temp_file_path = Path(file_path).parent / f"{Path(file_path).stem}_temp.sass"
# Write the SASS content to the temporary file
with open(temp_file_path, 'w', encoding='utf-8') as temp_file:
# Calculate relative path depth
depth = len(Path(file_path).parent.relative_to(Path(__file__).parent).parts)
print(Path(file_path).parent.relative_to(Path(__file__).parent).parts)
temp_file.write(sass_content.replace("src/", "../" * depth))
# Run the sass-migrator on the temporary file
try:
subprocess.run(['sass-migrator', 'module', str(temp_file_path)], check=True)
except subprocess.CalledProcessError as e:
print(f"Error while running sass-migrator on {file_path}: {e}")
continue
# Read back the migrated content
with open(temp_file_path, 'r', encoding='utf-8') as temp_file:
migrated_sass_content = temp_file.read()
# Create the new <style> block with the migrated SASS content
new_style_block = f'<style lang="sass"{scoped or ''}>{migrated_sass_content}</style>'
# Replace the original style block with the new one
updated_content = updated_content.replace(original_style_block, new_style_block)
# Remove the temporary file
temp_file_path.unlink()
# Write the updated content back to the original file
with open(file_path, 'w', encoding='utf-8') as file:
file.write(updated_content)
print(f"Updated {file_path}")
def process_svelte_files(directory):
"""Recursively processes all .svelte files in the given directory."""
svelte_files = Path(directory).rglob("*.vue")
for svelte_file in svelte_files:
extract_and_migrate_sass(svelte_file)
def main():
"""Main function to process all .svelte files in the current directory."""
current_directory = Path(__file__).parent
process_svelte_files(current_directory)
if __name__ == "__main__":
main()
+5
View File
@@ -0,0 +1,5 @@
import {BlogMeta} from "@/scripts/models";
export let globals = {
staticMeta: {tags: [], categories: [], posts: []} as BlogMeta,
}
+2 -1
View File
@@ -8,7 +8,8 @@ export const messages = {
blog: '记事本',
life: '生活',
projects: 'Projects',
others: '更多'
others: '更多',
photo: '相册'
}
},
zh: {
+12 -12
View File
@@ -8,12 +8,6 @@ const routes: Array<RouteRecordRaw> = [
meta: {title: 'Home', nav: true},
component: Home
},
{
path: '/new-home',
name: 'New Home',
meta: {title: 'Home'},
component: () => import('../views/NewHome.vue')
},
{
path: '/about',
name: 'About',
@@ -33,6 +27,18 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('../views/Blog.vue'),
props: route => (route.query)
},
{
path: '/photo',
name: 'Photo',
meta: {title: '相册', nav: true},
component: () => import('../views/Photo.vue')
},
{
path: '/photo/:id',
name: 'Photo ID',
meta: {title: '相册', navBookmark: 'Photo'},
component: () => import('../views/Photo.vue')
},
{
path: '/others',
name: 'Others',
@@ -57,12 +63,6 @@ const routes: Array<RouteRecordRaw> = [
meta: {title: '我做的', navBookmark: 'Others'},
component: () => import('../views/others/Projects.vue')
},
{
path: '/color',
name: 'ColorPicker',
meta: {title: 'Color Picker'},
component: () => import('../components/color/ColorPickerTest.vue')
},
]
export const router = createRouter({
+2 -4
View File
@@ -1,5 +1,3 @@
import {Vue} from "vue-class-component";
/**
* Same as python's range
*
@@ -41,9 +39,9 @@ export function minMax(val: number, min: number, max: number): number
export type Keybinds = {[id: string]: (e: KeyboardEvent) => unknown}
/**
* Key handler mixin
* Key handler standalone helper class (not a Vue mixin)
*/
export class KeyHandler extends Vue
export class KeyHandler
{
keybinds: Keybinds = {}
_keybinds: Keybinds = {}
+19 -30
View File
@@ -9,8 +9,8 @@
<Loading v-else></Loading>
</template>
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
<script setup lang="ts">
import {onMounted, ref} from 'vue'
import {marked} from 'marked';
import emojiRegex from 'emoji-regex';
import {parseExtensions} from '@/scripts/extended_markdown'
@@ -19,39 +19,28 @@ import {hosts} from "@/scripts/constants";
import Loading from "@/components/Loading.vue";
import {ZoteroAttachment, ZoteroItem} from "@/scripts/zotero";
@Options({components: {Loading, ZoteroPublication}})
export default class About extends Vue
{
html = ""
publications: ZoteroItem[] = []
const html = ref("")
const publications = ref<ZoteroItem[]>([])
mounted(): void
{
// Fetch readme
fetch(`${hosts.content}/README.md`).then(it => it.text())
.then(it => this.html = marked(parseExtensions(it.replace(emojiRegex(), (emoji) => {
return `<span class="emoji">${emoji}</span>`
}))))
onMounted((): void => {
fetch(`${hosts.content}/README.md`).then(it => it.text())
.then(it => html.value = marked(parseExtensions(it.replace(emojiRegex(), (emoji) => {
return `<span class="emoji">${emoji}</span>`
}))))
// Fetch publications from zotero
fetch(`${hosts.api}/zotero.json`)
.then(it => it.json()).then(it =>
{
// Filter out publications and attachments
this.publications = it
let files: ZoteroAttachment[] = it
files = files.filter(it => it.data.itemType === 'attachment')
this.publications = this.publications.filter(it => it.data.itemType !== 'attachment')
// Add attachments to
this.publications.forEach(it => it.attachments = files.filter(a => a.data.parentItem == it.key))
})
}
}
fetch(`${hosts.api}/zotero.json`)
.then(it => it.json()).then((it: ZoteroItem[]) => {
publications.value = it
let files: ZoteroAttachment[] = it as unknown as ZoteroAttachment[]
files = files.filter(file => file.data.itemType === 'attachment')
publications.value = publications.value.filter(pub => pub.data.itemType !== 'attachment')
publications.value.forEach(pub => pub.attachments = files.filter(a => a.data.parentItem == pub.key))
})
})
</script>
<style lang="sass">
@import "../css/colors"
@use "../css/colors"
#About
width: min(600px, 80vw)
+36 -39
View File
@@ -10,64 +10,61 @@
<span v-if="category">📂{{category}}</span>
<span class="no-after" v-if="post && activePost">{{activePost.title}}</span>
</div>
<BlogPostPreview v-for="m of filteredPosts" :key="m" :meta="m" :active="m === activePost"/>
<BlogPostPreview v-for="m of filteredPosts" :key="m.id" :meta="m" :active="m === activePost"/>
</div>
<Loading v-else></Loading>
</template>
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
<script setup lang="ts">
import BlogPostPreview from "@/components/BlogPost.vue";
import {hosts} from "@/scripts/constants";
import {Prop} from "vue-property-decorator";
import Loading from "@/components/Loading.vue";
import {BlogMeta, BlogPost} from "@/scripts/models";
import {BlogMeta} from "@/scripts/models";
import {Ref, ref, computed, onMounted} from "vue";
import {hosts} from "@/scripts/constants";
import {globals} from "@/scripts/global";
import {Router} from "vue-router";
export let staticMeta: BlogMeta = {tags: [], categories: [], posts: []}
let $router: Router
@Options({components: {Loading, BlogPostPreview}})
export default class Blog extends Vue
{
@Prop() post?: string
@Prop() category?: string
@Prop() tag?: string
const p = defineProps<{
post?: string,
category?: string,
tag?: string
}>()
meta: BlogMeta = {tags: [], categories: [], posts: []}
let meta: Ref<BlogMeta> = ref({tags: [], categories: [], posts: []})
mounted(): void
{
fetch(`${hosts.content}/content/generated/metas.json`).then(it => it.json()).then(it => {
this.meta = it
staticMeta = it
})
}
onMounted(() => {
fetch(`${hosts.content}/content/generated/metas.json`).then(it => it.json()).then(it => {
meta.value = it
globals.staticMeta = it
})
})
get activePost(): BlogPost | null
{
const p = this.filteredPosts
if (p.length == 0) return null
return this.post ? p.filter(it => it.url_name == this.post)[0] : p[0].pinned ? p[0] : null
}
const filteredPosts = computed(() => {
const posts = meta.value.posts.filter(it => it.pinned || (p.tag ? it.tags.includes(p.tag) :
p.category ? it.category == p.category : true))
get filteredPosts(): BlogPost[]
{
const posts = this.meta.posts.filter(it => it.pinned || (this.tag ? it.tags.includes(this.tag) :
this.category ? it.category == this.category : true))
// Put pinned posts on top
posts.sort((a, b) => (b.pinned ?? 0) - (a.pinned ?? 0))
// Put pinned posts on top
posts.sort((a, b) => (b.pinned ?? 0) - (a.pinned ?? 0))
return posts
})
return posts
}
}
const activePost = computed(() => {
const posts = filteredPosts.value
if (posts.length == 0) return null
if (!p.post) return null
return p.post ? posts.filter(it => it.url_name == p.post)[0] : posts[0].pinned ? posts[0] : null
})
</script>
<style lang="sass" scoped>
@import "src/css/colors"
@import "src/css/responsive"
@use "../css/colors"
@use "../css/responsive"
#breadcrumb
color: $color-text-light
color: colors.$color-text-light
margin-bottom: 20px
span:not(.no-after):after
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<div id="Home" class="fbox-center f-grow1">
<div id="box">
<div class="font-script-en">Hykilpikonna's</div>
<div class="font-script-en">Azalea's</div>
<div class="font-script-en bold">Road Less Traveled</div>
</div>
</div>
+2 -2
View File
@@ -20,8 +20,8 @@ import 'tg-blog/dist/style.css'
</script>
<style lang="sass" scoped>
@import "src/css/colors"
@import "src/css/responsive"
@use "../css/colors"
@use "../css/responsive"
.title
text-align: left
-161
View File
@@ -1,161 +0,0 @@
<template>
<div id="NewHome">
<canvas id="three"></canvas>
<!-- Editor controls -->
<div id="editor-controls">
<div>Editor</div>
<div class="separator"/>
<div>New</div>
<div>Save</div>
<div class="separator"/>
<div id="colors" class="fbox-h">
<div class="color" v-for="(c, i) in colors" :key="i" :style="{'background-color': c ?? '#333'}"
@click="e => openPicker(e, c)">
<div>{{(i + 1) % 10}}</div>
</div>
</div>
<div class="f-grow1"></div>
<div class="separator"/>
<div @click="_ => toggle('sky')">Sky</div>
</div>
<MyColorPicker v-if="pickerColor" :color="pickerColor" style="z-index: 3"
@close="pickerColor = ''" :initial-pos="initialPos"
@updatePalette="p => colors = p[0]"/>
</div>
</template>
<script lang="ts">
import {Options} from 'vue-class-component';
import {camera, editor, objects, start} from "@/animation/Home";
import {config} from "@/animation/Config";
import {KeyHandler, range} from "@/scripts/utils";
import MyColorPicker from "@/components/color/ColorPicker.vue";
@Options({components: {MyColorPicker}})
export default class NewHome extends KeyHandler
{
editMode = config.editMode
colors = localStorage.getItem('palette') ? JSON.parse(localStorage.getItem('palette') as string)[0] :
range(10).map(_ => '#ffa8a8')
pickerColor = ''
initialPos = {x: 0, y: 0}
started = false
created(): void
{
// Escape to close picker
this.keybinds.Escape = _ => this.pickerColor = ''
// Pick colors
range(10).forEach(i => this.keybinds[((i + 1) % 10)+''] = _ => editor.color = this.colors[i])
// Camera position binds
this.keybinds.ArrowLeft = _ => camera.position.x -= 1
this.keybinds.ArrowRight = _ => camera.position.x += 1
this.keybinds.ArrowUp = _ => camera.position.y += 1
this.keybinds.ArrowDown = _ => camera.position.y -= 1
this.keybinds.J = _ => camera.rotateY(Math.PI / 60)
this.keybinds.L = _ => camera.rotateY(-Math.PI / 60)
this.keybinds.I = _ => camera.rotateX(Math.PI / 60)
this.keybinds.K = _ => camera.rotateX(-Math.PI / 60)
this.keybinds.Ctrl0 = _ =>
{
camera.position.set(0, 0, 200)
camera.lookAt(0, 0, 0)
}
}
mounted(): void
{
if (!this.started)
{
start('three')
this.started = true
}
}
toggle(s: string): void
{
if (s == 'sky')
{
objects.hemiLight.visible = !objects.hemiLight.visible
objects.dirLight.visible = !objects.dirLight.visible
objects.sky.visible = !objects.sky.visible
}
}
openPicker(e: MouseEvent, c: string): void
{
this.pickerColor = c
this.initialPos = {x: e.clientX - 150, y: e.clientY + 50}
}
}
</script>
<style lang="sass" scoped>
#three
width: 100vw
height: 100vh
position: absolute
top: 0
left: 0
z-index: 1
cursor: none
#editor-controls
position: absolute
z-index: 2
user-select: none
// Positioning
top: 20px
left: 50px
height: 50px
width: calc(100vw - 100px - 40px)
padding: 0 20px
border-radius: 50px
// Flex center
display: flex
align-items: center
// Colors
color: #ffeedb
background-image: linear-gradient(180deg, #000000 0%, #434343 100%)
// Separator
.separator
width: 1px
height: 50%
border-radius: 100px
background-color: rgba(255, 238, 219, 0.37)
#colors
align-items: center
.color
width: 12px
height: 12px
div
margin-top: -10px
font-size: 8px
color: #8f8f8f
+ .color
margin-left: 5px
#editor-controls > * + *
margin-left: 10px
</style>
+5 -15
View File
@@ -8,25 +8,15 @@
<router-link class="rlink" to="/kitchen-menu">🍳 小桂桂的私房菜 菜单</router-link>
<router-link class="rlink" to="/friends">🎎 朋友们</router-link>
<a href="https://cast.hydev.org">📹 公开直播间</a>
<a href="http://status.hydev.org/">🔌 看看服务器们过得怎么样</a>
<a href="https://status.hydev.org/">🔌 看看服务器们过得怎么样</a>
</div>
</div>
</template>
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
@Options({})
export default class Others extends Vue
{
}
</script>
<style lang="sass" scoped>
@import "src/css/global"
@import "src/css/colors"
@import "src/css/responsive"
@use "../css/global"
@use "../css/colors"
@use "../css/responsive"
.links
display: flex
@@ -38,7 +28,7 @@ a
@extend .card
@extend .clickable
color: $color-text-main
color: colors.$color-text-main
text-decoration: none
</style>
+177
View File
@@ -0,0 +1,177 @@
<script setup lang="ts">
import {onMounted, ref} from 'vue'
import {useRoute} from "vue-router";
interface PhotoMetadata {
id: string
owner_key: string
upload_time: string
original_photo: string
edited_photo: string
thumbnail: string
thumbnail_edited: string
exif: {[id: string]: string}
}
function detRandom(seed: string): number {
return Array.from(seed).reduce((acc, char) => (acc + char.charCodeAt(0) * 65535) % 22859, 0) / 22859
}
async function waitTruthy<T>(condition: () => T, interval = 100): Promise<T> {
return new Promise((resolve) => {
const check = () => {
const value = condition()
if (value) resolve(value)
else setTimeout(check, interval)
}
check()
})
}
const route = useRoute()
const photos = ref<PhotoMetadata[]>([])
const photoRows = ref<PhotoMetadata[][]>([])
const rowProbabilityTable: Record<number, number> = {
1: 0,
2: 0.3,
3: 0.5
}
const initPhotos = async () => {
photos.value = await (await fetch('https://p.aza.moe/photos')).json()
photos.value.sort((a, b) => (a.exif.DateTime < b.exif.DateTime ? 1 : -1))
const rows: PhotoMetadata[][] = []
let currentRow: PhotoMetadata[] = []
photos.value.forEach((p) => {
if (currentRow.length === 0) currentRow.push(p)
else if (currentRow.length >= 3) {
rows.push(currentRow)
currentRow = [p]
} else {
const singleChance = detRandom(p.original_photo)
if (singleChance < rowProbabilityTable[currentRow.length]) {
rows.push(currentRow)
currentRow = [p]
} else currentRow.push(p)
}
})
if (currentRow.length > 0) rows.push(currentRow)
photoRows.value = rows
}
const url = (s: string): string => {
s = s.replace('data/photos', 'static').replace('./', '')
return `https://p.aza.moe/${s}`
}
const randomRotation = (s: string): string => {
const angle = (detRandom(s) * 20) - 10
return `rotate(${angle}deg)`
}
const clickPhoto = async (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 = ''
}
onMounted(async () => {
await initPhotos()
if (route.params.id) {
const photoEl = await waitTruthy(() => document.getElementById(`photo-${route.params.id}`))
photoEl.click()
}
})
</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 :id="`photo-${p.id}`">
<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
pointer-events: auto
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>
+31 -38
View File
@@ -23,61 +23,54 @@
</div>
</template>
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
<script setup lang="ts">
import {onMounted, ref} from 'vue'
import {fab, hosts} from "@/scripts/constants";
import {shuffle} from "@/scripts/utils";
export interface Friend {
interface Friend {
name: string
avatar: string
banner: string
desc?: string
[id: string]: string | undefined
}
const excludes = new Set(["name", "avatar", "banner", "desc"])
const icons = {
const icons: {[id: string]: string} = {
blog: 'fas fa-book'
}
@Options({components: {}})
export default class Friends extends Vue
{
friends: Friend[] = []
const friends = ref<Friend[]>([])
async created()
{
this.friends = await (await fetch(`${hosts.content}/content/generated/friends/friends.json`)).json()
// Fix avatar relative url
this.friends.forEach(f => {
if (!f.avatar.startsWith('http')) f.avatar = `${hosts.content}/${f.avatar}`
if (f.banner && !f.banner.startsWith('http')) f.banner = `${hosts.content}/${f.banner}`
})
this.friends = shuffle(this.friends)
}
bgStyle(f: Friend)
{
if (f.banner) return {'background-image': `url("${f.banner}")`}
else return {}
}
getFriendLinks(f: Friend): { link: string, icon: string }[]
{
return Object.entries(f).filter(pair => !excludes.has(pair[0].toString()))
.map(pair => {
return { link: pair[1], icon: fab.includes(pair[0]) ? `fab fa-${pair[0]}` :
pair[0] in icons ? icons[pair[0]] : pair[0] }
})
}
const bgStyle = (f: Friend) => {
if (f.banner) return {'background-image': `url("${f.banner}")`}
return {}
}
const getFriendLinks = (f: Friend): { link: string, icon: string }[] => {
return Object.entries(f)
.filter(([key, value]) => !excludes.has(key) && typeof value === 'string')
.map(([key, value]) => ({
link: value,
icon: fab.includes(key) ? `fab fa-${key}` : (key in icons ? icons[key] : key)
}))
}
onMounted(async () => {
friends.value = await (await fetch(`${hosts.content}/content/generated/friends/friends.json`)).json()
friends.value.forEach(f => {
if (!f.avatar.startsWith('http')) f.avatar = `${hosts.content}/${f.avatar}`
if (f.banner && !f.banner.startsWith('http')) f.banner = `${hosts.content}/${f.banner}`
})
friends.value = shuffle(friends.value)
})
</script>
<style lang="sass" scoped>
@import "src/css/colors"
@import "src/css/responsive"
@use "../../css/colors"
@use "../../css/responsive"
$card-min-width: 320px
@@ -126,7 +119,7 @@ $card-min-width: 320px
flex: 1
a
color: $color-text-main
color: colors.$color-text-main
a + a
margin-left: 10px
+27 -51
View File
@@ -23,37 +23,26 @@
</div>
</template>
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
<script setup lang="ts">
import {ref} from 'vue'
export enum Flavor
{
light,
normal,
salty
}
export interface MenuItem
{
interface MenuItem {
name: string
sub?: string
img?: string
recommend?: boolean
original?: boolean
id?: number
}
export interface MenuCategory
{
interface MenuCategory {
cat: string
subtitle?: string
items: MenuItem[]
column?: number
}
export const menu: MenuCategory[] = [
const menu: MenuCategory[] = [
{
cat: '🍖 猪肉',
items: [
@@ -141,43 +130,30 @@ export const menu: MenuCategory[] = [
},
]
@Options({components: {}})
export default class Menu extends Vue
{
max_cols = 2
cols: MenuCategory[][] = new Array(this.max_cols)
const maxCols = 2
const cols = ref<MenuCategory[][]>(Array.from({length: maxCols}, () => []))
created()
{
// Calculate menu layout
const tmp = Array.from(menu)
tmp.sort((a, b) => b.items.length - a.items.length)
const tmp = Array.from(menu)
tmp.sort((a, b) => b.items.length - a.items.length)
// Two columns
let col_counts = new Array(this.max_cols).fill(0)
for (const cat of tmp)
{
// Get column index with minimal item count
let col = col_counts.indexOf(Math.min(...col_counts))
cat.column = col
col_counts[col] += cat.items.length
}
// Separate arrays by column
for (let i = 0; i < this.max_cols; i++)
this.cols[i] = menu.filter(it => it.column == i)
// Assign ID to each item
let id = 0
this.cols.forEach(col => col.forEach(cat => cat.items.forEach(it => it.id = id++)))
}
const colCounts = new Array(maxCols).fill(0)
for (const cat of tmp) {
const col = colCounts.indexOf(Math.min(...colCounts))
cat.column = col
colCounts[col] += cat.items.length
}
for (let i = 0; i < maxCols; i++) {
cols.value[i] = menu.filter(it => it.column == i)
}
let id = 0
cols.value.forEach(col => col.forEach(cat => cat.items.forEach(it => it.id = id++)))
</script>
<style lang="sass" scoped>
@import "src/css/colors"
@import "src/css/responsive"
@use "../../css/colors"
@use "../../css/responsive"
.columns
display: flex
@@ -191,6 +167,8 @@ export default class Menu extends Vue
white-space: nowrap
.category
margin-bottom: 1em
.cat
font-size: 1.2em
font-weight: bold
@@ -198,16 +176,14 @@ export default class Menu extends Vue
.subtitle
font-size: 0.8em
margin-bottom: 0.5em
color: $color-text-light
margin-bottom: 1em
color: colors.$color-text-light
.items
.sub
font-size: 0.7em
.item.recommend
color: $color-text-special
color: colors.$color-text-special
.item.original:after
content: '原创'
+1 -8
View File
@@ -4,14 +4,7 @@
</div>
</template>
<script lang="ts">
import {Options, Vue} from 'vue-class-component';
@Options({components: {}})
export default class Projects extends Vue
{
}
<script setup lang="ts">
</script>
<style lang="sass" scoped>
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig, presetWind3, presetAttributify, presetIcons, presetTypography, presetWebFonts, transformerDirectives } from "unocss";
export default defineConfig({
presets: [
presetWind3(),
presetAttributify(),
presetIcons(),
presetTypography(),
]
})
+7 -1
View File
@@ -1,12 +1,13 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import UnoCSS from 'unocss/vite';
import path from "path";
const src = path.resolve(__dirname, 'src')
export default defineConfig({
plugins: [
vue()
vue(), UnoCSS()
],
resolve: {
alias: {
@@ -16,5 +17,10 @@ export default defineConfig({
vue: "vue/dist/vue.esm-bundler.js"
},
dedupe: ['vue'],
},
server: {
watch: {
usePolling: true
}
}
});
-1776
View File
File diff suppressed because it is too large Load Diff