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
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2.1.5
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/node_modules
|
||||
key: cache-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- name: Install and build
|
||||
run: |
|
||||
yarn install
|
||||
# pass --base if CNAME is not used
|
||||
# npm run build -- --base=/${{ github.event.repository.name }}/
|
||||
yarn build
|
||||
bun install
|
||||
bun run build
|
||||
|
||||
# Enable Vue Router history mode with 404.html hack for Github Pages
|
||||
cd dist
|
||||
ln -s index.html 404.html
|
||||
- name: Deploy to github pages
|
||||
uses: JamesIves/github-pages-deploy-action@4.1.0
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: dist
|
||||
|
||||
- name: Publish deployed code
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: published
|
||||
path: dist
|
||||
|
||||
Binary file not shown.
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"animsition": "^4.0.2",
|
||||
"core-js": "^3.29.0",
|
||||
"emoji-regex": "^10.2.1",
|
||||
"linkify-urls": "^4.0.0",
|
||||
"marked": "^4.2.12",
|
||||
"meshline": "^3.1.6",
|
||||
"moment": "^2.29.4",
|
||||
"tg-blog": "^1.1.0",
|
||||
"three": "^0.150.1",
|
||||
"vue": "^3.2.47",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue3-colorpicker": "^2.1.2"
|
||||
"core-js": "^3.37.1",
|
||||
"emoji-regex": "^10.3.0",
|
||||
"linkify-urls": "^4.1.0",
|
||||
"lxgw-wenkai-webfont": "^1.7.0",
|
||||
"marked": "^4.3.0",
|
||||
"moment": "^2.30.1",
|
||||
"tg-blog": "^1.1.7",
|
||||
"vue": "^3.4.35",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jquery": "^3.5.16",
|
||||
"@types/jqueryui": "^1.12.16",
|
||||
"@types/marked": "^4.0.7",
|
||||
"@types/node": "^18.14.5",
|
||||
"@types/three": "^0.149.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"eslint": "^8.35.0",
|
||||
"sass": "^1.58.3",
|
||||
"tslib": "^2.5.0",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.1.4",
|
||||
"vue-class-component": "^8.0.0-rc.1",
|
||||
"vue-property-decorator": "^10.0.0-rc.3",
|
||||
"vue-tsc": "^1.0.24"
|
||||
"@types/jquery": "^3.5.30",
|
||||
"@types/jqueryui": "^1.12.23",
|
||||
"@types/marked": "^4.3.2",
|
||||
"@types/node": "^24.9.1",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"eslint": "^9.8.0",
|
||||
"sass": "^1.77.8",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.4",
|
||||
"unocss": "^66.5.4",
|
||||
"vite": "^7.1.12",
|
||||
"vue-tsc": "3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
+85
-80
@@ -1,21 +1,22 @@
|
||||
<template>
|
||||
<div id="nav" class="fbox-v"
|
||||
:class="(currentRoute) + ' ' + (menuOpen ? 'open' : '')"
|
||||
v-if="currentRoute !== 'colorpicker'">
|
||||
v-if="currentRoute !== 'colorpicker'">
|
||||
<div id="menu" @click="showMenu"><i class="fas fa-bars"></i></div>
|
||||
|
||||
<div id="items" class="fbox-v">
|
||||
<router-link class="router-link" ref="others" to="/others">{{ $t('nav.others') }}</router-link>
|
||||
<router-link class="router-link" :ref="setNavRef('others')" to="/others">{{ $t('nav.others') }}</router-link>
|
||||
<div class="dot">·</div>
|
||||
<router-link class="router-link" ref="blog" to="/blog">{{ $t('nav.blog') }}</router-link>
|
||||
<router-link class="router-link" :ref="setNavRef('photo')" to="/photo">{{ $t('nav.photo') }}</router-link>
|
||||
<div class="dot">·</div>
|
||||
<router-link class="router-link" ref="life" to="/life">{{ $t('nav.life') }}</router-link>
|
||||
<router-link class="router-link" :ref="setNavRef('blog')" to="/blog">{{ $t('nav.blog') }}</router-link>
|
||||
<div class="dot">·</div>
|
||||
<router-link class="router-link" ref="about" to="/about">{{ $t('nav.about') }}</router-link>
|
||||
<router-link class="router-link" :ref="setNavRef('life')" to="/life">{{ $t('nav.life') }}</router-link>
|
||||
<div class="dot">·</div>
|
||||
<router-link class="router-link" ref="home" to="/">
|
||||
<svg focusable="false" data-prefix="fal" data-icon="house-night" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" class="svg-inline--fa fa-house-night fa-w-20"><path fill="currentColor" d="M112,224a111.5,111.5,0,0,0,87-41.45,20.51,20.51,0,0,0-19.75-33.08A59.2,59.2,0,0,1,138.84,39.85a20.3,20.3,0,0,0,10.07-21.27,20.26,20.26,0,0,0-16.47-16.7A136,136,0,0,0,112,0a112,112,0,0,0,0,224ZM97.78,33.27a91.21,91.21,0,0,0,54.47,147.9A80,80,0,1,1,97.78,33.27Zm97.15,35.51,39.72,16.56,16.56,39.72a5.33,5.33,0,0,0,9.55,0l16.56-39.72L317,68.78a5.33,5.33,0,0,0,0-9.54L277.32,42.68,260.76,3a5.33,5.33,0,0,0-9.55,0L234.65,42.68,194.93,59.24a5.34,5.34,0,0,0,0,9.54ZM157,379.24l-39.72-16.57L100.76,323a5.34,5.34,0,0,0-9.55,0L74.65,362.67,34.93,379.24a5.34,5.34,0,0,0,0,9.54l39.72,16.56,16.56,39.72a5.33,5.33,0,0,0,9.55,0l16.56-39.72L157,388.78a5.33,5.33,0,0,0,0-9.54Zm179-101.9v85.33A21.39,21.39,0,0,0,357.36,384h85.31A21.39,21.39,0,0,0,464,362.67V277.34A21.4,21.4,0,0,0,442.67,256H357.36A21.4,21.4,0,0,0,336,277.34ZM368,288H432v64H368Zm266.49,8L576,244.75V144a16,16,0,0,0-32,0v72.75L410.53,100a16,16,0,0,0-21.07,0l-224,196a16,16,0,0,0,21.07,24.09L224,287.28V464a48.05,48.05,0,0,0,48,48H528a48.06,48.06,0,0,0,48-48V287.28l37.46,32.78A16,16,0,0,0,634.53,296ZM544,464a16,16,0,0,1-16,16H272a16,16,0,0,1-16-16V264a15.94,15.94,0,0,0-.81-4L400,133.27l144,126Z" class=""></path></svg></router-link>
|
||||
<router-link class="router-link" :ref="setNavRef('about')" to="/about">{{ $t('nav.about') }}</router-link>
|
||||
<div class="dot">·</div>
|
||||
<router-link class="router-link" :ref="setNavRef('home')" to="/">
|
||||
<svg focusable="false" data-prefix="fal" data-icon="house-night" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" class="svg-inline--fa fa-house-night fa-w-20"><path fill="currentColor" d="M112,224a111.5,111.5,0,0,0,87-41.45,20.51,20.51,0,0,0-19.75-33.08A59.2,59.2,0,0,1,138.84,39.85a20.3,20.3,0,0,0,10.07-21.27,20.26,20.26,0,0,0-16.47-16.7A136,136,0,0,0,112,0a112,112,0,0,0,0,224ZM97.78,33.27a91.21,91.21,0,0,0,54.47,147.9A80,80,0,1,1,97.78,33.27Zm97.15,35.51,39.72,16.56,16.56,39.72a5.33,5.33,0,0,0,9.55,0l16.56-39.72L317,68.78a5.33,5.33,0,0,0,0-9.54L277.32,42.68,260.76,3a5.33,5.33,0,0,0-9.55,0L234.65,42.68,194.93,59.24a5.34,5.34,0,0,0,0,9.54ZM157,379.24l-39.72-16.57L100.76,323a5.34,5.34,0,0,0-9.55,0L74.65,362.67,34.93,379.24a5.34,5.34,0,0,0,0,9.54l39.72,16.56,16.56,39.72a5.33,5.33,0,0,0,9.55,0l16.56-39.72L157,388.78a5.33,5.33,0,0,0,0-9.54Zm179-101.9v85.33A21.39,21.39,0,0,0,357.36,384h85.31A21.39,21.39,0,0,0,464,362.67V277.34A21.4,21.4,0,0,0,442.67,256H357.36A21.4,21.4,0,0,0,336,277.34ZM368,288H432v64H368Zm266.49,8L576,244.75V144a16,16,0,0,0-32,0v72.75L410.53,100a16,16,0,0,0-21.07,0l-224,196a16,16,0,0,0,21.07,24.09L224,287.28V464a48.05,48.05,0,0,0,48,48H528a48.06,48.06,0,0,0,48-48V287.28l37.46,32.78A16,16,0,0,0,634.53,296ZM544,464a16,16,0,0,1-16,16H272a16,16,0,0,1-16-16V264a15.94,15.94,0,0,0-.81-4L400,133.27l144,126Z" class=""></path></svg></router-link>
|
||||
</div>
|
||||
|
||||
<div id="nav-bookmark" ref="bookmark" :style="bookmarkCss"></div>
|
||||
@@ -27,89 +28,93 @@
|
||||
<router-view/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from "vue-class-component";
|
||||
<script setup lang="ts">
|
||||
import {nextTick, onMounted, onUnmounted, ref, ComponentPublicInstance} from 'vue';
|
||||
import router from "@/scripts/router";
|
||||
import {RouteLocationNormalized} from "vue-router";
|
||||
import {RouteLocationNormalized, RouteLocationRaw, useRoute} from "vue-router";
|
||||
|
||||
@Options({components: {}})
|
||||
export default class App extends Vue
|
||||
{
|
||||
currentRoute = ''
|
||||
currentLink: Element = null as never as Element
|
||||
bookmarkCss = ""
|
||||
bookmarkUpdateIntervalId!: number
|
||||
lastTop = 0
|
||||
const route = useRoute()
|
||||
|
||||
menuOpen = false
|
||||
const currentRoute = ref('')
|
||||
const bookmarkCss = ref('')
|
||||
const lastTop = ref(0)
|
||||
const menuOpen = ref(false)
|
||||
const bookmarkUpdateIntervalId = ref<number | null>(null)
|
||||
const removeAfterEach = ref<(() => void) | null>(null)
|
||||
|
||||
showMenu(): void
|
||||
{
|
||||
this.menuOpen = !this.menuOpen
|
||||
const navRefs = ref<Record<string, Element | ComponentPublicInstance | null>>({})
|
||||
|
||||
// Auto close
|
||||
if (this.menuOpen) setTimeout(() => this.menuOpen = false, 2000)
|
||||
}
|
||||
const showMenu = (): void => {
|
||||
menuOpen.value = !menuOpen.value
|
||||
|
||||
updateBookmark(to: RouteLocationNormalized): void
|
||||
{
|
||||
// Update title
|
||||
// Use next tick to handle router history correctly
|
||||
// see: https://github.com/vuejs/vue-router/issues/914#issuecomment-384477609
|
||||
this.$nextTick(() => {
|
||||
if (to.name == 'Blog' && Object.keys(to.query).length != 0) return
|
||||
document.title = to.meta.title ? `Hykilpikonna - ${to.meta.title}` : 'Hykilpikonna - Home';
|
||||
})
|
||||
|
||||
console.log('AfterEach called', to)
|
||||
this.currentRoute = ((to.meta?.navBookmark ?? to.name) as string).toLowerCase()
|
||||
|
||||
this.calculateBookmarkCss()
|
||||
this.menuOpen = false
|
||||
}
|
||||
|
||||
mounted(): void
|
||||
{
|
||||
console.log('Mounted called', this.$route)
|
||||
router.afterEach(this.updateBookmark)
|
||||
if (this.$route.name) this.currentRoute = ((this.$route.meta?.navBookmark ?? this.$route.name) as string).toLowerCase()
|
||||
|
||||
// Resize listener
|
||||
window.addEventListener('resize', this.calculateBookmarkCss, true);
|
||||
|
||||
// Update every second
|
||||
this.bookmarkUpdateIntervalId = window.setInterval(this.calculateBookmarkCss, 1000)
|
||||
}
|
||||
|
||||
unmounted(): void
|
||||
{
|
||||
window.removeEventListener('resize', this.calculateBookmarkCss)
|
||||
window.clearInterval(this.bookmarkUpdateIntervalId)
|
||||
}
|
||||
|
||||
calculateBookmarkCss(): void
|
||||
{
|
||||
if (this.currentRoute in this.$refs)
|
||||
this.currentLink = (this.$refs[this.currentRoute] as Vue).$el
|
||||
else return
|
||||
|
||||
// https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect
|
||||
let box = this.currentLink.getBoundingClientRect()
|
||||
if (box.top == this.lastTop) return
|
||||
this.lastTop = box.top
|
||||
|
||||
let h = box.bottom - box.top
|
||||
let width = Math.round(h / 2) + 8
|
||||
|
||||
this.bookmarkCss = `top: ${box.top - 8}px;` +
|
||||
`border-width: ${width}px 20px ${width}px 50px;`
|
||||
}
|
||||
if (menuOpen.value) setTimeout(() => menuOpen.value = false, 2000)
|
||||
}
|
||||
|
||||
const setNavRef = (name: string) => (el: Element | ComponentPublicInstance | null) => {
|
||||
navRefs.value[name] = el
|
||||
}
|
||||
|
||||
const resolveNavElement = (target: Element | ComponentPublicInstance | null | undefined): Element | null => {
|
||||
if (!target) return null
|
||||
if (target instanceof Element) return target
|
||||
return (target.$el as Element | undefined) ?? null
|
||||
}
|
||||
|
||||
const calculateBookmarkCss = (): void => {
|
||||
const currentLink = resolveNavElement(navRefs.value[currentRoute.value])
|
||||
if (!currentLink) return
|
||||
|
||||
const box = currentLink.getBoundingClientRect()
|
||||
if (box.top === lastTop.value) return
|
||||
lastTop.value = box.top
|
||||
|
||||
const h = box.bottom - box.top
|
||||
const width = Math.round(h / 2) + 8
|
||||
|
||||
bookmarkCss.value = `top: ${box.top - 8}px;` +
|
||||
`border-width: ${width}px 20px ${width}px 50px;`
|
||||
}
|
||||
|
||||
const getRouteBookmark = (to: RouteLocationNormalized): string => {
|
||||
return ((to.meta?.navBookmark ?? to.name) as string).toLowerCase()
|
||||
}
|
||||
|
||||
const updateBookmark = (to: RouteLocationNormalized): void => {
|
||||
nextTick(() => {
|
||||
if (to.name == 'Blog' && Object.keys(to.query).length != 0) return
|
||||
document.title = to.meta.title ? `Aza - ${to.meta.title}` : 'Aza - Home';
|
||||
})
|
||||
|
||||
console.log('AfterEach called', to)
|
||||
currentRoute.value = getRouteBookmark(to)
|
||||
|
||||
calculateBookmarkCss()
|
||||
menuOpen.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('Mounted called', route)
|
||||
removeAfterEach.value = router.afterEach(updateBookmark)
|
||||
if (route.name) {
|
||||
currentRoute.value = getRouteBookmark(route as unknown as RouteLocationNormalized)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', calculateBookmarkCss, true)
|
||||
bookmarkUpdateIntervalId.value = window.setInterval(calculateBookmarkCss, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
removeAfterEach.value?.()
|
||||
window.removeEventListener('resize', calculateBookmarkCss, true)
|
||||
if (bookmarkUpdateIntervalId.value !== null) {
|
||||
window.clearInterval(bookmarkUpdateIntervalId.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@import "css/global"
|
||||
@import "css/animations"
|
||||
@use "css/global"
|
||||
@use "css/animations"
|
||||
|
||||
#nav
|
||||
position: fixed
|
||||
|
||||
@@ -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>
|
||||
<div class="index index-tags" v-if="mode === 'tags'">
|
||||
<Tag v-for="t in meta.tags" :key="t" :tag-name="t[0]" direction="right"
|
||||
<Tag v-for="t in meta.tags" :key="`${t[0]}-${t[1]}`" :tag-name="t[0]" direction="right"
|
||||
@click="e => clickTag(e, t)">{{ t[0] }} ({{ t[1] }})</Tag>
|
||||
</div>
|
||||
<div class="index index-categories" v-else>
|
||||
@@ -9,38 +9,31 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import {staticMeta} from '@/views/Blog.vue'
|
||||
<script setup lang="ts">
|
||||
import Tag from "@/components/Tag.vue";
|
||||
import {Prop} from "vue-property-decorator";
|
||||
import {pushQuery} from "@/scripts/router";
|
||||
import {BlogMeta} from "@/scripts/models";
|
||||
import {globals} from "@/scripts/global";
|
||||
|
||||
@Options({components: {Tag}})
|
||||
export default class BlogIndexLinks extends Vue
|
||||
{
|
||||
@Prop({default: 'tags'}) mode: 'tags' | 'categories' = 'tags'
|
||||
withDefaults(defineProps<{ mode?: 'tags' | 'categories' }>(), {
|
||||
mode: 'tags'
|
||||
})
|
||||
|
||||
meta: BlogMeta = staticMeta
|
||||
const meta: BlogMeta = globals.staticMeta
|
||||
|
||||
clickCat(e: MouseEvent, cat: [string, number]): void
|
||||
{
|
||||
e.stopPropagation()
|
||||
pushQuery({category: cat[0], tag: null})
|
||||
}
|
||||
const clickCat = (e: MouseEvent, cat: [string, number]): void => {
|
||||
e.stopPropagation()
|
||||
pushQuery({category: cat[0], tag: null})
|
||||
}
|
||||
|
||||
|
||||
clickTag(e: MouseEvent, tag: [string, number]): void
|
||||
{
|
||||
e.stopPropagation()
|
||||
pushQuery({tag: tag[0], category: null})
|
||||
}
|
||||
const clickTag = (e: MouseEvent, tag: [string, number]): void => {
|
||||
e.stopPropagation()
|
||||
pushQuery({tag: tag[0], category: null})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
@import 'src/css/colors'
|
||||
@use '../css/colors'
|
||||
|
||||
.index
|
||||
*
|
||||
@@ -56,7 +49,7 @@ export default class BlogIndexLinks extends Vue
|
||||
|
||||
.index-categories
|
||||
font-size: 0.8em
|
||||
color: $color-text-special
|
||||
color: colors.$color-text-special
|
||||
|
||||
*
|
||||
text-decoration: underline
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="BlogPostPreview" class="card" :class="elClass">
|
||||
<img class="title-image" :src="image" v-if="image && imageOnTop" alt="Title Image">
|
||||
<img class="title-image" :src="p.meta.title_image" v-if="p.meta.title_image && imageOnTop" alt="Title Image">
|
||||
|
||||
<div id="titles" class="unselectable clickable" @click="clickTitle">
|
||||
<div id="date">{{ date.format('YYYY-MM-DD') }}</div>
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<img class="title-image" :src="image" v-if="image && !imageOnTop" alt="Title Image">
|
||||
<img class="title-image" :src="p.meta.title_image" v-if="p.meta.title_image && !imageOnTop" alt="Title Image">
|
||||
<div id="text" class="markdown-content">
|
||||
<Dynamic :template="content"></Dynamic>
|
||||
</div>
|
||||
@@ -106,13 +106,12 @@ const elClass = computed(() =>
|
||||
return classes
|
||||
})
|
||||
|
||||
const content = computed(() => marked(p.meta.content.replaceAll('\n', ' \n')))
|
||||
const content = marked(p.meta.content.replaceAll('\n', ' \n').replaceAll("{src}", hosts.content))
|
||||
const date = moment(p.meta.date)
|
||||
const image = p.meta.title_image ? hosts.content + '/' + p.meta.title_image : null
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
@import 'src/css/colors'
|
||||
@use '../css/colors'
|
||||
|
||||
#BlogPostPreview
|
||||
text-align: left
|
||||
@@ -122,7 +121,7 @@ const image = p.meta.title_image ? hosts.content + '/' + p.meta.title_image : nu
|
||||
|
||||
#date
|
||||
font-size: 0.7em
|
||||
color: $color-text-light
|
||||
color: colors.$color-text-light
|
||||
|
||||
> * + *, #content > * + *
|
||||
padding-top: 10px
|
||||
@@ -151,7 +150,7 @@ const image = p.meta.title_image ? hosts.content + '/' + p.meta.title_image : nu
|
||||
|
||||
#subtitle
|
||||
font-size: 0.8em
|
||||
color: $color-text-light
|
||||
color: colors.$color-text-light
|
||||
|
||||
img
|
||||
$margin: 10px
|
||||
@@ -172,7 +171,7 @@ const image = p.meta.title_image ? hosts.content + '/' + p.meta.title_image : nu
|
||||
#expand
|
||||
font-size: 0.8em
|
||||
padding-top: 10px
|
||||
color: $color-text-light
|
||||
color: colors.$color-text-light
|
||||
|
||||
// Put image on top
|
||||
#BlogPostPreview.image-top
|
||||
|
||||
+14
-20
@@ -7,30 +7,24 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import {Prop} from "vue-property-decorator";
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted} from 'vue'
|
||||
import {$} from '@/scripts/constants';
|
||||
|
||||
@Options({components: {}})
|
||||
export default class Collapse extends Vue
|
||||
{
|
||||
@Prop() title!: string
|
||||
@Prop({default: false}) active = false
|
||||
const props = withDefaults(defineProps<{ title: string, active?: boolean }>(), {
|
||||
active: false
|
||||
})
|
||||
|
||||
show = false
|
||||
const displayTitle = computed((): string => decodeURIComponent(props.title))
|
||||
|
||||
get displayTitle(): string
|
||||
{
|
||||
return decodeURIComponent(this.title)
|
||||
}
|
||||
|
||||
mounted(): void
|
||||
{
|
||||
$('.collapse').accordion({collapsible: true, header: 'h3', heightStyle: 'content',
|
||||
active: this.active})
|
||||
}
|
||||
}
|
||||
onMounted((): void => {
|
||||
$('.collapse').accordion({
|
||||
collapsible: true,
|
||||
header: 'h3',
|
||||
heightStyle: 'content',
|
||||
active: props.active
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
@@ -7,41 +7,33 @@
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import {Prop} from "vue-property-decorator";
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
|
||||
@Options({components: {}})
|
||||
export default class MetaTable extends Vue
|
||||
{
|
||||
@Prop({required: true}) table!: {[id: string]: unknown}
|
||||
const props = defineProps<{ table: {[id: string]: unknown} }>()
|
||||
|
||||
get filteredTable(): {[id: string]: unknown}
|
||||
{
|
||||
const t: {[id: string]: unknown} = {}
|
||||
const filteredTable = computed((): {[id: string]: unknown} => {
|
||||
const t: {[id: string]: unknown} = {}
|
||||
|
||||
Object.keys(this.table).forEach(k => {
|
||||
// Ignore empty
|
||||
if (!this.table[k]) return
|
||||
Object.keys(props.table).forEach(k => {
|
||||
if (!props.table[k]) return
|
||||
|
||||
// Convert to sentence case (https://stackoverflow.com/a/7225450/7346633)
|
||||
let newK = k.replace(/([A-Z])/g, " $1")
|
||||
newK = newK.charAt(0).toUpperCase() + newK.slice(1)
|
||||
t[newK] = this.table[k]
|
||||
})
|
||||
let newK = k.replace(/([A-Z])/g, " $1")
|
||||
newK = newK.charAt(0).toUpperCase() + newK.slice(1)
|
||||
t[newK] = props.table[k]
|
||||
})
|
||||
|
||||
return t
|
||||
}
|
||||
}
|
||||
return t
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
@import "src/css/colors"
|
||||
@use "../css/colors"
|
||||
|
||||
.meta
|
||||
td:first-child
|
||||
text-align: right
|
||||
color: $color-text-light
|
||||
color: colors.$color-text-light
|
||||
|
||||
td:last-child
|
||||
display: inline-block
|
||||
|
||||
@@ -15,11 +15,11 @@ const props = defineProps({
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
@import src/css/colors
|
||||
@use "../css/colors"
|
||||
|
||||
$tag-height: 20px
|
||||
$tag-color: $color-bg-6
|
||||
$text-color: $color-text-light
|
||||
$tag-color: colors.$color-bg-6
|
||||
$text-color: colors.$color-text-light
|
||||
|
||||
$padding: calc($tag-height / 2)
|
||||
$triangle-width: calc($tag-height / 2) * 0.8
|
||||
|
||||
@@ -16,16 +16,15 @@
|
||||
<div id="attachments" v-if="item.attachments.length !== 0">
|
||||
<div class="label">Attachments</div>
|
||||
<div class="content" v-for="a of item.attachments" :key="a.data.key">
|
||||
<a :href="a.links['enclosure'].href">{{a.data.title}}</a>
|
||||
<a :href="a.links['enclosure']?.href">{{a.data.title}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
import {Prop} from "vue-property-decorator";
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted} from 'vue'
|
||||
import moment from "moment";
|
||||
import MetaTable from "@/components/MetaTable.vue";
|
||||
import {capitalize} from "@/scripts/utils";
|
||||
@@ -33,34 +32,32 @@ import linkifyUrls from "linkify-urls";
|
||||
import {$} from '@/scripts/constants';
|
||||
import {ZoteroData, ZoteroItem} from "@/scripts/zotero";
|
||||
|
||||
@Options({components: {MetaTable}})
|
||||
export default class ZoteroPublicationView extends Vue
|
||||
{
|
||||
@Prop({required: true}) item!: ZoteroItem
|
||||
const props = defineProps<{ item: ZoteroItem }>()
|
||||
|
||||
get d(): ZoteroData { return this.item.data }
|
||||
get date(): moment.Moment { return moment(this.item.meta.parsedDate) }
|
||||
get authors(): string { return this.d.creators.map(it => it.firstName + ' ' + it.lastName).join(' & ') }
|
||||
const d = computed((): ZoteroData => props.item.data)
|
||||
const date = computed((): moment.Moment => moment(props.item.meta.parsedDate))
|
||||
const authors = computed((): string => d.value.creators.map(it => it.firstName + ' ' + it.lastName).join(' & '))
|
||||
|
||||
get tableData(): {[id: string]: unknown}
|
||||
{
|
||||
const t: {[id: string]: unknown} = {...this.d}
|
||||
t.creators = this.authors
|
||||
delete t.key
|
||||
delete t.version
|
||||
delete t.title
|
||||
delete t.abstractNote
|
||||
if (t.itemType) t.itemType = capitalize(t.itemType as string)
|
||||
if (t.url) t.url = linkifyUrls(t.url as string)
|
||||
return t
|
||||
}
|
||||
const tableData = computed((): {[id: string]: unknown} => {
|
||||
const t: {[id: string]: unknown} = {...d.value}
|
||||
t.creators = authors.value
|
||||
delete t.key
|
||||
delete t.version
|
||||
delete t.title
|
||||
delete t.abstractNote
|
||||
if (t.itemType) t.itemType = capitalize(t.itemType as string)
|
||||
if (t.url) t.url = linkifyUrls(t.url as string)
|
||||
return t
|
||||
})
|
||||
|
||||
mounted(): void
|
||||
{
|
||||
$('.publication').accordion({collapsible: true, header: 'div.header', active: false,
|
||||
heightStyle: "content"})
|
||||
}
|
||||
}
|
||||
onMounted((): void => {
|
||||
$('.publication').accordion({
|
||||
collapsible: true,
|
||||
header: 'div.header',
|
||||
active: false,
|
||||
heightStyle: 'content'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
@@ -77,6 +74,7 @@ export default class ZoteroPublicationView extends Vue
|
||||
|
||||
#details
|
||||
padding-left: calc(1.6em + 6px)
|
||||
font-size: 0.8em
|
||||
|
||||
> div
|
||||
margin-bottom: 1em
|
||||
@@ -87,8 +85,6 @@ export default class ZoteroPublicationView extends Vue
|
||||
.label
|
||||
font-weight: bold
|
||||
|
||||
font-size: 0.8em
|
||||
|
||||
.header
|
||||
align-items: center
|
||||
|
||||
|
||||
@@ -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-family: 'WenKai'
|
||||
src: url("/fonts/LXGWWenKai-Regular.woff2") format('woff2'), url("/fonts/LXGWWenKai-Regular.woff") format('woff')
|
||||
//@font-face
|
||||
// font-family: 'WenKai'
|
||||
// src: url("/fonts/LXGWWenKai-Regular.woff2") format('woff2'), url("/fonts/LXGWWenKai-Regular.woff") format('woff')
|
||||
@import 'lxgw-wenkai-webfont/style.css'
|
||||
|
||||
.font-custom
|
||||
font-family: "WenKai", var(--font-fallback)
|
||||
//font-family: "WenKai", var(--font-fallback)
|
||||
font-family: "LXGW WenKai", var(--font-fallback), serif
|
||||
//line-height: 1.3em
|
||||
|
||||
+31
-9
@@ -1,5 +1,5 @@
|
||||
@import colors
|
||||
@import font
|
||||
@use "colors"
|
||||
@use "font"
|
||||
|
||||
// Google Fonts
|
||||
// TODO: Localize
|
||||
@@ -119,7 +119,7 @@ body
|
||||
-webkit-font-smoothing: antialiased
|
||||
-moz-osx-font-smoothing: grayscale
|
||||
text-align: center
|
||||
color: $color-text-main
|
||||
color: colors.$color-text-main
|
||||
|
||||
// Max width and center
|
||||
max-width: 900px
|
||||
@@ -170,30 +170,52 @@ html
|
||||
text-justify: inter-word
|
||||
|
||||
a
|
||||
color: $color-text-special
|
||||
color: colors.$color-text-special
|
||||
text-decoration: none
|
||||
|
||||
h1, h2
|
||||
border-bottom: 1px solid $color-text-special
|
||||
border-bottom: 1px solid colors.$color-text-special
|
||||
font-size: 1.5em
|
||||
margin-top: 1em
|
||||
|
||||
h1, h2
|
||||
line-height: 1.3
|
||||
margin-bottom: 0.25em
|
||||
padding: 0
|
||||
|
||||
h2
|
||||
font-size: 1.3em
|
||||
|
||||
h3
|
||||
font-size: 1.2em
|
||||
|
||||
p
|
||||
font-size: 0.9em
|
||||
margin: 0.5em 0
|
||||
font-size: 1em
|
||||
margin: 0.7em 0
|
||||
line-height: 1.6
|
||||
|
||||
p:last-child
|
||||
margin-bottom: 0
|
||||
|
||||
li
|
||||
font-size: 0.9em
|
||||
font-size: 1em
|
||||
margin-bottom: 0.25em
|
||||
|
||||
figure
|
||||
margin: 0
|
||||
|
||||
img
|
||||
width: calc(100% + 20px)
|
||||
margin-left: -10px
|
||||
margin-right: -10px
|
||||
border-radius: 10px
|
||||
object-fit: cover
|
||||
max-height: 500px
|
||||
|
||||
caption
|
||||
display: flex
|
||||
width: 100%
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
font-size: 0.8em
|
||||
color: colors.$color-text-light
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "src/css/colors"
|
||||
@use "../css/colors"
|
||||
|
||||
$width: 600px
|
||||
|
||||
@@ -14,7 +14,7 @@ $width: 600px
|
||||
margin-bottom: 0
|
||||
|
||||
.subtitle
|
||||
color: $color-text-light
|
||||
color: colors.$color-text-light
|
||||
|
||||
|
||||
// Phone layout
|
||||
|
||||
@@ -6,6 +6,7 @@ import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||
import Collapse from "@/components/Collapse.vue"
|
||||
import BlogIndex from "@/components/BlogIndex.vue";
|
||||
import Tag from "@/components/Tag.vue";
|
||||
import 'virtual:uno.css'
|
||||
|
||||
const app = createApp(App).use(router).use(i18n)
|
||||
.component('Collapse', Collapse)
|
||||
|
||||
@@ -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: '记事本',
|
||||
life: '生活',
|
||||
projects: 'Projects',
|
||||
others: '更多'
|
||||
others: '更多',
|
||||
photo: '相册'
|
||||
}
|
||||
},
|
||||
zh: {
|
||||
|
||||
+12
-12
@@ -8,12 +8,6 @@ const routes: Array<RouteRecordRaw> = [
|
||||
meta: {title: 'Home', nav: true},
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/new-home',
|
||||
name: 'New Home',
|
||||
meta: {title: 'Home'},
|
||||
component: () => import('../views/NewHome.vue')
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
@@ -33,6 +27,18 @@ const routes: Array<RouteRecordRaw> = [
|
||||
component: () => import('../views/Blog.vue'),
|
||||
props: route => (route.query)
|
||||
},
|
||||
{
|
||||
path: '/photo',
|
||||
name: 'Photo',
|
||||
meta: {title: '相册', nav: true},
|
||||
component: () => import('../views/Photo.vue')
|
||||
},
|
||||
{
|
||||
path: '/photo/:id',
|
||||
name: 'Photo ID',
|
||||
meta: {title: '相册', navBookmark: 'Photo'},
|
||||
component: () => import('../views/Photo.vue')
|
||||
},
|
||||
{
|
||||
path: '/others',
|
||||
name: 'Others',
|
||||
@@ -57,12 +63,6 @@ const routes: Array<RouteRecordRaw> = [
|
||||
meta: {title: '我做的', navBookmark: 'Others'},
|
||||
component: () => import('../views/others/Projects.vue')
|
||||
},
|
||||
{
|
||||
path: '/color',
|
||||
name: 'ColorPicker',
|
||||
meta: {title: 'Color Picker'},
|
||||
component: () => import('../components/color/ColorPickerTest.vue')
|
||||
},
|
||||
]
|
||||
|
||||
export const router = createRouter({
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import {Vue} from "vue-class-component";
|
||||
|
||||
/**
|
||||
* Same as python's range
|
||||
*
|
||||
@@ -41,9 +39,9 @@ export function minMax(val: number, min: number, max: number): number
|
||||
export type Keybinds = {[id: string]: (e: KeyboardEvent) => unknown}
|
||||
|
||||
/**
|
||||
* Key handler mixin
|
||||
* Key handler standalone helper class (not a Vue mixin)
|
||||
*/
|
||||
export class KeyHandler extends Vue
|
||||
export class KeyHandler
|
||||
{
|
||||
keybinds: Keybinds = {}
|
||||
_keybinds: Keybinds = {}
|
||||
|
||||
+19
-30
@@ -9,8 +9,8 @@
|
||||
<Loading v-else></Loading>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue'
|
||||
import {marked} from 'marked';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
import {parseExtensions} from '@/scripts/extended_markdown'
|
||||
@@ -19,39 +19,28 @@ import {hosts} from "@/scripts/constants";
|
||||
import Loading from "@/components/Loading.vue";
|
||||
import {ZoteroAttachment, ZoteroItem} from "@/scripts/zotero";
|
||||
|
||||
@Options({components: {Loading, ZoteroPublication}})
|
||||
export default class About extends Vue
|
||||
{
|
||||
html = ""
|
||||
publications: ZoteroItem[] = []
|
||||
const html = ref("")
|
||||
const publications = ref<ZoteroItem[]>([])
|
||||
|
||||
mounted(): void
|
||||
{
|
||||
// Fetch readme
|
||||
fetch(`${hosts.content}/README.md`).then(it => it.text())
|
||||
.then(it => this.html = marked(parseExtensions(it.replace(emojiRegex(), (emoji) => {
|
||||
return `<span class="emoji">${emoji}</span>`
|
||||
}))))
|
||||
onMounted((): void => {
|
||||
fetch(`${hosts.content}/README.md`).then(it => it.text())
|
||||
.then(it => html.value = marked(parseExtensions(it.replace(emojiRegex(), (emoji) => {
|
||||
return `<span class="emoji">${emoji}</span>`
|
||||
}))))
|
||||
|
||||
// Fetch publications from zotero
|
||||
fetch(`${hosts.api}/zotero.json`)
|
||||
.then(it => it.json()).then(it =>
|
||||
{
|
||||
// Filter out publications and attachments
|
||||
this.publications = it
|
||||
let files: ZoteroAttachment[] = it
|
||||
files = files.filter(it => it.data.itemType === 'attachment')
|
||||
this.publications = this.publications.filter(it => it.data.itemType !== 'attachment')
|
||||
|
||||
// Add attachments to
|
||||
this.publications.forEach(it => it.attachments = files.filter(a => a.data.parentItem == it.key))
|
||||
})
|
||||
}
|
||||
}
|
||||
fetch(`${hosts.api}/zotero.json`)
|
||||
.then(it => it.json()).then((it: ZoteroItem[]) => {
|
||||
publications.value = it
|
||||
let files: ZoteroAttachment[] = it as unknown as ZoteroAttachment[]
|
||||
files = files.filter(file => file.data.itemType === 'attachment')
|
||||
publications.value = publications.value.filter(pub => pub.data.itemType !== 'attachment')
|
||||
publications.value.forEach(pub => pub.attachments = files.filter(a => a.data.parentItem == pub.key))
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../css/colors"
|
||||
@use "../css/colors"
|
||||
|
||||
#About
|
||||
width: min(600px, 80vw)
|
||||
|
||||
+36
-39
@@ -10,64 +10,61 @@
|
||||
<span v-if="category">📂{{category}}</span>
|
||||
<span class="no-after" v-if="post && activePost">{{activePost.title}}</span>
|
||||
</div>
|
||||
<BlogPostPreview v-for="m of filteredPosts" :key="m" :meta="m" :active="m === activePost"/>
|
||||
<BlogPostPreview v-for="m of filteredPosts" :key="m.id" :meta="m" :active="m === activePost"/>
|
||||
</div>
|
||||
<Loading v-else></Loading>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
<script setup lang="ts">
|
||||
import BlogPostPreview from "@/components/BlogPost.vue";
|
||||
import {hosts} from "@/scripts/constants";
|
||||
import {Prop} from "vue-property-decorator";
|
||||
import Loading from "@/components/Loading.vue";
|
||||
import {BlogMeta, BlogPost} from "@/scripts/models";
|
||||
import {BlogMeta} from "@/scripts/models";
|
||||
import {Ref, ref, computed, onMounted} from "vue";
|
||||
import {hosts} from "@/scripts/constants";
|
||||
import {globals} from "@/scripts/global";
|
||||
import {Router} from "vue-router";
|
||||
|
||||
export let staticMeta: BlogMeta = {tags: [], categories: [], posts: []}
|
||||
let $router: Router
|
||||
|
||||
@Options({components: {Loading, BlogPostPreview}})
|
||||
export default class Blog extends Vue
|
||||
{
|
||||
@Prop() post?: string
|
||||
@Prop() category?: string
|
||||
@Prop() tag?: string
|
||||
const p = defineProps<{
|
||||
post?: string,
|
||||
category?: string,
|
||||
tag?: string
|
||||
}>()
|
||||
|
||||
meta: BlogMeta = {tags: [], categories: [], posts: []}
|
||||
let meta: Ref<BlogMeta> = ref({tags: [], categories: [], posts: []})
|
||||
|
||||
mounted(): void
|
||||
{
|
||||
fetch(`${hosts.content}/content/generated/metas.json`).then(it => it.json()).then(it => {
|
||||
this.meta = it
|
||||
staticMeta = it
|
||||
})
|
||||
}
|
||||
onMounted(() => {
|
||||
fetch(`${hosts.content}/content/generated/metas.json`).then(it => it.json()).then(it => {
|
||||
meta.value = it
|
||||
globals.staticMeta = it
|
||||
})
|
||||
})
|
||||
|
||||
get activePost(): BlogPost | null
|
||||
{
|
||||
const p = this.filteredPosts
|
||||
if (p.length == 0) return null
|
||||
return this.post ? p.filter(it => it.url_name == this.post)[0] : p[0].pinned ? p[0] : null
|
||||
}
|
||||
const filteredPosts = computed(() => {
|
||||
const posts = meta.value.posts.filter(it => it.pinned || (p.tag ? it.tags.includes(p.tag) :
|
||||
p.category ? it.category == p.category : true))
|
||||
|
||||
get filteredPosts(): BlogPost[]
|
||||
{
|
||||
const posts = this.meta.posts.filter(it => it.pinned || (this.tag ? it.tags.includes(this.tag) :
|
||||
this.category ? it.category == this.category : true))
|
||||
// Put pinned posts on top
|
||||
posts.sort((a, b) => (b.pinned ?? 0) - (a.pinned ?? 0))
|
||||
|
||||
// Put pinned posts on top
|
||||
posts.sort((a, b) => (b.pinned ?? 0) - (a.pinned ?? 0))
|
||||
return posts
|
||||
})
|
||||
|
||||
return posts
|
||||
}
|
||||
}
|
||||
const activePost = computed(() => {
|
||||
const posts = filteredPosts.value
|
||||
if (posts.length == 0) return null
|
||||
if (!p.post) return null
|
||||
return p.post ? posts.filter(it => it.url_name == p.post)[0] : posts[0].pinned ? posts[0] : null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
@import "src/css/colors"
|
||||
@import "src/css/responsive"
|
||||
@use "../css/colors"
|
||||
@use "../css/responsive"
|
||||
|
||||
#breadcrumb
|
||||
color: $color-text-light
|
||||
color: colors.$color-text-light
|
||||
margin-bottom: 20px
|
||||
|
||||
span:not(.no-after):after
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="Home" class="fbox-center f-grow1">
|
||||
<div id="box">
|
||||
<div class="font-script-en">Hykilpikonna's</div>
|
||||
<div class="font-script-en">Azalea's</div>
|
||||
<div class="font-script-en bold">Road Less Traveled</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -20,8 +20,8 @@ import 'tg-blog/dist/style.css'
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
@import "src/css/colors"
|
||||
@import "src/css/responsive"
|
||||
@use "../css/colors"
|
||||
@use "../css/responsive"
|
||||
|
||||
.title
|
||||
text-align: left
|
||||
|
||||
@@ -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="/friends">🎎 朋友们</router-link>
|
||||
<a href="https://cast.hydev.org">📹 公开直播间</a>
|
||||
<a href="http://status.hydev.org/">🔌 看看服务器们过得怎么样</a>
|
||||
<a href="https://status.hydev.org/">🔌 看看服务器们过得怎么样</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
|
||||
@Options({})
|
||||
export default class Others extends Vue
|
||||
{
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
@import "src/css/global"
|
||||
@import "src/css/colors"
|
||||
@import "src/css/responsive"
|
||||
@use "../css/global"
|
||||
@use "../css/colors"
|
||||
@use "../css/responsive"
|
||||
|
||||
.links
|
||||
display: flex
|
||||
@@ -38,7 +28,7 @@ a
|
||||
@extend .card
|
||||
@extend .clickable
|
||||
|
||||
color: $color-text-main
|
||||
color: colors.$color-text-main
|
||||
text-decoration: none
|
||||
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue'
|
||||
import {fab, hosts} from "@/scripts/constants";
|
||||
import {shuffle} from "@/scripts/utils";
|
||||
|
||||
export interface Friend {
|
||||
interface Friend {
|
||||
name: string
|
||||
avatar: string
|
||||
banner: string
|
||||
|
||||
desc?: string
|
||||
[id: string]: string | undefined
|
||||
}
|
||||
|
||||
const excludes = new Set(["name", "avatar", "banner", "desc"])
|
||||
const icons = {
|
||||
const icons: {[id: string]: string} = {
|
||||
blog: 'fas fa-book'
|
||||
}
|
||||
|
||||
@Options({components: {}})
|
||||
export default class Friends extends Vue
|
||||
{
|
||||
friends: Friend[] = []
|
||||
const friends = ref<Friend[]>([])
|
||||
|
||||
async created()
|
||||
{
|
||||
this.friends = await (await fetch(`${hosts.content}/content/generated/friends/friends.json`)).json()
|
||||
|
||||
// Fix avatar relative url
|
||||
this.friends.forEach(f => {
|
||||
if (!f.avatar.startsWith('http')) f.avatar = `${hosts.content}/${f.avatar}`
|
||||
if (f.banner && !f.banner.startsWith('http')) f.banner = `${hosts.content}/${f.banner}`
|
||||
})
|
||||
this.friends = shuffle(this.friends)
|
||||
}
|
||||
|
||||
bgStyle(f: Friend)
|
||||
{
|
||||
if (f.banner) return {'background-image': `url("${f.banner}")`}
|
||||
else return {}
|
||||
}
|
||||
|
||||
getFriendLinks(f: Friend): { link: string, icon: string }[]
|
||||
{
|
||||
return Object.entries(f).filter(pair => !excludes.has(pair[0].toString()))
|
||||
.map(pair => {
|
||||
return { link: pair[1], icon: fab.includes(pair[0]) ? `fab fa-${pair[0]}` :
|
||||
pair[0] in icons ? icons[pair[0]] : pair[0] }
|
||||
})
|
||||
}
|
||||
const bgStyle = (f: Friend) => {
|
||||
if (f.banner) return {'background-image': `url("${f.banner}")`}
|
||||
return {}
|
||||
}
|
||||
|
||||
const getFriendLinks = (f: Friend): { link: string, icon: string }[] => {
|
||||
return Object.entries(f)
|
||||
.filter(([key, value]) => !excludes.has(key) && typeof value === 'string')
|
||||
.map(([key, value]) => ({
|
||||
link: value,
|
||||
icon: fab.includes(key) ? `fab fa-${key}` : (key in icons ? icons[key] : key)
|
||||
}))
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
friends.value = await (await fetch(`${hosts.content}/content/generated/friends/friends.json`)).json()
|
||||
|
||||
friends.value.forEach(f => {
|
||||
if (!f.avatar.startsWith('http')) f.avatar = `${hosts.content}/${f.avatar}`
|
||||
if (f.banner && !f.banner.startsWith('http')) f.banner = `${hosts.content}/${f.banner}`
|
||||
})
|
||||
friends.value = shuffle(friends.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
@import "src/css/colors"
|
||||
@import "src/css/responsive"
|
||||
@use "../../css/colors"
|
||||
@use "../../css/responsive"
|
||||
|
||||
$card-min-width: 320px
|
||||
|
||||
@@ -126,7 +119,7 @@ $card-min-width: 320px
|
||||
flex: 1
|
||||
|
||||
a
|
||||
color: $color-text-main
|
||||
color: colors.$color-text-main
|
||||
|
||||
a + a
|
||||
margin-left: 10px
|
||||
|
||||
+27
-51
@@ -23,37 +23,26 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
|
||||
export enum Flavor
|
||||
{
|
||||
light,
|
||||
normal,
|
||||
salty
|
||||
}
|
||||
|
||||
export interface MenuItem
|
||||
{
|
||||
interface MenuItem {
|
||||
name: string
|
||||
sub?: string
|
||||
img?: string
|
||||
recommend?: boolean
|
||||
original?: boolean
|
||||
|
||||
id?: number
|
||||
}
|
||||
|
||||
export interface MenuCategory
|
||||
{
|
||||
interface MenuCategory {
|
||||
cat: string
|
||||
subtitle?: string
|
||||
items: MenuItem[]
|
||||
|
||||
column?: number
|
||||
}
|
||||
|
||||
export const menu: MenuCategory[] = [
|
||||
const menu: MenuCategory[] = [
|
||||
{
|
||||
cat: '🍖 猪肉',
|
||||
items: [
|
||||
@@ -141,43 +130,30 @@ export const menu: MenuCategory[] = [
|
||||
},
|
||||
]
|
||||
|
||||
@Options({components: {}})
|
||||
export default class Menu extends Vue
|
||||
{
|
||||
max_cols = 2
|
||||
cols: MenuCategory[][] = new Array(this.max_cols)
|
||||
const maxCols = 2
|
||||
const cols = ref<MenuCategory[][]>(Array.from({length: maxCols}, () => []))
|
||||
|
||||
created()
|
||||
{
|
||||
// Calculate menu layout
|
||||
const tmp = Array.from(menu)
|
||||
tmp.sort((a, b) => b.items.length - a.items.length)
|
||||
const tmp = Array.from(menu)
|
||||
tmp.sort((a, b) => b.items.length - a.items.length)
|
||||
|
||||
// Two columns
|
||||
let col_counts = new Array(this.max_cols).fill(0)
|
||||
for (const cat of tmp)
|
||||
{
|
||||
// Get column index with minimal item count
|
||||
let col = col_counts.indexOf(Math.min(...col_counts))
|
||||
|
||||
cat.column = col
|
||||
col_counts[col] += cat.items.length
|
||||
}
|
||||
|
||||
// Separate arrays by column
|
||||
for (let i = 0; i < this.max_cols; i++)
|
||||
this.cols[i] = menu.filter(it => it.column == i)
|
||||
|
||||
// Assign ID to each item
|
||||
let id = 0
|
||||
this.cols.forEach(col => col.forEach(cat => cat.items.forEach(it => it.id = id++)))
|
||||
}
|
||||
const colCounts = new Array(maxCols).fill(0)
|
||||
for (const cat of tmp) {
|
||||
const col = colCounts.indexOf(Math.min(...colCounts))
|
||||
cat.column = col
|
||||
colCounts[col] += cat.items.length
|
||||
}
|
||||
|
||||
for (let i = 0; i < maxCols; i++) {
|
||||
cols.value[i] = menu.filter(it => it.column == i)
|
||||
}
|
||||
|
||||
let id = 0
|
||||
cols.value.forEach(col => col.forEach(cat => cat.items.forEach(it => it.id = id++)))
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
@import "src/css/colors"
|
||||
@import "src/css/responsive"
|
||||
@use "../../css/colors"
|
||||
@use "../../css/responsive"
|
||||
|
||||
.columns
|
||||
display: flex
|
||||
@@ -191,6 +167,8 @@ export default class Menu extends Vue
|
||||
white-space: nowrap
|
||||
|
||||
.category
|
||||
margin-bottom: 1em
|
||||
|
||||
.cat
|
||||
font-size: 1.2em
|
||||
font-weight: bold
|
||||
@@ -198,16 +176,14 @@ export default class Menu extends Vue
|
||||
.subtitle
|
||||
font-size: 0.8em
|
||||
margin-bottom: 0.5em
|
||||
color: $color-text-light
|
||||
|
||||
margin-bottom: 1em
|
||||
color: colors.$color-text-light
|
||||
|
||||
.items
|
||||
.sub
|
||||
font-size: 0.7em
|
||||
|
||||
.item.recommend
|
||||
color: $color-text-special
|
||||
color: colors.$color-text-special
|
||||
|
||||
.item.original:after
|
||||
content: '原创'
|
||||
|
||||
@@ -4,14 +4,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Options, Vue} from 'vue-class-component';
|
||||
|
||||
@Options({components: {}})
|
||||
export default class Projects extends Vue
|
||||
{
|
||||
|
||||
}
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
||||
@@ -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 vue from "@vitejs/plugin-vue";
|
||||
import UnoCSS from 'unocss/vite';
|
||||
import path from "path";
|
||||
|
||||
const src = path.resolve(__dirname, 'src')
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue()
|
||||
vue(), UnoCSS()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -16,5 +17,10 @@ export default defineConfig({
|
||||
vue: "vue/dist/vue.esm-bundler.js"
|
||||
},
|
||||
dedupe: ['vue'],
|
||||
},
|
||||
server: {
|
||||
watch: {
|
||||
usePolling: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user