@dumiflex/wp-kit 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +92 -51
  2. package/LICENSE +674 -674
  3. package/README.md +73 -76
  4. package/package.json +78 -77
  5. package/src/Avatar.vue +83 -83
  6. package/src/Badge.vue +97 -97
  7. package/src/Banner.vue +118 -118
  8. package/src/BookmarkButton.vue +27 -27
  9. package/src/BundleMeta.vue +51 -51
  10. package/src/Button.vue +103 -103
  11. package/src/Card.vue +68 -68
  12. package/src/Checkbox.vue +109 -107
  13. package/src/Chip.vue +163 -97
  14. package/src/ChoiceCards.vue +133 -133
  15. package/src/CloseButton.vue +69 -69
  16. package/src/CodeBlock.vue +146 -119
  17. package/src/ColorField.vue +188 -206
  18. package/src/ColorPicker.vue +201 -201
  19. package/src/Combobox.vue +331 -334
  20. package/src/ConfirmDialog.vue +165 -163
  21. package/src/ConfirmModal.vue +147 -147
  22. package/src/ConstraintMatrixPreview.vue +708 -708
  23. package/src/ContextMenu.vue +386 -379
  24. package/src/CopyButton.vue +156 -156
  25. package/src/DataTable.vue +293 -292
  26. package/src/DatePicker.vue +234 -234
  27. package/src/DescriptionList.vue +73 -73
  28. package/src/DiffView.vue +68 -68
  29. package/src/Disclosure.vue +166 -166
  30. package/src/DragHandle.vue +49 -49
  31. package/src/Drawer.vue +131 -131
  32. package/src/DropZone.vue +196 -196
  33. package/src/DropdownMenu.vue +72 -72
  34. package/src/EmptyState.vue +111 -111
  35. package/src/Field.vue +28 -28
  36. package/src/HsvPicker.vue +208 -208
  37. package/src/Icon.vue +39 -39
  38. package/src/IconPicker.vue +209 -209
  39. package/src/ImageGrid.vue +127 -125
  40. package/src/InlineEdit.vue +183 -183
  41. package/src/Input.vue +99 -99
  42. package/src/Kbd.vue +56 -56
  43. package/src/Lightbox.vue +176 -173
  44. package/src/ListFilter.vue +85 -85
  45. package/src/ListRow.vue +204 -204
  46. package/src/LoadingMark.vue +217 -161
  47. package/src/Logo.vue +60 -60
  48. package/src/Modal.vue +78 -76
  49. package/src/ModalShell.vue +109 -107
  50. package/src/Pagination.vue +106 -106
  51. package/src/Popover.vue +180 -171
  52. package/src/PostCard.vue +210 -211
  53. package/src/ProgressBar.vue +90 -90
  54. package/src/RadioGroup.vue +177 -177
  55. package/src/RangeSlider.vue +183 -183
  56. package/src/RelativeDate.vue +35 -35
  57. package/src/ResizeHandle.vue +143 -143
  58. package/src/SearchInput.vue +192 -199
  59. package/src/Select.vue +644 -641
  60. package/src/Separator.vue +54 -54
  61. package/src/Skeleton.vue +98 -82
  62. package/src/Slider.vue +198 -198
  63. package/src/SortableList.vue +260 -260
  64. package/src/SplitPane.vue +68 -68
  65. package/src/StarButton.vue +40 -40
  66. package/src/Stepper.vue +141 -141
  67. package/src/SupportLinks.vue +93 -93
  68. package/src/TagInput.vue +297 -300
  69. package/src/Textarea.vue +57 -57
  70. package/src/Timeline.vue +92 -92
  71. package/src/ToastCard.vue +119 -119
  72. package/src/ToastHost.vue +40 -40
  73. package/src/Toggle.vue +36 -36
  74. package/src/TokenizedValue.vue +310 -310
  75. package/src/Toolbar.vue +110 -110
  76. package/src/VirtualList.vue +87 -87
  77. package/src/VisuallyHidden.vue +29 -29
  78. package/src/WpButton.vue +75 -75
  79. package/src/WpCheck.vue +104 -102
  80. package/src/WpIconButton.vue +107 -107
  81. package/src/WpImageZoom.vue +544 -544
  82. package/src/WpNumber.vue +178 -183
  83. package/src/WpSegmented.vue +175 -175
  84. package/src/WpSelect.vue +93 -89
  85. package/src/WpSwitch.vue +163 -163
  86. package/src/WpTabs.vue +188 -188
  87. package/src/_modal-motion.css +33 -33
  88. package/src/_skeleton.css +50 -50
  89. package/src/axis-color.ts +42 -42
  90. package/src/button.css +169 -169
  91. package/src/calendar.ts +59 -59
  92. package/src/clipboard.ts +50 -50
  93. package/src/colour-presets.ts +12 -12
  94. package/src/diff.ts +51 -51
  95. package/src/drop-zone.ts +41 -41
  96. package/src/field.css +50 -0
  97. package/src/flip.ts +237 -237
  98. package/src/floating-layer.ts +48 -0
  99. package/src/floating.ts +99 -99
  100. package/src/highlight.ts +199 -0
  101. package/src/hsv.ts +100 -100
  102. package/src/icon-catalog.ts +52 -52
  103. package/src/image-zoom.ts +93 -93
  104. package/src/kind-colour.ts +28 -0
  105. package/src/loading-mark-motion.ts +249 -0
  106. package/src/logo-paths.ts +21 -21
  107. package/src/matrix-axis.ts +91 -91
  108. package/src/pagination.ts +27 -27
  109. package/src/platform-keys.ts +29 -29
  110. package/src/rich-tokenize.ts +357 -357
  111. package/src/select-types.ts +32 -32
  112. package/src/slider-math.ts +45 -45
  113. package/src/sortable.ts +22 -22
  114. package/src/theme/fonts/OFL-Fraunces.txt +93 -93
  115. package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -93
  116. package/src/theme/fonts/OFL-Manrope.txt +93 -93
  117. package/src/theme/looks.css +428 -428
  118. package/src/theme/palettes.css +254 -254
  119. package/src/theme/tokens.css +2896 -2894
  120. package/src/tokens.ts +56 -56
  121. package/src/tooltip.css +43 -43
  122. package/src/tooltip.ts +146 -146
  123. package/src/useDialogFocus.ts +73 -73
  124. package/src/useGrowableField.ts +357 -357
  125. package/src/useModalKeyShield.ts +116 -116
  126. package/src/useToast.ts +48 -48
  127. package/src/wp-logo.svg +14 -14
  128. package/src/ChipToggle.vue +0 -92
  129. package/src/KindChip.vue +0 -53
  130. package/src/RatingBadge.vue +0 -42
@@ -1,52 +1,52 @@
1
- /**
2
- * Curated PrimeIcons glyphs offered by `IconPicker.vue`.
3
- *
4
- * PrimeIcons ships ~314 glyphs; this is a subset. The full set would need its
5
- * own search UI, and most of it (chevrons, spinners, media transport, brand
6
- * logos) means nothing as a library label. The groups below follow the kinds
7
- * of things people actually categorise prompt modules by.
8
- *
9
- * Names are PrimeIcons ids MINUS the `pi-` prefix (`"user"`, not `"pi-user"`)
10
- * — that is what the engine's `icon` column holds and what `Icon.vue` expects.
11
- *
12
- * In a plain module rather than inside the SFC so `icon-catalog.test.ts` can
13
- * check every name against the installed `primeicons.css` without mounting a
14
- * component. That test is the point: a name with no matching glyph renders as
15
- * an EMPTY CELL, which looks like a layout bug rather than a typo, and `brush`
16
- * shipped that way because nothing verified the list.
17
- */
18
- export interface IconGroup {
19
- label: string;
20
- icons: string[];
21
- }
22
-
23
- export const ICON_GROUPS: IconGroup[] = [
24
- {
25
- label: "Subject",
26
- icons: ["user", "users", "heart", "star", "eye", "face-smile", "crown", "id-card"],
27
- },
28
- {
29
- // `sliders-h` stands in for the art-supply glyph PrimeIcons doesn't have —
30
- // there is no brush/paint icon in the set, and `palette` already carries
31
- // "art style". Sliders read as "tune the look".
32
- label: "Style & look",
33
- icons: ["palette", "pencil", "sliders-h", "camera", "image", "sparkles", "sun", "moon"],
34
- },
35
- {
36
- label: "Scene",
37
- icons: ["home", "building", "map", "map-marker", "globe", "compass", "car", "cloud"],
38
- },
39
- {
40
- label: "Structure",
41
- icons: ["box", "folder", "book", "tag", "tags", "list", "sitemap", "objects-column"],
42
- },
43
- {
44
- label: "Signals",
45
- icons: ["bolt", "flag", "bell", "shield", "lock", "wrench", "filter", "info-circle"],
46
- },
47
- ];
48
-
49
- /** Every offered glyph, flattened — for tests and any future search box. */
50
- export function allCuratedIcons(): string[] {
51
- return ICON_GROUPS.flatMap((g) => g.icons);
52
- }
1
+ /**
2
+ * Curated PrimeIcons glyphs offered by `IconPicker.vue`.
3
+ *
4
+ * PrimeIcons ships ~314 glyphs; this is a subset. The full set would need its
5
+ * own search UI, and most of it (chevrons, spinners, media transport, brand
6
+ * logos) means nothing as a library label. The groups below follow the kinds
7
+ * of things people actually categorise prompt modules by.
8
+ *
9
+ * Names are PrimeIcons ids MINUS the `pi-` prefix (`"user"`, not `"pi-user"`)
10
+ * — that is what the engine's `icon` column holds and what `Icon.vue` expects.
11
+ *
12
+ * In a plain module rather than inside the SFC so `icon-catalog.test.ts` can
13
+ * check every name against the installed `primeicons.css` without mounting a
14
+ * component. That test is the point: a name with no matching glyph renders as
15
+ * an EMPTY CELL, which looks like a layout bug rather than a typo, and `brush`
16
+ * shipped that way because nothing verified the list.
17
+ */
18
+ export interface IconGroup {
19
+ label: string;
20
+ icons: string[];
21
+ }
22
+
23
+ export const ICON_GROUPS: IconGroup[] = [
24
+ {
25
+ label: "Subject",
26
+ icons: ["user", "users", "heart", "star", "eye", "face-smile", "crown", "id-card"],
27
+ },
28
+ {
29
+ // `sliders-h` stands in for the art-supply glyph PrimeIcons doesn't have —
30
+ // there is no brush/paint icon in the set, and `palette` already carries
31
+ // "art style". Sliders read as "tune the look".
32
+ label: "Style & look",
33
+ icons: ["palette", "pencil", "sliders-h", "camera", "image", "sparkles", "sun", "moon"],
34
+ },
35
+ {
36
+ label: "Scene",
37
+ icons: ["home", "building", "map", "map-marker", "globe", "compass", "car", "cloud"],
38
+ },
39
+ {
40
+ label: "Structure",
41
+ icons: ["box", "folder", "book", "tag", "tags", "list", "sitemap", "objects-column"],
42
+ },
43
+ {
44
+ label: "Signals",
45
+ icons: ["bolt", "flag", "bell", "shield", "lock", "wrench", "filter", "info-circle"],
46
+ },
47
+ ];
48
+
49
+ /** Every offered glyph, flattened — for tests and any future search box. */
50
+ export function allCuratedIcons(): string[] {
51
+ return ICON_GROUPS.flatMap((g) => g.icons);
52
+ }
package/src/image-zoom.ts CHANGED
@@ -1,93 +1,93 @@
1
- /**
2
- * The maths behind `WpImageZoom`: one transform, `translate(x, y) scale(s)`
3
- * with its origin at the image's top-left corner. Pure, so it is tested on
4
- * its own.
5
- */
6
-
7
- export interface Size { width: number; height: number }
8
- export interface View { scale: number; x: number; y: number }
9
-
10
- /** Zoom never goes below this share of Fit (or of 100% for a tiny image), nor above MAX_SCALE. */
11
- export const MAX_SCALE = 8;
12
- /** The steps the − / + buttons jump through, as real-size factors. */
13
- export const ZOOM_STEPS = [0.1, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4, 6, 8] as const;
14
- /** How long `WpImageZoom`'s bar stays after the last pointer move or key, with `autohide`. */
15
- export const IDLE_MS = 2000;
16
- /** One wheel notch. */
17
- export const WHEEL_FACTOR = 1.2;
18
-
19
- /** The scale that shows the whole image in the stage (it may be above 1 for a small image). */
20
- export function fitScale(image: Size, stage: Size): number {
21
- if (image.width <= 0 || image.height <= 0 || stage.width <= 0 || stage.height <= 0) return 1;
22
- return Math.min(stage.width / image.width, stage.height / image.height);
23
- }
24
-
25
- export function minScale(image: Size, stage: Size): number {
26
- return Math.min(fitScale(image, stage), 1) / 2;
27
- }
28
-
29
- export function clampScale(scale: number, image: Size, stage: Size): number {
30
- return Math.min(MAX_SCALE, Math.max(minScale(image, stage), scale));
31
- }
32
-
33
- /** Keep the image in view: centred on an axis where it is smaller than the
34
- * stage, otherwise never showing an empty edge. */
35
- export function clampView(view: View, image: Size, stage: Size): View {
36
- const axis = (pos: number, img: number, room: number): number => {
37
- const size = img * view.scale;
38
- if (size <= room) return (room - size) / 2;
39
- return Math.min(0, Math.max(room - size, pos));
40
- };
41
- return { scale: view.scale, x: axis(view.x, image.width, stage.width), y: axis(view.y, image.height, stage.height) };
42
- }
43
-
44
- /** Set the scale, keeping the image point under `at` (stage px) where it is. */
45
- export function zoomAt(view: View, scale: number, at: { x: number; y: number }, image: Size, stage: Size): View {
46
- const s = clampScale(scale, image, stage);
47
- const k = s / view.scale;
48
- return clampView({ scale: s, x: at.x - (at.x - view.x) * k, y: at.y - (at.y - view.y) * k }, image, stage);
49
- }
50
-
51
- /** The view at `scale` centred on the image point `centre` (fractions 0..1). */
52
- export function viewAround(scale: number, centre: { x: number; y: number }, image: Size, stage: Size): View {
53
- const s = clampScale(scale, image, stage);
54
- return clampView({
55
- scale: s,
56
- x: stage.width / 2 - centre.x * image.width * s,
57
- y: stage.height / 2 - centre.y * image.height * s,
58
- }, image, stage);
59
- }
60
-
61
- /** The image point (fractions 0..1) at the middle of the stage. */
62
- export function centreOf(view: View, image: Size, stage: Size): { x: number; y: number } {
63
- if (image.width <= 0 || image.height <= 0) return { x: 0.5, y: 0.5 };
64
- const f = (mid: number, pos: number, img: number): number => Math.min(1, Math.max(0, (mid - pos) / (img * view.scale)));
65
- return { x: f(stage.width / 2, view.x, image.width), y: f(stage.height / 2, view.y, image.height) };
66
- }
67
-
68
- /** The next step up (dir 1) or down (dir -1) from `scale`, with Fit slotted in among the steps. */
69
- export function stepScale(scale: number, dir: 1 | -1, fit: number): number {
70
- const steps = [...ZOOM_STEPS, fit].sort((a, b) => a - b);
71
- const eps = 1e-3;
72
- if (dir > 0) return steps.find((s) => s > scale + eps) ?? MAX_SCALE;
73
- return [...steps].reverse().find((s) => s < scale - eps) ?? steps[0];
74
- }
75
-
76
- /** "100%", "35%", "250%". */
77
- export function percent(scale: number): string {
78
- return `${Math.round(scale * 100)}%`;
79
- }
80
-
81
- /** The part of the image the stage shows, as fractions 0..1 of the image (for a mini-map). */
82
- export function visibleRect(view: View, image: Size, stage: Size): { x: number; y: number; w: number; h: number } {
83
- if (image.width <= 0 || image.height <= 0) return { x: 0, y: 0, w: 1, h: 1 };
84
- const axis = (pos: number, img: number, room: number): [number, number] => {
85
- const size = img * view.scale;
86
- const a = Math.min(1, Math.max(0, -pos / size));
87
- const b = Math.min(1, Math.max(0, (room - pos) / size));
88
- return [a, b - a];
89
- };
90
- const [x, w] = axis(view.x, image.width, stage.width);
91
- const [y, h] = axis(view.y, image.height, stage.height);
92
- return { x, y, w, h };
93
- }
1
+ /**
2
+ * The maths behind `WpImageZoom`: one transform, `translate(x, y) scale(s)`
3
+ * with its origin at the image's top-left corner. Pure, so it is tested on
4
+ * its own.
5
+ */
6
+
7
+ export interface Size { width: number; height: number }
8
+ export interface View { scale: number; x: number; y: number }
9
+
10
+ /** Zoom never goes below this share of Fit (or of 100% for a tiny image), nor above MAX_SCALE. */
11
+ export const MAX_SCALE = 8;
12
+ /** The steps the − / + buttons jump through, as real-size factors. */
13
+ export const ZOOM_STEPS = [0.1, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4, 6, 8] as const;
14
+ /** How long `WpImageZoom`'s bar stays after the last pointer move or key, with `autohide`. */
15
+ export const IDLE_MS = 2000;
16
+ /** One wheel notch. */
17
+ export const WHEEL_FACTOR = 1.2;
18
+
19
+ /** The scale that shows the whole image in the stage (it may be above 1 for a small image). */
20
+ export function fitScale(image: Size, stage: Size): number {
21
+ if (image.width <= 0 || image.height <= 0 || stage.width <= 0 || stage.height <= 0) return 1;
22
+ return Math.min(stage.width / image.width, stage.height / image.height);
23
+ }
24
+
25
+ export function minScale(image: Size, stage: Size): number {
26
+ return Math.min(fitScale(image, stage), 1) / 2;
27
+ }
28
+
29
+ export function clampScale(scale: number, image: Size, stage: Size): number {
30
+ return Math.min(MAX_SCALE, Math.max(minScale(image, stage), scale));
31
+ }
32
+
33
+ /** Keep the image in view: centred on an axis where it is smaller than the
34
+ * stage, otherwise never showing an empty edge. */
35
+ export function clampView(view: View, image: Size, stage: Size): View {
36
+ const axis = (pos: number, img: number, room: number): number => {
37
+ const size = img * view.scale;
38
+ if (size <= room) return (room - size) / 2;
39
+ return Math.min(0, Math.max(room - size, pos));
40
+ };
41
+ return { scale: view.scale, x: axis(view.x, image.width, stage.width), y: axis(view.y, image.height, stage.height) };
42
+ }
43
+
44
+ /** Set the scale, keeping the image point under `at` (stage px) where it is. */
45
+ export function zoomAt(view: View, scale: number, at: { x: number; y: number }, image: Size, stage: Size): View {
46
+ const s = clampScale(scale, image, stage);
47
+ const k = s / view.scale;
48
+ return clampView({ scale: s, x: at.x - (at.x - view.x) * k, y: at.y - (at.y - view.y) * k }, image, stage);
49
+ }
50
+
51
+ /** The view at `scale` centred on the image point `centre` (fractions 0..1). */
52
+ export function viewAround(scale: number, centre: { x: number; y: number }, image: Size, stage: Size): View {
53
+ const s = clampScale(scale, image, stage);
54
+ return clampView({
55
+ scale: s,
56
+ x: stage.width / 2 - centre.x * image.width * s,
57
+ y: stage.height / 2 - centre.y * image.height * s,
58
+ }, image, stage);
59
+ }
60
+
61
+ /** The image point (fractions 0..1) at the middle of the stage. */
62
+ export function centreOf(view: View, image: Size, stage: Size): { x: number; y: number } {
63
+ if (image.width <= 0 || image.height <= 0) return { x: 0.5, y: 0.5 };
64
+ const f = (mid: number, pos: number, img: number): number => Math.min(1, Math.max(0, (mid - pos) / (img * view.scale)));
65
+ return { x: f(stage.width / 2, view.x, image.width), y: f(stage.height / 2, view.y, image.height) };
66
+ }
67
+
68
+ /** The next step up (dir 1) or down (dir -1) from `scale`, with Fit slotted in among the steps. */
69
+ export function stepScale(scale: number, dir: 1 | -1, fit: number): number {
70
+ const steps = [...ZOOM_STEPS, fit].sort((a, b) => a - b);
71
+ const eps = 1e-3;
72
+ if (dir > 0) return steps.find((s) => s > scale + eps) ?? MAX_SCALE;
73
+ return [...steps].reverse().find((s) => s < scale - eps) ?? steps[0];
74
+ }
75
+
76
+ /** "100%", "35%", "250%". */
77
+ export function percent(scale: number): string {
78
+ return `${Math.round(scale * 100)}%`;
79
+ }
80
+
81
+ /** The part of the image the stage shows, as fractions 0..1 of the image (for a mini-map). */
82
+ export function visibleRect(view: View, image: Size, stage: Size): { x: number; y: number; w: number; h: number } {
83
+ if (image.width <= 0 || image.height <= 0) return { x: 0, y: 0, w: 1, h: 1 };
84
+ const axis = (pos: number, img: number, room: number): [number, number] => {
85
+ const size = img * view.scale;
86
+ const a = Math.min(1, Math.max(0, -pos / size));
87
+ const b = Math.min(1, Math.max(0, (room - pos) / size));
88
+ return [a, b - a];
89
+ };
90
+ const [x, w] = axis(view.x, image.width, stage.width);
91
+ const [y, h] = axis(view.y, image.height, stage.height);
92
+ return { x, y, w, h };
93
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The colour of each module type, for a Chip (or anything else) that names
3
+ * one: the `--wp-kind-*` colour the extension uses for that type, bundles
4
+ * slate. There is no kind chip: a kind is a Chip with this colour.
5
+ *
6
+ * <Chip :color="kindColour(itemType(post.kind, post.subtype))">{{ itemType(post.kind, post.subtype) }}</Chip>
7
+ */
8
+ export type ModuleSubtype = "fixed_values" | "combine" | "wildcard" | "constraint" | "derivation";
9
+ export type ItemType = ModuleSubtype | "bundle";
10
+
11
+ const COLOURS: Record<ItemType, string> = {
12
+ fixed_values: "var(--wp-kind-fixed)",
13
+ combine: "var(--wp-kind-combine)",
14
+ wildcard: "var(--wp-kind-wildcard)",
15
+ constraint: "var(--wp-kind-constraint)",
16
+ derivation: "var(--wp-kind-derivation)",
17
+ bundle: "var(--wp-bundle-default)",
18
+ };
19
+
20
+ /** A post or library item's type. A module with no subtype reads as
21
+ * `fixed_values` (the oldest type); a `"bundle"` subtype reads as a bundle. */
22
+ export function itemType(kind: "module" | "bundle", subtype?: ItemType | null): ItemType {
23
+ return kind === "bundle" ? "bundle" : (subtype ?? "fixed_values");
24
+ }
25
+
26
+ export function kindColour(type: ItemType): string {
27
+ return COLOURS[type];
28
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * LoadingMark's motion, drawn on a canvas.
3
+ *
4
+ * The mark used to move its SVG paths with CSS. A keyframed `transform` /
5
+ * `opacity` goes to the compositor, which blurs the path and snaps it to whole
6
+ * pixels as it starts; animating registered custom properties fixed that in
7
+ * Chromium, but Firefox still lays each frame of an SVG transform on the pixel
8
+ * grid, so the rings stepped and twitched as they slid. A canvas puts every
9
+ * shape at its exact fractional position each frame, sharp and smooth in both.
10
+ *
11
+ * One `requestAnimationFrame` loop draws every mark on the page. Each mark
12
+ * keeps its own clock, so a host can slow or pause the marks inside it with
13
+ * `data-wp-motion-speed` (a number) and `data-wp-motion-paused` on an
14
+ * ancestor, as the kit site's Animations section does.
15
+ */
16
+ import { RING_A, RING_B, STAR_A, STAR_B, STAR_BIG } from "./logo-paths";
17
+
18
+ /** One loop of the logo, in ms; the sparkle's swap takes two. */
19
+ export const LOOP_MS = 2400;
20
+ /** Room around the 1024 box for the rings as they slide apart (70 units)
21
+ * and for anti-aliasing at the edge. */
22
+ export const PAD = 82;
23
+ export const BOX = 1024 + 2 * PAD;
24
+
25
+ /** CSS `cubic-bezier(x1, y1, x2, y2)` as a function of progress. */
26
+ export function cubicBezier(x1: number, y1: number, x2: number, y2: number): (x: number) => number {
27
+ return (x) => {
28
+ if (x <= 0) return 0;
29
+ if (x >= 1) return 1;
30
+ let lo = 0;
31
+ let hi = 1;
32
+ let t = x;
33
+ for (let k = 0; k < 30; k++) {
34
+ t = (lo + hi) / 2;
35
+ const cx = 3 * x1 * t * (1 - t) ** 2 + 3 * x2 * t * t * (1 - t) + t ** 3;
36
+ if (cx < x) lo = t;
37
+ else hi = t;
38
+ }
39
+ return 3 * y1 * t * (1 - t) ** 2 + 3 * y2 * t * t * (1 - t) + t ** 3;
40
+ };
41
+ }
42
+
43
+ const SMOOTH = cubicBezier(0.65, 0, 0.35, 1);
44
+ const EASE_IN_OUT = cubicBezier(0.42, 0, 0.58, 1);
45
+
46
+ type Stop = readonly [at: number, values: readonly number[]];
47
+
48
+ /** Value of a keyframe track at progress `p` (0..1), eased per segment the
49
+ * way CSS eases between two keyframes. */
50
+ export function track(stops: readonly Stop[], p: number, ease: (x: number) => number): number[] {
51
+ for (let k = 0; k < stops.length - 1; k++) {
52
+ const a = stops[k];
53
+ const b = stops[k + 1];
54
+ if (!a || !b) break;
55
+ if (p >= a[0] && p <= b[0]) {
56
+ if (b[0] === a[0]) return [...b[1]];
57
+ const e = ease((p - a[0]) / (b[0] - a[0]));
58
+ return a[1].map((v, j) => v + ((b[1][j] ?? v) - v) * e);
59
+ }
60
+ }
61
+ return [...(stops[stops.length - 1]?.[1] ?? [])];
62
+ }
63
+
64
+ /** The rings' slide apart (units of the 1024 box). */
65
+ const SPLIT: readonly Stop[] = [[0, [0]], [0.15, [0]], [0.4, [70]], [0.6, [70]], [0.85, [0]], [1, [0]]];
66
+ /** The big star's dip: [scale, turn in degrees]. */
67
+ const DIP: readonly Stop[] = [[0, [1, 0]], [0.15, [1, 0]], [0.32, [0.7, 45]], [0.5, [1, 90]], [1, [1, 90]]];
68
+ /** The small stars' twinkle: [opacity, scale]. */
69
+ const TWINKLE: readonly Stop[] = [[0, [1, 1]], [0.5, [0.35, 0.6]], [1, [1, 1]]];
70
+ /** The sparkle's small stars trading places (degrees, over two loops). */
71
+ const SWAP: readonly Stop[] = [[0, [0]], [0.15, [0]], [0.5, [180]], [0.65, [180]], [1, [360]]];
72
+
73
+ export interface Pose {
74
+ split: number;
75
+ bigScale: number;
76
+ bigTurn: number;
77
+ twinkleAlpha: number;
78
+ twinkleScale: number;
79
+ swap: number;
80
+ }
81
+
82
+ /** Where every part is `ms` into the animation. */
83
+ export function poseAt(ms: number): Pose {
84
+ const p = (((ms % LOOP_MS) + LOOP_MS) % LOOP_MS) / LOOP_MS;
85
+ const p2 = (((ms % (2 * LOOP_MS)) + 2 * LOOP_MS) % (2 * LOOP_MS)) / (2 * LOOP_MS);
86
+ const [split = 0] = track(SPLIT, p, SMOOTH);
87
+ const [bigScale = 1, bigTurn = 0] = track(DIP, p, SMOOTH);
88
+ const [twinkleAlpha = 1, twinkleScale = 1] = track(TWINKLE, p, EASE_IN_OUT);
89
+ const [swap = 0] = track(SWAP, p2, SMOOTH);
90
+ return { split, bigScale, bigTurn, twinkleAlpha, twinkleScale, swap };
91
+ }
92
+
93
+ /** Centres of the stars in the 1024 box (from their bounding boxes). */
94
+ const C_BIG = [512, 512] as const;
95
+ const C_A = [360, 664] as const;
96
+ const C_B = [664, 360] as const;
97
+
98
+ let paths: Record<"ra" | "rb" | "big" | "a" | "b", Path2D> | null = null;
99
+ function shapes(): Record<"ra" | "rb" | "big" | "a" | "b", Path2D> {
100
+ paths ??= { ra: new Path2D(RING_A), rb: new Path2D(RING_B), big: new Path2D(STAR_BIG), a: new Path2D(STAR_A), b: new Path2D(STAR_B) };
101
+ return paths;
102
+ }
103
+
104
+ export interface Look {
105
+ variant: "logo" | "sparkle";
106
+ /** [violet, indigo, teal], or three times the text colour for `mono`. */
107
+ colours: readonly [string, string, string];
108
+ /** Flat colours (mono): no ring gradients. */
109
+ flat: boolean;
110
+ }
111
+
112
+ function around(ctx: CanvasRenderingContext2D, [x, y]: readonly [number, number], scale: number, turn: number, path: Path2D, fill: string, alpha: number): void {
113
+ ctx.save();
114
+ ctx.translate(x, y);
115
+ ctx.rotate((turn * Math.PI) / 180);
116
+ ctx.scale(scale, scale);
117
+ ctx.translate(-x, -y);
118
+ ctx.globalAlpha = alpha;
119
+ ctx.fillStyle = fill;
120
+ ctx.fill(path);
121
+ ctx.restore();
122
+ }
123
+
124
+ /** Draws one frame on a canvas `BOX` units square (scaled to its pixels). */
125
+ export function drawPose(ctx: CanvasRenderingContext2D, width: number, look: Look, pose: Pose): void {
126
+ const k = width / BOX;
127
+ const [violet, indigo, teal] = look.colours;
128
+ const p = shapes();
129
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
130
+ ctx.clearRect(0, 0, width, width);
131
+ ctx.setTransform(k, 0, 0, k, PAD * k, PAD * k);
132
+ if (look.variant === "logo") {
133
+ const ringA = (() => {
134
+ if (look.flat) return violet;
135
+ const g = ctx.createLinearGradient(312.56, 897.19, 312.56, 2);
136
+ g.addColorStop(0, violet);
137
+ g.addColorStop(1, indigo);
138
+ return g;
139
+ })();
140
+ const ringB = (() => {
141
+ if (look.flat) return violet;
142
+ const g = ctx.createLinearGradient(711.44, 126.81, 711.44, 1022);
143
+ g.addColorStop(0, indigo);
144
+ g.addColorStop(1, teal);
145
+ return g;
146
+ })();
147
+ ctx.save();
148
+ ctx.translate(-pose.split, -pose.split);
149
+ ctx.fillStyle = ringA;
150
+ ctx.fill(p.ra);
151
+ ctx.restore();
152
+ ctx.save();
153
+ ctx.translate(pose.split, pose.split);
154
+ ctx.fillStyle = ringB;
155
+ ctx.fill(p.rb);
156
+ ctx.restore();
157
+ around(ctx, C_BIG, pose.bigScale, pose.bigTurn, p.big, violet, 1);
158
+ around(ctx, C_A, pose.twinkleScale, 0, p.a, indigo, pose.twinkleAlpha);
159
+ around(ctx, C_B, pose.twinkleScale, 0, p.b, teal, pose.twinkleAlpha);
160
+ return;
161
+ }
162
+ ctx.translate(512, 512);
163
+ ctx.scale(1.45, 1.45);
164
+ ctx.translate(-512, -512);
165
+ around(ctx, C_BIG, pose.bigScale, pose.bigTurn, p.big, violet, 1);
166
+ ctx.save();
167
+ ctx.translate(512, 512);
168
+ ctx.rotate((pose.swap * Math.PI) / 180);
169
+ ctx.translate(-512, -512);
170
+ ctx.fillStyle = indigo;
171
+ ctx.fill(p.a);
172
+ ctx.fillStyle = teal;
173
+ ctx.fill(p.b);
174
+ ctx.restore();
175
+ }
176
+
177
+ /** A mark the loop draws. */
178
+ export interface Animated {
179
+ canvas: HTMLCanvasElement;
180
+ look: () => Look;
181
+ /** ms of animation shown so far, advanced by the loop. */
182
+ clock: number;
183
+ /** False while off screen: the clock still runs, nothing is drawn. */
184
+ visible: boolean;
185
+ }
186
+
187
+ const marks = new Set<Animated>();
188
+ let frame = 0;
189
+ let last = 0;
190
+
191
+ /** Speed and pause from the nearest host that sets them. */
192
+ export function motionOf(el: Element): { speed: number; paused: boolean } {
193
+ const host = el.closest("[data-wp-motion-speed], [data-wp-motion-paused]");
194
+ if (!host) return { speed: 1, paused: false };
195
+ const paused = host.closest("[data-wp-motion-paused]") !== null;
196
+ const speedHost = host.closest("[data-wp-motion-speed]");
197
+ const speed = Number(speedHost?.getAttribute("data-wp-motion-speed") ?? "1");
198
+ return { speed: Number.isFinite(speed) && speed >= 0 ? speed : 1, paused };
199
+ }
200
+
201
+ function fit(c: HTMLCanvasElement): void {
202
+ const css = c.clientWidth;
203
+ const want = Math.max(1, Math.round(css * (window.devicePixelRatio || 1)));
204
+ if (c.width !== want) {
205
+ c.width = want;
206
+ c.height = want;
207
+ }
208
+ }
209
+
210
+ function tick(now: number): void {
211
+ const dt = last ? Math.min(now - last, 100) : 0;
212
+ last = now;
213
+ for (const m of marks) {
214
+ const { speed, paused } = motionOf(m.canvas);
215
+ if (!paused) m.clock += dt * speed;
216
+ if (!m.visible) continue;
217
+ const ctx = m.canvas.getContext("2d");
218
+ if (!ctx) continue;
219
+ fit(m.canvas);
220
+ drawPose(ctx, m.canvas.width, m.look(), poseAt(m.clock));
221
+ }
222
+ frame = marks.size ? requestAnimationFrame(tick) : 0;
223
+ }
224
+
225
+ /** Starts drawing a mark; returns the function that stops it. */
226
+ export function animate(mark: Animated): () => void {
227
+ marks.add(mark);
228
+ if (!frame) {
229
+ last = 0;
230
+ frame = requestAnimationFrame(tick);
231
+ }
232
+ return () => {
233
+ marks.delete(mark);
234
+ if (!marks.size && frame) {
235
+ cancelAnimationFrame(frame);
236
+ frame = 0;
237
+ }
238
+ };
239
+ }
240
+
241
+ /** Draws one still frame right away (the first frame, before the loop runs,
242
+ * so the mark never shows blank). */
243
+ export function drawNow(mark: Animated): boolean {
244
+ const ctx = mark.canvas.getContext("2d");
245
+ if (!ctx) return false;
246
+ fit(mark.canvas);
247
+ drawPose(ctx, mark.canvas.width, mark.look(), poseAt(mark.clock));
248
+ return true;
249
+ }
package/src/logo-paths.ts CHANGED
@@ -1,21 +1,21 @@
1
- /** The Wildcard Pipeline logo, split into its parts so a drawing can move them
2
- * on their own (viewBox 0 0 1024 1024). Same shapes as `wp-logo.svg`. */
3
- export const RING_A =
4
- "m512,2c-53.6,0-98.47,38.16-108.84,88.73-75.07,19.29-143.32,58.26-198.74,113.68-55.42,55.42-94.39,123.68-113.68,198.74-50.58,10.38-88.73,55.24-88.73,108.84s38.16,98.47,88.73,108.84c13.62,53.14,37.03,102.64,69.77,147.47,37.14,50.86,85.83,93.86,140.81,124.33,5.54,3.08,11.56,4.54,17.48,4.54,12.7,0,25.02-6.72,31.61-18.61,9.68-17.44,3.38-39.42-14.07-49.09-85.32-47.32-147.22-125.71-173.38-218.21,36.3-18.29,61.27-55.92,61.27-99.28s-24.94-80.97-61.23-99.27c34.22-119.96,129.79-215.52,249.74-249.74,18.3,36.29,55.92,61.23,99.27,61.23,61.27,0,111.11-49.84,111.11-111.11S573.27,2,512,2ZM113.11,473.11c21.44,0,38.89,17.44,38.89,38.89,0,20.1-15.32,36.69-34.91,38.69h-.01c-.18.02-.36.03-.53.04-1.13.1-2.28.16-3.43.16-21.44,0-38.89-17.44-38.89-38.89s17.44-38.89,38.89-38.89Zm398.89-321.11c-21.44,0-38.89-17.44-38.89-38.89s17.44-38.89,38.89-38.89,38.89,17.44,38.89,38.89-17.44,38.89-38.89,38.89Z";
5
- export const RING_B =
6
- "m933.27,403.16c-13.62-53.14-37.03-102.64-69.77-147.47-37.14-50.86-85.83-93.86-140.81-124.33-17.44-9.68-39.42-3.38-49.09,14.07-9.68,17.44-3.38,39.42,14.07,49.09,85.32,47.32,147.22,125.71,173.38,218.21-36.3,18.29-61.27,55.92-61.27,99.28s24.94,80.97,61.23,99.27c-34.22,119.96-129.79,215.52-249.74,249.74-18.3-36.29-55.92-61.23-99.27-61.23-61.27,0-111.11,49.84-111.11,111.11s49.84,111.11,111.11,111.11c53.6,0,98.47-38.16,108.84-88.73,75.07-19.29,143.32-58.26,198.74-113.68,55.42-55.42,94.39-123.68,113.68-198.74,50.58-10.38,88.73-55.24,88.73-108.84s-38.16-98.47-88.73-108.84Zm-421.27,546.62c-21.44,0-38.89-17.44-38.89-38.89s17.44-38.89,38.89-38.89,38.89,17.44,38.89,38.89-17.44,38.89-38.89,38.89Zm398.89-398.89c-21.44,0-38.89-17.44-38.89-38.89,0-20.03,15.22-36.58,34.72-38.67.38-.04.77-.08,1.16-.11h.02c.99-.08,1.99-.11,2.99-.11,21.44,0,38.89,17.44,38.89,38.89s-17.44,38.89-38.89,38.89Z";
7
- export const STAR_BIG =
8
- "m735.02,517.56c-153.82,30.87-186.59,63.64-217.46,217.46-1.22,6.08-9.9,6.08-11.12,0-30.87-153.82-63.64-186.59-217.46-217.46-6.08-1.22-6.08-9.9,0-11.12,153.82-30.87,186.59-63.64,217.46-217.46,1.22-6.08,9.9-6.08,11.12,0,30.87,153.82,63.64,186.59,217.46,217.46,6.08,1.22,6.08,9.9,0,11.12Z";
9
- export const STAR_A =
10
- "m434.06,665.85c-51.08,10.25-61.96,21.13-72.22,72.22-.41,2.02-3.29,2.02-3.69,0-10.25-51.08-21.13-61.96-72.22-72.22-2.02-.41-2.02-3.29,0-3.69,51.08-10.25,61.96-21.13,72.22-72.22.41-2.02,3.29-2.02,3.69,0,10.25,51.08,21.13,61.96,72.22,72.22,2.02.41,2.02,3.29,0,3.69Z";
11
- export const STAR_B =
12
- "m738.07,361.84c-51.08,10.25-61.96,21.13-72.22,72.22-.41,2.02-3.29,2.02-3.69,0-10.25-51.08-21.13-61.96-72.22-72.22-2.02-.41-2.02-3.29,0-3.69,51.08-10.25,61.96-21.13,72.22-72.22.41-2.02,3.29-2.02,3.69,0,10.25,51.08,21.13,61.96,72.22,72.22,2.02.41,2.02,3.29,0,3.69Z";
13
-
14
- /** The logo's colours: [big star and first ring start, middle, small star and
15
- * second ring end]. Brand colours, not theme tokens: the mark looks the same
16
- * in light and dark. `community` is the community site's rust counterpart. */
17
- export const BRAND_COLOURS = {
18
- extension: ["#a970ff", "#6366f1", "#33d6c6"],
19
- community: ["#da7550", "#f1636c", "#ad33d6"],
20
- } as const;
21
- export type LogoBrand = keyof typeof BRAND_COLOURS;
1
+ /** The Wildcard Pipeline logo, split into its parts so a drawing can move them
2
+ * on their own (viewBox 0 0 1024 1024). Same shapes as `wp-logo.svg`. */
3
+ export const RING_A =
4
+ "m512,2c-53.6,0-98.47,38.16-108.84,88.73-75.07,19.29-143.32,58.26-198.74,113.68-55.42,55.42-94.39,123.68-113.68,198.74-50.58,10.38-88.73,55.24-88.73,108.84s38.16,98.47,88.73,108.84c13.62,53.14,37.03,102.64,69.77,147.47,37.14,50.86,85.83,93.86,140.81,124.33,5.54,3.08,11.56,4.54,17.48,4.54,12.7,0,25.02-6.72,31.61-18.61,9.68-17.44,3.38-39.42-14.07-49.09-85.32-47.32-147.22-125.71-173.38-218.21,36.3-18.29,61.27-55.92,61.27-99.28s-24.94-80.97-61.23-99.27c34.22-119.96,129.79-215.52,249.74-249.74,18.3,36.29,55.92,61.23,99.27,61.23,61.27,0,111.11-49.84,111.11-111.11S573.27,2,512,2ZM113.11,473.11c21.44,0,38.89,17.44,38.89,38.89,0,20.1-15.32,36.69-34.91,38.69h-.01c-.18.02-.36.03-.53.04-1.13.1-2.28.16-3.43.16-21.44,0-38.89-17.44-38.89-38.89s17.44-38.89,38.89-38.89Zm398.89-321.11c-21.44,0-38.89-17.44-38.89-38.89s17.44-38.89,38.89-38.89,38.89,17.44,38.89,38.89-17.44,38.89-38.89,38.89Z";
5
+ export const RING_B =
6
+ "m933.27,403.16c-13.62-53.14-37.03-102.64-69.77-147.47-37.14-50.86-85.83-93.86-140.81-124.33-17.44-9.68-39.42-3.38-49.09,14.07-9.68,17.44-3.38,39.42,14.07,49.09,85.32,47.32,147.22,125.71,173.38,218.21-36.3,18.29-61.27,55.92-61.27,99.28s24.94,80.97,61.23,99.27c-34.22,119.96-129.79,215.52-249.74,249.74-18.3-36.29-55.92-61.23-99.27-61.23-61.27,0-111.11,49.84-111.11,111.11s49.84,111.11,111.11,111.11c53.6,0,98.47-38.16,108.84-88.73,75.07-19.29,143.32-58.26,198.74-113.68,55.42-55.42,94.39-123.68,113.68-198.74,50.58-10.38,88.73-55.24,88.73-108.84s-38.16-98.47-88.73-108.84Zm-421.27,546.62c-21.44,0-38.89-17.44-38.89-38.89s17.44-38.89,38.89-38.89,38.89,17.44,38.89,38.89-17.44,38.89-38.89,38.89Zm398.89-398.89c-21.44,0-38.89-17.44-38.89-38.89,0-20.03,15.22-36.58,34.72-38.67.38-.04.77-.08,1.16-.11h.02c.99-.08,1.99-.11,2.99-.11,21.44,0,38.89,17.44,38.89,38.89s-17.44,38.89-38.89,38.89Z";
7
+ export const STAR_BIG =
8
+ "m735.02,517.56c-153.82,30.87-186.59,63.64-217.46,217.46-1.22,6.08-9.9,6.08-11.12,0-30.87-153.82-63.64-186.59-217.46-217.46-6.08-1.22-6.08-9.9,0-11.12,153.82-30.87,186.59-63.64,217.46-217.46,1.22-6.08,9.9-6.08,11.12,0,30.87,153.82,63.64,186.59,217.46,217.46,6.08,1.22,6.08,9.9,0,11.12Z";
9
+ export const STAR_A =
10
+ "m434.06,665.85c-51.08,10.25-61.96,21.13-72.22,72.22-.41,2.02-3.29,2.02-3.69,0-10.25-51.08-21.13-61.96-72.22-72.22-2.02-.41-2.02-3.29,0-3.69,51.08-10.25,61.96-21.13,72.22-72.22.41-2.02,3.29-2.02,3.69,0,10.25,51.08,21.13,61.96,72.22,72.22,2.02.41,2.02,3.29,0,3.69Z";
11
+ export const STAR_B =
12
+ "m738.07,361.84c-51.08,10.25-61.96,21.13-72.22,72.22-.41,2.02-3.29,2.02-3.69,0-10.25-51.08-21.13-61.96-72.22-72.22-2.02-.41-2.02-3.29,0-3.69,51.08-10.25,61.96-21.13,72.22-72.22.41-2.02,3.29-2.02,3.69,0,10.25,51.08,21.13,61.96,72.22,72.22,2.02.41,2.02,3.29,0,3.69Z";
13
+
14
+ /** The logo's colours: [big star and first ring start, middle, small star and
15
+ * second ring end]. Brand colours, not theme tokens: the mark looks the same
16
+ * in light and dark. `community` is the community site's rust counterpart. */
17
+ export const BRAND_COLOURS = {
18
+ extension: ["#a970ff", "#6366f1", "#33d6c6"],
19
+ community: ["#da7550", "#f1636c", "#ad33d6"],
20
+ } as const;
21
+ export type LogoBrand = keyof typeof BRAND_COLOURS;