2026-01-15 20:10:19 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="latent-space-viz">
|
2026-02-06 03:34:50 +08:00
|
|
|
|
<div class="viz-card">
|
|
|
|
|
|
<!-- Left: The Output (Pixel Space) -->
|
|
|
|
|
|
<div class="preview-section">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="emoji-display"
|
|
|
|
|
|
:style="{ transform: `scale(${1 + zoomLevel})` }"
|
|
|
|
|
|
>
|
2026-02-06 03:34:50 +08:00
|
|
|
|
{{ currentEmoji }}
|
2026-01-15 20:10:19 +08:00
|
|
|
|
</div>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="label">
|
|
|
|
|
|
像素空间 (Pixel Space)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="sub-label">
|
|
|
|
|
|
最终看到的图像
|
|
|
|
|
|
</div>
|
2026-02-06 03:34:50 +08:00
|
|
|
|
</div>
|
2026-01-15 20:10:19 +08:00
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
<!-- Center: The Mechanism -->
|
|
|
|
|
|
<div class="connection">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="arrow">
|
|
|
|
|
|
← 映射 →
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="vae-tag">
|
|
|
|
|
|
VAE Decoder
|
|
|
|
|
|
</div>
|
2026-02-06 03:34:50 +08:00
|
|
|
|
</div>
|
2026-01-15 20:10:19 +08:00
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
<!-- Right: The Input (Latent Space) -->
|
|
|
|
|
|
<div class="control-section">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
ref="gridRef"
|
|
|
|
|
|
class="latent-grid"
|
|
|
|
|
|
@mousedown="startDrag"
|
|
|
|
|
|
@touchstart="startDrag"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="grid-lines" />
|
|
|
|
|
|
<div class="axis-label x-axis">
|
|
|
|
|
|
开心值 (Happiness)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="axis-label y-axis">
|
|
|
|
|
|
惊讶值 (Surprise)
|
|
|
|
|
|
</div>
|
2026-02-06 03:34:50 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- The Latent Point -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="latent-point"
|
|
|
|
|
|
:style="{ left: `${point.x}%`, top: `${point.y}%` }"
|
|
|
|
|
|
>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="tooltip">
|
|
|
|
|
|
Latent Vector: [{{ ((point.x-50)/50).toFixed(1) }}, {{ ((50-point.y)/50).toFixed(1) }}]
|
|
|
|
|
|
</div>
|
2026-01-15 20:10:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="label">
|
|
|
|
|
|
潜空间 (Latent Space)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="sub-label">
|
|
|
|
|
|
拖动红点改变特征
|
|
|
|
|
|
</div>
|
2026-01-15 20:10:19 +08:00
|
|
|
|
</div>
|
2026-02-06 03:34:50 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="info-bar">
|
|
|
|
|
|
<span class="icon">💡</span>
|
|
|
|
|
|
<span>
|
|
|
|
|
|
<strong>核心原理:</strong>
|
|
|
|
|
|
在像素空间里修改图片很难(要改几千个像素)。但在潜空间里,我们只需要修改两个坐标(开心值、惊讶值),就能生成完全不同的表情。这就是 AI "画画" 的本质——在数学空间里找坐标。
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-01-15 20:10:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-02-06 03:34:50 +08:00
|
|
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
const gridRef = ref(null)
|
|
|
|
|
|
const isDragging = ref(false)
|
|
|
|
|
|
const point = ref({ x: 50, y: 50 }) // Percentage 0-100
|
|
|
|
|
|
const zoomLevel = ref(0)
|
|
|
|
|
|
|
|
|
|
|
|
// Emoji map based on quadrants
|
|
|
|
|
|
// X: Unhappy -> Happy
|
|
|
|
|
|
// Y: Calm -> Surprised (Top is 0 in CSS, so small Y is high surprise?)
|
|
|
|
|
|
// Let's map:
|
|
|
|
|
|
// X (0-100): Sad -> Happy
|
|
|
|
|
|
// Y (0-100): Surprised -> Sleepy (Top is 0, so 0 is Surprised, 100 is Sleepy)
|
|
|
|
|
|
|
|
|
|
|
|
const currentEmoji = computed(() => {
|
|
|
|
|
|
const x = point.value.x // 0 (Sad) to 100 (Happy)
|
|
|
|
|
|
const y = point.value.y // 0 (Surprised) to 100 (Sleepy)
|
|
|
|
|
|
|
|
|
|
|
|
if (x < 33) { // Sad Zone
|
|
|
|
|
|
if (y < 33) return '😨' // Sad + Surprised = Fear
|
|
|
|
|
|
if (y > 66) return '😪' // Sad + Sleepy = Tired
|
|
|
|
|
|
return '😢' // Just Sad
|
|
|
|
|
|
} else if (x > 66) { // Happy Zone
|
|
|
|
|
|
if (y < 33) return '🤩' // Happy + Surprised = Starstruck
|
|
|
|
|
|
if (y > 66) return '😌' // Happy + Sleepy = Relieved
|
|
|
|
|
|
return '😃' // Just Happy
|
|
|
|
|
|
} else { // Neutral Zone
|
|
|
|
|
|
if (y < 33) return '😮' // Neutral + Surprised
|
|
|
|
|
|
if (y > 66) return '😴' // Neutral + Sleepy
|
|
|
|
|
|
return '😐' // Just Neutral
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
2026-02-06 03:34:50 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const handleMove = (event) => {
|
|
|
|
|
|
if (!isDragging.value) return
|
|
|
|
|
|
|
|
|
|
|
|
const grid = gridRef.value.getBoundingClientRect()
|
|
|
|
|
|
const clientX = event.touches ? event.touches[0].clientX : event.clientX
|
|
|
|
|
|
const clientY = event.touches ? event.touches[0].clientY : event.clientY
|
|
|
|
|
|
|
|
|
|
|
|
let newX = ((clientX - grid.left) / grid.width) * 100
|
|
|
|
|
|
let newY = ((clientY - grid.top) / grid.height) * 100
|
|
|
|
|
|
|
|
|
|
|
|
// Clamp
|
|
|
|
|
|
newX = Math.max(0, Math.min(100, newX))
|
|
|
|
|
|
newY = Math.max(0, Math.min(100, newY))
|
|
|
|
|
|
|
|
|
|
|
|
point.value = { x: newX, y: newY }
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
const startDrag = (event) => {
|
|
|
|
|
|
isDragging.value = true
|
|
|
|
|
|
handleMove(event)
|
|
|
|
|
|
// Prevent default to stop scrolling on mobile
|
|
|
|
|
|
if (event.type === 'touchstart') event.preventDefault()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const stopDrag = () => {
|
|
|
|
|
|
isDragging.value = false
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
2026-02-06 03:34:50 +08:00
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
window.addEventListener('mousemove', handleMove)
|
|
|
|
|
|
window.addEventListener('mouseup', stopDrag)
|
|
|
|
|
|
window.addEventListener('touchmove', handleMove)
|
|
|
|
|
|
window.addEventListener('touchend', stopDrag)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
window.removeEventListener('mousemove', handleMove)
|
|
|
|
|
|
window.removeEventListener('mouseup', stopDrag)
|
|
|
|
|
|
window.removeEventListener('touchmove', handleMove)
|
|
|
|
|
|
window.removeEventListener('touchend', stopDrag)
|
|
|
|
|
|
})
|
2026-01-15 20:10:19 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.latent-space-viz {
|
|
|
|
|
|
margin: 20px 0;
|
2026-02-06 03:34:50 +08:00
|
|
|
|
font-family: var(--vp-font-family-base);
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.viz-card {
|
2026-01-15 20:10:19 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-02-06 03:34:50 +08:00
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 24px;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
gap: 20px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.preview-section, .control-section {
|
2026-01-15 20:10:19 +08:00
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
2026-02-06 03:34:50 +08:00
|
|
|
|
flex-direction: column;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
align-items: center;
|
2026-02-06 03:34:50 +08:00
|
|
|
|
min-width: 200px;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.emoji-display {
|
|
|
|
|
|
font-size: 80px;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
height: 100px;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-02-06 03:34:50 +08:00
|
|
|
|
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
|
|
|
|
cursor: default;
|
|
|
|
|
|
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.1));
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.latent-grid {
|
|
|
|
|
|
width: 200px;
|
|
|
|
|
|
height: 200px;
|
|
|
|
|
|
background: var(--vp-c-bg);
|
|
|
|
|
|
border: 2px solid var(--vp-c-brand);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
cursor: crosshair;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
box-shadow: inset 0 2px 8px rgba(0,0,0,0.05);
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.grid-lines {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
2026-02-06 03:34:50 +08:00
|
|
|
|
background-image:
|
|
|
|
|
|
linear-gradient(var(--vp-c-divider) 1px, transparent 1px),
|
|
|
|
|
|
linear-gradient(90deg, var(--vp-c-divider) 1px, transparent 1px);
|
|
|
|
|
|
background-size: 20px 20px;
|
|
|
|
|
|
opacity: 0.3;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.latent-point {
|
|
|
|
|
|
width: 20px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
background: var(--vp-c-brand);
|
|
|
|
|
|
border: 3px solid #fff;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
|
|
|
|
transition: transform 0.1s;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.latent-point:hover {
|
|
|
|
|
|
transform: translate(-50%, -50%) scale(1.2);
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.tooltip {
|
2026-01-15 20:10:19 +08:00
|
|
|
|
position: absolute;
|
2026-02-06 03:34:50 +08:00
|
|
|
|
bottom: 25px;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
background: rgba(0,0,0,0.8);
|
2026-01-15 20:10:19 +08:00
|
|
|
|
color: #fff;
|
|
|
|
|
|
padding: 4px 8px;
|
2026-02-06 03:34:50 +08:00
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transition: opacity 0.2s;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.latent-point:hover .tooltip,
|
|
|
|
|
|
.latent-point:active .tooltip {
|
|
|
|
|
|
opacity: 1;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.axis-label {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
pointer-events: none;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.x-axis {
|
|
|
|
|
|
bottom: 4px;
|
|
|
|
|
|
right: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.y-axis {
|
|
|
|
|
|
top: 8px;
|
|
|
|
|
|
left: 8px;
|
|
|
|
|
|
writing-mode: vertical-rl;
|
|
|
|
|
|
transform: rotate(180deg);
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.connection {
|
2026-01-15 20:10:19 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
2026-02-06 03:34:50 +08:00
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
font-size: 12px;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.arrow {
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
font-weight: bold;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.vae-tag {
|
|
|
|
|
|
background: var(--vp-c-brand-dimm);
|
|
|
|
|
|
color: var(--vp-c-brand-dark);
|
|
|
|
|
|
padding: 2px 8px;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
border-radius: 4px;
|
2026-02-06 03:34:50 +08:00
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.label {
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--vp-c-text-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sub-label {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-bar {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
background: var(--vp-c-bg-alt);
|
|
|
|
|
|
padding: 12px 16px;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-02-06 03:34:50 +08:00
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
2026-01-15 20:10:19 +08:00
|
|
|
|
display: flex;
|
2026-02-06 03:34:50 +08:00
|
|
|
|
gap: 8px;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.icon {
|
|
|
|
|
|
font-size: 16px;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 600px) {
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.viz-card {
|
|
|
|
|
|
flex-direction: column-reverse;
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
2026-02-06 03:34:50 +08:00
|
|
|
|
|
|
|
|
|
|
.connection {
|
2026-01-15 20:10:19 +08:00
|
|
|
|
transform: rotate(90deg);
|
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|