diff --git a/package.json b/package.json index ab63f4eb..2101c7cc 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/HolographicCard/components/Container.tsx b/src/HolographicCard/components/Container.tsx new file mode 100644 index 00000000..412b7412 --- /dev/null +++ b/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(({ back, foil, mask, children, className, loading }) => { + const { styles, cx } = useStyles(); + const { style: shineStyle, onMouseMove, onMouseOut } = useLaserShine(); + + return ( + + +
+ {children} + +
+
+ + ); +}); + +export default Container; diff --git a/src/HolographicCard/components/LaserShine/LaserShine.tsx b/src/HolographicCard/components/LaserShine/LaserShine.tsx new file mode 100644 index 00000000..9168bf2e --- /dev/null +++ b/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(({ mask, className, ...res }) => { + const { styles, cx } = useStyles(); + + console.log(className); + return ( + + ); +}); diff --git a/src/HolographicCard/components/LaserShine/index.ts b/src/HolographicCard/components/LaserShine/index.ts new file mode 100644 index 00000000..733ac304 --- /dev/null +++ b/src/HolographicCard/components/LaserShine/index.ts @@ -0,0 +1,2 @@ +export * from './LaserShine'; +export * from './useLaserShine'; diff --git a/src/HolographicCard/components/LaserShine/style.ts b/src/HolographicCard/components/LaserShine/style.ts new file mode 100644 index 00000000..e6c81e30 --- /dev/null +++ b/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} + } + `, + }; +}); diff --git a/src/HolographicCard/components/LaserShine/useLaserShine.ts b/src/HolographicCard/components/LaserShine/useLaserShine.ts new file mode 100644 index 00000000..df908440 --- /dev/null +++ b/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 }; +}; diff --git a/src/HolographicCard/components/Orbit/index.tsx b/src/HolographicCard/components/Orbit/index.tsx new file mode 100644 index 00000000..e82c94b8 --- /dev/null +++ b/src/HolographicCard/components/Orbit/index.tsx @@ -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; + onMouseOut?: MouseEventHandler; + /** + * @default: false + */ + followPointer?: boolean; +} + +const Orbit = forwardRef( + ( + { + 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 ( + `${x}deg`), + '--rotate-y': rotate.to((_, y) => `${y}deg`), + width, + height, + ...outStyles.container, + } as CSSProperties + } + ref={ref} + {...res} + > +
+
{ + handleInteract(event); + onMouseMove?.(event); + }} + onMouseOut={(event) => { + handleInteractEnd(delay); + onMouseOut?.(event); + }} + style={{ ...style, ...outStyles.rotator }} + > +
+ {children} +
+
+
+
+ ); + }, +); + +export default Orbit; diff --git a/src/HolographicCard/components/Orbit/styles.ts b/src/HolographicCard/components/Orbit/styles.ts new file mode 100644 index 00000000..58561add --- /dev/null +++ b/src/HolographicCard/components/Orbit/styles.ts @@ -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%; + `, + }; +}); diff --git a/src/HolographicCard/components/style.ts b/src/HolographicCard/components/style.ts new file mode 100644 index 00000000..b10d990a --- /dev/null +++ b/src/HolographicCard/components/style.ts @@ -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); + } + `, + }; +}); diff --git a/src/HolographicCard/demos/button.tsx b/src/HolographicCard/demos/button.tsx new file mode 100644 index 00000000..834aeb78 --- /dev/null +++ b/src/HolographicCard/demos/button.tsx @@ -0,0 +1,14 @@ +import { HolographicCard } from '@ant-design/pro-editor'; +import { Button } from 'antd'; +import { Center } from 'react-layout-kit'; + +const Demo = () => ( + +
+ + +
+
+); + +export default Demo; diff --git a/src/HolographicCard/demos/default.tsx b/src/HolographicCard/demos/default.tsx new file mode 100644 index 00000000..76941a8a --- /dev/null +++ b/src/HolographicCard/demos/default.tsx @@ -0,0 +1,15 @@ +import { HolographicCard } from '@ant-design/pro-editor'; +import { Center } from 'react-layout-kit'; + +const Demo = () => { + return ( +
+ +
+ ); +}; + +export default Demo; diff --git a/src/HolographicCard/demos/pure.tsx b/src/HolographicCard/demos/pure.tsx new file mode 100644 index 00000000..857ec801 --- /dev/null +++ b/src/HolographicCard/demos/pure.tsx @@ -0,0 +1,10 @@ +import { HolographicCard } from '@ant-design/pro-editor'; +import { Center } from 'react-layout-kit'; + +const Demo = () => ( + +
+ +); + +export default Demo; diff --git a/src/HolographicCard/index.md b/src/HolographicCard/index.md new file mode 100644 index 00000000..28911b12 --- /dev/null +++ b/src/HolographicCard/index.md @@ -0,0 +1,18 @@ +--- +title: HolographicCard 全息卡片 +group: + title: 复合组件 + order: 10 +--- + +# HolographicCard + +搭配 Orbit + LaserShine 实现: + +## 效果预览 + + + +## 不带图片 + + diff --git a/src/HolographicCard/index.tsx b/src/HolographicCard/index.tsx new file mode 100644 index 00000000..cf4858c9 --- /dev/null +++ b/src/HolographicCard/index.tsx @@ -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(({ img = '', mask, children }) => { + const [loading, setLoading] = useState(true); + const { styles } = useStyles(); + useEffect(() => { + if (children) + setTimeout(() => { + setLoading(false); + }, 500); + }, []); + + return ( + + {children ? ( +
+ {children} +
+ ) : ( + { + setTimeout(() => { + setLoading(false); + }, 500); + }} + loading="lazy" + width="660" + height="921" + /> + )} +
+ ); +}); + +export default HolographicCard; diff --git a/src/HolographicCard/store/card.ts b/src/HolographicCard/store/card.ts new file mode 100644 index 00000000..d6ce0e08 --- /dev/null +++ b/src/HolographicCard/store/card.ts @@ -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((set) => ({ + activeCard: undefined, + setActiveCard: (card) => set(() => ({ activeCard: card })), +})); diff --git a/src/HolographicCard/utils/math.ts b/src/HolographicCard/utils/math.ts new file mode 100644 index 00000000..3c92a2f5 --- /dev/null +++ b/src/HolographicCard/utils/math.ts @@ -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 }; diff --git a/src/index.ts b/src/index.ts index 821427b7..0fff8757 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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';