feat(skins): skin Steam Big Picture / SteamOS et vidéo de démarrage
Some checks are pending
CI / windows (push) Waiting to run

Thème CSS Loader Orbe (couleurs, police Archivo, fond orbe) avec noms de
classes lisibles traduits par CSS Loader, outil de vérification contre
l'interface Steam actuelle, rendu de l'orbe et de la vidéo.
This commit is contained in:
yasder5 2026-10-18 16:41:36 +02:00
parent 088d550856
commit 9a7fca6b3c
13 changed files with 348 additions and 0 deletions

View file

@ -0,0 +1,60 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Orbe Steam art</title>
<style>
/* Rendu image par image (pilote : tools/render.py). ?mode=bg : fond transparent 1920×1080 ; ?mode=boot : vidéo de démarrage 1280×800. */
@font-face { font-family: Archivo; src: url(../Orbe/fonts/Archivo.woff2) format('woff2'); font-weight: 100 900; }
html, body { margin: 0; background: transparent; overflow: hidden; }
body.boot { background: #0a0a0a; }
canvas { position: absolute; inset: 0; }
.mark { position: absolute; left: 0; right: 0; top: 50%; text-align: center; font: 900 120px/1 Archivo; letter-spacing: -.05em; color: #f5f5f0; opacity: 0; }
.mark span { color: #d3fd50; }
</style>
</head>
<body>
<canvas id="cv"></canvas>
<div class="mark" id="mark">orbe<span>.</span></div>
<script>
const mode = new URLSearchParams(location.search).get('mode') || 'bg'
const W = mode === 'boot' ? 1280 : 1920, H = mode === 'boot' ? 800 : 1080
const cv = document.getElementById('cv'), ctx = cv.getContext('2d'), mark = document.getElementById('mark')
cv.width = W; cv.height = H; cv.style.width = W + 'px'; cv.style.height = H + 'px'
if (mode === 'boot') document.body.className = 'boot'
/* Même sphère de Fibonacci que le launcher et le site ; départ dispersé (graine fixe) pour l'arrivée des particules */
const N = 620, pts = []
let s = 7; const rnd = () => (s = (s * 16807) % 2147483647) / 2147483647
for (let i = 0; i < N; i++) {
const y = 1 - (i / (N - 1)) * 2, r = Math.sqrt(1 - y * y), th = i * 2.39996323
const a = rnd() * 6.283, d = 1.6 + rnd() * 2.2
pts.push({ x: Math.cos(th) * r, y, z: Math.sin(th) * r, hot: i % 9 === 0, sx: Math.cos(a) * d, sy: Math.sin(a) * d, lag: rnd() * .35 })
}
const ease = t => 1 - Math.pow(1 - Math.min(1, Math.max(0, t)), 3)
/* t en secondes */
window.render = t => {
const boot = mode === 'boot'
const R = boot ? 250 : 430, cx = boot ? W / 2 : W * .74, cy = boot ? H / 2 - 40 : H * .5
const a = .6 + t * .35, b = Math.sin(t * .4) * .3, ca = Math.cos(a), sa = Math.sin(a), cb = Math.cos(b), sb = Math.sin(b)
/* vidéo : arrivée 0 → 1,6 s, l'orbe s'efface derrière le mot (2,2 → 3 s), fondu au noir à la fin (3,4 → 4 s) */
const fade = boot ? 1 - ease((t - 3.4) / .6) : 1, dim = boot ? 1 - .7 * ease((t - 2.2) / .8) : 1
ctx.clearRect(0, 0, W, H)
for (const p of pts) {
const k = boot ? ease((t - p.lag) / 1.25) : 1
const x1 = p.x * ca - p.z * sa, z1 = p.x * sa + p.z * ca, y1 = p.y * cb - z1 * sb, z2 = p.y * sb + z1 * cb, d = (z2 + 1.4) / 2.4
const px = p.sx + (x1 - p.sx) * k, py = p.sy + (y1 - p.sy) * k
const al = (p.hot ? .2 + d * .7 : .06 + d * .42) * fade * dim * (boot ? Math.min(1, k * 1.5) : 1)
ctx.fillStyle = p.hot ? `rgba(211,253,80,${al})` : `rgba(243,243,238,${al})`
ctx.beginPath(); ctx.arc(cx + px * R, cy + py * R, (p.hot ? 2.4 : 1.5) * (boot ? 1.4 : 2) * (.6 + d * .7), 0, 6.283); ctx.fill()
}
if (boot) {
const m = ease((t - 1.7) / .7) * fade
mark.style.opacity = m; mark.style.transform = `translateY(${-50 + (1 - m) * 12}%)`
}
}
render(0)
</script>
</body>
</html>

View file

@ -0,0 +1,16 @@
#!/usr/bin/env python3
"""Vérifie que chaque classe lisible du skin (module_Nom) est connue de CSS Loader et existe dans l'interface Steam actuelle.
Usage : check.py <map.json de api.deckthemes.com/stable.json> <sp.css du paquet steamui_websrc_all>"""
import json, re, sys
from pathlib import Path
mapping, sp = json.load(open(sys.argv[1])), open(sys.argv[2]).read()
alias = {a: v[-1] for v in mapping.values() for a in v}
bad = 0
for f in sorted((Path(__file__).parent.parent / "Orbe").glob("*.css")):
for name in sorted(set(re.findall(r"\.([a-z]+_[A-Za-z]+)\b", f.read_text()))):
cur = alias.get(name)
ok = cur is not None and re.search(r"\." + re.escape(cur) + r"(?![\w-])", sp)
bad += not ok
print("%-4s %-12s %-45s %s" % ("ok" if ok else "ABS", f.name, name, cur or "-"))
sys.exit(1 if bad else 0)

View file

@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Rend tools/art.html dans un Chromium headless (protocole CDP) : fond orbe (PNG transparent) et vidéo de démarrage (images PNG).
Usage : render.py <url de art.html> <cdp ws http://hôte:port> <dossier de sortie>"""
import base64, json, os, sys, time, urllib.request
import websocket
url, cdp, out = sys.argv[1:4]
os.makedirs(out + "/frames", exist_ok=True)
t = json.load(urllib.request.urlopen(urllib.request.Request(cdp + "/json/new?about:blank", method="PUT")))
ws, n = websocket.create_connection(t["webSocketDebuggerUrl"], timeout=60), 0
def cmd(method, **params):
global n
n += 1
ws.send(json.dumps({"id": n, "method": method, "params": params}))
while True:
m = json.loads(ws.recv())
if m.get("id") == n:
return m.get("result", {})
def shot(path, w, h):
png = cmd("Page.captureScreenshot", format="png", clip={"x": 0, "y": 0, "width": w, "height": h, "scale": 1})["data"]
open(path, "wb").write(base64.b64decode(png))
def load(mode, w, h):
cmd("Emulation.setDeviceMetricsOverride", width=w, height=h, deviceScaleFactor=1, mobile=False)
cmd("Emulation.setDefaultBackgroundColorOverride", color={"r": 0, "g": 0, "b": 0, "a": 0})
cmd("Page.navigate", url=url + "?mode=" + mode)
time.sleep(1.5)
cmd("Runtime.evaluate", expression="document.fonts.ready", awaitPromise=True)
cmd("Page.enable")
load("bg", 1920, 1080)
cmd("Runtime.evaluate", expression="render(2)")
shot(out + "/orb.png", 1920, 1080)
load("boot", 1280, 800)
for i in range(120): # 4 s à 30 i/s
cmd("Runtime.evaluate", expression="render(%f)" % (i / 30))
shot("%s/frames/%03d.png" % (out, i), 1280, 800)
cmd("Page.close")