Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea9c12eda5 | |||
| f25f468c99 | |||
| 495e106e4c | |||
| e52ef61c8b | |||
| 51157cae08 | |||
| 9b5a613593 | |||
| ab474204ce | |||
| 6c776c8a2e | |||
| fafe05a792 | |||
| c7303bd5fd | |||
| 2f6272818d | |||
| 58d0cb99b8 | |||
| 84128c52a5 | |||
| 34b076244b | |||
| 07a5ba0f75 | |||
| 285fe5d2de | |||
| 13da185b18 | |||
| ba52340363 | |||
| b65fdc6f99 | |||
| 0fc709709b | |||
| 2fbb36b830 | |||
| 0d42bfba2b | |||
| 86fc1a365e | |||
| 2ff45e1068 | |||
| 835bccf288 | |||
| db1b257df4 | |||
| 799f8515ec | |||
| 22e15e6beb | |||
| 51682f81a8 | |||
| 6cd3484f7c | |||
| db5bcc66ec | |||
| a4f6e21af8 | |||
| 9339af97d5 | |||
| 26e19af0a3 | |||
| 7cc6a57b49 | |||
| 4fbe9f156b | |||
| ab6919ca28 | |||
| 4f128a6fb8 | |||
| 8b8c6db10f | |||
| fd07dd04f7 | |||
| 848a8ddf91 | |||
| 473a84b91e | |||
| b68031238f | |||
| d1525096da | |||
| 83ba8b9f93 | |||
| 56ca0ab655 | |||
| d613ac62aa | |||
| 6ebd786b7c | |||
| 747d725c43 | |||
| 731c08dbcb | |||
| 0143b1edc9 | |||
| ac9bea5fd2 | |||
| 58462de365 |
@@ -13,33 +13,18 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v5
|
||||||
- uses: actions/setup-node@v2.1.5
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
- name: Cache node modules
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/node_modules
|
|
||||||
key: cache-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
|
|
||||||
- name: Install and build
|
- name: Install and build
|
||||||
run: |
|
run: |
|
||||||
yarn install
|
bun install
|
||||||
# pass --base if CNAME is not used
|
bun run build
|
||||||
# npm run build -- --base=/${{ github.event.repository.name }}/
|
|
||||||
yarn build
|
|
||||||
|
|
||||||
# Enable Vue Router history mode with 404.html hack for Github Pages
|
# Enable Vue Router history mode with 404.html hack for Github Pages
|
||||||
cd dist
|
cd dist
|
||||||
ln -s index.html 404.html
|
ln -s index.html 404.html
|
||||||
- name: Deploy to github pages
|
- name: Deploy to github pages
|
||||||
uses: JamesIves/github-pages-deploy-action@4.1.0
|
uses: JamesIves/github-pages-deploy-action@v4
|
||||||
with:
|
with:
|
||||||
branch: gh-pages
|
branch: gh-pages
|
||||||
folder: dist
|
folder: dist
|
||||||
|
|
||||||
- name: Publish deployed code
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: published
|
|
||||||
path: dist
|
|
||||||
|
|||||||
Binary file not shown.
Vendored
+894
File diff suppressed because one or more lines are too long
+23
-27
@@ -8,35 +8,31 @@
|
|||||||
"build": "vue-tsc --noEmit && vite build"
|
"build": "vue-tsc --noEmit && vite build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.3.0",
|
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||||
"animsition": "^4.0.2",
|
"animsition": "^4.0.2",
|
||||||
"core-js": "^3.29.0",
|
"core-js": "^3.37.1",
|
||||||
"emoji-regex": "^10.2.1",
|
"emoji-regex": "^10.3.0",
|
||||||
"linkify-urls": "^4.0.0",
|
"linkify-urls": "^4.1.0",
|
||||||
"marked": "^4.2.12",
|
"lxgw-wenkai-webfont": "^1.7.0",
|
||||||
"meshline": "^3.1.6",
|
"marked": "^4.3.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.30.1",
|
||||||
"tg-blog": "^1.1.0",
|
"tg-blog": "^1.1.7",
|
||||||
"three": "^0.150.1",
|
"vue": "^3.4.35",
|
||||||
"vue": "^3.2.47",
|
"vue-i18n": "^11.1.12",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue-router": "^4.4.0"
|
||||||
"vue-router": "^4.1.6",
|
|
||||||
"vue3-colorpicker": "^2.1.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jquery": "^3.5.16",
|
"@types/jquery": "^3.5.30",
|
||||||
"@types/jqueryui": "^1.12.16",
|
"@types/jqueryui": "^1.12.23",
|
||||||
"@types/marked": "^4.0.7",
|
"@types/marked": "^4.3.2",
|
||||||
"@types/node": "^18.14.5",
|
"@types/node": "^24.9.1",
|
||||||
"@types/three": "^0.149.0",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vitejs/plugin-vue": "^4.0.0",
|
"eslint": "^9.8.0",
|
||||||
"eslint": "^8.35.0",
|
"sass": "^1.77.8",
|
||||||
"sass": "^1.58.3",
|
"tslib": "^2.6.3",
|
||||||
"tslib": "^2.5.0",
|
"typescript": "^5.5.4",
|
||||||
"typescript": "^4.9.4",
|
"unocss": "^66.5.4",
|
||||||
"vite": "^4.1.4",
|
"vite": "^7.1.12",
|
||||||
"vue-class-component": "^8.0.0-rc.1",
|
"vue-tsc": "3.1.1"
|
||||||
"vue-property-decorator": "^10.0.0-rc.3",
|
|
||||||
"vue-tsc": "^1.0.24"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
+85
-80
@@ -1,21 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="nav" class="fbox-v"
|
<div id="nav" class="fbox-v"
|
||||||
:class="(currentRoute) + ' ' + (menuOpen ? 'open' : '')"
|
: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="menu" @click="showMenu"><i class="fas fa-bars"></i></div>
|
||||||
|
|
||||||
<div id="items" class="fbox-v">
|
<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>
|
<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>
|
<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>
|
<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>
|
<div class="dot">·</div>
|
||||||
<router-link class="router-link" ref="home" to="/">
|
<router-link class="router-link" :ref="setNavRef('about')" to="/about">{{ $t('nav.about') }}</router-link>
|
||||||
<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 class="dot">·</div>
|
<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>
|
||||||
|
|
||||||
<div id="nav-bookmark" ref="bookmark" :style="bookmarkCss"></div>
|
<div id="nav-bookmark" ref="bookmark" :style="bookmarkCss"></div>
|
||||||
@@ -27,89 +28,93 @@
|
|||||||
<router-view/>
|
<router-view/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {Options, Vue} from "vue-class-component";
|
import {nextTick, onMounted, onUnmounted, ref, ComponentPublicInstance} from 'vue';
|
||||||
import router from "@/scripts/router";
|
import router from "@/scripts/router";
|
||||||
import {RouteLocationNormalized} from "vue-router";
|
import {RouteLocationNormalized, RouteLocationRaw, useRoute} from "vue-router";
|
||||||
|
|
||||||
@Options({components: {}})
|
const route = useRoute()
|
||||||
export default class App extends Vue
|
|
||||||
{
|
|
||||||
currentRoute = ''
|
|
||||||
currentLink: Element = null as never as Element
|
|
||||||
bookmarkCss = ""
|
|
||||||
bookmarkUpdateIntervalId!: number
|
|
||||||
lastTop = 0
|
|
||||||
|
|
||||||
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
|
const navRefs = ref<Record<string, Element | ComponentPublicInstance | null>>({})
|
||||||
{
|
|
||||||
this.menuOpen = !this.menuOpen
|
|
||||||
|
|
||||||
// Auto close
|
const showMenu = (): void => {
|
||||||
if (this.menuOpen) setTimeout(() => this.menuOpen = false, 2000)
|
menuOpen.value = !menuOpen.value
|
||||||
}
|
|
||||||
|
|
||||||
updateBookmark(to: RouteLocationNormalized): void
|
if (menuOpen.value) setTimeout(() => menuOpen.value = false, 2000)
|
||||||
{
|
|
||||||
// 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;`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
@import "css/global"
|
@use "css/global"
|
||||||
@import "css/animations"
|
@use "css/animations"
|
||||||
|
|
||||||
#nav
|
#nav
|
||||||
position: fixed
|
position: fixed
|
||||||
|
|||||||
@@ -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')
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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) }
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export default interface IUpdatable
|
|
||||||
{
|
|
||||||
update: (dt: number) => void
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="index index-tags" v-if="mode === 'tags'">
|
<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>
|
@click="e => clickTag(e, t)">{{ t[0] }} ({{ t[1] }})</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="index index-categories" v-else>
|
<div class="index index-categories" v-else>
|
||||||
@@ -9,38 +9,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {Options, Vue} from 'vue-class-component';
|
|
||||||
import {staticMeta} from '@/views/Blog.vue'
|
|
||||||
import Tag from "@/components/Tag.vue";
|
import Tag from "@/components/Tag.vue";
|
||||||
import {Prop} from "vue-property-decorator";
|
|
||||||
import {pushQuery} from "@/scripts/router";
|
import {pushQuery} from "@/scripts/router";
|
||||||
import {BlogMeta} from "@/scripts/models";
|
import {BlogMeta} from "@/scripts/models";
|
||||||
|
import {globals} from "@/scripts/global";
|
||||||
|
|
||||||
@Options({components: {Tag}})
|
withDefaults(defineProps<{ mode?: 'tags' | 'categories' }>(), {
|
||||||
export default class BlogIndexLinks extends Vue
|
mode: 'tags'
|
||||||
{
|
})
|
||||||
@Prop({default: 'tags'}) mode: 'tags' | 'categories' = 'tags'
|
|
||||||
|
|
||||||
meta: BlogMeta = staticMeta
|
const meta: BlogMeta = globals.staticMeta
|
||||||
|
|
||||||
clickCat(e: MouseEvent, cat: [string, number]): void
|
const clickCat = (e: MouseEvent, cat: [string, number]): void => {
|
||||||
{
|
e.stopPropagation()
|
||||||
e.stopPropagation()
|
pushQuery({category: cat[0], tag: null})
|
||||||
pushQuery({category: cat[0], tag: null})
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
const clickTag = (e: MouseEvent, tag: [string, number]): void => {
|
||||||
clickTag(e: MouseEvent, tag: [string, number]): void
|
e.stopPropagation()
|
||||||
{
|
pushQuery({tag: tag[0], category: null})
|
||||||
e.stopPropagation()
|
|
||||||
pushQuery({tag: tag[0], category: null})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
@import 'src/css/colors'
|
@use '../css/colors'
|
||||||
|
|
||||||
.index
|
.index
|
||||||
*
|
*
|
||||||
@@ -56,7 +49,7 @@ export default class BlogIndexLinks extends Vue
|
|||||||
|
|
||||||
.index-categories
|
.index-categories
|
||||||
font-size: 0.8em
|
font-size: 0.8em
|
||||||
color: $color-text-special
|
color: colors.$color-text-special
|
||||||
|
|
||||||
*
|
*
|
||||||
text-decoration: underline
|
text-decoration: underline
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="BlogPostPreview" class="card" :class="elClass">
|
<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="titles" class="unselectable clickable" @click="clickTitle">
|
||||||
<div id="date">{{ date.format('YYYY-MM-DD') }}</div>
|
<div id="date">{{ date.format('YYYY-MM-DD') }}</div>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="content">
|
<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">
|
<div id="text" class="markdown-content">
|
||||||
<Dynamic :template="content"></Dynamic>
|
<Dynamic :template="content"></Dynamic>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,13 +106,12 @@ const elClass = computed(() =>
|
|||||||
return classes
|
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 date = moment(p.meta.date)
|
||||||
const image = p.meta.title_image ? hosts.content + '/' + p.meta.title_image : null
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
@import 'src/css/colors'
|
@use '../css/colors'
|
||||||
|
|
||||||
#BlogPostPreview
|
#BlogPostPreview
|
||||||
text-align: left
|
text-align: left
|
||||||
@@ -122,7 +121,7 @@ const image = p.meta.title_image ? hosts.content + '/' + p.meta.title_image : nu
|
|||||||
|
|
||||||
#date
|
#date
|
||||||
font-size: 0.7em
|
font-size: 0.7em
|
||||||
color: $color-text-light
|
color: colors.$color-text-light
|
||||||
|
|
||||||
> * + *, #content > * + *
|
> * + *, #content > * + *
|
||||||
padding-top: 10px
|
padding-top: 10px
|
||||||
@@ -151,7 +150,7 @@ const image = p.meta.title_image ? hosts.content + '/' + p.meta.title_image : nu
|
|||||||
|
|
||||||
#subtitle
|
#subtitle
|
||||||
font-size: 0.8em
|
font-size: 0.8em
|
||||||
color: $color-text-light
|
color: colors.$color-text-light
|
||||||
|
|
||||||
img
|
img
|
||||||
$margin: 10px
|
$margin: 10px
|
||||||
@@ -172,7 +171,7 @@ const image = p.meta.title_image ? hosts.content + '/' + p.meta.title_image : nu
|
|||||||
#expand
|
#expand
|
||||||
font-size: 0.8em
|
font-size: 0.8em
|
||||||
padding-top: 10px
|
padding-top: 10px
|
||||||
color: $color-text-light
|
color: colors.$color-text-light
|
||||||
|
|
||||||
// Put image on top
|
// Put image on top
|
||||||
#BlogPostPreview.image-top
|
#BlogPostPreview.image-top
|
||||||
|
|||||||
+14
-20
@@ -7,30 +7,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {Options, Vue} from 'vue-class-component';
|
import {computed, onMounted} from 'vue'
|
||||||
import {Prop} from "vue-property-decorator";
|
|
||||||
import {$} from '@/scripts/constants';
|
import {$} from '@/scripts/constants';
|
||||||
|
|
||||||
@Options({components: {}})
|
const props = withDefaults(defineProps<{ title: string, active?: boolean }>(), {
|
||||||
export default class Collapse extends Vue
|
active: false
|
||||||
{
|
})
|
||||||
@Prop() title!: string
|
|
||||||
@Prop({default: false}) active = false
|
|
||||||
|
|
||||||
show = false
|
const displayTitle = computed((): string => decodeURIComponent(props.title))
|
||||||
|
|
||||||
get displayTitle(): string
|
onMounted((): void => {
|
||||||
{
|
$('.collapse').accordion({
|
||||||
return decodeURIComponent(this.title)
|
collapsible: true,
|
||||||
}
|
header: 'h3',
|
||||||
|
heightStyle: 'content',
|
||||||
mounted(): void
|
active: props.active
|
||||||
{
|
})
|
||||||
$('.collapse').accordion({collapsible: true, header: 'h3', heightStyle: 'content',
|
})
|
||||||
active: this.active})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
|
|||||||
@@ -7,41 +7,33 @@
|
|||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {Options, Vue} from 'vue-class-component';
|
import {computed} from 'vue'
|
||||||
import {Prop} from "vue-property-decorator";
|
|
||||||
|
|
||||||
@Options({components: {}})
|
const props = defineProps<{ table: {[id: string]: unknown} }>()
|
||||||
export default class MetaTable extends Vue
|
|
||||||
{
|
|
||||||
@Prop({required: true}) table!: {[id: string]: unknown}
|
|
||||||
|
|
||||||
get filteredTable(): {[id: string]: unknown}
|
const filteredTable = computed((): {[id: string]: unknown} => {
|
||||||
{
|
const t: {[id: string]: unknown} = {}
|
||||||
const t: {[id: string]: unknown} = {}
|
|
||||||
|
|
||||||
Object.keys(this.table).forEach(k => {
|
Object.keys(props.table).forEach(k => {
|
||||||
// Ignore empty
|
if (!props.table[k]) return
|
||||||
if (!this.table[k]) return
|
|
||||||
|
|
||||||
// Convert to sentence case (https://stackoverflow.com/a/7225450/7346633)
|
let newK = k.replace(/([A-Z])/g, " $1")
|
||||||
let newK = k.replace(/([A-Z])/g, " $1")
|
newK = newK.charAt(0).toUpperCase() + newK.slice(1)
|
||||||
newK = newK.charAt(0).toUpperCase() + newK.slice(1)
|
t[newK] = props.table[k]
|
||||||
t[newK] = this.table[k]
|
})
|
||||||
})
|
|
||||||
|
|
||||||
return t
|
return t
|
||||||
}
|
})
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
@import "src/css/colors"
|
@use "../css/colors"
|
||||||
|
|
||||||
.meta
|
.meta
|
||||||
td:first-child
|
td:first-child
|
||||||
text-align: right
|
text-align: right
|
||||||
color: $color-text-light
|
color: colors.$color-text-light
|
||||||
|
|
||||||
td:last-child
|
td:last-child
|
||||||
display: inline-block
|
display: inline-block
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ const props = defineProps({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
@import src/css/colors
|
@use "../css/colors"
|
||||||
|
|
||||||
$tag-height: 20px
|
$tag-height: 20px
|
||||||
$tag-color: $color-bg-6
|
$tag-color: colors.$color-bg-6
|
||||||
$text-color: $color-text-light
|
$text-color: colors.$color-text-light
|
||||||
|
|
||||||
$padding: calc($tag-height / 2)
|
$padding: calc($tag-height / 2)
|
||||||
$triangle-width: calc($tag-height / 2) * 0.8
|
$triangle-width: calc($tag-height / 2) * 0.8
|
||||||
|
|||||||
@@ -16,16 +16,15 @@
|
|||||||
<div id="attachments" v-if="item.attachments.length !== 0">
|
<div id="attachments" v-if="item.attachments.length !== 0">
|
||||||
<div class="label">Attachments</div>
|
<div class="label">Attachments</div>
|
||||||
<div class="content" v-for="a of item.attachments" :key="a.data.key">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {Options, Vue} from 'vue-class-component';
|
import {computed, onMounted} from 'vue'
|
||||||
import {Prop} from "vue-property-decorator";
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import MetaTable from "@/components/MetaTable.vue";
|
import MetaTable from "@/components/MetaTable.vue";
|
||||||
import {capitalize} from "@/scripts/utils";
|
import {capitalize} from "@/scripts/utils";
|
||||||
@@ -33,34 +32,32 @@ import linkifyUrls from "linkify-urls";
|
|||||||
import {$} from '@/scripts/constants';
|
import {$} from '@/scripts/constants';
|
||||||
import {ZoteroData, ZoteroItem} from "@/scripts/zotero";
|
import {ZoteroData, ZoteroItem} from "@/scripts/zotero";
|
||||||
|
|
||||||
@Options({components: {MetaTable}})
|
const props = defineProps<{ item: ZoteroItem }>()
|
||||||
export default class ZoteroPublicationView extends Vue
|
|
||||||
{
|
|
||||||
@Prop({required: true}) item!: ZoteroItem
|
|
||||||
|
|
||||||
get d(): ZoteroData { return this.item.data }
|
const d = computed((): ZoteroData => props.item.data)
|
||||||
get date(): moment.Moment { return moment(this.item.meta.parsedDate) }
|
const date = computed((): moment.Moment => moment(props.item.meta.parsedDate))
|
||||||
get authors(): string { return this.d.creators.map(it => it.firstName + ' ' + it.lastName).join(' & ') }
|
const authors = computed((): string => d.value.creators.map(it => it.firstName + ' ' + it.lastName).join(' & '))
|
||||||
|
|
||||||
get tableData(): {[id: string]: unknown}
|
const tableData = computed((): {[id: string]: unknown} => {
|
||||||
{
|
const t: {[id: string]: unknown} = {...d.value}
|
||||||
const t: {[id: string]: unknown} = {...this.d}
|
t.creators = authors.value
|
||||||
t.creators = this.authors
|
delete t.key
|
||||||
delete t.key
|
delete t.version
|
||||||
delete t.version
|
delete t.title
|
||||||
delete t.title
|
delete t.abstractNote
|
||||||
delete t.abstractNote
|
if (t.itemType) t.itemType = capitalize(t.itemType as string)
|
||||||
if (t.itemType) t.itemType = capitalize(t.itemType as string)
|
if (t.url) t.url = linkifyUrls(t.url as string)
|
||||||
if (t.url) t.url = linkifyUrls(t.url as string)
|
return t
|
||||||
return t
|
})
|
||||||
}
|
|
||||||
|
|
||||||
mounted(): void
|
onMounted((): void => {
|
||||||
{
|
$('.publication').accordion({
|
||||||
$('.publication').accordion({collapsible: true, header: 'div.header', active: false,
|
collapsible: true,
|
||||||
heightStyle: "content"})
|
header: 'div.header',
|
||||||
}
|
active: false,
|
||||||
}
|
heightStyle: 'content'
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
@@ -77,6 +74,7 @@ export default class ZoteroPublicationView extends Vue
|
|||||||
|
|
||||||
#details
|
#details
|
||||||
padding-left: calc(1.6em + 6px)
|
padding-left: calc(1.6em + 6px)
|
||||||
|
font-size: 0.8em
|
||||||
|
|
||||||
> div
|
> div
|
||||||
margin-bottom: 1em
|
margin-bottom: 1em
|
||||||
@@ -87,8 +85,6 @@ export default class ZoteroPublicationView extends Vue
|
|||||||
.label
|
.label
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
|
|
||||||
font-size: 0.8em
|
|
||||||
|
|
||||||
.header
|
.header
|
||||||
align-items: center
|
align-items: center
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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
@@ -1,7 +1,9 @@
|
|||||||
@font-face
|
//@font-face
|
||||||
font-family: 'WenKai'
|
// font-family: 'WenKai'
|
||||||
src: url("/fonts/LXGWWenKai-Regular.woff2") format('woff2'), url("/fonts/LXGWWenKai-Regular.woff") format('woff')
|
// src: url("/fonts/LXGWWenKai-Regular.woff2") format('woff2'), url("/fonts/LXGWWenKai-Regular.woff") format('woff')
|
||||||
|
@import 'lxgw-wenkai-webfont/style.css'
|
||||||
|
|
||||||
.font-custom
|
.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
|
//line-height: 1.3em
|
||||||
|
|||||||
+31
-9
@@ -1,5 +1,5 @@
|
|||||||
@import colors
|
@use "colors"
|
||||||
@import font
|
@use "font"
|
||||||
|
|
||||||
// Google Fonts
|
// Google Fonts
|
||||||
// TODO: Localize
|
// TODO: Localize
|
||||||
@@ -119,7 +119,7 @@ body
|
|||||||
-webkit-font-smoothing: antialiased
|
-webkit-font-smoothing: antialiased
|
||||||
-moz-osx-font-smoothing: grayscale
|
-moz-osx-font-smoothing: grayscale
|
||||||
text-align: center
|
text-align: center
|
||||||
color: $color-text-main
|
color: colors.$color-text-main
|
||||||
|
|
||||||
// Max width and center
|
// Max width and center
|
||||||
max-width: 900px
|
max-width: 900px
|
||||||
@@ -170,30 +170,52 @@ html
|
|||||||
text-justify: inter-word
|
text-justify: inter-word
|
||||||
|
|
||||||
a
|
a
|
||||||
color: $color-text-special
|
color: colors.$color-text-special
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
|
|
||||||
h1, h2
|
h1, h2
|
||||||
border-bottom: 1px solid $color-text-special
|
border-bottom: 1px solid colors.$color-text-special
|
||||||
font-size: 1.5em
|
font-size: 1.5em
|
||||||
margin-top: 1em
|
margin-top: 1em
|
||||||
|
|
||||||
h1, h2
|
|
||||||
line-height: 1.3
|
line-height: 1.3
|
||||||
margin-bottom: 0.25em
|
margin-bottom: 0.25em
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
|
h2
|
||||||
|
font-size: 1.3em
|
||||||
|
|
||||||
h3
|
h3
|
||||||
font-size: 1.2em
|
font-size: 1.2em
|
||||||
|
|
||||||
p
|
p
|
||||||
font-size: 0.9em
|
font-size: 1em
|
||||||
margin: 0.5em 0
|
margin: 0.7em 0
|
||||||
line-height: 1.6
|
line-height: 1.6
|
||||||
|
|
||||||
p:last-child
|
p:last-child
|
||||||
margin-bottom: 0
|
margin-bottom: 0
|
||||||
|
|
||||||
li
|
li
|
||||||
font-size: 0.9em
|
font-size: 1em
|
||||||
margin-bottom: 0.25em
|
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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "src/css/colors"
|
@use "../css/colors"
|
||||||
|
|
||||||
$width: 600px
|
$width: 600px
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ $width: 600px
|
|||||||
margin-bottom: 0
|
margin-bottom: 0
|
||||||
|
|
||||||
.subtitle
|
.subtitle
|
||||||
color: $color-text-light
|
color: colors.$color-text-light
|
||||||
|
|
||||||
|
|
||||||
// Phone layout
|
// Phone layout
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '@fortawesome/fontawesome-free/css/all.min.css'
|
|||||||
import Collapse from "@/components/Collapse.vue"
|
import Collapse from "@/components/Collapse.vue"
|
||||||
import BlogIndex from "@/components/BlogIndex.vue";
|
import BlogIndex from "@/components/BlogIndex.vue";
|
||||||
import Tag from "@/components/Tag.vue";
|
import Tag from "@/components/Tag.vue";
|
||||||
|
import 'virtual:uno.css'
|
||||||
|
|
||||||
const app = createApp(App).use(router).use(i18n)
|
const app = createApp(App).use(router).use(i18n)
|
||||||
.component('Collapse', Collapse)
|
.component('Collapse', Collapse)
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {BlogMeta} from "@/scripts/models";
|
||||||
|
|
||||||
|
export let globals = {
|
||||||
|
staticMeta: {tags: [], categories: [], posts: []} as BlogMeta,
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@ export const messages = {
|
|||||||
blog: '记事本',
|
blog: '记事本',
|
||||||
life: '生活',
|
life: '生活',
|
||||||
projects: 'Projects',
|
projects: 'Projects',
|
||||||
others: '更多'
|
others: '更多',
|
||||||
|
photo: '相册'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
|
|||||||
+12
-12
@@ -8,12 +8,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
meta: {title: 'Home', nav: true},
|
meta: {title: 'Home', nav: true},
|
||||||
component: Home
|
component: Home
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/new-home',
|
|
||||||
name: 'New Home',
|
|
||||||
meta: {title: 'Home'},
|
|
||||||
component: () => import('../views/NewHome.vue')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/about',
|
path: '/about',
|
||||||
name: 'About',
|
name: 'About',
|
||||||
@@ -33,6 +27,18 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
component: () => import('../views/Blog.vue'),
|
component: () => import('../views/Blog.vue'),
|
||||||
props: route => (route.query)
|
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',
|
path: '/others',
|
||||||
name: 'Others',
|
name: 'Others',
|
||||||
@@ -57,12 +63,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
meta: {title: '我做的', navBookmark: 'Others'},
|
meta: {title: '我做的', navBookmark: 'Others'},
|
||||||
component: () => import('../views/others/Projects.vue')
|
component: () => import('../views/others/Projects.vue')
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/color',
|
|
||||||
name: 'ColorPicker',
|
|
||||||
meta: {title: 'Color Picker'},
|
|
||||||
component: () => import('../components/color/ColorPickerTest.vue')
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import {Vue} from "vue-class-component";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as python's range
|
* 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}
|
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 = {}
|
||||||
_keybinds: Keybinds = {}
|
_keybinds: Keybinds = {}
|
||||||
|
|||||||
+19
-30
@@ -9,8 +9,8 @@
|
|||||||
<Loading v-else></Loading>
|
<Loading v-else></Loading>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {Options, Vue} from 'vue-class-component';
|
import {onMounted, ref} from 'vue'
|
||||||
import {marked} from 'marked';
|
import {marked} from 'marked';
|
||||||
import emojiRegex from 'emoji-regex';
|
import emojiRegex from 'emoji-regex';
|
||||||
import {parseExtensions} from '@/scripts/extended_markdown'
|
import {parseExtensions} from '@/scripts/extended_markdown'
|
||||||
@@ -19,39 +19,28 @@ import {hosts} from "@/scripts/constants";
|
|||||||
import Loading from "@/components/Loading.vue";
|
import Loading from "@/components/Loading.vue";
|
||||||
import {ZoteroAttachment, ZoteroItem} from "@/scripts/zotero";
|
import {ZoteroAttachment, ZoteroItem} from "@/scripts/zotero";
|
||||||
|
|
||||||
@Options({components: {Loading, ZoteroPublication}})
|
const html = ref("")
|
||||||
export default class About extends Vue
|
const publications = ref<ZoteroItem[]>([])
|
||||||
{
|
|
||||||
html = ""
|
|
||||||
publications: ZoteroItem[] = []
|
|
||||||
|
|
||||||
mounted(): void
|
onMounted((): void => {
|
||||||
{
|
fetch(`${hosts.content}/README.md`).then(it => it.text())
|
||||||
// Fetch readme
|
.then(it => html.value = marked(parseExtensions(it.replace(emojiRegex(), (emoji) => {
|
||||||
fetch(`${hosts.content}/README.md`).then(it => it.text())
|
return `<span class="emoji">${emoji}</span>`
|
||||||
.then(it => this.html = marked(parseExtensions(it.replace(emojiRegex(), (emoji) => {
|
}))))
|
||||||
return `<span class="emoji">${emoji}</span>`
|
|
||||||
}))))
|
|
||||||
|
|
||||||
// Fetch publications from zotero
|
fetch(`${hosts.api}/zotero.json`)
|
||||||
fetch(`${hosts.api}/zotero.json`)
|
.then(it => it.json()).then((it: ZoteroItem[]) => {
|
||||||
.then(it => it.json()).then(it =>
|
publications.value = it
|
||||||
{
|
let files: ZoteroAttachment[] = it as unknown as ZoteroAttachment[]
|
||||||
// Filter out publications and attachments
|
files = files.filter(file => file.data.itemType === 'attachment')
|
||||||
this.publications = it
|
publications.value = publications.value.filter(pub => pub.data.itemType !== 'attachment')
|
||||||
let files: ZoteroAttachment[] = it
|
publications.value.forEach(pub => pub.attachments = files.filter(a => a.data.parentItem == pub.key))
|
||||||
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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
@import "../css/colors"
|
@use "../css/colors"
|
||||||
|
|
||||||
#About
|
#About
|
||||||
width: min(600px, 80vw)
|
width: min(600px, 80vw)
|
||||||
|
|||||||
+36
-39
@@ -10,64 +10,61 @@
|
|||||||
<span v-if="category">📂{{category}}</span>
|
<span v-if="category">📂{{category}}</span>
|
||||||
<span class="no-after" v-if="post && activePost">{{activePost.title}}</span>
|
<span class="no-after" v-if="post && activePost">{{activePost.title}}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<Loading v-else></Loading>
|
<Loading v-else></Loading>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {Options, Vue} from 'vue-class-component';
|
|
||||||
import BlogPostPreview from "@/components/BlogPost.vue";
|
import BlogPostPreview from "@/components/BlogPost.vue";
|
||||||
import {hosts} from "@/scripts/constants";
|
|
||||||
import {Prop} from "vue-property-decorator";
|
|
||||||
import Loading from "@/components/Loading.vue";
|
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}})
|
const p = defineProps<{
|
||||||
export default class Blog extends Vue
|
post?: string,
|
||||||
{
|
category?: string,
|
||||||
@Prop() post?: string
|
tag?: string
|
||||||
@Prop() category?: string
|
}>()
|
||||||
@Prop() tag?: string
|
|
||||||
|
|
||||||
meta: BlogMeta = {tags: [], categories: [], posts: []}
|
let meta: Ref<BlogMeta> = ref({tags: [], categories: [], posts: []})
|
||||||
|
|
||||||
mounted(): void
|
onMounted(() => {
|
||||||
{
|
fetch(`${hosts.content}/content/generated/metas.json`).then(it => it.json()).then(it => {
|
||||||
fetch(`${hosts.content}/content/generated/metas.json`).then(it => it.json()).then(it => {
|
meta.value = it
|
||||||
this.meta = it
|
globals.staticMeta = it
|
||||||
staticMeta = it
|
})
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
get activePost(): BlogPost | null
|
const filteredPosts = computed(() => {
|
||||||
{
|
const posts = meta.value.posts.filter(it => it.pinned || (p.tag ? it.tags.includes(p.tag) :
|
||||||
const p = this.filteredPosts
|
p.category ? it.category == p.category : true))
|
||||||
if (p.length == 0) return null
|
|
||||||
return this.post ? p.filter(it => it.url_name == this.post)[0] : p[0].pinned ? p[0] : null
|
|
||||||
}
|
|
||||||
|
|
||||||
get filteredPosts(): BlogPost[]
|
// Put pinned posts on top
|
||||||
{
|
posts.sort((a, b) => (b.pinned ?? 0) - (a.pinned ?? 0))
|
||||||
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
|
return posts
|
||||||
posts.sort((a, b) => (b.pinned ?? 0) - (a.pinned ?? 0))
|
})
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
@import "src/css/colors"
|
@use "../css/colors"
|
||||||
@import "src/css/responsive"
|
@use "../css/responsive"
|
||||||
|
|
||||||
#breadcrumb
|
#breadcrumb
|
||||||
color: $color-text-light
|
color: colors.$color-text-light
|
||||||
margin-bottom: 20px
|
margin-bottom: 20px
|
||||||
|
|
||||||
span:not(.no-after):after
|
span:not(.no-after):after
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="Home" class="fbox-center f-grow1">
|
<div id="Home" class="fbox-center f-grow1">
|
||||||
<div id="box">
|
<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 class="font-script-en bold">Road Less Traveled</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-2
@@ -20,8 +20,8 @@ import 'tg-blog/dist/style.css'
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
@import "src/css/colors"
|
@use "../css/colors"
|
||||||
@import "src/css/responsive"
|
@use "../css/responsive"
|
||||||
|
|
||||||
.title
|
.title
|
||||||
text-align: left
|
text-align: left
|
||||||
|
|||||||
@@ -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
@@ -8,25 +8,15 @@
|
|||||||
<router-link class="rlink" to="/kitchen-menu">🍳 小桂桂的私房菜 菜单</router-link>
|
<router-link class="rlink" to="/kitchen-menu">🍳 小桂桂的私房菜 菜单</router-link>
|
||||||
<router-link class="rlink" to="/friends">🎎 朋友们</router-link>
|
<router-link class="rlink" to="/friends">🎎 朋友们</router-link>
|
||||||
<a href="https://cast.hydev.org">📹 公开直播间</a>
|
<a href="https://cast.hydev.org">📹 公开直播间</a>
|
||||||
<a href="http://status.hydev.org/">🔌 看看服务器们过得怎么样</a>
|
<a href="https://status.hydev.org/">🔌 看看服务器们过得怎么样</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import {Options, Vue} from 'vue-class-component';
|
|
||||||
|
|
||||||
@Options({})
|
|
||||||
export default class Others extends Vue
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
@import "src/css/global"
|
@use "../css/global"
|
||||||
@import "src/css/colors"
|
@use "../css/colors"
|
||||||
@import "src/css/responsive"
|
@use "../css/responsive"
|
||||||
|
|
||||||
.links
|
.links
|
||||||
display: flex
|
display: flex
|
||||||
@@ -38,7 +28,7 @@ a
|
|||||||
@extend .card
|
@extend .card
|
||||||
@extend .clickable
|
@extend .clickable
|
||||||
|
|
||||||
color: $color-text-main
|
color: colors.$color-text-main
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -23,61 +23,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {Options, Vue} from 'vue-class-component';
|
import {onMounted, ref} from 'vue'
|
||||||
import {fab, hosts} from "@/scripts/constants";
|
import {fab, hosts} from "@/scripts/constants";
|
||||||
import {shuffle} from "@/scripts/utils";
|
import {shuffle} from "@/scripts/utils";
|
||||||
|
|
||||||
export interface Friend {
|
interface Friend {
|
||||||
name: string
|
name: string
|
||||||
avatar: string
|
avatar: string
|
||||||
banner: string
|
banner: string
|
||||||
|
|
||||||
desc?: string
|
desc?: string
|
||||||
|
[id: string]: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const excludes = new Set(["name", "avatar", "banner", "desc"])
|
const excludes = new Set(["name", "avatar", "banner", "desc"])
|
||||||
const icons = {
|
const icons: {[id: string]: string} = {
|
||||||
blog: 'fas fa-book'
|
blog: 'fas fa-book'
|
||||||
}
|
}
|
||||||
|
|
||||||
@Options({components: {}})
|
const friends = ref<Friend[]>([])
|
||||||
export default class Friends extends Vue
|
|
||||||
{
|
|
||||||
friends: Friend[] = []
|
|
||||||
|
|
||||||
async created()
|
const bgStyle = (f: Friend) => {
|
||||||
{
|
if (f.banner) return {'background-image': `url("${f.banner}")`}
|
||||||
this.friends = await (await fetch(`${hosts.content}/content/generated/friends/friends.json`)).json()
|
return {}
|
||||||
|
|
||||||
// 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 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>
|
</script>
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
@import "src/css/colors"
|
@use "../../css/colors"
|
||||||
@import "src/css/responsive"
|
@use "../../css/responsive"
|
||||||
|
|
||||||
$card-min-width: 320px
|
$card-min-width: 320px
|
||||||
|
|
||||||
@@ -126,7 +119,7 @@ $card-min-width: 320px
|
|||||||
flex: 1
|
flex: 1
|
||||||
|
|
||||||
a
|
a
|
||||||
color: $color-text-main
|
color: colors.$color-text-main
|
||||||
|
|
||||||
a + a
|
a + a
|
||||||
margin-left: 10px
|
margin-left: 10px
|
||||||
|
|||||||
+27
-51
@@ -23,37 +23,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {Options, Vue} from 'vue-class-component';
|
import {ref} from 'vue'
|
||||||
|
|
||||||
export enum Flavor
|
interface MenuItem {
|
||||||
{
|
|
||||||
light,
|
|
||||||
normal,
|
|
||||||
salty
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MenuItem
|
|
||||||
{
|
|
||||||
name: string
|
name: string
|
||||||
sub?: string
|
sub?: string
|
||||||
img?: string
|
img?: string
|
||||||
recommend?: boolean
|
recommend?: boolean
|
||||||
original?: boolean
|
original?: boolean
|
||||||
|
|
||||||
id?: number
|
id?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuCategory
|
interface MenuCategory {
|
||||||
{
|
|
||||||
cat: string
|
cat: string
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
items: MenuItem[]
|
items: MenuItem[]
|
||||||
|
|
||||||
column?: number
|
column?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const menu: MenuCategory[] = [
|
const menu: MenuCategory[] = [
|
||||||
{
|
{
|
||||||
cat: '🍖 猪肉',
|
cat: '🍖 猪肉',
|
||||||
items: [
|
items: [
|
||||||
@@ -141,43 +130,30 @@ export const menu: MenuCategory[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@Options({components: {}})
|
const maxCols = 2
|
||||||
export default class Menu extends Vue
|
const cols = ref<MenuCategory[][]>(Array.from({length: maxCols}, () => []))
|
||||||
{
|
|
||||||
max_cols = 2
|
|
||||||
cols: MenuCategory[][] = new Array(this.max_cols)
|
|
||||||
|
|
||||||
created()
|
const tmp = Array.from(menu)
|
||||||
{
|
tmp.sort((a, b) => b.items.length - a.items.length)
|
||||||
// Calculate menu layout
|
|
||||||
const tmp = Array.from(menu)
|
|
||||||
tmp.sort((a, b) => b.items.length - a.items.length)
|
|
||||||
|
|
||||||
// Two columns
|
const colCounts = new Array(maxCols).fill(0)
|
||||||
let col_counts = new Array(this.max_cols).fill(0)
|
for (const cat of tmp) {
|
||||||
for (const cat of tmp)
|
const col = colCounts.indexOf(Math.min(...colCounts))
|
||||||
{
|
cat.column = col
|
||||||
// Get column index with minimal item count
|
colCounts[col] += cat.items.length
|
||||||
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++)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
@import "src/css/colors"
|
@use "../../css/colors"
|
||||||
@import "src/css/responsive"
|
@use "../../css/responsive"
|
||||||
|
|
||||||
.columns
|
.columns
|
||||||
display: flex
|
display: flex
|
||||||
@@ -191,6 +167,8 @@ export default class Menu extends Vue
|
|||||||
white-space: nowrap
|
white-space: nowrap
|
||||||
|
|
||||||
.category
|
.category
|
||||||
|
margin-bottom: 1em
|
||||||
|
|
||||||
.cat
|
.cat
|
||||||
font-size: 1.2em
|
font-size: 1.2em
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
@@ -198,16 +176,14 @@ export default class Menu extends Vue
|
|||||||
.subtitle
|
.subtitle
|
||||||
font-size: 0.8em
|
font-size: 0.8em
|
||||||
margin-bottom: 0.5em
|
margin-bottom: 0.5em
|
||||||
color: $color-text-light
|
color: colors.$color-text-light
|
||||||
|
|
||||||
margin-bottom: 1em
|
|
||||||
|
|
||||||
.items
|
.items
|
||||||
.sub
|
.sub
|
||||||
font-size: 0.7em
|
font-size: 0.7em
|
||||||
|
|
||||||
.item.recommend
|
.item.recommend
|
||||||
color: $color-text-special
|
color: colors.$color-text-special
|
||||||
|
|
||||||
.item.original:after
|
.item.original:after
|
||||||
content: '原创'
|
content: '原创'
|
||||||
|
|||||||
@@ -4,14 +4,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {Options, Vue} from 'vue-class-component';
|
|
||||||
|
|
||||||
@Options({components: {}})
|
|
||||||
export default class Projects extends Vue
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig, presetWind3, presetAttributify, presetIcons, presetTypography, presetWebFonts, transformerDirectives } from "unocss";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
presets: [
|
||||||
|
presetWind3(),
|
||||||
|
presetAttributify(),
|
||||||
|
presetIcons(),
|
||||||
|
presetTypography(),
|
||||||
|
]
|
||||||
|
})
|
||||||
+7
-1
@@ -1,12 +1,13 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import UnoCSS from 'unocss/vite';
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
const src = path.resolve(__dirname, 'src')
|
const src = path.resolve(__dirname, 'src')
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue()
|
vue(), UnoCSS()
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
@@ -16,5 +17,10 @@ export default defineConfig({
|
|||||||
vue: "vue/dist/vue.esm-bundler.js"
|
vue: "vue/dist/vue.esm-bundler.js"
|
||||||
},
|
},
|
||||||
dedupe: ['vue'],
|
dedupe: ['vue'],
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
watch: {
|
||||||
|
usePolling: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user