Fondo interactivo de contenido estelar. trucos CSS
Tuve la suerte de recurrir a Sean Wang el año pasado (swyx) con respecto a la realización de cualquier trabajo TemporarioLa idea era echar un vistazo creativo a lo que hay en el sitio y pensar en algo algunas ideas esto le dará al sitio un pequeño "algo" extra. Esto fue todo un desafío, ya que me considero más un desarrollador que un diseñador. Pero me gusta aprender y alinear el diseño de mi juego.
Una de las ideas que me vino a la mente fue este fondo interactivo de estrellas. Puedes verlo funcionando en esta demostración compartida:
Bloquee el concepto de citas usando poca perspectiva y propiedades CSS personalizadas 😎
Disfruté de la libertad creativa para inventar cosas. @temporalio 🤓
Agregando fantasía a los materiales 🎉
⚒️ @reactjs && @tailwindcss (El sitio es NextJS)
👉 https://t.co/GHtUEcl674 a través de @CodePen pic.twitter.com/s9xP2tRrOx-Jhey 🔨🐻✨ (@jh3yy) 2 de julio de 2021
Lo bueno de este diseño es que está construido como un componente de React. Y es súper configurable en el sentido de que una vez que haya reunido los conceptos básicos, puede personalizarlo. No quieres estrellas • Pon algo más en su lugar. ¿No quieres partículas aleatorias? Colócalos de forma construida. Tienes control total sobre qué doblarlo a tu gusto.
Entonces, ¡veamos cómo podemos crear este componente entrante para su sitio! ¿Las armas preferidas de hoy? Reaccionar, Calcetín Verde y HTML <canvas>
La parte React es completamente opcional, por supuesto, pero tener este fondo interactivo como componente de entrada lo convierte en algo que puede usar en otros proyectos.
Comencemos con el andamiaje de la aplicación principal.
import React from 'https://cdn.skypack.dev/react'
import ReactDOM from 'https://cdn.skypack.dev/react-dom'
import gsap from 'https://cdn.skypack.dev/gsap'
const ROOT_NODE = document.querySelector('#app')
const Starscape = () => <h1>Cool Thingzzz!</h1>
const App = () => <Starscape/>
ReactDOM.render(<App/>, ROOT_NODE)
Lo primero que tenemos que hacer es dibujar un <canvas>
elemento y obtener una referencia a él que podemos usar en React's useEffect
Para aquellos que no usan React, mantengan el enlace a <canvas>
en cambio en una variable.
const Starscape = () => {
const canvasRef = React.useRef(null)
return <canvas ref={canvasRef} />
}
Nuestro <canvas>
algunos estilos también serán necesarios. Para empezar, podemos hacer que el lienzo ocupe el tamaño completo de la ventana gráfica y se sitúe detrás del contenido:
canvas {
position: fixed;
inset: 0;
background: #262626;
z-index: -1;
height: 100vh;
width: 100vw;
}
¡Estupendo! Pero todavía no hay nada que ver.
Necesitamos estrellas en nuestro cielo
Aquí vamos a "mentir" un poco. No dibujaremos la estrella puntiaguda "clásica". Usaremos círculos de diferente opacidad y tamaño.
Dibuja un círculo en un <canvas>
es un caso de tomar contexto de <canvas>
y con la ayuda de arc
función. Dibujemos un círculo, err estrella, en el medio. Podemos hacer eso en React useEffect
:
const Starscape = () => {
const canvasRef = React.useRef(null)
const contextRef = React.useRef(null)
React.useEffect(() => {
canvasRef.current.width = window.innerWidth
canvasRef.current.height = window.innerHeight
contextRef.current = canvasRef.current.getContext('2d')
contextRef.current.fillStyle="yellow"
contextRef.current.beginPath()
contextRef.current.arc(
window.innerWidth / 2, // X
window.innerHeight / 2, // Y
100, // Radius
0, // Start Angle (Radians)
Math.PI * 2 // End Angle (Radians)
)
contextRef.current.fill()
}, [])
return <canvas ref={canvasRef} />
}
Así que lo que tenemos es un gran círculo amarillo:
¡Este es un buen comienzo! El resto de nuestro código se ejecutará en él. useEffect
función. Por lo tanto, la parte React es opcional. Puede extraer este código y usarlo de la forma que desee.
Necesitamos pensar en cómo generaremos un montón de "estrellas" y las representaremos. Vamos a crear LOAD
Esta característica se encargará de la generación de nuestras estrellas, así como el general <canvas>
También podemos mover la lógica de dimensionamiento a <canvas>
dimensionando la lógica en esta función:
const LOAD = () => {
const VMIN = Math.min(window.innerHeight, window.innerWidth)
const STAR_COUNT = Math.floor(VMIN * densityRatio)
canvasRef.current.width = window.innerWidth
canvasRef.current.height = window.innerHeight
starsRef.current = new Array(STAR_COUNT).fill().map(() => ({
x: gsap.utils.random(0, window.innerWidth, 1),
y: gsap.utils.random(0, window.innerHeight, 1),
size: gsap.utils.random(1, sizeLimit, 1),
scale: 1,
alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),
}))
}
Nuestras estrellas ya son un conjunto de objetos, y cada estrella tiene propiedades que determinan sus características, que incluyen:
x
: La posición de la estrella en el eje xy
: La posición de la estrella en el eje ysize
: El tamaño de la estrella, en píxeles.scale
: La escala de la estrella que entrará en juego cuando interactuemos con el componentealpha
: Valor alfa de la estrella, oopacity
que también entrará en juego durante las interacciones
Podemos usar GreenSock random()
método para generar algunos de estos valores. También te estarás preguntando dónde sizeLimit
, defaultAlpha
y densityRatio
viene de. estos son ahora props
podemos pasar a Starscape
componente. Hemos proporcionado algunos valores predeterminados para ellos:
const Starscape = ({ densityRatio = 0.5, sizeLimit = 5, defaultAlpha = 0.5 }) => {
Estrella generada aleatoriamente Object
podría verse así:
{
"x": 1252,
"y": 29,
"size": 4,
"scale": 1,
"alpha": 0.5
}
Pero tenemos que ver estas estrellas y lo hacemos representándolas. Vamos a crear un RENDER
Esta característica orbitará nuestras estrellas y representará a cada una de ellas en <canvas>
utilizando arc
función:
const RENDER = () => {
contextRef.current.clearRect(
0,
0,
canvasRef.current.width,
canvasRef.current.height
)
starsRef.current.forEach(star => {
contextRef.current.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})`
contextRef.current.beginPath()
contextRef.current.arc(star.x, star.y, star.size / 2, 0, Math.PI * 2)
contextRef.current.fill()
})
}
No necesitamos eso ahora clearRect
función para nuestra conversión actual, ya que solo mostramos una vez en un espacio en blanco <canvas>
Pero la limpieza de <canvas>
antes de retratar cualquier cosa no es un mal hábito, y lo necesitaremos mientras lo hacemos canvas
interactivo.
Piense en esta demostración, que muestra el efecto de no borrar entre fotogramas.
Nuestro Starscape
el componente comienza a tomar forma.
ver el código
const Starscape = ({ densityRatio = 0.5, sizeLimit = 5, defaultAlpha = 0.5 }) => {
const canvasRef = React.useRef(null)
const contextRef = React.useRef(null)
const starsRef = React.useRef(null)
React.useEffect(() => {
contextRef.current = canvasRef.current.getContext('2d')
const LOAD = () => {
const VMIN = Math.min(window.innerHeight, window.innerWidth)
const STAR_COUNT = Math.floor(VMIN * densityRatio)
canvasRef.current.width = window.innerWidth
canvasRef.current.height = window.innerHeight
starsRef.current = new Array(STAR_COUNT).fill().map(() => ({
x: gsap.utils.random(0, window.innerWidth, 1),
y: gsap.utils.random(0, window.innerHeight, 1),
size: gsap.utils.random(1, sizeLimit, 1),
scale: 1,
alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),
}))
}
const RENDER = () => {
contextRef.current.clearRect(
0,
0,
canvasRef.current.width,
canvasRef.current.height
)
starsRef.current.forEach(star => {
contextRef.current.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})`
contextRef.current.beginPath()
contextRef.current.arc(star.x, star.y, star.size / 2, 0, Math.PI * 2)
contextRef.current.fill()
})
}
LOAD()
RENDER()
}, [])
return <canvas ref={canvasRef} />
}
Jugar con props
en esta demostración para ver cómo afectan la forma en que se representan las estrellas.
Antes de continuar, es posible que haya notado una extrañeza en la demostración en la que el cambio de tamaño de la vista se distorsiona <canvas>
Como victoria rápida podemos repetir la nuestra LOAD
y RENDER
funciones de resize
. En la mayoría de los casos, también querremos eliminar eso. Podemos agregar el siguiente código al nuestro useEffect
Observe cómo eliminamos el oyente del evento en la división.
// Naming things is hard...
const RUN = () => {
LOAD()
RENDER()
}
RUN()
// Set up event handling
window.addEventListener('resize', RUN)
return () => {
window.removeEventListener('resize', RUN)
}
Estupendo. Ahora, cuando cambiamos el tamaño de la vista, obtenemos una estrella recién generada.
Interacción con el fondo de estrellas.
¡Ahora viene la parte divertida! hagamos esto interactivo.
La idea es que a medida que movemos el puntero por la pantalla, detectemos la proximidad de las estrellas al cursor del mouse, dependiendo de esta proximidad, las estrellas se iluminan y magnifican.
Tendremos que agregar otro detector de eventos para hacer esto. llamémoslo así UPDATE
Esto calculará la distancia entre el puntero y cada estrella, luego calculará la distancia entre el puntero y cada estrella scale
y alpha
valores. Para asegurarnos de que estos valores tuiteados son correctos, podemos usar GreenSock mapRange()
utilidadDe hecho, dentro de la nuestra. LOAD
función, podemos crear referencias a algunas funciones de mapeo, así como el tamaño de la unidad, y luego compartirlas entre funciones si es necesario.
Aquí está nuestro nuevo LOAD
función Preste atención a la nueva props
por scaleLimit
y proximityRatio
Se utilizan para limitar el rango de cuán grande o pequeña puede llegar a ser una estrella, además de la proximidad en la que se basa.
const Starscape = ({
densityRatio = 0.5,
sizeLimit = 5,
defaultAlpha = 0.5,
scaleLimit = 2,
proximityRatio = 0.1
}) => {
const canvasRef = React.useRef(null)
const contextRef = React.useRef(null)
const starsRef = React.useRef(null)
const vminRef = React.useRef(null)
const scaleMapperRef = React.useRef(null)
const alphaMapperRef = React.useRef(null)
React.useEffect(() => {
contextRef.current = canvasRef.current.getContext('2d')
const LOAD = () => {
vminRef.current = Math.min(window.innerHeight, window.innerWidth)
const STAR_COUNT = Math.floor(vminRef.current * densityRatio)
scaleMapperRef.current = gsap.utils.mapRange(
0,
vminRef.current * proximityRatio,
scaleLimit,
1
);
alphaMapperRef.current = gsap.utils.mapRange(
0,
vminRef.current * proximityRatio,
1,
defaultAlpha
);
canvasRef.current.width = window.innerWidth
canvasRef.current.height = window.innerHeight
starsRef.current = new Array(STAR_COUNT).fill().map(() => ({
x: gsap.utils.random(0, window.innerWidth, 1),
y: gsap.utils.random(0, window.innerHeight, 1),
size: gsap.utils.random(1, sizeLimit, 1),
scale: 1,
alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),
}))
}
}
Y aquí está el nuestro UPDATE
Calcula la distancia y genera una función adecuada scale
y alpha
para estrella:
const UPDATE = ({ x, y }) => {
starsRef.current.forEach(STAR => {
const DISTANCE = Math.sqrt(Math.pow(STAR.x - x, 2) + Math.pow(STAR.y - y, 2));
gsap.to(STAR, {
scale: scaleMapperRef.current(
Math.min(DISTANCE, vminRef.current * proximityRatio)
),
alpha: alphaMapperRef.current(
Math.min(DISTANCE, vminRef.current * proximityRatio)
)
});
})
};
Pero espera... ¿no hace nada?
Bueno, así es, pero no hemos configurado nuestro componente para que muestre actualizaciones. Necesitamos capturar nuevas tomas a medida que interactuamos. podemos llegar a requestAnimationFrame
Pero debido a que usamos GreenSock, podemos aprovecharlo. gsap.ticker
Esto a menudo se denomina "latido del corazón del motor GSAP" y es un buen sustituto de requestAnimationFrame
.
Para usarlo, agregamos RENDER
función a ticker
y asegurarnos de haberlo quitado durante el desmontaje. Una de las cosas buenas para usar ticker
es que podemos dictar el número de fotogramas por segundo (fps
). Me gusta ir con "cinematográfico" 24fps
:
// Remove RUN
LOAD()
gsap.ticker.add(RENDER)
gsap.ticker.fps(24)
window.addEventListener('resize', LOAD)
document.addEventListener('pointermove', UPDATE)
return () => {
window.removeEventListener('resize', LOAD)
document.removeEventListener('pointermove', UPDATE)
gsap.ticker.remove(RENDER)
}
Observe cómo trabajamos ahora LOAD
Sobre resize
También tenemos que asegurarnos de que el nuestro scale
se eleva en este RENDER
función en uso arc
:
const RENDER = () => {
contextRef.current.clearRect(
0,
0,
canvasRef.current.width,
canvasRef.current.height
)
starsRef.current.forEach(star => {
contextRef.current.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})`
contextRef.current.beginPath()
contextRef.current.arc(
star.x,
star.y,
(star.size / 2) * star.scale,
0,
Math.PI * 2
)
contextRef.current.fill()
})
}
¡Funciona! 🙌
Este es un efecto muy sutil. Pero esto es intencional, porque aunque es muy bueno, no queremos que esas cosas distraigan la atención del contenido real. Recomendaría jugar con los accesorios del componente para ver los diferentes efectos. Tiene sentido poner todas las estrellas en un nivel bajo alpha
predeterminado también.
La siguiente demostración le permite jugar con diferentes accesorios. ¡Fui a algunas configuraciones predeterminadas bastante destacadas aquí para una demostración! Pero recuerda que este artículo se trata más de mostrarte las técnicas para que puedas ir y crear tus propios fondos geniales, teniendo cuidado de cómo interactúa con el contenido.
Mejoras
Hay un problema con nuestro fondo de estrellas interactivo. Si el cursor del ratón sale <canvas>
las estrellas permanecen brillantes y magnificadas, pero queremos que vuelvan a su estado original. Para corregir esto, podemos agregar un manipulador adicional para pointerleave
Cuando el puntero sale, reduce todas las estrellas a escala. 1
y el valor alfa original establecido por defaultAlpha
.
const EXIT = () => {
gsap.to(starsRef.current, {
scale: 1,
alpha: defaultAlpha,
})
}
// Set up event handling
window.addEventListener('resize', LOAD)
document.addEventListener('pointermove', UPDATE)
document.addEventListener('pointerleave', EXIT)
return () => {
window.removeEventListener('resize', LOAD)
document.removeEventListener('pointermove', UPDATE)
document.removeEventListener('pointerleave', EXIT)
gsap.ticker.remove(RENDER)
}
¡Bueno! Ahora nuestras estrellas se están alejando y regresando a su alfa anterior cuando el cursor del mouse abandona el escenario.
Bonificación: agregue un huevo de Pascua
Antes de terminar, agreguemos una pequeña sorpresa de huevo de Pascua a nuestro fondo de estrella interactivo. ¿Alguna vez has oído hablar de codigo konami• Este es un famoso código de trucos y una excelente manera de agregar un huevo de Pascua a nuestro componente.
De hecho, podemos hacer todo con el fondo una vez que se ejecuta el código. Por ejemplo, podemos hacer que todas las estrellas latan de cualquier forma. ¿O pueden cobrar vida con colores adicionales? ¡Esta es una oportunidad para ser creativo con las cosas!
Escucharemos los eventos del teclado y averiguaremos si se ha ingresado el código. Comencemos creando una variable para el código:
const KONAMI_CODE =
'arrowup,arrowup,arrowdown,arrowdown,arrowleft,arrowright,arrowleft,arrowright,keyb,keya';
Luego creamos un segundo efecto en nuestro fondo de estrellas. Esta es una buena manera de mantener la separación de preocupaciones, ya que un efecto procesa la imagen completa y el otro procesa el huevo de Pascua. En particular, escuchamos sobre keyup
eventos y comprobar que los datos que hemos introducido coinciden con el código.
const codeRef = React.useRef([])
React.useEffect(() => {
const handleCode = e => {
codeRef.current = [...codeRef.current, e.code]
.slice(
codeRef.current.length > 9 ? codeRef.current.length - 9 : 0
)
if (codeRef.current.join(',').toLowerCase() === KONAMI_CODE) {
// Party in here!!!
}
}
window.addEventListener('keyup', handleCode)
return () => {
window.removeEventListener('keyup', handleCode)
}
}, [])
Almacenamos la entrada del usuario en un Array
que almacenamos en un ref
Una vez que presionamos el código de la fiesta, podemos borrar Array
y hacer lo que queremos. Por ejemplo, podemos crear un gsap.timeline
que hace algo a nuestras estrellas por un período de tiempo. Si este es el caso, no queremos permitir la entrada del código Konami hasta que la línea de tiempo está activa. Podemos almacenar en su lugar timeline
en ref
y haga otra verificación antes de ejecutar el código de fiesta.
const partyRef = React.useRef(null)
const isPartying = () =>
partyRef.current &&
partyRef.current.progress() !== 0 &&
partyRef.current.progress() !== 1;
Para este ejemplo, creé una pequeña línea de tiempo que colorea cada estrella y la mueve a una nueva posición. Esto requiere que actualicemos el nuestro. LOAD
y RENDER
funciones
Primero, cada estrella debe tener su propia hue
, saturation
y lightness
:
// Generating stars! ⭐️
starsRef.current = new Array(STAR_COUNT).fill().map(() => ({
hue: 0,
saturation: 0,
lightness: 100,
x: gsap.utils.random(0, window.innerWidth, 1),
y: gsap.utils.random(0, window.innerHeight, 1),
size: gsap.utils.random(1, sizeLimit, 1),
scale: 1,
alpha: defaultAlpha
}));
En segundo lugar, debemos tener en cuenta estos nuevos valores al realizar el mapeo:
starsRef.current.forEach((star) => {
contextRef.current.fillStyle = `hsla(
${star.hue},
${star.saturation}%,
${star.lightness}%,
${star.alpha}
)`;
contextRef.current.beginPath();
contextRef.current.arc(
star.x,
star.y,
(star.size / 2) * star.scale,
0,
Math.PI * 2
);
contextRef.current.fill();
});
Y aquí está el divertido código corto que mueve todas las estrellas:
partyRef.current = gsap.timeline().to(starsRef.current, {
scale: 1,
alpha: defaultAlpha
});
const STAGGER = 0.01;
for (let s = 0; s < starsRef.current.length; s++) {
partyRef.current
.to(
starsRef.current[s],
{
onStart: () => {
gsap.set(starsRef.current[s], {
hue: gsap.utils.random(0, 360),
saturation: 80,
lightness: 60,
alpha: 1,
})
},
onComplete: () => {
gsap.set(starsRef.current[s], {
saturation: 0,
lightness: 100,
alpha: defaultAlpha,
})
},
x: gsap.utils.random(0, window.innerWidth),
y: gsap.utils.random(0, window.innerHeight),
duration: 0.3
},
s * STAGGER
);
}
A partir de ahí generamos una nueva línea de tiempo entre los valores de cada estrella. Estos nuevos valores son captados por RENDER
. Agregamos escalonamiento colocando cada interpolación en la línea de tiempo usando GSAP position
parámetro.
¡Está!
Esta es una forma de crear un fondo de estrella interactivo para su sitio. Combinamos GSAP y HTML <canvas>
e incluso rociado con un poco de React, haciéndolo más configurable y reutilizable. ¡Incluso dejamos caer un huevo de Pascua allí!
¿Dónde se puede obtener este componente de aquí? ¿Cómo podría usarlo en un sitio web? La combinación de GreenSock y <canvas>
¡Es muy divertido y espero ver lo que haces! Aquí hay algunas ideas más para hacer fluir sus jugos creativos...
Deja una respuesta