├── .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":""} --------------------------------------------------------------------------------