diff --git a/public/components/sparkly-text.js b/public/components/sparkly-text.js new file mode 100644 index 0000000..04c2ba6 --- /dev/null +++ b/public/components/sparkly-text.js @@ -0,0 +1,209 @@ +/** + * 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 } diff --git a/src/components/Head.astro b/src/components/Head.astro index f00621c..b1a2b23 100644 --- a/src/components/Head.astro +++ b/src/components/Head.astro @@ -54,4 +54,7 @@ const baseURL = Astro.site?.toString().slice(0, -1) // ?? 'https://ayo.ayco.io' + + + diff --git a/src/pages/index.astro b/src/pages/index.astro index 30c409a..fb9e62d 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -47,8 +47,8 @@ const avatarSize = 150

- I care about the Web, and I love to create stuff to inspireWeb, and I love to create stuff to inspire and serve others.