|
4 | 4 | --dur-develop (2.4 s), matching the ~3 s the hub takes to draft the six words. |
5 | 5 | state: blank | developing | developed. Reduced motion shows the final print at once. |
6 | 6 | `scene` is a short description used as the image's accessible name (SRS-ACC-5). |
| 7 | + Photo-fit: a real photo keeps its own shape (height is then the most it may take, never a |
| 8 | + crop), a dark one is brightened for display only, and a tap opens it full screen. |
7 | 9 | Motion (docs/design/motion.md 3.4): only opacity animates (animated blur is a known |
8 | 10 | slow path in iOS Safari). A white haze lifts first, the sepia veil clears from the |
9 | 11 | middle, and the edges clear last, like a chemical print. No flash. |
10 | 12 | --> |
11 | 13 | <script lang="ts"> |
12 | 14 | import { t } from "#lib/copy/index.ts"; |
13 | 15 | import SampleScene from "./SampleScene.svelte"; |
| 16 | + import { lit, zoom } from "#lib/photo-fit.ts"; |
14 | 17 | let { |
15 | 18 | state = "developed", |
16 | 19 | scene = "", |
|
29 | 32 | } = $props(); |
30 | 33 | </script> |
31 | 34 |
|
32 | | -<figure class="print {state}" style:--tilt="{tilt}deg" style:height="{height}px"> |
| 35 | +{#snippet layers()} |
| 36 | + {#if state === "developing"} |
| 37 | + <span class="veil" aria-hidden="true"></span> |
| 38 | + <span class="edge" aria-hidden="true"></span> |
| 39 | + <span class="haze" aria-hidden="true"></span> |
| 40 | + {/if} |
| 41 | +{/snippet} |
| 42 | + |
| 43 | +<figure class="print {state}" class:natural={!!src} style:--tilt="{tilt}deg" style:--max="{height}px" style:height={src ? undefined : `${height}px`}> |
33 | 44 | <div class="paper grain"> |
| 45 | + {#if src} |
| 46 | + <button type="button" class="image photo-art" aria-label={scene || t("invite.photoFallback")} onclick={zoom}> |
| 47 | + <img {src} alt="" onload={lit} /> |
| 48 | + {@render layers()} |
| 49 | + </button> |
| 50 | + {:else} |
34 | 51 | <div class="image photo-art" role="img" aria-label={scene || t("invite.photoFallback")}> |
35 | | - {#if src} |
36 | | - <img {src} alt="" /> |
37 | | - {:else if sample} |
| 52 | + {#if sample} |
38 | 53 | <SampleScene /> |
39 | 54 | {/if} |
40 | | - {#if state === "developing"} |
41 | | - <span class="veil" aria-hidden="true"></span> |
42 | | - <span class="edge" aria-hidden="true"></span> |
43 | | - <span class="haze" aria-hidden="true"></span> |
44 | | - {/if} |
| 55 | +{@render layers()} |
45 | 56 | </div> |
| 57 | + {/if} |
46 | 58 | </div> |
47 | 59 | </figure> |
48 | 60 |
|
|
51 | 63 | .paper { height: 100%; padding: 12px 12px 44px; background: var(--c-birch-50); border-radius: 6px; box-shadow: var(--e-3); } |
52 | 64 | .image { position: relative; width: 100%; height: 100%; overflow: hidden; border-radius: 3px; background: #efe6d4; } |
53 | 65 | .image :global(img), .image :global(svg) { width: 100%; height: 100%; object-fit: cover; } |
| 66 | + /* Photo-fit: the print wraps the photo at its own shape, at most --max tall (44% of the screen |
| 67 | + on short phones), so the main button still fits on a 375 x 667 screen. */ |
| 68 | + .natural { width: fit-content; max-width: 100%; margin: 0 auto; } |
| 69 | + .natural .paper, .natural .image { height: auto; } |
| 70 | + button.image { display: block; padding: 0; border: 0; cursor: zoom-in; color: inherit; } |
| 71 | + .natural .image :global(img) { display: block; width: auto; height: auto; max-width: 100%; max-height: calc(min(var(--max), 44dvh) - 56px); object-fit: initial; } |
54 | 72 | .blank .image > :global(*) { opacity: 0; } |
55 | 73 | .developed .image > :global(*) { opacity: 1; } |
56 | 74 | .haze, .veil, .edge { position: absolute; inset: 0; pointer-events: none; } |
|
0 commit comments