Skip to content

Commit 325fe8b

Browse files
committed
added speed dial and construction page
1 parent 85c409e commit 325fe8b

File tree

18 files changed

+553
-31
lines changed

18 files changed

+553
-31
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"tailwindCSS.classAttributes": ["active-class", "inactive-class"]
2+
"tailwindCSS.classAttributes": ["active-class", "inactive-class", "class"]
33
}

app/app.vue

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,53 @@
1-
<script setup></script>
1+
<script setup>
2+
const items = [{
3+
name: 'Blog',
4+
icon: 'i-carbon-blog',
5+
link: '/blog'
6+
}, {
7+
name: 'Contact Me',
8+
icon: 'i-material-symbols-light-contact-mail-outline-rounded',
9+
link: '/contact'
10+
}, {
11+
name: 'Projects',
12+
icon: 'i-material-symbols-light-terminal-rounded',
13+
link: '/projects'
14+
}, {
15+
name: 'About Me',
16+
icon: 'i-material-symbols-light-person-outline-rounded',
17+
link: '/about'
18+
}, {
19+
name: 'Snippets',
20+
icon: 'i-material-symbols-light-code-rounded',
21+
link: '/snippets'
22+
}]
23+
const route = useRoute();
24+
25+
const currentItems = computed(() => items.filter(i => route !== i.link))
26+
27+
const open = ref(false)
28+
</script>
229

330
<template>
431
<UApp>
532
<NuxtLoadingIndicator />
6-
<UContainer class="flex min-h-screen flex-col">
33+
<UContainer id="container" class="flex min-h-screen flex-col">
34+
<div class="z-40">
35+
<Transition>
36+
<div
37+
class="fixed inset-0 bg-muted/5 backdrop-blur-lg"
38+
@click="open = false"
39+
v-if="open"
40+
></div>
41+
</Transition>
42+
<SpeedDial v-model:items="currentItems" class="absolute mt-4 ml-4" v-model:modal="open">
43+
<template #main="{ active }">
44+
<div :class="['w-10 h-10 p-2 flex items-center justify-center rounded-full backdrop-blur-2xl backdrop-opacity-90 transition ease-in-out delay-100 outline outline-default/20 pointer-events-auto', active ? 'shadow-md hover:shadow-xl' : 'shadow-none hover:shadow-md']">
45+
<UIcon name="i-custom-n" class="text-lg" />
46+
</div>
47+
</template>
48+
</SpeedDial>
49+
</div>
50+
751
<!-- <UMain as="main"> -->
852
<NuxtLayout>
953
<NuxtPage />
@@ -33,3 +77,15 @@
3377
</UContainer>
3478
</UApp>
3579
</template>
80+
81+
<style lang="css" scoped>
82+
.v-enter-active,
83+
.v-leave-active {
84+
transition: opacity 0.5s ease;
85+
}
86+
87+
.v-enter-from,
88+
.v-leave-to {
89+
opacity: 0;
90+
}
91+
</style>

app/assets/css/main.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
@import "@nuxt/ui";
33

44
@theme static {
5-
--font-sans: "Public Sans", sans-serif;
5+
--font-sans: "IBM Plex Sans", sans-serif;
66
--font-mono: "Space Mono", monospace;
77

88
--color-green-50: #effdf5;

app/components/BatteryLoading.vue

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,152 @@
1-
<template></template>
1+
<script setup lang="ts">
2+
import { animate, createScope, createTimeline, random, randomPick, Scope, splitText, stagger } from 'animejs';
3+
4+
const props = defineProps<{
5+
complete?: boolean
6+
}>();
7+
const isComplete = props.complete ?? true;
8+
const batteryMax = isComplete ? 100 : (Math.random() * 100)
9+
10+
const model = defineModel('visible', { default: true });
11+
12+
const battery = reactive({
13+
percent: 0
14+
})
15+
const batteryLoaded = ref(false);
16+
17+
const batteryText = computed<string>(() => {
18+
return createASCIIBatteryText(battery.percent, batteryLoaded.value);
19+
})
20+
21+
const characterSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789*&^%$#@!~[]{};:,.<>?/|\\+-=';
22+
23+
const batteryEl = useTemplateRef('batt')
24+
const batteryScope = ref<Scope | undefined>(undefined)
25+
26+
onMounted(() => {
27+
batteryScope.value = createScope({ root: batteryEl.value }).add(self => {
28+
// animate random text
29+
const { words, chars } = splitText('.scrabble', {
30+
words: { wrap: "clip" },
31+
chars: '<pre class="p-2 bg-muted">{value}</pre>'
32+
})
33+
34+
function characterChangeLoop(chars: Array<HTMLElement>) {
35+
return new Promise(async (resolve) => {
36+
while (battery.percent < batteryMax) {
37+
await animate(chars, {
38+
innerText: (_, i, l) => randomPick(characterSet),
39+
duration: 500,
40+
delay: stagger(100, { from: 'random' }),
41+
});
42+
console.log('congo')
43+
}
44+
resolve(true);
45+
})
46+
}
47+
48+
// animate battery percent
49+
const batteryAnimation = animate(battery, {
50+
percent: batteryMax,
51+
duration: batteryMax * 50,
52+
easing: 'linear',
53+
})
54+
55+
const middleTwoIndexes = chars.length >= 4 ? [Math.floor((chars.length - 1) / 2), Math.ceil((chars.length - 1) / 2)] : [0, 1];
56+
const message = [':', isComplete ? ')' : '('];
57+
58+
// start both animations
59+
const timeLineA = createTimeline()
60+
.sync(batteryAnimation)
61+
.add(chars, {
62+
innerText: (_, i, l) => randomPick(characterSet),
63+
duration: 500,
64+
loop: Math.floor(batteryMax / 10),
65+
loopDelay: 100
66+
}, stagger(100, { from: 'random', start: 0 }));
67+
68+
const timelineB = createTimeline()
69+
.add('h2', {
70+
color: (_, i, l) => isComplete ? 'var(--ui-success)' : '#ffffff',
71+
loop: 3,
72+
alternate: true,
73+
duration: 500,
74+
})
75+
.add(chars, {
76+
innerText: (_, i, l) => middleTwoIndexes.includes(i) ? message[middleTwoIndexes.indexOf(i)] : ' ',
77+
}, stagger(100, { from: 'random', start: 0 }))
78+
.add('.msg', {
79+
opacity: ['0%', '100%'],
80+
duration: 1000,
81+
});
82+
83+
createTimeline()
84+
.sync(timeLineA, 0)
85+
.call(() => {
86+
console.log('timeline A complete');
87+
batteryLoaded.value = true;
88+
}, batteryMax * 50)
89+
.sync(timelineB, '<+=50').init()
90+
91+
// once percent/text is done, provide error message or success message and animate color scheme of elemnts based on complete or not
92+
93+
94+
// animate('h2', {
95+
// color: (_, i, l) => isComplete ? 'var(--ui-success)' : 'var(--ui-error)',
96+
// loop: 3,
97+
// alternate: true,
98+
// autoplay: true,
99+
// duration: 500,
100+
// });
101+
// animate(chars, {
102+
// innerText: (_, i, l) => middleTwoIndexes.includes(i) ? message[middleTwoIndexes.indexOf(i)] : ' ',
103+
// autoplay: true,
104+
// delay: stagger(100, { from: 'random' }),
105+
// })
106+
107+
108+
// // add blink text animation
109+
110+
// Promise.all([
111+
// characterChangeLoop(chars),
112+
// batteryAnimation.play()
113+
// ]).then(async () => {
114+
115+
// });
116+
117+
// type out text
118+
})
119+
})
120+
121+
</script>
122+
123+
<template>
124+
<div v-if="model" class="fixed inset-0 z-50 flex flex-col items-center justify-center bg-default pointer-events-auto font-mono font-light" ref="batt">
125+
<!-- Main text -->
126+
<h1 class="uppercase text-3xl tracking-wide py-4 font-light scrabble">Loading</h1>
127+
128+
<!-- Battery -->
129+
<div class="max-w-2xl w-full px-20 flex flex-col items-center justify-center space-y-2">
130+
<!-- TODO: Anime createAnimatable? -->
131+
<h2 class="text-2xl percentage">
132+
<span>{{ Math.floor(battery.percent) }}</span>
133+
<span>%</span>
134+
</h2>
135+
136+
<!-- The battery (needs to be big) -->
137+
<!-- TODO: Anime createAnimatable? -->
138+
<div class="flex flex-row items-center justify-center px-4 space-x-2 text-6xl">
139+
<pre
140+
v-for="(char, idx) in batteryText"
141+
:key="idx"
142+
class="font-stretch-ultra-expanded p-2 bg-muted"
143+
>{{ char }}</pre>
144+
</div>
145+
</div>
146+
147+
<!-- Optional Message and Others -->
148+
<p class="msg text-center text-sm p-8" v-if="!isComplete && batteryLoaded">
149+
The page cannot be displayed right now. It's probably under construction. Please check back again later.
150+
</p>
151+
</div>
152+
</template>

app/components/LogoOutline.vue

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

app/components/LogoOutlineAnimated.vue

Lines changed: 46 additions & 0 deletions
Large diffs are not rendered by default.

app/components/SpeedDial.vue

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,142 @@
1-
<template></template>
1+
<script setup lang="ts">
2+
import { animate, createScope, createTimeline, Scope, stagger } from "animejs";
3+
4+
const props = defineProps<{
5+
linear?: boolean;
6+
spread?: number;
7+
}>();
8+
9+
type Item = {
10+
name: string;
11+
icon: string;
12+
} & ({
13+
link: string;
14+
} | {
15+
action: () => void
16+
});
17+
18+
const modalModel = defineModel('modal', { default: false })
19+
const itemsModel = defineModel<Item[]>('items', { default: [] })
20+
21+
const spread = props.spread ?? 150;
22+
23+
const dialogAnimationFinished = ref(true);
24+
25+
const dialElement = useTemplateRef("dial");
26+
const dialScope = ref<Scope | undefined>();
27+
28+
function handleClick() {
29+
modalModel.value = !modalModel.value
30+
}
31+
32+
onMounted(() => {
33+
dialScope.value = createScope({ root: dialElement.value }).add((self) => {
34+
const dialDownAnimation = animate(".circle", {
35+
y: (_, i, l) => (spread / 3) * (l - i),
36+
ease: "inOut",
37+
delay: stagger(100, {
38+
reversed: true,
39+
}),
40+
autoplay: false,
41+
});
42+
const dialDownTextAnimation = animate('.circle-text', {
43+
x: spread / 3
44+
})
45+
46+
const dialQuarterTextAnimation = animate(".circle-text", {
47+
y: (_, i, l) => (spread * (35 / 150)) * Math.sin(((Math.PI / 2) * i) / (l - 1)),
48+
x: (_, i, l) => (spread * (55 / 150)) * Math.cos(((Math.PI / 2) * i) / (l - 1)),
49+
});
50+
const dialQuarterCircleAnimation = animate(".circle", {
51+
y: (_, i, l) => spread * Math.sin(((Math.PI / 2) * i) / (l - 1)),
52+
x: (_, i, l) => spread * Math.cos(((Math.PI / 2) * i) / (l - 1)),
53+
ease: "inOut",
54+
delay: stagger(150, {
55+
reversed: true,
56+
}),
57+
autoplay: false,
58+
});
59+
60+
const dialQuarterAnimation = createTimeline({
61+
autoplay: false,
62+
})
63+
.sync(dialQuarterCircleAnimation, 0)
64+
.sync(dialQuarterTextAnimation, 0);
65+
66+
self?.add("dialdown", (toOpen: boolean) => {
67+
dialogAnimationFinished.value = false;
68+
if (!toOpen) {
69+
dialDownAnimation.reverse();
70+
} else {
71+
dialDownAnimation.play();
72+
}
73+
dialogAnimationFinished.value = true;
74+
});
75+
76+
self?.add("dialquarter", (toOpen: boolean) => {
77+
dialogAnimationFinished.value = false;
78+
if (!toOpen) {
79+
dialQuarterAnimation.reverse();
80+
} else {
81+
dialQuarterAnimation.play();
82+
}
83+
dialogAnimationFinished.value = true;
84+
});
85+
});
86+
});
87+
88+
watch(modalModel, (newValue, oldValue) => {
89+
if (!dialScope.value) return;
90+
dialScope.value?.methods.dialquarter(newValue);
91+
})
92+
</script>
93+
94+
<template>
95+
<button
96+
class="relative h-10 w-10 pointer-events-auto z-10"
97+
ref="dial"
98+
@click="handleClick()"
99+
>
100+
<div v-for="item in itemsModel" :key="item.name" class="circle absolute inset-0 z-10">
101+
<!-- Circle + label wrapper -->
102+
<div class="relative flex items-center group">
103+
<!-- Text (behind) -->
104+
<div class="circle-text relative z-0 mt-3 text-sm whitespace-nowrap opacity-0 group-hover:opacity-100 transition ease-in-out delay-100 font-mono" v-show="modalModel && dialogAnimationFinished">
105+
{{ item.name }}
106+
</div>
107+
108+
<!-- Circle (front) -->
109+
<button
110+
v-if="'action' in item"
111+
@click="item.action()"
112+
:class="[`absolute top-0 left-0 z-10 rounded-full pointer-events-auto`]"
113+
>
114+
<slot name="item" :icon="item.icon" :name="item.name">
115+
<div class="w-10 h-10 p-2 flex items-center justify-center rounded-full backdrop-blur-xl bg-default/50 transition ease-in-out delay-200 shadow-none hover:shadow-lg outline outline-default/20 pointer-events-auto">
116+
<UIcon :name="item.icon" class="text-lg" />
117+
</div>
118+
</slot>
119+
</button>
120+
<ULink
121+
v-else
122+
:to="item.link"
123+
:external="item.link.startsWith('http')"
124+
:class="[`absolute top-0 left-0 z-10 rounded-full pointer-events-auto`]"
125+
>
126+
<slot name="item" :icon="item.icon" :name="item.name">
127+
<div class="w-10 h-10 p-2 flex items-center justify-center rounded-full backdrop-blur-lg bg-default/50 transition ease-in-out delay-100 shadow-none hover:shadow-md outline outline-default/20 pointer-events-auto">
128+
<UIcon :name="item.icon" class="text-lg" />
129+
</div>
130+
</slot>
131+
</ULink>
132+
</div>
133+
</div>
134+
135+
<!-- Center button -->
136+
<div class="absolute inset-0 rounded-full z-20">
137+
<slot name="main" :active="modalModel">
138+
<UAvatar icon="i-lucide-image" size="xl" />
139+
</slot>
140+
</div>
141+
</button>
142+
</template>

0 commit comments

Comments
 (0)