Skip to content

Commit

Permalink
✨ feat: add hyper card
Browse files Browse the repository at this point in the history
rdmclin2 committed Apr 3, 2024
1 parent 96010e1 commit d197fb2
Showing 17 changed files with 900 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
55 changes: 55 additions & 0 deletions src/HolographicCard/components/Container.tsx
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './LaserShine';
export * from './useLaserShine';
207 changes: 207 additions & 0 deletions src/HolographicCard/components/LaserShine/style.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 };
};
165 changes: 165 additions & 0 deletions src/HolographicCard/components/Orbit/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { animated, useSpring } from '@react-spring/web';
import { CSSProperties, MouseEventHandler, ReactNode, forwardRef } from 'react';
import { clamp, round } from '../../utils/math';
import { useStyles } from './styles';

export interface OrbitProps {
children?: ReactNode;
className?: string;
style?: CSSProperties;

classNames?: {
container?: string;
translator?: string;
rotator?: string;
content?: string;
};
/**
* @default: 400
*/
width?: number;
/**
* @default: 560
*/
height?: number;
styles?: {
container?: CSSProperties;
translator?: CSSProperties;
rotator?: CSSProperties;
content?: CSSProperties;
};
/**
* 延迟恢复动画的时间,单位:ms
* @default: 500
*/
delay?: number;
/**
* 可旋转方向
* @default both
*/
orbitDirection?: 'both' | 'vertical' | 'horizontal';
/**
* @default: 1
*/
sensitivity?: number;
damping?: number;
onMouseMove?: MouseEventHandler<HTMLDivElement>;
onMouseOut?: MouseEventHandler<HTMLDivElement>;
/**
* @default: false
*/
followPointer?: boolean;
}

const Orbit = forwardRef<HTMLDivElement, OrbitProps>(
(
{
children,
className,
classNames = {},
styles: outStyles = {},
style,
width = 400,
height = 560,
delay = 500,
onMouseOut,
onMouseMove,
sensitivity = 1,
orbitDirection = 'both',
followPointer,
...res
},
ref,
) => {
const { styles, cx } = useStyles();

const [{ rotate }, api] = useSpring(() => ({
rotate: [0, 0],
}));

const handleInteract = (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)),
};
const center = { x: percent.x - 50, y: percent.y - 50 };

let x = 0,
y = 0;

const calcX = () =>
// 使用 followPointer 时,移动跟随鼠标方向
round((followPointer ? 1 : -1) * (center.x / 3.5) * sensitivity);

const calcY = () => round((center.y / 2) * sensitivity);

switch (orbitDirection) {
case 'both':
x = calcX();
y = calcY();
break;

case 'horizontal':
x = calcX();
break;

case 'vertical':
y = calcY();
break;
}

api.start({ rotate: [x, y] });
};

const handleInteractEnd = (delayTime: number) => {
setTimeout(() => {
api.start({ rotate: [0, 0] });
}, delayTime);
};

return (
<animated.div
className={cx(styles.container, classNames.container)}
style={
{
'--card-aspect': width / height,
'--rotate-x': rotate.to((x) => `${x}deg`),
'--rotate-y': rotate.to((_, y) => `${y}deg`),
width,
height,
...outStyles.container,
} as CSSProperties
}
ref={ref}
{...res}
>
<div className={cx(styles.translator, classNames.translator)} style={outStyles.translator}>
<div
className={cx(styles.rotator, className, classNames.rotator)}
onMouseMove={(event) => {
handleInteract(event);
onMouseMove?.(event);
}}
onMouseOut={(event) => {
handleInteractEnd(delay);
onMouseOut?.(event);
}}
style={{ ...style, ...outStyles.rotator }}
>
<div className={cx(styles.content, classNames.content)} style={outStyles.content}>
{children}
</div>
</div>
</div>
</animated.div>
);
},
);

export default Orbit;
81 changes: 81 additions & 0 deletions src/HolographicCard/components/Orbit/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { createStyles } from 'antd-style';

export const useStyles = createStyles(({ css, cx }) => {
const prefix = `aha-orbit`;

const contour = css`
//--card-radius: 4.55% / 3.5%;
aspect-ratio: var(--card-aspect);
border-radius: var(--card-radius);
`;

const common = css`
display: grid;
// TODO: 看下600px 是否是动态值
perspective: 600px;
will-change: transform, box-shadow;
transform-origin: center;
`;

return {
container: cx(
`${prefix}-container`,
css`
/* place the card on a new transform layer and
make sure it has hardward acceleration... we gun'need that! */
transform: translate3d(0px, 0px, 0.01px);
transform-style: preserve-3d;
/* make sure the card is above others if it's scaled up */
z-index: calc(var(--card-scale) * 2);
/* every little helps! */
will-change: transform, visibility, z-index;
${contour};
/* outline is a little trick to anti-alias */
outline: 1px solid transparent;
& * {
outline: 1px solid transparent;
}
`,
),
rotator: cx(
`${prefix}-rotator`,
css`
${contour}
${common}
transform: rotateY(var(--rotate-x)) rotateX(var(--rotate-y));
transform-style: preserve-3d;
/* performance */
pointer-events: auto;
/* overflow: hidden; <-- this improves perf on mobile, but breaks backface visibility. */
/* isolation: isolate; <-- this improves perf, but breaks backface visibility on Chrome. */
`,
),
translator: cx(
`${prefix}-translator`,
css`
${common};
width: auto;
position: relative;
transform: translate3d(var(--translate-x), var(--translate-y), 0.1px)
scale(var(--card-scale));
`,
),
content: css`
height: 100%;
`,
};
});
126 changes: 126 additions & 0 deletions src/HolographicCard/components/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { createStyles } from 'antd-style';

export const useStyles = createStyles(({ css, cx }) => {
const perf = css`
will-change: transform, opacity, background-image, background-size, background-position,
background-blend-mode, filter;
`;

const shadow = css`
transition: box-shadow 0.4s ease, opacity 0.33s ease-out;
box-shadow: rgba(255, 255, 255, 0.1) 0px 1px 1px 0px inset,
rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px;
`;

const view = css`
width: 100%;
grid-area: 1/1;
aspect-ratio: var(--card-aspect);
border-radius: var(--card-radius);
image-rendering: optimizeQuality;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
pointer-events: none;
overflow: hidden;
`;

return {
container: css`
--card-radius: 4.55% / 3.5%;
--card-back: hsl(220, 52%, 6%);
--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);
--space: 5%;
--angle: 133deg;
--imgsize: cover;
`,

active: cx(shadow),
shine: cx(
perf,
css`
${view}
`,
),
glare: cx(
perf,
css`
/* make sure the glare doesn't clip */
transform: translateZ(1.41px);
overflow: hidden;
${view};
background-image: radial-gradient(
farthest-corner circle at var(--pointer-x) var(--pointer-y),
hsl(0, 0%, 75%) 5%,
hsl(200, 5%, 35%) 60%,
hsl(320, 40%, 10%) 150%
);
//background-size: 120% 150%;
background-position: center center;
//mix-blend-mode: hard-light;
opacity: calc(var(--card-opacity) * 0.75);
mix-blend-mode: multiply;
filter: brightness(1.5) contrast(1.4) saturate(1);
background-size: 170% 170%;
`,
),

front: css`
& * {
backface-visibility: hidden;
}
opacity: 1;
backface-visibility: hidden;
transition: opacity 0.33s ease-out;
transform: translate3d(0px, 0px, 0.01px);
display: grid;
${view}
`,
back: css`
background-color: var(--card-back);
transform: rotateY(180deg) translateZ(1px);
backface-visibility: visible;
${view}
`,
fontLoading: css`
opacity: 0;
`,
backLoading: css`
transform: rotateY(0deg);
`,

rotator: css`
${shadow}
img {
height: auto;
transform: translate3d(0px, 0px, 0.01px);
}
`,
};
});
14 changes: 14 additions & 0 deletions src/HolographicCard/demos/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { HolographicCard } from '@ant-design/pro-editor';
import { Button } from 'antd';
import { Center } from 'react-layout-kit';

const Demo = () => (
<HolographicCard>
<Center gap={16} style={{ height: '100%' }}>
<Button type={'primary'}>主按钮</Button>
<Button>普通按钮</Button>
</Center>
</HolographicCard>
);

export default Demo;
15 changes: 15 additions & 0 deletions src/HolographicCard/demos/default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { HolographicCard } from '@ant-design/pro-editor';
import { Center } from 'react-layout-kit';

const Demo = () => {
return (
<Center height={800}>
<HolographicCard
mask={'https://gw.alipayobjects.com/zos/kitchen/nbf5vouUl/illusion.png'}
img="https://registry.npmmirror.com/@v-idol/vidol-agent-sample-b/1.0.0/files/cover.png"
/>
</Center>
);
};

export default Demo;
10 changes: 10 additions & 0 deletions src/HolographicCard/demos/pure.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { HolographicCard } from '@ant-design/pro-editor';
import { Center } from 'react-layout-kit';

const Demo = () => (
<HolographicCard>
<Center gap={16} style={{ height: '100%' }} />
</HolographicCard>
);

export default Demo;
18 changes: 18 additions & 0 deletions src/HolographicCard/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
title: HolographicCard 全息卡片
group:
title: 复合组件
order: 10
---

# HolographicCard

搭配 Orbit + LaserShine 实现:

## 效果预览

<code src="./demos/default.tsx"></code>

## 不带图片

<code src="./demos/pure.tsx"></code>
69 changes: 69 additions & 0 deletions src/HolographicCard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createStyles } from 'antd-style';
import { ReactNode, memo, useEffect, useState } from 'react';

import Container from './components/Container';

const useStyles = createStyles(({ css }) => ({
img: css`
width: 100%;
grid-area: 1/1;
aspect-ratio: var(--card-aspect);
border-radius: var(--card-radius);
image-rendering: optimizeQuality;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
`,
}));

export interface HolographicCardProps {
img?: string;
back?: string;
foil?: string;
mask?: string;
children?: ReactNode;
}

const HolographicCard = memo<HolographicCardProps>(({ img = '', mask, children }) => {
const [loading, setLoading] = useState(true);
const { styles } = useStyles();
useEffect(() => {
if (children)
setTimeout(() => {
setLoading(false);
}, 500);
}, []);

return (
<Container mask={mask} loading={loading}>
{children ? (
<div
className={'card_children_container'}
style={{
height: '100%',
position: 'absolute',
width: '100%',
}}
>
{children}
</div>
) : (
<img
className={styles.img}
src={img}
onLoad={() => {
setTimeout(() => {
setLoading(false);
}, 500);
}}
loading="lazy"
width="660"
height="921"
/>
)}
</Container>
);
});

export default HolographicCard;
11 changes: 11 additions & 0 deletions src/HolographicCard/store/card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createWithEqualityFn } from 'zustand/traditional';

type ActiveCardStore = {
activeCard: HTMLDivElement | undefined | null;
setActiveCard: (card: HTMLDivElement | undefined | null) => void;
};

export const useActiveCard = createWithEqualityFn<ActiveCardStore>((set) => ({
activeCard: undefined,
setActiveCard: (card) => set(() => ({ activeCard: card })),
}));
37 changes: 37 additions & 0 deletions src/HolographicCard/utils/math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* return a value that has been rounded to a set precision
* @param {Number} value the value to round
* @param {Number} precision the precision (decimal places), default: 3
* @returns {Number}
*/
const round = (value: number, precision: number = 3) => parseFloat(value.toFixed(precision));

/**
* return a value that has been limited between min & max
* @param {Number} value the value to clamp
* @param {Number} min minimum value to allow, default: 0
* @param {Number} max maximum value to allow, default: 100
* @returns {Number}
*/
const clamp = (value: number, min = 0, max = 100) => {
return Math.min(Math.max(value, min), max);
};

/**
* return a value that has been re-mapped according to the from/to
* - for example, adjust(10, 0, 100, 100, 0) = 90
* @param {Number} value the value to re-map (or adjust)
* @param {Number} fromMin min value to re-map from
* @param {Number} fromMax max value to re-map from
* @param {Number} toMin min value to re-map to
* @param {Number} toMax max value to re-map to
* @returns {Number}
*/
const adjust = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
return round(toMin + ((toMax - toMin) * (value - fromMin)) / (fromMax - fromMin));
};

// const degToRad = () => {
//
// }
export { adjust, clamp, round };
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ export { default as ErrorBoundary } from './ErrorBoundary';
export { default as FreeCanvas } from './FreeCanvas';
export type { FreeCanvasProps } from './FreeCanvas';
export * from './Highlight';
export { default as HolographicCard } from './HolographicCard';
export * from './IconPicker';
export * from './InteractContainer';
export { Layout as EditorLayout } from './Layout';

0 comments on commit d197fb2

Please sign in to comment.