Cursor Blend Mode

Animación de cursor en evento mousemove aplicando blend-mode a imágenes y elementos html
Cursor Blend Mode

La idea es jugar un poco con el cambio de propiedades blend-mode en la que se cambie visualización de imagen y de cursor. Blend-mode fusiona colores e imágenes de elementos

  • Para imágenes con background-blend-mode
  • Para resto de elementos mix-blend-mode

Una imagen de fondo a la que se le da también color para que "fusione" con las diferentes opciones de blend-mode

const blendModes = [
  'normal',
  'multiply',
  'screen',
  'overlay',
  'darken',
  'lighten',
  'color-dodge',
  'color-burn',
  'hard-light',
  'soft-light',
  'difference',
  'exclusion',
  'hue',
  'saturation',
  'color',
  'luminosity',
];

Y elementos <span> con definición de color de fondo y blend-mode para que "fusione". Se mezcla además con la imagen de fondo ya que queda por encima de la imagen

Los elementos <span> son creados (y eliminados pasado un tiempo) cuando se produce el evento mousemove para dar esa sensación de "persecución" del ratón. Podría ajustarse este evento y aplicarlo para desktop y crear otra versión para "touchables"

Entrando a comentar código

Estilos blend-mode para imagen de fondo

He usado variables CSS para actualizar sus valores desde javascript y probar de manera fácil las combinaciones que se quieran

:root {
  --img-color: rgba(255, 255, 255, 0.5);
  --img-blend-mode: multiply;
}
body {
  // color de fondo + imagen de fondo
  background: var(--img-color) url('../img/tree.jpg') no-repeat center;
  background-size: cover;
  // definición de fusión blend-mode
  background-blend-mode: var(--img-blend-mode);
}

Estilos blend-mode para el span "cursor"

También he usado variables CSS. Igual que antes: color de fondo y fusión blend-mode

:root {
  --frame-color: rgba(255, 255, 255, 0.3);
  --frame-blend-mode: overlay;
}
// clase que se añade a los span creados con 'mousemove'
.circle {
  // color de fondo
  background-color: var(--frame-color);
  // definición de fusión blend-mode
  mix-blend-mode: var(--frame-blend-mode);
}

Animación mediante @keyframes

He aplicado animación a los span que "siguen" al cursor aplicando @keyframes a atributos color de fondo, escalado y blur

@keyframes fadeout {
  to {
    background-color: rgba(255, 255, 255, 0);
    transform: scale(var(--scale-end));
    filter: blur(calc(var(--blur-end) * 1px));
  }
}

javascript para animaciones blend-mode

Antes de entrar en detalle voy a comentar las funciones JS que he usado como helpers en todo el código. Comentado en el snippet :)

Helper functions

// Obtener el valor CSS de un atributo de un elemento DOM dado
const getCSSProp = (element, propName) =>
  getComputedStyle(element).getPropertyValue(propName);
// Crear elementos DOM:
//    Tag HTML
//    Atributos
//    Crear más nodos dentro de manera recursiva o texto
const elFactory = (type, attributes, ...children) => {
  const el = document.createElement(type);

  for (key in attributes) {
    el.setAttribute(key, attributes[key]);
  }

  children.forEach((child) => {
    if (typeof child === 'string')
      el.appendChild(document.createTextNode(child));
    else el.appendChild(child);
  });

  return el;
};
// Eliminar nodo cuando termina la animación (listener)
// Se elimina también el listener anterior
const deleteAnimatedNode = (el) => {
  el.addEventListener(
    'animationend',
    () => {
      el.removeEventListener('animationend', this);
      el.remove();
    },
    true
  );
};

Creación de nodos con "mousemove"

He decidido crear nuevos nodos cada vez que el usuario mueve el ratón con la posición X e Y del cursor que son eliminados después de 1 segundo con la modificación CSS de los atributos que he comentado más arriba

const handlerMousemove = (event) => {
  // Obtención de las coordenadas
  const [x, y] = [Math.floor(event.clientX), Math.floor(event.clientY)];
  // Creación de span con
  //  - clase circle
  //  - seteo de Custom Property para posiciones X / Y
  //    con corrección para que se centro del span
  const el = elFactory('span', {
    class: `circle`,
    style: `--x: ${x - CONFIG['--w-frame'] / 2}px; --y: ${
      y - CONFIG['--h-frame'] / 2
    }px`,
  });
  // Añadir nodo al DOM
  body.appendChild(el);
  // Eliminar el nodo cuando termina la animación
  deleteAnimatedNode(el);
};

Librería dat.gui para "debug"

En esta parte es donde más código javascript tenemos para poderlo customizar usando dat.gui

Obtenemos los valores de las variables CSS y creamos un objeto con dichos valores que serán usados más tarde en dat.gui

const body = document.body;
const root = document.documentElement;

// Obtener los valores que tengamos en CSS
const wFrame = getCSSProp(root, '--w-frame');
const hFrame = getCSSProp(root, '--h-frame');
const radius = getCSSProp(root, '--radius');
const blurStart = getCSSProp(root, '--blur-start');
const blurEnd = getCSSProp(root, '--blur-end');
const scaleEnd = getCSSProp(root, '--scale-end');
const imgBlendMode = getCSSProp(root, '--img-blend-mode').trim();
const frameBlendMode = getCSSProp(root, '--frame-blend-mode').trim();

// Se crea un objeto con los valores que serán personalizables
// usando dat.gui
const CONFIG = {
  '--w-frame': +wFrame,
  '--h-frame': +hFrame,
  '--radius': +radius,
  '--blur-start': +blurStart,
  '--blur-end': +blurEnd,
  '--scale-end': +scaleEnd,
  '--img-color': 'rgba(255,255,255,0.5)',
  '--frame-color': 'rgba(255, 255, 255, 0.3)',
  '--img-blend-mode': imgBlendMode,
  '--frame-blend-mode': frameBlendMode,
};

Método auxiliar para usarlo cuando se produzcan cambios en alguna propiedad

const update = (target, link) => (value) => {
  root.style.setProperty(`${target}`, value);
  if (CONFIG.linked && link) {
    CONFIG[link] = value;
    root.style.setProperty(`${link}`, value);
    gui.updateDisplay();
  }
};

Importación de dat.gui

const {
  dat: { GUI },
} = window;
const gui = new GUI();

La librería permite campos para números, colores, select... en cada caso se puede cambiar los parámetros que se pasan, incluso dar nombres (esto no quise personalizarlo, aparecerán los nombres de las variables CSS)

El primer parámetro

// Al método .add pasamos:
//  Objeto de CONFIG + Key, min, max
// Si se producen cambios actualizamos valor de la Key
gui.add(CONFIG, '--w-frame', 5, 200).onChange(update('--w-frame'));
gui.add(CONFIG, '--h-frame', 5, 200).onChange(update('--h-frame'));

// Creamos anidación
const cursor = gui.addFolder('Cursor');
cursor.add(CONFIG, '--radius', 0, 50).onChange(update('--radius'));
cursor.add(CONFIG, '--blur-start', 0, 20).onChange(update('--blur-start'));
cursor.add(CONFIG, '--blur-end', 0, 20).onChange(update('--blur-end'));
// El escalado va de 0 a 1 con steps de 0.1
cursor.add(CONFIG, '--scale-end', 0, 1, 0.1).onChange(update('--scale-end'));
cursor
  .addColor(CONFIG, '--frame-color', 'rgba(255, 255, 255, 0.3)')
  .onChange(update('--frame-color'));
cursor
  .add(CONFIG, '--frame-blend-mode', blendModes)
  .onChange(update('--frame-blend-mode'));

const image = gui.addFolder('Image');
image
  .addColor(CONFIG, '--img-color', 'rgba(255,255,255,0.5)')
  .onChange(update('--img-color'));
image
  .add(CONFIG, '--img-blend-mode', blendModes)
  .onChange(update('--img-blend-mode'));

Enlaces:

Código en GitHub

Codepen

Otra publicación usando blend-mode