更新 AI03 课程内容,替换为 AICODE-03 和 AICODE-06 课程体系
- 删除旧的 AI03 课程文件(第4-5课) - 新增 AICODE-03 课程体系(AI是怎么想的、AI训练师、我是大作家等) - 新增 AICODE-06 课程体系(从扣子到代码、个人主页、涂鸦PK等)
This commit is contained in:
422
.claude/lesson/AI03/AICODE-06/demo-pk/demo-1-draw-tool.html
Normal file
422
.claude/lesson/AI03/AICODE-06/demo-pk/demo-1-draw-tool.html
Normal 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 已导出!\n(128×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>
|
||||
Reference in New Issue
Block a user