Appearance
Feasibility: Animasjoner i Change- og ShowHide-operatorer
Dato: 2026-03-20 Relatert: Animation System V2 (add-animation-system branch) Relatert issue: #1804
Status (2026-05): Implementert. Analysen nedenfor er historisk kontekst. Den faktiske implementasjonen valgte en annen retning enn "Strategi C (Deferred v-if)":
- Visual blocks ble byttet fra
v-iftilv-show, som eliminerer race condition ved show-animasjoner (komponenten er alltid mountet og lytter påanimation:showHide).- Hide-animasjoner spilles av mens elementet er synlig, deretter setter
onComplete-callbackdisplay: noneviashowHideFunctions.hideElement().- Change-animasjoner bruker
animation:changeevent med halvert duration: fade-out, callback (innholdsendring), fade-in.- Se
ShowHide.ts,AnimationMixin.ts(handleShowHideAnimation, handleChangeAnimation) for implementasjonsdetaljer.
Bakgrunn
Animasjonssystemet V2 støtter i dag:
- Blokk-animasjoner — direkte på blokker (fade, slide, scale, etc. ved load/hover/click/scroll)
- Flow-triggered animasjoner —
OperatorAnimations-panelet lar conversation-operatorer (statement, answer, link, text_input, ar) trigge animasjoner på visuelle blokker
Operatorer som Change (ChangeText, ChangeImage, ChangeUrl, ChangeVideo) og ShowHide har i dag ingen animasjonsstøtte — endringene skjer øyeblikkelig uten noen visuell overgang.
1. Animasjoner i Change-operatorer
Hva er Change?
Change-operatorene endrer innholdet i en visuell blokk under kjøring:
ChangeText→ bytter tekst i tekst/button/html-blokkerChangeImage→ bytter bilde i graphic-blokkerChangeUrl→ bytter URL i link/button-blokkerChangeVideo→ bytter video-URL
Nåværende dataflyt
AF ChangeTextOp (op.properties.body.inputText.value + targetAbbrevOpName)
→ Composer remapData (comp.payload.text + comp.payload.targetAbbrevOpName)
→ CE funcBlocks.ChangeText → changeFunctions.changeText()
→ CE DataStore.overrides.value['t1']['changeText'] = 'ny tekst'
→ CE BlockMixin.blockWithOverrides (computed) → Vue re-rendrer blokkenEndringen skjer synkront — blokkens innhold oppdateres umiddelbart uten overgang.
Foreslått løsning
Legge til animationTriggers-data (identisk format som eksisterende OperatorAnimations) på Change-operatorer, slik at brukeren kan velge en animasjon som spilles av på målblokken når innholdet endres.
Eksempel: ChangeText bytter tekst i t1, og samtidig trigges en fade-animasjon på t1 → teksten "fader over" til nytt innhold.
Endringer per repo
Application-Frontend:
Vise animasjonsikon på Change-operatorer:
- Change-operatorer er IKKE i
STYLESCOMPONENTLIST(i_temp_buildercomponents.ts), som styrer om animasjons- og stil-ikonene vises - Alternativ A: Legg Change-typer til
STYLESCOMPONENTLIST— men da får de også stilpanelet, som gir lite mening - Alternativ B (anbefalt): Lag en separat
ANIMATIONCOMPONENTLISTelleropCanHaveAnimations-computed iOperatorBase.vuesom inkluderer Change-typer i tillegg til de eksisterende - Animasjonsikonet i
OperatorBase.vuebruker da den nye listen
- Change-operatorer er IKKE i
Lagre animasjonsdata:
- Samme JSON-blob som
OperatorAnimationsallerede bruker:op.properties.body.animationTrigger.value OperatorAnimations.vue-panelet gjenbrukes direkte — trenger ingen ny komponent- Forskjell: triggerType er alltid
appear(animasjonen spilles når Change-blokken prosesseres), såappear/click/hover-velgeren kan skjules eller default-settes
- Samme JSON-blob som
Target-auto-fill:
- Siden Change allerede har et
targetAbbrevOpName, kan animasjonspanelet forhåndsutfylle target-blokken til å matche Change-målet - Brukeren kan fortsatt velge en annen blokk (f.eks. animere en naboblokk når teksten endres)
- Siden Change allerede har et
Creative-Composer:
- Remap animasjonsdata fra Change-operatorer:
remapData.tsparser alleredebody.animationTrigger?.valuefor operatorer iSTYLESCOMPONENTLIST- Trenger å utvide denne parsingen til å også gjelde Change-operatorer
- Setter
comp.animationTriggerspå den remappede FlowComponent
Creative-Engine:
- Emitte animasjon etter Change:
- I
funcBlocks.ts, etter atchange('changeText', ...)er kalt, sjekkcomponentArray[0].animationTriggers - Hvis den finnes, emit
animation:flowevent viaDataStore.emitter— identisk mønster somconversationFlow.emitAnimationTriggers() - Animasjonen spilles av på målblokken via
AnimationMixin.handleFlowAnimation()
- I
Kompleksitet
| Område | Estimat | Kommentar |
|---|---|---|
| AF: Utvidelse av animasjonsikon-gate | Lav | Ny liste eller computed, ~10 linjer |
| AF: OperatorAnimations gjenbruk | Ingen | Panelet fungerer allerede |
| Composer: Remap-utvidelse | Lav | Utvide eksisterende if-sjekk, ~5 linjer |
| CE: Emit etter Change | Lav | ~15 linjer i funcBlocks.ts |
| Totalt | Lav | Følger eksisterende mønster 1:1 |
Begrensninger
- Animasjonen starter ETTER innholdet er endret — det er ikke en "crossfade" mellom gammelt og nytt innhold. For ekte crossfade trengs det å snapshot det gamle innholdet, noe som er vesentlig mer komplekst.
- For ChangeImage: bilder laster asynkront, så animasjonen kan starte før det nye bildet er rendret. En preload-mekanisme ville løst dette, men er ikke nødvendig for MVP.
2. Animasjoner i ShowHide-operatorer
Hva er ShowHide?
ShowHide viser eller skjuler blokker under kjøring. Brukeren velger flere targets med enten hide eller show action.
Nåværende dataflyt
AF ShowHideOp (op.properties.body.targets = [{target:'b1-wrap', action:'hide'}, ...])
→ Composer remapData (comp.payload.targets = [...])
→ CE funcBlocks.ShowHide → ShowHide.init()
→ CE showHideFunctions.hideElement('b1-wrap')
→ CE DataStore.runtimeHidden.value['b1-wrap'] = true
→ CE BlockMixin.isRuntimeHidden → v-if="!isRuntimeHidden" → element fjernes fra DOMBlokker vises/skjules øyeblikkelig via v-if. Ingen overgang.
Hovedutfordring: v-if vs v-show
De fleste visuelle blokker bruker v-if="!isRuntimeHidden", som fjerner elementet fra DOM. En animasjon kan ikke spilles av på et element som ikke eksisterer. For animert show/hide trengs en av disse:
| Strategi | Fordeler | Ulemper |
|---|---|---|
A: v-show + CSS-animasjon | Enkel, elementet er alltid i DOM | Skjulte blokker tar plass i layout (kan løses med height: 0; overflow: hidden) |
B: Vue <Transition> | Native Vue-løsning, håndterer inn/ut-animasjoner | Krever omstrukturering av alle blokk-templates |
C: Deferred v-if | Bevarer nåværende oppførsel + animasjon | Mer kompleks: sett v-if=true, spill animasjon, vent på animationend, sett v-if=false |
Foreslått løsning: Strategi C (Deferred v-if) med animasjons-events
Konsept: Behold v-if som primær mekanisme, men legg til et mellomsteg for animert skjuling:
Show (vise blokk):
DataStore.runtimeHidden.value['b1-wrap'] = false(blokken mountes i DOM)- Emit
animation:showevent til blokken AnimationMixinspiller inngangsanimasjon (fade-in, slide-in, etc.)
Hide (skjule blokk):
- Emit
animation:hideevent til blokken med animasjonsconfig AnimationMixinspiller utgangsanimasjon (fade-out, slide-out, etc.)- Lytter på
animationendevent - ETTER animasjonen:
DataStore.runtimeHidden.value['b1-wrap'] = true(blokken unmountes fra DOM)
Endringer per repo
Application-Frontend:
Utvide ShowHideOp.vue med animasjonsvalg:
- Per target i
targets[]-arrayet, legg til animasjonsconfig:
typescripttargets: [{ target: 'b1-wrap', action: 'hide', animation?: { effects: AnimationEffect[] // gjenbruk av eksisterende effekt-typer duration: number // ms easing: string } }]- En kompakt animasjonsvelger per target-rad — f.eks. en preset-dropdown (fade, slide, scale) og en duration-input
- Alternativt: "Advanced" toggle som åpner full effekt-editor (som AnimationConfigCard)
- Per target i
Forenklet animasjonsvelger (anbefalt for ShowHide):
- Ikke fullt
AnimationConfigCard— for overdrevet for show/hide - Heller en enkel rad med:
[Animasjon: None ▾] [Varighet: 300ms] - Preset-dropdown: None, Fade, Slide Up, Slide Down, Slide Left, Slide Right, Scale, Blur
- Mappet til forhåndsdefinerte
effects[]-arrays fraANIMATION_PRESETS - For avanserte brukere: en "Custom..." valg som åpner full effekt-editor
- Ikke fullt
Creative-Composer:
- Utvide remap for ShowHide:
- Parse
animation-objektet fra hver target og inkluder det icomp.payload.targets - Ingen ny type trengs — det er bare ekstra felter på eksisterende targets
- Parse
Creative-Engine:
Utvide
ShowHide.ts:typescript// Pseudo-kode targets.forEach(({ target, action, animation }) => { if (action === 'show') { showHideFunctions.showElement(target) if (animation) { DataStore.emitter.emit('animation:show', { targetBlock: target, effects: animation.effects, duration: animation.duration, easing: animation.easing }) } } else if (action === 'hide') { if (animation) { DataStore.emitter.emit('animation:hide', { targetBlock: target, effects: animation.effects, duration: animation.duration, easing: animation.easing, callback: () => showHideFunctions.hideElement(target) }) } else { showHideFunctions.hideElement(target) } } })Utvide
AnimationMixin.tsfor show/hide:- Lytt på
animation:show→ spill inngangsanimasjon (normalt) - Lytt på
animation:hide→ spill utgangsanimasjon (reversed), ventanimationend, kall callback - Viktig for hide: Callback-mønsteret sikrer at
runtimeHiddensettes ETTER animasjonen er ferdig - Trenger timeout-fallback i tilfelle
animationendikke fyrer (f.eks. element allerede skjult)
- Lytt på
Rapid toggle-håndtering:
- Hvis brukeren skjuler og viser raskt: avbryt pågående hide-animasjon, start show-animasjon
- Bruk en
pendingHideTimeoutsom slettes ved ny show-event animation.cancel()(Web Animations API) eller fjern CSS-klasse for å avbryte
Kompleksitet
| Område | Estimat | Kommentar |
|---|---|---|
| AF: ShowHideOp animasjonsvelger | Medium | Ny UI i eksisterende operator, ~80 linjer |
| AF: Preset-mapping for show/hide | Lav | Gjenbruk ANIMATION_PRESETS |
| Composer: Utvide target-remap | Lav | Ekstra felt, ~10 linjer |
| CE: ShowHide.ts med events | Lav-Medium | Deferred hide-mønsteret, ~40 linjer |
| CE: AnimationMixin show/hide | Medium | Ny event-handler, callback, cancel-logikk, ~60 linjer |
| CE: Rapid toggle edge cases | Medium | Må teste grundig |
| Totalt | Medium | Hovedkompleksiteten ligger i deferred hide + edge cases |
Begrensninger og edge cases
Layout shift: Når en blokk fader ut og deretter fjernes fra DOM (
v-if=false), vil innholdet under den "hoppe opp". For å unngå dette kan man:- Sette
visibility: hidden; height: 0; overflow: hiddeni stedet fordisplay: noneetter animasjonen - Eller akseptere layout-shift (ofte ønskelig — plassen frigjøres etter blokkfjerning)
- Sette
Conversation-operatorer i ShowHide: ShowHide kan også skjule conversation-elementer (statements, answers). Disse rendres i
MessageHolder.vue, ikke som visuelle blokker. Animasjon for disse er vanskeligere fordi de har egen scroll/rendering-logikk.Initielt skjulte blokker: Blokker som starter som skjult (
runtimeHidden = truefra start) mountes aldri, så show-animasjonen trigges når de mountes for første gang.v-ifsikrer at de ikke eksisterer i DOM — de må mountes FØRST, deretter animeres. Rekkefølge: fjernruntimeHidden→ Vue mounter →$nextTick→ spill animasjon.
3. Sammendrag og anbefaling
| Feature | Feasibility | Verdi for bruker | Anbefaling |
|---|---|---|---|
| Change + animasjon | Høy (lav kompleksitet) | Høy — innholdsendringer med visuell overgang er polert | Gjør dette først |
| ShowHide show-animasjon | Høy | Høy — blokker som fader inn er mye bedre enn pop-in | Gjør dette sammen med Change |
| ShowHide hide-animasjon | Medium | Medium-Høy — avhenger av brukscase | Gjør dette etter show er ferdig |
Prioritert rekkefølge
- Change-animasjoner — lavthengende frukt, gjenbruker alt eksisterende
- ShowHide show-animasjoner — enkel retning (element mountes, deretter animeres)
- ShowHide hide-animasjoner — deferred-pattern med callback krever mer testing
Alternativ: Forenklet tilnærming for V1
Dropp deferred hide-animasjon helt i V1. Gi brukeren:
- Change: animasjon på målblokken når innholdet endres
- ShowHide show: animasjon når blokken vises
- ShowHide hide: øyeblikkelig (som i dag)
Dette halverer kompleksiteten og dekker de vanligste brukscasene. Hide-animasjon kan legges til i V2.
Nøkkelfiler
| Fil | Formål |
|---|---|
AF: CavaiFlow/operators/ShowHideOp.vue | ShowHide operator UI |
AF: CavaiFlow/operators/ChangeTextOp.vue | ChangeText operator UI |
AF: CavaiFlow/flowactors/OperatorBase.vue | Operator-shell, styrer ikoner |
AF: CavaiFlow/flowactors/OperatorAnimations.vue | Animasjonspanel (gjenbrukes for Change) |
AF: utils/_temp_buildercomponents.ts | STYLESCOMPONENTLIST — gate for ikoner |
Composer: remapper/remapData.ts | Remapper AF→CE data |
CE: components/blocks/functional/ShowHide.ts | ShowHide prosessering |
CE: components/blocks/functional/Script.ts | Change-funksjoner, setOverride |
CE: utils/funcBlocks.ts | Funksjonell blokk-routing |
CE: mixins/AnimationMixin.ts | Animasjonslogikk på blokker |
CE: mixins/BlockMixin.ts | isRuntimeHidden, blockWithOverrides |
CE: services/dataStore.ts | runtimeHidden, overrides |