@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.
- package/README.md +35 -0
- package/dist/events/DatePicker.svelte +222 -0
- package/dist/events/DatePicker.svelte.d.ts +12 -0
- package/dist/events/DateTimePicker.svelte +112 -0
- package/dist/events/DateTimePicker.svelte.d.ts +10 -0
- package/dist/events/EventAttendees.svelte +199 -0
- package/dist/events/EventAttendees.svelte.d.ts +19 -0
- package/dist/events/EventCard.svelte +148 -0
- package/dist/events/EventCard.svelte.d.ts +8 -0
- package/dist/events/EventComments.svelte +98 -0
- package/dist/events/EventComments.svelte.d.ts +8 -0
- package/dist/events/EventEditor.svelte +540 -0
- package/dist/events/EventEditor.svelte.d.ts +18 -0
- package/dist/events/EventRsvp.svelte +178 -0
- package/dist/events/EventRsvp.svelte.d.ts +16 -0
- package/dist/events/EventView.svelte +488 -0
- package/dist/events/EventView.svelte.d.ts +23 -0
- package/dist/events/ImageDropper.svelte +66 -0
- package/dist/events/ImageDropper.svelte.d.ts +7 -0
- package/dist/events/Map.svelte +36 -0
- package/dist/events/Map.svelte.d.ts +8 -0
- package/dist/events/PostToBlueskyModal.svelte +246 -0
- package/dist/events/PostToBlueskyModal.svelte.d.ts +22 -0
- package/dist/events/ShareModal.svelte +174 -0
- package/dist/events/ShareModal.svelte.d.ts +23 -0
- package/dist/events/StarterPackButton.svelte +72 -0
- package/dist/events/StarterPackButton.svelte.d.ts +10 -0
- package/dist/events/ThemeApply.svelte +30 -0
- package/dist/events/ThemeApply.svelte.d.ts +7 -0
- package/dist/events/ThemeBackground.svelte +33 -0
- package/dist/events/ThemeBackground.svelte.d.ts +7 -0
- package/dist/events/ThemePicker.svelte +104 -0
- package/dist/events/ThemePicker.svelte.d.ts +7 -0
- package/dist/events/ThumbnailPresets.svelte +71 -0
- package/dist/events/ThumbnailPresets.svelte.d.ts +11 -0
- package/dist/events/TimePicker.svelte +117 -0
- package/dist/events/TimePicker.svelte.d.ts +10 -0
- package/dist/events/TimezonePicker.svelte +67 -0
- package/dist/events/TimezonePicker.svelte.d.ts +6 -0
- package/dist/events/VodPlayer.svelte +154 -0
- package/dist/events/VodPlayer.svelte.d.ts +14 -0
- package/dist/events/VodTranscript.svelte +75 -0
- package/dist/events/VodTranscript.svelte.d.ts +8 -0
- package/dist/events/atproto-helpers.d.ts +21 -0
- package/dist/events/atproto-helpers.js +61 -0
- package/dist/events/cal/helper.d.ts +1 -0
- package/dist/events/cal/helper.js +20 -0
- package/dist/events/cal/ical.d.ts +22 -0
- package/dist/events/cal/ical.js +188 -0
- package/dist/events/cal/sanitize.d.ts +3 -0
- package/dist/events/cal/sanitize.js +25 -0
- package/dist/events/conference.d.ts +65 -0
- package/dist/events/conference.js +61 -0
- package/dist/events/contrail.d.ts +49 -0
- package/dist/events/contrail.js +12 -0
- package/dist/events/date-format.d.ts +22 -0
- package/dist/events/date-format.js +43 -0
- package/dist/events/editor/LinkedRecordsPicker.svelte +104 -0
- package/dist/events/editor/LinkedRecordsPicker.svelte.d.ts +12 -0
- package/dist/events/editor/LinksSection.svelte +154 -0
- package/dist/events/editor/LinksSection.svelte.d.ts +13 -0
- package/dist/events/editor/LocationSection.svelte +163 -0
- package/dist/events/editor/LocationSection.svelte.d.ts +8 -0
- package/dist/events/editor/RecurringModal.svelte +313 -0
- package/dist/events/editor/RecurringModal.svelte.d.ts +30 -0
- package/dist/events/editor/ThemeSection.svelte +24 -0
- package/dist/events/editor/ThemeSection.svelte.d.ts +7 -0
- package/dist/events/editor/ThumbnailSection.svelte +181 -0
- package/dist/events/editor/ThumbnailSection.svelte.d.ts +13 -0
- package/dist/events/editor/adapter.d.ts +73 -0
- package/dist/events/editor/adapter.js +9 -0
- package/dist/events/editor/location.d.ts +16 -0
- package/dist/events/editor/location.js +50 -0
- package/dist/events/editor/save.d.ts +43 -0
- package/dist/events/editor/save.js +168 -0
- package/dist/events/editor/types.d.ts +39 -0
- package/dist/events/editor/types.js +9 -0
- package/dist/events/event-media.d.ts +4 -0
- package/dist/events/event-media.js +4 -0
- package/dist/events/event-types.d.ts +79 -0
- package/dist/events/event-types.js +11 -0
- package/dist/events/event-view/AddToCalendarButton.svelte +31 -0
- package/dist/events/event-view/AddToCalendarButton.svelte.d.ts +9 -0
- package/dist/events/event-view/EventBadges.svelte +20 -0
- package/dist/events/event-view/EventBadges.svelte.d.ts +7 -0
- package/dist/events/event-view/EventDateBlock.svelte +43 -0
- package/dist/events/event-view/EventDateBlock.svelte.d.ts +7 -0
- package/dist/events/event-view/EventHostedBy.svelte +61 -0
- package/dist/events/event-view/EventHostedBy.svelte.d.ts +16 -0
- package/dist/events/event-view/EventLinksList.svelte +24 -0
- package/dist/events/event-view/EventLinksList.svelte.d.ts +9 -0
- package/dist/events/event-view/EventLocationBlock.svelte +62 -0
- package/dist/events/event-view/EventLocationBlock.svelte.d.ts +7 -0
- package/dist/events/event-view/EventLocationMap.svelte +66 -0
- package/dist/events/event-view/EventLocationMap.svelte.d.ts +7 -0
- package/dist/events/event-view/ExternalRsvpNotice.svelte +34 -0
- package/dist/events/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
- package/dist/events/event-view/StreamPlacePlayer.svelte +209 -0
- package/dist/events/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
- package/dist/events/event-view/format.d.ts +10 -0
- package/dist/events/event-view/format.js +112 -0
- package/dist/events/event-view/locations.d.ts +36 -0
- package/dist/events/event-view/locations.js +243 -0
- package/dist/events/event-view/plyr-icons.d.ts +3 -0
- package/dist/events/event-view/plyr-icons.js +46 -0
- package/dist/events/linked-records.d.ts +18 -0
- package/dist/events/linked-records.js +11 -0
- package/dist/events/profile-url.d.ts +3 -0
- package/dist/events/profile-url.js +11 -0
- package/dist/events/schedule/ConferenceTimetable.svelte +340 -0
- package/dist/events/schedule/ConferenceTimetable.svelte.d.ts +22 -0
- package/dist/events/schedule/DaySchedule.svelte +298 -0
- package/dist/events/schedule/DaySchedule.svelte.d.ts +26 -0
- package/dist/events/schedule/ScheduleEventCell.svelte +166 -0
- package/dist/events/schedule/ScheduleEventCell.svelte.d.ts +25 -0
- package/dist/events/schedule/schedule-utils.d.ts +44 -0
- package/dist/events/schedule/schedule-utils.js +166 -0
- package/dist/events/starter-pack-url.d.ts +3 -0
- package/dist/events/starter-pack-url.js +16 -0
- package/dist/events/theme.d.ts +9 -0
- package/dist/events/theme.js +36 -0
- package/dist/events/themes/Blobs.svelte +64 -0
- package/dist/events/themes/Blobs.svelte.d.ts +18 -0
- package/dist/events/themes/Butterflies.svelte +183 -0
- package/dist/events/themes/Butterflies.svelte.d.ts +3 -0
- package/dist/events/themes/Fireflies.svelte +134 -0
- package/dist/events/themes/Fireflies.svelte.d.ts +3 -0
- package/dist/events/themes/Kaleidoscope.svelte +183 -0
- package/dist/events/themes/Kaleidoscope.svelte.d.ts +3 -0
- package/dist/events/themes/Matrix.svelte +153 -0
- package/dist/events/themes/Matrix.svelte.d.ts +3 -0
- package/dist/events/themes/Stars.svelte +98 -0
- package/dist/events/themes/Stars.svelte.d.ts +3 -0
- package/dist/events/thumbnails/designs.d.ts +18 -0
- package/dist/events/thumbnails/designs.js +316 -0
- package/dist/events/view-data.d.ts +38 -0
- package/dist/events/view-data.js +1 -0
- package/dist/events-data.d.ts +15 -0
- package/dist/events-data.js +9 -0
- package/dist/events-editor.d.ts +2 -0
- package/dist/events-editor.js +2 -0
- package/dist/events.d.ts +19 -0
- package/dist/events.js +17 -0
- 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,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>
|