795 lines
17 KiB
Vue
795 lines
17 KiB
Vue
|
|
<!--
|
|||
|
|
PerformanceDemo.vue
|
|||
|
|
Canvas 性能优化演示组件
|
|||
|
|
|
|||
|
|
用途:
|
|||
|
|
展示 Canvas 性能优化技术,包括离屏 Canvas、减少重绘、图层管理等
|
|||
|
|
|
|||
|
|
交互功能:
|
|||
|
|
- 性能对比:优化前后的性能对比
|
|||
|
|
- 对象数量调整:测试不同负载下的性能
|
|||
|
|
- FPS 显示:实时显示帧率
|
|||
|
|
- 优化开关:启用/禁用各种优化技术
|
|||
|
|
-->
|
|||
|
|
<template>
|
|||
|
|
<div class="performance-demo">
|
|||
|
|
<div class="control-panel">
|
|||
|
|
<div class="test-selector">
|
|||
|
|
<label>Performance Test / 性能测试</label>
|
|||
|
|
<div class="button-group">
|
|||
|
|
<button
|
|||
|
|
v-for="test in tests"
|
|||
|
|
:key="test.value"
|
|||
|
|
:class="{ active: currentTest === test.value }"
|
|||
|
|
@click="switchTest(test.value)"
|
|||
|
|
>
|
|||
|
|
{{ test.label }}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="parameters">
|
|||
|
|
<div class="param-row">
|
|||
|
|
<label>Object Count / 对象数量: {{ objectCount }}</label>
|
|||
|
|
<input
|
|||
|
|
type="range"
|
|||
|
|
v-model.number="objectCount"
|
|||
|
|
min="100"
|
|||
|
|
max="5000"
|
|||
|
|
step="100"
|
|||
|
|
@input="resetTest"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="optimization-toggles">
|
|||
|
|
<label>Optimizations / 优化技术</label>
|
|||
|
|
<div class="toggle-grid">
|
|||
|
|
<label class="toggle-option" v-if="currentTest === 'redraw'">
|
|||
|
|
<input type="checkbox" v-model="useDirtyRect" />
|
|||
|
|
<span>Dirty Rect / 脏矩形</span>
|
|||
|
|
</label>
|
|||
|
|
|
|||
|
|
<label class="toggle-option" v-if="currentTest === 'layer'">
|
|||
|
|
<input type="checkbox" v-model="useOffscreenCanvas" />
|
|||
|
|
<span>Offscreen Canvas / 离屏画布</span>
|
|||
|
|
</label>
|
|||
|
|
|
|||
|
|
<label class="toggle-option" v-if="currentTest === 'batch'">
|
|||
|
|
<input type="checkbox" v-model="useBatching" />
|
|||
|
|
<span>Batch Rendering / 批量渲染</span>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="stats">
|
|||
|
|
<div class="stat-item">
|
|||
|
|
<span class="label">FPS:</span>
|
|||
|
|
<span class="value" :class="{ good: fps >= 55, warning: fps >= 30 && fps < 55, bad: fps < 30 }">
|
|||
|
|
{{ fps }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-item">
|
|||
|
|
<span class="label">Frame Time:</span>
|
|||
|
|
<span class="value">{{ frameTime }}ms</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-item">
|
|||
|
|
<span class="label">Objects:</span>
|
|||
|
|
<span class="value">{{ objectCount }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<button class="reset-btn" @click="resetTest">
|
|||
|
|
<span class="icon">🔄</span>
|
|||
|
|
Restart Test / 重新测试
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="canvas-container">
|
|||
|
|
<canvas ref="canvasRef" width="600" height="400"></canvas>
|
|||
|
|
<canvas
|
|||
|
|
v-if="useOffscreenCanvas"
|
|||
|
|
ref="offscreenCanvasRef"
|
|||
|
|
width="600"
|
|||
|
|
height="400"
|
|||
|
|
style="display: none"
|
|||
|
|
></canvas>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="comparison" v-if="showComparison">
|
|||
|
|
<h4>Performance Comparison / 性能对比</h4>
|
|||
|
|
<div class="comparison-table">
|
|||
|
|
<table>
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>Technique / 技术</th>
|
|||
|
|
<th>FPS</th>
|
|||
|
|
<th>Improvement / 提升</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr>
|
|||
|
|
<td>Baseline / 基准</td>
|
|||
|
|
<td>{{ baselineFps }}</td>
|
|||
|
|
<td>-</td>
|
|||
|
|
</tr>
|
|||
|
|
<tr v-if="useDirtyRect">
|
|||
|
|
<td>Dirty Rect / 脏矩形</td>
|
|||
|
|
<td>{{ fps }}</td>
|
|||
|
|
<td>{{ (((fps - baselineFps) / baselineFps) * 100).toFixed(1) }}%</td>
|
|||
|
|
</tr>
|
|||
|
|
<tr v-if="useOffscreenCanvas">
|
|||
|
|
<td>Offscreen Canvas / 离屏画布</td>
|
|||
|
|
<td>{{ fps }}</td>
|
|||
|
|
<td>{{ (((fps - baselineFps) / baselineFps) * 100).toFixed(1) }}%</td>
|
|||
|
|
</tr>
|
|||
|
|
<tr v-if="useBatching">
|
|||
|
|
<td>Batch Rendering / 批量渲染</td>
|
|||
|
|
<td>{{ fps }}</td>
|
|||
|
|
<td>{{ (((fps - baselineFps) / baselineFps) * 100).toFixed(1) }}%</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="code-display">
|
|||
|
|
<h4>Optimization Code / 优化代码</h4>
|
|||
|
|
<pre><code>{{ optimizationCode }}</code></pre>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="explanation">
|
|||
|
|
<h4>Performance Tips / 性能优化要点</h4>
|
|||
|
|
<ul>
|
|||
|
|
<li>
|
|||
|
|
<strong>减少重绘:</strong>
|
|||
|
|
只重绘变化的部分(脏矩形技术),避免不必要的 clearRect
|
|||
|
|
</li>
|
|||
|
|
<li>
|
|||
|
|
<strong>离屏 Canvas:</strong>
|
|||
|
|
预渲染静态内容到离屏 Canvas,减少每帧的绘制操作
|
|||
|
|
</li>
|
|||
|
|
<li>
|
|||
|
|
<strong>批量渲染:</strong>
|
|||
|
|
减少状态切换(fillStyle、strokeStyle 等),批量处理相同类型的绘制
|
|||
|
|
</li>
|
|||
|
|
<li>
|
|||
|
|
<strong>对象池:</strong>
|
|||
|
|
复用对象,减少垃圾回收压力
|
|||
|
|
</li>
|
|||
|
|
<li>
|
|||
|
|
<strong>requestAnimationFrame:</strong>
|
|||
|
|
使用浏览器提供的动画 API,优化渲染时机
|
|||
|
|
</li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
|||
|
|
|
|||
|
|
const canvasRef = ref(null)
|
|||
|
|
const offscreenCanvasRef = ref(null)
|
|||
|
|
const currentTest = ref('redraw')
|
|||
|
|
const objectCount = ref(1000)
|
|||
|
|
const useDirtyRect = ref(false)
|
|||
|
|
const useOffscreenCanvas = ref(false)
|
|||
|
|
const useBatching = ref(false)
|
|||
|
|
const fps = ref(0)
|
|||
|
|
const frameTime = ref(0)
|
|||
|
|
const baselineFps = ref(0)
|
|||
|
|
const showComparison = ref(false)
|
|||
|
|
|
|||
|
|
let animationId = null
|
|||
|
|
let lastTime = 0
|
|||
|
|
let frameCount = 0
|
|||
|
|
let fpsTime = 0
|
|||
|
|
let objects = []
|
|||
|
|
let offscreenCtx = null
|
|||
|
|
|
|||
|
|
const tests = [
|
|||
|
|
{ value: 'redraw', label: 'Minimize Redraw / 减少重绘' },
|
|||
|
|
{ value: 'layer', label: 'Layer Management / 图层管理' },
|
|||
|
|
{ value: 'batch', label: 'Batch Rendering / 批量渲染' }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const optimizationCode = computed(() => {
|
|||
|
|
const templates = {
|
|||
|
|
redraw: `// 脏矩形优化 - 只重绘变化的部分
|
|||
|
|
function draw() {
|
|||
|
|
// 不清除整个画布,只清除变化的区域
|
|||
|
|
if (useDirtyRect) {
|
|||
|
|
objects.forEach(obj => {
|
|||
|
|
if (obj.moved) {
|
|||
|
|
// 清除旧位置
|
|||
|
|
ctx.clearRect(
|
|||
|
|
obj.oldX - obj.size,
|
|||
|
|
obj.oldY - obj.size,
|
|||
|
|
obj.size * 2,
|
|||
|
|
obj.size * 2
|
|||
|
|
)
|
|||
|
|
// 绘制新位置
|
|||
|
|
obj.draw(ctx)
|
|||
|
|
obj.moved = false
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
// 传统方式:清除整个画布
|
|||
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|||
|
|
objects.forEach(obj => obj.draw(ctx))
|
|||
|
|
}
|
|||
|
|
}`,
|
|||
|
|
|
|||
|
|
layer: `// 离屏 Canvas - 预渲染静态内容
|
|||
|
|
// 初始化时创建离屏 Canvas
|
|||
|
|
const offscreenCanvas = document.createElement('canvas')
|
|||
|
|
const offscreenCtx = offscreenCanvas.getContext('2d')
|
|||
|
|
|
|||
|
|
// 预渲染静态背景
|
|||
|
|
function drawBackground(ctx) {
|
|||
|
|
ctx.fillStyle = '#f0f0f0'
|
|||
|
|
ctx.fillRect(0, 0, 600, 400)
|
|||
|
|
// 绘制网格等静态内容...
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 只绘制一次到离屏 Canvas
|
|||
|
|
drawBackground(offscreenCtx)
|
|||
|
|
|
|||
|
|
// 主渲染循环
|
|||
|
|
function draw() {
|
|||
|
|
if (useOffscreenCanvas) {
|
|||
|
|
// 直接复制预渲染的内容
|
|||
|
|
ctx.drawImage(offscreenCanvas, 0, 0)
|
|||
|
|
} else {
|
|||
|
|
// 每帧重新绘制背景
|
|||
|
|
drawBackground(ctx)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 只绘制动态对象
|
|||
|
|
objects.forEach(obj => obj.draw(ctx))
|
|||
|
|
}`,
|
|||
|
|
|
|||
|
|
batch: `// 批量渲染 - 减少状态切换
|
|||
|
|
function draw() {
|
|||
|
|
if (useBatching) {
|
|||
|
|
// 按颜色分组
|
|||
|
|
const batches = {}
|
|||
|
|
objects.forEach(obj => {
|
|||
|
|
if (!batches[obj.color]) {
|
|||
|
|
batches[obj.color] = []
|
|||
|
|
}
|
|||
|
|
batches[obj.color].push(obj)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 批量绘制相同颜色的对象
|
|||
|
|
Object.keys(batches).forEach(color => {
|
|||
|
|
ctx.fillStyle = color // 只设置一次颜色
|
|||
|
|
batches[color].forEach(obj => {
|
|||
|
|
ctx.beginPath()
|
|||
|
|
ctx.arc(obj.x, obj.y, obj.size, 0, Math.PI * 2)
|
|||
|
|
ctx.fill()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
// 传统方式:每个对象都切换状态
|
|||
|
|
objects.forEach(obj => {
|
|||
|
|
ctx.fillStyle = obj.color // 频繁切换状态
|
|||
|
|
ctx.beginPath()
|
|||
|
|
ctx.arc(obj.x, obj.y, obj.size, 0, Math.PI * 2)
|
|||
|
|
ctx.fill()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return templates[currentTest.value]
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const initObjects = () => {
|
|||
|
|
objects = []
|
|||
|
|
const colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6']
|
|||
|
|
|
|||
|
|
for (let i = 0; i < objectCount.value; i++) {
|
|||
|
|
objects.push({
|
|||
|
|
x: Math.random() * 600,
|
|||
|
|
y: Math.random() * 400,
|
|||
|
|
size: 2 + Math.random() * 3,
|
|||
|
|
color: colors[Math.floor(Math.random() * colors.length)],
|
|||
|
|
vx: (Math.random() - 0.5) * 2,
|
|||
|
|
vy: (Math.random() - 0.5) * 2,
|
|||
|
|
oldX: 0,
|
|||
|
|
oldY: 0,
|
|||
|
|
moved: false
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const initOffscreenCanvas = () => {
|
|||
|
|
if (!offscreenCanvasRef.value) return
|
|||
|
|
offscreenCtx = offscreenCanvasRef.value.getContext('2d')
|
|||
|
|
|
|||
|
|
// 预渲染静态背景
|
|||
|
|
offscreenCtx.fillStyle = '#fafafa'
|
|||
|
|
offscreenCtx.fillRect(0, 0, 600, 400)
|
|||
|
|
|
|||
|
|
// 绘制网格
|
|||
|
|
offscreenCtx.strokeStyle = '#e0e0e0'
|
|||
|
|
offscreenCtx.lineWidth = 1
|
|||
|
|
for (let x = 0; x < 600; x += 50) {
|
|||
|
|
offscreenCtx.beginPath()
|
|||
|
|
offscreenCtx.moveTo(x, 0)
|
|||
|
|
offscreenCtx.lineTo(x, 400)
|
|||
|
|
offscreenCtx.stroke()
|
|||
|
|
}
|
|||
|
|
for (let y = 0; y < 400; y += 50) {
|
|||
|
|
offscreenCtx.beginPath()
|
|||
|
|
offscreenCtx.moveTo(0, y)
|
|||
|
|
offscreenCtx.lineTo(600, y)
|
|||
|
|
offscreenCtx.stroke()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const drawRedrawTest = (ctx) => {
|
|||
|
|
if (useDirtyRect.value) {
|
|||
|
|
// 只重绘移动的对象
|
|||
|
|
objects.forEach((obj) => {
|
|||
|
|
if (obj.moved) {
|
|||
|
|
ctx.clearRect(obj.oldX - obj.size - 1, obj.oldY - obj.size - 1, obj.size * 2 + 2, obj.size * 2 + 2)
|
|||
|
|
ctx.fillStyle = obj.color
|
|||
|
|
ctx.beginPath()
|
|||
|
|
ctx.arc(obj.x, obj.y, obj.size, 0, Math.PI * 2)
|
|||
|
|
ctx.fill()
|
|||
|
|
obj.moved = false
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
// 清除整个画布
|
|||
|
|
ctx.clearRect(0, 0, 600, 400)
|
|||
|
|
ctx.fillStyle = '#fafafa'
|
|||
|
|
ctx.fillRect(0, 0, 600, 400)
|
|||
|
|
|
|||
|
|
// 绘制所有对象
|
|||
|
|
objects.forEach((obj) => {
|
|||
|
|
ctx.fillStyle = obj.color
|
|||
|
|
ctx.beginPath()
|
|||
|
|
ctx.arc(obj.x, obj.y, obj.size, 0, Math.PI * 2)
|
|||
|
|
ctx.fill()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const drawLayerTest = (ctx) => {
|
|||
|
|
if (useOffscreenCanvas.value && offscreenCtx) {
|
|||
|
|
// 复制预渲染的背景
|
|||
|
|
ctx.drawImage(offscreenCanvasRef.value, 0, 0)
|
|||
|
|
} else {
|
|||
|
|
// 绘制背景
|
|||
|
|
ctx.fillStyle = '#fafafa'
|
|||
|
|
ctx.fillRect(0, 0, 600, 400)
|
|||
|
|
ctx.strokeStyle = '#e0e0e0'
|
|||
|
|
ctx.lineWidth = 1
|
|||
|
|
for (let x = 0; x < 600; x += 50) {
|
|||
|
|
ctx.beginPath()
|
|||
|
|
ctx.moveTo(x, 0)
|
|||
|
|
ctx.lineTo(x, 400)
|
|||
|
|
ctx.stroke()
|
|||
|
|
}
|
|||
|
|
for (let y = 0; y < 400; y += 50) {
|
|||
|
|
ctx.beginPath()
|
|||
|
|
ctx.moveTo(0, y)
|
|||
|
|
ctx.lineTo(600, y)
|
|||
|
|
ctx.stroke()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 绘制动态对象
|
|||
|
|
objects.forEach((obj) => {
|
|||
|
|
ctx.fillStyle = obj.color
|
|||
|
|
ctx.beginPath()
|
|||
|
|
ctx.arc(obj.x, obj.y, obj.size, 0, Math.PI * 2)
|
|||
|
|
ctx.fill()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const drawBatchTest = (ctx) => {
|
|||
|
|
ctx.clearRect(0, 0, 600, 400)
|
|||
|
|
ctx.fillStyle = '#fafafa'
|
|||
|
|
ctx.fillRect(0, 0, 600, 400)
|
|||
|
|
|
|||
|
|
if (useBatching.value) {
|
|||
|
|
// 按颜色分组批量渲染
|
|||
|
|
const batches = {}
|
|||
|
|
objects.forEach((obj) => {
|
|||
|
|
if (!batches[obj.color]) {
|
|||
|
|
batches[obj.color] = []
|
|||
|
|
}
|
|||
|
|
batches[obj.color].push(obj)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
Object.keys(batches).forEach((color) => {
|
|||
|
|
ctx.fillStyle = color
|
|||
|
|
batches[color].forEach((obj) => {
|
|||
|
|
ctx.beginPath()
|
|||
|
|
ctx.arc(obj.x, obj.y, obj.size, 0, Math.PI * 2)
|
|||
|
|
ctx.fill()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
// 逐个渲染
|
|||
|
|
objects.forEach((obj) => {
|
|||
|
|
ctx.fillStyle = obj.color
|
|||
|
|
ctx.beginPath()
|
|||
|
|
ctx.arc(obj.x, obj.y, obj.size, 0, Math.PI * 2)
|
|||
|
|
ctx.fill()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const updateObjects = () => {
|
|||
|
|
objects.forEach((obj) => {
|
|||
|
|
obj.oldX = obj.x
|
|||
|
|
obj.oldY = obj.y
|
|||
|
|
obj.x += obj.vx
|
|||
|
|
obj.y += obj.vy
|
|||
|
|
|
|||
|
|
if (obj.x < 0 || obj.x > 600) obj.vx = -obj.vx
|
|||
|
|
if (obj.y < 0 || obj.y > 400) obj.vy = -obj.vy
|
|||
|
|
|
|||
|
|
if (obj.x !== obj.oldX || obj.y !== obj.oldY) {
|
|||
|
|
obj.moved = true
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const draw = () => {
|
|||
|
|
const canvas = canvasRef.value
|
|||
|
|
if (!canvas) return
|
|||
|
|
const ctx = canvas.getContext('2d')
|
|||
|
|
|
|||
|
|
updateObjects()
|
|||
|
|
|
|||
|
|
switch (currentTest.value) {
|
|||
|
|
case 'redraw':
|
|||
|
|
drawRedrawTest(ctx)
|
|||
|
|
break
|
|||
|
|
case 'layer':
|
|||
|
|
drawLayerTest(ctx)
|
|||
|
|
break
|
|||
|
|
case 'batch':
|
|||
|
|
drawBatchTest(ctx)
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const animate = (timestamp) => {
|
|||
|
|
if (!lastTime) lastTime = timestamp
|
|||
|
|
const deltaTime = timestamp - lastTime
|
|||
|
|
frameTime.value = deltaTime.toFixed(2)
|
|||
|
|
|
|||
|
|
frameCount++
|
|||
|
|
fpsTime += deltaTime
|
|||
|
|
if (fpsTime >= 1000) {
|
|||
|
|
fps.value = Math.round((frameCount * 1000) / fpsTime)
|
|||
|
|
if (!showComparison.value && !useDirtyRect.value && !useOffscreenCanvas.value && !useBatching.value) {
|
|||
|
|
baselineFps.value = fps.value
|
|||
|
|
}
|
|||
|
|
frameCount = 0
|
|||
|
|
fpsTime = 0
|
|||
|
|
showComparison.value = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
lastTime = timestamp
|
|||
|
|
draw()
|
|||
|
|
animationId = requestAnimationFrame(animate)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const switchTest = (test) => {
|
|||
|
|
currentTest.value = test
|
|||
|
|
resetTest()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const resetTest = () => {
|
|||
|
|
if (animationId) {
|
|||
|
|
cancelAnimationFrame(animationId)
|
|||
|
|
}
|
|||
|
|
lastTime = 0
|
|||
|
|
frameCount = 0
|
|||
|
|
fpsTime = 0
|
|||
|
|
fps.value = 0
|
|||
|
|
baselineFps.value = 0
|
|||
|
|
showComparison.value = false
|
|||
|
|
useDirtyRect.value = false
|
|||
|
|
useOffscreenCanvas.value = false
|
|||
|
|
useBatching.value = false
|
|||
|
|
|
|||
|
|
initObjects()
|
|||
|
|
if (currentTest.value === 'layer') {
|
|||
|
|
initOffscreenCanvas()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
animationId = requestAnimationFrame(animate)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
watch([useDirtyRect, useOffscreenCanvas, useBatching], () => {
|
|||
|
|
showComparison.value = true
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
initObjects()
|
|||
|
|
initOffscreenCanvas()
|
|||
|
|
animationId = requestAnimationFrame(animate)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onUnmounted(() => {
|
|||
|
|
if (animationId) {
|
|||
|
|
cancelAnimationFrame(animationId)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.performance-demo {
|
|||
|
|
border: 1px solid #e0e0e0;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 20px;
|
|||
|
|
background: #fafafa;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.control-panel {
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.test-selector {
|
|||
|
|
margin-bottom: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.test-selector label {
|
|||
|
|
display: block;
|
|||
|
|
font-weight: 600;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
color: #2c3e50;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.button-group {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 8px;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.button-group button {
|
|||
|
|
padding: 8px 16px;
|
|||
|
|
border: 2px solid #ddd;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 14px;
|
|||
|
|
transition: all 0.2s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.button-group button:hover {
|
|||
|
|
border-color: #3498db;
|
|||
|
|
background: #f0f8ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.button-group button.active {
|
|||
|
|
border-color: #3498db;
|
|||
|
|
background: #3498db;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.parameters {
|
|||
|
|
margin-bottom: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.param-row {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 6px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.param-row label {
|
|||
|
|
font-size: 13px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
color: #555;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.param-row input[type="range"] {
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.optimization-toggles {
|
|||
|
|
margin-bottom: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.optimization-toggles label {
|
|||
|
|
display: block;
|
|||
|
|
font-weight: 600;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
color: #2c3e50;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toggle-grid {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 15px;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toggle-option {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 14px;
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
border: 1px solid #ddd;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toggle-option input[type="checkbox"] {
|
|||
|
|
width: 18px;
|
|||
|
|
height: 18px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stats {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 20px;
|
|||
|
|
margin-bottom: 15px;
|
|||
|
|
padding: 12px;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-item {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-item .label {
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #555;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-item .value {
|
|||
|
|
font-family: 'Courier New', monospace;
|
|||
|
|
color: #2c3e50;
|
|||
|
|
background: #f0f0f0;
|
|||
|
|
padding: 4px 12px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-item .value.good {
|
|||
|
|
background: #d4edda;
|
|||
|
|
color: #155724;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-item .value.warning {
|
|||
|
|
background: #fff3cd;
|
|||
|
|
color: #856404;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-item .value.bad {
|
|||
|
|
background: #f8d7da;
|
|||
|
|
color: #721c24;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.reset-btn {
|
|||
|
|
padding: 8px 16px;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
background: #95a5a6;
|
|||
|
|
color: white;
|
|||
|
|
font-size: 13px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.2s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.reset-btn:hover {
|
|||
|
|
background: #7f8c8d;
|
|||
|
|
transform: translateY(-1px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.canvas-container {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: center;
|
|||
|
|
margin: 20px 0;
|
|||
|
|
padding: 20px;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
canvas {
|
|||
|
|
border: 2px solid #ddd;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comparison {
|
|||
|
|
margin: 20px 0;
|
|||
|
|
padding: 15px;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comparison h4 {
|
|||
|
|
margin: 0 0 15px 0;
|
|||
|
|
color: #2c3e50;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comparison-table {
|
|||
|
|
overflow-x: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comparison-table table {
|
|||
|
|
width: 100%;
|
|||
|
|
border-collapse: collapse;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comparison-table th,
|
|||
|
|
.comparison-table td {
|
|||
|
|
padding: 10px;
|
|||
|
|
text-align: left;
|
|||
|
|
border-bottom: 1px solid #e0e0e0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comparison-table th {
|
|||
|
|
background: #f8f9fa;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #2c3e50;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.code-display {
|
|||
|
|
margin-top: 20px;
|
|||
|
|
padding: 15px;
|
|||
|
|
background: #2c3e50;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
overflow-x: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.code-display h4 {
|
|||
|
|
color: #ecf0f1;
|
|||
|
|
margin: 0 0 10px 0;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.code-display pre {
|
|||
|
|
margin: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.code-display code {
|
|||
|
|
color: #ecf0f1;
|
|||
|
|
font-family: 'Courier New', monospace;
|
|||
|
|
font-size: 12px;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.explanation {
|
|||
|
|
margin: 20px 0;
|
|||
|
|
padding: 15px;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.explanation h4 {
|
|||
|
|
margin: 0 0 10px 0;
|
|||
|
|
color: #2c3e50;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.explanation ul {
|
|||
|
|
margin: 0;
|
|||
|
|
padding-left: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.explanation li {
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
color: #555;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
</style>
|