Compare commits
7 commits
0caf1fbbf8
...
2f4994918d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f4994918d | ||
|
|
0474a9ab83 | ||
|
|
54666b0fc2 | ||
|
|
1c1a854875 | ||
|
|
6415112f9a | ||
|
|
05e465e33f | ||
|
|
998de4a945 |
7 changed files with 3316 additions and 24 deletions
|
|
@ -33,6 +33,9 @@ plugin/
|
||||||
contents/ui/
|
contents/ui/
|
||||||
main.qml # Plasma WallpaperItem — hosts everything
|
main.qml # Plasma WallpaperItem — hosts everything
|
||||||
dancer.html # Self-contained animation (HTML + CSS + JS)
|
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
|
### main.qml — the Plasma host
|
||||||
|
|
@ -47,7 +50,7 @@ Pure HTML/CSS/JS, no external dependencies. All visuals are self-contained:
|
||||||
|
|
||||||
| Layer | Technique |
|
| 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 |
|
| Nebula blobs, grid floor, horizon | CSS animations |
|
||||||
| DANCEBOT-9000 robot | Pure CSS (divs + `@keyframes`) |
|
| DANCEBOT-9000 robot | Pure CSS (divs + `@keyframes`) |
|
||||||
| Chest EQ bars & VU meter | JS-driven `requestAnimationFrame` |
|
| Chest EQ bars & VU meter | JS-driven `requestAnimationFrame` |
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,11 @@
|
||||||
<entry name="ChuvaMode" type="Bool">
|
<entry name="ChuvaMode" type="Bool">
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
</entry>
|
</entry>
|
||||||
|
<entry name="Latitude" type="Double">
|
||||||
|
<default>-24.72</default>
|
||||||
|
</entry>
|
||||||
|
<entry name="Longitude" type="Double">
|
||||||
|
<default>-53.74</default>
|
||||||
|
</entry>
|
||||||
</group>
|
</group>
|
||||||
</kcfg>
|
</kcfg>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ ColumnLayout {
|
||||||
property alias cfg_ShowSkeleton: skeletonCheck.checked
|
property alias cfg_ShowSkeleton: skeletonCheck.checked
|
||||||
property alias cfg_BaladaMode: baladaCheck.checked
|
property alias cfg_BaladaMode: baladaCheck.checked
|
||||||
property alias cfg_ChuvaMode: chuvaCheck.checked
|
property alias cfg_ChuvaMode: chuvaCheck.checked
|
||||||
|
property double cfg_Latitude
|
||||||
|
property double cfg_Longitude
|
||||||
|
|
||||||
spacing: Kirigami.Units.largeSpacing
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
|
@ -34,5 +36,28 @@ ColumnLayout {
|
||||||
text: i18n("Lua + chuva + relâmpagos")
|
text: i18n("Lua + chuva + relâmpagos")
|
||||||
onCheckedChanged: if (checked) baladaCheck.checked = false
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ body.balada .nebula::after { opacity: 1; }
|
||||||
.album-art {
|
.album-art {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4%;
|
top: 4%;
|
||||||
right: 2%;
|
left: 2%;
|
||||||
width: 330px;
|
width: 330px;
|
||||||
height: 330px;
|
height: 330px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
@ -254,7 +254,7 @@ body.balada .music-ticker {
|
||||||
============================================================ */
|
============================================================ */
|
||||||
.lyric-display {
|
.lyric-display {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 2%;
|
right: 2%;
|
||||||
top: 4%;
|
top: 4%;
|
||||||
width: 38%;
|
width: 38%;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
|
|
@ -273,6 +273,7 @@ body.balada .music-ticker {
|
||||||
.lyric-ctx {
|
.lyric-ctx {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
text-align: right; /* letra ancorada à direita da tela */
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
|
@ -1198,6 +1199,7 @@ body.chuva #sunChuvaFace { opacity: 1; transition: opacity 5s ease; }
|
||||||
|
|
||||||
</div><!-- .scene -->
|
</div><!-- .scene -->
|
||||||
|
|
||||||
|
<script src="starcatalog.js"></script>
|
||||||
<script>
|
<script>
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
CONSTANTS
|
CONSTANTS
|
||||||
|
|
@ -1207,47 +1209,287 @@ const BPM = 120;
|
||||||
const BEAT_MS = 60000 / BPM; // 500ms
|
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 canvas = document.getElementById('starfield');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
const STAR_COUNT = 220;
|
let obsLat = -24.72, obsLon = -53.74; // Toledo-PR; sobrescrito pela config
|
||||||
const stars = [];
|
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]];
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
function initStars() {
|
/* RA/Dec (graus) -> altitude/azimute (graus, az a partir do norte p/ leste) */
|
||||||
stars.length = 0;
|
function altAz(raDeg, decDeg, lstDeg) {
|
||||||
for (let i = 0; i < STAR_COUNT; i++) {
|
const H = (lstDeg - raDeg) * DEG;
|
||||||
const c = STAR_COLORS[Math.random() < 0.15 ? 0 : Math.random() < 0.5 ? 1 : 2];
|
const cd = Math.cos(decDeg * DEG), sd = Math.sin(decDeg * DEG);
|
||||||
stars.push({
|
const cl = Math.cos(obsLat * DEG), sl = Math.sin(obsLat * DEG);
|
||||||
x: Math.random() * canvas.width,
|
const xh = -cd * Math.cos(H) * sl + sd * cl; // norte
|
||||||
y: Math.random() * canvas.height * 0.62,
|
const yh = -cd * Math.sin(H); // leste
|
||||||
r: Math.random() * 1.6 + 0.2,
|
const zh = cd * Math.cos(H) * cl + sd * sl; // zênite
|
||||||
phase: Math.random() * Math.PI * 2,
|
return { alt: Math.asin(zh) / DEG, az: Math.atan2(yh, xh) / DEG };
|
||||||
freq: 0.4 + Math.random() * 1.2,
|
}
|
||||||
cr: c[0], cg: c[1], cb: c[2],
|
|
||||||
});
|
/* 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() {
|
function resizeCanvas() {
|
||||||
canvas.width = window.innerWidth;
|
canvas.width = window.innerWidth;
|
||||||
canvas.height = window.innerHeight;
|
canvas.height = window.innerHeight;
|
||||||
initStars();
|
projectSky();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawStars(ts) {
|
function drawStars(ts) {
|
||||||
|
if (ts - lastProjMs > 30000) { lastProjMs = ts; projectSky(); }
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.drawImage(baseCanvas, 0, 0);
|
||||||
const t = ts * 0.001;
|
const t = ts * 0.001;
|
||||||
stars.forEach(s => {
|
for (const s of twinkList) {
|
||||||
const alpha = (0.25 + 0.75 * (0.5 + 0.5 * Math.sin(t * s.freq + s.phase))).toFixed(2);
|
const tw = 1 - s.amp * (0.5 + 0.5 * Math.sin(t * s.freq + s.phase));
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
|
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();
|
ctx.fill();
|
||||||
});
|
}
|
||||||
requestAnimationFrame(drawStars);
|
requestAnimationFrame(drawStars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1984,6 +2226,7 @@ function setBaladaMode(on) {
|
||||||
if (on) { setChuvaMode(false); startGlobeLoop(); }
|
if (on) { setChuvaMode(false); startGlobeLoop(); }
|
||||||
document.body.classList.toggle('balada', balada);
|
document.body.classList.toggle('balada', balada);
|
||||||
/* opacidade do globo é controlada por drawGlobe via baladaAmt */
|
/* opacidade do globo é controlada por drawGlobe via baladaAmt */
|
||||||
|
projectSky(); // rebake do clarão solar no céu
|
||||||
}
|
}
|
||||||
function toggleBalada() { setBaladaMode(!balada); }
|
function toggleBalada() { setBaladaMode(!balada); }
|
||||||
|
|
||||||
|
|
@ -2137,6 +2380,7 @@ function setChuvaMode(on) {
|
||||||
} else if (lastThemeRgb) {
|
} else if (lastThemeRgb) {
|
||||||
applyTheme(lastThemeRgb); // devolve a cor do álbum ao fog/horizonte
|
applyTheme(lastThemeRgb); // devolve a cor do álbum ao fog/horizonte
|
||||||
}
|
}
|
||||||
|
projectSky(); // rebake do clarão solar no céu
|
||||||
}
|
}
|
||||||
function toggleChuva() { setChuvaMode(!chuva); }
|
function toggleChuva() { setChuvaMode(!chuva); }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ WallpaperItem {
|
||||||
webView.runJavaScript("setShowSkeleton(" + Plasmoid.configuration.ShowSkeleton + ")")
|
webView.runJavaScript("setShowSkeleton(" + Plasmoid.configuration.ShowSkeleton + ")")
|
||||||
webView.runJavaScript("setBaladaMode(" + Plasmoid.configuration.BaladaMode + ")")
|
webView.runJavaScript("setBaladaMode(" + Plasmoid.configuration.BaladaMode + ")")
|
||||||
webView.runJavaScript("setChuvaMode(" + Plasmoid.configuration.ChuvaMode + ")")
|
webView.runJavaScript("setChuvaMode(" + Plasmoid.configuration.ChuvaMode + ")")
|
||||||
|
webView.runJavaScript("setObserverLocation(" + Plasmoid.configuration.Latitude + ","
|
||||||
|
+ Plasmoid.configuration.Longitude + ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
|
@ -18,6 +20,8 @@ WallpaperItem {
|
||||||
function onShowSkeletonChanged() { applyConfig() }
|
function onShowSkeletonChanged() { applyConfig() }
|
||||||
function onBaladaModeChanged() { applyConfig() }
|
function onBaladaModeChanged() { applyConfig() }
|
||||||
function onChuvaModeChanged() { applyConfig() }
|
function onChuvaModeChanged() { applyConfig() }
|
||||||
|
function onLatitudeChanged() { applyConfig() }
|
||||||
|
function onLongitudeChanged() { applyConfig() }
|
||||||
}
|
}
|
||||||
|
|
||||||
WebEngineView {
|
WebEngineView {
|
||||||
|
|
|
||||||
2893
plugin/contents/ui/starcatalog.js
Normal file
2893
plugin/contents/ui/starcatalog.js
Normal file
File diff suppressed because it is too large
Load diff
117
tools/build_catalog.py
Normal file
117
tools/build_catalog.py
Normal 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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue