Construindo o Real Time Gradient: Transformando Gradientes CSS em um Primitivo de UI em Tempo de Execução
Como um pequeno experimento com gradientes CSS evoluiu para uma biblioteca JavaScript independente de framework para controlar gradientes em tempo de execução.
Sempre gostei daqueles pequenos detalhes de UI que fazem uma interface parecer um pouco mais viva.
Gradientes são um bom exemplo.
Normalmente, você define um em CSS, escolhe algumas cores e uma direção, e pronto:
.hero {
background: linear-gradient(to right, #ff7e5f, #feb47b);
}Não há nada de errado nisso. Na verdade, é exatamente para isso que gradientes CSS são bons.
Mas em algum momento comecei a me perguntar:
E se o gradiente não fosse apenas estilização, mas algo que a aplicação pudesse realmente controlar?
E se suas cores pudessem mudar em tempo de execução?
E se ele pudesse reagir ao estado da aplicação, fazer transições entre diferentes paletas, responder a eventos temporários ou até mesmo mudar automaticamente dependendo da hora do dia?
Essa pergunta acabou se tornando o Real Time Gradient.
Real Time Gradient é uma biblioteca JavaScript leve e independente de framework para criar gradientes suaves, personalizáveis e em tempo real, com controle sobre cores, direção, animação, efeitos e agendamento.
A Ideia Original
A primeira versão da ideia era muito mais simples do que a biblioteca que existe hoje.
Eu não estava tentando construir um mecanismo de gradientes.
O navegador já tem um.
CSS consegue criar um gradiente com algo tão simples quanto:
.hero {
background: linear-gradient(to right, #ff7e5f, #feb47b);
}O navegador cuida da renderização, interpolação e composição.
Então a pergunta não era:
Como eu renderizo um gradiente?
Era:
Como faço para que o gradiente seja algo que minha aplicação possa controlar?
Essa distinção se tornou a base do projeto.
Projetando a API de Runtime
A primeira versão da API poderia ter sido bastante simples.
Algo como:
gradient.setColors([...]);Mas conforme o comportamento da biblioteca cresceu, esse tipo de API começou a se tornar ambíguo.
Mudar o gradiente permanentemente é diferente de alterá-lo temporariamente.
Parar um efeito é diferente de destruir o gradiente.
Agendar um gradiente é diferente de acioná-lo manualmente.
Essas diferenças acabaram sendo refletidas na API.
gradient.persistEffect(["#9370db", "#4682b4"], 2000);persistEffect() representa uma mudança no estado visual contínuo do gradiente.
Se a aplicação quiser alterar temporariamente o gradiente, pode usar:
gradient.triggerEffect({
hue: "gold",
duration: 2000,
});Essa distinção pode parecer pequena, mas adiciona uma camada semântica útil à API.
O método informa algo sobre a intenção por trás da mudança.
A aplicação não está simplesmente dizendo:
Defina estas cores.
Ela está dizendo:
Este deve ser o novo estado visual.
Ou:
Este é um evento visual temporário.
Isso se tornou uma parte importante da forma como passei a pensar sobre a API.
O Ciclo de Vida do Gradiente
Quando um gradiente se torna stateful, animado e agendado, o ciclo de vida passa a ser uma parte importante do problema.
Uma declaração CSS estática praticamente não possui um ciclo de vida.
Ela existe como parte da stylesheet.
Um gradiente em tempo de execução é diferente.
Ele pode criar timers.
Pode manter estado de animação.
Pode reagir a efeitos.
Pode atualizar o DOM.
Pode agendar mudanças futuras.
Isso significa que precisa existir uma forma clara de interromper o que o runtime está fazendo.
Por exemplo:
gradient.stopEffects();permite interromper efeitos ativos sem necessariamente destruir o próprio gradiente.
E quando o gradiente não é mais necessário:
gradient.destroy();fornece uma maneira de desmontar completamente o runtime.
Isso se torna especialmente importante ao utilizar a biblioteca dentro de aplicações baseadas em componentes.
Um componente pode criar um gradiente quando é montado:
useEffect(() => {
const gradient = DynamicGradient.init("#hero", {
colors: ["#ff7e5f", "#feb47b"],
});
return () => {
gradient.destroy();
};
}, []);A biblioteca não precisa entender o ciclo de vida do React.
O React é responsável pelo ciclo de vida do componente.
A aplicação decide quando o gradiente deve existir.
O Dynamic Gradient é responsável por limpar os recursos associados àquela instância quando destroy() é chamado.
Essa separação mantém a biblioteca principal independente de frameworks e, ao mesmo tempo, permite que ela seja utilizada dentro de aplicações gerenciadas por frameworks.
E essa foi uma percepção importante durante o projeto:
Quando algo possui comportamento em tempo de execução, a limpeza dos recursos faz parte da API, não é uma preocupação secundária.
Pequenos primitivos, comportamentos diferentes
A superfície pública da biblioteca é intencionalmente relativamente pequena.
No núcleo, o runtime gira em torno de algumas operações:
init()
↓
estabelece o gradiente
persistEffect()
↓
altera o estado visual persistente
triggerEffect()
↓
aplica uma mudança visual temporária
stopEffects()
↓
interrompe efeitos ativos
destroy()
↓
limpa o runtimeNenhum desses métodos é particularmente complicado por si só.
A parte interessante está em como eles se combinam.
Um gradiente pode ser inicializado uma vez e então se tornar parte do sistema visual de uma aplicação maior.
Isso significava que eu não queria que a API expusesse todos os detalhes da implementação interna.
Quanto mais a biblioteca conseguisse expressar intenção em vez de implementação, mais fácil seria evoluir os componentes internos sem obrigar os consumidores a entender como tudo funciona.
Quando o Gradiente se Tornou Mais do que um Background
Em determinado momento do desenvolvimento, a abstração começou a se tornar maior do que o problema original.
O objetivo inicial era simplesmente fazer um gradiente mudar dinamicamente.
Depois vieram as transições.
Depois, efeitos temporários.
Depois, agendamento.
Depois, diferentes tipos de gradiente.
Depois, a possibilidade de usar o gradiente como preenchimento de texto.
A API começou a parecer menos com um helper para backgrounds e mais com um pequeno primitivo visual.
Por exemplo, um gradiente pode ser agendado de acordo com o horário:
schedule: [
{
time: "06:00",
colors: ["#7fff00ff", "#ffb347ff", "#ff69b4ff"],
},
{
time: "18:00",
colors: ["#ff4500ff", "#9370dbff", "#4682b4ff"],
},
];Ele também pode ser aplicado a texto:
const gradient = DynamicGradient.init("#title", {
colors: ["#ff7e5f", "#feb47b"],
textClip: true,
});E o mesmo runtime pode lidar com diferentes tipos de gradiente:
const gradient = DynamicGradient.init("#hero", {
type: "radial",
colors: ["#7f5af0", "#2cb67d"],
});Nenhum desses recursos fazia parte do problema original.
Mas todos se encaixavam naturalmente quando a abstração subjacente passou a ser:
Um gradiente visual controlado em tempo de execução.
Essa distinção é importante.
Eu não estava tentando construir uma coleção de recursos de gradientes sem relação entre si.
Estava explorando o que se torna possível quando uma propriedade visual é tratada como um primitivo de runtime de primeira classe.
E essa provavelmente é a parte mais interessante do projeto para mim.
De Propriedade CSS a Estado em Tempo de Execução
Um gradiente estático é essencialmente uma declaração:
background: linear-gradient(...);Mas quando uma aplicação precisa alterar esse gradiente dinamicamente, ele começa a se comportar mais como estado.
O modelo se torna algo mais próximo de:
Application
│
▼
Real Time Gradient
│
├── colors
├── direction
├── type
├── transitions
├── effects
└── schedule
│
▼
CSS gradient
│
▼
BrowserO Real Time Gradient não substitui as capacidades de renderização do navegador.
Ele fica entre o código da aplicação e a representação CSS do gradiente.
Isso tornou a API inicial bastante direta:
const gradient = DynamicGradient.init("#hero", {
type: "linear",
direction: "to right",
colors: ["#ff7e5f", "#feb47b"],
});Agora o gradiente possui um objeto que o representa.
A aplicação pode interagir com esse objeto em vez de tratar o gradiente como uma string estática dentro de uma stylesheet.
A parte importante dessa API não é realmente o init() em si.
É o fato de que a inicialização fornece à aplicação um objeto em runtime que representa o gradiente.
A partir daí, o gradiente pode ter comportamento.
-
Ele pode fazer transições.
-
Pode reagir temporariamente a um evento.
-
Pode mudar de acordo com um agendamento.
-
Pode ser interrompido.
-
Eventualmente, pode ser destruído.
O gradiente deixou de ser apenas um valor:
linear-gradient(...)
e passou a se aproximar de um pequeno primitivo de UI stateful:
Gradient
├── state
├── behavior
└── lifecycleE foi nesse ponto que o projeto começou a se tornar mais interessante do que um simples helper para gerar strings CSS.
Por que JavaScript?
À primeira vista, JavaScript pode parecer desnecessário aqui.
CSS já nos fornece gradientes. Ele consegue lidar com gradientes lineares e radiais, múltiplos color stops, direções e grande parte do comportamento visual que precisamos.
Então por que adicionar outra camada?
A resposta não é que CSS não consiga criar visuais dinâmicos.
Ele consegue.
O problema é onde surge a decisão de mudar o estado visual.
CSS descreve como algo deve parecer.
JavaScript frequentemente é responsável por saber quando e por que aquilo deve mudar.
Imagine um gradiente reagindo a um evento da aplicação:
user interaction
↓
application state
↓
visual state changes
↓
gradient changesOu um exemplo mais concreto:
if (userCompletedAction) {
gradient.triggerEffect({
hue: "gold",
duration: 2000,
});
}O gradiente não é a fonte dessa decisão.
A aplicação é.
É aí que JavaScript se torna útil.
O mesmo se aplica a comportamentos baseados em horário.
Um agendamento como:
schedule: [
{
time: "06:00",
colors: ["#7fff00ff", "#ffb347ff", "#ff69b4ff"],
},
{
time: "18:00",
colors: ["#ff4500ff", "#9370dbff", "#4682b4ff"],
},
];já não é realmente uma preocupação de estilização.
Algo precisa saber que horas são, determinar qual estado deve estar ativo e informar ao gradiente que ele deve fazer uma transição.
Isso é comportamento da aplicação.
JavaScript já é o lugar natural para essa lógica.
O objetivo não era substituir o CSS
Essa distinção foi importante para mim durante a construção da biblioteca.
Eu não queria criar uma versão JavaScript dos gradientes CSS.
Isso significaria pegar algo que o navegador já faz muito bem e reconstruí-lo em outro lugar.
Em vez disso, a ideia era deixar o CSS responsável pelo que ele faz bem:
CSS
└── descreve e renderiza o gradienteenquanto o JavaScript cuida do comportamento em tempo de execução:
JavaScript
├── decide quando o gradiente muda
├── gerencia o estado visual atual
├── dispara transições
├── agenda mudanças
└── gerencia o ciclo de vidaA biblioteca fica entre essas duas responsabilidades.
Application
│
│ "change the visual state"
▼
Real Time Gradient
│
│ "translate state into CSS"
▼
CSS
│
▼
BrowserEssa separação acabou sendo uma das decisões de design mais importantes do projeto.
O navegador continua sendo o mecanismo de renderização.
A aplicação continua no controle.
O Real Time Gradient simplesmente fornece ao gradiente uma API de runtime.
O que construir uma pequena biblioteca Open Source realmente mudou
Existe uma diferença entre construir algo que funciona e construir algo que outras pessoas podem usar.
Quando o Dynamic Gradient era apenas um experimento, eu podia fazer suposições.
Eu sabia como o código funcionava.
Sabia quais entradas esperava.
Sabia como o runtime se comportava.
Quando ele se tornou um pacote npm, essas suposições deixaram de ser detalhes privados da implementação.
Elas passaram a fazer parte de uma interface pública.
De repente, perguntas como estas passaram a importar:
- O que acontece se o elemento alvo não existir?
- Quais são as opções padrão?
- O que exatamente significa um efeito?
- Como o cleanup deve funcionar?
- O que acontece quando um agendamento atravessa a meia-noite?
- O que o TypeScript deve expor?
- Quanto da implementação os consumidores realmente precisam conhecer?
- Quais comportamentos são garantidos e quais são apenas detalhes de implementação?
As definições TypeScript também passaram a fazer parte desse contrato.
Por exemplo:
export interface GradientOptions {
type?: GradientType;
direction?: string;
colors?: string[];
transitionDuration?: number;
schedule?: ScheduleEntry[];
textClip?: boolean;
}Isso pode parecer apenas um exercício de tipagem, mas tipos públicos obrigam você a pensar cuidadosamente sobre o que sua API realmente promete.
O mesmo aconteceu com a documentação.
Escrever documentação para o próprio código é surpreendentemente bom para revelar ambiguidades.
Se você não consegue explicar o que um método faz sem descrever a implementação por trás dele, provavelmente a abstração ainda não está terminada.
Esse foi um dos benefícios inesperados de transformar o experimento em um pacote Open Source.
Isso me obrigou a pensar menos em:
Como meu código funciona?
e mais em:
O que outra pessoa poderia razoavelmente esperar que esse código fizesse?
O que aprendi
A maior lição não foi realmente sobre gradientes.
Foi sobre abstração.
O navegador já possuía o primitivo de renderização.
Eu não precisava recriá-lo.
O problema interessante era identificar a camada que faltava entre uma declaração CSS estática e um comportamento visual controlado pela aplicação.
Isso me levou a algumas coisas que considerei particularmente úteis.
Não reconstrua aquilo que a plataforma já faz bem
CSS e o navegador são muito bons em renderizar gradientes.
Tentar substituí-los adicionaria complexidade sem resolver o problema real.
A abstração útil era a camada de controle em runtime.
APIs devem comunicar intenção
Existe uma diferença significativa entre:
setColors(...)e:
persistEffect(...)ou:
triggerEffect(...)Os últimos comunicam por que a operação está acontecendo, e não apenas quais dados estão sendo passados.
Isso torna a API mais fácil de compreender e deixa mais liberdade para alterar a implementação por baixo dela.
Projetos pequenos também podem revelar problemas arquiteturais reais
Real Time Gradient é uma biblioteca relativamente pequena.
Mas mesmo um pequeno runtime precisa responder a questões reais de engenharia:
- estado
- ciclo de vida
- agendamento
- cleanup
- interfaces públicas
- tipagem
- empacotamento
- limites entre frameworks
Você não precisa de uma aplicação enorme para encontrar esses problemas.
Às vezes, um projeto pequeno é justamente um bom ambiente para aprendê-los, porque os limites são mais fáceis de enxergar.
O que vem a seguir?
Eu não tenho um roadmap enorme para o Real Time Gradient.
Isso é intencional.
Uma das coisas que gosto no projeto é que ele pode continuar pequeno e, ainda assim, ser útil.
Definitivamente existem áreas que poderiam evoluir:
- controles de animação mais ricos
- efeitos mais expressivos
- novos primitivos de gradiente
- interpolação aprimorada
- mais exemplos e demos
- integrações com frameworks quando elas realmente agregarem valor
Mas não quero adicionar recursos apenas para tornar a API maior.
Prefiro manter a ideia central clara:
Dar às aplicações uma maneira simples de tratar gradientes como primitivos de UI controláveis em tempo de execução.
Se as pessoas encontrarem usos interessantes para essa abstração, provavelmente essa será uma direção melhor para o projeto do que tentar prever todos os recursos possíveis por conta própria.
Considerações finais
Um gradiente normalmente é apenas um background.
Pelo menos, era assim que eu pensava quando comecei.
Mas quando você dá a esse gradiente estado, comportamento, transições, agendamento e um ciclo de vida, ele começa a parecer muito menos com uma declaração CSS e muito mais com um primitivo de UI.
Foi isso que o Real Time Gradient acabou se tornando.
Não um substituto para CSS.
Não um mecanismo de renderização.
Não um framework.
Apenas uma pequena camada entre o estado da aplicação e algo que o navegador já sabe como renderizar.
E essa provavelmente é a parte do projeto que mais me interessa.
A biblioteca começou com uma pergunta relativamente simples:
E se um gradiente não fosse apenas estilização, mas algo que a aplicação pudesse realmente controlar?
Acontece que essa pergunta era mais interessante do que o próprio gradiente.