UNPKG

chromatic-blur

Version:

A lightweight, zero-dependency JavaScript plugin for creating stunning chromatic aberration blur effects

401 lines (347 loc) 10.7 kB
/** * ChromaticBlur - A vanilla JavaScript plugin for creating chromatic aberration blur effects * * Modern best practices implemented: * - ES6+ module pattern with class-based architecture * - No external dependencies * - Declarative API with sensible defaults * - Method chaining support * - Automatic cleanup and memory management * - Accessible (doesn't interfere with screen readers) * - Performance optimized (reuses SVG filters) * - TypeScript-friendly JSDoc comments * * @version 1.0.0 * @license MIT */ class ChromaticBlur { /** * @typedef {Object} ChromaticBlurOptions * @property {number} [redOffset=5] - Red channel offset in pixels (positive = right) * @property {number} [blueOffset=-5] - Blue channel offset in pixels (negative = left) * @property {number} [blurAmount=3] - Gaussian blur standard deviation * @property {number} [turbulenceFrequency=0.001] - Turbulence noise frequency * @property {number} [displacementScale=50] - Displacement map scale * @property {string} [borderColor='rgba(156, 156, 156, 0.2)'] - Border color * @property {boolean} [addOverlay=true] - Add gradient overlay layer * @property {boolean} [addNoise=true] - Add noise overlay layer */ /** * Default configuration * @type {ChromaticBlurOptions} */ static defaults = { redOffset: 5, blueOffset: -5, blurAmount: 3, turbulenceFrequency: 0.001, displacementScale: 50, borderColor: 'rgba(156, 156, 156, 0.2)', addOverlay: true, addNoise: true }; /** * Track all instances for global cleanup * @type {Set<ChromaticBlur>} */ static instances = new Set(); /** * Creates a new ChromaticBlur instance * @param {HTMLElement|string} element - Target element or CSS selector * @param {ChromaticBlurOptions} options - Configuration options */ constructor(element, options = {}) { // Resolve element this.element = typeof element === 'string' ? document.querySelector(element) : element; if (!this.element) { throw new Error('ChromaticBlur: Invalid element provided'); } // Merge options with defaults this.options = { ...ChromaticBlur.defaults, ...options }; // Generate unique ID for this instance this.id = `chromatic-blur-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Store original styles for cleanup this.originalStyles = { backdropFilter: this.element.style.backdropFilter, WebkitBackdropFilter: this.element.style.WebkitBackdropFilter, position: this.element.style.position, overflow: this.element.style.overflow }; // Initialize this._init(); // Register instance ChromaticBlur.instances.add(this); } /** * Initialize the effect * @private */ _init() { // Ensure SVG container exists this._ensureSVGContainer(); // Create and inject the filter this._createFilter(); // Apply styles to element this._applyStyles(); // Add overlay layers if enabled if (this.options.addOverlay || this.options.addNoise) { this._addOverlays(); } } /** * Ensure global SVG container exists in document * @private */ _ensureSVGContainer() { let container = document.getElementById('chromatic-blur-filters'); if (!container) { container = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); container.id = 'chromatic-blur-filters'; container.style.cssText = 'position:absolute;width:0;height:0;pointer-events:none'; container.setAttribute('aria-hidden', 'true'); const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); container.appendChild(defs); document.body.insertBefore(container, document.body.firstChild); } this.svgContainer = container; this.defs = container.querySelector('defs'); } /** * Create SVG filter for chromatic aberration effect * @private */ _createFilter() { const filter = document.createElementNS('http://www.w3.org/2000/svg', 'filter'); filter.id = this.id; filter.setAttribute('x', '-50%'); filter.setAttribute('y', '-50%'); filter.setAttribute('width', '200%'); filter.setAttribute('height', '200%'); filter.innerHTML = ` <!-- Turbulence for organic noise --> <feTurbulence baseFrequency="${this.options.turbulenceFrequency}" numOctaves="1" type="turbulence" result="turbulence" /> <!-- RED CHANNEL: Offset to the right --> <feOffset in="SourceGraphic" dx="${this.options.redOffset}" dy="0" result="redOffset" /> <feColorMatrix in="redOffset" type="matrix" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="red" /> <!-- GREEN CHANNEL: No offset --> <feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" result="green" /> <!-- BLUE CHANNEL: Offset to the left --> <feOffset in="SourceGraphic" dx="${this.options.blueOffset}" dy="0" result="blueOffset" /> <feColorMatrix in="blueOffset" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0" result="blue" /> <!-- Combine red and green --> <feComposite in="red" in2="green" operator="arithmetic" k2="1" k3="1" result="redGreen" /> <!-- Combine with blue --> <feComposite in="redGreen" in2="blue" operator="arithmetic" k2="1" k3="1" result="colorSplit" /> <!-- Apply displacement for wavy distortion --> <feDisplacementMap in="colorSplit" in2="turbulence" scale="${this.options.displacementScale}" result="displacement" /> <!-- Final blur --> <feGaussianBlur in="displacement" stdDeviation="${this.options.blurAmount}" result="blurred" /> `; this.defs.appendChild(filter); this.filter = filter; } /** * Apply styles to the target element * @private */ _applyStyles() { const filterUrl = `url(#${this.id})`; // Get computed position to check CSS rules, not just inline styles const currentPosition = window.getComputedStyle(this.element).position; const needsPosition = currentPosition === 'static'; Object.assign(this.element.style, { backdropFilter: filterUrl, WebkitBackdropFilter: filterUrl, overflow: 'hidden', boxShadow: `0 0 0 1px ${this.options.borderColor} inset` }); // Only set position if element is static (needs positioning context for overlays) if (needsPosition) { this.element.style.position = 'relative'; } } /** * Add overlay layers for additional depth * @private */ _addOverlays() { // Create container for overlays const overlayContainer = document.createElement('div'); overlayContainer.className = 'chromatic-blur-overlays'; overlayContainer.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; border-radius: inherit; `; // Noise overlay if (this.options.addNoise) { const noise = document.createElement('div'); noise.className = 'chromatic-blur-noise'; noise.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; mix-blend-mode: overlay; background-color: rgba(255, 255, 255, 0); opacity: 0.05; border-radius: inherit; `; overlayContainer.appendChild(noise); } // Gradient overlay if (this.options.addOverlay) { const gradient = document.createElement('div'); gradient.className = 'chromatic-blur-gradient'; gradient.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-image: linear-gradient(225deg, rgba(255, 255, 255, 0.1) 0%, transparent 50%); mix-blend-mode: screen; pointer-events: none; border-radius: inherit; `; overlayContainer.appendChild(gradient); } this.element.insertBefore(overlayContainer, this.element.firstChild); this.overlayContainer = overlayContainer; } /** * Update effect options dynamically * @param {Partial<ChromaticBlurOptions>} newOptions - Options to update * @returns {ChromaticBlur} Returns this for chaining */ update(newOptions) { this.options = { ...this.options, ...newOptions }; // Remove and recreate filter if (this.filter) { this.filter.remove(); } this._createFilter(); this._applyStyles(); return this; } /** * Destroy the effect and cleanup */ destroy() { // Remove filter if (this.filter) { this.filter.remove(); } // Remove overlays if (this.overlayContainer) { this.overlayContainer.remove(); } // Restore original styles Object.assign(this.element.style, this.originalStyles); // Unregister instance ChromaticBlur.instances.delete(this); // Cleanup SVG container if no more instances if (ChromaticBlur.instances.size === 0) { const container = document.getElementById('chromatic-blur-filters'); if (container) { container.remove(); } } } /** * Enable the effect (if previously disabled) * @returns {ChromaticBlur} Returns this for chaining */ enable() { this.element.style.backdropFilter = `url(#${this.id})`; this.element.style.WebkitBackdropFilter = `url(#${this.id})`; return this; } /** * Disable the effect temporarily * @returns {ChromaticBlur} Returns this for chaining */ disable() { this.element.style.backdropFilter = 'none'; this.element.style.WebkitBackdropFilter = 'none'; return this; } /** * Cleanup all instances (useful for SPA cleanup) * @static */ static destroyAll() { ChromaticBlur.instances.forEach(instance => instance.destroy()); } } // Export for different module systems if (typeof module !== 'undefined' && module.exports) { module.exports = ChromaticBlur; } if (typeof define === 'function' && define.amd) { define([], () => ChromaticBlur); } // Global export if (typeof window !== 'undefined') { window.ChromaticBlur = ChromaticBlur; } export default ChromaticBlur;