cursor-park 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/objects.ts ADDED
@@ -0,0 +1,369 @@
1
+ import { library } from './assets/svgs';
2
+ import { MouseCursor } from './cursor';
3
+ import { ReactiveElement, css, property, type PropertyValues } from './reactive-element';
4
+ import { clamp, inlineSVG } from './utils';
5
+
6
+ export class CursorObject extends ReactiveElement {
7
+ static acquiredObject: CursorObject | null = null;
8
+
9
+ #cursor: MouseCursor | null = null;
10
+
11
+ get cursor() {
12
+ return this.#cursor;
13
+ }
14
+
15
+ constructor() {
16
+ super();
17
+
18
+ this.addEventListener('click', this.#onAcquireClick);
19
+ }
20
+
21
+ acquireCursor(x: number, y: number) {
22
+ if (this.#cursor === null) return;
23
+
24
+ CursorObject.acquiredObject?.releaseCursor();
25
+ CursorObject.acquiredObject = this;
26
+
27
+ this.#cursor.x = x;
28
+ this.#cursor.y = y;
29
+ this.appendChild(this.#cursor);
30
+ console.log('acquire', this);
31
+ }
32
+
33
+ releaseCursor() {
34
+ console.log('release', this);
35
+ if (this.cursor == null) return;
36
+
37
+ this.cursor.action = '';
38
+ this.cursor.remove();
39
+ this.#cursor = null;
40
+ CursorObject.acquiredObject = null;
41
+ }
42
+
43
+ #onAcquireClick = (event: PointerEvent) => {
44
+ event.preventDefault();
45
+ event.stopPropagation();
46
+ event.stopImmediatePropagation();
47
+
48
+ if (this.#cursor) {
49
+ this.releaseCursor();
50
+ } else {
51
+ this.#cursor = MouseCursor.self;
52
+
53
+ // compute point of click relative to the objects bounding box (which has to account for scroll)
54
+ const rect = this.getBoundingClientRect();
55
+ const x = event.pageX - (rect.x + window.scrollX);
56
+ const y = event.pageY - (rect.y + window.scrollY);
57
+
58
+ this.acquireCursor(x, y);
59
+ }
60
+ };
61
+ }
62
+
63
+ export class CursorBench extends CursorObject {
64
+ static override tagName = 'cursor-bench';
65
+
66
+ static override styles = css`
67
+ :host {
68
+ display: block;
69
+ position: relative;
70
+ aspect-ratio: 2.04;
71
+ width: 85px;
72
+ user-select: none;
73
+ }
74
+
75
+ img {
76
+ width: 100%;
77
+ height: 100%;
78
+ }
79
+
80
+ ::slotted(mouse-cursor) {
81
+ top: 50% !important;
82
+ translate: 0 -72%;
83
+ }
84
+ `;
85
+
86
+ #img = document.createElement('img');
87
+
88
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
89
+ const root = super.createRenderRoot();
90
+
91
+ const url = new URL('./assets/bench.png', import.meta.url);
92
+ this.#img.src = url.toString();
93
+ this.#img.alt = 'A tiny metal casted bench.';
94
+
95
+ root.append(this.#img, document.createElement('slot'));
96
+
97
+ return root;
98
+ }
99
+
100
+ override acquireCursor(x: number, y: number): void {
101
+ super.acquireCursor(x, y);
102
+
103
+ // Shift the cursor over by 1/3 because the sprite is slightly bigger than it's outline.
104
+ this.cursor!.x = clamp(0, x, this.offsetWidth) - this.cursor!.offsetWidth / 3;
105
+ this.cursor!.y = 0;
106
+ this.cursor!.action = 'sitting';
107
+
108
+ document.addEventListener('keydown', this.#onKeydown);
109
+ document.addEventListener('keyup', this.#onKeyup);
110
+ }
111
+
112
+ override releaseCursor(): void {
113
+ document.removeEventListener('keydown', this.#onKeydown);
114
+ document.removeEventListener('keyup', this.#onKeyup);
115
+
116
+ super.releaseCursor();
117
+ }
118
+
119
+ #onKeydown = (event: KeyboardEvent) => {
120
+ if (this.cursor === null) return;
121
+
122
+ if (event.code === 'ArrowLeft') {
123
+ event.preventDefault();
124
+ if (this.cursor.x > 3) this.#animateCursor(-2);
125
+ } else if (event.code === 'ArrowRight') {
126
+ event.preventDefault();
127
+ if (this.cursor.x + this.cursor.offsetWidth <= this.offsetWidth) this.#animateCursor(2);
128
+ } else if (event.code === 'ArrowUp') {
129
+ event.preventDefault();
130
+ this.cursor.action = 'sitting-forwards';
131
+ } else if (event.code === 'ArrowDown') {
132
+ event.preventDefault();
133
+ this.cursor.action = 'sitting-backwards';
134
+ }
135
+ };
136
+
137
+ #onKeyup = (event: KeyboardEvent) => {
138
+ if (this.cursor === null) return;
139
+
140
+ if (event.code === 'ArrowUp' || event.code === 'ArrowDown') {
141
+ this.cursor.action = 'sitting';
142
+ }
143
+ };
144
+
145
+ #animateCursor(delta: number) {
146
+ if (this.cursor === null) return;
147
+
148
+ this.cursor.x += delta;
149
+ }
150
+ }
151
+
152
+ export class CursorLibrary extends CursorObject {
153
+ static override tagName = 'cursor-library';
154
+
155
+ static override styles = css`
156
+ :host {
157
+ display: block;
158
+ position: relative;
159
+ /* 20 x 38 */
160
+ aspect-ratio: 0.53;
161
+ height: 80px;
162
+ user-select: none;
163
+ }
164
+
165
+ click-zone {
166
+ display: block;
167
+ position: absolute;
168
+ height: 100%;
169
+ width: 150%;
170
+ top: -25%;
171
+ right: 100%;
172
+ background: rgba(0, 0, 0, 0.15);
173
+ border-radius: 5px;
174
+ opacity: 0;
175
+ transition: opacity 0.2s ease-out;
176
+ }
177
+
178
+ img {
179
+ height: 100%;
180
+ width: 100%;
181
+ }
182
+
183
+ ::slotted(mouse-cursor) {
184
+ translate: -50% 0%;
185
+ }
186
+
187
+ div {
188
+ font-size: 16px;
189
+ pointer-events: none;
190
+ text-align: center;
191
+ opacity: 0;
192
+ position: absolute;
193
+ top: -75px;
194
+ bottom: -75px;
195
+ left: 125%;
196
+ background: #deeade;
197
+ border-radius: 4px;
198
+ transition: opacity 200ms ease-out;
199
+ box-sizing: border-box;
200
+ overflow: auto;
201
+ z-index: 2;
202
+ box-shadow: 3px 4px 8px 0px rgba(0, 0, 0, 0.5);
203
+ z-index: calc(Infinity);
204
+ padding: 12px 64px 4px 20px;
205
+
206
+ align-items: flex-end;
207
+ display: flex !important;
208
+ gap: 2px;
209
+ list-style-type: none;
210
+
211
+ ::slotted(a),
212
+ a {
213
+ writing-mode: vertical-rl;
214
+ text-orientation: mixed;
215
+ border-radius: 3px;
216
+ overflow: hidden;
217
+ color: white;
218
+ padding: 9px 4px;
219
+ line-height: 1.5;
220
+ text-decoration: none;
221
+ cursor: unset;
222
+ box-shadow: 1px 1px 3px 0px rgba(0, 0, 0, 0.5);
223
+ }
224
+
225
+ ::slotted(a:hover),
226
+ a:hover {
227
+ text-decoration: underline;
228
+ }
229
+
230
+ ::slotted(a:nth-child(even)),
231
+ a:nth-child(even) {
232
+ background: #3ebc1b;
233
+ }
234
+
235
+ ::slotted(a:nth-child(odd)),
236
+ a:nth-child(odd) {
237
+ background: #ff0000;
238
+ }
239
+
240
+ ::slotted(a:nth-child(2n + 1)),
241
+ a:nth-child(2n + 1) {
242
+ rotate: 2deg;
243
+ translate: -2px;
244
+ margin-left: 2px;
245
+ background: #286e75;
246
+ }
247
+
248
+ ::slotted(a:nth-child(3n)),
249
+ a:nth-child(3n) {
250
+ rotate: -1deg;
251
+ translate: 1px;
252
+ margin-left: 1px;
253
+ background: #d3d382;
254
+ color: black;
255
+ }
256
+
257
+ ::slotted(a:first-child),
258
+ a:first-child {
259
+ rotate: 8deg;
260
+ translate: -8px;
261
+ margin-left: 8px;
262
+ background: #ff0000;
263
+ }
264
+
265
+ ::slotted(a:last-of-type),
266
+ a:last-of-type {
267
+ rotate: -8deg;
268
+ translate: 8px;
269
+ background: #d3d382;
270
+ }
271
+ }
272
+ `;
273
+
274
+ #img = document.createElement('img');
275
+ #books = document.createElement('div');
276
+ #slot = document.createElement('slot');
277
+ #clickZone = document.createElement('click-zone');
278
+
279
+ @property({ type: String, reflect: true }) src = '';
280
+
281
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
282
+ const root = super.createRenderRoot();
283
+
284
+ this.#books.appendChild(this.#slot);
285
+ this.#books.addEventListener('click', (e) => e.stopPropagation());
286
+
287
+ this.#img.src = inlineSVG(library());
288
+
289
+ const cursorSlot = document.createElement('slot');
290
+ cursorSlot.name = 'cursor';
291
+
292
+ root.append(cursorSlot, this.#clickZone, this.#img, this.#books);
293
+
294
+ return root;
295
+ }
296
+
297
+ protected override update(changedProperties: PropertyValues<this>): void {
298
+ super.update(changedProperties);
299
+
300
+ if (changedProperties.has('src')) {
301
+ if (this.src === '') return;
302
+
303
+ const url = URL.parse(this.src);
304
+
305
+ if (url?.hostname === 'semble.so') {
306
+ const [, profile, identifier, collections, rkey] = url.pathname.split('/');
307
+ console.log('url', profile, identifier, collections, rkey);
308
+
309
+ if (profile === 'profile' && identifier && collections === 'collections' && rkey) {
310
+ this.#loadSembleCollection(identifier, rkey);
311
+ }
312
+ }
313
+ }
314
+ }
315
+
316
+ async #loadSembleCollection(identifier: string, rkey: string) {
317
+ const response = await fetch(
318
+ `https://api.semble.so/xrpc/network.cosmik.collection.getByAtUri?handle=${identifier}&recordKey=${rkey}&page=1&limit=15`,
319
+ );
320
+
321
+ if (!response.ok) return;
322
+
323
+ const collection = await response.json();
324
+
325
+ const links = collection.urlCards.map(({ cardContent }: any) => ({
326
+ url: cardContent.url,
327
+ title: cardContent.title || cardContent.description,
328
+ }));
329
+
330
+ this.#createLinks(links);
331
+ }
332
+
333
+ #createLinks(links: { url: string; title: string }[]) {
334
+ const anchors = links.map((link) => {
335
+ const a = document.createElement('a');
336
+ a.href = link.url;
337
+ a.textContent = link.title.length < 60 ? link.title : link.title.slice(0, 57) + '...';
338
+ a.target = '_blank';
339
+ const size = -2.5 * link.title.length + 70;
340
+ a.style.fontSize = `${clamp(12, size, 18)}px`;
341
+ return a;
342
+ });
343
+ this.#slot.append(...anchors);
344
+ console.log(anchors);
345
+ }
346
+
347
+ override acquireCursor(x: number, y: number): void {
348
+ super.acquireCursor(x, y);
349
+ this.cursor!.slot = 'cursor';
350
+ this.cursor!.action = 'looking-down';
351
+ this.#books.style.opacity = '0.9';
352
+ this.#books.style.pointerEvents = 'all';
353
+ }
354
+
355
+ override releaseCursor(): void {
356
+ this.cursor!.slot = '';
357
+ this.#books.style.opacity = '0';
358
+ this.#books.style.pointerEvents = '';
359
+
360
+ super.releaseCursor();
361
+ }
362
+ }
363
+
364
+ declare global {
365
+ interface HTMLElementTagNameMap {
366
+ 'cursor-bench': CursorBench;
367
+ 'cursor-library': CursorLibrary;
368
+ }
369
+ }
@@ -0,0 +1,24 @@
1
+ import { ReactiveElement as RE } from '@lit/reactive-element';
2
+
3
+ export { css, type PropertyValues, unsafeCSS } from '@lit/reactive-element';
4
+ export { property } from '@lit/reactive-element/decorators.js';
5
+
6
+ export class ReactiveElement extends RE {
7
+ /** Defines the name of the custom element, must include a hyphen or it will error out when defined.
8
+ * Defaults to a kebab-case version of the PascalCase class name
9
+ */
10
+ static tagName = '';
11
+
12
+ /** Defines the custom element with the global CustomElementRegistry, ignored if called more than once. Errors if no tagName is defined or it doesn't include a hyphen. */
13
+ static define() {
14
+ if (!this.tagName)
15
+ this.tagName = this.name
16
+ .replace(/([A-Z])/g, '-$1')
17
+ .toLowerCase()
18
+ .slice(1);
19
+
20
+ if (customElements.get(this.tagName)) return;
21
+
22
+ customElements.define(this.tagName, this);
23
+ }
24
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,26 @@
1
+ const COLORS = [
2
+ '#447F59',
3
+ '#A10314',
4
+ '#FB546E',
5
+ '#8750C9',
6
+ '#E601B2',
7
+ '#c44601',
8
+ '#f57600',
9
+ '#0073e6',
10
+ '#054fb9',
11
+ '#5ba300',
12
+ '#89ce00',
13
+ '#0073e6',
14
+ '#e6308a',
15
+ '#009eb0',
16
+ '#b51963',
17
+ '#f57600',
18
+ '#00b4c5',
19
+ '#5928ed',
20
+ ];
21
+ const SCALES = [1.7, 1.8, 1.9, 2, 2.1, 2.2, 2.3];
22
+ export const CURSOR_COLOR = COLORS[Math.floor(Math.random() * COLORS.length)];
23
+ export const CURSOR_SCALE = SCALES[Math.floor(Math.random() * SCALES.length)];
24
+ export const clamp = (min: number, value: number, max: number) => Math.min(Math.max(value, min), max);
25
+ export const inlineSVG = (svg: string) => `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
26
+ export const convertSVGIntoCssURL = (svg: string) => `url('${inlineSVG(svg)}')`;