Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add Font Effects category with Variable Font Cursor component and p… #75

Merged
merged 1 commit into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading