2026-01-15 20:10:19 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="flow-matching-demo">
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<div class="controls">
|
|
|
|
|
|
<el-button type="primary" @click="startAnimation" :disabled="isPlaying">
|
|
|
|
|
|
<el-icon><VideoPlay /></el-icon> 开始对比演示 (Start Demo)
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="comparison-grid">
|
|
|
|
|
|
<!-- Diffusion -->
|
|
|
|
|
|
<div class="viz-panel">
|
|
|
|
|
|
<div class="panel-header">
|
|
|
|
|
|
<el-icon color="#F56C6C"><RefreshLeft /></el-icon>
|
|
|
|
|
|
<span>Diffusion (扩散模型)</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="canvas-wrapper">
|
|
|
|
|
|
<canvas ref="diffCanvasRef" width="300" height="200"></canvas>
|
|
|
|
|
|
<div class="labels">
|
|
|
|
|
|
<span class="label-noise">噪声 (Noise)</span>
|
|
|
|
|
|
<span class="label-img">图像 (Image)</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stats-box">
|
|
|
|
|
|
<el-statistic title="步数 (Steps)" :value="diffSteps" />
|
|
|
|
|
|
<el-tag type="danger">路径弯曲 (Curved)</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Flow Matching -->
|
|
|
|
|
|
<div class="viz-panel">
|
|
|
|
|
|
<div class="panel-header">
|
|
|
|
|
|
<el-icon color="#67C23A"><Right /></el-icon>
|
|
|
|
|
|
<span>Flow Matching (流匹配)</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="canvas-wrapper">
|
|
|
|
|
|
<canvas ref="flowCanvasRef" width="300" height="200"></canvas>
|
|
|
|
|
|
<div class="labels">
|
|
|
|
|
|
<span class="label-noise">噪声 (Noise)</span>
|
|
|
|
|
|
<span class="label-img">图像 (Image)</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stats-box">
|
|
|
|
|
|
<el-statistic title="步数 (Steps)" :value="flowSteps" />
|
|
|
|
|
|
<el-tag type="success">路径直线 (Straight)</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-divider />
|
|
|
|
|
|
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
title="为什么 Flow Matching 更快?"
|
|
|
|
|
|
type="success"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #default>
|
|
|
|
|
|
<p>
|
2026-01-16 19:10:21 +08:00
|
|
|
|
<strong>Diffusion</strong>
|
|
|
|
|
|
就像在迷雾中摸索,路径充满了随机性,需要走很多弯路(步数多)才能到达终点。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
<strong>Flow Matching</strong> 就像使用了 GPS
|
|
|
|
|
|
导航,直接找到了从噪声到图像的<strong
|
|
|
|
|
|
>直线最优路径 (Optimal Transport)</strong
|
|
|
|
|
|
>,因此只需要极少的步数。
|
2026-01-15 20:10:19 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-alert>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
|
|
|
import { VideoPlay, RefreshLeft, Right } from '@element-plus/icons-vue'
|
|
|
|
|
|
|
|
|
|
|
|
const diffCanvasRef = ref(null)
|
|
|
|
|
|
const flowCanvasRef = ref(null)
|
|
|
|
|
|
const isPlaying = ref(false)
|
|
|
|
|
|
const diffSteps = ref(0)
|
|
|
|
|
|
const flowSteps = ref(0)
|
|
|
|
|
|
|
|
|
|
|
|
let animationFrame = null
|
|
|
|
|
|
|
|
|
|
|
|
// Animation State
|
|
|
|
|
|
let diffProgress = 0
|
|
|
|
|
|
let flowProgress = 0
|
|
|
|
|
|
const diffSpeed = 0.005 // Slow
|
2026-01-16 19:10:21 +08:00
|
|
|
|
const flowSpeed = 0.02 // Fast
|
2026-01-15 20:10:19 +08:00
|
|
|
|
|
|
|
|
|
|
// Particles
|
|
|
|
|
|
const particles = []
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
drawStatic(diffCanvasRef.value, 'curve')
|
|
|
|
|
|
drawStatic(flowCanvasRef.value, 'line')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
stopAnimation()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const startAnimation = () => {
|
|
|
|
|
|
if (isPlaying.value) return
|
|
|
|
|
|
isPlaying.value = true
|
|
|
|
|
|
diffProgress = 0
|
|
|
|
|
|
flowProgress = 0
|
|
|
|
|
|
diffSteps.value = 0
|
|
|
|
|
|
flowSteps.value = 0
|
2026-01-16 19:10:21 +08:00
|
|
|
|
|
2026-01-15 20:10:19 +08:00
|
|
|
|
animate()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const stopAnimation = () => {
|
|
|
|
|
|
isPlaying.value = false
|
|
|
|
|
|
if (animationFrame) {
|
|
|
|
|
|
cancelAnimationFrame(animationFrame)
|
|
|
|
|
|
animationFrame = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const animate = () => {
|
|
|
|
|
|
let finished = 0
|
2026-01-16 19:10:21 +08:00
|
|
|
|
|
2026-01-15 20:10:19 +08:00
|
|
|
|
// Update Diffusion
|
|
|
|
|
|
if (diffProgress < 1) {
|
|
|
|
|
|
diffProgress += diffSpeed
|
|
|
|
|
|
diffSteps.value = Math.floor(diffProgress * 50) // Simulate 50 steps
|
|
|
|
|
|
drawFrame(diffCanvasRef.value, diffProgress, 'curve')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
diffSteps.value = 50
|
|
|
|
|
|
drawFrame(diffCanvasRef.value, 1, 'curve')
|
|
|
|
|
|
finished++
|
|
|
|
|
|
}
|
2026-01-16 19:10:21 +08:00
|
|
|
|
|
2026-01-15 20:10:19 +08:00
|
|
|
|
// Update Flow
|
|
|
|
|
|
if (flowProgress < 1) {
|
|
|
|
|
|
flowProgress += flowSpeed
|
|
|
|
|
|
flowSteps.value = Math.floor(flowProgress * 8) // Simulate 8 steps
|
|
|
|
|
|
drawFrame(flowCanvasRef.value, flowProgress, 'line')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
flowSteps.value = 8
|
|
|
|
|
|
drawFrame(flowCanvasRef.value, 1, 'line')
|
|
|
|
|
|
finished++
|
|
|
|
|
|
}
|
2026-01-16 19:10:21 +08:00
|
|
|
|
|
2026-01-15 20:10:19 +08:00
|
|
|
|
if (finished < 2) {
|
|
|
|
|
|
animationFrame = requestAnimationFrame(animate)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
isPlaying.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const drawStatic = (canvas, type) => {
|
|
|
|
|
|
if (!canvas) return
|
|
|
|
|
|
const ctx = canvas.getContext('2d')
|
|
|
|
|
|
const w = canvas.width
|
|
|
|
|
|
const h = canvas.height
|
2026-01-16 19:10:21 +08:00
|
|
|
|
|
2026-01-15 20:10:19 +08:00
|
|
|
|
ctx.clearRect(0, 0, w, h)
|
|
|
|
|
|
drawBackground(ctx, w, h)
|
|
|
|
|
|
drawPath(ctx, w, h, type, false)
|
|
|
|
|
|
drawEndpoints(ctx, w, h)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const drawFrame = (canvas, progress, type) => {
|
|
|
|
|
|
if (!canvas) return
|
|
|
|
|
|
const ctx = canvas.getContext('2d')
|
|
|
|
|
|
const w = canvas.width
|
|
|
|
|
|
const h = canvas.height
|
2026-01-16 19:10:21 +08:00
|
|
|
|
|
2026-01-15 20:10:19 +08:00
|
|
|
|
ctx.clearRect(0, 0, w, h)
|
|
|
|
|
|
drawBackground(ctx, w, h)
|
|
|
|
|
|
drawPath(ctx, w, h, type, true) // active path
|
|
|
|
|
|
drawEndpoints(ctx, w, h)
|
2026-01-16 19:10:21 +08:00
|
|
|
|
|
2026-01-15 20:10:19 +08:00
|
|
|
|
// Draw Particle
|
|
|
|
|
|
const pos = getPosition(progress, type, w, h)
|
2026-01-16 19:10:21 +08:00
|
|
|
|
|
2026-01-15 20:10:19 +08:00
|
|
|
|
// Draw Trail
|
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
|
if (type === 'curve') {
|
|
|
|
|
|
ctx.moveTo(30, h - 30)
|
|
|
|
|
|
// Re-calculate curve up to progress
|
2026-01-16 19:10:21 +08:00
|
|
|
|
for (let t = 0; t <= progress; t += 0.01) {
|
2026-01-15 20:10:19 +08:00
|
|
|
|
const p = getPosition(t, type, w, h)
|
|
|
|
|
|
ctx.lineTo(p.x, p.y)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ctx.moveTo(30, h - 30)
|
|
|
|
|
|
ctx.lineTo(pos.x, pos.y)
|
|
|
|
|
|
}
|
|
|
|
|
|
ctx.strokeStyle = type === 'curve' ? '#F56C6C' : '#67C23A'
|
|
|
|
|
|
ctx.lineWidth = 3
|
|
|
|
|
|
ctx.stroke()
|
2026-01-16 19:10:21 +08:00
|
|
|
|
|
2026-01-15 20:10:19 +08:00
|
|
|
|
// Draw Head
|
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
|
ctx.arc(pos.x, pos.y, 6, 0, Math.PI * 2)
|
|
|
|
|
|
ctx.fillStyle = type === 'curve' ? '#F56C6C' : '#67C23A'
|
|
|
|
|
|
ctx.fill()
|
|
|
|
|
|
ctx.strokeStyle = '#fff'
|
|
|
|
|
|
ctx.lineWidth = 2
|
|
|
|
|
|
ctx.stroke()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const drawBackground = (ctx, w, h) => {
|
|
|
|
|
|
ctx.fillStyle = '#f9f9f9' // Light bg
|
|
|
|
|
|
// Grid
|
|
|
|
|
|
ctx.strokeStyle = '#eee'
|
|
|
|
|
|
ctx.lineWidth = 1
|
|
|
|
|
|
ctx.beginPath()
|
2026-01-16 19:10:21 +08:00
|
|
|
|
for (let x = 0; x <= w; x += 20) {
|
|
|
|
|
|
ctx.moveTo(x, 0)
|
|
|
|
|
|
ctx.lineTo(x, h)
|
|
|
|
|
|
}
|
|
|
|
|
|
for (let y = 0; y <= h; y += 20) {
|
|
|
|
|
|
ctx.moveTo(0, y)
|
|
|
|
|
|
ctx.lineTo(w, y)
|
|
|
|
|
|
}
|
2026-01-15 20:10:19 +08:00
|
|
|
|
ctx.stroke()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const drawEndpoints = (ctx, w, h) => {
|
|
|
|
|
|
// Start (Noise)
|
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
|
ctx.arc(30, h - 30, 8, 0, Math.PI * 2)
|
|
|
|
|
|
ctx.fillStyle = '#909399'
|
|
|
|
|
|
ctx.fill()
|
2026-01-16 19:10:21 +08:00
|
|
|
|
|
2026-01-15 20:10:19 +08:00
|
|
|
|
// End (Image)
|
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
|
ctx.arc(w - 30, 30, 8, 0, Math.PI * 2)
|
|
|
|
|
|
ctx.fillStyle = '#409EFF'
|
|
|
|
|
|
ctx.fill()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const drawPath = (ctx, w, h, type, isActive) => {
|
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
|
ctx.moveTo(30, h - 30)
|
2026-01-16 19:10:21 +08:00
|
|
|
|
|
2026-01-15 20:10:19 +08:00
|
|
|
|
if (type === 'line') {
|
|
|
|
|
|
ctx.lineTo(w - 30, 30)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Bezier curve for diffusion
|
|
|
|
|
|
ctx.quadraticCurveTo(w * 0.2, 30, w - 30, 30)
|
|
|
|
|
|
}
|
2026-01-16 19:10:21 +08:00
|
|
|
|
|
2026-01-15 20:10:19 +08:00
|
|
|
|
ctx.strokeStyle = isActive ? 'rgba(0,0,0,0.1)' : '#ddd'
|
|
|
|
|
|
ctx.lineWidth = 2
|
|
|
|
|
|
ctx.setLineDash([5, 5])
|
|
|
|
|
|
ctx.stroke()
|
|
|
|
|
|
ctx.setLineDash([])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getPosition = (t, type, w, h) => {
|
|
|
|
|
|
const startX = 30
|
|
|
|
|
|
const startY = h - 30
|
|
|
|
|
|
const endX = w - 30
|
|
|
|
|
|
const endY = 30
|
2026-01-16 19:10:21 +08:00
|
|
|
|
|
2026-01-15 20:10:19 +08:00
|
|
|
|
if (type === 'line') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
x: startX + (endX - startX) * t,
|
|
|
|
|
|
y: startY + (endY - startY) * t
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Quadratic Bezier: (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2
|
|
|
|
|
|
// Control Point
|
|
|
|
|
|
const cpX = w * 0.2
|
|
|
|
|
|
const cpY = 30
|
2026-01-16 19:10:21 +08:00
|
|
|
|
|
|
|
|
|
|
const x =
|
|
|
|
|
|
Math.pow(1 - t, 2) * startX +
|
|
|
|
|
|
2 * (1 - t) * t * cpX +
|
|
|
|
|
|
Math.pow(t, 2) * endX
|
|
|
|
|
|
const y =
|
|
|
|
|
|
Math.pow(1 - t, 2) * startY +
|
|
|
|
|
|
2 * (1 - t) * t * cpY +
|
|
|
|
|
|
Math.pow(t, 2) * endY
|
|
|
|
|
|
|
2026-01-15 20:10:19 +08:00
|
|
|
|
// Add some random jitter for diffusion look if t < 1
|
|
|
|
|
|
// const jitter = t < 1 ? (Math.random() - 0.5) * 5 : 0
|
|
|
|
|
|
// return { x: x + jitter, y: y + jitter }
|
|
|
|
|
|
return { x, y }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.flow-matching-demo {
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.controls {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.comparison-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 600px) {
|
|
|
|
|
|
.comparison-grid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.viz-panel {
|
|
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 5px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
font-size: 0.9em;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.canvas-wrapper {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
overflow: hidden;
|
2026-01-16 19:10:21 +08:00
|
|
|
|
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
|
2026-01-15 20:10:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
canvas {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.labels {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 5px;
|
|
|
|
|
|
left: 5px;
|
|
|
|
|
|
right: 5px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stats-box {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|