887 lines
32 KiB
HTML
887 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>McRogueFace - WebGL</title>
|
|
<!-- LZ-String for URL compression -->
|
|
<script src="https://unpkg.com/lz-string@1.5.0/libs/lz-string.min.js"></script>
|
|
<!-- CodeMirror from CDN -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/dracula.min.css">
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/python/python.min.js"></script>
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
margin: 0;
|
|
padding: 20px;
|
|
background: #1a1a2e;
|
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
color: #eee;
|
|
min-height: 100vh;
|
|
}
|
|
.container {
|
|
max-width: 1500px;
|
|
margin: 0 auto;
|
|
}
|
|
header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
}
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
h1 {
|
|
margin: 0;
|
|
color: #e94560;
|
|
}
|
|
h1 a {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
.tagline {
|
|
color: #666;
|
|
font-size: 14px;
|
|
}
|
|
#status {
|
|
color: #888;
|
|
}
|
|
.main-content {
|
|
display: flex;
|
|
gap: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.game-panel {
|
|
flex: 1;
|
|
min-width: 300px;
|
|
}
|
|
#canvas {
|
|
background: #000;
|
|
display: block;
|
|
max-width: 100%;
|
|
height: auto;
|
|
image-rendering: pixelated;
|
|
image-rendering: crisp-edges;
|
|
-ms-interpolation-mode: nearest-neighbor;
|
|
outline: none;
|
|
}
|
|
#canvas:focus {
|
|
box-shadow: 0 0 10px rgba(78, 204, 163, 0.5);
|
|
}
|
|
.repl-panel {
|
|
width: 450px;
|
|
min-width: 350px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: #16213e;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
.repl-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 10px 15px;
|
|
background: #0f3460;
|
|
border-bottom: 1px solid #e94560;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
.repl-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.repl-header h3 {
|
|
margin: 0;
|
|
font-size: 14px;
|
|
color: #e94560;
|
|
}
|
|
/* Interpreter status indicator */
|
|
.status-indicator {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
transition: background-color 0.3s;
|
|
}
|
|
.status-ok { background-color: #4ecca3; }
|
|
.status-error { background-color: #e94560; }
|
|
.status-busy { background-color: #f39c12; }
|
|
.repl-buttons {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
.repl-buttons button {
|
|
padding: 6px 12px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
transition: all 0.2s;
|
|
}
|
|
#runBtn {
|
|
background: #4ecca3;
|
|
color: #1a1a2e;
|
|
}
|
|
#runBtn:hover {
|
|
background: #3db892;
|
|
}
|
|
#shareBtn {
|
|
background: #667eea;
|
|
color: white;
|
|
}
|
|
#shareBtn:hover {
|
|
background: #5a6fd6;
|
|
}
|
|
#resetBtn {
|
|
background: #e94560;
|
|
color: white;
|
|
}
|
|
#resetBtn:hover {
|
|
background: #d63050;
|
|
}
|
|
#clearBtn {
|
|
background: #666;
|
|
color: white;
|
|
}
|
|
#clearBtn:hover {
|
|
background: #555;
|
|
}
|
|
.repl-buttons button:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
/* CodeMirror container */
|
|
.editor-container {
|
|
border-bottom: 1px solid #333;
|
|
}
|
|
.CodeMirror {
|
|
height: 430px;
|
|
font-size: 13px;
|
|
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
}
|
|
.repl-output-header {
|
|
padding: 8px 15px;
|
|
background: #0f3460;
|
|
font-size: 12px;
|
|
color: #888;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.shortcut-hint {
|
|
font-size: 11px;
|
|
color: #666;
|
|
}
|
|
#replOutput {
|
|
flex: 1;
|
|
min-height: 150px;
|
|
max-height: 250px;
|
|
overflow-y: auto;
|
|
padding: 12px;
|
|
background: #0a0a15;
|
|
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
font-size: 12px;
|
|
white-space: pre-wrap;
|
|
color: #ccc;
|
|
}
|
|
#replOutput .error {
|
|
color: #e94560;
|
|
}
|
|
#replOutput .success {
|
|
color: #4ecca3;
|
|
}
|
|
#replOutput .warning {
|
|
color: #f39c12;
|
|
}
|
|
#replOutput .input {
|
|
color: #888;
|
|
}
|
|
#output {
|
|
margin-top: 20px;
|
|
width: 100%;
|
|
max-height: 150px;
|
|
overflow-y: auto;
|
|
background: #0f0f23;
|
|
padding: 10px;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
white-space: pre-wrap;
|
|
border: 1px solid #333;
|
|
border-radius: 4px;
|
|
}
|
|
.spinner {
|
|
margin: 20px auto;
|
|
width: 50px;
|
|
height: 50px;
|
|
border: 5px solid #333;
|
|
border-top-color: #e94560;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
@media (max-width: 900px) {
|
|
.main-content {
|
|
flex-direction: column;
|
|
}
|
|
.repl-panel {
|
|
width: 100%;
|
|
}
|
|
}
|
|
/* Source indicator for gists */
|
|
.source-indicator {
|
|
padding: 5px 10px;
|
|
background: #0f3460;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
color: #4ecca3;
|
|
}
|
|
.source-indicator.hidden {
|
|
display: none;
|
|
}
|
|
.source-indicator a {
|
|
color: #667eea;
|
|
}
|
|
/* Share modal */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 100;
|
|
}
|
|
.modal-overlay.hidden {
|
|
display: none;
|
|
}
|
|
.modal {
|
|
background: #16213e;
|
|
padding: 25px;
|
|
border-radius: 12px;
|
|
max-width: 500px;
|
|
width: 90%;
|
|
border: 2px solid #e94560;
|
|
}
|
|
.modal h2 {
|
|
margin: 0 0 15px 0;
|
|
color: #e94560;
|
|
}
|
|
.modal p {
|
|
color: #888;
|
|
margin: 10px 0;
|
|
font-size: 14px;
|
|
}
|
|
.modal input[type="text"] {
|
|
width: 100%;
|
|
padding: 12px;
|
|
background: #0f0f23;
|
|
border: 1px solid #333;
|
|
color: #4ecca3;
|
|
font-family: monospace;
|
|
font-size: 13px;
|
|
border-radius: 4px;
|
|
margin: 10px 0;
|
|
}
|
|
.modal-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-top: 15px;
|
|
}
|
|
.modal-buttons button {
|
|
flex: 1;
|
|
padding: 10px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
transition: all 0.2s;
|
|
}
|
|
.modal .copy-btn {
|
|
background: #4ecca3;
|
|
color: #1a1a2e;
|
|
}
|
|
.modal .close-btn {
|
|
background: #666;
|
|
color: white;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<div class="header-left">
|
|
<h1><a href="/">McRogueFace</a></h1>
|
|
<span class="tagline">Playground</span>
|
|
<span id="sourceIndicator" class="source-indicator hidden"></span>
|
|
</div>
|
|
<div id="status">Downloading...</div>
|
|
</header>
|
|
|
|
<div id="spinner" class="spinner"></div>
|
|
|
|
<div class="main-content">
|
|
<div class="game-panel">
|
|
<canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
|
|
</div>
|
|
|
|
<div class="repl-panel" id="replPanel">
|
|
<div class="repl-header">
|
|
<div class="repl-title">
|
|
<span id="interpreterStatus" class="status-indicator status-busy" title="Initializing..."></span>
|
|
<h3>Python REPL</h3>
|
|
</div>
|
|
<div class="repl-buttons">
|
|
<button id="runBtn" disabled>Run</button>
|
|
<button id="shareBtn">Share</button>
|
|
<button id="resetBtn" disabled>Reset</button>
|
|
<button id="clearBtn">Clear</button>
|
|
</div>
|
|
</div>
|
|
<div class="editor-container">
|
|
<textarea id="codeEditor"></textarea>
|
|
</div>
|
|
<div class="repl-output-header">
|
|
<span>Output</span>
|
|
<span class="shortcut-hint">Ctrl+Enter to run | Ctrl+Up/Down for history</span>
|
|
</div>
|
|
<div id="replOutput"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="output"></div>
|
|
</div>
|
|
|
|
<!-- Share Modal -->
|
|
<div id="shareModal" class="modal-overlay hidden">
|
|
<div class="modal">
|
|
<h2>Share Your Code</h2>
|
|
<p>Copy this URL to share your playground:</p>
|
|
<input type="text" id="shareUrl" readonly onclick="this.select()">
|
|
<p id="shareSize" style="font-size: 12px;"></p>
|
|
<div class="modal-buttons">
|
|
<button class="copy-btn" id="copyBtn">Copy to Clipboard</button>
|
|
<button class="close-btn" id="closeModalBtn">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script type='text/javascript'>
|
|
// ===========================================
|
|
// CONFIGURATION
|
|
// ===========================================
|
|
const CONFIG = {
|
|
playgroundUrl: window.location.origin,
|
|
shortUrl: window.location.origin,
|
|
playgroundPath: window.location.pathname.replace(/\/[^\/]*$/, ''),
|
|
maxCodeSize: 16 * 1024,
|
|
githubApiBase: 'https://api.github.com'
|
|
};
|
|
|
|
// ===========================================
|
|
// URL Fragment Handling (Share & Gist support)
|
|
// ===========================================
|
|
|
|
async function parseUrlFragment() {
|
|
const hash = window.location.hash.slice(1);
|
|
if (!hash) return null;
|
|
|
|
const params = new URLSearchParams(hash);
|
|
|
|
// Handle #src=<lz-compressed>
|
|
if (params.has('src')) {
|
|
try {
|
|
const compressed = params.get('src');
|
|
const code = LZString.decompressFromEncodedURIComponent(compressed);
|
|
if (code) {
|
|
return { type: 'inline', code: code };
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to decompress code:', e);
|
|
}
|
|
}
|
|
|
|
// Handle #gist=user/id or #gist=id
|
|
if (params.has('gist')) {
|
|
const gistParam = params.get('gist');
|
|
const file = params.get('file');
|
|
return { type: 'gist', gistId: gistParam, file: file };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function loadGist(gistId, preferredFile) {
|
|
const id = gistId.includes('/') ? gistId.split('/').pop() : gistId;
|
|
|
|
try {
|
|
const resp = await fetch(`${CONFIG.githubApiBase}/gists/${id}`);
|
|
if (!resp.ok) {
|
|
throw new Error(`Gist not found (${resp.status})`);
|
|
}
|
|
|
|
const data = await resp.json();
|
|
const files = Object.values(data.files);
|
|
|
|
let targetFile = null;
|
|
if (preferredFile) {
|
|
targetFile = files.find(f => f.filename === preferredFile);
|
|
}
|
|
if (!targetFile) {
|
|
targetFile = files.find(f => f.filename.endsWith('.py'));
|
|
}
|
|
if (!targetFile) {
|
|
targetFile = files[0];
|
|
}
|
|
|
|
return {
|
|
code: targetFile.content,
|
|
filename: targetFile.filename,
|
|
gistUrl: data.html_url,
|
|
owner: data.owner?.login || 'anonymous',
|
|
description: data.description
|
|
};
|
|
} catch (e) {
|
|
console.error('Failed to load gist:', e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
function generateShareUrl(code) {
|
|
const compressed = LZString.compressToEncodedURIComponent(code);
|
|
const path = CONFIG.playgroundPath || '';
|
|
const url = `${CONFIG.shortUrl}${path}/#src=${compressed}`;
|
|
return {
|
|
url: url,
|
|
originalSize: code.length,
|
|
compressedSize: compressed.length,
|
|
ratio: ((compressed.length / code.length) * 100).toFixed(1)
|
|
};
|
|
}
|
|
|
|
// ===========================================
|
|
// DOM Elements
|
|
// ===========================================
|
|
|
|
var statusElement = document.getElementById('status');
|
|
var spinnerElement = document.getElementById('spinner');
|
|
var outputElement = document.getElementById('output');
|
|
var canvasElement = document.getElementById('canvas');
|
|
var replOutput = document.getElementById('replOutput');
|
|
var runBtn = document.getElementById('runBtn');
|
|
var shareBtn = document.getElementById('shareBtn');
|
|
var resetBtn = document.getElementById('resetBtn');
|
|
var clearBtn = document.getElementById('clearBtn');
|
|
var interpreterStatus = document.getElementById('interpreterStatus');
|
|
var sourceIndicator = document.getElementById('sourceIndicator');
|
|
var shareModal = document.getElementById('shareModal');
|
|
var shareUrlInput = document.getElementById('shareUrl');
|
|
var shareSizeEl = document.getElementById('shareSize');
|
|
var copyBtn = document.getElementById('copyBtn');
|
|
var closeModalBtn = document.getElementById('closeModalBtn');
|
|
|
|
// Initialize CodeMirror
|
|
var editor = CodeMirror.fromTextArea(document.getElementById('codeEditor'), {
|
|
mode: 'python',
|
|
theme: 'dracula',
|
|
lineNumbers: true,
|
|
indentUnit: 4,
|
|
tabSize: 4,
|
|
indentWithTabs: false,
|
|
electricChars: true,
|
|
matchBrackets: true
|
|
});
|
|
|
|
// Default content
|
|
var defaultCode = 'import mcrfpy\n\n# the default "playground" scene\nscene = mcrfpy.current_scene\n\n# Frame:\nframe = mcrfpy.Frame((10, 10), (50, 50), fill_color=(30,30,80))\nscene.children.append(frame)\n\n# Caption:\ncaption = mcrfpy.Caption((80,10), text="Hello\\nMcRogueFace!", font_size=32) # uses default font\nscene.children.append(caption)\n\n# Sprite:\nsprite = mcrfpy.Sprite((150, 150), sprite_index=84, scale=8.0) # uses default sprite sheet\nscene.children.append(sprite)\n\n# Grid:\ngrid = mcrfpy.Grid((694,10), (320,320), grid_size=(10,10), zoom=2.0)\nscene.children.append(grid)\n# place entities on grid squares\nmcrfpy.Entity((4,5), sprite_index=85, grid=grid) # uses default sprite sheet\nmcrfpy.Entity((5,9), sprite_index=87, grid=grid)\nmcrfpy.Entity((3,7), sprite_index=89, grid=grid)\n# fill with some dirt\nimport random\nfor x in range(10):\n for y in range(10):\n # mostly 0 for plain dirt, with two variations\n grid[x,y].tilesprite = random.choice([0, 0, 0, 0, 0, 0, 12, 24])\n\n\n# make the wizard sprite clickable\ndef poke(position, mousebtn, inputstate):\n if inputstate != mcrfpy.InputState.PRESSED:\n return\n say = random.choice(["oof", "ouch", "uff"])\n new_txt = mcrfpy.Caption(position, text=say)\n scene.children.append(new_txt)\n done = lambda *args: scene.children.remove(new_txt)\n new_txt.animate("y", # property\n -100, # target value\n 10.0 + random.randint(-4, 2), # duration\n callback=done # called on completion\n )\n new_txt.animate("x", position.x + random.randint(-70, +270), 5.5)\n new_txt.animate("fill_color.a", 0, 5.5)\n new_txt.animate("rotation", random.randint(-85, 85), 5.5, mcrfpy.Easing.EASE_OUT_QUAD)\nsprite.on_click = poke\n\n# make the wizard sprite moveable\ndef keypress(key, inputstate):\n actions = { mcrfpy.Key.W: mcrfpy.Vector(0, -10),\n mcrfpy.Key.A: mcrfpy.Vector(-10, 0),\n mcrfpy.Key.S: mcrfpy.Vector(0, 10),\n mcrfpy.Key.D: mcrfpy.Vector(10, 0) }\n if inputstate != mcrfpy.InputState.PRESSED:\n return\n if key in actions:\n sprite.pos += actions[key]\n\nscene.on_key = keypress\n\nprint(mcrfpy.current_scene.children)\n\n# Press F3 for stats\n\n# create a new scene and switch to it:\n#new_scene = mcrfpy.Scene("test")\n#new_scene.activate()';
|
|
editor.setValue(defaultCode);
|
|
|
|
// Stop keyboard events from bubbling to SDL (bubble phase)
|
|
var cmElement = editor.getWrapperElement();
|
|
cmElement.addEventListener('keydown', function(e) {
|
|
e.stopPropagation();
|
|
// Handle Ctrl+Enter for running code
|
|
if (e.ctrlKey && e.key === 'Enter') {
|
|
e.preventDefault();
|
|
if (!runBtn.disabled) {
|
|
runCode();
|
|
}
|
|
}
|
|
// Handle Ctrl+Up/Down for history
|
|
if (e.ctrlKey && e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
navigateHistory(-1);
|
|
}
|
|
if (e.ctrlKey && e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
navigateHistory(1);
|
|
}
|
|
}, false);
|
|
cmElement.addEventListener('keyup', function(e) {
|
|
e.stopPropagation();
|
|
}, false);
|
|
cmElement.addEventListener('keypress', function(e) {
|
|
e.stopPropagation();
|
|
}, false);
|
|
|
|
// Execution history
|
|
var cmdHistory = [];
|
|
var historyIndex = -1;
|
|
var currentInput = '';
|
|
|
|
function navigateHistory(direction) {
|
|
if (cmdHistory.length === 0) return;
|
|
|
|
if (historyIndex === -1) {
|
|
currentInput = editor.getValue();
|
|
}
|
|
|
|
historyIndex += direction;
|
|
|
|
if (historyIndex < -1) {
|
|
historyIndex = -1;
|
|
} else if (historyIndex >= cmdHistory.length) {
|
|
historyIndex = cmdHistory.length - 1;
|
|
}
|
|
|
|
if (historyIndex === -1) {
|
|
editor.setValue(currentInput);
|
|
} else {
|
|
editor.setValue(cmdHistory[cmdHistory.length - 1 - historyIndex]);
|
|
}
|
|
|
|
editor.setCursor(editor.lineCount(), 0);
|
|
}
|
|
|
|
// Pre-set canvas size
|
|
canvasElement.width = 1024;
|
|
canvasElement.height = 768;
|
|
|
|
// ===========================================
|
|
// Initialization (load from URL fragment)
|
|
// ===========================================
|
|
|
|
async function init() {
|
|
const source = await parseUrlFragment();
|
|
|
|
if (source) {
|
|
if (source.type === 'inline') {
|
|
editor.setValue(source.code);
|
|
sourceIndicator.textContent = 'Loaded from shared URL';
|
|
sourceIndicator.classList.remove('hidden');
|
|
} else if (source.type === 'gist') {
|
|
editor.setValue('# Loading gist...');
|
|
|
|
try {
|
|
const gist = await loadGist(source.gistId, source.file);
|
|
editor.setValue(gist.code);
|
|
|
|
sourceIndicator.innerHTML = `From: <a href="${gist.gistUrl}" target="_blank">${gist.owner}/${gist.filename}</a>`;
|
|
sourceIndicator.classList.remove('hidden');
|
|
} catch (e) {
|
|
editor.setValue(`# Failed to load gist: ${e.message}\n# Check the console for details.`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===========================================
|
|
// Emscripten Module
|
|
// ===========================================
|
|
|
|
var Module = {
|
|
print: (function() {
|
|
return function(text) {
|
|
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
|
|
console.log(text);
|
|
outputElement.textContent += text + '\n';
|
|
outputElement.scrollTop = outputElement.scrollHeight;
|
|
};
|
|
})(),
|
|
printErr: function(text) {
|
|
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
|
|
console.error(text);
|
|
outputElement.textContent += '[ERR] ' + text + '\n';
|
|
outputElement.scrollTop = outputElement.scrollHeight;
|
|
},
|
|
canvas: canvasElement,
|
|
setStatus: function(text) {
|
|
if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
|
|
if (text === Module.setStatus.last.text) return;
|
|
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
|
|
var now = Date.now();
|
|
if (m && now - Module.setStatus.last.time < 30) return;
|
|
Module.setStatus.last.time = now;
|
|
Module.setStatus.last.text = text;
|
|
if (m) {
|
|
text = m[1];
|
|
}
|
|
statusElement.innerHTML = text;
|
|
},
|
|
totalDependencies: 0,
|
|
monitorRunDependencies: function(left) {
|
|
this.totalDependencies = Math.max(this.totalDependencies, left);
|
|
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
|
|
},
|
|
onRuntimeInitialized: function() {
|
|
console.log('Emscripten runtime initialized');
|
|
spinnerElement.style.display = 'none';
|
|
|
|
// Enable REPL buttons
|
|
runBtn.disabled = false;
|
|
resetBtn.disabled = false;
|
|
|
|
// Make FS available globally for console access
|
|
window.FS = Module.FS;
|
|
|
|
// Create Python execution functions
|
|
window.runPython = function(code) {
|
|
return Module.ccall('run_python_string_with_output', 'string', ['string'], [code]);
|
|
};
|
|
window.resetGame = function() {
|
|
return Module.ccall('reset_python_environment', 'number', [], []);
|
|
};
|
|
|
|
// Interpreter health check functions
|
|
window.getPythonState = function() {
|
|
return Module.ccall('get_python_state', 'string', [], []);
|
|
};
|
|
window.clearPythonError = function() {
|
|
Module.ccall('clear_python_error', null, [], []);
|
|
};
|
|
window.getPythonGlobalsCount = function() {
|
|
return Module.ccall('get_python_globals_count', 'number', [], []);
|
|
};
|
|
|
|
updateInterpreterStatus();
|
|
|
|
appendToOutput('Python REPL ready. Enter code and click Run (or Ctrl+Enter).', 'success');
|
|
appendToOutput('Tip: Use Ctrl+Up/Down to navigate command history.', 'input');
|
|
|
|
setTimeout(function() {
|
|
canvasElement.focus();
|
|
window.dispatchEvent(new Event('resize'));
|
|
}, 100);
|
|
}
|
|
};
|
|
|
|
Module.setStatus('Downloading...');
|
|
|
|
window.onerror = function(event) {
|
|
Module.setStatus('Error! See console for details.');
|
|
spinnerElement.style.display = 'none';
|
|
};
|
|
|
|
if (typeof resolveGlobalSymbol === 'undefined') {
|
|
window.resolveGlobalSymbol = function(name, direct) {
|
|
return {
|
|
sym: Module['_' + name] || Module[name],
|
|
type: 'function'
|
|
};
|
|
};
|
|
}
|
|
|
|
// Click on canvas to focus it
|
|
canvasElement.addEventListener('click', function() {
|
|
canvasElement.focus();
|
|
});
|
|
|
|
canvasElement.addEventListener('mousedown', function() {
|
|
if (document.activeElement !== canvasElement) {
|
|
canvasElement.focus();
|
|
}
|
|
});
|
|
|
|
// ===========================================
|
|
// Interpreter Status
|
|
// ===========================================
|
|
|
|
function updateInterpreterStatus() {
|
|
if (!window.getPythonState) {
|
|
interpreterStatus.className = 'status-indicator status-busy';
|
|
interpreterStatus.title = 'Initializing...';
|
|
return;
|
|
}
|
|
|
|
var state = window.getPythonState();
|
|
if (state === 'OK') {
|
|
interpreterStatus.className = 'status-indicator status-ok';
|
|
interpreterStatus.title = 'Interpreter ready';
|
|
} else if (state === 'NOT_INITIALIZED') {
|
|
interpreterStatus.className = 'status-indicator status-busy';
|
|
interpreterStatus.title = 'Python not initialized';
|
|
} else {
|
|
interpreterStatus.className = 'status-indicator status-error';
|
|
interpreterStatus.title = state;
|
|
}
|
|
}
|
|
|
|
function appendToOutput(text, className) {
|
|
var span = document.createElement('span');
|
|
span.className = className || '';
|
|
span.textContent = text + '\n';
|
|
replOutput.appendChild(span);
|
|
replOutput.scrollTop = replOutput.scrollHeight;
|
|
}
|
|
|
|
// ===========================================
|
|
// REPL Functionality
|
|
// ===========================================
|
|
|
|
function runCode() {
|
|
var code = editor.getValue();
|
|
if (!code.trim()) return;
|
|
|
|
// Add to history
|
|
if (cmdHistory.length === 0 || cmdHistory[cmdHistory.length - 1] !== code) {
|
|
cmdHistory.push(code);
|
|
}
|
|
historyIndex = -1;
|
|
|
|
var inputDisplay = '>>> ' + code.split('\n')[0] + (code.includes('\n') ? '...' : '');
|
|
appendToOutput(inputDisplay, 'input');
|
|
|
|
// Check interpreter state and auto-recover if needed
|
|
var state = window.getPythonState();
|
|
if (state !== 'OK' && state !== 'NOT_INITIALIZED') {
|
|
appendToOutput('Warning: Clearing previous error state...', 'warning');
|
|
window.clearPythonError();
|
|
updateInterpreterStatus();
|
|
}
|
|
|
|
try {
|
|
// Reset environment before running for idempotent execution
|
|
window.runPython('_reset()');
|
|
|
|
var result = window.runPython(code);
|
|
updateInterpreterStatus();
|
|
|
|
if (result) {
|
|
if (result.includes('Traceback') ||
|
|
result.includes('Error:') ||
|
|
result.includes('Error\n') ||
|
|
result.startsWith('Internal REPL Error:')) {
|
|
appendToOutput(result, 'error');
|
|
} else {
|
|
appendToOutput(result, 'success');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
appendToOutput('JavaScript Error: ' + e.toString(), 'error');
|
|
updateInterpreterStatus();
|
|
}
|
|
}
|
|
|
|
function resetEnvironment() {
|
|
appendToOutput('>>> Resetting environment...', 'input');
|
|
|
|
if (window.clearPythonError) {
|
|
window.clearPythonError();
|
|
}
|
|
|
|
try {
|
|
window.resetGame();
|
|
updateInterpreterStatus();
|
|
appendToOutput('Environment reset.', 'success');
|
|
} catch (e) {
|
|
appendToOutput('Reset error: ' + e.toString(), 'error');
|
|
updateInterpreterStatus();
|
|
}
|
|
}
|
|
|
|
function clearOutput() {
|
|
replOutput.innerHTML = '';
|
|
}
|
|
|
|
// ===========================================
|
|
// Share Functionality
|
|
// ===========================================
|
|
|
|
function showShareModal() {
|
|
const code = editor.getValue();
|
|
|
|
if (code.length > CONFIG.maxCodeSize) {
|
|
alert(`Code is too large (${(code.length / 1024).toFixed(1)} KB). Maximum is ${CONFIG.maxCodeSize / 1024} KB.\n\nConsider using a GitHub Gist for larger code.`);
|
|
return;
|
|
}
|
|
|
|
const result = generateShareUrl(code);
|
|
shareUrlInput.value = result.url;
|
|
shareSizeEl.textContent = `${result.originalSize} bytes -> ${result.compressedSize} chars (${result.ratio}% of original)`;
|
|
|
|
if (result.url.length > 2000) {
|
|
shareSizeEl.textContent += ' Warning: URL is long - may not work in all browsers';
|
|
}
|
|
|
|
shareModal.classList.remove('hidden');
|
|
}
|
|
|
|
copyBtn.addEventListener('click', function() {
|
|
shareUrlInput.select();
|
|
navigator.clipboard.writeText(shareUrlInput.value).then(function() {
|
|
copyBtn.textContent = 'Copied!';
|
|
setTimeout(function() {
|
|
copyBtn.textContent = 'Copy to Clipboard';
|
|
}, 2000);
|
|
}).catch(function() {
|
|
document.execCommand('copy');
|
|
copyBtn.textContent = 'Copied!';
|
|
setTimeout(function() {
|
|
copyBtn.textContent = 'Copy to Clipboard';
|
|
}, 2000);
|
|
});
|
|
});
|
|
|
|
closeModalBtn.addEventListener('click', function() {
|
|
shareModal.classList.add('hidden');
|
|
});
|
|
|
|
shareModal.addEventListener('click', function(e) {
|
|
if (e.target === shareModal) {
|
|
shareModal.classList.add('hidden');
|
|
}
|
|
});
|
|
|
|
// Escape closes modal
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape' && !shareModal.classList.contains('hidden')) {
|
|
shareModal.classList.add('hidden');
|
|
}
|
|
});
|
|
|
|
// ===========================================
|
|
// Button Handlers
|
|
// ===========================================
|
|
|
|
runBtn.addEventListener('click', runCode);
|
|
shareBtn.addEventListener('click', showShareModal);
|
|
resetBtn.addEventListener('click', resetEnvironment);
|
|
clearBtn.addEventListener('click', clearOutput);
|
|
|
|
// ===========================================
|
|
// Initialize
|
|
// ===========================================
|
|
|
|
init();
|
|
</script>
|
|
{{{ SCRIPT }}}
|
|
</body>
|
|
</html>
|