mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-09 20:29:58 +02:00
chore(apple): add screenshot studio
This commit is contained in:
8
packaging/apple/studio/.gitignore
vendored
Normal file
8
packaging/apple/studio/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
generated/
|
||||
out-*.png
|
||||
assets/shots/**/*.png
|
||||
assets/mockup-*.png
|
||||
assets/mask-*.png
|
||||
assets/globe.png
|
||||
assets/ribbon.png
|
||||
!assets/README.md
|
||||
57
packaging/apple/studio/README.md
Normal file
57
packaging/apple/studio/README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# App Store screenshot studio
|
||||
|
||||
Code-driven, fully local App Store screenshots. **Typst** composes each frame
|
||||
(gradient background + blank device mockup + app screenshot + localized caption)
|
||||
and renders it to an exact-size PNG. Captions come from `strings.json` (9 locales);
|
||||
app screenshots come from the real app (see capture, step 3).
|
||||
|
||||
## Requirements
|
||||
- `typst` (0.15+) — installed.
|
||||
- `imagemagick` — only for the tilted hero shot's perspective warp (step 2). `brew install imagemagick`.
|
||||
|
||||
## Render
|
||||
```sh
|
||||
./build.sh # all locales × screens -> generated/<Language>/
|
||||
./build.sh --publish # -> ../<Language>/ (ships to App Store)
|
||||
LOCALES="fr de" SCREENS="share-securely" ./build.sh
|
||||
```
|
||||
Default writes to `generated/` (safe). `--publish` overwrites the real language folders.
|
||||
|
||||
Single frame while tuning:
|
||||
```sh
|
||||
typst compile screens.typ out.png --input locale=fr --input screen=share-securely --ppi 72
|
||||
```
|
||||
|
||||
## Files
|
||||
- `strings.json` — captions per locale (first-pass translations; review before shipping).
|
||||
- `screens.typ` — the composition + per-screen layout data (`screens` dict). Tweak numbers, re-run.
|
||||
- `build.sh` — loops locales × screens; maps locale→folder and screen→filename.
|
||||
- `assets/mockup-straight.png` — blank straight device (real alpha, black screen).
|
||||
- `assets/mockup-rotated.png` — blank tilted device (for the hero shot).
|
||||
- `assets/globe.png` — circle-clipped in Typst (the export has a baked checkerboard, not true alpha).
|
||||
- `assets/shots/<locale>/<screen>.png` — captured app screenshots (optional; placeholder if absent).
|
||||
|
||||
## How the screenshot gets into the phone
|
||||
The mockup's screen glass is a rectangle. `screens.typ` places the screenshot inset
|
||||
into that rectangle (`sx`/`sy`/`sr` fractions) with the bezel framing it. Measure those
|
||||
fractions once against `mockup-straight.png`.
|
||||
|
||||
## Full pipeline
|
||||
```sh
|
||||
./capture.sh # real localized app screens -> assets/shots/<locale>/<screen>.png
|
||||
./build.sh # composite everything -> generated/<Language>/
|
||||
./build.sh --publish # when happy -> ships to ../<Language>/
|
||||
```
|
||||
`capture.sh` drives the app into each screen via the DEBUG `-VniScreenshot` launch
|
||||
argument (see `apple/VniDrop/App/ScreenshotSupport.swift`), once per locale via
|
||||
`-AppleLanguages`, in dark mode with a 9:41 status bar. Screen ⇄ scenario map:
|
||||
`share-securely`→`share`, `choose-receivers`→`approval`, `send-anywhere`→`transfer-details`.
|
||||
|
||||
## Status
|
||||
1. ✅ Typst + JSON + straight-mockup compositing.
|
||||
2. ✅ Tilted hero (`send-anywhere`): `warp.sh` perspective-warps the screenshot onto the
|
||||
rotated mockup's screen quad (4 auto-detected corners); `build.sh` runs it automatically.
|
||||
3. ✅ Transparent globe layer (placed directly — use the alpha export, not a flattened one).
|
||||
4. ✅ Screenshot capture: `capture.sh` + the `#if DEBUG` fixture gateway, per locale, deterministic.
|
||||
5. ⬜ Ribbon art layer for `stay-private` (export as its own transparent PNG).
|
||||
6. ⬜ Layout polish: tune device positions/sizes in `screens.typ` against the originals.
|
||||
61
packaging/apple/studio/build.sh
Executable file
61
packaging/apple/studio/build.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Render all (locale x screen) App Store screenshots with Typst.
|
||||
#
|
||||
# ./build.sh # -> generated/<Language>/<Name>.png (safe default)
|
||||
# ./build.sh --publish # -> ../<Language>/<Name>.png (ships to App Store)
|
||||
# LOCALES="fr de" SCREENS="share-securely" ./build.sh # subset
|
||||
#
|
||||
# Screenshots are read from assets/shots/<locale>/<screen>.png when present
|
||||
# (pass nothing and you get the "screenshot here" placeholder).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
PUBLISH=""; [[ "${1:-}" == "--publish" ]] && PUBLISH=1
|
||||
|
||||
LOCALES=${LOCALES:-"en fr de es it nl pl pt ru"}
|
||||
SCREENS=${SCREENS:-"choose-receivers send-anywhere share-securely stay-private"}
|
||||
|
||||
# locale code -> output folder name (matches existing packaging/apple/<Language>/)
|
||||
folder_for() { case "$1" in
|
||||
en) echo English;; fr) echo French;; de) echo German;; es) echo Spanish;;
|
||||
it) echo Italian;; nl) echo Dutch;; pl) echo Polish;; pt) echo Portuguese;; ru) echo Russian;;
|
||||
*) echo "$1";; esac; }
|
||||
|
||||
# screen id -> output file basename (matches existing filenames)
|
||||
name_for() { case "$1" in
|
||||
choose-receivers) echo "Choose Receivers";; send-anywhere) echo "Send Anywhere";;
|
||||
share-securely) echo "Share Securely";; stay-private) echo "Stay private";;
|
||||
*) echo "$1";; esac; }
|
||||
|
||||
# screen id -> device mockup mode (which mockup frames the screenshot), or empty for none
|
||||
mode_for() { case "$1" in
|
||||
send-anywhere) echo "rotated";; choose-receivers|share-securely) echo "straight";;
|
||||
*) echo "";; esac; }
|
||||
|
||||
n=0
|
||||
for loc in $LOCALES; do
|
||||
folder=$(folder_for "$loc")
|
||||
outdir=$([[ -n "$PUBLISH" ]] && echo "../$folder" || echo "generated/$folder")
|
||||
mkdir -p "$outdir"
|
||||
for scr in $SCREENS; do
|
||||
shot="assets/shots/$loc/$scr.png"
|
||||
mode="$(mode_for "$scr")"
|
||||
|
||||
# Frame the screenshot into its mockup (frame.sh masks it to the real screen
|
||||
# shape) when a capture and a mockup mode exist; regenerate if the shot is newer.
|
||||
device_arg="none"
|
||||
if [[ -n "$mode" && -f "$shot" ]]; then
|
||||
device="assets/shots/$loc/$scr.device.png"
|
||||
[[ ! -f "$device" || "$shot" -nt "$device" ]] && ./frame.sh "$mode" "$shot" "$device" >/dev/null
|
||||
device_arg="$device"
|
||||
fi
|
||||
|
||||
out="$outdir/$(name_for "$scr").png"
|
||||
typst compile screens.typ "$out" \
|
||||
--input locale="$loc" --input screen="$scr" --input device="$device_arg" --ppi 72
|
||||
echo " ✅ $out"
|
||||
n=$((n+1))
|
||||
done
|
||||
done
|
||||
echo ""
|
||||
echo "Done — $n screenshot(s)$([[ -n "$PUBLISH" ]] && echo ' (published)')."
|
||||
64
packaging/apple/studio/capture.sh
Executable file
64
packaging/apple/studio/capture.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# Capture real, localized app screens for the studio pipeline.
|
||||
#
|
||||
# Drives the app straight into each marketing screen via the DEBUG `-VniScreenshot`
|
||||
# launch argument (no UI automation needed), once per locale via `-AppleLanguages`,
|
||||
# and writes assets/shots/<locale>/<screen>.png at the device's native resolution.
|
||||
#
|
||||
# ./capture.sh # all locales
|
||||
# LOCALES="en fr" ./capture.sh
|
||||
# SCREENSHOT_DEVICE="iPhone 17 Pro Max" ./capture.sh
|
||||
#
|
||||
# Requires a Debug build (the fixture gateway is compiled under #if DEBUG).
|
||||
set -euo pipefail
|
||||
trap 'echo "capture.sh: failed (rc=$?) at line $LINENO" >&2' ERR
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
APPLE_DIR="$(cd ../../../apple && pwd)"
|
||||
DEVICE="${SCREENSHOT_DEVICE:-iPhone 17 Pro Max}"
|
||||
BUNDLE_ID="com.vnidrop.app"
|
||||
LOCALES=${LOCALES:-"en fr de es it nl pl pt ru"}
|
||||
|
||||
# scenario (launch arg value) -> studio screen id (output filename stem)
|
||||
SCENARIOS="share:share-securely approval:choose-receivers transfer-details:send-anywhere"
|
||||
|
||||
echo "==> Regenerating project"; (cd "$APPLE_DIR" && xcodegen generate >/dev/null)
|
||||
|
||||
echo "==> Building VniDrop (Debug) for $DEVICE"
|
||||
xcodebuild build -project "$APPLE_DIR/VniDrop.xcodeproj" -scheme VniDrop \
|
||||
-destination "platform=iOS Simulator,name=$DEVICE" -configuration Debug \
|
||||
CODE_SIGNING_ALLOWED=NO >/dev/null
|
||||
APP="$(xcodebuild -project "$APPLE_DIR/VniDrop.xcodeproj" -scheme VniDrop \
|
||||
-destination "platform=iOS Simulator,name=$DEVICE" -configuration Debug \
|
||||
-showBuildSettings 2>/dev/null | awk -F' = ' '/ BUILT_PRODUCTS_DIR /{print $2; exit}')/VniDrop.app"
|
||||
|
||||
UDID="$(xcrun simctl list devices available | grep -F "$DEVICE (" | head -1 \
|
||||
| grep -oiE '[0-9a-f-]{36}' | head -1)"
|
||||
[ -n "$UDID" ] || { echo "error: no simulator '$DEVICE'"; exit 1; }
|
||||
echo " device: $UDID"
|
||||
echo " app: $APP"
|
||||
|
||||
xcrun simctl boot "$UDID" 2>/dev/null || true
|
||||
xcrun simctl bootstatus "$UDID" -b >/dev/null 2>&1 || true
|
||||
xcrun simctl ui "$UDID" appearance dark >/dev/null 2>&1 || true # match the dark marketing look
|
||||
xcrun simctl status_bar "$UDID" override --time "9:41" \
|
||||
--batteryState charged --batteryLevel 100 --cellularBars 4 --wifiBars 3 >/dev/null 2>&1 || true
|
||||
xcrun simctl install "$UDID" "$APP"
|
||||
|
||||
for loc in $LOCALES; do
|
||||
mkdir -p "assets/shots/$loc"
|
||||
for pair in $SCENARIOS; do
|
||||
scenario="${pair%%:*}"; screen="${pair##*:}"
|
||||
xcrun simctl launch --terminate-running-process "$UDID" "$BUNDLE_ID" \
|
||||
-VniScreenshot "$scenario" -AppleLanguages "($loc)" -AppleLocale "$loc" >/dev/null
|
||||
sleep 4
|
||||
# simctl can't write into the project tree (TCC blocks the CoreSimulator helper
|
||||
# on external/again-protected volumes), so capture to a temp file and move it in.
|
||||
tmp="$(mktemp -t vnishot).png"
|
||||
xcrun simctl io "$UDID" screenshot "$tmp" >/dev/null 2>&1
|
||||
mv "$tmp" "assets/shots/$loc/$screen.png"
|
||||
echo " 📸 $loc/$screen.png"
|
||||
done
|
||||
done
|
||||
echo ""
|
||||
echo "Done. Now run ./build.sh to composite."
|
||||
38
packaging/apple/studio/frame.sh
Executable file
38
packaging/apple/studio/frame.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Composite a screenshot into a device mockup using the mockup's real screen shape
|
||||
# as the mask — so the screen curvature comes from the mockup, never a guessed radius.
|
||||
# Output is a device PNG the size of the mockup, transparent outside the phone, for
|
||||
# Typst to place directly.
|
||||
#
|
||||
# ./frame.sh straight assets/shots/en/share-securely.png out-device.png
|
||||
# ./frame.sh rotated assets/shots/en/send-anywhere.png out-device.png
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
MODE="$1"; SHOT="$2"; OUT="$3"
|
||||
MOCKUP="assets/mockup-$MODE.png"
|
||||
MASK="assets/mask-$MODE.png"
|
||||
|
||||
# Cache the screen-glass mask (near-black region of the mockup, flattened off transparency).
|
||||
if [[ ! -f "$MASK" || "$MOCKUP" -nt "$MASK" ]]; then
|
||||
magick "$MOCKUP" -background magenta -flatten -colorspace Gray -threshold 6% -negate "$MASK"
|
||||
fi
|
||||
|
||||
CW=$(magick identify -format "%w" "$MOCKUP"); CH=$(magick identify -format "%h" "$MOCKUP")
|
||||
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
if [[ "$MODE" == "rotated" ]]; then
|
||||
# Perspective-warp onto the tilted glass quad (canvas-sized already).
|
||||
./warp.sh "$SHOT" "$tmp/screen.png" >/dev/null
|
||||
else
|
||||
# Cover-fit the screenshot to the screen's bounding box, placed at its offset.
|
||||
read SW SH SX SY <<<"$(magick "$MASK" -format "%@" info: | tr 'x+' ' ')"
|
||||
magick "$SHOT" -resize "${SW}x${SH}^" -gravity center -extent "${SW}x${SH}" "$tmp/fit.png"
|
||||
magick -size "${CW}x${CH}" xc:none "$tmp/fit.png" -geometry "+${SX}+${SY}" -composite "$tmp/screen.png"
|
||||
fi
|
||||
|
||||
# Clip the screen layer to the exact glass shape (rounded corners from the mask),
|
||||
# then lay it over the mockup so the bezel frames it.
|
||||
magick "$tmp/screen.png" "$MASK" -alpha off -compose CopyOpacity -composite "$tmp/clipped.png"
|
||||
magick "$MOCKUP" "$tmp/clipped.png" -compose over -composite "$OUT"
|
||||
echo "framed -> $OUT"
|
||||
78
packaging/apple/studio/screens.typ
Normal file
78
packaging/apple/studio/screens.typ
Normal file
@@ -0,0 +1,78 @@
|
||||
// App Store screenshot composition.
|
||||
// Rendered per (locale, screen) via:
|
||||
// typst compile screens.typ out.png --input locale=fr --input screen=share-securely --ppi 72
|
||||
// Canvas is 1284x2778pt -> at 72ppi that's exactly 1284x2778px.
|
||||
|
||||
#let strings = json("strings.json")
|
||||
|
||||
// ---- inputs (with dev-friendly defaults) --------------------------------
|
||||
#let locale = sys.inputs.at("locale", default: "en")
|
||||
#let screen-id = sys.inputs.at("screen", default: "share-securely")
|
||||
// A pre-framed device PNG (mockup + screenshot, produced by frame.sh), or "none"
|
||||
// to show the empty mockup. Screen curvature comes from the mockup mask, not Typst.
|
||||
#let device-image = sys.inputs.at("device", default: "none")
|
||||
|
||||
#let cap = strings.locales.at(locale).at(screen-id)
|
||||
|
||||
// ---- canvas -------------------------------------------------------------
|
||||
#let CW = 1284pt
|
||||
#let CH = 2778pt
|
||||
|
||||
// ---- per-screen layout data --------------------------------------------
|
||||
#let screens = (
|
||||
"choose-receivers": (
|
||||
bg: gradient.linear(angle: 160deg, rgb("#f1eafc"), rgb("#e6d8fb"), rgb("#d9c4f7")),
|
||||
text: (place: "top", theme: "dark"),
|
||||
device: (mockup: "straight", w: 860pt, dx: 212pt, dy: 560pt, rot: 4deg),
|
||||
),
|
||||
"share-securely": (
|
||||
bg: gradient.linear(angle: 165deg, rgb("#efe7fb"), rgb("#e3d3f8"), rgb("#d5bff4")),
|
||||
text: (place: "top", theme: "dark"),
|
||||
device: (mockup: "straight", w: 820pt, dx: 232pt, dy: 470pt, rot: 0deg),
|
||||
),
|
||||
"send-anywhere": (
|
||||
bg: gradient.linear(dir: ttb, rgb("#e9ddf9"), rgb("#ddc9f4")),
|
||||
text: (place: "bottom", theme: "dark"),
|
||||
globe: (w: 1180pt, dx: 52pt, dy: -160pt),
|
||||
device: (mockup: "rotated", w: 900pt, dx: 90pt, dy: 980pt, rot: 0deg),
|
||||
),
|
||||
"stay-private": (
|
||||
bg: gradient.linear(dir: ttb, rgb("#2a0f4d"), rgb("#6b3fa0"), rgb("#e9dcf8"), rgb("#ddc7f4")),
|
||||
text: (place: "top", theme: "light"),
|
||||
),
|
||||
)
|
||||
|
||||
#let cfg = screens.at(screen-id)
|
||||
|
||||
#set page(width: CW, height: CH, margin: 0pt, fill: cfg.bg)
|
||||
#set text(font: ("SF NS", "Helvetica Neue")) // "SF NS" is macOS San Francisco (= SF Pro)
|
||||
|
||||
// ---- compose ------------------------------------------------------------
|
||||
|
||||
// globe (transparent PNG)
|
||||
#if "globe" in cfg [
|
||||
#place(top + left, dx: cfg.globe.dx, dy: cfg.globe.dy,
|
||||
image("assets/globe.png", width: cfg.globe.w))
|
||||
]
|
||||
|
||||
// device: a pre-framed PNG (frame.sh) when available, else the empty mockup.
|
||||
#if "device" in cfg [
|
||||
#let d = cfg.device
|
||||
#let img = if device-image != "none" { device-image } else { "assets/mockup-" + d.mockup + ".png" }
|
||||
#place(top + left, dx: d.dx, dy: d.dy,
|
||||
rotate(d.rot, origin: center, image(img, width: d.w)))
|
||||
]
|
||||
|
||||
// caption
|
||||
#let theme-color = if cfg.text.theme == "light" { white } else { rgb("#1b1226") }
|
||||
#let sub-color = if cfg.text.theme == "light" { rgb("#f3ecfb") } else { rgb("#2c2138") }
|
||||
#let caption = align(center)[
|
||||
#text(size: 104pt, weight: 800, fill: theme-color)[#cap.title]
|
||||
#v(10pt, weak: true)
|
||||
#text(size: 60pt, weight: 600, fill: sub-color)[#cap.subtitle]
|
||||
]
|
||||
#if cfg.text.place == "top" [
|
||||
#place(top + center, dy: 96pt, box(width: CW - 160pt, caption))
|
||||
] else [
|
||||
#place(bottom + center, dy: -220pt, box(width: CW - 160pt, caption))
|
||||
]
|
||||
69
packaging/apple/studio/strings.json
Normal file
69
packaging/apple/studio/strings.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"_comment": "Marketing captions for App Store screenshots. NOT app strings — these live only here. Translations below the English source are a first pass and should be reviewed by a native speaker.",
|
||||
"screens": ["choose-receivers", "send-anywhere", "share-securely", "stay-private"],
|
||||
"locales": {
|
||||
"en": {
|
||||
"_folder": "English",
|
||||
"choose-receivers": { "title": "Choose Receivers", "subtitle": "Approve each person invited" },
|
||||
"send-anywhere": { "title": "Send Anywhere", "subtitle": "Direct file transfer worldwide" },
|
||||
"share-securely": { "title": "Share Securely", "subtitle": "QR, NFC or file" },
|
||||
"stay-private": { "title": "Stay Private", "subtitle": "Encrypted with no server copy" }
|
||||
},
|
||||
"fr": {
|
||||
"_folder": "French",
|
||||
"choose-receivers": { "title": "Choisissez les destinataires", "subtitle": "Approuvez chaque personne invitée" },
|
||||
"send-anywhere": { "title": "Envoyez partout", "subtitle": "Transfert direct dans le monde entier" },
|
||||
"share-securely": { "title": "Partagez en sécurité", "subtitle": "QR, NFC ou fichier" },
|
||||
"stay-private": { "title": "Restez privé", "subtitle": "Chiffré, sans copie serveur" }
|
||||
},
|
||||
"de": {
|
||||
"_folder": "German",
|
||||
"choose-receivers": { "title": "Empfänger auswählen", "subtitle": "Jede eingeladene Person bestätigen" },
|
||||
"send-anywhere": { "title": "Überallhin senden", "subtitle": "Direkte Dateiübertragung weltweit" },
|
||||
"share-securely": { "title": "Sicher teilen", "subtitle": "QR, NFC oder Datei" },
|
||||
"stay-private": { "title": "Privat bleiben", "subtitle": "Verschlüsselt, ohne Server-Kopie" }
|
||||
},
|
||||
"es": {
|
||||
"_folder": "Spanish",
|
||||
"choose-receivers": { "title": "Elige destinatarios", "subtitle": "Aprueba a cada invitado" },
|
||||
"send-anywhere": { "title": "Envía a cualquier lugar", "subtitle": "Transferencia directa en todo el mundo" },
|
||||
"share-securely": { "title": "Comparte con seguridad", "subtitle": "QR, NFC o archivo" },
|
||||
"stay-private": { "title": "Mantén la privacidad", "subtitle": "Cifrado, sin copia en servidor" }
|
||||
},
|
||||
"it": {
|
||||
"_folder": "Italian",
|
||||
"choose-receivers": { "title": "Scegli i destinatari", "subtitle": "Approva ogni invitato" },
|
||||
"send-anywhere": { "title": "Invia ovunque", "subtitle": "Trasferimento diretto in tutto il mondo" },
|
||||
"share-securely": { "title": "Condividi in sicurezza","subtitle": "QR, NFC o file" },
|
||||
"stay-private": { "title": "Resta privato", "subtitle": "Crittografato, senza copia sul server" }
|
||||
},
|
||||
"nl": {
|
||||
"_folder": "Dutch",
|
||||
"choose-receivers": { "title": "Kies ontvangers", "subtitle": "Keur elke genodigde goed" },
|
||||
"send-anywhere": { "title": "Verstuur overal", "subtitle": "Directe bestandsoverdracht wereldwijd" },
|
||||
"share-securely": { "title": "Deel veilig", "subtitle": "QR, NFC of bestand" },
|
||||
"stay-private": { "title": "Blijf privé", "subtitle": "Versleuteld, geen serverkopie" }
|
||||
},
|
||||
"pl": {
|
||||
"_folder": "Polish",
|
||||
"choose-receivers": { "title": "Wybierz odbiorców", "subtitle": "Zatwierdź każdą zaproszoną osobę" },
|
||||
"send-anywhere": { "title": "Wysyłaj wszędzie", "subtitle": "Bezpośredni transfer plików na całym świecie" },
|
||||
"share-securely": { "title": "Udostępniaj bezpiecznie", "subtitle": "Kod QR, NFC lub plik" },
|
||||
"stay-private": { "title": "Zachowaj prywatność", "subtitle": "Szyfrowane, bez kopii na serwerze" }
|
||||
},
|
||||
"pt": {
|
||||
"_folder": "Portuguese",
|
||||
"choose-receivers": { "title": "Escolha os destinatários", "subtitle": "Aprove cada pessoa convidada" },
|
||||
"send-anywhere": { "title": "Envie para qualquer lugar", "subtitle": "Transferência direta pelo mundo todo" },
|
||||
"share-securely": { "title": "Compartilhe com segurança", "subtitle": "QR, NFC ou arquivo" },
|
||||
"stay-private": { "title": "Mantenha a privacidade", "subtitle": "Criptografado, sem cópia no servidor" }
|
||||
},
|
||||
"ru": {
|
||||
"_folder": "Russian",
|
||||
"choose-receivers": { "title": "Выбирайте получателей", "subtitle": "Подтверждайте каждого приглашённого" },
|
||||
"send-anywhere": { "title": "Отправляйте куда угодно", "subtitle": "Прямая передача файлов по всему миру" },
|
||||
"share-securely": { "title": "Делитесь безопасно", "subtitle": "QR, NFC или файл" },
|
||||
"stay-private": { "title": "Оставайтесь приватными", "subtitle": "Шифрование без копии на сервере" }
|
||||
}
|
||||
}
|
||||
}
|
||||
25
packaging/apple/studio/warp.sh
Executable file
25
packaging/apple/studio/warp.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# Perspective-warp a flat screenshot onto the tilted mockup's screen glass.
|
||||
# Output is a transparent PNG the exact size of mockup-rotated.png, so Typst can
|
||||
# stack it directly over the mockup (bezel frames it).
|
||||
#
|
||||
# ./warp.sh assets/shots/en/send-anywhere.png assets/shots/en/send-anywhere.warped.png
|
||||
#
|
||||
# The 4 destination corners are the glass corners of mockup-rotated.png (1696x2528),
|
||||
# auto-detected once. Re-detect if the mockup changes (see README).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
SRC="$1"; OUT="$2"
|
||||
MW=1696; MH=2528 # mockup-rotated.png dimensions
|
||||
TL="136,28"; TR="836,236"; BL="944,2136"; BR="1668,2312"
|
||||
|
||||
W=$(magick identify -format "%w" "$SRC")
|
||||
H=$(magick identify -format "%h" "$SRC")
|
||||
|
||||
magick \
|
||||
\( -size ${MW}x${MH} xc:none \) \
|
||||
\( "$SRC" -virtual-pixel transparent +distort Perspective \
|
||||
"0,0 $TL $((W-1)),0 $TR 0,$((H-1)) $BL $((W-1)),$((H-1)) $BR" \) \
|
||||
-background none -flatten "$OUT"
|
||||
echo "warped -> $OUT"
|
||||
Reference in New Issue
Block a user