/** * Thanks to sparkly-text web component by Stefan Judis: https://github.com/stefanjudis/sparkly-text */ let sheet let sparkleTemplate // https://caniuse.com/mdn-api_cssstylesheet_replacesync const supportsConstructableStylesheets = 'replaceSync' in CSSStyleSheet.prototype const motionOK = window.matchMedia('(prefers-reduced-motion: no-preference)') class SparklyText extends HTMLElement { #numberOfSparkles = 3 #sparkleSvg = `` #css = ` :host { --_sparkle-base-size: var(--sparkly-text-size, 1em); --_sparkle-base-animation-length: var(--sparkly-text-animation-length, 1.5s); --_sparkle-base-color: var(--sparkly-text-color, #4ab9f8); position: relative; z-index: 0; } svg { position: absolute; z-index: -1; width: var(--_sparkle-base-size); height: var(--_sparkle-base-size); transform-origin: center; pointer-events: none; } @media (prefers-reduced-motion: no-preference) { svg { animation: sparkle-spin var(--_sparkle-base-animation-length) linear infinite; } svg.rainbow path { animation: rainbow-colors calc(var(--_sparkle-base-animation-length) * 2) linear infinite; } } svg path { fill: var(--_sparkle-base-color); } @keyframes rainbow-colors { 0%, 100% { fill: #ff0000; } 14% { fill: #ff8000; } 28% { fill: #ffff00; } 42% { fill: #00ff00; } 56% { fill: #0000ff; } 70% { fill: #4b0082; } 84% { fill: #8f00ff; } } @keyframes sparkle-spin { 0% { scale: 0; opacity: 0; rotate: 0deg; } 50% { scale: 1; opacity: 1; } 100% { scale: 0; opacity: 0; rotate: 180deg; } } ` static register() { if ('customElements' in window) { window.customElements.define('sparkly-text', SparklyText) } } generateCss() { if (!sheet) { if (supportsConstructableStylesheets) { sheet = new CSSStyleSheet() sheet.replaceSync(this.#css) } else { sheet = document.createElement('style') sheet.textContent = this.#css } } if (supportsConstructableStylesheets) { this.shadowRoot.adoptedStyleSheets = [sheet] } else { this.shadowRoot.append(sheet.cloneNode(true)) } } connectedCallback() { const needsSparkles = motionOK.matches || !this.shadowRoot if (!this.shadowRoot) { this.attachShadow({ mode: 'open' }) this.generateCss() this.shadowRoot.append(document.createElement('slot')) } if (needsSparkles) { this.#numberOfSparkles = parseInt( this.getAttribute('number-of-sparkles') || `${this.#numberOfSparkles}`, 10 ) if (Number.isNaN(this.#numberOfSparkles)) { throw new Error(`Invalid number-of-sparkles value`) } this.cleanupSparkles() this.addSparkles() } motionOK.addEventListener('change', this.motionOkChange) window.addEventListener('popstate', this.handleNavigation) window.addEventListener('pageshow', this.handlePageShow) } disconnectedCallback() { motionOK.removeEventListener('change', this.motionOkChange) window.removeEventListener('popstate', this.handleNavigation) window.removeEventListener('pageshow', this.handlePageShow) this.cleanupSparkles() } handleNavigation = () => { if (motionOK.matches) { this.cleanupSparkles() this.addSparkles() } } handlePageShow = (event) => { // If the page is being loaded from the bfcache if (event.persisted && motionOK.matches) { this.cleanupSparkles() this.addSparkles() } } cleanupSparkles() { // Remove all existing sparkle SVGs const sparkles = this.shadowRoot.querySelectorAll('svg') sparkles.forEach((sparkle) => sparkle.remove()) } // Declare as an arrow function to get the appropriate 'this' motionOkChange = () => { if (motionOK.matches) { this.addSparkles() } } addSparkles() { for (let i = 0; i < this.#numberOfSparkles; i++) { setTimeout(() => { this.addSparkle((sparkle) => { sparkle.style.top = `calc(${ Math.random() * 110 - 5 }% - var(--_sparkle-base-size) / 2)` sparkle.style.left = `calc(${ Math.random() * 110 - 5 }% - var(--_sparkle-base-size) / 2)` }) }, i * 500) } } addSparkle(update) { if (!sparkleTemplate) { const span = document.createElement('span') span.innerHTML = this.#sparkleSvg sparkleTemplate = span.firstElementChild.cloneNode(true) } const sparkleWrapper = sparkleTemplate.cloneNode(true) // Add rainbow class if --sparkly-text-color is set to 'rainbow' const styles = getComputedStyle(this) if (styles.getPropertyValue('--sparkly-text-color').trim() === 'rainbow') { sparkleWrapper.classList.add('rainbow') } update(sparkleWrapper) this.shadowRoot.appendChild(sparkleWrapper) sparkleWrapper.addEventListener('animationiteration', () => { update(sparkleWrapper) }) } } SparklyText.register() export { SparklyText }