Commit Graph

106 Commits

Author SHA1 Message Date
c973b67ec9 feat(images): self-host hero photos from Unsplash (#1)
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-05-07 01:01:54 +03:00
MVA Global Fret
713168ecbe Update hosting info: GitHub Pages → Hostinger VPS (Falkenstein)
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-06 20:28:00 +02:00
MVA Global Fret
70f8f86c7a Add Mentions Légales & Politique de Confidentialité pages
Create two new legal pages (FR/EN/MG) with LAATEL Corporation
company details (STAT, RCS, NIF). Add footer links to all pages
and translation keys for the three languages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-06 20:05:39 +02:00
8f51ef794a
Merge pull request #3 from MVA-Global-Fret/migration-brevo
confirmation.html: center button text in action row
2026-05-06 16:57:18 +02:00
MVA Global Fret
77baadffba confirmation.html: center button text in action row
The .btn class uses display:inline-flex with align-items:center but
no justify-content, so text stuck to the left when the parent flex
stretched the buttons to fill the row. Add justify-content:center
scoped to the confirmation card actions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:54:40 +02:00
c12f273e24
Merge pull request #2 from MVA-Global-Fret/migration-brevo
Worker: use Forms API for contact creation (no scope required)
2026-05-06 15:57:21 +02:00
MVA Global Fret
2b148a8682 Worker: use Forms API for contact creation (no scope required)
Direct CRM API needed crm.objects.contacts.write scope which the
existing Personal Access Key doesn't have. Using HubSpot Forms API
instead — same endpoint the browser used to call directly, but now
called server-side from the Worker after email verification.

This means HubSpot contact creation works without granting any
additional scopes to the token: anyone can submit a public form.

Also updates the welcome email warning to tell the customer to copy
the address EXACTLY as shown (the reference is now baked into line 1
of the address) and not to modify anything.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:54:58 +02:00
c66ca36620
Merge pull request #1 from MVA-Global-Fret/migration-brevo
Migration brevo
2026-05-06 14:00:51 +02:00
MVA Global Fret
07ccec0808 Anti-spam: only register HubSpot contact AFTER email confirmation
Previously the Forms API created the contact at form submission time —
which meant unverified signups (bots that pass Turnstile, typos, fake
emails) polluted HubSpot. Now:

- Form submit → Worker stores all data in KV (24h TTL) + sends Brevo
  verification email (no HubSpot write)
- User clicks email link → Worker generates ref + creates HubSpot
  contact via CRM API + sends welcome email with ref + Paris address

Plus this commit:
- Email header gets the MVA logo on the left of the dark blue banner
- Welcome email's first address line auto-injects (MVA-XXX) so the
  customer can copy it directly onto their package

Also handles idempotency — clicking the verification link a second time
returns the existing ref without creating a duplicate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:50:32 +02:00
MVA Global Fret
82bc8ba358 Switch from Resend to Brevo for transactional emails
Resend requires a verified domain to send to arbitrary recipients —
mvaglobalfret.com isn't registered. Brevo accepts single-sender
verification on a free email address, so we can send from
mvaglobalfret@gmail.com without owning a domain.

- Worker: replace resendSend() with brevoSend() (api.brevo.com/v3/smtp/email)
- Env vars: BREVO_API_KEY, BREVO_SENDER_EMAIL, BREVO_SENDER_NAME
- Update comments in confirmation.js and form-handler.js

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:26:50 +02:00
MVA Global Fret
eb5c4f1cee Email verification flow via Resend (Turnstile + click-to-confirm)
Architecture finale :

1. User remplit formulaire + passe Turnstile CAPTCHA → form-handler.js
2. form-handler.js POST au Worker avec action 'requestVerification'
3. Worker valide Turnstile, génère un token UUID, le stocke en KV (TTL 24h)
   avec firstname/email/reference_client, puis envoie un email via Resend
   avec un lien : confirmation.html?token=XXX
4. User reçoit email, clique 'Confirmer mon email'
5. confirmation.html lit le token de l'URL, POST au Worker avec action
   'verifyToken'
6. Worker valide le token, envoie le welcome email via Resend (avec ref +
   adresse Paris depuis env var), marque le token comme utilisé
7. confirmation.html affiche 'Inscription confirmée !'

Ainsi : ref + adresse Paris ne sortent JAMAIS avant validation email,
et les bots sont bloqués à l'étape 1 par Turnstile.

Setup Cloudflare requis (côté user) :
- RESEND_API_KEY  : clé API Resend (re_...)
- RESEND_FROM     : adresse expéditrice ('onboarding@resend.dev' pour test,
                    ou domain vérifié pour prod)
- SITE_URL        : optionnel, défaut https://mva-global-fret.github.io/site-mva-global-fret

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 10:59:26 +02:00
MVA Global Fret
a3a36df811 Anti-spam: add Cloudflare Turnstile (CAPTCHA) on registration form
- contact.html: ajout du widget Turnstile (site key: 0x4AAAAAADKDuc7Rmlb1svIL)
- form-handler.js: blocage de la soumission si pas de token Turnstile valide
- Worker: validation server-side du token via /turnstile/v0/siteverify
  avant chaque appel sendWelcomeNow → bloque les bots qui n'auraient
  pas passé le challenge côté client.

Le secret Turnstile est en env var Cloudflare (TURNSTILE_SECRET).

Limite humain : Turnstile détecte les bots avec très peu d'interaction
côté utilisateur (mode 'Managed', le plus souvent invisible).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 10:47:29 +02:00
MVA Global Fret
313c870ea4 Fix bugs inscription: ref dupliquée + email de bienvenue manquant
Bug 1 — Ref MVA-001 dupliquée :
Le filtre HubSpot 'HAS_PROPERTY' avec value:'' retournait 0 résultats.
Suppression du value:'' → maintenant le worker liste correctement les
contacts avec reference_client et incrémente bien (testé : MVA-004).

Bug 2 — Email post-inscription jamais reçu :
Le double opt-in HubSpot ne se déclenche pas via Forms API sans
subscription consent (impossible à configurer sans nouveaux scopes
Private App). Pivot vers une approche plus simple :
- L'email de bienvenue est désormais envoyé directement après
  soumission du formulaire (pas de DOI HubSpot)
- L'envoi passe par le Cloudflare Worker (action sendWelcomeNow)
  pour que l'adresse Paris reste dans les env vars Cloudflare et
  ne soit JAMAIS dans le JS public
- Worker appelle EmailJS REST avec firstname + reference + paris_address

Cleanup : message de succès reverti à 'Inscription réussie' (FR/EN/MG).

Anti-spam : protection légère via filtre email/téléphone côté formulaire.
La cron-based welcome (post-DOI) reste en place mais sera inerte tant
que aucun contact n'a le statut CONFIRMED côté HubSpot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:52:48 +02:00
MVA Global Fret
e1032b1405 Service Commande: redesign 'Et le transport ?' card
Avant : layout row asymétrique, icône en coin haut-gauche, bouton outline
qui flotte, hiérarchie visuelle plate.

Maintenant :
- Carte centrée 560px max, padding plus respirant, bordure or 6px en haut
- Icône 84px navy/or avec drop-shadow → gros pop visuel
- Prix éclaté : '70 000' en gros chiffre, 'Ar / kg' en doré accent
- Description plus compacte, contenue à 440px
- Bouton primary (or solide) avec flèche animée au hover
- Variant mobile (<480px) avec tailles réduites

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 08:40:14 +02:00
MVA Global Fret
0ef9f01fd9 Worker: cron post-confirmation pour envoyer le welcome email après opt-in
Architecture finale (Option A choisie) :

1. User submit form → contact créé en HubSpot avec reference_client
2. HubSpot envoie l'email de double opt-in (sans la ref ni l'adresse Paris)
3. User clique 'Confirmer' → HubSpot met hs_emailconfirmationstatus = CONFIRMED
4. Cron Cloudflare (toutes les 5 min) :
   - Liste les contacts CONFIRMED + créés après le cutoff
   - Filtre via Cloudflare KV (welcomed:<email>) pour idempotence
   - Envoie le welcome email via EmailJS REST API avec :
     • firstname
     • reference_client
     • paris_address (depuis env var PARIS_DEPOT_ADDRESS)
   - Marque envoyé dans KV avec TTL 1 an

Protection :
- L'adresse du dépôt Paris ne quitte JAMAIS Cloudflare/EmailJS
- Elle n'arrive au client que dans le mail de bienvenue post-opt-in
- Bots qui n'ont pas un vrai email ne peuvent pas valider → ne reçoivent rien
- Anti-spam et anti-cartons-vides blindé

Ajout d'une action 'triggerWelcomeQueue' pour debug/manual run.
Doc complète dans cloudflare-worker/DEPLOIEMENT.md (étapes 1 à 6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:24:26 +02:00
MVA Global Fret
f534376f90 Add post-confirmation page that triggers welcome email after double opt-in
Flux complet du double opt-in :
1. User soumet le formulaire → contact créé en HubSpot avec sa référence
2. HubSpot envoie un email 'Confirmez votre inscription'
3. User clique 'Confirmer' → HubSpot le marque 'subscribed'
4. HubSpot redirige vers confirmation.html?email=...
5. La page lit l'email, appelle le Worker Cloudflare pour récupérer la
   référence du contact, et déclenche l'envoi de l'email de bienvenue
   via EmailJS (avec la référence dedans)
6. Affiche succès + référence à l'écran

Idempotence via localStorage pour éviter de spammer l'email à chaque
rechargement de la page.

À configurer dans HubSpot Settings > Marketing > Email > Confirmation
d'inscription : URL de redirection après confirmation =
https://mva-global-fret.github.io/site-mva-global-fret/confirmation.html?email={{contact.email}}

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:03:19 +02:00
MVA Global Fret
c713d40946 Contact: ne plus afficher la référence avant validation email
- Retire le bloc 'Numéro de référence client' de la page de succès
- Met à jour le message en FR/EN/MG : 'Vous recevrez ensuite votre numéro
  de référence client' après confirmation
- Désactive l'envoi immédiat de l'email EmailJS de bienvenue (qui
  contenait déjà la référence). HubSpot envoie son email de
  double opt-in qui sera customisé pour inclure la référence
  via le token {{contact.reference_client}}.

Résultat : la référence n'est jamais visible avant que l'email ne soit
vérifié (puisque seuls les emails valides reçoivent le double opt-in).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:53:23 +02:00
MVA Global Fret
c2e3b1e0d5 Contact: clarify double opt-in flow on success message
HubSpot double opt-in is now enabled at the account level. After
submitting the form, contacts must click the confirmation link in
their email to be added to the marketing list.

The success message now explicitly tells the user to check their
inbox and click the confirmation link, instead of just saying
'inscription enregistrée'.

- title: 'Vérifiez votre boîte mail !' (FR), 'Check your inbox!' (EN), 'Jereo ny boaty mailaka!' (MG)
- main msg: focus on confirmation step
- icon: enveloppe-circle-check (gold) instead of generic green check
- note: nuance that the reference number is for tracking parcels
- emailSent: kept as is (informative footer)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:45:30 +02:00
MVA Global Fret
5ab50dae3b Tarifs: 'Livraison en Province' = à partir de 6 000 Ar (FR/EN/MG)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:23:54 +02:00
MVA Global Fret
422690c194 Intro: bouton 'Accéder au site' visible immédiatement, sans animation
- Retire l'état caché (scale 0.05 / opacity 0 / offset -27vh)
- Retire les transitions de révélation (transform/opacity 2.2s)
- Retire l'animation de pulse halo (.cta-btn::after)
- Retire le déclenchement JS via la classe .revealed (l'avion qui croise le centre n'a plus d'effet sur le bouton)

Le bouton est désormais centré au milieu du viewport dès le chargement,
toujours cliquable. Hover conserve le scale 1.04 + shine effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:21:30 +02:00
MVA Global Fret
8ba9fc9161 Service Commande: stack transport card on mobile (button no longer cropped)
Avant : flex row figé même sur mobile → l'icône 60px écrasait la colonne
texte → le bouton 'Voir nos tarifs détaillés' wrappait sur 2 lignes
avec un cadre asymétrique.

Maintenant : sous 600px de large, le layout passe en colonne (icône
au-dessus, contenu centré). Bouton avec white-space: nowrap pour rester
sur 1 ligne. Sur desktop (>=600px), le layout reste en row classique.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:07:51 +02:00
MVA Global Fret
e58e3d674d Mobile menu: scrollable + dvh height fix
- overflow-y: auto pour scroll vertical quand le menu déborde
- height: 100dvh (dynamic viewport) pour meilleur rendu mobile (barre URL)
- -webkit-overflow-scrolling: touch (smooth iOS)
- overscroll-behavior: contain (pas de scroll-chain sur le body en dessous)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:05:45 +02:00
MVA Global Fret
72da4fcfd5 Contact: full gold border around price-reminder block
Avant: border-left 4px (juste à gauche).
Maintenant: border 2px tout autour pour bien encadrer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:03:44 +02:00
MVA Global Fret
3b1a585444 Contact: fix price-reminder spacing (--space-xxl variable did not exist)
J'utilisais --space-xxl qui n'existe pas dans le système (les vraies
variables sont --space-2xl/3xl). Du coup margin-top tombait à 0 et le
bloc collait au formulaire.

Corrigé : margin-top=96px (--space-3xl) + max-width réduit à 680px
pour un meilleur centrage visuel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:01:08 +02:00
MVA Global Fret
baa71149b2 Contact: center the price reminder block below the 2-column grid
Avant : le bloc 'Rappel tarifaire' était collé en bas de la colonne droite,
créant un déséquilibre visuel avec le formulaire plus court à gauche.

Maintenant : sorti de la grille, centré avec max-width 720px sous les
deux colonnes, en grille 2 colonnes sur ses items pour une lecture
plus dense et équilibrée.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:57:39 +02:00
MVA Global Fret
a8bf913e68 Service Commande: clean up step cards (remove tiny icons, add proper grid)
- Retirer les icônes <i> minuscules à côté de chaque étape
- Ajouter le style CSS .steps-grid (grille 3 colonnes desktop, stack mobile)
- Ajouter le style .step-card (carte blanche, ombre, hover lift)
- Badge numéroté agrandi (64px) avec drop-shadow doré
- Ligne dorée discrète qui relie les 3 étapes en desktop

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:54:57 +02:00
MVA Global Fret
12a21852e3 Add Actions-based Pages deploy workflow
Le mode legacy était bloqué en errored. On passe au build via Actions
(actions/deploy-pages@v4) — plus fiable et debug-able.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 17:05:13 +02:00
MVA Global Fret
df6e03c0dc chore: re-trigger build now that verified-commits is disabled 2026-05-05 16:43:52 +02:00
MVA Global Fret
d22bb03d9c chore: trigger Pages redeploy for tiered delivery pricing 2026-05-05 16:25:23 +02:00
MVA Global Fret
94b168850b Tier home-delivery pricing in Antananarivo by parcel weight
Antananarivo home delivery is no longer a flat 6,000 Ar:
- 6,000 Ar  ≤ 5 kg
- 10,000 Ar ≤ 10 kg
- 20,000 Ar ≤ 20 kg
- > 20 kg: contact us

tarifs.html: replace the single price line with a 3-row tiered list
inside the Antananarivo card, keep the headline "À partir de 6 000 Ar"
on top, and turn the green "home delivery available" note into an
amber "over 20 kg: contact us" info banner.

translations.js: add delivery1Tier1/2/3 keys for FR/EN/MG, refresh
delivery1Title (now mentions "à domicile"), delivery1Price (now "À
partir de…"), delivery1Desc, delivery1Note. Province card unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 16:02:31 +02:00
MVA Global Fret
d79628ace3 Revert "Tarifs: grille de livraison Antananarivo par palier de poids"
This reverts commit aec3cdd0b8.
2026-05-05 15:41:49 +02:00
MVA Global Fret
f67fa16e09 chore: trigger Pages redeploy 2026-05-05 15:33:21 +02:00
MVA Global Fret
aec3cdd0b8 Tarifs: grille de livraison Antananarivo par palier de poids
Remplace le prix unique de 6 000 Ar par 3 paliers (5/10/20 kg) plus
une ligne « Nous contacter » pour les colis plus lourds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 15:21:37 +02:00
MVA Global Fret
1c44730d08 Mobile: faster cruise — 16 s → 10 s per traversal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 14:48:46 +02:00
MVA Global Fret
80957e804b Mobile: bump autonomous plane speed from 28 s → 16 s per traversal
Mobile screens are smaller and users glance at them for less time,
so the 28-second full-traversal on desktop felt sluggish there.
BASE_SPEED is now 1/16 on mobile vs 1/28 on desktop. The touch
boost (×6) still stacks on top, giving a sub-3-second sprint when
the user holds a finger down.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 14:46:23 +02:00
MVA Global Fret
fd0acea058 Mobile: lighter render path + touch-to-accelerate the plane
Detects mobile via UA + 768 px media query (IS_MOBILE constant) and
flips three knobs:

- Renderer: antialias off, pixelRatio capped at 1.5 (vs 2 on desktop)
- Parcels: spawn every 2.4 s (vs 1.4 s on desktop) to keep clone+
  draw cost down

Adds touch handlers so users without a mouse can speed the plane up:

- touchstart bumps `touchBoost` from 1 to 6 → BASE_SPEED is multiplied
  by 6 each tick while a finger is on the screen
- touchmove also feeds Math.hypot(dx, dy) * MOUSE_BOOST into
  targetProgress, so swiping advances the plane the same way mouse
  motion does on desktop
- touchend / touchcancel reset touchBoost to 1 and clear last-touch
  coordinates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 14:41:34 +02:00
MVA Global Fret
0b2fe83963 Expose translations on window so the inline lang switcher can find it
translations.js declared the dict as `const translations = {...}`,
which scopes it to the script but does NOT attach it to window. The
inline applyLang() in index.html reads `window.translations?.[l]` and
was always getting undefined → early-return → no DOM updates → the
button text stayed in French regardless of the lang switcher.

One-line fix: append `window.translations = translations;` so
classic-script inline code can pick it up. All page-level i18n keys
(intro.ctaBtn included) start translating again.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 14:18:14 +02:00
MVA Global Fret
c4328da11d Slow the CTA emerge tween — 1.2 s → 2.2 s, opacity 0.5 s → 0.9 s
User wanted a more deliberate descent. Bumps the transform transition
from 1.2 s to 2.2 s and the opacity transition from 0.5 s to 0.9 s.
Easing kept (slight overshoot spring). Halo delay (1 s) unchanged so
it kicks in mid-emerge while the button is still settling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 14:11:28 +02:00
MVA Global Fret
22c57e5b41 CTA emerges from the plane when it reaches center
Hide the gold pill button at page load (opacity 0, scale 0.05,
translated 27 vh up — roughly where the plane is when it crosses
the viewport center). When the plane's progress reaches 0.5, the
tick loop adds a `.revealed` class to .cta-btn; CSS variables flip
and a 1.2 s spring transition lands the button at viewport center
at full size.

Pulse halo (::after) is dormant until the .revealed class lands,
so it doesn't waste cycles on a hidden element. Hover scale (1.04)
re-introduced on `.cta-btn.revealed:hover` with the original 0.32 s
transition so it doesn't fight the slow emerge tween.

Plane and parcel logic untouched.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 14:07:48 +02:00
MVA Global Fret
4b622a7d85 Drop parcel spawn point lower under the fuselage
Parcels were appearing right at the plane's center line, looking like
they came out of a window. Move the spawn offset from y-0.4 to y-1.1
so they emerge from below the belly of the airliner, where a real
cargo bay would be.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 13:57:28 +02:00
MVA Global Fret
cf7f84f354 Drop cargo back below the harness, just touching the parachute base
Previous +0.55·height pushed the cargo up into the canopy itself —
user's annotation showed the cargo belongs immediately under the
strings' end, not inside the parachute. With the wrapper fix from
the previous commit, parachuteBottom now correctly points at the
harness end, so cargo center = parachuteBottom - cargoH/2 + 0.06
puts the cargo's top right at the harness with a tiny overlap.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 13:50:55 +02:00
MVA Global Fret
71d2f06920 Move cargo up into the canopy base, not below the strings
User's annotation showed the cargo belongs right at the canopy's
base — where the strings converge — not at the very bottom of the
strings. New position: parachuteBottom + 0.55·parachuteHeight,
i.e. about 55% up the parachute's vertical extent. Cargo also
shrunk to 0.36×0.30×0.36 to better match the small rectangle the
user drew.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 13:46:38 +02:00
MVA Global Fret
25ea448abe Make cargo a child of the parachute, drop the rogue strap meshes
The black rectangles the user circled in the screenshot were the
strap meshes (strapH/strapV, 0.04-thick black boxes) — they were
meant to wrap the cardboard box but ended up rendering as detached
rectangles in some viewing angles. They're gone.

Cargo positioning is now computed from the actual scaled parachute
bbox: parachuteBottom = -(size.y · baseScale)/2, cargo center sits
just inside that line for a slight overlap so the parachute strings
visually terminate on the box top. The cargo is now a child of the
parachute mesh (para.add(cargo)), so any transform applied to the
parachute — scale, rotation, position — carries the box along with
it. To keep the visible box size consistent regardless of the
parachute's baseScale, the cargo's local position and scale are
divided by baseScale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 13:39:58 +02:00
MVA Global Fret
6b0f8d9afb Switch parcel sprites to a real 3D parachute GLB with bound cargo
Drops the parcel.png sprite (which baked the parachute and the box
into a single image) for a real 3D model: assets/parachute.glb is
the CC-BY 3.0 « Parachute » by Poly by Google, decompressed from
poly.pizza's static.poly.pizza CDN.

The parcel template now stacks four meshes inside one Group so they
move as a unit:
- the loaded parachute (centered + scaled to ~1.6 world units max)
- a 0.46×0.36×0.46 brown box with metal-low MeshStandardMaterial,
  positioned at y = -0.96 — right below the parachute's harness
  point — so the model's strings appear to terminate on it
- two thin black straps wrapping the box (0.04-thick boxes, one
  horizontal one vertical) for visual reinforcement that the cargo
  is tied down

spawnParcel clones the template (deep), per-instance clones every
material so opacity can be modulated independently per parcel, and
adds a slight pendulum sway + slow Y spin. Falling/scale-up/fade
logic adapted from the sprite version. Cleanup disposes the cloned
materials when a parcel exits.

HTML credit comment extended to attribute both the airplane and the
new parachute under CC-BY 3.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 13:32:48 +02:00
MVA Global Fret
3fc8f26c2e Clean up scattered noise around the parcel sprite
Original Gemini export had small dim artifacts dotted around the
parachute (visible against the transparent backdrop in the rendered
sprite). Re-process: crop the canvas tightly around the subject
(720×768 starting at x=344), then run a geq pass that knocks any
pixel with alpha < 40 down to alpha 0. The main subject keeps its
clean alpha; the faint speckles disappear.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 13:01:40 +02:00
MVA Global Fret
3f773380c4 Drop parachuted parcels behind the plane as it crosses
Adds a sprite-based parcel system. The plane spawns a new
parcel-on-parachute (assets/parcel.png, 236 KB transparent) every
~1.4 s while it's visible (progress 0.04-0.92). Each parcel:

- Spawns at the plane's current position with a tiny random offset.
- Falls at 1.4 world units/s, with a small horizontal drift and a
  parachute sway sinusoid for character.
- Scales 0.35 → 2.6 over its lifetime, simulating the perspective
  of falling toward the camera.
- Fades in over 6% of life, fades out over the last 15%.
- Cleaned up (removed from scene + material disposed) when its
  lifetime expires or it drops below y = -10.

Implemented as THREE.Sprite so it always faces the camera, no need
to track per-parcel orientation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 12:46:27 +02:00
MVA Global Fret
6fcc772bf0 Match trajectory to user-drawn line: high cruise, gentle climb
User sketched a near-horizontal red line in the upper third of the
viewport, slightly higher on the left than on the right. Mapping
that to world coords with the camera at z=22 and 40° vfov:
- right end (entrance): y ≈ +3.5
- left end (exit):      y ≈ +5

So the plane keeps cruising in the upper portion of the frame and
climbs ~1.5 world units across 40 horizontal units — about 2° of
slope. Pitch follows: rotation.z = -0.06 - p·0.01 (3.5–4° nose-up,
matching the slope). Roll softened to 0.04 ± 0.02 since the path is
nearly straight.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 12:39:42 +02:00
MVA Global Fret
6cf619857a Switch trajectory to descending right→top to left→bottom, nose down
Reroute the plane: it now enters from the upper-right (y=+6) and
exits at the lower-left (y=-10), so the path slopes downward as a
~22° descent (slope = -16/40 in world units). Pitch reversed to
match — rotation.z = +0.32 + p·0.05 (≈18-21° nose-down) so the
plane's body aligns with the descent line. Roll and yaw unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 12:35:03 +02:00
MVA Global Fret
7cebdf86ed Fix: rotation.z is the actual pitch axis, not rotation.x
The labels in the previous commit were swapped. With the wrapper
rotated -π/2 around Y so the nose points -X, the plane's longitudinal
axis is world X (so rotation.x is roll) and its lateral axis is
world Z (so rotation.z is pitch). Earlier code applied "roll"
(positive 0.18) to .z, which was actually pitching the nose down —
no amount of tweaking rotation.x could compensate, hence the user
seeing the plane go forward+down even after sign flips.

Now:
- rotation.z = -0.30 - p·0.05  (nose up ~17–20°, climb attitude)
- rotation.x = 0.12 + small variation  (subtle roll)
- rotation.y = 0  (no yaw, plane already heading the right way)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 12:31:51 +02:00
MVA Global Fret
710551082c Flip pitch sign — turns out positive rotation.x was nose-down
After the wrapper's -π/2 yaw, applying positive rotation.x to the
planeHolder rotates the plane around world X with the nose dropping,
not lifting (visible in the user-supplied screenshot). Flipping to
-0.18 (≈10°) puts the nose where the trajectory says it should go.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 12:29:12 +02:00