@@ -19,10 +19,25 @@ import {
19
19
// tonal palettes then get used to create the different color roles (ex.
20
20
// on-primary) https://m3.material.io/styles/color/system/how-the-system-works
21
21
const HUE_TONES = [ 0 , 10 , 20 , 25 , 30 , 35 , 40 , 50 , 60 , 70 , 80 , 90 , 95 , 98 , 99 , 100 ] ;
22
+ // Map of neutral hues to the previous/next hues that
23
+ // can be used to estimate them, in case they're missing.
24
+ const NEUTRAL_HUES = new Map < number , { prev : number ; next : number } > ( [
25
+ [ 4 , { prev : 0 , next : 10 } ] ,
26
+ [ 6 , { prev : 0 , next : 10 } ] ,
27
+ [ 12 , { prev : 10 , next : 20 } ] ,
28
+ [ 17 , { prev : 10 , next : 20 } ] ,
29
+ [ 22 , { prev : 20 , next : 25 } ] ,
30
+ [ 24 , { prev : 20 , next : 25 } ] ,
31
+ [ 87 , { prev : 80 , next : 90 } ] ,
32
+ [ 92 , { prev : 90 , next : 95 } ] ,
33
+ [ 94 , { prev : 90 , next : 95 } ] ,
34
+ [ 96 , { prev : 95 , next : 98 } ] ,
35
+ ] ) ;
36
+
22
37
// Note: Some of the color tokens refer to additional hue tones, but this only
23
38
// applies for the neutral color palette (ex. surface container is neutral
24
39
// palette's 94 tone). https://m3.material.io/styles/color/static/baseline
25
- const NEUTRAL_HUE_TONES = HUE_TONES . concat ( [ 4 , 6 , 12 , 17 , 22 , 24 , 87 , 92 , 94 , 96 ] ) ;
40
+ const NEUTRAL_HUE_TONES = [ ... HUE_TONES , ... NEUTRAL_HUES . keys ( ) ] ;
26
41
27
42
/**
28
43
* Gets color tonal palettes generated by Material from the provided color.
@@ -117,7 +132,7 @@ export function generateSCSSTheme(
117
132
"@use '@angular/material' as mat;" ,
118
133
'' ,
119
134
'// Note: ' + colorComment ,
120
- '$_palettes: ' + getColorPalettesSCSS ( colorPalettes ) ,
135
+ '$_palettes: ' + getColorPalettesSCSS ( patchMissingHues ( colorPalettes ) ) ,
121
136
'' ,
122
137
'$_rest: (' ,
123
138
' secondary: map.get($_palettes, secondary),' ,
@@ -192,3 +207,110 @@ export default function (options: Schema): Rule {
192
207
createThemeFile ( themeScss , tree , options . directory ) ;
193
208
} ;
194
209
}
210
+
211
+ /**
212
+ * The hue map produced by `material-color-utilities` may miss some neutral hues depending on
213
+ * the provided colors. This function estimates the missing hues based on the generated ones
214
+ * to ensure that we always produce a full palette. See #29157.
215
+ *
216
+ * This is a TypeScript port of the logic in `core/theming/_palettes.scss#_patch-missing-hues`.
217
+ */
218
+ function patchMissingHues (
219
+ palettes : Map < string , Map < number , string > > ,
220
+ ) : Map < string , Map < number , string > > {
221
+ const neutral = palettes . get ( 'neutral' ) ;
222
+
223
+ if ( ! neutral ) {
224
+ return palettes ;
225
+ }
226
+
227
+ let newNeutral : Map < number , string > | null = null ;
228
+
229
+ for ( const [ hue , { prev, next} ] of NEUTRAL_HUES ) {
230
+ if ( ! neutral . has ( hue ) && neutral . has ( prev ) && neutral . has ( next ) ) {
231
+ const weight = ( next - hue ) / ( next - prev ) ;
232
+ const result = mixColors ( neutral . get ( prev ) ! , neutral . get ( next ) ! , weight ) ;
233
+
234
+ if ( result !== null ) {
235
+ newNeutral ??= new Map ( neutral . entries ( ) ) ;
236
+ newNeutral . set ( hue , result ) ;
237
+ }
238
+ }
239
+ }
240
+
241
+ if ( ! newNeutral ) {
242
+ return palettes ;
243
+ }
244
+
245
+ // Create a new map so we don't mutate the one that was passed in.
246
+ const newPalettes = new Map < string , Map < number , string > > ( ) ;
247
+ for ( const [ key , value ] of palettes ) {
248
+ if ( key === 'neutral' ) {
249
+ // Maps keep the order of their keys which can make the newly-added
250
+ // ones look out of place. Re-sort the the keys in ascending order.
251
+ const sortedNeutral = Array . from ( newNeutral . keys ( ) )
252
+ . sort ( ( a , b ) => a - b )
253
+ . reduce ( ( newHues , key ) => {
254
+ newHues . set ( key , newNeutral . get ( key ) ! ) ;
255
+ return newHues ;
256
+ } , new Map < number , string > ( ) ) ;
257
+ newPalettes . set ( key , sortedNeutral ) ;
258
+ } else {
259
+ newPalettes . set ( key , value ) ;
260
+ }
261
+ }
262
+
263
+ return newPalettes ;
264
+ }
265
+
266
+ /**
267
+ * TypeScript port of the `color.mix` function from Sass, simplified to only deal with hex colors.
268
+ * See https://github.com/sass/dart-sass/blob/main/lib/src/functions/color.dart#L803
269
+ *
270
+ * @param c1 First color to use in the mixture.
271
+ * @param c2 Second color to use in the mixture.
272
+ * @param weight Proportion of the first color to use in the mixture.
273
+ * Should be a number between 0 and 1.
274
+ */
275
+ function mixColors ( c1 : string , c2 : string , weight : number ) : string | null {
276
+ const normalizedWeight = weight * 2 - 1 ;
277
+ const weight1 = ( normalizedWeight + 1 ) / 2 ;
278
+ const weight2 = 1 - weight1 ;
279
+ const color1 = parseHexColor ( c1 ) ;
280
+ const color2 = parseHexColor ( c2 ) ;
281
+
282
+ if ( color1 === null || color2 === null ) {
283
+ return null ;
284
+ }
285
+
286
+ const red = Math . round ( color1 . red * weight1 + color2 . red * weight2 ) ;
287
+ const green = Math . round ( color1 . green * weight1 + color2 . green * weight2 ) ;
288
+ const blue = Math . round ( color1 . blue * weight1 + color2 . blue * weight2 ) ;
289
+ const intToHex = ( value : number ) => value . toString ( 16 ) . padStart ( 2 , '0' ) ;
290
+
291
+ return `#${ intToHex ( red ) } ${ intToHex ( green ) } ${ intToHex ( blue ) } ` ;
292
+ }
293
+
294
+ /** Parses a hex color to its numeric red, green and blue values. */
295
+ function parseHexColor ( value : string ) : { red : number ; green : number ; blue : number } | null {
296
+ if ( ! / ^ # (?: [ 0 - 9 a - f A - F ] { 3 } ) { 1 , 2 } $ / . test ( value ) ) {
297
+ return null ;
298
+ }
299
+
300
+ const hexToInt = ( value : string ) => parseInt ( value , 16 ) ;
301
+ let red : number ;
302
+ let green : number ;
303
+ let blue : number ;
304
+
305
+ if ( value . length === 4 ) {
306
+ red = hexToInt ( value [ 1 ] + value [ 1 ] ) ;
307
+ green = hexToInt ( value [ 2 ] + value [ 2 ] ) ;
308
+ blue = hexToInt ( value [ 3 ] + value [ 3 ] ) ;
309
+ } else {
310
+ red = hexToInt ( value . slice ( 1 , 3 ) ) ;
311
+ green = hexToInt ( value . slice ( 3 , 5 ) ) ;
312
+ blue = hexToInt ( value . slice ( 5 , 7 ) ) ;
313
+ }
314
+
315
+ return { red, green, blue} ;
316
+ }
0 commit comments