Librería Auxiliar OSINT (Sidebar). QBit Bridge, OpenCV Safe, YouTube/eBay Scraper.
Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/570442/1779451/Drawaria%20OSINT%20Sidebar%20Companion.js
// ==UserScript==
// @name Drawaria OSINT Sidebar Companion
// @namespace drawaria.osint.sidebar
// @version 8.1.4
// @description Sidebar 100% Nativo (Zero Dependencies). DOM Hijacker, Light Theme, API & DuckDuckGo.
// @author YouTubeDrawaria
// @match https://drawaria.online/profile/*
// @match https://drawaria.online/avatar/cache/*
// @match https://*.drawaria.online/profile/*
// @match https://*.drawaria.online/avatar/cache/*
// @match file:///*/drawaria.online/*.html
// @icon https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect bing.com
// @connect www.bing.com
// @connect yandex.com
// @connect lens.google.com
// @connect google.com
// @connect duckduckgo.com
// @connect downloads.khinsider.com
// @connect vgmtreasurechest.com
// @connect www.myinstants.com
// @connect wikipedia.org
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect drawaria.online
// @connect wikipedia.org
// @connect duckduckgo.com
// @connect wikipedia.org
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// Evitar inyecciones múltiples
if (document.getElementById('osint-sidebar-wrapper')) return;
// =========================================================================
// 1. QBIT BRIDGE (Capa de Compatibilidad y Tema Claro Nativo)
// =========================================================================
class DOMCreate {
Element() { return document.createElement.apply(document, arguments); }
TextNode() { return document.createTextNode.apply(document, arguments); }
Tree(type, attrs, childrenArray =[]) {
const el = this.Element(type);
childrenArray.forEach(child => {
if (typeof child === "string") el.appendChild(this.TextNode(child));
else if (child) el.appendChild(child);
});
for (const attr in attrs) {
if (attr === "className" || attr === "class") el.className = attrs[attr];
else el.setAttribute(attr, attrs[attr]);
}
el.appendAll = function (...nodes) { nodes.forEach((n) => el.appendChild(n)); };
return el;
}
Button(content) {
let btn = this.Tree("button", { class: "qbit-btn" });
btn.innerHTML = content;
return btn;
}
Row() { return this.Tree("div", { class: "qbit-row" }); }
}
globalThis.domMake = new DOMCreate();
globalThis.generate = { uuidv4: () => crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2) };
class Stylizer {
constructor() {
this.element = document.createElement("style");
document.head ? document.head.appendChild(this.element) : document.addEventListener("DOMContentLoaded", () => document.head.appendChild(this.element));
}
addRules(rules =[]) { rules.forEach(rule => this.element.appendChild(document.createTextNode(rule))); }
}
class QBit {
static Styles = new Stylizer();
static register(ext) { return QBit; }
static bind(ext, target) { return QBit; }
constructor(name, icon) {
this.name = name;
this.icon = icon;
this.htmlElements = {};
this.#onStartupBase();
}
#onStartupBase() {
this.htmlElements.details = domMake.Tree("details", { open: true, class: "qbit-details" });
this.htmlElements.summary = domMake.Tree("summary", {},[this.icon + " " + this.name]);
this.htmlElements.section = domMake.Tree("section", { class: "qbit-section" });
this.htmlElements.details.appendAll(this.htmlElements.summary, this.htmlElements.section);
}
notify(level, msg) { console.log(`[${this.name}]`, msg); }
}
globalThis.QBit = QBit;
// CSS GLOBAL DEL SIDEBAR LIGHT THEME (ANCLADO A LA IZQUIERDA)
QBit.Styles.addRules([
`#osint-sidebar-wrapper { position: fixed; top: 0; left: 0; height: 100vh; display: flex; z-index: 999999; transform: translateX(0); transition: transform 0.3s ease-in-out; }`,
`#osint-sidebar-wrapper.collapsed { transform: translateX(-350px); }`,
`#osint-sidebar-container { width: 350px; height: 100%; overflow-y: auto; overflow-x: hidden; background: #ffffff; border-right: 1px solid #dee2e6; padding: 15px; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; box-shadow: 4px 0 15px rgba(0,0,0,0.1); color: #212529; }`,
`#osint-sidebar-container::-webkit-scrollbar { width: 6px; }`,
`#osint-sidebar-container::-webkit-scrollbar-thumb { background: #007bff; border-radius: 3px; }`,
`#osint-sidebar-toggle { width: 25px; height: 60px; background: #f8f9fa; color: #007bff; border: 1px solid #dee2e6; border-left: none; display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 0 8px 8px 0; align-self: center; box-shadow: 3px 0 5px rgba(0,0,0,0.1); font-size: 14px; user-select: none; transition: 0.2s; font-weight: bold; }`,
`#osint-sidebar-toggle:hover { background: #e2e6ea; }`,
/* Estilos Internos QBit (White Theme) */
`.qbit-details { background: #ffffff; border: 1px solid #dee2e6; border-radius: 5px; margin-bottom: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }`,
`.qbit-details summary { background: #f8f9fa; color: #007bff; padding: 8px 10px; font-weight: bold; cursor: pointer; border-radius: 4px 4px 0 0; font-size: 0.9em; user-select: none; border-bottom: 1px solid #dee2e6; }`,
`.qbit-section { padding: 10px; font-size: 0.85em; display: flex; flex-direction: column; gap: 8px; color: #212529; }`,
`.qbit-btn { background: #f8f9fa; color: #212529; border: 1px solid #dee2e6; padding: 6px; border-radius: 4px; cursor: pointer; font-weight: 600; transition: 0.2s; width: 100%; box-sizing: border-box; text-align: center; display: inline-block; text-decoration: none; }`,
`.qbit-btn:hover { background: #e2e6ea; }`,
`.qbit-row { display: flex; gap: 5px; width: 100%; flex-wrap: wrap; justify-content: center; align-items: center; }`,
/* Tarjetas de Búsqueda */
`.osint-card { background: #ffffff; border: 1px solid #dee2e6; border-left: 3px solid #007bff; padding: 8px; border-radius: 4px; margin-bottom: 5px; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }`,
`.osint-card a { color: #007bff; text-decoration: none; font-weight: bold; display: block; margin-bottom: 4px; font-size: 1em; }`,
/* Utilidades para inputs */
`.osint-input-sm { width: 48%; border: 1px solid #dee2e6; padding: 4px; border-radius: 3px; font-size: 0.9em; box-sizing: border-box; background: #ffffff; color: #212529; }`,
`.icon-list { display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; margin-bottom: 10px; }`,
`.icon-list label.icon { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border: 1px solid #dee2e6; border-radius: 4px; background: #f8f9fa; cursor: pointer; color: #495057; font-size: 1.2em; }`,
`.icon-list input:checked + label.icon { background: #007bff; color: white; border-color: #0056b3; }`
]);
// =========================================================================
// 2. EXTRACCIÓN DE IMAGEN Y DETECCIÓN DE ENTORNO (Reg, Unreg, Local)
// =========================================================================
function autoLoadImageContext() {
const href = window.location.href;
const isLocal = href.startsWith('file:');
const isUnregistered = href.includes('/avatar/cache/');
const isRegistered = href.includes('/profile/');
const fetchAndDispatchBase64 = (url) => {
GM_xmlhttpRequest({
method: "GET", url: url, responseType: "blob",
onload: (res) => {
const reader = new FileReader();
reader.onloadend = () => window.dispatchEvent(new CustomEvent('osint-image-loaded', { detail: { base64: reader.result, url: url } }));
reader.readAsDataURL(res.response);
}
});
};
if (isLocal) {
// Entorno Local: Hacer hook al input file inyectado por el Script 2
const hookLocalFile = setInterval(() => {
const fileInput = document.getElementById('file-selector');
if (fileInput && !fileInput.dataset.hijacked) {
fileInput.dataset.hijacked = 'true'; // Marcar para evitar hooks duplicados
fileInput.addEventListener('change', (e) => {
if (e.target.files && e.target.files[0]) {
const reader = new FileReader();
reader.onloadend = () => {
window.dispatchEvent(new CustomEvent('osint-image-loaded', { detail: { base64: reader.result, url: 'local_file' } }));
};
reader.readAsDataURL(e.target.files[0]);
}
});
clearInterval(hookLocalFile);
}
}, 500);
} else if (isUnregistered) {
// Entorno Unregistered: La URL limpia es la imagen en sí misma
fetchAndDispatchBase64(href.split('?')[0]);
} else if (isRegistered) {
// Entorno Registered: Esperar a que el Script 2 o el juego carguen la imagen en el DOM
let retries = 0;
const findImg = setInterval(() => {
const img = document.querySelector('img[src*="/avatar/"]');
if (img && img.src) {
clearInterval(findImg);
fetchAndDispatchBase64(new URL(img.src, window.location.origin).href);
}
if (++retries > 20) clearInterval(findImg);
}, 500);
}
}
// =========================================================================
// 3. MÓDULOS DE PROPIEDADES DE IMAGEN (Ligeros y Nativos)
// =========================================================================
class ImageProperties extends QBit {
constructor() {
super("Image properties", '📏');
this.resultsDisplay = domMake.Tree("pre", { style: "white-space: pre-wrap; margin: 0; color: #212529; font-family: monospace;" },["Esperando imagen..."]);
this.htmlElements.section.appendChild(this.resultsDisplay);
window.addEventListener('osint-image-loaded', (e) => this.calculateProperties(e.detail.base64));
}
calculateProperties(base64) {
const img = new Image();
img.onload = () => {
const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
const divisor = gcd(img.width, img.height);
const aspectRatio = `${img.width / divisor}:${img.height / divisor}`;
const sizeInBytes = Math.floor((base64.length * (3/4)) - (base64.indexOf('=') > 0 ? (base64.length - base64.indexOf('=')) : 0));
const sizeInKb = (sizeInBytes / 1024).toFixed(2);
this.resultsDisplay.textContent =
`Dimensiones : ${img.width} x ${img.height} px\n` +
`Aspect Ratio : ${aspectRatio}\n` +
`Peso Estimado : ${sizeInKb} KB\n` +
`Formato : ${base64.substring(5, base64.indexOf(';'))}`;
};
img.src = base64;
}
}
class ReverseSearchHub extends QBit {
constructor() {
super("Reverse Search", '🌐');
this.container = domMake.Tree("div", { class: "qbit-row" }, ["Esperando imagen..."]);
this.htmlElements.section.appendChild(this.container);
window.addEventListener('osint-image-loaded', (e) => this.renderButtons(e.detail.url));
}
renderButtons(url) {
this.container.innerHTML = "";
if (!url || !url.startsWith("http")) {
this.container.innerHTML = "<span style='color: #dc3545;'>Se requiere una URL HTTP válida.</span>";
return;
}
const encUrl = encodeURIComponent(url);
const createLink = (name, link) => {
const a = document.createElement("a");
a.href = link;
a.target = "_blank";
a.className = "qbit-btn";
a.textContent = name;
this.container.appendChild(a);
};
createLink("Google Lens", `https://lens.google.com/uploadbyurl?url=${encUrl}`);
createLink("Yandex", `https://yandex.com/images/search?rpt=imageview&url=${encUrl}`);
createLink("Bing", `https://www.bing.com/images/searchbyimage?cbir=sbi&imgurl=${encUrl}`);
}
}
// =========================================================================
// 3. MÓDULOS DE PROPIEDADES DE IMAGEN (RESTAURADOS Y COMPLETOS)
// =========================================================================
class ImageAnalyzer extends QBit {
constructor() {
super("Image Analyzer", '📊');
this.lastAnalysisText = "";
this.#setupUI();
window.addEventListener('osint-image-loaded', (e) => this.analyzeImage(e.detail.base64));
}
#setupUI() {
const section = this.htmlElements.section;
// Contenedor visual de colores (Swatches)
this.swatchContainer = domMake.Tree("div", {
style: "display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; justify-content: center; padding: 5px; border: 1px solid #dee2e6; border-radius: 4px; background: #f8f9fa;"
});
// Pantalla de resultados técnicos
this.resultsDisplay = domMake.Tree("pre", {
style: "white-space: pre-wrap; margin: 0; color: #212529; font-family: monospace; font-size: 0.85em; background: #fff; padding: 8px; border: 1px solid #dee2e6; border-radius: 4px;"
}, ["Esperando imagen..."]);
// Botones de Exportación
const actionRow = domMake.Row();
actionRow.style.marginTop = "10px";
const btnCopy = domMake.Button('📋 Copiar Resultados');
btnCopy.onclick = () => {
if(!this.lastAnalysisText) return;
navigator.clipboard.writeText(this.lastAnalysisText);
this.notify("info", "Resultados copiados al portapapeles.");
};
const btnDownload = domMake.Button('💾 Descargar TXT');
btnDownload.onclick = () => {
if(!this.lastAnalysisText) return;
const blob = new Blob([this.lastAnalysisText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `osint_analysis_${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
};
actionRow.appendAll(btnCopy, btnDownload);
section.appendAll(
domMake.Tree("div", { style: "font-weight: bold; font-size: 0.85em; margin-bottom: 4px;" }, ["🎨 Colores Dominantes:"]),
this.swatchContainer,
this.resultsDisplay,
actionRow
);
}
analyzeImage(base64) {
this.resultsDisplay.textContent = "Analizando...";
this.swatchContainer.innerHTML = "";
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
let colorMap = {};
let totalBrightness = 0;
let validPixels = 0;
// Análisis completo de píxeles
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
if (a > 20) { // Omitir pixeles transparentes
// Color Hex
const hex = `#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0')}`.toUpperCase();
colorMap[hex] = (colorMap[hex] || 0) + 1;
// Brillo Avanzado (Luminancia Percibida)
totalBrightness += (0.299 * r + 0.587 * g + 0.114 * b);
validPixels++;
}
}
// Obtener Top Colores
const sortedColors = Object.entries(colorMap)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
const avgBright = validPixels > 0 ? (totalBrightness / validPixels).toFixed(2) : 0;
const brightPercent = ((avgBright / 255) * 100).toFixed(1);
// Generar Swatches (Bloques de colores)
sortedColors.forEach(([hex, count]) => {
const swatch = domMake.Tree("div", {
title: `Color: ${hex} (Click para copiar HEX)`,
style: `width: 25px; height: 25px; background: ${hex}; border: 1px solid #dee2e6; border-radius: 3px; cursor: pointer;`
});
swatch.onclick = () => {
navigator.clipboard.writeText(hex);
this.notify("info", `Copiado: ${hex}`);
};
this.swatchContainer.appendChild(swatch);
});
// Preparar texto para exportación y visualización
this.lastAnalysisText =
`--- REPORTE DE ANÁLISIS OSINT ---\n` +
`Dimensiones: ${img.width}x${img.height}\n` +
`Píxeles Sólidos: ${validPixels}\n` +
`Brillo Avanzado: ${avgBright}/255 (${brightPercent}%)\n` +
`Clasificación: ${avgBright > 127 ? 'Clara' : 'Oscura'}\n\n` +
`PALETA DETECTADA:\n` +
sortedColors.map((c, i) => `${i+1}. ${c[0]} (${((c[1]/validPixels)*100).toFixed(1)}%)`).join('\n');
this.resultsDisplay.textContent =
`✨ Brillo: ${brightPercent}% (${avgBright})\n` +
`📏 Tamaño: ${img.width}x${img.height} px\n` +
`✅ Píxeles: ${validPixels}\n` +
`🎨 Colores: ${Object.keys(colorMap).length} únicos`;
};
img.src = base64;
}
}
class ImageToolsFull extends QBit {
constructor() {
super("Image Editor Pro Elite", '🛠️');
this.injectStyles();
this.interactiveCanvas = document.createElement('canvas');
this.interactiveCanvas.style.cssText = `
max-width: 100%;
height: auto;
display: block;
margin: 8px auto;
border: 1px dashed #adb5bd;
border-radius: 4px;
cursor: crosshair;
background: repeating-conic-gradient(#f8f9fa 0% 25%, #e9ecef 0% 50%) 50% / 15px 15px;
box-shadow: 0 4px 8px rgba(0,0,0,0.05);
transition: all 0.3s ease;
`;
this.originalWidth = 0;
this.originalHeight = 0;
this.originalImageBase64OnLoad = null;
this.currentImageBase64 = null;
this.history = [];
this.currentHistoryIndex = -1;
this.isProcessing = false;
this.loadInterface();
window.addEventListener('osint-image-loaded', (e) => this.loadImageBase64(e.detail.base64));
}
injectStyles() {
if (document.getElementById('iet-custom-styles')) return;
const style = document.createElement('style');
style.id = 'iet-custom-styles';
style.textContent = `
.iet-tab-container { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 10px; background: #e9ecef; padding: 4px; border-radius: 6px; }
.iet-tab { flex: 1 1 auto; text-align: center; padding: 6px 8px; background: transparent; border: none; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; font-size: 11px; font-weight: 600; color: #495057; display: flex; align-items: center; justify-content: center; gap: 4px; }
.iet-tab:hover { background: #dee2e6; color: #212529; }
.iet-tab.active { background: #fff; color: #0d6efd; box-shadow: 0 1px 4px rgba(0,0,0,0.08); }
.iet-panel { display: none; padding: 8px; background: #fff; border: 1px solid #dee2e6; border-radius: 6px; margin-bottom: 8px; box-shadow: 0 2px 6px rgba(0,0,0,0.03); }
.iet-panel.active { display: block; animation: fadeIn 0.3s ease; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: translateY(0); } }
.iet-btn { padding: 6px 10px; border: none; border-radius: 4px; font-weight: 600; cursor: pointer; transition: all 0.2s; color: white; background: #0d6efd; font-size: 11px; display: inline-flex; align-items: center; justify-content: center; gap: 4px; margin: 2px 0; box-shadow: 0 1px 3px rgba(13,110,253,0.15); width: 100%; box-sizing: border-box; }
.iet-btn:hover { filter: brightness(1.1); transform: translateY(-1px); box-shadow: 0 2px 5px rgba(13,110,253,0.25); }
.iet-btn:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(13,110,253,0.15); }
.iet-btn-danger { background: #dc3545; box-shadow: 0 1px 3px rgba(220,53,69,0.15); }
.iet-btn-danger:hover { box-shadow: 0 2px 5px rgba(220,53,69,0.25); }
.iet-btn-success { background: #28a745; box-shadow: 0 1px 3px rgba(40,167,69,0.15); }
.iet-btn-success:hover { box-shadow: 0 2px 5px rgba(40,167,69,0.25); }
.iet-btn-warning { background: #ffc107; color: #212529; box-shadow: 0 1px 3px rgba(255,193,7,0.15); }
.iet-btn-warning:hover { box-shadow: 0 2px 5px rgba(255,193,7,0.25); }
.iet-btn-secondary { background: #6c757d; box-shadow: 0 1px 3px rgba(108,117,125,0.15); }
.iet-btn-secondary:hover { box-shadow: 0 2px 5px rgba(108,117,125,0.25); }
.iet-input-group { margin-bottom: 6px; display: flex; flex-direction: column; gap: 2px; width: 100%; }
.iet-input-group label { font-size: 10px; font-weight: 600; color: #495057; display: flex; align-items: center; gap: 2px; }
.iet-slider { width: 100%; accent-color: #0d6efd; cursor: pointer; height: 10px; margin: 4px 0; }
.iet-input-text { padding: 4px 6px; border: 1px solid #ced4da; border-radius: 4px; font-size: 11px; outline: none; transition: border-color 0.2s; box-sizing: border-box; width: 100%; }
.iet-input-text:focus { border-color: #0d6efd; box-shadow: 0 0 0 2px rgba(13,110,253,0.2); }
.iet-section-title { font-size: 12px; font-weight: bold; color: #212529; margin-bottom: 6px; padding-bottom: 4px; border-bottom: 1px solid #e9ecef; }
.iet-tool-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; }
.iet-tool-card { background: #f8f9fa; padding: 6px; border-radius: 6px; border: 1px solid #e9ecef; }
.iet-select { padding: 4px 6px; border: 1px solid #ced4da; border-radius: 4px; font-size: 11px; outline: none; width: 100%; background: white; cursor: pointer; margin-bottom: 4px; }
`;
document.head.appendChild(style);
}
createStyledBtn(text, icon, variantClass = '') {
return domMake.Tree("button", { class: `iet-btn ${variantClass}` }, [icon + " " + text]);
}
createControlGroup(labelText, inputElement, buttonElement) {
const group = domMake.Tree("div", { class: "iet-input-group" });
const label = domMake.Tree("label", {}, [labelText]);
group.appendAll(label, inputElement);
if (buttonElement) group.appendChild(buttonElement);
return group;
}
loadImageBase64(base64) {
this.currentImageBase64 = base64;
this.originalImageBase64OnLoad = base64;
this.history = [base64];
this.currentHistoryIndex = 0;
const img = new Image();
img.onload = () => {
this.originalWidth = img.naturalWidth;
this.originalHeight = img.naturalHeight;
this.interactiveCanvas.width = img.naturalWidth;
this.interactiveCanvas.height = img.naturalHeight;
this.interactiveCanvas.getContext('2d').drawImage(img, 0, 0);
this.notify("success", "Imagen cargada en el editor interactivo.");
};
img.src = base64;
}
async loadInterface() {
const section = this.htmlElements.section;
const tabsContainer = domMake.Tree("div", { class: "iet-tab-container" });
const panelsContainer = domMake.Tree("div");
const categories = [
{ id: "cat-geo", title: "Geometría", icon: "📐", build: this.buildGeometryUI.bind(this) },
{ id: "cat-adj", title: "Ajustes", icon: "☀️", build: this.buildAdjustmentsUI.bind(this) },
{ id: "cat-fil", title: "Filtros y Marcos", icon: "🎨", build: this.buildFiltersUI.bind(this) },
{ id: "cat-adv", title: "Efectos", icon: "✨", build: this.buildAdvancedUI.bind(this) }
];
categories.forEach((cat, index) => {
const tabBtn = domMake.Tree("button", { class: `iet-tab ${index === 0 ? 'active' : ''}` }, [cat.icon + " " + cat.title]);
const panel = domMake.Tree("div", { id: cat.id, class: `iet-panel ${index === 0 ? 'active' : ''}` });
panel.appendChild(cat.build());
tabBtn.addEventListener("click", () => {
document.querySelectorAll(".iet-tab").forEach(t => t.classList.remove("active"));
document.querySelectorAll(".iet-panel").forEach(p => p.classList.remove("active"));
tabBtn.classList.add("active");
panel.classList.add("active");
});
tabsContainer.appendChild(tabBtn);
panelsContainer.appendChild(panel);
});
section.appendChild(tabsContainer);
section.appendChild(panelsContainer);
section.appendChild(this.interactiveCanvas);
const bottomRow = domMake.Tree("div", { style: "display: flex; gap: 6px; margin-top: 10px;" });
const btnReset = this.createStyledBtn("Orig.", "🔙", "iet-btn-danger");
btnReset.style.flex = "1";
btnReset.addEventListener("click", () => {
if(this.originalImageBase64OnLoad) this.loadImageBase64(this.originalImageBase64OnLoad);
});
const btnUndo = this.createStyledBtn("Deshacer", "↩️", "iet-btn-warning");
btnUndo.style.flex = "1";
btnUndo.addEventListener("click", () => this.undo());
const btnRedo = this.createStyledBtn("Rehacer", "↪️", "iet-btn-secondary");
btnRedo.style.flex = "1";
btnRedo.addEventListener("click", () => this.redo());
const btnDownload = this.createStyledBtn("Guardar", "💾", "iet-btn-success");
btnDownload.style.flex = "1";
btnDownload.addEventListener("click", () => this.exportImage());
bottomRow.appendAll(btnReset, btnUndo, btnRedo, btnDownload);
section.appendChild(bottomRow);
}
undo() {
if (this.currentHistoryIndex > 0) {
this.currentHistoryIndex--;
this.currentImageBase64 = this.history[this.currentHistoryIndex];
this.redrawCanvas();
} else {
this.notify("warning", "No hay más acciones para deshacer.");
}
}
redo() {
if (this.currentHistoryIndex < this.history.length - 1) {
this.currentHistoryIndex++;
this.currentImageBase64 = this.history[this.currentHistoryIndex];
this.redrawCanvas();
} else {
this.notify("warning", "No hay acciones para rehacer.");
}
}
redrawCanvas() {
if (!this.currentImageBase64) return;
const img = new Image();
img.onload = () => {
this.originalWidth = img.naturalWidth;
this.originalHeight = img.naturalHeight;
this.interactiveCanvas.width = this.originalWidth;
this.interactiveCanvas.height = this.originalHeight;
this.interactiveCanvas.getContext('2d').drawImage(img, 0, 0);
};
img.src = this.currentImageBase64;
}
exportImage() {
if (!this.currentImageBase64) return;
const link = document.createElement("a");
link.href = this.interactiveCanvas.toDataURL("image/png");
link.download = `osint_edited_${Date.now()}.png`;
link.click();
}
async processImageTransform(transformFunction) {
if (!this.currentImageBase64 || this.isProcessing) return;
try {
this.isProcessing = true;
const img = new Image();
img.src = this.currentImageBase64;
await new Promise(r => img.onload = r);
const canvas = document.createElement('canvas');
canvas.width = this.originalWidth;
canvas.height = this.originalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
this.history = this.history.slice(0, this.currentHistoryIndex + 1);
await transformFunction(ctx, canvas.width, canvas.height, img);
this.originalWidth = canvas.width;
this.originalHeight = canvas.height;
this.currentImageBase64 = canvas.toDataURL('image/png');
this.history.push(this.currentImageBase64);
this.currentHistoryIndex++;
this.redrawCanvas();
} catch (error) {
console.error(error);
this.notify("error", "Error procesando imagen.");
} finally {
this.isProcessing = false;
}
}
applyConvolution(ctx, w, h, matrix, factor = 1, bias = 0) {
let imgData = ctx.getImageData(0, 0, w, h);
let src = new Uint8ClampedArray(imgData.data);
let data = imgData.data;
let side = Math.round(Math.sqrt(matrix.length));
let half = Math.floor(side / 2);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let r = 0, g = 0, b = 0;
for (let cy = 0; cy < side; cy++) {
for (let cx = 0; cx < side; cx++) {
let scy = y + cy - half;
let scx = x + cx - half;
if (scy >= 0 && scy < h && scx >= 0 && scx < w) {
let srcOff = (scy * w + scx) * 4;
let wt = matrix[cy * side + cx];
r += src[srcOff] * wt;
g += src[srcOff + 1] * wt;
b += src[srcOff + 2] * wt;
}
}
}
let off = (y * w + x) * 4;
data[off] = factor * r + bias;
data[off + 1] = factor * g + bias;
data[off + 2] = factor * b + bias;
}
}
ctx.putImageData(imgData, 0, 0);
}
// ================= PANELES DE HERRAMIENTAS =================
buildGeometryUI() {
const grid = domMake.Tree("div", { class: "iet-tool-grid" });
// Redimensionar
const cardResize = domMake.Tree("div", { class: "iet-tool-card" });
cardResize.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["↕️ Escalar"]));
const scaleInp = domMake.Tree("input", { type: "number", value: 100, class: "iet-input-text" });
const btnResize = this.createStyledBtn("Aplicar", "📏");
btnResize.addEventListener("click", () => {
let s = parseFloat(scaleInp.value) / 100;
this.processImageTransform((ctx, w, h, img) => {
let nW = Math.max(1, Math.round(w * s));
let nH = Math.max(1, Math.round(h * s));
ctx.canvas.width = nW; ctx.canvas.height = nH;
ctx.drawImage(img, 0, 0, nW, nH);
});
});
cardResize.appendChild(this.createControlGroup("Porcentaje (%)", scaleInp, btnResize));
// Recortar
const cardCrop = domMake.Tree("div", { class: "iet-tool-card" });
cardCrop.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["✂️ Recortar"]));
const cropInp = domMake.Tree("input", { type: "range", min: 1, max: 40, value: 10, class: "iet-slider" });
const btnCrop = this.createStyledBtn("Recortar", "🗡️");
btnCrop.addEventListener("click", () => {
let margin = parseFloat(cropInp.value) / 100;
this.processImageTransform((ctx, w, h, img) => {
let cX = Math.round(w * margin);
let cY = Math.round(h * margin);
let nW = w - (cX * 2);
let nH = h - (cY * 2);
if (nW < 10 || nH < 10) return;
ctx.canvas.width = nW; ctx.canvas.height = nH;
ctx.drawImage(img, cX, cY, nW, nH, 0, 0, nW, nH);
});
});
cardCrop.appendChild(this.createControlGroup("Margen", cropInp, btnCrop));
// Voltear / Rotar Rápido
const cardRotate = domMake.Tree("div", { class: "iet-tool-card" });
cardRotate.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["🔄 Orientación Rápida"]));
const btnH = this.createStyledBtn("Flip Horiz.", "↔️");
const btnRot = this.createStyledBtn("Rotar 90°", "↻");
btnH.addEventListener("click", () => this.processImageTransform((ctx, w, h, img) => {
ctx.translate(w, 0); ctx.scale(-1, 1); ctx.drawImage(img, 0, 0);
}));
btnRot.addEventListener("click", () => this.processImageTransform((ctx, w, h, img) => {
ctx.canvas.width = h; ctx.canvas.height = w;
ctx.translate(h/2, w/2); ctx.rotate(Math.PI/2); ctx.drawImage(img, -w/2, -h/2);
}));
cardRotate.appendAll(btnH, btnRot);
// Rotación Libre 360 (NUEVO)
const cardFreeRot = domMake.Tree("div", { class: "iet-tool-card" });
cardFreeRot.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["🎡 Rotación Libre"]));
const degInp = domMake.Tree("input", { type: "range", min: 0, max: 360, value: 0, class: "iet-slider" });
const btnFreeRot = this.createStyledBtn("Aplicar Rotación", "⚙️");
btnFreeRot.addEventListener("click", () => this.processImageTransform((ctx, w, h, img) => {
let deg = parseInt(degInp.value);
let rad = deg * Math.PI / 180;
// Recalcular el bounding box para que no se corte la imagen
let newW = Math.abs(w * Math.cos(rad)) + Math.abs(h * Math.sin(rad));
let newH = Math.abs(w * Math.sin(rad)) + Math.abs(h * Math.cos(rad));
ctx.canvas.width = newW; ctx.canvas.height = newH;
ctx.translate(newW / 2, newH / 2);
ctx.rotate(rad);
ctx.drawImage(img, -w / 2, -h / 2);
}));
cardFreeRot.appendChild(this.createControlGroup("Grados (0 - 360)", degInp, btnFreeRot));
grid.appendAll(cardResize, cardCrop, cardRotate, cardFreeRot);
return grid;
}
buildAdjustmentsUI() {
const grid = domMake.Tree("div", { class: "iet-tool-grid" });
// Brillo / Contraste
const cardBC = domMake.Tree("div", { class: "iet-tool-card" });
cardBC.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["☀️ Luz"]));
const bInp = domMake.Tree("input", { type: "range", min: -100, max: 100, value: 0, class: "iet-slider" });
const cInp = domMake.Tree("input", { type: "range", min: -100, max: 100, value: 0, class: "iet-slider" });
const btnBC = this.createStyledBtn("Aplicar Luz", "✨");
btnBC.addEventListener("click", () => this.processImageTransform((ctx, w, h) => {
let imgData = ctx.getImageData(0,0,w,h); let data = imgData.data;
let b = parseFloat(bInp.value)/100, c = (parseFloat(cInp.value)+100)/100;
let intercept = 128 * (1 - c);
for(let i=0; i<data.length; i+=4){
for(let j=0; j<3; j++) {
let val = data[i+j] + (b * 255);
data[i+j] = Math.max(0, Math.min(255, (val * c) + intercept));
}
}
ctx.putImageData(imgData, 0, 0);
}));
cardBC.appendAll(this.createControlGroup("Brillo", bInp), this.createControlGroup("Contraste", cInp, btnBC));
// Saturación
const cardSat = domMake.Tree("div", { class: "iet-tool-card" });
cardSat.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["🎨 Saturación"]));
const sInp = domMake.Tree("input", { type: "range", min: 0, max: 200, value: 150, class: "iet-slider" });
const btnSat = this.createStyledBtn("Aplicar Color", "🌈");
btnSat.addEventListener("click", () => this.processImageTransform((ctx, w, h) => {
let imgData = ctx.getImageData(0,0,w,h); let data = imgData.data; let s = parseFloat(sInp.value)/100;
for(let i=0; i<data.length; i+=4){
let gray = 0.299*data[i] + 0.587*data[i+1] + 0.114*data[i+2];
data[i] = Math.min(255, Math.max(0, gray + (data[i] - gray)*s));
data[i+1] = Math.min(255, Math.max(0, gray + (data[i+1] - gray)*s));
data[i+2] = Math.min(255, Math.max(0, gray + (data[i+2] - gray)*s));
}
ctx.putImageData(imgData, 0, 0);
}));
cardSat.appendChild(this.createControlGroup("Nivel", sInp, btnSat));
// Border Radius (NUEVO)
const cardBorder = domMake.Tree("div", { class: "iet-tool-card" });
cardBorder.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["🔲 Bordes Redondeados"]));
const radInp = domMake.Tree("input", { type: "range", min: 0, max: 50, value: 10, class: "iet-slider" });
const btnRadius = this.createStyledBtn("Aplicar Radius", "⭕");
btnRadius.addEventListener("click", () => this.processImageTransform((ctx, w, h, img) => {
let percentage = parseFloat(radInp.value) / 100;
let radius = percentage * Math.min(w, h); // Max radio = 50% = Círculo perfecto si es cuadrado
ctx.clearRect(0, 0, w, h);
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(0, 0, w, h, radius);
} else {
// Fallback para navegadores antiguos
ctx.moveTo(radius, 0); ctx.lineTo(w - radius, 0); ctx.quadraticCurveTo(w, 0, w, radius);
ctx.lineTo(w, h - radius); ctx.quadraticCurveTo(w, h, w - radius, h);
ctx.lineTo(radius, h); ctx.quadraticCurveTo(0, h, 0, h - radius);
ctx.lineTo(0, radius); ctx.quadraticCurveTo(0, 0, radius, 0);
}
ctx.clip();
ctx.drawImage(img, 0, 0, w, h);
}));
cardBorder.appendChild(this.createControlGroup("Curvatura (%)", radInp, btnRadius));
// Binarizar e Invertir
const cardBin = domMake.Tree("div", { class: "iet-tool-card" });
cardBin.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["🔀 Otros"]));
const tInp = domMake.Tree("input", { type: "range", min: 0, max: 255, value: 128, class: "iet-slider" });
const btnThresh = this.createStyledBtn("Umbral", "🦓");
btnThresh.addEventListener("click", () => this.processImageTransform((ctx, w, h) => {
let imgData = ctx.getImageData(0,0,w,h); let data = imgData.data; let th = parseInt(tInp.value);
for(let i=0; i<data.length; i+=4){
let v = (0.299*data[i] + 0.587*data[i+1] + 0.114*data[i+2]) >= th ? 255 : 0;
data[i] = data[i+1] = data[i+2] = v;
}
ctx.putImageData(imgData, 0, 0);
}));
const btnInv = this.createStyledBtn("Invertir", "💡");
btnInv.addEventListener("click", () => this.processImageTransform((ctx, w, h) => {
let imgData = ctx.getImageData(0,0,w,h); let data = imgData.data;
for(let i=0; i<data.length; i+=4){ data[i]=255-data[i]; data[i+1]=255-data[i+1]; data[i+2]=255-data[i+2]; }
ctx.putImageData(imgData, 0, 0);
}));
cardBin.appendAll(this.createControlGroup("Binarizar", tInp, btnThresh), btnInv);
grid.appendAll(cardBC, cardSat, cardBorder, cardBin);
return grid;
}
buildFiltersUI() {
const grid = domMake.Tree("div", { class: "iet-tool-grid" });
// Estilos Clásicos (Grises, Sepia)
const cardStyle = domMake.Tree("div", { class: "iet-tool-card" });
cardStyle.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["🎞️ Clásicos"]));
const btnGray = this.createStyledBtn("B&N", "🌚");
btnGray.addEventListener("click", () => this.processImageTransform((ctx, w, h) => {
let imgData = ctx.getImageData(0,0,w,h); let data = imgData.data;
for(let i=0; i<data.length; i+=4){
let avg = data[i]*0.299 + data[i+1]*0.587 + data[i+2]*0.114;
data[i] = data[i+1] = data[i+2] = avg;
}
ctx.putImageData(imgData, 0, 0);
}));
const btnSepia = this.createStyledBtn("Sepia", "📜");
btnSepia.addEventListener("click", () => this.processImageTransform((ctx, w, h) => {
let imgData = ctx.getImageData(0,0,w,h); let data = imgData.data;
for(let i=0; i<data.length; i+=4){
let r = data[i], g = data[i+1], b = data[i+2];
data[i] = Math.min(255, (r * 0.393) + (g * 0.769) + (b * 0.189));
data[i+1] = Math.min(255, (r * 0.349) + (g * 0.686) + (b * 0.168));
data[i+2] = Math.min(255, (r * 0.272) + (g * 0.534) + (b * 0.131));
}
ctx.putImageData(imgData, 0, 0);
}));
cardStyle.appendAll(btnGray, btnSepia);
// Overlays y Efectos (NUEVO - 10 Opciones)
const cardOverlay = domMake.Tree("div", { class: "iet-tool-card" });
cardOverlay.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["🌌 Filtros Ópticos"]));
const selOverlay = domMake.Tree("select", { class: "iet-select" });
["Scanlines", "Matrix", "Nieve", "Fuego", "Agua", "Light Leak", "Ruido TV", "Sepia Antiguo", "Viñeta Profunda", "Arcoíris"].forEach((o, i) => {
selOverlay.appendChild(domMake.Tree("option", { value: i }, [o]));
});
const btnOverlay = this.createStyledBtn("Aplicar Filtro", "🔮");
btnOverlay.addEventListener("click", () => this.processImageTransform((ctx, w, h, img) => {
let type = parseInt(selOverlay.value);
ctx.globalCompositeOperation = "source-over";
if (type === 0) { // Scanlines
ctx.fillStyle = "rgba(0,0,0,0.3)";
for(let y=0; y<h; y+=4) ctx.fillRect(0, y, w, 2);
} else if (type === 1) { // Matrix
ctx.fillStyle = "rgba(0, 255, 0, 0.15)";
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = "rgba(0, 255, 0, 0.6)";
for(let i=0; i<100; i++) ctx.fillRect(Math.random()*w, Math.random()*h, 2, Math.random()*50);
} else if (type === 2) { // Nieve
ctx.fillStyle = "rgba(255,255,255,0.7)";
for(let i=0; i<(w*h)/1000; i++) {
ctx.beginPath(); ctx.arc(Math.random()*w, Math.random()*h, Math.random()*3+1, 0, Math.PI*2); ctx.fill();
}
} else if (type === 3) { // Fuego
let grad = ctx.createLinearGradient(0, h, 0, h/2);
grad.addColorStop(0, "rgba(255, 69, 0, 0.6)"); grad.addColorStop(1, "rgba(255, 215, 0, 0)");
ctx.fillStyle = grad; ctx.globalCompositeOperation = "color-dodge"; ctx.fillRect(0, 0, w, h);
} else if (type === 4) { // Agua
ctx.fillStyle = "rgba(0, 105, 148, 0.3)";
ctx.globalCompositeOperation = "overlay"; ctx.fillRect(0, 0, w, h);
} else if (type === 5) { // Light Leak
let grad = ctx.createRadialGradient(0, 0, 0, 0, 0, w/1.5);
grad.addColorStop(0, "rgba(255, 100, 50, 0.6)"); grad.addColorStop(1, "rgba(255, 100, 50, 0)");
ctx.fillStyle = grad; ctx.globalCompositeOperation = "screen"; ctx.fillRect(0, 0, w, h);
} else if (type === 6) { // Ruido TV
let imgData = ctx.getImageData(0,0,w,h); let d = imgData.data;
for(let i=0; i<d.length; i+=4) { let val = (Math.random()-0.5)*50; d[i]+=val; d[i+1]+=val; d[i+2]+=val; }
ctx.putImageData(imgData, 0, 0);
} else if (type === 7) { // Sepia Antiguo
ctx.fillStyle = "rgba(112, 66, 20, 0.3)"; ctx.globalCompositeOperation = "color"; ctx.fillRect(0,0,w,h);
ctx.strokeStyle = "rgba(0,0,0,0.2)"; ctx.lineWidth = 1; ctx.globalCompositeOperation = "source-over";
for(let i=0; i<20; i++) { ctx.beginPath(); ctx.moveTo(Math.random()*w, 0); ctx.lineTo(Math.random()*w, h); ctx.stroke(); }
} else if (type === 8) { // Viñeta Profunda
let grad = ctx.createRadialGradient(w/2, h/2, w/4, w/2, h/2, w/1.2);
grad.addColorStop(0, "rgba(0,0,0,0)"); grad.addColorStop(1, "rgba(0,0,0,0.9)");
ctx.fillStyle = grad; ctx.fillRect(0, 0, w, h);
} else if (type === 9) { // Arcoíris
let grad = ctx.createLinearGradient(0, 0, w, h);
grad.addColorStop(0,"rgba(255,0,0,0.2)"); grad.addColorStop(0.33,"rgba(0,255,0,0.2)"); grad.addColorStop(0.66,"rgba(0,0,255,0.2)"); grad.addColorStop(1,"rgba(255,0,255,0.2)");
ctx.fillStyle = grad; ctx.globalCompositeOperation = "color"; ctx.fillRect(0, 0, w, h);
}
ctx.globalCompositeOperation = "source-over"; // Reset
}));
cardOverlay.appendAll(selOverlay, btnOverlay);
// Marcos (NUEVO - 10 Opciones)
const cardFrame = domMake.Tree("div", { class: "iet-tool-card" });
cardFrame.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["🖼️ Marcos Generativos"]));
const selFrame = domMake.Tree("select", { class: "iet-select" });
["Polaroid", "Cine", "Tecnológico", "Medieval", "Oro", "Neón", "Grunge", "Naturaleza", "Cómic", "Sello Postal"].forEach((o, i) => {
selFrame.appendChild(domMake.Tree("option", { value: i }, [o]));
});
const btnFrame = this.createStyledBtn("Aplicar Marco", "🎨");
btnFrame.addEventListener("click", () => this.processImageTransform((ctx, w, h, img) => {
let type = parseInt(selFrame.value);
let minDim = Math.min(w, h);
ctx.globalCompositeOperation = "source-over";
if (type === 0) { // Polaroid
let bw = minDim * 0.05, bh = minDim * 0.15;
let nW = w + bw*2, nH = h + bw + bh;
ctx.canvas.width = nW; ctx.canvas.height = nH;
ctx.fillStyle = "white"; ctx.fillRect(0, 0, nW, nH);
ctx.shadowColor = "rgba(0,0,0,0.3)"; ctx.shadowBlur = 10;
ctx.drawImage(img, bw, bw, w, h); ctx.shadowBlur = 0; // Reset
} else if (type === 1) { // Cine
let bar = h * 0.12;
ctx.fillStyle = "black"; ctx.fillRect(0, 0, w, bar); ctx.fillRect(0, h - bar, w, bar);
} else if (type === 2) { // Tecnológico
ctx.strokeStyle = "#00ffff"; ctx.lineWidth = minDim * 0.02;
ctx.strokeRect(w*0.05, h*0.05, w*0.9, h*0.9);
ctx.lineWidth = minDim * 0.05;
ctx.beginPath(); ctx.moveTo(w*0.05, h*0.1); ctx.lineTo(w*0.05, h*0.05); ctx.lineTo(w*0.1, h*0.05); ctx.stroke();
ctx.beginPath(); ctx.moveTo(w*0.95, h*0.1); ctx.lineTo(w*0.95, h*0.05); ctx.lineTo(w*0.9, h*0.05); ctx.stroke();
ctx.beginPath(); ctx.moveTo(w*0.05, h*0.9); ctx.lineTo(w*0.05, h*0.95); ctx.lineTo(w*0.1, h*0.95); ctx.stroke();
ctx.beginPath(); ctx.moveTo(w*0.95, h*0.9); ctx.lineTo(w*0.95, h*0.95); ctx.lineTo(w*0.9, h*0.95); ctx.stroke();
} else if (type === 3) { // Medieval
ctx.strokeStyle = "#3e2723"; ctx.lineWidth = minDim * 0.06; ctx.strokeRect(0, 0, w, h);
ctx.strokeStyle = "#ffb300"; ctx.lineWidth = minDim * 0.01; ctx.strokeRect(minDim*0.03, minDim*0.03, w - minDim*0.06, h - minDim*0.06);
} else if (type === 4) { // Oro
let grad = ctx.createLinearGradient(0, 0, w, h);
grad.addColorStop(0, "#bf953f"); grad.addColorStop(0.5, "#fcf6ba"); grad.addColorStop(1, "#b38728");
ctx.strokeStyle = grad; ctx.lineWidth = minDim * 0.05; ctx.strokeRect(0, 0, w, h);
} else if (type === 5) { // Neón
ctx.strokeStyle = "#ff00ff"; ctx.lineWidth = minDim * 0.03;
ctx.shadowColor = "#ff00ff"; ctx.shadowBlur = 20;
ctx.strokeRect(minDim*0.05, minDim*0.05, w - minDim*0.1, h - minDim*0.1); ctx.shadowBlur = 0;
} else if (type === 6) { // Grunge
ctx.fillStyle = "black";
for(let i=0; i<w; i+=10) { ctx.fillRect(i, 0, 10, Math.random()*minDim*0.05); ctx.fillRect(i, h - Math.random()*minDim*0.05, 10, minDim*0.05); }
for(let i=0; i<h; i+=10) { ctx.fillRect(0, i, Math.random()*minDim*0.05, 10); ctx.fillRect(w - Math.random()*minDim*0.05, i, minDim*0.05, 10); }
} else if (type === 7) { // Naturaleza
ctx.strokeStyle = "#2e7d32"; ctx.lineWidth = minDim * 0.04; ctx.strokeRect(0, 0, w, h);
ctx.fillStyle = "#4caf50";
ctx.beginPath(); ctx.ellipse(minDim*0.05, minDim*0.05, minDim*0.06, minDim*0.03, Math.PI/4, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(w - minDim*0.05, h - minDim*0.05, minDim*0.06, minDim*0.03, Math.PI/4, 0, Math.PI*2); ctx.fill();
} else if (type === 8) { // Cómic
ctx.strokeStyle = "black"; ctx.lineWidth = minDim * 0.05; ctx.strokeRect(0, 0, w, h);
ctx.strokeStyle = "white"; ctx.lineWidth = minDim * 0.01; ctx.strokeRect(minDim*0.025, minDim*0.025, w - minDim*0.05, h - minDim*0.05);
} else if (type === 9) { // Sello Postal
let b = minDim * 0.06;
ctx.canvas.width = w + b*2; ctx.canvas.height = h + b*2;
ctx.fillStyle = "white"; ctx.fillRect(0, 0, w + b*2, h + b*2);
ctx.drawImage(img, b, b, w, h);
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "black";
for(let i=0; i<w+b*2; i+=b*1.5) { ctx.beginPath(); ctx.arc(i, 0, b*0.4, 0, Math.PI*2); ctx.arc(i, h+b*2, b*0.4, 0, Math.PI*2); ctx.fill(); }
for(let i=0; i<h+b*2; i+=b*1.5) { ctx.beginPath(); ctx.arc(0, i, b*0.4, 0, Math.PI*2); ctx.arc(w+b*2, i, b*0.4, 0, Math.PI*2); ctx.fill(); }
}
}));
cardFrame.appendAll(selFrame, btnFrame);
grid.appendAll(cardStyle, cardOverlay, cardFrame);
return grid;
}
buildAdvancedUI() {
const grid = domMake.Tree("div", { class: "iet-tool-grid" });
// Tono (Hue) - (RESTAURADO)
const cardHue = domMake.Tree("div", { class: "iet-tool-card" });
cardHue.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["🌈 Ajuste de Tono (HUE)"]));
const hueInp = domMake.Tree("input", { type: "range", min: 0, max: 360, value: 0, class: "iet-slider" });
const btnHue = this.createStyledBtn("Girar Colores", "🔄");
btnHue.addEventListener("click", () => this.processImageTransform((ctx, w, h) => {
let imgData = ctx.getImageData(0,0,w,h); let data = imgData.data;
let hueShift = parseInt(hueInp.value);
for(let i=0; i<data.length; i+=4){
let hsl = rgbToHsl(data[i], data[i+1], data[i+2]);
hsl[0] = (hsl[0] + hueShift) % 360;
let rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
data[i] = rgb[0]; data[i+1] = rgb[1]; data[i+2] = rgb[2];
}
ctx.putImageData(imgData, 0, 0);
}));
cardHue.appendChild(this.createControlGroup("Grados HUE (0-360)", hueInp, btnHue));
// Blur & Pixelate
const cardObscure = domMake.Tree("div", { class: "iet-tool-card" });
cardObscure.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["💧 Ocultar"]));
const blurInp = domMake.Tree("input", { type: "range", min: 0, max: 10, value: 2, class: "iet-slider" });
const btnBlur = this.createStyledBtn("Blur", "🌫️");
btnBlur.addEventListener("click", () => this.processImageTransform((ctx, w, h) => {
let r = parseInt(blurInp.value); if(r===0) return;
let imgData = ctx.getImageData(0,0,w,h); let px = imgData.data; let out = new Uint8ClampedArray(px.length);
for(let y=0; y<h; y++){
for(let x=0; x<w; x++){
let rs=0, gs=0, bs=0, count=0;
for(let dy=-r; dy<=r; dy++){
for(let dx=-r; dx<=r; dx++){
let nx=x+dx, ny=y+dy;
if(nx>=0 && nx<w && ny>=0 && ny<h){
let idx=(ny*w+nx)*4; rs+=px[idx]; gs+=px[idx+1]; bs+=px[idx+2]; count++;
}
}
}
let i=(y*w+x)*4; out[i]=rs/count; out[i+1]=gs/count; out[i+2]=bs/count; out[i+3]=px[i+3];
}
}
imgData.data.set(out); ctx.putImageData(imgData, 0, 0);
}));
const pxInp = domMake.Tree("input", { type: "range", min: 2, max: 50, value: 10, class: "iet-slider" });
const btnPx = this.createStyledBtn("Pixelar", "👾");
btnPx.addEventListener("click", () => this.processImageTransform((ctx, w, h, img) => {
let size = parseInt(pxInp.value);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(img, 0, 0, w/size, h/size);
ctx.drawImage(ctx.canvas, 0, 0, w/size, h/size, 0, 0, w, h);
}));
cardObscure.appendAll(this.createControlGroup("Fuerza Blur", blurInp, btnBlur), this.createControlGroup("Bloque Pixel", pxInp, btnPx));
// Convoluciones (Nitidez, Bordes)
const cardConv = domMake.Tree("div", { class: "iet-tool-card" });
cardConv.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["👁️ Detalles Exactos"]));
const btnSharp = this.createStyledBtn("Nitidez", "🔪");
btnSharp.addEventListener("click", () => this.processImageTransform((ctx, w, h) => {
const matrix = [ 0, -1, 0, -1, 5, -1, 0, -1, 0 ];
this.applyConvolution(ctx, w, h, matrix);
}));
const btnEdge = this.createStyledBtn("Bordes", "🖊️");
btnEdge.addEventListener("click", () => this.processImageTransform((ctx, w, h) => {
const matrix = [ -1, -1, -1, -1, 8, -1, -1, -1, -1 ];
this.applyConvolution(ctx, w, h, matrix);
}));
cardConv.appendAll(btnSharp, btnEdge);
// Ruido
const cardNoise = domMake.Tree("div", { class: "iet-tool-card" });
cardNoise.appendChild(domMake.Tree("div", { class: "iet-section-title" }, ["🔊 Ruido"]));
const nSlider = domMake.Tree("input", { type: "range", min: 0, max: 100, value: 30, class: "iet-slider" });
const btnNoise = this.createStyledBtn("Suavizar", "🧹");
btnNoise.addEventListener("click", () => this.processImageTransform((ctx, w, h) => {
let imgData = ctx.getImageData(0,0,w,h); let data = imgData.data;
const intensity = parseInt(nSlider.value)/100;
for(let i=0; i<data.length; i+=4){
const avg = (data[i] + data[i+1] + data[i+2])/3;
data[i] = data[i+1] = data[i+2] = avg * (1-intensity) + data[i] * intensity;
}
ctx.putImageData(imgData, 0, 0);
}));
cardNoise.appendChild(this.createControlGroup("Fuerza", nSlider, btnNoise));
grid.appendAll(cardHue, cardObscure, cardConv, cardNoise);
return grid;
}
}
// ================= FUNCIONES DE COLOR GLOBALES =================
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) { h = s = 0; } else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h * 360, s, l];
}
function hslToRgb(h, s, l) {
let r, g, b;
if (s === 0) { r = g = b = l; } else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h / 360 + 1 / 3);
g = hue2rgb(p, q, h / 360);
b = hue2rgb(p, q, h / 360 - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
// ================= HELPERS =================
function hexToRgb(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return [r, g, b];
}
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h * 360, s, l];
}
function hslToRgb(h, s, l) {
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h / 360 + 1 / 3);
g = hue2rgb(p, q, h / 360);
b = hue2rgb(p, q, h / 360 - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
// ================= HELPERS =================
function hexToRgb(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return [r, g, b];
}
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h * 360, s, l];
}
function hslToRgb(h, s, l) {
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h / 360 + 1 / 3);
g = hue2rgb(p, q, h / 360);
b = hue2rgb(p, q, h / 360 - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
// =========================================================================
// 4. GRUPO B: NUEVOS MÓDULOS DE BÚSQUEDA (API & HTML ESTÁTICO)
// =========================================================================
class WikipediaLore extends QBit {
constructor() {
super("Wikipedia Lore Finder", '📚');
this.container = domMake.Tree("div");
this.htmlElements.section.appendChild(this.container);
this.htmlElements.details.open = true;
}
search(keyword) {
this.container.innerHTML = `<div style="text-align:center; padding: 10px; color:#007bff;">Buscando "${keyword}" en Wikipedia...</div>`;
// API Oficial de Wikipedia (JSON)
GM_xmlhttpRequest({
method: "GET",
url: `https://en.wikipedia.org/w/api.php?action=query&format=json&list=search&srsearch=${encodeURIComponent(keyword)}&utf8=1&origin=*`,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
const results = data.query.search;
this.container.innerHTML = "";
let count = 0;
results.forEach(item => {
if (count >= 3) return;
let title = item.title;
let snippet = item.snippet + "...";
let link = `https://en.wikipedia.org/?curid=${item.pageid}`;
let card = domMake.Tree("div", { class: "osint-card" });
card.innerHTML = `<a href="${link}" target="_blank" style="font-size:0.9em;">${title}</a>
<div style="font-size:0.8em; color:#495057; margin-top:4px;">${snippet}</div>`;
this.container.appendChild(card);
count++;
});
if(count === 0) this.container.innerHTML = "<div style='color:#6c757d; text-align:center;'>Sin resultados en Wikipedia.</div>";
} catch (e) {
this.container.innerHTML = `<div style="color:#dc3545; padding:5px;">Error API Wiki: ${e.message}</div>`;
}
},
onerror: (e) => {
this.container.innerHTML = `<div style="color:#dc3545; padding:5px;">Error Red Wiki: ${e.statusText}</div>`;
}
});
}
}
class DuckDuckGoPivot extends QBit {
constructor() {
super("Best Results", '🔎');
this.container = domMake.Tree("div");
this.htmlElements.section.appendChild(this.container);
this.htmlElements.details.open = true;
}
search(keyword) {
this.container.innerHTML = `<div style="text-align:center; padding: 10px; color:#007bff;">Buscando "${keyword}" en DDG...</div>`;
// Versión HTML Estática de DuckDuckGo (Sin JS)
GM_xmlhttpRequest({
method: "GET",
url: `https://duckduckgo.com/html/?q=${encodeURIComponent(keyword)}`,
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
},
onload: (res) => {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(res.responseText, "text/html");
const items = doc.querySelectorAll('.result');
this.container.innerHTML = "";
let count = 0;
items.forEach(item => {
if (count >= 5) return;
const a = item.querySelector('.result__a');
if (!a) return;
let title = a.textContent || "";
let rawLink = a.getAttribute('href') || "";
// Limpiar redirecciones de DDG
let link = rawLink;
if (link.includes('uddg=')) {
try { link = decodeURIComponent(link.split('uddg=')[1].split('&')[0]); } catch(e){}
} else if (!link.startsWith('http')) {
link = 'https://duckduckgo.com' + link;
}
let card = domMake.Tree("div", { class: "osint-card" });
card.innerHTML = `<a href="${link}" target="_blank" style="font-size:0.9em; word-break: break-all;">${title}</a>`;
this.container.appendChild(card);
count++;
});
if(count === 0) this.container.innerHTML = "<div style='color:#6c757d; text-align:center;'>Sin resultados en DuckDuckGo.</div>";
} catch (e) {
this.container.innerHTML = `<div style="color:#dc3545; padding:5px;">Error Scraping DDG: ${e.message}</div>`;
}
},
onerror: (e) => {
this.container.innerHTML = `<div style="color:#dc3545; padding:5px;">Error Red DDG: ${e.statusText}</div>`;
}
});
}
}
// =========================================================================
// 5. DOM HIJACKER (Sincronización Silenciosa)
// =========================================================================
function getCleanText(btn) {
const clone = btn.cloneNode(true);
const span = clone.querySelector('span');
if (span) span.remove();
return clone.textContent.trim();
}
function initDOMHijacker(wikiModule, ddgModule) {
setInterval(() => {
const picker = document.getElementById('identity-picker-ui');
if (!picker) return;
const buttons = picker.querySelectorAll('button:not([data-osint-hijacked])');
buttons.forEach(btn => {
btn.setAttribute('data-osint-hijacked', 'true');
btn.addEventListener('click', () => {
const cleanText = getCleanText(btn);
if (cleanText && cleanText.length > 0 && !cleanText.includes('Scanning...')) {
console.log(`[DOM Hijacker] Capturada keyword: "${cleanText}"`);
wikiModule.search(cleanText);
ddgModule.search(cleanText);
}
});
});
}, 1000);
}
// =========================================================================
// 6. ENSAMBLAJE FINAL, SUPERVIVENCIA AL DOM WIPE Y LÓGICA DE EXPANSIÓN
// =========================================================================
let sidebarWrapperInstance = null; // Singleton para guardar la estructura base
function mountSidebar() {
if (sidebarWrapperInstance) {
// Si la instancia ya existe pero el Script 2 la borró del DOM, la reinyectamos
if (!document.getElementById('osint-sidebar-wrapper')) {
document.body.appendChild(sidebarWrapperInstance);
}
return;
}
const wrapper = document.createElement("div");
wrapper.id = "osint-sidebar-wrapper";
wrapper.classList.add("collapsed");
const container = document.createElement("div");
container.id = "osint-sidebar-container";
const toggle = document.createElement("div");
toggle.id = "osint-sidebar-toggle";
toggle.innerHTML = '▶';
toggle.addEventListener("click", () => {
const isCollapsed = wrapper.classList.toggle("collapsed");
toggle.innerHTML = isCollapsed ? '▶' : '◀';
});
// Cabecera Principal
const header = document.createElement("h4");
header.style.margin = "0 0 15px 0";
header.style.color = "#007bff";
header.style.textAlign = "center";
header.style.borderBottom = "2px solid #dee2e6";
header.style.paddingBottom = "5px";
header.innerHTML = '🔍 EXTRA INFO';
container.appendChild(header);
wrapper.appendChild(container);
wrapper.appendChild(toggle);
document.body.appendChild(wrapper);
sidebarWrapperInstance = wrapper; // Guardamos el DOM generado en memoria
// Inicializar Módulos solo la primera vez
const propsMod = new ImageProperties();
const searchHub = new ReverseSearchHub();
const analyzerMod = new ImageAnalyzer();
const toolsMod = new ImageToolsFull(); // El nuevo Image Editor Pro Elite
const wikiMod = new WikipediaLore();
const ddgMod = new DuckDuckGoPivot();
// Inyectar en el contenedor
container.appendChild(propsMod.htmlElements.details);
container.appendChild(searchHub.htmlElements.details);
container.appendChild(analyzerMod.htmlElements.details);
container.appendChild(wikiMod.htmlElements.details);
container.appendChild(ddgMod.htmlElements.details);
container.appendChild(toolsMod.htmlElements.details);
// Activar el Hijacker de clics con los nuevos módulos
initDOMHijacker(wikiMod, ddgMod);
// Ejecutar Auto-Inferencia de imagen
autoLoadImageContext();
}
// EL ESCUDO: Vigila si el body sufre un innerHTML (como hace el Script 2)
// y restaura el Sidebar instantáneamente sin perder el estado de las herramientas.
function enforceSidebarSurvival() {
const observer = new MutationObserver(() => {
if (document.body && !document.getElementById('osint-sidebar-wrapper')) {
mountSidebar();
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
mountSidebar();
enforceSidebarSurvival();
});
} else {
mountSidebar();
enforceSidebarSurvival();
}
})();