exodeui-react-native 1.1.0 → 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 +125 -52
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,26 +1678,16 @@ 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
|
|
|
@@ -2084,12 +2105,45 @@ export class ExodeUIEngine {
|
|
|
2084
2105
|
const svgContent = geometry.svgContent;
|
|
2085
2106
|
if (!svgContent) return;
|
|
2086
2107
|
|
|
2087
|
-
//
|
|
2088
|
-
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
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
|
+
});
|
|
2093
2147
|
}
|
|
2094
2148
|
|
|
2095
2149
|
private renderLineGraph(canvas: any, geom: any, w: number, h: number) {
|
|
@@ -2284,27 +2338,46 @@ export class ExodeUIEngine {
|
|
|
2284
2338
|
if (style.fill) {
|
|
2285
2339
|
const paint = Skia.Paint();
|
|
2286
2340
|
|
|
2287
|
-
let
|
|
2288
|
-
if (
|
|
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) {
|
|
2289
2357
|
const img = this.getOrDecodeImage(style.fill.url);
|
|
2290
2358
|
if (img) {
|
|
2291
2359
|
const matrix = Skia.Matrix();
|
|
2292
2360
|
matrix.translate(-w/2, -h/2);
|
|
2293
2361
|
matrix.scale(w / img.width(), h / img.height());
|
|
2294
2362
|
paint.setShader(img.makeShaderOptions(0, 0, 0, 0, matrix));
|
|
2295
|
-
|
|
2363
|
+
hasShader = true;
|
|
2296
2364
|
}
|
|
2297
2365
|
}
|
|
2298
2366
|
|
|
2299
|
-
if (!
|
|
2300
|
-
const fillCol = style.fill.color;
|
|
2367
|
+
if (!hasShader) {
|
|
2368
|
+
const fillCol = style.fill.color || '#000000';
|
|
2301
2369
|
const fillColStr = typeof fillCol === 'string' ? fillCol.replace(/\s+/g, '') : '#000000';
|
|
2302
2370
|
try { paint.setColor(Skia.Color(fillColStr)); } catch { paint.setColor(Skia.Color('#000000')); }
|
|
2303
2371
|
}
|
|
2304
2372
|
|
|
2305
|
-
paint.setAlphaf(Math.max(0, Math.min(1, (state
|
|
2373
|
+
paint.setAlphaf(Math.max(0, Math.min(1, (state?.opacity ?? 1) * (style.fill.opacity ?? 1))));
|
|
2306
2374
|
paint.setStyle(PaintStyle.Fill);
|
|
2307
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
|
+
|
|
2308
2381
|
if (style.shadow && style.shadow.opacity > 0) {
|
|
2309
2382
|
const alphaHex = Math.round(style.shadow.opacity * 255).toString(16).padStart(2, '0');
|
|
2310
2383
|
const colorWithAlpha = `${style.shadow.color}${alphaHex}`;
|