From 97953bdbe1dbc4c39cf7971683ee9b8930b9b1f2 Mon Sep 17 00:00:00 2001 From: Damien Schneider <74979845+damien-schneider@users.noreply.github.com> Date: Tue, 7 Jan 2025 23:13:18 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20Font=20Effects=20category=20w?= =?UTF-8?q?ith=20Variable=20Font=20Cursor=20component=20and=20preview=20(#?= =?UTF-8?q?75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/categories-previews-list.ts | 2 + packages/ui/cuicui/hooks/use-mouse/index.ts | 19 +- .../ui/cuicui/other/font-effects/category.ts | 12 ++ .../ui/cuicui/other/font-effects/preview.tsx | 20 ++ .../variable-font-cursor/component.ts | 12 ++ .../variable-font-cursor.variant.tsx | 40 ++++ .../variable-font-proximity.tsx | 182 ++++++++++++++++++ packages/ui/section-list.ts | 23 +++ 8 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 packages/ui/cuicui/other/font-effects/category.ts create mode 100644 packages/ui/cuicui/other/font-effects/preview.tsx create mode 100644 packages/ui/cuicui/other/font-effects/variable-font-cursor/component.ts create mode 100644 packages/ui/cuicui/other/font-effects/variable-font-cursor/variable-font-cursor.variant.tsx create mode 100644 packages/ui/cuicui/other/font-effects/variable-font-cursor/variable-font-proximity.tsx diff --git a/packages/ui/categories-previews-list.ts b/packages/ui/categories-previews-list.ts index c160801..dae2963 100644 --- a/packages/ui/categories-previews-list.ts +++ b/packages/ui/categories-previews-list.ts @@ -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"; @@ -88,6 +89,7 @@ export const categoriesPreviewsList: Record 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, diff --git a/packages/ui/cuicui/hooks/use-mouse/index.ts b/packages/ui/cuicui/hooks/use-mouse/index.ts index 540d065..c641170 100644 --- a/packages/ui/cuicui/hooks/use-mouse/index.ts +++ b/packages/ui/cuicui/hooks/use-mouse/index.ts @@ -10,7 +10,9 @@ interface MouseState { elementPositionY: number | null; } -export function useMouse(): [MouseState, RefObject] { +export function useMouse( + containerRef?: RefObject, +): [MouseState, RefObject] { const [state, setState] = useState({ x: null, y: null, @@ -29,7 +31,18 @@ export function useMouse(): [MouseState, RefObject] { 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; @@ -53,7 +66,7 @@ export function useMouse(): [MouseState, RefObject] { return () => { document.removeEventListener("mousemove", handleMouseMove); }; - }, []); + }, [containerRef]); return [state, ref]; } diff --git a/packages/ui/cuicui/other/font-effects/category.ts b/packages/ui/cuicui/other/font-effects/category.ts new file mode 100644 index 0000000..a5369c3 --- /dev/null +++ b/packages/ui/cuicui/other/font-effects/category.ts @@ -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; diff --git a/packages/ui/cuicui/other/font-effects/preview.tsx b/packages/ui/cuicui/other/font-effects/preview.tsx new file mode 100644 index 0000000..c5ba59e --- /dev/null +++ b/packages/ui/cuicui/other/font-effects/preview.tsx @@ -0,0 +1,20 @@ +export default function () { + return ( +
+ {/* 3 characters in

*/} +

+ c + u + i + c + u + i +

+ + {/* Variable font preview bars */} +
+
+
+
+ ); +} diff --git a/packages/ui/cuicui/other/font-effects/variable-font-cursor/component.ts b/packages/ui/cuicui/other/font-effects/variable-font-cursor/component.ts new file mode 100644 index 0000000..6d8d85e --- /dev/null +++ b/packages/ui/cuicui/other/font-effects/variable-font-cursor/component.ts @@ -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; diff --git a/packages/ui/cuicui/other/font-effects/variable-font-cursor/variable-font-cursor.variant.tsx b/packages/ui/cuicui/other/font-effects/variable-font-cursor/variable-font-cursor.variant.tsx new file mode 100644 index 0000000..59908f4 --- /dev/null +++ b/packages/ui/cuicui/other/font-effects/variable-font-cursor/variable-font-cursor.variant.tsx @@ -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(null); + + return ( +
+ + This is + + + inspired by + + + Fancy components + +
+ ); +} + +export default PreviewVariableFontCursor; diff --git a/packages/ui/cuicui/other/font-effects/variable-font-cursor/variable-font-proximity.tsx b/packages/ui/cuicui/other/font-effects/variable-font-cursor/variable-font-proximity.tsx new file mode 100644 index 0000000..467e795 --- /dev/null +++ b/packages/ui/cuicui/other/font-effects/variable-font-cursor/variable-font-proximity.tsx @@ -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; + 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([]); + + 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 ( + + {words.map((word, wordIndex) => ( + + key={`${wordIndex}-letter-effect`} + className="inline-block whitespace-nowrap" + > + {word.split("").map((letter) => { + const currentLetterIndex = letterIndex++; + return ( + { + letterRefs.current[currentLetterIndex] = el; + }} + className="inline-block" + aria-hidden="true" + style={{ + fontVariationSettings: + interpolatedSettingsRef.current[currentLetterIndex], + }} + > + {letter} + + ); + })} + {wordIndex < words.length - 1 && ( +   + )} + + ))} + {children} + + ); +}; + +VariableFontCursorProximity.displayName = "VariableFontCursorProximity"; +export default VariableFontCursorProximity; diff --git a/packages/ui/section-list.ts b/packages/ui/section-list.ts index 17fce14..71d5810 100644 --- a/packages/ui/section-list.ts +++ b/packages/ui/section-list.ts @@ -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"; @@ -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"; @@ -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"; @@ -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",