Skip to content

Commit

Permalink
feat: wave & fix minor issues
Browse files Browse the repository at this point in the history
  • Loading branch information
nekomeowww committed Dec 28, 2024
1 parent 5a4e391 commit 4564c67
Show file tree
Hide file tree
Showing 10 changed files with 384 additions and 15 deletions.
175 changes: 175 additions & 0 deletions packages/stage/src/components/Backgrounds/Wave.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
import { onMounted, onUnmounted, ref, watch } from 'vue'
interface WaveProps {
verticalOffset?: number // Vertical offset of the wave in pixels
height?: number // Height of the wave in pixels
amplitude?: number // Wave height variation in pixels
waveLength?: number // Length of one wave cycle in pixels
fillColor?: string // Fill color of the wave
direction?: 'up' | 'down'// Direction of the wave: 'up' or 'down'
animationSpeed?: number // Speed of the wave animation in pixels per frame
}
const props = withDefaults(defineProps<WaveProps>(), {
verticalOffset: 20,
height: 40,
amplitude: 14,
waveLength: 250,
fillColor: '#f8e8f2',
direction: 'down',
animationSpeed: 0.5,
})
// Use either provided waves or defaults
// Refs
const container = ref<HTMLElement | null>(null)
const svg = ref<SVGSVGElement | null>(null)
// Reactive Variables
const svgWidth = ref(0)
const waveHeight = ref(props.height)
const waveAmplitude = ref(props.amplitude)
const waveLength = ref(props.waveLength)
const wavePath = ref('')
const waveFillColor = ref(props.fillColor)
const direction = ref<'up' | 'down'>(props.direction)
// Function to generate the SVG sine wave path
function generateSineWavePath(
width: number,
height: number,
amplitude: number,
waveLength: number,
direction: 'up' | 'down',
): string {
const points: string[] = []
// Calculate the number of complete waves to fill the SVG width
const numberOfWaves = Math.ceil(width / waveLength)
// Total width covered by all complete waves
const totalWavesWidth = numberOfWaves * waveLength
// Step size in pixels for generating points (1px for precision)
const step = 1
// Determine base Y position based on direction
const baseY = direction === 'up' ? height - amplitude : amplitude
// Start the path at the base Y position
points.push(`M 0 ${baseY}`)
// Generate points for the sine wave
for (let x = 0; x <= totalWavesWidth; x += step) {
const y = direction === 'up'
? baseY - amplitude * Math.sin((2 * Math.PI * x) / waveLength)
: baseY + amplitude * Math.sin((2 * Math.PI * x) / waveLength)
points.push(`L ${x} ${y}`)
}
// Close the path for filling
if (direction === 'up') {
points.push(`L ${totalWavesWidth} ${height}`)
points.push(`L 0 ${height} Z`)
}
else {
points.push(`L ${totalWavesWidth} 0`)
points.push(`L 0 0 Z`)
}
return points.join(' ')
}
// Function to handle container resize
function handleResize() {
if (container.value) {
const width = container.value.clientWidth
svgWidth.value = width
// Calculate the number of waves needed to cover twice the container width
const numberOfWaves = Math.ceil((width * 2) / waveLength.value)
// Total width is exact multiple of waveLength
const totalWavesWidth = numberOfWaves * waveLength.value
// Generate wave path based on the exact total width
wavePath.value = generateSineWavePath(
totalWavesWidth,
waveHeight.value,
waveAmplitude.value,
waveLength.value,
direction.value,
)
// Update SVG width to match the exact multiple
svg.value?.setAttribute('width', totalWavesWidth.toString())
}
}
// Animation Variables
let animationFrameId: number
const animationSpeed = ref(props.animationSpeed)
const animationPosition = ref(0)
// Function to animate the wave
function animateWave() {
animationPosition.value -= animationSpeed.value
if (Math.abs(animationPosition.value) >= waveLength.value) {
animationPosition.value += waveLength.value
}
if (svg.value) {
svg.value.style.transform = `translateX(${animationPosition.value}px)`
}
animationFrameId = requestAnimationFrame(animateWave)
}
watch(
() => [props.height, props.amplitude, props.waveLength, props.fillColor, props.direction],
() => {
waveHeight.value = props.height!
waveAmplitude.value = props.amplitude!
waveLength.value = props.waveLength!
waveFillColor.value = props.fillColor!
direction.value = props.direction!
handleResize() // Regenerate wave path on prop changes
},
{ immediate: true },
)
useEventListener('resize', handleResize)
// Setup on mount
onMounted(() => {
handleResize() // Initial wave generation
animateWave() // Start animation for wave1
})
// Cleanup on unmount
onUnmounted(() => {
cancelAnimationFrame(animationFrameId)
})
</script>

<template>
<div class="relative">
<slot />
<div ref="container" absolute left-0 right-0 top-0 w-full overflow-hidden>
<div v-if="direction === 'down'" :style="{ backgroundColor: waveFillColor, height: `${waveHeight}px` }" w-full />
<svg
ref="svg"
:width="waveLength * Math.ceil((svgWidth * 2) / waveLength)"
:height="waveHeight"
:viewBox="`0 0 ${waveLength * Math.ceil((svgWidth * 2) / waveLength)} ${waveHeight}`"
xmlns="http://www.w3.org/2000/svg"
h="[100%]" w="[200%]"
:style="{ willChange: 'transform' }"
>
<path :d="wavePath" :fill="waveFillColor" />
</svg>
<div v-if="direction === 'up'" :style="{ backgroundColor: waveFillColor, height: `${waveHeight}px` }" w-full />
</div>
</div>
</template>
175 changes: 175 additions & 0 deletions packages/stage/src/components/Layouts/AnimatedBackground.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
import { onMounted, onUnmounted, ref, watch } from 'vue'
interface WaveProps {
verticalOffset?: number // Vertical offset of the wave in pixels
height?: number // Height of the wave in pixels
amplitude?: number // Wave height variation in pixels
waveLength?: number // Length of one wave cycle in pixels
fillColor?: string // Fill color of the wave
direction?: 'up' | 'down'// Direction of the wave: 'up' or 'down'
animationSpeed?: number // Speed of the wave animation in pixels per frame
}
const props = withDefaults(defineProps<WaveProps>(), {
verticalOffset: 20,
height: 40,
amplitude: 14,
waveLength: 250,
fillColor: '#f8e8f2',
direction: 'down',
animationSpeed: 0.5,
})
// Use either provided waves or defaults
// Refs
const container = ref<HTMLElement | null>(null)
const svg = ref<SVGSVGElement | null>(null)
// Reactive Variables
const svgWidth = ref(0)
const waveHeight = ref(props.height)
const waveAmplitude = ref(props.amplitude)
const waveLength = ref(props.waveLength)
const wavePath = ref('')
const waveFillColor = ref(props.fillColor)
const direction = ref<'up' | 'down'>(props.direction)
// Function to generate the SVG sine wave path
function generateSineWavePath(
width: number,
height: number,
amplitude: number,
waveLength: number,
direction: 'up' | 'down',
): string {
const points: string[] = []
// Calculate the number of complete waves to fill the SVG width
const numberOfWaves = Math.ceil(width / waveLength)
// Total width covered by all complete waves
const totalWavesWidth = numberOfWaves * waveLength
// Step size in pixels for generating points (1px for precision)
const step = 1
// Determine base Y position based on direction
const baseY = direction === 'up' ? height - amplitude : amplitude
// Start the path at the base Y position
points.push(`M 0 ${baseY}`)
// Generate points for the sine wave
for (let x = 0; x <= totalWavesWidth; x += step) {
const y = direction === 'up'
? baseY - amplitude * Math.sin((2 * Math.PI * x) / waveLength)
: baseY + amplitude * Math.sin((2 * Math.PI * x) / waveLength)
points.push(`L ${x} ${y}`)
}
// Close the path for filling
if (direction === 'up') {
points.push(`L ${totalWavesWidth} ${height}`)
points.push(`L 0 ${height} Z`)
}
else {
points.push(`L ${totalWavesWidth} 0`)
points.push(`L 0 0 Z`)
}
return points.join(' ')
}
// Function to handle container resize
function handleResize() {
if (container.value) {
const width = container.value.clientWidth
svgWidth.value = width
// Calculate the number of waves needed to cover twice the container width
const numberOfWaves = Math.ceil((width * 2) / waveLength.value)
// Total width is exact multiple of waveLength
const totalWavesWidth = numberOfWaves * waveLength.value
// Generate wave path based on the exact total width
wavePath.value = generateSineWavePath(
totalWavesWidth,
waveHeight.value,
waveAmplitude.value,
waveLength.value,
direction.value,
)
// Update SVG width to match the exact multiple
svg.value?.setAttribute('width', totalWavesWidth.toString())
}
}
// Animation Variables
let animationFrameId: number
const animationSpeed = ref(props.animationSpeed)
const animationPosition = ref(0)
// Function to animate the wave
function animateWave() {
animationPosition.value -= animationSpeed.value
if (Math.abs(animationPosition.value) >= waveLength.value) {
animationPosition.value += waveLength.value
}
if (svg.value) {
svg.value.style.transform = `translateX(${animationPosition.value}px)`
}
animationFrameId = requestAnimationFrame(animateWave)
}
watch(
() => [props.height, props.amplitude, props.waveLength, props.fillColor, props.direction],
() => {
waveHeight.value = props.height!
waveAmplitude.value = props.amplitude!
waveLength.value = props.waveLength!
waveFillColor.value = props.fillColor!
direction.value = props.direction!
handleResize() // Regenerate wave path on prop changes
},
{ immediate: true },
)
useEventListener('resize', handleResize)
// Setup on mount
onMounted(() => {
handleResize() // Initial wave generation
animateWave() // Start animation for wave1
})
// Cleanup on unmount
onUnmounted(() => {
cancelAnimationFrame(animationFrameId)
})
</script>

<template>
<div class="relative">
<slot />
<div ref="container" absolute left-0 right-0 top-0 w-full overflow-hidden>
<div v-if="direction === 'down'" :style="{ backgroundColor: waveFillColor, height: `${waveHeight}px` }" w-full />
<svg
ref="svg"
:width="waveLength * Math.ceil((svgWidth * 2) / waveLength)"
:height="waveHeight"
:viewBox="`0 0 ${waveLength * Math.ceil((svgWidth * 2) / waveLength)} ${waveHeight}`"
xmlns="http://www.w3.org/2000/svg"
h="[100%]" w="[200%]"
:style="{ willChange: 'transform' }"
>
<path :d="wavePath" :fill="waveFillColor" />
</svg>
<div v-if="direction === 'up'" :style="{ backgroundColor: waveFillColor, height: `${waveHeight}px` }" w-full />
</div>
</div>
</template>
2 changes: 1 addition & 1 deletion packages/stage/src/components/Layouts/InteractiveArea.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ onAfterSend(async () => {
</script>

<template>
<div flex="col" h-full w-full items-center pt-4>
<div flex="col" items-center pt-4>
<fieldset flex="~ row" w-fit rounded-lg>
<label
:class="[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ onMounted(() => {
</script>

<template>
<div w-full flex gap-1>
<div gap-1>
<div flex flex-1>
<BasicTextarea
v-model="messageInput"
Expand Down
4 changes: 2 additions & 2 deletions packages/stage/src/components/Live2D/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function setScale(model: Ref<Live2DModel<InternalModel> | undefined>) {
let offsetFactor = 2.2
if (isMobile.value) {
offsetFactor = 2.5
offsetFactor = 2.2
}
const heightScale = height.value * 0.95 / initialModelHeight.value * offsetFactor
Expand Down Expand Up @@ -103,7 +103,7 @@ const handleResize = useDebounceFn(() => {
model.value.y = height.value
setScale(model)
}
}, 500)
}, 100)
watch([width, height], () => {
handleResize()
Expand Down
2 changes: 1 addition & 1 deletion packages/stage/src/components/Scenes/Live2D.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ defineExpose({
</label>
</div>
<TransitionVertical>
<div v-if="show" absolute w-full top="10" min-w="50vw" z="<md:20">
<div v-if="show" absolute w-full min-w="50vw" z="<md:20" class="<md:bottom-10 <md:right-0">
<div bg="zinc-200/20 dark:black/20" rounded-lg p-2 backdrop-blur-sm>
<div font-mono>
<span>Emotions</span>
Expand Down
Loading

0 comments on commit 4564c67

Please sign in to comment.