DEV Community

Cover image for Creando un Custom Element para transformar Imágenes en JavaScript
Javier Sabando
Javier Sabando

Posted on

Creando un Custom Element para transformar Imágenes en JavaScript

Hola, soy @ratapan y hoy quiero compartir con ustedes el primer custom element que he creado. Este elemento Input, que permite tomar un archivo de imagen, cortarlo y darle un nuevo tamaño, pueden personalizarlo tanto como quieran, es primera vez que hago uno, así que si tienen sugerencias, les agradecería.

¿Qué es un Custom Element?

Los Custom Elements son una parte fundamental de la suite de tecnologías de Web Components. Nos permiten crear nuestros propios elementos HTML, con su funcionalidad y estilos encapsulados, siendo todo esto nativo de la plataforma web.

Presentando rtp-crop

rtp-crop es un custom element que desarrollé para manipular imágenes directamente en el navegador. Puede tomar una imagen, cortarla a un tamaño específico y cambiar su resolución.

Código del Custom Element

El código de rtp-crop se divide en varias partes. Primero, tenemos los estilos CSS, que se tienen que insertar, ya que utiliza el shadow dom para crear el custom element, además podemos usar variables de CSS para personalizar todos a la vez:

//_rtp_image_crop.js
const styles = (height) => `
.rtp-crop{
  min-height: 30px;
  max-height:100%;
  height: ${height};
  min-width: 50px;
  max-width:100%;
  display: flex;
  justify-content: center;
  align-items: center;
  position:relative;
}
.rtp-crop__label{
  cursor: pointer;
  padding: var(--input-padding);
  border-radius: var(--input-radius);
  color: var(--c-font);
  background-color: var(--c-i-c);
  margin-right:auto;
}
.rtp-crop__input{
  display: none;
}
.rtp-crop__preview{
  max-width:100%;
  max-height:100%;
}`;
Enter fullscreen mode Exit fullscreen mode

Luego, la función principal que cambia la resolución de la imagen y su extensión.

/**
 * Reduces the resolution of an image file to specified dimensions.
 *
 * This function takes an image file as input along with the desired new width
 * and height. It returns a Promise that resolves with an object containing the
 * original image size, the new image size, and the reduced resolution image file.
 * If the input image is not square, it will be cropped to fit the specified
 * dimensions. The new image is returned in the WebP format.
 *
 * @param {File} imageFile - The image file to reduce resolution of.
 * @param {string} type - The type of the final image.
 * @param {number} newWidth - The desired width for the new image.
 * @param {number} newHeight - The desired height for the new image.
 * @returns {Promise<{img?: File, name?: string}>} A Promise that resolves with an object containing the new image file (`img`) and the name. The Promise is rejected if there is an error in processing the image.
 */
export async function reduceImageResolution(
  imageFile,
  type,
  newWidth,
  newHeight
) {
  return new Promise((resolve, reject) => {
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    const image = new Image();

    image.src = URL.createObjectURL(imageFile);
    image.onload = () => {
      const originalWidth = image.naturalWidth;
      const originalHeight = image.naturalHeight;

      const size = Math.min(originalWidth, originalHeight);

      canvas.width = newWidth;
      canvas.height = newHeight;

      const sourceX = (originalWidth - size) / 2;
      const sourceY = (originalHeight - size) / 2;
      const sourceWidth = size;
      const sourceHeight = size;

      ctx.drawImage(
        image,
        sourceX,
        sourceY,
        sourceWidth,
        sourceHeight,
        0,
        0,
        newWidth,
        newHeight
      );

      canvas.toBlob(
        (blob) => {
          if (blob) {
            const reducedImage = new File([blob], imageFile.name, {
              type:`image/${type}`,
            });
            resolve({
              img: reducedImage,
            });
          } else {
            reject(new Error("Error processing image"));
          }
        },
        imageFile.type,
        1
      );
    };

    image.onerror = () => {
      reject(new Error("Error loading image"));
    };
  });
}
Enter fullscreen mode Exit fullscreen mode

La lógica, en el caso de que el valor de salida "size" la relación es de 1:1, lo que hará será tomar "File" y en caso de ser más ancho que alto, eliminará los costados hasta quedar centrado y en caso de ser más alto que ancho, se eliminaría arriba y abajo para dejar el centro como en estos casos, además de cambiar por defecto el formato a Webp o al de elección:

input output
Imagen de ejemplo, más alta que ancha Output de la imagen más alta que ancha
Imagen de ejemplo, más ancha que alta Output de la imagen más ancha que alta

Y finalmente, la definición del custom element, este tiene diferentes valores default que podemos cambiar desde la declaración HTML de nuestro elemento, también a través del set value(val) generamos un evento tipo change para utilizar el valor de nuestro elemento:

///_rtp_image_crop.js
class RtpCropInput extends HTMLElement {
  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: "open" });
    this._value = null;

    // Styles
    this.myStyles = document.createElement("style");
    this.myStyles.innerHTML = styles();

    // Container
    this.inputContainer = document.createElement("div");

    // Image
    this.inputPreview = document.createElement("img");
    this.inputPreview.alt = "Preview input image";
    this.inputPreview.loading = "lazy";
    this.inputPreview.style.display = "none";

    // Label
    this.inputLabel = document.createElement("label");
    this.inputLabel.textContent = "Select an image";

    // Input
    this.inputThis = document.createElement("input");
    this.inputThis.type = "file";
    this.inputThis.accept = "image/*";
    this.inputThis.id = "file-input"; // Setting an ID for the input
    this.inputLabel.setAttribute("for", "file-input"); // Referencing the input ID in the label

    //clases
    this.inputContainer.className = "rtp-crop";
    this.inputLabel.className = "rtp-crop__label";
    this.inputThis.className = "rtp-crop__input";
    this.inputPreview.className = "rtp-crop__preview";

    // Append children
    this.shadow.appendChild(this.myStyles);
    this.shadow.appendChild(this.inputContainer);
    this.inputContainer.appendChild(this.inputThis);
    this.inputContainer.appendChild(this.inputLabel);
    this.inputContainer.appendChild(this.inputPreview);
  }
  get value() {
    return this._value;
  }
  set value(val) {
    this._value = val;
    this.dispatchEvent(new Event("change"));
  }
  connectedCallback() {
    let size = this.getAttribute("output-size") || 800;
    let type = this.getAttribute("output-type") || "webp";
    this.myStyles.innerHTML = styles(
      this.getAttribute("style-height") || "40px"
    );

    const previewImage = (file) => {
      const reader = new FileReader();
      reader.onload = (e) => {
        this.value = file;
        this.inputPreview.src = e.target.result;
        this.inputPreview.style.display = "inline-block";
      };
      reader.readAsDataURL(file);
    };

    // Event listener for input
    this.inputThis.addEventListener("change", async (ev) => {
      const target = ev.target;
      if (target.files && target.files[0]) {
        const data = await reduceImageResolution(
          target.files[0],
          type,
          size,
          size
        );
        if (data.img) {
          this.inputLabel.innerHTML = `Change image </br> ${
            data.img.name.length > 25
              ? data.img.name.substring(0, 19) + "..."
              : data.img.name
          }`;
          this.processedImage = data.img;
          previewImage(data.img);
        }
      }
    });
  }
}

customElements.define("rtp-crop", RtpCropInput);
Enter fullscreen mode Exit fullscreen mode

Custom elemnt base

Uso en HTML

Para usar rtp-crop, simplemente inclúyelo en tu HTML de la siguiente manera:

<script type="module" src="./webComponents/_rtp_image_crop.js"></script>
<article class="card">   
  <rtp-crop
    id="input-crop"
    output-size="500"
    output-type="webp"
    style-height="100px"/>
</article>   
Enter fullscreen mode Exit fullscreen mode

O de la siguiente manera, dejando los datos por defecto:

<script type="module" src="./webComponents/_rtp_image_crop.js">
<article class="card">   
  <rtp-crop id="input-crop"/>
</article>   
Enter fullscreen mode Exit fullscreen mode

Custom element cargado

Estilos Adicionales y Variables CSS

He definido varios estilos y variables CSS para personalizar la apariencia del elemento:

variables

//_variable.css
:root {
  --header-h: 40px;
  --mx-w: 1000px;

  --input-padding: 2px 4px;
  --input-radius: 4px;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 16px;
  --space-4: 24px;
  --space-5: 32px;
}
:root.dark {
  --c-bg: #222627;
  --c-bg_2: #0a0000;
  --c-font: #ffffff;
  --c-font_2: #fff2d6;
  --c-link: #fff8e8;
  --c-select: #4c463a;

  --c-i-a: #e3c4ad;
  --c-i-b: #dca780;
  --c-i-c: #a07250;
}
Enter fullscreen mode Exit fullscreen mode

style.css

@import "./style/_typography.css";
@import "./style/_variable.css";
@import "./style/_reset.css";

.section {
  height: 100dvh;
  padding: var(--space-2) var(--space-3);
  display: grid;
  place-items: center;
}
.card {
  width: 400px;
  max-width: 500px;
  border: 1px solid var(--c-font_2);
  box-shadow: 4px 4px 8px var(--c-bg_2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--space-2);
  -webkit-border-radius: var(--space-2);
  -moz-border-radius: var(--space-2);
  -ms-border-radius: var(--space-2);
  -o-border-radius: var(--space-2);
}

Enter fullscreen mode Exit fullscreen mode

Integración en JavaScript

Finalmente, el elemento se integra en el archivo JavaScript principal:

//main.js
import './webComponents/_rtp_image_crop.js';
const $ = (sele)=> document.querySelector(sele)

const cropInput = $('#input-crop')

cropInput.addEventListener("change",(event)=>{
  console.log(event.target.value)
})
Enter fullscreen mode Exit fullscreen mode

Conclusión

Crear este custom element ha sido una experiencia de aprendizaje increíble. Espero que rtp-crop sea útil en tus proyectos y te inspire a crear tus propios elementos personalizados.


🐙 GitHub
📸 Instagram
🐈‍⬛ Custom element en mi GitHub

Top comments (2)

Collapse
 
rvera5 profile image
R vera

Espectacular post! increíble trabajo😉⭐

Collapse
 
lucifxr63 profile image
lucifxr63

Buen post! justo lo que nesecitaba en este momento👌👌