// ==UserScript== // @name Image Paster // @namespace http://tampermonkey.net/ // @version 4.0 // @description Paste images to OWOT using quadrants with direct writeCharToXY at cursor position // @author soyim // @match https://www.ourworldoftext.com/* // @match https://ourworldoftext.com/* // @grant none // @run-at document-end // ==/UserScript== //paste at cursor //set dimensions (function() { 'use strict'; window.addEventListener('load', function() { console.log('OWOT Image Paster (Octant Direct) loading...'); setTimeout(initImagePaster, 1000); }); function initImagePaster() { // Octant character map - 8 triangular sectors per character // Sectors arranged: [1][2] // [3][4] // [5][6] // [7][8] const OCTANT_CHARS = [ ' ', // 00000000: None '๐œบจ', // 00000001: Sector 1 '๐œบซ', // 00000010: Sector 2 '๐Ÿฎ‚', // 00000011: Sectors 1,2 '๐œด€', // 00000100: Sector 3 'โ–˜', // 00000101: Sectors 1,3 '๐œด', // 00000110: Sectors 2,3 '๐œด‚', // 00000111: Sectors 1,2,3 '๐œดƒ', // 00001000: Sector 4 '๐œด„', // 00001001: Sectors 1,4 'โ–', // 00001010: Sectors 2,4 '๐œด…', // 00001011: Sectors 1,2,4 '๐œด†', // 00001100: Sectors 3,4 '๐œด‡', // 00001101: Sectors 1,3,4 '๐œดˆ', // 00001110: Sectors 2,3,4 'โ–€', // 00001111: Sectors 1,2,3,4 '๐œด‰', // 00010000: Sector 5 '๐œดŠ', // 00010001: Sectors 1,5 '๐œด‹', // 00010010: Sectors 2,5 '๐œดŒ', // 00010011: Sectors 1,2,5 '๐Ÿฏฆ', // 00010100: Sectors 3,5 '๐œด', // 00010101: Sectors 1,3,5 '๐œดŽ', // 00010110: Sectors 2,3,5 '๐œด', // 00010111: Sectors 1,2,3,5 '๐œด', // 00011000: Sectors 4,5 '๐œด‘', // 00011001: Sectors 1,4,5 '๐œด’', // 00011010: Sectors 2,4,5 '๐œด“', // 00011011: Sectors 1,2,4,5 '๐œด”', // 00011100: Sectors 3,4,5 '๐œด•', // 00011101: Sectors 1,3,4,5 '๐œด–', // 00011110: Sectors 2,3,4,5 '๐œด—', // 00011111: Sectors 1,2,3,4,5 '๐œด˜', // 00100000: Sector 6 '๐œด™', // 00100001: Sectors 1,6 '๐œดš', // 00100010: Sectors 2,6 '๐œด›', // 00100011: Sectors 1,2,6 '๐œดœ', // 00100100: Sectors 3,6 '๐œด', // 00100101: Sectors 1,3,6 '๐œดž', // 00100110: Sectors 2,3,6 '๐œดŸ', // 00100111: Sectors 1,2,3,6 '๐Ÿฏง', // 00101000: Sectors 4,6 '๐œด ', // 00101001: Sectors 1,4,6 '๐œดก', // 00101010: Sectors 2,4,6 '๐œดข', // 00101011: Sectors 1,2,4,6 '๐œดฃ', // 00101100: Sectors 3,4,6 '๐œดค', // 00101101: Sectors 1,3,4,6 '๐œดฅ', // 00101110: Sectors 2,3,4,6 '๐œดฆ', // 00101111: Sectors 1,2,3,4,6 '๐œดง', // 00110000: Sectors 5,6 '๐œดจ', // 00110001: Sectors 1,5,6 '๐œดฉ', // 00110010: Sectors 2,5,6 '๐œดช', // 00110011: Sectors 1,2,5,6 '๐œดซ', // 00110100: Sectors 3,5,6 '๐œดฌ', // 00110101: Sectors 1,3,5,6 '๐œดญ', // 00110110: Sectors 2,3,5,6 '๐œดฎ', // 00110111: Sectors 1,2,3,5,6 '๐œดฏ', // 00111000: Sectors 4,5,6 '๐œดฐ', // 00111001: Sectors 1,4,5,6 '๐œดฑ', // 00111010: Sectors 2,4,5,6 '๐œดฒ', // 00111011: Sectors 1,2,4,5,6 '๐œดณ', // 00111100: Sectors 3,4,5,6 '๐œดด', // 00111101: Sectors 1,3,4,5,6 '๐œดต', // 00111110: Sectors 2,3,4,5,6 '๐Ÿฎ…', // 00111111: Sectors 1,2,3,4,5,6 '๐œบฃ', // 01000000: Sector 7 '๐œดถ', // 01000001: Sectors 1,7 '๐œดท', // 01000010: Sectors 2,7 '๐œดธ', // 01000011: Sectors 1,2,7 '๐œดน', // 01000100: Sectors 3,7 '๐œดบ', // 01000101: Sectors 1,3,7 '๐œดป', // 01000110: Sectors 2,3,7 '๐œดผ', // 01000111: Sectors 1,2,3,7 '๐œดฝ', // 01001000: Sectors 4,7 '๐œดพ', // 01001001: Sectors 1,4,7 '๐œดฟ', // 01001010: Sectors 2,4,7 '๐œต€', // 01001011: Sectors 1,2,4,7 '๐œต', // 01001100: Sectors 3,4,7 '๐œต‚', // 01001101: Sectors 1,3,4,7 '๐œตƒ', // 01001110: Sectors 2,3,4,7 '๐œต„', // 01001111: Sectors 1,2,3,4,7 'โ––', // 01010000: Sectors 5,7 '๐œต…', // 01010001: Sectors 1,5,7 '๐œต†', // 01010010: Sectors 2,5,7 '๐œต‡', // 01010011: Sectors 1,2,5,7 '๐œตˆ', // 01010100: Sectors 3,5,7 'โ–Œ', // 01010101: Sectors 1,3,5,7 (left half) '๐œต‰', // 01010110: Sectors 2,3,5,7 '๐œตŠ', // 01010111: Sectors 1,2,3,5,7 '๐œต‹', // 01011000: Sectors 4,5,7 '๐œตŒ', // 01011001: Sectors 1,4,5,7 'โ–ž', // 01011010: Sectors 2,4,5,7 '๐œต', // 01011011: Sectors 1,2,4,5,7 '๐œตŽ', // 01011100: Sectors 3,4,5,7 '๐œต', // 01011101: Sectors 1,3,4,5,7 '๐œต', // 01011110: Sectors 2,3,4,5,7 'โ–›', // 01011111: Sectors 1,2,3,4,5,7 '๐œต‘', // 01100000: Sectors 6,7 '๐œต’', // 01100001: Sectors 1,6,7 '๐œต“', // 01100010: Sectors 2,6,7 '๐œต”', // 01100011: Sectors 1,2,6,7 '๐œต•', // 01100100: Sectors 3,6,7 '๐œต–', // 01100101: Sectors 1,3,6,7 '๐œต—', // 01100110: Sectors 2,3,6,7 '๐œต˜', // 01100111: Sectors 1,2,3,6,7 '๐œต™', // 01101000: Sectors 4,6,7 '๐œตš', // 01101001: Sectors 1,4,6,7 '๐œต›', // 01101010: Sectors 2,4,6,7 '๐œตœ', // 01101011: Sectors 1,2,4,6,7 '๐œต', // 01101100: Sectors 3,4,6,7 '๐œตž', // 01101101: Sectors 1,3,4,6,7 '๐œตŸ', // 01101110: Sectors 2,3,4,6,7 '๐œต ', // 01101111: Sectors 1,2,3,4,6,7 '๐œตก', // 01110000: Sectors 5,6,7 '๐œตข', // 01110001: Sectors 1,5,6,7 '๐œตฃ', // 01110010: Sectors 2,5,6,7 '๐œตค', // 01110011: Sectors 1,2,5,6,7 '๐œตฅ', // 01110100: Sectors 3,5,6,7 '๐œตฆ', // 01110101: Sectors 1,3,5,6,7 '๐œตง', // 01110110: Sectors 2,3,5,6,7 '๐œตจ', // 01110111: Sectors 1,2,3,5,6,7 '๐œตฉ', // 01111000: Sectors 4,5,6,7 '๐œตช', // 01111001: Sectors 1,4,5,6,7 '๐œตซ', // 01111010: Sectors 2,4,5,6,7 '๐œตฌ', // 01111011: Sectors 1,2,4,5,6,7 '๐œตญ', // 01111100: Sectors 3,4,5,6,7 '๐œตฎ', // 01111101: Sectors 1,3,4,5,6,7 '๐œตฏ', // 01111110: Sectors 2,3,4,5,6,7 '๐œตฐ', // 01111111: Sectors 1,2,3,4,5,6,7 '๐œบ ', // 10000000: Sector 8 '๐œตฑ', // 10000001: Sectors 1,8 '๐œตฒ', // 10000010: Sectors 2,8 '๐œตณ', // 10000011: Sectors 1,2,8 '๐œตด', // 10000100: Sectors 3,8 '๐œตต', // 10000101: Sectors 1,3,8 '๐œตถ', // 10000110: Sectors 2,3,8 '๐œตท', // 10000111: Sectors 1,2,3,8 '๐œตธ', // 10001000: Sectors 4,8 '๐œตน', // 10001001: Sectors 1,4,8 '๐œตบ', // 10001010: Sectors 2,4,8 '๐œตป', // 10001011: Sectors 1,2,4,8 '๐œตผ', // 10001100: Sectors 3,4,8 '๐œตฝ', // 10001101: Sectors 1,3,4,8 '๐œตพ', // 10001110: Sectors 2,3,4,8 '๐œตฟ', // 10001111: Sectors 1,2,3,4,8 '๐œถ€', // 10010000: Sectors 5,8 '๐œถ', // 10010001: Sectors 1,5,8 '๐œถ‚', // 10010010: Sectors 2,5,8 '๐œถƒ', // 10010011: Sectors 1,2,5,8 '๐œถ„', // 10010100: Sectors 3,5,8 '๐œถ…', // 10010101: Sectors 1,3,5,8 '๐œถ†', // 10010110: Sectors 2,3,5,8 '๐œถ‡', // 10010111: Sectors 1,2,3,5,8 '๐œถˆ', // 10011000: Sectors 4,5,8 '๐œถ‰', // 10011001: Sectors 1,4,5,8 '๐œถŠ', // 10011010: Sectors 2,4,5,8 '๐œถ‹', // 10011011: Sectors 1,2,4,5,8 '๐œถŒ', // 10011100: Sectors 3,4,5,8 '๐œถ', // 10011101: Sectors 1,3,4,5,8 '๐œถŽ', // 10011110: Sectors 2,3,4,5,8 '๐œถ', // 10011111: Sectors 1,2,3,4,5,8 'โ–—', // 10100000: Sectors 6,8 '๐œถ', // 10100001: Sectors 1,6,8 '๐œถ‘', // 10100010: Sectors 2,6,8 '๐œถ’', // 10100011: Sectors 1,2,6,8 '๐œถ“', // 10100100: Sectors 3,6,8 'โ–š', // 10100101: Sectors 1,3,6,8 '๐œถ”', // 10100110: Sectors 2,3,6,8 '๐œถ•', // 10100111: Sectors 1,2,3,6,8 '๐œถ–', // 10101000: Sectors 4,6,8 '๐œถ—', // 10101001: Sectors 1,4,6,8 'โ–', // 10101010: Sectors 2,4,6,8 (right half) '๐œถ˜', // 10101011: Sectors 1,2,4,6,8 '๐œถ™', // 10101100: Sectors 3,4,6,8 '๐œถš', // 10101101: Sectors 1,3,4,6,8 '๐œถ›', // 10101110: Sectors 2,3,4,6,8 'โ–œ', // 10101111: Sectors 1,2,3,4,6,8 '๐œถœ', // 10110000: Sectors 5,6,8 '๐œถ', // 10110001: Sectors 1,5,6,8 '๐œถž', // 10110010: Sectors 2,5,6,8 '๐œถŸ', // 10110011: Sectors 1,2,5,6,8 '๐œถ ', // 10110100: Sectors 3,5,6,8 '๐œถก', // 10110101: Sectors 1,3,5,6,8 '๐œถข', // 10110110: Sectors 2,3,5,6,8 '๐œถฃ', // 10110111: Sectors 1,2,3,5,6,8 '๐œถค', // 10111000: Sectors 4,5,6,8 '๐œถฅ', // 10111001: Sectors 1,4,5,6,8 '๐œถฆ', // 10111010: Sectors 2,4,5,6,8 '๐œถง', // 10111011: Sectors 1,2,4,5,6,8 '๐œถจ', // 10111100: Sectors 3,4,5,6,8 '๐œถฉ', // 10111101: Sectors 1,3,4,5,6,8 '๐œถช', // 10111110: Sectors 2,3,4,5,6,8 '๐œถซ', // 10111111: Sectors 1,2,3,4,5,6,8 'โ–‚', // 11000000: Sectors 7,8 '๐œถฌ', // 11000001: Sectors 1,7,8 '๐œถญ', // 11000010: Sectors 2,7,8 '๐œถฎ', // 11000011: Sectors 1,2,7,8 '๐œถฏ', // 11000100: Sectors 3,7,8 '๐œถฐ', // 11000101: Sectors 1,3,7,8 '๐œถฑ', // 11000110: Sectors 2,3,7,8 '๏ฟฝยฒ', // 11000111: Sectors 1,2,3,7,8 '๐œถณ', // 11001000: Sectors 4,7,8 '๐œถด', // 11001001: Sectors 1,4,7,8 '๐œถต', // 11001010: Sectors 2,4,7,8 '๐œถถ', // 11001011: Sectors 1,2,4,7,8 '๐œถท', // 11001100: Sectors 3,4,7,8 '๐œถธ', // 11001101: Sectors 1,3,4,7,8 '๐œถน', // 11001110: Sectors 2,3,4,7,8 '๐œถบ', // 11001111: Sectors 1,2,3,4,7,8 '๐œถป', // 11010000: Sectors 5,7,8 '๐œถผ', // 11010001: Sectors 1,5,7,8 '๐œถฝ', // 11010010: Sectors 2,5,7,8 '๐œถพ', // 11010011: Sectors 1,2,5,7,8 '๐œถฟ', // 11010100: Sectors 3,5,7,8 '๐œท€', // 11010101: Sectors 1,3,5,7,8 '๐œท', // 11010110: Sectors 2,3,5,7,8 '๐œท‚', // 11010111: Sectors 1,2,3,5,7,8 '๐œทƒ', // 11011000: Sectors 4,5,7,8 '๐œท„', // 11011001: Sectors 1,4,5,7,8 '๐œท…', // 11011010: Sectors 2,4,5,7,8 '๐œท†', // 11011011: Sectors 1,2,4,5,7,8 '๐œท‡', // 11011100: Sectors 3,4,5,7,8 '๐œทˆ', // 11011101: Sectors 1,3,4,5,7,8 '๐œท‰', // 11011110: Sectors 2,3,4,5,7,8 '๐œทŠ', // 11011111: Sectors 1,2,3,4,5,7,8 '๐œท‹', // 11100000: Sectors 6,7,8 '๐œทŒ', // 11100001: Sectors 1,6,7,8 '๐œท', // 11100010: Sectors 2,6,7,8 '๐œทŽ', // 11100011: Sectors 1,2,6,7,8 '๐œท', // 11100100: Sectors 3,6,7,8 '๐œท', // 11100101: Sectors 1,3,6,7,8 '๐œท‘', // 11100110: Sectors 2,3,6,7,8 '๐œท’', // 11100111: Sectors 1,2,3,6,7,8 '๐œท“', // 11101000: Sectors 4,6,7,8 '๐œท”', // 11101001: Sectors 1,4,6,7,8 '๐œท•', // 11101010: Sectors 2,4,6,7,8 '๐œท–', // 11101011: Sectors 1,2,4,6,7,8 '๐œท—', // 11101100: Sectors 3,4,6,7,8 '๐œท˜', // 11101101: Sectors 1,3,4,6,7,8 '๐œท™', // 11101110: Sectors 2,3,4,6,7,8 '๐œทš', // 11101111: Sectors 1,2,3,4,6,7,8 'โ–„', // 11110000: Sectors 5,6,7,8 (lower half) '๐œท›', // 11110001: Sectors 1,5,6,7,8 '๐œทœ', // 11110010: Sectors 2,5,6,7,8 '๐œท', // 11110011: Sectors 1,2,5,6,7,8 '๐œทž', // 11110100: Sectors 3,5,6,7,8 'โ–™', // 11110101: Sectors 1,3,5,6,7,8 '๐œทŸ', // 11110110: Sectors 2,3,5,6,7,8 '๐œท ', // 11110111: Sectors 1,2,3,5,6,7,8 '๐œทก', // 11111000: Sectors 4,5,6,7,8 '๐œทข', // 11111001: Sectors 1,4,5,6,7,8 'โ–Ÿ', // 11111010: Sectors 2,4,5,6,7,8 '๐œทฃ', // 11111011: Sectors 1,2,4,5,6,7,8 'โ–†', // 11111100: Sectors 3,4,5,6,7,8 '๐œทค', // 11111101: Sectors 1,3,4,5,6,7,8 '๐œทฅ', // 11111110: Sectors 2,3,4,5,6,7,8 'โ–ˆ' // 11111111: All sectors (full block) ]; // K-means clustering for 2-color reduction function kMeans2Colors(pixels) { if (pixels.length === 0) return [0, 0]; if (pixels.length === 1) return [pixels[0], pixels[0]]; let c1 = Math.min(...pixels); let c2 = Math.max(...pixels); for (let iter = 0; iter < 10; iter++) { let cluster1 = []; let cluster2 = []; for (let color of pixels) { const dist1 = colorDistance(color, c1); const dist2 = colorDistance(color, c2); if (dist1 < dist2) { cluster1.push(color); } else { cluster2.push(color); } } if (cluster1.length > 0) { c1 = averageColor(cluster1); } if (cluster2.length > 0) { c2 = averageColor(cluster2); } } return [c1, c2]; } function colorDistance(c1, c2) { const r1 = (c1 >> 16) & 0xFF; const g1 = (c1 >> 8) & 0xFF; const b1 = c1 & 0xFF; const r2 = (c2 >> 16) & 0xFF; const g2 = (c2 >> 8) & 0xFF; const b2 = c2 & 0xFF; return Math.sqrt((r1-r2)**2 + (g1-g2)**2 + (b1-b2)**2); } function averageColor(colors) { let r = 0, g = 0, b = 0; for (let color of colors) { r += (color >> 16) & 0xFF; g += (color >> 8) & 0xFF; b += color & 0xFF; } r = Math.round(r / colors.length); g = Math.round(g / colors.length); b = Math.round(b / colors.length); return (r << 16) | (g << 8) | b; } class OWOTImagePasterOctantDirect { constructor() { this.canvas = new OffscreenCanvas(0, 0); this.ctx = this.canvas.getContext('2d', { willReadFrequently: true }); this.maxWidth = 50; this.maxHeight = 50; this.maintainAspectRatio = true; this.currentImageData = null; this.isPasting = false; this.isSelectingRegion = false; this.regionStart = null; this.regionEnd = null; this.createUI(); this.setupPasteListener(); } createUI() { const existing = document.getElementById('owot-image-paster-ui'); if (existing) existing.remove(); const ui = document.createElement('div'); ui.id = 'owot-image-paster-ui'; ui.style.cssText = ` position: fixed; top: 20px; right: 20px; background: #2c3e50; color: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); z-index: 10000; font-family: Arial, sans-serif; width: 400px; max-height: 90vh; overflow-y: auto; `; ui.innerHTML = `

OWOT Octant Paster

โ„น๏ธ Uses octants (2x4 pixels) with K-means 2-color optimization - maximum resolution!
๐Ÿ“
Drop image here or click to select
Octants (2x4) - 2 colors per char
Ready to paste!
Or press Ctrl+V anywhere on page
`; document.body.appendChild(ui); const dropZone = document.getElementById('drop-zone'); const fileInput = document.getElementById('file-input'); const closeBtn = document.getElementById('owot-close-btn'); const aspectRatioToggle = document.getElementById('aspect-ratio-toggle'); const widthSlider = document.getElementById('width-slider'); const widthValue = document.getElementById('width-value'); const heightSlider = document.getElementById('height-slider'); const heightValue = document.getElementById('height-value'); const heightControl = document.getElementById('height-control'); const pasteBtn = document.getElementById('paste-btn'); const selectRegionBtn = document.getElementById('select-region-btn'); const reconvertBtn = document.getElementById('reconvert-btn'); closeBtn.onclick = () => ui.remove(); dropZone.onclick = () => fileInput.click(); aspectRatioToggle.onchange = (e) => { this.maintainAspectRatio = e.target.checked; if (this.maintainAspectRatio) { heightControl.style.opacity = '0.5'; heightControl.style.pointerEvents = 'none'; heightSlider.disabled = true; } else { heightControl.style.opacity = '1'; heightControl.style.pointerEvents = 'auto'; heightSlider.disabled = false; } }; dropZone.ondragover = (e) => { e.preventDefault(); dropZone.style.borderColor = '#2ecc71'; dropZone.style.background = '#2c3e50'; }; dropZone.ondragleave = () => { dropZone.style.borderColor = '#3498db'; dropZone.style.background = '#34495e'; }; dropZone.ondrop = (e) => { e.preventDefault(); dropZone.style.borderColor = '#3498db'; dropZone.style.background = '#34495e'; const file = e.dataTransfer.files[0]; if (file && file.type.startsWith('image/')) { this.handleFile(file); } }; fileInput.onchange = (e) => { const file = e.target.files[0]; if (file) this.handleFile(file); }; widthSlider.oninput = (e) => { this.maxWidth = parseInt(e.target.value); widthValue.textContent = this.maxWidth; }; heightSlider.oninput = (e) => { this.maxHeight = parseInt(e.target.value); heightValue.textContent = this.maxHeight; }; pasteBtn.onclick = () => { this.pasteImageAtCursor(); }; selectRegionBtn.onclick = () => { this.startRegionSelection(); }; reconvertBtn.onclick = () => { if (this.currentImageUrl) { this.updateStatus('Reconverting...', '#3498db'); this.processImage(this.currentImageUrl); } }; } updateStatus(message, color = '#95a5a6') { const status = document.getElementById('status'); if (status) { status.textContent = message; status.style.color = color; } } handleFile(file) { this.updateStatus('Processing image...', '#3498db'); const url = URL.createObjectURL(file); this.currentImageUrl = url; this.processImage(url); } setupPasteListener() { document.addEventListener('paste', (event) => { if (event.clipboardData) { const items = event.clipboardData.items; if (!items) return; for (let i = 0; i < items.length; i++) { if (items[i].type.indexOf('image') !== -1) { const file = items[i].getAsFile(); const url = URL.createObjectURL(file); this.currentImageUrl = url; this.updateStatus('Processing pasted image...', '#3498db'); this.processImage(url); event.preventDefault(); break; } } } }); } processImage(url) { const img = new Image(); img.onload = () => { // Octant is 2 pixels wide, 4 pixels tall let charWidth, charHeight; if (this.maintainAspectRatio) { const aspectRatio = img.height / img.width; charWidth = Math.min(this.maxWidth, Math.floor(img.width / 2)); charHeight = Math.floor(charWidth * aspectRatio / 2); } else { charWidth = this.maxWidth; charHeight = this.maxHeight; } const pixelWidth = charWidth * 2; const pixelHeight = charHeight * 4; this.canvas.width = pixelWidth; this.canvas.height = pixelHeight; this.ctx.drawImage(img, 0, 0, pixelWidth, pixelHeight); const imageData = this.ctx.getImageData(0, 0, pixelWidth, pixelHeight); this.currentImageData = { imageData: imageData, charWidth: charWidth, charHeight: charHeight, pixelWidth: pixelWidth, pixelHeight: pixelHeight }; this.updatePreview(imageData, charWidth, charHeight); }; img.src = url; } updatePreview(imageData, charWidth, charHeight) { const previewSection = document.getElementById('preview-section'); const previewBox = document.getElementById('preview-box'); const pasteBtn = document.getElementById('paste-btn'); const selectRegionBtn = document.getElementById('select-region-btn'); previewSection.style.display = 'block'; pasteBtn.style.display = 'block'; selectRegionBtn.style.display = 'block'; let previewText = ""; const data = imageData.data; for (let charY = 0; charY < charHeight; charY++) { for (let charX = 0; charX < charWidth; charX++) { const octantData = this.extractOctant(data, charX, charY, charWidth); const char = octantData.char; previewText += char; } previewText += "\n"; } previewBox.textContent = previewText; this.updateStatus(`โœ“ Ready to paste ${charWidth}ร—${charHeight} chars (${charWidth*2}ร—${charHeight*4}px)`, '#2ecc71'); } extractOctant(data, charX, charY, charWidth) { // Extract 2x4 pixel block (8 sectors arranged as triangles) // Sectors: [1][2] // [3][4] // [5][6] // [7][8] const pixels = []; const pixelColors = []; for (let py = 0; py < 4; py++) { for (let px = 0; px < 2; px++) { const x = charX * 2 + px; const y = charY * 4 + py; const idx = (y * charWidth * 2 + x) * 4; const r = data[idx] || 0; const g = data[idx + 1] || 0; const b = data[idx + 2] || 0; const a = data[idx + 3] || 0; const color = ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); pixels.push({ color, alpha: a, x: px, y: py }); if (a >= 128) { pixelColors.push(color); } } } // Handle fully transparent if (pixelColors.length === 0) { return { char: ' ', fgColor: 0, bgColor: 0 }; } // K-means to get 2 colors const [color1, color2] = kMeans2Colors(pixelColors); // Build octant pattern // Bit order (MSB to LSB): Sector 8, 7, 6, 5, 4, 3, 2, 1 let pattern = 0; for (let i = 0; i < 8; i++) { const pixel = pixels[i]; if (pixel.alpha >= 128) { const dist1 = colorDistance(pixel.color, color1); const dist2 = colorDistance(pixel.color, color2); if (dist1 < dist2) { pattern |= (1 << i); } } } // If all pixels are solid, use full block if (pixelColors.length === 8) { const allSameColor = pixelColors.every(c => colorDistance(c, color1) < 10); if (allSameColor || pattern === 0b11111111) { pattern = 0b11111111; } } const char = OCTANT_CHARS[pattern]; return { char: char, fgColor: color1, bgColor: color2 }; } pasteImageAtCursor() { if (!this.currentImageData || this.isPasting) return; if (typeof writeCharToXY !== 'function') { this.updateStatus('โœ— writeCharToXY not found!', '#e74c3c'); return; } if (typeof cursorCoords === 'undefined') { this.updateStatus('โœ— cursorCoords not found!', '#e74c3c'); return; } this.isPasting = true; this.updateStatus('Pasting image...', '#f39c12'); const { imageData, charWidth, charHeight } = this.currentImageData; const data = imageData.data; const startX = cursorCoords[0] * 16 + cursorCoords[2]; const startY = cursorCoords[1] * 8 + cursorCoords[3]; let charsPasted = 0; for (let charY = 0; charY < charHeight; charY++) { for (let charX = 0; charX < charWidth; charX++) { const octantData = this.extractOctant(data, charX, charY, charWidth); if (octantData.char === ' ') continue; const charPosX = startX + charX; const charPosY = startY + charY; writeCharToXY(octantData.char, octantData.fgColor, charPosX, charPosY, octantData.bgColor); charsPasted++; } } this.isPasting = false; this.updateStatus(`โœ“ Pasted ${charsPasted} octant characters!`, '#2ecc71'); } startRegionSelection() { if (!this.currentImageData) return; // Get current cursor position as starting point if (typeof cursorCoords === 'undefined') { this.updateStatus('โœ— cursorCoords not found!', '#e74c3c'); return; } const startX = cursorCoords[0] * 16 + cursorCoords[2]; const startY = cursorCoords[1] * 8 + cursorCoords[3]; this.isSelectingRegion = true; this.regionStart = { x: startX, y: startY }; this.regionEnd = null; this.updateStatus(`Start: (${startX}, ${startY}). Click end corner...`, '#9b59b6'); const selectRegionBtn = document.getElementById('select-region-btn'); selectRegionBtn.textContent = 'Cancel Selection'; selectRegionBtn.style.background = '#e74c3c'; selectRegionBtn.onclick = () => { this.cancelRegionSelection(); }; // Add click listener to canvas this.regionClickHandler = this.handleRegionClick.bind(this); document.addEventListener('click', this.regionClickHandler, true); } handleRegionClick(event) { if (!this.isSelectingRegion) return; event.preventDefault(); event.stopPropagation(); if (typeof cursorCoords === 'undefined') { this.updateStatus('โœ— cursorCoords not found!', '#e74c3c'); this.cancelRegionSelection(); return; } const clickX = cursorCoords[0] * 16 + cursorCoords[2]; const clickY = cursorCoords[1] * 8 + cursorCoords[3]; // Set end point and paste this.regionEnd = { x: clickX, y: clickY }; this.updateStatus(`End: (${clickX}, ${clickY}). Pasting...`, '#f39c12'); // Remove the click listener document.removeEventListener('click', this.regionClickHandler, true); // Paste to the selected region this.pasteImageToRegion(); } cancelRegionSelection() { this.isSelectingRegion = false; this.regionStart = null; this.regionEnd = null; if (this.regionClickHandler) { document.removeEventListener('click', this.regionClickHandler, true); this.regionClickHandler = null; } const selectRegionBtn = document.getElementById('select-region-btn'); if (selectRegionBtn) { selectRegionBtn.textContent = 'Select Region to Paste'; selectRegionBtn.style.background = '#9b59b6'; selectRegionBtn.onclick = () => { this.startRegionSelection(); }; } this.updateStatus('Selection cancelled', '#95a5a6'); } pasteImageToRegion() { if (!this.currentImageData || !this.regionStart || !this.regionEnd) return; if (typeof writeCharToXY !== 'function') { this.updateStatus('โœ— writeCharToXY not found!', '#e74c3c'); this.cancelRegionSelection(); return; } // Calculate region dimensions const minX = Math.min(this.regionStart.x, this.regionEnd.x); const maxX = Math.max(this.regionStart.x, this.regionEnd.x); const minY = Math.min(this.regionStart.y, this.regionEnd.y); const maxY = Math.max(this.regionStart.y, this.regionEnd.y); const regionWidth = maxX - minX + 1; const regionHeight = maxY - minY + 1; // Process image to fit the region const img = new Image(); img.onload = () => { const pixelWidth = regionWidth * 2; const pixelHeight = regionHeight * 4; this.canvas.width = pixelWidth; this.canvas.height = pixelHeight; this.ctx.drawImage(img, 0, 0, pixelWidth, pixelHeight); const imageData = this.ctx.getImageData(0, 0, pixelWidth, pixelHeight); const data = imageData.data; let charsPasted = 0; for (let charY = 0; charY < regionHeight; charY++) { for (let charX = 0; charX < regionWidth; charX++) { const octantData = this.extractOctant(data, charX, charY, regionWidth); if (octantData.char === ' ') continue; const charPosX = minX + charX; const charPosY = minY + charY; writeCharToXY(octantData.char, octantData.fgColor, charPosX, charPosY, octantData.bgColor); charsPasted++; } } this.updateStatus(`โœ“ Pasted ${charsPasted} chars to region (${regionWidth}ร—${regionHeight})!`, '#2ecc71'); this.cancelRegionSelection(); }; img.src = this.currentImageUrl; } } // Initialize const imagePaster = new OWOTImagePasterOctantDirect(); console.log("โœ“ OWOT Image Paster (Octant Direct) loaded!"); } })();