update
111
CONTRIBUTING.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# 贡献指南
|
||||||
|
|
||||||
|
感谢你对这个项目的关注!我们欢迎所有形式的贡献。
|
||||||
|
|
||||||
|
## 如何贡献
|
||||||
|
|
||||||
|
### 报告 Bug
|
||||||
|
|
||||||
|
如果你发现了 bug,请:
|
||||||
|
|
||||||
|
1. 查看 [Issues](https://github.com/ricocc/ricoui-portfolio/issues) 确认该 bug 尚未被报告
|
||||||
|
2. 如果是一个新的 bug,请创建一个新的 Issue,包含:
|
||||||
|
- 清晰的问题描述
|
||||||
|
- 复现步骤
|
||||||
|
- 预期行为
|
||||||
|
- 实际行为
|
||||||
|
- 截图(如果有)
|
||||||
|
- 环境信息(浏览器、操作系统等)
|
||||||
|
|
||||||
|
### 提出新功能
|
||||||
|
|
||||||
|
如果你有改进建议或新功能想法:
|
||||||
|
|
||||||
|
1. 查看现有 Issues 确认没有类似提议
|
||||||
|
2. 创建一个新的 Feature Request Issue,描述:
|
||||||
|
- 功能描述
|
||||||
|
- 使用场景
|
||||||
|
- 可能的实现方案(可选)
|
||||||
|
|
||||||
|
### 提交代码
|
||||||
|
|
||||||
|
1. **Fork 项目**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/your-username/ricoui-portfolio.git
|
||||||
|
cd ricoui-portfolio
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **创建分支**
|
||||||
|
```bash
|
||||||
|
git checkout -b feature/your-feature-name
|
||||||
|
# 或
|
||||||
|
git checkout -b fix/your-bug-fix
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **安装依赖**
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **进行修改**
|
||||||
|
- 编写代码
|
||||||
|
- 确保代码通过检查:`pnpm check`
|
||||||
|
- 测试你的修改
|
||||||
|
|
||||||
|
5. **提交更改**
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: 添加新功能描述"
|
||||||
|
# 或
|
||||||
|
git commit -m "fix: 修复bug描述"
|
||||||
|
```
|
||||||
|
|
||||||
|
提交信息请遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范:
|
||||||
|
- `feat:` 新功能
|
||||||
|
- `fix:` Bug 修复
|
||||||
|
- `docs:` 文档更新
|
||||||
|
- `style:` 代码格式调整(不影响功能)
|
||||||
|
- `refactor:` 代码重构
|
||||||
|
- `perf:` 性能优化
|
||||||
|
- `test:` 测试相关
|
||||||
|
- `chore:` 构建/工具相关
|
||||||
|
|
||||||
|
6. **推送并创建 Pull Request**
|
||||||
|
```bash
|
||||||
|
git push origin feature/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在 GitHub 上创建 Pull Request。
|
||||||
|
|
||||||
|
## 代码规范
|
||||||
|
|
||||||
|
- 使用 Biome 进行代码检查和格式化
|
||||||
|
- 运行 `pnpm check` 确保代码符合规范
|
||||||
|
- 保持代码简洁、可读
|
||||||
|
- 添加必要的注释
|
||||||
|
|
||||||
|
## 开发环境
|
||||||
|
|
||||||
|
- Node.js >= 18
|
||||||
|
- pnpm (推荐) 或 npm/yarn
|
||||||
|
- 现代浏览器(Chrome、Firefox、Safari、Edge)
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
在提交 PR 前,请确保:
|
||||||
|
|
||||||
|
- [ ] 代码通过 `pnpm check`
|
||||||
|
- [ ] 在本地运行 `pnpm dev` 测试正常
|
||||||
|
- [ ] 构建通过 `pnpm build`
|
||||||
|
- [ ] 测试了不同的浏览器和设备
|
||||||
|
|
||||||
|
## 问题?
|
||||||
|
|
||||||
|
如果遇到问题,可以:
|
||||||
|
|
||||||
|
- 查看 [Issues](https://github.com/ricocc/ricoui-portfolio/issues)
|
||||||
|
- 创建新的 Issue 提问
|
||||||
|
- 联系维护者:hello@ricoui.com
|
||||||
|
|
||||||
|
再次感谢你的贡献!🎉
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 674 KiB |
|
Before Width: | Height: | Size: 858 KiB |
|
Before Width: | Height: | Size: 390 KiB |
|
Before Width: | Height: | Size: 440 KiB |
|
Before Width: | Height: | Size: 878 KiB |
|
Before Width: | Height: | Size: 519 KiB |
|
Before Width: | Height: | Size: 689 KiB |
|
Before Width: | Height: | Size: 583 KiB |
|
Before Width: | Height: | Size: 420 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 401 KiB |
|
Before Width: | Height: | Size: 188 KiB |
@@ -18,11 +18,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Luon Models",
|
"name": "Luon Models",
|
||||||
"description": "Company Website",
|
"description": "Agency Company Website",
|
||||||
"tags": ["Company","Website","Branding"],
|
"tags": ["Agency","Website"],
|
||||||
"image": "/assets/works/luonmodels.jpg",
|
"image": "/assets/works/luonmodels.jpg",
|
||||||
"video": "/assets/works/luonmodels.mp4",
|
"video": "/assets/works/luonmodels.mp4",
|
||||||
"url": "https://luonmodels.netlify.app/"
|
"url": "/work/luonmodels"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Ricoui",
|
"name": "Ricoui",
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ const {title, description
|
|||||||
<h3>Writing</h3>
|
<h3>Writing</h3>
|
||||||
<p>Style guides, design notes, and quick reads.</p>
|
<p>Style guides, design notes, and quick reads.</p>
|
||||||
<div class="mt-2 btn opacity-0 transition duration-300 ease-in-out">
|
<div class="mt-2 btn opacity-0 transition duration-300 ease-in-out">
|
||||||
<Button url="/blog" size="sm">
|
<!-- <Button url="/blog" size="sm">
|
||||||
Blog
|
Blog
|
||||||
</Button>
|
</Button> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="explore-figure computer absolute right-[5%] sm:right-[-10%] md:right-[-7%] bottom-[14%] w-[120px] sm:w-[130px] md:w-[148px] h-auto">
|
<div class="explore-figure computer absolute right-[5%] sm:right-[-10%] md:right-[-7%] bottom-[14%] w-[120px] sm:w-[130px] md:w-[148px] h-auto">
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const currentSizeClasses = sizeClasses[size];
|
|||||||
target={type === "disabled" ? undefined : target}
|
target={type === "disabled" ? undefined : target}
|
||||||
class={`${
|
class={`${
|
||||||
type === "solid"
|
type === "solid"
|
||||||
? `flex justify-center items-center flex-grow-0 flex-shrink-0 relative gap-1.5 ${currentSizeClasses.padding} rounded-xl bg-gradient-to-b from-white to-[#edf1fa] dark:from-[#15233b] dark:to-[#0f1b2d] border-[1px] border-[#f3f5ff] dark:border-[#243757] max-w-60 transition duration-400 ease-in-out hover:translate-y-[-2px] hover:shadow-[0px_6px_9px_0_rgba(61,99,171,0.15)] dark:hover:shadow-[0px_6x_9px_0_rgba(59,123,217,0.25)]`
|
? `flex justify-center items-center flex-grow-0 flex-shrink-0 relative gap-1.5 ${currentSizeClasses.padding} rounded-xl bg-gradient-to-b from-white to-[#edf1fa] dark:from-[#15233b] dark:to-[#0f1b2d] border-[1px] border-primary/20 dark:border-[#243757] max-w-60 transition duration-400 ease-in-out hover:translate-y-[-2px] hover:shadow-[0px_6px_9px_0_rgba(61,99,171,0.15)] dark:hover:shadow-[0px_6x_9px_0_rgba(59,123,217,0.25)]`
|
||||||
: type === "fill"
|
: type === "fill"
|
||||||
? `flex justify-center items-center flex-grow-0 flex-shrink-0 relative gap-1.5 ${currentSizeClasses.padding} rounded-xl bg-btn-primary dark:bg-btn-primary-dark border-[0] border-[rgba(138,127,255,0.2)] shadow-[0px_2px_3px_0_rgba(0,0,0,0.1)] max-w-60 transition duration-400 ease-in-out hover:bg-btn-primary-hover dark:hover:bg-btn-primary-dark-hover hover:translate-y-[-2px] hover:shadow-[0px_6px_9px_0_rgba(61,99,171,0.15)] dark:shadow-[0px_6px_9px_0_rgba(0,0,0,0.2)]`
|
? `flex justify-center items-center flex-grow-0 flex-shrink-0 relative gap-1.5 ${currentSizeClasses.padding} rounded-xl bg-btn-primary dark:bg-btn-primary-dark border-[0] border-[rgba(138,127,255,0.2)] shadow-[0px_2px_3px_0_rgba(0,0,0,0.1)] max-w-60 transition duration-400 ease-in-out hover:bg-btn-primary-hover dark:hover:bg-btn-primary-dark-hover hover:translate-y-[-2px] hover:shadow-[0px_6px_9px_0_rgba(61,99,171,0.15)] dark:shadow-[0px_6px_9px_0_rgba(0,0,0,0.2)]`
|
||||||
: type === "disabled"
|
: type === "disabled"
|
||||||
@@ -58,11 +58,11 @@ const currentSizeClasses = sizeClasses[size];
|
|||||||
onclick={type === "disabled" ? "return false;" : undefined}
|
onclick={type === "disabled" ? "return false;" : undefined}
|
||||||
>
|
>
|
||||||
{type === "solid" ? (
|
{type === "solid" ? (
|
||||||
<div class={`flex gap-1 items-center justify-center ${currentSizeClasses.fontSize} font-medium text-center text-[#6f6c8f] dark:text-[#c9d7f2]`}>
|
<div class={`flex gap-1 items-center justify-center ${currentSizeClasses.fontSize} font-medium text-center text-neutral-700 dark:text-neutral-300`}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
) : type === "fill" ? (
|
) : type === "fill" ? (
|
||||||
<span class={`flex gap-1 items-center justify-center ${size === "md" ? "text-sm" : currentSizeClasses.fontSize} font-medium text-center text-white`}>
|
<span class={`flex gap-1 items-center justify-center ${size === "md" ? "text-sm" : currentSizeClasses.fontSize} font-medium text-center text-neutral-50`}>
|
||||||
<slot />
|
<slot />
|
||||||
</span>
|
</span>
|
||||||
) : type === "disabled" ? (
|
) : type === "disabled" ? (
|
||||||
|
|||||||
@@ -5,15 +5,3 @@
|
|||||||
<img src="/assets/logo.png" alt="Logo" class="w-10 h-10 rounded-full" />
|
<img src="/assets/logo.png" alt="Logo" class="w-10 h-10 rounded-full" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
{/* <a
|
|
||||||
href="/"
|
|
||||||
class="h-5 text-base group relative z-30 flex items-center space-x-1.5 text-black dark:text-white font-semibold"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="text-xl -translate-y-0.5 group-hover:-rotate-12 group-hover:scale-[1.2] ease-in-out duration-300"
|
|
||||||
>✦</span
|
|
||||||
>
|
|
||||||
<!-- Logo Text -->
|
|
||||||
<span class="-translate-y-0.5"> aria</span>
|
|
||||||
</a> */}
|
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
---
|
|
||||||
interface Props {
|
|
||||||
class?: string; // Class of the marquee
|
|
||||||
marqueeElements: number; // Number of elements
|
|
||||||
marqueeElementWidth: string; // Width of the marquee
|
|
||||||
marqueeElementWidthAuto?: boolean; // Is the width auto
|
|
||||||
marqueeElementWidthResponsive: string; // Width of the marquee
|
|
||||||
marqueePauseOnHover?: boolean; // Duration of the marquee
|
|
||||||
marqueeReverse?: "reverse" | "" | undefined; // Duration of the marquee
|
|
||||||
marqueeDuration?: string; // Duration of the marquee
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
marqueeElements,
|
|
||||||
marqueeElementWidth,
|
|
||||||
marqueeElementWidthAuto = true,
|
|
||||||
marqueeElementWidthResponsive,
|
|
||||||
marqueePauseOnHover = false,
|
|
||||||
marqueeDuration = "50s",
|
|
||||||
marqueeReverse = "",
|
|
||||||
} = Astro.props;
|
|
||||||
|
|
||||||
// Remove value of if `marqueeElementWidth`, `marqueeElementWidthResponsive` marqueeElementWidthAuto true
|
|
||||||
if (marqueeElementWidthAuto) {
|
|
||||||
marqueeElementWidth = "";
|
|
||||||
marqueeElementWidthResponsive = "";
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
<div
|
|
||||||
class:list={[
|
|
||||||
"marquee",
|
|
||||||
{
|
|
||||||
"marquee-revers": marqueeReverse,
|
|
||||||
"marquee-pause-on-hover": marqueePauseOnHover,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
style={`--marquee-elements:${marqueeElements};--marquee-element-width-responsive:calc(${marqueeElementWidthResponsive} / var(--marquee-elements) * ${marqueeElements});--marquee-element-width:calc(${marqueeElementWidth} / var(--marquee-elements) * ${marqueeElements});${"--marquee-duration:" + marqueeDuration};--marquee-reverse:${marqueeReverse};`}>
|
|
||||||
<div
|
|
||||||
class:list={[
|
|
||||||
"marquee-content flex items-center",
|
|
||||||
{ "marquee-element-width-auto": marqueeElementWidthAuto },
|
|
||||||
Astro.props.class,
|
|
||||||
]}>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
function updateMarquee() {
|
|
||||||
const marqueeContents = document.querySelectorAll(
|
|
||||||
".marquee-element-width-auto",
|
|
||||||
) as NodeListOf<HTMLElement>;
|
|
||||||
|
|
||||||
marqueeContents.forEach((marqueeContent) => {
|
|
||||||
const images = marqueeContent.querySelectorAll("img");
|
|
||||||
|
|
||||||
images.forEach((img) => {
|
|
||||||
if (!img.complete) {
|
|
||||||
// wait for images to load
|
|
||||||
img.addEventListener("load", () => updateImageSize(img));
|
|
||||||
} else {
|
|
||||||
updateImageSize(img);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// update total width after images resized
|
|
||||||
const totalWidth = marqueeContent.scrollWidth;
|
|
||||||
marqueeContent.style.setProperty("--total-width", `${totalWidth / 2}px`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateImageSize(img: HTMLImageElement) {
|
|
||||||
const computed = getComputedStyle(img);
|
|
||||||
const height = parseFloat(computed.height); // Tailwind-applied height
|
|
||||||
const aspectRatio = img.naturalWidth / img.naturalHeight;
|
|
||||||
const width = height * aspectRatio;
|
|
||||||
|
|
||||||
img.setAttribute("height", height.toString());
|
|
||||||
img.setAttribute("width", width.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounced resize handler
|
|
||||||
let resizeTimeout: number;
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
clearTimeout(resizeTimeout);
|
|
||||||
resizeTimeout = window.setTimeout(updateMarquee, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initial run
|
|
||||||
updateMarquee();
|
|
||||||
</script>
|
|
||||||
@@ -12,10 +12,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Matter from "matter-js"; // 修改为默认导入
|
import Matter from "matter-js";
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
// 懒加载:当 .tricks-view 进入视口时初始化
|
|
||||||
const target = document.querySelector(".tricks-view");
|
const target = document.querySelector(".tricks-view");
|
||||||
if (!target) {
|
if (!target) {
|
||||||
console.error("[Tricks] .tricks-view not found");
|
console.error("[Tricks] .tricks-view not found");
|
||||||
@@ -39,7 +39,7 @@ import Matter from "matter-js"; // 修改为默认导入
|
|||||||
async function initTricksAnimation() {
|
async function initTricksAnimation() {
|
||||||
const container = document.querySelector(".tool-stack-box") || document.querySelector(".matter-wrapper");
|
const container = document.querySelector(".tool-stack-box") || document.querySelector(".matter-wrapper");
|
||||||
const canvasHost = document.querySelector(".tricks-canvas") as HTMLElement;
|
const canvasHost = document.querySelector(".tricks-canvas") as HTMLElement;
|
||||||
const domLayer = document.querySelector(".tricks-elements") as HTMLElement; // 添加类型断言
|
const domLayer = document.querySelector(".tricks-elements") as HTMLElement;
|
||||||
|
|
||||||
if (!container || !canvasHost || !domLayer) {
|
if (!container || !canvasHost || !domLayer) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -48,36 +48,36 @@ import Matter from "matter-js"; // 修改为默认导入
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 配置(调大元素尺寸 & 缩小边缘间隙)
|
// Config (enlarge element size & reduce edge gaps)
|
||||||
const pad = 2; // 内边距,越小越贴边
|
const pad = 2; // Inner padding; smaller value sticks closer to edges
|
||||||
const margin = 1; // 静态边界与容器边缘距离,越小越贴边
|
const margin = 1; // Distance between static boundaries and container edges; smaller value sticks closer
|
||||||
const count = 15; // 元素数量
|
const count = 15; // Number of elements
|
||||||
const noRotate = false; // 不旋转可设为 true
|
const noRotate = false; // Set to true to disable rotation
|
||||||
const scaleFactor = 0.75; // 全局放大因子:>1 放大,<1 缩小
|
const scaleFactor = 0.75; // Global scaling factor: >1 enlarge, <1 shrink
|
||||||
|
|
||||||
// 尺寸
|
// Dimensions
|
||||||
let { width: cw, height: ch } = container.getBoundingClientRect();
|
let { width: cw, height: ch } = container.getBoundingClientRect();
|
||||||
let W = Math.max(cw - pad, 100);
|
let W = Math.max(cw - pad, 100);
|
||||||
let H = Math.max(ch - pad, 100);
|
let H = Math.max(ch - pad, 100);
|
||||||
|
|
||||||
// 物理引擎
|
// Physics engine
|
||||||
const engine = Matter.Engine.create();
|
const engine = Matter.Engine.create();
|
||||||
const world = engine.world;
|
const world = engine.world;
|
||||||
|
|
||||||
// 鼠标拖拽(事件落在 canvasHost 上)
|
// Mouse drag (events fall on canvasHost)
|
||||||
const mouseConstraint = Matter.MouseConstraint.create(engine, {
|
const mouseConstraint = Matter.MouseConstraint.create(engine, {
|
||||||
mouse: Matter.Mouse.create(canvasHost),
|
mouse: Matter.Mouse.create(canvasHost),
|
||||||
constraint: { render: { visible: false }, stiffness: 1 },
|
constraint: { render: { visible: false }, stiffness: 1 },
|
||||||
});
|
});
|
||||||
Matter.World.add(world, mouseConstraint);
|
Matter.World.add(world, mouseConstraint);
|
||||||
|
|
||||||
// 边界(地面与左右墙)
|
// Boundaries (ground and side walls)
|
||||||
let ground = Matter.Bodies.rectangle(W / 2, H - margin, W, 14, { isStatic: true });
|
let ground = Matter.Bodies.rectangle(W / 2, H - margin, W, 14, { isStatic: true });
|
||||||
let wallL = Matter.Bodies.rectangle(margin, H / 2, 14, H, { isStatic: true });
|
let wallL = Matter.Bodies.rectangle(margin, H / 2, 14, H, { isStatic: true });
|
||||||
let wallR = Matter.Bodies.rectangle(W - margin, H / 2, 14, H, { isStatic: true });
|
let wallR = Matter.Bodies.rectangle(W - margin, H / 2, 14, H, { isStatic: true });
|
||||||
Matter.World.add(world, [ground, wallL, wallR]);
|
Matter.World.add(world, [ground, wallL, wallR]);
|
||||||
|
|
||||||
// 图标路径(按需调整)
|
// Icon paths (adjust as needed)
|
||||||
const iconUrls = [
|
const iconUrls = [
|
||||||
"/assets/stack/astro.png",
|
"/assets/stack/astro.png",
|
||||||
"/assets/stack/css.png",
|
"/assets/stack/css.png",
|
||||||
@@ -95,12 +95,12 @@ import Matter from "matter-js"; // 修改为默认导入
|
|||||||
"/assets/stack/vscode.png",
|
"/assets/stack/vscode.png",
|
||||||
];
|
];
|
||||||
|
|
||||||
// 预加载图片,获取 naturalWidth/Height
|
// Preload images to obtain naturalWidth/Height
|
||||||
function loadImage(url: string) {
|
function loadImage(url: string) {
|
||||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.decoding = "async";
|
img.decoding = "async";
|
||||||
img.loading = "eager"; // 懒加载触发后尽快加载
|
img.loading = "eager"; // Load as soon as lazy-load is triggered
|
||||||
img.src = url;
|
img.src = url;
|
||||||
img.onload = () => resolve(img);
|
img.onload = () => resolve(img);
|
||||||
img.onerror = () => reject(new Error("Image load failed: " + url));
|
img.onerror = () => reject(new Error("Image load failed: " + url));
|
||||||
@@ -114,10 +114,10 @@ import Matter from "matter-js"; // 修改为默认导入
|
|||||||
console.error("[Tricks] image load failed:", e);
|
console.error("[Tricks] image load failed:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尺寸策略:显著增大目标面积区间(适配 720x480)
|
// Sizing strategy: significantly increase target area range (adapted for 720x480)
|
||||||
function computeAreaRange() {
|
function computeAreaRange() {
|
||||||
// 以容器面积为基准,提高图标面积比例
|
// Use container area as the base; increase icon area proportion
|
||||||
// 720*480 ≈ 345600;这里让单个图标面积大致在 1/20 ~ 1/10 容器面积之间
|
// 720*480 ≈ 345600; here a single icon area is roughly between 1/20 ~ 1/10 of the container area
|
||||||
const baseArea = W * H;
|
const baseArea = W * H;
|
||||||
const areaMin = clamp(baseArea / 20, 12000, 30000);
|
const areaMin = clamp(baseArea / 20, 12000, 30000);
|
||||||
const areaMax = clamp(baseArea / 10, 20000, 50000);
|
const areaMax = clamp(baseArea / 10, 20000, 50000);
|
||||||
@@ -125,7 +125,7 @@ import Matter from "matter-js"; // 修改为默认导入
|
|||||||
}
|
}
|
||||||
let { areaMin, areaMax } = computeAreaRange();
|
let { areaMin, areaMax } = computeAreaRange();
|
||||||
|
|
||||||
// 图标刚体类(矩形,尺寸基于 naturalWidth/Height)
|
// Icon rigid body class (rectangle, size based on naturalWidth/Height)
|
||||||
class IconBody {
|
class IconBody {
|
||||||
w: number;
|
w: number;
|
||||||
h: number;
|
h: number;
|
||||||
@@ -141,29 +141,29 @@ import Matter from "matter-js"; // 修改为默认导入
|
|||||||
? img.naturalWidth / img.naturalHeight
|
? img.naturalWidth / img.naturalHeight
|
||||||
: 1;
|
: 1;
|
||||||
|
|
||||||
// 目标面积(随机落在区间内)
|
// Target area (random within range)
|
||||||
const A = randRange(areaMin, areaMax);
|
const A = randRange(areaMin, areaMax);
|
||||||
// 推导宽高:w = sqrt(A*r), h = w/r
|
// Derive width/height: w = sqrt(A*r), h = w/r
|
||||||
let w = Math.sqrt(A * r);
|
let w = Math.sqrt(A * r);
|
||||||
let h = w / r;
|
let h = w / r;
|
||||||
|
|
||||||
// 全局放大或缩小
|
// Global scale up or down
|
||||||
w *= scaleFactor;
|
w *= scaleFactor;
|
||||||
h *= scaleFactor;
|
h *= scaleFactor;
|
||||||
|
|
||||||
this.w = w;
|
this.w = w;
|
||||||
this.h = h;
|
this.h = h;
|
||||||
|
|
||||||
// 物理矩形刚体
|
// Physics rectangle body
|
||||||
this.body = Matter.Bodies.rectangle(x, y, this.w, this.h, {
|
this.body = Matter.Bodies.rectangle(x, y, this.w, this.h, {
|
||||||
restitution: 0.35,
|
restitution: 0.35,
|
||||||
friction: 0.1,
|
friction: 0.1,
|
||||||
frictionAir: 0.02,
|
frictionAir: 0.02,
|
||||||
density: clamp((this.w * this.h) / 40000, 0.001, 0.02),
|
density: clamp((this.w * this.h) / 40000, 0.001, 0.02),
|
||||||
inertia: noRotate ? Infinity : undefined, // 不旋转:设置惯性无穷
|
inertia: noRotate ? Infinity : undefined, // No rotation: set infinite inertia
|
||||||
});
|
});
|
||||||
|
|
||||||
// DOM:容器 + img
|
// DOM: container + img
|
||||||
this.el = document.createElement("div");
|
this.el = document.createElement("div");
|
||||||
this.el.className = "tricks-circle";
|
this.el.className = "tricks-circle";
|
||||||
this.el.style.width = `${this.w}px`;
|
this.el.style.width = `${this.w}px`;
|
||||||
@@ -172,11 +172,11 @@ import Matter from "matter-js"; // 修改为默认导入
|
|||||||
const node = img.cloneNode(true) as HTMLImageElement;
|
const node = img.cloneNode(true) as HTMLImageElement;
|
||||||
node.style.width = "100%";
|
node.style.width = "100%";
|
||||||
node.style.height = "100%";
|
node.style.height = "100%";
|
||||||
node.style.objectFit = "contain"; // 或 "cover"
|
node.style.objectFit = "contain"; // or "cover"
|
||||||
node.alt = node.alt || "icon";
|
node.alt = node.alt || "icon";
|
||||||
|
|
||||||
this.el.appendChild(node);
|
this.el.appendChild(node);
|
||||||
domLayer.appendChild(this.el); // 这里 domLayer 已经确保不为 null
|
domLayer.appendChild(this.el); // domLayer is ensured to be non-null here
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
@@ -190,7 +190,7 @@ import Matter from "matter-js"; // 修改为默认导入
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建图标刚体
|
// Create icon rigid bodies
|
||||||
const total = Math.min(iconsImgs.length, count);
|
const total = Math.min(iconsImgs.length, count);
|
||||||
const iconsBodies: IconBody[] = [];
|
const iconsBodies: IconBody[] = [];
|
||||||
for (let i = 0; i < total; i++) {
|
for (let i = 0; i < total; i++) {
|
||||||
@@ -198,16 +198,16 @@ import Matter from "matter-js"; // 修改为默认导入
|
|||||||
}
|
}
|
||||||
Matter.World.add(world, iconsBodies.map((it) => it.body));
|
Matter.World.add(world, iconsBodies.map((it) => it.body));
|
||||||
|
|
||||||
// 启动引擎
|
// Start engine
|
||||||
const runner = Matter.Runner.create();
|
const runner = Matter.Runner.create();
|
||||||
Matter.Runner.run(runner, engine);
|
Matter.Runner.run(runner, engine);
|
||||||
|
|
||||||
// 每次物理步进后,同步 DOM
|
// After each physics step, sync DOM
|
||||||
Matter.Events.on(engine, "afterUpdate", () => {
|
Matter.Events.on(engine, "afterUpdate", () => {
|
||||||
iconsBodies.forEach((it) => it.update());
|
iconsBodies.forEach((it) => it.update());
|
||||||
});
|
});
|
||||||
|
|
||||||
// 自适应尺寸:更新边界位置,并重新计算面积区间
|
// Responsive sizing: update boundary positions and recompute area range
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("resize", () => {
|
||||||
const rect = container.getBoundingClientRect();
|
const rect = container.getBoundingClientRect();
|
||||||
W = Math.max(rect.width - pad, 100);
|
W = Math.max(rect.width - pad, 100);
|
||||||
@@ -223,7 +223,7 @@ import Matter from "matter-js"; // 修改为默认导入
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 工具函数(IIFE 外也可用)
|
// Utility functions (also usable outside the IIFE)
|
||||||
function clamp(v: number, min: number, max: number) {
|
function clamp(v: number, min: number, max: number) {
|
||||||
return Math.max(min, Math.min(max, v));
|
return Math.max(min, Math.min(max, v));
|
||||||
}
|
}
|
||||||
@@ -319,4 +319,4 @@ import Matter from "matter-js"; // 修改为默认导入
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ if (typeof logo === 'string') {
|
|||||||
data-aos-duration="500"
|
data-aos-duration="500"
|
||||||
data-aos-once="true"
|
data-aos-once="true"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2 sm:gap-3 rounded-2xl bg-neutral-900/85 dark:bg-neutral-900/85 text-neutral-100 shadow-[0_6px_28px_rgba(0,0,0,0.25)] ring-1 ring-white/10 backdrop-blur-xl px-2 py-2 max-w-[92vw]">
|
<div class="flex items-center gap-1 sm:gap-2 rounded-2xl bg-neutral-900/85 dark:bg-neutral-900/85 text-neutral-100 shadow-[0_6px_28px_rgba(0,0,0,0.25)] ring-1 ring-white/10 backdrop-blur-xl px-2 py-2 max-w-[92vw]">
|
||||||
<!-- Logo block -->
|
<!-- Logo block -->
|
||||||
<div class="shrink-0 h-10 w-10 sm:h-11 sm:w-11 rounded-xl bg-neutral-800 grid place-items-center ring-1 ring-white/10 overflow-hidden">
|
<div class="shrink-0 h-10 w-10 sm:h-10 sm:w-10 rounded-full bg-neutral-800 grid place-items-center ring-1 ring-white/10 overflow-hidden">
|
||||||
{logoSrc ? (
|
{logoSrc ? (
|
||||||
<img src={logoSrc} alt="logo" class="h-full w-full object-cover" loading="lazy" />
|
<img src={logoSrc} alt="logo" class="h-full w-full object-cover" loading="lazy" />
|
||||||
) : (
|
) : (
|
||||||
@@ -47,7 +47,7 @@ if (typeof logo === 'string') {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Middle pills (tags + optional github) -->
|
<!-- Middle pills (tags + optional github) -->
|
||||||
<div class="flex items-center gap-2sm:gap-3 overflow-x-auto scrollbar-none max-w-[46vw] sm:max-w-[52vw] md:max-w-[56vw] pr-1">
|
<div class="flex items-center gap-1 sm:gap-1 overflow-x-auto scrollbar-none max-w-[46vw] sm:max-w-[52vw] md:max-w-[56vw] pr-1">
|
||||||
{tags && tags.map((t) => (
|
{tags && tags.map((t) => (
|
||||||
<span class=" hidden font-light sm:inline-flex items-center h-10 px-3 sm:px-3 rounded-xl bg-neutral-800/80 ring-1 ring-white/10 text-neutral-300 text-sm whitespace-nowrap">
|
<span class=" hidden font-light sm:inline-flex items-center h-10 px-3 sm:px-3 rounded-xl bg-neutral-800/80 ring-1 ring-white/10 text-neutral-300 text-sm whitespace-nowrap">
|
||||||
{t}
|
{t}
|
||||||
@@ -72,7 +72,7 @@ if (typeof logo === 'string') {
|
|||||||
href={url}
|
href={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="nofollow noopener"
|
rel="nofollow noopener"
|
||||||
class="inline-flex items-center h-10 sm:h-11 px-4 sm:px-5 rounded-xl bg-yellow-300 text-neutral-900 font-medium hover:bg-yellow-200 transition-colors whitespace-nowrap"
|
class="inline-flex items-center h-10 sm:h-11 px-4 sm:px-5 rounded-xl bg-yellow-300 text-neutral-900 text-sm hover:bg-yellow-200 transition-colors whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{visitLabel}
|
{visitLabel}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -3,18 +3,19 @@
|
|||||||
const SITE_URL = process.env.PUBLIC_SITE_URL || 'https://portfolio.ricoui.com/';
|
const SITE_URL = process.env.PUBLIC_SITE_URL || 'https://portfolio.ricoui.com/';
|
||||||
|
|
||||||
export const siteConfig = {
|
export const siteConfig = {
|
||||||
title: "Rico Portfolio",
|
title: "Ricoui Portfolio",
|
||||||
author: "Ricoui",
|
author: "Ricoui",
|
||||||
url: SITE_URL,
|
url: SITE_URL,
|
||||||
mail: "hello@ricoui.com",
|
mail: "hello@ricoui.com",
|
||||||
resume: "#",
|
// resume add your resume file path here: /assets/resume.pdf
|
||||||
|
resume: "https://ricoui.com/",
|
||||||
utm: {
|
utm: {
|
||||||
source: `${SITE_URL}`,
|
source: `${SITE_URL}`,
|
||||||
medium: "referral",
|
medium: "referral",
|
||||||
campaign: "navigation",
|
campaign: "navigation",
|
||||||
},
|
},
|
||||||
meta:{
|
meta:{
|
||||||
title: "Rico Portfolio",
|
title: "Ricoui Portfolio",
|
||||||
description: "I'm Rico, a web designer passionate about both design and code. Currently developing a personal product for the design community.",
|
description: "I'm Rico, a web designer passionate about both design and code. Currently developing a personal product for the design community.",
|
||||||
keywords: "web designer, portfolio, design, code, personal website",
|
keywords: "web designer, portfolio, design, code, personal website",
|
||||||
image: `${SITE_URL}/og.jpg`,
|
image: `${SITE_URL}/og.jpg`,
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ const filtered = headings.filter(h => h.depth <= 3);
|
|||||||
data-aos-once="true"
|
data-aos-once="true"
|
||||||
>Let's Connect</h2>
|
>Let's Connect</h2>
|
||||||
<p
|
<p
|
||||||
class="text-sm leading-6 text-neutral-600 dark:text-neutral-300"
|
class="text-sm leading-6 text-neutral-700 dark:text-neutral-300"
|
||||||
data-aos="fade-up-xs"
|
data-aos="fade-up-xs"
|
||||||
data-aos-delay="100"
|
data-aos-delay="100"
|
||||||
data-aos-duration="500"
|
data-aos-duration="500"
|
||||||
@@ -339,7 +339,7 @@ const filtered = headings.filter(h => h.depth <= 3);
|
|||||||
href={siteConfig.social.twitter}
|
href={siteConfig.social.twitter}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="text-indigo-500 underline">follow me on twitter</a
|
class="text-indigo-500 underline">follow me on twitter</a
|
||||||
>, or you can send me an <a href="mailto:hello@ricouic.om" class="text-indigo-500 underline"
|
>, or you can send me an <a href="mailto:hello@ricoui.com" class=" underline"
|
||||||
>email</a
|
>email</a
|
||||||
> and I'll be sure to get back to you.
|
> and I'll be sure to get back to you.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import PageHeader from "@/components/elements/PageHeader.astro";
|
|||||||
import BlogSection from "@/components/sections/BlogSection.astro";
|
import BlogSection from "@/components/sections/BlogSection.astro";
|
||||||
|
|
||||||
// 使用与分页页面相同的每页文章数量
|
// 使用与分页页面相同的每页文章数量
|
||||||
const POSTS_PER_PAGE = 3;
|
const POSTS_PER_PAGE = 6;
|
||||||
// 博客首页显示第一页内容
|
// 博客首页显示第一页内容
|
||||||
const currentPage = 1;
|
const currentPage = 1;
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -10,8 +10,20 @@ export const POSTS_PER_PAGE = 6;
|
|||||||
// 生成分页路径
|
// 生成分页路径
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const allPosts = await getCollection("post");
|
const allPosts = await getCollection("post");
|
||||||
// 使用顶层定义的POSTS_PER_PAGE常量
|
|
||||||
const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE);
|
// 按发布日期排序文章
|
||||||
|
let posts = allPosts.sort(
|
||||||
|
(a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 查找特色文章,在列表页需要从主列表中移除
|
||||||
|
const featuredPost = posts.find((post) => post.data.featured);
|
||||||
|
if (featuredPost) {
|
||||||
|
posts = posts.filter((post) => post.data.featured !== true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用移除特色文章后的文章数量计算总页数
|
||||||
|
const totalPages = Math.ceil(posts.length / POSTS_PER_PAGE);
|
||||||
|
|
||||||
return Array.from({ length: totalPages }, (_, i) => {
|
return Array.from({ length: totalPages }, (_, i) => {
|
||||||
const page = i + 1;
|
const page = i + 1;
|
||||||
|
|||||||
58
src/pages/work/luonmodels.astro
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
import { Image } from "astro:assets";
|
||||||
|
import PostLayout from "@/layouts/PostLayout.astro";
|
||||||
|
import PageHeader from "@/components/elements/PageHeader.astro";
|
||||||
|
import ActionBar from "@/components/widgets/ActionBar.astro";
|
||||||
|
import SeparatorLine from "@/components/elements/SeparatorLine.astro";
|
||||||
|
|
||||||
|
//自动导入 Image 组件和图像
|
||||||
|
const allImages = await Astro.glob('../../assets/work/luonmodels/*.{jpg,png,webp}');
|
||||||
|
const logoUrl = "https://luonmodels.vercel.app/favicon.png";
|
||||||
|
---
|
||||||
|
<PostLayout
|
||||||
|
title="LUON - Agency Website Template"
|
||||||
|
description = 'LUONModels is a sleek and fluid Astro enterprise template specifically designed for modeling agencies, photographers, and studios.',
|
||||||
|
keywords = 'LUONModels, Agency Website Template, Astro, Modeling Agency, Photographer, Studio',
|
||||||
|
>
|
||||||
|
<div class="work article">
|
||||||
|
<main class="work-wrapper">
|
||||||
|
<PageHeader
|
||||||
|
title="LUON - Agency Website Template"
|
||||||
|
tags={["Designer","Portfolio","Open Source"]}
|
||||||
|
className="mb-6 md:mb-8"
|
||||||
|
>
|
||||||
|
</PageHeader>
|
||||||
|
</section>
|
||||||
|
<div class="site-container m-auto">
|
||||||
|
<SeparatorLine />
|
||||||
|
</div>
|
||||||
|
<section class="article-content">
|
||||||
|
<div class="site-container mx-auto my-8 md:my-16 ">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
LUONModels is a sleek and fluid Astro enterprise template specifically designed for modeling agencies, photographers, and studios. It aims to showcase people, photography, and information clearly and effectively, while also making it convenient to build event pages, content blogs, and various other specialized pages.
|
||||||
|
</p>
|
||||||
|
<p>Features</p>
|
||||||
|
<ul>
|
||||||
|
<li>✅ Responsive Design: Fully optimized for mobile and desktop viewing, ensuring a consistent experience across devices.</li>
|
||||||
|
<li>✅ Built with Astro.js: Leverages the powerful features of Astro.js for optimized performance and easy customize.</li>
|
||||||
|
<li>✅ Intuitive User Interface: Minimalist layout promotes ease of use, allowing users to find what they need without confusion.</li>
|
||||||
|
<li>✅ Speed Optimization: Engineered for exceptional loading times, ensuring that users don’t have to wait on page loads.</li>
|
||||||
|
<li>✅ Customizable Components: Flexibility to customize elements easily, aligning with your brand’s identity and needs.</li>
|
||||||
|
<li>✅ SEO Friendly: Built with search engine optimization in mind, helping improve visibility and rankings.</li>
|
||||||
|
</ul>
|
||||||
|
<p>🔗🔗 <a href="https://luonmodels.vercel.app/" target="_blank">Live Demo</a></p>
|
||||||
|
</div>
|
||||||
|
{allImages.map((img, index) => (
|
||||||
|
<picture class="picture mt-8 md:mt-12 mb-8 md:mb-12 rounded-2xl overflow-hidden">
|
||||||
|
<Image src={img.default} alt={`Image ${index + 1}`} class="w-full h-full object-cover" loading="lazy" decoding="async" quality={100}/>
|
||||||
|
</picture>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<ActionBar logo={logoUrl} tags={["Agency", "Website"]} url="https://luonmodels.vercel.app/" github="https://ricoui.gumroad.com/l/luon" visitLabel="Visit Site" />
|
||||||
|
</PostLayout>
|
||||||
|
|
||||||
|
|
||||||