diff --git a/.gitignore b/.gitignore
index a3c6b1e..3bfbf75 100644
--- a/.gitignore
+++ b/.gitignore
@@ -87,3 +87,12 @@ nitrogen/
# argent screen recordings
.argent/
+
+# App Store release
+.argent/
+example/goldie/out/
+example/fastlane/report.xml
+example/fastlane/README.md
+example/fastlane/metadata/review_information/
+example/build/
+example/.bundle/
diff --git a/README.md b/README.md
index 893c897..3458082 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
WebGPU shader effects for React Native. Drop in a component, or write your own effect in WGSL. Rendering runs on a separate JS runtime, so a busy JS thread does not stall the animation.
-
+
> **Experimental.** APIs may change without notice. The library relies on `react-native-worklets` bundle mode, which is off by default for now.
diff --git a/docs/app-store/asset-audit.md b/docs/app-store/asset-audit.md
index 4a808b6..c6f04a6 100644
--- a/docs/app-store/asset-audit.md
+++ b/docs/app-store/asset-audit.md
@@ -2,39 +2,34 @@
Audited 2026-10-30 from git history, source comments, project notes, EXIF and the Wikimedia Commons API. Paths are relative to `example/assets`. There are no bundled sound files or fonts: every sound is synthesized in code, and text uses system fonts.
-## Blockers: licence incompatible, unknown or third-party owned
-
-Each of these ships in the app bundle. Replace it or get written permission before submitting.
-
-| Asset | Used by | What the record shows | Fix |
-| --- | --- | --- | --- |
-| `foam.jpg` | Beer, Foam lab | Stock photo supplied by the owner, licence never checked (the code says so too) | Find the source and license it, or shoot one |
-| `candle/cake.jpg` | Birthday Candle | Cake photo pasted by the owner, then retouched. Source unknown | Find the source, or replace |
-| `namedrop/home-atlas.jpg` | NameDrop | Real iOS home screen capture showing Apple app icons and wallpaper | Replace with an invented home screen |
-| `iphone-background.png` | Siri Orb | Real iOS home screen capture showing Apple app icons and wallpaper | Replace with an invented home screen |
-| `telegram/pattern.png` | Telegram screens | A real Telegram chat wallpaper | Draw an original pattern |
-| `telegram/*.png` icons | Telegram screens | Cropped from a screen recording of Telegram | Redraw or use an MIT icon set |
-| `instagram/*.png` icons | Voice Delete | Cropped from a recording of Instagram | Redraw or use an MIT icon set |
-| `telegram/avatar.jpg` | Telegram screens | Identifiable man speaking on stage. No record of who took it or who he is | Confirm you own it and have consent, or swap for a credited or generated avatar |
-| `weather/*.png` | Weather | No source recorded in git or notes. They look like an existing weather icon set | Identify the set, or redraw |
-| `catalog/*.jpg` showing other products | Catalog thumbnails | App screenshots that show third-party marks: `holo-card.jpg` (Revolut, Monzo), `wallet.jpg` (Mastercard-style logo), `siri-orb.jpg` (Safari and Messages icons), `banners.jpg` (Duolingo), `imessage.jpg`, `telegram-*.jpg`, `voice-delete.jpg`, and `weather.jpg` | Retake after the matching screens change |
-
-Picsum images load at runtime in Voice Delete. Lorem Picsum draws from Unsplash photographs, and the owner has no record of which ones, so bundling known images is the safer route.
+## Blockers: resolved on 2026-10-31
+
+Every blocker from the audit of 2026-10-30 is out of the app. Licences of the new Commons sources were read from the Commons API on 2026-10-31 and are recorded in `docs/credits.md`. `example/assets/tools/generate-assets.py` rebuilds everything marked as generated.
+
+| Old asset | Replacement |
+| --- | --- |
+| `foam.jpg` | Generated foam and beer texture |
+| `candle/cake.jpg` | "3rd birthday cake", CC0, flame painted out and background extended |
+| `namedrop/home-atlas.jpg` | `wave/scene-atlas.jpg`, "Misty path", CC0, no icons |
+| `iphone-background.png` | `scenes/misty-path.jpg`, same scene |
+| `wave/avatar.jpg` | `wave/dusk.jpg`, a crop of "Seaside swing at dusk", CC0, with no person in frame |
+| `telegram/pattern.png` | Generated dot pattern, `chat/pattern.png` |
+| `telegram/*.png`, `instagram/*.png` | Generated icons in `chat/` |
+| `telegram/avatar.jpg` | Generated avatar, `chat/avatar-peaks.png` |
+| `weather/*.png` | Generated icons at the same paths |
+| Runtime Lorem Picsum images | Bundled: "Forest path at sunset" and "Misty path" (both CC0), two generated avatars |
+| `catalog/*.jpg` showing other products | Retaken from the app after the screens changed, or deleted with their screen |
+| `charizard.png` | Deleted |
## Needs care
-- **SF Symbols.** `cracks/*-fill.png`, `flashlight/camera.png`, `flashlight/flashlight-off.png`, `flashlight/torch-glyph.png` and `imessage/video.png` are SF Symbols rendered to PNG (confirmed in code for the FaceTime glyph, inferred from file names for the rest). Apple's terms allow them in apps for Apple platforms and bar use in logos or app icons. Keep them out of the Android build and out of the store icon.
-- **`polaroid/table.jpg` source comment is wrong.** `PolaroidScreen.tsx` credits "Weatherworn top of wooden table" (CC0), but the file's EXIF names Martin Michael Lorenz and it is visually identical to `bubblewrap/walnut.jpg`, so it is CC BY-SA 3.0. `docs/credits.md` has the correct credit.
-- **Portrait release.** `namedrop/avatar.jpg` and Black Rock beach show real people, and Commons flags personality rights on the beach photo. CC licences cover copyright only. The avatar appears as a fictional contact called Maren, so leave it out of store screenshots and marketing, where it could read as an endorsement.
+- **SF Symbols.** The flashlight PNGs are deleted. The screen now uses plain sliders and a text button, so no SF Symbol assets remain.
+- **Portrait release.** Black Rock beach shows real people, and Commons flags personality rights on it. CC licences cover copyright only. Keep it out of store screens.
- **ShareAlike.** The BY-SA images are adapted, so the adapted files must also be available under BY-SA. This repository is public, which works, as long as those files stay in it. The MIT license of the code does not extend to them. `docs/credits.md` states this.
- **DRM clause.** CC licences restrict adding technical measures to licensed works, and Apple's FairPlay wraps the app binary. Parallel distribution of the same images in the public repository is the usual mitigation. CC0 or CC BY 4.0 replacements remove the question.
## Clean
-Verified against the Commons API on 2026-10-30, author and licence match `docs/credits.md`: `namedrop/avatar.jpg`, `flashlight/corridor.jpg`, `polaroid/photo-*.jpg`, `polaroid/table.jpg` and `bubblewrap/walnut.jpg` (Lorenz), `dandelion/dandelion-atlas.jpg`, `hourglass/hourglass-atlas.jpg`, `dispersion/cat-atlas.png`, `lavalamp/lava-lamp.jpg`, `cracks/palms.jpg`, `cracks/swing.jpg`, `ferrofluid/marble.jpg`.
+Verified against the Commons API on 2026-10-30, author and licence match `docs/credits.md`: `flashlight/corridor.jpg`, `camera/photo-*.jpg`, `camera/table.jpg` and `bubblewrap/walnut.jpg` (Lorenz), `dandelion/dandelion-atlas.jpg`, `hourglass/hourglass-atlas.jpg`, `dispersion/cat-atlas.png`, `lavalamp/lava-lamp.jpg`, `cracks/palms.jpg`, `cracks/swing.jpg`, `ferrofluid/marble.jpg`.
Original to the project: `icon.png`, `splash-icon.png`, `adaptive-icon.png`, `favicon.png` (present from the initial commit, please confirm you made them), and the other `catalog/*.jpg` thumbnails, which are screenshots of this app.
-
-## In the repository but not shipped
-
-Nothing requires these, so Metro leaves them out: `charizard.png` (a scan of a Pokémon card, owned by Nintendo, Creatures and GAME FREAK). Delete `charizard.png` from the public repository regardless.
diff --git a/docs/app-store/metadata.md b/docs/app-store/metadata.md
index 2c88b52..d8f5a7d 100644
--- a/docs/app-store/metadata.md
+++ b/docs/app-store/metadata.md
@@ -6,15 +6,14 @@ Paste these into App Store Connect. Limits are noted where Apple enforces them.
| Field | Value |
| --- | --- |
-| App name (30) | Effects |
-| Subtitle (30) | Shaders you tilt, blow & shake |
+| App name (30) | Effects: shaders & fun |
+| Subtitle (30) | Toys you tilt, blow & shake |
| Bundle ID | com.blazejkustra.rneffects |
| SKU | rneffects-ios |
| Version | 1.0.0 |
| Build | 1 |
| Primary language | English (U.S.) |
| Primary category | Entertainment |
-| Secondary category | Graphics & Design |
| Price | Free, no in-app purchases |
| Devices | iPhone only |
| Copyright | 2026 Blazej Kustra |
@@ -41,7 +40,7 @@ Effects is a pocket collection of interactive visual effects. Every one is a rea
Things to try:
-- Tap hard and watch the screen glass crack where you hit it
+- Tap and watch the screen glass crack where you hit it
- Blow on your phone to send dandelion seeds drifting or put out a candle
- Tilt to stir a lava lamp and slosh a full glass
- Hold a magnet near your phone and watch ferrofluid reach for it
@@ -52,11 +51,11 @@ There are also fabrics, liquid metal, northern lights, fire, ink and weather ski
The app has no accounts, no ads and no tracking. It is built on react-native-effects, an open-source library, and the source code is on GitHub.
-Sensor and microphone readings are used live for the animation and stay on your iPhone. The cracks, chats and payment scenes are visual effects only: nothing happens to your device, and no messages or payments are sent.
+Sensor and microphone readings are used live for the animation and stay on your iPhone. The cracks, chats and cards are visual effects only: nothing happens to your device, and no messages or payments are sent.
## Keywords (100)
-shader,gpu,visual,interactive,physics,particles,webgpu,generative,satisfying,fidget,art,animation
+interactive,physics,particles,satisfying,fidget,toy,art,animation,visual,sensory,relaxing,simulation
Leave out every third-party name, including Apple, iPhone, Instagram, Telegram and Polaroid.
@@ -84,11 +83,11 @@ Apple's current questionnaire. Answers reflect the catalog as of 2026-10-30.
| Realistic violence | None | |
| Guns or other weapons | None | |
| Gambling | No | No real money |
-| Simulated gambling | Infrequent | Scratch-off card with dollar prizes |
+| Simulated gambling | None | Scratch-off uses symbols only, with no points, money or wagering |
| Contests | None | |
| Loot boxes | No | |
-Expected result: 13+. Apple maps Frequent alcohol or tobacco references to 18+, and two effects out of more than forty supports Infrequent. Apple lists Infrequent simulated gambling as R 18+ in Australia, so see the Scratch-off entry in `review-risks.md`. Confirm the outcome on the Age Ratings page in App Store Connect.
+Expected result: 13+. Apple maps Frequent alcohol or tobacco references to 18+, and two effects out of about forty supports Infrequent. Confirm the outcome on the Age Ratings page in App Store Connect.
Do not tick "Made for Kids".
@@ -100,26 +99,33 @@ Do not tick "Made for Kids".
Why this holds: nothing the app reads leaves the device and there is no analytics, crash SDK, account or backend. Microphone and sensor data is analysed in memory, photos stay in temporary storage, and Apple defines collection as data transmitted off the device and kept beyond the real-time request.
-One caveat: Voice Delete fetches four images from picsum.photos, so that service sees the user's IP address. The developer never receives it, which keeps the answer accurate, and bundling those images removes the caveat altogether.
-
## Export compliance
`ITSAppUsesNonExemptEncryption` is `false` in `example/app.json`, so App Store Connect will skip the encryption question.
-Basis: the app contains no proprietary or non-standard encryption and only uses HTTPS through iOS. The FFmpeg libraries inside react-native-audio-api include OpenSSL code, and the app never calls it. Setting `disableFFmpeg` removes that code entirely (see the decision in `review-risks.md`). The owner is the party certifying this to Apple.
+Basis: the app contains no proprietary or non-standard encryption and only uses HTTPS through iOS. `disableFFmpeg` removes the OpenSSL code that FFmpeg would bring in. The owner is the party certifying this to Apple.
## Review notes
Paste into App Review Information > Notes. No demo account is needed.
- Effects is a catalog of interactive GPU shader effects built with the open-source react-native-effects library. Each effect is an original real-time shader written for this app. There is no login, no backend, no purchases and no user-generated content.
+ Effects is a catalog of interactive real-time visual effects rendered on the GPU. There is no login, no backend, no purchases, no network use and no user-generated content. No demo account is needed.
+
+ All people, chats, cards, banners, balances and forecasts in the app are fictional sample content. Chat screens are scripted animations and send nothing. Card screens make no payments. Weather is a labelled sample forecast. Banners are invented notifications and use no calendar or weather data. AI Orb, AI Shimmer and AI Voice are visual effects and contain no AI service.
- Everything shaped like another product is a visual demonstration running inside this app. The chat, lock screen, payment and contact screens are scripted animations with invented content: they send no messages, make no payments, and exchange no contacts. The cracked screen effects are clearly part of the app and end when you go back.
+ Device features (a real iPhone is recommended, because the simulator has no motion sensors):
+ - Microphone: open Dandelion or Birthday Candle and blow on the phone. AI Orb and AI Voice start on tap. Audio is analysed live and never recorded. Refusing permission leaves the screens usable.
+ - Camera: open Instant Camera, press Continue, allow the camera, then press the shutter. The photo stays in temporary storage and is not saved or uploaded.
+ - Motion: tilt in Beer, Hourglass or Lava Lamp. Shake in Instant Camera or Mystic Ball.
+ - Magnetometer: hold a magnet near the phone in Ferrofluid, or touch and hold the screen if no magnet is available.
- Device features to test: a real iPhone is recommended, since the simulator has no motion sensors.
- - Microphone: open Dandelion or Birthday Candle and blow on the phone. AI Orb and AI Voice start on tap. Audio is analysed live and is never recorded.
- - Camera: open Polaroid and allow the camera, then press the shutter. The photo stays in temporary storage.
- - Motion: tilt in Beer, Hourglass or Lava Lamp. Shake in Polaroid or Magic 8-Ball.
- - Magnetometer: hold a magnet near the phone in Ferrofluid.
+ Other notes:
+ - Cracked Screen and Hinge Crack show a permanent caption saying they are an effect. They end when you go back.
+ - Hourglass is a working focus timer.
+ - Flashlight is an on-screen beam and does not use the LED.
+ - Scratch-off uses symbols and always reveals a match. There is no money, stake or prize.
+ - Beer is a physics toy with no brand, drinking game or challenge.
+ - Links on the About screen, including the Privacy Policy, open in the system browser.
+ - The app is iPhone only and runs in compatibility mode on iPad.
Contact: kustrablazej@gmail.com
diff --git a/docs/app-store/review-risks.md b/docs/app-store/review-risks.md
index 79d6da2..26083dd 100644
--- a/docs/app-store/review-risks.md
+++ b/docs/app-store/review-risks.md
@@ -107,6 +107,33 @@ Apple allows device frames in marketing within its guidelines, but avoid iPhone
### 21. Permissions on entry
Dandelion and Birthday Candle ask for the microphone as soon as they open. Apple accepts this when the purpose is clear, and refusing must leave the app usable. Verified on a simulator: after refusal both screens stay usable and show "Microphone permission denied".
+## Status on 2026-10-31
+
+The owner asked to release, so the fixes below are applied. Where an item above names an effect by its old title, this table gives the new one.
+
+| Item | Status | What changed |
+| --- | --- | --- |
+| 1 Wallet | Fixed | Now "Metal Card": a single tap, no payment flow or system phrases, invented mark, app-styled sheet, no card stack. The glint shader is unchanged |
+| 2 Home screens | Fixed | No app-icon grid remains. Glow Orb sits on a CC0 misty path photo and Wave Reveal carries that scene to a dusk photo |
+| 3 Telegram screens | Fixed | Now "Message Dissolve" and "Hidden Text", with redrawn icons, an original wallpaper and an invented chat style |
+| 4 Voice Delete | Fixed | Restyled, redrawn icons, four bundled images, no network |
+| 5 iMessage Effects | Fixed | Now "Message Flourishes", with teal bubbles, a flat top bar and no call button. Encore is now rings rippling out of the bubble and Beams is now warm daylight shafts, so neither matches Echo or Lasers |
+| 6 NameDrop | Fixed | Now "Wave Reveal", an animation from a misty morning scene to a dusk photo. No contact poster, no real person, no share choice |
+| 7 Cracked Screen, Crackgate | Fixed | Abstract scene with an invented clock and a persistent caption. Crackgate is now "Hinge Crack" and the product name is gone |
+| 8 Thanos | Fixed | Now "Dust" |
+| 9 Weather | Fixed | New copy, labelled "Sample forecast", left-aligned layout, solid range bars, redrawn icons |
+| 10 Orbs, Banners | Fixed | "Glow Orb" is a centred gold and jade orb with a soft halo and no screen-edge glow, "Morphing thinking blob", invented app name in the banner, new Banners line |
+| 11 Cards | Fixed | Invented issuers, obviously fake numbers, contactless mark removed |
+| 12 Trademarked titles | Fixed | "Instant Camera" with a neutral stripe, "Mystic Ball" with a 7, "The Eye" with new copy |
+| 13 Positioning | Fixed in the catalog | Beer Foam (Foam lab), Linear Gradient, Circular Gradient and Scroll Reactive removed. A reviewer may still call the app a tech demo |
+| 14 App name | Open | Unchanged: "Effects" |
+| 15, 16 Lighter, Beer | Open | Age rating answers, and keep Beer out of screenshots |
+| 17 Scratch-off | Fixed | Now "Lucky Clover" in the UI, with symbols only: no points, bonus strip or ticket price. The simulated gambling answer can be None |
+| 18 Jokes and names | Fixed | Invented people, everyday chat scripts, developer in-jokes removed |
+| 19 Device frames | Open | Nothing changed. Applies to marketing images |
+| 20 Flashlight | Fixed | New copy. The SF Symbol PNGs are deleted and the screen uses plain sliders and a switch |
+| 21 Permissions | No change needed | Already verified |
+
## Fixed in the repository (Applied)
| Risk | Change |
@@ -122,6 +149,6 @@ Dandelion and Birthday Candle ask for the microphone as soon as they open. Apple
## Other decisions
-- **Portrait lock:** `app.json` now sets portrait, since tilt effects would otherwise rotate the layout mid-gesture. It does change Crackgate: rotating a phone no longer replays it (a tap still does), and the unfolded landscape layout on iPhone Duo was never tested. Check it on a Duo, or remove `orientation` if that effect matters more.
-- **FFmpeg:** set `"disableFFmpeg": true` on the react-native-audio-api plugin. The app decodes no audio files, so this should remove LGPL libraries, OpenSSL and about 6.5 MB. It needs `pod install`, so it was not applied or tested.
-- **Barcode scanning:** set `"barcodeScannerEnabled": false` on expo-camera. It removes ZXingObjC, which the app never uses. Also needs `pod install`.
+- **Portrait lock:** `app.json` now sets portrait, since tilt effects would otherwise rotate the layout mid-gesture. Hinge Crack replays on a tap. Rotating no longer replays it.
+- **FFmpeg:** `"disableFFmpeg": true` is set on the react-native-audio-api plugin. The Release build on 2026-10-31 has no FFmpeg frameworks.
+- **Barcode scanning:** `"barcodeScannerEnabled": false` is set on expo-camera. See `docs/credits.md`.
diff --git a/docs/credits.md b/docs/credits.md
index 38cd262..4792cac 100644
--- a/docs/credits.md
+++ b/docs/credits.md
@@ -1,6 +1,6 @@
# Effects credits and licenses
-Effects uses the third-party photographs and open-source software listed here. Thank you to their authors.
+Effects uses the third-party photographs and open-source software listed here. Everything else in the app, including icons, chat wallpapers, weather symbols and the foam texture, is original and drawn in code or by `example/assets/tools/generate-assets.py`. Thank you to their authors.
The app's own code and the react-native-effects library are MIT licensed, Copyright (c) 2026 Blazej Kustra. See `LICENSE` in this repository.
@@ -10,28 +10,26 @@ All from Wikimedia Commons. Each is cropped, resized or otherwise adapted for th
| Effect | File | Work | Author | License |
| --- | --- | --- | --- | --- |
-| NameDrop | `namedrop/avatar.jpg` | [Smiling senior woman with curly hair portrait](https://commons.wikimedia.org/wiki/File:Smiling_senior_woman_with_curly_hair_portrait.jpg), cropped to the face | Shixart1985 | [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) |
| Flashlight | `flashlight/corridor.jpg` | [Grungy corridor of abandoned school](https://commons.wikimedia.org/wiki/File:Grungy_corridor_of_abandoned_school.jpg) | Shixart1985 | [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) |
-| Polaroid | `polaroid/photo-beach.jpg` | [Black Rock beach](https://commons.wikimedia.org/wiki/File:Black_Rock_beach.jpg) | Carey Ciuro | [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) |
-| Polaroid | `polaroid/photo-lake.jpg` | [Evening on Kathleen Lake](https://commons.wikimedia.org/wiki/File:Evening_on_Kathleen_Lake.jpg) | Jakub Fryš | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
-| Polaroid | `polaroid/photo-mountains.jpg` | [Mountain lake, Mylgval mountain cirque before sunset, Caucasus Mountains](https://commons.wikimedia.org/wiki/File:Mountain_lake,_Mylgval_mountain_cirque_before_sunset,_Caucasus_Mountains.jpg) | Vyacheslav Argenberg | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
-| Polaroid, Bubble Wrap | `polaroid/table.jpg`, `bubblewrap/walnut.jpg` | [Oberfläche Wohnzimmertisch](https://commons.wikimedia.org/wiki/File:Oberfl%C3%A4che_Wohnzimmertisch.JPG) | Martin Lorenz | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
+| Instant Camera | `camera/photo-beach.jpg` | [Black Rock beach](https://commons.wikimedia.org/wiki/File:Black_Rock_beach.jpg) | Carey Ciuro | [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) |
+| Instant Camera | `camera/photo-lake.jpg` | [Evening on Kathleen Lake](https://commons.wikimedia.org/wiki/File:Evening_on_Kathleen_Lake.jpg) | Jakub Fryš | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
+| Instant Camera | `camera/photo-mountains.jpg` | [Mountain lake, Mylgval mountain cirque before sunset, Caucasus Mountains](https://commons.wikimedia.org/wiki/File:Mountain_lake,_Mylgval_mountain_cirque_before_sunset,_Caucasus_Mountains.jpg) | Vyacheslav Argenberg | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
+| Instant Camera, Bubble Wrap | `camera/table.jpg`, `bubblewrap/walnut.jpg` | [Oberfläche Wohnzimmertisch](https://commons.wikimedia.org/wiki/File:Oberfl%C3%A4che_Wohnzimmertisch.JPG) | Martin Lorenz | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| Dandelion | `dandelion/dandelion-atlas.jpg` | [Taraxacum seedhead Paslieres 2013-05-09 n01](https://commons.wikimedia.org/wiki/File:Taraxacum_seedhead_Paslieres_2013-05-09_n01.jpg), extended and masked | Marie-Lan Nguyen | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
| Hourglass | `hourglass/hourglass-atlas.jpg` | [Wooden hourglass 3](https://commons.wikimedia.org/wiki/File:Wooden_hourglass_3.jpg), sand painted out and masked | S Sepp | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| Particle Dispersion | `dispersion/cat-atlas.png` | [Orange tabby cat sitting on fallen leaves](https://commons.wikimedia.org/wiki/File:Orange_tabby_cat_sitting_on_fallen_leaves-Hisashi-01.jpg), masked | Hisashi | [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/) |
| Lava Lamp | `lavalamp/lava-lamp.jpg` | [Lava lamp (yellow)](https://commons.wikimedia.org/wiki/File:Lava_lamp_(yellow).jpg) | DMGualtieri | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
-| Crackgate | `cracks/palms.jpg` | [Palm silhouettes](https://commons.wikimedia.org/wiki/File:Palm_silhouettes.jpg) | Prakash G (Prakash Srivaths) | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
-| Cracked Screen | `cracks/swing.jpg` | [Seaside swing at dusk](https://commons.wikimedia.org/wiki/File:Seaside_swing_at_dusk_(Unsplash).jpg) | Georg Nietsch | [CC0](https://creativecommons.org/publicdomain/zero/1.0/) |
+| Hinge Crack | `cracks/palms.jpg` | [Palm silhouettes](https://commons.wikimedia.org/wiki/File:Palm_silhouettes.jpg) | Prakash G (Prakash Srivaths) | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
+| Cracked Screen, Wave Reveal | `cracks/swing.jpg`, cropped for `wave/dusk.jpg` | [Seaside swing at dusk](https://commons.wikimedia.org/wiki/File:Seaside_swing_at_dusk_(Unsplash).jpg) | Georg Nietsch | [CC0](https://creativecommons.org/publicdomain/zero/1.0/) |
| Ferrofluid | `ferrofluid/marble.jpg` | [Marble Kymi Euboea Greece](https://commons.wikimedia.org/wiki/File:Marble_Kymi_Euboea_Greece.jpg) | Jebulon | [CC0](https://creativecommons.org/publicdomain/zero/1.0/) |
-
-Lorem Picsum (picsum.photos) serves the four placeholder pictures in the Voice Delete effect at runtime.
+| Birthday Candle | `candle/cake.jpg` | [3rd birthday cake](https://commons.wikimedia.org/wiki/File:3rd_birthday_cake_(Unsplash_10b8Lvvc-4g).jpg), background extended and flame painted out | Annie Spratt | [CC0](https://creativecommons.org/publicdomain/zero/1.0/) |
+| Voice Delete | `chat/photo-post.jpg` | [Forest path at sunset](https://commons.wikimedia.org/wiki/File:Forest_path_at_sunset_(Unsplash).jpg) | Sergei Akulich | [CC0](https://creativecommons.org/publicdomain/zero/1.0/) |
+| Voice Delete, Glow Orb, Wave Reveal | `chat/photo-reel.jpg`, `scenes/misty-path.jpg` and `wave/scene-atlas.jpg` | [Misty path](https://commons.wikimedia.org/wiki/File:Misty_path_(Unsplash).jpg) | Alessio Lin | [CC0](https://creativecommons.org/publicdomain/zero/1.0/) |
## Native software
| Component | Used through | License |
| --- | --- | --- |
-| FFmpeg 8.0.1 (libavcodec, libavformat, libavutil, libswresample) | react-native-audio-api, as separate dynamic frameworks | LGPL 2.1 or later. Source: https://ffmpeg.org. Build scripts: https://github.com/software-mansion-labs/rn-audio-libs |
-| OpenSSL | statically inside the FFmpeg frameworks | Apache-2.0 |
| Dawn (WebGPU) | react-native-webgpu | BSD-3-Clause |
| miniaudio | react-native-audio-api | MIT-0 or public domain |
| PFFFT | react-native-audio-api | BSD-style |
@@ -42,7 +40,6 @@ Lorem Picsum (picsum.photos) serves the four placeholder pictures in the Voice D
| glog | react-native | BSD-3-Clause |
| double-conversion | react-native | BSD-3-Clause |
| fmt | react-native | MIT |
-| ZXingObjC | expo-camera barcode scanning | Apache-2.0 |
| Expo modules | expo | MIT |
## JavaScript packages in the app bundle
@@ -52,18 +49,20 @@ Taken from the production bundle's source map. MIT and ISC packages are distribu
| Package | Version | License | Copyright |
| --- | --- | --- | --- |
| @babel/runtime | 7.29.7 | MIT | Copyright (c) 2014-present Sebastian McKenzie and other contributors |
-| @expo/cli | 56.1.14 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
-| @react-native/assets-registry | 0.85.3 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates |
-| @react-native/js-polyfills | 0.85.3 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates |
-| @react-native/normalize-colors | 0.85.3 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates |
-| @react-native/virtualized-lists | 0.85.3 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates |
+| @expo/cli | 58.1.17 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
+| @expo/log-box-utils | 58.0.2 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
+| @expo/metro | 58.0.0 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates. |
+| @react-native/asset-utils | 0.88.1 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates. |
+| @react-native/js-polyfills | 0.88.1 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates |
+| @react-native/normalize-colors | 0.88.1 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates |
+| @react-native/virtualized-lists | 0.88.1 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates |
| @react-navigation/core | 7.17.5 | MIT | Copyright (c) 2017 React Navigation Contributors |
| @react-navigation/elements | 2.9.19 | MIT | Copyright (c) 2017 React Navigation Contributors |
| @react-navigation/native | 7.2.5 | MIT | Copyright (c) 2017 React Navigation Contributors |
| @react-navigation/native-stack | 7.16.0 | MIT | Copyright (c) 2017 React Navigation Contributors |
| @react-navigation/routers | 7.5.5 | MIT | Copyright (c) 2017 React Navigation Contributors |
| @ungap/structured-clone | 1.3.0 | ISC | Copyright (c) 2021, Andrea Giammarchi, @WebReflection |
-| abort-controller | 3.0.0 | MIT | Copyright (c) 2017 Toru Nagashima |
+| ansi-styles | 4.3.0 | MIT | Copyright (c) Sindre Sorhus (https://sindresorhus.com) |
| base64-js | 1.5.1 | MIT | Copyright (c) 2014 Jameson Little |
| color | 4.2.3 | MIT | Copyright (c) 2012 Heather Arthur |
| color-convert | 2.0.1 | MIT | Copyright (c) 2011-2016 Heather Arthur |
@@ -71,39 +70,42 @@ Taken from the production bundle's source map. MIT and ISC packages are distribu
| color-string | 1.9.1 | MIT | Copyright (c) 2011 Heather Arthur |
| decode-uri-component | 0.2.2 | MIT | Copyright (c) 2017, Sam Verschueren (github.com/SamVerschueren) |
| escape-string-regexp | 4.0.0 | MIT | Copyright (c) Sindre Sorhus (https://sindresorhus.com) |
-| event-target-shim | 5.0.1 | MIT | Copyright (c) 2015 Toru Nagashima |
-| expo | 56.0.9 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
-| expo-asset | 56.0.16 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
-| expo-camera | 56.0.8 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
-| expo-constants | 56.0.17 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
-| expo-modules-core | 56.0.15 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
-| expo-sensors | 56.0.5 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
+| expo | 58.0.21 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
+| expo-asset | 58.0.19 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
+| expo-blur | 58.0.5 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
+| expo-camera | 58.0.14 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
+| expo-constants | 58.0.14 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
+| expo-glass-effect | 58.0.5 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
+| expo-modules-core | 58.0.28 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
+| expo-sensors | 58.0.4 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
+| expo-symbols | 58.0.4 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) |
| fast-deep-equal | 3.1.3 | MIT | Copyright (c) 2017 Evgeny Poberezkin |
| filter-obj | 1.1.0 | MIT | Copyright (c) Sindre Sorhus (sindresorhus.com) |
| flow-enums-runtime | 0.0.6 | MIT | Copyright (c) Facebook, Inc. and its affiliates. |
-| hoist-non-react-statics | 3.3.2 | BSD-3-Clause | Copyright (c) 2015, Yahoo! Inc. All rights reserved. |
| invariant | 2.2.4 | MIT | Copyright (c) 2013-present, Facebook, Inc. |
| is-arrayish | 0.3.4 | MIT | Copyright (c) 2015 JD Ballard |
| memoize-one | 5.2.1 | MIT | Copyright (c) 2019 Alexander Reardon |
+| metro-runtime | 0.87.1 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates. |
| nanoid | 3.3.12 | MIT | Copyright 2017 Andrey Sitnik |
| nullthrows | 1.1.1 | MIT | Copyright (c) 2016 Andres Suarez |
+| pretty-format | 29.7.0 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates. |
| promise | 8.3.0 | MIT | Copyright (c) 2014 Forbes Lindesay |
| query-string | 7.1.3 | MIT | Copyright (c) Sindre Sorhus (http://sindresorhus.com) |
-| react | 19.2.3 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates. |
+| react | 19.3.0 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates. |
| react-freeze | 1.0.4 | MIT | Copyright (c) 2021 Software Mansion |
| react-is | 18.3.1 | MIT | Copyright (c) Facebook, Inc. and its affiliates. |
-| react-native | 0.85.3 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates. |
-| react-native-audio-api | 0.12.2 | MIT | Copyright (c) Software Mansion |
-| react-native-gesture-handler | 2.31.2 | MIT | Copyright (c) 2016 Software Mansion |
+| react-native | 0.88.1 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates. |
+| react-native-audio-api | 0.13.6 | MIT | Copyright (c) Software Mansion |
+| react-native-gesture-handler | 3.3.0 | MIT | Copyright (c) 2016 Software Mansion |
| react-native-is-edge-to-edge | 1.3.1 | MIT | Copyright (c) 2024 Mathieu Acthernoene |
-| react-native-reanimated | 4.3.1 | MIT | Copyright (c) 2016 Software Mansion |
-| react-native-safe-area-context | 5.7.0 | MIT | Copyright (c) 2019 Th3rd Wave |
-| react-native-screens | 4.25.2 | MIT | Copyright (c) 2018 Software Mansion |
-| react-native-view-shot | 5.1.0 | MIT | Copyright (c) 2016 Gaëtan Renaudeau |
-| react-native-webgpu | 0.5.15 | MIT | Copyright (c) 2024 William Candillon |
-| react-native-worklets | 0.8.3 | MIT | Copyright (c) 2024 nobody |
+| react-native-reanimated | 4.7.1 | MIT | Copyright (c) 2016 Software Mansion |
+| react-native-safe-area-context | 5.10.1 | MIT | Copyright (c) 2019 Th3rd Wave |
+| react-native-screens | 4.30.0 | MIT | Copyright (c) 2018 Software Mansion |
+| react-native-view-shot | 6.1.1 | MIT | Copyright (c) 2016 Gaëtan Renaudeau |
+| react-native-webgpu | 0.17.0 | MIT | Copyright (c) 2024 William Candillon |
+| react-native-worklets | 0.13.0 | MIT | Copyright (c) 2024 nobody |
| regenerator-runtime | 0.13.11 | MIT | Copyright (c) 2014-present, Facebook, Inc. |
-| scheduler | 0.27.0 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates. |
+| scheduler | 0.28.0 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates. |
| semver | 7.8.2 | ISC | Copyright (c) Isaac Z. Schlueter and Contributors |
| simple-swizzle | 0.2.4 | MIT | Copyright (c) 2015 Josh Junon |
| split-on-first | 1.1.0 | MIT | Copyright (c) Sindre Sorhus (sindresorhus.com) |
@@ -113,7 +115,7 @@ Taken from the production bundle's source map. MIT and ISC packages are distribu
| use-sync-external-store | 1.6.0 | MIT | Copyright (c) Meta Platforms, Inc. and affiliates. |
| warn-once | 0.1.1 | MIT | Copyright (c) 2022 Satyajit Sahoo |
| whatwg-fetch | 3.6.20 | MIT | Copyright (c) 2014-2023 GitHub, Inc. |
-| whatwg-url-minimum | 0.1.2 | MIT | Copyright (c) Phil Pluckthun, |
+| whatwg-url-minimum | 0.2.0 | MIT | Copyright (c) Phil Pluckthun, |
## MIT License text
diff --git a/docs/privacy-policy.md b/docs/privacy-policy.md
index 7dcb2b0..0a6063d 100644
--- a/docs/privacy-policy.md
+++ b/docs/privacy-policy.md
@@ -1,6 +1,6 @@
# Effects Privacy Policy
-Effective date: 2026-10-30
+Effective date: 2026-10-31
Effects is a free iPhone app that shows interactive visual effects. It is published by Blazej Kustra, an individual developer ("I" in this policy). This policy explains what the app does with your data.
@@ -20,7 +20,7 @@ The app reads the sound level and frequency of the live signal to drive the anim
### Camera
-The Polaroid effect uses the back camera so you can take a photo and watch it develop as an instant print. You have to allow the camera and press the shutter.
+The Instant Camera effect uses the back camera so you can take a photo and watch it develop as an instant print. You have to allow the camera and press the shutter.
The photo is held in the app's temporary storage on your iPhone so it can be shown on screen. It is never uploaded and is never added to your photo library. iOS clears this temporary storage over time, and deleting the app removes it.
@@ -34,9 +34,7 @@ Some effects take a picture of their own screen content to animate it, for examp
## Network use
-The app works without an account and sends none of your data to me. There is one network feature:
-
-- The Voice Delete effect downloads four placeholder pictures from the public image service Lorem Picsum (picsum.photos). As with any web request, that service and its hosting providers receive your IP address and basic request details. I do not receive this information and the request contains nothing else about you. Lorem Picsum's own terms and privacy practices apply to it.
+The app makes no network requests of its own. Everything it shows is bundled with it, and it works offline.
Links on the About screen open in your browser, where the website's own privacy policy applies.
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 7c5878d..64125b4 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -25,6 +25,6 @@ export default defineConfig([
},
},
{
- ignores: ['node_modules/', 'lib/'],
+ ignores: ['node_modules/', 'lib/', 'example/vendor/'],
},
]);
diff --git a/example/Gemfile b/example/Gemfile
new file mode 100644
index 0000000..7a118b4
--- /dev/null
+++ b/example/Gemfile
@@ -0,0 +1,3 @@
+source "https://rubygems.org"
+
+gem "fastlane"
diff --git a/example/Gemfile.lock b/example/Gemfile.lock
new file mode 100644
index 0000000..d604fc1
--- /dev/null
+++ b/example/Gemfile.lock
@@ -0,0 +1,397 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ CFPropertyList (3.0.8)
+ abbrev (0.1.2)
+ addressable (2.9.1)
+ public_suffix (>= 2.0.2, < 8.0)
+ artifactory (3.0.17)
+ atomos (0.1.3)
+ aws-eventstream (1.4.1)
+ aws-partitions (1.1300.0)
+ aws-sdk-core (3.263.0)
+ aws-eventstream (~> 1, >= 1.4.1)
+ aws-partitions (~> 1, >= 1.992.0)
+ aws-sigv4 (~> 1.9)
+ base64
+ bigdecimal
+ jmespath (~> 1, >= 1.6.1)
+ logger
+ rexml (~> 3.4, >= 3.4.2)
+ aws-sdk-kms (1.138.0)
+ aws-sdk-core (~> 3, >= 3.263.0)
+ aws-sigv4 (~> 1.5)
+ aws-sdk-s3 (1.237.0)
+ aws-sdk-core (~> 3, >= 3.263.0)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.5)
+ aws-sigv4 (1.12.1)
+ aws-eventstream (~> 1, >= 1.0.2)
+ babosa (1.0.4)
+ base64 (0.3.0)
+ benchmark (0.5.0)
+ bigdecimal (4.1.3)
+ cgi (0.5.2)
+ claide (1.1.0)
+ colored (1.2)
+ colored2 (3.1.2)
+ commander (4.6.0)
+ highline (~> 2.0.0)
+ csv (3.3.7)
+ declarative (0.0.20)
+ digest-crc (0.7.0)
+ rake (>= 12.0.0, < 14.0.0)
+ domain_name (0.7.20261007)
+ dotenv (2.8.1)
+ emoji_regex (3.2.3)
+ erb (6.0.7)
+ excon (1.10.0)
+ faraday (2.15.1)
+ faraday-net_http (>= 2.0, < 3.6)
+ json
+ logger
+ faraday-cookie_jar (0.0.8)
+ faraday (>= 0.8.0)
+ http-cookie (>= 1.0.0)
+ faraday-follow_redirects (0.5.0)
+ faraday (>= 1, < 3)
+ faraday-multipart (1.2.0)
+ multipart-post (~> 2.0)
+ faraday-net_http (3.5.0)
+ net-http (~> 0.5)
+ faraday-retry (2.4.0)
+ faraday (~> 2.0)
+ fastimage (2.4.3)
+ fastlane (2.241.2)
+ CFPropertyList (>= 2.3, < 5.0.0)
+ abbrev (~> 0.1)
+ addressable (>= 2.9.0, < 3.0.0)
+ artifactory (~> 3.0)
+ aws-sdk-s3 (~> 1.197)
+ babosa (>= 1.0.3, < 2.0.0)
+ base64 (~> 0.2)
+ benchmark (>= 0.1.0)
+ bundler (>= 2.4.0, < 5.0.0)
+ cgi (~> 0.4)
+ colored (~> 1.2)
+ commander (~> 4.6)
+ csv (~> 3.3)
+ dotenv (>= 2.1.1, < 3.0.0)
+ emoji_regex (>= 0.1, < 4.0)
+ erb (>= 4.0, < 7.0)
+ excon (>= 1.0.0, < 2.0.0)
+ faraday (~> 2.7)
+ faraday-cookie_jar (~> 0.0.8)
+ faraday-follow_redirects (~> 0.3)
+ faraday-multipart (~> 1.0)
+ faraday-retry (~> 2.0)
+ fastimage (>= 2.1.0, < 3.0.0)
+ fastlane-sirp (>= 1.1.0)
+ gh_inspector (>= 1.1.2, < 2.0.0)
+ google-apis-androidpublisher_v3 (~> 0.99)
+ google-apis-playcustomapp_v1 (~> 0.1)
+ google-cloud-env (>= 1.6.0, < 2.5.0)
+ google-cloud-storage (~> 1.31)
+ highline (~> 2.0)
+ irb (>= 1.8)
+ json (< 3.0.0)
+ jwt (>= 2.10.3, < 4)
+ logger (>= 1.6, < 2.0)
+ mini_magick (>= 4.9.4, < 5.0.0)
+ multi_json (~> 1.12)
+ multipart-post (>= 2.0.0, < 3.0.0)
+ mutex_m (~> 0.3)
+ naturally (~> 2.2)
+ nkf (~> 0.2)
+ optparse (>= 0.1.1, < 1.0.0)
+ ostruct (>= 0.1.0)
+ plist (>= 3.1.0, < 4.0.0)
+ rubyzip (>= 3.4.0, < 4.0.0)
+ security (~> 0.3)
+ simctl (~> 1.6.3)
+ terminal-notifier (>= 3.1.0, < 4.0.0)
+ terminal-table (~> 4)
+ tty-screen (>= 0.6.3, < 1.0.0)
+ tty-spinner (>= 0.8.0, < 1.0.0)
+ word_wrap (~> 1.0.0)
+ xcodeproj (>= 1.13.0, < 2.0.0)
+ xcpretty (~> 0.4.1)
+ xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
+ fastlane-sirp (1.1.0)
+ gh_inspector (1.1.3)
+ google-apis-androidpublisher_v3 (0.110.0)
+ google-apis-core (>= 0.15.0, < 2.a)
+ google-apis-core (1.4.0)
+ addressable (~> 2.9)
+ faraday (~> 2.13)
+ faraday-follow_redirects (~> 0.3)
+ googleauth (~> 1.14)
+ mini_mime (~> 1.1)
+ multi_json (~> 1.11)
+ representable (~> 3.0)
+ retriable (>= 3.1, < 5.0)
+ google-apis-iamcredentials_v1 (0.28.0)
+ google-apis-core (>= 0.15.0, < 2.a)
+ google-apis-playcustomapp_v1 (0.18.0)
+ google-apis-core (>= 0.15.0, < 2.a)
+ google-apis-storage_v1 (0.70.0)
+ google-apis-core (>= 0.15.0, < 2.a)
+ google-cloud-core (1.9.1)
+ google-cloud-env (>= 1.0, < 3.a)
+ google-cloud-errors (~> 1.0)
+ google-cloud-env (2.4.1)
+ base64 (~> 0.2)
+ faraday (>= 1.0, < 3.a)
+ google-cloud-errors (1.7.1)
+ google-cloud-storage (1.62.1)
+ addressable (~> 2.8)
+ digest-crc (~> 0.4)
+ google-apis-core (>= 0.18, < 2)
+ google-apis-iamcredentials_v1 (~> 0.18)
+ google-apis-storage_v1 (>= 0.42)
+ google-cloud-core (~> 1.6)
+ googleauth (~> 1.9)
+ mini_mime (~> 1.0)
+ google-logging-utils (0.2.2)
+ googleauth (1.17.7)
+ faraday (>= 1.0, < 3.a)
+ google-cloud-env (~> 2.2)
+ google-logging-utils (~> 0.1)
+ jwt (>= 1.4, < 4.0)
+ os (>= 0.9, < 2.0)
+ pstore (~> 0.1)
+ signet (>= 0.16, < 2.a)
+ highline (2.0.3)
+ http-cookie (1.1.6)
+ domain_name (~> 0.5)
+ io-console (0.9.5)
+ irb (1.18.0)
+ pp (>= 0.6.0)
+ prism (>= 1.3.0)
+ rdoc (>= 4.0.0)
+ reline (>= 0.4.2)
+ jmespath (1.6.2)
+ json (2.21.2)
+ jwt (3.3.0)
+ base64
+ logger (1.7.0)
+ mini_magick (4.13.2)
+ mini_mime (1.1.5)
+ multi_json (1.21.2)
+ multipart-post (2.4.1)
+ mutex_m (0.3.0)
+ nanaimo (0.4.0)
+ naturally (2.3.0)
+ net-http (0.10.0)
+ net-protocol (>= 0.3.0)
+ uri (>= 0.11.1)
+ net-protocol (0.4.0)
+ timeout
+ nkf (0.3.0)
+ optparse (0.8.1)
+ os (1.1.4)
+ ostruct (0.6.3)
+ plist (3.7.2)
+ pp (0.6.4)
+ prettyprint
+ prettyprint (0.2.1)
+ prism (2.1.0)
+ prism (2.1.0-aarch64-linux-gnu)
+ prism (2.1.0-aarch64-linux-musl)
+ prism (2.1.0-arm-linux-gnu)
+ prism (2.1.0-arm-linux-musl)
+ prism (2.1.0-arm64-darwin)
+ prism (2.1.0-x86_64-darwin)
+ prism (2.1.0-x86_64-linux-gnu)
+ prism (2.1.0-x86_64-linux-musl)
+ pstore (0.3.0)
+ public_suffix (7.0.5)
+ rake (13.4.2)
+ rbs (4.2.0)
+ logger
+ prism (>= 1.6.0)
+ tsort
+ rdoc (8.1.0)
+ erb
+ prism (>= 1.6.0)
+ rbs (>= 4.0.0)
+ tsort
+ reline (0.7.0)
+ io-console (~> 0.5)
+ representable (3.3.1)
+ declarative (< 0.1.0)
+ trailblazer-option (>= 0.1.1, < 0.2.0)
+ uber (< 0.2.0)
+ retriable (4.2.0)
+ rexml (3.4.4)
+ rouge (3.28.0)
+ rubyzip (3.7.0)
+ security (0.4.0)
+ signet (0.22.2)
+ addressable (~> 2.8)
+ faraday (>= 0.17.5, < 3.a)
+ jwt (>= 1.5, < 4.0)
+ simctl (1.6.10)
+ CFPropertyList
+ naturally
+ terminal-notifier (3.1.0)
+ terminal-table (4.0.0)
+ unicode-display_width (>= 1.1.1, < 4)
+ timeout (0.6.1)
+ trailblazer-option (0.1.2)
+ tsort (0.2.0)
+ tty-cursor (0.7.1)
+ tty-screen (0.8.2)
+ tty-spinner (0.9.3)
+ tty-cursor (~> 0.7)
+ uber (0.1.0)
+ unicode-display_width (3.3.0)
+ unicode-emoji (~> 4.3)
+ unicode-emoji (4.3.0)
+ uri (1.1.2)
+ word_wrap (1.0.0)
+ xcodeproj (1.28.1)
+ CFPropertyList (>= 2.3.3, < 4.0)
+ atomos (~> 0.1.3)
+ base64
+ claide (>= 1.0.2, < 2.0)
+ colored2 (~> 3.1)
+ nanaimo (~> 0.4.0)
+ nkf
+ rexml (>= 3.3.6, < 4.0)
+ xcpretty (0.4.1)
+ rouge (~> 3.28.0)
+ xcpretty-travis-formatter (1.0.1)
+ xcpretty (~> 0.2, >= 0.0.7)
+
+PLATFORMS
+ aarch64-linux-gnu
+ aarch64-linux-musl
+ arm-linux-gnu
+ arm-linux-musl
+ arm64-darwin
+ ruby
+ x86_64-darwin
+ x86_64-linux-gnu
+ x86_64-linux-musl
+
+DEPENDENCIES
+ fastlane
+
+CHECKSUMS
+ CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261
+ abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242
+ addressable (2.9.1) sha256=f1212365945455c84e8d031eea40fc6bedf0c663a78d95b3ab93591040bc5c35
+ artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263
+ atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f
+ aws-eventstream (1.4.1) sha256=ceb6a99c61df4bb00344e54aed0351574beca24f87a0d1487d1d2ed80929ce96
+ aws-partitions (1.1300.0) sha256=e3cc87a4ffe875d7326e6b9f5fe696ff9a59632227f3252061dcba1782664924
+ aws-sdk-core (3.263.0) sha256=9b6d396ed8b3e5ba19b0640fbdbfa1875b0c485353e1bb836f1e72b403c8892d
+ aws-sdk-kms (1.138.0) sha256=b3b4da50b962faa1333d6072f2eb04d8ad71569b32ee0d639657df3f8be8b37d
+ aws-sdk-s3 (1.237.0) sha256=1e8a96a5411124d66f85f397bd00a1d6df53da1946725b6d5980d921a8e11191
+ aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00
+ babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
+ benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
+ bigdecimal (4.1.3) sha256=61ebe1e5e559bdc3cc6f2c0ee7f427321fc838f59611c294356eb04d6e21cf66
+ bundler (4.0.23) sha256=407ba8a1d928f412847f819aa5f8b6aef7c59fb51feb517112af8a165487287b
+ cgi (0.5.2) sha256=61ca30298171190fd4fa0d8018e57ada456eae9b7a2b78526debf7f0a0e6f8bb
+ claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e
+ colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c
+ colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a
+ commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9
+ csv (3.3.7) sha256=15a3f16f6628da8664d455eb634e7f22031bcf53044923caf56975109e71b9f8
+ declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9
+ digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07
+ domain_name (0.7.20261007) sha256=0976c6ab837dbde991b433d03acc38fdb91fa8839df31a1ed75ce0318dc0441c
+ dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8
+ emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b
+ erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92
+ excon (1.10.0) sha256=868d456c6db29f965d7cc6c5eddbb4d77df08e2a2b2dca7ca3547f2a55afc4b4
+ faraday (2.15.1) sha256=8708154deda739c5e86cc42b74960e23e8491fce0eba9e11639a6a34b1ac288f
+ faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb
+ faraday-follow_redirects (0.5.0) sha256=5cde93c894b30943a5d2b93c2fe9284216a6b756f7af406a1e55f211d97d10ad
+ faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757
+ faraday-net_http (3.5.0) sha256=2e7094c34e38754ee048cc3df47376f8657005d9375f8473bce1c3b526a22e36
+ faraday-retry (2.4.0) sha256=7b79c48fb7e56526faf247b12d94a680071ff40c9fda7cf1ec1549439ad11ebe
+ fastimage (2.4.3) sha256=304457e3c35bfdaefda3f1dacab2222a96b230b8ce6977b6b54748c1cc7ec539
+ fastlane (2.241.2) sha256=9c23f6d1b4aa5d8570ff8584186d5fdb1fdf7a01dd7ad168501b43fe360fe122
+ fastlane-sirp (1.1.0) sha256=10bc94f9682efd8e1badfb31452a76dd8981f1f3a33717c765fde6d75b54d847
+ gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939
+ google-apis-androidpublisher_v3 (0.110.0) sha256=0b0b84af02fb7f3a062849bc60d545ff8b37670ea7e90bd947199cc671916513
+ google-apis-core (1.4.0) sha256=74e838a334926e79ce685d3ff9f418956cc23e48c5fd7cade779fea933e96582
+ google-apis-iamcredentials_v1 (0.28.0) sha256=0a92ffe6cc39c569554af2a77a25dfc61519ed8bbb64ab04cffdd352dc5ef106
+ google-apis-playcustomapp_v1 (0.18.0) sha256=44b277b9dee4a59ac5e9d98be1485edc5e382d2f9d73c79ae8908a455786a254
+ google-apis-storage_v1 (0.70.0) sha256=6a2925cc39c27a133239738130a4cf37601e00627a36d91e4f528f5a4620ad0c
+ google-cloud-core (1.9.1) sha256=6590e393cbeadf018e6880038e19348d2ccfc83000625c569a9b21ab3b450af0
+ google-cloud-env (2.4.1) sha256=987cc58afc50ce2903128ab4dcd442159be4457246b8137e41d49d5c3fc590dd
+ google-cloud-errors (1.7.1) sha256=818bcf30998d10f28f30532a2595b49438cb7abf42f6ac495843078839987207
+ google-cloud-storage (1.62.1) sha256=10dbf2ebda3af1b4b841335364cb889827c6cb17bf6c9f8bc5313b6bef7a4d3c
+ google-logging-utils (0.2.2) sha256=ad2436ef22e918e2b1c3ad2bbaf5949f42f5389d49fd37e41b43bbdd3068e27c
+ googleauth (1.17.7) sha256=9db21857ddbe055a8b0d44ddb75d4ceda97511167431e30121c77c25a9ce9fe5
+ highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479
+ http-cookie (1.1.6) sha256=ba4b82be64de61dc281243dac70e3c382c45142f20268ed9276a3670c93feaa9
+ io-console (0.9.5) sha256=83dd9bbf4c4680416629245b39cd4212eb5f4d9a4ea91804870c1f6b846414d5
+ irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
+ jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
+ json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
+ jwt (3.3.0) sha256=44cc34fbc341c148233cf66641522b4bce0dfb1ce55e8073f05c56f44f4ee546
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
+ mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9
+ mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
+ multi_json (1.21.2) sha256=245531eaf54bdba57aba515ff08b4503505c5e09911daa89c5a561e2f85bea8e
+ multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8
+ mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751
+ nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723
+ naturally (2.3.0) sha256=459923cf76c2e6613048301742363200c3c7e4904c324097d54a67401e179e01
+ net-http (0.10.0) sha256=1a2ff6e37a05724973c43f2bf7bf105f59c36a4cd0a7e9b1c021a0a566987c27
+ net-protocol (0.4.0) sha256=f9f90c8c6f64794a45160bf422177a336db12809de3beeeaa26b88fcd2e8c853
+ nkf (0.3.0) sha256=357a8dbeba38b727b75930f665146546076a394a1c243faf634ff176e3588895
+ optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a
+ os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f
+ ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
+ plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42
+ pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570
+ prettyprint (0.2.1) sha256=d555d57ea6d7cf001d4774b693546a6bd88bec1e778ddffb5c60b68ceec4b7d4
+ prism (2.1.0) sha256=1c0a7c6dea0e93183390fb123f007564dbe663198e2f86ad87204151147601f3
+ prism (2.1.0-aarch64-linux-gnu) sha256=808dddc72def68e1a0463689414c97e0ba211938eddbab05387a4844aa2b3ec2
+ prism (2.1.0-aarch64-linux-musl) sha256=dd1a0732620c1e575dabb07a297a62911fabd72f8e20ce850b522a866f877930
+ prism (2.1.0-arm-linux-gnu) sha256=96b2426b9dbab05448c4add735619929042be3cd7e871ff186468121b62bcfa8
+ prism (2.1.0-arm-linux-musl) sha256=00ef918e42a3dc98f11a461bf473586a3424b0f78bee8a4e43b5a65a5f5e2cd0
+ prism (2.1.0-arm64-darwin) sha256=2d0c2e3e011cfd89c041940907d3ff3def379d7b0f2ba158d1d42238e8ad8afc
+ prism (2.1.0-x86_64-darwin) sha256=6c645d8e6afc742956c7518220ce5106367727c4d9980ac9ab386d55004ddab4
+ prism (2.1.0-x86_64-linux-gnu) sha256=b2b647afce09551e650b8bb3e2fc91d7d73d392a09534ca307ec0d619c33bfd4
+ prism (2.1.0-x86_64-linux-musl) sha256=249f45981726923e259b927902246219fa25ad10a76f43849c9aee1a3d3ec5b8
+ pstore (0.3.0) sha256=97888d57cdc8b99b3587dde5e1d01fc5ca4a9124dfeb2afb84e3dd982e78f829
+ public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
+ rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
+ rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674
+ rdoc (8.1.0) sha256=befc37880a5cfd36dcb176c748b5fd5ee39dd3536a75e81824e4972ce230f60a
+ reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
+ representable (3.3.1) sha256=8a5fc8eef99f598147eee0390f606386268b429a840419fcfd3f7a8ccc40f566
+ retriable (4.2.0) sha256=90c3b257472a1c02f2a3dfa64dd35a420f7a624cef1a5981e20fdac5730f8cdc
+ rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
+ rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20
+ rubyzip (3.7.0) sha256=65c19294da75297a939006f3516deacc33185fbd721ff1954f7a231db6d3e121
+ security (0.4.0) sha256=fd3fdbd2e50e45616082dbeec0bf0813b1134b2420a034ad35c3af2807f3fdbc
+ signet (0.22.2) sha256=9c6861bb599845ea0cb05ffadd3d64c7a3b07a29bdee0f5352bcbe87b7ade21d
+ simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b
+ terminal-notifier (3.1.0) sha256=073609e9da7bb60176ffadda903c8d9231fcdf18ffa7074dc9478b63fe25c69c
+ terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2
+ timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
+ trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3
+ tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
+ tty-cursor (0.7.1) sha256=79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48
+ tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50
+ tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542
+ uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc
+ unicode-display_width (3.3.0) sha256=4b7aa66a4b11db50f6f7e98411215cd0dd10eaecb665d36eec5bcacb3fa0b613
+ unicode-emoji (4.3.0) sha256=11c02fa73290378c066bb0562cd4c87d8e1b706fbbe0059ca12746a4244de8ce
+ uri (1.1.2) sha256=89292e98eabbef6248b0a0ed5e7e398e152f83f17560023a6d94a7f492b169fb
+ word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7
+ xcodeproj (1.28.1) sha256=6f12670f00739d9817ca27ac89d6ef01cc86050e22a0bc08a3131487e5b5cddc
+ xcpretty (0.4.1) sha256=b14c50e721f6589ee3d6f5353e2c2cfcd8541fa1ea16d6c602807dd7327f3892
+ xcpretty-travis-formatter (1.0.1) sha256=aacc332f17cb7b2cba222994e2adc74223db88724fe76341483ad3098e232f93
+
+BUNDLED WITH
+ 4.0.23
diff --git a/example/app.json b/example/app.json
index 5079e0f..5cd4cf2 100644
--- a/example/app.json
+++ b/example/app.json
@@ -69,13 +69,15 @@
"react-native-audio-api",
{
"iosMicrophonePermission": "Effects listens to the microphone so effects can react to your voice and to you blowing on the screen. Audio is analysed live on your iPhone and is never recorded or sent anywhere.",
- "iosBackgroundMode": false
+ "iosBackgroundMode": false,
+ "disableFFmpeg": true
}
],
[
"expo-camera",
{
"cameraPermission": "Effects uses the camera to take a photo for the instant camera effect. The photo stays on your iPhone and is never uploaded.",
+ "barcodeScannerEnabled": false,
"microphonePermission": "Effects listens to the microphone so effects can react to your voice and to you blowing on the screen. Audio is analysed live on your iPhone and is never recorded or sent anywhere."
}
],
diff --git a/example/assets/polaroid/photo-beach.jpg b/example/assets/camera/photo-beach.jpg
similarity index 100%
rename from example/assets/polaroid/photo-beach.jpg
rename to example/assets/camera/photo-beach.jpg
diff --git a/example/assets/polaroid/photo-lake.jpg b/example/assets/camera/photo-lake.jpg
similarity index 100%
rename from example/assets/polaroid/photo-lake.jpg
rename to example/assets/camera/photo-lake.jpg
diff --git a/example/assets/polaroid/photo-mountains.jpg b/example/assets/camera/photo-mountains.jpg
similarity index 100%
rename from example/assets/polaroid/photo-mountains.jpg
rename to example/assets/camera/photo-mountains.jpg
diff --git a/example/assets/polaroid/table.jpg b/example/assets/camera/table.jpg
similarity index 100%
rename from example/assets/polaroid/table.jpg
rename to example/assets/camera/table.jpg
diff --git a/example/assets/candle/cake.jpg b/example/assets/candle/cake.jpg
index bdf6d2a..15c788c 100644
Binary files a/example/assets/candle/cake.jpg and b/example/assets/candle/cake.jpg differ
diff --git a/example/assets/catalog/ai-shimmer.jpg b/example/assets/catalog/ai-shimmer.jpg
index 73dfd93..94bc4d0 100644
Binary files a/example/assets/catalog/ai-shimmer.jpg and b/example/assets/catalog/ai-shimmer.jpg differ
diff --git a/example/assets/catalog/banners.jpg b/example/assets/catalog/banners.jpg
index f578687..81061c0 100644
Binary files a/example/assets/catalog/banners.jpg and b/example/assets/catalog/banners.jpg differ
diff --git a/example/assets/catalog/beer.jpg b/example/assets/catalog/beer.jpg
index 3dd2324..5fdb7c4 100644
Binary files a/example/assets/catalog/beer.jpg and b/example/assets/catalog/beer.jpg differ
diff --git a/example/assets/catalog/birthday-candle-hero.jpg b/example/assets/catalog/birthday-candle-hero.jpg
index eaf0c37..0c7158f 100644
Binary files a/example/assets/catalog/birthday-candle-hero.jpg and b/example/assets/catalog/birthday-candle-hero.jpg differ
diff --git a/example/assets/catalog/birthday-candle.jpg b/example/assets/catalog/birthday-candle.jpg
index fd610d0..a8f9c92 100644
Binary files a/example/assets/catalog/birthday-candle.jpg and b/example/assets/catalog/birthday-candle.jpg differ
diff --git a/example/assets/catalog/circular-gradient.jpg b/example/assets/catalog/circular-gradient.jpg
deleted file mode 100644
index 8bf07c2..0000000
Binary files a/example/assets/catalog/circular-gradient.jpg and /dev/null differ
diff --git a/example/assets/catalog/cracked-screen-hero.jpg b/example/assets/catalog/cracked-screen-hero.jpg
index 44293ea..4a7e677 100644
Binary files a/example/assets/catalog/cracked-screen-hero.jpg and b/example/assets/catalog/cracked-screen-hero.jpg differ
diff --git a/example/assets/catalog/cracked-screen.jpg b/example/assets/catalog/cracked-screen.jpg
index 548c3e4..6b25b53 100644
Binary files a/example/assets/catalog/cracked-screen.jpg and b/example/assets/catalog/cracked-screen.jpg differ
diff --git a/example/assets/catalog/dandelion-hero.jpg b/example/assets/catalog/dandelion-hero.jpg
new file mode 100644
index 0000000..d9d0e18
Binary files /dev/null and b/example/assets/catalog/dandelion-hero.jpg differ
diff --git a/example/assets/catalog/duo-crackgate.jpg b/example/assets/catalog/duo-crackgate.jpg
deleted file mode 100644
index 2074552..0000000
Binary files a/example/assets/catalog/duo-crackgate.jpg and /dev/null differ
diff --git a/example/assets/catalog/thanos.jpg b/example/assets/catalog/dust.jpg
similarity index 100%
rename from example/assets/catalog/thanos.jpg
rename to example/assets/catalog/dust.jpg
diff --git a/example/assets/catalog/flashlight.jpg b/example/assets/catalog/flashlight.jpg
index 82aee33..7d1e919 100644
Binary files a/example/assets/catalog/flashlight.jpg and b/example/assets/catalog/flashlight.jpg differ
diff --git a/example/assets/catalog/foamlab.jpg b/example/assets/catalog/foamlab.jpg
deleted file mode 100644
index fed123e..0000000
Binary files a/example/assets/catalog/foamlab.jpg and /dev/null differ
diff --git a/example/assets/catalog/glow-orb.jpg b/example/assets/catalog/glow-orb.jpg
new file mode 100644
index 0000000..69495b2
Binary files /dev/null and b/example/assets/catalog/glow-orb.jpg differ
diff --git a/example/assets/catalog/hidden-text.jpg b/example/assets/catalog/hidden-text.jpg
new file mode 100644
index 0000000..7f53a49
Binary files /dev/null and b/example/assets/catalog/hidden-text.jpg differ
diff --git a/example/assets/catalog/hinge-crack.jpg b/example/assets/catalog/hinge-crack.jpg
new file mode 100644
index 0000000..f156142
Binary files /dev/null and b/example/assets/catalog/hinge-crack.jpg differ
diff --git a/example/assets/catalog/holo-card.jpg b/example/assets/catalog/holo-card.jpg
index 7376c0a..1ca80c6 100644
Binary files a/example/assets/catalog/holo-card.jpg and b/example/assets/catalog/holo-card.jpg differ
diff --git a/example/assets/catalog/imessage.jpg b/example/assets/catalog/imessage.jpg
deleted file mode 100644
index 924604a..0000000
Binary files a/example/assets/catalog/imessage.jpg and /dev/null differ
diff --git a/example/assets/catalog/polaroid.jpg b/example/assets/catalog/instant-camera.jpg
similarity index 100%
rename from example/assets/catalog/polaroid.jpg
rename to example/assets/catalog/instant-camera.jpg
diff --git a/example/assets/catalog/lighter-hero.jpg b/example/assets/catalog/lighter-hero.jpg
deleted file mode 100644
index 44bbaab..0000000
Binary files a/example/assets/catalog/lighter-hero.jpg and /dev/null differ
diff --git a/example/assets/catalog/linear-gradient-animated.jpg b/example/assets/catalog/linear-gradient-animated.jpg
deleted file mode 100644
index 20f685d..0000000
Binary files a/example/assets/catalog/linear-gradient-animated.jpg and /dev/null differ
diff --git a/example/assets/catalog/linear-gradient.jpg b/example/assets/catalog/linear-gradient.jpg
deleted file mode 100644
index ebf0485..0000000
Binary files a/example/assets/catalog/linear-gradient.jpg and /dev/null differ
diff --git a/example/assets/catalog/magic-8-ball.jpg b/example/assets/catalog/magic-8-ball.jpg
deleted file mode 100644
index e07606f..0000000
Binary files a/example/assets/catalog/magic-8-ball.jpg and /dev/null differ
diff --git a/example/assets/catalog/message-dissolve.jpg b/example/assets/catalog/message-dissolve.jpg
new file mode 100644
index 0000000..c08814c
Binary files /dev/null and b/example/assets/catalog/message-dissolve.jpg differ
diff --git a/example/assets/catalog/metal-card.jpg b/example/assets/catalog/metal-card.jpg
new file mode 100644
index 0000000..8675ddc
Binary files /dev/null and b/example/assets/catalog/metal-card.jpg differ
diff --git a/example/assets/catalog/mystic-ball.jpg b/example/assets/catalog/mystic-ball.jpg
new file mode 100644
index 0000000..ff5ca36
Binary files /dev/null and b/example/assets/catalog/mystic-ball.jpg differ
diff --git a/example/assets/catalog/namedrop.jpg b/example/assets/catalog/namedrop.jpg
deleted file mode 100644
index 69fc692..0000000
Binary files a/example/assets/catalog/namedrop.jpg and /dev/null differ
diff --git a/example/assets/catalog/scratch-off.jpg b/example/assets/catalog/scratch-off.jpg
index fa0036b..ac75ad0 100644
Binary files a/example/assets/catalog/scratch-off.jpg and b/example/assets/catalog/scratch-off.jpg differ
diff --git a/example/assets/catalog/scroll-reactive.jpg b/example/assets/catalog/scroll-reactive.jpg
deleted file mode 100644
index 4524e25..0000000
Binary files a/example/assets/catalog/scroll-reactive.jpg and /dev/null differ
diff --git a/example/assets/catalog/send-effects.jpg b/example/assets/catalog/send-effects.jpg
new file mode 100644
index 0000000..2a38d85
Binary files /dev/null and b/example/assets/catalog/send-effects.jpg differ
diff --git a/example/assets/catalog/siri-orb.jpg b/example/assets/catalog/siri-orb.jpg
deleted file mode 100644
index a641c72..0000000
Binary files a/example/assets/catalog/siri-orb.jpg and /dev/null differ
diff --git a/example/assets/catalog/telegram-dissolve.jpg b/example/assets/catalog/telegram-dissolve.jpg
deleted file mode 100644
index 23dd0ef..0000000
Binary files a/example/assets/catalog/telegram-dissolve.jpg and /dev/null differ
diff --git a/example/assets/catalog/telegram-spoiler.jpg b/example/assets/catalog/telegram-spoiler.jpg
deleted file mode 100644
index e041bd9..0000000
Binary files a/example/assets/catalog/telegram-spoiler.jpg and /dev/null differ
diff --git a/example/assets/catalog/voice-delete.jpg b/example/assets/catalog/voice-delete.jpg
index 12609f0..cc31d92 100644
Binary files a/example/assets/catalog/voice-delete.jpg and b/example/assets/catalog/voice-delete.jpg differ
diff --git a/example/assets/catalog/wallet.jpg b/example/assets/catalog/wallet.jpg
deleted file mode 100644
index 6524e99..0000000
Binary files a/example/assets/catalog/wallet.jpg and /dev/null differ
diff --git a/example/assets/catalog/wave-reveal.jpg b/example/assets/catalog/wave-reveal.jpg
new file mode 100644
index 0000000..c195cca
Binary files /dev/null and b/example/assets/catalog/wave-reveal.jpg differ
diff --git a/example/assets/catalog/weather.jpg b/example/assets/catalog/weather.jpg
index 300cb00..2a01e1c 100644
Binary files a/example/assets/catalog/weather.jpg and b/example/assets/catalog/weather.jpg differ
diff --git a/example/assets/charizard.png b/example/assets/charizard.png
deleted file mode 100644
index 2ba382c..0000000
Binary files a/example/assets/charizard.png and /dev/null differ
diff --git a/example/assets/chat/avatar-bars.png b/example/assets/chat/avatar-bars.png
new file mode 100644
index 0000000..e64f086
Binary files /dev/null and b/example/assets/chat/avatar-bars.png differ
diff --git a/example/assets/chat/avatar-peaks.png b/example/assets/chat/avatar-peaks.png
new file mode 100644
index 0000000..08ceabf
Binary files /dev/null and b/example/assets/chat/avatar-peaks.png differ
diff --git a/example/assets/chat/avatar-rings.png b/example/assets/chat/avatar-rings.png
new file mode 100644
index 0000000..b4b9714
Binary files /dev/null and b/example/assets/chat/avatar-rings.png differ
diff --git a/example/assets/chat/back.png b/example/assets/chat/back.png
new file mode 100644
index 0000000..b4342f9
Binary files /dev/null and b/example/assets/chat/back.png differ
diff --git a/example/assets/chat/copy.png b/example/assets/chat/copy.png
new file mode 100644
index 0000000..f57d293
Binary files /dev/null and b/example/assets/chat/copy.png differ
diff --git a/example/assets/chat/mic.png b/example/assets/chat/mic.png
new file mode 100644
index 0000000..17e5c6f
Binary files /dev/null and b/example/assets/chat/mic.png differ
diff --git a/example/assets/chat/pattern.png b/example/assets/chat/pattern.png
new file mode 100644
index 0000000..ac69dd5
Binary files /dev/null and b/example/assets/chat/pattern.png differ
diff --git a/example/assets/chat/photo-post.jpg b/example/assets/chat/photo-post.jpg
new file mode 100644
index 0000000..e8c6f55
Binary files /dev/null and b/example/assets/chat/photo-post.jpg differ
diff --git a/example/assets/chat/photo-reel.jpg b/example/assets/chat/photo-reel.jpg
new file mode 100644
index 0000000..69dcb9c
Binary files /dev/null and b/example/assets/chat/photo-reel.jpg differ
diff --git a/example/assets/chat/play.png b/example/assets/chat/play.png
new file mode 100644
index 0000000..cb7853a
Binary files /dev/null and b/example/assets/chat/play.png differ
diff --git a/example/assets/chat/plus.png b/example/assets/chat/plus.png
new file mode 100644
index 0000000..3a73c66
Binary files /dev/null and b/example/assets/chat/plus.png differ
diff --git a/example/assets/chat/reply.png b/example/assets/chat/reply.png
new file mode 100644
index 0000000..e6d3d02
Binary files /dev/null and b/example/assets/chat/reply.png differ
diff --git a/example/assets/chat/send.png b/example/assets/chat/send.png
new file mode 100644
index 0000000..ef5fa0f
Binary files /dev/null and b/example/assets/chat/send.png differ
diff --git a/example/assets/chat/trash.png b/example/assets/chat/trash.png
new file mode 100644
index 0000000..eaf1fa0
Binary files /dev/null and b/example/assets/chat/trash.png differ
diff --git a/example/assets/cracks/camera-fill.png b/example/assets/cracks/camera-fill.png
deleted file mode 100644
index 824eb89..0000000
Binary files a/example/assets/cracks/camera-fill.png and /dev/null differ
diff --git a/example/assets/cracks/flashlight-off-fill.png b/example/assets/cracks/flashlight-off-fill.png
deleted file mode 100644
index 6da5922..0000000
Binary files a/example/assets/cracks/flashlight-off-fill.png and /dev/null differ
diff --git a/example/assets/cracks/lock-fill.png b/example/assets/cracks/lock-fill.png
deleted file mode 100644
index 59689c9..0000000
Binary files a/example/assets/cracks/lock-fill.png and /dev/null differ
diff --git a/example/assets/cracks/message-fill.png b/example/assets/cracks/message-fill.png
deleted file mode 100644
index 2430cc7..0000000
Binary files a/example/assets/cracks/message-fill.png and /dev/null differ
diff --git a/example/assets/flashlight/camera.png b/example/assets/flashlight/camera.png
deleted file mode 100644
index 7d0e7cd..0000000
Binary files a/example/assets/flashlight/camera.png and /dev/null differ
diff --git a/example/assets/flashlight/flashlight-off.png b/example/assets/flashlight/flashlight-off.png
deleted file mode 100644
index f274b46..0000000
Binary files a/example/assets/flashlight/flashlight-off.png and /dev/null differ
diff --git a/example/assets/flashlight/torch-glyph.png b/example/assets/flashlight/torch-glyph.png
deleted file mode 100644
index c061934..0000000
Binary files a/example/assets/flashlight/torch-glyph.png and /dev/null differ
diff --git a/example/assets/foam.jpg b/example/assets/foam.jpg
index a780d89..1b2b00f 100644
Binary files a/example/assets/foam.jpg and b/example/assets/foam.jpg differ
diff --git a/example/assets/imessage/video.png b/example/assets/imessage/video.png
deleted file mode 100644
index cdcf269..0000000
Binary files a/example/assets/imessage/video.png and /dev/null differ
diff --git a/example/assets/instagram/back.png b/example/assets/instagram/back.png
deleted file mode 100644
index f5d99ef..0000000
Binary files a/example/assets/instagram/back.png and /dev/null differ
diff --git a/example/assets/instagram/bookmark.png b/example/assets/instagram/bookmark.png
deleted file mode 100644
index 5969fa1..0000000
Binary files a/example/assets/instagram/bookmark.png and /dev/null differ
diff --git a/example/assets/instagram/camera.png b/example/assets/instagram/camera.png
deleted file mode 100644
index b013874..0000000
Binary files a/example/assets/instagram/camera.png and /dev/null differ
diff --git a/example/assets/instagram/image.png b/example/assets/instagram/image.png
deleted file mode 100644
index 83987df..0000000
Binary files a/example/assets/instagram/image.png and /dev/null differ
diff --git a/example/assets/instagram/info.png b/example/assets/instagram/info.png
deleted file mode 100644
index 5a10819..0000000
Binary files a/example/assets/instagram/info.png and /dev/null differ
diff --git a/example/assets/instagram/mic.png b/example/assets/instagram/mic.png
deleted file mode 100644
index 9b89ac6..0000000
Binary files a/example/assets/instagram/mic.png and /dev/null differ
diff --git a/example/assets/instagram/phone.png b/example/assets/instagram/phone.png
deleted file mode 100644
index 7d9ca11..0000000
Binary files a/example/assets/instagram/phone.png and /dev/null differ
diff --git a/example/assets/instagram/play.png b/example/assets/instagram/play.png
deleted file mode 100644
index 1af7987..0000000
Binary files a/example/assets/instagram/play.png and /dev/null differ
diff --git a/example/assets/instagram/plus.png b/example/assets/instagram/plus.png
deleted file mode 100644
index 01601b5..0000000
Binary files a/example/assets/instagram/plus.png and /dev/null differ
diff --git a/example/assets/instagram/send.png b/example/assets/instagram/send.png
deleted file mode 100644
index e8ed077..0000000
Binary files a/example/assets/instagram/send.png and /dev/null differ
diff --git a/example/assets/instagram/share.png b/example/assets/instagram/share.png
deleted file mode 100644
index a4d32e8..0000000
Binary files a/example/assets/instagram/share.png and /dev/null differ
diff --git a/example/assets/instagram/sticker.png b/example/assets/instagram/sticker.png
deleted file mode 100644
index a2be709..0000000
Binary files a/example/assets/instagram/sticker.png and /dev/null differ
diff --git a/example/assets/instagram/trash.png b/example/assets/instagram/trash.png
deleted file mode 100644
index fcdf2ee..0000000
Binary files a/example/assets/instagram/trash.png and /dev/null differ
diff --git a/example/assets/instagram/video.png b/example/assets/instagram/video.png
deleted file mode 100644
index afea376..0000000
Binary files a/example/assets/instagram/video.png and /dev/null differ
diff --git a/example/assets/iphone-background.png b/example/assets/iphone-background.png
deleted file mode 100644
index b0f6cbe..0000000
Binary files a/example/assets/iphone-background.png and /dev/null differ
diff --git a/example/assets/namedrop/avatar.jpg b/example/assets/namedrop/avatar.jpg
deleted file mode 100644
index 6e9d919..0000000
Binary files a/example/assets/namedrop/avatar.jpg and /dev/null differ
diff --git a/example/assets/namedrop/home-atlas.jpg b/example/assets/namedrop/home-atlas.jpg
deleted file mode 100644
index a887b8f..0000000
Binary files a/example/assets/namedrop/home-atlas.jpg and /dev/null differ
diff --git a/example/assets/scenes/misty-path.jpg b/example/assets/scenes/misty-path.jpg
new file mode 100644
index 0000000..3dfa79d
Binary files /dev/null and b/example/assets/scenes/misty-path.jpg differ
diff --git a/example/assets/telegram/avatar.jpg b/example/assets/telegram/avatar.jpg
deleted file mode 100644
index 1b75837..0000000
Binary files a/example/assets/telegram/avatar.jpg and /dev/null differ
diff --git a/example/assets/telegram/back.png b/example/assets/telegram/back.png
deleted file mode 100644
index e0e05c3..0000000
Binary files a/example/assets/telegram/back.png and /dev/null differ
diff --git a/example/assets/telegram/checks.png b/example/assets/telegram/checks.png
deleted file mode 100644
index e24beae..0000000
Binary files a/example/assets/telegram/checks.png and /dev/null differ
diff --git a/example/assets/telegram/chevron.png b/example/assets/telegram/chevron.png
deleted file mode 100644
index b360798..0000000
Binary files a/example/assets/telegram/chevron.png and /dev/null differ
diff --git a/example/assets/telegram/close.png b/example/assets/telegram/close.png
deleted file mode 100644
index acf37fa..0000000
Binary files a/example/assets/telegram/close.png and /dev/null differ
diff --git a/example/assets/telegram/copy.png b/example/assets/telegram/copy.png
deleted file mode 100644
index fb0a5b1..0000000
Binary files a/example/assets/telegram/copy.png and /dev/null differ
diff --git a/example/assets/telegram/delete.png b/example/assets/telegram/delete.png
deleted file mode 100644
index f346151..0000000
Binary files a/example/assets/telegram/delete.png and /dev/null differ
diff --git a/example/assets/telegram/edit.png b/example/assets/telegram/edit.png
deleted file mode 100644
index 80f57d8..0000000
Binary files a/example/assets/telegram/edit.png and /dev/null differ
diff --git a/example/assets/telegram/forward.png b/example/assets/telegram/forward.png
deleted file mode 100644
index 7dd98df..0000000
Binary files a/example/assets/telegram/forward.png and /dev/null differ
diff --git a/example/assets/telegram/mic.png b/example/assets/telegram/mic.png
deleted file mode 100644
index 3487b05..0000000
Binary files a/example/assets/telegram/mic.png and /dev/null differ
diff --git a/example/assets/telegram/paperclip.png b/example/assets/telegram/paperclip.png
deleted file mode 100644
index fbfab27..0000000
Binary files a/example/assets/telegram/paperclip.png and /dev/null differ
diff --git a/example/assets/telegram/pattern.png b/example/assets/telegram/pattern.png
deleted file mode 100644
index c157d7d..0000000
Binary files a/example/assets/telegram/pattern.png and /dev/null differ
diff --git a/example/assets/telegram/pin.png b/example/assets/telegram/pin.png
deleted file mode 100644
index 4af88d2..0000000
Binary files a/example/assets/telegram/pin.png and /dev/null differ
diff --git a/example/assets/telegram/reply.png b/example/assets/telegram/reply.png
deleted file mode 100644
index 927e3ef..0000000
Binary files a/example/assets/telegram/reply.png and /dev/null differ
diff --git a/example/assets/telegram/select.png b/example/assets/telegram/select.png
deleted file mode 100644
index 714730f..0000000
Binary files a/example/assets/telegram/select.png and /dev/null differ
diff --git a/example/assets/telegram/sticker.png b/example/assets/telegram/sticker.png
deleted file mode 100644
index 9f1fa28..0000000
Binary files a/example/assets/telegram/sticker.png and /dev/null differ
diff --git a/example/assets/telegram/viewed.png b/example/assets/telegram/viewed.png
deleted file mode 100644
index c4f37a3..0000000
Binary files a/example/assets/telegram/viewed.png and /dev/null differ
diff --git a/example/assets/tools/generate-assets.py b/example/assets/tools/generate-assets.py
new file mode 100644
index 0000000..728489e
--- /dev/null
+++ b/example/assets/tools/generate-assets.py
@@ -0,0 +1,427 @@
+#!/usr/bin/env python3
+"""Draws the app's original assets. Needs Pillow and numpy.
+
+Usage: generate-assets.py
+
+ holds the CC0 sources listed in docs/credits.md:
+ cake.jpg "3rd birthday cake (Unsplash 10b8Lvvc-4g).jpg", 3840 px wide
+ post.jpg "Forest path at sunset (Unsplash).jpg"
+ reel.jpg "Misty path (Unsplash).jpg"
+"""
+import math
+import random
+import sys
+from pathlib import Path
+
+import numpy as np
+from PIL import Image, ImageDraw, ImageFilter
+
+ASSETS = Path(__file__).resolve().parent.parent
+ICON = 96
+SS = 4
+UNIT = ICON * SS / 24
+
+
+def canvas(size=ICON * SS):
+ image = Image.new('RGBA', (size, size), (0, 0, 0, 0))
+ return image, ImageDraw.Draw(image)
+
+
+def stroke(draw, points, color='white', width=2.0):
+ scaled = [(x * UNIT, y * UNIT) for x, y in points]
+ w = width * UNIT
+ draw.line(scaled, fill=color, width=round(w), joint='curve')
+ for x, y in (scaled[0], scaled[-1]):
+ draw.ellipse((x - w / 2, y - w / 2, x + w / 2, y + w / 2), fill=color)
+
+
+def arc(cx, cy, r, start, end, steps=48):
+ return [
+ (
+ cx + r * math.cos(math.radians(start + (end - start) * i / steps)),
+ cy + r * math.sin(math.radians(start + (end - start) * i / steps)),
+ )
+ for i in range(steps + 1)
+ ]
+
+
+def disc(draw, cx, cy, r, color):
+ draw.ellipse(
+ ((cx - r) * UNIT, (cy - r) * UNIT, (cx + r) * UNIT, (cy + r) * UNIT),
+ fill=color,
+ )
+
+
+def box(draw, x0, y0, x1, y1, radius, color):
+ draw.rounded_rectangle(
+ (x0 * UNIT, y0 * UNIT, x1 * UNIT, y1 * UNIT),
+ radius=radius * UNIT,
+ fill=color,
+ )
+
+
+def save_icon(image, path, size=ICON):
+ path.parent.mkdir(parents=True, exist_ok=True)
+ image.resize((size, size), Image.LANCZOS).save(path, optimize=True)
+
+
+def chat_icons():
+ def back(d):
+ stroke(d, [(15, 5), (8, 12), (15, 19)], width=2.4)
+
+ def plus(d):
+ stroke(d, [(12, 5), (12, 19)], width=2.2)
+ stroke(d, [(5, 12), (19, 12)], width=2.2)
+
+ def mic(d):
+ box(d, 8.5, 3, 15.5, 14.5, 3.5, 'white')
+ stroke(d, arc(12, 11, 7, 0, 180), width=1.9)
+ stroke(d, [(12, 18), (12, 21)], width=1.9)
+
+ def reply(d):
+ stroke(d, [(10, 5), (4, 11), (10, 17)], width=2.1)
+ stroke(d, [(4, 11), (13, 11)] + arc(13, 17, 6, -90, 0), width=2.1)
+
+ def copy(d):
+ box(d, 9, 9, 20, 20, 2.5, 'white')
+ box(d, 10.8, 10.8, 18.2, 18.2, 1.2, (0, 0, 0, 0))
+ stroke(d, [(5, 15), (4, 14), (4, 5), (5, 4), (14, 4), (15, 5)], width=1.8)
+
+ def trash(d):
+ stroke(d, [(4, 7), (20, 7)], width=2)
+ stroke(d, [(9.5, 7), (10, 4), (14, 4), (14.5, 7)], width=1.8)
+ stroke(d, [(6.5, 7), (7.5, 20), (16.5, 20), (17.5, 7)], width=2)
+ stroke(d, [(12, 11), (12, 16.5)], width=1.6)
+
+ def send(d):
+ stroke(d, [(12, 20), (12, 5)], width=2.4)
+ stroke(d, [(6, 11), (12, 5), (18, 11)], width=2.4)
+
+ def play(d):
+ d.polygon(
+ [(7 * UNIT, 4 * UNIT), (20 * UNIT, 12 * UNIT), (7 * UNIT, 20 * UNIT)],
+ fill='white',
+ )
+
+ for draw_icon in (back, plus, mic, reply, copy, trash, send, play):
+ image, draw = canvas()
+ draw_icon(draw)
+ save_icon(image, ASSETS / 'chat' / f'{draw_icon.__name__}.png')
+
+
+def chat_pattern():
+ image = Image.new('RGB', (736, 1139), (15, 22, 21))
+ draw = ImageDraw.Draw(image)
+ for row, y in enumerate(range(14, 1139, 28)):
+ for x in range(14 + (row % 2) * 14, 736, 28):
+ draw.ellipse((x - 1.5, y - 1.5, x + 1.5, y + 1.5), fill=(31, 42, 40))
+ image.save(ASSETS / 'chat' / 'pattern.png', optimize=True)
+
+
+def avatar(name, background, foreground, shape):
+ image, draw = canvas(320)
+ draw.ellipse((0, 0, 319, 319), fill=background)
+ if shape == 'peaks':
+ draw.polygon([(40, 240), (130, 90), (195, 190), (225, 150), (285, 240)], fill=foreground)
+ elif shape == 'rings':
+ for r in (110, 72, 34):
+ draw.ellipse((160 - r, 160 - r, 160 + r, 160 + r), outline=foreground, width=18)
+ elif shape == 'bars':
+ for i, h in enumerate((70, 130, 190, 110)):
+ x = 72 + i * 48
+ draw.rounded_rectangle((x, 160 - h / 2, x + 28, 160 + h / 2), 14, fill=foreground)
+ image.resize((160, 160), Image.LANCZOS).save(ASSETS / 'chat' / name, optimize=True)
+
+
+def cover_crop(image, width, height, focus_y=0.5):
+ scale = max(width / image.width, height / image.height)
+ resized = image.resize(
+ (round(image.width * scale), round(image.height * scale)), Image.LANCZOS
+ )
+ left = (resized.width - width) // 2
+ top = round((resized.height - height) * focus_y)
+ return resized.crop((left, top, left + width, top + height))
+
+
+def chat_photos(commons):
+ post = Image.open(commons / 'post.jpg').convert('RGB')
+ cover_crop(post, 720, 612).save(ASSETS / 'chat' / 'photo-post.jpg', quality=84)
+ reel = Image.open(commons / 'reel.jpg').convert('RGB')
+ cover_crop(reel, 480, 870).save(ASSETS / 'chat' / 'photo-reel.jpg', quality=84)
+
+
+CLOUD = (205, 214, 226, 255)
+CLOUD_DARK = (150, 161, 178, 255)
+SUN = (255, 196, 66, 255)
+MOON = (232, 226, 200, 255)
+WATER = (104, 190, 255, 255)
+SNOW = (226, 240, 255, 255)
+BOLT = (255, 214, 92, 255)
+
+
+def cloud(d, color=CLOUD, dy=0.0):
+ box(d, 3, 11 + dy, 21, 18 + dy, 3.5, color)
+ disc(d, 9, 11 + dy, 4, color)
+ disc(d, 14.5, 9.5 + dy, 5, color)
+
+
+def sun(d, cx=12, cy=12, r=4.6):
+ disc(d, cx, cy, r, SUN)
+ for i in range(8):
+ a = math.radians(i * 45)
+ stroke(
+ d,
+ [
+ (cx + (r + 2) * math.cos(a), cy + (r + 2) * math.sin(a)),
+ (cx + (r + 4) * math.cos(a), cy + (r + 4) * math.sin(a)),
+ ],
+ color=SUN,
+ width=1.5,
+ )
+
+
+def moon(d, cx=12, cy=12, r=7):
+ layer, ld = canvas()
+ disc(ld, cx, cy, r, MOON)
+ disc(ld, cx + r * 0.55, cy - r * 0.4, r * 0.85, (0, 0, 0, 0))
+ d._image.alpha_composite(layer)
+
+
+def drops(d, xs, color=WATER):
+ for x in xs:
+ stroke(d, [(x + 0.8, 18.5), (x - 0.8, 21.5)], color=color, width=1.5)
+
+
+def flakes(d, xs):
+ for x in xs:
+ disc(d, x, 20.5, 1.1, SNOW)
+
+
+def lines(d, rows, color=CLOUD):
+ for y, x0, x1 in rows:
+ stroke(d, [(x0, y), (x1, y)], color=color, width=1.8)
+
+
+def horizon_sun(d, arrow_up):
+ sun_y = 15
+ disc(d, 12, sun_y, 4.5, SUN)
+ box(d, 0, sun_y, 24, 24, 0, (0, 0, 0, 0))
+ lines(d, [(16.5, 3, 21)], color=SUN)
+ tip, tail = (3, 8) if arrow_up else (8, 3)
+ stroke(d, [(12, tail), (12, tip)], color=SNOW, width=1.6)
+ stroke(d, [(9.5, tip + (2.5 if arrow_up else -2.5)), (12, tip), (14.5, tip + (2.5 if arrow_up else -2.5))], color=SNOW, width=1.6)
+
+
+def weather_icons():
+ def shifted_moon(d):
+ moon(d, 16, 8, 6)
+
+ def clear_day(d):
+ sun(d)
+
+ def partly_cloudy_day(d):
+ sun(d, 16, 8, 3.8)
+ cloud(d, dy=1.5)
+
+ def cloudy(d):
+ cloud(d, CLOUD_DARK, dy=-3)
+ cloud(d, dy=1.5)
+
+ def haze(d):
+ sun(d, 12, 8, 3.4)
+ lines(d, [(15, 4, 20), (18.5, 7, 17)])
+
+ def fog(d):
+ lines(d, [(8, 5, 19), (12, 3, 16), (16, 8, 21), (20, 5, 15)])
+
+ def windy(d):
+ stroke(d, [(3, 9), (15, 9)] + arc(15, 6, 3, 90, -120), color=CLOUD)
+ stroke(d, [(3, 13), (19, 13)] + arc(19, 16, 3, -90, 120), color=CLOUD)
+ stroke(d, [(3, 17), (10, 17)], color=CLOUD)
+
+ def drizzle(d):
+ cloud(d, dy=-2)
+ drops(d, (9, 15))
+
+ def rain(d):
+ cloud(d, dy=-2)
+ drops(d, (7.5, 12, 16.5))
+
+ def heavy_rain(d):
+ cloud(d, CLOUD_DARK, dy=-2)
+ drops(d, (5.5, 9.5, 13.5, 17.5))
+
+ def thunderstorm(d):
+ cloud(d, CLOUD_DARK, dy=-3)
+ d.polygon(
+ [(c * UNIT, r * UNIT) for c, r in ((13.5, 13), (9, 19), (12, 19), (10.5, 23.5), (16, 17), (13, 17))],
+ fill=BOLT,
+ )
+
+ def snow(d):
+ cloud(d, dy=-2)
+ flakes(d, (8, 12, 16))
+
+ def heavy_snow(d):
+ cloud(d, CLOUD_DARK, dy=-2)
+ flakes(d, (6, 10, 14, 18))
+ disc(d, 8, 22.8, 1, SNOW)
+ disc(d, 16, 22.8, 1, SNOW)
+
+ def sleet(d):
+ cloud(d, dy=-2)
+ drops(d, (8, 16))
+ flakes(d, (12,))
+
+ def clear_night(d):
+ moon(d, 11, 12, 8)
+ disc(d, 19, 6, 0.9, SNOW)
+ disc(d, 20, 13, 0.7, SNOW)
+
+ def partly_cloudy_night(d):
+ shifted_moon(d)
+ cloud(d, dy=1.5)
+
+ def drizzle_night(d):
+ shifted_moon(d)
+ cloud(d, dy=-1)
+ drops(d, (9, 15))
+
+ def sunrise(d):
+ horizon_sun(d, True)
+
+ def sunset(d):
+ horizon_sun(d, False)
+
+ for draw_icon in (
+ clear_day, partly_cloudy_day, cloudy, haze, fog, windy, drizzle, rain,
+ heavy_rain, thunderstorm, snow, heavy_snow, sleet, clear_night,
+ partly_cloudy_night, drizzle_night, sunrise, sunset,
+ ):
+ image, draw = canvas()
+ draw._image = image
+ draw_icon(draw)
+ name = draw_icon.__name__.replace('_', '-')
+ save_icon(image, ASSETS / 'weather' / f'{name}.png')
+
+ image, draw = canvas()
+ stroke(draw, arc(12, 12, 9, 0, 360), color=(255, 255, 255, 170), width=2)
+ stroke(draw, [(12, 7), (12, 12), (16, 14)], color=(255, 255, 255, 170), width=2)
+ save_icon(image, ASSETS / 'weather' / 'clock.png')
+
+ image, draw = canvas()
+ box(draw, 3, 5, 21, 21, 3, (255, 255, 255, 170))
+ box(draw, 5, 10, 19, 19, 1.5, (0, 0, 0, 0))
+ stroke(draw, [(8, 3), (8, 6)], color=(255, 255, 255, 170), width=2)
+ stroke(draw, [(16, 3), (16, 6)], color=(255, 255, 255, 170), width=2)
+ save_icon(image, ASSETS / 'weather' / 'calendar.png')
+
+
+def foam():
+ random.seed(7)
+ size = 1536
+ seam = round(size * 0.615)
+ image = Image.new('RGB', (size, size), (250, 244, 228))
+ draw = ImageDraw.Draw(image)
+
+ for y in range(seam, size):
+ k = (y - seam) / (size - seam)
+ draw.line((0, y, size, y), fill=(int(238 - 14 * k), int(170 - 22 * k), int(24 + 4 * k)))
+
+ def foam_bubble(x, y, r):
+ draw.ellipse((x - r, y - r, x + r, y + r), fill=(236, 226, 200))
+ draw.ellipse((x - r, y - r, x + r, y + r), outline=(255, 252, 244), width=max(2, round(r * 0.14)))
+
+ for _ in range(1500):
+ foam_bubble(random.uniform(0, size), random.uniform(0, seam - 20), random.uniform(3, 12))
+ for _ in range(130):
+ foam_bubble(random.uniform(0, size), random.uniform(0, seam - 60), random.uniform(14, 44))
+ for _ in range(420):
+ x = random.uniform(0, size)
+ y = seam + random.gauss(-26, 22)
+ draw.ellipse((x - 9, y - 9, x + 9, y + 9), fill=(255, 250, 236))
+ foam_bubble(x, y, random.uniform(3, 15))
+
+ for _ in range(620):
+ x, y, r = random.uniform(0, size), random.uniform(seam + 30, size), random.uniform(3, 20)
+ draw.ellipse((x - r, y - r, x + r, y + r), fill=(246, 188, 42), outline=(190, 112, 12), width=max(2, round(r * 0.18)))
+ h = r * 0.34
+ draw.ellipse((x - h, y - r * 0.6, x + h, y - r * 0.6 + 2 * h), fill=(255, 244, 196))
+
+ result = image.filter(ImageFilter.GaussianBlur(1.6)).resize((768, 768), Image.LANCZOS)
+ pixels = np.asarray(result, dtype=np.float32)
+ grain = np.random.default_rng(7).normal(0, 2.2, pixels.shape[:2])[..., None]
+ Image.fromarray(np.clip(pixels + grain, 0, 255).astype(np.uint8)).save(ASSETS / 'foam.jpg', quality=88)
+
+
+def wax_top(x):
+ points = [
+ (1100, 840), (1180, 835), (1230, 850), (1270, 880), (1305, 898),
+ (1340, 902), (1375, 875), (1410, 858), (1460, 862), (1600, 880),
+ ]
+ xs, ys = zip(*points)
+ return np.interp(x, xs, ys)
+
+
+def cake(commons):
+ source = np.asarray(Image.open(commons / 'cake.jpg').convert('RGB'), dtype=np.float32)
+ height, width, _ = source.shape
+ background = source[60:140].mean(axis=(0, 1))
+ yy, xx = np.mgrid[0:height, 0:width]
+ in_glow = ((xx - 1300) / 240.0) ** 2 + ((yy - 700) / 340.0) ** 2 < 1
+ above_wax = yy < wax_top(xx) - 3
+ mask = Image.fromarray(((in_glow & above_wax) * 255).astype(np.uint8))
+ mask = np.asarray(mask.filter(ImageFilter.GaussianBlur(4)), dtype=np.float32)[..., None] / 255
+ noise = np.random.default_rng(3).normal(0, 1.1, source.shape[:2])[..., None]
+ source = source * (1 - mask) + (background + noise) * mask
+
+ crop_left, crop_width = 130, 2350
+ pad = round(crop_width * 1043 / 730) - height
+ top = background + np.random.default_rng(4).normal(0, 1.1, (pad, crop_width, 1))
+ full = np.concatenate([top, source[:, crop_left:crop_left + crop_width]], axis=0)
+ image = Image.fromarray(np.clip(full, 0, 255).astype(np.uint8))
+ image.resize((730, 1043), Image.LANCZOS).save(ASSETS / 'candle' / 'cake.jpg', quality=88)
+
+
+def wave_scenes(commons):
+ width, height = 1206, 2622
+ photo = cover_crop(Image.open(commons / 'reel.jpg').convert('RGB'), width, height, 0.5)
+ shade = Image.new('RGB', (width, height), (10, 26, 34))
+ scene = Image.blend(photo, shade, 0.34)
+ (ASSETS / 'scenes').mkdir(exist_ok=True)
+ scene.save(ASSETS / 'scenes' / 'misty-path.jpg', quality=86)
+
+ island = ImageDraw.Draw(atlas_panel := scene.copy())
+ pill_w, pill_h = 0.312 * width, 0.0408 * height
+ island.rounded_rectangle(
+ (width / 2 - pill_w / 2, 0.0376 * height - pill_h / 2, width / 2 + pill_w / 2, 0.0376 * height + pill_h / 2),
+ pill_h / 2,
+ fill=(0, 0, 0),
+ )
+ atlas = Image.new('RGB', (width * 3, height))
+ atlas.paste(atlas_panel, (0, 0))
+ atlas.paste(atlas_panel.filter(ImageFilter.GaussianBlur(22)), (width, 0))
+ atlas.paste(atlas_panel.filter(ImageFilter.GaussianBlur(60)), (width * 2, 0))
+ atlas.resize((2412, 1748), Image.LANCZOS).save(ASSETS / 'wave' / 'scene-atlas.jpg', quality=84)
+
+ swing = Image.open(ASSETS / 'cracks' / 'swing.jpg').convert('RGB')
+ dusk = swing.crop((820, 0, 1498, 1473)).resize((width, height), Image.LANCZOS)
+ dusk.save(ASSETS / 'wave' / 'dusk.jpg', quality=86)
+
+
+def main():
+ commons = Path(sys.argv[1])
+ chat_icons()
+ chat_pattern()
+ avatar('avatar-peaks.png', (34, 92, 84), (222, 240, 226), 'peaks')
+ avatar('avatar-rings.png', (64, 58, 110), (236, 230, 250), 'rings')
+ avatar('avatar-bars.png', (150, 104, 32), (255, 240, 212), 'bars')
+ chat_photos(commons)
+ weather_icons()
+ foam()
+ cake(commons)
+ wave_scenes(commons)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/example/assets/wave/dusk.jpg b/example/assets/wave/dusk.jpg
new file mode 100644
index 0000000..e12269e
Binary files /dev/null and b/example/assets/wave/dusk.jpg differ
diff --git a/example/assets/wave/scene-atlas.jpg b/example/assets/wave/scene-atlas.jpg
new file mode 100644
index 0000000..cb8ce8b
Binary files /dev/null and b/example/assets/wave/scene-atlas.jpg differ
diff --git a/example/assets/weather/calendar.png b/example/assets/weather/calendar.png
index f51b1d9..c614a1d 100644
Binary files a/example/assets/weather/calendar.png and b/example/assets/weather/calendar.png differ
diff --git a/example/assets/weather/clear-day.png b/example/assets/weather/clear-day.png
index 43fe86b..c92cbd3 100644
Binary files a/example/assets/weather/clear-day.png and b/example/assets/weather/clear-day.png differ
diff --git a/example/assets/weather/clear-night.png b/example/assets/weather/clear-night.png
index 1f8894d..2f08190 100644
Binary files a/example/assets/weather/clear-night.png and b/example/assets/weather/clear-night.png differ
diff --git a/example/assets/weather/clock.png b/example/assets/weather/clock.png
index 5461008..2c3ac50 100644
Binary files a/example/assets/weather/clock.png and b/example/assets/weather/clock.png differ
diff --git a/example/assets/weather/cloudy.png b/example/assets/weather/cloudy.png
index 3e14e9e..24c565c 100644
Binary files a/example/assets/weather/cloudy.png and b/example/assets/weather/cloudy.png differ
diff --git a/example/assets/weather/drizzle-night.png b/example/assets/weather/drizzle-night.png
index 79d7294..d575a6c 100644
Binary files a/example/assets/weather/drizzle-night.png and b/example/assets/weather/drizzle-night.png differ
diff --git a/example/assets/weather/drizzle.png b/example/assets/weather/drizzle.png
index 0d09492..e448a42 100644
Binary files a/example/assets/weather/drizzle.png and b/example/assets/weather/drizzle.png differ
diff --git a/example/assets/weather/fog.png b/example/assets/weather/fog.png
index 30f2e7d..1c725ec 100644
Binary files a/example/assets/weather/fog.png and b/example/assets/weather/fog.png differ
diff --git a/example/assets/weather/haze.png b/example/assets/weather/haze.png
index 85d709c..61faa39 100644
Binary files a/example/assets/weather/haze.png and b/example/assets/weather/haze.png differ
diff --git a/example/assets/weather/heavy-rain.png b/example/assets/weather/heavy-rain.png
index 665806d..1b7d211 100644
Binary files a/example/assets/weather/heavy-rain.png and b/example/assets/weather/heavy-rain.png differ
diff --git a/example/assets/weather/heavy-snow.png b/example/assets/weather/heavy-snow.png
index b9335a7..3f394e2 100644
Binary files a/example/assets/weather/heavy-snow.png and b/example/assets/weather/heavy-snow.png differ
diff --git a/example/assets/weather/partly-cloudy-day.png b/example/assets/weather/partly-cloudy-day.png
index 360d58f..6a88fea 100644
Binary files a/example/assets/weather/partly-cloudy-day.png and b/example/assets/weather/partly-cloudy-day.png differ
diff --git a/example/assets/weather/partly-cloudy-night.png b/example/assets/weather/partly-cloudy-night.png
index 2a8ece0..6d02f71 100644
Binary files a/example/assets/weather/partly-cloudy-night.png and b/example/assets/weather/partly-cloudy-night.png differ
diff --git a/example/assets/weather/rain.png b/example/assets/weather/rain.png
index 562f80d..1e012ff 100644
Binary files a/example/assets/weather/rain.png and b/example/assets/weather/rain.png differ
diff --git a/example/assets/weather/sleet.png b/example/assets/weather/sleet.png
index 7697260..6aad4bb 100644
Binary files a/example/assets/weather/sleet.png and b/example/assets/weather/sleet.png differ
diff --git a/example/assets/weather/snow.png b/example/assets/weather/snow.png
index ed4730c..13af37a 100644
Binary files a/example/assets/weather/snow.png and b/example/assets/weather/snow.png differ
diff --git a/example/assets/weather/sunrise.png b/example/assets/weather/sunrise.png
index 87b5adc..01140a5 100644
Binary files a/example/assets/weather/sunrise.png and b/example/assets/weather/sunrise.png differ
diff --git a/example/assets/weather/sunset.png b/example/assets/weather/sunset.png
index 2892be0..b0f9307 100644
Binary files a/example/assets/weather/sunset.png and b/example/assets/weather/sunset.png differ
diff --git a/example/assets/weather/thunderstorm.png b/example/assets/weather/thunderstorm.png
index a6936c9..d8c1a62 100644
Binary files a/example/assets/weather/thunderstorm.png and b/example/assets/weather/thunderstorm.png differ
diff --git a/example/assets/weather/windy.png b/example/assets/weather/windy.png
index 06b78bf..561d46b 100644
Binary files a/example/assets/weather/windy.png and b/example/assets/weather/windy.png differ
diff --git a/example/fastlane/Appfile b/example/fastlane/Appfile
new file mode 100644
index 0000000..551958c
--- /dev/null
+++ b/example/fastlane/Appfile
@@ -0,0 +1,3 @@
+app_identifier("com.blazejkustra.rneffects")
+apple_id(ENV["APPLE_ID"]) if ENV["APPLE_ID"]
+team_id(ENV["APPLE_TEAM_ID"]) if ENV["APPLE_TEAM_ID"]
diff --git a/example/fastlane/Fastfile b/example/fastlane/Fastfile
new file mode 100644
index 0000000..d83b6df
--- /dev/null
+++ b/example/fastlane/Fastfile
@@ -0,0 +1,66 @@
+default_platform(:ios)
+
+APP_ID = "com.blazejkustra.rneffects"
+WORKSPACE = "ios/Effects.xcworkspace"
+
+def next_build_number
+ latest_testflight_build_number(app_identifier: APP_ID, initial_build_number: 0) + 1
+end
+
+def api_key_auth_args
+ path = ENV["APP_STORE_CONNECT_API_KEY_PATH"]
+ return [] unless path
+ key = JSON.parse(File.read(File.expand_path(path)))
+ return [] unless key["key_filepath"]
+ [
+ "-authenticationKeyPath #{File.expand_path(key['key_filepath']).shellescape}",
+ "-authenticationKeyID #{key['key_id']}",
+ "-authenticationKeyIssuerID #{key['issuer_id']}"
+ ]
+end
+
+def signing_xcargs(build_number)
+ args = ["-allowProvisioningUpdates", "CURRENT_PROJECT_VERSION=#{build_number}"] + api_key_auth_args
+ args << "DEVELOPMENT_TEAM=#{ENV['APPLE_TEAM_ID']}" if ENV["APPLE_TEAM_ID"]
+ args.join(" ")
+end
+
+platform :ios do
+ private_lane :build do
+ Dir.chdir("..") do
+ sh("yarn", "prebuild:clean", "--platform", "ios")
+ end
+ build_number = next_build_number
+ UI.message("Building #{APP_ID} build #{build_number}")
+ build_app(
+ workspace: WORKSPACE,
+ scheme: "Effects",
+ configuration: "Release",
+ export_method: "app-store",
+ xcargs: signing_xcargs(build_number),
+ output_directory: "build/ios",
+ clean: true
+ )
+ end
+
+ desc "Build and upload to TestFlight"
+ lane :beta do
+ build
+ upload_to_testflight(skip_waiting_for_build_processing: true)
+ end
+
+ desc "Upload the listing and screenshots without a binary"
+ lane :listing do
+ upload_to_app_store(
+ app_version: "1.0.0",
+ metadata_path: "fastlane/metadata",
+ screenshots_path: "fastlane/screenshots",
+ skip_binary_upload: true,
+ overwrite_screenshots: true,
+ force: true,
+ submit_for_review: false,
+ automatic_release: false,
+ precheck_include_in_app_purchases: false
+ )
+ end
+end
diff --git a/example/fastlane/metadata/copyright.txt b/example/fastlane/metadata/copyright.txt
new file mode 100644
index 0000000..bdf6335
--- /dev/null
+++ b/example/fastlane/metadata/copyright.txt
@@ -0,0 +1 @@
+2026 Blazej Kustra
diff --git a/example/fastlane/metadata/en-US/description.txt b/example/fastlane/metadata/en-US/description.txt
new file mode 100644
index 0000000..21be57c
--- /dev/null
+++ b/example/fastlane/metadata/en-US/description.txt
@@ -0,0 +1,16 @@
+Effects is a pocket collection of interactive visual effects. Every one renders live on your iPhone's GPU, and many respond to you: touch, tilt, sound and motion.
+
+Things to try:
+
+- Tap and watch the screen glass crack where you hit it
+- Blow on your phone to send dandelion seeds drifting or put out a candle
+- Tilt to stir a lava lamp and slosh a full glass
+- Hold a magnet near your phone and watch ferrofluid reach for it
+- Shake an instant print while it develops
+- Scratch foil, pop bubble wrap, flip an hourglass
+
+There are also fabrics, liquid metal, northern lights, fire, ink and weather skies to play with, plus chat animations that turn messages to dust.
+
+The app has no accounts, no ads and no tracking.
+
+Sensor and microphone readings are used live for the animation and stay on your iPhone. The cracks, chats and cards are visual effects only: nothing happens to your device, and no messages or payments are sent.
diff --git a/example/fastlane/metadata/en-US/keywords.txt b/example/fastlane/metadata/en-US/keywords.txt
new file mode 100644
index 0000000..88dc625
--- /dev/null
+++ b/example/fastlane/metadata/en-US/keywords.txt
@@ -0,0 +1 @@
+interactive,physics,particles,satisfying,fidget,toy,art,animation,visual,sensory,relaxing,simulation
diff --git a/example/fastlane/metadata/en-US/marketing_url.txt b/example/fastlane/metadata/en-US/marketing_url.txt
new file mode 100644
index 0000000..fa40cbe
--- /dev/null
+++ b/example/fastlane/metadata/en-US/marketing_url.txt
@@ -0,0 +1 @@
+https://github.com/blazejkustra/react-native-effects
diff --git a/example/fastlane/metadata/en-US/name.txt b/example/fastlane/metadata/en-US/name.txt
new file mode 100644
index 0000000..049442a
--- /dev/null
+++ b/example/fastlane/metadata/en-US/name.txt
@@ -0,0 +1 @@
+Effects: shaders & fun
diff --git a/example/fastlane/metadata/en-US/privacy_url.txt b/example/fastlane/metadata/en-US/privacy_url.txt
new file mode 100644
index 0000000..865b46b
--- /dev/null
+++ b/example/fastlane/metadata/en-US/privacy_url.txt
@@ -0,0 +1 @@
+https://github.com/blazejkustra/react-native-effects/blob/main/docs/privacy-policy.md
diff --git a/example/fastlane/metadata/en-US/promotional_text.txt b/example/fastlane/metadata/en-US/promotional_text.txt
new file mode 100644
index 0000000..434a3e7
--- /dev/null
+++ b/example/fastlane/metadata/en-US/promotional_text.txt
@@ -0,0 +1 @@
+Dozens of live GPU effects you can touch. Crack the glass, blow out a candle, pop bubble wrap, stir a lava lamp. Every frame renders in real time on your iPhone.
diff --git a/example/fastlane/metadata/en-US/subtitle.txt b/example/fastlane/metadata/en-US/subtitle.txt
new file mode 100644
index 0000000..808cd7d
--- /dev/null
+++ b/example/fastlane/metadata/en-US/subtitle.txt
@@ -0,0 +1 @@
+Toys you tilt, blow & shake
diff --git a/example/fastlane/metadata/en-US/support_url.txt b/example/fastlane/metadata/en-US/support_url.txt
new file mode 100644
index 0000000..fb42cab
--- /dev/null
+++ b/example/fastlane/metadata/en-US/support_url.txt
@@ -0,0 +1 @@
+https://github.com/blazejkustra/react-native-effects/issues
diff --git a/example/fastlane/metadata/primary_category.txt b/example/fastlane/metadata/primary_category.txt
new file mode 100644
index 0000000..edbeebe
--- /dev/null
+++ b/example/fastlane/metadata/primary_category.txt
@@ -0,0 +1 @@
+ENTERTAINMENT
diff --git a/example/fastlane/screenshots/en-US/01-catalog.png b/example/fastlane/screenshots/en-US/01-catalog.png
new file mode 100644
index 0000000..beb2d0e
Binary files /dev/null and b/example/fastlane/screenshots/en-US/01-catalog.png differ
diff --git a/example/fastlane/screenshots/en-US/02-lava-lamp.png b/example/fastlane/screenshots/en-US/02-lava-lamp.png
new file mode 100644
index 0000000..63f985d
Binary files /dev/null and b/example/fastlane/screenshots/en-US/02-lava-lamp.png differ
diff --git a/example/fastlane/screenshots/en-US/03-cracked-screen.png b/example/fastlane/screenshots/en-US/03-cracked-screen.png
new file mode 100644
index 0000000..d2b9914
Binary files /dev/null and b/example/fastlane/screenshots/en-US/03-cracked-screen.png differ
diff --git a/example/fastlane/screenshots/en-US/04-bubble-wrap.png b/example/fastlane/screenshots/en-US/04-bubble-wrap.png
new file mode 100644
index 0000000..42319df
Binary files /dev/null and b/example/fastlane/screenshots/en-US/04-bubble-wrap.png differ
diff --git a/example/fastlane/screenshots/en-US/05-dandelion.png b/example/fastlane/screenshots/en-US/05-dandelion.png
new file mode 100644
index 0000000..3b43ced
Binary files /dev/null and b/example/fastlane/screenshots/en-US/05-dandelion.png differ
diff --git a/example/fastlane/screenshots/en-US/06-hourglass.png b/example/fastlane/screenshots/en-US/06-hourglass.png
new file mode 100644
index 0000000..5e3648b
Binary files /dev/null and b/example/fastlane/screenshots/en-US/06-hourglass.png differ
diff --git a/example/fastlane/screenshots/en-US/07-ferrofluid.png b/example/fastlane/screenshots/en-US/07-ferrofluid.png
new file mode 100644
index 0000000..2b601de
Binary files /dev/null and b/example/fastlane/screenshots/en-US/07-ferrofluid.png differ
diff --git a/example/goldie/flows/store-01-catalog.yaml b/example/goldie/flows/store-01-catalog.yaml
new file mode 100644
index 0000000..a759a28
--- /dev/null
+++ b/example/goldie/flows/store-01-catalog.yaml
@@ -0,0 +1,9 @@
+steps:
+ - launch: com.blazejkustra.rneffects
+ - await: { visible: { text: Search effects } }
+ - tool: gesture-swipe
+ args: { fromX: 0.5, fromY: 0.85, toX: 0.5, toY: 0.1, durationMs: 700, momentum: false }
+ - tool: gesture-swipe
+ args: { fromX: 0.5, fromY: 0.8, toX: 0.5, toY: 0.2, durationMs: 700, momentum: false }
+ - wait: 1500
+ - await: { idle: true }
diff --git a/example/goldie/flows/store-02-lava-lamp.yaml b/example/goldie/flows/store-02-lava-lamp.yaml
new file mode 100644
index 0000000..660f037
--- /dev/null
+++ b/example/goldie/flows/store-02-lava-lamp.yaml
@@ -0,0 +1,10 @@
+steps:
+ - launch: com.blazejkustra.rneffects
+ - await: { visible: { text: Search effects } }
+ - tap: { text: Search effects }
+ - wait: 1000
+ - tool: keyboard
+ args: { text: "lava" }
+ - wait: 500
+ - tap: { text: "Lava Lamp" }
+ - wait: 9000
diff --git a/example/goldie/flows/store-03-cracked-screen.yaml b/example/goldie/flows/store-03-cracked-screen.yaml
new file mode 100644
index 0000000..b2b4c14
--- /dev/null
+++ b/example/goldie/flows/store-03-cracked-screen.yaml
@@ -0,0 +1,19 @@
+steps:
+ - launch: com.blazejkustra.rneffects
+ - await: { visible: { text: Search effects } }
+ - tap: { text: Search effects }
+ - wait: 1000
+ - tool: keyboard
+ args: { text: "cracked" }
+ - wait: 500
+ - tap: { text: "Cracked Screen" }
+ - wait: 2000
+ - tool: gesture-tap
+ args: { x: 0.3, y: 0.3 }
+ - wait: 600
+ - tool: gesture-tap
+ args: { x: 0.7, y: 0.5 }
+ - wait: 600
+ - tool: gesture-tap
+ args: { x: 0.3, y: 0.62 }
+ - wait: 1500
diff --git a/example/goldie/flows/store-04-bubble-wrap.yaml b/example/goldie/flows/store-04-bubble-wrap.yaml
new file mode 100644
index 0000000..53d2469
--- /dev/null
+++ b/example/goldie/flows/store-04-bubble-wrap.yaml
@@ -0,0 +1,20 @@
+steps:
+ - launch: com.blazejkustra.rneffects
+ - await: { visible: { text: Search effects } }
+ - tap: { text: Search effects }
+ - wait: 1000
+ - tool: keyboard
+ args: { text: "bubble" }
+ - wait: 500
+ - tap: { text: "Bubble Wrap" }
+ - wait: 2000
+ - echo: Near-vertical drags, because a mostly sideways drag triggers the swipe-back gesture
+ - tool: gesture-swipe
+ args: { fromX: 0.3, fromY: 0.2, toX: 0.3, toY: 0.72, durationMs: 700, momentum: false }
+ - tool: gesture-swipe
+ args: { fromX: 0.42, fromY: 0.2, toX: 0.42, toY: 0.72, durationMs: 700, momentum: false }
+ - tool: gesture-swipe
+ args: { fromX: 0.54, fromY: 0.2, toX: 0.54, toY: 0.72, durationMs: 700, momentum: false }
+ - tool: gesture-swipe
+ args: { fromX: 0.66, fromY: 0.2, toX: 0.66, toY: 0.72, durationMs: 700, momentum: false }
+ - wait: 1500
diff --git a/example/goldie/flows/store-05-dandelion.yaml b/example/goldie/flows/store-05-dandelion.yaml
new file mode 100644
index 0000000..e15f237
--- /dev/null
+++ b/example/goldie/flows/store-05-dandelion.yaml
@@ -0,0 +1,14 @@
+steps:
+ - launch: com.blazejkustra.rneffects
+ - await: { visible: { text: Search effects } }
+ - tap: { text: Search effects }
+ - wait: 1000
+ - tool: keyboard
+ args: { text: "dandelion" }
+ - wait: 500
+ - tap: { text: "Dandelion" }
+ - wait: 2000
+ - echo: Allow on the microphone prompt when it shows. Without a prompt this tap lands on the intact head and does nothing
+ - tool: gesture-tap
+ args: { x: 0.668, y: 0.652 }
+ - wait: 2000
diff --git a/example/goldie/flows/store-06-hourglass.yaml b/example/goldie/flows/store-06-hourglass.yaml
new file mode 100644
index 0000000..8a71f7a
--- /dev/null
+++ b/example/goldie/flows/store-06-hourglass.yaml
@@ -0,0 +1,13 @@
+steps:
+ - launch: com.blazejkustra.rneffects
+ - await: { visible: { text: Search effects } }
+ - tap: { text: Search effects }
+ - wait: 1000
+ - tool: keyboard
+ args: { text: "hourglass" }
+ - wait: 500
+ - tap: { text: "Hourglass" }
+ - wait: 1000
+ - tap: { text: 5 minutes }
+ - echo: Let about a quarter of the sand fall
+ - wait: 75000
diff --git a/example/goldie/flows/store-07-ferrofluid.yaml b/example/goldie/flows/store-07-ferrofluid.yaml
new file mode 100644
index 0000000..54020dc
--- /dev/null
+++ b/example/goldie/flows/store-07-ferrofluid.yaml
@@ -0,0 +1,14 @@
+steps:
+ - launch: com.blazejkustra.rneffects
+ - await: { visible: { text: Search effects } }
+ - tap: { text: Search effects }
+ - wait: 1000
+ - tool: keyboard
+ args: { text: "ferro" }
+ - wait: 500
+ - tap: { text: "Ferrofluid" }
+ - wait: 2000
+ - echo: A held touch is the magnet, so the flow ends with the finger still down for the capture
+ - tool: gesture-custom
+ args: { events: [{ type: Down, x: 0.5, y: 0.45 }] }
+ - wait: 4000
diff --git a/example/goldie/flows/store-preview.yaml b/example/goldie/flows/store-preview.yaml
new file mode 100644
index 0000000..77707a8
--- /dev/null
+++ b/example/goldie/flows/store-preview.yaml
@@ -0,0 +1,68 @@
+executionPrerequisite: App freshly restarted by goldie, settled on the catalog home.
+steps:
+ - echo: Lift the finger the ferrofluid screenshot left down
+ - tool: gesture-custom
+ args: { events: [{ type: Up, x: 0.5, y: 0.45 }] }
+ - await: { visible: { text: Search effects } }
+ - tap: { text: Search effects }
+ - wait: 1000
+ - tool: keyboard
+ args: { text: "cracked" }
+ - wait: 300
+ - tap: { text: "Cracked Screen" }
+ - wait: 700
+ - tool: gesture-tap
+ args: { x: 0.3, y: 0.3 }
+ - wait: 500
+ - tool: gesture-tap
+ args: { x: 0.7, y: 0.5 }
+ - wait: 500
+ - tool: gesture-tap
+ args: { x: 0.3, y: 0.62 }
+ - wait: 800
+ - echo: Left edge swipe back to the search results
+ - tool: gesture-swipe
+ args: { fromX: 0.01, fromY: 0.5, toX: 0.95, toY: 0.5, durationMs: 300 }
+ - wait: 400
+ - tap: { text: Clear text }
+ - tool: keyboard
+ args: { text: "ferro" }
+ - wait: 300
+ - tap: { text: "Ferrofluid" }
+ - wait: 900
+ - tool: gesture-custom
+ args:
+ interpolate: 30
+ events:
+ - { type: Down, x: 0.5, y: 0.45 }
+ - { type: Move, x: 0.5, y: 0.45, delayMs: 1500 }
+ - { type: Move, x: 0.62, y: 0.55, delayMs: 700 }
+ - { type: Up, x: 0.62, y: 0.55, delayMs: 300 }
+ - wait: 300
+ - echo: Left edge swipe back to the search results
+ - tool: gesture-swipe
+ args: { fromX: 0.01, fromY: 0.5, toX: 0.95, toY: 0.5, durationMs: 300 }
+ - wait: 400
+ - tap: { text: Clear text }
+ - tool: keyboard
+ args: { text: "bloom" }
+ - wait: 300
+ - tap: { text: "Ink Bloom" }
+ - wait: 800
+ - tool: gesture-tap
+ args: { x: 0.35, y: 0.35 }
+ - wait: 1000
+ - tool: gesture-tap
+ args: { x: 0.65, y: 0.6 }
+ - wait: 1500
+ - echo: Left edge swipe back to the search results
+ - tool: gesture-swipe
+ args: { fromX: 0.01, fromY: 0.5, toX: 0.95, toY: 0.5, durationMs: 300 }
+ - wait: 400
+ - tap: { text: Clear text }
+ - tool: keyboard
+ args: { text: "lava" }
+ - wait: 300
+ - tap: { text: "Lava Lamp" }
+ - echo: End on a screen that keeps animating, because the recording drops a static tail
+ - wait: 2500
diff --git a/example/goldie/goldie.config.ts b/example/goldie/goldie.config.ts
new file mode 100644
index 0000000..d424651
--- /dev/null
+++ b/example/goldie/goldie.config.ts
@@ -0,0 +1,99 @@
+const screenshot = (
+ id: string,
+ flow: string,
+ headline: string,
+ subhead: string,
+ layout = 'hero'
+) => ({
+ kind: 'screenshot',
+ id,
+ flow,
+ layout,
+ headline: { 'en-US': headline },
+ subhead: { 'en-US': subhead },
+});
+
+export default {
+ appRoot: '..',
+ appPath:
+ process.env.EFFECTS_APP_PATH ??
+ '/tmp/rnfx-dd/Build/Products/Release-iphonesimulator/Effects.app',
+ bundleId: 'com.blazejkustra.rneffects',
+ flowsDir: 'flows',
+ devices: ['iphone-6.9'],
+ locales: ['en-US'],
+ appearance: 'dark',
+ frame: { variant: '17-pro-silver' },
+ theme: {
+ background:
+ 'linear-gradient(170deg, #1a1a1c 0%, #0b0b0c 55%, #000000 100%)',
+ headlineColor: '#f5f5f3',
+ subheadColor: '#a3a3a0',
+ fontFamily: 'Merriweather',
+ layout: 'hero',
+ copyHeightRatio: 0.2,
+ deviceWidthRatio: 0.82,
+ },
+ store: {
+ name: 'Effects',
+ subtitle: { 'en-US': 'Screens you can touch' },
+ developer: 'Blazej Kustra',
+ category: 'Entertainment',
+ ageRating: '13+',
+ description: {
+ 'en-US':
+ 'A collection of real-time effects that respond to touch, tilt, breath and voice.',
+ },
+ },
+ scenes: [
+ screenshot(
+ 'catalog',
+ 'store-01-catalog',
+ 'Tilt it. Blow on it. Break it.',
+ 'Effects that answer your touch.'
+ ),
+ screenshot(
+ 'lava-lamp',
+ 'store-02-lava-lamp',
+ 'A pocket lava lamp',
+ 'Warm wax rises and sinks. Tilt to stir it.'
+ ),
+ screenshot(
+ 'cracked-screen',
+ 'store-03-cracked-screen',
+ 'Crack the glass',
+ 'Tap and it breaks where you hit it.',
+ 'classic'
+ ),
+ screenshot(
+ 'bubble-wrap',
+ 'store-04-bubble-wrap',
+ 'Pop every bubble',
+ 'Drag a finger and hear each one snap.'
+ ),
+ screenshot(
+ 'dandelion',
+ 'store-05-dandelion',
+ 'Blow the seeds away',
+ 'Breathe on your phone and watch them drift.'
+ ),
+ screenshot(
+ 'hourglass',
+ 'store-06-hourglass',
+ 'Time it with sand',
+ 'Pick a length, tap to pause, flip to restart.',
+ 'classic'
+ ),
+ screenshot(
+ 'ferrofluid',
+ 'store-07-ferrofluid',
+ 'Touch to raise spikes',
+ 'Hold a finger down and the fluid stands up.'
+ ),
+ {
+ kind: 'preview',
+ id: 'preview',
+ segments: [{ id: 'tour', flow: 'store-preview' }],
+ },
+ ],
+};
diff --git a/example/package.json b/example/package.json
index 4ba7820..b840378 100644
--- a/example/package.json
+++ b/example/package.json
@@ -8,7 +8,9 @@
"android": "expo run:android",
"ios": "expo run:ios",
"prebuild": "expo prebuild",
- "prebuild:clean": "expo prebuild --clean"
+ "prebuild:clean": "expo prebuild --clean",
+ "beta:ios": "bundle exec fastlane ios beta",
+ "listing:ios": "bundle exec fastlane ios listing"
},
"dependencies": {
"@react-navigation/native": "7.2.5",
diff --git a/example/src/App.tsx b/example/src/App.tsx
index feee9be..c2cd48f 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -6,25 +6,19 @@ import {
} from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Platform, StyleSheet } from 'react-native';
-import type { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import HomeScreen from './screens/HomeScreen';
import SearchScreen from './screens/SearchScreen';
import AboutScreen from './screens/AboutScreen';
-import LinearGradientListScreen from './screens/LinearGradient/LinearGradientListScreen';
-import LinearGradientAnimatedScreen from './screens/LinearGradient/LinearGradientAnimatedScreen';
-import LinearGradientStaticScreen from './screens/LinearGradient/LinearGradientStaticScreen';
import IridescenceScreen from './screens/IridescenceScreen';
import LiquidChromeScreen from './screens/LiquidChromeScreen';
import SilkScreen from './screens/SilkScreen';
import CampfireScreen from './screens/CampfireScreen';
import CalicoSwirlScreen from './screens/CalicoSwirlScreen';
import AuroraScreen from './screens/AuroraScreen';
-import CircularGradientScreen from './screens/CircularGradientScreen';
import SiriOrbScreen from './screens/SiriOrbScreen';
import HoloCardScreen from './screens/HoloCardScreen';
import WeatherScreen from './screens/WeatherScreen';
-import ScrollReactiveScreen from './screens/ScrollReactiveScreen';
import TouchFieldScreen from './screens/TouchFieldScreen';
import ThanosScreen from './screens/ThanosScreen';
import AiShimmerScreen from './screens/AiShimmerScreen';
@@ -39,7 +33,6 @@ import LighterScreen from './screens/LighterScreen';
import TelegramDissolveScreen from './screens/TelegramDissolveScreen';
import BirthdayCandleScreen from './screens/BirthdayCandleScreen';
import ParticleDispersionScreen from './screens/ParticleDispersionScreen';
-import FoamLabScreen from './screens/FoamLabScreen';
import VoiceDeleteScreen from './screens/VoiceDeleteScreen';
import Magic8BallScreen from './screens/Magic8BallScreen';
import DandelionScreen from './screens/DandelionScreen';
@@ -73,16 +66,6 @@ const navTheme: Theme = {
},
};
-const listHeader = (title: string): NativeStackNavigationOptions => ({
- title,
- headerShown: true,
- headerTintColor: '#fff',
- headerShadowVisible: false,
- headerBackButtonDisplayMode: 'minimal',
- headerStyle: { backgroundColor: '#000' },
- headerTitleStyle: { color: '#fff' },
-});
-
export default function App() {
return (
@@ -109,20 +92,6 @@ export default function App() {
contentStyle: { backgroundColor: '#121212' },
}}
/>
-
-
-
-
-
+
-
-
+
@@ -162,7 +122,7 @@ export default function App() {
-
-
+
-
+
-
+
-
-
+
+
diff --git a/example/src/catalog/effects.ts b/example/src/catalog/effects.ts
index d6caf88..01f60b7 100644
--- a/example/src/catalog/effects.ts
+++ b/example/src/catalog/effects.ts
@@ -33,25 +33,10 @@ export const SECTIONS: CatalogSection[] = [
title: 'Tilt it.',
layout: 'posters',
effects: [
- {
- id: 'beer',
- title: 'Beer',
- line: 'Tilt to slosh, rotate to drink, tap to refill',
- screen: 'BeerExample',
- image: require('../../assets/catalog/beer.jpg'),
- },
- {
- id: 'lighter',
- title: 'Lighter',
- line: 'Tap to strike. The flame stays upright as you tilt',
- screen: 'LighterExample',
- image: require('../../assets/catalog/lighter.jpg'),
- hero: require('../../assets/catalog/lighter-hero.jpg'),
- },
{
id: 'hourglass',
title: 'Hourglass',
- line: 'Run a Pomodoro, then flip your phone to restart it',
+ line: 'Run a focus timer, then flip your phone to restart it',
screen: 'HourglassExample',
image: require('../../assets/catalog/hourglass.jpg'),
},
@@ -98,6 +83,7 @@ export const SECTIONS: CatalogSection[] = [
line: 'Blow on your phone and the seeds drift away',
screen: 'DandelionExample',
image: require('../../assets/catalog/dandelion.jpg'),
+ hero: require('../../assets/catalog/dandelion-hero.jpg'),
},
],
},
@@ -109,31 +95,31 @@ export const SECTIONS: CatalogSection[] = [
{
id: 'cracked-screen',
title: 'Cracked Screen',
- line: 'Tap hard and the glass breaks where you hit it',
+ line: 'Tap and the glass breaks where you hit it',
screen: 'CrackedScreenExample',
image: require('../../assets/catalog/cracked-screen.jpg'),
hero: require('../../assets/catalog/cracked-screen-hero.jpg'),
},
{
- id: 'duo-crackgate',
- title: 'Crackgate',
- line: 'The iPhone Duo hinge crack, white pixels and all',
- screen: 'DuoCrackgateExample',
- image: require('../../assets/catalog/duo-crackgate.jpg'),
+ id: 'hinge-crack',
+ title: 'Hinge Crack',
+ line: 'A folding screen gives out along its crease',
+ screen: 'HingeCrackExample',
+ image: require('../../assets/catalog/hinge-crack.jpg'),
},
{
id: 'crt-off',
title: 'CRT Off',
- line: 'Close the app like an old TV',
+ line: 'Switch off like an old TV',
screen: 'CrtOffExample',
image: require('../../assets/catalog/crt-off.jpg'),
},
{
- id: 'thanos',
- title: 'Thanos',
+ id: 'dust',
+ title: 'Dust',
line: 'Tap to snap a card into dust',
- screen: 'ThanosExample',
- image: require('../../assets/catalog/thanos.jpg'),
+ screen: 'DustExample',
+ image: require('../../assets/catalog/dust.jpg'),
},
{
id: 'particle-dispersion',
@@ -152,7 +138,7 @@ export const SECTIONS: CatalogSection[] = [
{
id: 'scratch-off',
title: 'Scratch-off',
- line: 'Scratch the foil to reveal your prize',
+ line: 'Scratch the foil to find three matching symbols',
screen: 'ScratchOffExample',
image: require('../../assets/catalog/scratch-off.jpg'),
},
@@ -166,7 +152,7 @@ export const SECTIONS: CatalogSection[] = [
{
id: 'flashlight',
title: 'Flashlight',
- line: 'The Dynamic Island torch. Swipe to shape the beam',
+ line: 'Tilt a beam around a dark corridor and shape it',
screen: 'FlashlightExample',
image: require('../../assets/catalog/flashlight.jpg'),
},
@@ -178,32 +164,39 @@ export const SECTIONS: CatalogSection[] = [
image: require('../../assets/catalog/ferrofluid.jpg'),
},
{
- id: 'wallet',
- title: 'Wallet',
- line: 'Double-click to pay, the card lifts and glints',
- screen: 'WalletPayExample',
- image: require('../../assets/catalog/wallet.jpg'),
+ id: 'metal-card',
+ title: 'Metal Card',
+ line: 'Tap and the card lifts as light runs across it',
+ screen: 'MetalCardExample',
+ image: require('../../assets/catalog/metal-card.jpg'),
},
{
- id: 'namedrop',
- title: 'NameDrop',
- line: 'Bring two iPhones together and swap contacts',
- screen: 'NameDropExample',
- image: require('../../assets/catalog/namedrop.jpg'),
+ id: 'wave-reveal',
+ title: 'Wave Reveal',
+ line: 'A wave of light carries you from morning to dusk',
+ screen: 'WaveRevealExample',
+ image: require('../../assets/catalog/wave-reveal.jpg'),
},
{
id: 'touch-field',
- title: 'Sauron Eye',
- line: 'The lidless Eye, wreathed in flame',
+ title: 'The Eye',
+ line: 'A burning eye that follows your finger',
screen: 'TouchFieldExample',
image: require('../../assets/catalog/touch-field.jpg'),
},
{
- id: 'scroll-reactive',
- title: 'Scroll Reactive',
- line: 'A background that reacts to your scroll',
- screen: 'ScrollReactiveExample',
- image: require('../../assets/catalog/scroll-reactive.jpg'),
+ id: 'lighter',
+ title: 'Lighter',
+ line: 'Tap to strike. The flame stays upright as you tilt',
+ screen: 'LighterExample',
+ image: require('../../assets/catalog/lighter.jpg'),
+ },
+ {
+ id: 'beer',
+ title: 'Beer',
+ line: 'Tilt to slosh, rotate to empty it, tap to refill',
+ screen: 'BeerExample',
+ image: require('../../assets/catalog/beer.jpg'),
},
],
},
@@ -213,18 +206,18 @@ export const SECTIONS: CatalogSection[] = [
layout: 'wide',
effects: [
{
- id: 'magic-8-ball',
- title: 'Magic 8-ball',
- line: 'Shake your phone and the answer floats up through the ink',
- screen: 'Magic8BallExample',
- image: require('../../assets/catalog/magic-8-ball.jpg'),
+ id: 'mystic-ball',
+ title: 'Mystic Ball',
+ line: 'Shake your phone and the answer surfaces through the mist',
+ screen: 'MysticBallExample',
+ image: require('../../assets/catalog/mystic-ball.jpg'),
},
{
- id: 'polaroid',
- title: 'Polaroid',
+ id: 'instant-camera',
+ title: 'Instant Camera',
line: 'Snap a photo and shake it while it develops',
- screen: 'PolaroidExample',
- image: require('../../assets/catalog/polaroid.jpg'),
+ screen: 'InstantCameraExample',
+ image: require('../../assets/catalog/instant-camera.jpg'),
},
],
},
@@ -236,16 +229,16 @@ export const SECTIONS: CatalogSection[] = [
{
id: 'ai-orb',
title: 'AI Orb',
- line: 'A morphing orb that listens while it thinks',
+ line: 'Morphing thinking blob',
screen: 'AiOrbExample',
image: require('../../assets/catalog/ai-orb.jpg'),
},
{
- id: 'siri-orb',
- title: 'Siri Orb',
+ id: 'glow-orb',
+ title: 'Glow Orb',
line: 'Luminous breathing sphere with swirling colors',
- screen: 'SiriOrbStatic',
- image: require('../../assets/catalog/siri-orb.jpg'),
+ screen: 'GlowOrbStatic',
+ image: require('../../assets/catalog/glow-orb.jpg'),
},
{
id: 'ai-voice',
@@ -269,25 +262,25 @@ export const SECTIONS: CatalogSection[] = [
layout: 'rows',
effects: [
{
- id: 'imessage',
- title: 'iMessage Effects',
- line: 'Hold send to pick lasers, confetti, fireworks and more',
- screen: 'IMessageExample',
- image: require('../../assets/catalog/imessage.jpg'),
+ id: 'send-effects',
+ title: 'Message Flourishes',
+ line: 'Pick a chip and your message lands with a flourish',
+ screen: 'SendEffectsExample',
+ image: require('../../assets/catalog/send-effects.jpg'),
},
{
- id: 'telegram-spoiler',
- title: 'Telegram Spoiler',
+ id: 'hidden-text',
+ title: 'Hidden Text',
line: 'Tap the hidden text to reveal it for a moment',
- screen: 'TelegramSpoilerExample',
- image: require('../../assets/catalog/telegram-spoiler.jpg'),
+ screen: 'HiddenTextExample',
+ image: require('../../assets/catalog/hidden-text.jpg'),
},
{
- id: 'telegram-dissolve',
- title: 'Telegram Dissolve',
+ id: 'message-dissolve',
+ title: 'Message Dissolve',
line: 'Delete a message and watch it turn to dust',
- screen: 'TelegramDissolveExample',
- image: require('../../assets/catalog/telegram-dissolve.jpg'),
+ screen: 'MessageDissolveExample',
+ image: require('../../assets/catalog/message-dissolve.jpg'),
},
{
id: 'voice-delete',
@@ -299,7 +292,7 @@ export const SECTIONS: CatalogSection[] = [
{
id: 'banners',
title: 'Banners',
- line: 'iOS notification banners with shader effects',
+ line: 'Alert banners with living shader backgrounds',
screen: 'BannersExample',
image: require('../../assets/catalog/banners.jpg'),
},
@@ -356,7 +349,7 @@ export const SECTIONS: CatalogSection[] = [
{
id: 'weather',
title: 'Weather',
- line: 'A full forecast app with living skies',
+ line: 'Skies that change with the weather',
screen: 'WeatherExample',
image: require('../../assets/catalog/weather.jpg'),
},
@@ -374,27 +367,6 @@ export const SECTIONS: CatalogSection[] = [
screen: 'MaterialsExample',
image: require('../../assets/catalog/materials.jpg'),
},
- {
- id: 'foamlab',
- title: 'Beer Foam',
- line: 'Three foam treatments side by side',
- screen: 'FoamLabExample',
- image: require('../../assets/catalog/foamlab.jpg'),
- },
- {
- id: 'circular-gradient',
- title: 'Circular Gradient',
- line: 'Customizable radial gradients',
- screen: 'CircularGradientList',
- image: require('../../assets/catalog/circular-gradient.jpg'),
- },
- {
- id: 'linear-gradient',
- title: 'Linear Gradient',
- line: 'Smooth linear gradients, still or animated',
- screen: 'LinearGradientList',
- image: require('../../assets/catalog/linear-gradient.jpg'),
- },
],
},
];
@@ -404,7 +376,7 @@ const ALL_EFFECTS = SECTIONS.flatMap((section) => section.effects);
const FEATURED_IDS = [
'lava-lamp',
'cracked-screen',
- 'lighter',
+ 'dandelion',
'birthday-candle',
'aurora',
];
@@ -422,8 +394,13 @@ const ALPHABETICAL_ENTRIES = SECTIONS.flatMap((section) =>
}))
).sort((a, b) => a.effect.title.localeCompare(b.effect.title));
+const MIN_QUERY_LENGTH = 2;
+
export function searchEffects(query: string) {
const needle = query.trim().toLowerCase();
+ if (needle.length < MIN_QUERY_LENGTH) {
+ return ALL_EFFECTS;
+ }
return ALPHABETICAL_ENTRIES.filter((entry) =>
entry.haystack.includes(needle)
).map((entry) => entry.effect);
diff --git a/example/src/components/BeerGlass.tsx b/example/src/components/BeerGlass.tsx
index 4f20eb2..06d8839 100644
--- a/example/src/components/BeerGlass.tsx
+++ b/example/src/components/BeerGlass.tsx
@@ -23,10 +23,8 @@ type Props = Omit<
foamColor?: ColorInput;
};
-// The head is a photograph of foam (iBeer's own trick — every procedural
-// head read as drawn). Foam over beer, its lacy bottom line at 0.615 of the
-// height; the shader keys the photo's beer out. Stock image: clear its
-// licence before shipping.
+// Foam over beer, drawn by assets/tools/generate-assets.py, its lacy bottom
+// line at 0.615 of the height. The shader keys the image's beer out.
const FOAM_PHOTO = require('../../assets/foam.jpg');
/**
diff --git a/example/src/components/BirthdayFireworks.tsx b/example/src/components/BirthdayFireworks.tsx
index 2fd425b..5e961e1 100644
--- a/example/src/components/BirthdayFireworks.tsx
+++ b/example/src/components/BirthdayFireworks.tsx
@@ -9,7 +9,7 @@ type Props = ViewProps & {
};
/**
- * The iOS birthday-notification banner: glitter-dust fireworks popping at
+ * A birthday banner: glitter-dust fireworks popping at
* random spots in the dark, behind the notification text. Each burst is an
* expanding sphere of twinkling sparkle cells — no particles, just a hashed
* glitter field gated by the shell's radius — with a brief white flash at
@@ -17,8 +17,8 @@ type Props = ViewProps & {
* silver, gold) picked per burst.
*/
export default function BirthdayFireworks({
- caption = 'Birthday · Found in Contacts',
- title = 'Today, Jun 9',
+ caption = 'Happy birthday',
+ title = 'Make a wish',
style,
...rest
}: Props) {
@@ -76,7 +76,7 @@ fn main(@location(0) ndc: vec2) -> @location(0) vec4 {
);
// Six launchers on offset clocks → a few distinct balls in flight, with
- // black space between them like the real notification.
+ // black space between them.
for (var i = 0; i < 6; i = i + 1) {
let fi = f32(i);
let period = 2.9 + 1.7 * fract(fi * 0.6180339);
diff --git a/example/src/components/CoinGeyser.tsx b/example/src/components/CoinGeyser.tsx
index 1b58003..9b48f3d 100644
--- a/example/src/components/CoinGeyser.tsx
+++ b/example/src/components/CoinGeyser.tsx
@@ -13,7 +13,7 @@ type Props = ViewProps & {
// with each burst.
const ERUPTION_PERIOD_MS = 5200;
-// More money, more coins: one coin per $300, within the shader's budget.
+// One shader coin per 300 saved, within the shader's budget.
const coinsForAmount = (amount: number) =>
Math.min(64, Math.max(10, Math.round(amount / 300)));
@@ -99,10 +99,7 @@ export default function CoinGeyser({
return () => clearInterval(id);
}, [scale]);
- const title = `$${display.toLocaleString('en-US', {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- })}`;
+ const title = `${Math.round(display).toLocaleString('en-US')} coins`;
return (
diff --git a/example/src/components/ScrollReactive.tsx b/example/src/components/ScrollReactive.tsx
deleted file mode 100644
index e878eb0..0000000
--- a/example/src/components/ScrollReactive.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import { useMemo } from 'react';
-import type { ViewProps } from 'react-native';
-import {
- ShaderView,
- type ColorInput,
- type ParamsSynchronizable,
-} from 'react-native-effects';
-
-type Props = ViewProps & {
- /**
- * Live scroll channel written into `u.live` every frame: `x` = progress
- * (0..1), `y` = rubber-band overscroll. Drive it with `setParamsSynchronizable` from a scroll
- * handler (see `useParamsSynchronizable`) so scrolling never re-renders React.
- */
- paramsSynchronizable?: ParamsSynchronizable;
- /** Contour line color (and the index lines). */
- colorA?: ColorInput;
- /** Secondary tone for the index lines and the low-ground wash. */
- colorB?: ColorInput;
- /** Animation speed multiplier. Default: 1.0 */
- speed?: number;
-};
-
-/**
- * An animated topographic contour map. A noise elevation field is sliced into
- * thin survey-map contour lines (every 5th an "index" line) drawn in `colorA`
- * over black. `progress` (typically a normalized scroll offset) pans the terrain
- * and tightens the contours, so the background drifts as the user scrolls.
- *
- * Lives in the example app rather than the library — it is a demo of what you
- * can build on top of `ShaderView`, not a general-purpose library primitive.
- */
-export default function ScrollReactive({
- paramsSynchronizable,
- colorA = '#3457ff',
- colorB = '#c026d3',
- speed = 1.0,
- ...viewProps
-}: Props) {
- const colors = useMemo(() => [colorA, colorB], [colorA, colorB]);
-
- return (
-
- );
-}
-
-const SCROLL_REACTIVE_SHADER = /* wgsl */ `
-struct Uniforms {
- resolution: vec4,
- time: vec4,
- color0: vec4,
- color1: vec4,
- params0: vec4,
- params1: vec4,
- live: vec4,
-};
-@group(0) @binding(0) var u: Uniforms;
-
-fn hash21(p: vec2) -> f32 {
- var p3 = fract(vec3(p.xyx) * 0.1031);
- p3 = p3 + dot(p3, p3.yzx + 33.33);
- return fract((p3.x + p3.y) * p3.z);
-}
-
-fn vnoise(p: vec2) -> f32 {
- let i = floor(p);
- let f = fract(p);
- let u2 = f * f * (3.0 - 2.0 * f);
- let a = hash21(i);
- let b = hash21(i + vec2(1.0, 0.0));
- let c = hash21(i + vec2(0.0, 1.0));
- let d = hash21(i + vec2(1.0, 1.0));
- return mix(mix(a, b, u2.x), mix(c, d, u2.x), u2.y);
-}
-
-fn fbm(p0: vec2) -> f32 {
- var p = p0;
- var v = 0.0;
- var a = 0.5;
- let m = mat2x2(1.6, 1.2, -1.2, 1.6);
- for (var i = 0; i < 5; i = i + 1) {
- v = v + a * vnoise(p);
- p = m * p;
- a = a * 0.5;
- }
- return v;
-}
-
-@fragment
-fn main(@location(0) ndc: vec2) -> @location(0) vec4 {
- let t = u.time.x;
- let aspect = u.resolution.z;
- let uv = ndc * 0.5 + 0.5;
- var p = (uv - 0.5) * vec2(aspect, 1.0);
-
- // Live scroll channel (u.live): x = progress 0..1, y = overscroll.
- let prog = clamp(u.live.x, 0.0, 1.0);
- // Rubber-band overscroll (screen-heights): negative at the top, positive at
- // the bottom. Lets the terrain keep reacting past either end of the list.
- let over = u.live.y;
-
- // A slowly evolving elevation field. Scrolling pans the terrain upward and
- // lifts the relief, so the contour map drifts and tightens as you read.
- p = p * 1.7;
- // Scroll gently pans the terrain — keep this small or a flick of the
- // scrollview rockets the whole field across the screen. Overscroll adds an
- // extra springy pan at both ends.
- p.y = p.y + prog * 0.3 + over * 1.6;
- var h = fbm(p + vec2(t * 0.012, t * 0.008));
- h = fbm(p + vec2(h, h) * (0.5 + prog * 0.15 + abs(over) * 0.5) + vec2(0.0, t * 0.01));
-
- // Slice the field into topographic contour lines — density barely shifts
- // with scroll so the map stays calm.
- let bands = 25.0 + prog * 2.0;
- let f = h * bands;
- let d = abs(fract(f) - 0.5); // 0 exactly on a contour line
- let aa = fwidth(f) * 1.1; // screen-space anti-aliasing falloff
- let thickness = 0.14; // half-width of each contour line
- let lineMask = 1.0 - smoothstep(thickness, thickness + aa, d);
-
- // Every 5th line is a brighter "index" contour, like a real survey map.
- let idx = floor(f);
- let isMajor = select(0.0, 1.0, (idx - 5.0 * floor(idx / 5.0)) < 0.5);
-
- let lineCol = mix(u.color0.rgb, u.color1.rgb, isMajor * 0.5);
- var col = lineCol * lineMask * (0.5 + isMajor * 0.5);
-
- // A faint warm wash pooling in the low ground gives quiet depth between lines.
- col = col + u.color1.rgb * smoothstep(0.55, 0.0, h) * 0.04;
-
- // Vignette to settle the edges into black.
- let vd = uv - vec2(0.5, 0.5);
- col = col * clamp(1.0 - dot(vd, vd) * 0.8, 0.4, 1.0);
-
- // A whisper of grain so the darks never band on real displays.
- let grain = hash21(uv * u.resolution.xy + fract(t) * 311.0) - 0.5;
- col = col + grain * 0.01;
-
- return vec4(clamp(col, vec3(0.0), vec3(1.0)), 1.0);
-}
-`;
diff --git a/example/src/components/SiriEdgeGlow.tsx b/example/src/components/SiriEdgeGlow.tsx
deleted file mode 100644
index 3a74cdd..0000000
--- a/example/src/components/SiriEdgeGlow.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-import { useMemo } from 'react';
-import type { ViewProps } from 'react-native';
-import { ShaderView } from 'react-native-effects';
-
-type Props = ViewProps & {
- /** Animation speed multiplier. Default: 1.0 */
- speed?: number;
- /** Activity level 0.0 (idle) to 1.0 (listening). Default: 0.0 */
- activity?: number;
-};
-
-export default function SiriEdgeGlow({
- speed = 1.0,
- activity = 0.0,
- ...viewProps
-}: Props) {
- const params = useMemo(() => [activity], [activity]);
-
- return (
-
- );
-}
-
-const SIRI_EDGE_GLOW_SHADER = /* wgsl */ `
-struct Uniforms {
- resolution: vec4,
- time: vec4,
- color0: vec4,
- color1: vec4,
- params0: vec4,
- params1: vec4,
-};
-@group(0) @binding(0) var u: Uniforms;
-
-fn hash(p: vec2) -> f32 {
- var p3 = fract(vec3(p.x, p.y, p.x) * 0.1031);
- p3 += dot(p3, vec3(p3.y + 33.33, p3.z + 33.33, p3.x + 33.33));
- return fract((p3.x + p3.y) * p3.z);
-}
-
-fn noise(p: vec2) -> f32 {
- let i = floor(p);
- let f = fract(p);
- let s = f * f * (3.0 - 2.0 * f);
- return mix(
- mix(hash(i), hash(i + vec2(1.0, 0.0)), s.x),
- mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), s.x),
- s.y
- );
-}
-
-fn fbm3(p_in: vec2) -> f32 {
- var p = p_in;
- var v = 0.0;
- var a = 0.5;
- for (var i = 0; i < 3; i++) {
- v += a * noise(p);
- p = p * 2.01 + vec2(1.7, 9.2);
- a *= 0.5;
- }
- return v;
-}
-
-fn siriPalette(t: f32) -> vec3 {
- let magenta = vec3(0.95, 0.05, 0.55);
- let purple = vec3(0.50, 0.05, 0.95);
- let cyan = vec3(0.0, 0.85, 0.95);
- let pink = vec3(1.0, 0.25, 0.55);
- let blue = vec3(0.15, 0.25, 0.95);
-
- let tt = fract(t);
- if (tt < 0.2) {
- return mix(magenta, purple, tt / 0.2);
- } else if (tt < 0.4) {
- return mix(purple, blue, (tt - 0.2) / 0.2);
- } else if (tt < 0.6) {
- return mix(blue, cyan, (tt - 0.4) / 0.2);
- } else if (tt < 0.8) {
- return mix(cyan, pink, (tt - 0.6) / 0.2);
- }
- return mix(pink, magenta, (tt - 0.8) / 0.2);
-}
-
-// Rounded rectangle signed distance field
-// p = point, b = half-size, r = corner radius
-fn sdRoundedRect(p: vec2, b: vec2, r: f32) -> f32 {
- let q = abs(p) - b + vec2(r);
- return length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - r;
-}
-
-@fragment
-fn main(@location(0) ndc: vec2) -> @location(0) vec4 {
- let time = u.time.x;
- let activity = clamp(u.params0.x, 0.0, 1.0);
- let aspect = u.resolution.z;
-
- let uv = ndc * 0.5 + 0.5;
-
- // Work in pixel-like coords centered at screen middle
- // p goes from (-0.5, -0.5) to (0.5, 0.5)
- let p = vec2((uv.x - 0.5) * aspect, uv.y - 0.5);
-
- // Rounded rect matching iPhone shape
- // Half-size of the screen rect, corner radius ~0.06 normalized
- let halfSize = vec2(aspect * 0.5, 0.5);
- let cornerR = 0.07;
-
- // SDF: negative inside, positive outside, 0 on border
- let d = sdRoundedRect(p, halfSize, cornerR);
-
- // We want glow only near the border (d ~ 0), fading inward
- // d is negative inside the rect; border is at d=0
- // Glow peaks near the edge and fades toward center
- let borderDist = abs(d);
-
- // ---- Noise for wispy organic inner edge ----
- let n1 = fbm3(uv * 8.0 + vec2(time * 0.1, time * 0.07)) - 0.5;
- let n2 = fbm3(uv * 5.0 + vec2(-time * 0.08, time * 0.12) + 3.0) - 0.5;
- let warp = (n1 * 0.6 + n2 * 0.4) * 0.012 * (1.0 + activity * 0.5);
-
- // ---- Main glow: tight gaussian around the rounded border ----
- let warpedDist = borderDist + warp;
- let glowWidth = 0.025;
- let glow = exp(-warpedDist * warpedDist / (glowWidth * glowWidth));
-
- // ---- Color: rotating around the perimeter ----
- // Use atan2 of centered position for perimeter angle
- let sa = sin(atan2(p.y, p.x));
- let ca = cos(atan2(p.y, p.x));
- let perimT = (sa * 0.5 + ca * 0.3) * 0.5 + 0.5;
- let rot = time * 0.18;
- let edgeColor = siriPalette(perimT + rot);
-
- // ---- Breathing pulse (synced with SiriOrb) ----
- let pulseSpeed = 2.2 + activity * 1.8;
- let pulse = 0.85 + 0.15 * sin(time * pulseSpeed);
-
- // ---- Secondary inner glow (dimmer, offset inward) ----
- let innerDist = max(0.0, -d - 0.015) + warp * 0.5;
- let innerGlow = exp(-innerDist * innerDist / (0.018 * 0.018)) * 0.25;
- let innerColor = siriPalette(perimT + rot + 0.12);
-
- // ---- Compose ----
- let intensity = 0.6 + activity * 0.3;
- var col = edgeColor * glow * pulse * intensity;
- col += innerColor * innerGlow * pulse * intensity;
-
- // Only show inside the screen (d < 0)
- let insideMask = smoothstep(0.002, -0.002, d);
- col *= insideMask;
-
- let alpha = clamp((glow + innerGlow) * pulse * intensity * insideMask, 0.0, 1.0);
-
- return vec4(col, alpha);
-}
-`;
diff --git a/example/src/components/SiriOrb.tsx b/example/src/components/SiriOrb.tsx
index 2c5f2da..46dee56 100644
--- a/example/src/components/SiriOrb.tsx
+++ b/example/src/components/SiriOrb.tsx
@@ -4,9 +4,9 @@ import { ShaderView } from 'react-native-effects';
import type { ColorInput } from 'react-native-effects';
type Props = ViewProps & {
- /** Primary orb color (magenta/purple). */
+ /** Primary orb color. */
color?: ColorInput;
- /** Accent color for chromatic edges (cyan). */
+ /** Second orb color, swirled through the first. */
accentColor?: ColorInput;
/** Animation speed multiplier. Default: 1.0 */
speed?: number;
@@ -17,8 +17,8 @@ type Props = ViewProps & {
};
export default function SiriOrb({
- color = '#a855f6',
- accentColor = '#06b6d4',
+ color = '#d9a441',
+ accentColor = '#0f6b5a',
speed = 1.0,
activity = 0.0,
bloom = 0.6,
@@ -29,7 +29,7 @@ export default function SiriOrb({
return (
,
time: vec4,
@@ -80,26 +80,8 @@ fn fbm3(p_in: vec2) -> f32 {
return v;
}
-// Palette: vivid neon Siri colors
-fn siriPalette(t: f32) -> vec3 {
- // Deep magenta -> electric purple -> hot cyan -> warm pink -> back
- let magenta = vec3(0.95, 0.05, 0.55);
- let purple = vec3(0.50, 0.05, 0.95);
- let cyan = vec3(0.0, 0.85, 0.95);
- let pink = vec3(1.0, 0.25, 0.55);
- let blue = vec3(0.15, 0.25, 0.95);
-
- let tt = fract(t);
- if (tt < 0.2) {
- return mix(magenta, purple, tt / 0.2);
- } else if (tt < 0.4) {
- return mix(purple, blue, (tt - 0.2) / 0.2);
- } else if (tt < 0.6) {
- return mix(blue, cyan, (tt - 0.4) / 0.2);
- } else if (tt < 0.8) {
- return mix(cyan, pink, (tt - 0.6) / 0.2);
- }
- return mix(pink, magenta, (tt - 0.8) / 0.2);
+fn orbPalette(t: f32) -> vec3 {
+ return mix(u.color0.rgb, u.color1.rgb, 0.5 + 0.5 * sin(t * 6.2831853));
}
@fragment
@@ -121,33 +103,19 @@ fn main(@location(0) ndc: vec2) -> @location(0) vec4 {
let radius = 0.22 + breathe;
// ---- Internal swirling colors ----
- // Use sin/cos of angle to avoid atan2 discontinuity
let warpAmt = 1.5 + activity * 1.5;
- let angle = atan2(centered.y, centered.x);
- let sa = sin(angle);
- let ca = cos(angle);
-
- // Seamless polar-like coords using sin/cos (no wrapping seam)
- let polar1 = vec2(ca + dist * 2.0, sa + dist * 2.0);
- let polar2 = vec2(sa * 1.5 + dist * 3.0, ca * 1.5 - dist * 3.0);
// Two layers of warped noise for rich color variation
- let warp1 = fbm3(polar1 + vec2(time * 0.2, time * 0.15)) - 0.5;
- let warp2 = fbm3(polar2 + vec2(-time * 0.18, time * 0.22) + 5.0) - 0.5;
-
- // Color index: use sin/cos blend instead of raw angle to stay seamless
- let colorIdx = (sa * 0.5 + ca * 0.3) * 0.5 + 0.5 + warp1 * warpAmt * 0.3 + warp2 * warpAmt * 0.2 + time * 0.05;
+ let warp1 = fbm3(centered * 3.5 + vec2(time * 0.2, time * 0.15)) - 0.5;
+ let warp2 = fbm3(centered * 6.0 + vec2(-time * 0.18, time * 0.22) + 5.0) - 0.5;
- // Get vivid palette color
- var orbCol = siriPalette(colorIdx);
+ let colorIdx = warp1 * warpAmt + warp2 * warpAmt * 0.6 + time * 0.05;
- // Boost saturation and vibrancy
- let lum = dot(orbCol, vec3(0.299, 0.587, 0.114));
- orbCol = mix(vec3(lum), orbCol, 1.6);
+ var orbCol = orbPalette(colorIdx);
// Add radial brightness variation (brighter center)
let normDist = dist / radius;
- let coreBright = 1.0 + (1.0 - smoothstep(0.0, 0.7, normDist)) * 0.8;
+ let coreBright = 1.0 + (1.0 - smoothstep(0.0, 0.8, normDist)) * 0.3;
orbCol *= coreBright;
// Add subtle noise shimmer
@@ -161,49 +129,32 @@ fn main(@location(0) ndc: vec2) -> @location(0) vec4 {
// Bright rim at the edge (Fresnel-like)
let rimStart = radius - edgeWidth * 3.0;
let rim = smoothstep(rimStart, radius, dist) * (1.0 - smoothstep(radius, radius + edgeWidth, dist));
- let rimColor = siriPalette(colorIdx + 0.3) * 1.5;
+ let rimColor = orbPalette(colorIdx + 0.3) * 1.5;
// ---- Specular highlights ----
let sp1 = vec2(cos(time * 0.6) * 0.1, sin(time * 0.8) * 0.08);
- let spec1 = exp(-length(centered - sp1) * length(centered - sp1) * 350.0) * 1.2;
+ let spec1 = exp(-length(centered - sp1) * length(centered - sp1) * 350.0) * 0.35;
let sp2 = vec2(cos(time * -0.4 + 2.0) * 0.06, sin(time * 0.55 + 1.0) * 0.07);
- let spec2 = exp(-length(centered - sp2) * length(centered - sp2) * 600.0) * 0.7;
+ let spec2 = exp(-length(centered - sp2) * length(centered - sp2) * 600.0) * 0.2;
// ---- Compose orb ----
var col = orbCol * mask;
- col += rimColor * rim * 0.6;
+ col += rimColor * rim * 0.3;
col += vec3(1.0, 0.95, 1.0) * (spec1 + spec2) * mask;
// ---- Outer glow / bloom ----
- // Multi-colored glow that bleeds past the edge
let glowFalloff = 80.0;
let gd = max(0.0, dist - radius + 0.01);
let glowMask = exp(-gd * gd * glowFalloff) * bloomIntensity;
- // Chromatic glow: different colors in different directions (seamless)
- let glowCol = siriPalette((sa * 0.5 + ca * 0.3) * 0.5 + 0.5 + time * 0.03);
- col += glowCol * glowMask * 0.7;
+ col += u.color0.rgb * glowMask * (1.0 - mask) * 0.7;
// Wide subtle ambient glow
let ambientGlow = exp(-dist * dist * 12.0) * 0.08 * bloomIntensity;
- let ambientCol = siriPalette(time * 0.02 + 0.5);
- col += ambientCol * ambientGlow;
-
- // ---- Chromatic aberration at edges ----
- let caStr = smoothstep(radius * 0.5, radius * 1.2, dist) * 0.02;
- let caOff = normalize(centered + vec2(0.0001)) * caStr;
-
- let distR = length(centered + caOff);
- let distB = length(centered - caOff);
- let maskR = 1.0 - smoothstep(radius - edgeWidth, radius + edgeWidth * 0.5, distR);
- let maskB = 1.0 - smoothstep(radius - edgeWidth, radius + edgeWidth * 0.5, distB);
-
- // Shift red outward, blue inward for energy feel
- col.r = col.r * 0.5 + orbCol.r * coreBright * maskR * 0.5;
- col.b = col.b * 0.5 + orbCol.b * coreBright * maskB * 0.5;
+ col += u.color0.rgb * ambientGlow;
- // Tone mapping — preserve vibrancy
+ // Tone mapping
col = pow(col, vec3(0.92));
col = clamp(col, vec3(0.0), vec3(1.0));
diff --git a/example/src/components/StreakFire.tsx b/example/src/components/StreakFire.tsx
index bd5f2d4..487742b 100644
--- a/example/src/components/StreakFire.tsx
+++ b/example/src/components/StreakFire.tsx
@@ -15,7 +15,7 @@ type Props = ViewProps & {
* climb. The blaze stays bottom-weighted so the title keeps its contrast.
*/
export default function StreakFire({
- caption = 'Duolingo · Don’t lose your streak',
+ caption = 'Sprout · Keep it going',
title = '365 day streak 🔥',
style,
...rest
diff --git a/example/src/components/ThinkingOrb.tsx b/example/src/components/ThinkingOrb.tsx
index d4bce6e..27891ff 100644
--- a/example/src/components/ThinkingOrb.tsx
+++ b/example/src/components/ThinkingOrb.tsx
@@ -3,7 +3,7 @@ import { ShaderView, type ShaderViewProps } from 'react-native-effects';
type Props = Omit;
/**
- * A Gemini-style "thinking" blob — a soft metaball that gently morphs its shape
+ * A "thinking" blob, a soft metaball that gently morphs its shape
* with a smooth dark→blue gradient fill, an inner moving highlight and a clean
* single-colour glow (no plasma spilling outside, no hard rim). Render it
* `transparent` in a square container. Pure procedural — no texture sampling.
diff --git a/example/src/components/Thunderstorm.tsx b/example/src/components/Thunderstorm.tsx
index ebdd239..31e8894 100644
--- a/example/src/components/Thunderstorm.tsx
+++ b/example/src/components/Thunderstorm.tsx
@@ -9,15 +9,15 @@ type Props = ViewProps & {
};
/**
- * A severe-weather banner with a live storm inside. A brooding fbm cloud deck
+ * A stormy weather banner with a live storm inside. A brooding fbm cloud deck
* hangs across the top; every couple of seconds a branching lightning bolt
* tears a noise-driven path from the clouds to the bottom edge, strobing
* 2–3 times and flash-lighting the whole card (clouds included) while faint
* rain streaks fall through the glow.
*/
export default function Thunderstorm({
- caption = 'Weather Alert · Severe',
- title = 'Storm until 9 PM',
+ caption = 'Weather · Rainy evening',
+ title = 'Thunder later tonight',
style,
...rest
}: Props) {
diff --git a/example/src/components/cracks/EffectCaption.tsx b/example/src/components/cracks/EffectCaption.tsx
new file mode 100644
index 0000000..f4bc7fe
--- /dev/null
+++ b/example/src/components/cracks/EffectCaption.tsx
@@ -0,0 +1,31 @@
+import { StyleSheet, Text, View } from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+/** Stays on screen so nobody can read the damage as their real display. */
+export default function EffectCaption() {
+ const insets = useSafeAreaInsets();
+ return (
+
+ Just an effect. Your screen is fine.
+
+ );
+}
+
+const styles = StyleSheet.create({
+ pill: {
+ position: 'absolute',
+ alignSelf: 'center',
+ paddingHorizontal: 14,
+ paddingVertical: 7,
+ borderRadius: 16,
+ backgroundColor: 'rgba(0,0,0,0.62)',
+ },
+ text: {
+ color: '#fff',
+ fontSize: 13,
+ fontWeight: '600',
+ },
+});
diff --git a/example/src/components/cracks/LockScreen.tsx b/example/src/components/cracks/LockScreen.tsx
index b253095..46d3c72 100644
--- a/example/src/components/cracks/LockScreen.tsx
+++ b/example/src/components/cracks/LockScreen.tsx
@@ -8,36 +8,7 @@ import {
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
-const GLYPHS = {
- lock: require('../../../assets/cracks/lock-fill.png'),
- flashlight: require('../../../assets/cracks/flashlight-off-fill.png'),
- camera: require('../../../assets/cracks/camera-fill.png'),
- message: require('../../../assets/cracks/message-fill.png'),
-};
-
-const DAYS = [
- 'Sunday',
- 'Monday',
- 'Tuesday',
- 'Wednesday',
- 'Thursday',
- 'Friday',
- 'Saturday',
-];
-const MONTHS = [
- 'January',
- 'February',
- 'March',
- 'April',
- 'May',
- 'June',
- 'July',
- 'August',
- 'September',
- 'October',
- 'November',
- 'December',
-];
+const SERIF = 'Georgia';
function pad(n: number) {
return n < 10 ? `0${n}` : `${n}`;
@@ -55,39 +26,12 @@ function useClock(live: boolean) {
const id = setInterval(() => setNow(new Date()), TICK_MS);
return () => clearInterval(id);
}, [live]);
- return {
- time: `${pad(now.getHours())}:${pad(now.getMinutes())}`,
- date: `${DAYS[now.getDay()]} ${now.getDate()} ${MONTHS[now.getMonth()]}`,
- };
-}
-
-function Glyph({
- source,
- height,
- aspect,
- color = '#fff',
-}: {
- source: ImageSourcePropType;
- height: number;
- aspect: number;
- color?: string;
-}) {
- return (
-
- );
+ return `${pad(now.getHours())} ${pad(now.getMinutes())}`;
}
type LockScreenProps = {
wallpaper: ImageSourcePropType;
ref?: Ref>;
- /** Side by side layout of the unfolded iPhone Duo inner panel. */
- landscape?: boolean;
- /** Show a Messages notification above the bottom buttons. */
- notification?: { title: string; body: string };
clockColor?: string;
/** Black wash over the wallpaper, 0..1. */
dim?: number;
@@ -97,85 +41,33 @@ type LockScreenProps = {
};
/**
- * A static clone of the iOS 26 lock screen (lock glyph, date,
- * clock, flashlight and camera buttons, home indicator) over a wallpaper
- * photo. Rendered as plain views so it can be snapshotted into a texture.
+ * An abstract scene for the glass effects: a full-bleed photo with a small
+ * serif clock in the corner. Rendered as plain views so it can be snapshotted
+ * into a texture.
*/
export default function LockScreen({
wallpaper,
ref,
- landscape = false,
- notification,
- clockColor = 'rgba(255,255,255,0.94)',
+ clockColor = 'rgba(255,255,255,0.9)',
dim = 0.12,
liveClock = false,
onLayout,
}: LockScreenProps) {
const insets = useSafeAreaInsets();
- const { time, date } = useClock(liveClock);
- const top = Math.max(insets.top, 20);
- const clockSize = landscape ? 104 : 96;
+ const time = useClock(liveClock);
return (
-
-
-
- {date}
-
- {time}
-
-
-
- {notification ? (
-
-
-
-
-
-
- {notification.title}
- now
-
-
- {notification.body}
-
-
-
- ) : null}
-
-
-
-
-
-
-
-
-
-
-
+ {time}
+
);
}
@@ -195,86 +87,12 @@ const styles = StyleSheet.create({
...StyleSheet.absoluteFill,
backgroundColor: '#000',
},
- head: {
- alignItems: 'center',
- },
- date: {
- marginTop: 8,
- color: 'rgba(255,255,255,0.92)',
- fontSize: 20,
- fontWeight: '600',
- },
clock: {
- fontWeight: '700',
- letterSpacing: -2,
- fontVariant: ['tabular-nums'],
- },
- notification: {
position: 'absolute',
- left: 12,
- right: 12,
- flexDirection: 'row',
- alignItems: 'center',
- padding: 13,
- gap: 11,
- borderRadius: 24,
- backgroundColor: 'rgba(40,40,46,0.55)',
- },
- appIcon: {
- width: 38,
- height: 38,
- borderRadius: 9,
- backgroundColor: '#34C759',
- alignItems: 'center',
- justifyContent: 'center',
- },
- notificationText: {
- flex: 1,
- },
- notificationRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- },
- notificationTitle: {
- color: '#fff',
- fontSize: 15,
- fontWeight: '600',
- },
- notificationTime: {
- color: 'rgba(255,255,255,0.6)',
- fontSize: 13,
- },
- notificationBody: {
- color: '#fff',
- fontSize: 15,
- marginTop: 1,
- },
- bottomRow: {
- position: 'absolute',
- left: 46,
- right: 46,
- flexDirection: 'row',
- justifyContent: 'space-between',
- },
- bottomRowWide: {
- left: 60,
- right: 60,
- },
- roundButton: {
- width: 50,
- height: 50,
- borderRadius: 25,
- backgroundColor: 'rgba(30,30,34,0.42)',
- alignItems: 'center',
- justifyContent: 'center',
- },
- homeIndicator: {
- position: 'absolute',
- bottom: 8,
- alignSelf: 'center',
- width: 140,
- height: 5,
- borderRadius: 3,
- backgroundColor: 'rgba(255,255,255,0.9)',
+ left: 24,
+ fontFamily: SERIF,
+ fontSize: 44,
+ fontStyle: 'italic',
+ fontVariant: ['tabular-nums'],
},
});
diff --git a/example/src/components/dandelion/Dandelion.tsx b/example/src/components/dandelion/Dandelion.tsx
index 3bc61c7..6f4c825 100644
--- a/example/src/components/dandelion/Dandelion.tsx
+++ b/example/src/components/dandelion/Dandelion.tsx
@@ -37,7 +37,7 @@ type DandelionProps = Omit<
* translated by the wind distance since its band let go, drifting up,
* tumbling slowly and fading out.
*
- * Same fragment-only particle trick as the Telegram dissolve: the wind is a
+ * Same fragment-only particle trick as the message dissolve: the wind is a
* smooth field of the origin, inverted per fragment to find the neighbourhood
* of cells that could land here, then gathered from a 5x5 window.
*/
diff --git a/example/src/components/dispersion/ParticleDispersion.tsx b/example/src/components/dispersion/ParticleDispersion.tsx
index 5489f75..29f5c5e 100644
--- a/example/src/components/dispersion/ParticleDispersion.tsx
+++ b/example/src/components/dispersion/ParticleDispersion.tsx
@@ -42,7 +42,7 @@ type ParticleDispersionProps = Omit<
* from the top down both sides while the cut-out shrinks a little and settles
* in the middle of the screen.
*
- * Same fragment-only particle trick as the Telegram dissolve: each ~1.3pt
+ * Same fragment-only particle trick as the message dissolve: each ~1.3pt
* cell of the background is one grain, released in order of its distance
* from the subject (precomputed into the atlas), carried by a smooth wind
* field that is inverted per fragment, and gathered from a 5×5 cell window.
diff --git a/example/src/components/flashlight/TorchControls.tsx b/example/src/components/flashlight/TorchControls.tsx
new file mode 100644
index 0000000..c0d6c81
--- /dev/null
+++ b/example/src/components/flashlight/TorchControls.tsx
@@ -0,0 +1,182 @@
+import { useState } from 'react';
+import {
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+ type AccessibilityActionEvent,
+ type LayoutChangeEvent,
+} from 'react-native';
+import { Gesture, GestureDetector } from 'react-native-gesture-handler';
+
+const THUMB_SIZE = 22;
+const ACCESSIBILITY_STEP = 0.1;
+const ACCESSIBILITY_ACTIONS = [
+ { name: 'increment' },
+ { name: 'decrement' },
+] as const;
+
+function clamp01(v: number) {
+ return Math.max(0, Math.min(1, v));
+}
+
+type TorchSliderProps = {
+ label: string;
+ value: number;
+ onChange: (value: number) => void;
+};
+
+function TorchSlider({ label, value, onChange }: TorchSliderProps) {
+ const [trackWidth, setTrackWidth] = useState(1);
+
+ const onLayout = (e: LayoutChangeEvent) =>
+ setTrackWidth(e.nativeEvent.layout.width);
+ const setFromTouch = (x: number) => onChange(clamp01(x / trackWidth));
+ const pan = Gesture.Pan()
+ .runOnJS(true)
+ .minDistance(0)
+ .onBegin((e) => setFromTouch(e.x))
+ .onUpdate((e) => setFromTouch(e.x));
+
+ const onAccessibilityAction = (e: AccessibilityActionEvent) => {
+ const direction = e.nativeEvent.actionName === 'increment' ? 1 : -1;
+ onChange(clamp01(value + direction * ACCESSIBILITY_STEP));
+ };
+
+ return (
+
+ {label}
+
+
+
+
+
+
+
+
+ );
+}
+
+type TorchControlsProps = {
+ isOn: boolean;
+ beam: number;
+ level: number;
+ bottomInset: number;
+ onToggle: () => void;
+ onBeamChange: (value: number) => void;
+ onLevelChange: (value: number) => void;
+};
+
+/** The light switch and the two sliders that shape the beam. */
+export default function TorchControls({
+ isOn,
+ beam,
+ level,
+ bottomInset,
+ onToggle,
+ onBeamChange,
+ onLevelChange,
+}: TorchControlsProps) {
+ return (
+
+
+
+ [
+ styles.toggle,
+ isOn && styles.toggleOn,
+ pressed && styles.togglePressed,
+ ]}
+ accessibilityRole="switch"
+ accessibilityState={{ checked: isOn }}
+ >
+
+ {isOn ? 'Switch off' : 'Switch on'}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ panel: {
+ position: 'absolute',
+ left: 28,
+ right: 28,
+ gap: 10,
+ padding: 16,
+ borderRadius: 18,
+ backgroundColor: 'rgba(12, 12, 14, 0.78)',
+ },
+ sliderRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 12,
+ },
+ label: {
+ width: 48,
+ color: 'rgba(255, 255, 255, 0.72)',
+ fontSize: 14,
+ fontWeight: '500',
+ },
+ track: {
+ flex: 1,
+ height: 32,
+ justifyContent: 'center',
+ },
+ groove: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ height: 3,
+ borderRadius: 2,
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ },
+ fill: {
+ position: 'absolute',
+ left: 0,
+ height: 3,
+ borderRadius: 2,
+ backgroundColor: '#f2e6c8',
+ },
+ thumb: {
+ position: 'absolute',
+ width: THUMB_SIZE,
+ height: THUMB_SIZE,
+ borderRadius: THUMB_SIZE / 2,
+ backgroundColor: '#f2e6c8',
+ },
+ toggle: {
+ alignItems: 'center',
+ marginTop: 4,
+ paddingVertical: 11,
+ borderRadius: 12,
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
+ },
+ toggleOn: {
+ backgroundColor: '#f2e6c8',
+ },
+ togglePressed: {
+ opacity: 0.7,
+ },
+ toggleText: {
+ color: '#fff',
+ fontSize: 15,
+ fontWeight: '600',
+ },
+ toggleTextOn: {
+ color: '#17150f',
+ },
+});
diff --git a/example/src/components/flashlight/TorchIsland.tsx b/example/src/components/flashlight/TorchIsland.tsx
deleted file mode 100644
index ad976a5..0000000
--- a/example/src/components/flashlight/TorchIsland.tsx
+++ /dev/null
@@ -1,292 +0,0 @@
-import { useEffect } from 'react';
-import { Image, Pressable, StyleSheet } from 'react-native';
-import { Gesture, GestureDetector } from 'react-native-gesture-handler';
-import Animated, {
- useAnimatedStyle,
- useSharedValue,
- withDelay,
- withSpring,
- withTiming,
-} from 'react-native-reanimated';
-import { ShaderView, type ParamsSynchronizable } from 'react-native-effects';
-
-const GLYPH = require('../../../assets/flashlight/torch-glyph.png');
-
-/** The hardware Dynamic Island cut-out, measured on the iPhone 18 Pro Max. */
-export const ISLAND = { width: 95, height: 37, top: 14 };
-/** The expanded torch control, scaled from the iOS 18 screenshots. */
-export const PANEL = { width: 220, height: 318, top: 8, radius: 48 };
-const COMPACT_WIDTH = ISLAND.width + 74;
-const APEX_Y = 226;
-const GLYPH_SIZE = { width: 26, height: 46 };
-
-export type IslandMode = 'idle' | 'compact' | 'expanded';
-
-const SPRING = { damping: 17, stiffness: 190, mass: 0.9 };
-
-function layoutFor(mode: IslandMode) {
- if (mode === 'expanded') {
- return { ...PANEL };
- }
- return {
- width: mode === 'compact' ? COMPACT_WIDTH : ISLAND.width,
- height: ISLAND.height,
- top: ISLAND.top,
- radius: ISLAND.height / 2,
- };
-}
-
-type TorchIslandProps = {
- mode: IslandMode;
- isOn: boolean;
- /** `u.live = (beam 0..1, intensity 0..1, power 0..1, 0)`. */
- paramsSynchronizable: ParamsSynchronizable;
- onExpand: () => void;
- onToggle: () => void;
- /** Drag deltas in pt since the last call. */
- onAdjust: (dx: number, dy: number) => void;
-};
-
-/**
- * The iOS 18 flashlight Dynamic Island: a plain island while the torch is
- * off, a compact pill with the torch glyph once it is on, and the expanded
- * control (a top-down torch whose beam, solid arc and sun mark follow the
- * beam width and intensity). Drag across it to shape the beam, tap it to
- * switch the light.
- */
-export default function TorchIsland({
- mode,
- isOn,
- paramsSynchronizable,
- onExpand,
- onToggle,
- onAdjust,
-}: TorchIslandProps) {
- const start = layoutFor(mode);
- const width = useSharedValue(start.width);
- const height = useSharedValue(start.height);
- const top = useSharedValue(start.top);
- const radius = useSharedValue(start.radius);
- const panelContent = useSharedValue(mode === 'expanded' ? 1 : 0);
- const compactContent = useSharedValue(mode === 'compact' ? 1 : 0);
-
- useEffect(() => {
- const next = layoutFor(mode);
- width.value = withSpring(next.width, SPRING);
- height.value = withSpring(next.height, SPRING);
- top.value = withSpring(next.top, SPRING);
- radius.value = withSpring(next.radius, SPRING);
- panelContent.value =
- mode === 'expanded'
- ? withDelay(90, withTiming(1, { duration: 220 }))
- : withTiming(0, { duration: 90 });
- compactContent.value =
- mode === 'compact'
- ? withDelay(160, withTiming(1, { duration: 180 }))
- : withTiming(0, { duration: 80 });
- }, [mode, width, height, top, radius, panelContent, compactContent]);
-
- const shellStyle = useAnimatedStyle(() => ({
- width: width.value,
- height: height.value,
- top: top.value,
- borderRadius: radius.value,
- marginLeft: -width.value / 2,
- }));
- const panelStyle = useAnimatedStyle(() => ({
- opacity: panelContent.value,
- transform: [{ scale: 0.85 + 0.15 * panelContent.value }],
- }));
- const compactStyle = useAnimatedStyle(() => ({
- opacity: compactContent.value,
- }));
-
- const tap = Gesture.Tap()
- .runOnJS(true)
- .onEnd(() => {
- if (mode === 'expanded') {
- onToggle();
- } else if (mode === 'compact') {
- onExpand();
- }
- });
- const pan = Gesture.Pan()
- .runOnJS(true)
- .minDistance(3)
- .onChange((e) => {
- if (mode === 'expanded') onAdjust(e.changeX, e.changeY);
- });
- const gesture = Gesture.Race(pan, tap);
-
- return (
- <>
- {mode === 'compact' && (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
-
-const styles = StyleSheet.create({
- // Touches on the status bar strip can be claimed by the system, so the
- // compact pill also answers taps just below the hardware island.
- compactReach: {
- position: 'absolute',
- top: ISLAND.top + ISLAND.height - 6,
- left: '50%',
- marginLeft: -COMPACT_WIDTH / 2,
- width: COMPACT_WIDTH,
- height: 34,
- zIndex: 49,
- },
- shell: {
- position: 'absolute',
- left: '50%',
- backgroundColor: '#000',
- overflow: 'hidden',
- zIndex: 50,
- },
- panel: {
- position: 'absolute',
- top: 0,
- left: '50%',
- marginLeft: -PANEL.width / 2,
- width: PANEL.width,
- height: PANEL.height,
- },
- glyph: {
- position: 'absolute',
- left: (PANEL.width - GLYPH_SIZE.width) / 2,
- top: APEX_Y + 2,
- width: GLYPH_SIZE.width,
- height: GLYPH_SIZE.height,
- },
- glyphOn: {
- tintColor: '#e6e4f7',
- },
- glyphOff: {
- tintColor: '#5d5d66',
- },
- compactGlyphWrap: {
- position: 'absolute',
- left: 15,
- top: 0,
- bottom: 0,
- justifyContent: 'center',
- },
- compactGlyph: {
- width: 11,
- height: 20,
- tintColor: '#e6e4f7',
- },
-});
-
-const SHADER = /* wgsl */ `
-struct Uniforms {
- resolution: vec4,
- time: vec4,
- color0: vec4,
- color1: vec4,
- params0: vec4,
- params1: vec4,
- live: vec4,
-};
-@group(0) @binding(0) var u: Uniforms;
-
-const MAX_HALF: f32 = 0.74;
-const MIN_HALF: f32 = 0.05;
-const DOT_GAP: f32 = 5.2;
-
-fn spectrum(x: f32) -> vec3 {
- return 0.5 + 0.5 * cos(6.2831 * (vec3(x) + vec3(0.0, 0.33, 0.67)));
-}
-
-// Point on the arc of radius r at angle a from straight up, panel pt y-down.
-fn onArc(apex: vec2, r: f32, a: f32) -> vec2 {
- return apex + vec2(sin(a), -cos(a)) * r;
-}
-
-fn sunMark(q: vec2, c: vec2) -> f32 {
- let v = q - c;
- let d = length(v);
- let disc = 1.0 - smoothstep(2.3, 3.0, d);
- let a = atan2(v.y, v.x);
- let spoke = smoothstep(0.75, 0.95, cos(a * 8.0));
- let ray = spoke * smoothstep(3.9, 4.4, d) * (1.0 - smoothstep(5.6, 6.2, d));
- return max(disc, ray);
-}
-
-@fragment
-fn main(@location(0) ndc: vec2) -> @location(0) vec4 {
- let uv = ndc * 0.5 + 0.5;
- let size = u.params0.xy;
- let q = vec2(uv.x, 1.0 - uv.y) * size;
- let apex = vec2(size.x * 0.5, u.params0.z);
-
- let beam = clamp(u.live.x, 0.0, 1.0);
- let level = clamp(u.live.y, 0.0, 1.0);
- let power = clamp(u.live.z, 0.0, 1.0);
-
- let v = vec2(q.x - apex.x, apex.y - q.y);
- let dist = length(v);
- let ang = atan2(v.x, v.y);
- let a = abs(ang);
- let halfA = mix(MIN_HALF, MAX_HALF, beam);
- let reach = mix(72.0, 128.0, level) * mix(1.08, 0.94, beam);
- let gain = (0.3 + 0.7 * level) * mix(1.5, 1.0, beam);
-
- let cone = smoothstep(halfA + 0.05, halfA * 0.6, a) * step(0.0, v.y);
- let along = pow(smoothstep(reach * 1.04, reach * 0.05, dist), 0.8);
- let core = exp(-dist * dist / 180.0);
- let body = cone * along * 0.85 + core * 0.95;
- let edge = exp(-pow((a - halfA * 0.86) / (0.03 + 0.12 * halfA), 2.0))
- * along * step(0.0, v.y) * 0.22;
- let tint = spectrum(a / max(halfA, 0.01) * 0.55 + dist / 150.0);
-
- var rgb = vec3(0.96, 0.96, 1.0) * body * gain + tint * edge * gain;
- var alpha = clamp((body + edge * 0.5) * gain, 0.0, 1.0);
- rgb = rgb * power;
- alpha = alpha * power;
-
- let ring = 1.0 - smoothstep(0.55, 1.35, abs(dist - reach));
- let solid = ring * (1.0 - smoothstep(halfA, halfA + 0.02, a)) * step(0.0, v.y);
- let s = ang * reach;
- let sc = (floor(s / DOT_GAP) + 0.5) * DOT_GAP;
- let dotC = onArc(apex, reach, sc / reach);
- let dotted = (1.0 - smoothstep(0.55, 1.15, length(q - dotC)))
- * step(halfA + 0.03, a) * step(a, MAX_HALF + 0.06);
- let sun = sunMark(q, onArc(apex, reach, max(halfA, 0.1) + 0.17));
- let marks = solid * 0.85 + dotted * 0.4 + sun * 0.75;
- let markAlpha = marks * mix(0.35, 1.0, power);
-
- rgb = rgb + vec3(1.0) * markAlpha * (1.0 - alpha);
- alpha = alpha + markAlpha * (1.0 - alpha);
- return vec4(min(rgb, vec3(1.0)), clamp(alpha, 0.0, 1.0));
-}
-`;
diff --git a/example/src/components/imessage/Bubble.tsx b/example/src/components/imessage/Bubble.tsx
index 79edc31..094a125 100644
--- a/example/src/components/imessage/Bubble.tsx
+++ b/example/src/components/imessage/Bubble.tsx
@@ -1,27 +1,23 @@
import { useCallback, useRef, type ComponentRef } from 'react';
import { StyleSheet, Text, View, type LayoutChangeEvent } from 'react-native';
-import { captureRef } from 'react-native-view-shot';
import Animated, { Easing, withTiming } from 'react-native-reanimated';
-import type { BubbleTarget, Message, Rect } from './types';
+import type { BubbleTarget, Message } from './types';
-// Colours sampled from a real Messages thread in the dark appearance.
-export const IM_BLUE = '#4C95F7';
-export const IM_GREY = '#262629';
-export const IM_BG = '#000';
-export const IM_MUTED = '#8E8E93';
-export const IM_CHROME = '#181818';
-const RADIUS = 20;
+export const IM_BLUE = '#2D7D6A';
+export const IM_GREY = '#2A2F36';
+export const IM_BG = '#0E1216';
+export const IM_CHROME = '#181E24';
+const RADIUS = 12;
type ViewHandle = ComponentRef;
-// Measure and snapshot only once the entrance animation has finished and the
-// thread has scrolled to the end; earlier, the snapshot would catch the
-// bubble mid-fade and the rect would be where it was, not where it is.
+// Measure only once the entrance animation has finished and the thread has
+// scrolled to the end; earlier, the rect would be where it was, not where it is.
const ENTER_MS = 260;
const MEASURE_DELAY_MS = ENTER_MS + 60;
-// Bubbles ease in from their tail corner, the way Messages lands them: a
-// short fade with a slight rise and grow, no overshoot.
+// Bubbles ease in from their low corner: a short fade with a slight rise and
+// grow, no overshoot.
const EASE_OUT = Easing.out(Easing.cubic);
const popIn = () => {
'worklet';
@@ -42,25 +38,17 @@ const popIn = () => {
type Props = {
msg: Message;
- /** Only the last bubble of a run gets iMessage's tail. */
+ /** The last bubble of a run gets a square corner on the sender's side. */
tail: boolean;
- /** Report the window rect (and, when asked, a snapshot) once laid out. */
+ /** Report the window rect once laid out. */
onTarget?: (target: BubbleTarget) => void;
- wantSnapshot?: boolean;
};
/**
- * One iMessage bubble: a real `Text` in a rounded view with the tail drawn
- * under it. A bubble that a screen effect will play for measures itself in
- * the window once laid out, and snapshots itself when the effect (echo)
- * needs its pixels.
+ * One chat bubble: a real `Text` in a rounded view. A bubble that a screen
+ * effect will play for measures itself in the window once laid out.
*/
-export default function Bubble({
- msg,
- tail,
- onTarget,
- wantSnapshot = false,
-}: Props) {
+export default function Bubble({ msg, tail, onTarget }: Props) {
const mine = msg.from === 'me';
const bubbleRef = useRef(null);
const reported = useRef(false);
@@ -71,33 +59,13 @@ export default function Bubble({
return;
}
reported.current = true;
- setTimeout(async () => {
- const node = bubbleRef.current;
- if (!node) {
- return;
- }
- try {
- const rect = await new Promise((resolve) =>
- node.measureInWindow((x, y, w, h) => resolve({ x, y, w, h }))
- );
- let snapshotUri: string | null = null;
- if (wantSnapshot) {
- const raw = await captureRef(bubbleRef, {
- format: 'png',
- quality: 1,
- result: 'tmpfile',
- });
- snapshotUri = raw.startsWith('file://') ? raw : `file://${raw}`;
- }
- onTarget({ rect, snapshotUri });
- } catch (err) {
- if (__DEV__) {
- console.warn('bubble capture failed', err);
- }
- }
+ setTimeout(() => {
+ bubbleRef.current?.measureInWindow((x, y, w, h) =>
+ onTarget({ rect: { x, y, w, h } })
+ );
}, MEASURE_DELAY_MS);
},
- [onTarget, wantSnapshot]
+ [onTarget]
);
const bg = mine ? IM_BLUE : IM_GREY;
@@ -107,25 +75,15 @@ export default function Bubble({
style={[styles.wrap, mine ? styles.wrapMine : styles.wrapThem]}
entering={popIn}
>
- {tail && (
- <>
-
-
- >
- )}
{msg.text}
@@ -147,8 +105,7 @@ const styles = StyleSheet.create({
justifyContent: 'flex-start',
},
wrap: {
- // Messages caps a bubble at about two thirds of the screen.
- maxWidth: '68%',
+ maxWidth: '74%',
},
wrapMine: {
transformOrigin: 'right bottom',
@@ -167,35 +124,10 @@ const styles = StyleSheet.create({
fontSize: 17,
lineHeight: 21,
},
- // The tail is the CSS two-shape trick: a filled corner under the bubble,
- // cut by a ground-coloured shape beside it.
- tailA: {
- position: 'absolute',
- bottom: 0,
- width: 20,
- height: 24,
+ cornerMine: {
+ borderBottomRightRadius: 2,
},
- tailAMine: {
- right: -8,
- borderBottomLeftRadius: 16,
- },
- tailAThem: {
- left: -8,
- borderBottomRightRadius: 16,
- },
- tailB: {
- position: 'absolute',
- bottom: 0,
- width: 26,
- height: 24,
- backgroundColor: IM_BG,
- },
- tailBMine: {
- right: -26,
- borderBottomLeftRadius: 10,
- },
- tailBThem: {
- left: -26,
- borderBottomRightRadius: 10,
+ cornerThem: {
+ borderBottomLeftRadius: 2,
},
});
diff --git a/example/src/components/imessage/ConfettiEffect.tsx b/example/src/components/imessage/ConfettiEffect.tsx
index adfbde4..607f8cb 100644
--- a/example/src/components/imessage/ConfettiEffect.tsx
+++ b/example/src/components/imessage/ConfettiEffect.tsx
@@ -9,7 +9,7 @@ type Props = {
};
/**
- * iMessage's Confetti: a shower of paper pieces dropped from above the
+ * Confetti: a shower of paper pieces dropped from above the
* screen that fall through the chat in front of the bubbles, tumbling and
* swaying, and are gone once they clear the bottom. Every piece is a cell
* in a column grid with its own drop time, so the first second is a dense
diff --git a/example/src/components/imessage/EchoEffect.tsx b/example/src/components/imessage/EchoEffect.tsx
index d446c89..0cb8575 100644
--- a/example/src/components/imessage/EchoEffect.tsx
+++ b/example/src/components/imessage/EchoEffect.tsx
@@ -8,19 +8,14 @@ export const ECHO_MS = 2400;
type Props = {
/** The sent bubble, window logical px. */
rect: Rect;
- /** View-shot of that bubble; every echo is a copy of it. */
- snapshotUri: string;
onDone: () => void;
};
/**
- * iMessage's Echo: the bubble is copied a couple of dozen times and the
- * copies swirl out of it across the whole screen, shrinking a little and
- * fading at the end of their flight, while the real bubble stays put. The copies are the bubble's own
- * pixels: for every fragment each copy's transform is undone and the
- * snapshot sampled where it lands inside the rect.
+ * Encore: rings in the outline of the bubble ripple out of it across the
+ * screen, thinning and fading as they travel, while the bubble stays put.
*/
-export default function EchoEffect({ rect, snapshotUri, onDone }: Props) {
+export default function EchoEffect({ rect, onDone }: Props) {
const paramsSynchronizable = useEffectProgress(ECHO_MS, onDone);
return (
);
@@ -46,18 +40,18 @@ struct Uniforms {
live: vec4,
};
@group(0) @binding(0) var u: Uniforms;
-@group(0) @binding(1) var samp: sampler;
-@group(0) @binding(2) var tex: texture_2d;
-const COPIES: i32 = 22;
-const STAGGER: f32 = 0.38; // share of the play spent launching copies
-const GOLDEN: f32 = 2.39996; // sunflower angle between neighbouring slots
-const SWIRL: f32 = 1.2; // radians a copy swings about the bubble in flight
+const RINGS: i32 = 5;
+const STAGGER: f32 = 0.14; // share of the play between two rings
+const WIDTH: f32 = 2.2; // ring half-width, px
+const WAKE: f32 = 46.0; // e-fold length of the glow behind a ring, px
+const CORNER: f32 = 12.0; // bubble corner radius, px
+const MINT = vec3(0.62, 0.95, 0.83);
+const TEAL = vec3(0.18, 0.49, 0.42);
-fn hash21(p: vec2) -> f32 {
- var p3 = fract(vec3(p.xyx) * 0.1031);
- p3 = p3 + dot(p3, p3.yzx + 33.33);
- return fract((p3.x + p3.y) * p3.z);
+fn sdRoundBox(p: vec2, halfSize: vec2, r: f32) -> f32 {
+ let q = abs(p) - halfSize + vec2(r);
+ return length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - r;
}
@fragment
@@ -71,54 +65,30 @@ fn main(@location(0) ndc: vec2) -> @location(0) vec4 {
let res = u.resolution.xy / u.resolution.w;
let q = vec2(uv.x, 1.0 - uv.y) * res;
- let origin = u.params0.xy;
let size = u.params0.zw;
- let centre = origin + size * 0.5;
+ let centre = u.params0.xy + size * 0.5;
+ let d = sdRoundBox(q - centre, size * 0.5, CORNER);
+ let reach = res.y * 0.9;
+ let flight = 1.0 - STAGGER * f32(RINGS - 1);
var rgb = vec3(0.0);
var a = 0.0;
- for (var i = 0; i < COPIES; i = i + 1) {
- let fi = f32(i);
- let h2 = hash21(vec2(fi, 7.9));
- // Copies leave one after another, then all fly on the same clock.
- let ti = clamp((p - STAGGER * fi / f32(COPIES)) / (1.0 - STAGGER), 0.0, 1.0);
- if (ti <= 0.0) {
+ for (var i = 0; i < RINGS; i = i + 1) {
+ let ti = clamp((p - STAGGER * f32(i)) / flight, 0.0, 1.0);
+ if (ti <= 0.0 || ti >= 1.0) {
continue;
}
- let ease = 1.0 - pow(1.0 - ti, 2.4);
- // Each copy owns a slot in a sunflower laid over the whole screen, so
- // the swarm fills it evenly the way iMessage does, and swings into its
- // slot about the bubble rather than flying straight, so the flight
- // reads as a spiral.
- let slot = vec2(cos(fi * GOLDEN), sin(fi * GOLDEN))
- * sqrt((fi + 0.5) / f32(COPIES));
- let dest = res * 0.5 + slot * res * vec2(0.44, 0.46);
- let arm = dest - centre;
- let turn = SWIRL * (1.0 - ease);
- let ct = cos(turn);
- let st = sin(turn);
- let c = centre
- + vec2(ct * arm.x - st * arm.y, st * arm.x + ct * arm.y) * ease;
- let scale = mix(1.0, 0.7, ease);
- // The swarm holds together and each copy thins only at the end of its
- // flight, so the screen is full before anything is gone.
- let alpha = 1.0 - smoothstep(0.65, 1.0, ti);
- let rot = (h2 - 0.5) * 0.9 * ease;
-
- // Undo this copy's transform: where in the bubble does q come from?
- let d = q - c;
- let cr = cos(rot);
- let sr = sin(rot);
- let local = vec2(cr * d.x + sr * d.y, -sr * d.x + cr * d.y) / scale;
- let t = (local + size * 0.5) / size;
- if (t.x < 0.0 || t.x > 1.0 || t.y < 0.0 || t.y > 1.0) {
- continue;
- }
- let s = textureSampleLevel(tex, samp, t, 0.0);
- let w = s.a * alpha * (1.0 - a);
- rgb = rgb + s.rgb * w;
- a = a + w;
+ let x = d - reach * (1.0 - pow(1.0 - ti, 1.8));
+ let ring = exp(-x * x / (2.0 * WIDTH * WIDTH));
+ let wake = select(0.0, exp(x / WAKE) * 0.16, x < 0.0);
+ let fade = smoothstep(0.0, 0.06, ti) * pow(1.0 - ti, 1.4);
+ let inten = (ring + wake) * fade;
+ rgb = rgb + mix(MINT, TEAL, ti) * inten;
+ a = a + inten;
}
- return vec4(rgb, a);
+
+ let outside = smoothstep(0.0, 2.0, d);
+ let alpha = clamp(a * outside, 0.0, 1.0);
+ return vec4(min(rgb * outside, vec3(alpha)), alpha);
}
`;
diff --git a/example/src/components/imessage/EffectsSheet.tsx b/example/src/components/imessage/EffectsSheet.tsx
deleted file mode 100644
index f792cfe..0000000
--- a/example/src/components/imessage/EffectsSheet.tsx
+++ /dev/null
@@ -1,247 +0,0 @@
-import { useCallback, useEffect, useRef, useState } from 'react';
-import {
- Pressable,
- ScrollView,
- StyleSheet,
- Text,
- View,
- useWindowDimensions,
- type NativeScrollEvent,
- type NativeSyntheticEvent,
-} from 'react-native';
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import Bubble, { IM_BLUE } from './Bubble';
-import ScreenEffectPlayer from './ScreenEffectPlayer';
-import {
- EFFECT_LABEL,
- SCREEN_EFFECTS,
- type BubbleTarget,
- type EffectId,
- type Message,
- type ScreenEffectId,
-} from './types';
-
-type Props = {
- text: string;
- onClose: () => void;
- onSend: (effect: EffectId) => void;
-};
-
-/**
- * The "Send with effect" sheet: the pending message shown as a real bubble
- * and the screen effects in a pager, each previewing on the bubble the way
- * iMessage previews before you commit. The arrow sends with the effect.
- */
-export default function EffectsSheet({ text, onClose, onSend }: Props) {
- const insets = useSafeAreaInsets();
- const { width } = useWindowDimensions();
- const [page, setPage] = useState(0);
- const [target, setTarget] = useState(null);
- const [playKey, setPlayKey] = useState(0);
- const replay = useRef | null>(null);
-
- const effect: ScreenEffectId = SCREEN_EFFECTS[page] ?? 'echo';
-
- // The preview bubble is remounted per effect so its snapshot is fresh for
- // the effects that need one.
- const preview: Message = {
- id: `preview-${effect}`,
- from: 'me',
- text,
- effect,
- time: '',
- };
-
- const onDone = useCallback(() => {
- replay.current = setTimeout(() => setPlayKey((k) => k + 1), 600);
- }, []);
- useEffect(
- () => () => {
- if (replay.current) {
- clearTimeout(replay.current);
- }
- },
- []
- );
- useEffect(() => {
- // A new effect starts its preview from the top.
- setTarget(null);
- setPlayKey((k) => k + 1);
- }, [effect]);
-
- const onPage = useCallback(
- (e: NativeSyntheticEvent) => {
- const i = Math.round(e.nativeEvent.contentOffset.x / width);
- setPage(Math.max(0, Math.min(SCREEN_EFFECTS.length - 1, i)));
- },
- [width]
- );
-
- const player = target ? (
-
- ) : null;
-
- return (
-
-
- Send with effect
-
- ✕
-
-
-
-
-
-
-
-
-
- {SCREEN_EFFECTS.map((id) => (
-
- {EFFECT_LABEL[id]}
-
- ))}
-
-
- {SCREEN_EFFECTS.map((id, i) => (
-
- ))}
-
-
- Swipe to preview · tap ↑ to send
- onSend(effect)}
- style={styles.send}
- accessibilityRole="button"
- accessibilityLabel={`Send with ${EFFECT_LABEL[effect]}`}
- >
- ↑
-
-
-
-
- {player}
-
- );
-}
-
-const styles = StyleSheet.create({
- root: {
- position: 'absolute',
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- backgroundColor: 'rgba(0,0,0,0.78)',
- },
- top: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- paddingHorizontal: 16,
- paddingBottom: 10,
- },
- title: {
- color: '#fff',
- fontSize: 15,
- fontWeight: '600',
- },
- close: {
- position: 'absolute',
- right: 16,
- bottom: 4,
- width: 32,
- height: 32,
- borderRadius: 16,
- backgroundColor: 'rgba(118,118,128,0.3)',
- alignItems: 'center',
- justifyContent: 'center',
- },
- closeText: {
- color: '#fff',
- fontSize: 15,
- fontWeight: '700',
- },
- stage: {
- flex: 1,
- justifyContent: 'flex-end',
- paddingBottom: 28,
- },
- bottom: {
- paddingTop: 8,
- },
- pager: {
- height: 56,
- },
- page: {
- alignItems: 'center',
- justifyContent: 'center',
- },
- pageText: {
- color: '#fff',
- fontSize: 22,
- fontWeight: '600',
- },
- dots: {
- flexDirection: 'row',
- justifyContent: 'center',
- gap: 7,
- marginTop: 2,
- },
- dot: {
- width: 7,
- height: 7,
- borderRadius: 4,
- backgroundColor: 'rgba(255,255,255,0.3)',
- },
- dotOn: {
- backgroundColor: '#fff',
- },
- sendRow: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- paddingHorizontal: 20,
- marginTop: 18,
- },
- hint: {
- color: '#8e8e93',
- fontSize: 13,
- },
- send: {
- width: 40,
- height: 40,
- borderRadius: 20,
- backgroundColor: IM_BLUE,
- alignItems: 'center',
- justifyContent: 'center',
- },
- sendArrow: {
- color: '#fff',
- fontSize: 22,
- fontWeight: '700',
- marginTop: -2,
- },
-});
diff --git a/example/src/components/imessage/FireworksEffect.tsx b/example/src/components/imessage/FireworksEffect.tsx
index 25bfc74..fe1b70b 100644
--- a/example/src/components/imessage/FireworksEffect.tsx
+++ b/example/src/components/imessage/FireworksEffect.tsx
@@ -13,7 +13,7 @@ type Props = {
};
/**
- * iMessage's Fireworks: glitter-dust shells popping over the chat. The
+ * Fireworks: glitter-dust shells popping over the chat. The
* bursts are the birthday banner's (components/BirthdayFireworks.tsx) —
* hollow polar shells of twinkling stars with an ignition flash — drawn as
* a transparent overlay in front of the bubbles instead of on the banner's
diff --git a/example/src/components/imessage/LasersEffect.tsx b/example/src/components/imessage/LasersEffect.tsx
index 5f95f74..5a1987f 100644
--- a/example/src/components/imessage/LasersEffect.tsx
+++ b/example/src/components/imessage/LasersEffect.tsx
@@ -9,10 +9,9 @@ type Props = {
};
/**
- * iMessage's Lasers: coloured beams fan up from below the screen and sweep
- * across the chat through a thin haze. Each beam is a soft line with a wide
- * glow; the fan breathes rather than strobes, and the whole thing fades in
- * and out so it reads as a light show, not a flicker.
+ * Beams: broad shafts of warm daylight fall from the top corner across the
+ * chat, the way sun comes through a window, drifting slowly as they fade in
+ * and out.
*/
export default function LasersEffect({ onDone }: Props) {
const paramsSynchronizable = useEffectProgress(LASERS_MS, onDone);
@@ -39,22 +38,14 @@ struct Uniforms {
};
@group(0) @binding(0) var u: Uniforms;
-const BEAMS: i32 = 8;
-const CORE: f32 = 1.6; // beam half-width at full brightness, px
-const GLOW: f32 = 34.0; // e-fold width of the haze around a beam, px
-const SPREAD: f32 = 0.62; // half fan angle, radians
-const SWEEP: f32 = 0.16; // how far each beam swings, radians
-const PI: f32 = 3.14159265;
+const SUN = vec3(1.0, 0.83, 0.52);
+const SHAFT_SHARPNESS: f32 = 2.4;
-fn hash21(p: vec2) -> f32 {
- var p3 = fract(vec3(p.xyx) * 0.1031);
- p3 = p3 + dot(p3, p3.yzx + 33.33);
- return fract((p3.x + p3.y) * p3.z);
-}
-
-// Saturated stage-light colours around the hue wheel.
-fn beamColour(h: f32) -> vec3 {
- return 0.55 + 0.45 * cos(6.2831853 * (h + vec3(0.0, 0.33, 0.67)));
+fn shafts(angle: f32, t: f32) -> f32 {
+ let wide = 0.5 + 0.5 * sin(angle * 9.0 + t * 0.25);
+ let mid = 0.5 + 0.5 * sin(angle * 17.0 - t * 0.18 + 1.3);
+ let fine = 0.5 + 0.5 * sin(angle * 31.0 + t * 0.11 + 4.1);
+ return pow(wide * 0.55 + mid * 0.3 + fine * 0.15, SHAFT_SHARPNESS);
}
@fragment
@@ -68,49 +59,12 @@ fn main(@location(0) ndc: vec2) -> @location(0) vec4 {
let res = u.resolution.xy / u.resolution.w;
let q = vec2(uv.x, 1.0 - uv.y) * res;
- let env = smoothstep(0.0, 0.14, p) * (1.0 - smoothstep(0.78, 1.0, p));
- // The whole fan breathes slowly; no beam ever cuts out.
- let breathe = 0.82 + 0.18 * sin(t * 4.2);
-
- // Beams come from a point just under the bottom edge, mid-screen.
- let origin = vec2(res.x * 0.5, res.y + 30.0);
- let v = q - origin;
-
- var rgb = vec3(0.0);
- var lum = 0.0;
- for (var i = 0; i < BEAMS; i = i + 1) {
- let fi = f32(i);
- let h = hash21(vec2(fi, 3.7));
- let g = hash21(vec2(fi, 9.1));
- // Fan spread evenly, each beam swinging on its own slow clock; the
- // whole fan also leans with the play so it reads as a sweep, not a hold.
- let base = -PI * 0.5 + SPREAD * ((fi + 0.5) / f32(BEAMS) * 2.0 - 1.0);
- let ang = base
- + SWEEP * sin(t * (0.7 + 0.5 * h) + h * 6.2831853)
- + 0.22 * sin(p * PI * 2.0 + g * 3.0);
- let dir = vec2(cos(ang), sin(ang));
- let along = dot(v, dir);
- if (along <= 0.0) {
- continue;
- }
- let perp = abs(v.x * dir.y - v.y * dir.x);
- // Beams thin out as they travel, like light through haze.
- let travel = exp(-along / (res.y * 1.6));
- let core = exp(-perp * perp / (2.0 * CORE * CORE));
- let glow = exp(-perp / GLOW) * 0.16;
- let inten = (core + glow) * travel * (0.75 + 0.25 * g);
- let col = beamColour(fi / f32(BEAMS) + t * 0.03);
- rgb = rgb + col * inten;
- lum = lum + inten;
- }
-
- // A faint warm haze where the beams are densest, near their source.
- let fog = exp(-length(v) / (res.y * 0.45)) * 0.10;
- rgb = rgb + vec3(0.9, 0.7, 1.0) * fog;
- lum = lum + fog;
+ let env = smoothstep(0.0, 0.25, p) * (1.0 - smoothstep(0.7, 1.0, p));
+ let v = q - vec2(res.x * 1.1, -res.y * 0.12);
+ let falloff = exp(-length(v) / (res.y * 0.75));
+ let inten = shafts(atan2(v.y, v.x), t + p * 6.0) * falloff * env * 1.5;
- let gain = env * breathe;
- let a = clamp(lum * 0.9 * gain, 0.0, 1.0);
- return vec4(min(rgb * gain, vec3(1.0)), a);
+ let a = clamp(inten, 0.0, 1.0);
+ return vec4(min(SUN * inten, vec3(a)), a);
}
`;
diff --git a/example/src/components/imessage/ScreenEffectPlayer.tsx b/example/src/components/imessage/ScreenEffectPlayer.tsx
index d877708..97c2f18 100644
--- a/example/src/components/imessage/ScreenEffectPlayer.tsx
+++ b/example/src/components/imessage/ScreenEffectPlayer.tsx
@@ -13,7 +13,7 @@ type Props = {
/**
* One play of a screen effect for a bubble, as a transparent overlay in
- * front of the chat, the way iMessage draws them; unmount it when `onDone`
+ * front of the chat, drawn above everything; unmount it when `onDone`
* fires.
*/
export default function ScreenEffectPlayer({ effect, target, onDone }: Props) {
@@ -27,15 +27,6 @@ export default function ScreenEffectPlayer({ effect, target, onDone }: Props) {
case 'lasers':
return ;
case 'echo':
- if (!target.snapshotUri) {
- return null;
- }
- return (
-
- );
+ return ;
}
}
diff --git a/example/src/components/imessage/SpotlightEffect.tsx b/example/src/components/imessage/SpotlightEffect.tsx
index 6d5b056..b201fe4 100644
--- a/example/src/components/imessage/SpotlightEffect.tsx
+++ b/example/src/components/imessage/SpotlightEffect.tsx
@@ -12,7 +12,7 @@ type Props = {
};
/**
- * iMessage's Spotlight: the room goes dark and a soft beam sweeps down from
+ * Spotlight: the room goes dark and a soft beam sweeps down from
* the top onto the new bubble, holds, and lifts. The overlay only ever
* darkens, so the bubble itself is the real view seen through a hole in the
* dim: nothing here redraws it.
diff --git a/example/src/components/imessage/glyphs.tsx b/example/src/components/imessage/glyphs.tsx
index 03b4ed8..e8e456c 100644
--- a/example/src/components/imessage/glyphs.tsx
+++ b/example/src/components/imessage/glyphs.tsx
@@ -1,9 +1,4 @@
-import { Image, StyleSheet, Text, View } from 'react-native';
-
-// Messages' chrome glyphs. Most are drawn from plain views so they stay crisp
-// at any scale without an icon font; the video camera is the SF Symbol
-// "video" rendered to a white mask (assets/imessage/video.png) and tinted,
-// because its flared lens does not draw well from rectangles.
+import { StyleSheet, View } from 'react-native';
const WHITE = '#fff';
@@ -31,108 +26,9 @@ export function Chevron({
);
}
-const VIDEO_GLYPH = require('../../../assets/imessage/video.png');
-
-export function VideoCamera({ color = WHITE }: { color?: string }) {
- return (
-
- );
-}
-
-export function Plus({ color = WHITE }: { color?: string }) {
- return +;
-}
-
-export function Microphone({ color = WHITE }: { color?: string }) {
- return (
-
-
-
-
-
-
- );
-}
-
-export function Lock({ color = WHITE }: { color?: string }) {
- return (
-
-
-
-
- );
-}
-
const styles = StyleSheet.create({
chevron: {
borderLeftWidth: 2.5,
borderBottomWidth: 2.5,
},
- camera: {
- width: 26,
- height: 18,
- },
- plus: {
- fontSize: 28,
- fontWeight: '300',
- lineHeight: 30,
- marginTop: -1,
- },
- mic: {
- width: 14,
- height: 22,
- alignItems: 'center',
- },
- micCapsule: {
- width: 8,
- height: 13,
- borderRadius: 4,
- borderWidth: 2,
- },
- micArc: {
- position: 'absolute',
- top: 6,
- width: 14,
- height: 11,
- borderWidth: 2,
- borderTopWidth: 0,
- borderBottomLeftRadius: 7,
- borderBottomRightRadius: 7,
- },
- micStem: {
- position: 'absolute',
- top: 17,
- width: 2,
- height: 4,
- },
- micFoot: {
- position: 'absolute',
- top: 20,
- width: 8,
- height: 2,
- borderRadius: 1,
- },
- lock: {
- width: 10,
- height: 12,
- alignItems: 'center',
- },
- lockShackle: {
- width: 7,
- height: 7,
- borderWidth: 1.5,
- borderBottomWidth: 0,
- borderTopLeftRadius: 4,
- borderTopRightRadius: 4,
- },
- lockBody: {
- width: 10,
- height: 7,
- borderRadius: 2,
- marginTop: -1,
- },
});
diff --git a/example/src/components/imessage/types.ts b/example/src/components/imessage/types.ts
index 446c655..5629868 100644
--- a/example/src/components/imessage/types.ts
+++ b/example/src/components/imessage/types.ts
@@ -17,11 +17,11 @@ export type ScreenEffectId = (typeof SCREEN_EFFECTS)[number];
export const EFFECT_LABEL: Record = {
none: 'None',
- echo: 'Echo',
- spotlight: 'Spotlight',
- confetti: 'Confetti',
- fireworks: 'Fireworks',
- lasers: 'Lasers',
+ echo: 'Encore',
+ spotlight: 'Limelight',
+ confetti: 'Flurry',
+ fireworks: 'Skyburst',
+ lasers: 'Beams',
};
export function isScreenEffect(e: EffectId): e is ScreenEffectId {
@@ -42,6 +42,4 @@ export type Rect = { x: number; y: number; w: number; h: number };
/** What a screen effect needs to know about the bubble it plays for. */
export type BubbleTarget = {
rect: Rect;
- /** A view-shot of the bubble, present once captured; echo needs it. */
- snapshotUri: string | null;
};
diff --git a/example/src/components/instagram/SmokePoof.tsx b/example/src/components/instagram/SmokePoof.tsx
index 62b143c..22a5093 100644
--- a/example/src/components/instagram/SmokePoof.tsx
+++ b/example/src/components/instagram/SmokePoof.tsx
@@ -14,7 +14,7 @@ type SmokePoofProps = Omit<
/**
* A single grey smoke puff that bursts out of the centre, billows, drifts
- * up-left and thins into wisps — the "poof" Instagram plays when a voice
+ * up-left and thins into wisps, the "poof" that plays when a voice
* message is discarded. Progress is pushed through the live channel so a
* Reanimated timing curve drives it frame-accurately.
*/
diff --git a/example/src/components/instagram/VoiceComposer.tsx b/example/src/components/instagram/VoiceComposer.tsx
index 8b4db32..76e4b84 100644
--- a/example/src/components/instagram/VoiceComposer.tsx
+++ b/example/src/components/instagram/VoiceComposer.tsx
@@ -23,8 +23,7 @@ import { useParamsSynchronizable } from 'react-native-effects';
import SmokePoof from './SmokePoof';
import { IG_BAR, IG_BLUE, IG_ICONS, IG_MUTED, IG_RED } from './icons';
-// Timings measured off the Instagram recording (60 fps frames).
-const EXPAND_MS = 320; // mic tap → full blue bar
+const EXPAND_MS = 320; // mic tap → full bar
const COLLAPSE_MS = 130; // trash tap → bar sucked into the trash button
const HOLD_MS = 300; // red trash button sits still before it pops
const POOF_MS = 700; // smoke burst → wisps gone
@@ -57,10 +56,10 @@ function Icon({
type Phase = 'idle' | 'recording' | 'deleting';
/**
- * Instagram DM composer with the voice-message flow: tap the mic and a blue bar
- * swells out of the camera button and starts drawing a waveform; tap the trash
+ * A chat composer with the voice-message flow: tap the mic and a blue bar
+ * swells out of the left button and starts drawing a waveform; tap the trash
* and the bar is sucked back into the button, which turns red, holds, and
- * bursts into a puff of smoke that clears to reveal the camera again.
+ * bursts into a puff of smoke that clears to reveal the button again.
*/
export default function VoiceComposer() {
const [phase, setPhase] = useState('idle');
@@ -69,7 +68,7 @@ export default function VoiceComposer() {
const [width, setWidth] = useState(0);
const lastTall = useRef(false);
- // 0 → 1 as the blue bar expands out of the camera button.
+ // 0 → 1 as the bar expands out of the left button.
const rec = useSharedValue(0);
// 0 → 1 as the bar collapses back into the (now red) trash button.
const del = useSharedValue(0);
@@ -172,7 +171,7 @@ export default function VoiceComposer() {
// --- animated styles -----------------------------------------------------
- // The blue pill grows out of the camera circle, then shrinks back into it.
+ // The pill grows out of the left circle, then shrinks back into it.
const pillStyle = useAnimatedStyle(() => {
const open = rec.value * (1 - del.value);
const w = interpolate(open, [0, 1], [BTN + PAD * 2, Math.max(width, 1)]);
@@ -226,9 +225,9 @@ export default function VoiceComposer() {
{/* Idle composer — always mounted; the pill slides over it. */}
-
+
- Message...
+ Write something
-
-
-
-
-
-
-
-
-
{/* Recording pill */}
diff --git a/example/src/components/instagram/icons.ts b/example/src/components/instagram/icons.ts
index f11bbd4..1a22437 100644
--- a/example/src/components/instagram/icons.ts
+++ b/example/src/components/instagram/icons.ts
@@ -1,25 +1,22 @@
-// Instagram glyphs cropped from a screen recording and converted to white
-// alpha masks — tint them with `tintColor`.
+// White alpha masks drawn by assets/tools/generate-assets.py. Tint them with
+// `tintColor`.
export const IG_ICONS = {
- back: require('../../../assets/instagram/back.png'),
- phone: require('../../../assets/instagram/phone.png'),
- video: require('../../../assets/instagram/video.png'),
- share: require('../../../assets/instagram/share.png'),
- bookmark: require('../../../assets/instagram/bookmark.png'),
- info: require('../../../assets/instagram/info.png'),
- play: require('../../../assets/instagram/play.png'),
- camera: require('../../../assets/instagram/camera.png'),
- mic: require('../../../assets/instagram/mic.png'),
- image: require('../../../assets/instagram/image.png'),
- sticker: require('../../../assets/instagram/sticker.png'),
- plus: require('../../../assets/instagram/plus.png'),
- trash: require('../../../assets/instagram/trash.png'),
- send: require('../../../assets/instagram/send.png'),
+ back: require('../../../assets/chat/back.png'),
+ play: require('../../../assets/chat/play.png'),
+ mic: require('../../../assets/chat/mic.png'),
+ plus: require('../../../assets/chat/plus.png'),
+ trash: require('../../../assets/chat/trash.png'),
+ send: require('../../../assets/chat/send.png'),
};
-export const IG_BLUE = '#4C49FA';
-export const IG_RED = '#FE5971';
-export const IG_BG = '#0E1115';
-export const IG_SURFACE = '#21242A';
-export const IG_BAR = '#16191F';
-export const IG_MUTED = '#8B8F96';
+export const IG_AVATAR = require('../../../assets/chat/avatar-rings.png');
+export const IG_CARD_AVATAR = require('../../../assets/chat/avatar-bars.png');
+export const IG_POST_PHOTO = require('../../../assets/chat/photo-post.jpg');
+export const IG_CARD_PHOTO = require('../../../assets/chat/photo-reel.jpg');
+
+export const IG_BLUE = '#C98A1B';
+export const IG_RED = '#E5484D';
+export const IG_BG = '#131311';
+export const IG_SURFACE = '#23221E';
+export const IG_BAR = '#1C1B18';
+export const IG_MUTED = '#9A968B';
diff --git a/example/src/components/magic8ball/Magic8Ball.tsx b/example/src/components/magic8ball/Magic8Ball.tsx
index a6eacb3..dcdd80c 100644
--- a/example/src/components/magic8ball/Magic8Ball.tsx
+++ b/example/src/components/magic8ball/Magic8Ball.tsx
@@ -3,35 +3,19 @@ import { ShaderView, type ParamsSynchronizable } from 'react-native-effects';
type Props = ViewProps & {
/**
- * `[centerX, centerY, radius, windowRadius, windowY, 0, 0, 0]`. The centre
+ * `[centerX, centerY, radius, revealRadius, revealY, 0, 0, 0]`. The centre
* is in screen uv (y-up) and `radius` is a fraction of the screen HEIGHT;
- * `windowRadius` and `windowY` are in ball radii, so the whole object keeps
- * its proportions on any display.
+ * `revealRadius` and `revealY` are in orb radii.
*/
params: number[];
paramsSynchronizable: ParamsSynchronizable;
};
/**
- * A Magic 8-ball you shake for an answer.
- *
- * One opaque full-screen pass: a studio backdrop, the glossy black sphere with
- * its printed 8, the moulded bezel, the domed glass, and the window — murky
- * blue fluid and the answer die floating up through it.
- *
- * Everything shiny is lit by ONE environment (`envColor`): a rectangular
- * softbox up and to the left, a wide dim fill on the right, and a studio sweep
- * behind. The sphere, the bezel and the glass all reflect that same room, so
- * their highlights agree — which is the difference between a photographed
- * object and a pile of `pow(dot(n, h), k)` blobs.
- *
- * The die is drawn in the GRAVITY frame: it rises along whichever way is
- * really up, so tipping the phone changes the corner of the window the answer
- * arrives from. It is blurred and lost in the ink while it is deep (the
- * triangle's edge softness IS the focus cue) and snaps sharp as it presses
- * against the glass. The answer text itself is a React Native `Text` laid over
- * the window, since a fragment shader is the wrong tool for a glyph.
- *
+ * A clear glass orb full of pale mist, resting on a brass stand. Shaking stirs
+ * the mist shut. Once it settles, a clearing opens along whichever way is
+ * really up and the answer, a React Native `Text` laid over the shader, shows
+ * through it.
*/
export default function Magic8Ball({
params,
@@ -40,7 +24,7 @@ export default function Magic8Ball({
}: Props) {
return (
,
time: vec4,
@@ -61,6 +45,10 @@ struct Uniforms {
};
@group(0) @binding(0) var u: Uniforms;
+// Must match SUBMERGED_DEPTH in useMagic8BallPhysics.
+const SUBMERGED_DEPTH: f32 = 1.2;
+const KEY: vec3 = vec3(-0.56, 0.72, 0.41);
+
fn hash21(p: vec2) -> f32 {
var p3 = fract(vec3(p.xyx) * 0.1031);
p3 = p3 + dot(p3, p3.yzx + 33.33);
@@ -78,471 +66,145 @@ fn vnoise(p: vec2) -> f32 {
return mix(mix(a, b, w.x), mix(c, d, w.x), w.y);
}
-/** Three octaves is enough for ink: any more and the murk turns to gravel. */
fn fbm(p0: vec2) -> f32 {
var p = p0;
var amp = 0.5;
var acc = 0.0;
- for (var i = 0; i < 3; i = i + 1) {
+ for (var i = 0; i < 4; i = i + 1) {
acc = acc + amp * vnoise(p);
- p = p * 2.07;
+ p = p * 2.03 + vec2(3.1, 1.7);
amp = amp * 0.5;
}
return acc;
}
-/** The one key light in the room: a softbox up and to the left, in front. */
-fn keyDir() -> vec3 {
- return normalize(vec3(-0.67, 0.70, 0.26));
+fn rotate(p: vec2, a: f32) -> vec2 {
+ let c = cos(a);
+ let s = sin(a);
+ return vec2(p.x * c - p.y * s, p.x * s + p.y * c);
}
-/**
- * The room, as a function of direction — the only light source in this shader.
- *
- * A glossy black sphere has almost no diffuse of its own: what you read as its
- * shape is entirely the room reflected in it. So the key is modelled as an
- * actual RECTANGLE, not a phong lobe. A softbox has straight edges and a hard
- * corner, and that straight-edged reflection sliding over a curve is most of
- * what says "photographed plastic".
- *
- * \`rough\` widens the box edges and dims it to match, so the same room can be
- * sampled by a mirror sphere, a satin bezel and a glass dome and still look
- * like one room at three gloss levels.
- */
-fn envColor(d: vec3, rough: f32) -> vec3 {
- // The sweep behind: dim and cool up high, falling to near black at the floor.
- var c = mix(vec3(0.013, 0.015, 0.021),
- vec3(0.085, 0.100, 0.140),
- smoothstep(-0.55, 0.85, d.y));
- c = mix(vec3(0.006, 0.006, 0.009), c, smoothstep(-0.95, -0.30, d.y));
+fn backdrop(p: vec2) -> vec3 {
+ let pool = exp(-dot(p - vec2(0.0, 0.5), p - vec2(0.0, 0.5)) / 7.0);
+ return mix(vec3(0.008, 0.014, 0.016), vec3(0.060, 0.115, 0.120), pool);
+}
- let k = keyDir();
+// A paned window high on the left and a dim fill on the right: all the glass
+// ever reflects, so every highlight on the orb agrees.
+fn room(d: vec3) -> vec3 {
+ var c = mix(vec3(0.01, 0.015, 0.018), vec3(0.10, 0.15, 0.16),
+ smoothstep(-0.6, 0.9, d.y));
+ let k = normalize(KEY);
let tx = normalize(cross(k, vec3(0.0, 1.0, 0.0)));
let ty = cross(tx, k);
- let ck = dot(d, k);
- if (ck > 0.02) {
- // Project onto the plane facing the light, then a soft-edged rectangle.
- let pd = d - k * ck;
- let ax = abs(dot(pd, tx));
- let ay = abs(dot(pd, ty));
- // A rounded box, not two crossed smoothsteps: a softbox has a diffuser
- // and a frame, so its corners are round, and a sphere stretches whatever
- // shape you give it. Square corners come back as a strip of tape.
- let soft = 0.026 + rough * 0.55;
- let crn = 0.075;
- let bd = vec2(ax, ay) - vec2(0.125, 0.195) + crn;
- let sdf = length(max(bd, vec2(0.0)))
- + min(max(bd.x, bd.y), 0.0) - crn;
- let box = 1.0 - smoothstep(-soft, soft, sdf);
- // A real softbox is orders of magnitude over mid grey, which is why its
- // reflection clips to white through a 4% Fresnel. Dim with roughness so
- // spreading it over a wider lobe does not also brighten it.
- c = c + vec3(1.0, 0.98, 0.95) * box * (26.0 / (1.0 + rough * 7.0));
+ let facing = dot(d, k);
+ if (facing > 0.0) {
+ let w = vec2(dot(d, tx), dot(d, ty));
+ let frame = 1.0 - smoothstep(0.0, 0.02, max(abs(w.x) - 0.15, abs(w.y) - 0.21));
+ let mullion = smoothstep(0.004, 0.012, abs(w.x))
+ * smoothstep(0.004, 0.012, abs(w.y));
+ c = c + vec3(1.0, 0.97, 0.9) * frame * mullion * 9.0;
}
-
- // A broad, dim fill panel on the other side, so the shadow half of anything
- // round keeps some shape instead of going flat black.
- let fl = normalize(vec3(0.80, -0.10, 0.58));
- let cf = max(dot(d, fl), 0.0);
- c = c + vec3(0.20, 0.25, 0.36) * cf * cf * cf * 0.55;
-
- // Two wall washes BEHIND the ball. A sphere mirrors everything in front of
- // it into the inner half of its disc, which on this object is covered by the
- // window — so the only thing that can shape the black ring around the bezel
- // is light from behind. These two draw the crescents down its silhouette.
- let bkl = normalize(vec3(-0.66, 0.46, -0.60));
- let cl = max(dot(d, bkl), 0.0);
- c = c + vec3(0.62, 0.70, 0.92) * cl * cl * cl * cl * 1.75;
- let bkr = normalize(vec3(0.78, 0.10, -0.62));
- let cr = max(dot(d, bkr), 0.0);
- c = c + vec3(0.30, 0.35, 0.48) * cr * cr * cr * cr * cr * cr * 0.85;
- return c;
-}
-
-/** Schlick, for a dielectric. \`ct\` is cos(view, normal). */
-fn fres(ct: f32) -> f32 {
- let m = clamp(1.0 - ct, 0.0, 1.0);
- let m2 = m * m;
- return 0.04 + 0.96 * m2 * m2 * m;
-}
-
-/** Mirror the view direction (0,0,1) about a normal. */
-fn mirror(n: vec3) -> vec3 {
- return 2.0 * n.z * n - vec3(0.0, 0.0, 1.0);
+ let fill = max(dot(d, normalize(vec3(0.8, -0.2, 0.55))), 0.0);
+ return c + vec3(0.3, 0.42, 0.42) * fill * fill * fill * 0.5;
}
-/** Circle outline, signed. Positive outside the stroke. */
-fn ringField(q: vec2, rad: f32, th: f32) -> f32 {
- return abs(length(q) - rad) - th;
+fn fresnel(cosine: f32) -> f32 {
+ let m = clamp(1.0 - cosine, 0.0, 1.0);
+ return 0.04 + 0.96 * m * m * m * m * m;
}
-/**
- * Signed distance to the line through p and q, positive on the side away from
- * the interior point g. The building block for every part of the die.
- */
-fn edgeHp(x: vec2, p: vec2, q: vec2, g: vec2) -> f32 {
- let e = q - p;
- var n = normalize(vec2(e.y, -e.x));
- if (dot(p - g, n) < 0.0) {
- n = -n;
- }
- return dot(x - p, n);
-}
-
-/** Distance from x to the segment a-b. */
-fn segD(x: vec2, a: vec2, b: vec2) -> f32 {
- let xa = x - a;
- let ba = b - a;
- let h = clamp(dot(xa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0);
- return length(xa - ba * h);
+fn roundBox(p: vec2, half: vec2, r: f32) -> f32 {
+ let d = abs(p) - half + r;
+ return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - r;
}
-/**
- * True signed distance to convex triangle a-b-c. Negative inside.
- *
- * A max of the three edge half-planes would be cheaper and is exact inside —
- * but outside a corner it reads the distance to the nearer EDGE LINE, not to
- * the corner, so subtracting a radius from it mitres the corner instead of
- * rounding it. Rounded corners are the whole reason this is here.
- */
-fn sdTri(x: vec2, a: vec2, b: vec2, c: vec2) -> f32 {
- let g = (a + b + c) / 3.0;
- let d = min(min(segD(x, a, b), segD(x, b, c)), segD(x, c, a));
- var s = edgeHp(x, a, b, g);
- s = max(s, edgeHp(x, b, c, g));
- s = max(s, edgeHp(x, c, a, g));
- return select(d, -d, s < 0.0);
+fn brass(x: f32, y: f32) -> vec3 {
+ let sheen = pow(max(cos(x * 2.6 + 0.7), 0.0), 3.0);
+ let shade = mix(vec3(0.10, 0.065, 0.025), vec3(0.62, 0.45, 0.19), sheen);
+ return shade * (0.7 + 0.5 * smoothstep(-0.12, 0.12, y));
}
-/** True signed distance to convex quad a-b-c-d, wound in order. */
-fn sdQuad(x: vec2