refactor: removed fallback image processing and updated portrait image processing

This commit is contained in:
Haoyu Xu
2025-11-16 17:05:28 +08:00
parent ea0bd20211
commit 2a99186867
13 changed files with 138 additions and 346 deletions

View File

@@ -120,7 +120,7 @@ export default function Home() {
const list = navigationList.filter((item) => { const list = navigationList.filter((item) => {
return ( return (
item.name.toLowerCase().indexOf(searchField.toLowerCase()) !== item.name.toLowerCase().indexOf(searchField.toLowerCase()) !==
-1 || item.type === 'date' -1 || item.type === 'date'
) )
}) })
const newList = [] const newList = []
@@ -222,7 +222,7 @@ export default function Home() {
} }
viewBox={ viewBox={
entry.type === entry.type ===
'operator' 'operator'
? '0 0 88.969 71.469' ? '0 0 88.969 71.469'
: '0 0 94.563 67.437' : '0 0 94.563 67.437'
} }
@@ -234,25 +234,24 @@ export default function Home() {
} }
> >
{language === {language ===
'zh-CN' 'zh-CN'
? entry.type === ? entry.type ===
'skin' 'skin'
? `${ ? `${entry
entry .skinName[
.skinName[ 'zh-CN'
'zh-CN' ]
] } · ${entry.operatorName}`
} · ${entry.operatorName}`
: entry.operatorName : entry.operatorName
: entry : entry
.skinName[ .skinName[
'en-US' 'en-US'
]} ]}
</section> </section>
<section <section
className={ className={
classes[ classes[
'arrow-icon' 'arrow-icon'
] ]
} }
> >
@@ -369,9 +368,9 @@ function OperatorElement({ item, hidden, handleVoicePlay }) {
<span className={classes.text}> <span className={classes.text}>
{ {
item.codename[ item.codename[
language.startsWith('en') language.startsWith('en')
? alternateLang ? alternateLang
: textDefaultLang : textDefaultLang
] ]
} }
</span> </span>
@@ -430,13 +429,13 @@ function ImageElement({ item }) {
const { language } = useLanguage() const { language } = useLanguage()
return ( return (
<img <img
src={`/${buildConfig.directory_folder}/${buildConfig.portraits}/${item.fallback_name.replace(/#/g, '%23')}_portrait.png`} src={`/${buildConfig.directory_folder}/${buildConfig.portraits}/${item.portrait_filename.replace(/#/g, '%23')}.png`}
alt={item.codename[language]} alt={item.codename[language]}
/> />
) )
} }
ImageElement.propTypes = { ImageElement.propTypes = {
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
fallback_name: PropTypes.string, portrait_filename: PropTypes.string,
codename: PropTypes.object, codename: PropTypes.object,
} }

View File

@@ -1,10 +1,8 @@
import Background from '@/components/background' import Background from '@/components/background'
import Events from '@/components/events' import Events from '@/components/events'
import Fallback from '@/components/fallback'
import { import {
addEventListeners, addEventListeners,
insertHTMLChild, insertHTMLChild,
isWebGLSupported,
updateElementPosition, updateElementPosition,
} from '@/components/helper' } from '@/components/helper'
import Insight from '@/components/insight' import Insight from '@/components/insight'
@@ -42,11 +40,7 @@ export default class AKLive2D {
this.#background = new Background(this.#appEl) this.#background = new Background(this.#appEl)
this.#voice = new Voice(this.#appEl) this.#voice = new Voice(this.#appEl)
this.#music = new Music(this.#appEl) this.#music = new Music(this.#appEl)
if (isWebGLSupported()) { this.#player = new Player(this.#appEl)
this.#player = new Player(this.#appEl)
} else {
new Fallback(this.#appEl)
}
addEventListeners([ addEventListeners([
{ {
event: Events.Player.Ready.name, event: Events.Player.Ready.name,

View File

@@ -1,16 +0,0 @@
#fallback-box {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
#fallback {
margin: auto;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
width: 100%;
height: 100%;
}

View File

@@ -1,40 +0,0 @@
import { insertHTMLChild } from '@/components/helper'
import '@/components/fallback.css'
import buildConfig from '!/config.json'
export default class Fallback {
#el = document.createElement('div')
constructor(parentEl) {
alert('WebGL is unavailable. Fallback image will be used.')
const calculateScale = (width, height) => {
return {
x: window.innerWidth / width,
y: window.innerHeight / height,
}
}
const fallback = () => {
const el = document.getElementById('fallback-container')
const scale = calculateScale(
buildConfig.image_width,
buildConfig.image_height
)
el.style.width = '100%'
el.style.height =
buildConfig.image_height *
(scale.x > scale.y ? scale.y : scale.x) +
'px'
}
window.addEventListener('resize', fallback, true)
this.#el.id = 'fallback-box'
this.#el.innerHTML = `
<div id="fallback-container">
<div id="fallback"
style="background-image: url(${import.meta.env.BASE_URL}${buildConfig.default_assets_dir}${buildConfig.fallback_name}.png)"
/>
</div>
`
insertHTMLChild(parentEl, this.#el)
fallback()
}
}

View File

@@ -1,15 +1,3 @@
export const isWebGLSupported = () => {
try {
const canvas = document.createElement('canvas')
const ctx =
canvas.getContext('webgl') ||
canvas.getContext('experimental-webgl')
return ctx != null
} catch {
return false
}
}
export const insertHTMLChild = (parent, child) => { export const insertHTMLChild = (parent, child) => {
parent.appendChild(child) parent.appendChild(child)
} }

View File

@@ -1,12 +1,12 @@
dynchars: dynchars dynchars: dynchars
item_to_download: item_to_download:
- homebackground/wrapper - homebackground/wrapper
- ui_camp_logo - ui_camp_logo
- charportraits - voice.*/extra
- voice.*/extra - char_128
- char_128 - spritepack/char_portrait.*
additional_regex: additional_regex:
- ^(?!(avg|charpack))(.*)$ - ^(?!(avg|charpack))(.*)$
servers: servers:
- name: cn - name: cn
url: https://ak-conf.hypergryph.com/config/prod/official/network_config url: https://ak-conf.hypergryph.com/config/prod/official/network_config

View File

@@ -36,6 +36,7 @@ const download = async (
const lpacksRes: Response = await fetch( const lpacksRes: Response = await fetch(
`${urls.hu}/Android/assets/${version}/hot_update_list.json` `${urls.hu}/Android/assets/${version}/hot_update_list.json`
) )
console.log(urls, lpacksRes)
const updateList: UpdateList = await lpacksRes.json() const updateList: UpdateList = await lpacksRes.json()
const itemToDownload: Set<ItemToDownload> = new Set(config.item_to_download) const itemToDownload: Set<ItemToDownload> = new Set(config.item_to_download)
updateList.abInfos.map((item: AbInfosItem) => { updateList.abInfos.map((item: AbInfosItem) => {

View File

@@ -1,109 +1,106 @@
site_id: aklive2d site_id: aklive2d
total_size: 26214400 # 1024 * 1024 * 25 total_size: 26214400 # 1024 * 1024 * 25
akassets: akassets:
project_name: akassets project_name: akassets
url: https://akassets.pages.dev url: https://akassets.pages.dev
insight: insight:
id: aklive2d id: aklive2d
url: https://insight.halyul.dev/on-demand.js url: https://insight.halyul.dev/on-demand.js
module: module:
assets: assets:
config_yaml: config.yaml config_yaml: config.yaml
background: background background: background
music: music music: music
charword_table: charword_table charword_table: charword_table
project_json: project_json project_json: project_json
background: background:
operator_bg_png: operator_bg.png operator_bg_png: operator_bg.png
charword_table: charword_table:
charword_table_json: charword_table.json charword_table_json: charword_table.json
music: music:
music_table_json: music_table.json music_table_json: music_table.json
display_meta_table_json: display_meta_table.json display_meta_table_json: display_meta_table.json
audio_data_json: audio_data.json audio_data_json: audio_data.json
official_info: official_info:
official_info_json: official_info.json official_info_json: official_info.json
operator: operator:
operator: operator operator: operator
config: config config: config
template_yaml: _template.yaml template_yaml: _template.yaml
config_yaml: config.yaml config_yaml: config.yaml
portraits: _portraits logos_assets: _logos
logos_assets: _logos logos: logos
logos: logos directory_assets: _directory
directory_assets: _directory character_table_json: character_table.json
MonoBehaviour: MonoBehaviour skin_table_json: skin_table.json
Texture2D: Texture2D title:
character_table_json: character_table.json zh-CN: "明日方舟:"
skin_table_json: skin_table.json en-US: "Arknights: "
title: sp_filename_prefix: sp_
zh-CN: '明日方舟:' sp_title:
en-US: 'Arknights: ' zh-CN: "「SP」 "
sp_filename_prefix: sp_ en-US: "[SP] "
sp_title: project_json:
zh-CN: '「SP」 ' project_json: project.json
en-US: '[SP] ' preview_jpg: preview.jpg
project_json: template_yaml: project_json.yaml
project_json: project.json wrangler:
preview_jpg: preview.jpg index_json: index.json
template_yaml: project_json.yaml vite_helpers:
wrangler: config_json: config.json
index_json: index.json
vite_helpers:
config_json: config.json
app: app:
showcase: showcase:
public: public public: public
assets: assets assets: assets
release: release release: release
directory: directory:
assets: _assets assets: _assets
title: AKLive2D title: AKLive2D
voice: jp/CN_037.ogg voice: jp/CN_037.ogg
portraits: portraits portraits: portraits
error: error:
files: files:
- key: build_char_128_plosis_epoque#3 - key: build_char_128_plosis_epoque#3
paddings: paddings:
left: -120 left: -120
right: 150 right: 150
top: 10 top: 10
bottom: 0 bottom: 0
- key: build_char_128_plosis - key: build_char_128_plosis
paddings: paddings:
left: -90 left: -90
right: 100 right: 100
top: 10 top: 10
bottom: 0 bottom: 0
- key: build_char_128_plosis_yun#4 - key: build_char_128_plosis_yun#4
paddings: paddings:
left: -90 left: -90
right: 100 right: 100
top: 10 top: 10
bottom: 0 bottom: 0
voice: voice:
file: CN_034.ogg file: CN_034.ogg
target: error.ogg target: error.ogg
dir_name: dir_name:
data: data data: data
dist: dist dist: dist
extracted: extracted extracted: extracted
auto_update: auto_update auto_update: auto_update
voice: voice:
main: voice main: voice
sub: sub:
- name: jp - name: jp
lang: JP lang: JP
lookup_region: zh_CN lookup_region: zh_CN
- name: cn - name: cn
lang: CN_MANDARIN lang: CN_MANDARIN
lookup_region: zh_CN lookup_region: zh_CN
- name: en - name: en
lang: EN lang: EN
lookup_region: en_US lookup_region: en_US
- name: kr - name: kr
lang: KR lang: KR
lookup_region: ko_KR lookup_region: ko_KR
- name: custom - name: custom
lang: CUSTOM lang: CUSTOM
lookup_region: zh_CN lookup_region: zh_CN

View File

@@ -41,12 +41,9 @@ export type Config = {
config: string config: string
template_yaml: string template_yaml: string
config_yaml: string config_yaml: string
portraits: string
logos_assets: string logos_assets: string
logos: string logos: string
directory_assets: string directory_assets: string
MonoBehaviour: string
Texture2D: string
character_table_json: string character_table_json: string
skin_table_json: string skin_table_json: string
title: TitleLanguages title: TitleLanguages

View File

@@ -162,7 +162,6 @@ const generateMapping = () => {
type === 'skin' type === 'skin'
? skinEntry.skinId.replace(/@/, '_') ? skinEntry.skinId.replace(/@/, '_')
: `${skinEntry.charId}_2` : `${skinEntry.charId}_2`
operator.fallback_name = `${operator.portrait_filename}${operator.isSP ? '_sp' : ''}`
const regions = Object.keys( const regions = Object.keys(
operator.codename operator.codename

View File

@@ -1,12 +1,11 @@
import path from 'node:path' import path from 'node:path'
import config from '@aklive2d/config' import config from '@aklive2d/config'
import { alphaComposite, file } from '@aklive2d/libs' import { file } from '@aklive2d/libs'
import operators, { import operators, {
DIST_DIR, DIST_DIR,
generateAssetsJson, generateAssetsJson,
OPERATOR_SOURCE_FOLDER, OPERATOR_SOURCE_FOLDER,
} from '../index.ts' } from '../index.ts'
import type { PortraitHub, PortraitJson } from '../types.ts'
import { getDistFolder, getExtractedFolder } from './utils.ts' import { getDistFolder, getExtractedFolder } from './utils.ts'
export const build = async (namesToBuild: string[]) => { export const build = async (namesToBuild: string[]) => {
@@ -39,75 +38,10 @@ const generateAssets = async (name: string) => {
file.rmdir(outDir) file.rmdir(outDir)
file.mkdir(outDir) file.mkdir(outDir)
const fallback_name = operators[name].fallback_name const portraitFilename = `${operators[name].portrait_filename}.png`
const fallbackFilename = `${fallback_name}.png` await file.copy(
const alphaCompositeFilename = `${path.parse(fallbackFilename).name}[alpha].png` path.join(extractedDir, portraitFilename),
if (file.exists(path.join(extractedDir, alphaCompositeFilename))) { path.join(getDistFolder(name), portraitFilename)
const fallbackBuffer = await alphaComposite.process(
fallbackFilename,
alphaCompositeFilename,
extractedDir
)
file.writeSync(
fallbackBuffer,
path.join(getDistFolder(name), fallbackFilename)
)
} else {
await file.copy(
path.join(extractedDir, fallbackFilename),
path.join(getDistFolder(name), fallbackFilename)
)
}
// generate portrait
const portraitDir = path.join(
OPERATOR_SOURCE_FOLDER,
config.module.operator.portraits
)
const portraitHubContent = file.readSync(
path.join(
portraitDir,
config.module.operator.MonoBehaviour,
'portrait_hub.json'
)
) as string
if (!portraitHubContent) throw new Error('portrait_hub.json not found')
const portraitHub: PortraitHub = JSON.parse(portraitHubContent)
const portrait_filename_lowerCase =
operators[name].portrait_filename.toLowerCase()
const portraitItem = portraitHub._sprites.find(
(item) => item.name.toLowerCase() === portrait_filename_lowerCase
)
if (!portraitItem) throw new Error(`portrait ${fallback_name} not found`)
const portraitAtlas = portraitItem.atlas
const portraitJsonText = file.readSync(
path.join(
portraitDir,
config.module.operator.MonoBehaviour,
`portraits#${portraitAtlas}.json`
)
) as string
if (!portraitJsonText)
throw new Error(`portrait ${fallback_name} json not found`)
const portraitJson: PortraitJson = JSON.parse(portraitJsonText)
const item = portraitJson._sprites.find(
(item) => item.name.toLowerCase() === portrait_filename_lowerCase
)
if (!item) throw new Error(`portrait ${fallback_name} not found`)
const rect = {
...item.rect,
rotate: item.rotate,
}
const protraitFilename = `portraits#${portraitAtlas}.png`
const portraitBuffer = await alphaComposite.process(
protraitFilename,
`${path.parse(protraitFilename).name}a.png`,
path.join(portraitDir, config.module.operator.Texture2D)
)
const croppedBuffer = await alphaComposite.crop(portraitBuffer, rect)
file.writeSync(
croppedBuffer,
path.join(getDistFolder(name), `${fallback_name}_portrait.png`)
) )
await generateAssetsJson( await generateAssetsJson(

View File

@@ -5,7 +5,6 @@ export type OperatorEntryType = 'operator' | 'skin'
export interface OperatorConfig { export interface OperatorConfig {
filename: string filename: string
logo: string logo: string
fallback_name: string
portrait_filename: string portrait_filename: string
viewport_left: number // should be default to 0 in the future viewport_left: number // should be default to 0 in the future
viewport_right: number viewport_right: number
@@ -29,69 +28,6 @@ export type Config = {
[name: string]: OperatorConfig [name: string]: OperatorConfig
} }
type FileIDPathID = {
m_FileID: number
m_PathID: number
}
export type PortraitHub = {
m_GameObject: FileIDPathID
m_Enabled: number
m_Script: FileIDPathID
m_Name: string
_sprites: {
name: string
atlas: number
}[]
_atlases: string[]
_inputSpriteDir: string
_outputAtlasDir: string
_rootAtlasName: string
_spriteSize: {
width: number
height: number
}
_cntPerAtlas: number
_maxAtlasSize: number
}
type SignedItem = {
name: string
guid: string
md5: string
}
export type PortraitJson = {
m_GameObject: FileIDPathID
m_Enabled: number
m_Script: FileIDPathID
m_Name: string
_sprites: {
name: string
guid: string
atlas: number
rect: {
x: number
y: number
w: number
h: number
}
rotate: number
}[]
_atlas: {
index: number
texture: FileIDPathID
alpha: FileIDPathID
size: number
}
_index: number
_sign: {
m_sprites: SignedItem[]
m_atlases: SignedItem[]
m_alphas: SignedItem[]
}
}
export type AssetsJson = { export type AssetsJson = {
filename: string filename: string
path?: string path?: string

View File

@@ -76,7 +76,7 @@ export const copyShowcaseData = (
}, },
{ {
fn: file.symlink, fn: file.symlink,
filename: `${operators[name].fallback_name}.png`, filename: `${operators[name].portrait_filename}.png`,
source: path.resolve( source: path.resolve(
ASSETS_DIST_DIR, ASSETS_DIST_DIR,
config.module.operator.operator, config.module.operator.operator,
@@ -129,7 +129,10 @@ export const copyShowcaseData = (
link: operators[name].link, link: operators[name].link,
filename: filename.replace(/#/g, '%23'), filename: filename.replace(/#/g, '%23'),
logo_filename: operators[name].logo, logo_filename: operators[name].logo,
fallback_filename: operators[name].fallback_name.replace(/#/g, '%23'), portrait_filename: operators[name].portrait_filename.replace(
/#/g,
'%23'
),
viewport_left: operators[name].viewport_left, viewport_left: operators[name].viewport_left,
viewport_right: operators[name].viewport_right, viewport_right: operators[name].viewport_right,
viewport_top: operators[name].viewport_top, viewport_top: operators[name].viewport_top,
@@ -319,7 +322,7 @@ export const copyDirectoryData = async ({
] ]
operatorFilesToCopy.map((key) => { operatorFilesToCopy.map((key) => {
const portraitName = `${operators[key].fallback_name}_portrait.png` const portraitName = `${operators[key].portrait_filename}.png`
filesToCopy.push({ filesToCopy.push({
src: path.join( src: path.join(
sourceFolder, sourceFolder,