diff --git a/.changeset/long-llamas-wash.md b/.changeset/long-llamas-wash.md new file mode 100644 index 0000000..9ee8f20 --- /dev/null +++ b/.changeset/long-llamas-wash.md @@ -0,0 +1,5 @@ +--- +'@vtbag/inspection-chamber': patch +--- + +Improves CCS information on animation panel diff --git a/README.md b/README.md index 7ec40b1..9b35049 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,17 @@ The @vtbag website can be found at https://vtbag.pages.dev/ ## !!! News !!! -The current version also includes smaller improvements, moving the Chamber closer to becoming a stable product rather than just a prototype. +You can now observe in real-time how CSS properties are updated by animations and easily see which other properties are applied to the pseudo-elements created by the View Transition API. + +The Chamber now also includes improved error reporting for missing animations or corrupted keyframe definitions. For details, see the [CHANGELOG](https://github.com/vtbag/inspection-chamber/blob/main/CHANGELOG.md) ## What happened so far: -v1.0.5 introduced the ability to switch single animations on and off as you navigate through the timeline of view transitions. For example, you can temporarily disable fades while investigating a morph animation or disable morphing to focus on other features of your transition. +Continuous smaller improvements, moving the Chamber closer to becoming a stable product rather than just a prototype. + +Added ability to switch single animations on and off as you navigate through the timeline of view transitions. For example, you can temporarily disable fades while investigating a morph animation or disable morphing to focus on other features of your transition. v1.0.4 improves opening the Chamber on mobiles, adds an explicit close button to the inner panel and fixes some styling issues. diff --git a/package-lock.json b/package-lock.json index 3922a9d..39acf62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@vtbag/inspection-chamber", - "version": "1.0.8", + "version": "1.0.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vtbag/inspection-chamber", - "version": "1.0.8", + "version": "1.0.9", "license": "ISC", "devDependencies": { "@changesets/cli": "^2.27.7", - "@eslint/js": "^9.9.0", + "@eslint/js": "^9.9.1", "@types/dom-view-transitions": "^1.0.5", "esbuild": "^0.23.1", "prettier": "^3.3.3", @@ -688,9 +688,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz", - "integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", + "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1337,9 +1337,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { "braces": "^3.0.3", diff --git a/package.json b/package.json index c21d980..54eaee4 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ }, "devDependencies": { "@changesets/cli": "^2.27.7", - "@eslint/js": "^9.9.0", + "@eslint/js": "^9.9.1", "@types/dom-view-transitions": "^1.0.5", "esbuild": "^0.23.1", "prettier": "^3.3.3", diff --git a/src/animations.ts b/src/animations.ts index bdb740a..218c426 100644 --- a/src/animations.ts +++ b/src/animations.ts @@ -1,3 +1,4 @@ +import { getNonDefaultPropNames, getNonDefaultProps } from './default-styles'; import { control, namesOfAnimation } from './panel/full-control'; import { plugInPanel } from './panel/inner'; import { getModus } from './panel/modus'; @@ -85,11 +86,15 @@ export async function retrieveViewTransitionAnimations() { const endTime = (animation: Animation) => (animation.effect?.getComputedTiming().endTime?.valueOf() as number) ?? 0; - inspectionChamber.longestAnimation = animations.reduce((acc, anim) => - endTime(anim) > endTime(acc) ? anim : acc - ); - inspectionChamber.animationEndTime = ~~endTime(inspectionChamber.longestAnimation); - + if (animations.length > 0) { + inspectionChamber.longestAnimation = animations.reduce((acc, anim) => + endTime(anim) > endTime(acc) ? anim : acc + ); + inspectionChamber.animationEndTime = ~~endTime(inspectionChamber.longestAnimation); + } else { + inspectionChamber.longestAnimation = undefined; + inspectionChamber.animationEndTime = 0; + } const oldNames = new Set(); const newNames = new Set(); @@ -111,11 +116,13 @@ export function unleashAllAnimations() { }); } +const row = (field: string, value: string) => + value + ? `${field}${value}` + : ''; + export function listAnimations(name: string) { - const row = (field: string, value: string) => - value - ? `${field}${value}` - : ''; + const allProps: Set = new Set(); const meta = ['offset', 'computedOffset', 'easing', 'composite']; const inspectionChamber = top!.__vtbag.inspectionChamber!; const styleMap = inspectionChamber.styleMap!; @@ -133,12 +140,17 @@ export function listAnimations(name: string) { 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) => { + box.addEventListener('change', () => { if (!stopAndGo(name, context.pseudo, context.idx, box.checked)) { box.checked = !box.checked; } }); }); + anim + .querySelectorAll('[data-vtbag-snapshot]') + .forEach((s) => s.addEventListener('toggle', updateSnapshots)); + updateLiveValues(); + updateSnapshots(); plugInPanel(anim); function animationPanel(pseudo: string) { @@ -146,6 +158,7 @@ export function listAnimations(name: string) { const style = styleMap.get(`${pseudo}-${name}`) as CSSStyleDeclaration; const cssAnimation = style?.animation; const animationName = style?.animationName; + allProps.clear(); if (animationName && animationName !== 'vtbag-twin-noop' && animationName !== 'none') { const animationNames = animationName.split(', '); let skipped = 0; @@ -156,22 +169,31 @@ export function listAnimations(name: string) { ); } else { res.push( - `🔴 ${pseudo}: ${animationNames[idx]}
` + `⚠️ ${pseudo}: keyframes ${animationNames[idx]} not found.
` ); ++skipped; - console.error( - `[inspection chamber] did not find keyframes named "${animationNames[idx]}" for ::view-transition-${pseudo}(${name})` - ); } }); + if (allProps.size > 0) { + res.push( + `
 🌀  ${pseudo}: live values
` + ); + } + } + + if (style) { + res.push( + `
 📸 ${pseudo}: CSS snapshot
` + ); } return res.length > 0 ? res.join('') + '
' : ''; function details(keyframeName: string, animation: string) { + const properties = keyframeProperties(keyframeName) || `⚠️ no properties? `; return ` ${row('animation:', animation)} - ${row('animates:', keyframeProperties(keyframeName))} + ${row('animates:', properties)} ${keyframes(keyframeName)}
`; } @@ -182,10 +204,9 @@ export function listAnimations(name: string) { inspectionChamber.keyframesMap ?.get(name) ?.forEach((k) => Object.keys(k).forEach((key) => keys.add(key))); - return [...keys] - .filter((k) => !meta.includes(k)) - .sort() - .join(', '); + const props = [...keys].filter((k) => !meta.includes(k)).sort(); + props.forEach((p) => allProps.add(p)); + return props.join(', '); } function keyframes(name: string) { @@ -255,3 +276,45 @@ export function selectAnimation(name: string, pseudo: string, idx: number) { } return result; } + +export function updateLiveValues() { + const name = + top!.document.querySelector('h4[data-vtbag-name]')?.dataset.vtbagName; + const chamber = top!.__vtbag.inspectionChamber!; + const styleMap = chamber.styleMap!; + top!.document + .querySelectorAll('[data-vtbag-live-values]') + .forEach((details) => { + const [pseudo, ...props] = details.dataset.vtbagLiveValues!.split(','); + const style = styleMap.get(`${pseudo}-${name}`) as CSSStyleDeclaration; + const values = props.map((p) => row(p + ':', style.getPropertyValue(p))); + details.innerHTML = + ` 🌀  ${pseudo}: live values` + + values.join('') + + '
'; + }); +} + +function updateSnapshots() { + const name = + top!.document.querySelector('h4[data-vtbag-name]')?.dataset.vtbagName; + const chamber = top!.__vtbag.inspectionChamber!; + const styleMap = chamber.styleMap!; + top!.document.querySelectorAll('[data-vtbag-snapshot]').forEach((details) => { + if (details.open) { + const pseudo = details.dataset.vtbagSnapshot; + const live: string[] = ( + details.previousElementSibling as HTMLElement + )?.dataset.vtbagLiveValues + ?.split(',') + .slice(1) ?? ['']; + const style = styleMap.get(`${pseudo}-${name}`) as CSSStyleDeclaration; + const values = getNonDefaultPropNames(style) + .filter((p) => !live.includes(p)) + .sort() + .map((p) => row(p + ':', style.getPropertyValue(p))); + details.innerHTML = + ` 📸 ${pseudo}: CSS snapshot` + values.join('') + '
'; + } + }); +} diff --git a/src/bench.txt b/src/bench.txt index d447061..c0d9497 100644 --- a/src/bench.txt +++ b/src/bench.txt @@ -7,7 +7,7 @@ - +
@@ -827,5 +827,9 @@ :is(#vtbag-ui-panel, #vtbag-ui-inner-panel) :is(p, hr) { margin-bottom: 8px; } + details[data-vtbag-live-values], + details[data-vtbag-snapshot] { + margin-top: 0.15rem; + } diff --git a/src/default-styles.ts b/src/default-styles.ts index fe441a5..6944004 100644 --- a/src/default-styles.ts +++ b/src/default-styles.ts @@ -1,4 +1,5 @@ -export const endProps = []; +const allProps: string[] = []; +const redundantProps = ['top', 'left', 'right', 'bottom']; const basicProps = [ 'animation', 'direction', @@ -19,7 +20,7 @@ let defaultStyleValues: Record; export function setNonDefaultProps(elemStyle: CSSStyleDeclaration, style: CSSStyleDeclaration) { defaultStyleValues ??= init(); - basicProps.forEach((prop) => { + allProps.forEach((prop) => { const val = style.getPropertyValue(prop); if (val !== defaultStyleValues[prop]) { elemStyle.setProperty(prop, val); @@ -27,16 +28,45 @@ export function setNonDefaultProps(elemStyle: CSSStyleDeclaration, style: CSSSty }); } +export function getNonDefaultProps(elemStyle: CSSStyleDeclaration) { + defaultStyleValues ??= init(); + const res: Record = {}; + allProps.forEach((prop) => { + const val = elemStyle.getPropertyValue(prop); + if (val !== defaultStyleValues[prop]) { + res[prop] = val; + } + }); + return res; +} + +export function getNonDefaultPropNames(elemStyle: CSSStyleDeclaration) { + defaultStyleValues ??= init(); + const res: string[] = []; + res.push('inset'); + allProps.forEach((prop) => { + const val = elemStyle.getPropertyValue(prop); + if (val !== defaultStyleValues[prop]) { + res.push(prop); + } + }); + return res.filter( + (prop) => + !(prop.startsWith('inset-') || prop.startsWith('animation-') || redundantProps.includes(prop)) + ); +} + function init() { const defaultDiv = top!.document.createElement('div'); top!.document.body.appendChild(defaultDiv); const defaultStyles = top!.getComputedStyle(defaultDiv); - const res = basicProps.reduce( - (acc: Record, prop: string) => ( - (acc[prop] = defaultStyles.getPropertyValue(prop)), acc - ), - {} - ); + const res: Record = {}; + + for (let i = 0; i < defaultStyles.length; i++) { + const prop = defaultStyles.item(i); + allProps.push(prop); + res[prop] = defaultStyles.getPropertyValue(prop); + } top!.document.body.removeChild(defaultDiv); return res; } diff --git a/src/dragging.ts b/src/dragging.ts index 9d601cd..977776d 100644 --- a/src/dragging.ts +++ b/src/dragging.ts @@ -9,12 +9,12 @@ export function initDragging( let startX: number; let startY: number; - const lX = (x: number) => x < 0 ? 0 : x > (innerWidth - 16) ? (innerWidth - 16) : x; - const lY = (y: number) => y < 0 ? 0 : y > (innerHeight - 16) ? (innerHeight - 16) : y; + const lX = (x: number) => (x < 0 ? 0 : x > innerWidth - 16 ? innerWidth - 16 : x); + const lY = (y: number) => (y < 0 ? 0 : y > innerHeight - 16 ? innerHeight - 16 : y); const xe = (e: TouchEvent | MouseEvent, delta = 0) => - lX(((e instanceof TouchEvent ? e.touches[0]?.clientX : e.clientX) ?? 0)) - delta; + lX((e instanceof TouchEvent ? e.touches[0]?.clientX : e.clientX) ?? 0) - delta; const ye = (e: TouchEvent | MouseEvent, delta = 0) => - lY(((e instanceof TouchEvent ? e.touches[0]?.clientY : e.clientY) ?? 0)) - delta; + lY((e instanceof TouchEvent ? e.touches[0]?.clientY : e.clientY) ?? 0) - delta; const startDragging = (e: TouchEvent | MouseEvent, t: HTMLElement) => { dragged = t; diff --git a/src/panel/full-control.ts b/src/panel/full-control.ts index 6080c17..b85a219 100644 --- a/src/panel/full-control.ts +++ b/src/panel/full-control.ts @@ -1,3 +1,4 @@ +import { updateLiveValues } from '../animations'; import { getModus } from './modus'; import { showSomeAnimations, THROTTLING_DELAY, updateNameVisibility } from './names'; import { vtActive } from './transition'; @@ -55,6 +56,7 @@ export function control() { } }); } + updateLiveValues(); } export function namesOfAnimation(animation: Animation) { diff --git a/src/styles.ts b/src/styles.ts index a4cc72c..2a426d4 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -36,15 +36,30 @@ function frame(namedOnly = false) { ::view-transition-old(*) { outline: 3px dashed darkslateblue; - border-radius: 5px; +} +[data-vtbag-transition-pseudo="old"] { + outline: 5px dashed darkslateblue; } ::view-transition-new(*) { outline: 3px dashed darkolivegreen; } +[data-vtbag-transition-pseudo="new"] { + outline: 5px dashed darkolivegreen; +} + ::view-transition-group(*), [data-vtbag-transition-name] { outline: 2px dotted darkgoldenrod; } +[data-vtbag-transition-pseudo="group"] { + outline: 3px dotted darkgoldenrod; +} +::view-transition-image-pair(*) { + outline: 1px solid #8888; +} +[data-vtbag-transition-pseudo="image-pair"] { + outline: 3px solid #8888; +} ::view-transition-group(*), ::view-transition-image-pair(*), ::view-transition-old(*), diff --git a/src/twin.ts b/src/twin.ts index 1ac9202..c0ea1e8 100644 --- a/src/twin.ts +++ b/src/twin.ts @@ -51,7 +51,7 @@ export function initTwin( } function twinId(name: string, pseudo: string) { - return pseudo ? `vtbag-twin--view-transition-${pseudo}-${name}` : 'vtbag-twin--view-transition'; + return name ? `vtbag-twin--view-transition-${pseudo}-${name}` : 'vtbag-twin--view-transition'; } export async function syncTwins(hidden: Set) { top!.document.documentElement.classList.add('vtbag-twin-sync');