Identity storyDesign system
VisualSite

Identity story

Dot animation of domains, email addresses, names, and tickers resolving to a brand.

npx context-ds add story-identity
On the site

Usage

Use it for

Resolving an identity to a brand on a site card. Plays only on screen; shows the seal with reduced motion.

Avoid

  • Stories as the main content of a page

Dependencies: dot-engine, story-freshness

Example

examples/story-identity.tsx
import { Card } from "@/components/ui/card";import { IdentityStory } from "@/components/visuals/stories/identity-story";export function Site() {  return (    <Card variant="ring" className="min-h-[22rem] w-full max-w-xl items-center justify-center">      <div className="relative w-full max-w-[26rem]">        <IdentityStory />      </div>    </Card>  );}

Source

components/visuals/stories/identity-story.tsx
'use client';import { IDENTITY_RESTING, IDENTITY_STORY_GRID } from '@/components/visuals/stories/identity-story-scenes';import { DotStory, type LoadDotStory } from '@/components/visuals/dot-engine/dot-story';const loadIdentityStory: LoadDotStory = async (stage) => {	const { createIdentityStoryPlayer } = await import('@/components/visuals/stories/identity-story-player');	return createIdentityStoryPlayer(stage);};/** * The "Resolve an identity" card's picture: a domain, an email, a name and a ticker melt into one another, then into * the seal of the brand they resolve to. With reduced motion it shows the seal. */export function IdentityStory({ className }: { className?: string }) {	return <DotStory grid={IDENTITY_STORY_GRID} resting={IDENTITY_RESTING} load={loadIdentityStory} className={className} />;}
components/visuals/stories/identity-story-player.ts
import { CAPTIONS, CHART_ARRIVES_DRAWN, CHART_DRAWN, CHART_STARTED, DOMAIN_GLOBE, ENVELOPE_PICTURE, IDENTITY_STORY_GRID, NAME_BADGE, SEAL_TICKED, SEAL_UNTICKED, paintChart, paintSeal } from '@/components/visuals/stories/identity-story-scenes';import { captionedBeat } from '@/components/visuals/dot-engine/story-caption';import { createBeatStoryPlayer } from '@/components/visuals/dot-engine/dot-story-beats';import { clamp01, easeOut } from '@/components/visuals/dot-engine/dot-story-cells';import type { DotStoryPlayer, DotStoryStage } from '@/components/visuals/dot-engine/dot-story-player';/** * One lookup, played as a loop: the four identifiers a brand can be found by melt into one another, a domain's * globe, an email's envelope, a name badge and a ticker's chart drawing its line; then the chart melts into a seal, * and its tick is cut as the brand they all resolve to is named. */const IDENTIFIER_HOLD_SECONDS = 1.7;const DRAWING = { startsAt: 0.2, seconds: 1 };const TICKING = { startsAt: 0.3, seconds: 0.5 };export function createIdentityStoryPlayer(stage: DotStoryStage): DotStoryPlayer {	return createBeatStoryPlayer(stage, {		grid: IDENTITY_STORY_GRID,		resting: { beat: 4, heldSeconds: 2 },		beats: [			captionedBeat({ picture: DOMAIN_GLOBE, caption: CAPTIONS.domain, holdSeconds: IDENTIFIER_HOLD_SECONDS, isTyped: true }),			captionedBeat({ picture: ENVELOPE_PICTURE, caption: CAPTIONS.email, holdSeconds: IDENTIFIER_HOLD_SECONDS, isTyped: true }),			captionedBeat({ picture: NAME_BADGE, caption: CAPTIONS.name, holdSeconds: IDENTIFIER_HOLD_SECONDS, isTyped: true }),			captionedBeat({				picture: CHART_STARTED,				departure: CHART_DRAWN,				caption: CAPTIONS.ticker,				holdSeconds: IDENTIFIER_HOLD_SECONDS + 0.3,				isTyped: true,				animate: (states, heldSeconds) => void paintChart(states, CHART_ARRIVES_DRAWN + (1 - CHART_ARRIVES_DRAWN) * easeOut(clamp01((heldSeconds - DRAWING.startsAt) / DRAWING.seconds))),			}),			captionedBeat({				picture: SEAL_UNTICKED,				departure: SEAL_TICKED,				caption: CAPTIONS.brand,				holdSeconds: 3.4,				animate: (states, heldSeconds) => void paintSeal(states, clamp01((heldSeconds - TICKING.startsAt) / TICKING.seconds)),			}),		],	});}
components/visuals/stories/identity-story-scenes.ts
import { GLOBE_AT_REST } from '@/components/visuals/stories/freshness-story-scenes';import { storyCaption } from '@/components/visuals/dot-engine/story-caption';import { DOT_ABSENT, DOT_FILLED, DOT_HOLLOW, type DotGrid } from '@/components/visuals/dot-engine/dot-morph-dots';import type { StoryPicture } from '@/components/visuals/dot-engine/dot-story-beats';import { cellsOfStates, distanceToSegment, paintShape, solidWithSpeckle, type DotShape, type GridPoint } from '@/components/visuals/dot-engine/dot-story-cells';/** * The "Resolve an identity" card's pictures, in the brand dot grid: the four things a brand can be looked up by, a * domain's globe, an email's envelope, a name badge and a ticker's chart, and the seal that says which brand it is. */// The ring pattern behind the picture already keeps clear of it, so the dots need no halo of their own.export const IDENTITY_STORY_GRID: DotGrid = { columns: 58, rows: 38, pitch: 10, dotRadius: 3.2, ringStroke: 1, haloRadius: 0 };const { columns: COLUMNS, rows: ROWS } = IDENTITY_STORY_GRID;const pictureOfStates = (states: Uint8Array): StoryPicture => ({ states, cells: cellsOfStates(IDENTITY_STORY_GRID, states) });const blank = () => new Uint8Array(COLUMNS * ROWS);const between = (value: number, from: number, to: number) => value >= from && value <= to;// Every picture stands in the same place, left of the words that say what it is.const CENTER = 16;const LINE_WIDTH = 0.6;export const DOMAIN_GLOBE = GLOBE_AT_REST;const ENVELOPE = { left: 3, right: 29, top: 8, bottom: 29, flapTo: { column: CENTER, row: 20 } };const isEnvelope: DotShape = (column, row) => between(column, ENVELOPE.left, ENVELOPE.right) && between(row, ENVELOPE.top, ENVELOPE.bottom);// How far down the flap's edge is in this column: it runs from each top corner to a point in the middle.const flapEdge = (column: number) => ENVELOPE.top + ((ENVELOPE.flapTo.row - ENVELOPE.top) * Math.min(column - ENVELOPE.left, ENVELOPE.right - column)) / (CENTER - ENVELOPE.left);export const ENVELOPE_PICTURE = pictureOfStates(	paintShape(IDENTITY_STORY_GRID, blank(), isEnvelope, (column, row) => {		const belowFlap = row - flapEdge(column);		if (Math.abs(belowFlap) < 0.8) return DOT_FILLED;		return belowFlap < 0 ? solidWithSpeckle(column, row) : DOT_HOLLOW;	}));const BADGE = { left: 4, right: 28, top: 7, bottom: 31, clip: { left: 13, right: 19, top: 3 }, header: 13, name: { top: 18, bottom: 21, left: 8, right: 24 }, line: { row: 25, left: 10, right: 22 } };const isBadge: DotShape = (column, row) => (between(column, BADGE.left, BADGE.right) && between(row, BADGE.top, BADGE.bottom)) || (between(column, BADGE.clip.left, BADGE.clip.right) && between(row, BADGE.clip.top, BADGE.top));// A name badge on its clip: a solid band across the top, the name in solid dots, a quieter line under it.export const NAME_BADGE = pictureOfStates(	paintShape(IDENTITY_STORY_GRID, blank(), isBadge, (column, row) => {		if (row <= BADGE.header) return DOT_FILLED;		if (between(row, BADGE.name.top, BADGE.name.bottom) && between(column, BADGE.name.left, BADGE.name.right)) return DOT_FILLED;		if (row === BADGE.line.row && between(column, BADGE.line.left, BADGE.line.right)) return DOT_ABSENT;		return DOT_HOLLOW;	}));const CHART = { left: 4, right: 29, top: 5, bottom: 31, lineWidth: 1 };const PRICES: GridPoint[] = [	{ column: 5, row: 27 },	{ column: 10, row: 21 },	{ column: 14, row: 24 },	{ column: 20, row: 14 },	{ column: 24, row: 17 },	{ column: 29, row: 7 },];const fromPriceLine = (column: number, row: number) => Math.min(...PRICES.slice(1).map((point, index) => distanceToSegment(column, row, PRICES[index], point)));const priceAt = (column: number) => {	const to = Math.max(1, PRICES.findIndex((point) => point.column >= column));	const from = PRICES[to - 1];	return from.row + ((PRICES[to].row - from.row) * (column - from.column)) / (PRICES[to].column - from.column);};/** A ticker's chart with `drawn` of its price line in, from the left: two axes, the line, and rings filling under it. */export function paintChart(states: Uint8Array, drawn: number) {	const drawnTo = CHART.left + drawn * (CHART.right - CHART.left);	for (let row = 0; row < ROWS; row++) {		for (let column = 0; column < COLUMNS / 2 + 2; column++) {			const cell = row * COLUMNS + column;			const isAxis = (column === CHART.left && between(row, CHART.top, CHART.bottom)) || (row === CHART.bottom && between(column, CHART.left, CHART.right));			if (isAxis) states[cell] = DOT_FILLED;			else if (column <= CHART.left || column > Math.min(drawnTo, CHART.right) || row >= CHART.bottom) states[cell] = DOT_ABSENT;			else if (fromPriceLine(column, row) < CHART.lineWidth) states[cell] = DOT_FILLED;			else states[cell] = row > priceAt(column) ? DOT_HOLLOW : DOT_ABSENT;		}	}	return states;}/** How much of the price line is already in when the chart arrives; an axis alone has too little in it to melt into. */export const CHART_ARRIVES_DRAWN = 0.35;export const CHART_STARTED = pictureOfStates(paintChart(blank(), CHART_ARRIVES_DRAWN));export const CHART_DRAWN = pictureOfStates(paintChart(blank(), 1));const SEAL = { center: { column: CENTER, row: 19 }, radius: 12, scallop: 1.4, scallops: 8, tick: { from: { column: 10, row: 19.5 }, corner: { column: 14.5, row: 24 }, to: { column: 22.5, row: 14 }, width: 1.3 } };const isSeal: DotShape = (column, row) => {	const across = column - SEAL.center.column;	const down = row - SEAL.center.row;	return Math.hypot(across, down) <= SEAL.radius + SEAL.scallop * Math.cos(SEAL.scallops * Math.atan2(down, across));};const isTick = (column: number, row: number) => Math.min(distanceToSegment(column, row, SEAL.tick.from, SEAL.tick.corner), distanceToSegment(column, row, SEAL.tick.corner, SEAL.tick.to)) < SEAL.tick.width;/** The seal, with `ticked` of its tick cut out of it in rings, from the tick's short stroke to the end of its long one. */export function paintSeal(states: Uint8Array, ticked: number) {	const tickedTo = SEAL.tick.from.column - 2 + ticked * (SEAL.tick.to.column + 4 - SEAL.tick.from.column);	return paintShape(IDENTITY_STORY_GRID, states, isSeal, (column, row) => {		if (isTick(column, row) && column <= tickedTo) return DOT_HOLLOW;		return DOT_FILLED;	});}export const SEAL_UNTICKED = pictureOfStates(paintSeal(blank(), 0));export const SEAL_TICKED = pictureOfStates(paintSeal(blank(), 1));const caption = (label: string, value: string, detail: string) => storyCaption(IDENTITY_STORY_GRID, 34, label, value, detail);export const CAPTIONS = {	domain: caption('look up by', 'domain', 'shopify.com'),	email: caption('or by', 'email', 'ana@shopify.com'),	name: caption('or by', 'name', '"Shopify"'),	ticker: caption('or by', 'ticker', 'SHOP'),	brand: caption('all four resolve to', 'Shopify', 'one brand profile'),};/** The resolved brand: what the picture shows before it plays, and instead of playing with reduced motion. */export const IDENTITY_RESTING = { ...SEAL_TICKED, texts: CAPTIONS.brand };

Reference: site src/components/sections/products/identity-story.tsx.