@udixio/theme 1.0.0-beta.10

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 (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/app.container.d.ts +5 -0
  4. package/dist/app.module.d.ts +2 -0
  5. package/dist/app.service.d.ts +13 -0
  6. package/dist/color/color.interface.d.ts +8 -0
  7. package/dist/color/color.module.d.ts +2 -0
  8. package/dist/color/entities/color.entity.d.ts +42 -0
  9. package/dist/color/entities/index.d.ts +1 -0
  10. package/dist/color/index.d.ts +5 -0
  11. package/dist/color/models/default-color.model.d.ts +3 -0
  12. package/dist/color/models/index.d.ts +1 -0
  13. package/dist/color/services/color-manager.service.d.ts +18 -0
  14. package/dist/color/services/color.service.d.ts +21 -0
  15. package/dist/color/services/index.d.ts +2 -0
  16. package/dist/config/config.interface.d.ts +13 -0
  17. package/dist/config/config.module.d.ts +2 -0
  18. package/dist/config/config.service.d.ts +12 -0
  19. package/dist/config/index.d.ts +3 -0
  20. package/dist/index.d.ts +11 -0
  21. package/dist/index.js +8 -0
  22. package/dist/main.d.ts +3 -0
  23. package/dist/material-color-utilities/contrastCurve.d.ts +46 -0
  24. package/dist/material-color-utilities/dynamic_color.d.ts +171 -0
  25. package/dist/material-color-utilities/index.d.ts +3 -0
  26. package/dist/material-color-utilities/toneDeltaPair.d.ts +60 -0
  27. package/dist/plugin/index.d.ts +3 -0
  28. package/dist/plugin/plugin.abstract.d.ts +6 -0
  29. package/dist/plugin/plugin.module.d.ts +2 -0
  30. package/dist/plugin/plugin.service.d.ts +10 -0
  31. package/dist/plugins/tailwind/Tailwind.plugin.d.ts +14 -0
  32. package/dist/plugins/tailwind/index.d.ts +3 -0
  33. package/dist/plugins/tailwind/main.d.ts +10 -0
  34. package/dist/plugins/tailwind/plugins-tailwind/index.d.ts +2 -0
  35. package/dist/plugins/tailwind/plugins-tailwind/state.d.ts +4 -0
  36. package/dist/plugins/tailwind/plugins-tailwind/themer.d.ts +4 -0
  37. package/dist/theme/entities/index.d.ts +2 -0
  38. package/dist/theme/entities/scheme.entity.d.ts +15 -0
  39. package/dist/theme/entities/variant.entity.d.ts +7 -0
  40. package/dist/theme/index.d.ts +4 -0
  41. package/dist/theme/models/index.d.ts +1 -0
  42. package/dist/theme/models/variant.model.d.ts +8 -0
  43. package/dist/theme/services/index.d.ts +3 -0
  44. package/dist/theme/services/scheme.service.d.ts +17 -0
  45. package/dist/theme/services/theme.service.d.ts +22 -0
  46. package/dist/theme/services/variant.service.d.ts +13 -0
  47. package/dist/theme/theme.module.d.ts +2 -0
  48. package/dist/theme.cjs.development.js +2193 -0
  49. package/dist/theme.cjs.development.js.map +1 -0
  50. package/dist/theme.cjs.production.min.js +2 -0
  51. package/dist/theme.cjs.production.min.js.map +1 -0
  52. package/dist/theme.esm.js +2157 -0
  53. package/dist/theme.esm.js.map +1 -0
  54. package/package.json +95 -0
  55. package/src/app.container.ts +46 -0
  56. package/src/app.module.ts +7 -0
  57. package/src/app.service.spec.ts +15 -0
  58. package/src/app.service.ts +23 -0
  59. package/src/color/color.interface.ts +13 -0
  60. package/src/color/color.module.ts +9 -0
  61. package/src/color/entities/color.entity.ts +71 -0
  62. package/src/color/entities/index.ts +1 -0
  63. package/src/color/index.ts +5 -0
  64. package/src/color/models/default-color.model.ts +300 -0
  65. package/src/color/models/index.ts +1 -0
  66. package/src/color/services/color-manager.service.ts +191 -0
  67. package/src/color/services/color.service.spec.ts +28 -0
  68. package/src/color/services/color.service.ts +75 -0
  69. package/src/color/services/index.ts +2 -0
  70. package/src/config/config.interface.ts +14 -0
  71. package/src/config/config.module.ts +7 -0
  72. package/src/config/config.service.ts +74 -0
  73. package/src/config/index.ts +3 -0
  74. package/src/index.ts +11 -0
  75. package/src/main.ts +14 -0
  76. package/src/material-color-utilities/contrastCurve.ts +63 -0
  77. package/src/material-color-utilities/dynamic_color.ts +450 -0
  78. package/src/material-color-utilities/index.ts +3 -0
  79. package/src/material-color-utilities/toneDeltaPair.ts +64 -0
  80. package/src/plugin/index.ts +3 -0
  81. package/src/plugin/plugin.abstract.ts +8 -0
  82. package/src/plugin/plugin.module.ts +7 -0
  83. package/src/plugin/plugin.service.ts +30 -0
  84. package/src/plugins/tailwind/Tailwind.plugin.ts +53 -0
  85. package/src/plugins/tailwind/index.ts +3 -0
  86. package/src/plugins/tailwind/main.ts +18 -0
  87. package/src/plugins/tailwind/plugins-tailwind/index.ts +2 -0
  88. package/src/plugins/tailwind/plugins-tailwind/state.ts +88 -0
  89. package/src/plugins/tailwind/plugins-tailwind/themer.ts +53 -0
  90. package/src/theme/entities/index.ts +2 -0
  91. package/src/theme/entities/scheme.entity.ts +44 -0
  92. package/src/theme/entities/variant.entity.ts +39 -0
  93. package/src/theme/index.ts +4 -0
  94. package/src/theme/models/index.ts +1 -0
  95. package/src/theme/models/variant.model.ts +63 -0
  96. package/src/theme/services/index.ts +3 -0
  97. package/src/theme/services/scheme.service.ts +80 -0
  98. package/src/theme/services/theme.service.ts +74 -0
  99. package/src/theme/services/variant.service.ts +52 -0
  100. package/src/theme/theme.module.ts +9 -0
@@ -0,0 +1,74 @@
1
+ import { ConfigInterface } from './config.interface';
2
+
3
+ import { resolve } from 'path';
4
+ import { defaultColors } from '../color';
5
+ import { VariantModel } from '../theme';
6
+ import { AppService } from '../app.service';
7
+
8
+ export function defineConfig(configObject: ConfigInterface): ConfigInterface {
9
+ if (!configObject || typeof configObject !== 'object') {
10
+ throw new Error('The configuration is missing or not an object');
11
+ }
12
+ if (!('sourceColor' in configObject)) {
13
+ throw new Error('Invalid configuration');
14
+ }
15
+ return configObject as ConfigInterface;
16
+ }
17
+
18
+ export class ConfigService {
19
+ configPath = './theme.config.ts';
20
+
21
+ private appService: AppService;
22
+
23
+ constructor({ appService }: { appService: AppService }) {
24
+ this.appService = appService;
25
+ }
26
+
27
+ public async loadConfig(): Promise<void> {
28
+ const { themeService, colorService, pluginService } = this.appService;
29
+ const {
30
+ sourceColor,
31
+ contrastLevel = 0,
32
+ isDark = false,
33
+ variant = VariantModel.tonalSpot,
34
+ palettes,
35
+ colors,
36
+ useDefaultColors = true,
37
+ plugins,
38
+ } = await this.getConfig();
39
+ themeService.create({
40
+ contrastLevel: contrastLevel,
41
+ isDark: isDark,
42
+ sourceColorHex: sourceColor,
43
+ variant: variant,
44
+ });
45
+ if (palettes) {
46
+ Object.entries(palettes).forEach(([key, value]) =>
47
+ themeService.addCustomPalette(key, value)
48
+ );
49
+ }
50
+ if (useDefaultColors) {
51
+ colorService.addColors(defaultColors);
52
+ }
53
+ if (colors) {
54
+ colorService.addColors(colors);
55
+ }
56
+ if (plugins) {
57
+ plugins.forEach((plugin) => {
58
+ if (Array.isArray(plugin)) {
59
+ pluginService.addPlugin(plugin[0], plugin[1]);
60
+ } else {
61
+ pluginService.addPlugin(plugin, {});
62
+ }
63
+ });
64
+ pluginService.loadPlugins(this.appService);
65
+ }
66
+ }
67
+
68
+ private async getConfig(): Promise<ConfigInterface> {
69
+ const path = resolve(this.configPath);
70
+ const configImport = await import(path);
71
+ const config: unknown = configImport.default;
72
+ return config as ConfigInterface;
73
+ }
74
+ }
@@ -0,0 +1,3 @@
1
+ export * from './config.interface';
2
+ export * from './config.module';
3
+ export * from './config.service';
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { default as AppContainer } from './app.container';
2
+ export * from './app.container';
3
+ export * from './app.module';
4
+ export * from './app.service';
5
+ export * from './color';
6
+ export * from './config';
7
+ export * from './main';
8
+ export * from './material-color-utilities';
9
+ export * from './plugin';
10
+ export * from './plugins/tailwind';
11
+ export * from './theme';
package/src/main.ts ADDED
@@ -0,0 +1,14 @@
1
+ import AppContainer from './app.container';
2
+ import { AppService } from './app.service';
3
+ import { ConfigService } from './config';
4
+
5
+ export function bootstrap(): AppService {
6
+ return AppContainer.resolve<AppService>('appService');
7
+ }
8
+
9
+ export async function bootstrapFromConfig(path?: string): Promise<AppService> {
10
+ const configService = AppContainer.resolve<ConfigService>('configService');
11
+ if (path) configService.configPath = path;
12
+ await configService.loadConfig();
13
+ return AppContainer.resolve<AppService>('appService');
14
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2023 Google LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import { lerp } from '@material/material-color-utilities';
19
+
20
+ /**
21
+ * A class containing a value that changes with the contrast level.
22
+ *
23
+ * Usually represents the contrast requirements for a dynamic color on its
24
+ * background. The four values correspond to values for contrast levels -1.0,
25
+ * 0.0, 0.5, and 1.0, respectively.
26
+ */
27
+ export class ContrastCurve {
28
+ /**
29
+ * Creates a `ContrastCurve` object.
30
+ *
31
+ * @param low Value for contrast level -1.0
32
+ * @param normal Value for contrast level 0.0
33
+ * @param medium Value for contrast level 0.5
34
+ * @param high Value for contrast level 1.0
35
+ */
36
+ constructor(
37
+ readonly low: number,
38
+ readonly normal: number,
39
+ readonly medium: number,
40
+ readonly high: number
41
+ ) {}
42
+
43
+ /**
44
+ * Returns the value at a given contrast level.
45
+ *
46
+ * @param contrastLevel The contrast level. 0.0 is the default (normal); -1.0
47
+ * is the lowest; 1.0 is the highest.
48
+ * @return The value. For contrast ratios, a number between 1.0 and 21.0.
49
+ */
50
+ get(contrastLevel: number): number {
51
+ if (contrastLevel <= -1.0) {
52
+ return this.low;
53
+ } else if (contrastLevel < 0.0) {
54
+ return lerp(this.low, this.normal, (contrastLevel - -1) / 1);
55
+ } else if (contrastLevel < 0.5) {
56
+ return lerp(this.normal, this.medium, (contrastLevel - 0) / 0.5);
57
+ } else if (contrastLevel < 1.0) {
58
+ return lerp(this.medium, this.high, (contrastLevel - 0.5) / 0.5);
59
+ } else {
60
+ return this.high;
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,450 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import {
19
+ clampDouble,
20
+ Contrast,
21
+ Hct,
22
+ TonalPalette,
23
+ } from '@material/material-color-utilities';
24
+ import { ContrastCurve } from './contrastCurve';
25
+ import { ToneDeltaPair } from './toneDeltaPair';
26
+ import { SchemeEntity } from '../theme/entities/scheme.entity';
27
+
28
+ /**
29
+ * @param name The name of the dynamic color. Defaults to empty.
30
+ * @param palette Function that provides a TonalPalette given
31
+ * SchemeEntity. A TonalPalette is defined by a hue and chroma, so this
32
+ * replaces the need to specify hue/chroma. By providing a tonal palette, when
33
+ * contrast adjustments are made, intended chroma can be preserved.
34
+ * @param tone Function that provides a tone given SchemeEntity.
35
+ * @param isBackground Whether this dynamic color is a background, with
36
+ * some other color as the foreground. Defaults to false.
37
+ * @param background The background of the dynamic color (as a function of a
38
+ * `SchemeEntity`), if it exists.
39
+ * @param secondBackground A second background of the dynamic color (as a
40
+ * function of a `SchemeEntity`), if it
41
+ * exists.
42
+ * @param contrastCurve A `ContrastCurve` object specifying how its contrast
43
+ * against its background should behave in various contrast levels options.
44
+ * @param toneDeltaPair A `ToneDeltaPair` object specifying a tone delta
45
+ * constraint between two colors. One of them must be the color being
46
+ * constructed.
47
+ */
48
+ interface FromPaletteOptions {
49
+ name?: string;
50
+ palette: (scheme: SchemeEntity) => TonalPalette;
51
+ tone: (scheme: SchemeEntity) => number;
52
+ isBackground?: boolean;
53
+ background?: (scheme: SchemeEntity) => DynamicColor;
54
+ secondBackground?: (scheme: SchemeEntity) => DynamicColor;
55
+ contrastCurve?: ContrastCurve;
56
+ toneDeltaPair?: (scheme: SchemeEntity) => ToneDeltaPair;
57
+ }
58
+
59
+ /**
60
+ * A color that adjusts itself based on UI state provided by SchemeEntity.
61
+ *
62
+ * Colors without backgrounds do not change tone when contrast changes. Colors
63
+ * with backgrounds become closer to their background as contrast lowers, and
64
+ * further when contrast increases.
65
+ *
66
+ * Prefer static constructors. They require either a hexcode, a palette and
67
+ * tone, or a hue and chroma. Optionally, they can provide a background
68
+ * DynamicColor.
69
+ */
70
+ export class DynamicColor {
71
+ private readonly hctCache = new Map<SchemeEntity, Hct>();
72
+
73
+ /**
74
+ * The base constructor for DynamicColor.
75
+ *
76
+ * _Strongly_ prefer using one of the convenience constructors. This class is
77
+ * arguably too flexible to ensure it can support any scenario. Functional
78
+ * arguments allow overriding without risks that come with subclasses.
79
+ *
80
+ * For example, the default behavior of adjust tone at max contrast
81
+ * to be at a 7.0 ratio with its background is principled and
82
+ * matches accessibility guidance. That does not mean it's the desired
83
+ * approach for _every_ design system, and every color pairing,
84
+ * always, in every case.
85
+ *
86
+ * @param name The name of the dynamic color. Defaults to empty.
87
+ * @param palette Function that provides a TonalPalette given
88
+ * SchemeEntity. A TonalPalette is defined by a hue and chroma, so this
89
+ * replaces the need to specify hue/chroma. By providing a tonal palette, when
90
+ * contrast adjustments are made, intended chroma can be preserved.
91
+ * @param tone Function that provides a tone, given a SchemeEntity.
92
+ * @param isBackground Whether this dynamic color is a background, with
93
+ * some other color as the foreground. Defaults to false.
94
+ * @param background The background of the dynamic color (as a function of a
95
+ * `SchemeEntity`), if it exists.
96
+ * @param secondBackground A second background of the dynamic color (as a
97
+ * function of a `SchemeEntity`), if it
98
+ * exists.
99
+ * @param contrastCurve A `ContrastCurve` object specifying how its contrast
100
+ * against its background should behave in various contrast levels options.
101
+ * @param toneDeltaPair A `ToneDeltaPair` object specifying a tone delta
102
+ * constraint between two colors. One of them must be the color being
103
+ * constructed.
104
+ */
105
+ constructor(
106
+ readonly name: string,
107
+ readonly palette: (scheme: SchemeEntity) => TonalPalette,
108
+ readonly tone: (scheme: SchemeEntity) => number,
109
+ readonly isBackground: boolean,
110
+ readonly background?: (scheme: SchemeEntity) => DynamicColor,
111
+ readonly secondBackground?: (scheme: SchemeEntity) => DynamicColor,
112
+ readonly contrastCurve?: ContrastCurve,
113
+ readonly toneDeltaPair?: (scheme: SchemeEntity) => ToneDeltaPair
114
+ ) {
115
+ if (!background && secondBackground) {
116
+ throw new Error(
117
+ `Color ${name} has secondBackground` +
118
+ `defined, but background is not defined.`
119
+ );
120
+ }
121
+ if (!background && contrastCurve) {
122
+ throw new Error(
123
+ `Color ${name} has contrastCurve` +
124
+ `defined, but background is not defined.`
125
+ );
126
+ }
127
+ if (background && !contrastCurve) {
128
+ throw new Error(
129
+ `Color ${name} has background` +
130
+ `defined, but contrastCurve is not defined.`
131
+ );
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Create a DynamicColor defined by a TonalPalette and HCT tone.
137
+ *
138
+ * @param args Functions with SchemeEntity as input. Must provide a palette
139
+ * and tone. May provide a background DynamicColor and ToneDeltaConstraint.
140
+ */
141
+ static fromPalette(args: FromPaletteOptions): DynamicColor {
142
+ return new DynamicColor(
143
+ args.name ?? '',
144
+ args.palette,
145
+ args.tone,
146
+ args.isBackground ?? false,
147
+ args.background,
148
+ args.secondBackground,
149
+ args.contrastCurve,
150
+ args.toneDeltaPair
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Given a background tone, find a foreground tone, while ensuring they reach
156
+ * a contrast ratio that is as close to [ratio] as possible.
157
+ *
158
+ * @param bgTone Tone in HCT. Range is 0 to 100, undefined behavior when it
159
+ * falls outside that range.
160
+ * @param ratio The contrast ratio desired between bgTone and the return
161
+ * value.
162
+ */
163
+ static foregroundTone(bgTone: number, ratio: number): number {
164
+ const lighterTone = Contrast.lighterUnsafe(bgTone, ratio);
165
+ const darkerTone = Contrast.darkerUnsafe(bgTone, ratio);
166
+ const lighterRatio = Contrast.ratioOfTones(lighterTone, bgTone);
167
+ const darkerRatio = Contrast.ratioOfTones(darkerTone, bgTone);
168
+ const preferLighter = DynamicColor.tonePrefersLightForeground(bgTone);
169
+
170
+ if (preferLighter) {
171
+ // This handles an edge case where the initial contrast ratio is high
172
+ // (ex. 13.0), and the ratio passed to the function is that high
173
+ // ratio, and both the lighter and darker ratio fails to pass that
174
+ // ratio.
175
+ //
176
+ // This was observed with Tonal Spot's On Primary Container turning
177
+ // black momentarily between high and max contrast in light mode. PC's
178
+ // standard tone was T90, OPC's was T10, it was light mode, and the
179
+ // contrast value was 0.6568521221032331.
180
+ const negligibleDifference =
181
+ Math.abs(lighterRatio - darkerRatio) < 0.1 &&
182
+ lighterRatio < ratio &&
183
+ darkerRatio < ratio;
184
+ return lighterRatio >= ratio ||
185
+ lighterRatio >= darkerRatio ||
186
+ negligibleDifference
187
+ ? lighterTone
188
+ : darkerTone;
189
+ } else {
190
+ return darkerRatio >= ratio || darkerRatio >= lighterRatio
191
+ ? darkerTone
192
+ : lighterTone;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Returns whether [tone] prefers a light foreground.
198
+ *
199
+ * People prefer white foregrounds on ~T60-70. Observed over time, and also
200
+ * by Andrew Somers during research for APCA.
201
+ *
202
+ * T60 used as to create the smallest discontinuity possible when skipping
203
+ * down to T49 in order to ensure light foregrounds.
204
+ * Since `tertiaryContainer` in dark monochrome scheme requires a tone of
205
+ * 60, it should not be adjusted. Therefore, 60 is excluded here.
206
+ */
207
+ static tonePrefersLightForeground(tone: number): boolean {
208
+ return Math.round(tone) < 60.0;
209
+ }
210
+
211
+ /**
212
+ * Returns whether [tone] can reach a contrast ratio of 4.5 with a lighter
213
+ * color.
214
+ */
215
+ static toneAllowsLightForeground(tone: number): boolean {
216
+ return Math.round(tone) <= 49.0;
217
+ }
218
+
219
+ /**
220
+ * Adjust a tone such that white has 4.5 contrast, if the tone is
221
+ * reasonably close to supporting it.
222
+ */
223
+ static enableLightForeground(tone: number): number {
224
+ if (
225
+ DynamicColor.tonePrefersLightForeground(tone) &&
226
+ !DynamicColor.toneAllowsLightForeground(tone)
227
+ ) {
228
+ return 49.0;
229
+ }
230
+ return tone;
231
+ }
232
+
233
+ /**
234
+ * Return a ARGB integer (i.e. a hex code).
235
+ *
236
+ * @param scheme Defines the conditions of the user interface, for example,
237
+ * whether or not it is dark mode or light mode, and what the desired
238
+ * contrast level is.
239
+ */
240
+ getArgb(scheme: SchemeEntity): number {
241
+ return this.getHct(scheme).toInt();
242
+ }
243
+
244
+ /**
245
+ * Return a color, expressed in the HCT color space, that this
246
+ * DynamicColor is under the conditions in scheme.
247
+ *
248
+ * @param scheme Defines the conditions of the user interface, for example,
249
+ * whether or not it is dark mode or light mode, and what the desired
250
+ * contrast level is.
251
+ */
252
+ getHct(scheme: SchemeEntity): Hct {
253
+ const cachedAnswer = this.hctCache.get(scheme);
254
+ if (cachedAnswer != null) {
255
+ return cachedAnswer;
256
+ }
257
+ const tone = this.getTone(scheme);
258
+ const answer = this.palette(scheme).getHct(tone);
259
+ if (this.hctCache.size > 4) {
260
+ this.hctCache.clear();
261
+ }
262
+ this.hctCache.set(scheme, answer);
263
+ return answer;
264
+ }
265
+
266
+ /**
267
+ * Return a tone, T in the HCT color space, that this DynamicColor is under
268
+ * the conditions in scheme.
269
+ *
270
+ * @param scheme Defines the conditions of the user interface, for example,
271
+ * whether or not it is dark mode or light mode, and what the desired
272
+ * contrast level is.
273
+ */
274
+ getTone(scheme: SchemeEntity): number {
275
+ const decreasingContrast = scheme.contrastLevel < 0;
276
+
277
+ // Case 1: dual foreground, pair of colors with delta constraint.
278
+ if (this.toneDeltaPair) {
279
+ const toneDeltaPair = this.toneDeltaPair(scheme);
280
+ const roleA = toneDeltaPair.roleA;
281
+ const roleB = toneDeltaPair.roleB;
282
+ const delta = toneDeltaPair.delta;
283
+ const polarity = toneDeltaPair.polarity;
284
+ const stayTogether = toneDeltaPair.stayTogether;
285
+
286
+ const bg = this.background!(scheme);
287
+ const bgTone = bg.getTone(scheme);
288
+
289
+ const aIsNearer =
290
+ polarity === 'nearer' ||
291
+ (polarity === 'lighter' && !scheme.isDark) ||
292
+ (polarity === 'darker' && scheme.isDark);
293
+ const nearer = aIsNearer ? roleA : roleB;
294
+ const farther = aIsNearer ? roleB : roleA;
295
+ const amNearer = this.name === nearer.name;
296
+ const expansionDir = scheme.isDark ? 1 : -1;
297
+
298
+ // 1st round: solve to min, each
299
+ const nContrast = nearer.contrastCurve!.get(scheme.contrastLevel);
300
+ const fContrast = farther.contrastCurve!.get(scheme.contrastLevel);
301
+
302
+ // If a color is good enough, it is not adjusted.
303
+ // Initial and adjusted tones for `nearer`
304
+ const nInitialTone = nearer.tone(scheme);
305
+ let nTone =
306
+ Contrast.ratioOfTones(bgTone, nInitialTone) >= nContrast
307
+ ? nInitialTone
308
+ : DynamicColor.foregroundTone(bgTone, nContrast);
309
+ // Initial and adjusted tones for `farther`
310
+ const fInitialTone = farther.tone(scheme);
311
+ let fTone =
312
+ Contrast.ratioOfTones(bgTone, fInitialTone) >= fContrast
313
+ ? fInitialTone
314
+ : DynamicColor.foregroundTone(bgTone, fContrast);
315
+
316
+ if (decreasingContrast) {
317
+ // If decreasing contrast, adjust color to the "bare minimum"
318
+ // that satisfies contrast.
319
+ nTone = DynamicColor.foregroundTone(bgTone, nContrast);
320
+ fTone = DynamicColor.foregroundTone(bgTone, fContrast);
321
+ }
322
+
323
+ if ((fTone - nTone) * expansionDir >= delta) {
324
+ // Good! Tones satisfy the constraint; no change needed.
325
+ } else {
326
+ // 2nd round: expand farther to match delta.
327
+ fTone = clampDouble(0, 100, nTone + delta * expansionDir);
328
+ if ((fTone - nTone) * expansionDir >= delta) {
329
+ // Good! Tones now satisfy the constraint; no change needed.
330
+ } else {
331
+ // 3rd round: contract nearer to match delta.
332
+ nTone = clampDouble(0, 100, fTone - delta * expansionDir);
333
+ }
334
+ }
335
+
336
+ // Avoids the 50-59 awkward zone.
337
+ if (50 <= nTone && nTone < 60) {
338
+ // If `nearer` is in the awkward zone, move it away, together with
339
+ // `farther`.
340
+ if (expansionDir > 0) {
341
+ nTone = 60;
342
+ fTone = Math.max(fTone, nTone + delta * expansionDir);
343
+ } else {
344
+ nTone = 49;
345
+ fTone = Math.min(fTone, nTone + delta * expansionDir);
346
+ }
347
+ } else if (50 <= fTone && fTone < 60) {
348
+ if (stayTogether) {
349
+ // Fixes both, to avoid two colors on opposite sides of the "awkward
350
+ // zone".
351
+ if (expansionDir > 0) {
352
+ nTone = 60;
353
+ fTone = Math.max(fTone, nTone + delta * expansionDir);
354
+ } else {
355
+ nTone = 49;
356
+ fTone = Math.min(fTone, nTone + delta * expansionDir);
357
+ }
358
+ } else {
359
+ // Not required to stay together; fixes just one.
360
+ if (expansionDir > 0) {
361
+ fTone = 60;
362
+ } else {
363
+ fTone = 49;
364
+ }
365
+ }
366
+ }
367
+
368
+ // Returns `nTone` if this color is `nearer`, otherwise `fTone`.
369
+ return amNearer ? nTone : fTone;
370
+ } else {
371
+ // Case 2: No contrast pair; just solve for itself.
372
+ let answer = this.tone(scheme);
373
+
374
+ if (this.background == null) {
375
+ return answer; // No adjustment for colors with no background.
376
+ }
377
+
378
+ const bgTone = this.background(scheme).getTone(scheme);
379
+
380
+ const desiredRatio = this.contrastCurve!.get(scheme.contrastLevel);
381
+
382
+ if (Contrast.ratioOfTones(bgTone, answer) >= desiredRatio) {
383
+ // Don't "improve" what's good enough.
384
+ } else {
385
+ // Rough improvement.
386
+ answer = DynamicColor.foregroundTone(bgTone, desiredRatio);
387
+ }
388
+
389
+ if (decreasingContrast) {
390
+ answer = DynamicColor.foregroundTone(bgTone, desiredRatio);
391
+ }
392
+
393
+ if (this.isBackground && 50 <= answer && answer < 60) {
394
+ // Must adjust
395
+ if (Contrast.ratioOfTones(49, bgTone) >= desiredRatio) {
396
+ answer = 49;
397
+ } else {
398
+ answer = 60;
399
+ }
400
+ }
401
+
402
+ if (this.secondBackground) {
403
+ // Case 3: Adjust for dual backgrounds.
404
+
405
+ const [bg1, bg2] = [this.background, this.secondBackground];
406
+ const [bgTone1, bgTone2] = [
407
+ bg1(scheme).getTone(scheme),
408
+ bg2(scheme).getTone(scheme),
409
+ ];
410
+ const [upper, lower] = [
411
+ Math.max(bgTone1, bgTone2),
412
+ Math.min(bgTone1, bgTone2),
413
+ ];
414
+
415
+ if (
416
+ Contrast.ratioOfTones(upper, answer) >= desiredRatio &&
417
+ Contrast.ratioOfTones(lower, answer) >= desiredRatio
418
+ ) {
419
+ return answer;
420
+ }
421
+
422
+ // The darkest light tone that satisfies the desired ratio,
423
+ // or -1 if such ratio cannot be reached.
424
+ const lightOption = Contrast.lighter(upper, desiredRatio);
425
+
426
+ // The lightest dark tone that satisfies the desired ratio,
427
+ // or -1 if such ratio cannot be reached.
428
+ const darkOption = Contrast.darker(lower, desiredRatio);
429
+
430
+ // Tones suitable for the foreground.
431
+ const availables = [];
432
+ if (lightOption !== -1) availables.push(lightOption);
433
+ if (darkOption !== -1) availables.push(darkOption);
434
+
435
+ const prefersLight =
436
+ DynamicColor.tonePrefersLightForeground(bgTone1) ||
437
+ DynamicColor.tonePrefersLightForeground(bgTone2);
438
+ if (prefersLight) {
439
+ return lightOption < 0 ? 100 : lightOption;
440
+ }
441
+ if (availables.length === 1) {
442
+ return availables[0];
443
+ }
444
+ return darkOption < 0 ? 0 : darkOption;
445
+ }
446
+
447
+ return answer;
448
+ }
449
+ }
450
+ }
@@ -0,0 +1,3 @@
1
+ export * from './contrastCurve';
2
+ export * from './dynamic_color';
3
+ export * from './toneDeltaPair';
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2023 Google LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import { DynamicColor } from './dynamic_color';
19
+
20
+ /**
21
+ * Describes the different in tone between colors.
22
+ */
23
+ export type TonePolarity = 'darker' | 'lighter' | 'nearer' | 'farther';
24
+
25
+ /**
26
+ * Documents a constraint between two DynamicColors, in which their tones must
27
+ * have a certain distance from each other.
28
+ *
29
+ * Prefer a DynamicColor with a background, this is for special cases when
30
+ * designers want tonal distance, literally contrast, between two colors that
31
+ * don't have a background / foreground relationship or a contrast guarantee.
32
+ */
33
+ export class ToneDeltaPair {
34
+ /**
35
+ * Documents a constraint in tone distance between two DynamicColors.
36
+ *
37
+ * The polarity is an adjective that describes "A", compared to "B".
38
+ *
39
+ * For instance, ToneDeltaPair(A, B, 15, 'darker', stayTogether) states that
40
+ * A's tone should be at least 15 darker than B's.
41
+ *
42
+ * 'nearer' and 'farther' describes closeness to the surface roles. For
43
+ * instance, ToneDeltaPair(A, B, 10, 'nearer', stayTogether) states that A
44
+ * should be 10 lighter than B in light mode, and 10 darker than B in dark
45
+ * mode.
46
+ *
47
+ * @param roleA The first role in a pair.
48
+ * @param roleB The second role in a pair.
49
+ * @param delta Required difference between tones. Absolute value, negative
50
+ * values have undefined behavior.
51
+ * @param polarity The relative relation between tones of roleA and roleB,
52
+ * as described above.
53
+ * @param stayTogether Whether these two roles should stay on the same side of
54
+ * the "awkward zone" (T50-59). This is necessary for certain cases where
55
+ * one role has two backgrounds.
56
+ */
57
+ constructor(
58
+ readonly roleA: DynamicColor,
59
+ readonly roleB: DynamicColor,
60
+ readonly delta: number,
61
+ readonly polarity: TonePolarity,
62
+ readonly stayTogether: boolean
63
+ ) {}
64
+ }
@@ -0,0 +1,3 @@
1
+ export * from './plugin.abstract';
2
+ export * from './plugin.module';
3
+ export * from './plugin.service';