Application iOS contemplative de peinture de nuages volumétriques au-dessus d'un paysage.
Aether — du grec αἰθήρ, divinité primordiale de l'air supérieur, du ciel lumineux, et cinquième élément classique (au-delà des quatre éléments terrestres). Le nom porte le concept : l'utilisateur agit sur le ciel.
Registre éditorial (UI + App Store) : sobre, atmosphérique, contemplatif. Jamais « fun » ni enfantin.
L'utilisateur choisit un paysage dans une galerie curée, peint des silhouettes de nuages dans un canvas 2D simple, et un moteur de rendu volumétrique Metal transforme ces silhouettes en nuages 3D plausibles, éclairés par la position réelle du soleil et de la lune à l'endroit et à l'heure choisis. Une météo statique propre à chaque paysage curé informe l'état initial.
Expérience visée : contemplative, lente, satisfaisante. Pas d'éditeur 3D complexe — une poignée de sliders au maximum.
- Swift 6, Xcode 16+ (dev : Xcode 26.5 / Swift 6.3), iOS 17+ minimum
- SwiftUI pour toute la chrome UI (gallery, palette, sliders, settings)
- MetalKit (
MTKView) pour le canvas de rendu volumétrique - Metal Shading Language : raymarching, bruit 3D, composition
- Météo statique : chaque paysage curé porte un
WeatherSnapshotfigé (plus de récupération réseau / WeatherKit) - CoreLocation : lieu de la scène (orientation du ciel)
- SwiftAA : positions soleil/lune (fallback impl. interne formules Meeus)
- Swift Concurrency (async/await, actors) — pas de Combine, pas de RxSwift
- SwiftPM uniquement, pas de CocoaPods
- Tests : XCTest + Swift Testing pour le métier (astro, météo→nuage, calques/persistance)
Le projet Xcode est généré par XcodeGen depuis project.yml (source de
vérité). Aether.xcodeproj est git-ignoré et régénéré via
xcodegen generate. Ajouter un fichier = créer le fichier, pas de churn du
.pbxproj.
- Bundle id :
io.github.glandais.aether - Icône :
Aether/Resources/Assets.xcassets/AppIcon.appiconset/icon.png(1024², nuage au crépuscule) - Signature : automatique, équipe
7Q49262697(GABRIEL JEAN YVES ANNE LANDAIS), réglée dansproject.yml
Simulateur : celui déjà démarré, ciblé par son UDID — jamais par nom ni
booted (cf. instructions globales : un seul simulateur à la fois). Sur la
machine de dev : iPhone 17 Pro Max, iOS 26.5
(05EC0174-83A8-4D6D-B334-BB93E84C8D46). DerivedData dans build/dd.
scripts/verify.sh applique la même règle (surcharge : AETHER_DESTINATION).
# 0. UDID du simulateur démarré
SIM="$(xcrun simctl list devices booted | grep -oE '[0-9A-F]{8}(-[0-9A-F]{4}){3}-[0-9A-F]{12}' | head -1)"
# 1. (Re)générer le projet après tout ajout/déplacement de fichier
xcodegen generate
# 2. Compiler
xcodebuild build -project Aether.xcodeproj -scheme Aether \
-destination "platform=iOS Simulator,id=$SIM" -derivedDataPath build/dd
# 3. Tests (métier : astro, météo, éclairage…)
xcodebuild test -project Aether.xcodeproj -scheme Aether \
-destination "platform=iOS Simulator,id=$SIM" -derivedDataPath build/dd
# 4. Vérification visuelle : installer, lancer, capturer
APP="build/dd/Build/Products/Debug-iphonesimulator/Aether.app"
xcrun simctl install "$SIM" "$APP"
xcrun simctl launch "$SIM" io.github.glandais.aether
xcrun simctl io "$SIM" screenshot build/shot.pngLe rendu Metal n'étant pas testable unitairement, chaque étape se vérifie par capture d'écran sur simulateur. Les gestes ne se pilotent pas sans interaction : pour injecter une scène déterministe (paysage + nuages + heure), utiliser le harnais DEBUG ci-dessous plutôt que du code temporaire.
Compilé hors des builds Release/App Store (#if DEBUG). Pour l'activer dans
un build Release (profilage perf en -O), passer
SWIFT_ACTIVE_COMPILATION_CONDITIONS='DEBUG' à xcodebuild.
ScreenshotHarness(Aether/App/ScreenshotHarness.swift) : démarre l'app sur une scène injectée via la variable d'envAETHER_SHOT=scene:hour:chrome:cloud.scene:dusk|dawn|blue|noon(index galerie 0…3)hour: heure locale décimale (13,18.5,-= défaut scène)chrome:hidden(ciel plein cadre) |showncloud:cumulus|scattered|band|perf(pire cas 3 étages) |none- Les calques injectés reçoivent les défauts météo de la scène (comme la peinture à la main) — pas de divergence debug/réel.
- Lancement device :
xcrun devicectl device process launch --device <id> --environment-variables '{"AETHER_SHOT":"noon:13:shown:perf"}' <bundle>. Simulateur : préfixeSIMCTL_CHILD_(SIMCTL_CHILD_AETHER_SHOT=…).
- Overlay FPS (
DebugHUD+ badgeCanvasView) : le FPS mesuré parRenderer.draws'affiche à l'écran (à gauche, centré) — lecture perf dans l'app sur device, sans Console. - Interrupteurs de passe (profilage) :
AETHER_PERF_NOSKY/AETHER_PERF_NOSEA/AETHER_PERF_NOCLOUD=1désactivent une passe pour isoler son coût, sans rebuild (relancer avec une autre combinaison de variables d'env). AETHER_SCALE(device uniquement) : force le repli bilinéaire (1) ou une échelle interne ≠ ½ du chemin MetalFX (cf.docs/PIPELINE.md« Upscale MetalFX »).
Après toute modification de code, lancer scripts/verify.sh : il régénère le
projet (XcodeGen) puis enchaîne SwiftLint (--strict, config
.swiftlint.yml), xcodebuild analyze (analyseur du compilateur) et
Periphery (code mort, config .periphery.yml). Le script échoue au premier
avertissement ; il doit rester vert. Outils à installer via Homebrew (hors du
script) : brew install swiftlint periphery.
Site statique de présentation, HTML + CSS pur (pas de build, pas de
framework), dans web/ : accueil (index.html), politique de confidentialité
(privacy/) et support (support/) — ces deux dernières fournissent les
URLs requises par App Store Connect. Thème crépusculaire propre à Aether ;
l'icône de l'app est copiée en web/icon.png. Aperçu local :
cd web && python3 -m http.server.
Hébergé sur GitHub Pages, base canonique https://glandais.github.io/aether/ :
- Support :
https://glandais.github.io/aether/support/ - Confidentialité :
https://glandais.github.io/aether/privacy/
Déploiement automatique via .github/workflows/pages.yml à chaque push sur
develop touchant web/**. Prérequis une fois : régler la source Pages du
dépôt sur « GitHub Actions ». Les captures réelles peuvent remplacer l'art
CSS aux emplacements <!-- SCREENSHOT SLOT: … --> de index.html.
- Le Domain définit un type
Scene, qui masqueSwiftUI.Scene. DansAetherApp, lebodydoit être typésome SwiftUI.Scene(qualifié). - Le catalogue de strings est
Aether.xcstrings→ tableAether, pas la tableLocalizablepar défaut. Toujours passertableName: "Aether"(Text("clé", tableName: "Aether"),String(localized:table:)). - Filtrage de texture :
R32Floatn'est pas filtrable sur GPU iOS (seulement sur Mac → le simulateur masque le bug). Toute texture échantillonnée enfilter::lineardoit être ≤ 16 bits (R8Unorm, RGBA16Float). RéserverR32Floataux échantillonnagesnearest.
| Couche | Rôle | Dépend de |
|---|---|---|
Aether/App/ |
entry point SwiftUI, navigation racine | Features |
Aether/Features/ |
modules SwiftUI par feature (Canvas, Gallery, About, Settings) | Domain, Services |
Aether/Rendering/ |
pipeline Metal, shaders, atlas de couverture / bruit 3D | Domain uniquement |
Aether/Domain/ |
modèles purs (Scene, CloudLayer/CloudGenus, BrushStroke, WeatherSnapshot, CelestialPosition) |
rien |
Aether/Services/ |
AstroService, LocationService (protocoles + impl) |
Domain |
Aether/Resources/ |
assets, paysages curés | — |
Règles de dépendance :
- Domain ne dépend de rien.
- Services dépendent de Domain.
- Features dépendent de Domain et Services.
- Rendering dépend de Domain uniquement (pas de Service direct ; on lui passe des modèles déjà résolus).
- Toute dépendance externe (CoreLocation, SwiftAA) est cachée derrière un protocole pour la testabilité.
Pipeline volumétrique en couches (raymarching → bruit 3D → pinceau → scattering →
demi-rés/temporel → astro → météo), plus ciel atmosphérique, mer, astres et
étoiles. Complet et vérifié. Les nuages vivent dans des coquilles
sphériques concentriques enveloppant le ciel (modèle realtime_clouds) : un
calque éditable = une coquille à une altitude, l'empilement crée les étages
(CloudLayer/CloudGenus, couverture peinte en carte 2D directionnelle, raymarch
concentrique). La peinture se dépose par direction du ciel ; le regard l'oriente,
il ne crée plus de domaine. Détail des étapes, choix d'implémentation et notes par
fonctionnalité : docs/PIPELINE.md ; plan et décisions du
modèle coquilles : docs/SHELLS.md. Références algorithmiques
(papers + code) : BIBLIO.md.
Toute évolution du rendu doit rester visuellement vérifiable par capture sur simulateur (cf. « Build & vérification »).
- Nommage Apple-style (
UpperCamelCasetypes,lowerCamelCasemembres, pas de préfixe) - Pas de force-unwrap en prod sauf justifié en commentaire
- Pas de singleton sauf
App— environnement SwiftUI ou injection explicite - Strings UI dans
Aether.xcstringsdès le début (fr + en min., registre sobre/atmosphérique) - Logs via
Logger(os.log), subsystemio.github.glandais.aether, pas deprint - Shaders Metal commentés en anglais ; code Swift commenté en français OK
- Un commit logique par responsabilité, jamais « WIP » ni « fixes »
- Branche d'intégration :
develop(pas demain) — commits et PR ciblentdevelop
Import de photo personnelle (profondeur CoreML/LiDAR, EXIF, caméra calée sur la photo) · partage social / comptes / backend · export vidéo ou animation · spécifique iPad au-delà de l'universal de base · localisation au-delà de fr/en · analytics, crash reporting. Seul achat intégré : les pourboires (ci-dessous).
Trois achats consommables qui ne débloquent rien, depuis la feuille « À
propos » → « Soutenir Aether » (Features/Tips/). Un pourboire au développeur,
dans l'app, passe par l'achat intégré (directive 3.1.1) : aucun lien de don
externe dans l'app, Ko-fi reste sur le site et le README.
- Produits ASC :
io.github.glandais.aether.tip.small(0,99 €,6814744465),.medium(2,99 €,6814744411),.large(4,99 €,6814744528). Pays de base France, 175 territoires, noms fr/en. Le premier achat intégré part avec une version de l'app : les joindre à la soumission de la 1.0. TipJar.swiftest copié du fichier de référence commun aux apps du développeur (dépôtdonations, hors de ce dépôt) ;TipJarViewen est l'adaptation à la tableAether. Noms et prix viennent du store (displayName,displayPrice) : le catalogue n'en porte aucun.Tips.storekit(racine) est branché sur le schéma parstoreKitConfiguration. Il ne vaut que pour un lancement depuis Xcode : installée parsimctl, l'app interroge le vrai store et l'écran dit « indisponible » tant que les produits ne sont pas validés.- Sur simulateur,
axe tapne déclenche pas lesNavigationLinkni le bouton « Acheter » de la feuille de test : passer--tap-style physical.
Distribution — App Store. Pipeline de rendu complet plus galerie curée,
éclairage selon la scène, météo statique, heure choisie + lune, fond de ciel
atmosphérique, caméra à regard libre, peinture en calques multi-coquilles (une
coquille concentrique par étage : cumulus / altocumulus / cirrus, couverture peinte
par direction du ciel), mer raymarchée, soleil / lune / étoiles dessinés dans le
ciel, et persistance d'un ciel en fichier .aether — tout est implémenté et
vérifié. Version courante :
1.0.3 (build 4), bundle io.github.glandais.aether (MARKETING_VERSION /
CURRENT_PROJECT_VERSION dans project.yml).
- Détail du pipeline et des fonctionnalités :
docs/PIPELINE.md - Persistance d'un ciel (
.aether: enregistrer / rouvrir à l'identique) :docs/PERSISTENCE.md - Distribution App Store / TestFlight (coordonnées ASC, flux release) :
docs/DISTRIBUTION.md
- Release App Store publique (TestFlight-first pour l'instant) : captures,
métadonnées, App Privacy, prix, soumission — détail dans
docs/DISTRIBUTION.md. Skillsasc-*. URLs support / confidentialité fournies par le site vitrine (cf. « Site vitrine »). - Réglages (
Features/Settings) : choisir le lieu (l'heure est déjà réglable au canvas ; manque la sélection géographique manuelle). - Profilage perf sur device réel : mer demi-rés confirmée à 60 ips
(iPhone 13 Pro Max). Leviers de qualité si besoin :
SEA_ITER_FRAGMENT, résolution de la passe ciel+mer (½ → ⅔), nombre de pas de la trace.