```html Multi Tool Hub

Multi Tool Hub

``` ```css /* style.css */ :root { --bg-color: #1E1E2F; --text-color: #EAEAEA; --header-bg: #2B2D42; --accent-color: #FFD700; /* Gold */ --tool-card-bg: #3A3D5B; --tool-card-hover-bg: #FFD700; --tool-card-hover-text: #1E1E2F; --button-hover-color: #E6C200; --box-shadow-color: rgba(255, 215, 0, 0.2); --input-bg-color: #2B2D42; --input-border-color: #4A4E69; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: var(--bg-color); color: var(--text-color); line-height: 1.6; transition: background-color 0.3s ease; } header { background-color: var(--header-bg); color: var(--accent-color); padding: 1.5rem 0; text-align: center; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); margin-bottom: 2rem; } header h1 { font-size: 2.5rem; font-weight: 600; } main { max-width: 1200px; margin: 0 auto; padding: 0 1rem; } .tool-grid { display: grid; gap: 1.5rem; grid-template-columns: repeat(3, 1fr); } .tool-card { background-color: var(--tool-card-bg); padding: 1.5rem; border-radius: 8px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); transition: transform 0.3s ease, background-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease; display: flex; flex-direction: column; justify-content: space-between; } .tool-card:hover { transform: translateY(-5px); background-color: var(--tool-card-hover-bg); color: var(--tool-card-hover-text); box-shadow: 0 8px 25px var(--box-shadow-color); } .tool-card h2 { font-size: 1.5rem; margin-bottom: 0.75rem; color: var(--accent-color); } .tool-card:hover h2 { color: var(--tool-card-hover-text); } .tool-card p { font-size: 0.95rem; margin-bottom: 1rem; flex-grow: 1; } .btn { background-color: var(--accent-color); color: var(--bg-color); border: none; padding: 0.75rem 1.5rem; border-radius: 5px; font-weight: bold; cursor: pointer; text-align: center; transition: background-color 0.3s ease; display: inline-block; font-size: 0.9rem; } .btn:hover { background-color: var(--button-hover-color); } .tool-card .btn { width: 100%; } /* Active Tool Container */ .active-tool-container { background-color: var(--tool-card-bg); padding: 2rem; border-radius: 8px; margin-top: 2rem; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); } #active-tool-container h2 { color: var(--accent-color); margin-bottom: 1.5rem; text-align: center; font-size: 2rem; } #back-to-tools-btn { margin-bottom: 1.5rem; background-color: var(--header-bg); color: var(--accent-color); } #back-to-tools-btn:hover { background-color: #4a4e69; } /* Tool-specific UI Elements Styling */ .tool-content label { display: block; margin-bottom: 0.5rem; font-weight: bold; color: var(--text-color); } .tool-content input[type="text"], .tool-content input[type="number"], .tool-content input[type="date"], .tool-content input[type="file"], .tool-content select, .tool-content textarea { width: 100%; padding: 0.75rem; margin-bottom: 1rem; border-radius: 5px; border: 1px solid var(--input-border-color); background-color: var(--input-bg-color); color: var(--text-color); font-size: 1rem; } .tool-content input[type="file"] { padding: 0.5rem; /* Specific padding for file input */ } .tool-content textarea { min-height: 100px; resize: vertical; } .tool-content .btn { margin-top: 0.5rem; margin-right: 0.5rem; /* Spacing between buttons */ } .tool-content .output-area, .tool-content .preview-area { margin-top: 1.5rem; padding: 1rem; background-color: var(--input-bg-color); border-radius: 5px; border: 1px solid var(--input-border-color); word-wrap: break-word; } .tool-content .output-area h4, .tool-content .preview-area h4 { color: var(--accent-color); margin-bottom: 0.5rem; } .tool-content img, .tool-content video, .tool-content audio { max-width: 100%; height: auto; border-radius: 5px; margin-top: 0.5rem; } .tool-content canvas { max-width: 100%; border: 1px solid var(--input-border-color); border-radius: 5px; margin-top: 0.5rem; } /* For range inputs, etc. */ .tool-content input[type="range"] { width: calc(100% - 60px); /* Adjust if you have labels next to it */ display: inline-block; vertical-align: middle; } .tool-content .range-value { display: inline-block; width: 50px; text-align: right; vertical-align: middle; } /* Checkbox styling for password generator */ .tool-content .checkbox-group { margin-bottom: 1rem; } .tool-content .checkbox-group label { display: inline-block; margin-right: 1rem; font-weight: normal; } .tool-content .checkbox-group input[type="checkbox"] { margin-right: 0.3rem; vertical-align: middle; } footer { text-align: center; padding: 2rem 0; margin-top: 3rem; color: #aaa; font-size: 0.9rem; border-top: 1px solid var(--header-bg); } /* Loading Overlay */ #loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(30, 30, 47, 0.8); /* Semi-transparent --bg-color */ z-index: 1000; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--accent-color); } .loading-spinner { border: 8px solid var(--tool-card-bg); border-top: 8px solid var(--accent-color); border-radius: 50%; width: 60px; height: 60px; animation: spin 1s linear infinite; margin-bottom: 1rem; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Responsive Design */ @media (max-width: 992px) { /* Tablet */ .tool-grid { grid-template-columns: repeat(2, 1fr); } header h1 { font-size: 2rem; } } @media (max-width: 600px) { /* Mobile */ .tool-grid { grid-template-columns: 1fr; } header h1 { font-size: 1.8rem; } .btn { padding: 0.6rem 1.2rem; font-size: 0.85rem; } .active-tool-container { padding: 1.5rem; } #active-tool-container h2 { font-size: 1.5rem; } } /* Specific styles for image cropper preview */ #imageCropperPreviewContainer { width: 100%; max-height: 400px; /* Or any appropriate height */ overflow: hidden; /* If image is larger than container */ position: relative; /* For crop box positioning */ border: 1px solid var(--input-border-color); background-color: var(--input-bg-color); /* Dark background for contrast */ } #imageCropperCanvas { display: block; /* Remove extra space below canvas */ max-width: 100%; max-height: 100%; } #cropBox { position: absolute; border: 2px dashed var(--accent-color); cursor: move; box-sizing: border-box; /* Important for correct sizing */ } .resizer { position: absolute; width: 10px; height: 10px; background-color: var(--accent-color); border: 1px solid var(--bg-color); } .resizer.top-left { top: -5px; left: -5px; cursor: nwse-resize; } .resizer.top-right { top: -5px; right: -5px; cursor: nesw-resize; } .resizer.bottom-left { bottom: -5px; left: -5px; cursor: nesw-resize; } .resizer.bottom-right { bottom: -5px; right: -5px; cursor: nwse-resize; } ``` ```javascript // script.js document.addEventListener('DOMContentLoaded', () => { const toolGrid = document.getElementById('tool-grid'); const activeToolContainer = document.getElementById('active-tool-container'); const activeToolTitle = document.getElementById('active-tool-title'); const activeToolContent = document.getElementById('active-tool-content'); const backToToolsBtn = document.getElementById('back-to-tools-btn'); const loadingOverlay = document.getElementById('loading-overlay'); const showLoading = (message = "Processing...") => { loadingOverlay.querySelector('p').textContent = message; loadingOverlay.style.display = 'flex'; }; const hideLoading = () => { loadingOverlay.style.display = 'none'; }; // --- START QR Code Generator Library (minimal, self-contained) --- // This is a simplified version based on `qrcodegen.js` by Nayuki. // For a full-featured version, a dedicated library is better. // This one is embedded to adhere to "no external libraries" constraint. const qrcodegen = (() => { function QRCanvas(options) { this.options = options; this.canvas = document.createElement('canvas'); this.ctx = this.canvas.getContext('2d'); this.scale = this.options.scale || 4; this.padding = this.options.padding || 10; } QRCanvas.prototype.draw = function (qr) { const cells = qr.getModules(); const numModules = qr.size; const canvasSize = (numModules * this.scale) + (this.padding * 2); this.canvas.width = canvasSize; this.canvas.height = canvasSize; this.ctx.fillStyle = this.options.bgColor || '#FFFFFF'; this.ctx.fillRect(0, 0, canvasSize, canvasSize); this.ctx.fillStyle = this.options.fgColor || '#000000'; for (let r = 0; r < numModules; r++) { for (let c = 0; c < numModules; c++) { if (cells[r][c]) { this.ctx.fillRect( this.padding + c * this.scale, this.padding + r * this.scale, this.scale, this.scale); } } } return this.canvas; }; // Bare-bones QR Code data generation logic (simplified) // This is a placeholder for a more robust QR generation algorithm. // A full QR code generator is complex. This will generate a very basic pattern. // For real applications, a more complete internal library or a component is needed. // The below is a simplified representation. class QrCode { constructor(text) { this.text = text; // Simplified: treating size as fixed or very basically derived this.size = 21; // Smallest QR code size this.modules = []; for (let i = 0; i < this.size; i++) { this.modules[i] = []; for (let j = 0; j < this.size; j++) { // Super simple pattern based on text length and char codes // THIS IS NOT A VALID QR ENCODING - JUST A VISUAL PATTERN this.modules[i][j] = (text.charCodeAt((i * j) % text.length) % 2 === 0) && (i > 2 && i < this.size -3 && j > 2 && j < this.size - 3); // Basic finder pattern attempt (very crude) if ((i < 7 && j < 7) || (i < 7 && j > this.size - 8) || (i > this.size - 8 && j < 7)) { if (i % 6 === 0 || j % 6 === 0 || (i % 6 === 1 && (j>0 && j<6)) || (j % 6 === 1 && (i>0 && i<6)) || (i%6 === 5 && (j>0 && j<6)) || (j%6 === 5 && (i>0 && i<6))) this.modules[i][j] = true; if ((i > 1 && i < 5) && (j > 1 && j < 5)) this.modules[i][j] = false; if ((i > 1 && i < 5) && (j > this.size-6 && j < this.size-2)) this.modules[i][j] = false; if ((i > this.size-6 && i < this.size-2) && (j > 1 && j < 5)) this.modules[i][j] = false; if (i === 3 && j === 3) this.modules[i][j] = true; if (i === 3 && j === this.size-4) this.modules[i][j] = true; if (i === this.size-4 && j === 3) this.modules[i][j] = true; } } } } getModules() { return this.modules; } // In a real library, this would use Reed-Solomon error correction, etc. static encodeText(text, ecl) { return new QrCode(text); } // ecl is error correction level } return { QrCode, QRCanvas }; })(); // --- END QR Code Generator Library --- const tools = [ { id: "imageConverter", title: "Image Converter", description: "Convert images between JPG, PNG, and WEBP formats.", initUI: (container) => { container.innerHTML = `
`; document.getElementById('imgConvBtn').addEventListener('click', ToolModules.imageConverter.process); } }, { id: "imageCompressor", title: "Image Compressor", description: "Compress image file size with quality settings (for JPG/WEBP).", initUI: (container) => { container.innerHTML = `
0.7
`; const qualitySlider = document.getElementById('imgCompQuality'); const qualityValue = document.getElementById('imgCompQualityValue'); qualitySlider.oninput = () => qualityValue.textContent = qualitySlider.value; document.getElementById('imgCompBtn').addEventListener('click', ToolModules.imageCompressor.process); } }, { id: "imageCropper", title: "Image Cropper", description: "Upload, crop image with preview, and export.", initUI: (container) => { container.innerHTML = `
`; document.getElementById('imgCropFile').addEventListener('change', ToolModules.imageCropper.loadFile); document.getElementById('imgCropBtn').addEventListener('click', ToolModules.imageCropper.crop); } }, { id: "videoConverter", title: "Video Converter (Re-encoder)", description: "Re-encode video to WebM or MP4 (browser dependent). This can be slow.", initUI: (container) => { container.innerHTML = `

Note: Conversion is done by re-encoding via canvas, which can be slow and resource-intensive. Output quality may vary.

`; document.getElementById('vidConvBtn').addEventListener('click', ToolModules.videoConverter.process); } }, { id: "audioConverter", title: "Audio to WAV Converter", description: "Convert audio files (MP3, OGG, etc.) to WAV format.", initUI: (container) => { container.innerHTML = `
`; document.getElementById('audConvBtn').addEventListener('click', ToolModules.audioConverter.process); } }, { id: "audioTrimmer", title: "Audio Trimmer", description: "Upload, trim audio based on start/end time, and export.", initUI: (container) => { container.innerHTML = `
`; document.getElementById('audTrimFile').addEventListener('change', ToolModules.audioTrimmer.loadFile); document.getElementById('audTrimBtn').addEventListener('click', ToolModules.audioTrimmer.process); } }, { id: "ageCalculator", title: "Age Calculator", description: "Input date of birth to find age in years, months, and days.", initUI: (container) => { container.innerHTML = `
`; document.getElementById('calcAgeBtn').addEventListener('click', ToolModules.ageCalculator.calculate); } }, { id: "emiCalculator", title: "EMI Calculator", description: "Calculate Equated Monthly Installment (EMI) for loans.", initUI: (container) => { container.innerHTML = `
`; document.getElementById('calcEmiBtn').addEventListener('click', ToolModules.emiCalculator.calculate); } }, { id: "sipCalculator", title: "SIP Calculator", description: "Calculate the future value of your Systematic Investment Plan (SIP).", initUI: (container) => { container.innerHTML = `
`; document.getElementById('calcSipBtn').addEventListener('click', ToolModules.sipCalculator.calculate); } }, { id: "qrCodeGenerator", title: "QR Code Generator", description: "Enter text or URL to generate a downloadable QR code.", initUI: (container) => { container.innerHTML = `

Note: This QR generator is basic. For complex data or high error correction, dedicated tools are recommended.

`; document.getElementById('generateQrBtn').addEventListener('click', ToolModules.qrCodeGenerator.generate); } }, { id: "passwordGenerator", title: "Password Generator", description: "Generate secure passwords with customizable options.", initUI: (container) => { container.innerHTML = `
`; document.getElementById('generatePassBtn').addEventListener('click', ToolModules.passwordGenerator.generate); document.getElementById('copyPassBtn')?.addEventListener('click', ToolModules.passwordGenerator.copy); } }, { id: "wordCounter", title: "Word Counter", description: "Count words, characters, spaces, and estimate reading time.", initUI: (container) => { container.innerHTML = `

Words: 0

Characters (with spaces): 0

Characters (no spaces): 0

Spaces: 0

Reading Time (approx.): 0 minutes

`; document.getElementById('wordCountText').addEventListener('input', ToolModules.wordCounter.count); } }, { id: "base64EncoderDecoder", title: "Base64 Encoder/Decoder", description: "Convert text to Base64 and vice versa.", initUI: (container) => { container.innerHTML = `
`; document.getElementById('b64EncodeBtn').addEventListener('click', ToolModules.base64EncoderDecoder.encode); document.getElementById('b64DecodeBtn').addEventListener('click', ToolModules.base64EncoderDecoder.decode); } }, { id: "colorPicker", title: "Color Picker Tool", description: "Pick a color and display its HEX, RGB, and HSL values.", initUI: (container) => { container.innerHTML = `

HEX: #FFD700

RGB: rgb(255, 215, 0)

HSL: hsl(51, 100%, 50%)

`; document.getElementById('colorPickerInput').addEventListener('input', ToolModules.colorPicker.update); ToolModules.colorPicker.update(); // Initial update } }, { id: "textToSpeech", title: "Text to Speech", description: "Enter text and listen to it using browser's speech synthesis.", initUI: (container) => { container.innerHTML = `
`; ToolModules.textToSpeech.initVoices(); document.getElementById('ttsSpeakBtn').addEventListener('click', ToolModules.textToSpeech.speak); document.getElementById('ttsStopBtn').addEventListener('click', ToolModules.textToSpeech.stop); } }, { id: "speechToText", title: "Speech to Text", description: "Use your microphone to convert voice into text.", initUI: (container) => { container.innerHTML = `

Status: Idle

`; ToolModules.speechToText.init(); document.getElementById('sttStartBtn').addEventListener('click', ToolModules.speechToText.start); document.getElementById('sttStopBtn').addEventListener('click', ToolModules.speechToText.stop); } }, { id: "jsonFormatter", title: "JSON Formatter", description: "Paste JSON to auto-format, validate, and pretty-print.", initUI: (container) => { container.innerHTML = `

`; document.getElementById('jsonFormatBtn').addEventListener('click', ToolModules.jsonFormatter.format); } }, { id: "unitConverter", title: "Unit Converter", description: "Convert values between units (length, weight, temperature).", initUI: (container) => { container.innerHTML = `
=
`; ToolModules.unitConverter.init(); document.getElementById('unitCategory').addEventListener('change', ToolModules.unitConverter.init); document.getElementById('unitInput').addEventListener('input', ToolModules.unitConverter.convert); document.getElementById('unitFrom').addEventListener('change', ToolModules.unitConverter.convert); document.getElementById('unitTo').addEventListener('change', ToolModules.unitConverter.convert); } }, { id: "bmiCalculator", title: "BMI Calculator", description: "Calculate your Body Mass Index (BMI).", initUI: (container) => { container.innerHTML = `
`; document.getElementById('calcBmiBtn').addEventListener('click', ToolModules.bmiCalculator.calculate); } }, { id: "timerStopwatch", title: "Timer / Stopwatch", description: "Simple timer and stopwatch functionality.", initUI: (container) => { container.innerHTML = `
00:00:00.0

Timer

: :
`; // Alarm sound (simple beep) const alarmBase64 = "UklGRkFBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YVxBAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA"; // Short beep document.getElementById('timerAlarm').src = `data:audio/wav;base64,${alarmBase64}`; ToolModules.timerStopwatch.init(); } } ]; // Populate Tool Grid tools.forEach(tool => { const card = document.createElement('div'); card.className = 'tool-card'; card.innerHTML = `

${tool.title}

${tool.description}

`; toolGrid.appendChild(card); card.querySelector('button').addEventListener('click', () => { openTool(tool); }); }); function openTool(tool) { toolGrid.style.display = 'none'; activeToolTitle.textContent = tool.title; activeToolContent.innerHTML = ''; // Clear previous tool tool.initUI(activeToolContent); // Initialize new tool's UI activeToolContainer.style.display = 'block'; window.scrollTo(0, 0); // Scroll to top } backToToolsBtn.addEventListener('click', () => { activeToolContainer.style.display = 'none'; toolGrid.style.display = 'grid'; activeToolContent.innerHTML = ''; // Clear active tool content }); // --- Utility Functions --- function downloadDataUrl(dataUrl, filename) { const link = document.createElement('a'); link.href = dataUrl; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); downloadDataUrl(url, filename); URL.revokeObjectURL(url); } // --- Tool Modules Logic --- const ToolModules = { imageConverter: { process: () => { const fileInput = document.getElementById('imgConvFile'); const formatSelect = document.getElementById('imgConvFormat'); const outputDiv = document.getElementById('imgConvOutput'); const resultImg = document.getElementById('imgConvResult'); const downloadLink = document.getElementById('imgConvDownload'); if (!fileInput.files || fileInput.files.length === 0) { alert("Please select an image file."); return; } const file = fileInput.files[0]; const format = formatSelect.value; const filename = file.name.substring(0, file.name.lastIndexOf('.')) + '.' + format.split('/')[1]; showLoading("Converting image..."); const reader = new FileReader(); reader.onload = (e) => { 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); try { const dataUrl = canvas.toDataURL(format); resultImg.src = dataUrl; downloadLink.href = dataUrl; downloadLink.download = filename; outputDiv.style.display = 'block'; } catch (error) { console.error("Conversion error:", error); alert(`Error converting image. Format ${format} might not be supported by your browser for encoding or source image is problematic.`); } finally { hideLoading(); } }; img.onerror = () => { alert("Failed to load image. It might be corrupted or an unsupported format."); hideLoading(); }; img.src = e.target.result; }; reader.readAsDataURL(file); } }, imageCompressor: { process: () => { const fileInput = document.getElementById('imgCompFile'); const qualitySlider = document.getElementById('imgCompQuality'); const outputDiv = document.getElementById('imgCompOutput'); const resultImg = document.getElementById('imgCompResult'); const downloadLink = document.getElementById('imgCompDownload'); const infoP = document.getElementById('imgCompInfo'); if (!fileInput.files || fileInput.files.length === 0) { alert("Please select an image file."); return; } const file = fileInput.files[0]; const quality = parseFloat(qualitySlider.value); let outputFormat = 'image/jpeg'; // Default for compression if (file.type === 'image/png') outputFormat = 'image/png'; // PNG compression is lossless, quality ignored by most browsers but good to specify if (file.type === 'image/webp') outputFormat = 'image/webp'; // WEBP can use quality const filename = file.name.substring(0, file.name.lastIndexOf('.')) + '_compressed.' + outputFormat.split('/')[1]; showLoading("Compressing image..."); const reader = new FileReader(); reader.onload = (e) => { const originalSize = file.size; 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); try { const dataUrl = canvas.toDataURL(outputFormat, (outputFormat === 'image/png' ? undefined : quality)); resultImg.src = dataUrl; downloadLink.href = dataUrl; downloadLink.download = filename; // Estimate new size const newSize = dataUrl.length * 0.75; // Base64 to bytes approx infoP.textContent = `Original size: ${(originalSize / 1024).toFixed(2)} KB. Compressed size: ${(newSize / 1024).toFixed(2)} KB.`; outputDiv.style.display = 'block'; } catch (error) { console.error("Compression error:", error); alert(`Error compressing image. Format ${outputFormat} with quality ${quality} might not be supported, or source image is problematic.`); } finally { hideLoading(); } }; img.onerror = () => { alert("Failed to load image for compression."); hideLoading(); }; img.src = e.target.result; }; reader.readAsDataURL(file); } }, imageCropper: { img: null, canvas: null, ctx: null, cropBoxElement: null, resizers: [], cropRect: { x: 50, y: 50, width: 150, height: 100 }, isDragging: false, isResizing: false, resizeHandle: null, startX: 0, startY: 0, previewContainer: null, loadFile: (event) => { const C = ToolModules.imageCropper; const file = event.target.files[0]; if (!file) return; C.previewContainer = document.getElementById('imageCropperPreviewContainer'); C.canvas = document.getElementById('imageCropperCanvas'); C.ctx = C.canvas.getContext('2d'); C.cropBoxElement = document.getElementById('cropBox'); document.getElementById('imgCropBtn').style.display = 'inline-block'; document.getElementById('imgCropOutput').style.display = 'none'; const reader = new FileReader(); reader.onload = (e) => { C.img = new Image(); C.img.onload = () => { // Scale image to fit preview container while maintaining aspect ratio const containerWidth = C.previewContainer.clientWidth; const containerHeight = C.previewContainer.clientHeight; let canvasWidth = C.img.width; let canvasHeight = C.img.height; if (canvasWidth > containerWidth) { const ratio = containerWidth / canvasWidth; canvasWidth = containerWidth; canvasHeight *= ratio; } if (canvasHeight > containerHeight) { const ratio = containerHeight / canvasHeight; canvasHeight = containerHeight; canvasWidth *= ratio; } C.canvas.width = canvasWidth; C.canvas.height = canvasHeight; C.ctx.drawImage(C.img, 0, 0, canvasWidth, canvasHeight); C.previewContainer.style.display = 'block'; C.cropBoxElement.style.display = 'block'; C.initCropBox(); }; C.img.src = e.target.result; }; reader.readAsDataURL(file); }, initCropBox: () => { const C = ToolModules.imageCropper; C.cropRect = { x: C.canvas.width / 4, y: C.canvas.height / 4, width: C.canvas.width / 2, height: C.canvas.height / 2 }; C.updateCropBox(); C.cropBoxElement.onmousedown = C.onMouseDown; document.onmousemove = C.onMouseMove; // Attach to document to drag outside box document.onmouseup = C.onMouseUp; C.resizers = Array.from(C.cropBoxElement.querySelectorAll('.resizer')); C.resizers.forEach(resizer => { resizer.onmousedown = (e) => { e.stopPropagation(); // Prevent cropBox onMouseDown C.isResizing = true; C.resizeHandle = resizer.className.split(' ')[1]; // e.g., "top-left" C.startX = e.clientX; C.startY = e.clientY; }; }); }, updateCropBox: () => { const C = ToolModules.imageCropper; C.cropBoxElement.style.left = C.cropRect.x + 'px'; C.cropBoxElement.style.top = C.cropRect.y + 'px'; C.cropBoxElement.style.width = C.cropRect.width + 'px'; C.cropBoxElement.style.height = C.cropRect.height + 'px'; }, onMouseDown: (e) => { const C = ToolModules.imageCropper; if (e.target === C.cropBoxElement) { // Only if mousedown is on the box itself, not resizers C.isDragging = true; C.startX = e.clientX - C.cropRect.x; C.startY = e.clientY - C.cropRect.y; } }, onMouseMove: (e) => { const C = ToolModules.imageCropper; if (C.isDragging) { C.cropRect.x = e.clientX - C.startX; C.cropRect.y = e.clientY - C.startY; // Boundary checks C.cropRect.x = Math.max(0, Math.min(C.cropRect.x, C.canvas.width - C.cropRect.width)); C.cropRect.y = Math.max(0, Math.min(C.cropRect.y, C.canvas.height - C.cropRect.height)); C.updateCropBox(); } else if (C.isResizing) { const dx = e.clientX - C.startX; const dy = e.clientY - C.startY; let newX = C.cropRect.x, newY = C.cropRect.y, newWidth = C.cropRect.width, newHeight = C.cropRect.height; if (C.resizeHandle.includes('left')) { newWidth -= dx; newX += dx; } if (C.resizeHandle.includes('right')) { newWidth += dx; } if (C.resizeHandle.includes('top')) { newHeight -= dy; newY += dy; } if (C.resizeHandle.includes('bottom')) { newHeight += dy; } // Min size and boundary checks if (newWidth > 10 && newX >=0 && (newX + newWidth) <= C.canvas.width) { C.cropRect.x = newX; C.cropRect.width = newWidth; } if (newHeight > 10 && newY >=0 && (newY + newHeight) <= C.canvas.height) { C.cropRect.y = newY; C.cropRect.height = newHeight; } C.startX = e.clientX; C.startY = e.clientY; C.updateCropBox(); } }, onMouseUp: () => { ToolModules.imageCropper.isDragging = false; ToolModules.imageCropper.isResizing = false; }, crop: () => { const C = ToolModules.imageCropper; if (!C.img) { alert("Please load an image first."); return; } showLoading("Cropping image..."); // Calculate crop parameters based on original image dimensions const scaleX = C.img.naturalWidth / C.canvas.width; const scaleY = C.img.naturalHeight / C.canvas.height; const sourceX = C.cropRect.x * scaleX; const sourceY = C.cropRect.y * scaleY; const sourceWidth = C.cropRect.width * scaleX; const sourceHeight = C.cropRect.height * scaleY; const cropCanvas = document.createElement('canvas'); cropCanvas.width = sourceWidth; cropCanvas.height = sourceHeight; const cropCtx = cropCanvas.getContext('2d'); cropCtx.drawImage(C.img, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, sourceWidth, sourceHeight); const dataUrl = cropCanvas.toDataURL(C.img.src.startsWith('data:image/png') ? 'image/png' : 'image/jpeg'); document.getElementById('imgCropResult').src = dataUrl; document.getElementById('imgCropDownload').href = dataUrl; document.getElementById('imgCropDownload').download = "cropped_image.png"; // Or jpg document.getElementById('imgCropOutput').style.display = 'block'; hideLoading(); // Cleanup mouse listeners if tool is closed/reopened document.onmousemove = null; document.onmouseup = null; } }, videoConverter: { process: async () => { const fileInput = document.getElementById('vidConvFile'); const formatSelect = document.getElementById('vidConvFormat'); const outputDiv = document.getElementById('vidConvOutput'); const resultVideo = document.getElementById('vidConvResult'); const downloadLink = document.getElementById('vidConvDownload'); if (!fileInput.files || fileInput.files.length === 0) { alert("Please select a video file."); return; } const file = fileInput.files[0]; const outputMimeType = formatSelect.value; if (!MediaRecorder.isTypeSupported(outputMimeType)) { alert(`Your browser does not support recording to ${outputMimeType}. Try WebM.`); return; } showLoading("Preparing video for conversion..."); const video = document.createElement('video'); video.src = URL.createObjectURL(file); video.onloadedmetadata = async () => { const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); const stream = canvas.captureStream(30); // 30 FPS const mediaRecorder = new MediaRecorder(stream, { mimeType: outputMimeType }); const chunks = []; mediaRecorder.ondataavailable = (e) => chunks.push(e.data); mediaRecorder.onstop = () => { const blob = new Blob(chunks, { type: outputMimeType }); const url = URL.createObjectURL(blob); resultVideo.src = url; downloadLink.href = url; downloadLink.download = `converted.${outputMimeType.split('/')[1]}`; outputDiv.style.display = 'block'; hideLoading(); URL.revokeObjectURL(video.src); // Clean up }; let playing = false; video.onplay = () => playing = true; video.onpause = () => playing = false; // Should not pause during process function drawFrame() { if (!video.paused && !video.ended) { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); requestAnimationFrame(drawFrame); } else if (video.ended && mediaRecorder.state === "recording") { mediaRecorder.stop(); } } video.onended = () => { if (mediaRecorder.state === "recording") { mediaRecorder.stop(); } }; try { await video.play(); showLoading(`Converting... Drawing frame ${Math.floor(video.currentTime)}s / ${Math.floor(video.duration)}s`); mediaRecorder.start(); drawFrame(); // Start drawing frames // Monitor progress const progressInterval = setInterval(() => { if (playing) { showLoading(`Converting... Frame at ${video.currentTime.toFixed(1)}s / ${video.duration.toFixed(1)}s`); } if (video.ended || mediaRecorder.state !== "recording") { clearInterval(progressInterval); } }, 500); } catch (err) { console.error("Error playing video for conversion:", err); alert("Could not start video playback for conversion. The video file might be corrupted or in an unsupported format."); hideLoading(); URL.revokeObjectURL(video.src); } }; video.onerror = () => { alert("Error loading video. File might be corrupted or unsupported."); hideLoading(); }; } }, audioConverter: { audioCtx: null, process: async () => { const fileInput = document.getElementById('audConvFile'); const outputDiv = document.getElementById('audConvOutput'); const resultAudio = document.getElementById('audConvResult'); const downloadLink = document.getElementById('audConvDownload'); if (!fileInput.files || fileInput.files.length === 0) { alert("Please select an audio file."); return; } const file = fileInput.files[0]; showLoading("Converting audio to WAV..."); if (!ToolModules.audioConverter.audioCtx) { ToolModules.audioConverter.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } const audioCtx = ToolModules.audioConverter.audioCtx; const reader = new FileReader(); reader.onload = async (e) => { try { const audioBuffer = await audioCtx.decodeAudioData(e.target.result); const wavBlob = ToolModules.audioConverter.bufferToWav(audioBuffer, audioCtx.sampleRate); const url = URL.createObjectURL(wavBlob); resultAudio.src = url; downloadLink.href = url; outputDiv.style.display = 'block'; } catch (error) { console.error("Audio conversion error:", error); alert("Error converting audio. The file might be corrupted or in an unsupported format."); } finally { hideLoading(); } }; reader.readAsArrayBuffer(file); }, // Helper function to convert AudioBuffer to WAV Blob bufferToWav: (buffer, sampleRate) => { const numChannels = buffer.numberOfChannels; const numFrames = buffer.length; const length = numFrames * numChannels * 2 + 44; const wavBuffer = new ArrayBuffer(length); const view = new DataView(wavBuffer); let offset = 0; function writeString(str) { for (let i = 0; i < str.length; i++) { view.setUint8(offset + i, str.charCodeAt(i)); } offset += str.length; } function writeUint32(val) { view.setUint32(offset, val, true); offset += 4; } function writeUint16(val) { view.setUint16(offset, val, true); offset += 2; } writeString('RIFF'); writeUint32(length - 8); writeString('WAVE'); writeString('fmt '); writeUint32(16); // Subchunk1Size (16 for PCM) writeUint16(1); // AudioFormat (1 for PCM) writeUint16(numChannels); writeUint32(sampleRate); writeUint32(sampleRate * numChannels * 2); // ByteRate writeUint16(numChannels * 2); // BlockAlign writeUint16(16); // BitsPerSample writeString('data'); writeUint32(numFrames * numChannels * 2); // Subchunk2Size // Write PCM data for (let i = 0; i < numFrames; i++) { for (let channel = 0; channel < numChannels; channel++) { const sample = buffer.getChannelData(channel)[i]; let s = Math.max(-1, Math.min(1, sample)); s = s < 0 ? s * 0x8000 : s * 0x7FFF; view.setInt16(offset, s, true); offset += 2; } } return new Blob([view], { type: 'audio/wav' }); } }, audioTrimmer: { audioCtx: null, originalBuffer: null, loadFile: (event) => { const T = ToolModules.audioTrimmer; const file = event.target.files[0]; if (!file) return; showLoading("Loading audio..."); if (!T.audioCtx) { T.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } const reader = new FileReader(); reader.onload = async (e) => { try { T.originalBuffer = await T.audioCtx.decodeAudioData(e.target.result); document.getElementById('audTrimControls').style.display = 'block'; document.getElementById('audTrimOutput').style.display = 'none'; const duration = T.originalBuffer.duration; document.getElementById('audTrimDuration').textContent = duration.toFixed(1); document.getElementById('audTrimEnd').value = duration.toFixed(1); document.getElementById('audTrimEnd').max = duration.toFixed(1); document.getElementById('audTrimStart').max = duration.toFixed(1); } catch (error) { alert("Error decoding audio file."); console.error(error); } finally { hideLoading(); } }; reader.readAsArrayBuffer(file); }, process: () => { const T = ToolModules.audioTrimmer; if (!T.originalBuffer) { alert("Please load an audio file first."); return; } const startTime = parseFloat(document.getElementById('audTrimStart').value); const endTime = parseFloat(document.getElementById('audTrimEnd').value); if (isNaN(startTime) || isNaN(endTime) || startTime < 0 || endTime <= startTime || endTime > T.originalBuffer.duration) { alert("Invalid start or end time."); return; } showLoading("Trimming audio..."); const startFrame = Math.floor(startTime * T.originalBuffer.sampleRate); const endFrame = Math.floor(endTime * T.originalBuffer.sampleRate); const newLength = endFrame - startFrame; if (newLength <= 0) { alert("Trimmed duration is zero or negative."); hideLoading(); return; } const trimmedBuffer = T.audioCtx.createBuffer( T.originalBuffer.numberOfChannels, newLength, T.originalBuffer.sampleRate ); for (let i = 0; i < T.originalBuffer.numberOfChannels; i++) { const channelData = T.originalBuffer.getChannelData(i); const trimmedChannelData = trimmedBuffer.getChannelData(i); trimmedChannelData.set(channelData.subarray(startFrame, endFrame)); } const wavBlob = ToolModules.audioConverter.bufferToWav(trimmedBuffer, T.audioCtx.sampleRate); // Reuse WAV conversion const url = URL.createObjectURL(wavBlob); document.getElementById('audTrimResult').src = url; document.getElementById('audTrimDownload').href = url; document.getElementById('audTrimOutput').style.display = 'block'; hideLoading(); } }, ageCalculator: { calculate: () => { const birthDateInput = document.getElementById('birthDate').value; const resultDiv = document.getElementById('ageResult'); if (!birthDateInput) { resultDiv.textContent = "Please enter your date of birth."; resultDiv.style.display = 'block'; return; } const birthDate = new Date(birthDateInput); const today = new Date(); let years = today.getFullYear() - birthDate.getFullYear(); let months = today.getMonth() - birthDate.getMonth(); let days = today.getDate() - birthDate.getDate(); if (days < 0) { months--; days += new Date(today.getFullYear(), today.getMonth(), 0).getDate(); // Days in previous month } if (months < 0) { years--; months += 12; } resultDiv.innerHTML = `

Your Age:

${years} years, ${months} months, and ${days} days

`; resultDiv.style.display = 'block'; } }, emiCalculator: { calculate: () => { const P = parseFloat(document.getElementById('loanAmount').value); const annualRate = parseFloat(document.getElementById('interestRate').value); const tenureYears = parseFloat(document.getElementById('loanTenure').value); const resultDiv = document.getElementById('emiResult'); if (isNaN(P) || isNaN(annualRate) || isNaN(tenureYears) || P <= 0 || annualRate <= 0 || tenureYears <= 0) { resultDiv.textContent = "Please enter valid positive values for all fields."; resultDiv.style.display = 'block'; return; } const r = (annualRate / 100) / 12; // Monthly interest rate const n = tenureYears * 12; // Number of months const emi = P * r * (Math.pow(1 + r, n) / (Math.pow(1 + r, n) - 1)); const totalAmountPayable = emi * n; const totalInterest = totalAmountPayable - P; if (!isFinite(emi)) { resultDiv.textContent = "Calculation resulted in an invalid EMI. Check inputs, especially interest rate if it's too low or tenure is very short."; } else { resultDiv.innerHTML = `

EMI Details:

Monthly EMI: ${emi.toFixed(2)}

Total Interest Payable: ${totalInterest.toFixed(2)}

Total Amount Payable: ${totalAmountPayable.toFixed(2)}

`; } resultDiv.style.display = 'block'; } }, sipCalculator: { calculate: () => { const P = parseFloat(document.getElementById('monthlyInvestment').value); const annualRate = parseFloat(document.getElementById('expectedRate').value); const years = parseFloat(document.getElementById('investmentDuration').value); const resultDiv = document.getElementById('sipResult'); if (isNaN(P) || isNaN(annualRate) || isNaN(years) || P <= 0 || annualRate < 0 || years <= 0) { resultDiv.textContent = "Please enter valid positive values for all fields."; resultDiv.style.display = 'block'; return; } const n = years * 12; // Number of months const i = (annualRate / 100) / 12; // Monthly interest rate // M = P * {[(1+i)^n - 1] / i} * (1+i) const futureValue = P * ( (Math.pow(1 + i, n) - 1) / i ) * (1 + i); const totalInvested = P * n; const wealthGained = futureValue - totalInvested; resultDiv.innerHTML = `

SIP Projection:

Total Invested Amount: ${totalInvested.toFixed(2)}

Estimated Returns (Wealth Gained): ${wealthGained.toFixed(2)}

Future Value (Maturity Amount): ${futureValue.toFixed(2)}

`; resultDiv.style.display = 'block'; } }, qrCodeGenerator: { generate: () => { const text = document.getElementById('qrText').value; const outputDiv = document.getElementById('qrOutput'); const canvasContainer = document.getElementById('qrCanvasContainer'); const downloadLink = document.getElementById('qrDownload'); if (!text.trim()) { alert("Please enter text or URL for the QR code."); return; } showLoading("Generating QR Code..."); canvasContainer.innerHTML = ''; // Clear previous QR try { // Using the embedded qrcodegen const qr = qrcodegen.QrCode.encodeText(text, qrcodegen.QrCode.Ecc.MEDIUM); // Ecc is not fully implemented in this stub const qrCanvasGenerator = new qrcodegen.QRCanvas({ scale: 6, padding: 20, fgColor: getComputedStyle(document.documentElement).getPropertyValue('--accent-color').trim(), // Gold bgColor: getComputedStyle(document.documentElement).getPropertyValue('--bg-color').trim() // Dark Navy }); const canvasElement = qrCanvasGenerator.draw(qr); canvasContainer.appendChild(canvasElement); downloadLink.href = canvasElement.toDataURL('image/png'); outputDiv.style.display = 'block'; } catch (e) { console.error("QR Generation Error:", e); alert("Failed to generate QR code. The embedded QR generator is very basic and might not support long text or certain characters well."); outputDiv.style.display = 'none'; } finally { hideLoading(); } } }, passwordGenerator: { generate: () => { const length = parseInt(document.getElementById('passLength').value); const useUppercase = document.getElementById('passUppercase').checked; const useLowercase = document.getElementById('passLowercase').checked; const useNumbers = document.getElementById('passNumbers').checked; const useSymbols = document.getElementById('passSymbols').checked; const passwordField = document.getElementById('generatedPassword'); const resultDiv = document.getElementById('passResult'); const uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const lowercaseChars = "abcdefghijklmnopqrstuvwxyz"; const numberChars = "0123456789"; const symbolChars = "!@#$%^&*()_+-=[]{}|;:',.<>/?"; let charPool = ""; if (useUppercase) charPool += uppercaseChars; if (useLowercase) charPool += lowercaseChars; if (useNumbers) charPool += numberChars; if (useSymbols) charPool += symbolChars; if (charPool === "") { alert("Please select at least one character type."); passwordField.value = ""; resultDiv.style.display = 'none'; return; } let password = ""; for (let i = 0; i < length; i++) { const randomIndex = Math.floor(Math.random() * charPool.length); password += charPool[randomIndex]; } passwordField.value = password; resultDiv.style.display = 'block'; // Ensure copy button is available if (!document.getElementById('copyPassBtn')) { const copyBtn = document.createElement('button'); copyBtn.id = 'copyPassBtn'; copyBtn.className = 'btn'; copyBtn.textContent = 'Copy'; copyBtn.addEventListener('click', ToolModules.passwordGenerator.copy); passwordField.parentNode.appendChild(copyBtn); } }, copy: () => { const passwordField = document.getElementById('generatedPassword'); passwordField.select(); passwordField.setSelectionRange(0, 99999); // For mobile devices try { document.execCommand('copy'); alert("Password copied to clipboard!"); } catch (err) { alert("Failed to copy password. Please copy it manually."); } } }, wordCounter: { count: () => { const text = document.getElementById('wordCountText').value; const words = text.match(/\b\w+\b/g) || []; const charsWithSpace = text.length; const charsNoSpace = text.replace(/\s/g, "").length; const spaces = (text.match(/\s/g) || []).length; const readingTime = Math.ceil(words.length / 200); // Avg 200 WPM document.getElementById('wcWords').textContent = words.length; document.getElementById('wcCharsWithSpace').textContent = charsWithSpace; document.getElementById('wcCharsNoSpace').textContent = charsNoSpace; document.getElementById('wcSpaces').textContent = spaces; document.getElementById('wcReadTime').textContent = readingTime; } }, base64EncoderDecoder: { encode: () => { const input = document.getElementById('b64Input').value; const outputField = document.getElementById('b64Output'); try { // Handle Unicode characters correctly const utf8Bytes = new TextEncoder().encode(input); let binaryString = ''; utf8Bytes.forEach(byte => binaryString += String.fromCharCode(byte)); outputField.value = btoa(binaryString); } catch (e) { outputField.value = "Error encoding: " + e.message; } }, decode: () => { const input = document.getElementById('b64Input').value; const outputField = document.getElementById('b64Output'); try { const binaryString = atob(input); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } outputField.value = new TextDecoder().decode(bytes); } catch (e) { outputField.value = "Error decoding: Invalid Base64 string or " + e.message; } } }, colorPicker: { update: () => { const hex = document.getElementById('colorPickerInput').value; document.getElementById('hexValue').textContent = hex; const rgb = ToolModules.colorPicker.hexToRgb(hex); document.getElementById('rgbValue').textContent = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; const hsl = ToolModules.colorPicker.rgbToHsl(rgb.r, rgb.g, rgb.b); document.getElementById('hslValue').textContent = `hsl(${hsl.h.toFixed(0)}, ${hsl.s.toFixed(0)}%, ${hsl.l.toFixed(0)}%)`; }, hexToRgb: (hex) => { hex = hex.replace(/^#/, ''); const bigint = parseInt(hex, 16); return { r: (bigint >> 16) & 255, g: (bigint >> 8) & 255, b: bigint & 255 }; }, 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; // achromatic } 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: h * 360, s: s * 100, l: l * 100 }; } }, textToSpeech: { synth: window.speechSynthesis, voices: [], initVoices: () => { const T = ToolModules.textToSpeech; const voiceSelect = document.getElementById('ttsVoice'); if (!T.synth) { alert("Speech synthesis not supported in this browser."); document.getElementById('ttsSpeakBtn').disabled = true; document.getElementById('ttsStopBtn').disabled = true; return; } function populateVoiceList() { T.voices = T.synth.getVoices(); voiceSelect.innerHTML = ''; T.voices.forEach(voice => { const option = document.createElement('option'); option.textContent = `${voice.name} (${voice.lang})`; option.setAttribute('data-lang', voice.lang); option.setAttribute('data-name', voice.name); voiceSelect.appendChild(option); }); } populateVoiceList(); if (T.synth.onvoiceschanged !== undefined) { T.synth.onvoiceschanged = populateVoiceList; } }, speak: () => { const T = ToolModules.textToSpeech; if (T.synth.speaking) { T.synth.cancel(); // Cancel previous before speaking new } const text = document.getElementById('ttsText').value; if (!text.trim()) { alert("Please enter some text to speak."); return; } const utterThis = new SpeechSynthesisUtterance(text); const selectedVoiceName = document.getElementById('ttsVoice').selectedOptions[0]?.getAttribute('data-name'); if (selectedVoiceName) { utterThis.voice = T.voices.find(voice => voice.name === selectedVoiceName); } T.synth.speak(utterThis); }, stop: () => { ToolModules.textToSpeech.synth.cancel(); } }, speechToText: { recognition: null, init: () => { const S = ToolModules.speechToText; const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { document.getElementById('sttStatus').textContent = "Speech recognition not supported in this browser."; document.getElementById('sttStartBtn').disabled = true; return; } S.recognition = new SpeechRecognition(); S.recognition.continuous = false; // Set to true for continuous listening S.recognition.lang = 'en-US'; // Default language S.recognition.interimResults = true; // Show interim results S.recognition.onstart = () => { document.getElementById('sttStatus').textContent = "Listening..."; document.getElementById('sttStartBtn').disabled = true; document.getElementById('sttStopBtn').disabled = false; }; S.recognition.onresult = (event) => { let interimTranscript = ''; let finalTranscript = ''; for (let i = event.resultIndex; i < event.results.length; ++i) { if (event.results[i].isFinal) { finalTranscript += event.results[i][0].transcript; } else { interimTranscript += event.results[i][0].transcript; } } document.getElementById('sttResult').value = finalTranscript + interimTranscript; }; S.recognition.onerror = (event) => { document.getElementById('sttStatus').textContent = `Error: ${event.error}`; document.getElementById('sttStartBtn').disabled = false; document.getElementById('sttStopBtn').disabled = true; }; S.recognition.onend = () => { document.getElementById('sttStatus').textContent = "Status: Idle (stopped)"; document.getElementById('sttStartBtn').disabled = false; document.getElementById('sttStopBtn').disabled = true; }; }, start: () => { const S = ToolModules.speechToText; if (S.recognition) { document.getElementById('sttResult').value = ''; // Clear previous results S.recognition.start(); } else { document.getElementById('sttStatus').textContent = "Speech recognition not initialized."; } }, stop: () => { const S = ToolModules.speechToText; if (S.recognition) { S.recognition.stop(); } } }, jsonFormatter: { format: () => { const input = document.getElementById('jsonInput').value; const outputField = document.getElementById('jsonOutput'); const errorP = document.getElementById('jsonError'); errorP.textContent = ''; try { const parsedJson = JSON.parse(input); outputField.value = JSON.stringify(parsedJson, null, 2); // 2 spaces for indentation } catch (e) { outputField.value = "Invalid JSON: " + e.message; errorP.textContent = "Error: " + e.message; } } }, unitConverter: { units: { length: { meter: 1, kilometer: 1000, centimeter: 0.01, millimeter: 0.001, mile: 1609.34, yard: 0.9144, foot: 0.3048, inch: 0.0254 }, weight: { kilogram: 1, gram: 0.001, milligram: 0.000001, pound: 0.453592, ounce: 0.0283495 }, temperature: { // Special handling needed celsius: 'celsius', fahrenheit: 'fahrenheit', kelvin: 'kelvin' } }, init: () => { const U = ToolModules.unitConverter; const category = document.getElementById('unitCategory').value; const fromSelect = document.getElementById('unitFrom'); const toSelect = document.getElementById('unitTo'); fromSelect.innerHTML = ''; toSelect.innerHTML = ''; for (const unit in U.units[category]) { const option1 = document.createElement('option'); option1.value = unit; option1.textContent = unit.charAt(0).toUpperCase() + unit.slice(1); fromSelect.appendChild(option1); const option2 = document.createElement('option'); option2.value = unit; option2.textContent = unit.charAt(0).toUpperCase() + unit.slice(1); toSelect.appendChild(option2); } if (category === "length") { // Set defaults for length fromSelect.value = "meter"; toSelect.value = "kilometer"; } else if (category === "weight") { fromSelect.value = "kilogram"; toSelect.value = "gram"; } else if (category === "temperature") { fromSelect.value = "celsius"; toSelect.value = "fahrenheit"; } U.convert(); }, convert: () => { const U = ToolModules.unitConverter; const category = document.getElementById('unitCategory').value; const inputValue = parseFloat(document.getElementById('unitInput').value); const fromUnit = document.getElementById('unitFrom').value; const toUnit = document.getElementById('unitTo').value; const outputField = document.getElementById('unitOutput'); if (isNaN(inputValue)) { outputField.value = ""; return; } let result; if (category === 'temperature') { result = U.convertTemperature(inputValue, fromUnit, toUnit); } else { const baseValue = inputValue * U.units[category][fromUnit]; // Convert to base unit (meter or kg) result = baseValue / U.units[category][toUnit]; // Convert from base to target unit } outputField.value = result.toFixed(5); // Adjust precision as needed }, convertTemperature: (value, from, to) => { if (from === to) return value; let celsius; // Convert input to Celsius first if (from === 'fahrenheit') celsius = (value - 32) * 5/9; else if (from === 'kelvin') celsius = value - 273.15; else celsius = value; // Already Celsius // Convert Celsius to target unit if (to === 'fahrenheit') return (celsius * 9/5) + 32; if (to === 'kelvin') return celsius + 273.15; return celsius; // Target is Celsius } }, bmiCalculator: { calculate: () => { const weight = parseFloat(document.getElementById('bmiWeight').value); const heightCm = parseFloat(document.getElementById('bmiHeight').value); const resultDiv = document.getElementById('bmiResult'); if (isNaN(weight) || isNaN(heightCm) || weight <= 0 || heightCm <= 0) { resultDiv.textContent = "Please enter valid positive values for weight and height."; resultDiv.style.display = 'block'; return; } const heightM = heightCm / 100; const bmi = weight / (heightM * heightM); let category = ""; if (bmi < 18.5) category = "Underweight"; else if (bmi < 24.9) category = "Normal weight"; else if (bmi < 29.9) category = "Overweight"; else category = "Obesity"; resultDiv.innerHTML = `

Your BMI:

BMI Value: ${bmi.toFixed(2)}

Category: ${category}

`; resultDiv.style.display = 'block'; } }, timerStopwatch: { stopwatch: { interval: null, startTime: 0, elapsedTime: 0, running: false }, timer: { interval: null, endTime: 0, running: false, duration: 0 }, alarmSound: null, init: () => { const TS = ToolModules.timerStopwatch; TS.alarmSound = document.getElementById('timerAlarm'); // Stopwatch buttons document.getElementById('tsStartBtn').onclick = TS.startStopwatch; document.getElementById('tsStopBtn').onclick = TS.stopStopwatch; document.getElementById('tsResetBtn').onclick = TS.resetStopwatch; // Timer buttons document.getElementById('timerStartBtn').onclick = TS.startTimer; document.getElementById('timerStopBtn').onclick = TS.stopTimer; document.getElementById('timerResetBtn').onclick = TS.resetTimer; TS.updateDisplay(0); // Initial stopwatch display }, formatTime: (ms) => { const totalSeconds = Math.floor(ms / 1000); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; const milliseconds = Math.floor((ms % 1000) / 100); // Tenths of a second return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}:${String(milliseconds).padStart(1,'0')}`; }, formatTimerTime: (ms) => { if (ms < 0) ms = 0; const totalSeconds = Math.floor(ms / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; }, updateDisplay: (timeMs, isTimer = false) => { document.getElementById('tsDisplay').textContent = isTimer ? ToolModules.timerStopwatch.formatTimerTime(timeMs) : ToolModules.timerStopwatch.formatTime(timeMs); }, // Stopwatch startStopwatch: () => { const S = ToolModules.timerStopwatch.stopwatch; if (S.running) return; S.running = true; S.startTime = Date.now() - S.elapsedTime; S.interval = setInterval(() => { S.elapsedTime = Date.now() - S.startTime; ToolModules.timerStopwatch.updateDisplay(S.elapsedTime); }, 100); // Update every 100ms for smooth tenth of second }, stopStopwatch: () => { const S = ToolModules.timerStopwatch.stopwatch; if (!S.running) return; S.running = false; clearInterval(S.interval); S.elapsedTime = Date.now() - S.startTime; // Final accurate time ToolModules.timerStopwatch.updateDisplay(S.elapsedTime); }, resetStopwatch: () => { const S = ToolModules.timerStopwatch.stopwatch; ToolModules.timerStopwatch.stopStopwatch(); // Stop if running S.elapsedTime = 0; ToolModules.timerStopwatch.updateDisplay(0); }, // Timer startTimer: () => { const T = ToolModules.timerStopwatch.timer; const TS = ToolModules.timerStopwatch; if (T.running) return; const hours = parseInt(document.getElementById('timerHours').value) || 0; const minutes = parseInt(document.getElementById('timerMinutes').value) || 0; const seconds = parseInt(document.getElementById('timerSeconds').value) || 0; T.duration = (hours * 3600 + minutes * 60 + seconds) * 1000; if (T.duration <= 0) { alert("Please set a valid timer duration."); return; } T.running = true; T.endTime = Date.now() + T.duration; TS.updateDisplay(T.duration, true); T.interval = setInterval(() => { const timeLeft = T.endTime - Date.now(); if (timeLeft <= 0) { TS.stopTimer(); TS.updateDisplay(0, true); TS.alarmSound.play(); alert("Timer Finished!"); // Optionally stop alarm after a few seconds: setTimeout(() => TS.alarmSound.pause(), 5000); return; } TS.updateDisplay(timeLeft, true); }, 1000); }, stopTimer: () => { const T = ToolModules.timerStopwatch.timer; const TS = ToolModules.timerStopwatch; if (!T.running) return; T.running = false; clearInterval(T.interval); T.duration = T.endTime - Date.now(); // Save remaining duration if (T.duration < 0) T.duration = 0; TS.alarmSound.pause(); TS.alarmSound.currentTime = 0; }, resetTimer: () => { const T = ToolModules.timerStopwatch.timer; const TS = ToolModules.timerStopwatch; TS.stopTimer(); T.duration = 0; document.getElementById('timerHours').value = "0"; document.getElementById('timerMinutes').value = "0"; document.getElementById('timerSeconds').value = "0"; TS.updateDisplay(0, true); // Reset display for timer section } } }; // Expose ToolModules if any tool needs to call another (e.g. audioConverter.bufferToWav) // window.ToolModules = ToolModules; // Not strictly needed if modules are self-contained or pass `this` }); // End DOMContentLoaded ```