├── .codesandbox └── workspace.json ├── .gitignore ├── LICENSE ├── README.md ├── build ├── asset-manifest.json ├── favicon.ico ├── img │ ├── 1.jpg │ ├── 10.jpg │ ├── 11.jpg │ ├── 12.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ ├── 7.jpg │ ├── 8.jpg │ └── 9.jpg ├── index.html └── static │ ├── css │ ├── main.05199dc0.css │ └── main.05199dc0.css.map │ └── js │ ├── main.cdd68f98.js │ ├── main.cdd68f98.js.LICENSE.txt │ └── main.cdd68f98.js.map ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── img │ ├── 1.jpg │ ├── 10.jpg │ ├── 11.jpg │ ├── 12.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ ├── 7.jpg │ ├── 8.jpg │ └── 9.jpg └── index.html └── src ├── App.js ├── data └── images.js ├── index.js ├── style └── styles.css ├── three ├── Carousel.js ├── CarouselItem.js ├── Plane.js └── PostProcessing.js └── utils └── index.js /.codesandbox/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "responsive-preview": { 3 | "Mobile": [ 4 | 320, 5 | 675 6 | ], 7 | "Tablet": [ 8 | 1024, 9 | 765 10 | ], 11 | "Desktop": [ 12 | 1400, 13 | 800 14 | ], 15 | "Desktop HD": [ 16 | 1920, 17 | 1080 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2009 - 2022 [Codrops](https://tympanus.net/codrops) 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebGL Carousel 2 | 3 | A recreation of a carousel similar to the one seen on alcre.co.kr, created by Eum Ray, using WebGL, react-three-fiber, and GSAP. 4 | 5 | ![Image Title](https://tympanus.net/codrops/wp-content/uploads/2023/04/webglcarousel.jpg) 6 | 7 | [Article on Codrops](https://tympanus.net/codrops/?p=71727) 8 | 9 | [Demo](http://tympanus.net/Development/WebGLCarousel/) 10 | 11 | ## Installation 12 | 13 | Install dependencies: 14 | 15 | ``` 16 | npm install 17 | ``` 18 | 19 | Compile the code for development and start a local server: 20 | 21 | ``` 22 | npm start 23 | ``` 24 | 25 | Create the build: 26 | 27 | ``` 28 | npm run build 29 | ``` 30 | 31 | ## Credits 32 | 33 | - Images generated with [Midjourney](https://midjourney.com/) 34 | 35 | ## Misc 36 | 37 | Follow Fabio: [Twitter](https://twitter.com/supahfunk), [Instagram](https://www.instagram.com/supahfunk/), [Codepen](https://codepen.io/supah), [LinkedIn](https://www.linkedin.com/in/fabio-ottaviani-82b0776/) 38 | 39 | Follow Codrops: [Twitter](http://www.twitter.com/codrops), [Facebook](http://www.facebook.com/codrops), [GitHub](https://github.com/codrops), [Instagram](https://www.instagram.com/codropsss/) 40 | 41 | ## License 42 | [MIT](LICENSE) 43 | 44 | Made with :blue_heart: by [Codrops](http://www.codrops.com) 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "./static/css/main.05199dc0.css", 4 | "main.js": "./static/js/main.cdd68f98.js", 5 | "index.html": "./index.html", 6 | "main.05199dc0.css.map": "./static/css/main.05199dc0.css.map", 7 | "main.cdd68f98.js.map": "./static/js/main.cdd68f98.js.map" 8 | }, 9 | "entrypoints": [ 10 | "static/css/main.05199dc0.css", 11 | "static/js/main.cdd68f98.js" 12 | ] 13 | } -------------------------------------------------------------------------------- /build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/build/favicon.ico -------------------------------------------------------------------------------- /build/img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/build/img/1.jpg -------------------------------------------------------------------------------- /build/img/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/build/img/10.jpg -------------------------------------------------------------------------------- /build/img/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/build/img/11.jpg -------------------------------------------------------------------------------- /build/img/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/build/img/12.jpg -------------------------------------------------------------------------------- /build/img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/build/img/2.jpg -------------------------------------------------------------------------------- /build/img/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/build/img/3.jpg -------------------------------------------------------------------------------- /build/img/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/build/img/4.jpg -------------------------------------------------------------------------------- /build/img/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/build/img/5.jpg -------------------------------------------------------------------------------- /build/img/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/build/img/6.jpg -------------------------------------------------------------------------------- /build/img/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/build/img/7.jpg -------------------------------------------------------------------------------- /build/img/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/build/img/8.jpg -------------------------------------------------------------------------------- /build/img/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/build/img/9.jpg -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | WebGL Carousel | Codrops

WebGL Carousel

Read the tutorial Previous demo
-------------------------------------------------------------------------------- /build/static/css/main.05199dc0.css: -------------------------------------------------------------------------------- 1 | *,:after,:before{box-sizing:border-box}:root{--color-text:#343131;--color-bg:#d5d5d5;--color-link:#868282;--color-link-hover:#000;font-size:13px}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-color:#d5d5d5;background-color:var(--color-bg);color:#343131;color:var(--color-text);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;height:100vh;margin:0;overflow:hidden;scroll-behavior:none;-webkit-user-select:none;user-select:none}.js .loading:after,.js .loading:before{content:"";position:fixed;z-index:1000}.js .loading:before{background:#d5d5d5;background:var(--color-bg);height:100%;left:0;top:0;width:100%}.js .loading:after{-webkit-animation:loaderAnim .7s linear infinite alternate forwards;animation:loaderAnim .7s linear infinite alternate forwards;background:#868282;background:var(--color-link);border-radius:50%;height:60px;left:50%;margin:-30px 0 0 -30px;opacity:.4;top:50%;width:60px}@-webkit-keyframes loaderAnim{to{opacity:1;-webkit-transform:scale3d(.5,.5,1);transform:scale3d(.5,.5,1)}}@keyframes loaderAnim{to{opacity:1;-webkit-transform:scale3d(.5,.5,1);transform:scale3d(.5,.5,1)}}a{color:#868282;color:var(--color-link);cursor:pointer;text-decoration:none}a,a:hover{outline:none}a:hover{color:#000;color:var(--color-link-hover)}a:focus{background:#d3d3d3;outline:none}a:focus:not(:focus-visible){background:transparent}a:focus-visible{background:transparent;outline:2px solid red}.unbutton{background:none;border:0;cursor:pointer;font:inherit;margin:0;padding:0}.unbutton:focus{outline:none}.frame{grid-gap:.5rem;align-items:center;align-self:start;display:grid;grid-area:main;grid-template-areas:"title" "back" "prev" "sponsor";grid-template-columns:100%;justify-items:start;padding:1.5rem;pointer-events:none;position:relative;text-transform:uppercase;width:100%;z-index:1000}.frame,body #cdawrap{justify-self:start}.frame a{overflow:hidden;pointer-events:auto;position:relative;white-space:nowrap}.frame a:before{background:currentColor;content:"";height:1px;position:absolute;top:90%;-webkit-transform-origin:0 50%;transform-origin:0 50%;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;width:100%}.frame a:hover:before{-webkit-transform:scaleX(0);transform:scaleX(0);-webkit-transform-origin:100% 50%;transform-origin:100% 50%}.frame__title{font-size:inherit;font-weight:inherit;grid-area:title;margin:0}.frame__back{align-items:flex-end;display:flex;grid-area:back}.frame__prev{grid-area:prev}canvas{height:100vh}@media screen and (min-width:53em){.frame{grid-gap:2rem;align-content:space-between;grid-template-areas:"title back ..." "prev ... sponsor";grid-template-columns:auto auto 1fr;grid-template-rows:auto auto;height:100vh;justify-items:start;padding:.75rem;position:fixed}body #cdawrap{justify-self:end}} 2 | /*# sourceMappingURL=main.05199dc0.css.map*/ -------------------------------------------------------------------------------- /build/static/css/main.05199dc0.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/css/main.05199dc0.css","mappings":"AAAA,iBAGE,qBACF,CAEA,MAEE,oBAAqB,CACrB,kBAAmB,CACnB,oBAAqB,CACrB,uBAAwB,CAJxB,cAKF,CAEA,KAKE,kCAAmC,CACnC,iCAAkC,CAHlC,wBAAiC,CAAjC,gCAAiC,CADjC,aAAwB,CAAxB,uBAAwB,CAExB,gFAAsF,CAKtF,YAAa,CARb,QAAS,CAMT,eAAgB,CAChB,oBAAqB,CAErB,wBAAiB,CAAjB,gBACF,CAGA,uCAEE,UAAW,CACX,cAAe,CACf,YACF,CAEA,oBAKE,kBAA2B,CAA3B,0BAA2B,CAD3B,WAAY,CAFZ,MAAO,CADP,KAAM,CAEN,UAGF,CAEA,mBASE,mEAA6D,CAA7D,2DAA6D,CAD7D,kBAA6B,CAA7B,4BAA6B,CAF7B,iBAAkB,CAFlB,WAAY,CAFZ,QAAS,CAGT,sBAAuB,CAEvB,UAAY,CANZ,OAAQ,CAER,UAQF,CAEA,8BACE,GACE,SAAU,CACV,kCAA6B,CAA7B,0BACF,CACF,CALA,sBACE,GACE,SAAU,CACV,kCAA6B,CAA7B,0BACF,CACF,CAEA,EAEE,aAAwB,CAAxB,uBAAwB,CAExB,cAAe,CAHf,oBAIF,CAEA,UAJE,YAOF,CAHA,QACE,UAA8B,CAA9B,6BAEF,CAGA,QAIE,kBAAqB,CADrB,YAEF,CAEA,4BAGE,sBACF,CAEA,gBAKE,sBAAuB,CADvB,qBAEF,CAEA,UACE,eAAgB,CAChB,QAAS,CAIT,cAAe,CADf,YAAa,CADb,QAAS,CADT,SAIF,CAEA,gBACE,YACF,CAEA,OASE,cAAgB,CAKhB,kBAAmB,CAHnB,gBAAiB,CALjB,YAAa,CALb,cAAe,CAOf,mDAAoD,CADpD,0BAA2B,CAG3B,mBAAoB,CALpB,cAAe,CAQf,mBAAoB,CAVpB,iBAAkB,CAYlB,wBAAyB,CAXzB,UAAW,CAFX,YAcF,CAEA,qBANE,kBAQF,CAEA,SAGE,eAAgB,CAFhB,mBAAoB,CAGpB,iBAAkB,CAFlB,kBAGF,CAEA,gBAIE,uBAAwB,CAHxB,UAAW,CACX,UAAW,CAGX,iBAAkB,CAClB,OAAQ,CAER,8BAAwB,CAAxB,sBAAwB,CADxB,gCAA0B,CAA1B,wBAA0B,CAA1B,8CAA0B,CAJ1B,UAMF,CAEA,sBACE,2BAAoB,CAApB,mBAAoB,CACpB,iCAA0B,CAA1B,yBACF,CAEA,cAEE,iBAAkB,CAElB,mBAAoB,CAHpB,eAAgB,CAEhB,QAEF,CAEA,aAGE,oBAAqB,CADrB,YAAa,CADb,cAGF,CAEA,aACE,cACF,CAEA,OACE,YACF,CAEA,mCACE,OAQE,aAAc,CAFd,2BAA4B,CAD5B,uDAAwD,CAFxD,mCAAoC,CACpC,4BAA6B,CAF7B,YAAa,CAKb,mBAAoB,CAEpB,cAAgB,CARhB,cASF,CACA,cACI,gBACJ,CACF","sources":["style/styles.css"],"sourcesContent":["*,\n*::after,\n*::before {\n box-sizing: border-box;\n}\n\n:root {\n font-size: 13px;\n --color-text: #343131;\n --color-bg: #d5d5d5;\n --color-link: #868282;\n --color-link-hover: #000;\n}\n\nbody {\n margin: 0;\n color: var(--color-text);\n background-color: var(--color-bg);\n font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n overflow: hidden;\n scroll-behavior: none;\n height: 100vh;\n user-select: none;\n}\n\n/* Page Loader */\n.js .loading::before,\n.js .loading::after {\n content: '';\n position: fixed;\n z-index: 1000;\n}\n\n.js .loading::before {\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: var(--color-bg);\n}\n\n.js .loading::after {\n top: 50%;\n left: 50%;\n width: 60px;\n height: 60px;\n margin: -30px 0 0 -30px;\n border-radius: 50%;\n opacity: 0.4;\n background: var(--color-link);\n animation: loaderAnim 0.7s linear infinite alternate forwards;\n\n}\n\n@keyframes loaderAnim {\n to {\n opacity: 1;\n transform: scale3d(0.5,0.5,1);\n }\n}\n\na {\n text-decoration: none;\n color: var(--color-link);\n outline: none;\n cursor: pointer;\n}\n\na:hover {\n color: var(--color-link-hover);\n outline: none;\n}\n\n/* Better focus styles from https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible */\na:focus {\n /* Provide a fallback style for browsers\n that don't support :focus-visible */\n outline: none;\n background: lightgrey;\n}\n\na:focus:not(:focus-visible) {\n /* Remove the focus indicator on mouse-focus for browsers\n that do support :focus-visible */\n background: transparent;\n}\n\na:focus-visible {\n /* Draw a very noticeable focus style for\n keyboard-focus on browsers that do support\n :focus-visible */\n outline: 2px solid red;\n background: transparent;\n}\n\n.unbutton {\n background: none;\n border: 0;\n padding: 0;\n margin: 0;\n font: inherit;\n cursor: pointer;\n}\n\n.unbutton:focus {\n outline: none;\n}\n\n.frame {\n grid-area: main;\n z-index: 1000;\n position: relative;\n width: 100%;\n padding: 1.5rem;\n display: grid;\n grid-template-columns: 100%;\n grid-template-areas: 'title' 'back' 'prev' 'sponsor';\n grid-gap: 0.5rem;\n justify-items: start;\n align-self: start;\n justify-self: start;\n pointer-events: none;\n align-items: center;\n text-transform: uppercase;\n}\n\nbody #cdawrap {\n justify-self: start;\n}\n\n.frame a {\n pointer-events: auto;\n white-space: nowrap;\n overflow: hidden;\n position: relative;\n}\n\n.frame a::before {\n content: '';\n height: 1px;\n width: 100%;\n background: currentColor;\n position: absolute;\n top: 90%;\n transition: transform 0.3s;\n transform-origin: 0% 50%;\n}\n\n.frame a:hover::before {\n transform: scaleX(0);\n transform-origin: 100% 50%;\n}\n\n.frame__title {\n grid-area: title;\n font-size: inherit;\n margin: 0;\n font-weight: inherit;\n}\n\n.frame__back {\n grid-area: back;\n display: flex;\n align-items: flex-end;\n}\n\n.frame__prev {\n grid-area: prev;\n}\n\ncanvas {\n height: 100vh;\n}\n\n@media screen and (min-width: 53em) {\n .frame {\n position: fixed;\n height: 100vh;\n grid-template-columns: auto auto 1fr;\n grid-template-rows: auto auto;\n grid-template-areas: 'title back ...' 'prev ... sponsor';\n align-content: space-between;\n justify-items: start;\n grid-gap: 2rem;\n padding: 0.75rem;\n }\n body #cdawrap {\n justify-self: end;\n }\n}\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /build/static/js/main.cdd68f98.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * GSAP 3.11.5 3 | * https://greensock.com 4 | * 5 | * @license Copyright 2008-2023, GreenSock. All rights reserved. 6 | * Subject to the terms at https://greensock.com/standard-license or for 7 | * Club GreenSock members, the agreement issued with that membership. 8 | * @author: Jack Doyle, jack@greensock.com 9 | */ 10 | 11 | /*! 12 | * assign-symbols 13 | * 14 | * Copyright (c) 2015, Jon Schlinkert. 15 | * Licensed under the MIT License. 16 | */ 17 | 18 | /*! 19 | * for-in 20 | * 21 | * Copyright (c) 2014-2017, Jon Schlinkert. 22 | * Released under the MIT License. 23 | */ 24 | 25 | /*! 26 | * get-value 27 | * 28 | * Copyright (c) 2014-2015, Jon Schlinkert. 29 | * Licensed under the MIT License. 30 | */ 31 | 32 | /*! 33 | * is-extendable 34 | * 35 | * Copyright (c) 2015, Jon Schlinkert. 36 | * Licensed under the MIT License. 37 | */ 38 | 39 | /*! 40 | * is-extendable 41 | * 42 | * Copyright (c) 2015-2017, Jon Schlinkert. 43 | * Released under the MIT License. 44 | */ 45 | 46 | /*! 47 | * is-plain-object 48 | * 49 | * Copyright (c) 2014-2017, Jon Schlinkert. 50 | * Released under the MIT License. 51 | */ 52 | 53 | /*! 54 | * isobject 55 | * 56 | * Copyright (c) 2014-2017, Jon Schlinkert. 57 | * Released under the MIT License. 58 | */ 59 | 60 | /*! 61 | * set-value 62 | * 63 | * Copyright (c) 2014-2015, 2017, Jon Schlinkert. 64 | * Released under the MIT License. 65 | */ 66 | 67 | /*! 68 | * split-string 69 | * 70 | * Copyright (c) 2015-2017, Jon Schlinkert. 71 | * Released under the MIT License. 72 | */ 73 | 74 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 75 | 76 | /** 77 | * @license 78 | * Copyright 2010-2023 Three.js Authors 79 | * SPDX-License-Identifier: MIT 80 | */ 81 | 82 | /** 83 | * @license React 84 | * react-dom.production.min.js 85 | * 86 | * Copyright (c) Facebook, Inc. and its affiliates. 87 | * 88 | * This source code is licensed under the MIT license found in the 89 | * LICENSE file in the root directory of this source tree. 90 | */ 91 | 92 | /** 93 | * @license React 94 | * react-jsx-runtime.production.min.js 95 | * 96 | * Copyright (c) Facebook, Inc. and its affiliates. 97 | * 98 | * This source code is licensed under the MIT license found in the 99 | * LICENSE file in the root directory of this source tree. 100 | */ 101 | 102 | /** 103 | * @license React 104 | * react-reconciler-constants.production.min.js 105 | * 106 | * Copyright (c) Facebook, Inc. and its affiliates. 107 | * 108 | * This source code is licensed under the MIT license found in the 109 | * LICENSE file in the root directory of this source tree. 110 | */ 111 | 112 | /** 113 | * @license React 114 | * react-reconciler.production.min.js 115 | * 116 | * Copyright (c) Facebook, Inc. and its affiliates. 117 | * 118 | * This source code is licensed under the MIT license found in the 119 | * LICENSE file in the root directory of this source tree. 120 | */ 121 | 122 | /** 123 | * @license React 124 | * react.production.min.js 125 | * 126 | * Copyright (c) Facebook, Inc. and its affiliates. 127 | * 128 | * This source code is licensed under the MIT license found in the 129 | * LICENSE file in the root directory of this source tree. 130 | */ 131 | 132 | /** 133 | * @license React 134 | * scheduler.production.min.js 135 | * 136 | * Copyright (c) Facebook, Inc. and its affiliates. 137 | * 138 | * This source code is licensed under the MIT license found in the 139 | * LICENSE file in the root directory of this source tree. 140 | */ 141 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgl-carousel-postprocessing", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "homepage": "./", 7 | "main": "src/index.js", 8 | "dependencies": { 9 | "@react-three/drei": "9.65.3", 10 | "@react-three/fiber": "8.12.1", 11 | "gsap": "3.11.5", 12 | "leva": "0.9.34", 13 | "loader-utils": "3.2.1", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0", 16 | "react-scripts": "5.0.1", 17 | "react-use": "17.4.0", 18 | "three": "0.151.3" 19 | }, 20 | "devDependencies": { 21 | "@babel/runtime": "7.13.8", 22 | "typescript": "4.1.3" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test --env=jsdom", 28 | "eject": "react-scripts eject" 29 | }, 30 | "browserslist": [ 31 | ">0.2%", 32 | "not dead", 33 | "not ie <= 11", 34 | "not op_mini all" 35 | ] 36 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/public/favicon.ico -------------------------------------------------------------------------------- /public/img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/public/img/1.jpg -------------------------------------------------------------------------------- /public/img/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/public/img/10.jpg -------------------------------------------------------------------------------- /public/img/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/public/img/11.jpg -------------------------------------------------------------------------------- /public/img/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/public/img/12.jpg -------------------------------------------------------------------------------- /public/img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/public/img/2.jpg -------------------------------------------------------------------------------- /public/img/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/public/img/3.jpg -------------------------------------------------------------------------------- /public/img/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/public/img/4.jpg -------------------------------------------------------------------------------- /public/img/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/public/img/5.jpg -------------------------------------------------------------------------------- /public/img/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/public/img/6.jpg -------------------------------------------------------------------------------- /public/img/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/public/img/7.jpg -------------------------------------------------------------------------------- /public/img/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/public/img/8.jpg -------------------------------------------------------------------------------- /public/img/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supahfunk/webgl-carousel/d6f72f2c194dda9d1777bccfe52928825393652f/public/img/9.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebGL Carousel | Codrops 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

WebGL Carousel

18 | Read the tutorial 19 | Previous demo 20 |
21 | 24 |
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | import { Canvas } from '@react-three/fiber' 3 | import Carousel from './three/Carousel' 4 | import './style/styles.css' 5 | 6 | export default function App() { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/data/images.js: -------------------------------------------------------------------------------- 1 | const images = [ 2 | { 3 | image: 4 | 'img/1.jpg' 5 | }, 6 | { 7 | image: 8 | 'img/2.jpg' 9 | }, 10 | { 11 | image: 12 | 'img/3.jpg' 13 | }, 14 | { 15 | image: 16 | 'img/4.jpg' 17 | }, 18 | { 19 | image: 20 | 'img/5.jpg' 21 | }, 22 | { 23 | image: 24 | 'img/6.jpg' 25 | }, 26 | { 27 | image: 28 | 'img/7.jpg' 29 | }, 30 | { 31 | image: 32 | 'img/8.jpg' 33 | }, 34 | { 35 | image: 36 | 'img/9.jpg' 37 | }, 38 | { 39 | image: 40 | 'img/10.jpg' 41 | }, 42 | { 43 | image: 44 | 'img/11.jpg' 45 | }, 46 | { 47 | image: 48 | 'img/12.jpg' 49 | } 50 | ] 51 | 52 | export default images 53 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | 4 | import App from "./App"; 5 | 6 | const rootElement = document.getElementById("root"); 7 | const root = createRoot(rootElement); 8 | 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /src/style/styles.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | font-size: 13px; 9 | --color-text: #343131; 10 | --color-bg: #d5d5d5; 11 | --color-link: #868282; 12 | --color-link-hover: #000; 13 | } 14 | 15 | body { 16 | margin: 0; 17 | color: var(--color-text); 18 | background-color: var(--color-bg); 19 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif; 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | overflow: hidden; 23 | scroll-behavior: none; 24 | height: 100vh; 25 | user-select: none; 26 | } 27 | 28 | /* Page Loader */ 29 | .js .loading::before, 30 | .js .loading::after { 31 | content: ''; 32 | position: fixed; 33 | z-index: 1000; 34 | } 35 | 36 | .js .loading::before { 37 | top: 0; 38 | left: 0; 39 | width: 100%; 40 | height: 100%; 41 | background: var(--color-bg); 42 | } 43 | 44 | .js .loading::after { 45 | top: 50%; 46 | left: 50%; 47 | width: 60px; 48 | height: 60px; 49 | margin: -30px 0 0 -30px; 50 | border-radius: 50%; 51 | opacity: 0.4; 52 | background: var(--color-link); 53 | animation: loaderAnim 0.7s linear infinite alternate forwards; 54 | 55 | } 56 | 57 | @keyframes loaderAnim { 58 | to { 59 | opacity: 1; 60 | transform: scale3d(0.5,0.5,1); 61 | } 62 | } 63 | 64 | a { 65 | text-decoration: none; 66 | color: var(--color-link); 67 | outline: none; 68 | cursor: pointer; 69 | } 70 | 71 | a:hover { 72 | color: var(--color-link-hover); 73 | outline: none; 74 | } 75 | 76 | /* Better focus styles from https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible */ 77 | a:focus { 78 | /* Provide a fallback style for browsers 79 | that don't support :focus-visible */ 80 | outline: none; 81 | background: lightgrey; 82 | } 83 | 84 | a:focus:not(:focus-visible) { 85 | /* Remove the focus indicator on mouse-focus for browsers 86 | that do support :focus-visible */ 87 | background: transparent; 88 | } 89 | 90 | a:focus-visible { 91 | /* Draw a very noticeable focus style for 92 | keyboard-focus on browsers that do support 93 | :focus-visible */ 94 | outline: 2px solid red; 95 | background: transparent; 96 | } 97 | 98 | .unbutton { 99 | background: none; 100 | border: 0; 101 | padding: 0; 102 | margin: 0; 103 | font: inherit; 104 | cursor: pointer; 105 | } 106 | 107 | .unbutton:focus { 108 | outline: none; 109 | } 110 | 111 | .frame { 112 | grid-area: main; 113 | z-index: 1000; 114 | position: relative; 115 | width: 100%; 116 | padding: 1.5rem; 117 | display: grid; 118 | grid-template-columns: 100%; 119 | grid-template-areas: 'title' 'back' 'prev' 'sponsor'; 120 | grid-gap: 0.5rem; 121 | justify-items: start; 122 | align-self: start; 123 | justify-self: start; 124 | pointer-events: none; 125 | align-items: center; 126 | text-transform: uppercase; 127 | } 128 | 129 | body #cdawrap { 130 | justify-self: start; 131 | } 132 | 133 | .frame a { 134 | pointer-events: auto; 135 | white-space: nowrap; 136 | overflow: hidden; 137 | position: relative; 138 | } 139 | 140 | .frame a::before { 141 | content: ''; 142 | height: 1px; 143 | width: 100%; 144 | background: currentColor; 145 | position: absolute; 146 | top: 90%; 147 | transition: transform 0.3s; 148 | transform-origin: 0% 50%; 149 | } 150 | 151 | .frame a:hover::before { 152 | transform: scaleX(0); 153 | transform-origin: 100% 50%; 154 | } 155 | 156 | .frame__title { 157 | grid-area: title; 158 | font-size: inherit; 159 | margin: 0; 160 | font-weight: inherit; 161 | } 162 | 163 | .frame__back { 164 | grid-area: back; 165 | display: flex; 166 | align-items: flex-end; 167 | } 168 | 169 | .frame__prev { 170 | grid-area: prev; 171 | } 172 | 173 | canvas { 174 | height: 100vh; 175 | } 176 | 177 | @media screen and (min-width: 53em) { 178 | .frame { 179 | position: fixed; 180 | height: 100vh; 181 | grid-template-columns: auto auto 1fr; 182 | grid-template-rows: auto auto; 183 | grid-template-areas: 'title back ...' 'prev ... sponsor'; 184 | align-content: space-between; 185 | justify-items: start; 186 | grid-gap: 2rem; 187 | padding: 0.75rem; 188 | } 189 | body #cdawrap { 190 | justify-self: end; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/three/Carousel.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState, useMemo } from 'react' 2 | import { useFrame, useThree } from '@react-three/fiber' 3 | import { usePrevious } from 'react-use' 4 | import gsap from 'gsap' 5 | import PostProcessing from './PostProcessing' 6 | import CarouselItem from './CarouselItem' 7 | import { lerp, getPiramidalIndex } from '../utils' 8 | import images from '../data/images' 9 | 10 | /*------------------------------ 11 | Plane Settings 12 | ------------------------------*/ 13 | const planeSettings = { 14 | width: 1, 15 | height: 2.5, 16 | gap: 0.1 17 | } 18 | 19 | /*------------------------------ 20 | Gsap Defaults 21 | ------------------------------*/ 22 | gsap.defaults({ 23 | duration: 2.5, 24 | ease: 'power3.out' 25 | }) 26 | 27 | /*------------------------------ 28 | Carousel 29 | ------------------------------*/ 30 | const Carousel = () => { 31 | const [$root, setRoot] = useState() 32 | const $post = useRef() 33 | 34 | const [activePlane, setActivePlane] = useState(null) 35 | const prevActivePlane = usePrevious(activePlane) 36 | const { viewport } = useThree() 37 | 38 | /*-------------------- 39 | Vars 40 | --------------------*/ 41 | const progress = useRef(0) 42 | const startX = useRef(0) 43 | const isDown = useRef(false) 44 | const speedWheel = 0.02 45 | const speedDrag = -0.3 46 | const oldProgress = useRef(0) 47 | const speed = useRef(0) 48 | const $items = useMemo(() => { 49 | if ($root) return $root.children 50 | }, [$root]) 51 | 52 | /*-------------------- 53 | Diaplay Items 54 | --------------------*/ 55 | const displayItems = (item, index, active) => { 56 | const piramidalIndex = getPiramidalIndex($items, active)[index] 57 | gsap.to(item.position, { 58 | x: (index - active) * (planeSettings.width + planeSettings.gap), 59 | y: $items.length * -0.1 + piramidalIndex * 0.1 60 | }) 61 | } 62 | 63 | /*-------------------- 64 | RAF 65 | --------------------*/ 66 | useFrame(() => { 67 | progress.current = Math.max(0, Math.min(progress.current, 100)) 68 | 69 | const active = Math.floor((progress.current / 100) * ($items.length - 1)) 70 | $items.forEach((item, index) => displayItems(item, index, active)) 71 | speed.current = lerp( 72 | speed.current, 73 | Math.abs(oldProgress.current - progress.current), 74 | 0.1 75 | ) 76 | 77 | oldProgress.current = lerp(oldProgress.current, progress.current, 0.1) 78 | 79 | if ($post.current) { 80 | $post.current.thickness = speed.current 81 | } 82 | }) 83 | 84 | /*-------------------- 85 | Handle Wheel 86 | --------------------*/ 87 | const handleWheel = (e) => { 88 | if (activePlane !== null) return 89 | const isVerticalScroll = Math.abs(e.deltaY) > Math.abs(e.deltaX) 90 | const wheelProgress = isVerticalScroll ? e.deltaY : e.deltaX 91 | progress.current = progress.current + wheelProgress * speedWheel 92 | } 93 | 94 | /*-------------------- 95 | Handle Down 96 | --------------------*/ 97 | const handleDown = (e) => { 98 | if (activePlane !== null) return 99 | isDown.current = true 100 | startX.current = e.clientX || (e.touches && e.touches[0].clientX) || 0 101 | } 102 | 103 | /*-------------------- 104 | Handle Up 105 | --------------------*/ 106 | const handleUp = () => { 107 | isDown.current = false 108 | } 109 | 110 | /*-------------------- 111 | Handle Move 112 | --------------------*/ 113 | const handleMove = (e) => { 114 | if (activePlane !== null || !isDown.current) return 115 | const x = e.clientX || (e.touches && e.touches[0].clientX) || 0 116 | const mouseProgress = (x - startX.current) * speedDrag 117 | progress.current = progress.current + mouseProgress 118 | startX.current = x 119 | } 120 | 121 | /*-------------------- 122 | Click 123 | --------------------*/ 124 | useEffect(() => { 125 | if (!$items) return 126 | if (activePlane !== null && prevActivePlane === null) { 127 | progress.current = (activePlane / ($items.length - 1)) * 100 // Calculate the progress.current based on activePlane 128 | } 129 | }, [activePlane, $items]) 130 | 131 | /*-------------------- 132 | Render Plane Events 133 | --------------------*/ 134 | const renderPlaneEvents = () => { 135 | return ( 136 | 145 | 146 | 147 | 148 | ) 149 | } 150 | 151 | /*-------------------- 152 | Render Slider 153 | --------------------*/ 154 | const renderSlider = () => { 155 | return ( 156 | 157 | {images.map((item, i) => ( 158 | 167 | ))} 168 | 169 | ) 170 | } 171 | 172 | return ( 173 | 174 | {renderPlaneEvents()} 175 | {renderSlider()} 176 | 177 | 178 | ) 179 | } 180 | 181 | export default Carousel 182 | -------------------------------------------------------------------------------- /src/three/CarouselItem.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react' 2 | import { useThree } from '@react-three/fiber' 3 | import gsap from 'gsap' 4 | import Plane from './Plane' 5 | 6 | const CarouselItem = ({ 7 | index, 8 | width, 9 | height, 10 | setActivePlane, 11 | activePlane, 12 | item 13 | }) => { 14 | const $root = useRef() 15 | const [hover, setHover] = useState(false) 16 | const [isActive, setIsActive] = useState(false) 17 | const [isCloseActive, setCloseActive] = useState(false) 18 | const { viewport } = useThree() 19 | const timeoutID = useRef() 20 | 21 | useEffect(() => { 22 | if (activePlane === index) { 23 | setIsActive(activePlane === index) 24 | setCloseActive(true) 25 | } else { 26 | setIsActive(null) 27 | } 28 | }, [activePlane]) 29 | 30 | useEffect(() => { 31 | gsap.killTweensOf($root.current.position) 32 | gsap.to($root.current.position, { 33 | z: isActive ? 0 : -0.01, 34 | duration: 0.2, 35 | ease: 'power3.out', 36 | delay: isActive ? 0 : 2 37 | }) 38 | }, [isActive]) 39 | 40 | /*------------------------------ 41 | Hover effect 42 | ------------------------------*/ 43 | useEffect(() => { 44 | const hoverScale = hover && !isActive ? 1.1 : 1 45 | gsap.to($root.current.scale, { 46 | x: hoverScale, 47 | y: hoverScale, 48 | duration: 0.5, 49 | ease: 'power3.out' 50 | }) 51 | }, [hover, isActive]) 52 | 53 | const handleClose = (e) => { 54 | e.stopPropagation() 55 | if (!isActive) return 56 | setActivePlane(null) 57 | setHover(false) 58 | clearTimeout(timeoutID.current) 59 | timeoutID.current = setTimeout(() => { 60 | setCloseActive(false) 61 | }, 1500) // The duration of this timer depends on the duration of the plane's closing animation. 62 | } 63 | 64 | return ( 65 | { 68 | setActivePlane(index) 69 | }} 70 | onPointerEnter={() => setHover(true)} 71 | onPointerLeave={() => setHover(false)} 72 | > 73 | 79 | 80 | {isCloseActive ? ( 81 | 82 | 83 | 84 | 85 | ) : null} 86 | 87 | ) 88 | } 89 | 90 | export default CarouselItem 91 | -------------------------------------------------------------------------------- /src/three/Plane.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef } from 'react' 2 | import { useThree } from '@react-three/fiber' 3 | import { useTexture } from '@react-three/drei' 4 | import gsap from 'gsap' 5 | 6 | const Plane = ({ texture, width, height, active, ...props }) => { 7 | const $mesh = useRef() 8 | const { viewport } = useThree() 9 | const tex = useTexture(texture) 10 | 11 | useEffect(() => { 12 | if ($mesh.current.material) { 13 | // Setting the 'uZoomScale' uniform in the 'Plane' component to resize the texture proportionally to the dimensions of the viewport. 14 | $mesh.current.material.uniforms.uZoomScale.value.x = 15 | viewport.width / width 16 | $mesh.current.material.uniforms.uZoomScale.value.y = 17 | viewport.height / height 18 | 19 | gsap.to($mesh.current.material.uniforms.uProgress, { 20 | value: active ? 1 : 0 21 | }) 22 | 23 | gsap.to($mesh.current.material.uniforms.uRes.value, { 24 | x: active ? viewport.width : width, 25 | y: active ? viewport.height : height 26 | }) 27 | } 28 | }, [viewport, active]) 29 | 30 | const shaderArgs = useMemo( 31 | () => ({ 32 | uniforms: { 33 | uProgress: { value: 0 }, 34 | uZoomScale: { value: { x: 1, y: 1 } }, 35 | uTex: { value: tex }, 36 | uRes: { value: { x: 1, y: 1 } }, 37 | uImageRes: { 38 | value: { x: tex.source.data.width, y: tex.source.data.height } 39 | } 40 | }, 41 | vertexShader: /* glsl */ ` 42 | varying vec2 vUv; 43 | uniform float uProgress; 44 | uniform vec2 uZoomScale; 45 | 46 | void main() { 47 | vUv = uv; 48 | vec3 pos = position; 49 | float angle = uProgress * 3.14159265 / 2.; 50 | float wave = cos(angle); 51 | float c = sin(length(uv - .5) * 15. + uProgress * 12.) * .5 + .5; 52 | pos.x *= mix(1., uZoomScale.x + wave * c, uProgress); 53 | pos.y *= mix(1., uZoomScale.y + wave * c, uProgress); 54 | 55 | gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 ); 56 | } 57 | `, 58 | fragmentShader: /* glsl */ ` 59 | uniform sampler2D uTex; 60 | uniform vec2 uRes; 61 | uniform vec2 uZoomScale; 62 | uniform vec2 uImageRes; 63 | 64 | /*------------------------------ 65 | Background Cover UV 66 | -------------------------------- 67 | u = basic UV 68 | s = screensize 69 | i = image size 70 | ------------------------------*/ 71 | vec2 CoverUV(vec2 u, vec2 s, vec2 i) { 72 | float rs = s.x / s.y; // Aspect screen size 73 | float ri = i.x / i.y; // Aspect image size 74 | vec2 st = rs < ri ? vec2(i.x * s.y / i.y, s.y) : vec2(s.x, i.y * s.x / i.x); // New st 75 | vec2 o = (rs < ri ? vec2((st.x - s.x) / 2.0, 0.0) : vec2(0.0, (st.y - s.y) / 2.0)) / st; // Offset 76 | return u * s / st + o; 77 | } 78 | 79 | varying vec2 vUv; 80 | void main() { 81 | vec2 uv = CoverUV(vUv, uRes, uImageRes); 82 | vec3 tex = texture2D(uTex, uv).rgb; 83 | gl_FragColor = vec4( tex, 1.0 ); 84 | } 85 | ` 86 | }), 87 | [tex] 88 | ) 89 | 90 | return ( 91 | 92 | 93 | 94 | 95 | ) 96 | } 97 | 98 | export default Plane 99 | -------------------------------------------------------------------------------- /src/three/PostProcessing.js: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react' 2 | import { useThree } from '@react-three/fiber' 3 | import { MeshTransmissionMaterial } from '@react-three/drei' 4 | import { Color } from 'three' 5 | import { useControls } from 'leva' 6 | 7 | const PostProcessing = forwardRef((_, ref) => { 8 | const { viewport } = useThree() 9 | 10 | const { active, ior } = useControls({ 11 | active: { 12 | value: true 13 | }, 14 | ior: { 15 | value: 0.9, 16 | min: 0.8, 17 | max: 1.2 18 | } 19 | }) 20 | 21 | return active ? ( 22 | 23 | 24 | 34 | 35 | ) : null 36 | }) 37 | 38 | export default PostProcessing 39 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /*------------------------------ 2 | Lerp 3 | ------------------------------*/ 4 | export const lerp = (v0, v1, t) => v0 * (1 - t) + v1 * t 5 | 6 | /*-------------------- 7 | Get Piramidal Index 8 | --------------------*/ 9 | // Returns an array of decreasing index values in a pyramid shape, starting from the specified index with the highest value. These indices are often used to create overlapping effects among elements. 10 | export const getPiramidalIndex = (array, index) => 11 | array.map((_, i) => 12 | index === i ? array.length : array.length - Math.abs(index - i) 13 | ) 14 | --------------------------------------------------------------------------------