@heyramzi/ui 2.11.0 → 2.11.2
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/dist/agents/helpers.js +38 -38
- package/dist/agents/page-state.js +49 -50
- package/dist/agents/vendor/color.js +146 -127
- package/dist/agents/vendor/draw.js +617 -508
- package/dist/agents/vendor/engine.js +698 -599
- package/dist/agents/vendor/mesh.js +653 -683
- package/dist/agents/vendor/plastic.js +3913 -3112
- package/dist/agents/vendor/presets.js +163 -164
- package/dist/agents/vendor/shapes.js +21 -20
- package/dist/agents/vendor/ticker.js +41 -42
- package/dist/agents/vendor/types.js +1 -0
- package/dist/agents/vendor/wear.js +1421 -1448
- package/dist/ai/confirmation/confirmation-context.svelte.js +6 -6
- package/dist/ai/confirmation/index.js +3 -1
- package/dist/ai/index.js +33 -0
- package/dist/ai/prompt-input/index.js +3 -1
- package/dist/ai/reasoning/index.js +3 -1
- package/dist/ai/reasoning/reasoning-context.svelte.js +6 -6
- package/dist/ai/suggestion/index.js +3 -1
- package/dist/ai/typing-dots/index.js +3 -1
- package/dist/animations/text-swap.js +24 -24
- package/dist/brand/index.js +54 -48
- package/dist/canvas/card/resize.js +5 -3
- package/dist/canvas/edges/edge-path.js +255 -242
- package/dist/canvas/editor/context-menu.js +39 -36
- package/dist/canvas/editor/dispatch.svelte.js +154 -151
- package/dist/canvas/editor/editor-context.js +7 -2
- package/dist/canvas/editor/editor-mode.js +1 -0
- package/dist/canvas/editor/frame.js +73 -76
- package/dist/canvas/editor/hand-context.js +5 -2
- package/dist/canvas/editor/history-core.js +163 -191
- package/dist/canvas/editor/keymap.js +95 -169
- package/dist/canvas/editor/layout-actions.js +143 -144
- package/dist/canvas/editor/lock.js +52 -51
- package/dist/canvas/editor/rebase.js +217 -240
- package/dist/canvas/editor/style.js +132 -137
- package/dist/canvas/editor/text-field-context.js +7 -2
- package/dist/canvas/editor/z-order.js +65 -70
- package/dist/canvas/headless/alignment.js +22 -16
- package/dist/canvas/headless/break-apart-find.js +286 -279
- package/dist/canvas/headless/break-apart-pieces.js +62 -58
- package/dist/canvas/headless/break-apart-plan.js +299 -325
- package/dist/canvas/headless/break-apart-style.js +75 -70
- package/dist/canvas/headless/break-apart.js +188 -181
- package/dist/canvas/headless/canvas-cleanup.js +78 -58
- package/dist/canvas/headless/edge-anchors.js +115 -116
- package/dist/canvas/headless/edge-memo.js +24 -22
- package/dist/canvas/headless/edge-serialization.js +17 -27
- package/dist/canvas/headless/figure-geometry.js +232 -264
- package/dist/canvas/headless/fit-viewport.js +84 -67
- package/dist/canvas/headless/grid.js +88 -86
- package/dist/canvas/headless/icon-node.js +44 -36
- package/dist/canvas/headless/index.js +1 -0
- package/dist/canvas/headless/linkify.js +17 -19
- package/dist/canvas/headless/pen-marks.js +101 -95
- package/dist/canvas/headless/sketch.js +231 -209
- package/dist/canvas/headless/snap-guides.js +99 -83
- package/dist/canvas/headless/svg-figure.js +133 -117
- package/dist/canvas/headless/table-layout.js +146 -107
- package/dist/canvas/index.js +2 -0
- package/dist/canvas/lod/lod-mode.svelte.js +20 -18
- package/dist/canvas/lod/render-all.svelte.js +36 -39
- package/dist/canvas/ops/arrange.js +78 -71
- package/dist/canvas/ops/ops.js +260 -255
- package/dist/canvas/pen/colors.js +37 -35
- package/dist/canvas/pen/eraser.js +28 -27
- package/dist/canvas/pen/geometry.js +120 -130
- package/dist/canvas/pen/index.js +1 -0
- package/dist/canvas/pen/ops.js +11 -11
- package/dist/canvas/pen/pen-session.js +150 -159
- package/dist/canvas/pen/plugin.js +107 -104
- package/dist/canvas/pen/shapes.js +73 -68
- package/dist/canvas/pen/stroke-path.js +32 -30
- package/dist/canvas/pen/types.js +21 -21
- package/dist/canvas/pen/ui.js +10 -8
- package/dist/canvas/plugin.js +46 -46
- package/dist/canvas/screen/glyphs.js +12 -11
- package/dist/canvas/screen/labels.js +22 -22
- package/dist/canvas/screen/parts.js +98 -111
- package/dist/canvas/screen/plugin.js +47 -56
- package/dist/canvas/screen/types.js +30 -35
- package/dist/canvas/screen/ui.js +7 -7
- package/dist/canvas/test-utils.js +14 -14
- package/dist/canvas/types.js +1 -0
- package/dist/canvas/ui/forward-wheel.js +24 -25
- package/dist/canvas/ui/icons.js +64 -131
- package/dist/canvas/ui/place-panel.js +58 -49
- package/dist/canvas/ui/shortcut-tip.js +14 -12
- package/dist/canvas/ui/tools.js +300 -385
- package/dist/charts/dashboard/chart.js +4 -0
- package/dist/charts/dashboard/data-table.js +11 -0
- package/dist/charts/dashboard/windows.js +8 -3
- package/dist/charts/edge-fade.js +10 -5
- package/dist/charts/hover-band.js +16 -11
- package/dist/charts/index.js +0 -1
- package/dist/charts/motion.js +11 -7
- package/dist/charts/primitives/chart-utils.js +12 -12
- package/dist/components/copy/clipboard.js +32 -32
- package/dist/components/copy/copied.svelte.js +31 -23
- package/dist/components/data-table/data-table.js +39 -39
- package/dist/components/docs/index.js +4 -0
- package/dist/components/docs/scroll-spy.svelte.js +73 -68
- package/dist/components/list-page/filter-section.js +4 -1
- package/dist/components/list-page/index.js +2 -0
- package/dist/components/list-page/list-table.js +7 -2
- package/dist/components/list-page/queue-rail.js +14 -0
- package/dist/components/list-page/section-host.js +4 -2
- package/dist/components/list-page/step.js +4 -3
- package/dist/components/list-page/types.js +1 -0
- package/dist/components/picker/trigger.js +48 -30
- package/dist/components/picker/types.js +1 -0
- package/dist/components/segmented/collapse.js +38 -27
- package/dist/components/segmented/index.js +8 -0
- package/dist/components/segmented/segmented.js +67 -42
- package/dist/components/star-rating-input/index.js +3 -1
- package/dist/components/table-view/types.js +1 -0
- package/dist/components/tags-input/tags.js +81 -69
- package/dist/components/theme/index.js +4 -0
- package/dist/components/toolbar/index.js +4 -0
- package/dist/components/toolbar/toolbar-trigger.js +14 -1
- package/dist/editor/anchors.js +17 -13
- package/dist/editor/block-id.js +52 -51
- package/dist/editor/block-ops.js +108 -80
- package/dist/editor/doc-blocks.js +155 -166
- package/dist/editor/extensions.js +139 -180
- package/dist/editor/index.js +4 -0
- package/dist/editor/markdown-test-helpers.js +4 -2
- package/dist/editor/markdown.js +420 -408
- package/dist/editor/serializer.js +204 -200
- package/dist/editor/slash-items.js +89 -101
- package/dist/editor/youtube-url.js +25 -20
- package/dist/effects/animated-beam/path-calculator.svelte.js +43 -38
- package/dist/effects/animated-beam/types.js +1 -0
- package/dist/effects/border-beam/css.js +92 -78
- package/dist/effects/border-beam/presets.js +1705 -1720
- package/dist/effects/border-beam/register-properties.js +43 -33
- package/dist/effects/border-beam/types.js +1 -0
- package/dist/effects/image-comparison/compare.js +3 -2
- package/dist/effects/image-reveal/types.js +1 -0
- package/dist/effects/magnetic/magnetic.js +58 -52
- package/dist/effects/reveal/observe.js +50 -40
- package/dist/effects/reveal/types.js +1 -0
- package/dist/effects/shader-gradient/color.js +36 -29
- package/dist/effects/shader-gradient/renderer.js +384 -295
- package/dist/effects/shader-gradient/shaders.js +31 -0
- package/dist/effects/shader-gradient/types.js +1 -0
- package/dist/icons/apple-symbol-exports.js +99 -124
- package/dist/icons/sf-symbols.js +357 -461
- package/dist/icons/trades.js +43 -28
- package/dist/inbox/index.js +1 -0
- package/dist/inbox/logic.js +126 -122
- package/dist/inbox/types.js +43 -43
- package/dist/lib/arrive.js +19 -12
- package/dist/lib/icon-types.js +1 -0
- package/dist/lib/initials.js +16 -10
- package/dist/lib/layout.js +5 -0
- package/dist/lib/panel.js +13 -0
- package/dist/lib/result.js +22 -22
- package/dist/lib/sliding-pill.js +59 -44
- package/dist/lib/theme-transition.js +43 -43
- package/dist/lib/tone.js +31 -24
- package/dist/lib/utils.js +3 -1
- package/dist/onboarding/define-tours.js +24 -23
- package/dist/onboarding/onboarding.svelte.js +16 -12
- package/dist/onboarding/pick.js +10 -6
- package/dist/onboarding/stores.js +40 -41
- package/dist/onboarding/wait.js +24 -23
- package/dist/panel/side-panel-types.js +1 -0
- package/dist/record/demo-data.js +459 -486
- package/dist/record/fields.js +129 -131
- package/dist/record/format.js +70 -82
- package/dist/record/gutter.js +1 -0
- package/dist/record/kinds.js +95 -105
- package/dist/record/logic.js +238 -219
- package/dist/record/types.js +49 -48
- package/dist/shadcn/accordion/index.js +3 -1
- package/dist/shadcn/alert/index.js +3 -1
- package/dist/shadcn/alert-dialog/index.js +3 -1
- package/dist/shadcn/attachment/index.js +3 -1
- package/dist/shadcn/avatar/index.js +3 -1
- package/dist/shadcn/breadcrumb/index.js +3 -1
- package/dist/shadcn/bubble/index.js +3 -1
- package/dist/shadcn/button/index.js +3 -1
- package/dist/shadcn/calendar/index.js +3 -1
- package/dist/shadcn/card/index.js +3 -1
- package/dist/shadcn/checkbox/index.js +3 -1
- package/dist/shadcn/code-block/index.js +3 -1
- package/dist/shadcn/collapsible/index.js +3 -1
- package/dist/shadcn/command/index.js +4 -1
- package/dist/shadcn/context-menu/index.js +3 -1
- package/dist/shadcn/dialog/index.js +3 -1
- package/dist/shadcn/dropdown-menu/menu-item.js +40 -20
- package/dist/shadcn/empty/index.js +3 -1
- package/dist/shadcn/field/index.js +3 -1
- package/dist/shadcn/hooks/highlighter.js +77 -75
- package/dist/shadcn/hooks/is-mobile.svelte.js +3 -3
- package/dist/shadcn/hooks/use-highlighted-code.svelte.js +62 -62
- package/dist/shadcn/input/index.js +3 -1
- package/dist/shadcn/kbd/index.js +3 -1
- package/dist/shadcn/label/index.js +3 -1
- package/dist/shadcn/marker/index.js +3 -1
- package/dist/shadcn/message/index.js +3 -1
- package/dist/shadcn/message-scroller/index.js +3 -1
- package/dist/shadcn/popover/index.js +3 -1
- package/dist/shadcn/progress/index.js +3 -1
- package/dist/shadcn/progress/tone.js +8 -7
- package/dist/shadcn/radio-group/index.js +3 -1
- package/dist/shadcn/scroll-area/index.js +3 -1
- package/dist/shadcn/select/index.js +3 -1
- package/dist/shadcn/separator/index.js +3 -1
- package/dist/shadcn/sheet/index.js +3 -1
- package/dist/shadcn/sidebar/context.svelte.js +42 -28
- package/dist/shadcn/sidebar/index.js +3 -1
- package/dist/shadcn/skeleton/index.js +3 -1
- package/dist/shadcn/slider/index.js +3 -1
- package/dist/shadcn/switch/index.js +3 -1
- package/dist/shadcn/table/index.js +3 -1
- package/dist/shadcn/tabs/index.js +3 -1
- package/dist/shadcn/textarea/index.js +3 -1
- package/dist/shadcn/toggle/index.js +3 -1
- package/dist/shadcn/toggle-group/index.js +3 -1
- package/dist/shadcn/tooltip/context.js +7 -2
- package/dist/shadcn/tooltip/index.js +3 -1
- package/dist/shadcn/trace/index.js +3 -1
- package/dist/slides/bridges.js +65 -59
- package/dist/slides/deck-controller.svelte.js +210 -207
- package/dist/slides/export/capture.js +66 -71
- package/dist/slides/export/ffmpeg.js +76 -72
- package/dist/slides/export/index.js +3 -0
- package/dist/slides/export/launch.js +9 -6
- package/dist/slides/export/pdf.js +60 -54
- package/dist/slides/export/types.js +1 -0
- package/dist/slides/labels.js +24 -24
- package/dist/slides/layout.js +39 -41
- package/dist/slides/model.js +9 -7
- package/dist/slides/navigation.js +78 -73
- package/dist/slides/types.js +2 -2
- package/dist/status/define-statuses.js +18 -14
- package/dist/status/status-types.js +1 -0
- package/dist/test-setup.js +24 -20
- package/dist/thread/logic.js +73 -77
- package/dist/thread/types.js +21 -21
- package/dist/work/board-order.js +94 -90
- package/dist/work/dates.js +16 -9
- package/dist/work/detail/detail-types.js +18 -18
- package/dist/work/docs/edited.js +32 -29
- package/dist/work/docs/index.js +4 -0
- package/dist/work/docs/labels.js +34 -34
- package/dist/work/docs/menu.js +113 -106
- package/dist/work/docs/outline.js +39 -29
- package/dist/work/gantt/logic.js +211 -235
- package/dist/work/inbox/inbox-logic.js +71 -75
- package/dist/work/inbox/inbox-statuses.js +75 -72
- package/dist/work/inbox/inbox-types.js +1 -0
- package/dist/work/kanban-drag.js +27 -22
- package/dist/work/kanban-types.js +1 -0
- package/dist/work/presets/sales.js +36 -32
- package/dist/work/reorder.js +9 -8
- package/dist/work/task-types.js +1 -0
- package/dist/work/time/duration.js +51 -46
- package/dist/work/time/time-types.js +23 -23
- package/dist/work/use-kanban-dnd.svelte.js +126 -125
- package/package.json +3 -1
- package/src/components/list-page/master-detail.svelte +3 -1
- package/src/components/list-page/rail-row.svelte +4 -4
- package/src/components/tags-input/tags-input.svelte +2 -0
- package/src/editor/markdown.ts +30 -35
|
@@ -1,1525 +1,1498 @@
|
|
|
1
1
|
import { rasterize, PAD, SPAN } from "./plastic.js";
|
|
2
2
|
import { shade, parseColor } from "./color.js";
|
|
3
3
|
import { makeMesh, makeTube, makeView, projectMesh, section, paintLayers, boundsOf, hull, norm, cross, sub, segs, felt, velvet, paper, plastic, satin, gold, silver, metal, sphereSprite, cutGem, scale } from "./mesh.js";
|
|
4
|
+
/* where a hat sits: the first row, from the top, where the head is at
|
|
5
|
+
least `frac` of its full width — a narrow top (a star's point, a
|
|
6
|
+
triangle's apex) seats a hat lower than a round one */
|
|
4
7
|
function seat(m, frac) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
const W = m.right - m.left;
|
|
9
|
+
for (let i = 0; i < m.widths.length; i++) if (m.widths[i] >= frac * W) return i * m.step - PAD;
|
|
10
|
+
return m.top;
|
|
8
11
|
}
|
|
9
12
|
const marks = new WeakMap();
|
|
10
13
|
function marksFor(path) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
left: toX(bl) - 0.5 * u,
|
|
73
|
-
right: toX(br) + 0.5 * u,
|
|
74
|
-
bottom: toX(bottom) + 0.5 * u,
|
|
75
|
-
widths,
|
|
76
|
-
step: u,
|
|
77
|
-
hull: hull(edge)
|
|
78
|
-
};
|
|
79
|
-
marks.set(path, m);
|
|
80
|
-
return m;
|
|
14
|
+
const hit = marks.get(path);
|
|
15
|
+
if (hit) return hit;
|
|
16
|
+
const N = 128;
|
|
17
|
+
const cov = rasterize(path, N);
|
|
18
|
+
if (!cov) return null;
|
|
19
|
+
const u = SPAN / N;
|
|
20
|
+
const toX = (i) => (i + .5) * u - PAD;
|
|
21
|
+
const inside = (x, y) => cov[y * N + x] >= 128;
|
|
22
|
+
const c0 = Math.floor((42 + PAD) / u), c1 = Math.ceil((58 + PAD) / u);
|
|
23
|
+
let top = N, bottom = -1;
|
|
24
|
+
for (let x = c0; x <= c1; x++) {
|
|
25
|
+
for (let y = 0; y < N; y++) if (inside(x, y)) {
|
|
26
|
+
if (y < top) top = y;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
for (let y = N - 1; y >= 0; y--) if (inside(x, y)) {
|
|
30
|
+
if (y > bottom) bottom = y;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const row = (y) => {
|
|
35
|
+
let l = -1, r = -1;
|
|
36
|
+
for (let x = 0; x < N; x++) if (inside(x, y)) {
|
|
37
|
+
if (l < 0) l = x;
|
|
38
|
+
r = x;
|
|
39
|
+
}
|
|
40
|
+
return [l, r];
|
|
41
|
+
};
|
|
42
|
+
const yT = Math.min(N - 1, top + Math.round(8 / u));
|
|
43
|
+
const [tl, tr] = row(yT);
|
|
44
|
+
let best = -1, bl = 0, br = 0, by = 0;
|
|
45
|
+
for (let y = Math.round((33 + PAD) / u); y <= Math.round((67 + PAD) / u); y++) {
|
|
46
|
+
const [l, r] = row(y);
|
|
47
|
+
if (l >= 0 && r - l > best) {
|
|
48
|
+
best = r - l;
|
|
49
|
+
bl = l;
|
|
50
|
+
br = r;
|
|
51
|
+
by = y;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const widths = new Float32Array(N);
|
|
55
|
+
const edge = [];
|
|
56
|
+
for (let y = 0; y < N; y++) {
|
|
57
|
+
const [l, r] = row(y);
|
|
58
|
+
widths[y] = l < 0 ? 0 : (r - l + 1) * u;
|
|
59
|
+
if (l >= 0) edge.push([toX(l) - .5 * u, toX(y)], [toX(r) + .5 * u, toX(y)]);
|
|
60
|
+
}
|
|
61
|
+
const m = {
|
|
62
|
+
top: toX(top) - .5 * u,
|
|
63
|
+
topL: toX(tl),
|
|
64
|
+
topR: toX(tr),
|
|
65
|
+
mid: toX(by),
|
|
66
|
+
left: toX(bl) - .5 * u,
|
|
67
|
+
right: toX(br) + .5 * u,
|
|
68
|
+
bottom: toX(bottom) + .5 * u,
|
|
69
|
+
widths,
|
|
70
|
+
step: u,
|
|
71
|
+
hull: hull(edge)
|
|
72
|
+
};
|
|
73
|
+
marks.set(path, m);
|
|
74
|
+
return m;
|
|
81
75
|
}
|
|
76
|
+
/* ── projection ─────────────────────────────────────────────────────── */
|
|
77
|
+
/* the slices' orthographic turn: design (x, y) about the centre, z along
|
|
78
|
+
the depth in design units (positive toward the front cap) */
|
|
82
79
|
function project(r, p) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
r.cy * X + r.sy * p[2],
|
|
86
|
-
r.sy * r.sp * X + r.cp * Y - r.cy * r.sp * p[2]
|
|
87
|
-
];
|
|
80
|
+
const X = p[0] - 50, Y = p[1] - 50;
|
|
81
|
+
return [r.cy * X + r.sy * p[2], r.sy * r.sp * X + r.cp * Y - r.cy * r.sp * p[2]];
|
|
88
82
|
}
|
|
83
|
+
/* how far a point sits toward the viewer */
|
|
89
84
|
function depthOf(r, p) {
|
|
90
|
-
|
|
85
|
+
return -r.sy * r.cp * (p[0] - 50) + r.sp * (p[1] - 50) + r.cy * r.cp * p[2];
|
|
91
86
|
}
|
|
87
|
+
/* rotate a local offset by a tilt about the depth axis, then a lean about x */
|
|
92
88
|
function tilt(v, roll, lean) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
89
|
+
const cr = Math.cos(roll), sr = Math.sin(roll);
|
|
90
|
+
const x = v[0] * cr - v[1] * sr, y = v[0] * sr + v[1] * cr;
|
|
91
|
+
const cl = Math.cos(lean), sl = Math.sin(lean);
|
|
92
|
+
return [
|
|
93
|
+
x,
|
|
94
|
+
y * cl - v[2] * sl,
|
|
95
|
+
y * sl + v[2] * cl
|
|
96
|
+
];
|
|
101
97
|
}
|
|
102
|
-
const add = (a, b)=>[
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const along = (a, d, k)=>[
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
98
|
+
const add = (a, b) => [
|
|
99
|
+
a[0] + b[0],
|
|
100
|
+
a[1] + b[1],
|
|
101
|
+
a[2] + b[2]
|
|
102
|
+
];
|
|
103
|
+
const along = (a, d, k) => [
|
|
104
|
+
a[0] + d[0] * k,
|
|
105
|
+
a[1] + d[1] * k,
|
|
106
|
+
a[2] + d[2] * k
|
|
107
|
+
];
|
|
112
108
|
function polyPath(pts, close = true) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
const p = new Path2D();
|
|
110
|
+
pts.forEach((q, i) => i ? p.lineTo(q[0], q[1]) : p.moveTo(q[0], q[1]));
|
|
111
|
+
if (close) p.closePath();
|
|
112
|
+
return p;
|
|
117
113
|
}
|
|
118
114
|
function bounds(pts) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
115
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
116
|
+
for (const [x, y] of pts) {
|
|
117
|
+
if (x < x0) x0 = x;
|
|
118
|
+
if (y < y0) y0 = y;
|
|
119
|
+
if (x > x1) x1 = x;
|
|
120
|
+
if (y > y1) y1 = y;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
x0,
|
|
124
|
+
y0,
|
|
125
|
+
x1,
|
|
126
|
+
y1,
|
|
127
|
+
cx: (x0 + x1) / 2,
|
|
128
|
+
cy: (y0 + y1) / 2,
|
|
129
|
+
w: x1 - x0,
|
|
130
|
+
h: y1 - y0
|
|
131
|
+
};
|
|
136
132
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
133
|
+
/* a stable pseudo-random per index, so fuzz does not shimmer frame to frame */
|
|
134
|
+
const rnd = (i, k = 0) => {
|
|
135
|
+
let h = i * 2654435761 + k * 40503 | 0;
|
|
136
|
+
h = Math.imul(h ^ h >>> 15, 2246822507);
|
|
137
|
+
h = Math.imul(h ^ h >>> 13, 3266489909);
|
|
138
|
+
return ((h ^ h >>> 16) >>> 0) / 4294967296;
|
|
142
139
|
};
|
|
140
|
+
/* ── texture and small touches ──────────────────────────────────────── */
|
|
141
|
+
/* felt's grain: a small tile of soft noise, multiplied over felt faintly */
|
|
143
142
|
let grain = null;
|
|
144
143
|
function grainTile() {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
144
|
+
if (grain) return grain;
|
|
145
|
+
const n = 48;
|
|
146
|
+
const c = typeof OffscreenCanvas === "function" ? new OffscreenCanvas(n, n) : typeof document !== "undefined" ? Object.assign(document.createElement("canvas"), {
|
|
147
|
+
width: n,
|
|
148
|
+
height: n
|
|
149
|
+
}) : null;
|
|
150
|
+
const g = c && c.getContext("2d");
|
|
151
|
+
if (!c || !g) return null;
|
|
152
|
+
const img = g.createImageData(n, n);
|
|
153
|
+
for (let i = 0; i < n * n; i++) {
|
|
154
|
+
/* short diagonal fibres: noise smeared along one direction */
|
|
155
|
+
const x = i % n, y = i / n | 0;
|
|
156
|
+
let v = 0;
|
|
157
|
+
for (let k = -2; k <= 2; k++) v += rnd((x + k + n) % n + (y + k + n) % n * n, 5);
|
|
158
|
+
v /= 5;
|
|
159
|
+
const m = Math.round(255 * (.82 + .36 * v));
|
|
160
|
+
img.data[i * 4] = img.data[i * 4 + 1] = img.data[i * 4 + 2] = Math.min(255, m);
|
|
161
|
+
img.data[i * 4 + 3] = 255;
|
|
162
|
+
}
|
|
163
|
+
g.putImageData(img, 0, 0);
|
|
164
|
+
grain = c;
|
|
165
|
+
return c;
|
|
166
166
|
}
|
|
167
|
+
/* the grain, laid over a painted outline; its pattern made once per
|
|
168
|
+
context and scale */
|
|
167
169
|
const patterns = new WeakMap();
|
|
168
|
-
function grainOver(ctx, outline, alpha, scaleK =
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
170
|
+
function grainOver(ctx, outline, alpha, scaleK = .18) {
|
|
171
|
+
const tile = grainTile();
|
|
172
|
+
if (!tile || outline.length < 3) return;
|
|
173
|
+
let byScale = patterns.get(ctx);
|
|
174
|
+
if (!byScale) patterns.set(ctx, byScale = new Map());
|
|
175
|
+
let pat = byScale.get(scaleK);
|
|
176
|
+
if (pat === undefined) {
|
|
177
|
+
pat = ctx.createPattern(tile, "repeat");
|
|
178
|
+
pat?.setTransform?.(new DOMMatrix().scale(scaleK));
|
|
179
|
+
byScale.set(scaleK, pat);
|
|
180
|
+
}
|
|
181
|
+
if (!pat) return;
|
|
182
|
+
const b = bounds(outline);
|
|
183
|
+
ctx.save();
|
|
184
|
+
ctx.clip(polyPath(outline));
|
|
185
|
+
ctx.globalCompositeOperation = "multiply";
|
|
186
|
+
ctx.globalAlpha = alpha;
|
|
187
|
+
ctx.fillStyle = pat;
|
|
188
|
+
ctx.fillRect(b.x0 - 1, b.y0 - 1, b.w + 2, b.h + 2);
|
|
189
|
+
ctx.restore();
|
|
188
190
|
}
|
|
191
|
+
/* short hairs standing off an outline: a felt or pile edge. Two batched
|
|
192
|
+
strokes, a lighter and a darker set, rather than one call a hair. */
|
|
189
193
|
function fuzz(ctx, outline, base, seed, len, px = 2.4) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
194
|
+
const n = outline.length;
|
|
195
|
+
if (n < 3) return;
|
|
196
|
+
let total = 0;
|
|
197
|
+
const seg = [];
|
|
198
|
+
for (let i = 0; i < n; i++) {
|
|
199
|
+
const a = outline[i], c = outline[(i + 1) % n];
|
|
200
|
+
const d = Math.hypot(c[0] - a[0], c[1] - a[1]);
|
|
201
|
+
seg.push(d);
|
|
202
|
+
total += d;
|
|
203
|
+
}
|
|
204
|
+
/* about a hair every other device pixel along the edge */
|
|
205
|
+
const count = Math.min(360, Math.max(24, Math.round(total * Math.min(2.4, px * .5))));
|
|
206
|
+
const b = bounds(outline);
|
|
207
|
+
const lightP = new Path2D(), darkP = new Path2D();
|
|
208
|
+
let si = 0, acc = 0;
|
|
209
|
+
for (let k = 0; k < count; k++) {
|
|
210
|
+
const want = (k + rnd(k, seed) * .8) * (total / count);
|
|
211
|
+
while (si < n - 1 && acc + seg[si] < want) acc += seg[si++];
|
|
212
|
+
const a = outline[si], c = outline[(si + 1) % n];
|
|
213
|
+
const t = seg[si] ? (want - acc) / seg[si] : 0;
|
|
214
|
+
const x = a[0] + (c[0] - a[0]) * t, y = a[1] + (c[1] - a[1]) * t;
|
|
215
|
+
/* outward: away from the shape's centre, turned a little at random */
|
|
216
|
+
let ox = x - b.cx, oy = y - b.cy;
|
|
217
|
+
const ol = Math.hypot(ox, oy) || 1;
|
|
218
|
+
ox /= ol;
|
|
219
|
+
oy /= ol;
|
|
220
|
+
const ang = (rnd(k, seed + 1) - .5) * .9;
|
|
221
|
+
const ca = Math.cos(ang), sa = Math.sin(ang);
|
|
222
|
+
const hx = ox * ca - oy * sa, hy = ox * sa + oy * ca;
|
|
223
|
+
const l = len * (.35 + rnd(k, seed + 2));
|
|
224
|
+
const p = rnd(k, seed + 3) > .5 ? lightP : darkP;
|
|
225
|
+
p.moveTo(x - hx * .4, y - hy * .4);
|
|
226
|
+
p.lineTo(x + hx * l, y + hy * l);
|
|
227
|
+
}
|
|
228
|
+
ctx.save();
|
|
229
|
+
ctx.lineCap = "round";
|
|
230
|
+
ctx.lineWidth = .4;
|
|
231
|
+
ctx.globalAlpha = .55;
|
|
232
|
+
ctx.strokeStyle = shade(base, .14);
|
|
233
|
+
ctx.stroke(lightP);
|
|
234
|
+
ctx.strokeStyle = shade(base, -.06);
|
|
235
|
+
ctx.stroke(darkP);
|
|
236
|
+
ctx.restore();
|
|
231
237
|
}
|
|
238
|
+
/* a soft contact shadow cast on the body under an object's lower edge */
|
|
232
239
|
function contact(ctx, clip, cx, cy, rx, ry, a) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
240
|
+
if (!(rx > 0) || !(ry > 0)) return;
|
|
241
|
+
ctx.save();
|
|
242
|
+
if (clip) ctx.clip(clip);
|
|
243
|
+
ctx.translate(cx, cy);
|
|
244
|
+
ctx.scale(1, ry / rx);
|
|
245
|
+
const g = ctx.createRadialGradient(0, 0, 0, 0, 0, rx);
|
|
246
|
+
g.addColorStop(0, `rgba(0,0,0,${a})`);
|
|
247
|
+
g.addColorStop(.55, `rgba(0,0,0,${a * .45})`);
|
|
248
|
+
g.addColorStop(1, "rgba(0,0,0,0)");
|
|
249
|
+
ctx.fillStyle = g;
|
|
250
|
+
ctx.beginPath();
|
|
251
|
+
ctx.arc(0, 0, rx, 0, Math.PI * 2);
|
|
252
|
+
ctx.fill();
|
|
253
|
+
ctx.restore();
|
|
247
254
|
}
|
|
255
|
+
/* ── the view for the meshes ───────────────────────────────────────── */
|
|
256
|
+
/* light elevation off the screen plane, as the body's material has it */
|
|
248
257
|
const EL = 48 * Math.PI / 180;
|
|
249
258
|
function lightOf(lx, ly) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
259
|
+
const le = Math.cos(EL), lz = Math.sin(EL);
|
|
260
|
+
const L = norm([
|
|
261
|
+
lx * le,
|
|
262
|
+
ly * le,
|
|
263
|
+
lz
|
|
264
|
+
]);
|
|
265
|
+
return {
|
|
266
|
+
L,
|
|
267
|
+
H: norm([
|
|
268
|
+
L[0],
|
|
269
|
+
L[1],
|
|
270
|
+
L[2] + 1
|
|
271
|
+
])
|
|
272
|
+
};
|
|
264
273
|
}
|
|
274
|
+
/* device pixels per unit of the context's current space */
|
|
265
275
|
function pxOf(ctx) {
|
|
266
|
-
|
|
267
|
-
|
|
276
|
+
const t = ctx.getTransform?.();
|
|
277
|
+
return t ? Math.sqrt(Math.abs(t.a * t.d - t.b * t.c)) || 1 : 2;
|
|
268
278
|
}
|
|
269
279
|
function viewOf(r, px) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
280
|
+
const { L, H } = lightOf(r.lx, r.ly);
|
|
281
|
+
/* the rows of project() and depthOf() */
|
|
282
|
+
return makeView([
|
|
283
|
+
r.cy,
|
|
284
|
+
0,
|
|
285
|
+
r.sy,
|
|
286
|
+
r.sy * r.sp,
|
|
287
|
+
r.cp,
|
|
288
|
+
-r.cy * r.sp,
|
|
289
|
+
-r.sy * r.cp,
|
|
290
|
+
r.sp,
|
|
291
|
+
r.cy * r.cp
|
|
292
|
+
], [
|
|
293
|
+
50,
|
|
294
|
+
50,
|
|
295
|
+
0
|
|
296
|
+
], L, H, px);
|
|
286
297
|
}
|
|
298
|
+
/* The meshes, built once per shape, part and grid size and kept: a frame
|
|
299
|
+
only projects them. `owner` is what they belong to — the shape's marks,
|
|
300
|
+
or the glasses. */
|
|
287
301
|
const kept = new WeakMap();
|
|
288
302
|
let owner = kept;
|
|
289
303
|
function keep(key, make) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
304
|
+
let c = kept.get(owner);
|
|
305
|
+
if (!c) kept.set(owner, c = new Map());
|
|
306
|
+
let g = c.get(key);
|
|
307
|
+
if (!g) {
|
|
308
|
+
if (c.size > 240) c.clear();
|
|
309
|
+
g = make();
|
|
310
|
+
c.set(key, g);
|
|
311
|
+
}
|
|
312
|
+
return g;
|
|
299
313
|
}
|
|
300
314
|
function surface(view, key, surf, nu, nv, inside, smooth = true) {
|
|
301
|
-
|
|
315
|
+
return projectMesh(view, keep(`${key}|${nu}x${nv}`, () => makeMesh(surf, nu, nv, inside, smooth)));
|
|
302
316
|
}
|
|
303
317
|
function tube(view, key, path, up, prof, nu, nv, closed = false) {
|
|
304
|
-
|
|
318
|
+
return projectMesh(view, keep(`${key}|${nu}x${nv}`, () => makeTube(path, up, prof, nu, nv, closed)));
|
|
305
319
|
}
|
|
306
320
|
const rgbOf = (c, fallback = [
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
])=>parseColor(c) ?? fallback;
|
|
321
|
+
40,
|
|
322
|
+
40,
|
|
323
|
+
44
|
|
324
|
+
]) => parseColor(c) ?? fallback;
|
|
311
325
|
const TAU = Math.PI * 2;
|
|
312
|
-
const smooth01 = (t)=>t <= 0 ? 0 : t >= 1 ? 1 : t * t * (3 - 2 * t);
|
|
313
|
-
|
|
326
|
+
const smooth01 = (t) => t <= 0 ? 0 : t >= 1 ? 1 : t * t * (3 - 2 * t);
|
|
327
|
+
/* a signed power, for superellipses */
|
|
328
|
+
const spow = (c, e) => Math.sign(c) * Math.pow(Math.abs(c), e);
|
|
329
|
+
/* a sphere, as a mesh: pompoms and the like */
|
|
314
330
|
function sphereMesh(view, key, c, rad, nu, nv) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
331
|
+
return surface(view, key, (u, v) => {
|
|
332
|
+
const th = u * TAU, ph = v * Math.PI;
|
|
333
|
+
return [
|
|
334
|
+
c[0] + rad * Math.sin(ph) * Math.cos(th),
|
|
335
|
+
c[1] - rad * Math.cos(ph),
|
|
336
|
+
c[2] + rad * Math.sin(ph) * Math.sin(th)
|
|
337
|
+
];
|
|
338
|
+
}, nu, nv, c);
|
|
323
339
|
}
|
|
340
|
+
/* a ring round a centre line in a hat's own frame: a binding, a trim, a
|
|
341
|
+
rim — a tube round a circle, each normal turned away from the circle */
|
|
324
342
|
function ringMesh(view, key, place, R, y, t, zScale, nu, nv) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
343
|
+
return surface(view, key, (u, v) => {
|
|
344
|
+
const th = u * TAU, ph = v * TAU;
|
|
345
|
+
const rr = R + t * Math.cos(ph);
|
|
346
|
+
return place([
|
|
347
|
+
rr * Math.cos(th),
|
|
348
|
+
y - t * Math.sin(ph),
|
|
349
|
+
rr * zScale * Math.sin(th)
|
|
350
|
+
]);
|
|
351
|
+
}, nu, nv, (u) => place([
|
|
352
|
+
R * Math.cos(u * TAU),
|
|
353
|
+
y,
|
|
354
|
+
R * zScale * Math.sin(u * TAU)
|
|
355
|
+
]));
|
|
338
356
|
}
|
|
357
|
+
/* a path moved sideways off itself, in the plane across `up` */
|
|
339
358
|
function offsetPath(path, up, off) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
359
|
+
return (u) => {
|
|
360
|
+
const e = .001;
|
|
361
|
+
const T = norm(sub(path(Math.min(1, u + e)), path(Math.max(0, u - e))));
|
|
362
|
+
const B = cross(T, up);
|
|
363
|
+
return along(path(u), B, off(u));
|
|
364
|
+
};
|
|
346
365
|
}
|
|
366
|
+
/* a yarn or tinsel pompom: a shaded ball under a mass of short strands,
|
|
367
|
+
lighter and darker, standing out all round */
|
|
347
368
|
function yarnBall(ctx, view, c, rad, base, seed) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
ctx.globalAlpha = 0.6;
|
|
386
|
-
ctx.stroke(dark);
|
|
387
|
-
ctx.restore();
|
|
369
|
+
const rgb = rgbOf(base);
|
|
370
|
+
const n = segs(view, TAU * rad, 12, 20);
|
|
371
|
+
const vis = paintLayers(ctx, [{
|
|
372
|
+
parts: [sphereMesh(view, `ball${seed}`, c, rad * .86, n, Math.max(6, n >> 1))],
|
|
373
|
+
shade: felt(view, rgb, .92)
|
|
374
|
+
}], view.px);
|
|
375
|
+
const centre = view.toScreen(c);
|
|
376
|
+
const b = bounds(vis);
|
|
377
|
+
const R = Math.max(b.w, b.h) / 2;
|
|
378
|
+
const count = Math.round(Math.min(340, 50 + R * view.px * 5));
|
|
379
|
+
const light = new Path2D(), dark = new Path2D();
|
|
380
|
+
for (let k = 0; k < count; k++) {
|
|
381
|
+
const a = rnd(k, seed) * TAU;
|
|
382
|
+
const r0 = R * (.25 + .7 * Math.sqrt(rnd(k, seed + 1)));
|
|
383
|
+
const len = R * (.25 + .35 * rnd(k, seed + 2));
|
|
384
|
+
const x0 = centre[0] + Math.cos(a) * r0, y0 = centre[1] + Math.sin(a) * r0;
|
|
385
|
+
const bend = (rnd(k, seed + 3) - .5) * .6;
|
|
386
|
+
const ex = Math.cos(a + bend), ey = Math.sin(a + bend);
|
|
387
|
+
const p = rnd(k, seed + 4) > .45 ? light : dark;
|
|
388
|
+
p.moveTo(x0, y0);
|
|
389
|
+
p.quadraticCurveTo(x0 + Math.cos(a) * len * .5, y0 + Math.sin(a) * len * .5, x0 + ex * len, y0 + ey * len);
|
|
390
|
+
}
|
|
391
|
+
ctx.save();
|
|
392
|
+
ctx.lineCap = "round";
|
|
393
|
+
ctx.lineWidth = Math.max(.3, R * .06);
|
|
394
|
+
/* the strands take the ball's own light: lighter toward the light */
|
|
395
|
+
const L = view.L;
|
|
396
|
+
const g = ctx.createLinearGradient(centre[0] + L[0] * R, centre[1] + L[1] * R, centre[0] - L[0] * R, centre[1] - L[1] * R);
|
|
397
|
+
g.addColorStop(0, shade(base, .2, -.03));
|
|
398
|
+
g.addColorStop(1, shade(base, -.12, .02));
|
|
399
|
+
ctx.strokeStyle = g;
|
|
400
|
+
ctx.globalAlpha = .85;
|
|
401
|
+
ctx.stroke(light);
|
|
402
|
+
ctx.strokeStyle = shade(base, -.18, .03);
|
|
403
|
+
ctx.globalAlpha = .6;
|
|
404
|
+
ctx.stroke(dark);
|
|
405
|
+
ctx.restore();
|
|
388
406
|
}
|
|
407
|
+
/* ── the hats ───────────────────────────────────────────────────────── */
|
|
408
|
+
/* The beret: a felt dome settled over the crown of the head, its widest
|
|
409
|
+
ring a little above the band and the underside tucking in to it, slid
|
|
410
|
+
and tipped over to one side the way a beret is worn. Soft pleats run
|
|
411
|
+
round it, so the light breaks along the brim; a narrow leather binding
|
|
412
|
+
sits where it meets the head; a short stalk stands on top. */
|
|
389
413
|
function drawBeret(ctx, view, m, base, bodyClip) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
], view.px);
|
|
491
|
-
sphereSprite(ctx, view, place([
|
|
492
|
-
x0 + 0.014 * W,
|
|
493
|
-
y0 - 0.047 * W,
|
|
494
|
-
0
|
|
495
|
-
]), 0.019 * W, scale(rgb, 1.15));
|
|
414
|
+
const W = m.right - m.left;
|
|
415
|
+
const R = .46 * W, Hh = .21 * W, Rb = .33 * W, lift = .05 * W;
|
|
416
|
+
const C = [
|
|
417
|
+
50 - .07 * W,
|
|
418
|
+
Math.max(m.top + .22 * W, seat(m, .5) + .1 * W),
|
|
419
|
+
0
|
|
420
|
+
];
|
|
421
|
+
/* a negative lean tips the top toward the viewer, so the crown of the
|
|
422
|
+
beret shows and its underside stays hidden */
|
|
423
|
+
const roll = -.24, lean = -.2;
|
|
424
|
+
const place = (p) => add(C, tilt(p, roll, lean));
|
|
425
|
+
const nu = segs(view, TAU * R, 32, 56), nv = segs(view, 1.2 * R, 10, 32, 6);
|
|
426
|
+
const surf = (u, v) => {
|
|
427
|
+
const th = u * TAU;
|
|
428
|
+
let rr, hh;
|
|
429
|
+
const E = .74;
|
|
430
|
+
if (v <= E) {
|
|
431
|
+
const phi = v / E * (Math.PI / 2);
|
|
432
|
+
rr = R * Math.sin(phi);
|
|
433
|
+
/* a soft, slightly flattened top */
|
|
434
|
+
hh = -(lift + Hh * Math.pow(Math.cos(phi), .8));
|
|
435
|
+
} else {
|
|
436
|
+
const f = smooth01((v - E) / (1 - E));
|
|
437
|
+
rr = R + (Rb - R) * f;
|
|
438
|
+
hh = -lift * (1 - f);
|
|
439
|
+
}
|
|
440
|
+
/* pleats: strongest round the widest ring, gone at the top */
|
|
441
|
+
const band = Math.sin(Math.min(1, v / E) * Math.PI * .9);
|
|
442
|
+
rr *= 1 + .035 * Math.sin(th * 7 + .4) * band;
|
|
443
|
+
return place([
|
|
444
|
+
rr * Math.cos(th),
|
|
445
|
+
hh,
|
|
446
|
+
rr * .93 * Math.sin(th)
|
|
447
|
+
]);
|
|
448
|
+
};
|
|
449
|
+
const dome = surface(view, "beret:dome", surf, nu, nv, place([
|
|
450
|
+
0,
|
|
451
|
+
-lift - Hh * .4,
|
|
452
|
+
0
|
|
453
|
+
]));
|
|
454
|
+
/* the underside inside the band, closed */
|
|
455
|
+
const under = surface(view, "beret:under", (u, v) => {
|
|
456
|
+
const th = u * TAU, rr = Rb * (1 - v);
|
|
457
|
+
return place([
|
|
458
|
+
rr * Math.cos(th),
|
|
459
|
+
-.004 * W * v,
|
|
460
|
+
rr * .93 * Math.sin(th)
|
|
461
|
+
]);
|
|
462
|
+
}, 24, 2, place([
|
|
463
|
+
0,
|
|
464
|
+
-lift - Hh * .4,
|
|
465
|
+
0
|
|
466
|
+
]));
|
|
467
|
+
const rgb = rgbOf(base);
|
|
468
|
+
/* its shadow on the head, under the band */
|
|
469
|
+
const bandMid = view.toScreen(place([
|
|
470
|
+
0,
|
|
471
|
+
.02 * W,
|
|
472
|
+
0
|
|
473
|
+
]));
|
|
474
|
+
contact(ctx, bodyClip, bandMid[0] + .02 * W, bandMid[1] + .045 * W, Rb * 1.25, .08 * W, .4);
|
|
475
|
+
const binding = ringMesh(view, "beret:binding", place, Rb, -.005 * W, .022 * W, .93, segs(view, TAU * Rb, 24, 48), 5);
|
|
476
|
+
const leather = rgbOf(shade(base, -.06));
|
|
477
|
+
const vis = paintLayers(ctx, [
|
|
478
|
+
{
|
|
479
|
+
parts: [under],
|
|
480
|
+
shade: felt(view, rgb, .7)
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
parts: [binding],
|
|
484
|
+
shade: plastic(view, leather, .45)
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
parts: [dome],
|
|
488
|
+
shade: felt(view, rgb)
|
|
489
|
+
}
|
|
490
|
+
], view.px);
|
|
491
|
+
const outline = hull(vis);
|
|
492
|
+
grainOver(ctx, outline, .5, .14);
|
|
493
|
+
fuzz(ctx, outline, base, 11, .4, view.px);
|
|
494
|
+
/* the stalk: a short tail of felt on the crown, a little bent */
|
|
495
|
+
const x0 = .02 * W, y0 = -lift - Hh + .006 * W;
|
|
496
|
+
const stalk = tube(view, "beret:stalk", (u) => place([
|
|
497
|
+
x0 + .014 * W * u * u,
|
|
498
|
+
y0 - .045 * W * u,
|
|
499
|
+
0
|
|
500
|
+
]), [
|
|
501
|
+
0,
|
|
502
|
+
0,
|
|
503
|
+
1
|
|
504
|
+
], (u, ph) => [.016 * W * (1 - .25 * u) * Math.cos(ph), .016 * W * (1 - .25 * u) * Math.sin(ph)], 8, 3);
|
|
505
|
+
paintLayers(ctx, [{
|
|
506
|
+
parts: [stalk],
|
|
507
|
+
shade: felt(view, rgb, .95)
|
|
508
|
+
}], view.px);
|
|
509
|
+
sphereSprite(ctx, view, place([
|
|
510
|
+
x0 + .014 * W,
|
|
511
|
+
y0 - .047 * W,
|
|
512
|
+
0
|
|
513
|
+
]), .019 * W, scale(rgb, 1.15));
|
|
496
514
|
}
|
|
515
|
+
/* The beanie: a knitted dome with a folded, ribbed cuff and a yarn pompom.
|
|
516
|
+
Every stitch is drawn — a V in each cell of the dome, pointing to the
|
|
517
|
+
brim, with the yarn's light edge beside its dark gap — and the cuff's
|
|
518
|
+
ribs rise and fall round it. */
|
|
497
519
|
function drawBeanie(ctx, view, m, base, bodyClip) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
], view.px);
|
|
628
|
-
const ribGap = TAU * Rc / 30;
|
|
629
|
-
if (ribGap * view.px >= 3) {
|
|
630
|
-
const groove = new Path2D(), ridge = new Path2D();
|
|
631
|
-
for(let k = 0; k < 60; k++){
|
|
632
|
-
const u = k / 60;
|
|
633
|
-
const th = u * TAU;
|
|
634
|
-
if (view.dir(tilt([
|
|
635
|
-
Math.cos(th),
|
|
636
|
-
0,
|
|
637
|
-
0.92 * Math.sin(th)
|
|
638
|
-
], roll, lean))[2] < 0.08) continue;
|
|
639
|
-
const a = view.toScreen(cuffSurf(u, 0.1)), b = view.toScreen(cuffSurf(u, 0.9));
|
|
640
|
-
const p = k % 2 ? groove : ridge;
|
|
641
|
-
p.moveTo(a[0], a[1]);
|
|
642
|
-
p.lineTo(b[0], b[1]);
|
|
643
|
-
}
|
|
644
|
-
ctx.save();
|
|
645
|
-
ctx.lineCap = "round";
|
|
646
|
-
ctx.lineWidth = ribGap * 0.34;
|
|
647
|
-
ctx.strokeStyle = shade(base, -0.2, 0.03);
|
|
648
|
-
ctx.globalAlpha = 0.45;
|
|
649
|
-
ctx.stroke(groove);
|
|
650
|
-
ctx.lineWidth = ribGap * 0.22;
|
|
651
|
-
ctx.strokeStyle = shade(base, 0.16, -0.02);
|
|
652
|
-
ctx.globalAlpha = 0.4;
|
|
653
|
-
ctx.stroke(ridge);
|
|
654
|
-
ctx.restore();
|
|
655
|
-
}
|
|
656
|
-
const outline = hull(visDome.concat(visCuff));
|
|
657
|
-
fuzz(ctx, outline, base, 23, 0.35, view.px);
|
|
658
|
-
yarnBall(ctx, view, place([
|
|
659
|
-
0,
|
|
660
|
-
-cuffH - Hd - 0.055 * W,
|
|
661
|
-
0
|
|
662
|
-
]), 0.085 * W, shade(base, 0.06), 31);
|
|
520
|
+
const W = m.right - m.left;
|
|
521
|
+
const headW = Math.max(.5 * W, m.topR - m.topL);
|
|
522
|
+
const Rd = .53 * headW + .08 * W, Hd = .32 * W, cuffH = .1 * W, Rc = Rd * 1.05;
|
|
523
|
+
const C = [
|
|
524
|
+
50,
|
|
525
|
+
seat(m, .72) + .06 * W,
|
|
526
|
+
0
|
|
527
|
+
];
|
|
528
|
+
const roll = 0, lean = -.14;
|
|
529
|
+
const place = (p) => add(C, tilt(p, roll, lean));
|
|
530
|
+
const domeSurf = (u, v) => {
|
|
531
|
+
const th = u * TAU, phi = v * (Math.PI / 2);
|
|
532
|
+
const rr = Rd * Math.sin(phi) * (1 - .03 * (1 - v));
|
|
533
|
+
const hh = -(cuffH + Hd * Math.cos(phi));
|
|
534
|
+
return place([
|
|
535
|
+
rr * Math.cos(th),
|
|
536
|
+
hh,
|
|
537
|
+
rr * .92 * Math.sin(th)
|
|
538
|
+
]);
|
|
539
|
+
};
|
|
540
|
+
const inDome = place([
|
|
541
|
+
0,
|
|
542
|
+
-cuffH - Hd * .4,
|
|
543
|
+
0
|
|
544
|
+
]);
|
|
545
|
+
const dome = surface(view, "beanie:dome", domeSurf, segs(view, TAU * Rd, 28, 48), segs(view, 1.3 * Hd, 9, 24, 6), inDome);
|
|
546
|
+
const cuffSurf = (u, v) => {
|
|
547
|
+
const th = u * TAU;
|
|
548
|
+
const e = 2 * v - 1;
|
|
549
|
+
const rr = Rc * (1 - .07 * e * e * e * e) * 1.01;
|
|
550
|
+
return place([
|
|
551
|
+
rr * Math.cos(th),
|
|
552
|
+
-cuffH * (1 - v),
|
|
553
|
+
rr * .92 * Math.sin(th)
|
|
554
|
+
]);
|
|
555
|
+
};
|
|
556
|
+
const cuff = surface(view, "beanie:cuff", cuffSurf, segs(view, TAU * Rc, 28, 56), 4, place([
|
|
557
|
+
0,
|
|
558
|
+
-cuffH / 2,
|
|
559
|
+
0
|
|
560
|
+
]));
|
|
561
|
+
const rgb = rgbOf(base), cuffRgb = rgbOf(shade(base, .03));
|
|
562
|
+
const bb = boundsOf([cuff]);
|
|
563
|
+
contact(ctx, bodyClip, bb.cx, bb.y1 - .01 * W, bb.w * .5, .06 * W, .34);
|
|
564
|
+
const visDome = paintLayers(ctx, [{
|
|
565
|
+
parts: [dome],
|
|
566
|
+
shade: felt(view, rgb, .96)
|
|
567
|
+
}], view.px);
|
|
568
|
+
/* the stitches: one V per cell of their own grid, dark gap then light
|
|
569
|
+
yarn, only on the side facing the viewer and only when big enough */
|
|
570
|
+
const NU = 40, NV = 11;
|
|
571
|
+
const cell = Rd * TAU / NU * view.px;
|
|
572
|
+
if (cell >= 2.2) {
|
|
573
|
+
const grid = surface(view, "beanie:stitch", domeSurf, NU, NV, inDome, false);
|
|
574
|
+
const dark = new Path2D(), light = new Path2D();
|
|
575
|
+
const GW = NU + 1;
|
|
576
|
+
for (let c = 0; c < NU * NV; c++) {
|
|
577
|
+
if (grid.f[c * 3 + 2] < .12) continue;
|
|
578
|
+
const k = Math.floor(c / NU) * GW + c % NU;
|
|
579
|
+
const A = [grid.x[k], grid.y[k]], B = [grid.x[k + 1], grid.y[k + 1]];
|
|
580
|
+
const Cq = [grid.x[k + GW + 1], grid.y[k + GW + 1]], D = [grid.x[k + GW], grid.y[k + GW]];
|
|
581
|
+
const at = (s, t) => {
|
|
582
|
+
const top = [A[0] + (B[0] - A[0]) * s, A[1] + (B[1] - A[1]) * s];
|
|
583
|
+
const bot = [D[0] + (Cq[0] - D[0]) * s, D[1] + (Cq[1] - D[1]) * s];
|
|
584
|
+
return [top[0] + (bot[0] - top[0]) * t, top[1] + (bot[1] - top[1]) * t];
|
|
585
|
+
};
|
|
586
|
+
for (const [p, dx] of [[dark, 0], [light, -.06]]) {
|
|
587
|
+
const a = at(.14 + dx, .12), b = at(.5 + dx, .88), c = at(.86 + dx, .12);
|
|
588
|
+
p.moveTo(a[0], a[1]);
|
|
589
|
+
p.lineTo(b[0], b[1]);
|
|
590
|
+
p.lineTo(c[0], c[1]);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const cw = Rd * TAU / NU;
|
|
594
|
+
ctx.save();
|
|
595
|
+
ctx.clip(polyPath(hull(visDome)));
|
|
596
|
+
ctx.lineCap = "round";
|
|
597
|
+
ctx.lineJoin = "round";
|
|
598
|
+
ctx.lineWidth = Math.max(.35, cw * .2);
|
|
599
|
+
ctx.strokeStyle = shade(base, -.22, .03);
|
|
600
|
+
ctx.globalAlpha = .5;
|
|
601
|
+
ctx.stroke(dark);
|
|
602
|
+
ctx.lineWidth = Math.max(.3, cw * .12);
|
|
603
|
+
ctx.strokeStyle = shade(base, .2, -.03);
|
|
604
|
+
ctx.globalAlpha = .35;
|
|
605
|
+
ctx.stroke(light);
|
|
606
|
+
ctx.restore();
|
|
607
|
+
}
|
|
608
|
+
/* the cuff over the dome's lower edge, and its ribs: a groove and a
|
|
609
|
+
ridge of yarn every twelfth of a turn, on the side toward the viewer */
|
|
610
|
+
const visCuff = paintLayers(ctx, [{
|
|
611
|
+
parts: [cuff],
|
|
612
|
+
shade: felt(view, cuffRgb, .98)
|
|
613
|
+
}], view.px);
|
|
614
|
+
const ribGap = TAU * Rc / 30;
|
|
615
|
+
if (ribGap * view.px >= 3) {
|
|
616
|
+
const groove = new Path2D(), ridge = new Path2D();
|
|
617
|
+
for (let k = 0; k < 60; k++) {
|
|
618
|
+
const u = k / 60;
|
|
619
|
+
const th = u * TAU;
|
|
620
|
+
if (view.dir(tilt([
|
|
621
|
+
Math.cos(th),
|
|
622
|
+
0,
|
|
623
|
+
.92 * Math.sin(th)
|
|
624
|
+
], roll, lean))[2] < .08) continue;
|
|
625
|
+
const a = view.toScreen(cuffSurf(u, .1)), b = view.toScreen(cuffSurf(u, .9));
|
|
626
|
+
const p = k % 2 ? groove : ridge;
|
|
627
|
+
p.moveTo(a[0], a[1]);
|
|
628
|
+
p.lineTo(b[0], b[1]);
|
|
629
|
+
}
|
|
630
|
+
ctx.save();
|
|
631
|
+
ctx.lineCap = "round";
|
|
632
|
+
ctx.lineWidth = ribGap * .34;
|
|
633
|
+
ctx.strokeStyle = shade(base, -.2, .03);
|
|
634
|
+
ctx.globalAlpha = .45;
|
|
635
|
+
ctx.stroke(groove);
|
|
636
|
+
ctx.lineWidth = ribGap * .22;
|
|
637
|
+
ctx.strokeStyle = shade(base, .16, -.02);
|
|
638
|
+
ctx.globalAlpha = .4;
|
|
639
|
+
ctx.stroke(ridge);
|
|
640
|
+
ctx.restore();
|
|
641
|
+
}
|
|
642
|
+
const outline = hull(visDome.concat(visCuff));
|
|
643
|
+
fuzz(ctx, outline, base, 23, .35, view.px);
|
|
644
|
+
yarnBall(ctx, view, place([
|
|
645
|
+
0,
|
|
646
|
+
-cuffH - Hd - .055 * W,
|
|
647
|
+
0
|
|
648
|
+
]), .085 * W, shade(base, .06), 31);
|
|
663
649
|
}
|
|
650
|
+
/* The party hat: a paper cone with stripes that wrap it, a foil trim at
|
|
651
|
+
the base and a tinsel pompom at the tip. */
|
|
664
652
|
function drawParty(ctx, view, m, base, bodyClip) {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
724
|
-
], view.px);
|
|
725
|
-
grainOver(ctx, hull(vis), 0.18, 0.1);
|
|
726
|
-
yarnBall(ctx, view, place([
|
|
727
|
-
0,
|
|
728
|
-
-H - 0.035 * W,
|
|
729
|
-
0
|
|
730
|
-
]), 0.06 * W, "#f4efe6", 37);
|
|
653
|
+
const W = m.right - m.left;
|
|
654
|
+
const C = [
|
|
655
|
+
50 + .06 * W,
|
|
656
|
+
seat(m, .3) + .04 * W,
|
|
657
|
+
0
|
|
658
|
+
];
|
|
659
|
+
/* as tall as the canvas has room for above the head */
|
|
660
|
+
const R = .18 * W, H = Math.max(.3 * W, Math.min(.46 * W, C[1] + 31 - .1 * W));
|
|
661
|
+
const roll = .18, lean = -.12;
|
|
662
|
+
const place = (p) => add(C, tilt(p, roll, lean));
|
|
663
|
+
const coneAt = (u, v, grow = 1) => {
|
|
664
|
+
const th = u * TAU;
|
|
665
|
+
const rr = R * (1 - v) * (1 + .04 * Math.sin(v * Math.PI)) * grow;
|
|
666
|
+
return place([
|
|
667
|
+
rr * Math.cos(th),
|
|
668
|
+
-H * v,
|
|
669
|
+
rr * Math.sin(th)
|
|
670
|
+
]);
|
|
671
|
+
};
|
|
672
|
+
const inCone = (_u, v) => place([
|
|
673
|
+
0,
|
|
674
|
+
-H * Math.min(.97, v),
|
|
675
|
+
0
|
|
676
|
+
]);
|
|
677
|
+
const nAround = segs(view, TAU * R, 20, 40), nUp = segs(view, H, 10, 24, 6);
|
|
678
|
+
const cone = surface(view, "party:cone", (u, v) => coneAt(u, v), nAround, nUp, inCone);
|
|
679
|
+
const a = rgbOf(base), b = [
|
|
680
|
+
252,
|
|
681
|
+
246,
|
|
682
|
+
234
|
|
683
|
+
];
|
|
684
|
+
const bb = boundsOf([cone]);
|
|
685
|
+
contact(ctx, bodyClip, bb.cx, bb.y1, R * 1.15, .04 * W, .3);
|
|
686
|
+
const trim = ringMesh(view, "party:trim", place, R * 1.02, .004 * W, .016 * W, 1, segs(view, TAU * R, 20, 40), 5);
|
|
687
|
+
/* the stripes: bands spiralling up the cone, each its own strip of the
|
|
688
|
+
cone's surface, so their edges run smooth rather than stepping */
|
|
689
|
+
const stripes = [];
|
|
690
|
+
const K = 6, twist = 3.2;
|
|
691
|
+
for (let k = 0; k < K; k += 1) {
|
|
692
|
+
stripes.push(surface(view, `party:stripe${k}`, (s, t) => coneAt((k + .5 * t - twist * s * .97) / K, s * .97, 1.004), nUp + 3, 1, inCone));
|
|
693
|
+
}
|
|
694
|
+
const vis = paintLayers(ctx, [{
|
|
695
|
+
parts: [cone],
|
|
696
|
+
shade: paper(view, b)
|
|
697
|
+
}, {
|
|
698
|
+
parts: [trim],
|
|
699
|
+
shade: gold(view)
|
|
700
|
+
}], view.px);
|
|
701
|
+
paintLayers(ctx, [{
|
|
702
|
+
parts: stripes,
|
|
703
|
+
shade: paper(view, a)
|
|
704
|
+
}], view.px);
|
|
705
|
+
grainOver(ctx, hull(vis), .18, .1);
|
|
706
|
+
yarnBall(ctx, view, place([
|
|
707
|
+
0,
|
|
708
|
+
-H - .035 * W,
|
|
709
|
+
0
|
|
710
|
+
]), .06 * W, "#f4efe6", 37);
|
|
731
711
|
}
|
|
712
|
+
/* The crown: a gold band between two rolled rims, five points with a
|
|
713
|
+
ridge up each and a pearl on its tip, small gold buds between them, a
|
|
714
|
+
cut stone in a gold setting under every point, a puffed velvet cap
|
|
715
|
+
filling it, and an orb and cross on top. */
|
|
732
716
|
function drawCrown(ctx, view, m, bodyClip) {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
]);
|
|
940
|
-
sphereSprite(ctx, view, orb, orbR, "gold");
|
|
941
|
-
paintLayers(ctx, [
|
|
942
|
-
{
|
|
943
|
-
parts: [
|
|
944
|
-
bar("crown:barV", c0, c1)
|
|
945
|
-
],
|
|
946
|
-
shade: g
|
|
947
|
-
},
|
|
948
|
-
{
|
|
949
|
-
parts: [
|
|
950
|
-
bar("crown:barH", along(cm, side, -0.026 * W), along(cm, side, 0.026 * W))
|
|
951
|
-
],
|
|
952
|
-
shade: g
|
|
953
|
-
}
|
|
954
|
-
], view.px);
|
|
717
|
+
const W = m.right - m.left;
|
|
718
|
+
const headW = Math.max(.45 * W, m.topR - m.topL);
|
|
719
|
+
const R = .44 * headW + .07 * W, bandH = .11 * W, pointH = .15 * W, ez = .9;
|
|
720
|
+
const C = [
|
|
721
|
+
50,
|
|
722
|
+
seat(m, .42) + .05 * W,
|
|
723
|
+
0
|
|
724
|
+
];
|
|
725
|
+
const roll = .05, lean = -.22;
|
|
726
|
+
const place = (p) => add(C, tilt(p, roll, lean));
|
|
727
|
+
const dirOf = (v) => norm(tilt(v, roll, lean));
|
|
728
|
+
const on = (th, rr, y) => place([
|
|
729
|
+
rr * Math.cos(th),
|
|
730
|
+
y,
|
|
731
|
+
rr * ez * Math.sin(th)
|
|
732
|
+
]);
|
|
733
|
+
const axis = (y) => place([
|
|
734
|
+
0,
|
|
735
|
+
y,
|
|
736
|
+
0
|
|
737
|
+
]);
|
|
738
|
+
const nu = segs(view, TAU * R, 32, 64);
|
|
739
|
+
const band = surface(view, "crown:band", (u, v) => on(u * TAU, R * (1 - .018 * Math.sin(v * Math.PI)), -bandH * (1 - v)), nu, 2, (_u, v) => axis(-bandH * (1 - v)));
|
|
740
|
+
const rimT = .017 * W;
|
|
741
|
+
const rimTop = ringMesh(view, "crown:rimTop", place, R + .004 * W, -bandH, rimT, ez, nu, 5);
|
|
742
|
+
const rimBot = ringMesh(view, "crown:rimBot", place, R + .004 * W, 0, rimT * 1.15, ez, nu, 5);
|
|
743
|
+
/* the velvet: puffed between the points, tall enough to cover the head */
|
|
744
|
+
const capBase = -bandH * .55;
|
|
745
|
+
const capH = Math.max(.12 * W, C[1] - m.top + .04 * W + capBase);
|
|
746
|
+
const P0 = Math.PI / 2;
|
|
747
|
+
const cap = surface(view, "crown:cap", (u, v) => {
|
|
748
|
+
const th = u * TAU, phi = v * (Math.PI / 2);
|
|
749
|
+
/* the puffs swell between the points, above the band, and tuck in
|
|
750
|
+
under it */
|
|
751
|
+
const puff = .08 * Math.sin(2 * phi) * (.55 + .45 * Math.cos(5 * (th - P0 - Math.PI / 5)));
|
|
752
|
+
return on(th, R * (.9 * Math.sin(phi) + puff), capBase - capH * Math.cos(phi));
|
|
753
|
+
}, segs(view, TAU * R, 24, 48), segs(view, 1.4 * capH, 8, 20, 6), axis(capBase - capH * .4));
|
|
754
|
+
/* the points: plates on the band's curve with a ridge up the middle,
|
|
755
|
+
their sides curving in to the tip, flaring out a little as they rise */
|
|
756
|
+
const points = [], buds = [];
|
|
757
|
+
const pearls = [], budTops = [];
|
|
758
|
+
const plate = (key, a0, w, h, ridge, flare, out) => {
|
|
759
|
+
out.push(surface(view, key, (u, v) => {
|
|
760
|
+
const s = u * 2 - 1;
|
|
761
|
+
const th = a0 + s * w * Math.pow(1 - v, 1.3);
|
|
762
|
+
const rr = R + .004 * W + ridge * (1 - Math.abs(s)) * (1 - .5 * v) + flare * v * v;
|
|
763
|
+
return on(th, rr, -bandH - h * v);
|
|
764
|
+
}, 2, segs(view, h, 3, 5), (_u, v) => on(a0, R * .5, -bandH - h * v), false));
|
|
765
|
+
};
|
|
766
|
+
for (let i = 0; i < 5; i++) {
|
|
767
|
+
const a0 = P0 + i / 5 * TAU;
|
|
768
|
+
plate(`crown:point${i}`, a0, TAU / 5 * .34, pointH, .05 * W, .03 * W, points);
|
|
769
|
+
pearls.push(on(a0, R + .004 * W + .03 * W + .02 * W, -bandH - pointH - .018 * W));
|
|
770
|
+
const b0 = a0 + Math.PI / 5;
|
|
771
|
+
plate(`crown:bud${i}`, b0, TAU / 5 * .12, pointH * .36, .02 * W, .01 * W, buds);
|
|
772
|
+
budTops.push(on(b0, R + .004 * W + .012 * W, -bandH - pointH * .36 - .012 * W));
|
|
773
|
+
}
|
|
774
|
+
const bb = boundsOf([band]);
|
|
775
|
+
contact(ctx, bodyClip, bb.cx, bb.y1, bb.w * .5, .045 * W, .34);
|
|
776
|
+
/* what sits behind the cap goes first, so the cap covers it */
|
|
777
|
+
const mid = view.depth(axis(capBase));
|
|
778
|
+
const g = gold(view);
|
|
779
|
+
/* the points' flat faces take a warmer, softer glint than the curves:
|
|
780
|
+
a whole facet flaring white reads as paper, not gold */
|
|
781
|
+
const facet = metal(view, [
|
|
782
|
+
255,
|
|
783
|
+
196,
|
|
784
|
+
80
|
|
785
|
+
], [
|
|
786
|
+
92,
|
|
787
|
+
54,
|
|
788
|
+
10
|
|
789
|
+
], [
|
|
790
|
+
255,
|
|
791
|
+
222,
|
|
792
|
+
150
|
|
793
|
+
]);
|
|
794
|
+
const goldBoth = (n) => facet(n[2] < 0 ? [
|
|
795
|
+
-n[0],
|
|
796
|
+
-n[1],
|
|
797
|
+
-n[2]
|
|
798
|
+
] : n);
|
|
799
|
+
const pearlR = .024 * W, budR = .019 * W;
|
|
800
|
+
for (const p of pearls) if (view.depth(p) < mid) sphereSprite(ctx, view, p, pearlR, "pearl");
|
|
801
|
+
for (const p of budTops) if (view.depth(p) < mid) sphereSprite(ctx, view, p, budR, "gold");
|
|
802
|
+
paintLayers(ctx, [
|
|
803
|
+
{
|
|
804
|
+
parts: [cap],
|
|
805
|
+
shade: velvet(view, [
|
|
806
|
+
150,
|
|
807
|
+
22,
|
|
808
|
+
44
|
|
809
|
+
])
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
parts: [band],
|
|
813
|
+
shade: g,
|
|
814
|
+
bias: .01 * W
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
parts: [rimTop],
|
|
818
|
+
shade: g,
|
|
819
|
+
bias: .012 * W
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
parts: [rimBot],
|
|
823
|
+
shade: g,
|
|
824
|
+
bias: .012 * W
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
parts: points,
|
|
828
|
+
shade: goldBoth,
|
|
829
|
+
cull: false,
|
|
830
|
+
flat: true
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
parts: buds,
|
|
834
|
+
shade: goldBoth,
|
|
835
|
+
cull: false,
|
|
836
|
+
flat: true
|
|
837
|
+
}
|
|
838
|
+
], view.px);
|
|
839
|
+
/* the stones in their settings, under each point, and gold studs between */
|
|
840
|
+
const stones = [
|
|
841
|
+
[
|
|
842
|
+
208,
|
|
843
|
+
20,
|
|
844
|
+
60
|
|
845
|
+
],
|
|
846
|
+
[
|
|
847
|
+
30,
|
|
848
|
+
150,
|
|
849
|
+
92
|
|
850
|
+
],
|
|
851
|
+
[
|
|
852
|
+
36,
|
|
853
|
+
90,
|
|
854
|
+
220
|
|
855
|
+
],
|
|
856
|
+
[
|
|
857
|
+
36,
|
|
858
|
+
90,
|
|
859
|
+
220
|
|
860
|
+
],
|
|
861
|
+
[
|
|
862
|
+
30,
|
|
863
|
+
150,
|
|
864
|
+
92
|
|
865
|
+
]
|
|
866
|
+
];
|
|
867
|
+
for (let i = 0; i < 5; i++) {
|
|
868
|
+
for (const [th, kind] of [[P0 + i / 5 * TAU, "stone"], [P0 + i / 5 * TAU + Math.PI / 5, "stud"]]) {
|
|
869
|
+
const N = dirOf([
|
|
870
|
+
Math.cos(th),
|
|
871
|
+
0,
|
|
872
|
+
ez * Math.sin(th)
|
|
873
|
+
]);
|
|
874
|
+
if (view.dir(N)[2] < .12) continue;
|
|
875
|
+
const c = on(th, R + .01 * W, -bandH * .5);
|
|
876
|
+
if (kind === "stud") {
|
|
877
|
+
sphereSprite(ctx, view, c, .013 * W, "gold");
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
const size = (i === 0 ? .034 : .027) * W;
|
|
881
|
+
sphereSprite(ctx, view, c, size * 1.3, "gold");
|
|
882
|
+
const T = dirOf([
|
|
883
|
+
-Math.sin(th),
|
|
884
|
+
0,
|
|
885
|
+
ez * Math.cos(th)
|
|
886
|
+
]);
|
|
887
|
+
cutGem(ctx, view, along(c, N, .006 * W), N, T, size, stones[i]);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
for (const p of pearls) if (view.depth(p) >= mid) sphereSprite(ctx, view, p, pearlR, "pearl");
|
|
891
|
+
for (const p of budTops) if (view.depth(p) >= mid) sphereSprite(ctx, view, p, budR, "gold");
|
|
892
|
+
/* the orb and the cross on top of the cap */
|
|
893
|
+
const orbR = .034 * W;
|
|
894
|
+
const orb = axis(capBase - capH - orbR * .75);
|
|
895
|
+
const up = dirOf([
|
|
896
|
+
0,
|
|
897
|
+
-1,
|
|
898
|
+
0
|
|
899
|
+
]);
|
|
900
|
+
const bar = (key, a, b) => tube(view, key, (u) => [
|
|
901
|
+
a[0] + (b[0] - a[0]) * u,
|
|
902
|
+
a[1] + (b[1] - a[1]) * u,
|
|
903
|
+
a[2] + (b[2] - a[2]) * u
|
|
904
|
+
], dirOf([
|
|
905
|
+
0,
|
|
906
|
+
0,
|
|
907
|
+
1
|
|
908
|
+
]), section(.009 * W, .009 * W, .5), 2, 6);
|
|
909
|
+
const c0 = along(orb, up, orbR * .8), c1 = along(orb, up, orbR * .8 + .075 * W);
|
|
910
|
+
const cm = along(orb, up, orbR * .8 + .05 * W), side = dirOf([
|
|
911
|
+
1,
|
|
912
|
+
0,
|
|
913
|
+
0
|
|
914
|
+
]);
|
|
915
|
+
sphereSprite(ctx, view, orb, orbR, "gold");
|
|
916
|
+
paintLayers(ctx, [{
|
|
917
|
+
parts: [bar("crown:barV", c0, c1)],
|
|
918
|
+
shade: g
|
|
919
|
+
}, {
|
|
920
|
+
parts: [bar("crown:barH", along(cm, side, -.026 * W), along(cm, side, .026 * W))],
|
|
921
|
+
shade: g
|
|
922
|
+
}], view.px);
|
|
955
923
|
}
|
|
956
924
|
function phonesOf(view, m, base) {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
shade: plastic(view, padRgb, 0.3)
|
|
1129
|
-
}, {
|
|
1130
|
-
parts: sliders,
|
|
1131
|
-
shade: g
|
|
1132
|
-
});
|
|
1133
|
-
const stitch = [];
|
|
1134
|
-
for(let i = 0; i <= 48; i++)stitch.push(along(cushionPath(0.04 + 0.92 * i / 48), Z, 0.034 * W + 0.001 * W));
|
|
1135
|
-
return {
|
|
1136
|
-
layers,
|
|
1137
|
-
pivots,
|
|
1138
|
-
pivotR: 0.017 * W,
|
|
1139
|
-
stitch,
|
|
1140
|
-
seats
|
|
1141
|
-
};
|
|
925
|
+
const W = m.right - m.left;
|
|
926
|
+
const rc = .15 * W, oval = 1.1, pad = .05 * W, tc = .068 * W;
|
|
927
|
+
const yc = m.mid - .07 * W;
|
|
928
|
+
const tilt = .5;
|
|
929
|
+
const shellRgb = rgbOf(base), padRgb = rgbOf(shade(base, -.12)), plateRgb = rgbOf(shade(base, .08));
|
|
930
|
+
const nu = segs(view, TAU * rc, 20, 40);
|
|
931
|
+
const layers = [];
|
|
932
|
+
const pivots = [];
|
|
933
|
+
const tops = [];
|
|
934
|
+
const seats = [];
|
|
935
|
+
const g = silver(view);
|
|
936
|
+
for (const s of [-1, 1]) {
|
|
937
|
+
/* the cup's axis, out from the head and a little toward the viewer,
|
|
938
|
+
and its face's two directions */
|
|
939
|
+
const A = [
|
|
940
|
+
s * Math.cos(tilt),
|
|
941
|
+
0,
|
|
942
|
+
Math.sin(tilt)
|
|
943
|
+
];
|
|
944
|
+
const E1 = [
|
|
945
|
+
0,
|
|
946
|
+
1,
|
|
947
|
+
0
|
|
948
|
+
];
|
|
949
|
+
const E2 = [
|
|
950
|
+
-Math.sin(tilt),
|
|
951
|
+
0,
|
|
952
|
+
s * Math.cos(tilt)
|
|
953
|
+
];
|
|
954
|
+
const O = [
|
|
955
|
+
(s < 0 ? m.left : m.right) - s * .02 * W,
|
|
956
|
+
yc,
|
|
957
|
+
.01 * W
|
|
958
|
+
];
|
|
959
|
+
const at = (t, rho, th) => {
|
|
960
|
+
const c = Math.cos(th) * oval * rho, sn = Math.sin(th) * rho;
|
|
961
|
+
return [
|
|
962
|
+
O[0] + A[0] * t + E1[0] * c + E2[0] * sn,
|
|
963
|
+
O[1] + A[1] * t + E1[1] * c + E2[1] * sn,
|
|
964
|
+
O[2] + A[2] * t + E1[2] * c + E2[2] * sn
|
|
965
|
+
];
|
|
966
|
+
};
|
|
967
|
+
const axis = (t) => along(O, A, t);
|
|
968
|
+
seats.push({
|
|
969
|
+
c: O,
|
|
970
|
+
ry: rc * oval
|
|
971
|
+
});
|
|
972
|
+
/* the cushion: a plump roll of leather, creased round when there is room */
|
|
973
|
+
const rho0 = rc * .9 - pad * .5;
|
|
974
|
+
const creases = nu >= 48 ? 1 : 0;
|
|
975
|
+
const cushion = surface(view, `phones:cushion${s}`, (u, v) => {
|
|
976
|
+
const th = u * TAU, a = v * Math.PI;
|
|
977
|
+
const crease = 1 + .006 * creases * Math.sin(th * 24) * Math.sin(a);
|
|
978
|
+
return at(pad * .5 * (1 - Math.cos(a)), (rho0 + pad * .62 * Math.sin(a)) * crease, th);
|
|
979
|
+
}, nu, 5, (_u, v) => axis(pad * .5 * (1 - Math.cos(v * Math.PI))));
|
|
980
|
+
/* the shell: a rounded puck, its side meeting its face in a soft shoulder */
|
|
981
|
+
const shell = surface(view, `phones:shell${s}`, (u, v) => {
|
|
982
|
+
const a = v * (Math.PI / 2);
|
|
983
|
+
return at(pad + tc * Math.pow(Math.sin(a), .55), rc * (.7 + .3 * Math.pow(Math.cos(a), .55)), u * TAU);
|
|
984
|
+
}, nu, 5, axis(pad + tc * .5));
|
|
985
|
+
/* the ring round its face, and the plate inside it: a shallow gloss dome */
|
|
986
|
+
const ringR = .66 * rc, ringT = .017 * W;
|
|
987
|
+
const ring = surface(view, `phones:ring${s}`, (u, v) => at(pad + tc + ringT * .45 * Math.sin(v * TAU), ringR + ringT * Math.cos(v * TAU), u * TAU), nu, 5, (u) => at(pad + tc, ringR, u * TAU));
|
|
988
|
+
const plateR = ringR - ringT * .5;
|
|
989
|
+
const plate = surface(view, `phones:plate${s}`, (u, v) => at(pad + tc + .013 * W * (1 - v * v), plateR * v, u * TAU), nu, 3, axis(pad + tc - .02 * W));
|
|
990
|
+
/* the yoke: an arm round the top of the cup, pivot to pivot */
|
|
991
|
+
const ty = pad + tc * .45, ry = rc * 1.04 + .022 * W;
|
|
992
|
+
const yokeAt = (u) => {
|
|
993
|
+
const a = u * Math.PI;
|
|
994
|
+
const c = -Math.sin(a) * oval * ry, sn = Math.cos(a) * ry;
|
|
995
|
+
return [
|
|
996
|
+
O[0] + A[0] * ty + E1[0] * c + E2[0] * sn,
|
|
997
|
+
O[1] + A[1] * ty + E1[1] * c + E2[1] * sn,
|
|
998
|
+
O[2] + A[2] * ty + E1[2] * c + E2[2] * sn
|
|
999
|
+
];
|
|
1000
|
+
};
|
|
1001
|
+
const yoke = tube(view, `phones:yoke${s}`, yokeAt, A, section(.013 * W, .008 * W, .6), segs(view, Math.PI * ry, 12, 24), 6);
|
|
1002
|
+
pivots.push(yokeAt(0), yokeAt(1));
|
|
1003
|
+
tops.push(yokeAt(.5));
|
|
1004
|
+
layers.push({
|
|
1005
|
+
parts: [cushion],
|
|
1006
|
+
shade: plastic(view, padRgb, .35)
|
|
1007
|
+
}, {
|
|
1008
|
+
parts: [shell],
|
|
1009
|
+
shade: plastic(view, shellRgb, .8)
|
|
1010
|
+
}, {
|
|
1011
|
+
parts: [ring],
|
|
1012
|
+
shade: g,
|
|
1013
|
+
bias: .02 * W
|
|
1014
|
+
}, {
|
|
1015
|
+
parts: [plate],
|
|
1016
|
+
shade: plastic(view, plateRgb, 1.3),
|
|
1017
|
+
bias: .01 * W
|
|
1018
|
+
}, {
|
|
1019
|
+
parts: [yoke],
|
|
1020
|
+
shade: g
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
/* the band: a smooth arch from one yoke over the head to the other,
|
|
1024
|
+
just high enough to clear the head everywhere — resting on it where
|
|
1025
|
+
the head is highest, bridging any dip — with a metal slider at each
|
|
1026
|
+
end running into the yoke */
|
|
1027
|
+
const [PL, PR] = tops;
|
|
1028
|
+
const zb = (PL[2] + PR[2]) / 2, cx = (PL[0] + PR[0]) / 2, ax = (PR[0] - PL[0]) / 2, yEnd = (PL[1] + PR[1]) / 2;
|
|
1029
|
+
const padT = (u) => .005 * W + .022 * W * Math.pow(Math.sin(Math.PI * u), .35);
|
|
1030
|
+
const e = 2 / 2.2;
|
|
1031
|
+
/* the head's top edge over x, from its hull */
|
|
1032
|
+
const topAt = (x) => {
|
|
1033
|
+
let y = Infinity;
|
|
1034
|
+
const h = m.hull;
|
|
1035
|
+
for (let i = 0; i < h.length; i++) {
|
|
1036
|
+
const p = h[i], q = h[(i + 1) % h.length];
|
|
1037
|
+
if (p[0] === q[0] || x < Math.min(p[0], q[0]) || x > Math.max(p[0], q[0])) continue;
|
|
1038
|
+
y = Math.min(y, p[1] + (q[1] - p[1]) * (x - p[0]) / (q[0] - p[0]));
|
|
1039
|
+
}
|
|
1040
|
+
return y;
|
|
1041
|
+
};
|
|
1042
|
+
let lift = yEnd - m.top;
|
|
1043
|
+
for (let i = 1; i < 64; i++) {
|
|
1044
|
+
const t = Math.PI * (1 - i / 64);
|
|
1045
|
+
const s = Math.pow(Math.abs(Math.sin(t)), e);
|
|
1046
|
+
const yt = topAt(cx + ax * spow(Math.cos(t), e));
|
|
1047
|
+
if (s > .05 && Number.isFinite(yt)) lift = Math.max(lift, (yEnd - yt + 2 * padT(i / 64) + .006 * W) / s);
|
|
1048
|
+
}
|
|
1049
|
+
const arch = (u) => {
|
|
1050
|
+
const t = Math.PI * (1 - u);
|
|
1051
|
+
return [
|
|
1052
|
+
cx + ax * spow(Math.cos(t), e),
|
|
1053
|
+
yEnd - lift * Math.pow(Math.abs(Math.sin(t)), e),
|
|
1054
|
+
zb
|
|
1055
|
+
];
|
|
1056
|
+
};
|
|
1057
|
+
const part = (u0, u1) => (u) => arch(u0 + (u1 - u0) * u);
|
|
1058
|
+
const Z = [
|
|
1059
|
+
0,
|
|
1060
|
+
0,
|
|
1061
|
+
1
|
|
1062
|
+
];
|
|
1063
|
+
const span = ax * 2 + lift * 2;
|
|
1064
|
+
const nb = segs(view, span, 24, 48);
|
|
1065
|
+
const us = .09;
|
|
1066
|
+
const shellOff = .016 * W;
|
|
1067
|
+
const shellPath = offsetPath(part(us, 1 - us), Z, () => shellOff);
|
|
1068
|
+
/* the shell narrows at its ends to the slider it holds */
|
|
1069
|
+
const taper = (u) => smooth01(Math.min(u, 1 - u) / .07);
|
|
1070
|
+
const bandShell = tube(view, "phones:bandShell", shellPath, Z, (u, ph) => {
|
|
1071
|
+
const k = taper(u);
|
|
1072
|
+
return [(.024 + .016 * k) * W * spow(Math.cos(ph), .4), (.008 + .008 * k) * W * spow(Math.sin(ph), .4)];
|
|
1073
|
+
}, nb, 6);
|
|
1074
|
+
const cushionPath = offsetPath(part(us + .02, 1 - us - .02), Z, (u) => -padT(us + .02 + (1 - 2 * us - .04) * u));
|
|
1075
|
+
const bandPad = tube(view, "phones:bandPad", cushionPath, Z, (u, ph) => [.034 * W * spow(Math.cos(ph), .5), padT(us + .02 + (1 - 2 * us - .04) * u) * spow(Math.sin(ph), .7)], nb, 6);
|
|
1076
|
+
const sliders = [part(0, us + .03), part(1 - us - .03, 1)].map((p, i) => tube(view, `phones:slider${i}`, p, Z, section(.022 * W, .006 * W, .4), 4, 6));
|
|
1077
|
+
layers.push({
|
|
1078
|
+
parts: [bandShell],
|
|
1079
|
+
shade: plastic(view, shellRgb, .9)
|
|
1080
|
+
}, {
|
|
1081
|
+
parts: [bandPad],
|
|
1082
|
+
shade: plastic(view, padRgb, .3)
|
|
1083
|
+
}, {
|
|
1084
|
+
parts: sliders,
|
|
1085
|
+
shade: g
|
|
1086
|
+
});
|
|
1087
|
+
const stitch = [];
|
|
1088
|
+
for (let i = 0; i <= 48; i++) stitch.push(along(cushionPath(.04 + .92 * i / 48), Z, .034 * W + .001 * W));
|
|
1089
|
+
return {
|
|
1090
|
+
layers,
|
|
1091
|
+
pivots,
|
|
1092
|
+
pivotR: .017 * W,
|
|
1093
|
+
stitch,
|
|
1094
|
+
seats
|
|
1095
|
+
};
|
|
1142
1096
|
}
|
|
1097
|
+
/* the behind and front passes of one frame share one projection */
|
|
1143
1098
|
let lastPhones = null;
|
|
1144
1099
|
function drawPhones(ctx, view, m, base, pass, bodyClip) {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
ctx.restore();
|
|
1183
|
-
}
|
|
1100
|
+
const key = `${view.m.join()}|${view.px}|${view.L.join()}|${base}`;
|
|
1101
|
+
if (!lastPhones || lastPhones.m !== m || lastPhones.key !== key) lastPhones = {
|
|
1102
|
+
m,
|
|
1103
|
+
key,
|
|
1104
|
+
ph: phonesOf(view, m, base)
|
|
1105
|
+
};
|
|
1106
|
+
const ph = lastPhones.ph;
|
|
1107
|
+
const W = m.right - m.left;
|
|
1108
|
+
const near = (d) => pass === "front" ? d >= 0 : d < 0;
|
|
1109
|
+
if (pass === "front") {
|
|
1110
|
+
for (const s of ph.seats) {
|
|
1111
|
+
if (view.depth(s.c) < 0) continue;
|
|
1112
|
+
const c = view.toScreen(s.c);
|
|
1113
|
+
contact(ctx, bodyClip, c[0], c[1] + .01 * W, .06 * W, s.ry * 1.05, .26);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
paintLayers(ctx, ph.layers.map((l) => ({
|
|
1117
|
+
...l,
|
|
1118
|
+
keep: near
|
|
1119
|
+
})), view.px);
|
|
1120
|
+
for (const p of ph.pivots) if (near(view.depth(p))) sphereSprite(ctx, view, p, ph.pivotR, "silver");
|
|
1121
|
+
/* the stitches along the band's cushion, on its face toward the viewer */
|
|
1122
|
+
if (pass === "front" && W * view.px > 120 && view.dir([
|
|
1123
|
+
0,
|
|
1124
|
+
0,
|
|
1125
|
+
1
|
|
1126
|
+
])[2] > .3) {
|
|
1127
|
+
const pts = ph.stitch.map((p) => view.toScreen(p));
|
|
1128
|
+
ctx.save();
|
|
1129
|
+
ctx.setLineDash([.022 * W, .014 * W]);
|
|
1130
|
+
ctx.lineCap = "round";
|
|
1131
|
+
ctx.lineWidth = .0045 * W;
|
|
1132
|
+
ctx.strokeStyle = shade(base, .12);
|
|
1133
|
+
ctx.globalAlpha = .55;
|
|
1134
|
+
ctx.stroke(polyPath(pts, false));
|
|
1135
|
+
ctx.restore();
|
|
1136
|
+
}
|
|
1184
1137
|
}
|
|
1138
|
+
/* ── the bow tie ────────────────────────────────────────────────────── */
|
|
1139
|
+
/* Two satin wings gathered into a knot: each wing narrow where the knot
|
|
1140
|
+
pinches it and full at its end, puffed toward the viewer, with folds
|
|
1141
|
+
that run out from the knot and fade toward the tip, and a shallow notch
|
|
1142
|
+
in its end; the knot a small wrapped pillow over them. */
|
|
1185
1143
|
function drawBowTie(ctx, view, r, m, base, bodyZ, bodyClip, face, fabricBow = false) {
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1144
|
+
if (r.facing < .05) return;
|
|
1145
|
+
const W = m.right - m.left;
|
|
1146
|
+
/* under the face, clear of anything worn on it, and small enough to fit
|
|
1147
|
+
between the face and the bottom — a low face (a triangle's) gets a
|
|
1148
|
+
smaller bow */
|
|
1149
|
+
const clear = face.y + 22 * face.scale;
|
|
1150
|
+
const cyB = Math.max(clear, m.bottom - .15 * W);
|
|
1151
|
+
const room = Math.max(0, m.bottom - 3 - cyB);
|
|
1152
|
+
const k = Math.min(W, 100) * Math.max(.5, Math.min(1, room / 15));
|
|
1153
|
+
/* on the body's front surface where the bow sits */
|
|
1154
|
+
const zFront = bodyZ(50, cyB);
|
|
1155
|
+
/* the bow's own frame, in fractions of the body's width: x across, y
|
|
1156
|
+
down, z out of the body's front */
|
|
1157
|
+
const Q = (x, y, z) => [
|
|
1158
|
+
50 + x * k,
|
|
1159
|
+
cyB + y * k,
|
|
1160
|
+
zFront + z * k
|
|
1161
|
+
];
|
|
1162
|
+
const rgb = rgbOf(base);
|
|
1163
|
+
const corners = [
|
|
1164
|
+
Q(-.36, -.18, 0),
|
|
1165
|
+
Q(.36, .18, 0),
|
|
1166
|
+
Q(-.36, .18, 0),
|
|
1167
|
+
Q(.36, -.18, 0)
|
|
1168
|
+
].map((p) => view.toScreen(p));
|
|
1169
|
+
const bb = bounds(corners);
|
|
1170
|
+
const sh = view.toScreen(Q(.01, .08, 0));
|
|
1171
|
+
contact(ctx, bodyClip, sh[0], sh[1], bb.w * .52, bb.h * .5, .3);
|
|
1172
|
+
const nu = segs(view, .32 * k, 8, 16), nv = segs(view, .3 * k, 8, 16, 6);
|
|
1173
|
+
const at = `${zFront.toFixed(2)},${cyB.toFixed(2)},${k.toFixed(2)}`;
|
|
1174
|
+
/* satin on a plastic body; on a plush one the same felt as the body */
|
|
1175
|
+
const soft = !!fabricBow;
|
|
1176
|
+
const wingShade = soft ? felt(view, rgb) : satin(view, rgb);
|
|
1177
|
+
const layers = [];
|
|
1178
|
+
for (const dir of [-1, 1]) {
|
|
1179
|
+
const wing = (u, v) => {
|
|
1180
|
+
const x = .058 + .3 * u - .035 * Math.pow(u, 4) * Math.sin(Math.PI * v);
|
|
1181
|
+
const h = .05 + .12 * Math.pow(u, .8);
|
|
1182
|
+
const y = -h * Math.cos(Math.PI * v) + .012 * u * u;
|
|
1183
|
+
const bulge = .018 + .058 * Math.sin(Math.PI * (.12 + .88 * u));
|
|
1184
|
+
const fold = .016 * Math.sin(3 * Math.PI * v) * Math.pow(1 - u, 1.3);
|
|
1185
|
+
return Q(dir * x, y, (bulge + fold) * Math.sin(Math.PI * v) + .004);
|
|
1186
|
+
};
|
|
1187
|
+
layers.push({
|
|
1188
|
+
parts: [surface(view, `bow:wing${dir}@${at}`, wing, nu, nv, (u) => Q(dir * (.058 + .3 * u), 0, -.03))],
|
|
1189
|
+
shade: wingShade
|
|
1190
|
+
});
|
|
1191
|
+
/* the wing's end, closed down to the body */
|
|
1192
|
+
layers.push({
|
|
1193
|
+
parts: [surface(view, `bow:end${dir}@${at}`, (s, t) => {
|
|
1194
|
+
const p = wing(1, t);
|
|
1195
|
+
return [
|
|
1196
|
+
p[0],
|
|
1197
|
+
p[1],
|
|
1198
|
+
p[2] - (p[2] - zFront) * s
|
|
1199
|
+
];
|
|
1200
|
+
}, 1, nv, Q(dir * .2, 0, -.03))],
|
|
1201
|
+
shade: wingShade
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
/* the knot: pinched at its waist, rounded all round */
|
|
1205
|
+
const knot = surface(view, `bow:knot@${at}`, (u, v) => {
|
|
1206
|
+
const x = .068 * (2 * u - 1) * (1 - .12 * Math.sin(Math.PI * v));
|
|
1207
|
+
const y = .075 * (2 * v - 1);
|
|
1208
|
+
const zx = Math.pow(Math.max(0, 1 - Math.pow(Math.abs(2 * u - 1), 3)), .5);
|
|
1209
|
+
const zy = Math.pow(Math.max(0, 1 - Math.pow(Math.abs(2 * v - 1), 5)), .4);
|
|
1210
|
+
return Q(x, y, .012 + .075 * zx * zy);
|
|
1211
|
+
}, 6, 6, Q(0, 0, -.02));
|
|
1212
|
+
layers.push({
|
|
1213
|
+
parts: [knot],
|
|
1214
|
+
shade: soft ? felt(view, rgb, .9) : satin(view, scale(rgb, .9))
|
|
1215
|
+
});
|
|
1216
|
+
if (!soft) {
|
|
1217
|
+
paintLayers(ctx, layers, view.px);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
/* felt: each wing and the knot in turn, each with its own grain and fuzz
|
|
1221
|
+
round its own outline (the bow's is not convex) */
|
|
1222
|
+
for (let i = 0; i < layers.length; i += 2) {
|
|
1223
|
+
const part = layers.slice(i, i + 2);
|
|
1224
|
+
const outline = hull(paintLayers(ctx, part, view.px));
|
|
1225
|
+
grainOver(ctx, outline, .5, .1);
|
|
1226
|
+
fuzz(ctx, outline, base, 41 + i, .3, view.px);
|
|
1227
|
+
}
|
|
1268
1228
|
}
|
|
1229
|
+
/* the frames: each lens's half width and height, how square its corners
|
|
1230
|
+
(a superellipse's exponent), the rim's half thickness across and its
|
|
1231
|
+
half depth */
|
|
1269
1232
|
const FRAMES = {
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1233
|
+
round: {
|
|
1234
|
+
rx: 10.8,
|
|
1235
|
+
ry: 10.8,
|
|
1236
|
+
n: 2,
|
|
1237
|
+
tr: 1.15,
|
|
1238
|
+
td: 1.2
|
|
1239
|
+
},
|
|
1240
|
+
square: {
|
|
1241
|
+
rx: 12,
|
|
1242
|
+
ry: 9.6,
|
|
1243
|
+
n: 4.2,
|
|
1244
|
+
tr: 1.45,
|
|
1245
|
+
td: 1.4
|
|
1246
|
+
},
|
|
1247
|
+
shades: {
|
|
1248
|
+
rx: 12.6,
|
|
1249
|
+
ry: 10,
|
|
1250
|
+
n: 3.4,
|
|
1251
|
+
tr: 1.6,
|
|
1252
|
+
td: 1.5
|
|
1253
|
+
}
|
|
1291
1254
|
};
|
|
1255
|
+
/** Glasses over two eye spots (face space, already placed on the face's
|
|
1256
|
+
sphere), turned rigidly with the head: acetate rims with rounded
|
|
1257
|
+
edges, a bridge, small metal rivets at the hinges, and lenses — clear
|
|
1258
|
+
with a reflection, or dark. Drawn after the face, unclipped, with a
|
|
1259
|
+
soft shadow on the face laid first (clipped by the caller's face clip
|
|
1260
|
+
through `shadow`). */
|
|
1292
1261
|
export function drawGlasses(ctx, kind, eyes, light, pass, turn = {
|
|
1293
|
-
|
|
1294
|
-
|
|
1262
|
+
yaw: 0,
|
|
1263
|
+
pitch: 0
|
|
1295
1264
|
}) {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
}
|
|
1265
|
+
if (kind === "none") return;
|
|
1266
|
+
owner = FRAMES;
|
|
1267
|
+
const F = FRAMES[kind];
|
|
1268
|
+
const cy = Math.cos(turn.yaw), sy = Math.sin(turn.yaw), cp = Math.cos(turn.pitch), sp = Math.sin(turn.pitch);
|
|
1269
|
+
const { L: lightL, H: lightH } = lightOf(light.lx, light.ly);
|
|
1270
|
+
/* the head's turn, rigidly: yaw about y, then pitch about x */
|
|
1271
|
+
const view = makeView([
|
|
1272
|
+
cy,
|
|
1273
|
+
0,
|
|
1274
|
+
sy,
|
|
1275
|
+
sp * sy,
|
|
1276
|
+
cp,
|
|
1277
|
+
-sp * cy,
|
|
1278
|
+
-cp * sy,
|
|
1279
|
+
sp,
|
|
1280
|
+
cp * cy
|
|
1281
|
+
], [
|
|
1282
|
+
0,
|
|
1283
|
+
0,
|
|
1284
|
+
0
|
|
1285
|
+
], lightL, lightH, pxOf(ctx));
|
|
1286
|
+
/* the front of the frame: a plane just off the face, wrapping back a
|
|
1287
|
+
little toward its outer edges */
|
|
1288
|
+
const zPlane = Math.max(eyes[0].fz, eyes[1].fz) + 3.2;
|
|
1289
|
+
const zAt = (x) => zPlane - .0045 * x * x;
|
|
1290
|
+
const e2 = 2 / F.n;
|
|
1291
|
+
const rimAt = (e, grow) => (u) => {
|
|
1292
|
+
const t = u * TAU;
|
|
1293
|
+
const x = e.ox + (F.rx + grow) * spow(Math.cos(t), e2), y = e.oy + (F.ry + grow) * spow(Math.sin(t), e2);
|
|
1294
|
+
return [
|
|
1295
|
+
x,
|
|
1296
|
+
y,
|
|
1297
|
+
zAt(x)
|
|
1298
|
+
];
|
|
1299
|
+
};
|
|
1300
|
+
const ring = (e, grow, n = 48) => {
|
|
1301
|
+
const f = rimAt(e, grow);
|
|
1302
|
+
const pts = [];
|
|
1303
|
+
for (let i = 0; i < n; i++) pts.push(view.toScreen(f(i / n)));
|
|
1304
|
+
return pts;
|
|
1305
|
+
};
|
|
1306
|
+
const vis = eyes.map((e) => Math.max(0, Math.min(1, (e.z - .18) * 3.2)));
|
|
1307
|
+
if (pass === "shadow") {
|
|
1308
|
+
for (let i = 0; i < 2; i++) {
|
|
1309
|
+
if (vis[i] <= 0) continue;
|
|
1310
|
+
ctx.save();
|
|
1311
|
+
ctx.globalAlpha = .22 * vis[i];
|
|
1312
|
+
ctx.translate(-light.lx * 1.6, -light.ly * 1.6 + .8);
|
|
1313
|
+
ctx.lineWidth = F.tr * 2 + .6;
|
|
1314
|
+
ctx.lineJoin = "round";
|
|
1315
|
+
ctx.strokeStyle = "#000";
|
|
1316
|
+
ctx.stroke(polyPath(ring(eyes[i], 0)));
|
|
1317
|
+
ctx.restore();
|
|
1318
|
+
}
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const frame = [
|
|
1322
|
+
22,
|
|
1323
|
+
22,
|
|
1324
|
+
26
|
|
1325
|
+
];
|
|
1326
|
+
const rimShade = plastic(view, frame, 1.1);
|
|
1327
|
+
/* the lenses first, under the rims */
|
|
1328
|
+
for (let i = 0; i < 2; i++) {
|
|
1329
|
+
if (vis[i] <= 0) continue;
|
|
1330
|
+
const lens = polyPath(ring(eyes[i], -F.tr * .5));
|
|
1331
|
+
const b = bounds(ring(eyes[i], 0, 16));
|
|
1332
|
+
ctx.save();
|
|
1333
|
+
ctx.globalAlpha = vis[i];
|
|
1334
|
+
if (kind === "shades") {
|
|
1335
|
+
const g = ctx.createLinearGradient(b.cx, b.y0, b.cx, b.y1);
|
|
1336
|
+
g.addColorStop(0, "rgba(58,60,78,0.97)");
|
|
1337
|
+
g.addColorStop(.55, "rgba(18,18,26,0.98)");
|
|
1338
|
+
g.addColorStop(1, "rgba(30,26,44,0.98)");
|
|
1339
|
+
ctx.fillStyle = g;
|
|
1340
|
+
} else {
|
|
1341
|
+
const g = ctx.createLinearGradient(b.cx, b.y0, b.cx, b.y1);
|
|
1342
|
+
g.addColorStop(0, "rgba(236,244,255,0.26)");
|
|
1343
|
+
g.addColorStop(.45, "rgba(214,230,255,0.08)");
|
|
1344
|
+
g.addColorStop(1, "rgba(214,230,255,0.14)");
|
|
1345
|
+
ctx.fillStyle = g;
|
|
1346
|
+
}
|
|
1347
|
+
ctx.fill(lens);
|
|
1348
|
+
ctx.clip(lens);
|
|
1349
|
+
/* the reflection: a broad soft band and a thin bright streak, slanting */
|
|
1350
|
+
const w = b.w, h = b.h;
|
|
1351
|
+
const streak = (x0, width, a) => {
|
|
1352
|
+
ctx.globalAlpha = vis[i] * a;
|
|
1353
|
+
ctx.fillStyle = "#fff";
|
|
1354
|
+
ctx.beginPath();
|
|
1355
|
+
ctx.moveTo(b.x0 + w * x0, b.y1 + h * .1);
|
|
1356
|
+
ctx.lineTo(b.x0 + w * (x0 + .55), b.y0 - h * .1);
|
|
1357
|
+
ctx.lineTo(b.x0 + w * (x0 + .55 + width), b.y0 - h * .1);
|
|
1358
|
+
ctx.lineTo(b.x0 + w * (x0 + width), b.y1 + h * .1);
|
|
1359
|
+
ctx.closePath();
|
|
1360
|
+
ctx.fill();
|
|
1361
|
+
};
|
|
1362
|
+
const k = kind === "shades" ? 1.4 : 1;
|
|
1363
|
+
streak(-.28, .26, .16 * k);
|
|
1364
|
+
streak(.06, .07, .3 * k);
|
|
1365
|
+
ctx.restore();
|
|
1366
|
+
}
|
|
1367
|
+
/* the rims, the bridge between them and the rivets at the hinges */
|
|
1368
|
+
const nr = segs(view, TAU * F.rx * 1.1, 28, 56, 8);
|
|
1369
|
+
const rim = (e) => tube(view, `rim:${kind}:${e.ox},${e.oy},${e.fz.toFixed(2)}`, rimAt(e, 0), [
|
|
1370
|
+
0,
|
|
1371
|
+
0,
|
|
1372
|
+
1
|
|
1373
|
+
], section(F.td, F.tr, .55), nr, 6, true);
|
|
1374
|
+
const [L, R] = eyes;
|
|
1375
|
+
const bridgeAt = (u) => {
|
|
1376
|
+
const round = kind === "round";
|
|
1377
|
+
const a = [
|
|
1378
|
+
L.ox + F.rx * (round ? .9 : .98),
|
|
1379
|
+
L.oy - F.ry * (round ? .32 : .42),
|
|
1380
|
+
0
|
|
1381
|
+
];
|
|
1382
|
+
const b = [
|
|
1383
|
+
R.ox - F.rx * (round ? .9 : .98),
|
|
1384
|
+
R.oy - F.ry * (round ? .32 : .42),
|
|
1385
|
+
0
|
|
1386
|
+
];
|
|
1387
|
+
const c = [
|
|
1388
|
+
0,
|
|
1389
|
+
(L.oy + R.oy) / 2 - F.ry * (round ? .8 : .6),
|
|
1390
|
+
0
|
|
1391
|
+
];
|
|
1392
|
+
const m1 = 1 - u;
|
|
1393
|
+
const x = m1 * m1 * a[0] + 2 * m1 * u * c[0] + u * u * b[0], y = m1 * m1 * a[1] + 2 * m1 * u * c[1] + u * u * b[1];
|
|
1394
|
+
return [
|
|
1395
|
+
x,
|
|
1396
|
+
y,
|
|
1397
|
+
zAt(x) + .3
|
|
1398
|
+
];
|
|
1399
|
+
};
|
|
1400
|
+
const bridge = tube(view, `bridge:${kind}:${L.ox},${L.oy},${R.ox},${zPlane.toFixed(2)}`, bridgeAt, [
|
|
1401
|
+
0,
|
|
1402
|
+
0,
|
|
1403
|
+
1
|
|
1404
|
+
], section(F.td * .8, F.tr * .75, .7), 8, 6);
|
|
1405
|
+
const parts = [
|
|
1406
|
+
{
|
|
1407
|
+
part: rim(L),
|
|
1408
|
+
a: vis[0],
|
|
1409
|
+
d: view.depth([
|
|
1410
|
+
L.ox,
|
|
1411
|
+
L.oy,
|
|
1412
|
+
zPlane
|
|
1413
|
+
])
|
|
1414
|
+
},
|
|
1415
|
+
{
|
|
1416
|
+
part: rim(R),
|
|
1417
|
+
a: vis[1],
|
|
1418
|
+
d: view.depth([
|
|
1419
|
+
R.ox,
|
|
1420
|
+
R.oy,
|
|
1421
|
+
zPlane
|
|
1422
|
+
])
|
|
1423
|
+
},
|
|
1424
|
+
{
|
|
1425
|
+
part: bridge,
|
|
1426
|
+
a: Math.min(vis[0], vis[1]),
|
|
1427
|
+
d: view.depth([
|
|
1428
|
+
0,
|
|
1429
|
+
L.oy,
|
|
1430
|
+
zPlane
|
|
1431
|
+
])
|
|
1432
|
+
}
|
|
1433
|
+
].sort((p, q) => p.d - q.d);
|
|
1434
|
+
for (const p of parts) {
|
|
1435
|
+
if (p.a <= 0) continue;
|
|
1436
|
+
ctx.save();
|
|
1437
|
+
ctx.globalAlpha = p.a;
|
|
1438
|
+
paintLayers(ctx, [{
|
|
1439
|
+
parts: [p.part],
|
|
1440
|
+
shade: rimShade
|
|
1441
|
+
}], view.px);
|
|
1442
|
+
ctx.restore();
|
|
1443
|
+
}
|
|
1444
|
+
for (const [e, s, a] of [[
|
|
1445
|
+
L,
|
|
1446
|
+
-1,
|
|
1447
|
+
vis[0]
|
|
1448
|
+
], [
|
|
1449
|
+
R,
|
|
1450
|
+
1,
|
|
1451
|
+
vis[1]
|
|
1452
|
+
]]) {
|
|
1453
|
+
if (a <= 0) continue;
|
|
1454
|
+
ctx.save();
|
|
1455
|
+
ctx.globalAlpha = a;
|
|
1456
|
+
const x = e.ox + s * F.rx * .93;
|
|
1457
|
+
sphereSprite(ctx, view, [
|
|
1458
|
+
x,
|
|
1459
|
+
e.oy - F.ry * .5,
|
|
1460
|
+
zAt(x) + F.td
|
|
1461
|
+
], .75, "silver");
|
|
1462
|
+
ctx.restore();
|
|
1463
|
+
}
|
|
1496
1464
|
}
|
|
1465
|
+
/* ── the pass entry points ──────────────────────────────────────────── */
|
|
1466
|
+
/** Behind the body: what of the headphones is behind its middle plane. */
|
|
1497
1467
|
export function drawWearBehind(ctx, path, r, wear) {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1468
|
+
if (!wear.headphones) return;
|
|
1469
|
+
const m = marksFor(path);
|
|
1470
|
+
if (!m) return;
|
|
1471
|
+
owner = m;
|
|
1472
|
+
drawPhones(ctx, viewOf(r, pxOf(ctx)), m, wear.color, "behind", null);
|
|
1503
1473
|
}
|
|
1474
|
+
/** Over the body: the hat, the headphones' near part, the bow tie. The
|
|
1475
|
+
`bodyClip` is the body's front outline, for the shadows things cast on
|
|
1476
|
+
it; `bodyZ` how far the body's front surface stands at a design point. */
|
|
1504
1477
|
export function drawWearFront(ctx, path, r, wear, bodyClip, bodyZ, face) {
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1478
|
+
const m = marksFor(path);
|
|
1479
|
+
if (!m) return;
|
|
1480
|
+
owner = m;
|
|
1481
|
+
const view = viewOf(r, pxOf(ctx));
|
|
1482
|
+
if (wear.bowTie) drawBowTie(ctx, view, r, m, wear.color, bodyZ, bodyClip, face, wear.fabric);
|
|
1483
|
+
if (wear.headphones) drawPhones(ctx, view, m, wear.color, "front", bodyClip);
|
|
1484
|
+
switch (wear.hat) {
|
|
1485
|
+
case "beret":
|
|
1486
|
+
drawBeret(ctx, view, m, wear.color, bodyClip);
|
|
1487
|
+
break;
|
|
1488
|
+
case "beanie":
|
|
1489
|
+
drawBeanie(ctx, view, m, wear.color, bodyClip);
|
|
1490
|
+
break;
|
|
1491
|
+
case "party":
|
|
1492
|
+
drawParty(ctx, view, m, wear.color, bodyClip);
|
|
1493
|
+
break;
|
|
1494
|
+
case "crown":
|
|
1495
|
+
drawCrown(ctx, view, m, bodyClip);
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1525
1498
|
}
|