Skip to content

Commit

Permalink
deselect animations
Browse files Browse the repository at this point in the history
  • Loading branch information
martrapp committed Aug 9, 2024
1 parent b946e4e commit eb0e145
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 51 deletions.
5 changes: 5 additions & 0 deletions .changeset/thick-kangaroos-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vtbag/inspection-chamber': patch
---

Adds the ability to deselect single animations
228 changes: 177 additions & 51 deletions src/animations.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -25,48 +27,67 @@ export async function retrieveViewTransitionAnimations() {
const inspectionChamber = top!.__vtbag.inspectionChamber!;
const frameDoc = inspectionChamber.frameDocument!;
const animations: Animation[] = (inspectionChamber.animations = []);
const animationMap = (inspectionChamber.animationMap = new Map<string, Animation>());
const keyframesMap = (inspectionChamber.keyframesMap = new Map<string, Keyframe[]>());
const names = new Set<string>();

inspectionChamber.styleMap = new Map<string, CSSStyleDeclaration>();

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<string>();
Expand All @@ -75,12 +96,12 @@ export async function retrieveViewTransitionAnimations() {
initTwin(frameDoc, frameDoc, names, oldNames, newNames);

updateNames(oldNames, newNames);
top!.document.querySelector<HTMLCanvasElement>('#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();
Expand All @@ -91,22 +112,64 @@ export function unleashAllAnimations() {
}

export function listAnimations(name: string) {
const row = (name: string, value: string) =>
value ? `<tr><td style="text-align:right">${name}</td><td>${value}</td><tr>` : '';
const row = (field: string, value: string) =>
value
? `<tr><td style="text-align:right">${field}</td><td><tt><b>${value}</b></tt></td><tr>`
: '';
const meta = ['offset', 'computedOffset', 'easing', 'composite'];
const inspectionChamber = top!.__vtbag.inspectionChamber!;
const styleMap = inspectionChamber.styleMap!;

const anim = top!.document.querySelector<HTMLDivElement>('#vtbag-ui-animations')!;
anim.innerHTML = !vtActive()
? ''
: `<h4>Animations of ${name}:</h4>` +
animation('old') +
animation('new') +
animation('group') +
animation('image-pair');
: `<h4 data-vtbag-name=${name}>Animations of ${name}:</h4>` +
animationPanel2('group') +
animationPanel2('image-pair') +
animationPanel2('old') +
animationPanel2('new');

anim.querySelectorAll<HTMLInputElement>('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(
`<details><summary><input type="checkbox" data-vtbag-context='{"pseudo":"${pseudo}","idx":${idx}}'/> ${pseudo}: <tt>${animationNames[idx]}</tt></summary>${details(animationNames[idx], animation.endsWith(animationName) ? animation.slice(0, -animationName.length) : animation)}</details>`
);
});
}
//const cssTtransition = style?.transition;

return res.length > 0 ? res.join('') + '<hr>' : '';

function details(keyframeName: string, animation: string) {
return `
<table>
${row('animation:', animation)}
${row('animates:', keyframeProperties(keyframeName))}
${keyframes(keyframeName)}
</table>`;
}
}

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 '';
Expand All @@ -116,40 +179,103 @@ 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(
`<details><summary>${pseudo}: ${animationName}(${delays[idx % delays.length]}, ${durations[idx % durations.length]})</summary>${details(idx, animationName)}</details>`
`<details><summary><input type="checkbox" data-vtbag-context='{"pseudo":"${pseudo}","idx":${idx}}'/> ${pseudo}: <tt>${animationName}(${durations[idx % durations.length]}, ${delays[idx % delays.length]})</tt></summary>${details(idx, animationName)}</details>`
);
});
return res.join('') + '<hr>';

function details(idx: number, animationName: string) {
function details(idx: number, keyframeName: string) {
return `
<table>
${row('direction:', directions[idx % directions.length])}
${row('fill-mode:', fillModes[idx % fillModes.length])}
${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)}
</table>`;
}
}
function keyframeProperties(name: string) {
const keys = new Set<string>();
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<string>();
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<HTMLHeadingElement>('#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;
}

0 comments on commit eb0e145

Please sign in to comment.