react-native-nano-icons 0.1.1 → 0.1.3
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 +20 -164
- package/android/build.gradle +28 -0
- package/android/src/main/java/com/nanoicons/NanoIconView.kt +78 -0
- package/android/src/main/java/com/nanoicons/NanoIconViewManager.kt +84 -0
- package/android/src/main/java/com/nanoicons/NanoIconsPackage.kt +22 -0
- package/ios/NanoIconView.h +4 -0
- package/ios/NanoIconView.mm +246 -0
- package/lib/commonjs/cli/build.js +1 -1
- package/lib/commonjs/cli/config.d.ts +2 -2
- package/lib/commonjs/cli/config.js +7 -6
- package/lib/commonjs/plugin/src/buildFonts.d.ts +1 -0
- package/lib/commonjs/plugin/src/buildFonts.js +9 -0
- package/lib/commonjs/plugin/src/index.js +1 -34
- package/lib/commonjs/plugin/src/withNanoIconsFontLinking.d.ts +6 -6
- package/lib/commonjs/plugin/src/withNanoIconsFontLinking.js +11 -15
- package/lib/commonjs/scripts/cli.js +15 -5
- package/lib/commonjs/src/core/font/compile.d.ts +13 -2
- package/lib/commonjs/src/core/font/compile.js +49 -46
- package/lib/commonjs/src/core/pipeline/managers.js +19 -3
- package/lib/commonjs/src/core/pipeline/run.js +121 -32
- package/lib/commonjs/src/core/svg/layers.d.ts +16 -0
- package/lib/commonjs/src/core/svg/layers.js +27 -0
- package/lib/commonjs/src/core/svg/svg_dom.d.ts +29 -1
- package/lib/commonjs/src/core/svg/svg_dom.js +78 -2
- package/lib/commonjs/src/core/svg/svg_pathops.d.ts +11 -0
- package/lib/commonjs/src/core/svg/svg_pathops.js +209 -19
- package/lib/commonjs/src/core/types.d.ts +30 -15
- package/lib/module/core/font/compile.js +52 -41
- package/lib/module/core/font/compile.js.map +1 -1
- package/lib/module/core/pipeline/managers.js +17 -3
- package/lib/module/core/pipeline/managers.js.map +1 -1
- package/lib/module/core/pipeline/run.js +131 -44
- package/lib/module/core/pipeline/run.js.map +1 -1
- package/lib/module/core/shims/picosvg-0.22.3-py3-none-any.whl +0 -0
- package/lib/module/core/svg/layers.js +34 -0
- package/lib/module/core/svg/layers.js.map +1 -1
- package/lib/module/core/svg/svg_dom.js +91 -4
- package/lib/module/core/svg/svg_dom.js.map +1 -1
- package/lib/module/core/svg/svg_pathops.js +203 -19
- package/lib/module/core/svg/svg_pathops.js.map +1 -1
- package/lib/module/createNanoIconsSet.js +3 -79
- package/lib/module/createNanoIconsSet.js.map +1 -1
- package/lib/module/createNanoIconsSet.native.js +68 -0
- package/lib/module/createNanoIconsSet.native.js.map +1 -0
- package/lib/module/createNanoIconsSet.shared.js +91 -0
- package/lib/module/createNanoIconsSet.shared.js.map +1 -0
- package/lib/module/index.js +1 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/specs/NanoIconViewNativeComponent.ts +15 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/shallowEqualColor.js +15 -0
- package/lib/module/utils/shallowEqualColor.js.map +1 -0
- package/lib/typescript/src/core/font/compile.d.ts +13 -2
- package/lib/typescript/src/core/font/compile.d.ts.map +1 -1
- package/lib/typescript/src/core/pipeline/managers.d.ts.map +1 -1
- package/lib/typescript/src/core/pipeline/run.d.ts.map +1 -1
- package/lib/typescript/src/core/svg/layers.d.ts +16 -0
- package/lib/typescript/src/core/svg/layers.d.ts.map +1 -1
- package/lib/typescript/src/core/svg/svg_dom.d.ts +29 -1
- package/lib/typescript/src/core/svg/svg_dom.d.ts.map +1 -1
- package/lib/typescript/src/core/svg/svg_pathops.d.ts +11 -0
- package/lib/typescript/src/core/svg/svg_pathops.d.ts.map +1 -1
- package/lib/typescript/src/core/types.d.ts +30 -15
- package/lib/typescript/src/core/types.d.ts.map +1 -1
- package/lib/typescript/src/createNanoIconsSet.d.ts +5 -18
- package/lib/typescript/src/createNanoIconsSet.d.ts.map +1 -1
- package/lib/typescript/src/createNanoIconsSet.native.d.ts +7 -0
- package/lib/typescript/src/createNanoIconsSet.native.d.ts.map +1 -0
- package/lib/typescript/src/createNanoIconsSet.shared.d.ts +11 -0
- package/lib/typescript/src/createNanoIconsSet.shared.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts +14 -0
- package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +19 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/shallowEqualColor.d.ts +4 -0
- package/lib/typescript/src/utils/shallowEqualColor.d.ts.map +1 -0
- package/package.json +22 -5
- package/plugin/src/buildFonts.ts +13 -0
- package/plugin/src/index.ts +3 -50
- package/plugin/src/withNanoIconsFontLinking.ts +22 -24
- package/react-native-nano-icons.podspec +18 -0
- package/scripts/cli.ts +14 -5
- package/src/core/font/compile.ts +65 -61
- package/src/core/pipeline/managers.ts +26 -3
- package/src/core/pipeline/run.ts +156 -38
- package/src/core/shims/picosvg-0.22.3-py3-none-any.whl +0 -0
- package/src/core/svg/layers.ts +44 -0
- package/src/core/svg/svg_dom.ts +96 -4
- package/src/core/svg/svg_pathops.ts +245 -27
- package/src/core/types.ts +21 -10
- package/src/createNanoIconsSet.native.tsx +108 -0
- package/src/createNanoIconsSet.shared.tsx +121 -0
- package/src/createNanoIconsSet.tsx +7 -126
- package/src/index.ts +1 -2
- package/src/specs/NanoIconViewNativeComponent.ts +15 -0
- package/src/types.ts +27 -0
- package/src/utils/shallowEqualColor.ts +17 -0
|
@@ -457,9 +457,13 @@ function bestStartMinYMinX(
|
|
|
457
457
|
return best;
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
460
|
+
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
// Containment helpers (module-level for reuse by fixPathWinding)
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
|
|
465
|
+
function buildVerbMap(PathKit: PathKitModule): VerbMap {
|
|
466
|
+
return {
|
|
463
467
|
MOVE: PathKit.MOVE_VERB ?? 0,
|
|
464
468
|
LINE: PathKit.LINE_VERB ?? 1,
|
|
465
469
|
QUAD: PathKit.QUAD_VERB ?? 2,
|
|
@@ -467,6 +471,241 @@ export function buildPathopsBackend(PathKit: PathKitModule) {
|
|
|
467
471
|
CUBIC: PathKit.CUBIC_VERB ?? 4,
|
|
468
472
|
CLOSE: PathKit.CLOSE_VERB ?? 5,
|
|
469
473
|
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Convert contour commands to a polyline by sampling curves.
|
|
478
|
+
* Used for ray-casting containment tests.
|
|
479
|
+
*/
|
|
480
|
+
function contourToPolyline(
|
|
481
|
+
contourCmds: readonly Cmd[],
|
|
482
|
+
V: VerbMap,
|
|
483
|
+
steps = 8
|
|
484
|
+
): Point[] {
|
|
485
|
+
let cx = 0,
|
|
486
|
+
cy = 0;
|
|
487
|
+
let sx = 0,
|
|
488
|
+
sy = 0;
|
|
489
|
+
const pts: Point[] = [];
|
|
490
|
+
|
|
491
|
+
for (const cmd of contourCmds) {
|
|
492
|
+
const v = cmd[0]!;
|
|
493
|
+
if (v === V.MOVE) {
|
|
494
|
+
cx = cmd[1]!;
|
|
495
|
+
cy = cmd[2]!;
|
|
496
|
+
sx = cx;
|
|
497
|
+
sy = cy;
|
|
498
|
+
pts.push([cx, cy]);
|
|
499
|
+
} else if (v === V.LINE) {
|
|
500
|
+
cx = cmd[1]!;
|
|
501
|
+
cy = cmd[2]!;
|
|
502
|
+
pts.push([cx, cy]);
|
|
503
|
+
} else if (v === V.QUAD) {
|
|
504
|
+
const x0 = cx,
|
|
505
|
+
y0 = cy;
|
|
506
|
+
const x1 = cmd[1]!,
|
|
507
|
+
y1 = cmd[2]!;
|
|
508
|
+
const x2 = cmd[3]!,
|
|
509
|
+
y2 = cmd[4]!;
|
|
510
|
+
for (let i = 1; i <= steps; i++) {
|
|
511
|
+
const t = i / steps;
|
|
512
|
+
const mt = 1 - t;
|
|
513
|
+
pts.push([mt * mt * x0 + 2 * mt * t * x1 + t * t * x2, mt * mt * y0 + 2 * mt * t * y1 + t * t * y2]);
|
|
514
|
+
}
|
|
515
|
+
cx = x2;
|
|
516
|
+
cy = y2;
|
|
517
|
+
} else if (v === V.CUBIC) {
|
|
518
|
+
const x0 = cx,
|
|
519
|
+
y0 = cy;
|
|
520
|
+
const x1 = cmd[1]!,
|
|
521
|
+
y1 = cmd[2]!;
|
|
522
|
+
const x2 = cmd[3]!,
|
|
523
|
+
y2 = cmd[4]!;
|
|
524
|
+
const x3 = cmd[5]!,
|
|
525
|
+
y3 = cmd[6]!;
|
|
526
|
+
for (let i = 1; i <= steps; i++) {
|
|
527
|
+
const t = i / steps;
|
|
528
|
+
const mt = 1 - t;
|
|
529
|
+
pts.push([
|
|
530
|
+
mt * mt * mt * x0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t * x3,
|
|
531
|
+
mt * mt * mt * y0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t * y3,
|
|
532
|
+
]);
|
|
533
|
+
}
|
|
534
|
+
cx = x3;
|
|
535
|
+
cy = y3;
|
|
536
|
+
} else if (v === V.CLOSE) {
|
|
537
|
+
if (cx !== sx || cy !== sy) pts.push([sx, sy]);
|
|
538
|
+
cx = sx;
|
|
539
|
+
cy = sy;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return pts;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Ray-casting point-in-polygon test.
|
|
547
|
+
*/
|
|
548
|
+
function pointInPolygon(px: number, py: number, poly: Point[]): boolean {
|
|
549
|
+
let inside = false;
|
|
550
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
551
|
+
const [xi, yi] = poly[i]!;
|
|
552
|
+
const [xj, yj] = poly[j]!;
|
|
553
|
+
if (
|
|
554
|
+
yi > py !== yj > py &&
|
|
555
|
+
px < ((xj - xi) * (py - yi)) / (yj - yi) + xi
|
|
556
|
+
) {
|
|
557
|
+
inside = !inside;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return inside;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Get a representative point on the contour boundary (midpoint of first segment).
|
|
565
|
+
*/
|
|
566
|
+
function getContourSamplePoint(contourCmds: readonly Cmd[], V: VerbMap): Point | null {
|
|
567
|
+
let cx = 0,
|
|
568
|
+
cy = 0;
|
|
569
|
+
for (const cmd of contourCmds) {
|
|
570
|
+
const v = cmd[0]!;
|
|
571
|
+
if (v === V.MOVE) {
|
|
572
|
+
cx = cmd[1]!;
|
|
573
|
+
cy = cmd[2]!;
|
|
574
|
+
} else if (v === V.LINE) {
|
|
575
|
+
return [(cx + cmd[1]!) / 2, (cy + cmd[2]!) / 2];
|
|
576
|
+
} else if (v === V.QUAD) {
|
|
577
|
+
const t = 0.5,
|
|
578
|
+
mt = 0.5;
|
|
579
|
+
return [
|
|
580
|
+
mt * mt * cx + 2 * mt * t * cmd[1]! + t * t * cmd[3]!,
|
|
581
|
+
mt * mt * cy + 2 * mt * t * cmd[2]! + t * t * cmd[4]!,
|
|
582
|
+
];
|
|
583
|
+
} else if (v === V.CUBIC) {
|
|
584
|
+
const t = 0.5,
|
|
585
|
+
mt = 0.5;
|
|
586
|
+
return [
|
|
587
|
+
mt ** 3 * cx + 3 * mt ** 2 * t * cmd[1]! + 3 * mt * t ** 2 * cmd[3]! + t ** 3 * cmd[5]!,
|
|
588
|
+
mt ** 3 * cy + 3 * mt ** 2 * t * cmd[2]! + 3 * mt * t ** 2 * cmd[4]! + t ** 3 * cmd[6]!,
|
|
589
|
+
];
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Apply containment-based winding fix to contour objects.
|
|
597
|
+
* Even nesting depth = CCW (outer), odd = CW (hole).
|
|
598
|
+
*/
|
|
599
|
+
function applyContainmentWinding(
|
|
600
|
+
contourObjs: Array<{ cmds: Cmd[]; explicitCloseWanted: boolean; absA: number }>,
|
|
601
|
+
V: VerbMap
|
|
602
|
+
): void {
|
|
603
|
+
const n = contourObjs.length;
|
|
604
|
+
if (n === 0) return;
|
|
605
|
+
|
|
606
|
+
const polylines = contourObjs.map((obj) => contourToPolyline(obj.cmds, V));
|
|
607
|
+
const samplePts = contourObjs.map((obj) => getContourSamplePoint(obj.cmds, V));
|
|
608
|
+
const depths = new Array<number>(n).fill(0);
|
|
609
|
+
|
|
610
|
+
for (let i = 0; i < n; i++) {
|
|
611
|
+
const pt = samplePts[i];
|
|
612
|
+
if (!pt) continue;
|
|
613
|
+
for (let j = 0; j < n; j++) {
|
|
614
|
+
if (i === j) continue;
|
|
615
|
+
if (pointInPolygon(pt[0], pt[1], polylines[j]!)) {
|
|
616
|
+
depths[i] = (depths[i] ?? 0) + 1;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const ensureOrient = (
|
|
622
|
+
obj: { cmds: Cmd[]; explicitCloseWanted: boolean },
|
|
623
|
+
wantCCW: boolean
|
|
624
|
+
) => {
|
|
625
|
+
const a = approxSignedAreaFromContourCmds(obj.cmds, V);
|
|
626
|
+
const isCCW = a > 0;
|
|
627
|
+
if (wantCCW !== isCCW) {
|
|
628
|
+
obj.cmds = reverseClosedContourKeepStart(obj.cmds, obj.explicitCloseWanted, V);
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
for (let i = 0; i < n; i++) {
|
|
633
|
+
ensureOrient(contourObjs[i]!, depths[i]! % 2 === 0);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Convert a path `d` string with evenodd fill semantics to an equivalent
|
|
639
|
+
* path that renders identically under nonzero winding.
|
|
640
|
+
*
|
|
641
|
+
* Steps:
|
|
642
|
+
* 1. Parse via PathKit, set fill type to EVENODD, simplify (resolve topology)
|
|
643
|
+
* 2. Split into contours, compute containment depths
|
|
644
|
+
* 3. Fix winding: even depth = CCW (outer), odd depth = CW (hole)
|
|
645
|
+
* 4. Reconstruct d string
|
|
646
|
+
*/
|
|
647
|
+
export function convertEvenoddToWinding(
|
|
648
|
+
PathKit: PathKitModule,
|
|
649
|
+
d: string
|
|
650
|
+
): string {
|
|
651
|
+
const V = buildVerbMap(PathKit);
|
|
652
|
+
|
|
653
|
+
// 1. Parse and simplify with EVENODD fill type
|
|
654
|
+
const p = PathKit.FromSVGString(d);
|
|
655
|
+
if (!p) return d;
|
|
656
|
+
|
|
657
|
+
const FILL_EVENODD =
|
|
658
|
+
PathKit?.FillType?.EVENODD ?? PathKit?.FillType?.EVEN_ODD ?? 1;
|
|
659
|
+
p.setFillType(FILL_EVENODD);
|
|
660
|
+
p.simplify();
|
|
661
|
+
|
|
662
|
+
// Get the simplified SVG string and re-parse for command access
|
|
663
|
+
const simplified = p.toSVGString();
|
|
664
|
+
p.delete?.();
|
|
665
|
+
|
|
666
|
+
const p2 = PathKit.FromSVGString(simplified);
|
|
667
|
+
if (!p2) return simplified;
|
|
668
|
+
|
|
669
|
+
const cmds: Cmd[] = p2.toCmds();
|
|
670
|
+
p2.delete?.();
|
|
671
|
+
|
|
672
|
+
if (cmds.length === 0) return simplified;
|
|
673
|
+
|
|
674
|
+
// 2. Split into contours
|
|
675
|
+
const contourObjs = splitContours(cmds, V).map((c) => {
|
|
676
|
+
const explicitCloseWanted = explicitCloseWantedFromCmds(c, V);
|
|
677
|
+
const cc = ensureClosed(c, V);
|
|
678
|
+
const a = approxSignedAreaFromContourCmds(cc, V);
|
|
679
|
+
return { cmds: cc, absA: Math.abs(a), explicitCloseWanted };
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
contourObjs.sort((x, y) => y.absA - x.absA);
|
|
683
|
+
|
|
684
|
+
// 3. Fix winding via containment analysis
|
|
685
|
+
applyContainmentWinding(contourObjs, V);
|
|
686
|
+
|
|
687
|
+
// 4. Reconstruct d string from fixed commands
|
|
688
|
+
const allCmds = contourObjs.flatMap((x) => x.cmds);
|
|
689
|
+
const parts: string[] = [];
|
|
690
|
+
for (const cmd of allCmds) {
|
|
691
|
+
const v = cmd[0]!;
|
|
692
|
+
if (v === V.MOVE) parts.push(`M${roundN(cmd[1]!)} ${roundN(cmd[2]!)}`);
|
|
693
|
+
else if (v === V.LINE) parts.push(`L${roundN(cmd[1]!)} ${roundN(cmd[2]!)}`);
|
|
694
|
+
else if (v === V.QUAD)
|
|
695
|
+
parts.push(`Q${roundN(cmd[1]!)} ${roundN(cmd[2]!)} ${roundN(cmd[3]!)} ${roundN(cmd[4]!)}`);
|
|
696
|
+
else if (v === V.CUBIC)
|
|
697
|
+
parts.push(
|
|
698
|
+
`C${roundN(cmd[1]!)} ${roundN(cmd[2]!)} ${roundN(cmd[3]!)} ${roundN(cmd[4]!)} ${roundN(cmd[5]!)} ${roundN(cmd[6]!)}`
|
|
699
|
+
);
|
|
700
|
+
else if (v === V.CLOSE) parts.push('Z');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return parts.join(' ');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// proxy to for picosvg to interatc with pathkit
|
|
707
|
+
export function buildPathopsBackend(PathKit: PathKitModule) {
|
|
708
|
+
const VERB: VerbMap = buildVerbMap(PathKit);
|
|
470
709
|
|
|
471
710
|
const FILL_EVENODD =
|
|
472
711
|
PathKit?.FillType?.EVENODD ?? PathKit?.FillType?.EVEN_ODD ?? 1;
|
|
@@ -500,30 +739,9 @@ export function buildPathopsBackend(PathKit: PathKitModule) {
|
|
|
500
739
|
// Big-to-small ordering gives deterministic outer→inner ordering.
|
|
501
740
|
contourObjs.sort((x, y) => y.absA - x.absA);
|
|
502
741
|
|
|
503
|
-
//
|
|
504
|
-
// -
|
|
505
|
-
|
|
506
|
-
const ensureOrient = (
|
|
507
|
-
obj: { cmds: Cmd[]; explicitCloseWanted: boolean },
|
|
508
|
-
wantCCW: boolean
|
|
509
|
-
) => {
|
|
510
|
-
const a = approxSignedAreaFromContourCmds(obj.cmds, VERB);
|
|
511
|
-
const isCCW = a > 0;
|
|
512
|
-
if (wantCCW !== isCCW) {
|
|
513
|
-
obj.cmds = reverseClosedContourKeepStart(
|
|
514
|
-
obj.cmds,
|
|
515
|
-
obj.explicitCloseWanted,
|
|
516
|
-
VERB
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
if (contourObjs.length) {
|
|
522
|
-
ensureOrient(contourObjs[0]!, true);
|
|
523
|
-
for (let i = 1; i < contourObjs.length; i++) {
|
|
524
|
-
ensureOrient(contourObjs[i]!, false);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
742
|
+
// Containment-based winding: compute nesting depth per contour via
|
|
743
|
+
// ray-casting point-in-polygon. Even depth = outer (CCW), odd = hole (CW).
|
|
744
|
+
applyContainmentWinding(contourObjs, VERB);
|
|
527
745
|
|
|
528
746
|
// Rotate starts deterministically:
|
|
529
747
|
// 1) If we have recorded MOVE points, try to rotate contour to a move point that lies on it.
|
package/src/core/types.ts
CHANGED
|
@@ -123,16 +123,27 @@ export type PyodideModule = {
|
|
|
123
123
|
};
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
-
export type GlyphLayer =
|
|
127
|
-
export type GlyphEntry =
|
|
126
|
+
export type GlyphLayer = [codepoint: number, color: string];
|
|
127
|
+
export type GlyphEntry = [adv: number, layers: GlyphLayer[]];
|
|
128
128
|
export type IconsMap = Record<string, GlyphEntry>;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* m - metadata,
|
|
132
|
+
* f - font family,
|
|
133
|
+
* u - units per em,
|
|
134
|
+
* z - safe zone,
|
|
135
|
+
* s - start unicode,
|
|
136
|
+
* h - hash,
|
|
137
|
+
* i - icons,
|
|
138
|
+
* adv - advance width,
|
|
139
|
+
*/
|
|
129
140
|
export type NanoGlyphMap = {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
};
|
|
137
|
-
|
|
141
|
+
m: { f: string; u: number; z: number; s: number; h?: string };
|
|
142
|
+
i: IconsMap;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** Accepts JSON-inferred types where arrays aren't tuples. */
|
|
146
|
+
export type NanoGlyphMapInput = {
|
|
147
|
+
m: { f: string; u: number; z: number; s: number; h?: string };
|
|
148
|
+
i: Record<string, readonly unknown[]>;
|
|
138
149
|
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { memo, useMemo } from 'react';
|
|
2
|
+
import { PixelRatio, UIManager, processColor } from 'react-native';
|
|
3
|
+
import type { NanoGlyphMapInput, GlyphEntry } from './core/types';
|
|
4
|
+
import type { IconComponent, IconProps } from './types';
|
|
5
|
+
import { shallowEqualColor } from './utils/shallowEqualColor';
|
|
6
|
+
import NanoIconViewNative from './specs/NanoIconViewNativeComponent';
|
|
7
|
+
import { createJSIconSet } from './createNanoIconsSet.shared';
|
|
8
|
+
|
|
9
|
+
export type { IconComponent, IconProps };
|
|
10
|
+
export { shallowEqualColor };
|
|
11
|
+
|
|
12
|
+
const DEFAULT_ICON_SIZE = 12;
|
|
13
|
+
|
|
14
|
+
const HAS_NATIVE_IMPL = UIManager.hasViewManagerConfig('NanoIconView');
|
|
15
|
+
|
|
16
|
+
export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
17
|
+
glyphMap: GM
|
|
18
|
+
): IconComponent<GM> {
|
|
19
|
+
if (!HAS_NATIVE_IMPL) {
|
|
20
|
+
return createJSIconSet(glyphMap);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const fontFamilyBasename = glyphMap.m.f;
|
|
24
|
+
const unitsPerEm = glyphMap.m.u;
|
|
25
|
+
|
|
26
|
+
const resolveEntry = (name: keyof GM['i']): GlyphEntry => {
|
|
27
|
+
return (glyphMap.i[name as string] ?? [
|
|
28
|
+
unitsPerEm,
|
|
29
|
+
[[63, 'black']],
|
|
30
|
+
]) as GlyphEntry;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const Icon = memo(
|
|
34
|
+
({
|
|
35
|
+
name,
|
|
36
|
+
size = DEFAULT_ICON_SIZE,
|
|
37
|
+
color,
|
|
38
|
+
style,
|
|
39
|
+
allowFontScaling = true,
|
|
40
|
+
accessible,
|
|
41
|
+
accessibilityLabel,
|
|
42
|
+
accessibilityRole = 'image',
|
|
43
|
+
testID,
|
|
44
|
+
ref,
|
|
45
|
+
}: IconProps<keyof GM['i']>) => {
|
|
46
|
+
const fontScale = allowFontScaling ? PixelRatio.getFontScale() : 1;
|
|
47
|
+
const [adv, layers] = resolveEntry(name);
|
|
48
|
+
const scaledSize = size * fontScale;
|
|
49
|
+
const width = (adv / unitsPerEm) * scaledSize;
|
|
50
|
+
|
|
51
|
+
const colorArray = Array.isArray(color) ? color : [color];
|
|
52
|
+
const lastPaletteColor = colorArray?.length
|
|
53
|
+
? colorArray[colorArray.length - 1]
|
|
54
|
+
: undefined;
|
|
55
|
+
|
|
56
|
+
const codepoints = useMemo(
|
|
57
|
+
() => layers.map(([cp]) => cp),
|
|
58
|
+
|
|
59
|
+
[name]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const processedColors = useMemo(
|
|
63
|
+
() =>
|
|
64
|
+
layers.map(([, srcColor], i) => {
|
|
65
|
+
const layerColor =
|
|
66
|
+
colorArray?.[i] ?? lastPaletteColor ?? srcColor ?? 'black';
|
|
67
|
+
return (processColor(layerColor) ?? 0xff000000) as number;
|
|
68
|
+
}),
|
|
69
|
+
|
|
70
|
+
[name, color]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const nativeStyle = useMemo(
|
|
74
|
+
() => [{ width, height: scaledSize }, style],
|
|
75
|
+
[scaledSize, width, style]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<NanoIconViewNative
|
|
80
|
+
ref={ref}
|
|
81
|
+
fontFamily={fontFamilyBasename}
|
|
82
|
+
codepoints={codepoints}
|
|
83
|
+
colors={processedColors}
|
|
84
|
+
fontSize={size}
|
|
85
|
+
advanceWidth={adv}
|
|
86
|
+
unitsPerEm={unitsPerEm}
|
|
87
|
+
iconWidth={width}
|
|
88
|
+
iconHeight={scaledSize}
|
|
89
|
+
style={nativeStyle}
|
|
90
|
+
accessible={accessible}
|
|
91
|
+
accessibilityRole={accessibilityRole}
|
|
92
|
+
accessibilityLabel={accessibilityLabel ?? (name as string)}
|
|
93
|
+
testID={testID}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
(prev, next) =>
|
|
98
|
+
prev.name === next.name &&
|
|
99
|
+
prev.size === next.size &&
|
|
100
|
+
prev.allowFontScaling === next.allowFontScaling &&
|
|
101
|
+
prev.style === next.style &&
|
|
102
|
+
shallowEqualColor(prev.color, next.color)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
Icon.displayName = `NanoIcon(${fontFamilyBasename})`;
|
|
106
|
+
|
|
107
|
+
return Icon;
|
|
108
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { memo, useMemo } from 'react';
|
|
2
|
+
import { PixelRatio, Platform, Text, View, type TextProps } from 'react-native';
|
|
3
|
+
import type { NanoGlyphMapInput, GlyphEntry } from './core/types';
|
|
4
|
+
import type { IconComponent, IconProps } from './types';
|
|
5
|
+
import { shallowEqualColor } from './utils/shallowEqualColor';
|
|
6
|
+
|
|
7
|
+
export type { IconComponent, IconProps };
|
|
8
|
+
export { shallowEqualColor };
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ICON_SIZE = 12;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* JS implementation using <View> + <Text> layers.
|
|
14
|
+
* Used on web and as a fallback when native component is unavailable (e.g. Expo Go).
|
|
15
|
+
*/
|
|
16
|
+
export function createJSIconSet<GM extends NanoGlyphMapInput>(
|
|
17
|
+
glyphMap: GM
|
|
18
|
+
): IconComponent<GM> {
|
|
19
|
+
const fontBasename = glyphMap.m.f;
|
|
20
|
+
|
|
21
|
+
const fontReference = Platform.select({
|
|
22
|
+
windows: `/Assets/${fontBasename}`,
|
|
23
|
+
default: fontBasename,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const styleOverrides: TextProps['style'] = {
|
|
27
|
+
fontFamily: fontReference,
|
|
28
|
+
fontWeight: 'normal',
|
|
29
|
+
fontStyle: 'normal',
|
|
30
|
+
position: 'absolute',
|
|
31
|
+
includeFontPadding: false,
|
|
32
|
+
bottom: 0,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const unitsPerEm = glyphMap.m.u;
|
|
36
|
+
|
|
37
|
+
const resolveEntry = (name: keyof GM['i']): GlyphEntry => {
|
|
38
|
+
return (glyphMap.i[name as string] ?? [
|
|
39
|
+
unitsPerEm,
|
|
40
|
+
[[63, 'black']],
|
|
41
|
+
]) as GlyphEntry;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const codepointCache = new Map<number, string>();
|
|
45
|
+
const getChar = (codepoint: number): string => {
|
|
46
|
+
let ch = codepointCache.get(codepoint);
|
|
47
|
+
if (ch === undefined) {
|
|
48
|
+
ch = String.fromCodePoint(codepoint);
|
|
49
|
+
codepointCache.set(codepoint, ch);
|
|
50
|
+
}
|
|
51
|
+
return ch;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const Icon = memo(
|
|
55
|
+
({
|
|
56
|
+
name,
|
|
57
|
+
size = DEFAULT_ICON_SIZE,
|
|
58
|
+
color,
|
|
59
|
+
style,
|
|
60
|
+
allowFontScaling = true,
|
|
61
|
+
accessible,
|
|
62
|
+
accessibilityLabel,
|
|
63
|
+
accessibilityRole = 'image',
|
|
64
|
+
testID,
|
|
65
|
+
ref,
|
|
66
|
+
}: IconProps<keyof GM['i']>) => {
|
|
67
|
+
const fontScale = allowFontScaling ? PixelRatio.getFontScale() : 1;
|
|
68
|
+
const [adv, layers] = resolveEntry(name);
|
|
69
|
+
const scaledSize = size * fontScale;
|
|
70
|
+
const width = (adv / unitsPerEm) * scaledSize;
|
|
71
|
+
|
|
72
|
+
const colorArray = Array.isArray(color) ? color : [color];
|
|
73
|
+
const lastPaletteColor = colorArray?.length
|
|
74
|
+
? colorArray[colorArray.length - 1]
|
|
75
|
+
: undefined;
|
|
76
|
+
|
|
77
|
+
const containerStyle = useMemo(
|
|
78
|
+
() => [{ height: scaledSize, width, bottom: 0 as const }, style],
|
|
79
|
+
[scaledSize, width, style]
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const sizeStyle = useMemo(() => ({ fontSize: size }), [size]);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<View
|
|
86
|
+
ref={ref}
|
|
87
|
+
style={containerStyle}
|
|
88
|
+
accessible={accessible}
|
|
89
|
+
accessibilityRole={accessibilityRole}
|
|
90
|
+
accessibilityLabel={accessibilityLabel ?? (name as string)}
|
|
91
|
+
testID={testID}>
|
|
92
|
+
{layers.map(([codepoint, srcColor], i) => {
|
|
93
|
+
const layerColor =
|
|
94
|
+
colorArray?.[i] ?? lastPaletteColor ?? srcColor ?? 'black';
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Text
|
|
98
|
+
key={i}
|
|
99
|
+
selectable={false}
|
|
100
|
+
accessible={false}
|
|
101
|
+
allowFontScaling={allowFontScaling}
|
|
102
|
+
style={[styleOverrides, sizeStyle, { color: layerColor }]}>
|
|
103
|
+
{getChar(codepoint)}
|
|
104
|
+
</Text>
|
|
105
|
+
);
|
|
106
|
+
})}
|
|
107
|
+
</View>
|
|
108
|
+
);
|
|
109
|
+
},
|
|
110
|
+
(prev, next) =>
|
|
111
|
+
prev.name === next.name &&
|
|
112
|
+
prev.size === next.size &&
|
|
113
|
+
prev.allowFontScaling === next.allowFontScaling &&
|
|
114
|
+
prev.style === next.style &&
|
|
115
|
+
shallowEqualColor(prev.color, next.color)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
Icon.displayName = `NanoIcon(${fontBasename})`;
|
|
119
|
+
|
|
120
|
+
return Icon;
|
|
121
|
+
}
|
|
@@ -1,131 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
Text,
|
|
5
|
-
View,
|
|
6
|
-
type ViewProps,
|
|
7
|
-
type ColorValue,
|
|
8
|
-
type TextProps,
|
|
9
|
-
useWindowDimensions,
|
|
10
|
-
} from 'react-native';
|
|
11
|
-
import type { GlyphEntry, NanoGlyphMap } from './core/types';
|
|
1
|
+
import type { NanoGlyphMapInput } from './core/types';
|
|
2
|
+
import type { IconComponent } from './types';
|
|
3
|
+
import { createJSIconSet } from './createNanoIconsSet.shared';
|
|
12
4
|
|
|
13
|
-
|
|
5
|
+
export type { IconComponent, IconProps } from './types';
|
|
6
|
+
export { shallowEqualColor } from './utils/shallowEqualColor';
|
|
14
7
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export type IconProps<Name> = TextProps & {
|
|
18
|
-
name: Name;
|
|
19
|
-
size?: number;
|
|
20
|
-
colorPalette?: ColorValue[];
|
|
21
|
-
innerRef?: Ref<ViewRef>;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export type IconComponent<GM extends NanoGlyphMap> = React.FC<
|
|
25
|
-
TextProps & {
|
|
26
|
-
name: keyof GM['icons'];
|
|
27
|
-
size?: number;
|
|
28
|
-
colorPalette?: ColorValue[];
|
|
29
|
-
innerRef?: Ref<ViewRef>;
|
|
30
|
-
} & React.RefAttributes<ViewRef>
|
|
31
|
-
>;
|
|
32
|
-
|
|
33
|
-
export function createIconSet<GM extends NanoGlyphMap>(
|
|
8
|
+
export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
34
9
|
glyphMap: GM
|
|
35
10
|
): IconComponent<GM> {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const fontReference = Platform.select({
|
|
39
|
-
windows: `/Assets/${fontBasename}`,
|
|
40
|
-
android: fontBasename,
|
|
41
|
-
default: fontBasename,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const styleOverrides: TextProps['style'] = {
|
|
45
|
-
fontFamily: fontReference,
|
|
46
|
-
fontWeight: 'normal',
|
|
47
|
-
fontStyle: 'normal',
|
|
48
|
-
position: 'absolute',
|
|
49
|
-
includeFontPadding: false,
|
|
50
|
-
bottom: 0,
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const resolveEntry = (name: keyof GM['icons']): GlyphEntry => {
|
|
54
|
-
return (
|
|
55
|
-
glyphMap.icons[name as string] ?? {
|
|
56
|
-
adv: glyphMap.meta.upm,
|
|
57
|
-
layers: [{ codepoint: 63, color: 'black' }], // "?"
|
|
58
|
-
}
|
|
59
|
-
);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const Icon = ({
|
|
63
|
-
name,
|
|
64
|
-
size = DEFAULT_ICON_SIZE,
|
|
65
|
-
colorPalette,
|
|
66
|
-
style,
|
|
67
|
-
allowFontScaling = true,
|
|
68
|
-
innerRef,
|
|
69
|
-
...props
|
|
70
|
-
}: IconProps<keyof GM['icons']>) => {
|
|
71
|
-
const { fontScale } = useWindowDimensions();
|
|
72
|
-
|
|
73
|
-
const entry = resolveEntry(name);
|
|
74
|
-
const layers = entry.layers ?? [];
|
|
75
|
-
|
|
76
|
-
const scaledSize = allowFontScaling ? size * fontScale : size;
|
|
77
|
-
const width = (entry.adv / glyphMap.meta.upm) * scaledSize;
|
|
78
|
-
|
|
79
|
-
const containerProps: ViewProps = {
|
|
80
|
-
style: {
|
|
81
|
-
height: scaledSize,
|
|
82
|
-
width,
|
|
83
|
-
bottom: 0,
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const lastPaletteColor = colorPalette?.length
|
|
88
|
-
? colorPalette[colorPalette.length - 1]
|
|
89
|
-
: undefined;
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<View
|
|
93
|
-
nativeID={`nano-icon-container-${String(name)}`}
|
|
94
|
-
ref={innerRef}
|
|
95
|
-
{...containerProps}
|
|
96
|
-
>
|
|
97
|
-
{layers.map(({ codepoint, color: srcColor }, i) => {
|
|
98
|
-
const layerColor =
|
|
99
|
-
colorPalette?.[i] ?? lastPaletteColor ?? srcColor ?? 'black';
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<Text
|
|
103
|
-
key={`${codepoint}-${i}`}
|
|
104
|
-
selectable={false}
|
|
105
|
-
{...props}
|
|
106
|
-
allowFontScaling={allowFontScaling}
|
|
107
|
-
style={[
|
|
108
|
-
style,
|
|
109
|
-
styleOverrides,
|
|
110
|
-
{
|
|
111
|
-
fontSize: size,
|
|
112
|
-
color: layerColor,
|
|
113
|
-
},
|
|
114
|
-
]}
|
|
115
|
-
>
|
|
116
|
-
{String.fromCodePoint(codepoint)}
|
|
117
|
-
</Text>
|
|
118
|
-
);
|
|
119
|
-
})}
|
|
120
|
-
</View>
|
|
121
|
-
);
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const WrappedIcon = forwardRef<ViewRef, IconProps<keyof GM['icons']>>(
|
|
125
|
-
(props, ref) => <Icon innerRef={ref} {...props} />
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
WrappedIcon.displayName = `NanoIcon(${fontBasename})`;
|
|
129
|
-
|
|
130
|
-
return WrappedIcon;
|
|
11
|
+
return createJSIconSet(glyphMap);
|
|
131
12
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// Extension-less so Metro can resolve when example uses path alias to ../src
|
|
2
1
|
export {
|
|
3
2
|
createIconSet as createNanoIconSet,
|
|
4
3
|
type IconComponent,
|
|
5
4
|
type IconProps,
|
|
6
5
|
} from './createNanoIconsSet';
|
|
7
|
-
export type { ColorValue } from 'react-native';
|
|
6
|
+
export type { ColorValue } from 'react-native';
|