Skip to content

Commit

Permalink
✨ feat: add hyper card
Browse files Browse the repository at this point in the history
  • Loading branch information
rdmclin2 committed Apr 3, 2024
1 parent 96010e1 commit d197fb2
Show file tree
Hide file tree
Showing 17 changed files with 900 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -67,6 +67,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@emotion/styled": "^11.11.0",
"@floating-ui/react": "^0.24.8",
"@react-spring/web": "^9.7.3",
"ahooks": "3.7.8",
"classnames": "^2.5.1",
"color": "^4.2.3",
Expand Down
55 changes: 55 additions & 0 deletions src/HolographicCard/components/Container.tsx
@@ -0,0 +1,55 @@
import { CSSProperties, ReactNode, memo } from 'react';
import { LaserShine, useLaserShine } from './LaserShine';
import Orbit from './Orbit';
import { useStyles } from './style';

export interface ContainerProps {
back?: string;
foil?: string;
mask?: string;
children?: ReactNode;
className?: string;
loading?: boolean;
}

const Container = memo<ContainerProps>(({ back, foil, mask, children, className, loading }) => {
const { styles, cx } = useStyles();
const { style: shineStyle, onMouseMove, onMouseOut } = useLaserShine();

return (
<Orbit
classNames={{
container: cx(`${className} ${mask ? 'masked' : ''}`, styles.container),
rotator: cx(styles.rotator),
}}
styles={{
container: {
...shineStyle,
'--mask': `url(${mask ?? ''})`,
'--foil': `url(${foil ?? ''})`,
width: 380,
} as CSSProperties,
content: {
display: 'grid',
},
}}
onMouseMove={onMouseMove}
onMouseOut={onMouseOut}
>
<img
className={cx(styles.back, loading && styles.backLoading)}
src={back}
loading="lazy"
width="660"
height="921"
/>
<div className={cx(styles.front, loading && styles.fontLoading)}>
{children}
<LaserShine mask={!!mask} className={styles.shine} />
<div className={cx('card__glare', styles.glare)} />
</div>
</Orbit>
);
});

export default Container;
26 changes: 26 additions & 0 deletions src/HolographicCard/components/LaserShine/LaserShine.tsx
@@ -0,0 +1,26 @@
import { animated } from '@react-spring/web';
import { CSSProperties, memo } from 'react';
import { DivProps } from 'react-layout-kit';
import { useStyles } from './style';

export interface LaserShineProps extends DivProps {
mask?: boolean;
className?: string;
style?: CSSProperties;
}

export const LaserShine = memo<LaserShineProps>(({ mask, className, ...res }) => {
const { styles, cx } = useStyles();

console.log(className);
return (
<animated.div
className={cx(
styles.composeShine,
mask ? styles.maskedShine : styles.noMaskedShine,
className,
)}
{...res}
/>
);
});
2 changes: 2 additions & 0 deletions src/HolographicCard/components/LaserShine/index.ts
@@ -0,0 +1,2 @@
export * from './LaserShine';
export * from './useLaserShine';
207 changes: 207 additions & 0 deletions src/HolographicCard/components/LaserShine/style.ts
@@ -0,0 +1,207 @@
import { createStyles } from 'antd-style';

export const useStyles = createStyles(({ css, cx }) => {
const shine = css`
background-image: var(--foil),
repeating-linear-gradient(
0deg,
var(--sunpillar-clr-1) calc(var(--space) * 1),
var(--sunpillar-clr-2) calc(var(--space) * 2),
var(--sunpillar-clr-3) calc(var(--space) * 3),
var(--sunpillar-clr-4) calc(var(--space) * 4),
var(--sunpillar-clr-5) calc(var(--space) * 5),
var(--sunpillar-clr-6) calc(var(--space) * 6),
var(--sunpillar-clr-1) calc(var(--space) * 7)
),
repeating-linear-gradient(
var(--angle),
#0e152e 0%,
hsl(180, 10%, 60%) 3.8%,
hsl(180, 29%, 66%) 4.5%,
hsl(180, 10%, 60%) 5.2%,
#0e152e 10%,
#0e152e 12%
),
radial-gradient(
farthest-corner circle at var(--pointer-x) var(--pointer-y),
hsla(0, 0%, 0%, 0.1) 12%,
hsla(0, 0%, 0%, 0.15) 20%,
hsla(0, 0%, 0%, 0.25) 120%
);
background-position: center center, 0% var(--background-y),
calc(var(--background-x) + (var(--background-y) * 0.2)) var(--background-y),
var(--background-x) var(--background-y);
background-blend-mode: soft-light, hue, hard-light;
background-size: var(--imgsize), 200% 700%, 300% 100%, 200% 100%;
filter: brightness(calc((var(--pointer-from-center) * 0.05) + 0.8)) contrast(1.75) saturate(1.2);
`;

const shineAfter = css`
content: '';
--space: 5%;
--angle: 133deg;
--imgsize: cover;
background-image: var(--foil),
repeating-linear-gradient(
0deg,
var(--sunpillar-clr-1) calc(var(--space) * 1),
var(--sunpillar-clr-2) calc(var(--space) * 2),
var(--sunpillar-clr-3) calc(var(--space) * 3),
var(--sunpillar-clr-4) calc(var(--space) * 4),
var(--sunpillar-clr-5) calc(var(--space) * 5),
var(--sunpillar-clr-6) calc(var(--space) * 6),
var(--sunpillar-clr-1) calc(var(--space) * 7)
),
repeating-linear-gradient(
var(--angle),
#0e152e 0%,
hsl(180, 10%, 60%) 3.8%,
hsl(180, 29%, 66%) 4.5%,
hsl(180, 10%, 60%) 5.2%,
#0e152e 10%,
#0e152e 12%
),
radial-gradient(
farthest-corner circle at var(--pointer-x) var(--pointer-y),
hsla(0, 0%, 0%, 0.1) 12%,
hsla(0, 0%, 0%, 0.15) 20%,
hsla(0, 0%, 0%, 0.25) 120%
);
background-blend-mode: soft-light, hue, hard-light;
background-position: center center, 0% var(--background-y),
calc((var(--background-x) + (var(--background-y) * 0.2)) * -1) calc(var(--background-y) * -1),
var(--background-x) var(--background-y);
background-size: var(--imgsize), 200% 400%, 195% 100%, 200% 100%;
filter: brightness(calc((var(--pointer-from-center) * 0.4) + 0.85)) contrast(2) saturate(0.5);
mix-blend-mode: exclusion;
`;

const shineBefore = css`
content: '';
-webkit-mask-image: none;
mask-image: none;
background-position: center;
background-size: cover;
z-index: 1;
background-image: radial-gradient(
farthest-corner circle at var(--pointer-x) var(--pointer-y),
hsl(0, 0%, 100%) 0%,
hsla(0, 0%, 0%, 0) 80%
);
mix-blend-mode: screen;
opacity: 0.5;
`;

const masked = css`
/** masking image for cards which are masked **/
mask-image: var(--mask);
mask-size: cover;
mask-position: center center;
`;

const nomasked = css`
--mask: none;
--foil: none;
--imgsize: 20%;
background-blend-mode: color-burn, hue, hard-light;
filter: brightness(calc((var(--pointer-from-center) * 0.05) + 0.6)) contrast(1.5) saturate(1.2);
`;

return {
composeShine: cx(
'aha-shine',
css`
--space: 5%;
--angle: 133deg;
--imgsize: cover;
--sunpillar-1: hsl(2, 100%, 73%);
--sunpillar-2: hsl(53, 100%, 69%);
--sunpillar-3: hsl(93, 100%, 69%);
--sunpillar-4: hsl(176, 100%, 76%);
--sunpillar-5: hsl(228, 100%, 74%);
--sunpillar-6: hsl(283, 100%, 73%);
--sunpillar-clr-1: var(--sunpillar-1);
--sunpillar-clr-2: var(--sunpillar-2);
--sunpillar-clr-3: var(--sunpillar-3);
--sunpillar-clr-4: var(--sunpillar-4);
--sunpillar-clr-5: var(--sunpillar-5);
--sunpillar-clr-6: var(--sunpillar-6);
display: grid;
transform: translateZ(1px);
overflow: hidden;
z-index: 3;
mix-blend-mode: color-dodge;
opacity: var(--card-opacity);
&:before {
--sunpillar-clr-1: var(--sunpillar-5);
--sunpillar-clr-2: var(--sunpillar-6);
--sunpillar-clr-3: var(--sunpillar-1);
--sunpillar-clr-4: var(--sunpillar-2);
--sunpillar-clr-5: var(--sunpillar-3);
--sunpillar-clr-6: var(--sunpillar-4);
grid-area: 1/1;
transform: translateZ(1px);
border-radius: var(--card-radius);
}
&:after {
--sunpillar-clr-1: var(--sunpillar-6);
--sunpillar-clr-2: var(--sunpillar-1);
--sunpillar-clr-3: var(--sunpillar-2);
--sunpillar-clr-4: var(--sunpillar-3);
--sunpillar-clr-5: var(--sunpillar-4);
--sunpillar-clr-6: var(--sunpillar-5);
transform: translateZ(1.2px);
grid-area: 1/1;
border-radius: var(--card-radius);
}
${shine};
&:before {
${shineBefore}
}
&:after {
${shineAfter}
}
`,
),

maskedShine: css`
${masked}
&:before,
&:after {
${masked}
}
`,
noMaskedShine: css`
${nomasked}
&:after {
${nomasked}
}
`,
};
});
62 changes: 62 additions & 0 deletions src/HolographicCard/components/LaserShine/useLaserShine.ts
@@ -0,0 +1,62 @@
import { useSpring } from '@react-spring/web';
import { CSSProperties } from 'react';
import { adjust, clamp, round } from '../../utils/math';

const randomSeed = {
x: Math.random(),
y: Math.random(),
};

const cosmosPosition = {
x: Math.floor(randomSeed.x * 734),
y: Math.floor(randomSeed.y * 1280),
};

export const useLaserShine = (delay = 500) => {
const [{ background, glare }, api] = useSpring(() => ({
background: [0, 50],
glare: [50, 50, 0],
}));

const onMouseMove = (e: any) => {
const rect = e.target.getBoundingClientRect();
const absolute = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
const percent = {
x: clamp(round((100 / rect.width) * absolute.x)),
y: clamp(round((100 / rect.height) * absolute.y)),
};

api.start({
background: [adjust(percent.x, 0, 100, 37, 63), adjust(percent.y, 0, 100, 33, 67)],
glare: [round(percent.x), round(percent.y), 1],
});
};

const onMouseOut = () => {
setTimeout(() => {
api.start({ glare: [50, 50, 0], background: [50, 50] });
}, delay);
};

const style = {
'--pointer-x': glare.to((x) => `${x}%`),
'--pointer-y': glare.to((_, y) => `${y}%`),
'--pointer-from-center': glare.to((x, y) =>
clamp(Math.sqrt((y - 50) * (y - 50) + (x - 50) * (x - 50)) / 50, 0, 1),
),

'--pointer-from-top': glare.to((_, y) => y / 100),
'--pointer-from-left': glare.to((x) => x / 100),
'--card-opacity': glare.to((_, __, o) => o),
'--background-x': background.to((x) => `${x}%`),
'--background-y': background.to((_, y) => `${y}%`),
'--seedx': randomSeed.x,
'--seedy': randomSeed.y,
'--cosmosbg': `${cosmosPosition.x}px ${cosmosPosition.y}px`,
} as CSSProperties;

return { onMouseMove, onMouseOut, style };
};

0 comments on commit d197fb2

Please sign in to comment.