Compare commits

..

7 commits

Author SHA1 Message Date
vini
2f4994918d feat: Via Láctea estilo astrofoto + capa/letra trocadas de lado
Via Láctea em três camadas ancoradas em coordenadas galácticas reais:
véu difuso (3600 blobs) com gradiente térmico — creme-âmbar no núcleo,
azul no anticentro —, grão estelar fino (9500 pontos sub-pixel) e
estrutura: bojo central, Grande Fenda + faixa de poeira secundária,
14 nuvens estelares (Scutum/Sagitário/Carina).

Layout: capa do álbum vai para a esquerda, letra para a direita
(alinhada à direita).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj
2026-07-19 11:08:07 -03:00
vini
0474a9ab83 feat: vista do céu voltada para o leste — a tela física olha p/ leste
O wallpaper age como janela real: estrelas nascem subindo dentro da
cena e o núcleo galáctico entra em cena nas noites de inverno.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj
2026-07-19 11:02:32 -03:00
vini
54666b0fc2 fix: Via Láctea ~3.5x mais brilhante — era imperceptível sob scanlines
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj
2026-07-19 10:55:07 -03:00
vini
1c1a854875 docs: CLAUDE.md — céu realista e tools/build_catalog.py
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj
2026-07-19 10:52:06 -03:00
vini
6415112f9a feat: latitude/longitude configuráveis para o céu realista
Padrão Toledo-PR (-24.72, -53.74); campos aceitam vírgula ou ponto.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj
2026-07-19 10:52:05 -03:00
vini
05e465e33f feat: céu realista em tempo real — estrelas do BSC + Via Láctea
Substitui os 220 pontos aleatórios por projeção alt-az real: tempo
sideral local (GMST validado contra Meeus 12b), estrelas do catálogo
com cor/brilho reais e Via Láctea procedural gerada em coordenadas
galácticas (matriz Hipparcos J2000) — a banda gira rígida com o céu,
com Grande Fenda e núcleo em Sagitário. Vista para o sul (Cruzeiro,
α Cen e Carina quase sempre em cena). Camada estática pré-renderizada
offscreen; só estrelas de mag<=4 cintilam por frame; reprojeção a cada
30s. Extinção atmosférica no horizonte e clarão ao redor do sol
cenográfico. Debug: window.__skyTimeOffset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj
2026-07-19 10:52:05 -03:00
vini
998de4a945 feat: catálogo de estrelas embutido (Yale BSC, V<=5.5, 2887 estrelas)
tools/build_catalog.py baixa o bsc5-short.json, converte RA/Dec/V/K e
emite starcatalog.js compacto (65KB) com paleta de cores por
temperatura (corpo negro dessaturado).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj
2026-07-19 10:52:05 -03:00
7 changed files with 3316 additions and 24 deletions

View file

@ -33,6 +33,9 @@ plugin/
contents/ui/
main.qml # Plasma WallpaperItem — hosts everything
dancer.html # Self-contained animation (HTML + CSS + JS)
starcatalog.js # Generated star data (Yale BSC) — see tools/build_catalog.py
tools/
build_catalog.py # Regenerates starcatalog.js from the Yale Bright Star Catalog
```
### main.qml — the Plasma host
@ -47,7 +50,7 @@ Pure HTML/CSS/JS, no external dependencies. All visuals are self-contained:
| Layer | Technique |
|---|---|
| Starfield | `<canvas>` + `requestAnimationFrame` |
| Realistic sky (stars + Milky Way) | `<canvas>`; real-time alt-az projection of `starcatalog.js` (2,887 Yale BSC stars) + seeded procedural Milky Way for the configured lat/long (default Toledo-PR). Static layer pre-rendered offscreen, bright stars twinkle per frame; reprojects every 30 s. Debug: `window.__skyTimeOffset` (ms) fast-forwards the sky |
| Nebula blobs, grid floor, horizon | CSS animations |
| DANCEBOT-9000 robot | Pure CSS (divs + `@keyframes`) |
| Chest EQ bars & VU meter | JS-driven `requestAnimationFrame` |

View file

@ -14,5 +14,11 @@
<entry name="ChuvaMode" type="Bool">
<default>false</default>
</entry>
<entry name="Latitude" type="Double">
<default>-24.72</default>
</entry>
<entry name="Longitude" type="Double">
<default>-53.74</default>
</entry>
</group>
</kcfg>

View file

@ -9,6 +9,8 @@ ColumnLayout {
property alias cfg_ShowSkeleton: skeletonCheck.checked
property alias cfg_BaladaMode: baladaCheck.checked
property alias cfg_ChuvaMode: chuvaCheck.checked
property double cfg_Latitude
property double cfg_Longitude
spacing: Kirigami.Units.largeSpacing
@ -34,5 +36,28 @@ ColumnLayout {
text: i18n("Lua + chuva + relâmpagos")
onCheckedChanged: if (checked) baladaCheck.checked = false
}
Kirigami.Separator { Kirigami.FormData.isSection: true }
// Observador do céu realista (aceita vírgula ou ponto decimal)
TextField {
id: latField
Kirigami.FormData.label: i18n("Latitude:")
Component.onCompleted: text = root.cfg_Latitude
onTextChanged: {
const v = parseFloat(text.replace(",", "."))
if (!isNaN(v) && v >= -90 && v <= 90) root.cfg_Latitude = v
}
}
TextField {
id: lonField
Kirigami.FormData.label: i18n("Longitude:")
Component.onCompleted: text = root.cfg_Longitude
onTextChanged: {
const v = parseFloat(text.replace(",", "."))
if (!isNaN(v) && v >= -180 && v <= 180) root.cfg_Longitude = v
}
}
}
}

View file

@ -164,7 +164,7 @@ body.balada .nebula::after { opacity: 1; }
.album-art {
position: absolute;
top: 4%;
right: 2%;
left: 2%;
width: 330px;
height: 330px;
border-radius: 6px;
@ -254,7 +254,7 @@ body.balada .music-ticker {
============================================================ */
.lyric-display {
position: absolute;
left: 2%;
right: 2%;
top: 4%;
width: 38%;
height: 150px;
@ -273,6 +273,7 @@ body.balada .music-ticker {
.lyric-ctx {
display: block;
width: 100%;
text-align: right; /* letra ancorada à direita da tela */
font-family: 'Courier New', monospace;
letter-spacing: 0.02em;
white-space: normal;
@ -1198,6 +1199,7 @@ body.chuva #sunChuvaFace { opacity: 1; transition: opacity 5s ease; }
</div><!-- .scene -->
<script src="starcatalog.js"></script>
<script>
/* ============================================================
CONSTANTS
@ -1207,47 +1209,287 @@ const BPM = 120;
const BEAT_MS = 60000 / BPM; // 500ms
/* ============================================================
STARFIELD
CÉU REALISTA — Yale BSC (starcatalog.js) + Via Láctea
Posições reais para o observador via tempo sideral local.
O sol vaporwave é cenográfico — só a camada de estrelas é real.
============================================================ */
const canvas = document.getElementById('starfield');
const ctx = canvas.getContext('2d');
const STAR_COUNT = 220;
const stars = [];
let obsLat = -24.72, obsLon = -53.74; // Toledo-PR; sobrescrito pela config
const VIEW_AZ = 90; // centro da tela olha para o LESTE — a tela física do
// usuário está voltada p/ leste, então o wallpaper age
// como janela: estrelas nascem subindo dentro da cena
const FOV_AZ = 140; // largura da tela em graus de azimute
const ALT_TOP = 75; // altitude no topo da tela
const HORIZON_F = 0.585; // fração da altura da tela até o horizonte
const TWINKLE_MAG = 4.0; // até esta magnitude a estrela cintila; acima é estática
const DEG = Math.PI / 180;
window.__skyTimeOffset = 0; // debug: ms somados ao relógio p/ testar a rotação
const STAR_COLORS = [[255,221,170],[204,216,255],[238,238,255]];
function initStars() {
stars.length = 0;
for (let i = 0; i < STAR_COUNT; i++) {
const c = STAR_COLORS[Math.random() < 0.15 ? 0 : Math.random() < 0.5 ? 1 : 2];
stars.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height * 0.62,
r: Math.random() * 1.6 + 0.2,
phase: Math.random() * Math.PI * 2,
freq: 0.4 + Math.random() * 1.2,
cr: c[0], cg: c[1], cb: c[2],
});
/* Tempo sideral local em graus (GMST + longitude leste) */
function localSiderealDeg(ms) {
const d = ms / 86400000 - 10957.5; // dias desde J2000.0
return ((280.46061837 + 360.98564736629 * d + obsLon) % 360 + 360) % 360;
}
/* RA/Dec (graus) -> altitude/azimute (graus, az a partir do norte p/ leste) */
function altAz(raDeg, decDeg, lstDeg) {
const H = (lstDeg - raDeg) * DEG;
const cd = Math.cos(decDeg * DEG), sd = Math.sin(decDeg * DEG);
const cl = Math.cos(obsLat * DEG), sl = Math.sin(obsLat * DEG);
const xh = -cd * Math.cos(H) * sl + sd * cl; // norte
const yh = -cd * Math.sin(H); // leste
const zh = cd * Math.cos(H) * cl + sd * sl; // zênite
return { alt: Math.asin(zh) / DEG, az: Math.atan2(yh, xh) / DEG };
}
/* Projeção cilíndrica: azimute -> x, altitude -> y. null = fora da tela */
function skyToScreen(alt, az, W, horizonPx) {
const daz = ((az - VIEW_AZ) % 360 + 540) % 360 - 180;
if (Math.abs(daz) > FOV_AZ / 2 || alt < -3 || alt > ALT_TOP) return null;
return { x: (0.5 + daz / FOV_AZ) * W, y: horizonPx * (1 - alt / ALT_TOP) };
}
/* Extinção atmosférica: estrelas somem suavemente perto do horizonte */
function extinction(alt) {
return Math.max(0, Math.min(1, (alt + 1) / 14));
}
/* Clarão ao redor do sol retrowave (só no modo padrão — nos outros o sol
sobe/encolhe e o próprio elemento cobre o que está atrás) */
function glareFactor(x, y, W, H, on) {
if (!on) return 1;
const d = Math.hypot(x - W / 2, y - H * HORIZON_F);
return Math.max(0, Math.min(1, (d - 150) / 240));
}
/* ── Via Láctea: partículas geradas em coords galácticas (RNG com semente,
estável entre reloads) e convertidas p/ equatorial J2000 uma única vez ── */
function mulberry32(seed) {
return function () {
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
/* Transposta da matriz equatorial->galáctica (Hipparcos, J2000) */
const G2E = [
[-0.0548755604, 0.4941094279, -0.8676661490],
[-0.8734370902, -0.4448296300, -0.1980763734],
[-0.4838350155, 0.7469822445, 0.4559837762],
];
/* Visual de astrofoto de longa exposição: grão estelar fino + brilho difuso
com gradiente térmico (núcleo âmbar -> anticentro azul) + faixas de poeira */
function galToEq(l, b) {
const cb = Math.cos(b * DEG);
const gx = cb * Math.cos(l * DEG), gy = cb * Math.sin(l * DEG), gz = Math.sin(b * DEG);
return {
ra: (Math.atan2(
G2E[1][0] * gx + G2E[1][1] * gy + G2E[1][2] * gz,
G2E[0][0] * gx + G2E[0][1] * gy + G2E[0][2] * gz) / DEG + 360) % 360,
dec: Math.asin(G2E[2][0] * gx + G2E[2][1] * gy + G2E[2][2] * gz) / DEG,
};
}
/* Cor por distância angular ao centro galáctico: creme-âmbar -> neutro -> azul */
function mwColor(dl) {
const t = Math.min(1, dl / 140);
const mix = (a, b2, c) => Math.round(t < 0.5 ? a + (b2 - a) * t * 2 : b2 + (c - b2) * (t - 0.5) * 2);
return [mix(255, 236, 186), mix(226, 224, 205), mix(188, 214, 242)];
}
/* Faixas de poeira (Grande Fenda + secundária): 1 = livre, ~0.1 = dentro da poeira */
function mwDust(l, b, dl) {
if (dl > 100) return 1;
const fade = dl > 70 ? (100 - dl) / 30 : 1; // fenda some longe do centro
const lane1 = Math.abs(b - 1.6 * Math.sin(l * DEG * 2.2)) <
(2.2 + 1.2 * Math.sin(l * 0.7)) * fade;
const lane2 = Math.abs(b + 2.6 - 1.2 * Math.sin(l * DEG * 1.4 + 1.7)) <
(1.3 + 0.8 * Math.sin(l * 1.3 + 0.5)) * fade;
return (lane1 || lane2) ? 0.1 : 1;
}
/* Nuvens estelares (Scutum, Sagitário, Carina...) — adensamentos fixos na banda */
const MW_CLOUDS = [];
const mwGlow = []; // blobs difusos {ra,dec,size,alpha,ci}
const mwGrain = []; // grão estelar fino {ra,dec,alpha,rgb}
(function buildMilkyWay() {
const rand = mulberry32(0x5EEDED);
const gauss = () => {
const u = Math.max(rand(), 1e-9), v = rand();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
};
for (let i = 0; i < 14; i++) {
const l = (rand() * 160 - 80 + 360) % 360; // nuvens no lado do centro
MW_CLOUDS.push({ l, b: gauss() * 2.5, r: 2 + rand() * 4 });
}
const cloudBoost = (l, b) => {
let boost = 0;
for (const c of MW_CLOUDS) {
const dlc = Math.min(Math.abs(l - c.l), 360 - Math.abs(l - c.l));
const d2 = (dlc / c.r) ** 2 + ((b - c.b) / c.r) ** 2;
if (d2 < 1) boost = Math.max(boost, 1 - d2);
}
return boost;
};
let guard = 0;
while (mwGlow.length < 3600 && guard++ < 300000) {
const l = rand() * 360;
const dl = Math.min(l, 360 - l);
const central = Math.exp(-((dl / 75) ** 2));
if (rand() > 0.28 + 0.72 * central) continue;
/* bojo central: banda mais gorda perto do núcleo */
const b = gauss() * (5 + 3 * central + 4 * Math.exp(-((dl / 22) ** 2)));
const dust = mwDust(l, b, dl);
const boost = cloudBoost(l, b);
const alpha = (0.35 + 0.65 * central) * dust * (1 + 1.2 * boost) *
(0.028 + rand() * 0.05);
if (alpha < 0.005) continue;
const eq = galToEq(l, b);
mwGlow.push({ ra: eq.ra, dec: eq.dec, size: 14 + rand() * 30,
alpha, ci: mwColor(dl) });
}
guard = 0;
while (mwGrain.length < 9500 && guard++ < 500000) {
const l = rand() * 360;
const dl = Math.min(l, 360 - l);
const central = Math.exp(-((dl / 80) ** 2));
if (rand() > 0.22 + 0.78 * central) continue;
const b = gauss() * (4.5 + 2.5 * central + 3.5 * Math.exp(-((dl / 22) ** 2)));
const dust = mwDust(l, b, dl);
if (rand() > dust + 0.06) continue; // poeira esconde o grão
const boost = cloudBoost(l, b);
if (boost < 0.3 && rand() < 0.25 * central) continue;
const eq = galToEq(l, b);
mwGrain.push({ ra: eq.ra, dec: eq.dec,
alpha: 0.10 + rand() * 0.30 + 0.2 * boost,
ci: mwColor(dl) });
}
})();
/* Sprites do brilho difuso: quente / neutro / frio (escolhido por cor) */
function makeMwSprite(r, g, b) {
const c = document.createElement('canvas');
c.width = c.height = 64;
const s = c.getContext('2d');
const grad = s.createRadialGradient(32, 32, 0, 32, 32, 32);
grad.addColorStop(0, `rgba(${r},${g},${b},1)`);
grad.addColorStop(0.5, `rgba(${r},${g},${b},0.4)`);
grad.addColorStop(1, `rgba(${r},${g},${b},0)`);
s.fillStyle = grad;
s.fillRect(0, 0, 64, 64);
return c;
}
const mwSprites = [makeMwSprite(255, 226, 188), // quente (núcleo)
makeMwSprite(236, 224, 214), // neutro
makeMwSprite(188, 214, 242)]; // frio (anticentro)
function spriteFor(ci) { return mwSprites[ci[2] > ci[0] ? 2 : (ci[0] > 240 ? 0 : 1)]; }
/* ── Passo de projeção: recalcula posições de tela (a cada 30s o céu gira
~0.125°, imperceptível). Camada estática (Via Láctea + estrelas fracas)
é pré-renderizada em baseCanvas; só as brilhantes cintilam por frame ── */
const baseCanvas = document.createElement('canvas');
let twinkList = [];
let lastProjMs = -Infinity;
function projectSky() {
const W = canvas.width, H = canvas.height;
if (!W || !H) return;
baseCanvas.width = W;
baseCanvas.height = H;
const bctx = baseCanvas.getContext('2d');
const horizonPx = H * HORIZON_F;
const lst = localSiderealDeg(Date.now() + window.__skyTimeOffset);
const glareOn = !document.body.classList.contains('balada') &&
!document.body.classList.contains('chuva');
twinkList = [];
bctx.globalCompositeOperation = 'lighter';
for (const p of mwGlow) {
const aa = altAz(p.ra, p.dec, lst);
const sc = skyToScreen(aa.alt, aa.az, W, horizonPx);
if (!sc) continue;
const a = p.alpha * extinction(aa.alt) * glareFactor(sc.x, sc.y, W, H, glareOn);
if (a < 0.002) continue;
bctx.globalAlpha = a;
bctx.drawImage(spriteFor(p.ci), sc.x - p.size / 2, sc.y - p.size / 2, p.size, p.size);
}
bctx.globalAlpha = 1;
for (const p of mwGrain) {
const aa = altAz(p.ra, p.dec, lst);
const sc = skyToScreen(aa.alt, aa.az, W, horizonPx);
if (!sc) continue;
const a = p.alpha * extinction(aa.alt) * glareFactor(sc.x, sc.y, W, H, glareOn);
if (a < 0.01) continue;
bctx.fillStyle = `rgba(${p.ci[0]},${p.ci[1]},${p.ci[2]},${a.toFixed(3)})`;
bctx.fillRect(sc.x - 0.5, sc.y - 0.5, 1, 1);
}
bctx.globalCompositeOperation = 'source-over';
for (let i = 0; i < STAR_DATA.length; i++) {
const [ra, dec, mag, ci] = STAR_DATA[i];
const aa = altAz(ra, dec, lst);
const sc = skyToScreen(aa.alt, aa.az, W, horizonPx);
if (!sc) continue;
const a = Math.max(0.25, Math.min(1, 1.15 - 0.15 * mag)) *
extinction(aa.alt) * glareFactor(sc.x, sc.y, W, H, glareOn);
if (a < 0.01) continue;
const r = Math.max(0.35, 2.7 - 0.42 * mag);
const c = STAR_PALETTE[ci];
if (mag <= TWINKLE_MAG) {
/* fase/freq determinísticas por índice — cintilação estável */
twinkList.push({
x: sc.x, y: sc.y, r, a,
cr: c[0], cg: c[1], cb: c[2],
phase: i * 2.3999,
freq: 0.35 + ((i * 0.61803) % 1) * 1.1,
amp: mag < 1.5 ? 0.18 : 0.45,
});
} else if (r > 0.8) {
bctx.beginPath();
bctx.arc(sc.x, sc.y, r, 0, Math.PI * 2);
bctx.fillStyle = `rgba(${c[0]},${c[1]},${c[2]},${a.toFixed(3)})`;
bctx.fill();
} else {
bctx.fillStyle = `rgba(${c[0]},${c[1]},${c[2]},${a.toFixed(3)})`;
bctx.fillRect(sc.x - r, sc.y - r, r * 2, r * 2);
}
}
}
function setObserverLocation(lat, lon) {
lat = parseFloat(lat); lon = parseFloat(lon);
if (!isFinite(lat) || !isFinite(lon)) return;
if (lat === obsLat && lon === obsLon) return;
obsLat = lat;
obsLon = lon;
projectSky();
}
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
initStars();
projectSky();
}
function drawStars(ts) {
if (ts - lastProjMs > 30000) { lastProjMs = ts; projectSky(); }
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(baseCanvas, 0, 0);
const t = ts * 0.001;
stars.forEach(s => {
const alpha = (0.25 + 0.75 * (0.5 + 0.5 * Math.sin(t * s.freq + s.phase))).toFixed(2);
for (const s of twinkList) {
const tw = 1 - s.amp * (0.5 + 0.5 * Math.sin(t * s.freq + s.phase));
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${s.cr},${s.cg},${s.cb},${alpha})`;
ctx.fillStyle = `rgba(${s.cr},${s.cg},${s.cb},${(s.a * tw).toFixed(3)})`;
ctx.fill();
});
}
requestAnimationFrame(drawStars);
}
@ -1984,6 +2226,7 @@ function setBaladaMode(on) {
if (on) { setChuvaMode(false); startGlobeLoop(); }
document.body.classList.toggle('balada', balada);
/* opacidade do globo é controlada por drawGlobe via baladaAmt */
projectSky(); // rebake do clarão solar no céu
}
function toggleBalada() { setBaladaMode(!balada); }
@ -2137,6 +2380,7 @@ function setChuvaMode(on) {
} else if (lastThemeRgb) {
applyTheme(lastThemeRgb); // devolve a cor do álbum ao fog/horizonte
}
projectSky(); // rebake do clarão solar no céu
}
function toggleChuva() { setChuvaMode(!chuva); }

View file

@ -11,6 +11,8 @@ WallpaperItem {
webView.runJavaScript("setShowSkeleton(" + Plasmoid.configuration.ShowSkeleton + ")")
webView.runJavaScript("setBaladaMode(" + Plasmoid.configuration.BaladaMode + ")")
webView.runJavaScript("setChuvaMode(" + Plasmoid.configuration.ChuvaMode + ")")
webView.runJavaScript("setObserverLocation(" + Plasmoid.configuration.Latitude + ","
+ Plasmoid.configuration.Longitude + ")")
}
Connections {
@ -18,6 +20,8 @@ WallpaperItem {
function onShowSkeletonChanged() { applyConfig() }
function onBaladaModeChanged() { applyConfig() }
function onChuvaModeChanged() { applyConfig() }
function onLatitudeChanged() { applyConfig() }
function onLongitudeChanged() { applyConfig() }
}
WebEngineView {

File diff suppressed because it is too large Load diff

117
tools/build_catalog.py Normal file
View file

@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Gera plugin/contents/ui/starcatalog.js a partir do Yale Bright Star Catalog.
Baixa bsc5-short.json (RA/Dec/V/K), filtra V <= MAG_LIMIT, converte a
temperatura de cor em RGB (aprox. corpo negro, quantizada numa paleta pequena)
e emite um JS compacto:
const STAR_PALETTE = [[r,g,b], ...];
const STAR_DATA = [[raDeg, decDeg, vmag, colorIdx], ...]; // ordenado por mag
Uso: python3 tools/build_catalog.py
"""
import json
import math
import re
import urllib.request
from pathlib import Path
URL = "https://raw.githubusercontent.com/brettonw/YaleBrightStarCatalog/master/bsc5-short.json"
OUT = Path(__file__).resolve().parent.parent / "plugin/contents/ui/starcatalog.js"
MAG_LIMIT = 5.5
# Limites (K) e centro representativo de cada faixa de temperatura
TEMP_BINS = [
(0, 3500, 3000), # M — alaranjado-avermelhado
(3500, 4500, 4000), # K — laranja
(4500, 5500, 5000), # G — amarelo-branco
(5500, 6500, 6000), # F — branco-amarelado
(6500, 8000, 7200), # A — branco
(8000, 11000, 9500), # B tardio — branco-azulado
(11000, 16000, 13000), # B — azul-branco
(16000, 10**9, 20000), # O/B quente — azul
]
SATURATION = 0.6 # estrelas reais parecem quase brancas; 1.0 = cor plena
def blackbody_rgb(kelvin: float) -> tuple[int, int, int]:
"""Aproximação de Tanner Helland (válida ~1000K-40000K)."""
t = kelvin / 100.0
if t <= 66:
r = 255.0
g = 99.4708025861 * math.log(t) - 161.1195681661
b = 0.0 if t <= 19 else 138.5177312231 * math.log(t - 10) - 305.0447927307
else:
r = 329.698727446 * (t - 60) ** -0.1332047592
g = 288.1221695283 * (t - 60) ** -0.0755148492
b = 255.0
def clamp(x):
return max(0, min(255, round(x)))
return clamp(r), clamp(g), clamp(b)
def desaturate(rgb, amount):
r, g, b = rgb
lum = 0.2126 * r + 0.7152 * g + 0.0722 * b
mix = lambda c: round(lum + (c - lum) * amount)
return mix(r), mix(g), mix(b)
def parse_ra(s: str) -> float:
m = re.match(r"(\d+)h\s*(\d+)m\s*([\d.]+)s", s)
h, mi, se = float(m.group(1)), float(m.group(2)), float(m.group(3))
return (h + mi / 60 + se / 3600) * 15.0
def parse_dec(s: str) -> float:
m = re.match(r"([+-])(\d+)°\s*(\d+)\s*([\d.]+)″", s)
sign = -1.0 if m.group(1) == "-" else 1.0
d, mi, se = float(m.group(2)), float(m.group(3)), float(m.group(4))
return sign * (d + mi / 60 + se / 3600)
def temp_index(kelvin: float) -> int:
for i, (lo, hi, _) in enumerate(TEMP_BINS):
if lo <= kelvin < hi:
return i
return len(TEMP_BINS) - 1
def main():
raw = json.loads(urllib.request.urlopen(URL, timeout=30).read())
stars = []
for entry in raw:
if "V" not in entry or "RA" not in entry or "Dec" not in entry:
continue
vmag = float(entry["V"])
if vmag > MAG_LIMIT:
continue
kelvin = float(entry.get("K", 6000))
stars.append((
round(parse_ra(entry["RA"]), 2),
round(parse_dec(entry["Dec"]), 2),
round(vmag, 2),
temp_index(kelvin),
))
stars.sort(key=lambda s: s[2]) # mais brilhantes primeiro
palette = [desaturate(blackbody_rgb(center), SATURATION)
for _, _, center in TEMP_BINS]
rows = ",\n".join(
f"[{ra},{dec},{mag},{ci}]" for ra, dec, mag, ci in stars
)
OUT.write_text(
"/* Gerado por tools/build_catalog.py — NÃO editar à mão.\n"
f" Yale Bright Star Catalog, V <= {MAG_LIMIT}, {len(stars)} estrelas.\n"
" Formato: [raGraus, decGraus, magV, idxPaleta] ordenado por magnitude. */\n"
f"const STAR_PALETTE = {json.dumps([list(p) for p in palette])};\n"
f"const STAR_DATA = [\n{rows}\n];\n"
)
print(f"{len(stars)} estrelas -> {OUT} ({OUT.stat().st_size / 1024:.0f} KB)")
if __name__ == "__main__":
main()