├── .releaserc
├── example
├── static
│ ├── styles
│ │ └── index.css
│ └── images
│ │ ├── layer-1.png
│ │ ├── layer-2.png
│ │ └── layer-3.png
├── index.html
└── index.tsx
├── .prettierrc
├── commitlint.config.js
├── lib
├── index.ts
├── utils.ts
└── LayeredImage.tsx
├── tsconfig.json
├── .gitignore
├── .editorconfig
├── webpack.common.js
├── .travis.yml
├── dist
├── index.d.ts
├── index.js
└── index.js.map
├── types
└── LayeredImage.d.ts
├── webpack.dev.js
├── webpack.prod.js
├── LICENSE
├── package.json
├── .eslintrc
└── README.md
/.releaserc:
--------------------------------------------------------------------------------
1 | {
2 | "branches": ["main"]
3 | }
4 |
--------------------------------------------------------------------------------
/example/static/styles/index.css:
--------------------------------------------------------------------------------
1 | #root {
2 | }
3 |
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "semi": false,
4 | "trailingComma": "all"
5 | }
6 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["@commitlint/config-conventional"],
3 | };
4 |
--------------------------------------------------------------------------------
/example/static/images/layer-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llorca/react-layered-image/HEAD/example/static/images/layer-1.png
--------------------------------------------------------------------------------
/example/static/images/layer-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llorca/react-layered-image/HEAD/example/static/images/layer-2.png
--------------------------------------------------------------------------------
/example/static/images/layer-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llorca/react-layered-image/HEAD/example/static/images/layer-3.png
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | import { ILayeredImageProps, LayeredImage } from "./LayeredImage"
2 |
3 | export { ILayeredImageProps, LayeredImage }
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "sourceMap": true,
7 | "target": "es5"
8 | },
9 | "include": ["./lib/**/*"],
10 | "exclude": ["node_modules"]
11 | }
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | # Production
5 | lib/**/*.js
6 |
7 | # Misc
8 | .DS_Store
9 | .env.local
10 | .env.development.local
11 | .env.test.local
12 | .env.production.local
13 |
14 | npm-debug.log*
15 | yarn-debug.log*
16 | yarn-error.log*
17 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | react-layered-image
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | module: {
3 | rules: [
4 | {
5 | test: /\.tsx?$/,
6 | loader: "ts-loader",
7 | exclude: /node_modules/,
8 | },
9 | ],
10 | },
11 | resolve: {
12 | extensions: [".ts", ".tsx", ".js", "jsx"],
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | cache:
3 | directories:
4 | - ~/.npm
5 | notifications:
6 | email: false
7 | node_js:
8 | - "12"
9 | - "10"
10 | script:
11 | - npm run build
12 | after_success:
13 | - npm run release
14 | deploy:
15 | provider: pages
16 | skip_cleanup: true
17 | github_token: $GH_TOKEN
18 | local_dir: ./example/build
19 | on:
20 | branch: main
21 | branches:
22 | except:
23 | - /^v\d+\.\d+\.\d+$/
24 |
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | export interface ILayeredImageProps extends React.HTMLProps {
3 | layers: Array
4 | aspectRatio?: number
5 | borderRadius?: React.CSSProperties["borderRadius"]
6 | transitionDuration?: number
7 | transitionTimingFunction?: React.CSSProperties["transitionTimingFunction"]
8 | lightColor?: React.CSSProperties["color"]
9 | lightOpacity?: React.CSSProperties["opacity"]
10 | shadowColor?: React.CSSProperties["color"]
11 | shadowOpacity?: React.CSSProperties["opacity"]
12 | }
13 | export declare const LayeredImage: React.FC
14 |
--------------------------------------------------------------------------------
/types/LayeredImage.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | export interface ILayeredImageProps extends React.HTMLProps {
3 | layers: Array
4 | aspectRatio?: number
5 | borderRadius?: React.CSSProperties["borderRadius"]
6 | transitionDuration?: number
7 | transitionTimingFunction?: React.CSSProperties["transitionTimingFunction"]
8 | lightColor?: React.CSSProperties["color"]
9 | lightOpacity?: React.CSSProperties["opacity"]
10 | shadowColor?: React.CSSProperties["color"]
11 | shadowOpacity?: React.CSSProperties["opacity"]
12 | }
13 | export declare const LayeredImage: React.FC
14 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { render } from "react-dom"
3 |
4 | import { LayeredImage } from "../lib"
5 |
6 | const style: React.CSSProperties = {
7 | position: "absolute",
8 | top: 0,
9 | right: 0,
10 | bottom: 0,
11 | left: 0,
12 | padding: 30,
13 | display: "flex",
14 | justifyContent: "center",
15 | alignItems: "center",
16 | backgroundColor: "#e1e8ed",
17 | }
18 |
19 | render(
20 |
21 | `./static/images/layer-${index}.png`)}
23 | shadowColor="#1f2933"
24 | style={{ width: 400 }}
25 | />
26 |
,
27 | document.getElementById("root"),
28 | )
29 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Apply a set of styles to an HTML element.
3 | */
4 | export const applyStyles = (element: HTMLDivElement, styles: React.CSSProperties) => {
5 | for (const [style, value] of Object.entries(styles)) {
6 | element.style[style] = value
7 | }
8 | }
9 |
10 | /**
11 | * Clamp the `value` between `min` and `max` inclusively.
12 | */
13 | export const clamp = (value: number, min: number, max: number) => {
14 | const maximum = max < min ? min : max
15 |
16 | return value != null ? Math.min(Math.max(value, min), maximum) : value
17 | }
18 |
19 | /**
20 | * Return `true` if the value is a `function`.
21 | */
22 | // eslint-disable-next-line @typescript-eslint/ban-types
23 | export const isFunction = (value: unknown): value is Function => typeof value === "function"
24 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 | const { CleanWebpackPlugin } = require("clean-webpack-plugin")
3 | const CopyWebpackPlugin = require("copy-webpack-plugin")
4 |
5 | const common = require("./webpack.common.js")
6 |
7 | module.exports = {
8 | ...common,
9 | mode: "development",
10 | entry: "./example/index.tsx",
11 | output: {
12 | path: path.resolve(__dirname, "./example/build"),
13 | filename: "index.js",
14 | library: "react-layered-image",
15 | libraryTarget: "umd",
16 | globalObject: "this",
17 | },
18 | devServer: {
19 | static: "./example/build",
20 | host: "0.0.0.0",
21 | port: 8080,
22 | },
23 | plugins: [
24 | new CleanWebpackPlugin({
25 | cleanAfterEveryBuildPatterns: ["example/build"],
26 | }),
27 | new CopyWebpackPlugin({
28 | patterns: [
29 | {
30 | from: "./example/index.html",
31 | to: "./index.html",
32 | },
33 | {
34 | from: "./example/static/",
35 | to: "./static/",
36 | },
37 | ],
38 | }),
39 | ],
40 | }
41 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 | const webpack = require("webpack")
3 | const { CleanWebpackPlugin } = require("clean-webpack-plugin")
4 | const CopyWebpackPlugin = require("copy-webpack-plugin")
5 | const UglifyJSPlugin = require("uglifyjs-webpack-plugin")
6 |
7 | const common = require("./webpack.common.js")
8 |
9 | module.exports = {
10 | ...common,
11 | mode: "production",
12 | entry: "./lib/index.ts",
13 | output: {
14 | path: path.resolve(__dirname, "dist"),
15 | filename: "index.js",
16 | library: "react-layered-image",
17 | libraryTarget: "umd",
18 | globalObject: "this",
19 | },
20 | devtool: "source-map",
21 | externals: {
22 | react: "react",
23 | },
24 | plugins: [
25 | new CleanWebpackPlugin(),
26 | new CopyWebpackPlugin({
27 | patterns: [
28 | {
29 | from: "types/LayeredImage.d.ts",
30 | to: "index.d.ts",
31 | },
32 | ],
33 | }),
34 | new UglifyJSPlugin({
35 | sourceMap: true,
36 | }),
37 | new webpack.DefinePlugin({
38 | "process.env.NODE_ENV": '"production"',
39 | }),
40 | ],
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017-present, Antoine Llorca
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-layered-image",
3 | "description": "An interactive, multi-layer image component for React",
4 | "version": "2.0.9",
5 | "author": "Antoine Llorca",
6 | "main": "dist/index.js",
7 | "types": "dist/index.d.ts",
8 | "files": [
9 | "dist"
10 | ],
11 | "scripts": {
12 | "dev": "webpack-dev-server --progress --config webpack.dev.js",
13 | "build:example": "webpack --config webpack.dev.js",
14 | "build:dist": "webpack --config webpack.prod.js",
15 | "build": "npm run build:dist && npm run build:example",
16 | "commitmsg": "commitlint -e $GIT_PARAMS",
17 | "commit": "cz",
18 | "release": "npm run build && semantic-release"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/llorca/react-layered-image.git"
23 | },
24 | "config": {
25 | "commitizen": {
26 | "path": "./node_modules/cz-conventional-changelog"
27 | }
28 | },
29 | "peerDependencies": {
30 | "react": "^18.2.0",
31 | "react-dom": "^18.2.0"
32 | },
33 | "devDependencies": {
34 | "@commitlint/cli": "^17.0.3",
35 | "@commitlint/config-conventional": "^17.0.3",
36 | "@semantic-release/github": "^8.0.4",
37 | "@semantic-release/npm": "^9.0.1",
38 | "@types/react": "^18.0.14",
39 | "@types/react-dom": "^18.0.5",
40 | "@typescript-eslint/eslint-plugin": "^5.30.5",
41 | "@typescript-eslint/parser": "^5.30.5",
42 | "clean-webpack-plugin": "^4.0.0",
43 | "commitizen": "^4.2.1",
44 | "copy-webpack-plugin": "^11.0.0",
45 | "cz-conventional-changelog": "^3.3.0",
46 | "eslint": "^8.19.0",
47 | "eslint-config-prettier": "^8.5.0",
48 | "eslint-plugin-import": "^2.22.0",
49 | "eslint-plugin-prettier": "^4.2.1",
50 | "eslint-plugin-react": "^7.20.6",
51 | "eslint-plugin-simple-import-sort": "^7.0.0",
52 | "husky": "^8.0.1",
53 | "prettier": "^2.1.1",
54 | "react": "^18.2.0",
55 | "react-dom": "^18.2.0",
56 | "semantic-release": "^19.0.3",
57 | "ts-loader": "^9.3.1",
58 | "typescript": "^4.0.2",
59 | "uglifyjs-webpack-plugin": "^2.2.0",
60 | "webpack": "^5.73.0",
61 | "webpack-cli": "^4.10.0",
62 | "webpack-dev-server": "^4.9.3"
63 | },
64 | "license": "MIT"
65 | }
66 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:react/recommended",
9 | "plugin:import/recommended",
10 | "plugin:import/typescript",
11 | "plugin:@typescript-eslint/recommended",
12 | "prettier/@typescript-eslint",
13 | "plugin:prettier/recommended"
14 | ],
15 | "parser": "@typescript-eslint/parser",
16 | "parserOptions": {
17 | "ecmaVersion": 2020,
18 | "sourceType": "module",
19 | "ecmaFeatures": {
20 | "jsx": true
21 | }
22 | },
23 | "plugins": ["simple-import-sort"],
24 | "rules": {
25 | "@typescript-eslint/array-type": "off",
26 | "@typescript-eslint/ban-ts-comment": "warn",
27 | "@typescript-eslint/explicit-function-return-type": "off",
28 | "@typescript-eslint/explicit-member-accessibility": "off",
29 | "@typescript-eslint/explicit-module-boundary-types": "off",
30 | "@typescript-eslint/interface-name-prefix": "off",
31 | "@typescript-eslint/no-empty-function": "off",
32 | "@typescript-eslint/no-empty-interface": "off",
33 | "@typescript-eslint/no-explicit-any": "off",
34 | "@typescript-eslint/no-use-before-define": "off",
35 | "import/no-extraneous-dependencies": "error",
36 | "import/no-named-as-default": "off",
37 | "import/no-unresolved": "off",
38 | "import/order": "off",
39 | "no-undef": "off",
40 | "react/display-name": "off",
41 | "react/no-unescaped-entities": "off",
42 | "react/prop-types": "off",
43 | "simple-import-sort/sort": ["error", {
44 | "groups": [
45 | // Node.js built-in modules
46 | [
47 | "^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)"
48 | ],
49 | // External packages
50 | // React comes first
51 | ["^react$", "^@?\\w"],
52 | // Internal packages
53 | ["^(@\\w)(/.*|$)"],
54 | // Side effect imports
55 | ["^\\u0000"],
56 | // Parent imports
57 | // Put `..` last
58 | ["^\\.\\.(?!/?$)", "^\\.\\./?$"],
59 | // Other relative imports
60 | // Put same-folder imports and `.` last
61 | ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
62 | // Style imports
63 | ["^.+\\.s?css$"]
64 | ]
65 | }]
66 | },
67 | "settings": {
68 | "react": {
69 | "version": "detect"
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-layered-image
2 |
3 | [react-layered-image](https://llorca.github.io/react-layered-image/) is an interactive, multi-layer image component for
4 | [React](https://reactjs.org/), inspired by the
5 | [Apple TV layered images](https://developer.apple.com/tvos/human-interface-guidelines/icons-and-images/layered-images/).
6 |
7 | #### Features
8 |
9 | * Runs at 60fps on Chrome 54+, Firefox 49+, Safari 6.1+
10 | * Preserves aspect ratio through resizing
11 | * Loads images asynchronously
12 |
13 | ## Installation
14 |
15 | ```
16 | npm install react-layered-image
17 | ```
18 |
19 | ## Basic example
20 |
21 | ```js
22 | import * as React from "react";
23 | import { render } from "react-dom";
24 |
25 | import { LayeredImage } from "react-layered-image";
26 |
27 | const style = {
28 | position: "absolute",
29 | top: 0,
30 | right: 0,
31 | bottom: 0,
32 | left: 0,
33 | display: "flex",
34 | justifyContent: "center",
35 | alignItems: "center",
36 | };
37 |
38 | const layers = [
39 | "https://llorca.github.io/react-layered-image/static/images/layer-1.png",
40 | "https://llorca.github.io/react-layered-image/static/images/layer-2.png",
41 | "https://llorca.github.io/react-layered-image/static/images/layer-3.png",
42 | ];
43 |
44 | render(
45 |
46 |
47 |
,
48 | document.getElementById("root"),
49 | );
50 | ```
51 |
52 | ## API
53 |
54 | By default, `LayeredImage` has a width of `100%`. You can set the CSS
55 | [`width`](https://developer.mozilla.org/en-US/docs/Web/CSS/width) property via a class name or via the `style` prop
56 | directly. You can use any length or percentage value.
57 |
58 | | Prop | Type | Default | Description |
59 | | -------------------------- | --------------- | ------------ | --------------------------------------------------------------------------------- |
60 | | `layers` | `Array` | | **Required**. Array of image URLs. Use images of same dimension for best results. |
61 | | `aspectRatio` | `number` | `16 / 9` | Aspect ratio (`width / height`) of the element. |
62 | | `borderRadius` | `number` | `6` | Radius of the element. |
63 | | `transitionDuration` | `number` | `0.15` | Duration of the transition. |
64 | | `transitionTimingFunction` | `string` | `"ease-out"` | Timing function of the transition. |
65 | | `lightColor` | `string` | `"#fff"` | Color of the light element. |
66 | | `lightOpacity` | `number` | `0.2` | Opacity of the light element. |
67 | | `shadowColor` | `string` | `"#000"` | Color of the shadow element. |
68 | | `shadowOpacity` | `number` | `0.6` | Opacity of the shadow element. |
69 |
70 | ## Development
71 |
72 | Start the [webpack](https://github.com/webpack/webpack) development server:
73 |
74 | ```
75 | npm run dev
76 | ```
77 |
78 | Use [Commitizen](https://github.com/commitizen/cz-cli) to commit changes:
79 |
80 | ```
81 | npm run commit
82 | ```
83 |
84 | ## License
85 |
86 | [MIT](./LICENSE)
87 |
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["react"],t):"object"==typeof exports?exports["react-layered-image"]=t(require("react")):e["react-layered-image"]=t(e.react)}(this,(function(e){return n={},t.m=r=[function(t,r){t.exports=e},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.LayeredImage=void 0;var n=r(2);Object.defineProperty(t,"LayeredImage",{enumerable:!0,get:function(){return n.LayeredImage}})},function(e,t,r){"use strict";var n=this&&this.__assign||function(){return(n=Object.assign||function(e){for(var t,r=1,n=arguments.length;r React.CSSProperties)
23 | light?: React.CSSProperties
24 | shadow?: React.CSSProperties
25 | }
26 |
27 | export interface ILayeredImageProps extends React.HTMLProps {
28 | layers: Array
29 | aspectRatio?: number
30 | borderRadius?: React.CSSProperties["borderRadius"]
31 | transitionDuration?: number
32 | transitionTimingFunction?: React.CSSProperties["transitionTimingFunction"]
33 | lightColor?: React.CSSProperties["color"]
34 | lightOpacity?: React.CSSProperties["opacity"]
35 | shadowColor?: React.CSSProperties["color"]
36 | shadowOpacity?: React.CSSProperties["opacity"]
37 | }
38 |
39 | export const LayeredImage: React.FC = ({
40 | layers,
41 | aspectRatio = 16 / 10,
42 | borderRadius = 12,
43 | transitionDuration = 0.15,
44 | transitionTimingFunction = "ease-out",
45 | lightColor = "#fff",
46 | lightOpacity = 0.2,
47 | shadowColor = "#000",
48 | shadowOpacity = 0.6,
49 | className,
50 | style,
51 | }) => {
52 | const interactionRef = useRef(Interaction.None)
53 | const [size, setSize] = useState({ width: 0, height: 0 })
54 | const [loaded, setLoaded] = useState(0)
55 | const [, setError] = useState(0)
56 |
57 | const elementsRef = useRef({
58 | root: createRef(),
59 | container: createRef(),
60 | layers: layers.map(() => createRef()),
61 | shadow: createRef(),
62 | light: createRef(),
63 | })
64 |
65 | const defaultStyles = useMemo(
66 | () => getDefaultStyles(transitionDuration, lightColor, shadowColor),
67 | [transitionDuration, lightColor, shadowColor],
68 | )
69 | const styles = useMemo(
70 | () => ({
71 | root: {
72 | borderRadius,
73 | ...defaultStyles.root,
74 | ...staticStyles.root,
75 | },
76 | container: {
77 | borderRadius,
78 | transitionTimingFunction,
79 | ...defaultStyles.container,
80 | ...staticStyles.container,
81 | },
82 | stack: {
83 | borderRadius,
84 | ...defaultStyles.stack,
85 | ...staticStyles.stack,
86 | },
87 | layer: {
88 | transitionTimingFunction,
89 | ...defaultStyles.layer,
90 | ...staticStyles.layer,
91 | },
92 | light: {
93 | borderRadius,
94 | opacity: lightOpacity,
95 | ...defaultStyles.light,
96 | ...staticStyles.light,
97 | },
98 | shadow: {
99 | borderRadius,
100 | opacity: shadowOpacity,
101 | transitionTimingFunction,
102 | ...defaultStyles.shadow,
103 | ...staticStyles.shadow,
104 | },
105 | }),
106 | [defaultStyles, borderRadius, transitionTimingFunction, lightOpacity, shadowOpacity],
107 | )
108 |
109 | const getDimensions = () => {
110 | const containerRef = elementsRef.current.container
111 | // prettier-ignore
112 | const width =
113 | containerRef.current.offsetWidth ||
114 | containerRef.current.clientWidth ||
115 | containerRef.current.scrollWidth;
116 | const height = Math.round(width / aspectRatio)
117 |
118 | return { width, height }
119 | }
120 |
121 | const computeStyles = (
122 | interaction: Interaction = interactionRef.current,
123 | event?: React.SyntheticEvent,
124 | pageX?: number,
125 | pageY?: number,
126 | preventDefault = false,
127 | ) => {
128 | const { width, height } = interaction === Interaction.Resize ? getDimensions() : size
129 |
130 | const scrollLeft =
131 | document.documentElement.scrollLeft ||
132 | document.scrollingElement.scrollLeft ||
133 | window.scrollX ||
134 | window.pageXOffset
135 | // prettier-ignore
136 | const scrollTop =
137 | document.documentElement.scrollTop ||
138 | document.scrollingElement.scrollTop ||
139 | window.scrollY ||
140 | window.pageYOffset
141 |
142 | const containerRect = elementsRef.current.container.current.getBoundingClientRect()
143 |
144 | const offsetX = (pageX - containerRect.left - scrollLeft) / width
145 | const offsetY = (pageY - containerRect.top - scrollTop) / height
146 | const containerCenterX = pageX - containerRect.left - scrollLeft - width / 2
147 | const containerCenterY = pageY - containerRect.top - scrollTop - height / 2
148 |
149 | const containerRotationX = ((offsetY - containerCenterY) / (height / 2)) * 8
150 | const containerRotationY = ((containerCenterX - offsetX) / (width / 2)) * 8
151 | const layerTranslationX = (offsetX - containerCenterX) * 0.01
152 | const layerTranslationY = (offsetY - containerCenterY) * 0.01
153 | const lightAngle = (Math.atan2(containerCenterY, containerCenterX) * 180) / Math.PI - 90
154 |
155 | const computedStyles: ILayeredImageStyles = {
156 | [Interaction.None]: defaultStyles,
157 | [Interaction.Resize]: {
158 | root: {
159 | height: `${height}px`,
160 | transform: `perspective(${width * 3}px)`,
161 | },
162 | },
163 | [Interaction.Hover]: {
164 | container: {
165 | transform: `rotateX(${-clamp(containerRotationX, -8, 8)}deg)
166 | rotateY(${-clamp(containerRotationY, -8, 8)}deg)
167 | translateX(${-layerTranslationX * 5}px)
168 | translateY(${-layerTranslationY * 5}px)
169 | scale(1.1)`,
170 | },
171 | layer: (index: number) => ({
172 | transform: `translateX(${clamp(layerTranslationX, -2, 2) * 1.4 * index}px)
173 | translateY(${clamp(layerTranslationY, -2, 2) * 1.4 * index}px)
174 | scale(1.04)`,
175 | }),
176 | light: {
177 | backgroundImage: `linear-gradient(${lightAngle}deg, ${lightColor} 0%, transparent 80%)`,
178 | },
179 | shadow: {
180 | boxShadow: `0 40px 100px ${shadowColor}, 0 10px 20px ${shadowColor}`,
181 | },
182 | },
183 | [Interaction.Active]: {
184 | container: {
185 | transitionDuration: "0.075s",
186 | transform: `rotateX(${containerRotationX / 1.4}deg)
187 | rotateY(${containerRotationY / 1.4}deg)
188 | scale(1)`,
189 | },
190 | layer: (index: number) => ({
191 | transform: `translateX(${-layerTranslationX * index}px)
192 | translateY(${-layerTranslationY * index}px)
193 | scale(1.02)`,
194 | }),
195 | light: {
196 | backgroundImage: `linear-gradient(${lightAngle}deg, ${lightColor} 0%, transparent 80%)`,
197 | },
198 | shadow: {
199 | ...defaultStyles.shadow,
200 | transitionDuration: "0.075s",
201 | },
202 | },
203 | }[interaction]
204 |
205 | if (preventDefault) {
206 | event.preventDefault()
207 | }
208 |
209 | for (const [element, styles] of Object.entries(computedStyles)) {
210 | if (element === "layer") {
211 | layers.forEach((_, index) =>
212 | applyStyles(elementsRef.current.layers[index].current, isFunction(styles) ? styles(index) : styles),
213 | )
214 | } else {
215 | applyStyles(elementsRef.current[element].current, styles)
216 | }
217 | }
218 |
219 | if (interaction === Interaction.Resize) {
220 | setSize({ width, height })
221 | }
222 |
223 | interactionRef.current = interaction
224 | }
225 |
226 | // prettier-ignore
227 | const handleMouseInteraction =
228 | (interaction?: Interaction) =>
229 | (event: React.MouseEvent) =>
230 | computeStyles(interaction, event, event.pageX, event.pageY, true);
231 |
232 | // prettier-ignore
233 | const handleTouchInteraction =
234 | (interaction?: Interaction) =>
235 | (event: React.TouchEvent) =>
236 | computeStyles(interaction, event, event.touches[0].pageX, event.touches[0].pageY);
237 |
238 | const handleInteractionEnd = () => computeStyles(Interaction.None)
239 |
240 | useEffect(() => {
241 | const handleWindowResize = () => computeStyles(Interaction.Resize)
242 |
243 | layers.forEach((layer) => {
244 | const image = new Image()
245 |
246 | image.src = layer
247 | image.onload = () => setLoaded((loaded) => loaded + 1)
248 | image.onerror = () => setError((error) => error + 1)
249 | })
250 |
251 | window.addEventListener("resize", handleWindowResize)
252 |
253 | return () => {
254 | window.removeEventListener("resize", handleWindowResize)
255 | }
256 | }, [])
257 |
258 | useEffect(() => {
259 | computeStyles(Interaction.Resize)
260 | }, [elementsRef.current, aspectRatio])
261 |
262 | return (
263 |
276 |
277 |
278 |
279 | {layers.map((src, index) => (
280 |
289 | ))}
290 |
291 |
292 |
293 |
294 | )
295 | }
296 | LayeredImage.displayName = "LayeredImage"
297 |
298 | /*
299 | * Initial styles in resting state.
300 | */
301 | const getDefaultStyles = (
302 | transitionDuration: ILayeredImageProps["transitionDuration"],
303 | lightColor: ILayeredImageProps["lightColor"],
304 | shadowColor: ILayeredImageProps["shadowColor"],
305 | ): ILayeredImageStyles => ({
306 | container: {
307 | transform: "none",
308 | transitionDuration: `${transitionDuration}s`,
309 | },
310 | layer: {
311 | transform: "none",
312 | transitionDuration: `${transitionDuration}s, 500ms`,
313 | },
314 | light: {
315 | backgroundImage: `linear-gradient(180deg, ${lightColor} 0%, transparent 80%)`,
316 | },
317 | shadow: {
318 | boxShadow: `0 10px 30px ${shadowColor}, 0 6px 10px ${shadowColor}`,
319 | transitionDuration: `${transitionDuration}s`,
320 | },
321 | })
322 |
323 | /*
324 | * Static styles that never change.
325 | */
326 | const staticStyles: ILayeredImageStyles = {
327 | root: {
328 | position: "relative",
329 | width: "100%",
330 | transformStyle: "preserve-3d",
331 | cursor: "pointer",
332 | WebkitTapHighlightColor: "rgba(0, 0, 0, 0)",
333 | },
334 | container: {
335 | position: "absolute",
336 | width: "100%",
337 | height: "100%",
338 | transitionProperty: "transform",
339 | transformStyle: "preserve-3d",
340 | },
341 | stack: {
342 | position: "absolute",
343 | width: "100%",
344 | height: "100%",
345 | background: "black",
346 | transformStyle: "preserve-3d",
347 | overflow: "hidden",
348 | },
349 | layer: {
350 | position: "absolute",
351 | width: "100%",
352 | height: "100%",
353 | backgroundRepeat: "no-repeat",
354 | backgroundPosition: "center",
355 | backgroundColor: "transparent",
356 | backgroundSize: "cover",
357 | transitionProperty: "transform, opacity",
358 | opacity: 0,
359 | },
360 | light: {
361 | position: "absolute",
362 | width: "100%",
363 | height: "100%",
364 | },
365 | shadow: {
366 | position: "absolute",
367 | width: "100%",
368 | height: "100%",
369 | transitionProperty: "transform, box-shadow",
370 | transform: "translateZ(-10px) scale(0.95)",
371 | },
372 | }
373 |
--------------------------------------------------------------------------------
/dist/index.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["webpack://react-layered-image/webpack/universalModuleDefinition","webpack://react-layered-image/webpack/bootstrap","webpack://react-layered-image/external \"react\"","webpack://react-layered-image/./lib/index.ts","webpack://react-layered-image/./lib/LayeredImage.tsx","webpack://react-layered-image/./lib/utils.ts"],"names":["root","factory","exports","module","require","define","amd","this","__WEBPACK_EXTERNAL_MODULE__0__","installedModules","__webpack_require__","m","LayeredImage","Interaction","None","Resize","Hover","Active","computeStyles","_interaction","event","pageX","pageY","preventDefault","containerRef","width","elementsRef","current","container","offsetWidth","clientWidth","scrollWidth","height","Math","round","aspectRatio","size","scrollLeft","document","documentElement","scrollingElement","window","scrollX","pageXOffset","scrollTop","scrollY","pageYOffset","containerRect","getBoundingClientRect","offsetX","left","offsetY","top","containerCenterX","containerCenterY","containerRotationX","containerRotationY","layerTranslationX","layerTranslationY","lightAngle","atan2","PI","computedStyles","defaultStyles","transform","clamp","layer","index","light","backgroundImage","lightColor","shadow","boxShadow","shadowColor","transitionDuration","O","Object","entries","element","layers","forEach","_","applyStyles","isFunction","setSize","setInteraction","handleMouseInteraction","handleTouchInteraction","touches","handleInteractionEnd","a","borderRadius","transitionTimingFunction","lightOpacity","shadowOpacity","className","style","useState","interaction","loaded","setLoaded","setError","useRef","createRef","map","useMemo","getDefaultStyles","styles","staticStyles","stack","opacity","useEffect","handleWindowResize","image","Image","src","onload","onerror","error","addEventListener","removeEventListener","onMouseEnter","onMouseMove","onMouseDown","onMouseUp","onMouseLeave","onTouchStart","onTouchMove","onTouchEnd","ref","length","key","displayName","position","transformStyle","cursor","WebkitTapHighlightColor","transitionProperty","background","overflow","backgroundRepeat","backgroundPosition","backgroundColor","backgroundSize","value","isSafariDesktop","requestAnimationFrame","min","max","maximum","userAgent","navigator","vendor","test","c","d","name","getter","o","defineProperty","enumerable","get","r","Symbol","toStringTag","t","mode","__esModule","ns","create","bind","n","object","property","prototype","hasOwnProperty","call","p","s","moduleId","i","l","modules"],"mappings":"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","file":"index.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"react\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"react\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"react-layered-image\"] = factory(require(\"react\"));\n\telse\n\t\troot[\"react-layered-image\"] = factory(root[\"react\"]);\n})(this, function(__WEBPACK_EXTERNAL_MODULE__0__) {\nreturn "," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 1);\n","module.exports = __WEBPACK_EXTERNAL_MODULE__0__;","import { ILayeredImageProps, LayeredImage } from \"./LayeredImage\"\n\nexport { ILayeredImageProps, LayeredImage }\n","import * as React from \"react\"\nimport { createRef, useEffect, useMemo, useRef, useState } from \"react\"\n\nimport { applyStyles, clamp, isFunction } from \"./utils\"\n\ninterface Size {\n width: number\n height: number\n}\n\nenum Interaction {\n None = \"NONE\",\n Resize = \"RESIZE\",\n Hover = \"HOVER\",\n Active = \"ACTIVE\",\n}\n\ninterface ILayeredImageStyles {\n root?: React.CSSProperties\n container?: React.CSSProperties\n stack?: React.CSSProperties\n layer?: React.CSSProperties | ((index: number) => React.CSSProperties)\n light?: React.CSSProperties\n shadow?: React.CSSProperties\n}\n\nexport interface ILayeredImageProps extends React.HTMLProps {\n layers: Array\n aspectRatio?: number\n borderRadius?: React.CSSProperties[\"borderRadius\"]\n transitionDuration?: number\n transitionTimingFunction?: React.CSSProperties[\"transitionTimingFunction\"]\n lightColor?: React.CSSProperties[\"color\"]\n lightOpacity?: React.CSSProperties[\"opacity\"]\n shadowColor?: React.CSSProperties[\"color\"]\n shadowOpacity?: React.CSSProperties[\"opacity\"]\n}\n\nexport const LayeredImage: React.FC = ({\n layers,\n aspectRatio = 16 / 10,\n borderRadius = 12,\n transitionDuration = 0.15,\n transitionTimingFunction = \"ease-out\",\n lightColor = \"#fff\",\n lightOpacity = 0.2,\n shadowColor = \"#000\",\n shadowOpacity = 0.6,\n className,\n style,\n}) => {\n const [size, setSize] = useState({ width: 0, height: 0 })\n const [interaction, setInteraction] = useState(Interaction.None)\n const [loaded, setLoaded] = useState(0)\n const [, setError] = useState(0)\n\n const elementsRef = useRef({\n root: createRef(),\n container: createRef(),\n layers: layers.map(() => createRef()),\n shadow: createRef(),\n light: createRef(),\n })\n\n const defaultStyles = useMemo(\n () => getDefaultStyles(transitionDuration, lightColor, shadowColor),\n [transitionDuration, lightColor, shadowColor],\n )\n const styles = useMemo(\n () => ({\n root: {\n borderRadius,\n ...defaultStyles.root,\n ...staticStyles.root,\n },\n container: {\n borderRadius,\n transitionTimingFunction,\n ...defaultStyles.container,\n ...staticStyles.container,\n },\n stack: {\n borderRadius,\n ...defaultStyles.stack,\n ...staticStyles.stack,\n },\n layer: {\n transitionTimingFunction,\n ...defaultStyles.layer,\n ...staticStyles.layer,\n },\n light: {\n borderRadius,\n opacity: lightOpacity,\n ...defaultStyles.light,\n ...staticStyles.light,\n },\n shadow: {\n borderRadius,\n opacity: shadowOpacity,\n transitionTimingFunction,\n ...defaultStyles.shadow,\n ...staticStyles.shadow,\n },\n }),\n [defaultStyles, borderRadius, transitionTimingFunction, lightOpacity, shadowOpacity],\n )\n\n const getDimensions = () => {\n const containerRef = elementsRef.current.container\n // prettier-ignore\n const width =\n containerRef.current.offsetWidth ||\n containerRef.current.clientWidth ||\n containerRef.current.scrollWidth;\n const height = Math.round(width / aspectRatio)\n\n return { width, height }\n }\n\n const computeStyles = (\n _interaction: Interaction = interaction,\n event?: React.SyntheticEvent,\n pageX?: number,\n pageY?: number,\n preventDefault = false,\n ) => {\n const { width, height } = _interaction === Interaction.Resize ? getDimensions() : size\n\n const scrollLeft =\n document.documentElement.scrollLeft ||\n document.scrollingElement.scrollLeft ||\n window.scrollX ||\n window.pageXOffset\n // prettier-ignore\n const scrollTop =\n document.documentElement.scrollTop ||\n document.scrollingElement.scrollTop ||\n window.scrollY ||\n window.pageYOffset\n\n const containerRect = elementsRef.current.container.current.getBoundingClientRect()\n\n const offsetX = (pageX - containerRect.left - scrollLeft) / width\n const offsetY = (pageY - containerRect.top - scrollTop) / height\n const containerCenterX = pageX - containerRect.left - scrollLeft - width / 2\n const containerCenterY = pageY - containerRect.top - scrollTop - height / 2\n\n const containerRotationX = ((offsetY - containerCenterY) / (height / 2)) * 8\n const containerRotationY = ((containerCenterX - offsetX) / (width / 2)) * 8\n const layerTranslationX = (offsetX - containerCenterX) * 0.01\n const layerTranslationY = (offsetY - containerCenterY) * 0.01\n const lightAngle = (Math.atan2(containerCenterY, containerCenterX) * 180) / Math.PI - 90\n\n const computedStyles: ILayeredImageStyles = {\n [Interaction.None]: defaultStyles,\n [Interaction.Resize]: {\n root: {\n height: `${height}px`,\n transform: `perspective(${width * 3}px)`,\n },\n },\n [Interaction.Hover]: {\n container: {\n transform: `rotateX(${-clamp(containerRotationX, -8, 8)}deg)\n rotateY(${-clamp(containerRotationY, -8, 8)}deg)\n translateX(${-layerTranslationX * 5}px)\n translateY(${-layerTranslationY * 5}px)\n scale(1.1)`,\n },\n layer: (index: number) => ({\n transform: `translateX(${clamp(layerTranslationX, -2, 2) * 1.4 * index}px)\n translateY(${clamp(layerTranslationY, -2, 2) * 1.4 * index}px)\n scale(1.04)`,\n }),\n light: {\n backgroundImage: `linear-gradient(${lightAngle}deg, ${lightColor} 0%, transparent 80%)`,\n },\n shadow: {\n boxShadow: `0 40px 100px ${shadowColor}, 0 10px 20px ${shadowColor}`,\n },\n },\n [Interaction.Active]: {\n container: {\n transitionDuration: \"0.075s\",\n transform: `rotateX(${containerRotationX / 1.4}deg)\n rotateY(${containerRotationY / 1.4}deg)\n scale(1)`,\n },\n layer: (index: number) => ({\n transform: `translateX(${-layerTranslationX * index}px)\n translateY(${-layerTranslationY * index}px)\n scale(1.02)`,\n }),\n light: {\n backgroundImage: `linear-gradient(${lightAngle}deg, ${lightColor} 0%, transparent 80%)`,\n },\n shadow: {\n ...defaultStyles.shadow,\n transitionDuration: \"0.075s\",\n },\n },\n }[_interaction]\n\n if (preventDefault) {\n event.preventDefault()\n }\n\n for (const [element, styles] of Object.entries(computedStyles)) {\n if (element === \"layer\") {\n layers.forEach((_, index) =>\n applyStyles(elementsRef.current.layers[index].current, isFunction(styles) ? styles(index) : styles),\n )\n } else {\n applyStyles(elementsRef.current[element].current, styles)\n }\n }\n\n setSize({ width, height })\n setInteraction(_interaction)\n }\n\n // prettier-ignore\n const handleMouseInteraction =\n (_interaction?: Interaction) =>\n (event: React.MouseEvent) =>\n computeStyles(_interaction, event, event.pageX, event.pageY, true);\n\n // prettier-ignore\n const handleTouchInteraction =\n (_interaction?: Interaction) =>\n (event: React.TouchEvent) =>\n computeStyles(_interaction, event, event.touches[0].pageX, event.touches[0].pageY);\n\n const handleInteractionEnd = () => computeStyles(Interaction.None)\n\n useEffect(() => {\n const handleWindowResize = () => computeStyles(Interaction.Resize)\n\n layers.forEach((layer) => {\n const image = new Image()\n\n image.src = layer\n image.onload = () => setLoaded((loaded) => loaded + 1)\n image.onerror = () => setError((error) => error + 1)\n })\n\n window.addEventListener(\"resize\", handleWindowResize)\n\n return () => {\n window.removeEventListener(\"resize\", handleWindowResize)\n }\n }, [])\n\n useEffect(() => {\n computeStyles(Interaction.Resize)\n }, [elementsRef.current, aspectRatio])\n\n return (\n \n
\n
\n
\n {layers.map((src, index) => (\n
\n ))}\n
\n
\n
\n
\n )\n}\nLayeredImage.displayName = \"LayeredImage\"\n\n/*\n * Initial styles in resting state.\n */\nconst getDefaultStyles = (\n transitionDuration: ILayeredImageProps[\"transitionDuration\"],\n lightColor: ILayeredImageProps[\"lightColor\"],\n shadowColor: ILayeredImageProps[\"shadowColor\"],\n): ILayeredImageStyles => ({\n container: {\n transform: \"none\",\n transitionDuration: `${transitionDuration}s`,\n },\n layer: {\n transform: \"none\",\n transitionDuration: `${transitionDuration}s, 500ms`,\n },\n light: {\n backgroundImage: `linear-gradient(180deg, ${lightColor} 0%, transparent 80%)`,\n },\n shadow: {\n boxShadow: `0 10px 30px ${shadowColor}, 0 6px 10px ${shadowColor}`,\n transitionDuration: `${transitionDuration}s`,\n },\n})\n\n/*\n * Static styles that never change.\n */\nconst staticStyles: ILayeredImageStyles = {\n root: {\n position: \"relative\",\n width: \"100%\",\n transformStyle: \"preserve-3d\",\n cursor: \"pointer\",\n WebkitTapHighlightColor: \"rgba(0, 0, 0, 0)\",\n },\n container: {\n position: \"absolute\",\n width: \"100%\",\n height: \"100%\",\n transitionProperty: \"transform\",\n transformStyle: \"preserve-3d\",\n },\n stack: {\n position: \"absolute\",\n width: \"100%\",\n height: \"100%\",\n background: \"black\",\n transformStyle: \"preserve-3d\",\n overflow: \"hidden\",\n },\n layer: {\n position: \"absolute\",\n width: \"100%\",\n height: \"100%\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"center\",\n backgroundColor: \"transparent\",\n backgroundSize: \"cover\",\n transitionProperty: \"transform, opacity\",\n opacity: 0,\n },\n light: {\n position: \"absolute\",\n width: \"100%\",\n height: \"100%\",\n },\n shadow: {\n position: \"absolute\",\n width: \"100%\",\n height: \"100%\",\n transitionProperty: \"transform, box-shadow\",\n transform: \"translateZ(-10px) scale(0.95)\",\n },\n}\n","/**\n * Apply a set of styles to an HTML element.\n */\nexport const applyStyles = (element: HTMLDivElement, styles: React.CSSProperties) => {\n for (const [style, value] of Object.entries(styles)) {\n if (isSafariDesktop()) {\n element.style[style] = value\n } else {\n requestAnimationFrame(() => {\n element.style[style] = value\n })\n }\n }\n}\n\n/**\n * Clamp the `value` between `min` and `max` inclusively.\n */\nexport const clamp = (value: number, min: number, max: number) => {\n const maximum = max < min ? min : max\n\n return value != null ? Math.min(Math.max(value, min), maximum) : value\n}\n\n/**\n * Return `true` if the value is a `function`.\n */\n// eslint-disable-next-line @typescript-eslint/ban-types\nexport const isFunction = (value: unknown): value is Function => typeof value === \"function\"\n\n/**\n * Detect whether the current browser is a desktop version of Safari.\n */\nexport const isSafariDesktop = () => {\n const { userAgent, vendor } = navigator\n\n return /Safari/i.test(userAgent) && /Apple Computer/.test(vendor) && !/Mobi|Android/i.test(userAgent)\n}\n"],"sourceRoot":""}
--------------------------------------------------------------------------------