diff --git a/.changeset/thick-kangaroos-retire.md b/.changeset/thick-kangaroos-retire.md new file mode 100644 index 0000000..5dd4025 --- /dev/null +++ b/.changeset/thick-kangaroos-retire.md @@ -0,0 +1,5 @@ +--- +'@vtbag/inspection-chamber': patch +--- + +Adds the ability to deselect single animations diff --git a/src/animations.ts b/src/animations.ts index 0da07d9..e9b6611 100644 --- a/src/animations.ts +++ b/src/animations.ts @@ -1,11 +1,13 @@ -import { namesOfAnimation } from './panel/full-control'; +import { control, namesOfAnimation } from './panel/full-control'; import { plugInPanel } from './panel/inner'; +import { getModus } from './panel/modus'; import { updateNames } from './panel/names'; import { vtActive } from './panel/transition'; import { setStyles } from './styles'; import { initTwin } from './twin'; export function forceAnimations() { + const frameBody = top!.__vtbag.inspectionChamber!.frameDocument!.body; setStyles( ` @keyframes vtbag-twin-noop { @@ -25,7 +27,6 @@ export async function retrieveViewTransitionAnimations() { const inspectionChamber = top!.__vtbag.inspectionChamber!; const frameDoc = inspectionChamber.frameDocument!; const animations: Animation[] = (inspectionChamber.animations = []); - const animationMap = (inspectionChamber.animationMap = new Map()); const keyframesMap = (inspectionChamber.keyframesMap = new Map()); const names = new Set(); @@ -33,40 +34,60 @@ export async function retrieveViewTransitionAnimations() { const set = new WeakSet(); let growing = true; + let rootRootAnimation = false; while (growing) { growing = false; - frameDoc.getAnimations().forEach((a) => { - const animation = a as CSSAnimation; - if ( - !set.has(animation) && - animation.effect?.pseudoElement?.startsWith('::view-transition') && - animation.playState !== 'finished' - ) { - const animationName = animation.animationName; - if (animationName !== 'vtbag-twin-noop' && animationName !== 'none') { - const { viewTransitionName } = namesOfAnimation(animation)!; - names.add(viewTransitionName); - animations.push(animation); - animationMap.set(animationName, animation); - animation.pause(); - animation.currentTime = 0; - keyframesMap!.set(animationName, animation.effect?.getKeyframes()); + frameDoc.getAnimations().forEach((animationObject) => { + if (animationObject.effect?.pseudoElement?.startsWith('::view-transition')) { + const { pseudoName, viewTransitionName } = namesOfAnimation(animationObject)!; + if (!set.has(animationObject)) { + const keyframeName = + animationObject instanceof CSSAnimation ? animationObject.animationName : undefined; + const transitionProperty = + animationObject instanceof CSSTransition + ? animationObject.transitionProperty + : undefined; + if (transitionProperty) { + console.warn( + '[inspection-chamber] Unhandled transition:', + viewTransitionName, + pseudoName, + transitionProperty + ); + } else if (keyframeName) { + animationObject.pause(); + animationObject.currentTime = 0; + viewTransitionName && pseudoName === 'image-pair' && names.add(viewTransitionName); + !viewTransitionName && pseudoName === '::view-transition' && (rootRootAnimation = true); + if (keyframeName === 'vtbag-twin-noop') { + animationObject.cancel(); + } else { + animations.push(animationObject); + keyframesMap.set(keyframeName, animationObject.effect?.getKeyframes()!); + } + } else { + console.warn( + '[inspection-chamber] Unhandled animation:', + viewTransitionName, + pseudoName, + animationObject.constructor.name + ); + } + growing = true; } - growing = true; + set.add(animationObject); } - set.add(animation); }); growing && (await new Promise((r) => frameDoc.defaultView!.setTimeout(r))); + rootRootAnimation && console.warn('[inspection-chamber] Root root animation detected'); } const endTime = (animation: Animation) => (animation.effect?.getComputedTiming().endTime?.valueOf() as number) ?? 0; - inspectionChamber.longestAnimation = animations.reduce( - (acc, anim) => (endTime(anim) > endTime(acc) ? anim : acc), - animations[0] + inspectionChamber.longestAnimation = animations.reduce((acc, anim) => + endTime(anim) > endTime(acc) ? anim : acc ); - inspectionChamber.animationEndTime = ~~endTime(inspectionChamber.longestAnimation); const oldNames = new Set(); @@ -75,12 +96,12 @@ export async function retrieveViewTransitionAnimations() { initTwin(frameDoc, frameDoc, names, oldNames, newNames); updateNames(oldNames, newNames); - top!.document.querySelector('#canvas')!.style.zIndex = ''; } export function unleashAllAnimations() { const chamber = top!.__vtbag.inspectionChamber!; - chamber!.frameDocument!.querySelector('#vtbag-adopted-sheet')?.remove(); + const frameDocument = chamber!.frameDocument!; + frameDocument.querySelector('#vtbag-adopted-sheet')?.remove(); chamber.animations?.forEach((a) => { try { a.finish(); @@ -91,22 +112,64 @@ export function unleashAllAnimations() { } export function listAnimations(name: string) { - const row = (name: string, value: string) => - value ? `${name}${value}` : ''; + const row = (field: string, value: string) => + value + ? `${field}${value}` + : ''; + const meta = ['offset', 'computedOffset', 'easing', 'composite']; + const inspectionChamber = top!.__vtbag.inspectionChamber!; + const styleMap = inspectionChamber.styleMap!; const anim = top!.document.querySelector('#vtbag-ui-animations')!; anim.innerHTML = !vtActive() ? '' - : `

Animations of ${name}:

` + - animation('old') + - animation('new') + - animation('group') + - animation('image-pair'); + : `

Animations of ${name}:

` + + animationPanel2('group') + + animationPanel2('image-pair') + + animationPanel2('old') + + animationPanel2('new'); + + anim.querySelectorAll('input[type="checkbox"]').forEach((box) => { + const context = JSON.parse(box.dataset.vtbagContext!); + box.removeAttribute('data-vtbag-context'); + box.checked = selectAnimation(name, context.pseudo, context.idx)?.playState === 'paused'; + box.addEventListener('change', (e) => { + if (!stopAndGo(name, context.pseudo, context.idx, box.checked)) { + box.checked = !box.checked; + } + }); + }); plugInPanel(anim); - function animation(pseudo: string) { - const inspectionChamber = top!.__vtbag.inspectionChamber!; - const style = inspectionChamber.styleMap?.get(`${pseudo}-${name}`); + function animationPanel2(pseudo: string) { + const res: string[] = []; + const style = styleMap.get(`${pseudo}-${name}`) as CSSStyleDeclaration; + const cssAnimation = style?.animation; + const animationName = style?.animationName; + if (animationName && animationName !== 'vtbag-twin-noop' && animationName !== 'none') { + const animationNames = style.animationName.split(', '); + cssAnimation.split(/,(?![^(]*\))/).forEach((animation, idx) => { + res.push( + `
${pseudo}: ${animationNames[idx]}${details(animationNames[idx], animation.endsWith(animationName) ? animation.slice(0, -animationName.length) : animation)}
` + ); + }); + } + //const cssTtransition = style?.transition; + + return res.length > 0 ? res.join('') + '
' : ''; + + function details(keyframeName: string, animation: string) { + return ` + + ${row('animation:', animation)} + ${row('animates:', keyframeProperties(keyframeName))} + ${keyframes(keyframeName)} +
`; + } + } + + function animationPanel(pseudo: string) { + const style = styleMap.get(`${pseudo}-${name}`) as CSSStyleDeclaration; const animationName = style?.animationName; if (!style || !animationName || animationName === 'vtbag-twin-noop' || animationName === 'none') return ''; @@ -116,19 +179,19 @@ export function listAnimations(name: string) { const directions = style.animationDirection.split(', '); const fillModes = style.animationFillMode.split(', '); const iterationCounts = style.animationIterationCount.split(', '); - const timingFunctions = style.animationTimingFunction.replace(/\),/g, ')@').split('@ '); + const timingFunctions = style.animationTimingFunction.split(/,(?![^(]*\))/); const timelines = 'animationTimeline' in style ? (style['animationTimeline'] as string).split(', ') : []; const res: string[] = []; animationNames.forEach((animationName, idx) => { res.push( - `
${pseudo}: ${animationName}(${delays[idx % delays.length]}, ${durations[idx % durations.length]})${details(idx, animationName)}
` + `
${pseudo}: ${animationName}(${durations[idx % durations.length]}, ${delays[idx % delays.length]})${details(idx, animationName)}
` ); }); return res.join('') + '
'; - function details(idx: number, animationName: string) { + function details(idx: number, keyframeName: string) { return ` ${row('direction:', directions[idx % directions.length])} @@ -136,20 +199,83 @@ export function listAnimations(name: string) { ${row('iteration-count:', iterationCounts[idx % iterationCounts.length])} ${row('timing-function:', timingFunctions[idx % timingFunctions.length])} ${row('timeline:', timelines[idx % timelines.length])} - ${row('animates:', keyframeProperties(animationName))} + ${row('animates:', keyframeProperties(keyframeName))} + ${keyframes(keyframeName)}
`; + } + } + function keyframeProperties(name: string) { + const keys = new Set(); + inspectionChamber.keyframesMap + ?.get(name) + ?.forEach((k) => Object.keys(k).forEach((key) => keys.add(key))); + const meta = ['offset', 'computedOffset', 'easing', 'composite']; + return [...keys] + .filter((k) => !meta.includes(k)) + .sort() + .join(', '); + } - function keyframeProperties(name: string) { - const keys = new Set(); - inspectionChamber.keyframesMap - ?.get(name) - ?.forEach((k) => Object.keys(k).forEach((key) => keys.add(key))); - const meta = ['offset', 'computedOffset', 'easing', 'composite']; - return [...keys] - .filter((k) => !meta.includes(k)) - .sort() - .join(', '); - } + function keyframes(name: string) { + return inspectionChamber.keyframesMap + ?.get(name) + ?.map((key, idx) => + row( + +(key.computedOffset ?? 0) * 100 + '% :', + Object.keys(key) + .sort() + .filter((k) => !meta.includes(k)) + .map((k) => key[k]) + .join(', ') + ) + ) + .join(''); + } +} + +export function resetAnimationVisibility() { + top!.__vtbag.inspectionChamber!.animations?.forEach((anim) => { + if (anim instanceof CSSAnimation && anim.animationName !== 'vtbag-twin-noop' && anim.playState === 'idle') { + anim.pause(); } + }); + listAnimations( + top!.document.querySelector('#vtbag-ui-animations h4')!.dataset.vtbagName! + ); + control(); +} + +function stopAndGo(name: string, pseudo: any, idx: any, checked: boolean) { + const anim = selectAnimation(name, pseudo, idx); + if (!anim) return false; + if (checked) { + anim.pause(); + getModus() === 'full-control' && control(); + } else { + anim.cancel(); + } + return true; +} + +export function selectAnimation(name: string, pseudo: string, idx: number) { + const chamber = top!.__vtbag.inspectionChamber!; + const styleMap = chamber.styleMap!; + const animationName = styleMap.get(`${pseudo}-${name}`)!.animationName.split(', ')[idx]; + const animations = chamber.animations!; + const pseudoElement = `::view-transition-${pseudo}(${name})`; + const selected = animations.filter((anim) => anim.effect?.pseudoElement === pseudoElement); + if (idx >= selected.length) { + console.error('[injection chamber] no animation found with idx', idx); + return; + } + const result = selected[idx] ; + if (result instanceof CSSAnimation && result.animationName !== animationName) { + console.error( + '[injection chamber] animation name mismatch', + animationName, + result.animationName + ); + return; } + return result; }