exodeui-react-native 1.0.10 → 1.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/package.json +1 -1
- package/src/engine.ts +179 -53
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SkCanvas, SkImage, SkPaint, PaintStyle, Skia, SkPath, SkColor, BlurStyle, SkImageFilter, SkMaskFilter, ClipOp, matchFont, FontStyle, SkFont } from '@shopify/react-native-skia';
|
|
1
|
+
import { SkCanvas, SkImage, SkPaint, PaintStyle, Skia, SkPath, SkColor, BlurStyle, SkImageFilter, SkMaskFilter, ClipOp, matchFont, FontStyle, SkFont, TileMode } from '@shopify/react-native-skia';
|
|
2
2
|
import { Artboard, Animation as SDKAnimation, ShapeObject, StateMachine, State, Fit, Alignment, Layout, LogicNode, LogicOp, Constraint, ComponentEvent } from './types';
|
|
3
3
|
import { PhysicsEngine, MatterPhysics } from './physics';
|
|
4
4
|
|
|
@@ -1240,21 +1240,21 @@ export class ExodeUIEngine {
|
|
|
1240
1240
|
|
|
1241
1241
|
private getWorldTransform(objId: string): { x: number, y: number, scaleX: number, scaleY: number, rotation: number } {
|
|
1242
1242
|
const state = this.objectStates.get(objId);
|
|
1243
|
-
if (!state) return { x: 0, y: 0, scaleX: 1, scaleY: 1, rotation: 0 };
|
|
1244
|
-
|
|
1245
1243
|
const obj = this.artboard?.objects.find(o => o.id === objId);
|
|
1246
|
-
if (!obj
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1244
|
+
if (!obj) return { x: 0, y: 0, scaleX: 1, scaleY: 1, rotation: 0 };
|
|
1245
|
+
|
|
1246
|
+
const transform = obj.transform || { x: 0, y: 0, rotation: 0, scale_x: 1, scale_y: 1 };
|
|
1247
|
+
const lx = state?.x !== undefined ? state.x : transform.x;
|
|
1248
|
+
const ly = state?.y !== undefined ? state.y : transform.y;
|
|
1249
|
+
const lsX = state?.scale_x !== undefined ? state.scale_x : (transform.scale_x ?? 1);
|
|
1250
|
+
const lsY = state?.scale_y !== undefined ? state.scale_y : (transform.scale_y ?? 1);
|
|
1251
|
+
const lRot = state?.rotation !== undefined ? state.rotation : transform.rotation;
|
|
1252
|
+
|
|
1253
|
+
if (!obj.parentId || obj.parentId === 'root') {
|
|
1254
|
+
return { x: lx, y: ly, scaleX: lsX, scaleY: lsY, rotation: lRot };
|
|
1254
1255
|
}
|
|
1255
1256
|
|
|
1256
1257
|
const parentWorld = this.getWorldTransform(obj.parentId);
|
|
1257
|
-
|
|
1258
1258
|
const rad = parentWorld.rotation * (Math.PI / 180);
|
|
1259
1259
|
const cos = Math.cos(rad);
|
|
1260
1260
|
const sin = Math.sin(rad);
|
|
@@ -1518,23 +1518,27 @@ export class ExodeUIEngine {
|
|
|
1518
1518
|
|
|
1519
1519
|
private renderObject(canvas: any, obj: ShapeObject) {
|
|
1520
1520
|
const state = this.objectStates.get(obj.id);
|
|
1521
|
-
|
|
1521
|
+
const isVisible = state?.visible !== undefined ? state.visible : obj.isVisible !== false;
|
|
1522
|
+
if (!isVisible) return;
|
|
1522
1523
|
|
|
1523
|
-
const geometry = state
|
|
1524
|
-
const w = state
|
|
1525
|
-
const h = state
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
const
|
|
1529
|
-
const
|
|
1530
|
-
const
|
|
1524
|
+
const geometry = state?.geometry || obj.geometry;
|
|
1525
|
+
const w = state?.width || (geometry as any).width || 0;
|
|
1526
|
+
const h = state?.height || (geometry as any).height || 0;
|
|
1527
|
+
|
|
1528
|
+
// Resolve transform: use state if present, otherwise use object defaults
|
|
1529
|
+
const transform = obj.transform || { x: 0, y: 0, rotation: 0, scale_x: 1, scale_y: 1 };
|
|
1530
|
+
const cx = state?.x !== undefined ? state.x : transform.x;
|
|
1531
|
+
const cy = state?.y !== undefined ? state.y : transform.y;
|
|
1532
|
+
const rotation = state?.rotation !== undefined ? state.rotation : transform.rotation;
|
|
1533
|
+
const scaleX = state?.scale_x !== undefined ? state.scale_x : (transform.scale_x ?? 1);
|
|
1534
|
+
const scaleY = state?.scale_y !== undefined ? state.scale_y : (transform.scale_y ?? 1);
|
|
1531
1535
|
|
|
1532
1536
|
canvas.save();
|
|
1533
1537
|
canvas.translate(cx, cy);
|
|
1534
|
-
canvas.rotate(rotation * Math.PI / 180, 0, 0);
|
|
1535
|
-
canvas.scale(scaleX, scaleY);
|
|
1538
|
+
if (rotation !== 0) canvas.rotate(rotation * Math.PI / 180, 0, 0);
|
|
1539
|
+
if (scaleX !== 1 || scaleY !== 1) canvas.scale(scaleX, scaleY);
|
|
1536
1540
|
|
|
1537
|
-
if (this._renderCount %
|
|
1541
|
+
if (this._renderCount % 120 === 1) {
|
|
1538
1542
|
console.log(`[ExodeUIEngine] - Drawing object: ${obj.name || obj.id} (${obj.type}), pos: ${cx.toFixed(1)},${cy.toFixed(1)}`);
|
|
1539
1543
|
}
|
|
1540
1544
|
|
|
@@ -1630,13 +1634,40 @@ export class ExodeUIEngine {
|
|
|
1630
1634
|
private renderText(canvas: any, obj: any, w: number, h: number) {
|
|
1631
1635
|
const state = this.objectStates.get(obj.id);
|
|
1632
1636
|
const geom = state?.geometry || obj.geometry;
|
|
1637
|
+
const enableSegments = geom.enableSegments || obj.enableSegments || false;
|
|
1638
|
+
const segments = geom.segments || obj.segments || [];
|
|
1639
|
+
|
|
1640
|
+
if (enableSegments && segments.length > 0) {
|
|
1641
|
+
let offsetX = -w/2;
|
|
1642
|
+
const totalWidth = segments.reduce((acc: number, seg: any) => {
|
|
1643
|
+
const font = this.getFont(seg.fontSize || geom.fontSize || 14, seg.fontFamily || geom.fontFamily || 'System');
|
|
1644
|
+
return acc + font.getTextWidth(seg.text);
|
|
1645
|
+
}, 0);
|
|
1646
|
+
|
|
1647
|
+
const align = geom.textAlign || 'left';
|
|
1648
|
+
if (align === 'center') offsetX = -totalWidth / 2;
|
|
1649
|
+
else if (align === 'right') offsetX = w/2 - totalWidth;
|
|
1650
|
+
|
|
1651
|
+
segments.forEach((seg: any) => {
|
|
1652
|
+
const font = this.getFont(seg.fontSize || geom.fontSize || 14, seg.fontFamily || geom.fontFamily || 'System');
|
|
1653
|
+
const paint = Skia.Paint();
|
|
1654
|
+
const col = seg.fill?.color || geom.fill?.color || '#ffffff';
|
|
1655
|
+
try { paint.setColor(Skia.Color(col.replace(/\s+/g, ''))); } catch { paint.setColor(Skia.Color('#ffffff')); }
|
|
1656
|
+
paint.setAlphaf((state?.opacity ?? 1) * (seg.fill?.opacity ?? 1));
|
|
1657
|
+
|
|
1658
|
+
canvas.drawText(seg.text, offsetX, 0, paint, font);
|
|
1659
|
+
offsetX += font.getTextWidth(seg.text);
|
|
1660
|
+
});
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1633
1664
|
const text = obj.text || geom.text || '';
|
|
1634
1665
|
if (!text) return;
|
|
1635
1666
|
|
|
1636
1667
|
const paint = Skia.Paint();
|
|
1637
1668
|
const colorValue = obj.style?.fill?.color || '#ffffff';
|
|
1638
1669
|
try {
|
|
1639
|
-
const skColor = Skia.Color(colorValue);
|
|
1670
|
+
const skColor = Skia.Color(colorValue.replace(/\s+/g, ''));
|
|
1640
1671
|
paint.setColor(skColor);
|
|
1641
1672
|
} catch (e) {
|
|
1642
1673
|
paint.setColor(Skia.Color('#ffffff'));
|
|
@@ -1647,35 +1678,60 @@ export class ExodeUIEngine {
|
|
|
1647
1678
|
paint.setAlphaf(Math.max(0, Math.min(1, opacity1 * opacity2)));
|
|
1648
1679
|
|
|
1649
1680
|
const fontSize = geom.fontSize || 14;
|
|
1650
|
-
const font = this.getFont(fontSize, geom.fontFamily || obj.fontFamily || '
|
|
1681
|
+
const font = this.getFont(fontSize, geom.fontFamily || obj.fontFamily || 'Helvetica Neue');
|
|
1651
1682
|
|
|
1652
1683
|
const align = geom.textAlign || obj.textAlign || 'center';
|
|
1653
|
-
let x = -w/2;
|
|
1654
|
-
|
|
1655
|
-
// Calculate text width for alignment
|
|
1656
1684
|
const textWidth = font.getTextWidth(text);
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
x = w/2 - textWidth;
|
|
1661
|
-
}
|
|
1685
|
+
let x = -w/2;
|
|
1686
|
+
if (align === 'center') x = -textWidth / 2;
|
|
1687
|
+
else if (align === 'right') x = w/2 - textWidth;
|
|
1662
1688
|
|
|
1663
|
-
|
|
1664
|
-
console.log(`[ExodeUIEngine] Drawing text: "${text}" align=${align} width=${textWidth} x=${x} color=${colorValue} alpha=${paint.getAlphaf()} font_size=${font.getSize()}`);
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
// Check baseline (web typically aligns center of text roughly to center if no baseline is provided, but standard is y=fontSize/2 or similar)
|
|
1689
|
+
// Check baseline
|
|
1668
1690
|
const y = geom.verticalAlign === 'center' || !geom.verticalAlign ? fontSize / 3 : fontSize / 2;
|
|
1669
|
-
|
|
1670
1691
|
canvas.drawText(text, x, y, paint, font);
|
|
1671
1692
|
}
|
|
1672
1693
|
|
|
1694
|
+
private getOrDecodeImage(src: string): any {
|
|
1695
|
+
if (!src) return null;
|
|
1696
|
+
if (this.imageCache.has(src)) return this.imageCache.get(src)!;
|
|
1697
|
+
|
|
1698
|
+
if (src.startsWith('data:image')) {
|
|
1699
|
+
try {
|
|
1700
|
+
const base64Str = src.includes(',') ? src.split(',')[1] : src;
|
|
1701
|
+
const data = Skia.Data.fromBase64(base64Str);
|
|
1702
|
+
const img = Skia.Image.MakeImageFromEncoded(data);
|
|
1703
|
+
if (img) {
|
|
1704
|
+
this.imageCache.set(src, img);
|
|
1705
|
+
return img;
|
|
1706
|
+
}
|
|
1707
|
+
} catch (e) {
|
|
1708
|
+
console.error('[ExodeUIEngine] Failed to decode base64 image:', e);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
return null;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1673
1714
|
private renderImage(canvas: any, obj: any, w: number, h: number) {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
const
|
|
1677
|
-
|
|
1678
|
-
|
|
1715
|
+
const state = this.objectStates.get(obj.id);
|
|
1716
|
+
const geom = state?.geometry || obj.geometry;
|
|
1717
|
+
const src = obj.src || geom.src || '';
|
|
1718
|
+
|
|
1719
|
+
const img = this.getOrDecodeImage(src);
|
|
1720
|
+
if (img) {
|
|
1721
|
+
const paint = Skia.Paint();
|
|
1722
|
+
paint.setAlphaf(state?.opacity ?? 1);
|
|
1723
|
+
canvas.drawImageRect(
|
|
1724
|
+
img,
|
|
1725
|
+
{ x: 0, y: 0, width: img.width(), height: img.height() },
|
|
1726
|
+
{ x: -w/2, y: -h/2, width: w, height: h },
|
|
1727
|
+
paint
|
|
1728
|
+
);
|
|
1729
|
+
} else {
|
|
1730
|
+
// Fallback placeholder
|
|
1731
|
+
const paint = Skia.Paint();
|
|
1732
|
+
paint.setColor(Skia.Color('#374151'));
|
|
1733
|
+
canvas.drawRect({ x: -w/2, y: -h/2, width: w, height: h }, paint);
|
|
1734
|
+
}
|
|
1679
1735
|
}
|
|
1680
1736
|
|
|
1681
1737
|
private renderButton(canvas: any, obj: any, w: number, h: number) {
|
|
@@ -2049,12 +2105,45 @@ export class ExodeUIEngine {
|
|
|
2049
2105
|
const svgContent = geometry.svgContent;
|
|
2050
2106
|
if (!svgContent) return;
|
|
2051
2107
|
|
|
2052
|
-
//
|
|
2053
|
-
|
|
2054
|
-
const
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2108
|
+
// Extract path data from potentially multiple <path d="..."> tags
|
|
2109
|
+
const pathMatches = svgContent.matchAll(/d="([^"]+)"/g);
|
|
2110
|
+
const paths: string[] = [];
|
|
2111
|
+
for (const match of pathMatches) {
|
|
2112
|
+
paths.push(match[1] as string);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
if (paths.length === 0) return;
|
|
2116
|
+
|
|
2117
|
+
const state = this.objectStates.get(obj.id);
|
|
2118
|
+
const style = state?.style || obj.style || {};
|
|
2119
|
+
|
|
2120
|
+
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);
|
|
2145
|
+
}
|
|
2146
|
+
});
|
|
2058
2147
|
}
|
|
2059
2148
|
|
|
2060
2149
|
private renderLineGraph(canvas: any, geom: any, w: number, h: number) {
|
|
@@ -2248,10 +2337,47 @@ export class ExodeUIEngine {
|
|
|
2248
2337
|
|
|
2249
2338
|
if (style.fill) {
|
|
2250
2339
|
const paint = Skia.Paint();
|
|
2251
|
-
|
|
2252
|
-
|
|
2340
|
+
|
|
2341
|
+
let hasShader = false;
|
|
2342
|
+
if (style.fill.type === 'LinearGradient' && style.fill.stops && style.fill.stops.length >= 2) {
|
|
2343
|
+
const colors = style.fill.stops.map((s: any) => Skia.Color(s.color.replace(/\s+/g, '')));
|
|
2344
|
+
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 };
|
|
2347
|
+
paint.setShader(Skia.Shader.MakeLinearGradient(Skia.Point(start.x, start.y), Skia.Point(end.x, end.y), colors, offsets, TileMode.Clamp));
|
|
2348
|
+
hasShader = true;
|
|
2349
|
+
} else if (style.fill.type === 'RadialGradient' && style.fill.stops && style.fill.stops.length >= 2) {
|
|
2350
|
+
const colors = style.fill.stops.map((s: any) => Skia.Color(s.color.replace(/\s+/g, '')));
|
|
2351
|
+
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 };
|
|
2353
|
+
const radius = (style.fill.radius || 0.5) * Math.max(w, h);
|
|
2354
|
+
paint.setShader(Skia.Shader.MakeRadialGradient(Skia.Point(center.x, center.y), radius, colors, offsets, TileMode.Clamp));
|
|
2355
|
+
hasShader = true;
|
|
2356
|
+
} else if ((style.fill.type === 'Image' || style.fill.type === 'Pattern') && style.fill.url) {
|
|
2357
|
+
const img = this.getOrDecodeImage(style.fill.url);
|
|
2358
|
+
if (img) {
|
|
2359
|
+
const matrix = Skia.Matrix();
|
|
2360
|
+
matrix.translate(-w/2, -h/2);
|
|
2361
|
+
matrix.scale(w / img.width(), h / img.height());
|
|
2362
|
+
paint.setShader(img.makeShaderOptions(0, 0, 0, 0, matrix));
|
|
2363
|
+
hasShader = true;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
if (!hasShader) {
|
|
2368
|
+
const fillCol = style.fill.color || '#000000';
|
|
2369
|
+
const fillColStr = typeof fillCol === 'string' ? fillCol.replace(/\s+/g, '') : '#000000';
|
|
2370
|
+
try { paint.setColor(Skia.Color(fillColStr)); } catch { paint.setColor(Skia.Color('#000000')); }
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
paint.setAlphaf(Math.max(0, Math.min(1, (state?.opacity ?? 1) * (style.fill.opacity ?? 1))));
|
|
2253
2374
|
paint.setStyle(PaintStyle.Fill);
|
|
2254
2375
|
|
|
2376
|
+
// Apply clipping for Frame types before drawing
|
|
2377
|
+
if (obj.type === 'Frame' || (obj as any).clipContent) {
|
|
2378
|
+
canvas.clipRect({ x: -w/2, y: -h/2, width: w, height: h }, ClipOp.Intersect, true);
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2255
2381
|
if (style.shadow && style.shadow.opacity > 0) {
|
|
2256
2382
|
const alphaHex = Math.round(style.shadow.opacity * 255).toString(16).padStart(2, '0');
|
|
2257
2383
|
const colorWithAlpha = `${style.shadow.color}${alphaHex}`;
|