-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #13 from Zachdidit/develop
Develop
- Loading branch information
Showing
8 changed files
with
317 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +0,0 @@ | ||
|
||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
@@ -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 | ||
|
Oops, something went wrong.