<header class="main-header">
	<div class="boxes">
		<h2>Filter - Resize - Convert</h2>
	</div>
</header>
<main class="main-content">
	<section class="boxes">
		<div class="box">
			<div class="panel">
				<input type="file" id="selectImage" accept="image/*">
				<img id="image">
				<canvas id="canvas"></canvas>
				<button id="save">💾 Save</button>
				<textarea id="output" rows="10"></textarea>
				<button id="downloadImage">💾 Download</button>
			</div>
		</div>
		<div class="box">
			<div class="panel">
				<label for="contrast">Contrast</label>
				<input type="range" min="0" max="200" value="100" class="slider" id="contrast">
				<label for="brightness">Brightness</label>
				<input type="range" min="0" max="200" value="100" class="slider" id="brightness">
				<label for="saturation">Saturation</label>
				<input type="range" min="0" max="200" value="100" class="slider" id="saturation">
				<label for="blur">Blur</label>
				<input type="range" min="0" max="10" step="0.1" value="0" class="slider" id="blur">
				<label for="hue">Hue</label>
				<input type="range" min="0" max="360" value="0" class="slider" id="hue">
				<label for="sepia">Sepia</label>
				<input type="range" min="0" max="100" value="0" class="slider" id="sepia">
				<label for="grayscale">Grayscale</label>
				<input type="range" min="0" max="100" value="0" class="slider" id="grayscale">
				<label for="invert">Invert</label>
				<input type="range" min="0" max="100" value="0" class="slider" id="invert">

				<select name="size" id="imageSize">
					<option value="100">100</option>
					<option value="200">200</option>
					<option value="300">300</option>
					<option value="400" selected>400</option>
				</select>

				<button id="reset">Reset</button>
			</div>
		</div>
	</section>
</main>
*,
*:after,
*:before {
	box-sizing: border-box;
	margin: 0;
	padding: 0;
}

:root {
	--primary: #1b1f24;
	--secondary: white;
	--txt: black;
	--font-family: system-ui, -apple-system, Roboto, Arial, sans-serif;
	--font-size: 1rem;
	--font-weight: 400;
	--line-height: 1.6;
	--padding: 0.5rem 0.8rem;
	--border-radius: 0.2rem;
	--shadow: 5px 5px 0px var(--text);
	--gutter: 0.5rem;
}

body,
html {
	height: 100%;
	position: relative;
}

body {
	background: var(--primary);
	color: var(--txt);
	font-family: var(--font-family);
	font-size: var(--font-size);
	font-weight: var(--font-weight);
	height: 100%;
	line-height: var(--line-height);
	margin: 0 auto;
	overflow: auto;
	padding: 0;
}

.boxes {
	display: flex;
	flex-direction: row;
}

.box {
	width: calc(70% - 10px);
	margin: 5px;
	padding: 5px;
}

.box:last-child {
	width: calc(30% - 10px);
}

img {
	max-width: 100%;
}

button,
textarea,
label,
select,
input {
	width: 100%;
	display: block;
	margin: 10px 0;
}
select,
textarea,
input[type="file"] {
	padding: var(--padding);
	border-radius: var(--border-radius);
	background: color-mix(in srgb, var(--primary), var(--secondary) 15%);
	border: 1px solid color-mix(in srgb, var(--primary), var(--secondary) 2%);
	color: var(--secondary);
}
button {
	display: block;
	width: fit-content;
	cursor: pointer;
	padding: var(--padding);
	border-radius: var(--border-radius);
	background: color-mix(in srgb, var(--primary), var(--secondary) 15%);
	border: 1px solid color-mix(in srgb, var(--primary), var(--secondary) 2%);
	color: var(--secondary);
	transition: background 500ms ease;
	&:is(:hover, :focus) {
		background: color-mix(in srgb, var(--primary), var(--secondary) 23%);
		transition: background 500ms ease;
	}
}

button,
textarea {
	display: none;
	resize: none;
}

textarea::-webkit-scrollbar {
	width: 10px;
	height: 10px;
	background: color-mix(in srgb, var(--primary), var(--secondary) 18%);
}
textarea::-webkit-scrollbar-thumb {
	background: color-mix(in srgb, var(--primary), var(--secondary) 5%);
}

.panel {
	padding: var(--padding);
	border-radius: var(--border-radius);
	background: color-mix(in srgb, var(--primary), var(--secondary) 5%);
	border: 1px solid color-mix(in srgb, var(--primary), var(--secondary) 2%);
	color: var(--secondary);
}

header.main-header {
	background: color-mix(in srgb, var(--primary), var(--secondary) 5%);
	color: var(--secondary);
	padding: 5px 10px;
	height: 3rem;
}

main.main-content {
	height: calc(100% - 3rem);
	background: color-mix(in srgb, var(--primary), var(--secondary) 10%);
}
const _id = (id) => document.getElementById(id);

// Cachear elementos del DOM
const domElements = {
	image: _id("image"),
	input: _id("selectImage"),
	canvas: _id("canvas"),
	sliders: document.querySelectorAll(".slider"),
	imageSize: _id("imageSize"),
	output: _id("output"),
	save: _id("save"),
	reset: _id("reset"),
	downloadImage: _id("downloadImage")
};

// Asignar eventos
domElements.input.addEventListener("change", handleImage);
domElements.sliders.forEach((slider) =>
	slider.addEventListener("input", applyFilters)
);
domElements.imageSize.addEventListener("change", applyFilters);
domElements.save.addEventListener("click", saveImage);
domElements.reset.addEventListener("click", resetFilters);
domElements.downloadImage.addEventListener("click", download);

// Manejar la carga de la imagen
function handleImage(e) {
	const [file] = e.target.files;
	const reader = new FileReader();
	reader.onload = () => {
		domElements.image.src = reader.result;
		domElements.image.onload = () => {
			updateImageSize(domElements.imageSize.valueAsNumber);
			applyFilters();
		};
	};
	reader.readAsDataURL(file);
}

// Actualizar tamaño de la imagen
function updateImageSize(newWidth) {
	const { naturalWidth, naturalHeight } = domElements.image;
	const aspectRatio = naturalWidth / naturalHeight;
	const { width, height } = calculateNewDimensions(newWidth, aspectRatio);
	domElements.image.style.width = `${width}px`;
	domElements.canvas.width = width;
	domElements.canvas.height = height;
}

// Calcular nuevas dimensiones
function calculateNewDimensions(newWidth, aspectRatio) {
	return { width: newWidth, height: newWidth / aspectRatio };
}

// Aplicar filtros a la imagen
function applyFilters() {
	const ctx = domElements.canvas.getContext("2d");
	const newWidth =
		parseInt(domElements.imageSize.value, 10) || domElements.imageSize.value;
	const { width, height } = calculateNewDimensions(
		newWidth,
		domElements.image.naturalWidth / domElements.image.naturalHeight
	);

	domElements.canvas.width = width;
	domElements.canvas.height = height;
	ctx.clearRect(0, 0, width, height);

	const filters = [
		`contrast(${getValue("contrast")}%)`,
		`brightness(${getValue("brightness")}%)`,
		`saturate(${getValue("saturation")}%)`,
		`blur(${getValue("blur")}px)`,
		`hue-rotate(${getValue("hue")}deg)`,
		`sepia(${getValue("sepia")}%)`,
		`grayscale(${getValue("grayscale")}%)`,
		`invert(${getValue("invert")}%)`
	].join(" ");

	ctx.filter = filters;
	ctx.drawImage(domElements.image, 0, 0, width, height);

	domElements.image.style.display = "none";
	domElements.canvas.style.display = "initial";
	domElements.save.style.display = "block";
	domElements.reset.style.display = "block";
	domElements.downloadImage.style.display = "none";
}

// Obtener valor de los filtros
function getValue(id) {
	return _id(id).value;
}

// Guardar la imagen procesada
function saveImage() {
	domElements.output.value = domElements.canvas.toDataURL("image/png");
	domElements.output.style.display = "initial";
	domElements.downloadImage.style.display = "block";
}

// Resetear los filtros
function resetFilters() {
	const resetValues = {
		contrast: "100",
		brightness: "100",
		saturation: "100",
		blur: "0",
		hue: "0",
		sepia: "0",
		grayscale: "0",
		invert: "0"
	};

	Object.keys(resetValues).forEach((key) => (_id(key).value = resetValues[key]));
	domElements.imageSize.value = "400";
	applyFilters();
}

// Descargar la imagen
function download() {
	saveDataUriToFile(domElements.output.value, "output.png");
}

// Guardar DataURI como archivo
function saveDataUriToFile(dataUri, filename) {
	const blob = dataURItoBlob(dataUri);
	const url = URL.createObjectURL(blob);
	const link = document.createElement("a");
	link.href = url;
	link.download = filename;
	link.style.display = "none";
	document.body.appendChild(link);
	link.click();
	document.body.removeChild(link);
	URL.revokeObjectURL(url);
}

// Convertir DataURI a Blob
function dataURItoBlob(dataURI) {
	const byteString = atob(dataURI.split(",")[1]);
	const mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0];
	const ab = new ArrayBuffer(byteString.length);
	const ia = new Uint8Array(ab);
	for (let i = 0; i < byteString.length; i++) {
		ia[i] = byteString.charCodeAt(i);
	}
	return new Blob([ab], { type: mimeString });
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.