capture-flow 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +238 -2
- package/bin/capture-flow.mjs +12 -0
- package/package.json +55 -4
- package/src/build.ts +186 -0
- package/src/capture.ts +77 -0
- package/src/cli.ts +139 -0
- package/src/index.ts +11 -0
- package/src/manifest.ts +98 -0
- package/src/render.ts +75 -0
- package/src/verify.ts +90 -0
- package/vendor/wajha/LICENSE +21 -0
- package/vendor/wajha/NOTICE +21 -0
- package/vendor/wajha/engine/background.ts +55 -0
- package/vendor/wajha/engine/bidi.ts +589 -0
- package/vendor/wajha/engine/cache.ts +96 -0
- package/vendor/wajha/engine/color.ts +125 -0
- package/vendor/wajha/engine/devices/android.ts +53 -0
- package/vendor/wajha/engine/devices/browser.ts +170 -0
- package/vendor/wajha/engine/devices/finish.ts +154 -0
- package/vendor/wajha/engine/devices/geometry.ts +326 -0
- package/vendor/wajha/engine/devices/index.ts +143 -0
- package/vendor/wajha/engine/devices/ipad.ts +21 -0
- package/vendor/wajha/engine/devices/iphone.ts +55 -0
- package/vendor/wajha/engine/devices/macbook.ts +187 -0
- package/vendor/wajha/engine/devices/mock/apps/calendar.ts +145 -0
- package/vendor/wajha/engine/devices/mock/apps/chat.ts +213 -0
- package/vendor/wajha/engine/devices/mock/apps/finance.ts +164 -0
- package/vendor/wajha/engine/devices/mock/apps/fitness.ts +145 -0
- package/vendor/wajha/engine/devices/mock/apps/index.ts +14 -0
- package/vendor/wajha/engine/devices/mock/apps/meditation.ts +167 -0
- package/vendor/wajha/engine/devices/mock/apps/music.ts +152 -0
- package/vendor/wajha/engine/devices/mock/apps/shop.ts +116 -0
- package/vendor/wajha/engine/devices/mock/apps/social.ts +152 -0
- package/vendor/wajha/engine/devices/mock/apps/tasks.ts +131 -0
- package/vendor/wajha/engine/devices/mock/apps/travel.ts +121 -0
- package/vendor/wajha/engine/devices/mock/art.ts +557 -0
- package/vendor/wajha/engine/devices/mock/chrome.ts +337 -0
- package/vendor/wajha/engine/devices/mock/icons.ts +614 -0
- package/vendor/wajha/engine/devices/mock/kit.ts +213 -0
- package/vendor/wajha/engine/devices/mock/rng.ts +40 -0
- package/vendor/wajha/engine/devices/mock/theme.ts +133 -0
- package/vendor/wajha/engine/devices/mock/widgets.ts +231 -0
- package/vendor/wajha/engine/devices/paths.ts +113 -0
- package/vendor/wajha/engine/devices/screen.ts +75 -0
- package/vendor/wajha/engine/devices/shadow.ts +124 -0
- package/vendor/wajha/engine/devices/slab.ts +200 -0
- package/vendor/wajha/engine/devices/vision.ts +43 -0
- package/vendor/wajha/engine/devices/watch.ts +14 -0
- package/vendor/wajha/engine/geometry.ts +152 -0
- package/vendor/wajha/engine/images.ts +96 -0
- package/vendor/wajha/engine/index.ts +20 -0
- package/vendor/wajha/engine/linebreak.ts +226 -0
- package/vendor/wajha/engine/markup.ts +84 -0
- package/vendor/wajha/engine/mock.ts +158 -0
- package/vendor/wajha/engine/paint.ts +474 -0
- package/vendor/wajha/engine/patterns.ts +263 -0
- package/vendor/wajha/engine/render.ts +364 -0
- package/vendor/wajha/engine/shapes.ts +260 -0
- package/vendor/wajha/engine/testing/canvasNode.ts +121 -0
- package/vendor/wajha/engine/testing/fakeDevices.ts +85 -0
- package/vendor/wajha/engine/text.ts +694 -0
- package/vendor/wajha/engine/types.ts +190 -0
- package/vendor/wajha/export/paths.ts +197 -0
- package/vendor/wajha/export/png.ts +223 -0
- package/vendor/wajha/export/types.ts +60 -0
- package/vendor/wajha/model/adapt.ts +889 -0
- package/vendor/wajha/model/catalog.ts +369 -0
- package/vendor/wajha/model/clone.ts +58 -0
- package/vendor/wajha/model/colorMath.ts +128 -0
- package/vendor/wajha/model/defaults.ts +386 -0
- package/vendor/wajha/model/geometry.ts +95 -0
- package/vendor/wajha/model/ids.ts +13 -0
- package/vendor/wajha/model/instantiate.ts +280 -0
- package/vendor/wajha/model/mirror.ts +43 -0
- package/vendor/wajha/model/resolve.ts +87 -0
- package/vendor/wajha/model/rules.ts +249 -0
- package/vendor/wajha/model/stub.ts +4 -0
- package/vendor/wajha/model/types.ts +456 -0
- package/vendor/wajha/model/typography.ts +28 -0
- package/vendor/wajha/model/validate.ts +917 -0
- package/vendor/wajha/templates/builtin/aurora.ts +163 -0
- package/vendor/wajha/templates/builtin/bubblegum.ts +132 -0
- package/vendor/wajha/templates/builtin/citrusPop.ts +124 -0
- package/vendor/wajha/templates/builtin/clayGarden.ts +114 -0
- package/vendor/wajha/templates/builtin/desertDune.ts +97 -0
- package/vendor/wajha/templates/builtin/glassPrism.ts +139 -0
- package/vendor/wajha/templates/builtin/launchpad.ts +140 -0
- package/vendor/wajha/templates/builtin/loudAndClear.ts +109 -0
- package/vendor/wajha/templates/builtin/midnightNeon.ts +108 -0
- package/vendor/wajha/templates/builtin/monoSwiss.ts +149 -0
- package/vendor/wajha/templates/builtin/noirGold.ts +149 -0
- package/vendor/wajha/templates/builtin/noor.ts +107 -0
- package/vendor/wajha/templates/builtin/obsidian.ts +137 -0
- package/vendor/wajha/templates/builtin/panoramaPeaks.ts +145 -0
- package/vendor/wajha/templates/builtin/panoramaRibbon.ts +138 -0
- package/vendor/wajha/templates/builtin/panoramaWaves.ts +138 -0
- package/vendor/wajha/templates/builtin/paperAndInk.ts +98 -0
- package/vendor/wajha/templates/builtin/sage.ts +94 -0
- package/vendor/wajha/templates/builtin/splitTone.ts +129 -0
- package/vendor/wajha/templates/builtin/sunsetBoulevard.ts +127 -0
- package/vendor/wajha/templates/index.ts +55 -0
- package/vendor/wajha/templates/kit.ts +761 -0
- package/vendor/wajha/ui/sortShots.ts +239 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/** Finance dashboard: balance, smooth area chart, quick actions, transactions. */
|
|
2
|
+
import { rgba } from '../../../color';
|
|
3
|
+
import { avatar, avatarStack } from '../art';
|
|
4
|
+
import { icon, type IconName } from '../icons';
|
|
5
|
+
import type { Area, Mk } from '../kit';
|
|
6
|
+
import { bar, body, box, brandFill, card, circle, money, num, series, title } from '../kit';
|
|
7
|
+
import type { MockApp } from '../widgets';
|
|
8
|
+
import { areaChart, chip, iconButton, listRow, sectionHeader, segmented } from '../widgets';
|
|
9
|
+
|
|
10
|
+
const TX_ICONS: IconName[] = ['bag', 'music', 'card', 'flame', 'compass', 'phone', 'leaf'];
|
|
11
|
+
|
|
12
|
+
function transactions(m: Mk, x: number, y: number, w: number, rows: number, rowH = 66): void {
|
|
13
|
+
const t = m.t;
|
|
14
|
+
for (let i = 0; i < rows; i++) {
|
|
15
|
+
const income = i % 4 === 2;
|
|
16
|
+
const v = income ? m.rng.range(800, 3200) : m.rng.range(4, 180);
|
|
17
|
+
listRow(m, x, y + i * rowH, w, {
|
|
18
|
+
icon: TX_ICONS[(i + m.seed) % TX_ICONS.length],
|
|
19
|
+
color: t.accents[i % 4],
|
|
20
|
+
titleW: 80 + m.rng.range(0, 60),
|
|
21
|
+
subW: 50 + m.rng.range(0, 40),
|
|
22
|
+
value: `${income ? '+' : '-'}${money(v)}`,
|
|
23
|
+
valueColor: income ? t.good : t.text,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function balanceCard(m: Mk, x: number, y: number, w: number, h: number): void {
|
|
29
|
+
card(m, x, y, w, h, 26, brandFill(m, x, y, w, h), 1.2);
|
|
30
|
+
const ctx = m.ctx;
|
|
31
|
+
ctx.save();
|
|
32
|
+
ctx.beginPath();
|
|
33
|
+
ctx.rect(x, y, w, h);
|
|
34
|
+
ctx.clip();
|
|
35
|
+
circle(m, x + w * 0.92, y + h * 0.05, h * 0.62, 'rgba(255, 255, 255, 0.10)');
|
|
36
|
+
circle(m, x + w * 0.98, y + h * 0.95, h * 0.42, 'rgba(255, 255, 255, 0.08)');
|
|
37
|
+
ctx.restore();
|
|
38
|
+
bar(m, x + 22, y + 24, 70, 6, 'rgba(255, 255, 255, 0.7)');
|
|
39
|
+
const big = money(m.rng.range(12000, 68000), 0);
|
|
40
|
+
num(m, big, x + 22, y + 72, Math.min(36, w * 0.11), 700, '#ffffff');
|
|
41
|
+
num(m, '•••• 4821', x + 22, y + h - 22, 14, 600, 'rgba(255, 255, 255, 0.82)');
|
|
42
|
+
circle(m, x + w - 50, y + h - 28, 13, 'rgba(255, 255, 255, 0.55)');
|
|
43
|
+
circle(m, x + w - 34, y + h - 28, 13, 'rgba(255, 255, 255, 0.32)');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function chartCard(m: Mk, r: Area, points: number, labels: number): void {
|
|
47
|
+
const t = m.t;
|
|
48
|
+
card(m, r.x, r.y, r.w, r.h, 26);
|
|
49
|
+
title(m, r.x + 20, r.y + 18, 96, 16);
|
|
50
|
+
segmented(m, r.x + r.w - Math.min(200, r.w * 0.5) - 16, r.y + 14, Math.min(200, r.w * 0.5), 4, 2, 30);
|
|
51
|
+
const values = series(m.rng, points, 0.35, 0.2, 0.025);
|
|
52
|
+
const hi = Math.max(1, points - 4);
|
|
53
|
+
areaChart(m, { x: r.x + 18, y: r.y + 70, w: r.w - 36, h: r.h - 112 }, values, {
|
|
54
|
+
color: t.accent,
|
|
55
|
+
highlight: hi,
|
|
56
|
+
tooltip: money(4000 + values[hi] * 9000, 0),
|
|
57
|
+
grid: r.w > 420,
|
|
58
|
+
});
|
|
59
|
+
for (let i = 0; i < labels; i++) {
|
|
60
|
+
const lx = r.x + 18 + ((r.w - 36) * (i + 0.5)) / labels;
|
|
61
|
+
bar(m, lx - 11, r.y + r.h - 26, 22, 5, t.bar3);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ACTIONS: IconName[] = ['arrowUpRight', 'arrowDown', 'plus', 'grid'];
|
|
66
|
+
|
|
67
|
+
export const finance: MockApp = {
|
|
68
|
+
id: 'finance',
|
|
69
|
+
nav: ['home', 'chart', 'card', 'layers', 'users', 'settings'],
|
|
70
|
+
glyph: 'card',
|
|
71
|
+
tabs: ['home', 'chart', 'card', 'user'],
|
|
72
|
+
tabSelected: 0,
|
|
73
|
+
narrow(m, a) {
|
|
74
|
+
const t = m.t;
|
|
75
|
+
const M = 20;
|
|
76
|
+
const x = a.x + M;
|
|
77
|
+
const w = a.w - 2 * M;
|
|
78
|
+
let y = a.y + 8;
|
|
79
|
+
avatar(m, x + 21, y + 22, 42, m.seed);
|
|
80
|
+
body(m, x + 54, y + 4, 62, 12);
|
|
81
|
+
title(m, x + 54, y + 20, 108, 17);
|
|
82
|
+
iconButton(m, x + w - 21, y + 22, 42, 'bell', { badge: true });
|
|
83
|
+
iconButton(m, x + w - 73, y + 22, 42, 'search');
|
|
84
|
+
y += 70;
|
|
85
|
+
body(m, x, y, 96, 13);
|
|
86
|
+
const big = money(m.rng.range(9000, 86000), 0);
|
|
87
|
+
const bw = num(m, big, x, y + 56, 46, 700, t.text);
|
|
88
|
+
num(m, `.${String(m.rng.int(10, 99))}`, x + bw + 2, y + 56, 26, 600, t.text2);
|
|
89
|
+
chip(m, x, y + 72, 28, { icon: 'arrowUpRight', text: `+${(m.rng.range(1.2, 9.8)).toFixed(1)}%`, fill: rgba(t.good, t.dark ? 0.16 : 0.12), ink: t.good });
|
|
90
|
+
y += 122;
|
|
91
|
+
const ch = Math.min(236, Math.max(180, a.h * 0.3));
|
|
92
|
+
chartCard(m, { x, y, w, h: ch }, 13, 6);
|
|
93
|
+
y += ch + 22;
|
|
94
|
+
const slot = w / ACTIONS.length;
|
|
95
|
+
ACTIONS.forEach((name, i) => {
|
|
96
|
+
const cx = x + slot * (i + 0.5);
|
|
97
|
+
const on = i === 0;
|
|
98
|
+
if (on) circle(m, cx, y + 28, 28, t.accent);
|
|
99
|
+
else iconButton(m, cx, y + 28, 56, name, { fill: t.surface });
|
|
100
|
+
if (on) icon(m.ctx, name, cx, y + 28, 24, t.onAccent, false, 2.2);
|
|
101
|
+
bar(m, cx - 18, y + 68, 36, 5.5, t.bar2);
|
|
102
|
+
});
|
|
103
|
+
y += 98;
|
|
104
|
+
sectionHeader(m, x, y, w);
|
|
105
|
+
y += 40;
|
|
106
|
+
const rows = Math.max(3, Math.ceil((a.y + a.h - y) / 66));
|
|
107
|
+
card(m, x - 6, y - 10, w + 12, rows * 66 + 14, 24);
|
|
108
|
+
transactions(m, x + 8, y, w - 16, rows);
|
|
109
|
+
},
|
|
110
|
+
wide(m, a) {
|
|
111
|
+
const t = m.t;
|
|
112
|
+
const gap = 20;
|
|
113
|
+
const cols = a.w > 860 ? 4 : 3;
|
|
114
|
+
const cw = (a.w - gap * (cols - 1)) / cols;
|
|
115
|
+
const statIcons: IconName[] = ['card', 'arrowUpRight', 'arrowDown', 'flame'];
|
|
116
|
+
for (let i = 0; i < cols; i++) {
|
|
117
|
+
const x = a.x + i * (cw + gap);
|
|
118
|
+
card(m, x, a.y, cw, 122, 22);
|
|
119
|
+
box(m, x + 18, a.y + 18, 38, 38, 12, rgba(t.accents[i], t.dark ? 0.2 : 0.12));
|
|
120
|
+
icon(m.ctx, statIcons[i], x + 37, a.y + 37, 19, t.accents[i], false, 2.1);
|
|
121
|
+
body(m, x + 66, a.y + 23, 70, 13);
|
|
122
|
+
num(m, money(m.rng.range(1200, 48000), 0), x + 18, a.y + 98, Math.min(28, cw * 0.13), 700, t.text);
|
|
123
|
+
const up = i !== 2;
|
|
124
|
+
chip(m, x + cw - 82, a.y + 76, 24, { icon: up ? 'arrowUpRight' : 'arrowDown', text: `${(m.rng.range(1, 12)).toFixed(1)}%`, fill: rgba(up ? t.good : t.bad, 0.12), ink: up ? t.good : t.bad });
|
|
125
|
+
}
|
|
126
|
+
const y2 = a.y + 122 + gap;
|
|
127
|
+
const h2 = Math.min(340, Math.max(220, (a.h - 142) * 0.58));
|
|
128
|
+
const leftW = a.w * 0.64;
|
|
129
|
+
chartCard(m, { x: a.x, y: y2, w: leftW, h: h2 }, 22, 12);
|
|
130
|
+
const rx = a.x + leftW + gap;
|
|
131
|
+
const rw = a.w - leftW - gap;
|
|
132
|
+
const bh = h2 * 0.58;
|
|
133
|
+
balanceCard(m, rx, y2, rw, bh);
|
|
134
|
+
card(m, rx, y2 + bh + gap, rw, h2 - bh - gap, 22);
|
|
135
|
+
title(m, rx + 18, y2 + bh + gap + 16, 92, 15);
|
|
136
|
+
avatarStack(m, rx + 18, y2 + bh + gap + (h2 - bh - gap) * 0.62, 34, 4, m.seed + 3, t.surface);
|
|
137
|
+
circle(m, rx + rw - 36, y2 + bh + gap + (h2 - bh - gap) * 0.62, 17, t.accent);
|
|
138
|
+
icon(m.ctx, 'plus', rx + rw - 36, y2 + bh + gap + (h2 - bh - gap) * 0.62, 16, t.onAccent, false, 2.6);
|
|
139
|
+
|
|
140
|
+
const y3 = y2 + h2 + gap;
|
|
141
|
+
const h3 = a.y + a.h - y3;
|
|
142
|
+
if (h3 < 80) return;
|
|
143
|
+
card(m, a.x, y3, a.w, h3 + 40, 22);
|
|
144
|
+
sectionHeader(m, a.x + 20, y3 + 18, a.w - 40, 120);
|
|
145
|
+
const rows = Math.max(1, Math.ceil((h3 - 60) / 62));
|
|
146
|
+
for (let i = 0; i < rows; i++) {
|
|
147
|
+
const ry = y3 + 60 + i * 62;
|
|
148
|
+
listRow(m, a.x + 20, ry, a.w * 0.38, {
|
|
149
|
+
icon: TX_ICONS[(i + m.seed) % TX_ICONS.length],
|
|
150
|
+
color: t.accents[i % 4],
|
|
151
|
+
titleW: 90 + m.rng.range(0, 50),
|
|
152
|
+
subW: 60,
|
|
153
|
+
});
|
|
154
|
+
num(m, `${m.rng.int(1, 28)}.${String(m.rng.int(1, 12)).padStart(2, '0')}.2026`, a.x + a.w * 0.5, ry + 29, 13.5, 500, t.text2);
|
|
155
|
+
const ok = i % 3 !== 1;
|
|
156
|
+
chip(m, a.x + a.w * 0.68, ry + 10, 26, { fill: rgba(ok ? t.good : t.warn, 0.14), ink: ok ? t.good : t.warn, barW: 44 });
|
|
157
|
+
num(m, `-${money(m.rng.range(8, 420))}`, a.x + a.w - 22, ry + 29, 15, 650, t.text, 'right');
|
|
158
|
+
if (i < rows - 1) {
|
|
159
|
+
m.ctx.fillStyle = t.line;
|
|
160
|
+
m.ctx.fillRect(a.x + 20, ry + 54, a.w - 40, 1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** Fitness: activity rings, stat tiles, weekly bars, workouts. */
|
|
2
|
+
import { rgba } from '../../../color';
|
|
3
|
+
import { avatar, photo } from '../art';
|
|
4
|
+
import { icon, type IconName } from '../icons';
|
|
5
|
+
import type { Area, Mk } from '../kit';
|
|
6
|
+
import { bar, body, card, circle, linear, money, num, progressRing, series, smoothPath, title } from '../kit';
|
|
7
|
+
import { tint } from '../theme';
|
|
8
|
+
import type { MockApp } from '../widgets';
|
|
9
|
+
import { barChart, chip, segmented } from '../widgets';
|
|
10
|
+
|
|
11
|
+
/** Ring colours: vivid, Apple-like, but drawn from the palette. */
|
|
12
|
+
function ringColors(m: Mk): [string, string, string] {
|
|
13
|
+
const a = m.t.accents;
|
|
14
|
+
return [a[0], a[1], a[2]];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function rings(m: Mk, cx: number, cy: number, r: number, lw: number): void {
|
|
18
|
+
const cols = ringColors(m);
|
|
19
|
+
const prog = [0.82, 0.64, 0.92];
|
|
20
|
+
cols.forEach((c, i) => {
|
|
21
|
+
const rr = r - i * (lw + 4);
|
|
22
|
+
progressRing(m, cx, cy, rr, lw, prog[i], linear(m, cx - rr, cy - rr, cx + rr, cy + rr, [
|
|
23
|
+
[0, c],
|
|
24
|
+
[1, rgba(c, 0.82)],
|
|
25
|
+
]), rgba(c, m.t.dark ? 0.2 : 0.14));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const STATS: { icon: IconName; value: (m: Mk) => string }[] = [
|
|
30
|
+
{ icon: 'flame', value: (m) => money(m.rng.int(320, 780), 0) },
|
|
31
|
+
{ icon: 'activity', value: (m) => money(m.rng.int(5200, 12800), 0) },
|
|
32
|
+
{ icon: 'drop', value: (m) => (m.rng.range(1.2, 2.6)).toFixed(1) },
|
|
33
|
+
{ icon: 'moon', value: (m) => `${m.rng.int(6, 8)}:${String(m.rng.int(0, 59)).padStart(2, '0')}` },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function statTile(m: Mk, r: Area, i: number): void {
|
|
37
|
+
const t = m.t;
|
|
38
|
+
const c = t.accents[i % 4];
|
|
39
|
+
card(m, r.x, r.y, r.w, r.h, 22);
|
|
40
|
+
circle(m, r.x + 34, r.y + 34, 18, tint(t, c, 0.84));
|
|
41
|
+
icon(m.ctx, STATS[i].icon, r.x + 34, r.y + 34, 19, c, false, 2.1);
|
|
42
|
+
num(m, STATS[i].value(m), r.x + 18, r.y + r.h - 34, Math.min(26, r.w * 0.17), 700, t.text);
|
|
43
|
+
body(m, r.x + 18, r.y + r.h - 26, r.w * 0.38, 12);
|
|
44
|
+
// Tiny trend line.
|
|
45
|
+
const vals = series(m.rng, 7, 0.4, 0.3, 0.03);
|
|
46
|
+
const sx = r.x + r.w - 70;
|
|
47
|
+
const sy = r.y + 22;
|
|
48
|
+
const ctx = m.ctx;
|
|
49
|
+
ctx.beginPath();
|
|
50
|
+
smoothPath(ctx, vals.map((v, j) => [sx + j * 9, sy + 26 - v * 26] as [number, number]));
|
|
51
|
+
ctx.lineWidth = 2.2;
|
|
52
|
+
ctx.lineCap = 'round';
|
|
53
|
+
ctx.strokeStyle = c;
|
|
54
|
+
ctx.stroke();
|
|
55
|
+
ctx.lineCap = 'butt';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ringCard(m: Mk, r: Area): void {
|
|
59
|
+
const t = m.t;
|
|
60
|
+
card(m, r.x, r.y, r.w, r.h, 26);
|
|
61
|
+
const rad = Math.min(r.h * 0.38, r.w * 0.22);
|
|
62
|
+
const cx = r.x + 22 + rad;
|
|
63
|
+
const cy = r.y + r.h / 2;
|
|
64
|
+
rings(m, cx, cy, rad, Math.max(10, rad * 0.21));
|
|
65
|
+
const cols = ringColors(m);
|
|
66
|
+
const vals = [`${m.rng.int(380, 560)}/600`, `${m.rng.int(20, 34)}/45`, `${m.rng.int(9, 12)}/12`];
|
|
67
|
+
const tx = cx + rad + 26;
|
|
68
|
+
const step = Math.min(46, (r.h - 40) / 3);
|
|
69
|
+
vals.forEach((v, i) => {
|
|
70
|
+
const ty = cy - step + i * step;
|
|
71
|
+
circle(m, tx + 5, ty - 5, 5, cols[i]);
|
|
72
|
+
num(m, v, tx + 18, ty + 1, 18, 700, t.text);
|
|
73
|
+
body(m, tx + 18, ty + 6, 54, 11);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function weekCard(m: Mk, r: Area): void {
|
|
78
|
+
const t = m.t;
|
|
79
|
+
card(m, r.x, r.y, r.w, r.h, 24);
|
|
80
|
+
title(m, r.x + 20, r.y + 18, 100, 15);
|
|
81
|
+
chip(m, r.x + r.w - 100, r.y + 14, 26, { icon: 'arrowUpRight', text: '+12%', fill: rgba(t.good, 0.13), ink: t.good });
|
|
82
|
+
const vals = series(m.rng, 7, 0.55, 0.45, 0);
|
|
83
|
+
barChart(m, { x: r.x + 14, y: r.y + 58, w: r.w - 28, h: r.h - 96 }, vals, t.accent, 4);
|
|
84
|
+
for (let i = 0; i < 7; i++) bar(m, r.x + 14 + ((r.w - 28) * (i + 0.5)) / 7 - 6, r.y + r.h - 24, 12, 5, i === 4 ? t.accent : t.bar3);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function workoutRow(m: Mk, x: number, y: number, w: number, k: number): void {
|
|
88
|
+
const t = m.t;
|
|
89
|
+
photo(m, x, y, 58, 58, 16, m.seed + k * 3 + 1);
|
|
90
|
+
title(m, x + 72, y + 8, 110 + (k % 3) * 20, 15);
|
|
91
|
+
icon(m.ctx, 'clock', x + 79, y + 41, 13, t.text2, false, 2.2);
|
|
92
|
+
num(m, `${m.rng.int(20, 59)}:00`, x + 90, y + 45.5, 12.5, 500, t.text2);
|
|
93
|
+
icon(m.ctx, 'flame', x + 146, y + 41, 13, t.text2, false, 2.2);
|
|
94
|
+
num(m, String(m.rng.int(180, 640)), x + 157, y + 45.5, 12.5, 500, t.text2);
|
|
95
|
+
icon(m.ctx, 'chevronRight', x + w - 10, y + 29, 18, t.text2, false, 2.2);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const fitness: MockApp = {
|
|
99
|
+
id: 'fitness',
|
|
100
|
+
nav: ['activity', 'flame', 'calendar', 'chart', 'users', 'settings'],
|
|
101
|
+
glyph: 'activity',
|
|
102
|
+
tabs: ['activity', 'flame', 'chart', 'user'],
|
|
103
|
+
tabSelected: 0,
|
|
104
|
+
narrow(m, a) {
|
|
105
|
+
const t = m.t;
|
|
106
|
+
const M = 20;
|
|
107
|
+
const x = a.x + M;
|
|
108
|
+
const w = a.w - 2 * M;
|
|
109
|
+
let y = a.y + 8;
|
|
110
|
+
bar(m, x, y + 6, 92, 6, t.bar2);
|
|
111
|
+
title(m, x, y + 18, 130, 28);
|
|
112
|
+
avatar(m, x + w - 21, y + 24, 42, m.seed + 4);
|
|
113
|
+
y += 74;
|
|
114
|
+
ringCard(m, { x, y, w, h: 196 });
|
|
115
|
+
y += 214;
|
|
116
|
+
const gap = 14;
|
|
117
|
+
const tw = (w - gap) / 2;
|
|
118
|
+
for (let i = 0; i < 4; i++) statTile(m, { x: x + (i % 2) * (tw + gap), y: y + Math.floor(i / 2) * (128 + gap), w: tw, h: 128 }, i);
|
|
119
|
+
y += 2 * 128 + gap + 18;
|
|
120
|
+
weekCard(m, { x, y, w, h: 196 });
|
|
121
|
+
y += 214;
|
|
122
|
+
title(m, x, y, 120, 17);
|
|
123
|
+
y += 36;
|
|
124
|
+
for (let k = 0; y < a.y + a.h + 30; k++, y += 76) workoutRow(m, x, y, w, k);
|
|
125
|
+
},
|
|
126
|
+
wide(m, a) {
|
|
127
|
+
const gap = 20;
|
|
128
|
+
const leftW = a.w * 0.4;
|
|
129
|
+
ringCard(m, { x: a.x, y: a.y, w: leftW, h: 260 });
|
|
130
|
+
const rx = a.x + leftW + gap;
|
|
131
|
+
const rw = a.w - leftW - gap;
|
|
132
|
+
const tw = (rw - gap) / 2;
|
|
133
|
+
for (let i = 0; i < 4; i++) statTile(m, { x: rx + (i % 2) * (tw + gap), y: a.y + Math.floor(i / 2) * (120 + gap), w: tw, h: 120 }, i);
|
|
134
|
+
const y2 = a.y + 260 + gap;
|
|
135
|
+
const h2 = Math.max(160, a.y + a.h - y2);
|
|
136
|
+
const cw = a.w * 0.6;
|
|
137
|
+
weekCard(m, { x: a.x, y: y2, w: cw, h: h2 });
|
|
138
|
+
const lx = a.x + cw + gap;
|
|
139
|
+
const lw = a.w - cw - gap;
|
|
140
|
+
card(m, lx, y2, lw, h2 + 30, 24);
|
|
141
|
+
title(m, lx + 20, y2 + 18, 110, 15);
|
|
142
|
+
segmented(m, lx + lw - 150, y2 + 12, 130, 3, 0, 28);
|
|
143
|
+
for (let k = 0, y = y2 + 62; y < a.y + a.h; k++, y += 74) workoutRow(m, lx + 20, y, lw - 40, k);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Every mock app, in seed order. Seed n shows APPS[n mod length]. */
|
|
2
|
+
import type { MockApp } from '../widgets';
|
|
3
|
+
import { calendar } from './calendar';
|
|
4
|
+
import { chat } from './chat';
|
|
5
|
+
import { finance } from './finance';
|
|
6
|
+
import { fitness } from './fitness';
|
|
7
|
+
import { meditation } from './meditation';
|
|
8
|
+
import { music } from './music';
|
|
9
|
+
import { shop } from './shop';
|
|
10
|
+
import { social } from './social';
|
|
11
|
+
import { tasks } from './tasks';
|
|
12
|
+
import { travel } from './travel';
|
|
13
|
+
|
|
14
|
+
export const APPS: readonly MockApp[] = [finance, social, tasks, fitness, chat, music, travel, shop, calendar, meditation];
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/** Meditation timer: calm gradient, breathing halo, big countdown ring, sessions. */
|
|
2
|
+
import { lighten, mix, rgba } from '../../../color';
|
|
3
|
+
import { photo } from '../art';
|
|
4
|
+
import { icon, type IconName } from '../icons';
|
|
5
|
+
import type { Mk } from '../kit';
|
|
6
|
+
import { bar, body, box, card, circle, linear, num, radial, shadowed, title } from '../kit';
|
|
7
|
+
import type { MockApp } from '../widgets';
|
|
8
|
+
|
|
9
|
+
function halo(m: Mk, cx: number, cy: number, r: number, progress: number, ink: string): void {
|
|
10
|
+
const t = m.t;
|
|
11
|
+
const ctx = m.ctx;
|
|
12
|
+
for (let i = 3; i >= 1; i--) circle(m, cx, cy, r * (1 + i * 0.13), rgba(t.accent, t.dark ? 0.05 + (3 - i) * 0.03 : 0.06 + (3 - i) * 0.035));
|
|
13
|
+
circle(m, cx, cy, r, radial(m, cx, cy - r * 0.3, r * 1.3, [
|
|
14
|
+
[0, t.dark ? rgba(lighten(t.accent, 0.2), 0.32) : rgba('#ffffff', 0.95)],
|
|
15
|
+
[1, t.dark ? rgba(t.accent, 0.1) : rgba(lighten(t.accent, 0.7), 0.9)],
|
|
16
|
+
]));
|
|
17
|
+
const lw = Math.max(8, r * 0.075);
|
|
18
|
+
const rr = r * 0.86;
|
|
19
|
+
ctx.beginPath();
|
|
20
|
+
ctx.arc(cx, cy, rr, 0, Math.PI * 2);
|
|
21
|
+
ctx.lineWidth = lw;
|
|
22
|
+
ctx.strokeStyle = rgba(t.accent, t.dark ? 0.2 : 0.14);
|
|
23
|
+
ctx.stroke();
|
|
24
|
+
const a1 = -Math.PI / 2 + Math.PI * 2 * progress;
|
|
25
|
+
ctx.beginPath();
|
|
26
|
+
ctx.arc(cx, cy, rr, -Math.PI / 2, a1);
|
|
27
|
+
ctx.lineCap = 'round';
|
|
28
|
+
ctx.strokeStyle = linear(m, cx - rr, cy - rr, cx + rr, cy + rr, [
|
|
29
|
+
[0, t.companion],
|
|
30
|
+
[1, t.accent],
|
|
31
|
+
]);
|
|
32
|
+
ctx.stroke();
|
|
33
|
+
ctx.lineCap = 'butt';
|
|
34
|
+
const kx = cx + Math.cos(a1) * rr;
|
|
35
|
+
const ky = cy + Math.sin(a1) * rr;
|
|
36
|
+
shadowed(m, 10, 3, rgba(t.accent, 0.4), () => circle(m, kx, ky, lw * 0.95, '#ffffff'));
|
|
37
|
+
circle(m, kx, ky, lw * 0.45, t.accent);
|
|
38
|
+
num(m, '12:45', cx, cy + r * 0.13, Math.min(60, r * 0.42), 300, ink, 'center');
|
|
39
|
+
bar(m, cx - r * 0.22, cy + r * 0.28, r * 0.44, 6, rgba(ink, 0.35));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const STATS: { icon: IconName; value: string }[] = [
|
|
43
|
+
{ icon: 'flame', value: '7' },
|
|
44
|
+
{ icon: 'clock', value: '128' },
|
|
45
|
+
{ icon: 'heart', value: '14' },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function sessionCard(m: Mk, x: number, y: number, w: number, h: number, k: number): void {
|
|
49
|
+
photo(m, x, y, w, h, 22, m.seed + k * 4 + 3);
|
|
50
|
+
box(m, x, y + h * 0.4, w, h * 0.6, [0, 0, 22, 22], linear(m, 0, y + h * 0.4, 0, y + h, [
|
|
51
|
+
[0, 'rgba(0, 0, 0, 0)'],
|
|
52
|
+
[1, 'rgba(0, 0, 0, 0.5)'],
|
|
53
|
+
]));
|
|
54
|
+
bar(m, x + 14, y + h - 40, w * 0.55, 8, 'rgba(255, 255, 255, 0.95)');
|
|
55
|
+
num(m, `${5 + ((k * 7) % 20)}:00`, x + 14, y + h - 14, 12, 600, 'rgba(255, 255, 255, 0.85)');
|
|
56
|
+
circle(m, x + w - 24, y + 24, 14, 'rgba(255, 255, 255, 0.3)');
|
|
57
|
+
icon(m.ctx, 'play', x + w - 23, y + 24, 12, '#ffffff', false, 1.6);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function inkFor(m: Mk): string {
|
|
61
|
+
return m.t.dark ? '#ffffff' : m.t.text;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const meditation: MockApp = {
|
|
65
|
+
id: 'meditation',
|
|
66
|
+
nav: ['moon', 'leaf', 'music', 'chart', 'heart', 'settings'],
|
|
67
|
+
glyph: 'leaf',
|
|
68
|
+
tabs: null,
|
|
69
|
+
tabSelected: 0,
|
|
70
|
+
backdrop(m, W, H) {
|
|
71
|
+
const t = m.t;
|
|
72
|
+
const ctx = m.ctx;
|
|
73
|
+
ctx.fillStyle = linear(m, 0, 0, 0, H, t.dark
|
|
74
|
+
? [
|
|
75
|
+
[0, mix(t.accent, '#090912', 0.72)],
|
|
76
|
+
[0.6, '#0b0b14'],
|
|
77
|
+
[1, '#07070b'],
|
|
78
|
+
]
|
|
79
|
+
: [
|
|
80
|
+
[0, lighten(t.accent, 0.82)],
|
|
81
|
+
[0.55, lighten(t.companion, 0.9)],
|
|
82
|
+
[1, '#f7f7fa'],
|
|
83
|
+
]);
|
|
84
|
+
ctx.fillRect(0, 0, W, H);
|
|
85
|
+
ctx.fillStyle = radial(m, W * 0.15, H * 0.12, W * 0.8, [
|
|
86
|
+
[0, rgba(t.companion, t.dark ? 0.22 : 0.25)],
|
|
87
|
+
[1, rgba(t.companion, 0)],
|
|
88
|
+
]);
|
|
89
|
+
ctx.fillRect(0, 0, W, H);
|
|
90
|
+
},
|
|
91
|
+
ink: inkFor,
|
|
92
|
+
narrow(m, a) {
|
|
93
|
+
const t = m.t;
|
|
94
|
+
const ink = inkFor(m);
|
|
95
|
+
const M = 22;
|
|
96
|
+
const x = a.x + M;
|
|
97
|
+
const w = a.w - 2 * M;
|
|
98
|
+
let y = a.y + 8;
|
|
99
|
+
icon(m.ctx, 'close', x + 10, y + 18, 22, ink, false, 2.2);
|
|
100
|
+
bar(m, a.x + a.w / 2 - 40, y + 14, 80, 8, rgba(ink, 0.7));
|
|
101
|
+
icon(m.ctx, 'more', x + w - 10, y + 18, 24, ink, false);
|
|
102
|
+
y += 54;
|
|
103
|
+
bar(m, a.x + a.w / 2 - 92, y, 184, 16, rgba(ink, 0.92));
|
|
104
|
+
bar(m, a.x + a.w / 2 - 70, y + 30, 140, 8, rgba(ink, 0.4));
|
|
105
|
+
y += 64;
|
|
106
|
+
const r = Math.min(w * 0.38, a.h * 0.21, 170);
|
|
107
|
+
halo(m, a.x + a.w / 2, y + r * 1.3, r, 0.62, ink);
|
|
108
|
+
y += r * 2.6 + 18;
|
|
109
|
+
const step = w / 3;
|
|
110
|
+
STATS.forEach((s, i) => {
|
|
111
|
+
const cx = x + step * (i + 0.5);
|
|
112
|
+
icon(m.ctx, s.icon, cx - 20, y + 10, 18, t.accent, false, 2.2);
|
|
113
|
+
num(m, s.value, cx - 6, y + 16, 18, 700, ink);
|
|
114
|
+
bar(m, cx - 20, y + 30, 40, 5, rgba(ink, 0.3));
|
|
115
|
+
});
|
|
116
|
+
y += 64;
|
|
117
|
+
const cy = y + 40;
|
|
118
|
+
const glass = t.dark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.8)';
|
|
119
|
+
circle(m, a.x + a.w / 2 - 92, cy, 26, glass);
|
|
120
|
+
icon(m.ctx, 'prev', a.x + a.w / 2 - 92, cy, 20, ink, false, 1.6);
|
|
121
|
+
shadowed(m, 28, 12, rgba(t.accent, 0.45), () => circle(m, a.x + a.w / 2, cy, 40, t.accent));
|
|
122
|
+
icon(m.ctx, 'pause', a.x + a.w / 2, cy, 30, t.onAccent, false);
|
|
123
|
+
circle(m, a.x + a.w / 2 + 92, cy, 26, glass);
|
|
124
|
+
icon(m.ctx, 'next', a.x + a.w / 2 + 92, cy, 20, ink, false, 1.6);
|
|
125
|
+
y += 112;
|
|
126
|
+
title(m, x, y, 120, 17, rgba(ink, 0.85));
|
|
127
|
+
y += 36;
|
|
128
|
+
const cw = Math.min(170, w * 0.46);
|
|
129
|
+
for (let i = 0; x + i * (cw + 12) < a.x + a.w; i++) sessionCard(m, x + i * (cw + 12), y, cw, Math.max(120, Math.min(170, a.y + a.h - y + 40)), i);
|
|
130
|
+
},
|
|
131
|
+
wide(m, a) {
|
|
132
|
+
const t = m.t;
|
|
133
|
+
const gap = 20;
|
|
134
|
+
const lw = a.w * 0.5;
|
|
135
|
+
card(m, a.x, a.y, lw, a.h + 40, 28, linear(m, a.x, a.y, a.x + lw, a.y + a.h, t.dark
|
|
136
|
+
? [
|
|
137
|
+
[0, mix(t.accent, '#0b0b14', 0.7)],
|
|
138
|
+
[1, '#0b0b12'],
|
|
139
|
+
]
|
|
140
|
+
: [
|
|
141
|
+
[0, lighten(t.accent, 0.85)],
|
|
142
|
+
[1, lighten(t.companion, 0.9)],
|
|
143
|
+
]), 1);
|
|
144
|
+
const ink = inkFor(m);
|
|
145
|
+
const r = Math.min(lw * 0.3, a.h * 0.3);
|
|
146
|
+
halo(m, a.x + lw / 2, a.y + a.h * 0.44, r, 0.62, ink);
|
|
147
|
+
const cy = a.y + a.h * 0.44 + r * 1.55;
|
|
148
|
+
shadowed(m, 24, 10, rgba(t.accent, 0.4), () => circle(m, a.x + lw / 2, cy, 34, t.accent));
|
|
149
|
+
icon(m.ctx, 'pause', a.x + lw / 2, cy, 26, t.onAccent, false);
|
|
150
|
+
const rx = a.x + lw + gap;
|
|
151
|
+
const rw = a.w - lw - gap;
|
|
152
|
+
title(m, rx, a.y + 4, 140, 18);
|
|
153
|
+
const cw = (rw - gap) / 2;
|
|
154
|
+
const ch = Math.min(200, (a.h - 200) / 2);
|
|
155
|
+
for (let i = 0; i < 4; i++) sessionCard(m, rx + (i % 2) * (cw + gap), a.y + 40 + Math.floor(i / 2) * (ch + gap), cw, ch, i);
|
|
156
|
+
const sy = a.y + 40 + 2 * (ch + gap);
|
|
157
|
+
const sw = (rw - gap * 2) / 3;
|
|
158
|
+
STATS.forEach((s, i) => {
|
|
159
|
+
const sx = rx + i * (sw + gap);
|
|
160
|
+
card(m, sx, sy, sw, 110, 22);
|
|
161
|
+
circle(m, sx + 32, sy + 32, 16, rgba(t.accents[i], 0.14));
|
|
162
|
+
icon(m.ctx, s.icon, sx + 32, sy + 32, 17, t.accents[i], false, 2.2);
|
|
163
|
+
num(m, s.value, sx + 18, sy + 86, 26, 700, t.text);
|
|
164
|
+
body(m, sx + 18 + 44, sy + 72, sw * 0.3, 12);
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/** Music player: large cover art on a colour-matched backdrop, waveform scrubber, transport controls. */
|
|
2
|
+
import { darken, lighten, mix, rgba, rotateHue } from '../../../color';
|
|
3
|
+
import { albumArt } from '../art';
|
|
4
|
+
import { icon } from '../icons';
|
|
5
|
+
import type { Mk } from '../kit';
|
|
6
|
+
import { bar, body, box, card, circle, linear, num, paragraph, radial, shadowed, title } from '../kit';
|
|
7
|
+
import type { MockApp } from '../widgets';
|
|
8
|
+
import { sectionHeader } from '../widgets';
|
|
9
|
+
|
|
10
|
+
/** Colour the backdrop takes from the cover. */
|
|
11
|
+
function coverTone(m: Mk): string {
|
|
12
|
+
return rotateHue(m.t.accents[0], ((m.seed * 37) % 60) - 30);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function waveform(m: Mk, x: number, cy: number, w: number, played: number, color: string, rest: string): void {
|
|
16
|
+
const n = Math.max(12, Math.floor(w / 6));
|
|
17
|
+
const step = w / n;
|
|
18
|
+
for (let i = 0; i < n; i++) {
|
|
19
|
+
const v = 0.18 + 0.82 * Math.abs(Math.sin(i * 0.9 + m.seed) * 0.6 + Math.sin(i * 0.31) * 0.4);
|
|
20
|
+
const h = 4 + v * 26;
|
|
21
|
+
box(m, x + i * step, cy - h / 2, step * 0.55, h, step * 0.27, i / n < played ? color : rest);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function controls(m: Mk, cx: number, cy: number, w: number, ink: string): void {
|
|
26
|
+
const t = m.t;
|
|
27
|
+
const ctx = m.ctx;
|
|
28
|
+
const sp = Math.min(w / 4.6, 82);
|
|
29
|
+
icon(ctx, 'shuffle', cx - sp * 2, cy, 22, rgba(ink, 0.55), false, 2);
|
|
30
|
+
icon(ctx, 'prev', cx - sp, cy, 32, ink, false, 1.6);
|
|
31
|
+
shadowed(m, 24, 10, rgba(t.accent, 0.4), () => circle(m, cx, cy, 37, t.accent));
|
|
32
|
+
icon(ctx, 'pause', cx, cy, 30, t.onAccent, false);
|
|
33
|
+
icon(ctx, 'next', cx + sp, cy, 32, ink, false, 1.6);
|
|
34
|
+
icon(ctx, 'repeat', cx + sp * 2, cy, 22, t.accent, false, 2);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const music: MockApp = {
|
|
38
|
+
id: 'music',
|
|
39
|
+
nav: ['home', 'search', 'music', 'list', 'heart', 'settings'],
|
|
40
|
+
glyph: 'music',
|
|
41
|
+
tabs: null,
|
|
42
|
+
tabSelected: 0,
|
|
43
|
+
backdrop(m, W, H) {
|
|
44
|
+
const t = m.t;
|
|
45
|
+
const c = coverTone(m);
|
|
46
|
+
const ctx = m.ctx;
|
|
47
|
+
ctx.fillStyle = linear(m, 0, 0, 0, H, t.dark
|
|
48
|
+
? [
|
|
49
|
+
[0, darken(c, 0.45)],
|
|
50
|
+
[0.55, mix(darken(c, 0.7), t.bg, 0.4)],
|
|
51
|
+
[1, t.bg],
|
|
52
|
+
]
|
|
53
|
+
: [
|
|
54
|
+
[0, lighten(c, 0.72)],
|
|
55
|
+
[0.6, lighten(c, 0.9)],
|
|
56
|
+
[1, t.bg],
|
|
57
|
+
]);
|
|
58
|
+
ctx.fillRect(0, 0, W, H);
|
|
59
|
+
ctx.fillStyle = radial(m, W * 0.8, H * 0.1, W * 0.9, [
|
|
60
|
+
[0, rgba(rotateHue(c, 40), t.dark ? 0.35 : 0.28)],
|
|
61
|
+
[1, rgba(c, 0)],
|
|
62
|
+
]);
|
|
63
|
+
ctx.fillRect(0, 0, W, H);
|
|
64
|
+
},
|
|
65
|
+
ink: (m) => (m.t.dark ? '#ffffff' : m.t.text),
|
|
66
|
+
narrow(m, a) {
|
|
67
|
+
const t = m.t;
|
|
68
|
+
const ink = t.dark ? '#ffffff' : t.text;
|
|
69
|
+
const M = 26;
|
|
70
|
+
const x = a.x + M;
|
|
71
|
+
const w = a.w - 2 * M;
|
|
72
|
+
let y = a.y + 8;
|
|
73
|
+
icon(m.ctx, 'chevronDown', x + 10, y + 18, 26, ink, false, 2.4);
|
|
74
|
+
bar(m, a.x + a.w / 2 - 34, y + 6, 68, 5, rgba(ink, 0.45));
|
|
75
|
+
bar(m, a.x + a.w / 2 - 54, y + 20, 108, 8, rgba(ink, 0.85));
|
|
76
|
+
icon(m.ctx, 'more', x + w - 10, y + 18, 24, ink, false);
|
|
77
|
+
y += 58;
|
|
78
|
+
const s = Math.min(w, a.h * 0.46);
|
|
79
|
+
const ax = a.x + (a.w - s) / 2;
|
|
80
|
+
shadowed(m, 46, 22, rgba(darken(coverTone(m), 0.4), 0.45), () => box(m, ax, y, s, s, 24, coverTone(m)));
|
|
81
|
+
albumArt(m, ax, y, s, 24, m.seed + 2);
|
|
82
|
+
y += s + 30;
|
|
83
|
+
bar(m, x, y + 4, w * 0.56, 13, ink);
|
|
84
|
+
bar(m, x, y + 30, w * 0.36, 8, rgba(ink, 0.5));
|
|
85
|
+
icon(m.ctx, 'heart', x + w - 14, y + 18, 26, t.accent, true, 2);
|
|
86
|
+
y += 66;
|
|
87
|
+
waveform(m, x, y + 14, w, 0.38, ink, rgba(ink, 0.22));
|
|
88
|
+
num(m, '1:24', x, y + 48, 12.5, 600, rgba(ink, 0.6));
|
|
89
|
+
num(m, '-2:31', x + w, y + 48, 12.5, 600, rgba(ink, 0.6), 'right');
|
|
90
|
+
y += 98;
|
|
91
|
+
controls(m, a.x + a.w / 2, y, w, ink);
|
|
92
|
+
y += 70;
|
|
93
|
+
icon(m.ctx, 'phone', x + 10, y, 20, rgba(ink, 0.55), false, 2);
|
|
94
|
+
icon(m.ctx, 'share', a.x + a.w / 2, y, 20, rgba(ink, 0.55), false, 2);
|
|
95
|
+
icon(m.ctx, 'list', x + w - 10, y, 20, rgba(ink, 0.55), false, 2);
|
|
96
|
+
y += 34;
|
|
97
|
+
if (y < a.y + a.h - 60) {
|
|
98
|
+
const lh = Math.max(120, a.y + a.h - y + 40);
|
|
99
|
+
box(m, x - 8, y, w + 16, lh, 26, t.dark ? 'rgba(255, 255, 255, 0.07)' : 'rgba(255, 255, 255, 0.6)');
|
|
100
|
+
bar(m, x + 12, y + 22, 70, 8, rgba(ink, 0.8));
|
|
101
|
+
paragraph(m, x + 12, y + 50, w - 24, Math.max(1, Math.min(6, Math.floor((lh - 60) / 30))), 18, rgba(ink, 0.32));
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
wide(m, a) {
|
|
105
|
+
const t = m.t;
|
|
106
|
+
const c = coverTone(m);
|
|
107
|
+
const heroH = Math.min(240, a.h * 0.36);
|
|
108
|
+
card(m, a.x, a.y, a.w, heroH, 26, linear(m, a.x, a.y, a.x + a.w, a.y + heroH, [
|
|
109
|
+
[0, darken(c, 0.25)],
|
|
110
|
+
[1, rotateHue(c, 50)],
|
|
111
|
+
]), 1);
|
|
112
|
+
bar(m, a.x + 32, a.y + 38, 80, 7, 'rgba(255, 255, 255, 0.7)');
|
|
113
|
+
bar(m, a.x + 32, a.y + 62, Math.min(320, a.w * 0.34), 22, '#ffffff');
|
|
114
|
+
bar(m, a.x + 32, a.y + 98, Math.min(240, a.w * 0.26), 8, 'rgba(255, 255, 255, 0.7)');
|
|
115
|
+
box(m, a.x + 32, a.y + heroH - 70, 128, 42, 21, '#ffffff');
|
|
116
|
+
icon(m.ctx, 'play', a.x + 58, a.y + heroH - 49, 16, c, false, 1.6);
|
|
117
|
+
bar(m, a.x + 76, a.y + heroH - 52, 58, 6, darken(c, 0.2));
|
|
118
|
+
const as = heroH - 48;
|
|
119
|
+
albumArt(m, a.x + a.w - as - 24, a.y + 24, as, 18, m.seed + 5);
|
|
120
|
+
albumArt(m, a.x + a.w - as * 1.7 - 40, a.y + 24 + as * 0.15, as * 0.7, 16, m.seed + 6);
|
|
121
|
+
|
|
122
|
+
let y = a.y + heroH + 26;
|
|
123
|
+
sectionHeader(m, a.x, y, a.w, 150);
|
|
124
|
+
y += 40;
|
|
125
|
+
const cols = a.w > 900 ? 6 : 5;
|
|
126
|
+
const gap = 18;
|
|
127
|
+
const cw = (a.w - gap * (cols - 1)) / cols;
|
|
128
|
+
for (let row = 0; y + cw * 0.5 < a.y + a.h - 70; row++) {
|
|
129
|
+
for (let i = 0; i < cols; i++) {
|
|
130
|
+
const cx = a.x + i * (cw + gap);
|
|
131
|
+
albumArt(m, cx, y, cw, 16, m.seed + row * cols + i + 9);
|
|
132
|
+
title(m, cx, y + cw + 10, cw * 0.7, 14);
|
|
133
|
+
body(m, cx, y + cw + 30, cw * 0.48, 12);
|
|
134
|
+
}
|
|
135
|
+
y += cw + 64;
|
|
136
|
+
}
|
|
137
|
+
// Now playing bar.
|
|
138
|
+
const py = a.y + a.h - 64;
|
|
139
|
+
card(m, a.x, py, a.w, 76, 22, t.dark ? t.surface2 : t.surface, 1.2);
|
|
140
|
+
albumArt(m, a.x + 12, py + 10, 56, 12, m.seed + 2);
|
|
141
|
+
title(m, a.x + 82, py + 16, 120, 15);
|
|
142
|
+
body(m, a.x + 82, py + 38, 80, 12);
|
|
143
|
+
const ccx = a.x + a.w / 2;
|
|
144
|
+
icon(m.ctx, 'prev', ccx - 54, py + 32, 22, t.text, false, 1.6);
|
|
145
|
+
circle(m, ccx, py + 32, 22, t.accent);
|
|
146
|
+
icon(m.ctx, 'pause', ccx, py + 32, 18, t.onAccent, false);
|
|
147
|
+
icon(m.ctx, 'next', ccx + 54, py + 32, 22, t.text, false, 1.6);
|
|
148
|
+
const wx = ccx + 100;
|
|
149
|
+
const ww = Math.max(0, a.x + a.w - 40 - wx);
|
|
150
|
+
if (ww > 60) waveform(m, wx, py + 32, ww, 0.42, t.accent, t.bar3);
|
|
151
|
+
},
|
|
152
|
+
};
|