Skip to content

Commit d1c0793

Browse files
committed
feat: implement Share button with native Web Share and clip fallback
Adds an eject button emoji (⏏️) Share button next to the Screenshot button in the control panel. Integrates the native navigator.share API in src/Renderer.js to share both the canvas screenshot image as a File and the current deep-zoom URL, alongside the text '\''Look what I found in the Mandelbrot fractal with #fractious'\''. Implements an elegant fallback for desktop and other non-supporting browsers by alerting the user, copying the location URL directly to the clipboard, and triggering a screenshot download automatically.
1 parent e448087 commit d1c0793

6 files changed

Lines changed: 87 additions & 2 deletions

File tree

index.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,17 @@ <h1>Fractious</h1>
304304
<span aria-hidden="true">🖼️</span>
305305
</button>
306306
</fieldset>
307+
<fieldset>
308+
<button
309+
type="button"
310+
class="fullscreen"
311+
title="Share"
312+
aria-label="Share location and screenshot"
313+
id="btn-share"
314+
>
315+
<span aria-hidden="true">⏏️</span>
316+
</button>
317+
</fieldset>
307318
<fieldset>
308319
<a href="." class="" title="Reset" aria-label="Reset"
309320
><span aria-hidden="true">🔁</span></a

src/Fractious.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,14 @@ export class Fractious {
172172
}
173173
}
174174

175+
requestShare() {
176+
this.state.shareRequested = true;
177+
if (!this.state.isFrameScheduled) {
178+
this.state.isFrameScheduled = true;
179+
requestAnimationFrame(this.frame);
180+
}
181+
}
182+
175183
frame() {
176184
this.state.isFrameScheduled = false;
177185

src/InteractionManager.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,10 @@ export class InteractionManager {
310310
this.callbacks.onScreenshotRequest();
311311
});
312312

313+
this._bindBtn('btn-share', () => {
314+
this.callbacks.onShareRequest();
315+
});
316+
313317
const btnFullscreen = document.getElementById('btn-fullscreen');
314318
if (btnFullscreen) {
315319
btnFullscreen.onclick = () => {

src/Renderer.js

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,10 +400,65 @@ export class Renderer {
400400
}
401401
}
402402

403+
_handleShare(state) {
404+
if (state.shareRequested && state.currentPass >= state.totalPasses) {
405+
state.shareRequested = false;
406+
407+
this.canvas.toBlob((blob) => {
408+
if (!blob) return;
409+
const file = new File([blob], 'mandelbrot-fractious.png', {
410+
type: 'image/png',
411+
});
412+
413+
const shareData = {
414+
title: 'Fractious Mandelbrot',
415+
text: 'Look what I found in the Mandelbrot fractal with #fractious',
416+
url: window.location.href,
417+
};
418+
419+
if (navigator.canShare && navigator.canShare({ files: [file] })) {
420+
shareData.files = [file];
421+
}
422+
423+
if (navigator.share) {
424+
navigator.share(shareData).catch((err) => {
425+
console.error('Error sharing:', err);
426+
});
427+
} else {
428+
// Fallback if navigator.share is not supported (e.g. some desktop browsers)
429+
// We can copy the link to clipboard and download the screenshot, or alert the user
430+
alert(
431+
'Web Share is not supported in this browser. Downloading screenshot and copying link to clipboard!',
432+
);
433+
434+
// Copy link to clipboard
435+
navigator.clipboard
436+
.writeText(window.location.href)
437+
.then(() => {
438+
// Download screenshot
439+
const url = URL.createObjectURL(blob);
440+
const link = document.createElement('a');
441+
link.download = 'mandelbrot-fractious.png';
442+
link.href = url;
443+
link.click();
444+
setTimeout(() => URL.revokeObjectURL(url), 1000);
445+
})
446+
.catch((err) => {
447+
console.error('Error copying link:', err);
448+
});
449+
}
450+
}, 'image/png');
451+
}
452+
}
453+
403454
render(config, state) {
404455
this._calculatePassesAndResize(config, state);
405456

406-
if (state.currentPass >= state.totalPasses && !state.screenshotRequested) {
457+
if (
458+
state.currentPass >= state.totalPasses &&
459+
!state.screenshotRequested &&
460+
!state.shareRequested
461+
) {
407462
return false; // No more passes needed
408463
}
409464

@@ -418,8 +473,13 @@ export class Renderer {
418473

419474
this._updateBackgroundCanvas(state);
420475
this._handleScreenshot(state);
476+
this._handleShare(state);
421477

422-
return state.currentPass < state.totalPasses || state.screenshotRequested;
478+
return (
479+
state.currentPass < state.totalPasses ||
480+
state.screenshotRequested ||
481+
state.shareRequested
482+
);
423483
}
424484

425485
onSubmittedWorkDone() {

src/State.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export function createDefaultState() {
2323

2424
isFrameScheduled: false,
2525
screenshotRequested: false,
26+
shareRequested: false,
2627

2728
currentPass: 0,
2829
totalPasses: 1,

src/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const interactionCallbacks = {
3131
onRequestRender: () => app.requestRender(),
3232
onResize: () => app.handleResize(),
3333
onScreenshotRequest: () => app.requestScreenshot(),
34+
onShareRequest: () => app.requestShare(),
3435
};
3536

3637
const interactionManager = new InteractionManager(

0 commit comments

Comments
 (0)