{"id":3415,"date":"2021-02-25T04:41:12","date_gmt":"2021-02-25T04:41:12","guid":{"rendered":"https:\/\/rengga.dev\/blog\/?p=3415"},"modified":"2023-02-28T05:23:44","modified_gmt":"2023-02-28T05:23:44","slug":"js-tutorial-webgl-apple-cards","status":"publish","type":"post","link":"https:\/\/rengga.dev\/blog\/js-tutorial-webgl-apple-cards\/","title":{"rendered":"JS Tutorial &#8211; WebGL Apple Cards"},"content":{"rendered":"<p><span style=\"color: #ef3207;\"><a style=\"color: #ef3207;\" href=\"https:\/\/rengga.dev\/\" target=\"_blank\" rel=\"noopener\"><strong>Rengga Dev<\/strong><\/a> <\/span>&#8211; Try playing with the parameters on the gui to the right. The Apple Cards is just one effect of WebGL.<\/p>\n<p><iframe style=\"width: 100%;\" title=\"Untitled\" src=\"https:\/\/codepen.io\/renggagumilar\/embed\/zYjxGBj?default-tab=result&amp;theme-id=dark\" height=\"500\" frameborder=\"no\" scrolling=\"no\" allowfullscreen=\"allowfullscreen\"><br \/>\nSee the Pen <a href=\"https:\/\/codepen.io\/renggagumilar\/pen\/zYjxGBj\"><br \/>\nUntitled<\/a> by Rengga Gumilar (<a href=\"https:\/\/codepen.io\/renggagumilar\">@renggagumilar<\/a>)<br \/>\non <a href=\"https:\/\/codepen.io\">CodePen<\/a>.<br \/>\n<\/iframe><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">import * as THREE from 'https:\/\/cdn.skypack.dev\/three@v0.122.0';\r\n\r\n\/\/ Helper functions\r\nconst rgb = function(r, g, b) {\r\n    return new THREE.Vector3(r, g, b);\r\n}\r\nconst loader = function(path, texture) {\r\n    return new Promise((resolve, reject) =&gt; {\r\n        let loader = new THREE.FileLoader();\r\n        if(typeof texture !== \"undefined\") {\r\n            loader = new THREE.TextureLoader();\r\n        }\r\n        loader.load(path, (item) =&gt; resolve(item));\r\n    })\r\n}\r\nconst randomInteger = function(min, max) {\r\n    return Math.floor(Math.random() * (max - min + 1)) + min;\r\n}\r\n\/\/ -- End Helper Functions\r\n\r\nconst config = {\r\n    individualItem: '.album-item', \/\/ class of individual item\r\n    carouselWidth: 1000, \/\/ in px\r\n    carouselId: '#album-rotator', \/\/ carousel selector\r\n    carouselHolderId: '#album-rotator-holder', \/\/ carousel should be &lt;div id=\"carouselId\"&gt;&lt;div id=\"carouselHolderId\"&gt;{items}&lt;\/div&gt;&lt;\/div&gt;\r\n    colors: [\r\n        \/\/ Define colors for each item. If more items than colors, then first color will be used as default\r\n        \/\/ Format { low: rgb(), high: rgb() for each color }\r\n        { low: rgb(0, 114, 255), high: rgb(48, 0, 255) },\r\n        { low: rgb(236, 166, 15), high: rgb(233, 104, 0) },\r\n        { low: rgb(43, 75, 235), high: rgb(213, 51, 248) },\r\n        { low: rgb(175, 49, 49), high: rgb(123, 16, 16) }\r\n    ]\r\n}\r\n\r\n\/\/ Async function for generating webGL waves\r\nconst createWave = async function(selector, colors) {      \r\n    if(document.querySelectorAll(selector) !== null &amp;&amp; document.querySelectorAll(selector).length &gt; 0) {\r\n        \/\/ Import all the fragment and vertex shaders\r\n        const noise = document.getElementById('noise').textContent;\r\n        const fragment = document.getElementById('fragment').textContent\r\n        const vertex =document.getElementById('vertex').textContent\r\n        let i = 0;\r\n        \/\/ For each of the selector elements\r\n        document.querySelectorAll(selector).forEach(function(item) {\r\n            \/\/ Create a renderer\r\n            \r\n            const newCanvas = document.createElement('canvas');\r\n            newCanvas.id = `canvas-${i}`;\r\n            item.appendChild(newCanvas);\r\n            \r\n            const renderer = new THREE.WebGLRenderer({\r\n                powerPreference: \"high-performance\",\r\n                antialias: true, \r\n                alpha: true,\r\n                canvas: document.getElementById(`canvas-${i}`)\r\n            });\r\n\r\n\r\n            \/\/ Get el width and height\r\n            const elWidth = parseFloat(window.getComputedStyle(item).width);\r\n            const elHeight = parseFloat(window.getComputedStyle(item).height);\r\n\r\n            \/\/ Set sizes and set scene\/camera\r\n            renderer.setSize( elWidth, elHeight );\r\n            renderer.setPixelRatio( window.devicePixelRatio );\r\n\r\n            const scene = new THREE.Scene();\r\n            const camera = new THREE.PerspectiveCamera( 75, elWidth \/ elHeight, 0.1, 1000 );\r\n\r\n            \/\/ Check on colors to use\r\n            let high = colors[0].high; \r\n            let low = colors[0].low;\r\n            if(typeof colors[i] !== \"undefined\") {\r\n                high = colors[i].high;\r\n                low = colors[i].low;\r\n                ++i;\r\n            }\r\n\r\n            \/\/ And use the high color for the subtext.\r\n            if(item.querySelector('.subtext') !== null) {\r\n                item.querySelector('.subtext').style.background = `rgba(${high.x},${high.y},${high.z},0.75)`;\r\n            }\r\n\r\n            \/\/ Create a plane, and pass that through to our shaders\r\n            let geometry = new THREE.PlaneGeometry(600, 600, 100, 100);\r\n            let material = new THREE.ShaderMaterial({\r\n                uniforms: {\r\n                    u_lowColor: {type: 'v3', value: low },\r\n                    u_highColor: {type: 'v3', value: high },\r\n                    u_time: {type: 'f', value: 0},\r\n                    u_height: {type: 'f', value: 1},\r\n                    u_rand: {type: 'f', value: new THREE.Vector2(randomInteger(6, 10), randomInteger(8, 10)) }\r\n                },\r\n                fragmentShader: noise + fragment,\r\n                vertexShader: noise + vertex,\r\n            });\r\n\r\n            \/\/ Create the mesh and position appropriately\r\n            let mesh = new THREE.Mesh(geometry, material);\r\n            mesh.position.set(0, 0, -300);\r\n            mesh.material.needsUpdate = true;\r\n            scene.add(mesh);\r\n            \r\n            \/\/ On hover effects for each item\r\n            let enterTimer, exitTimer;\r\n            item.addEventListener('mouseenter', function(e) {\r\n                if(typeof exitTimer !== \"undefined\") {\r\n                    clearTimeout(exitTimer);\r\n                }\r\n                enterTimer = setInterval(function() {\r\n                    if(mesh.material.uniforms.u_height.value &gt;= 0.5) {\r\n                        mesh.material.uniforms.u_height.value -= 0.05;\r\n                    } else {\r\n                        clearTimeout(enterTimer);\r\n                    }\r\n                }, 10);\r\n            });\r\n            item.addEventListener('mouseleave', function(e) {\r\n                if(typeof enterTimer !== \"undefined\") {\r\n                    clearTimeout(enterTimer);\r\n                }\r\n                exitTimer = setInterval(function() {\r\n                    if(mesh.material.uniforms.u_height.value &lt; 1) {\r\n                        mesh.material.uniforms.u_height.value += 0.05;\r\n                    } else {\r\n                        clearTimeout(exitTimer);\r\n                    }\r\n                }, 10);\r\n            });\r\n\r\n            \/\/ Render\r\n            renderer.render( scene, camera );\r\n            let t = 0;\r\n\r\n            \/\/ Animate\r\n            const animate = function () {\r\n              requestAnimationFrame( animate );\r\n                renderer.render( scene, camera );\r\n                mesh.material.uniforms.u_time.value = t;\r\n                t = t + 0.02;\r\n            };\r\n            animate();\r\n        });\r\n    }\r\n}\r\n\r\ndocument.addEventListener(\"DOMContentLoaded\", function(e) {\r\n    createWave(config.individualItem, config.colors);\r\n\r\n    \/\/ Get items\r\n    const el = document.querySelector(config.individualItem);\r\n    const elWidth = parseFloat(window.getComputedStyle(el).width) + parseFloat(window.getComputedStyle(el).marginLeft) + parseFloat(window.getComputedStyle(el).marginRight);\r\n    \r\n    \/\/ Track carousel\r\n    let mousedown = false;\r\n    let movement = false;\r\n    let initialPosition = 0;\r\n    let selectedItem;\r\n    let currentDelta = 0;\r\n\r\n    document.querySelectorAll(config.carouselId).forEach(function(item) { \r\n        item.style.width = `${config.carouselWidth}px`;\r\n    });\r\n    \r\n    document.querySelectorAll(config.carouselId).forEach(function(item) {\r\n        item.addEventListener('pointerdown', function(e) {\r\n            mousedown = true;\r\n            selectedItem = item;\r\n            initialPosition = e.pageX;\r\n            currentDelta = parseFloat(item.querySelector(config.carouselHolderId).style.transform.split('translateX(')[1]) || 0;\r\n        }); \r\n    });\r\n    \r\n    const scrollCarousel = function(change, currentDelta, selectedItem) {\r\n        let numberThatFit = Math.floor(config.carouselWidth \/ elWidth);\r\n        let newDelta = currentDelta + change;\r\n        let elLength = selectedItem.querySelectorAll(config.individualItem).length - numberThatFit;\r\n        if(newDelta &lt;= 0 &amp;&amp; newDelta &gt;= -elWidth * elLength) {\r\n            selectedItem.querySelector(config.carouselHolderId).style.transform = `translateX(${newDelta}px)`;\r\n        } else {\r\n            if(newDelta &lt;= -elWidth * elLength) {\r\n                selectedItem.querySelector(config.carouselHolderId).style.transform = `translateX(${-elWidth * elLength}px)`;\r\n            } else if(newDelta &gt;= 0) {\r\n                selectedItem.querySelector(config.carouselHolderId).style.transform = `translateX(0px)`;\r\n            }\r\n        }\r\n    }\r\n\r\n    document.body.addEventListener('pointermove', function(e) {\r\n        if(mousedown == true &amp;&amp; typeof selectedItem !== \"undefined\") {\r\n            let change = -(initialPosition - e.pageX);\r\n            scrollCarousel(change, currentDelta, document.body);\r\n            document.querySelectorAll(`${config.carouselId} a`).forEach(function(item) {\r\n                item.style.pointerEvents = 'none';\r\n            });\r\n            movement = true;\r\n        }\r\n    });\r\n    \r\n    ['pointerup', 'mouseleave'].forEach(function(item) {\r\n        document.body.addEventListener(item, function(e) {\r\n            selectedItem = undefined;\r\n            movement = false;\r\n            document.querySelectorAll(`${config.carouselId} a`).forEach(function(item) {\r\n                item.style.pointerEvents = 'all';\r\n            });\r\n        });\r\n    });\r\n\r\n    document.querySelectorAll(config.carouselId).forEach(function(item) {\r\n        let trigger = 0;\r\n        item.addEventListener('wheel', function(e) {\r\n            if(trigger !== 1) {\r\n                ++trigger;\r\n            } else {\r\n                let change = e.deltaX * -3;\r\n                let currentDelta = parseFloat(item.querySelector(config.carouselHolderId).style.transform.split('translateX(')[1]) || 0;\r\n                scrollCarousel(change, currentDelta, item);\r\n                trigger = 0;\r\n            }\r\n            e.preventDefault();\r\n            e.stopImmediatePropagation();\r\n            return false;\r\n        });\r\n    });\r\n});<\/pre>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Rengga Dev &#8211; Try playing with the parameters on the gui to <a class=\"read-more\" href=\"https:\/\/rengga.dev\/blog\/js-tutorial-webgl-apple-cards\/\" title=\"JS Tutorial &#8211; WebGL Apple Cards\" itemprop=\"url\"><\/a><\/p>\n","protected":false},"author":1,"featured_media":3830,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[20],"tags":[334,325,264,263,103,227,265],"newstopic":[],"class_list":{"0":"post-3415","1":"post","2":"type-post","3":"status-publish","4":"format-standard","5":"has-post-thumbnail","7":"category-javascript","8":"tag-apple-cards","9":"tag-background-effects","10":"tag-javascript-tutorial","11":"tag-js-tutorial","12":"tag-web-design","13":"tag-web-designer","14":"tag-webgl"},"_links":{"self":[{"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/posts\/3415","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/comments?post=3415"}],"version-history":[{"count":1,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/posts\/3415\/revisions"}],"predecessor-version":[{"id":3417,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/posts\/3415\/revisions\/3417"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/media\/3830"}],"wp:attachment":[{"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/media?parent=3415"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/categories?post=3415"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/tags?post=3415"},{"taxonomy":"newstopic","embeddable":true,"href":"https:\/\/rengga.dev\/blog\/wp-json\/wp\/v2\/newstopic?post=3415"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}