first
This commit is contained in:
commit
eb122de994
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Cypress
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Vitest
|
||||
__screenshots__/
|
||||
52
README.md
Normal file
52
README.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Hover Tilt Card Design Website
|
||||
|
||||
这是一个基于 Vue.js 和 vanilla-tilt.js 构建的卡片设计网站,展示了多种具有 3D 悬停倾斜效果的卡片组件。
|
||||
|
||||
## 功能特点
|
||||
|
||||
- **3D 倾斜效果**:使用 vanilla-tilt.js 实现流畅的 3D 倾斜效果
|
||||
- **多种卡片类型**:
|
||||
- 基础倾斜卡片 (TiltCard)
|
||||
- 产品卡片 (ProductCard)
|
||||
- 个人资料卡片 (ProfileCard)
|
||||
- **响应式设计**:适配各种屏幕尺寸
|
||||
- **动态光影效果**:根据鼠标位置产生逼真的光影变化
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vue 3
|
||||
- vanilla-tilt.js
|
||||
- Vite
|
||||
- CSS Grid & Flexbox
|
||||
|
||||
## 组件说明
|
||||
|
||||
### TiltCard
|
||||
基础倾斜卡片组件,适用于展示一般信息内容,包含标题、描述和图标插槽。
|
||||
|
||||
### ProductCard
|
||||
产品展示卡片,专为电商产品设计,包含产品图片、价格和操作按钮。
|
||||
|
||||
### ProfileCard
|
||||
个人资料卡片,适合展示用户信息,包含头像、姓名、职位和个人简介。
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 克隆项目
|
||||
2. 安装依赖:`npm install`
|
||||
3. 启动开发服务器:`npm run dev`
|
||||
4. 访问 `http://localhost:5174`
|
||||
|
||||
## 自定义选项
|
||||
|
||||
所有卡片组件都支持自定义 vanilla-tilt 选项,包括:
|
||||
|
||||
- `max`: 最大倾斜角度
|
||||
- `speed`: 动画速度
|
||||
- `glare`: 是否启用眩光效果
|
||||
- `scale`: 倾斜时的缩放比例
|
||||
- `perspective`: 透视距离
|
||||
|
||||
## 设计理念
|
||||
|
||||
该项目旨在展示现代网页设计中的交互体验,通过微妙的 3D 倾斜效果增强用户参与感,同时保持界面简洁美观。
|
||||
15
index.html
Normal file
15
index.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script type="module" src="node_modules/hover-tilt/dist/hover-tilt.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
8
jsconfig.json
Normal file
8
jsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
2854
package-lock.json
generated
Normal file
2854
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "card",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tweakpane/plugin-essentials": "^0.2.1",
|
||||
"hover-tilt": "^1.0.0",
|
||||
"lodash-es": "^4.17.23",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"tweakpane": "^4.0.5",
|
||||
"vanilla-tilt": "^1.8.1",
|
||||
"vue": "^3.5.26",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"vite": "^7.3.0",
|
||||
"vite-plugin-vue-devtools": "^8.0.5"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
96
src/App.vue
Normal file
96
src/App.vue
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<main class="app-container">
|
||||
<header class="toolbar">
|
||||
<h1>Card Holographic Editor</h1>
|
||||
<div class="actions">
|
||||
<button @click="store.resetConfig">重置设计</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="preview-zone">
|
||||
<CardRoot />
|
||||
</section>
|
||||
|
||||
<div id="tweakpane-container"></div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { useCardStore } from '@/store/cardStore';
|
||||
import { useTweakpane } from '@/composables/useTweakpane';
|
||||
import CardRoot from '@/components/Card/CardRoot.vue';
|
||||
|
||||
const store = useCardStore();
|
||||
|
||||
// 在 App 级别初始化编辑器面板
|
||||
useTweakpane('tweakpane-container');
|
||||
|
||||
onMounted(() => {
|
||||
console.log('Editor Initialized with Config:', store.config.id);
|
||||
console.table(store.cssVariables)
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 全局基础样式清空 */
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #0f0f0f;
|
||||
color: white;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 2rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.preview-zone {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
perspective: 2000px; /* 为整体场景增加透视感 */
|
||||
}
|
||||
|
||||
.card-stage {
|
||||
/* 这个容器持有所有的 CSS 变量映射 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Tweakpane 容器样式 */
|
||||
#tweakpane-container {
|
||||
position: fixed;
|
||||
top: 70px; /* 避开 toolbar */
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* 针对移动端的微调 */
|
||||
@media (max-width: 768px) {
|
||||
#tweakpane-container {
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
90
src/assets/base.css
Normal file
90
src/assets/base.css
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
background: var(--color-background);
|
||||
}
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
1
src/assets/logo.svg
Normal file
1
src/assets/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
52
src/assets/main.css
Normal file
52
src/assets/main.css
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
@import './base.css';
|
||||
|
||||
|
||||
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: block;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
/* 初始化 hover-tilt 变量,防止计算未开始时出现 NaN */
|
||||
--tilt-x: 50%;
|
||||
--tilt-y: 50%;
|
||||
--tilt-active: 0;
|
||||
--tilt-r-x: 0deg;
|
||||
--tilt-r-y: 0deg;
|
||||
}
|
||||
|
||||
/* 所有的卡片容器都应该开启 3D 渲染上下文 */
|
||||
[v-tilt] {
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform;
|
||||
}
|
||||
57
src/components/Card/CardHolo.vue
Normal file
57
src/components/Card/CardHolo.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="holo-layer" :class="[config.type, { 'shift': config.colorShift }]" :style="holoStyle"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{ config: any }>();
|
||||
|
||||
const holoStyle = computed(() => ({
|
||||
'--o': props.config.opacity,
|
||||
'--bm': props.config.blendMode,
|
||||
// 使用 hover-tilt 自动注入的变量
|
||||
'--p-x': 'var(--tilt-x)',
|
||||
'--p-y': 'var(--tilt-y)',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.holo-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: var(--o);
|
||||
mix-blend-mode: var(--bm);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
background-size: 200% 200%;
|
||||
background-position: var(--p-x) var(--p-y);
|
||||
}
|
||||
|
||||
/* 1. 银河碎钻:使用噪点和点状渐变 */
|
||||
.galaxy {
|
||||
background-image:
|
||||
radial-gradient(circle at var(--p-x) var(--p-y), white 1%, transparent 2%),
|
||||
url('https://res.cloudinary.com/simey/image/upload/v1631535194/pokemon-cards/glitter.png');
|
||||
filter: brightness(1.2) contrast(1.5);
|
||||
}
|
||||
|
||||
/* 2. 黄金限定:使用深黄色到亮金色的线性渐变 */
|
||||
.GOLD .holo-layer {
|
||||
background-image: linear-gradient(110deg,
|
||||
transparent 40%, rgba(255,215,0,0.8) 45%, #fff 50%, rgba(255,215,0,0.8) 55%, transparent 60%
|
||||
);
|
||||
}
|
||||
|
||||
/* 3. 彩虹稀有:经典的光谱渐变 */
|
||||
.rainbow {
|
||||
background-image: linear-gradient(115deg,
|
||||
#ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #8b00ff
|
||||
);
|
||||
}
|
||||
|
||||
/* 色相旋转动画:让卡片动起来 */
|
||||
.shift {
|
||||
filter: hue-rotate(calc(var(--tilt-x) * 3.6deg));
|
||||
}
|
||||
</style>
|
||||
109
src/components/Card/CardRoot.vue
Normal file
109
src/components/Card/CardRoot.vue
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<template>
|
||||
<div class="scene">
|
||||
<div class="style-provider">
|
||||
<hover-tilt
|
||||
class="card-container"
|
||||
:max="store.config.interaction.tiltMax"
|
||||
:speed="store.config.interaction.speed"
|
||||
:perspective="store.config.interaction.perspective"
|
||||
:tilt-factor="store.config.interaction.tiltFactor"
|
||||
:tilt-factor-y="store.config.interaction.tiltFactorY"
|
||||
:scale-factor="store.config.interaction.scaleFactor"
|
||||
:enter-delay="store.config.interaction.enterDelay"
|
||||
:exit-delay="store.config.interaction.exitDelay"
|
||||
:spring-options="JSON.stringify(store.config.interaction.springOptions)"
|
||||
:tilt-spring-options="JSON.stringify(store.config.interaction.tiltSpringOptions)"
|
||||
:shadow="store.config.visualEffects.hoverTiltVisual.shadow"
|
||||
:shadow-blur="store.config.visualEffects.hoverTiltVisual.shadowBlur"
|
||||
:blend-mode="store.config.visualEffects.hoverTiltVisual.blendMode"
|
||||
:glare-intensity="store.config.visualEffects.hoverTiltVisual.glareIntensity"
|
||||
:glare-hue="store.config.visualEffects.hoverTiltVisual.glareHue"
|
||||
:glare-mask="store.config.visualEffects.hoverTiltVisual.glareMask"
|
||||
:glare-mask-mode="store.config.visualEffects.hoverTiltVisual.glareMaskMode"
|
||||
:glare-mask-composite="store.config.visualEffects.hoverTiltVisual.glareMaskComposite"
|
||||
:reverse="store.config.interaction.reverse"
|
||||
:start-x="store.config.interaction.startX"
|
||||
:start-y="store.config.interaction.startY"
|
||||
:scale="store.config.interaction.scale"
|
||||
:transition="store.config.interaction.transition"
|
||||
:axis="store.config.interaction.axis"
|
||||
:reset="store.config.interaction.reset"
|
||||
:reset-to-start="store.config.interaction.resetToStart"
|
||||
:easing="store.config.interaction.easing"
|
||||
|
||||
:glare="store.config.interaction.glare"
|
||||
:max-glare="store.config.interaction.maxGlare"
|
||||
:glare-prerender="store.config.interaction.glarePrerender"
|
||||
|
||||
|
||||
:mouse-event-element="store.config.interaction.mouseEventElement"
|
||||
:full-page-listening="store.config.interaction.fullPageListening"
|
||||
|
||||
:gyroscope-min-angle-x="store.config.interaction.gyroscopeMinAngleX"
|
||||
:gyroscope-max-angle-x="store.config.interaction.gyroscopeMaxAngleX"
|
||||
:gyroscope-min-angle-y="store.config.interaction.gyroscopeMinAngleY"
|
||||
:gyroscope-max-angle-y="store.config.interaction.gyroscopeMaxAngleY"
|
||||
:gyroscope-samples="store.config.interaction.gyroscopeSamples"
|
||||
:border-radius="store.config.interaction.borderRadius"
|
||||
>
|
||||
<div class="card-inner">
|
||||
<div class="card-content" :style="safeCssVariables">
|
||||
<img :src="store.config.content.imageUrl"/>
|
||||
</div>
|
||||
<CardHolo :config="store.config.visualEffects.holo"/>
|
||||
</div>
|
||||
</hover-tilt>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useCardStore} from '@/store/cardStore';
|
||||
import CardHolo from "@/components/Card/CardHolo.vue";
|
||||
import {computed} from 'vue';
|
||||
|
||||
const store = useCardStore();
|
||||
|
||||
// 确保 CSS 变量安全,避免无效值
|
||||
const safeCssVariables = computed(() => {
|
||||
const vars = {...store.cssVariables};
|
||||
// return ;
|
||||
// 确保所有值都是有效的 CSS 值
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
if (value === undefined || value === null || Number.isNaN(value)) {
|
||||
vars[key] = '0';
|
||||
}
|
||||
}
|
||||
|
||||
return vars;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 确保提供变量的容器不会破坏布局 */
|
||||
.style-provider {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.card-inner {
|
||||
/* 这里的变量会从 .style-provider 继承过来 */
|
||||
width: var(--card-width);
|
||||
border-radius: var(--card-radius);
|
||||
background: var(--card-bg);
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
::part(tilt){
|
||||
border-radius: var(--border-radius,50px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-content img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
</style>
|
||||
29
src/components/Editor/EditorPane.vue
Normal file
29
src/components/Editor/EditorPane.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div id="tweakpane-container" class="pane-wrapper"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTweakpane } from '@/composables/useTweakpane';
|
||||
|
||||
// 初始化 Tweakpane
|
||||
useTweakpane('tweakpane-container');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pane-wrapper {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
width: 280px;
|
||||
max-height: calc(100vh - 40px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 深度定制 Tweakpane 的样式,使其更符合你的 UI 风格 */
|
||||
:deep(.tp-dfv) {
|
||||
border-radius: 8px;
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
</style>
|
||||
44
src/composables/useTilt.ts
Normal file
44
src/composables/useTilt.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { ref, onMounted, onUnmounted, reactive } from 'vue';
|
||||
|
||||
// @ts-ignore
|
||||
export function useTilt(targetRef: ref<HTMLElement | null>) {
|
||||
const state = reactive({
|
||||
x: 0.5, y: 0.5,
|
||||
rx: 0, ry: 0,
|
||||
active: false
|
||||
});
|
||||
|
||||
let rafId: number;
|
||||
const target = { x: 0.5, y: 0.5 };
|
||||
|
||||
const update = () => {
|
||||
// 线性插值:当前值 + (目标值 - 当前值) * 系数
|
||||
state.x += (target.x - state.x) * 0.1;
|
||||
state.y += (target.y - state.y) * 0.1;
|
||||
|
||||
// 计算旋转角度
|
||||
state.rx = (state.y - 0.5) * -30; // 绕X轴转(上下倾斜)
|
||||
state.ry = (state.x - 0.5) * 30; // 绕Y轴转(左右倾斜)
|
||||
|
||||
rafId = requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (!targetRef.value) return;
|
||||
const { left, top, width, height } = targetRef.value.getBoundingClientRect();
|
||||
target.x = (e.clientX - left) / width;
|
||||
target.y = (e.clientY - top) / height;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
rafId = requestAnimationFrame(update);
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(rafId);
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
});
|
||||
|
||||
return { state };
|
||||
}
|
||||
258
src/composables/useTweakpane.ts
Normal file
258
src/composables/useTweakpane.ts
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
import { Pane } from 'tweakpane';
|
||||
import * as EssentialPlugins from '@tweakpane/plugin-essentials';
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { useCardStore } from '../store/cardStore';
|
||||
import { watch } from 'vue';
|
||||
import { CARD_PRESETS } from '@/constants/presets';
|
||||
|
||||
export function useTweakpane(containerId: string) {
|
||||
const store = useCardStore();
|
||||
let pane: Pane;
|
||||
|
||||
onMounted(() => {
|
||||
pane = new Pane({
|
||||
container: document.getElementById(containerId) as HTMLElement,
|
||||
title: '卡片配置编辑器',
|
||||
});
|
||||
pane.registerPlugin(EssentialPlugins);
|
||||
|
||||
// 1. 基础外观文件夹
|
||||
const appearance = pane.addFolder({ title: '外观样式' });
|
||||
appearance.addBinding(store.config.appearance, 'width', {
|
||||
min: 200, max: 1000, step: 10, label: '宽度'
|
||||
});
|
||||
appearance.addBinding(store.config.appearance, 'borderRadius', {
|
||||
min: 0, max: 50, step: 1, label: '圆角'
|
||||
});
|
||||
appearance.addBinding(store.config.appearance, 'backgroundColor', {
|
||||
label: '背景色'
|
||||
});
|
||||
|
||||
// 2. 交互配置 (Hover-tilt)
|
||||
const interaction = pane.addFolder({ title: '交互参数' });
|
||||
interaction.addBinding(store.config.interaction, 'tiltMax', {
|
||||
min: 0, max: 45, label: '最大倾斜'
|
||||
});
|
||||
interaction.addBinding(store.config.interaction, 'perspective', {
|
||||
min: 500, max: 2000, label: '透视距离'
|
||||
});
|
||||
|
||||
// 基础交互参数
|
||||
interaction.addBinding(store.config.interaction, 'scaleOnHover', {
|
||||
min: 0.5, max: 2, step: 0.05, label: '悬停缩放'
|
||||
});
|
||||
interaction.addBinding(store.config.interaction, 'speed', {
|
||||
min: 100, max: 2000, step: 100, label: '动画速度'
|
||||
});
|
||||
interaction.addBinding(store.config.interaction, 'gyroscope', {
|
||||
label: '陀螺仪效果'
|
||||
});
|
||||
|
||||
// Hover-tilt 特定选项
|
||||
interaction.addBinding(store.config.interaction, 'tiltFactor', {
|
||||
min: 0, max: 3, step: 0.1, label: '倾斜强度(X)'
|
||||
});
|
||||
interaction.addBinding(store.config.interaction, 'tiltFactorY', {
|
||||
min: 0, max: 3, step: 0.1, label: '倾斜强度(Y)'
|
||||
});
|
||||
interaction.addBinding(store.config.interaction, 'scaleFactor', {
|
||||
min: 0.5, max: 2, step: 0.05, label: '缩放系数'
|
||||
});
|
||||
interaction.addBinding(store.config.interaction, 'enterDelay', {
|
||||
min: 0, max: 1000, step: 50, label: '进入延迟(ms)'
|
||||
});
|
||||
interaction.addBinding(store.config.interaction, 'exitDelay', {
|
||||
min: 0, max: 1000, step: 50, label: '退出延迟(ms)'
|
||||
});
|
||||
|
||||
// 弹簧物理选项
|
||||
const springFolder = interaction.addFolder({ title: '弹簧物理' });
|
||||
springFolder.addBinding(store.config.interaction.springOptions, 'stiffness', {
|
||||
min: 0.01, max: 1, step: 0.01, label: '刚度'
|
||||
});
|
||||
springFolder.addBinding(store.config.interaction.springOptions, 'damping', {
|
||||
min: 0.01, max: 1, step: 0.01, label: '阻尼'
|
||||
});
|
||||
springFolder.addBinding(store.config.interaction.tiltSpringOptions, 'stiffness', {
|
||||
min: 0.01, max: 1, step: 0.01, label: '倾斜刚度'
|
||||
});
|
||||
springFolder.addBinding(store.config.interaction.tiltSpringOptions, 'damping', {
|
||||
min: 0.01, max: 1, step: 0.01, label: '倾斜阻尼'
|
||||
});
|
||||
|
||||
// vanilla-tilt.js 属性
|
||||
const vanillaTiltFolder = interaction.addFolder({ title: 'Vanilla-Tilt 属性' });
|
||||
vanillaTiltFolder.addBinding(store.config.interaction, 'reverse', {
|
||||
label: '反转倾斜方向'
|
||||
});
|
||||
vanillaTiltFolder.addBinding(store.config.interaction, 'startX', {
|
||||
min: -90, max: 90, step: 1, label: '起始X倾斜'
|
||||
});
|
||||
vanillaTiltFolder.addBinding(store.config.interaction, 'startY', {
|
||||
min: -90, max: 90, step: 1, label: '起始Y倾斜'
|
||||
});
|
||||
vanillaTiltFolder.addBinding(store.config.interaction, 'scale', {
|
||||
min: 0.5, max: 3, step: 0.1, label: '缩放比例'
|
||||
});
|
||||
vanillaTiltFolder.addBinding(store.config.interaction, 'transition', {
|
||||
label: '启用过渡动画'
|
||||
});
|
||||
vanillaTiltFolder.addBinding(store.config.interaction, 'axis', {
|
||||
options: { '无限制': 'xy', '仅X轴': 'x', '仅Y轴': 'y' },
|
||||
label: '限制轴向'
|
||||
});
|
||||
vanillaTiltFolder.addBinding(store.config.interaction, 'reset', {
|
||||
label: '退出时重置'
|
||||
});
|
||||
vanillaTiltFolder.addBinding(store.config.interaction, 'resetToStart', {
|
||||
label: '重置到起始位置'
|
||||
});
|
||||
vanillaTiltFolder.addBinding(store.config.interaction, 'easing', {
|
||||
label: '缓动函数'
|
||||
});
|
||||
|
||||
// 眩光效果
|
||||
const glareFolder = interaction.addFolder({ title: '眩光效果' });
|
||||
glareFolder.addBinding(store.config.interaction, 'glare', {
|
||||
label: '启用眩光'
|
||||
});
|
||||
glareFolder.addBinding(store.config.interaction, 'maxGlare', {
|
||||
min: 0, max: 1, step: 0.1, label: '最大眩光透明度'
|
||||
});
|
||||
glareFolder.addBinding(store.config.interaction, 'glarePrerender', {
|
||||
label: '预渲染眩光'
|
||||
});
|
||||
|
||||
// 陀螺仪设置
|
||||
const gyroFolder = interaction.addFolder({ title: '陀螺仪设置' });
|
||||
gyroFolder.addBinding(store.config.interaction, 'gyroscopeMinAngleX', {
|
||||
min: -90, max: 90, step: 1, label: '陀螺仪X最小角度'
|
||||
});
|
||||
gyroFolder.addBinding(store.config.interaction, 'gyroscopeMaxAngleX', {
|
||||
min: -90, max: 90, step: 1, label: '陀螺仪X最大角度'
|
||||
});
|
||||
gyroFolder.addBinding(store.config.interaction, 'gyroscopeMinAngleY', {
|
||||
min: -90, max: 90, step: 1, label: '陀螺仪Y最小角度'
|
||||
});
|
||||
gyroFolder.addBinding(store.config.interaction, 'gyroscopeMaxAngleY', {
|
||||
min: -90, max: 90, step: 1, label: '陀螺仪Y最大角度'
|
||||
});
|
||||
gyroFolder.addBinding(store.config.interaction, 'gyroscopeSamples', {
|
||||
min: 1, max: 50, step: 1, label: '陀螺仪采样数'
|
||||
});
|
||||
|
||||
// 3. 全息材质
|
||||
const visual = pane.addFolder({ title: '特效材质' });
|
||||
visual.addBinding(store.config.visualEffects.holo, 'type', {
|
||||
options: {
|
||||
None: 'none',
|
||||
Rainbow: 'rainbow',
|
||||
Sparkle: 'sparkle',
|
||||
Galaxy: 'galaxy'
|
||||
},
|
||||
label: '材质类型'
|
||||
});
|
||||
visual.addBinding(store.config.visualEffects.holo, 'opacity', {
|
||||
min: 0, max: 1, label: '透明度'
|
||||
});
|
||||
|
||||
// 4. Hover-tilt 视觉效果
|
||||
const hoverTiltVisual = pane.addFolder({ title: 'Hover-Tilt 视觉' });
|
||||
hoverTiltVisual.addBinding(store.config.visualEffects.hoverTiltVisual, 'shadow', {
|
||||
label: '动态阴影'
|
||||
});
|
||||
hoverTiltVisual.addBinding(store.config.visualEffects.hoverTiltVisual, 'shadowBlur', {
|
||||
min: 0, max: 100, step: 1, label: '阴影模糊'
|
||||
});
|
||||
hoverTiltVisual.addBinding(store.config.visualEffects.hoverTiltVisual, 'blendMode', {
|
||||
options: {
|
||||
'覆盖': 'overlay',
|
||||
'屏幕': 'screen',
|
||||
'正片叠底': 'multiply',
|
||||
'加亮': 'plus-lighter',
|
||||
'色彩减淡': 'color-dodge'
|
||||
},
|
||||
label: '混合模式'
|
||||
});
|
||||
hoverTiltVisual.addBinding(store.config.visualEffects.hoverTiltVisual, 'glareIntensity', {
|
||||
min: 0, max: 4, step: 0.1, label: '眩光强度'
|
||||
});
|
||||
hoverTiltVisual.addBinding(store.config.visualEffects.hoverTiltVisual, 'glareHue', {
|
||||
min: 0, max: 360, step: 1, label: '眩光色调'
|
||||
});
|
||||
hoverTiltVisual.addBinding(store.config.visualEffects.hoverTiltVisual, 'glareMask', {
|
||||
label: '眩光遮罩'
|
||||
});
|
||||
hoverTiltVisual.addBinding(store.config.visualEffects.hoverTiltVisual, 'glareMaskMode', {
|
||||
options: {
|
||||
'匹配源': 'match-source',
|
||||
'亮度': 'luminance',
|
||||
'Alpha': 'alpha',
|
||||
'无': 'none'
|
||||
},
|
||||
label: '遮罩模式'
|
||||
});
|
||||
hoverTiltVisual.addBinding(store.config.visualEffects.hoverTiltVisual, 'glareMaskComposite', {
|
||||
options: {
|
||||
'相加': 'add',
|
||||
'相减': 'subtract',
|
||||
'排除': 'exclude',
|
||||
'交集': 'intersect'
|
||||
},
|
||||
label: '遮罩合成'
|
||||
});
|
||||
|
||||
// 按钮操作
|
||||
const btn = pane.addButton({ title: '导出 JSON' });
|
||||
btn.on('click', () => {
|
||||
console.log(JSON.stringify(store.config, null, 2));
|
||||
alert('配置已输出到控制台');
|
||||
});
|
||||
|
||||
watch(() => store.config, () => {
|
||||
pane.refresh(); // Tweakpane 官方提供的 API,强制同步 UI 状态
|
||||
}, { deep: true });
|
||||
|
||||
|
||||
const presetFolder = pane.addFolder({ title: '样式预设' });
|
||||
|
||||
const presetOptions = {
|
||||
options: {
|
||||
'默认配置': 'DEFAULT',
|
||||
'银河碎钻': 'GALAXY',
|
||||
'黄金限定': 'GOLD',
|
||||
'彩虹稀有': 'RAINBOW'
|
||||
},
|
||||
label: '选择预设'
|
||||
};
|
||||
|
||||
const presetInput = presetFolder.addBinding({ preset: 'DEFAULT' }, 'preset', presetOptions);
|
||||
|
||||
presetInput.on('change', (ev) => {
|
||||
const selectedKey = ev.value as keyof typeof CARD_PRESETS;
|
||||
if (CARD_PRESETS[selectedKey]) {
|
||||
store.applyPreset(CARD_PRESETS[selectedKey]);
|
||||
|
||||
// 关键:预设改变后,通知 Tweakpane 刷新 UI 上的其他滑块位置
|
||||
pane.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
// 在 onMounted 内部末尾添加
|
||||
watch(
|
||||
() => store.config,
|
||||
() => {
|
||||
pane.refresh(); // 核心:让 Tweakpane 的滑块和颜色选择器同步最新的 Store 状态
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pane) pane.dispose();
|
||||
});
|
||||
|
||||
|
||||
|
||||
return { pane };
|
||||
}
|
||||
26
src/constants/presets.ts
Normal file
26
src/constants/presets.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export const CARD_PRESETS = {
|
||||
DEFAULT: {
|
||||
visualEffects: {
|
||||
holo: { type: 'none', opacity: 0, blendMode: 'normal', colorShift: false }
|
||||
},
|
||||
appearance: { backgroundColor: '#ffffff', borderRadius: 16 }
|
||||
},
|
||||
GALAXY: {
|
||||
visualEffects: {
|
||||
holo: { type: 'galaxy', opacity: 0.7, blendMode: 'color-dodge', colorShift: true }
|
||||
},
|
||||
appearance: { backgroundColor: '#1a1a2e', borderRadius: 20 }
|
||||
},
|
||||
GOLD: {
|
||||
visualEffects: {
|
||||
holo: { type: 'rainbow', opacity: 0.4, blendMode: 'overlay', colorShift: false }
|
||||
},
|
||||
appearance: { backgroundColor: '#f9d423', borderRadius: 12 }
|
||||
},
|
||||
RAINBOW: {
|
||||
visualEffects: {
|
||||
holo: { type: 'rainbow', opacity: 0.6, blendMode: 'exclusion', colorShift: true }
|
||||
},
|
||||
appearance: { backgroundColor: '#efefef', borderRadius: 16 }
|
||||
}
|
||||
};
|
||||
0
src/directives/tilt.ts
Normal file
0
src/directives/tilt.ts
Normal file
16
src/main.js
Normal file
16
src/main.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; // 导入插件
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate); // 核心步骤:使用插件
|
||||
app.use(pinia)
|
||||
app.mount('#app')
|
||||
12
src/router/index.js
Normal file
12
src/router/index.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
138
src/store/cardStore.ts
Normal file
138
src/store/cardStore.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { reactive, computed } from 'vue';
|
||||
import type { CardConfig } from '@/types/card';
|
||||
import { merge } from 'lodash-es'; // 推荐安装 lodash-es: pnpm add lodash-es @types/lodash-es
|
||||
export const useCardStore = defineStore('card', () => {
|
||||
// --- 1. State: 初始配置 ---
|
||||
const config = reactive<CardConfig>({
|
||||
version: "1.0",
|
||||
id: '222',
|
||||
metadata: { name: "我的定制卡片", createdAt: Date.now() },
|
||||
content: {
|
||||
imageUrl: "https://images.pokemontcg.io/swsh4/25_hires.png",
|
||||
imageFit: "cover",
|
||||
imageScale: 1,
|
||||
imageOffset: { x: 0, y: 0 }
|
||||
},
|
||||
appearance: {
|
||||
width: 300,
|
||||
aspectRatio: 0.714,
|
||||
borderRadius: 16,
|
||||
backgroundColor: "#1a1a1a",
|
||||
border: { width: 0, color: "#ffffff", style: "solid" },
|
||||
shadow: { x: 0, y: 15, blur: 30, spread: -10, color: "#000000", opacity: 0.5 }
|
||||
},
|
||||
visualEffects: {
|
||||
holo: { type: "galaxy", intensity: 0.8, blendMode: "color-dodge", opacity: 0.6, colorShift: true },
|
||||
glare: { enabled: true, opacity: 0.5, blendMode: "overlay" },
|
||||
hoverTiltVisual: {
|
||||
shadow: false,
|
||||
shadowBlur: 12,
|
||||
blendMode: "overlay",
|
||||
glareIntensity: 1,
|
||||
glareHue: 270,
|
||||
glareMask: "",
|
||||
glareMaskMode: "match-source",
|
||||
glareMaskComposite: "add",
|
||||
// vanilla-tilt.js 眩光相关
|
||||
glareEnable: false,
|
||||
glarePosition: "bottom",
|
||||
glareBorderRadius: "0px",
|
||||
glareOpacity: 0.5
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
// 基础交互参数
|
||||
tiltMax: 20,
|
||||
perspective: 1200,
|
||||
scaleOnHover: 1.05,
|
||||
speed: 800,
|
||||
gyroscope: true,
|
||||
|
||||
// Hover-tilt 特定选项
|
||||
tiltFactor: 1,
|
||||
tiltFactorY: 1,
|
||||
scaleFactor: 1,
|
||||
enterDelay: 0,
|
||||
exitDelay: 200,
|
||||
|
||||
// vanilla-tilt.js 属性
|
||||
reverse: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
scale: 1,
|
||||
transition: true,
|
||||
axis: 'xy', // 无限制
|
||||
reset: true,
|
||||
resetToStart: true,
|
||||
easing: "cubic-bezier(.03,.98,.52,.99)",
|
||||
|
||||
// 眩光效果
|
||||
glare: false,
|
||||
maxGlare: 1,
|
||||
glarePrerender: false,
|
||||
|
||||
// 事件监听
|
||||
mouseEventElement: '', // 空字符串表示默认元素
|
||||
fullPageListening: false,
|
||||
|
||||
// 陀螺仪设置
|
||||
gyroscopeMinAngleX: -45,
|
||||
gyroscopeMaxAngleX: 45,
|
||||
gyroscopeMinAngleY: -45,
|
||||
gyroscopeMaxAngleY: 45,
|
||||
gyroscopeSamples: 10,
|
||||
|
||||
// 弹簧物理选项
|
||||
springOptions: {
|
||||
stiffness: 0.2,
|
||||
damping: 0.8
|
||||
},
|
||||
tiltSpringOptions: {
|
||||
stiffness: 0.2,
|
||||
damping: 0.8
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- 2. Getters: 核心计算逻辑 ---
|
||||
// 将 Config 映射为 CSS 变量,供组件直接通过 :style 使用
|
||||
// src/store/cardStore.ts
|
||||
const cssVariables = computed(() => {
|
||||
const conf = config.appearance;
|
||||
const s = conf.shadow;
|
||||
|
||||
return {
|
||||
'width': `${conf.width}px`,
|
||||
'--border-radius': `${conf.borderRadius}px`,
|
||||
'background-color': conf.backgroundColor,
|
||||
|
||||
// ✅ 修复:将 shadow.offset 对象转化为字符串
|
||||
// 假设你的 2DPad 绑定的是 s.offset
|
||||
'--shadow-x': `${s.x || 0}px`,
|
||||
'--shadow-y': `${s.y || 0}px`,
|
||||
|
||||
// 组合成完整的阴影字符串
|
||||
'--card-shadow': `${s.x || 0}px ${s.y || 0}px ${s.blur}px ${s.spread}px ${s.color}`,
|
||||
|
||||
'--holo-opacity': config.visualEffects.holo.opacity,
|
||||
};
|
||||
});
|
||||
|
||||
// --- 3. Actions: 副作用处理 ---
|
||||
const resetConfig = () => {
|
||||
// 实际开发中可以存储一个 default 副本进行 Object.assign
|
||||
location.reload();
|
||||
};
|
||||
|
||||
const applyPreset = (preset: Partial<CardConfig>) => {
|
||||
// 深度合并配置(可使用 lodash-es 的 merge)
|
||||
Object.assign(config.visualEffects.holo, preset.visualEffects?.holo);
|
||||
Object.assign(config.appearance, preset.appearance);
|
||||
merge(config, preset);
|
||||
};
|
||||
|
||||
return { config, cssVariables, resetConfig, applyPreset };
|
||||
}, {
|
||||
persist: true // 开启插件持久化
|
||||
});
|
||||
129
src/types/card.d.ts
vendored
Normal file
129
src/types/card.d.ts
vendored
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* 核心卡片配置对象
|
||||
*/
|
||||
export interface CardConfig {
|
||||
version: string; // 配置文件版本,例如 "1.0.0"
|
||||
id: string; // 唯一标识符
|
||||
metadata: {
|
||||
name: string; // 卡片名称(内部管理用)
|
||||
createdAt: number; // 创建时间戳
|
||||
};
|
||||
|
||||
// 1. 基础内容配置
|
||||
content: {
|
||||
imageUrl: string; // 图片资源地址(支持 base64 或 URL)
|
||||
imageFit: 'cover' | 'contain' | 'fill';
|
||||
imageScale: number; // 图片内部缩放 (1.0 = 原大)
|
||||
imageOffset: { x: number; y: number }; // 图片在容器内的偏移
|
||||
};
|
||||
|
||||
// 2. 容器样式配置
|
||||
appearance: {
|
||||
width: number; // 卡片宽度
|
||||
aspectRatio: number; // 宽高比 (例如 0.714)
|
||||
borderRadius: number; // 圆角大小 (px)
|
||||
backgroundColor: string; // 兜底背景色
|
||||
border: {
|
||||
width: number;
|
||||
color: string;
|
||||
style: 'solid' | 'double' | 'groove';
|
||||
};
|
||||
shadow: {
|
||||
x: number;
|
||||
y: number;
|
||||
blur: number;
|
||||
spread: number;
|
||||
color: string;
|
||||
opacity: number;
|
||||
};
|
||||
};
|
||||
|
||||
// 3. 交互逻辑配置 (Hover-tilt 映射)
|
||||
interaction: {
|
||||
// 基础交互参数
|
||||
tiltMax: number; // 最大倾斜角度
|
||||
perspective: number; // 透视距离
|
||||
scaleOnHover: number; // 悬停时的放大倍数
|
||||
speed: number; // 过渡动画速度
|
||||
gyroscope: boolean; // 是否开启移动端陀螺仪
|
||||
|
||||
// Hover-tilt 特定选项
|
||||
tiltFactor: number; // 水平倾斜强度
|
||||
tiltFactorY: number; // 垂直倾斜强度
|
||||
scaleFactor: number; // 缩放系数
|
||||
enterDelay: number; // 进入延迟 (毫秒)
|
||||
exitDelay: number; // 退出延迟 (毫秒)
|
||||
|
||||
// vanilla-tilt.js 属性
|
||||
reverse: boolean; // 反转倾斜方向
|
||||
startX: number; // X轴起始倾斜角度
|
||||
startY: number; // Y轴起始倾斜角度
|
||||
scale: number; // 缩放比例
|
||||
transition: boolean; // 是否设置进入/退出过渡
|
||||
axis: 'x' | 'y' | 'xy'; // 启用哪个轴(xy表示无限制)
|
||||
reset: boolean; // 退出时是否重置倾斜效果
|
||||
resetToStart: boolean; // 退出重置是否回到起始位置
|
||||
easing: string; // 进入/退出缓动函数
|
||||
|
||||
// 眩光效果
|
||||
glare: boolean; // 是否启用眩光效果
|
||||
maxGlare: number; // 最大眩光透明度
|
||||
glarePrerender: boolean; // 是否预先渲染眩光元素
|
||||
|
||||
// 事件监听
|
||||
mouseEventElement: string; // 监听鼠标事件的元素选择器
|
||||
fullPageListening: boolean; // 是否在整个文档上监听鼠标事件
|
||||
|
||||
// 陀螺仪设置
|
||||
gyroscopeMinAngleX: number; // X轴陀螺仪最小角度
|
||||
gyroscopeMaxAngleX: number; // X轴陀螺仪最大角度
|
||||
gyroscopeMinAngleY: number; // Y轴陀螺仪最小角度
|
||||
gyroscopeMaxAngleY: number; // Y轴陀螺仪最大角度
|
||||
gyroscopeSamples: number; // 陀螺仪采样数量
|
||||
|
||||
// 弹簧物理选项
|
||||
springOptions: {
|
||||
stiffness: number; // 刚度
|
||||
damping: number; // 阻尼
|
||||
};
|
||||
tiltSpringOptions: {
|
||||
stiffness: number; // 倾斜动画的刚度
|
||||
damping: number; // 倾斜动画的阻尼
|
||||
};
|
||||
};
|
||||
|
||||
// 4. 视觉效果配置
|
||||
visualEffects: {
|
||||
holo: {
|
||||
type: 'none' | 'rainbow' | 'sparkle' | 'galaxy' | 'glass';
|
||||
intensity: number; // 0-1 强度
|
||||
blendMode: string; // 例如 'color-dodge', 'screen'
|
||||
opacity: number; // 全息层透明度
|
||||
colorShift: boolean; // 是否随角度色相旋转
|
||||
};
|
||||
glare: {
|
||||
enabled: boolean;
|
||||
opacity: number;
|
||||
blendMode: string;
|
||||
};
|
||||
|
||||
// Hover-tilt 视觉效果
|
||||
hoverTiltVisual: {
|
||||
shadow: boolean; // 是否启用动态阴影
|
||||
shadowBlur: number; // 阴影模糊半径
|
||||
blendMode: string; // 晕影效果混合模式
|
||||
glareIntensity: number; // 晕影效果强度
|
||||
glareHue: number; // 晕影色调值 (0-360)
|
||||
glareMask: string; // 晕影遮罩
|
||||
glareMaskMode: 'match-source' | 'luminance' | 'alpha' | 'none'; // 遮罩模式
|
||||
glareMaskComposite: 'add' | 'subtract' | 'exclude' | 'intersect'; // 遮罩合成
|
||||
|
||||
// vanilla-tilt.js 眩光相关
|
||||
glareEnable: boolean; // 启用眩光效果
|
||||
glarePosition: string; // 眩光位置
|
||||
glareBorderRadius: string; // 眩光圆角
|
||||
glareOpacity: number; // 眩光透明度
|
||||
};
|
||||
overlayTexture?: string; // 可选的纹理贴图(如磨砂、拉丝)
|
||||
};
|
||||
}
|
||||
137
test.html
Normal file
137
test.html
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>CSS Holographic Card Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--color1: #00e7ff;
|
||||
--color2: #ff00e7;
|
||||
--back: #1a1a1a;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--back);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
/* 卡片容器:设置透视距离 */
|
||||
.card-container {
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
/* 卡片主体 */
|
||||
.card {
|
||||
width: 320px;
|
||||
height: 450px;
|
||||
position: relative;
|
||||
border-radius: 15px;
|
||||
background-image: url('https://images.pokemontcg.io/swsh4/25_hires.png'); /* 示例卡面 */
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.5);
|
||||
|
||||
/* 关键:通过 JS 更新变量 */
|
||||
transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
|
||||
transition: transform 0.1s ease-out;
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 全息层:彩虹光泽 */
|
||||
.card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
|
||||
/* 核心:利用渐变和混合模式 */
|
||||
background: linear-gradient(
|
||||
115deg,
|
||||
transparent 20%,
|
||||
var(--color1) 36%,
|
||||
var(--color2) 43%,
|
||||
var(--color1) 50%,
|
||||
var(--color2) 57%,
|
||||
var(--color1) 64%,
|
||||
transparent 80%
|
||||
);
|
||||
background-size: 300% 300%;
|
||||
background-position: var(--pos-x, 50%) var(--pos-y, 50%);
|
||||
|
||||
/* 混合模式:这是全息感的来源 */
|
||||
mix-blend-mode: color-dodge;
|
||||
opacity: var(--opacity, 0);
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
/* 光斑层:模拟点光源直射 */
|
||||
.card::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
background: radial-gradient(
|
||||
circle at var(--pos-x, 50%) var(--pos-y, 50%),
|
||||
rgba(255, 255, 255, 0.6) 0%,
|
||||
transparent 60%
|
||||
);
|
||||
mix-blend-mode: overlay;
|
||||
opacity: var(--opacity, 0);
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="card-container">
|
||||
<div class="card" id="card"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const card = document.getElementById('card');
|
||||
|
||||
card.addEventListener('mousemove', (e) => {
|
||||
const rect = card.getBoundingClientRect();
|
||||
|
||||
// 计算鼠标在卡片上的相对坐标 (0 to 1)
|
||||
const x = (e.clientX - rect.left) / rect.width;
|
||||
const y = (e.clientY - rect.top) / rect.height;
|
||||
|
||||
// 计算旋转角度 (最大旋转 20 度)
|
||||
const rx = (y - 0.5) * -40; // 垂直旋转
|
||||
const ry = (x - 0.5) * 40; // 水平旋转
|
||||
|
||||
// 计算渐变背景的位置 (根据鼠标反向移动增加灵动感)
|
||||
const posX = 100 - x * 100;
|
||||
const posY = 100 - y * 100;
|
||||
|
||||
// 更新 CSS 变量
|
||||
card.style.setProperty('--rx', `${rx}deg`);
|
||||
card.style.setProperty('--ry', `${ry}deg`);
|
||||
card.style.setProperty('--pos-x', `${posX}%`);
|
||||
card.style.setProperty('--pos-y', `${posY}%`);
|
||||
card.style.setProperty('--opacity', `1`);
|
||||
});
|
||||
|
||||
// 鼠标移出重置
|
||||
card.addEventListener('mouseleave', () => {
|
||||
card.style.setProperty('--rx', `0deg`);
|
||||
card.style.setProperty('--ry', `0deg`);
|
||||
card.style.setProperty('--opacity', `0`);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
29
vite.config.js
Normal file
29
vite.config.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
// 告诉 Vue 编译器,所有以 'hover-' 开头的标签都是自定义元素
|
||||
isCustomElement: (tag) => tag.startsWith('hover-')
|
||||
}
|
||||
}
|
||||
}),
|
||||
vueDevTools(),
|
||||
],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user