@debuffjp/share 0.0.0-stage → 0.9.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 CHANGED
@@ -1,3 +1,28 @@
1
- # Temporary Holding Version
1
+ # @debuffjp/share
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Make your game easy to pass around: clips of the last few seconds, the system share sheet, and "beat my run" challenge links. No dependencies, and each export can be used on its own.
4
+
5
+ ```ts
6
+ import { ClipRecorder, challengeLink, clipFile, readChallenge, share } from "@debuffjp/share";
7
+
8
+ // A friend opened a challenge link: play their seed
9
+ const challenge = readChallenge<{ seed: number; score: number }>();
10
+ if (challenge) game.random.reseed(challenge.seed);
11
+
12
+ // Keep the last ~12 seconds while a run is going
13
+ const clips = new ClipRecorder(game.canvas, { seconds: 12 });
14
+ clips.start();
15
+
16
+ // On a "share" button (share needs a click or key press)
17
+ const clip = await clips.clip();
18
+ await share({
19
+ text: `I scored ${score}. Beat it?`,
20
+ url: challengeLink({ seed, score }),
21
+ files: clip ? [clipFile(clip, "my-run")] : [],
22
+ });
23
+ ```
24
+
25
+ - `share()` opens the share sheet where there is one and copies the text and link otherwise. It returns `"shared"`, `"copied"`, `"cancelled"` or `"unsupported"` so you can show the right toast.
26
+ - `ClipRecorder` runs two staggered `MediaRecorder`s, so start it when a run begins rather than on page load. `ClipRecorder.supported()` tells you whether to show a clip button at all.
27
+ - Challenge data is visible in the URL. Put game state in it, never anything personal.
28
+ - `screenshot(canvas)` and `download(blob, name)` cover "save image" buttons.
package/dist/clip.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ export interface ClipOptions {
2
+ /** Longest clip, in seconds. Clips are always at least half this long once recording has run that long. Default 12. */
3
+ seconds?: number;
4
+ /** Capture frame rate. Default 30. */
5
+ fps?: number;
6
+ /** Video bitrate in bits per second. Default 4 Mbps. */
7
+ bitrate?: number;
8
+ /** Preferred container types, first supported wins. Default webm (VP9, then VP8), then mp4. */
9
+ mimeTypes?: string[];
10
+ }
11
+ export interface Clip {
12
+ blob: Blob;
13
+ mimeType: string;
14
+ /** Suggested file extension without the dot, e.g. `webm`. */
15
+ extension: string;
16
+ /** Approximate length in seconds. */
17
+ seconds: number;
18
+ }
19
+ /**
20
+ * Rolling recorder for the last few seconds of a canvas, for "share that moment" buttons.
21
+ *
22
+ * A WebM file can't be trimmed by dropping its early chunks, so two recorders run staggered by half the
23
+ * window and restart every window. `clip()` keeps whichever has run longer, so a clip is between half and
24
+ * all of `seconds` long. That costs two encoders while recording: start it when a run begins, not on load.
25
+ *
26
+ * ```ts
27
+ * const clips = new ClipRecorder(game.canvas);
28
+ * clips.start();
29
+ * // later, on a big moment or a "share" button
30
+ * const clip = await clips.clip();
31
+ * if (clip) await share({ text: "12x combo!", files: [clipFile(clip, "squish")] });
32
+ * ```
33
+ */
34
+ export declare class ClipRecorder {
35
+ private readonly canvas;
36
+ readonly mimeType: string | undefined;
37
+ private readonly seconds;
38
+ private readonly fps;
39
+ private readonly bitrate;
40
+ private stream;
41
+ private slots;
42
+ private timers;
43
+ private now;
44
+ constructor(canvas: HTMLCanvasElement, options?: ClipOptions);
45
+ /** First supported clip type, or `undefined` where this browser can't record a canvas. */
46
+ static supported(types?: string[]): string | undefined;
47
+ get recording(): boolean;
48
+ /** Start recording. Returns false (and does nothing) where clips aren't supported. */
49
+ start(): boolean;
50
+ /** Stop recording and drop anything buffered. */
51
+ stop(): void;
52
+ /** The recent past as a video, or `undefined` if not recording. Recording carries on afterwards. */
53
+ clip(): Promise<Clip | undefined>;
54
+ private cycle;
55
+ }
56
+ /** Wrap a clip as a `File` for `share({ files })` or a download. */
57
+ export declare function clipFile(clip: Clip, name?: string): File;
58
+ //# sourceMappingURL=clip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clip.d.ts","sourceRoot":"","sources":["../src/clip.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,uHAAuH;IACvH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB;AAUD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,YAAY;IAWrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAVzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,KAAK,CAAgD;IAC7D,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,GAAG,CAAe;IAE1B,YACmB,MAAM,EAAE,iBAAiB,EAC1C,OAAO,GAAE,WAAgB,EAO1B;IAED,0FAA0F;IAC1F,MAAM,CAAC,SAAS,CAAC,KAAK,GAAE,MAAM,EAAkB,GAAG,MAAM,GAAG,SAAS,CAIpE;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,sFAAsF;IACtF,KAAK,IAAI,OAAO,CAOf;IAED,iDAAiD;IACjD,IAAI,IAAI,IAAI,CAOX;IAED,oGAAoG;IAC9F,IAAI,IAAI,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC,CActC;IAED,OAAO,CAAC,KAAK;CAoBd;AAED,oEAAoE;AACpE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,SAAS,GAAG,IAAI,CAExD"}
package/dist/clip.js ADDED
@@ -0,0 +1,115 @@
1
+ const DEFAULT_TYPES = ["video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm", "video/mp4"];
2
+ /**
3
+ * Rolling recorder for the last few seconds of a canvas, for "share that moment" buttons.
4
+ *
5
+ * A WebM file can't be trimmed by dropping its early chunks, so two recorders run staggered by half the
6
+ * window and restart every window. `clip()` keeps whichever has run longer, so a clip is between half and
7
+ * all of `seconds` long. That costs two encoders while recording: start it when a run begins, not on load.
8
+ *
9
+ * ```ts
10
+ * const clips = new ClipRecorder(game.canvas);
11
+ * clips.start();
12
+ * // later, on a big moment or a "share" button
13
+ * const clip = await clips.clip();
14
+ * if (clip) await share({ text: "12x combo!", files: [clipFile(clip, "squish")] });
15
+ * ```
16
+ */
17
+ export class ClipRecorder {
18
+ canvas;
19
+ mimeType;
20
+ seconds;
21
+ fps;
22
+ bitrate;
23
+ stream;
24
+ slots = [undefined, undefined];
25
+ timers = [];
26
+ now;
27
+ constructor(canvas, options = {}) {
28
+ this.canvas = canvas;
29
+ this.seconds = options.seconds ?? 12;
30
+ this.fps = options.fps ?? 30;
31
+ this.bitrate = options.bitrate ?? 4_000_000;
32
+ this.mimeType = ClipRecorder.supported(options.mimeTypes);
33
+ this.now = () => performance.now();
34
+ }
35
+ /** First supported clip type, or `undefined` where this browser can't record a canvas. */
36
+ static supported(types = DEFAULT_TYPES) {
37
+ if (typeof MediaRecorder === "undefined" || typeof HTMLCanvasElement === "undefined")
38
+ return undefined;
39
+ if (!("captureStream" in HTMLCanvasElement.prototype))
40
+ return undefined;
41
+ return types.find((t) => MediaRecorder.isTypeSupported(t));
42
+ }
43
+ get recording() {
44
+ return this.stream !== undefined;
45
+ }
46
+ /** Start recording. Returns false (and does nothing) where clips aren't supported. */
47
+ start() {
48
+ if (this.stream)
49
+ return true;
50
+ if (!this.mimeType)
51
+ return false;
52
+ this.stream = this.canvas.captureStream(this.fps);
53
+ this.cycle(0);
54
+ this.timers.push(setTimeout(() => this.cycle(1), (this.seconds * 1000) / 2));
55
+ return true;
56
+ }
57
+ /** Stop recording and drop anything buffered. */
58
+ stop() {
59
+ for (const t of this.timers)
60
+ clearTimeout(t);
61
+ this.timers = [];
62
+ for (const slot of this.slots)
63
+ if (slot && slot.recorder.state !== "inactive")
64
+ slot.recorder.stop();
65
+ this.slots = [undefined, undefined];
66
+ for (const track of this.stream?.getTracks() ?? [])
67
+ track.stop();
68
+ this.stream = undefined;
69
+ }
70
+ /** The recent past as a video, or `undefined` if not recording. Recording carries on afterwards. */
71
+ async clip() {
72
+ const at = this.now();
73
+ const live = this.slots.filter((s) => s !== undefined);
74
+ if (!live.length || !this.mimeType)
75
+ return undefined;
76
+ const oldest = live.reduce((a, b) => (a.started <= b.started ? a : b));
77
+ const index = this.slots.indexOf(oldest);
78
+ const mimeType = this.mimeType;
79
+ const blob = await new Promise((resolve) => {
80
+ oldest.recorder.onstop = () => resolve(new Blob(oldest.chunks, { type: mimeType.split(";")[0] ?? mimeType }));
81
+ oldest.recorder.stop();
82
+ });
83
+ // Restart the slot we took so the rolling window keeps going.
84
+ if (this.stream)
85
+ this.cycle(index);
86
+ return { blob, mimeType, extension: mimeType.includes("mp4") ? "mp4" : "webm", seconds: (at - oldest.started) / 1000 };
87
+ }
88
+ cycle(index) {
89
+ if (!this.stream || !this.mimeType)
90
+ return;
91
+ const previous = this.slots[index];
92
+ if (previous && previous.recorder.state !== "inactive") {
93
+ previous.recorder.onstop = null;
94
+ previous.recorder.stop();
95
+ }
96
+ const recorder = new MediaRecorder(this.stream, { mimeType: this.mimeType, videoBitsPerSecond: this.bitrate });
97
+ const slot = { recorder, chunks: [], started: this.now() };
98
+ recorder.ondataavailable = (e) => {
99
+ if (e.data.size)
100
+ slot.chunks.push(e.data);
101
+ };
102
+ recorder.start(1000);
103
+ this.slots[index] = slot;
104
+ const timer = setTimeout(() => {
105
+ this.timers = this.timers.filter((t) => t !== timer);
106
+ this.cycle(index);
107
+ }, this.seconds * 1000);
108
+ this.timers.push(timer);
109
+ }
110
+ }
111
+ /** Wrap a clip as a `File` for `share({ files })` or a download. */
112
+ export function clipFile(clip, name = "clip") {
113
+ return new File([clip.blob], `${name}.${clip.extension}`, { type: clip.blob.type });
114
+ }
115
+ //# sourceMappingURL=clip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clip.js","sourceRoot":"","sources":["../src/clip.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { type Clip, type ClipOptions, ClipRecorder, clipFile } from "./clip.js";
2
+ export { CHALLENGE_PARAM, type ChallengeData, challengeLink, readChallenge } from "./link.js";
3
+ export { download, type ShareData, type ShareResult, screenshot, share } from "./share.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,KAAK,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC9F,OAAO,EAAE,QAAQ,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { ClipRecorder, clipFile } from "./clip.js";
2
+ export { CHALLENGE_PARAM, challengeLink, readChallenge } from "./link.js";
3
+ export { download, screenshot, share } from "./share.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,YAAY,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,eAAe,EAAsB,aAAa,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC9F,OAAO,EAAE,QAAQ,EAAoC,UAAU,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC"}
package/dist/link.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /** Anything JSON can hold: a seed, a level id, a score to beat. */
2
+ export type ChallengeData = Record<string, string | number | boolean | null>;
3
+ /** Query parameter that carries a challenge. */
4
+ export declare const CHALLENGE_PARAM = "challenge";
5
+ /**
6
+ * A link that opens this game with `data` attached, for "beat my run" sharing. Pair it with a seeded
7
+ * `game.random` so the friend plays the same run.
8
+ *
9
+ * ```ts
10
+ * const url = challengeLink({ seed: game.random.seed, score: 4200 });
11
+ * ```
12
+ *
13
+ * The data is visible in the URL, so put game state in it, never anything personal.
14
+ */
15
+ export declare function challengeLink(data: ChallengeData, base?: string): string;
16
+ /** The challenge this page was opened with, or `undefined`. Malformed links read as no challenge. */
17
+ export declare function readChallenge<T extends ChallengeData = ChallengeData>(href?: string): T | undefined;
18
+ //# sourceMappingURL=link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;AAE7E,gDAAgD;AAChD,eAAO,MAAM,eAAe,cAAc,CAAC;AAc3C;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,GAAE,MAAsB,GAAG,MAAM,CAKvF;AAED,qGAAqG;AACrG,wBAAgB,aAAa,CAAC,CAAC,SAAS,aAAa,GAAG,aAAa,EAAE,IAAI,GAAE,MAAsB,GAAG,CAAC,GAAG,SAAS,CASlH"}
package/dist/link.js ADDED
@@ -0,0 +1,43 @@
1
+ /** Query parameter that carries a challenge. */
2
+ export const CHALLENGE_PARAM = "challenge";
3
+ function encode(text) {
4
+ const bytes = new TextEncoder().encode(text);
5
+ let binary = "";
6
+ for (const b of bytes)
7
+ binary += String.fromCharCode(b);
8
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
9
+ }
10
+ function decode(value) {
11
+ const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
12
+ return new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
13
+ }
14
+ /**
15
+ * A link that opens this game with `data` attached, for "beat my run" sharing. Pair it with a seeded
16
+ * `game.random` so the friend plays the same run.
17
+ *
18
+ * ```ts
19
+ * const url = challengeLink({ seed: game.random.seed, score: 4200 });
20
+ * ```
21
+ *
22
+ * The data is visible in the URL, so put game state in it, never anything personal.
23
+ */
24
+ export function challengeLink(data, base = location.href) {
25
+ const url = new URL(base);
26
+ url.hash = "";
27
+ url.searchParams.set(CHALLENGE_PARAM, encode(JSON.stringify(data)));
28
+ return url.toString();
29
+ }
30
+ /** The challenge this page was opened with, or `undefined`. Malformed links read as no challenge. */
31
+ export function readChallenge(href = location.href) {
32
+ const value = new URL(href).searchParams.get(CHALLENGE_PARAM);
33
+ if (!value)
34
+ return undefined;
35
+ try {
36
+ const data = JSON.parse(decode(value));
37
+ return data && typeof data === "object" && !Array.isArray(data) ? data : undefined;
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ //# sourceMappingURL=link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.js","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":"AAGA,gDAAgD;AAChD,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAE3C,SAAS,MAAM,CAAC,IAAY;IAC1B,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7C,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACxD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACjF,CAAC;AAED,SAAS,MAAM,CAAC,KAAa;IAC3B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IACjE,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACnF,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,IAAmB,EAAE,IAAI,GAAW,QAAQ,CAAC,IAAI;IAC7E,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;IAC1B,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC;IACd,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpE,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;AACxB,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,aAAa,CAA0C,IAAI,GAAW,QAAQ,CAAC,IAAI;IACjG,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAC9D,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,IAAI,CAAC;QACH,MAAM,IAAI,GAAY,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAChD,OAAO,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,IAAU,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
@@ -0,0 +1,20 @@
1
+ export interface ShareData {
2
+ title?: string;
3
+ text?: string;
4
+ /** Defaults to the current page. */
5
+ url?: string;
6
+ /** Clips or screenshots. Dropped where the share sheet can't take files. */
7
+ files?: File[];
8
+ }
9
+ /** How a `share()` call ended. */
10
+ export type ShareResult = "shared" | "copied" | "cancelled" | "unsupported";
11
+ /**
12
+ * Share with the system share sheet where there is one (phones, most desktops), otherwise copy the text
13
+ * and link to the clipboard. Call it from a click or key handler: browsers only allow it after user input.
14
+ */
15
+ export declare function share(data: ShareData): Promise<ShareResult>;
16
+ /** Save a blob (a clip, a screenshot) as a file download. */
17
+ export declare function download(blob: Blob, filename: string): void;
18
+ /** The canvas as it is now, as a PNG. Call it right after a frame renders (e.g. in `onRender`). */
19
+ export declare function screenshot(canvas: HTMLCanvasElement): Promise<Blob | undefined>;
20
+ //# sourceMappingURL=share.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;CAChB;AAED,kCAAkC;AAClC,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,aAAa,CAAC;AAE5E;;;GAGG;AACH,wBAAsB,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAyBjE;AAED,6DAA6D;AAC7D,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAO3D;AAED,mGAAmG;AACnG,wBAAgB,UAAU,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC,CAE/E"}
package/dist/share.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Share with the system share sheet where there is one (phones, most desktops), otherwise copy the text
3
+ * and link to the clipboard. Call it from a click or key handler: browsers only allow it after user input.
4
+ */
5
+ export async function share(data) {
6
+ const url = data.url ?? location.href;
7
+ const nav = typeof navigator === "undefined" ? undefined : navigator;
8
+ if (nav?.share) {
9
+ const payload = { url };
10
+ if (data.title !== undefined)
11
+ payload.title = data.title;
12
+ if (data.text !== undefined)
13
+ payload.text = data.text;
14
+ if (data.files?.length && nav.canShare?.({ files: data.files }))
15
+ payload.files = data.files;
16
+ try {
17
+ await nav.share(payload);
18
+ return "shared";
19
+ }
20
+ catch (err) {
21
+ if (err instanceof DOMException && err.name === "AbortError")
22
+ return "cancelled";
23
+ // Some browsers expose share() but refuse it (permissions policy, iframes): fall through to copy.
24
+ }
25
+ }
26
+ if (nav?.clipboard?.writeText) {
27
+ try {
28
+ await nav.clipboard.writeText([data.text, url].filter(Boolean).join(" "));
29
+ return "copied";
30
+ }
31
+ catch {
32
+ // Clipboard blocked too.
33
+ }
34
+ }
35
+ return "unsupported";
36
+ }
37
+ /** Save a blob (a clip, a screenshot) as a file download. */
38
+ export function download(blob, filename) {
39
+ const href = URL.createObjectURL(blob);
40
+ const a = document.createElement("a");
41
+ a.href = href;
42
+ a.download = filename;
43
+ a.click();
44
+ setTimeout(() => URL.revokeObjectURL(href), 0);
45
+ }
46
+ /** The canvas as it is now, as a PNG. Call it right after a frame renders (e.g. in `onRender`). */
47
+ export function screenshot(canvas) {
48
+ return new Promise((resolve) => canvas.toBlob((blob) => resolve(blob ?? undefined), "image/png"));
49
+ }
50
+ //# sourceMappingURL=share.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share.js","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"AAYA;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,IAAe;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC;IACtC,MAAM,GAAG,GAAG,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;IACrE,IAAI,GAAG,EAAE,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAc,EAAE,GAAG,EAAE,CAAC;QACnC,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;YAAE,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACzD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;YAAE,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtD,IAAI,IAAI,CAAC,KAAK,EAAE,MAAM,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;YAAE,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QAC5F,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACzB,OAAO,QAAQ,CAAC;QAClB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,YAAY,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY;gBAAE,OAAO,WAAW,CAAC;YACjF,kGAAkG;QACpG,CAAC;IACH,CAAC;IACD,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1E,OAAO,QAAQ,CAAC;QAClB,CAAC;QAAC,MAAM,CAAC;YACP,yBAAyB;QAC3B,CAAC;IACH,CAAC;IACD,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,QAAQ,CAAC,IAAU,EAAE,QAAgB;IACnD,MAAM,IAAI,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;IACtC,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC;IACd,CAAC,CAAC,QAAQ,GAAG,QAAQ,CAAC;IACtB,CAAC,CAAC,KAAK,EAAE,CAAC;IACV,UAAU,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,UAAU,CAAC,MAAyB;IAClD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,IAAI,SAAS,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC;AACpG,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@debuffjp/share",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.9.0",
4
+ "description": "Clips, share sheets and challenge links for debuff games",
5
+ "keywords": [
6
+ "game",
7
+ "gamedev",
8
+ "game-engine",
9
+ "2d",
10
+ "html5-game",
11
+ "browser-game",
12
+ "debuff",
13
+ "share",
14
+ "clip",
15
+ "social",
16
+ "web-share"
17
+ ],
18
+ "license": "MIT",
19
+ "homepage": "https://debuff.jp",
20
+ "bugs": "https://github.com/debuffjp/debuff/issues",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/debuffjp/debuff.git",
24
+ "directory": "packages/share"
25
+ },
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "README.md"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsc -b"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }