Created
June 9, 2026 07:04
-
-
Save netomx/560542b6f500d774f1d1f258c507dce6 to your computer and use it in GitHub Desktop.
LOGO clone, made with Qwen3.6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>LOGO - Educational Programming Language</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=VT323&family=Press+Start+2P&display=swap'); | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| background: #1a1a2e; | |
| color: #e0e0e0; | |
| font-family: 'VT323', monospace; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| header { | |
| text-align: center; | |
| padding: 20px; | |
| width: 100%; | |
| background: linear-gradient(180deg, #16213e, #1a1a2e); | |
| border-bottom: 3px solid #0f3460; | |
| } | |
| header h1 { | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: 2.2em; | |
| color: #e94560; | |
| text-shadow: 0 0 20px rgba(233,69,96,0.5), 0 0 40px rgba(233,69,96,0.2); | |
| letter-spacing: 8px; | |
| } | |
| header p { | |
| font-size: 1.3em; | |
| color: #7f8fa6; | |
| margin-top: 8px; | |
| letter-spacing: 2px; | |
| } | |
| .main-container { | |
| display: flex; | |
| gap: 20px; | |
| padding: 20px; | |
| max-width: 1200px; | |
| width: 100%; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| .canvas-panel { | |
| background: #16213e; | |
| border: 2px solid #0f3460; | |
| border-radius: 8px; | |
| padding: 15px; | |
| box-shadow: 0 0 30px rgba(15,52,96,0.5); | |
| } | |
| .canvas-panel h2 { | |
| color: #e94560; | |
| font-size: 1.4em; | |
| margin-bottom: 10px; | |
| text-align: center; | |
| letter-spacing: 3px; | |
| } | |
| canvas { | |
| background: #0a0a1a; | |
| border: 1px solid #0f3460; | |
| border-radius: 4px; | |
| display: block; | |
| cursor: crosshair; | |
| } | |
| .right-panel { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| min-width: 340px; | |
| max-width: 420px; | |
| flex: 1; | |
| } | |
| .command-input-panel { | |
| background: #16213e; | |
| border: 2px solid #0f3460; | |
| border-radius: 8px; | |
| padding: 15px; | |
| box-shadow: 0 0 20px rgba(15,52,96,0.4); | |
| } | |
| .command-input-panel h2 { | |
| color: #e94560; | |
| font-size: 1.4em; | |
| margin-bottom: 10px; | |
| text-align: center; | |
| letter-spacing: 3px; | |
| } | |
| .command-row { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .logo-prompt { | |
| color: #53d769; | |
| font-size: 1.5em; | |
| user-select: none; | |
| } | |
| #commandInput { | |
| flex: 1; | |
| background: #0a0a1a; | |
| border: 1px solid #0f3460; | |
| color: #53d769; | |
| font-family: 'VT323', monospace; | |
| font-size: 1.4em; | |
| padding: 8px 12px; | |
| border-radius: 4px; | |
| outline: none; | |
| caret-color: #53d769; | |
| } | |
| #commandInput:focus { | |
| border-color: #e94560; | |
| box-shadow: 0 0 10px rgba(233,69,96,0.3); | |
| } | |
| #commandInput::placeholder { | |
| color: #2a2a4a; | |
| } | |
| .button-row { | |
| display: flex; | |
| gap: 8px; | |
| margin-top: 10px; | |
| flex-wrap: wrap; | |
| } | |
| .btn { | |
| background: #0f3460; | |
| color: #e0e0e0; | |
| border: 1px solid #1a4a8a; | |
| font-family: 'VT323', monospace; | |
| font-size: 1.1em; | |
| padding: 8px 16px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| letter-spacing: 1px; | |
| } | |
| .btn:hover { | |
| background: #1a4a8a; | |
| border-color: #e94560; | |
| color: #fff; | |
| box-shadow: 0 0 10px rgba(233,69,96,0.3); | |
| } | |
| .btn:active { | |
| transform: scale(0.97); | |
| } | |
| .btn.danger { | |
| background: #5c1a1a; | |
| border-color: #e94560; | |
| } | |
| .btn.danger:hover { | |
| background: #8a2a2a; | |
| } | |
| .btn.success { | |
| background: #1a4a2a; | |
| border-color: #53d769; | |
| } | |
| .btn.success:hover { | |
| background: #2a6a3a; | |
| } | |
| .output-panel { | |
| background: #16213e; | |
| border: 2px solid #0f3460; | |
| border-radius: 8px; | |
| padding: 15px; | |
| box-shadow: 0 0 20px rgba(15,52,96,0.4); | |
| flex: 1; | |
| } | |
| .output-panel h2 { | |
| color: #e94560; | |
| font-size: 1.4em; | |
| margin-bottom: 10px; | |
| text-align: center; | |
| letter-spacing: 3px; | |
| } | |
| #output { | |
| background: #0a0a1a; | |
| border: 1px solid #0f3460; | |
| border-radius: 4px; | |
| padding: 12px; | |
| height: 200px; | |
| overflow-y: auto; | |
| font-size: 1.2em; | |
| line-height: 1.6; | |
| } | |
| #output::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| #output::-webkit-scrollbar-track { | |
| background: #0a0a1a; | |
| } | |
| #output::-webkit-scrollbar-thumb { | |
| background: #0f3460; | |
| border-radius: 4px; | |
| } | |
| .output-line { | |
| margin-bottom: 2px; | |
| } | |
| .output-line.cmd { | |
| color: #53d769; | |
| } | |
| .output-line.result { | |
| color: #ffd700; | |
| } | |
| .output-line.error { | |
| color: #e94560; | |
| } | |
| .output-line.info { | |
| color: #7f8fa6; | |
| } | |
| .commands-list { | |
| background: #16213e; | |
| border: 2px solid #0f3460; | |
| border-radius: 8px; | |
| padding: 15px; | |
| box-shadow: 0 0 20px rgba(15,52,96,0.4); | |
| } | |
| .commands-list h2 { | |
| color: #e94560; | |
| font-size: 1.4em; | |
| margin-bottom: 10px; | |
| text-align: center; | |
| letter-spacing: 3px; | |
| } | |
| .commands-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 6px; | |
| } | |
| .cmd-item { | |
| background: #0a0a1a; | |
| border: 1px solid #0f3460; | |
| border-radius: 4px; | |
| padding: 6px 10px; | |
| font-size: 1em; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .cmd-item:hover { | |
| border-color: #53d769; | |
| color: #53d769; | |
| background: #0a1a2a; | |
| } | |
| .cmd-item .cmd-name { | |
| color: #ffd700; | |
| font-weight: bold; | |
| } | |
| .cmd-item .cmd-desc { | |
| color: #7f8fa6; | |
| font-size: 0.85em; | |
| } | |
| .status-bar { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 6px 12px; | |
| background: #0a0a1a; | |
| border: 1px solid #0f3460; | |
| border-radius: 4px; | |
| margin-top: 8px; | |
| font-size: 1em; | |
| color: #7f8fa6; | |
| } | |
| .status-bar span { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| } | |
| .status-indicator { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| display: inline-block; | |
| } | |
| .status-indicator.pen-down { background: #53d769; } | |
| .status-indicator.pen-up { background: #e94560; } | |
| .examples-panel { | |
| background: #16213e; | |
| border: 2px solid #0f3460; | |
| border-radius: 8px; | |
| padding: 15px; | |
| box-shadow: 0 0 20px rgba(15,52,96,0.4); | |
| } | |
| .examples-panel h2 { | |
| color: #e94560; | |
| font-size: 1.4em; | |
| margin-bottom: 10px; | |
| text-align: center; | |
| letter-spacing: 3px; | |
| } | |
| .example-btn { | |
| display: block; | |
| width: 100%; | |
| text-align: left; | |
| background: #0a0a1a; | |
| border: 1px solid #0f3460; | |
| color: #e0e0e0; | |
| font-family: 'VT323', monospace; | |
| font-size: 1.1em; | |
| padding: 8px 12px; | |
| border-radius: 4px; | |
| margin-bottom: 6px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .example-btn:hover { | |
| border-color: #ffd700; | |
| color: #ffd700; | |
| background: #0a1a2a; | |
| } | |
| .example-btn .example-title { | |
| color: #ffd700; | |
| } | |
| .example-btn .example-desc { | |
| color: #7f8fa6; | |
| font-size: 0.85em; | |
| } | |
| @media (max-width: 800px) { | |
| .main-container { | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| .right-panel { | |
| min-width: unset; | |
| width: 100%; | |
| max-width: 600px; | |
| } | |
| header h1 { | |
| font-size: 1.4em; | |
| } | |
| } | |
| .turtle-trail { | |
| animation: fadeIn 0.3s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| .speed-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-top: 8px; | |
| } | |
| .speed-control label { | |
| color: #7f8fa6; | |
| font-size: 1em; | |
| } | |
| #speedSlider { | |
| flex: 1; | |
| -webkit-appearance: none; | |
| background: #0a0a1a; | |
| height: 6px; | |
| border-radius: 3px; | |
| outline: none; | |
| border: 1px solid #0f3460; | |
| } | |
| #speedSlider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 50%; | |
| background: #e94560; | |
| cursor: pointer; | |
| } | |
| #speedValue { | |
| color: #53d769; | |
| font-size: 1em; | |
| min-width: 30px; | |
| text-align: right; | |
| } | |
| footer { | |
| padding: 15px; | |
| text-align: center; | |
| color: #2a2a4a; | |
| font-size: 1em; | |
| border-top: 1px solid #0f3460; | |
| width: 100%; | |
| margin-top: 20px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>◆ LOGO ◆</h1> | |
| <p>Educational Programming Language — Designed in 1967 by Wally Feurzeig & Seymour Papert</p> | |
| </header> | |
| <div class="main-container"> | |
| <div class="canvas-panel"> | |
| <h2>⬡ TURTLE GRAPHICS ⬡</h2> | |
| <canvas id="canvas" width="560" height="420"></canvas> | |
| <div class="status-bar"> | |
| <span>X: <span id="statusX">0</span></span> | |
| <span>Y: <span id="statusY">0</span></span> | |
| <span>Angle: <span id="statusAngle">0</span>°</span> | |
| <span>Pen: <span class="status-indicator pen-down" id="penIndicator"></span> <span id="penStatus">DOWN</span></span> | |
| <span>Color: <span id="statusColor" style="color:#ffd700">◆</span></span> | |
| </div> | |
| <div class="speed-control"> | |
| <label>Speed:</label> | |
| <input type="range" id="speedSlider" min="1" max="100" value="50"> | |
| <span id="speedValue">50</span> | |
| </div> | |
| </div> | |
| <div class="right-panel"> | |
| <div class="command-input-panel"> | |
| <h2>⌨ COMMAND INPUT ⌨</h2> | |
| <div class="command-row"> | |
| <span class="logo-prompt">logo></span> | |
| <input type="text" id="commandInput" placeholder="Type a command..." autocomplete="off"> | |
| </div> | |
| <div class="button-row"> | |
| <button class="btn success" onclick="executeCommand()">RUN</button> | |
| <button class="btn" onclick="executeLine()">ENTER</button> | |
| <button class="btn" onclick="undoCommand()">↶ UNDO</button> | |
| <button class="btn danger" onclick="clearAll()">✕ CLEAR</button> | |
| </div> | |
| </div> | |
| <div class="output-panel"> | |
| <h2>📟 OUTPUT 📟</h2> | |
| <div id="output"></div> | |
| </div> | |
| <div class="commands-list"> | |
| <h2>📖 COMMANDS 📖</h2> | |
| <div class="commands-grid"> | |
| <div class="cmd-item" onclick="insertCmd('FD <n>')"><span class="cmd-name">FD</span> <span class="cmd-desc">Move forward</span></div> | |
| <div class="cmd-item" onclick="insertCmd('BK <n>')"><span class="cmd-name">BK</span> <span class="cmd-desc">Move backward</span></div> | |
| <div class="cmd-item" onclick="insertCmd('LT <n>')"><span class="cmd-name">LT</span> <span class="cmd-desc">Turn left</span></div> | |
| <div class="cmd-item" onclick="insertCmd('RT <n>')"><span class="cmd-name">RT</span> <span class="cmd-desc">Turn right</span></div> | |
| <div class="cmd-item" onclick="insertCmd('HOME')"><span class="cmd-name">HOME</span> <span class="cmd-desc">Go home</span></div> | |
| <div class="cmd-item" onclick="insertCmd('CS')"><span class="cmd-name">CS</span> <span class="cmd-desc">Clear screen</span></div> | |
| <div class="cmd-item" onclick="insertCmd('PU')"><span class="cmd-name">PU</span> <span class="cmd-desc">Pen up</span></div> | |
| <div class="cmd-item" onclick="insertCmd('PD')"><span class="cmd-name">PD</span> <span class="cmd-desc">Pen down</span></div> | |
| <div class="cmd-item" onclick="insertCmd('SETCOLOR <c>')"><span class="cmd-name">SETCOLOR</span> <span class="cmd-desc">Set color</span></div> | |
| <div class="cmd-item" onclick="insertCmd('SETPC')"><span class="cmd-name">SETPC</span> <span class="cmd-desc">Random color</span></div> | |
| <div class="cmd-item" onclick="insertCmd('SETX <n>')"><span class="cmd-name">SETX</span> <span class="cmd-desc">Set X position</span></div> | |
| <div class="cmd-item" onclick="insertCmd('SETY <n>')"><span class="cmd-name">SETY</span> <span class="cmd-desc">Set Y position</span></div> | |
| <div class="cmd-item" onclick="insertCmd('SETHEADING <n>')"><span class="cmd-name">SETHEADING</span> <span class="cmd-desc">Set heading</span></div> | |
| <div class="cmd-item" onclick="insertCmd('SHOWTURTLE')"><span class="cmd-name">SHOWTURTLE</span> <span class="cmd-desc">Show turtle</span></div> | |
| <div class="cmd-item" onclick="insertCmd('HIDETURTLE')"><span class="cmd-name">HIDETURTLE</span> <span class="cmd-desc">Hide turtle</span></div> | |
| <div class="cmd-item" onclick="insertCmd('REPEAT <n> [<cmds>]')"><span class="cmd-name">REPEAT</span> <span class="cmd-desc">Repeat commands</span></div> | |
| </div> | |
| </div> | |
| <div class="examples-panel"> | |
| <h2>✨ EXAMPLES ✨</h2> | |
| <button class="example-btn" onclick="loadExample('square')"> | |
| <span class="example-title">▢ Square</span><br> | |
| <span class="example-desc">REPEAT 4 [FD 100 RT 90]</span> | |
| </button> | |
| <button class="example-btn" onclick="loadExample('hexagon')"> | |
| <span class="example-title">⬡ Hexagon</span><br> | |
| <span class="example-desc">REPEAT 6 [FD 100 RT 60]</span> | |
| </button> | |
| <button class="example-btn" onclick="loadExample('star')"> | |
| <span class="example-title">★ Star</span><br> | |
| <span class="example-desc">REPEAT 5 [FD 150 RT 144]</span> | |
| </button> | |
| <button class="example-btn" onclick="loadExample('spiral')"> | |
| <span class="example-title">🌀 Spiral</span><br> | |
| <span class="example-desc">REPEAT 100 [FD 5 RT 1]</span> | |
| </button> | |
| <button class="example-btn" onclick="loadExample('flower')"> | |
| <span class="example-title">🌸 Flower</span><br> | |
| <span class="example-desc">REPEAT 36 [FD 80 RT 100 FD 80 BK 80 RT 10]</span> | |
| </button> | |
| <button class="example-btn" onclick="loadExample('sunburst')"> | |
| <span class="example-title">☀ Sunburst</span><br> | |
| <span class="example-desc">REPEAT 36 [SETCOLOR RANDOM FD 120 RT 10]</span> | |
| </button> | |
| <button class="example-btn" onclick="loadExample('tree')"> | |
| <span class="example-title">🌲 Recursive Tree</span><br> | |
| <span class="example-desc">Uses DEFINE to create a fractal tree</span> | |
| </button> | |
| <button class="example-btn" onclick="loadExample('mandala')"> | |
| <span class="example-title">🔮 Mandala</span><br> | |
| <span class="example-desc">REPEAT 12 [REPEAT 36 [FD 3 RT 10] RT 30]</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <footer> | |
| LOGO — A programming language designed for learning — MIT Media Lab — 1967 | |
| </footer> | |
| <script> | |
| const canvas = document.getElementById('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const output = document.getElementById('output'); | |
| const commandInput = document.getElementById('commandInput'); | |
| // Turtle state | |
| let turtle = { | |
| x: 280, | |
| y: 210, | |
| angle: -90, // pointing up | |
| penDown: true, | |
| color: '#ffd700', | |
| visible: true, | |
| size: 10 | |
| }; | |
| // Drawing state | |
| let trail = []; // array of {x1,y1,x2,y2,color} | |
| let undoStack = []; | |
| // Color palette | |
| const COLORS = { | |
| 'RED': '#e94560', | |
| 'GREEN': '#53d769', | |
| 'BLUE': '#4a90d9', | |
| 'YELLOW': '#ffd700', | |
| 'CYAN': '#00bcd4', | |
| 'MAGENTA': '#e040fb', | |
| 'WHITE': '#ffffff', | |
| 'ORANGE': '#ff9800', | |
| 'PURPLE': '#9c27b0', | |
| 'PINK': '#f48fb1', | |
| 'LIME': '#00e676', | |
| 'TEAL': '#009688', | |
| 'NAVY': '#1a237e', | |
| 'BROWN': '#795548', | |
| 'GRAY': '#9e9e9e', | |
| 'BLACK': '#1a1a2e', | |
| 'CORAL': '#ff6f61', | |
| 'GOLD': '#ffd700' | |
| }; | |
| const COLOR_NAMES = Object.keys(COLORS); | |
| // Speed control | |
| const speedSlider = document.getElementById('speedSlider'); | |
| const speedValue = document.getElementById('speedValue'); | |
| speedSlider.addEventListener('input', () => { | |
| speedValue.textContent = speedSlider.value; | |
| }); | |
| function getDelay() { | |
| const v = parseInt(speedSlider.value); | |
| return Math.max(1, 200 - v * 1.9); | |
| } | |
| function addOutput(text, className = 'cmd') { | |
| const line = document.createElement('div'); | |
| line.className = `output-line ${className}`; | |
| line.textContent = text; | |
| output.appendChild(line); | |
| output.scrollTop = output.scrollHeight; | |
| } | |
| function insertCmd(cmd) { | |
| commandInput.value = cmd; | |
| commandInput.focus(); | |
| } | |
| function updateStatus() { | |
| document.getElementById('statusX').textContent = Math.round(turtle.x); | |
| document.getElementById('statusY').textContent = Math.round(turtle.y); | |
| let deg = ((turtle.angle % 360) + 360) % 360; | |
| document.getElementById('statusAngle').textContent = Math.round(deg); | |
| document.getElementById('penStatus').textContent = turtle.penDown ? 'DOWN' : 'UP'; | |
| const pi = document.getElementById('penIndicator'); | |
| pi.className = `status-indicator ${turtle.penDown ? 'pen-down' : 'pen-up'}`; | |
| document.getElementById('statusColor').textContent = '◆'; | |
| document.getElementById('statusColor').style.color = turtle.color; | |
| } | |
| function drawTurtle() { | |
| if (!turtle.visible) return; | |
| const rad = (turtle.angle * Math.PI) / 180; | |
| const size = turtle.size; | |
| ctx.save(); | |
| ctx.translate(turtle.x, turtle.y); | |
| ctx.rotate(rad); | |
| // Turtle body (triangle) | |
| ctx.beginPath(); | |
| ctx.moveTo(size, 0); | |
| ctx.lineTo(-size * 0.6, -size * 0.7); | |
| ctx.lineTo(-size * 0.3, 0); | |
| ctx.lineTo(-size * 0.6, size * 0.7); | |
| ctx.closePath(); | |
| ctx.fillStyle = turtle.color; | |
| ctx.fill(); | |
| ctx.strokeStyle = '#fff'; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| // Eye | |
| ctx.beginPath(); | |
| ctx.arc(size * 0.2, -size * 0.15, 1.5, 0, Math.PI * 2); | |
| ctx.fillStyle = '#000'; | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| function drawTrail() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Draw grid | |
| ctx.strokeStyle = '#0f1a2e'; | |
| ctx.lineWidth = 0.5; | |
| 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(); | |
| } | |
| // Draw center | |
| ctx.strokeStyle = '#1a2a4a'; | |
| ctx.lineWidth = 1; | |
| ctx.beginPath(); | |
| ctx.moveTo(canvas.width/2, 0); | |
| ctx.lineTo(canvas.width/2, canvas.height); | |
| ctx.stroke(); | |
| ctx.beginPath(); | |
| ctx.moveTo(0, canvas.height/2); | |
| ctx.lineTo(canvas.width, canvas.height/2); | |
| ctx.stroke(); | |
| // Draw trails | |
| for (const t of trail) { | |
| ctx.beginPath(); | |
| ctx.strokeStyle = t.color || turtle.color; | |
| ctx.lineWidth = 2; | |
| ctx.moveTo(t.x1, t.y1); | |
| ctx.lineTo(t.x2, t.y2); | |
| ctx.stroke(); | |
| } | |
| drawTurtle(); | |
| } | |
| function drawTrailAnimated(x1, y1, x2, y2, color) { | |
| return new Promise(resolve => { | |
| const dx = x2 - x1; | |
| const dy = y2 - y1; | |
| const dist = Math.sqrt(dx*dx + dy*dy); | |
| const steps = Math.max(1, Math.round(dist / 2)); | |
| let step = 0; | |
| function animate() { | |
| const progress = Math.min(step / steps, 1); | |
| const cx = x1 + dx * progress; | |
| const cy = y1 + dy * progress; | |
| ctx.beginPath(); | |
| ctx.strokeStyle = color || turtle.color; | |
| ctx.lineWidth = 2; | |
| ctx.moveTo(x1, y1); | |
| ctx.lineTo(cx, cy); | |
| ctx.stroke(); | |
| turtle.x = cx; | |
| turtle.y = cy; | |
| step++; | |
| if (progress < 1) { | |
| setTimeout(animate, getDelay() / (dist > 50 ? 3 : 1)); | |
| } else { | |
| trail.push({x1, y1, x2: x2, y2: y2, color: color || turtle.color}); | |
| undoStack.push({type: 'trail', x1, y1, x2, y2, color: color || turtle.color}); | |
| drawTurtle(); | |
| updateStatus(); | |
| resolve(); | |
| } | |
| } | |
| animate(); | |
| }); | |
| } | |
| async function executeCommand() { | |
| const cmd = commandInput.value.trim(); | |
| if (!cmd) return; | |
| addOutput(`logo> ${cmd}`, 'cmd'); | |
| commandInput.value = ''; | |
| try { | |
| await parseAndExecute(cmd); | |
| } catch (e) { | |
| addOutput(`** ${e.message}`, 'error'); | |
| } | |
| } | |
| function executeLine() { | |
| executeCommand(); | |
| } | |
| function undoCommand() { | |
| if (undoStack.length === 0) { | |
| addOutput('(nothing to undo)', 'info'); | |
| return; | |
| } | |
| const action = undoStack.pop(); | |
| // Remove the last trail entry | |
| trail.pop(); | |
| drawTrail(); | |
| updateStatus(); | |
| addOutput('↶ undone', 'info'); | |
| } | |
| function clearAll() { | |
| trail = []; | |
| undoStack = []; | |
| turtle.x = 280; | |
| turtle.y = 210; | |
| turtle.angle = -90; | |
| turtle.penDown = true; | |
| turtle.color = '#ffd700'; | |
| turtle.visible = true; | |
| drawTrail(); | |
| updateStatus(); | |
| addOutput('Screen cleared.', 'info'); | |
| } | |
| function parseValue(val) { | |
| val = val.trim(); | |
| if (val === 'RANDOM') { | |
| return Math.random() * 200; | |
| } | |
| const num = parseFloat(val); | |
| if (isNaN(num)) { | |
| throw new Error(`Unknown value: ${val}`); | |
| } | |
| return num; | |
| } | |
| function parseColor(val) { | |
| val = val.trim().toUpperCase(); | |
| if (val === 'RANDOM') { | |
| return COLOR_NAMES[Math.floor(Math.random() * COLOR_NAMES.length)]; | |
| } | |
| if (COLORS[val]) { | |
| return COLORS[val]; | |
| } | |
| // Try hex | |
| if (val.startsWith('#') && (val.length === 4 || val.length === 7)) { | |
| return val; | |
| } | |
| throw new Error(`Unknown color: ${val}`); | |
| } | |
| function parseCommand(cmd) { | |
| const upper = cmd.toUpperCase().trim(); | |
| // REPEAT n [commands] | |
| const repeatMatch = upper.match(/^REPEAT\s+(\d+)\s+\[(.+)\]$/s); | |
| if (repeatMatch) { | |
| return { type: 'repeat', count: parseInt(repeatMatch[1]), commands: repeatMatch[2] }; | |
| } | |
| // DEFINE name [params] [commands] | |
| const defineMatch = upper.match(/^DEFINE\s+(\w+)\s*\[([^\]]+)\]$/s); | |
| if (defineMatch) { | |
| return { type: 'define', name: defineMatch[1], commands: defineMatch[2] }; | |
| } | |
| // Simple command | |
| const parts = upper.split(/\s+/); | |
| const base = parts[0]; | |
| switch(base) { | |
| case 'FD': | |
| case 'FORWARD': | |
| return { type: 'forward', distance: parseValue(parts[1]) }; | |
| case 'BK': | |
| case 'BACK': | |
| return { type: 'backward', distance: parseValue(parts[1]) }; | |
| case 'LT': | |
| case 'LEFT': | |
| return { type: 'left', degrees: parseValue(parts[1]) }; | |
| case 'RT': | |
| case 'RIGHT': | |
| return { type: 'right', degrees: parseValue(parts[1]) }; | |
| case 'HOME': | |
| return { type: 'home' }; | |
| case 'CS': | |
| case 'CLEARSCREEN': | |
| case 'CLEARSCREEN': | |
| return { type: 'clearscreen' }; | |
| case 'PU': | |
| case 'PENUP': | |
| return { type: 'penup' }; | |
| case 'PD': | |
| case 'PENDOWN': | |
| return { type: 'pendown' }; | |
| case 'SETCOLOR': | |
| case 'SETPENCOLOR': | |
| return { type: 'setcolor', color: parseColor(parts[1]) }; | |
| case 'SETPC': | |
| return { type: 'setcolor', color: parseColor('RANDOM') }; | |
| case 'SETX': | |
| return { type: 'setx', x: parseValue(parts[1]) }; | |
| case 'SETY': | |
| return { type: 'sety', y: parseValue(parts[1]) }; | |
| case 'SETHEADING': | |
| case 'SETHEADING': | |
| return { type: 'setheading', angle: parseValue(parts[1]) }; | |
| case 'SHOWTURTLE': | |
| case 'SHOWT': | |
| return { type: 'showturtle' }; | |
| case 'HIDETURTLE': | |
| case 'HIDET': | |
| return { type: 'hideturtle' }; | |
| case 'SETPENSIZE': | |
| return { type: 'setpensize', size: parseValue(parts[1]) }; | |
| case 'WAIT': | |
| return { type: 'wait', ms: parseValue(parts[1]) }; | |
| case 'PRINT': | |
| case 'PR': | |
| return { type: 'print', text: parts.slice(1).join(' ') }; | |
| case 'DRAW': | |
| return { type: 'draw', name: parts[1] }; | |
| default: | |
| throw new Error(`Unknown command: ${base}`); | |
| } | |
| } | |
| async function executeCommandObj(cmdObj) { | |
| switch(cmdObj.type) { | |
| case 'forward': { | |
| const rad = (turtle.angle * Math.PI) / 180; | |
| const nx = turtle.x + cmdObj.distance * Math.cos(rad); | |
| const ny = turtle.y + cmdObj.distance * Math.sin(rad); | |
| if (turtle.penDown) { | |
| await drawTrailAnimated(turtle.x, turtle.y, nx, ny, turtle.color); | |
| } else { | |
| turtle.x = nx; | |
| turtle.y = ny; | |
| updateStatus(); | |
| } | |
| break; | |
| } | |
| case 'backward': { | |
| const rad = (turtle.angle * Math.PI) / 180; | |
| const nx = turtle.x - cmdObj.distance * Math.cos(rad); | |
| const ny = turtle.y - cmdObj.distance * Math.sin(rad); | |
| if (turtle.penDown) { | |
| await drawTrailAnimated(turtle.x, turtle.y, nx, ny, turtle.color); | |
| } else { | |
| turtle.x = nx; | |
| turtle.y = ny; | |
| updateStatus(); | |
| } | |
| break; | |
| } | |
| case 'left': { | |
| turtle.angle -= cmdObj.degrees; | |
| updateStatus(); | |
| break; | |
| } | |
| case 'right': { | |
| turtle.angle += cmdObj.degrees; | |
| updateStatus(); | |
| break; | |
| } | |
| case 'home': { | |
| if (turtle.penDown) { | |
| await drawTrailAnimated(turtle.x, turtle.y, 280, 210, turtle.color); | |
| } else { | |
| turtle.x = 280; | |
| turtle.y = 210; | |
| updateStatus(); | |
| } | |
| turtle.angle = -90; | |
| updateStatus(); | |
| break; | |
| } | |
| case 'clearscreen': { | |
| clearAll(); | |
| break; | |
| } | |
| case 'penup': { | |
| turtle.penDown = false; | |
| updateStatus(); | |
| break; | |
| } | |
| case 'pendown': { | |
| turtle.penDown = true; | |
| updateStatus(); | |
| break; | |
| } | |
| case 'setcolor': { | |
| turtle.color = cmdObj.color; | |
| updateStatus(); | |
| break; | |
| } | |
| case 'setx': { | |
| const oldX = turtle.x; | |
| turtle.x = cmdObj.x; | |
| if (turtle.penDown) { | |
| await drawTrailAnimated(oldX, turtle.y, turtle.x, turtle.y, turtle.color); | |
| } | |
| updateStatus(); | |
| break; | |
| } | |
| case 'sety': { | |
| const oldY = turtle.y; | |
| turtle.y = cmdObj.y; | |
| if (turtle.penDown) { | |
| await drawTrailAnimated(turtle.x, oldY, turtle.x, turtle.y, turtle.color); | |
| } | |
| updateStatus(); | |
| break; | |
| } | |
| case 'setheading': { | |
| turtle.angle = cmdObj.angle; | |
| updateStatus(); | |
| break; | |
| } | |
| case 'showturtle': { | |
| turtle.visible = true; | |
| drawTurtle(); | |
| break; | |
| } | |
| case 'hideturtle': { | |
| turtle.visible = false; | |
| drawTurtle(); | |
| break; | |
| } | |
| case 'setpensize': { | |
| turtle.size = Math.max(2, Math.min(30, cmdObj.size)); | |
| drawTurtle(); | |
| break; | |
| } | |
| case 'wait': { | |
| await new Promise(r => setTimeout(r, cmdObj.ms)); | |
| break; | |
| } | |
| case 'print': { | |
| addOutput(cmdObj.text, 'result'); | |
| break; | |
| } | |
| case 'draw': { | |
| await executeDrawCommand(cmdObj.name); | |
| break; | |
| } | |
| case 'repeat': { | |
| for (let i = 0; i < cmdObj.count; i++) { | |
| await parseAndExecute(cmdObj.commands); | |
| } | |
| break; | |
| } | |
| case 'define': { | |
| userCommands[cmdObj.name] = cmdObj.commands; | |
| addOutput(`Defined: ${cmdObj.name}`, 'info'); | |
| break; | |
| } | |
| default: | |
| throw new Error(`Unknown command type: ${cmdObj.type}`); | |
| } | |
| } | |
| // User-defined commands storage | |
| let userCommands = {}; | |
| async function executeDrawCommand(name) { | |
| name = name.toUpperCase(); | |
| if (userCommands[name]) { | |
| await parseAndExecute(userCommands[name]); | |
| } else { | |
| throw new Error(`Undefined command: ${name}`); | |
| } | |
| } | |
| async function parseAndExecute(cmd) { | |
| const cmdObj = parseCommand(cmd); | |
| await executeCommandObj(cmdObj); | |
| } | |
| // Load example | |
| async function loadExample(name) { | |
| clearAll(); | |
| const examples = { | |
| 'square': 'REPEAT 4 [FD 100 RT 90]', | |
| 'hexagon': 'REPEAT 6 [FD 100 RT 60]', | |
| 'star': 'REPEAT 5 [FD 150 RT 144]', | |
| 'spiral': 'REPEAT 100 [FD 5 RT 1]', | |
| 'flower': 'REPEAT 36 [FD 80 RT 100 FD 80 BK 80 RT 10]', | |
| 'sunburst': 'REPEAT 36 [SETCOLOR RANDOM FD 120 RT 10]', | |
| 'tree': 'DEFINE TREE [REPEAT 2 [FD 30 LT 20 TREE FD 30 RT 40 TREE FD 30 RT 20]] CS SETCOLOR GREEN TREE', | |
| 'mandala': 'REPEAT 12 [REPEAT 36 [FD 3 RT 10] RT 30]' | |
| }; | |
| const cmd = examples[name]; | |
| if (cmd) { | |
| addOutput(`Loading: ${name}...`, 'info'); | |
| await parseAndExecute(cmd); | |
| addOutput(`Done: ${name}`, 'info'); | |
| } | |
| } | |
| // Keyboard handling | |
| commandInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') { | |
| e.preventDefault(); | |
| executeCommand(); | |
| } | |
| }); | |
| // Focus input on click | |
| canvas.addEventListener('click', () => { | |
| commandInput.focus(); | |
| }); | |
| // Initial draw | |
| drawTrail(); | |
| updateStatus(); | |
| addOutput('LOGO Ready! Type a command or click an example.', 'info'); | |
| addOutput('Type DEFINE name [commands] to create custom commands.', 'info'); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment