230 lines
4.9 KiB
HTML
230 lines
4.9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>Canvas Text Editor</title>
|
|
<style>
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
height: 100%;
|
|
background: #d8cbb0;
|
|
overflow: hidden;
|
|
}
|
|
canvas {
|
|
display: block;
|
|
background: #e7dcc6;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<canvas id="c"></canvas>
|
|
|
|
<script>
|
|
const canvas = document.getElementById("c");
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
const dpr = window.devicePixelRatio || 1;
|
|
|
|
function resize() {
|
|
const w = window.innerWidth;
|
|
const h = window.innerHeight;
|
|
|
|
canvas.style.width = w + "px";
|
|
canvas.style.height = h + "px";
|
|
|
|
canvas.width = Math.floor(w * dpr);
|
|
canvas.height = Math.floor(h * dpr);
|
|
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
}
|
|
window.addEventListener("resize", resize);
|
|
resize();
|
|
|
|
/* =====================
|
|
Editor State
|
|
===================== */
|
|
|
|
const fontSize = 13;
|
|
const lineHeight = 20;
|
|
const padding = 20;
|
|
const font = `${fontSize}px monospace`;
|
|
|
|
let lines = [""];
|
|
let cursor = { line: 0, col: 0 };
|
|
|
|
let blink = true;
|
|
let blinkEnabled = true;
|
|
let typingTimeout = null;
|
|
|
|
/* =====================
|
|
Rendering
|
|
===================== */
|
|
|
|
function draw() {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
ctx.fillStyle = "#631414";
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
ctx.font = font;
|
|
ctx.textBaseline = "top";
|
|
ctx.fillStyle = "#d2531a";
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
ctx.fillText(
|
|
lines[i],
|
|
padding,
|
|
padding + i * lineHeight
|
|
);
|
|
}
|
|
|
|
if (blink) {
|
|
const before = lines[cursor.line].slice(0, cursor.col);
|
|
const x = padding + ctx.measureText(before).width;
|
|
const y = padding - 2 + cursor.line * lineHeight;
|
|
|
|
ctx.fillRect(x, y, 2, fontSize + 2);
|
|
}
|
|
}
|
|
|
|
/* =====================
|
|
Cursor Blink Logic
|
|
===================== */
|
|
|
|
setInterval(() => {
|
|
if (!blinkEnabled) return;
|
|
blink = !blink;
|
|
draw();
|
|
}, 500);
|
|
|
|
function stopBlinkWhileTyping() {
|
|
blinkEnabled = false;
|
|
blink = true;
|
|
draw();
|
|
|
|
clearTimeout(typingTimeout);
|
|
typingTimeout = setTimeout(() => {
|
|
blinkEnabled = true;
|
|
blink = true;
|
|
draw();
|
|
}, 600);
|
|
}
|
|
|
|
/* =====================
|
|
Helpers
|
|
===================== */
|
|
|
|
function clampCursor() {
|
|
cursor.line = Math.max(0, Math.min(cursor.line, lines.length - 1));
|
|
cursor.col = Math.max(0, Math.min(cursor.col, lines[cursor.line].length));
|
|
}
|
|
|
|
function insertText(text) {
|
|
const line = lines[cursor.line];
|
|
lines[cursor.line] =
|
|
line.slice(0, cursor.col) +
|
|
text +
|
|
line.slice(cursor.col);
|
|
cursor.col += text.length;
|
|
}
|
|
|
|
function newline() {
|
|
const line = lines[cursor.line];
|
|
lines[cursor.line] = line.slice(0, cursor.col);
|
|
lines.splice(cursor.line + 1, 0, line.slice(cursor.col));
|
|
cursor.line++;
|
|
cursor.col = 0;
|
|
}
|
|
|
|
function backspace() {
|
|
if (cursor.col > 0) {
|
|
const line = lines[cursor.line];
|
|
lines[cursor.line] =
|
|
line.slice(0, cursor.col - 1) +
|
|
line.slice(cursor.col);
|
|
cursor.col--;
|
|
} else if (cursor.line > 0) {
|
|
const prevLen = lines[cursor.line - 1].length;
|
|
lines[cursor.line - 1] += lines[cursor.line];
|
|
lines.splice(cursor.line, 1);
|
|
cursor.line--;
|
|
cursor.col = prevLen;
|
|
}
|
|
}
|
|
|
|
/* =====================
|
|
Keyboard Input
|
|
===================== */
|
|
|
|
window.addEventListener("keydown", (e) => {
|
|
if (e.metaKey || e.ctrlKey) return;
|
|
|
|
let changed = true;
|
|
|
|
switch (e.key) {
|
|
case "ArrowLeft": cursor.col--; break;
|
|
case "ArrowRight": cursor.col++; break;
|
|
case "ArrowUp": cursor.line--; break;
|
|
case "ArrowDown": cursor.line++; break;
|
|
case "Backspace": backspace(); e.preventDefault(); break;
|
|
case "Enter": newline(); e.preventDefault(); break;
|
|
case "Tab": insertText(" "); e.preventDefault(); break;
|
|
default:
|
|
if (e.key.length === 1) {
|
|
insertText(e.key);
|
|
e.preventDefault();
|
|
} else {
|
|
changed = false;
|
|
}
|
|
}
|
|
|
|
clampCursor();
|
|
|
|
if (changed) {
|
|
stopBlinkWhileTyping();
|
|
}
|
|
|
|
draw();
|
|
});
|
|
|
|
/* =====================
|
|
Mouse → Cursor
|
|
===================== */
|
|
|
|
canvas.addEventListener("mousedown", (e) => {
|
|
ctx.font = font;
|
|
|
|
const mx = e.offsetX - padding;
|
|
const my = e.offsetY - padding;
|
|
|
|
cursor.line = Math.max(
|
|
0,
|
|
Math.min(Math.floor(my / lineHeight), lines.length - 1)
|
|
);
|
|
|
|
let x = 0;
|
|
cursor.col = 0;
|
|
for (let i = 0; i < lines[cursor.line].length; i++) {
|
|
const w = ctx.measureText(lines[cursor.line][i]).width;
|
|
if (x + w / 2 >= mx) break;
|
|
x += w;
|
|
cursor.col++;
|
|
}
|
|
|
|
blink = true;
|
|
blinkEnabled = true;
|
|
draw();
|
|
});
|
|
|
|
/* =====================
|
|
Initial Draw
|
|
===================== */
|
|
|
|
draw();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|