先看效果,代码写的比较乱,有待优化
效果
https://linyisonger.github.io/H5.Examples/?name=./089.%E7%9C%8B%E6%98%9F%E6%98%9F%E7%9A%84%E8%88%9E%E8%80%85.html
思路
看起来很简单,实则也不是很难,就是需要思路要打开。
一开始的流程思路是
思路一
- 通过视频获取骨骼节点动画 ✔
- 使用AI文生图+骨骼节点生成人物信息 ❌ 效果不达预期
- 确定人物头部位置+序列帧动画
思路二
- 通过视频获取骨骼节点动画 ✔
- 通过骨骼动画进行canvas渲染,节点连接从而打到火柴人的效果。✔
- 确定人物头部位置+序列帧动画 ❌ 画布太大无法渲染一张图
- 确定人物头部位置+序列帧动画 + JSON存储 ✔
实现
- 通过视频播放+requestAnimationFrame获取每帧图片
- 通过@tensorflow/tfjs+@tensorflow-models/posenet来获取图片骨骼节点
- 通过canvas进行骨骼连接
这又是一篇新的内容,AI方面不是很了解,只是看着教程做的。
https://linyisonger.github.io/H5.Examples/?name=./090.%E7%81%AB%E6%9F%B4%E4%BA%BA%E7%94%9F%E6%88%90%E5%99%A8.html
上传视频后输出的JSON文件是这个示例所需要的。
里面包含每一帧的火柴人Base64图片,头像应该放置的位置。
代码
获取GitHub仓库点星星的用户列表
⚠ 当然这不是很好的写法,一旦出现报错就是死循环
/**
* 获取star的用户 默认30一页
* @author linyisonger
* @date 2025-02-18
*/
async function getStargazers(page = 1) {
const result = await fetch(`https://api.github.com/repos/linyisonger/H5.Examples/stargazers?page=${page}`)
return await result.json()
}
/**
* 获取所有star的用户
* @author linyisonger
* @date 2025-02-18
*/
async function getAllStargazers(page = 1, users = []) {
let stargazers = await getStargazers(page)
users = users.concat(stargazers)
if (stargazers.length < 30) return users
return await getAllStargazers(page + 1, users)
}
其他的感觉没什么重点
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./assets/global.css">
<style>
#container {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
height: 100vh;
align-content: center;
position: relative;
}
#container canvas {
margin-top: -140px
}
.welcome-statement {
position: absolute;
top: 100px;
font-size: 40px;
color: #999;
}
.join-us {
position: absolute;
bottom: 200px;
z-index: 100;
display: inline-flex;
padding: 0 20px 3px;
line-height: 40px;
background: linear-gradient(to bottom, rgb(87, 196, 245), rgb(26, 147, 206));
color: rgb(254, 252, 255);
cursor: pointer;
border-radius: 4px;
font-weight: bold;
box-shadow: inset 0px -3px 0 rgb(19, 98, 139);
}
.join-us:active {
opacity: .7;
box-shadow: inset 0px 0px 0 transparent;
}
.bgm-controller {
position: absolute;
right: 20px;
top: 20px;
width: 40px;
}
.bgm-controller:active {
opacity: .7;
}
</style>
</head>
<body>
<div id="container">
<!-- <audio class="bgm" muted="true">
<source src="./assets/dance/swing-dance.mp3" />
</audio> -->
<video class="bgm" muted style="display: none;">
<source src="./assets/dance/kemusan.mp4" />
</video>
<div class="welcome-statement">感谢各位给 H5.Examples 点⭐⭐~</div>
<a class="join-us" href="https://github.com/linyisonger/H5.Examples">
加入我们
</a>
<img class="bgm-controller" src="./assets/dance/bgm-c.png">
</div>
<script type="module">
/**
* 加载图
* @param {string} src
* @returns {Promise<HTMLImageElement>}
*/
function loadImage(src) {
return new Promise((resolve) => {
let image = new Image()
image.src = src;
image.onload = (ev) => {
resolve(image)
}
})
}
/**
* 加载音乐
* @param {string} src
* @returns {Promise<HTMLImageElement>}
*/
function loadAudio(src) {
return new Promise((resolve) => {
let audio = new Audio(src)
audio.addEventListener("loadeddata", resolve)
})
}
/**
* 获取star的用户 默认30一页
* @author linyisonger
* @date 2025-02-18
*/
async function getStargazers(page = 1) {
const result = await fetch(`https://api.github.com/repos/linyisonger/H5.Examples/stargazers?page=${page}`)
return await result.json()
}
/**
* 获取所有star的用户
* @author linyisonger
* @date 2025-02-18
*/
async function getAllStargazers(page = 1, users = []) {
let stargazers = await getStargazers(page)
users = users.concat(stargazers)
if (stargazers.length < 30) return users
return await getAllStargazers(page + 1, users)
}
// getAllStargazers().then((res) => {
// console.log("获取star的用户", res);
// })
let dancers = [
{
"login": "AnChangSu",
"id": 5037050,
"node_id": "MDQ6VXNlcjUwMzcwNTA=",
"avatar_url": "https://avatars.githubusercontent.com/u/5037050?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/AnChangSu",
"html_url": "https://github.com/AnChangSu",
"followers_url": "https://api.github.com/users/AnChangSu/followers",
"following_url": "https://api.github.com/users/AnChangSu/following{/other_user}",
"gists_url": "https://api.github.com/users/AnChangSu/gists{/gist_id}",
"starred_url": "https://api.github.com/users/AnChangSu/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/AnChangSu/subscriptions",
"organizations_url": "https://api.github.com/users/AnChangSu/orgs",
"repos_url": "https://api.github.com/users/AnChangSu/repos",
"events_url": "https://api.github.com/users/AnChangSu/events{/privacy}",
"received_events_url": "https://api.github.com/users/AnChangSu/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
{
"login": "HGinGitHub",
"id": 103415496,
"node_id": "U_kgDOBin-yA",
"avatar_url": "https://avatars.githubusercontent.com/u/103415496?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/HGinGitHub",
"html_url": "https://github.com/HGinGitHub",
"followers_url": "https://api.github.com/users/HGinGitHub/followers",
"following_url": "https://api.github.com/users/HGinGitHub/following{/other_user}",
"gists_url": "https://api.github.com/users/HGinGitHub/gists{/gist_id}",
"starred_url": "https://api.github.com/users/HGinGitHub/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/HGinGitHub/subscriptions",
"organizations_url": "https://api.github.com/users/HGinGitHub/orgs",
"repos_url": "https://api.github.com/users/HGinGitHub/repos",
"events_url": "https://api.github.com/users/HGinGitHub/events{/privacy}",
"received_events_url": "https://api.github.com/users/HGinGitHub/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
{
"login": "harris2012",
"id": 12846977,
"node_id": "MDQ6VXNlcjEyODQ2OTc3",
"avatar_url": "https://avatars.githubusercontent.com/u/12846977?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/harris2012",
"html_url": "https://github.com/harris2012",
"followers_url": "https://api.github.com/users/harris2012/followers",
"following_url": "https://api.github.com/users/harris2012/following{/other_user}",
"gists_url": "https://api.github.com/users/harris2012/gists{/gist_id}",
"starred_url": "https://api.github.com/users/harris2012/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/harris2012/subscriptions",
"organizations_url": "https://api.github.com/users/harris2012/orgs",
"repos_url": "https://api.github.com/users/harris2012/repos",
"events_url": "https://api.github.com/users/harris2012/events{/privacy}",
"received_events_url": "https://api.github.com/users/harris2012/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
{
"login": "Lavenir7",
"id": 105573717,
"node_id": "U_kgDOBkrtVQ",
"avatar_url": "https://avatars.githubusercontent.com/u/105573717?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Lavenir7",
"html_url": "https://github.com/Lavenir7",
"followers_url": "https://api.github.com/users/Lavenir7/followers",
"following_url": "https://api.github.com/users/Lavenir7/following{/other_user}",
"gists_url": "https://api.github.com/users/Lavenir7/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Lavenir7/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Lavenir7/subscriptions",
"organizations_url": "https://api.github.com/users/Lavenir7/orgs",
"repos_url": "https://api.github.com/users/Lavenir7/repos",
"events_url": "https://api.github.com/users/Lavenir7/events{/privacy}",
"received_events_url": "https://api.github.com/users/Lavenir7/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
{
"login": "linyisonger",
"id": 34770610,
"node_id": "MDQ6VXNlcjM0NzcwNjEw",
"avatar_url": "https://avatars.githubusercontent.com/u/34770610?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/linyisonger",
"html_url": "https://github.com/linyisonger",
"followers_url": "https://api.github.com/users/linyisonger/followers",
"following_url": "https://api.github.com/users/linyisonger/following{/other_user}",
"gists_url": "https://api.github.com/users/linyisonger/gists{/gist_id}",
"starred_url": "https://api.github.com/users/linyisonger/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/linyisonger/subscriptions",
"organizations_url": "https://api.github.com/users/linyisonger/orgs",
"repos_url": "https://api.github.com/users/linyisonger/repos",
"events_url": "https://api.github.com/users/linyisonger/events{/privacy}",
"received_events_url": "https://api.github.com/users/linyisonger/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
{
"login": "lpleipeng",
"id": 39250004,
"node_id": "MDQ6VXNlcjM5MjUwMDA0",
"avatar_url": "https://avatars.githubusercontent.com/u/39250004?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/lpleipeng",
"html_url": "https://github.com/lpleipeng",
"followers_url": "https://api.github.com/users/lpleipeng/followers",
"following_url": "https://api.github.com/users/lpleipeng/following{/other_user}",
"gists_url": "https://api.github.com/users/lpleipeng/gists{/gist_id}",
"starred_url": "https://api.github.com/users/lpleipeng/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/lpleipeng/subscriptions",
"organizations_url": "https://api.github.com/users/lpleipeng/orgs",
"repos_url": "https://api.github.com/users/lpleipeng/repos",
"events_url": "https://api.github.com/users/lpleipeng/events{/privacy}",
"received_events_url": "https://api.github.com/users/lpleipeng/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
{
"login": "xxxggg-ctrl",
"id": 63829555,
"node_id": "MDQ6VXNlcjYzODI5NTU1",
"avatar_url": "https://avatars.githubusercontent.com/u/63829555?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/xxxggg-ctrl",
"html_url": "https://github.com/xxxggg-ctrl",
"followers_url": "https://api.github.com/users/xxxggg-ctrl/followers",
"following_url": "https://api.github.com/users/xxxggg-ctrl/following{/other_user}",
"gists_url": "https://api.github.com/users/xxxggg-ctrl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/xxxggg-ctrl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/xxxggg-ctrl/subscriptions",
"organizations_url": "https://api.github.com/users/xxxggg-ctrl/orgs",
"repos_url": "https://api.github.com/users/xxxggg-ctrl/repos",
"events_url": "https://api.github.com/users/xxxggg-ctrl/events{/privacy}",
"received_events_url": "https://api.github.com/users/xxxggg-ctrl/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
}
]
let bgmControllerDom = document.querySelector('.bgm-controller')
bgmControllerDom.addEventListener("click", () => {
const bgm = document.body.querySelector('.bgm')
bgm.muted = !bgm.muted;
bgmControllerDom.setAttribute('src', bgm.muted ? './assets/dance/bgm-c.png' : './assets/dance/bgm-o.png')
})
// 2D火柴人 贴图
function fetchLoad(url) {
return new Promise((resolve) => {
fetch(url).then((response) => response.json()).then(resolve)
})
}
async function initGame() {
dancers = await getAllStargazers()
let dance = await fetchLoad("./assets/dance/kemusan.json")
const DROP_FRAME = 5 // 抽帧
const ZOOM_OUT = .5
// 检查动作信息
// for (let i = 0; i < dance.frames.length; i++) {
// console.log(i);
// const frame = dance.frames[i];
// const img = document.createElement('img')
// img.src = frame.url;
// document.body.appendChild(img)
// }
let danceCvsList = []
for (let i = 0; i < dancers.length; i++) {
const dancer = dancers[i];
let danceCvs = await createCanvas(dancer)
danceCvsList.push({
dancer,
cvs: danceCvs
})
}
let i = 0
async function animationFrame() {
if (i % DROP_FRAME == 0) {
for (let j = 0; j < danceCvsList.length; j++) {
const { cvs, dancer } = danceCvsList[j];
await drawFrame(cvs, dance.frames[(i / DROP_FRAME) % dance.frames.length], dancer.avatar_url)
}
}
requestAnimationFrame(animationFrame)
i++;
}
await animationFrame()
document.body.querySelector('.bgm').play()
document.body.querySelector('.bgm').loop = true;
/**
* 创建一个用户
* @author linyisonger
* @date 2025-02-23
*/
function createCanvas(dancer) {
let avatarUrl = dancer.avatar_url
let cvs = document.createElement("canvas")
cvs.setAttribute('width', dance.width)
cvs.setAttribute('height', dance.height)
cvs.width = dance.width * ZOOM_OUT;
cvs.height = dance.height * ZOOM_OUT;
document.body.querySelector("#container").appendChild(cvs)
return cvs
}
/**
* 渲染一帧
* @author linyisonger
* @date 2025-02-23
*/
async function drawFrame(cvs, frame, avatar) {
/** @type {CanvasRenderingContext2D } */
let ctx = cvs.getContext('2d')
let roleImg = await loadImage(frame.url)
let avatarImg = await loadImage(avatar)
ctx.clearRect(0, 0, cvs.width, cvs.height)
ctx.drawImage(roleImg, 0, 0, cvs.width, cvs.height)
let avatarWidth = 40 * ZOOM_OUT
ctx.drawImage(avatarImg, (frame.avatar.x * ZOOM_OUT - avatarWidth / 2), (frame.avatar.y * ZOOM_OUT - avatarWidth / 2), avatarWidth, avatarWidth)
}
}
initGame()
</script>
</body>
</html>
源码仓库
更新的话文章可能不一定会更新,仓库会可能更新,有问题可以提issue~
https://github.com/linyisonger/H5.Examples