diff --git a/DESIGN.md b/DESIGN.md index c71f3f6c..8433592b 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -347,6 +347,9 @@ Rules for every texture: | Skill | Name, then owner, then the curator note or the curator count | | Collection | Name, description, curator, then the curator note or the first Skills | | Curator | Avatar, name, handle with counts, description, then the first Skills | +| Demo | The demo's picture under a band: the lockup, then "What /{name} made" and the recording line. Its own frame, below | + +**Demo card.** A demo page shares what the Agent made, so its card is a picture first, and `OgLayout` does not frame it. `OgImage/Demo.takumi.vue` puts the film's poster frame, or the page's first screen, in a window that fills the card below a band. The band has the lockup on the left. On the right, it has "What /{name} made" and the recording line. It has no Braille names field: the picture is the content. Rose inside the picture is depicted content, so the lockup's dot stays the card's one rose element. ### Motifs diff --git a/app/components/OgImage/Demo.takumi.vue b/app/components/OgImage/Demo.takumi.vue new file mode 100644 index 00000000..b6326c68 --- /dev/null +++ b/app/components/OgImage/Demo.takumi.vue @@ -0,0 +1,83 @@ + + + diff --git a/app/utils/home-demos.ts b/app/utils/home-demos.ts index c28e9270..25429a99 100644 --- a/app/utils/home-demos.ts +++ b/app/utils/home-demos.ts @@ -22,7 +22,7 @@ export interface HomeDemoItem { model: string /** `YYYY-MM-DD`. */ recordedAt: string - shots: { src: string, width: number, height: number, alt: string, viewport: 'desktop' | 'mobile' }[] + shots: HomeDemoShot[] /** A Skill whose output is a film. The poster is a JPG frame; `src` is a muted H.264 MP4. */ /** The sandboxed output page of a page demo; null for a film. */ liveUrl: string | null @@ -37,7 +37,15 @@ export interface HomeDemoVideo { durationSeconds: number } -export type HomeDemoShot = HomeDemoItem['shots'][number] +export interface HomeDemoShot { + src: string + width: number + height: number + alt: string + viewport: 'desktop' | 'mobile' + /** The first screen, when the shot runs longer than one. */ + poster: { src: string, width: number, height: number } | null +} /** Fewer than this reads as a broken feature, as with trending. */ export const HOME_DEMOS_MIN = 3 @@ -104,6 +112,26 @@ export function demoScreens(shot: HomeDemoShot): number { return Math.max(1, Math.round(shot.height / screenHeight)) } +export interface DemoPicture { + src: string + width: number + height: number +} + +/** + * The picture a demo page's social card shows: a film's poster frame, or the + * first screen of the desktop page. A long page never serves whole, since a + * 6000px shot would shrink to a strip. Undefined when the demo has neither; + * the page keeps its text card then. + */ +export function demoSocialPicture(demo: HomeDemoItem): DemoPicture | undefined { + if (demo.video) + return { src: demo.video.poster, width: demo.video.width, height: demo.video.height } + const shot = demo.shots.find(shot => shot.viewport === 'desktop') + const screen = shot?.poster ?? (shot && demoScreens(shot) === 1 ? shot : undefined) + return screen && { src: screen.src, width: screen.width, height: screen.height } +} + /** The Skill as `SkillCard` reads it, so a demo names its author the way every Skill embed does. */ export function demoCardSkill(demo: HomeDemoItem): SkillCardSkill { return { diff --git a/layers/marketing/app/pages/skills/demos/[...demo].vue b/layers/marketing/app/pages/skills/demos/[...demo].vue index 8dd6cf33..0ec770cd 100644 --- a/layers/marketing/app/pages/skills/demos/[...demo].vue +++ b/layers/marketing/app/pages/skills/demos/[...demo].vue @@ -1,7 +1,7 @@