@@ -284,6 +284,14 @@ Live2DManager.prototype.setupDragAndDrop = function (model) {
284284 let isDragging = false ;
285285 let dragStartPos = new PIXI . Point ( ) ;
286286
287+ // 点击检测相关变量
288+ let clickStartTime = 0 ;
289+ let clickStartX = 0 ;
290+ let clickStartY = 0 ;
291+ let hasMoved = false ;
292+ const CLICK_THRESHOLD_DISTANCE = 10 ; // 移动距离阈值(像素)
293+ const CLICK_THRESHOLD_TIME = 300 ; // 时间阈值(毫秒)
294+
287295 // 使用 live2d-ui-drag.js 中的共享工具函数(按钮 pointer-events 管理)
288296 const disableButtonPointerEvents = ( ) => {
289297 if ( window . DragHelpers ) {
@@ -297,6 +305,164 @@ Live2DManager.prototype.setupDragAndDrop = function (model) {
297305 }
298306 } ;
299307
308+ const playTutorialMotion = async ( ) => {
309+ if ( ! this . currentModel || ! this . currentModel . motion ) {
310+ return false ;
311+ }
312+
313+ const fileRefMotions = this . fileReferences && this . fileReferences . Motions ;
314+ let motionGroups = [ ] ;
315+
316+ if ( fileRefMotions && typeof fileRefMotions === 'object' ) {
317+ motionGroups = Object . keys ( fileRefMotions )
318+ . filter ( group => Array . isArray ( fileRefMotions [ group ] ) && fileRefMotions [ group ] . length > 0 ) ;
319+ }
320+
321+ if ( motionGroups . length === 0 &&
322+ this . currentModel . internalModel &&
323+ this . currentModel . internalModel . motionManager &&
324+ this . currentModel . internalModel . motionManager . definitions ) {
325+ const defs = this . currentModel . internalModel . motionManager . definitions ;
326+ motionGroups = Object . keys ( defs )
327+ . filter ( group => Array . isArray ( defs [ group ] ) && defs [ group ] . length > 0 ) ;
328+ }
329+
330+ if ( motionGroups . length === 0 ) {
331+ return false ;
332+ }
333+
334+ const group = this . getRandomElement ( motionGroups ) ;
335+ if ( ! group ) return false ;
336+
337+ const groupList =
338+ ( fileRefMotions && fileRefMotions [ group ] ) ||
339+ ( this . currentModel . internalModel &&
340+ this . currentModel . internalModel . motionManager &&
341+ this . currentModel . internalModel . motionManager . definitions &&
342+ this . currentModel . internalModel . motionManager . definitions [ group ] ) ||
343+ [ ] ;
344+
345+ if ( ! Array . isArray ( groupList ) || groupList . length === 0 ) {
346+ return false ;
347+ }
348+
349+ const index = Math . floor ( Math . random ( ) * groupList . length ) ;
350+
351+ try {
352+ const motion = await this . currentModel . motion ( group , index ) ;
353+ if ( motion ) {
354+ console . log ( `[Interaction] 教程模式 - 播放动作: ${ group } [${ index } ]` ) ;
355+ return true ;
356+ }
357+ } catch ( error ) {
358+ console . warn ( '[Interaction] 教程模式 - 动作播放失败:' , error ) ;
359+ }
360+
361+ return false ;
362+ } ;
363+
364+ // 点击触发随机表情和动作
365+ const triggerRandomEmotion = async ( ) => {
366+ // 教程模式:直接随机播放表情
367+ if ( window . isInTutorial ) {
368+ console . log ( '[Interaction] 教程模式 - 随机播放表情' ) ;
369+ try {
370+ // 获取表情列表
371+ let expressionNames = [ ] ;
372+ if ( this . fileReferences && Array . isArray ( this . fileReferences . Expressions ) ) {
373+ expressionNames = this . fileReferences . Expressions . map ( e => e . Name ) . filter ( Boolean ) ;
374+ }
375+
376+ // 随机播放表情
377+ if ( expressionNames . length > 0 ) {
378+ const randomExpression = expressionNames [ Math . floor ( Math . random ( ) * expressionNames . length ) ] ;
379+ console . log ( `[Interaction] 教程模式 - 播放表情: ${ randomExpression } ` ) ;
380+ await this . currentModel . expression ( randomExpression ) ;
381+
382+ const playedMotion = await playTutorialMotion ( ) ;
383+
384+ if ( ! playedMotion ) {
385+ // 动作不可用时,回退到参数动画模拟效果
386+ const model = this . currentModel . internalModel ;
387+ if ( model && model . coreModel ) {
388+ // 随机晃动头部
389+ const angleXIndex = model . coreModel . getParameterIndex ( 'ParamAngleX' ) ;
390+ const angleYIndex = model . coreModel . getParameterIndex ( 'ParamAngleY' ) ;
391+ const bodyAngleXIndex = model . coreModel . getParameterIndex ( 'ParamBodyAngleX' ) ;
392+
393+ const duration = 1000 + Math . random ( ) * 1000 ; // 1-2秒
394+ const startTime = Date . now ( ) ;
395+
396+ const setParamByIndex = ( index , value ) => {
397+ if ( index < 0 ) return ;
398+ if ( typeof model . coreModel . setParameterValueByIndex === 'function' ) {
399+ model . coreModel . setParameterValueByIndex ( index , value ) ;
400+ } else {
401+ model . coreModel . setParameterValueById ( index , value ) ;
402+ }
403+ } ;
404+
405+ const animate = ( ) => {
406+ const elapsed = Date . now ( ) - startTime ;
407+ const progress = Math . min ( elapsed / duration , 1 ) ;
408+ const t = progress * Math . PI * 2 ; // 一个完整周期
409+
410+ setParamByIndex ( angleXIndex , Math . sin ( t ) * 15 ) ; // -15 到 15 度
411+ setParamByIndex ( angleYIndex , Math . cos ( t ) * 10 ) ; // -10 到 10 度
412+ setParamByIndex ( bodyAngleXIndex , Math . sin ( t * 0.5 ) * 5 ) ; // 更慢的身体晃动
413+
414+ if ( progress < 1 ) {
415+ requestAnimationFrame ( animate ) ;
416+ } else {
417+ // 动画结束,恢复默认值
418+ setParamByIndex ( angleXIndex , 0 ) ;
419+ setParamByIndex ( angleYIndex , 0 ) ;
420+ setParamByIndex ( bodyAngleXIndex , 0 ) ;
421+ }
422+ } ;
423+
424+ animate ( ) ;
425+ console . log ( '[Interaction] 教程模式 - 播放参数动画' ) ;
426+ }
427+ }
428+ }
429+ } catch ( error ) {
430+ console . warn ( '[Interaction] 教程模式播放表情失败:' , error ) ;
431+ }
432+ return ;
433+ }
434+
435+ // 正常模式:使用情感系统
436+ if ( ! this . emotionMapping ) {
437+ console . log ( '[Interaction] 没有情感映射配置,跳过点击触发' ) ;
438+ return ;
439+ }
440+
441+ // 获取可用的情感列表
442+ let availableEmotions = [ ] ;
443+
444+ // 从 emotionMapping 中获取可用情感
445+ if ( this . emotionMapping . expressions ) {
446+ availableEmotions = Object . keys ( this . emotionMapping . expressions ) . filter ( e => e !== '常驻' ) ;
447+ }
448+
449+ // 如果没有配置情感,使用默认列表
450+ if ( availableEmotions . length === 0 ) {
451+ availableEmotions = [ 'happy' , 'sad' , 'angry' , 'neutral' ] ;
452+ }
453+
454+ // 随机选择一个情感
455+ const randomEmotion = availableEmotions [ Math . floor ( Math . random ( ) * availableEmotions . length ) ] ;
456+ console . log ( `[Interaction] 点击触发随机情感: ${ randomEmotion } ` ) ;
457+
458+ // 触发情感
459+ try {
460+ await this . setEmotion ( randomEmotion ) ;
461+ } catch ( error ) {
462+ console . warn ( '[Interaction] 触发情感失败:' , error ) ;
463+ }
464+ } ;
465+
300466 model . on ( 'pointerdown' , ( event ) => {
301467 if ( this . isLocked ) return ;
302468
@@ -312,6 +478,13 @@ Live2DManager.prototype.setupDragAndDrop = function (model) {
312478 const globalPos = event . data . global ;
313479 dragStartPos . x = globalPos . x - model . x ;
314480 dragStartPos . y = globalPos . y - model . y ;
481+
482+ // 记录点击开始信息
483+ clickStartTime = Date . now ( ) ;
484+ clickStartX = globalPos . x ;
485+ clickStartY = globalPos . y ;
486+ hasMoved = false ;
487+
315488 document . getElementById ( 'live2d-canvas' ) . style . cursor = 'grabbing' ;
316489
317490 // 开始拖动时,临时禁用按钮的 pointer-events
@@ -326,6 +499,15 @@ Live2DManager.prototype.setupDragAndDrop = function (model) {
326499 // 拖拽结束后恢复按钮的 pointer-events
327500 restoreButtonPointerEvents ( ) ;
328501
502+ // 检测是否为点击(非拖拽)
503+ const clickDuration = Date . now ( ) - clickStartTime ;
504+ if ( ! hasMoved && clickDuration < CLICK_THRESHOLD_TIME ) {
505+ // 这是一个点击,触发随机表情和动作
506+ console . log ( `[Interaction] 检测到点击(时长: ${ clickDuration } ms)` ) ;
507+ await triggerRandomEmotion ( ) ;
508+ return ; // 点击不需要保存位置
509+ }
510+
329511 // 检测是否需要切换屏幕(多屏幕支持)
330512 // _checkAndSwitchDisplay returns true if a display switch occurred (and saved internally)
331513 const displaySwitched = await this . _checkAndSwitchDisplay ( model ) ;
@@ -359,6 +541,14 @@ Live2DManager.prototype.setupDragAndDrop = function (model) {
359541 const x = event . clientX ;
360542 const y = event . clientY ;
361543
544+ // 检测是否移动超过阈值
545+ const moveDistance = Math . sqrt (
546+ Math . pow ( x - clickStartX , 2 ) + Math . pow ( y - clickStartY , 2 )
547+ ) ;
548+ if ( moveDistance > CLICK_THRESHOLD_DISTANCE ) {
549+ hasMoved = true ;
550+ }
551+
362552 model . x = x - dragStartPos . x ;
363553 model . y = y - dragStartPos . y ;
364554 }
@@ -1242,4 +1432,3 @@ Live2DManager.prototype.destroy = function () {
12421432
12431433 console . log ( '[Live2D] Live2DManager 实例已销毁' ) ;
12441434} ;
1245-
0 commit comments