From 8f2f22cc0b5bbd1380494fe64ac9fdb58851e093 Mon Sep 17 00:00:00 2001 From: Tamago Date: Mon, 4 Nov 2024 16:13:17 +0800 Subject: [PATCH 1/3] feat: support computed style --- package.json | 2 +- .../src/composables/use-computed-style.ts | 41 ++++++ packages/core/src/composables/use-hero.ts | 126 ++--------------- packages/core/src/composables/use-layout.ts | 128 ++++++++++++++++++ packages/core/src/directive/index.ts | 6 +- packages/core/src/types.ts | 6 +- packages/core/src/utils/index.ts | 10 ++ playgrounds/vite/src/App.vue | 7 +- pnpm-lock.yaml | 11 +- 9 files changed, 204 insertions(+), 133 deletions(-) create mode 100644 packages/core/src/composables/use-computed-style.ts create mode 100644 packages/core/src/composables/use-layout.ts create mode 100644 packages/core/src/utils/index.ts diff --git a/package.json b/package.json index 2372a33..c6140ad 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "vue": ">=3.0.0" }, "dependencies": { - "@tmg0/vueuse-extra": "0.0.2-alpha.2", + "@tmg0/vueuse-extra": "0.0.2-alpha.3", "@vueuse/core": "^11.2.0", "defu": "^6.1.4", "scule": "^1.3.0" diff --git a/packages/core/src/composables/use-computed-style.ts b/packages/core/src/composables/use-computed-style.ts new file mode 100644 index 0000000..5b98e02 --- /dev/null +++ b/packages/core/src/composables/use-computed-style.ts @@ -0,0 +1,41 @@ +import type { PermissiveTarget } from '../types' +import { watchImmediate } from '@vueuse/core' +import { camelCase } from 'scule' +import { nextTick, ref, toRef, unref } from 'vue' + +interface UseComputedStyleOptions { + parse: (v: string | number) => string | number + filter: (k: string, v: string | number) => boolean +} + +export function useComputedStyle(target: PermissiveTarget, options: Partial = {}) { + const domRef = toRef(target) + const style = ref>({}) + + watchImmediate(domRef, update) + + async function update() { + const dom = unref(domRef) + + if (!dom) + return + + await nextTick() + + const css = getComputedStyle(dom) + + for (let i = 0; i < css.length; i++) { + const o = css[i] + const k = camelCase(o) + const v = css.getPropertyValue(o) + if (options.filter && !options.filter(k, v)) + continue + style.value[k] = v ?? '' + } + } + + return { + style, + update, + } +} diff --git a/packages/core/src/composables/use-hero.ts b/packages/core/src/composables/use-hero.ts index d3b453f..7f891bb 100644 --- a/packages/core/src/composables/use-hero.ts +++ b/packages/core/src/composables/use-hero.ts @@ -1,10 +1,8 @@ +import type { MaybeRef } from 'vue' import type { HeroProps } from '../components/hero' -import { useStyle } from '@tmg0/vueuse-extra' -import { tryOnBeforeUnmount, tryOnMounted, useElementBounding } from '@vueuse/core' -import { useElementTransform, useMotion } from '@vueuse/motion' -import { defu } from 'defu' -import { computed, type MaybeRef, unref, watch } from 'vue' -import { type HeroContext, useHeroContext } from '../composables/use-hero-context' +import type { HeroContext } from '../composables/use-hero-context' +import { tryOnBeforeUnmount, tryOnMounted } from '@vueuse/core' +import { useLayout } from './use-layout' export interface UseHeroProps extends Omit { ignore?: string[] @@ -12,124 +10,16 @@ export interface UseHeroProps extends Omit { onComplete?: () => void } -export const defaultTransition = {} - -export function omit, K extends keyof T>(source: T, keys: K[] = []): Omit { - if (!keys.length) - return source - const picks: any = {} - for (const key in source) { - if (!keys.includes(key as unknown as K)) - picks[key] = source[key] - } - return picks as Omit -} - -function useBorderRadius(domRef: MaybeRef) { - return Number.parseInt(getComputedStyle(unref(domRef)!).borderRadius) -} - export function useHero(target: MaybeRef, options: MaybeRef, ctx?: HeroContext) { - let motionInstance: any - - const bounding: Record = { x: 0, y: 0, width: 0, height: 0 } - const { layouts, props: ctxProps } = ctx ?? useHeroContext() - const { height, width, x, y, update } = useElementBounding(target) - const props = computed(() => unref(options)) - const { transform } = useStyle(target) - - const scaleX = computed(() => transform.value.scaleX) - const scaleY = computed(() => transform.value.scaleY) - - const style = computed(() => ({ borderRadius: useBorderRadius(target), ...props.value?.style ?? {} })) - - const transition = computed(() => defu(props.value.transition ?? {}, ctxProps.value.transition ?? {}, defaultTransition)) - - const previous = computed({ - get() { - if (!props.value.layoutId) - return {} - return layouts.value[props.value.layoutId] ?? {} - }, - set(value) { - if (!props.value.layoutId) - return - layouts.value[props.value.layoutId] = value - }, - }) - - watch(() => [scaleX.value, scaleY.value], ([x, y]) => { - const elt = unref(target) - - if (!elt) - return - - for (let i = 0; i < elt.children.length; i++) { - const child = elt.children[i] as HTMLElement - child.style.transform = `scaleX(${1 / (x as number)}) scaleY(${1 / (y as number)})` - } - }) + const { scaleX, scaleY, setup, snapshot } = useLayout(target, options, ctx) tryOnMounted(setup) - tryOnBeforeUnmount(clean) - - function setup() { - update() - bounding.x = x.value + width.value / 2 - bounding.y = y.value + height.value / 2 - bounding.width = width.value - bounding.height = height.value - - let _y = 0 - if (previous.value.y) - _y = previous.value.y - bounding.y - - let _x = 0 - if (previous.value.x) - _x = previous.value.x - bounding.x - - const _transition = { - ...unref(transition), - onComplete: props.value.onComplete, - } - - const size = { width: bounding.width, height: bounding.height } - const scale = { x: previous.value.width / size.width, y: previous.value.height / size.height } - const previousBorderRadius = (previous.value?.borderRadius ?? 0) / scale.x - - const initial = { ...unref(previous), x: _x, y: _y, scaleX: scale.x, scaleY: scale.y, borderRadius: previousBorderRadius, ...size } - const enter = { ...style.value, x: 0, y: 0, scaleX: 1, scaleY: 1, ...size, transition: _transition } - - motionInstance = useMotion(unref(target), { - initial: omit(initial, props.value.ignore as any), - enter: omit(enter, props.value.ignore as any), - }) - } - - function clean() { - update() - bounding.x = x.value + width.value / 2 - bounding.y = y.value + height.value / 2 - const { transform } = useElementTransform(target) - bounding.x = bounding.x + (transform.x as number ?? 0) - bounding.y = bounding.y + (transform.y as number ?? 0) - bounding.z = bounding.z + (transform.x as number ?? 0) - const motionProperties = motionInstance ? motionInstance.motionProperties : style.value - const _props = { ...motionProperties, ...bounding, borderRadius: useBorderRadius(target) } - if (transform.scaleX) - _props.width = _props.width * (transform.scaleX as number ?? 1) - if (transform.scaleY) - _props.height = _props.height * (transform.scaleY as number ?? 1) - previous.value = _props - } + tryOnBeforeUnmount(snapshot) return { - bounding, - x, - y, scaleX, scaleY, - setup, - clean, + mounted: setup, + unmounted: snapshot, } } diff --git a/packages/core/src/composables/use-layout.ts b/packages/core/src/composables/use-layout.ts new file mode 100644 index 0000000..1a1d547 --- /dev/null +++ b/packages/core/src/composables/use-layout.ts @@ -0,0 +1,128 @@ +import type { UseHeroProps } from './use-hero' +import { useStyle } from '@tmg0/vueuse-extra' +import { useElementBounding } from '@vueuse/core' +import { useElementTransform, useMotion } from '@vueuse/motion' +import { defu } from 'defu' +import { computed, type MaybeRef, nextTick, unref, watch } from 'vue' +import { type HeroContext, useHeroContext } from '../composables/use-hero-context' +import { omit } from '../utils' +import { useComputedStyle } from './use-computed-style' + +export const defaultTransition = {} + +const STYLE_INCLUDES = [ + 'backgroundColor', + 'backgroundPosition', +] + +function useBorderRadius(domRef: MaybeRef) { + const dom = unref(domRef) + if (!dom) + return 0 + return Number.parseInt(getComputedStyle(dom).borderRadius) +} + +export function useLayout(target: MaybeRef, options: MaybeRef, ctx?: HeroContext) { + let motionInstance: any + + const bounding: Record = { x: 0, y: 0, width: 0, height: 0 } + const { layouts, props: ctxProps } = ctx ?? useHeroContext() + const { height, width, x, y, update } = useElementBounding(target) + const props = computed(() => unref(options)) + const { transform } = useStyle(target) + + const scaleX = computed(() => transform.value.scaleX) + const scaleY = computed(() => transform.value.scaleY) + + const { style: computedStyle } = useComputedStyle(target, { filter: k => STYLE_INCLUDES.includes(k) }) + const style = computed(() => ({ ...computedStyle.value, borderRadius: useBorderRadius(target), ...props.value?.style ?? {} })) + const transition = computed(() => defu(props.value.transition ?? {}, ctxProps.value.transition ?? {}, defaultTransition)) + + const previous = computed({ + get() { + if (!props.value.layoutId) + return {} + return layouts.value[props.value.layoutId] ?? {} + }, + set(value) { + if (!props.value.layoutId) + return + layouts.value[props.value.layoutId] = value + }, + }) + + watch(() => [scaleX.value, scaleY.value], ([x, y]) => { + const dom = unref(target) + + if (!dom) + return + + for (let i = 0; i < dom.children.length; i++) { + const child = dom.children[i] as HTMLElement + child.style.transform = `scaleX(${1 / (x as number)}) scaleY(${1 / (y as number)})` + } + }) + + async function setup() { + update() + bounding.x = x.value + width.value / 2 + bounding.y = y.value + height.value / 2 + bounding.width = width.value + bounding.height = height.value + + let _y = 0 + if (previous.value.y) + _y = previous.value.y - bounding.y + + let _x = 0 + if (previous.value.x) + _x = previous.value.x - bounding.x + + const _transition = { + ...unref(transition), + onComplete: props.value.onComplete, + } + + const size = { width: bounding.width, height: bounding.height } + const scale = { x: previous.value.width / size.width, y: previous.value.height / size.height } + const previousBorderRadius = (previous.value?.borderRadius ?? 0) / scale.x + await nextTick() + + const initial = { ...unref(previous), x: _x, y: _y, scaleX: scale.x, scaleY: scale.y, borderRadius: previousBorderRadius, ...size } + const enter = { ...style.value, x: 0, y: 0, scaleX: 1, scaleY: 1, ...size, transition: _transition } + + motionInstance = useMotion(unref(target), { + initial: omit(initial, props.value.ignore as any), + enter: omit(enter, props.value.ignore as any), + }) + + previous.value = enter + } + + function snapshot() { + update() + bounding.x = x.value + width.value / 2 + bounding.y = y.value + height.value / 2 + const { transform } = useElementTransform(target) + bounding.x = bounding.x + (transform.x as number ?? 0) + bounding.y = bounding.y + (transform.y as number ?? 0) + bounding.z = bounding.z + (transform.x as number ?? 0) + const motionProperties = motionInstance ? motionInstance.motionProperties : style.value + const _props = { ...motionProperties, ...bounding, borderRadius: useBorderRadius(target) } + if (transform.scaleX) + _props.width = _props.width * (transform.scaleX as number ?? 1) + if (transform.scaleY) + _props.height = _props.height * (transform.scaleY as number ?? 1) + previous.value = _props + } + + return { + bounding, + x, + y, + scaleX, + scaleY, + setup, + snapshot, + } +} diff --git a/packages/core/src/directive/index.ts b/packages/core/src/directive/index.ts index 7863903..e7488d7 100644 --- a/packages/core/src/directive/index.ts +++ b/packages/core/src/directive/index.ts @@ -6,7 +6,7 @@ export function directive() { const props = ref({}) const domRef = ref() - const { setup, clean } = useHero(domRef, props) + const { mounted, unmounted } = useHero(domRef, props) return { mounted(dom: HTMLElement | SVGElement, _: any, vnode: any) { @@ -17,11 +17,11 @@ export function directive() { }, {}) domRef.value = dom - setup() + mounted() }, beforeUnmount() { - clean() + unmounted() }, } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 09c012d..2ab16b6 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,3 +1,5 @@ -import type { Transition as MotionTransition } from '@vueuse/motion' +import type { MaybeRefOrGetter } from '@vueuse/core' -export type Transition = MotionTransition +export type { Transition } from '@vueuse/motion' + +export type PermissiveTarget = MaybeRefOrGetter diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts new file mode 100644 index 0000000..75fe859 --- /dev/null +++ b/packages/core/src/utils/index.ts @@ -0,0 +1,10 @@ +export function omit, K extends keyof T>(source: T, keys: K[] = []): Omit { + if (!keys.length) + return source + const picks: any = {} + for (const key in source) { + if (!keys.includes(key as unknown as K)) + picks[key] = source[key] + } + return picks as Omit +} diff --git a/playgrounds/vite/src/App.vue b/playgrounds/vite/src/App.vue index 615acc2..a8730f1 100644 --- a/playgrounds/vite/src/App.vue +++ b/playgrounds/vite/src/App.vue @@ -1,12 +1,11 @@