@flare-engine/a11y 0.2.1 → 0.3.1
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 +201 -21
- package/NOTICE +2 -0
- package/README.md +3 -3
- package/dist/index.d.mts +90 -99
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +90 -99
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +170 -146
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +158 -108
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -9
package/dist/index.js
CHANGED
|
@@ -1,156 +1,180 @@
|
|
|
1
|
-
"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/index.ts
|
|
21
|
-
var index_exports = {};
|
|
22
|
-
__export(index_exports, {
|
|
23
|
-
A11yManager: () => A11yManager,
|
|
24
|
-
auditColors: () => auditColors,
|
|
25
|
-
contrastRatio: () => contrastRatio,
|
|
26
|
-
createA11yBridge: () => createA11yBridge,
|
|
27
|
-
dampenedMotion: () => dampenedMotion,
|
|
28
|
-
deltaE: () => deltaE,
|
|
29
|
-
isDistinguishable: () => isDistinguishable,
|
|
30
|
-
motionScale: () => motionScale,
|
|
31
|
-
pickContrast: () => pickContrast,
|
|
32
|
-
wcagLevel: () => wcagLevel
|
|
33
|
-
});
|
|
34
|
-
module.exports = __toCommonJS(index_exports);
|
|
35
|
-
|
|
36
|
-
// src/manager.ts
|
|
37
|
-
var import_events = require("@flare-engine/events");
|
|
38
|
-
var DEFAULTS = {
|
|
39
|
-
reducedMotion: false,
|
|
40
|
-
contrast: "normal",
|
|
41
|
-
uiScale: 1
|
|
42
|
-
};
|
|
43
|
-
var A11yManager = class {
|
|
44
|
-
onChange = new import_events.Signal();
|
|
45
|
-
_settings;
|
|
46
|
-
constructor(options = {}) {
|
|
47
|
-
this._settings = { ...DEFAULTS, ...options.initial };
|
|
48
|
-
}
|
|
49
|
-
get settings() {
|
|
50
|
-
return this._settings;
|
|
51
|
-
}
|
|
52
|
-
get reducedMotion() {
|
|
53
|
-
return this._settings.reducedMotion;
|
|
54
|
-
}
|
|
55
|
-
get contrast() {
|
|
56
|
-
return this._settings.contrast;
|
|
57
|
-
}
|
|
58
|
-
get uiScale() {
|
|
59
|
-
return this._settings.uiScale;
|
|
60
|
-
}
|
|
61
|
-
update(patch) {
|
|
62
|
-
const next = { ...this._settings, ...patch };
|
|
63
|
-
if (next.reducedMotion === this._settings.reducedMotion && next.contrast === this._settings.contrast && next.uiScale === this._settings.uiScale) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
this._settings = next;
|
|
67
|
-
this.onChange.emit(next);
|
|
68
|
-
}
|
|
69
|
-
setReducedMotion(v) {
|
|
70
|
-
this.update({ reducedMotion: v });
|
|
71
|
-
}
|
|
72
|
-
setContrast(v) {
|
|
73
|
-
this.update({ contrast: v });
|
|
74
|
-
}
|
|
75
|
-
setUiScale(v) {
|
|
76
|
-
this.update({ uiScale: v });
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// src/helpers.ts
|
|
81
|
-
function motionScale(settings, base = 1) {
|
|
82
|
-
return settings.reducedMotion ? 0 : base;
|
|
83
|
-
}
|
|
84
|
-
function dampenedMotion(settings, base, reducedFactor = 0.2) {
|
|
85
|
-
return settings.reducedMotion ? base * reducedFactor : base;
|
|
86
|
-
}
|
|
87
|
-
function pickContrast(settings, normal, high) {
|
|
88
|
-
return settings.contrast === "high" ? high : normal;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// src/bridge.ts
|
|
92
|
-
function createA11yBridge(options = {}) {
|
|
93
|
-
const manager = options.manager ?? new A11yManager({ initial: options.initial });
|
|
94
|
-
function attach(target) {
|
|
95
|
-
const apply = () => {
|
|
96
|
-
target.setMotionScale(motionScale(manager.settings));
|
|
97
|
-
};
|
|
98
|
-
apply();
|
|
99
|
-
manager.onChange.add(apply);
|
|
100
|
-
return () => manager.onChange.remove(apply);
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
manager,
|
|
104
|
-
attach,
|
|
105
|
-
attachCamera: attach,
|
|
106
|
-
attachPostfx: attach
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// src/audit.ts
|
|
111
|
-
var import_math = require("@flare-engine/math");
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _flare_engine_math = require("@flare-engine/math");
|
|
3
|
+
let _flare_engine_events = require("@flare-engine/events");
|
|
4
|
+
//#region src/audit.ts
|
|
5
|
+
/**
|
|
6
|
+
* WCAG 2.x contrast ratio between two colors. Order-independent; range [1, 21].
|
|
7
|
+
* Delegates to Color.contrastRatio (relative luminance formula).
|
|
8
|
+
*/
|
|
112
9
|
function contrastRatio(fg, bg) {
|
|
113
|
-
|
|
10
|
+
return _flare_engine_math.Color.contrastRatio(fg, bg);
|
|
114
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Classify a contrast ratio against WCAG 2.x thresholds.
|
|
14
|
+
*
|
|
15
|
+
* Normal text (largeText = false):
|
|
16
|
+
* ratio >= 7.0 → AAA, ratio >= 4.5 → AA, else fail
|
|
17
|
+
* Large text (largeText = true, >= 18pt normal / >= 14pt bold):
|
|
18
|
+
* ratio >= 4.5 → AAA, ratio >= 3.0 → AA, else fail
|
|
19
|
+
*/
|
|
115
20
|
function wcagLevel(ratio, largeText = false) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
21
|
+
if (largeText) {
|
|
22
|
+
if (ratio >= 4.5) return "AAA";
|
|
23
|
+
if (ratio >= 3) return "AA";
|
|
24
|
+
return "fail";
|
|
25
|
+
}
|
|
26
|
+
if (ratio >= 7) return "AAA";
|
|
27
|
+
if (ratio >= 4.5) return "AA";
|
|
28
|
+
return "fail";
|
|
124
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Perceptual distinguishability between two colors in OKLab space.
|
|
32
|
+
* Returns the Euclidean ΔE distance (OKLab). Delegates to Color.deltaEOk.
|
|
33
|
+
* Rule of thumb: < 0.05 ≈ imperceptibly similar; > 0.1 ≈ distinguishable.
|
|
34
|
+
*/
|
|
125
35
|
function deltaE(a, b) {
|
|
126
|
-
|
|
36
|
+
return _flare_engine_math.Color.deltaEOk(a, b);
|
|
127
37
|
}
|
|
128
|
-
|
|
129
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Returns true when two colors are perceptually distinguishable.
|
|
40
|
+
* Uses OKLab ΔE with a caller-supplied threshold (default 0.1).
|
|
41
|
+
* The threshold is a design-level constant — tune per visual layer.
|
|
42
|
+
*/
|
|
43
|
+
function isDistinguishable(a, b, threshold = .1) {
|
|
44
|
+
return _flare_engine_math.Color.deltaEOk(a, b) >= threshold;
|
|
130
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Run contrast ratio + distinguishability check in one call.
|
|
48
|
+
* Returns structured results, never throws.
|
|
49
|
+
*/
|
|
131
50
|
function auditColors(fg, bg, options) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
51
|
+
const largeText = options?.largeText ?? false;
|
|
52
|
+
const threshold = options?.deltaEThreshold ?? .1;
|
|
53
|
+
const ratio = _flare_engine_math.Color.contrastRatio(fg, bg);
|
|
54
|
+
const de = _flare_engine_math.Color.deltaEOk(fg, bg);
|
|
55
|
+
return {
|
|
56
|
+
contrastRatio: ratio,
|
|
57
|
+
wcagLevel: wcagLevel(ratio, largeText),
|
|
58
|
+
deltaE: de,
|
|
59
|
+
distinguishable: de >= threshold
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/helpers.ts
|
|
64
|
+
/**
|
|
65
|
+
* Strength multiplier used by animation + postfx systems. Caller chooses a
|
|
66
|
+
* base intensity; this function clamps it for reduced-motion users.
|
|
67
|
+
*/
|
|
68
|
+
function motionScale(settings, base = 1) {
|
|
69
|
+
return settings.reducedMotion ? 0 : base;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns a second, dampened intensity. Useful for effects you want to keep
|
|
73
|
+
* but soften (camera shake, chromatic aberration). `reducedFactor` controls
|
|
74
|
+
* how much survives when reduced-motion is on (default 20%).
|
|
75
|
+
*/
|
|
76
|
+
function dampenedMotion(settings, base, reducedFactor = .2) {
|
|
77
|
+
return settings.reducedMotion ? base * reducedFactor : base;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Helper for UI layers: pick a color based on contrast preference.
|
|
81
|
+
*/
|
|
82
|
+
function pickContrast(settings, normal, high) {
|
|
83
|
+
return settings.contrast === "high" ? high : normal;
|
|
142
84
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/manager.ts
|
|
87
|
+
const DEFAULTS = {
|
|
88
|
+
reducedMotion: false,
|
|
89
|
+
contrast: "normal",
|
|
90
|
+
uiScale: 1
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Central accessibility state. Systems subscribe via `onChange` and read the
|
|
94
|
+
* current snapshot via `settings`. The manager owns no platform wiring — a
|
|
95
|
+
* host app may bridge OS preferences (e.g., `AccessibilityInfo.isReduceMotionEnabled`)
|
|
96
|
+
* by pushing values in on startup.
|
|
97
|
+
*/
|
|
98
|
+
var A11yManager = class {
|
|
99
|
+
onChange = new _flare_engine_events.Signal();
|
|
100
|
+
_settings;
|
|
101
|
+
constructor(options = {}) {
|
|
102
|
+
this._settings = {
|
|
103
|
+
...DEFAULTS,
|
|
104
|
+
...options.initial
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
get settings() {
|
|
108
|
+
return this._settings;
|
|
109
|
+
}
|
|
110
|
+
get reducedMotion() {
|
|
111
|
+
return this._settings.reducedMotion;
|
|
112
|
+
}
|
|
113
|
+
get contrast() {
|
|
114
|
+
return this._settings.contrast;
|
|
115
|
+
}
|
|
116
|
+
get uiScale() {
|
|
117
|
+
return this._settings.uiScale;
|
|
118
|
+
}
|
|
119
|
+
update(patch) {
|
|
120
|
+
const next = {
|
|
121
|
+
...this._settings,
|
|
122
|
+
...patch
|
|
123
|
+
};
|
|
124
|
+
if (next.reducedMotion === this._settings.reducedMotion && next.contrast === this._settings.contrast && next.uiScale === this._settings.uiScale) return;
|
|
125
|
+
this._settings = next;
|
|
126
|
+
this.onChange.emit(next);
|
|
127
|
+
}
|
|
128
|
+
setReducedMotion(v) {
|
|
129
|
+
this.update({ reducedMotion: v });
|
|
130
|
+
}
|
|
131
|
+
setContrast(v) {
|
|
132
|
+
this.update({ contrast: v });
|
|
133
|
+
}
|
|
134
|
+
setUiScale(v) {
|
|
135
|
+
this.update({ uiScale: v });
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/bridge.ts
|
|
140
|
+
/**
|
|
141
|
+
* A11yBridge — one-liner wiring of an `A11yManager` into runtime systems that
|
|
142
|
+
* expose `setMotionScale(k)`. Typical targets: `Camera2D` (screen-shake) and
|
|
143
|
+
* `PostFxController` (global effect intensity).
|
|
144
|
+
*
|
|
145
|
+
* The bridge owns no platform wiring — callers push OS preferences (e.g.
|
|
146
|
+
* `AccessibilityInfo.isReduceMotionEnabled()`) into the manager; the bridge
|
|
147
|
+
* forwards the derived motion scale to every attached target.
|
|
148
|
+
*
|
|
149
|
+
* Duck-typed on the target side so `@flare-engine/a11y` stays dependency-free.
|
|
150
|
+
*/
|
|
151
|
+
function createA11yBridge(options = {}) {
|
|
152
|
+
const manager = options.manager ?? new A11yManager({ initial: options.initial });
|
|
153
|
+
function attach(target) {
|
|
154
|
+
const apply = () => {
|
|
155
|
+
target.setMotionScale(motionScale(manager.settings));
|
|
156
|
+
};
|
|
157
|
+
apply();
|
|
158
|
+
manager.onChange.add(apply);
|
|
159
|
+
return () => manager.onChange.remove(apply);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
manager,
|
|
163
|
+
attach,
|
|
164
|
+
attachCamera: attach,
|
|
165
|
+
attachPostfx: attach
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
//#endregion
|
|
169
|
+
exports.A11yManager = A11yManager;
|
|
170
|
+
exports.auditColors = auditColors;
|
|
171
|
+
exports.contrastRatio = contrastRatio;
|
|
172
|
+
exports.createA11yBridge = createA11yBridge;
|
|
173
|
+
exports.dampenedMotion = dampenedMotion;
|
|
174
|
+
exports.deltaE = deltaE;
|
|
175
|
+
exports.isDistinguishable = isDistinguishable;
|
|
176
|
+
exports.motionScale = motionScale;
|
|
177
|
+
exports.pickContrast = pickContrast;
|
|
178
|
+
exports.wcagLevel = wcagLevel;
|
|
179
|
+
|
|
156
180
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/manager.ts","../src/helpers.ts","../src/bridge.ts","../src/audit.ts"],"sourcesContent":["export {\n A11yManager,\n type A11yManagerOptions,\n type A11ySettings,\n type ContrastLevel,\n} from './manager.js';\nexport { motionScale, dampenedMotion, pickContrast } from './helpers.js';\nexport {\n type A11yBridge,\n type A11yBridgeOptions,\n type MotionScaleTarget,\n createA11yBridge,\n} from './bridge.js';\nexport {\n contrastRatio,\n wcagLevel,\n deltaE,\n isDistinguishable,\n auditColors,\n type WcagLevel,\n type AuditResult,\n type AuditOptions,\n} from './audit.js';\n","import { Signal } from '@flare-engine/events';\n\nexport type ContrastLevel = 'normal' | 'high';\n\nexport interface A11ySettings {\n /** When true, animations and heavy postfx should be minimized. */\n reducedMotion: boolean;\n /** Current contrast preference — UI tokens swap accordingly. */\n contrast: ContrastLevel;\n /** Global UI scale multiplier (`1` = default). */\n uiScale: number;\n}\n\nexport interface A11yManagerOptions {\n initial?: Partial<A11ySettings>;\n}\n\nconst DEFAULTS: A11ySettings = {\n reducedMotion: false,\n contrast: 'normal',\n uiScale: 1,\n};\n\n/**\n * Central accessibility state. Systems subscribe via `onChange` and read the\n * current snapshot via `settings`. The manager owns no platform wiring — a\n * host app may bridge OS preferences (e.g., `AccessibilityInfo.isReduceMotionEnabled`)\n * by pushing values in on startup.\n */\nexport class A11yManager {\n readonly onChange = new Signal<A11ySettings>();\n private _settings: A11ySettings;\n\n constructor(options: A11yManagerOptions = {}) {\n this._settings = { ...DEFAULTS, ...options.initial };\n }\n\n get settings(): A11ySettings {\n return this._settings;\n }\n\n get reducedMotion(): boolean {\n return this._settings.reducedMotion;\n }\n\n get contrast(): ContrastLevel {\n return this._settings.contrast;\n }\n\n get uiScale(): number {\n return this._settings.uiScale;\n }\n\n update(patch: Partial<A11ySettings>): void {\n const next: A11ySettings = { ...this._settings, ...patch };\n if (\n next.reducedMotion === this._settings.reducedMotion &&\n next.contrast === this._settings.contrast &&\n next.uiScale === this._settings.uiScale\n ) {\n return;\n }\n this._settings = next;\n this.onChange.emit(next);\n }\n\n setReducedMotion(v: boolean): void {\n this.update({ reducedMotion: v });\n }\n\n setContrast(v: ContrastLevel): void {\n this.update({ contrast: v });\n }\n\n setUiScale(v: number): void {\n this.update({ uiScale: v });\n }\n}\n","import type { A11ySettings } from './manager.js';\n\n/**\n * Strength multiplier used by animation + postfx systems. Caller chooses a\n * base intensity; this function clamps it for reduced-motion users.\n */\nexport function motionScale(settings: { reducedMotion: boolean }, base = 1): number {\n return settings.reducedMotion ? 0 : base;\n}\n\n/**\n * Returns a second, dampened intensity. Useful for effects you want to keep\n * but soften (camera shake, chromatic aberration). `reducedFactor` controls\n * how much survives when reduced-motion is on (default 20%).\n */\nexport function dampenedMotion(\n settings: { reducedMotion: boolean },\n base: number,\n reducedFactor = 0.2,\n): number {\n return settings.reducedMotion ? base * reducedFactor : base;\n}\n\n/**\n * Helper for UI layers: pick a color based on contrast preference.\n */\nexport function pickContrast<T>(settings: Pick<A11ySettings, 'contrast'>, normal: T, high: T): T {\n return settings.contrast === 'high' ? high : normal;\n}\n","/**\n * A11yBridge — one-liner wiring of an `A11yManager` into runtime systems that\n * expose `setMotionScale(k)`. Typical targets: `Camera2D` (screen-shake) and\n * `PostFxController` (global effect intensity).\n *\n * The bridge owns no platform wiring — callers push OS preferences (e.g.\n * `AccessibilityInfo.isReduceMotionEnabled()`) into the manager; the bridge\n * forwards the derived motion scale to every attached target.\n *\n * Extracted from Pan Tvardowski per Phase 11.14. Duck-typed on the target\n * side so `@flare-engine/a11y` stays dependency-free.\n */\n\nimport { motionScale } from './helpers.js';\nimport { A11yManager, type A11yManagerOptions } from './manager.js';\n\n/** Minimal structural contract. `Camera2D` and `PostFxController` both match. */\nexport interface MotionScaleTarget {\n setMotionScale(k: number): void;\n}\n\nexport interface A11yBridge {\n readonly manager: A11yManager;\n /**\n * Apply the current motion scale to `target` and subscribe for updates.\n * Returns an unsubscribe fn — call on target disposal.\n */\n attach(target: MotionScaleTarget): () => void;\n /** Alias of `attach` with a name that documents intent at call sites. */\n attachCamera(camera: MotionScaleTarget): () => void;\n /** Alias of `attach` with a name that documents intent at call sites. */\n attachPostfx(postfx: MotionScaleTarget): () => void;\n}\n\nexport interface A11yBridgeOptions extends A11yManagerOptions {\n /** Provide an existing manager — otherwise one is constructed from `initial`. */\n readonly manager?: A11yManager;\n}\n\nexport function createA11yBridge(options: A11yBridgeOptions = {}): A11yBridge {\n const manager = options.manager ?? new A11yManager({ initial: options.initial });\n\n function attach(target: MotionScaleTarget): () => void {\n const apply = (): void => {\n target.setMotionScale(motionScale(manager.settings));\n };\n apply();\n manager.onChange.add(apply);\n return () => manager.onChange.remove(apply);\n }\n\n return {\n manager,\n attach,\n attachCamera: attach,\n attachPostfx: attach,\n };\n}\n","import { Color } from '@flare-engine/math';\nimport type { Color as ColorType } from '@flare-engine/math';\n\n/** WCAG 2.x conformance level for a contrast ratio. */\nexport type WcagLevel = 'fail' | 'AA' | 'AAA';\n\n/**\n * WCAG 2.x contrast ratio between two colors. Order-independent; range [1, 21].\n * Delegates to Color.contrastRatio (relative luminance formula).\n */\nexport function contrastRatio(fg: ColorType, bg: ColorType): number {\n return Color.contrastRatio(fg, bg);\n}\n\n/**\n * Classify a contrast ratio against WCAG 2.x thresholds.\n *\n * Normal text (largeText = false):\n * ratio >= 7.0 → AAA, ratio >= 4.5 → AA, else fail\n * Large text (largeText = true, >= 18pt normal / >= 14pt bold):\n * ratio >= 4.5 → AAA, ratio >= 3.0 → AA, else fail\n */\nexport function wcagLevel(ratio: number, largeText = false): WcagLevel {\n if (largeText) {\n if (ratio >= 4.5) return 'AAA';\n if (ratio >= 3.0) return 'AA';\n return 'fail';\n }\n if (ratio >= 7.0) return 'AAA';\n if (ratio >= 4.5) return 'AA';\n return 'fail';\n}\n\n/**\n * Perceptual distinguishability between two colors in OKLab space.\n * Returns the Euclidean ΔE distance (OKLab). Delegates to Color.deltaEOk.\n * Rule of thumb: < 0.05 ≈ imperceptibly similar; > 0.1 ≈ distinguishable.\n */\nexport function deltaE(a: ColorType, b: ColorType): number {\n return Color.deltaEOk(a, b);\n}\n\n/**\n * Returns true when two colors are perceptually distinguishable.\n * Uses OKLab ΔE with a caller-supplied threshold (default 0.1).\n * The threshold is a design-level constant — tune per visual layer.\n */\nexport function isDistinguishable(a: ColorType, b: ColorType, threshold = 0.1): boolean {\n return Color.deltaEOk(a, b) >= threshold;\n}\n\n/** Structured result from auditColors. */\nexport interface AuditResult {\n readonly contrastRatio: number;\n readonly wcagLevel: WcagLevel;\n readonly deltaE: number;\n readonly distinguishable: boolean;\n}\n\nexport interface AuditOptions {\n /** When true, applies large-text WCAG thresholds. Default false. */\n largeText?: boolean;\n /** OKLab ΔE threshold for isDistinguishable. Default 0.1. */\n deltaEThreshold?: number;\n}\n\n/**\n * Run contrast ratio + distinguishability check in one call.\n * Returns structured results, never throws.\n */\nexport function auditColors(fg: ColorType, bg: ColorType, options?: AuditOptions): AuditResult {\n const largeText = options?.largeText ?? false;\n const threshold = options?.deltaEThreshold ?? 0.1;\n const ratio = Color.contrastRatio(fg, bg);\n const de = Color.deltaEOk(fg, bg);\n return {\n contrastRatio: ratio,\n wcagLevel: wcagLevel(ratio, largeText),\n deltaE: de,\n distinguishable: de >= threshold,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"file":"index.js","names":["Color","Signal"],"sources":["../src/audit.ts","../src/helpers.ts","../src/manager.ts","../src/bridge.ts"],"sourcesContent":["import type { Color as ColorType } from '@flare-engine/math';\nimport { Color } from '@flare-engine/math';\n\n/** WCAG 2.x conformance level for a contrast ratio. */\nexport type WcagLevel = 'fail' | 'AA' | 'AAA';\n\n/**\n * WCAG 2.x contrast ratio between two colors. Order-independent; range [1, 21].\n * Delegates to Color.contrastRatio (relative luminance formula).\n */\nexport function contrastRatio(fg: ColorType, bg: ColorType): number {\n return Color.contrastRatio(fg, bg);\n}\n\n/**\n * Classify a contrast ratio against WCAG 2.x thresholds.\n *\n * Normal text (largeText = false):\n * ratio >= 7.0 → AAA, ratio >= 4.5 → AA, else fail\n * Large text (largeText = true, >= 18pt normal / >= 14pt bold):\n * ratio >= 4.5 → AAA, ratio >= 3.0 → AA, else fail\n */\nexport function wcagLevel(ratio: number, largeText = false): WcagLevel {\n if (largeText) {\n if (ratio >= 4.5) return 'AAA';\n if (ratio >= 3.0) return 'AA';\n return 'fail';\n }\n if (ratio >= 7.0) return 'AAA';\n if (ratio >= 4.5) return 'AA';\n return 'fail';\n}\n\n/**\n * Perceptual distinguishability between two colors in OKLab space.\n * Returns the Euclidean ΔE distance (OKLab). Delegates to Color.deltaEOk.\n * Rule of thumb: < 0.05 ≈ imperceptibly similar; > 0.1 ≈ distinguishable.\n */\nexport function deltaE(a: ColorType, b: ColorType): number {\n return Color.deltaEOk(a, b);\n}\n\n/**\n * Returns true when two colors are perceptually distinguishable.\n * Uses OKLab ΔE with a caller-supplied threshold (default 0.1).\n * The threshold is a design-level constant — tune per visual layer.\n */\nexport function isDistinguishable(a: ColorType, b: ColorType, threshold = 0.1): boolean {\n return Color.deltaEOk(a, b) >= threshold;\n}\n\n/** Structured result from auditColors. */\nexport interface AuditResult {\n readonly contrastRatio: number;\n readonly wcagLevel: WcagLevel;\n readonly deltaE: number;\n readonly distinguishable: boolean;\n}\n\nexport interface AuditOptions {\n /** When true, applies large-text WCAG thresholds. Default false. */\n largeText?: boolean;\n /** OKLab ΔE threshold for isDistinguishable. Default 0.1. */\n deltaEThreshold?: number;\n}\n\n/**\n * Run contrast ratio + distinguishability check in one call.\n * Returns structured results, never throws.\n */\nexport function auditColors(fg: ColorType, bg: ColorType, options?: AuditOptions): AuditResult {\n const largeText = options?.largeText ?? false;\n const threshold = options?.deltaEThreshold ?? 0.1;\n const ratio = Color.contrastRatio(fg, bg);\n const de = Color.deltaEOk(fg, bg);\n return {\n contrastRatio: ratio,\n wcagLevel: wcagLevel(ratio, largeText),\n deltaE: de,\n distinguishable: de >= threshold,\n };\n}\n","import type { A11ySettings } from './manager.js';\n\n/**\n * Strength multiplier used by animation + postfx systems. Caller chooses a\n * base intensity; this function clamps it for reduced-motion users.\n */\nexport function motionScale(settings: { reducedMotion: boolean }, base = 1): number {\n return settings.reducedMotion ? 0 : base;\n}\n\n/**\n * Returns a second, dampened intensity. Useful for effects you want to keep\n * but soften (camera shake, chromatic aberration). `reducedFactor` controls\n * how much survives when reduced-motion is on (default 20%).\n */\nexport function dampenedMotion(\n settings: { reducedMotion: boolean },\n base: number,\n reducedFactor = 0.2,\n): number {\n return settings.reducedMotion ? base * reducedFactor : base;\n}\n\n/**\n * Helper for UI layers: pick a color based on contrast preference.\n */\nexport function pickContrast<T>(settings: Pick<A11ySettings, 'contrast'>, normal: T, high: T): T {\n return settings.contrast === 'high' ? high : normal;\n}\n","import { Signal } from '@flare-engine/events';\n\nexport type ContrastLevel = 'normal' | 'high';\n\nexport interface A11ySettings {\n /** When true, animations and heavy postfx should be minimized. */\n reducedMotion: boolean;\n /** Current contrast preference — UI tokens swap accordingly. */\n contrast: ContrastLevel;\n /** Global UI scale multiplier (`1` = default). */\n uiScale: number;\n}\n\nexport interface A11yManagerOptions {\n initial?: Partial<A11ySettings>;\n}\n\nconst DEFAULTS: A11ySettings = {\n reducedMotion: false,\n contrast: 'normal',\n uiScale: 1,\n};\n\n/**\n * Central accessibility state. Systems subscribe via `onChange` and read the\n * current snapshot via `settings`. The manager owns no platform wiring — a\n * host app may bridge OS preferences (e.g., `AccessibilityInfo.isReduceMotionEnabled`)\n * by pushing values in on startup.\n */\nexport class A11yManager {\n readonly onChange = new Signal<A11ySettings>();\n private _settings: A11ySettings;\n\n constructor(options: A11yManagerOptions = {}) {\n this._settings = { ...DEFAULTS, ...options.initial };\n }\n\n get settings(): A11ySettings {\n return this._settings;\n }\n\n get reducedMotion(): boolean {\n return this._settings.reducedMotion;\n }\n\n get contrast(): ContrastLevel {\n return this._settings.contrast;\n }\n\n get uiScale(): number {\n return this._settings.uiScale;\n }\n\n update(patch: Partial<A11ySettings>): void {\n const next: A11ySettings = { ...this._settings, ...patch };\n if (\n next.reducedMotion === this._settings.reducedMotion &&\n next.contrast === this._settings.contrast &&\n next.uiScale === this._settings.uiScale\n ) {\n return;\n }\n this._settings = next;\n this.onChange.emit(next);\n }\n\n setReducedMotion(v: boolean): void {\n this.update({ reducedMotion: v });\n }\n\n setContrast(v: ContrastLevel): void {\n this.update({ contrast: v });\n }\n\n setUiScale(v: number): void {\n this.update({ uiScale: v });\n }\n}\n","/**\n * A11yBridge — one-liner wiring of an `A11yManager` into runtime systems that\n * expose `setMotionScale(k)`. Typical targets: `Camera2D` (screen-shake) and\n * `PostFxController` (global effect intensity).\n *\n * The bridge owns no platform wiring — callers push OS preferences (e.g.\n * `AccessibilityInfo.isReduceMotionEnabled()`) into the manager; the bridge\n * forwards the derived motion scale to every attached target.\n *\n * Duck-typed on the target side so `@flare-engine/a11y` stays dependency-free.\n */\n\nimport { motionScale } from './helpers.js';\nimport { A11yManager, type A11yManagerOptions } from './manager.js';\n\n/** Minimal structural contract. `Camera2D` and `PostFxController` both match. */\nexport interface MotionScaleTarget {\n setMotionScale(k: number): void;\n}\n\nexport interface A11yBridge {\n readonly manager: A11yManager;\n /**\n * Apply the current motion scale to `target` and subscribe for updates.\n * Returns an unsubscribe fn — call on target disposal.\n */\n attach(target: MotionScaleTarget): () => void;\n /** Alias of `attach` with a name that documents intent at call sites. */\n attachCamera(camera: MotionScaleTarget): () => void;\n /** Alias of `attach` with a name that documents intent at call sites. */\n attachPostfx(postfx: MotionScaleTarget): () => void;\n}\n\nexport interface A11yBridgeOptions extends A11yManagerOptions {\n /** Provide an existing manager — otherwise one is constructed from `initial`. */\n readonly manager?: A11yManager;\n}\n\nexport function createA11yBridge(options: A11yBridgeOptions = {}): A11yBridge {\n const manager = options.manager ?? new A11yManager({ initial: options.initial });\n\n function attach(target: MotionScaleTarget): () => void {\n const apply = (): void => {\n target.setMotionScale(motionScale(manager.settings));\n };\n apply();\n manager.onChange.add(apply);\n return () => manager.onChange.remove(apply);\n }\n\n return {\n manager,\n attach,\n attachCamera: attach,\n attachPostfx: attach,\n };\n}\n"],"mappings":";;;;;;;;AAUA,SAAgB,cAAc,IAAe,IAAuB;CAClE,OAAOA,mBAAAA,MAAM,cAAc,IAAI,EAAE;AACnC;;;;;;;;;AAUA,SAAgB,UAAU,OAAe,YAAY,OAAkB;CACrE,IAAI,WAAW;EACb,IAAI,SAAS,KAAK,OAAO;EACzB,IAAI,SAAS,GAAK,OAAO;EACzB,OAAO;CACT;CACA,IAAI,SAAS,GAAK,OAAO;CACzB,IAAI,SAAS,KAAK,OAAO;CACzB,OAAO;AACT;;;;;;AAOA,SAAgB,OAAO,GAAc,GAAsB;CACzD,OAAOA,mBAAAA,MAAM,SAAS,GAAG,CAAC;AAC5B;;;;;;AAOA,SAAgB,kBAAkB,GAAc,GAAc,YAAY,IAAc;CACtF,OAAOA,mBAAAA,MAAM,SAAS,GAAG,CAAC,KAAK;AACjC;;;;;AAqBA,SAAgB,YAAY,IAAe,IAAe,SAAqC;CAC7F,MAAM,YAAY,SAAS,aAAa;CACxC,MAAM,YAAY,SAAS,mBAAmB;CAC9C,MAAM,QAAQA,mBAAAA,MAAM,cAAc,IAAI,EAAE;CACxC,MAAM,KAAKA,mBAAAA,MAAM,SAAS,IAAI,EAAE;CAChC,OAAO;EACL,eAAe;EACf,WAAW,UAAU,OAAO,SAAS;EACrC,QAAQ;EACR,iBAAiB,MAAM;CACzB;AACF;;;;;;;AC3EA,SAAgB,YAAY,UAAsC,OAAO,GAAW;CAClF,OAAO,SAAS,gBAAgB,IAAI;AACtC;;;;;;AAOA,SAAgB,eACd,UACA,MACA,gBAAgB,IACR;CACR,OAAO,SAAS,gBAAgB,OAAO,gBAAgB;AACzD;;;;AAKA,SAAgB,aAAgB,UAA0C,QAAW,MAAY;CAC/F,OAAO,SAAS,aAAa,SAAS,OAAO;AAC/C;;;ACXA,MAAM,WAAyB;CAC7B,eAAe;CACf,UAAU;CACV,SAAS;AACX;;;;;;;AAQA,IAAa,cAAb,MAAyB;CACvB,WAAoB,IAAIC,qBAAAA,OAAqB;CAC7C;CAEA,YAAY,UAA8B,CAAC,GAAG;EAC5C,KAAK,YAAY;GAAE,GAAG;GAAU,GAAG,QAAQ;EAAQ;CACrD;CAEA,IAAI,WAAyB;EAC3B,OAAO,KAAK;CACd;CAEA,IAAI,gBAAyB;EAC3B,OAAO,KAAK,UAAU;CACxB;CAEA,IAAI,WAA0B;EAC5B,OAAO,KAAK,UAAU;CACxB;CAEA,IAAI,UAAkB;EACpB,OAAO,KAAK,UAAU;CACxB;CAEA,OAAO,OAAoC;EACzC,MAAM,OAAqB;GAAE,GAAG,KAAK;GAAW,GAAG;EAAM;EACzD,IACE,KAAK,kBAAkB,KAAK,UAAU,iBACtC,KAAK,aAAa,KAAK,UAAU,YACjC,KAAK,YAAY,KAAK,UAAU,SAEhC;EAEF,KAAK,YAAY;EACjB,KAAK,SAAS,KAAK,IAAI;CACzB;CAEA,iBAAiB,GAAkB;EACjC,KAAK,OAAO,EAAE,eAAe,EAAE,CAAC;CAClC;CAEA,YAAY,GAAwB;EAClC,KAAK,OAAO,EAAE,UAAU,EAAE,CAAC;CAC7B;CAEA,WAAW,GAAiB;EAC1B,KAAK,OAAO,EAAE,SAAS,EAAE,CAAC;CAC5B;AACF;;;;;;;;;;;;;;ACvCA,SAAgB,iBAAiB,UAA6B,CAAC,GAAe;CAC5E,MAAM,UAAU,QAAQ,WAAW,IAAI,YAAY,EAAE,SAAS,QAAQ,QAAQ,CAAC;CAE/E,SAAS,OAAO,QAAuC;EACrD,MAAM,cAAoB;GACxB,OAAO,eAAe,YAAY,QAAQ,QAAQ,CAAC;EACrD;EACA,MAAM;EACN,QAAQ,SAAS,IAAI,KAAK;EAC1B,aAAa,QAAQ,SAAS,OAAO,KAAK;CAC5C;CAEA,OAAO;EACL;EACA;EACA,cAAc;EACd,cAAc;CAChB;AACF"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,120 +1,170 @@
|
|
|
1
|
-
// src/manager.ts
|
|
2
|
-
import { Signal } from "@flare-engine/events";
|
|
3
|
-
var DEFAULTS = {
|
|
4
|
-
reducedMotion: false,
|
|
5
|
-
contrast: "normal",
|
|
6
|
-
uiScale: 1
|
|
7
|
-
};
|
|
8
|
-
var A11yManager = class {
|
|
9
|
-
onChange = new Signal();
|
|
10
|
-
_settings;
|
|
11
|
-
constructor(options = {}) {
|
|
12
|
-
this._settings = { ...DEFAULTS, ...options.initial };
|
|
13
|
-
}
|
|
14
|
-
get settings() {
|
|
15
|
-
return this._settings;
|
|
16
|
-
}
|
|
17
|
-
get reducedMotion() {
|
|
18
|
-
return this._settings.reducedMotion;
|
|
19
|
-
}
|
|
20
|
-
get contrast() {
|
|
21
|
-
return this._settings.contrast;
|
|
22
|
-
}
|
|
23
|
-
get uiScale() {
|
|
24
|
-
return this._settings.uiScale;
|
|
25
|
-
}
|
|
26
|
-
update(patch) {
|
|
27
|
-
const next = { ...this._settings, ...patch };
|
|
28
|
-
if (next.reducedMotion === this._settings.reducedMotion && next.contrast === this._settings.contrast && next.uiScale === this._settings.uiScale) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
this._settings = next;
|
|
32
|
-
this.onChange.emit(next);
|
|
33
|
-
}
|
|
34
|
-
setReducedMotion(v) {
|
|
35
|
-
this.update({ reducedMotion: v });
|
|
36
|
-
}
|
|
37
|
-
setContrast(v) {
|
|
38
|
-
this.update({ contrast: v });
|
|
39
|
-
}
|
|
40
|
-
setUiScale(v) {
|
|
41
|
-
this.update({ uiScale: v });
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// src/helpers.ts
|
|
46
|
-
function motionScale(settings, base = 1) {
|
|
47
|
-
return settings.reducedMotion ? 0 : base;
|
|
48
|
-
}
|
|
49
|
-
function dampenedMotion(settings, base, reducedFactor = 0.2) {
|
|
50
|
-
return settings.reducedMotion ? base * reducedFactor : base;
|
|
51
|
-
}
|
|
52
|
-
function pickContrast(settings, normal, high) {
|
|
53
|
-
return settings.contrast === "high" ? high : normal;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// src/bridge.ts
|
|
57
|
-
function createA11yBridge(options = {}) {
|
|
58
|
-
const manager = options.manager ?? new A11yManager({ initial: options.initial });
|
|
59
|
-
function attach(target) {
|
|
60
|
-
const apply = () => {
|
|
61
|
-
target.setMotionScale(motionScale(manager.settings));
|
|
62
|
-
};
|
|
63
|
-
apply();
|
|
64
|
-
manager.onChange.add(apply);
|
|
65
|
-
return () => manager.onChange.remove(apply);
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
manager,
|
|
69
|
-
attach,
|
|
70
|
-
attachCamera: attach,
|
|
71
|
-
attachPostfx: attach
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// src/audit.ts
|
|
76
1
|
import { Color } from "@flare-engine/math";
|
|
2
|
+
import { Signal } from "@flare-engine/events";
|
|
3
|
+
//#region src/audit.ts
|
|
4
|
+
/**
|
|
5
|
+
* WCAG 2.x contrast ratio between two colors. Order-independent; range [1, 21].
|
|
6
|
+
* Delegates to Color.contrastRatio (relative luminance formula).
|
|
7
|
+
*/
|
|
77
8
|
function contrastRatio(fg, bg) {
|
|
78
|
-
|
|
9
|
+
return Color.contrastRatio(fg, bg);
|
|
79
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Classify a contrast ratio against WCAG 2.x thresholds.
|
|
13
|
+
*
|
|
14
|
+
* Normal text (largeText = false):
|
|
15
|
+
* ratio >= 7.0 → AAA, ratio >= 4.5 → AA, else fail
|
|
16
|
+
* Large text (largeText = true, >= 18pt normal / >= 14pt bold):
|
|
17
|
+
* ratio >= 4.5 → AAA, ratio >= 3.0 → AA, else fail
|
|
18
|
+
*/
|
|
80
19
|
function wcagLevel(ratio, largeText = false) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
20
|
+
if (largeText) {
|
|
21
|
+
if (ratio >= 4.5) return "AAA";
|
|
22
|
+
if (ratio >= 3) return "AA";
|
|
23
|
+
return "fail";
|
|
24
|
+
}
|
|
25
|
+
if (ratio >= 7) return "AAA";
|
|
26
|
+
if (ratio >= 4.5) return "AA";
|
|
27
|
+
return "fail";
|
|
89
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Perceptual distinguishability between two colors in OKLab space.
|
|
31
|
+
* Returns the Euclidean ΔE distance (OKLab). Delegates to Color.deltaEOk.
|
|
32
|
+
* Rule of thumb: < 0.05 ≈ imperceptibly similar; > 0.1 ≈ distinguishable.
|
|
33
|
+
*/
|
|
90
34
|
function deltaE(a, b) {
|
|
91
|
-
|
|
35
|
+
return Color.deltaEOk(a, b);
|
|
92
36
|
}
|
|
93
|
-
|
|
94
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Returns true when two colors are perceptually distinguishable.
|
|
39
|
+
* Uses OKLab ΔE with a caller-supplied threshold (default 0.1).
|
|
40
|
+
* The threshold is a design-level constant — tune per visual layer.
|
|
41
|
+
*/
|
|
42
|
+
function isDistinguishable(a, b, threshold = .1) {
|
|
43
|
+
return Color.deltaEOk(a, b) >= threshold;
|
|
95
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Run contrast ratio + distinguishability check in one call.
|
|
47
|
+
* Returns structured results, never throws.
|
|
48
|
+
*/
|
|
96
49
|
function auditColors(fg, bg, options) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
50
|
+
const largeText = options?.largeText ?? false;
|
|
51
|
+
const threshold = options?.deltaEThreshold ?? .1;
|
|
52
|
+
const ratio = Color.contrastRatio(fg, bg);
|
|
53
|
+
const de = Color.deltaEOk(fg, bg);
|
|
54
|
+
return {
|
|
55
|
+
contrastRatio: ratio,
|
|
56
|
+
wcagLevel: wcagLevel(ratio, largeText),
|
|
57
|
+
deltaE: de,
|
|
58
|
+
distinguishable: de >= threshold
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/helpers.ts
|
|
63
|
+
/**
|
|
64
|
+
* Strength multiplier used by animation + postfx systems. Caller chooses a
|
|
65
|
+
* base intensity; this function clamps it for reduced-motion users.
|
|
66
|
+
*/
|
|
67
|
+
function motionScale(settings, base = 1) {
|
|
68
|
+
return settings.reducedMotion ? 0 : base;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Returns a second, dampened intensity. Useful for effects you want to keep
|
|
72
|
+
* but soften (camera shake, chromatic aberration). `reducedFactor` controls
|
|
73
|
+
* how much survives when reduced-motion is on (default 20%).
|
|
74
|
+
*/
|
|
75
|
+
function dampenedMotion(settings, base, reducedFactor = .2) {
|
|
76
|
+
return settings.reducedMotion ? base * reducedFactor : base;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Helper for UI layers: pick a color based on contrast preference.
|
|
80
|
+
*/
|
|
81
|
+
function pickContrast(settings, normal, high) {
|
|
82
|
+
return settings.contrast === "high" ? high : normal;
|
|
107
83
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/manager.ts
|
|
86
|
+
const DEFAULTS = {
|
|
87
|
+
reducedMotion: false,
|
|
88
|
+
contrast: "normal",
|
|
89
|
+
uiScale: 1
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Central accessibility state. Systems subscribe via `onChange` and read the
|
|
93
|
+
* current snapshot via `settings`. The manager owns no platform wiring — a
|
|
94
|
+
* host app may bridge OS preferences (e.g., `AccessibilityInfo.isReduceMotionEnabled`)
|
|
95
|
+
* by pushing values in on startup.
|
|
96
|
+
*/
|
|
97
|
+
var A11yManager = class {
|
|
98
|
+
onChange = new Signal();
|
|
99
|
+
_settings;
|
|
100
|
+
constructor(options = {}) {
|
|
101
|
+
this._settings = {
|
|
102
|
+
...DEFAULTS,
|
|
103
|
+
...options.initial
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
get settings() {
|
|
107
|
+
return this._settings;
|
|
108
|
+
}
|
|
109
|
+
get reducedMotion() {
|
|
110
|
+
return this._settings.reducedMotion;
|
|
111
|
+
}
|
|
112
|
+
get contrast() {
|
|
113
|
+
return this._settings.contrast;
|
|
114
|
+
}
|
|
115
|
+
get uiScale() {
|
|
116
|
+
return this._settings.uiScale;
|
|
117
|
+
}
|
|
118
|
+
update(patch) {
|
|
119
|
+
const next = {
|
|
120
|
+
...this._settings,
|
|
121
|
+
...patch
|
|
122
|
+
};
|
|
123
|
+
if (next.reducedMotion === this._settings.reducedMotion && next.contrast === this._settings.contrast && next.uiScale === this._settings.uiScale) return;
|
|
124
|
+
this._settings = next;
|
|
125
|
+
this.onChange.emit(next);
|
|
126
|
+
}
|
|
127
|
+
setReducedMotion(v) {
|
|
128
|
+
this.update({ reducedMotion: v });
|
|
129
|
+
}
|
|
130
|
+
setContrast(v) {
|
|
131
|
+
this.update({ contrast: v });
|
|
132
|
+
}
|
|
133
|
+
setUiScale(v) {
|
|
134
|
+
this.update({ uiScale: v });
|
|
135
|
+
}
|
|
119
136
|
};
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/bridge.ts
|
|
139
|
+
/**
|
|
140
|
+
* A11yBridge — one-liner wiring of an `A11yManager` into runtime systems that
|
|
141
|
+
* expose `setMotionScale(k)`. Typical targets: `Camera2D` (screen-shake) and
|
|
142
|
+
* `PostFxController` (global effect intensity).
|
|
143
|
+
*
|
|
144
|
+
* The bridge owns no platform wiring — callers push OS preferences (e.g.
|
|
145
|
+
* `AccessibilityInfo.isReduceMotionEnabled()`) into the manager; the bridge
|
|
146
|
+
* forwards the derived motion scale to every attached target.
|
|
147
|
+
*
|
|
148
|
+
* Duck-typed on the target side so `@flare-engine/a11y` stays dependency-free.
|
|
149
|
+
*/
|
|
150
|
+
function createA11yBridge(options = {}) {
|
|
151
|
+
const manager = options.manager ?? new A11yManager({ initial: options.initial });
|
|
152
|
+
function attach(target) {
|
|
153
|
+
const apply = () => {
|
|
154
|
+
target.setMotionScale(motionScale(manager.settings));
|
|
155
|
+
};
|
|
156
|
+
apply();
|
|
157
|
+
manager.onChange.add(apply);
|
|
158
|
+
return () => manager.onChange.remove(apply);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
manager,
|
|
162
|
+
attach,
|
|
163
|
+
attachCamera: attach,
|
|
164
|
+
attachPostfx: attach
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
export { A11yManager, auditColors, contrastRatio, createA11yBridge, dampenedMotion, deltaE, isDistinguishable, motionScale, pickContrast, wcagLevel };
|
|
169
|
+
|
|
120
170
|
//# sourceMappingURL=index.mjs.map
|