@doan-labs/peek 0.1.0 → 1.1.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/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/identity.d.ts +4 -1
- package/dist/identity.js +22 -2
- package/dist/index.d.ts +6 -4
- package/dist/index.js +282 -87
- package/dist/svg.d.ts +1 -1
- package/dist/svg.js +203 -10
- package/dist/tables.d.ts +14 -1
- package/package.json +3 -1
- package/src/draw.ts +221 -1
- package/src/identity.ts +11 -0
- package/src/index.ts +9 -2
- package/src/peek.tsx +1 -1
- package/src/svg.ts +16 -2
- package/src/tables.ts +27 -2
package/dist/svg.js
CHANGED
|
@@ -121,8 +121,25 @@ var PARTS = {
|
|
|
121
121
|
cheeks: ["oval", "dots", "lines"],
|
|
122
122
|
trait: ["square", "fin", "ring", "dot", "peak"]
|
|
123
123
|
};
|
|
124
|
+
var ACCESSORY_INK = { knot: "#E5412D" };
|
|
125
|
+
var ACCESSORIES = {
|
|
126
|
+
eyewear: ["none", "glasses", "sunglasses"],
|
|
127
|
+
headwear: ["none", "bow", "cap", "sprout"],
|
|
128
|
+
neckwear: ["none", "tie"]
|
|
129
|
+
};
|
|
124
130
|
var VERSIONS = {
|
|
125
|
-
1: {
|
|
131
|
+
1: {
|
|
132
|
+
face: 4,
|
|
133
|
+
color: 7,
|
|
134
|
+
eyes: 3,
|
|
135
|
+
brows: 4,
|
|
136
|
+
mouth: 4,
|
|
137
|
+
cheeks: 3,
|
|
138
|
+
trait: 5,
|
|
139
|
+
eyewear: 1,
|
|
140
|
+
headwear: 1,
|
|
141
|
+
neckwear: 1
|
|
142
|
+
}
|
|
126
143
|
};
|
|
127
144
|
var LATEST = 1;
|
|
128
145
|
var BASE = {
|
|
@@ -335,6 +352,9 @@ var f2 = (v) => String(Math.round(v * 100) / 100);
|
|
|
335
352
|
var f3 = (v) => String(Math.round(v * 1000) / 1000);
|
|
336
353
|
var strokeFor = (px) => px >= 96 ? 1 : px >= 56 ? 1.25 : px >= 40 ? 1.6 : px >= 30 ? 2 : 2.4;
|
|
337
354
|
var RAD = Math.PI / 180;
|
|
355
|
+
var STEM = "M0 0Q3 -19 0 -36";
|
|
356
|
+
var LEAF0 = "M0 -33C-24 -30 -38 -52 -25 -53C-12 -54 -2 -43 0 -33Z";
|
|
357
|
+
var LEAF1 = "M0 -33C5 -54 28 -61 27 -48C26 -36 11 -31 0 -33Z";
|
|
338
358
|
function frameOf(face) {
|
|
339
359
|
const baseY = FLOOR + face.sink - face.bottom;
|
|
340
360
|
const eyeY = face.eyes[0][1];
|
|
@@ -709,7 +729,7 @@ function draw(who, pose, o) {
|
|
|
709
729
|
const side = i === 0 ? -1 : 1;
|
|
710
730
|
const hw = sh.brow.hw;
|
|
711
731
|
const c = -sh.brow.arch * arch;
|
|
712
|
-
const [
|
|
732
|
+
const [w2, ca, k2] = who.brows === "bar" ? [hw * 0.9, c * 0.3, 1.75] : who.brows === "dash" ? [hw * 0.42, c * 0.25, 1.9] : [hw, c, 1];
|
|
713
733
|
const ti = bw * 1.7;
|
|
714
734
|
const to = bw * 0.45;
|
|
715
735
|
const [tl, tr] = side < 0 ? [to, ti] : [ti, to];
|
|
@@ -722,7 +742,7 @@ function draw(who, pose, o) {
|
|
|
722
742
|
const sin = Math.sin(side * btilt * RAD);
|
|
723
743
|
const inside = (dy) => {
|
|
724
744
|
for (let t = 0;t <= 1; t += 0.25) {
|
|
725
|
-
const lx =
|
|
745
|
+
const lx = w2 * (2 * t - 1);
|
|
726
746
|
const ly = 2 * t * (1 - t) * ca;
|
|
727
747
|
const px = bx + lx * cos - ly * sin;
|
|
728
748
|
const py = by + dy + lx * sin + ly * cos;
|
|
@@ -757,7 +777,7 @@ function draw(who, pose, o) {
|
|
|
757
777
|
});
|
|
758
778
|
}
|
|
759
779
|
return n("path", key, {
|
|
760
|
-
d: `M${f2(-
|
|
780
|
+
d: `M${f2(-w2)} 0Q0 ${f2(ca)} ${f2(w2)} 0`,
|
|
761
781
|
transform,
|
|
762
782
|
fill: "none",
|
|
763
783
|
stroke: INK.ink,
|
|
@@ -847,8 +867,8 @@ function draw(who, pose, o) {
|
|
|
847
867
|
});
|
|
848
868
|
} else if (who.trait === "ring") {
|
|
849
869
|
const b = 35 - hair * 14 - (hair < 0 ? hair * -21 : 0) + lag * 0.9;
|
|
850
|
-
const
|
|
851
|
-
const s = seat(sh.crown, 8, Math.max(
|
|
870
|
+
const w2 = ringWalk(sh);
|
|
871
|
+
const s = seat(sh.crown, 8, Math.max(w2.min, w2.rest + 158 * (b - 35) * RAD));
|
|
852
872
|
traitT = `translate(${f2(s.x)} ${f2(s.y)}) scale(${f2(1 + Math.max(0, hair) * 0.1)})`;
|
|
853
873
|
shape = n("circle", "trait.shape", {
|
|
854
874
|
cx: 0,
|
|
@@ -882,6 +902,169 @@ function draw(who, pose, o) {
|
|
|
882
902
|
});
|
|
883
903
|
}
|
|
884
904
|
const trait = n("g", "trait", { transform: traitT }, [shape]);
|
|
905
|
+
const wardrobe = [];
|
|
906
|
+
const w = (px) => f2(px * Math.min(st, 1.6));
|
|
907
|
+
const line = {
|
|
908
|
+
fill: "none",
|
|
909
|
+
stroke: INK.ink,
|
|
910
|
+
"stroke-width": w(5.25),
|
|
911
|
+
"stroke-linecap": "round",
|
|
912
|
+
"stroke-linejoin": "round"
|
|
913
|
+
};
|
|
914
|
+
if (who.eyewear !== "none") {
|
|
915
|
+
const lx = sh.eyes[0][0] - spread;
|
|
916
|
+
const rr = sh.eyes[1][0] + spread;
|
|
917
|
+
const ey = sh.eyes[0][1];
|
|
918
|
+
const gap = rr - lx;
|
|
919
|
+
const erx = Math.min(rx + 9, (gap - 15) / 2);
|
|
920
|
+
const ery = ry + 9;
|
|
921
|
+
const glasses = who.eyewear === "glasses";
|
|
922
|
+
const lensX = glasses ? erx : Math.min(rx + 11, (gap - 12) / 2);
|
|
923
|
+
const top = glasses ? ey - 4 : ey - ry - 2;
|
|
924
|
+
const frames = [
|
|
925
|
+
n("path", "eyewear.bridge", {
|
|
926
|
+
...line,
|
|
927
|
+
d: `M${f2(lx + lensX)} ${f2(top)}Q${f2((lx + rr) / 2)} ${f2(top - (glasses ? 13 : 0))} ${f2(rr - lensX)} ${f2(top)}`
|
|
928
|
+
})
|
|
929
|
+
];
|
|
930
|
+
for (const [i, cx] of [lx, rr].entries()) {
|
|
931
|
+
const key = `eyewear.lens${i}`;
|
|
932
|
+
frames.push(n("path", `${key}.arm`, {
|
|
933
|
+
...line,
|
|
934
|
+
d: i === 0 ? `M${f2(cx - lensX - 9)} ${f2(top + 2)}L${f2(cx - lensX)} ${f2(top)}` : `M${f2(cx + lensX)} ${f2(top)}L${f2(cx + lensX + 9)} ${f2(top + 2)}`
|
|
935
|
+
}));
|
|
936
|
+
if (glasses) {
|
|
937
|
+
frames.push(n("ellipse", key, {
|
|
938
|
+
...line,
|
|
939
|
+
cx: f2(cx),
|
|
940
|
+
cy: ey,
|
|
941
|
+
rx: f2(erx),
|
|
942
|
+
ry: f2(ery)
|
|
943
|
+
}));
|
|
944
|
+
frames.push(n("path", `${key}.glint`, {
|
|
945
|
+
...line,
|
|
946
|
+
stroke: INK.paper,
|
|
947
|
+
"stroke-width": w(3),
|
|
948
|
+
d: `M${f2(cx - erx * 0.65)} ${f2(ey - ery * 0.45)}Q${f2(cx - erx * 0.5)} ${f2(ey - ery * 0.8)} ${f2(cx - erx * 0.2)} ${f2(ey - ery * 0.86)}`
|
|
949
|
+
}));
|
|
950
|
+
} else {
|
|
951
|
+
const h = ry * 2 + 9;
|
|
952
|
+
frames.push(n("path", key, {
|
|
953
|
+
d: `M${f2(cx - lensX)} ${f2(top)}H${f2(cx + lensX)}V${f2(top + h * 0.55)}Q${f2(cx + lensX)} ${f2(top + h)} ${f2(cx)} ${f2(top + h)}Q${f2(cx - lensX)} ${f2(top + h)} ${f2(cx - lensX)} ${f2(top + h * 0.55)}Z`,
|
|
954
|
+
fill: INK.ink,
|
|
955
|
+
stroke: INK.ink,
|
|
956
|
+
"stroke-width": w(3),
|
|
957
|
+
"stroke-linejoin": "round"
|
|
958
|
+
}));
|
|
959
|
+
frames.push(n("path", `${key}.glint`, {
|
|
960
|
+
...line,
|
|
961
|
+
stroke: INK.paper,
|
|
962
|
+
"stroke-width": w(4),
|
|
963
|
+
d: `M${f2(cx - lensX * 0.58)} ${f2(top + 19)}l8 -8m-1 15 6 -6`
|
|
964
|
+
}), n("circle", `${key}.pin`, {
|
|
965
|
+
cx: f2(cx + lensX * 0.55),
|
|
966
|
+
cy: f2(top + h - 9),
|
|
967
|
+
r: 1.7,
|
|
968
|
+
fill: INK.paper
|
|
969
|
+
}));
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
wardrobe.push(n("g", "eyewear", {}, frames));
|
|
973
|
+
}
|
|
974
|
+
if (who.headwear !== "none") {
|
|
975
|
+
const side = who.trait === "ring" || who.trait === "dot" ? -1 : 1;
|
|
976
|
+
const cap = who.headwear === "cap";
|
|
977
|
+
const sprout = who.headwear === "sprout";
|
|
978
|
+
const inset = sprout ? -6 : cap ? -8 : 0;
|
|
979
|
+
const offset = sprout ? 34 : 40;
|
|
980
|
+
let lo = 0;
|
|
981
|
+
let hi = 160;
|
|
982
|
+
for (let i = 0;i < 16; i++) {
|
|
983
|
+
const mid2 = (lo + hi) / 2;
|
|
984
|
+
const p = seat(sh.crown, inset, side * mid2);
|
|
985
|
+
if (side * (p.x - 170) < offset)
|
|
986
|
+
lo = mid2;
|
|
987
|
+
else
|
|
988
|
+
hi = mid2;
|
|
989
|
+
}
|
|
990
|
+
const anchor = seat(sh.crown, inset, side * (lo + hi) / 2);
|
|
991
|
+
const angle = clamp(anchor.a, -40, 40);
|
|
992
|
+
const parts = [];
|
|
993
|
+
if (cap) {
|
|
994
|
+
parts.push(n("path", "headwear.crown", {
|
|
995
|
+
...line,
|
|
996
|
+
fill: INK.paper,
|
|
997
|
+
d: "M-33 0C-35 -20 -20 -39 0 -39C20 -39 35 -20 33 0Z"
|
|
998
|
+
}), n("path", "headwear.seam", {
|
|
999
|
+
...line,
|
|
1000
|
+
"stroke-width": w(3.5),
|
|
1001
|
+
d: "M-4 -38Q9 -24 8 -1"
|
|
1002
|
+
}), n("path", "headwear.brim", {
|
|
1003
|
+
...line,
|
|
1004
|
+
"stroke-width": w(7),
|
|
1005
|
+
d: side > 0 ? "M-35 1Q8 4 52 3" : "M-52 3Q-8 4 35 1"
|
|
1006
|
+
}), n("circle", "headwear.button", {
|
|
1007
|
+
cx: 0,
|
|
1008
|
+
cy: -40,
|
|
1009
|
+
r: 3.5,
|
|
1010
|
+
fill: INK.ink
|
|
1011
|
+
}));
|
|
1012
|
+
} else if (sprout) {
|
|
1013
|
+
parts.push(n("path", "headwear.halo", {
|
|
1014
|
+
...line,
|
|
1015
|
+
stroke: fill,
|
|
1016
|
+
"stroke-width": w(11),
|
|
1017
|
+
d: `${STEM}${LEAF0}${LEAF1}`
|
|
1018
|
+
}), n("path", "headwear.stem", {
|
|
1019
|
+
...line,
|
|
1020
|
+
"stroke-width": w(4.5),
|
|
1021
|
+
d: STEM
|
|
1022
|
+
}), n("path", "headwear.leaf0", {
|
|
1023
|
+
...line,
|
|
1024
|
+
"stroke-width": w(4),
|
|
1025
|
+
fill: COLORS.mint.body,
|
|
1026
|
+
d: LEAF0
|
|
1027
|
+
}), n("path", "headwear.leaf1", {
|
|
1028
|
+
...line,
|
|
1029
|
+
"stroke-width": w(4),
|
|
1030
|
+
fill: COLORS.mint.deep,
|
|
1031
|
+
d: LEAF1
|
|
1032
|
+
}));
|
|
1033
|
+
} else {
|
|
1034
|
+
parts.push(n("path", "headwear.wings", {
|
|
1035
|
+
d: "M-3 0C-13 -16 -28 -23 -29 -10C-31 2 -26 19 -16 13L-3 4L3 4L16 13C26 19 31 2 29 -10C28 -23 13 -16 3 0Z",
|
|
1036
|
+
fill: INK.ink,
|
|
1037
|
+
stroke: fill,
|
|
1038
|
+
"stroke-width": w(6),
|
|
1039
|
+
"stroke-linejoin": "round",
|
|
1040
|
+
"paint-order": "stroke"
|
|
1041
|
+
}), n("ellipse", "headwear.knot", {
|
|
1042
|
+
cx: 0,
|
|
1043
|
+
cy: 1,
|
|
1044
|
+
rx: 6,
|
|
1045
|
+
ry: 8,
|
|
1046
|
+
fill: ACCESSORY_INK.knot
|
|
1047
|
+
}));
|
|
1048
|
+
}
|
|
1049
|
+
wardrobe.push(n("g", "headwear", {
|
|
1050
|
+
transform: `translate(${f2(anchor.x)} ${f2(anchor.y)}) rotate(${f2(sprout ? angle * 0.5 : angle)})`
|
|
1051
|
+
}, parts));
|
|
1052
|
+
}
|
|
1053
|
+
if (who.neckwear === "tie") {
|
|
1054
|
+
const skew = Math.abs(Math.sin(g("mk") * RAD)) * mw;
|
|
1055
|
+
const bottom = Math.max(0, mt * kb, mb * kb) + skew + m.sw * st;
|
|
1056
|
+
wardrobe.push(n("g", "neckwear", {
|
|
1057
|
+
transform: `translate(${m.x} ${f2(m.y + g("my") + bottom + 8)})`,
|
|
1058
|
+
fill: INK.ink
|
|
1059
|
+
}, [
|
|
1060
|
+
n("path", "neckwear.knot", {
|
|
1061
|
+
d: "M-9 0Q-11 0 -9 4L-5 11Q0 15 5 11L9 4Q11 0 9 0Z"
|
|
1062
|
+
}),
|
|
1063
|
+
n("path", "neckwear.blade", {
|
|
1064
|
+
d: "M-4 15L-13 63Q-14 66 -11 69L0 79L11 69Q14 66 13 63L4 15Z"
|
|
1065
|
+
})
|
|
1066
|
+
]));
|
|
1067
|
+
}
|
|
885
1068
|
const off = (dx, dy) => riso ? `translate(${f2(dx * k * upp)} ${f2(dy * k * upp)})` : undefined;
|
|
886
1069
|
const av = n("g", "av", {
|
|
887
1070
|
transform: `translate(${f2(x)} ${f2(ty)}) rotate(${f2(rot)} 170 ${B}) translate(170 ${B}) scale(${sc(sx)} ${sc(sy)}) translate(-170 ${-B})`,
|
|
@@ -894,7 +1077,7 @@ function draw(who, pose, o) {
|
|
|
894
1077
|
n("g", "plateB", {
|
|
895
1078
|
transform: off(0.6, -0.5),
|
|
896
1079
|
filter: riso ? `url(#${id}-pB)` : undefined
|
|
897
|
-
}, [...eyes, ...brows, mouth])
|
|
1080
|
+
}, [...eyes, ...brows, mouth, ...wardrobe])
|
|
898
1081
|
]);
|
|
899
1082
|
const scene = [];
|
|
900
1083
|
if (o.frame !== "none") {
|
|
@@ -960,7 +1143,8 @@ function mulberry32(a) {
|
|
|
960
1143
|
var LISTS = {
|
|
961
1144
|
face: Object.keys(FACES),
|
|
962
1145
|
color: Object.keys(COLORS),
|
|
963
|
-
...PARTS
|
|
1146
|
+
...PARTS,
|
|
1147
|
+
...ACCESSORIES
|
|
964
1148
|
};
|
|
965
1149
|
var AXES = Object.keys(LISTS);
|
|
966
1150
|
function seed(name, axis, version = LATEST) {
|
|
@@ -1004,11 +1188,15 @@ function identify(name, { version = LATEST } = {}) {
|
|
|
1004
1188
|
mouth: pick("mouth"),
|
|
1005
1189
|
cheeks: pick("cheeks"),
|
|
1006
1190
|
trait: pick("trait"),
|
|
1191
|
+
eyewear: pick("eyewear"),
|
|
1192
|
+
headwear: pick("headwear"),
|
|
1193
|
+
neckwear: pick("neckwear"),
|
|
1007
1194
|
persona
|
|
1008
1195
|
};
|
|
1009
1196
|
}
|
|
1010
1197
|
|
|
1011
1198
|
// src/svg.ts
|
|
1199
|
+
var wear = (o, base, s) => ACCESSORIES[s].includes(o[s]) ? o[s] : base[s];
|
|
1012
1200
|
function settle(name, o, live = false) {
|
|
1013
1201
|
const base = identify(name, { version: o.version });
|
|
1014
1202
|
const who = {
|
|
@@ -1019,7 +1207,10 @@ function settle(name, o, live = false) {
|
|
|
1019
1207
|
brows: o.brows ?? base.brows,
|
|
1020
1208
|
mouth: o.mouth ?? base.mouth,
|
|
1021
1209
|
cheeks: o.cheeks ?? base.cheeks,
|
|
1022
|
-
trait: o.trait ?? base.trait
|
|
1210
|
+
trait: o.trait ?? base.trait,
|
|
1211
|
+
eyewear: wear(o, base, "eyewear"),
|
|
1212
|
+
headwear: wear(o, base, "headwear"),
|
|
1213
|
+
neckwear: wear(o, base, "neckwear")
|
|
1023
1214
|
};
|
|
1024
1215
|
const pose = restPose(who, o.expression ?? "normal", o.gaze);
|
|
1025
1216
|
const look = {
|
|
@@ -1029,7 +1220,9 @@ function settle(name, o, live = false) {
|
|
|
1029
1220
|
riso: o.riso ?? false,
|
|
1030
1221
|
live
|
|
1031
1222
|
};
|
|
1032
|
-
const
|
|
1223
|
+
const { eyewear, headwear, neckwear, ...legacy } = who;
|
|
1224
|
+
const wardrobe = eyewear !== "none" || headwear !== "none" || neckwear !== "none";
|
|
1225
|
+
const id = o.id ?? `peek-${fnv1a(JSON.stringify([wardrobe ? who : legacy, pose, look])).toString(36)}`;
|
|
1033
1226
|
return { who, pose, opts: { ...look, id, title: o.title ?? name } };
|
|
1034
1227
|
}
|
|
1035
1228
|
var ILLEGAL = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
package/dist/tables.d.ts
CHANGED
|
@@ -218,6 +218,19 @@ export declare const PARTS: {
|
|
|
218
218
|
readonly cheeks: readonly ["oval", "dots", "lines"];
|
|
219
219
|
readonly trait: readonly ["square", "fin", "ring", "dot", "peak"];
|
|
220
220
|
};
|
|
221
|
+
export declare const ACCESSORY_INK: {
|
|
222
|
+
readonly knot: "#E5412D";
|
|
223
|
+
};
|
|
224
|
+
/** Independent wardrobe slots. peek@1 picks none; callers can dress it. */
|
|
225
|
+
export declare const ACCESSORIES: {
|
|
226
|
+
readonly eyewear: readonly ["none", "glasses", "sunglasses"];
|
|
227
|
+
readonly headwear: readonly ["none", "bow", "cap", "sprout"];
|
|
228
|
+
readonly neckwear: readonly ["none", "tie"];
|
|
229
|
+
};
|
|
230
|
+
export type Eyewear = (typeof ACCESSORIES.eyewear)[number];
|
|
231
|
+
export type Headwear = (typeof ACCESSORIES.headwear)[number];
|
|
232
|
+
export type Neckwear = (typeof ACCESSORIES.neckwear)[number];
|
|
233
|
+
export type AccessorySlot = keyof typeof ACCESSORIES;
|
|
221
234
|
export type Face = keyof typeof FACES;
|
|
222
235
|
export type Color = keyof typeof COLORS;
|
|
223
236
|
export type Part = keyof typeof PARTS;
|
|
@@ -226,7 +239,7 @@ export type Brows = (typeof PARTS.brows)[number];
|
|
|
226
239
|
export type Mouth = (typeof PARTS.mouth)[number];
|
|
227
240
|
export type Cheeks = (typeof PARTS.cheeks)[number];
|
|
228
241
|
export type Trait = (typeof PARTS.trait)[number];
|
|
229
|
-
export type Axis = 'face' | 'color' | Part;
|
|
242
|
+
export type Axis = 'face' | 'color' | Part | AccessorySlot;
|
|
230
243
|
/** How much of each list a version may pick from. Append-only too. */
|
|
231
244
|
export declare const VERSIONS: Record<number, Record<Axis, number>>;
|
|
232
245
|
export declare const LATEST = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doan-labs/peek",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
"typescript": "^6.0.2"
|
|
40
40
|
},
|
|
41
41
|
"description": "Deterministic SVG avatars from a name, with expressions and a React component.",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"author": "Doan Labs",
|
|
42
44
|
"homepage": "https://peek.doan-labs.com",
|
|
43
45
|
"repository": {
|
|
44
46
|
"type": "git",
|
package/src/draw.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { Axes, Persona } from './identity'
|
|
13
13
|
import {
|
|
14
|
+
ACCESSORY_INK,
|
|
14
15
|
type Channel,
|
|
15
16
|
type Channels,
|
|
16
17
|
COLORS,
|
|
@@ -74,6 +75,10 @@ const f3 = (v: number) => String(Math.round(v * 1000) / 1000)
|
|
|
74
75
|
const strokeFor = (px: number) =>
|
|
75
76
|
px >= 96 ? 1 : px >= 56 ? 1.25 : px >= 40 ? 1.6 : px >= 30 ? 2 : 2.4
|
|
76
77
|
const RAD = Math.PI / 180
|
|
78
|
+
// the sprout, drawn upward from where it is planted
|
|
79
|
+
const STEM = 'M0 0Q3 -19 0 -36'
|
|
80
|
+
const LEAF0 = 'M0 -33C-24 -30 -38 -52 -25 -53C-12 -54 -2 -43 0 -33Z'
|
|
81
|
+
const LEAF1 = 'M0 -33C5 -54 28 -61 27 -48C26 -36 11 -31 0 -33Z'
|
|
77
82
|
|
|
78
83
|
/** Where a face rests: the body offset, the eye line, one altitude unit. */
|
|
79
84
|
export function frameOf(face: FaceGeom) {
|
|
@@ -760,6 +765,221 @@ export function draw(who: Drawable, pose: Pose, o: DrawOptions): Node {
|
|
|
760
765
|
}
|
|
761
766
|
const trait = n('g', 'trait', { transform: traitT }, [shape])
|
|
762
767
|
|
|
768
|
+
// Wardrobe lives on the same body transform as the face. Glasses grow
|
|
769
|
+
// around the eye sockets, never around the pupils, so gaze stays free.
|
|
770
|
+
const wardrobe: Node[] = []
|
|
771
|
+
// strokes follow the same ramp as the face, capped so small hats stay hats
|
|
772
|
+
const w = (px: number) => f2(px * Math.min(st, 1.6))
|
|
773
|
+
const line = {
|
|
774
|
+
fill: 'none',
|
|
775
|
+
stroke: INK.ink,
|
|
776
|
+
'stroke-width': w(5.25),
|
|
777
|
+
'stroke-linecap': 'round',
|
|
778
|
+
'stroke-linejoin': 'round',
|
|
779
|
+
}
|
|
780
|
+
if (who.eyewear !== 'none') {
|
|
781
|
+
const lx = sh.eyes[0][0] - spread
|
|
782
|
+
const rr = sh.eyes[1][0] + spread
|
|
783
|
+
const ey = sh.eyes[0][1]
|
|
784
|
+
const gap = rr - lx
|
|
785
|
+
const erx = Math.min(rx + 9, (gap - 15) / 2)
|
|
786
|
+
const ery = ry + 9
|
|
787
|
+
const glasses = who.eyewear === 'glasses'
|
|
788
|
+
const lensX = glasses ? erx : Math.min(rx + 11, (gap - 12) / 2)
|
|
789
|
+
const top = glasses ? ey - 4 : ey - ry - 2
|
|
790
|
+
const frames: Node[] = [
|
|
791
|
+
n('path', 'eyewear.bridge', {
|
|
792
|
+
...line,
|
|
793
|
+
d: `M${f2(lx + lensX)} ${f2(top)}Q${f2((lx + rr) / 2)} ${f2(top - (glasses ? 13 : 0))} ${f2(rr - lensX)} ${f2(top)}`,
|
|
794
|
+
}),
|
|
795
|
+
]
|
|
796
|
+
for (const [i, cx] of [lx, rr].entries()) {
|
|
797
|
+
const key = `eyewear.lens${i}`
|
|
798
|
+
frames.push(
|
|
799
|
+
n('path', `${key}.arm`, {
|
|
800
|
+
...line,
|
|
801
|
+
d:
|
|
802
|
+
i === 0
|
|
803
|
+
? `M${f2(cx - lensX - 9)} ${f2(top + 2)}L${f2(cx - lensX)} ${f2(top)}`
|
|
804
|
+
: `M${f2(cx + lensX)} ${f2(top)}L${f2(cx + lensX + 9)} ${f2(top + 2)}`,
|
|
805
|
+
}),
|
|
806
|
+
)
|
|
807
|
+
if (glasses) {
|
|
808
|
+
frames.push(
|
|
809
|
+
n('ellipse', key, {
|
|
810
|
+
...line,
|
|
811
|
+
cx: f2(cx),
|
|
812
|
+
cy: ey,
|
|
813
|
+
rx: f2(erx),
|
|
814
|
+
ry: f2(ery),
|
|
815
|
+
}),
|
|
816
|
+
)
|
|
817
|
+
frames.push(
|
|
818
|
+
n('path', `${key}.glint`, {
|
|
819
|
+
...line,
|
|
820
|
+
stroke: INK.paper,
|
|
821
|
+
'stroke-width': w(3),
|
|
822
|
+
d: `M${f2(cx - erx * 0.65)} ${f2(ey - ery * 0.45)}Q${f2(cx - erx * 0.5)} ${f2(ey - ery * 0.8)} ${f2(cx - erx * 0.2)} ${f2(ey - ery * 0.86)}`,
|
|
823
|
+
}),
|
|
824
|
+
)
|
|
825
|
+
} else {
|
|
826
|
+
const h = ry * 2 + 9
|
|
827
|
+
frames.push(
|
|
828
|
+
n('path', key, {
|
|
829
|
+
d: `M${f2(cx - lensX)} ${f2(top)}H${f2(cx + lensX)}V${f2(top + h * 0.55)}Q${f2(cx + lensX)} ${f2(top + h)} ${f2(cx)} ${f2(top + h)}Q${f2(cx - lensX)} ${f2(top + h)} ${f2(cx - lensX)} ${f2(top + h * 0.55)}Z`,
|
|
830
|
+
fill: INK.ink,
|
|
831
|
+
stroke: INK.ink,
|
|
832
|
+
'stroke-width': w(3),
|
|
833
|
+
'stroke-linejoin': 'round',
|
|
834
|
+
}),
|
|
835
|
+
)
|
|
836
|
+
frames.push(
|
|
837
|
+
n('path', `${key}.glint`, {
|
|
838
|
+
...line,
|
|
839
|
+
stroke: INK.paper,
|
|
840
|
+
'stroke-width': w(4),
|
|
841
|
+
d: `M${f2(cx - lensX * 0.58)} ${f2(top + 19)}l8 -8m-1 15 6 -6`,
|
|
842
|
+
}),
|
|
843
|
+
n('circle', `${key}.pin`, {
|
|
844
|
+
cx: f2(cx + lensX * 0.55),
|
|
845
|
+
cy: f2(top + h - 9),
|
|
846
|
+
r: 1.7,
|
|
847
|
+
fill: INK.paper,
|
|
848
|
+
}),
|
|
849
|
+
)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
wardrobe.push(n('g', 'eyewear', {}, frames))
|
|
853
|
+
}
|
|
854
|
+
if (who.headwear !== 'none') {
|
|
855
|
+
// Reserve the side opposite the crest. The seat stays fixed through
|
|
856
|
+
// expressions, so a moving crest never makes a hat switch sides.
|
|
857
|
+
const side = who.trait === 'ring' || who.trait === 'dot' ? -1 : 1
|
|
858
|
+
const cap = who.headwear === 'cap'
|
|
859
|
+
const sprout = who.headwear === 'sprout'
|
|
860
|
+
// The sheet measures placement across the crown, not along its arc.
|
|
861
|
+
// Solve that horizontal offset so low, round faces keep the brim above
|
|
862
|
+
// their brows instead of walking the hat down the side of the body.
|
|
863
|
+
const inset = sprout ? -6 : cap ? -8 : 0
|
|
864
|
+
const offset = sprout ? 34 : 40
|
|
865
|
+
let lo = 0
|
|
866
|
+
let hi = 160
|
|
867
|
+
for (let i = 0; i < 16; i++) {
|
|
868
|
+
const mid = (lo + hi) / 2
|
|
869
|
+
const p = seat(sh.crown, inset, side * mid)
|
|
870
|
+
if (side * (p.x - 170) < offset) lo = mid
|
|
871
|
+
else hi = mid
|
|
872
|
+
}
|
|
873
|
+
const anchor = seat(sh.crown, inset, (side * (lo + hi)) / 2)
|
|
874
|
+
const angle = clamp(anchor.a, -40, 40)
|
|
875
|
+
const parts: Node[] = []
|
|
876
|
+
if (cap) {
|
|
877
|
+
parts.push(
|
|
878
|
+
n('path', 'headwear.crown', {
|
|
879
|
+
...line,
|
|
880
|
+
fill: INK.paper,
|
|
881
|
+
d: 'M-33 0C-35 -20 -20 -39 0 -39C20 -39 35 -20 33 0Z',
|
|
882
|
+
}),
|
|
883
|
+
n('path', 'headwear.seam', {
|
|
884
|
+
...line,
|
|
885
|
+
'stroke-width': w(3.5),
|
|
886
|
+
d: 'M-4 -38Q9 -24 8 -1',
|
|
887
|
+
}),
|
|
888
|
+
n('path', 'headwear.brim', {
|
|
889
|
+
...line,
|
|
890
|
+
'stroke-width': w(7),
|
|
891
|
+
d: side > 0 ? 'M-35 1Q8 4 52 3' : 'M-52 3Q-8 4 35 1',
|
|
892
|
+
}),
|
|
893
|
+
n('circle', 'headwear.button', {
|
|
894
|
+
cx: 0,
|
|
895
|
+
cy: -40,
|
|
896
|
+
r: 3.5,
|
|
897
|
+
fill: INK.ink,
|
|
898
|
+
}),
|
|
899
|
+
)
|
|
900
|
+
} else if (sprout) {
|
|
901
|
+
parts.push(
|
|
902
|
+
// a body-colored halo keeps the ink stem off an ink ground
|
|
903
|
+
n('path', 'headwear.halo', {
|
|
904
|
+
...line,
|
|
905
|
+
stroke: fill,
|
|
906
|
+
'stroke-width': w(11),
|
|
907
|
+
d: `${STEM}${LEAF0}${LEAF1}`,
|
|
908
|
+
}),
|
|
909
|
+
n('path', 'headwear.stem', {
|
|
910
|
+
...line,
|
|
911
|
+
'stroke-width': w(4.5),
|
|
912
|
+
d: STEM,
|
|
913
|
+
}),
|
|
914
|
+
n('path', 'headwear.leaf0', {
|
|
915
|
+
...line,
|
|
916
|
+
'stroke-width': w(4),
|
|
917
|
+
fill: COLORS.mint.body,
|
|
918
|
+
d: LEAF0,
|
|
919
|
+
}),
|
|
920
|
+
n('path', 'headwear.leaf1', {
|
|
921
|
+
...line,
|
|
922
|
+
'stroke-width': w(4),
|
|
923
|
+
fill: COLORS.mint.deep,
|
|
924
|
+
d: LEAF1,
|
|
925
|
+
}),
|
|
926
|
+
)
|
|
927
|
+
} else {
|
|
928
|
+
parts.push(
|
|
929
|
+
n('path', 'headwear.wings', {
|
|
930
|
+
d: 'M-3 0C-13 -16 -28 -23 -29 -10C-31 2 -26 19 -16 13L-3 4L3 4L16 13C26 19 31 2 29 -10C28 -23 13 -16 3 0Z',
|
|
931
|
+
fill: INK.ink,
|
|
932
|
+
// the same halo, painted under the fill
|
|
933
|
+
stroke: fill,
|
|
934
|
+
'stroke-width': w(6),
|
|
935
|
+
'stroke-linejoin': 'round',
|
|
936
|
+
'paint-order': 'stroke',
|
|
937
|
+
}),
|
|
938
|
+
n('ellipse', 'headwear.knot', {
|
|
939
|
+
cx: 0,
|
|
940
|
+
cy: 1,
|
|
941
|
+
rx: 6,
|
|
942
|
+
ry: 8,
|
|
943
|
+
fill: ACCESSORY_INK.knot,
|
|
944
|
+
}),
|
|
945
|
+
)
|
|
946
|
+
}
|
|
947
|
+
wardrobe.push(
|
|
948
|
+
n(
|
|
949
|
+
'g',
|
|
950
|
+
'headwear',
|
|
951
|
+
{
|
|
952
|
+
transform: `translate(${f2(anchor.x)} ${f2(anchor.y)}) rotate(${f2(sprout ? angle * 0.5 : angle)})`,
|
|
953
|
+
},
|
|
954
|
+
parts,
|
|
955
|
+
),
|
|
956
|
+
)
|
|
957
|
+
}
|
|
958
|
+
if (who.neckwear === 'tie') {
|
|
959
|
+
// The knot follows the mouth's lowest edge, including its skew. The
|
|
960
|
+
// floor clips the blade, just as it clips the creature itself.
|
|
961
|
+
const skew = Math.abs(Math.sin(g('mk') * RAD)) * mw
|
|
962
|
+
const bottom = Math.max(0, mt * kb, mb * kb) + skew + m.sw * st
|
|
963
|
+
wardrobe.push(
|
|
964
|
+
n(
|
|
965
|
+
'g',
|
|
966
|
+
'neckwear',
|
|
967
|
+
{
|
|
968
|
+
transform: `translate(${m.x} ${f2(m.y + g('my') + bottom + 8)})`,
|
|
969
|
+
fill: INK.ink,
|
|
970
|
+
},
|
|
971
|
+
[
|
|
972
|
+
n('path', 'neckwear.knot', {
|
|
973
|
+
d: 'M-9 0Q-11 0 -9 4L-5 11Q0 15 5 11L9 4Q11 0 9 0Z',
|
|
974
|
+
}),
|
|
975
|
+
n('path', 'neckwear.blade', {
|
|
976
|
+
d: 'M-4 15L-13 63Q-14 66 -11 69L0 79L11 69Q14 66 13 63L4 15Z',
|
|
977
|
+
}),
|
|
978
|
+
],
|
|
979
|
+
),
|
|
980
|
+
)
|
|
981
|
+
}
|
|
982
|
+
|
|
763
983
|
// the plates sit a hair out of register at large sizes
|
|
764
984
|
const off = (dx: number, dy: number) =>
|
|
765
985
|
riso ? `translate(${f2(dx * k * upp)} ${f2(dy * k * upp)})` : undefined
|
|
@@ -787,7 +1007,7 @@ export function draw(who: Drawable, pose: Pose, o: DrawOptions): Node {
|
|
|
787
1007
|
transform: off(0.6, -0.5),
|
|
788
1008
|
filter: riso ? `url(#${id}-pB)` : undefined,
|
|
789
1009
|
},
|
|
790
|
-
[...eyes, ...brows, mouth],
|
|
1010
|
+
[...eyes, ...brows, mouth, ...wardrobe],
|
|
791
1011
|
),
|
|
792
1012
|
],
|
|
793
1013
|
)
|
package/src/identity.ts
CHANGED
|
@@ -9,16 +9,20 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
+
ACCESSORIES,
|
|
12
13
|
type Axis,
|
|
13
14
|
type Brows,
|
|
14
15
|
type Cheeks,
|
|
15
16
|
COLORS,
|
|
16
17
|
type Color,
|
|
17
18
|
type Eyes,
|
|
19
|
+
type Eyewear,
|
|
18
20
|
FACES,
|
|
19
21
|
type Face,
|
|
22
|
+
type Headwear,
|
|
20
23
|
LATEST,
|
|
21
24
|
type Mouth,
|
|
25
|
+
type Neckwear,
|
|
22
26
|
PARTS,
|
|
23
27
|
type Trait,
|
|
24
28
|
VERSIONS,
|
|
@@ -74,6 +78,9 @@ export type Axes = {
|
|
|
74
78
|
mouth: Mouth
|
|
75
79
|
cheeks: Cheeks
|
|
76
80
|
trait: Trait
|
|
81
|
+
eyewear: Eyewear
|
|
82
|
+
headwear: Headwear
|
|
83
|
+
neckwear: Neckwear
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
export type Identity = Axes & {
|
|
@@ -90,6 +97,7 @@ export const LISTS: { readonly [A in Axis]: readonly Axes[A][] } = {
|
|
|
90
97
|
face: Object.keys(FACES) as Face[],
|
|
91
98
|
color: Object.keys(COLORS) as Color[],
|
|
92
99
|
...PARTS,
|
|
100
|
+
...ACCESSORIES,
|
|
93
101
|
}
|
|
94
102
|
/** The axes in readout order. */
|
|
95
103
|
export const AXES = Object.keys(LISTS) as Axis[]
|
|
@@ -142,6 +150,9 @@ export function identify(
|
|
|
142
150
|
mouth: pick('mouth'),
|
|
143
151
|
cheeks: pick('cheeks'),
|
|
144
152
|
trait: pick('trait'),
|
|
153
|
+
eyewear: pick('eyewear'),
|
|
154
|
+
headwear: pick('headwear'),
|
|
155
|
+
neckwear: pick('neckwear'),
|
|
145
156
|
persona,
|
|
146
157
|
}
|
|
147
158
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
export type { Frame, Gaze } from './draw'
|
|
1
|
+
export type { Frame, Gaze, Node } from './draw'
|
|
2
|
+
/** Low level: the drawing tree that toSvg and <Peek> both render. */
|
|
3
|
+
export { draw } from './draw'
|
|
2
4
|
export type { Axes, Identity, Persona } from './identity'
|
|
3
5
|
export { AXES, identify, LISTS, tidy } from './identity'
|
|
4
6
|
export type { PeekProps } from './peek'
|
|
5
7
|
export { Peek } from './peek'
|
|
6
8
|
export type { PeekOptions } from './svg'
|
|
7
|
-
export { toSvg } from './svg'
|
|
9
|
+
export { settle, toSvg } from './svg'
|
|
8
10
|
export type {
|
|
11
|
+
AccessorySlot,
|
|
9
12
|
Axis,
|
|
10
13
|
Brows,
|
|
11
14
|
Channel,
|
|
@@ -13,11 +16,15 @@ export type {
|
|
|
13
16
|
Color,
|
|
14
17
|
Expression,
|
|
15
18
|
Eyes,
|
|
19
|
+
Eyewear,
|
|
16
20
|
Face,
|
|
21
|
+
Headwear,
|
|
17
22
|
Mouth,
|
|
23
|
+
Neckwear,
|
|
18
24
|
Trait,
|
|
19
25
|
} from './tables'
|
|
20
26
|
export {
|
|
27
|
+
ACCESSORIES,
|
|
21
28
|
COLORS,
|
|
22
29
|
EXPRESSIONS,
|
|
23
30
|
FACES,
|
package/src/peek.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* come from useId, so the server and the client agree.
|
|
4
4
|
*
|
|
5
5
|
* A name decides identity unless the caller overrides an axis: pass `face`,
|
|
6
|
-
* `color`,
|
|
6
|
+
* `color`, a part, or a wardrobe slot and that axis wins
|
|
7
7
|
* over the hash while every other axis stays as the name made it.
|
|
8
8
|
*
|
|
9
9
|
* With `animate`, the first paint (server and hydration) is still the static
|