Created
February 5, 2026 13:20
-
-
Save lovelaced/9aff050f1885d09497371c403272d5f0 to your computer and use it in GitHub Desktop.
b&w logo converter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useState, useRef, useCallback, useEffect } from 'react'; | |
| function calculateOtsuThreshold(imageData) { | |
| const histogram = new Array(256).fill(0); | |
| const data = imageData.data; | |
| let totalPixels = 0; | |
| for (let i = 0; i < data.length; i += 4) { | |
| if (data[i + 3] > 0) { | |
| const gray = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]); | |
| histogram[gray]++; | |
| totalPixels++; | |
| } | |
| } | |
| if (totalPixels === 0) return 128; | |
| let sum = 0; | |
| for (let i = 0; i < 256; i++) sum += i * histogram[i]; | |
| let sumB = 0, wB = 0, maxVariance = 0, threshold = 128; | |
| for (let t = 0; t < 256; t++) { | |
| wB += histogram[t]; | |
| if (wB === 0) continue; | |
| const wF = totalPixels - wB; | |
| if (wF === 0) break; | |
| sumB += t * histogram[t]; | |
| const mB = sumB / wB; | |
| const mF = (sum - sumB) / wF; | |
| const variance = wB * wF * (mB - mF) * (mB - mF); | |
| if (variance > maxVariance) { | |
| maxVariance = variance; | |
| threshold = t; | |
| } | |
| } | |
| return threshold; | |
| } | |
| function processImage(canvas, ctx, img, settings) { | |
| const { threshold, autoThreshold, softness, invert, transparentBg, transparentColor, round, roundness, padding } = settings; | |
| const srcWidth = img.naturalWidth || img.width; | |
| const srcHeight = img.naturalHeight || img.height; | |
| const maxDim = Math.max(srcWidth, srcHeight); | |
| const paddingPx = round ? Math.round(maxDim * (padding / 100)) : 0; | |
| if (round) { | |
| canvas.width = maxDim + paddingPx * 2; | |
| canvas.height = maxDim + paddingPx * 2; | |
| } else { | |
| canvas.width = srcWidth; | |
| canvas.height = srcHeight; | |
| } | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| const offsetX = round ? (canvas.width - srcWidth) / 2 : 0; | |
| const offsetY = round ? (canvas.height - srcHeight) / 2 : 0; | |
| ctx.drawImage(img, offsetX, offsetY); | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| const data = imageData.data; | |
| const grayValues = new Float32Array(canvas.width * canvas.height); | |
| for (let i = 0; i < data.length; i += 4) { | |
| grayValues[i / 4] = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]; | |
| } | |
| const finalThreshold = autoThreshold ? calculateOtsuThreshold(imageData) : threshold; | |
| const centerX = canvas.width / 2; | |
| const centerY = canvas.height / 2; | |
| const maskRadius = Math.min(canvas.width, canvas.height) / 2; | |
| const softRange = softness * 2.5; | |
| for (let i = 0; i < data.length; i += 4) { | |
| const pixelIndex = i / 4; | |
| const x = pixelIndex % canvas.width; | |
| const y = Math.floor(pixelIndex / canvas.width); | |
| let maskAlpha = 255; | |
| // Round mask | |
| if (round) { | |
| const dx = x - centerX; | |
| const dy = y - centerY; | |
| if (roundness >= 100) { | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist > maskRadius) { | |
| data[i] = data[i + 1] = data[i + 2] = 0; | |
| data[i + 3] = 0; | |
| continue; | |
| } | |
| if (dist > maskRadius - 1.5) { | |
| maskAlpha = Math.round(255 * Math.max(0, (maskRadius - dist) / 1.5)); | |
| } | |
| } else { | |
| const cornerRadius = (roundness / 100) * maskRadius; | |
| const halfW = canvas.width / 2; | |
| const halfH = canvas.height / 2; | |
| const ax = Math.abs(dx); | |
| const ay = Math.abs(dy); | |
| if (ax > halfW - cornerRadius && ay > halfH - cornerRadius) { | |
| const cx = ax - (halfW - cornerRadius); | |
| const cy = ay - (halfH - cornerRadius); | |
| const cornerDist = Math.sqrt(cx * cx + cy * cy); | |
| if (cornerDist > cornerRadius) { | |
| data[i] = data[i + 1] = data[i + 2] = 0; | |
| data[i + 3] = 0; | |
| continue; | |
| } | |
| if (cornerDist > cornerRadius - 1.5) { | |
| maskAlpha = Math.round(255 * Math.max(0, (cornerRadius - cornerDist) / 1.5)); | |
| } | |
| } | |
| } | |
| } | |
| const originalAlpha = data[i + 3]; | |
| if (originalAlpha < 10) { | |
| data[i] = data[i + 1] = data[i + 2] = 0; | |
| data[i + 3] = 0; | |
| continue; | |
| } | |
| const gray = grayValues[pixelIndex]; | |
| let bw; | |
| if (softness === 0) { | |
| bw = gray < finalThreshold ? 0 : 255; | |
| } else { | |
| const edgeRadius = Math.max(1, Math.ceil(softness / 15)); | |
| let isNearEdge = false; | |
| const myBinary = gray < finalThreshold; | |
| outer: | |
| for (let dy = -edgeRadius; dy <= edgeRadius; dy++) { | |
| for (let dx = -edgeRadius; dx <= edgeRadius; dx++) { | |
| const nx = x + dx; | |
| const ny = y + dy; | |
| if (nx >= 0 && nx < canvas.width && ny >= 0 && ny < canvas.height) { | |
| const nidx = ny * canvas.width + nx; | |
| const neighborBinary = grayValues[nidx] < finalThreshold; | |
| if (neighborBinary !== myBinary) { | |
| isNearEdge = true; | |
| break outer; | |
| } | |
| } | |
| } | |
| } | |
| if (isNearEdge) { | |
| let avgGray = 0; | |
| let count = 0; | |
| const sampleRadius = Math.max(1, Math.ceil(softness / 20)); | |
| for (let dy = -sampleRadius; dy <= sampleRadius; dy++) { | |
| for (let dx = -sampleRadius; dx <= sampleRadius; dx++) { | |
| const nx = x + dx; | |
| const ny = y + dy; | |
| if (nx >= 0 && nx < canvas.width && ny >= 0 && ny < canvas.height) { | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist <= sampleRadius) { | |
| const weight = 1 - (dist / (sampleRadius + 1)); | |
| const nidx = ny * canvas.width + nx; | |
| avgGray += grayValues[nidx] * weight; | |
| count += weight; | |
| } | |
| } | |
| } | |
| } | |
| avgGray /= count; | |
| const distFromThreshold = avgGray - finalThreshold; | |
| const t = 1 / (1 + Math.exp(-distFromThreshold / (softRange / 3))); | |
| bw = Math.round(t * 255); | |
| } else { | |
| bw = gray < finalThreshold ? 0 : 255; | |
| } | |
| } | |
| if (invert) bw = 255 - bw; | |
| // Calculate final alpha | |
| let finalAlpha = Math.min(originalAlpha, maskAlpha); | |
| if (transparentBg) { | |
| if (transparentColor === 'white') { | |
| // White becomes transparent, black stays opaque | |
| const colorAlpha = 255 - bw; | |
| finalAlpha = Math.round((finalAlpha / 255) * colorAlpha); | |
| data[i] = data[i + 1] = data[i + 2] = 0; | |
| } else { | |
| // Black becomes transparent, white stays opaque | |
| const colorAlpha = bw; | |
| finalAlpha = Math.round((finalAlpha / 255) * colorAlpha); | |
| data[i] = data[i + 1] = data[i + 2] = 255; | |
| } | |
| } else { | |
| data[i] = data[i + 1] = data[i + 2] = bw; | |
| } | |
| data[i + 3] = finalAlpha; | |
| } | |
| ctx.putImageData(imageData, 0, 0); | |
| return finalThreshold; | |
| } | |
| export default function LogoConverter() { | |
| const [image, setImage] = useState(null); | |
| const [imageName, setImageName] = useState(''); | |
| const [settings, setSettings] = useState({ | |
| threshold: 128, | |
| autoThreshold: true, | |
| softness: 40, | |
| invert: false, | |
| transparentBg: true, | |
| transparentColor: 'white', // 'white' or 'black' | |
| round: false, | |
| roundness: 100, | |
| padding: 0 | |
| }); | |
| const [computedThreshold, setComputedThreshold] = useState(128); | |
| const [previewKey, setPreviewKey] = useState(0); | |
| const [previewBg, setPreviewBg] = useState('dark'); | |
| const [dragOver, setDragOver] = useState(false); | |
| const originalCanvasRef = useRef(null); | |
| const processedCanvasRef = useRef(null); | |
| const fileInputRef = useRef(null); | |
| const processCurrentImage = useCallback(() => { | |
| if (!image || !processedCanvasRef.current) return; | |
| const ctx = processedCanvasRef.current.getContext('2d'); | |
| const threshold = processImage(processedCanvasRef.current, ctx, image, settings); | |
| setComputedThreshold(threshold); | |
| setPreviewKey(k => k + 1); | |
| }, [image, settings]); | |
| useEffect(() => { | |
| processCurrentImage(); | |
| }, [processCurrentImage]); | |
| useEffect(() => { | |
| if (!image || !originalCanvasRef.current) return; | |
| const canvas = originalCanvasRef.current; | |
| canvas.width = image.naturalWidth || image.width; | |
| canvas.height = image.naturalHeight || image.height; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.drawImage(image, 0, 0); | |
| }, [image]); | |
| const handleFile = (file) => { | |
| if (!file || !file.type.startsWith('image/')) return; | |
| setImageName(file.name); | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| const img = new Image(); | |
| img.onload = () => setImage(img); | |
| img.src = e.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| }; | |
| const handleDrop = (e) => { | |
| e.preventDefault(); | |
| setDragOver(false); | |
| handleFile(e.dataTransfer.files[0]); | |
| }; | |
| const handleExport = (format) => { | |
| if (!processedCanvasRef.current) return; | |
| const link = document.createElement('a'); | |
| const baseName = imageName.replace(/\.[^/.]+$/, '') || 'logo'; | |
| link.download = `${baseName}-bw.${format}`; | |
| link.href = processedCanvasRef.current.toDataURL(`image/${format}`, 1.0); | |
| link.click(); | |
| }; | |
| const updateSetting = (key, value) => { | |
| setSettings(prev => ({ ...prev, [key]: value })); | |
| }; | |
| const PreviewCanvas = ({ size, className }) => ( | |
| <canvas | |
| key={`preview-${size}-${previewKey}`} | |
| width={size} | |
| height={size} | |
| ref={el => { | |
| if (el && processedCanvasRef.current) { | |
| const ctx = el.getContext('2d'); | |
| ctx.clearRect(0, 0, size, size); | |
| ctx.drawImage(processedCanvasRef.current, 0, 0, size, size); | |
| } | |
| }} | |
| className={className} | |
| /> | |
| ); | |
| return ( | |
| <div className="min-h-screen bg-zinc-950 text-zinc-100 p-6"> | |
| <div className="max-w-6xl mx-auto"> | |
| <div className="mb-8"> | |
| <h1 className="text-2xl font-semibold mb-2">Image → Logo</h1> | |
| <p className="text-zinc-400">Turn any image into a high-contrast logo</p> | |
| </div> | |
| {!image ? ( | |
| <div | |
| onDrop={handleDrop} | |
| onDragOver={(e) => { e.preventDefault(); setDragOver(true); }} | |
| onDragLeave={() => setDragOver(false)} | |
| onClick={() => fileInputRef.current?.click()} | |
| className={`border-2 border-dashed rounded-xl p-16 text-center cursor-pointer transition-all ${ | |
| dragOver ? 'border-white bg-zinc-900' : 'border-zinc-700 hover:border-zinc-500' | |
| }`} | |
| > | |
| <div className="text-5xl mb-4">↓</div> | |
| <p className="text-lg mb-2">Drop an image here</p> | |
| <p className="text-zinc-500">or click to browse</p> | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| accept="image/*" | |
| onChange={(e) => handleFile(e.target.files[0])} | |
| className="hidden" | |
| /> | |
| </div> | |
| ) : ( | |
| <div className="grid lg:grid-cols-3 gap-6"> | |
| <div className="lg:col-span-2 space-y-4"> | |
| <div className="grid md:grid-cols-2 gap-4"> | |
| <div className="bg-zinc-900 rounded-xl p-4"> | |
| <div className="text-sm text-zinc-500 mb-3">Original</div> | |
| <div className="bg-[repeating-conic-gradient(#333_0%_25%,#222_0%_50%)] bg-[length:16px_16px] rounded-lg flex items-center justify-center min-h-48 p-4"> | |
| <canvas ref={originalCanvasRef} className="max-w-full max-h-64 object-contain" /> | |
| </div> | |
| </div> | |
| <div className="bg-zinc-900 rounded-xl p-4"> | |
| <div className="text-sm text-zinc-500 mb-3">Processed</div> | |
| <div className="bg-[repeating-conic-gradient(#333_0%_25%,#222_0%_50%)] bg-[length:16px_16px] rounded-lg flex items-center justify-center min-h-48 p-4"> | |
| <canvas ref={processedCanvasRef} className="max-w-full max-h-64 object-contain" /> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Contextual previews */} | |
| <div className="bg-zinc-900 rounded-xl p-4"> | |
| <div className="flex justify-between items-center mb-4"> | |
| <div className="text-sm text-zinc-500">Preview in context</div> | |
| <div className="flex gap-1"> | |
| {['dark', 'light'].map(id => ( | |
| <button | |
| key={id} | |
| onClick={() => setPreviewBg(id)} | |
| className={`px-2 py-1 text-xs rounded capitalize ${ | |
| previewBg === id ? 'bg-zinc-700 text-white' : 'text-zinc-500 hover:text-zinc-300' | |
| }`} | |
| > | |
| {id} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="space-y-4"> | |
| {/* Browser tab */} | |
| <div className={`rounded-lg overflow-hidden ${previewBg === 'dark' ? 'bg-zinc-800' : 'bg-gray-200'}`}> | |
| <div className={`flex items-center gap-2 px-2 py-1.5 ${previewBg === 'dark' ? 'bg-zinc-700' : 'bg-gray-300'}`}> | |
| <div className={`flex items-center gap-2 px-2 py-1 rounded-t text-xs ${previewBg === 'dark' ? 'bg-zinc-800 text-zinc-300' : 'bg-white text-gray-700'}`}> | |
| <PreviewCanvas size={16} className="w-4 h-4" /> | |
| <span>My Website</span> | |
| <span className={previewBg === 'dark' ? 'text-zinc-500' : 'text-gray-400'}>×</span> | |
| </div> | |
| </div> | |
| <div className={`h-6 ${previewBg === 'dark' ? 'bg-zinc-800' : 'bg-white'}`} /> | |
| </div> | |
| {/* Website header */} | |
| <div className={`rounded-lg overflow-hidden ${previewBg === 'dark' ? 'bg-zinc-800' : 'bg-white'}`}> | |
| <div className={`flex items-center justify-between px-4 py-3 border-b ${previewBg === 'dark' ? 'border-zinc-700' : 'border-gray-200'}`}> | |
| <div className="flex items-center gap-3"> | |
| <PreviewCanvas size={32} className="w-8 h-8" /> | |
| <span className={`font-medium ${previewBg === 'dark' ? 'text-white' : 'text-gray-900'}`}>Brand Name</span> | |
| </div> | |
| <div className={`flex gap-4 text-sm ${previewBg === 'dark' ? 'text-zinc-400' : 'text-gray-600'}`}> | |
| <span>Products</span> | |
| <span>About</span> | |
| <span>Contact</span> | |
| </div> | |
| </div> | |
| <div className="h-12" /> | |
| </div> | |
| {/* App icon + bookmarks */} | |
| <div className="flex gap-6"> | |
| <div className="flex flex-col items-center gap-2"> | |
| <div className={`p-3 rounded-2xl ${previewBg === 'dark' ? 'bg-zinc-800' : 'bg-white shadow-md'}`}> | |
| <PreviewCanvas size={60} className="w-[60px] h-[60px] rounded-xl" /> | |
| </div> | |
| <span className={`text-xs ${previewBg === 'dark' ? 'text-zinc-400' : 'text-gray-600'}`}>App Icon</span> | |
| </div> | |
| <div className="flex-1"> | |
| <div className={`rounded-lg px-3 py-2 ${previewBg === 'dark' ? 'bg-zinc-800' : 'bg-gray-100'}`}> | |
| <div className="flex items-center gap-4"> | |
| <div className="flex items-center gap-2"> | |
| <PreviewCanvas size={16} className="w-4 h-4" /> | |
| <span className={`text-xs ${previewBg === 'dark' ? 'text-zinc-400' : 'text-gray-600'}`}>Your Site</span> | |
| </div> | |
| <div className={`flex items-center gap-2 ${previewBg === 'dark' ? 'text-zinc-600' : 'text-gray-400'}`}> | |
| <div className="w-4 h-4 rounded bg-current opacity-30" /> | |
| <span className="text-xs">Other</span> | |
| </div> | |
| </div> | |
| </div> | |
| <span className={`text-xs mt-1 block ${previewBg === 'dark' ? 'text-zinc-500' : 'text-gray-500'}`}>Bookmarks bar</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex gap-3"> | |
| <button | |
| onClick={() => handleExport('png')} | |
| className="flex-1 bg-white text-black font-medium py-3 px-4 rounded-lg hover:bg-zinc-200 transition-colors" | |
| > | |
| Export PNG | |
| </button> | |
| <button | |
| onClick={() => handleExport('webp')} | |
| className="flex-1 bg-zinc-800 font-medium py-3 px-4 rounded-lg hover:bg-zinc-700 transition-colors" | |
| > | |
| Export WebP | |
| </button> | |
| <button | |
| onClick={() => { setImage(null); setImageName(''); }} | |
| className="bg-zinc-800 font-medium py-3 px-4 rounded-lg hover:bg-zinc-700 transition-colors" | |
| > | |
| Reset | |
| </button> | |
| </div> | |
| </div> | |
| <div className="bg-zinc-900 rounded-xl p-5 space-y-6 h-fit"> | |
| <div className="text-sm font-medium text-zinc-400 uppercase tracking-wide">Settings</div> | |
| <div> | |
| <label className="flex items-center justify-between mb-3"> | |
| <span>Auto Threshold (Otsu)</span> | |
| <button | |
| onClick={() => updateSetting('autoThreshold', !settings.autoThreshold)} | |
| className={`w-12 h-6 rounded-full transition-colors relative ${ | |
| settings.autoThreshold ? 'bg-white' : 'bg-zinc-700' | |
| }`} | |
| > | |
| <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-zinc-900 transition-all ${ | |
| settings.autoThreshold ? 'left-6' : 'left-0.5' | |
| }`} /> | |
| </button> | |
| </label> | |
| {settings.autoThreshold && ( | |
| <div className="text-sm text-zinc-500">Computed threshold: {computedThreshold}</div> | |
| )} | |
| </div> | |
| {!settings.autoThreshold && ( | |
| <div> | |
| <label className="flex justify-between mb-2"> | |
| <span>Manual Threshold</span> | |
| <span className="text-zinc-500">{settings.threshold}</span> | |
| </label> | |
| <input | |
| type="range" | |
| min="0" | |
| max="255" | |
| value={settings.threshold} | |
| onChange={(e) => updateSetting('threshold', parseInt(e.target.value))} | |
| className="w-full accent-white" | |
| /> | |
| </div> | |
| )} | |
| <div> | |
| <label className="flex justify-between mb-2"> | |
| <span>Edge Smoothing</span> | |
| <span className="text-zinc-500">{settings.softness}</span> | |
| </label> | |
| <input | |
| type="range" | |
| min="0" | |
| max="100" | |
| value={settings.softness} | |
| onChange={(e) => updateSetting('softness', parseInt(e.target.value))} | |
| className="w-full accent-white" | |
| /> | |
| </div> | |
| <div> | |
| <label className="flex items-center justify-between"> | |
| <span>Transparent Background</span> | |
| <button | |
| onClick={() => updateSetting('transparentBg', !settings.transparentBg)} | |
| className={`w-12 h-6 rounded-full transition-colors relative ${ | |
| settings.transparentBg ? 'bg-white' : 'bg-zinc-700' | |
| }`} | |
| > | |
| <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-zinc-900 transition-all ${ | |
| settings.transparentBg ? 'left-6' : 'left-0.5' | |
| }`} /> | |
| </button> | |
| </label> | |
| {settings.transparentBg && ( | |
| <div className="mt-3 flex gap-2"> | |
| <button | |
| onClick={() => updateSetting('transparentColor', 'white')} | |
| className={`flex-1 py-2 px-3 rounded text-sm flex items-center justify-center gap-2 ${ | |
| settings.transparentColor === 'white' | |
| ? 'bg-zinc-700 text-white' | |
| : 'bg-zinc-800 text-zinc-400 hover:text-zinc-300' | |
| }`} | |
| > | |
| <div className="w-4 h-4 rounded bg-white border border-zinc-600" /> | |
| White → Clear | |
| </button> | |
| <button | |
| onClick={() => updateSetting('transparentColor', 'black')} | |
| className={`flex-1 py-2 px-3 rounded text-sm flex items-center justify-center gap-2 ${ | |
| settings.transparentColor === 'black' | |
| ? 'bg-zinc-700 text-white' | |
| : 'bg-zinc-800 text-zinc-400 hover:text-zinc-300' | |
| }`} | |
| > | |
| <div className="w-4 h-4 rounded bg-black border border-zinc-600" /> | |
| Black → Clear | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| <div> | |
| <label className="flex items-center justify-between"> | |
| <span>Invert</span> | |
| <button | |
| onClick={() => updateSetting('invert', !settings.invert)} | |
| className={`w-12 h-6 rounded-full transition-colors relative ${ | |
| settings.invert ? 'bg-white' : 'bg-zinc-700' | |
| }`} | |
| > | |
| <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-zinc-900 transition-all ${ | |
| settings.invert ? 'left-6' : 'left-0.5' | |
| }`} /> | |
| </button> | |
| </label> | |
| </div> | |
| <div className="border-t border-zinc-800 pt-6"> | |
| <label className="flex items-center justify-between mb-3"> | |
| <span>Round Mask</span> | |
| <button | |
| onClick={() => updateSetting('round', !settings.round)} | |
| className={`w-12 h-6 rounded-full transition-colors relative ${ | |
| settings.round ? 'bg-white' : 'bg-zinc-700' | |
| }`} | |
| > | |
| <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-zinc-900 transition-all ${ | |
| settings.round ? 'left-6' : 'left-0.5' | |
| }`} /> | |
| </button> | |
| </label> | |
| {settings.round && ( | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="flex justify-between mb-2 text-sm"> | |
| <span className="text-zinc-400">Roundness</span> | |
| <span className="text-zinc-500">{settings.roundness}%</span> | |
| </label> | |
| <input | |
| type="range" | |
| min="10" | |
| max="100" | |
| value={settings.roundness} | |
| onChange={(e) => updateSetting('roundness', parseInt(e.target.value))} | |
| className="w-full accent-white" | |
| /> | |
| </div> | |
| <div> | |
| <label className="flex justify-between mb-2 text-sm"> | |
| <span className="text-zinc-400">Padding</span> | |
| <span className="text-zinc-500">{settings.padding}%</span> | |
| </label> | |
| <input | |
| type="range" | |
| min="0" | |
| max="30" | |
| value={settings.padding} | |
| onChange={(e) => updateSetting('padding', parseInt(e.target.value))} | |
| className="w-full accent-white" | |
| /> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment