11import * as THREE from 'three' ;
22import type { AvatarMotion } from './petal-avatar' ;
3+ import type { DanceStyle } from './appearance' ;
34
45export type VroidGestureStyle = 'luna' | 'apron' | 'rose' | 'serin' ;
56
@@ -52,6 +53,8 @@ const GESTURES: Record<VroidGestureStyle, Record<'win' | 'lose', Pose>> = {
5253 } ,
5354} ;
5455
56+ export type DanceAvatar = VroidGestureStyle | 'petal' ;
57+
5558type Dance = {
5659 leftZ : number ;
5760 rightZ : number ;
@@ -65,86 +68,107 @@ type Dance = {
6568 torso : number ;
6669 head : number ;
6770} ;
71+ // Eight clear counts followed by a mirrored return phrase. Every character
72+ // has a different arm, footwork and turn sequence; Encore changes the count
73+ // order, lead side and accents rather than replaying the same loop faster.
74+ type Step = readonly [ number , number , number , number , number , number , number , number , number , number , number ] ;
75+ const CHOREOGRAPHY : Record < DanceAvatar , readonly Step [ ] > = {
76+ petal : [
77+ [ - 0.35 , 0.35 , - 0.2 , - 0.2 , 0.15 , - 0.1 , - 0.08 , 0.01 , - 0.18 , 0.1 , - 0.08 ] ,
78+ [ - 0.85 , 0.15 , 0.1 , - 0.5 , 0.55 , - 0.2 , - 0.16 , 0.1 , - 0.35 , - 0.16 , 0.12 ] ,
79+ [ - 0.35 , 0.95 , - 0.65 , 0.15 , - 0.15 , 0.45 , 0.12 , 0.02 , 0.16 , 0.2 , - 0.12 ] ,
80+ [ 0.45 , 0.45 , - 0.75 , - 0.75 , 0.2 , 0.2 , 0 , 0.12 , 0.48 , - 0.18 , 0.1 ] ,
81+ [ 0.7 , - 0.7 , - 0.25 , - 0.25 , - 0.35 , 0.55 , 0.16 , 0.02 , 0.72 , 0.24 , - 0.16 ] ,
82+ [ - 1.05 , 0.85 , 0.25 , - 0.4 , 0.5 , - 0.3 , - 0.12 , 0.1 , 0.35 , - 0.2 , 0.14 ] ,
83+ [ - 0.15 , 0.15 , - 0.8 , - 0.8 , - 0.2 , 0.5 , 0.12 , 0.04 , - 0.28 , 0.18 , - 0.1 ] ,
84+ [ - 0.8 , 0.8 , - 0.15 , - 0.15 , 0.2 , 0.2 , 0 , 0.12 , 0 , - 0.08 , 0 ] ,
85+ ] ,
86+ luna : [
87+ [ - 0.25 , 0.55 , - 0.55 , 0.1 , 0.35 , - 0.2 , - 0.12 , 0.02 , - 0.2 , 0.16 , - 0.1 ] ,
88+ [ - 1.05 , 0.2 , - 0.1 , - 0.65 , 0.65 , - 0.3 , - 0.2 , 0.09 , - 0.42 , - 0.16 , 0.15 ] ,
89+ [ - 0.25 , 1.1 , - 0.8 , 0.2 , - 0.2 , 0.5 , 0.16 , 0.03 , 0.25 , 0.22 , - 0.12 ] ,
90+ [ 0.65 , 0.65 , - 0.9 , - 0.9 , 0.25 , 0.25 , 0 , 0.12 , 0.55 , - 0.2 , 0.1 ] ,
91+ [ 0.9 , - 0.6 , 0.15 , - 0.5 , - 0.4 , 0.65 , 0.2 , 0.02 , 0.8 , 0.26 , - 0.18 ] ,
92+ [ - 0.8 , 1.1 , - 0.5 , 0.2 , 0.55 , - 0.4 , - 0.16 , 0.11 , 0.4 , - 0.22 , 0.16 ] ,
93+ [ - 0.5 , 0.4 , - 0.95 , - 0.25 , - 0.15 , 0.55 , 0.14 , 0.03 , - 0.32 , 0.18 , - 0.08 ] ,
94+ [ - 1.15 , 1.15 , - 0.25 , - 0.25 , 0.25 , 0.25 , 0 , 0.13 , 0 , - 0.1 , 0 ] ,
95+ ] ,
96+ rose : [
97+ [ - 0.4 , 0.8 , 0.15 , - 0.65 , 0.4 , - 0.2 , - 0.15 , 0.02 , - 0.28 , 0.25 , - 0.14 ] ,
98+ [ 0.45 , 1.05 , - 0.7 , - 0.25 , 0.65 , - 0.3 , - 0.23 , 0.06 , - 0.58 , - 0.2 , 0.18 ] ,
99+ [ - 1.05 , 0.15 , - 0.25 , - 0.8 , - 0.2 , 0.6 , 0.18 , 0.04 , 0.18 , 0.32 , - 0.2 ] ,
100+ [ - 0.95 , 0.95 , - 0.8 , - 0.8 , 0.15 , 0.2 , 0.05 , 0.09 , 0.8 , - 0.24 , 0.14 ] ,
101+ [ 0.65 , - 0.95 , - 0.6 , 0.1 , - 0.35 , 0.65 , 0.23 , 0.02 , 1.1 , 0.3 , - 0.18 ] ,
102+ [ - 0.3 , 0.9 , 0.2 , - 0.85 , 0.6 , - 0.3 , - 0.2 , 0.08 , 0.68 , - 0.3 , 0.19 ] ,
103+ [ - 1.1 , 0.45 , - 0.7 , - 0.15 , - 0.25 , 0.5 , 0.16 , 0.03 , 0.16 , 0.22 , - 0.12 ] ,
104+ [ - 0.8 , 0.8 , - 0.35 , - 0.35 , 0.2 , 0.2 , 0 , 0.09 , - 0.2 , - 0.1 , 0 ] ,
105+ ] ,
106+ apron : [
107+ [ - 0.7 , 0.25 , - 0.45 , 0.1 , 0.4 , - 0.15 , - 0.1 , 0.04 , - 0.25 , 0.14 , - 0.1 ] ,
108+ [ - 0.25 , 1.0 , 0.15 , - 0.6 , 0.7 , - 0.2 , - 0.17 , 0.12 , - 0.42 , - 0.18 , 0.16 ] ,
109+ [ - 1.0 , 0.55 , - 0.6 , - 0.15 , - 0.2 , 0.55 , 0.13 , 0.03 , 0.2 , 0.22 , - 0.1 ] ,
110+ [ - 0.65 , 0.65 , - 0.95 , - 0.95 , 0.15 , 0.15 , 0 , 0.13 , 0.45 , - 0.2 , 0.12 ] ,
111+ [ 0.3 , - 0.75 , 0.1 , - 0.65 , - 0.3 , 0.65 , 0.16 , 0.04 , 0.65 , 0.2 , - 0.12 ] ,
112+ [ - 1.1 , 0.4 , - 0.15 , - 0.6 , 0.6 , - 0.3 , - 0.14 , 0.12 , 0.25 , - 0.18 , 0.14 ] ,
113+ [ - 0.45 , 1.05 , - 0.75 , - 0.15 , - 0.2 , 0.55 , 0.12 , 0.04 , - 0.24 , 0.2 , - 0.1 ] ,
114+ [ - 1.05 , 1.05 , - 0.3 , - 0.3 , 0.25 , 0.25 , 0 , 0.14 , 0 , - 0.08 , 0 ] ,
115+ ] ,
116+ serin : [
117+ [ - 0.2 , 0.8 , - 0.65 , 0.1 , 0.45 , - 0.2 , - 0.12 , 0.02 , - 0.24 , 0.18 , - 0.12 ] ,
118+ [ - 1.05 , 0.35 , 0.2 , - 0.8 , 0.65 , - 0.3 , - 0.2 , 0.1 , - 0.5 , - 0.22 , 0.16 ] ,
119+ [ - 0.6 , 1.05 , - 0.95 , - 0.1 , - 0.2 , 0.6 , 0.16 , 0.03 , 0.26 , 0.28 , - 0.14 ] ,
120+ [ 0.8 , 0.8 , - 0.7 , - 0.7 , 0.2 , 0.2 , 0 , 0.13 , 0.7 , - 0.24 , 0.12 ] ,
121+ [ 1.1 , - 0.5 , 0.1 , - 0.75 , - 0.4 , 0.7 , 0.2 , 0.02 , 0.95 , 0.3 , - 0.2 ] ,
122+ [ - 0.9 , 1.15 , - 0.6 , 0.15 , 0.65 , - 0.35 , - 0.17 , 0.11 , 0.44 , - 0.25 , 0.18 ] ,
123+ [ - 0.45 , 0.65 , - 1.05 , - 0.35 , - 0.25 , 0.65 , 0.15 , 0.04 , - 0.32 , 0.23 , - 0.1 ] ,
124+ [ - 1.1 , 1.1 , - 0.35 , - 0.35 , 0.3 , 0.3 , 0 , 0.14 , 0 , - 0.1 , 0 ] ,
125+ ] ,
126+ } ;
127+ const KEYS : ( keyof Dance ) [ ] = [
128+ 'leftZ' ,
129+ 'rightZ' ,
130+ 'leftX' ,
131+ 'rightX' ,
132+ 'leftLeg' ,
133+ 'rightLeg' ,
134+ 'hipX' ,
135+ 'hipY' ,
136+ 'hipTurn' ,
137+ 'torso' ,
138+ 'head' ,
139+ ] ;
140+ const ENCORE_ORDER = [ 0 , 2 , 5 , 3 , 6 , 1 , 4 , 7 ] ;
141+
142+ function atCount ( style : DanceAvatar , variant : DanceStyle , count : number ) : Dance {
143+ const secondPhrase = count >= 8 ;
144+ const index = count % 8 ;
145+ const sourceIndex =
146+ variant === 'encore' ? ENCORE_ORDER [ secondPhrase ? 7 - index : index ] : secondPhrase ? 7 - index : index ;
147+ const step = CHOREOGRAPHY [ style ] [ sourceIndex ] ;
148+ const mirrored = ( variant === 'encore' ) !== secondPhrase ;
149+ const values = mirrored
150+ ? [ - step [ 1 ] , - step [ 0 ] , step [ 3 ] , step [ 2 ] , step [ 5 ] , step [ 4 ] , - step [ 6 ] , step [ 7 ] , - step [ 8 ] , - step [ 9 ] , - step [ 10 ] ]
151+ : step ;
152+ return Object . fromEntries ( KEYS . map ( ( key , i ) => [ key , values [ i ] ] ) ) as Dance ;
153+ }
68154
69- function dance ( style : VroidGestureStyle , time : number ) : Dance {
70- const beat = ( speed : number , offset = 0 ) => Math . sin ( time * speed + offset ) ;
71- const hop = ( speed : number ) => Math . max ( 0 , beat ( speed ) ) ;
72- switch ( style ) {
73- case 'luna' : {
74- // Paw steps: alternating knees and playful double paw punches.
75- const step = beat ( 6.5 ) ;
76- return {
77- leftZ : - 0.3 * step ,
78- rightZ : 0.3 * step ,
79- leftX : 0.48 * hop ( 6.5 ) ,
80- rightX : 0.48 * hop ( 6.5 + 0.8 ) ,
81- leftLeg : 0.35 * step ,
82- rightLeg : - 0.35 * step ,
83- hipX : 0.08 * step ,
84- hipY : 0.085 * hop ( 13 ) ,
85- hipTurn : 0.15 * beat ( 3.25 ) ,
86- torso : 0.13 * step ,
87- head : - 0.1 * step ,
88- } ;
89- }
90- case 'apron' : {
91- // Heel taps and a clap on every other beat.
92- const step = beat ( 5.2 ) ;
93- const clap = hop ( 5.2 ) ;
94- return {
95- leftZ : - 0.38 * clap ,
96- rightZ : 0.38 * clap ,
97- leftX : - 0.36 * clap ,
98- rightX : - 0.36 * clap ,
99- leftLeg : 0.28 * hop ( 5.2 ) ,
100- rightLeg : 0.28 * hop ( 5.2 + Math . PI ) ,
101- hipX : 0.12 * step ,
102- hipY : 0.045 * clap ,
103- hipTurn : - 0.14 * step ,
104- torso : 0.14 * step ,
105- head : 0.09 * step ,
106- } ;
107- }
108- case 'rose' : {
109- // Slower waltz, sweeping arms, and a three-count turn.
110- const step = beat ( 3.6 ) ;
111- return {
112- leftZ : - 0.24 * beat ( 3.6 , 1 ) ,
113- rightZ : 0.42 * step ,
114- leftX : 0.25 * step ,
115- rightX : - 0.25 * step ,
116- leftLeg : 0.29 * step ,
117- rightLeg : - 0.29 * step ,
118- hipX : 0.15 * step ,
119- hipY : 0.055 * hop ( 7.2 ) ,
120- hipTurn : 0.35 * beat ( 1.8 ) ,
121- torso : 0.22 * step ,
122- head : - 0.15 * step ,
123- } ;
124- }
125- case 'serin' : {
126- // A two-step groove with alternating heel lifts and crossing arm sweeps.
127- const step = beat ( 5.1 ) ;
128- return {
129- leftZ : - 0.53 * step ,
130- rightZ : 0.48 * beat ( 5.1 , Math . PI / 2 ) ,
131- leftX : - 0.3 * beat ( 5.1 , 0.8 ) ,
132- rightX : 0.3 * beat ( 5.1 , 0.8 ) ,
133- leftLeg : 0.38 * Math . max ( 0 , step ) ,
134- rightLeg : 0.38 * Math . max ( 0 , - step ) ,
135- hipX : 0.17 * step ,
136- hipY : 0.07 * hop ( 10.2 ) ,
137- hipTurn : - 0.26 * beat ( 2.55 ) ,
138- torso : 0.2 * beat ( 5.1 , 0.7 ) ,
139- head : - 0.13 * step ,
140- } ;
141- }
142- }
155+ export function sampleVictoryDance ( style : DanceAvatar , variant : DanceStyle , time : number ) : Dance {
156+ const pace = style === 'rose' ? 1.7 : style === 'apron' ? 2.2 : 2 ;
157+ const count = ( time * pace ) % 16 ;
158+ const index = Math . floor ( count ) ;
159+ const phase = count - index ;
160+ const blend = phase * phase * ( 3 - 2 * phase ) ;
161+ const from = atCount ( style , variant , index ) ;
162+ const to = atCount ( style , variant , ( index + 1 ) % 16 ) ;
163+ const sampled = Object . fromEntries ( KEYS . map ( ( key ) => [ key , from [ key ] + ( to [ key ] - from [ key ] ) * blend ] ) ) as Dance ;
164+ sampled . hipY += Math . sin ( phase * Math . PI ) * ( variant === 'encore' ? 0.055 : 0.035 ) ;
165+ return sampled ;
143166}
144167
145168export function updateVroidGesture (
146169 style : VroidGestureStyle ,
147170 motion : AvatarMotion ,
171+ variant : DanceStyle ,
148172 elapsed : number ,
149173 delta : number ,
150174 restHipY : number ,
@@ -154,16 +178,20 @@ export function updateVroidGesture(
154178 head : THREE . Object3D ;
155179 leftArm : THREE . Object3D ;
156180 rightArm : THREE . Object3D ;
181+ leftForearm ?: THREE . Object3D ;
182+ rightForearm ?: THREE . Object3D ;
157183 leftLeg : THREE . Object3D ;
158184 rightLeg : THREE . Object3D ;
185+ leftShin ?: THREE . Object3D ;
186+ rightShin ?: THREE . Object3D ;
159187 } ,
160188) {
161189 const idle = motion === 'idle' ;
162190 const target = idle ? pose ( 1.19 , - 1.19 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0.006 , 1.8 ) : GESTURES [ style ] [ motion ] ;
163191 const wave = Math . sin ( elapsed * target . speed ) ;
164192 const follow = delta === 0 ? 1 : 1 - Math . exp ( - 7 * delta ) ;
165193 const settle = ( current : number , next : number ) => current + ( next - current ) * follow ;
166- const move = motion === 'win' ? dance ( style , elapsed ) : null ;
194+ const move = motion === 'win' ? sampleVictoryDance ( style , variant , elapsed ) : null ;
167195 const activity = idle ? 0.015 : 0.025 ;
168196 bones . leftArm . rotation . z = settle ( bones . leftArm . rotation . z , target . left + activity * wave + ( move ?. leftZ ?? 0 ) * 1.5 ) ;
169197 bones . rightArm . rotation . z = settle (
@@ -180,6 +208,14 @@ export function updateVroidGesture(
180208 bones . rightLeg . rotation . x ,
181209 move ? move . rightLeg * 1.4 : motion === 'lose' ? - 0.08 * wave : 0 ,
182210 ) ;
211+ if ( bones . leftForearm )
212+ bones . leftForearm . rotation . x = settle ( bones . leftForearm . rotation . x , move ? - 0.25 + move . leftX * 0.55 : 0 ) ;
213+ if ( bones . rightForearm )
214+ bones . rightForearm . rotation . x = settle ( bones . rightForearm . rotation . x , move ? - 0.25 + move . rightX * 0.55 : 0 ) ;
215+ if ( bones . leftShin )
216+ bones . leftShin . rotation . x = settle ( bones . leftShin . rotation . x , move ? Math . max ( 0 , - move . leftLeg ) * 0.8 : 0 ) ;
217+ if ( bones . rightShin )
218+ bones . rightShin . rotation . x = settle ( bones . rightShin . rotation . x , move ? Math . max ( 0 , - move . rightLeg ) * 0.8 : 0 ) ;
183219 bones . head . rotation . x = settle ( bones . head . rotation . x , target . headX + 0.012 * wave ) ;
184220 bones . head . rotation . z = settle ( bones . head . rotation . z , target . headZ + 0.016 * wave + ( move ?. head ?? 0 ) * 1.2 ) ;
185221 bones . spine . rotation . x = settle ( bones . spine . rotation . x , target . spineX ) ;
0 commit comments