You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/web/routes/demos/paloma2.svelte

688 lines
23 KiB

<script lang="ts">
type ContactPayload = {
email: string;
text: string;
};
type Status =
| 'editing'
| 'folding'
| 'ready'
| 'dragging'
| 'delivering'
| 'flying'
| 'sent';
type Props = {
endpoint?: string;
onSent?: (payload: ContactPayload) => void | Promise<void>;
};
let { endpoint = '/api/contact', onSent }: Props = $props();
let form = $state<ContactPayload>({
email: '',
text: ''
});
let status = $state<Status>('editing');
let error = $state('');
let stageEl: HTMLElement;
let formCardEl: HTMLElement;
let letterEl: HTMLElement;
let pigeonBeakEl: SVGElement;
let pigeonDropZoneEl: SVGRectElement;
const letterSize = { width: 140, height: 90 };
let letter = $state({
x: 0,
y: 0,
homeX: 0,
homeY: 0,
dragStartX: 0,
dragStartY: 0,
pointerStartX: 0,
pointerStartY: 0,
isCaptured: false
});
let canSubmit = $derived(
status === 'editing' &&
form.email.trim().length > 0 &&
form.text.trim().length > 0
);
let showForm = $derived(status === 'editing' || status === 'folding');
let showLetter = $derived(
status === 'ready' ||
status === 'dragging' ||
status === 'delivering' ||
status === 'flying'
);
let isNearPigeon = $state(false);
let instruction = $derived.by(() => {
if (status === 'editing') return 'Rellena el formulario.';
if (status === 'folding') return 'Preparando el mensaje...';
if (status === 'ready') return 'Arrastra la carta hacia el pico de la paloma.';
if (status === 'dragging') return isNearPigeon ? '¡Suéltala ahí! La paloma la recogerá.' : 'Acerca la carta a la paloma.';
if (status === 'delivering') return '¡La tiene!';
if (status === 'flying') return 'La paloma vuela hacia el horizonte...';
return 'Mensaje enviado correctamente.';
});
function sleep(ms: number) {
return new Promise((resolve) => window.setTimeout(resolve, ms));
}
function startChallenge(event: SubmitEvent) {
event.preventDefault();
if (!canSubmit) {
error = 'Introduce un email y un mensaje antes de enviar.';
return;
}
error = '';
status = 'folding';
window.setTimeout(() => {
placeLetterOverForm();
status = 'ready';
}, 800);
}
function placeLetterOverForm() {
const stageRect = stageEl.getBoundingClientRect();
const formRect = formCardEl.getBoundingClientRect();
letter.x = formRect.left - stageRect.left + formRect.width / 2 - letterSize.width / 2;
letter.y = formRect.top - stageRect.top + formRect.height / 2 - letterSize.height / 2;
letter.homeX = letter.x;
letter.homeY = letter.y;
}
function grabLetter(event: PointerEvent) {
if (status !== 'ready') return;
const target = event.currentTarget as HTMLElement;
target.setPointerCapture(event.pointerId);
letter.isCaptured = true;
letter.dragStartX = letter.x;
letter.dragStartY = letter.y;
letter.pointerStartX = event.clientX;
letter.pointerStartY = event.clientY;
error = '';
status = 'dragging';
}
function moveLetter(event: PointerEvent) {
if (status !== 'dragging' || !letter.isCaptured) return;
letter.x = letter.dragStartX + event.clientX - letter.pointerStartX;
letter.y = letter.dragStartY + event.clientY - letter.pointerStartY;
isNearPigeon = checkProximity();
}
function checkProximity() {
if (!pigeonDropZoneEl || !letterEl) return false;
const dropRect = pigeonDropZoneEl.getBoundingClientRect();
const letterRect = letterEl.getBoundingClientRect();
const letterCenterX = letterRect.left + letterRect.width / 2;
const letterCenterY = letterRect.top + letterRect.height / 2;
return (
letterCenterX >= dropRect.left - 40 &&
letterCenterX <= dropRect.right + 40 &&
letterCenterY >= dropRect.top - 40 &&
letterCenterY <= dropRect.bottom + 40
);
}
async function releaseLetter(event: PointerEvent) {
if (status !== 'dragging') return;
const target = event.currentTarget as HTMLElement;
if (target.hasPointerCapture(event.pointerId)) {
target.releasePointerCapture(event.pointerId);
}
letter.isCaptured = false;
if (!checkProximity()) {
error = 'Lleva la carta hasta el pico de la paloma.';
status = 'ready';
isNearPigeon = false;
letter.x = letter.homeX;
letter.y = letter.homeY;
return;
}
await deliverToPigeon();
}
async function deliverToPigeon() {
error = '';
status = 'delivering';
const stageRect = stageEl.getBoundingClientRect();
const beakRect = pigeonBeakEl.getBoundingClientRect();
letter.x = beakRect.left - stageRect.left - 20;
letter.y = beakRect.top - stageRect.top - 20;
await sleep(800);
status = 'flying';
sendForm();
}
async function sendForm() {
const payload: ContactPayload = {
email: form.email.trim(),
text: form.text.trim()
};
try {
if (onSent) {
await onSent(payload);
} else {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error();
}
await sleep(1500);
status = 'sent';
} catch {
status = 'ready';
isNearPigeon = false;
letter.x = letter.homeX;
letter.y = letter.homeY;
error = 'La paloma se perdió en la tormenta. Inténtalo de nuevo.';
}
}
function resetForm() {
form.email = '';
form.text = '';
error = '';
status = 'editing';
isNearPigeon = false;
}
</script>
<section bind:this={stageEl} class="contact-stage">
<div class="floating-hint">
<p>{instruction}</p>
{#if error} <span role="alert" class="err-text">{error}</span> {/if}
</div>
<div class="left-panel">
{#if showForm}
<div bind:this={formCardEl} class={`form-card ${status === 'folding' ? 'is-folding' : ''}`}>
<form onsubmit={startChallenge}>
<div class="eyebrow">Mensajería Alada</div>
<h2>Enviar Correo</h2>
<label>
<span>Tu Email</span>
<input type="email" bind:value={form.email} disabled={status !== 'editing'} required placeholder="tu@email.com" />
</label>
<label>
<span>Mensaje</span>
<textarea bind:value={form.text} disabled={status !== 'editing'} required placeholder="Escribe algo bonito..." rows="4"></textarea>
</label>
<button type="submit" disabled={!canSubmit}>Sellar carta</button>
</form>
</div>
{/if}
{#if status === 'sent'}
<div class="success-card">
<div class="success-icon">🕊️</div>
<h3>¡Buen viaje!</h3>
<p>La paloma se ha llevado tu mensaje más allá del horizonte.</p>
<button type="button" onclick={resetForm}>Enviar otra paloma</button>
</div>
{/if}
</div>
<div class="right-panel">
<div class={`pigeon-container state-${status} ${isNearPigeon ? 'is-hungry' : ''}`}>
<svg class="pigeon-svg" viewBox="0 0 400 400" width="100%" height="100%">
<defs>
<radialGradient id="skyGrad" cx="0.5" cy="0.5" r="0.5">
<stop offset="0%" stop-color="#e0efff" stop-opacity="0.8" />
<stop offset="100%" stop-color="#ffffff" stop-opacity="0" />
</radialGradient>
<linearGradient id="pigeonBody" x1="0.3" x2="0.7" y1="0" y2="1">
<stop offset="0%" stop-color="#e5e7eb" />
<stop offset="55%" stop-color="#9ca3af" />
<stop offset="100%" stop-color="#4b5563" />
</linearGradient>
<linearGradient id="pigeonBelly" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#f3f4f6" />
<stop offset="100%" stop-color="#d1d5db" />
</linearGradient>
<linearGradient id="pigeonWing" x1="0" x2="0.4" y1="0" y2="1">
<stop offset="0%" stop-color="#9ca3af" />
<stop offset="55%" stop-color="#4b5563" />
<stop offset="100%" stop-color="#1f2937" />
</linearGradient>
<linearGradient id="neckShimmer" x1="0" x2="1" y1="0" y2="1">
<stop offset="0%" stop-color="#14b8a6" />
<stop offset="50%" stop-color="#8b5cf6" />
<stop offset="100%" stop-color="#ec4899" />
</linearGradient>
<linearGradient id="capGrad" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#ef4444" />
<stop offset="100%" stop-color="#991b1b" />
</linearGradient>
<linearGradient id="bagGrad" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#b45309" />
<stop offset="100%" stop-color="#78350f" />
</linearGradient>
<linearGradient id="bagFlapGrad" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#a16207" />
<stop offset="100%" stop-color="#713f12" />
</linearGradient>
<filter id="softShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="6" stdDeviation="6" flood-color="#1e293b" flood-opacity="0.18" />
</filter>
</defs>
<!-- Aura del cielo -->
<circle cx="200" cy="200" r="170" fill="url(#skyGrad)" />
<!-- Nubes decorativas -->
<g opacity="0.5">
<ellipse cx="60" cy="90" rx="22" ry="7" fill="#ffffff" />
<ellipse cx="75" cy="85" rx="16" ry="6" fill="#ffffff" />
<ellipse cx="340" cy="70" rx="20" ry="6" fill="#ffffff" />
<ellipse cx="355" cy="65" rx="14" ry="5" fill="#ffffff" />
</g>
<!-- Rama (estática, no vuela) -->
<g class="branch" filter="url(#softShadow)">
<path d="M 30 322 C 130 308, 280 326, 370 296"
fill="none" stroke="#5c4033" stroke-width="14" stroke-linecap="round" />
<path d="M 30 322 C 130 308, 280 326, 370 296"
fill="none" stroke="#7c5535" stroke-width="3" stroke-linecap="round" opacity="0.6" />
<path d="M 295 312 C 315 295, 330 275, 340 248"
fill="none" stroke="#5c4033" stroke-width="6" stroke-linecap="round" />
<!-- Hojas -->
<g>
<path d="M 338 250 Q 354 238, 358 250 Q 348 266, 338 250 Z" fill="#22c55e" />
<path d="M 348 248 L 348 252" stroke="#15803d" stroke-width="0.8" />
<path d="M 330 240 Q 320 232, 324 226 Q 336 234, 330 240 Z" fill="#16a34a" />
<path d="M 100 314 Q 90 300, 96 294 Q 110 304, 100 314 Z" fill="#22c55e" />
<path d="M 220 322 Q 212 308, 218 302 Q 230 314, 220 322 Z" fill="#4ade80" />
</g>
</g>
<!-- ACTOR PALOMA: todo lo que despega cuando vuela -->
<g class="pigeon-actor" filter="url(#softShadow)">
<!-- COLA: plumas detrás del cuerpo -->
<g class="pigeon-tail">
<path d="M 110 230 L 72 245 L 80 258 L 118 248 Z" fill="#4b5563" />
<path d="M 108 242 L 65 268 L 82 280 L 118 260 Z" fill="#6b7280" />
<path d="M 112 254 L 75 288 L 100 295 L 122 272 Z" fill="#9ca3af" />
<!-- línea central de cola -->
<line x1="90" y1="252" x2="115" y2="252" stroke="#374151" stroke-width="0.6" opacity="0.5" />
<line x1="88" y1="270" x2="115" y2="265" stroke="#374151" stroke-width="0.6" opacity="0.5" />
</g>
<!-- ALA TRASERA: solo visible al volar -->
<path class="pigeon-wing-back"
d="M 175 200 C 145 130, 100 130, 125 200 C 145 225, 175 220, 175 200 Z"
fill="#374151" opacity="0.75" />
<!-- PATAS sobre la rama -->
<g class="pigeon-legs" stroke="#fb7185" stroke-width="4" stroke-linecap="round" fill="none">
<!-- pata izquierda -->
<path d="M 175 292 L 172 317" />
<path d="M 172 317 L 164 320 M 172 317 L 180 320 M 172 317 L 172 323" stroke-width="2.5" />
<!-- pata derecha -->
<path d="M 200 292 L 204 317" />
<path d="M 204 317 L 196 320 M 204 317 L 212 320 M 204 317 L 204 323" stroke-width="2.5" />
</g>
<!-- CUERPO principal (regordete) -->
<path d="M 250 195
C 285 205, 290 252, 270 285
C 250 305, 175 308, 138 290
C 108 270, 105 222, 130 198
C 160 176, 225 178, 250 195 Z"
fill="url(#pigeonBody)" />
<!-- Vientre (luz inferior) -->
<ellipse cx="195" cy="268" rx="58" ry="28" fill="url(#pigeonBelly)" opacity="0.85" />
<!-- Brillo iridiscente del cuello (típico de paloma) -->
<path d="M 222 175 C 218 200, 240 218, 265 207 C 270 192, 263 175, 245 168 Z"
fill="url(#neckShimmer)" opacity="0.55" />
<path d="M 235 178 C 230 195, 248 208, 262 200" fill="none" stroke="#ffffff" stroke-width="1" opacity="0.4" />
<!-- CORREA de la cartera (diagonal cruzando el pecho) -->
<path d="M 250 180 Q 222 230, 168 268"
fill="none" stroke="#78350f" stroke-width="5" stroke-linecap="round" />
<path d="M 250 180 Q 222 230, 168 268"
fill="none" stroke="#a16207" stroke-width="1.5" stroke-linecap="round" opacity="0.6" />
<!-- CARTERA (bolsa de mensajero) -->
<g class="messenger-bag">
<!-- cuerpo de la cartera -->
<path d="M 138 252
L 198 252
L 196 295
Q 193 304, 184 304
L 152 304
Q 141 304, 139 295 Z"
fill="url(#bagGrad)" stroke="#451a03" stroke-width="0.8" />
<!-- solapa de la cartera -->
<path d="M 135 248
L 201 248
L 199 274
Q 168 282, 137 273 Z"
fill="url(#bagFlapGrad)" stroke="#451a03" stroke-width="0.8" />
<!-- costuras de la solapa -->
<path d="M 138 252 Q 168 258, 198 252" fill="none" stroke="#451a03" stroke-width="0.5" stroke-dasharray="2 2" opacity="0.6" />
<!-- hebilla -->
<rect x="163" y="268" width="11" height="7" rx="1.5" fill="#fbbf24" stroke="#92400e" stroke-width="0.6" />
<rect x="165" y="270" width="7" height="3" rx="0.5" fill="#92400e" />
<!-- emblema postal en la cartera -->
<circle cx="168" cy="290" r="8" fill="#fef3c7" stroke="#92400e" stroke-width="1" />
<path d="M 163 288 L 168 292 L 173 288 M 163 290 L 173 290" fill="none" stroke="#92400e" stroke-width="0.9" stroke-linecap="round" />
<!-- carta asomando -->
<g transform="rotate(-10 188 245)">
<rect x="178" y="238" width="22" height="14" rx="1" fill="#fefcf8" stroke="#d6cfc0" stroke-width="0.8" />
<line x1="181" y1="244" x2="195" y2="244" stroke="#cbd5e1" stroke-width="0.6" />
<line x1="181" y1="248" x2="192" y2="248" stroke="#cbd5e1" stroke-width="0.6" />
</g>
</g>
<!-- CABEZA -->
<circle cx="252" cy="148" r="36" fill="url(#pigeonBody)" />
<!-- Brillo de la cabeza -->
<ellipse cx="246" cy="134" rx="20" ry="12" fill="#f3f4f6" opacity="0.45" />
<!-- OJO expresivo -->
<circle cx="272" cy="143" r="5.5" fill="#1f2937" />
<circle cx="270.5" cy="141" r="2" fill="#ffffff" />
<circle cx="274" cy="145.5" r="0.8" fill="#ffffff" opacity="0.7" />
<!-- PICO con cere (la parte blanca característica de paloma) -->
<g class="pigeon-beak" bind:this={pigeonBeakEl}>
<!-- cere (base blanca del pico) -->
<ellipse cx="287" cy="148" rx="7" ry="4.5" fill="#f9fafb" stroke="#a8a29e" stroke-width="0.8" />
<circle cx="284" cy="147" r="0.8" fill="#78716c" />
<circle cx="290" cy="147" r="0.8" fill="#78716c" />
<!-- pico superior -->
<path class="beak-top"
d="M 286 146 L 311 150 L 307 155 L 286 153 Z"
fill="#fbbf24" stroke="#b45309" stroke-width="1" stroke-linejoin="round" />
<!-- pico inferior -->
<path class="beak-bottom"
d="M 286 154 L 307 156 L 304 159 L 286 159 Z"
fill="#f59e0b" stroke="#b45309" stroke-width="1" stroke-linejoin="round" />
</g>
<!-- GORRA de mensajero -->
<g class="messenger-cap">
<!-- sombra de la gorra sobre la cabeza -->
<ellipse cx="252" cy="120" rx="32" ry="3.5" fill="#1f2937" opacity="0.18" />
<!-- domo principal -->
<path d="M 222 119
Q 220 92, 252 89
Q 284 92, 282 119
Q 282 122, 278 122
L 226 122
Q 222 122, 222 119 Z"
fill="url(#capGrad)" stroke="#7f1d1d" stroke-width="1" stroke-linejoin="round" />
<!-- banda inferior de la gorra -->
<path d="M 222 118 Q 252 124, 282 118 L 282 122 Q 252 126, 222 122 Z"
fill="#7f1d1d" />
<!-- pequeño brillo en el domo -->
<path d="M 232 100 Q 240 94, 252 94" fill="none" stroke="#fca5a5" stroke-width="2" stroke-linecap="round" opacity="0.55" />
<!-- botón superior -->
<circle cx="252" cy="91" r="3" fill="#fbbf24" stroke="#92400e" stroke-width="0.6" />
<!-- visera (hacia el pico) -->
<path d="M 278 121
Q 297 121, 301 130
Q 298 134, 282 132
Q 278 130, 278 121 Z"
fill="#7f1d1d" stroke="#450a0a" stroke-width="0.8" stroke-linejoin="round" />
<!-- emblema postal en el frente -->
<circle cx="266" cy="111" r="5" fill="#fef3c7" stroke="#7f1d1d" stroke-width="1" />
<path d="M 263 110 L 266 113 L 269 110 M 263 112 L 269 112"
fill="none" stroke="#7f1d1d" stroke-width="0.9" stroke-linecap="round" />
</g>
<!-- ALA FRONTAL (con animación de aleteo) -->
<g class="pigeon-wing">
<path d="M 175 200
C 145 195, 128 245, 162 273
C 188 282, 218 270, 222 244
C 224 218, 200 204, 175 200 Z"
fill="url(#pigeonWing)" stroke="#1f2937" stroke-width="0.5" />
<!-- líneas de plumas -->
<path d="M 168 218 Q 176 240, 178 265" fill="none" stroke="#0f172a" stroke-width="1" opacity="0.45" stroke-linecap="round" />
<path d="M 184 215 Q 192 240, 192 268" fill="none" stroke="#0f172a" stroke-width="1" opacity="0.45" stroke-linecap="round" />
<path d="M 200 213 Q 207 238, 205 265" fill="none" stroke="#0f172a" stroke-width="1" opacity="0.45" stroke-linecap="round" />
<!-- borde superior del ala (covertas) -->
<path d="M 175 200 C 188 198, 205 205, 218 222" fill="none" stroke="#374151" stroke-width="1.5" opacity="0.6" stroke-linecap="round" />
</g>
<!-- Zona de drop invisible sobre el pico -->
<rect
bind:this={pigeonDropZoneEl}
x="262" y="118" width="62" height="58"
fill="transparent"
pointer-events="none"
/>
</g>
</svg>
</div>
</div>
<!-- CARTA arrastrable -->
{#if showLetter}
<div
bind:this={letterEl}
class={`letter-envelope status-${status}`}
style={`--x: ${letter.x}px; --y: ${letter.y}px;`}
onpointerdown={grabLetter}
onpointermove={moveLetter}
onpointerup={releaseLetter}
>
<svg width="140" height="90" viewBox="0 0 140 90" filter="url(#softShadow)">
<rect width="140" height="90" rx="6" fill="#fefcf8" stroke="#e6dfd1" stroke-width="1.5" />
<rect x="110" y="8" width="18" height="22" fill="#3b82f6" rx="1" />
<circle cx="119" cy="19" r="4" fill="#ffffff" opacity="0.3" />
<line x1="16" y1="50" x2="90" y2="50" stroke="#94a3b8" stroke-width="2" stroke-linecap="round" />
<line x1="16" y1="62" x2="80" y2="62" stroke="#94a3b8" stroke-width="2" stroke-linecap="round" />
<line x1="16" y1="74" x2="96" y2="74" stroke="#94a3b8" stroke-width="2" stroke-linecap="round" />
<path d="M 2 2 L 70 42 L 138 2" fill="none" stroke="#e2d8c5" stroke-width="1.5" />
<!-- Sello de cera rojo -->
<circle cx="70" cy="42" r="7" fill="#dc2626" stroke="#7f1d1d" stroke-width="0.8" />
<text x="70" y="46" text-anchor="middle" font-size="9" font-weight="bold" fill="#fef3c7" font-family="serif">M</text>
</svg>
</div>
{/if}
</section>
<style>
:global(*) { box-sizing: border-box; }
.contact-stage {
position: relative;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
min-height: 550px;
padding: 2rem;
background: linear-gradient(135deg, #f0f7ff 0%, #e2eeff 100%);
border-radius: 24px;
overflow: hidden;
align-items: center;
}
.floating-hint {
position: absolute;
top: 1.5rem;
left: 1.5rem;
z-index: 10;
background: rgba(255, 255, 255, 0.85);
padding: 0.75rem 1.2rem;
border-radius: 16px;
backdrop-filter: blur(8px);
box-shadow: 0 8px 24px rgba(0,0,0,0.05);
max-width: 340px;
}
.floating-hint p { margin: 0; font-weight: 700; color: #1e3a8a; }
.err-text { display: block; margin-top: 0.25rem; color: #ef4444; font-size: 0.88rem; }
.left-panel, .right-panel { position: relative; width: 100%; }
.form-card, .success-card {
background: #ffffff;
padding: 2rem;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 50, 150, 0.06);
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s ease;
}
.form-card.is-folding {
transform: scale(0.3) rotateX(45deg) translateY(100px);
opacity: 0;
pointer-events: none;
}
.eyebrow { font-size: 0.8rem; font-weight: 700; color: #3b82f6; text-transform: uppercase; margin-bottom: 0.5rem; }
h2 { margin: 0 0 1.5rem 0; color: #1e293b; font-size: 1.8rem; }
label { display: grid; gap: 0.4rem; margin-bottom: 1rem; font-weight: 600; color: #475569; }
input, textarea {
padding: 0.75rem 1rem;
border: 1.5px solid #cbd5e1;
border-radius: 12px;
outline: none;
font: inherit;
}
input:focus, textarea:focus { border-color: #3b82f6; box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15); }
button {
width: 100%;
padding: 0.85rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 12px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 10px 20px rgba(59, 130, 246, 0.2);
transition: transform 0.2s, background 0.2s;
}
button:hover:not(:disabled) { background: #2563eb; transform: translateY(-2px); }
button:disabled { opacity: 0.5; cursor: not-allowed; box-shadow: none; }
/* --- PALOMA --- */
.pigeon-container {
width: 100%;
height: 400px;
display: flex;
justify-content: center;
align-items: center;
}
.pigeon-actor {
transform-origin: 200px 220px;
transition: transform 0.3s ease;
}
/* Pico: rotación al abrir/cerrar */
.beak-top, .beak-bottom {
transform-origin: 286px 150px;
transition: transform 0.2s ease;
}
/* "Hambriento" -> pico abierto */
.is-hungry .beak-top { transform: rotate(-15deg); }
.is-hungry .beak-bottom { transform: rotate(15deg); }
/* Carta atrapada -> pico ligeramente apretado (BUG FIX: era descendant combinator) */
.state-delivering .beak-top,
.state-flying .beak-top { transform: rotate(-2deg); }
.state-delivering .beak-bottom,
.state-flying .beak-bottom { transform: rotate(2deg); }
/* Vuelo */
.state-flying .pigeon-actor {
animation: pigeonFlyAway 1.8s cubic-bezier(0.55, 0.055, 0.675, 0.19) forwards;
}
.state-flying .pigeon-wing {
animation: wingFlap 0.15s ease-in-out infinite alternate;
transform-origin: 200px 210px;
}
.state-flying .pigeon-wing-back {
animation: wingFlapBack 0.15s ease-in-out infinite alternate;
transform-origin: 175px 200px;
}
.state-flying .pigeon-legs {
transform: scaleY(0.2) translateY(-30px);
opacity: 0.5;
transition: transform 0.4s ease;
}
@keyframes pigeonFlyAway {
0% { transform: translate(0, 0) scale(1); }
20% { transform: translate(-10px, 15px) scale(0.98); }
100% { transform: translate(320px, -450px) scale(0.25); }
}
@keyframes wingFlap {
0% { transform: rotate(0deg) scaleY(1); }
100% { transform: rotate(-65deg) scaleY(0.4); }
}
@keyframes wingFlapBack {
0% { transform: rotate(0deg) scaleY(0.4); }
100% { transform: rotate(50deg) scaleY(1.2); }
}
/* --- CARTA --- */
.letter-envelope {
position: absolute;
z-index: 50;
left: 0;
top: 0;
width: 140px;
height: 90px;
transform: translate3d(var(--x), var(--y), 0);
touch-action: none;
cursor: grab;
will-change: transform;
}
.letter-envelope:active { cursor: grabbing; }
.status-delivering {
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
pointer-events: none;
}
.status-flying {
pointer-events: none;
animation: letterFlyAway 1.8s cubic-bezier(0.55, 0.055, 0.675, 0.19) forwards;
transform-origin: left center;
}
@keyframes letterFlyAway {
0% { transform: translate3d(var(--x), var(--y), 0) scale(1) rotate(0deg); }
20% { transform: translate3d(calc(var(--x) - 10px), calc(var(--y) + 15px), 0) scale(0.98); }
100% {
transform: translate3d(calc(var(--x) + 320px), calc(var(--y) - -30px - 450px), 0) scale(0.25) rotate(-15deg);
opacity: 0;
}
}
.success-card { text-align: center; }
.success-icon { font-size: 3rem; margin-bottom: 1rem; }
@media (max-width: 768px) {
.contact-stage { grid-template-columns: 1fr; }
.pigeon-container { height: 300px; }
}
</style>

Powered by TurnKey Linux.