Skip to content

Commit

Permalink
perf: steam状态可用canvas渲染 包括gif
Browse files Browse the repository at this point in the history
  • Loading branch information
XasYer committed Jan 6, 2025
1 parent 63d0c05 commit 92272b1
Show file tree
Hide file tree
Showing 13 changed files with 365 additions and 161 deletions.
11 changes: 3 additions & 8 deletions apps/info.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import moment from 'moment'
import { segment } from '#lib'
import { App, Config, Render } from '#components'
import { db, utils, api } from '#models'
import { App, Config, Render } from '#components'

const appInfo = {
id: 'info',
Expand Down Expand Up @@ -35,7 +35,7 @@ const rule = {
return true
}
if (Config.other.infoMode == 2) {
const color = info.gameid ? 1 : info.personastate === 0 ? 3 : 2
const color = info.gameid ? '#90ba3c' : info.personastate === 0 ? '#898989' : '#57cbde'
const bg = await api.IPlayerService.GetProfileItemsEquipped(steamId)
const avatar = utils.steam.getStaticUrl(bg.animated_avatar.image_small) || info.avatarfull
const options = {
Expand All @@ -52,17 +52,12 @@ const rule = {
country: info.loccountrycode ? getLoccountryCode(info.loccountrycode) : '',
color,
scale: 1.2,
pageGotoParams: {
waitUntil: 'load'
},
tempName: steamId,
backgroundWebm: utils.steam.getStaticUrl(bg.mini_profile_background.movie_webm),
toGif: Config.gif.infoGif
}

const target = Config.gif.infoGif ? 'renderGif' : 'render'

const img = await Render[target]('info/index', options)
const img = await Render.render('info/index', options)
await e.reply(img)
} else {
const avatarBuffer = Config.other.steamAvatar ? await utils.getImgUrlBuffer(info.avatarfull) : ''
Expand Down
124 changes: 20 additions & 104 deletions components/Render.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import fs from 'fs'
import _ from 'lodash'
import { join } from 'path'
import { canvas } from '#models'
import { puppeteer } from '#lib'
import template from 'art-template'
import { execSync } from 'child_process'
import { canvas, info } from '#models'
import { Version, Config } from '#components'
import { logger, puppeteer, segment } from '#lib'

function scale (pct = 1) {
const scale = Math.min(2, Math.max(0.5, Config.other.renderScale / 100))
Expand Down Expand Up @@ -61,6 +60,18 @@ const Render = {
} else {
return this.simpleRender(path, params)
}
} else if (path === 'info/index') {
if (params.toGif) {
try {
return info.gif.render(data)
} catch (error) {
const tempPath = join(Version.pluginPath, 'temp', String(data.tempName || Date.now())).replace(/\\/g, '/')
fs.rmdirSync(tempPath, { recursive: true })
throw error
}
} else if (Config.other.renderType == 2) {
return await canvas.info.render(data)
}
}
const img = await puppeteer.screenshot(`${Version.pluginName}/${path}`, data)
if (img) {
Expand Down Expand Up @@ -88,108 +99,13 @@ const Render = {
return '制作图片出错辣!再试一次吧'
}
},
/**
* 渲染gif 需要传入tempName参数 用于存放临时文件 建议使用唯一id 比如steamId
* @param {string} path
* @param {{
* tempName: string,
* [key: string]: any
* }} params
*/
async renderGif (path, params) {
if (Version.BotName === 'Karin') {
throw new Error('暂不支持karin渲染gif')
}

const tempPath = join(Version.pluginPath, 'temp', String(params.tempName || Date.now())).replace(/\\/g, '/')
if (fs.existsSync(tempPath)) {
fs.rmSync(tempPath, { force: true, recursive: true })
}
fs.mkdirSync(tempPath, { recursive: true })

path = path.replace(/.html$/, '')
const layoutPath = join(Version.pluginPath, 'resources', 'common', 'layout')
const data = {
tplFile: `${Version.pluginPath}/resources/${path}.html`,
pluResPath: `${Version.pluginPath}/resources/`,
defaultLayout: join(layoutPath, 'default.html'),
sys: {
scale: scale(params.scale || 1),
copyright: params.copyright || `Created By <span class="version"> ${Version.BotName} v${Version.BotVersion} </span> & <span class="version"> ${Version.pluginName} v${Version.pluginVersion} </span>`
},
...params
}
const tplPath = tplFile(path, data, tempPath)
if (!puppeteer.browser) {
await puppeteer.browserInit()
}
const page = await puppeteer.browser.newPage()
const output = `${tempPath}/output.gif`
const fps = Math.abs(Config.gif.frameRate || 20)
try {
await page.goto(`file://${tplPath}`)

const body = await page.$('#container') || await page.$('body')

if (Config.gif.gifMode == 2) {
const { PuppeteerScreenRecorder } = await import('puppeteer-screen-recorder')
const boundingBox = await body.boundingBox()

page.setViewport({
width: Math.round(boundingBox.width),
height: Math.round(boundingBox.height)
})

const recorder = new PuppeteerScreenRecorder(page, {
fps,
followNewTab: false
})

const input = `${tempPath}/output.mp4`

await recorder.start(input)
const sleep = Math.abs(Config.gif.videoLimit || 3)
await new Promise(resolve => setTimeout(resolve, sleep * 1000))

await recorder.stop()
execSync(`ffmpeg -i ${input} "${output}"`)
} else {
const sleep = Math.abs(Config.gif.frameSleep || 50)
const count = Math.abs(Config.gif.frameCount || 30)

const task = []
for (let i = 1; i < count; i++) {
await new Promise(resolve => setTimeout(resolve, sleep))
task.push(
body.screenshot({
path: `${tempPath}/${i}.jpeg`,
type: 'jpeg'
})
)
}
await Promise.all(task)

execSync(`ffmpeg -framerate ${fps} -i "${tempPath}/%d.jpeg" "${output}"`)
}
page.close().catch((err) => logger.error(err))
} catch (error) {
page.close().catch((err) => logger.error(err))
// 仅用于关闭页面
throw error
}
setTimeout(() => {
fs.rmSync(tempPath, { force: true, recursive: true })
}, 1000 * 60 * 5) // 5分钟后删除
return segment.image(`file://${output}`)
tplFile (path, params, tempPath) {
const name = path.split('/').pop()
const tplPath = join(tempPath, name + '.html')
const tmp = template.render(fs.readFileSync(params.tplFile, 'utf-8'), params)
fs.writeFileSync(tplPath, tmp)
return tplPath.replace(/\\/g, '/')
}
}

function tplFile (path, params, tempPath) {
const name = path.split('/').pop()
const tplPath = join(tempPath, name + '.html')
const tmp = template.render(fs.readFileSync(params.tplFile, 'utf-8'), params)
fs.writeFileSync(tplPath, tmp)
return tplPath.replace(/\\/g, '/')
}

export default Render
18 changes: 18 additions & 0 deletions models/canvas/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const hasCanvas = !!canvasPKG

const startTimeMap = new Map()

export const loadImage = hasCanvas && canvasPKG.loadImage

export function createCanvas (width, height) {
if (!hasCanvas) throw new Error('请先pnpm i 安装依赖')
const canvas = canvasPKG.createCanvas(width, height)
Expand Down Expand Up @@ -54,6 +56,22 @@ export function toImage (canvas) {
}
}

/**
* 缩短文本
* @param {import('@napi-rs/canvas').SKRSContext2D} ctx
* @param {string} text
* @param {number} maxWidth
* @param {string} replace
*/
export function shortenText (ctx, text, maxWidth, replace = '...') {
if (!text) return ''
if (ctx.measureText(text).width <= maxWidth) return text
while (ctx.measureText(text).width > maxWidth) {
text = text.slice(0, -1)
}
return text + replace
}

export function drawBackgroundColor (ctx, color, x, y, width, height, radius) {
ctx.beginPath()
const backgroundX = x - 5
Expand Down
22 changes: 3 additions & 19 deletions models/canvas/game.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { join } from 'path'
import { canvasPKG, createCanvas, toImage } from './canvas.js'
import { Version } from '#components'
import { loadImage, createCanvas, toImage, shortenText } from './canvas.js'

export async function render (data) {
const { loadImage } = canvasPKG

const bg = await loadImage(join(Version.pluginPath, 'resources', 'game', 'game.png'))

const { ctx, canvas } = createCanvas(bg.width, bg.height * data.length)
Expand Down Expand Up @@ -32,14 +30,7 @@ export async function render (data) {
ctx.font = '19px MiSans'
ctx.fillStyle = '#e3ffc2'

let nickname = i.isAvatar ? i.name : i.appid

if (ctx.measureText(nickname).width > 300) {
while (ctx.measureText(nickname).width > 300) {
nickname = nickname.slice(0, -1)
}
nickname += ' ...'
}
const nickname = shortenText(ctx, i.isAvatar ? i.name : i.appid, 300)

x += 85
y += 15
Expand All @@ -51,14 +42,7 @@ export async function render (data) {
ctx.fillStyle = '#969696'
ctx.fillText(i.type === 'end' ? '结束玩' : '正在玩', x, y)

let name = i.name

if (ctx.measureText(name).width > 357) {
while (ctx.measureText(name).width > 357) {
name = name.slice(0, -1)
}
name += ' ...'
}
const name = shortenText(ctx, i.name, 357)

y += 25
ctx.font = '14px Bold'
Expand Down
1 change: 1 addition & 0 deletions models/canvas/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * as game from './game.js'
export * as info from './info.js'
export * as inventory from './inventory.js'
Loading

0 comments on commit 92272b1

Please sign in to comment.