Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221<script lang="ts"> import "#lib/components/landing/features/agents.css"; import { play } from "#lib/components/landing/sound.ts"; import type { Component } from "svelte"; import { prefersReducedMotion, Spring } from "svelte/motion"; import type { SvelteHTMLElements } from "svelte/elements"; import ArrowDownRight from "$icon/arrow-down-right"; import ArrowUpRight from "$icon/arrow-up-right"; import Bot from "$icon/bot"; import GitCommit from "$icon/git-commit-horizontal"; import GitPullRequest from "$icon/git-pull-request"; import ShieldCheck from "$icon/shield-check"; import UserRound from "$icon/user-round"; import { Live } from "#lib/components/landing/live.svelte.ts";
/* * Designed for humans and agents: a person's profile and an agent's, as the same plate * laid flat in iso, each with the profile page's activity chart — a stat per month, the * months joined by curves, green rising and red falling. Pointing at a month (or * tapping it) moves the highlight to it on both charts at once, so they read side by * side. Once it's on screen the plates rise, the curves draw and the stats land. */
type Icon = Component<SvelteHTMLElements["svg"]>;
interface Metric { icon: Icon; label: string; /** The band of the chart its points keep to, top and bottom. */ band: [number, number]; }
const METRICS: Metric[] = [ { icon: GitCommit, label: "commits", band: [12, 36] }, { icon: GitPullRequest, label: "pull requests", band: [50, 72] } ];
const PROFILES = [ { kind: "human", label: "A person's profile", values: [ [34, 52, 41, 63], [6, 9, 7, 11] ] }, { kind: "agent", label: "An agent's profile", values: [ [88, 140, 121, 176], [14, 22, 26, 19] ] } ] as const; // the chart: four months on the plate's 268px, as the profile page's six on its 988px const WIDTH = 268; const COLUMN = WIDTH / 4; const INSET = 8;
const now = new Date(); const MONTHS = [3, 2, 1, 0].map((ago) => new Intl.DateTimeFormat("en", { month: "short", timeZone: "UTC" }).format( new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - ago, 1)) ) );
interface Point { x: number; y: number; value: number; rising: boolean; }
const pointsOf = (values: readonly number[], [top, bottom]: [number, number]): Point[] => { const min = Math.min(...values); const max = Math.max(...values); return values.map((value, i) => ({ x: i * COLUMN + INSET, y: bottom - ((value - min) / (max - min)) * (bottom - top), value, rising: i === 0 || value >= values[i - 1] })); };
// the profile page's curves: a lead-in from the left edge, an s-curve between months // coloured by whether it rose, and a tail out to the right edge const segmentsOf = (points: Point[]) => [ { d: `M 0 ${points[0].y} L ${points[0].x} ${points[0].y}`, rising: points[1].rising }, ...points.slice(1).map((next, i) => { const at = points[i]; const c = (next.x - at.x) / 2; return { d: `M ${at.x} ${at.y} C ${at.x + c} ${at.y}, ${next.x - c} ${next.y}, ${next.x} ${next.y}`, rising: next.rising }; }), { d: `M ${points.at(-1)!.x} ${points.at(-1)!.y} L ${WIDTH} ${points.at(-1)!.y}`, rising: points.at(-1)!.rising } ];
const CHARTS = PROFILES.map((profile) => ({ ...profile, metrics: METRICS.map((metric, m) => { const points = pointsOf(profile.values[m], metric.band); return { ...metric, points, segments: segmentsOf(points) }; }) }));
/** The stage's width: the 460px group scales down to fit a narrower one. */ let width: number | undefined = $state(); /** The month highlighted on both charts: the current one, as on the profile page. */ let month = $state(3); const tint = new Spring(3, { stiffness: 0.12, damping: 0.55 }); const choose = (c: number) => { month = c; tint.set(c, { instant: prefersReducedMotion.current }); }; let playing = $state(false); const live = new Live(0.5); const reveal = $props.id(); $effect(() => { if (live.running) playing = true; });</script>
<div class="ag" bind:clientWidth={width} {@attach live.watch}> <div class="ag-frame" style:--z={width ? Math.min(1, width / 460) : 1}> <div class={["ag-plane composite", playing && "on"]} style:--t={tint.current}> {#each CHARTS as chart, p (chart.kind)} <section class={["ag-p glint", `is-${chart.kind}`]} style:--p={p} aria-label={chart.label} > <header class="ag-head"> <span class="ag-av" aria-hidden="true"> {#if chart.kind === "agent"}<Bot />{:else}<UserRound />{/if} </span> <span class="ag-who" aria-hidden="true"><i></i><i></i></span> <span class="ag-title">Activity</span> </header> <div class="ag-chart"> <span class="ag-sel" aria-hidden="true"></span> <svg class="ag-curves" viewBox="0 0 {WIDTH} 84" aria-hidden="true"> {#each chart.metrics as metric, m (metric.label)} {#each metric.segments as segment, s (s)} <path d={segment.d} pathLength="1" class={segment.rising ? "up" : "down"} style:--s={s + m} /> {/each} {/each} </svg> {#each MONTHS as name, c (name)} <button type="button" class="ag-col" style:--c={c} aria-pressed={c === month} aria-label={name} onpointerenter={() => choose(c)} onfocus={() => choose(c)} onclick={() => { choose(c); play("tap"); }} > <span class="ag-month">{name}</span> </button> {#each chart.metrics as metric (metric.label)} {@const point = metric.points[c]} {@const Stat = metric.icon} <span class={["ag-stat", c === month && "is-current"]} style:--c={c} style:--d={Math.abs(c - month)} style:left="{point.x}px" style:top="{point.y}px" aria-hidden="true" > <Stat /> {#if point.rising} <ArrowUpRight class="ag-up" /> {:else} <ArrowDownRight class="ag-down" /> {/if} </span> {/each} {/each} </div> </section> {/each} <svg class="ag-thread" viewBox="0 0 300 360" aria-hidden="true"> <mask id={reveal}> <path class="ag-reveal" d="M -30 180 L -30 240 C -30 270, -30 290, 0 290" pathLength="1" /> </mask> <path class="ag-tie" d="M 0 70 C -30 70, -30 90, -30 120 L -30 180" pathLength="1" /> <path class="ag-dash" d="M -30 180 L -30 240 C -30 270, -30 290, 0 290" mask="url(#{reveal})" /> </svg> <span class="ag-knot" role="img" aria-label="vouched for"><ShieldCheck /></span> </div> </div></div>