加载中…
创作者 · Aceternity UI
查看原始来源 ↗
Prompt
// components/ui/canvas-text.tsx
"use client";
import { cn } from "@/lib/utils";
import React, { useEffect, useRef, useState, useCallback } from "react";
interface CanvasTextProps {
text: string;
className?: string;
backgroundClassName?: string;
colors?: string[];
animationDuration?: number;
lineWidth?: number;
lineGap?: number;
curveIntensity?: number;
overlay?: boolean;
}
function resolveColor(color: string): string {
if (color.startsWith("var(")) {
const varName = color.slice(4, -1).trim();
const resolved = getComputedStyle(document.documentElement)
.getPropertyValue(varName)
.trim();
return resolved || color;
}
return color;
}
export function CanvasText({
text,
className = "",
backgroundClassName = "bg-white dark:bg-neutral-950",
colors = ["#ff6b6b", "#4ecdc4", "#45b7d1", "#96ceb4", "#ffeaa7", "#dfe6e9"],
animationDuration = 5,
lineWidth = 1.5,
lineGap = 10,
curveIntensity = 60,
overlay = false,
}: CanvasTextProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const textRef = useRef<HTMLSpanElement>(null);
const bgRef = useRef<HTMLSpanElement>(null);
const animationRef = useRef<number>(0);
const startTimeRef = useRef<number>(0);
const [bgColor, setBgColor] = useState("#0a0a0a");
const [resolvedColors, setResolvedColors] = useState<string[]>([]);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [font, setFont] = useState("");
const updateColors = useCallback(() => {
if (bgRef.current) {
const computed = window.getComputedStyle(bgRef.current);
setBgColor(computed.backgroundColor);
}
const resolved = colors.map(resolveColor);
setResolvedColors(resolved);
}, [colors]);
useEffect(() => {
updateColors();
const observer = new MutationObserver(updateColors);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
}, [updateColors]);
useEffect(() => {
const textEl = textRef.current;
if (!textEl) return;
const updateDimensions = () => {
const rect = textEl.getBoundingClientRect();
const computed = window.getComputedStyle(textEl);
setDimensions({
width: Math.ceil(rect.width) || 400,
height: Math.ceil(rect.height) || 200,
});
setFont(
`${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`,
);
};
updateDimensions();
const resizeObserver = new ResizeObserver(updateDimensions);
resizeObserver.observe(textEl);
return () => resizeObserver.disconnect();
}, [text, className]);
useEffect(() => {
const canvas = canvasRef.current;
if (
!canvas ||
resolvedColors.length === 0 ||
dimensions.width === 0 ||
!font
)
return;
const ctx = canvas.getContext("2d", { alpha: true });
if (!ctx) return;
const { width, height } = dimensions;
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.font = font;
const metrics = ctx.measureText(text);
const ascent = metrics.actualBoundingBoxAscent;
const descent = metrics.actualBoundingBoxDescent;
const baselineY = (height + ascent - descent) / 2;
const numLines = Math.floor(height / lineGap) + 10;
startTimeRef.current = performance.now();
const animate = (currentTime: number) => {
const elapsed = (currentTime - startTimeRef.current) / 1000;
const phase = (elapsed / animationDuration) * Math.PI * 2;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, width, height);
ctx.globalCompositeOperation = "source-over";
ctx.font = font;
ctx.textBaseline = "alphabetic";
ctx.textAlign = "left";
ctx.fillStyle = "#000";
ctx.fillText(text, 0, baselineY);
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = "source-atop";
for (let i = 0; i < numLines; i++) {
const y = i * lineGap;
const curve1 = Math.sin(phase) * curveIntensity;
const curve2 = Math.sin(phase + 0.5) * curveIntensity * 0.6;
const colorIndex = i % resolvedColors.length;
ctx.strokeStyle = resolvedColors[colorIndex];
ctx.lineWidth = lineWidth;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.bezierCurveTo(
width * 0.33,
y + curve1,
width * 0.66,
y + curve2,
width,
y,
);
ctx.stroke();
}
animationRef.current = requestAnimationFrame(animate);
};
animationRef.current = requestAnimationFrame(animate);
return () => {
cancelAnimationFrame(animationRef.current);
};
}, [
text,
font,
bgColor,
resolvedColors,
animationDuration,
lineWidth,
lineGap,
curveIntensity,
dimensions,
]);
return (
<span
className={cn(
"relative inline-block",
overlay && "absolute inset-0",
className,
)}
>
<span
ref={bgRef}
className={cn(
"pointer-events-none absolute h-0 w-0 opacity-0",
backgroundClassName,
)}
aria-hidden="true"
/>
<span ref={textRef} className="invisible inline-block" aria-hidden="true">
{text}
</span>
<canvas
ref={canvasRef}
className="pointer-events-none absolute top-0 left-0"
style={{
width: dimensions.width || "auto",
height: dimensions.height || "auto",
}}
aria-label={text}
role="img"
/>
</span>
);
}适用模型与稳定度
建议模型
继续探索
@Oluwaphilemon1 使用 Claude Fable 5 · Three.js · Blender · GSAP完成的网页案例,包含公开结果、完整 Prompt 与原始来源。
提示语
Claude Fable 5 和 GPT-5.6 很强🥵🥵🥵 下面是我如何使用 Claude 构建的 👇 提示词:「设计一个机构网站,把服务以 3D 超市商品包装的形式呈现」 使用 Three.js 和 WebGL 在浏览器中实时渲染所有产品 使用 Blender 制作包装模型,然后导出为 GLTF 以加载到 Three.js 中 使用 GSAP 实现流畅的产品旋转和悬停交互 如果你想做一个让客户感觉像是在购买高级商品的作品集,请保存这条 🛒
复制这段 TSX 组件源码,交给 Cursor、v0、Claude 等 AI 编程工具,说明这是「3D Animated Pin」的 Aceternity UI 组件实现,即可让它接入你的 React 项目,再按需替换文案、配色与触发参数复用。
提示语
// components/ui/3d-pin.tsx "use client"; import React, { useState } from "react"; import { motion } from "motion/react"; import { cn } from "@/lib/utils"; export const PinContainer = ({ children, title, href, className, containerClassName…

提示语
首先搜索 "/unlazy" skill,完整阅读它,并在整个任务中持续使用。 将此视为一项至少持续 {argument name="minimum duration" default="5 小时"} 的大型视觉构建。如果环境允许,最好进行 {argument name="optimal duration" default="8–24 小时"} 有实质意义的实现和迭代。不要在制作出一个还不错的场景后就停下。 在浏览器中创建一个细节极其丰富的实时 {argument nam