@manufosela/convert2webp
Image to WebP conversion utility using Canvas API
Demo code (CodePen-ready HTML, CSS, JS)
HTML (html)
<div class="top-links">
<a href="https://manufosela.dev/utils/">Utils catalog</a>
<a href="https://manufosela.dev/">Main catalog</a>
<a href="https://github.com/manufosela/utils/tree/main/packages/convert2webp">Source</a>
</div>
<demo-theme-toggle></demo-theme-toggle>
<h1>convert2webp</h1>
<p class="subtitle">Image to WebP conversion utility using Canvas API</p>
<div class="options" style="margin-top: 0;">
<div class="option-group">
<label>Quick usage</label>
<pre class="code-block"><code>import { convertToWebP } from '@manufosela/convert2webp';
const result = await convertToWebP(file, 0.8);
</code></pre>
</div>
</div>
<div class="upload-area" id="uploadArea">
<p>Drop images here or click to select</p>
<p style="font-size: 0.875rem; color: #999;">Supports PNG, JPEG, GIF, BMP</p>
<input type="file" id="fileInput" accept="image/*" multiple>
</div>
<div class="options">
<div class="option-group">
<label for="quality">Quality (0-100%)</label>
<input type="range" id="quality" min="1" max="100" value="80">
<span id="qualityValue">80%</span>
</div>
<div class="option-group">
<label for="maxWidth">Max Width (px)</label>
<input type="number" id="maxWidth" placeholder="No limit" min="1">
</div>
<div class="option-group">
<label for="maxHeight">Max Height (px)</label>
<input type="number" id="maxHeight" placeholder="No limit" min="1">
</div>
</div>
<div class="results" id="results"></div> CSS (css)
:root {
--bg: #0c0f14;
--bg-elevated: #141923;
--bg-panel: #171d28;
--border: #262f3f;
--text: #f4f6fb;
--text-muted: #a7b0c2;
--text-dim: #7d879b;
--accent: #ff8a3d;
--accent-strong: #ff6a00;
--accent-soft: rgba(255, 138, 61, 0.16);
--shadow: 0 20px 50px rgba(5, 8, 14, 0.45);
--radius-lg: 22px;
--radius-md: 14px;
--radius-sm: 10px;
--max-width: 1160px;
}
* {
box-sizing: border-box;
}
h1 {
color: var(--accent);
margin-bottom: 0.5rem;
}
.subtitle {
color: var(--text-muted);
margin-bottom: 2rem;
}
.upload-area {
border: 2px dashed var(--border);
border-radius: 8px;
padding: 2rem;
text-align: center;
background: var(--bg-elevated);
transition: border-color 0.2s, background-color 0.2s;
cursor: pointer;
}
.upload-area:hover,
.upload-area.dragover {
border-color: var(--accent);
background: #f8f9ff;
}
.upload-area input {
display: none;
}
.options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin: 1.5rem 0;
padding: 1rem;
background: var(--bg-elevated);
border-radius: 8px;
}
.option-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.option-group label {
font-weight: 500;
color: var(--text-muted);
}
.option-group input {
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 4px;
}
.results {
margin-top: 2rem;
}
.result-card {
background: var(--bg-elevated);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
}
.result-images {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.image-container {
text-align: center;
}
.image-container img {
max-width: 100%;
max-height: 200px;
border-radius: 4px;
border: 1px solid var(--border);
}
.image-container p {
margin: 0.5rem 0 0;
font-size: 0.875rem;
color: var(--text-muted);
}
.top-links {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.top-links a {
font-size: 0.8rem;
color: var(--text-muted);
text-decoration: none;
border: 1px solid var(--border);
padding: 0.35rem 0.75rem;
border-radius: 999px;
}
.stats {
display: flex;
gap: 2rem;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--border);
}
.stat {
text-align: center;
}
.stat-value {
font-size: 1.5rem;
font-weight: bold;
color: var(--text);
}
.stat-label {
font-size: 0.875rem;
color: var(--text-muted);
}
.savings {
color: #28a745;
}
button {
padding: 0.5rem 1rem;
background: var(--accent);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
}
button:hover {
background: var(--accent-strong);
}
.download-btn {
background: #28a745;
}
.download-btn:hover {
background: #1e7e34;
}
.code-block {
background: #0b0f1a;
border: 1px solid #2b3247;
border-radius: 8px;
padding: 12px;
color: #d6d9e6;
font-family: "JetBrains Mono", "Courier New", monospace;
font-size: 0.85rem;
white-space: pre-wrap;
word-break: break-word;
}
:root {
--bg: #0f1117;
--bg-2: #151a26;
--bg-spot-1: #1a2136;
--bg-spot-2: #1d1b34;
--card: #1c2233;
--text: #f3f6ff;
--muted: #b8c0d9;
--line: #2b3247;
--accent: #ffb000;
--accent-2: #00d0ff;
--surface: #0b0f1a;
--code-bg: #0b0f1a;
--code-text: #d6d9e6;
}
:root.light {
--bg: #f5f5f7;
--bg-2: #ffffff;
--bg-spot-1: #f8e9d0;
--bg-spot-2: #e8eef8;
--card: #ffffff;
--text: #1d1d1f;
--muted: #6b7280;
--line: #e5e7eb;
--accent: #ffb000;
--accent-2: #00a7d6;
--surface: #f3f4f6;
--code-bg: #111827;
--code-text: #f9fafb;
}
:root.dark {
--bg: #0f1117;
--bg-2: #151a26;
--bg-spot-1: #1a2136;
--bg-spot-2: #1d1b34;
--card: #1c2233;
--text: #f3f6ff;
--muted: #b8c0d9;
--line: #2b3247;
--accent: #ffb000;
--accent-2: #00d0ff;
--surface: #0b0f1a;
--code-bg: #0b0f1a;
--code-text: #d6d9e6;
}
h1 {
color: var(--accent) !important;
}
.top-links a,
.topbar a {
color: var(--muted) !important;
border-color: var(--line) !important;
}
.top-links a:hover,
.topbar a:hover {
color: var(--text) !important;
}
.card,
.section,
.demo-section,
.panel {
background: var(--card) !important;
border-color: var(--line) !important;
color: var(--text) !important;
}
.code-block,
pre,
code,
.output,
.current-url,
.event-log,
.result-card,
.log {
background: var(--code-bg) !important;
color: var(--code-text) !important;
border-color: var(--line) !important;
}
input,
select,
textarea {
background: var(--surface) !important;
color: var(--text) !important;
border-color: var(--line) !important;
}
button {
background: linear-gradient(120deg, var(--accent), #ff6a00) !important;
color: #111 !important;
}
footer {
color: var(--muted) !important;
}
footer a {
color: var(--text) !important;
} JS (js)
import {
convertToWebP,
isWebPSupported,
createDownloadUrl
} from "https://esm.sh/@manufosela/convert2webp";
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const qualityInput = document.getElementById('quality');
const qualityValue = document.getElementById('qualityValue');
const maxWidthInput = document.getElementById('maxWidth');
const maxHeightInput = document.getElementById('maxHeight');
const results = document.getElementById('results');
// Check WebP support
isWebPSupported().then(supported => {
if (!supported) {
alert('Your browser does not support WebP conversion');
}
});
// Quality slider
qualityInput.addEventListener('input', () => {
qualityValue.textContent = `${qualityInput.value}%`;
});
// Drag and drop
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
async function handleFiles(files) {
const options = {
quality: qualityInput.value / 100,
maxWidth: maxWidthInput.value ? parseInt(maxWidthInput.value) : undefined,
maxHeight: maxHeightInput.value ? parseInt(maxHeightInput.value) : undefined
};
for (const file of files) {
if (!file.type.startsWith('image/')) {
continue;
}
try {
const result = await convertToWebP(file, options);
displayResult(file, result);
} catch (error) {
console.error('Conversion failed:', error);
alert(`Failed to convert ${file.name}: ${error.message}`);
}
}
}
function formatBytes(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}
function displayResult(originalFile, result) {
const originalUrl = URL.createObjectURL(originalFile);
const webpUrl = createDownloadUrl(result.blob);
const savings = ((1 - result.size / originalFile.size) * 100).toFixed(1);
const card = document.createElement('div');
card.className = 'result-card';
card.innerHTML = `
<div class="result-header">
<strong>${originalFile.name}</strong>
<button class="download-btn" onclick="downloadFile('${webpUrl}', '${originalFile.name.replace(/\.[^.]+$/, '.webp')}')">
Download WebP
</button>
</div>
<div class="result-images">
<div class="image-container">
<img src="${originalUrl}" alt="Original">
<p>Original (${formatBytes(originalFile.size)})</p>
</div>
<div class="image-container">
<img src="${webpUrl}" alt="WebP">
<p>WebP (${formatBytes(result.size)})</p>
</div>
</div>
<div class="stats">
<div class="stat">
<div class="stat-value">${result.width} x ${result.height}</div>
<div class="stat-label">Dimensions</div>
</div>
<div class="stat">
<div class="stat-value savings">${savings}%</div>
<div class="stat-label">Size Reduction</div>
</div>
<div class="stat">
<div class="stat-value">${(qualityInput.value)}%</div>
<div class="stat-label">Quality</div>
</div>
</div>
`;
results.insertBefore(card, results.firstChild);
}
// Global download function
window.downloadFile = function(url, filename) {
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
};
class DemoThemeToggle extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.handleClick = this.handleClick.bind(this);
}
connectedCallback() {
const saved = localStorage.getItem('utils-demo-theme');
document.documentElement.classList.remove('dark');
document.documentElement.classList.toggle('light', saved === 'light');
this.render();
}
render() {
const isLight = document.documentElement.classList.contains('light');
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-flex;
}
.toggle {
display: inline-flex;
align-items: center;
border: 1px solid var(--line);
border-radius: 999px;
overflow: hidden;
background: var(--surface);
}
button {
border: none;
background: transparent;
color: var(--muted);
padding: 6px 12px;
font-size: 0.8rem;
cursor: pointer;
font-family: "Space Grotesk", "Trebuchet MS", Arial, sans-serif;
}
button.active {
background: linear-gradient(120deg, var(--accent), #ff6a00);
color: #111;
font-weight: 600;
}
</style>
<div class="toggle" role="group" aria-label="Theme toggle">
<button class="${isLight ? 'active' : ''}" data-theme="light">Light</button>
<button class="${isLight ? '' : 'active'}" data-theme="dark">Dark</button>
</div>
`;
this.shadowRoot.querySelectorAll('button').forEach((btn) => {
btn.addEventListener('click', this.handleClick);
});
}
handleClick(event) {
const theme = event.currentTarget.dataset.theme;
const isLight = theme === 'light';
document.documentElement.classList.toggle('light', isLight);
document.documentElement.classList.remove('dark');
localStorage.setItem('utils-demo-theme', isLight ? 'light' : 'dark');
this.render();
}
}
customElements.define('demo-theme-toggle', DemoThemeToggle);