exodeui-react-native 1.2.0 → 1.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/engine.ts +67 -51
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exodeui-react-native",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "React Native runtime for ExodeUI animations",
5
5
  "main": "index.js",
6
6
  "files": [
package/src/engine.ts CHANGED
@@ -1535,8 +1535,19 @@ export class ExodeUIEngine {
1535
1535
 
1536
1536
  canvas.save();
1537
1537
  canvas.translate(cx, cy);
1538
- if (rotation !== 0) canvas.rotate(rotation * Math.PI / 180, 0, 0);
1539
- if (scaleX !== 1 || scaleY !== 1) canvas.scale(scaleX, scaleY);
1538
+
1539
+ // Rotate around the center of the object
1540
+ if (rotation !== 0) {
1541
+ canvas.translate(w / 2, h / 2);
1542
+ canvas.rotate(rotation * Math.PI / 180, 0, 0);
1543
+ canvas.translate(-w / 2, -h / 2);
1544
+ }
1545
+
1546
+ if (scaleX !== 1 || scaleY !== 1) {
1547
+ canvas.translate(w / 2, h / 2);
1548
+ canvas.scale(scaleX, scaleY);
1549
+ canvas.translate(-w / 2, -h / 2);
1550
+ }
1540
1551
 
1541
1552
  if (this._renderCount % 120 === 1) {
1542
1553
  console.log(`[ExodeUIEngine] - Drawing object: ${obj.name || obj.id} (${obj.type}), pos: ${cx.toFixed(1)},${cy.toFixed(1)}`);
@@ -1638,15 +1649,15 @@ export class ExodeUIEngine {
1638
1649
  const segments = geom.segments || obj.segments || [];
1639
1650
 
1640
1651
  if (enableSegments && segments.length > 0) {
1641
- let offsetX = -w/2;
1652
+ let offsetX = 0;
1642
1653
  const totalWidth = segments.reduce((acc: number, seg: any) => {
1643
1654
  const font = this.getFont(seg.fontSize || geom.fontSize || 14, seg.fontFamily || geom.fontFamily || 'System');
1644
1655
  return acc + font.getTextWidth(seg.text);
1645
1656
  }, 0);
1646
1657
 
1647
1658
  const align = geom.textAlign || 'left';
1648
- if (align === 'center') offsetX = -totalWidth / 2;
1649
- else if (align === 'right') offsetX = w/2 - totalWidth;
1659
+ if (align === 'center') offsetX = (w - totalWidth) / 2;
1660
+ else if (align === 'right') offsetX = w - totalWidth;
1650
1661
 
1651
1662
  segments.forEach((seg: any) => {
1652
1663
  const font = this.getFont(seg.fontSize || geom.fontSize || 14, seg.fontFamily || geom.fontFamily || 'System');
@@ -1655,7 +1666,9 @@ export class ExodeUIEngine {
1655
1666
  try { paint.setColor(Skia.Color(col.replace(/\s+/g, ''))); } catch { paint.setColor(Skia.Color('#ffffff')); }
1656
1667
  paint.setAlphaf((state?.opacity ?? 1) * (seg.fill?.opacity ?? 1));
1657
1668
 
1658
- canvas.drawText(seg.text, offsetX, 0, paint, font);
1669
+ const fontSize = seg.fontSize || geom.fontSize || 14;
1670
+ const y = fontSize; // Basic baseline
1671
+ canvas.drawText(seg.text, offsetX, y, paint, font);
1659
1672
  offsetX += font.getTextWidth(seg.text);
1660
1673
  });
1661
1674
  return;
@@ -1682,12 +1695,12 @@ export class ExodeUIEngine {
1682
1695
 
1683
1696
  const align = geom.textAlign || obj.textAlign || 'center';
1684
1697
  const textWidth = font.getTextWidth(text);
1685
- let x = -w/2;
1686
- if (align === 'center') x = -textWidth / 2;
1687
- else if (align === 'right') x = w/2 - textWidth;
1698
+ let x = 0;
1699
+ if (align === 'center') x = (w - textWidth) / 2;
1700
+ else if (align === 'right') x = w - textWidth;
1688
1701
 
1689
1702
  // Check baseline
1690
- const y = geom.verticalAlign === 'center' || !geom.verticalAlign ? fontSize / 3 : fontSize / 2;
1703
+ const y = geom.verticalAlign === 'center' || !geom.verticalAlign ? (h + fontSize/2) / 2 : fontSize;
1691
1704
  canvas.drawText(text, x, y, paint, font);
1692
1705
  }
1693
1706
 
@@ -1722,8 +1735,8 @@ export class ExodeUIEngine {
1722
1735
  paint.setAlphaf(state?.opacity ?? 1);
1723
1736
  canvas.drawImageRect(
1724
1737
  img,
1725
- { x: 0, y: 0, width: img.width(), height: img.height() },
1726
- { x: -w/2, y: -h/2, width: w, height: h },
1738
+ Skia.XYWHRect(0, 0, img.width(), img.height()),
1739
+ Skia.XYWHRect(0, 0, w, h),
1727
1740
  paint
1728
1741
  );
1729
1742
  } else {
@@ -2105,11 +2118,11 @@ export class ExodeUIEngine {
2105
2118
  const svgContent = geometry.svgContent;
2106
2119
  if (!svgContent) return;
2107
2120
 
2108
- // Extract path data from potentially multiple <path d="..."> tags
2109
- const pathMatches = svgContent.matchAll(/d="([^"]+)"/g);
2121
+ // Extract path data from potentially multiple <path d="..."> or d='...' tags
2122
+ const pathMatches = svgContent.matchAll(/d=["']([^"']+)["']/g);
2110
2123
  const paths: string[] = [];
2111
2124
  for (const match of pathMatches) {
2112
- paths.push(match[1] as string);
2125
+ if (match[1]) paths.push(match[1]);
2113
2126
  }
2114
2127
 
2115
2128
  if (paths.length === 0) return;
@@ -2118,30 +2131,34 @@ export class ExodeUIEngine {
2118
2131
  const style = state?.style || obj.style || {};
2119
2132
 
2120
2133
  paths.forEach(d => {
2121
- const path = Skia.Path.MakeFromSVGString(d);
2122
- if (!path) return;
2123
-
2124
- // Draw fill
2125
- if (style.fill && style.fill.type !== 'None') {
2126
- const paint = Skia.Paint();
2127
- const fillCol = style.fill.color || '#ffffff';
2128
- const fillColStr = typeof fillCol === 'string' ? fillCol.replace(/\s+/g, '') : '#ffffff';
2129
- try { paint.setColor(Skia.Color(fillColStr)); } catch { paint.setColor(Skia.Color('#ffffff')); }
2130
- paint.setAlphaf((state?.opacity ?? 1) * (style.fill.opacity ?? 1));
2131
- paint.setStyle(PaintStyle.Fill);
2132
- canvas.drawPath(path, paint);
2133
- }
2134
-
2135
- // Draw stroke if enabled
2136
- if (style.stroke && style.stroke.width > 0 && style.stroke.isEnabled !== false) {
2137
- const paint = Skia.Paint();
2138
- const strokeCol = style.stroke.color || '#000000';
2139
- const strokeColStr = typeof strokeCol === 'string' ? strokeCol.replace(/\s+/g, '') : '#000000';
2140
- try { paint.setColor(Skia.Color(strokeColStr)); } catch { paint.setColor(Skia.Color('#000000')); }
2141
- paint.setStrokeWidth(style.stroke.width);
2142
- paint.setAlphaf((state?.opacity ?? 1) * (style.stroke.opacity ?? 1));
2143
- paint.setStyle(PaintStyle.Stroke);
2144
- canvas.drawPath(path, paint);
2134
+ try {
2135
+ const path = Skia.Path.MakeFromSVGString(d);
2136
+ if (!path) return;
2137
+
2138
+ // Draw fill
2139
+ if (style.fill && style.fill.type !== 'None') {
2140
+ const paint = Skia.Paint();
2141
+ const fillCol = style.fill.color || '#ffffff';
2142
+ const fillColStr = typeof fillCol === 'string' ? fillCol.replace(/\s+/g, '') : '#ffffff';
2143
+ try { paint.setColor(Skia.Color(fillColStr)); } catch { paint.setColor(Skia.Color('#ffffff')); }
2144
+ paint.setAlphaf((state?.opacity ?? 1) * (style.fill.opacity ?? 1));
2145
+ paint.setStyle(PaintStyle.Fill);
2146
+ canvas.drawPath(path, paint);
2147
+ }
2148
+
2149
+ // Draw stroke if enabled
2150
+ if (style.stroke && style.stroke.width > 0 && style.stroke.isEnabled !== false) {
2151
+ const paint = Skia.Paint();
2152
+ const strokeCol = style.stroke.color || '#000000';
2153
+ const strokeColStr = typeof strokeCol === 'string' ? strokeCol.replace(/\s+/g, '') : '#000000';
2154
+ try { paint.setColor(Skia.Color(strokeColStr)); } catch { paint.setColor(Skia.Color('#000000')); }
2155
+ paint.setStrokeWidth(style.stroke.width);
2156
+ paint.setAlphaf((state?.opacity ?? 1) * (style.stroke.opacity ?? 1));
2157
+ paint.setStyle(PaintStyle.Stroke);
2158
+ canvas.drawPath(path, paint);
2159
+ }
2160
+ } catch (e) {
2161
+ console.warn(`[ExodeUIEngine] Failed to parse SVG path for ${obj.id}:`, e);
2145
2162
  }
2146
2163
  });
2147
2164
  }
@@ -2304,8 +2321,8 @@ export class ExodeUIEngine {
2304
2321
  const style = state.style || obj.style;
2305
2322
 
2306
2323
  const path = Skia.Path.Make();
2324
+ const rect = Skia.XYWHRect(0, 0, w, h);
2307
2325
  if (geometry.type === 'Rectangle') {
2308
- const rect = { x: -w/2, y: -h/2, width: w, height: h };
2309
2326
  if (state.cornerRadius !== undefined || geometry.corner_radius !== undefined) {
2310
2327
  const rawCr = state.cornerRadius ?? geometry.corner_radius;
2311
2328
  const cr = Array.isArray(rawCr) ? rawCr[0] : Number(rawCr);
@@ -2314,11 +2331,11 @@ export class ExodeUIEngine {
2314
2331
  path.addRect(rect);
2315
2332
  }
2316
2333
  } else if (geometry.type === 'Ellipse') {
2317
- path.addOval({ x: -w/2, y: -h/2, width: w, height: h });
2334
+ path.addOval(rect);
2318
2335
  } else if (geometry.type === 'Triangle') {
2319
- path.moveTo(0, -h/2);
2320
- path.lineTo(w/2, h/2);
2321
- path.lineTo(-w/2, h/2);
2336
+ path.moveTo(w / 2, 0);
2337
+ path.lineTo(w, h);
2338
+ path.lineTo(0, h);
2322
2339
  path.close();
2323
2340
  } else if (geometry.type === 'Star') {
2324
2341
  const ir = geometry.inner_radius || 20;
@@ -2327,8 +2344,8 @@ export class ExodeUIEngine {
2327
2344
  for (let i = 0; i < sp * 2; i++) {
2328
2345
  const a = (i * Math.PI / sp) - (Math.PI / 2);
2329
2346
  const rad = i % 2 === 0 ? or : ir;
2330
- const px = rad * Math.cos(a);
2331
- const py = rad * Math.sin(a);
2347
+ const px = w/2 + rad * Math.cos(a);
2348
+ const py = h/2 + rad * Math.sin(a);
2332
2349
  if (i === 0) path.moveTo(px, py);
2333
2350
  else path.lineTo(px, py);
2334
2351
  }
@@ -2342,14 +2359,14 @@ export class ExodeUIEngine {
2342
2359
  if (style.fill.type === 'LinearGradient' && style.fill.stops && style.fill.stops.length >= 2) {
2343
2360
  const colors = style.fill.stops.map((s: any) => Skia.Color(s.color.replace(/\s+/g, '')));
2344
2361
  const offsets = style.fill.stops.map((s: any) => s.offset);
2345
- const start = style.fill.start ? { x: -w/2 + style.fill.start[0] * w, y: -h/2 + style.fill.start[1] * h } : { x: -w/2, y: 0 };
2346
- const end = style.fill.end ? { x: -w/2 + style.fill.end[0] * w, y: -h/2 + style.fill.end[1] * h } : { x: w/2, y: 0 };
2362
+ const start = style.fill.start ? { x: style.fill.start[0] * w, y: style.fill.start[1] * h } : { x: 0, y: 0 };
2363
+ const end = style.fill.end ? { x: style.fill.end[0] * w, y: style.fill.end[1] * h } : { x: w, y: 0 };
2347
2364
  paint.setShader(Skia.Shader.MakeLinearGradient(Skia.Point(start.x, start.y), Skia.Point(end.x, end.y), colors, offsets, TileMode.Clamp));
2348
2365
  hasShader = true;
2349
2366
  } else if (style.fill.type === 'RadialGradient' && style.fill.stops && style.fill.stops.length >= 2) {
2350
2367
  const colors = style.fill.stops.map((s: any) => Skia.Color(s.color.replace(/\s+/g, '')));
2351
2368
  const offsets = style.fill.stops.map((s: any) => s.offset);
2352
- const center = style.fill.center ? { x: -w/2 + style.fill.center[0] * w, y: -h/2 + style.fill.center[1] * h } : { x: 0, y: 0 };
2369
+ const center = style.fill.center ? { x: style.fill.center[0] * w, y: style.fill.center[1] * h } : { x: w/2, y: h/2 };
2353
2370
  const radius = (style.fill.radius || 0.5) * Math.max(w, h);
2354
2371
  paint.setShader(Skia.Shader.MakeRadialGradient(Skia.Point(center.x, center.y), radius, colors, offsets, TileMode.Clamp));
2355
2372
  hasShader = true;
@@ -2357,7 +2374,6 @@ export class ExodeUIEngine {
2357
2374
  const img = this.getOrDecodeImage(style.fill.url);
2358
2375
  if (img) {
2359
2376
  const matrix = Skia.Matrix();
2360
- matrix.translate(-w/2, -h/2);
2361
2377
  matrix.scale(w / img.width(), h / img.height());
2362
2378
  paint.setShader(img.makeShaderOptions(0, 0, 0, 0, matrix));
2363
2379
  hasShader = true;
@@ -2375,7 +2391,7 @@ export class ExodeUIEngine {
2375
2391
 
2376
2392
  // Apply clipping for Frame types before drawing
2377
2393
  if (obj.type === 'Frame' || (obj as any).clipContent) {
2378
- canvas.clipRect({ x: -w/2, y: -h/2, width: w, height: h }, ClipOp.Intersect, true);
2394
+ canvas.clipRect(rect, ClipOp.Intersect, true);
2379
2395
  }
2380
2396
 
2381
2397
  if (style.shadow && style.shadow.opacity > 0) {