Um girassol no bolso
índice
- 01Por que essas moedas parecem ter relevo?
- 02E a cor que muda com a luz?
- 03Como distribuir as moedas
- 04Uma moeda de cada vez
- 05De onde vêm os 137,5°
- 06Dando espaço pro desenho crescer
- 07As curvas também estão na flor
- 08Conferindo no próprio cartão
- 09De volta à tela de pedido
- 10Experimente no cartão
- 11Para continuar explorando

O que faz uma interface parecer bem feita? Quase nunca é uma coisa só. São dezenas de detalhes que ninguém nota sozinhos, mas que, somados, fazem a gente sentir que alguém se importou.
É isso que eu admiro no design da Apple, e foi por isso que perdi horas olhando pro cartão do Apple Cash (abre em nova guia), o cartão digital da Carteira do iPhone onde fica o dinheiro que você manda e recebe pelas Mensagens. No fundo, é um saldo numa tela.
Ninguém precisaria que ele fosse bonito pra ser útil. Mesmo assim, ele reage quando você inclina o celular e anda no bolso de muita gente, junto com a chave de casa, sem pedir atenção nenhuma.
Inclina o celular e a cor brilha pelas moedas como se fosse um CD sob a luz. Cada moeda tem uma setinha recortada e com um efeito cunhado 3D, e os espaços vazios entre elas desenham uma flor (pelo menos foi o que imaginai). Eu jurava que era um lótus. Errei a flor: era um girassol.
Tentando reproduzir esse cartão, fui juntando perguntas. São elas que o post responde:
- Esse padrão do cartão tem nome? E por que ele existe?
- Como reproduzir o brilho iridescente de um CD num computador, com WebGL?
- Como espalhar as moedas exatamente como na imagem de referência? Será que elas obedecem a uma função?
- O que um girassol tem a ver com um cartão de pagamento? E por que a natureza usa esse padrão?
- E a Apple usou mesmo essa regra, ou eu só quis ver uma flor? Será que eu tô doido?
Aviso antes: não sei se alguém na Apple pensou num girassol, nem quais ferramentas usaram. O que vem a seguir é uma reconstrução minha, apoiada no trabalho de outras pessoas, e não uma engenharia reversa.
Chegando mais perto, aparecem detalhes que eu tinha visto sem perceber:
Repara nas setas: elas não apontam todas pra cima. À esquerda, inclinam pra esquerda; no meio, sobem retas; à direita, abrem pra direita. Cada uma aponta pra fora do centro, acompanhando o leque do desenho. E, conforme sobem, as moedas vão encolhendo até virar pontinhos, e o padrão some no preto sem nenhuma borda marcando onde termina.
Olhar é uma coisa; querer fazer é outra. No começo de 2026, eu estava criando o programa de indicação para a nuture chamado rafobase (abre em nova guia) e vi ali a desculpa perfeita pra tentar reproduzir aquele cartão. Meses depois, desenhando a tela de detalhe de pedido do Nutools, o sistema operacional da Nuture que orquestra toda a nossa operação, e achei a tela meio sem graça… resolvi colocar um detalhe pequeno. Pequeno e completamente overengineered: levou a tarde toda (mas pelo menos deu a luz a esse post).
No caminho, passei por física da luz, uma técnica de videogame, um pouco de botânica e bastante código. Não comecei do zero: o Heshan Wijesinghe (abre em nova guia) já tinha recriado o cartão à sua visão, mas haviam ainda algumas perguntas sem resposta.
Subi no ombro dele, levei o efeito pra web, troquei as posições fixas por moedas geradas em código e fiz o caminho de volta: medi um print do cartão pra ver se a regra do girassol aparecia lá também.
Por que essas moedas parecem ter relevo?
A versão do rafobase foi quase toda construída em cima do trabalho do Heshan, que publicou como recriou o Apple Cash em Flutter (abre em nova guia). Aproveitei as texturas dele e a base do efeito pra fazer a minha adaptação na web.
No relato, ele conta que começou com gradientes e empacou no controle das curvas, principalmente em volta da seta. Foi isso que o levou a uma técnica usada em jogos: normal mapping.
O contorno da moeda mostra o problema. A borda parece arredondada; em volta da seta, a superfície muda de inclinação. Quando a luz se move, cada trecho precisa responder de um jeito. Um brilho que só atravessasse o círculo inteiro apagaria esses detalhes.
Na demonstração, veja a luz passar pelo contorno da seta. Depois assuma o controle, deixe a luz numa posição e escolha Sem relevo pra comparar sem mudar a luz de lugar.
O que aparece na tela
O recorte da moeda não muda.
O relevo representado · corte A–A
A luz se move sozinha. Arraste-a para assumir o controle; com teclado, use Tab e ← →.
As setas verdes no corte são as normais: direções perpendiculares a cada pedacinho da superfície. Na borda arredondada, elas se inclinam; na parte plana, apontam todas pro mesmo lado. Com todas paralelas, o recorte continua sendo uma moeda, mas a luz já não revela as curvas.
Um normal map (abre em nova guia) guarda essas direções numa imagem. Os canais vermelho, verde e azul armazenam as três componentes de cada seta. Por isso as cores estranhas abaixo: são dados que o programa lê pra calcular a iluminação.
Normal map de Heshan Wijesinghe, do projeto holographic-spiral (abre em nova guia). Exibido aqui em tamanho reduzido.
O Heshan montou esse material no Blender, combinando as normais da geometria com um mapa de alturas pra formar o chanfro da seta. Também fez a máscara que recorta o desenho. É o tipo de trabalho que some no resultado final, e que eu reaproveitei inteiro.
São duas informações diferentes: a máscara decide onde a moeda aparece; o normal map diz como a superfície responde à luz. O shader, que é o programa que calcula a cor de cada pixel na placa de vídeo, usa essas direções nas contas. A luz se move, a imagem do mapa continua exatamente igual, e o resultado muda.
Dá pra ver isso como uma conta: máscara vezes mapa, mais a luz, dá a moeda. Abaixo, o sol gira sozinho e só a última imagem muda. Toque num ponto de qualquer imagem pra ler o que o mapa guarda ali, ou arraste o sol pra colocar a luz onde quiser.
Máscaraonde tem moeda
×Normal mappra onde cada ponto aponta
+Luzarraste o sol
=Moedaa única que muda
rgb(22, 128, 199)a superfície inclina 56° pra esquerdapor isso recebe 82% da luzNa demo, a máscara e as inclinações da superfície ficam guardadas; mover a luz só recalcula a cor. Este é o núcleo em JavaScript, tirado do modelo da demo:
const normal = normalFromSlope(samples[i], samples[i + 1], relief);
const { diffuse, specular } = lighting(normal, light);
for (let c = 0; c < 3; c++) {
pixels[p + c] = Math.round(255 * Math.sqrt(
clamp((color[c] / 255) ** 2 * diffuse + specular, 0, 1)
));
}
samples[i] e samples[i + 1] guardam as inclinações; samples[i + 2] controla a transparência da máscara. A função lighting usa o produto escalar pra medir o quanto a normal aponta pra luz: quanto mais alinhadas, mais brilho. Ela devolve as parcelas difusa e especular.
O normal map não acrescenta volume nenhum à geometria. Ele ilumina uma superfície plana como se ela tivesse aquelas pequenas inclinações. Uma imagem guardando relevo, que continua convincente enquanto a luz passeia, sem precisar de uma textura nova pra cada posição. Truque de videogame dos bons.
E a cor que muda com a luz?
O relevo explica parte da aparência. Falta a cor que passeia pela superfície, parecida com a que aparece quando a gente inclina um CD.
Essa mudança de cor conforme o ângulo tem nome: iridescência. No CD, as trilhas microscópicas, muito próximas e regularmente espaçadas, funcionam como uma rede de difração.
Pra entender o que isso faz, dá pra pensar na luz como onda. A luz branca junta vários comprimentos de onda, cada um associado a uma cor. Ao passar por uma estrutura repetida, essas ondas se sobrepõem: em algumas direções se reforçam, em outras se cancelam. Isso é interferência. Como o resultado depende do comprimento de onda, cada cor fica mais forte numa direção diferente. Inclinar o disco muda quais cores chegam ao seu olho. O Alan Zucconi explica bem essa relação entre a estrutura da superfície, a difração e a cor (abre em nova guia).
Veja uma onda se deslocar em relação à outra; depois pause e experimente outros pontos. Quando um pico encontra um vale de mesma amplitude, a soma naquele ponto é zero. Quando os picos coincidem, eles se reforçam.
Onda AOnda B
Soma das duas · amplitude 2,00× a de uma onda
O gráfico soma duas ondas da mesma frequência. Deslocar a segunda é mudar só a fase; a primeira continua igual:
export function waveSum(x, phase) {
return Math.sin(x) + Math.sin(x + phase * Math.PI / 180);
}
Foi no trabalho do Zucconi que o Heshan achou a referência pra essa parte do efeito. Ele testou o shader no KodeLife, vendo cada ajuste em tempo real, antes de levar pro Flutter.
No código que aproveitei, a função spectral_zucconi6 (abre em nova guia) produz uma aproximação das cores do espectro. A posição do foco e a orientação guardada no normal map entram na escolha da cor de cada ponto. Assim, a faixa colorida acompanha os detalhes da moeda em vez de passar por cima deles como uma pintura uniforme.
É uma aproximação visual, não uma simulação da física de uma rede de difração, e pra um cartão isso basta. Na minha adaptação, mantive essa base, ajustei a iluminação e fiz o foco do efeito seguir o mouse ou a inclinação do celular, pra superfície responder a quem mexe no cartão.
No port pra WebGL que aparece abaixo, esta é a conta de verdade no fragment shader. Aqui a linguagem é GLSL, porque é o programa que roda na placa de vídeo. A relação entre normal, foco e cor foi adaptada da recriação do Heshan; spectral_zucconi6 é do Zucconi. colorDistance é um parâmetro do efeito, não um comprimento de onda de verdade:
vec3 normal = normalize(texture2D(normalMap, st).rgb * 2. - 1.);
normal.x = -normal.x;
vec2 delta = st - focus;
float distanceToFocus = length(delta);
vec3 direction = vec3(delta / max(distanceToFocus, .00001), 1.);
float incidence = dot(normal, direction);
float colorDistance = distanceToFocus - incidence * incidence * .5;
vec3 color = mix(spectral_zucconi6(colorDistance), vec3(1., 1., 1.25), .25);
float mask = texture2D(alphaMap, st).a;
vec3 background = mix(vec3(.065), vec3(.035), uv.y);
gl_FragColor = vec4(mix(background, color, mask), 1.);
Abaixo, o vídeo do site da Apple aparece ao lado do port em WebGL, usando as texturas do Heshan. Os dois começam a se mover quando entram na tela. Olhe as bordas das moedas e das setas. Depois pause ou mova o foco no render: o vídeo tem o movimento gravado; o render recalcula a cor conforme você mexe. Os parâmetros e o movimento não são os mesmos, então a comparação é de comportamento, não de pixel.
Apple Vídeo original
Arquivo original, com o telefone fora do recorte. Ver vídeo completo.
Reconstrução Carregando WebGL
Preparando as texturas e o shader…
Mova o mouse ou arraste. A cor é calculada a cada mudança do foco.
A comparação começa quando entra na tela, sem som, e pausa ao sair. O vídeo não responde ao ponteiro; o render, sim. Mover o foco manualmente pausa a animação.
Tinha trabalho também na distribuição. O Heshan saiu de um script em Python pra Geometry Nodes no Blender, onde conseguia ajustar o ângulo entre as moedas e controlar o tamanho delas com uma curva. Ele disponibilizou o código, as texturas e o arquivo do Blender (abre em nova guia).
Na versão do rafobase, as posições vinham prontas nessas texturas. Dava pra mexer na luz, mas não numa moeda. Pro rafobase, tudo bem. Pro Nutools, não ia servir.
Como distribuir as moedas
Até aqui, o mérito é quase todo do Heshan; eu só expliquei o que aprendi com ele. Daqui pra frente, o caminho é meu.
Na tela de pedido do Nutools, o padrão ia ocupar um recorte pequeno no canto direito de um card, e dessa vez eu queria gerar as moedas por código.
Fui na ideia mais óbvia: olhei a referência e desenhei cinco anéis concêntricos, com mais moedas nos anéis de fora. Relevo e cor seguindo o mouse, tudo no lugar:
Ficou bonito. Não ficou parecido. Poucas moedas, algumas grandes demais, e os intervalos entre os anéis bem marcados. No Apple Cash, eu via curvas atravessando o conjunto nos dois sentidos, as pétalas do tal lótus. Nos meus anéis, eu via anéis.
A comparação abaixo mostra a versão com anéis e a reconstrução com espiral que veio depois. À direita, além do arranjo novo, há muito mais moedas, menores e mais próximas:
A pista estava no artigo do Heshan o tempo todo: entre uma moeda e a próxima, ele girava −137,508°. Eu podia copiar o número e seguir em frente. Mas queria entender o que ele tinha a ver com o desenho.
Até ali eu pensava em anéis: escolher um raio, preencher o círculo, passar pro próximo. A regra dele sugeria outro jeito de construir: colocar uma moeda de cada vez, girando sempre o mesmo tanto em volta do centro.
Uma moeda de cada vez
Veja as moedas surgirem primeiro. Depois escolha o giro de 180° e volte a quantidade pra uma moeda. Acrescente as próximas, uma por vez, e repare nas direções que elas ocupam.
A primeira moeda vai perto do centro. Pra segunda, você dá meia-volta e se afasta um pouco. Mais meia-volta pra terceira, mais um passinho pra fora, e ela cai na direção da primeira.
Com 180°, você forma duas fileiras. Pode colocar mil moedas: elas vão continuar se alternando entre os mesmos dois lados, e o resto do espaço fica vazio.
Um terço de volta, 120°, abre uma terceira direção, mas logo repete o caminho: a cada três passos, uma volta completa. Com 144°, a repetição leva cinco passos, que somam duas voltas. Cinco fileiras.
Isso acontece porque esses giros são frações simples de uma volta: metade, um terço, dois quintos. Depois de poucos passos a conta fecha, e as moedas voltam pra direções já usadas. Pra preencher o disco, cada moeda nova precisaria achar um espaço diferente.
Dá pra escapar da repetição exata usando uma fração irracional da volta, que não pode ser escrita como divisão entre dois inteiros. Só que chegar quase na mesma direção também atrapalha. Um giro muito perto de 120° ainda desenha três fileiras bem visíveis por vários passos; elas só vão entortando aos poucos.
É aqui que o número do Heshan começa a fazer sentido. O giro de aproximadamente 137,5° é uma fração da volta especialmente difícil de aproximar por frações de números pequenos. Ele evita esses quase-reencontros e ajuda as moedas a se espalharem. Essa mesma propriedade é estudada pra entender a distribuição das folhas nas plantas (abre em nova guia).
De onde vêm os 137,5°
Esse giro se chama ângulo áureo porque vem da proporção áurea. O nome soa como papo de estética, mas dá pra acompanhar a construção inteira.
Pegue uma volta completa e divida em dois arcos, um maior e um menor. Procure a divisão em que a volta inteira está pro arco maior na mesma proporção que o arco maior está pro menor:
volta inteira / arco maior = arco maior / arco menor
Antes da conta, tente achar essa divisão no olho. Arraste o ponto no círculo e compare as duas razões. O botão mostra onde elas coincidem.
Arraste o ponto. Com teclado, use Tab e as setas.
Essa é a condição que define a proporção áurea. A razão que satisfaz a igualdade vale aproximadamente 1,618 e costuma ser representada pela letra grega φ. Numa volta de 360°, os dois arcos medem aproximadamente 222,492° e 137,508°. Dá pra conferir:
360 / 222,492 ≈ 222,492 / 137,508 ≈ 1,618
O arco menor é o ângulo áureo. É daí que vem o número do Heshan; o sinal negativo na implementação dele só escolhe o sentido do giro.
O legal é ver a proporção trabalhando: repetir esse giro muda o jeito como centenas de moedas dividem o espaço. Na demonstração, quantidade, tamanho e distâncias ficam iguais. Só o giro muda.
Veja o desenho mudar enquanto o giro oscila em torno do ângulo áureo. Depois experimente 137° e 138°. Olhe os espaços vazios e repare nos corredores que se abrem entre os braços do desenho.
Entre o ângulo áureo e cada um desses valores há cerca de meio grau de diferença. Parece nada, mas o desvio se acumula a cada ponto novo. Em algumas centenas de pontos, o desenho já é outro.
Dando espaço pro desenho crescer
O giro escolhe a direção da próxima moeda. Falta decidir a distância até o centro.
Se a distância aumentar sempre pelo mesmo valor, as moedas vão ficando mais espalhadas conforme o desenho cresce. Um passo pra fora perto da borda cobre uma faixa de área muito maior do que o mesmo passo perto do centro.
Pra manter o preenchimento uniforme, a área precisa crescer junto com o número de moedas. Quatro vezes mais moedas pedem quatro vezes mais área, e isso significa um raio duas vezes maior, porque a área de um círculo cresce com o quadrado do raio.
Por isso aparece uma raiz quadrada na conta. Ela faz o raio crescer no ritmo certo pra que cada moeda nova acrescente a mesma quantidade de área ao conjunto.
Veja os dois discos crescerem. As linhas dividem cada um em quatro partes de mesma área. Repare quantos pontos se amontoam no meio quando o raio cresce de forma linear.
Raio proporcional a n
No centro (¼ da área): 40 pontos
Raio proporcional a √n
No centro (¼ da área): 20 pontos
Mesmos pontos, giro, tamanho e raio externo. Só muda a regra da distância.
Juntando esse crescimento com o ângulo áureo, chega-se ao modelo que Helmut Vogel publicou em 1979 (abre em nova guia) pra reproduzir o arranjo de uma cabeça de girassol.
Volte na foto da abertura. No miolo, dá pra seguir pequenas formas verdes em curvas que vão nos dois sentidos e se cruzam. Pra deixar essas curvas mais visíveis, gerei pontos com o modelo e liguei vizinhos de três jeitos diferentes:
Essas linhas são guias sobre um modelo geométrico, não uma medição da fotografia do girassol.
O miolo do girassol junta muitas florzinhas, e é delas que as sementes se desenvolvem. O modelo de Vogel aproxima esse arranjo colocando um ponto de cada vez. Na minha reconstrução, cada ponto vira uma moeda. Pra moeda de número n:
distância do centro = c · √n
ângulo = n · 137,508…°
O c ajusta a escala: quanto maior, mais afastadas ficam todas as moedas. A raiz quadrada cuida da relação entre quantidade e área; o ângulo, das direções.
Pra passar de distância e ângulo pra coordenadas na tela, entram seno e cosseno. A função das demonstrações gera posições num disco de raio 1; no card, eu só escalo esse disco e escolho onde fica o centro:
export const PHI = (1 + Math.sqrt(5)) / 2;
export const GOLDEN_ANGLE = 360 / (PHI * PHI);
export function pattern(count, angle = GOLDEN_ANGLE, law = 'sqrt', capacity = count) {
return Array.from({ length: count }, (_, i) => {
const n = i + 1;
const fraction = n / capacity;
const r = law === 'linear' ? fraction : Math.sqrt(fraction);
const theta = n * angle * Math.PI / 180;
return { n, r, theta, x: r * Math.cos(theta), y: r * Math.sin(theta) };
});
}
count diz quantos pontos já apareceram; capacity fixa o tamanho do desenho completo, pra que os pontos antigos não mudem de lugar enquanto a animação acrescenta novos. A opção linear existe só pra comparação da demonstração. No card, é raiz quadrada.
Foi aqui que o projeto virou outra coisa pra mim. O desenho que eu queria copiar de um cartão seguia uma regra que também descreve uma flor. E, em poucas linhas, dava pra entender cada pedaço da conta, mexer nos valores e ver o resultado na hora.
As curvas também estão na flor
No modelo, pontos seguidos na numeração ficam separados por aquele giro de quase 138°. Mas os vizinhos no desenho podem estar a várias posições de distância na numeração. Seguindo esses vizinhos, aparecem as espirais.
Na demonstração, veja um caminho ser traçado. Depois mude o salto de oito pra treze e pra vinte e um. Os pontos não saem do lugar; o que muda é quais vizinhos estamos ligando.
Esses números são da sequência de Fibonacci, em que cada número é a soma dos dois anteriores. A razão entre dois números seguidos se aproxima da proporção áurea. No desenho, isso ajuda a explicar por que certos saltos encontram pontos em direções próximas e formam famílias de curvas.
Dá pra procurar essas curvas numa pinha também. Na foto abaixo, comece pela ponta e siga as escamas em volta dela. O relevo ajuda a ver como cada peça se encaixa entre as vizinhas:
Fotografia de Richard Flink, Fibonacci’s Cone (abre em nova guia), CC BY-SA 2.0 (abre em nova guia). Sem alterações.
Na pinha, as espirais percorrem uma superfície alongada; no girassol, se espalham por um disco. Esse arranjo de partes das plantas, como folhas, flores e escamas, se chama filotaxia. O capítulo sobre filotaxia de The Algorithmic Beauty of Plants (abre em nova guia), de Przemysław Prusinkiewicz e Aristid Lindenmayer, faz exatamente essa ponte entre observar as formas e encontrar modelos pra desenhá-las, de flores a pinhas e abacaxis.
Então sim, o padrão do cartão tem nome. Nas plantas, esse arranjo é filotaxia; a regra que desenha as moedas é uma espiral de Vogel.
O modelo é uma aproximação geométrica. Girassóis de verdade têm irregularidades, e as espirais nem sempre batem com os números de Fibonacci. Essas variações foram documentadas num estudo com girassóis cultivados por voluntários (abre em nova guia).
Olhar pra uma planta crescendo levanta outra pergunta: como esse arranjo se forma? Christophe Godin, Christophe Golé e Stéphane Douady discutem como a posição dos órgãos que já estão se formando limita o espaço pros próximos (abre em nova guia). Na explicação deles, a organização vai aparecendo aos poucos, a partir dessas interações locais durante o crescimento.
E por que as plantas fariam isso? A resposta curta é espaço. Cada broto novo tende a aparecer onde sobra mais lugar entre os que já estão ali. Repetido muitas vezes, esse jeito de ocupar o espaço leva a um giro perto do ângulo áureo, o mesmo que evita os quase-reencontros das moedas. Num disco de girassol, isso quer dizer muita semente e pouco vão desperdiçado.
E tem um experimento que tira a pergunta das plantas. Em 1992, Douady e Couder reproduziram padrões de filotaxia num experimento físico e numa simulação (abre em nova guia). Os padrões ligados a Fibonacci surgiam sozinhos, por auto-organização, conforme novos elementos entravam no sistema.
A mesma ideia aparece em escalas completamente diferentes: um ponto entrando num desenho, uma organização surgindo ao longo de um crescimento, curvas visíveis numa flor. A fórmula deixou de ser só um jeito de posicionar moedas.
Conferindo no próprio cartão
O Heshan chegou ao ângulo, e o modelo de Vogel reproduz um girassol. Mas a pergunta que eu queria responder era outra: o cartão da Apple confirma isso? Dá pra tirar a regra de volta a partir de uma imagem?
Comecei pelo que a imagem oferecia: pixels. Separei as regiões claras das escuras, preenchi o furo da seta no meio de cada moeda e localizei o centro das formas isoladas. O script encontrou 284 formas candidatas. Não eram 284 moedas: algumas eram letras do cartão, outras eram moedas cortadas ou cobertas. Tirando as detecções fora da cúpula e as letras de “VISA”, sobraram 267 pontos pra estudar.
A segmentação rodou em Python com OpenCV: marquei os pixels cujo maior canal RGB passava de 120, preenchi os furos das setas pra que elas não quebrassem uma moeda em pedaços e separei as formas conectadas. A conta que transforma cada região num ponto, traduzida pra TypeScript, é esta. O raio é o de um círculo com a mesma área da forma, uma estimativa, não a borda exata:
type Region = { centerX: number; centerY: number; area: number };
function coinCandidates(regions: Region[]) {
return regions
.filter(({ area }) => area > 3 && area < 5000)
.map(({ centerX, centerY, area }) => ({
x: centerX,
y: centerY,
radius: Math.sqrt(area / Math.PI),
}));
}
Ordenei esses centros pela distância ao meio da base. Colocando essa distância contra a raiz quadrada da ordem dos pontos visíveis, apareceu uma relação quase linear. Primeira pista tirada direto da Apple: a quantidade de moedas cresce mais ou menos com a área ocupada. Só que a ordem das moedas que eu consigo ver não é o número original de cada uma na sequência, porque o recorte esconde parte do desenho. A reta sozinha não prova Vogel.
Faltava a pergunta mais difícil: quanto o desenho gira de uma moeda pra próxima? A busca percorreu ângulos de 130° a 145°, em passos de 0,5°. Pra cada ângulo, testei os dois sentidos de giro e ajustei centro, espaçamento e rotação inicial, comparando os pontos gerados com os centros medidos. A pontuação do script, em TypeScript:
type Point = readonly [number, number];
function nearestDistanceSquared([x, y]: Point, points: Point[]) {
let best = Infinity;
for (const [px, py] of points) {
const dx = x - px;
const dy = y - py;
best = Math.min(best, dx * dx + dy * dy);
}
return Math.min(best, 625); // 25 pixels ao quadrado
}
function fitScore(measured: Point[], generated: Point[]) {
if (!measured.length || !generated.length) return Infinity;
const average = (from: Point[], to: Point[]) =>
from.reduce((sum, point) => sum + nearestDistanceSquared(point, to), 0) / from.length;
return average(measured, generated)
+ average(generated, measured)
+ 1.5 * Math.abs(generated.length - measured.length);
}
A comparação vai nos dois sentidos porque uma hipótese que acerta algumas moedas mas inventa várias outras também precisa perder pontos. Cada distância fica limitada a 25 pixels, pra que moedas cortadas ou escondidas não dominem o erro.
Não dei 137,508° como resposta pra busca. Ela encontrou mais de um encaixe plausível: um perto de 137,5° e outro perto de 139,5°. Esse é o achado. O print é compatível com uma espiral de Vogel, mas não carrega informação suficiente pra cravar o ângulo. A hipótese do Heshan passa no teste; só não dá pra dizer, a partir de uma imagem parcial, que ela é a única.
Então não, eu não tô doido: a flor está lá. Só não dá pra provar que é exatamente a mesma.
Medi também a área de cada forma pra estimar o tamanho das moedas. Agrupadas pela distância ao centro, elas crescem em direção à parte de fora da cúpula e voltam a diminuir perto da borda. Transformei essa tendência numa curva pro meu render.
No experimento abaixo, dá pra acompanhar cada passo sobre o print, mudar os parâmetros e ver onde a reconstrução se aproxima e onde ela falha.
O script separou pixels claros, preencheu o furo de cada seta e mediu o centro e a área de cada forma isolada. Cada círculo abaixo marca uma detecção, não uma moeda confirmada.
A primeira contagem deu 284 formas candidatas. Treze estão fora da cúpula estimada; outras quatro são letras de “VISA” reconhecidas como manchas. Mesmo após esse filtro, a lista continua sendo uma medida aproximada da imagem.
Não saiu um encaixe moeda por moeda, e nem precisava. As medidas viraram régua pra ajustar a reconstrução, junto com a comparação no olho.
De volta à tela de pedido
Com o Vogel, passei a gerar o padrão direto no shader. A primeira tentativa tinha espaçamento demais: umas sessenta moedas no recorte, tudo meio ralo. Com as medidas, cheguei a cerca de 170 moedas visíveis.
Aí voltei pro que tinha me prendido no começo: os detalhes. O relevo precisava aparecer naquele tamanho. A luz passou a seguir o mouse com um pequeno atraso, pro movimento ficar suave. Na borda esquerda, o padrão some antes de disputar espaço com o texto.
Algumas escolhas mudaram pra caber na tela. As moedas menores ficaram proporcionalmente maiores que as da referência, porque a seta sumia quando eu reduzia mais. E as cores ficaram presas ao status do pagamento: verde pra aprovado, vermelho pra recusado. O efeito passou a repetir a informação que a tela já mostrava, em vez de só enfeitar.
O fade perto do texto, por exemplo, é só isto. A opacidade de cada moeda depende da posição horizontal x: à esquerda, perto da mensagem, ela diminui; à direita, chega a 1. A curva suave evita uma faixa dura entre as duas áreas:
const t = clamp((x - 280) / 180, 0, 1);
ctx.globalAlpha = 1 - fade / 100 * (1 - t * t * (3 - 2 * t));
E teve o cuidado que ninguém vê. Quando a luz alcança o ponteiro, a animação para de pedir quadros novos. Quem prefere menos movimento vê o desenho parado. Sem WebGL, a mesma distribuição é desenhada em SVG.
Experimente no cartão
Agora dá pra juntar tudo. A luz percorre o cartão uma vez quando ele entra na tela. Depois, mova a luz, diminua as moedas e tire o fade perto do texto. Repare no que acontece com a seta e com a leitura da mensagem. Experimente também o estado recusado.
Este sandbox foi feito pro artigo, com a moeda e o modelo de Vogel das demonstrações. Ele isola os ajustes de acabamento; a iridescência ficou de fora.
EXPERIMENTO · CARTEIRA
Pagamento aprovadoTudo certo por aqui.A luz faz um percurso curto ao entrar na tela. Depois arraste-a; com teclado, Tab e setas. Com movimento reduzido, a resposta é direta.
Uma tarde inteira num detalhe de uma tela de pedido. Quase ninguém vai reparar nas moedas, mas quem abrir aquela tela vai sentir que alguém se importou. Que é exatamente o que o cartão da Apple fez comigo.
No caminho, aprendi por que um CD muda de cor, como um videogame finge relevo e que regra descreve as sementes de um girassol. O cartão nunca chegou no meu bolso, mas agora eu sei que tem um girassol dentro dele. E, na tela de pedido do Nutools, tem outro.
Se você sair daqui com vontade de ir atrás de alguma dessas referências, ou de juntar coisas de áreas diferentes num detalhe que ninguém pediu, o post cumpriu o papel.
Para continuar explorando
- A recriação que serviu de base: How I Replicate The Apple Cash Card UI in Flutter (abre em nova guia), de Heshan Wijesinghe, e seu projeto com código, texturas e arquivo Blender (abre em nova guia).
- Entender o relevo sem acrescentar geometria: Introduction to normal maps (abre em nova guia), da documentação do Unity. Explica as normais da superfície e como suas direções podem ser armazenadas numa textura.
- Entender a cor que depende do ângulo: Understanding Diffraction Grating (abre em nova guia), de Alan Zucconi, e sua série sobre iridescência. O texto Improving the Rainbow (abre em nova guia) apresenta aproximações computacionais das cores do espectro.
- Desenhar plantas com matemática: capítulo 4 de The Algorithmic Beauty of Plants (abre em nova guia), de Przemysław Prusinkiewicz e Aristid Lindenmayer. Apresenta o modelo de Vogel e modelos para estruturas alongadas como pinhas.
- Entender o crescimento: Phyllotaxis as geometric canalization during plant development (abre em nova guia), de Christophe Godin, Christophe Golé e Stéphane Douady (2020). Relaciona geometria, formação de novos órgãos e padrões nas plantas.
- Ver o padrão surgir em um experimento: Phyllotaxis as a physical self-organized growth process (abre em nova guia), de Stéphane Douady e Yves Couder (1992).
- Conferir a diversidade dos girassóis reais: Novel Fibonacci and non-Fibonacci structure in the sunflower (abre em nova guia), de Jonathan Swinton, Erinma Ochu e o MSI Turing’s Sunflower Consortium (2016). O estudo reúne dados de 657 girassóis e disponibiliza os dados para consulta.