阅读视图

发现新文章,点击刷新页面。

dijstra 模板题(对每个字母运行一次dijstra算法)

Problem: 2976. 转换字符串的最小成本 I

[TOC]

思路

dijstra 模板题(对每个字母运行一次dijstra算法)

Code

###Python3

class Solution:
    def minimumCost(self, source: str, target: str, original: List[str], changed: List[str], cost: List[int]) -> int:
        # 先建图,有向图(26个字母映射到0-25)
        g = [[] for _ in range(26)]
        dt = defaultdict()
        n = len(original)
        for i, x in enumerate(original):
            y = changed[i]
            c = cost[i]
            x0 = ord(x) - ord('a')
            y0 = ord(y) - ord('a')
            if (x0, y0) not in dt.keys():
                dt[(x0, y0)] = c 
                g[x0].append(y0)
            else:
                dt[(x0, y0)] = min(c, dt[(x0, y0)])
        
        # 运行dijstra 算法,统计所有点(26个字母)能到达的其他点的最短距离
        final_dt = defaultdict()
        
        # dijstra算法 统计x能到达的所有点点最短距路
        def dfs (x: int) -> None:
            dist = {} # 本次dijstra统计的距离,后续加到final_dt中
            unvisited_nodes = {} # 先用广度优先搜索将x能达到的点初始化到inf
            q = [(0, x, x)]
            vis = [0] * 26
            cur = [x]
            vis[x] = 1
            while cur:
                pre = cur
                cur = []
                for el in pre:
                    for y in g[el]:
                        if vis[y] == 0:
                            unvisited_nodes[(x, y)] = inf
                            vis[y] = 1
                            cur.append(y)
            # 开始最小路径搜索
            unvisited_nodes[(x, x)] = 0
            seen = set()
            # 使用 dijstra算法计算达到各店的最短值
            while unvisited_nodes:
                current_distance, x1, y1 = heapq.heappop(q)
                if y1 in seen:
                    continue
                seen.add(y1)
                for el in g[y1]:
                    if (x, el) not in unvisited_nodes: continue
                    new_distance = current_distance + dt[(y1,el)]
                    if new_distance < unvisited_nodes[(x, el)]:
                        unvisited_nodes[(x, el)] = new_distance
                        heapq.heappush(q, (new_distance, x, el))
                dist[(x,y1)] = current_distance
                unvisited_nodes.pop((x, y1))
            for k, v in dist.items():
                final_dt[k] = v

        # 对每个字母运行dijstra算法
        for i in range(26):
            dfs(i)
        ans = 0
        # 统计完,开始对每个字母改变计算答案,如果达不到,返回-1
        for i, x in enumerate(source):
            x1 = ord(x) - ord('a')
            y1 = ord(target[i]) - ord('a')
            if x1 != y1:
                if (x1, y1) not in final_dt.keys():
                    return - 1
                else:
                    ans += final_dt[(x1, y1)]
        return ans

每日一题-转换字符串的最小成本 I🟡

给你两个下标从 0 开始的字符串 sourcetarget ,它们的长度均为 n 并且由 小写 英文字母组成。

另给你两个下标从 0 开始的字符数组 originalchanged ,以及一个整数数组 cost ,其中 cost[i] 代表将字符 original[i] 更改为字符 changed[i] 的成本。

你从字符串 source 开始。在一次操作中,如果 存在 任意 下标 j 满足 cost[j] == z  、original[j] == x 以及 changed[j] == y 。你就可以选择字符串中的一个字符 x 并以 z 的成本将其更改为字符 y

返回将字符串 source 转换为字符串 target 所需的 最小 成本。如果不可能完成转换,则返回 -1

注意,可能存在下标 ij 使得 original[j] == original[i]changed[j] == changed[i]

 

示例 1:

输入:source = "abcd", target = "acbe", original = ["a","b","c","c","e","d"], changed = ["b","c","b","e","b","e"], cost = [2,5,5,1,2,20]
输出:28
解释:将字符串 "abcd" 转换为字符串 "acbe" :
- 更改下标 1 处的值 'b' 为 'c' ,成本为 5 。
- 更改下标 2 处的值 'c' 为 'e' ,成本为 1 。
- 更改下标 2 处的值 'e' 为 'b' ,成本为 2 。
- 更改下标 3 处的值 'd' 为 'e' ,成本为 20 。
产生的总成本是 5 + 1 + 2 + 20 = 28 。
可以证明这是可能的最小成本。

示例 2:

输入:source = "aaaa", target = "bbbb", original = ["a","c"], changed = ["c","b"], cost = [1,2]
输出:12
解释:要将字符 'a' 更改为 'b':
- 将字符 'a' 更改为 'c',成本为 1 
- 将字符 'c' 更改为 'b',成本为 2 
产生的总成本是 1 + 2 = 3。
将所有 'a' 更改为 'b',产生的总成本是 3 * 4 = 12 。

示例 3:

输入:source = "abcd", target = "abce", original = ["a"], changed = ["e"], cost = [10000]
输出:-1
解释:无法将 source 字符串转换为 target 字符串,因为下标 3 处的值无法从 'd' 更改为 'e' 。

 

提示:

  • 1 <= source.length == target.length <= 105
  • sourcetarget 均由小写英文字母组成
  • 1 <= cost.length== original.length == changed.length <= 2000
  • original[i]changed[i] 是小写英文字母
  • 1 <= cost[i] <= 106
  • original[i] != changed[i]

构建无障碍组件之Link Pattern

Link Pattern 详解:构建无障碍链接组件

链接是 Web 页面中最核心的导航元素,它将用户从一个资源引导到另一个资源。根据 W3C WAI-ARIA Link Pattern 规范,正确实现的链接组件不仅要提供清晰的导航功能,更要确保所有用户都能顺利访问,包括依赖屏幕阅读器等辅助技术的用户。本文将深入探讨 Link Pattern 的核心概念、实现要点以及最佳实践。

一、链接的定义与核心功能

链接是一个允许用户导航到另一个页面、页面位置或其他资源的界面组件。链接的本质是超文本引用,它告诉用户这里有你可能感兴趣的另一个资源。与按钮执行动作不同,链接的作用是导航,这是两者最本质的区别。

在实际开发中,浏览器为原生 HTML 链接提供了丰富的功能支持,例如在新窗口中打开目标页面、将目标 URL 复制到系统剪贴板等。因此,应尽可能使用 HTML a 元素创建链接

二、何时需要自定义链接实现

在某些情况下,需要使用非 a 元素实现链接功能,例如:

  • 图片作为导航入口
  • 使用 CSS 伪元素创建的可视化链接
  • 复杂的 UI 组件中需要链接行为的元素

根据 WAI-ARIA 规范,当必须使用非 a 元素时,需要手动添加必要的 ARIA 属性和键盘支持。

三、键盘交互规范

键盘可访问性是 Web 无障碍设计的核心要素之一。链接组件必须支持完整的键盘交互,确保无法使用鼠标的用户也能顺利操作。根据 Link Pattern 规范:

回车键是激活链接的主要方式。当用户按下回车键时,链接被触发执行导航操作。

上下文菜单(可选):按 Shift + F10 键可以打开链接的上下文菜单,提供复制链接地址、在新窗口中打开等选项。

操作系统 打开上下文菜单
Windows Shift + F10Menu
macOS Control + 点击

四、WAI-ARIA 角色、状态和属性

正确使用 WAI-ARIA 属性是构建无障碍链接组件的技术基础。

角色声明是基础要求。非 a 元素的链接需要将 role 属性设置为 link,向辅助技术表明这是一个链接组件。

示例:使用 span 元素模拟链接:

<span
  tabindex="0"
  role="link"
  onclick="goToLink(event, 'https://example.com/')"
  onkeydown="goToLink(event, 'https://example.com/')">
  示例网站
</span>

可访问名称是链接最重要的可访问性特征之一。链接必须有可访问的名称,可以通过元素文本内容、aria-label 或 alt 属性提供。

示例 1:使用 img 元素作为链接时,通过 alt 属性提供可访问名称:

<img
  tabindex="0"
  role="link"
  onclick="goToLink(event, 'https://example.com/')"
  onkeydown="goToLink(event, 'https://example.com/')"
  src="logo.png"
  alt="示例网站" />

示例 2:使用 aria-label 为链接提供可访问名称:

<span
  tabindex="0"
  role="link"
  class="text-link"
  onclick="goToLink(event, 'https://example.com/')"
  onkeydown="goToLink(event, 'https://example.com/')"
  aria-label="访问示例网站"
  >🔗</span
>

焦点管理需要使用 tabindex="0",将链接元素包含在页面 Tab 序列中,使其可通过键盘聚焦。

五、完整示例

以下是使用不同元素实现链接的完整示例:

<!-- 示例 1:span 元素作为链接 -->
<span
  tabindex="0"
  role="link"
  onclick="goToLink(event, 'https://w3.org/')"
  onkeydown="goToLink(event, 'https://w3.org/')">
  W3C 网站
</span>

<!-- 示例 2:img 元素作为链接 -->
<img
  tabindex="0"
  role="link"
  onclick="goToLink(event, 'https://w3.org/')"
  onkeydown="goToLink(event, 'https://w3.org/')"
  src="logo.svg"
  alt="W3C 网站" />

<!-- 示例 3:使用 aria-label 的链接 -->
<span
  tabindex="0"
  role="link"
  class="link-styled"
  onclick="goToLink(event, 'https://w3.org/')"
  onkeydown="goToLink(event, 'https://w3.org/')"
  aria-label="W3C 网站"
  >🔗</span
>

<script>
  function goToLink(event, url) {
    if (event.type === 'keydown' && event.key !== 'Enter') {
      return;
    }
    window.open(url, '_blank');
  }
</script>

<style>
  .link-styled {
    color: blue;
    text-decoration: underline;
    cursor: pointer;
  }
  .link-styled:focus {
    outline: 2px solid blue;
    outline-offset: 2px;
  }
</style>

六、最佳实践

6.1 优先使用原生元素

尽可能使用原生 HTML a 元素创建链接。浏览器为原生链接提供了丰富的功能和更好的兼容性,无需额外添加 ARIA 属性。

<!-- 推荐做法 -->
<a
  href="https://example.com/"
  target="_blank"
  >访问示例</a
>

<!-- 不推荐做法 -->
<span
  role="link"
  tabindex="0"
  >访问示例</span
>

6.2 正确处理键盘事件

自定义链接需要同时处理 onclick 和 onkeydown 事件,确保用户可以通过回车键激活链接。

element.addEventListener('keydown', function (e) {
  if (e.key === 'Enter') {
    e.preventDefault();
    // 执行导航操作
    window.location.href = this.dataset.url;
  }
});

6.3 提供视觉反馈

链接应该有明确的视觉样式,让用户能够识别这是一个可交互的元素。同时,应该提供键盘焦点样式。

a,
[role='link'] {
  color: #0066cc;
  text-decoration: underline;
  cursor: pointer;
}

/* 焦点状态:仅对键盘 Tab 导航显示焦点框,鼠标点击时不显示 */
a:focus-visible,
[role='link']:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
  border-radius: 2px;
}

/* 悬停状态:加深颜色并加粗下划线,提供鼠标交互反馈 */
a:hover,
[role='link']:hover {
  color: #004499;
  text-decoration-thickness: 2px;
}

/* 已访问状态:使用紫色标识用户已访问的链接 */
a:visited,
[role='link']:visited {
  color: #551a8b;
}

/* 激活状态:点击瞬间的颜色变化 */
a:active,
[role='link']:active {
  color: #ff0000;
}

6.4 避免过度使用 ARIA

WAI-ARIA 有一条重要原则:没有 ARIA 比糟糕的 ARIA 更好。在某些情况下,错误使用 ARIA 可能会导致比不使用更糟糕的可访问性体验。只有在确实需要时才使用自定义链接实现。

七、链接与按钮的区别

在 Web 开发中,正确区分按钮和链接至关重要。

特性 链接 按钮
功能 导航到其他资源 触发动作
HTML 元素 <a> <button><input type="button">
键盘激活 Enter Space、Enter
role 属性 link button
典型用途 页面跳转、锚点导航 提交表单、打开对话框

八、总结

构建无障碍的链接组件需要关注多个层面的细节。从语义化角度,应优先使用原生 HTML a 元素;从键盘交互角度,必须支持回车键激活;从 ARIA 属性角度,需要正确使用 role="link" 和可访问名称。

WAI-ARIA Link Pattern 为我们提供了清晰的指导方针,遵循这些规范能够帮助我们创建更加包容和易用的 Web 应用。每一个正确实现的链接组件,都是构建无障碍网络环境的重要一步。

python dijkstra

Problem: 100156. 转换字符串的最小成本 I

[TOC]

思路

python dijkstra

解题方法

python dijkstra

Code

###Python3

class Solution:
    def minimumCost(self, source: str, target: str, original: List[str], changed: List[str], cost: List[int]) -> int:
        ans=0
        g=[[] for _ in range(26)]
        for i,j,c in zip(original,changed,cost):
            heappush(g[ord(i)-ord('a')],[c,ord(j)-ord('a')])
        d=defaultdict(int)
        def dijkstra(x,y):#dijkstra
            distant=0
            vis=set()
            q=[]
            q.append([0,x])
            while q:
                c,temp=heappop(q)
                if temp==y:
                    return c
                if temp in vis:
                    continue
                vis.add(temp)
                for cc,xx in g[temp]:
                    heappush(q,[c+cc,xx])
            return -1
        for i,j in zip(source,target):
            if i!=j:
                if (ord(i)-ord('a'),ord(j)-ord('a')) in d:
                    ans+=d[ord(i)-ord('a'),ord(j)-ord('a')]
                else :
                    res=dijkstra(ord(i)-ord('a'),ord(j)-ord('a'))
                    if res==-1:
                        return -1
                    ans+=res
                    d[ord(i)-ord('a'),ord(j)-ord('a')]=res
        return ans

Floyd 算法(Python/Java/C++/Go)

建图,从 $\textit{original}[i]$ 向 $\textit{changed}[i]$ 连边,边权为 $\textit{cost}[i]$。

然后用 Floyd 算法求图中任意两点最短路,得到 $\textit{dis}$ 矩阵,原理请看 带你发明 Floyd 算法!包含为什么循环顺序是 $kij$ 的讲解。

这里得到的 $\textit{dis}[i][j]$ 表示字母 $i$ 通过若干次替换操作变成字母 $j$ 的最小成本。

最后累加所有 $\textit{dis}[\textit{original}[i]][\textit{changed}[i]]$,即为答案。如果答案为无穷大,返回 $-1$。

本题视频讲解

###py

class Solution:
    def minimumCost(self, source: str, target: str, original: List[str], changed: List[str], cost: List[int]) -> int:
        dis = [[inf] * 26 for _ in range(26)]
        for i in range(26):
            dis[i][i] = 0

        for x, y, c in zip(original, changed, cost):
            x = ord(x) - ord('a')
            y = ord(y) - ord('a')
            dis[x][y] = min(dis[x][y], c)

        for k in range(26):
            for i in range(26):
                if dis[i][k] == inf:
                    continue  # 巨大优化!
                for j in range(26):
                    dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j])

        ans = sum(dis[ord(x) - ord('a')][ord(y) - ord('a')] for x, y in zip(source, target))
        return ans if ans < inf else -1

###java

class Solution {
    public long minimumCost(String source, String target, char[] original, char[] changed, int[] cost) {
        final int INF = Integer.MAX_VALUE / 2;
        int[][] dis = new int[26][26];
        for (int i = 0; i < 26; i++) {
            Arrays.fill(dis[i], INF);
            dis[i][i] = 0;
        }
        for (int i = 0; i < cost.length; i++) {
            int x = original[i] - 'a';
            int y = changed[i] - 'a';
            dis[x][y] = Math.min(dis[x][y], cost[i]);
        }
        for (int k = 0; k < 26; k++) {
            for (int i = 0; i < 26; i++) {
                if (dis[i][k] == INF) {
                    continue; // 巨大优化!
                }
                for (int j = 0; j < 26; j++) {
                    dis[i][j] = Math.min(dis[i][j], dis[i][k] + dis[k][j]);
                }
            }
        }

        long ans = 0;
        for (int i = 0; i < source.length(); i++) {
            int d = dis[source.charAt(i) - 'a'][target.charAt(i) - 'a'];
            if (d == INF) {
                return -1;
            }
            ans += d;
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    long long minimumCost(string source, string target, vector<char>& original, vector<char>& changed, vector<int>& cost) {
        const int INF = 0x3f3f3f3f;
        int dis[26][26];
        memset(dis, 0x3f, sizeof(dis));
        for (int i = 0; i < 26; i++) {
            dis[i][i] = 0;
        }
        for (int i = 0; i < cost.size(); i++) {
            int x = original[i] - 'a';
            int y = changed[i] - 'a';
            dis[x][y] = min(dis[x][y], cost[i]);
        }
        for (int k = 0; k < 26; k++) {
            for (int i = 0; i < 26; i++) {
                if (dis[i][k] == INF) {
                    continue; // 巨大优化!
                }
                for (int j = 0; j < 26; j++) {
                    dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
                }
            }
        }

        long long ans = 0;
        for (int i = 0; i < source.length(); i++) {
            int d = dis[source[i] - 'a'][target[i] - 'a'];
            if (d == INF) {
                return -1;
            }
            ans += d;
        }
        return ans;
    }
};

###go

func minimumCost(source, target string, original, changed []byte, cost []int) (ans int64) {
const inf = math.MaxInt / 2
dis := [26][26]int{}
for i := range dis {
for j := range dis[i] {
if j != i {
dis[i][j] = inf
}
}
}
for i, c := range cost {
x := original[i] - 'a'
y := changed[i] - 'a'
dis[x][y] = min(dis[x][y], c)
}
for k := range dis {
for i := range dis {
if dis[i][k] == inf {
continue // 巨大优化!
}
for j := range dis {
dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j])
}
}
}

for i, b := range source {
d := dis[b-'a'][target[i]-'a']
if d == inf {
return -1
}
ans += int64(d)
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n+m+|\Sigma|^3)$,其中 $n$ 为 $\textit{source}$ 的长度,$m$ 为 $\textit{cost}$ 的长度,$|\Sigma|$ 为字符集合的大小,本题中字符均为小写字母,所以 $|\Sigma|=26$。
  • 空间复杂度:$\mathcal{O}(|\Sigma|^2)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

Floyd 求最短路

解法:Floyd 求最短路

本题使用到了 Floyd 求最短路算法。关于这个算法,我给力扣专栏写过一篇文章进行了详细的介绍,欢迎有兴趣的读者阅读:https://zhuanlan.zhihu.com/p/623757829

由于本题只能将单个字符改为其它字符,所以不同位置之间的修改互不影响,那么答案就是把 source[i] 改成 target[i] 的代价加起来,即 $\sum\limits_{i = 1}^n g(s_i, t_i)$,其中 $g(s_i, t_i)$ 是把字符 $s_i$ 改成 $t_i$ 的最小代价。

我们把每个字母看成一个点,如果能把字母 $x$ 改成字母 $y$,就从 $x$ 到 $y$ 连一条长度为 cost[i] 的有向边。这样对于任意两个字母 $x$ 和 $y$,把 $x$ 改成 $y$ 的最小代价就是从 $x$ 到 $y$ 的最短路。

由于我们需要知道任意两个点之间的最短路,所以可以使用 Floyd 算法。

复杂度 $\mathcal{O}(n + m + \Sigma^3)$,其中 $n$ 是 source 的长度,$m$ 是 original 的长度,$\Sigma$ 是字符集的大小,即 $26$。

参考代码(c++)

###c++

class Solution {
public:
    long long minimumCost(string source, string target, vector<char>& original, vector<char>& changed, vector<int>& cost) {
        const int INF = 1e9;
        // 建有向图
        long long g[26][26];
        for (int i = 0; i < 26; i++) for (int j = 0; j < 26; j++) g[i][j] = INF;
        for (int i = 0; i < 26; i++) g[i][i] = 0;
        for (int i = 0; i < original.size(); i++) {
            int x = original[i] - 'a', y = changed[i] - 'a';
            g[x][y] = min(g[x][y], 1LL * cost[i]);
        }

        // floyd 求最短路
        for (int k = 0; k < 26; k++) for (int i = 0; i < 26; i++) for (int j = 0; j < 26; j++)
            g[i][j] = min(g[i][j], g[i][k] + g[k][j]);

        long long ans = 0;
        // 把每个位置的修改代价加起来
        for (int i = 0; i < source.size(); i++) {
            int x = source[i] - 'a', y = target[i] - 'a';
            if (x != y) {
                // x 不能变成 y,无解
                if (g[x][y] >= INF) return -1;
                // 否则答案增加把 x 改成 y 的最小代价
                ans += g[x][y];
            }
        }
        return ans;
    }
};

mri@1.2.0源码阅读

mri(全称 Minimalist CLI Argument Parser)的核心作用是解析命令行传入的参数,把用户在终端输入的命令行参数(比如 --port 3000、-v)转换成 js对象

Three.js 变形动画-打造花瓣绽放

概述

本文将详细介绍如何使用 Three.js 实现变形动画效果。我们将学习如何利用 Morph Targets(形态目标)技术,让 3D 模型在不同形状之间平滑过渡,创造出花瓣绽放等生动的动画效果。

screenshot_2026-01-28_17-56-51.gif

准备工作

首先,我们需要引入必要的 Three.js 库和相关工具:

import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";

场景初始化

首先,我们需要创建一个基本的 Three.js 场景:

const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();

// 设置相机位置
camera.position.set(0, 0, 20);
scene.add(camera);

环境设置

添加 HDR 环境纹理,提升场景的真实感:

// 添加hdr环境纹理
const loader = new RGBELoader();
loader.load("./textures/038.hdr", function (texture) {
  texture.mapping = THREE.EquirectangularReflectionMapping;
  scene.environment = texture;
});

DRACO 压缩模型加载

使用 DRACO 压缩技术加载 GLB 模型:

// 加载压缩的glb模型
const gltfLoader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("./draco/gltf/");
dracoLoader.setDecoderConfig({ type: "js" });
dracoLoader.preload();
gltfLoader.setDRACOLoader(dracoLoader);

变形动画核心实现

这是变形动画的关键部分,通过 Morph Targets 技术实现模型变形:

let params = {
  value: 0,
  value1: 0,
};

let mixer;
let stem, petal, stem1, petal1, stem2, petal2;

// 加载第一个模型(初始状态)
gltfLoader.load("./model/f4.glb", function (gltf1) {
  console.log(gltf1);
  stem = gltf1.scene.children[0];
  petal = gltf1.scene.children[1];
  gltf1.scene.rotation.x = Math.PI;

  // 遍历场景中的对象并处理材质
  gltf1.scene.traverse((item) => {
    if (item.material && item.material.name == "Water") {
      item.material = new THREE.MeshStandardMaterial({
        color: "skyblue",
        depthWrite: false,
        transparent: true,
        depthTest: false,
        opacity: 0.5,
      });
    }
    if (item.material && item.material.name == "Stem") {
      stem = item;
    }
    if (item.material && item.material.name == "Petal") {
      petal = item;
    }
  });

  // 加载第二个模型(中间状态)
  gltfLoader.load("./model/f2.glb", function (gltf2) {
    gltf2.scene.traverse((item) => {
      if (item.material && item.material.name == "Stem") {
        stem1 = item;
        // 将第二个模型的几何体作为第一个形态目标添加到基础模型
        stem.geometry.morphAttributes.position = [
          stem1.geometry.attributes.position,
        ];
        stem.updateMorphTargets();
      }
      if (item.material && item.material.name == "Petal") {
        petal1 = item;
        // 将第二个模型的几何体作为第一个形态目标添加到基础模型
        petal.geometry.morphAttributes.position = [
          petal1.geometry.attributes.position,
        ];
        petal.updateMorphTargets();
        console.log(petal.morphTargetInfluences);
      }

      // 加载第三个模型(最终状态)
      gltfLoader.load("./model/f1.glb", function (gltf2) {
        gltf2.scene.traverse((item) => {
          if (item.material && item.material.name == "Stem") {
            stem2 = item;
            // 将第三个模型的几何体作为第二个形态目标添加到基础模型
            stem.geometry.morphAttributes.position.push(
              stem2.geometry.attributes.position
            );
            stem.updateMorphTargets();
          }
          if (item.material && item.material.name == "Petal") {
            petal2 = item;
            // 将第三个模型的几何体作为第二个形态目标添加到基础模型
            petal.geometry.morphAttributes.position.push(
              petal2.geometry.attributes.position
            );
            petal.updateMorphTargets();
            console.log(petal.morphTargetInfluences);
          }
        });
      });
    });
  });

  // 使用 GSAP 创建变形动画
  gsap.to(params, {
    value: 1,
    duration: 4,
    onUpdate: function () {
      // 控制第一个形态目标的影响程度
      stem.morphTargetInfluences[0] = params.value;
      petal.morphTargetInfluences[0] = params.value;
    },
    onComplete: function () {
      // 在第一个动画完成后,开始第二个变形动画
      gsap.to(params, {
        value1: 1,
        duration: 4,
        onUpdate: function () {
          // 控制第二个形态目标的影响程度
          stem.morphTargetInfluences[1] = params.value1;
          petal.morphTargetInfluences[1] = params.value1;
        },
      });
    },
  });

  scene.add(gltf1.scene);
});

渲染器设置

配置 WebGL 渲染器:

// 初始化渲染器
const renderer = new THREE.WebGLRenderer({
  logarithmicDepthBuffer: true,
  antialias: true,
});
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;
renderer.physicallyCorrectLights = true;
renderer.setClearColor(0xcccccc, 1);
renderer.autoClear = false;
// 设置电影渲染模式
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.sortObjects = true;
renderer.logarithmicDepthBuffer = true;

// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);

控制器和动画循环

设置轨道控制器和渲染循环:

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;

// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

// 设置时钟
const clock = new THREE.Clock();
function render() {
  let time = clock.getDelta();
  if (mixer) {
    mixer.update(time);
  }
  controls.update();

  renderer.render(scene, camera);
  // 渲染下一帧的时候就会调用render函数
  requestAnimationFrame(render);
}

render();

变形动画原理详解

Morph Targets(形态目标)是一种在计算机图形学中用于实现网格变形的技术。其基本原理是:

  1. 基础几何体: 定义一个基础的网格几何体
  2. 目标几何体: 定义一个或多个"目标"几何体,它们与基础几何体有相同的拓扑结构(相同的顶点数量和连接关系),但顶点位置不同
  3. 权重控制: 通过权重值(0到1之间)来控制目标几何体对基础几何体的影响程度

在代码中,我们使用 morphTargetInfluences 数组来控制每个形态目标的影响程度:

  • morphTargetInfluences[0] = 0 时,模型呈现初始状态
  • morphTargetInfluences[0] = 1 时,模型完全变成第一个目标状态
  • morphTargetInfluences[0] = 0.5 时,模型是初始状态和目标状态的中间形态

应用场景

变形动画在 3D 应用中有广泛的应用:

  1. 角色面部表情: 实现人物的表情变化
  2. 物体形态变化: 如花朵绽放、物体变形等
  3. 动画过渡: 在不同模型状态之间平滑过渡
  4. 程序化生成: 根据参数动态改变模型形状

性能优化建议

  1. 合理使用: Morph Targets 会增加内存消耗,只在必要时使用
  2. 减少目标数量: 尽量减少形态目标的数量以提高性能
  3. 压缩模型: 使用 DRACO 等压缩技术减少模型文件大小
  4. 优化动画: 使用高效的动画库如 GSAP 来控制变形过程

总结

通过这个项目,我们学习了如何使用 Three.js 的 Morph Targets 技术:

  1. 如何加载多个具有相同拓扑结构的模型
  2. 如何将目标模型的几何体作为形态目标添加到基础模型
  3. 如何通过控制权重来实现平滑的变形动画
  4. 如何使用 GSAP 等动画库来管理复杂的动画序列

变形动画是一个强大而灵活的技术,能够为你的 3D 应用增添生动有趣的视觉效果,特别是在创建有机形态变化(如植物生长、花朵绽放等)方面特别有效。

React Fiber 架构全方位梳理

背景 作为 react 高频考点,React Fiber反复出现,为啥会成为高频考点,我觉得,很大程度上是因为 React Fiber架构真的解决了问题,并且巧妙的思想或许在未来可以给我们一些性能优化

Three.js 曲线应用详解

概述 本文将详细介绍如何使用 Three.js 中的曲线功能。我们将学习如何创建 CatmullRomCurve3 样条曲线,并利用曲线来控制物体的运动轨迹,以及如何将曲线可视化地显示在场景中。 准备
❌