Something went wrong. Try again.
[PRE-ALPHA] an open source web-based 2D/3D graphing calculator cartesium.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441<script lang="ts"> import { resolve } from '$app/paths'; import { onMount } from 'svelte';
import { calculatorUrl } from '#lib/navigation/calculator-routes.js'; import { activateGraphEmbed, observeGraphEmbedActivation } from '#lib/embed/coordinator.js'; import { docsPreviewUrl } from '#lib/embed/preview.js'; import { GRAPH_EMBED_PROTOCOL_VERSION, isGraphEmbedViewerMessage, type GraphEmbedHostMessage, type GraphEmbedViewport } from '#lib/embed/protocol.js'; import { findGraphPreset } from '#lib/document/presets.js';
let { title, preset, document: documentPayload, previewUrl, description = '', expressions }: { title: string; preset?: string; document?: string; previewUrl?: string; description?: string; expressions?: string[]; } = $props();
let frame = $state<HTMLIFrameElement>(); let active = $state(false); let expanded = $state(false); let ready = $state(false); let error = $state(''); let rememberedViewport: GraphEmbedViewport | null = null; const id = `graph-embed-${crypto.randomUUID()}`;
const fragment = $derived(new URLSearchParams(documentPayload ? { doc: documentPayload } : { preset: preset ?? 'hello' }).toString());
const embedUrl = $derived(resolve(`embed#${fragment}`)); const openUrl = $derived(calculatorUrl(resolve(''), '2d', '', `#${fragment}`)); const restingPreviewUrl = $derived(previewUrl ?? docsPreviewUrl(preset));
const expressionSources = $derived( expressions ?? (preset ? findGraphPreset(preset)?.expressions.map(({ source }) => source) : []) );
onMount(() => { const stopObserving = observeGraphEmbedActivation((activeId) => { if (activeId === id || !active) return; active = false; ready = false; error = ''; });
const receiveMessage = (event: MessageEvent) => { if (event.origin !== window.location.origin || event.source !== frame?.contentWindow) return;
if (!isGraphEmbedViewerMessage(event.data)) return;
if (event.data.type === 'cartesium:embed:ready') { ready = true; error = ''; postExpandedState();
if (rememberedViewport) { post({ type: 'cartesium:embed:set-viewport', version: GRAPH_EMBED_PROTOCOL_VERSION, viewport: rememberedViewport }); } } else if (event.data.type === 'cartesium:embed:viewport-changed') { rememberedViewport = event.data.viewport; } else { error = event.data.message; } };
const handlePopState = () => { if (expanded) { expanded = false; postExpandedState(); } };
const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape' && expanded) closeExpanded(); };
window.addEventListener('message', receiveMessage); window.addEventListener('popstate', handlePopState); window.addEventListener('keydown', handleKeyDown);
return () => { stopObserving(); window.removeEventListener('message', receiveMessage); window.removeEventListener('popstate', handlePopState); window.removeEventListener('keydown', handleKeyDown); }; });
function activate() { activateGraphEmbed(id); active = true; ready = false; error = ''; }
function expand() { if (!active) activate(); expanded = true; window.history.pushState({ ...window.history.state, cartesiumEmbed: id }, '');
if (ready) postExpandedState(); }
function closeExpanded() { if (!expanded) return;
if (window.history.state?.cartesiumEmbed === id) { window.history.back(); } else { expanded = false; postExpandedState(); } }
function postExpandedState() { post({ type: 'cartesium:embed:set-expanded', version: GRAPH_EMBED_PROTOCOL_VERSION, expanded }); }
function reset() { rememberedViewport = null; post({ type: 'cartesium:embed:reset-viewport', version: GRAPH_EMBED_PROTOCOL_VERSION }); }
function post(message: GraphEmbedHostMessage) { frame?.contentWindow?.postMessage(message, window.location.origin); }</script>
<figure class:expanded class="graph-embed"> {#if !expanded} <figcaption> <strong>{title}</strong> {#if description}<span>{description}</span>{/if} </figcaption> {#if expressionSources?.length} <pre class="expression-source"><code>{expressionSources.join('\n')}</code></pre> {/if} {/if} <div class="viewport"> {#if active} <iframe bind:this={frame} src={embedUrl} title={`Interactive graph: ${title}`} sandbox="allow-scripts allow-same-origin" allow="fullscreen" ></iframe> <div class="toolbar" aria-label={`Controls for ${title}`}> {#if expanded} <button type="button" onclick={closeExpanded} aria-label="Return to documentation"> <span aria-hidden="true">←</span> <span class="wide-label">Back to documentation</span> </button> {/if} {#if !expanded} <strong>{title}</strong> {/if} <button type="button" onclick={reset}>Reset</button> {#if !expanded} <button type="button" onclick={expand} aria-label={`Expand ${title}`}>Expand</button> {/if} <a href={openUrl} target="_blank" rel="noreferrer">Edit</a> </div> {#if !ready && !error}<p class="loading">Loading graph…</p>{/if} {#if error}<p class="error">{error}</p>{/if} {:else} {#if restingPreviewUrl} <img src={restingPreviewUrl} alt={`Preview of ${title}`} loading="lazy" /> {:else} <div class="placeholder" aria-hidden="true"> <svg viewBox="0 0 120 72"> <path d="M8 36h104M60 7v58"></path>
<path class="curve" d="M8 52c18-46 34-46 52 0s34 46 52 0" ></path> </svg> </div> {/if} <button type="button" class="activate" onclick={activate}> <span>Explore {title}</span> <small>Click to load the interactive graph</small> </button> {/if} </div></figure>
<style> .graph-embed { --embed-border: #cbd5e1; --embed-panel: rgb(255 255 255 / 94%); position: relative; min-width: 0; margin: 1.25rem 0; }
.viewport { position: relative; overflow: hidden; aspect-ratio: 16 / 10; min-height: 240px; border: 1px solid var(--embed-border); border-radius: 12px; background: #f8fafc; box-shadow: 0 12px 32px rgb(15 23 42 / 8%); }
iframe, img, .placeholder { display: block; width: 100%; height: 100%; border: 0; }
img { object-fit: cover; }
.placeholder { display: grid; place-items: center; background: linear-gradient(rgb(148 163 184 / 18%) 1px, transparent 1px), linear-gradient(90deg, rgb(148 163 184 / 18%) 1px, transparent 1px), #f8fafc; background-size: 32px 32px; }
.placeholder svg { width: min(68%, 440px); fill: none; stroke: #94a3b8; stroke-width: 1; }
.placeholder .curve { stroke: #2563eb; stroke-width: 3; }
.activate { position: absolute; inset: 0; display: grid; place-content: center; gap: 0.35rem; width: 100%; padding: 1rem; border: 0; background: rgb(15 23 42 / 18%); color: white; text-align: center; cursor: pointer; text-shadow: 0 1px 5px rgb(15 23 42 / 80%); }
.activate span { font-size: clamp(1.05rem, 2vw, 1.35rem); font-weight: 750; }
.activate small { font-size: 0.82rem; }
.activate:hover, .activate:focus-visible { background: rgb(37 99 235 / 26%); outline: 4px solid rgb(59 130 246 / 50%); outline-offset: -4px; }
.toolbar { position: absolute; z-index: 2; top: 10px; right: 10px; left: 10px; display: flex; align-items: center; gap: 0.45rem; pointer-events: none; }
.toolbar strong { overflow: hidden; margin-right: auto; padding: 0.38rem 0.65rem; border: 1px solid var(--embed-border); border-radius: 7px; background: var(--embed-panel); box-shadow: 0 2px 8px rgb(15 23 42 / 10%); text-overflow: ellipsis; white-space: nowrap; }
.toolbar button, .toolbar a { pointer-events: auto; padding: 0.38rem 0.65rem; border: 1px solid var(--embed-border); border-radius: 7px; background: var(--embed-panel); box-shadow: 0 2px 8px rgb(15 23 42 / 10%); color: #1e293b; font: inherit; font-size: 0.82rem; text-decoration: none; cursor: pointer; }
.toolbar button:hover, .toolbar button:focus-visible, .toolbar a:hover, .toolbar a:focus-visible { border-color: #3b82f6; outline: 2px solid rgb(147 197 253 / 65%); }
.loading, .error { position: absolute; right: 12px; bottom: 12px; margin: 0; padding: 0.4rem 0.6rem; border-radius: 6px; background: var(--embed-panel); color: #475569; font-size: 0.78rem; }
.error { color: #be123c; }
figcaption { display: grid; gap: 0.2rem; padding: 0 0.2rem 0.55rem; color: #334155; }
figcaption span { color: #64748b; font-size: 0.9rem; line-height: 1.45; }
.expression-source { margin: 0 0 0.65rem; padding: 0.75rem 0.9rem; overflow-x: auto; border: 1px solid #dbe4ef; border-radius: 8px; background: #f8fafc; color: #0f172a; font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; font-size: 0.86rem; line-height: 1.5; }
.expression-source code { padding: 0; background: transparent; font: inherit; }
.graph-embed.expanded { position: fixed; z-index: 1000; inset: 0; margin: 0; background: #fff; }
.expanded .viewport { width: 100%; height: 100%; min-height: 0; aspect-ratio: auto; border: 0; border-radius: 0; box-shadow: none; }
@media (max-width: 560px) { .viewport { min-height: 210px; }
.toolbar { top: 7px; right: 7px; left: 7px; gap: 0.3rem; }
.toolbar strong { display: none; }
.toolbar button, .toolbar a { padding: 0.34rem 0.5rem; }
.wide-label { display: none; } }</style>