Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0531566
chore: add new directory for PsdCharacter
aqiq-marine Mar 7, 2026
a226a1f
chore: add index.ts
aqiq-marine Mar 7, 2026
3a32d9b
feature: add ast
aqiq-marine Mar 7, 2026
d89cce5
feature: add psd character component
aqiq-marine Mar 7, 2026
97b4747
fix: export components
aqiq-marine Mar 7, 2026
ed7ec45
fix: fix DeclareVariable
aqiq-marine Mar 7, 2026
e5b9068
feature: add parser
aqiq-marine Mar 8, 2026
519773c
fix: fix fetch url
aqiq-marine Mar 8, 2026
7dabdd9
feature: add runtime
aqiq-marine Mar 8, 2026
186f19a
feature: add voice volume tune
aqiq-marine Mar 8, 2026
07926ce
feature: fix voice volume
aqiq-marine Mar 8, 2026
8653b8f
fix: fix register cache
aqiq-marine Mar 8, 2026
e4cd0fd
feature: add blink
aqiq-marine Mar 8, 2026
8dc4c22
fix: performance tune
aqiq-marine Mar 8, 2026
d658b9d
fix: animation error
aqiq-marine Mar 8, 2026
62f2452
fix: add package for psd
aqiq-marine Mar 8, 2026
88bf1e0
add: add comment
aqiq-marine Mar 8, 2026
0a0c5c3
chore: delete duplicated package
aqiq-marine Mar 8, 2026
2eb640b
fix: duplicated register
aqiq-marine Mar 10, 2026
fcbae2a
fix: add className
aqiq-marine Mar 12, 2026
a2358da
fix: add sound props
aqiq-marine Mar 13, 2026
2236022
init character manager branch
aqiq-marine Mar 14, 2026
f2143a5
feature: add ast
aqiq-marine Mar 14, 2026
3ee8456
feature: add character-manager-component
aqiq-marine Mar 14, 2026
139c413
feature: add dialogSenario
aqiq-marine Mar 14, 2026
94a14bc
fix: type validate
aqiq-marine Mar 14, 2026
43392a8
fix: add className
aqiq-marine Mar 14, 2026
cdec21e
feature: non-speaker component in chapter
aqiq-marine Mar 15, 2026
5642b44
fix: render first frame
aqiq-marine Mar 15, 2026
51aa617
fix: export manager component
aqiq-marine Mar 15, 2026
d7df7a3
fix: lint
aqiq-marine Mar 15, 2026
0286cf5
feature: add option for merge implicit characters
aqiq-marine Mar 15, 2026
77bf9ad
fix: type DeclareVariable
aqiq-marine Mar 20, 2026
bf11c88
feature: add declareVariables
aqiq-marine Mar 20, 2026
51c0f66
fix: type AnimationContext
aqiq-marine Mar 20, 2026
3bb8604
fix: delete debug log
aqiq-marine Mar 20, 2026
53038b5
fix: await animation
aqiq-marine Mar 21, 2026
a68930e
feature: type variables value
aqiq-marine Mar 21, 2026
edc5b0e
fix: rename animation definition
aqiq-marine Mar 21, 2026
48504cf
fix: add comments
aqiq-marine Mar 21, 2026
eb67150
change: rename Block to MotionClip
aqiq-marine Mar 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
372 changes: 240 additions & 132 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@ffprobe-installer/ffprobe": "^2.1.2",
"@types/opentype.js": "^1.3.4",
"ag-psd": "^30.1.0",
"ag-psd-psdtool": "^1.1.10",
"mathjax-full": "^3.2.1",
"opentype.js": "^1.3.4",
"prismjs": "^1.30.0",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ type MoveController<T> = {
to: (value: T, durationFrames: number, easing?: Easing) => AnimationHandle
}

type AnimationContext = {
export type AnimationContext = {
sleep: (frames: number) => AnimationHandle
waitUntil: (frame: number) => AnimationHandle
waitUntilClip: (label: string) => AnimationHandle
Expand Down
56 changes: 56 additions & 0 deletions src/lib/character/character-manager/ast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { ReactNode } from "react"

export const CharacterManagerElement = {
CharacterManager: "CharacterManager",
DeclareCharacters: "DeclareCharacters",
Senario: "Senario",
DeclareCharacter: "DeclareCharacter",
Chapter: "Chapter",
Speaker: "Speaker",
} as const

export type ChapterChild =
| { kind: "speaker", node: SpeakerNode }
| { kind: "other", node: ReactNode }


/* =========================
Nodes
========================= */

export interface CharacterManagerNode {
type: typeof CharacterManagerElement.CharacterManager
characters: DeclareCharactersNode
senario: SenarioNode
}

export interface DeclareCharactersNode {
type: typeof CharacterManagerElement.DeclareCharacters
children: DeclareCharacterNode[]
}

export interface SenarioNode {
type: typeof CharacterManagerElement.Senario
children: ChapterNode[]
}

export interface DeclareCharacterNode {
type: typeof CharacterManagerElement.DeclareCharacter
idleClassName?: string
speakingClassName?: string
name: string
psd: string
children: ReactNode
}

export interface ChapterNode {
type: typeof CharacterManagerElement.Chapter
children: ChapterChild[]
}

export interface SpeakerNode {
type: typeof CharacterManagerElement.Speaker
className?: string
name: string
children: ReactNode
}
56 changes: 56 additions & 0 deletions src/lib/character/character-manager/character-manager-component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { ReactElement } from "react"
import { defineDSL } from "../utils/defineDSL"
import { CharacterManagerElement } from "./ast"
import type { OneOrMany } from "../utils/util-types"

type ChildrenOf<T> = OneOrMany<ReactElement<T>>

/**
* 子要素でシナリオ内で使用するキャラクターを宣言する
*/
export const DeclareCharacters = defineDSL<{
children: ChildrenOf<typeof DeclareCharacter>
}>(CharacterManagerElement.DeclareCharacters)

/**
* 子要素としてChapterをとり、シナリオを作成する
*/
export const Senario = defineDSL<{
children?: ChildrenOf<typeof Chapter>
}>(CharacterManagerElement.Senario)

/**
* 使用するキャラクターを宣言する。
* @param idleClassName 非話者であるときのPsdCharacterに付与されるclassName
* @param speakingClassName 話者であるときのPsdCharacterに付与されるclassName
* @param name Senario内で使用するキャラクター名
* @param children 非話者時のキャラクターの状態を定義するPsdCharacterのchildrenと同様
*/
export const DeclareCharacter = defineDSL<{
idleClassName?: string
speakingClassName?: string
name: string
psd: string
children: React.ReactNode
}>(CharacterManagerElement.DeclareCharacter)

/**
* キャラクターの喋るチャプターを宣言する。
* 話者をSpeakerとして登録する
* Chapter内でSpeakerとして登録されなかったキャラクターは宣言時に登録された状態になる
*/
export const Chapter = defineDSL<{
children: React.ReactNode
}>(CharacterManagerElement.Chapter)

/**
* 話者を登録する
* @param className canvasに渡すclassName
* @param name DeclareCharactersで宣言したキャラクター名
* @param children PsdCharacterの子要素と同様
*/
export const Speaker = defineDSL<{
className?: string
name: string
children: React.ReactNode
}>(CharacterManagerElement.Speaker)
87 changes: 87 additions & 0 deletions src/lib/character/character-manager/character-manager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { ReactElement, ReactNode } from "react"
import { parseCharacterManager } from "./parser"
import { PsdCharacter } from "../character-unit"
import { DeclareCharacters, Senario } from "./character-manager-component"
import { Clip, ClipSequence } from "../../clip"
import type { OneOrMany } from "../utils/util-types"

export type ImplicitCharacterPlacement = "front" | "back"

type DialogueSenarioProps = {
implicitPlacement?: ImplicitCharacterPlacement
children: OneOrMany<ReactElement<typeof DeclareCharacters> | ReactElement<typeof Senario>>
}

/**
* 対話的に進行するシナリオを書く
* @param implicitPlacement 非話者時のキャラクターをどこに配置するか
* @param children 専用コンポーネントを配置し、シナリオを書く。
* DeclareCharactersで使用するキャラクターを定義する。
* SenarioでChapterごとにキャラクターを配置する。
*/
export const DialogueSenario = ({
implicitPlacement = "back",
children
}: DialogueSenarioProps) => {
const ast = parseCharacterManager(children)

const characters = new Map(ast.characters.children.map(character => {
return [
character.name,
{
psd: character.psd,
speakingClassName: character.speakingClassName,
idleState: <PsdCharacter
key={character.name}
className={character.idleClassName}
psd={character.psd}
>
{character.children}
</PsdCharacter>
}
]
}))

const senario = ast.senario.children.map(chapter => {
const explicitSpeakers = chapter.children.filter(child => child.kind == "speaker").map(s => s.node.name)
const implicitCharacters = Array.from(characters.entries()).filter(([key, _]) => !explicitSpeakers.includes(key))

const explicits = chapter.children.map(elm => {
if (elm.kind == "speaker") {
let defaultClass = ""
if (characters.get(elm.node.name)?.speakingClassName) {
defaultClass = " " + characters.get(elm.node.name)!.speakingClassName
}
return (
<PsdCharacter key={elm.node.name} className={elm.node.className + defaultClass} psd={characters.get(elm.node.name)?.psd!}>
{elm.node.children}
</PsdCharacter>
)
} else {
return elm.node
}
})
const implicits = implicitCharacters.map(([_, character]) => character.idleState)

const merged = mergeImplicitCharacters(implicitPlacement, explicits, implicits)

return <Clip> {merged} </Clip>
})

return (
<ClipSequence>
{senario}
</ClipSequence>
)
}

const mergeImplicitCharacters = (implicitPlacement: ImplicitCharacterPlacement, explicits: ReactNode[], implicits: ReactNode[]) => {
switch (implicitPlacement) {
case "front":
return [...explicits, ...implicits]
case "back":
return [...implicits, ...explicits]
default:
throw `unknown merge option: {implicitPlacement}`
}
}
2 changes: 2 additions & 0 deletions src/lib/character/character-manager/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./character-manager"
export * from "./character-manager-component"
147 changes: 147 additions & 0 deletions src/lib/character/character-manager/parser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React, { isValidElement, type ReactElement, type ReactNode } from "react"
import type { CharacterManagerNode, DeclareCharactersNode, SenarioNode, DeclareCharacterNode, ChapterNode, SpeakerNode, ChapterChild } from "./ast"
import { CharacterManagerElement as ManagerElm} from "./ast"


type AnyElement = ReactElement<any, any>


export const parseCharacterManager = (
children: ReactNode,
): CharacterManagerNode => {
const childrenArray = React.Children.toArray(children)
if (childrenArray.length != 2) {
throw "CharacterManager need DeclareCharacters and Senario."
}

if (!isValidElement(childrenArray[0]) || !isValidElement(childrenArray[1])) {
throw new Error(`Invalid Element in ${ManagerElm.CharacterManager}`)
}

const characters = parseDeclareCharacters(childrenArray[0])
const senario = parseSenario(childrenArray[1])

return {
type: ManagerElm.CharacterManager,
characters: characters,
senario: senario,
}
}

const parseDeclareCharacters = (
self: AnyElement
): DeclareCharactersNode => {
const { children } = self.props
const body = parseDeclareCharactersChildren(children)
return {
type: ManagerElm.DeclareCharacters,
children: body,
}
}

const parseDeclareCharactersChildren = (
children: ReactNode
): DeclareCharacterNode[] => {
return React.Children.map(children, (child) => {
if (!isValidElement(child)) return

const type = getDslType(child)
if (type == ManagerElm.DeclareCharacter) {
return parseDeclareCharacter(child)
} else {
throw `Invalid DSL type in ${ManagerElm.DeclareCharacters}: ${type}`
}
}) ?? []

}

const parseSenario = (
self: AnyElement
): SenarioNode => {
const { children } = self.props
const body = parseSenarioChildren(children)
return {
type: ManagerElm.Senario,
children: body,
}
}

const parseSenarioChildren = (
children: ReactNode
): ChapterNode[] => {
return React.Children.map(children, (child) => {
if (!isValidElement(child)) return

const type = getDslType(child)
if (type == ManagerElm.Chapter) {
return parseChapter(child)
} else {
throw `Invalid DSL type in ${ManagerElm.DeclareCharacters}: ${type}`
}
}) ?? []

}

const parseDeclareCharacter = (
self: AnyElement
): DeclareCharacterNode => {
const { name, psd, idleClassName, speakingClassName, children } = self.props
return {
type: ManagerElm.DeclareCharacter,
name,
psd,
idleClassName,
speakingClassName,
children,
}
}

const parseChapter = (
self: AnyElement
): ChapterNode => {
const { children } = self.props
const body = parseChapterChildren(children)
return {
type: ManagerElm.Chapter,
children: body,
}
}

const parseChapterChildren = (
children: ReactNode
): ChapterChild[] => {
return React.Children.map(children, child => {
if (!isValidElement(child)) return

const type = getDslType(child)
if (type == ManagerElm.Speaker) {
return { kind: "speaker", node: parseSpeaker(child) }
} else {
return { kind: "other", node: child }
}
}) ?? []

}

const parseSpeaker = (
self: AnyElement
): SpeakerNode => {
const { className, name, children } = self.props
return {
type: ManagerElm.Speaker,
className,
name,
children,
}
}


const getDslType = (el: AnyElement): string | undefined => {
const type = el.type as any

if (type?.__dslType) {
return type.__dslType
}

return undefined
}
Loading