<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简易Scratch编程工具</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #6e8efb, #a777e3);
color: #333;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.9);
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
header {
background: #4a6cf9;
color: white;
padding: 20px;
text-align: center;
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
}
.main-content {
display: flex;
padding: 20px;
gap: 20px;
}
.blocks-panel {
flex: 1;
background: #f0f4ff;
border-radius: 10px;
padding: 15px;
}
.blocks-category {
margin-bottom: 20px;
}
.category-title {
font-size: 1.2rem;
color: #4a6cf9;
padding: 10px;
border-bottom: 2px solid #4a6cf9;
margin-bottom: 10px;
}
.block {
background: white;
padding: 12px;
margin: 8px 0;
border-radius: 8px;
cursor: move;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
border-left: 5px solid #4a6cf9;
transition: transform 0.2s, box-shadow 0.2s;
user-select: none;
}
.block:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.motion-block {
border-left-color: #4a6cf9;
}
.looks-block {
border-left-color: #9c56e0;
}
.control-block {
border-left-color: #e74c3c;
}
.workspace {
flex: 2;
background: #e6e9ff;
border-radius: 10px;
padding: 15px;
min-height: 400px;
}
.workspace .block {
margin: 10px 0;
}
.stage-container {
flex: 2;
background: #d1d9ff;
border-radius: 10px;
padding: 15px;
display: flex;
flex-direction: column;
}
.stage {
background: white;
border: 3px solid #4a6cf9;
border-radius: 8px;
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
canvas {
background: white;
}
.controls {
display: flex;
justify-content: center;
gap: 20px;
padding: 20px;
}
.btn {
padding: 12px 25px;
border: none;
border-radius: 50px;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.run-btn {
background: #2ecc71;
color: white;
}
.stop-btn {
background: #e74c3c;
color: white;
}
.reset-btn {
background: #f39c12;
color: white;
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.25);
}
.btn:active {
transform: translateY(0);
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.instructions {
background: #f8f9ff;
padding: 20px;
border-radius: 10px;
margin: 20px;
}
.instructions h3 {
color: #4a6cf9;
margin-bottom: 10px;
}
.status {
text-align: center;
padding: 10px;
font-weight: bold;
color: #4a6cf9;
}
footer {
text-align: center;
padding: 20px;
color: white;
background: #4a6cf9;
margin-top: 20px;
}
@media (max-width: 900px) {
.main-content {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>简易Scratch编程工具 </h1>
<p class="subtitle">通过拖放积木创建程序,并查看运行效果</p>
</header>
<div class="status" id="status">就绪</div>
<div class="main-content">
<div class="blocks-panel">
<div class="blocks-category">
<h3 class="category-title">运动</h3>
<div class="block motion-block" draggable="true" data-type="move">移动 10 步</div>
<div class="block motion-block" draggable="true" data-type="turn">向右旋转 15 度</div>
<div class="block motion-block" draggable="true" data-type="goto">移到随机位置</div>
</div>
<div class="blocks-category">
<h3 class="category-title">外观</h3>
<div class="block looks-block" draggable="true" data-type="say">说 你好! 2 秒</div>
<div class="block looks-block" draggable="true" data-type="size">将大小增加 10</div>
</div>
<div class="blocks-category">
<h3 class="category-title">控制</h3>
<div class="block control-block" draggable="true" data-type="repeat">重复执行 3 次</div>
<div class="block control-block" draggable="true" data-type="forever">永远重复</div>
</div>
</div>
<div class="workspace" id="workspace">
<h3>拖放积木到这里</h3>
</div>
<div class="stage-container">
<h3>舞台</h3>
<div class="stage">
<canvas id="stageCanvas" width="300" height="300"></canvas>
</div>
</div>
</div>
<div class="controls">
<button class="btn run-btn" id="runBtn">运行</button>
<button class="btn stop-btn" id="stopBtn" disabled>停止</button>
<button class="btn reset-btn" id="resetBtn">重置</button>
</div>
<div class="instructions">
<h3>使用说明</h3>
<p>1. 从左侧选择积木并拖放到中间工作区</p>
<p>2. 点击"运行"按钮执行程序</p>
<p>3. 在右侧舞台查看程序运行效果</p>
<p>4. 点击"停止"按钮停止程序执行</p>
<p>5. 点击"重置"按钮清除工作区和重置舞台</p>
<p>6. 双击工作区中的积木可以删除它</p>
</div>
<footer>
<p>© 2023 简易Scratch编程工具 | 设计用于教育目的</p>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const workspace = document.getElementById('workspace');
const canvas = document.getElementById('stageCanvas');
const ctx = canvas.getContext('2d');
const runBtn = document.getElementById('runBtn');
const stopBtn = document.getElementById('stopBtn');
const resetBtn = document.getElementById('resetBtn');
const status = document.getElementById('status');
// 角色状态
let character = {
x: canvas.width / 2,
y: canvas.height / 2,
size: 30,
direction: 90, // 角度,90度向右
visible: true,
speech: null,
speechTime: 0
};
let animationId = null;
let isRunning = false;
let executionQueue = [];
// 初始化舞台
drawCharacter();
// 设置拖放事件
const blocks = document.querySelectorAll('.blocks-panel .block');
blocks.forEach(block => {
block.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', block.getAttribute('data-type'));
});
});
workspace.addEventListener('dragover', function(e) {
e.preventDefault();
workspace.style.backgroundColor = '#d6dcff';
});
workspace.addEventListener('dragleave', function() {
workspace.style.backgroundColor = '#e6e9ff';
});
workspace.addEventListener('drop', function(e) {
e.preventDefault();
workspace.style.backgroundColor = '#e6e9ff';
const blockType = e.dataTransfer.getData('text/plain');
const originalBlock = document.querySelector(`.blocks-panel .block[data-type="${blockType}"]`);
const newBlock = originalBlock.cloneNode(true);
// 设置可拖动和删除
newBlock.addEventListener('dragstart', function(ev) {
ev.dataTransfer.setData('text/plain', blockType);
});
newBlock.addEventListener('dblclick', function() {
if (!isRunning) {
workspace.removeChild(newBlock);
updateStatus("积木已删除");
} else {
updateStatus("请先停止程序再删除积木");
}
});
workspace.appendChild(newBlock);
updateStatus("积木已添加到工作区");
});
// 按钮事件
runBtn.addEventListener('click', function() {
if (isRunning) return;
runProgram();
});
stopBtn.addEventListener('click', function() {
stopProgram();
});
resetBtn.addEventListener('click', function() {
resetProgram();
});
// 运行程序
function runProgram() {
const blocks = workspace.querySelectorAll('.block');
// 修复:正确识别工作区中的积木数量
if (blocks.length === 0) {
updateStatus("请拖放一些积木到工作区!");
return;
}
isRunning = true;
runBtn.disabled = true;
stopBtn.disabled = false;
updateStatus("程序运行中...");
// 清空执行队列
executionQueue = [];
// 将积木转换为执行指令
for (let i = 0; i < blocks.length; i++) {
const type = blocks[i].getAttribute('data-type');
executionQueue.push({
type: type,
element: blocks[i]
});
}
// 开始执行
executeNextCommand(0);
}
function executeNextCommand(index) {
if (!isRunning || index >= executionQueue.length) {
if (isRunning) {
updateStatus("程序执行完成");
}
isRunning = false;
runBtn.disabled = false;
stopBtn.disabled = true;
return;
}
const command = executionQueue[index];
highlightBlock(command.element, true);
switch(command.type) {
case 'move':
moveCharacter(10);
setTimeout(() => {
highlightBlock(command.element, false);
executeNextCommand(index + 1);
}, 500);
break;
case 'turn':
turnCharacter(15);
setTimeout(() => {
highlightBlock(command.element, false);
executeNextCommand(index + 1);
}, 500);
break;
case 'goto':
gotoRandom();
setTimeout(() => {
highlightBlock(command.element, false);
executeNextCommand(index + 1);
}, 500);
break;
case 'say':
saySomething("你好!", 2);
setTimeout(() => {
highlightBlock(command.element, false);
executeNextCommand(index + 1);
}, 2000);
break;
case 'size':
changeSize(10);
setTimeout(() => {
highlightBlock(command.element, false);
executeNextCommand(index + 1);
}, 500);
break;
case 'repeat':
// 修复:使用递归实现重复执行
const repeatCount = 3;
highlightBlock(command.element, false);
executeRepeatCommands(index + 1, repeatCount, () => {
// 找到重复块结束的位置
let endIndex = index + 1;
while (endIndex < executionQueue.length &&
executionQueue[endIndex].type !== 'repeat' &&
executionQueue[endIndex].type !== 'forever') {
endIndex++;
}
executeNextCommand(endIndex);
});
break;
case 'forever':
// 修复:使用递归实现永久重复
if (isRunning) {
highlightBlock(command.element, false);
executeForeverCommands(index + 1, () => {
executeNextCommand(index); // 循环回自己
});
}
break;
default:
setTimeout(() => {
highlightBlock(command.element, false);
executeNextCommand(index + 1);
}, 300);
}
}
function executeRepeatCommands(startIndex, count, callback) {
if (count <= 0 || !isRunning) {
callback();
return;
}
// 执行重复块内的命令
executeCommandSequence(startIndex, () => {
// 完成后减少计数并再次执行
executeRepeatCommands(startIndex, count - 1, callback);
});
}
function executeForeverCommands(startIndex, callback) {
if (!isRunning) {
callback();
return;
}
// 执行永久重复块内的命令
executeCommandSequence(startIndex, () => {
// 完成后再次执行
if (isRunning) {
executeForeverCommands(startIndex, callback);
} else {
callback();
}
});
}
function executeCommandSequence(startIndex, callback) {
if (!isRunning || startIndex >= executionQueue.length) {
callback();
return;
}
const command = executionQueue[startIndex];
// 如果遇到控制块,停止执行
if (command.type === 'repeat' || command.type === 'forever') {
callback();
return;
}
highlightBlock(command.element, true);
switch(command.type) {
case 'move':
moveCharacter(10);
setTimeout(() => {
highlightBlock(command.element, false);
executeCommandSequence(startIndex + 1, callback);
}, 500);
break;
case 'turn':
turnCharacter(15);
setTimeout(() => {
highlightBlock(command.element, false);
executeCommandSequence(startIndex + 1, callback);
}, 500);
break;
case 'goto':
gotoRandom();
setTimeout(() => {
highlightBlock(command.element, false);
executeCommandSequence(startIndex + 1, callback);
}, 500);
break;
case 'say':
saySomething("你好!", 2);
setTimeout(() => {
highlightBlock(command.element, false);
executeCommandSequence(startIndex + 1, callback);
}, 2000);
break;
case 'size':
changeSize(10);
setTimeout(() => {
highlightBlock(command.element, false);
executeCommandSequence(startIndex + 1, callback);
}, 500);
break;
default:
setTimeout(() => {
highlightBlock(command.element, false);
executeCommandSequence(startIndex + 1, callback);
}, 300);
}
}
function highlightBlock(element, highlight) {
if (highlight) {
element.style.backgroundColor = '#ffeb3b';
element.style.boxShadow = '0 0 10px #ffeb3b';
} else {
element.style.backgroundColor = 'white';
element.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.1)';
}
}
function stopProgram() {
isRunning = false;
runBtn.disabled = false;
stopBtn.disabled = true;
updateStatus("程序已停止");
// 移除所有高亮
const blocks = workspace.querySelectorAll('.block');
blocks.forEach(block => {
highlightBlock(block, false);
});
}
function resetProgram() {
stopProgram();
// 重置角色
character = {
x: canvas.width / 2,
y: canvas.height / 2,
size: 30,
direction: 90,
visible: true,
speech: null,
speechTime: 0
};
// 清空工作区
while (workspace.childNodes.length > 1) {
workspace.removeChild(workspace.lastChild);
}
drawCharacter();
updateStatus("已重置");
}
function updateStatus(message) {
status.textContent = message;
}
// 角色动作函数
function moveCharacter(steps) {
const rad = character.direction * Math.PI / 180;
character.x += steps * Math.cos(rad);
character.y -= steps * Math.sin(rad);
// 边界检查
character.x = Math.max(0, Math.min(canvas.width, character.x));
character.y = Math.max(0, Math.min(canvas.height, character.y));
drawCharacter();
}
function turnCharacter(degrees) {
character.direction += degrees;
drawCharacter();
}
function gotoRandom() {
character.x = Math.random() * canvas.width;
character.y = Math.random() * canvas.height;
drawCharacter();
}
function saySomething(text, seconds) {
character.speech = text;
drawCharacter();
setTimeout(() => {
character.speech = null;
drawCharacter();
}, seconds * 1000);
}
function changeSize(delta) {
character.size += delta;
character.size = Math.max(5, Math.min(100, character.size));
drawCharacter();
}
// 绘制角色
function drawCharacter() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制背景网格
ctx.strokeStyle = '#eee';
ctx.lineWidth = 1;
for (let x = 0; x < canvas.width; x += 20) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = 0; y < canvas.height; y += 20) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
if (!character.visible) return;
// 绘制角色(简单圆形)
ctx.fillStyle = '#4a6cf9';
ctx.beginPath();
ctx.arc(character.x, character.y, character.size / 2, 0, Math.PI * 2);
ctx.fill();
// 绘制方向指示器
const rad = character.direction * Math.PI / 180;
const indicatorX = character.x + (character.size / 2) * Math.cos(rad);
const indicatorY = character.y - (character.size / 2) * Math.sin(rad);
ctx.strokeStyle = '#ff9900';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(character.x, character.y);
ctx.lineTo(indicatorX, indicatorY);
ctx.stroke();
// 绘制说话气泡
if (character.speech) {
ctx.fillStyle = 'white';
ctx.strokeStyle = '#4a6cf9';
ctx.lineWidth = 2;
const text = character.speech;
ctx.font = '14px Arial';
const textWidth = ctx.measureText(text).width;
const bubbleWidth = textWidth + 20;
const bubbleHeight = 30;
const bubbleX = character.x - bubbleWidth / 2;
const bubbleY = character.y - character.size / 2 - bubbleHeight - 10;
// 绘制气泡
ctx.beginPath();
ctx.roundRect(bubbleX, bubbleY, bubbleWidth, bubbleHeight, 10);
ctx.fill();
ctx.stroke();
// 绘制指向线
ctx.beginPath();
ctx.moveTo(character.x, character.y - character.size / 2);
ctx.lineTo(character.x, bubbleY + bubbleHeight);
ctx.lineTo(character.x + 10, bubbleY + bubbleHeight - 10);
ctx.stroke();
// 绘制文本
ctx.fillStyle = 'black';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, character.x, bubbleY + bubbleHeight / 2);
}
}
// 添加roundRect方法如果不支持
if (!CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius) {
if (width < 2 * radius) radius = width / 2;
if (height < 2 * radius) radius = height / 2;
this.beginPath();
this.moveTo(x + radius, y);
this.arcTo(x + width, y, x + width, y + height, radius);
this.arcTo(x + width, y + height, x, y + height, radius);
this.arcTo(x, y + height, x, y, radius);
this.arcTo(x, y, x + width, y, radius);
this.closePath();
return this;
};
}
});
</script>
</body>
</html>
index.html