├── .DS_Store ├── .gitattributes ├── admin ├── base.gif ├── base.png └── .DS_Store ├── README.md ├── index.html ├── css └── base.css └── js └── index.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/cursor-filled-circle-filter-effect-gsap/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md linguist-language=scss 2 | *.html linguist-language=js 3 | *.css linguist-language=js -------------------------------------------------------------------------------- /admin/base.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/cursor-filled-circle-filter-effect-gsap/HEAD/admin/base.gif -------------------------------------------------------------------------------- /admin/base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/cursor-filled-circle-filter-effect-gsap/HEAD/admin/base.png -------------------------------------------------------------------------------- /admin/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/cursor-filled-circle-filter-effect-gsap/HEAD/admin/.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Cursor Filled Circle with Filter Effect Gsap | Crimson 4 | 5 | 6 | 7 | ### by SUBUX 8 | 9 |
-------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Cursor Filled Circle with Filter Effect Gsap | Crimson 7 | 8 | 9 | 10 | 11 | 12 |
13 | 19 |
20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /css/base.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | font-size: 15px; 9 | } 10 | 11 | body { 12 | margin: 0; 13 | --color-text: #a5a5a5; 14 | --color-text-alt: #747474; 15 | --color-bg: #f3ede8; 16 | --color-link: #000000; 17 | --color-link-hover: #d6106c; 18 | color: var(--color-text); 19 | background: var(--color-bg); 20 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif; 21 | font-weight: 500; 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | --cursor-fill: #d6106c; 25 | } 26 | 27 | a { 28 | text-decoration: none; 29 | color: var(--color-link); 30 | outline: none; 31 | } 32 | 33 | a:hover { 34 | color: var(--color-link-hover); 35 | outline: none; 36 | } 37 | 38 | /* Better focus styles from https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible */ 39 | a:focus { 40 | /* Provide a fallback style for browsers 41 | that don't support :focus-visible */ 42 | outline: none; 43 | background: lightgrey; 44 | } 45 | 46 | a:focus:not(:focus-visible) { 47 | /* Remove the focus indicator on mouse-focus for browsers 48 | that do support :focus-visible */ 49 | background: transparent; 50 | } 51 | 52 | a:focus-visible { 53 | /* Draw a very noticeable focus style for 54 | keyboard-focus on browsers that do support 55 | :focus-visible */ 56 | outline: 2px solid red; 57 | background: transparent; 58 | } 59 | 60 | main { 61 | height: 100vh; 62 | display: flex; 63 | flex-direction: column; 64 | } 65 | 66 | .frame { 67 | padding: 1.5rem 2rem 10vh; 68 | text-align: center; 69 | position: relative; 70 | } 71 | 72 | .frame__title { 73 | margin: 0; 74 | font-size: 1rem; 75 | font-weight: 500; 76 | } 77 | 78 | .frame__links { 79 | margin: 0.5rem 0 2rem; 80 | } 81 | 82 | .frame__links a:not(:last-child) { 83 | margin-right: 1rem; 84 | } 85 | 86 | .menu { 87 | flex: 1; 88 | display: grid; 89 | place-items: center; 90 | align-self: center; 91 | grid-gap: 7vw; 92 | font-size: 2rem; 93 | font-family: "Goudy Old Style", Garamond, "Big Caslon", "Times New Roman", serif; 94 | 95 | } 96 | 97 | .cursor { 98 | display: none; 99 | } 100 | 101 | @media screen and (min-width: 53em) { 102 | .menu { 103 | font-size: 3vw; 104 | grid-auto-flow: column; 105 | max-width: min-content; 106 | } 107 | .frame { 108 | padding: 1.5rem 2rem 0; 109 | display: grid; 110 | grid-template-columns: auto 1fr auto; 111 | grid-template-areas: 'title links sponsor'; 112 | grid-gap: 3vw; 113 | justify-content: space-between; 114 | text-align: left; 115 | } 116 | .frame__links { 117 | margin: 0; 118 | } 119 | } 120 | 121 | @media (any-pointer:fine) { 122 | .cursor { 123 | position: fixed; 124 | top: 0; 125 | left: 0; 126 | display: block; 127 | pointer-events: none; 128 | z-index: 10000; 129 | } 130 | 131 | .cursor__inner { 132 | fill: var(--cursor-fill); 133 | } 134 | 135 | .no-js .cursor { 136 | display: none; 137 | } 138 | 139 | } -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Linear interpolation 3 | * @param {Number} a - first value to interpolate 4 | * @param {Number} b - second value to interpolate 5 | * @param {Number} n - amount to interpolate 6 | */ 7 | const lerp = (a, b, n) => (1 - n) * a + n * b; 8 | 9 | /** 10 | * Map number x from range [a, b] to [c, d] 11 | * @param {Number} x - changing value 12 | * @param {Number} a 13 | * @param {Number} b 14 | * @param {Number} c 15 | * @param {Number} d 16 | */ 17 | const map = (x, a, b, c, d) => (x - a) * (d - c) / (b - a) + c; 18 | 19 | /** 20 | * Gets the cursor position 21 | * @param {Event} ev - mousemove event 22 | */ 23 | const getCursorPos = ev => { 24 | return { 25 | x : ev.clientX, 26 | y : ev.clientY 27 | }; 28 | }; 29 | 30 | // Track the cursor position 31 | let cursor = {x: 0, y: 0}; 32 | window.addEventListener('mousemove', ev => cursor = getCursorPos(ev)); 33 | 34 | /** 35 | * Class representing a custom cursor. 36 | * A Cursor can have multiple elements/svgs 37 | */ 38 | class Cursor { 39 | // DOM elements 40 | DOM = { 41 | // cursor elements (SVGs .cursor) 42 | elements: null, 43 | } 44 | // All CursorElement instances 45 | cursorElements = []; 46 | 47 | /** 48 | * Constructor. 49 | * @param {NodeList} Dom_elems - all .cursor elements 50 | * @param {String} triggerSelector - Trigger the cursor enter/leave method on the this selector returned elements. Default is all . 51 | */ 52 | constructor(Dom_elems, triggerSelector = 'a') { 53 | this.DOM.elements = Dom_elems; 54 | 55 | [...this.DOM.elements].forEach(el => this.cursorElements.push(new CursorElement(el))); 56 | 57 | [...document.querySelectorAll(triggerSelector)].forEach(link => { 58 | link.addEventListener('mouseenter', () => this.enter()); 59 | link.addEventListener('mouseleave', () => this.leave()); 60 | }); 61 | } 62 | /** 63 | * Mouseenter event 64 | */ 65 | enter() { 66 | for (const el of this.cursorElements) { 67 | el.enter(); 68 | } 69 | } 70 | 71 | /** 72 | * Mouseleave event 73 | */ 74 | leave() { 75 | for (const el of this.cursorElements) { 76 | el.leave(); 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * Class representing a .cursor element 83 | */ 84 | class CursorElement { 85 | // DOM elements 86 | DOM = { 87 | // Main element (.cursor) 88 | el: null, 89 | // Inner element (.cursor__inner) 90 | inner: null, 91 | // feTurbulence element 92 | feTurbulence: null 93 | } 94 | // Scales value when entering an element 95 | radiusOnEnter = 30; 96 | // Opacity value when entering an element 97 | opacityOnEnter = 1; 98 | // radius 99 | radius; 100 | // Element style properties 101 | renderedStyles = { 102 | tx: {previous: 0, current: 0, amt: 0.15}, 103 | ty: {previous: 0, current: 0, amt: 0.15}, 104 | // The scale and opacity will change when hovering over any element specified in [triggerSelector] 105 | // Defaults are 1 for both properties 106 | //scale: {previous: 1, current: 1, amt: 0.2}, 107 | radius: {previous: 20, current: 20, amt: 0.15}, 108 | opacity: {previous: 1, current: 1, amt: 0.15} 109 | }; 110 | // Element size and position 111 | bounds; 112 | // SVG filter id 113 | filterId = '#cursor-filter'; 114 | // for the filter animation 115 | primitiveValues = {turbulence: 0}; 116 | 117 | /** 118 | * Constructor. 119 | */ 120 | constructor(DOM_el) { 121 | this.DOM.el = DOM_el; 122 | this.DOM.inner = this.DOM.el.querySelector('.cursor__inner'); 123 | this.DOM.feTurbulence = document.querySelector(`${this.filterId} > feTurbulence`); 124 | 125 | this.createFilterTimeline(); 126 | 127 | // Hide it initially 128 | this.DOM.el.style.opacity = 0; 129 | 130 | // Calculate size and position 131 | this.bounds = this.DOM.el.getBoundingClientRect(); 132 | 133 | // Check if any options passed in data attributes 134 | this.radiusOnEnter = this.DOM.el.dataset.radiusEnter || this.radiusOnEnter; 135 | this.opacityOnEnter = this.DOM.el.dataset.opacityEnter || this.opacityOnEnter; 136 | for (const key in this.renderedStyles) { 137 | this.renderedStyles[key].amt = this.DOM.el.dataset.amt || this.renderedStyles[key].amt; 138 | } 139 | 140 | this.radius = this.DOM.inner.getAttribute('r'); 141 | this.renderedStyles['radius'].previous = this.renderedStyles['radius'].current = this.radius; 142 | 143 | // Show the element and start tracking its position as soon as the user moves the cursor. 144 | const onMouseMoveEv = () => { 145 | // Set up the initial values to be the same 146 | this.renderedStyles.tx.previous = this.renderedStyles.tx.current = cursor.x - this.bounds.width/2; 147 | this.renderedStyles.ty.previous = this.renderedStyles.ty.previous = cursor.y - this.bounds.height/2; 148 | // Show it 149 | this.DOM.el.style.opacity = 1; 150 | // Start rAF loop 151 | requestAnimationFrame(() => this.render()); 152 | // Remove the initial mousemove event 153 | window.removeEventListener('mousemove', onMouseMoveEv); 154 | }; 155 | window.addEventListener('mousemove', onMouseMoveEv); 156 | } 157 | 158 | /** 159 | * Mouseenter event 160 | * Scale up and fade out. 161 | */ 162 | enter() { 163 | this.renderedStyles['radius'].current = this.radiusOnEnter; 164 | this.renderedStyles['opacity'].current = this.opacityOnEnter; 165 | 166 | this.filterTimeline.restart(); 167 | } 168 | 169 | /** 170 | * Mouseleave event 171 | * Reset scale and opacity. 172 | */ 173 | leave() { 174 | this.DOM.inner.style.filter = 'none'; 175 | this.filterTimeline.kill(); 176 | this.renderedStyles['radius'].current = this.radius; 177 | this.renderedStyles['opacity'].current = 1; 178 | } 179 | 180 | createFilterTimeline() { 181 | const turbulenceValues = {from: 0.15, to: 0.25}; 182 | 183 | this.filterTimeline = gsap.timeline({ 184 | paused: true, 185 | onStart: () => { 186 | this.DOM.feTurbulence.setAttribute('seed', Math.round(gsap.utils.random(1,20))); 187 | this.DOM.inner.style.filter = `url(${this.filterId}`; 188 | this.renderedStyles['opacity'].current = 1; 189 | }, 190 | onUpdate: () => { 191 | this.DOM.feTurbulence.setAttribute('baseFrequency', this.primitiveValues.turbulence); 192 | this.renderedStyles['opacity'].current = this.renderedStyles['opacity'].previous = map(this.primitiveValues.turbulence, turbulenceValues.from, turbulenceValues.to, 1, 0); 193 | }, 194 | onComplete: () => { 195 | this.DOM.inner.style.filter = 'none'; 196 | this.renderedStyles['radius'].current = this.renderedStyles['radius'].previous = this.radius; 197 | } 198 | }) 199 | .to(this.primitiveValues, { 200 | duration: 2, 201 | ease: 'none', 202 | startAt: {turbulence: turbulenceValues.from}, 203 | turbulence: turbulenceValues.to 204 | }); 205 | } 206 | 207 | /** 208 | * Loop / Interpolation 209 | */ 210 | render() { 211 | // New cursor positions 212 | this.renderedStyles['tx'].current = cursor.x - this.bounds.width/2; 213 | this.renderedStyles['ty'].current = cursor.y - this.bounds.height/2; 214 | 215 | // Interpolation 216 | for (const key in this.renderedStyles ) { 217 | this.renderedStyles[key].previous = lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].amt); 218 | } 219 | 220 | // Apply interpolated values (smooth effect) 221 | this.DOM.el.style.transform = `translateX(${(this.renderedStyles['tx'].previous)}px) translateY(${this.renderedStyles['ty'].previous}px)`; 222 | this.DOM.inner.setAttribute('r', this.renderedStyles['radius'].previous); 223 | this.DOM.el.style.opacity = this.renderedStyles['opacity'].previous; 224 | 225 | // loop... 226 | requestAnimationFrame(() => this.render()); 227 | } 228 | } 229 | 230 | // Initialize custom cursor 231 | const customCursor = new Cursor(document.querySelectorAll('.cursor')); --------------------------------------------------------------------------------