first commit
This commit is contained in:
131
components/card/card.module.scss
Normal file
131
components/card/card.module.scss
Normal file
@@ -0,0 +1,131 @@
|
||||
.card {
|
||||
border-radius: 36px;
|
||||
overflow: hidden;
|
||||
padding: 36px;
|
||||
background-color: var(--background);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.textWrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--on-background);
|
||||
font-size: 1.8rem;
|
||||
max-width: 500px;
|
||||
font-weight: 800;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--on-background-secondary);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
max-width: 500px;
|
||||
margin-top: 6px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.backdropOnText {
|
||||
background-color: var(--background);
|
||||
padding: 0.5rem 8px;
|
||||
box-decoration-break: clone;
|
||||
-webkit-box-decoration-break: clone;
|
||||
}
|
||||
|
||||
.backdropOnText {
|
||||
background-color: var(--background);
|
||||
padding: 0 8px;
|
||||
box-decoration-break: clone;
|
||||
-webkit-box-decoration-break: clone;
|
||||
z-index: 1 !important;
|
||||
position: relative;
|
||||
box-shadow: 0px 5px 0px var(--background);
|
||||
}
|
||||
|
||||
.backdropTextHelper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 0 !important;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.backdropTextHelper .backdropOnText {
|
||||
box-shadow: 00px -5px 0px var(--background);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
margin-top: 48px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.cardLinkWrapper {
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.cardLink {
|
||||
border-radius: 16px;
|
||||
background-color: var(--background);
|
||||
|
||||
a {
|
||||
cursor: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cardLink:first-child {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.cardLinkContent {
|
||||
padding: 16px;
|
||||
padding-right: 48px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
color: var(--on-background);
|
||||
}
|
||||
|
||||
.cardLink .title {
|
||||
color: var(--on-background);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.cardLink .subtitle {
|
||||
color: var(--on-background-secondary);
|
||||
font-size: 0.75rem;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 620px) {
|
||||
.cardLink {
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
}
|
||||
}
|
||||
84
components/card/card.tsx
Normal file
84
components/card/card.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { clsx } from 'clsx';
|
||||
import styles from './card.module.scss';
|
||||
import { manrope } from '@/styles/fonts';
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
type CardProps = Omit<
|
||||
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
|
||||
'title'
|
||||
> & {
|
||||
title?: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
backdropOnText?: boolean;
|
||||
classes?: { [key: string]: string };
|
||||
};
|
||||
|
||||
const Card = forwardRef<HTMLDivElement, CardProps>(function Card(
|
||||
props: CardProps,
|
||||
ref
|
||||
) {
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
backdropOnText,
|
||||
children,
|
||||
classes = {},
|
||||
className: restClassName,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div ref={ref} className={clsx(styles.card, restClassName)} {...restProps}>
|
||||
<div className={clsx(styles.textWrapper, classes.textWrapper)}>
|
||||
<h3
|
||||
className={clsx(manrope.className, styles.title, classes.title)}
|
||||
>
|
||||
<span className={clsx(backdropOnText && styles.backdropOnText)}>
|
||||
{title}
|
||||
</span>
|
||||
</h3>
|
||||
{backdropOnText && (
|
||||
<h3
|
||||
className={clsx(
|
||||
manrope.className,
|
||||
styles.title,
|
||||
classes.title,
|
||||
styles.backdropTextHelper
|
||||
)}
|
||||
>
|
||||
<span className={clsx(backdropOnText && styles.backdropOnText)}>
|
||||
{title}
|
||||
</span>
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
{subtitle && (
|
||||
<div className={clsx(styles.textWrapper, classes.textWrapper)}>
|
||||
<p className={clsx(styles.subtitle, classes.subtitle)}>
|
||||
<span className={clsx(backdropOnText && styles.backdropOnText)}>
|
||||
{subtitle}
|
||||
</span>
|
||||
</p>
|
||||
{backdropOnText && (
|
||||
<p
|
||||
className={clsx(
|
||||
styles.subtitle,
|
||||
classes.subtitle,
|
||||
styles.backdropTextHelper
|
||||
)}
|
||||
>
|
||||
<span className={clsx(backdropOnText && styles.backdropOnText)}>
|
||||
{subtitle}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{children && (
|
||||
<div className={clsx(styles.content, classes.content)}>{children}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export { Card };
|
||||
64
components/card/cardLink.tsx
Normal file
64
components/card/cardLink.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { clsx } from 'clsx';
|
||||
import Link from 'next/link';
|
||||
import styles from './card.module.scss';
|
||||
import LinkIcon from '@/assets/icons/link.svg';
|
||||
import { animated, useSpring } from '@react-spring/web';
|
||||
import { CursorEffect } from '@/components/cursor';
|
||||
|
||||
type CardProps = React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {
|
||||
title?: string;
|
||||
hrefTitle?: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export function CardLink(props: CardProps) {
|
||||
const {
|
||||
title,
|
||||
hrefTitle,
|
||||
href,
|
||||
className: restClassName,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const [springs, api] = useSpring(() => ({
|
||||
scale: 1,
|
||||
config: { mass: 1, tension: 150, friction: 10 },
|
||||
}));
|
||||
|
||||
const handleEnter = () => {
|
||||
api.start({
|
||||
scale: 1.01,
|
||||
});
|
||||
};
|
||||
const handleLeave = () => {
|
||||
api.start({
|
||||
scale: 1,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<CursorEffect
|
||||
className={clsx(styles.cardLinkWrapper, restClassName)}
|
||||
cursorBorderRadius={16}
|
||||
cursorPadding={6}
|
||||
>
|
||||
<animated.div
|
||||
className={clsx(styles.cardLink)}
|
||||
style={{ ...springs }}
|
||||
onMouseEnter={handleEnter}
|
||||
onMouseLeave={handleLeave}
|
||||
>
|
||||
<Link href={href} target="_blank">
|
||||
<div className={styles.cardLinkContent}>
|
||||
<h3 className={styles.title}>{title}</h3>
|
||||
<p className={styles.subtitle}>{hrefTitle}</p>
|
||||
</div>
|
||||
<LinkIcon className={styles.icon} />
|
||||
</Link>
|
||||
</animated.div>
|
||||
</CursorEffect>
|
||||
);
|
||||
}
|
||||
2
components/card/index.tsx
Normal file
2
components/card/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './card';
|
||||
export * from './cardLink';
|
||||
29
components/cursor/cursor.module.css
Normal file
29
components/cursor/cursor.module.css
Normal file
@@ -0,0 +1,29 @@
|
||||
.root {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1000000;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hovered {
|
||||
backdrop-filter: invert(1);
|
||||
}
|
||||
|
||||
.idle {
|
||||
background-color: #000;
|
||||
|
||||
.spot {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.spot {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 50%;
|
||||
background-image: radial-gradient(#ffffffe6 0%, #ffffffbd 30%, #ffffff6e 50%, #ffffff00 70%);
|
||||
opacity: 0.2;
|
||||
}
|
||||
293
components/cursor/cursor.tsx
Normal file
293
components/cursor/cursor.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
import useFrame from '@/utils/useFrame';
|
||||
import { Fragment, useEffect, useRef } from 'react';
|
||||
import styles from './cursor.module.css';
|
||||
import { smooth } from '@/utils/smooth';
|
||||
|
||||
type CursorProps = {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Cursor(props: CursorProps) {
|
||||
const { children } = props;
|
||||
const mousePosition = useRef<{
|
||||
x: number;
|
||||
y: number;
|
||||
cursorPadding: number;
|
||||
cursorBorderRadius: number;
|
||||
targetRect: DOMRect | null;
|
||||
target: { link: boolean; button: boolean; cursorEffector: boolean };
|
||||
pressed: boolean;
|
||||
active: boolean;
|
||||
}>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
cursorPadding: 0,
|
||||
cursorBorderRadius: 0,
|
||||
targetRect: null,
|
||||
target: { link: false, button: false, cursorEffector: false },
|
||||
pressed: false,
|
||||
active: false,
|
||||
});
|
||||
const cursorRef = useRef<HTMLDivElement>(null);
|
||||
const cursorSpotRef = useRef<HTMLDivElement>(null);
|
||||
const prevCursorState = useRef<{
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
borderRadius: number;
|
||||
}>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderRadius: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let isReadyForHide = false;
|
||||
let isTouch = false;
|
||||
|
||||
const updateTargetFromPath = (path: any[]) => {
|
||||
const isLink = path.find((el) => el?.tagName === 'A');
|
||||
const isButton = path.find((el) => el?.tagName === 'BUTTON');
|
||||
const isCursorEffector = path.find((el) =>
|
||||
el?.getAttribute?.('data-cursor-effect')
|
||||
);
|
||||
const cursorPadding = +(
|
||||
path
|
||||
.find((el) => el?.getAttribute?.('data-cursor-padding') !== null)
|
||||
?.getAttribute?.('data-cursor-padding') || 16
|
||||
);
|
||||
|
||||
const cursorBorderRadius = +(
|
||||
path
|
||||
.find(
|
||||
(el) => el?.getAttribute?.('data-cursor-border-radius') !== null
|
||||
)
|
||||
?.getAttribute?.('data-cursor-border-radius') || 0
|
||||
);
|
||||
|
||||
let targetRect = null;
|
||||
|
||||
if (isLink) {
|
||||
targetRect = isLink.getBoundingClientRect();
|
||||
}
|
||||
|
||||
if (isButton) {
|
||||
targetRect = isButton.getBoundingClientRect();
|
||||
}
|
||||
|
||||
if (isCursorEffector) {
|
||||
targetRect = isCursorEffector.getBoundingClientRect();
|
||||
}
|
||||
|
||||
mousePosition.current = {
|
||||
...mousePosition.current,
|
||||
cursorPadding,
|
||||
cursorBorderRadius,
|
||||
targetRect,
|
||||
target: {
|
||||
link: Boolean(isLink),
|
||||
button: Boolean(isButton),
|
||||
cursorEffector: Boolean(isCursorEffector),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (isTouch) return;
|
||||
updateTargetFromPath(event.composedPath());
|
||||
|
||||
if (!mousePosition.current.active) {
|
||||
prevCursorState.current = {
|
||||
...prevCursorState.current,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
};
|
||||
}
|
||||
|
||||
isReadyForHide = false;
|
||||
|
||||
mousePosition.current = {
|
||||
...mousePosition.current,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
active: true,
|
||||
};
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
isReadyForHide = true;
|
||||
|
||||
setTimeout(() => {
|
||||
if (!isReadyForHide) return;
|
||||
|
||||
console.log('mouse leave');
|
||||
|
||||
mousePosition.current = {
|
||||
...mousePosition.current,
|
||||
active: false,
|
||||
};
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (isTouch) return;
|
||||
updateTargetFromPath(
|
||||
document.elementsFromPoint(
|
||||
mousePosition.current.x,
|
||||
mousePosition.current.y
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleMouseDown = (event: MouseEvent) => {
|
||||
if (isTouch) return;
|
||||
updateTargetFromPath(event.composedPath());
|
||||
mousePosition.current = {
|
||||
...mousePosition.current,
|
||||
pressed: true,
|
||||
};
|
||||
};
|
||||
|
||||
const handleMouseUp = (event: MouseEvent) => {
|
||||
if (isTouch) return;
|
||||
updateTargetFromPath(event.composedPath());
|
||||
mousePosition.current = {
|
||||
...mousePosition.current,
|
||||
pressed: false,
|
||||
};
|
||||
};
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
isTouch = event.pointerType === 'touch';
|
||||
|
||||
if (isTouch) handleMouseLeave();
|
||||
};
|
||||
|
||||
window.addEventListener('pointerdown', handlePointerDown);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.document.body.addEventListener('mouseleave', handleMouseLeave);
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
window.addEventListener('mousedown', handleMouseDown);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('pointerdown', handlePointerDown);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.document.body.removeEventListener('mouseleave', handleMouseLeave);
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('click', handleMouseDown);
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useFrame((delta) => {
|
||||
let x = mousePosition.current.x;
|
||||
let y = mousePosition.current.y;
|
||||
let width = 30;
|
||||
let height = 30;
|
||||
let borderRadius = 0;
|
||||
|
||||
let gapHover = mousePosition.current.cursorPadding;
|
||||
const targetRect = mousePosition.current.targetRect;
|
||||
|
||||
const isHovered =
|
||||
(mousePosition.current.target.link ||
|
||||
mousePosition.current.target.button ||
|
||||
mousePosition.current.target.cursorEffector) &&
|
||||
targetRect !== null;
|
||||
|
||||
if (!mousePosition.current.active) {
|
||||
width = 0;
|
||||
height = 0;
|
||||
borderRadius = 15;
|
||||
} else if (isHovered) {
|
||||
const targetCenterX = targetRect.left + targetRect.width / 2;
|
||||
const targetCenterY = targetRect.top + targetRect.height / 2;
|
||||
|
||||
width = targetRect.width + gapHover * 2;
|
||||
height = targetRect.height + gapHover * 2;
|
||||
|
||||
x = x * 0.05 + targetCenterX * 0.95 - width / 2;
|
||||
y = y * 0.05 + targetCenterY * 0.95 - height / 2;
|
||||
} else {
|
||||
width = 30;
|
||||
height = 30;
|
||||
x = x - width / 2;
|
||||
y = y - height / 2;
|
||||
borderRadius = 15;
|
||||
}
|
||||
|
||||
if (mousePosition.current.pressed) {
|
||||
width -= 10;
|
||||
height -= 10;
|
||||
x += 5;
|
||||
y += 5;
|
||||
gapHover -= 5;
|
||||
}
|
||||
|
||||
if (mousePosition.current.cursorBorderRadius) {
|
||||
borderRadius = mousePosition.current.cursorBorderRadius + gapHover;
|
||||
} else {
|
||||
borderRadius = Math.min(width / 2, height / 2);
|
||||
}
|
||||
|
||||
x = smooth(prevCursorState.current.x, x, 0.02 * delta);
|
||||
y = smooth(prevCursorState.current.y, y, 0.02 * delta);
|
||||
width = smooth(prevCursorState.current.width, width, 0.02 * delta);
|
||||
height = smooth(prevCursorState.current.height, height, 0.02 * delta);
|
||||
borderRadius = smooth(
|
||||
prevCursorState.current.borderRadius,
|
||||
borderRadius,
|
||||
0.02 * delta
|
||||
);
|
||||
|
||||
prevCursorState.current = {
|
||||
...prevCursorState.current,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
borderRadius,
|
||||
};
|
||||
|
||||
const cursorNode = cursorRef.current;
|
||||
const cursorSpotNode = cursorSpotRef.current;
|
||||
|
||||
if (cursorNode !== null && cursorSpotNode !== null) {
|
||||
cursorNode.style.transform = `translate3D(${x}px, ${y}px, 0)`;
|
||||
cursorNode.style.width = `${width}px`;
|
||||
cursorNode.style.height = `${height}px`;
|
||||
cursorNode.style.borderRadius = `${Math.min(borderRadius, 36 + gapHover)}px`;
|
||||
|
||||
const size = Math.max((width + height) / 2, 48);
|
||||
|
||||
cursorSpotNode.style.transform = `translate(${mousePosition.current.x - x - size / 2}px, ${mousePosition.current.y - y - size / 2}px)`;
|
||||
cursorSpotNode.style.width = `${size}px`;
|
||||
cursorSpotNode.style.height = `${size}px`;
|
||||
|
||||
if (isHovered) {
|
||||
if (!cursorNode.classList.contains(styles.hovered)) {
|
||||
cursorNode.classList.add(styles.hovered);
|
||||
cursorNode.classList.remove(styles.idle);
|
||||
}
|
||||
} else {
|
||||
if (!cursorNode.classList.contains(styles.idle)) {
|
||||
cursorNode.classList.add(styles.idle);
|
||||
cursorNode.classList.remove(styles.hovered);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div ref={cursorRef} className={styles.root}>
|
||||
<div ref={cursorSpotRef} className={styles.spot} />
|
||||
</div>
|
||||
{children}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
196
components/cursor/cursorEffect.tsx
Normal file
196
components/cursor/cursorEffect.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import { smooth } from '@/utils/smooth';
|
||||
import useFrame from '@/utils/useFrame';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
type CursorEffectProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
effectDistance?: number;
|
||||
effectForce?: number;
|
||||
cursorPadding?: number;
|
||||
cursorBorderRadius?: number;
|
||||
};
|
||||
|
||||
const clamp = (value: number, min: number, max: number) => {
|
||||
const minOffset = Math.max(value - min, 0);
|
||||
|
||||
return Math.min(minOffset / (max - min), 1);
|
||||
};
|
||||
|
||||
const inRect = (x: number, y: number, rect: DOMRect, gap: number = 0) => {
|
||||
return (
|
||||
rect.left - gap < x &&
|
||||
x < rect.right + gap &&
|
||||
rect.top - gap < y &&
|
||||
y < rect.bottom + gap
|
||||
);
|
||||
};
|
||||
|
||||
export function CursorEffect(props: CursorEffectProps) {
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
effectDistance = 128,
|
||||
effectForce = 6,
|
||||
cursorPadding = 16,
|
||||
cursorBorderRadius = 0,
|
||||
} = props;
|
||||
const cursorAffectorRef = useRef<HTMLDivElement>(null);
|
||||
const cursorAffectoContainerrRef = useRef<HTMLDivElement>(null);
|
||||
const mousePosition = useRef<{
|
||||
x: number;
|
||||
y: number;
|
||||
targetRect: DOMRect | null;
|
||||
}>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
targetRect: null,
|
||||
});
|
||||
const prevCursorState = useRef<{
|
||||
x: number;
|
||||
y: number;
|
||||
}>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let isTouch = false;
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (isTouch) return;
|
||||
|
||||
let targetRect =
|
||||
cursorAffectoContainerrRef.current?.getBoundingClientRect() || null;
|
||||
|
||||
mousePosition.current = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
targetRect,
|
||||
};
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (isTouch) return;
|
||||
|
||||
let targetRect =
|
||||
cursorAffectoContainerrRef.current?.getBoundingClientRect() || null;
|
||||
|
||||
mousePosition.current = {
|
||||
...mousePosition.current,
|
||||
targetRect,
|
||||
};
|
||||
};
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
isTouch = event.pointerType === 'touch';
|
||||
|
||||
let targetRect =
|
||||
cursorAffectoContainerrRef.current?.getBoundingClientRect() || null;
|
||||
|
||||
mousePosition.current = {
|
||||
...mousePosition.current,
|
||||
targetRect,
|
||||
x: -9999999,
|
||||
y: -9999999,
|
||||
};
|
||||
};
|
||||
|
||||
window.addEventListener('pointerdown', handlePointerDown);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('pointerdown', handlePointerDown);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useFrame((delta) => {
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
||||
const gap = effectDistance;
|
||||
const offsetMax = effectForce;
|
||||
|
||||
const mousePos = mousePosition.current;
|
||||
const rect = mousePosition.current.targetRect;
|
||||
|
||||
const isActive = rect && inRect(mousePos.x, mousePos.y, rect, gap);
|
||||
|
||||
if (isActive) {
|
||||
const halfWidth = rect.width / 2;
|
||||
const targetCenterX = rect.left + halfWidth;
|
||||
const leftStart = rect.left - gap;
|
||||
const leftMiddle = rect.left - offsetMax;
|
||||
const rightMiddle = rect.right + offsetMax;
|
||||
const rightEnd = rect.right + gap;
|
||||
|
||||
const affectX =
|
||||
clamp(mousePos.y, rect.top - gap, rect.top) *
|
||||
(1 - clamp(mousePos.y, rect.bottom, rect.bottom + gap));
|
||||
|
||||
if (mousePos.x < leftMiddle) {
|
||||
x = -clamp(mousePos.x, leftStart, leftMiddle);
|
||||
} else if (mousePos.x < targetCenterX) {
|
||||
x = -(1 - clamp(mousePos.x, leftMiddle, targetCenterX));
|
||||
} else if (mousePos.x < rightMiddle) {
|
||||
x = clamp(mousePos.x, targetCenterX, rightMiddle);
|
||||
} else if (mousePos.x < rightEnd) {
|
||||
x = 1 - clamp(mousePos.x, rightMiddle, rightEnd);
|
||||
}
|
||||
|
||||
x = x * affectX;
|
||||
|
||||
const halfHeight = rect.height / 2;
|
||||
const targetCenterY = rect.top + halfHeight;
|
||||
const topStart = rect.top - gap;
|
||||
const topMiddle = rect.top - offsetMax;
|
||||
const bottomMiddle = rect.bottom + offsetMax;
|
||||
const bottomEnd = rect.bottom + gap;
|
||||
|
||||
const affectY =
|
||||
clamp(mousePos.x, rect.left - gap, rect.left) *
|
||||
(1 - clamp(mousePos.x, rect.right, rect.right + gap));
|
||||
|
||||
if (mousePos.y < topMiddle) {
|
||||
y = -clamp(mousePos.y, topStart, topMiddle);
|
||||
} else if (mousePos.y < targetCenterY) {
|
||||
y = -(1 - clamp(mousePos.y, topMiddle, targetCenterY));
|
||||
} else if (mousePos.y < bottomMiddle) {
|
||||
y = clamp(mousePos.y, targetCenterY, bottomMiddle);
|
||||
} else if (mousePos.y < bottomEnd) {
|
||||
y = 1 - clamp(mousePos.y, bottomMiddle, bottomEnd);
|
||||
}
|
||||
|
||||
y = y * affectY;
|
||||
|
||||
x = x * offsetMax;
|
||||
y = y * offsetMax;
|
||||
}
|
||||
|
||||
x = smooth(prevCursorState.current.x, x, 0.02 * delta);
|
||||
y = smooth(prevCursorState.current.y, y, 0.02 * delta);
|
||||
|
||||
prevCursorState.current = { x, y };
|
||||
|
||||
if (cursorAffectorRef.current) {
|
||||
cursorAffectorRef.current.style.transform = `translate3D(${x}px, ${y}px, 0)`;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={cursorAffectoContainerrRef} className={className}>
|
||||
<div
|
||||
ref={cursorAffectorRef}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
data-cursor-effect
|
||||
data-cursor-padding={cursorPadding}
|
||||
data-cursor-border-radius={cursorBorderRadius}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
2
components/cursor/index.tsx
Normal file
2
components/cursor/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './cursor';
|
||||
export * from './cursorEffect';
|
||||
51
components/footer/footer.module.scss
Normal file
51
components/footer/footer.module.scss
Normal file
@@ -0,0 +1,51 @@
|
||||
.footer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0px 48px 48px;
|
||||
max-width: 1500px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.fatLittleStar {
|
||||
position: relative;
|
||||
margin-right: auto;
|
||||
transform: translate(30px, 10px);
|
||||
color: #0059ff;
|
||||
z-index: -1;
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.xtimms {
|
||||
color: #0059ff;
|
||||
cursor: none !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1240px) {
|
||||
.footer {
|
||||
padding: 0px 36px 36px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.footer {
|
||||
padding: 0px 24px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.footer {
|
||||
padding: 0px 16px 16px;
|
||||
}
|
||||
}
|
||||
63
components/footer/footer.tsx
Normal file
63
components/footer/footer.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import useLocale, { LocalesMap } from '@/utils/useLocale';
|
||||
import { clsx } from 'clsx';
|
||||
import Link from 'next/link';
|
||||
import styles from './footer.module.scss';
|
||||
import FatLittleStar from '@/assets/images/fat-little-star.svg';
|
||||
import { useRef } from 'react';
|
||||
import useScroll from '@/utils/useScroll';
|
||||
|
||||
type CardProps = React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>;
|
||||
|
||||
const localesMap: LocalesMap = {
|
||||
ru: {
|
||||
madeBy: 'сделано',
|
||||
},
|
||||
en: {
|
||||
madeBy: 'made by',
|
||||
},
|
||||
};
|
||||
|
||||
export function Footer(props: CardProps) {
|
||||
const { className: restClassName, ...restProps } = props;
|
||||
const t = useLocale(localesMap);
|
||||
const fatLittleStarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useScroll((scrollY) => {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
window.document.scrollingElement === null
|
||||
)
|
||||
return;
|
||||
|
||||
const scrollOffset =
|
||||
window.document.scrollingElement.scrollHeight -
|
||||
window.innerHeight -
|
||||
scrollY;
|
||||
|
||||
if (fatLittleStarRef.current) {
|
||||
fatLittleStarRef.current.style.transform = `translate3D(30px, ${scrollOffset / -2 + 10}px, 0) rotate(${scrollOffset / 10}deg)`;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<footer className={clsx(restClassName, styles.footer)} {...restProps}>
|
||||
<div ref={fatLittleStarRef} className={styles.fatLittleStar}>
|
||||
<FatLittleStar />
|
||||
</div>
|
||||
<p className={styles.author}>
|
||||
{`${t('madeBy')} `}
|
||||
<Link
|
||||
className={styles.xtimms}
|
||||
href="https://xtimms.t.me"
|
||||
target="_blank"
|
||||
prefetch={false}
|
||||
>
|
||||
@xtimms
|
||||
</Link>
|
||||
</p>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
1
components/footer/index.tsx
Normal file
1
components/footer/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './footer';
|
||||
38
components/githubCardButton/githubCardButton.module.scss
Normal file
38
components/githubCardButton/githubCardButton.module.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
.card {
|
||||
border-radius: 36px;
|
||||
overflow: hidden;
|
||||
background-color: #a4a4a41c;
|
||||
backdrop-filter: blur(18px);
|
||||
|
||||
a {
|
||||
cursor: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 14px;
|
||||
padding-left: 28px;
|
||||
padding-right: 22px;
|
||||
transition: 0.3s ease;
|
||||
}
|
||||
|
||||
.linkIcon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.storeNameText {
|
||||
color: #242424;
|
||||
font-weight: 700;
|
||||
font-size: 2rem;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 620px) {
|
||||
.content {
|
||||
padding: 36px;
|
||||
}
|
||||
}
|
||||
28
components/githubCardButton/githubCardButton.tsx
Normal file
28
components/githubCardButton/githubCardButton.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import styles from './githubCardButton.module.scss';
|
||||
import Link, { LinkProps } from 'next/link';
|
||||
import LinkIcon from '@/assets/icons/link.svg';
|
||||
import clsx from 'clsx';
|
||||
import { CursorEffect } from '@/components/cursor';
|
||||
|
||||
type GithubCardButtonProps = Omit<LinkProps, 'href'> & { className?: string };
|
||||
|
||||
export function GithubCardButton(props: GithubCardButtonProps) {
|
||||
const { className: restCalssName, ...restProps } = props;
|
||||
|
||||
return (
|
||||
<CursorEffect cursorBorderRadius={36} cursorPadding={12}>
|
||||
<div className={clsx(styles.card, restCalssName)}>
|
||||
<Link
|
||||
{...restProps}
|
||||
target="_blank"
|
||||
href="https://github.com/ztimms73/ridebus"
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<span className={styles.storeNameText}>GitHub</span>
|
||||
<LinkIcon className={styles.linkIcon} />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</CursorEffect>
|
||||
);
|
||||
}
|
||||
1
components/githubCardButton/index.tsx
Normal file
1
components/githubCardButton/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './githubCardButton';
|
||||
14
components/githubLink/githubLink.module.scss
Normal file
14
components/githubLink/githubLink.module.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
.linkWrapper {
|
||||
display: inline-flex;
|
||||
height: min-content;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: inline-flex;
|
||||
padding: 18px;
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.linkIcon {
|
||||
color: var(--on-background);
|
||||
}
|
||||
30
components/githubLink/githubLink.tsx
Normal file
30
components/githubLink/githubLink.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import styles from './githubLink.module.scss';
|
||||
import Link, { LinkProps } from 'next/link';
|
||||
import GithubIcon from '@/assets/icons/github.svg';
|
||||
import clsx from 'clsx';
|
||||
import { CursorEffect } from '@/components/cursor';
|
||||
|
||||
type GithubLinkProps = Omit<LinkProps, 'href'> & { className?: string };
|
||||
|
||||
export function GithubLink(props: GithubLinkProps) {
|
||||
const { className: restClassName, ...restProps } = props;
|
||||
|
||||
return (
|
||||
<CursorEffect
|
||||
effectDistance={48}
|
||||
effectForce={4}
|
||||
cursorPadding={0}
|
||||
className={clsx(styles.linkWrapper)}
|
||||
>
|
||||
<Link
|
||||
{...restProps}
|
||||
className={clsx(styles.link, restClassName)}
|
||||
target="_blank"
|
||||
href="https://github.com/ztimms73/ridebus"
|
||||
prefetch={false}
|
||||
>
|
||||
<GithubIcon className={styles.linkIcon} />
|
||||
</Link>
|
||||
</CursorEffect>
|
||||
);
|
||||
}
|
||||
1
components/githubLink/index.tsx
Normal file
1
components/githubLink/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './githubLink';
|
||||
89
components/googlePlayButton/googlePlayButton.module.scss
Normal file
89
components/googlePlayButton/googlePlayButton.module.scss
Normal file
@@ -0,0 +1,89 @@
|
||||
.card {
|
||||
border-radius: 36px;
|
||||
overflow: hidden;
|
||||
background-color: #a4a4a41c;
|
||||
transition: 0.3s ease;
|
||||
backdrop-filter: blur(18px);
|
||||
|
||||
&:hover {
|
||||
background-color: #3d3d3d1c;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: none;
|
||||
}
|
||||
}
|
||||
|
||||
.disableGlass {
|
||||
background-color: #f5f5f5;
|
||||
backdrop-filter: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.qrCode {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin-right: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.textContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.linkIcon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.getText {
|
||||
color: #838383;
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
margin-top: auto;
|
||||
margin-bottom: -6px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.storeNameText {
|
||||
color: #242424;
|
||||
font-weight: 700;
|
||||
font-size: 2rem;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1180px) {
|
||||
.qrCode {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 980px) {
|
||||
.qrCode {
|
||||
display: none;
|
||||
}
|
||||
.content {
|
||||
padding-left: 32px;
|
||||
padding-bottom: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 620px) {
|
||||
.content {
|
||||
padding: 36px;
|
||||
}
|
||||
}
|
||||
57
components/googlePlayButton/googlePlayButton.tsx
Normal file
57
components/googlePlayButton/googlePlayButton.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import styles from './googlePlayButton.module.scss';
|
||||
import Link, { LinkProps } from 'next/link';
|
||||
import QRCodeSvg from '@/assets/images/qr-code.svg';
|
||||
import LinkIcon from '@/assets/icons/link.svg';
|
||||
import useLocale, { LocalesMap } from '@/utils/useLocale';
|
||||
import clsx from 'clsx';
|
||||
import { CursorEffect } from '@/components/cursor';
|
||||
|
||||
const locales: LocalesMap = {
|
||||
ru: {
|
||||
getText: 'Доступно в',
|
||||
},
|
||||
en: {
|
||||
getText: 'Get it on',
|
||||
},
|
||||
};
|
||||
|
||||
type GooglePlayButtonProps = Omit<LinkProps, 'href'> & {
|
||||
className?: string;
|
||||
disableGlass?: boolean;
|
||||
};
|
||||
|
||||
export function GooglePlayButton(props: GooglePlayButtonProps) {
|
||||
const {
|
||||
className: restCalssName,
|
||||
disableGlass = false,
|
||||
...restProps
|
||||
} = props;
|
||||
const t = useLocale(locales);
|
||||
|
||||
return (
|
||||
<CursorEffect cursorBorderRadius={36} cursorPadding={12}>
|
||||
<div
|
||||
className={clsx(
|
||||
styles.card,
|
||||
disableGlass && styles.disableGlass,
|
||||
restCalssName
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
{...restProps}
|
||||
target="_blank"
|
||||
href="https://play.google.com/store/apps/details?id=org.xtimms.ridebus"
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<QRCodeSvg className={styles.qrCode} />
|
||||
<div className={styles.textContainer}>
|
||||
<LinkIcon className={styles.linkIcon} />
|
||||
<span className={styles.getText}>{t('getText')}</span>
|
||||
<span className={styles.storeNameText}>Google Play</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</CursorEffect>
|
||||
);
|
||||
}
|
||||
1
components/googlePlayButton/index.tsx
Normal file
1
components/googlePlayButton/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './googlePlayButton';
|
||||
13
components/googlePlayLink/googlePlayLink.module.scss
Normal file
13
components/googlePlayLink/googlePlayLink.module.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
.rootWrapper {
|
||||
}
|
||||
|
||||
.root {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--on-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 18px;
|
||||
cursor: none;
|
||||
}
|
||||
40
components/googlePlayLink/googlePlayLink.tsx
Normal file
40
components/googlePlayLink/googlePlayLink.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import styles from './googlePlayLink.module.scss';
|
||||
import Link, { LinkProps } from 'next/link';
|
||||
import LinkIcon from '@/assets/icons/link.svg';
|
||||
import useLocale, { LocalesMap } from '@/utils/useLocale';
|
||||
import clsx from 'clsx';
|
||||
import { CursorEffect } from '@/components/cursor';
|
||||
|
||||
const locales: LocalesMap = {
|
||||
ru: {
|
||||
getText: 'Доступно в Google Play',
|
||||
},
|
||||
en: {
|
||||
getText: 'Get it on Google Play',
|
||||
},
|
||||
};
|
||||
|
||||
type GooglePlayLinkProps = Omit<LinkProps, 'href'> & { className?: string };
|
||||
|
||||
export function GooglePlayLink(props: GooglePlayLinkProps) {
|
||||
const { className: restCalssName, ...restProps } = props;
|
||||
const t = useLocale(locales);
|
||||
|
||||
return (
|
||||
<CursorEffect
|
||||
effectDistance={48}
|
||||
effectForce={4}
|
||||
cursorPadding={0}
|
||||
className={clsx(styles.rootWrapper, restCalssName)}
|
||||
>
|
||||
<Link
|
||||
{...restProps}
|
||||
className={clsx(styles.root)}
|
||||
target="_blank"
|
||||
href="https://play.google.com/store/apps/details?id=org.xtimms.ridebus"
|
||||
>
|
||||
{t('getText')} <LinkIcon />
|
||||
</Link>
|
||||
</CursorEffect>
|
||||
);
|
||||
}
|
||||
1
components/googlePlayLink/index.tsx
Normal file
1
components/googlePlayLink/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './googlePlayLink';
|
||||
18
components/gradients/gradient.fragmentShader.glsl
Normal file
18
components/gradients/gradient.fragmentShader.glsl
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifdef GL_ES
|
||||
precision mediump float;
|
||||
#endif
|
||||
|
||||
const vec3 background = vec3(1.0);
|
||||
const vec3 primary = vec3(0., 0.345, 0.761);
|
||||
const vec3 tertiary = vec3(0.675, 0.514, 0.682);
|
||||
|
||||
varying float vHeight;
|
||||
varying float vColor;
|
||||
|
||||
void main() {
|
||||
float height = vHeight * 2.0 + 0.9;
|
||||
|
||||
vec3 color = mix(primary, tertiary, vColor * 3.0 + 0.5);
|
||||
|
||||
gl_FragColor = vec4(mix(color, background, height), 1.0);
|
||||
}
|
||||
116
components/gradients/gradient.tsx
Normal file
116
components/gradients/gradient.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import styles from './gradients.module.css';
|
||||
import { initGrainFilter } from './grainPostProcessing';
|
||||
import { InitThreeOptions, initThree } from './initThree';
|
||||
import { initScene } from './initScene';
|
||||
import { initGradient } from './initGradients';
|
||||
import Stats from 'stats.js';
|
||||
|
||||
const threeCanvas = (
|
||||
canvas: HTMLCanvasElement,
|
||||
viewportSize: InitThreeOptions,
|
||||
initTimeOffset: number = 0
|
||||
) => {
|
||||
const { renderer, clock } = initThree(canvas, viewportSize);
|
||||
const { scene, camera, updateSize: updateCameraSize } = initScene(renderer);
|
||||
const {
|
||||
render: renderGradient,
|
||||
updateSize: updateGradientSize,
|
||||
unload: unloadGradinet,
|
||||
} = initGradient(scene, renderer);
|
||||
const { render: renderFrame, resize: resizeFrame } = initGrainFilter(
|
||||
renderer,
|
||||
scene,
|
||||
camera
|
||||
);
|
||||
//const stats = new Stats();
|
||||
// the number will decide which information will be displayed
|
||||
// 0 => FPS Frames rendered in the last second. The higher the number the better.
|
||||
// 1 => MS Milliseconds needed to render a frame. The lower the number the better.
|
||||
// 2 => MB MBytes of allocated memory. (Run Chrome with --enable-precise-memory-info)
|
||||
// 3 => CUSTOM User-defined panel support.
|
||||
//stats.showPanel(0);
|
||||
|
||||
//document.body.appendChild(stats.dom);
|
||||
|
||||
let elapsedTime = 0;
|
||||
let delta = 0;
|
||||
let isActive = true;
|
||||
|
||||
const animate = () => {
|
||||
if (!isActive) return;
|
||||
//stats.begin();
|
||||
elapsedTime = clock.elapsedTime + initTimeOffset;
|
||||
delta = clock.getDelta();
|
||||
|
||||
renderGradient(elapsedTime, delta);
|
||||
renderFrame(elapsedTime, delta);
|
||||
|
||||
//stats.end();
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
const resizeCanvas = (width: number, height: number) => {
|
||||
renderer.setSize(width, height);
|
||||
updateCameraSize();
|
||||
updateGradientSize();
|
||||
resizeFrame();
|
||||
};
|
||||
|
||||
animate();
|
||||
resizeCanvas(viewportSize.width, viewportSize.height);
|
||||
|
||||
const unload = () => {
|
||||
isActive = false;
|
||||
unloadGradinet();
|
||||
renderer.dispose();
|
||||
};
|
||||
|
||||
return {
|
||||
resizeCanvas,
|
||||
unload,
|
||||
};
|
||||
};
|
||||
|
||||
export function Gradients() {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [timeOffset] = useState(Math.random() * 1000);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const canvas = threeCanvas(
|
||||
canvasRef.current as HTMLCanvasElement,
|
||||
{
|
||||
width: rootRef.current?.clientWidth || 0,
|
||||
height: rootRef.current?.clientHeight || 0,
|
||||
},
|
||||
timeOffset
|
||||
);
|
||||
|
||||
const resize = () => {
|
||||
canvas.resizeCanvas(
|
||||
rootRef.current?.clientWidth || 0,
|
||||
rootRef.current?.clientHeight || 0
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize);
|
||||
|
||||
canvas.unload();
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, [timeOffset]);
|
||||
|
||||
return (
|
||||
<div className={styles.root} ref={rootRef}>
|
||||
<canvas className={styles.canvas} ref={canvasRef}></canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
75
components/gradients/gradient.vertexShader.glsl
Normal file
75
components/gradients/gradient.vertexShader.glsl
Normal file
@@ -0,0 +1,75 @@
|
||||
#ifdef GL_ES
|
||||
precision mediump float;
|
||||
#endif
|
||||
|
||||
const float PI = 3.14159265359;
|
||||
const float SCALE = 0.0002;
|
||||
const float SPEED = 0.5;
|
||||
const float NOISE_FREQ = 3.5;
|
||||
const float NOISE_AMP = 0.3;
|
||||
|
||||
varying vec2 vUv;
|
||||
varying float vHeight;
|
||||
varying float vColor;
|
||||
uniform float uTime;
|
||||
uniform sampler2D map;
|
||||
|
||||
float rand(vec2 co, float l, float t) {
|
||||
return fract(sin(dot(co.xy + vec2(l, t), vec2(12.9898, 78.233))) * 43758.5453);
|
||||
}
|
||||
|
||||
float perlin(vec2 p, float dim, float time) {
|
||||
vec2 pos = floor(p * dim);
|
||||
vec2 posx = pos + vec2(1.0, 0.0);
|
||||
vec2 posy = pos + vec2(0.0, 1.0);
|
||||
vec2 posxy = pos + vec2(1.0);
|
||||
|
||||
float c = rand(pos, dim, time);
|
||||
float cx = rand(posx, dim, time);
|
||||
float cy = rand(posy, dim, time);
|
||||
float cxy = rand(posxy, dim, time);
|
||||
|
||||
vec2 d = fract(p * dim);
|
||||
d = -0.5 * cos(d * PI) + 0.5;
|
||||
|
||||
float ccx = mix(c, cx, d.x);
|
||||
float cycxy = mix(cy, cxy, d.x);
|
||||
float center = mix(ccx, cycxy, d.y);
|
||||
|
||||
return center * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
float snoiseFoam(vec2 pos, const float freq) {
|
||||
return perlin(vec2(pos.y * freq, pos.x * freq), 9.0, 0.0);
|
||||
}
|
||||
|
||||
float snoiseWater(vec2 pos, const float freq) {
|
||||
return perlin(vec2(pos.y * freq, pos.x * freq), 6.0, 0.0);
|
||||
}
|
||||
|
||||
mat2 rotate2d(float _angle) {
|
||||
return mat2(cos(_angle), -sin(_angle), sin(_angle), cos(_angle));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
|
||||
vec2 rTimeSpeed = vec2(uTime) * rotate2d(PI * (70.0 / 180.0)) * 0.15 * SPEED;
|
||||
vec2 rTimeSlow = vec2(uTime) * rotate2d(PI * (250.0 / 180.0)) * 0.1 * SPEED;
|
||||
vec2 rTimeMiddle = vec2(uTime + 100.0) * rotate2d(PI * (300.0 / 180.0)) * 0.12 * SPEED;
|
||||
|
||||
vec2 freqPos = position.xy * SCALE * NOISE_FREQ;
|
||||
|
||||
float waveBig = snoiseWater(freqPos + rTimeSlow, 0.08) * 0.4;
|
||||
float waveMiddle = snoiseFoam(freqPos + rTimeMiddle, 0.1) * 0.3;
|
||||
float waveSmall = snoiseFoam(freqPos + rTimeSpeed, 0.1) * 0.3;
|
||||
|
||||
vColor = waveMiddle;
|
||||
|
||||
vHeight = (waveBig + waveSmall + waveMiddle) * NOISE_AMP;
|
||||
|
||||
vec3 pos = position;
|
||||
pos.z += vHeight / SCALE;
|
||||
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
||||
}
|
||||
14
components/gradients/gradients.module.css
Normal file
14
components/gradients/gradients.module.css
Normal file
@@ -0,0 +1,14 @@
|
||||
.root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
7
components/gradients/gradients.module.scss
Normal file
7
components/gradients/gradients.module.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.main {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
95
components/gradients/grain.fragmentShader.glsl
Normal file
95
components/gradients/grain.fragmentShader.glsl
Normal file
@@ -0,0 +1,95 @@
|
||||
uniform float uTime;
|
||||
uniform sampler2D tDiffuse;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
#define SHOW_NOISE 0
|
||||
#define SRGB 0
|
||||
// 0: Addition, 1: Screen, 2: Overlay, 3: Soft Light, 4: Lighten-Only
|
||||
#define BLEND_MODE 0
|
||||
|
||||
//0 to 1
|
||||
#define SPEED 0.000001
|
||||
|
||||
#define INTENSITY 0.02
|
||||
// What gray level noise should tend to.
|
||||
#define MEAN 0.0
|
||||
|
||||
// Controls the contrast/variance of noise.
|
||||
#define VARIANCE .3
|
||||
|
||||
float rand(vec2 co) {
|
||||
return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453);
|
||||
}
|
||||
float rand(vec2 co, float l) {
|
||||
return rand(vec2(rand(co), l));
|
||||
}
|
||||
float rand(vec2 co, float l, float t) {
|
||||
return rand(vec2(rand(co, l), t));
|
||||
}
|
||||
|
||||
vec3 channel_mix(vec3 a, vec3 b, vec3 w) {
|
||||
return vec3(mix(a.r, b.r, w.r), mix(a.g, b.g, w.g), mix(a.b, b.b, w.b));
|
||||
}
|
||||
|
||||
float gaussian(float z, float u, float o) {
|
||||
return (1.0 / (o * sqrt(2.0 * 3.1415))) * exp(-(((z - u) * (z - u)) / (2.0 * (o * o))));
|
||||
}
|
||||
|
||||
vec3 madd(vec3 a, vec3 b, float w) {
|
||||
return a + a * b * w;
|
||||
}
|
||||
|
||||
vec3 screen(vec3 a, vec3 b, float w) {
|
||||
return mix(a, vec3(1.0) - (vec3(1.0) - a) * (vec3(1.0) - b), w);
|
||||
}
|
||||
|
||||
vec3 overlay(vec3 a, vec3 b, float w) {
|
||||
return mix(a, channel_mix(2.0 * a * b, vec3(1.0) - 2.0 * (vec3(1.0) - a) * (vec3(1.0) - b), step(vec3(0.5), a)), w);
|
||||
}
|
||||
|
||||
vec3 soft_light(vec3 a, vec3 b, float w) {
|
||||
return mix(a, pow(a, pow(vec3(2.0), 2.0 * (vec3(0.5) - b))), w);
|
||||
}
|
||||
|
||||
void main() {
|
||||
float t = uTime * float(SPEED);
|
||||
|
||||
vec2 coord = gl_FragCoord.xy;
|
||||
vec2 ps = vec2(1.0) / gl_FragCoord.xy;
|
||||
vec2 uv = coord * ps;
|
||||
vec4 color = texture2D(tDiffuse, vUv);
|
||||
#if SRGB
|
||||
color = pow(color, vec4(2.2));
|
||||
#endif
|
||||
|
||||
float noise = gaussian(rand(ps, 1.0 / (color.r + color.g + color.b), t), float(MEAN), float(VARIANCE) * float(VARIANCE));
|
||||
|
||||
#if SHOW_NOISE
|
||||
color = vec4(noise);
|
||||
gl_FragColor = vec4(vec3(0.0), color.r);
|
||||
#else
|
||||
float w = float(INTENSITY);
|
||||
|
||||
vec3 grain = vec3(noise);//vec3(noise) * (1.0 - color.rgb);
|
||||
|
||||
#if BLEND_MODE == 0
|
||||
color.rgb += grain * w;
|
||||
#elif BLEND_MODE == 1
|
||||
color.rgb = screen(color.rgb, grain, w);
|
||||
#elif BLEND_MODE == 2
|
||||
color.rgb = overlay(color.rgb, grain, w);
|
||||
#elif BLEND_MODE == 3
|
||||
color.rgb = soft_light(color.rgb, grain, w);
|
||||
#elif BLEND_MODE == 4
|
||||
color.rgb = max(color.rgb, grain * w);
|
||||
#endif
|
||||
|
||||
#if SRGB
|
||||
color = pow(color, vec4(1.0 / 2.2));
|
||||
#endif
|
||||
|
||||
gl_FragColor = color;
|
||||
#endif
|
||||
|
||||
}
|
||||
46
components/gradients/grainPostProcessing.ts
Normal file
46
components/gradients/grainPostProcessing.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
|
||||
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
|
||||
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
|
||||
import { GrainShader } from './grainPostprocessingShader';
|
||||
|
||||
export function initGrainFilter(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
scene: THREE.Scene,
|
||||
camera: THREE.OrthographicCamera
|
||||
) {
|
||||
const width = window.innerWidth || 2;
|
||||
const height = window.innerHeight || 2;
|
||||
|
||||
const effectFilm = new ShaderPass(GrainShader);
|
||||
|
||||
const rtParameters = {
|
||||
stencilBuffer: true,
|
||||
};
|
||||
|
||||
const renderScenePass = new RenderPass(scene, camera);
|
||||
|
||||
let composer2 = new EffectComposer(
|
||||
renderer,
|
||||
new THREE.WebGLRenderTarget(width, height, rtParameters)
|
||||
);
|
||||
|
||||
composer2.setPixelRatio(window.devicePixelRatio);
|
||||
|
||||
composer2.addPass(renderScenePass);
|
||||
composer2.addPass(effectFilm);
|
||||
|
||||
return {
|
||||
render: (elapsed: number, delta: number) => {
|
||||
composer2.render(delta);
|
||||
|
||||
effectFilm.uniforms['uTime'].value = elapsed;
|
||||
},
|
||||
resize: () => {
|
||||
const viewportSize = renderer.getSize(new THREE.Vector2());
|
||||
|
||||
composer2.setSize(viewportSize.width, viewportSize.height);
|
||||
},
|
||||
};
|
||||
}
|
||||
30
components/gradients/grainPostprocessingShader.ts
Normal file
30
components/gradients/grainPostprocessingShader.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import grainFragmentShader from './grain.fragmentShader.glsl?raw';
|
||||
|
||||
/**
|
||||
* Full-screen textured quad shader
|
||||
*/
|
||||
|
||||
const GrainShader = {
|
||||
name: 'GrainShader',
|
||||
|
||||
uniforms: {
|
||||
tDiffuse: { value: null },
|
||||
opacity: { value: 1.0 },
|
||||
uTime: { value: 0 },
|
||||
},
|
||||
|
||||
vertexShader: /* glsl */ `
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
|
||||
}`,
|
||||
|
||||
fragmentShader: grainFragmentShader,
|
||||
};
|
||||
|
||||
export { GrainShader };
|
||||
1
components/gradients/index.tsx
Normal file
1
components/gradients/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './gradient';
|
||||
57
components/gradients/initGradients.ts
Normal file
57
components/gradients/initGradients.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import gradientFragmentShader from './gradient.fragmentShader.glsl?raw';
|
||||
import gradientVertexShader from './gradient.vertexShader.glsl?raw';
|
||||
|
||||
function getMaterial() {
|
||||
var uniforms = {
|
||||
uTime: {
|
||||
value: 0,
|
||||
},
|
||||
};
|
||||
var material = new THREE.ShaderMaterial({
|
||||
uniforms: uniforms,
|
||||
vertexShader: gradientVertexShader,
|
||||
fragmentShader: gradientFragmentShader,
|
||||
//wireframe: true,
|
||||
});
|
||||
|
||||
return material;
|
||||
}
|
||||
|
||||
export function initGradient(
|
||||
scene: THREE.Scene,
|
||||
renderer: THREE.WebGLRenderer
|
||||
) {
|
||||
const quality = 0.02;
|
||||
|
||||
const material = getMaterial();
|
||||
|
||||
const plane = new THREE.Mesh(new THREE.PlaneGeometry(), material);
|
||||
scene.add(plane);
|
||||
|
||||
const render = (elapsed: number, delta: number) => {
|
||||
material.uniforms.uTime.value = elapsed;
|
||||
};
|
||||
|
||||
const updateSize = () => {
|
||||
const viewportSize = renderer.getSize(new THREE.Vector2());
|
||||
plane.geometry.dispose();
|
||||
plane.geometry = new THREE.PlaneGeometry(
|
||||
viewportSize.width,
|
||||
viewportSize.height * 4,
|
||||
viewportSize.width * quality,
|
||||
viewportSize.height * 4 * quality
|
||||
);
|
||||
};
|
||||
|
||||
updateSize();
|
||||
|
||||
const unload = () => {
|
||||
scene.remove(plane);
|
||||
plane.geometry.dispose();
|
||||
material.dispose();
|
||||
};
|
||||
|
||||
return { render, updateSize, unload };
|
||||
}
|
||||
26
components/gradients/initScene.ts
Normal file
26
components/gradients/initScene.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export function initScene(renderer: THREE.WebGLRenderer) {
|
||||
const scene = new THREE.Scene();
|
||||
|
||||
scene.background = new THREE.Color(0xffffff);
|
||||
|
||||
const camera = new THREE.OrthographicCamera(-1, -1, -1, -1, -100000, 100000);
|
||||
camera.position.z = 5;
|
||||
camera.position.y = 8;
|
||||
camera.lookAt(scene.position);
|
||||
|
||||
const updateSize = () => {
|
||||
const viewportSize = renderer.getSize(new THREE.Vector2());
|
||||
|
||||
camera.left = viewportSize.width / -2;
|
||||
camera.right = viewportSize.width / 2;
|
||||
camera.top = viewportSize.height / 2;
|
||||
camera.bottom = -viewportSize.height / 2;
|
||||
camera.updateProjectionMatrix();
|
||||
};
|
||||
|
||||
updateSize();
|
||||
|
||||
return { scene, camera, updateSize };
|
||||
}
|
||||
19
components/gradients/initThree.ts
Normal file
19
components/gradients/initThree.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export type InitThreeOptions = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export function initThree(
|
||||
canvas: HTMLCanvasElement,
|
||||
{ width, height }: InitThreeOptions
|
||||
) {
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
|
||||
const clock = new THREE.Clock();
|
||||
|
||||
return { renderer, clock };
|
||||
}
|
||||
1
components/langSwitcher/index.tsx
Normal file
1
components/langSwitcher/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './langSwitcher';
|
||||
102
components/langSwitcher/langSwitcher.module.scss
Normal file
102
components/langSwitcher/langSwitcher.module.scss
Normal file
@@ -0,0 +1,102 @@
|
||||
.langSwitcherRoot {
|
||||
|
||||
}
|
||||
|
||||
.langSwitcher {
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.currentLanguage {
|
||||
border: none;
|
||||
background: none;
|
||||
font: inherit;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--on-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 18px;
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.selector {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: var(--background);
|
||||
padding: 8px 8px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--divider);
|
||||
box-shadow: 0px 4px 16px #00000014;
|
||||
background-color: #fafafa;
|
||||
z-index: 1000;
|
||||
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
|
||||
.header {
|
||||
color: #6f6f6f;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
margin: 4px 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
padding: 6px 16px;
|
||||
cursor: none !important;
|
||||
border-radius: 8px;
|
||||
transition: 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--divider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.openSelector {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.hideCurrent {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 570px) {
|
||||
.currentLangLabel {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 540px) {
|
||||
.selector {
|
||||
width: calc(100vw - 48px);
|
||||
right: -24px;
|
||||
box-shadow: 0px 4px 180px rgb(0 0 0 / 31%);
|
||||
|
||||
.header {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
99
components/langSwitcher/langSwitcher.tsx
Normal file
99
components/langSwitcher/langSwitcher.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import useLocale, { LocalesMap } from '@/utils/useLocale';
|
||||
import { clsx } from 'clsx';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import styles from './langSwitcher.module.scss';
|
||||
import LanguageIcon from '@/assets/icons/language.svg';
|
||||
import { CursorEffect } from '@/components/cursor';
|
||||
|
||||
type LangSwitcherProps = React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {};
|
||||
|
||||
const locales: LocalesMap = {
|
||||
ru: {
|
||||
chooseHeader: 'Выберите язык',
|
||||
},
|
||||
en: {
|
||||
chooseHeader: 'Choose language',
|
||||
},
|
||||
};
|
||||
|
||||
const localesMap: { [key: string]: string } = {
|
||||
en: 'English',
|
||||
ru: 'Русский',
|
||||
};
|
||||
|
||||
export function LangSwitcher(props: LangSwitcherProps) {
|
||||
const { className: restClassName, ...restProps } = props;
|
||||
const router = useRouter();
|
||||
const selectorRef = useRef(null);
|
||||
const buttonRef = useRef(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const t = useLocale(locales);
|
||||
const { pathname, asPath, query } = router;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const handleClose = (event: any) => {
|
||||
setOpen(
|
||||
event.composedPath().includes(selectorRef.current) ||
|
||||
event.composedPath().includes(buttonRef.current)
|
||||
);
|
||||
};
|
||||
|
||||
addEventListener('click', handleClose, false);
|
||||
|
||||
return () => removeEventListener('click', handleClose, false);
|
||||
}, [open]);
|
||||
|
||||
const handleSetLanguage = (locale: string) => () => {
|
||||
router.push({ pathname, query }, asPath, { locale });
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.langSwitcher, restClassName)} {...restProps}>
|
||||
<CursorEffect
|
||||
effectDistance={48}
|
||||
effectForce={4}
|
||||
cursorPadding={0}
|
||||
className={clsx(styles.langSwitcherRoot)}
|
||||
>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
className={clsx(styles.currentLanguage, open && styles.hideCurrent)}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<LanguageIcon />{' '}
|
||||
<span className={styles.currentLangLabel}>
|
||||
{localesMap[router.locale || 'en']}
|
||||
</span>
|
||||
</button>
|
||||
</CursorEffect>
|
||||
<div
|
||||
ref={selectorRef}
|
||||
className={clsx(styles.selector, open && styles.openSelector)}
|
||||
>
|
||||
<h5 className={styles.header}>{t('chooseHeader')}</h5>
|
||||
<ul>
|
||||
{Object.keys(localesMap).map((locale) => (
|
||||
<CursorEffect
|
||||
key={locale}
|
||||
effectForce={2}
|
||||
effectDistance={36}
|
||||
cursorPadding={2}
|
||||
cursorBorderRadius={8}
|
||||
>
|
||||
<button onClick={handleSetLanguage(locale)}>
|
||||
{localesMap[locale]}
|
||||
</button>
|
||||
</CursorEffect>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
components/logo/index.tsx
Normal file
1
components/logo/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './logo';
|
||||
31
components/logo/logo.module.css
Normal file
31
components/logo/logo.module.css
Normal file
@@ -0,0 +1,31 @@
|
||||
.logo {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 800;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.logo {
|
||||
gap: 8px;
|
||||
}
|
||||
.icon {
|
||||
width: 40px;
|
||||
}
|
||||
.title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
57
components/logo/logo.tsx
Normal file
57
components/logo/logo.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { clsx } from 'clsx';
|
||||
import styles from './logo.module.css';
|
||||
import LogoSVG from '@/assets/images/logo-alt.svg';
|
||||
import useLocale, { LocalesMap } from '@/utils/useLocale';
|
||||
|
||||
export enum LogoType {
|
||||
Full,
|
||||
Title,
|
||||
Icon,
|
||||
}
|
||||
export enum Orientation {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
type LogoProps = React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {
|
||||
orientation?: Orientation;
|
||||
variant?: LogoType;
|
||||
};
|
||||
|
||||
const localesMap: LocalesMap = {
|
||||
ru: {
|
||||
title: 'ridebus',
|
||||
},
|
||||
en: {
|
||||
title: 'ridebus',
|
||||
},
|
||||
};
|
||||
|
||||
export function Logo(props: LogoProps) {
|
||||
const {
|
||||
orientation = Orientation.Horizontal,
|
||||
variant = LogoType.Full,
|
||||
className: restClassName,
|
||||
...restProps
|
||||
} = props;
|
||||
const t = useLocale(localesMap);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
restClassName,
|
||||
styles.logo,
|
||||
orientation === Orientation.Vertical && styles.vertical
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{variant !== LogoType.Title && <LogoSVG className={styles.icon} />}
|
||||
{variant !== LogoType.Icon && (
|
||||
<h1 className={styles.title}>{t('title')}</h1>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
components/topBar/index.tsx
Normal file
1
components/topBar/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './topBar';
|
||||
53
components/topBar/topBar.module.scss
Normal file
53
components/topBar/topBar.module.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
.root {
|
||||
position: absolute;
|
||||
max-width: 1400px;
|
||||
left: 0;
|
||||
top: 100px;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 0 48px;
|
||||
gap: 0px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.homeLink {
|
||||
padding: 8px;
|
||||
padding-right: 24px;
|
||||
border-radius: 20px;
|
||||
display: block;
|
||||
margin-right: auto;
|
||||
margin-left: -8px;
|
||||
cursor: none !important;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1100px) {
|
||||
.root {
|
||||
top: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.googlePlayLink {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.root {
|
||||
top: 30px;
|
||||
}
|
||||
|
||||
.topBar {
|
||||
top: 20px;
|
||||
padding: 0 36px;
|
||||
gap: 36px;
|
||||
}
|
||||
}
|
||||
52
components/topBar/topBar.tsx
Normal file
52
components/topBar/topBar.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import styles from './topBar.module.scss';
|
||||
import { LangSwitcher } from '@/components/langSwitcher';
|
||||
import { Logo, LogoType } from '@/components/logo';
|
||||
import { GithubLink } from '@/components/githubLink';
|
||||
import { GooglePlayLink } from '@/components/googlePlayLink';
|
||||
import { useRef } from 'react';
|
||||
import useScroll from '@/utils/useScroll';
|
||||
import { CursorEffect } from '@/components/cursor';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
type TopBarProps = {
|
||||
notHide?: boolean;
|
||||
};
|
||||
|
||||
export function TopBar(props: TopBarProps) {
|
||||
const { notHide = false } = props;
|
||||
const router = useRouter();
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useScroll((scrollY, deltaTime, time) => {
|
||||
if (notHide) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rootRef.current) {
|
||||
rootRef.current.style.transform = `translate3D(0, ${Math.max(-Math.expm1(scrollY / 6), -400)}px, 1000px)`;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.root} ref={rootRef}>
|
||||
{router.pathname === '/' && (
|
||||
<Logo className={styles.logo} variant={LogoType.Full} />
|
||||
)}
|
||||
{router.pathname !== '/' && (
|
||||
<CursorEffect
|
||||
cursorPadding={4}
|
||||
cursorBorderRadius={26}
|
||||
className={styles.logo}
|
||||
>
|
||||
<Link href="/" className={styles.homeLink}>
|
||||
<Logo variant={LogoType.Full} />
|
||||
</Link>
|
||||
</CursorEffect>
|
||||
)}
|
||||
<GooglePlayLink className={styles.googlePlayLink} />
|
||||
<GithubLink />
|
||||
<LangSwitcher />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user