capture-flow 0.0.0-stage → 0.1.1
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 +256 -2
- package/bin/capture-flow.mjs +12 -0
- package/package.json +56 -4
- package/skills/capture-flow/SKILL.md +248 -0
- package/skills/capture-flow/references/capture-recipes.md +155 -0
- package/skills/capture-flow/references/store-sizes.md +91 -0
- 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,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MacBook Pro seen straight on: aluminium-rimmed glass lid with a notch,
|
|
3
|
+
* a glimpse of the hinge, and the thin front slab of the base with its
|
|
4
|
+
* thumb scoop. The base is the widest part and spans the whole geometry.
|
|
5
|
+
*/
|
|
6
|
+
import type { Ctx2D } from '../types';
|
|
7
|
+
import { darken, lighten, mix } from '../color';
|
|
8
|
+
import type { MacLayout } from './geometry';
|
|
9
|
+
import type { FramePaint } from './finish';
|
|
10
|
+
import { roundRectPath } from './paths';
|
|
11
|
+
import type { ScreenContent } from './screen';
|
|
12
|
+
import { paintScreen } from './screen';
|
|
13
|
+
import { paintBand, paintInnerShadow, paintLens } from './slab';
|
|
14
|
+
|
|
15
|
+
function lidPath(ctx: Ctx2D, L: MacLayout, inset: number): void {
|
|
16
|
+
const { lid } = L;
|
|
17
|
+
const t = Math.max(0, lid.rTop - inset);
|
|
18
|
+
const b = Math.max(0, lid.rBottom - inset);
|
|
19
|
+
roundRectPath(ctx, lid.x + inset, lid.y + inset, lid.w - 2 * inset, lid.h - 2 * inset, [t, t, b, b]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Front slab of the base: tiny top corners, soft elliptical bottom corners where the case curves under. */
|
|
23
|
+
export function basePath(ctx: Ctx2D, L: MacLayout): void {
|
|
24
|
+
const { x, y, w, h } = L.base;
|
|
25
|
+
if (!(w > 0) || !(h > 0)) return;
|
|
26
|
+
const rt = Math.min(h * 0.22, w / 4);
|
|
27
|
+
const rx = Math.min(w * 0.034, w / 4);
|
|
28
|
+
const ry = Math.min(h * 0.78, h - rt);
|
|
29
|
+
const k = 0.5523; // cubic circle constant
|
|
30
|
+
ctx.moveTo(x + rt, y);
|
|
31
|
+
ctx.lineTo(x + w - rt, y);
|
|
32
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + rt);
|
|
33
|
+
ctx.lineTo(x + w, y + h - ry);
|
|
34
|
+
ctx.bezierCurveTo(x + w, y + h - ry * (1 - k), x + w - rx * (1 - k), y + h, x + w - rx, y + h);
|
|
35
|
+
ctx.lineTo(x + rx, y + h);
|
|
36
|
+
ctx.bezierCurveTo(x + rx * (1 - k), y + h, x, y + h - ry * (1 - k), x, y + h - ry);
|
|
37
|
+
ctx.lineTo(x, y + rt);
|
|
38
|
+
ctx.quadraticCurveTo(x, y, x + rt, y);
|
|
39
|
+
ctx.closePath();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Lid and base outlines, for the drop shadow. */
|
|
43
|
+
export function macBodyPath(ctx: Ctx2D, L: MacLayout): void {
|
|
44
|
+
lidPath(ctx, L, 0);
|
|
45
|
+
basePath(ctx, L);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function paintBase(ctx: Ctx2D, L: MacLayout, paint: FramePaint, px: number): void {
|
|
49
|
+
const { x, y, w, h } = L.base;
|
|
50
|
+
const c = paint.base;
|
|
51
|
+
const g = ctx.createLinearGradient(0, y, 0, y + h);
|
|
52
|
+
if (paint.finish === 'clay') {
|
|
53
|
+
g.addColorStop(0, lighten(c, 0.05));
|
|
54
|
+
g.addColorStop(1, darken(c, 0.12));
|
|
55
|
+
} else if (paint.finish === 'matte') {
|
|
56
|
+
g.addColorStop(0, lighten(c, 0.16));
|
|
57
|
+
g.addColorStop(0.5, c);
|
|
58
|
+
g.addColorStop(1, darken(c, 0.28));
|
|
59
|
+
} else {
|
|
60
|
+
g.addColorStop(0, lighten(c, 0.5));
|
|
61
|
+
g.addColorStop(0.1, lighten(c, 0.2));
|
|
62
|
+
g.addColorStop(0.45, c);
|
|
63
|
+
g.addColorStop(0.82, darken(c, 0.2));
|
|
64
|
+
g.addColorStop(1, darken(c, 0.45));
|
|
65
|
+
}
|
|
66
|
+
ctx.beginPath();
|
|
67
|
+
basePath(ctx, L);
|
|
68
|
+
ctx.fillStyle = g;
|
|
69
|
+
ctx.fill();
|
|
70
|
+
|
|
71
|
+
if (paint.finish !== 'clay') {
|
|
72
|
+
// Brushed aluminium falls off towards the ends and brightens in the middle.
|
|
73
|
+
const hg = ctx.createLinearGradient(x, 0, x + w, 0);
|
|
74
|
+
hg.addColorStop(0, 'rgba(0, 0, 0, 0.2)');
|
|
75
|
+
hg.addColorStop(0.07, 'rgba(0, 0, 0, 0)');
|
|
76
|
+
hg.addColorStop(0.5, `rgba(255, 255, 255, ${paint.finish === 'metal' ? 0.1 : 0.05})`);
|
|
77
|
+
hg.addColorStop(0.93, 'rgba(0, 0, 0, 0)');
|
|
78
|
+
hg.addColorStop(1, 'rgba(0, 0, 0, 0.2)');
|
|
79
|
+
ctx.beginPath();
|
|
80
|
+
basePath(ctx, L);
|
|
81
|
+
ctx.fillStyle = hg;
|
|
82
|
+
ctx.fill();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Thumb scoop in the top edge.
|
|
86
|
+
const nw = w * 0.13;
|
|
87
|
+
const nh = h * 0.42;
|
|
88
|
+
const nx = x + (w - nw) / 2;
|
|
89
|
+
const sg = ctx.createLinearGradient(0, y, 0, y + nh);
|
|
90
|
+
sg.addColorStop(0, darken(c, paint.finish === 'clay' ? 0.16 : 0.38));
|
|
91
|
+
sg.addColorStop(1, darken(c, paint.finish === 'clay' ? 0.05 : 0.1));
|
|
92
|
+
ctx.beginPath();
|
|
93
|
+
ctx.moveTo(nx, y);
|
|
94
|
+
ctx.bezierCurveTo(nx + nh * 0.9, y, nx + nh * 0.55, y + nh, nx + nh * 1.9, y + nh);
|
|
95
|
+
ctx.lineTo(nx + nw - nh * 1.9, y + nh);
|
|
96
|
+
ctx.bezierCurveTo(nx + nw - nh * 0.55, y + nh, nx + nw - nh * 0.9, y, nx + nw, y);
|
|
97
|
+
ctx.closePath();
|
|
98
|
+
ctx.fillStyle = sg;
|
|
99
|
+
ctx.fill();
|
|
100
|
+
|
|
101
|
+
if (paint.finish === 'metal') {
|
|
102
|
+
// Chamfered top edge catching the light.
|
|
103
|
+
const hair = Math.min(1 / px, h * 0.08);
|
|
104
|
+
ctx.fillStyle = lighten(c, 0.72);
|
|
105
|
+
ctx.fillRect(x + w * 0.004, y, w * 0.992, hair);
|
|
106
|
+
}
|
|
107
|
+
// Underside in shadow, with the case curving away from view.
|
|
108
|
+
const ug = ctx.createLinearGradient(0, y + h * 0.82, 0, y + h);
|
|
109
|
+
ug.addColorStop(0, 'rgba(0, 0, 0, 0)');
|
|
110
|
+
ug.addColorStop(1, `rgba(0, 0, 0, ${paint.finish === 'clay' ? 0.12 : 0.28})`);
|
|
111
|
+
ctx.beginPath();
|
|
112
|
+
basePath(ctx, L);
|
|
113
|
+
ctx.fillStyle = ug;
|
|
114
|
+
ctx.fill();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function paintHinge(ctx: Ctx2D, L: MacLayout, paint: FramePaint): void {
|
|
118
|
+
const { x, y, w, h } = L.hinge;
|
|
119
|
+
const g = ctx.createLinearGradient(0, y, 0, y + h);
|
|
120
|
+
if (paint.finish === 'clay') {
|
|
121
|
+
g.addColorStop(0, darken(paint.base, 0.35));
|
|
122
|
+
g.addColorStop(1, darken(paint.base, 0.2));
|
|
123
|
+
} else {
|
|
124
|
+
g.addColorStop(0, '#0b0b0d');
|
|
125
|
+
g.addColorStop(0.55, mix('#26262a', paint.base, 0.15));
|
|
126
|
+
g.addColorStop(1, '#0e0e10');
|
|
127
|
+
}
|
|
128
|
+
ctx.beginPath();
|
|
129
|
+
roundRectPath(ctx, x, y, w, h, [0, 0, h * 0.5, h * 0.5]);
|
|
130
|
+
ctx.fillStyle = g;
|
|
131
|
+
ctx.fill();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function paintNotch(ctx: Ctx2D, L: MacLayout, paint: FramePaint, px: number): void {
|
|
135
|
+
const s = L.screen;
|
|
136
|
+
const nw = L.notch.w;
|
|
137
|
+
const nh = L.notch.h;
|
|
138
|
+
if (!(nw > 0) || !(nh > 0)) return;
|
|
139
|
+
const x = s.x + (s.w - nw) / 2;
|
|
140
|
+
const y = s.y - Math.min(1 / px, (s.y - L.rim) * 0.5); // overlap the bezel so no seam shows
|
|
141
|
+
const r = nh * 0.36;
|
|
142
|
+
const f = Math.min(nh * 0.24, nw * 0.1);
|
|
143
|
+
const bottom = s.y + nh;
|
|
144
|
+
ctx.beginPath();
|
|
145
|
+
ctx.moveTo(x - f, y);
|
|
146
|
+
ctx.quadraticCurveTo(x, y, x, y + f);
|
|
147
|
+
ctx.lineTo(x, bottom - r);
|
|
148
|
+
ctx.quadraticCurveTo(x, bottom, x + r, bottom);
|
|
149
|
+
ctx.lineTo(x + nw - r, bottom);
|
|
150
|
+
ctx.quadraticCurveTo(x + nw, bottom, x + nw, bottom - r);
|
|
151
|
+
ctx.lineTo(x + nw, y + f);
|
|
152
|
+
ctx.quadraticCurveTo(x + nw, y, x + nw + f, y);
|
|
153
|
+
ctx.closePath();
|
|
154
|
+
ctx.fillStyle = paint.bezel;
|
|
155
|
+
ctx.fill();
|
|
156
|
+
paintLens(ctx, s.x + s.w / 2, s.y + nh * 0.46, nh * 0.17);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Paints the whole laptop with its top-left at the current origin. */
|
|
160
|
+
export function paintMacbook(ctx: Ctx2D, L: MacLayout, paint: FramePaint, px: number, content: ScreenContent): void {
|
|
161
|
+
paintHinge(ctx, L, paint);
|
|
162
|
+
paintBase(ctx, L, paint, px);
|
|
163
|
+
|
|
164
|
+
const lidBox = { x: L.lid.x, y: L.lid.y, w: L.lid.w, h: L.lid.h };
|
|
165
|
+
paintBand(ctx, (inset) => lidPath(ctx, L, inset), L.rim, paint, px, lidBox);
|
|
166
|
+
// Black glass around the display, as a ring that tucks one pixel under the screen.
|
|
167
|
+
const s = L.screen;
|
|
168
|
+
const o = Math.min(1 / px, s.w * 0.01, s.h * 0.01);
|
|
169
|
+
ctx.beginPath();
|
|
170
|
+
lidPath(ctx, L, L.rim);
|
|
171
|
+
roundRectPath(ctx, s.x + o, s.y + o, s.w - 2 * o, s.h - 2 * o, [s.radius, s.radius, 0, 0]);
|
|
172
|
+
ctx.fillStyle = paint.bezel;
|
|
173
|
+
ctx.fill('evenodd');
|
|
174
|
+
if (!paint.bezelMatchesBody) {
|
|
175
|
+
const hair = Math.min(1 / px, L.rim);
|
|
176
|
+
ctx.lineWidth = hair;
|
|
177
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
|
|
178
|
+
ctx.beginPath();
|
|
179
|
+
lidPath(ctx, L, L.rim + hair * 0.5);
|
|
180
|
+
ctx.stroke();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const screenPath = (c: Ctx2D) => roundRectPath(c, s.x, s.y, s.w, s.h, [s.radius, s.radius, 0, 0]);
|
|
184
|
+
paintScreen(ctx, s, screenPath, content);
|
|
185
|
+
if (paint.bezelMatchesBody) paintInnerShadow(ctx, s, screenPath, L.lid.w * 0.008 * px, L.lid.w * 0.0015 * px, 0.18);
|
|
186
|
+
paintNotch(ctx, L, paint, px);
|
|
187
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** Calendar / agenda: week strip, hourly timeline with coloured events, now indicator. */
|
|
2
|
+
import { darken, mix, rgba } from '../../../color';
|
|
3
|
+
import { avatarStack } from '../art';
|
|
4
|
+
import { icon } from '../icons';
|
|
5
|
+
import type { Area, Mk } from '../kit';
|
|
6
|
+
import { bar, box, card, circle, num, title } from '../kit';
|
|
7
|
+
import { tint } from '../theme';
|
|
8
|
+
import type { MockApp } from '../widgets';
|
|
9
|
+
import { iconButton } from '../widgets';
|
|
10
|
+
|
|
11
|
+
interface Event {
|
|
12
|
+
start: number;
|
|
13
|
+
end: number;
|
|
14
|
+
color: number;
|
|
15
|
+
people: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const EVENTS: readonly Event[] = [
|
|
19
|
+
{ start: 9, end: 10, color: 0, people: 3 },
|
|
20
|
+
{ start: 10.5, end: 12, color: 1, people: 0 },
|
|
21
|
+
{ start: 13, end: 13.75, color: 2, people: 2 },
|
|
22
|
+
{ start: 14.5, end: 16, color: 3, people: 4 },
|
|
23
|
+
{ start: 16.5, end: 17.5, color: 0, people: 0 },
|
|
24
|
+
{ start: 18.5, end: 20, color: 2, people: 2 },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const hourLabel = (h: number) => {
|
|
28
|
+
const total = Math.round(h * 60);
|
|
29
|
+
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}`;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function eventBlock(m: Mk, x: number, y: number, w: number, h: number, ev: Event, k: number): void {
|
|
33
|
+
const t = m.t;
|
|
34
|
+
const c = t.accents[ev.color % 4];
|
|
35
|
+
box(m, x, y, w, h, 14, tint(t, c, t.dark ? 0.76 : 0.85));
|
|
36
|
+
box(m, x + 6, y + 8, 4, Math.max(4, h - 16), 2, c);
|
|
37
|
+
const ink = t.dark ? mix(c, '#ffffff', 0.35) : darken(c, 0.35);
|
|
38
|
+
bar(m, x + 20, y + 13, Math.min(w - 40, 100 + ((k * 23) % 60)), 7.5, ink);
|
|
39
|
+
if (h > 46) num(m, `${hourLabel(ev.start)} - ${hourLabel(ev.end)}`, x + 20, y + 41, 12, 600, rgba(ink, 0.75));
|
|
40
|
+
if (ev.people > 0 && h > 60 && w > 160) avatarStack(m, x + w - 16 - 24 * (1 + (ev.people - 1) * 0.66), y + h - 24, 24, ev.people, m.seed + k * 5, tint(t, c, t.dark ? 0.76 : 0.85));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function weekStrip(m: Mk, x: number, y: number, w: number, first: number, selected: number): void {
|
|
44
|
+
const t = m.t;
|
|
45
|
+
const step = w / 7;
|
|
46
|
+
for (let i = 0; i < 7; i++) {
|
|
47
|
+
const cx = x + step * (i + 0.5);
|
|
48
|
+
const on = i === selected;
|
|
49
|
+
if (on) box(m, cx - Math.min(23, step * 0.42), y, Math.min(46, step * 0.84), 70, 18, t.accent);
|
|
50
|
+
bar(m, cx - 6, y + 14, 12, 5, on ? rgba(t.onAccent, 0.75) : t.bar3);
|
|
51
|
+
num(m, String(first + i), cx, y + 48, 18, 700, on ? t.onAccent : t.text, 'center');
|
|
52
|
+
if (i % 3 !== 2 && !on) circle(m, cx, y + 62, 2.6, t.accents[i % 4]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function timeline(m: Mk, a: Area, top: number, startHour: number, hourH: number, labelW: number): void {
|
|
57
|
+
const t = m.t;
|
|
58
|
+
const ctx = m.ctx;
|
|
59
|
+
const hours = Math.ceil((a.y + a.h - top) / hourH) + 1;
|
|
60
|
+
for (let i = 0; i < hours; i++) {
|
|
61
|
+
const hy = top + i * hourH;
|
|
62
|
+
num(m, `${startHour + i}:00`, a.x + labelW - 10, hy + 4, 11.5, 500, t.text2, 'right');
|
|
63
|
+
ctx.fillStyle = t.line;
|
|
64
|
+
ctx.fillRect(a.x + labelW, hy, a.w - labelW, 1);
|
|
65
|
+
}
|
|
66
|
+
EVENTS.forEach((ev, k) => {
|
|
67
|
+
const y = top + (ev.start - startHour) * hourH + 2;
|
|
68
|
+
const h = (ev.end - ev.start) * hourH - 4;
|
|
69
|
+
if (y > a.y + a.h + 40) return;
|
|
70
|
+
eventBlock(m, a.x + labelW + 6, y, a.w - labelW - 6, h, ev, k);
|
|
71
|
+
});
|
|
72
|
+
const ny = top + (11.3 - startHour) * hourH;
|
|
73
|
+
ctx.fillStyle = t.bad;
|
|
74
|
+
ctx.fillRect(a.x + labelW, ny - 1, a.w - labelW, 2);
|
|
75
|
+
circle(m, a.x + labelW, ny, 5, t.bad);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const calendar: MockApp = {
|
|
79
|
+
id: 'calendar',
|
|
80
|
+
nav: ['calendar', 'list', 'clock', 'users', 'inbox', 'settings'],
|
|
81
|
+
glyph: 'calendar',
|
|
82
|
+
tabs: ['calendar', 'list', 'users', 'settings'],
|
|
83
|
+
tabSelected: 0,
|
|
84
|
+
narrow(m, a) {
|
|
85
|
+
const t = m.t;
|
|
86
|
+
const M = 20;
|
|
87
|
+
const x = a.x + M;
|
|
88
|
+
const w = a.w - 2 * M;
|
|
89
|
+
let y = a.y + 8;
|
|
90
|
+
num(m, '2026', x, y + 14, 14, 600, t.accent);
|
|
91
|
+
title(m, x, y + 20, 136, 28);
|
|
92
|
+
iconButton(m, x + w - 73, y + 26, 42, 'search');
|
|
93
|
+
circle(m, x + w - 21, y + 26, 21, t.accent);
|
|
94
|
+
icon(m.ctx, 'plus', x + w - 21, y + 26, 20, t.onAccent, false, 2.6);
|
|
95
|
+
y += 72;
|
|
96
|
+
const first = 12 + (m.seed % 9);
|
|
97
|
+
weekStrip(m, x, y, w, first, 2);
|
|
98
|
+
y += 86;
|
|
99
|
+
card(m, x - 6, y, w + 12, a.y + a.h - y + 60, 26);
|
|
100
|
+
timeline(m, { x: x + 4, y: a.y, w: w - 8, h: a.h }, y + 24, 9, 74, 52);
|
|
101
|
+
},
|
|
102
|
+
wide(m, a) {
|
|
103
|
+
const t = m.t;
|
|
104
|
+
const ctx = m.ctx;
|
|
105
|
+
card(m, a.x, a.y, a.w, a.h + 40, 24);
|
|
106
|
+
const labelW = 64;
|
|
107
|
+
const top = a.y + 78;
|
|
108
|
+
const cols = 7;
|
|
109
|
+
const cw = (a.w - labelW - 16) / cols;
|
|
110
|
+
const first = 12 + (m.seed % 9);
|
|
111
|
+
for (let i = 0; i < cols; i++) {
|
|
112
|
+
const cx = a.x + labelW + cw * (i + 0.5);
|
|
113
|
+
const on = i === 2;
|
|
114
|
+
bar(m, cx - 10, a.y + 18, 20, 5, t.bar3);
|
|
115
|
+
if (on) circle(m, cx, a.y + 46, 18, t.accent);
|
|
116
|
+
num(m, String(first + i), cx, a.y + 52, 17, 700, on ? t.onAccent : t.text, 'center');
|
|
117
|
+
if (i > 0) {
|
|
118
|
+
ctx.fillStyle = t.line;
|
|
119
|
+
ctx.fillRect(a.x + labelW + cw * i, top, 1, a.h);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const hourH = 64;
|
|
123
|
+
const hours = Math.ceil((a.y + a.h - top) / hourH) + 1;
|
|
124
|
+
for (let i = 0; i < hours; i++) {
|
|
125
|
+
const hy = top + i * hourH;
|
|
126
|
+
num(m, `${8 + i}:00`, a.x + labelW - 12, hy + 4, 11.5, 500, t.text2, 'right');
|
|
127
|
+
ctx.fillStyle = t.line;
|
|
128
|
+
ctx.fillRect(a.x + labelW, hy, a.w - labelW - 16, 1);
|
|
129
|
+
}
|
|
130
|
+
for (let d = 0; d < cols; d++) {
|
|
131
|
+
for (let j = 0; j < 2 + ((d * 5 + m.seed) % 2); j++) {
|
|
132
|
+
const ev = EVENTS[(d * 2 + j + m.seed) % EVENTS.length];
|
|
133
|
+
const start = 8.5 + ((d * 3 + j * 4) % 9);
|
|
134
|
+
const len = ev.end - ev.start;
|
|
135
|
+
const y = top + (start - 8) * hourH + 2;
|
|
136
|
+
if (y > a.y + a.h) continue;
|
|
137
|
+
eventBlock(m, a.x + labelW + cw * d + 4, y, cw - 8, len * hourH - 4, { ...ev, start, end: start + len }, d * 3 + j);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const ny = top + 3.3 * hourH;
|
|
141
|
+
ctx.fillStyle = t.bad;
|
|
142
|
+
ctx.fillRect(a.x + labelW, ny - 1, a.w - labelW - 16, 2);
|
|
143
|
+
circle(m, a.x + labelW + cw * 2, ny, 5, t.bad);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/** Messaging: a conversation laid out bottom-up like a real chat, with photo and voice bubbles. */
|
|
2
|
+
import { mix, rgba } from '../../../color';
|
|
3
|
+
import { avatar, photo } from '../art';
|
|
4
|
+
import { icon } from '../icons';
|
|
5
|
+
import type { Area, Mk } from '../kit';
|
|
6
|
+
import { bar, body, box, card, circle, linear, num, title } from '../kit';
|
|
7
|
+
import type { MockApp } from '../widgets';
|
|
8
|
+
|
|
9
|
+
type Msg =
|
|
10
|
+
| { kind: 'text'; out: boolean; lines: number[]; time?: string }
|
|
11
|
+
| { kind: 'image'; out: boolean }
|
|
12
|
+
| { kind: 'voice'; out: boolean }
|
|
13
|
+
| { kind: 'typing' }
|
|
14
|
+
| { kind: 'date' };
|
|
15
|
+
|
|
16
|
+
const THREAD: readonly Msg[] = [
|
|
17
|
+
{ kind: 'date' },
|
|
18
|
+
{ kind: 'text', out: false, lines: [0.92, 0.58] },
|
|
19
|
+
{ kind: 'text', out: true, lines: [0.52] },
|
|
20
|
+
{ kind: 'text', out: true, lines: [0.96, 0.7], time: '9:38' },
|
|
21
|
+
{ kind: 'image', out: false },
|
|
22
|
+
{ kind: 'text', out: false, lines: [0.48] },
|
|
23
|
+
{ kind: 'voice', out: true },
|
|
24
|
+
{ kind: 'text', out: false, lines: [1, 0.86, 0.42], time: '9:40' },
|
|
25
|
+
{ kind: 'text', out: true, lines: [0.62], time: '9:41' },
|
|
26
|
+
{ kind: 'typing' },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function msgHeight(msg: Msg, maxW: number): number {
|
|
30
|
+
switch (msg.kind) {
|
|
31
|
+
case 'text':
|
|
32
|
+
return 20 + msg.lines.length * 19 + (msg.time ? 16 : 0);
|
|
33
|
+
case 'image':
|
|
34
|
+
return Math.round(maxW * 0.66);
|
|
35
|
+
case 'voice':
|
|
36
|
+
return 54;
|
|
37
|
+
case 'typing':
|
|
38
|
+
return 42;
|
|
39
|
+
case 'date':
|
|
40
|
+
return 28;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sender(msg: Msg): 'in' | 'out' | 'none' {
|
|
45
|
+
if (msg.kind === 'date') return 'none';
|
|
46
|
+
if (msg.kind === 'typing') return 'in';
|
|
47
|
+
return msg.out ? 'out' : 'in';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function bubbleFill(m: Mk, out: boolean, x: number, y: number, w: number, h: number): string | CanvasGradient {
|
|
51
|
+
const t = m.t;
|
|
52
|
+
if (out)
|
|
53
|
+
return linear(m, x, y, x + w, y + h, [
|
|
54
|
+
[0, t.accent],
|
|
55
|
+
[1, mix(t.accent, t.companion, 0.45)],
|
|
56
|
+
]);
|
|
57
|
+
return t.dark ? t.surface2 : t.surface;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function drawMsg(m: Mk, msg: Msg, a: Area, y: number, h: number, maxW: number, k: number): void {
|
|
61
|
+
const t = m.t;
|
|
62
|
+
const M = 16;
|
|
63
|
+
if (msg.kind === 'date') {
|
|
64
|
+
box(m, a.x + a.w / 2 - 44, y, 88, 24, 12, t.dark ? t.surface2 : rgba(t.text, 0.06));
|
|
65
|
+
bar(m, a.x + a.w / 2 - 24, y + 9.5, 48, 5, t.bar2);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const out = msg.kind !== 'typing' && msg.out;
|
|
69
|
+
const radii: [number, number, number, number] = out ? [20, 20, 6, 20] : [20, 20, 20, 6];
|
|
70
|
+
let w: number;
|
|
71
|
+
if (msg.kind === 'text') w = Math.max(...msg.lines) * (maxW - 28) + 28;
|
|
72
|
+
else if (msg.kind === 'image') w = maxW * 0.8;
|
|
73
|
+
else if (msg.kind === 'voice') w = Math.min(maxW, 236);
|
|
74
|
+
else w = 70;
|
|
75
|
+
const x = out ? a.x + a.w - M - w : a.x + M;
|
|
76
|
+
if (msg.kind === 'image') {
|
|
77
|
+
photo(m, x, y, w, h, radii, m.seed + k);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const fill = bubbleFill(m, out, x, y, w, h);
|
|
81
|
+
if (!out && !t.dark) card(m, x, y, w, h, radii[0], fill, 0.3);
|
|
82
|
+
box(m, x, y, w, h, radii, fill);
|
|
83
|
+
const ink = out ? rgba(t.onAccent, 0.88) : t.bar2;
|
|
84
|
+
if (msg.kind === 'text') {
|
|
85
|
+
msg.lines.forEach((f, i) => bar(m, x + 14, y + 16 + i * 19, (maxW - 28) * f, 7, ink));
|
|
86
|
+
if (msg.time) {
|
|
87
|
+
num(m, msg.time, x + w - 14 - (out ? 18 : 0), y + h - 9, 11, 500, out ? rgba(t.onAccent, 0.7) : t.text2, 'right');
|
|
88
|
+
if (out) {
|
|
89
|
+
icon(m.ctx, 'check', x + w - 22, y + h - 13, 12, rgba(t.onAccent, 0.8), false, 2.6);
|
|
90
|
+
icon(m.ctx, 'check', x + w - 17, y + h - 13, 12, rgba(t.onAccent, 0.8), false, 2.6);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} else if (msg.kind === 'voice') {
|
|
94
|
+
circle(m, x + 28, y + h / 2, 16, out ? 'rgba(255, 255, 255, 0.22)' : t.accent);
|
|
95
|
+
icon(m.ctx, 'play', x + 29.5, y + h / 2, 14, t.onAccent, false, 1.6);
|
|
96
|
+
const bars = 22;
|
|
97
|
+
const bx0 = x + 54;
|
|
98
|
+
const bw = (w - 54 - 52) / bars;
|
|
99
|
+
for (let i = 0; i < bars; i++) {
|
|
100
|
+
const v = 0.25 + 0.75 * Math.abs(Math.sin(i * 1.7 + k) * Math.cos(i * 0.6));
|
|
101
|
+
const bh = 4 + v * 22;
|
|
102
|
+
box(m, bx0 + i * bw, y + h / 2 - bh / 2, Math.max(1.5, bw * 0.55), bh, 2, i < 9 ? (out ? t.onAccent : t.accent) : out ? 'rgba(255, 255, 255, 0.45)' : t.bar3);
|
|
103
|
+
}
|
|
104
|
+
num(m, '0:24', x + w - 14, y + h / 2 + 4.5, 12, 600, out ? rgba(t.onAccent, 0.85) : t.text2, 'right');
|
|
105
|
+
} else {
|
|
106
|
+
for (let i = 0; i < 3; i++) circle(m, x + 22 + i * 13, y + h / 2, 4.2, rgba(t.text2, 0.4 + i * 0.2));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Lays the thread out from `bottom` upwards until `top`, cycling when the screen is tall. */
|
|
111
|
+
function thread(m: Mk, a: Area, top: number, bottom: number): void {
|
|
112
|
+
const maxW = Math.min(a.w * 0.74, 360);
|
|
113
|
+
let y = bottom;
|
|
114
|
+
let i = THREAD.length - 1;
|
|
115
|
+
let prev: Msg | null = null;
|
|
116
|
+
for (let n = 0; n < 80 && y > top; n++) {
|
|
117
|
+
const msg = THREAD[((i % THREAD.length) + THREAD.length) % THREAD.length];
|
|
118
|
+
const h = msgHeight(msg, maxW);
|
|
119
|
+
const gap = prev && sender(prev) === sender(msg) ? 6 : 14;
|
|
120
|
+
y -= h + (prev ? gap : 0);
|
|
121
|
+
drawMsg(m, msg, a, y, h, maxW, n);
|
|
122
|
+
prev = msg;
|
|
123
|
+
i--;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function inputBar(m: Mk, x: number, y: number, w: number): void {
|
|
128
|
+
const t = m.t;
|
|
129
|
+
circle(m, x + 20, y + 22, 20, t.dark ? t.surface2 : t.surface);
|
|
130
|
+
icon(m.ctx, 'plus', x + 20, y + 22, 20, t.text2, false, 2.3);
|
|
131
|
+
const fx = x + 50;
|
|
132
|
+
const fw = w - 100;
|
|
133
|
+
if (!t.dark) card(m, fx, y, fw, 44, 22, t.surface, 0.35);
|
|
134
|
+
else box(m, fx, y, fw, 44, 22, t.surface2);
|
|
135
|
+
bar(m, fx + 18, y + 19, fw * 0.42, 6, t.bar3);
|
|
136
|
+
icon(m.ctx, 'camera', fx + fw - 22, y + 22, 20, t.text2, false, 2);
|
|
137
|
+
circle(m, x + w - 22, y + 22, 22, t.accent);
|
|
138
|
+
icon(m.ctx, 'mic', x + w - 22, y + 22, 20, t.onAccent, false, 2.3);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function header(m: Mk, x: number, y: number, w: number): void {
|
|
142
|
+
const t = m.t;
|
|
143
|
+
icon(m.ctx, 'chevronLeft', x + 8, y + 22, 24, t.accent, false, 2.6);
|
|
144
|
+
avatar(m, x + 52, y + 22, 42, m.seed + 21);
|
|
145
|
+
circle(m, x + 67, y + 37, 6.5, t.dark ? t.bg : t.surface);
|
|
146
|
+
circle(m, x + 67, y + 37, 4.5, t.good);
|
|
147
|
+
title(m, x + 82, y + 4, 112, 16);
|
|
148
|
+
bar(m, x + 82, y + 30, 46, 5.5, t.good);
|
|
149
|
+
icon(m.ctx, 'video', x + w - 52, y + 22, 24, t.accent, false, 2);
|
|
150
|
+
icon(m.ctx, 'phone', x + w - 10, y + 22, 22, t.accent, false, 2);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const chat: MockApp = {
|
|
154
|
+
id: 'chat',
|
|
155
|
+
nav: ['chat', 'users', 'phone', 'bookmark', 'folder', 'settings'],
|
|
156
|
+
glyph: 'chat',
|
|
157
|
+
tabs: null,
|
|
158
|
+
tabSelected: 0,
|
|
159
|
+
narrow(m, a) {
|
|
160
|
+
const t = m.t;
|
|
161
|
+
const x = a.x + 12;
|
|
162
|
+
const w = a.w - 24;
|
|
163
|
+
const inputY = a.y + a.h - 52;
|
|
164
|
+
thread(m, a, a.y + 64, inputY - 14);
|
|
165
|
+
// Header and input sit on solid panels so the cut-off thread slides under them.
|
|
166
|
+
m.ctx.fillStyle = linear(m, 0, a.y - 60, 0, a.y + 76, [
|
|
167
|
+
[0.75, t.bg],
|
|
168
|
+
[1, rgba(t.bg, 0)],
|
|
169
|
+
]);
|
|
170
|
+
m.ctx.fillRect(a.x, a.y - 60, a.w, 136);
|
|
171
|
+
header(m, x, a.y + 4, w);
|
|
172
|
+
m.ctx.fillStyle = linear(m, 0, inputY - 24, 0, inputY + 6, [
|
|
173
|
+
[0, rgba(t.bg, 0)],
|
|
174
|
+
[1, t.bg],
|
|
175
|
+
]);
|
|
176
|
+
m.ctx.fillRect(a.x, inputY - 24, a.w, a.y + a.h - inputY + 60);
|
|
177
|
+
inputBar(m, x, inputY, w);
|
|
178
|
+
},
|
|
179
|
+
wide(m, a) {
|
|
180
|
+
const t = m.t;
|
|
181
|
+
const lw = Math.min(330, a.w * 0.34);
|
|
182
|
+
card(m, a.x, a.y, lw, a.h + 40, 22);
|
|
183
|
+
box(m, a.x + 16, a.y + 16, lw - 32, 38, 12, t.surface2);
|
|
184
|
+
icon(m.ctx, 'search', a.x + 36, a.y + 35, 16, t.text2, false, 2.2);
|
|
185
|
+
bar(m, a.x + 52, a.y + 32, 70, 5.5, t.bar3);
|
|
186
|
+
for (let i = 0, y = a.y + 70; y < a.y + a.h; i++, y += 72) {
|
|
187
|
+
const on = i === 1;
|
|
188
|
+
if (on) box(m, a.x + 8, y - 6, lw - 16, 68, 16, t.dark ? rgba(t.accent, 0.16) : rgba(t.accent, 0.08));
|
|
189
|
+
avatar(m, a.x + 40, y + 28, 48, m.seed + 20 + i);
|
|
190
|
+
title(m, a.x + 74, y + 8, 90 + (i % 3) * 18, 15);
|
|
191
|
+
body(m, a.x + 74, y + 30, lw - 140, 12);
|
|
192
|
+
num(m, `${9 - (i % 6)}:${String((i * 17) % 60).padStart(2, '0')}`, a.x + lw - 18, y + 22, 11.5, 500, t.text2, 'right');
|
|
193
|
+
if (i % 3 === 0) {
|
|
194
|
+
circle(m, a.x + lw - 30, y + 38, 10, t.accent);
|
|
195
|
+
num(m, String(1 + (i % 4)), a.x + lw - 30, y + 42, 11, 700, t.onAccent, 'center');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const tx = a.x + lw + 20;
|
|
199
|
+
const tw = a.w - lw - 20;
|
|
200
|
+
card(m, tx, a.y, tw, a.h + 40, 22);
|
|
201
|
+
header(m, tx + 16, a.y + 14, tw - 32);
|
|
202
|
+
m.ctx.fillStyle = t.line;
|
|
203
|
+
m.ctx.fillRect(tx, a.y + 72, tw, 1);
|
|
204
|
+
const ta: Area = { x: tx + 8, y: a.y, w: tw - 16, h: a.h };
|
|
205
|
+
m.ctx.save();
|
|
206
|
+
m.ctx.beginPath();
|
|
207
|
+
m.ctx.rect(tx, a.y + 73, tw, a.h - 73);
|
|
208
|
+
m.ctx.clip();
|
|
209
|
+
thread(m, ta, a.y + 73, a.y + a.h - 72);
|
|
210
|
+
m.ctx.restore();
|
|
211
|
+
inputBar(m, tx + 16, a.y + a.h - 58, tw - 32);
|
|
212
|
+
},
|
|
213
|
+
};
|