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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +238 -2
  3. package/bin/capture-flow.mjs +12 -0
  4. package/package.json +55 -4
  5. package/src/build.ts +186 -0
  6. package/src/capture.ts +77 -0
  7. package/src/cli.ts +139 -0
  8. package/src/index.ts +11 -0
  9. package/src/manifest.ts +98 -0
  10. package/src/render.ts +75 -0
  11. package/src/verify.ts +90 -0
  12. package/vendor/wajha/LICENSE +21 -0
  13. package/vendor/wajha/NOTICE +21 -0
  14. package/vendor/wajha/engine/background.ts +55 -0
  15. package/vendor/wajha/engine/bidi.ts +589 -0
  16. package/vendor/wajha/engine/cache.ts +96 -0
  17. package/vendor/wajha/engine/color.ts +125 -0
  18. package/vendor/wajha/engine/devices/android.ts +53 -0
  19. package/vendor/wajha/engine/devices/browser.ts +170 -0
  20. package/vendor/wajha/engine/devices/finish.ts +154 -0
  21. package/vendor/wajha/engine/devices/geometry.ts +326 -0
  22. package/vendor/wajha/engine/devices/index.ts +143 -0
  23. package/vendor/wajha/engine/devices/ipad.ts +21 -0
  24. package/vendor/wajha/engine/devices/iphone.ts +55 -0
  25. package/vendor/wajha/engine/devices/macbook.ts +187 -0
  26. package/vendor/wajha/engine/devices/mock/apps/calendar.ts +145 -0
  27. package/vendor/wajha/engine/devices/mock/apps/chat.ts +213 -0
  28. package/vendor/wajha/engine/devices/mock/apps/finance.ts +164 -0
  29. package/vendor/wajha/engine/devices/mock/apps/fitness.ts +145 -0
  30. package/vendor/wajha/engine/devices/mock/apps/index.ts +14 -0
  31. package/vendor/wajha/engine/devices/mock/apps/meditation.ts +167 -0
  32. package/vendor/wajha/engine/devices/mock/apps/music.ts +152 -0
  33. package/vendor/wajha/engine/devices/mock/apps/shop.ts +116 -0
  34. package/vendor/wajha/engine/devices/mock/apps/social.ts +152 -0
  35. package/vendor/wajha/engine/devices/mock/apps/tasks.ts +131 -0
  36. package/vendor/wajha/engine/devices/mock/apps/travel.ts +121 -0
  37. package/vendor/wajha/engine/devices/mock/art.ts +557 -0
  38. package/vendor/wajha/engine/devices/mock/chrome.ts +337 -0
  39. package/vendor/wajha/engine/devices/mock/icons.ts +614 -0
  40. package/vendor/wajha/engine/devices/mock/kit.ts +213 -0
  41. package/vendor/wajha/engine/devices/mock/rng.ts +40 -0
  42. package/vendor/wajha/engine/devices/mock/theme.ts +133 -0
  43. package/vendor/wajha/engine/devices/mock/widgets.ts +231 -0
  44. package/vendor/wajha/engine/devices/paths.ts +113 -0
  45. package/vendor/wajha/engine/devices/screen.ts +75 -0
  46. package/vendor/wajha/engine/devices/shadow.ts +124 -0
  47. package/vendor/wajha/engine/devices/slab.ts +200 -0
  48. package/vendor/wajha/engine/devices/vision.ts +43 -0
  49. package/vendor/wajha/engine/devices/watch.ts +14 -0
  50. package/vendor/wajha/engine/geometry.ts +152 -0
  51. package/vendor/wajha/engine/images.ts +96 -0
  52. package/vendor/wajha/engine/index.ts +20 -0
  53. package/vendor/wajha/engine/linebreak.ts +226 -0
  54. package/vendor/wajha/engine/markup.ts +84 -0
  55. package/vendor/wajha/engine/mock.ts +158 -0
  56. package/vendor/wajha/engine/paint.ts +474 -0
  57. package/vendor/wajha/engine/patterns.ts +263 -0
  58. package/vendor/wajha/engine/render.ts +364 -0
  59. package/vendor/wajha/engine/shapes.ts +260 -0
  60. package/vendor/wajha/engine/testing/canvasNode.ts +121 -0
  61. package/vendor/wajha/engine/testing/fakeDevices.ts +85 -0
  62. package/vendor/wajha/engine/text.ts +694 -0
  63. package/vendor/wajha/engine/types.ts +190 -0
  64. package/vendor/wajha/export/paths.ts +197 -0
  65. package/vendor/wajha/export/png.ts +223 -0
  66. package/vendor/wajha/export/types.ts +60 -0
  67. package/vendor/wajha/model/adapt.ts +889 -0
  68. package/vendor/wajha/model/catalog.ts +369 -0
  69. package/vendor/wajha/model/clone.ts +58 -0
  70. package/vendor/wajha/model/colorMath.ts +128 -0
  71. package/vendor/wajha/model/defaults.ts +386 -0
  72. package/vendor/wajha/model/geometry.ts +95 -0
  73. package/vendor/wajha/model/ids.ts +13 -0
  74. package/vendor/wajha/model/instantiate.ts +280 -0
  75. package/vendor/wajha/model/mirror.ts +43 -0
  76. package/vendor/wajha/model/resolve.ts +87 -0
  77. package/vendor/wajha/model/rules.ts +249 -0
  78. package/vendor/wajha/model/stub.ts +4 -0
  79. package/vendor/wajha/model/types.ts +456 -0
  80. package/vendor/wajha/model/typography.ts +28 -0
  81. package/vendor/wajha/model/validate.ts +917 -0
  82. package/vendor/wajha/templates/builtin/aurora.ts +163 -0
  83. package/vendor/wajha/templates/builtin/bubblegum.ts +132 -0
  84. package/vendor/wajha/templates/builtin/citrusPop.ts +124 -0
  85. package/vendor/wajha/templates/builtin/clayGarden.ts +114 -0
  86. package/vendor/wajha/templates/builtin/desertDune.ts +97 -0
  87. package/vendor/wajha/templates/builtin/glassPrism.ts +139 -0
  88. package/vendor/wajha/templates/builtin/launchpad.ts +140 -0
  89. package/vendor/wajha/templates/builtin/loudAndClear.ts +109 -0
  90. package/vendor/wajha/templates/builtin/midnightNeon.ts +108 -0
  91. package/vendor/wajha/templates/builtin/monoSwiss.ts +149 -0
  92. package/vendor/wajha/templates/builtin/noirGold.ts +149 -0
  93. package/vendor/wajha/templates/builtin/noor.ts +107 -0
  94. package/vendor/wajha/templates/builtin/obsidian.ts +137 -0
  95. package/vendor/wajha/templates/builtin/panoramaPeaks.ts +145 -0
  96. package/vendor/wajha/templates/builtin/panoramaRibbon.ts +138 -0
  97. package/vendor/wajha/templates/builtin/panoramaWaves.ts +138 -0
  98. package/vendor/wajha/templates/builtin/paperAndInk.ts +98 -0
  99. package/vendor/wajha/templates/builtin/sage.ts +94 -0
  100. package/vendor/wajha/templates/builtin/splitTone.ts +129 -0
  101. package/vendor/wajha/templates/builtin/sunsetBoulevard.ts +127 -0
  102. package/vendor/wajha/templates/index.ts +55 -0
  103. package/vendor/wajha/templates/kit.ts +761 -0
  104. package/vendor/wajha/ui/sortShots.ts +239 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Noir Gold: a boutique after closing time. Warm black with a pool of
3
+ * light, Playfair Display in ivory with gold highlights, a spaced gold
4
+ * eyebrow, hairline rings and gold devices on every canvas. Story: a
5
+ * luxury watch and jewellery boutique.
6
+ */
7
+ import type { SlideElement } from '../../model/types';
8
+ import { ALL_CLASSES, F, METAL, background, copyAt, device, r5, radial, rect, ring, slide, template, theme, typeset, type Cls, type DeviceOptions } from '../kit';
9
+
10
+ const GOLD = '#d6b46a';
11
+ const t = typeset({
12
+ heading: F.playfair,
13
+ body: F.dmSans,
14
+ ink: '#f6f0e4',
15
+ soft: '#c9c0ae',
16
+ accent: GOLD,
17
+ head: { weight: 600, size: 0.094, lineHeight: 1.14, letterSpacing: -0.01 },
18
+ sub: { weight: 400, size: 0.039, lineHeight: 1.45 },
19
+ eyebrow: { size: 0.026, letterSpacing: 0.32, weight: 600 },
20
+ });
21
+
22
+ const SHADOW = { color: '#000000b3', blur: 0.08, x: 0, y: 0.035 };
23
+ const phone = (o: DeviceOptions) => device({ color: METAL.gold, shadow: SHADOW, ...o });
24
+ const pad = (o: DeviceOptions) => device({ cls: 'tablet', frame: 'ipad', color: METAL.gold, shadow: SHADOW, ...o });
25
+ const laptop = (o: DeviceOptions) => device({ cls: 'wide', frame: 'macbook', color: METAL.gold, shadow: SHADOW, ...o });
26
+
27
+ /** Eyebrow, a short gold rule and the headline / subtitle pair. */
28
+ function copy(cls: Cls, seam?: number): SlideElement[] {
29
+ if (cls === 'wide') {
30
+ const x = 0.235;
31
+ return [
32
+ t.eyebrow({ x, y: 0.22, width: 0.5, align: 'start' }),
33
+ rect({ name: 'Gold rule', x: 0.0975, y: 0.272, width: 0.08, height: 0.003, radius: 0, color: GOLD }),
34
+ ...copyAt(t, { cls, seam: seam ?? 0.57, x: 0.229, width: 0.5, align: 'start', head: { size: 0.076, maxLines: 3 }, sub: { size: 0.032, maxLines: 3 } }),
35
+ ];
36
+ }
37
+ const tablet = cls === 'tablet';
38
+ return [
39
+ t.eyebrow({ y: tablet ? 0.05 : 0.048 }),
40
+ rect({ name: 'Gold rule', x: 0.5, y: tablet ? 0.089 : 0.071, width: 0.08, height: 0.003, radius: 0, color: GOLD }),
41
+ ...copyAt(t, tablet ? { cls, seam: seam ?? 0.2, width: 0.8, subWidth: 0.62, head: { size: 0.07 }, sub: { size: 0.031 } } : { seam: seam ?? 0.185 }),
42
+ ];
43
+ }
44
+
45
+ const halo = (x: number, y: number, s = 1): SlideElement[] => [
46
+ ring({ name: 'Gold ring', x, y, size: 1.2 * s, stroke: 0.0022, color: GOLD, opacity: 0.35 }),
47
+ ring({ name: 'Gold ring', x, y, size: 1.04 * s, stroke: 0.0022, color: GOLD, opacity: 0.22 }),
48
+ ];
49
+
50
+ function hero(cls: Cls): SlideElement[] {
51
+ if (cls === 'wide') return [...halo(0.705, 0.52, 0.8), ...copy(cls), laptop({ width: 0.88, x: 0.705, y: 0.53 })];
52
+ if (cls === 'tablet') return [...halo(0.5, 0.78, 1.05), ...copy(cls), pad({ width: 0.74, top: 0.32 })];
53
+ return [...halo(0.5, 0.64), ...copy(cls), phone({ width: 0.76, top: 0.282 })];
54
+ }
55
+
56
+ function crafted(cls: Cls): SlideElement[] {
57
+ if (cls === 'wide') {
58
+ // Mirrored: the laptop on the left, the copy in the right column.
59
+ const x = 0.77;
60
+ const left = x * 1.6 - 0.24;
61
+ return [
62
+ ...halo(0.3, 0.52, 0.85),
63
+ t.eyebrow({ x, y: 0.22, width: 0.48, align: 'start' }),
64
+ rect({ name: 'Gold rule', x: r5((left + 0.04) / 1.6), y: 0.272, width: 0.08, height: 0.003, radius: 0, color: GOLD }),
65
+ ...copyAt(t, { cls, seam: 0.57, x, width: 0.48, align: 'start', head: { size: 0.076, maxLines: 3 }, sub: { size: 0.032, maxLines: 3 } }),
66
+ laptop({ width: 0.86, x: 0.31, y: 0.53 }),
67
+ ];
68
+ }
69
+ if (cls !== 'tall') return hero(cls);
70
+ return [...halo(0.46, 0.62, 0.92), ...copy(cls), phone({ width: 0.76, top: 0.286, x: 0.53, rotation: -6 })];
71
+ }
72
+
73
+ function salon(cls: Cls): SlideElement[] {
74
+ if (cls === 'wide') return [...halo(0.72, 0.52, 0.78), ...copy(cls), laptop({ width: 0.78, x: 0.73, y: 0.6, slot: 'second' }), device({ cls, frame: 'none', width: 0.46, x: 0.57, y: 0.4, cornerRadius: 0.025, shadow: SHADOW })];
75
+ if (cls === 'tablet') return [...halo(0.5, 0.8, 1.05), ...copy(cls), pad({ width: 0.58, top: 0.38, x: 0.66, slot: 'second' }), pad({ width: 0.58, top: 0.33, x: 0.36 })];
76
+ return [...halo(0.5, 0.66), ...copy(cls), phone({ width: 0.58, top: 0.33, x: 0.7, rotation: 5, slot: 'second' }), phone({ width: 0.62, top: 0.29, x: 0.33, rotation: -4 })];
77
+ }
78
+
79
+ function delivered(cls: Cls): SlideElement[] {
80
+ if (cls !== 'tall') return hero(cls);
81
+ return [...halo(0.5, 0.36), phone({ width: 0.78, bottom: 0.655 }), t.eyebrow({ y: 0.698 }), ...copyAt(t, { seam: 0.845 })];
82
+ }
83
+
84
+ function circle(cls: Cls): SlideElement[] {
85
+ // Wide and tablet canvases have no room for a larger halo beside or under the copy: the second pair sits inside the first.
86
+ if (cls === 'wide') return [...halo(0.705, 0.52, 0.6), ...hero(cls)];
87
+ if (cls === 'tablet') return [...halo(0.5, 0.78, 0.84), ...hero(cls)];
88
+ return [...halo(0.5, 0.64, 1.25), ...hero(cls)];
89
+ }
90
+
91
+ const o = { classes: ALL_CLASSES };
92
+
93
+ export const noirGold = template({
94
+ id: 'noir-gold',
95
+ name: 'Noir Gold',
96
+ description: 'A boutique after closing time: warm black with a pool of light, Playfair Display in ivory with gold highlights, a spaced gold eyebrow, hairline rings and gold devices on every canvas.',
97
+ category: 'elegant',
98
+ tags: ['luxury', 'serif', 'gold', 'black', 'fashion'],
99
+ apps: ['shop', 'travel', 'calendar', 'social', 'chat', 'finance'],
100
+ theme: theme({
101
+ background: background(radial(0.5, 0.45, 0.78, ['#25201a', 0], ['#141210', 0.6], ['#0a0908', 1])),
102
+ heading: F.playfair,
103
+ body: F.dmSans,
104
+ palette: [GOLD, '#b88a4a', '#8e6f3e', '#f6f0e4', '#0a0908'],
105
+ mockDark: true,
106
+ }),
107
+ slides: [
108
+ slide(
109
+ {
110
+ en: { eyebrow: 'The collection', headline: 'Timeless, *curated*', subtitle: 'Rare watches and fine jewelry from the great houses.' },
111
+ ar: { eyebrow: 'المجموعة', headline: 'فخامة *خالدة*', subtitle: 'ساعات نادرة ومجوهرات فاخرة من أعرق الدور العالمية.' },
112
+ },
113
+ hero,
114
+ o,
115
+ ),
116
+ slide(
117
+ {
118
+ en: { eyebrow: 'Authenticity', headline: 'Crafted to *last*', subtitle: 'Every piece inspected and certified by our experts.' },
119
+ ar: { eyebrow: 'الأصالة', headline: 'صُنعت *لتدوم*', subtitle: 'كل قطعة يفحصها خبراؤنا ويوثّقون أصالتها.' },
120
+ },
121
+ crafted,
122
+ o,
123
+ ),
124
+ slide(
125
+ {
126
+ en: { eyebrow: 'Private salon', headline: 'Your personal *stylist*', subtitle: 'Book a private appointment at home or in store.' },
127
+ ar: { eyebrow: 'الصالون الخاص', headline: 'مستشار أناقة *خاص* بك', subtitle: 'احجز موعدًا خاصًا في منزلك أو في المتجر.' },
128
+ },
129
+ salon,
130
+ o,
131
+ ),
132
+ slide(
133
+ {
134
+ en: { eyebrow: 'White glove', headline: 'Delivered with *care*', subtitle: 'Insured, hand-delivered to your door.' },
135
+ ar: { eyebrow: 'خدمة راقية', headline: 'توصيل *بعناية*', subtitle: 'توصيل مؤمَّن يدًا بيد حتى باب منزلك.' },
136
+ },
137
+ delivered,
138
+ o,
139
+ ),
140
+ slide(
141
+ {
142
+ en: { eyebrow: 'Members', headline: 'Join the *circle*', subtitle: 'Early access to limited editions and private events.' },
143
+ ar: { eyebrow: 'العضوية', headline: 'انضم إلى *النخبة*', subtitle: 'وصول مبكر إلى الإصدارات المحدودة والفعاليات الخاصة.' },
144
+ },
145
+ circle,
146
+ o,
147
+ ),
148
+ ],
149
+ });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Noor (نور, "light"): designed Arabic first. Deep emerald with a fine
3
+ * diagonal lattice, gold eight-point stars and rings in the spirit of
4
+ * Islamic geometric ornament, Cairo headlines and Noto Sans Arabic text.
5
+ * Story: an Arabic books and audiobooks app.
6
+ */
7
+ import type { SlideElement } from '../../model/types';
8
+ import { F, METAL, background, copyAt, device, glow, pattern, radial, rect, ring, slide, template, theme, typeset } from '../kit';
9
+
10
+ const GOLD = '#e9c98b';
11
+
12
+ const t = typeset({
13
+ heading: F.cairo,
14
+ body: F.notoArabic,
15
+ ink: '#fbf5e6',
16
+ soft: '#d3e2da',
17
+ accent: GOLD,
18
+ head: { weight: 800, size: 0.088, lineHeight: 1.22, letterSpacing: -0.01 },
19
+ sub: { weight: 500, size: 0.04, lineHeight: 1.45 },
20
+ });
21
+
22
+ const phone = (o: Parameters<typeof device>[0]) => device({ color: METAL.black, shadow: { color: '#000000a6', blur: 0.08, x: 0, y: 0.035 }, ...o });
23
+
24
+ /** An eight-point star (two squares, one turned 45 degrees). */
25
+ const star = (x: number, y: number, size: number, opacity = 1, color = GOLD): SlideElement[] => [
26
+ rect({ name: 'Star', x, y, size, radius: 0, color, opacity }),
27
+ rect({ name: 'Star', x, y, size, radius: 0, rotation: 45, color, opacity }),
28
+ ];
29
+
30
+ /** Ornament behind a device: a faint star and two gold rings. */
31
+ const rosette = (x: number, y: number, s = 1): SlideElement[] => [
32
+ ...star(x, y, 0.78 * s, 0.07),
33
+ ring({ name: 'Gold ring', x, y, size: 1.16 * s, stroke: 0.003, color: GOLD, opacity: 0.4 }),
34
+ ring({ name: 'Gold ring', x, y, size: 1.06 * s, stroke: 0.0015, color: GOLD, opacity: 0.32 }),
35
+ ];
36
+
37
+ const top = () => copyAt(t, { seam: 0.16 });
38
+ const emblem = () => star(0.5, 0.042, 0.04);
39
+
40
+ const hero = (): SlideElement[] => [glow({ x: 0.5, y: 0.62, size: 0.9, color: '#1f8a72', opacity: 0.4 }), ...rosette(0.5, 0.62), ...emblem(), ...top(), phone({ width: 0.8, top: 0.27 })];
41
+
42
+ const tilted = (): SlideElement[] => [...rosette(0.56, 0.6, 0.9), ...emblem(), ...top(), phone({ width: 0.78, top: 0.275, x: 0.54, rotation: -7 })];
43
+
44
+ const pair = (): SlideElement[] => [
45
+ glow({ x: 0.5, y: 0.66, size: 1.0, color: '#1f8a72', opacity: 0.35 }),
46
+ ...emblem(),
47
+ ...top(),
48
+ phone({ width: 0.6, top: 0.32, x: 0.7, rotation: 6, slot: 'second' }),
49
+ phone({ width: 0.63, top: 0.275, x: 0.33, rotation: -5 }),
50
+ ];
51
+
52
+ const hanging = (): SlideElement[] => [...rosette(0.5, 0.36, 0.95), phone({ width: 0.8, bottom: 0.67 }), ...star(0.5, 0.715, 0.034), ...copyAt(t, { seam: 0.82 })];
53
+
54
+ const calm = (): SlideElement[] => [...rosette(0.5, 0.64, 1.08), ...emblem(), ...top(), phone({ width: 0.76, top: 0.28 })];
55
+
56
+ export const noor = template({
57
+ id: 'noor',
58
+ name: 'Noor',
59
+ description: 'Designed Arabic first: deep emerald with a fine lattice, gold eight-point stars and rings inspired by Islamic geometric ornament, Cairo headlines and Noto Sans Arabic text.',
60
+ category: 'dark',
61
+ tags: ['arabic', 'emerald', 'gold', 'geometric', 'books'],
62
+ apps: ['music', 'calendar', 'social', 'chat', 'meditation', 'tasks', 'shop'],
63
+ theme: theme({
64
+ background: background(radial(0.5, 0.32, 0.92, ['#14493f', 0], ['#0a2c27', 0.55], ['#061a17', 1]), pattern('diagonal', GOLD, 0.05, 0.055)),
65
+ heading: F.cairo,
66
+ body: F.notoArabic,
67
+ palette: [GOLD, '#2bb39a', '#f4a261', '#fbf5e6', '#061a17'],
68
+ mockDark: true,
69
+ }),
70
+ slides: [
71
+ slide(
72
+ {
73
+ ar: { headline: 'اقرأ *واستمع* بالعربية', subtitle: 'آلاف الكتب والكتب الصوتية بين يديك.' },
74
+ en: { headline: 'Read and *listen* in Arabic', subtitle: 'Thousands of books and audiobooks in your pocket.' },
75
+ },
76
+ hero,
77
+ ),
78
+ slide(
79
+ {
80
+ ar: { headline: 'مكتبتك *ترافقك* أينما كنت', subtitle: 'تابع القراءة من حيث توقفت على كل أجهزتك.' },
81
+ en: { headline: 'Your library, *everywhere*', subtitle: 'Pick up where you left off, on every device.' },
82
+ },
83
+ tilted,
84
+ ),
85
+ slide(
86
+ {
87
+ ar: { headline: 'أصوات *تأسرك*', subtitle: 'روايات وكتب يرويها نخبة من أجمل الأصوات العربية.' },
88
+ en: { headline: 'Voices that *captivate*', subtitle: 'Novels and nonfiction, narrated by the finest voices.' },
89
+ },
90
+ pair,
91
+ ),
92
+ slide(
93
+ {
94
+ ar: { headline: 'اقرأ *أكثر* كل يوم', subtitle: 'أهداف قراءة يومية وسلاسل إنجاز تحفّزك.' },
95
+ en: { headline: 'Read *more* every day', subtitle: 'Daily goals and streaks that keep you going.' },
96
+ },
97
+ hanging,
98
+ ),
99
+ slide(
100
+ {
101
+ ar: { headline: 'شارك *ما يُلهمك*', subtitle: 'اقتبس أجمل العبارات وشاركها مع أصدقائك.' },
102
+ en: { headline: 'Share what *inspires* you', subtitle: 'Quote your favorite passages and share them with friends.' },
103
+ },
104
+ calm,
105
+ ),
106
+ ],
107
+ });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Obsidian: a pro tool after hours. Near-black slate with a cold light from
3
+ * above and fine grain, Manrope headlines with periwinkle highlights,
4
+ * deep-blue titanium phones, tablets and a graphite MacBook on every canvas.
5
+ * Story: a project workspace for makers and engineers.
6
+ */
7
+ import type { SlideElement } from '../../model/types';
8
+ import { ALL_CLASSES, F, METAL, background, copyAt, device, glow, pattern, radial, ring, slide, template, theme, typeset, type Cls, type DeviceOptions } from '../kit';
9
+
10
+ const BLUE = '#8fb0ff';
11
+ const t = typeset({
12
+ heading: F.manrope,
13
+ body: F.inter,
14
+ ink: '#f4f6fb',
15
+ soft: '#aab4c7',
16
+ accent: BLUE,
17
+ head: { weight: 800, size: 0.088, lineHeight: 1.1, letterSpacing: -0.035 },
18
+ sub: { weight: 450, size: 0.04 },
19
+ });
20
+
21
+ const SHADOW = { color: '#000000b3', blur: 0.08, x: 0, y: 0.035 };
22
+ const phone = (o: DeviceOptions) => device({ color: METAL.deepBlue, shadow: SHADOW, ...o });
23
+ const pad = (o: DeviceOptions) => device({ cls: 'tablet', frame: 'ipad', color: METAL.deepBlue, shadow: SHADOW, ...o });
24
+ const laptop = (o: DeviceOptions) => device({ cls: 'wide', frame: 'macbook', color: METAL.graphite, shadow: SHADOW, ...o });
25
+
26
+ function copy(cls: Cls, seam?: number): SlideElement[] {
27
+ if (cls === 'wide') return copyAt(t, { cls, seam: seam ?? 0.5, x: 0.22, width: 0.46, align: 'start', head: { size: 0.072, maxLines: 3 }, sub: { size: 0.032, maxLines: 3 } });
28
+ if (cls === 'tablet') return copyAt(t, { cls, seam: seam ?? 0.165, width: 0.8, subWidth: 0.62, head: { size: 0.066 }, sub: { size: 0.031 } });
29
+ return copyAt(t, { seam: seam ?? 0.15 });
30
+ }
31
+
32
+ /** The cold light the devices sit in. */
33
+ const light = (cls: Cls) => (cls === 'wide' ? glow({ x: 0.68, y: 0.3, size: 0.9, color: '#4c6fff', opacity: 0.3 }) : glow({ x: 0.5, y: cls === 'tablet' ? 0.42 : 0.36, size: 1.0, color: '#4c6fff', opacity: 0.3 }));
34
+
35
+ function hero(cls: Cls): SlideElement[] {
36
+ if (cls === 'wide') return [light(cls), ...copy(cls), laptop({ width: 0.92, x: 0.685, y: 0.53 })];
37
+ if (cls === 'tablet') return [light(cls), ...copy(cls), pad({ width: 0.78, top: 0.285 })];
38
+ return [light(cls), ...copy(cls), phone({ width: 0.82, top: 0.24 })];
39
+ }
40
+
41
+ function projects(cls: Cls): SlideElement[] {
42
+ if (cls === 'wide') return [light(cls), ...copy(cls), laptop({ width: 0.8, x: 0.72, y: 0.6, slot: 'second' }), device({ cls, frame: 'none', width: 0.48, x: 0.56, y: 0.38, cornerRadius: 0.025, shadow: SHADOW })];
43
+ if (cls === 'tablet') return [light(cls), ...copy(cls), pad({ width: 0.6, top: 0.36, x: 0.66, slot: 'second' }), pad({ width: 0.6, top: 0.3, x: 0.36 })];
44
+ return [light(cls), ...copy(cls), phone({ width: 0.6, top: 0.3, x: 0.7, slot: 'second' }), phone({ width: 0.64, top: 0.255, x: 0.34 })];
45
+ }
46
+
47
+ function shortcuts(cls: Cls): SlideElement[] {
48
+ if (cls === 'wide') {
49
+ // Copy on top, the laptop below it: a second arrangement for the wide canvas.
50
+ return [glow({ x: 0.5, y: 0.6, size: 1.0, color: '#4c6fff', opacity: 0.28 }), ...copyAt(t, { cls, seam: 0.215, width: 1.2, subWidth: 0.95, head: { size: 0.066 }, sub: { size: 0.03 } }), laptop({ width: 1.02, top: 0.35 })];
51
+ }
52
+ if (cls === 'tablet') return hero(cls);
53
+ return [glow({ x: 0.5, y: 0.34, size: 1.0, color: '#4c6fff', opacity: 0.3 }), phone({ width: 0.82, bottom: 0.69 }), ...copy(cls, 0.81)];
54
+ }
55
+
56
+ function teams(cls: Cls): SlideElement[] {
57
+ if (cls === 'wide') {
58
+ // Mirrored: the laptop on the left, the copy in the right column.
59
+ return [
60
+ glow({ x: 0.32, y: 0.3, size: 0.9, color: '#4c6fff', opacity: 0.3 }),
61
+ ...copyAt(t, { cls, seam: 0.5, x: 0.77, width: 0.48, align: 'start', head: { size: 0.074, maxLines: 3 }, sub: { size: 0.033, maxLines: 3 } }),
62
+ laptop({ width: 0.86, x: 0.31, y: 0.53 }),
63
+ ];
64
+ }
65
+ if (cls !== 'tall') return projects(cls);
66
+ return [light(cls), ...copy(cls), phone({ width: 0.78, top: 0.262, x: 0.53, rotation: -7 })];
67
+ }
68
+
69
+ function orbit(cls: Cls): SlideElement[] {
70
+ const rings = (x: number, y: number, s: number) => [
71
+ ring({ name: 'Orbit', x, y, size: 1.24 * s, stroke: 0.0025, color: BLUE, opacity: 0.22 }),
72
+ ring({ name: 'Orbit', x, y, size: 1.0 * s, stroke: 0.0025, color: BLUE, opacity: 0.38 }),
73
+ ];
74
+ if (cls === 'wide') return [...rings(0.685, 0.53, 0.78), ...hero(cls)];
75
+ if (cls === 'tablet') return [...rings(0.5, 0.76, 1.06), ...hero(cls)];
76
+ return [...rings(0.5, 0.6, 1.02), ...hero(cls)];
77
+ }
78
+
79
+ const o = { classes: ALL_CLASSES };
80
+
81
+ export const obsidian = template({
82
+ id: 'obsidian',
83
+ name: 'Obsidian',
84
+ description: 'A pro tool after hours: near-black slate with a cold light from above and fine grain, Manrope headlines with periwinkle highlights, and deep-blue devices on every canvas.',
85
+ category: 'dark',
86
+ tags: ['dark', 'pro', 'mac', 'productivity', 'minimal'],
87
+ apps: ['tasks', 'calendar', 'chat', 'finance', 'music', 'meditation', 'fitness'],
88
+ theme: theme({
89
+ background: background(radial(0.5, -0.05, 0.95, ['#2a3654', 0], ['#141925', 0.55], ['#0c0e13', 1]), pattern('noise', '#ffffff', 0.04, 0.05)),
90
+ heading: F.manrope,
91
+ body: F.inter,
92
+ palette: ['#6d8dff', '#3ddc97', '#ffb547', '#ff6b81', '#f4f6fb', '#0c0e13'],
93
+ mockDark: true,
94
+ }),
95
+ slides: [
96
+ slide(
97
+ {
98
+ en: { headline: 'Deep work, *on demand*', subtitle: 'A focused workspace for projects, notes and code.' },
99
+ ar: { headline: 'عمل عميق *متى شئت*', subtitle: 'مساحة عمل مركّزة للمشاريع والملاحظات والبرمجة.' },
100
+ },
101
+ hero,
102
+ o,
103
+ ),
104
+ slide(
105
+ {
106
+ en: { headline: 'Every project, *one* place', subtitle: 'Boards, docs and timelines that stay in sync.' },
107
+ ar: { headline: 'كل مشاريعك في *مكان* واحد', subtitle: 'لوحات ومستندات وجداول زمنية متزامنة دائمًا.' },
108
+ },
109
+ projects,
110
+ o,
111
+ ),
112
+ slide(
113
+ {
114
+ en: { headline: 'Shortcuts for *everything*', subtitle: 'Fly through your day without touching the mouse.' },
115
+ ar: { headline: 'اختصارات *لكل شيء*', subtitle: 'أنجز يومك بسرعة دون أن تلمس الفأرة.' },
116
+ },
117
+ shortcuts,
118
+ o,
119
+ ),
120
+ slide(
121
+ {
122
+ en: { headline: 'Built for *teams*', subtitle: 'Comments, mentions and reviews in real time.' },
123
+ ar: { headline: 'مصمّم *للفرق*', subtitle: 'تعليقات وإشارات ومراجعات لحظة بلحظة.' },
124
+ },
125
+ teams,
126
+ o,
127
+ ),
128
+ slide(
129
+ {
130
+ en: { headline: 'Dark by *default*', subtitle: 'Easy on the eyes, from first coffee to last commit.' },
131
+ ar: { headline: 'داكن *افتراضيًا*', subtitle: 'مريح للعين من أول فنجان قهوة حتى آخر سطر برمجي.' },
132
+ },
133
+ orbit,
134
+ o,
135
+ ),
136
+ ],
137
+ });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Panorama Peaks: dawn over a mountain range that spans the whole listing.
3
+ * Two ridges of peaks (squares turned 45 degrees) roll across every seam
4
+ * under a violet-to-apricot sky, with the sun rising between slides 2 and
5
+ * 3. Fraunces headlines. Story: a hiking and trail app.
6
+ */
7
+ import type { SlideElement } from '../../model/types';
8
+ import { ALL_CLASSES, F, METAL, background, canvas, circle, copyAt, device, glow, linear, onStrip, rect, r5, slide, template, theme, typeset, type Cls, type DeviceOptions } from '../kit';
9
+
10
+ const t = typeset({
11
+ heading: F.fraunces,
12
+ body: F.manrope,
13
+ ink: '#fff8ee',
14
+ soft: '#ece2f7',
15
+ accent: '#ffc387',
16
+ head: { weight: 650, size: 0.092, lineHeight: 1.1, letterSpacing: -0.02 },
17
+ sub: { weight: 500, size: 0.04 },
18
+ });
19
+
20
+ const SHADOW = { color: '#12082a8c', blur: 0.07, x: 0, y: 0.03 };
21
+
22
+ /** Peak sizes per canvas: the ridges keep their silhouette on every shape of canvas. */
23
+ const SCALE: Record<Cls, number> = { tall: 1, tablet: 0.72, wide: 0.5 };
24
+
25
+ /** A peak whose summit sits at `apex` (fraction of the canvas height): a square turned 45 degrees, mostly below the edge. */
26
+ function peak(cls: Cls, i: number, X: number, apex: number, side: number, color: string): SlideElement[] {
27
+ const { wu, hu } = canvas(cls);
28
+ const size = side * SCALE[cls];
29
+ const half = (size * Math.SQRT2) / 2;
30
+ return onStrip(i, X, half / wu, (x) => [rect({ name: 'Peak', x, y: r5(apex + half / hu), size: r5(size), rotation: 45, radius: 0.02, color })]);
31
+ }
32
+
33
+ const FAR: [number, number, number][] = [
34
+ [0.48, 0.64, 1.0],
35
+ [1.24, 0.6, 1.15],
36
+ [1.98, 0.67, 0.95],
37
+ [2.74, 0.61, 1.1],
38
+ [3.5, 0.66, 1.0],
39
+ [4.26, 0.6, 1.15],
40
+ [5.0, 0.65, 1.0],
41
+ ];
42
+ const NEAR: [number, number, number][] = [
43
+ [0.12, 0.8, 1.05],
44
+ [0.86, 0.76, 1.2],
45
+ [1.6, 0.81, 1.05],
46
+ [2.36, 0.77, 1.2],
47
+ [3.12, 0.8, 1.1],
48
+ [3.88, 0.76, 1.2],
49
+ [4.64, 0.8, 1.1],
50
+ ];
51
+
52
+ /** The sky and far ridge behind the device, as seen from slide `i`. */
53
+ function behind(cls: Cls, i: number): SlideElement[] {
54
+ const k = SCALE[cls];
55
+ return [
56
+ ...onStrip(i, 2.0, 0.6, (x) => [glow({ name: 'Sun glow', x, y: 0.62, size: r5(1.0 * k), color: '#ffb085', opacity: 0.6 }), circle({ name: 'Sun', x, y: 0.62, size: r5(0.46 * k), fill: linear(180, '#fff3d6', '#ffc58f') })]),
57
+ ...FAR.flatMap(([X, apex, side]) => peak(cls, i, X, apex, side, '#6b4b95')),
58
+ ];
59
+ }
60
+
61
+ /** The near ridge, drawn over the lower part of the device so it rises out of the range. */
62
+ const front = (cls: Cls, i: number): SlideElement[] => NEAR.flatMap(([X, apex, side]) => peak(cls, i, X, apex, side, '#2b1e4e'));
63
+
64
+ function shot(i: number, o: DeviceOptions = {}) {
65
+ return (cls: Cls): SlideElement[] => {
66
+ if (cls === 'wide') {
67
+ // Side by side: the laptop stands in front of the far ridge, clear of the near one.
68
+ return [
69
+ ...behind(cls, i),
70
+ ...copyAt(t, { cls, seam: 0.36, x: 0.235, width: 0.5, align: 'start', head: { size: 0.074, maxLines: 3 }, sub: { size: 0.033, maxLines: 3 } }),
71
+ device({ cls, frame: 'macbook', color: METAL.graphite, shadow: SHADOW, width: 0.86, x: 0.68, y: 0.41 }),
72
+ ...front(cls, i),
73
+ ];
74
+ }
75
+ if (cls === 'tablet') {
76
+ return [
77
+ ...behind(cls, i),
78
+ ...copyAt(t, { cls, seam: 0.165, width: 0.8, subWidth: 0.64, head: { size: 0.068 }, sub: { size: 0.032 } }),
79
+ device({ cls, frame: 'ipad', color: METAL.graphite, shadow: SHADOW, width: 0.62, top: 0.29, rotation: (o.rotation ?? 0) / 2 }),
80
+ ...front(cls, i),
81
+ ];
82
+ }
83
+ return [...behind(cls, i), ...copyAt(t, { seam: 0.15 }), device({ color: METAL.graphite, shadow: SHADOW, width: 0.66, top: 0.255, ...o }), ...front(cls, i)];
84
+ };
85
+ }
86
+
87
+ const o = { classes: ALL_CLASSES };
88
+
89
+ export const panoramaPeaks = template({
90
+ id: 'panorama-peaks',
91
+ name: 'Panorama Peaks',
92
+ description: 'Dawn over a mountain range that spans the whole listing: two ridges roll across every seam under a violet-to-apricot sky, the sun rising between slides two and three.',
93
+ category: 'panorama',
94
+ tags: ['panorama', 'mountains', 'sunrise', 'outdoors', 'serif'],
95
+ apps: ['travel', 'fitness', 'calendar', 'social', 'music', 'chat', 'shop'],
96
+ theme: theme({
97
+ background: background(linear(180, ['#171a45', 0], ['#3d2a72', 0.42], ['#b5577a', 0.68], ['#f39b6d', 0.84], ['#ffd29d', 1])),
98
+ heading: F.fraunces,
99
+ body: F.manrope,
100
+ palette: ['#f2785c', '#ffc387', '#7c5cff', '#2b1e4e', '#fff8ee'],
101
+ panorama: true,
102
+ }),
103
+ slides: [
104
+ slide(
105
+ {
106
+ en: { headline: 'Find your next *summit*', subtitle: '40,000 trails with offline maps and elevation.' },
107
+ ar: { headline: 'اكتشف *قمّتك* القادمة', subtitle: '40 ألف مسار مع خرائط تعمل دون إنترنت وبيانات الارتفاع.' },
108
+ },
109
+ shot(0),
110
+ o,
111
+ ),
112
+ slide(
113
+ {
114
+ en: { headline: 'Every step, *tracked*', subtitle: 'Distance, pace and climb, even without signal.' },
115
+ ar: { headline: 'كل خطوة *محسوبة*', subtitle: 'المسافة والسرعة والصعود حتى دون تغطية.' },
116
+ },
117
+ shot(1),
118
+ o,
119
+ ),
120
+ slide(
121
+ {
122
+ en: { headline: 'Start at *sunrise*', subtitle: 'Golden-hour alerts for views worth waking up for.' },
123
+ ar: { headline: 'ابدأ مع *الشروق*', subtitle: 'تنبيهات للحظات الذهبية التي تستحق الاستيقاظ مبكرًا.' },
124
+ },
125
+ shot(2),
126
+ o,
127
+ ),
128
+ slide(
129
+ {
130
+ en: { headline: 'Hike *safer*', subtitle: 'Share your live route with people you trust.' },
131
+ ar: { headline: 'تنزّه *بأمان*', subtitle: 'شارك مسارك المباشر مع من تثق بهم.' },
132
+ },
133
+ shot(3),
134
+ o,
135
+ ),
136
+ slide(
137
+ {
138
+ en: { headline: 'Collect the *peaks*', subtitle: 'Badges and stats for every mountain you climb.' },
139
+ ar: { headline: 'اجمع *القمم*', subtitle: 'أوسمة وإحصاءات لكل جبل تتسلّقه.' },
140
+ },
141
+ shot(4),
142
+ o,
143
+ ),
144
+ ],
145
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Panorama Ribbon: bright ribbons of colour loop through the whole listing.
3
+ * A pastel paper strip drifting from peach to lilac to mint, thick gradient
4
+ * rings centred on the seams so every loop continues into the next slide,
5
+ * Bricolage Grotesque headlines with hot pink highlights. Story: a photo
6
+ * editing app.
7
+ */
8
+ import type { SlideElement } from '../../model/types';
9
+ import { ALL_CLASSES, F, METAL, background, canvas, copyAt, device, linear, onStrip, ring, slide, template, theme, typeset, type Cls, type DeviceOptions } from '../kit';
10
+
11
+ const PINK = '#ff3d7f';
12
+
13
+ const t = typeset({
14
+ heading: F.bricolage,
15
+ body: F.jakarta,
16
+ ink: '#1a1424',
17
+ soft: '#4b4458',
18
+ accent: PINK,
19
+ head: { weight: 750, size: 0.092, lineHeight: 1.1, letterSpacing: -0.03 },
20
+ sub: { weight: 500, size: 0.04 },
21
+ });
22
+
23
+ const SHADOW = { color: '#3a1f4d40', blur: 0.07, x: 0, y: 0.03 };
24
+
25
+ type Loop = [X: number, y: number, size: number, stroke: number, from: string, to: string, angle: number];
26
+
27
+ /** The loops per canvas shape, kept below the copy so highlights stay readable. */
28
+ const LOOPS: Record<Cls, Loop[]> = {
29
+ tall: [
30
+ [-0.02, 0.47, 0.7, 0.08, '#ffb347', '#ff5f8f', 90],
31
+ [1.0, 0.69, 1.5, 0.11, '#ff5f8f', '#ffb347', 120],
32
+ [2.0, 0.94, 1.22, 0.09, '#2ec4b6', '#7c5cff', 30],
33
+ [3.0, 0.65, 1.36, 0.11, '#7c5cff', '#ff5f8f', 60],
34
+ [4.06, 0.92, 1.0, 0.09, '#ffb347', '#2ec4b6', 150],
35
+ [5.02, 0.5, 0.8, 0.08, '#2ec4b6', '#7c5cff', 90],
36
+ ],
37
+ tablet: [
38
+ [-0.02, 0.52, 0.5, 0.06, '#ffb347', '#ff5f8f', 90],
39
+ [1.0, 0.74, 1.04, 0.08, '#ff5f8f', '#ffb347', 120],
40
+ [2.0, 1.0, 0.9, 0.065, '#2ec4b6', '#7c5cff', 30],
41
+ [3.0, 0.7, 0.96, 0.08, '#7c5cff', '#ff5f8f', 60],
42
+ [4.06, 0.98, 0.72, 0.065, '#ffb347', '#2ec4b6', 150],
43
+ [5.02, 0.52, 0.58, 0.06, '#2ec4b6', '#7c5cff', 90],
44
+ ],
45
+ wide: [
46
+ [-0.02, 0.56, 0.44, 0.05, '#ffb347', '#ff5f8f', 90],
47
+ [1.0, 0.66, 0.84, 0.065, '#ff5f8f', '#ffb347', 120],
48
+ [2.0, 0.96, 0.7, 0.055, '#2ec4b6', '#7c5cff', 30],
49
+ [3.0, 0.62, 0.78, 0.065, '#7c5cff', '#ff5f8f', 60],
50
+ [4.04, 0.94, 0.6, 0.055, '#ffb347', '#2ec4b6', 150],
51
+ [5.02, 0.56, 0.5, 0.05, '#2ec4b6', '#7c5cff', 90],
52
+ ],
53
+ };
54
+
55
+ function ribbons(cls: Cls, i: number): SlideElement[] {
56
+ const { wu } = canvas(cls);
57
+ return LOOPS[cls].flatMap(([X, y, size, stroke, from, to, angle]) => onStrip(i, X, size / 2 / wu, (x) => [ring({ name: 'Ribbon', x, y, size, stroke, fill: linear(angle, from, to) })]));
58
+ }
59
+
60
+ function shot(i: number, o: DeviceOptions = {}) {
61
+ return (cls: Cls): SlideElement[] => {
62
+ if (cls === 'wide') {
63
+ return [
64
+ ...ribbons(cls, i),
65
+ ...copyAt(t, { cls, seam: 0.2, width: 1.2, subWidth: 0.95, head: { size: 0.068 }, sub: { size: 0.03 } }),
66
+ device({ cls, frame: 'macbook', color: METAL.silver, shadow: SHADOW, width: 0.98, top: 0.35 }),
67
+ ];
68
+ }
69
+ if (cls === 'tablet') {
70
+ return [
71
+ ...ribbons(cls, i),
72
+ ...copyAt(t, { cls, seam: 0.165, width: 0.8, subWidth: 0.64, head: { size: 0.068 }, sub: { size: 0.032 } }),
73
+ device({ cls, frame: 'ipad', color: METAL.silver, shadow: SHADOW, width: 0.68, top: 0.3, rotation: (o.rotation ?? 0) / 2 }),
74
+ ];
75
+ }
76
+ return [...ribbons(cls, i), ...copyAt(t, { seam: 0.15 }), device({ color: METAL.silver, shadow: SHADOW, width: 0.74, top: 0.262, ...o })];
77
+ };
78
+ }
79
+
80
+ const o = { classes: ALL_CLASSES };
81
+
82
+ export const panoramaRibbon = template({
83
+ id: 'panorama-ribbon',
84
+ name: 'Panorama Ribbon',
85
+ description: 'Bright ribbons loop through the whole listing: a pastel paper strip drifting from peach to lilac to mint, with thick gradient rings centred on the seams so every loop continues into the next slide.',
86
+ category: 'panorama',
87
+ tags: ['panorama', 'light', 'colorful', 'creative', 'continuous'],
88
+ apps: ['social', 'shop', 'music', 'chat', 'calendar', 'travel'],
89
+ theme: theme({
90
+ background: background(linear(90, '#fff2e8', '#f6efff', '#edf4ff', '#effaf4')),
91
+ heading: F.bricolage,
92
+ body: F.jakarta,
93
+ palette: [PINK, '#ffb347', '#7c5cff', '#2ec4b6', '#ffffff', '#1a1424'],
94
+ panorama: true,
95
+ }),
96
+ slides: [
97
+ slide(
98
+ {
99
+ en: { headline: 'Color that *flows*', subtitle: 'Pro-grade photo editing in a single swipe.' },
100
+ ar: { headline: 'ألوان *تنساب* بسلاسة', subtitle: 'تحرير صور احترافي بلمسة واحدة.' },
101
+ },
102
+ shot(0),
103
+ o,
104
+ ),
105
+ slide(
106
+ {
107
+ en: { headline: 'Presets with *soul*', subtitle: 'Film-inspired looks crafted by real photographers.' },
108
+ ar: { headline: 'فلاتر *بروح* فنية', subtitle: 'لمسات مستوحاة من الأفلام صمّمها مصوّرون محترفون.' },
109
+ },
110
+ shot(1, { rotation: -4 }),
111
+ o,
112
+ ),
113
+ slide(
114
+ {
115
+ en: { headline: 'Edit *anywhere*', subtitle: 'Your edits sync across phone, tablet and desktop.' },
116
+ ar: { headline: 'حرّر صورك *أينما* كنت', subtitle: 'تعديلاتك متزامنة بين الهاتف والجهاز اللوحي والحاسوب.' },
117
+ },
118
+ shot(2),
119
+ o,
120
+ ),
121
+ slide(
122
+ {
123
+ en: { headline: 'Share in *style*', subtitle: 'Perfect crops for every feed, story and post.' },
124
+ ar: { headline: 'شارك *بأناقة*', subtitle: 'مقاسات مثالية لكل منشور وقصة.' },
125
+ },
126
+ shot(3, { rotation: 4 }),
127
+ o,
128
+ ),
129
+ slide(
130
+ {
131
+ en: { headline: 'Made for *makers*', subtitle: 'Layers, masks and curves when you want more control.' },
132
+ ar: { headline: 'صُمّم *للمبدعين*', subtitle: 'طبقات وأقنعة ومنحنيات حين تريد تحكّمًا أكبر.' },
133
+ },
134
+ shot(4),
135
+ o,
136
+ ),
137
+ ],
138
+ });