mtrl 0.10.1 → 0.10.3

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 (91) hide show
  1. package/dist/components/chips/api.js +4 -0
  2. package/dist/components/chips/features/controller.js +6 -1
  3. package/dist/components/chips/types.d.ts +10 -0
  4. package/dist/components/loading-indicator/constants.js +2 -2
  5. package/dist/components/loading-indicator/features/renderer.js +13 -2
  6. package/dist/components/tabs/utils.js +3 -2
  7. package/dist/core/shapes/cubic.d.ts +2 -0
  8. package/dist/core/shapes/cubic.js +14 -0
  9. package/dist/core/shapes/index.d.ts +3 -3
  10. package/dist/core/shapes/index.js +3 -3
  11. package/dist/core/shapes/material.d.ts +136 -6
  12. package/dist/core/shapes/material.js +226 -25
  13. package/dist/core/shapes/polygon.d.ts +2 -0
  14. package/dist/core/shapes/polygon.js +27 -10
  15. package/dist/core/theme/shape.d.ts +1 -1
  16. package/dist/elements/css/checkbox.js +1 -1
  17. package/dist/elements/css/search.js +1 -1
  18. package/dist/elements/css/textfield.js +1 -1
  19. package/dist/elements/preupgrade.css +1 -1
  20. package/dist/index.cjs +0 -0
  21. package/dist/package.json +1 -1
  22. package/dist/styles/badge.css +1 -1
  23. package/dist/styles/base.css +2 -2
  24. package/dist/styles/bottom-app-bar.css +1 -1
  25. package/dist/styles/bottom-sheet.css +1 -1
  26. package/dist/styles/button-group.css +1 -1
  27. package/dist/styles/button.css +1 -1
  28. package/dist/styles/card.css +1 -1
  29. package/dist/styles/carousel.css +1 -1
  30. package/dist/styles/checkbox.css +2 -2
  31. package/dist/styles/chips.css +1 -1
  32. package/dist/styles/datepicker.css +1 -1
  33. package/dist/styles/dialog.css +1 -1
  34. package/dist/styles/divider.css +1 -1
  35. package/dist/styles/drawer.css +1 -1
  36. package/dist/styles/extended-fab.css +1 -1
  37. package/dist/styles/fab-menu.css +1 -1
  38. package/dist/styles/fab.css +1 -1
  39. package/dist/styles/icon-button.css +1 -1
  40. package/dist/styles/list.css +1 -1
  41. package/dist/styles/loading-indicator.css +1 -1
  42. package/dist/styles/menu.css +1 -1
  43. package/dist/styles/navigation-rail.css +1 -1
  44. package/dist/styles/progress.css +1 -1
  45. package/dist/styles/radios.css +1 -1
  46. package/dist/styles/search.css +2 -2
  47. package/dist/styles/segmented-button.css +1 -1
  48. package/dist/styles/select.css +1 -1
  49. package/dist/styles/side-sheet.css +1 -1
  50. package/dist/styles/slider.css +1 -1
  51. package/dist/styles/snackbar.css +1 -1
  52. package/dist/styles/split-button.css +1 -1
  53. package/dist/styles/switch.css +1 -1
  54. package/dist/styles/tabs.css +1 -1
  55. package/dist/styles/textfield.css +2 -2
  56. package/dist/styles/timepicker.css +1 -1
  57. package/dist/styles/toolbar.css +1 -1
  58. package/dist/styles/tooltip.css +1 -1
  59. package/dist/styles/top-app-bar.css +1 -1
  60. package/dist/styles/utilities.css +1 -1
  61. package/dist/styles.css +2 -2
  62. package/dist/themes/autumn.css +1 -1
  63. package/dist/themes/baseline.css +1 -1
  64. package/dist/themes/brownbeige.css +1 -1
  65. package/dist/themes/browngreen.css +1 -1
  66. package/dist/themes/content.css +1 -1
  67. package/dist/themes/desert.css +1 -1
  68. package/dist/themes/expressive.css +1 -1
  69. package/dist/themes/fidelity.css +1 -1
  70. package/dist/themes/forest.css +1 -1
  71. package/dist/themes/fruit-salad.css +1 -1
  72. package/dist/themes/highcontrast.css +1 -1
  73. package/dist/themes/legacy.css +1 -1
  74. package/dist/themes/material.css +1 -1
  75. package/dist/themes/monochrome.css +1 -1
  76. package/dist/themes/neutral.css +1 -1
  77. package/dist/themes/ocean.css +1 -1
  78. package/dist/themes/rainbow.css +1 -1
  79. package/dist/themes/sageivory.css +1 -1
  80. package/dist/themes/spring.css +1 -1
  81. package/dist/themes/summer.css +1 -1
  82. package/dist/themes/sunset.css +1 -1
  83. package/dist/themes/tealcaramel.css +1 -1
  84. package/dist/themes/vibrant.css +1 -1
  85. package/dist/themes/winter.css +1 -1
  86. package/package.json +1 -1
  87. package/src/styles/abstract/_theme.scss +4 -0
  88. package/src/styles/abstract/_variables.scss +7 -0
  89. package/src/styles/components/_checkbox.scss +2 -1
  90. package/src/styles/components/_search.scss +7 -3
  91. package/src/styles/components/_textfield.scss +20 -4
@@ -133,6 +133,10 @@ export const withAPI = (options) => (component) => {
133
133
  }
134
134
  return this;
135
135
  },
136
+ keyboard: {
137
+ enable: () => options.keyboard?.enableKeyboardNavigation?.(),
138
+ disable: () => options.keyboard?.disableKeyboardNavigation?.(),
139
+ },
136
140
  on(event, handler) {
137
141
  if (options.events && typeof options.events.on === "function") {
138
142
  options.events.on(event, handler);
@@ -9,6 +9,7 @@ export const withController = (config) => (component) => {
9
9
  remove: [],
10
10
  };
11
11
  let focusedChipIndex = -1;
12
+ let keyboardEnabled = true;
12
13
  const dispatchEvent = (eventName, ...args) => {
13
14
  if (eventListeners[eventName]) {
14
15
  eventListeners[eventName].forEach((handler) => handler(...args));
@@ -58,11 +59,13 @@ export const withController = (config) => (component) => {
58
59
  current.tabIndex = 0;
59
60
  };
60
61
  const handleKeyboardNavigation = (event) => {
62
+ if (!keyboardEnabled)
63
+ return;
61
64
  const list = targets();
62
65
  if (list.length === 0)
63
66
  return;
64
67
  const isVertical = !!(component.layout && component.layout.isVertical());
65
- const rtl = component.element.closest("[dir]")?.getAttribute("dir")?.toLowerCase() === "rtl";
68
+ const rtl = component.element.matches(":dir(rtl)");
66
69
  const back = isVertical ? "ArrowUp" : rtl ? "ArrowRight" : "ArrowLeft";
67
70
  const forward = isVertical ? "ArrowDown" : rtl ? "ArrowLeft" : "ArrowRight";
68
71
  const from = list.indexOf(event.target);
@@ -236,11 +239,13 @@ export const withController = (config) => (component) => {
236
239
  }
237
240
  };
238
241
  const enableKeyboardNavigation = () => {
242
+ keyboardEnabled = true;
239
243
  component.chipInstances.forEach((chip) => {
240
244
  chip.element.addEventListener("keydown", handleKeyboardNavigation);
241
245
  });
242
246
  };
243
247
  const disableKeyboardNavigation = () => {
248
+ keyboardEnabled = false;
244
249
  component.chipInstances.forEach((chip) => {
245
250
  chip.element.removeEventListener("keydown", handleKeyboardNavigation);
246
251
  });
@@ -345,6 +345,16 @@ export interface ChipsComponent {
345
345
  * @returns The chips instance for chaining
346
346
  */
347
347
  enableKeyboardNavigation: () => ChipsComponent;
348
+ /**
349
+ * Turns keyboard navigation between the chips on and off: while disabled,
350
+ * the arrows, Home and End no longer move focus in the set (FLO-352)
351
+ */
352
+ keyboard: {
353
+ /** Moves focus with the arrows, Home and End again */
354
+ enable: () => void;
355
+ /** Stops moving focus with the arrows, Home and End */
356
+ disable: () => void;
357
+ };
348
358
  /**
349
359
  * Destroys the chips container and all contained chips
350
360
  */
@@ -16,11 +16,11 @@ export const LOADING_INDICATOR_SPRING = {
16
16
  };
17
17
  export const LOADING_INDICATOR_SHAPES = [
18
18
  'softBurst',
19
- 'cookie9',
19
+ 'cookie9Sided',
20
20
  'pentagon',
21
21
  'pill',
22
22
  'sunny',
23
- 'cookie4',
23
+ 'cookie4Sided',
24
24
  'oval'
25
25
  ];
26
26
  export const LOADING_INDICATOR_DETERMINATE_SHAPES = ['circle', 'softBurst'];
@@ -1,12 +1,23 @@
1
1
  import { createCanvasContext, updateCanvasDimensions } from "../../../core/canvas/utils.js";
2
2
  import { onThemeChange } from "../../../core/utils/theme.js";
3
- import { materialShape, radialProfile } from "../../../core/shapes/index.js";
3
+ import { shapeCircle, shapeCookie4Sided, shapeCookie9Sided, shapeOval, shapePentagon, shapePill, shapeSoftBurst, shapeSunny, } from "../../../core/shapes/index.js";
4
+ import { sampleProfile } from "../../../core/shapes/material.js";
5
+ const indicatorShapes = {
6
+ circle: shapeCircle,
7
+ oval: shapeOval,
8
+ pentagon: shapePentagon,
9
+ pill: shapePill,
10
+ softBurst: shapeSoftBurst,
11
+ sunny: shapeSunny,
12
+ cookie4Sided: shapeCookie4Sided,
13
+ cookie9Sided: shapeCookie9Sided,
14
+ };
4
15
  import { LOADING_INDICATOR_CLASSES, LOADING_INDICATOR_DEFAULTS } from "../constants.js";
5
16
  const profileCache = new Map();
6
17
  const profileFor = (name) => {
7
18
  let profile = profileCache.get(name);
8
19
  if (!profile) {
9
- profile = radialProfile(materialShape(name), LOADING_INDICATOR_DEFAULTS.SAMPLES);
20
+ profile = sampleProfile((indicatorShapes[name] ?? shapeCircle)(), LOADING_INDICATOR_DEFAULTS.SAMPLES);
10
21
  profileCache.set(name, profile);
11
22
  }
12
23
  return profile;
@@ -62,11 +62,12 @@ export function updateTabPanels(component) {
62
62
  }
63
63
  }
64
64
  }
65
+ const isEnabledTab = (tab) => !tab.element.disabled && tab.element.getAttribute("aria-disabled") !== "true";
65
66
  export function syncTabStops(component) {
66
67
  if (typeof component.getTabs !== "function")
67
68
  return;
68
69
  const tabs = component.getTabs();
69
- const enabled = tabs.filter((tab) => !tab.element.disabled);
70
+ const enabled = tabs.filter(isEnabledTab);
70
71
  const active = getActiveTab(component);
71
72
  const stop = active && enabled.includes(active) ? active : enabled[0];
72
73
  tabs.forEach((tab) => tab.element.setAttribute("tabindex", tab === stop ? "0" : "-1"));
@@ -89,7 +90,7 @@ export function setupKeyboardNavigation(component, options = {}) {
89
90
  return;
90
91
  const tabs = component
91
92
  .getTabs()
92
- .filter((tab) => !tab.element.disabled);
93
+ .filter(isEnabledTab);
93
94
  const currentIndex = tabs.findIndex((tab) => tab.element === tabElement);
94
95
  if (currentIndex === -1)
95
96
  return;
@@ -18,6 +18,8 @@ export declare const straightLine: (x0: number, y0: number, x1: number, y1: numb
18
18
  */
19
19
  export declare const circularArc: (centerX: number, centerY: number, x0: number, y0: number, x1: number, y1: number) => Cubic;
20
20
  export declare const pointOnCurve: (c: Cubic, t: number) => Point;
21
+ /** Splits a cubic at `t` into two that together trace it (de Casteljau) */
22
+ export declare const splitCubic: (c: Cubic, t: number) => [Cubic, Cubic];
21
23
  export declare const reverseCubic: (c: Cubic) => Cubic;
22
24
  export declare const zeroLength: (c: Cubic) => boolean;
23
25
  export type PointTransformer = (x: number, y: number) => Point;
@@ -41,6 +41,20 @@ export const pointOnCurve = (c, t) => {
41
41
  c[1] * (u * u * u) + c[3] * (3 * t * u * u) + c[5] * (3 * t * t * u) + c[7] * (t * t * t),
42
42
  ];
43
43
  };
44
+ export const splitCubic = (c, t) => {
45
+ const u = 1 - t;
46
+ const [x0, y0, x1, y1, x2, y2, x3, y3] = c;
47
+ const ax = u * x0 + t * x1, ay = u * y0 + t * y1;
48
+ const bx = u * x1 + t * x2, by = u * y1 + t * y2;
49
+ const cx = u * x2 + t * x3, cy = u * y2 + t * y3;
50
+ const dx = u * ax + t * bx, dy = u * ay + t * by;
51
+ const ex = u * bx + t * cx, ey = u * by + t * cy;
52
+ const mx = u * dx + t * ex, my = u * dy + t * ey;
53
+ return [
54
+ [x0, y0, ax, ay, dx, dy, mx, my],
55
+ [mx, my, ex, ey, cx, cy, x3, y3],
56
+ ];
57
+ };
44
58
  export const reverseCubic = (c) => [c[6], c[7], c[4], c[5], c[2], c[3], c[0], c[1]];
45
59
  export const zeroLength = (c) => Math.abs(c[0] - c[6]) < DISTANCE_EPSILON && Math.abs(c[1] - c[7]) < DISTANCE_EPSILON;
46
60
  export const transformCubic = (c, f) => {
@@ -1,6 +1,6 @@
1
1
  export type { Cubic, Point, PointTransformer } from "./cubic.js";
2
- export { straightLine, circularArc, pointOnCurve, cubicBounds } from "./cubic.js";
2
+ export { straightLine, circularArc, pointOnCurve, cubicBounds, splitCubic } from "./cubic.js";
3
3
  export type { CornerRounding, RoundedPolygon } from "./polygon.js";
4
- export { UNROUNDED, roundedPolygon, regularPolygon, circle, star, transformPolygon, rotatePolygon, scalePolygon, polygonBounds, polygonMaxRadius, normalizePolygon, } from "./polygon.js";
4
+ export { UNROUNDED, roundedPolygon, rectangle, regularPolygon, circle, star, transformPolygon, rotatePolygon, scalePolygon, polygonBounds, polygonMaxRadius, normalizePolygon, } from "./polygon.js";
5
5
  export type { MaterialShapeName, RadialProfile } from "./material.js";
6
- export { materialShape, radialProfile } from "./material.js";
6
+ export { materialShape, materialShapePath, polygonPath, radialProfile, shapeCircle, shapeSquare, shapeSlanted, shapeArch, shapeFan, shapeArrow, shapeSemiCircle, shapeOval, shapePill, shapeTriangle, shapeDiamond, shapeClamShell, shapePentagon, shapeGem, shapeSunny, shapeVerySunny, shapeCookie4Sided, shapeCookie6Sided, shapeCookie7Sided, shapeCookie9Sided, shapeCookie12Sided, shapeGhostish, shapeClover4Leaf, shapeClover8Leaf, shapeBurst, shapeSoftBurst, shapeBoom, shapeSoftBoom, shapeFlower, shapePuffy, shapePuffyDiamond, shapePixelCircle, shapePixelTriangle, shapeBun, shapeHeart, } from "./material.js";
@@ -1,3 +1,3 @@
1
- export { straightLine, circularArc, pointOnCurve, cubicBounds } from "./cubic.js";
2
- export { UNROUNDED, roundedPolygon, regularPolygon, circle, star, transformPolygon, rotatePolygon, scalePolygon, polygonBounds, polygonMaxRadius, normalizePolygon, } from "./polygon.js";
3
- export { materialShape, radialProfile } from "./material.js";
1
+ export { straightLine, circularArc, pointOnCurve, cubicBounds, splitCubic } from "./cubic.js";
2
+ export { UNROUNDED, roundedPolygon, rectangle, regularPolygon, circle, star, transformPolygon, rotatePolygon, scalePolygon, polygonBounds, polygonMaxRadius, normalizePolygon, } from "./polygon.js";
3
+ export { materialShape, materialShapePath, polygonPath, radialProfile, shapeCircle, shapeSquare, shapeSlanted, shapeArch, shapeFan, shapeArrow, shapeSemiCircle, shapeOval, shapePill, shapeTriangle, shapeDiamond, shapeClamShell, shapePentagon, shapeGem, shapeSunny, shapeVerySunny, shapeCookie4Sided, shapeCookie6Sided, shapeCookie7Sided, shapeCookie9Sided, shapeCookie12Sided, shapeGhostish, shapeClover4Leaf, shapeClover8Leaf, shapeBurst, shapeSoftBurst, shapeBoom, shapeSoftBoom, shapeFlower, shapePuffy, shapePuffyDiamond, shapePixelCircle, shapePixelTriangle, shapeBun, shapeHeart, } from "./material.js";
@@ -1,8 +1,129 @@
1
1
  import { RoundedPolygon, polygonMaxRadius } from "./polygon.js";
2
- /** Names of the Material shapes available here */
3
- export type MaterialShapeName = 'circle' | 'oval' | 'pill' | 'pentagon' | 'sunny' | 'cookie4' | 'cookie9' | 'softBurst';
4
- /** A Material shape, normalised into the unit square */
2
+ /** Circle */
3
+ export declare const shapeCircle: () => RoundedPolygon;
4
+ /** A rounded square */
5
+ export declare const shapeSquare: () => RoundedPolygon;
6
+ /** A slanted square */
7
+ export declare const shapeSlanted: () => RoundedPolygon;
8
+ /** An arch */
9
+ export declare const shapeArch: () => RoundedPolygon;
10
+ /** A fan */
11
+ export declare const shapeFan: () => RoundedPolygon;
12
+ /** An arrow */
13
+ export declare const shapeArrow: () => RoundedPolygon;
14
+ /** A semicircle */
15
+ export declare const shapeSemiCircle: () => RoundedPolygon;
16
+ /** An oval */
17
+ export declare const shapeOval: () => RoundedPolygon;
18
+ /** A pill */
19
+ export declare const shapePill: () => RoundedPolygon;
20
+ /** A triangle */
21
+ export declare const shapeTriangle: () => RoundedPolygon;
22
+ /** A diamond */
23
+ export declare const shapeDiamond: () => RoundedPolygon;
24
+ /** A clam shell */
25
+ export declare const shapeClamShell: () => RoundedPolygon;
26
+ /** A pentagon */
27
+ export declare const shapePentagon: () => RoundedPolygon;
28
+ /** A gem */
29
+ export declare const shapeGem: () => RoundedPolygon;
30
+ /** A sunny shape: an 8-point star */
31
+ export declare const shapeSunny: () => RoundedPolygon;
32
+ /** A very sunny shape */
33
+ export declare const shapeVerySunny: () => RoundedPolygon;
34
+ /** A 4-sided cookie */
35
+ export declare const shapeCookie4Sided: () => RoundedPolygon;
36
+ /** A 6-sided cookie */
37
+ export declare const shapeCookie6Sided: () => RoundedPolygon;
38
+ /** A 7-sided cookie */
39
+ export declare const shapeCookie7Sided: () => RoundedPolygon;
40
+ /** A 9-sided cookie */
41
+ export declare const shapeCookie9Sided: () => RoundedPolygon;
42
+ /** A 12-sided cookie */
43
+ export declare const shapeCookie12Sided: () => RoundedPolygon;
44
+ /** A ghost-ish shape */
45
+ export declare const shapeGhostish: () => RoundedPolygon;
46
+ /** A 4-leaf clover */
47
+ export declare const shapeClover4Leaf: () => RoundedPolygon;
48
+ /** An 8-leaf clover */
49
+ export declare const shapeClover8Leaf: () => RoundedPolygon;
50
+ /** A burst */
51
+ export declare const shapeBurst: () => RoundedPolygon;
52
+ /** A soft burst */
53
+ export declare const shapeSoftBurst: () => RoundedPolygon;
54
+ /** A boom */
55
+ export declare const shapeBoom: () => RoundedPolygon;
56
+ /** A soft boom */
57
+ export declare const shapeSoftBoom: () => RoundedPolygon;
58
+ /** A flower */
59
+ export declare const shapeFlower: () => RoundedPolygon;
60
+ /** A puffy shape */
61
+ export declare const shapePuffy: () => RoundedPolygon;
62
+ /** A puffy diamond */
63
+ export declare const shapePuffyDiamond: () => RoundedPolygon;
64
+ /** A pixelated circle */
65
+ export declare const shapePixelCircle: () => RoundedPolygon;
66
+ /** A pixelated triangle */
67
+ export declare const shapePixelTriangle: () => RoundedPolygon;
68
+ /** A bun */
69
+ export declare const shapeBun: () => RoundedPolygon;
70
+ /** A heart */
71
+ export declare const shapeHeart: () => RoundedPolygon;
72
+ declare const byName: {
73
+ circle: () => RoundedPolygon;
74
+ square: () => RoundedPolygon;
75
+ slanted: () => RoundedPolygon;
76
+ arch: () => RoundedPolygon;
77
+ fan: () => RoundedPolygon;
78
+ arrow: () => RoundedPolygon;
79
+ semiCircle: () => RoundedPolygon;
80
+ oval: () => RoundedPolygon;
81
+ pill: () => RoundedPolygon;
82
+ triangle: () => RoundedPolygon;
83
+ diamond: () => RoundedPolygon;
84
+ clamShell: () => RoundedPolygon;
85
+ pentagon: () => RoundedPolygon;
86
+ gem: () => RoundedPolygon;
87
+ sunny: () => RoundedPolygon;
88
+ verySunny: () => RoundedPolygon;
89
+ cookie4Sided: () => RoundedPolygon;
90
+ cookie6Sided: () => RoundedPolygon;
91
+ cookie7Sided: () => RoundedPolygon;
92
+ cookie9Sided: () => RoundedPolygon;
93
+ cookie12Sided: () => RoundedPolygon;
94
+ ghostish: () => RoundedPolygon;
95
+ clover4Leaf: () => RoundedPolygon;
96
+ clover8Leaf: () => RoundedPolygon;
97
+ burst: () => RoundedPolygon;
98
+ softBurst: () => RoundedPolygon;
99
+ boom: () => RoundedPolygon;
100
+ softBoom: () => RoundedPolygon;
101
+ flower: () => RoundedPolygon;
102
+ puffy: () => RoundedPolygon;
103
+ puffyDiamond: () => RoundedPolygon;
104
+ pixelCircle: () => RoundedPolygon;
105
+ pixelTriangle: () => RoundedPolygon;
106
+ bun: () => RoundedPolygon;
107
+ heart: () => RoundedPolygon;
108
+ /** @deprecated Use 'cookie4Sided' */
109
+ cookie4: () => RoundedPolygon;
110
+ /** @deprecated Use 'cookie9Sided' */
111
+ cookie9: () => RoundedPolygon;
112
+ };
113
+ /**
114
+ * Names of the Material shapes: Compose's, camelCased. 'cookie4' and
115
+ * 'cookie9' are deprecated aliases of 'cookie4Sided' and 'cookie9Sided'.
116
+ */
117
+ export type MaterialShapeName = keyof typeof byName;
118
+ /** A Material shape by name, normalised into the unit square. Carries every shape; import `shapeX` to carry one. */
5
119
  export declare const materialShape: (name: MaterialShapeName) => RoundedPolygon;
120
+ /**
121
+ * A polygon's outline as an SVG path ("M … C … Z"), scaled by `size`: a
122
+ * normalised shape fills a `size` × `size` box. Coordinates keep three decimals.
123
+ */
124
+ export declare const polygonPath: (polygon: RoundedPolygon, size?: number) => string;
125
+ /** A Material shape as an SVG path in a `size` × `size` box. Carries every shape, as materialShape does. */
126
+ export declare const materialShapePath: (name: MaterialShapeName, size?: number) => string;
6
127
  /** A shape's outline as radii at evenly spaced angles around its centre */
7
128
  export interface RadialProfile {
8
129
  /** Radius at angle i * 2π / radii.length, clockwise from the positive x axis on a y-down canvas */
@@ -14,11 +135,20 @@ export interface RadialProfile {
14
135
  maxRadius: number;
15
136
  }
16
137
  /**
17
- * Samples the outline at `samples` angles around the shape's area centroid.
138
+ * radialProfile without its star-shape check, for callers whose shapes are
139
+ * known to be star-shaped: the loading indicator's, which the tests check.
18
140
  * The centroid, not the bounds centre, is the pivot: a pentagon's mass sits
19
141
  * below the middle of its box, and turning it about the box would make it
20
- * orbit. The shape must be star-shaped around its centroid, which every
21
- * Material shape is: each ray from the centre crosses the outline once.
142
+ * orbit.
143
+ * @internal
144
+ */
145
+ export declare const sampleProfile: (polygon: RoundedPolygon, samples?: number, stepsPerCubic?: number) => RadialProfile;
146
+ /**
147
+ * Samples the outline at `samples` angles around the shape's area centroid.
148
+ * Throws for a shape that is not star-shaped around it. Of the Material
149
+ * shapes, 33 are; Puffy and PixelTriangle are not (their outlines double back
150
+ * as seen from the centroid, Puffy by about 0.1°): a ray from the centroid
151
+ * would cross the outline more than once, and the profile would be wrong.
22
152
  */
23
153
  export declare const radialProfile: (polygon: RoundedPolygon, samples?: number, stepsPerCubic?: number) => RadialProfile;
24
154
  export { polygonMaxRadius };
@@ -1,9 +1,33 @@
1
+ // src/core/shapes/material.ts
2
+ //
3
+ // The Material 3 Expressive shapes: all 35 of Compose Material 3's
4
+ // `MaterialShapes`, built as it builds them (vertex lists repeated around a
5
+ // centre, stars, rectangles, circles), normalised into the unit square.
6
+ //
7
+ // Ported from androidx (Apache License 2.0, Copyright The Android Open Source
8
+ // Project): compose/material3/material3/src/commonMain/kotlin/androidx/compose/
9
+ // material3/MaterialShapes.kt at 080d2b3e5326ba80392d93442c4a51a02dc22650, on
10
+ // the geometry of androidx.graphics:graphics-shapes 1.0.1, the version Compose
11
+ // Material 3 depends on. test/fixtures/material-shapes.json holds every shape
12
+ // as Compose builds it (scripts/generate-material-shapes.kt), and the tests
13
+ // hold this port to it.
14
+ //
15
+ // Each shape is its own export, built once on first use, so a bundle carries
16
+ // only the shapes it names. `materialShape(name)` looks any of them up by name,
17
+ // and carries them all.
1
18
  import { pointOnCurve } from "./cubic.js";
2
- import { circle, normalizePolygon, polygonBounds, polygonMaxRadius, rotatePolygon, roundedPolygon, scalePolygon, star, } from "./polygon.js";
19
+ import { circle, normalizePolygon, polygonBounds, polygonMaxRadius, rectangle, regularPolygon, rotatePolygon, roundedPolygon, scalePolygon, star, } from "./polygon.js";
3
20
  const cornerRound15 = { radius: 0.15 };
21
+ const cornerRound20 = { radius: 0.2 };
22
+ const cornerRound30 = { radius: 0.3 };
4
23
  const cornerRound50 = { radius: 0.5 };
5
- const point = (x, y, r = { radius: 0 }) => ({ x, y, r });
24
+ const cornerRound100 = { radius: 1 };
25
+ const point = (x, y, radius = 0, smoothing = 0) => ({ x, y, r: { radius, smoothing } });
6
26
  const toRadians = (degrees) => (degrees / 360) * 2 * Math.PI;
27
+ /**
28
+ * Repeats a few points around the centre, `reps` times, optionally mirroring
29
+ * every other repetition (MaterialShapes.doRepeat)
30
+ */
7
31
  const repeat = (points, reps, cx, cy, mirroring) => {
8
32
  const result = [];
9
33
  if (mirroring) {
@@ -16,7 +40,7 @@ const repeat = (points, reps, cx, cy, mirroring) => {
16
40
  const i = it % 2 === 0 ? index : points.length - 1 - index;
17
41
  if (i > 0 || it % 2 === 0) {
18
42
  const a = toRadians(sectionAngle * it + (it % 2 === 0 ? angles[i] : sectionAngle - angles[i] + 2 * angles[0]));
19
- result.push(point(Math.cos(a) * distances[i] + cx, Math.sin(a) * distances[i] + cy, points[i].r));
43
+ result.push({ x: Math.cos(a) * distances[i] + cx, y: Math.sin(a) * distances[i] + cy, r: points[i].r });
20
44
  }
21
45
  }
22
46
  }
@@ -28,43 +52,184 @@ const repeat = (points, reps, cx, cy, mirroring) => {
28
52
  const a = toRadians(Math.floor(it / np) * (360 / reps));
29
53
  const dx = p.x - cx;
30
54
  const dy = p.y - cy;
31
- result.push(point(dx * Math.cos(a) - dy * Math.sin(a) + cx, dx * Math.sin(a) + dy * Math.cos(a) + cy, p.r));
55
+ result.push({ x: dx * Math.cos(a) - dy * Math.sin(a) + cx, y: dx * Math.sin(a) + dy * Math.cos(a) + cy, r: p.r });
32
56
  }
33
57
  return result;
34
58
  };
35
- const customPolygon = (pnr, reps, mirroring = false, cx = 0.5, cy = 0.5) => {
36
- const points = repeat(pnr, reps, cx, cy, mirroring);
59
+ /** MaterialShapes.customPolygon: points repeated around (0.5, 0.5) */
60
+ const custom = (pnr, reps, mirroring = false) => {
61
+ const points = repeat(pnr, reps, 0.5, 0.5, mirroring);
37
62
  const vertices = [];
38
63
  for (const p of points)
39
64
  vertices.push(p.x, p.y);
40
- return roundedPolygon(vertices, undefined, points.map((p) => p.r), cx, cy);
65
+ return roundedPolygon(vertices, undefined, points.map((p) => p.r), 0.5, 0.5);
41
66
  };
42
- const builders = {
43
- circle: () => circle(10),
44
- oval: () => rotatePolygon(scalePolygon(circle(), 1, 0.64), -45),
45
- pill: () => customPolygon([point(0.961, 0.039, { radius: 0.426 }), point(1.001, 0.428), point(1.0, 0.609, { radius: 1 })], 2, true),
46
- pentagon: () => customPolygon([point(0.5, -0.009, { radius: 0.172 }), point(1.03, 0.365, { radius: 0.164 }), point(0.828, 0.97, { radius: 0.169 })], 1, true),
47
- sunny: () => star(8, 1, 0.8, cornerRound15),
48
- cookie4: () => customPolygon([point(1.237, 1.236, { radius: 0.258 }), point(0.5, 0.918, { radius: 0.233 })], 4),
49
- cookie9: () => rotatePolygon(star(9, 1, 0.8, cornerRound50), -90),
50
- softBurst: () => customPolygon([point(0.193, 0.277, { radius: 0.053 }), point(0.176, 0.055, { radius: 0.053 })], 10),
67
+ /** A shape built once, on first use, and normalised into the unit square */
68
+ const shape = (build) => {
69
+ let built;
70
+ return () => (built ?? (built = normalizePolygon(build())));
51
71
  };
52
- const cache = new Map();
53
- export const materialShape = (name) => {
54
- let shape = cache.get(name);
55
- if (!shape) {
56
- shape = normalizePolygon(builders[name]());
57
- cache.set(name, shape);
58
- }
59
- return shape;
72
+ /** Circle */
73
+ export const shapeCircle = /*#__PURE__*/ shape(() => circle(10));
74
+ /** A rounded square */
75
+ export const shapeSquare = /*#__PURE__*/ shape(() => rectangle(1, 1, cornerRound30));
76
+ /** A slanted square */
77
+ export const shapeSlanted = /*#__PURE__*/ shape(() => custom([point(0.926, 0.97, 0.189, 0.811), point(-0.021, 0.967, 0.187, 0.057)], 2));
78
+ /** An arch */
79
+ export const shapeArch = /*#__PURE__*/ shape(() => rotatePolygon(regularPolygon(4, 1, 0, 0, undefined, [cornerRound100, cornerRound100, cornerRound20, cornerRound20]), -135));
80
+ /** A fan */
81
+ export const shapeFan = /*#__PURE__*/ shape(() => custom([point(1.004, 1.0, 0.148, 0.417), point(0.0, 1.0, 0.151), point(0.0, -0.003, 0.148), point(0.978, 0.02, 0.803)], 1));
82
+ /** An arrow */
83
+ export const shapeArrow = /*#__PURE__*/ shape(() => custom([point(0.5, 0.892, 0.313), point(-0.216, 1.05, 0.207), point(0.499, -0.16, 0.215, 1.0), point(1.225, 1.06, 0.211)], 1));
84
+ /** A semicircle */
85
+ export const shapeSemiCircle = /*#__PURE__*/ shape(() => rectangle(1.6, 1, undefined, [cornerRound20, cornerRound20, cornerRound100, cornerRound100]));
86
+ /** An oval */
87
+ export const shapeOval = /*#__PURE__*/ shape(() => rotatePolygon(scalePolygon(circle(), 1, 0.64), -45));
88
+ /** A pill */
89
+ export const shapePill = /*#__PURE__*/ shape(() => custom([point(0.961, 0.039, 0.426), point(1.001, 0.428), point(1.0, 0.609, 1.0)], 2, true));
90
+ /** A triangle */
91
+ export const shapeTriangle = /*#__PURE__*/ shape(() => rotatePolygon(regularPolygon(3, 1, 0, 0, cornerRound20), -90));
92
+ /** A diamond */
93
+ export const shapeDiamond = /*#__PURE__*/ shape(() => custom([point(0.5, 1.096, 0.151, 0.524), point(0.04, 0.5, 0.159)], 2));
94
+ /** A clam shell */
95
+ export const shapeClamShell = /*#__PURE__*/ shape(() => custom([point(0.171, 0.841, 0.159), point(-0.02, 0.5, 0.14), point(0.17, 0.159, 0.159)], 2));
96
+ /** A pentagon */
97
+ export const shapePentagon = /*#__PURE__*/ shape(() => custom([point(0.5, -0.009, 0.172), point(1.03, 0.365, 0.164), point(0.828, 0.97, 0.169)], 1, true));
98
+ /** A gem */
99
+ export const shapeGem = /*#__PURE__*/ shape(() => custom([point(0.499, 1.023, 0.241, 0.778), point(-0.005, 0.792, 0.208), point(0.073, 0.258, 0.228), point(0.433, -0.0, 0.491)], 1, true));
100
+ /** A sunny shape: an 8-point star */
101
+ export const shapeSunny = /*#__PURE__*/ shape(() => star(8, 1, 0.8, cornerRound15));
102
+ /** A very sunny shape */
103
+ export const shapeVerySunny = /*#__PURE__*/ shape(() => custom([point(0.5, 1.08, 0.085), point(0.358, 0.843, 0.085)], 8));
104
+ /** A 4-sided cookie */
105
+ export const shapeCookie4Sided = /*#__PURE__*/ shape(() => custom([point(1.237, 1.236, 0.258), point(0.5, 0.918, 0.233)], 4));
106
+ /** A 6-sided cookie */
107
+ export const shapeCookie6Sided = /*#__PURE__*/ shape(() => custom([point(0.723, 0.884, 0.394), point(0.5, 1.099, 0.398)], 6));
108
+ /** A 7-sided cookie */
109
+ export const shapeCookie7Sided = /*#__PURE__*/ shape(() => rotatePolygon(star(7, 1, 0.75, cornerRound50), -90));
110
+ /** A 9-sided cookie */
111
+ export const shapeCookie9Sided = /*#__PURE__*/ shape(() => rotatePolygon(star(9, 1, 0.8, cornerRound50), -90));
112
+ /** A 12-sided cookie */
113
+ export const shapeCookie12Sided = /*#__PURE__*/ shape(() => rotatePolygon(star(12, 1, 0.8, cornerRound50), -90));
114
+ /** A ghost-ish shape */
115
+ export const shapeGhostish = /*#__PURE__*/ shape(() => custom([point(0.5, 0, 1.0), point(1, 0, 1.0), point(1, 1.14, 0.254, 0.106), point(0.575, 0.906, 0.253)], 1, true));
116
+ /** A 4-leaf clover */
117
+ export const shapeClover4Leaf = /*#__PURE__*/ shape(() => custom([point(0.5, 0.074), point(0.725, -0.099, 0.476)], 4, true));
118
+ /** An 8-leaf clover */
119
+ export const shapeClover8Leaf = /*#__PURE__*/ shape(() => custom([point(0.5, 0.036), point(0.758, -0.101, 0.209)], 8));
120
+ /** A burst */
121
+ export const shapeBurst = /*#__PURE__*/ shape(() => custom([point(0.5, -0.006, 0.006), point(0.592, 0.158, 0.006)], 12));
122
+ /** A soft burst */
123
+ export const shapeSoftBurst = /*#__PURE__*/ shape(() => custom([point(0.193, 0.277, 0.053), point(0.176, 0.055, 0.053)], 10));
124
+ /** A boom */
125
+ export const shapeBoom = /*#__PURE__*/ shape(() => custom([point(0.457, 0.296, 0.007), point(0.5, -0.051, 0.007)], 15));
126
+ /** A soft boom */
127
+ export const shapeSoftBoom = /*#__PURE__*/ shape(() => custom([point(0.733, 0.454), point(0.839, 0.437, 0.532), point(0.949, 0.449, 0.439, 1.0), point(0.998, 0.478, 0.174)], 16, true));
128
+ /** A flower */
129
+ export const shapeFlower = /*#__PURE__*/ shape(() => custom([point(0.37, 0.187), point(0.416, 0.049, 0.381), point(0.479, 0.001, 0.095)], 8, true));
130
+ /** A puffy shape */
131
+ export const shapePuffy = /*#__PURE__*/ shape(() => scalePolygon(custom([
132
+ point(0.5, 0.053), point(0.545, -0.04, 0.405), point(0.67, -0.035, 0.426), point(0.717, 0.066, 0.574),
133
+ point(0.722, 0.128), point(0.777, 0.002, 0.36), point(0.914, 0.149, 0.66), point(0.926, 0.289, 0.66),
134
+ point(0.881, 0.346), point(0.94, 0.344, 0.126), point(1.003, 0.437, 0.255),
135
+ ], 2, true), 1, 0.742));
136
+ /** A puffy diamond */
137
+ export const shapePuffyDiamond = /*#__PURE__*/ shape(() => custom([point(0.87, 0.13, 0.146), point(0.818, 0.357), point(1.0, 0.332, 0.853)], 4, true));
138
+ /** A pixelated circle */
139
+ export const shapePixelCircle = /*#__PURE__*/ shape(() => custom([
140
+ point(0.5, 0.0), point(0.704, 0.0), point(0.704, 0.065), point(0.843, 0.065),
141
+ point(0.843, 0.148), point(0.926, 0.148), point(0.926, 0.296), point(1.0, 0.296),
142
+ ], 2, true));
143
+ /** A pixelated triangle */
144
+ export const shapePixelTriangle = /*#__PURE__*/ shape(() => custom([
145
+ point(0.11, 0.5), point(0.113, 0.0), point(0.287, 0.0), point(0.287, 0.087), point(0.421, 0.087),
146
+ point(0.421, 0.17), point(0.56, 0.17), point(0.56, 0.265), point(0.674, 0.265), point(0.675, 0.344),
147
+ point(0.789, 0.344), point(0.789, 0.439), point(0.888, 0.439),
148
+ ], 1, true));
149
+ /** A bun */
150
+ export const shapeBun = /*#__PURE__*/ shape(() => custom([point(0.796, 0.5), point(0.853, 0.518, 1), point(0.992, 0.631, 1), point(0.968, 1.0, 1)], 2, true));
151
+ /** A heart */
152
+ export const shapeHeart = /*#__PURE__*/ shape(() => custom([point(0.5, 0.268, 0.016), point(0.792, -0.066, 0.958), point(1.064, 0.276, 1.0), point(0.501, 0.946, 0.129)], 1, true));
153
+ const byName = {
154
+ circle: shapeCircle,
155
+ square: shapeSquare,
156
+ slanted: shapeSlanted,
157
+ arch: shapeArch,
158
+ fan: shapeFan,
159
+ arrow: shapeArrow,
160
+ semiCircle: shapeSemiCircle,
161
+ oval: shapeOval,
162
+ pill: shapePill,
163
+ triangle: shapeTriangle,
164
+ diamond: shapeDiamond,
165
+ clamShell: shapeClamShell,
166
+ pentagon: shapePentagon,
167
+ gem: shapeGem,
168
+ sunny: shapeSunny,
169
+ verySunny: shapeVerySunny,
170
+ cookie4Sided: shapeCookie4Sided,
171
+ cookie6Sided: shapeCookie6Sided,
172
+ cookie7Sided: shapeCookie7Sided,
173
+ cookie9Sided: shapeCookie9Sided,
174
+ cookie12Sided: shapeCookie12Sided,
175
+ ghostish: shapeGhostish,
176
+ clover4Leaf: shapeClover4Leaf,
177
+ clover8Leaf: shapeClover8Leaf,
178
+ burst: shapeBurst,
179
+ softBurst: shapeSoftBurst,
180
+ boom: shapeBoom,
181
+ softBoom: shapeSoftBoom,
182
+ flower: shapeFlower,
183
+ puffy: shapePuffy,
184
+ puffyDiamond: shapePuffyDiamond,
185
+ pixelCircle: shapePixelCircle,
186
+ pixelTriangle: shapePixelTriangle,
187
+ bun: shapeBun,
188
+ heart: shapeHeart,
189
+ /** @deprecated Use 'cookie4Sided' */
190
+ cookie4: shapeCookie4Sided,
191
+ /** @deprecated Use 'cookie9Sided' */
192
+ cookie9: shapeCookie9Sided,
193
+ };
194
+ /** A Material shape by name, normalised into the unit square. Carries every shape; import `shapeX` to carry one. */
195
+ export const materialShape = (name) => byName[name]();
196
+ const num = (value) => {
197
+ const rounded = Math.round(value * 1000) / 1000;
198
+ return String(Object.is(rounded, -0) ? 0 : rounded);
60
199
  };
200
+ /**
201
+ * A polygon's outline as an SVG path ("M … C … Z"), scaled by `size`: a
202
+ * normalised shape fills a `size` × `size` box. Coordinates keep three decimals.
203
+ */
204
+ export const polygonPath = (polygon, size = 1) => {
205
+ const cubics = polygon.cubics;
206
+ if (!cubics.length)
207
+ return '';
208
+ const s = (v) => num(v * size);
209
+ let d = `M${s(cubics[0][0])} ${s(cubics[0][1])}`;
210
+ for (const c of cubics)
211
+ d += `C${s(c[2])} ${s(c[3])} ${s(c[4])} ${s(c[5])} ${s(c[6])} ${s(c[7])}`;
212
+ return `${d}Z`;
213
+ };
214
+ /** A Material shape as an SVG path in a `size` × `size` box. Carries every shape, as materialShape does. */
215
+ export const materialShapePath = (name, size = 1) => polygonPath(materialShape(name), size);
61
216
  const TWO_PI = Math.PI * 2;
62
- export const radialProfile = (polygon, samples = 360, stepsPerCubic = 24) => {
217
+ /**
218
+ * radialProfile without its star-shape check, for callers whose shapes are
219
+ * known to be star-shaped: the loading indicator's, which the tests check.
220
+ * The centroid, not the bounds centre, is the pivot: a pentagon's mass sits
221
+ * below the middle of its box, and turning it about the box would make it
222
+ * orbit.
223
+ * @internal
224
+ */
225
+ export const sampleProfile = (polygon, samples = 360, stepsPerCubic = 24) => {
226
+ // The outline as a dense polygon
63
227
  const points = [];
64
228
  for (const cubic of polygon.cubics) {
65
229
  for (let s = 0; s < stepsPerCubic; s++)
66
230
  points.push(pointOnCurve(cubic, s / stepsPerCubic));
67
231
  }
232
+ // Its area centroid (shoelace formula)
68
233
  let area = 0;
69
234
  let centerX = 0;
70
235
  let centerY = 0;
@@ -85,6 +250,7 @@ export const radialProfile = (polygon, samples = 360, stepsPerCubic = 24) => {
85
250
  centerX = (bounds[0] + bounds[2]) / 2;
86
251
  centerY = (bounds[1] + bounds[3]) / 2;
87
252
  }
253
+ // The outline as (angle, radius) pairs, sorted by angle
88
254
  const outline = [];
89
255
  for (const [x, y] of points) {
90
256
  const dx = x - centerX;
@@ -116,4 +282,39 @@ export const radialProfile = (polygon, samples = 360, stepsPerCubic = 24) => {
116
282
  }
117
283
  return { radii, centerX, centerY, maxRadius };
118
284
  };
285
+ /**
286
+ * Samples the outline at `samples` angles around the shape's area centroid.
287
+ * Throws for a shape that is not star-shaped around it. Of the Material
288
+ * shapes, 33 are; Puffy and PixelTriangle are not (their outlines double back
289
+ * as seen from the centroid, Puffy by about 0.1°): a ray from the centroid
290
+ * would cross the outline more than once, and the profile would be wrong.
291
+ */
292
+ export const radialProfile = (polygon, samples = 360, stepsPerCubic = 24) => {
293
+ const profile = sampleProfile(polygon, samples, stepsPerCubic);
294
+ const { centerX, centerY } = profile;
295
+ const points = [];
296
+ for (const cubic of polygon.cubics) {
297
+ for (let s = 0; s < stepsPerCubic; s++)
298
+ points.push(pointOnCurve(cubic, s / stepsPerCubic));
299
+ }
300
+ // Star-shaped around the centroid: walking the outline, the angle seen from
301
+ // the centroid only ever turns one way. A shape that doubles back (a heart's
302
+ // notch, an arch's inner curve) would be sampled wrong: refuse it.
303
+ let turn = 0;
304
+ for (let i = 0; i < points.length; i++) {
305
+ const [x0, y0] = points[i];
306
+ const [x1, y1] = points[(i + 1) % points.length];
307
+ let step = Math.atan2(y1 - centerY, x1 - centerX) - Math.atan2(y0 - centerY, x0 - centerX);
308
+ if (step > Math.PI)
309
+ step -= TWO_PI;
310
+ else if (step < -Math.PI)
311
+ step += TWO_PI;
312
+ if (turn === 0)
313
+ turn = Math.sign(step);
314
+ else if (step * turn < -1e-9) {
315
+ throw new Error("radialProfile: the shape is not star-shaped around its centroid, so it has no radial profile");
316
+ }
317
+ }
318
+ return profile;
319
+ };
119
320
  export { polygonMaxRadius };
@@ -23,6 +23,8 @@ export declare const regularPolygon: (numVertices: number, radius?: number, cent
23
23
  export declare const circle: (numVertices?: number, radius?: number, centerX?: number, centerY?: number) => RoundedPolygon;
24
24
  /** A star with `numVerticesPerRadius` outer and inner vertices, outer first at angle 0 */
25
25
  export declare const star: (numVerticesPerRadius: number, radius?: number, innerRadius?: number, rounding?: CornerRounding, innerRounding?: CornerRounding, perVertexRounding?: CornerRounding[], centerX?: number, centerY?: number) => RoundedPolygon;
26
+ /** A rectangle centred on (centerX, centerY), its first vertex at the bottom right (graphics-shapes rectangle) */
27
+ export declare const rectangle: (width?: number, height?: number, rounding?: CornerRounding, perVertexRounding?: CornerRounding[], centerX?: number, centerY?: number) => RoundedPolygon;
26
28
  export declare const transformPolygon: (polygon: RoundedPolygon, f: PointTransformer) => RoundedPolygon;
27
29
  /** Rotation about the origin, in degrees, positive clockwise on a y-down canvas */
28
30
  export declare const rotatePolygon: (polygon: RoundedPolygon, degrees: number) => RoundedPolygon;