morphico 0.2.0 → 0.3.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.
- package/README.md +37 -3
- package/dist/morphico.js +711 -5
- package/dist/morphico.mjs +711 -5
- package/dist/types/clip.d.ts +55 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/morph.d.ts +5 -1
- package/dist/types/sets/shapes.d.ts +5 -0
- package/package.json +1 -1
package/dist/morphico.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! morphico v0.
|
|
1
|
+
/*! morphico v0.3.0 | code: MIT License, Copyright (c) 2026 Raycho Ivanov (see LICENSE)
|
|
2
2
|
* Icons based on Lucide (https://lucide.dev), some of them from Feather (https://feathericons.com):
|
|
3
3
|
*
|
|
4
4
|
* ISC License
|
|
@@ -3015,7 +3015,7 @@ return { iconSets, resolveIconSet };
|
|
|
3015
3015
|
})();
|
|
3016
3016
|
|
|
3017
3017
|
// morph.ts
|
|
3018
|
-
const { EMPTY_STATE, hasState, createMorphingPath } = (() => {
|
|
3018
|
+
const { EMPTY_STATE, DEFAULT_DURATION, shapesOf, hasState, prefersReducedMotion, clampToUnit, createMorphingPath } = (() => {
|
|
3019
3019
|
// One SVG <path> that morphs between the states of an icon set. Shared by icon() and every text() cell.
|
|
3020
3020
|
/** The empty state: nothing is drawn. Shapes grow out of it and shrink into it while fading. */
|
|
3021
3021
|
const EMPTY_STATE = ' ';
|
|
@@ -3120,7 +3120,438 @@ const createMorphingPath = (pathElement, iconSet, initialState) => {
|
|
|
3120
3120
|
stop: () => finish(true),
|
|
3121
3121
|
};
|
|
3122
3122
|
};
|
|
3123
|
-
return { EMPTY_STATE, hasState, createMorphingPath };
|
|
3123
|
+
return { EMPTY_STATE, DEFAULT_DURATION, shapesOf, hasState, prefersReducedMotion, clampToUnit, createMorphingPath };
|
|
3124
|
+
})();
|
|
3125
|
+
|
|
3126
|
+
// sets/shapes.ts
|
|
3127
|
+
const { lucideShapeOutlines, clipShapes } = (() => {
|
|
3128
|
+
// Shapes for clip(): closed outlines that cut out an element. From Lucide 1.61.0 (ISC, see LICENSE-lucide), imported
|
|
3129
|
+
// by `node tools/import-lucide.ts <name>`; tag without its dot. The states are the Lucide outlines made comparable for
|
|
3130
|
+
// morphing: clockwise, starting where the outline crosses the vertical middle at the top, scaled evenly to fill the
|
|
3131
|
+
// 24×24 box and centred (computed on first use, not at import). Roundness 1 = Lucide's own corners … 0 = sharp.
|
|
3132
|
+
const DEGREE = Math.PI / 180;
|
|
3133
|
+
const GRID_SIZE = 24;
|
|
3134
|
+
const GRID_CENTER = 12;
|
|
3135
|
+
const SAMPLES_PER_STROKE = 48;
|
|
3136
|
+
/** Enough for the shape that needs the most (puzzle: 24 strokes, its knobs ≈ 40 segments); the rest spreads by length. */
|
|
3137
|
+
const SEGMENT_COUNT = 48;
|
|
3138
|
+
/** The shapes as Lucide draws them (Lucide coordinates); the set's states are made from these. */
|
|
3139
|
+
const lucideShapeOutlines = {
|
|
3140
|
+
circle: [arc(12, 12, 10, -90, 270)],
|
|
3141
|
+
ellipse: ellipse(12, 12, 10, 6),
|
|
3142
|
+
heart: [
|
|
3143
|
+
arc(7.5, 9.5001, 5.5, -179.9989, -41.9423),
|
|
3144
|
+
arc(12, 5.4415, 0.56, 136.9163, 43.0837),
|
|
3145
|
+
arc(16.5101, 9.4738, 5.49, -138.3317, 0.2729),
|
|
3146
|
+
cubic(22, 9.5, 22, 11.79, 20.5, 13.5, 19, 15),
|
|
3147
|
+
line(19, 15, 13.508, 20.313),
|
|
3148
|
+
arc(11.9996, 18.9997, 2, 41.0455, 138.2288),
|
|
3149
|
+
line(10.508, 20.332, 5, 15),
|
|
3150
|
+
cubic(5, 15, 3.5, 13.5, 2, 11.8, 2, 9.5),
|
|
3151
|
+
],
|
|
3152
|
+
sparkle: [
|
|
3153
|
+
arc(12, 2.9976, 1, -169.4202, -10.5798),
|
|
3154
|
+
line(12.983, 2.814, 14.034, 8.372),
|
|
3155
|
+
arc(15.9992, 8.0008, 2, 169.3026, 100.6974),
|
|
3156
|
+
line(15.628, 9.966, 21.186, 11.017),
|
|
3157
|
+
arc(21.0024, 12, 1, -79.4202, 79.4202),
|
|
3158
|
+
line(21.186, 12.983, 15.628, 14.034),
|
|
3159
|
+
arc(15.9992, 15.9992, 2, -100.6974, -169.3026),
|
|
3160
|
+
line(14.034, 15.628, 12.983, 21.186),
|
|
3161
|
+
arc(12, 21.0024, 1, 10.5798, 169.4202),
|
|
3162
|
+
line(11.017, 21.186, 9.966, 15.628),
|
|
3163
|
+
arc(8.0008, 15.9992, 2, -10.6974, -79.3026),
|
|
3164
|
+
line(8.372, 14.034, 2.814, 12.983),
|
|
3165
|
+
arc(2.9976, 12, 1, 100.5798, 259.4202),
|
|
3166
|
+
line(2.814, 11.017, 8.372, 9.966),
|
|
3167
|
+
arc(8.0008, 8.0008, 2, 79.3026, 10.6974),
|
|
3168
|
+
line(9.966, 8.372, 11.017, 2.814),
|
|
3169
|
+
],
|
|
3170
|
+
astroid: [
|
|
3171
|
+
arc(12, 21.0024, 1, 10.5798, 169.4202),
|
|
3172
|
+
arc(1.1555, 22.8445, 10, -9.5467, -80.4533),
|
|
3173
|
+
arc(2.9976, 12, 1, 100.5798, 259.4202),
|
|
3174
|
+
arc(1.1555, 1.1555, 10, 80.4533, 9.5467),
|
|
3175
|
+
arc(12, 2.9976, 1, -169.4202, -10.5798),
|
|
3176
|
+
arc(22.8445, 1.1555, 10, 170.4533, 99.5467),
|
|
3177
|
+
arc(21.0024, 12, 1, -79.4202, 79.4202),
|
|
3178
|
+
arc(22.8445, 22.8445, 10, -99.5467, -170.4533),
|
|
3179
|
+
],
|
|
3180
|
+
bookmark: [
|
|
3181
|
+
arc(17, 5, 2, -90, 0),
|
|
3182
|
+
line(19, 5, 19, 20),
|
|
3183
|
+
arc(18, 19.9997, 1, 0.0185, 119.7357),
|
|
3184
|
+
line(17.504, 20.868, 12.992, 18.29),
|
|
3185
|
+
arc(12, 20.0266, 2, -60.2643, -119.7357),
|
|
3186
|
+
line(11.008, 18.29, 6.496, 20.868),
|
|
3187
|
+
arc(6, 19.9997, 1, 60.2643, 179.9815),
|
|
3188
|
+
line(5, 20, 5, 5),
|
|
3189
|
+
arc(7, 5, 2, 180, 270),
|
|
3190
|
+
line(7, 3, 17, 3),
|
|
3191
|
+
],
|
|
3192
|
+
shield: [
|
|
3193
|
+
cubic(20, 13, 20, 18, 16.5, 20.5, 12.34, 21.95),
|
|
3194
|
+
arc(12.0191, 21.0029, 1, 71.2803, 110.4299),
|
|
3195
|
+
cubic(11.67, 21.94, 7.5, 20.5, 4, 18, 4, 13),
|
|
3196
|
+
line(4, 13, 4, 6),
|
|
3197
|
+
arc(5, 6, 1, 180, 270),
|
|
3198
|
+
cubic(5, 5, 7, 5, 9.5, 3.8, 11.24, 2.28),
|
|
3199
|
+
arc(12, 3.1696, 1.17, -130.5094, -49.4906),
|
|
3200
|
+
cubic(12.76, 2.28, 14.51, 3.81, 17, 5, 19, 5),
|
|
3201
|
+
arc(19, 6, 1, -90, 0),
|
|
3202
|
+
line(20, 6, 20, 13),
|
|
3203
|
+
],
|
|
3204
|
+
moon: [
|
|
3205
|
+
arc(11.9981, 12.0009, 9, 3.09, 266.904),
|
|
3206
|
+
cubic(11.512, 3.014, 11.917, 2.992, 12.129, 3.474, 11.914, 3.817),
|
|
3207
|
+
arc(17.002, 6.997, 6, -147.9941, -302.0059),
|
|
3208
|
+
cubic(20.182, 12.085, 20.526, 11.87, 21.007, 12.081, 20.985, 12.486),
|
|
3209
|
+
],
|
|
3210
|
+
tag: [
|
|
3211
|
+
arc(11.1716, 4, 2, -44.9913, -89.9878),
|
|
3212
|
+
line(11.172, 2, 4, 2),
|
|
3213
|
+
arc(4, 4, 2, -90, -180),
|
|
3214
|
+
line(2, 4, 2, 11.172),
|
|
3215
|
+
arc(4, 11.1716, 2, 179.9878, 134.9913),
|
|
3216
|
+
line(2.586, 12.586, 11.29, 21.29),
|
|
3217
|
+
arc(13, 19.5691, 2.426, 134.8186, 45.1814),
|
|
3218
|
+
line(14.71, 21.29, 21.29, 14.71),
|
|
3219
|
+
arc(19.5691, 13, 2.426, 44.8186, -44.8186),
|
|
3220
|
+
line(21.29, 11.29, 12.586, 2.586),
|
|
3221
|
+
],
|
|
3222
|
+
square: [
|
|
3223
|
+
line(5, 3, 19, 3),
|
|
3224
|
+
arc(19, 5, 2, -90, 0),
|
|
3225
|
+
line(21, 5, 21, 19),
|
|
3226
|
+
arc(19, 19, 2, 0, 90),
|
|
3227
|
+
line(19, 21, 5, 21),
|
|
3228
|
+
arc(5, 19, 2, 90, 180),
|
|
3229
|
+
line(3, 19, 3, 5),
|
|
3230
|
+
arc(5, 5, 2, 180, 270),
|
|
3231
|
+
],
|
|
3232
|
+
badge: [
|
|
3233
|
+
arc(7.7549, 7.7531, 4, 167.4834, 282.6366),
|
|
3234
|
+
arc(12, 6.0048, 4, -147.4051, -32.5949),
|
|
3235
|
+
arc(16.2474, 7.7526, 4, -102.6711, 12.6711),
|
|
3236
|
+
arc(17.9952, 12, 4, -57.4051, 57.4051),
|
|
3237
|
+
arc(16.2469, 16.2451, 4, -12.6366, 102.5166),
|
|
3238
|
+
arc(12.005, 18.0031, 4, 32.4617, 147.5383),
|
|
3239
|
+
arc(7.7549, 16.2469, 4, 77.3634, 192.5166),
|
|
3240
|
+
arc(5.9891, 12, 4, 122.3281, 237.6719),
|
|
3241
|
+
],
|
|
3242
|
+
triangle: [
|
|
3243
|
+
arc(12, 5.0035, 2, -30.1173, -149.8827),
|
|
3244
|
+
line(10.27, 4, 2.27, 18),
|
|
3245
|
+
arc(4.0021, 19, 2, -150, -269.9413),
|
|
3246
|
+
line(4, 21, 20, 21),
|
|
3247
|
+
arc(19.9979, 19, 2, 89.9413, -30),
|
|
3248
|
+
line(21.73, 18, 13.73, 4),
|
|
3249
|
+
],
|
|
3250
|
+
droplet: [
|
|
3251
|
+
arc(12, 15, 7, 90, 0),
|
|
3252
|
+
cubic(19, 15, 19, 13, 18, 11.1, 16, 9.5),
|
|
3253
|
+
cubic(16, 9.5, 14, 7.9, 12.5, 5.5, 12, 3),
|
|
3254
|
+
cubic(12, 3, 11.5, 5.5, 10, 7.9, 8, 9.5),
|
|
3255
|
+
cubic(8, 9.5, 6, 11.1, 5, 13, 5, 15),
|
|
3256
|
+
arc(12, 15, 7, 180, 90),
|
|
3257
|
+
],
|
|
3258
|
+
pentagon: [
|
|
3259
|
+
arc(12, 4.0021, 2, -125.803, -54.197),
|
|
3260
|
+
line(13.17, 2.38, 21.17, 8.12),
|
|
3261
|
+
arc(20.0009, 9.7427, 2, -54.2293, 18.2786),
|
|
3262
|
+
line(21.9, 10.37, 18.86, 19.63),
|
|
3263
|
+
arc(16.9618, 19, 2, 18.3608, 90.052),
|
|
3264
|
+
line(16.96, 21, 7.04, 21),
|
|
3265
|
+
arc(7.0382, 19, 2, 89.948, 161.6392),
|
|
3266
|
+
line(5.14, 19.63, 2.1, 10.37),
|
|
3267
|
+
arc(3.9991, 9.7427, 2, 161.7214, 234.2293),
|
|
3268
|
+
line(2.83, 8.12, 10.83, 2.38),
|
|
3269
|
+
],
|
|
3270
|
+
hexagon: [
|
|
3271
|
+
line(21, 16, 21, 8),
|
|
3272
|
+
arc(19, 8.0021, 2, -0.0588, -60),
|
|
3273
|
+
line(20, 6.27, 13, 2.27),
|
|
3274
|
+
arc(12, 4.0021, 2, -60, -120),
|
|
3275
|
+
line(11, 2.27, 4, 6.27),
|
|
3276
|
+
arc(5, 8.0021, 2, -120, -179.9412),
|
|
3277
|
+
line(3, 8, 3, 16),
|
|
3278
|
+
arc(5, 15.9979, 2, 179.9412, 120),
|
|
3279
|
+
line(4, 17.73, 11, 21.73),
|
|
3280
|
+
arc(12, 19.9979, 2, 120, 60),
|
|
3281
|
+
line(13, 21.73, 20, 17.73),
|
|
3282
|
+
arc(19, 15.9979, 2, 60, 0.0588),
|
|
3283
|
+
],
|
|
3284
|
+
octagon: [
|
|
3285
|
+
arc(4, 15.3116, 2, 134.9913, 179.9878),
|
|
3286
|
+
line(2, 15.312, 2, 8.688),
|
|
3287
|
+
arc(4, 8.6884, 2, -179.9878, -134.9913),
|
|
3288
|
+
line(2.586, 7.274, 7.274, 2.586),
|
|
3289
|
+
arc(8.6884, 4, 2, -135.0087, -90.0122),
|
|
3290
|
+
line(8.688, 2, 15.312, 2),
|
|
3291
|
+
arc(15.3116, 4, 2, -89.9878, -44.9913),
|
|
3292
|
+
line(16.726, 2.586, 21.414, 7.274),
|
|
3293
|
+
arc(20, 8.6884, 2, -45.0087, -0.0122),
|
|
3294
|
+
line(22, 8.688, 22, 15.312),
|
|
3295
|
+
arc(20, 15.3116, 2, 0.0122, 45.0087),
|
|
3296
|
+
line(21.414, 16.726, 16.726, 21.414),
|
|
3297
|
+
arc(15.3116, 20, 2, 44.9913, 89.9878),
|
|
3298
|
+
line(15.312, 22, 8.688, 22),
|
|
3299
|
+
arc(8.6884, 20, 2, 90.0122, 135.0087),
|
|
3300
|
+
line(7.274, 21.414, 2.586, 16.726),
|
|
3301
|
+
],
|
|
3302
|
+
squircle: [
|
|
3303
|
+
cubic(12, 3, 19.2, 3, 21, 4.8, 21, 12),
|
|
3304
|
+
cubic(21, 12, 21, 19.2, 19.2, 21, 12, 21),
|
|
3305
|
+
cubic(12, 21, 4.8, 21, 3, 19.2, 3, 12),
|
|
3306
|
+
cubic(3, 12, 3, 4.8, 4.8, 3, 12, 3),
|
|
3307
|
+
],
|
|
3308
|
+
star: [
|
|
3309
|
+
arc(12, 2.5301, 0.53, -153.6664, -26.3336),
|
|
3310
|
+
line(12.475, 2.295, 14.785, 6.974),
|
|
3311
|
+
arc(16.6883, 6.0335, 2.123, 153.7044, 98.3503),
|
|
3312
|
+
line(16.38, 8.134, 21.546, 8.89),
|
|
3313
|
+
arc(21.47, 9.4145, 0.53, -81.756, 45.7247),
|
|
3314
|
+
line(21.84, 9.794, 18.104, 13.432),
|
|
3315
|
+
arc(19.5857, 14.9525, 2.123, -134.26, -189.6958),
|
|
3316
|
+
line(17.493, 15.31, 18.375, 20.45),
|
|
3317
|
+
arc(17.8531, 20.5422, 0.53, -10.0155, 118.0314),
|
|
3318
|
+
line(17.604, 21.01, 12.986, 18.582),
|
|
3319
|
+
arc(11.9995, 20.4608, 2.122, -62.2968, -117.7032),
|
|
3320
|
+
line(11.013, 18.582, 6.396, 21.01),
|
|
3321
|
+
arc(6.148, 20.5416, 0.53, 62.1036, 189.9512),
|
|
3322
|
+
line(5.626, 20.45, 6.507, 15.311),
|
|
3323
|
+
arc(4.4155, 14.9522, 2.122, 9.7335, -45.7597),
|
|
3324
|
+
line(5.896, 13.432, 2.16, 9.795),
|
|
3325
|
+
arc(2.5282, 9.4138, 0.53, 134.0047, 261.9521),
|
|
3326
|
+
line(2.454, 8.889, 7.619, 8.134),
|
|
3327
|
+
arc(7.3135, 6.0341, 2.122, 81.7227, 26.2909),
|
|
3328
|
+
line(9.216, 6.974, 11.525, 2.295),
|
|
3329
|
+
],
|
|
3330
|
+
'rectangle-horizontal': [
|
|
3331
|
+
line(4, 6, 20, 6),
|
|
3332
|
+
arc(20, 8, 2, -90, 0),
|
|
3333
|
+
line(22, 8, 22, 16),
|
|
3334
|
+
arc(20, 16, 2, 0, 90),
|
|
3335
|
+
line(20, 18, 4, 18),
|
|
3336
|
+
arc(4, 16, 2, 90, 180),
|
|
3337
|
+
line(2, 16, 2, 8),
|
|
3338
|
+
arc(4, 8, 2, 180, 270),
|
|
3339
|
+
],
|
|
3340
|
+
'rectangle-vertical': [
|
|
3341
|
+
line(8, 2, 16, 2),
|
|
3342
|
+
arc(16, 4, 2, -90, 0),
|
|
3343
|
+
line(18, 4, 18, 20),
|
|
3344
|
+
arc(16, 20, 2, 0, 90),
|
|
3345
|
+
line(16, 22, 8, 22),
|
|
3346
|
+
arc(8, 20, 2, 90, 180),
|
|
3347
|
+
line(6, 20, 6, 4),
|
|
3348
|
+
arc(8, 4, 2, 180, 270),
|
|
3349
|
+
],
|
|
3350
|
+
'message-circle': [
|
|
3351
|
+
arc(1.1328, 17.079, 2, -21.6245, 12.4142),
|
|
3352
|
+
line(3.086, 17.509, 2.021, 20.799),
|
|
3353
|
+
arc(3.0005, 21.0005, 1, -168.3779, -284.8626),
|
|
3354
|
+
line(3.257, 21.967, 6.67, 20.969),
|
|
3355
|
+
arc(7.0591, 22.9308, 2, -101.2192, -69.2104),
|
|
3356
|
+
arc(12.0003, 12.0003, 10, 115.0325, -205.7324),
|
|
3357
|
+
],
|
|
3358
|
+
'message-square': [
|
|
3359
|
+
arc(20, 17, 2, 0, 90),
|
|
3360
|
+
line(20, 19, 6.828, 19),
|
|
3361
|
+
arc(6.8284, 21, 2, -90.0122, -135.0087),
|
|
3362
|
+
line(5.414, 19.586, 3.212, 21.788),
|
|
3363
|
+
arc(2.71, 21.2859, 0.71, 45.0052, 179.9926),
|
|
3364
|
+
line(2, 21.286, 2, 5),
|
|
3365
|
+
arc(4, 5, 2, 180, 270),
|
|
3366
|
+
line(4, 3, 20, 3),
|
|
3367
|
+
arc(20, 5, 2, -90, 0),
|
|
3368
|
+
line(22, 5, 22, 17),
|
|
3369
|
+
],
|
|
3370
|
+
diamond: [
|
|
3371
|
+
arc(4.4033, 12.005, 2.41, -134.9707, -225.0293),
|
|
3372
|
+
line(2.7, 13.71, 10.29, 21.3),
|
|
3373
|
+
arc(11.995, 19.5967, 2.41, 135.0293, 44.9707),
|
|
3374
|
+
line(13.7, 21.3, 21.29, 13.71),
|
|
3375
|
+
arc(19.5867, 12.005, 2.41, 45.0293, -45.0293),
|
|
3376
|
+
line(21.29, 10.3, 13.7, 2.71),
|
|
3377
|
+
arc(11.995, 4.4133, 2.41, -44.9707, -135.0293),
|
|
3378
|
+
line(10.29, 2.71, 2.7, 10.3),
|
|
3379
|
+
],
|
|
3380
|
+
cloud: [
|
|
3381
|
+
line(17.5, 19, 9, 19),
|
|
3382
|
+
arc(9.0018, 12, 7, 90.0147, 343.3984),
|
|
3383
|
+
line(15.71, 10, 17.5, 10),
|
|
3384
|
+
arc(17.5, 14.5, 4.5, -90, 90),
|
|
3385
|
+
],
|
|
3386
|
+
puzzle: [
|
|
3387
|
+
arc(16.0975, 3.6833, 1, 135.0289, 13.459),
|
|
3388
|
+
arc(19.5008, 4.5, 2.5, -166.4917, 76.5107),
|
|
3389
|
+
arc(20.3167, 7.9035, 1, -103.459, -225.0289),
|
|
3390
|
+
line(19.61, 8.611, 21.293, 10.293),
|
|
3391
|
+
arc(19.5861, 12, 2.414, -45.0015, 45.0015),
|
|
3392
|
+
line(21.293, 13.707, 19.61, 15.39),
|
|
3393
|
+
arc(18.9025, 14.6833, 1, 44.9711, 166.541),
|
|
3394
|
+
arc(15.4992, 15.5, 2.5, -13.5083, -256.5107),
|
|
3395
|
+
arc(14.6833, 18.9035, 1, -76.541, 45.0289),
|
|
3396
|
+
line(15.39, 19.611, 13.707, 21.293),
|
|
3397
|
+
arc(12, 19.5861, 2.414, 44.9985, 135.0015),
|
|
3398
|
+
line(10.293, 21.293, 8.61, 19.61),
|
|
3399
|
+
arc(7.9025, 20.3167, 1, -44.9711, -166.541),
|
|
3400
|
+
arc(4.4992, 19.5, 2.5, 13.5083, 256.5107),
|
|
3401
|
+
arc(3.6833, 16.0965, 1, 76.541, -45.0289),
|
|
3402
|
+
line(4.39, 15.389, 2.707, 13.707),
|
|
3403
|
+
arc(4.4139, 12, 2.414, 134.9985, 225.0015),
|
|
3404
|
+
line(2.707, 10.293, 4.39, 8.61),
|
|
3405
|
+
arc(5.0975, 9.3167, 1, -135.0289, -13.459),
|
|
3406
|
+
arc(8.5008, 8.5, 2.5, 166.4917, -76.5107),
|
|
3407
|
+
arc(9.3167, 5.0965, 1, 103.459, 225.0289),
|
|
3408
|
+
line(8.61, 4.389, 10.293, 2.707),
|
|
3409
|
+
arc(12, 4.4139, 2.414, -135.0015, -44.9985),
|
|
3410
|
+
line(13.707, 2.707, 15.39, 4.39),
|
|
3411
|
+
],
|
|
3412
|
+
};
|
|
3413
|
+
// ---- making the outlines comparable ----
|
|
3414
|
+
const pointOnStroke = (stroke, progress) => strokePiece(stroke, 0, progress)[3];
|
|
3415
|
+
const samplesOf = (strokes) => strokes.flatMap((stroke, strokeIndex) => Array.from({ length: SAMPLES_PER_STROKE }, (_, sample) => {
|
|
3416
|
+
const progress = sample / SAMPLES_PER_STROKE;
|
|
3417
|
+
return { point: pointOnStroke(stroke, progress), strokeIndex, progress };
|
|
3418
|
+
}));
|
|
3419
|
+
const transformStroke = (stroke, scale, offsetX, offsetY) => {
|
|
3420
|
+
const move = ([x, y]) => [x * scale + offsetX, y * scale + offsetY];
|
|
3421
|
+
switch (stroke.kind) {
|
|
3422
|
+
case 'line': return { ...stroke, from: move(stroke.from), to: move(stroke.to) };
|
|
3423
|
+
case 'arc': return { ...stroke, center: move(stroke.center), radius: stroke.radius * scale };
|
|
3424
|
+
case 'cubic': return { kind: 'cubic', segment: stroke.segment.map(move) };
|
|
3425
|
+
}
|
|
3426
|
+
};
|
|
3427
|
+
/** Splits a stroke at `progress` into two strokes of the same kind (arcs stay arcs, so they keep their segment rule). */
|
|
3428
|
+
const splitStroke = (stroke, progress) => {
|
|
3429
|
+
if (stroke.kind === 'arc') {
|
|
3430
|
+
const splitAngle = stroke.startAngle + (stroke.endAngle - stroke.startAngle) * progress;
|
|
3431
|
+
return [{ ...stroke, endAngle: splitAngle }, { ...stroke, startAngle: splitAngle }];
|
|
3432
|
+
}
|
|
3433
|
+
if (stroke.kind === 'line') {
|
|
3434
|
+
const splitPoint = pointOnStroke(stroke, progress);
|
|
3435
|
+
return [{ ...stroke, to: splitPoint }, { ...stroke, from: splitPoint }];
|
|
3436
|
+
}
|
|
3437
|
+
return [{ kind: 'cubic', segment: strokePiece(stroke, 0, progress) }, { kind: 'cubic', segment: strokePiece(stroke, progress, 1) }];
|
|
3438
|
+
};
|
|
3439
|
+
const fitToBox = (strokes) => {
|
|
3440
|
+
const points = samplesOf(strokes).map(({ point }) => point);
|
|
3441
|
+
const xValues = points.map(([x]) => x), yValues = points.map(([, y]) => y);
|
|
3442
|
+
const minX = Math.min(...xValues), maxX = Math.max(...xValues), minY = Math.min(...yValues), maxY = Math.max(...yValues);
|
|
3443
|
+
const scale = GRID_SIZE / Math.max(maxX - minX, maxY - minY);
|
|
3444
|
+
return strokes.map((stroke) => transformStroke(stroke, scale, GRID_CENTER - ((minX + maxX) / 2) * scale, GRID_CENTER - ((minY + maxY) / 2) * scale));
|
|
3445
|
+
};
|
|
3446
|
+
/** Positive = clockwise on screen (y points down). */
|
|
3447
|
+
const signedArea = (strokes) => {
|
|
3448
|
+
const points = samplesOf(strokes).map(({ point }) => point);
|
|
3449
|
+
return points.reduce((sum, [x, y], index) => {
|
|
3450
|
+
const [nextX, nextY] = points[(index + 1) % points.length];
|
|
3451
|
+
return sum + x * nextY - nextX * y;
|
|
3452
|
+
}, 0);
|
|
3453
|
+
};
|
|
3454
|
+
const clockwise = (strokes) => (signedArea(strokes) >= 0 ? strokes : strokes.slice().reverse().map(reversedStroke));
|
|
3455
|
+
/** Starts the closed outline where it crosses x = 12 highest up (splitting that stroke). */
|
|
3456
|
+
const startAtTopMiddle = (strokes) => {
|
|
3457
|
+
const samples = samplesOf(strokes);
|
|
3458
|
+
let best = null;
|
|
3459
|
+
for (let index = 0; index < samples.length; index++) {
|
|
3460
|
+
const sample = samples[index], next = samples[(index + 1) % samples.length];
|
|
3461
|
+
const [x, y] = sample.point, [nextX, nextY] = next.point;
|
|
3462
|
+
if ((x - GRID_CENTER) * (nextX - GRID_CENTER) > 0 || x === nextX)
|
|
3463
|
+
continue;
|
|
3464
|
+
const fraction = (GRID_CENTER - x) / (nextX - x);
|
|
3465
|
+
const crossingY = y + (nextY - y) * fraction;
|
|
3466
|
+
if (best && crossingY >= best.y)
|
|
3467
|
+
continue;
|
|
3468
|
+
const nextProgress = next.strokeIndex === sample.strokeIndex ? next.progress : 1;
|
|
3469
|
+
best = { strokeIndex: sample.strokeIndex, progress: sample.progress + (nextProgress - sample.progress) * fraction, y: crossingY };
|
|
3470
|
+
}
|
|
3471
|
+
if (!best)
|
|
3472
|
+
return strokes;
|
|
3473
|
+
const { strokeIndex, progress } = best;
|
|
3474
|
+
const before = strokes.slice(0, strokeIndex), after = strokes.slice(strokeIndex + 1);
|
|
3475
|
+
if (progress < 1e-6)
|
|
3476
|
+
return [...strokes.slice(strokeIndex), ...before];
|
|
3477
|
+
if (progress > 1 - 1e-6)
|
|
3478
|
+
return [...after, ...before, strokes[strokeIndex]];
|
|
3479
|
+
const [firstPart, secondPart] = splitStroke(strokes[strokeIndex], progress);
|
|
3480
|
+
return [secondPart, ...after, ...before, firstPart];
|
|
3481
|
+
};
|
|
3482
|
+
// ---- roundness: 1 = Lucide's own corners, 0 = sharp ----
|
|
3483
|
+
/** Lucide's corner arcs are small (radius 0.56 … 2.43); bigger arcs are part of the shape (circle, moon, droplet). */
|
|
3484
|
+
const LARGEST_CORNER_RADIUS = 2.5;
|
|
3485
|
+
const crossingOfLines = (point, direction, otherPoint, otherDirection) => {
|
|
3486
|
+
const determinant = direction[0] * otherDirection[1] - direction[1] * otherDirection[0];
|
|
3487
|
+
if (Math.abs(determinant) < 1e-9)
|
|
3488
|
+
return null;
|
|
3489
|
+
const along = ((otherPoint[0] - point[0]) * otherDirection[1] - (otherPoint[1] - point[1]) * otherDirection[0]) / determinant;
|
|
3490
|
+
return [point[0] + direction[0] * along, point[1] + direction[1] * along];
|
|
3491
|
+
};
|
|
3492
|
+
/** Moves a stroke's start (or end) to `point`; a cubic's neighbouring control point moves along, so its direction there stays. */
|
|
3493
|
+
const withMovedEnd = (stroke, which, point) => {
|
|
3494
|
+
if (stroke.kind === 'line')
|
|
3495
|
+
return which === 'start' ? { ...stroke, from: point } : { ...stroke, to: point };
|
|
3496
|
+
if (stroke.kind !== 'cubic')
|
|
3497
|
+
return stroke;
|
|
3498
|
+
const [start, control1, control2, end] = stroke.segment;
|
|
3499
|
+
if (which === 'start') {
|
|
3500
|
+
const shift = [point[0] - start[0], point[1] - start[1]];
|
|
3501
|
+
return { kind: 'cubic', segment: [point, [control1[0] + shift[0], control1[1] + shift[1]], control2, end] };
|
|
3502
|
+
}
|
|
3503
|
+
const shift = [point[0] - end[0], point[1] - end[1]];
|
|
3504
|
+
return { kind: 'cubic', segment: [start, control1, [control2[0] + shift[0], control2[1] + shift[1]], point] };
|
|
3505
|
+
};
|
|
3506
|
+
/**
|
|
3507
|
+
* Every corner arc (small, between two lines / cubics) shrinks towards the sharp corner where its two neighbours'
|
|
3508
|
+
* directions meet: radius × roundness, angles unchanged. At 0 the arc is a point on the corner. Same strokes in the
|
|
3509
|
+
* same order at any roundness, so morphs keep working.
|
|
3510
|
+
*/
|
|
3511
|
+
const withCornerRoundness = (strokes, roundness) => {
|
|
3512
|
+
const result = strokes.slice();
|
|
3513
|
+
strokes.forEach((stroke, index) => {
|
|
3514
|
+
const previousIndex = (index - 1 + strokes.length) % strokes.length, nextIndex = (index + 1) % strokes.length;
|
|
3515
|
+
const isEdge = (neighbour) => neighbour.kind === 'line' || neighbour.kind === 'cubic';
|
|
3516
|
+
if (stroke.kind !== 'arc' || stroke.radius > LARGEST_CORNER_RADIUS)
|
|
3517
|
+
return;
|
|
3518
|
+
if (!isEdge(strokes[previousIndex]) || !isEdge(strokes[nextIndex]))
|
|
3519
|
+
return;
|
|
3520
|
+
const turning = Math.sign(stroke.endAngle - stroke.startAngle);
|
|
3521
|
+
const tangent = (angle) => [-Math.sin(angle * DEGREE) * turning, Math.cos(angle * DEGREE) * turning];
|
|
3522
|
+
const start = pointOnStroke(stroke, 0), end = pointOnStroke(stroke, 1);
|
|
3523
|
+
const corner = crossingOfLines(start, tangent(stroke.startAngle), end, tangent(stroke.endAngle));
|
|
3524
|
+
if (!corner)
|
|
3525
|
+
return;
|
|
3526
|
+
const towardsCorner = ([x, y]) => [corner[0] + (x - corner[0]) * roundness, corner[1] + (y - corner[1]) * roundness];
|
|
3527
|
+
result[index] = { ...stroke, center: towardsCorner(stroke.center), radius: stroke.radius * roundness };
|
|
3528
|
+
result[previousIndex] = withMovedEnd(result[previousIndex], 'end', towardsCorner(start));
|
|
3529
|
+
result[nextIndex] = withMovedEnd(result[nextIndex], 'start', towardsCorner(end));
|
|
3530
|
+
});
|
|
3531
|
+
return result;
|
|
3532
|
+
};
|
|
3533
|
+
/** The shape set at a roundness (0 = sharp … 1 = Lucide's own corners); its strokes are built on first use. */
|
|
3534
|
+
const shapeSetWithRoundness = (roundness) => {
|
|
3535
|
+
let strokes = null;
|
|
3536
|
+
return {
|
|
3537
|
+
get strokes() {
|
|
3538
|
+
strokes ??= Object.fromEntries(Object.entries(lucideShapeOutlines).map(([name, outline]) => [name, startAtTopMiddle(clockwise(fitToBox(withCornerRoundness(outline, roundness))))]));
|
|
3539
|
+
return strokes;
|
|
3540
|
+
},
|
|
3541
|
+
alternativeStrokes: {},
|
|
3542
|
+
pairLayouts: {},
|
|
3543
|
+
spins: {},
|
|
3544
|
+
segmentCount: SEGMENT_COUNT,
|
|
3545
|
+
// shapes are filled (clip), not stroked; these only matter for the outlined band's corners
|
|
3546
|
+
strokeWidth: 0,
|
|
3547
|
+
lineCap: 'butt',
|
|
3548
|
+
lineJoin: 'miter',
|
|
3549
|
+
withRoundness: shapeSetWithRoundness,
|
|
3550
|
+
};
|
|
3551
|
+
};
|
|
3552
|
+
/** The shape set of clip(): states `circle`, `ellipse`, `heart` … `puzzle` (see `lucideShapeOutlines`). */
|
|
3553
|
+
const clipShapes = shapeSetWithRoundness(1);
|
|
3554
|
+
return { lucideShapeOutlines, clipShapes };
|
|
3124
3555
|
})();
|
|
3125
3556
|
|
|
3126
3557
|
// svg.ts
|
|
@@ -3158,6 +3589,281 @@ const createStrokedElement = (tagName, style, attributes = {}) => createSvgEleme
|
|
|
3158
3589
|
return { hasFixedStrokeWidth, createSvgElement, createIconSvg, createStrokedElement };
|
|
3159
3590
|
})();
|
|
3160
3591
|
|
|
3592
|
+
// clip.ts
|
|
3593
|
+
const { clip } = (() => {
|
|
3594
|
+
// Layer 3: an element cut out by a morphing shape (a star, a heart, a speech bubble …), e.g. a photo or a card.
|
|
3595
|
+
// The shape is square (1:1), placed by its centre (x, y) and size in % or px of the element, can be turned and
|
|
3596
|
+
// outlined (only a band along its edge shows). Filled → CSS `clip-path: url(#id)`; outlined → `mask: url(#id)`
|
|
3597
|
+
// (clip-path ignores line width). The <clipPath> / <mask> live in one hidden <svg> per clip.
|
|
3598
|
+
const GRID_SIZE = 24;
|
|
3599
|
+
const GRID_CENTER = 12;
|
|
3600
|
+
const DEGREE = Math.PI / 180;
|
|
3601
|
+
/** Masks and their clip regions reach far past the element, so bands and turned shapes are never cut by them. */
|
|
3602
|
+
const FAR = 100000;
|
|
3603
|
+
let clipCount = 0;
|
|
3604
|
+
const lengthToPixels = (length, reference, name) => {
|
|
3605
|
+
if (typeof length === 'number' && Number.isFinite(length))
|
|
3606
|
+
return (length / 100) * reference;
|
|
3607
|
+
const match = typeof length === 'string' ? /^\s*(-?[\d.]+(?:e[-+]?\d+)?)\s*(px|%)\s*$/i.exec(length) : null;
|
|
3608
|
+
if (!match)
|
|
3609
|
+
throw new Error(`morphico: ${name} "${length}" is not a length — use a number (= %), '40%' or '120px'`);
|
|
3610
|
+
const value = Number(match[1]);
|
|
3611
|
+
return match[2] === 'px' ? value : (value / 100) * reference;
|
|
3612
|
+
};
|
|
3613
|
+
const checkedOutline = (outline) => {
|
|
3614
|
+
if (!outline)
|
|
3615
|
+
return false;
|
|
3616
|
+
if (!(outline.width > 0))
|
|
3617
|
+
throw new Error('morphico: outline.width must be a number of px above 0');
|
|
3618
|
+
if (outline.band && !['inside', 'centre', 'outside'].includes(outline.band)) {
|
|
3619
|
+
throw new Error(`morphico: outline.band "${outline.band}" — use 'inside', 'centre' or 'outside'`);
|
|
3620
|
+
}
|
|
3621
|
+
return outline;
|
|
3622
|
+
};
|
|
3623
|
+
/** Cuts `element` out by a morphing shape. */
|
|
3624
|
+
const clip = (element, options = {}) => {
|
|
3625
|
+
const { set = clipShapes, state: initialState = 'circle', x = 50, y = 50, size, rotation = 0, roundness = 1, outline = false, ...defaultMorphOptions } = options;
|
|
3626
|
+
const checkedState = (shapeSet, state) => {
|
|
3627
|
+
if (!(state in shapeSet.strokes)) {
|
|
3628
|
+
throw new Error(`morphico: unknown shape "${state}" (known: ${Object.keys(shapeSet.strokes).join(', ')})`);
|
|
3629
|
+
}
|
|
3630
|
+
return state;
|
|
3631
|
+
};
|
|
3632
|
+
for (const [name, length] of [['x', x], ['y', y], ['size', size]]) {
|
|
3633
|
+
if (length !== undefined)
|
|
3634
|
+
lengthToPixels(length, 1, name);
|
|
3635
|
+
}
|
|
3636
|
+
/** Where the change goes (or what shows when nothing moves). */
|
|
3637
|
+
const target = {
|
|
3638
|
+
state: checkedState(set, initialState), set: resolveIconSet(set, roundness), roundness,
|
|
3639
|
+
x, y, size, rotation, outline: checkedOutline(outline),
|
|
3640
|
+
};
|
|
3641
|
+
const pieceLength = pieceLengthOf(set);
|
|
3642
|
+
let frame = null; // null = resting at the target
|
|
3643
|
+
let running = null;
|
|
3644
|
+
let elementWidth = 0, elementHeight = 0;
|
|
3645
|
+
// ---- the hidden <svg> with the <clipPath> (filled) and the <mask> (outlined) ----
|
|
3646
|
+
const id = `morphico-clip-${++clipCount}`;
|
|
3647
|
+
const svg = createSvgElement('svg', { width: 0, height: 0, 'aria-hidden': 'true', focusable: 'false' });
|
|
3648
|
+
svg.style.position = 'absolute';
|
|
3649
|
+
svg.style.pointerEvents = 'none';
|
|
3650
|
+
const clipPathElement = svg.appendChild(createSvgElement('clipPath', { id: `${id}-shape`, clipPathUnits: 'objectBoundingBox' }));
|
|
3651
|
+
const clipShape = clipPathElement.appendChild(createSvgElement('path'));
|
|
3652
|
+
const farRegion = { maskUnits: 'userSpaceOnUse', x: -FAR, y: -FAR, width: 2 * FAR, height: 2 * FAR };
|
|
3653
|
+
const maskElement = svg.appendChild(createSvgElement('mask', { id: `${id}-band`, ...farRegion }));
|
|
3654
|
+
/** Every path drawn in element pixels for the outlined band (the band itself and its helpers). */
|
|
3655
|
+
let bandPaths = [];
|
|
3656
|
+
const buildBand = () => {
|
|
3657
|
+
for (const helper of svg.querySelectorAll('.morphico-band-helper'))
|
|
3658
|
+
helper.remove();
|
|
3659
|
+
maskElement.replaceChildren();
|
|
3660
|
+
bandPaths = [];
|
|
3661
|
+
if (!target.outline)
|
|
3662
|
+
return;
|
|
3663
|
+
const { width, band = 'inside' } = target.outline;
|
|
3664
|
+
const pixelPath = (attributes) => {
|
|
3665
|
+
const path = createSvgElement('path', attributes);
|
|
3666
|
+
bandPaths.push(path);
|
|
3667
|
+
return path;
|
|
3668
|
+
};
|
|
3669
|
+
// inside = a line twice as wide, clipped to the shape; outside = a line twice as wide, the shape cut out of it
|
|
3670
|
+
const edgeLine = (lineWidth) => pixelPath({ fill: 'none', stroke: 'white', 'stroke-width': lineWidth, 'stroke-linejoin': 'miter' });
|
|
3671
|
+
if (band === 'centre') {
|
|
3672
|
+
maskElement.append(edgeLine(width));
|
|
3673
|
+
}
|
|
3674
|
+
else if (band === 'inside') {
|
|
3675
|
+
const insideClip = createSvgElement('clipPath', { id: `${id}-inside`, class: 'morphico-band-helper' });
|
|
3676
|
+
insideClip.append(pixelPath({}));
|
|
3677
|
+
svg.append(insideClip);
|
|
3678
|
+
const line = edgeLine(2 * width);
|
|
3679
|
+
line.setAttribute('clip-path', `url(#${id}-inside)`);
|
|
3680
|
+
maskElement.append(line);
|
|
3681
|
+
}
|
|
3682
|
+
else {
|
|
3683
|
+
// painting black over the inside would only work for luminance masks; a mask cut out of the line works for both
|
|
3684
|
+
const outsideMask = createSvgElement('mask', { id: `${id}-outside`, class: 'morphico-band-helper', ...farRegion });
|
|
3685
|
+
outsideMask.append(createSvgElement('rect', { x: -FAR, y: -FAR, width: 2 * FAR, height: 2 * FAR, fill: 'white' }), pixelPath({ fill: 'black' }));
|
|
3686
|
+
svg.append(outsideMask);
|
|
3687
|
+
const line = edgeLine(2 * width);
|
|
3688
|
+
line.setAttribute('mask', `url(#${id}-outside)`);
|
|
3689
|
+
maskElement.append(line);
|
|
3690
|
+
}
|
|
3691
|
+
};
|
|
3692
|
+
const ownClipPath = element.style.clipPath;
|
|
3693
|
+
const ownMask = element.style.mask;
|
|
3694
|
+
const applyStyle = () => {
|
|
3695
|
+
const mask = target.outline ? `url(#${id}-band)` : ownMask;
|
|
3696
|
+
element.style.clipPath = target.outline ? ownClipPath : `url(#${id}-shape)`;
|
|
3697
|
+
element.style.mask = mask;
|
|
3698
|
+
element.style.setProperty('-webkit-mask', mask);
|
|
3699
|
+
};
|
|
3700
|
+
// ---- placement and drawing ----
|
|
3701
|
+
const restingPlacement = () => ({
|
|
3702
|
+
centerX: lengthToPixels(target.x, elementWidth, 'x'),
|
|
3703
|
+
centerY: lengthToPixels(target.y, elementHeight, 'y'),
|
|
3704
|
+
side: target.size === undefined ? Math.min(elementWidth, elementHeight) : lengthToPixels(target.size, elementWidth, 'size'),
|
|
3705
|
+
});
|
|
3706
|
+
const restingFrame = () => ({ shape: shapesOf(target.set)[target.state], rotation: target.rotation, placement: restingPlacement() });
|
|
3707
|
+
/** The shape in element pixels: scaled to its square, turned around the square's centre, moved into place. */
|
|
3708
|
+
const toElementPixels = ({ shape, rotation: angle, placement: { centerX, centerY, side } }) => {
|
|
3709
|
+
const scale = side / GRID_SIZE;
|
|
3710
|
+
const cos = Math.cos(angle * DEGREE), sin = Math.sin(angle * DEGREE);
|
|
3711
|
+
const pixels = [];
|
|
3712
|
+
for (let index = 0; index < shape.length; index += 2) {
|
|
3713
|
+
const offsetX = (shape[index] - GRID_CENTER) * scale, offsetY = (shape[index + 1] - GRID_CENTER) * scale;
|
|
3714
|
+
pixels.push(centerX + offsetX * cos - offsetY * sin, centerY + offsetX * sin + offsetY * cos);
|
|
3715
|
+
}
|
|
3716
|
+
return pixels;
|
|
3717
|
+
};
|
|
3718
|
+
const render = () => {
|
|
3719
|
+
if (!elementWidth || !elementHeight)
|
|
3720
|
+
return;
|
|
3721
|
+
const pixels = toElementPixels(frame ?? restingFrame());
|
|
3722
|
+
if (target.outline) {
|
|
3723
|
+
const pathData = toPathData(pixels, pieceLength);
|
|
3724
|
+
for (const path of bandPaths)
|
|
3725
|
+
path.setAttribute('d', pathData);
|
|
3726
|
+
}
|
|
3727
|
+
else {
|
|
3728
|
+
// objectBoundingBox: 0…1 of the element's width / height
|
|
3729
|
+
const boxUnits = pixels.map((value, index) => value / (index % 2 ? elementHeight : elementWidth));
|
|
3730
|
+
clipShape.setAttribute('d', toPathData(boxUnits, pieceLength));
|
|
3731
|
+
}
|
|
3732
|
+
};
|
|
3733
|
+
const measure = (width, height) => {
|
|
3734
|
+
elementWidth = width;
|
|
3735
|
+
elementHeight = height;
|
|
3736
|
+
render();
|
|
3737
|
+
};
|
|
3738
|
+
// ---- changes ----
|
|
3739
|
+
/** Ends the running change and resolves everyone waiting for it. */
|
|
3740
|
+
const finish = (interrupted) => {
|
|
3741
|
+
if (!running)
|
|
3742
|
+
return;
|
|
3743
|
+
const { stopAnimation, resolvers } = running;
|
|
3744
|
+
running = null;
|
|
3745
|
+
stopAnimation();
|
|
3746
|
+
for (const resolve of resolvers)
|
|
3747
|
+
resolve({ state: target.state, interrupted });
|
|
3748
|
+
};
|
|
3749
|
+
/** Updates the target (checked first, so a wrong value throws right away, not in an animation frame). */
|
|
3750
|
+
const applyChanges = (changes) => {
|
|
3751
|
+
for (const name of ['x', 'y', 'size']) {
|
|
3752
|
+
if (changes[name] !== undefined)
|
|
3753
|
+
lengthToPixels(changes[name], 1, name);
|
|
3754
|
+
}
|
|
3755
|
+
if (changes.state !== undefined)
|
|
3756
|
+
target.state = checkedState(target.set, changes.state);
|
|
3757
|
+
if (changes.roundness !== undefined && changes.roundness !== target.roundness) {
|
|
3758
|
+
target.roundness = changes.roundness;
|
|
3759
|
+
target.set = resolveIconSet(set, changes.roundness);
|
|
3760
|
+
}
|
|
3761
|
+
if (changes.x !== undefined)
|
|
3762
|
+
target.x = changes.x;
|
|
3763
|
+
if (changes.y !== undefined)
|
|
3764
|
+
target.y = changes.y;
|
|
3765
|
+
if (changes.size !== undefined)
|
|
3766
|
+
target.size = changes.size;
|
|
3767
|
+
if (changes.rotation !== undefined)
|
|
3768
|
+
target.rotation = changes.rotation;
|
|
3769
|
+
if (changes.outline !== undefined) {
|
|
3770
|
+
target.outline = checkedOutline(changes.outline);
|
|
3771
|
+
buildBand();
|
|
3772
|
+
applyStyle();
|
|
3773
|
+
}
|
|
3774
|
+
};
|
|
3775
|
+
const change = (changes, morphOptions) => {
|
|
3776
|
+
const settled = !running;
|
|
3777
|
+
finish(true);
|
|
3778
|
+
const previous = { state: target.state, set: target.set };
|
|
3779
|
+
const from = frame ?? restingFrame();
|
|
3780
|
+
applyChanges(changes);
|
|
3781
|
+
const { duration = DEFAULT_DURATION, easing = 'easeInOutCubic' } = morphOptions;
|
|
3782
|
+
if (duration <= 0 || prefersReducedMotion() || !elementWidth || !elementHeight) {
|
|
3783
|
+
frame = null;
|
|
3784
|
+
render();
|
|
3785
|
+
return Promise.resolve({ state: target.state, interrupted: false });
|
|
3786
|
+
}
|
|
3787
|
+
// settled: both sides use the pair's layouts; interrupted: start from the shape on screen
|
|
3788
|
+
const fromShape = settled
|
|
3789
|
+
? shapeForPair(previous.set, shapesOf(previous.set), previous.state, target.state) : from.shape;
|
|
3790
|
+
const toShape = shapeForPair(target.set, shapesOf(target.set), target.state, previous.state);
|
|
3791
|
+
const to = restingPlacement();
|
|
3792
|
+
const ease = typeof easing === 'function' ? easing : easings[easing];
|
|
3793
|
+
const startTime = performance.now();
|
|
3794
|
+
return new Promise((resolve) => {
|
|
3795
|
+
const stopAnimation = animate((frameTime) => {
|
|
3796
|
+
const progress = clampToUnit((frameTime - startTime) / duration);
|
|
3797
|
+
if (progress >= 1) {
|
|
3798
|
+
frame = null;
|
|
3799
|
+
render();
|
|
3800
|
+
finish(false);
|
|
3801
|
+
return false;
|
|
3802
|
+
}
|
|
3803
|
+
const easedProgress = ease(progress);
|
|
3804
|
+
const between = (start, end) => start + (end - start) * easedProgress;
|
|
3805
|
+
frame = {
|
|
3806
|
+
shape: interpolate(fromShape, toShape, easedProgress),
|
|
3807
|
+
rotation: between(from.rotation, target.rotation),
|
|
3808
|
+
placement: {
|
|
3809
|
+
centerX: between(from.placement.centerX, to.centerX),
|
|
3810
|
+
centerY: between(from.placement.centerY, to.centerY),
|
|
3811
|
+
side: between(from.placement.side, to.side),
|
|
3812
|
+
},
|
|
3813
|
+
};
|
|
3814
|
+
render();
|
|
3815
|
+
return true;
|
|
3816
|
+
});
|
|
3817
|
+
running = { stopAnimation, resolvers: [resolve] };
|
|
3818
|
+
});
|
|
3819
|
+
};
|
|
3820
|
+
// ---- start ----
|
|
3821
|
+
const root = element.getRootNode();
|
|
3822
|
+
// inside a shadow root, url(#id) only finds ids of that root
|
|
3823
|
+
(root instanceof ShadowRoot ? root : document.body).append(svg);
|
|
3824
|
+
buildBand();
|
|
3825
|
+
applyStyle();
|
|
3826
|
+
const box = element.getBoundingClientRect();
|
|
3827
|
+
measure(box.width, box.height);
|
|
3828
|
+
const resizeObserver = typeof ResizeObserver === 'function'
|
|
3829
|
+
? new ResizeObserver(([entry]) => {
|
|
3830
|
+
const borderBox = entry.borderBoxSize?.[0];
|
|
3831
|
+
if (borderBox)
|
|
3832
|
+
measure(borderBox.inlineSize, borderBox.blockSize);
|
|
3833
|
+
else
|
|
3834
|
+
measure(entry.contentRect.width, entry.contentRect.height);
|
|
3835
|
+
})
|
|
3836
|
+
: null;
|
|
3837
|
+
resizeObserver?.observe(element);
|
|
3838
|
+
return {
|
|
3839
|
+
get state() { return target.state; },
|
|
3840
|
+
to(stateOrChanges, options = {}) {
|
|
3841
|
+
const { onEnd, duration, easing, ...changes } = typeof stateOrChanges === 'string'
|
|
3842
|
+
? { ...options, state: stateOrChanges } : stateOrChanges;
|
|
3843
|
+
const result = change(changes, { ...defaultMorphOptions, ...(duration !== undefined && { duration }), ...(easing && { easing }) });
|
|
3844
|
+
if (onEnd)
|
|
3845
|
+
result.then(onEnd);
|
|
3846
|
+
return result;
|
|
3847
|
+
},
|
|
3848
|
+
set(changes) {
|
|
3849
|
+
finish(true);
|
|
3850
|
+
frame = null;
|
|
3851
|
+
applyChanges(changes);
|
|
3852
|
+
render();
|
|
3853
|
+
},
|
|
3854
|
+
destroy() {
|
|
3855
|
+
finish(true);
|
|
3856
|
+
resizeObserver?.disconnect();
|
|
3857
|
+
svg.remove();
|
|
3858
|
+
element.style.clipPath = ownClipPath;
|
|
3859
|
+
element.style.mask = ownMask;
|
|
3860
|
+
element.style.setProperty('-webkit-mask', ownMask);
|
|
3861
|
+
},
|
|
3862
|
+
};
|
|
3863
|
+
};
|
|
3864
|
+
return { clip };
|
|
3865
|
+
})();
|
|
3866
|
+
|
|
3161
3867
|
// icon.ts
|
|
3162
3868
|
const { icon } = (() => {
|
|
3163
3869
|
// Layer 1: one icon that morphs between the states of a set, e.g. a single digit.
|
|
@@ -3292,6 +3998,6 @@ return { text };
|
|
|
3292
3998
|
// index.ts
|
|
3293
3999
|
// morphico public API.
|
|
3294
4000
|
/** Package version, e.g. "0.1.0" (tools/build.ts replaces 'dev'). */
|
|
3295
|
-
const version = '0.
|
|
4001
|
+
const version = '0.3.0';
|
|
3296
4002
|
|
|
3297
|
-
export { easings, icon, iconSets, text, version };
|
|
4003
|
+
export { clip, easings, icon, iconSets, text, version };
|