d35211071a
- standardize border-radius from 8px to 6px for consistent styling - adjust padding values from 1rem to 0.75rem for better visual hierarchy - remove redundant overflow-y properties for cleaner code
307 lines
7.2 KiB
Vue
307 lines
7.2 KiB
Vue
<template>
|
||
<div class="flow-matching-demo">
|
||
<div class="demo-card">
|
||
<div class="controls">
|
||
<button class="play-btn" @click="startRace" :disabled="isPlaying">
|
||
<span class="icon">{{ isPlaying ? 'Running...' : '🚀 开始比赛 (Start Race)' }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="track-container">
|
||
<!-- Track 1: Diffusion -->
|
||
<div class="track">
|
||
<div class="track-info">
|
||
<span class="track-name">Diffusion (迷宫模式)</span>
|
||
<span class="step-count">{{ diffSteps }} Steps</span>
|
||
</div>
|
||
<div class="canvas-wrapper">
|
||
<canvas ref="diffCanvasRef" width="400" height="100"></canvas>
|
||
<div class="marker start">噪声</div>
|
||
<div class="marker end">图像</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Track 2: Flow Matching -->
|
||
<div class="track">
|
||
<div class="track-info">
|
||
<span class="track-name">Flow Matching (直通模式)</span>
|
||
<span class="step-count highlight">{{ flowSteps }} Steps</span>
|
||
</div>
|
||
<div class="canvas-wrapper">
|
||
<canvas ref="flowCanvasRef" width="400" height="100"></canvas>
|
||
<div class="marker start">噪声</div>
|
||
<div class="marker end">图像</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-bar">
|
||
<span class="icon">💡</span>
|
||
<span>
|
||
<strong>核心区别:</strong>
|
||
Diffusion 就像在走迷宫,虽然也能到终点,但绕了很多弯路(步数多)。Flow Matching 则是直接修了一条直线高速公路,所以 8 步就能走完别人 50 步的路。
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, onUnmounted } from 'vue'
|
||
|
||
const diffCanvasRef = ref(null)
|
||
const flowCanvasRef = ref(null)
|
||
const isPlaying = ref(false)
|
||
const diffSteps = ref(0)
|
||
const flowSteps = ref(0)
|
||
let animationId = null
|
||
|
||
// Constants
|
||
const TARGET_STEPS_DIFF = 50
|
||
const TARGET_STEPS_FLOW = 8
|
||
const DURATION = 3000 // 3 seconds for the whole race
|
||
|
||
// Particles state
|
||
let particles = []
|
||
const NUM_PARTICLES = 5
|
||
|
||
class Particle {
|
||
constructor(type) {
|
||
this.type = type // 'diff' or 'flow'
|
||
this.progress = 0
|
||
this.path = []
|
||
this.noiseOffset = Math.random() * 1000
|
||
this.yOffset = (Math.random() - 0.5) * 60 // Spread vertically
|
||
}
|
||
|
||
update(dt) {
|
||
// Speed varies: Flow is faster because it covers distance in fewer steps?
|
||
// Actually, let's make them finish at the same TIME, but show the path difference.
|
||
// Or make Flow finish faster. Let's make Flow finish faster.
|
||
|
||
const speed = this.type === 'flow' ? 0.8 : 0.3
|
||
this.progress += speed * dt
|
||
|
||
if (this.progress > 1) this.progress = 1
|
||
|
||
// Calculate Position
|
||
const startX = 20
|
||
const endX = 380
|
||
const startY = 50 + this.yOffset
|
||
const endY = 50
|
||
|
||
// Linear interpolation base
|
||
let x = startX + (endX - startX) * this.progress
|
||
let y = startY + (endY - startY) * this.progress
|
||
|
||
if (this.type === 'diff') {
|
||
// Add noise to path
|
||
if (this.progress < 1) {
|
||
const noise = Math.sin(this.progress * 20 + this.noiseOffset) * 30 * (1 - this.progress)
|
||
y += noise
|
||
}
|
||
}
|
||
|
||
this.path.push({x, y})
|
||
return {x, y}
|
||
}
|
||
|
||
draw(ctx) {
|
||
ctx.beginPath()
|
||
ctx.moveTo(this.path[0].x, this.path[0].y)
|
||
for (let p of this.path) {
|
||
ctx.lineTo(p.x, p.y)
|
||
}
|
||
ctx.strokeStyle = this.type === 'flow' ? '#10b981' : '#f43f5e'
|
||
ctx.lineWidth = 2
|
||
ctx.stroke()
|
||
|
||
const current = this.path[this.path.length - 1]
|
||
ctx.beginPath()
|
||
ctx.arc(current.x, current.y, 4, 0, Math.PI * 2)
|
||
ctx.fillStyle = this.type === 'flow' ? '#10b981' : '#f43f5e'
|
||
ctx.fill()
|
||
}
|
||
}
|
||
|
||
const startRace = () => {
|
||
if (isPlaying.value) return
|
||
isPlaying.value = true
|
||
diffSteps.value = 0
|
||
flowSteps.value = 0
|
||
particles = []
|
||
|
||
// Create particles
|
||
for(let i=0; i<NUM_PARTICLES; i++) {
|
||
particles.push(new Particle('diff'))
|
||
particles.push(new Particle('flow'))
|
||
}
|
||
|
||
let lastTime = performance.now()
|
||
|
||
const animate = (time) => {
|
||
const dt = (time - lastTime) / 1000
|
||
lastTime = time
|
||
|
||
const dCtx = diffCanvasRef.value.getContext('2d')
|
||
const fCtx = flowCanvasRef.value.getContext('2d')
|
||
|
||
// Clear
|
||
dCtx.clearRect(0, 0, 400, 100)
|
||
fCtx.clearRect(0, 0, 400, 100)
|
||
|
||
// Draw Guidelines
|
||
drawGuide(dCtx)
|
||
drawGuide(fCtx)
|
||
|
||
let allFinished = true
|
||
|
||
particles.forEach(p => {
|
||
p.update(dt)
|
||
if (p.progress < 1) allFinished = false
|
||
|
||
if (p.type === 'diff') p.draw(dCtx)
|
||
else p.draw(fCtx)
|
||
})
|
||
|
||
// Update steps counter simulation
|
||
// Flow finishes in 8 steps, Diff in 50
|
||
// Map progress to steps
|
||
const flowP = particles.find(p => p.type === 'flow')
|
||
const diffP = particles.find(p => p.type === 'diff')
|
||
|
||
if (flowP) flowSteps.value = Math.floor(flowP.progress * TARGET_STEPS_FLOW)
|
||
if (diffP) diffSteps.value = Math.floor(diffP.progress * TARGET_STEPS_DIFF)
|
||
|
||
if (!allFinished) {
|
||
animationId = requestAnimationFrame(animate)
|
||
} else {
|
||
isPlaying.value = false
|
||
flowSteps.value = TARGET_STEPS_FLOW
|
||
diffSteps.value = TARGET_STEPS_DIFF
|
||
}
|
||
}
|
||
|
||
requestAnimationFrame(animate)
|
||
}
|
||
|
||
const drawGuide = (ctx) => {
|
||
ctx.strokeStyle = 'rgba(128,128,128,0.1)'
|
||
ctx.lineWidth = 1
|
||
ctx.setLineDash([5, 5])
|
||
ctx.beginPath()
|
||
ctx.moveTo(20, 50)
|
||
ctx.lineTo(380, 50)
|
||
ctx.stroke()
|
||
ctx.setLineDash([])
|
||
}
|
||
|
||
onMounted(() => {
|
||
// Initial draw
|
||
const dCtx = diffCanvasRef.value.getContext('2d')
|
||
const fCtx = flowCanvasRef.value.getContext('2d')
|
||
drawGuide(dCtx)
|
||
drawGuide(fCtx)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
if (animationId) cancelAnimationFrame(animationId)
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.flow-matching-demo {
|
||
margin: 20px 0;
|
||
font-family: var(--vp-font-family-base);
|
||
}
|
||
|
||
.demo-card {
|
||
background: var(--vp-c-bg-soft);
|
||
border: 1px solid var(--vp-c-divider);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.controls {
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.play-btn {
|
||
background: var(--vp-c-brand);
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 24px;
|
||
border-radius: 20px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: transform 0.1s;
|
||
}
|
||
|
||
.play-btn:hover:not(:disabled) {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.play-btn:disabled {
|
||
opacity: 0.7;
|
||
cursor: default;
|
||
}
|
||
|
||
.track {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.track-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.step-count {
|
||
font-family: monospace;
|
||
background: var(--vp-c-bg-alt);
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.step-count.highlight {
|
||
color: #10b981;
|
||
}
|
||
|
||
.canvas-wrapper {
|
||
position: relative;
|
||
background: var(--vp-c-bg);
|
||
border-radius: 6px;
|
||
border: 1px solid var(--vp-c-divider);
|
||
height: 100px;
|
||
}
|
||
|
||
.marker {
|
||
position: absolute;
|
||
bottom: 4px;
|
||
font-size: 10px;
|
||
color: var(--vp-c-text-3);
|
||
}
|
||
|
||
.marker.start { left: 10px; }
|
||
.marker.end { right: 10px; }
|
||
|
||
canvas {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.info-bar {
|
||
margin-top: 16px;
|
||
font-size: 13px;
|
||
color: var(--vp-c-text-2);
|
||
display: flex;
|
||
gap: 8px;
|
||
line-height: 1.4;
|
||
padding: 0 8px;
|
||
}
|
||
</style>
|