mochi-avatar 1.0.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/BRAND.md +85 -0
- package/LICENSE.md +104 -0
- package/README.md +135 -0
- package/dist/canvas2d/avatar.d.ts +264 -0
- package/dist/canvas2d/avatar.d.ts.map +1 -0
- package/dist/canvas2d/avatar.js +811 -0
- package/dist/canvas2d/avatar.js.map +1 -0
- package/dist/canvas2d/face.d.ts +74 -0
- package/dist/canvas2d/face.d.ts.map +1 -0
- package/dist/canvas2d/face.js +298 -0
- package/dist/canvas2d/face.js.map +1 -0
- package/dist/canvas2d/paths.d.ts +13 -0
- package/dist/canvas2d/paths.d.ts.map +1 -0
- package/dist/canvas2d/paths.js +24 -0
- package/dist/canvas2d/paths.js.map +1 -0
- package/dist/characters/colourways.d.ts +78 -0
- package/dist/characters/colourways.d.ts.map +1 -0
- package/dist/characters/colourways.js +76 -0
- package/dist/characters/colourways.js.map +1 -0
- package/dist/characters/index.d.ts +8 -0
- package/dist/characters/index.d.ts.map +1 -0
- package/dist/characters/index.js +8 -0
- package/dist/characters/index.js.map +1 -0
- package/dist/characters/mochi.d.ts +19 -0
- package/dist/characters/mochi.d.ts.map +1 -0
- package/dist/characters/mochi.js +66 -0
- package/dist/characters/mochi.js.map +1 -0
- package/dist/core/colour.d.ts +25 -0
- package/dist/core/colour.d.ts.map +1 -0
- package/dist/core/colour.js +37 -0
- package/dist/core/colour.js.map +1 -0
- package/dist/core/envelope.d.ts +153 -0
- package/dist/core/envelope.d.ts.map +1 -0
- package/dist/core/envelope.js +141 -0
- package/dist/core/envelope.js.map +1 -0
- package/dist/core/geometry.d.ts +106 -0
- package/dist/core/geometry.d.ts.map +1 -0
- package/dist/core/geometry.js +157 -0
- package/dist/core/geometry.js.map +1 -0
- package/dist/core/idle.d.ts +163 -0
- package/dist/core/idle.d.ts.map +1 -0
- package/dist/core/idle.js +262 -0
- package/dist/core/idle.js.map +1 -0
- package/dist/core/layout.d.ts +142 -0
- package/dist/core/layout.d.ts.map +1 -0
- package/dist/core/layout.js +172 -0
- package/dist/core/layout.js.map +1 -0
- package/dist/core/lens.d.ts +47 -0
- package/dist/core/lens.d.ts.map +1 -0
- package/dist/core/lens.js +67 -0
- package/dist/core/lens.js.map +1 -0
- package/dist/core/looks.d.ts +77 -0
- package/dist/core/looks.d.ts.map +1 -0
- package/dist/core/looks.js +168 -0
- package/dist/core/looks.js.map +1 -0
- package/dist/core/motion.d.ts +168 -0
- package/dist/core/motion.d.ts.map +1 -0
- package/dist/core/motion.js +527 -0
- package/dist/core/motion.js.map +1 -0
- package/dist/core/mouth.d.ts +93 -0
- package/dist/core/mouth.d.ts.map +1 -0
- package/dist/core/mouth.js +92 -0
- package/dist/core/mouth.js.map +1 -0
- package/dist/core/plain.d.ts +23 -0
- package/dist/core/plain.d.ts.map +1 -0
- package/dist/core/plain.js +63 -0
- package/dist/core/plain.js.map +1 -0
- package/dist/core/spec.d.ts +156 -0
- package/dist/core/spec.d.ts.map +1 -0
- package/dist/core/spec.js +236 -0
- package/dist/core/spec.js.map +1 -0
- package/dist/core/spring.d.ts +53 -0
- package/dist/core/spring.d.ts.map +1 -0
- package/dist/core/spring.js +68 -0
- package/dist/core/spring.js.map +1 -0
- package/dist/core/vocabulary.d.ts +182 -0
- package/dist/core/vocabulary.d.ts.map +1 -0
- package/dist/core/vocabulary.js +66 -0
- package/dist/core/vocabulary.js.map +1 -0
- package/dist/element/dough-avatar.d.ts +34 -0
- package/dist/element/dough-avatar.d.ts.map +1 -0
- package/dist/element/dough-avatar.js +189 -0
- package/dist/element/dough-avatar.js.map +1 -0
- package/dist/element/index.d.ts +3 -0
- package/dist/element/index.d.ts.map +1 -0
- package/dist/element/index.js +35 -0
- package/dist/element/index.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/svg/silhouette.d.ts +97 -0
- package/dist/svg/silhouette.d.ts.map +1 -0
- package/dist/svg/silhouette.js +143 -0
- package/dist/svg/silhouette.js.map +1 -0
- package/package.json +90 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,kBAAkB,CAAA;AAGhC,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,uBAAuB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A soft-body companion character engine.
|
|
3
|
+
*
|
|
4
|
+
* The shape is a superellipse ovoid with independent exponents above and below
|
|
5
|
+
* its widest point; it squashes area-preservingly, leans by a shear pinned at
|
|
6
|
+
* its contact point, breathes on a one-sided curve and settles on a
|
|
7
|
+
* second-order spring. A face rides on it at a configurable grip, so it deforms
|
|
8
|
+
* with the body without being printed on it.
|
|
9
|
+
*
|
|
10
|
+
* Everything in `core` is pure arithmetic returning points and numbers. Nothing
|
|
11
|
+
* there imports a canvas, a document, or a platform. `canvas2d` turns those
|
|
12
|
+
* points into fills on any 2D context — a browser canvas, an OffscreenCanvas in
|
|
13
|
+
* a worker, or `@napi-rs/canvas` under Node. `svg` emits the same silhouette as
|
|
14
|
+
* a vector, so an icon pipeline and the running app cannot disagree about the
|
|
15
|
+
* shape.
|
|
16
|
+
*
|
|
17
|
+
* A face is DATA — see `FaceSpec` — so a design is a JSON file rather than a
|
|
18
|
+
* fork. `PLAIN` is the default; it is intended to be replaced.
|
|
19
|
+
*/
|
|
20
|
+
// --- core: the character as arithmetic -------------------------------------
|
|
21
|
+
export * from './core/spec.js';
|
|
22
|
+
export * from './core/plain.js';
|
|
23
|
+
export * from './core/vocabulary.js';
|
|
24
|
+
export * from './core/layout.js';
|
|
25
|
+
export * from './core/geometry.js';
|
|
26
|
+
export * from './core/lens.js';
|
|
27
|
+
export * from './core/looks.js';
|
|
28
|
+
export * from './core/idle.js';
|
|
29
|
+
export * from './core/spring.js';
|
|
30
|
+
export * from './core/motion.js';
|
|
31
|
+
export * from './core/envelope.js';
|
|
32
|
+
export * from './core/mouth.js';
|
|
33
|
+
export * from './core/colour.js';
|
|
34
|
+
// --- canvas2d: points into pixels ------------------------------------------
|
|
35
|
+
export * from './canvas2d/avatar.js';
|
|
36
|
+
export * from './canvas2d/face.js';
|
|
37
|
+
export * from './canvas2d/paths.js';
|
|
38
|
+
// --- svg: the same silhouette, as a vector ---------------------------------
|
|
39
|
+
export * from './svg/silhouette.js';
|
|
40
|
+
// --- characters: NOT MIT. See LICENSE.md ------------------------------------
|
|
41
|
+
export * from './characters/index.js';
|
|
42
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,8EAA8E;AAC9E,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,kBAAkB,CAAA;AAEhC,8EAA8E;AAC9E,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AAEnC,8EAA8E;AAC9E,cAAc,qBAAqB,CAAA;AAEnC,+EAA+E;AAC/E,cAAc,uBAAuB,CAAA"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { FaceSpec } from '../core/spec.js';
|
|
2
|
+
import { type BodyShape, type Point } from '../core/geometry.js';
|
|
3
|
+
/**
|
|
4
|
+
* Her, as a vector, from the same geometry that paints her.
|
|
5
|
+
*
|
|
6
|
+
* ## The claim this exists to make true
|
|
7
|
+
*
|
|
8
|
+
* `electron-builder.yml` says it plainly: *"the generator that produced them did
|
|
9
|
+
* not survive the rewrite, so `resources/` is hand-maintained"*. And
|
|
10
|
+
* `shipped-icons.test.ts` says what that cost: the handoff claimed the mark and
|
|
11
|
+
* the mochi "cannot drift", and the test's own header answers that the claim
|
|
12
|
+
* "was not true, it was merely unfalsified".
|
|
13
|
+
*
|
|
14
|
+
* Measuring drift is the second-best answer. This is the first: every shipped
|
|
15
|
+
* asset is rasterised from an SVG emitted **by `domeOutline`**, which is the
|
|
16
|
+
* function `paint` calls to draw her on screen. Change her shape and the icons
|
|
17
|
+
* change with her, because there is one shape and this is it.
|
|
18
|
+
*
|
|
19
|
+
* ## Why SVG rather than emitting PNGs from the canvas directly
|
|
20
|
+
*
|
|
21
|
+
* The rig could rasterise these itself -- `@napi-rs/canvas` is already here and
|
|
22
|
+
* `paint` already knows how. Three things a vector step buys that a direct
|
|
23
|
+
* raster does not:
|
|
24
|
+
*
|
|
25
|
+
* 1. **A source somebody can open.** A designer can look at the shape, and a
|
|
26
|
+
* diff can show what changed about it, without decoding a PNG.
|
|
27
|
+
* 2. **One geometry, many rasterisers.** macOS wants `.icns`, Windows wants
|
|
28
|
+
* `.ico`, the web wants whatever it wants; every one of those pipelines
|
|
29
|
+
* takes SVG, and none of them take a `CanvasRenderingContext2D`.
|
|
30
|
+
* 3. **Resolution is a parameter rather than a re-render.** The 16px mark and
|
|
31
|
+
* the 1024px icon come out of the same document, so they cannot disagree
|
|
32
|
+
* about her proportions -- which is the exact failure the profile test in
|
|
33
|
+
* `shipped-icons.test.ts` was written to detect.
|
|
34
|
+
*
|
|
35
|
+
* ## What is NOT drawn
|
|
36
|
+
*
|
|
37
|
+
* No eyes, no mouth, no cheeks. Every shipped asset is her SILHOUETTE, measured
|
|
38
|
+
* off the existing artwork: a face at 16px is four grey pixels, and the mark has
|
|
39
|
+
* to read at the size a menu bar gives it. `looks.ts` deforms a face; this draws
|
|
40
|
+
* a body, and the two have never been the same picture.
|
|
41
|
+
*/
|
|
42
|
+
/** How one asset is coloured and how much of its square she fills. */
|
|
43
|
+
export interface Treatment {
|
|
44
|
+
/** The lit fill. */
|
|
45
|
+
readonly body: string;
|
|
46
|
+
/** The fill behind it, offset by `shadowX`/`shadowY`. See `paintBody`. */
|
|
47
|
+
readonly shadow: string;
|
|
48
|
+
/**
|
|
49
|
+
* A vertical gradient behind her, or null for transparency.
|
|
50
|
+
*
|
|
51
|
+
* `size` and `radius` describe a PLATE rather than a full-bleed square, and
|
|
52
|
+
* they exist because leaving them out lost an asset. `icons/dock.png` was
|
|
53
|
+
* hand-drawn as an 852px rounded plate inside a 1024 canvas with transparent
|
|
54
|
+
* margin — the pre-masked tile `dock.setIcon` needs, because an image set at
|
|
55
|
+
* runtime gets no squircle from the system. When every asset was moved to
|
|
56
|
+
* this generator the plate became `<rect width="px" height="px">`, a
|
|
57
|
+
* full-bleed square, and the mark it framed went from 62% of a plate to 51%
|
|
58
|
+
* of a tile without a single number changing.
|
|
59
|
+
*
|
|
60
|
+
* Both default to the full square, so every other treatment is untouched.
|
|
61
|
+
*/
|
|
62
|
+
readonly background: {
|
|
63
|
+
readonly from: string;
|
|
64
|
+
readonly to: string;
|
|
65
|
+
/** Side of the plate as a fraction of the square. 1 is full bleed. */
|
|
66
|
+
readonly size?: number;
|
|
67
|
+
/** Corner radius as a fraction of the PLATE, not of the square. */
|
|
68
|
+
readonly radius?: number;
|
|
69
|
+
} | null;
|
|
70
|
+
/**
|
|
71
|
+
* Her width as a fraction of the square.
|
|
72
|
+
*
|
|
73
|
+
* Measured off the shipped artwork rather than chosen: the app icon has her
|
|
74
|
+
* at 0.64 of its width and the Windows tray mark at 0.91, because a mark in a
|
|
75
|
+
* 16px strip has no room for margin and an app icon on a gradient needs one.
|
|
76
|
+
* One number per treatment, so a change of mind is a change of number.
|
|
77
|
+
*/
|
|
78
|
+
readonly width: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Her outline as an SVG path, centred in a `px` square at the given width.
|
|
82
|
+
*
|
|
83
|
+
* Exported because the tests measure it: a profile taken from these points can
|
|
84
|
+
* be compared with one taken from the rig's own `domeOutline` without going
|
|
85
|
+
* near a rasteriser, which is a comparison with no tolerance in it at all.
|
|
86
|
+
*/
|
|
87
|
+
export declare function outlineFor(face: FaceSpec, px: number, width: number): {
|
|
88
|
+
readonly points: readonly Point[];
|
|
89
|
+
readonly shape: BodyShape;
|
|
90
|
+
readonly scale: number;
|
|
91
|
+
readonly offset: {
|
|
92
|
+
readonly dx: number;
|
|
93
|
+
readonly dy: number;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
export declare function mochiSvg(face: FaceSpec, px: number, treatment: Treatment): string;
|
|
97
|
+
//# sourceMappingURL=silhouette.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"silhouette.d.ts","sourceRoot":"","sources":["../../src/svg/silhouette.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAe,KAAK,SAAS,EAAE,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,sEAAsE;AACtE,MAAM,WAAW,SAAS;IACxB,oBAAoB;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,UAAU,EAAE;QACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;QACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;QACnB,sEAAsE;QACtE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;QACtB,mEAAmE;QACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KACzB,GAAG,IAAI,CAAA;IACR;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAaD;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,QAAQ,EACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,GACZ;IACD,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAA;IACjC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;CAC9D,CAiDA;AAsDD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,MAAM,CA8BjF"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { domeOutline } from '../core/geometry.js';
|
|
2
|
+
/** Two decimals is a tenth of a pixel at 1024, and shortens the file by half. */
|
|
3
|
+
function round(value) {
|
|
4
|
+
return Math.round(value * 100) / 100;
|
|
5
|
+
}
|
|
6
|
+
function toD(points) {
|
|
7
|
+
return `${points
|
|
8
|
+
.map((point, index) => `${index === 0 ? 'M' : 'L'}${round(point.x)} ${round(point.y)}`)
|
|
9
|
+
.join('')}Z`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Her outline as an SVG path, centred in a `px` square at the given width.
|
|
13
|
+
*
|
|
14
|
+
* Exported because the tests measure it: a profile taken from these points can
|
|
15
|
+
* be compared with one taken from the rig's own `domeOutline` without going
|
|
16
|
+
* near a rasteriser, which is a comparison with no tolerance in it at all.
|
|
17
|
+
*/
|
|
18
|
+
export function outlineFor(face, px, width) {
|
|
19
|
+
/*
|
|
20
|
+
`width` is her SILHOUETTE, and the offset copy does not extend it.
|
|
21
|
+
|
|
22
|
+
That is only true because the lit copy is clipped to the outline — see
|
|
23
|
+
`mochiSvg`. An earlier draft drew it unclipped and had to solve for the
|
|
24
|
+
overshoot here, which produced a mark 4% too wide and the wrong shape
|
|
25
|
+
entirely: measured, 1.23 wide-to-tall against the rig's 1.28.
|
|
26
|
+
|
|
27
|
+
Clipped, her extent is exactly the outline, so this is the plain division it
|
|
28
|
+
looks like. Checked against the shipped 1024, whose mark is 0.6396 of the
|
|
29
|
+
square wide and 0.499 tall — 0.64 and 0.64/1.2821, to three decimals.
|
|
30
|
+
*/
|
|
31
|
+
const scale = (px * width) / face.bodyW;
|
|
32
|
+
const shape = {
|
|
33
|
+
halfWidth: (face.bodyW / 2) * scale,
|
|
34
|
+
height: face.bodyH * scale,
|
|
35
|
+
waist: face.waist,
|
|
36
|
+
upperShoulder: face.upperShoulder,
|
|
37
|
+
lowerShoulder: face.lowerShoulder,
|
|
38
|
+
// Upright. A lean is something a MOTION does to her; an icon is her at rest,
|
|
39
|
+
// and a mark that leaned would look like a mistake rather than a pose.
|
|
40
|
+
lean: 0,
|
|
41
|
+
};
|
|
42
|
+
const dx = face.shadowX * shape.halfWidth * 2;
|
|
43
|
+
const dy = face.shadowY * shape.height;
|
|
44
|
+
/*
|
|
45
|
+
Centred, and SNAPPED to whole pixels.
|
|
46
|
+
|
|
47
|
+
Centring alone puts her baseline at a fraction — 53.875 in a 64px square —
|
|
48
|
+
so the bottom row of the dome is a partial coverage the rasteriser
|
|
49
|
+
antialiases away, and her base reads narrower than it is. That is not
|
|
50
|
+
theoretical: `window-64.png` was the one asset of thirty-three that failed
|
|
51
|
+
the profile check against the rig, by 0.112 against a tolerance of 0.1, at
|
|
52
|
+
the 5% height band which is exactly where her base is.
|
|
53
|
+
|
|
54
|
+
Snapping costs at most half a pixel of centring and buys a crisp edge at
|
|
55
|
+
every size. It matters most at 16 and 22, where half a pixel is a twentieth
|
|
56
|
+
of the mark.
|
|
57
|
+
*/
|
|
58
|
+
const originX = Math.round(px / 2);
|
|
59
|
+
const originY = Math.round(px / 2 + shape.height / 2);
|
|
60
|
+
// Local space is +y up with the base at 0; SVG is +y down, like the canvas.
|
|
61
|
+
const points = domeOutline(shape).map((point) => ({
|
|
62
|
+
x: originX + point.x,
|
|
63
|
+
y: originY - point.y,
|
|
64
|
+
}));
|
|
65
|
+
return { points, shape, scale, offset: { dx, dy } };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* One asset, as an SVG document.
|
|
69
|
+
*
|
|
70
|
+
* The body is drawn TWICE and the pair is CLIPPED to the first, which is not
|
|
71
|
+
* decoration and is not optional -- it is exactly what `paint` does. The
|
|
72
|
+
* silhouette is clipped before `paintBody` is called, so the lit copy moving up
|
|
73
|
+
* and to the right does not make her bigger: it uncovers a crescent of shadow
|
|
74
|
+
* along her lower left and is cut off everywhere else.
|
|
75
|
+
*
|
|
76
|
+
* Getting this wrong is not subtle and was caught by measurement rather than by
|
|
77
|
+
* eye. Unclipped, the offset copy extends her bounding box by the offset, and
|
|
78
|
+
* the mark comes out **1.23 wide-to-tall against the rig's 1.28** -- outside the
|
|
79
|
+
* tolerance `shipped-icons.test.ts` allows, on the one axis a mark is most
|
|
80
|
+
* recognisable by. Two paths and a `clipPath` reproduce her; two paths alone
|
|
81
|
+
* reproduce something else.
|
|
82
|
+
*/
|
|
83
|
+
/**
|
|
84
|
+
* The plate she sits on: the whole square, or a rounded tile inside it.
|
|
85
|
+
*
|
|
86
|
+
* Centred, and the original artwork was not quite — its margins were 86 either
|
|
87
|
+
* side and 99 over 74 top to bottom, so the plate sat 12px low in a 1024
|
|
88
|
+
* square. That is a tenth of a percent of the tile and reproducing it would be
|
|
89
|
+
* reproducing a wobble rather than a decision.
|
|
90
|
+
*/
|
|
91
|
+
function plateRect(px, background) {
|
|
92
|
+
if (background === null)
|
|
93
|
+
return '';
|
|
94
|
+
/*
|
|
95
|
+
THE SIZE TAKES THE PARITY OF THE TILE, so the margins are equal.
|
|
96
|
+
|
|
97
|
+
Coordinates in the emitted SVG are integers. `at = round((px - size) / 2)`
|
|
98
|
+
with an odd remainder puts the plate half a pixel off centre and leaves
|
|
99
|
+
margins that differ by one — `px = 16` with `size = 13` gives 2 above and 1
|
|
100
|
+
below. The docblock above says "Centred", and at the sizes a tray icon is
|
|
101
|
+
drawn at, one pixel of the sixteen is a visible lean.
|
|
102
|
+
|
|
103
|
+
Losing a pixel of SIZE is the cheaper correction than gaining a pixel of
|
|
104
|
+
offset: the plate is a proportion of the tile and one pixel narrower is
|
|
105
|
+
within the rounding that already happened, while an off-centre plate is a
|
|
106
|
+
different shape from the one asked for.
|
|
107
|
+
*/
|
|
108
|
+
const wanted = Math.round(px * (background.size ?? 1));
|
|
109
|
+
const size = Math.max(0, (px - wanted) % 2 === 0 ? wanted : wanted - 1);
|
|
110
|
+
const at = (px - size) / 2;
|
|
111
|
+
const radius = Math.round(size * (background.radius ?? 0));
|
|
112
|
+
return (`<rect x="${String(at)}" y="${String(at)}" ` +
|
|
113
|
+
`width="${String(size)}" height="${String(size)}" ` +
|
|
114
|
+
(radius > 0 ? `rx="${String(radius)}" ry="${String(radius)}" ` : '') +
|
|
115
|
+
`fill="url(#bg)"/>`);
|
|
116
|
+
}
|
|
117
|
+
export function mochiSvg(face, px, treatment) {
|
|
118
|
+
const { points, offset } = outlineFor(face, px, treatment.width);
|
|
119
|
+
const d = toD(points);
|
|
120
|
+
const dx = round(offset.dx);
|
|
121
|
+
// Canvas and SVG both grow y downward and the light comes from above, so the
|
|
122
|
+
// lit copy moves toward smaller y.
|
|
123
|
+
const dy = round(offset.dy);
|
|
124
|
+
const gradient = treatment.background === null
|
|
125
|
+
? ''
|
|
126
|
+
: `<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">` +
|
|
127
|
+
`<stop offset="0" stop-color="${treatment.background.from}"/>` +
|
|
128
|
+
`<stop offset="1" stop-color="${treatment.background.to}"/>` +
|
|
129
|
+
`</linearGradient>`;
|
|
130
|
+
// OUTSIDE the clip: the plate is what she sits on, and clipping it to her
|
|
131
|
+
// would produce an icon that is only her, on nothing.
|
|
132
|
+
const plate = plateRect(px, treatment.background);
|
|
133
|
+
return (`<svg xmlns="http://www.w3.org/2000/svg" width="${String(px)}" height="${String(px)}" ` +
|
|
134
|
+
`viewBox="0 0 ${String(px)} ${String(px)}">` +
|
|
135
|
+
`<defs>${gradient}<clipPath id="her"><path d="${d}"/></clipPath></defs>` +
|
|
136
|
+
plate +
|
|
137
|
+
`<g clip-path="url(#her)">` +
|
|
138
|
+
`<path d="${d}" fill="${treatment.shadow}"/>` +
|
|
139
|
+
`<path d="${d}" fill="${treatment.body}" transform="translate(${String(dx)} ${String(-dy)})"/>` +
|
|
140
|
+
`</g>` +
|
|
141
|
+
`</svg>`);
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=silhouette.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"silhouette.js","sourceRoot":"","sources":["../../src/svg/silhouette.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mochi-avatar",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Mochi \u2014 a soft-body companion character, and the engine that draws her. Superellipse geometry, area-preserving squash, spring physics and an idle layer. Canvas2D, SVG, or one custom element. Zero runtime dependencies.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
|
+
"author": "HANDO K.K.",
|
|
7
|
+
"homepage": "https://github.com/xiaolai/mochi#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/xiaolai/mochi.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": "https://github.com/xiaolai/mochi/issues",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"sideEffects": [
|
|
15
|
+
"./dist/element/index.js"
|
|
16
|
+
],
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./element": {
|
|
25
|
+
"types": "./dist/element/index.d.ts",
|
|
26
|
+
"default": "./dist/element/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./characters": {
|
|
29
|
+
"types": "./dist/characters/index.d.ts",
|
|
30
|
+
"default": "./dist/characters/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./core/*": {
|
|
33
|
+
"types": "./dist/core/*.d.ts",
|
|
34
|
+
"default": "./dist/core/*.js"
|
|
35
|
+
},
|
|
36
|
+
"./canvas2d/*": {
|
|
37
|
+
"types": "./dist/canvas2d/*.d.ts",
|
|
38
|
+
"default": "./dist/canvas2d/*.js"
|
|
39
|
+
},
|
|
40
|
+
"./svg/*": {
|
|
41
|
+
"types": "./dist/svg/*.d.ts",
|
|
42
|
+
"default": "./dist/svg/*.js"
|
|
43
|
+
},
|
|
44
|
+
"./package.json": "./package.json"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"dist",
|
|
48
|
+
"README.md",
|
|
49
|
+
"LICENSE.md",
|
|
50
|
+
"BRAND.md"
|
|
51
|
+
],
|
|
52
|
+
"keywords": [
|
|
53
|
+
"avatar",
|
|
54
|
+
"character",
|
|
55
|
+
"mascot",
|
|
56
|
+
"canvas",
|
|
57
|
+
"svg",
|
|
58
|
+
"custom-element",
|
|
59
|
+
"web-component",
|
|
60
|
+
"superellipse",
|
|
61
|
+
"spring",
|
|
62
|
+
"animation"
|
|
63
|
+
],
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=20"
|
|
66
|
+
},
|
|
67
|
+
"packageManager": "pnpm@10.33.0",
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "tsc -p tsconfig.build.json",
|
|
70
|
+
"test": "vitest run",
|
|
71
|
+
"test:watch": "vitest",
|
|
72
|
+
"test:coverage": "vitest run --coverage",
|
|
73
|
+
"typecheck": "tsc --noEmit",
|
|
74
|
+
"lint": "oxlint --type-aware",
|
|
75
|
+
"format": "prettier --write --cache .",
|
|
76
|
+
"format:check": "prettier --check --cache .",
|
|
77
|
+
"verify": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test && pnpm build",
|
|
78
|
+
"prepublishOnly": "pnpm verify"
|
|
79
|
+
},
|
|
80
|
+
"devDependencies": {
|
|
81
|
+
"@napi-rs/canvas": "^1.0.6",
|
|
82
|
+
"@types/node": "^26.1.2",
|
|
83
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
84
|
+
"oxlint": "1.77.0",
|
|
85
|
+
"oxlint-tsgolint": "7.0.2001",
|
|
86
|
+
"prettier": "3.9.6",
|
|
87
|
+
"typescript": "^7.0.2",
|
|
88
|
+
"vitest": "^4.1.10"
|
|
89
|
+
}
|
|
90
|
+
}
|