Files
PyCanvas/editor.html
2025-12-26 10:33:57 +00:00

378 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Code Editor</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css" />
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
<script src="http://skulpt.org/js/skulpt.min.js"></script>
<script src="http://skulpt.org/js/skulpt-stdlib.js"></script>
<script src="https://unpkg.com/litecanvas/dist/dist.dev.js"></script>
</head>
<body>
<style>
:root {
--header-height: 48px;
--bg-dark: #1e1e1e;
--bg-editor: #272822;
/* Monokai background matches ACE default */
--bg-canvas: #0e0e0e;
--text-color: #f0f0f0;
--btn-bg: #f8f8f8;
--btn-hover: #e0e0e0;
--icon-color: #333;
}
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
background-color: var(--bg-dark);
color: var(--text-color);
font-family: 'Inter', 'Segoe UI', Tahoma, sans-serif;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Header */
header {
height: var(--header-height);
background-color: #2c292d;
/* Slightly different dark */
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
border-bottom: 1px solid #111;
z-index: 100;
}
.header-left,
.header-right {
display: flex;
gap: 8px;
}
.header-right {
float: right;
}
.label-btn {
height: 34px;
background-color: #f0f0e6;
border: 1px solid #ccc;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s;
padding: 2;
}
.label-btn p {
font-size: 16px;
font-weight: bold;
}
.icon-btn {
width: 34px;
height: 34px;
background-color: #f0f0e6;
border: 1px solid #ccc;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s;
padding: 0;
}
.icon-btn:hover {
background-color: #fff;
}
.icon-btn svg {
width: 20px;
height: 20px;
fill: #222;
}
main {
display: flex;
flex: 1;
height: calc(100vh - var(--header-height));
overflow: hidden;
}
#editor-container {
width: 50%;
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--bg-editor);
border-right: 2px solid #333;
}
#editor {
flex: 1;
width: 100%;
height: 100%;
}
#preview-container {
width: 50%;
height: 100%;
background-color: var(--bg-canvas);
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
.overlay-text {
position: absolute;
pointer-events: none;
color: rgba(255, 255, 255, 0.7);
font-family: 'Segoe UI', sans-serif;
font-weight: 500;
}
#terminal-container {
height: 150px;
width: 100%;
background-color: #000;
border-top: 2px solid #333;
overflow: hidden;
display: block;
}
#terminal {
width: 100%;
height: 100%;
}
</style>
<header>
<div class="header-left">
<button class="label-btn" title="PyStudio">
<p>PyStudio</p>
</button>
<button class="icon-btn" title="Run Code" onclick="runCurrentCode()">
<svg viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
</button>
<button class="icon-btn" title="Stop Code" onclick="stopCurrentCode()">
<svg viewBox="0 0 24 24">
<path d="M6 6h12v12H6z" />
</svg>
</button>
</div>
<div class="header-right">
<button class="icon-btn" title="Toggle Fullscreen" onclick="toggleFullscreen()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
<path
d="M128 96C110.3 96 96 110.3 96 128L96 224C96 241.7 110.3 256 128 256C145.7 256 160 241.7 160 224L160 160L224 160C241.7 160 256 145.7 256 128C256 110.3 241.7 96 224 96L128 96zM160 416C160 398.3 145.7 384 128 384C110.3 384 96 398.3 96 416L96 512C96 529.7 110.3 544 128 544L224 544C241.7 544 256 529.7 256 512C256 494.3 241.7 480 224 480L160 480L160 416zM416 96C398.3 96 384 110.3 384 128C384 145.7 398.3 160 416 160L480 160L480 224C480 241.7 494.3 256 512 256C529.7 256 544 241.7 544 224L544 128C544 110.3 529.7 96 512 96L416 96zM544 416C544 398.3 529.7 384 512 384C494.3 384 480 398.3 480 416L480 480L416 480C398.3 480 384 494.3 384 512C384 529.7 398.3 544 416 544L512 544C529.7 544 544 529.7 544 512L544 416z" />
</svg>
</button>
</div>
</header>
<main>
<div id="editor-container">
<div id="editor"></div>
<div id="terminal-container">
<div id="terminal"></div>
</div>
</div>
<div id="preview-container">
<canvas id="canvas"></canvas>
</div>
</main>
<script src="https://www.unpkg.com/ace-builds@latest/src-noconflict/ace.js" crossorigin="anonymous"></script>
<script>
codeEditor = ace.edit("editor", {
theme: "ace/theme/monokai",
mode: "ace/mode/python",
value: "import screen as s\n\ndef draw():\n s.cls(0)\n s.text(20, 20, \"Hello World!\", 3)\n\ns.start({\n \"loop\":{\n \"draw\":draw\n }\n})",
fontFamily: 'Consolas, monospace',
fontSize: '14px'
});
codeEditor.setShowPrintMargin(false);
let StopExecution;
let term;
let fitAddon;
let inputResolve;
term = new Terminal({
cursorBlink: true,
theme: {
background: '#1e1e1e'
},
scrollback: 10000,
allowProposedApi: true
});
fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById('terminal'));
// Use a small timeout to ensure the DOM is ready before fitting
setTimeout(() => fitAddon.fit(), 0);
function outf(text) {
term.write(text.replace(/\n/g, '\r\n'));
}
function inputf(promptText = '') {
return new Promise(resolve => {
inputBuffer = '';
inputResolve = resolve;
acceptingInput = true;
console.log("Input requested: " + promptText);
});
}
function stopCurrentCode() {
StopExecution = true;
if (window.__lc_instance) {
if (window.__lc_instance.pause) window.__lc_instance.pause();
window.__lc_instance = null;
}
const oldCanvas = document.getElementById('canvas');
if (oldCanvas) {
const newCanvas = oldCanvas.cloneNode(true);
oldCanvas.parentNode.replaceChild(newCanvas, oldCanvas);
const ctx = newCanvas.getContext('2d');
if (ctx) {
ctx.fillStyle = '#0e0e0e';
ctx.fillRect(0, 0, newCanvas.width, newCanvas.height);
}
}
const globals = ['W', 'H', 'T', 'MX', 'MY', 'CENTERX', 'CENTERY', 'ENGINE', 'WIDTH', 'HEIGHT'];
globals.forEach(g => {
delete window[g];
window[g] = undefined;
});
}
function builtinRead(x) {
if (x === 'screen' || x === './screen.js') {
const url = new URL('screen.js?t=' + Date.now(), window.location.href).href;
const xhr = new XMLHttpRequest();
xhr.open("GET", url, false);
xhr.send();
if (xhr.status === 200) return xhr.responseText;
else throw "Could not load screen.js";
}
if (Sk.builtinFiles === undefined || Sk.builtinFiles["files"][x] === undefined)
if (x === 'src/builtin/sys.js') return '';
else throw "File not found: '" + x + "'";
return Sk.builtinFiles["files"][x];
}
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}
function runCurrentCode() {
stopCurrentCode();
if (term) term.clear();
StopExecution = false;
var code = codeEditor.getValue();
const canvas = document.getElementById('canvas');
const container = document.getElementById('preview-container');
if (canvas && container) {
const w = Math.floor(container.clientWidth);
const h = Math.floor(container.clientHeight);
canvas.width = w;
canvas.height = h;
}
Sk.configure({
output: outf,
read: builtinRead,
inputfun: inputf,
inputfunTakesPrompt: true
});
Sk.misceval.asyncToPromise(() => Sk.importMainWithBody('<stdin>', false, code, true), {
"*": () => {
if (StopExecution) throw "Execution interrupted"
}
})
.catch(err => {
if (err !== "Execution interrupted") outf(err.toString() + "\n");
});
}
function toggleEditor() {
runCurrentCode();
}
function takeScreenshot() {
const canvas = document.getElementById('canvas');
if (canvas) {
const link = document.createElement('a');
link.download = 'litecanvas-screenshot.png';
link.href = canvas.toDataURL();
link.click();
}
}
document.addEventListener('keydown', function (e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
runCurrentCode();
}
});
window.addEventListener('load', () => {
setTimeout(runCurrentCode, 100);
});
window.addEventListener('resize', () => {
const canvas = document.getElementById('canvas');
const container = document.getElementById('preview-container');
if (canvas && container) {
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
}
if (fitAddon) {
fitAddon.fit();
}
});
</script>
</body>
</html>