style: mobile responsive
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<title>アイリ</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0" />
|
||||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@airi-proj/elevenlabs": "workspace:^",
|
"@airi-proj/elevenlabs": "workspace:^",
|
||||||
"@iconify-json/carbon": "^1.2.4",
|
"@iconify-json/carbon": "^1.2.4",
|
||||||
|
"@iconify-json/eos-icons": "^1.2.1",
|
||||||
"@intlify/unplugin-vue-i18n": "^6.0.0",
|
"@intlify/unplugin-vue-i18n": "^6.0.0",
|
||||||
"@shikijs/markdown-it": "^1.24.0",
|
"@shikijs/markdown-it": "^1.24.0",
|
||||||
"@types/markdown-it-link-attributes": "^3.0.5",
|
"@types/markdown-it-link-attributes": "^3.0.5",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Application } from '@pixi/app'
|
import { Application } from '@pixi/app'
|
||||||
import { extensions } from '@pixi/extensions'
|
import { extensions } from '@pixi/extensions'
|
||||||
import { Ticker, TickerPlugin } from '@pixi/ticker'
|
import { Ticker, TickerPlugin } from '@pixi/ticker'
|
||||||
import { useElementBounding, useWindowSize } from '@vueuse/core'
|
import { breakpointsTailwind, useBreakpoints, useElementBounding, useWindowSize } from '@vueuse/core'
|
||||||
import { Live2DModel, MotionPreloadStrategy, MotionPriority } from 'pixi-live2d-display/cubism4'
|
import { Live2DModel, MotionPreloadStrategy, MotionPriority } from 'pixi-live2d-display/cubism4'
|
||||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
@@ -21,10 +21,37 @@ const mouthOpenSize = computed(() => {
|
|||||||
return Math.max(0, Math.min(100, props.mouthOpenSize))
|
return Math.max(0, Math.min(100, props.mouthOpenSize))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||||
const { width, height } = useWindowSize()
|
const { width, height } = useWindowSize()
|
||||||
const containerElementBounding = useElementBounding(containerRef)
|
const containerElementBounding = useElementBounding(containerRef)
|
||||||
const containerParentElementBounding = useElementBounding(containerRef.value?.parentElement)
|
const containerParentElementBounding = useElementBounding(containerRef.value?.parentElement)
|
||||||
|
|
||||||
|
const isMobile = computed(() => breakpoints.between('sm', 'md').value || breakpoints.smaller('sm').value)
|
||||||
|
const isTablet = computed(() => breakpoints.between('md', 'lg').value)
|
||||||
|
const isDesktop = computed(() => breakpoints.greaterOrEqual('lg').value)
|
||||||
|
|
||||||
|
const canvasWidth = computed(() => {
|
||||||
|
if (isDesktop.value)
|
||||||
|
return containerElementBounding.width.value
|
||||||
|
else if (isMobile.value)
|
||||||
|
return (width.value - 16) // padding
|
||||||
|
else if (isTablet.value)
|
||||||
|
return (width.value - 16) // padding
|
||||||
|
else
|
||||||
|
return containerElementBounding.width.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const canvasHeight = computed(() => {
|
||||||
|
if (isDesktop.value)
|
||||||
|
return Math.max(600, containerParentElementBounding.height.value)
|
||||||
|
else if (isMobile.value)
|
||||||
|
return 600
|
||||||
|
else if (isTablet.value)
|
||||||
|
return 600
|
||||||
|
else
|
||||||
|
return Math.max(600, containerParentElementBounding.height.value)
|
||||||
|
})
|
||||||
|
|
||||||
function getCoreModel() {
|
function getCoreModel() {
|
||||||
return model.value!.internalModel.coreModel as any
|
return model.value!.internalModel.coreModel as any
|
||||||
}
|
}
|
||||||
@@ -38,19 +65,20 @@ async function initLive2DPixiStage(parent: HTMLDivElement) {
|
|||||||
extensions.add(TickerPlugin)
|
extensions.add(TickerPlugin)
|
||||||
|
|
||||||
pixiApp.value = new Application({
|
pixiApp.value = new Application({
|
||||||
width: containerElementBounding.width.value,
|
width: canvasWidth.value,
|
||||||
height: Math.max(600, containerParentElementBounding.height.value),
|
height: canvasHeight.value,
|
||||||
backgroundAlpha: 0,
|
backgroundAlpha: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
pixiAppCanvas.value = pixiApp.value.view
|
pixiAppCanvas.value = pixiApp.value.view
|
||||||
|
pixiAppCanvas.value.style.objectFit = 'contain'
|
||||||
parent.appendChild(pixiApp.value.view)
|
parent.appendChild(pixiApp.value.view)
|
||||||
|
|
||||||
model.value = await Live2DModel.from(props.model, { motionPreload: MotionPreloadStrategy.ALL })
|
model.value = await Live2DModel.from(props.model, { motionPreload: MotionPreloadStrategy.ALL })
|
||||||
pixiApp.value.stage.addChild(model.value as any)
|
pixiApp.value.stage.addChild(model.value as any)
|
||||||
|
|
||||||
model.value.x = containerElementBounding.width.value / 2
|
model.value.x = canvasWidth.value / 2
|
||||||
model.value.y = Math.max(600, containerParentElementBounding.height.value)
|
model.value.y = canvasHeight.value
|
||||||
model.value.rotation = Math.PI
|
model.value.rotation = Math.PI
|
||||||
model.value.skew.x = Math.PI
|
model.value.skew.x = Math.PI
|
||||||
model.value.scale.set(0.3, 0.3)
|
model.value.scale.set(0.3, 0.3)
|
||||||
@@ -69,18 +97,18 @@ async function setMotion(motionName: string) {
|
|||||||
await model.value!.motion(motionName, undefined, MotionPriority.FORCE)
|
await model.value!.motion(motionName, undefined, MotionPriority.FORCE)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([width, height], () => {
|
watch([width, height, canvasWidth, canvasHeight], () => {
|
||||||
if (pixiApp.value)
|
if (pixiApp.value)
|
||||||
pixiApp.value.renderer.resize((width.value - 16) / 2, 550)
|
pixiApp.value.renderer.resize(canvasWidth.value, canvasHeight.value)
|
||||||
|
|
||||||
if (pixiAppCanvas.value) {
|
if (pixiAppCanvas.value) {
|
||||||
pixiAppCanvas.value.width = (width.value - 16) / 2
|
pixiAppCanvas.value.width = canvasWidth.value
|
||||||
pixiAppCanvas.value.height = Math.max(600, containerParentElementBounding.height.value)
|
pixiAppCanvas.value.height = canvasHeight.value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.value) {
|
if (model.value) {
|
||||||
model.value.x = (width.value - 16) / 4
|
model.value.x = canvasWidth.value / 2
|
||||||
model.value.y = Math.max(600, containerParentElementBounding.height.value)
|
model.value.y = canvasHeight.value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -105,5 +133,5 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="containerRef" h-full w-full />
|
<div ref="containerRef" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -273,15 +273,25 @@ onUnmounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div max-h="[100vh]" h-full p="2" flex="~ col">
|
<div max-h="[100vh]" h-full p="2" flex="~ col">
|
||||||
<Settings />
|
<Settings />
|
||||||
<div flex="~ row 1" w-full items-end space-x-2>
|
<div flex="~ row 1" relative w-full items-end gap-2>
|
||||||
<div w-full min-h="100 sm:100">
|
<div w="50%" min-w="50% <lg:full" min-h="100 sm:100" flex-1>
|
||||||
<Live2DViewer ref="live2DViewerRef" :mouth-open-size="mouthOpenSize" model="/assets/live2d/models/hiyori_pro_zh/runtime/hiyori_pro_t11.model3.json" />
|
<Live2DViewer
|
||||||
|
ref="live2DViewerRef"
|
||||||
|
:mouth-open-size="mouthOpenSize"
|
||||||
|
model="/assets/live2d/models/hiyori_pro_zh/runtime/hiyori_pro_t11.model3.json"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div my="2" w-full space-y-2 max-h="[calc(100vh-117px)]">
|
<div
|
||||||
|
class="relative <lg:(absolute bottom-0 from-zinc-800/80 to-zinc-800/0 bg-gradient-to-t p-2)"
|
||||||
|
px="<sm:2" py="<sm:2" rounded="<sm:lg"
|
||||||
|
w="50% <lg:full" flex="~ col 1" gap-2 max-h="[calc(100vh-117px)]"
|
||||||
|
>
|
||||||
<div v-for="(message, index) in messages" :key="index">
|
<div v-for="(message, index) in messages" :key="index">
|
||||||
<div v-if="message.role === 'assistant'" flex mr="12">
|
<div v-if="message.role === 'assistant'" flex mr="12">
|
||||||
<div
|
<div
|
||||||
mr-2 h-10 min-h-10 min-w-10 w-10 overflow-hidden rounded-full
|
mr-2 h-10
|
||||||
|
min-h-10 min-w-10 w-10
|
||||||
|
overflow-hidden rounded-full
|
||||||
border="solid 3"
|
border="solid 3"
|
||||||
transition="all ease-in-out" duration-100
|
transition="all ease-in-out" duration-100
|
||||||
:style="{
|
:style="{
|
||||||
@@ -290,22 +300,44 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<img :src="Avatar">
|
<img :src="Avatar">
|
||||||
</div>
|
</div>
|
||||||
<div flex="~ col" bg="pink-50/50 dark:pink-900/50" p="2" border="2 solid pink/10" rounded-lg>
|
<div
|
||||||
|
flex="~ col"
|
||||||
|
bg="pink-50 dark:pink-900"
|
||||||
|
p="2"
|
||||||
|
border="2 solid pink dark:pink-700"
|
||||||
|
rounded-lg
|
||||||
|
h="unset <sm:fit"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<span font-semibold>Neuro</span>
|
<span font-semibold class="inline <sm:hidden">Neuro</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-html="process(message.content as string)" />
|
<div v-if="message.content" text="base <sm:xs" v-html="process(message.content as string)" />
|
||||||
|
<div v-else i-eos-icons:three-dots-loading />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="message.role === 'user'" flex="~ row-reverse" ml="12">
|
<div v-else-if="message.role === 'user'" flex="~ row-reverse" ml="12">
|
||||||
<div border="purple solid 3" ml="2" h-10 min-h-10 min-w-10 w-10 overflow-hidden rounded-full>
|
<div
|
||||||
|
class="block <sm:hidden"
|
||||||
|
border="purple solid 3"
|
||||||
|
ml="2"
|
||||||
|
h-10 min-h-10 min-w-10 w-10
|
||||||
|
overflow-hidden rounded-full
|
||||||
|
>
|
||||||
<div i-carbon:user-avatar-filled text="purple" h-full w-full p="0" m="0" />
|
<div i-carbon:user-avatar-filled text="purple" h-full w-full p="0" m="0" />
|
||||||
</div>
|
</div>
|
||||||
<div flex="~ col" bg="purple-50/50 dark:purple-900/50" p="2" border="2 solid pink/10" rounded-lg>
|
<div
|
||||||
|
flex="~ col"
|
||||||
|
bg="purple-50 dark:purple-900"
|
||||||
|
p="2"
|
||||||
|
border="2 solid purple dark:purple-700"
|
||||||
|
rounded-lg
|
||||||
|
h="unset <sm:fit"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<span font-semibold>You</span>
|
<span font-semibold class="inline <sm:hidden">You</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-html="process(message.content as string)" />
|
<div v-if="message.content" text="base <sm:xs" v-html="process(message.content as string)" />
|
||||||
|
<div v-else />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -329,10 +361,10 @@ onUnmounted(() => {
|
|||||||
{{ 'name' in m ? `${m.name} (${m.id})` : m.id }}
|
{{ 'name' in m ? `${m.name} (${m.id})` : m.id }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div absolute bottom="5" left="50%" translate-x="-50%">
|
<div fixed bottom="5" left="50%" translate-x="-50%">
|
||||||
<button
|
<button
|
||||||
bg="zinc-100 dark:zinc-700" flex="~ row"
|
bg="zinc-100 dark:zinc-700" flex="~ row"
|
||||||
items-center rounded-full px-4 py-2
|
items-center rounded-full px-4 py-2 border="2 solid zinc-500/50"
|
||||||
transition="all ease-in-out"
|
transition="all ease-in-out"
|
||||||
@click="listening = !listening"
|
@click="listening = !listening"
|
||||||
>
|
>
|
||||||
|
|||||||
Generated
+10
@@ -223,6 +223,9 @@ importers:
|
|||||||
'@iconify-json/carbon':
|
'@iconify-json/carbon':
|
||||||
specifier: ^1.2.4
|
specifier: ^1.2.4
|
||||||
version: 1.2.4
|
version: 1.2.4
|
||||||
|
'@iconify-json/eos-icons':
|
||||||
|
specifier: ^1.2.1
|
||||||
|
version: 1.2.1
|
||||||
'@intlify/unplugin-vue-i18n':
|
'@intlify/unplugin-vue-i18n':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0(@vue/compiler-dom@3.5.13)(eslint@9.16.0(jiti@2.4.0))(rollup@2.79.1)(typescript@5.7.2)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))
|
version: 6.0.0(@vue/compiler-dom@3.5.13)(eslint@9.16.0(jiti@2.4.0))(rollup@2.79.1)(typescript@5.7.2)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))
|
||||||
@@ -1683,6 +1686,9 @@ packages:
|
|||||||
'@iconify-json/carbon@1.2.4':
|
'@iconify-json/carbon@1.2.4':
|
||||||
resolution: {integrity: sha512-DhW2jjMVGwV0DLHc0cmDYohdtGxMra8UuwgjHrryPy+rQX4gXhJwCBBVP2h2UG/92AoRCTn7zUJve4WvY5MLYg==}
|
resolution: {integrity: sha512-DhW2jjMVGwV0DLHc0cmDYohdtGxMra8UuwgjHrryPy+rQX4gXhJwCBBVP2h2UG/92AoRCTn7zUJve4WvY5MLYg==}
|
||||||
|
|
||||||
|
'@iconify-json/eos-icons@1.2.1':
|
||||||
|
resolution: {integrity: sha512-nqucN9E670dRHDk5osNNObhxQg/eBI8wvYA+zWaOMsJR0g8VodF9z/cpDvMH7YyUm4elOF9r8cGIj3byFBFLRQ==}
|
||||||
|
|
||||||
'@iconify/types@2.0.0':
|
'@iconify/types@2.0.0':
|
||||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||||
|
|
||||||
@@ -8170,6 +8176,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
|
'@iconify-json/eos-icons@1.2.1':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
'@iconify/types@2.0.0': {}
|
'@iconify/types@2.0.0': {}
|
||||||
|
|
||||||
'@iconify/utils@2.1.33':
|
'@iconify/utils@2.1.33':
|
||||||
|
|||||||
Reference in New Issue
Block a user