Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
poweredByHeader: false
poweredByHeader: false,
webpack: (config, { isServer }) => {
if (!isServer) {
// O build do ammo.js vem do emscripten e referencia `fs`/`path` para o
// caminho Node, que nunca é tomado no navegador. Sem estes stubs o
// webpack quebra o build inteiro tentando resolvê-los (#99).
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
path: false,
crypto: false
};
}
return config;
}
};
export default nextConfig;
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,22 @@
"dependencies": {
"@next/env": "14.2.18",
"@supabase/supabase-js": "^2.108.1",
"ammojs-typed": "^1.0.6",
"clsx": "^2.1.1",
"framer-motion": "^11.11.17",
"lucide-react": "^0.460.0",
"nanoid": "^5.0.9",
"next": "14.2.18",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^2.5.4"
"tailwind-merge": "^2.5.4",
"three": "^0.185.1"
},
"devDependencies": {
"@types/node": "^22.9.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/three": "^0.185.1",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
"eslint-config-next": "14.2.18",
Expand Down
61 changes: 61 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added public/banners/bash.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/c.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/clojure.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/cobol.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/cpp.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/csharp.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/dart.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/elixir.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/erlang.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/fortran.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/go.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/haskell.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/java.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/javascript.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/julia.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/kotlin.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/lua.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/php.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/python.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/ruby.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/rust.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/scala.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/sql.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/swift.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/banners/typescript.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
122 changes: 122 additions & 0 deletions scripts/slice-banners.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Fatia as composicoes de estandartes (#97) em um asset por linguagem.

Ferramenta de conteudo, roda a mao — nao entra no build nem na CI.

pip install pillow numpy scipy
python scripts/slice-banners.py public/banners 640 80

As composicoes de origem (`ref1.png`, `ref2.png`, `ref3.png`) NAO sao versionadas
— sao os anexos da issue #97, com ~2,5 MB cada. Baixe-as para o diretorio de
trabalho antes de rodar:

ref1 -> https://github.com/user-attachments/assets/d8f1ce62-3b96-496e-8d80-7cb4976ea51a
ref2 -> https://github.com/user-attachments/assets/2815635c-e653-47a5-a343-218215f943a3
ref3 -> https://github.com/user-attachments/assets/b645adf0-13b8-4da8-ab9a-d38180a7d084

RECORTE POR SCANLINE (o alpha e essencial: os estandartes ficam suspensos sobre
o fundo da aplicacao, sem moldura). Duas abordagens obvias falham e vale
registrar por que, para ninguem repeti-las:

* flood do fundo escuro conectado as bordas: o veludo de PHP, C#, C++, SQL e
Ruby tem luminancia ~9-16, igual ao fundo — o flood entra pelo topo (os
estandartes encostam na borda da composicao) e come o tecido, sobrando so os
detalhes dourados;
* preencher o contorno da moldura: o fio dourado se interrompe em varios
pontos e o preenchimento vaza (Python ficou com 12% de cobertura).

O scanline resolve porque so precisa do limite EXTERNO: varre cada linha de fora
para dentro ate achar o primeiro pixel claro (a moldura, que salta para 46-148
contra um fundo de ~9-16) e preenche entre os dois limites. O interior escuro
nunca e visitado, entao nao ha o que comer. A ponta em V e a franja saem de
graca: nas linhas de baixo os dois limites convergem.
"""
import sys
import numpy as np
from scipy import ndimage
from PIL import Image, ImageFilter

RATIO = 5.0 # altura / largura da celula final
EDGE = 40 # luminancia que marca a moldura do estandarte
SEG = 34 # luminancia de fundo, para separar as colunas


def luminance(im):
a = np.asarray(im.convert("RGB")).astype(np.float32)
return 0.299 * a[:, :, 0] + 0.587 * a[:, :, 1] + 0.114 * a[:, :, 2]


def segment(lum):
"""Limites de coluna de cada estandarte na composicao."""
lab, _ = ndimage.label(lum < SEG, structure=np.ones((3, 3)))
edge = set(lab[:, 0]) | set(lab[:, -1]) | set(lab[-1, :])
edge.discard(0)
has = (~np.isin(lab, list(edge))).max(axis=0)
runs, start = [], None
for x, v in enumerate(has):
if v and start is None:
start = x
elif not v and start is not None:
if x - start >= 40:
runs.append((start, x))
start = None
if start is not None and len(has) - start >= 40:
runs.append((start, len(has)))
return runs


def scanline_alpha(cell_lum):
"""Alpha do estandarte: preenche entre a primeira e a ultima coluna clara.

Linhas SEM nenhum pixel claro ficam transparentes — e o que corta a ponta em
V e o que sobra acima/abaixo. Nao suavize os limites usando essas linhas
vazias como se fossem borda em zero: perto da base a maioria das linhas e
vazia, e a media puxaria o limite para a coluna 0, preenchendo a largura
inteira exatamente onde o V deveria afunilar.
"""
h, w = cell_lum.shape
bright = cell_lum > EDGE
mask = np.zeros((h, w), bool)
for y in range(h):
xs = np.flatnonzero(bright[y])
if xs.size:
mask[y, xs[0] : xs[-1] + 1] = True
# Fecha frestas de 1-2 linhas onde a moldura escurece abaixo do limiar, sem
# deslocar a silhueta (fechamento so na vertical).
return (ndimage.binary_closing(mask, structure=np.ones((5, 1))) * 255).astype(np.uint8)


def main(path, names, out_dir, target_h, quality):
im = Image.open(path).convert("RGB")
lum = luminance(im)
runs = segment(lum)
print(f"{path}: {len(runs)} estandartes (esperado {len(names)})")
if len(runs) != len(names):
sys.exit("ABORTA: contagem divergente")

tw = round(target_h / RATIO)
for (x0, x1), name in zip(runs, names):
a0, a1 = max(0, x0 - 3), min(im.width, x1 + 3)
cell = im.crop((a0, 0, a1, im.height)).convert("RGBA")
alpha = scanline_alpha(lum[:, a0:a1])
cell.putalpha(Image.fromarray(alpha).filter(ImageFilter.GaussianBlur(0.6)))
bbox = cell.split()[3].getbbox()
cell = cell.crop(bbox)
# Escala pela ALTURA e centraliza numa celula de razao fixa: o carousel
# fica com itens identicos e cada estandarte mantem sua proporcao.
w = round(cell.width * target_h / cell.height)
cell = cell.resize((w, target_h), Image.LANCZOS)
canvas = Image.new("RGBA", (tw, target_h), (0, 0, 0, 0))
canvas.paste(cell, ((tw - w) // 2, 0), cell)
canvas.save(f"{out_dir}/{name}.webp", "WEBP", quality=quality, method=6)
cover = np.asarray(canvas.split()[3]).mean() / 255
print(f" {name:12s} -> {tw}x{target_h} cobertura={cover:.0%}")


if __name__ == "__main__":
REF1 = ["julia", "clojure", "fortran", "cobol", "c"]
REF2 = ["typescript", "bash", "sql", "ruby", "lua", "dart", "scala", "elixir", "erlang", "haskell"]
REF3 = ["python", "javascript", "cpp", "java", "csharp", "php", "rust", "go", "swift", "kotlin"]
out_dir, target_h, q = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
main("ref1.png", REF1, out_dir, target_h, q)
main("ref2.png", REF2, out_dir, target_h, q)
main("ref3.png", REF3, out_dir, target_h, q)
36 changes: 36 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,42 @@
font-weight: 600;
}

/* Gôndola de estandartes (#97/#99). Sem fundo próprio: os estandartes ficam
suspensos sobre o fundo da aplicação. `data-centered` é escrito pelo rAF do
BannerCarousel — nada aqui depende de re-render do React.

Os estandartes têm alpha, então sombra e brilho vão em `filter` (drop-shadow
segue a silhueta) e não em `box-shadow`, que desenharia a caixa retangular
e devolveria a moldura que a #99 pediu para tirar. */
/* Com o motor pesado ativo, o tecido é a malha do ammo no canvas: o pano do
DOM some, mas o botão continua ali com semântica, foco e área de clique. */
.banner-gondola[data-heavy="true"] .banner-cloth {
opacity: 0;
}
.banner-item .banner-cloth {
transition: filter 200ms ease-out;
/* Sem drop-shadow aqui: são 24 estandartes, e sombra na silhueta é cara.
Só quem está em evidência (1–2 por vez) paga esse custo. */
filter: brightness(0.5) saturate(0.6);
}
/* Centralizado = à luz da tocha, com destaque forte contra os vizinhos. */
.banner-item[data-centered="true"] .banner-cloth {
filter: brightness(1.12) saturate(1.15) contrast(1.06)
drop-shadow(0 10px 18px rgba(0, 0, 0, 0.65));
}
/* Escolhido = halo verde na silhueta. Estado distinto do centralizado: na
votação do lobby dá para navegar pela gôndola sem votar. */
.banner-item[aria-checked="true"] .banner-cloth {
filter: brightness(1.14) saturate(1.2) contrast(1.06)
drop-shadow(0 0 5px rgba(0, 255, 136, 0.85)) drop-shadow(0 0 14px rgba(0, 255, 136, 0.4))
drop-shadow(0 10px 18px rgba(0, 0, 0, 0.65));
}
.banner-item:focus-visible .banner-cloth {
filter: brightness(1.14) saturate(1.15)
drop-shadow(0 0 4px rgba(0, 229, 255, 0.95)) drop-shadow(0 0 12px rgba(0, 229, 255, 0.5))
drop-shadow(0 10px 18px rgba(0, 0, 0, 0.65));
}

.glow-text-green { text-shadow: 0 0 8px rgba(0,255,136,0.6); }
.glow-text-cyan { text-shadow: 0 0 8px rgba(0,229,255,0.6); }
.glow-text-violet { text-shadow: 0 0 8px rgba(168,85,247,0.6); }
Expand Down
Loading
Loading