Skip to content

Fix: __self__ animation targeting on choices + delay support

Issue: #1888Repo: Creative-Engine only (bug fix + delay). Delay UI in AF is a follow-up. Branch: TBD (off main in CE)


Problem

__self__ as animation target on choices (and all flow components) silently fails. The animation never plays.

Root cause

Three __self__ resolution sites emit animation:flow with comp.name (e.g., answer_0), but handleFlowAnimation does a direct string comparison against this.block?.blockName. No block has blockName === 'answer_0', so the event is dropped.

FileContextResolves toWorks?
Choice.vue:272Hover triggercomp.name (answer_0)No
Choice.vue:292Click triggercomp.name (answer_0)No
conversationFlow.ts:266Appear triggercomp.name (statement_0)No
funcBlocks.ts:68Change operatorstargetAbbrevOpName (t1)Yes

Change operators work because they resolve to targetAbbrevOpName (a proper abbreviation) and use animation:change + matchesBlockTarget().

Why it was never caught

  • Targeting OTHER blocks from choices (e.g., choice -> g1) works fine
  • __self__ on Change operators works (different resolution path)
  • The built-in choice appearing stagger (animationDelay: ms(index * 120)) works but is hardcoded CSS, not flow-triggered
  • handleFlowAnimation has used direct comparison since its introduction; matchesBlockTarget() was added later but only applied to handleChangeAnimation and handleShowHideAnimation

Plan

Part 1: Fix __self__ resolution (3 files)

Choice.vue (2 sites: onHoverTrigger, doSelection)

Change:

ts
const resolvedTarget = targetBlock === '__self__' ? comp.name : targetBlock

To:

ts
const resolvedTarget = targetBlock === '__self__'
  ? abbrevName(comp.type, idFromNamePlusOne(comp.name), false)
  : targetBlock

Import abbrevName and idFromNamePlusOne from StyleAndClassNameGenerationMixin (already used in MessageHolder.vue).

conversationFlow.ts (1 site: emitAnimationTriggers)

Same change. Import the same helpers.

Part 2: Fix handleFlowAnimation matching (1 file)

AnimationMixin.ts

Change handleFlowAnimation from:

ts
handleFlowAnimation({ targetBlock, effects, duration, easing }) {
  if (this.block?.blockName !== targetBlock) return
  this.playFlowAnimation({ effects, duration, easing })
}

To using matchesBlockTarget() + sub-element targeting (same pattern as handleChangeAnimation):

ts
handleFlowAnimation({ targetBlock, effects, duration, easing }) {
  if (!this.matchesBlockTarget(targetBlock)) return

  // Conversation sub-elements: animate the specific element, not the whole block
  const isSubElement = this.block?.blockName === 'conversationProperties'
    && /^[mclir]\d+$/.test(targetBlock)
  const subEl = isSubElement
    ? this.$el?.querySelector(`[data-showhide-target="${targetBlock}"]`)
    : null

  if (subEl) {
    // Use Web Animations API directly on the sub-element
    playDOMAnimation(subEl, effects, duration, easing)
    return
  }

  this.playFlowAnimation({ effects, duration, easing })
}

Note: playDOMAnimation is already defined in ShowHide.ts. Extract it to a shared util or import it. Alternatively, inline the Web Animations API call (it's ~10 lines).

Part 3: Add delay field to animation triggers

Data model change (all 3 repos):

Add optional delay?: number (milliseconds) to each target in animationTriggers.targets.

AF types (Blocks/data/types.ts):

ts
// In AnimationTriggerTarget or equivalent
delay?: number  // ms before animation starts

Composer (remapData.ts): Pass through -- delay is already on the target object, no special mapping needed.

Engine (Choice.vue, conversationFlow.ts):

ts
// Wrap emission in setTimeout when delay is set
const emit = () => DataStore.emitter.emit('animation:flow', { ... })
if (delay) setTimeout(emit, delay)
else emit()

Frontend UI (follow-up, not in this branch): Add a number input for delay in OperatorAnimations.vue per target row.


Testing checklist

  • [ ] Choice with __self__ click trigger -> choice element animates
  • [ ] Choice with __self__ hover trigger -> choice element animates on hover
  • [ ] Appear trigger with __self__ on a statement -> message element animates on appear
  • [ ] Choice targeting an explicit block (e.g., g1) still works (regression check)
  • [ ] Change operator __self__ still works (regression check)
  • [ ] ShowHide animations still work (regression check)
  • [ ] Delay field: choice with 200ms delay animates after 200ms
  • [ ] Multiple choices with staggered delays (0, 100, 200) animate sequentially

Future: Multi-select stagger UX

See issue comment. Multi-select operators in flow, apply one animation with auto-stagger delay. Acts as baseline -- individual operators can override. Purely frontend batch operation, no engine changes beyond the delay field.

Internal documentation