个人项目平台的构建历程:从过程与探索中诞生的作品集

本文详细记录了设计师Mike van der Sanden如何将个人作品集演进为动态项目平台的技术实践。文章深入探讨了使用Nuxt.js框架构建交互式组件的过程,包含p5.js画布动画、Three.js 3D渲染、CSS网格布局等具体代码实现,展现了创意与技术的完美融合。

个人项目平台的构建历程:从过程与探索中诞生的作品集

Mike van der Sanden邀请我们深入了解他的作品集如何演变成一个充满活力的项目资源库。

作者:Mike van der Sanden
发表于:2025年9月17日

这个夏天我创建了我的个人项目平台。这并非完全有意为之。当我意识到自己的创作过程走向时,已经走了一段路。

说到创作过程,我是它的忠实粉丝。当你准备好放手时,你会发现自己置身于意想不到的境地。总之,当我发现自己正在构建个人项目平台时,两条路径交汇在了一起。让我们先谈谈第一条路径。

路径一:必要的快乐空间

作为设计师,或者说作为人类,并非每天都充满灵感。特别是当设计与AI领域变化如此之快时,有时很难看清大局。

作为补救措施,我开始构建一个情绪板作为我的快乐空间。每当我遇到让我微笑的参考内容,就会把它放进去。它包括我的梦想办公室板块、引起共鸣的引文和想法,以及随机的图像片段,这些片段组合起来感觉就像我——或者至少是设计师版本的我。我开始添加自己的涂鸦、笔记和关于目标的思考:我为什么还在做这个?作为设计师我在寻找什么?

路径二:Instagram实验

2022年12月的一个晚上,我和一位设计师朋友喝酒。我们只是为了好玩而随意创作。在工作中,我已经转向更多的管理角色,但我怀念设计工作。

然后我想:为什么不把它放到网上?于是我创建了一个Instagram账号并发布了我的第一个Processing草图。

我创作得越多,就越想创作。随着时间的推移,这个习惯成为了我的一部分。草图变得具有交互性,但让我困扰的是它们只能在本地运行——只有我能与之互动。我也开始分享快速教程,并对收到的大量积极回应感到惊讶,这些回应来自那些受到启发想要自己创作的人。

两条路径的交汇

与此同时,我的"快乐空间"笔记变得更长、更有目的性。我希望更多人能与我的草图互动。既然我做这些都是为了好玩,为什么不分享源代码?为什么不收集我的资源供他人使用?

慢慢地,这变成了一个平台的想法:一个让有意为之和意外发现共存的地方,向新设计师展示——特别是在AI取代所有乐趣的时代——学习手艺、练习和训练创意肌肉仍然很重要。

现在我需要构建它了。

构建平台

既然我们在Codrops上,让我们谈谈代码。我有PHP和JavaScript背景——老派的,在ES6或TypeScript之前,更不用说Vue或React了。我想用这个项目学习新东西。

经过一些研究,我决定使用Nuxt.js。根据我的了解,它比Next.js更容易设置。而且由于我的平台不太可能很快扩展,我认为它能胜任工作。几年前我也玩过Prismic CMS。轻量级,功能不多,但对我来说足够了。于是我观看了一些Nuxt.js+Prismic教程,然后就开始了。

英雄区域

我知道我想要交互式组件。让访问者立即感受到我的工作。让我们从英雄区域开始。

在摩擦中发现美 用鼠标在画布上绘制对象,简单明了。我希望这些对象与自然有联系——某种会生长、繁荣的东西——就像你承担许多个人项目时那样。

在我的第一个草图中,花朵从小到大缩放,实际上是在生长。但后来我想:有多少次我被一个草图卡住,对一个行不通的想法感到沮丧?所以我决定线性增长是不诚实的。大多数时候,当我从事项目时,我的头脑到处都是。事物应该随机缩放,它们甚至不需要在宽度和高度上匹配。我喜欢这样,它反映了我工作中控制与混乱之间的张力。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
 * 获取下一张图像的一部分
 */
public getPortion(): p5.Image {
  // 获取原始图像
  const original = this.getNext();
  if (!original) return null;

  // 源图像
  const ow = original.width;
  const oh = original.height;
  const sx = Math.random() * ow;
  const sy = Math.random() * oh;

  // 剩余部分
  const loW = ow - sx;
  const loH = oh - sy;

  let sw = Math.round(loW * Math.random()) + 10;
  let sh = Math.round(loH * Math.random()) + 10;

  // 目标位置
  const dx = 0;
  const dy = 0;
  const dw = sw;
  const dh = sh;
   
  // 创建新图像
  const copy = this.p.createImage(dw, dh);
  copy.copy(original, sx, sy, sw, sh, dx, dy, dw, dh);

  return copy;
}

public getRandomSizedPortion(): p5.Image {
  // 获取部分图像
  const img = this.getPortion();
  if (!img) return null;

  // 随机尺寸
  const maxSize = this.p.width * .1;
  img.resize(this.p.random(10,maxSize), this.p.random(10,maxSize));

  return img;
}

页脚

为了平衡英雄区域,我也让页脚具有交互性。我使用了一个较早的草图作为基础,添加了深度和纹理,使其感觉有点像抽象的海洋。

对我来说,它带来了一种平静和专注感——具有微妙的垂直运动,以及随着鼠标沿x轴移动而变化的色调。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
 * 计算所有数据
 */
public update() {
  // 动画设置
  let duration: number = 128;
  let progress: number = this.p.frameCount % duration;
  if(progress == 0) this.iteration++;
   
  // 行数和高度
  let numRowsDrawn: number = this.numRows + 1 + this.iteration;
  let colW: number = this.p.width / this.numCols;
  let rowH: number = this.p.height / this.numRows;

  let count = 0;
  // 遍历行
  for (let y: number = this.iteration; y<numRowsDrawn; y++) {
     
    // 计算y位置(从底部开始)
    let targetY: number = this.p.height - (y+1) * rowH + this.iteration * rowH;

    // 当前进度位置
    let posY: number = this.p.map(progress, 0, duration, targetY, targetY+rowH);
    // 鼠标影响
    const smoothing = 0.06;
    this.currentMouseX += (this.p.mouseX - this.currentMouseX) * smoothing;
    const mouseInfluence: number = this.p.map(this.currentMouseX, 0, this.p.width, .8, -.3);

    // 基于y位置的影响
    let yInfluence: number = this.p.map(posY / this.numRows, 0, rowH, 1, this.numRows+1) * mouseInfluence;
    // 每行双倍列数
    let extraCols: number = Math.exp(yInfluence * Math.LN2); 
    // 尺寸和位置
    let currentW: number = colW + extraCols * colW;
     
    // 遍历列
    for (let x:number = 0; x<this.numCols; x++) {
      // 计算x位置
      let posX: number = x * currentW - (extraCols * yInfluence + 1) * colW;

      // 不绘制屏幕外的内容
      if(posX > this.p.width) continue;
      if(posX + currentW < 0) continue;

      // 绘制
      this.display(x, y, posX, posY, currentW, rowH);
      count++;
     }
   }
 }

瀑布流网格

我一直喜欢充满活力的灵感网站。你会看到各种图像和视频,它们本身就很强大,但在不同背景下获得新的意义。这就是我想要的项目概览效果。

由于我不追求任何特定的图形风格,我喜欢它更像参考资料的集合。这就是为什么我决定使用瀑布流网格。我不想使用插件,所以我构建了这个CSS/JavaScript小工具,使用CSS网格行来分布图像,JavaScript根据CMS中设置的宽高比计算应该跨越多少行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function applyMasonry() {
  // 获取网格和项目
  const grid = document.querySelector('.masonry-grid');
  const items = grid?.querySelectorAll('.masonry-item');

  // 确保两者都已加载
  if (!grid || !items) return

  // 从CSS获取属性
  const rowHeight = parseInt(getComputedStyle(grid).getPropertyValue('grid-auto-rows'))
  const gap = parseInt(getComputedStyle(grid).getPropertyValue('gap') || 0)
   
  items.forEach(item => {
    // 分别获取媒体和信息容器
    const media = item.querySelector('.masonry-item__image-container')
    const info = item.querySelector('.masonry-item__info-container')

    if (!media || !info) return

    // 组合它们得到项目高度
    const mediaHeight = media.getBoundingClientRect().height
    const infoHeight = info.getBoundingClientRect().height
    const itemHeight = mediaHeight + infoHeight

    // 计算要跨越的行数
    const rowSpan = Math.ceil((itemHeight + gap) / (rowHeight + gap))

    // 应用行跨度
    item.style.gridRowEnd = `span ${rowSpan}`;
    item.style.opacity = 1;
  })
}

资源与代码

由于我真心想鼓励人们开始自己的个人项目之旅,我想分享资源和代码示例来帮助他们入门。

P5.js与Three.js对比

为了在Instagram上发布我的标志,我创建了一个Processing草图,将标志放置在像素化的3D场景中旋转。我喜欢它几乎变成了某种雕塑或建筑。现在我只需要构建一个网页版本。

因为我的英雄区域和页脚组件都是p5.js,这是我的首选。但速度很慢——我是说真的很慢。无论我如何尝试优化,3D工作负载都会严重影响性能。几年前我只用过一次Three.js,但我记得它处理3D相当好。不确定使用多个库是否能获得最佳性能,但既然都是为了好玩,我决定试一试。使用Three.js版本,我可以为结构添加更多细节,而且与p5.js版本相比,它的性能仍然完美无缺。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let instanceId: number = 0;

// 使用体素分辨率(细节)循环,而不是图像分辨率
for (let z: number = 0; z < detail; z++) {
  for (let y: number = 0; y < detail; y++) {
    const flippedY: number = detail - 1 - y;

    for (let x: number = 0; x < detail; x++) {
      // 使用归一化坐标采样图像
      const sampleX: number = Math.floor((x / detail) * imgDetail);
      const sampleY: number = Math.floor((flippedY / detail) * imgDetail);
      const sampleZ: number = Math.floor((z / detail) * imgDetail);

      const brightness1: number = getBrightnessAt(imgData, imgDetail, sampleX, sampleY);
      const brightness2: number = getBrightnessAt(imgData, imgDetail, sampleZ, sampleY);

      if (brightness1 < 100 && brightness2 < 100 && instanceId < maxInstances) {
        dummy.position.set(
          x * cellSize - (detail * cellSize) / 2,
          y * cellSize - (detail * cellSize) / 2,
          z * cellSize - (detail * cellSize) / 2
          );
        dummy.updateMatrix();
        mesh.setMatrixAt(instanceId, dummy.matrix);
        instanceId++;
      }
    }
  }
}

总结

这个平台还没有完成——这就是重点。这是一个与我的编码工具互动的空间,是分享草图以供进一步探索的空间,是让过程本身保持可见的空间。如果你是设计师或程序员,我希望它能推动你开始或继续自己的副业项目。这就是创造力保持活力的方式。感谢阅读。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计