Compare commits

...

2 commits

Author SHA1 Message Date
Ayo
7d86adf6f3 feat: set sparkly-text to orange 2026-04-07 16:24:10 +02:00
Ayo
3a4a450fbb feat: add sparkly-text to animate "inspire" 2026-04-05 21:26:28 +02:00
3 changed files with 218 additions and 2 deletions

View file

@ -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 = `<svg width="1200" height="1200" viewBox="0 0 1200 1200" aria-hidden="true">
<path fill="red" d="m611.04 866.16c17.418-61.09 50.25-116.68 95.352-161.42 45.098-44.742 100.94-77.133 162.17-94.062l38.641-10.68-38.641-10.68c-61.227-16.93-117.07-49.32-162.17-94.062-45.102-44.738-77.934-100.33-95.352-161.42l-11.039-38.641-11.039 38.641c-17.418 61.09-50.25 116.68-95.352 161.42-45.098 44.742-100.94 77.133-162.17 94.062l-38.641 10.68 38.641 10.68c61.227 16.93 117.07 49.32 162.17 94.062 45.102 44.738 77.934 100.33 95.352 161.42l11.039 38.641z"/>
</svg>`
#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 }

View file

@ -54,4 +54,7 @@ const baseURL = Astro.site?.toString().slice(0, -1) // ?? 'https://ayo.ayco.io'
<link rel="icon" href="favicon.svg" /> <link rel="icon" href="favicon.svg" />
<link rel="mask-icon" href="mask-icon.svg" color="#000000" /> <link rel="mask-icon" href="mask-icon.svg" color="#000000" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" /> <link rel="apple-touch-icon" href="apple-touch-icon.png" />
<!-- web components -->
<script type="module" src="components/sparkly-text.js"></script>
</head> </head>

View file

@ -47,8 +47,8 @@ const avatarSize = 150
<main> <main>
<section class="introduction-section"> <section class="introduction-section">
<p> <p>
I care about the <em>Web</em>, and I love to <em>create</em> stuff to <em I care about the <em>Web</em>, and I love to <em>create</em> stuff to <sparkly-text
>inspire</em ><em>inspire</em></sparkly-text
> and <em>serve</em> others. > and <em>serve</em> others.
<!-- <!--
<a href="/about">More?</a> <a href="/about">More?</a>
@ -245,4 +245,8 @@ const avatarSize = 150
font-size: var(--font-size-base); font-size: var(--font-size-base);
} }
} }
sparkly-text {
--sparkly-text-color: orange;
}
</style> </style>