Skip to content

Commit

Permalink
Merge pull request #13 from Zachdidit/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
Zachdidit authored Oct 4, 2024
2 parents 664721a + 761645c commit 19283a7
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 36 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@

13 changes: 7 additions & 6 deletions src/app/components/ImageCards.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,14 @@
}
}

.space {
height: 90vh;
}
}

.space--small {
height: 40vh;
}
.space {
height: 50vh;
}

.spacesmall {
height: 20vh;
}

@media (max-width: 600px) {
Expand Down
56 changes: 46 additions & 10 deletions src/app/components/ImageCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,12 @@ import React, {
useRef,
useEffect,
RefObject,
useMemo,
createRef,
} from "react";
import { Heading, Text } from "@/once-ui/components";
import Image from "next/image";
import { Heading, Text, Flex } from "@/once-ui/components";
import imageCardsStyle from "./ImageCards.module.scss";

import { Card } from "../types";
import { inherits } from "util";

import { ScrollObserver, valueAtPercentage } from '@/app/lib/aat';

const sizeMap: Record<string, string> = {
xs: "var(--static-space-16)",
Expand All @@ -30,6 +27,7 @@ type ImageCardsProps = {
const cardOffset: number = 20;
const cardScale: number = 1;


const useCardEffect = (
cardsContainerRef: RefObject<HTMLDivElement>,
cardsRef: RefObject<HTMLDivElement[]>,
Expand Down Expand Up @@ -57,14 +55,41 @@ const useCardEffect = (
);

cards?.forEach((card, index) => {
card.style.paddingTop = `${cardOffset + index * cardOffset}px`;
const offsetTop = cardOffset + cardOffset * cardOffset;
card.style.paddingTop = `${cardOffset + index * cardOffset}px`;
if (index === cards.length - 1) return;
const toScale = cardScale - (cards.length - 1 - index) * 0.1;
const nextCard = cards[index + 1];
const cardInner = card.querySelector(".inner");
const cardInner = card.children[0] as HTMLElement;

new ScrollObserver();
ScrollObserver.Element(nextCard, {
offsetTop: 0,
offsetBottom: window.innerHeight - card.clientHeight,
offsetLeft: 0,
offsetRight: 0,
addWrapper: false,
wrapperClass: '',
container: document.documentElement

}).onScroll(({ percentageY }) => {
if(cardInner === null) return;
if(cardInner.style === null) return;
cardInner.style.scale = valueAtPercentage({
from: 1,
to: toScale,
percentage: percentageY
}).toString();
cardInner.style.filter = `brightness(${valueAtPercentage({
from: 1,
to: 0.6,
percentage: percentageY
}).toString()})`;
});

});
return () => {};
}, [cardsContainerRef, cardsRef]);
}, [cardsContainerRef, cardsRef]); // Work here to ensure useEffect runs ONCE
};
const ImageCards = forwardRef<HTMLDivElement, ImageCardsProps>(
({ cards }, ref) => {
Expand All @@ -73,7 +98,16 @@ const ImageCards = forwardRef<HTMLDivElement, ImageCardsProps>(

useCardEffect(cardContainerRef, cardsRefsById);
return (
<div className={imageCardsStyle.cards} ref={cardContainerRef}>
<Flex
position="relative"
as="section"
overflow="hidden"
fillWidth
minHeight="0"
direction="column"
alignItems="center">
<div className={`${imageCardsStyle.space} ${imageCardsStyle.spacesmall}`}></div>
<div className={imageCardsStyle.cards} ref={cardContainerRef}>
{cards.map((card, i) => (
<div
key={i}
Expand Down Expand Up @@ -104,6 +138,8 @@ const ImageCards = forwardRef<HTMLDivElement, ImageCardsProps>(
</div>
))}
</div>
<div className={imageCardsStyle.space}></div>
</Flex>
);
},
);
Expand Down
192 changes: 192 additions & 0 deletions src/app/lib/aat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { clamp } from './helpers';
export { valueAtPercentage } from './helpers';


interface ScrollObserverOptions {
offsetBottom?: number | (() => number);
offsetTop?: number | (() => number);
offsetRight?: number | (() => number);
offsetLeft?: number | (() => number);
addWrapper?: boolean;
wrapperClass?: string;
container?: HTMLElement | Window;
}

interface ScrollHandlerParams {
percentageY: number;
percentageX: number;
}

const defaultOptions: ScrollObserverOptions = {
offsetBottom: 0,
offsetTop: 0,
offsetRight: 0,
offsetLeft: 0,
addWrapper: false,
wrapperClass: ''
};

export class ScrollObserver {
protected _handler?: (params: ScrollHandlerParams) => void;

static Container(container: HTMLElement | Window): ContainerScrollObserver {
return new ContainerScrollObserver(container);
}

static Element(element: HTMLElement, options: Partial<ScrollObserverOptions>): ElementScrollObserver {
return new ElementScrollObserver(element, { ...defaultOptions, ...options });
}

public onScroll(handler: (params: ScrollHandlerParams) => void): void {
this._handler = handler;
this._onScroll();
}

protected _onScroll(): void {
// This method is implemented in subclasses.
}
}

class ContainerScrollObserver extends ScrollObserver {
private readonly _container: HTMLElement | Window;

constructor(container: HTMLElement | Window) {
super();
this._container = container;
const scrollElement = container === document.documentElement ? window : container;
scrollElement.addEventListener('scroll', this._onScroll.bind(this));
}

protected _onScroll(): void {
const currentScrollY = (this._container as HTMLElement).scrollTop;
const totalScrollY = (this._container as HTMLElement).scrollHeight - (this._container as HTMLElement).clientHeight;
const percentageY = clamp(currentScrollY / totalScrollY, 0, 1) || 0;

const currentScrollX = (this._container as HTMLElement).scrollLeft;
const totalScrollX = (this._container as HTMLElement).scrollWidth - (this._container as HTMLElement).clientWidth;
const percentageX = clamp(currentScrollX / totalScrollX, 0, 1) || 0;

if (this._handler && typeof this._handler === 'function') {
requestAnimationFrame(() => this._handler!({ percentageY, percentageX }));
}
}
}

class ElementScrollObserver extends ScrollObserver {
private readonly _element: HTMLElement;
private readonly _options: ScrollObserverOptions;
private _wrapper?: HTMLElement;
private _lastPercentageY: number | null = null;
private _lastPercentageX: number | null = null;

constructor(element: HTMLElement, options: ScrollObserverOptions) {
super();
this._element = element;
this._options = options;

if (this._options.addWrapper) {
this._addWrapper();
}

const scrollContainer = this._options.container === document.documentElement ? window : this._options.container;
scrollContainer?.addEventListener('scroll', this._onScroll.bind(this));
requestAnimationFrame(() => this._onScroll());
}

private _addWrapper(): void {
this._wrapper = document.createElement('div');
if (this._options.wrapperClass) {
this._wrapper.classList.add(this._options.wrapperClass);
}
this._element.parentNode!.insertBefore(this._wrapper, this._element);
this._wrapper.appendChild(this._element);
}

private get _containerClientHeight(): number {
return this._options.container === window
? window.innerHeight
: (this._options.container as HTMLElement).clientHeight;
}

private get _containerClientWidth(): number {
return this._options.container === window
? window.innerWidth
: (this._options.container as HTMLElement).clientWidth;
}

private get _elRectRelativeToContainer(): DOMRect {
const element = this._options.addWrapper ? this._wrapper! : this._element;
const rect: DOMRect = element.getBoundingClientRect();
if (this._options.container === document.documentElement) {
return rect;
}
const containerRect = (this._options.container as HTMLElement).getBoundingClientRect();
return {
width: rect.width,
height: rect.height,
left: rect.left - containerRect.left,
top: rect.top - containerRect.top,
right: rect.right - containerRect.right,
bottom: rect.bottom - containerRect.bottom,
x: rect.x,
y: rect.y,
toJSON: rect.toJSON.bind(rect)
};
}

private getOffsetValue(side: 'Top' | 'Bottom' | 'Left' | 'Right'): number {
const key = `offset${side}` as keyof ScrollObserverOptions;
const value = this._options[key];
return typeof value === 'function' ? (value as () => number)() : (value as number);
}

private get _offsetBottom(): number {
return this.getOffsetValue('Bottom');
}

private get _offsetTop(): number {
return this.getOffsetValue('Top');
}

private get _offsetLeft(): number {
return this.getOffsetValue('Left');
}

private get _offsetRight(): number {
return this.getOffsetValue('Right');
}

private _calculatePercentageY(): number {
const rect = this._elRectRelativeToContainer;
const startPoint = this._containerClientHeight - this._offsetBottom;
const endPoint = this._offsetTop;

const viewHeight = startPoint - endPoint;

return clamp((startPoint - rect.top) / viewHeight, 0, 1);
}

private _calculatePercentageX(): number {
const rect = this._elRectRelativeToContainer;
const startPoint = this._containerClientWidth - this._offsetRight;
const endPoint = this._offsetLeft;

const viewWidth = startPoint - endPoint;

return clamp((startPoint - rect.left) / viewWidth, 0, 1);
}

protected _onScroll(): void {
const percentageY = this._calculatePercentageY();
const percentageX = this._calculatePercentageX();
if (
this._handler &&
typeof this._handler === 'function' &&
(this._lastPercentageY !== percentageY || this._lastPercentageX !== percentageX)
) {
requestAnimationFrame(() => this._handler!({ percentageY, percentageX }));
}
this._lastPercentageY = percentageY;
this._lastPercentageX = percentageX;
}
}
21 changes: 21 additions & 0 deletions src/app/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// TypeScript version of clamp
export function clamp(num: number, min: number, max: number): number {
return Math.min(Math.max(num, min), max);
}

// TypeScript version of valueAtPercentage
interface ValueAtPercentageParams {
from: number;
to: number;
percentage: number;
unit?: string; // Optional unit parameter
}

export function valueAtPercentage({
from,
to,
percentage,
unit = ''
}: ValueAtPercentageParams): string | number {
return from + (to - from) * percentage + unit;
}
Empty file added src/app/lib/utils.ts
Empty file.
9 changes: 2 additions & 7 deletions src/app/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ export default function Navbar() {
title: "Home",
description: "",
},
{
href: "/projects",
title: "Projects",
description: "What I've worked on",
},
{
href: "mailto:[email protected]",
title: "Hire Me",
Expand All @@ -32,14 +27,14 @@ export default function Navbar() {
overflow="hidden"
fillWidth
direction="row"
alignItems="start"
alignItems="center"
flex={1}
>
<Grid
radius="l"
border="neutral-medium"
borderStyle="solid-1"
columns="repeat(4, 1fr)"
columns="repeat(3, 1fr)"
tabletColumns="1col"
mobileColumns="1col"
fillWidth
Expand Down
Loading

0 comments on commit 19283a7

Please sign in to comment.