Files
test-repo/docs/.vitepress/theme/components/appendix/image-gen-intro/ImageGenQuickStartDemo.vue
T
sanbuphy e5b1c6cc88 docs: update content and components across multiple files
- Refine chapter introductions in zh-cn docs for clarity and conciseness
- Update navigation links to include '/easy-vibe' prefix
- Simplify UI components (ChapterIntroduction, ContextWindowVisualizer)
- Add new agent-related demo components (AgentMemoryDemo, AgentToolUseDemo)
- Improve context compression demo with better visuals and metrics
- Adjust styling and layout across various components
2026-02-03 01:46:03 +08:00

570 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
ImageGenQuickStartDemo.vue
AI 绘画快速体验组件
用途
让用户在文章开头就能体验 AI 绘画的魅力通过交互式演示理解文生图的基本概念
交互功能
- 预设提示词选择快速体验不同风格的图像生成
- 模拟生成过程展示从文本到图像的渐进过程
- 参数调节调整生成步数CFG Scale 等参数
- 对比展示对比不同参数下的生成效果
-->
<template>
<div class="image-gen-quickstart">
<el-card shadow="never">
<template #header>
<div class="header-title">
<el-icon><Picture /></el-icon>
<span>🎨 AI 绘画体验室</span>
</div>
</template>
<div class="demo-layout">
<!-- 左侧控制面板 -->
<div class="control-panel">
<div class="input-section">
<label>提示词 (Prompt)</label>
<el-input
v-model="prompt"
type="textarea"
:rows="3"
placeholder="描述你想生成的图像..."
/>
<div class="prompt-tags">
<el-tag
v-for="tag in presetPrompts"
:key="tag.label"
size="small"
class="prompt-tag"
@click="prompt = tag.prompt"
>
{{ tag.label }}
</el-tag>
</div>
</div>
<div class="params-section">
<div class="param-row">
<label>生成步数</label>
<el-slider v-model="steps" :min="10" :max="50" :step="5" show-stops />
</div>
<div class="param-row">
<label>CFG Scale (提示词遵循度)</label>
<el-slider v-model="cfgScale" :min="1" :max="15" :step="0.5" />
</div>
<div class="param-row">
<label>采样器</label>
<el-select v-model="sampler" size="small">
<el-option label="Euler" value="euler" />
<el-option label="DPM++" value="dpm" />
<el-option label="DDIM" value="ddim" />
</el-select>
</div>
</div>
<el-button
type="primary"
:loading="isGenerating"
@click="startGeneration"
class="generate-btn"
>
<el-icon><MagicStick /></el-icon>
{{ isGenerating ? '生成中...' : '开始生成' }}
</el-button>
</div>
<!-- 右侧生成展示 -->
<div class="display-panel">
<div class="canvas-container">
<canvas
ref="canvasRef"
width="400"
height="400"
class="gen-canvas"
/>
<div v-if="isGenerating" class="progress-overlay">
<el-progress
type="circle"
:percentage="progress"
:status="progress === 100 ? 'success' : ''"
/>
<p class="step-info">Step {{ currentStep }} / {{ steps }}</p>
</div>
</div>
<div class="image-info" v-if="!isGenerating && hasGenerated">
<el-descriptions :column="2" size="small" border>
<el-descriptions-item label="分辨率">512 × 512</el-descriptions-item>
<el-descriptions-item label="生成步数">{{ steps }}</el-descriptions-item>
<el-descriptions-item label="CFG Scale">{{ cfgScale }}</el-descriptions-item>
<el-descriptions-item label="采样器">{{ sampler }}</el-descriptions-item>
</el-descriptions>
</div>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>小提示</strong>
提示词越详细生成效果越好尝试使用 "风格词" "赛博朋克""水彩画"来控制图像风格
</p>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Picture, MagicStick } from '@element-plus/icons-vue'
const canvasRef = ref(null)
const prompt = ref('一只戴着墨镜的猫,赛博朋克风格,霓虹灯光')
const steps = ref(20)
const cfgScale = ref(7.5)
const sampler = ref('euler')
const isGenerating = ref(false)
const progress = ref(0)
const currentStep = ref(0)
const hasGenerated = ref(false)
const presetPrompts = [
{ label: '🐱 赛博朋克猫', prompt: '一只戴着墨镜的猫,赛博朋克风格,霓虹灯光' },
{ label: '🏔️ 山水画', prompt: '中国山水画,云雾缭绕,水墨风格' },
{ label: '🚀 太空', prompt: '宇航员在火星表面,日落时分,科幻风格' },
{ label: '🌸 樱花', prompt: '樱花树下,日本传统建筑,春天,柔和光线' },
]
// 模拟生成过程
const startGeneration = async () => {
isGenerating.value = true
progress.value = 0
currentStep.value = 0
hasGenerated.value = false
const canvas = canvasRef.value
const ctx = canvas.getContext('2d')
// 生成噪声图像作为起点
const generateNoise = () => {
const imageData = ctx.createImageData(400, 400)
for (let i = 0; i < imageData.data.length; i += 4) {
const val = Math.random() * 255
imageData.data[i] = val
imageData.data[i + 1] = val
imageData.data[i + 2] = val
imageData.data[i + 3] = 255
}
return imageData
}
// 模拟渐进生成
for (let i = 0; i <= steps.value; i++) {
await new Promise(resolve => setTimeout(resolve, 100))
currentStep.value = i
progress.value = Math.round((i / steps.value) * 100)
// 绘制渐进图像(模拟)
const noiseRatio = 1 - (i / steps.value)
drawSimulatedImage(ctx, noiseRatio)
}
isGenerating.value = false
hasGenerated.value = true
}
// 根据提示词绘制模拟图像
const drawSimulatedImage = (ctx, noiseRatio) => {
const width = 400
const height = 400
// 清空画布
ctx.fillStyle = '#1a1a2e'
ctx.fillRect(0, 0, width, height)
// 根据提示词关键词绘制不同内容
const promptLower = prompt.value.toLowerCase()
// 赛博朋克风格
if (promptLower.includes('赛博') || promptLower.includes('cyber')) {
drawCyberpunkScene(ctx, width, height, noiseRatio)
}
// 山水画
else if (promptLower.includes('山水') || promptLower.includes('水墨')) {
drawLandscape(ctx, width, height, noiseRatio)
}
// 太空
else if (promptLower.includes('太空') || promptLower.includes('宇航员')) {
drawSpaceScene(ctx, width, height, noiseRatio)
}
// 樱花
else if (promptLower.includes('樱花') || promptLower.includes('日本')) {
drawSakuraScene(ctx, width, height, noiseRatio)
}
// 默认:抽象艺术
else {
drawAbstractArt(ctx, width, height, noiseRatio)
}
// 添加噪声
if (noiseRatio > 0) {
addNoise(ctx, width, height, noiseRatio)
}
}
const drawCyberpunkScene = (ctx, w, h, noise) => {
// 霓虹背景
const gradient = ctx.createLinearGradient(0, 0, 0, h)
gradient.addColorStop(0, '#0a0a1a')
gradient.addColorStop(1, '#1a0a2e')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, w, h)
// 霓虹灯条
ctx.shadowBlur = 20 * (1 - noise)
ctx.shadowColor = '#ff00ff'
ctx.fillStyle = `rgba(255, 0, 255, ${0.8 * (1 - noise)})`
ctx.fillRect(50, 100, 300, 5)
ctx.shadowColor = '#00ffff'
ctx.fillStyle = `rgba(0, 255, 255, ${0.8 * (1 - noise)})`
ctx.fillRect(100, 200, 200, 5)
// 猫的形状(简化)
ctx.shadowBlur = 0
ctx.fillStyle = `rgba(100, 100, 150, ${0.9 * (1 - noise)})`
ctx.beginPath()
ctx.ellipse(200, 280, 60, 50, 0, 0, Math.PI * 2)
ctx.fill()
// 耳朵
ctx.beginPath()
ctx.moveTo(160, 250)
ctx.lineTo(150, 200)
ctx.lineTo(180, 240)
ctx.fill()
ctx.beginPath()
ctx.moveTo(240, 250)
ctx.lineTo(250, 200)
ctx.lineTo(220, 240)
ctx.fill()
// 墨镜
ctx.fillStyle = `rgba(0, 0, 0, ${0.9 * (1 - noise)})`
ctx.beginPath()
ctx.ellipse(180, 270, 20, 12, 0, 0, Math.PI * 2)
ctx.fill()
ctx.beginPath()
ctx.ellipse(220, 270, 20, 12, 0, 0, Math.PI * 2)
ctx.fill()
// 镜片反光
ctx.fillStyle = `rgba(255, 0, 255, ${0.6 * (1 - noise)})`
ctx.beginPath()
ctx.ellipse(175, 268, 5, 3, 0, 0, Math.PI * 2)
ctx.fill()
ctx.beginPath()
ctx.ellipse(215, 268, 5, 3, 0, 0, Math.PI * 2)
ctx.fill()
}
const drawLandscape = (ctx, w, h, noise) => {
// 天空渐变
const gradient = ctx.createLinearGradient(0, 0, 0, h)
gradient.addColorStop(0, '#e8f4f8')
gradient.addColorStop(0.5, '#c8e0e8')
gradient.addColorStop(1, '#a8c8d8')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, w, h)
// 远山
ctx.fillStyle = `rgba(100, 120, 140, ${0.5 * (1 - noise)})`
ctx.beginPath()
ctx.moveTo(0, 250)
ctx.lineTo(100, 150)
ctx.lineTo(200, 200)
ctx.lineTo(300, 120)
ctx.lineTo(400, 180)
ctx.lineTo(400, 400)
ctx.lineTo(0, 400)
ctx.fill()
// 近山
ctx.fillStyle = `rgba(60, 80, 100, ${0.7 * (1 - noise)})`
ctx.beginPath()
ctx.moveTo(0, 300)
ctx.lineTo(150, 220)
ctx.lineTo(300, 280)
ctx.lineTo(400, 240)
ctx.lineTo(400, 400)
ctx.lineTo(0, 400)
ctx.fill()
// 云雾
ctx.fillStyle = `rgba(255, 255, 255, ${0.4 * (1 - noise)})`
for (let i = 0; i < 5; i++) {
ctx.beginPath()
ctx.ellipse(80 + i * 70, 180 + i * 20, 40, 20, 0, 0, Math.PI * 2)
ctx.fill()
}
}
const drawSpaceScene = (ctx, w, h, noise) => {
// 太空背景
ctx.fillStyle = '#0a0a15'
ctx.fillRect(0, 0, w, h)
// 星星
ctx.fillStyle = `rgba(255, 255, 255, ${1 - noise})`
for (let i = 0; i < 100; i++) {
const x = (i * 37) % w
const y = (i * 73) % h
ctx.beginPath()
ctx.arc(x, y, Math.random() * 2, 0, Math.PI * 2)
ctx.fill()
}
// 火星地面
ctx.fillStyle = `rgba(180, 80, 40, ${0.8 * (1 - noise)})`
ctx.beginPath()
ctx.moveTo(0, 350)
ctx.lineTo(100, 320)
ctx.lineTo(200, 340)
ctx.lineTo(300, 310)
ctx.lineTo(400, 330)
ctx.lineTo(400, 400)
ctx.lineTo(0, 400)
ctx.fill()
// 宇航员(简化)
ctx.fillStyle = `rgba(220, 220, 230, ${0.9 * (1 - noise)})`
ctx.beginPath()
ctx.arc(200, 220, 40, 0, Math.PI * 2)
ctx.fill()
// 头盔反光
ctx.fillStyle = `rgba(255, 200, 100, ${0.5 * (1 - noise)})`
ctx.beginPath()
ctx.arc(185, 210, 15, 0, Math.PI * 2)
ctx.fill()
// 日落
const sunGradient = ctx.createRadialGradient(300, 100, 0, 300, 100, 60)
sunGradient.addColorStop(0, `rgba(255, 200, 100, ${1 - noise})`)
sunGradient.addColorStop(1, `rgba(255, 100, 50, ${0.5 * (1 - noise)})`)
ctx.fillStyle = sunGradient
ctx.beginPath()
ctx.arc(300, 100, 50, 0, Math.PI * 2)
ctx.fill()
}
const drawSakuraScene = (ctx, w, h, noise) => {
// 天空
const gradient = ctx.createLinearGradient(0, 0, 0, h)
gradient.addColorStop(0, '#ffe4e1')
gradient.addColorStop(1, '#ffb6c1')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, w, h)
// 传统建筑屋顶
ctx.fillStyle = `rgba(80, 60, 60, ${0.9 * (1 - noise)})`
ctx.beginPath()
ctx.moveTo(100, 300)
ctx.lineTo(150, 200)
ctx.lineTo(250, 200)
ctx.lineTo(300, 300)
ctx.fill()
// 樱花树
ctx.fillStyle = `rgba(139, 90, 43, ${0.9 * (1 - noise)})`
ctx.fillRect(50, 200, 20, 200)
// 樱花
ctx.fillStyle = `rgba(255, 183, 197, ${0.8 * (1 - noise)})`
for (let i = 0; i < 30; i++) {
const x = 30 + (i * 13) % 80
const y = 150 + (i * 17) % 100
ctx.beginPath()
ctx.arc(x, y, 8 + Math.random() * 5, 0, Math.PI * 2)
ctx.fill()
}
// 飘落的樱花
ctx.fillStyle = `rgba(255, 192, 203, ${0.6 * (1 - noise)})`
for (let i = 0; i < 20; i++) {
const x = (i * 23) % w
const y = 250 + (i * 31) % 150
ctx.beginPath()
ctx.ellipse(x, y, 4, 2, i, 0, Math.PI * 2)
ctx.fill()
}
}
const drawAbstractArt = (ctx, w, h, noise) => {
// 渐变背景
const gradient = ctx.createLinearGradient(0, 0, w, h)
gradient.addColorStop(0, '#667eea')
gradient.addColorStop(1, '#764ba2')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, w, h)
// 抽象形状
const colors = ['#f093fb', '#f5576c', '#4facfe', '#00f2fe']
for (let i = 0; i < 8; i++) {
ctx.fillStyle = colors[i % colors.length] + Math.floor((1 - noise) * 255).toString(16).padStart(2, '0')
ctx.beginPath()
const x = 100 + (i * 50) % 300
const y = 100 + (i * 70) % 250
const r = 30 + i * 5
ctx.arc(x, y, r, 0, Math.PI * 2)
ctx.fill()
}
}
const addNoise = (ctx, w, h, ratio) => {
const imageData = ctx.getImageData(0, 0, w, h)
for (let i = 0; i < imageData.data.length; i += 4) {
const noise = (Math.random() - 0.5) * 50 * ratio
imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + noise))
imageData.data[i + 1] = Math.max(0, Math.min(255, imageData.data[i + 1] + noise))
imageData.data[i + 2] = Math.max(0, Math.min(255, imageData.data[i + 2] + noise))
}
ctx.putImageData(imageData, 0, 0)
}
onMounted(() => {
// 初始绘制
const canvas = canvasRef.value
if (canvas) {
const ctx = canvas.getContext('2d')
drawSimulatedImage(ctx, 1)
}
})
</script>
<style scoped>
.image-gen-quickstart {
margin: 1rem 0;
}
.header-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.demo-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 768px) {
.demo-layout {
grid-template-columns: 1fr;
}
}
.control-panel {
display: flex;
flex-direction: column;
gap: 16px;
}
.input-section label,
.params-section label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 8px;
color: var(--vp-c-text-2);
}
.prompt-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.prompt-tag {
cursor: pointer;
transition: all 0.2s;
}
.prompt-tag:hover {
transform: translateY(-2px);
}
.param-row {
margin-bottom: 12px;
}
.param-row label {
font-size: 0.8rem;
margin-bottom: 4px;
}
.generate-btn {
width: 100%;
margin-top: auto;
}
.display-panel {
display: flex;
flex-direction: column;
gap: 16px;
}
.canvas-container {
position: relative;
width: 100%;
aspect-ratio: 1;
background: var(--vp-c-bg-mute);
border-radius: 8px;
overflow: hidden;
}
.gen-canvas {
width: 100%;
height: 100%;
object-fit: contain;
}
.progress-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
gap: 16px;
}
.step-info {
color: white;
font-size: 0.9rem;
}
.image-info {
font-size: 0.8rem;
}
.info-box {
margin-top: 16px;
padding: 12px;
background: var(--vp-c-bg-mute);
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.6;
}
.icon {
font-size: 1.2em;
}
</style>