@atmo-dev/kit 0.4.0 → 0.5.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 (144) hide show
  1. package/README.md +35 -0
  2. package/dist/events/DatePicker.svelte +222 -0
  3. package/dist/events/DatePicker.svelte.d.ts +12 -0
  4. package/dist/events/DateTimePicker.svelte +112 -0
  5. package/dist/events/DateTimePicker.svelte.d.ts +10 -0
  6. package/dist/events/EventAttendees.svelte +199 -0
  7. package/dist/events/EventAttendees.svelte.d.ts +19 -0
  8. package/dist/events/EventCard.svelte +148 -0
  9. package/dist/events/EventCard.svelte.d.ts +8 -0
  10. package/dist/events/EventComments.svelte +98 -0
  11. package/dist/events/EventComments.svelte.d.ts +8 -0
  12. package/dist/events/EventEditor.svelte +540 -0
  13. package/dist/events/EventEditor.svelte.d.ts +18 -0
  14. package/dist/events/EventRsvp.svelte +178 -0
  15. package/dist/events/EventRsvp.svelte.d.ts +16 -0
  16. package/dist/events/EventView.svelte +488 -0
  17. package/dist/events/EventView.svelte.d.ts +23 -0
  18. package/dist/events/ImageDropper.svelte +66 -0
  19. package/dist/events/ImageDropper.svelte.d.ts +7 -0
  20. package/dist/events/Map.svelte +36 -0
  21. package/dist/events/Map.svelte.d.ts +8 -0
  22. package/dist/events/PostToBlueskyModal.svelte +246 -0
  23. package/dist/events/PostToBlueskyModal.svelte.d.ts +22 -0
  24. package/dist/events/ShareModal.svelte +174 -0
  25. package/dist/events/ShareModal.svelte.d.ts +23 -0
  26. package/dist/events/StarterPackButton.svelte +72 -0
  27. package/dist/events/StarterPackButton.svelte.d.ts +10 -0
  28. package/dist/events/ThemeApply.svelte +30 -0
  29. package/dist/events/ThemeApply.svelte.d.ts +7 -0
  30. package/dist/events/ThemeBackground.svelte +33 -0
  31. package/dist/events/ThemeBackground.svelte.d.ts +7 -0
  32. package/dist/events/ThemePicker.svelte +104 -0
  33. package/dist/events/ThemePicker.svelte.d.ts +7 -0
  34. package/dist/events/ThumbnailPresets.svelte +71 -0
  35. package/dist/events/ThumbnailPresets.svelte.d.ts +11 -0
  36. package/dist/events/TimePicker.svelte +117 -0
  37. package/dist/events/TimePicker.svelte.d.ts +10 -0
  38. package/dist/events/TimezonePicker.svelte +67 -0
  39. package/dist/events/TimezonePicker.svelte.d.ts +6 -0
  40. package/dist/events/VodPlayer.svelte +154 -0
  41. package/dist/events/VodPlayer.svelte.d.ts +14 -0
  42. package/dist/events/VodTranscript.svelte +75 -0
  43. package/dist/events/VodTranscript.svelte.d.ts +8 -0
  44. package/dist/events/atproto-helpers.d.ts +21 -0
  45. package/dist/events/atproto-helpers.js +61 -0
  46. package/dist/events/cal/helper.d.ts +1 -0
  47. package/dist/events/cal/helper.js +20 -0
  48. package/dist/events/cal/ical.d.ts +22 -0
  49. package/dist/events/cal/ical.js +188 -0
  50. package/dist/events/cal/sanitize.d.ts +3 -0
  51. package/dist/events/cal/sanitize.js +25 -0
  52. package/dist/events/conference.d.ts +65 -0
  53. package/dist/events/conference.js +61 -0
  54. package/dist/events/contrail.d.ts +49 -0
  55. package/dist/events/contrail.js +12 -0
  56. package/dist/events/date-format.d.ts +22 -0
  57. package/dist/events/date-format.js +43 -0
  58. package/dist/events/editor/LinkedRecordsPicker.svelte +104 -0
  59. package/dist/events/editor/LinkedRecordsPicker.svelte.d.ts +12 -0
  60. package/dist/events/editor/LinksSection.svelte +154 -0
  61. package/dist/events/editor/LinksSection.svelte.d.ts +13 -0
  62. package/dist/events/editor/LocationSection.svelte +163 -0
  63. package/dist/events/editor/LocationSection.svelte.d.ts +8 -0
  64. package/dist/events/editor/RecurringModal.svelte +313 -0
  65. package/dist/events/editor/RecurringModal.svelte.d.ts +30 -0
  66. package/dist/events/editor/ThemeSection.svelte +24 -0
  67. package/dist/events/editor/ThemeSection.svelte.d.ts +7 -0
  68. package/dist/events/editor/ThumbnailSection.svelte +181 -0
  69. package/dist/events/editor/ThumbnailSection.svelte.d.ts +13 -0
  70. package/dist/events/editor/adapter.d.ts +73 -0
  71. package/dist/events/editor/adapter.js +9 -0
  72. package/dist/events/editor/location.d.ts +16 -0
  73. package/dist/events/editor/location.js +50 -0
  74. package/dist/events/editor/save.d.ts +43 -0
  75. package/dist/events/editor/save.js +168 -0
  76. package/dist/events/editor/types.d.ts +39 -0
  77. package/dist/events/editor/types.js +9 -0
  78. package/dist/events/event-media.d.ts +4 -0
  79. package/dist/events/event-media.js +4 -0
  80. package/dist/events/event-types.d.ts +79 -0
  81. package/dist/events/event-types.js +11 -0
  82. package/dist/events/event-view/AddToCalendarButton.svelte +31 -0
  83. package/dist/events/event-view/AddToCalendarButton.svelte.d.ts +9 -0
  84. package/dist/events/event-view/EventBadges.svelte +20 -0
  85. package/dist/events/event-view/EventBadges.svelte.d.ts +7 -0
  86. package/dist/events/event-view/EventDateBlock.svelte +43 -0
  87. package/dist/events/event-view/EventDateBlock.svelte.d.ts +7 -0
  88. package/dist/events/event-view/EventHostedBy.svelte +61 -0
  89. package/dist/events/event-view/EventHostedBy.svelte.d.ts +16 -0
  90. package/dist/events/event-view/EventLinksList.svelte +24 -0
  91. package/dist/events/event-view/EventLinksList.svelte.d.ts +9 -0
  92. package/dist/events/event-view/EventLocationBlock.svelte +62 -0
  93. package/dist/events/event-view/EventLocationBlock.svelte.d.ts +7 -0
  94. package/dist/events/event-view/EventLocationMap.svelte +66 -0
  95. package/dist/events/event-view/EventLocationMap.svelte.d.ts +7 -0
  96. package/dist/events/event-view/ExternalRsvpNotice.svelte +34 -0
  97. package/dist/events/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
  98. package/dist/events/event-view/StreamPlacePlayer.svelte +209 -0
  99. package/dist/events/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
  100. package/dist/events/event-view/format.d.ts +10 -0
  101. package/dist/events/event-view/format.js +112 -0
  102. package/dist/events/event-view/locations.d.ts +36 -0
  103. package/dist/events/event-view/locations.js +243 -0
  104. package/dist/events/event-view/plyr-icons.d.ts +3 -0
  105. package/dist/events/event-view/plyr-icons.js +46 -0
  106. package/dist/events/linked-records.d.ts +18 -0
  107. package/dist/events/linked-records.js +11 -0
  108. package/dist/events/profile-url.d.ts +3 -0
  109. package/dist/events/profile-url.js +11 -0
  110. package/dist/events/schedule/ConferenceTimetable.svelte +340 -0
  111. package/dist/events/schedule/ConferenceTimetable.svelte.d.ts +22 -0
  112. package/dist/events/schedule/DaySchedule.svelte +298 -0
  113. package/dist/events/schedule/DaySchedule.svelte.d.ts +26 -0
  114. package/dist/events/schedule/ScheduleEventCell.svelte +166 -0
  115. package/dist/events/schedule/ScheduleEventCell.svelte.d.ts +25 -0
  116. package/dist/events/schedule/schedule-utils.d.ts +44 -0
  117. package/dist/events/schedule/schedule-utils.js +166 -0
  118. package/dist/events/starter-pack-url.d.ts +3 -0
  119. package/dist/events/starter-pack-url.js +16 -0
  120. package/dist/events/theme.d.ts +9 -0
  121. package/dist/events/theme.js +36 -0
  122. package/dist/events/themes/Blobs.svelte +64 -0
  123. package/dist/events/themes/Blobs.svelte.d.ts +18 -0
  124. package/dist/events/themes/Butterflies.svelte +183 -0
  125. package/dist/events/themes/Butterflies.svelte.d.ts +3 -0
  126. package/dist/events/themes/Fireflies.svelte +134 -0
  127. package/dist/events/themes/Fireflies.svelte.d.ts +3 -0
  128. package/dist/events/themes/Kaleidoscope.svelte +183 -0
  129. package/dist/events/themes/Kaleidoscope.svelte.d.ts +3 -0
  130. package/dist/events/themes/Matrix.svelte +153 -0
  131. package/dist/events/themes/Matrix.svelte.d.ts +3 -0
  132. package/dist/events/themes/Stars.svelte +98 -0
  133. package/dist/events/themes/Stars.svelte.d.ts +3 -0
  134. package/dist/events/thumbnails/designs.d.ts +18 -0
  135. package/dist/events/thumbnails/designs.js +316 -0
  136. package/dist/events/view-data.d.ts +38 -0
  137. package/dist/events/view-data.js +1 -0
  138. package/dist/events-data.d.ts +15 -0
  139. package/dist/events-data.js +9 -0
  140. package/dist/events-editor.d.ts +2 -0
  141. package/dist/events-editor.js +2 -0
  142. package/dist/events.d.ts +19 -0
  143. package/dist/events.js +17 -0
  144. package/package.json +46 -4
@@ -0,0 +1,166 @@
1
+ export const SLOT = 5;
2
+ export const SLOT_HEIGHT = '0.6rem';
3
+ export const linkableTypes = new Set([
4
+ 'talk',
5
+ 'presentation',
6
+ 'workshop',
7
+ 'lightning-talk',
8
+ 'panel'
9
+ ]);
10
+ export function isLightning(type) {
11
+ return type === 'lightning-talk';
12
+ }
13
+ export function isCompact(durationMin, type) {
14
+ return type === 'lightning-talk' || durationMin <= 15;
15
+ }
16
+ /**
17
+ * Wall-clock components of an instant as seen in `tz`, returned as a Date built
18
+ * from those local parts (so getHours()/getMinutes() read the tz time, not the
19
+ * runtime's). Used to lay the grid out in the conference's own timezone.
20
+ */
21
+ export function toTzDate(iso, tz) {
22
+ const date = new Date(iso);
23
+ const parts = new Intl.DateTimeFormat('en-US', {
24
+ timeZone: tz,
25
+ year: 'numeric',
26
+ month: '2-digit',
27
+ day: '2-digit',
28
+ hour: '2-digit',
29
+ minute: '2-digit',
30
+ hour12: false
31
+ }).formatToParts(date);
32
+ const get = (type) => parseInt(parts.find((p) => p.type === type)?.value ?? '0');
33
+ return new Date(get('year'), get('month') - 1, get('day'), get('hour'), get('minute'));
34
+ }
35
+ export function isoToMinutes(iso, tz) {
36
+ const d = toTzDate(iso, tz);
37
+ return d.getHours() * 60 + d.getMinutes();
38
+ }
39
+ export function formatTime(iso, tz) {
40
+ return new Date(iso).toLocaleTimeString('en-US', {
41
+ hour: 'numeric',
42
+ minute: '2-digit',
43
+ hour12: true,
44
+ timeZone: tz
45
+ });
46
+ }
47
+ /** Hour label for a "minutes since midnight" value (0–1439). Timezone-agnostic. */
48
+ export function formatHour(minutes) {
49
+ const h = Math.floor(minutes / 60) % 24;
50
+ const p = h >= 12 ? 'PM' : 'AM';
51
+ const display = h % 12 === 0 ? 12 : h % 12;
52
+ return `${display}${p}`;
53
+ }
54
+ export function getDayKey(iso, tz) {
55
+ const d = toTzDate(iso, tz);
56
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
57
+ }
58
+ export function getDayLabel(iso, tz) {
59
+ return new Date(iso).toLocaleDateString('en-US', {
60
+ timeZone: tz,
61
+ weekday: 'long',
62
+ month: 'long',
63
+ day: 'numeric'
64
+ });
65
+ }
66
+ export function getDayShortLabel(iso, tz) {
67
+ return new Date(iso).toLocaleDateString('en-US', { weekday: 'short', timeZone: tz });
68
+ }
69
+ export function getEventColor(type) {
70
+ switch (type) {
71
+ case 'info':
72
+ return 'bg-base-200 dark:bg-base-800 text-base-700 dark:text-base-300';
73
+ case 'workshop':
74
+ return 'bg-cyan-100 dark:bg-cyan-950 text-cyan-900 dark:text-cyan-200';
75
+ case 'lightning-talk':
76
+ return 'bg-sky-100 dark:bg-sky-950 text-sky-900 dark:text-sky-200';
77
+ case 'panel':
78
+ return 'bg-violet-100 dark:bg-violet-950 text-violet-900 dark:text-violet-200';
79
+ case 'activity':
80
+ return 'bg-emerald-100 dark:bg-emerald-950 text-emerald-900 dark:text-emerald-200';
81
+ default:
82
+ return 'bg-teal-100 dark:bg-teal-950 text-teal-900 dark:text-teal-200';
83
+ }
84
+ }
85
+ export function durationMinutes(start, end, tz) {
86
+ if (!end)
87
+ return 30;
88
+ return isoToMinutes(end, tz) - isoToMinutes(start, tz);
89
+ }
90
+ /** Rooms present in a set of events, in display order. */
91
+ export function getRooms(events, explicitOrder) {
92
+ const present = new Set();
93
+ for (const e of events) {
94
+ if (e.room && e.room !== 'none' && e.type !== 'info')
95
+ present.add(e.room);
96
+ }
97
+ if (explicitOrder?.length) {
98
+ // Honour the declared order, then append any rooms not in the list.
99
+ const ordered = explicitOrder.filter((r) => present.has(r));
100
+ const extra = [...present].filter((r) => !explicitOrder.includes(r)).sort();
101
+ return [...ordered, ...extra];
102
+ }
103
+ return [...present].sort();
104
+ }
105
+ export function buildGrid(events, rooms, tz) {
106
+ const parsed = events
107
+ .filter((e) => e.start && e.title?.trim())
108
+ .map((e) => {
109
+ const startMin = isoToMinutes(e.start, tz);
110
+ const endMin = e.end ? isoToMinutes(e.end, tz) : startMin + 30;
111
+ return { ...e, startMin, endMin };
112
+ });
113
+ const gridRelevant = parsed.filter((e) => e.type === 'info' || rooms.includes(e.room || ''));
114
+ if (gridRelevant.length === 0)
115
+ return { gridEvents: [], minTime: 0, totalSlots: 0, timeGridRows: '' };
116
+ const minTime = Math.floor(Math.min(...gridRelevant.map((e) => e.startMin)) / SLOT) * SLOT;
117
+ const maxTime = Math.ceil(Math.max(...gridRelevant.map((e) => e.endMin)) / SLOT) * SLOT;
118
+ const totalSlots = (maxTime - minTime) / SLOT;
119
+ // Collapse duplicate "doors open"-style info rows that share a start time
120
+ // with another info event, so they don't stack on top of each other.
121
+ const infoStartTimes = new Map();
122
+ for (const e of gridRelevant) {
123
+ if (e.type === 'info')
124
+ infoStartTimes.set(e.startMin, (infoStartTimes.get(e.startMin) ?? 0) + 1);
125
+ }
126
+ const filteredRelevant = gridRelevant.filter((e) => {
127
+ if (e.type === 'info' &&
128
+ /doors\s*open/i.test(e.title) &&
129
+ (infoStartTimes.get(e.startMin) ?? 0) > 1)
130
+ return false;
131
+ return true;
132
+ });
133
+ // Trim each info banner's end so consecutive ones don't overlap.
134
+ const infoEvents = filteredRelevant
135
+ .filter((e) => e.type === 'info')
136
+ .sort((a, b) => a.startMin - b.startMin);
137
+ for (let i = 0; i < infoEvents.length - 1; i++) {
138
+ const next = infoEvents[i + 1];
139
+ if (infoEvents[i].endMin > next.startMin) {
140
+ infoEvents[i].endMin = next.startMin;
141
+ }
142
+ }
143
+ const gridEvents = filteredRelevant.map((e) => {
144
+ const fullWidth = e.type === 'info';
145
+ return {
146
+ ...e,
147
+ startRow: Math.round((e.startMin - minTime) / SLOT) + 2,
148
+ spanRows: Math.max(1, Math.round((e.endMin - e.startMin) / SLOT)),
149
+ colStart: fullWidth ? 1 : rooms.indexOf(e.room) + 1,
150
+ colSpan: fullWidth ? rooms.length : 1,
151
+ zIndex: fullWidth ? 0 : e.startMin
152
+ };
153
+ });
154
+ const timeGridRows = `grid-template-rows: 0.5rem repeat(${totalSlots}, ${SLOT_HEIGHT})`;
155
+ return { gridEvents, minTime, totalSlots, timeGridRows };
156
+ }
157
+ export function getNowGridRow(grid, dayKey, nowKey, nowMinutes) {
158
+ if (grid.totalSlots === 0 || nowKey !== dayKey)
159
+ return null;
160
+ const exactSlot = (nowMinutes - grid.minTime) / SLOT;
161
+ const row = Math.floor(exactSlot) + 2;
162
+ const offsetPercent = (exactSlot - Math.floor(exactSlot)) * 100;
163
+ if (row < 2 || row > grid.totalSlots + 2)
164
+ return null;
165
+ return { row, offsetPercent };
166
+ }
@@ -0,0 +1,3 @@
1
+ import { type PreferredSocialClient } from '../social.js';
2
+ /** The pack service returns canonical Bluesky links; navigation is viewer-specific. */
3
+ export declare function starterPackClientUrl(canonicalUrl: string, socialClient?: PreferredSocialClient | null): string | null;
@@ -0,0 +1,16 @@
1
+ import { socialClientInfo } from '../social.js';
2
+ /** The pack service returns canonical Bluesky links; navigation is viewer-specific. */
3
+ export function starterPackClientUrl(canonicalUrl, socialClient) {
4
+ try {
5
+ const url = new URL(canonicalUrl);
6
+ if (url.origin !== 'https://bsky.app' ||
7
+ url.username ||
8
+ url.password ||
9
+ !/^\/starter-pack\/[^/]+\/[^/]+$/.test(url.pathname))
10
+ return null;
11
+ return `${socialClientInfo(socialClient).url}${url.pathname}${url.search}${url.hash}`;
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ export interface EventTheme {
2
+ name: string;
3
+ accentColor: string;
4
+ baseColor: string;
5
+ }
6
+ export declare const accentColors: readonly ["red", "orange", "amber", "yellow", "lime", "green", "emerald", "teal", "cyan", "sky", "blue", "indigo", "violet", "purple", "fuchsia", "pink", "rose"];
7
+ export declare const defaultTheme: EventTheme;
8
+ export declare function randomAccentColor(): string;
9
+ export declare const themeBackgrounds: Record<string, string>;
@@ -0,0 +1,36 @@
1
+ export const accentColors = [
2
+ 'red',
3
+ 'orange',
4
+ 'amber',
5
+ 'yellow',
6
+ 'lime',
7
+ 'green',
8
+ 'emerald',
9
+ 'teal',
10
+ 'cyan',
11
+ 'sky',
12
+ 'blue',
13
+ 'indigo',
14
+ 'violet',
15
+ 'purple',
16
+ 'fuchsia',
17
+ 'pink',
18
+ 'rose'
19
+ ];
20
+ export const defaultTheme = {
21
+ name: 'minimal',
22
+ accentColor: 'pink',
23
+ baseColor: 'zinc'
24
+ };
25
+ export function randomAccentColor() {
26
+ return accentColors[Math.floor(Math.random() * accentColors.length)];
27
+ }
28
+ export const themeBackgrounds = {
29
+ minimal: 'Minimal',
30
+ blobs: 'Blobs',
31
+ warp: 'Stars',
32
+ matrix: 'Matrix',
33
+ fireflies: 'Fireflies',
34
+ butterflies: 'Butterflies',
35
+ kaleidoscope: 'Kaleidoscope'
36
+ };
@@ -0,0 +1,64 @@
1
+ <script lang="ts"></script>
2
+
3
+ <div class="pointer-events-none fixed inset-0 -z-10 overflow-hidden bg-base-50 dark:bg-base-900">
4
+ <div
5
+ class="blob-1 absolute rounded-full bg-accent-500 opacity-30 blur-3xl"
6
+ style="width: 40vw; height: 40vw; top: -10%; left: -5%;"
7
+ ></div>
8
+ <div
9
+ class="blob-2 absolute rounded-full bg-accent-500 opacity-20 blur-3xl"
10
+ style="width: 35vw; height: 35vw; bottom: -5%; right: -10%;"
11
+ ></div>
12
+ <div
13
+ class="blob-3 absolute rounded-full bg-accent-400 opacity-15 blur-3xl"
14
+ style="width: 25vw; height: 25vw; top: 40%; left: 50%;"
15
+ ></div>
16
+ </div>
17
+
18
+ <style>
19
+ @keyframes blob-float-1 {
20
+ 0%,
21
+ 100% {
22
+ transform: translate(0, 0) scale(1);
23
+ }
24
+ 33% {
25
+ transform: translate(5vw, 8vh) scale(1.1);
26
+ }
27
+ 66% {
28
+ transform: translate(-3vw, 4vh) scale(0.95);
29
+ }
30
+ }
31
+ @keyframes blob-float-2 {
32
+ 0%,
33
+ 100% {
34
+ transform: translate(0, 0) scale(1);
35
+ }
36
+ 33% {
37
+ transform: translate(-6vw, -5vh) scale(1.05);
38
+ }
39
+ 66% {
40
+ transform: translate(4vw, -8vh) scale(1.1);
41
+ }
42
+ }
43
+ @keyframes blob-float-3 {
44
+ 0%,
45
+ 100% {
46
+ transform: translate(0, 0) scale(1);
47
+ }
48
+ 33% {
49
+ transform: translate(8vw, -4vh) scale(1.15);
50
+ }
51
+ 66% {
52
+ transform: translate(-5vw, 6vh) scale(0.9);
53
+ }
54
+ }
55
+ .blob-1 {
56
+ animation: blob-float-1 20s ease-in-out infinite;
57
+ }
58
+ .blob-2 {
59
+ animation: blob-float-2 25s ease-in-out infinite;
60
+ }
61
+ .blob-3 {
62
+ animation: blob-float-3 22s ease-in-out infinite;
63
+ }
64
+ </style>
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const Blobs: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type Blobs = InstanceType<typeof Blobs>;
18
+ export default Blobs;
@@ -0,0 +1,183 @@
1
+ <script lang="ts">
2
+ import { BROWSER as browser } from 'esm-env';
3
+
4
+ let canvas: HTMLCanvasElement | undefined = $state(undefined);
5
+
6
+ $effect(() => {
7
+ if (!canvas || !browser) return;
8
+
9
+ const ctx = canvas.getContext('2d')!;
10
+ let animId: number;
11
+
12
+ const COUNT = 22;
13
+ const DRIFT_SPEED = 45; // px/sec forward flight
14
+ const DART_RATE = 0.15; // darts per second per butterfly
15
+ const ROTATION_EASE = 4; // rad/sec — how fast body turns toward heading
16
+
17
+ interface Butterfly {
18
+ x: number;
19
+ y: number;
20
+ heading: number; // radians — current direction of travel
21
+ rotation: number; // body orientation, lags heading slightly
22
+ turnRate: number; // radians/sec of random-walk steering
23
+ flapPhase: number;
24
+ flapRate: number; // Hz-ish
25
+ depthPhase: number; // slow "closer/further" scaling
26
+ depthRate: number; // Hz of depth oscillation
27
+ size: number;
28
+ hueShift: number;
29
+ alpha: number;
30
+ }
31
+
32
+ let lastWidth = 0;
33
+ function resize() {
34
+ const w = window.innerWidth;
35
+ if (w === lastWidth) return;
36
+ lastWidth = w;
37
+ canvas!.width = w;
38
+ canvas!.height = window.screen.height;
39
+ }
40
+ resize();
41
+ window.addEventListener('resize', resize);
42
+
43
+ function spawn(): Butterfly {
44
+ const angle = Math.random() * Math.PI * 2;
45
+ return {
46
+ x: Math.random() * canvas!.width,
47
+ y: Math.random() * canvas!.height,
48
+ heading: angle,
49
+ rotation: angle,
50
+ turnRate: 0.8 + Math.random() * 1.2, // 0.8–2 rad/√s of wander
51
+ flapPhase: Math.random() * Math.PI * 2,
52
+ flapRate: 1.5 + Math.random() * 1.5, // 1.5–3 flaps/sec
53
+ depthPhase: Math.random() * Math.PI * 2,
54
+ depthRate: 0.08 + Math.random() * 0.08, // 1/12s to 1/6s — 6–12s period
55
+ size: 10 + Math.random() * 18,
56
+ hueShift: (Math.random() - 0.5) * 40,
57
+ alpha: 0.35 + Math.random() * 0.4
58
+ };
59
+ }
60
+
61
+ const butterflies: Butterfly[] = Array.from({ length: COUNT }, spawn);
62
+
63
+ const accentColor = getComputedStyle(document.documentElement)
64
+ .getPropertyValue('--color-accent-500')
65
+ .trim();
66
+
67
+ // Right wing: top + bottom lobe drawn as a single path so their overlap
68
+ // fills once (no alpha accumulation). Positioned so the inner edge sits
69
+ // at +x ≈ 0.05, keeping the two wings from bleeding into each other at
70
+ // the body axis.
71
+ function drawWing(s: number) {
72
+ ctx.beginPath();
73
+ ctx.ellipse(s * 0.65, -s * 0.3, s * 0.6, s * 0.5, -0.25, 0, Math.PI * 2);
74
+ ctx.ellipse(s * 0.55, s * 0.35, s * 0.5, s * 0.4, 0.2, 0, Math.PI * 2);
75
+ ctx.fill();
76
+ }
77
+
78
+ function drawButterfly(b: Butterfly) {
79
+ // Smooth 0→1 flap at the natural flapRate (Hz)
80
+ const flap = (Math.sin(b.flapPhase) + 1) / 2;
81
+ const wingScaleX = 0.45 + 0.55 * flap; // 0.45 (folded) → 1.0 (open)
82
+
83
+ // Combined scale: slow "depth" breathing (±25% over ~10s) plus a tiny
84
+ // bump synced to each wing flap so peak-open wings read as closer.
85
+ const depth = 1 + 0.25 * Math.sin(b.depthPhase);
86
+ const flapBump = 1 + 0.08 * flap;
87
+ const scale = depth * flapBump;
88
+
89
+ ctx.save();
90
+ ctx.translate(b.x, b.y);
91
+ ctx.scale(scale, scale);
92
+ ctx.rotate(b.rotation + Math.PI / 2); // body points along velocity
93
+
94
+ const fill = accentColor
95
+ ? `oklch(from ${accentColor} l c calc(h + ${b.hueShift}) / ${b.alpha})`
96
+ : `rgba(80, 140, 240, ${b.alpha})`;
97
+ ctx.fillStyle = fill;
98
+
99
+ // Right wing
100
+ ctx.save();
101
+ ctx.scale(wingScaleX, 1);
102
+ drawWing(b.size);
103
+ ctx.restore();
104
+
105
+ // Left wing (mirrored)
106
+ ctx.save();
107
+ ctx.scale(-wingScaleX, 1);
108
+ drawWing(b.size);
109
+ ctx.restore();
110
+
111
+ // Tiny body — slightly darker tint of the accent
112
+ ctx.fillStyle = accentColor
113
+ ? `oklch(from ${accentColor} calc(l * 0.55) c calc(h + ${b.hueShift}) / ${Math.min(1, b.alpha + 0.2)})`
114
+ : `rgba(30, 40, 80, ${Math.min(1, b.alpha + 0.2)})`;
115
+ ctx.beginPath();
116
+ ctx.ellipse(0, 0, b.size * 0.08, b.size * 0.55, 0, 0, Math.PI * 2);
117
+ ctx.fill();
118
+
119
+ ctx.restore();
120
+ }
121
+
122
+ let lastTime = performance.now();
123
+
124
+ function draw(now: number) {
125
+ const dt = Math.min((now - lastTime) / 1000, 0.1);
126
+ lastTime = now;
127
+
128
+ const w = canvas!.width;
129
+ const h = canvas!.height;
130
+
131
+ ctx.clearRect(0, 0, w, h);
132
+
133
+ // Framerate-independent turn-toward-heading easing factor.
134
+ const rotationBlend = 1 - Math.exp(-ROTATION_EASE * dt);
135
+ const sqrtDt = Math.sqrt(dt);
136
+
137
+ for (const b of butterflies) {
138
+ b.flapPhase += b.flapRate * 2 * Math.PI * dt;
139
+ b.depthPhase += b.depthRate * 2 * Math.PI * dt;
140
+
141
+ // Heading random walk — sqrt(dt) keeps the per-second angular
142
+ // variance constant regardless of framerate (Brownian scaling).
143
+ b.heading += (Math.random() - 0.5) * 2 * b.turnRate * sqrtDt;
144
+
145
+ // Occasional sharp dart, as a Poisson process at DART_RATE/sec.
146
+ if (Math.random() < DART_RATE * dt) {
147
+ b.heading += (Math.random() - 0.5) * Math.PI;
148
+ }
149
+
150
+ b.x += Math.cos(b.heading) * DRIFT_SPEED * dt;
151
+ b.y += Math.sin(b.heading) * DRIFT_SPEED * dt;
152
+
153
+ // Body orientation eases toward heading with constant time constant.
154
+ let delta = b.heading - b.rotation;
155
+ while (delta > Math.PI) delta -= Math.PI * 2;
156
+ while (delta < -Math.PI) delta += Math.PI * 2;
157
+ b.rotation += delta * rotationBlend;
158
+
159
+ const margin = b.size * 2;
160
+ if (b.x < -margin) b.x = w + margin;
161
+ if (b.x > w + margin) b.x = -margin;
162
+ if (b.y < -margin) b.y = h + margin;
163
+ if (b.y > h + margin) b.y = -margin;
164
+
165
+ drawButterfly(b);
166
+ }
167
+
168
+ animId = requestAnimationFrame(draw);
169
+ }
170
+
171
+ animId = requestAnimationFrame(draw);
172
+
173
+ return () => {
174
+ cancelAnimationFrame(animId);
175
+ window.removeEventListener('resize', resize);
176
+ };
177
+ });
178
+ </script>
179
+
180
+ <div class="pointer-events-none fixed inset-0 -z-10 bg-base-50 dark:bg-base-900">
181
+ <canvas bind:this={canvas} class="absolute inset-0 h-full w-full opacity-60 blur-[1.5px]"
182
+ ></canvas>
183
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const Butterflies: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Butterflies = ReturnType<typeof Butterflies>;
3
+ export default Butterflies;
@@ -0,0 +1,134 @@
1
+ <script lang="ts">
2
+ import { BROWSER as browser } from 'esm-env';
3
+
4
+ let canvas: HTMLCanvasElement | undefined = $state(undefined);
5
+
6
+ $effect(() => {
7
+ if (!canvas || !browser) return;
8
+
9
+ const ctx = canvas.getContext('2d')!;
10
+ let animId: number;
11
+
12
+ const COUNT = 50;
13
+ const DRIFT_SPEED = 15;
14
+ const PULSE_SPEED = 0.6;
15
+
16
+ interface Firefly {
17
+ x: number;
18
+ y: number;
19
+ vx: number;
20
+ vy: number;
21
+ phase: number;
22
+ pulseRate: number;
23
+ size: number;
24
+ hueShift: number;
25
+ }
26
+
27
+ let lastWidth = 0;
28
+ function resize() {
29
+ const w = window.innerWidth;
30
+ if (w === lastWidth) return;
31
+ lastWidth = w;
32
+ canvas!.width = w;
33
+ canvas!.height = window.screen.height;
34
+ }
35
+ resize();
36
+ window.addEventListener('resize', resize);
37
+
38
+ const flies: Firefly[] = Array.from({ length: COUNT }, () => ({
39
+ x: Math.random() * canvas!.width,
40
+ y: Math.random() * canvas!.height,
41
+ vx: (Math.random() - 0.5) * 2,
42
+ vy: (Math.random() - 0.5) * 2,
43
+ phase: Math.random() * Math.PI * 2,
44
+ pulseRate: PULSE_SPEED * (0.6 + Math.random() * 0.8),
45
+ size: 3 + Math.random() * 5,
46
+ hueShift: (Math.random() - 0.5) * 60 // -30 to +30 degrees
47
+ }));
48
+
49
+ const accentColor = getComputedStyle(document.documentElement)
50
+ .getPropertyValue('--color-accent-500')
51
+ .trim();
52
+
53
+ let lastTime = performance.now();
54
+
55
+ function draw(now: number) {
56
+ const dt = Math.min((now - lastTime) / 1000, 0.1);
57
+ lastTime = now;
58
+
59
+ const w = canvas!.width;
60
+ const h = canvas!.height;
61
+
62
+ ctx.clearRect(0, 0, w, h);
63
+
64
+ for (const fly of flies) {
65
+ fly.phase += fly.pulseRate * dt;
66
+ fly.x += fly.vx * DRIFT_SPEED * dt;
67
+ fly.y += fly.vy * DRIFT_SPEED * dt;
68
+
69
+ fly.vx += (Math.random() - 0.5) * 2 * dt;
70
+ fly.vy += (Math.random() - 0.5) * 2 * dt;
71
+ const len = Math.sqrt(fly.vx * fly.vx + fly.vy * fly.vy);
72
+ if (len > 1) {
73
+ fly.vx /= len;
74
+ fly.vy /= len;
75
+ }
76
+
77
+ if (fly.x < -40) fly.x = w + 40;
78
+ if (fly.x > w + 40) fly.x = -40;
79
+ if (fly.y < -40) fly.y = h + 40;
80
+ if (fly.y > h + 40) fly.y = -40;
81
+
82
+ const glow = (Math.sin(fly.phase) + 1) / 2;
83
+ const alpha = 0.05 + glow * 0.5;
84
+ const radius = fly.size * (0.5 + glow * 0.5);
85
+ const glowRadius = radius * 12;
86
+ const h_shift = fly.hueShift;
87
+
88
+ // Large soft glow
89
+ const gradient = ctx.createRadialGradient(fly.x, fly.y, 0, fly.x, fly.y, glowRadius);
90
+ gradient.addColorStop(
91
+ 0,
92
+ accentColor
93
+ ? `oklch(from ${accentColor} l c calc(h + ${h_shift}) / ${alpha * 0.4})`
94
+ : `rgba(255, 200, 50, ${alpha * 0.4})`
95
+ );
96
+ gradient.addColorStop(
97
+ 0.4,
98
+ accentColor
99
+ ? `oklch(from ${accentColor} l c calc(h + ${h_shift}) / ${alpha * 0.15})`
100
+ : `rgba(255, 200, 50, ${alpha * 0.15})`
101
+ );
102
+ gradient.addColorStop(
103
+ 1,
104
+ accentColor
105
+ ? `oklch(from ${accentColor} l c calc(h + ${h_shift}) / 0)`
106
+ : `rgba(255, 200, 50, 0)`
107
+ );
108
+ ctx.fillStyle = gradient;
109
+ ctx.fillRect(fly.x - glowRadius, fly.y - glowRadius, glowRadius * 2, glowRadius * 2);
110
+
111
+ // Bright core
112
+ ctx.beginPath();
113
+ ctx.arc(fly.x, fly.y, radius, 0, Math.PI * 2);
114
+ ctx.fillStyle = accentColor
115
+ ? `oklch(from ${accentColor} calc(l * 1.4) c calc(h + ${h_shift}) / ${alpha})`
116
+ : `rgba(255, 230, 100, ${alpha})`;
117
+ ctx.fill();
118
+ }
119
+
120
+ animId = requestAnimationFrame(draw);
121
+ }
122
+
123
+ animId = requestAnimationFrame(draw);
124
+
125
+ return () => {
126
+ cancelAnimationFrame(animId);
127
+ window.removeEventListener('resize', resize);
128
+ };
129
+ });
130
+ </script>
131
+
132
+ <div class="pointer-events-none fixed inset-0 -z-10 bg-base-50 dark:bg-base-900">
133
+ <canvas bind:this={canvas} class="absolute inset-0 h-full w-full opacity-50"></canvas>
134
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const Fireflies: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Fireflies = ReturnType<typeof Fireflies>;
3
+ export default Fireflies;