feat: 新增涂鸦PK四课教案(第8-11课)及大纲更新

- 新增 AICODE06-08~11 完整逐字稿教案(每课600+行)
- 涂鸦PK主题:画图工具→基础对战→动画音效→班级锦标赛
- 核心工程思维:需求驱动→测试验证→增量迭代→数据驱动
- 更新 AICODE-06 课程大纲,追加第8-11课内容
- 新增 demo-pk/ 目录(画图工具/对战/动画三个demo)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rocky
2026-04-09 20:28:42 +02:00
parent 63d8edaa18
commit bad433a121
10 changed files with 4785 additions and 244 deletions

View File

@@ -0,0 +1,422 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>涂鸦角色画图工具</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
font-family: Arial, sans-serif;
color: #fff;
padding: 20px;
gap: 16px;
}
h1 { font-size: 22px; color: #ffd700; letter-spacing: 2px; }
.app { display: flex; gap: 24px; align-items: flex-start; }
/* 左侧画板 */
.left { display: flex; flex-direction: column; gap: 10px; }
.frame-tabs { display: flex; gap: 0; }
.tab {
padding: 8px 20px;
background: #2a2a4a;
border: 2px solid #444;
cursor: pointer;
font-size: 13px;
color: #aaa;
border-bottom: none;
}
.tab:first-child { border-radius: 8px 0 0 0; }
.tab:last-child { border-radius: 0 8px 0 0; }
.tab.active { background: #3a3a6a; border-color: #ffd700; color: #ffd700; }
#display-canvas {
display: block;
cursor: crosshair;
border: 2px solid #ffd700;
image-rendering: pixelated;
}
.tools { display: flex; gap: 8px; flex-wrap: wrap; }
.tool-btn {
padding: 7px 14px;
background: #2a2a4a;
border: 2px solid #555;
border-radius: 6px;
cursor: pointer;
color: #fff;
font-size: 13px;
transition: all 0.1s;
}
.tool-btn.active { border-color: #ffd700; background: #3a3a6a; color: #ffd700; }
.tool-btn:hover:not(.active) { border-color: #888; }
.palette { display: flex; flex-wrap: wrap; gap: 4px; max-width: 520px; }
.swatch {
width: 26px; height: 26px;
border-radius: 4px;
cursor: pointer;
border: 2px solid transparent;
transition: transform 0.1s;
}
.swatch:hover { transform: scale(1.2); }
.swatch.active { border-color: #fff; transform: scale(1.2); }
/* 右侧面板 */
.right { display: flex; flex-direction: column; gap: 12px; min-width: 160px; }
.panel {
background: #16213e;
border: 1px solid #0f3460;
border-radius: 10px;
padding: 14px;
display: flex;
flex-direction: column;
gap: 8px;
}
.panel h3 { font-size: 12px; color: #ffd700; text-transform: uppercase; letter-spacing: 1px; }
#preview-canvas {
image-rendering: pixelated;
align-self: center;
border: 1px solid #333;
background: #0a0a1a;
}
.btn {
padding: 9px 14px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: bold;
width: 100%;
transition: opacity 0.1s;
}
.btn:hover { opacity: 0.85; }
.btn-gold { background: #ffd700; color: #1a1a2e; }
.btn-blue { background: #0f3460; color: #fff; border: 1px solid #1a5276; }
.btn-red { background: #c0392b; color: #fff; }
.hint {
font-size: 11px;
color: #556;
line-height: 1.7;
}
.size-tag { font-size: 11px; color: #556; text-align: center; }
</style>
</head>
<body>
<h1>🎨 涂鸦角色画图工具</h1>
<div class="app">
<div class="left">
<div class="frame-tabs">
<div class="tab active" id="tab0" onclick="switchFrame(0)">帧1 · 待机</div>
<div class="tab" id="tab1" onclick="switchFrame(1)">帧2 · 攻击</div>
</div>
<canvas id="display-canvas"></canvas>
<div class="tools">
<button class="tool-btn active" id="btn-pen" onclick="setTool('pen')">🖊 画笔</button>
<button class="tool-btn" id="btn-eraser" onclick="setTool('eraser')">⬜ 橡皮</button>
<button class="tool-btn" id="btn-fill" onclick="setTool('fill')">🪣 填充</button>
<input type="color" id="custom-color" value="#ffffff"
onchange="setColor(this.value)"
title="自定义颜色"
style="width:36px;height:34px;border:2px solid #555;border-radius:6px;cursor:pointer;background:none;padding:1px;">
</div>
<div class="palette" id="palette"></div>
<div class="size-tag">画布 64×64 像素 · 8倍放大显示</div>
</div>
<div class="right">
<div class="panel">
<h3>动画预览</h3>
<canvas id="preview-canvas" width="128" height="128"></canvas>
<button class="btn btn-blue" onclick="togglePreview()" id="preview-btn">▶ 播放预览</button>
</div>
<div class="panel">
<h3>操作</h3>
<button class="btn btn-blue" onclick="copyFrame1to2()">📋 复制帧1→帧2</button>
<button class="btn btn-red" onclick="clearCurrentFrame()">🗑 清空当前帧</button>
</div>
<div class="panel">
<h3>导出</h3>
<button class="btn btn-gold" onclick="exportSpritesheet()">💾 导出 Spritesheet</button>
<button class="btn btn-blue" onclick="exportCurrentFrame()">💾 导出单帧 PNG</button>
</div>
<div class="panel">
<h3>使用说明</h3>
<div class="hint">
1. 在<b>帧1</b>画待机姿势<br>
2. 点「复制帧1→帧2」<br>
3. 切到<b>帧2</b>修改成攻击姿势<br>
4. 点▶预览动画效果<br>
5. 导出 Spritesheet 用于游戏
</div>
</div>
</div>
</div>
<script>
// ============================================================
// 配置
// ============================================================
const PX = 64; // 画布逻辑尺寸
const ZOOM = 8; // 显示倍率
const W = PX * ZOOM; // 显示尺寸 = 512
// ============================================================
// 数据
// ============================================================
const frameCanvases = [makeOffscreen(), makeOffscreen()];
let currentFrame = 0;
let currentTool = 'pen';
let currentColor = '#ffffff';
let isDrawing = false;
let previewTimer = null;
let previewIdx = 0;
function makeOffscreen() {
const c = document.createElement('canvas');
c.width = PX; c.height = PX;
return c;
}
// ============================================================
// 颜色调色板
// ============================================================
const COLORS = [
'#ffffff','#cccccc','#888888','#444444','#111111',
'#ff4444','#ff8800','#ffcc00','#aaff00','#00ff88',
'#00ffff','#0088ff','#4400ff','#aa00ff','#ff00aa',
'#ffaaaa','#ffddaa','#ffffaa','#aaffcc','#aaddff',
'#8B4513','#d2691e','#f4a460','#228B22','#006400',
'#2e86ab','#e94560','#f5a623','#7ed321','#417505',
];
function buildPalette() {
const el = document.getElementById('palette');
COLORS.forEach(c => {
const s = document.createElement('div');
s.className = 'swatch' + (c === currentColor ? ' active' : '');
s.style.background = c;
s.title = c;
s.onclick = () => {
currentColor = c;
document.getElementById('custom-color').value = c;
document.querySelectorAll('.swatch').forEach(x => x.classList.remove('active'));
s.classList.add('active');
};
el.appendChild(s);
});
}
function setColor(hex) {
currentColor = hex;
document.querySelectorAll('.swatch').forEach(x => x.classList.remove('active'));
}
// ============================================================
// 工具切换
// ============================================================
function setTool(t) {
currentTool = t;
['pen','eraser','fill'].forEach(n => {
document.getElementById('btn-' + n)?.classList.toggle('active', n === t);
});
}
// ============================================================
// 显示画布
// ============================================================
const display = document.getElementById('display-canvas');
display.width = W;
display.height = W;
const dCtx = display.getContext('2d');
dCtx.imageSmoothingEnabled = false;
function render() {
dCtx.clearRect(0, 0, W, W);
// 棋盘格背景(表示透明区域)
for (let y = 0; y < PX; y++) {
for (let x = 0; x < PX; x++) {
dCtx.fillStyle = (x + y) % 2 === 0 ? '#222' : '#1a1a1a';
dCtx.fillRect(x * ZOOM, y * ZOOM, ZOOM, ZOOM);
}
}
// 当前帧像素
dCtx.imageSmoothingEnabled = false;
dCtx.drawImage(frameCanvases[currentFrame], 0, 0, PX, PX, 0, 0, W, W);
// 网格线
dCtx.strokeStyle = 'rgba(255,255,255,0.04)';
dCtx.lineWidth = 0.5;
for (let i = 0; i <= PX; i++) {
dCtx.beginPath(); dCtx.moveTo(i * ZOOM, 0); dCtx.lineTo(i * ZOOM, W); dCtx.stroke();
dCtx.beginPath(); dCtx.moveTo(0, i * ZOOM); dCtx.lineTo(W, i * ZOOM); dCtx.stroke();
}
}
// ============================================================
// 画像素
// ============================================================
function getPos(e) {
const r = display.getBoundingClientRect();
return {
x: Math.max(0, Math.min(PX - 1, Math.floor((e.clientX - r.left) / ZOOM))),
y: Math.max(0, Math.min(PX - 1, Math.floor((e.clientY - r.top) / ZOOM))),
};
}
function putPixel(x, y) {
const fc = frameCanvases[currentFrame].getContext('2d');
if (currentTool === 'eraser') {
fc.clearRect(x, y, 1, 1);
} else {
fc.fillStyle = currentColor;
fc.fillRect(x, y, 1, 1);
}
render();
}
function floodFill(sx, sy) {
const fc = frameCanvases[currentFrame].getContext('2d');
const img = fc.getImageData(0, 0, PX, PX);
const d = img.data;
const ti = (sx + sy * PX) * 4;
const tR = d[ti], tG = d[ti+1], tB = d[ti+2], tA = d[ti+3];
const hex = currentColor.replace('#','');
const fR = parseInt(hex.slice(0,2),16);
const fG = parseInt(hex.slice(2,4),16);
const fB = parseInt(hex.slice(4,6),16);
if (tR===fR && tG===fG && tB===fB && tA===255) return;
const stack = [[sx, sy]];
const seen = new Set();
while (stack.length) {
const [x, y] = stack.pop();
if (x<0||x>=PX||y<0||y>=PX) continue;
const k = x + ',' + y;
if (seen.has(k)) continue;
seen.add(k);
const i = (x + y * PX) * 4;
if (d[i]!==tR||d[i+1]!==tG||d[i+2]!==tB||d[i+3]!==tA) continue;
d[i]=fR; d[i+1]=fG; d[i+2]=fB; d[i+3]=255;
stack.push([x+1,y],[x-1,y],[x,y+1],[x,y-1]);
}
fc.putImageData(img, 0, 0);
render();
}
display.addEventListener('mousedown', e => {
isDrawing = true;
const { x, y } = getPos(e);
currentTool === 'fill' ? floodFill(x, y) : putPixel(x, y);
});
display.addEventListener('mousemove', e => {
if (!isDrawing || currentTool === 'fill') return;
putPixel(getPos(e).x, getPos(e).y);
});
display.addEventListener('mouseup', () => isDrawing = false);
display.addEventListener('mouseleave', () => isDrawing = false);
// ============================================================
// 帧切换
// ============================================================
function switchFrame(idx) {
currentFrame = idx;
document.getElementById('tab0').classList.toggle('active', idx === 0);
document.getElementById('tab1').classList.toggle('active', idx === 1);
render();
}
// ============================================================
// 动画预览
// ============================================================
const previewCanvas = document.getElementById('preview-canvas');
const pCtx = previewCanvas.getContext('2d');
pCtx.imageSmoothingEnabled = false;
function togglePreview() {
const btn = document.getElementById('preview-btn');
if (previewTimer) {
clearInterval(previewTimer);
previewTimer = null;
btn.textContent = '▶ 播放预览';
pCtx.clearRect(0,0,128,128);
return;
}
btn.textContent = '⏹ 停止预览';
previewTimer = setInterval(() => {
pCtx.clearRect(0,0,128,128);
// 棋盘格
for (let y=0;y<4;y++) for (let x=0;x<4;x++) {
pCtx.fillStyle=(x+y)%2===0?'#222':'#1a1a1a';
pCtx.fillRect(x*32,y*32,32,32);
}
pCtx.imageSmoothingEnabled = false;
pCtx.drawImage(frameCanvases[previewIdx], 0, 0, PX, PX, 0, 0, 128, 128);
previewIdx = (previewIdx + 1) % 2;
}, 350);
}
// ============================================================
// 操作
// ============================================================
function copyFrame1to2() {
const src = frameCanvases[0].getContext('2d').getImageData(0,0,PX,PX);
frameCanvases[1].getContext('2d').putImageData(src, 0, 0);
if (currentFrame === 1) render();
alert('✅ 已将帧1复制到帧2\n现在切到帧2修改出攻击姿势吧');
}
function clearCurrentFrame() {
if (!confirm('确定清空当前帧吗?')) return;
frameCanvases[currentFrame].getContext('2d').clearRect(0,0,PX,PX);
render();
}
// ============================================================
// 导出
// ============================================================
function exportSpritesheet() {
const out = document.createElement('canvas');
out.width = PX * 2;
out.height = PX;
const oc = out.getContext('2d');
oc.imageSmoothingEnabled = false;
oc.drawImage(frameCanvases[0], 0, 0);
oc.drawImage(frameCanvases[1], PX, 0);
downloadCanvas(out, 'character-sheet.png');
alert('✅ Spritesheet 已导出!\n128×64 像素包含帧1和帧2');
}
function exportCurrentFrame() {
const name = currentFrame === 0 ? 'character-idle.png' : 'character-attack.png';
downloadCanvas(frameCanvases[currentFrame], name);
}
function downloadCanvas(c, name) {
const a = document.createElement('a');
a.download = name;
a.href = c.toDataURL('image/png');
a.click();
}
// ============================================================
// 初始化
// ============================================================
buildPalette();
render();
</script>
</body>
</html>