react-native-parity-blur 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/ParityBlur.podspec +20 -0
- package/README.md +198 -0
- package/android/build.gradle +57 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/parityblur/AndroidBlurCalibration.kt +82 -0
- package/android/src/main/java/com/parityblur/BlurEngine.kt +132 -0
- package/android/src/main/java/com/parityblur/ParityBlurDebug.kt +24 -0
- package/android/src/main/java/com/parityblur/ParityBlurPackage.kt +17 -0
- package/android/src/main/java/com/parityblur/ParityBlurView.kt +542 -0
- package/android/src/main/java/com/parityblur/ParityBlurViewManager.kt +161 -0
- package/android/src/main/java/com/parityblur/PipelineMath.kt +202 -0
- package/android/src/main/java/com/parityblur/PixelCopySnapshotProvider.kt +108 -0
- package/android/src/main/java/com/parityblur/SoftwareSnapshotProvider.kt +173 -0
- package/android/src/main/java/com/parityblur/WindowBlurContext.kt +116 -0
- package/android/src/test/java/com/parityblur/MiniJson.kt +179 -0
- package/android/src/test/java/com/parityblur/PipelineFixturesTest.kt +222 -0
- package/ios/BlurEngine.swift +106 -0
- package/ios/ColorPipeline.swift +72 -0
- package/ios/MetalPresentationSurface.swift +51 -0
- package/ios/ParityBlurCoreView.swift +382 -0
- package/ios/ParityBlurDebug.swift +18 -0
- package/ios/ParityBlurView.h +14 -0
- package/ios/ParityBlurView.mm +135 -0
- package/ios/PipelineMath.swift +177 -0
- package/ios/SnapshotProvider.swift +128 -0
- package/ios/WindowBlurContext.swift +111 -0
- package/lib/module/BlurView.js +124 -0
- package/lib/module/BlurView.js.map +1 -0
- package/lib/module/ParityBlurViewNativeComponent.ts +82 -0
- package/lib/module/defaults.js +13 -0
- package/lib/module/defaults.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/pipeline/androidCalibration.js +55 -0
- package/lib/module/pipeline/androidCalibration.js.map +1 -0
- package/lib/module/pipeline/captureRect.js +151 -0
- package/lib/module/pipeline/captureRect.js.map +1 -0
- package/lib/module/pipeline/color.js +82 -0
- package/lib/module/pipeline/color.js.map +1 -0
- package/lib/module/pipeline/constants.js +65 -0
- package/lib/module/pipeline/constants.js.map +1 -0
- package/lib/module/pipeline/downsample.js +61 -0
- package/lib/module/pipeline/downsample.js.map +1 -0
- package/lib/module/pipeline/index.js +23 -0
- package/lib/module/pipeline/index.js.map +1 -0
- package/lib/module/pipeline/overlay.js +56 -0
- package/lib/module/pipeline/overlay.js.map +1 -0
- package/lib/module/pipeline/saturation.js +52 -0
- package/lib/module/pipeline/saturation.js.map +1 -0
- package/lib/module/pipeline/types.js +23 -0
- package/lib/module/pipeline/types.js.map +1 -0
- package/lib/module/pipeline/units.js +33 -0
- package/lib/module/pipeline/units.js.map +1 -0
- package/lib/module/presets.js +96 -0
- package/lib/module/presets.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/BlurView.d.ts +16 -0
- package/lib/typescript/src/BlurView.d.ts.map +1 -0
- package/lib/typescript/src/ParityBlurViewNativeComponent.d.ts +63 -0
- package/lib/typescript/src/ParityBlurViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/defaults.d.ts +12 -0
- package/lib/typescript/src/defaults.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/androidCalibration.d.ts +42 -0
- package/lib/typescript/src/pipeline/androidCalibration.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/captureRect.d.ts +84 -0
- package/lib/typescript/src/pipeline/captureRect.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/color.d.ts +19 -0
- package/lib/typescript/src/pipeline/color.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/constants.d.ts +52 -0
- package/lib/typescript/src/pipeline/constants.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/downsample.d.ts +31 -0
- package/lib/typescript/src/pipeline/downsample.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/index.d.ts +20 -0
- package/lib/typescript/src/pipeline/index.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/overlay.d.ts +31 -0
- package/lib/typescript/src/pipeline/overlay.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/saturation.d.ts +38 -0
- package/lib/typescript/src/pipeline/saturation.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/types.d.ts +32 -0
- package/lib/typescript/src/pipeline/types.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/units.d.ts +23 -0
- package/lib/typescript/src/pipeline/units.d.ts.map +1 -0
- package/lib/typescript/src/presets.d.ts +57 -0
- package/lib/typescript/src/presets.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +116 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +167 -0
- package/src/BlurView.tsx +195 -0
- package/src/ParityBlurViewNativeComponent.ts +82 -0
- package/src/defaults.ts +20 -0
- package/src/index.ts +10 -0
- package/src/pipeline/androidCalibration.ts +64 -0
- package/src/pipeline/captureRect.ts +172 -0
- package/src/pipeline/color.ts +66 -0
- package/src/pipeline/constants.ts +65 -0
- package/src/pipeline/downsample.ts +74 -0
- package/src/pipeline/index.ts +20 -0
- package/src/pipeline/overlay.ts +46 -0
- package/src/pipeline/saturation.ts +65 -0
- package/src/pipeline/types.ts +37 -0
- package/src/pipeline/units.ts +32 -0
- package/src/presets.ts +103 -0
- package/src/types.ts +132 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lakhwinder Singh
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "ParityBlur"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/lakhwindersingh/react-native-parity-blur.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
18
|
+
|
|
19
|
+
install_modules_dependencies(s)
|
|
20
|
+
end
|
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# react-native-parity-blur
|
|
2
|
+
|
|
3
|
+
Cross-platform native backdrop blur for React Native (New Architecture / Fabric).
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Every React Native blur library today wraps a different native primitive with a different
|
|
8
|
+
default look — `UIVisualEffectView` on iOS, a custom renderscript/RenderEffect approximation on
|
|
9
|
+
Android — and ships no cross-platform definition of what a given "blur amount" should look like.
|
|
10
|
+
The result: the same prop value produces a visibly different blur on each platform, and there is
|
|
11
|
+
no way to reason about it other than eyeballing a device.
|
|
12
|
+
|
|
13
|
+
`react-native-parity-blur` starts from one canonical, documented blur pipeline
|
|
14
|
+
(docs/PIPELINE_SPEC.md) — a single Gaussian-sigma unit, a single capture/downsample/blur/
|
|
15
|
+
saturate/overlay order, the same edge handling, the same color space — and implements it natively
|
|
16
|
+
on both platforms: real `RenderEffect` on Android (API 31+), real `MPSImageGaussianBlur` on iOS.
|
|
17
|
+
The two backends were then calibrated against each other on physical devices
|
|
18
|
+
(docs/CALIBRATION_REPORT.md): across 110 measured strip comparisons, the worst per-strip color
|
|
19
|
+
difference is **ΔE76 0.90** and the lowest structural similarity is **SSIM 0.983** — i.e. the same
|
|
20
|
+
`blurRadius` really does mean the same thing on both platforms, not just approximately.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install react-native-parity-blur
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
or
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
yarn add react-native-parity-blur
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This library ships native Kotlin and Swift code and requires **autolinking** (no manual native
|
|
35
|
+
setup) and a native rebuild after install (`pod install` for iOS, then a full app rebuild — a
|
|
36
|
+
JS-only reload is not enough).
|
|
37
|
+
|
|
38
|
+
### Requirements
|
|
39
|
+
|
|
40
|
+
- **React Native 0.76+ with the New Architecture (Fabric) enabled.** This is a Fabric-only
|
|
41
|
+
package (`create-react-native-library` type `fabric-view`); it does not support the legacy
|
|
42
|
+
(Paper) renderer.
|
|
43
|
+
- **iOS**: real blur on all supported iOS versions via Metal Performance Shaders.
|
|
44
|
+
- **Android**: real blur requires **API 31+** (`RenderEffect`). Below API 31 there is no CPU blur
|
|
45
|
+
fallback by design (plan §44) — the view instead renders `fallbackColor` as a flat color. Set a
|
|
46
|
+
`fallbackColor` close to your intended blurred backdrop's average color so older devices still
|
|
47
|
+
look reasonable.
|
|
48
|
+
- iOS "Reduce Transparency" (accessibility) forces the same `fallbackColor` fallback path on iOS,
|
|
49
|
+
for the same reason system blur is disabled there.
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { BlurView } from 'react-native-parity-blur';
|
|
55
|
+
|
|
56
|
+
function BottomSheet() {
|
|
57
|
+
return (
|
|
58
|
+
<BlurView
|
|
59
|
+
style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: 280 }}
|
|
60
|
+
blurRadius={20}
|
|
61
|
+
overlayColor="rgba(16,16,16,0.35)"
|
|
62
|
+
saturation={1.4}
|
|
63
|
+
fallbackColor="rgba(20,20,20,0.92)"
|
|
64
|
+
>
|
|
65
|
+
<Text>Sharp content on top of a blurred backdrop</Text>
|
|
66
|
+
</BlurView>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
By default (`mode="static"`), the backdrop is captured once — on mount and on layout/relevant-prop
|
|
72
|
+
changes — and presented as a still blurred snapshot. If content behind the view changes afterward
|
|
73
|
+
without a layout change, call `refresh()` (see below) or switch to `mode="live"`.
|
|
74
|
+
|
|
75
|
+
## The canonical unit: `blurRadius`
|
|
76
|
+
|
|
77
|
+
`blurRadius` is a **Gaussian sigma expressed in dp** (density-independent pixels — iOS `pt` and
|
|
78
|
+
Android `dp` are treated as the same unit). It is *not* a platform-native blur-radius parameter:
|
|
79
|
+
internally it's converted to whatever each backend's real API expects (Android's `RenderEffect`
|
|
80
|
+
radius, iOS's MPS sigma — see docs/PIPELINE_SPEC.md §2), so you write one number and get a
|
|
81
|
+
calibrated, matching blur weight on both platforms. There is no "roughly similar" — see
|
|
82
|
+
[Parity](#parity) below for the measured numbers.
|
|
83
|
+
|
|
84
|
+
## Props
|
|
85
|
+
|
|
86
|
+
| Prop | Type | Default | Semantics |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| `blurRadius` | `number` | `0` | Gaussian sigma in dp — the canonical cross-platform blur unit (see above). `0` (or omitted) renders no blur. Negative/non-finite values clamp to `0` with a DEV warning. |
|
|
89
|
+
| `mode` | `'static' \| 'live'` | `'static'` | `'static'` captures the backdrop once (mount/layout/relevant-prop change) and shows a still snapshot — call `refresh()` after further backdrop changes. `'live'` recaptures continuously, throttled by `maxFps`, while the view is visible. |
|
|
90
|
+
| `preset` | `BlurPresetName` | none | Named material-like preset (see [Presets](#presets)) resolved client-side to `{ blurRadius, saturation, overlayColor }` before this component's own explicit props are layered on top. Pure JS sugar — no native prop is added. |
|
|
91
|
+
| `overlayColor` | `string` (color) | `'transparent'` | Straight-alpha color composited source-over the blurred + saturated result — the only tint anywhere in the pipeline (no hidden per-platform tint). Unparseable colors resolve to "no overlay" with a DEV warning. |
|
|
92
|
+
| `saturation` | `number` | `1` | Post-blur, pre-overlay saturation multiplier: `1` = unchanged, `0` = Rec.709 grayscale, `>1` = boosted. Negative/non-finite values clamp to `1` with a DEV warning. |
|
|
93
|
+
| `quality` | `'high' \| 'balanced' \| 'performance'` | `'balanced'` | Biases automatic downsample selection: `high` prefers 1×/2× snapshots, `balanced` prefers 2×/4×, `performance` prefers 4×/8×. No effect when `downsample` is an explicit number. |
|
|
94
|
+
| `downsample` | `'auto' \| 1 \| 2 \| 4 \| 8` | `'auto'` | Snapshot downsample factor. `'auto'` derives a factor from sigma, capture area, and `quality`; an explicit factor overrides selection for advanced tuning. |
|
|
95
|
+
| `maxFps` | `number` | `30` | Upper bound on live-mode recapture rate. Ignored in `'static'` mode. Clamped to `[1, 120]` with a DEV warning outside that range. |
|
|
96
|
+
| `fallbackColor` | `string` (color) | none | Color rendered instead of real blur wherever real blur is unavailable (Android < API 31, or "Reduce Transparency" on either platform). No effect where real blur runs. If unset, the fallback path renders fully transparent. |
|
|
97
|
+
| `blurTarget` | `React.RefObject<any>` | none | **Not implemented in v1** — accepted only to keep the public API shape stable; passing it currently has no effect. See [Known limitations](docs/LIMITATIONS.md). Planned for v1.1. |
|
|
98
|
+
| `children` | `React.ReactNode` | — | Rendered on top of the blur, unaffected by it. |
|
|
99
|
+
| `style` | `StyleProp<ViewStyle>` | — | Standard RN view style. `borderRadius` (and other clip styles) clip the blur output itself, not just the children. |
|
|
100
|
+
|
|
101
|
+
Imperative ref:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
const blurRef = useRef<BlurViewRef>(null);
|
|
105
|
+
// ...
|
|
106
|
+
<BlurView ref={blurRef} mode="static" blurRadius={20}>
|
|
107
|
+
{children}
|
|
108
|
+
</BlurView>
|
|
109
|
+
// later, after the backdrop behind it changed:
|
|
110
|
+
blurRef.current?.refresh();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`refresh()` schedules a coalesced recapture on the next valid frame — it is a real, fully wired
|
|
114
|
+
native command on both platforms (not a stub). Repeated calls before the pending capture runs
|
|
115
|
+
coalesce into a single recapture, so it's safe to call from scroll/animation callbacks.
|
|
116
|
+
|
|
117
|
+
## Presets
|
|
118
|
+
|
|
119
|
+
A preset is nothing more than a fixed `{ blurRadius, saturation, overlayColor }` bundle — plain
|
|
120
|
+
public props, no hidden or platform-specific backend behavior (plan §11). Spread one directly:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { BlurPresets } from 'react-native-parity-blur';
|
|
124
|
+
|
|
125
|
+
<BlurView {...BlurPresets.dark} style={styles.sheet}>
|
|
126
|
+
{children}
|
|
127
|
+
</BlurView>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
or pass the name via the `preset` prop, still overridable per-field:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
<BlurView preset="dark" blurRadius={30} style={styles.sheet}>
|
|
134
|
+
{children}
|
|
135
|
+
</BlurView>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Available presets: `ultraThin`, `thin`, `regular`, `thick`, `chrome`, `light`, `dark`. These are
|
|
139
|
+
deterministic approximations of common material "weights" (inspired by, but not measured against,
|
|
140
|
+
iOS system materials) — see `src/presets.ts` for the exact values and rationale. Because
|
|
141
|
+
`blurRadius`/`saturation`/`overlayColor` are calibrated cross-platform, any preset renders the same
|
|
142
|
+
on iOS and Android.
|
|
143
|
+
|
|
144
|
+
## Static vs. live mode
|
|
145
|
+
|
|
146
|
+
- **`mode="static"`** (default): capture happens once, then the blurred result is a cheap static
|
|
147
|
+
presentation with no ongoing per-frame cost — measured at **0 rendered frames over 6s of idle
|
|
148
|
+
time** on Android (docs/HARDENING_REPORT.md). Use this for bottom sheets, modals, and any
|
|
149
|
+
backdrop that only changes on discrete events (call `refresh()` after those events).
|
|
150
|
+
- **`mode="live"`** continuously recaptures the backdrop, throttled to `maxFps` (default 30) and
|
|
151
|
+
paused automatically when the view isn't visible. Use this for blur over actively scrolling or
|
|
152
|
+
animating content (e.g. a blurred header above a scrolling list). It costs strictly more than
|
|
153
|
+
static — see [Performance](docs/PERFORMANCE.md) for measured numbers and when the extra cost is
|
|
154
|
+
and isn't worth it, and note iOS live is the more expensive of the two platforms' live paths.
|
|
155
|
+
|
|
156
|
+
## Parity
|
|
157
|
+
|
|
158
|
+
Measured on physical devices (Android Pixel 6a API 36, iOS iPhone 14 Pro) across 5 blur radii × 6
|
|
159
|
+
color/contrast fixtures × clear/tinted variants — 110 valid strip comparisons
|
|
160
|
+
(docs/CALIBRATION_REPORT.md):
|
|
161
|
+
|
|
162
|
+
| Metric | Worst observed | Notes |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| Per-strip mean ΔE76 (color) | **0.90** | at σ=32dp on a full-frame luminance ramp (screen-edge capture-support truncation); every other strip ≤ 0.64 |
|
|
165
|
+
| Per-strip SSIM (structural similarity) | **0.983** | same worst-case strip; all others ≥ 0.995 |
|
|
166
|
+
| Edge-crossing displacement | ≤ 0.96 dp | sub-pixel on both device rasters |
|
|
167
|
+
| Overlay/saturation tint residual | ≤ 1.45 /255 | confirms the overlay and saturation math match on both platforms |
|
|
168
|
+
| Convolution color space | gamma-space on both | confirmed via a checkerboard gamma-midpoint discriminator |
|
|
169
|
+
|
|
170
|
+
The framing is deliberately "perceptually matched within documented tolerance," not pixel-identical
|
|
171
|
+
— exact pixel identity isn't guaranteed (and isn't the goal) across OS versions, GPU vendors, and
|
|
172
|
+
device scales. See docs/CALIBRATION_REPORT.md for the full methodology and per-fixture breakdown,
|
|
173
|
+
and docs/PIPELINE_SPEC.md for the locked pipeline both backends implement.
|
|
174
|
+
|
|
175
|
+
## Known limitations & performance
|
|
176
|
+
|
|
177
|
+
- [docs/LIMITATIONS.md](docs/LIMITATIONS.md) — cross-window/Modal capture, surface-backed content
|
|
178
|
+
(video/camera/maps), static-mode staleness, iOS live cost, nested-blur semantics, `blurTarget`
|
|
179
|
+
status, and more.
|
|
180
|
+
- [docs/PERFORMANCE.md](docs/PERFORMANCE.md) — measured frame/GPU costs, static-vs-live guidance,
|
|
181
|
+
downsample/quality tuning, `maxFps` guidance.
|
|
182
|
+
- [docs/PIPELINE_SPEC.md](docs/PIPELINE_SPEC.md) — the normative, language-neutral pipeline spec
|
|
183
|
+
both native backends implement.
|
|
184
|
+
- [docs/CALIBRATION_REPORT.md](docs/CALIBRATION_REPORT.md) — the full parity measurement report.
|
|
185
|
+
|
|
186
|
+
## Contributing
|
|
187
|
+
|
|
188
|
+
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
189
|
+
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
190
|
+
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.ParityBlur = [
|
|
3
|
+
kotlinVersion: "2.0.21",
|
|
4
|
+
minSdkVersion: 24,
|
|
5
|
+
compileSdkVersion: 36
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
ext.getExtOrDefault = { prop ->
|
|
9
|
+
if (rootProject.ext.has(prop)) {
|
|
10
|
+
return rootProject.ext.get(prop)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return ParityBlur[prop]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
repositories {
|
|
17
|
+
google()
|
|
18
|
+
mavenCentral()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
dependencies {
|
|
22
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
23
|
+
// noinspection DifferentKotlinGradleVersion
|
|
24
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
apply plugin: "com.android.library"
|
|
30
|
+
apply plugin: "kotlin-android"
|
|
31
|
+
|
|
32
|
+
apply plugin: "com.facebook.react"
|
|
33
|
+
|
|
34
|
+
android {
|
|
35
|
+
namespace "com.parityblur"
|
|
36
|
+
|
|
37
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
38
|
+
|
|
39
|
+
defaultConfig {
|
|
40
|
+
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
compileOptions {
|
|
44
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
45
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
dependencies {
|
|
50
|
+
implementation "com.facebook.react:react-android"
|
|
51
|
+
|
|
52
|
+
// Minimal JVM unit-test setup for the Milestone 3 pipeline-fixture parity suite
|
|
53
|
+
// (android/src/test/java/com/parityblur/PipelineFixturesTest.kt). Pure JUnit4 -- the
|
|
54
|
+
// AndroidBlurCalibration/PipelineMath classes under test have no android.* imports, so no
|
|
55
|
+
// Robolectric/instrumentation is needed.
|
|
56
|
+
testImplementation "junit:junit:4.13.2"
|
|
57
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
package com.parityblur
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Locked calibration constants (docs/PIPELINE_SPEC.md §1-2, plan §5.2, §10, §12-13).
|
|
5
|
+
*
|
|
6
|
+
* Exact Kotlin mirror of `src/pipeline/constants.ts` + `src/pipeline/androidCalibration.ts`.
|
|
7
|
+
* This is the SINGLE home for the HWUI sigma<->radius relation and the other locked pipeline
|
|
8
|
+
* constants on the Android side (plan §45.10: do not hard-code Android conversion logic
|
|
9
|
+
* throughout the codebase). `android/src/test/.../PipelineFixturesTest.kt` asserts these
|
|
10
|
+
* functions match `test/pipeline-fixtures.json` within 1e-6.
|
|
11
|
+
*
|
|
12
|
+
* All math here is pure Double arithmetic with NO android.* imports, so it is testable as a
|
|
13
|
+
* plain JVM unit test without Robolectric.
|
|
14
|
+
*/
|
|
15
|
+
object AndroidBlurCalibration {
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* HWUI RenderEffect Gaussian relation, verified exact on a Pixel 6a (API 36) in M0:
|
|
19
|
+
* `sigma = HWUI_SIGMA_SLOPE * radiusPlatform + HWUI_SIGMA_INTERCEPT`.
|
|
20
|
+
*/
|
|
21
|
+
const val HWUI_SIGMA_SLOPE: Double = 0.57735
|
|
22
|
+
const val HWUI_SIGMA_INTERCEPT: Double = 0.5
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Below this snapshot-domain sigma the inverse radius is non-positive, so we emit a no-blur
|
|
26
|
+
* passthrough instead of feeding a bogus radius to RenderEffect.createBlurEffect. Equal to
|
|
27
|
+
* HWUI_SIGMA_INTERCEPT by construction (radius 0 <=> sigma 0.5).
|
|
28
|
+
*/
|
|
29
|
+
const val ANDROID_MIN_BLUR_SIGMA: Double = HWUI_SIGMA_INTERCEPT
|
|
30
|
+
|
|
31
|
+
/** Gaussian support multiplier for capture expansion (plan §7). */
|
|
32
|
+
const val CAPTURE_SUPPORT_K: Double = 3.0
|
|
33
|
+
|
|
34
|
+
/** Auto-downsample: keep sigmaSnapshot = sigmaPx / D >= this many snapshot px. */
|
|
35
|
+
const val MIN_SIGMA_SNAPSHOT: Double = 1.0
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Auto-downsample: captures with area (device px^2) below this are capped at 2x.
|
|
39
|
+
* 256 * 256 device px.
|
|
40
|
+
*/
|
|
41
|
+
const val SMALL_CAPTURE_AREA_PX: Double = 256.0 * 256.0
|
|
42
|
+
|
|
43
|
+
/** Auto-downsample: per-quality ceiling on the downsample factor (plan §13). */
|
|
44
|
+
const val QUALITY_MAX_DOWNSAMPLE_HIGH: Int = 2
|
|
45
|
+
const val QUALITY_MAX_DOWNSAMPLE_BALANCED: Int = 4
|
|
46
|
+
const val QUALITY_MAX_DOWNSAMPLE_PERFORMANCE: Int = 8
|
|
47
|
+
|
|
48
|
+
/** Rec. 709 luminance coefficients (plan §10), used by the saturation matrix. */
|
|
49
|
+
const val LUMA_R: Double = 0.2126
|
|
50
|
+
const val LUMA_G: Double = 0.7152
|
|
51
|
+
const val LUMA_B: Double = 0.0722
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Result of [radiusForSigma]. [radiusPlatform] is always >= 0; it is 0 when [noBlur] is true.
|
|
55
|
+
* `radiusPlatform` is intentionally Double (not Float) so the pure-math mirror can be asserted
|
|
56
|
+
* against the fixture table within 1e-6 -- callers cast to Float only at the RenderEffect call
|
|
57
|
+
* site.
|
|
58
|
+
*/
|
|
59
|
+
data class BlurParam(val noBlur: Boolean, val radiusPlatform: Double)
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Forward relation, exposed for tests/verification: the sigma HWUI produces for a given
|
|
63
|
+
* platform radius.
|
|
64
|
+
*/
|
|
65
|
+
fun sigmaForRadius(radiusPlatform: Double): Double =
|
|
66
|
+
HWUI_SIGMA_SLOPE * radiusPlatform + HWUI_SIGMA_INTERCEPT
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Inverse relation: the platform radius required to achieve a target snapshot-domain sigma.
|
|
70
|
+
* Below the intercept this returns a no-blur result. Monotonically non-decreasing in
|
|
71
|
+
* sigmaSnapshot. Exact mirror of androidCalibration.ts `radiusForSigma`.
|
|
72
|
+
*/
|
|
73
|
+
fun radiusForSigma(sigmaSnapshot: Double): BlurParam {
|
|
74
|
+
if (!sigmaSnapshot.isFinite() || sigmaSnapshot <= ANDROID_MIN_BLUR_SIGMA) {
|
|
75
|
+
return BlurParam(noBlur = true, radiusPlatform = 0.0)
|
|
76
|
+
}
|
|
77
|
+
return BlurParam(
|
|
78
|
+
noBlur = false,
|
|
79
|
+
radiusPlatform = (sigmaSnapshot - HWUI_SIGMA_INTERCEPT) / HWUI_SIGMA_SLOPE
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
package com.parityblur
|
|
2
|
+
|
|
3
|
+
import android.content.ComponentCallbacks2
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.res.Configuration
|
|
6
|
+
import android.graphics.Bitmap
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import android.view.View
|
|
9
|
+
import java.util.WeakHashMap
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Process-wide blur engine (plan §4, §14.5, §26, §27).
|
|
13
|
+
*
|
|
14
|
+
* Created LAZILY on first eligible capture -- NEVER at class load / package init / module
|
|
15
|
+
* import. API<31 and no-blur-only [ParityBlurView] instances must never call [get] (plan §27,
|
|
16
|
+
* §45.1); enforcing that is the caller's responsibility (see ParityBlurView.isRealBlurSupported).
|
|
17
|
+
*
|
|
18
|
+
* Holds:
|
|
19
|
+
* - trim-memory registration (ComponentCallbacks2) that clears the bitmap pool under memory
|
|
20
|
+
* pressure (plan §26).
|
|
21
|
+
* - a small bitmap pool keyed by (width, height), capped in retained bytes (plan §14.5: a pool
|
|
22
|
+
* is justified here because the software capture path allocates a bitmap per capture --
|
|
23
|
+
* M0-REPORT "Plan amendments" #2).
|
|
24
|
+
* - a weak per-window registry of [WindowBlurContext] (plan §4), keyed by each window's root
|
|
25
|
+
* view so contexts disappear with their window and are never a source of leaks.
|
|
26
|
+
*/
|
|
27
|
+
class BlurEngine private constructor() : ComponentCallbacks2 {
|
|
28
|
+
|
|
29
|
+
private val bitmapPool = HashMap<Long, MutableList<Bitmap>>()
|
|
30
|
+
private var pooledBytes = 0L
|
|
31
|
+
|
|
32
|
+
private val windowContexts = WeakHashMap<View, WindowBlurContext>()
|
|
33
|
+
|
|
34
|
+
@Synchronized
|
|
35
|
+
fun windowContextFor(rootView: View): WindowBlurContext {
|
|
36
|
+
return windowContexts.getOrPut(rootView) { WindowBlurContext(rootView) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Synchronized
|
|
40
|
+
fun releaseWindowContext(rootView: View) {
|
|
41
|
+
windowContexts.remove(rootView)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Acquire a pooled ARGB_8888 bitmap sized exactly (width x height), or allocate one. */
|
|
45
|
+
@Synchronized
|
|
46
|
+
fun acquireBitmap(width: Int, height: Int): Bitmap {
|
|
47
|
+
val key = poolKey(width, height)
|
|
48
|
+
val bucket = bitmapPool[key]
|
|
49
|
+
val reused = if (!bucket.isNullOrEmpty()) bucket.removeAt(bucket.size - 1) else null
|
|
50
|
+
if (reused != null && !reused.isRecycled) {
|
|
51
|
+
pooledBytes -= reused.allocationByteCount.toLong()
|
|
52
|
+
reused.eraseColor(0)
|
|
53
|
+
return reused
|
|
54
|
+
}
|
|
55
|
+
return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Return a bitmap to the pool for reuse, capping total retained bytes (plan §14.5). */
|
|
59
|
+
@Synchronized
|
|
60
|
+
fun releaseBitmap(bitmap: Bitmap) {
|
|
61
|
+
if (bitmap.isRecycled) return
|
|
62
|
+
val size = bitmap.allocationByteCount.toLong()
|
|
63
|
+
if (pooledBytes + size > MAX_POOL_BYTES) {
|
|
64
|
+
// Do not grow unbounded and do not evict-and-thrash; just drop this one (plan §45.13:
|
|
65
|
+
// do not pool large resources without measured need -- keep the cap conservative).
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
val key = poolKey(bitmap.width, bitmap.height)
|
|
69
|
+
val bucket = bitmapPool.getOrPut(key) { mutableListOf() }
|
|
70
|
+
bucket.add(bitmap)
|
|
71
|
+
pooledBytes += size
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Synchronized
|
|
75
|
+
private fun clearPool() {
|
|
76
|
+
for (bucket in bitmapPool.values) {
|
|
77
|
+
for (bmp in bucket) if (!bmp.isRecycled) bmp.recycle()
|
|
78
|
+
bucket.clear()
|
|
79
|
+
}
|
|
80
|
+
bitmapPool.clear()
|
|
81
|
+
pooledBytes = 0L
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ------------------------------------------------------- ComponentCallbacks2 (plan §26)
|
|
85
|
+
|
|
86
|
+
@Suppress("DEPRECATION")
|
|
87
|
+
override fun onTrimMemory(level: Int) {
|
|
88
|
+
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW ||
|
|
89
|
+
level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
|
|
90
|
+
) {
|
|
91
|
+
Log.d(TAG, "onTrimMemory($level): clearing bitmap pool ($pooledBytes bytes)")
|
|
92
|
+
clearPool()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
override fun onConfigurationChanged(newConfig: Configuration) = Unit
|
|
97
|
+
|
|
98
|
+
@Suppress("OVERRIDE_DEPRECATION")
|
|
99
|
+
override fun onLowMemory() {
|
|
100
|
+
clearPool()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
companion object {
|
|
104
|
+
private const val TAG = "ParityBlur.Engine"
|
|
105
|
+
|
|
106
|
+
/** Conservative retained-bitmap cap; snapshots are already downsampled (plan §14.5). */
|
|
107
|
+
private const val MAX_POOL_BYTES = 16L * 1024 * 1024 // 16 MiB
|
|
108
|
+
|
|
109
|
+
@Volatile
|
|
110
|
+
private var instance: BlurEngine? = null
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Lazily create (or return) the process-wide engine. MUST be called only from the first
|
|
114
|
+
* eligible real-blur capture path -- never from class init, static fields, or fallback-only
|
|
115
|
+
* view code (plan §27, §45.1).
|
|
116
|
+
*/
|
|
117
|
+
fun get(context: Context): BlurEngine {
|
|
118
|
+
instance?.let { return it }
|
|
119
|
+
synchronized(this) {
|
|
120
|
+
instance?.let { return it }
|
|
121
|
+
val created = BlurEngine()
|
|
122
|
+
context.applicationContext.registerComponentCallbacks(created)
|
|
123
|
+
instance = created
|
|
124
|
+
ParityBlurDebug.log { "engine-init" }
|
|
125
|
+
return created
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private fun poolKey(width: Int, height: Int): Long =
|
|
130
|
+
(width.toLong() shl 32) or (height.toLong() and 0xffffffffL)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package com.parityblur
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Milestone 7 laziness/lifecycle instrumentation (plan §42.4, §40).
|
|
7
|
+
*
|
|
8
|
+
* Gated behind [ENABLED], which defaults to `false` so a shipped build pays zero cost here (no
|
|
9
|
+
* fields mutated, no string built, no log call made -- the `inline` + constant-folded branch is
|
|
10
|
+
* dropped by the compiler). Flip to `true` only for local hardening runs to prove:
|
|
11
|
+
* - package import -> no engine init (nothing logs before the first eligible capture)
|
|
12
|
+
* - first eligible BlurView -> "engine-init" logs exactly once per process
|
|
13
|
+
* - live registration -> "scheduler-install"/"scheduler-uninstall" bracket the live window
|
|
14
|
+
* - detach -> "instance-release" logs for every ParityBlurView, live or static
|
|
15
|
+
*/
|
|
16
|
+
internal object ParityBlurDebug {
|
|
17
|
+
const val ENABLED = false // debug-only laziness/lifecycle instrumentation (plan §42.4); flip true for local hardening runs
|
|
18
|
+
|
|
19
|
+
private const val TAG = "ParityBlur.Debug"
|
|
20
|
+
|
|
21
|
+
inline fun log(event: () -> String) {
|
|
22
|
+
if (ENABLED) Log.d(TAG, event())
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.parityblur
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
7
|
+
import com.facebook.react.uimanager.ViewManager
|
|
8
|
+
|
|
9
|
+
class ParityBlurViewPackage : BaseReactPackage() {
|
|
10
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
11
|
+
return listOf(ParityBlurViewManager())
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
|
|
15
|
+
|
|
16
|
+
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider { emptyMap() }
|
|
17
|
+
}
|