Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45e3da0527 | ||
|
|
6101a4f113 | ||
|
|
da8fe7f6f7 |
@@ -17,7 +17,6 @@ import {
|
||||
type ListLayerVersionsCommandInput,
|
||||
LogType,
|
||||
PublishLayerVersionCommand,
|
||||
ResourceConflictException,
|
||||
ResourceNotFoundException,
|
||||
UpdateFunctionConfigurationCommand,
|
||||
waitUntilFunctionActiveV2,
|
||||
@@ -984,7 +983,6 @@ export class LambdaDriver implements LogicFunctionDriver {
|
||||
|
||||
const canSkip =
|
||||
isDefined(lambdaExecutor) &&
|
||||
lambdaExecutor.Configuration?.State === 'Active' &&
|
||||
!flatApplication.isSdkLayerStale &&
|
||||
this.hasExpectedLayers({
|
||||
lambdaExecutor,
|
||||
@@ -1200,17 +1198,6 @@ export class LambdaDriver implements LogicFunctionDriver {
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof ResourceConflictException) {
|
||||
this.logger.warn(
|
||||
`Lambda function '${flatLogicFunction.id}' is not yet active (state conflict). Retrying after waiting for active state.`,
|
||||
);
|
||||
|
||||
throw new LogicFunctionException(
|
||||
`Function '${flatLogicFunction.id}' is not ready for invocation (currently updating or pending)`,
|
||||
LogicFunctionExceptionCode.LOGIC_FUNCTION_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof LogicFunctionException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -62,11 +62,19 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
|
||||
return { value: [] };
|
||||
});
|
||||
|
||||
const folders = (response.value as MicrosoftGraphFolder[]) || [];
|
||||
const rootFolderId = this.getRootFolderId(folders);
|
||||
const topLevelFolders =
|
||||
(response.value as MicrosoftGraphFolder[]) || [];
|
||||
|
||||
const allFolders = await this.fetchFoldersRecursively(
|
||||
microsoftClient,
|
||||
connectedAccount,
|
||||
topLevelFolders,
|
||||
);
|
||||
|
||||
const rootFolderId = this.getRootFolderId(topLevelFolders);
|
||||
const folderInfos: DiscoveredMessageFolder[] = [];
|
||||
|
||||
for (const folder of folders) {
|
||||
for (const folder of allFolders) {
|
||||
if (!folder.displayName) {
|
||||
continue;
|
||||
}
|
||||
@@ -111,6 +119,44 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchFoldersRecursively(
|
||||
microsoftClient: any,
|
||||
connectedAccount: Pick<ConnectedAccountEntity, 'id'>,
|
||||
folders: MicrosoftGraphFolder[],
|
||||
): Promise<MicrosoftGraphFolder[]> {
|
||||
const allFolders: MicrosoftGraphFolder[] = [...folders];
|
||||
|
||||
for (const folder of folders) {
|
||||
if (folder.childFolderCount && folder.childFolderCount > 0) {
|
||||
const childResponse = await microsoftClient
|
||||
.api(`/me/mailFolders/${folder.id}/childFolders`)
|
||||
.version('beta')
|
||||
.top(MESSAGING_MICROSOFT_MAIL_FOLDERS_LIST_MAX_RESULT)
|
||||
.get()
|
||||
.catch((error: Error) => {
|
||||
this.logger.error(
|
||||
`Connected account ${connectedAccount.id}: Error fetching child folders for ${folder.id}: ${error.message}`,
|
||||
);
|
||||
|
||||
return { value: [] };
|
||||
});
|
||||
|
||||
const childFolders =
|
||||
(childResponse.value as MicrosoftGraphFolder[]) || [];
|
||||
|
||||
const nestedFolders = await this.fetchFoldersRecursively(
|
||||
microsoftClient,
|
||||
connectedAccount,
|
||||
childFolders,
|
||||
);
|
||||
|
||||
allFolders.push(...nestedFolders);
|
||||
}
|
||||
}
|
||||
|
||||
return allFolders;
|
||||
}
|
||||
|
||||
private isSentFolder(standardFolder: StandardFolder | null): boolean {
|
||||
return standardFolder === StandardFolder.SENT;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 892 B |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<rect width="16" height="16" rx="2" fill="#000" />
|
||||
<path
|
||||
d="M2.147 9.383c-.027-.114.109-.186.192-.103L6.72 13.66c.083.083.011.219-.103.192a6.015 6.015 0 0 1-4.47-4.47ZM2 7.627a.119.119 0 0 0 .035.091l6.247 6.247a.119.119 0 0 0 .091.035c.285-.018.564-.055.836-.111a.117.117 0 0 0 .057-.198L2.31 6.734a.117.117 0 0 0-.198.057 6.007 6.007 0 0 0-.11.836ZM2.505 5.565a.119.119 0 0 0 .025.132l7.773 7.773a.118.118 0 0 0 .132.025c.215-.096.422-.203.623-.322a.118.118 0 0 0 .022-.185L3.012 4.92a.118.118 0 0 0-.185.022c-.119.2-.226.408-.322.623ZM3.519 4.169a.118.118 0 0 1-.005-.163 6.006 6.006 0 1 1 8.48 8.48.118.118 0 0 1-.163-.005L3.52 4.169Z"
|
||||
fill="#FFF"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 760 B |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 696 B |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 1003 B After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40"
|
||||
fill="none"
|
||||
>
|
||||
<rect width="40" height="40" rx="8" fill="#000000" />
|
||||
<path
|
||||
d="M6.822 14.174c0-2.435 1.996-4.411 4.456-4.411h8.574c.125 0 .241.075.293.19a.31.31 0 0 1-.056.344l-1.88 2.023c-.326.35-.787.552-1.27.552H11.3c-.738 0-1.338.594-1.338 1.325v3.336a.78.78 0 0 1-.783.777H7.61a.78.78 0 0 1-.783-.777v-3.36zM33.5 25.553c0 2.434-1.996 4.411-4.456 4.411h-3.642c-2.46 0-4.454-1.977-4.454-4.411v-6.315c0-.43.16-.842.456-1.16l2.124-2.285a.33.33 0 0 1 .355-.081.32.32 0 0 1 .205.295v9.527c0 .73.598 1.322 1.337 1.322h3.6a1.33 1.33 0 0 0 1.337-1.322V14.197c0-.73-.599-1.325-1.337-1.325H24.84c-.481 0-.938.201-1.265.547L11.088 26.856h7.503a.78.78 0 0 1 .784.778v1.552a.78.78 0 0 1-.784.778H8.481a1.655 1.655 0 0 1-1.662-1.644v-.824c0-.412.156-.809.44-1.114l13.999-15.06a4.9 4.9 0 0 1 3.594-1.56h4.189c2.46 0 4.454 1.977 4.454 4.412v11.379z"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 968 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
@@ -8,15 +8,32 @@ import type {
|
||||
import { SHARED_PEOPLE_AVATAR_URLS } from '@/lib/shared-asset-paths';
|
||||
|
||||
const PEOPLE_AVATAR_URLS = {
|
||||
craigFederighi:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-33.png',
|
||||
eddyCue: 'https://twentyhq.github.io/placeholder-images/people/image-18.png',
|
||||
jeffWilliams:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-22.png',
|
||||
anonymousIndira: SHARED_PEOPLE_AVATAR_URLS.anonymousIndira,
|
||||
anonymousLaura: SHARED_PEOPLE_AVATAR_URLS.anonymousLaura,
|
||||
brianChesky: SHARED_PEOPLE_AVATAR_URLS.brianChesky,
|
||||
benChestnut: SHARED_PEOPLE_AVATAR_URLS.benChestnut,
|
||||
chrisWanstrath: SHARED_PEOPLE_AVATAR_URLS.chrisWanstrath,
|
||||
craigFederighi: SHARED_PEOPLE_AVATAR_URLS.craigFederighi,
|
||||
darioAmodei: SHARED_PEOPLE_AVATAR_URLS.darioAmodei,
|
||||
dylanField: SHARED_PEOPLE_AVATAR_URLS.dylanField,
|
||||
eddyCue: SHARED_PEOPLE_AVATAR_URLS.eddyCue,
|
||||
ivanZhao: SHARED_PEOPLE_AVATAR_URLS.ivanZhao,
|
||||
jeffWilliams: SHARED_PEOPLE_AVATAR_URLS.jeffWilliams,
|
||||
joeGebbia: SHARED_PEOPLE_AVATAR_URLS.joeGebbia,
|
||||
katherineAdams: SHARED_PEOPLE_AVATAR_URLS.katherineAdams,
|
||||
philSchiller:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-14.png',
|
||||
timCook: 'https://twentyhq.github.io/placeholder-images/people/image-27.png',
|
||||
patrickCollison: SHARED_PEOPLE_AVATAR_URLS.patrickCollison,
|
||||
pingLi: SHARED_PEOPLE_AVATAR_URLS.pingLi,
|
||||
peterReinhardt: SHARED_PEOPLE_AVATAR_URLS.peterReinhardt,
|
||||
peterThiel: SHARED_PEOPLE_AVATAR_URLS.peterThiel,
|
||||
philSchiller: SHARED_PEOPLE_AVATAR_URLS.philSchiller,
|
||||
rayDamm: SHARED_PEOPLE_AVATAR_URLS.rayDamm,
|
||||
reidHoffman: SHARED_PEOPLE_AVATAR_URLS.reidHoffman,
|
||||
roelofBotha: SHARED_PEOPLE_AVATAR_URLS.roelofBotha,
|
||||
ryanRoslansky: SHARED_PEOPLE_AVATAR_URLS.ryanRoslansky,
|
||||
stewartButterfield: SHARED_PEOPLE_AVATAR_URLS.stewartButterfield,
|
||||
sundarPichai: SHARED_PEOPLE_AVATAR_URLS.sundarPichai,
|
||||
thomasDohmke: SHARED_PEOPLE_AVATAR_URLS.thomasDohmke,
|
||||
timCook: SHARED_PEOPLE_AVATAR_URLS.timCook,
|
||||
} as const;
|
||||
|
||||
const SALES_DASHBOARD_DATA: HeroDashboardDataType = {
|
||||
@@ -87,10 +104,10 @@ const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
|
||||
},
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Phil Schiller',
|
||||
tone: 'amber',
|
||||
name: 'Dario Amodei',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.philSchiller,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei,
|
||||
},
|
||||
rating: 2,
|
||||
date: 'Jul 1, 2023',
|
||||
@@ -100,8 +117,7 @@ const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
|
||||
shortLabel: 'D',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-11.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei,
|
||||
},
|
||||
recordId: 'OPP-1',
|
||||
},
|
||||
@@ -112,10 +128,10 @@ const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
|
||||
company: { type: 'entity', name: 'Figma', domain: 'figma.com' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Tim Cook',
|
||||
tone: 'teal',
|
||||
name: 'Dylan Field',
|
||||
tone: 'purple',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.timCook,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.dylanField,
|
||||
},
|
||||
rating: 2,
|
||||
date: 'Jul 12, 2023',
|
||||
@@ -125,8 +141,7 @@ const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
|
||||
shortLabel: 'D',
|
||||
tone: 'purple',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-31.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.dylanField,
|
||||
},
|
||||
recordId: 'OPP-2',
|
||||
},
|
||||
@@ -144,10 +159,10 @@ const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
|
||||
company: { type: 'entity', name: 'Notion', domain: 'notion.com' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Phil Schiller',
|
||||
tone: 'amber',
|
||||
name: 'Ivan Zhao',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.philSchiller,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao,
|
||||
},
|
||||
rating: 4,
|
||||
date: 'Jul 8, 2023',
|
||||
@@ -157,8 +172,7 @@ const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
|
||||
shortLabel: 'I',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-09.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao,
|
||||
},
|
||||
recordId: 'OPP-3',
|
||||
},
|
||||
@@ -176,10 +190,10 @@ const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
|
||||
company: { type: 'entity', name: 'Github', domain: 'github.com' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Jeff Williams',
|
||||
tone: 'blue',
|
||||
name: 'Chris Wanstrath',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.chrisWanstrath,
|
||||
},
|
||||
rating: 3,
|
||||
date: 'Jul 14, 2023',
|
||||
@@ -189,8 +203,7 @@ const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
|
||||
shortLabel: 'T',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-24.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.thomasDohmke,
|
||||
},
|
||||
recordId: 'OPP-4',
|
||||
},
|
||||
@@ -201,10 +214,10 @@ const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
|
||||
company: { type: 'entity', name: 'Stripe', domain: 'stripe.com' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Tim Cook',
|
||||
tone: 'teal',
|
||||
name: 'Patrick Collison',
|
||||
tone: 'blue',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.timCook,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.patrickCollison,
|
||||
},
|
||||
rating: 5,
|
||||
date: 'Jul 17, 2023',
|
||||
@@ -214,8 +227,7 @@ const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
|
||||
shortLabel: 'P',
|
||||
tone: 'blue',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-16.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.patrickCollison,
|
||||
},
|
||||
recordId: 'OPP-5',
|
||||
},
|
||||
@@ -226,10 +238,10 @@ const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
|
||||
company: { type: 'entity', name: 'Airbnb', domain: 'airbnb.com' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Eddy Cue',
|
||||
tone: 'gray',
|
||||
name: 'Joe Gebbia',
|
||||
tone: 'pink',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.eddyCue,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.joeGebbia,
|
||||
},
|
||||
rating: 3,
|
||||
date: 'Jul 15, 2023',
|
||||
@@ -239,8 +251,7 @@ const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
|
||||
shortLabel: 'B',
|
||||
tone: 'pink',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-01.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.brianChesky,
|
||||
},
|
||||
recordId: 'OPP-6',
|
||||
},
|
||||
@@ -268,10 +279,10 @@ const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
|
||||
},
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Craig Federighi',
|
||||
tone: 'purple',
|
||||
name: 'Ben Chestnut',
|
||||
tone: 'amber',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.benChestnut,
|
||||
},
|
||||
rating: 4,
|
||||
date: 'Jul 23, 2023',
|
||||
@@ -281,8 +292,7 @@ const OPPORTUNITY_KANBAN_PAGE: HeroKanbanPageDefinition = {
|
||||
shortLabel: 'R',
|
||||
tone: 'amber',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-28.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.anonymousLaura,
|
||||
},
|
||||
recordId: 'OPP-7',
|
||||
},
|
||||
@@ -373,21 +383,21 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
name: 'Anthropic',
|
||||
domain: 'anthropic.com',
|
||||
},
|
||||
url: { type: 'link', value: 'qonto.com' },
|
||||
url: { type: 'link', value: 'anthropic.com' },
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Jeff Williams',
|
||||
tone: 'blue',
|
||||
name: 'Dario Amodei',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei,
|
||||
},
|
||||
address: { type: 'text', value: '18 Rue De Navarin' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Phil Schiller',
|
||||
tone: 'amber',
|
||||
name: 'Dario Amodei',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.philSchiller,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei,
|
||||
},
|
||||
icp: { type: 'boolean', value: true },
|
||||
arr: { type: 'number', value: '$500,000' },
|
||||
@@ -399,8 +409,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'D',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-11.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei,
|
||||
},
|
||||
employees: { type: 'number', value: '612' },
|
||||
opportunities: {
|
||||
@@ -427,18 +436,18 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
url: { type: 'link', value: 'linkedin.com' },
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Craig Federighi',
|
||||
name: 'Reid Hoffman',
|
||||
tone: 'purple',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.reidHoffman,
|
||||
},
|
||||
address: { type: 'text', value: '1226 Moises Causeway' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Craig Federighi',
|
||||
tone: 'purple',
|
||||
name: 'Ryan Roslansky',
|
||||
tone: 'teal',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.ryanRoslansky,
|
||||
},
|
||||
icp: { type: 'boolean', value: false },
|
||||
arr: { type: 'number', value: '$1,000,000' },
|
||||
@@ -450,8 +459,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'R',
|
||||
tone: 'teal',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-26.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.ryanRoslansky,
|
||||
},
|
||||
employees: { type: 'number', value: '19,300' },
|
||||
opportunities: {
|
||||
@@ -474,18 +482,18 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
url: { type: 'link', value: 'slack.com' },
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Eddy Cue',
|
||||
tone: 'gray',
|
||||
name: 'Stewart Butterfield',
|
||||
tone: 'teal',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.eddyCue,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.stewartButterfield,
|
||||
},
|
||||
address: { type: 'text', value: '1316 Dameon Mountain' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Katherine Adams',
|
||||
tone: 'red',
|
||||
name: 'Stewart Butterfield',
|
||||
tone: 'teal',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.katherineAdams,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.stewartButterfield,
|
||||
},
|
||||
icp: { type: 'boolean', value: true },
|
||||
arr: { type: 'number', value: '$2,300,000' },
|
||||
@@ -497,8 +505,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'LJ',
|
||||
tone: 'pink',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-04.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.anonymousIndira,
|
||||
},
|
||||
employees: { type: 'number', value: '4,500' },
|
||||
opportunities: {
|
||||
@@ -533,10 +540,10 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
address: { type: 'text', value: '1162 Sammy Creek' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Phil Schiller',
|
||||
tone: 'amber',
|
||||
name: 'Ivan Zhao',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.philSchiller,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao,
|
||||
},
|
||||
icp: { type: 'boolean', value: false },
|
||||
arr: { type: 'number', value: '$750,000' },
|
||||
@@ -548,8 +555,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'I',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-09.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao,
|
||||
},
|
||||
employees: { type: 'number', value: '620' },
|
||||
opportunities: {
|
||||
@@ -580,10 +586,10 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
address: { type: 'text', value: '110 Oswald Junction' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Tim Cook',
|
||||
tone: 'teal',
|
||||
name: 'Dylan Field',
|
||||
tone: 'purple',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.timCook,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.dylanField,
|
||||
},
|
||||
icp: { type: 'boolean', value: true },
|
||||
arr: { type: 'number', value: '$3,500,000' },
|
||||
@@ -595,8 +601,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'D',
|
||||
tone: 'purple',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-31.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.dylanField,
|
||||
},
|
||||
employees: { type: 'number', value: '1,300' },
|
||||
opportunities: {
|
||||
@@ -624,18 +629,18 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
url: { type: 'link', value: 'github.com' },
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Jeff Williams',
|
||||
tone: 'blue',
|
||||
name: 'Chris Wanstrath',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.chrisWanstrath,
|
||||
},
|
||||
address: { type: 'text', value: '3891 Ranchview Drive' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Jeff Williams',
|
||||
tone: 'blue',
|
||||
name: 'Thomas Dohmke',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.thomasDohmke,
|
||||
},
|
||||
icp: { type: 'boolean', value: true },
|
||||
arr: { type: 'number', value: '$900,000' },
|
||||
@@ -647,8 +652,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'T',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-24.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.thomasDohmke,
|
||||
},
|
||||
employees: { type: 'number', value: '3,800' },
|
||||
opportunities: {
|
||||
@@ -671,18 +675,18 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
url: { type: 'link', value: 'airbnb.com' },
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Tim Cook',
|
||||
tone: 'teal',
|
||||
name: 'Joe Gebbia',
|
||||
tone: 'pink',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.timCook,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.joeGebbia,
|
||||
},
|
||||
address: { type: 'text', value: '4517 Washington Avenue' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Eddy Cue',
|
||||
tone: 'gray',
|
||||
name: 'Brian Chesky',
|
||||
tone: 'pink',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.eddyCue,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.brianChesky,
|
||||
},
|
||||
icp: { type: 'boolean', value: true },
|
||||
arr: { type: 'number', value: '$4,200,000' },
|
||||
@@ -694,8 +698,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'B',
|
||||
tone: 'pink',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-01.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.brianChesky,
|
||||
},
|
||||
employees: { type: 'number', value: '6,900' },
|
||||
opportunities: {
|
||||
@@ -716,18 +719,18 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
url: { type: 'link', value: 'stripe.com' },
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Katherine Adams',
|
||||
tone: 'red',
|
||||
name: 'Patrick Collison',
|
||||
tone: 'blue',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.katherineAdams,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.patrickCollison,
|
||||
},
|
||||
address: { type: 'text', value: '2118 Thornridge Circle' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Tim Cook',
|
||||
tone: 'teal',
|
||||
name: 'Patrick Collison',
|
||||
tone: 'blue',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.timCook,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.patrickCollison,
|
||||
},
|
||||
icp: { type: 'boolean', value: true },
|
||||
arr: { type: 'number', value: '$1,800,000' },
|
||||
@@ -739,8 +742,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'P',
|
||||
tone: 'blue',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-16.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.patrickCollison,
|
||||
},
|
||||
employees: { type: 'number', value: '7,400' },
|
||||
opportunities: {
|
||||
@@ -767,18 +769,18 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
url: { type: 'link', value: 'sequoia.com' },
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Phil Schiller',
|
||||
tone: 'amber',
|
||||
name: 'Roelof Botha',
|
||||
tone: 'green',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.philSchiller,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.roelofBotha,
|
||||
},
|
||||
address: { type: 'text', value: '1316 Dameon Mountain' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Phil Schiller',
|
||||
tone: 'amber',
|
||||
name: 'Roelof Botha',
|
||||
tone: 'green',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.philSchiller,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.roelofBotha,
|
||||
},
|
||||
icp: { type: 'boolean', value: false },
|
||||
arr: { type: 'number', value: '$6,000,000' },
|
||||
@@ -790,8 +792,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'R',
|
||||
tone: 'green',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-13.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.roelofBotha,
|
||||
},
|
||||
employees: { type: 'number', value: '1,100' },
|
||||
opportunities: {
|
||||
@@ -812,18 +813,18 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
url: { type: 'link', value: 'segment.com' },
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Phil Schiller',
|
||||
tone: 'amber',
|
||||
name: 'Peter Reinhardt',
|
||||
tone: 'teal',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.philSchiller,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.peterReinhardt,
|
||||
},
|
||||
address: { type: 'text', value: '8502 Preston Rd. East' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Eddy Cue',
|
||||
tone: 'gray',
|
||||
name: 'Peter Reinhardt',
|
||||
tone: 'teal',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.eddyCue,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.peterReinhardt,
|
||||
},
|
||||
icp: { type: 'boolean', value: true },
|
||||
arr: { type: 'number', value: '$2,750,000' },
|
||||
@@ -835,8 +836,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'P',
|
||||
tone: 'teal',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-20.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.peterReinhardt,
|
||||
},
|
||||
employees: { type: 'number', value: '1,550' },
|
||||
opportunities: {
|
||||
@@ -863,18 +863,18 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
url: { type: 'link', value: 'mailchimp.com' },
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Eddy Cue',
|
||||
tone: 'gray',
|
||||
name: 'Ben Chestnut',
|
||||
tone: 'amber',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.eddyCue,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.benChestnut,
|
||||
},
|
||||
address: { type: 'text', value: '3517 W. Gray St.' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Craig Federighi',
|
||||
tone: 'purple',
|
||||
name: 'Ben Chestnut',
|
||||
tone: 'amber',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.benChestnut,
|
||||
},
|
||||
icp: { type: 'boolean', value: false },
|
||||
arr: { type: 'number', value: '$1,250,000' },
|
||||
@@ -886,8 +886,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'R',
|
||||
tone: 'amber',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-28.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.anonymousLaura,
|
||||
},
|
||||
employees: { type: 'number', value: '1,900' },
|
||||
opportunities: {
|
||||
@@ -910,18 +909,18 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
url: { type: 'link', value: 'accel.com' },
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Craig Federighi',
|
||||
name: 'Ray Damm',
|
||||
tone: 'purple',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.rayDamm,
|
||||
},
|
||||
address: { type: 'text', value: '4140 Parker Rd.' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Katherine Adams',
|
||||
tone: 'red',
|
||||
name: 'Ping Li',
|
||||
tone: 'purple',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.katherineAdams,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.pingLi,
|
||||
},
|
||||
icp: { type: 'boolean', value: true },
|
||||
arr: { type: 'number', value: '$5,800,000' },
|
||||
@@ -933,8 +932,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'P',
|
||||
tone: 'purple',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-30.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.pingLi,
|
||||
},
|
||||
employees: { type: 'number', value: '540' },
|
||||
opportunities: {
|
||||
@@ -965,10 +963,10 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
address: { type: 'text', value: '2715 Ash Dr. San Jose' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Tim Cook',
|
||||
tone: 'teal',
|
||||
name: 'Peter Thiel',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.timCook,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.peterThiel,
|
||||
},
|
||||
icp: { type: 'boolean', value: true },
|
||||
arr: { type: 'number', value: '$2,100,000' },
|
||||
@@ -980,8 +978,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'P',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-05.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.peterThiel,
|
||||
},
|
||||
employees: { type: 'number', value: '734' },
|
||||
opportunities: {
|
||||
@@ -1004,18 +1001,18 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
url: { type: 'link', value: 'google.com' },
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Tim Cook',
|
||||
name: 'Sundar Pichai',
|
||||
tone: 'teal',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.timCook,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.sundarPichai,
|
||||
},
|
||||
address: { type: 'text', value: '4140 Parker Rd.' },
|
||||
accountOwner: {
|
||||
type: 'person',
|
||||
name: 'Jeff Williams',
|
||||
tone: 'blue',
|
||||
name: 'Sundar Pichai',
|
||||
tone: 'teal',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.sundarPichai,
|
||||
},
|
||||
icp: { type: 'boolean', value: false },
|
||||
arr: { type: 'number', value: '$7,500,000' },
|
||||
@@ -1027,8 +1024,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'S',
|
||||
tone: 'teal',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-32.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.sundarPichai,
|
||||
},
|
||||
employees: { type: 'number', value: '734' },
|
||||
opportunities: {
|
||||
@@ -1068,51 +1064,120 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
id: 'jeff-williams',
|
||||
id: 'dario-amodei',
|
||||
cells: {
|
||||
name: {
|
||||
type: 'person',
|
||||
name: 'Jeff Williams',
|
||||
tone: 'blue',
|
||||
name: 'Dario Amodei',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei,
|
||||
},
|
||||
company: {
|
||||
type: 'entity',
|
||||
name: 'Anthropic',
|
||||
domain: 'anthropic.com',
|
||||
},
|
||||
email: { type: 'link', value: 'jeff@anthropic.com' },
|
||||
phone: { type: 'text', value: '+33 1 23 45 67 89' },
|
||||
jobTitle: { type: 'text', value: 'COO' },
|
||||
city: { type: 'text', value: 'Paris' },
|
||||
linkedin: { type: 'link', value: 'jeff-williams' },
|
||||
email: { type: 'link', value: 'dario@anthropic.com' },
|
||||
phone: { type: 'text', value: '+1 415 555 0101' },
|
||||
jobTitle: { type: 'text', value: 'CEO' },
|
||||
city: { type: 'text', value: 'San Francisco' },
|
||||
linkedin: { type: 'link', value: 'dario-amodei' },
|
||||
added: { type: 'text', value: 'Jul 3, 2023' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'craig-federighi',
|
||||
id: 'ryan-roslansky',
|
||||
cells: {
|
||||
name: {
|
||||
type: 'person',
|
||||
name: 'Craig Federighi',
|
||||
tone: 'purple',
|
||||
name: 'Ryan Roslansky',
|
||||
tone: 'teal',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.craigFederighi,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.ryanRoslansky,
|
||||
},
|
||||
company: {
|
||||
type: 'entity',
|
||||
name: 'Linkedin',
|
||||
domain: 'linkedin.com',
|
||||
},
|
||||
email: { type: 'link', value: 'craig@linkedin.com' },
|
||||
phone: { type: 'text', value: '+1 234 567 8900' },
|
||||
jobTitle: { type: 'text', value: 'SVP Software Engineering' },
|
||||
city: { type: 'text', value: 'Tabithaville' },
|
||||
linkedin: { type: 'link', value: 'craig-federighi' },
|
||||
email: { type: 'link', value: 'ryan@linkedin.com' },
|
||||
phone: { type: 'text', value: '+1 650 555 0134' },
|
||||
jobTitle: { type: 'text', value: 'CEO' },
|
||||
city: { type: 'text', value: 'Sunnyvale' },
|
||||
linkedin: { type: 'link', value: 'ryanroslansky' },
|
||||
added: { type: 'text', value: 'Jul 28, 2023' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'stewart-butterfield',
|
||||
cells: {
|
||||
name: {
|
||||
type: 'person',
|
||||
name: 'Stewart Butterfield',
|
||||
tone: 'teal',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.stewartButterfield,
|
||||
},
|
||||
company: {
|
||||
type: 'entity',
|
||||
name: 'Slack',
|
||||
domain: 'slack.com',
|
||||
},
|
||||
email: { type: 'link', value: 'stewart@slack.com' },
|
||||
phone: { type: 'text', value: '+1 415 555 0142' },
|
||||
jobTitle: { type: 'text', value: 'Co-founder' },
|
||||
city: { type: 'text', value: 'San Francisco' },
|
||||
linkedin: { type: 'link', value: 'stewart-butterfield' },
|
||||
added: { type: 'text', value: 'Jul 18, 2023' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ivan-zhao',
|
||||
cells: {
|
||||
name: {
|
||||
type: 'person',
|
||||
name: 'Ivan Zhao',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao,
|
||||
},
|
||||
company: {
|
||||
type: 'entity',
|
||||
name: 'Notion',
|
||||
domain: 'notion.com',
|
||||
},
|
||||
email: { type: 'link', value: 'ivan@notion.com' },
|
||||
phone: { type: 'text', value: '+1 628 555 0186' },
|
||||
jobTitle: { type: 'text', value: 'CEO' },
|
||||
city: { type: 'text', value: 'San Francisco' },
|
||||
linkedin: { type: 'link', value: 'ivanhzhao' },
|
||||
added: { type: 'text', value: 'Jul 8, 2023' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'dylan-field',
|
||||
cells: {
|
||||
name: {
|
||||
type: 'person',
|
||||
name: 'Dylan Field',
|
||||
tone: 'purple',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.dylanField,
|
||||
},
|
||||
company: {
|
||||
type: 'entity',
|
||||
name: 'Figma',
|
||||
domain: 'figma.com',
|
||||
},
|
||||
email: { type: 'link', value: 'dylan@figma.com' },
|
||||
phone: { type: 'text', value: '+1 415 555 0128' },
|
||||
jobTitle: { type: 'text', value: 'CEO' },
|
||||
city: { type: 'text', value: 'San Francisco' },
|
||||
linkedin: { type: 'link', value: 'dylanfield' },
|
||||
added: { type: 'text', value: 'Jul 12, 2023' },
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
@@ -1148,10 +1213,10 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
},
|
||||
assignee: {
|
||||
type: 'person',
|
||||
name: 'Tim Cook',
|
||||
tone: 'teal',
|
||||
name: 'Dario Amodei',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.timCook,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei,
|
||||
},
|
||||
dueDate: { type: 'text', value: 'Oct 25, 2023' },
|
||||
relatedTo: {
|
||||
@@ -1173,10 +1238,10 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
},
|
||||
assignee: {
|
||||
type: 'person',
|
||||
name: 'Eddy Cue',
|
||||
tone: 'gray',
|
||||
name: 'Stewart Butterfield',
|
||||
tone: 'teal',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.eddyCue,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.stewartButterfield,
|
||||
},
|
||||
dueDate: { type: 'text', value: 'Oct 28, 2023' },
|
||||
relatedTo: {
|
||||
@@ -1215,10 +1280,10 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
},
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Phil Schiller',
|
||||
tone: 'amber',
|
||||
name: 'Ivan Zhao',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.philSchiller,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao,
|
||||
},
|
||||
relatedTo: {
|
||||
type: 'entity',
|
||||
@@ -1239,10 +1304,10 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
},
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Tim Cook',
|
||||
tone: 'teal',
|
||||
name: 'Dylan Field',
|
||||
tone: 'purple',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.timCook,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.dylanField,
|
||||
},
|
||||
relatedTo: {
|
||||
type: 'entity',
|
||||
@@ -1280,10 +1345,10 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
},
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Phil Schiller',
|
||||
tone: 'amber',
|
||||
name: 'Dario Amodei',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.philSchiller,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.darioAmodei,
|
||||
},
|
||||
added: { type: 'text', value: 'Oct 24, 2023' },
|
||||
},
|
||||
@@ -1299,10 +1364,10 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
},
|
||||
createdBy: {
|
||||
type: 'person',
|
||||
name: 'Jeff Williams',
|
||||
name: 'Patrick Collison',
|
||||
tone: 'blue',
|
||||
kind: 'person',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.jeffWilliams,
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.patrickCollison,
|
||||
},
|
||||
added: { type: 'text', value: 'Oct 19, 2023' },
|
||||
},
|
||||
@@ -1477,8 +1542,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'I',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-09.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1502,8 +1566,7 @@ export const HERO_DATA: HeroHomeDataType = {
|
||||
shortLabel: 'I',
|
||||
tone: 'gray',
|
||||
kind: 'person',
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-09.png',
|
||||
avatarUrl: PEOPLE_AVATAR_URLS.ivanZhao,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -18,6 +18,8 @@ export const THREE_CARDS_FEATURE_DATA: ThreeCardsFeatureCardsDataType = {
|
||||
body: {
|
||||
text: "Twenty makes it simple. It's clean, intuitive, and built to feel like Notion.",
|
||||
},
|
||||
backgroundImageSrc:
|
||||
'/images/home/three-cards-feature/familiar-interface-gradient.webp',
|
||||
icon: 'users-group',
|
||||
illustration: 'familiar-interface',
|
||||
},
|
||||
@@ -26,6 +28,8 @@ export const THREE_CARDS_FEATURE_DATA: ThreeCardsFeatureCardsDataType = {
|
||||
body: {
|
||||
text: 'Everything updates in real time, with AI chat always ready to help you move faster.',
|
||||
},
|
||||
backgroundImageSrc:
|
||||
'/images/home/three-cards-feature/live-data-gradient.webp',
|
||||
icon: 'live-data',
|
||||
illustration: 'live-data',
|
||||
},
|
||||
@@ -34,6 +38,8 @@ export const THREE_CARDS_FEATURE_DATA: ThreeCardsFeatureCardsDataType = {
|
||||
body: {
|
||||
text: 'Smart patterns, shortcuts, and layouts make everyday tasks faster and easier to execute.',
|
||||
},
|
||||
backgroundImageSrc:
|
||||
'/images/home/three-cards-feature/fast-path-gradient.webp',
|
||||
icon: 'fast-path',
|
||||
illustration: 'fast-path',
|
||||
},
|
||||
|
||||
@@ -265,8 +265,9 @@ const IMAGE_HOVER_FADE_IN = 18;
|
||||
const IMAGE_HOVER_FADE_OUT = 7;
|
||||
const MAX_PREVIEW_PIXEL_RATIO = 2;
|
||||
|
||||
const CanvasMount = styled.div<{ $background: string }>`
|
||||
background: ${(props) => props.$background};
|
||||
const CanvasMount = styled.div<{ $background: string; $transparent: boolean }>`
|
||||
background: ${(props) =>
|
||||
props.$transparent ? 'transparent' : props.$background};
|
||||
display: block;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
@@ -1618,10 +1619,10 @@ export function HalftoneCanvas({
|
||||
-interaction.pointerVelocityY * logicalHeight,
|
||||
);
|
||||
halftoneMaterial.uniforms.dragOffset.value.set(0, 0);
|
||||
halftoneMaterial.uniforms.hoverHalftoneActive.value =
|
||||
activeSettings.animation.hoverHalftoneEnabled
|
||||
? interaction.hoverStrength
|
||||
: 0;
|
||||
halftoneMaterial.uniforms.hoverHalftoneActive.value = activeSettings
|
||||
.animation.hoverHalftoneEnabled
|
||||
? interaction.hoverStrength
|
||||
: 0;
|
||||
halftoneMaterial.uniforms.hoverHalftonePowerShift.value =
|
||||
activeSettings.animation.hoverHalftoneEnabled
|
||||
? activeSettings.animation.hoverHalftonePowerShift
|
||||
@@ -1632,11 +1633,11 @@ export function HalftoneCanvas({
|
||||
activeSettings.animation.hoverHalftoneEnabled
|
||||
? activeSettings.animation.hoverHalftoneWidthShift
|
||||
: 0;
|
||||
halftoneMaterial.uniforms.hoverLightStrength.value =
|
||||
activeSettings.animation.hoverLightEnabled
|
||||
? activeSettings.animation.hoverLightIntensity *
|
||||
interaction.hoverStrength
|
||||
: 0;
|
||||
halftoneMaterial.uniforms.hoverLightStrength.value = activeSettings
|
||||
.animation.hoverLightEnabled
|
||||
? activeSettings.animation.hoverLightIntensity *
|
||||
interaction.hoverStrength
|
||||
: 0;
|
||||
halftoneMaterial.uniforms.hoverLightRadius.value =
|
||||
activeSettings.animation.hoverLightRadius;
|
||||
halftoneMaterial.uniforms.hoverFlowStrength.value = 0;
|
||||
@@ -2031,6 +2032,7 @@ export function HalftoneCanvas({
|
||||
return (
|
||||
<CanvasMount
|
||||
$background={settings.background.color}
|
||||
$transparent={settings.background.transparent}
|
||||
aria-hidden
|
||||
ref={mountReference}
|
||||
/>
|
||||
|
||||
@@ -225,12 +225,12 @@ export const SALESFORCE_DATA: SalesforceDataType = {
|
||||
productIconAlt: 'Retro help document icon',
|
||||
productIconSrc: '/images/pricing/salesforce/help-icon.webp',
|
||||
priceSuffix: ' / seat / month - billed yearly',
|
||||
productTitle: 'Salesforce Pro',
|
||||
productTitle: 'Salesfarce Pro',
|
||||
secondaryCtaNote: 'More options available!',
|
||||
secondaryCtaHref:
|
||||
'https://www.salesforce.com/en-us/wp-content/uploads/sites/4/documents/pricing/all-add-ons.pdf',
|
||||
secondaryCtaLabel: 'Check more add-ons',
|
||||
totalPriceLabel: 'total per month with fixed cost',
|
||||
windowTitle: 'Salesforce Pro pricing',
|
||||
windowTitle: 'Salesfarce Add-on Center',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { HalftoneCanvas } from '@/app/halftone/_components/HalftoneCanvas';
|
||||
import {
|
||||
DEFAULT_GLASS_ANIMATION_SETTINGS,
|
||||
DEFAULT_GLASS_BACKGROUND_SETTINGS,
|
||||
DEFAULT_GLASS_MATERIAL_SETTINGS,
|
||||
DEFAULT_SHAPE_HALFTONE_SETTINGS,
|
||||
DEFAULT_SOLID_ANIMATION_SETTINGS,
|
||||
DEFAULT_SOLID_BACKGROUND_SETTINGS,
|
||||
DEFAULT_SOLID_MATERIAL_SETTINGS,
|
||||
type HalftoneMaterialSurface,
|
||||
type HalftoneStudioSettings,
|
||||
} from '@/app/halftone/_lib/state';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import * as THREE from 'three';
|
||||
@@ -17,6 +29,7 @@ const MIN_FOOTPRINT_SCALE = 0.001;
|
||||
|
||||
type HalftoneRotateAxis = 'x' | 'y' | 'z' | 'xy' | '-x' | '-y' | '-z' | '-xy';
|
||||
type HalftoneRotatePreset = 'axis' | 'lissajous' | 'orbit' | 'tumble';
|
||||
type HelpedHalftoneRenderer = 'legacy' | 'studio';
|
||||
|
||||
type ViewRect = {
|
||||
x: number;
|
||||
@@ -47,13 +60,20 @@ export type HelpedHalftoneSettings = {
|
||||
material: {
|
||||
roughness: number;
|
||||
metalness: number;
|
||||
surface?: HalftoneMaterialSurface;
|
||||
color?: string;
|
||||
thickness?: number;
|
||||
refraction?: number;
|
||||
environmentPower?: number;
|
||||
};
|
||||
halftone: {
|
||||
enabled: boolean;
|
||||
scale: number;
|
||||
power: number;
|
||||
width: number;
|
||||
imageContrast?: number;
|
||||
dashColor: string;
|
||||
hoverDashColor?: string;
|
||||
};
|
||||
animation: {
|
||||
autoRotateEnabled: boolean;
|
||||
@@ -91,6 +111,10 @@ export type HelpedHalftoneSettings = {
|
||||
springDamping: number;
|
||||
springReturnEnabled: boolean;
|
||||
springStrength: number;
|
||||
hoverHalftoneEnabled?: boolean;
|
||||
hoverHalftonePowerShift?: number;
|
||||
hoverHalftoneRadius?: number;
|
||||
hoverHalftoneWidthShift?: number;
|
||||
hoverLightIntensity: number;
|
||||
hoverLightRadius: number;
|
||||
dragFlowDecay: number;
|
||||
@@ -533,10 +557,7 @@ function getFootprintScaleFromRects(
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Math.max(
|
||||
Math.sqrt(currentArea / referenceArea),
|
||||
MIN_FOOTPRINT_SCALE,
|
||||
);
|
||||
return Math.max(Math.sqrt(currentArea / referenceArea), MIN_FOOTPRINT_SCALE);
|
||||
}
|
||||
|
||||
function projectBox3ToViewport({
|
||||
@@ -552,11 +573,7 @@ function projectBox3ToViewport({
|
||||
viewportHeight: number;
|
||||
viewportWidth: number;
|
||||
}) {
|
||||
if (
|
||||
localBounds.isEmpty() ||
|
||||
viewportWidth <= 0 ||
|
||||
viewportHeight <= 0
|
||||
) {
|
||||
if (localBounds.isEmpty() || viewportWidth <= 0 || viewportHeight <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -728,6 +745,57 @@ const StyledVisualMount = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const NOOP = () => {};
|
||||
|
||||
function getStudioMaterialSurface(
|
||||
settings: HelpedHalftoneSettings,
|
||||
): HalftoneMaterialSurface {
|
||||
return settings.material.surface === 'glass' ? 'glass' : 'solid';
|
||||
}
|
||||
|
||||
function createStudioSettings(
|
||||
settings: HelpedHalftoneSettings,
|
||||
): HalftoneStudioSettings {
|
||||
const surface = getStudioMaterialSurface(settings);
|
||||
const defaultMaterial =
|
||||
surface === 'glass'
|
||||
? DEFAULT_GLASS_MATERIAL_SETTINGS
|
||||
: DEFAULT_SOLID_MATERIAL_SETTINGS;
|
||||
const defaultAnimation =
|
||||
surface === 'glass'
|
||||
? DEFAULT_GLASS_ANIMATION_SETTINGS
|
||||
: DEFAULT_SOLID_ANIMATION_SETTINGS;
|
||||
const defaultBackground =
|
||||
surface === 'glass'
|
||||
? DEFAULT_GLASS_BACKGROUND_SETTINGS
|
||||
: DEFAULT_SOLID_BACKGROUND_SETTINGS;
|
||||
|
||||
return {
|
||||
sourceMode: 'shape',
|
||||
shapeKey: 'helped',
|
||||
lighting: { ...settings.lighting },
|
||||
material: {
|
||||
...defaultMaterial,
|
||||
...settings.material,
|
||||
surface,
|
||||
},
|
||||
halftone: {
|
||||
...DEFAULT_SHAPE_HALFTONE_SETTINGS,
|
||||
...settings.halftone,
|
||||
hoverDashColor:
|
||||
settings.halftone.hoverDashColor ?? settings.halftone.dashColor,
|
||||
imageContrast:
|
||||
settings.halftone.imageContrast ??
|
||||
DEFAULT_SHAPE_HALFTONE_SETTINGS.imageContrast,
|
||||
},
|
||||
background: defaultBackground,
|
||||
animation: {
|
||||
...defaultAnimation,
|
||||
...settings.animation,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type HelpedHalftoneCanvasProps = {
|
||||
geometry: THREE.BufferGeometry;
|
||||
initialPose: HelpedHalftonePose;
|
||||
@@ -770,7 +838,9 @@ function HelpedHalftoneCanvas({
|
||||
renderer.setSize(getVirtualWidth(), getVirtualHeight(), false);
|
||||
|
||||
const canvas = renderer.domElement;
|
||||
canvas.style.cursor = settings.animation.followDragEnabled ? 'grab' : 'default';
|
||||
canvas.style.cursor = settings.animation.followDragEnabled
|
||||
? 'grab'
|
||||
: 'default';
|
||||
canvas.style.display = 'block';
|
||||
canvas.style.height = '100%';
|
||||
canvas.style.touchAction = 'none';
|
||||
@@ -855,7 +925,9 @@ function HelpedHalftoneCanvas({
|
||||
dashColor: { value: new THREE.Color(settings.halftone.dashColor) },
|
||||
time: { value: 0 },
|
||||
waveAmount: {
|
||||
value: settings.animation.waveEnabled ? settings.animation.waveAmount : 0,
|
||||
value: settings.animation.waveEnabled
|
||||
? settings.animation.waveAmount
|
||||
: 0,
|
||||
},
|
||||
waveSpeed: { value: settings.animation.waveSpeed },
|
||||
footprintScale: { value: 1.0 },
|
||||
@@ -1073,8 +1145,7 @@ function HelpedHalftoneCanvas({
|
||||
const floatPhase = elapsedTime * settings.animation.floatSpeed;
|
||||
const driftAmount = (settings.animation.driftAmount * Math.PI) / 180;
|
||||
|
||||
meshOffsetY +=
|
||||
Math.sin(floatPhase) * settings.animation.floatAmplitude;
|
||||
meshOffsetY += Math.sin(floatPhase) * settings.animation.floatAmplitude;
|
||||
baseRotationX += Math.sin(floatPhase * 0.72) * driftAmount * 0.45;
|
||||
baseRotationZ += Math.cos(floatPhase * 0.93) * driftAmount * 0.3;
|
||||
}
|
||||
@@ -1089,13 +1160,15 @@ function HelpedHalftoneCanvas({
|
||||
if (rotateEnabled) {
|
||||
interaction.rotateElapsed += delta;
|
||||
const rotateProgress = settings.animation.rotatePingPong
|
||||
? Math.sin(interaction.rotateElapsed * settings.animation.rotateSpeed) *
|
||||
Math.PI
|
||||
? Math.sin(
|
||||
interaction.rotateElapsed * settings.animation.rotateSpeed,
|
||||
) * Math.PI
|
||||
: interaction.rotateElapsed * settings.animation.rotateSpeed;
|
||||
|
||||
if (settings.animation.rotatePreset === 'axis') {
|
||||
const axisDirection =
|
||||
settings.animation.rotateAxis.startsWith('-') ? -1 : 1;
|
||||
const axisDirection = settings.animation.rotateAxis.startsWith('-')
|
||||
? -1
|
||||
: 1;
|
||||
const axisProgress = rotateProgress * axisDirection;
|
||||
|
||||
if (
|
||||
@@ -1256,8 +1329,7 @@ function HelpedHalftoneCanvas({
|
||||
const orbitPitch = centeredY * cameraRange * 0.7;
|
||||
const horizontalRadius = Math.cos(orbitPitch) * baseCameraDistance;
|
||||
const targetCameraX = Math.sin(orbitYaw) * horizontalRadius;
|
||||
const targetCameraY =
|
||||
Math.sin(orbitPitch) * baseCameraDistance * 0.85;
|
||||
const targetCameraY = Math.sin(orbitPitch) * baseCameraDistance * 0.85;
|
||||
const targetCameraZ = Math.cos(orbitYaw) * horizontalRadius;
|
||||
|
||||
camera.position.x += (targetCameraX - camera.position.x) * cameraEase;
|
||||
@@ -1266,8 +1338,7 @@ function HelpedHalftoneCanvas({
|
||||
} else {
|
||||
camera.position.x += (0 - camera.position.x) * 0.12;
|
||||
camera.position.y += (0 - camera.position.y) * 0.12;
|
||||
camera.position.z +=
|
||||
(baseCameraDistance - camera.position.z) * 0.12;
|
||||
camera.position.z += (baseCameraDistance - camera.position.z) * 0.12;
|
||||
}
|
||||
|
||||
lookAtTarget.set(0, meshOffsetY * 0.2, 0);
|
||||
@@ -1329,11 +1400,37 @@ function HelpedHalftoneCanvas({
|
||||
return <StyledVisualMount aria-hidden ref={mountReference} />;
|
||||
}
|
||||
|
||||
function StudioHelpedHalftoneCanvas({
|
||||
geometry,
|
||||
initialPose,
|
||||
previewDistance,
|
||||
settings,
|
||||
}: HelpedHalftoneCanvasProps) {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
geometry.dispose();
|
||||
};
|
||||
}, [geometry]);
|
||||
|
||||
return (
|
||||
<HalftoneCanvas
|
||||
geometry={geometry}
|
||||
imageElement={null}
|
||||
initialPose={initialPose}
|
||||
onFirstInteraction={NOOP}
|
||||
onPoseChange={NOOP}
|
||||
previewDistance={previewDistance}
|
||||
settings={createStudioSettings(settings)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type HelpedHalftoneModelProps = {
|
||||
initialPose: HelpedHalftonePose;
|
||||
label: string;
|
||||
modelUrl: string;
|
||||
previewDistance: number;
|
||||
renderer?: HelpedHalftoneRenderer;
|
||||
settings: HelpedHalftoneSettings;
|
||||
};
|
||||
|
||||
@@ -1342,6 +1439,7 @@ export function HelpedHalftoneModel({
|
||||
label,
|
||||
modelUrl,
|
||||
previewDistance,
|
||||
renderer = 'legacy',
|
||||
settings,
|
||||
}: HelpedHalftoneModelProps) {
|
||||
const [geometry, setGeometry] = useState<THREE.BufferGeometry | null>(null);
|
||||
@@ -1371,6 +1469,17 @@ export function HelpedHalftoneModel({
|
||||
return <StyledVisualMount aria-hidden />;
|
||||
}
|
||||
|
||||
if (renderer === 'studio') {
|
||||
return (
|
||||
<StudioHelpedHalftoneCanvas
|
||||
geometry={geometry}
|
||||
initialPose={initialPose}
|
||||
previewDistance={previewDistance}
|
||||
settings={settings}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HelpedHalftoneCanvas
|
||||
geometry={geometry}
|
||||
|
||||
@@ -11,26 +11,33 @@ const MONEY_PREVIEW_DISTANCE = 5;
|
||||
|
||||
const MONEY_SETTINGS: HelpedHalftoneSettings = {
|
||||
lighting: {
|
||||
intensity: 1,
|
||||
intensity: 3,
|
||||
fillIntensity: 0,
|
||||
ambientIntensity: 0,
|
||||
angleDegrees: 103,
|
||||
height: -3.6,
|
||||
angleDegrees: 47,
|
||||
height: 1.4,
|
||||
},
|
||||
material: {
|
||||
roughness: 0.32,
|
||||
metalness: 0.98,
|
||||
surface: 'glass',
|
||||
color: '#7d7d7d',
|
||||
roughness: 0.26,
|
||||
metalness: 0.15,
|
||||
thickness: 15.58,
|
||||
refraction: 3,
|
||||
environmentPower: 5,
|
||||
},
|
||||
halftone: {
|
||||
enabled: true,
|
||||
scale: 14.97,
|
||||
power: -0.25,
|
||||
width: 1.4,
|
||||
dashColor: '#FDFE98',
|
||||
scale: 25.64,
|
||||
power: -1.24,
|
||||
width: 0.5,
|
||||
imageContrast: 1,
|
||||
dashColor: '#FEFFB7',
|
||||
hoverDashColor: '#4A38F5',
|
||||
},
|
||||
animation: {
|
||||
autoRotateEnabled: false,
|
||||
breatheEnabled: false,
|
||||
breatheEnabled: true,
|
||||
cameraParallaxEnabled: false,
|
||||
followHoverEnabled: false,
|
||||
followDragEnabled: true,
|
||||
@@ -39,31 +46,35 @@ const MONEY_SETTINGS: HelpedHalftoneSettings = {
|
||||
dragFlowEnabled: false,
|
||||
lightSweepEnabled: false,
|
||||
rotateEnabled: false,
|
||||
autoSpeed: 0.1,
|
||||
autoWobble: 0,
|
||||
breatheAmount: 0.04,
|
||||
breatheSpeed: 0.8,
|
||||
autoSpeed: 0.15,
|
||||
autoWobble: 0.3,
|
||||
breatheAmount: 0.015,
|
||||
breatheSpeed: 0.25,
|
||||
cameraParallaxAmount: 0.3,
|
||||
cameraParallaxEase: 0.08,
|
||||
driftAmount: 8,
|
||||
hoverRange: 24,
|
||||
hoverEase: 0.16,
|
||||
hoverRange: 25,
|
||||
hoverEase: 0.08,
|
||||
hoverReturn: true,
|
||||
dragSens: 0.008,
|
||||
dragFriction: 0.08,
|
||||
dragMomentum: true,
|
||||
rotateAxis: '-xy',
|
||||
rotateAxis: 'y',
|
||||
rotatePreset: 'axis',
|
||||
rotateSpeed: 0.1,
|
||||
rotatePingPong: false,
|
||||
floatAmplitude: 0.16,
|
||||
floatSpeed: 0.8,
|
||||
lightSweepHeightRange: 0.5,
|
||||
lightSweepHeightRange: 0,
|
||||
lightSweepRange: 28,
|
||||
lightSweepSpeed: 0.2,
|
||||
springDamping: 0.4,
|
||||
lightSweepSpeed: 0.25,
|
||||
springDamping: 0.5,
|
||||
springReturnEnabled: true,
|
||||
springStrength: 0.06,
|
||||
springStrength: 0.1,
|
||||
hoverHalftoneEnabled: false,
|
||||
hoverHalftonePowerShift: 0.42,
|
||||
hoverHalftoneRadius: 0.2,
|
||||
hoverHalftoneWidthShift: -0.18,
|
||||
hoverLightIntensity: 0.8,
|
||||
hoverLightRadius: 0.2,
|
||||
dragFlowDecay: 0.08,
|
||||
@@ -79,14 +90,14 @@ const MONEY_SETTINGS: HelpedHalftoneSettings = {
|
||||
};
|
||||
|
||||
const MONEY_INITIAL_POSE: HelpedHalftonePose = {
|
||||
autoElapsed: 0.05,
|
||||
autoElapsed: 9.604400000000004,
|
||||
rotateElapsed: 0,
|
||||
rotationX: 2.0543643190124016e-89,
|
||||
rotationY: 1.3695919155712744e-87,
|
||||
rotationZ: 0,
|
||||
rotationX: 9.425994949837918e-229,
|
||||
rotationY: 2.798289108321203e-229,
|
||||
rotationZ: -2.301463116952276e-231,
|
||||
targetRotationX: 0,
|
||||
targetRotationY: 0,
|
||||
timeElapsed: 482.5497999999506,
|
||||
timeElapsed: 303.7692999998331,
|
||||
};
|
||||
|
||||
export function Money() {
|
||||
@@ -96,6 +107,7 @@ export function Money() {
|
||||
label="money.glb"
|
||||
modelUrl={GLB_URL}
|
||||
previewDistance={MONEY_PREVIEW_DISTANCE}
|
||||
renderer="studio"
|
||||
settings={MONEY_SETTINGS}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -11,26 +11,33 @@ const SPACESHIP_PREVIEW_DISTANCE = 3.5;
|
||||
|
||||
const SPACESHIP_SETTINGS: HelpedHalftoneSettings = {
|
||||
lighting: {
|
||||
intensity: 1,
|
||||
intensity: 3,
|
||||
fillIntensity: 0,
|
||||
ambientIntensity: 0,
|
||||
angleDegrees: 103,
|
||||
height: -3.6,
|
||||
angleDegrees: 47,
|
||||
height: 1.4,
|
||||
},
|
||||
material: {
|
||||
roughness: 0.32,
|
||||
metalness: 0.98,
|
||||
surface: 'glass',
|
||||
color: '#7d7d7d',
|
||||
roughness: 0.26,
|
||||
metalness: 0.15,
|
||||
thickness: 15.58,
|
||||
refraction: 3,
|
||||
environmentPower: 5,
|
||||
},
|
||||
halftone: {
|
||||
enabled: true,
|
||||
scale: 14.97,
|
||||
power: -0.25,
|
||||
width: 1.4,
|
||||
scale: 16.64,
|
||||
power: -1.24,
|
||||
width: 0.5,
|
||||
imageContrast: 1,
|
||||
dashColor: '#89FC9A',
|
||||
hoverDashColor: '#89FC9A',
|
||||
},
|
||||
animation: {
|
||||
autoRotateEnabled: false,
|
||||
breatheEnabled: false,
|
||||
breatheEnabled: true,
|
||||
cameraParallaxEnabled: false,
|
||||
followHoverEnabled: false,
|
||||
followDragEnabled: true,
|
||||
@@ -39,31 +46,35 @@ const SPACESHIP_SETTINGS: HelpedHalftoneSettings = {
|
||||
dragFlowEnabled: false,
|
||||
lightSweepEnabled: false,
|
||||
rotateEnabled: false,
|
||||
autoSpeed: 0.1,
|
||||
autoWobble: 0,
|
||||
breatheAmount: 0.04,
|
||||
breatheSpeed: 0.8,
|
||||
autoSpeed: 0.15,
|
||||
autoWobble: 0.3,
|
||||
breatheAmount: 0.015,
|
||||
breatheSpeed: 0.25,
|
||||
cameraParallaxAmount: 0.3,
|
||||
cameraParallaxEase: 0.08,
|
||||
driftAmount: 8,
|
||||
hoverRange: 24,
|
||||
hoverEase: 0.16,
|
||||
hoverRange: 25,
|
||||
hoverEase: 0.08,
|
||||
hoverReturn: true,
|
||||
dragSens: 0.008,
|
||||
dragFriction: 0.08,
|
||||
dragMomentum: true,
|
||||
rotateAxis: '-xy',
|
||||
rotateAxis: 'y',
|
||||
rotatePreset: 'axis',
|
||||
rotateSpeed: 0.1,
|
||||
rotatePingPong: false,
|
||||
floatAmplitude: 0.16,
|
||||
floatSpeed: 0.8,
|
||||
lightSweepHeightRange: 0.5,
|
||||
lightSweepHeightRange: 0,
|
||||
lightSweepRange: 28,
|
||||
lightSweepSpeed: 0.2,
|
||||
springDamping: 0.4,
|
||||
lightSweepSpeed: 0.25,
|
||||
springDamping: 0.5,
|
||||
springReturnEnabled: true,
|
||||
springStrength: 0.06,
|
||||
springStrength: 0.1,
|
||||
hoverHalftoneEnabled: false,
|
||||
hoverHalftonePowerShift: 0.42,
|
||||
hoverHalftoneRadius: 0.2,
|
||||
hoverHalftoneWidthShift: -0.18,
|
||||
hoverLightIntensity: 0.8,
|
||||
hoverLightRadius: 0.2,
|
||||
dragFlowDecay: 0.08,
|
||||
@@ -96,6 +107,7 @@ export function Spaceship() {
|
||||
label="spaceship.glb"
|
||||
modelUrl={GLB_URL}
|
||||
previewDistance={SPACESHIP_PREVIEW_DISTANCE}
|
||||
renderer="studio"
|
||||
settings={SPACESHIP_SETTINGS}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -11,26 +11,33 @@ const TARGET_PREVIEW_DISTANCE = 4;
|
||||
|
||||
const TARGET_SETTINGS: HelpedHalftoneSettings = {
|
||||
lighting: {
|
||||
intensity: 1,
|
||||
intensity: 3,
|
||||
fillIntensity: 0,
|
||||
ambientIntensity: 0,
|
||||
angleDegrees: 103,
|
||||
height: -3.6,
|
||||
angleDegrees: 47,
|
||||
height: 1.4,
|
||||
},
|
||||
material: {
|
||||
roughness: 0.32,
|
||||
metalness: 0.98,
|
||||
surface: 'glass',
|
||||
color: '#7d7d7d',
|
||||
roughness: 0.26,
|
||||
metalness: 0.15,
|
||||
thickness: 15.58,
|
||||
refraction: 3,
|
||||
environmentPower: 5,
|
||||
},
|
||||
halftone: {
|
||||
enabled: true,
|
||||
scale: 14.97,
|
||||
power: -0.25,
|
||||
width: 1.4,
|
||||
scale: 25.64,
|
||||
power: -1.24,
|
||||
width: 0.5,
|
||||
imageContrast: 1,
|
||||
dashColor: '#ED87FC',
|
||||
hoverDashColor: '#ED87FC',
|
||||
},
|
||||
animation: {
|
||||
autoRotateEnabled: false,
|
||||
breatheEnabled: false,
|
||||
breatheEnabled: true,
|
||||
cameraParallaxEnabled: false,
|
||||
followHoverEnabled: false,
|
||||
followDragEnabled: true,
|
||||
@@ -39,31 +46,35 @@ const TARGET_SETTINGS: HelpedHalftoneSettings = {
|
||||
dragFlowEnabled: false,
|
||||
lightSweepEnabled: false,
|
||||
rotateEnabled: false,
|
||||
autoSpeed: 0.1,
|
||||
autoWobble: 0,
|
||||
breatheAmount: 0.04,
|
||||
breatheSpeed: 0.8,
|
||||
autoSpeed: 0.15,
|
||||
autoWobble: 0.3,
|
||||
breatheAmount: 0.015,
|
||||
breatheSpeed: 0.25,
|
||||
cameraParallaxAmount: 0.3,
|
||||
cameraParallaxEase: 0.08,
|
||||
driftAmount: 8,
|
||||
hoverRange: 24,
|
||||
hoverEase: 0.16,
|
||||
hoverRange: 25,
|
||||
hoverEase: 0.08,
|
||||
hoverReturn: true,
|
||||
dragSens: 0.008,
|
||||
dragFriction: 0.08,
|
||||
dragMomentum: true,
|
||||
rotateAxis: '-xy',
|
||||
rotateAxis: 'y',
|
||||
rotatePreset: 'axis',
|
||||
rotateSpeed: 0.1,
|
||||
rotatePingPong: false,
|
||||
floatAmplitude: 0.16,
|
||||
floatSpeed: 0.8,
|
||||
lightSweepHeightRange: 0.5,
|
||||
lightSweepHeightRange: 0,
|
||||
lightSweepRange: 28,
|
||||
lightSweepSpeed: 0.2,
|
||||
springDamping: 0.4,
|
||||
lightSweepSpeed: 0.25,
|
||||
springDamping: 0.5,
|
||||
springReturnEnabled: true,
|
||||
springStrength: 0.06,
|
||||
springStrength: 0.1,
|
||||
hoverHalftoneEnabled: false,
|
||||
hoverHalftonePowerShift: 0.42,
|
||||
hoverHalftoneRadius: 0.2,
|
||||
hoverHalftoneWidthShift: -0.18,
|
||||
hoverLightIntensity: 0.8,
|
||||
hoverLightRadius: 0.2,
|
||||
dragFlowDecay: 0.08,
|
||||
@@ -96,6 +107,7 @@ export function Target() {
|
||||
label="target.glb"
|
||||
modelUrl={GLB_URL}
|
||||
previewDistance={TARGET_PREVIEW_DISTANCE}
|
||||
renderer="studio"
|
||||
settings={TARGET_SETTINGS}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,114 @@
|
||||
export const SHARED_COMPANY_LOGO_URLS = {
|
||||
a16z: '/images/shared/companies/logos/a16z.png',
|
||||
accel: '/images/shared/companies/logos/accel.png',
|
||||
airbnb: '/images/shared/companies/logos/airbnb.png',
|
||||
airtable: '/images/shared/companies/logos/airtable.png',
|
||||
anthropic: '/images/shared/companies/logos/anthropic.png',
|
||||
apple: '/images/shared/companies/logos/apple.png',
|
||||
apple1977: '/images/shared/companies/logos/apple-1977.png',
|
||||
calendar: '/images/shared/companies/logos/calendar.png',
|
||||
claude: '/images/shared/companies/logos/claude.png',
|
||||
cursor: '/images/shared/companies/logos/cursor.png',
|
||||
docusign: '/images/shared/companies/logos/docusign.png',
|
||||
figma: '/images/shared/companies/logos/figma.png',
|
||||
foundersFund: '/images/shared/companies/logos/founders-fund.png',
|
||||
github: '/images/shared/companies/logos/github.png',
|
||||
gmail: '/images/shared/companies/logos/gmail.png',
|
||||
google: '/images/shared/companies/logos/google.png',
|
||||
hubspot: '/images/shared/companies/logos/hubspot.png',
|
||||
kleinerPerkins: '/images/shared/companies/logos/kleiner-perkins.png',
|
||||
lemlist: '/images/shared/companies/logos/lemlist.png',
|
||||
linear: '/images/shared/companies/logos/linear.svg',
|
||||
linkedin: '/images/shared/companies/logos/linkedin.png',
|
||||
mailchimp: '/images/shared/companies/logos/mailchimp.png',
|
||||
meet: '/images/shared/companies/logos/meet.png',
|
||||
metabase: '/images/shared/companies/logos/metabase.png',
|
||||
microsoft: '/images/shared/companies/logos/microsoft.png',
|
||||
notion: '/images/shared/companies/logos/notion.png',
|
||||
okta: '/images/shared/companies/logos/okta.png',
|
||||
openai: '/images/shared/companies/logos/openai.png',
|
||||
outlook: '/images/shared/companies/logos/outlook.png',
|
||||
outreach: '/images/shared/companies/logos/outreach.png',
|
||||
postgresql: '/images/shared/companies/logos/postgresql.png',
|
||||
qonto: '/images/shared/companies/logos/qonto.png',
|
||||
salesforce: '/images/shared/companies/logos/salesforce.png',
|
||||
segment: '/images/shared/companies/logos/segment.png',
|
||||
sequoia: '/images/shared/companies/logos/sequoia.png',
|
||||
slack: '/images/shared/companies/logos/slack.png',
|
||||
stripe: '/images/shared/companies/logos/stripe.png',
|
||||
tally: '/images/shared/companies/logos/tally.png',
|
||||
twenty: '/images/shared/companies/logos/twenty.png',
|
||||
whatsapp: '/images/shared/companies/logos/whatsapp.png',
|
||||
zapier: '/images/shared/companies/logos/zapier.png',
|
||||
} as const;
|
||||
|
||||
export const SHARED_PEOPLE_AVATAR_URLS = {
|
||||
alexandreProt: '/images/shared/people/avatars/alexandre-prot.jpg',
|
||||
anonymousAnna: '/images/shared/people/avatars/anonymous-anna.jpg',
|
||||
anonymousFabrice: '/images/shared/people/avatars/anonymous-fabrice.jpg',
|
||||
anonymousFelix: '/images/shared/people/avatars/anonymous-felix.jpg',
|
||||
anonymousIndira: '/images/shared/people/avatars/anonymous-indira.jpg',
|
||||
anonymousLaura: '/images/shared/people/avatars/anonymous-laura.jpg',
|
||||
anonymousMike: '/images/shared/people/avatars/anonymous-mike.jpg',
|
||||
anonymousThomas: '/images/shared/people/avatars/anonymous-thomas.jpg',
|
||||
benChestnut: '/images/shared/people/avatars/ben-chestnut.jpg',
|
||||
brianChesky: '/images/shared/people/avatars/brian-chesky.jpg',
|
||||
chrisWanstrath: '/images/shared/people/avatars/chris-wanstrath.jpg',
|
||||
craigFederighi: '/images/shared/people/avatars/craig-federighi.jpg',
|
||||
darioAmodei: '/images/shared/people/avatars/dario-amodei.jpg',
|
||||
dylanField: '/images/shared/people/avatars/dylan-field.jpg',
|
||||
eddyCue: '/images/shared/people/avatars/eddy-cue.jpg',
|
||||
ivanZhao: '/images/shared/people/avatars/ivan-zhao.jpg',
|
||||
jeffWilliams: '/images/shared/people/avatars/jeff-williams.jpg',
|
||||
joeGebbia: '/images/shared/people/avatars/joe-gebbia.jpg',
|
||||
katherineAdams: '/images/shared/people/avatars/katherine-adams.jpg',
|
||||
patrickCollison: '/images/shared/people/avatars/patrick-collison.jpg',
|
||||
pingLi: '/images/shared/people/avatars/ping-li.jpg',
|
||||
peterReinhardt: '/images/shared/people/avatars/peter-reinhardt.jpg',
|
||||
peterThiel: '/images/shared/people/avatars/peter-thiel.jpg',
|
||||
philSchiller: '/images/shared/people/avatars/phil-schiller.jpg',
|
||||
rayDamm: '/images/shared/people/avatars/ray-damm.jpg',
|
||||
reidHoffman: '/images/shared/people/avatars/reid-hoffman.jpg',
|
||||
roelofBotha: '/images/shared/people/avatars/roelof-botha.jpg',
|
||||
ryanRoslansky: '/images/shared/people/avatars/ryan-roslansky.jpg',
|
||||
steveAnavi: '/images/shared/people/avatars/steve-anavi.jpg',
|
||||
stewartButterfield:
|
||||
'/images/shared/people/avatars/stewart-butterfield.jpg',
|
||||
sundarPichai: '/images/shared/people/avatars/sundar-pichai.jpg',
|
||||
thomasDohmke: '/images/shared/people/avatars/thomas-dohmke.jpg',
|
||||
timCook: '/images/shared/people/avatars/tim-cook.jpg',
|
||||
} as const;
|
||||
|
||||
export const SHARED_COMPANY_LOGO_URLS_BY_DOMAIN = {
|
||||
'accel.com': SHARED_COMPANY_LOGO_URLS.accel,
|
||||
'airbnb.com': SHARED_COMPANY_LOGO_URLS.airbnb,
|
||||
'anthropic.com': SHARED_COMPANY_LOGO_URLS.anthropic,
|
||||
'apple.com': SHARED_COMPANY_LOGO_URLS.apple,
|
||||
'figma.com': SHARED_COMPANY_LOGO_URLS.figma,
|
||||
'foundersfund.com': SHARED_COMPANY_LOGO_URLS.foundersFund,
|
||||
'github.com': SHARED_COMPANY_LOGO_URLS.github,
|
||||
'google.com': SHARED_COMPANY_LOGO_URLS.google,
|
||||
'linkedin.com': SHARED_COMPANY_LOGO_URLS.linkedin,
|
||||
'mailchimp.com': SHARED_COMPANY_LOGO_URLS.mailchimp,
|
||||
'notion.com': SHARED_COMPANY_LOGO_URLS.notion,
|
||||
'qonto.com': SHARED_COMPANY_LOGO_URLS.qonto,
|
||||
'segment.com': SHARED_COMPANY_LOGO_URLS.segment,
|
||||
'sequoia.com': SHARED_COMPANY_LOGO_URLS.sequoia,
|
||||
'slack.com': SHARED_COMPANY_LOGO_URLS.slack,
|
||||
'stripe.com': SHARED_COMPANY_LOGO_URLS.stripe,
|
||||
} as const;
|
||||
|
||||
export function getSharedCompanyLogoUrlFromDomainName(domainName?: string) {
|
||||
if (!domainName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sanitizedDomain = domainName
|
||||
.replace(/(https?:\/\/)|(www\.)/g, '')
|
||||
.replace(/\/.*$/, '')
|
||||
.toLowerCase();
|
||||
|
||||
return SHARED_COMPANY_LOGO_URLS_BY_DOMAIN[
|
||||
sanitizedDomain as keyof typeof SHARED_COMPANY_LOGO_URLS_BY_DOMAIN
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { getSharedCompanyLogoUrlFromDomainName } from '@/lib/shared-asset-paths';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
import {
|
||||
@@ -1197,6 +1198,12 @@ function sanitizeURL(link: string | null | undefined) {
|
||||
}
|
||||
|
||||
function getLogoUrlFromDomainName(domainName?: string): string | undefined {
|
||||
const sharedLogoUrl = getSharedCompanyLogoUrlFromDomainName(domainName);
|
||||
|
||||
if (sharedLogoUrl) {
|
||||
return sharedLogoUrl;
|
||||
}
|
||||
|
||||
const sanitizedDomain = sanitizeURL(domainName);
|
||||
|
||||
return sanitizedDomain
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { getSharedCompanyLogoUrlFromDomainName } from '@/lib/shared-asset-paths';
|
||||
import { RatingStarIcon } from '@/icons';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
@@ -321,6 +322,12 @@ function sanitizeURL(link: string | null | undefined) {
|
||||
}
|
||||
|
||||
function getLogoUrlFromDomainName(domainName?: string): string | undefined {
|
||||
const sharedLogoUrl = getSharedCompanyLogoUrlFromDomainName(domainName);
|
||||
|
||||
if (sharedLogoUrl) {
|
||||
return sharedLogoUrl;
|
||||
}
|
||||
|
||||
const sanitizedDomain = sanitizeURL(domainName);
|
||||
|
||||
return sanitizedDomain
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { getSharedCompanyLogoUrlFromDomainName } from '@/lib/shared-asset-paths';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
import {
|
||||
@@ -448,6 +449,12 @@ function sanitizeURL(link: string | null | undefined) {
|
||||
}
|
||||
|
||||
function getLogoUrlFromDomainName(domainName?: string): string | undefined {
|
||||
const sharedLogoUrl = getSharedCompanyLogoUrlFromDomainName(domainName);
|
||||
|
||||
if (sharedLogoUrl) {
|
||||
return sharedLogoUrl;
|
||||
}
|
||||
|
||||
const sanitizedDomain = sanitizeURL(domainName);
|
||||
|
||||
return sanitizedDomain
|
||||
|
||||
@@ -15,25 +15,24 @@ const HOVER_FADE_OUT = 7;
|
||||
|
||||
const HALFTONE_SETTINGS = {
|
||||
animation: {
|
||||
hoverHalftoneEnabled: true,
|
||||
hoverHalftonePowerShift: 0.62,
|
||||
hoverHalftoneRadius: 0.6,
|
||||
hoverHalftoneEnabled: false,
|
||||
hoverHalftonePowerShift: 0.42,
|
||||
hoverHalftoneRadius: 0.45,
|
||||
hoverHalftoneWidthShift: -0.18,
|
||||
hoverLightEnabled: false,
|
||||
hoverLightIntensity: 0.12,
|
||||
hoverLightRadius: 0.8,
|
||||
hoverLightEnabled: true,
|
||||
hoverLightIntensity: 0.35,
|
||||
hoverLightRadius: 0.42,
|
||||
waveAmount: 2,
|
||||
waveEnabled: false,
|
||||
waveSpeed: 1,
|
||||
},
|
||||
halftone: {
|
||||
dashColor: '#dddddd',
|
||||
hoverDashColor: '#FFF',
|
||||
imageContrast: 1.12,
|
||||
minimumTone: 0.26,
|
||||
power: 0.18,
|
||||
scale: 12,
|
||||
width: 0.72,
|
||||
dashColor: '#868686',
|
||||
hoverDashColor: '#F5F5F5',
|
||||
imageContrast: 1,
|
||||
power: 0.5,
|
||||
scale: 8,
|
||||
width: 0.3,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -63,7 +62,6 @@ const imagePassthroughFragmentShader = /* glsl */ `
|
||||
|
||||
vec2 uv = vUv;
|
||||
|
||||
// Cover: match the underlying NextImage background crop.
|
||||
if (imageAspect > viewAspect) {
|
||||
float scale = viewAspect / imageAspect;
|
||||
uv.x = (uv.x - 0.5) * scale + 0.5;
|
||||
@@ -95,7 +93,6 @@ const halftoneFragmentShader = /* glsl */ `
|
||||
uniform float s_4;
|
||||
uniform vec3 dashColor;
|
||||
uniform vec3 hoverDashColor;
|
||||
uniform float minimumTone;
|
||||
uniform float time;
|
||||
uniform float waveAmount;
|
||||
uniform float waveSpeed;
|
||||
@@ -209,17 +206,15 @@ const halftoneFragmentShader = /* glsl */ `
|
||||
);
|
||||
float lightLift =
|
||||
hoverLightStrength * hoverLightMask * mix(0.78, 1.18, motionBias) * 0.22;
|
||||
float tonalAverage = (
|
||||
float bandRadius = clamp(
|
||||
(
|
||||
sceneSample.r +
|
||||
sceneSample.g +
|
||||
sceneSample.b +
|
||||
localPower * length(vec2(0.5))
|
||||
) *
|
||||
(1.0 / 3.0)
|
||||
) + lightLift;
|
||||
float bandRadius = clamp(
|
||||
max(tonalAverage, minimumTone),
|
||||
(1.0 / 3.0) +
|
||||
lightLift,
|
||||
0.0,
|
||||
1.0
|
||||
) * 1.86 * 0.5;
|
||||
@@ -391,7 +386,6 @@ async function mountHalftoneCanvas({
|
||||
logicalResolution: {
|
||||
value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()),
|
||||
},
|
||||
minimumTone: { value: HALFTONE_SETTINGS.halftone.minimumTone },
|
||||
s_3: { value: HALFTONE_SETTINGS.halftone.power },
|
||||
s_4: { value: HALFTONE_SETTINGS.halftone.width },
|
||||
tScene: { value: sceneTarget.texture },
|
||||
|
||||
@@ -6,7 +6,8 @@ import { StepperVisualFrame } from '../StepperVisualFrame/StepperVisualFrame';
|
||||
import { StepperBackgroundHalftone } from './StepperBackgroundHalftone';
|
||||
import { StepperLottie } from './StepperLottie';
|
||||
|
||||
const HOME_STEPPER_BACKGROUND = '/images/home/stepper/gears.jpg';
|
||||
const HOME_STEPPER_BACKGROUND =
|
||||
'/images/home/stepper/download-worker.webp';
|
||||
const HOME_STEPPER_SHAPE = '/images/home/stepper/background-shape.webp';
|
||||
|
||||
type VisualProps = {
|
||||
@@ -16,7 +17,7 @@ type VisualProps = {
|
||||
export function Visual({ scrollProgress }: VisualProps) {
|
||||
return (
|
||||
<StepperVisualFrame
|
||||
backgroundColor="#F5F5F5"
|
||||
backgroundColor="#424242"
|
||||
backgroundSrc={HOME_STEPPER_BACKGROUND}
|
||||
backgroundOverlay={
|
||||
<VisibleWhenTabActive>
|
||||
|
||||
@@ -86,7 +86,7 @@ export function Root(props: RootProps) {
|
||||
<SignoffShape fillColor={shapeFillColor} />
|
||||
) : null}
|
||||
{page === Pages.Partner ? (
|
||||
<GuideCrosshair crossX="calc(50% - 334px)" crossY="198px" />
|
||||
<GuideCrosshair crossX="calc(50% + 334px)" crossY="198px" />
|
||||
) : null}
|
||||
<StyledContainer>{children}</StyledContainer>
|
||||
</StyledSection>
|
||||
|
||||
@@ -0,0 +1,921 @@
|
||||
'use client';
|
||||
|
||||
import { styled } from '@linaria/react';
|
||||
import { type RefObject, useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
const DEFAULT_IMAGE_URL =
|
||||
'/images/home/three-cards-feature/familiar-interface-gradient.webp';
|
||||
const VIRTUAL_RENDER_HEIGHT = 800;
|
||||
const REFERENCE_PREVIEW_DISTANCE = 4;
|
||||
const PREVIEW_DISTANCE = REFERENCE_PREVIEW_DISTANCE;
|
||||
const MIN_FOOTPRINT_SCALE = 0.001;
|
||||
const HALFTONE_SCALE = 18;
|
||||
const HALFTONE_POWER = -0.3;
|
||||
const HALFTONE_WIDTH = 0.3;
|
||||
const IMAGE_CONTRAST = 0.7;
|
||||
const DASH_COLOR = '#777';
|
||||
const HOVER_DASH_COLOR = '#777';
|
||||
const HOVER_LIGHT_INTENSITY = 0.85;
|
||||
const HOVER_LIGHT_RADIUS = 0.6;
|
||||
const HOVER_HALFTONE_RADIUS = 0.45;
|
||||
const IMAGE_POINTER_FOLLOW = 0.38;
|
||||
const IMAGE_POINTER_VELOCITY_DAMPING = 0.82;
|
||||
const IMAGE_HOVER_FADE_IN = 18;
|
||||
const IMAGE_HOVER_FADE_OUT = 7;
|
||||
|
||||
export type HalftoneBackdropConfig = {
|
||||
activeHoverX?: number;
|
||||
activeHoverY?: number;
|
||||
dashColor?: string;
|
||||
halftonePower?: number;
|
||||
halftoneScale?: number;
|
||||
halftoneWidth?: number;
|
||||
hoverDashColor?: string;
|
||||
hoverHalftoneRadius?: number;
|
||||
hoverLightIntensity?: number;
|
||||
hoverLightRadius?: number;
|
||||
imageContrast?: number;
|
||||
previewDistance?: number;
|
||||
};
|
||||
|
||||
type ResolvedHalftoneBackdropConfig = Required<HalftoneBackdropConfig>;
|
||||
|
||||
const DEFAULT_HALFTONE_BACKDROP_CONFIG: ResolvedHalftoneBackdropConfig = {
|
||||
activeHoverX: 0.16,
|
||||
activeHoverY: 0.46,
|
||||
dashColor: DASH_COLOR,
|
||||
halftonePower: HALFTONE_POWER,
|
||||
halftoneScale: HALFTONE_SCALE,
|
||||
halftoneWidth: HALFTONE_WIDTH,
|
||||
hoverDashColor: HOVER_DASH_COLOR,
|
||||
hoverHalftoneRadius: HOVER_HALFTONE_RADIUS,
|
||||
hoverLightIntensity: HOVER_LIGHT_INTENSITY,
|
||||
hoverLightRadius: HOVER_LIGHT_RADIUS,
|
||||
imageContrast: IMAGE_CONTRAST,
|
||||
previewDistance: PREVIEW_DISTANCE,
|
||||
};
|
||||
|
||||
const passThroughVertexShader = `
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const blurFragmentShader = `
|
||||
precision highp float;
|
||||
|
||||
uniform sampler2D tInput;
|
||||
uniform vec2 dir;
|
||||
uniform vec2 res;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vec4 sum = vec4(0.0);
|
||||
vec2 px = dir / res;
|
||||
|
||||
float w[5];
|
||||
w[0] = 0.227027;
|
||||
w[1] = 0.1945946;
|
||||
w[2] = 0.1216216;
|
||||
w[3] = 0.054054;
|
||||
w[4] = 0.016216;
|
||||
|
||||
sum += texture2D(tInput, vUv) * w[0];
|
||||
|
||||
for (int i = 1; i < 5; i++) {
|
||||
float fi = float(i) * 3.0;
|
||||
sum += texture2D(tInput, vUv + px * fi) * w[i];
|
||||
sum += texture2D(tInput, vUv - px * fi) * w[i];
|
||||
}
|
||||
|
||||
gl_FragColor = sum;
|
||||
}
|
||||
`;
|
||||
|
||||
const halftoneFragmentShader = `
|
||||
precision highp float;
|
||||
|
||||
uniform sampler2D tScene;
|
||||
uniform vec2 effectResolution;
|
||||
uniform vec2 logicalResolution;
|
||||
uniform float tile;
|
||||
uniform float s_3;
|
||||
uniform float s_4;
|
||||
uniform vec3 dashColor;
|
||||
uniform vec3 hoverDashColor;
|
||||
uniform float footprintScale;
|
||||
uniform vec2 interactionUv;
|
||||
uniform vec2 interactionVelocity;
|
||||
uniform float hoverLightStrength;
|
||||
uniform float hoverLightRadius;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
float distSegment(in vec2 p, in vec2 a, in vec2 b) {
|
||||
vec2 pa = p - a;
|
||||
vec2 ba = b - a;
|
||||
float denom = max(dot(ba, ba), 0.000001);
|
||||
float h = clamp(dot(pa, ba) / denom, 0.0, 1.0);
|
||||
return length(pa - ba * h);
|
||||
}
|
||||
|
||||
float lineSimpleEt(in vec2 p, in float r, in float thickness) {
|
||||
vec2 a = vec2(0.5) + vec2(-r, 0.0);
|
||||
vec2 b = vec2(0.5) + vec2(r, 0.0);
|
||||
float distToSegment = distSegment(p, a, b);
|
||||
float halfThickness = thickness * r;
|
||||
return distToSegment - halfThickness;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 fragCoord =
|
||||
(gl_FragCoord.xy / max(effectResolution, vec2(1.0))) * logicalResolution;
|
||||
float halftoneSize = max(tile * max(footprintScale, 0.001), 1.0);
|
||||
vec2 pointerPx = interactionUv * logicalResolution;
|
||||
vec2 fragDelta = fragCoord - pointerPx;
|
||||
float fragDist = length(fragDelta);
|
||||
vec2 radialDir = fragDist > 0.001 ? fragDelta / fragDist : vec2(0.0, 1.0);
|
||||
float velocityMagnitude = length(interactionVelocity);
|
||||
vec2 motionDir = velocityMagnitude > 0.001
|
||||
? interactionVelocity / velocityMagnitude
|
||||
: vec2(0.0, 0.0);
|
||||
float motionBias = velocityMagnitude > 0.001
|
||||
? dot(-radialDir, motionDir) * 0.5 + 0.5
|
||||
: 0.5;
|
||||
|
||||
float hoverLightMask = 0.0;
|
||||
if (hoverLightStrength > 0.0) {
|
||||
float lightRadiusPx = hoverLightRadius * logicalResolution.y;
|
||||
hoverLightMask = smoothstep(lightRadiusPx, 0.0, fragDist);
|
||||
}
|
||||
|
||||
vec2 effectCoord = fragCoord;
|
||||
|
||||
vec2 cellIndex = floor(effectCoord / halftoneSize);
|
||||
vec2 sampleUv = clamp(
|
||||
(cellIndex + 0.5) * halftoneSize / logicalResolution,
|
||||
vec2(0.0),
|
||||
vec2(1.0)
|
||||
);
|
||||
vec2 cellUv = fract(effectCoord / halftoneSize);
|
||||
|
||||
vec4 sceneSample = texture2D(tScene, sampleUv);
|
||||
float mask = smoothstep(0.02, 0.08, sceneSample.a);
|
||||
float lightLift =
|
||||
hoverLightStrength * hoverLightMask * mix(0.78, 1.18, motionBias) * 0.22;
|
||||
float bandRadius = clamp(
|
||||
(
|
||||
(
|
||||
sceneSample.r +
|
||||
sceneSample.g +
|
||||
sceneSample.b +
|
||||
s_3 * length(vec2(0.5))
|
||||
) *
|
||||
(1.0 / 3.0)
|
||||
) + lightLift,
|
||||
0.0,
|
||||
1.0
|
||||
) * 1.86 * 0.5;
|
||||
|
||||
float alpha = 0.0;
|
||||
if (bandRadius > 0.0001) {
|
||||
float signedDistance = lineSimpleEt(cellUv, bandRadius, s_4);
|
||||
float edge = 0.02;
|
||||
alpha = (1.0 - smoothstep(0.0, edge, signedDistance)) * mask;
|
||||
}
|
||||
|
||||
float hoverStrength = clamp(hoverLightMask * hoverLightStrength, 0.0, 1.0);
|
||||
vec3 activeDashColor = mix(dashColor, hoverDashColor, hoverStrength);
|
||||
vec3 color = activeDashColor * alpha;
|
||||
gl_FragColor = vec4(color, alpha);
|
||||
|
||||
#include <tonemapping_fragment>
|
||||
#include <colorspace_fragment>
|
||||
}
|
||||
`;
|
||||
|
||||
const imagePassthroughFragmentShader = `
|
||||
precision highp float;
|
||||
|
||||
uniform sampler2D tImage;
|
||||
uniform vec2 imageSize;
|
||||
uniform vec2 viewportSize;
|
||||
uniform float zoom;
|
||||
uniform float contrast;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
float imageAspect = imageSize.x / imageSize.y;
|
||||
float viewAspect = viewportSize.x / viewportSize.y;
|
||||
|
||||
vec2 uv = vUv;
|
||||
|
||||
if (imageAspect > viewAspect) {
|
||||
float scale = viewAspect / imageAspect;
|
||||
uv.x = (uv.x - 0.5) * scale + 0.5;
|
||||
} else {
|
||||
float scale = imageAspect / viewAspect;
|
||||
uv.y = (uv.y - 0.5) * scale + 0.5;
|
||||
}
|
||||
|
||||
uv = (uv - 0.5) / zoom + 0.5;
|
||||
uv.y = 1.0 - uv.y;
|
||||
|
||||
float inBounds = step(0.0, uv.x) * step(uv.x, 1.0)
|
||||
* step(0.0, uv.y) * step(uv.y, 1.0);
|
||||
|
||||
vec4 color = texture2D(tImage, clamp(uv, 0.0, 1.0));
|
||||
vec3 contrastColor = clamp((color.rgb - 0.5) * contrast + 0.5, 0.0, 1.0);
|
||||
|
||||
gl_FragColor = vec4(contrastColor, inBounds);
|
||||
}
|
||||
`;
|
||||
|
||||
type Rect = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type InteractionState = {
|
||||
hoverStrength: number;
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
pointerInside: boolean;
|
||||
pointerVelocityX: number;
|
||||
pointerVelocityY: number;
|
||||
smoothedMouseX: number;
|
||||
smoothedMouseY: number;
|
||||
};
|
||||
|
||||
function clampRectToViewport(
|
||||
rect: Rect,
|
||||
viewportWidth: number,
|
||||
viewportHeight: number,
|
||||
) {
|
||||
const minX = Math.max(rect.x, 0);
|
||||
const minY = Math.max(rect.y, 0);
|
||||
const maxX = Math.min(rect.x + rect.width, viewportWidth);
|
||||
const maxY = Math.min(rect.y + rect.height, viewportHeight);
|
||||
|
||||
if (maxX <= minX || maxY <= minY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
};
|
||||
}
|
||||
|
||||
function getRectArea(rect: Rect | null) {
|
||||
if (!rect) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.max(rect.width, 0) * Math.max(rect.height, 0);
|
||||
}
|
||||
|
||||
function getImagePreviewZoom(previewDistance: number) {
|
||||
return REFERENCE_PREVIEW_DISTANCE / Math.max(previewDistance, 0.001);
|
||||
}
|
||||
|
||||
function getContainedImageRect({
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
viewportHeight,
|
||||
viewportWidth,
|
||||
zoom,
|
||||
}: {
|
||||
imageHeight: number;
|
||||
imageWidth: number;
|
||||
viewportHeight: number;
|
||||
viewportWidth: number;
|
||||
zoom: number;
|
||||
}) {
|
||||
if (
|
||||
imageWidth <= 0 ||
|
||||
imageHeight <= 0 ||
|
||||
viewportWidth <= 0 ||
|
||||
viewportHeight <= 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const imageAspect = imageWidth / imageHeight;
|
||||
const viewAspect = viewportWidth / viewportHeight;
|
||||
|
||||
let fittedWidth = viewportWidth;
|
||||
let fittedHeight = viewportHeight;
|
||||
|
||||
if (imageAspect > viewAspect) {
|
||||
fittedHeight = viewportWidth / imageAspect;
|
||||
} else {
|
||||
fittedWidth = viewportHeight * imageAspect;
|
||||
}
|
||||
|
||||
const scaledWidth = fittedWidth * zoom;
|
||||
const scaledHeight = fittedHeight * zoom;
|
||||
|
||||
return clampRectToViewport(
|
||||
{
|
||||
x: (viewportWidth - scaledWidth) * 0.5,
|
||||
y: (viewportHeight - scaledHeight) * 0.5,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
},
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
);
|
||||
}
|
||||
|
||||
function getFootprintScaleFromRects(
|
||||
currentRect: Rect | null,
|
||||
referenceRect: Rect | null,
|
||||
) {
|
||||
const currentArea = getRectArea(currentRect);
|
||||
const referenceArea = getRectArea(referenceRect);
|
||||
|
||||
if (currentArea <= 0 || referenceArea <= 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Math.max(Math.sqrt(currentArea / referenceArea), MIN_FOOTPRINT_SCALE);
|
||||
}
|
||||
|
||||
function getImageFootprintScale({
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
previewDistance,
|
||||
viewportHeight,
|
||||
viewportWidth,
|
||||
}: {
|
||||
imageHeight: number;
|
||||
imageWidth: number;
|
||||
previewDistance: number;
|
||||
viewportHeight: number;
|
||||
viewportWidth: number;
|
||||
}) {
|
||||
const currentRect = getContainedImageRect({
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
viewportHeight,
|
||||
viewportWidth,
|
||||
zoom: getImagePreviewZoom(previewDistance),
|
||||
});
|
||||
const referenceRect = getContainedImageRect({
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
viewportHeight,
|
||||
viewportWidth,
|
||||
zoom: 1,
|
||||
});
|
||||
|
||||
return getFootprintScaleFromRects(currentRect, referenceRect);
|
||||
}
|
||||
|
||||
function createRenderTarget(width: number, height: number) {
|
||||
return new THREE.WebGLRenderTarget(width, height, {
|
||||
minFilter: THREE.LinearFilter,
|
||||
magFilter: THREE.LinearFilter,
|
||||
format: THREE.RGBAFormat,
|
||||
});
|
||||
}
|
||||
|
||||
function createInteractionState(): InteractionState {
|
||||
return {
|
||||
hoverStrength: 0,
|
||||
mouseX: 0.5,
|
||||
mouseY: 0.5,
|
||||
pointerInside: false,
|
||||
pointerVelocityX: 0,
|
||||
pointerVelocityY: 0,
|
||||
smoothedMouseX: 0.5,
|
||||
smoothedMouseY: 0.5,
|
||||
};
|
||||
}
|
||||
|
||||
function loadImage(imageUrl: string) {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = () => reject(new Error(`Failed to load image: ${imageUrl}`));
|
||||
image.src = imageUrl;
|
||||
});
|
||||
}
|
||||
|
||||
async function mountFamiliarInterfaceGradient({
|
||||
container,
|
||||
config,
|
||||
imageUrl,
|
||||
isExternallyActive,
|
||||
pointerTarget,
|
||||
}: {
|
||||
container: HTMLDivElement;
|
||||
config: ResolvedHalftoneBackdropConfig;
|
||||
imageUrl: string;
|
||||
isExternallyActive: () => boolean;
|
||||
pointerTarget?: HTMLElement | null;
|
||||
}) {
|
||||
const HOVER_STRENGTH_STOP_EPSILON = 0.001;
|
||||
const POINTER_POSITION_STOP_EPSILON = 0.001;
|
||||
const POINTER_VELOCITY_STOP_EPSILON = 0.0001;
|
||||
const image = await loadImage(imageUrl);
|
||||
const getWidth = () => Math.max(container.clientWidth, 1);
|
||||
const getHeight = () => Math.max(container.clientHeight, 1);
|
||||
const getVirtualHeight = () => Math.max(VIRTUAL_RENDER_HEIGHT, getHeight());
|
||||
const getVirtualWidth = () =>
|
||||
Math.max(
|
||||
Math.round(getVirtualHeight() * (getWidth() / Math.max(getHeight(), 1))),
|
||||
1,
|
||||
);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
renderer.setSize(getVirtualWidth(), getVirtualHeight(), false);
|
||||
|
||||
const canvas = renderer.domElement;
|
||||
canvas.style.cursor = 'default';
|
||||
canvas.style.display = 'block';
|
||||
canvas.style.height = '100%';
|
||||
canvas.style.touchAction = 'none';
|
||||
canvas.style.width = '100%';
|
||||
container.appendChild(canvas);
|
||||
const trackingElement = pointerTarget ?? canvas;
|
||||
|
||||
const imageTexture = new THREE.Texture(image);
|
||||
imageTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
imageTexture.needsUpdate = true;
|
||||
|
||||
const sceneTarget = createRenderTarget(getVirtualWidth(), getVirtualHeight());
|
||||
const blurTargetA = createRenderTarget(getVirtualWidth(), getVirtualHeight());
|
||||
const blurTargetB = createRenderTarget(getVirtualWidth(), getVirtualHeight());
|
||||
const fullScreenGeometry = new THREE.PlaneGeometry(2, 2);
|
||||
const orthographicCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
|
||||
const imageMaterial = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
contrast: { value: config.imageContrast },
|
||||
imageSize: { value: new THREE.Vector2(image.width, image.height) },
|
||||
tImage: { value: imageTexture },
|
||||
viewportSize: {
|
||||
value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()),
|
||||
},
|
||||
zoom: { value: getImagePreviewZoom(config.previewDistance) },
|
||||
},
|
||||
fragmentShader: imagePassthroughFragmentShader,
|
||||
vertexShader: passThroughVertexShader,
|
||||
});
|
||||
|
||||
const blurHorizontalMaterial = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
dir: { value: new THREE.Vector2(1, 0) },
|
||||
res: { value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()) },
|
||||
tInput: { value: null },
|
||||
},
|
||||
fragmentShader: blurFragmentShader,
|
||||
vertexShader: passThroughVertexShader,
|
||||
});
|
||||
|
||||
const blurVerticalMaterial = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
dir: { value: new THREE.Vector2(0, 1) },
|
||||
res: { value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()) },
|
||||
tInput: { value: null },
|
||||
},
|
||||
fragmentShader: blurFragmentShader,
|
||||
vertexShader: passThroughVertexShader,
|
||||
});
|
||||
|
||||
const halftoneMaterial = new THREE.ShaderMaterial({
|
||||
fragmentShader: halftoneFragmentShader,
|
||||
transparent: true,
|
||||
uniforms: {
|
||||
dashColor: { value: new THREE.Color(config.dashColor) },
|
||||
effectResolution: {
|
||||
value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()),
|
||||
},
|
||||
footprintScale: { value: 1 },
|
||||
hoverDashColor: { value: new THREE.Color(config.hoverDashColor) },
|
||||
hoverLightRadius: { value: config.hoverLightRadius },
|
||||
hoverLightStrength: { value: 0 },
|
||||
interactionUv: { value: new THREE.Vector2(0.5, 0.5) },
|
||||
interactionVelocity: { value: new THREE.Vector2(0, 0) },
|
||||
logicalResolution: {
|
||||
value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()),
|
||||
},
|
||||
s_3: { value: config.halftonePower },
|
||||
s_4: { value: config.halftoneWidth },
|
||||
tScene: { value: sceneTarget.texture },
|
||||
tile: { value: config.halftoneScale },
|
||||
},
|
||||
vertexShader: passThroughVertexShader,
|
||||
});
|
||||
|
||||
const fullScreenMesh = new THREE.Mesh(fullScreenGeometry, imageMaterial);
|
||||
const blurHorizontalMesh = new THREE.Mesh(
|
||||
fullScreenGeometry,
|
||||
blurHorizontalMaterial,
|
||||
);
|
||||
const blurVerticalMesh = new THREE.Mesh(
|
||||
fullScreenGeometry,
|
||||
blurVerticalMaterial,
|
||||
);
|
||||
const postProcessMesh = new THREE.Mesh(fullScreenGeometry, halftoneMaterial);
|
||||
|
||||
const imageScene = new THREE.Scene();
|
||||
imageScene.add(fullScreenMesh);
|
||||
|
||||
const blurHorizontalScene = new THREE.Scene();
|
||||
blurHorizontalScene.add(blurHorizontalMesh);
|
||||
|
||||
const blurVerticalScene = new THREE.Scene();
|
||||
blurVerticalScene.add(blurVerticalMesh);
|
||||
|
||||
const postScene = new THREE.Scene();
|
||||
postScene.add(postProcessMesh);
|
||||
|
||||
const updateViewportUniforms = (
|
||||
logicalWidth: number,
|
||||
logicalHeight: number,
|
||||
effectWidth: number,
|
||||
effectHeight: number,
|
||||
) => {
|
||||
blurHorizontalMaterial.uniforms.res.value.set(effectWidth, effectHeight);
|
||||
blurVerticalMaterial.uniforms.res.value.set(effectWidth, effectHeight);
|
||||
halftoneMaterial.uniforms.effectResolution.value.set(
|
||||
effectWidth,
|
||||
effectHeight,
|
||||
);
|
||||
halftoneMaterial.uniforms.logicalResolution.value.set(
|
||||
logicalWidth,
|
||||
logicalHeight,
|
||||
);
|
||||
imageMaterial.uniforms.viewportSize.value.set(logicalWidth, logicalHeight);
|
||||
};
|
||||
|
||||
const getHalftoneScale = () =>
|
||||
getImageFootprintScale({
|
||||
imageHeight: image.height,
|
||||
imageWidth: image.width,
|
||||
previewDistance: config.previewDistance,
|
||||
viewportHeight: getVirtualHeight(),
|
||||
viewportWidth: getVirtualWidth(),
|
||||
});
|
||||
|
||||
const interaction = createInteractionState();
|
||||
let animationFrameId: number | null = null;
|
||||
let lastFrameTime: number | null = null;
|
||||
|
||||
const renderBackdrop = (deltaSeconds: number) => {
|
||||
const externalActive = isExternallyActive();
|
||||
const isHoverActive = externalActive || interaction.pointerInside;
|
||||
const targetHoverStrength = isHoverActive ? 1 : 0;
|
||||
const hoverEasing =
|
||||
1 -
|
||||
Math.exp(
|
||||
-deltaSeconds *
|
||||
(isHoverActive ? IMAGE_HOVER_FADE_IN : IMAGE_HOVER_FADE_OUT),
|
||||
);
|
||||
|
||||
interaction.hoverStrength +=
|
||||
(targetHoverStrength - interaction.hoverStrength) * hoverEasing;
|
||||
|
||||
if (
|
||||
Math.abs(targetHoverStrength - interaction.hoverStrength) <=
|
||||
HOVER_STRENGTH_STOP_EPSILON
|
||||
) {
|
||||
interaction.hoverStrength = targetHoverStrength;
|
||||
}
|
||||
|
||||
const targetMouseX =
|
||||
externalActive && !interaction.pointerInside
|
||||
? config.activeHoverX
|
||||
: interaction.mouseX;
|
||||
const targetMouseY =
|
||||
externalActive && !interaction.pointerInside
|
||||
? config.activeHoverY
|
||||
: interaction.mouseY;
|
||||
|
||||
interaction.smoothedMouseX +=
|
||||
(targetMouseX - interaction.smoothedMouseX) * IMAGE_POINTER_FOLLOW;
|
||||
interaction.smoothedMouseY +=
|
||||
(targetMouseY - interaction.smoothedMouseY) * IMAGE_POINTER_FOLLOW;
|
||||
|
||||
if (
|
||||
Math.abs(targetMouseX - interaction.smoothedMouseX) <=
|
||||
POINTER_POSITION_STOP_EPSILON
|
||||
) {
|
||||
interaction.smoothedMouseX = targetMouseX;
|
||||
}
|
||||
|
||||
if (
|
||||
Math.abs(targetMouseY - interaction.smoothedMouseY) <=
|
||||
POINTER_POSITION_STOP_EPSILON
|
||||
) {
|
||||
interaction.smoothedMouseY = targetMouseY;
|
||||
}
|
||||
|
||||
interaction.pointerVelocityX *= IMAGE_POINTER_VELOCITY_DAMPING;
|
||||
interaction.pointerVelocityY *= IMAGE_POINTER_VELOCITY_DAMPING;
|
||||
|
||||
if (
|
||||
Math.abs(interaction.pointerVelocityX) <= POINTER_VELOCITY_STOP_EPSILON
|
||||
) {
|
||||
interaction.pointerVelocityX = 0;
|
||||
}
|
||||
|
||||
if (
|
||||
Math.abs(interaction.pointerVelocityY) <= POINTER_VELOCITY_STOP_EPSILON
|
||||
) {
|
||||
interaction.pointerVelocityY = 0;
|
||||
}
|
||||
|
||||
halftoneMaterial.uniforms.interactionUv.value.set(
|
||||
interaction.smoothedMouseX,
|
||||
1 - interaction.smoothedMouseY,
|
||||
);
|
||||
halftoneMaterial.uniforms.interactionVelocity.value.set(
|
||||
interaction.pointerVelocityX * getVirtualWidth(),
|
||||
-interaction.pointerVelocityY * getVirtualHeight(),
|
||||
);
|
||||
halftoneMaterial.uniforms.hoverLightStrength.value =
|
||||
config.hoverLightIntensity * interaction.hoverStrength;
|
||||
halftoneMaterial.uniforms.hoverLightRadius.value =
|
||||
config.hoverHalftoneRadius + config.hoverLightRadius * 0.5;
|
||||
halftoneMaterial.uniforms.footprintScale.value = getHalftoneScale();
|
||||
imageMaterial.uniforms.zoom.value = getImagePreviewZoom(
|
||||
config.previewDistance,
|
||||
);
|
||||
|
||||
renderer.setRenderTarget(sceneTarget);
|
||||
renderer.render(imageScene, orthographicCamera);
|
||||
|
||||
blurHorizontalMaterial.uniforms.tInput.value = sceneTarget.texture;
|
||||
renderer.setRenderTarget(blurTargetA);
|
||||
renderer.render(blurHorizontalScene, orthographicCamera);
|
||||
|
||||
blurVerticalMaterial.uniforms.tInput.value = blurTargetA.texture;
|
||||
renderer.setRenderTarget(blurTargetB);
|
||||
renderer.render(blurVerticalScene, orthographicCamera);
|
||||
|
||||
blurHorizontalMaterial.uniforms.tInput.value = blurTargetB.texture;
|
||||
renderer.setRenderTarget(blurTargetA);
|
||||
renderer.render(blurHorizontalScene, orthographicCamera);
|
||||
|
||||
blurVerticalMaterial.uniforms.tInput.value = blurTargetA.texture;
|
||||
renderer.setRenderTarget(blurTargetB);
|
||||
renderer.render(blurVerticalScene, orthographicCamera);
|
||||
|
||||
renderer.setRenderTarget(null);
|
||||
renderer.clear();
|
||||
renderer.render(postScene, orthographicCamera);
|
||||
|
||||
return (
|
||||
isHoverActive ||
|
||||
interaction.hoverStrength !== targetHoverStrength ||
|
||||
interaction.smoothedMouseX !== targetMouseX ||
|
||||
interaction.smoothedMouseY !== targetMouseY ||
|
||||
interaction.pointerVelocityX !== 0 ||
|
||||
interaction.pointerVelocityY !== 0
|
||||
);
|
||||
};
|
||||
|
||||
const runFrame = (timestamp: number) => {
|
||||
animationFrameId = null;
|
||||
|
||||
const deltaSeconds =
|
||||
lastFrameTime === null
|
||||
? 1 / 60
|
||||
: Math.min((timestamp - lastFrameTime) / 1000, 0.1);
|
||||
|
||||
lastFrameTime = timestamp;
|
||||
|
||||
if (renderBackdrop(deltaSeconds)) {
|
||||
animationFrameId = window.requestAnimationFrame(runFrame);
|
||||
return;
|
||||
}
|
||||
|
||||
lastFrameTime = null;
|
||||
};
|
||||
|
||||
const ensureAnimationLoop = () => {
|
||||
if (animationFrameId !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
animationFrameId = window.requestAnimationFrame(runFrame);
|
||||
};
|
||||
|
||||
const syncSize = () => {
|
||||
const virtualWidth = getVirtualWidth();
|
||||
const virtualHeight = getVirtualHeight();
|
||||
|
||||
renderer.setSize(virtualWidth, virtualHeight, false);
|
||||
sceneTarget.setSize(virtualWidth, virtualHeight);
|
||||
blurTargetA.setSize(virtualWidth, virtualHeight);
|
||||
blurTargetB.setSize(virtualWidth, virtualHeight);
|
||||
updateViewportUniforms(
|
||||
virtualWidth,
|
||||
virtualHeight,
|
||||
virtualWidth,
|
||||
virtualHeight,
|
||||
);
|
||||
|
||||
if (animationFrameId === null) {
|
||||
renderBackdrop(0);
|
||||
}
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(syncSize);
|
||||
resizeObserver.observe(container);
|
||||
syncSize();
|
||||
|
||||
const updatePointerPosition = (
|
||||
event: PointerEvent,
|
||||
options?: { resetVelocity?: boolean },
|
||||
) => {
|
||||
const rect = trackingElement.getBoundingClientRect();
|
||||
const width = Math.max(rect.width, 1);
|
||||
const height = Math.max(rect.height, 1);
|
||||
const nextMouseX = THREE.MathUtils.clamp(
|
||||
(event.clientX - rect.left) / width,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
const nextMouseY = THREE.MathUtils.clamp(
|
||||
(event.clientY - rect.top) / height,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
const deltaX = nextMouseX - interaction.mouseX;
|
||||
const deltaY = nextMouseY - interaction.mouseY;
|
||||
|
||||
interaction.mouseX = nextMouseX;
|
||||
interaction.mouseY = nextMouseY;
|
||||
interaction.pointerInside =
|
||||
event.clientX >= rect.left &&
|
||||
event.clientX <= rect.right &&
|
||||
event.clientY >= rect.top &&
|
||||
event.clientY <= rect.bottom;
|
||||
|
||||
if (options?.resetVelocity) {
|
||||
interaction.pointerVelocityX = 0;
|
||||
interaction.pointerVelocityY = 0;
|
||||
interaction.smoothedMouseX = nextMouseX;
|
||||
interaction.smoothedMouseY = nextMouseY;
|
||||
return;
|
||||
}
|
||||
|
||||
interaction.pointerVelocityX = deltaX;
|
||||
interaction.pointerVelocityY = deltaY;
|
||||
};
|
||||
|
||||
const handlePointerMove = (event: PointerEvent) => {
|
||||
const shouldReset = !interaction.pointerInside;
|
||||
updatePointerPosition(
|
||||
event,
|
||||
shouldReset ? { resetVelocity: true } : undefined,
|
||||
);
|
||||
ensureAnimationLoop();
|
||||
};
|
||||
|
||||
const handlePointerLeave = () => {
|
||||
interaction.pointerInside = false;
|
||||
interaction.pointerVelocityX = 0;
|
||||
interaction.pointerVelocityY = 0;
|
||||
ensureAnimationLoop();
|
||||
};
|
||||
|
||||
trackingElement.addEventListener('pointermove', handlePointerMove);
|
||||
trackingElement.addEventListener('pointerleave', handlePointerLeave);
|
||||
|
||||
if (isExternallyActive()) {
|
||||
ensureAnimationLoop();
|
||||
}
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
if (animationFrameId !== null) {
|
||||
window.cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
|
||||
resizeObserver.disconnect();
|
||||
trackingElement.removeEventListener('pointermove', handlePointerMove);
|
||||
trackingElement.removeEventListener('pointerleave', handlePointerLeave);
|
||||
blurHorizontalMaterial.dispose();
|
||||
blurVerticalMaterial.dispose();
|
||||
halftoneMaterial.dispose();
|
||||
imageMaterial.dispose();
|
||||
imageTexture.dispose();
|
||||
fullScreenGeometry.dispose();
|
||||
sceneTarget.dispose();
|
||||
blurTargetA.dispose();
|
||||
blurTargetB.dispose();
|
||||
renderer.dispose();
|
||||
|
||||
if (canvas.parentNode === container) {
|
||||
container.removeChild(canvas);
|
||||
}
|
||||
},
|
||||
wake: ensureAnimationLoop,
|
||||
};
|
||||
}
|
||||
|
||||
const VisualMount = styled.div`
|
||||
background: transparent;
|
||||
display: block;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type FamiliarInterfaceGradientBackdropProps = {
|
||||
active?: boolean;
|
||||
config?: HalftoneBackdropConfig;
|
||||
imageUrl?: string;
|
||||
pointerTargetRef?: RefObject<HTMLElement | null>;
|
||||
};
|
||||
|
||||
export function FamiliarInterfaceGradientBackdrop({
|
||||
active = false,
|
||||
config,
|
||||
imageUrl = DEFAULT_IMAGE_URL,
|
||||
pointerTargetRef,
|
||||
}: FamiliarInterfaceGradientBackdropProps) {
|
||||
const mountRef = useRef<HTMLDivElement>(null);
|
||||
const activeRef = useRef(active);
|
||||
const wakeRef = useRef<VoidFunction | null>(null);
|
||||
const resolvedConfig = {
|
||||
...DEFAULT_HALFTONE_BACKDROP_CONFIG,
|
||||
...config,
|
||||
};
|
||||
|
||||
activeRef.current = active;
|
||||
|
||||
useEffect(() => {
|
||||
const container = mountRef.current;
|
||||
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dispose: VoidFunction | undefined;
|
||||
let cancelled = false;
|
||||
|
||||
void mountFamiliarInterfaceGradient({
|
||||
config: resolvedConfig,
|
||||
container,
|
||||
imageUrl,
|
||||
isExternallyActive: () => activeRef.current,
|
||||
pointerTarget: pointerTargetRef?.current,
|
||||
})
|
||||
.then((mountedGradient) => {
|
||||
if (cancelled) {
|
||||
mountedGradient.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
wakeRef.current = mountedGradient.wake;
|
||||
dispose = mountedGradient.dispose;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
wakeRef.current = null;
|
||||
dispose?.();
|
||||
};
|
||||
}, [
|
||||
imageUrl,
|
||||
resolvedConfig.dashColor,
|
||||
resolvedConfig.halftonePower,
|
||||
resolvedConfig.halftoneScale,
|
||||
resolvedConfig.halftoneWidth,
|
||||
resolvedConfig.hoverDashColor,
|
||||
resolvedConfig.hoverHalftoneRadius,
|
||||
resolvedConfig.hoverLightIntensity,
|
||||
resolvedConfig.hoverLightRadius,
|
||||
resolvedConfig.imageContrast,
|
||||
resolvedConfig.previewDistance,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
wakeRef.current?.();
|
||||
}, [active]);
|
||||
|
||||
return <VisualMount aria-hidden ref={mountRef} />;
|
||||
}
|
||||
@@ -9,10 +9,13 @@ import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
import {
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
type RefObject,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { FamiliarInterfaceGradientBackdrop } from './FamiliarInterfaceGradientBackdrop';
|
||||
import {
|
||||
IconBuildingSkyscraper,
|
||||
IconCalendarEvent,
|
||||
@@ -40,6 +43,8 @@ const FIGMA_FIELD_STACK_GAP = 1.839;
|
||||
const FIGMA_CARD_RADIUS = 3.677;
|
||||
const FIGMA_CARD_SHADOW =
|
||||
'0px 0px 3.677px rgba(0, 0, 0, 0.08), 0px 1.839px 3.677px rgba(0, 0, 0, 0.04)';
|
||||
const FIGMA_COLUMN_GAP = 8;
|
||||
const FIGMA_COLUMN_SIDE_PADDING = 5.516;
|
||||
const FIGMA_HEADER_PADDING_X = 5.516;
|
||||
const FIGMA_HEADER_PADDING_TOP = 7.354;
|
||||
const FIGMA_HEADER_PADDING_BOTTOM = 3.677;
|
||||
@@ -66,15 +71,20 @@ type CardPlacement =
|
||||
| { laneIndex: LaneIndex; type: 'lane' };
|
||||
type DropTarget = { cardIndex: number; laneIndex: LaneIndex };
|
||||
type LaneCards = [CardId[], CardId[]];
|
||||
type FamiliarInterfaceVisualProps = { active?: boolean };
|
||||
type FamiliarInterfaceVisualProps = {
|
||||
active?: boolean;
|
||||
backgroundImageRotationDeg?: number;
|
||||
backgroundImageSrc?: string;
|
||||
pointerTargetRef?: RefObject<HTMLElement | null>;
|
||||
};
|
||||
|
||||
const COLORS = {
|
||||
backdrop: '#1b1b1b',
|
||||
backdropStripe: 'rgba(255, 255, 255, 0.28)',
|
||||
border: '#ebebeb',
|
||||
borderLight: '#f1f1f1',
|
||||
borderStrong: '#d6d6d6',
|
||||
boardSurface: '#ffffff',
|
||||
cardSurface: '#fcfcfc',
|
||||
activeCardBorder: '#b5ccff',
|
||||
activeCardSurface: '#e8f1ff',
|
||||
imageAreaSurface: '#f5f5f3',
|
||||
@@ -288,7 +298,7 @@ const SceneViewport = styled.div`
|
||||
height: ${SCENE_HEIGHT}px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
top: 0;
|
||||
transform: translateX(-50%) scale(${SCENE_SCALE});
|
||||
transform-origin: top center;
|
||||
width: ${SCENE_WIDTH}px;
|
||||
@@ -309,35 +319,20 @@ const SceneFrame = styled.div`
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 411px;
|
||||
`;
|
||||
|
||||
&::before {
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 1px 1px,
|
||||
rgba(255, 255, 255, 0.09) 1px,
|
||||
transparent 0
|
||||
)
|
||||
right top / 12px 12px repeat,
|
||||
linear-gradient(rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.02));
|
||||
content: '';
|
||||
inset: 0;
|
||||
opacity: 0.42;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&::after {
|
||||
background: repeating-linear-gradient(
|
||||
180deg,
|
||||
transparent 0 11px,
|
||||
${COLORS.backdropStripe} 11px 15px,
|
||||
transparent 15px 27px
|
||||
);
|
||||
content: '';
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
const SceneBackdrop = styled.div<{
|
||||
$backgroundImageRotationDeg?: number;
|
||||
}>`
|
||||
background-color: ${COLORS.backdrop};
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
transform: rotate(
|
||||
${({ $backgroundImageRotationDeg = 0 }) => $backgroundImageRotationDeg}deg
|
||||
);
|
||||
transform-origin: center center;
|
||||
`;
|
||||
|
||||
const BoardGroup = styled.div<{ $active: boolean }>`
|
||||
@@ -434,10 +429,13 @@ const BoardTitleCount = styled.span`
|
||||
`;
|
||||
|
||||
const ColumnsHeaderGrid = styled.div`
|
||||
background: ${COLORS.boardSurface};
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 186.355px);
|
||||
justify-content: center;
|
||||
min-height: 29.354px;
|
||||
padding-left: 7.354px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const LaneHeader = styled.div`
|
||||
@@ -446,7 +444,7 @@ const LaneHeader = styled.div`
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
min-height: 29.354px;
|
||||
padding: 7.354px 7.354px 0;
|
||||
padding: 7.354px ${FIGMA_COLUMN_SIDE_PADDING}px 0;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
@@ -480,18 +478,18 @@ const LaneCount = styled.span`
|
||||
const ColumnsGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 186.355px);
|
||||
justify-content: center;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding-left: 7.354px;
|
||||
`;
|
||||
|
||||
const LaneBody = styled.div`
|
||||
border-right: 1px solid ${COLORS.borderLight};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7.354px;
|
||||
gap: ${FIGMA_COLUMN_GAP}px;
|
||||
min-height: 0;
|
||||
padding: 7.354px 7.354px 8px;
|
||||
padding: 7.354px ${FIGMA_COLUMN_SIDE_PADDING}px 8px;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
@@ -511,7 +509,7 @@ const AddCardRow = styled.div`
|
||||
|
||||
const OpportunityCard = styled.div<{ $variant?: 'active' | 'board' }>`
|
||||
background: ${({ $variant }) =>
|
||||
$variant === 'active' ? COLORS.activeCardSurface : COLORS.boardSurface};
|
||||
$variant === 'active' ? COLORS.activeCardSurface : COLORS.cardSurface};
|
||||
border: ${({ $variant }) => ($variant === 'active' ? '0.919px' : '1px')} solid
|
||||
${({ $variant }) =>
|
||||
$variant === 'active' ? COLORS.activeCardBorder : COLORS.border};
|
||||
@@ -519,7 +517,26 @@ const OpportunityCard = styled.div<{ $variant?: 'active' | 'board' }>`
|
||||
box-shadow: ${FIGMA_CARD_SHADOW};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: background-color 120ms ease;
|
||||
width: ${FIGMA_CARD_WIDTH}px;
|
||||
|
||||
&::after {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
content: '';
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
transition: opacity 120ms ease;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover::after {
|
||||
opacity: ${({ $variant }) => ($variant === 'board' ? 1 : 0)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CardHeader = styled.div`
|
||||
@@ -1163,7 +1180,11 @@ function OpportunityPreviewCard({
|
||||
|
||||
export function FamiliarInterfaceVisual({
|
||||
active = false,
|
||||
backgroundImageRotationDeg,
|
||||
backgroundImageSrc,
|
||||
pointerTargetRef,
|
||||
}: FamiliarInterfaceVisualProps) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const sceneFrameRef = useRef<HTMLDivElement>(null);
|
||||
const interactionLayerRef = useRef<HTMLDivElement>(null);
|
||||
const laneBodyRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
@@ -1187,8 +1208,11 @@ export function FamiliarInterfaceVisual({
|
||||
const [draggedCardId, setDraggedCardId] = useState<CardId | null>(null);
|
||||
const [hasDraggedCard, setHasDraggedCard] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const activePointerIdRef = useRef<number | null>(null);
|
||||
const dragStateRef = useRef<{
|
||||
cardId: CardId;
|
||||
lastClientX: number;
|
||||
lastClientY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
minX: number;
|
||||
@@ -1272,6 +1296,51 @@ export function FamiliarInterfaceVisual({
|
||||
pendingCardAnimationRectsRef.current = nextCardRects;
|
||||
};
|
||||
|
||||
const releaseActivePointerCapture = () => {
|
||||
const activePointerId = activePointerIdRef.current;
|
||||
const interactionLayer = interactionLayerRef.current;
|
||||
|
||||
if (
|
||||
activePointerId !== null &&
|
||||
interactionLayer?.hasPointerCapture(activePointerId)
|
||||
) {
|
||||
interactionLayer.releasePointerCapture(activePointerId);
|
||||
}
|
||||
|
||||
activePointerIdRef.current = null;
|
||||
};
|
||||
|
||||
const clearDragState = () => {
|
||||
dragStateRef.current = null;
|
||||
setActiveCardId(null);
|
||||
setDraggedCardId(null);
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const updateDragOffsetFromPointer = (clientX: number, clientY: number) => {
|
||||
const currentDragState = dragStateRef.current;
|
||||
|
||||
if (currentDragState === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentDragState.lastClientX = clientX;
|
||||
currentDragState.lastClientY = clientY;
|
||||
|
||||
const nextX = clamp(
|
||||
currentDragState.originX + clientX - currentDragState.pointerX,
|
||||
currentDragState.minX,
|
||||
currentDragState.maxX,
|
||||
);
|
||||
const nextY = clamp(
|
||||
currentDragState.originY + clientY - currentDragState.pointerY,
|
||||
currentDragState.minY,
|
||||
currentDragState.maxY,
|
||||
);
|
||||
|
||||
setDragOffset({ x: nextX, y: nextY });
|
||||
};
|
||||
|
||||
const getDropTarget = (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
@@ -1332,6 +1401,10 @@ export function FamiliarInterfaceVisual({
|
||||
return;
|
||||
}
|
||||
|
||||
if (dragStateRef.current !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHasDraggedCard(true);
|
||||
setActiveCardId(cardId);
|
||||
setDraggedCardId(cardId);
|
||||
@@ -1347,6 +1420,8 @@ export function FamiliarInterfaceVisual({
|
||||
|
||||
dragStateRef.current = {
|
||||
cardId,
|
||||
lastClientX: event.clientX,
|
||||
lastClientY: event.clientY,
|
||||
maxX: interactionLayerRect.width - cardRect.width,
|
||||
maxY: interactionLayerRect.height - cardRect.height,
|
||||
minX: 0,
|
||||
@@ -1359,37 +1434,11 @@ export function FamiliarInterfaceVisual({
|
||||
};
|
||||
setDragOffset({ x: originX, y: originY });
|
||||
setIsDragging(true);
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
activePointerIdRef.current = event.pointerId;
|
||||
interactionLayerRef.current?.setPointerCapture(event.pointerId);
|
||||
};
|
||||
|
||||
const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (dragStateRef.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const nextX = clamp(
|
||||
dragStateRef.current.originX +
|
||||
event.clientX -
|
||||
dragStateRef.current.pointerX,
|
||||
dragStateRef.current.minX,
|
||||
dragStateRef.current.maxX,
|
||||
);
|
||||
const nextY = clamp(
|
||||
dragStateRef.current.originY +
|
||||
event.clientY -
|
||||
dragStateRef.current.pointerY,
|
||||
dragStateRef.current.minY,
|
||||
dragStateRef.current.maxY,
|
||||
);
|
||||
|
||||
setDragOffset({ x: nextX, y: nextY });
|
||||
};
|
||||
|
||||
const handlePointerUp = (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const finishDragAtPosition = (clientX: number, clientY: number) => {
|
||||
const currentDragState = dragStateRef.current;
|
||||
|
||||
if (currentDragState === null) {
|
||||
@@ -1398,22 +1447,18 @@ export function FamiliarInterfaceVisual({
|
||||
|
||||
const releasedOffset = {
|
||||
x: clamp(
|
||||
currentDragState.originX + event.clientX - currentDragState.pointerX,
|
||||
currentDragState.originX + clientX - currentDragState.pointerX,
|
||||
currentDragState.minX,
|
||||
currentDragState.maxX,
|
||||
),
|
||||
y: clamp(
|
||||
currentDragState.originY + event.clientY - currentDragState.pointerY,
|
||||
currentDragState.originY + clientY - currentDragState.pointerY,
|
||||
currentDragState.minY,
|
||||
currentDragState.maxY,
|
||||
),
|
||||
};
|
||||
|
||||
const dropTarget = getDropTarget(
|
||||
event.clientX,
|
||||
event.clientY,
|
||||
currentDragState.cardId,
|
||||
);
|
||||
const dropTarget = getDropTarget(clientX, clientY, currentDragState.cardId);
|
||||
|
||||
if (dropTarget !== null) {
|
||||
captureCardAnimationRects(currentDragState.cardId);
|
||||
@@ -1442,14 +1487,72 @@ export function FamiliarInterfaceVisual({
|
||||
setFloatingPosition(releasedOffset);
|
||||
}
|
||||
|
||||
dragStateRef.current = null;
|
||||
setDraggedCardId(null);
|
||||
setIsDragging(false);
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
clearDragState();
|
||||
};
|
||||
|
||||
const handleCapturedPointerMove = (
|
||||
event: ReactPointerEvent<HTMLDivElement>,
|
||||
) => {
|
||||
if (event.pointerId !== activePointerIdRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
updateDragOffsetFromPointer(event.clientX, event.clientY);
|
||||
};
|
||||
|
||||
const handleCapturedPointerUp = (
|
||||
event: ReactPointerEvent<HTMLDivElement>,
|
||||
) => {
|
||||
if (event.pointerId !== activePointerIdRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
finishDragAtPosition(event.clientX, event.clientY);
|
||||
releaseActivePointerCapture();
|
||||
};
|
||||
|
||||
const handleCapturedPointerCancel = (
|
||||
event: ReactPointerEvent<HTMLDivElement>,
|
||||
) => {
|
||||
if (event.pointerId !== activePointerIdRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const currentDragState = dragStateRef.current;
|
||||
|
||||
if (currentDragState !== null) {
|
||||
finishDragAtPosition(
|
||||
currentDragState.lastClientX,
|
||||
currentDragState.lastClientY,
|
||||
);
|
||||
}
|
||||
|
||||
releaseActivePointerCapture();
|
||||
};
|
||||
|
||||
const handleLostPointerCapture = () => {
|
||||
const currentDragState = dragStateRef.current;
|
||||
|
||||
if (currentDragState !== null) {
|
||||
finishDragAtPosition(
|
||||
currentDragState.lastClientX,
|
||||
currentDragState.lastClientY,
|
||||
);
|
||||
}
|
||||
|
||||
activePointerIdRef.current = null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
releaseActivePointerCapture();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const showHandCursor =
|
||||
!hasDraggedCard && !isDragging && activeCardId === null;
|
||||
|
||||
@@ -1468,9 +1571,6 @@ export function FamiliarInterfaceVisual({
|
||||
onPointerDown={(event) => {
|
||||
handlePointerDown(event, cardId, { laneIndex, type: 'lane' });
|
||||
}}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerCancel={handlePointerUp}
|
||||
style={isDraggedCard ? { visibility: 'hidden' } : undefined}
|
||||
>
|
||||
<OpportunityPreviewCard
|
||||
@@ -1482,9 +1582,18 @@ export function FamiliarInterfaceVisual({
|
||||
};
|
||||
|
||||
return (
|
||||
<VisualRoot aria-hidden="true">
|
||||
<VisualRoot aria-hidden="true" ref={rootRef}>
|
||||
<SceneViewport>
|
||||
<SceneFrame ref={sceneFrameRef}>
|
||||
<SceneBackdrop
|
||||
$backgroundImageRotationDeg={backgroundImageRotationDeg}
|
||||
>
|
||||
<FamiliarInterfaceGradientBackdrop
|
||||
active={active}
|
||||
imageUrl={backgroundImageSrc}
|
||||
pointerTargetRef={pointerTargetRef ?? rootRef}
|
||||
/>
|
||||
</SceneBackdrop>
|
||||
<BoardGroup $active={active}>
|
||||
<BoardSurface>
|
||||
<BoardTitleRow>
|
||||
@@ -1559,7 +1668,13 @@ export function FamiliarInterfaceVisual({
|
||||
</ColumnsGrid>
|
||||
</BoardSurface>
|
||||
|
||||
<InteractionLayer ref={interactionLayerRef}>
|
||||
<InteractionLayer
|
||||
ref={interactionLayerRef}
|
||||
onLostPointerCapture={handleLostPointerCapture}
|
||||
onPointerCancel={handleCapturedPointerCancel}
|
||||
onPointerMove={handleCapturedPointerMove}
|
||||
onPointerUp={handleCapturedPointerUp}
|
||||
>
|
||||
{showHandCursor ? (
|
||||
<DragCursor $active={active}>
|
||||
<DragCursorInner $active={active}>
|
||||
@@ -1578,9 +1693,6 @@ export function FamiliarInterfaceVisual({
|
||||
type: 'floating',
|
||||
});
|
||||
}}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerCancel={handlePointerUp}
|
||||
style={{
|
||||
transform: `translate3d(${floatingPosition.x}px, ${floatingPosition.y}px, 0)`,
|
||||
visibility:
|
||||
|
||||
@@ -0,0 +1,924 @@
|
||||
'use client';
|
||||
|
||||
import { styled } from '@linaria/react';
|
||||
import { type RefObject, useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
const DEFAULT_IMAGE_URL =
|
||||
'/images/home/three-cards-feature/fast-path-gradient.webp';
|
||||
const VIRTUAL_RENDER_HEIGHT = 1200;
|
||||
const REFERENCE_PREVIEW_DISTANCE = 4;
|
||||
const PREVIEW_DISTANCE = REFERENCE_PREVIEW_DISTANCE;
|
||||
const MIN_FOOTPRINT_SCALE = 0.001;
|
||||
const HALFTONE_SCALE = 28;
|
||||
const HALFTONE_POWER = 0.3;
|
||||
const HALFTONE_WIDTH = 0.3;
|
||||
const IMAGE_CONTRAST = 1;
|
||||
const DASH_COLOR = '#777';
|
||||
const HOVER_DASH_COLOR = '#777';
|
||||
const HOVER_LIGHT_INTENSITY = 0.85;
|
||||
const HOVER_LIGHT_RADIUS = 0.6;
|
||||
const HOVER_HALFTONE_RADIUS = 0.45;
|
||||
const IMAGE_POINTER_FOLLOW = 0.38;
|
||||
const IMAGE_POINTER_VELOCITY_DAMPING = 0.82;
|
||||
const IMAGE_HOVER_FADE_IN = 18;
|
||||
const IMAGE_HOVER_FADE_OUT = 7;
|
||||
|
||||
export type HalftoneBackdropConfig = {
|
||||
activeHoverX?: number;
|
||||
activeHoverY?: number;
|
||||
dashColor?: string;
|
||||
halftonePower?: number;
|
||||
halftoneScale?: number;
|
||||
halftoneWidth?: number;
|
||||
hoverDashColor?: string;
|
||||
hoverHalftoneRadius?: number;
|
||||
hoverLightIntensity?: number;
|
||||
hoverLightRadius?: number;
|
||||
imageContrast?: number;
|
||||
previewDistance?: number;
|
||||
};
|
||||
|
||||
type ResolvedHalftoneBackdropConfig = Required<HalftoneBackdropConfig>;
|
||||
|
||||
const DEFAULT_HALFTONE_BACKDROP_CONFIG: ResolvedHalftoneBackdropConfig = {
|
||||
activeHoverX: 0.16,
|
||||
activeHoverY: 0.46,
|
||||
dashColor: DASH_COLOR,
|
||||
halftonePower: HALFTONE_POWER,
|
||||
halftoneScale: HALFTONE_SCALE,
|
||||
halftoneWidth: HALFTONE_WIDTH,
|
||||
hoverDashColor: HOVER_DASH_COLOR,
|
||||
hoverHalftoneRadius: HOVER_HALFTONE_RADIUS,
|
||||
hoverLightIntensity: HOVER_LIGHT_INTENSITY,
|
||||
hoverLightRadius: HOVER_LIGHT_RADIUS,
|
||||
imageContrast: IMAGE_CONTRAST,
|
||||
previewDistance: PREVIEW_DISTANCE,
|
||||
};
|
||||
|
||||
const passThroughVertexShader = `
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const blurFragmentShader = `
|
||||
precision highp float;
|
||||
|
||||
uniform sampler2D tInput;
|
||||
uniform vec2 dir;
|
||||
uniform vec2 res;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vec4 sum = vec4(0.0);
|
||||
vec2 px = dir / res;
|
||||
|
||||
float w[5];
|
||||
w[0] = 0.227027;
|
||||
w[1] = 0.1945946;
|
||||
w[2] = 0.1216216;
|
||||
w[3] = 0.054054;
|
||||
w[4] = 0.016216;
|
||||
|
||||
sum += texture2D(tInput, vUv) * w[0];
|
||||
|
||||
for (int i = 1; i < 5; i++) {
|
||||
float fi = float(i) * 3.0;
|
||||
sum += texture2D(tInput, vUv + px * fi) * w[i];
|
||||
sum += texture2D(tInput, vUv - px * fi) * w[i];
|
||||
}
|
||||
|
||||
gl_FragColor = sum;
|
||||
}
|
||||
`;
|
||||
|
||||
const halftoneFragmentShader = `
|
||||
precision highp float;
|
||||
|
||||
uniform sampler2D tScene;
|
||||
uniform vec2 effectResolution;
|
||||
uniform vec2 logicalResolution;
|
||||
uniform float tile;
|
||||
uniform float s_3;
|
||||
uniform float s_4;
|
||||
uniform vec3 dashColor;
|
||||
uniform vec3 hoverDashColor;
|
||||
uniform float footprintScale;
|
||||
uniform vec2 interactionUv;
|
||||
uniform vec2 interactionVelocity;
|
||||
uniform float hoverLightStrength;
|
||||
uniform float hoverLightRadius;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
float distSegment(in vec2 p, in vec2 a, in vec2 b) {
|
||||
vec2 pa = p - a;
|
||||
vec2 ba = b - a;
|
||||
float denom = max(dot(ba, ba), 0.000001);
|
||||
float h = clamp(dot(pa, ba) / denom, 0.0, 1.0);
|
||||
return length(pa - ba * h);
|
||||
}
|
||||
|
||||
float lineSimpleEt(in vec2 p, in float r, in float thickness) {
|
||||
vec2 a = vec2(0.5) + vec2(-r, 0.0);
|
||||
vec2 b = vec2(0.5) + vec2(r, 0.0);
|
||||
float distToSegment = distSegment(p, a, b);
|
||||
float halfThickness = thickness * r;
|
||||
return distToSegment - halfThickness;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 fragCoord =
|
||||
(gl_FragCoord.xy / max(effectResolution, vec2(1.0))) * logicalResolution;
|
||||
float halftoneSize = max(tile * max(footprintScale, 0.001), 1.0);
|
||||
vec2 pointerPx = interactionUv * logicalResolution;
|
||||
vec2 fragDelta = fragCoord - pointerPx;
|
||||
float fragDist = length(fragDelta);
|
||||
vec2 radialDir = fragDist > 0.001 ? fragDelta / fragDist : vec2(0.0, 1.0);
|
||||
float velocityMagnitude = length(interactionVelocity);
|
||||
vec2 motionDir = velocityMagnitude > 0.001
|
||||
? interactionVelocity / velocityMagnitude
|
||||
: vec2(0.0, 0.0);
|
||||
float motionBias = velocityMagnitude > 0.001
|
||||
? dot(-radialDir, motionDir) * 0.5 + 0.5
|
||||
: 0.5;
|
||||
|
||||
float hoverLightMask = 0.0;
|
||||
if (hoverLightStrength > 0.0) {
|
||||
float lightRadiusPx = hoverLightRadius * logicalResolution.y;
|
||||
hoverLightMask = smoothstep(lightRadiusPx, 0.0, fragDist);
|
||||
}
|
||||
|
||||
vec2 effectCoord = fragCoord;
|
||||
|
||||
vec2 cellIndex = floor(effectCoord / halftoneSize);
|
||||
vec2 sampleUv = clamp(
|
||||
(cellIndex + 0.5) * halftoneSize / logicalResolution,
|
||||
vec2(0.0),
|
||||
vec2(1.0)
|
||||
);
|
||||
vec2 cellUv = fract(effectCoord / halftoneSize);
|
||||
|
||||
vec4 sceneSample = texture2D(tScene, sampleUv);
|
||||
float mask = smoothstep(0.02, 0.08, sceneSample.a);
|
||||
float lightLift =
|
||||
hoverLightStrength * hoverLightMask * mix(0.78, 1.18, motionBias) * 0.22;
|
||||
float bandRadius = clamp(
|
||||
(
|
||||
(
|
||||
sceneSample.r +
|
||||
sceneSample.g +
|
||||
sceneSample.b +
|
||||
s_3 * length(vec2(0.5))
|
||||
) *
|
||||
(1.0 / 3.0)
|
||||
) + lightLift,
|
||||
0.0,
|
||||
1.0
|
||||
) * 1.86 * 0.5;
|
||||
|
||||
float alpha = 0.0;
|
||||
if (bandRadius > 0.0001) {
|
||||
float signedDistance = lineSimpleEt(cellUv, bandRadius, s_4);
|
||||
float edge = 0.02;
|
||||
alpha = (1.0 - smoothstep(0.0, edge, signedDistance)) * mask;
|
||||
}
|
||||
|
||||
float hoverStrength = clamp(hoverLightMask * hoverLightStrength, 0.0, 1.0);
|
||||
vec3 activeDashColor = mix(dashColor, hoverDashColor, hoverStrength);
|
||||
vec3 color = activeDashColor * alpha;
|
||||
gl_FragColor = vec4(color, alpha);
|
||||
|
||||
#include <tonemapping_fragment>
|
||||
#include <colorspace_fragment>
|
||||
}
|
||||
`;
|
||||
|
||||
const imagePassthroughFragmentShader = `
|
||||
precision highp float;
|
||||
|
||||
uniform sampler2D tImage;
|
||||
uniform vec2 imageSize;
|
||||
uniform vec2 viewportSize;
|
||||
uniform float zoom;
|
||||
uniform float contrast;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
float imageAspect = imageSize.x / imageSize.y;
|
||||
float viewAspect = viewportSize.x / viewportSize.y;
|
||||
|
||||
vec2 uv = vUv;
|
||||
|
||||
if (imageAspect > viewAspect) {
|
||||
float scale = viewAspect / imageAspect;
|
||||
uv.x = (uv.x - 0.5) * scale + 0.5;
|
||||
} else {
|
||||
float scale = imageAspect / viewAspect;
|
||||
uv.y = (uv.y - 0.5) * scale + 0.5;
|
||||
}
|
||||
|
||||
uv = (uv - 0.5) / zoom + 0.5;
|
||||
uv.y = 1.0 - uv.y;
|
||||
|
||||
float inBounds = step(0.0, uv.x) * step(uv.x, 1.0)
|
||||
* step(0.0, uv.y) * step(uv.y, 1.0);
|
||||
|
||||
vec4 color = texture2D(tImage, clamp(uv, 0.0, 1.0));
|
||||
vec3 contrastColor = clamp((color.rgb - 0.5) * contrast + 0.5, 0.0, 1.0);
|
||||
|
||||
gl_FragColor = vec4(contrastColor, inBounds);
|
||||
}
|
||||
`;
|
||||
|
||||
type Rect = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type InteractionState = {
|
||||
hoverStrength: number;
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
pointerInside: boolean;
|
||||
pointerVelocityX: number;
|
||||
pointerVelocityY: number;
|
||||
smoothedMouseX: number;
|
||||
smoothedMouseY: number;
|
||||
};
|
||||
|
||||
function clampRectToViewport(
|
||||
rect: Rect,
|
||||
viewportWidth: number,
|
||||
viewportHeight: number,
|
||||
) {
|
||||
const minX = Math.max(rect.x, 0);
|
||||
const minY = Math.max(rect.y, 0);
|
||||
const maxX = Math.min(rect.x + rect.width, viewportWidth);
|
||||
const maxY = Math.min(rect.y + rect.height, viewportHeight);
|
||||
|
||||
if (maxX <= minX || maxY <= minY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
};
|
||||
}
|
||||
|
||||
function getRectArea(rect: Rect | null) {
|
||||
if (!rect) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.max(rect.width, 0) * Math.max(rect.height, 0);
|
||||
}
|
||||
|
||||
function getImagePreviewZoom(previewDistance: number) {
|
||||
return REFERENCE_PREVIEW_DISTANCE / Math.max(previewDistance, 0.001);
|
||||
}
|
||||
|
||||
function getContainedImageRect({
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
viewportHeight,
|
||||
viewportWidth,
|
||||
zoom,
|
||||
}: {
|
||||
imageHeight: number;
|
||||
imageWidth: number;
|
||||
viewportHeight: number;
|
||||
viewportWidth: number;
|
||||
zoom: number;
|
||||
}) {
|
||||
if (
|
||||
imageWidth <= 0 ||
|
||||
imageHeight <= 0 ||
|
||||
viewportWidth <= 0 ||
|
||||
viewportHeight <= 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const imageAspect = imageWidth / imageHeight;
|
||||
const viewAspect = viewportWidth / viewportHeight;
|
||||
|
||||
let fittedWidth = viewportWidth;
|
||||
let fittedHeight = viewportHeight;
|
||||
|
||||
if (imageAspect > viewAspect) {
|
||||
fittedHeight = viewportWidth / imageAspect;
|
||||
} else {
|
||||
fittedWidth = viewportHeight * imageAspect;
|
||||
}
|
||||
|
||||
const scaledWidth = fittedWidth * zoom;
|
||||
const scaledHeight = fittedHeight * zoom;
|
||||
|
||||
return clampRectToViewport(
|
||||
{
|
||||
x: (viewportWidth - scaledWidth) * 0.5,
|
||||
y: (viewportHeight - scaledHeight) * 0.5,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
},
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
);
|
||||
}
|
||||
|
||||
function getFootprintScaleFromRects(
|
||||
currentRect: Rect | null,
|
||||
referenceRect: Rect | null,
|
||||
) {
|
||||
const currentArea = getRectArea(currentRect);
|
||||
const referenceArea = getRectArea(referenceRect);
|
||||
|
||||
if (currentArea <= 0 || referenceArea <= 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Math.max(
|
||||
Math.sqrt(currentArea / referenceArea),
|
||||
MIN_FOOTPRINT_SCALE,
|
||||
);
|
||||
}
|
||||
|
||||
function getImageFootprintScale({
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
previewDistance,
|
||||
viewportHeight,
|
||||
viewportWidth,
|
||||
}: {
|
||||
imageHeight: number;
|
||||
imageWidth: number;
|
||||
previewDistance: number;
|
||||
viewportHeight: number;
|
||||
viewportWidth: number;
|
||||
}) {
|
||||
const currentRect = getContainedImageRect({
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
viewportHeight,
|
||||
viewportWidth,
|
||||
zoom: getImagePreviewZoom(previewDistance),
|
||||
});
|
||||
const referenceRect = getContainedImageRect({
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
viewportHeight,
|
||||
viewportWidth,
|
||||
zoom: 1,
|
||||
});
|
||||
|
||||
return getFootprintScaleFromRects(currentRect, referenceRect);
|
||||
}
|
||||
|
||||
function createRenderTarget(width: number, height: number) {
|
||||
return new THREE.WebGLRenderTarget(width, height, {
|
||||
minFilter: THREE.LinearFilter,
|
||||
magFilter: THREE.LinearFilter,
|
||||
format: THREE.RGBAFormat,
|
||||
});
|
||||
}
|
||||
|
||||
function createInteractionState(): InteractionState {
|
||||
return {
|
||||
hoverStrength: 0,
|
||||
mouseX: 0.5,
|
||||
mouseY: 0.5,
|
||||
pointerInside: false,
|
||||
pointerVelocityX: 0,
|
||||
pointerVelocityY: 0,
|
||||
smoothedMouseX: 0.5,
|
||||
smoothedMouseY: 0.5,
|
||||
};
|
||||
}
|
||||
|
||||
function loadImage(imageUrl: string) {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = () => reject(new Error(`Failed to load image: ${imageUrl}`));
|
||||
image.src = imageUrl;
|
||||
});
|
||||
}
|
||||
|
||||
async function mountFastPathGradient({
|
||||
container,
|
||||
config,
|
||||
imageUrl,
|
||||
isExternallyActive,
|
||||
pointerTarget,
|
||||
}: {
|
||||
container: HTMLDivElement;
|
||||
config: ResolvedHalftoneBackdropConfig;
|
||||
imageUrl: string;
|
||||
isExternallyActive: () => boolean;
|
||||
pointerTarget?: HTMLElement | null;
|
||||
}) {
|
||||
const HOVER_STRENGTH_STOP_EPSILON = 0.001;
|
||||
const POINTER_POSITION_STOP_EPSILON = 0.001;
|
||||
const POINTER_VELOCITY_STOP_EPSILON = 0.0001;
|
||||
const image = await loadImage(imageUrl);
|
||||
const getWidth = () => Math.max(container.clientWidth, 1);
|
||||
const getHeight = () => Math.max(container.clientHeight, 1);
|
||||
const getVirtualHeight = () => Math.max(VIRTUAL_RENDER_HEIGHT, getHeight());
|
||||
const getVirtualWidth = () =>
|
||||
Math.max(
|
||||
Math.round(getVirtualHeight() * (getWidth() / Math.max(getHeight(), 1))),
|
||||
1,
|
||||
);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
renderer.setSize(getVirtualWidth(), getVirtualHeight(), false);
|
||||
|
||||
const canvas = renderer.domElement;
|
||||
canvas.style.cursor = 'default';
|
||||
canvas.style.display = 'block';
|
||||
canvas.style.height = '100%';
|
||||
canvas.style.touchAction = 'none';
|
||||
canvas.style.width = '100%';
|
||||
container.appendChild(canvas);
|
||||
const trackingElement = pointerTarget ?? canvas;
|
||||
|
||||
const imageTexture = new THREE.Texture(image);
|
||||
imageTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
imageTexture.needsUpdate = true;
|
||||
|
||||
const sceneTarget = createRenderTarget(getVirtualWidth(), getVirtualHeight());
|
||||
const blurTargetA = createRenderTarget(getVirtualWidth(), getVirtualHeight());
|
||||
const blurTargetB = createRenderTarget(getVirtualWidth(), getVirtualHeight());
|
||||
const fullScreenGeometry = new THREE.PlaneGeometry(2, 2);
|
||||
const orthographicCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
|
||||
const imageMaterial = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
contrast: { value: config.imageContrast },
|
||||
imageSize: { value: new THREE.Vector2(image.width, image.height) },
|
||||
tImage: { value: imageTexture },
|
||||
viewportSize: {
|
||||
value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()),
|
||||
},
|
||||
zoom: { value: getImagePreviewZoom(config.previewDistance) },
|
||||
},
|
||||
fragmentShader: imagePassthroughFragmentShader,
|
||||
vertexShader: passThroughVertexShader,
|
||||
});
|
||||
|
||||
const blurHorizontalMaterial = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
dir: { value: new THREE.Vector2(1, 0) },
|
||||
res: { value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()) },
|
||||
tInput: { value: null },
|
||||
},
|
||||
fragmentShader: blurFragmentShader,
|
||||
vertexShader: passThroughVertexShader,
|
||||
});
|
||||
|
||||
const blurVerticalMaterial = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
dir: { value: new THREE.Vector2(0, 1) },
|
||||
res: { value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()) },
|
||||
tInput: { value: null },
|
||||
},
|
||||
fragmentShader: blurFragmentShader,
|
||||
vertexShader: passThroughVertexShader,
|
||||
});
|
||||
|
||||
const halftoneMaterial = new THREE.ShaderMaterial({
|
||||
fragmentShader: halftoneFragmentShader,
|
||||
transparent: true,
|
||||
uniforms: {
|
||||
dashColor: { value: new THREE.Color(config.dashColor) },
|
||||
effectResolution: {
|
||||
value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()),
|
||||
},
|
||||
footprintScale: { value: 1 },
|
||||
hoverDashColor: { value: new THREE.Color(config.hoverDashColor) },
|
||||
hoverLightRadius: { value: config.hoverLightRadius },
|
||||
hoverLightStrength: { value: 0 },
|
||||
interactionUv: { value: new THREE.Vector2(0.5, 0.5) },
|
||||
interactionVelocity: { value: new THREE.Vector2(0, 0) },
|
||||
logicalResolution: {
|
||||
value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()),
|
||||
},
|
||||
s_3: { value: config.halftonePower },
|
||||
s_4: { value: config.halftoneWidth },
|
||||
tScene: { value: sceneTarget.texture },
|
||||
tile: { value: config.halftoneScale },
|
||||
},
|
||||
vertexShader: passThroughVertexShader,
|
||||
});
|
||||
|
||||
const fullScreenMesh = new THREE.Mesh(fullScreenGeometry, imageMaterial);
|
||||
const blurHorizontalMesh = new THREE.Mesh(
|
||||
fullScreenGeometry,
|
||||
blurHorizontalMaterial,
|
||||
);
|
||||
const blurVerticalMesh = new THREE.Mesh(
|
||||
fullScreenGeometry,
|
||||
blurVerticalMaterial,
|
||||
);
|
||||
const postProcessMesh = new THREE.Mesh(fullScreenGeometry, halftoneMaterial);
|
||||
|
||||
const imageScene = new THREE.Scene();
|
||||
imageScene.add(fullScreenMesh);
|
||||
|
||||
const blurHorizontalScene = new THREE.Scene();
|
||||
blurHorizontalScene.add(blurHorizontalMesh);
|
||||
|
||||
const blurVerticalScene = new THREE.Scene();
|
||||
blurVerticalScene.add(blurVerticalMesh);
|
||||
|
||||
const postScene = new THREE.Scene();
|
||||
postScene.add(postProcessMesh);
|
||||
|
||||
const updateViewportUniforms = (
|
||||
logicalWidth: number,
|
||||
logicalHeight: number,
|
||||
effectWidth: number,
|
||||
effectHeight: number,
|
||||
) => {
|
||||
blurHorizontalMaterial.uniforms.res.value.set(effectWidth, effectHeight);
|
||||
blurVerticalMaterial.uniforms.res.value.set(effectWidth, effectHeight);
|
||||
halftoneMaterial.uniforms.effectResolution.value.set(
|
||||
effectWidth,
|
||||
effectHeight,
|
||||
);
|
||||
halftoneMaterial.uniforms.logicalResolution.value.set(
|
||||
logicalWidth,
|
||||
logicalHeight,
|
||||
);
|
||||
imageMaterial.uniforms.viewportSize.value.set(logicalWidth, logicalHeight);
|
||||
};
|
||||
|
||||
const getHalftoneScale = () =>
|
||||
getImageFootprintScale({
|
||||
imageHeight: image.height,
|
||||
imageWidth: image.width,
|
||||
previewDistance: config.previewDistance,
|
||||
viewportHeight: getVirtualHeight(),
|
||||
viewportWidth: getVirtualWidth(),
|
||||
});
|
||||
|
||||
const interaction = createInteractionState();
|
||||
let animationFrameId: number | null = null;
|
||||
let lastFrameTime: number | null = null;
|
||||
|
||||
const renderBackdrop = (deltaSeconds: number) => {
|
||||
const externalActive = isExternallyActive();
|
||||
const isHoverActive = externalActive || interaction.pointerInside;
|
||||
const targetHoverStrength = isHoverActive ? 1 : 0;
|
||||
const hoverEasing =
|
||||
1 -
|
||||
Math.exp(
|
||||
-deltaSeconds *
|
||||
(isHoverActive ? IMAGE_HOVER_FADE_IN : IMAGE_HOVER_FADE_OUT),
|
||||
);
|
||||
|
||||
interaction.hoverStrength +=
|
||||
(targetHoverStrength - interaction.hoverStrength) * hoverEasing;
|
||||
|
||||
if (
|
||||
Math.abs(targetHoverStrength - interaction.hoverStrength) <=
|
||||
HOVER_STRENGTH_STOP_EPSILON
|
||||
) {
|
||||
interaction.hoverStrength = targetHoverStrength;
|
||||
}
|
||||
|
||||
const targetMouseX =
|
||||
externalActive && !interaction.pointerInside
|
||||
? config.activeHoverX
|
||||
: interaction.mouseX;
|
||||
const targetMouseY =
|
||||
externalActive && !interaction.pointerInside
|
||||
? config.activeHoverY
|
||||
: interaction.mouseY;
|
||||
|
||||
interaction.smoothedMouseX +=
|
||||
(targetMouseX - interaction.smoothedMouseX) * IMAGE_POINTER_FOLLOW;
|
||||
interaction.smoothedMouseY +=
|
||||
(targetMouseY - interaction.smoothedMouseY) * IMAGE_POINTER_FOLLOW;
|
||||
|
||||
if (
|
||||
Math.abs(targetMouseX - interaction.smoothedMouseX) <=
|
||||
POINTER_POSITION_STOP_EPSILON
|
||||
) {
|
||||
interaction.smoothedMouseX = targetMouseX;
|
||||
}
|
||||
|
||||
if (
|
||||
Math.abs(targetMouseY - interaction.smoothedMouseY) <=
|
||||
POINTER_POSITION_STOP_EPSILON
|
||||
) {
|
||||
interaction.smoothedMouseY = targetMouseY;
|
||||
}
|
||||
|
||||
interaction.pointerVelocityX *= IMAGE_POINTER_VELOCITY_DAMPING;
|
||||
interaction.pointerVelocityY *= IMAGE_POINTER_VELOCITY_DAMPING;
|
||||
|
||||
if (
|
||||
Math.abs(interaction.pointerVelocityX) <= POINTER_VELOCITY_STOP_EPSILON
|
||||
) {
|
||||
interaction.pointerVelocityX = 0;
|
||||
}
|
||||
|
||||
if (
|
||||
Math.abs(interaction.pointerVelocityY) <= POINTER_VELOCITY_STOP_EPSILON
|
||||
) {
|
||||
interaction.pointerVelocityY = 0;
|
||||
}
|
||||
|
||||
halftoneMaterial.uniforms.interactionUv.value.set(
|
||||
interaction.smoothedMouseX,
|
||||
1 - interaction.smoothedMouseY,
|
||||
);
|
||||
halftoneMaterial.uniforms.interactionVelocity.value.set(
|
||||
interaction.pointerVelocityX * getVirtualWidth(),
|
||||
-interaction.pointerVelocityY * getVirtualHeight(),
|
||||
);
|
||||
halftoneMaterial.uniforms.hoverLightStrength.value =
|
||||
config.hoverLightIntensity * interaction.hoverStrength;
|
||||
halftoneMaterial.uniforms.hoverLightRadius.value =
|
||||
config.hoverHalftoneRadius + config.hoverLightRadius * 0.5;
|
||||
halftoneMaterial.uniforms.footprintScale.value = getHalftoneScale();
|
||||
imageMaterial.uniforms.zoom.value = getImagePreviewZoom(
|
||||
config.previewDistance,
|
||||
);
|
||||
|
||||
renderer.setRenderTarget(sceneTarget);
|
||||
renderer.render(imageScene, orthographicCamera);
|
||||
|
||||
blurHorizontalMaterial.uniforms.tInput.value = sceneTarget.texture;
|
||||
renderer.setRenderTarget(blurTargetA);
|
||||
renderer.render(blurHorizontalScene, orthographicCamera);
|
||||
|
||||
blurVerticalMaterial.uniforms.tInput.value = blurTargetA.texture;
|
||||
renderer.setRenderTarget(blurTargetB);
|
||||
renderer.render(blurVerticalScene, orthographicCamera);
|
||||
|
||||
blurHorizontalMaterial.uniforms.tInput.value = blurTargetB.texture;
|
||||
renderer.setRenderTarget(blurTargetA);
|
||||
renderer.render(blurHorizontalScene, orthographicCamera);
|
||||
|
||||
blurVerticalMaterial.uniforms.tInput.value = blurTargetA.texture;
|
||||
renderer.setRenderTarget(blurTargetB);
|
||||
renderer.render(blurVerticalScene, orthographicCamera);
|
||||
|
||||
renderer.setRenderTarget(null);
|
||||
renderer.clear();
|
||||
renderer.render(postScene, orthographicCamera);
|
||||
|
||||
return (
|
||||
isHoverActive ||
|
||||
interaction.hoverStrength !== targetHoverStrength ||
|
||||
interaction.smoothedMouseX !== targetMouseX ||
|
||||
interaction.smoothedMouseY !== targetMouseY ||
|
||||
interaction.pointerVelocityX !== 0 ||
|
||||
interaction.pointerVelocityY !== 0
|
||||
);
|
||||
};
|
||||
|
||||
const runFrame = (timestamp: number) => {
|
||||
animationFrameId = null;
|
||||
|
||||
const deltaSeconds =
|
||||
lastFrameTime === null
|
||||
? 1 / 60
|
||||
: Math.min((timestamp - lastFrameTime) / 1000, 0.1);
|
||||
|
||||
lastFrameTime = timestamp;
|
||||
|
||||
if (renderBackdrop(deltaSeconds)) {
|
||||
animationFrameId = window.requestAnimationFrame(runFrame);
|
||||
return;
|
||||
}
|
||||
|
||||
lastFrameTime = null;
|
||||
};
|
||||
|
||||
const ensureAnimationLoop = () => {
|
||||
if (animationFrameId !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
animationFrameId = window.requestAnimationFrame(runFrame);
|
||||
};
|
||||
|
||||
const syncSize = () => {
|
||||
const virtualWidth = getVirtualWidth();
|
||||
const virtualHeight = getVirtualHeight();
|
||||
|
||||
renderer.setSize(virtualWidth, virtualHeight, false);
|
||||
sceneTarget.setSize(virtualWidth, virtualHeight);
|
||||
blurTargetA.setSize(virtualWidth, virtualHeight);
|
||||
blurTargetB.setSize(virtualWidth, virtualHeight);
|
||||
updateViewportUniforms(
|
||||
virtualWidth,
|
||||
virtualHeight,
|
||||
virtualWidth,
|
||||
virtualHeight,
|
||||
);
|
||||
|
||||
if (animationFrameId === null) {
|
||||
renderBackdrop(0);
|
||||
}
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(syncSize);
|
||||
resizeObserver.observe(container);
|
||||
syncSize();
|
||||
|
||||
const updatePointerPosition = (
|
||||
event: PointerEvent,
|
||||
options?: { resetVelocity?: boolean },
|
||||
) => {
|
||||
const rect = trackingElement.getBoundingClientRect();
|
||||
const width = Math.max(rect.width, 1);
|
||||
const height = Math.max(rect.height, 1);
|
||||
const nextMouseX = THREE.MathUtils.clamp(
|
||||
(event.clientX - rect.left) / width,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
const nextMouseY = THREE.MathUtils.clamp(
|
||||
(event.clientY - rect.top) / height,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
const deltaX = nextMouseX - interaction.mouseX;
|
||||
const deltaY = nextMouseY - interaction.mouseY;
|
||||
|
||||
interaction.mouseX = nextMouseX;
|
||||
interaction.mouseY = nextMouseY;
|
||||
interaction.pointerInside =
|
||||
event.clientX >= rect.left &&
|
||||
event.clientX <= rect.right &&
|
||||
event.clientY >= rect.top &&
|
||||
event.clientY <= rect.bottom;
|
||||
|
||||
if (options?.resetVelocity) {
|
||||
interaction.pointerVelocityX = 0;
|
||||
interaction.pointerVelocityY = 0;
|
||||
interaction.smoothedMouseX = nextMouseX;
|
||||
interaction.smoothedMouseY = nextMouseY;
|
||||
return;
|
||||
}
|
||||
|
||||
interaction.pointerVelocityX = deltaX;
|
||||
interaction.pointerVelocityY = deltaY;
|
||||
};
|
||||
|
||||
const handlePointerMove = (event: PointerEvent) => {
|
||||
const shouldReset = !interaction.pointerInside;
|
||||
updatePointerPosition(
|
||||
event,
|
||||
shouldReset ? { resetVelocity: true } : undefined,
|
||||
);
|
||||
ensureAnimationLoop();
|
||||
};
|
||||
|
||||
const handlePointerLeave = () => {
|
||||
interaction.pointerInside = false;
|
||||
interaction.pointerVelocityX = 0;
|
||||
interaction.pointerVelocityY = 0;
|
||||
ensureAnimationLoop();
|
||||
};
|
||||
|
||||
trackingElement.addEventListener('pointermove', handlePointerMove);
|
||||
trackingElement.addEventListener('pointerleave', handlePointerLeave);
|
||||
|
||||
if (isExternallyActive()) {
|
||||
ensureAnimationLoop();
|
||||
}
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
if (animationFrameId !== null) {
|
||||
window.cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
|
||||
resizeObserver.disconnect();
|
||||
trackingElement.removeEventListener('pointermove', handlePointerMove);
|
||||
trackingElement.removeEventListener('pointerleave', handlePointerLeave);
|
||||
blurHorizontalMaterial.dispose();
|
||||
blurVerticalMaterial.dispose();
|
||||
halftoneMaterial.dispose();
|
||||
imageMaterial.dispose();
|
||||
imageTexture.dispose();
|
||||
fullScreenGeometry.dispose();
|
||||
sceneTarget.dispose();
|
||||
blurTargetA.dispose();
|
||||
blurTargetB.dispose();
|
||||
renderer.dispose();
|
||||
|
||||
if (canvas.parentNode === container) {
|
||||
container.removeChild(canvas);
|
||||
}
|
||||
},
|
||||
wake: ensureAnimationLoop,
|
||||
};
|
||||
}
|
||||
|
||||
const VisualMount = styled.div`
|
||||
background: transparent;
|
||||
display: block;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type FastPathGradientBackdropProps = {
|
||||
active?: boolean;
|
||||
config?: HalftoneBackdropConfig;
|
||||
imageUrl?: string;
|
||||
pointerTargetRef?: RefObject<HTMLElement | null>;
|
||||
};
|
||||
|
||||
export function FastPathGradientBackdrop({
|
||||
active = false,
|
||||
config,
|
||||
imageUrl = DEFAULT_IMAGE_URL,
|
||||
pointerTargetRef,
|
||||
}: FastPathGradientBackdropProps) {
|
||||
const mountRef = useRef<HTMLDivElement>(null);
|
||||
const activeRef = useRef(active);
|
||||
const wakeRef = useRef<VoidFunction | null>(null);
|
||||
const resolvedConfig = {
|
||||
...DEFAULT_HALFTONE_BACKDROP_CONFIG,
|
||||
...config,
|
||||
};
|
||||
|
||||
activeRef.current = active;
|
||||
|
||||
useEffect(() => {
|
||||
const container = mountRef.current;
|
||||
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dispose: VoidFunction | undefined;
|
||||
let cancelled = false;
|
||||
|
||||
void mountFastPathGradient({
|
||||
config: resolvedConfig,
|
||||
container,
|
||||
imageUrl,
|
||||
isExternallyActive: () => activeRef.current,
|
||||
pointerTarget: pointerTargetRef?.current,
|
||||
})
|
||||
.then((mountedGradient) => {
|
||||
if (cancelled) {
|
||||
mountedGradient.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
wakeRef.current = mountedGradient.wake;
|
||||
dispose = mountedGradient.dispose;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
wakeRef.current = null;
|
||||
dispose?.();
|
||||
};
|
||||
}, [
|
||||
imageUrl,
|
||||
resolvedConfig.dashColor,
|
||||
resolvedConfig.halftonePower,
|
||||
resolvedConfig.halftoneScale,
|
||||
resolvedConfig.halftoneWidth,
|
||||
resolvedConfig.hoverDashColor,
|
||||
resolvedConfig.hoverHalftoneRadius,
|
||||
resolvedConfig.hoverLightIntensity,
|
||||
resolvedConfig.hoverLightRadius,
|
||||
resolvedConfig.imageContrast,
|
||||
resolvedConfig.previewDistance,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
wakeRef.current?.();
|
||||
}, [active]);
|
||||
|
||||
return <VisualMount aria-hidden ref={mountRef} />;
|
||||
}
|
||||