npx shadcn@latest add @react-bits/SplashCursor-JS-CSS import SplashCursor from './SplashCursor'

AUGUSTO GODOI

COPYWRITER

REDATOR DE RESPOSTA DIRETA para marcas de saúde e desempenho

Textos de resposta direta que transformam tráfego frio em compradores

Ajudo marcas de saúde, bem-estar e performance a crescerem com vídeos de vendas, páginas de vendas, sequência de e-mail e anúncios criados em um sistema próprio — sem palpites.

VAMOS ESCREVER SUA HISTÓRIA JUNTOS


COPYWRITING▫️TRABALHANDO EM VÁRIOS FUNIS DE SAÚDE E DESEMPENHO ▫️

O QUE EU ESCREVO

Funis de vendas, não apenas "textos publicitários"

Sou especialista em textos que geram resultados concretos — não apenas palavras em uma página.

  • VSLs & Cartas de Vendas

    Vídeos de vendas e páginas de formato longo, elaborados para atrair novos clientes, com uma estrutura sólida, apelo emocional e um promessa clara.

  • Páginas de Vendas & Fluxos de Finalização de Compra (Checkout)

    Da pré-venda às vendas adicionais, cada página é estruturada para eliminar atritos, lidar com objeções e aumentar o valor médio do pedido (AOV).

  • Sequência de E-mail & Campanhas de Lançamento

    Processos de integração, abertura/fechamento do carrinho, reativação e sequências pós-compra que mantêm o fluxo de caixa entre lançamentos.

  • Anúncios (Ads) & Scripts de Conteúdo Gerado pelo Usuário (UGC)

    Ganchos, elementos de bloqueio de rolagem, e scripts curtos para Meta, YouTube, TikTok e conteúdo nativo — alinhados com sua linguagem de estilo de vídeo (VSL) e em conformidade com as políticas da plataforma.

Com o sistema
Copy Engine X™

Seu Funil de Vendas, Construído com um Sistema de Resposta Direta Comprovado

A maioria das companhas fracassa porque o texto é escrito "de improviso", ou por meio de sugestões genéricas de IA, sem nenhuma pesquisa real por trás.

Eu uso o Copy Engine X™, meu sistema proprietário para criar, analisar e otimizar textos de resposta direta. Ele transforma dados brutos de mercado, pesquisas de avatar e informações sobre a concorrência em VSLs (Vídeos de Ofertas de Sites), páginas e anúncios com um processo claro e repetível.

Na prática, isso significa:

- Pesquisa aprofundada sobre nicho de mercado, concorrentes, promessas, mecanismos e provas

- Um dossiê completo de Avatar (Lead) mapeando dores, desejos, linguagem, crenças, objeções etc

- Um documento estruturado de Mecanismo e Provas que conecta a ciência à narrativa de vendas

- Uma matriz de Ideia Pricipal (Big Idea) + Ângulo bem definida antes de qualquer roteiro ser escrito

- A linguagem visual e os criativos foram gerados e refinados com instruções guiadas por IA — nunca “IA no piloto automático”

O resultado: seu funil não é um projeto isolado. Ele se torna um ativo construído com rigor de engenharia e criatividade.

Copy Engine X Review

Copy Engine X também inclui uma versão de Revisão, orientada por um experiente Chefe de Redação (Copy Chief) de Resposta Direta. Este especialista avalia honestamente o desempenho do seu texto e o classifica emelementos-chave como gancho, apelo emocional, benefícios, mecanismo, comprovação/autoridade, clareza e fluidez, e chamada para ação (CTA).

POR QUE OS CLIENTES ME CONTRATAM

Por que trabalhar com um engenheiro que se tornou redator publicitário (copywriter)?

Antes de me aprofundar em marketing de resposta direta, passei mais de 18 anos em engenharia de produtos, segurança e conformidade, liderando projetos globais para máquinas complexas e produtos regulamentados.

Essa experiência significa que:

- tenho familiaridade com
produtos técnicos, mecanismos científicos e nichos com forte regulamentação;

- penso em sistemas e processos, não em criatividade aleatória;

- e entendo todo o ciclo de vida, do
conceito → produto → lançamento no mercado → escalabilidade.

Além disso, cosntruí minha própria loja virtual e uma consultoria de crescimento digital, gerenciando funis de vendas, anúncios no Meta Ads, otimização da taxa de conversão (CRO) e automação de e-mail de ponta a ponta.

Portanto, quando escrevo seus textos, não apenas supondo o que "talvez" funcione — estou pensando como um profissional que vivenciou metas de desempenho, conformidade e crescimento simultaneamente.

Pronto para construir um funil que realmente possa ser escalado?

Se você precisa de um VSL totalmente novo, um funil completo construído do zero ou um controle que deseja superar, adoraria dar uma olhada no seu projeto.

SERVIÇOS

Serviços de Direct Response Copywriting | Augusto Godoi

Eu me concentro em textos de performance para marcas que precisam de resultados mensuráveis — mais leads, mais clientes, maior valor médio do pedido (AOV) e maior valor vitalício do cliente (LTV). Cada projeto segue o processo Copy Engine X™ para que suas campanhas sejam construídas com base em dados reais, não em palpites.

1. Listas de Vendas em Vídeo (VSLs) & Cartas de Vendas Longas

Transforme visitantes em compradores com uma história, um mecanismo e uma oferta irresistíveis.

Se você vende suplementos, infoprodutos ou soluções de alto valor agregado, seu vídeo de vendas (VSL) ou cartas de vendas completa é o motor do seu funil de vendas.

Eu te ajudo a:

- Definir uma Grande Ideia (Big Idea) e uma promessa central que se destaquem em um mercado saturado
- Estruturar a narrativa em torno de um mecanismo claro (problema, função, solução)
- Integrar provas, estudos, depoimentos e credibilidade de form adequada às políticas
- Criar envolvimento emocional e senso de urgência sem exageros ou promessas de "cura milagrosa"

Entregáveis típicos:

- Roteiro do VSL (ou carta) totalmente estruturado em caítulos
- Sugestões de recursos visuais, slides e ênfase na tela
- Opcional: "variações" mais curtas para testar ganchos e abordagens

2. Funil Completo: VSL + Página de Vendas + E-mails

Do primeiro clique à venda final — uma história consistente.

Em vez de juntar peças diferentes freelancers, você obtém um funil coeso cosntruído sob uma única estratégia e um único sistema.

O que está incluído (personalizado para cada projeto):

- Roteiro de VSL ou carta de vendas completa
- Texto de páginas de vendas e checkout
- Texto de páginas de Upsell/downsell (1–3 etapas, conforme necessário)
- Sequências de e-mail (lançamento, abertura/fechamento do carrinho, pós-compra, reativação)
- Ganchos de anúncios + scripts alinhandos com o VSL

Tudo é criado dentro do framework do Copy Engine X™, que garante que pesquisa, avatar, mecanismo, oferta e criativos falem a mesma linguagem.

3. Campanhas de E-mail & Sequências de Ciclo de Vida

Mantenha os clientes comprando entre os lançamentos.

E-mail é onde reside sua margem de lucro. Eu crio campanhas de e-mail que:

- Integram novos assinates e compradores
- Educam e constroem confiança em torno do seu mecanismo e produto
- Retivam segmentos inativos com ofertas relevantes
- Apoiam lançamentos, campanhas sazonais e pacotes de produtos

Examplos de fluxos:

- Sequências de boas-vindas e integração
- Campanhas de lançamento (desafio, webinar, baseadas em vídeos, etc.)
- Campanhas promocionais e vendas relâmpago
- Sequências pós-compra e focadas no valor vitalício do cliente (LTV)

4. Anúncios, Ganchos & Scripts de UGC para Desempenho

Criativos que conquistam cliques e preparam o terreno para a venda.

Seus anúncios não precisam apenas de atenção — eles precisam antecipar a venda. Eu escrevo:


- Ganchos, ângulos e abordagens para Meta, YouTube, TikTok, anúncios nativos
- Roteiros curtos para criativos no estilo UGC (Conteúdo Gerado pelo Usuário)
- Páginas de publieditoriais/pré-venda (Advertorial/pre-sell) que aquecem o tráfego frio
- Variações criadas a partir da análise reversa de anúncios de sucesso no seu nicho

Todo o texto do anúncio está alinhado com sua proposta de valor e oferta, para que você não acabe com criativos que prometem uma coisa e a página entrega outra.

5. Pesquisa, Avatar, Estratégia de Oferta & Implementação do Copy Engine X™

Para marcas e equipes que desejam uma base mais sólid antes de escrever uma única linha de texto.

Às vezes, você não precisa de mais texto ainda — você precisa de melhores informações.

Usando o sistema Copy Engine X™, posso ajudar sua equipe com:

- Documento de Nicho e Oportunidade (Etapa 0–1) – análise comparativa de concorrentes, promessas, mecanismos e lacunas em aberto
- Dossiê Avatar (Etapa 2) – páginas com dores, desejos, linguagem, crenças e objeções
- Mecanismo & Provas (Etapa 3) – problema, função, solução e evidências de apoio
- Matriz de Big Idea & Ângulo (Etapa 4) – multiplos ganchos e ângulos para impulsionar suas criações
- Briefing Mestre (Etapa 5) – uma única fonte de informações para todos no projeto


Ideal se você já possui redatores/compradores de mídia internos e deseja que eles trabalhem com um briefing de alta qualidade e baseado em dados, em vez de começar do zero a cada vez.

6. Engajamentos Personalizados Sob Medida para Seu Negócio

Quando seu funil, equipe ou planejamento não se encaixam em um modelo padrão.

Nem todo projeto se parece com um clássico “VSL + funil” ou uma campanha de e-mail simples.


Às vezes, você já tem recursos, redatores internos, compradores de mídia e dados... mas precisa de um especialista para diagnosticar, reorganizar ou escalar o que você já construiu.

É aí que entram os projetos personalizados.

Crio um escopo de acordo com suas prioridades – seja para corrigir um funil com problemas, estruturar uma nova oferta ou ajudar sua equipe a implementar o sistema Copy Engine X™ em sua própria operação.

Este tipo de projeto pode incluir:

- Auditoria de Funil & Conteúdo
Análise detalhada do seu VSL, página de vendas, e-mails e anúncios, com um relatório escrito sobre o que corrigir, o que testar e o que descartar.

- Diagnóstico de Conteúdo de Controle & Plano de Aprimoramento
Análise do seu conteúdo de controle atual (ou variante com desempenho insatisfatório) e uma lista priorizada de novas abordagens, leads e mudanças estruturais para testar.

- Suporte à Estratégia de Lançamento & Promoção
Contribuição estratégica sobre posicionamento, ganchos, estrutura de campanha, calendário de e-mails e combinação de ofertas para lançamentos ou promoções importantes.

- Copy Engine X™ para Sua Equipe
Implementação do sistema (etapas de pesquisa, avatar, mecanismo, Big Idea, briefings e backlog de otimização) para que seus redatores e compradores de mídia internos trabalharem com um estrutura compartilhada e de alta quantidade.

- Consultoria Contínua / Contrato de prestação de serviços
Um número fixo de horas por mês para revisões, novos trechos de texto, testes de ideias e reuniões estratégicas à medida que seus funis evoluem.


Quando esta opção é ideal:

- Você já tem tráfego e dados, mas os resultados são inconsistentes.
- Você tem suporte interno ou de agência, mas não possui um framework central que oriente a redação e a criação.
- Você quer agir rapidamente, mas não quer reinventar tudo do zero.

Como funciona:

1. Definimos o escopo e as prioridade juntos (auditoria, estratégica, implementação ou uma combinação).
2. Mapeio seus ativos e objetos sob ótica do Copy Engine X™.
3. Você recebe entreguáveis claros: relatórios, planos deação, briefings, novos trechos de texto ou suporte contínuo – dependendo do escopo acordado.

→ Diga-me o que você precisa e eu vou porpor uma configuração personalizada.

Como Funciona Nossa Colaboração

Como os Projetos Normalmente Funcionam

1. Descoberta & Adequação
Discutimos sua oferta, público-alvo, estrutura do funil e objetos para verificar se sou o parceiro ideal.

2. Pesquisa & Estratégia
Analiso seu nicho utilizando o processo Copy Engine X™ — pesquisa, avatar, mecanismo, oferta e mapeamento de ângulos.

3. Rascunhos & Revisão
Você recebe o texto em etapas estruturadas (lead + esboço → rascunho completo → refinementos). O feedback é integrado em 1 em 2 rodadas de revisão, dependendo do escopo.


4. Otimização & Ideias de Teste
Posso fornecer ideias de teste para ganchos, ângulos e seções da página para que sua equipe de mídia saiba por onde começar a otimizar.

→ Pronto para conversar sobre um projeto? Entre em contato.

ALGUNS EXEMPLOS DO PORTFÓLIO

SOBRE

Sobre Augusto Godoi
Direct Response Copywriter & Estrategista de Funil

Da Engenharia e Segurança de Produtos… À Engenharia de Funis de Alta Conversão

Sou Augusto Godoi, redator de resposta direta e estrategista de funis com uma experiência que poucos redatores possuem: mais de 18 anos em engenharia de produtos, segurança e conformidade, liderando projetos globais em setores altamente regulamentados — e, paralelamente, construindo negócios digitais, funis e campanhas na área de saúde e desempenho.

Hoje, utilizo essa combinação de rigor técnico e criatividade em resposta direta para ajudar marcas a construir funis persuasivos e estruturalmente sólidos.

O Lado da Engenharia

Experiência em Engenharia e Conformidade

Por mais de quinze anos, trabalhei em produtos complexos e lançamentos globais — incluindo máquinas agrícolas e fora de estrada — garantindo que atendessem aos rigorosos padrões de segurança, conformidade e regulamentação em diversos mercados.

Minhas experiências incluem:
- Liderança de equipes multifuncionais nas área de engenharia, marketing, validação e gestão de produtos
- Trabalho direto com órgãos de padronização e estruturas regulatórias
- Garantia de que os produtos pudessem ser comercializados com segurança em diversos países

Isso me capacitou a:
- Pensar em sistemas (e não em partes isoladas)
- Lidar com detalhes técnicos sem me perder neles
- Equilibrar inovação com gestão de riscos e sustentabilidade a longo prazo

Toda essa experiência agora contribui para a forma como construo e testo funis de vendas e textos para otimizar o desempenho.

O Operador & Empreendedor

Operador de Negócios Digitais

Paralelamente à minha carreira em engenharia, construí negócios digitais onde era responsável por tudo:

- Festivano.com – uma loja virtual de decoração sazonal, onde gerenciava CRO, Meta Ads, SEO, coordenação de fornecedores e experiência do cliente usando Shopify e marketing.

- Abovy Solutions – uma consultoria de crescimento digital que auxilia startups e PMEs com posicionamento, geração de leads, funis de resposta direta e campanhas com ROI mensurável.

Essa perspectiva de operador significa que eu entendo:

- A pressão para que as campanhas se paguem
- As restrições enfrentadas por compradores de mídia, fundadores e equipes de produto
- Como o texto interage com rastreamento, ofertas, preços e sistemas de back-end.

Não me importo apenas se o texto "é bem escrito". Me importo se ele atinge os KPIs.

A Vantagem do Copy Engine X™

Por que criei o Copy Engine X™?

Depois de trabalhar em vários funis de vendas focados em saúde e performance, eu me deparava com os mesmos problemas:

- Todos os projetos começavam do zero, sem um método repetível
- A IA era usada de forma aleatória — com prompts sem estratégia definida
- Ads, VSLs, e ofertas não estavam alinhados, causando desconexão e baixa conversão

Então, criei o Copy Engine X™ — um sistema proprietário que:

- Divide o processo em etapas claras, desde a documentação do nicho até os controles e o backlog de otimização
- Utiliza pesquisa, mapeamento de avatares, definição de mecanismos, ofertas e ângulos antes da redação
- Integra fluxos de trabalho guiado por IA para acelerar a produção sem perder a profundidade
- Inclui engenharia reversa de vídeos de vendas e anúncios de sucesso no seu nicho para encontrar padrões e atalhos

Para você, isso significa: um parceiro de copywriting que não "improvisa" — mas que executa seu funil por meio de um mecanismo repetível e baseado em dados.

O que isso significa para sua marca

Como posso te ajudar na prática?

Ao trabalharmos juntos, você terá um redator que:

- Domina mecanismos complexos (ingredientes, vias metabólicas, alegações científicas) e os traduz em histórias claras e envolventes

- Entende as normas de conformidade na área da saúde e bem-estar e evita alegações imprudentes

- Falo Inglês, Português (e Espanhol básico), facilitando para projetos internacionais

- Traz para a mesa tanto experiência em estratégia digital B2B/B2C quanto em funis de vendas


Se você leva a sério a criação ou o escalonamento de um funil de vendas de resposta direta nas áreas de saúde, suplementos ou performance, adoraria conversar.

→ Veja os serviços e entre em contato.

Referências / Mais Informações

CONTATO

Trabalhe com Augusto Godoi – Direct Response Copywriter

Vamos Falar Sobre o Seu Funil

Fale-me sobre sua oferta, público-alvo e objetivos — eu lhe direi se sou o parceiro certo e como posso ajudar.

Se você procura um redator para escrever "apenas um e-mail", talvez eu não seja a melhor opção.

Mas se você quer alguém que pense estrategicamente sobre o seu funil de vendas — com VSL, páginas, e-mails e anúncios trabalhando juntos em um único sistema — envie-me uma mensagem com o máximo de detalhes possível. Normalmente respondo em 2 a 3 dias úteis com os próximos passos e, se for o caso, um link para agendar uma conversa.

'use client'; import { useEffect, useRef } from 'react'; function SplashCursor({ SIM_RESOLUTION = 128, DYE_RESOLUTION = 1440, CAPTURE_RESOLUTION = 512, DENSITY_DISSIPATION = 3.5, VELOCITY_DISSIPATION = 2, PRESSURE = 0.1, PRESSURE_ITERATIONS = 20, CURL = 3, SPLAT_RADIUS = 0.2, SPLAT_FORCE = 6000, SHADING = true, COLOR_UPDATE_SPEED = 10, BACK_COLOR = { r: 0.5, g: 0, b: 0 }, TRANSPARENT = true }) { const canvasRef = useRef(null); const animationFrameId = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; // Track if the effect is still active for cleanup let isActive = true; function pointerPrototype() { this.id = -1; this.texcoordX = 0; this.texcoordY = 0; this.prevTexcoordX = 0; this.prevTexcoordY = 0; this.deltaX = 0; this.deltaY = 0; this.down = false; this.moved = false; this.color = [0, 0, 0]; } let config = { SIM_RESOLUTION, DYE_RESOLUTION, CAPTURE_RESOLUTION, DENSITY_DISSIPATION, VELOCITY_DISSIPATION, PRESSURE, PRESSURE_ITERATIONS, CURL, SPLAT_RADIUS, SPLAT_FORCE, SHADING, COLOR_UPDATE_SPEED, PAUSED: false, BACK_COLOR, TRANSPARENT }; let pointers = [new pointerPrototype()]; const { gl, ext } = getWebGLContext(canvas); if (!ext.supportLinearFiltering) { config.DYE_RESOLUTION = 256; config.SHADING = false; } function getWebGLContext(canvas) { const params = { alpha: true, depth: false, stencil: false, antialias: false, preserveDrawingBuffer: false }; let gl = canvas.getContext('webgl2', params); const isWebGL2 = !!gl; if (!isWebGL2) gl = canvas.getContext('webgl', params) || canvas.getContext('experimental-webgl', params); let halfFloat; let supportLinearFiltering; if (isWebGL2) { gl.getExtension('EXT_color_buffer_float'); supportLinearFiltering = gl.getExtension('OES_texture_float_linear'); } else { halfFloat = gl.getExtension('OES_texture_half_float'); supportLinearFiltering = gl.getExtension('OES_texture_half_float_linear'); } gl.clearColor(0.0, 0.0, 0.0, 1.0); const halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat && halfFloat.HALF_FLOAT_OES; let formatRGBA; let formatRG; let formatR; if (isWebGL2) { formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType); formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType); formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType); } else { formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); } return { gl, ext: { formatRGBA, formatRG, formatR, halfFloatTexType, supportLinearFiltering } }; } function getSupportedFormat(gl, internalFormat, format, type) { if (!supportRenderTextureFormat(gl, internalFormat, format, type)) { switch (internalFormat) { case gl.R16F: return getSupportedFormat(gl, gl.RG16F, gl.RG, type); case gl.RG16F: return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type); default: return null; } } return { internalFormat, format }; } function supportRenderTextureFormat(gl, internalFormat, format, type) { const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null); const fbo = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); return status === gl.FRAMEBUFFER_COMPLETE; } class Material { constructor(vertexShader, fragmentShaderSource) { this.vertexShader = vertexShader; this.fragmentShaderSource = fragmentShaderSource; this.programs = []; this.activeProgram = null; this.uniforms = []; } setKeywords(keywords) { let hash = 0; for (let i = 0; i < keywords.length; i++) hash += hashCode(keywords[i]); let program = this.programs[hash]; if (program == null) { let fragmentShader = compileShader(gl.FRAGMENT_SHADER, this.fragmentShaderSource, keywords); program = createProgram(this.vertexShader, fragmentShader); this.programs[hash] = program; } if (program === this.activeProgram) return; this.uniforms = getUniforms(program); this.activeProgram = program; } bind() { gl.useProgram(this.activeProgram); } } class Program { constructor(vertexShader, fragmentShader) { this.uniforms = {}; this.program = createProgram(vertexShader, fragmentShader); this.uniforms = getUniforms(this.program); } bind() { gl.useProgram(this.program); } } function createProgram(vertexShader, fragmentShader) { let program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) console.trace(gl.getProgramInfoLog(program)); return program; } function getUniforms(program) { let uniforms = []; let uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); for (let i = 0; i < uniformCount; i++) { let uniformName = gl.getActiveUniform(program, i).name; uniforms[uniformName] = gl.getUniformLocation(program, uniformName); } return uniforms; } function compileShader(type, source, keywords) { source = addKeywords(source, keywords); const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) console.trace(gl.getShaderInfoLog(shader)); return shader; } function addKeywords(source, keywords) { if (!keywords) return source; let keywordsString = ''; keywords.forEach(keyword => { keywordsString += '#define ' + keyword + '\n'; }); return keywordsString + source; } const baseVertexShader = compileShader( gl.VERTEX_SHADER, ` precision highp float; attribute vec2 aPosition; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform vec2 texelSize; void main () { vUv = aPosition * 0.5 + 0.5; vL = vUv - vec2(texelSize.x, 0.0); vR = vUv + vec2(texelSize.x, 0.0); vT = vUv + vec2(0.0, texelSize.y); vB = vUv - vec2(0.0, texelSize.y); gl_Position = vec4(aPosition, 0.0, 1.0); } ` ); const copyShader = compileShader( gl.FRAGMENT_SHADER, ` precision mediump float; precision mediump sampler2D; varying highp vec2 vUv; uniform sampler2D uTexture; void main () { gl_FragColor = texture2D(uTexture, vUv); } ` ); const clearShader = compileShader( gl.FRAGMENT_SHADER, ` precision mediump float; precision mediump sampler2D; varying highp vec2 vUv; uniform sampler2D uTexture; uniform float value; void main () { gl_FragColor = value * texture2D(uTexture, vUv); } ` ); const displayShaderSource = ` precision highp float; precision highp sampler2D; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform sampler2D uTexture; uniform sampler2D uDithering; uniform vec2 ditherScale; uniform vec2 texelSize; vec3 linearToGamma (vec3 color) { color = max(color, vec3(0)); return max(1.055 * pow(color, vec3(0.416666667)) - 0.055, vec3(0)); } void main () { vec3 c = texture2D(uTexture, vUv).rgb; #ifdef SHADING vec3 lc = texture2D(uTexture, vL).rgb; vec3 rc = texture2D(uTexture, vR).rgb; vec3 tc = texture2D(uTexture, vT).rgb; vec3 bc = texture2D(uTexture, vB).rgb; float dx = length(rc) - length(lc); float dy = length(tc) - length(bc); vec3 n = normalize(vec3(dx, dy, length(texelSize))); vec3 l = vec3(0.0, 0.0, 1.0); float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0); c *= diffuse; #endif float a = max(c.r, max(c.g, c.b)); gl_FragColor = vec4(c, a); } `; const splatShader = compileShader( gl.FRAGMENT_SHADER, ` precision highp float; precision highp sampler2D; varying vec2 vUv; uniform sampler2D uTarget; uniform float aspectRatio; uniform vec3 color; uniform vec2 point; uniform float radius; void main () { vec2 p = vUv - point.xy; p.x *= aspectRatio; vec3 splat = exp(-dot(p, p) / radius) * color; vec3 base = texture2D(uTarget, vUv).xyz; gl_FragColor = vec4(base + splat, 1.0); } ` ); const advectionShader = compileShader( gl.FRAGMENT_SHADER, ` precision highp float; precision highp sampler2D; varying vec2 vUv; uniform sampler2D uVelocity; uniform sampler2D uSource; uniform vec2 texelSize; uniform vec2 dyeTexelSize; uniform float dt; uniform float dissipation; vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) { vec2 st = uv / tsize - 0.5; vec2 iuv = floor(st); vec2 fuv = fract(st); vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize); vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize); vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize); vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize); return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y); } void main () { #ifdef MANUAL_FILTERING vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize; vec4 result = bilerp(uSource, coord, dyeTexelSize); #else vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize; vec4 result = texture2D(uSource, coord); #endif float decay = 1.0 + dissipation * dt; gl_FragColor = result / decay; } `, ext.supportLinearFiltering ? null : ['MANUAL_FILTERING'] ); const divergenceShader = compileShader( gl.FRAGMENT_SHADER, ` precision mediump float; precision mediump sampler2D; varying highp vec2 vUv; varying highp vec2 vL; varying highp vec2 vR; varying highp vec2 vT; varying highp vec2 vB; uniform sampler2D uVelocity; void main () { float L = texture2D(uVelocity, vL).x; float R = texture2D(uVelocity, vR).x; float T = texture2D(uVelocity, vT).y; float B = texture2D(uVelocity, vB).y; vec2 C = texture2D(uVelocity, vUv).xy; if (vL.x < 0.0) { L = -C.x; } if (vR.x > 1.0) { R = -C.x; } if (vT.y > 1.0) { T = -C.y; } if (vB.y < 0.0) { B = -C.y; } float div = 0.5 * (R - L + T - B); gl_FragColor = vec4(div, 0.0, 0.0, 1.0); } ` ); const curlShader = compileShader( gl.FRAGMENT_SHADER, ` precision mediump float; precision mediump sampler2D; varying highp vec2 vUv; varying highp vec2 vL; varying highp vec2 vR; varying highp vec2 vT; varying highp vec2 vB; uniform sampler2D uVelocity; void main () { float L = texture2D(uVelocity, vL).y; float R = texture2D(uVelocity, vR).y; float T = texture2D(uVelocity, vT).x; float B = texture2D(uVelocity, vB).x; float vorticity = R - L - T + B; gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0); } ` ); const vorticityShader = compileShader( gl.FRAGMENT_SHADER, ` precision highp float; precision highp sampler2D; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform sampler2D uVelocity; uniform sampler2D uCurl; uniform float curl; uniform float dt; void main () { float L = texture2D(uCurl, vL).x; float R = texture2D(uCurl, vR).x; float T = texture2D(uCurl, vT).x; float B = texture2D(uCurl, vB).x; float C = texture2D(uCurl, vUv).x; vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L)); force /= length(force) + 0.0001; force *= curl * C; force.y *= -1.0; vec2 velocity = texture2D(uVelocity, vUv).xy; velocity += force * dt; velocity = min(max(velocity, -1000.0), 1000.0); gl_FragColor = vec4(velocity, 0.0, 1.0); } ` ); const pressureShader = compileShader( gl.FRAGMENT_SHADER, ` precision mediump float; precision mediump sampler2D; varying highp vec2 vUv; varying highp vec2 vL; varying highp vec2 vR; varying highp vec2 vT; varying highp vec2 vB; uniform sampler2D uPressure; uniform sampler2D uDivergence; void main () { float L = texture2D(uPressure, vL).x; float R = texture2D(uPressure, vR).x; float T = texture2D(uPressure, vT).x; float B = texture2D(uPressure, vB).x; float C = texture2D(uPressure, vUv).x; float divergence = texture2D(uDivergence, vUv).x; float pressure = (L + R + B + T - divergence) * 0.25; gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0); } ` ); const gradientSubtractShader = compileShader( gl.FRAGMENT_SHADER, ` precision mediump float; precision mediump sampler2D; varying highp vec2 vUv; varying highp vec2 vL; varying highp vec2 vR; varying highp vec2 vT; varying highp vec2 vB; uniform sampler2D uPressure; uniform sampler2D uVelocity; void main () { float L = texture2D(uPressure, vL).x; float R = texture2D(uPressure, vR).x; float T = texture2D(uPressure, vT).x; float B = texture2D(uPressure, vB).x; vec2 velocity = texture2D(uVelocity, vUv).xy; velocity.xy -= vec2(R - L, T - B); gl_FragColor = vec4(velocity, 0.0, 1.0); } ` ); const blit = (() => { gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0); return (target, clear = false) => { if (target == null) { gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); gl.bindFramebuffer(gl.FRAMEBUFFER, null); } else { gl.viewport(0, 0, target.width, target.height); gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo); } if (clear) { gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); } gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); }; })(); let dye, velocity, divergence, curl, pressure; const copyProgram = new Program(baseVertexShader, copyShader); const clearProgram = new Program(baseVertexShader, clearShader); const splatProgram = new Program(baseVertexShader, splatShader); const advectionProgram = new Program(baseVertexShader, advectionShader); const divergenceProgram = new Program(baseVertexShader, divergenceShader); const curlProgram = new Program(baseVertexShader, curlShader); const vorticityProgram = new Program(baseVertexShader, vorticityShader); const pressureProgram = new Program(baseVertexShader, pressureShader); const gradienSubtractProgram = new Program(baseVertexShader, gradientSubtractShader); const displayMaterial = new Material(baseVertexShader, displayShaderSource); function initFramebuffers() { let simRes = getResolution(config.SIM_RESOLUTION); let dyeRes = getResolution(config.DYE_RESOLUTION); const texType = ext.halfFloatTexType; const rgba = ext.formatRGBA; const rg = ext.formatRG; const r = ext.formatR; const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; gl.disable(gl.BLEND); if (!dye) dye = createDoubleFBO(dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering); else dye = resizeDoubleFBO(dye, dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering); if (!velocity) velocity = createDoubleFBO(simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering); else velocity = resizeDoubleFBO( velocity, simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering ); divergence = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); curl = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); pressure = createDoubleFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); } function createFBO(w, h, internalFormat, format, type, param) { gl.activeTexture(gl.TEXTURE0); let texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null); let fbo = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); gl.viewport(0, 0, w, h); gl.clear(gl.COLOR_BUFFER_BIT); let texelSizeX = 1.0 / w; let texelSizeY = 1.0 / h; return { texture, fbo, width: w, height: h, texelSizeX, texelSizeY, attach(id) { gl.activeTexture(gl.TEXTURE0 + id); gl.bindTexture(gl.TEXTURE_2D, texture); return id; } }; } function createDoubleFBO(w, h, internalFormat, format, type, param) { let fbo1 = createFBO(w, h, internalFormat, format, type, param); let fbo2 = createFBO(w, h, internalFormat, format, type, param); return { width: w, height: h, texelSizeX: fbo1.texelSizeX, texelSizeY: fbo1.texelSizeY, get read() { return fbo1; }, set read(value) { fbo1 = value; }, get write() { return fbo2; }, set write(value) { fbo2 = value; }, swap() { let temp = fbo1; fbo1 = fbo2; fbo2 = temp; } }; } function resizeFBO(target, w, h, internalFormat, format, type, param) { let newFBO = createFBO(w, h, internalFormat, format, type, param); copyProgram.bind(); gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0)); blit(newFBO); return newFBO; } function resizeDoubleFBO(target, w, h, internalFormat, format, type, param) { if (target.width === w && target.height === h) return target; target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param); target.write = createFBO(w, h, internalFormat, format, type, param); target.width = w; target.height = h; target.texelSizeX = 1.0 / w; target.texelSizeY = 1.0 / h; return target; } function updateKeywords() { let displayKeywords = []; if (config.SHADING) displayKeywords.push('SHADING'); displayMaterial.setKeywords(displayKeywords); } updateKeywords(); initFramebuffers(); let lastUpdateTime = Date.now(); let colorUpdateTimer = 0.0; function updateFrame() { if (!isActive) return; const dt = calcDeltaTime(); if (resizeCanvas()) initFramebuffers(); updateColors(dt); applyInputs(); step(dt); render(null); animationFrameId.current = requestAnimationFrame(updateFrame); } function calcDeltaTime() { let now = Date.now(); let dt = (now - lastUpdateTime) / 1000; dt = Math.min(dt, 0.016666); lastUpdateTime = now; return dt; } function resizeCanvas() { let width = scaleByPixelRatio(canvas.clientWidth); let height = scaleByPixelRatio(canvas.clientHeight); if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; return true; } return false; } function updateColors(dt) { colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED; if (colorUpdateTimer >= 1) { colorUpdateTimer = wrap(colorUpdateTimer, 0, 1); pointers.forEach(p => { p.color = generateColor(); }); } } function applyInputs() { pointers.forEach(p => { if (p.moved) { p.moved = false; splatPointer(p); } }); } function step(dt) { gl.disable(gl.BLEND); curlProgram.bind(); gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0)); blit(curl); vorticityProgram.bind(); gl.uniform2f(vorticityProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0)); gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1)); gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL); gl.uniform1f(vorticityProgram.uniforms.dt, dt); blit(velocity.write); velocity.swap(); divergenceProgram.bind(); gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0)); blit(divergence); clearProgram.bind(); gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0)); gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE); blit(pressure.write); pressure.swap(); pressureProgram.bind(); gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0)); for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) { gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1)); blit(pressure.write); pressure.swap(); } gradienSubtractProgram.bind(); gl.uniform2f(gradienSubtractProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.read.attach(0)); gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1)); blit(velocity.write); velocity.swap(); advectionProgram.bind(); gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); if (!ext.supportLinearFiltering) gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, velocity.texelSizeX, velocity.texelSizeY); let velocityId = velocity.read.attach(0); gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId); gl.uniform1i(advectionProgram.uniforms.uSource, velocityId); gl.uniform1f(advectionProgram.uniforms.dt, dt); gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION); blit(velocity.write); velocity.swap(); if (!ext.supportLinearFiltering) gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY); gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0)); gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1)); gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION); blit(dye.write); dye.swap(); } function render(target) { gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.enable(gl.BLEND); drawDisplay(target); } function drawDisplay(target) { let width = target == null ? gl.drawingBufferWidth : target.width; let height = target == null ? gl.drawingBufferHeight : target.height; displayMaterial.bind(); if (config.SHADING) gl.uniform2f(displayMaterial.uniforms.texelSize, 1.0 / width, 1.0 / height); gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0)); blit(target); } function splatPointer(pointer) { let dx = pointer.deltaX * config.SPLAT_FORCE; let dy = pointer.deltaY * config.SPLAT_FORCE; splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color); } function clickSplat(pointer) { const color = generateColor(); color.r *= 10.0; color.g *= 10.0; color.b *= 10.0; let dx = 10 * (Math.random() - 0.5); let dy = 30 * (Math.random() - 0.5); splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color); } function splat(x, y, dx, dy, color) { splatProgram.bind(); gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0)); gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height); gl.uniform2f(splatProgram.uniforms.point, x, y); gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0); gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100.0)); blit(velocity.write); velocity.swap(); gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0)); gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b); blit(dye.write); dye.swap(); } function correctRadius(radius) { let aspectRatio = canvas.width / canvas.height; if (aspectRatio > 1) radius *= aspectRatio; return radius; } function updatePointerDownData(pointer, id, posX, posY) { pointer.id = id; pointer.down = true; pointer.moved = false; pointer.texcoordX = posX / canvas.width; pointer.texcoordY = 1.0 - posY / canvas.height; pointer.prevTexcoordX = pointer.texcoordX; pointer.prevTexcoordY = pointer.texcoordY; pointer.deltaX = 0; pointer.deltaY = 0; pointer.color = generateColor(); } function updatePointerMoveData(pointer, posX, posY, color) { pointer.prevTexcoordX = pointer.texcoordX; pointer.prevTexcoordY = pointer.texcoordY; pointer.texcoordX = posX / canvas.width; pointer.texcoordY = 1.0 - posY / canvas.height; pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX); pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY); pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0; pointer.color = color; } function updatePointerUpData(pointer) { pointer.down = false; } function correctDeltaX(delta) { let aspectRatio = canvas.width / canvas.height; if (aspectRatio < 1) delta *= aspectRatio; return delta; } function correctDeltaY(delta) { let aspectRatio = canvas.width / canvas.height; if (aspectRatio > 1) delta /= aspectRatio; return delta; } function generateColor() { let c = HSVtoRGB(Math.random(), 1.0, 1.0); c.r *= 0.15; c.g *= 0.15; c.b *= 0.15; return c; } function HSVtoRGB(h, s, v) { let r, g, b, i, f, p, q, t; i = Math.floor(h * 6); f = h * 6 - i; p = v * (1 - s); q = v * (1 - f * s); t = v * (1 - (1 - f) * s); switch (i % 6) { case 0: r = v; g = t; b = p; break; case 1: r = q; g = v; b = p; break; case 2: r = p; g = v; b = t; break; case 3: r = p; g = q; b = v; break; case 4: r = t; g = p; b = v; break; case 5: r = v; g = p; b = q; break; default: break; } return { r, g, b }; } function wrap(value, min, max) { const range = max - min; if (range === 0) return min; return ((value - min) % range) + min; } function getResolution(resolution) { let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight; if (aspectRatio < 1) aspectRatio = 1.0 / aspectRatio; const min = Math.round(resolution); const max = Math.round(resolution * aspectRatio); if (gl.drawingBufferWidth > gl.drawingBufferHeight) return { width: max, height: min }; else return { width: min, height: max }; } function scaleByPixelRatio(input) { const pixelRatio = window.devicePixelRatio || 1; return Math.floor(input * pixelRatio); } function hashCode(s) { if (s.length === 0) return 0; let hash = 0; for (let i = 0; i < s.length; i++) { hash = (hash << 5) - hash + s.charCodeAt(i); hash |= 0; } return hash; } // Named event handlers for proper cleanup function handleMouseDown(e) { let pointer = pointers[0]; let posX = scaleByPixelRatio(e.clientX); let posY = scaleByPixelRatio(e.clientY); updatePointerDownData(pointer, -1, posX, posY); clickSplat(pointer); } let firstMouseMoveHandled = false; function handleMouseMove(e) { let pointer = pointers[0]; let posX = scaleByPixelRatio(e.clientX); let posY = scaleByPixelRatio(e.clientY); if (!firstMouseMoveHandled) { let color = generateColor(); updatePointerMoveData(pointer, posX, posY, color); firstMouseMoveHandled = true; } else { updatePointerMoveData(pointer, posX, posY, pointer.color); } } function handleTouchStart(e) { const touches = e.targetTouches; let pointer = pointers[0]; for (let i = 0; i < touches.length; i++) { let posX = scaleByPixelRatio(touches[i].clientX); let posY = scaleByPixelRatio(touches[i].clientY); updatePointerDownData(pointer, touches[i].identifier, posX, posY); } } function handleTouchMove(e) { const touches = e.targetTouches; let pointer = pointers[0]; for (let i = 0; i < touches.length; i++) { let posX = scaleByPixelRatio(touches[i].clientX); let posY = scaleByPixelRatio(touches[i].clientY); updatePointerMoveData(pointer, posX, posY, pointer.color); } } function handleTouchEnd(e) { const touches = e.changedTouches; let pointer = pointers[0]; for (let i = 0; i < touches.length; i++) { updatePointerUpData(pointer); } } // Add event listeners window.addEventListener('mousedown', handleMouseDown); window.addEventListener('mousemove', handleMouseMove); window.addEventListener('touchstart', handleTouchStart); window.addEventListener('touchmove', handleTouchMove, false); window.addEventListener('touchend', handleTouchEnd); updateFrame(); // Cleanup function return () => { isActive = false; // Cancel animation frame if (animationFrameId.current) { cancelAnimationFrame(animationFrameId.current); animationFrameId.current = null; } // Remove event listeners window.removeEventListener('mousedown', handleMouseDown); window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('touchstart', handleTouchStart); window.removeEventListener('touchmove', handleTouchMove); window.removeEventListener('touchend', handleTouchEnd); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return (
); } export default SplashCursor;