morphico 0.4.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 (72) hide show
  1. package/README.md +62 -45
  2. package/dist/esm/all.js +50 -0
  3. package/dist/esm/clip.js +283 -0
  4. package/dist/esm/engine.js +25 -0
  5. package/dist/esm/face.js +153 -0
  6. package/dist/esm/geometry.js +459 -0
  7. package/dist/esm/icon.js +41 -0
  8. package/dist/esm/index.js +50 -0
  9. package/dist/esm/morph.js +107 -0
  10. package/dist/esm/roundness.js +37 -0
  11. package/dist/esm/sets/battery.js +87 -0
  12. package/dist/esm/sets/bell.js +101 -0
  13. package/dist/esm/sets/characters.js +43 -0
  14. package/dist/esm/sets/check-mark.js +74 -0
  15. package/dist/esm/sets/checkbox.js +91 -0
  16. package/dist/esm/sets/chevron.js +70 -0
  17. package/dist/esm/sets/condensed.js +137 -0
  18. package/dist/esm/sets/copy.js +87 -0
  19. package/dist/esm/sets/eye.js +105 -0
  20. package/dist/esm/sets/faces.js +482 -0
  21. package/dist/esm/sets/folder.js +112 -0
  22. package/dist/esm/sets/handwritten.js +136 -0
  23. package/dist/esm/sets/lock.js +75 -0
  24. package/dist/esm/sets/lucide.js +66 -0
  25. package/dist/esm/sets/media.js +109 -0
  26. package/dist/esm/sets/menu.js +84 -0
  27. package/dist/esm/sets/mic.js +102 -0
  28. package/dist/esm/sets/radio.js +84 -0
  29. package/dist/esm/sets/round.js +244 -0
  30. package/dist/esm/sets/shapes.js +455 -0
  31. package/dist/esm/sets/slim.js +148 -0
  32. package/dist/esm/sets/square.js +149 -0
  33. package/dist/esm/sets/theme.js +107 -0
  34. package/dist/esm/sets/toggle.js +73 -0
  35. package/dist/esm/sets/volume.js +89 -0
  36. package/dist/esm/sets/weather.js +691 -0
  37. package/dist/esm/sets/wifi.js +68 -0
  38. package/dist/esm/svg.js +31 -0
  39. package/dist/esm/text.js +88 -0
  40. package/dist/esm/types.js +3 -0
  41. package/dist/morphico.js +3764 -3750
  42. package/dist/types/{iconSets.d.ts → all.d.ts} +0 -2
  43. package/dist/types/clip.d.ts +4 -11
  44. package/dist/types/face.d.ts +2 -6
  45. package/dist/types/icon.d.ts +4 -9
  46. package/dist/types/index.d.ts +1 -3
  47. package/dist/types/roundness.d.ts +8 -0
  48. package/dist/types/sets/battery.d.ts +1 -1
  49. package/dist/types/sets/bell.d.ts +1 -1
  50. package/dist/types/sets/checkbox.d.ts +1 -1
  51. package/dist/types/sets/chevron.d.ts +1 -1
  52. package/dist/types/sets/condensed.d.ts +1 -1
  53. package/dist/types/sets/copy.d.ts +1 -1
  54. package/dist/types/sets/eye.d.ts +1 -1
  55. package/dist/types/sets/folder.d.ts +1 -1
  56. package/dist/types/sets/handwritten.d.ts +1 -1
  57. package/dist/types/sets/lock.d.ts +1 -1
  58. package/dist/types/sets/media.d.ts +1 -1
  59. package/dist/types/sets/menu.d.ts +1 -1
  60. package/dist/types/sets/mic.d.ts +1 -1
  61. package/dist/types/sets/radio.d.ts +1 -1
  62. package/dist/types/sets/round.d.ts +1 -1
  63. package/dist/types/sets/slim.d.ts +1 -1
  64. package/dist/types/sets/square.d.ts +1 -1
  65. package/dist/types/sets/theme.d.ts +1 -1
  66. package/dist/types/sets/toggle.d.ts +1 -1
  67. package/dist/types/sets/volume.d.ts +1 -1
  68. package/dist/types/sets/weather.d.ts +1 -1
  69. package/dist/types/sets/wifi.d.ts +1 -1
  70. package/dist/types/text.d.ts +8 -13
  71. package/package.json +95 -3
  72. package/dist/morphico.mjs +0 -4642
@@ -0,0 +1,691 @@
1
+ /*! morphico v0.5.0 | code: MIT License, Copyright (c) 2026 Raycho Ivanov (see LICENSE)
2
+ * Icons based on Lucide (https://lucide.dev), some of them from Feather (https://feathericons.com):
3
+ *
4
+ * ISC License
5
+ *
6
+ * Copyright (c) 2026 Lucide Icons and Contributors
7
+ *
8
+ * Permission to use, copy, modify, and/or distribute this software for any
9
+ * purpose with or without fee is hereby granted, provided that the above
10
+ * copyright notice and this permission notice appear in all copies.
11
+ *
12
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
13
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
14
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
15
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
16
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
17
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
18
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19
+ *
20
+ * The MIT License (MIT) (for the Lucide icons derived from Feather, listed in LICENSE-lucide)
21
+ *
22
+ * Copyright (c) 2013-present Cole Bemis
23
+ *
24
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
25
+ * of this software and associated documentation files (the "Software"), to deal
26
+ * in the Software without restriction, including without limitation the rights
27
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
28
+ * copies of the Software, and to permit persons to whom the Software is
29
+ * furnished to do so, subject to the following conditions:
30
+ *
31
+ * The above copyright notice and this permission notice shall be included in all
32
+ * copies or substantial portions of the Software.
33
+ *
34
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40
+ * SOFTWARE.
41
+ */
42
+ // Icon set "weather": sunny, frosty, partly-cloudy, showers, cloudy, thunder, rain, snow, drizzle — Lucide 1.61.0
43
+ // icons "sun", "sun-snow", "cloud-sun", "cloud-sun-rain", "cloud", "cloud-lightning", "cloud-rain", "cloud-snow",
44
+ // "cloud-drizzle" (ISC, see LICENSE-lucide). frosty is added last, onto the finished set (withFrosty, at the end).
45
+ // Coordinates below are Lucide's; lucideLine / lucideArc scale them into our icon box (see lucide.ts).
46
+ // Pieces:
47
+ // sunny ↔ cloudy (experiment, the user's idea for a more realistic look): the cloud crawls in from the left over
48
+ // the sun — it slides in at full size from the box's left side (PairLayout.wipe + slide; its part outside the
49
+ // box doesn't show), and the sun's circle and rays disappear where its front has passed. (Rejected: the cloud
50
+ // standing still, revealed behind the edge — user: even worse.) (Before: sunny's circle inflated into the cloud with the
51
+ // default layout — confirmed "great" by the user — and the rays flew out, `draw: 0.6`.)
52
+ // piece 1, the cloud.
53
+ // sunny → partly-cloudy / showers: the small cloud crawls in from the left like the big one (user's choice; before:
54
+ // it expanded from its bottom left corner — rejected earlier: a circle copy turning into it, drawing it in).
55
+ // piece 2, the sun's circle: in cloudy a copy of the cloud (drawn on top of it, so it doesn't show) — sunny ↔ cloudy
56
+ // morphs it exactly like piece 1. sunny → partly-cloudy: the circle opens behind the cloud — its hidden part pulls
57
+ // back from the middle into both ends of the arc, the arc itself stays.
58
+ // pieces 3–10: one ray each | nothing — rays run from their outer end inwards; sunny → partly-cloudy: the 4 rays partly-cloudy
59
+ // keeps stay, the other 4 fly out. At rest a missing ray is a single point (never drawn, see toPathData).
60
+ // pieces 11–12: showers' rain lines, which are also rain's left and middle line; piece 14: rain's right line —
61
+ // nothing = a single point. partly-cloudy → showers: drawn downwards like a pen, one after the other, while the
62
+ // cloud's bottom opens. cloudy → rain: like cloudy → thunder (the cloud rises and opens), the 3 lines drawn down
63
+ // one after the other, left to right; from partly-cloudy / showers / thunder the same (showers' lines stretch,
64
+ // the bolt is erased); sunny → rain: the cloud crawls in with its rain (its left end carried by the bolt piece).
65
+ // pieces 11, 12, 14 are also snow's top flakes (the top of each rain line), pieces 15–17 its bottom flakes (hidden
66
+ // at the bottom of each rain line in rain): rain → snow: each line shrinks into its top flake while the bottom
67
+ // flake comes out of its lower end. → snow otherwise: the flakes appear one after the other, row by row (ride
68
+ // in with the cloud from sunny).
69
+ // drizzle's 6 short lines sit where snow's flakes are, so they are the same 6 pieces: rain → drizzle: each line
70
+ // shrinks into its short top piece, the bottom piece grows out of its lower end; snow ↔ drizzle: flake ↔ short
71
+ // line (halves, as rain ↔ snow); → drizzle otherwise like → snow (drawn row by row, riding in from sunny).
72
+ // snow and drizzle have rain's cloud: every other piece morphs exactly like rain's (withRainTwins).
73
+ // The rules of this set (the user's): sun → any cloud = the cloud crawls in from the left and covers the sun;
74
+ // partly-cloudy / showers → a cloud without sun = like → cloudy (the sun erased back down).
75
+ // piece 13: the lightning bolt | nothing (a single point) — cloudy → thunder: the cloud rises a little and opens
76
+ // at the bottom (like cloudy → showers), the bolt strikes down from it like a pen. partly-cloudy / showers →
77
+ // thunder: like → cloudy (the cloud grows, the sun is erased back down), plus the bolt (and showers' rain erased).
78
+ // sunny → thunder: the cloud crawls in like sunny → cloudy, the bolt rides with it. The open cloud's lower left
79
+ // end curls back right, so during the crawl it is carried by the first rain piece (else it would poke out of the
80
+ // box's left side): piece 1 is laid out without it — together they look the same.
81
+ // showers otherwise morphs like partly-cloudy (sunny: the cloud crawls in, the circle opens; cloudy: the cloud
82
+ // shrinks and opens, the sun is drawn in), plus the rain: drawn down, or (from sunny) riding in with the cloud.
83
+ import { arc, defaultBreakpoints, line, piecesOf, reversedStroke } from '../geometry.js';
84
+ import { fromLucide, LUCIDE_STROKE_WIDTH, lucideLength } from './lucide.js';
85
+ const lucideLine = (fromX, fromY, toX, toY) => line(...fromLucide([fromX, fromY]), ...fromLucide([toX, toY]));
86
+ const lucideArc = (centerX, centerY, radius, startAngle, endAngle) => arc(...fromLucide([centerX, centerY]), lucideLength(radius), startAngle, endAngle);
87
+ /** Lucide "cloud", clockwise from the bottom right: bottom line, big bump, top line, right bump. */
88
+ const CLOUD = [
89
+ lucideLine(17.5, 19, 9, 19),
90
+ lucideArc(9.0018, 12, 7, 90.0147, 343.3984),
91
+ lucideLine(15.71, 10, 17.5, 10),
92
+ lucideArc(17.5, 14.5, 4.5, -90, 90),
93
+ ];
94
+ /** sunny → partly-cloudy / showers: the small cloud (and its rain) crawls in a bit later (user's request). */
95
+ const SMALL_CLOUD_CRAWL = [0.4, 1];
96
+ /** CLOUD traced from its leftmost point (sunny ↔ cloudy: it slides in from the left, its left side shows last). */
97
+ const CLOUD_FROM_LEFT = [
98
+ lucideArc(9.0018, 12, 7, 180, 343.3984),
99
+ ...CLOUD.slice(2),
100
+ CLOUD[0],
101
+ lucideArc(9.0018, 12, 7, 90.0147, 180),
102
+ ];
103
+ /** The sun's circle traced from its leftmost point (sunny ↔ cloudy: the wiping edge reaches it first). */
104
+ const CIRCLE_FROM_LEFT = [lucideArc(12, 12, 4, 180, 540)];
105
+ /** A wipe's piece laid out along an alternative tracing with the default spread. */
106
+ const tracedForWipe = (tracing, strokes) => ({ tracing, breakpoints: defaultBreakpoints(strokes, 12) });
107
+ /** The angle (from the icon centre) where the cloud starts, so the circle starts in the same direction. */
108
+ const CIRCLE_START = 51.84;
109
+ /** Lucide "sun"'s circle, clockwise from CIRCLE_START. */
110
+ const SUN_CIRCLE = [lucideArc(12, 12, 4, CIRCLE_START, CIRCLE_START + 360)];
111
+ /** Lucide "cloud-sun"'s small cloud, clockwise from the bottom right (the same build as CLOUD). */
112
+ const SMALL_CLOUD = [
113
+ lucideLine(13, 22, 7, 22),
114
+ lucideArc(7.001, 17, 5, 90.0117, 348.463),
115
+ lucideLine(11.9, 16, 13, 16),
116
+ lucideArc(13, 19, 3, -90, 90),
117
+ ];
118
+ /** SMALL_CLOUD traced from its leftmost point (sunny ↔ partly-cloudy: it slides in like the big cloud). */
119
+ const SMALL_CLOUD_FROM_LEFT = [
120
+ lucideArc(7.001, 17, 5, 180, 348.463),
121
+ ...SMALL_CLOUD.slice(2),
122
+ SMALL_CLOUD[0],
123
+ lucideArc(7.001, 17, 5, 90.0117, 180),
124
+ ];
125
+ /** Lucide "cloud-sun-rain"'s cloud: SMALL_CLOUD open at the bottom (where the rain falls), from its lower left end. */
126
+ const OPEN_SMALL_CLOUD = [
127
+ lucideArc(7.0014, 17.0019, 5, 143.1569, 348.4412),
128
+ lucideLine(11.9, 16, 13, 16),
129
+ lucideArc(13.0044, 19, 3, -90.0842, 48.3025),
130
+ ];
131
+ /** Where SMALL_CLOUD's gap (bottom) has its middle: the gap's two halves pull back into its ends from here. */
132
+ const GAP_MIDDLE_X = 8.77;
133
+ // partly-cloudy ↔ showers: the closed cloud traced from the gap's middle — 2 segments to the gap's left end, the open
134
+ // cloud's part (4 along the big bump, 1 top line, 3 along the right bump), 2 back to the middle; the open cloud gets
135
+ // 2 zero-length segments at each end.
136
+ const CLOSED_FOR_OPEN = {
137
+ tracing: 'small-cloud-from-gap-middle',
138
+ breakpoints: [0, 1, 1.2057, 1.4043, 1.6029, 1.8014, 2, 3, 3.2561, 3.5122, 3.7683, 4, 5],
139
+ };
140
+ const OPEN_FOR_CLOSED = { breakpoints: [0, 0, 0, 0.25, 0.5, 0.75, 1, 2, 2.3333, 2.6667, 3, 3, 3] };
141
+ /** The same gap on the big CLOUD (its bottom, from 143.16° on the big bump to 48.3° on the right bump). */
142
+ const BIG_GAP_MIDDLE_X = 11.65;
143
+ // cloudy ↔ showers: the big cloud traced from its gap's middle, split like CLOSED_FOR_OPEN — it shrinks and opens
144
+ const BIG_CLOSED_FOR_OPEN = {
145
+ tracing: 'cloud-from-gap-middle',
146
+ breakpoints: [0, 1, 1.2097, 1.4073, 1.6049, 1.8024, 2, 3, 3.2561, 3.5122, 3.7683, 4, 5],
147
+ };
148
+ /** Lucide "cloud-lightning"'s cloud: CLOUD 2 units higher, open at the bottom (where the bolt strikes), from its
149
+ * lower left end. */
150
+ const THUNDER_CLOUD = [
151
+ lucideArc(9.0026, 10.0027, 7, 115.4004, 343.3756),
152
+ lucideLine(15.71, 8, 17.5, 8),
153
+ lucideArc(17.5079, 12.5, 4.5, -90.1, 83.7212),
154
+ ];
155
+ /** The middle of CLOUD's part that is missing in THUNDER_CLOUD (its bottom, from 115.4° to 83.7°). */
156
+ const THUNDER_GAP_MIDDLE_X = 11.94;
157
+ // cloudy ↔ thunder: CLOUD traced from that middle — 2 segments to the gap's left end, 4 along the big bump, 1 top line,
158
+ // 3 along the right bump, 2 back to the middle; the open cloud gets 2 zero-length segments at each end (OPEN_FOR_CLOSED).
159
+ const CLOUD_FROM_THUNDER_GAP_MIDDLE = [
160
+ lucideLine(THUNDER_GAP_MIDDLE_X, 19, 9, 19),
161
+ ...CLOUD.slice(1),
162
+ lucideLine(17.5, 19, THUNDER_GAP_MIDDLE_X, 19),
163
+ ];
164
+ const CLOUD_FOR_THUNDER = {
165
+ tracing: 'cloud-from-thunder-gap-middle',
166
+ breakpoints: [0, 1, 1.1002, 1.3252, 1.5501, 1.7751, 2, 3, 3.3217, 3.6434, 3.9651, 4, 5],
167
+ };
168
+ /** sunny ↔ thunder: THUNDER_CLOUD split at its leftmost point — the rest (from there over the top), and its lower
169
+ * left end (from there down), each running away from the left side so the crawl shows one stretch of it. */
170
+ const THUNDER_CLOUD_FROM_LEFT = [
171
+ lucideArc(9.0026, 10.0027, 7, 180, 343.3756),
172
+ ...THUNDER_CLOUD.slice(1),
173
+ ];
174
+ const THUNDER_CLOUD_LEFT_END = [lucideArc(9.0026, 10.0027, 7, 180, 115.4004)];
175
+ /** Lucide "cloud-lightning"'s bolt, from the top down. */
176
+ const BOLT = [lucideLine(13, 12, 10, 17), lucideLine(10, 17, 14, 17), lucideLine(14, 17, 11, 22)];
177
+ /** cloudy → thunder: the bolt strikes in the second half of the morph. */
178
+ const BOLT_DRAWN = [0.5, 0.85];
179
+ /** Lucide "cloud-rain"'s cloud (also "cloud-snow", "cloud-drizzle"): THUNDER_CLOUD with a wider gap, from its lower
180
+ * left end. */
181
+ const RAIN_CLOUD = [
182
+ lucideArc(9.0026, 10.0027, 7, 135.615, 343.3757),
183
+ lucideLine(15.71, 8, 17.5, 8),
184
+ lucideArc(17.5005, 12.5, 4.5, -90.0065, 56.2589),
185
+ ];
186
+ /** The middle of CLOUD's part that is missing in RAIN_CLOUD (its bottom, from 135.6° to 56.3°). */
187
+ const RAIN_GAP_MIDDLE_X = 11.79;
188
+ const CLOUD_FROM_RAIN_GAP_MIDDLE = [
189
+ lucideLine(RAIN_GAP_MIDDLE_X, 19, 9, 19),
190
+ ...CLOUD.slice(1),
191
+ lucideLine(17.5, 19, RAIN_GAP_MIDDLE_X, 19),
192
+ ];
193
+ // cloudy ↔ rain: split like CLOUD_FOR_THUNDER (2 | 4 big bump | 1 | 3 right bump | 2), RAIN_CLOUD with OPEN_FOR_CLOSED
194
+ const CLOUD_FOR_RAIN = {
195
+ tracing: 'cloud-from-rain-gap-middle',
196
+ breakpoints: [0, 1, 1.18, 1.385, 1.59, 1.795, 2, 3, 3.2709, 3.5417, 3.8126, 4, 5],
197
+ };
198
+ /** sunny ↔ rain: RAIN_CLOUD split at its leftmost point, like THUNDER_CLOUD_FROM_LEFT / THUNDER_CLOUD_LEFT_END. */
199
+ const RAIN_CLOUD_FROM_LEFT = [lucideArc(9.0026, 10.0027, 7, 180, 343.3757), ...RAIN_CLOUD.slice(1)];
200
+ const RAIN_CLOUD_LEFT_END = [lucideArc(9.0026, 10.0027, 7, 180, 135.615)];
201
+ /** Lucide "cloud-rain"'s lines, left to right, each from the top down. */
202
+ const HEAVY_RAIN_LINES = [lucideLine(8, 14, 8, 20), lucideLine(12, 16, 12, 22), lucideLine(16, 14, 16, 20)];
203
+ /** cloudy → rain: the lines are drawn one after the other. */
204
+ const HEAVY_RAIN_DRAWN = [[0.45, 0.8], [0.55, 0.9], [0.65, 1]]; // once the bottom has opened
205
+ /** Lucide "cloud-snow"'s flake centres for the pieces 11, 12, 14 (top flakes) and 15, 16, 17 (bottom flakes). */
206
+ const SNOWFLAKE_CENTERS = [[8, 15], [12, 17], [16, 15], [8, 19], [12, 21], [16, 19]];
207
+ /**
208
+ * Lucide draws a flake as a dot (a tiny round-capped line, as wide as the line). Ours are a little bigger so they
209
+ * show at small sizes (user's request): a tiny closed octagon of this radius, which the thick line fills in.
210
+ */
211
+ const SNOWFLAKE_RADIUS = 0.25;
212
+ const SNOWFLAKES = SNOWFLAKE_CENTERS.map(([centerX, centerY]) => {
213
+ // clockwise from the top corner
214
+ const corners = Array.from({ length: 8 }, (_, index) => [
215
+ centerX + SNOWFLAKE_RADIUS * Math.cos(((index - 2) * Math.PI) / 4),
216
+ centerY + SNOWFLAKE_RADIUS * Math.sin(((index - 2) * Math.PI) / 4),
217
+ ]);
218
+ return corners.map((corner, index) => lucideLine(...corner, ...corners[(index + 1) % 8]));
219
+ });
220
+ // line → flake (rain / showers → snow): the line traced down and back up, its down pass ↔ the flake's right half, its
221
+ // up pass ↔ the left half — the line narrows into the flake (segment by segment the octagon's corners zigzag)
222
+ const LINE_OUT_AND_BACK = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((index) => +(index / 6).toFixed(4));
223
+ const FLAKE_IN_HALVES = { breakpoints: [0, 0.5, 1, 2, 3, 3.5, 4, 4.5, 5, 6, 7, 7.5, 8] };
224
+ const outAndBack = (stroke) => [stroke, reversedStroke(stroke)];
225
+ /** → snow / drizzle: when each flake / short line appears (row by row: 8,15 · 16,15 · 12,17 · 8,19 · 16,19 · 12,21). */
226
+ const SNOWFLAKE_STARTS = [0.45, 0.59, 0.52, 0.66, 0.8, 0.73];
227
+ const fallingDrawn = (flakeIndex) => ({ draw: [SNOWFLAKE_STARTS[flakeIndex], SNOWFLAKE_STARTS[flakeIndex] + 0.1] });
228
+ /** Lucide "cloud-drizzle"'s short lines (top down), in the order of the flakes (each one where a flake is). */
229
+ const DRIZZLE_LINES = [[8, 14], [12, 16], [16, 14], [8, 19], [12, 21], [16, 19]]
230
+ .map(([x, y]) => lucideLine(x, y, x, y + 1));
231
+ /** Pair layouts of several groups put together (per state). */
232
+ const mergePairLayouts = (...groups) => {
233
+ const merged = {};
234
+ for (const group of groups) {
235
+ for (const [state, byOtherState] of Object.entries(group))
236
+ merged[state] = { ...merged[state], ...byOtherState };
237
+ }
238
+ return merged;
239
+ };
240
+ /** The drizzle pairs of precipitation piece `fallingIndex` (0…5) — showers' own lines (0, 1) just shorten. */
241
+ const drizzlePairLayouts = (fallingIndex, hasShowersLine) => ({
242
+ ...Object.fromEntries(['cloudy', 'partly-cloudy', 'thunder', ...(hasShowersLine ? [] : ['showers'])]
243
+ .map((state) => [state, { drizzle: fallingDrawn(fallingIndex) }])),
244
+ sunny: { drizzle: RIDING_IN },
245
+ snow: { drizzle: FLAKE_IN_HALVES },
246
+ drizzle: { snow: { tracing: 'drizzle-line-out-and-back', breakpoints: LINE_OUT_AND_BACK } },
247
+ });
248
+ /** sunny → rain / snow: the precipitation rides in with the crawling cloud. */
249
+ const RIDING_IN = { wipe: 'right', slide: true };
250
+ /** Lucide "cloud-sun-rain"'s rain lines, each from the top down. */
251
+ const RAIN_LINES = [lucideLine(7, 19, 7, 21), lucideLine(11, 20, 11, 22)];
252
+ /** partly-cloudy → showers: the rain lines are drawn one after the other while the cloud opens. */
253
+ const RAIN_DRAWN = [[0.35, 0.75], [0.55, 0.95]];
254
+ /** Lucide "cloud-sun"'s sun: the part of the circle above the cloud, clockwise from its left end. */
255
+ const SUN_ARC = [lucideArc(12.0004, 11.9985, 4, -119.6437, 9.3742)];
256
+ /** The middle of the sun's hidden part (behind the cloud), from where it pulls back into both ends of SUN_ARC. */
257
+ const HIDDEN_MIDDLE = (9.3742 + 240.3563) / 2;
258
+ /** Lucide "sun"'s rays, each from its outer end inwards (erasing keeps the outer end: the ray flies out). */
259
+ const RAYS = [
260
+ lucideLine(12, 2, 12, 4),
261
+ lucideLine(19.07, 4.93, 17.66, 6.34),
262
+ lucideLine(22, 12, 20, 12),
263
+ lucideLine(19.07, 19.07, 17.66, 17.66),
264
+ lucideLine(12, 22, 12, 20),
265
+ lucideLine(4.93, 19.07, 6.34, 17.66),
266
+ lucideLine(2, 12, 4, 12),
267
+ lucideLine(4.93, 4.93, 6.34, 6.34),
268
+ ];
269
+ /** A missing piece at rest: a single point (never drawn), here on the cloud's bottom line. */
270
+ const pointAt = (x, y) => [lucideLine(x, y, x, y)];
271
+ const HIDDEN_RAY = pointAt(13, 19);
272
+ /** partly-cloudy's and showers' missing rays (bottom right, bottom, bottom left, left). */
273
+ const HIDDEN_RAY_PARTLY = pointAt(10, 22);
274
+ const MISSING_RAYS_PARTLY = [3, 4, 5, 6];
275
+ // sunny ↔ partly-cloudy, the sun's circle: traced from the middle of the hidden part, 4 segments to the arc's left
276
+ // end, 4 along the arc (it stays), 4 back to the middle; the arc gets 4 zero-length segments at each end.
277
+ const ARC_SHARE = (9.3742 + 119.6437) / 360;
278
+ const CIRCLE_FOR_SUN_ARC = {
279
+ tracing: 'circle-from-hidden-middle',
280
+ breakpoints: [
281
+ ...[0, 1, 2, 3].map((index) => (index / 4) * ((1 - ARC_SHARE) / 2)),
282
+ ...[0, 1, 2, 3].map((index) => (1 - ARC_SHARE) / 2 + (index / 4) * ARC_SHARE),
283
+ ...[0, 1, 2, 3, 4].map((index) => (1 + ARC_SHARE) / 2 + (index / 4) * ((1 - ARC_SHARE) / 2)),
284
+ ].map((breakpoint) => +breakpoint.toFixed(4)),
285
+ };
286
+ const SUN_ARC_FOR_CIRCLE = { breakpoints: [0, 0, 0, 0, 0, 0.25, 0.5, 0.75, 1, 1, 1, 1, 1] };
287
+ // cloudy → partly-cloudy: the sun is drawn like a pen from the arc's lower right end up over the top (the user's
288
+ // direction), and the kept rays follow one after another, each growing out from the sun when the pen passes it.
289
+ const SUN_ARC_DRAWN = [0.45, 0.85]; // after the cloud has shrunk away from the arc's start
290
+ const RAY_DRAW_DURATION = 0.15;
291
+ /** Kept rays → how far along the arc (from its lower right end, 0…1) the pen is when the ray starts. */
292
+ const KEPT_RAY_PEN_POSITIONS = { 2: 0.07, 1: 0.42, 0: 0.77, 7: 1 };
293
+ const rayDrawnAfterPen = (penPosition) => {
294
+ const start = SUN_ARC_DRAWN[0] + penPosition * (SUN_ARC_DRAWN[1] - SUN_ARC_DRAWN[0]);
295
+ return [+start.toFixed(4), +Math.min(start + RAY_DRAW_DURATION, 1).toFixed(4)];
296
+ };
297
+ /** States drawn with rain's cloud: apart from the precipitation they morph exactly like rain. */
298
+ const RAIN_TWINS = ['snow', 'drizzle'];
299
+ /** A piece that is the same in rain and its twins: the twins get rain's strokes and pair layouts. */
300
+ const withRainTwins = (pieceSet) => {
301
+ const pairLayouts = Object.fromEntries(Object.entries(pieceSet.pairLayouts).map(([state, layouts]) => [state, { ...layouts, ...(layouts.rain ? Object.fromEntries(RAIN_TWINS.map((twin) => [twin, layouts.rain])) : {}) }]));
302
+ for (const twin of RAIN_TWINS)
303
+ if (pieceSet.pairLayouts.rain)
304
+ pairLayouts[twin] = pieceSet.pairLayouts.rain;
305
+ return {
306
+ ...pieceSet,
307
+ strokes: { ...pieceSet.strokes, ...Object.fromEntries(RAIN_TWINS.map((twin) => [twin, pieceSet.strokes.rain])) },
308
+ pairLayouts,
309
+ };
310
+ };
311
+ const sunCirclePiece = {
312
+ strokes: { sunny: SUN_CIRCLE, cloudy: CLOUD, 'partly-cloudy': SUN_ARC, showers: SUN_ARC, thunder: THUNDER_CLOUD, rain: RAIN_CLOUD },
313
+ alternativeStrokes: {
314
+ 'cloud-from-rain-gap-middle': CLOUD_FROM_RAIN_GAP_MIDDLE,
315
+ 'cloud-from-thunder-gap-middle': CLOUD_FROM_THUNDER_GAP_MIDDLE,
316
+ 'circle-from-hidden-middle': [lucideArc(12, 12, 4, HIDDEN_MIDDLE, HIDDEN_MIDDLE + 360)],
317
+ 'circle-from-left': CIRCLE_FROM_LEFT,
318
+ },
319
+ pairLayouts: {
320
+ sunny: {
321
+ cloudy: tracedForWipe('circle-from-left', CIRCLE_FROM_LEFT),
322
+ thunder: tracedForWipe('circle-from-left', CIRCLE_FROM_LEFT),
323
+ rain: tracedForWipe('circle-from-left', CIRCLE_FROM_LEFT),
324
+ 'partly-cloudy': CIRCLE_FOR_SUN_ARC,
325
+ showers: CIRCLE_FOR_SUN_ARC,
326
+ },
327
+ cloudy: {
328
+ sunny: { wipe: 'left' },
329
+ 'partly-cloudy': { draw: SUN_ARC_DRAWN },
330
+ showers: { draw: SUN_ARC_DRAWN },
331
+ thunder: CLOUD_FOR_THUNDER,
332
+ rain: CLOUD_FOR_RAIN,
333
+ },
334
+ rain: {
335
+ cloudy: OPEN_FOR_CLOSED,
336
+ sunny: { wipe: 'left' },
337
+ 'partly-cloudy': { draw: SUN_ARC_DRAWN },
338
+ showers: { draw: SUN_ARC_DRAWN },
339
+ },
340
+ 'partly-cloudy': { sunny: SUN_ARC_FOR_CIRCLE, cloudy: { reversed: true }, thunder: { reversed: true }, rain: { reversed: true } },
341
+ showers: { sunny: SUN_ARC_FOR_CIRCLE, cloudy: { reversed: true }, thunder: { reversed: true }, rain: { reversed: true } },
342
+ // the sun goes like → cloudy: crawled over from sunny, erased back down from partly-cloudy / showers
343
+ thunder: { cloudy: OPEN_FOR_CLOSED, sunny: { wipe: 'left' }, 'partly-cloudy': { draw: SUN_ARC_DRAWN }, showers: { draw: SUN_ARC_DRAWN } },
344
+ },
345
+ };
346
+ const rayPiece = (ray, rayIndex) => {
347
+ const missingInPartly = MISSING_RAYS_PARTLY.includes(rayIndex);
348
+ return {
349
+ strokes: {
350
+ sunny: [ray],
351
+ cloudy: HIDDEN_RAY,
352
+ 'partly-cloudy': missingInPartly ? HIDDEN_RAY_PARTLY : [ray],
353
+ showers: missingInPartly ? HIDDEN_RAY_PARTLY : [ray],
354
+ thunder: HIDDEN_RAY,
355
+ rain: HIDDEN_RAY,
356
+ },
357
+ alternativeStrokes: {},
358
+ pairLayouts: {
359
+ cloudy: { sunny: { wipe: 'left' } },
360
+ thunder: { sunny: { wipe: 'left' } },
361
+ rain: { sunny: { wipe: 'left' } },
362
+ ...(missingInPartly
363
+ ? { 'partly-cloudy': { sunny: { draw: 0.6 } }, showers: { sunny: { draw: 0.6 } } }
364
+ // drawn from the sun outwards (the rays run outer end → inner end, so reversed)
365
+ : {
366
+ cloudy: {
367
+ sunny: { wipe: 'left' },
368
+ 'partly-cloudy': { draw: rayDrawnAfterPen(KEPT_RAY_PEN_POSITIONS[rayIndex]) },
369
+ showers: { draw: rayDrawnAfterPen(KEPT_RAY_PEN_POSITIONS[rayIndex]) },
370
+ },
371
+ thunder: {
372
+ sunny: { wipe: 'left' },
373
+ 'partly-cloudy': { draw: rayDrawnAfterPen(KEPT_RAY_PEN_POSITIONS[rayIndex]) },
374
+ showers: { draw: rayDrawnAfterPen(KEPT_RAY_PEN_POSITIONS[rayIndex]) },
375
+ },
376
+ rain: {
377
+ sunny: { wipe: 'left' },
378
+ 'partly-cloudy': { draw: rayDrawnAfterPen(KEPT_RAY_PEN_POSITIONS[rayIndex]) },
379
+ showers: { draw: rayDrawnAfterPen(KEPT_RAY_PEN_POSITIONS[rayIndex]) },
380
+ },
381
+ 'partly-cloudy': { cloudy: { reversed: true }, thunder: { reversed: true }, rain: { reversed: true } },
382
+ showers: { cloudy: { reversed: true }, thunder: { reversed: true }, rain: { reversed: true } },
383
+ }),
384
+ },
385
+ };
386
+ };
387
+ /** Showers' rain line (if it has one) | rain's line: pieces 11, 12 (both) and 14 (rain only). */
388
+ const rainPiece = (heavyRainLine, rainIndex) => {
389
+ const showersLine = RAIN_LINES[rainIndex];
390
+ const top = (showersLine ?? heavyRainLine).from;
391
+ const nothing = [line(...top, ...top)];
392
+ const carriesCloudEnd = rainIndex === 0;
393
+ return {
394
+ strokes: {
395
+ sunny: nothing,
396
+ cloudy: nothing,
397
+ 'partly-cloudy': nothing,
398
+ showers: showersLine ? [showersLine] : nothing,
399
+ thunder: nothing,
400
+ rain: [heavyRainLine],
401
+ snow: SNOWFLAKES[rainIndex],
402
+ drizzle: [DRIZZLE_LINES[rainIndex]],
403
+ },
404
+ alternativeStrokes: {
405
+ ...(carriesCloudEnd ? { 'thunder-cloud-left-end': THUNDER_CLOUD_LEFT_END } : {}),
406
+ 'rain-line-out-and-back': outAndBack(heavyRainLine),
407
+ 'drizzle-line-out-and-back': outAndBack(DRIZZLE_LINES[rainIndex]),
408
+ ...(showersLine ? { 'showers-line-out-and-back': outAndBack(showersLine) } : {}),
409
+ },
410
+ pairLayouts: mergePairLayouts(drizzlePairLayouts(rainIndex, Boolean(showersLine)), showersLine
411
+ ? {
412
+ cloudy: { showers: { draw: RAIN_DRAWN[rainIndex] }, rain: { draw: HEAVY_RAIN_DRAWN[rainIndex] }, snow: fallingDrawn(rainIndex) },
413
+ 'partly-cloudy': {
414
+ showers: { draw: RAIN_DRAWN[rainIndex] },
415
+ rain: { draw: HEAVY_RAIN_DRAWN[rainIndex] },
416
+ snow: fallingDrawn(rainIndex),
417
+ },
418
+ // sunny → showers / rain: the rain rides in with the crawling cloud; sunny → thunder: it carries the cloud's
419
+ // left end
420
+ sunny: {
421
+ showers: { wipe: 'right', slide: true, draw: SMALL_CLOUD_CRAWL },
422
+ rain: RIDING_IN,
423
+ snow: RIDING_IN,
424
+ ...(carriesCloudEnd ? { thunder: RIDING_IN } : {}),
425
+ },
426
+ // showers → thunder: erased back up; thunder → rain: drawn down
427
+ thunder: {
428
+ showers: { draw: RAIN_DRAWN[rainIndex] },
429
+ rain: { draw: HEAVY_RAIN_DRAWN[rainIndex] },
430
+ snow: fallingDrawn(rainIndex),
431
+ ...(carriesCloudEnd ? { sunny: tracedForWipe('thunder-cloud-left-end', THUNDER_CLOUD_LEFT_END) } : {}),
432
+ },
433
+ rain: { snow: { tracing: 'rain-line-out-and-back', breakpoints: LINE_OUT_AND_BACK } },
434
+ showers: { snow: { tracing: 'showers-line-out-and-back', breakpoints: LINE_OUT_AND_BACK } },
435
+ snow: { rain: FLAKE_IN_HALVES, showers: FLAKE_IN_HALVES },
436
+ }
437
+ : {
438
+ ...Object.fromEntries(['cloudy', 'partly-cloudy', 'showers', 'thunder']
439
+ .map((state) => [state, { rain: { draw: HEAVY_RAIN_DRAWN[rainIndex] }, snow: fallingDrawn(rainIndex) }])),
440
+ sunny: { rain: RIDING_IN, snow: RIDING_IN },
441
+ rain: { snow: { tracing: 'rain-line-out-and-back', breakpoints: LINE_OUT_AND_BACK } },
442
+ snow: { rain: FLAKE_IN_HALVES },
443
+ }),
444
+ };
445
+ };
446
+ /** Snow's bottom flakes / drizzle's bottom lines (pieces 15–17): in rain hidden at the bottom of its line, so
447
+ * rain → snow / drizzle pulls them out of it. */
448
+ const bottomFlakePiece = (flakeIndex) => {
449
+ const nothing = pointAt(...SNOWFLAKE_CENTERS[flakeIndex]);
450
+ const [lineBottomX, lineBottomY] = HEAVY_RAIN_LINES[flakeIndex - 3].to;
451
+ return {
452
+ strokes: {
453
+ sunny: nothing,
454
+ cloudy: nothing,
455
+ 'partly-cloudy': nothing,
456
+ showers: nothing,
457
+ thunder: nothing,
458
+ rain: [line(lineBottomX, lineBottomY, lineBottomX, lineBottomY)],
459
+ snow: SNOWFLAKES[flakeIndex],
460
+ drizzle: [DRIZZLE_LINES[flakeIndex]],
461
+ },
462
+ alternativeStrokes: { 'drizzle-line-out-and-back': outAndBack(DRIZZLE_LINES[flakeIndex]) },
463
+ pairLayouts: mergePairLayouts(drizzlePairLayouts(flakeIndex, false), {
464
+ ...Object.fromEntries(['cloudy', 'partly-cloudy', 'showers', 'thunder'].map((state) => [state, { snow: fallingDrawn(flakeIndex) }])),
465
+ sunny: { snow: RIDING_IN },
466
+ }),
467
+ };
468
+ };
469
+ const boltTop = pointAt(13, 12);
470
+ const boltPiece = {
471
+ strokes: { sunny: boltTop, cloudy: boltTop, 'partly-cloudy': boltTop, showers: boltTop, thunder: BOLT, rain: boltTop },
472
+ // (snow: like rain, withRainTwins)
473
+ alternativeStrokes: { 'rain-cloud-left-end': RAIN_CLOUD_LEFT_END },
474
+ pairLayouts: {
475
+ cloudy: { thunder: { draw: BOLT_DRAWN } },
476
+ 'partly-cloudy': { thunder: { draw: BOLT_DRAWN } },
477
+ showers: { thunder: { draw: BOLT_DRAWN } },
478
+ // thunder → rain: pulled back up into the cloud
479
+ rain: { thunder: { draw: BOLT_DRAWN }, sunny: tracedForWipe('rain-cloud-left-end', RAIN_CLOUD_LEFT_END) },
480
+ // rides in with the crawling cloud; sunny → rain: carries the rain cloud's left end
481
+ sunny: { thunder: { wipe: 'right', slide: true }, rain: { wipe: 'right', slide: true } },
482
+ },
483
+ };
484
+ const rainTwinnedIcons = withRainTwins({
485
+ segmentCount: 12,
486
+ strokeWidth: LUCIDE_STROKE_WIDTH,
487
+ lineCap: 'round',
488
+ lineJoin: 'round',
489
+ strokes: {
490
+ sunny: SUN_CIRCLE,
491
+ cloudy: CLOUD,
492
+ 'partly-cloudy': SMALL_CLOUD,
493
+ showers: OPEN_SMALL_CLOUD,
494
+ thunder: THUNDER_CLOUD,
495
+ rain: RAIN_CLOUD,
496
+ },
497
+ alternativeStrokes: {
498
+ 'cloud-from-rain-gap-middle': CLOUD_FROM_RAIN_GAP_MIDDLE,
499
+ 'cloud-from-thunder-gap-middle': CLOUD_FROM_THUNDER_GAP_MIDDLE,
500
+ 'thunder-cloud-from-left': THUNDER_CLOUD_FROM_LEFT,
501
+ 'rain-cloud-from-left': RAIN_CLOUD_FROM_LEFT,
502
+ 'cloud-from-left': CLOUD_FROM_LEFT,
503
+ 'small-cloud-from-left': SMALL_CLOUD_FROM_LEFT,
504
+ 'cloud-from-gap-middle': [
505
+ lucideLine(BIG_GAP_MIDDLE_X, 19, 9, 19),
506
+ ...CLOUD.slice(1),
507
+ lucideLine(17.5, 19, BIG_GAP_MIDDLE_X, 19),
508
+ ],
509
+ 'small-cloud-from-gap-middle': [
510
+ lucideLine(GAP_MIDDLE_X, 22, 7, 22),
511
+ ...SMALL_CLOUD.slice(1),
512
+ lucideLine(13, 22, GAP_MIDDLE_X, 22),
513
+ ],
514
+ },
515
+ pairLayouts: {
516
+ // every cloud crawls in from the left over the sun
517
+ sunny: {
518
+ cloudy: { wipe: 'right', slide: true },
519
+ 'partly-cloudy': { wipe: 'right', slide: true, draw: SMALL_CLOUD_CRAWL },
520
+ showers: { wipe: 'right', slide: true, draw: SMALL_CLOUD_CRAWL },
521
+ thunder: { wipe: 'right', slide: true },
522
+ rain: { wipe: 'right', slide: true },
523
+ },
524
+ cloudy: { sunny: tracedForWipe('cloud-from-left', CLOUD_FROM_LEFT), showers: BIG_CLOSED_FOR_OPEN, thunder: CLOUD_FOR_THUNDER, rain: CLOUD_FOR_RAIN },
525
+ 'partly-cloudy': {
526
+ sunny: tracedForWipe('small-cloud-from-left', SMALL_CLOUD_FROM_LEFT),
527
+ showers: CLOSED_FOR_OPEN,
528
+ thunder: CLOSED_FOR_OPEN,
529
+ rain: CLOSED_FOR_OPEN,
530
+ },
531
+ showers: { 'partly-cloudy': OPEN_FOR_CLOSED, cloudy: OPEN_FOR_CLOSED },
532
+ rain: {
533
+ cloudy: OPEN_FOR_CLOSED,
534
+ 'partly-cloudy': OPEN_FOR_CLOSED,
535
+ sunny: tracedForWipe('rain-cloud-from-left', RAIN_CLOUD_FROM_LEFT),
536
+ },
537
+ thunder: {
538
+ cloudy: OPEN_FOR_CLOSED,
539
+ 'partly-cloudy': OPEN_FOR_CLOSED,
540
+ sunny: tracedForWipe('thunder-cloud-from-left', THUNDER_CLOUD_FROM_LEFT),
541
+ },
542
+ },
543
+ morePieces: [
544
+ ...[sunCirclePiece, ...RAYS.map(rayPiece)].map(withRainTwins),
545
+ ...HEAVY_RAIN_LINES.slice(0, 2).map(rainPiece),
546
+ withRainTwins(boltPiece),
547
+ rainPiece(HEAVY_RAIN_LINES[2], 2),
548
+ ...[3, 4, 5].map(bottomFlakePiece),
549
+ ],
550
+ spins: {},
551
+ });
552
+ // ---- frosty (Lucide "sun-snow"): half a small sun on the left, half a snowflake on the right ----
553
+ // sunny ↔ frosty: the sun shrinks to its left half (its right half pulls back into the half's ends) while a copy of
554
+ // that right half (the bolt piece) straightens into the snowflake's spine; the 3 right rays bend into its branches,
555
+ // the other 5 move to frosty's shorter rays.
556
+ // frosty → any cloud state (the set's rule): the cloud crawls in from the left and covers everything (the rain etc.
557
+ // rides along). A wipe needs every piece to show one stretch, so during the crawl the half sun's upper quarter and
558
+ // the spine (4 x-monotone parts) are carried by 6 spare pieces (a single point at rest, in every state); the 6th
559
+ // carries the open clouds' lower left end. frosty ↔ partly-cloudy / showers: the half sun becomes their sun arc,
560
+ // the kept rays move, the rest is covered.
561
+ /** frosty's half sun, clockwise from its bottom (Lucide: centre 10,12, radius 3). */
562
+ const HALF_SUN = [lucideArc(10, 12, 3, 90, 270)];
563
+ /** For the crawl: from its leftmost point down / up (each one stretch for the wipe). */
564
+ const HALF_SUN_LOWER = [lucideArc(10, 12, 3, 180, 90)];
565
+ const HALF_SUN_UPPER = [lucideArc(10, 12, 3, 180, 270)];
566
+ /** frosty's rays by ray piece (outer end first), the right three bent into the snowflake's branches. */
567
+ const FROSTY_RAYS = [
568
+ [lucideLine(10, 3, 10, 4)],
569
+ [lucideLine(18, 6, 15.25, 6.5), lucideLine(15.25, 6.5, 14, 4)],
570
+ [lucideLine(20, 10, 18.5, 12), lucideLine(18.5, 12, 20, 14)],
571
+ [lucideLine(18, 18, 15.25, 17.5), lucideLine(15.25, 17.5, 14, 20)],
572
+ [lucideLine(10, 21, 10, 20)],
573
+ [lucideLine(3.64, 18.36, 4.34, 17.66)],
574
+ [lucideLine(2, 12, 3, 12)],
575
+ [lucideLine(3.64, 5.64, 4.34, 6.34)],
576
+ ];
577
+ /** The snowflake's spine as one line: upper arm, out along the middle arm and back, lower arm. */
578
+ const SPINE = [
579
+ lucideLine(17, 3, 14, 9), lucideLine(14, 9, 15.5, 12), lucideLine(15.5, 12, 22, 12),
580
+ lucideLine(22, 12, 15.5, 12), lucideLine(15.5, 12, 14, 15), lucideLine(14, 15, 17, 21),
581
+ ];
582
+ /** The spine in parts that each run one way across (for the crawl). */
583
+ const SPINE_PARTS = [
584
+ [lucideLine(17, 3, 14, 9)],
585
+ [lucideLine(14, 9, 15.5, 12), lucideLine(15.5, 12, 22, 12)],
586
+ [lucideLine(14, 15, 15.5, 12)],
587
+ [lucideLine(14, 15, 17, 21)],
588
+ ];
589
+ // sunny ↔ frosty: the circle traced from its right point — 3 segments of its lower right quarter pull back into the half
590
+ // sun's bottom end, 6 along the left half, 3 of the upper right quarter into its top end
591
+ const CIRCLE_FOR_HALF_SUN = {
592
+ tracing: 'circle-from-right',
593
+ breakpoints: Array.from({ length: 13 }, (_, index) => +(index / 12).toFixed(4)),
594
+ };
595
+ const HALF_SUN_FOR_CIRCLE = { breakpoints: [0, 0, 0, 0, 0.1667, 0.3333, 0.5, 0.6667, 0.8333, 1, 1, 1, 1] };
596
+ /** The circle's right half (top → right → bottom): the bolt piece's start in sunny → frosty (under the circle). */
597
+ const CIRCLE_RIGHT_HALF = [lucideArc(12, 12, 4, -90, 90)];
598
+ const CLOUD_STATES = ['cloudy', 'partly-cloudy', 'showers', 'thunder', 'rain', 'snow', 'drizzle'];
599
+ /** Cloud states that keep a sun (its arc and 4 rays). */
600
+ const SUN_CLOUD_STATES = ['partly-cloudy', 'showers'];
601
+ const isNothing = (strokes) => strokes.length === 1 && strokes[0].kind === 'line' && strokes[0].from.every((value, axis) => value === strokes[0].to[axis]);
602
+ const POINT_LAYOUT = { tracing: 'nothing', breakpoints: Array.from({ length: 13 }, (_, index) => +(index / 12).toFixed(4)) };
603
+ /** The open clouds' lower left ends, carried by a spare piece while they crawl in. */
604
+ const CLOUD_LEFT_ENDS = {
605
+ thunder: THUNDER_CLOUD_LEFT_END, rain: RAIN_CLOUD_LEFT_END, snow: RAIN_CLOUD_LEFT_END, drizzle: RAIN_CLOUD_LEFT_END,
606
+ };
607
+ const COVERED = { wipe: 'left' };
608
+ /** `pieceSet` with frosty's strokes and `layouts[state][otherState]` added. */
609
+ const addFrosty = (pieceSet, strokes, layouts, alternativeStrokes = {}) => ({
610
+ strokes: { ...pieceSet.strokes, frosty: strokes },
611
+ alternativeStrokes: { ...pieceSet.alternativeStrokes, ...alternativeStrokes },
612
+ pairLayouts: mergePairLayouts(pieceSet.pairLayouts, layouts),
613
+ });
614
+ /** Layouts for the given cloud states X: `frosty.X` and `X.frosty` (either may be left out). */
615
+ const forCloudStates = (states, frostySide, otherSide) => {
616
+ const layouts = { frosty: {} };
617
+ for (const state of states) {
618
+ const frostyLayout = frostySide?.(state), otherLayout = otherSide?.(state);
619
+ if (frostyLayout)
620
+ layouts.frosty[state] = frostyLayout;
621
+ if (otherLayout)
622
+ layouts[state] = { frosty: otherLayout };
623
+ }
624
+ return layouts;
625
+ };
626
+ /** Erasing runs a window backwards: [0, 0.6] erases during 40 … 100 %, with SMALL_CLOUD_CRAWL. */
627
+ const COVERED_BEFORE_CRAWL_ENDS = [1 - SMALL_CLOUD_CRAWL[1], 1 - SMALL_CLOUD_CRAWL[0]];
628
+ // frosty ↔ partly-cloudy / showers: the spine's centre sits where the sun arc's right end is — the spine traced
629
+ // backwards (lower arm, middle arm out and back, upper arm) shrinks into that end with its first 6 segments while
630
+ // its upper arm unrolls from there up over the top into the arc with the last 6
631
+ const SPINE_FOR_SUN_ARC = { tracing: 'spine-reversed', breakpoints: [0, 0.3333, 0.6667, 1, 2, 3, 4, 4.5, 5, 5.25, 5.5, 5.75, 6] };
632
+ const SUN_ARC_FOR_SPINE = { tracing: 'sun-arc-reversed', breakpoints: [0, 0, 0, 0, 0, 0, 0, 0.1667, 0.3333, 0.5, 0.6667, 0.8333, 1] };
633
+ /** Covered by the crawling cloud — for partly-cloudy / showers in step with their later crawl. */
634
+ const coveredIn = (state) => (SUN_CLOUD_STATES.includes(state) ? { wipe: 'left', draw: COVERED_BEFORE_CRAWL_ENDS } : { wipe: 'left' });
635
+ const withFrosty = (iconSet) => {
636
+ const [cloudPiece, sunPiece, ...otherPieces] = piecesOf(iconSet);
637
+ const rayPieces = otherPieces.slice(0, 8);
638
+ const [rain1, rain2, bolt, rain3, ...bottomFlakes] = otherPieces.slice(8);
639
+ const nothing = pointAt(12, 12);
640
+ const sparePiece = (layouts, alternativeStrokes) => ({
641
+ strokes: Object.fromEntries([...Object.keys(iconSet.strokes), 'frosty'].map((state) => [state, nothing])),
642
+ alternativeStrokes,
643
+ pairLayouts: layouts,
644
+ });
645
+ const coveredStates = CLOUD_STATES.filter((state) => !SUN_CLOUD_STATES.includes(state));
646
+ const circleFromRight = [lucideArc(12, 12, 4, 0, 360)];
647
+ const sunnyToFrosty = { sunny: { frosty: CIRCLE_FOR_HALF_SUN }, frosty: { sunny: HALF_SUN_FOR_CIRCLE } };
648
+ const cloud = {
649
+ ...iconSet,
650
+ ...addFrosty(cloudPiece, HALF_SUN, mergePairLayouts(
651
+ // sunny ↔ frosty: a copy of the sun (under piece 2), morphing like it
652
+ sunnyToFrosty,
653
+ // → any cloud: it crawls in exactly as from sunny
654
+ forCloudStates(CLOUD_STATES, (state) => iconSet.pairLayouts.sunny[state], (state) => iconSet.pairLayouts[state]?.sunny)), { 'circle-from-right': circleFromRight }),
655
+ };
656
+ const sun = addFrosty(sunPiece, HALF_SUN, mergePairLayouts(sunnyToFrosty,
657
+ // covered by the crawling cloud (its upper quarter on a spare piece)
658
+ forCloudStates(coveredStates, () => tracedForWipe('half-sun-lower', HALF_SUN_LOWER), coveredIn),
659
+ // partly-cloudy / showers: laid out as the spine (the spares carry the half sun), which bends into the sun arc
660
+ forCloudStates(SUN_CLOUD_STATES, () => SPINE_FOR_SUN_ARC, () => SUN_ARC_FOR_SPINE)), {
661
+ 'circle-from-right': circleFromRight,
662
+ 'half-sun-lower': HALF_SUN_LOWER,
663
+ 'spine-reversed': SPINE.map(reversedStroke).reverse(),
664
+ 'sun-arc-reversed': SUN_ARC.map(reversedStroke).reverse(),
665
+ });
666
+ const rays = rayPieces.map((rayPiece, rayIndex) => addFrosty(rayPiece, FROSTY_RAYS[rayIndex],
667
+ // where the cloud state has no such ray: covered, or (partly-cloudy / showers) flying out as from sunny;
668
+ // kept rays move
669
+ forCloudStates(CLOUD_STATES.filter((state) => isNothing(rayPiece.strokes[state])), undefined, (state) => (SUN_CLOUD_STATES.includes(state) ? { draw: 0.6 } : COVERED))));
670
+ // precipitation: rides in with the cloud, as from sunny
671
+ const precipitation = (pieceSet) => addFrosty(pieceSet, pieceSet.strokes.sunny, forCloudStates(CLOUD_STATES.filter((state) => !isNothing(pieceSet.strokes[state])), (state) => pieceSet.pairLayouts.sunny?.[state] ?? RIDING_IN));
672
+ const boltWithSpine = addFrosty(bolt, SPINE, mergePairLayouts({ sunny: { frosty: tracedForWipe('circle-right-half', CIRCLE_RIGHT_HALF) } },
673
+ // during the crawl the spares (or the sun piece) carry the spine; thunder's bolt rides in
674
+ forCloudStates(CLOUD_STATES, (state) => (state === 'thunder' ? RIDING_IN : POINT_LAYOUT))), { 'circle-right-half': CIRCLE_RIGHT_HALF, nothing: [lucideLine(12, 12, 12, 12)] });
675
+ const spares = [
676
+ // the spine's parts (covered); the first also carries the half sun's lower quarter for partly-cloudy / showers
677
+ ...SPINE_PARTS.map((part, partIndex) => sparePiece(mergePairLayouts(forCloudStates(coveredStates, () => tracedForWipe(`spine-part-${partIndex}`, part), coveredIn), partIndex === 0 ? forCloudStates(SUN_CLOUD_STATES, () => tracedForWipe('half-sun-lower', HALF_SUN_LOWER), coveredIn) : {}), { [`spine-part-${partIndex}`]: part, 'half-sun-lower': HALF_SUN_LOWER })),
678
+ // the half sun's upper quarter
679
+ sparePiece(forCloudStates(CLOUD_STATES, () => tracedForWipe('half-sun-upper', HALF_SUN_UPPER), coveredIn), { 'half-sun-upper': HALF_SUN_UPPER }),
680
+ // the open clouds' lower left end
681
+ sparePiece(forCloudStates(Object.keys(CLOUD_LEFT_ENDS), () => RIDING_IN, (state) => tracedForWipe(`${state}-cloud-left-end`, CLOUD_LEFT_ENDS[state])), Object.fromEntries(Object.entries(CLOUD_LEFT_ENDS).map(([state, end]) => [`${state}-cloud-left-end`, end]))),
682
+ ];
683
+ return {
684
+ ...cloud,
685
+ morePieces: [
686
+ sun, ...rays, precipitation(rain1), precipitation(rain2), boltWithSpine, precipitation(rain3),
687
+ ...bottomFlakes.map(precipitation), ...spares,
688
+ ],
689
+ };
690
+ };
691
+ export const iconsWeather = withFrosty(rainTwinnedIcons);