INTERACTIVE CARD

Aprende a crear una tarjeta de identificación interactiva con física real para la web, utilizando JavaScript, CSS y Elementor.

INTERACTIVE CARD

En este tutorial no solo vas a maquetar un elemento visual, sino que vas a entender cómo darle comportamiento, peso y movimiento, creando una experiencia que responde al usuario.

¿Qué vamos a construir?

Una tarjeta colgante que:

  • Se comporta como un objeto físico (gravedad y movimiento)
  • Reacciona al scroll
  • Puede ser arrastrada (en desktop)
  • Mantiene una estética limpia y adaptable

Código

HTML

				
					<div class="aure0-rope"
     data-img="https://aure0.com/wp-content/uploads/2026/04/NachoCard.png"
     data-img-width="300"
     data-rope-length="350"
     data-rope-color="#e73d33"
     data-rope-width="10"
     data-x="50">
</div>
				
			

CSS, Javascript

				
					<style>
.aure0-rope {
  position: relative;
  width: 100%;
  margin: 0 auto;
}

.aure0-rope .matter-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
}

.aure0-rope .rope-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 2;
  pointer-events: none;
}

.aure0-rope img {
  position: absolute;
  transform: translate(-50%, -50%);
  will-change: left, top, transform;
  pointer-events: none;
  -webkit-user-drag: none;
  user-select: none;
  filter: drop-shadow(-30px 30px 20px #00000070);
}
</style>

<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>

<script>
document.addEventListener("DOMContentLoaded", () => {

  const ropeStiffness = 0.005;
  const ropeDamping = 0.03;
  const ropeAnimDuration = 500;
  const ropeEase = p => 1 - (1 - p) * (1 - p);

  const isMobile =
    window.matchMedia("(max-width: 768px)").matches &&
    /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);

  const {
    Engine, Render, Runner, Bodies,
    Composite, Constraint, Mouse,
    MouseConstraint, Body, Events
  } = Matter;

  document.querySelectorAll(".aure0-rope").forEach(container => {

    const imgURL = container.dataset.img;
    const ropeLengthTarget = parseInt(container.dataset.ropeLength || "220", 10);
    const ropeColor = container.dataset.ropeColor || "#e73d33";
    const imgW = parseInt(container.dataset.imgWidth || "200", 10);
    const ropeLineWidth = parseFloat(container.dataset.ropeWidth || "6");

    const img = document.createElement("img");
    img.src = imgURL;
    img.style.width = imgW + "px";
    img.draggable = false;
    container.appendChild(img);

    img.onload = () => {

      const imgH = img.naturalHeight * (imgW / img.naturalWidth);
      const totalHeight = ropeLengthTarget + imgH + 10;
      container.style.height = totalHeight + "px";

      const ropeCanvas = document.createElement("canvas");
      ropeCanvas.className = "rope-canvas";
      container.appendChild(ropeCanvas);

      const width = container.clientWidth;
      const height = totalHeight;

      const engine = Engine.create();
      engine.world.gravity.y = 1;

      const render = Render.create({
        element: container,
        engine,
        options: {
          width,
          height,
          wireframes: false,
          background: "transparent"
        }
      });

      Render.run(render);
      Runner.run(Runner.create(), engine);

      render.canvas.classList.add("matter-canvas");

      const ctx = ropeCanvas.getContext("2d");
      const dpr = window.devicePixelRatio || 1;

      ropeCanvas.width = width * dpr;
      ropeCanvas.height = height * dpr;
      ropeCanvas.style.width = width + "px";
      ropeCanvas.style.height = height + "px";

      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

      const posX = (width * (parseFloat(container.dataset.x || "50"))) / 100;
      const startY = 30 + imgH / 2;

      const box = Bodies.rectangle(posX, startY, imgW, imgH, {
        frictionAir: 0.03,
        restitution: 0.02,
        render: { visible: false }
      });

      Composite.add(engine.world, box);

      const rope = Constraint.create({
        pointA: { x: posX, y: 0 },
        bodyB: box,
        length: 40,
        stiffness: ropeStiffness,
        damping: ropeDamping,
        render: { visible: false }
      });

      Composite.add(engine.world, rope);

      if (!isMobile) {
        const mouse = Mouse.create(render.canvas);
        const mouseConstraint = MouseConstraint.create(engine, {
          mouse,
          constraint: {
            stiffness: 0.015,
            damping: 0.2,
            render: { visible: false }
          }
        });
        Composite.add(engine.world, mouseConstraint);
      }

      function drawRope() {
        ctx.clearRect(0, 0, width, height);

        const p0 = rope.pointA;
        const p1 = { x: box.position.x, y: box.position.y - imgH / 2 };

        ctx.lineWidth = ropeLineWidth;
        ctx.strokeStyle = ropeColor;
        ctx.lineCap = "round";

        const midX = (p0.x + p1.x) / 2;
        const curve = Math.min(120, Math.abs(p1.y - p0.y) * 0.4 + 40);

        ctx.beginPath();
        ctx.moveTo(p0.x, p0.y);
        ctx.bezierCurveTo(
          midX, p0.y + curve,
          midX, p1.y - curve * 0.6,
          p1.x, p1.y
        );
        ctx.stroke();
      }

      Events.on(engine, "afterUpdate", () => {
        img.style.left = box.position.x + "px";
        img.style.top = box.position.y + "px";
        img.style.transform =
          `translate(-50%, -50%) rotate(${box.angle}rad)`;
        drawRope();
      });

      function animateRopeLength(from, to) {
        const start = performance.now();
        function tick(t) {
          const p = Math.min(1, (t - start) / ropeAnimDuration);
          rope.length = from + (to - from) * ropeEase(p);
          if (p < 1) requestAnimationFrame(tick);
        }
        requestAnimationFrame(tick);
      }

      const observer = new IntersectionObserver(entries => {
        if (entries[0].isIntersecting) {
          animateRopeLength(40, ropeLengthTarget);
          Body.applyForce(box, box.position, {
            x: (Math.random() - 0.5) * 0.004,
            y: 0.015
          });
          observer.disconnect();
        }
      }, { threshold: 0.35 });

      observer.observe(container);

      if (!isMobile) {
        window.addEventListener("resize", () => location.reload());
      }
    };
  });
});
</script>
				
			

Explicación

Estructura base (HTML)

Este es el contenedor donde vivirá todo el sistema:

 
				
					<div class="aure0-rope" style="height:0px; width:100%;" 
     data-img="https://aure0.com/wp-content/uploads/2026/04/NachoCard.png"
    data-img-width="300"
     data-rope-length="350"
     data-rope-color="#e73d33"
    data-rope-width="10"
     data-x=50>
</div>

				
			

Qué está pasando aquí?

En lugar de código rígido, usamos data-* para controlar:

  • Imagen
  • Tamaño
  • Largo de la cuerda
  • Color
  • Posición

Esto hace que el componente sea reutilizable y escalable.

 


 

Estilos (CSS)

Definimos una base mínima para posicionar los elementos correctamente:

				
					<style>
.aure0-rope {
  position: relative;
  width: 100%;
}

.aure0-rope img {
  position: absolute;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
</style>
				
			

Motor de física

Importamos Matter.js:

En lugar de animaciones falsas, usamos un motor físico real:

  • gravedad
  • inercia
  • colisiones
  • fuerzas
				
					<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
				
			

Lógica principal (JavaScript)

Aquí es donde se construye el comportamiento.

Creamos el objeto (la tarjeta)

				
					const box = Bodies.rectangle(posX, startY, imgW, imgH, {
  frictionAir: 0.03,
  restitution: 0.02
});
				
			

Démosle forma a tu idea

Cada proyecto comienza con una intuición. Queremos entender la tuya para dar una estructura visual con propósito, claridad y coherencia.