Skip to content

Commit

Permalink
chore(util): add a utility function to create an interactive 3D effect
Browse files Browse the repository at this point in the history
This enables consumer components to display a nice
3D effect, when being hovered; enabling them to
follow the cursor's position and tilt towards it.
  • Loading branch information
Kiarokh authored and LucyChyzhova committed Nov 21, 2024
1 parent fe34ba9 commit 1f0f1d8
Show file tree
Hide file tree
Showing 2 changed files with 246 additions and 0 deletions.
95 changes: 95 additions & 0 deletions src/style/mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -456,3 +456,98 @@ $clickable-normal-state-transitions: (
clip-path: inset(50%);
white-space: nowrap;
}

// This mixin designed to enhance the visual effects,
// when the `tiltFollowingTheCursor` utility function from `3d-tilt-hover-effect.ts`
// is implemented in a component.
// This adds styles to a `<div class"limel-3d-hover-effect-glow" />`, needed to create
// a glow effect on a 3D element when the parent element is hovered.
// when the `tiltFollowingTheCursor` utility function from `3d-tilt-hover-effect.ts`
// Parts of these styles are controlled by the `titleFollowingTheCursor` function.
@mixin limel-3d-hover-effect-glow($the3dElement, $border-radius) {
.limel-3d-hover-effect-glow {
transition:
background 0.4s ease,
opacity 0.4s ease;
pointer-events: none;

position: absolute;
inset: 0;
border-radius: $border-radius;

opacity: 0.1;
#{$the3dElement}:hover & {
opacity: 0.5;
}

background-image: radial-gradient(
circle at var(--limel-3d-hover-effect-glow-position, 50% -20%),
rgb(var(--color-white), 0.3),
rgb(var(--color-white), 0)
);

mix-blend-mode: plus-lighter;
}
}

// These mixins below are designed to apply the necessary visual effects,
// when the `tiltFollowingTheCursor` utility function from `3d-tilt-hover-effect.ts`
// is implemented in a component.
@mixin parent-of-the-3d-element {
isolation: isolate;
transform-style: preserve-3d;
perspective: 1000px;
}

@mixin the-3d-element {
position: relative;

transition-duration: 0.8s;
transition-property: transform, box-shadow, background-color;
transition-timing-function: ease-out;
transform: scale3d(1, 1, 1) rotate3d(0, 0, 0, 0deg);

&:hover,
&:focus,
&:focus-visible,
&:focus-within {
will-change: background-color, box-shadow, transform;
}

&:hover,
&:focus,
&:focus-visible,
&:active {
transition-duration: 0.2s;
}

&:hover,
&:focus-visible {
box-shadow: var(--button-shadow-hovered);
}

&:hover {
transform: scale3d(1.01, 1.01, 1.01)
rotate3d(var(--limel-3d-hover-effect-rotate3d));
}
&:focus-visible {
transform: scale3d(1.01, 1.01, 1.01);
}
}

@mixin the-3d-element--clickable {
cursor: pointer;
box-shadow: var(--button-shadow-normal);

&:active {
transform: scale3d(1, 1, 1) rotate3d(0, 0, 0, 0deg);
box-shadow: var(--button-shadow-pressed);
}

&:focus-visible {
box-shadow: var(--button-shadow-hovered), var(--shadow-depth-8-focused);
}
&:focus-visible:active {
box-shadow: var(--button-shadow-pressed), var(--shadow-depth-8-focused);
}
}
151 changes: 151 additions & 0 deletions src/util/3d-tilt-hover-effect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* Utility functions for creating a 3D tilt hover effect.
*
* This module provides functions that enables consumer components to display a nice 3D effect,
* when being hovered; enabling them to follow the cursor's position and tilt towards it.
*
* ## What you need, to make this work
* ### Typescript
* 1. Import the functions:
*
* ```tsx
* import {
* tiltFollowingTheCursor,
* handleMouseEnter,
* handleMouseLeave,
* } from './path/to/3d-tilt-hover-effect';
* ```
*
* 2. In your component, define the necessary properties:
*
* ```tsx
* @Element() private element: HTMLElement;
* private the3dElementBounds: DOMRect;
* ```
*
* 3. If your component does not already have event handlers,
* implement them using the imported functions from this file:
*
* ```tsx
* private handleMouseEnter = () => {
* handleMouseEnter(this.element, 'section', (bounds) => {
* this.the3dElementBounds = bounds;
* }, this.tiltFollowingTheCursor);
* };
*
* private handleMouseLeave = () => {
* handleMouseLeave(this.element, this.tiltFollowingTheCursor);
* };
*
* private tiltFollowingTheCursor = (e: MouseEvent) => {
* tiltFollowingTheCursor(e, this.the3dElementBounds, this.element);
* };
* ```
*
* 4. Attach the event handlers to the relevant elements in your render method:
*
* ```tsx
* public render() {
* return (
* <section
* onMouseEnter={this.handleMouseEnter}
* onMouseLeave={this.handleMouseLeave}
* >
* Your content here
* </section>
* );
* }
* ```
*
* :::note
* - Ensure that the `element` and `the3dElementBounds` properties are properly
* defined in your component.
* - The `selector` parameter in `handleMouseEnter` should match the selector
* of the element you want to apply the 3D effect to.
* - The `tiltFollowingTheCursor` function calculates the 3D rotation and glow
* position based on the cursor's position relative to the element's bounds.
* :::
*
* ### HTML elements + CSS
* 1. Add a `<div class="limel-3d-hover-effect-glow" />` element to your component's template,
* inside the element you want to apply the 3D effect to, and preferably at the bottom of all
* other elements within that element (to avoid the need to specifying `z-index`es).
* 2. Add the following `mixin` to your component's SCSS file:
* `limel-3d-hover-effect-glow($the3dElement, $border-radius);`
*
* and don't forget to define the `$the3dElement` variables for the mixin to work
* (and optionally the `$border-radius`).
* 3. Keep in mind that the `<div class="limel-3d-hover-effect-glow" />` will be
* absolutely positioned inside the parent element, so make sure the parent element
* has `position` set.
* 4. Add the following `mixin` to the host element: `parent-of-the-3d-element`.
* 5. Add the following `mixin` to the 3D element: `the-3d-element`.
* 6. And if your element is supposed to be clickable, add this `mixin` as well: `the-3d-element--clickable`.
*
*/

export const MOUSE_SCALE_FACTOR = 100;
export const ROTATION_DEGREE_MULTIPLIER = 1.6;
export const GLOW_POSITION_MULTIPLIER = 2;
export const CENTER_DIVISOR = 2;

export function tiltFollowingTheCursor(
e: MouseEvent,
the3dElementBounds: DOMRect,
element: HTMLElement,
) {
const mouseX = e.clientX;
const mouseY = e.clientY;
const leftX = mouseX - the3dElementBounds.x;
const topY = mouseY - the3dElementBounds.y;
const center = {
x: leftX - the3dElementBounds.width / CENTER_DIVISOR,
y: topY - the3dElementBounds.height / CENTER_DIVISOR,
};
const distance = Math.sqrt(
center.x ** CENTER_DIVISOR + center.y ** CENTER_DIVISOR,
);

const rotate3d = `
${center.y / MOUSE_SCALE_FACTOR},
${-center.x / MOUSE_SCALE_FACTOR},
0,
${Math.log(distance) * ROTATION_DEGREE_MULTIPLIER}deg
`;
element.style.setProperty('--limel-3d-hover-effect-rotate3d', rotate3d);

const glowPosition = `
${center.x * GLOW_POSITION_MULTIPLIER + the3dElementBounds.width / CENTER_DIVISOR}px
`;
element.style.setProperty(
'--limel-3d-hover-effect-glow-position',
glowPosition,
);
}

export function handleMouseEnter(
element: HTMLElement,
selector: string,
setBounds: (bounds: DOMRect) => void,
tiltCallback: (e: MouseEvent) => void,
) {
const the3dElement = element.shadowRoot.querySelector(
selector,
) as HTMLElement;
const bounds = the3dElement.getBoundingClientRect();
setBounds(bounds);
document.addEventListener('mousemove', tiltCallback);
}

export function handleMouseLeave(
element: HTMLElement,
selector: string,
tiltCallback: (e: MouseEvent) => void,
) {
const the3dElement = element.shadowRoot.querySelector(
selector,
) as HTMLElement;
document.removeEventListener('mousemove', tiltCallback);
the3dElement.style.removeProperty('--limel-3d-hover-effect-rotate3d');
the3dElement.style.removeProperty('--limel-3d-hover-effect-glow-position');
}

0 comments on commit 1f0f1d8

Please sign in to comment.