@ikijs/editor 0.1.0 → 0.2.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/dist/index.mjs CHANGED
@@ -1604,14 +1604,25 @@ import {
1604
1604
  parseIkiModel as parseIkiModel3
1605
1605
  } from "@ikijs/format";
1606
1606
  var ROLE_TABLE = {
1607
- // hair_back stays rigid (silhouette behind the face); per-layer front/back
1608
- // depth parallax is a later slice.
1609
- hair_back: { deformer: "headDeformer", order: 0, mesh: false },
1607
+ // The silhouette behind the face. It rides the rigid head (not faceWarp,
1608
+ // whose grid it would stretch across the shoulders) as a MESH: it bends on
1609
+ // the turn through its own part warp (bakeHairBackTurnWarp) and swings its
1610
+ // ends on the hair-sway warps.
1611
+ hair_back: { deformer: "headDeformer", order: 0, mesh: true },
1612
+ // The torso. Alone among the roles it hangs from NO deformer: the head turns
1613
+ // about the neck pivot while the shoulders stay put, which is what keeps the
1614
+ // character from reading as a floating head. Drawn over the back hair so long
1615
+ // hair falls behind the shoulders.
1616
+ body: { deformer: "none", order: 5, mesh: false },
1610
1617
  face: { deformer: "faceWarp", order: 10, mesh: true },
1611
1618
  nose: { deformer: "faceWarp", order: 15, mesh: true },
1612
1619
  blush_L: { deformer: "faceWarp", order: 20, mesh: true },
1613
1620
  blush_R: { deformer: "faceWarp", order: 20, mesh: true },
1614
1621
  mouth: { deformer: "faceWarp", order: 25, mesh: true },
1622
+ // An OPTIONAL second mouth drawing, open. When present the two cross-fade on
1623
+ // MouthOpen instead of the closed one being stretched, which is the
1624
+ // difference between a portrait rig and one that can lip-sync.
1625
+ mouth_open: { deformer: "faceWarp", order: 26, mesh: true },
1615
1626
  eye_L: { deformer: "faceWarp", order: 30, mesh: true, eyeSide: "L" },
1616
1627
  eye_R: { deformer: "faceWarp", order: 30, mesh: true, eyeSide: "R" },
1617
1628
  iris_L: { deformer: "faceWarp", order: 31, mesh: true, eyeSide: "L" },
@@ -1763,29 +1774,63 @@ function createPixelGridMesh(cols, rows, w, h) {
1763
1774
  }
1764
1775
  return { vertices, uvs, indices };
1765
1776
  }
1766
- function bakeHeadTurnGridWarpCentered(grid, parameter, centerX) {
1767
- const ANGLES = [-30, 0, 30];
1777
+ var HEAD_CYLINDER_RADIUS_FACTOR = 0.6 / 0.5;
1778
+ var HEAD_TURN_MAX_DEG = 30;
1779
+ function headTurnParallaxUnit(gridHalfWidth) {
1780
+ return gridHalfWidth * HEAD_CYLINDER_RADIUS_FACTOR * Math.sin(HEAD_TURN_MAX_DEG * (Math.PI / 180));
1781
+ }
1782
+ function pinnedCylinderBend(local, radius, theta) {
1783
+ const alpha = Math.asin(Math.max(-1, Math.min(1, local / radius)));
1784
+ return radius * Math.sin(alpha + theta) - local - radius * Math.sin(theta);
1785
+ }
1786
+ function bakeHeadTurnGridWarp2DCentered(grid, parameterX, parameterY, centerX, centerY) {
1787
+ const STOPS = [-HEAD_TURN_MAX_DEG, 0, HEAD_TURN_MAX_DEG];
1768
1788
  const halfWidth = (grid.points[grid.cols * 2] - grid.points[0]) / 2;
1769
- const RADIUS = halfWidth * (0.6 / 0.5);
1789
+ const RADIUS_X = halfWidth * HEAD_CYLINDER_RADIUS_FACTOR;
1770
1790
  const pointCount = grid.points.length / 2;
1791
+ let halfHeight = 0;
1792
+ for (let i = 0; i < pointCount; i++) {
1793
+ halfHeight = Math.max(
1794
+ halfHeight,
1795
+ Math.abs(grid.points[i * 2 + 1] - centerY)
1796
+ );
1797
+ }
1798
+ const RADIUS_Y = halfHeight * HEAD_CYLINDER_RADIUS_FACTOR;
1771
1799
  const DEG_TO_RAD = Math.PI / 180;
1772
- const keyforms = ANGLES.map((angleDeg) => {
1773
- const theta = angleDeg * DEG_TO_RAD;
1774
- const offsets = [];
1775
- for (let i = 0; i < pointCount; i++) {
1776
- const x = grid.points[i * 2];
1777
- const localX = x - centerX;
1778
- const alpha = Math.asin(Math.max(-1, Math.min(1, localX / RADIUS)));
1779
- const xPrime = centerX + RADIUS * Math.sin(alpha + theta);
1780
- const dx = xPrime - x;
1781
- offsets.push(dx, 0);
1782
- }
1783
- return { value: angleDeg, offsets };
1784
- });
1785
- return { parameter, keyforms };
1800
+ const keyforms2d = [];
1801
+ for (const angleY of STOPS) {
1802
+ const thetaY = angleY * NOD_BEND * DEG_TO_RAD;
1803
+ for (const angleX of STOPS) {
1804
+ const thetaX = angleX * DEG_TO_RAD;
1805
+ const offsets = [];
1806
+ for (let i = 0; i < pointCount; i++) {
1807
+ offsets.push(
1808
+ pinnedCylinderBend(grid.points[i * 2] - centerX, RADIUS_X, thetaX),
1809
+ pinnedCylinderBend(
1810
+ grid.points[i * 2 + 1] - centerY,
1811
+ RADIUS_Y,
1812
+ thetaY
1813
+ )
1814
+ );
1815
+ }
1816
+ keyforms2d.push({ offsets });
1817
+ }
1818
+ }
1819
+ return {
1820
+ parameter: parameterX,
1821
+ parameterY,
1822
+ valuesX: STOPS,
1823
+ valuesY: STOPS,
1824
+ keyforms2d
1825
+ };
1786
1826
  }
1787
1827
  var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
1788
- function bindingsForRole(spec, role, cropW, cropH) {
1828
+ var HAIR_FRONT_DEPTH = 0.1;
1829
+ var HAIR_BACK_DEPTH = -0.08;
1830
+ var NOD_TRAVEL = 30;
1831
+ var NOD_BEND = 0.5;
1832
+ var HAIR_BACK_NOD_DEPTH = -0.07;
1833
+ function bindingsForRole(spec, role, cropW, cropH, options = {}) {
1789
1834
  const isEyeStack = EYE_STACK_PREFIXES.some((p) => role.startsWith(p));
1790
1835
  if (isEyeStack && spec.eyeSide !== void 0) {
1791
1836
  const isGazeRole = role.startsWith("iris_") || role.startsWith("pupil_") || role.startsWith("highlight_");
@@ -1807,22 +1852,47 @@ function bindingsForRole(spec, role, cropW, cropH) {
1807
1852
  }
1808
1853
  ];
1809
1854
  }
1855
+ const mouthForm = {
1856
+ // Mouth form: scaleX from -0.2 (pursed, param=-1) to 0.4 (wide, param=1).
1857
+ parameter: StandardParameter.MouthForm,
1858
+ channel: "scaleX",
1859
+ from: -0.2,
1860
+ to: 0.4
1861
+ };
1810
1862
  if (role === "mouth") {
1863
+ if (options.hasMouthOpen) {
1864
+ return [
1865
+ {
1866
+ parameter: StandardParameter.MouthOpen,
1867
+ channel: "opacity",
1868
+ from: 1,
1869
+ to: 0
1870
+ },
1871
+ mouthForm
1872
+ ];
1873
+ }
1811
1874
  return [
1812
1875
  // Mouth open: scaleY from 0 (closed, param=0) to 3 (wide open, param=1).
1876
+ // Only a fallback — it distorts, but it is better than a mouth that
1877
+ // cannot open at all when the layer set has no open drawing.
1813
1878
  {
1814
1879
  parameter: StandardParameter.MouthOpen,
1815
1880
  channel: "scaleY",
1816
1881
  from: 0,
1817
1882
  to: 3
1818
1883
  },
1819
- // Mouth form: scaleX from -0.2 (pursed, param=-1) to 0.4 (wide, param=1).
1884
+ mouthForm
1885
+ ];
1886
+ }
1887
+ if (role === "mouth_open") {
1888
+ return [
1820
1889
  {
1821
- parameter: StandardParameter.MouthForm,
1822
- channel: "scaleX",
1823
- from: -0.2,
1824
- to: 0.4
1825
- }
1890
+ parameter: StandardParameter.MouthOpen,
1891
+ channel: "opacity",
1892
+ from: 0,
1893
+ to: 1
1894
+ },
1895
+ mouthForm
1826
1896
  ];
1827
1897
  }
1828
1898
  if (role === "brow_L" || role === "brow_R") {
@@ -1860,21 +1930,29 @@ function bindingsForRole(spec, role, cropW, cropH) {
1860
1930
  ];
1861
1931
  }
1862
1932
  }
1863
- if (role === "hair_front") {
1864
- return [
1865
- {
1866
- parameter: StandardParameter.HairSwayX,
1867
- channel: "rotate",
1868
- from: -8,
1869
- to: 8
1870
- },
1871
- {
1872
- parameter: StandardParameter.HairSwayX,
1933
+ if (role === "hair_front" || role === "hair_back") {
1934
+ const isFront = role === "hair_front";
1935
+ const depth = isFront ? HAIR_FRONT_DEPTH : HAIR_BACK_DEPTH;
1936
+ const parallax = [];
1937
+ const shiftX = depth * (options.parallaxUnit ?? 0);
1938
+ if (shiftX !== 0) {
1939
+ parallax.push({
1940
+ parameter: StandardParameter.AngleX,
1873
1941
  channel: "translateX",
1874
- from: -10,
1875
- to: 10
1876
- }
1877
- ];
1942
+ from: -shiftX,
1943
+ to: shiftX
1944
+ });
1945
+ }
1946
+ const shiftY = (isFront ? 0 : HAIR_BACK_NOD_DEPTH) * (options.parallaxUnitY ?? 0);
1947
+ if (shiftY !== 0) {
1948
+ parallax.push({
1949
+ parameter: StandardParameter.AngleY,
1950
+ channel: "translateY",
1951
+ from: -shiftY,
1952
+ to: shiftY
1953
+ });
1954
+ }
1955
+ return parallax;
1878
1956
  }
1879
1957
  return [];
1880
1958
  }
@@ -1897,9 +1975,61 @@ function bakeEyelidFoldWarp(mesh, parameter, creaseOffsetY, k) {
1897
1975
  ]
1898
1976
  };
1899
1977
  }
1978
+ var HAIR_BACK_BEND_RADIUS_FACTOR = 2.5;
1979
+ var HAIR_BACK_FAR_BULGE = 0.22;
1980
+ function bakeHairBackTurnWarp(mesh, parameter) {
1981
+ let left = Infinity;
1982
+ let right = -Infinity;
1983
+ for (let i = 0; i < mesh.vertices.length; i += 2) {
1984
+ left = Math.min(left, mesh.vertices[i]);
1985
+ right = Math.max(right, mesh.vertices[i]);
1986
+ }
1987
+ const halfWidth = (right - left) / 2;
1988
+ const radius = halfWidth * HAIR_BACK_BEND_RADIUS_FACTOR;
1989
+ const bulge = HAIR_BACK_FAR_BULGE * halfWidth;
1990
+ const DEG_TO_RAD = Math.PI / 180;
1991
+ const keyforms = [-HEAD_TURN_MAX_DEG, 0, HEAD_TURN_MAX_DEG].map((deg) => {
1992
+ const theta = deg * DEG_TO_RAD;
1993
+ const s = Math.sign(deg);
1994
+ const offsets = [];
1995
+ for (let i = 0; i < mesh.vertices.length; i += 2) {
1996
+ const x = mesh.vertices[i];
1997
+ const far = Math.max(0, -s * x / halfWidth);
1998
+ offsets.push(pinnedCylinderBend(x, radius, theta) - s * bulge * far, 0);
1999
+ }
2000
+ return { value: deg, offsets };
2001
+ });
2002
+ return { parameter, keyforms };
2003
+ }
2004
+ var HAIR_SWAY_TIP_FRACTION = 0.09;
2005
+ var HAIR_SWAY_CURL = 1.5;
2006
+ var HAIR_SWAY_RANGE = 20;
2007
+ function bakeHairSwayWarp(mesh, parameter, tipShift, range) {
2008
+ let top = -Infinity;
2009
+ let bottom = Infinity;
2010
+ for (let i = 1; i < mesh.vertices.length; i += 2) {
2011
+ top = Math.max(top, mesh.vertices[i]);
2012
+ bottom = Math.min(bottom, mesh.vertices[i]);
2013
+ }
2014
+ const height = top - bottom;
2015
+ const swing = [];
2016
+ for (let i = 0; i < mesh.vertices.length; i += 2) {
2017
+ const u = height > 0 ? (top - mesh.vertices[i + 1]) / height : 0;
2018
+ swing.push(tipShift * Math.pow(u, HAIR_SWAY_CURL), 0);
2019
+ }
2020
+ return {
2021
+ parameter,
2022
+ keyforms: [
2023
+ // `0 - v`, not `-v`: the pinned root row must stay a plain +0.
2024
+ { value: -range, offsets: swing.map((v) => 0 - v) },
2025
+ { value: range, offsets: swing }
2026
+ ]
2027
+ };
2028
+ }
1900
2029
  function generateIkiFromLayerSet(layers, canvas) {
1901
2030
  validateLayerInputs(layers, canvas);
1902
2031
  const hasHair = layers.some((l) => l.role === "hair_front");
2032
+ const hasMouthOpen = layers.some((l) => l.role === "mouth_open");
1903
2033
  const parameters = [
1904
2034
  {
1905
2035
  id: StandardParameter.MouthOpen,
@@ -1950,6 +2080,20 @@ function generateIkiFromLayerSet(layers, canvas) {
1950
2080
  max: 30,
1951
2081
  default: 0
1952
2082
  },
2083
+ {
2084
+ id: StandardParameter.AngleY,
2085
+ name: "Head Angle Y",
2086
+ min: -30,
2087
+ max: 30,
2088
+ default: 0
2089
+ },
2090
+ {
2091
+ id: StandardParameter.AngleZ,
2092
+ name: "Head Angle Z",
2093
+ min: -30,
2094
+ max: 30,
2095
+ default: 0
2096
+ },
1953
2097
  {
1954
2098
  id: StandardParameter.Breath,
1955
2099
  name: "Breath",
@@ -1987,13 +2131,22 @@ function generateIkiFromLayerSet(layers, canvas) {
1987
2131
  }
1988
2132
  ];
1989
2133
  if (hasHair) {
1990
- parameters.push({
1991
- id: StandardParameter.HairSwayX,
1992
- name: "Hair Sway X",
1993
- min: -20,
1994
- max: 20,
1995
- default: 0
1996
- });
2134
+ parameters.push(
2135
+ {
2136
+ id: StandardParameter.HairSwayX,
2137
+ name: "Hair Sway X",
2138
+ min: -HAIR_SWAY_RANGE,
2139
+ max: HAIR_SWAY_RANGE,
2140
+ default: 0
2141
+ },
2142
+ {
2143
+ id: StandardParameter.HairSwayZ,
2144
+ name: "Hair Sway Z",
2145
+ min: -HAIR_SWAY_RANGE,
2146
+ max: HAIR_SWAY_RANGE,
2147
+ default: 0
2148
+ }
2149
+ );
1997
2150
  }
1998
2151
  const faceLayers = layers.filter((l) => l.role === "face");
1999
2152
  const faceLayer = faceLayers[0];
@@ -2051,16 +2204,22 @@ function generateIkiFromLayerSet(layers, canvas) {
2051
2204
  unionMaxY
2052
2205
  )
2053
2206
  };
2207
+ const faceCenterY = faceTransform.y;
2208
+ const halfH = Math.max(faceCenterY - unionMinY, unionMaxY - faceCenterY);
2209
+ const parallaxUnit = headTurnParallaxUnit(halfW);
2210
+ const parallaxUnitY = headTurnParallaxUnit(halfH);
2054
2211
  const faceBottom = faceTransform.y - faceCropH / 2;
2055
2212
  const neckPivot = {
2056
2213
  x: faceCenterX,
2057
2214
  y: faceBottom - faceCropH * 0.15
2058
2215
  // 15% below face bottom = neck
2059
2216
  };
2060
- const faceWarpBake = bakeHeadTurnGridWarpCentered(
2217
+ const faceWarp2d = bakeHeadTurnGridWarp2DCentered(
2061
2218
  faceGrid,
2062
2219
  StandardParameter.AngleX,
2063
- faceCenterX
2220
+ StandardParameter.AngleY,
2221
+ faceCenterX,
2222
+ faceCenterY
2064
2223
  );
2065
2224
  const deformers = [
2066
2225
  // headDeformer: rigid matrix rotating/translating the whole head about the
@@ -2081,6 +2240,27 @@ function generateIkiFromLayerSet(layers, canvas) {
2081
2240
  from: -50,
2082
2241
  to: 50
2083
2242
  },
2243
+ // Nod: a vertical translate only. No rotate — a pitch expressed as a
2244
+ // rigid rotation would sum with the roll below at diagonal poses,
2245
+ // collapsing pitch into roll.
2246
+ {
2247
+ parameter: StandardParameter.AngleY,
2248
+ channel: "translateY",
2249
+ from: -NOD_TRAVEL,
2250
+ to: NOD_TRAVEL
2251
+ },
2252
+ // Tilt: the whole head rolls about the neck pivot, one degree per
2253
+ // degree. Positive AngleZ is clockwise on screen (engine rotate is
2254
+ // CCW-positive, hence the flipped from/to) — Live2D's convention, and
2255
+ // the same sense as the lean above: in Live2D's own sample motions
2256
+ // AngleZ carries the sign of AngleX 214 times out of 222. The two
2257
+ // rotations sum, which is correct for two rolls about one pivot.
2258
+ {
2259
+ parameter: StandardParameter.AngleZ,
2260
+ channel: "rotate",
2261
+ from: 30,
2262
+ to: -30
2263
+ },
2084
2264
  {
2085
2265
  parameter: StandardParameter.Breath,
2086
2266
  channel: "translateY",
@@ -2091,12 +2271,14 @@ function generateIkiFromLayerSet(layers, canvas) {
2091
2271
  },
2092
2272
  // faceWarp: cylinder-bend warp parented to headDeformer; grid is symmetric
2093
2273
  // about faceCenterX so the bake's cylinder axis aligns with the face center.
2274
+ // One 2D warp carries both the turn and the nod (a deformer holds either
2275
+ // `warps` or `warp2d`, never both).
2094
2276
  {
2095
2277
  kind: "warp",
2096
2278
  id: "faceWarp",
2097
2279
  parent: "headDeformer",
2098
2280
  grid: faceGrid,
2099
- warps: [faceWarpBake]
2281
+ warp2d: faceWarp2d
2100
2282
  }
2101
2283
  ];
2102
2284
  const eyeCreaseBySide = {};
@@ -2116,7 +2298,12 @@ function generateIkiFromLayerSet(layers, canvas) {
2116
2298
  const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;
2117
2299
  const spec = ROLE_TABLE[role];
2118
2300
  const t = bboxToTransform(bbox, canvasW, canvasH, role);
2119
- const roleBindings = bindingsForRole(spec, role, cropW, cropH);
2301
+ const roleBindings = bindingsForRole(spec, role, cropW, cropH, {
2302
+ hasMouthOpen,
2303
+ parallaxUnit,
2304
+ parallaxUnitY
2305
+ });
2306
+ const deformerId = spec.deformer === "none" ? void 0 : spec.deformer;
2120
2307
  if (spec.mesh) {
2121
2308
  const mesh = createPixelGridMesh(4, 4, cropW, cropH);
2122
2309
  const part = {
@@ -2126,23 +2313,47 @@ function generateIkiFromLayerSet(layers, canvas) {
2126
2313
  height: 1,
2127
2314
  order: spec.order,
2128
2315
  transform: t,
2129
- deformer: spec.deformer,
2316
+ deformer: deformerId,
2130
2317
  mesh
2131
2318
  };
2132
2319
  if (roleBindings.length > 0) {
2133
2320
  part.bindings = roleBindings;
2134
2321
  }
2322
+ if ((role === "hair_front" || role === "hair_back") && hasHair) {
2323
+ const tipShift = HAIR_SWAY_TIP_FRACTION * cropH;
2324
+ part.warps = [
2325
+ bakeHairSwayWarp(
2326
+ mesh,
2327
+ StandardParameter.HairSwayX,
2328
+ tipShift,
2329
+ HAIR_SWAY_RANGE
2330
+ ),
2331
+ bakeHairSwayWarp(
2332
+ mesh,
2333
+ StandardParameter.HairSwayZ,
2334
+ tipShift,
2335
+ HAIR_SWAY_RANGE
2336
+ )
2337
+ ];
2338
+ }
2339
+ if (role === "hair_back") {
2340
+ part.warps = [
2341
+ ...part.warps ?? [],
2342
+ bakeHairBackTurnWarp(mesh, StandardParameter.AngleX)
2343
+ ];
2344
+ }
2135
2345
  if (spec.eyeSide !== void 0) {
2136
2346
  const isLash = role.startsWith("lash_");
2137
2347
  if (role.startsWith("eye_") || isLash) {
2138
2348
  const openParam = spec.eyeSide === "L" ? StandardParameter.EyeOpenLeft : StandardParameter.EyeOpenRight;
2139
2349
  const creaseWorldY = eyeCreaseBySide[spec.eyeSide] ?? t.y - EYELID_FOLD_CREASE * cropH;
2350
+ const hasLash = layers.some((l) => l.role === `lash_${spec.eyeSide}`);
2140
2351
  part.warps = [
2141
2352
  bakeEyelidFoldWarp(
2142
2353
  mesh,
2143
2354
  openParam,
2144
2355
  creaseWorldY - t.y,
2145
- isLash ? LASH_FOLD_K : EYELID_FOLD_K
2356
+ isLash ? LASH_FOLD_K : hasLash ? 0 : EYELID_FOLD_K
2146
2357
  )
2147
2358
  ];
2148
2359
  } else {
@@ -2151,15 +2362,19 @@ function generateIkiFromLayerSet(layers, canvas) {
2151
2362
  }
2152
2363
  return part;
2153
2364
  } else {
2154
- return {
2365
+ const part = {
2155
2366
  id: role,
2156
2367
  color: [1, 1, 1, 1],
2157
2368
  width: cropW,
2158
2369
  height: cropH,
2159
2370
  order: spec.order,
2160
2371
  transform: t,
2161
- deformer: spec.deformer
2372
+ deformer: deformerId
2162
2373
  };
2374
+ if (roleBindings.length > 0) {
2375
+ part.bindings = roleBindings;
2376
+ }
2377
+ return part;
2163
2378
  }
2164
2379
  });
2165
2380
  const model = {
@@ -2170,9 +2385,12 @@ function generateIkiFromLayerSet(layers, canvas) {
2170
2385
  parameters,
2171
2386
  deformers,
2172
2387
  parts,
2173
- // Secondary motion: a spring lags AngleX onto HairSwayX so front hair sways
2174
- // behind the head turn (same constants as the hand-authored sample). Omitted
2175
- // when there is no front hair to drive.
2388
+ // Secondary motion: one spring lags AngleX onto HairSwayX so front hair
2389
+ // sways behind the head turn (same constants as the hand-authored sample),
2390
+ // and a second lags AngleZ onto HairSwayZ so it swings behind a tilt. Two
2391
+ // rigs because a rig has one input and one output (validator-enforced).
2392
+ // Both hair parts read the outputs through root-pinned sway warps.
2393
+ // Omitted when there is no front hair to drive.
2176
2394
  physics: hasHair ? [
2177
2395
  {
2178
2396
  id: "hairSway",
@@ -2181,6 +2399,14 @@ function generateIkiFromLayerSet(layers, canvas) {
2181
2399
  mass: 1,
2182
2400
  stiffness: 80,
2183
2401
  damping: 10
2402
+ },
2403
+ {
2404
+ id: "hairTilt",
2405
+ input: { parameter: StandardParameter.AngleZ, weight: 1 },
2406
+ output: { parameter: StandardParameter.HairSwayZ, scale: -10 },
2407
+ mass: 1,
2408
+ stiffness: 80,
2409
+ damping: 10
2184
2410
  }
2185
2411
  ] : void 0
2186
2412
  };