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.
Files changed (109) hide show
  1. package/LICENSE +20 -0
  2. package/ParityBlur.podspec +20 -0
  3. package/README.md +198 -0
  4. package/android/build.gradle +57 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/parityblur/AndroidBlurCalibration.kt +82 -0
  7. package/android/src/main/java/com/parityblur/BlurEngine.kt +132 -0
  8. package/android/src/main/java/com/parityblur/ParityBlurDebug.kt +24 -0
  9. package/android/src/main/java/com/parityblur/ParityBlurPackage.kt +17 -0
  10. package/android/src/main/java/com/parityblur/ParityBlurView.kt +542 -0
  11. package/android/src/main/java/com/parityblur/ParityBlurViewManager.kt +161 -0
  12. package/android/src/main/java/com/parityblur/PipelineMath.kt +202 -0
  13. package/android/src/main/java/com/parityblur/PixelCopySnapshotProvider.kt +108 -0
  14. package/android/src/main/java/com/parityblur/SoftwareSnapshotProvider.kt +173 -0
  15. package/android/src/main/java/com/parityblur/WindowBlurContext.kt +116 -0
  16. package/android/src/test/java/com/parityblur/MiniJson.kt +179 -0
  17. package/android/src/test/java/com/parityblur/PipelineFixturesTest.kt +222 -0
  18. package/ios/BlurEngine.swift +106 -0
  19. package/ios/ColorPipeline.swift +72 -0
  20. package/ios/MetalPresentationSurface.swift +51 -0
  21. package/ios/ParityBlurCoreView.swift +382 -0
  22. package/ios/ParityBlurDebug.swift +18 -0
  23. package/ios/ParityBlurView.h +14 -0
  24. package/ios/ParityBlurView.mm +135 -0
  25. package/ios/PipelineMath.swift +177 -0
  26. package/ios/SnapshotProvider.swift +128 -0
  27. package/ios/WindowBlurContext.swift +111 -0
  28. package/lib/module/BlurView.js +124 -0
  29. package/lib/module/BlurView.js.map +1 -0
  30. package/lib/module/ParityBlurViewNativeComponent.ts +82 -0
  31. package/lib/module/defaults.js +13 -0
  32. package/lib/module/defaults.js.map +1 -0
  33. package/lib/module/index.js +5 -0
  34. package/lib/module/index.js.map +1 -0
  35. package/lib/module/package.json +1 -0
  36. package/lib/module/pipeline/androidCalibration.js +55 -0
  37. package/lib/module/pipeline/androidCalibration.js.map +1 -0
  38. package/lib/module/pipeline/captureRect.js +151 -0
  39. package/lib/module/pipeline/captureRect.js.map +1 -0
  40. package/lib/module/pipeline/color.js +82 -0
  41. package/lib/module/pipeline/color.js.map +1 -0
  42. package/lib/module/pipeline/constants.js +65 -0
  43. package/lib/module/pipeline/constants.js.map +1 -0
  44. package/lib/module/pipeline/downsample.js +61 -0
  45. package/lib/module/pipeline/downsample.js.map +1 -0
  46. package/lib/module/pipeline/index.js +23 -0
  47. package/lib/module/pipeline/index.js.map +1 -0
  48. package/lib/module/pipeline/overlay.js +56 -0
  49. package/lib/module/pipeline/overlay.js.map +1 -0
  50. package/lib/module/pipeline/saturation.js +52 -0
  51. package/lib/module/pipeline/saturation.js.map +1 -0
  52. package/lib/module/pipeline/types.js +23 -0
  53. package/lib/module/pipeline/types.js.map +1 -0
  54. package/lib/module/pipeline/units.js +33 -0
  55. package/lib/module/pipeline/units.js.map +1 -0
  56. package/lib/module/presets.js +96 -0
  57. package/lib/module/presets.js.map +1 -0
  58. package/lib/module/types.js +4 -0
  59. package/lib/module/types.js.map +1 -0
  60. package/lib/typescript/package.json +1 -0
  61. package/lib/typescript/src/BlurView.d.ts +16 -0
  62. package/lib/typescript/src/BlurView.d.ts.map +1 -0
  63. package/lib/typescript/src/ParityBlurViewNativeComponent.d.ts +63 -0
  64. package/lib/typescript/src/ParityBlurViewNativeComponent.d.ts.map +1 -0
  65. package/lib/typescript/src/defaults.d.ts +12 -0
  66. package/lib/typescript/src/defaults.d.ts.map +1 -0
  67. package/lib/typescript/src/index.d.ts +5 -0
  68. package/lib/typescript/src/index.d.ts.map +1 -0
  69. package/lib/typescript/src/pipeline/androidCalibration.d.ts +42 -0
  70. package/lib/typescript/src/pipeline/androidCalibration.d.ts.map +1 -0
  71. package/lib/typescript/src/pipeline/captureRect.d.ts +84 -0
  72. package/lib/typescript/src/pipeline/captureRect.d.ts.map +1 -0
  73. package/lib/typescript/src/pipeline/color.d.ts +19 -0
  74. package/lib/typescript/src/pipeline/color.d.ts.map +1 -0
  75. package/lib/typescript/src/pipeline/constants.d.ts +52 -0
  76. package/lib/typescript/src/pipeline/constants.d.ts.map +1 -0
  77. package/lib/typescript/src/pipeline/downsample.d.ts +31 -0
  78. package/lib/typescript/src/pipeline/downsample.d.ts.map +1 -0
  79. package/lib/typescript/src/pipeline/index.d.ts +20 -0
  80. package/lib/typescript/src/pipeline/index.d.ts.map +1 -0
  81. package/lib/typescript/src/pipeline/overlay.d.ts +31 -0
  82. package/lib/typescript/src/pipeline/overlay.d.ts.map +1 -0
  83. package/lib/typescript/src/pipeline/saturation.d.ts +38 -0
  84. package/lib/typescript/src/pipeline/saturation.d.ts.map +1 -0
  85. package/lib/typescript/src/pipeline/types.d.ts +32 -0
  86. package/lib/typescript/src/pipeline/types.d.ts.map +1 -0
  87. package/lib/typescript/src/pipeline/units.d.ts +23 -0
  88. package/lib/typescript/src/pipeline/units.d.ts.map +1 -0
  89. package/lib/typescript/src/presets.d.ts +57 -0
  90. package/lib/typescript/src/presets.d.ts.map +1 -0
  91. package/lib/typescript/src/types.d.ts +116 -0
  92. package/lib/typescript/src/types.d.ts.map +1 -0
  93. package/package.json +167 -0
  94. package/src/BlurView.tsx +195 -0
  95. package/src/ParityBlurViewNativeComponent.ts +82 -0
  96. package/src/defaults.ts +20 -0
  97. package/src/index.ts +10 -0
  98. package/src/pipeline/androidCalibration.ts +64 -0
  99. package/src/pipeline/captureRect.ts +172 -0
  100. package/src/pipeline/color.ts +66 -0
  101. package/src/pipeline/constants.ts +65 -0
  102. package/src/pipeline/downsample.ts +74 -0
  103. package/src/pipeline/index.ts +20 -0
  104. package/src/pipeline/overlay.ts +46 -0
  105. package/src/pipeline/saturation.ts +65 -0
  106. package/src/pipeline/types.ts +37 -0
  107. package/src/pipeline/units.ts +32 -0
  108. package/src/presets.ts +103 -0
  109. package/src/types.ts +132 -0
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export { BlurView, default } from './BlurView';
2
+ export type {
3
+ BlurDownsample,
4
+ BlurMode,
5
+ BlurQuality,
6
+ BlurViewProps,
7
+ BlurViewRef,
8
+ } from './types';
9
+ export { BlurPresets } from './presets';
10
+ export type { BlurPreset, BlurPresetName } from './presets';
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Android RenderEffect calibration (plan §5.2).
3
+ *
4
+ * INTERNAL MODULE. The single place the HWUI sigma<->radius relation lives on
5
+ * the TypeScript side; the Kotlin `AndroidBlurCalibration` must mirror it
6
+ * exactly. Verified on a Pixel 6a (API 36) in M0: radiusForSigma(10) exactly
7
+ * equals 16.454486... (matching the on-device measurement 16.45449).
8
+ *
9
+ * Relation (HWUI): sigma = SLOPE * radiusPlatform + INTERCEPT
10
+ * Inverse: radiusPlatform = (sigma - INTERCEPT) / SLOPE
11
+ *
12
+ * When sigmaSnapshot <= INTERCEPT (0.5) the inverse is non-positive, so we
13
+ * clamp to a no-blur passthrough rather than feeding a bogus radius to
14
+ * RenderEffect.createBlurEffect (plan §5.2 "no-op blur" for very small sigma).
15
+ */
16
+
17
+ import {
18
+ ANDROID_MIN_BLUR_SIGMA,
19
+ HWUI_SIGMA_INTERCEPT,
20
+ HWUI_SIGMA_SLOPE,
21
+ } from './constants';
22
+
23
+ export interface AndroidBlurParam {
24
+ /** True when the requested blur collapses to a no-op passthrough. */
25
+ noBlur: boolean;
26
+ /**
27
+ * Platform radius to pass to RenderEffect.createBlurEffect. 0 when noBlur.
28
+ * Always >= 0.
29
+ */
30
+ radiusPlatform: number;
31
+ }
32
+
33
+ /**
34
+ * Forward relation, exposed for tests/verification: the sigma that HWUI
35
+ * produces for a given platform radius.
36
+ */
37
+ export function sigmaForRadius(radiusPlatform: number): number {
38
+ return HWUI_SIGMA_SLOPE * radiusPlatform + HWUI_SIGMA_INTERCEPT;
39
+ }
40
+
41
+ /**
42
+ * Inverse relation: the platform radius required to achieve a target
43
+ * snapshot-domain sigma. Below the intercept this returns a no-blur result.
44
+ * Monotonically non-decreasing in sigmaSnapshot.
45
+ */
46
+ export function radiusForSigma(sigmaSnapshot: number): AndroidBlurParam {
47
+ if (!Number.isFinite(sigmaSnapshot) || sigmaSnapshot <= ANDROID_MIN_BLUR_SIGMA) {
48
+ return { noBlur: true, radiusPlatform: 0 };
49
+ }
50
+ return {
51
+ noBlur: false,
52
+ radiusPlatform: (sigmaSnapshot - HWUI_SIGMA_INTERCEPT) / HWUI_SIGMA_SLOPE,
53
+ };
54
+ }
55
+
56
+ /**
57
+ * iOS MPSImageGaussianBlur takes the snapshot-domain sigma directly (plan §5.3).
58
+ * Provided here for symmetry so both platform mappings live in the reference.
59
+ * Sigma at or below 0 means no blur.
60
+ */
61
+ export function iosSigmaForSigma(sigmaSnapshot: number): number {
62
+ if (!Number.isFinite(sigmaSnapshot) || sigmaSnapshot <= 0) return 0;
63
+ return sigmaSnapshot;
64
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Capture-rect math (plan §7, §12).
3
+ *
4
+ * INTERNAL MODULE. All rectangles here are in TARGET-LOCAL DEVICE PIXELS unless
5
+ * a field/return is explicitly documented as snapshot pixels.
6
+ *
7
+ * Pipeline of this module:
8
+ * 1. supportMarginPx — Gaussian support margin = ceil(K * sigmaPx)
9
+ * 2. expandCaptureRect — expand visible rect by margin, clamp to target bounds
10
+ * 3. snapshotRectFor — map the clamped device-px rect into integer snapshot
11
+ * pixels (floor origin, ceil far edge = conservative
12
+ * cover)
13
+ * 4. cropRectFor — the sub-region of the snapshot that maps back to the
14
+ * visible rect (fractional, snapshot px)
15
+ * 5. cropRectToViewPx — inverse mapping used to prove round-tripping
16
+ *
17
+ * Rounding rule (LOCKED): in the snapshot domain the origin FLOORS and the far
18
+ * edge CEILS, so `size = ceil(farEdge/D) - floor(origin/D)`. This is the
19
+ * conservative reading of "floor for origin, ceil for size": it guarantees
20
+ * every device pixel of the clamped capture rect is covered by the snapshot
21
+ * rect, at the cost of at most one extra snapshot pixel per side. A naive
22
+ * `floor(origin), ceil(width)` would fail to cover the far edge and is NOT used.
23
+ */
24
+
25
+ import { CAPTURE_SUPPORT_K } from './constants';
26
+ import type { Downsample, Rect } from './types';
27
+
28
+ /** Gaussian support margin in device px (plan §7): ceil(K * sigmaPx). */
29
+ export function supportMarginPx(sigmaPx: number, k: number = CAPTURE_SUPPORT_K): number {
30
+ if (!Number.isFinite(sigmaPx) || sigmaPx <= 0) return 0;
31
+ return Math.ceil(k * sigmaPx);
32
+ }
33
+
34
+ /** Intersection of two rects; empty (zero-size) rect if they do not overlap. */
35
+ export function intersectRect(a: Rect, b: Rect): Rect {
36
+ const x0 = Math.max(a.x, b.x);
37
+ const y0 = Math.max(a.y, b.y);
38
+ const x1 = Math.min(a.x + a.width, b.x + b.width);
39
+ const y1 = Math.min(a.y + a.height, b.y + b.height);
40
+ const width = Math.max(0, x1 - x0);
41
+ const height = Math.max(0, y1 - y0);
42
+ if (width === 0 || height === 0) {
43
+ return { x: x0, y: y0, width: 0, height: 0 };
44
+ }
45
+ return { x: x0, y: y0, width, height };
46
+ }
47
+
48
+ /**
49
+ * Expand the visible blur rect by the Gaussian support margin and clamp to the
50
+ * target bounds. Returns a float device-px rect (plan §7). The margin only
51
+ * covers content that exists inside the target; CLAMP handles the outer edge
52
+ * where the margin was cut off (plan §8).
53
+ */
54
+ export function expandCaptureRect(
55
+ visibleRect: Rect,
56
+ targetBounds: Rect,
57
+ sigmaPx: number,
58
+ k: number = CAPTURE_SUPPORT_K
59
+ ): Rect {
60
+ const margin = supportMarginPx(sigmaPx, k);
61
+ const expanded: Rect = {
62
+ x: visibleRect.x - margin,
63
+ y: visibleRect.y - margin,
64
+ width: visibleRect.width + 2 * margin,
65
+ height: visibleRect.height + 2 * margin,
66
+ };
67
+ return intersectRect(expanded, targetBounds);
68
+ }
69
+
70
+ /** Area of a rect in device px^2 (helper for downsample selection). */
71
+ export function rectArea(r: Rect): number {
72
+ return r.width * r.height;
73
+ }
74
+
75
+ /**
76
+ * Map a clamped device-px capture rect into integer snapshot pixels for
77
+ * downsample factor D. Floor the origin, ceil the far edge (conservative
78
+ * cover). The returned rect is in SNAPSHOT PIXELS (indices), so its device-px
79
+ * footprint is `snapshotRect * D`.
80
+ */
81
+ export function snapshotRectFor(captureRectPx: Rect, downsample: Downsample): Rect {
82
+ const x = Math.floor(captureRectPx.x / downsample);
83
+ const y = Math.floor(captureRectPx.y / downsample);
84
+ const farX = Math.ceil((captureRectPx.x + captureRectPx.width) / downsample);
85
+ const farY = Math.ceil((captureRectPx.y + captureRectPx.height) / downsample);
86
+ return { x, y, width: Math.max(0, farX - x), height: Math.max(0, farY - y) };
87
+ }
88
+
89
+ /**
90
+ * The fractional crop rect, in SNAPSHOT PIXELS, that selects the visible region
91
+ * out of the (larger) snapshot rect after blur. Because the snapshot origin was
92
+ * floored, the crop origin carries a fractional remainder in [0, 1) snapshot px
93
+ * which the upsample step resolves with bilinear sampling (plan §12).
94
+ *
95
+ * cropX = visibleRect.x / D - snapshotRect.x
96
+ * cropW = visibleRect.width / D
97
+ */
98
+ export function cropRectFor(
99
+ visibleRect: Rect,
100
+ snapshotRect: Rect,
101
+ downsample: Downsample
102
+ ): Rect {
103
+ return {
104
+ x: visibleRect.x / downsample - snapshotRect.x,
105
+ y: visibleRect.y / downsample - snapshotRect.y,
106
+ width: visibleRect.width / downsample,
107
+ height: visibleRect.height / downsample,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Inverse of {@link cropRectFor}: map a snapshot-px crop rect back to
113
+ * target-local device px. Used to verify round-tripping (should recover the
114
+ * original visible rect within half a snapshot pixel = D/2 device px).
115
+ */
116
+ export function cropRectToViewPx(
117
+ cropRect: Rect,
118
+ snapshotRect: Rect,
119
+ downsample: Downsample
120
+ ): Rect {
121
+ return {
122
+ x: (snapshotRect.x + cropRect.x) * downsample,
123
+ y: (snapshotRect.y + cropRect.y) * downsample,
124
+ width: cropRect.width * downsample,
125
+ height: cropRect.height * downsample,
126
+ };
127
+ }
128
+
129
+ export interface CapturePlan {
130
+ /** Input visible blur rect (target-local device px). */
131
+ visibleRect: Rect;
132
+ /** Gaussian support margin (device px). */
133
+ marginPx: number;
134
+ /** Expanded + clamped capture rect (target-local device px, float). */
135
+ captureRectPx: Rect;
136
+ /** Capture area used for downsample selection (device px^2). */
137
+ captureAreaPx: number;
138
+ /** Selected downsample factor. */
139
+ downsample: Downsample;
140
+ /** Integer snapshot-pixel rect to allocate/blur. */
141
+ snapshotRect: Rect;
142
+ /** Fractional snapshot-px crop rect selecting the visible region. */
143
+ cropRect: Rect;
144
+ }
145
+
146
+ /**
147
+ * Assemble the full capture plan for a given visible rect, target bounds,
148
+ * sigmaPx and pre-resolved downsample factor. Downsample resolution lives in
149
+ * downsample.ts and takes `captureAreaPx` from this same computation, so the
150
+ * caller sequences: expand -> area -> resolveDownsample -> buildCapturePlan.
151
+ */
152
+ export function buildCapturePlan(
153
+ visibleRect: Rect,
154
+ targetBounds: Rect,
155
+ sigmaPx: number,
156
+ downsample: Downsample,
157
+ k: number = CAPTURE_SUPPORT_K
158
+ ): CapturePlan {
159
+ const marginPx = supportMarginPx(sigmaPx, k);
160
+ const captureRectPx = expandCaptureRect(visibleRect, targetBounds, sigmaPx, k);
161
+ const snapshotRect = snapshotRectFor(captureRectPx, downsample);
162
+ const cropRect = cropRectFor(visibleRect, snapshotRect, downsample);
163
+ return {
164
+ visibleRect,
165
+ marginPx,
166
+ captureRectPx,
167
+ captureAreaPx: rectArea(captureRectPx),
168
+ downsample,
169
+ snapshotRect,
170
+ cropRect,
171
+ };
172
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Color parsing for the overlay prop.
3
+ *
4
+ * INTERNAL MODULE. Parses the subset of CSS color syntaxes React Native
5
+ * accepts for a string color prop into straight-alpha RGBA in [0,1]:
6
+ * - 'transparent'
7
+ * - #rgb / #rgba / #rrggbb / #rrggbbaa
8
+ * - rgb(r,g,b) / rgba(r,g,b,a) (r,g,b integer 0-255, a float 0-1)
9
+ *
10
+ * Any unrecognised value returns a fully-transparent color (no overlay), which
11
+ * is the documented default (plan §11). The native backends parse the same
12
+ * prop with their platform color parsers; this reference fixes the expected
13
+ * numeric result for parity fixtures.
14
+ */
15
+
16
+ import type { RGBA } from './types';
17
+
18
+ export const TRANSPARENT: RGBA = { r: 0, g: 0, b: 0, a: 0 };
19
+
20
+ function clamp01(v: number): number {
21
+ return v < 0 ? 0 : v > 1 ? 1 : v;
22
+ }
23
+
24
+ function hexPair(s: string): number {
25
+ return parseInt(s, 16) / 255;
26
+ }
27
+
28
+ /** Parse a color string into straight-alpha RGBA. Unrecognised -> transparent. */
29
+ export function parseColor(input: string | null | undefined): RGBA {
30
+ if (input == null) return TRANSPARENT;
31
+ const s = input.trim().toLowerCase();
32
+ if (s === 'transparent') return TRANSPARENT;
33
+
34
+ if (s.startsWith('#')) {
35
+ const hex = s.slice(1);
36
+ if (hex.length === 3 || hex.length === 4) {
37
+ const r = hexPair(hex[0]! + hex[0]!);
38
+ const g = hexPair(hex[1]! + hex[1]!);
39
+ const b = hexPair(hex[2]! + hex[2]!);
40
+ const a = hex.length === 4 ? hexPair(hex[3]! + hex[3]!) : 1;
41
+ return { r, g, b, a };
42
+ }
43
+ if (hex.length === 6 || hex.length === 8) {
44
+ const r = hexPair(hex.slice(0, 2));
45
+ const g = hexPair(hex.slice(2, 4));
46
+ const b = hexPair(hex.slice(4, 6));
47
+ const a = hex.length === 8 ? hexPair(hex.slice(6, 8)) : 1;
48
+ return { r, g, b, a };
49
+ }
50
+ return TRANSPARENT;
51
+ }
52
+
53
+ const m = s.match(/^rgba?\(([^)]+)\)$/);
54
+ if (m) {
55
+ const parts = m[1]!.split(',').map((p) => p.trim());
56
+ if (parts.length === 3 || parts.length === 4) {
57
+ const r = clamp01(parseFloat(parts[0]!) / 255);
58
+ const g = clamp01(parseFloat(parts[1]!) / 255);
59
+ const b = clamp01(parseFloat(parts[2]!) / 255);
60
+ const a = parts.length === 4 ? clamp01(parseFloat(parts[3]!)) : 1;
61
+ if ([r, g, b, a].every((v) => Number.isFinite(v))) return { r, g, b, a };
62
+ }
63
+ }
64
+
65
+ return TRANSPARENT;
66
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Locked calibration constants for the canonical pipeline.
3
+ *
4
+ * INTERNAL MODULE. These are the single source of truth for the reference
5
+ * implementation. Native backends must mirror these exact values. Per the
6
+ * plan (§5.2, §38) constants remain provisional until the parity milestone,
7
+ * but M0 device verification already confirmed the HWUI relation on API 36.
8
+ */
9
+
10
+ /**
11
+ * HWUI RenderEffect Gaussian relation, verified exact on a Pixel 6a (API 36)
12
+ * in M0: `sigma = HWUI_SIGMA_SLOPE * radiusPlatform + HWUI_SIGMA_INTERCEPT`.
13
+ * Isolated here so a future version-aware calibration can override it in one
14
+ * place (plan §5.2). Do NOT inline these anywhere else.
15
+ */
16
+ export const HWUI_SIGMA_SLOPE = 0.57735;
17
+ export const HWUI_SIGMA_INTERCEPT = 0.5;
18
+
19
+ /**
20
+ * Below this snapshot-domain sigma the Android RenderEffect relation inverts
21
+ * to a non-positive radius, so we emit a no-blur passthrough instead. Equal to
22
+ * HWUI_SIGMA_INTERCEPT by construction (radius 0 <=> sigma 0.5).
23
+ */
24
+ export const ANDROID_MIN_BLUR_SIGMA = HWUI_SIGMA_INTERCEPT;
25
+
26
+ /** Gaussian support multiplier for capture expansion (plan §7). */
27
+ export const CAPTURE_SUPPORT_K = 3;
28
+
29
+ /**
30
+ * Auto-downsample: target lower bound on snapshot-domain sigma. Keeping
31
+ * sigmaSnapshot at or above this prevents over-downsampling that would make
32
+ * the kernel degenerate (plan §12). 1 snapshot px.
33
+ */
34
+ export const MIN_SIGMA_SNAPSHOT = 1.0;
35
+
36
+ /**
37
+ * Auto-downsample: captures with area (device px^2) below this are considered
38
+ * "small" and are never downsampled beyond 2x — aggressive reduction of a tiny
39
+ * region buys no meaningful performance while degrading quality.
40
+ * 256 * 256 device px.
41
+ */
42
+ export const SMALL_CAPTURE_AREA_PX = 256 * 256;
43
+
44
+ /**
45
+ * Auto-downsample: per-quality ceiling on the downsample factor (plan §13).
46
+ * high -> prefer 1x or 2x
47
+ * balanced -> prefer 2x or 4x
48
+ * performance -> prefer 4x or 8x
49
+ */
50
+ export const QUALITY_MAX_DOWNSAMPLE: Record<
51
+ 'high' | 'balanced' | 'performance',
52
+ 1 | 2 | 4 | 8
53
+ > = {
54
+ high: 2,
55
+ balanced: 4,
56
+ performance: 8,
57
+ };
58
+
59
+ /**
60
+ * Rec. 709 luminance coefficients (plan §10). Sum to 1.0. Used for the
61
+ * saturation matrix on both platforms.
62
+ */
63
+ export const LUMA_R = 0.2126;
64
+ export const LUMA_G = 0.7152;
65
+ export const LUMA_B = 0.0722;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Auto downsample selection (plan §12, §13).
3
+ *
4
+ * INTERNAL MODULE. Deterministic function of (sigmaPx, captureAreaPx, quality)
5
+ * -> D in {1,2,4,8}. Both backends must select identically for identical input.
6
+ *
7
+ * The chosen factor is the largest allowed value that satisfies ALL of three
8
+ * independent ceilings, then snapped down to a real factor in {1,2,4,8}:
9
+ *
10
+ * 1. quality ceiling — QUALITY_MAX_DOWNSAMPLE[quality]
11
+ * 2. sigma floor — keep sigmaSnapshot = sigmaPx / D >= MIN_SIGMA_SNAPSHOT,
12
+ * i.e. D <= floor(sigmaPx / MIN_SIGMA_SNAPSHOT)
13
+ * 3. area ceiling — tiny captures (< SMALL_CAPTURE_AREA_PX) never exceed 2x
14
+ *
15
+ * See docs/PIPELINE_SPEC.md for the locked decision table.
16
+ */
17
+
18
+ import {
19
+ MIN_SIGMA_SNAPSHOT,
20
+ QUALITY_MAX_DOWNSAMPLE,
21
+ SMALL_CAPTURE_AREA_PX,
22
+ } from './constants';
23
+ import type { BlurDownsample, BlurQuality, Downsample } from './types';
24
+ import { DOWNSAMPLE_FACTORS } from './types';
25
+
26
+ /** Largest allowed factor in {8,4,2,1} that is <= cap (never below 1). */
27
+ function largestFactorAtMost(cap: number): Downsample {
28
+ for (const factor of DOWNSAMPLE_FACTORS) {
29
+ if (factor <= cap) return factor;
30
+ }
31
+ return 1;
32
+ }
33
+
34
+ /**
35
+ * Resolve the auto-selected downsample factor.
36
+ *
37
+ * @param sigmaPx device-pixel sigma (blurRadius * displayScale)
38
+ * @param captureAreaPx expanded capture area in device px^2
39
+ * @param quality quality tier
40
+ */
41
+ export function autoDownsample(
42
+ sigmaPx: number,
43
+ captureAreaPx: number,
44
+ quality: BlurQuality
45
+ ): Downsample {
46
+ // No blur requested -> never downsample.
47
+ if (!Number.isFinite(sigmaPx) || sigmaPx <= 0) return 1;
48
+
49
+ const maxByQuality = QUALITY_MAX_DOWNSAMPLE[quality];
50
+
51
+ // Sigma floor: keep snapshot sigma at/above the minimum useful value.
52
+ const maxBySigma = Math.floor(sigmaPx / MIN_SIGMA_SNAPSHOT);
53
+
54
+ // Area ceiling: small captures are capped at 2x.
55
+ const area = Number.isFinite(captureAreaPx) ? captureAreaPx : 0;
56
+ const maxByArea = area < SMALL_CAPTURE_AREA_PX ? 2 : 8;
57
+
58
+ const cap = Math.min(maxByQuality, maxBySigma, maxByArea);
59
+ return largestFactorAtMost(cap);
60
+ }
61
+
62
+ /**
63
+ * Resolve a public `downsample` prop to a concrete factor. An explicit numeric
64
+ * value is honoured verbatim; 'auto' delegates to {@link autoDownsample}.
65
+ */
66
+ export function resolveDownsample(
67
+ prop: BlurDownsample,
68
+ sigmaPx: number,
69
+ captureAreaPx: number,
70
+ quality: BlurQuality
71
+ ): Downsample {
72
+ if (prop === 'auto') return autoDownsample(sigmaPx, captureAreaPx, quality);
73
+ return prop;
74
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Canonical pipeline reference implementation — INTERNAL barrel.
3
+ *
4
+ * This module is the executable form of docs/PIPELINE_SPEC.md: pure TypeScript,
5
+ * no react-native imports, no side effects. Native backends (Kotlin / Swift)
6
+ * must reproduce these exact rules.
7
+ *
8
+ * IMPORTANT: this barrel is intentionally NOT re-exported from src/index.ts.
9
+ * It is a spec/testing artifact, not part of the package's public API.
10
+ */
11
+
12
+ export * from './types';
13
+ export * from './constants';
14
+ export * from './units';
15
+ export * from './androidCalibration';
16
+ export * from './downsample';
17
+ export * from './captureRect';
18
+ export * from './saturation';
19
+ export * from './color';
20
+ export * from './overlay';
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Overlay source-over compositing (plan §11).
3
+ *
4
+ * INTERNAL MODULE. The overlay color (straight alpha, parsed from the prop) is
5
+ * the SOURCE; the saturated blur result is the DESTINATION. Applied AFTER
6
+ * saturation, BEFORE final clipping (plan §6 step 10). This is the only tint in
7
+ * the pipeline — no hidden platform tint is permitted.
8
+ *
9
+ * Straight-alpha (non-premultiplied) source-over, per channel:
10
+ *
11
+ * outA = srcA + dstA * (1 - srcA)
12
+ * outC = (srcC*srcA + dstC*dstA*(1 - srcA)) / outA (outA > 0)
13
+ * 0 (outA = 0)
14
+ *
15
+ * In v1 the capture surface is an opaque backdrop, so dstA = 1 and this reduces
16
+ * to the familiar linear blend outC = srcC*srcA + dstC*(1 - srcA), outA = 1.
17
+ * The general form is implemented so the reference stays correct if a future
18
+ * version captures translucent backdrops.
19
+ */
20
+
21
+ import type { RGBA } from './types';
22
+
23
+ /**
24
+ * Source-over composite of a straight-alpha source over a straight-alpha
25
+ * destination. Returns straight-alpha RGBA.
26
+ */
27
+ export function sourceOver(src: RGBA, dst: RGBA): RGBA {
28
+ const outA = src.a + dst.a * (1 - src.a);
29
+ if (outA <= 0) return { r: 0, g: 0, b: 0, a: 0 };
30
+ const blend = (sc: number, dc: number) =>
31
+ (sc * src.a + dc * dst.a * (1 - src.a)) / outA;
32
+ return {
33
+ r: blend(src.r, dst.r),
34
+ g: blend(src.g, dst.g),
35
+ b: blend(src.b, dst.b),
36
+ a: outA,
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Convenience: composite an overlay color over an opaque (alpha=1) blurred
42
+ * pixel — the v1 case. Equivalent to sourceOver(overlay, {..dst, a:1}).
43
+ */
44
+ export function overlayOnOpaque(overlay: RGBA, dst: RGBA): RGBA {
45
+ return sourceOver(overlay, { r: dst.r, g: dst.g, b: dst.b, a: 1 });
46
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Saturation color matrix (plan §10).
3
+ *
4
+ * INTERNAL MODULE. One canonical 4x5 color matrix (Android ColorMatrix layout:
5
+ * 4 output rows R,G,B,A; 5 columns R,G,B,A,offset) using Rec.709 luminance
6
+ * coefficients. Applied AFTER blur, BEFORE overlay (plan §6 step 9).
7
+ *
8
+ * For saturation s and luma coefficients (lr,lg,lb) with t = 1 - s:
9
+ *
10
+ * R' = (t*lr + s)*R + t*lg*G + t*lb*B
11
+ * G' = t*lr*R + (t*lg + s)*G + t*lb*B
12
+ * B' = t*lr*R + t*lg*G + (t*lb + s)*B
13
+ * A' = A
14
+ *
15
+ * s = 1 -> identity; s = 0 -> luminance grayscale; s > 1 -> more saturated.
16
+ *
17
+ * The alpha row is untouched and there is no offset column (all offsets 0).
18
+ * The matrix operates on the same channel representation the backend blurs in
19
+ * (gamma-space, straight color for the opaque v1 backdrop). It does NOT itself
20
+ * premultiply — see docs/PIPELINE_SPEC.md §Color.
21
+ */
22
+
23
+ import { LUMA_B, LUMA_G, LUMA_R } from './constants';
24
+
25
+ /**
26
+ * Row-major 4x5 saturation matrix (20 numbers), matching Android's
27
+ * ColorMatrix and directly transcribable to a Metal 4x4 + bias.
28
+ */
29
+ export function saturationMatrix(
30
+ s: number,
31
+ lr: number = LUMA_R,
32
+ lg: number = LUMA_G,
33
+ lb: number = LUMA_B
34
+ ): number[] {
35
+ const t = 1 - s;
36
+ return [
37
+ t * lr + s, t * lg, t * lb, 0, 0,
38
+ t * lr, t * lg + s, t * lb, 0, 0,
39
+ t * lr, t * lg, t * lb + s, 0, 0,
40
+ 0, 0, 0, 1, 0,
41
+ ];
42
+ }
43
+
44
+ /**
45
+ * Apply a 4x5 saturation matrix to a straight-alpha RGBA tuple (channels in
46
+ * [0,1]). Returned RGB is clamped to [0,1]; alpha is passed through untouched.
47
+ * Reference for backend parity tests.
48
+ */
49
+ export function applySaturation(
50
+ matrix: number[],
51
+ r: number,
52
+ g: number,
53
+ b: number,
54
+ a: number
55
+ ): { r: number; g: number; b: number; a: number } {
56
+ const clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);
57
+ const m = matrix;
58
+ return {
59
+ // Indices per row-major 4x5 layout.
60
+ r: clamp01(m[0]! * r + m[1]! * g + m[2]! * b + m[3]! * a + m[4]!),
61
+ g: clamp01(m[5]! * r + m[6]! * g + m[7]! * b + m[8]! * a + m[9]!),
62
+ b: clamp01(m[10]! * r + m[11]! * g + m[12]! * b + m[13]! * a + m[14]!),
63
+ a: m[15]! * r + m[16]! * g + m[17]! * b + m[18]! * a + m[19]!,
64
+ };
65
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared value types for the canonical pipeline reference implementation.
3
+ *
4
+ * INTERNAL MODULE. Nothing here is re-exported from src/index.ts. These types
5
+ * describe the language-neutral contract that both native backends implement;
6
+ * they are the executable form of docs/PIPELINE_SPEC.md.
7
+ */
8
+
9
+ import type { BlurDownsample, BlurQuality } from '../types';
10
+
11
+ /** Integer downsample factor actually used by the pipeline (never 'auto'). */
12
+ export type Downsample = 1 | 2 | 4 | 8;
13
+
14
+ /** The four concrete downsample factors, largest first (selection order). */
15
+ export const DOWNSAMPLE_FACTORS: readonly Downsample[] = [8, 4, 2, 1];
16
+
17
+ /**
18
+ * Axis-aligned rectangle. Units are documented per-field by the producing
19
+ * function (device px in target-local space, or snapshot px). Origin is the
20
+ * top-left corner; width/height grow right/down.
21
+ */
22
+ export interface Rect {
23
+ x: number;
24
+ y: number;
25
+ width: number;
26
+ height: number;
27
+ }
28
+
29
+ /** Straight-alpha RGBA colour, all channels normalised to [0, 1]. */
30
+ export interface RGBA {
31
+ r: number;
32
+ g: number;
33
+ b: number;
34
+ a: number;
35
+ }
36
+
37
+ export type { BlurDownsample, BlurQuality };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Canonical unit conversions (plan §5.1).
3
+ *
4
+ * INTERNAL MODULE. Pure functions, no side effects, no react-native imports.
5
+ *
6
+ * blurRadius = Gaussian sigma in dp (public unit)
7
+ * sigmaPx = blurRadius * displayScale (device pixels)
8
+ * sigmaSnapshot= sigmaPx / D (downsampled snapshot px)
9
+ *
10
+ * The blur backend always receives a parameter derived from sigmaSnapshot.
11
+ */
12
+
13
+ import type { Downsample } from './types';
14
+
15
+ /**
16
+ * Convert the public blurRadius (sigma in dp) to sigma in device pixels.
17
+ * Non-finite or negative inputs clamp to 0 (plan §28 safe clamping).
18
+ */
19
+ export function sigmaPxFromDp(blurRadiusDp: number, displayScale: number): number {
20
+ if (!Number.isFinite(blurRadiusDp) || blurRadiusDp <= 0) return 0;
21
+ if (!Number.isFinite(displayScale) || displayScale <= 0) return 0;
22
+ return blurRadiusDp * displayScale;
23
+ }
24
+
25
+ /**
26
+ * Convert device-pixel sigma to snapshot-domain sigma given a downsample
27
+ * factor D. D is a positive integer in {1,2,4,8}.
28
+ */
29
+ export function sigmaSnapshotFromPx(sigmaPx: number, downsample: Downsample): number {
30
+ if (!Number.isFinite(sigmaPx) || sigmaPx <= 0) return 0;
31
+ return sigmaPx / downsample;
32
+ }