commit 333509c64b45640cdbc80f1f90091f8be7328677
Author: MrjensK <jens.se@icloud.com>
AuthorDate: Wed Apr 22 20:53:44 2026 +0200
Commit: MrjensK <jens.se@icloud.com>
CommitDate: Wed Apr 22 20:53:44 2026 +0200
lots of upgrades
---
fox.ans | 75 ++++++++++
index.html | 476 ++++++++++++++++++++++++++++++++++++++++++++++++++-----------
2 files changed, 467 insertions(+), 84 deletions(-)
diff --git a/fox.ans b/fox.ans
new file mode 100644
index 0000000..06dd862
--- /dev/null
+++ b/fox.ans
@@ -0,0 +1,75 @@
+[0;37m
+
+
+
+
+
+
+
+
+
+
+
+
+
+ .@
+ .#%.. +*..+
+ @@@@@. @@=* .@
+ %%@# @ @%%
+ ##..% @++
+ %%%@@###+ %@. -
+ #..@@..=. .@@. ....
+ #@**%%==@%--*# #::@*
+ .@@%%@@**@@%%@@ @@%@@@ .
+ :-::##..=+..-- =..+= =
+ ..%@==@@--#@ @@-- ++@@%%@@.
+ %..@@.. . * *#@@@@@@@@@@.
+ --- . #::%%==@@==%%--
+ :. + .@@@@@@@@@@@@@@@@
+ --@@%%@@%%@@%%@@++
+ .@@##@@##@@%%@@%%@@%%@@.
+ + .:@@@@@@@@@@@@@@@@@@@@@@@@@
+ *..#*::*%++@@++@@**@@++@@++@@..
+ ..@@+=@@@@@@@@@@@@@@@@@@@@@@@@
+ . ....@@##@@@@@@@@@@@@@@@@@@@@@@@@@
+ .. .#%..#%::%%..@@--@@::%#..+=
+ . @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.
+ =+%%@@*#@@%%@@@@@@%%@@@@@@##@@
+ ==::%%--@@**@@**@@**@@--@%:.
+ @@@*==@@**@@@@@@@@@@@@@@@@@@@@@@%%@
+ ==% +- +@=-@@==@@==@@**@@##%# #
+ --: ==..@@%%@@##@@%%@@%%@@%%@%
+ @@@@@@%@@@@@@@@@@@@@@@@@@@@%@@. +
+ #%::%%--@@..+%::@@::@#..%@::%*....
+ .@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@**@@ .
+ #%@@@@@@@@@@@@@@@@@@@@@@%%@@%%@@+=@@
+ .%%==@%==@@++@@++@@==%%..*%==#*..=- .
+ .@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+ .@@**@@##@@##@@##@@**@@**@@##@%::*=--@@
+ +*@@##@@%%@@%% @. . %@##@@+*@@%%@@%%-@##@@##@@##@@--==--@#
+ **@@@@@@@@@@@@@@@@@@@@@@ %@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%@@++@@
+ +::*%::@@--@@==@@==@@--#* ::: %::%#..#@--@@--%%--%%-:%#--#* - :
+ . @@@@@@@@@@@@@@@@@@@@@%@@@ ..%@@@++@%%%@@@@@@@@@@##@@@@@@++@@...
+ * . .@@@@@@@@@@@@@@@@@@@@@@@@@#*@ %@@@::@@ .@@@@@@@@@@%%@@@@@@##@@
+ =@==@@==@@--%@--%#..%%..+=..##::## #==@@--@%--=...+= .
+ =* @@@@@@@@@@@@@@@@@@@@@%@@%@@@@@%%@@@@@@@@@@@@@@@%:.@@**@@@@
+ @%%@@@@@@%%@@%%@@%%@@*+@@==@@%%@@%%@@%@@@%%@@%%@...##..@@#*
+ .##@@%#@@##@@==@#==@@==@@##@@##@@##@@%%@@##@@##:. .@@##@
+ @@@@@@@@@@@@@@@@@##@@@@@@@@@@@@@@@@@@@@@@#* @@@@@.
+ :@==%%::#*..**..%@--%*..@@++%%..##++@@
+ @@%%@@##@@--@@==@@**%# %+==%@**
+ @%@@@##. .- @@%%@+ =@..@.##:.
+ . .
+ %%@:
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/index.html b/index.html
index 29a1c91..ec0133c 100644
--- a/index.html
+++ b/index.html
@@ -62,7 +62,7 @@
.eb-btn{background:none;border:1px solid var(--dim);color:var(--dim2);font-family:inherit;font-size:10px;padding:2px 8px;cursor:pointer;letter-spacing:1px;transition:all .15s;white-space:nowrap}
.eb-btn:hover{border-color:var(--cyan-dk);color:var(--cyan)}
.eb-btn.active{border-color:var(--cyan);color:var(--cyan)}
- .swatch-row{display:flex;gap:2px}
+
#active-char-wrap{display:flex;align-items:center;gap:4px;cursor:pointer}
#active-char-box{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:1px solid var(--cyan-dk);font-family:'IBM VGA 8x16',monospace;font-size:16px;line-height:1;background:#000;color:#fff}
#active-char-wrap:hover #active-char-box{border-color:var(--cyan)}
@@ -70,9 +70,15 @@
#charpicker.visible{display:block}
#canvas-bg-picker{display:none;position:fixed;z-index:200;background:#000;border:1px solid var(--cyan-dk);padding:6px;box-shadow:0 0 20px rgba(0,0,0,.9)}
#canvas-bg-picker.visible{display:block}
- #cbp-grid{display:grid;grid-template-columns:repeat(8,18px);gap:2px}
+ #cbp-grid{display:grid;grid-template-columns:repeat(8,22px);gap:2px}
#canvas-bg-swatch{width:22px;height:22px;border:1px solid var(--cyan-dk);cursor:pointer;flex-shrink:0}
#screen.eyedropper{cursor:crosshair !important}
+ #new-wrap{position:relative;display:inline-block}
+ #new-menu{display:none;position:absolute;top:calc(100% + 2px);left:0;background:#000;border:1px solid var(--cyan-dk);z-index:200;flex-direction:column;min-width:100%;white-space:nowrap}
+ #new-menu.visible{display:flex}
+ #new-menu .tb-btn{border:none;border-bottom:1px solid var(--border);text-align:left;width:100%;padding:4px 12px}
+ #new-menu .tb-btn:last-child{border-bottom:none}
+ #na-text{width:100%;height:130px;background:#000;border:1px solid var(--border);color:#aaa;font-family:'Share Tech Mono','Courier New',monospace;font-size:11px;padding:5px;resize:vertical;box-sizing:border-box;margin:8px 0}
#export-wrap{position:relative;display:none}
#export-wrap.visible{display:inline-block}
#export-menu{display:none;position:absolute;top:calc(100% + 2px);left:0;background:#000;border:1px solid var(--cyan-dk);z-index:200;flex-direction:column;min-width:100%}
@@ -93,9 +99,9 @@
.cpc{width:18px;height:18px;display:flex;align-items:center;justify-content:center;font-family:'IBM VGA 8x16',monospace;font-size:13px;line-height:1;cursor:pointer;color:#aaa;border:1px solid transparent;-webkit-user-select:none;user-select:none}
.cpc:hover{background:var(--cyan-dk);color:#fff}
.cpc.sel{border-color:var(--cyan);color:var(--cyan)}
- #new-dlg{display:none;position:fixed;z-index:200;top:50%;left:50%;transform:translate(-50%,-50%);background:#0a0a0a;border:1px solid var(--cyan-dk);padding:24px;min-width:260px}
- #new-dlg.visible{display:block}
- #new-dlg h3{color:var(--cyan);font-size:11px;letter-spacing:2px;margin-bottom:16px}
+ #new-dlg,#new-ansi-dlg{display:none;position:fixed;z-index:200;top:50%;left:50%;transform:translate(-50%,-50%);background:#0a0a0a;border:1px solid var(--cyan-dk);padding:24px;min-width:260px}
+ #new-dlg.visible,#new-ansi-dlg.visible{display:block}
+ #new-dlg h3,#new-ansi-dlg h3{color:var(--cyan);font-size:11px;letter-spacing:2px;margin-bottom:16px}
.dlg-row{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.dlg-row label{color:var(--dim2);font-size:11px;width:70px}
.dlg-row input{background:#000;border:1px solid var(--dim);color:var(--cyan);font-family:inherit;font-size:11px;padding:3px 8px;width:90px}
@@ -119,10 +125,23 @@
#os-toggle{display:flex;border:1px solid var(--cyan-dk);font-size:10px}
.os-btn{background:none;color:var(--dim2);border:none;padding:3px 10px;font-family:inherit;cursor:pointer;letter-spacing:1px}
.os-btn.active{background:var(--cyan-dk);color:#fff}
- /* Active color preview */
- #color-preview{position:relative;width:30px;height:24px;flex-shrink:0;cursor:default}
- #cp-bg-box{position:absolute;bottom:0;right:0;width:18px;height:18px;border:1px solid #444}
- #cp-fg-box{position:absolute;top:0;left:0;width:18px;height:18px;border:1px solid #aaa;box-shadow:0 0 0 1px #000}
+ .color-btn{width:28px;height:18px;cursor:pointer;border:1px solid var(--cyan-dk);flex-shrink:0;transition:border-color .1s}
+ .color-btn:hover{border-color:var(--cyan)}
+ #color256-picker{display:none;position:fixed;z-index:200;background:#0a0a0a;border:1px solid var(--cyan-dk);padding:8px;box-shadow:0 0 24px rgba(0,0,0,.95)}
+ #color256-picker.visible{display:block}
+ #c256-title{color:var(--cyan);font-size:10px;letter-spacing:2px;margin-bottom:5px}
+ .c256-lbl{color:var(--dim2);font-size:9px;letter-spacing:1px;margin:5px 0 2px}
+ #c256-sys-grid{display:grid;grid-template-columns:repeat(16,16px);gap:1px;margin-bottom:4px}
+ #c256-cube-grid{display:flex;gap:4px;margin-bottom:4px}
+ .c256-plane{display:grid;grid-template-columns:repeat(6,9px);gap:1px}
+ #c256-gray-grid{display:grid;grid-template-columns:repeat(24,12px);gap:1px}
+ .c256c{cursor:pointer;border:1px solid transparent;box-sizing:border-box;position:relative}
+ .c256c:hover{border-color:#fff;transform:scale(1.3);z-index:1}
+ .c256c.sel{border:2px solid #fff !important}
+ #c256-transp{display:none;margin-top:6px}
+ #c256-transp-btn{color:var(--dim2);font-size:10px;cursor:pointer;border:1px solid var(--dim);padding:2px 10px;letter-spacing:1px;display:inline-block}
+ #c256-transp-btn:hover{color:var(--cyan);border-color:var(--cyan-dk)}
+ #c256-transp-btn.sel{color:var(--cyan);border-color:var(--cyan)}
/* Image convert dialog */
#img-dlg{display:none;position:fixed;z-index:200;top:50%;left:50%;transform:translate(-50%,-50%);background:#0a0a0a;border:1px solid var(--cyan-dk);padding:24px;min-width:280px}
#img-dlg.visible{display:block}
@@ -138,9 +157,16 @@
<div id="topbar">
<div class="logo">NFO<em>::</em>ANS<em>::</em>EDITOR</div>
- <button type="button" class="tb-btn" onclick="document.getElementById('file-input').click()" title="Ctrl+O">[ OPEN ]</button>
- <button type="button" class="tb-btn dim" onclick="showNewDlg()" title="Ctrl+N">[ NEW ]</button>
- <button type="button" class="tb-btn dim" onclick="document.getElementById('img-input').click()" title="Konvertera bild till ANSI">[ BILD→ANS ]</button>
+ <button type="button" class="tb-btn" onclick="openWithPicker()" title="Ctrl+O">[ OPEN ]</button>
+ <div id="new-wrap">
+ <button type="button" class="tb-btn dim" onclick="toggleNewMenu()" title="Ctrl+N">[ NEW ▾ ]</button>
+ <div id="new-menu">
+ <button type="button" class="tb-btn dim" onclick="showNewDlg();hideNewMenu()">[ EMPTY ]</button>
+ <button type="button" class="tb-btn dim" onclick="newFromClipboard()">[ FROM CLIPBOARD ]</button>
+ <button type="button" class="tb-btn dim" onclick="showNewAnsiDlg();hideNewMenu()">[ FROM ANSI CODE ]</button>
+ </div>
+ </div>
+ <button type="button" class="tb-btn dim" onclick="document.getElementById('img-input').click()" title="Konvertera bild till ANSI">[ IMAGE → ANS ]</button>
<input type="file" id="file-input" accept=".nfo,.ans,.txt,.diz,.asc,.ansi">
<input type="file" id="img-input" accept="image/*" title="Välj bild att konvertera till ANSI">
<div id="filename">drop .nfo / .ans anywhere</div>
@@ -152,6 +178,7 @@
<option value="0">auto</option>
</select>
</div>
+ <button type="button" id="save-btn" class="tb-btn dim" onclick="saveANS()" title="Ctrl+S">[ SAVE ]</button>
<div id="export-wrap">
<button type="button" id="export-btn" class="tb-btn dim" onclick="toggleExportMenu()">[ EXPORT ▾ ]</button>
<div id="export-menu">
@@ -207,6 +234,13 @@
<div class="help-row"><span class="help-key">Valfritt tecken</span><span class="help-desc">Skriv med aktiv FG/BG-färg</span></div>
</div>
+ <div class="help-section">
+ <h3>MARKERING & KLIPPBORD</h3>
+ <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>C</kbd></span><span class="help-desc">Kopiera markering (till app + systemklippbord)</span></div>
+ <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>X</kbd></span><span class="help-desc">Klipp ut markering</span></div>
+ <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>V</kbd></span><span class="help-desc">Klistra in — även extern ASCII-art från systemklippbord</span></div>
+ </div>
+
<div class="help-section">
<h3>DRAW-LÄGE</h3>
<div class="help-row"><span class="help-key">Klick</span><span class="help-desc">Måla cell med aktivt tecken</span></div>
@@ -224,21 +258,16 @@
<button type="button" class="eb-btn" id="mode-sel-btn" onclick="setMode('select')" title="Markera område">[ SEL ]</button>
<button type="button" class="eb-btn" id="pick-btn" onclick="toggleEyedropper()" title="Högerklick på canvas plockar också färg">[ PICK ]</button>
<div class="eb-sep"></div>
- <div id="color-preview" title="Aktiv FG/BG">
- <div id="cp-bg-box"></div>
- <div id="cp-fg-box"></div>
- </div>
- <div class="eb-sep"></div>
<div id="active-char-wrap" onclick="toggleCharPicker(event)" title="F2 — pick character">
<span class="eb-label">CHAR:</span>
<div id="active-char-box">█</div>
</div>
<div class="eb-sep"></div>
<span class="eb-label">FG:</span>
- <div id="fg-swatches" class="swatch-row"></div>
+ <div id="cp-fg-box" class="color-btn" onclick="openColorPicker('fg',event)" title="Teckenfärg (klicka för att byta)"></div>
<div class="eb-sep"></div>
<span class="eb-label">BG:</span>
- <div id="bg-swatches" class="swatch-row"></div>
+ <div id="cp-bg-box" class="color-btn" onclick="openColorPicker('bg',event)" title="Bakgrundsfärg (klicka för att byta)"></div>
<div class="eb-sep"></div>
<span class="eb-label">YTA:</span>
<div id="canvas-bg-swatch" title="Arbetsytans bakgrundsfärg — klicka för att byta" onclick="toggleCanvasBgPicker(event)"></div>
@@ -257,6 +286,16 @@
<div id="charpicker"><div id="cp-grid"></div></div>
<div id="canvas-bg-picker"><div id="cbp-grid"></div></div>
+<div id="color256-picker">
+ <div id="c256-title">FG</div>
+ <div class="c256-lbl">SYSTEM</div>
+ <div id="c256-sys-grid"></div>
+ <div class="c256-lbl">FÄRGKUB</div>
+ <div id="c256-cube-grid"></div>
+ <div class="c256-lbl">GRÅSKALA</div>
+ <div id="c256-gray-grid"></div>
+ <div id="c256-transp"><span id="c256-transp-btn" onmousedown="event.preventDefault()" onclick="setActiveBg(-1);hideColorPicker()">[ INGEN BG ]</span></div>
+</div>
<div id="overlay" onclick="hideActiveDialog()"></div>
<div id="new-dlg">
@@ -269,11 +308,22 @@
</div>
</div>
+<div id="new-ansi-dlg">
+ <h3>NEW FROM ANSI CODE</h3>
+ <div class="dlg-row"><label>Cols</label><input type="number" id="na-cols" value="80" min="20" max="320"></div>
+ <textarea id="na-text" placeholder="Klistra in ANSI-kod (ESC-sekvenser) här..."></textarea>
+ <div class="dlg-btns">
+ <button type="button" class="eb-btn" onclick="hideNewAnsiDlg()">[ CANCEL ]</button>
+ <button class="eb-btn active" onclick="createFromAnsiCode()">[ CREATE ]</button>
+ </div>
+</div>
+
<div id="img-dlg">
<h3>BILD → ANSI</h3>
<div class="dlg-row"><label>Kolonner</label><input type="number" id="img-cols" value="80" min="10" max="320" oninput="imgAutoRows()"></div>
<div class="dlg-row"><label>Rader</label><input type="number" id="img-rows" value="40" min="5" max="300"></div>
- <div class="dlg-check"><input type="checkbox" id="img-half" checked><label for="img-half">Halvblock ▀ (dubbel vertikal upplösning)</label></div>
+ <div class="dlg-check"><input type="checkbox" id="img-ascii" onchange="imgAsciiToggle()"><label for="img-ascii">ASCII-tecken <span id="img-ascii-ramp" style="color:var(--dim2);font-size:10px;letter-spacing:1px"> .:-=+*#%@</span></label></div>
+ <div class="dlg-check" id="img-half-row"><input type="checkbox" id="img-half" checked><label for="img-half">Halvblock ▀ (dubbel vertikal upplösning)</label></div>
<div class="dlg-check"><input type="checkbox" id="img-dither" checked><label for="img-dither">Dithering (bättre färgövergångar)</label></div>
<div class="dlg-check"><input type="checkbox" id="img-aspect" checked onchange="imgAutoRows()"><label for="img-aspect">Bevara proportioner</label></div>
<div id="img-preview-wrap"></div>
@@ -336,12 +386,20 @@ for (let i = 1; i < 256; i++) CP437_REV.set(CP437[i], i);
CP437_REV.set(0x20, 0x20);
const TRANSP_BG = 'repeating-conic-gradient(#888 0% 25%,#fff 0% 50%) 0/8px 8px';
-const PALETTE = [
- '#000000','#aa0000','#00aa00','#aa5500',
- '#0000aa','#aa00aa','#00aaaa','#aaaaaa',
- '#555555','#ff5555','#55ff55','#ffff55',
- '#5555ff','#ff55ff','#55ffff','#ffffff'
-];
+const PALETTE = (() => {
+ const h = n => n.toString(16).padStart(2,'0');
+ const p = [
+ '#000000','#aa0000','#00aa00','#aa5500',
+ '#0000aa','#aa00aa','#00aaaa','#aaaaaa',
+ '#555555','#ff5555','#55ff55','#ffff55',
+ '#5555ff','#ff55ff','#55ffff','#ffffff'
+ ];
+ const lvl = [0,95,135,175,215,255];
+ for (let r=0;r<6;r++) for (let g=0;g<6;g++) for (let b=0;b<6;b++)
+ p.push(`#${h(lvl[r])}${h(lvl[g])}${h(lvl[b])}`);
+ for (let i=0;i<24;i++) { const v=8+i*10; p.push(`#${h(v)}${h(v)}${h(v)}`); }
+ return p;
+})();
// ── Data model ───────────────────────────────────────────────────────────────
let sparseMap = new Map();
@@ -378,11 +436,13 @@ let undoStack = [], redoStack = [];
let selActive = false, selDragging = false;
let selR1 = 0, selC1 = 0, selR2 = 0, selC2 = 0;
let clipboard = null;
+let clipboardText = null;
+let lastFileHandle = null;
// ── Tabs ──────────────────────────────────────────────────────────────────────
function newTabState() {
return { name:'untitled', sparseMap:new Map(), lastFile:null, lastBytes:null,
- canvasCols:80, canvasRows:25, canvasBg:'#000000',
+ lastFileHandle:null, canvasCols:80, canvasRows:25, canvasBg:'#000000',
undoStack:[], redoStack:[], isCentered:false };
}
let tabs = [newTabState()];
@@ -391,6 +451,13 @@ let activeTab = 0;
updateColorPreview();
document.getElementById('canvas-bg-swatch').style.background = canvasBg;
renderTabs();
+if (!window.showOpenFilePicker) {
+ const sb = document.getElementById('save-btn');
+ sb.disabled = true;
+ sb.title = 'Save function only supported by Chrome and Edge at the moment';
+ sb.style.opacity = '0.35';
+ sb.style.cursor = 'not-allowed';
+}
// ── Canvas rendering ─────────────────────────────────────────────────────────
const canvas = document.getElementById('screen');
@@ -640,36 +707,79 @@ function pickColorAt(row, col) {
if (cell.bg >= 0) setActiveBg(cell.bg);
}
-// ── Color swatches ────────────────────────────────────────────────────────────
-(function buildSwatches() {
- const fgDiv = document.getElementById('fg-swatches');
- const bgDiv = document.getElementById('bg-swatches');
- PALETTE.forEach((color, i) => {
- const fs = document.createElement('span');
- fs.className = 'csw' + (i === activeFg ? ' sel' : '');
- fs.style.background = color;
- fs.title = 'FG ' + i;
- fs.dataset.idx = i;
- fs.addEventListener('mousedown', e => { e.preventDefault(); setActiveFg(i); });
- fgDiv.appendChild(fs);
- const bs = document.createElement('span');
- bs.className = 'csw';
- bs.style.background = color;
- bs.title = 'BG ' + i;
- bs.dataset.idx = i;
- bs.addEventListener('mousedown', e => { e.preventDefault(); setActiveBg(i); });
- bgDiv.appendChild(bs);
- });
- // Transparent BG swatch
- const ns = document.createElement('span');
- ns.className = 'csw sel';
- ns.style.background = TRANSP_BG;
- ns.title = 'BG none';
- ns.dataset.idx = -1;
- ns.addEventListener('mousedown', e => { e.preventDefault(); setActiveBg(-1); });
- bgDiv.appendChild(ns);
+// ── 256-color picker ──────────────────────────────────────────────────────────
+let _colorPickerMode = 'fg';
+
+(function buildColorPicker256() {
+ function makeCell(i, size) {
+ const c = document.createElement('div');
+ c.className = 'c256c';
+ c.style.cssText = `background:${PALETTE[i]};width:${size}px;height:${size}px`;
+ c.title = `#${i} ${PALETTE[i]}`;
+ c.dataset.idx = i;
+ c.addEventListener('mousedown', e => e.preventDefault());
+ c.addEventListener('click', () => {
+ if (_colorPickerMode === 'fg') setActiveFg(i);
+ else setActiveBg(i);
+ hideColorPicker();
+ });
+ return c;
+ }
+ const sys = document.getElementById('c256-sys-grid');
+ const cube = document.getElementById('c256-cube-grid');
+ const gray = document.getElementById('c256-gray-grid');
+ for (let i=0; i<16; i++) sys.appendChild(makeCell(i, 16));
+ for (let r=0; r<6; r++) {
+ const plane = document.createElement('div');
+ plane.className = 'c256-plane';
+ for (let g=0; g<6; g++)
+ for (let b=0; b<6; b++)
+ plane.appendChild(makeCell(16 + r*36 + g*6 + b, 9));
+ cube.appendChild(plane);
+ }
+ for (let i=232; i<256; i++) gray.appendChild(makeCell(i, 12));
})();
+function updateColorPicker256Selection() {
+ document.querySelectorAll('.c256c').forEach(c =>
+ c.classList.toggle('sel',
+ _colorPickerMode==='fg' ? +c.dataset.idx===activeFg : +c.dataset.idx===activeBg));
+ const tb = document.getElementById('c256-transp-btn');
+ if (tb) tb.classList.toggle('sel', _colorPickerMode==='bg' && activeBg===-1);
+}
+
+function openColorPicker(mode, e) {
+ e.stopPropagation();
+ const picker = document.getElementById('color256-picker');
+ if (picker.classList.contains('visible') && _colorPickerMode===mode) {
+ hideColorPicker(); return;
+ }
+ _colorPickerMode = mode;
+ document.getElementById('c256-title').textContent = mode === 'fg' ? 'FG — TECKENFÄRG' : 'BG — BAKGRUNDSFÄRG';
+ document.getElementById('c256-transp').style.display = mode === 'bg' ? 'block' : 'none';
+ updateColorPicker256Selection();
+ const ebH = document.getElementById('editbar').offsetHeight;
+ picker.style.bottom = (ebH + 4) + 'px';
+ const btn = mode === 'fg'
+ ? document.getElementById('cp-fg-box')
+ : document.getElementById('cp-bg-box');
+ const rect = btn.getBoundingClientRect();
+ const pickerW = 374 + 16;
+ const left = Math.min(rect.left, window.innerWidth - pickerW - 8);
+ picker.style.left = Math.max(0, left) + 'px';
+ picker.classList.add('visible');
+}
+function hideColorPicker() {
+ document.getElementById('color256-picker').classList.remove('visible');
+}
+document.addEventListener('click', e => {
+ const p = document.getElementById('color256-picker');
+ if (!p.contains(e.target) &&
+ !document.getElementById('cp-fg-box').contains(e.target) &&
+ !document.getElementById('cp-bg-box').contains(e.target))
+ hideColorPicker();
+});
+
function updateColorPreview() {
document.getElementById('cp-fg-box').style.background = PALETTE[activeFg];
document.getElementById('cp-bg-box').style.background =
@@ -677,14 +787,10 @@ function updateColorPreview() {
}
function setActiveFg(i) {
activeFg = i;
- document.querySelectorAll('#fg-swatches .csw').forEach(s =>
- s.classList.toggle('sel', +s.dataset.idx === i));
updateColorPreview();
}
function setActiveBg(i) {
activeBg = i;
- document.querySelectorAll('#bg-swatches .csw').forEach(s =>
- s.classList.toggle('sel', +s.dataset.idx === i));
updateColorPreview();
}
@@ -723,7 +829,7 @@ function hideCharPicker() {
// ── Canvas background picker ──────────────────────────────────────────────────
(function buildCanvasBgPicker() {
const grid = document.getElementById('cbp-grid');
- PALETTE.forEach((color, i) => {
+ PALETTE.slice(0, 16).forEach((color, i) => {
const s = document.createElement('div');
s.className = 'csw';
s.style.background = color;
@@ -953,6 +1059,8 @@ document.addEventListener('keydown', e => {
if (document.getElementById('help-overlay').classList.contains('visible')) { hideHelp(); return; }
if (document.getElementById('img-dlg').classList.contains('visible')) { hideImgDlg(); return; }
if (document.getElementById('new-dlg').classList.contains('visible')) { hideNewDlg(); return; }
+ if (document.getElementById('new-ansi-dlg').classList.contains('visible')) { hideNewAnsiDlg(); return; }
+ hideNewMenu();
}
// ⌘+Backspace = Delete, works even when canvas lost focus
if (e.metaKey && e.key === 'Backspace' && editMode && drawMode === 'text') {
@@ -971,9 +1079,9 @@ document.addEventListener('keydown', e => {
if (e.key==='c'||e.key==='C') { e.preventDefault(); copySelection(); }
if (e.key==='x'||e.key==='X') { e.preventDefault(); cutSelection(); }
if (e.key==='v'||e.key==='V') { e.preventDefault(); pasteClipboard(); }
- if (e.key==='o'||e.key==='O') { e.preventDefault(); document.getElementById('file-input').click(); }
- if (e.key==='n'||e.key==='N') { e.preventDefault(); showNewDlg(); }
- if (e.key==='s'&&!e.shiftKey) { e.preventDefault(); exportANS(); }
+ if (e.key==='o'||e.key==='O') { e.preventDefault(); openWithPicker(); }
+ if (e.key==='n'||e.key==='N') { e.preventDefault(); toggleNewMenu(); }
+ if (e.key==='s'&&!e.shiftKey) { e.preventDefault(); saveANS(); }
if (e.key==='s'&& e.shiftKey) { e.preventDefault(); exportHTML(); }
});
@@ -1018,12 +1126,19 @@ function copySelection() {
const r1=Math.min(selR1,selR2), r2=Math.max(selR1,selR2);
const c1=Math.min(selC1,selC2), c2=Math.max(selC1,selC2);
const cells = new Map();
- for (let r=r1; r<=r2; r++)
+ const lines = [];
+ for (let r=r1; r<=r2; r++) {
+ let line = '';
for (let c=c1; c<=c2; c++) {
const cell = sparseMap.get(r*100000+c);
- if (cell) cells.set((r-r1)*100000+(c-c1), {...cell});
+ if (cell) { cells.set((r-r1)*100000+(c-c1), {...cell}); line += cell.ch; }
+ else line += ' ';
}
+ lines.push(line);
+ }
clipboard = { cells, rows:r2-r1+1, cols:c2-c1+1 };
+ clipboardText = lines.join('\n');
+ navigator.clipboard.writeText(clipboardText).catch(()=>{});
}
function cutSelection() {
if (!selActive) return;
@@ -1037,14 +1152,41 @@ function cutSelection() {
selActive = false;
drawAll();
}
-function pasteClipboard() {
- if (!clipboard || !editMode) return;
- pushUndo();
- for (const [key, cell] of clipboard.cells) {
- const dr = Math.floor(key/100000), dc = key%100000;
- sparseMap.set((curRow+dr)*100000+(curCol+dc), {...cell});
+async function pasteClipboard() {
+ if (!editMode) return;
+ let sysText = null;
+ try { sysText = await navigator.clipboard.readText(); } catch(e) {}
+
+ if (sysText !== null && sysText === clipboardText && clipboard) {
+ // intern kopia matchar systemet → klistra med färger
+ pushUndo();
+ for (const [key, cell] of clipboard.cells) {
+ const dr = Math.floor(key/100000), dc = key%100000;
+ sparseMap.set((curRow+dr)*100000+(curCol+dc), {...cell});
+ }
+ drawAll();
+ } else if (sysText !== null && sysText !== '') {
+ // extern text (t.ex. ASCII-art) → klistra rad för rad
+ pushUndo();
+ const lines = sysText.split('\n');
+ const startCol = curCol;
+ for (let li = 0; li < lines.length; li++) {
+ const line = lines[li].replace(/\r$/, '');
+ for (let ci = 0; ci < line.length; ci++) {
+ const ch = line[ci];
+ sparseMap.set((curRow+li)*100000+(startCol+ci), { ch, fg: activeFg, bg: activeBg });
+ }
+ }
+ drawAll();
+ } else if (clipboard) {
+ // systemklippbord tomt, använd intern kopia
+ pushUndo();
+ for (const [key, cell] of clipboard.cells) {
+ const dr = Math.floor(key/100000), dc = key%100000;
+ sparseMap.set((curRow+dr)*100000+(curCol+dc), {...cell});
+ }
+ drawAll();
}
- drawAll();
}
// ── Tabs ──────────────────────────────────────────────────────────────────────
@@ -1052,7 +1194,7 @@ function saveCurrentTab() {
if (activeTab < 0 || activeTab >= tabs.length) return;
const t = tabs[activeTab];
t.name = lastFile ? lastFile.name : t.name;
- t.sparseMap=sparseMap; t.lastFile=lastFile; t.lastBytes=lastBytes;
+ t.sparseMap=sparseMap; t.lastFile=lastFile; t.lastBytes=lastBytes; t.lastFileHandle=lastFileHandle;
t.canvasCols=canvasCols; t.canvasRows=canvasRows; t.canvasBg=canvasBg;
t.undoStack=undoStack; t.redoStack=redoStack; t.isCentered=isCentered;
}
@@ -1089,7 +1231,7 @@ function switchTab(idx) {
if (editMode) toggleEdit();
activeTab = idx;
const t = tabs[idx];
- sparseMap=t.sparseMap; lastFile=t.lastFile; lastBytes=t.lastBytes;
+ sparseMap=t.sparseMap; lastFile=t.lastFile; lastBytes=t.lastBytes; lastFileHandle=t.lastFileHandle||null;
canvasCols=t.canvasCols; canvasRows=t.canvasRows; canvasBg=t.canvasBg;
undoStack=t.undoStack; redoStack=t.redoStack; isCentered=t.isCentered;
curRow=0; curCol=0; selActive=false;
@@ -1118,7 +1260,7 @@ function addTab() {
tabs.push(newTabState());
activeTab = tabs.length - 1;
const t = tabs[activeTab];
- sparseMap=t.sparseMap; lastFile=null; lastBytes=null;
+ sparseMap=t.sparseMap; lastFile=null; lastBytes=null; lastFileHandle=null;
canvasCols=80; canvasRows=25; canvasBg='#000000'; isCentered=false;
undoStack=[]; redoStack=[]; curRow=0; curCol=0; selActive=false;
document.getElementById('canvas-bg-swatch').style.background = canvasBg;
@@ -1165,7 +1307,8 @@ function showNewDlg() {
document.getElementById('new-dlg').classList.add('visible');
}
function hideActiveDialog() {
- if (document.getElementById('img-dlg').classList.contains('visible')) { hideImgDlg(); return; }
+ if (document.getElementById('img-dlg').classList.contains('visible')) { hideImgDlg(); return; }
+ if (document.getElementById('new-ansi-dlg').classList.contains('visible')) { hideNewAnsiDlg(); return; }
hideNewDlg();
}
function hideNewDlg() {
@@ -1195,6 +1338,91 @@ function createNewCanvas() {
updateCursor();
}
+// ── New menu ──────────────────────────────────────────────────────────────────
+function toggleNewMenu() {
+ document.getElementById('new-menu').classList.toggle('visible');
+}
+function hideNewMenu() {
+ document.getElementById('new-menu').classList.remove('visible');
+}
+document.addEventListener('click', e => {
+ if (!document.getElementById('new-wrap').contains(e.target)) hideNewMenu();
+});
+
+function textToCP437Bytes(text) {
+ // Unescape common escape-sequence notations (\x1b, \033) from source/doc pastes
+ text = text.replace(/\\x1b/gi, '\x1b').replace(/\\033/g, '\x1b');
+ const out = [];
+ for (const ch of text) {
+ const cp = ch.codePointAt(0);
+ if (cp < 128) {
+ out.push(cp);
+ } else {
+ const b = CP437_REV.get(cp);
+ out.push(b !== undefined ? b : 0x3F);
+ }
+ }
+ return new Uint8Array(out);
+}
+
+function _loadFromBytes(bytes, cols, name) {
+ sparseMap = new Map();
+ canvasCols = cols;
+ parse(bytes, cols);
+ const { maxRow: mr } = getBounds(sparseMap);
+ canvasRows = Math.max(mr + 1, 5);
+ lastFile = null; lastBytes = bytes;
+ undoStack = []; redoStack = [];
+ drawAll();
+ showOutput();
+ document.getElementById('filename').textContent = name;
+ document.getElementById('s-name').textContent = name;
+ document.getElementById('s-size').textContent = fmt(bytes.length);
+ const hasColor = [...sparseMap.values()].some(c => c.fg !== 7 || c.bg >= 0);
+ document.getElementById('s-type').textContent = hasColor ? 'ANSI color' : 'plain';
+ const { maxRow, maxCol } = getBounds(sparseMap);
+ document.getElementById('s-dims').textContent = `${maxCol+1}×${maxRow+1}`;
+ document.getElementById('width-wrap').classList.add('visible');
+ tabs[activeTab].name = name;
+ renderTabs();
+ if (!editMode) toggleEdit();
+ curRow = 0; curCol = 0;
+ updateCursor();
+}
+
+async function newFromClipboard() {
+ // Read clipboard BEFORE hiding the menu so the user-gesture context is intact
+ let text = '';
+ try { text = await navigator.clipboard.readText(); } catch(e) {}
+ hideNewMenu();
+ if (!text.trim()) {
+ showNewAnsiDlg();
+ return;
+ }
+ const stripped = text.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
+ let maxLen = 0;
+ stripped.split('\n').forEach(l => maxLen = Math.max(maxLen, l.replace(/\r$/, '').length));
+ const cols = Math.min(Math.max(maxLen || 80, 40), 320);
+ _loadFromBytes(textToCP437Bytes(text), cols, 'clipboard.ans');
+}
+
+function showNewAnsiDlg() {
+ document.getElementById('overlay').classList.add('visible');
+ document.getElementById('new-ansi-dlg').classList.add('visible');
+ document.getElementById('na-text').focus();
+}
+function hideNewAnsiDlg() {
+ document.getElementById('overlay').classList.remove('visible');
+ document.getElementById('new-ansi-dlg').classList.remove('visible');
+}
+function createFromAnsiCode() {
+ const cols = parseInt(document.getElementById('na-cols').value, 10) || 80;
+ const text = document.getElementById('na-text').value;
+ hideNewAnsiDlg();
+ if (!text.trim()) return;
+ _loadFromBytes(textToCP437Bytes(text), cols, 'ansi.ans');
+}
+
// ── Center / font size ────────────────────────────────────────────────────────
function deleteRow() {
pushUndo();
@@ -1327,8 +1555,8 @@ function exportNFO() {
}
// ── Export ANS ────────────────────────────────────────────────────────────────
-function exportANS() {
- if (sparseMap.size === 0) return;
+function buildANSBytes() {
+ if (sparseMap.size === 0) return null;
const { maxRow } = getBounds(sparseMap);
const rowMaxCol = new Array(maxRow+1).fill(-1);
for (const k of sparseMap.keys()) {
@@ -1359,13 +1587,72 @@ function exportANS() {
}
if (r<maxRow) pushStr('\r\n');
}
+ return new Uint8Array(bytes);
+}
+
+function exportANS() {
+ const bytes = buildANSBytes();
+ if (!bytes) return;
const fname = lastFile ? lastFile.name.replace(/\.[^.]+$/,'') : 'untitled';
const a = document.createElement('a');
- a.href = URL.createObjectURL(new Blob([new Uint8Array(bytes)],{type:'application/octet-stream'}));
+ a.href = URL.createObjectURL(new Blob([bytes],{type:'application/octet-stream'}));
a.download = fname + '.ans';
a.click();
}
+async function openWithPicker() {
+ if (!window.showOpenFilePicker) {
+ document.getElementById('file-input').click();
+ return;
+ }
+ try {
+ const [handle] = await window.showOpenFilePicker({
+ types: [{ description: 'ANS / NFO / TXT', accept: { 'application/octet-stream': ['.nfo','.ans','.diz','.asc','.txt','.ansi'] } }],
+ multiple: false
+ });
+ lastFileHandle = handle;
+ processFile(await handle.getFile());
+ } catch(e) { /* avbröts */ }
+}
+
+function flashSaved() {
+ const el = document.getElementById('filename');
+ const orig = el.textContent;
+ el.textContent = '✓ SAVED';
+ el.style.color = 'var(--cyan)';
+ setTimeout(() => { el.textContent = orig; el.style.color = ''; }, 1500);
+}
+
+async function saveANS() {
+ const bytes = buildANSBytes();
+ if (!bytes) return;
+ if (lastFileHandle) {
+ try {
+ const w = await lastFileHandle.createWritable();
+ await w.write(bytes);
+ await w.close();
+ flashSaved();
+ return;
+ } catch(e) { /* tillstånd nekat, fortsätt till dialog */ }
+ }
+ if (window.showSaveFilePicker) {
+ try {
+ const fname = lastFile ? lastFile.name : 'untitled.ans';
+ const handle = await window.showSaveFilePicker({
+ suggestedName: fname,
+ types: [{ description: 'ANS file', accept: { 'application/octet-stream': ['.ans'] } }]
+ });
+ lastFileHandle = handle;
+ const w = await handle.createWritable();
+ await w.write(bytes);
+ await w.close();
+ flashSaved();
+ return;
+ } catch(e) { /* avbröts */ }
+ }
+ exportANS();
+}
+
// ── Image → ANSI ─────────────────────────────────────────────────────────────
let imgSrc = null;
@@ -1395,6 +1682,15 @@ document.getElementById('img-input').addEventListener('change', function () {
this.value = '';
});
+const ASCII_RAMP = " .:-=+*#%@";
+
+function imgAsciiToggle() {
+ const ascii = document.getElementById('img-ascii').checked;
+ const halfRow = document.getElementById('img-half-row');
+ halfRow.style.opacity = ascii ? '0.35' : '';
+ document.getElementById('img-half').disabled = ascii;
+}
+
function imgAutoRows() {
if (!imgSrc) return;
if (!document.getElementById('img-aspect').checked) return;
@@ -1420,7 +1716,8 @@ function doImgConvert() {
if (!imgSrc) return;
const cols = parseInt(document.getElementById('img-cols').value, 10) || 80;
const rows = parseInt(document.getElementById('img-rows').value, 10) || 40;
- const half = document.getElementById('img-half').checked;
+ const ascii = document.getElementById('img-ascii').checked;
+ const half = !ascii && document.getElementById('img-half').checked;
const dither = document.getElementById('img-dither').checked;
hideImgDlg();
@@ -1461,7 +1758,18 @@ function doImgConvert() {
for (let row=0; row<rows; row++) {
for (let col=0; col<cols; col++) {
- if (half) {
+ if (ascii) {
+ const [r,g,b] = getP(col, row);
+ const fg = nearest(r,g,b);
+ if (dither) {
+ const [pr,pg,pb]=palRGB[fg]; const er=r-pr,eg=g-pg,eb=b-pb;
+ addErr(col+1,row, er,eg,eb,7/16); addErr(col-1,row+1,er,eg,eb,3/16);
+ addErr(col, row+1,er,eg,eb,5/16); addErr(col+1,row+1,er,eg,eb,1/16);
+ }
+ const lum = (0.299*r + 0.587*g + 0.114*b) / 255;
+ const ch = ASCII_RAMP[Math.round(lum * (ASCII_RAMP.length - 1))];
+ if (ch !== ' ') sparseMap.set(row*100000+col, { ch, fg, bg:-1 });
+ } else if (half) {
const [r1,g1,b1] = getP(col, row*2);
const [r2,g2,b2] = getP(col, row*2+1);
const fg = nearest(r1,g1,b1);
@@ -1603,10 +1911,10 @@ document.getElementById('tabbar').addEventListener('click', e => {
const dz = document.getElementById('dropzone');
dz.addEventListener('dragover', e=>{e.preventDefault();dz.classList.add('over')});
dz.addEventListener('dragleave', ()=>dz.classList.remove('over'));
-dz.addEventListener('drop', e=>{e.preventDefault();dz.classList.remove('over');const f=e.dataTransfer.files[0];if(f)processFile(f)});
-document.getElementById('file-input').addEventListener('change', function(){if(this.files[0])processFile(this.files[0])});
+dz.addEventListener('drop', e=>{e.preventDefault();dz.classList.remove('over');const f=e.dataTransfer.files[0];if(f){lastFileHandle=null;processFile(f);}});
+document.getElementById('file-input').addEventListener('change', function(){if(this.files[0]){lastFileHandle=null;processFile(this.files[0]);}});
document.addEventListener('dragover', e=>e.preventDefault());
-document.addEventListener('drop', e=>{e.preventDefault();const f=e.dataTransfer.files[0];if(f)processFile(f)});
+document.addEventListener('drop', e=>{e.preventDefault();const f=e.dataTransfer.files[0];if(f){lastFileHandle=null;processFile(f);}});
loadSession();
</script>