Skip to content

Commit

Permalink
✨ Add Font Effects category with Variable Font Cursor component and p…
Browse files Browse the repository at this point in the history
…review (#75)
  • Loading branch information
damien-schneider authored Jan 7, 2025
1 parent dda443c commit 97953bd
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 3 deletions.
2 changes: 2 additions & 0 deletions packages/ui/categories-previews-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import other_patterns_preview from "./cuicui/other/patterns/preview";
import other_equalizer_preview from "./cuicui/other/equalizer/preview";
import other_qr_code_preview from "./cuicui/other/qr-code/preview";
import other_transition_wrappers_preview from "./cuicui/other/transition-wrappers/preview";
import other_font_effects_preview from "./cuicui/other/font-effects/preview";
import other_cursors_preview from "./cuicui/other/cursors/preview";
import utils_catch_error_preview from "./cuicui/utils/catch-error/preview";
import utils_sleep_preview from "./cuicui/utils/sleep/preview";
Expand Down Expand Up @@ -88,6 +89,7 @@ export const categoriesPreviewsList: Record<string, () => JSX.Element> = {
equalizer: other_equalizer_preview,
"qr-code": other_qr_code_preview,
"transition-wrappers": other_transition_wrappers_preview,
"font-effects": other_font_effects_preview,
cursors: other_cursors_preview,
"catch-error": utils_catch_error_preview,
sleep: utils_sleep_preview,
Expand Down
19 changes: 16 additions & 3 deletions packages/ui/cuicui/hooks/use-mouse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ interface MouseState {
elementPositionY: number | null;
}

export function useMouse(): [MouseState, RefObject<HTMLDivElement | null>] {
export function useMouse(
containerRef?: RefObject<HTMLElement | SVGElement | null>,
): [MouseState, RefObject<HTMLDivElement | null>] {
const [state, setState] = useState<MouseState>({
x: null,
y: null,
Expand All @@ -29,7 +31,18 @@ export function useMouse(): [MouseState, RefObject<HTMLDivElement | null>] {
y: event.pageY,
};

if (ref.current instanceof Element) {
if (containerRef?.current instanceof Element) {
const { left, top } = containerRef.current.getBoundingClientRect();
const containerPositionX = left + window.scrollX;
const containerPositionY = top + window.scrollY;
const containerX = event.pageX - containerPositionX;
const containerY = event.pageY - containerPositionY;

newState.elementX = containerX;
newState.elementY = containerY;
newState.elementPositionX = containerPositionX;
newState.elementPositionY = containerPositionY;
} else if (ref.current instanceof Element) {
const { left, top } = ref.current.getBoundingClientRect();
const elementPositionX = left + window.scrollX;
const elementPositionY = top + window.scrollY;
Expand All @@ -53,7 +66,7 @@ export function useMouse(): [MouseState, RefObject<HTMLDivElement | null>] {
return () => {
document.removeEventListener("mousemove", handleMouseMove);
};
}, []);
}, [containerRef]);

return [state, ref];
}
12 changes: 12 additions & 0 deletions packages/ui/cuicui/other/font-effects/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { CategoryMetaType } from "@/lib/types/component";
import { CaseSensitiveIcon } from "lucide-react";

export const Category: CategoryMetaType = {
name: "Font Effects",
description:
"An all bunch of creative effects that can be used in any project with any artisitic style",
latestUpdateDate: new Date("2025-01-05"),
icon: CaseSensitiveIcon,
};

export default Category;
20 changes: 20 additions & 0 deletions packages/ui/cuicui/other/font-effects/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default function () {
return (
<div className="w-fit p-4 bg-neutral-400/15 rounded-xl flex flex-col justify-center items-center space-y-2">
{/* 3 characters in <p> */}
<p className="inline text-neutral-500/70 text-lg font-medium tracking-wide">
<span className="text-xl">c</span>
<span className="text-2xl">u</span>
<span className="text-3xl">i</span>
<span className="text-xl">c</span>
<span className="text-2xl">u</span>
<span className="text-3xl">i</span>
</p>

{/* Variable font preview bars */}
<div className="w-32 h-1 bg-neutral-400/40 rounded-full" />
<div className="w-32 h-2 bg-neutral-400/40 rounded-full" />
<div className="w-32 h-3 bg-neutral-400/40 rounded-full" />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ComponentMetaType } from "@/lib/types/component";

export const Component: ComponentMetaType = {
name: "Variable Font Cursor",
description:
"A cursor that changes its font size based on the cursor position",
inspiration: "Fancy Components",
inspirationLink:
"https://www.fancycomponents.dev/docs/components/text/variable-font-cursor-proximity",
};

export default Component;
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use client";
import { useRef } from "react";

import { cn } from "@/cuicui/utils/cn";
import { VariableFontCursorProximity } from "@/cuicui/other/font-effects/variable-font-cursor/variable-font-proximity";

export function PreviewVariableFontCursor() {
const containerRef = useRef<HTMLDivElement>(null);

return (
<div
className="dark:text-neutral-50 text-neutral-700 *:block"
ref={containerRef}
>
<VariableFontCursorProximity
className={cn("text-2xl md:text-3xl lg:text-4xl")}
radiusZoomingZone={200}
containerRef={containerRef}
>
This is
</VariableFontCursorProximity>
<VariableFontCursorProximity
className={cn("text-3xl md:text-4xl lg:text-5xl")}
radiusZoomingZone={200}
containerRef={containerRef}
>
inspired by
</VariableFontCursorProximity>
<VariableFontCursorProximity
className={cn("text-4xl md:text-5xl lg:text-6xl")}
radiusZoomingZone={200}
containerRef={containerRef}
>
Fancy components
</VariableFontCursorProximity>
</div>
);
}

export default PreviewVariableFontCursor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
"use client";
import { type ComponentProps, type RefObject, useMemo, useRef } from "react";
import { motion, useAnimationFrame } from "motion/react";
import { useMouse } from "@/cuicui/hooks/use-mouse";

interface TextProps {
children: string;
fromFontVariationSettings?: string;
toFontVariationSettings?: string;
containerRef: RefObject<HTMLDivElement | null>;
radiusZoomingZone?: number;
falloff?: "linear" | "exponential" | "gaussian";
}

export const VariableFontCursorProximity = ({
children,
fromFontVariationSettings = "'wght' 400, 'slnt' 0",
toFontVariationSettings = "'wght' 900, 'slnt' -10",
containerRef,
radiusZoomingZone = 50,
falloff = "linear",
className,
onClick,
ref,
...props
}: TextProps & ComponentProps<"span">) => {
const letterRefs = useRef<(HTMLSpanElement | null)[]>([]);
const interpolatedSettingsRef = useRef<string[]>([]);

const [mousePosition, _] = useMouse(containerRef);

// Parse the font variation settings strings. see the docs or the demo on how one should look like
const parsedSettings = useMemo(() => {
const fromSettings = new Map(
fromFontVariationSettings
.split(",")
.map((s) => s.trim())
.map((s) => {
const [name, value] = s.split(" ");
return [name.replace(/['"]/g, ""), Number.parseFloat(value)];
}),
);

const toSettings = new Map(
toFontVariationSettings
.split(",")
.map((s) => s.trim())
.map((s) => {
const [name, value] = s.split(" ");
return [name.replace(/['"]/g, ""), Number.parseFloat(value)];
}),
);

return Array.from(fromSettings.entries()).map(([axis, fromValue]) => ({
axis,
fromValue,
toValue: toSettings.get(axis) ?? fromValue,
}));
}, [fromFontVariationSettings, toFontVariationSettings]);

const calculateDistance = (
x1: number,
y1: number,
x2: number,
y2: number,
): number => {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
};

const calculateFalloff = (distance: number): number => {
const normalizedDistance = Math.min(
Math.max(1 - distance / radiusZoomingZone, 0),
1,
);

switch (falloff) {
case "exponential":
return normalizedDistance ** 2;
case "gaussian":
return Math.exp(-((distance / (radiusZoomingZone / 2)) ** 2) / 2);
// case "linear":
default:
return normalizedDistance;
}
};

useAnimationFrame(() => {
if (!containerRef.current) {
return;
}
const containerRect = containerRef.current.getBoundingClientRect();

letterRefs.current.forEach((letterRef, index) => {
if (!(mousePosition.elementX && mousePosition.elementY)) {
return;
}
if (!letterRef) {
return;
}

const rect = letterRef.getBoundingClientRect();
const letterCenterX = rect.left + rect.width / 2 - containerRect.left;
const letterCenterY = rect.top + rect.height / 2 - containerRect.top;

const distance = calculateDistance(
mousePosition.elementX,
mousePosition.elementY,
letterCenterX,
letterCenterY,
);

if (distance >= radiusZoomingZone) {
if (
letterRef.style.fontVariationSettings !== fromFontVariationSettings
) {
letterRef.style.fontVariationSettings = fromFontVariationSettings;
}
return;
}

const falloffValue = calculateFalloff(distance);

const newSettings = parsedSettings
.map(({ axis, fromValue, toValue }) => {
const interpolatedValue =
fromValue + (toValue - fromValue) * falloffValue;
return `'${axis}' ${interpolatedValue}`;
})
.join(", ");

interpolatedSettingsRef.current[index] = newSettings;
letterRef.style.fontVariationSettings = newSettings;
});
});

const words = children.split(" ");
let letterIndex = 0;

return (
<span
ref={ref}
className={`${className} inline`}
onClick={onClick}
{...props}
>
{words.map((word, wordIndex) => (
<span
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
key={`${wordIndex}-letter-effect`}
className="inline-block whitespace-nowrap"
>
{word.split("").map((letter) => {
const currentLetterIndex = letterIndex++;
return (
<motion.span
key={currentLetterIndex}
ref={(el: HTMLSpanElement | null) => {
letterRefs.current[currentLetterIndex] = el;
}}
className="inline-block"
aria-hidden="true"
style={{
fontVariationSettings:
interpolatedSettingsRef.current[currentLetterIndex],
}}
>
{letter}
</motion.span>
);
})}
{wordIndex < words.length - 1 && (
<span className="inline-block">&nbsp;</span>
)}
</span>
))}
<span className="sr-only">{children}</span>
</span>
);
};

VariableFontCursorProximity.displayName = "VariableFontCursorProximity";
export default VariableFontCursorProximity;
23 changes: 23 additions & 0 deletions packages/ui/section-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import marketing_ui_testimonials_category from "@/cuicui/marketing-ui/testimonia
import other_creative_effects_category from "@/cuicui/other/creative-effects/category";
import other_cursors_category from "@/cuicui/other/cursors/category";
import other_equalizer_category from "@/cuicui/other/equalizer/category";
import other_font_effects_category from "@/cuicui/other/font-effects/category";
import other_mock_ups_category from "@/cuicui/other/mock-ups/category";
import other_patterns_category from "@/cuicui/other/patterns/category";
import other_qr_code_category from "@/cuicui/other/qr-code/category";
Expand Down Expand Up @@ -193,6 +194,7 @@ import other_creative_effects_wavy_line_component from "@/cuicui/other/creative-
import other_cursors_dynamic_cards_component from "@/cuicui/other/cursors/dynamic-cards/component";
import other_cursors_follow_cursor_component from "@/cuicui/other/cursors/follow-cursor/component";
import other_equalizer_equalizer_component from "@/cuicui/other/equalizer/equalizer/component";
import other_font_effects_variable_font_cursor_component from "@/cuicui/other/font-effects/variable-font-cursor/component";
import other_mock_ups_airpods_component from "@/cuicui/other/mock-ups/airpods/component";
import other_mock_ups_laptops_component from "@/cuicui/other/mock-ups/laptops/component";
import other_mock_ups_smartphone_component from "@/cuicui/other/mock-ups/smartphone/component";
Expand Down Expand Up @@ -340,6 +342,7 @@ import other_cursors_dynamic_cards_only_border_card_effect_variant from "@/cuicu
import other_cursors_follow_cursor_replace_cursor_variant from "@/cuicui/other/cursors/follow-cursor/replace-cursor.variant";
import other_cursors_follow_cursor_with_cursor_variant from "@/cuicui/other/cursors/follow-cursor/with-cursor.variant";
import other_equalizer_equalizer_equalizer_variant from "@/cuicui/other/equalizer/equalizer/equalizer.variant";
import other_font_effects_variable_font_cursor_variable_font_cursor_variant from "@/cuicui/other/font-effects/variable-font-cursor/variable-font-cursor.variant";
import other_mock_ups_airpods_airpods_pro_variant from "@/cuicui/other/mock-ups/airpods/airpods-pro.variant";
import other_mock_ups_airpods_airpods_variant from "@/cuicui/other/mock-ups/airpods/airpods.variant";
import other_mock_ups_laptops_mackbook_variant from "@/cuicui/other/mock-ups/laptops/mackbook.variant";
Expand Down Expand Up @@ -2505,6 +2508,26 @@ export const sectionList: SectionType[] = [
},
],
},
{
meta: other_font_effects_category,
slug: "font-effects",
components: [
{
meta: other_font_effects_variable_font_cursor_component,
slug: "variable-font-cursor",
variants: [
{
name: "variable-font-cursor",
variantComponent:
other_font_effects_variable_font_cursor_variable_font_cursor_variant,
slug: "variable-font-cursor",
pathname:
"cuicui/other/font-effects/variable-font-cursor/variable-font-cursor.variant.tsx",
},
],
},
],
},
{
meta: other_mock_ups_category,
slug: "mock-ups",
Expand Down

0 comments on commit 97953bd

Please sign in to comment.