feat: add sparkly-text to animate "inspire"
This commit is contained in:
parent
3284eccc6d
commit
3a4a450fbb
3 changed files with 218 additions and 2 deletions
209
public/components/sparkly-text.js
Normal file
209
public/components/sparkly-text.js
Normal 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 }
|
||||
|
|
@ -54,4 +54,7 @@ const baseURL = Astro.site?.toString().slice(0, -1) // ?? 'https://ayo.ayco.io'
|
|||
<link rel="icon" href="favicon.svg" />
|
||||
<link rel="mask-icon" href="mask-icon.svg" color="#000000" />
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
||||
|
||||
<!-- web components -->
|
||||
<script type="module" src="components/sparkly-text.js"></script>
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ const avatarSize = 150
|
|||
<main>
|
||||
<section class="introduction-section">
|
||||
<p>
|
||||
I care about the <em>Web</em>, and I love to <em>create</em> stuff to <em
|
||||
>inspire</em
|
||||
I care about the <em>Web</em>, and I love to <em>create</em> stuff to <sparkly-text
|
||||
><em>inspire</em></sparkly-text
|
||||
> and <em>serve</em> others.
|
||||
<!--
|
||||
<a href="/about">More?</a>
|
||||
|
|
@ -245,4 +245,8 @@ const avatarSize = 150
|
|||
font-size: var(--font-size-base);
|
||||
}
|
||||
}
|
||||
|
||||
sparkly-text {
|
||||
--sparkly-text-color: yellow;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue