Une interface (Classique, Slant, Cabinet…) est un thème : un dossier simple, lu sur disque comme les mods et les skins, qu'on supprime pour le retirer. Le joueur choisit son thème dans Skins → Interface (réglage uiTheme, l'identifiant du thème).
Pourquoi des données et pas du code
Les scripts n'ont jamais accès au DOM, au CSS ni au JavaScript de la WebView, et toute valeur qui entre dans la page est validée en Rust (AGENTS.md). Un thème est donc de la donnée : un manifeste, un fichier de jetons (un sous-ensemble strict de CSS) et des images. Le crate themes (crates/themes) valide tout contre un contrat de jetons typés, puis l'hôte publie à la page l'événement interfaces avec les seules valeurs qui ont passé. La page les pose comme variables CSS sous [data-direction="<id>"]. Le CSS structurel (plaques, boutons, onglets, liste, graphes) reste dans l'application et ne lit que ces jetons : un thème tiers restyle toute l'interface sans balisage ni script, et ne peut rien exécuter ni rien charger.
Où sont les thèmes
Ordre de recherche : themes/ à côté de prism.exe (en développement : le dossier themes/ du dépôt, trouvé en remontant jusqu'à Cargo.lock), puis <data>/themes (PRISM_THEMES_DIR change ce dossier). Le même identifiant deux fois : le premier gagne, le second est signalé. Chaque dossier s'appelle comme son identifiant (minuscules, chiffres, tirets, 32 caractères au plus).
themes/<id>/
theme.json le manifeste
tokens.css les jetons
preview.png (facultatif) aperçu ; sans lui, la carte du sélecteur est dessinée avec les jetons
images/… (facultatif) images citées par les jetons de type image
theme.json
| Champ | Rôle |
|---|---|
id |
doit être le nom du dossier |
name |
1 à 48 caractères |
description |
jusqu'à 240 caractères |
version |
lettres, chiffres, . _ + -, 32 au plus |
author |
jusqu'à 64 caractères |
order |
place dans le sélecteur (0 à 1000, 100 par défaut), puis l'identifiant |
preview |
image du dossier (facultatif) |
accent |
"preference" (défaut) : l'accent suit le réglage du joueur (Réglages → Interface → Couleur d'accent : default, c'est le --pv-accent du thème ; artwork, la teinte de l'illustration de la map, optionnelle ; custom, une couleur choisie) ; "fixed" : l'accent est toujours le --pv-accent du thème (couleur signature : vert phosphore…). L'ancien mot "artwork" est accepté et vaut "preference" |
tokens |
fichier de jetons (tokens.css par défaut) |
localized |
{ "fr": { "name": "…", "description": "…" }, "zh": { … } } : nom et description par langue (code de langue du catalogue : en, fr, zh) |
tokens.css
Un bloc :root { --pv-x: valeur; … } et jusqu'à deux blocs @media (max-width: <n>px) { :root { … } } pour les fenêtres étroites (320 à 4000 px). Rien d'autre : aucun autre sélecteur, propriété, @import, @font-face, imbrication, url(), expression, var() hors couleurs. Les commentaires /* … */ sont permis. Limites : 64 Kio par fichier, 200 déclarations, 600 caractères par valeur, images de 4 Mio au plus. Un jeton absent garde la valeur neutre de l'application : un thème peut être partiel. Un fichier refusé retire son thème de la liste, avec un message précis (numéro de ligne, jeton, raison) dans le journal.
Le contrat des jetons
Types de valeurs :
- color : couleur :
#rgb,#rrggbb,#rrggbbaa,rgb()/rgba()/hsl()/hsla()(nombres, syntaxe virgules ou espaces avec/ alpha),color-mix(in srgb, <couleur> [n%], <couleur> [n%]),transparent,white,black, etvar(--pv-<jeton couleur>)(la seule référence permise, vers un autre jeton de type couleur, par exemple l'accent). - length : longueur : nombre +
px,em,rem,%,vw,vh,vmin,vmax(ou0),clamp(a, b, c),min(a, b),max(a, b). - font : pile de polices : 1 à 8 familles (noms entre guillemets ou mots simples : lettres, chiffres, espaces,
.,_,-), la dernière une famille générique (sans-serif,serif,monospace,system-ui,ui-monospace,cursive) ;PrismLegacyest la police du jeu. - weight : graisse : entier de 1 à 1000.
- opacity : nombre de 0 à 1.
- easing :
ease,ease-in,ease-out,ease-in-out,linearoucubic-bezier(x1, y1, x2, y2)(x de 0 à 1, y de -4 à 4). - clip : forme :
none,polygon(x y, …)(3 à 16 points, coordonnées en longueurs oucalc(<n>% ± <n>px)) ouinset(1 à 4 décalages [round 1 à 4 rayons]). - layers : dégradés :
noneou 1 à 4linear-gradient(…)/repeating-linear-gradient(…)séparés par des virgules (angle90degouto right, 2 à 12 arrêts : couleur + 0 à 2 positions). - filter : filtre :
noneou jusqu'à 6 fonctions parmisaturate,brightness,contrast,grayscale,sepia,opacity,hue-rotate(<n>deg),blur(≤ 12px). - image : image :
noneouimage("chemin/relatif.png")(un fichier image du dossier du thème : png, jpg, webp, gif, svg). - case :
none,uppercase,lowercaseoucapitalize. - scheme :
darkoulight. - choice : un mot pris dans la liste du jeton (voir « Choix de mise en page »). Tout autre mot, plusieurs mots, une valeur vide, un nombre ou un
var()sont refusés.
| Jetons | Type | Rôle |
|---|---|---|
--pv-bg |
color | Page background and the colour gaps and cut-outs show. |
--pv-plate, --pv-plate-raised, --pv-plate-hover, --pv-plate-active, --pv-plate-active-hover |
color | Surfaces of panels (plate), raised controls, hovered and selected ones. |
--pv-line, --pv-track |
color | Hairlines and the empty part of gauges. |
--pv-text, --pv-text-2, --pv-text-3 |
color | Primary, secondary and tertiary text. |
--pv-ink |
color | Text drawn on an accent-coloured surface. |
--pv-accent |
color | The theme's own accent: what the interface uses unless the player opted in to the map's artwork or a colour of their own (accentSource), and always for a theme with "accent": "fixed". |
--pv-accent-2 |
color | A second accent for secondary marks (the goal pin, a goal that follows the current judgement). |
--pv-ok, --pv-warn, --pv-danger |
color | Success, warning and error tones. |
--pv-offset-ok, --pv-offset-warn, --pv-offset-high, --pv-offset-bad |
color | Couleur du décalage moyen sur l'écran de résultat, selon son éloignement de zéro : correct (≤ 2 ms), à surveiller (≈ 4 ms), à corriger (≈ 7 ms), à corriger tout de suite (≥ 10 ms). L'interface interpole en continu entre ces quatre couleurs ; un thème clair les redéfinit pour rester lisible. |
--pv-tier-platinum, --pv-tier-diamond, --pv-tier-prism, --pv-tier-prism-b, --pv-tier-prism-c |
color | Couleurs des paliers de succès au-dessus de l'or (platine, diamant, puis prisme, le plus haut ; bronze, argent et or sont les couleurs du podium). Le prisme est un bleu, un violet et un cyan : seul l'anneau de la médaille les fond en dégradé conique, le texte et la barre n'utilisent que --pv-tier-prism. Un thème clair les redéfinit pour rester lisibles. |
--pv-gold, --pv-silver, --pv-bronze |
color | Podium ranks. |
--pv-graph-surface, --pv-graph-grid, --pv-graph-zero, --pv-graph-cursor |
color | Chart plot background, grid lines, zero line and inspection cursor. |
--pv-graph-low, --pv-graph-mid, --pv-graph-high, --pv-graph-peak |
color | Palette des graphes, des valeurs calmes aux plus chargées (la chronologie de densité monte de low à peak). Les couleurs des paliers de jugement et des difficultés ne sont jamais des jetons. |
--pv-graph-bar-stroke |
color | Contour des barres de graphe. |
--pv-clip-lg, --pv-clip-md, --pv-clip-sm |
clip | Shape of large (rows, panels), medium (buttons, tabs) and small (badges) plates. |
--pv-clip-edge-lg, --pv-clip-edge-md, --pv-clip-edge-sm |
clip | Shape of the accent edge drawn on the leading side of each plate size. |
--pv-clip-meter, --pv-graph-bar-clip |
clip | Shape of gauge fills and of chart bars. |
--pv-art-clip |
clip | Shape of cover pictures. |
--pv-inset-lg, --pv-inset-md, --pv-inset-sm |
length | Horizontal padding that clears the slanted or chamfered ends of each plate size. |
--pv-edge-w, --pv-meter-h, --pv-radius-input, --pv-radius-panel, --pv-graph-line-w, --pv-graph-bar-gap |
length (au plus 64px) | Accent edge width, gauge height, input corner radius, chart line width and gap between chart bars. |
--pv-h-sm, --pv-h-md, --pv-h-lg |
length | Control heights. |
--pv-art-inset, --pv-art-left, --pv-art-w, --pv-art-pad |
length | Cover picture of a song row: inset, left offset, width and the text padding it needs. |
--pv-set-row, --pv-diff-row |
length (au moins 8px, au plus 400px) | Hauteur d'une carte de set et d'une ligne de difficulté de la liste (rapportée au modèle de la liste). |
--pv-set-gap, --pv-diff-gap |
length (0 à 100px) | Espace sous une carte de set et sous une ligne de difficulté. |
--pv-shift-base |
length | Distance entrance animations travel (zero under reduced motion). |
--pv-rail-w, --pv-drawer-w |
length (au moins 40px, au plus 600px) | Width of the collapsed (icon rail) and expanded Escape drawer. |
--pv-display-ls, --pv-label-ls, --pv-title-ls, --pv-hero-ls |
length (peut être négatif) | Letter spacing of display text, labels, titles and big numerals. |
--pv-fs-xs, --pv-fs-sm, --pv-fs-md, --pv-fs-lg, --pv-fs-xl, --pv-fs-2xl, --pv-fs-3xl, --pv-fs-hero, --pv-fs-meta, --pv-fs-accuracy, --pv-fs-title, --pv-fs-rating |
length (au moins 12px) | Type scale; every size is at least 12px. |
--pv-font-display, --pv-font-body, --pv-font-num, --pv-font-label, --pv-font-mono |
font | Font stacks: display text, body, numerals, labels, technical details (PrismLegacy is the game font). |
--pv-fw-display, --pv-fw-num, --pv-fw-label |
weight | Font weights. |
--pv-art-opacity |
opacity | Opacity of cover pictures. |
--pv-ease, --pv-ease-spring |
easing | Easing of motion and of its springy variant. |
--pv-display-case, --pv-label-case |
case | Text transform of display text and labels. |
--pv-color-scheme |
scheme | Whether native controls (scrollbars, inputs) are dark or light. |
--pv-meter-ticks, --pv-art-mask, --pv-graph-scan, --pv-shade |
layers | Gradient layers: gauge segment ticks, cover fade mask, the chart scanline overlay and the shade over the map picture. |
--pv-art-filter |
filter | Filter applied to the map picture behind the menus. |
--pv-plate-filter |
filter | Profondeur sous chaque plaque : drop-shadow(<x> <y> [<flou>] <couleur>) (décalages ≤ 48 px), ou none. Un filtre et non une ombre de boîte, car les plaques sont des formes découpées. |
--pv-plate-sheen |
layers | Jusqu'à quatre dégradés linéaires peints au-dessus de la couleur de chaque plaque (biseau, relief d'une touche, reflet) ; none pour une plaque plate. |
--pv-overlay-image |
image | Une image du dossier du thème dessinée sur l'illustration de la map et sous les menus (vignette, grille, décor), étirée à la fenêtre (le preserveAspectRatio du SVG choisit entre rognage et déformation) : image("images/overlay.svg"). |
--pv-thumb |
color | Pouce du segment choisi d'un contrôle segmenté (avec --pv-layout-controls: pill) ; la plaque survolée si absent. |
--pv-switch |
color | Couleur d'un interrupteur allumé (avec pill) ; l'accent si absent. |
--pv-bg-image |
image | Picture of the theme's folder drawn on the page background (image("images/bg.png")). |
Choix de mise en page
Un thème peut changer la construction des menus, toujours comme des données : un jeton de type choice prend un mot dans une liste fermée (vérifiée par Rust, puis par la page). La page pose chaque choix valide en attribut data-layout-<nom> sur <html> ; les feuilles de style de l'application (theme/layout.css et les composants) y réagissent avec grid-template-areas, order et des variables. Un thème ne fournit aucun sélecteur ni aucun CSS : seulement ces mots. Le premier mot de chaque liste est la mise en page actuelle : un choix omis (ou refusé) vaut ce mot, donc un thème qui n'en dit rien ne change rien.
| Jeton | Mots | Effet |
|---|---|---|
--pv-layout-list |
right · left · center |
Côté de la liste de morceaux ; le panneau de la map est de l'autre côté. center : liste au milieu, panneau à droite, illustration visible à gauche. La hauteur des lignes reste celle des jetons --pv-set-row et --pv-diff-row. |
--pv-layout-tabs |
top · rail · dock · sidebar |
Onglets de la map : barre en haut, rail vertical à gauche du panneau, dock en bas, ou barre latérale pleine hauteur à gauche de tout le panneau (une liste de source ; elle se replie en icônes dans les fenêtres étroites). |
--pv-layout-play |
bottom · top |
Notation, jugement et bouton Jouer en bas du panneau, ou en haut. |
--pv-layout-density |
comfortable · compact · dense |
Espacements et hauteurs de lignes plus serrés d'un cran à chaque palier (la liste virtualisée relit ses hauteurs). |
--pv-layout-chrome |
none · titlebar · nav |
Bandeau décoratif : titlebar, une barre de fenêtre (trois pastilles et le nom de l'application) ; nav, une fine barre de navigation de site, teinte translucide à 80 % (le seul flou) avec le nom de l'application et le titre de la page. Sans fonction : les vrais boutons sont ceux du système. |
--pv-layout-drawer |
left · right · bottom |
Bord d'où vient le tiroir d'Échap ; bottom : un dock d'icônes. |
--pv-layout-rows |
cover · strip · text · list |
Ligne de la liste : vignette à gauche, illustration en bandeau estompé derrière la ligne, texte seul (ligne de tableau), ou ligne plate de list (vignette et texte sans plaque, une teinte sous la ligne survolée et la ligne choisie, comme une liste d'application musicale). |
--pv-layout-profile |
top · rail |
Identité du joueur sur la page Profil : bandeau en haut, ou rail à gauche. |
--pv-layout-cards |
rich · compact |
Carte de score du classement : grande précision et compteurs dessous, ou une ligne de tableau. |
--pv-layout-buttons |
plate · bracket · link |
link : tous les boutons sauf le principal deviennent du texte d'accent sans plaque. Boutons, onglets et boutons-icônes : plaque pleine à la forme du thème, ou contour en crochets dessiné au dégradé (sans remplissage, comme un [ ] de terminal) ; les éléments à fond d'accent gardent l'accent pour les crochets et leur texte. |
--pv-layout-focus |
ring · blink |
Focus clavier : le contour habituel, ou ce contour qui clignote comme un curseur de texte (fixe, en anneau, sous prefers-reduced-motion). |
--pv-layout-icons |
none · icon |
Les onglets (Info, Classement, Mods, Entraînement, Éditeur) montrent leur icône avant le libellé. |
--pv-layout-groups |
plain · inset |
Lignes des Réglages : filets entre sections, ou groupes en cartes arrondies à filets fins entre les lignes (fenêtre de réglages système). |
--pv-layout-controls |
plain · pill |
Interrupteurs, contrôles segmentés, menus déroulants et barres de défilement : formes du thème, ou interrupteurs en pilule à pastille blanche, contrôle segmenté à segment choisi en relief, menus arrondis à sélection en couleur d'accent, barres fines superposées. |
--pv-layout-hero |
plate · open |
En-tête de la map choisie et ses quatre chiffres : sur des plaques, ou open, posés sur la page (titre et grand chiffre sans carte, chiffres en colonnes entre filets, comme une page produit). |
--pv-rating-dark |
opacity | Même idée pour le texte des valeurs de difficulté et de compétences (la couleur ratingColor elle-même n'est jamais modifiée). |
--pv-tier-dark |
opacity | Part de noir mélangée au TEXTE des paliers de jugement (0 : couleurs du jeu). Les thèmes clairs la montent pour que les noms de paliers restent lisibles ; les pastilles et les données ne changent pas. ratingColor n'est jamais touché. |
Graphe de progression du replay
Le graphe de progression (combo, précision, densité de la map sur un axe de temps commun) se règle par les mêmes jetons. Le composant lit ces mots (attributs data-graph-* sur <html>) sans aucun balisage par thème ; zoom, panoramique, popovers, légende et masquage des intervalles de miss ne changent pas.
| Jeton | Valeurs | Effet |
|---|---|---|
--pv-graph-progression |
steps · lines · area · bars |
steps (défaut) : chaque échantillon tenu jusqu'au suivant, densité en aire pâle ; lines : segments droits entre les vrais échantillons, rien de lissé ; area : courbes en escalier remplies ; bars : densité en barres, combo et précision en escalier. Le joueur peut le remplacer dans Réglages → Interface (« Style du graphe de progression »). |
--pv-graph-grid-style |
lines · none · dots · scanlines |
Quadrillage derrière les graphes du replay. |
--pv-graph-cursor-shape |
dashed · solid · band |
Curseur : trait pointillé, plein ou bande translucide. |
--pv-graph-marker |
none · dot · diamond · square |
Repère là où le curseur croise chaque courbe visible. |
--pv-graph-miss-style |
bands · ticks |
Intervalles de miss : bandes pleine hauteur ou petites marques en bas. |
--pv-graph-curve-w |
length 1–6 px | Épaisseur de la courbe de combo (précision : 75 %, contour de densité : 60 %). |
--pv-graph-fill-opacity |
opacity | Opacité des remplissages sous les courbes. |
--pv-graph-combo, --pv-graph-accuracy, --pv-graph-density-low, --pv-graph-density-mid, --pv-graph-density-high |
#rrggbb |
Couleurs des trois courbes (la densité est un dégradé low → mid → high selon l'ampleur). |
Les trois courbes restent distinguables, quoi que fasse le thème. Le combo est toujours plein, la précision toujours pointillée, la densité toujours une aire ou des barres (ce n'est pas réglable). Côté couleur, le combo, la précision et le milieu de la densité doivent différer deux à deux d'au moins 30° de teinte (pour des gris : 25 % de luminosité) ; sinon le thème entier est refusé au chargement, avec le nom des deux jetons. Les couleurs des paliers de jugement et ratingColor ne sont jamais des jetons ; --pv-rating-dark et --pv-tier-dark ne font qu'assombrir leur texte.
Les tailles du jeu (espacements --pv-s*, durées --pv-dur*, --pv-stagger) appartiennent à l'application ; sous prefers-reduced-motion les durées et --pv-shift-base tombent à zéro quel que soit le thème.
L'illustration de la map reste visible, quel que soit le thème. L'image de la map derrière les menus fait partie de l'identité du jeu : un thème peut la teinter (--pv-shade), l'assombrir, la flouter, la désaturer ou la recolorer (--pv-art-filter), jamais la retirer ni l'enterrer. Le validateur (crates/themes/src/artwork.rs) calcule la part de lumière de l'image qui atteint l'écran, en moyenne sur la fenêtre : chaque dégradé de --pv-shade compte pour la moyenne de son opacité le long de son axe, les couches se composent, et le filtre compte pour ses brightness() et opacity(). Les règles : au moins 20 % de l'image visible ; aucune couche de --pv-shade ne couvre plus de 90 % en moyenne (un aplat opaque est refusé) ; dans le filtre, brightness ≥ 0,25, opacity ≥ 0,4 et contrast ≥ 0,5. Le contrôle s'applique aussi à chaque bloc @media. Le joueur garde son interrupteur : « Fond noir » (Vidéo → Fond en jeu) masque l'image en jeu ; la sélection de morceaux n'est pas concernée, et la luminosité du fond (backgroundBrightness) ne change pas non plus. Pour des plaques lisibles sans cacher l'image, préférer des plaques translucides (opacité de 0,7 à 0,85).
Un thème ne change pas la géométrie d'un bouton-icône. Un bouton-icône (IconButton, les « ? » d'aide, les icônes d'export et d'import) est un carré dont le côté vient des jetons --pv-h-md (--pv-h-sm pour la petite taille) et dont la forme vient de --pv-clip-sm ; l'icône est centrée dedans (display: inline-grid; place-items: center, svg en bloc), et ni la casse, ni l'interlettrage, ni la hauteur de ligne, ni le remplissage latéral que les thèmes donnent aux boutons de texte ne l'atteignent. Un thème en capitales, en mono ou espacé ne peut donc pas décentrer ni déformer une icône. Les boutons de texte (Utiliser, Personnaliser…) restent libres d'avoir le style du thème. Garde-fou : tests/icon-button-geometry.spec.ts parcourt tous les thèmes livrés, à 1400×850 et 1000×700, et vérifie que chaque bouton-icône de la page Skins est un carré, que son icône est centrée à ±0,5 px, qu'elle ne déborde pas, que les boutons d'une même rangée ont la même hauteur, et que le survol, le focus clavier et l'appui ne changent pas la taille.
Écrire un quatrième thème
- Copier
themes/slant/enthemes/<mon-id>/(ou dans<data>/themes). - Éditer
theme.json(id= nom du dossier) ettokens.css. - Dans Skins → Recharger (ou relancer), la carte du thème apparaît ; l'aperçu est dessiné avec vos jetons.
Pour changer la construction des écrans, ajouter des mots --pv-layout-* dans tokens.css (voir « Choix de mise en page ») ; un mot omis garde la mise en page actuelle. Les thèmes livrés servent d'exemples : macos (rail d'onglets, bandeau de fenêtre), hack (crochets, focus clignotant, dense), ledger (rail, profil en colonne, tiroir à droite). Les jetons --pv-tier-dark et --pv-rating-dark servent à un thème clair (texte des paliers assombri).
Rien d'autre : pas de code, pas d'enregistrement. Le thème par défaut (réglage uiTheme absent ou invalide) est cabinet ; le thème d'identifiant classic reste choisissable et est réservé à la structure d'origine des menus (les composants d'avant le système de design lui sont rattachés par cet identifiant) ; un autre identifiant reçoit la structure du système de design.
Repli
Le thème choisi est absent : le premier thème disponible est utilisé, avec un avis. Aucun thème n'existe : l'application utilise ses jetons neutres intégrés (theme/fallback.css), de sorte que rien n'est jamais affiché sans style.
L'éditeur de skin
L'éditeur de skin (Skins → Personnaliser) vit dans la surcouche, une page WebView à part qui n'a ni la liste des thèmes ni les réglages : l'hôte lui envoie overlayTheme (le thème choisi uiTheme, accentSource, accentCustom et les thèmes déjà validés, les mêmes données que interfaces, aucune de plus et rien qu'un script puisse atteindre). La page en tire, avec le même code que les menus (theme/apply.ts, direction.ts, accent.ts), les variables CSS sous [data-direction], les mots data-layout-*, data-scheme et l'accent du joueur (un thème accent: fixed garde le sien). Sur la page, PrismLegacy est déclarée comme dans les menus pour les piles de polices qui la nomment.
Le panneau, les onglets, les champs, les interrupteurs, la barre d'outils et les cadres de l'éditeur ne lisent que ces jetons, par une couche de variables --ed-* (apps/web/src/overlay/editor/editor-theme.css) : surfaces (--pv-plate, --pv-bg, --pv-plate-raised), lignes (--pv-line), textes (--pv-text, -2, -3), accent et encre (--pv-accent, --pv-ink), sélection (--pv-accent-2, à défaut l'accent), rayons (--pv-radius-input, --pv-radius-panel), polices (--pv-font-body, -display, -label, -num), --pv-switch et --pv-thumb avec le mot --pv-layout-controls: pill (interrupteur en pilule, onglets en capsule à pouce relevé). Aucune couleur n'est écrite en dur dans l'éditeur (un test le vérifie) : un thème ajouté plus tard le restyle sans changement. Seuls le voile sur le jeu, les ombres et l'espace colorimétrique du sélecteur de couleur sont neutres.