feat(theme): add home hero effects (#738)
@ -82,7 +82,7 @@ export default defineUserConfig({
|
||||
// provider: 'algolia',
|
||||
// appId: '',
|
||||
// apiKey: '',
|
||||
// indexName: '',
|
||||
// indices: [''],
|
||||
// },
|
||||
|
||||
/**
|
||||
|
||||
@ -5,7 +5,7 @@ config:
|
||||
-
|
||||
type: hero
|
||||
full: true
|
||||
background: tint-plate
|
||||
effect: lightning
|
||||
hero:
|
||||
name: Theme Plume
|
||||
tagline: VuePress Next Theme
|
||||
|
||||
@ -5,7 +5,7 @@ config:
|
||||
-
|
||||
type: hero
|
||||
full: true
|
||||
background: tint-plate
|
||||
effect: lightning
|
||||
hero:
|
||||
name: Theme Plume
|
||||
tagline: VuePress Next Theme
|
||||
|
||||
@ -172,7 +172,7 @@ export const themeGuide: ThemeCollectionItem = defineCollection({
|
||||
collapsed: false,
|
||||
prefix: 'custom',
|
||||
items: [
|
||||
'home',
|
||||
{ text: 'Custom Homepage', link: 'home', items: ['home-hero-effect'] },
|
||||
'style',
|
||||
'slots',
|
||||
'component-overrides',
|
||||
|
||||
@ -172,7 +172,7 @@ export const themeGuide: ThemeCollectionItem = defineCollection({
|
||||
collapsed: false,
|
||||
prefix: 'custom',
|
||||
items: [
|
||||
'home',
|
||||
{ text: '自定义首页', link: 'home', items: ['home-hero-effect'] },
|
||||
'style',
|
||||
'slots',
|
||||
'component-overrides',
|
||||
|
||||
BIN
docs/.vuepress/public/images/hero-effects/beams.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
docs/.vuepress/public/images/hero-effects/dot-grid.png
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
docs/.vuepress/public/images/hero-effects/hyper-speed.png
Normal file
|
After Width: | Height: | Size: 689 KiB |
BIN
docs/.vuepress/public/images/hero-effects/iridescence.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/.vuepress/public/images/hero-effects/lightning.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/.vuepress/public/images/hero-effects/liquid-ether.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
docs/.vuepress/public/images/hero-effects/orb.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/.vuepress/public/images/hero-effects/pixel-blast.png
Normal file
|
After Width: | Height: | Size: 348 KiB |
BIN
docs/.vuepress/public/images/hero-effects/prism.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
docs/.vuepress/public/images/hero-effects/tint-plate.png
Normal file
|
After Width: | Height: | Size: 770 KiB |
@ -5,7 +5,8 @@ config:
|
||||
-
|
||||
type: hero
|
||||
full: true
|
||||
background: tint-plate
|
||||
effect: lightning
|
||||
forceDark: true
|
||||
hero:
|
||||
name: Theme Plume
|
||||
tagline: VuePress Next Theme
|
||||
|
||||
@ -4,7 +4,8 @@ config:
|
||||
-
|
||||
type: hero
|
||||
full: true
|
||||
background: tint-plate
|
||||
effect: lightning
|
||||
forceDark: true
|
||||
hero:
|
||||
name: Theme Plume
|
||||
tagline: VuePress Next Theme
|
||||
|
||||
1306
docs/en/guide/custom/home-hero-effect.md
Normal file
@ -140,8 +140,6 @@ config:
|
||||
|
||||
Suitable for documentation-type sites, placed at the top.
|
||||
|
||||
**Tool Support: [Homepage Hero Tint Plate Configuration Tool](../../tools/home-hero-tint-plate.md)**
|
||||
|
||||
```ts
|
||||
interface PlumeThemeHomeHero extends PlumeHomeConfigBase {
|
||||
type: 'hero'
|
||||
@ -160,97 +158,22 @@ interface PlumeThemeHomeHero extends PlumeHomeConfigBase {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Background image. "tint-plate" is a preset effect, or a custom image URL can be provided.
|
||||
* The built-in background effects of the theme, if they are not preset background effects, allow for a background image link address to be passed in.
|
||||
*/
|
||||
background?: 'tint-plate' | string
|
||||
|
||||
effect?: 'tint-plate' | 'prism' | 'pixel-blast' | 'hyper-speed' | 'liquid-ether'
|
||||
| 'dot-grid' | 'iridescence' | 'orb' | 'beams' | 'lightning' | string
|
||||
/**
|
||||
* When background is the preset, configure RGB values to adjust the background color.
|
||||
* This configuration only takes effect when `background` is set to `tint-plate`.
|
||||
* Background effect configuration options vary depending on the value of `effect`.
|
||||
*/
|
||||
tintPlate?: TintPlate
|
||||
effectConfig?: any
|
||||
/**
|
||||
* If using a non-preset background, set the filter effect for the background image.
|
||||
*/
|
||||
filter?: string
|
||||
}
|
||||
interface TintPlateObj {
|
||||
// value represents the base color value, range 0 ~ 255
|
||||
// offset represents the offset from the base value, range 0 ~ (255 - value)
|
||||
r: { value: number, offset: number }
|
||||
g: { value: number, offset: number }
|
||||
b: { value: number, offset: number }
|
||||
}
|
||||
type TintPlate
|
||||
= | number // e.g., 210
|
||||
| string // e.g., '210,210,210' => red,green,blue
|
||||
// e.g., { r: { value: 220, offset: 36 }, g: { value: 220, offset: 36 }, b: { value: 220, offset: 36 } }
|
||||
| TintPlateObj
|
||||
// e.g., { light: 210, dark: 20 }
|
||||
// e.g., { light: '210,210,210', dark: '20,20,20' }
|
||||
| { light: number | string, dark: number | string }
|
||||
| { light: TintPlateObj, dark: TintPlateObj }
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```md
|
||||
---
|
||||
home: true
|
||||
config:
|
||||
-
|
||||
type: hero
|
||||
full: true
|
||||
background: tint-plate
|
||||
hero:
|
||||
name: Theme Plume
|
||||
tagline: Vuepress Next Theme
|
||||
text: A minimalistic, feature-rich vuepress documentation & blog theme
|
||||
actions:
|
||||
-
|
||||
theme: brand
|
||||
text: Get Started →
|
||||
link: /
|
||||
-
|
||||
theme: alt
|
||||
text: Github
|
||||
link: https://github.com/pengzhanbo/vuepress-theme-plume
|
||||
---
|
||||
```
|
||||
|
||||
**Result:**
|
||||
|
||||
:::demo-wrapper img no-padding
|
||||
<img src="/images/custom-hero.jpg" alt="Theme Plume" />
|
||||
:::
|
||||
|
||||
When `background` is configured as `tint-plate`, you can additionally configure `tintPlate` to adjust
|
||||
the background hue, with a range of `0 ~ 255`:
|
||||
|
||||
```md
|
||||
---
|
||||
home: true
|
||||
config:
|
||||
-
|
||||
type: hero
|
||||
full: true
|
||||
background: tint-plate
|
||||
tintPlate: 210
|
||||
---
|
||||
```
|
||||
|
||||
`tintPlate` is used to configure RGB values:
|
||||
|
||||
- When configured as a single number, it sets the red, green, and blue color channels to the same value (range: 0 - 255). Example: `210`.
|
||||
- When configured as three comma-separated values, it sets the red, green, and blue channels to different values (range: 0 - 255). Example: `210,210,210`.
|
||||
- When configured as a `TintPlateObj`, it allows more granular control over each color channel and its corresponding offset.
|
||||
- It can also be configured as `{ light, dark }` to use different color values in dark and light modes.
|
||||
|
||||
::: info
|
||||
To facilitate the configuration of aesthetically pleasing and personalized backgrounds,
|
||||
the theme also provides a [Homepage Hero Tint Plate Configuration Tool](../../tools/custom-theme.md)
|
||||
for visual configuration. You can generate configuration content and copy it directly for use in your own project.
|
||||
:::
|
||||
[See **Background Effects Configuration & Demo** to learn more.](./home-hero-effect.md){.read-more}
|
||||
|
||||
The theme also supports customizing the colors of `name`, `tagline`, and `text`.
|
||||
|
||||
|
||||
1303
docs/guide/custom/home-hero-effect.md
Normal file
@ -139,8 +139,6 @@ config:
|
||||
|
||||
适用于 文档 类型站点,放置于 首位。
|
||||
|
||||
**工具支持: [首页背景色板配置工具](../../tools/home-hero-tint-plate.md)**
|
||||
|
||||
```ts
|
||||
interface PlumeThemeHomeHero extends PlumeHomeConfigBase {
|
||||
type: 'hero'
|
||||
@ -159,95 +157,22 @@ interface PlumeThemeHomeHero extends PlumeHomeConfigBase {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 背景图片,"tint-plate" 为预设效果, 也可以配置为图片地址
|
||||
* 主题内置的背景效果,如果为非预设背景效果,则可以传入背景图片链接地址
|
||||
*/
|
||||
background?: 'tint-plate' | string
|
||||
|
||||
effect?: 'tint-plate' | 'prism' | 'pixel-blast' | 'hyper-speed' | 'liquid-ether'
|
||||
| 'dot-grid' | 'iridescence' | 'orb' | 'beams' | 'lightning' | string
|
||||
/**
|
||||
* 当 background 为预设背景时,可以配置 RGB 值,用于调整背景
|
||||
* 该配置仅在 `background` 为 `tint-plate` 时生效
|
||||
* 背景效果配置项,根据 `effect` 值不同,配置项不同
|
||||
*/
|
||||
tintPlate?: TintPlate
|
||||
effectConfig?: any
|
||||
/**
|
||||
* 如果是非预设背景,可以设置背景图片的滤镜效果
|
||||
*/
|
||||
filter?: string
|
||||
}
|
||||
interface TintPlateObj {
|
||||
// value 表示 基准色值,范围为 0 ~ 255
|
||||
// offset 表示 基准色值的偏移量,范围为 0 ~ (255 - value)
|
||||
r: { value: number, offset: number }
|
||||
g: { value: number, offset: number }
|
||||
b: { value: number, offset: number }
|
||||
}
|
||||
type TintPlate
|
||||
= | number // 210
|
||||
| string // '210,210,210' => red,green,blue
|
||||
// { r: { value: 220, offset: 36 }, g: { value: 220, offset: 36 }, b: { value: 220, offset: 36 } }
|
||||
| TintPlate
|
||||
// { light: 210, dark: 20 }
|
||||
// { light: '210,210,210', dark: '20,20,20' }
|
||||
| { light: number | string, dark: number | string }
|
||||
| { light: TintPlate, dark: TintPlate }
|
||||
```
|
||||
|
||||
**示例:**
|
||||
|
||||
```md
|
||||
---
|
||||
home: true
|
||||
config:
|
||||
-
|
||||
type: hero
|
||||
full: true
|
||||
background: tint-plate
|
||||
hero:
|
||||
name: Theme Plume
|
||||
tagline: Vuepress Next Theme
|
||||
text: 一个简约的,功能丰富的 vuepress 文档&博客 主题
|
||||
actions:
|
||||
-
|
||||
theme: brand
|
||||
text: 快速开始 →
|
||||
link: /
|
||||
-
|
||||
theme: alt
|
||||
text: Github
|
||||
link: https://github.com/pengzhanbo/vuepress-theme-plume
|
||||
---
|
||||
```
|
||||
|
||||
**效果:**
|
||||
|
||||
:::demo-wrapper img no-padding
|
||||
<img src="/images/custom-hero.jpg" alt="Theme Plume" />
|
||||
:::
|
||||
|
||||
当 `background` 配置为 `tint-plate` 时,还可以额外配置 `tintPlate` 调整 背景色调,范围为 `0 ~ 255`:
|
||||
|
||||
```md
|
||||
---
|
||||
home: true
|
||||
config:
|
||||
-
|
||||
type: hero
|
||||
full: true
|
||||
background: tint-plate
|
||||
tintPlate: 210
|
||||
---
|
||||
```
|
||||
|
||||
`tintPlate` 用于配置 RGB 值:
|
||||
|
||||
- 配置为单个值时,表示配置 red,green,blue 三个颜色值为相同值,范围: 0 - 255。示例: `210`。
|
||||
- 配置为三个值时,表示配置 red,green,blue 三个颜色值为不同值,范围: 0 - 255。示例: `210,210,210`。
|
||||
- 配置为 `TintPlate`,则可以更加灵活的控制每个颜色值和对应的偏移量。
|
||||
- 还可以配置为 `{ light, dark }`,在深色模式和浅色模式下使用不同的颜色值。
|
||||
|
||||
::: info
|
||||
为了便于用户配置 美观的个性化的背景,主题还提供了 [首页背景色板配置工具](../../tools/custom-theme.md)
|
||||
进行可视化操作,生成配置内容,你可以直接复制它们用于自己的项目中。
|
||||
:::
|
||||
[查看 **背景效果 配置 & 演示** 了解更多](./home-hero-effect.md){.read-more}
|
||||
|
||||
主题还支持自定义 `name`, `tagline`, `text` 的颜色。
|
||||
|
||||
|
||||
@ -51,6 +51,7 @@
|
||||
"@types/node": "catalog:dev",
|
||||
"@types/picomatch": "catalog:dev",
|
||||
"@types/stylus": "catalog:dev",
|
||||
"@types/three": "catalog:dev",
|
||||
"@types/webpack-env": "catalog:dev",
|
||||
"@vitest/coverage-istanbul": "catalog:dev",
|
||||
"bumpp": "catalog:dev",
|
||||
|
||||
98
pnpm-lock.yaml
generated
@ -51,6 +51,9 @@ catalogs:
|
||||
'@types/stylus':
|
||||
specifier: ^0.48.43
|
||||
version: 0.48.43
|
||||
'@types/three':
|
||||
specifier: ^0.180.0
|
||||
version: 0.180.0
|
||||
'@types/webpack-env':
|
||||
specifier: ^1.18.8
|
||||
version: 1.18.8
|
||||
@ -142,6 +145,9 @@ catalogs:
|
||||
dashjs:
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3
|
||||
gsap:
|
||||
specifier: ^3.13.0
|
||||
version: 3.13.0
|
||||
hls.js:
|
||||
specifier: ^1.6.13
|
||||
version: 1.6.13
|
||||
@ -151,6 +157,12 @@ catalogs:
|
||||
mpegts.js:
|
||||
specifier: 1.7.3
|
||||
version: 1.7.3
|
||||
ogl:
|
||||
specifier: ^1.0.11
|
||||
version: 1.0.11
|
||||
postprocessing:
|
||||
specifier: ^6.37.8
|
||||
version: 6.37.8
|
||||
pyodide:
|
||||
specifier: ^0.29.0
|
||||
version: 0.29.0
|
||||
@ -160,6 +172,9 @@ catalogs:
|
||||
swiper:
|
||||
specifier: ^12.0.2
|
||||
version: 12.0.2
|
||||
three:
|
||||
specifier: ^0.180.0
|
||||
version: 0.180.0
|
||||
prod:
|
||||
'@clack/prompts':
|
||||
specifier: ^0.11.0
|
||||
@ -418,6 +433,9 @@ importers:
|
||||
'@types/stylus':
|
||||
specifier: catalog:dev
|
||||
version: 0.48.43
|
||||
'@types/three':
|
||||
specifier: catalog:dev
|
||||
version: 0.180.0
|
||||
'@types/webpack-env':
|
||||
specifier: catalog:dev
|
||||
version: 1.18.8
|
||||
@ -898,9 +916,21 @@ importers:
|
||||
'@iconify/json':
|
||||
specifier: catalog:peer
|
||||
version: 2.2.398
|
||||
gsap:
|
||||
specifier: catalog:peer
|
||||
version: 3.13.0
|
||||
ogl:
|
||||
specifier: catalog:peer
|
||||
version: 1.0.11
|
||||
postprocessing:
|
||||
specifier: catalog:peer
|
||||
version: 6.37.8(three@0.180.0)
|
||||
swiper:
|
||||
specifier: catalog:peer
|
||||
version: 12.0.2
|
||||
three:
|
||||
specifier: catalog:peer
|
||||
version: 0.180.0
|
||||
vue-router:
|
||||
specifier: catalog:dev
|
||||
version: 4.6.3(vue@3.5.22(typescript@5.9.3))
|
||||
@ -1254,6 +1284,9 @@ packages:
|
||||
peerDependencies:
|
||||
postcss-selector-parser: ^7.0.0
|
||||
|
||||
'@dimforge/rapier3d-compat@0.12.0':
|
||||
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
|
||||
|
||||
'@docsearch/css@4.2.0':
|
||||
resolution: {integrity: sha512-65KU9Fw5fGsPPPlgIghonMcndyx1bszzrDQYLfierN+Ha29yotMHzVS94bPkZS6On9LS8dE4qmW4P/fGjtCf/g==}
|
||||
|
||||
@ -2480,6 +2513,9 @@ packages:
|
||||
'@svta/common-media-library@0.12.4':
|
||||
resolution: {integrity: sha512-9EuOoaNmz7JrfGwjsrD9SxF9otU5TNMnbLu1yU4BeLK0W5cDxVXXR58Z89q9u2AnHjIctscjMTYdlqQ1gojTuw==}
|
||||
|
||||
'@tweenjs/tween.js@23.1.3':
|
||||
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||
|
||||
@ -2699,9 +2735,15 @@ packages:
|
||||
'@types/serve-static@1.15.8':
|
||||
resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==}
|
||||
|
||||
'@types/stats.js@0.17.4':
|
||||
resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
|
||||
|
||||
'@types/stylus@0.48.43':
|
||||
resolution: {integrity: sha512-72dv/zdhuyXWVHUXG2VTPEQdOG+oen95/DNFx2aMFFaY6LoITI6PwEqf5x31JF49kp2w9hvUzkNfTGBIeg61LQ==}
|
||||
|
||||
'@types/three@0.180.0':
|
||||
resolution: {integrity: sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==}
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
@ -2714,6 +2756,9 @@ packages:
|
||||
'@types/webpack-env@1.18.8':
|
||||
resolution: {integrity: sha512-G9eAoJRMLjcvN4I08wB5I7YofOb/kaJNd5uoCMX+LbKXTPCF+ZIHuqTnFaK9Jz1rgs035f9JUPUhNFtqgucy/A==}
|
||||
|
||||
'@types/webxr@0.5.24':
|
||||
resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.45.0':
|
||||
resolution: {integrity: sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@ -3178,6 +3223,9 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
'@webgpu/types@0.1.66':
|
||||
resolution: {integrity: sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==}
|
||||
|
||||
'@xmldom/xmldom@0.9.8':
|
||||
resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==}
|
||||
engines: {node: '>=14.6'}
|
||||
@ -4840,6 +4888,9 @@ packages:
|
||||
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
||||
gsap@3.13.0:
|
||||
resolution: {integrity: sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==}
|
||||
|
||||
hachure-fill@0.5.2:
|
||||
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
|
||||
|
||||
@ -5632,6 +5683,7 @@ packages:
|
||||
|
||||
mathjax-full@3.2.2:
|
||||
resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==}
|
||||
deprecated: Version 4 replaces this package with the scoped package @mathjax/src
|
||||
|
||||
mathml-tag-names@2.1.3:
|
||||
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
|
||||
@ -5705,6 +5757,9 @@ packages:
|
||||
mermaid@11.12.0:
|
||||
resolution: {integrity: sha512-ZudVx73BwrMJfCFmSSJT84y6u5brEoV8DOItdHomNLz32uBjNrelm7mg95X7g+C6UoQH/W6mBLGDEDv73JdxBg==}
|
||||
|
||||
meshoptimizer@0.22.0:
|
||||
resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==}
|
||||
|
||||
mhchemparser@4.2.1:
|
||||
resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==}
|
||||
|
||||
@ -5960,6 +6015,9 @@ packages:
|
||||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
ogl@1.0.11:
|
||||
resolution: {integrity: sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==}
|
||||
|
||||
ohash@2.0.11:
|
||||
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
|
||||
|
||||
@ -6246,6 +6304,11 @@ packages:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
postprocessing@6.37.8:
|
||||
resolution: {integrity: sha512-qTFUKS51z/fuw2U+irz4/TiKJ/0oI70cNtvQG1WxlPKvBdJUfS1CcFswJd5ATY3slotWfvkDDZAsj1X0fU8BOQ==}
|
||||
peerDependencies:
|
||||
three: '>= 0.157.0 < 0.181.0'
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@ -7013,6 +7076,9 @@ packages:
|
||||
peerDependencies:
|
||||
tslib: ^2
|
||||
|
||||
three@0.180.0:
|
||||
resolution: {integrity: sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==}
|
||||
|
||||
throttleit@2.1.0:
|
||||
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
||||
engines: {node: '>=18'}
|
||||
@ -8118,6 +8184,8 @@ snapshots:
|
||||
dependencies:
|
||||
postcss-selector-parser: 7.1.0
|
||||
|
||||
'@dimforge/rapier3d-compat@0.12.0': {}
|
||||
|
||||
'@docsearch/css@4.2.0': {}
|
||||
|
||||
'@docsearch/js@4.2.0': {}
|
||||
@ -9154,6 +9222,8 @@ snapshots:
|
||||
|
||||
'@svta/common-media-library@0.12.4': {}
|
||||
|
||||
'@tweenjs/tween.js@23.1.3': {}
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@ -9407,10 +9477,22 @@ snapshots:
|
||||
'@types/node': 24.8.1
|
||||
'@types/send': 0.17.5
|
||||
|
||||
'@types/stats.js@0.17.4': {}
|
||||
|
||||
'@types/stylus@0.48.43':
|
||||
dependencies:
|
||||
'@types/node': 24.9.1
|
||||
|
||||
'@types/three@0.180.0':
|
||||
dependencies:
|
||||
'@dimforge/rapier3d-compat': 0.12.0
|
||||
'@tweenjs/tween.js': 23.1.3
|
||||
'@types/stats.js': 0.17.4
|
||||
'@types/webxr': 0.5.24
|
||||
'@webgpu/types': 0.1.66
|
||||
fflate: 0.8.2
|
||||
meshoptimizer: 0.22.0
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
@ -9419,6 +9501,8 @@ snapshots:
|
||||
|
||||
'@types/webpack-env@1.18.8': {}
|
||||
|
||||
'@types/webxr@0.5.24': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.38.0(jiti@2.5.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.5.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.1
|
||||
@ -10148,6 +10232,8 @@ snapshots:
|
||||
dependencies:
|
||||
vue: 3.5.22(typescript@5.9.3)
|
||||
|
||||
'@webgpu/types@0.1.66': {}
|
||||
|
||||
'@xmldom/xmldom@0.9.8': {}
|
||||
|
||||
JSONStream@1.3.5:
|
||||
@ -12078,6 +12164,8 @@ snapshots:
|
||||
section-matter: 1.0.0
|
||||
strip-bom-string: 1.0.0
|
||||
|
||||
gsap@3.13.0: {}
|
||||
|
||||
hachure-fill@0.5.2: {}
|
||||
|
||||
handlebars@4.7.8:
|
||||
@ -13089,6 +13177,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
meshoptimizer@0.22.0: {}
|
||||
|
||||
mhchemparser@4.2.1: {}
|
||||
|
||||
micromark-core-commonmark@2.0.3:
|
||||
@ -13436,6 +13526,8 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
ogl@1.0.11: {}
|
||||
|
||||
ohash@2.0.11: {}
|
||||
|
||||
once@1.4.0:
|
||||
@ -13709,6 +13801,10 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
postprocessing@6.37.8(three@0.180.0):
|
||||
dependencies:
|
||||
three: 0.180.0
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prismjs@1.30.0: {}
|
||||
@ -14608,6 +14704,8 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
three@0.180.0: {}
|
||||
|
||||
throttleit@2.1.0: {}
|
||||
|
||||
through@2.3.8: {}
|
||||
|
||||
@ -13,7 +13,6 @@ overrides:
|
||||
|
||||
patchedDependencies:
|
||||
floating-vue: patches/floating-vue.patch
|
||||
|
||||
catalogs:
|
||||
dev:
|
||||
'@commitlint/cli': ^20.1.0
|
||||
@ -31,6 +30,7 @@ catalogs:
|
||||
'@types/node': ^24.9.1
|
||||
'@types/picomatch': ^4.0.2
|
||||
'@types/stylus': ^0.48.43
|
||||
'@types/three': ^0.180.0
|
||||
'@types/webpack-env': ^1.18.8
|
||||
'@vitest/coverage-istanbul': ^3.2.4
|
||||
bumpp: ^10.3.1
|
||||
@ -62,13 +62,17 @@ catalogs:
|
||||
'@iconify/json': ^2.2.398
|
||||
artplayer: ^5.3.0
|
||||
dashjs: ^5.0.3
|
||||
gsap: ^3.13.0
|
||||
hls.js: ^1.6.13
|
||||
mathjax-full: ^3.2.2
|
||||
mpegts.js: 1.7.3
|
||||
ogl: ^1.0.11
|
||||
postprocessing: ^6.37.8
|
||||
pyodide: ^0.29.0
|
||||
sass: ^1.93.2
|
||||
sass-embedded: ^1.93.2
|
||||
swiper: ^12.0.2
|
||||
three: ^0.180.0
|
||||
prod:
|
||||
'@clack/prompts': ^0.11.0
|
||||
'@iconify/utils': ^3.0.2
|
||||
|
||||
@ -67,8 +67,12 @@
|
||||
"peerDependencies": {
|
||||
"@iconify/json": "catalog:peer",
|
||||
"@vuepress/shiki-twoslash": "catalog:vuepress",
|
||||
"gsap": "catalog:peer",
|
||||
"mathjax-full": "catalog:peer",
|
||||
"ogl": "catalog:peer",
|
||||
"postprocessing": "catalog:peer",
|
||||
"swiper": "catalog:peer",
|
||||
"three": "catalog:peer",
|
||||
"vuepress": "catalog:vuepress"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@ -78,11 +82,23 @@
|
||||
"@vuepress/shiki-twoslash": {
|
||||
"optional": true
|
||||
},
|
||||
"gsap": {
|
||||
"optional": true
|
||||
},
|
||||
"mathjax-full": {
|
||||
"optional": true
|
||||
},
|
||||
"ogl": {
|
||||
"optional": true
|
||||
},
|
||||
"postprocessing": {
|
||||
"optional": true
|
||||
},
|
||||
"swiper": {
|
||||
"optional": true
|
||||
},
|
||||
"three": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@ -128,7 +144,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "catalog:peer",
|
||||
"gsap": "catalog:peer",
|
||||
"ogl": "catalog:peer",
|
||||
"postprocessing": "catalog:peer",
|
||||
"swiper": "catalog:peer",
|
||||
"three": "catalog:peer",
|
||||
"vue-router": "catalog:dev"
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,6 +104,7 @@ onUnmounted(() => {
|
||||
<component
|
||||
:is="resolveComponentName(item.type)"
|
||||
v-bind="item"
|
||||
:index="index"
|
||||
:only-once="onlyOnce"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -17,6 +17,9 @@ const styles = computed(() => {
|
||||
|
||||
const image = typeof props.backgroundImage === 'string' ? props.backgroundImage : (props.backgroundImage[isDark.value ? 'dark' : 'light'] ?? props.backgroundImage.light)
|
||||
|
||||
if (!image)
|
||||
return null
|
||||
|
||||
const link = isLinkHttp(image) ? props.backgroundImage : withBase(image)
|
||||
return {
|
||||
'background-image': `url(${link})`,
|
||||
|
||||
@ -12,8 +12,8 @@ const actions = computed(() => hero.value.actions ?? [])
|
||||
|
||||
<template>
|
||||
<div class="vp-home-doc-hero" :class="{ 'has-image': hero.image }">
|
||||
<div class="container">
|
||||
<div class="main">
|
||||
<div class="doc-hero-container">
|
||||
<div class="doc-hero-main">
|
||||
<h1 class="heading">
|
||||
<span v-if="hero.name" class="name clip" v-html="hero.name" />
|
||||
<span v-if="hero.text" class="text" v-html="hero.text" />
|
||||
@ -69,7 +69,7 @@ const actions = computed(() => hero.value.actions ?? [])
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
.doc-hero-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1152px;
|
||||
@ -77,12 +77,12 @@ const actions = computed(() => hero.value.actions ?? [])
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.container {
|
||||
.doc-hero-container {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
.doc-hero-main {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
flex-grow: 1;
|
||||
@ -90,23 +90,23 @@ const actions = computed(() => hero.value.actions ?? [])
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.vp-home-doc-hero.has-image .container {
|
||||
.vp-home-doc-hero.has-image .doc-hero-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.vp-home-doc-hero.has-image .container {
|
||||
.vp-home-doc-hero.has-image .doc-hero-container {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.main {
|
||||
.doc-hero-main {
|
||||
order: 1;
|
||||
width: calc((100% / 3) * 2);
|
||||
}
|
||||
|
||||
.vp-home-doc-hero.has-image .main {
|
||||
.vp-home-doc-hero.has-image .doc-hero-main {
|
||||
max-width: 592px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ const grid = computed(() => {
|
||||
:background-image="backgroundImage"
|
||||
:background-attachment="backgroundAttachment"
|
||||
:full="full"
|
||||
:index="index"
|
||||
>
|
||||
<h2 v-if="title" class="title" v-html="title" />
|
||||
<p v-if="description" class="description" v-html="description" />
|
||||
|
||||
@ -1,57 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import type { ThemeHomeHero } from '../../../shared/index.js'
|
||||
import { effectComponents, effects } from '@internal/home-hero-effects'
|
||||
import ImageBg from '@theme/background/ImageBg.vue'
|
||||
import VPButton from '@theme/VPButton.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { withBase } from 'vuepress/client'
|
||||
import { isLinkHttp } from 'vuepress/shared'
|
||||
import { useData, useHomeHeroTintPlate } from '../../composables/index.js'
|
||||
import { computed, nextTick, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { isPlainObject } from 'vuepress/shared'
|
||||
import { useData } from '../../composables/index.js'
|
||||
import { inBrowser } from '../../utils/index.js'
|
||||
|
||||
const props = defineProps<ThemeHomeHero>()
|
||||
|
||||
const { isDark, frontmatter: matter } = useData<'home'>()
|
||||
const { frontmatter, isDark } = useData<'home'>()
|
||||
const hero = computed(() => props.hero ?? frontmatter.value.hero ?? {})
|
||||
const actions = computed(() => hero.value.actions ?? [])
|
||||
|
||||
const heroBackground = computed(() => {
|
||||
if (props.background === 'tint-plate')
|
||||
const effect = computed(() => {
|
||||
const effect = props.effect || props.background
|
||||
if (!effect || !effects.includes(effect))
|
||||
return null
|
||||
const image = props.backgroundImage
|
||||
? typeof props.backgroundImage === 'string'
|
||||
? props.backgroundImage
|
||||
: (props.backgroundImage[isDark.value ? 'dark' : 'light'] ?? props.backgroundImage.light)
|
||||
: ''
|
||||
const background = image || props.background
|
||||
return effect as typeof effects[number]
|
||||
})
|
||||
|
||||
if (!background)
|
||||
const effectConfig = computed(() => {
|
||||
// compatibility
|
||||
if (effect.value === 'tint-plate') {
|
||||
const plate = props.tintPlate ?? props.effectConfig
|
||||
if (typeof plate === 'number' || typeof plate === 'string') {
|
||||
return { rgb: plate }
|
||||
}
|
||||
return plate
|
||||
}
|
||||
// guide compatible
|
||||
if (!isPlainObject(props.effectConfig))
|
||||
return null
|
||||
|
||||
const link = isLinkHttp(background) ? background : withBase(background)
|
||||
return {
|
||||
'background-image': `url(${link})`,
|
||||
'background-attachment': props.backgroundAttachment || '',
|
||||
'--vp-hero-bg-filter': props.filter,
|
||||
return props.effectConfig
|
||||
})
|
||||
|
||||
function noTransition() {
|
||||
document.documentElement.classList.add('no-transition')
|
||||
setTimeout(() => {
|
||||
document.documentElement.classList.remove('no-transition')
|
||||
}, 300)
|
||||
}
|
||||
|
||||
let defaultTheme: string | undefined
|
||||
watch(() => props.forceDark, () => {
|
||||
if (inBrowser && props.forceDark) {
|
||||
defaultTheme ??= document.documentElement.dataset.theme
|
||||
document.documentElement.dataset.theme = 'dark'
|
||||
document.documentElement.classList.add('force-dark')
|
||||
nextTick(() => isDark.value = true)
|
||||
noTransition()
|
||||
}
|
||||
document.documentElement.classList.add(`effect-${effect.value}`)
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
if (props.forceDark) {
|
||||
window.addEventListener('unload', () => {
|
||||
isDark.value = defaultTheme === 'dark'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const hero = computed(() => props.hero ?? matter.value.hero ?? {})
|
||||
const actions = computed(() => hero.value.actions ?? [])
|
||||
|
||||
const canvas = ref<HTMLCanvasElement>()
|
||||
useHomeHeroTintPlate(
|
||||
canvas,
|
||||
computed(() => props.background === 'tint-plate'),
|
||||
computed(() => props.tintPlate),
|
||||
)
|
||||
onUnmounted(() => {
|
||||
if (props.forceDark) {
|
||||
isDark.value = defaultTheme === 'dark'
|
||||
document.documentElement.classList.remove('force-dark', `effect-${effect.value}`)
|
||||
noTransition()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-home-hero" :class="{ full: props.full, once: props.onlyOnce }">
|
||||
<div v-if="heroBackground" class="home-hero-bg" :style="heroBackground" />
|
||||
<div
|
||||
class="vp-home-hero"
|
||||
:class="{
|
||||
full,
|
||||
once: onlyOnce,
|
||||
first: props.index === 0,
|
||||
[effect ?? '']: !!effect,
|
||||
}"
|
||||
>
|
||||
<component :is="effectComponents[effect]" v-if="effect" v-bind="effectConfig" />
|
||||
<ImageBg v-else v-bind="props" />
|
||||
|
||||
<div v-if="background === 'tint-plate'" class="bg-filter">
|
||||
<canvas ref="canvas" width="32" height="32" />
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<div class="hero-container">
|
||||
<div class="hero-content">
|
||||
<h1 v-if="hero.name" class="hero-name" v-html="hero.name" />
|
||||
<p v-if="hero.tagline" class="hero-tagline" v-html="hero.tagline" />
|
||||
<p v-if="hero.text" class="hero-text" v-html="hero.text" />
|
||||
@ -84,61 +120,60 @@ useHomeHeroTintPlate(
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vp-home-hero.first {
|
||||
margin-top: calc(0px - var(--vp-nav-height));
|
||||
}
|
||||
|
||||
.vp-home-hero.full {
|
||||
height: calc(100vh - var(--vp-nav-height));
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.vp-home-hero.full.once {
|
||||
height: calc(100vh - var(--vp-nav-height) - var(--vp-footer-height, 0px));
|
||||
height: calc(100vh - var(--vp-footer-height, 0px));
|
||||
}
|
||||
|
||||
.home-hero-bg {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: var(--vp-hero-bg-filter);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.container {
|
||||
.hero-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vp-home-hero.full .container {
|
||||
.vp-home-hero.full .hero-container {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.vp-home-hero:not(.full) .container {
|
||||
.vp-home-hero:not(.full) .hero-container {
|
||||
padding-top: 80px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.content {
|
||||
.hero-content {
|
||||
width: max-content;
|
||||
max-width: 960px;
|
||||
padding: 0 20px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vp-home-hero.full .container .content {
|
||||
.vp-home-hero.full .hero-container .hero-content {
|
||||
margin-top: -40px;
|
||||
}
|
||||
|
||||
.hero-name,
|
||||
.hero-tagline {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
font-size: 48px;
|
||||
font-weight: 900;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.5px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.hero-name {
|
||||
@ -160,6 +195,7 @@ useHomeHeroTintPlate(
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-home-hero-text, var(--vp-c-text-3));
|
||||
white-space: pre-wrap;
|
||||
pointer-events: auto;
|
||||
transition: color var(--vp-t-color);
|
||||
}
|
||||
|
||||
@ -180,54 +216,13 @@ useHomeHeroTintPlate(
|
||||
|
||||
.action :deep(.vp-button) {
|
||||
margin-left: 0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.action :deep(.vp-button:last-of-type) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* =========== background filter begin ======= */
|
||||
.bg-filter {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.vp-home-hero.full.once .bg-filter {
|
||||
height: calc(100% + var(--vp-footer-height, 0px));
|
||||
}
|
||||
|
||||
@property --vp-home-hero-bg-filter {
|
||||
inherits: false;
|
||||
initial-value: #fff;
|
||||
syntax: "<color>";
|
||||
}
|
||||
|
||||
.bg-filter::after {
|
||||
--vp-home-hero-bg-filter: var(--vp-c-bg);
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: "";
|
||||
background: linear-gradient(to bottom, var(--vp-home-hero-bg-filter) 0, transparent 45%, transparent 55%, var(--vp-home-hero-bg-filter) 140%);
|
||||
transition: --vp-home-hero-bg-filter var(--vp-t-color);
|
||||
}
|
||||
|
||||
.bg-filter canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* =========== background filter end ======= */
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.hero-name,
|
||||
.hero-tagline {
|
||||
@ -250,3 +245,32 @@ useHomeHeroTintPlate(
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
html.no-transition *,
|
||||
html.no-transition *::before,
|
||||
html.no-transition *::after {
|
||||
background-attachment: initial !important;
|
||||
transition-delay: 0s !important;
|
||||
transition-duration: 0s !important;
|
||||
animation-duration: 1ms !important;
|
||||
animation-delay: -1ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
}
|
||||
|
||||
html[class*="effect-"].force-dark .vp-navbar-appearance {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[class*="effect-"].force-dark * {
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale !important;
|
||||
text-rendering: optimizelegibility !important;
|
||||
}
|
||||
|
||||
html[class*="effect-"].force-dark .vp-navbar,
|
||||
html[class*="effect-"].force-dark .vp-navbar:not(.top) {
|
||||
background: rgb(15 15 15 / 0.7) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -28,6 +28,7 @@ const profile = computed(() => {
|
||||
:background-image="backgroundImage"
|
||||
:background-attachment="backgroundAttachment"
|
||||
:full="full"
|
||||
:index="index"
|
||||
>
|
||||
<VPImage v-if="profile.avatar" :image="profile.avatar" :class="{ circle: profile.circle }" />
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ const maxWidth = computed(() => {
|
||||
:background-attachment="backgroundAttachment"
|
||||
:full="full"
|
||||
:container-class="{ reverse: type === 'text-image' }"
|
||||
:index="index"
|
||||
>
|
||||
<div class="content-image">
|
||||
<VPImage :image="image" :style="{ maxWidth }" />
|
||||
|
||||
465
theme/src/client/components/background/Beams.vue
Normal file
@ -0,0 +1,465 @@
|
||||
<!--
|
||||
***************************************************
|
||||
fork from https://github.com/DavidHDev/vue-bits MIT License
|
||||
modified by pengzhanbo
|
||||
***************************************************
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type { ThemeHomeHeroBeams } from '../../../shared/index.js'
|
||||
import * as THREE from 'three'
|
||||
import { degToRad } from 'three/src/math/MathUtils.js'
|
||||
import { computed, onMounted, onUnmounted, useTemplateRef, watch } from 'vue'
|
||||
import { isPlainObject } from 'vuepress/shared'
|
||||
import { useDarkMode } from '../../composables/index.js'
|
||||
|
||||
const props = withDefaults(defineProps<ThemeHomeHeroBeams>(), {
|
||||
beamWidth: 2,
|
||||
beamHeight: 15,
|
||||
beamNumber: 12,
|
||||
lightColor: '#fff',
|
||||
speed: 2,
|
||||
noiseIntensity: 1.75,
|
||||
scale: 0.2,
|
||||
rotation: 0,
|
||||
})
|
||||
|
||||
const isDark = useDarkMode()
|
||||
|
||||
const DEFAULT_LIGHT_COLOR = '#fff'
|
||||
const lightColor = computed(() => {
|
||||
if (typeof props.lightColor === 'string')
|
||||
return props.lightColor || DEFAULT_LIGHT_COLOR
|
||||
if (isPlainObject(props.lightColor))
|
||||
return props.lightColor[isDark.value ? 'dark' : 'light'] || props.lightColor.light || DEFAULT_LIGHT_COLOR
|
||||
|
||||
return DEFAULT_LIGHT_COLOR
|
||||
})
|
||||
|
||||
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
|
||||
|
||||
let renderer: THREE.WebGLRenderer | null = null
|
||||
let scene: THREE.Scene | null = null
|
||||
let camera: THREE.PerspectiveCamera | null = null
|
||||
let beamMesh: THREE.Mesh<THREE.BufferGeometry, THREE.ShaderMaterial> | null = null
|
||||
let directionalLight: THREE.DirectionalLight | null = null
|
||||
let ambientLight: THREE.AmbientLight | null = null
|
||||
let animationId: number | null = null
|
||||
|
||||
type UniformValue = THREE.IUniform<unknown> | unknown
|
||||
|
||||
interface ExtendMaterialConfig {
|
||||
header: string
|
||||
vertexHeader?: string
|
||||
fragmentHeader?: string
|
||||
material?: THREE.MeshPhysicalMaterialParameters & { fog?: boolean }
|
||||
uniforms?: Record<string, UniformValue>
|
||||
vertex?: Record<string, string>
|
||||
fragment?: Record<string, string>
|
||||
}
|
||||
|
||||
type ShaderWithDefines = THREE.ShaderLibShader & {
|
||||
defines?: Record<string, string | number | boolean>
|
||||
}
|
||||
|
||||
function hexToNormalizedRGB(hex: string): [number, number, number] {
|
||||
const clean = hex.replace('#', '')
|
||||
const r = Number.parseInt(clean.substring(0, 2), 16)
|
||||
const g = Number.parseInt(clean.substring(2, 4), 16)
|
||||
const b = Number.parseInt(clean.substring(4, 6), 16)
|
||||
return [r / 255, g / 255, b / 255]
|
||||
}
|
||||
|
||||
const noise = `
|
||||
float random (in vec2 st) {
|
||||
return fract(sin(dot(st.xy, vec2(12.9898,78.233)))* 43758.5453123);
|
||||
}
|
||||
float noise (in vec2 st) {
|
||||
vec2 i = floor(st);
|
||||
vec2 f = fract(st);
|
||||
float a = random(i);
|
||||
float b = random(i + vec2(1.0, 0.0));
|
||||
float c = random(i + vec2(0.0, 1.0));
|
||||
float d = random(i + vec2(1.0, 1.0));
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(a, b, u.x) +
|
||||
(c - a)* u.y * (1.0 - u.x) +
|
||||
(d - b) * u.x * u.y;
|
||||
}
|
||||
vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
|
||||
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
|
||||
vec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}
|
||||
float cnoise(vec3 P){
|
||||
vec3 Pi0 = floor(P);
|
||||
vec3 Pi1 = Pi0 + vec3(1.0);
|
||||
Pi0 = mod(Pi0, 289.0);
|
||||
Pi1 = mod(Pi1, 289.0);
|
||||
vec3 Pf0 = fract(P);
|
||||
vec3 Pf1 = Pf0 - vec3(1.0);
|
||||
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
|
||||
vec4 iy = vec4(Pi0.yy, Pi1.yy);
|
||||
vec4 iz0 = Pi0.zzzz;
|
||||
vec4 iz1 = Pi1.zzzz;
|
||||
vec4 ixy = permute(permute(ix) + iy);
|
||||
vec4 ixy0 = permute(ixy + iz0);
|
||||
vec4 ixy1 = permute(ixy + iz1);
|
||||
vec4 gx0 = ixy0 / 7.0;
|
||||
vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;
|
||||
gx0 = fract(gx0);
|
||||
vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
|
||||
vec4 sz0 = step(gz0, vec4(0.0));
|
||||
gx0 -= sz0 * (step(0.0, gx0) - 0.5);
|
||||
gy0 -= sz0 * (step(0.0, gy0) - 0.5);
|
||||
vec4 gx1 = ixy1 / 7.0;
|
||||
vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;
|
||||
gx1 = fract(gx1);
|
||||
vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
|
||||
vec4 sz1 = step(gz1, vec4(0.0));
|
||||
gx1 -= sz1 * (step(0.0, gx1) - 0.5);
|
||||
gy1 -= sz1 * (step(0.0, gy1) - 0.5);
|
||||
vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
|
||||
vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
|
||||
vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
|
||||
vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
|
||||
vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
|
||||
vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
|
||||
vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
|
||||
vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);
|
||||
vec4 norm0 = taylorInvSqrt(vec4(dot(g000,g000),dot(g010,g010),dot(g100,g100),dot(g110,g110)));
|
||||
g000 *= norm0.x; g010 *= norm0.y; g100 *= norm0.z; g110 *= norm0.w;
|
||||
vec4 norm1 = taylorInvSqrt(vec4(dot(g001,g001),dot(g011,g011),dot(g101,g101),dot(g111,g111)));
|
||||
g001 *= norm1.x; g011 *= norm1.y; g101 *= norm1.z; g111 *= norm1.w;
|
||||
float n000 = dot(g000, Pf0);
|
||||
float n100 = dot(g100, vec3(Pf1.x,Pf0.yz));
|
||||
float n010 = dot(g010, vec3(Pf0.x,Pf1.y,Pf0.z));
|
||||
float n110 = dot(g110, vec3(Pf1.xy,Pf0.z));
|
||||
float n001 = dot(g001, vec3(Pf0.xy,Pf1.z));
|
||||
float n101 = dot(g101, vec3(Pf1.x,Pf0.y,Pf1.z));
|
||||
float n011 = dot(g011, vec3(Pf0.x,Pf1.yz));
|
||||
float n111 = dot(g111, Pf1);
|
||||
vec3 fade_xyz = fade(Pf0);
|
||||
vec4 n_z = mix(vec4(n000,n100,n010,n110),vec4(n001,n101,n011,n111),fade_xyz.z);
|
||||
vec2 n_yz = mix(n_z.xy,n_z.zw,fade_xyz.y);
|
||||
float n_xyz = mix(n_yz.x,n_yz.y,fade_xyz.x);
|
||||
return 2.2 * n_xyz;
|
||||
}
|
||||
`
|
||||
|
||||
function extendMaterial<T extends THREE.Material = THREE.Material>(
|
||||
BaseMaterial: new (params?: THREE.MaterialParameters) => T,
|
||||
cfg: ExtendMaterialConfig,
|
||||
): THREE.ShaderMaterial {
|
||||
const physical = THREE.ShaderLib.physical as ShaderWithDefines
|
||||
const { vertexShader: baseVert, fragmentShader: baseFrag, uniforms: baseUniforms } = physical
|
||||
const baseDefines = physical.defines ?? {}
|
||||
|
||||
const uniforms: Record<string, THREE.IUniform> = THREE.UniformsUtils.clone(baseUniforms)
|
||||
|
||||
const defaults = new BaseMaterial(cfg.material || {}) as T & {
|
||||
color?: THREE.Color
|
||||
roughness?: number
|
||||
metalness?: number
|
||||
envMap?: THREE.Texture
|
||||
envMapIntensity?: number
|
||||
}
|
||||
|
||||
if (defaults.color)
|
||||
uniforms.diffuse.value = defaults.color
|
||||
if ('roughness' in defaults)
|
||||
uniforms.roughness.value = defaults.roughness
|
||||
if ('metalness' in defaults)
|
||||
uniforms.metalness.value = defaults.metalness
|
||||
if ('envMap' in defaults)
|
||||
uniforms.envMap.value = defaults.envMap
|
||||
if ('envMapIntensity' in defaults)
|
||||
uniforms.envMapIntensity.value = defaults.envMapIntensity
|
||||
|
||||
Object.entries(cfg.uniforms ?? {}).forEach(([key, u]) => {
|
||||
uniforms[key]
|
||||
= u !== null && typeof u === 'object' && 'value' in u
|
||||
? (u as THREE.IUniform<unknown>)
|
||||
: ({ value: u } as THREE.IUniform<unknown>)
|
||||
})
|
||||
|
||||
let vert = `${cfg.header}\n${cfg.vertexHeader ?? ''}\n${baseVert}`
|
||||
let frag = `${cfg.header}\n${cfg.fragmentHeader ?? ''}\n${baseFrag}`
|
||||
|
||||
for (const [inc, code] of Object.entries(cfg.vertex ?? {})) {
|
||||
vert = vert.replace(inc, `${inc}\n${code}`)
|
||||
}
|
||||
for (const [inc, code] of Object.entries(cfg.fragment ?? {})) {
|
||||
frag = frag.replace(inc, `${inc}\n${code}`)
|
||||
}
|
||||
|
||||
const mat = new THREE.ShaderMaterial({
|
||||
defines: { ...baseDefines },
|
||||
uniforms,
|
||||
vertexShader: vert,
|
||||
fragmentShader: frag,
|
||||
lights: true,
|
||||
fog: !!cfg.material?.fog,
|
||||
})
|
||||
|
||||
return mat
|
||||
}
|
||||
|
||||
function createStackedPlanesBufferGeometry(
|
||||
n: number,
|
||||
width: number,
|
||||
height: number,
|
||||
spacing: number,
|
||||
heightSegments: number,
|
||||
): THREE.BufferGeometry {
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
const numVertices = n * (heightSegments + 1) * 2
|
||||
const numFaces = n * heightSegments * 2
|
||||
const positions = new Float32Array(numVertices * 3)
|
||||
const indices = new Uint32Array(numFaces * 3)
|
||||
const uvs = new Float32Array(numVertices * 2)
|
||||
|
||||
let vertexOffset = 0
|
||||
let indexOffset = 0
|
||||
let uvOffset = 0
|
||||
const totalWidth = n * width + (n - 1) * spacing
|
||||
const xOffsetBase = -totalWidth / 2
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const xOffset = xOffsetBase + i * (width + spacing)
|
||||
const uvXOffset = Math.random() * 300
|
||||
const uvYOffset = Math.random() * 300
|
||||
|
||||
for (let j = 0; j <= heightSegments; j++) {
|
||||
const y = height * (j / heightSegments - 0.5)
|
||||
const v0 = [xOffset, y, 0]
|
||||
const v1 = [xOffset + width, y, 0]
|
||||
positions.set([...v0, ...v1], vertexOffset * 3)
|
||||
|
||||
const uvY = j / heightSegments
|
||||
uvs.set([uvXOffset, uvY + uvYOffset, uvXOffset + 1, uvY + uvYOffset], uvOffset)
|
||||
|
||||
if (j < heightSegments) {
|
||||
const a = vertexOffset
|
||||
const b = vertexOffset + 1
|
||||
const c = vertexOffset + 2
|
||||
const d = vertexOffset + 3
|
||||
indices.set([a, b, c, c, b, d], indexOffset)
|
||||
indexOffset += 6
|
||||
}
|
||||
vertexOffset += 2
|
||||
uvOffset += 4
|
||||
}
|
||||
}
|
||||
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
|
||||
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2))
|
||||
geometry.setIndex(new THREE.BufferAttribute(indices, 1))
|
||||
geometry.computeVertexNormals()
|
||||
return geometry
|
||||
}
|
||||
|
||||
const beamMaterial = computed(() =>
|
||||
extendMaterial(THREE.MeshStandardMaterial, {
|
||||
header: `
|
||||
varying vec3 vEye;
|
||||
varying float vNoise;
|
||||
varying vec2 vUv;
|
||||
varying vec3 vPosition;
|
||||
uniform float time;
|
||||
uniform float uSpeed;
|
||||
uniform float uNoiseIntensity;
|
||||
uniform float uScale;
|
||||
${noise}`,
|
||||
vertexHeader: `
|
||||
float getPos(vec3 pos) {
|
||||
vec3 noisePos =
|
||||
vec3(pos.x * 0., pos.y - uv.y, pos.z + time * uSpeed * 3.) * uScale;
|
||||
return cnoise(noisePos);
|
||||
}
|
||||
vec3 getCurrentPos(vec3 pos) {
|
||||
vec3 newpos = pos;
|
||||
newpos.z += getPos(pos);
|
||||
return newpos;
|
||||
}
|
||||
vec3 getNormal(vec3 pos) {
|
||||
vec3 curpos = getCurrentPos(pos);
|
||||
vec3 nextposX = getCurrentPos(pos + vec3(0.01, 0.0, 0.0));
|
||||
vec3 nextposZ = getCurrentPos(pos + vec3(0.0, -0.01, 0.0));
|
||||
vec3 tangentX = normalize(nextposX - curpos);
|
||||
vec3 tangentZ = normalize(nextposZ - curpos);
|
||||
return normalize(cross(tangentZ, tangentX));
|
||||
}`,
|
||||
fragmentHeader: '',
|
||||
vertex: {
|
||||
'#include <begin_vertex>': `transformed.z += getPos(transformed.xyz);`,
|
||||
'#include <beginnormal_vertex>': `objectNormal = getNormal(position.xyz);`,
|
||||
},
|
||||
fragment: {
|
||||
'#include <dithering_fragment>': `
|
||||
float randomNoise = noise(gl_FragCoord.xy);
|
||||
gl_FragColor.rgb -= randomNoise / 15. * uNoiseIntensity;`,
|
||||
},
|
||||
material: { fog: true },
|
||||
uniforms: {
|
||||
diffuse: new THREE.Color(...hexToNormalizedRGB('#000000')),
|
||||
time: { shared: true, mixed: true, linked: true, value: 0 },
|
||||
roughness: 0.3,
|
||||
metalness: 0.3,
|
||||
uSpeed: { shared: true, mixed: true, linked: true, value: props.speed },
|
||||
envMapIntensity: 10,
|
||||
uNoiseIntensity: props.noiseIntensity,
|
||||
uScale: props.scale,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
function initThreeJS() {
|
||||
if (!containerRef.value)
|
||||
return
|
||||
|
||||
cleanup()
|
||||
|
||||
const container = containerRef.value
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
||||
renderer.setClearColor(0x000000, 1)
|
||||
|
||||
scene = new THREE.Scene()
|
||||
|
||||
camera = new THREE.PerspectiveCamera(30, 1, 0.1, 1000)
|
||||
camera.position.set(0, 0, 20)
|
||||
|
||||
const geometry = createStackedPlanesBufferGeometry(props.beamNumber, props.beamWidth, props.beamHeight, 0, 100)
|
||||
|
||||
const material = beamMaterial.value
|
||||
beamMesh = new THREE.Mesh(geometry, material)
|
||||
|
||||
const group = new THREE.Group()
|
||||
group.rotation.z = degToRad(props.rotation)
|
||||
group.add(beamMesh)
|
||||
scene.add(group)
|
||||
|
||||
directionalLight = new THREE.DirectionalLight(new THREE.Color(lightColor.value), 1)
|
||||
directionalLight.position.set(0, 3, 10)
|
||||
const shadowCamera = directionalLight.shadow.camera as THREE.OrthographicCamera
|
||||
shadowCamera.top = 24
|
||||
shadowCamera.bottom = -24
|
||||
shadowCamera.left = -24
|
||||
shadowCamera.right = 24
|
||||
shadowCamera.far = 64
|
||||
directionalLight.shadow.bias = -0.004
|
||||
scene.add(directionalLight)
|
||||
|
||||
ambientLight = new THREE.AmbientLight(0xFFFFFF, 1)
|
||||
scene.add(ambientLight)
|
||||
|
||||
container.appendChild(renderer.domElement)
|
||||
|
||||
const resize = () => {
|
||||
if (!container || !renderer || !camera)
|
||||
return
|
||||
|
||||
const width = container.offsetWidth
|
||||
const height = container.offsetHeight
|
||||
|
||||
renderer.setSize(width, height)
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(resize)
|
||||
resizeObserver.observe(container)
|
||||
|
||||
resize()
|
||||
|
||||
const animate = () => {
|
||||
animationId = requestAnimationFrame(animate)
|
||||
|
||||
if (beamMesh && beamMesh.material) {
|
||||
beamMesh.material.uniforms.time.value += 0.1 * 0.016
|
||||
}
|
||||
|
||||
if (renderer && scene && camera) {
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(animate);
|
||||
(container as HTMLDivElement & { _resizeObserver?: ResizeObserver })._resizeObserver = resizeObserver
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
animationId = null
|
||||
}
|
||||
|
||||
if (containerRef.value) {
|
||||
const container = containerRef.value as HTMLDivElement & { _resizeObserver?: ResizeObserver }
|
||||
|
||||
if (container._resizeObserver) {
|
||||
container._resizeObserver.disconnect()
|
||||
delete container._resizeObserver
|
||||
}
|
||||
|
||||
if (renderer && renderer.domElement.parentNode === container) {
|
||||
container.removeChild(renderer.domElement)
|
||||
}
|
||||
}
|
||||
|
||||
if (beamMesh) {
|
||||
if (beamMesh.geometry)
|
||||
beamMesh.geometry.dispose()
|
||||
if (beamMesh.material)
|
||||
beamMesh.material.dispose()
|
||||
beamMesh = null
|
||||
}
|
||||
|
||||
if (renderer) {
|
||||
renderer.dispose()
|
||||
renderer = null
|
||||
}
|
||||
|
||||
scene = null
|
||||
camera = null
|
||||
directionalLight = null
|
||||
ambientLight = null
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [
|
||||
props.beamWidth,
|
||||
props.beamHeight,
|
||||
props.beamNumber,
|
||||
props.lightColor,
|
||||
props.speed,
|
||||
props.noiseIntensity,
|
||||
props.scale,
|
||||
props.rotation,
|
||||
],
|
||||
() => {
|
||||
initThreeJS()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
initThreeJS()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="home-hero-effect-beams" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-hero-effect-beams {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
</style>
|
||||
346
theme/src/client/components/background/DotGrid.vue
Normal file
@ -0,0 +1,346 @@
|
||||
<!--
|
||||
***************************************************
|
||||
fork from https://github.com/DavidHDev/vue-bits MIT License
|
||||
modified by pengzhanbo
|
||||
***************************************************
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type { ThemeHomeHeroDotGrid } from '../../../shared/index.js'
|
||||
import { gsap } from 'gsap'
|
||||
import { InertiaPlugin } from 'gsap/InertiaPlugin'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useCssVar } from '../../composables/index.js'
|
||||
|
||||
const props = withDefaults(defineProps<ThemeHomeHeroDotGrid>(), {
|
||||
dotSize: 5,
|
||||
gap: 15,
|
||||
baseColor: '',
|
||||
activeColor: '',
|
||||
proximity: 120,
|
||||
speedTrigger: 100,
|
||||
shockRadius: 250,
|
||||
shockStrength: 5,
|
||||
maxSpeed: 5000,
|
||||
resistance: 750,
|
||||
returnDuration: 1.5,
|
||||
className: '',
|
||||
style: () => ({}),
|
||||
})
|
||||
|
||||
gsap.registerPlugin(InertiaPlugin)
|
||||
|
||||
function throttle<T extends unknown[]>(func: (...args: T) => void, limit: number) {
|
||||
let lastCall = 0
|
||||
return function (this: unknown, ...args: T) {
|
||||
const now = performance.now()
|
||||
if (now - lastCall >= limit) {
|
||||
lastCall = now
|
||||
func.apply(this, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Dot {
|
||||
cx: number
|
||||
cy: number
|
||||
xOffset: number
|
||||
yOffset: number
|
||||
_inertiaApplied: boolean
|
||||
}
|
||||
|
||||
const wrapperRef = useTemplateRef<HTMLDivElement>('wrapperRef')
|
||||
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef')
|
||||
const dots = ref<Dot[]>([])
|
||||
const pointer = ref({
|
||||
x: 0,
|
||||
y: 0,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
speed: 0,
|
||||
lastTime: 0,
|
||||
lastX: 0,
|
||||
lastY: 0,
|
||||
})
|
||||
|
||||
function hexToRgb(hex: string) {
|
||||
const m = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i)
|
||||
if (!m)
|
||||
return { r: 0, g: 0, b: 0 }
|
||||
return {
|
||||
r: Number.parseInt(m[1], 16),
|
||||
g: Number.parseInt(m[2], 16),
|
||||
b: Number.parseInt(m[3], 16),
|
||||
}
|
||||
}
|
||||
|
||||
const brandColor = useCssVar('--vp-c-brand-3', '#8cccd5')
|
||||
const mutedColor = useCssVar('--vp-c-divider', '#ebebf5')
|
||||
|
||||
const baseRgb = computed(() => hexToRgb(props.baseColor || mutedColor.value!))
|
||||
const activeRgb = computed(() => hexToRgb(props.activeColor || brandColor.value!))
|
||||
|
||||
const circlePath = computed(() => {
|
||||
if (typeof window === 'undefined' || !window.Path2D)
|
||||
return null
|
||||
|
||||
const p = new Path2D()
|
||||
p.arc(0, 0, props.dotSize / 2, 0, Math.PI * 2)
|
||||
return p
|
||||
})
|
||||
|
||||
function buildGrid() {
|
||||
const wrap = wrapperRef.value
|
||||
const canvas = canvasRef.value
|
||||
if (!wrap || !canvas)
|
||||
return
|
||||
|
||||
const { width, height } = wrap.getBoundingClientRect()
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (ctx)
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const cols = Math.floor((width + props.gap) / (props.dotSize + props.gap))
|
||||
const rows = Math.floor((height + props.gap) / (props.dotSize + props.gap))
|
||||
const cell = props.dotSize + props.gap
|
||||
|
||||
const gridW = cell * cols - props.gap
|
||||
const gridH = cell * rows - props.gap
|
||||
|
||||
const extraX = width - gridW
|
||||
const extraY = height - gridH
|
||||
|
||||
const startX = extraX / 2 + props.dotSize / 2
|
||||
const startY = extraY / 2 + props.dotSize / 2
|
||||
|
||||
const newDots: Dot[] = []
|
||||
for (let y = 0; y < rows; y++) {
|
||||
for (let x = 0; x < cols; x++) {
|
||||
const cx = startX + x * cell
|
||||
const cy = startY + y * cell
|
||||
newDots.push({ cx, cy, xOffset: 0, yOffset: 0, _inertiaApplied: false })
|
||||
}
|
||||
}
|
||||
dots.value = newDots
|
||||
}
|
||||
|
||||
let rafId: number
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
function draw() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas)
|
||||
return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx)
|
||||
return
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
const { x: px, y: py } = pointer.value
|
||||
const proxSq = props.proximity * props.proximity
|
||||
|
||||
for (const dot of dots.value) {
|
||||
const ox = dot.cx + dot.xOffset
|
||||
const oy = dot.cy + dot.yOffset
|
||||
const dx = dot.cx - px
|
||||
const dy = dot.cy - py
|
||||
const dsq = dx * dx + dy * dy
|
||||
|
||||
let style = props.baseColor || mutedColor.value!
|
||||
if (dsq <= proxSq) {
|
||||
const dist = Math.sqrt(dsq)
|
||||
const t = 1 - dist / props.proximity
|
||||
const r = Math.round(baseRgb.value.r + (activeRgb.value.r - baseRgb.value.r) * t)
|
||||
const g = Math.round(baseRgb.value.g + (activeRgb.value.g - baseRgb.value.g) * t)
|
||||
const b = Math.round(baseRgb.value.b + (activeRgb.value.b - baseRgb.value.b) * t)
|
||||
style = `rgb(${r},${g},${b})`
|
||||
}
|
||||
|
||||
if (circlePath.value) {
|
||||
ctx.save()
|
||||
ctx.translate(ox, oy)
|
||||
ctx.fillStyle = style
|
||||
ctx.fill(circlePath.value)
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
|
||||
rafId = requestAnimationFrame(draw)
|
||||
}
|
||||
|
||||
function onMove(e: MouseEvent) {
|
||||
const now = performance.now()
|
||||
const pr = pointer.value
|
||||
const dt = pr.lastTime ? now - pr.lastTime : 16
|
||||
const dx = e.clientX - pr.lastX
|
||||
const dy = e.clientY - pr.lastY
|
||||
let vx = (dx / dt) * 1000
|
||||
let vy = (dy / dt) * 1000
|
||||
let speed = Math.hypot(vx, vy)
|
||||
if (speed > props.maxSpeed) {
|
||||
const scale = props.maxSpeed / speed
|
||||
vx *= scale
|
||||
vy *= scale
|
||||
speed = props.maxSpeed
|
||||
}
|
||||
pr.lastTime = now
|
||||
pr.lastX = e.clientX
|
||||
pr.lastY = e.clientY
|
||||
pr.vx = vx
|
||||
pr.vy = vy
|
||||
pr.speed = speed
|
||||
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas)
|
||||
return
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
pr.x = e.clientX - rect.left
|
||||
pr.y = e.clientY - rect.top
|
||||
|
||||
for (const dot of dots.value) {
|
||||
const dist = Math.hypot(dot.cx - pr.x, dot.cy - pr.y)
|
||||
if (speed > props.speedTrigger && dist < props.proximity && !dot._inertiaApplied) {
|
||||
dot._inertiaApplied = true
|
||||
gsap.killTweensOf(dot)
|
||||
const pushX = dot.cx - pr.x + vx * 0.005
|
||||
const pushY = dot.cy - pr.y + vy * 0.005
|
||||
gsap.to(dot, {
|
||||
inertia: { xOffset: pushX, yOffset: pushY, resistance: props.resistance },
|
||||
onComplete: () => {
|
||||
gsap.to(dot, {
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
duration: props.returnDuration,
|
||||
ease: 'elastic.out(1,0.75)',
|
||||
})
|
||||
dot._inertiaApplied = false
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onClick(e: MouseEvent) {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas)
|
||||
return
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const cx = e.clientX - rect.left
|
||||
const cy = e.clientY - rect.top
|
||||
for (const dot of dots.value) {
|
||||
const dist = Math.hypot(dot.cx - cx, dot.cy - cy)
|
||||
if (dist < props.shockRadius && !dot._inertiaApplied) {
|
||||
dot._inertiaApplied = true
|
||||
gsap.killTweensOf(dot)
|
||||
const falloff = Math.max(0, 1 - dist / props.shockRadius)
|
||||
const pushX = (dot.cx - cx) * props.shockStrength * falloff
|
||||
const pushY = (dot.cy - cy) * props.shockStrength * falloff
|
||||
gsap.to(dot, {
|
||||
inertia: { xOffset: pushX, yOffset: pushY, resistance: props.resistance },
|
||||
onComplete: () => {
|
||||
gsap.to(dot, {
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
duration: props.returnDuration,
|
||||
ease: 'elastic.out(1,0.75)',
|
||||
})
|
||||
dot._inertiaApplied = false
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const throttledMove = throttle(onMove, 50)
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
|
||||
buildGrid()
|
||||
|
||||
if (circlePath.value) {
|
||||
draw()
|
||||
}
|
||||
|
||||
if ('ResizeObserver' in window) {
|
||||
resizeObserver = new ResizeObserver(buildGrid)
|
||||
if (wrapperRef.value) {
|
||||
resizeObserver.observe(wrapperRef.value)
|
||||
}
|
||||
}
|
||||
else {
|
||||
(window as Window).addEventListener('resize', buildGrid)
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', throttledMove, { passive: true })
|
||||
window.addEventListener('click', onClick)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId)
|
||||
}
|
||||
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
else {
|
||||
window.removeEventListener('resize', buildGrid)
|
||||
}
|
||||
|
||||
window.removeEventListener('mousemove', throttledMove)
|
||||
window.removeEventListener('click', onClick)
|
||||
})
|
||||
|
||||
watch([() => props.dotSize, () => props.gap, baseRgb], () => {
|
||||
buildGrid()
|
||||
})
|
||||
|
||||
watch([() => props.proximity, () => props.baseColor, activeRgb, baseRgb, circlePath], () => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId)
|
||||
}
|
||||
if (circlePath.value) {
|
||||
draw()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section :class="`home-hero-effect-dot-grid ${className}`" :style="style">
|
||||
<div ref="wrapperRef" class="dot-grid-container">
|
||||
<canvas ref="canvasRef" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-hero-effect-dot-grid {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dot-grid-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dot-grid-container canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
1256
theme/src/client/components/background/HyperSpeed.vue
Normal file
52
theme/src/client/components/background/ImageBg.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import type { ThemeHomeHero } from '../../../shared/index.js'
|
||||
import { computed } from 'vue'
|
||||
import { withBase } from 'vuepress/client'
|
||||
import { isLinkHttp } from 'vuepress/shared'
|
||||
import { useData } from '../../composables/index.js'
|
||||
import { isGradient } from '../../utils/index.js'
|
||||
|
||||
const props = defineProps<ThemeHomeHero>()
|
||||
|
||||
const { isDark } = useData()
|
||||
|
||||
const heroBackground = computed(() => {
|
||||
const image = props.backgroundImage
|
||||
? typeof props.backgroundImage === 'string'
|
||||
? props.backgroundImage
|
||||
// 如果 dark 没有,取 light
|
||||
: (props.backgroundImage[isDark.value ? 'dark' : 'light'] ?? props.backgroundImage.light)
|
||||
: ''
|
||||
const background = (image || props.background)?.trim()
|
||||
|
||||
if (!background)
|
||||
return null
|
||||
|
||||
const gradient = isGradient(background)
|
||||
|
||||
const link = isLinkHttp(background) || gradient ? background : withBase(background)
|
||||
return {
|
||||
'background-image': gradient ? background : `url(${link})`,
|
||||
'background-attachment': props.backgroundAttachment,
|
||||
'--vp-hero-bg-filter': props.filter,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="heroBackground" class="home-hero-bg" :style="heroBackground" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-hero-bg {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: var(--vp-hero-bg-filter);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
</style>
|
||||
227
theme/src/client/components/background/Iridescence.vue
Normal file
@ -0,0 +1,227 @@
|
||||
<!--
|
||||
***************************************************
|
||||
fork from https://github.com/DavidHDev/vue-bits MIT License
|
||||
modified by pengzhanbo
|
||||
***************************************************
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type { OGLRenderingContext } from 'ogl'
|
||||
import type { ThemeHomeHeroIridescence } from '../../../shared/index.js'
|
||||
import { Color, Mesh, Program, Renderer, Triangle } from 'ogl'
|
||||
import { computed, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { isPlainObject } from 'vuepress/shared'
|
||||
import { useDarkMode } from '../../composables/index.js'
|
||||
|
||||
const props = withDefaults(defineProps<ThemeHomeHeroIridescence>(), {
|
||||
color: () => ({ light: [1, 1, 1], dark: [0.5, 0.5, 0.5] }),
|
||||
speed: 1.0,
|
||||
amplitude: 0.1,
|
||||
mouseReact: true,
|
||||
})
|
||||
|
||||
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
|
||||
const mousePos = ref({ x: 0.5, y: 0.5 })
|
||||
|
||||
const isDark = useDarkMode()
|
||||
|
||||
const DEFAULT_COLOR = [1, 1, 1] as const
|
||||
const color = computed(() => {
|
||||
if (isPlainObject(props.color))
|
||||
return props.color[isDark.value ? 'dark' : 'light'] || DEFAULT_COLOR
|
||||
|
||||
if (Array.isArray(props.color))
|
||||
return props.color
|
||||
return DEFAULT_COLOR
|
||||
})
|
||||
|
||||
let renderer: Renderer | null = null
|
||||
let gl: OGLRenderingContext | null = null
|
||||
let program: Program | null = null
|
||||
let mesh: Mesh | null = null
|
||||
let animationId: number | null = null
|
||||
|
||||
const vertexShader = `
|
||||
attribute vec2 uv;
|
||||
attribute vec2 position;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position, 0, 1);
|
||||
}
|
||||
`
|
||||
|
||||
const fragmentShader = `
|
||||
precision highp float;
|
||||
|
||||
uniform float uTime;
|
||||
uniform vec3 uColor;
|
||||
uniform vec3 uResolution;
|
||||
uniform vec2 uMouse;
|
||||
uniform float uAmplitude;
|
||||
uniform float uSpeed;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
float mr = min(uResolution.x, uResolution.y);
|
||||
vec2 uv = (vUv.xy * 2.0 - 1.0) * uResolution.xy / mr;
|
||||
|
||||
uv += (uMouse - vec2(0.5)) * uAmplitude;
|
||||
|
||||
float d = -uTime * 0.5 * uSpeed;
|
||||
float a = 0.0;
|
||||
for (float i = 0.0; i < 8.0; ++i) {
|
||||
a += cos(i - d - a * uv.x);
|
||||
d += sin(uv.y * i + a);
|
||||
}
|
||||
d += uTime * 0.5 * uSpeed;
|
||||
vec3 col = vec3(cos(uv * vec2(d, a)) * 0.6 + 0.4, cos(a + d) * 0.5 + 0.5);
|
||||
col = cos(col * cos(vec3(d, a, 2.5)) * 0.5 + 0.5) * uColor;
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}
|
||||
`
|
||||
|
||||
function resize() {
|
||||
if (!containerRef.value || !renderer || !program || !gl)
|
||||
return
|
||||
|
||||
const container = containerRef.value
|
||||
const scale = 1
|
||||
renderer.setSize(container.offsetWidth * scale, container.offsetHeight * scale)
|
||||
|
||||
if (program) {
|
||||
program.uniforms.uResolution.value = new Color(
|
||||
gl.canvas.width,
|
||||
gl.canvas.height,
|
||||
gl.canvas.width / gl.canvas.height,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
if (!containerRef.value || !program)
|
||||
return
|
||||
|
||||
const rect = containerRef.value.getBoundingClientRect()
|
||||
const x = (e.clientX - rect.left) / rect.width
|
||||
const y = 1.0 - (e.clientY - rect.top) / rect.height
|
||||
|
||||
mousePos.value = { x, y }
|
||||
if (program.uniforms.uMouse.value) {
|
||||
program.uniforms.uMouse.value[0] = x
|
||||
program.uniforms.uMouse.value[1] = y
|
||||
}
|
||||
}
|
||||
|
||||
function update(t: number) {
|
||||
if (!program || !renderer || !mesh)
|
||||
return
|
||||
|
||||
animationId = requestAnimationFrame(update)
|
||||
program.uniforms.uTime.value = t * 0.001
|
||||
renderer.render({ scene: mesh })
|
||||
}
|
||||
|
||||
function initializeScene() {
|
||||
if (!containerRef.value)
|
||||
return
|
||||
|
||||
cleanup()
|
||||
|
||||
const container = containerRef.value
|
||||
renderer = new Renderer()
|
||||
gl = renderer.gl
|
||||
gl.clearColor(1, 1, 1, 1)
|
||||
|
||||
const geometry = new Triangle(gl)
|
||||
program = new Program(gl, {
|
||||
vertex: vertexShader,
|
||||
fragment: fragmentShader,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uColor: { value: new Color(...color.value) },
|
||||
uResolution: {
|
||||
value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height),
|
||||
},
|
||||
uMouse: { value: new Float32Array([mousePos.value.x, mousePos.value.y]) },
|
||||
uAmplitude: { value: props.amplitude },
|
||||
uSpeed: { value: props.speed },
|
||||
},
|
||||
})
|
||||
|
||||
mesh = new Mesh(gl, { geometry, program })
|
||||
|
||||
const canvas = gl.canvas as HTMLCanvasElement
|
||||
canvas.style.width = '100%'
|
||||
canvas.style.height = '100%'
|
||||
canvas.style.display = 'block'
|
||||
|
||||
container.appendChild(canvas)
|
||||
|
||||
window.addEventListener('resize', resize)
|
||||
if (props.mouseReact) {
|
||||
container.addEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
|
||||
resize()
|
||||
animationId = requestAnimationFrame(update)
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
animationId = null
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', resize)
|
||||
|
||||
if (containerRef.value) {
|
||||
containerRef.value.removeEventListener('mousemove', handleMouseMove)
|
||||
|
||||
const canvas = containerRef.value.querySelector('canvas')
|
||||
if (canvas) {
|
||||
containerRef.value.removeChild(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
if (gl) {
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
||||
}
|
||||
|
||||
renderer = null
|
||||
gl = null
|
||||
program = null
|
||||
mesh = null
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initializeScene()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
watch(
|
||||
[color, () => props.speed, () => props.amplitude, () => props.mouseReact],
|
||||
() => {
|
||||
initializeScene()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="home-hero-effect-iridescence" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-hero-effect-iridescence {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
</style>
|
||||
284
theme/src/client/components/background/Lightning.vue
Normal file
@ -0,0 +1,284 @@
|
||||
<!--
|
||||
***************************************************
|
||||
fork from https://github.com/DavidHDev/vue-bits MIT License
|
||||
modified by pengzhanbo
|
||||
***************************************************
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type { ThemeHomeHeroLightning } from '../../../shared/index.js'
|
||||
import { onMounted, onUnmounted, useTemplateRef, watch } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<ThemeHomeHeroLightning>(), {
|
||||
hue: 255,
|
||||
xOffset: 0,
|
||||
speed: 1,
|
||||
intensity: 1,
|
||||
size: 1,
|
||||
})
|
||||
|
||||
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef')
|
||||
let animationId = 0
|
||||
let gl: WebGLRenderingContext | null = null
|
||||
let program: WebGLProgram | null = null
|
||||
let startTime = 0
|
||||
|
||||
const vertexShaderSource = `
|
||||
attribute vec2 aPosition;
|
||||
void main() {
|
||||
gl_Position = vec4(aPosition, 0.0, 1.0);
|
||||
}
|
||||
`
|
||||
|
||||
const fragmentShaderSource = `
|
||||
precision mediump float;
|
||||
uniform vec2 iResolution;
|
||||
uniform float iTime;
|
||||
uniform float uHue;
|
||||
uniform float uXOffset;
|
||||
uniform float uSpeed;
|
||||
uniform float uIntensity;
|
||||
uniform float uSize;
|
||||
|
||||
#define OCTAVE_COUNT 10
|
||||
|
||||
vec3 hsv2rgb(vec3 c) {
|
||||
vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0,4.0,2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
|
||||
return c.z * mix(vec3(1.0), rgb, c.y);
|
||||
}
|
||||
|
||||
float hash11(float p) {
|
||||
p = fract(p * .1031);
|
||||
p *= p + 33.33;
|
||||
p *= p + p;
|
||||
return fract(p);
|
||||
}
|
||||
|
||||
float hash12(vec2 p) {
|
||||
vec3 p3 = fract(vec3(p.xyx) * .1031);
|
||||
p3 += dot(p3, p3.yzx + 33.33);
|
||||
return fract((p3.x + p3.y) * p3.z);
|
||||
}
|
||||
|
||||
mat2 rotate2d(float theta) {
|
||||
float c = cos(theta);
|
||||
float s = sin(theta);
|
||||
return mat2(c, -s, s, c);
|
||||
}
|
||||
|
||||
float noise(vec2 p) {
|
||||
vec2 ip = floor(p);
|
||||
vec2 fp = fract(p);
|
||||
float a = hash12(ip);
|
||||
float b = hash12(ip + vec2(1.0, 0.0));
|
||||
float c = hash12(ip + vec2(0.0, 1.0));
|
||||
float d = hash12(ip + vec2(1.0, 1.0));
|
||||
|
||||
vec2 t = smoothstep(0.0, 1.0, fp);
|
||||
return mix(mix(a, b, t.x), mix(c, d, t.x), t.y);
|
||||
}
|
||||
|
||||
float fbm(vec2 p) {
|
||||
float value = 0.0;
|
||||
float amplitude = 0.5;
|
||||
for (int i = 0; i < OCTAVE_COUNT; ++i) {
|
||||
value += amplitude * noise(p);
|
||||
p *= rotate2d(0.45);
|
||||
p *= 2.0;
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
|
||||
vec2 uv = fragCoord / iResolution.xy;
|
||||
uv = 2.0 * uv - 1.0;
|
||||
uv.x *= iResolution.x / iResolution.y;
|
||||
uv.x += uXOffset;
|
||||
|
||||
uv += 2.0 * fbm(uv * uSize + 0.8 * iTime * uSpeed) - 1.0;
|
||||
|
||||
float dist = abs(uv.x);
|
||||
vec3 baseColor = hsv2rgb(vec3(uHue / 360.0, 0.7, 0.8));
|
||||
vec3 col = baseColor * pow(mix(0.0, 0.07, hash11(iTime * uSpeed)) / dist, 1.0) * uIntensity;
|
||||
col = pow(col, vec3(1.0));
|
||||
fragColor = vec4(col, 1.0);
|
||||
}
|
||||
|
||||
void main() {
|
||||
mainImage(gl_FragColor, gl_FragCoord.xy);
|
||||
}
|
||||
`
|
||||
|
||||
function compileShader(source: string, type: number): WebGLShader | null {
|
||||
if (!gl)
|
||||
return null
|
||||
const shader = gl.createShader(type)
|
||||
if (!shader)
|
||||
return null
|
||||
gl.shaderSource(shader, source)
|
||||
gl.compileShader(shader)
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
console.error('Shader compile error:', gl.getShaderInfoLog(shader))
|
||||
gl.deleteShader(shader)
|
||||
return null
|
||||
}
|
||||
return shader
|
||||
}
|
||||
|
||||
function initWebGL() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas)
|
||||
return
|
||||
|
||||
const resizeCanvas = () => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
|
||||
let width = rect.width
|
||||
let height = rect.height
|
||||
|
||||
let parent = canvas.parentElement
|
||||
while (parent && (!width || !height)) {
|
||||
if (parent.offsetWidth && parent.offsetHeight) {
|
||||
width = parent.offsetWidth
|
||||
height = parent.offsetHeight
|
||||
break
|
||||
}
|
||||
parent = parent.parentElement
|
||||
}
|
||||
|
||||
if (!width || !height) {
|
||||
width = window.innerWidth
|
||||
height = window.innerHeight
|
||||
}
|
||||
|
||||
width = Math.max(width, 300)
|
||||
height = Math.max(height, 300)
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
|
||||
canvas.style.width = '100%'
|
||||
canvas.style.height = '100%'
|
||||
canvas.style.display = 'block'
|
||||
canvas.style.position = 'absolute'
|
||||
canvas.style.top = '0'
|
||||
canvas.style.left = '0'
|
||||
}
|
||||
|
||||
resizeCanvas()
|
||||
window.addEventListener('resize', resizeCanvas)
|
||||
|
||||
gl = canvas.getContext('webgl')
|
||||
if (!gl) {
|
||||
console.error('WebGL not supported')
|
||||
return
|
||||
}
|
||||
|
||||
const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER)
|
||||
const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
|
||||
if (!vertexShader || !fragmentShader)
|
||||
return
|
||||
|
||||
program = gl.createProgram()
|
||||
if (!program)
|
||||
return
|
||||
gl.attachShader(program, vertexShader)
|
||||
gl.attachShader(program, fragmentShader)
|
||||
gl.linkProgram(program)
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||
console.error('Program linking error:', gl.getProgramInfoLog(program))
|
||||
return
|
||||
}
|
||||
gl.useProgram(program)
|
||||
|
||||
const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1])
|
||||
const vertexBuffer = gl.createBuffer()
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
|
||||
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
|
||||
|
||||
const aPosition = gl.getAttribLocation(program, 'aPosition')
|
||||
gl.enableVertexAttribArray(aPosition)
|
||||
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)
|
||||
|
||||
startTime = performance.now()
|
||||
render()
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizeCanvas)
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!gl || !program || !canvasRef.value)
|
||||
return
|
||||
|
||||
const canvas = canvasRef.value
|
||||
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
if (canvas.width !== rect.width || canvas.height !== rect.height) {
|
||||
canvas.width = rect.width
|
||||
canvas.height = rect.height
|
||||
canvas.style.width = `${rect.width}px`
|
||||
canvas.style.height = `${rect.height}px`
|
||||
}
|
||||
|
||||
gl.viewport(0, 0, canvas.width, canvas.height)
|
||||
|
||||
const iResolutionLocation = gl.getUniformLocation(program, 'iResolution')
|
||||
const iTimeLocation = gl.getUniformLocation(program, 'iTime')
|
||||
const uHueLocation = gl.getUniformLocation(program, 'uHue')
|
||||
const uXOffsetLocation = gl.getUniformLocation(program, 'uXOffset')
|
||||
const uSpeedLocation = gl.getUniformLocation(program, 'uSpeed')
|
||||
const uIntensityLocation = gl.getUniformLocation(program, 'uIntensity')
|
||||
const uSizeLocation = gl.getUniformLocation(program, 'uSize')
|
||||
|
||||
gl.uniform2f(iResolutionLocation, canvas.width, canvas.height)
|
||||
const currentTime = performance.now()
|
||||
gl.uniform1f(iTimeLocation, (currentTime - startTime) / 1000.0)
|
||||
gl.uniform1f(uHueLocation, props.hue)
|
||||
gl.uniform1f(uXOffsetLocation, props.xOffset)
|
||||
gl.uniform1f(uSpeedLocation, props.speed)
|
||||
gl.uniform1f(uIntensityLocation, props.intensity)
|
||||
gl.uniform1f(uSizeLocation, props.size)
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6)
|
||||
animationId = requestAnimationFrame(render)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initWebGL()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [props.hue, props.xOffset, props.speed, props.intensity, props.size],
|
||||
() => {},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="home-hero-effect-lighting">
|
||||
<canvas ref="canvasRef" class=" mix-blend-screen" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-hero-effect-lighting {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.home-hero-effect-lighting canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
</style>
|
||||
1341
theme/src/client/components/background/LiquidEther.vue
Normal file
323
theme/src/client/components/background/Orb.vue
Normal file
@ -0,0 +1,323 @@
|
||||
<!--
|
||||
***************************************************
|
||||
fork from https://github.com/DavidHDev/vue-bits MIT License
|
||||
modified by pengzhanbo
|
||||
***************************************************
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type { ThemeHomeHeroOrb } from '../../../shared/index.js'
|
||||
import { Mesh, Program, Renderer, Triangle, Vec3 } from 'ogl'
|
||||
import { onMounted, onUnmounted, useTemplateRef, watch } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<ThemeHomeHeroOrb>(), {
|
||||
hue: 0,
|
||||
hoverIntensity: 0.2,
|
||||
rotateOnHover: true,
|
||||
forceHoverState: false,
|
||||
className: '',
|
||||
})
|
||||
|
||||
const ctnDom = useTemplateRef<HTMLDivElement>('ctnDom')
|
||||
|
||||
const vert = /* glsl */ `
|
||||
precision highp float;
|
||||
attribute vec2 position;
|
||||
attribute vec2 uv;
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`
|
||||
|
||||
const frag = /* glsl */ `
|
||||
precision highp float;
|
||||
|
||||
uniform float iTime;
|
||||
uniform vec3 iResolution;
|
||||
uniform float hue;
|
||||
uniform float hover;
|
||||
uniform float rot;
|
||||
uniform float hoverIntensity;
|
||||
varying vec2 vUv;
|
||||
|
||||
vec3 rgb2yiq(vec3 c) {
|
||||
float y = dot(c, vec3(0.299, 0.587, 0.114));
|
||||
float i = dot(c, vec3(0.596, -0.274, -0.322));
|
||||
float q = dot(c, vec3(0.211, -0.523, 0.312));
|
||||
return vec3(y, i, q);
|
||||
}
|
||||
|
||||
vec3 yiq2rgb(vec3 c) {
|
||||
float r = c.x + 0.956 * c.y + 0.621 * c.z;
|
||||
float g = c.x - 0.272 * c.y - 0.647 * c.z;
|
||||
float b = c.x - 1.106 * c.y + 1.703 * c.z;
|
||||
return vec3(r, g, b);
|
||||
}
|
||||
|
||||
vec3 adjustHue(vec3 color, float hueDeg) {
|
||||
float hueRad = hueDeg * 3.14159265 / 180.0;
|
||||
vec3 yiq = rgb2yiq(color);
|
||||
float cosA = cos(hueRad);
|
||||
float sinA = sin(hueRad);
|
||||
float i = yiq.y * cosA - yiq.z * sinA;
|
||||
float q = yiq.y * sinA + yiq.z * cosA;
|
||||
yiq.y = i;
|
||||
yiq.z = q;
|
||||
return yiq2rgb(yiq);
|
||||
}
|
||||
|
||||
vec3 hash33(vec3 p3) {
|
||||
p3 = fract(p3 * vec3(0.1031, 0.11369, 0.13787));
|
||||
p3 += dot(p3, p3.yxz + 19.19);
|
||||
return -1.0 + 2.0 * fract(vec3(
|
||||
p3.x + p3.y,
|
||||
p3.x + p3.z,
|
||||
p3.y + p3.z
|
||||
) * p3.zyx);
|
||||
}
|
||||
|
||||
float snoise3(vec3 p) {
|
||||
const float K1 = 0.333333333;
|
||||
const float K2 = 0.166666667;
|
||||
vec3 i = floor(p + (p.x + p.y + p.z) * K1);
|
||||
vec3 d0 = p - (i - (i.x + i.y + i.z) * K2);
|
||||
vec3 e = step(vec3(0.0), d0 - d0.yzx);
|
||||
vec3 i1 = e * (1.0 - e.zxy);
|
||||
vec3 i2 = 1.0 - e.zxy * (1.0 - e);
|
||||
vec3 d1 = d0 - (i1 - K2);
|
||||
vec3 d2 = d0 - (i2 - K1);
|
||||
vec3 d3 = d0 - 0.5;
|
||||
vec4 h = max(0.6 - vec4(
|
||||
dot(d0, d0),
|
||||
dot(d1, d1),
|
||||
dot(d2, d2),
|
||||
dot(d3, d3)
|
||||
), 0.0);
|
||||
vec4 n = h * h * h * h * vec4(
|
||||
dot(d0, hash33(i)),
|
||||
dot(d1, hash33(i + i1)),
|
||||
dot(d2, hash33(i + i2)),
|
||||
dot(d3, hash33(i + 1.0))
|
||||
);
|
||||
return dot(vec4(31.316), n);
|
||||
}
|
||||
|
||||
vec4 extractAlpha(vec3 colorIn) {
|
||||
float a = max(max(colorIn.r, colorIn.g), colorIn.b);
|
||||
return vec4(colorIn.rgb / (a + 1e-5), a);
|
||||
}
|
||||
|
||||
const vec3 baseColor1 = vec3(0.611765, 0.262745, 0.996078);
|
||||
const vec3 baseColor2 = vec3(0.298039, 0.760784, 0.913725);
|
||||
const vec3 baseColor3 = vec3(0.062745, 0.078431, 0.600000);
|
||||
const float innerRadius = 0.6;
|
||||
const float noiseScale = 0.65;
|
||||
|
||||
float light1(float intensity, float attenuation, float dist) {
|
||||
return intensity / (1.0 + dist * attenuation);
|
||||
}
|
||||
|
||||
float light2(float intensity, float attenuation, float dist) {
|
||||
return intensity / (1.0 + dist * dist * attenuation);
|
||||
}
|
||||
|
||||
vec4 draw(vec2 uv) {
|
||||
vec3 color1 = adjustHue(baseColor1, hue);
|
||||
vec3 color2 = adjustHue(baseColor2, hue);
|
||||
vec3 color3 = adjustHue(baseColor3, hue);
|
||||
|
||||
float ang = atan(uv.y, uv.x);
|
||||
float len = length(uv);
|
||||
float invLen = len > 0.0 ? 1.0 / len : 0.0;
|
||||
|
||||
float n0 = snoise3(vec3(uv * noiseScale, iTime * 0.5)) * 0.5 + 0.5;
|
||||
float r0 = mix(mix(innerRadius, 1.0, 0.4), mix(innerRadius, 1.0, 0.6), n0);
|
||||
float d0 = distance(uv, (r0 * invLen) * uv);
|
||||
float v0 = light1(1.0, 10.0, d0);
|
||||
v0 *= smoothstep(r0 * 1.05, r0, len);
|
||||
float cl = cos(ang + iTime * 2.0) * 0.5 + 0.5;
|
||||
|
||||
float a = iTime * -1.0;
|
||||
vec2 pos = vec2(cos(a), sin(a)) * r0;
|
||||
float d = distance(uv, pos);
|
||||
float v1 = light2(1.5, 5.0, d);
|
||||
v1 *= light1(1.0, 50.0, d0);
|
||||
|
||||
float v2 = smoothstep(1.0, mix(innerRadius, 1.0, n0 * 0.5), len);
|
||||
float v3 = smoothstep(innerRadius, mix(innerRadius, 1.0, 0.5), len);
|
||||
|
||||
vec3 col = mix(color1, color2, cl);
|
||||
col = mix(color3, col, v0);
|
||||
col = (col + v1) * v2 * v3;
|
||||
col = clamp(col, 0.0, 1.0);
|
||||
|
||||
return extractAlpha(col);
|
||||
}
|
||||
|
||||
vec4 mainImage(vec2 fragCoord) {
|
||||
vec2 center = iResolution.xy * 0.5;
|
||||
float size = min(iResolution.x, iResolution.y);
|
||||
vec2 uv = (fragCoord - center) / size * 2.0;
|
||||
|
||||
float angle = rot;
|
||||
float s = sin(angle);
|
||||
float c = cos(angle);
|
||||
uv = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y);
|
||||
|
||||
uv.x += hover * hoverIntensity * 0.1 * sin(uv.y * 10.0 + iTime);
|
||||
uv.y += hover * hoverIntensity * 0.1 * sin(uv.x * 10.0 + iTime);
|
||||
|
||||
return draw(uv);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 fragCoord = vUv * iResolution.xy;
|
||||
vec4 col = mainImage(fragCoord);
|
||||
gl_FragColor = vec4(col.rgb * col.a, col.a);
|
||||
}
|
||||
`
|
||||
|
||||
let cleanupAnimation: (() => void) | null = null
|
||||
|
||||
function setupAnimation() {
|
||||
const container = ctnDom.value
|
||||
if (!container)
|
||||
return
|
||||
|
||||
const renderer = new Renderer({ alpha: true, premultipliedAlpha: false })
|
||||
const gl = renderer.gl
|
||||
gl.clearColor(0, 0, 0, 0)
|
||||
container.appendChild(gl.canvas)
|
||||
|
||||
const geometry = new Triangle(gl)
|
||||
const program = new Program(gl, {
|
||||
vertex: vert,
|
||||
fragment: frag,
|
||||
uniforms: {
|
||||
iTime: { value: 0 },
|
||||
iResolution: {
|
||||
value: new Vec3(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height),
|
||||
},
|
||||
hue: { value: props.hue },
|
||||
hover: { value: 0 },
|
||||
rot: { value: 0 },
|
||||
hoverIntensity: { value: props.hoverIntensity },
|
||||
},
|
||||
})
|
||||
|
||||
const mesh = new Mesh(gl, { geometry, program })
|
||||
|
||||
function resize() {
|
||||
if (!container)
|
||||
return
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const width = container.clientWidth
|
||||
const height = container.clientHeight
|
||||
renderer.setSize(width * dpr, height * dpr)
|
||||
gl.canvas.style.width = `${width}px`
|
||||
gl.canvas.style.height = `${height}px`
|
||||
program.uniforms.iResolution.value.set(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)
|
||||
}
|
||||
window.addEventListener('resize', resize)
|
||||
resize()
|
||||
|
||||
let targetHover = 0
|
||||
let lastTime = 0
|
||||
let currentRot = 0
|
||||
const rotationSpeed = 0.3
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const rect = container.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
const width = rect.width
|
||||
const height = rect.height
|
||||
const size = Math.min(width, height)
|
||||
const centerX = width / 2
|
||||
const centerY = height / 2
|
||||
const uvX = ((x - centerX) / size) * 2.0
|
||||
const uvY = ((y - centerY) / size) * 2.0
|
||||
|
||||
if (Math.sqrt(uvX * uvX + uvY * uvY) < 0.8) {
|
||||
targetHover = 1
|
||||
}
|
||||
else {
|
||||
targetHover = 0
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
targetHover = 0
|
||||
}
|
||||
|
||||
container.addEventListener('mousemove', handleMouseMove)
|
||||
container.addEventListener('mouseleave', handleMouseLeave)
|
||||
|
||||
let rafId: number
|
||||
const update = (t: number) => {
|
||||
rafId = requestAnimationFrame(update)
|
||||
const dt = (t - lastTime) * 0.001
|
||||
lastTime = t
|
||||
program.uniforms.iTime.value = t * 0.001
|
||||
program.uniforms.hue.value = props.hue
|
||||
program.uniforms.hoverIntensity.value = props.hoverIntensity
|
||||
|
||||
const effectiveHover = props.forceHoverState ? 1 : targetHover
|
||||
program.uniforms.hover.value += (effectiveHover - program.uniforms.hover.value) * 0.1
|
||||
|
||||
if (props.rotateOnHover && effectiveHover > 0.5) {
|
||||
currentRot += dt * rotationSpeed
|
||||
}
|
||||
program.uniforms.rot.value = currentRot
|
||||
|
||||
renderer.render({ scene: mesh })
|
||||
}
|
||||
rafId = requestAnimationFrame(update)
|
||||
|
||||
cleanupAnimation = () => {
|
||||
cancelAnimationFrame(rafId)
|
||||
window.removeEventListener('resize', resize)
|
||||
container.removeEventListener('mousemove', handleMouseMove)
|
||||
container.removeEventListener('mouseleave', handleMouseLeave)
|
||||
container.removeChild(gl.canvas)
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setupAnimation()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (cleanupAnimation) {
|
||||
cleanupAnimation()
|
||||
cleanupAnimation = null
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
() => {
|
||||
if (cleanupAnimation) {
|
||||
cleanupAnimation()
|
||||
cleanupAnimation = null
|
||||
}
|
||||
setupAnimation()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="ctnDom" :class="`home-hero-effect-orb ${className}`" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-hero-effect-orb {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
</style>
|
||||
700
theme/src/client/components/background/PixelBlast.vue
Normal file
@ -0,0 +1,700 @@
|
||||
<!--
|
||||
***************************************************
|
||||
fork from https://github.com/DavidHDev/vue-bits MIT License
|
||||
modified by pengzhanbo
|
||||
***************************************************
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type { ThemeHomeHeroPixelBlastVariant, ThemeHomeHeroPixelPixelBlast } from '../../../shared/index.js'
|
||||
import { Effect, EffectComposer, EffectPass, RenderPass } from 'postprocessing'
|
||||
import * as THREE from 'three'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useCssVar } from '../../composables/index.js'
|
||||
|
||||
const props = withDefaults(defineProps<ThemeHomeHeroPixelPixelBlast>(), {
|
||||
variant: 'square',
|
||||
pixelSize: 4,
|
||||
color: '',
|
||||
antialias: true,
|
||||
patternScale: 2,
|
||||
patternDensity: 1,
|
||||
liquid: false,
|
||||
liquidStrength: 0.1,
|
||||
liquidRadius: 1,
|
||||
pixelSizeJitter: 0,
|
||||
enableRipples: true,
|
||||
rippleIntensityScale: 1,
|
||||
rippleThickness: 0.1,
|
||||
rippleSpeed: 0.3,
|
||||
liquidWobbleSpeed: 4.5,
|
||||
autoPauseOffscreen: true,
|
||||
speed: 0.5,
|
||||
transparent: true,
|
||||
edgeFade: 0.5,
|
||||
noiseAmount: 0,
|
||||
})
|
||||
|
||||
function createTouchTexture() {
|
||||
const size = 64
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = size
|
||||
canvas.height = size
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx)
|
||||
throw new Error('2D context not available')
|
||||
ctx.fillStyle = 'black'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
const texture = new THREE.Texture(canvas)
|
||||
texture.minFilter = THREE.LinearFilter
|
||||
texture.magFilter = THREE.LinearFilter
|
||||
texture.generateMipmaps = false
|
||||
const trail: {
|
||||
x: number
|
||||
y: number
|
||||
vx: number
|
||||
vy: number
|
||||
force: number
|
||||
age: number
|
||||
}[] = []
|
||||
let last: { x: number, y: number } | null = null
|
||||
const maxAge = 64
|
||||
let radius = 0.1 * size
|
||||
const speed = 1 / maxAge
|
||||
const clear = () => {
|
||||
ctx.fillStyle = 'black'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
const drawPoint = (p: { x: number, y: number, vx: number, vy: number, force: number, age: number }) => {
|
||||
const pos = { x: p.x * size, y: (1 - p.y) * size }
|
||||
let intensity = 1
|
||||
const easeOutSine = (t: number) => Math.sin((t * Math.PI) / 2)
|
||||
const easeOutQuad = (t: number) => -t * (t - 2)
|
||||
if (p.age < maxAge * 0.3)
|
||||
intensity = easeOutSine(p.age / (maxAge * 0.3))
|
||||
else intensity = easeOutQuad(1 - (p.age - maxAge * 0.3) / (maxAge * 0.7)) || 0
|
||||
intensity *= p.force
|
||||
const color = `${((p.vx + 1) / 2) * 255}, ${((p.vy + 1) / 2) * 255}, ${intensity * 255}`
|
||||
const offset = size * 5
|
||||
ctx.shadowOffsetX = offset
|
||||
ctx.shadowOffsetY = offset
|
||||
ctx.shadowBlur = radius
|
||||
ctx.shadowColor = `rgba(${color},${0.22 * intensity})`
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = 'rgba(255,0,0,1)'
|
||||
ctx.arc(pos.x - offset, pos.y - offset, radius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
const addTouch = (norm: { x: number, y: number }) => {
|
||||
let force = 0
|
||||
let vx = 0
|
||||
let vy = 0
|
||||
if (last) {
|
||||
const dx = norm.x - last.x
|
||||
const dy = norm.y - last.y
|
||||
if (dx === 0 && dy === 0)
|
||||
return
|
||||
const dd = dx * dx + dy * dy
|
||||
const d = Math.sqrt(dd)
|
||||
vx = dx / (d || 1)
|
||||
vy = dy / (d || 1)
|
||||
force = Math.min(dd * 10000, 1)
|
||||
}
|
||||
last = { x: norm.x, y: norm.y }
|
||||
trail.push({ x: norm.x, y: norm.y, age: 0, force, vx, vy })
|
||||
}
|
||||
const update = () => {
|
||||
clear()
|
||||
for (let i = trail.length - 1; i >= 0; i--) {
|
||||
const point = trail[i]
|
||||
const f = point.force * speed * (1 - point.age / maxAge)
|
||||
point.x += point.vx * f
|
||||
point.y += point.vy * f
|
||||
point.age++
|
||||
if (point.age > maxAge)
|
||||
trail.splice(i, 1)
|
||||
}
|
||||
for (let i = 0; i < trail.length; i++) drawPoint(trail[i])
|
||||
texture.needsUpdate = true
|
||||
}
|
||||
return {
|
||||
canvas,
|
||||
texture,
|
||||
addTouch,
|
||||
update,
|
||||
set radiusScale(v: number) {
|
||||
radius = 0.1 * size * v
|
||||
},
|
||||
get radiusScale() {
|
||||
return radius / (0.1 * size)
|
||||
},
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
function createLiquidEffect(texture: THREE.Texture, opts?: { strength?: number, freq?: number }) {
|
||||
const fragment = `
|
||||
uniform sampler2D uTexture;
|
||||
uniform float uStrength;
|
||||
uniform float uTime;
|
||||
uniform float uFreq;
|
||||
|
||||
void mainUv(inout vec2 uv) {
|
||||
vec4 tex = texture2D(uTexture, uv);
|
||||
float vx = tex.r * 2.0 - 1.0;
|
||||
float vy = tex.g * 2.0 - 1.0;
|
||||
float intensity = tex.b;
|
||||
|
||||
float wave = 0.5 + 0.5 * sin(uTime * uFreq + intensity * 6.2831853);
|
||||
|
||||
float amt = uStrength * intensity * wave;
|
||||
|
||||
uv += vec2(vx, vy) * amt;
|
||||
}
|
||||
`
|
||||
return new Effect('LiquidEffect', fragment, {
|
||||
uniforms: new Map<string, THREE.Uniform>([
|
||||
['uTexture', new THREE.Uniform(texture)],
|
||||
['uStrength', new THREE.Uniform(opts?.strength ?? 0.025)],
|
||||
['uTime', new THREE.Uniform(0)],
|
||||
['uFreq', new THREE.Uniform(opts?.freq ?? 4.5)],
|
||||
]),
|
||||
})
|
||||
}
|
||||
|
||||
const SHAPE_MAP: Record<ThemeHomeHeroPixelBlastVariant, number> = {
|
||||
square: 0,
|
||||
circle: 1,
|
||||
triangle: 2,
|
||||
diamond: 3,
|
||||
}
|
||||
|
||||
const VERTEX_SRC = `
|
||||
void main() {
|
||||
gl_Position = vec4(position, 1.0);
|
||||
}
|
||||
`
|
||||
|
||||
const FRAGMENT_SRC = `
|
||||
precision highp float;
|
||||
|
||||
uniform vec3 uColor;
|
||||
uniform vec2 uResolution;
|
||||
uniform float uTime;
|
||||
uniform float uPixelSize;
|
||||
uniform float uScale;
|
||||
uniform float uDensity;
|
||||
uniform float uPixelJitter;
|
||||
uniform int uEnableRipples;
|
||||
uniform float uRippleSpeed;
|
||||
uniform float uRippleThickness;
|
||||
uniform float uRippleIntensity;
|
||||
uniform float uEdgeFade;
|
||||
|
||||
uniform int uShapeType;
|
||||
const int SHAPE_SQUARE = 0;
|
||||
const int SHAPE_CIRCLE = 1;
|
||||
const int SHAPE_TRIANGLE = 2;
|
||||
const int SHAPE_DIAMOND = 3;
|
||||
|
||||
const int MAX_CLICKS = 10;
|
||||
|
||||
uniform vec2 uClickPos [MAX_CLICKS];
|
||||
uniform float uClickTimes[MAX_CLICKS];
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
float Bayer2(vec2 a) {
|
||||
a = floor(a);
|
||||
return fract(a.x / 2. + a.y * a.y * .75);
|
||||
}
|
||||
#define Bayer4(a) (Bayer2(.5*(a))*0.25 + Bayer2(a))
|
||||
#define Bayer8(a) (Bayer4(.5*(a))*0.25 + Bayer2(a))
|
||||
|
||||
#define FBM_OCTAVES 5
|
||||
#define FBM_LACUNARITY 1.25
|
||||
#define FBM_GAIN 1.0
|
||||
|
||||
float hash11(float n){ return fract(sin(n)*43758.5453); }
|
||||
|
||||
float vnoise(vec3 p){
|
||||
vec3 ip = floor(p);
|
||||
vec3 fp = fract(p);
|
||||
float n000 = hash11(dot(ip + vec3(0.0,0.0,0.0), vec3(1.0,57.0,113.0)));
|
||||
float n100 = hash11(dot(ip + vec3(1.0,0.0,0.0), vec3(1.0,57.0,113.0)));
|
||||
float n010 = hash11(dot(ip + vec3(0.0,1.0,0.0), vec3(1.0,57.0,113.0)));
|
||||
float n110 = hash11(dot(ip + vec3(1.0,1.0,0.0), vec3(1.0,57.0,113.0)));
|
||||
float n001 = hash11(dot(ip + vec3(0.0,0.0,1.0), vec3(1.0,57.0,113.0)));
|
||||
float n101 = hash11(dot(ip + vec3(1.0,0.0,1.0), vec3(1.0,57.0,113.0)));
|
||||
float n011 = hash11(dot(ip + vec3(0.0,1.0,1.0), vec3(1.0,57.0,113.0)));
|
||||
float n111 = hash11(dot(ip + vec3(1.0,1.0,1.0), vec3(1.0,57.0,113.0)));
|
||||
vec3 w = fp*fp*fp*(fp*(fp*6.0-15.0)+10.0);
|
||||
float x00 = mix(n000, n100, w.x);
|
||||
float x10 = mix(n010, n110, w.x);
|
||||
float x01 = mix(n001, n101, w.x);
|
||||
float x11 = mix(n011, n111, w.x);
|
||||
float y0 = mix(x00, x10, w.y);
|
||||
float y1 = mix(x01, x11, w.y);
|
||||
return mix(y0, y1, w.z) * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
float fbm2(vec2 uv, float t){
|
||||
vec3 p = vec3(uv * uScale, t);
|
||||
float amp = 1.0;
|
||||
float freq = 1.0;
|
||||
float sum = 1.0;
|
||||
for (int i = 0; i < FBM_OCTAVES; ++i){
|
||||
sum += amp * vnoise(p * freq);
|
||||
freq *= FBM_LACUNARITY;
|
||||
amp *= FBM_GAIN;
|
||||
}
|
||||
return sum * 0.5 + 0.5;
|
||||
}
|
||||
|
||||
float maskCircle(vec2 p, float cov){
|
||||
float r = sqrt(cov) * .25;
|
||||
float d = length(p - 0.5) - r;
|
||||
float aa = 0.5 * fwidth(d);
|
||||
return cov * (1.0 - smoothstep(-aa, aa, d * 2.0));
|
||||
}
|
||||
|
||||
float maskTriangle(vec2 p, vec2 id, float cov){
|
||||
bool flip = mod(id.x + id.y, 2.0) > 0.5;
|
||||
if (flip) p.x = 1.0 - p.x;
|
||||
float r = sqrt(cov);
|
||||
float d = p.y - r*(1.0 - p.x);
|
||||
float aa = fwidth(d);
|
||||
return cov * clamp(0.5 - d/aa, 0.0, 1.0);
|
||||
}
|
||||
|
||||
float maskDiamond(vec2 p, float cov){
|
||||
float r = sqrt(cov) * 0.564;
|
||||
return step(abs(p.x - 0.49) + abs(p.y - 0.49), r);
|
||||
}
|
||||
|
||||
void main(){
|
||||
float pixelSize = uPixelSize;
|
||||
vec2 fragCoord = gl_FragCoord.xy - uResolution * .5;
|
||||
float aspectRatio = uResolution.x / uResolution.y;
|
||||
|
||||
vec2 pixelId = floor(fragCoord / pixelSize);
|
||||
vec2 pixelUV = fract(fragCoord / pixelSize);
|
||||
|
||||
float cellPixelSize = 8.0 * pixelSize;
|
||||
vec2 cellId = floor(fragCoord / cellPixelSize);
|
||||
vec2 cellCoord = cellId * cellPixelSize;
|
||||
vec2 uv = cellCoord / uResolution * vec2(aspectRatio, 1.0);
|
||||
|
||||
float base = fbm2(uv, uTime * 0.05);
|
||||
base = base * 0.5 - 0.65;
|
||||
|
||||
float feed = base + (uDensity - 0.5) * 0.3;
|
||||
|
||||
float speed = uRippleSpeed;
|
||||
float thickness = uRippleThickness;
|
||||
const float dampT = 1.0;
|
||||
const float dampR = 10.0;
|
||||
|
||||
if (uEnableRipples == 1) {
|
||||
for (int i = 0; i < MAX_CLICKS; ++i){
|
||||
vec2 pos = uClickPos[i];
|
||||
if (pos.x < 0.0) continue;
|
||||
float cellPixelSize = 8.0 * pixelSize;
|
||||
vec2 cuv = (((pos - uResolution * .5 - cellPixelSize * .5) / (uResolution))) * vec2(aspectRatio, 1.0);
|
||||
float t = max(uTime - uClickTimes[i], 0.0);
|
||||
float r = distance(uv, cuv);
|
||||
float waveR = speed * t;
|
||||
float ring = exp(-pow((r - waveR) / thickness, 2.0));
|
||||
float atten = exp(-dampT * t) * exp(-dampR * r);
|
||||
feed = max(feed, ring * atten * uRippleIntensity);
|
||||
}
|
||||
}
|
||||
|
||||
float bayer = Bayer8(fragCoord / uPixelSize) - 0.5;
|
||||
float bw = step(0.5, feed + bayer);
|
||||
|
||||
float h = fract(sin(dot(floor(fragCoord / uPixelSize), vec2(127.1, 311.7))) * 43758.5453);
|
||||
float jitterScale = 1.0 + (h - 0.5) * uPixelJitter;
|
||||
float coverage = bw * jitterScale;
|
||||
float M;
|
||||
if (uShapeType == SHAPE_CIRCLE) M = maskCircle (pixelUV, coverage);
|
||||
else if (uShapeType == SHAPE_TRIANGLE) M = maskTriangle(pixelUV, pixelId, coverage);
|
||||
else if (uShapeType == SHAPE_DIAMOND) M = maskDiamond(pixelUV, coverage);
|
||||
else M = coverage;
|
||||
|
||||
if (uEdgeFade > 0.0) {
|
||||
vec2 norm = gl_FragCoord.xy / uResolution;
|
||||
float edge = min(min(norm.x, norm.y), min(1.0 - norm.x, 1.0 - norm.y));
|
||||
float fade = smoothstep(0.0, uEdgeFade, edge);
|
||||
M *= fade;
|
||||
}
|
||||
|
||||
vec3 color = uColor;
|
||||
fragColor = vec4(color, M);
|
||||
}
|
||||
`
|
||||
|
||||
const MAX_CLICKS = 10
|
||||
|
||||
const brandColor = useCssVar('--vp-c-brand-1', '#5086a1')
|
||||
|
||||
const color = computed(() => props.color || brandColor.value!)
|
||||
|
||||
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
|
||||
const visibilityRef = ref({ visible: true })
|
||||
const speedRef = ref(props.speed)
|
||||
|
||||
const threeRef = ref<{
|
||||
renderer: THREE.WebGLRenderer
|
||||
scene: THREE.Scene
|
||||
camera: THREE.OrthographicCamera
|
||||
material: THREE.ShaderMaterial
|
||||
clock: THREE.Clock
|
||||
clickIx: number
|
||||
uniforms: {
|
||||
uResolution: { value: THREE.Vector2 }
|
||||
uTime: { value: number }
|
||||
uColor: { value: THREE.Color }
|
||||
uClickPos: { value: THREE.Vector2[] }
|
||||
uClickTimes: { value: Float32Array }
|
||||
uShapeType: { value: number }
|
||||
uPixelSize: { value: number }
|
||||
uScale: { value: number }
|
||||
uDensity: { value: number }
|
||||
uPixelJitter: { value: number }
|
||||
uEnableRipples: { value: number }
|
||||
uRippleSpeed: { value: number }
|
||||
uRippleThickness: { value: number }
|
||||
uRippleIntensity: { value: number }
|
||||
uEdgeFade: { value: number }
|
||||
}
|
||||
resizeObserver?: ResizeObserver
|
||||
raf?: number
|
||||
quad?: THREE.Mesh<THREE.PlaneGeometry, THREE.ShaderMaterial>
|
||||
timeOffset?: number
|
||||
composer?: EffectComposer
|
||||
touch?: ReturnType<typeof createTouchTexture>
|
||||
liquidEffect?: Effect
|
||||
} | null>(null)
|
||||
|
||||
interface PixelBlastConfig {
|
||||
antialias: boolean
|
||||
liquid: boolean
|
||||
noiseAmount: number
|
||||
}
|
||||
const prevConfigRef = ref<PixelBlastConfig | null>(null)
|
||||
|
||||
let cleanup: (() => void) | null = null
|
||||
|
||||
function setup() {
|
||||
const container = containerRef.value
|
||||
if (!container)
|
||||
return
|
||||
speedRef.value = props.speed
|
||||
const needsReinitKeys: (keyof PixelBlastConfig)[] = ['antialias', 'liquid', 'noiseAmount']
|
||||
const cfg: PixelBlastConfig = {
|
||||
antialias: props.antialias,
|
||||
liquid: props.liquid,
|
||||
noiseAmount: props.noiseAmount,
|
||||
}
|
||||
let mustReinit = false
|
||||
if (!threeRef.value) {
|
||||
mustReinit = true
|
||||
}
|
||||
else if (prevConfigRef.value) {
|
||||
for (const k of needsReinitKeys) {
|
||||
if (prevConfigRef.value[k] !== cfg[k]) {
|
||||
mustReinit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mustReinit) {
|
||||
if (threeRef.value) {
|
||||
const t = threeRef.value
|
||||
t.resizeObserver?.disconnect()
|
||||
cancelAnimationFrame(t.raf!)
|
||||
t.quad?.geometry.dispose()
|
||||
t.material.dispose()
|
||||
t.composer?.dispose()
|
||||
t.renderer.dispose()
|
||||
if (t.renderer.domElement.parentElement === container)
|
||||
container.removeChild(t.renderer.domElement)
|
||||
threeRef.value = null
|
||||
}
|
||||
const canvas = document.createElement('canvas')
|
||||
const gl = canvas.getContext('webgl2', { antialias: props.antialias, alpha: true })
|
||||
if (!gl)
|
||||
return
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
canvas,
|
||||
context: gl as WebGL2RenderingContext,
|
||||
antialias: props.antialias,
|
||||
alpha: true,
|
||||
})
|
||||
renderer.domElement.style.width = '100%'
|
||||
renderer.domElement.style.height = '100%'
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
|
||||
container.appendChild(renderer.domElement)
|
||||
const uniforms = {
|
||||
uResolution: { value: new THREE.Vector2(0, 0) },
|
||||
uTime: { value: 0 },
|
||||
uColor: { value: new THREE.Color(color.value) },
|
||||
uClickPos: {
|
||||
value: Array.from({ length: MAX_CLICKS }, () => new THREE.Vector2(-1, -1)),
|
||||
},
|
||||
uClickTimes: { value: new Float32Array(MAX_CLICKS) },
|
||||
uShapeType: { value: SHAPE_MAP[props.variant] ?? 0 },
|
||||
uPixelSize: { value: props.pixelSize * renderer.getPixelRatio() },
|
||||
uScale: { value: props.patternScale },
|
||||
uDensity: { value: props.patternDensity },
|
||||
uPixelJitter: { value: props.pixelSizeJitter },
|
||||
uEnableRipples: { value: props.enableRipples ? 1 : 0 },
|
||||
uRippleSpeed: { value: props.rippleSpeed },
|
||||
uRippleThickness: { value: props.rippleThickness },
|
||||
uRippleIntensity: { value: props.rippleIntensityScale },
|
||||
uEdgeFade: { value: props.edgeFade },
|
||||
}
|
||||
const scene = new THREE.Scene()
|
||||
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)
|
||||
const material = new THREE.ShaderMaterial({
|
||||
vertexShader: VERTEX_SRC,
|
||||
fragmentShader: FRAGMENT_SRC,
|
||||
uniforms,
|
||||
transparent: true,
|
||||
glslVersion: THREE.GLSL3,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
})
|
||||
const quadGeom = new THREE.PlaneGeometry(2, 2)
|
||||
const quad = new THREE.Mesh(quadGeom, material)
|
||||
scene.add(quad)
|
||||
const clock = new THREE.Clock()
|
||||
const setSize = () => {
|
||||
const w = container.clientWidth || 1
|
||||
const h = container.clientHeight || 1
|
||||
renderer.setSize(w, h, false)
|
||||
uniforms.uResolution.value.set(renderer.domElement.width, renderer.domElement.height)
|
||||
if (threeRef.value?.composer)
|
||||
threeRef.value.composer.setSize(renderer.domElement.width, renderer.domElement.height)
|
||||
uniforms.uPixelSize.value = props.pixelSize * renderer.getPixelRatio()
|
||||
}
|
||||
setSize()
|
||||
const ro = new ResizeObserver(setSize)
|
||||
ro.observe(container)
|
||||
const randomFloat = () => {
|
||||
if (typeof window !== 'undefined' && window.crypto?.getRandomValues) {
|
||||
const u32 = new Uint32Array(1)
|
||||
window.crypto.getRandomValues(u32)
|
||||
return u32[0] / 0xFFFFFFFF
|
||||
}
|
||||
return Math.random()
|
||||
}
|
||||
const timeOffset = randomFloat() * 1000
|
||||
let composer: EffectComposer | undefined
|
||||
let touch: ReturnType<typeof createTouchTexture> | undefined
|
||||
let liquidEffect: Effect | undefined
|
||||
if (props.liquid) {
|
||||
touch = createTouchTexture()
|
||||
touch.radiusScale = props.liquidRadius
|
||||
composer = new EffectComposer(renderer)
|
||||
const renderPass = new RenderPass(scene, camera)
|
||||
liquidEffect = createLiquidEffect(touch.texture, {
|
||||
strength: props.liquidStrength,
|
||||
freq: props.liquidWobbleSpeed,
|
||||
})
|
||||
const effectPass = new EffectPass(camera, liquidEffect)
|
||||
effectPass.renderToScreen = true
|
||||
composer.addPass(renderPass)
|
||||
composer.addPass(effectPass)
|
||||
}
|
||||
if (props.noiseAmount > 0) {
|
||||
if (!composer) {
|
||||
composer = new EffectComposer(renderer)
|
||||
composer.addPass(new RenderPass(scene, camera))
|
||||
}
|
||||
const noiseEffect = new Effect(
|
||||
'NoiseEffect',
|
||||
`uniform float uTime; uniform float uAmount; float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453);} void mainUv(inout vec2 uv){} void mainImage(const in vec4 inputColor,const in vec2 uv,out vec4 outputColor){ float n=hash(floor(uv*vec2(1920.0,1080.0))+floor(uTime*60.0)); float g=(n-0.5)*uAmount; outputColor=inputColor+vec4(vec3(g),0.0);} `,
|
||||
{
|
||||
uniforms: new Map<string, THREE.Uniform>([
|
||||
['uTime', new THREE.Uniform(0)],
|
||||
['uAmount', new THREE.Uniform(props.noiseAmount)],
|
||||
]),
|
||||
},
|
||||
)
|
||||
const noisePass = new EffectPass(camera, noiseEffect)
|
||||
noisePass.renderToScreen = true
|
||||
if (composer && composer.passes.length > 0) {
|
||||
composer.passes.forEach((p) => {
|
||||
// EffectPass has renderToScreen; ensure we turn it off before adding a new final pass
|
||||
if ('renderToScreen' in p)
|
||||
(p as { renderToScreen?: boolean }).renderToScreen = false
|
||||
})
|
||||
}
|
||||
composer.addPass(noisePass)
|
||||
}
|
||||
if (composer)
|
||||
composer.setSize(renderer.domElement.width, renderer.domElement.height)
|
||||
const mapToPixels = (e: PointerEvent) => {
|
||||
const rect = renderer.domElement.getBoundingClientRect()
|
||||
const scaleX = renderer.domElement.width / rect.width
|
||||
const scaleY = renderer.domElement.height / rect.height
|
||||
const fx = (e.clientX - rect.left) * scaleX
|
||||
const fy = (rect.height - (e.clientY - rect.top)) * scaleY
|
||||
return {
|
||||
fx,
|
||||
fy,
|
||||
w: renderer.domElement.width,
|
||||
h: renderer.domElement.height,
|
||||
}
|
||||
}
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
const { fx, fy } = mapToPixels(e)
|
||||
const ix = threeRef.value?.clickIx ?? 0
|
||||
uniforms.uClickPos.value[ix].set(fx, fy)
|
||||
uniforms.uClickTimes.value[ix] = uniforms.uTime.value
|
||||
if (threeRef.value)
|
||||
threeRef.value.clickIx = (ix + 1) % MAX_CLICKS
|
||||
}
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
if (!touch)
|
||||
return
|
||||
const { fx, fy, w, h } = mapToPixels(e)
|
||||
touch.addTouch({ x: fx / w, y: fy / h })
|
||||
}
|
||||
renderer.domElement.addEventListener('pointerdown', onPointerDown, {
|
||||
passive: true,
|
||||
})
|
||||
renderer.domElement.addEventListener('pointermove', onPointerMove, {
|
||||
passive: true,
|
||||
})
|
||||
let raf = 0
|
||||
const animate = () => {
|
||||
if (props.autoPauseOffscreen && !visibilityRef.value.visible) {
|
||||
raf = requestAnimationFrame(animate)
|
||||
return
|
||||
}
|
||||
uniforms.uTime.value = timeOffset + clock.getElapsedTime() * speedRef.value
|
||||
if (liquidEffect)
|
||||
liquidEffect.uniforms.get('uTime')!.value = uniforms.uTime.value
|
||||
if (composer) {
|
||||
if (touch)
|
||||
touch.update()
|
||||
composer.passes.forEach((p) => {
|
||||
if (p instanceof EffectPass) {
|
||||
const effs = (p as unknown as { effects?: Effect[] }).effects
|
||||
effs?.forEach((eff) => {
|
||||
const u = eff.uniforms.get('uTime')
|
||||
if (u)
|
||||
u.value = uniforms.uTime.value
|
||||
})
|
||||
}
|
||||
})
|
||||
composer.render()
|
||||
}
|
||||
else {
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
raf = requestAnimationFrame(animate)
|
||||
}
|
||||
raf = requestAnimationFrame(animate)
|
||||
threeRef.value = {
|
||||
renderer,
|
||||
scene,
|
||||
camera,
|
||||
material,
|
||||
clock,
|
||||
clickIx: 0,
|
||||
uniforms,
|
||||
resizeObserver: ro,
|
||||
raf,
|
||||
quad,
|
||||
timeOffset,
|
||||
composer,
|
||||
touch,
|
||||
liquidEffect,
|
||||
}
|
||||
}
|
||||
else {
|
||||
const t = threeRef.value!
|
||||
t.uniforms.uShapeType.value = SHAPE_MAP[props.variant] ?? 0
|
||||
t.uniforms.uPixelSize.value = props.pixelSize * t.renderer.getPixelRatio()
|
||||
t.uniforms.uColor.value.set(color.value)
|
||||
t.uniforms.uScale.value = props.patternScale
|
||||
t.uniforms.uDensity.value = props.patternDensity
|
||||
t.uniforms.uPixelJitter.value = props.pixelSizeJitter
|
||||
t.uniforms.uEnableRipples.value = props.enableRipples ? 1 : 0
|
||||
t.uniforms.uRippleIntensity.value = props.rippleIntensityScale
|
||||
t.uniforms.uRippleThickness.value = props.rippleThickness
|
||||
t.uniforms.uRippleSpeed.value = props.rippleSpeed
|
||||
t.uniforms.uEdgeFade.value = props.edgeFade
|
||||
if (props.transparent)
|
||||
t.renderer.setClearAlpha(0)
|
||||
else t.renderer.setClearColor(0x000000, 1)
|
||||
if (t.liquidEffect) {
|
||||
const uStrength = t.liquidEffect?.uniforms.get('uStrength')
|
||||
if (uStrength)
|
||||
uStrength.value = props.liquidStrength
|
||||
const uFreq = t.liquidEffect?.uniforms.get('uFreq')
|
||||
if (uFreq)
|
||||
uFreq.value = props.liquidWobbleSpeed
|
||||
}
|
||||
if (t.touch)
|
||||
t.touch.radiusScale = props.liquidRadius
|
||||
}
|
||||
prevConfigRef.value = cfg
|
||||
|
||||
cleanup = () => {
|
||||
if (threeRef.value && mustReinit)
|
||||
return
|
||||
if (!threeRef.value)
|
||||
return
|
||||
const t = threeRef.value
|
||||
t.resizeObserver?.disconnect()
|
||||
cancelAnimationFrame(t.raf!)
|
||||
t.quad?.geometry.dispose()
|
||||
t.material.dispose()
|
||||
t.composer?.dispose()
|
||||
t.renderer.dispose()
|
||||
if (t.renderer.domElement.parentElement === container)
|
||||
container.removeChild(t.renderer.domElement)
|
||||
threeRef.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setup()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanup?.()
|
||||
})
|
||||
|
||||
watch(
|
||||
props,
|
||||
() => {
|
||||
cleanup?.()
|
||||
setup()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="home-hero-effect-pixel-blast" :class="[className]"
|
||||
:style="style"
|
||||
aria-label="PixelBlast interactive background"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-hero-effect-pixel-blast {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
</style>
|
||||
474
theme/src/client/components/background/Prism.vue
Normal file
@ -0,0 +1,474 @@
|
||||
<!--
|
||||
***************************************************
|
||||
fork from https://github.com/DavidHDev/vue-bits MIT License
|
||||
modified by pengzhanbo
|
||||
***************************************************
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type { ThemeHomeHeroPrism } from '../../../shared/index.js'
|
||||
import { Mesh, Program, Renderer, Triangle } from 'ogl'
|
||||
import { onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<ThemeHomeHeroPrism>(), {
|
||||
height: 3.5,
|
||||
baseWidth: 5.5,
|
||||
animationType: 'rotate',
|
||||
glow: 1,
|
||||
offset: () => ({ x: 0, y: 0 }),
|
||||
noise: 0,
|
||||
transparent: true,
|
||||
scale: 3.6,
|
||||
hueShift: 0,
|
||||
colorFrequency: 1,
|
||||
hoverStrength: 2,
|
||||
inertia: 0.05,
|
||||
bloom: 1,
|
||||
suspendWhenOffscreen: true,
|
||||
timeScale: 0.5,
|
||||
})
|
||||
|
||||
const containerRef = useTemplateRef('containerRef')
|
||||
|
||||
let cleanup: (() => void) | null = null
|
||||
|
||||
function setup() {
|
||||
const container = containerRef.value
|
||||
if (!container)
|
||||
return
|
||||
|
||||
const H = Math.max(0.001, props.height)
|
||||
const BW = Math.max(0.001, props.baseWidth)
|
||||
const BASE_HALF = BW * 0.5
|
||||
const GLOW = Math.max(0.0, props.glow)
|
||||
const NOISE = Math.max(0.0, props.noise)
|
||||
const offX = props.offset?.x ?? 0
|
||||
const offY = props.offset?.y ?? 0
|
||||
const SAT = props.transparent ? 1.5 : 1
|
||||
const SCALE = Math.max(0.001, props.scale)
|
||||
const HUE = props.hueShift || 0
|
||||
const CFREQ = Math.max(0.0, props.colorFrequency || 1)
|
||||
const BLOOM = Math.max(0.0, props.bloom || 1)
|
||||
const RSX = 1
|
||||
const RSY = 1
|
||||
const RSZ = 1
|
||||
const TS = Math.max(0, props.timeScale || 1)
|
||||
const HOVSTR = Math.max(0, props.hoverStrength || 1)
|
||||
const INERT = Math.max(0, Math.min(1, props.inertia || 0.12))
|
||||
|
||||
const dpr = Math.min(2, window.devicePixelRatio || 1)
|
||||
const renderer = new Renderer({
|
||||
dpr,
|
||||
alpha: props.transparent,
|
||||
antialias: false,
|
||||
})
|
||||
const gl = renderer.gl
|
||||
gl.disable(gl.DEPTH_TEST)
|
||||
gl.disable(gl.CULL_FACE)
|
||||
gl.disable(gl.BLEND)
|
||||
|
||||
Object.assign(gl.canvas.style, {
|
||||
position: 'absolute',
|
||||
inset: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'block',
|
||||
} as Partial<CSSStyleDeclaration>)
|
||||
container.appendChild(gl.canvas)
|
||||
|
||||
const vertex = /* glsl */ `
|
||||
attribute vec2 position;
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`
|
||||
|
||||
const fragment = /* glsl */ `
|
||||
precision highp float;
|
||||
|
||||
uniform vec2 iResolution;
|
||||
uniform float iTime;
|
||||
|
||||
uniform float uHeight;
|
||||
uniform float uBaseHalf;
|
||||
uniform mat3 uRot;
|
||||
uniform int uUseBaseWobble;
|
||||
uniform float uGlow;
|
||||
uniform vec2 uOffsetPx;
|
||||
uniform float uNoise;
|
||||
uniform float uSaturation;
|
||||
uniform float uScale;
|
||||
uniform float uHueShift;
|
||||
uniform float uColorFreq;
|
||||
uniform float uBloom;
|
||||
uniform float uCenterShift;
|
||||
uniform float uInvBaseHalf;
|
||||
uniform float uInvHeight;
|
||||
uniform float uMinAxis;
|
||||
uniform float uPxScale;
|
||||
uniform float uTimeScale;
|
||||
|
||||
vec4 tanh4(vec4 x){
|
||||
vec4 e2x = exp(2.0*x);
|
||||
return (e2x - 1.0) / (e2x + 1.0);
|
||||
}
|
||||
|
||||
float rand(vec2 co){
|
||||
return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453123);
|
||||
}
|
||||
|
||||
float sdOctaAnisoInv(vec3 p){
|
||||
vec3 q = vec3(abs(p.x) * uInvBaseHalf, abs(p.y) * uInvHeight, abs(p.z) * uInvBaseHalf);
|
||||
float m = q.x + q.y + q.z - 1.0;
|
||||
return m * uMinAxis * 0.5773502691896258;
|
||||
}
|
||||
|
||||
float sdPyramidUpInv(vec3 p){
|
||||
float oct = sdOctaAnisoInv(p);
|
||||
float halfSpace = -p.y;
|
||||
return max(oct, halfSpace);
|
||||
}
|
||||
|
||||
mat3 hueRotation(float a){
|
||||
float c = cos(a), s = sin(a);
|
||||
mat3 W = mat3(
|
||||
0.299, 0.587, 0.114,
|
||||
0.299, 0.587, 0.114,
|
||||
0.299, 0.587, 0.114
|
||||
);
|
||||
mat3 U = mat3(
|
||||
0.701, -0.587, -0.114,
|
||||
-0.299, 0.413, -0.114,
|
||||
-0.300, -0.588, 0.886
|
||||
);
|
||||
mat3 V = mat3(
|
||||
0.168, -0.331, 0.500,
|
||||
0.328, 0.035, -0.500,
|
||||
-0.497, 0.296, 0.201
|
||||
);
|
||||
return W + U * c + V * s;
|
||||
}
|
||||
|
||||
void main(){
|
||||
vec2 f = (gl_FragCoord.xy - 0.5 * iResolution.xy - uOffsetPx) * uPxScale;
|
||||
|
||||
float z = 5.0;
|
||||
float d = 0.0;
|
||||
|
||||
vec3 p;
|
||||
vec4 o = vec4(0.0);
|
||||
|
||||
float centerShift = uCenterShift;
|
||||
float cf = uColorFreq;
|
||||
|
||||
mat2 wob = mat2(1.0);
|
||||
if (uUseBaseWobble == 1) {
|
||||
float t = iTime * uTimeScale;
|
||||
float c0 = cos(t + 0.0);
|
||||
float c1 = cos(t + 33.0);
|
||||
float c2 = cos(t + 11.0);
|
||||
wob = mat2(c0, c1, c2, c0);
|
||||
}
|
||||
|
||||
const int STEPS = 100;
|
||||
for (int i = 0; i < STEPS; i++) {
|
||||
p = vec3(f, z);
|
||||
p.xz = p.xz * wob;
|
||||
p = uRot * p;
|
||||
vec3 q = p;
|
||||
q.y += centerShift;
|
||||
d = 0.1 + 0.2 * abs(sdPyramidUpInv(q));
|
||||
z -= d;
|
||||
o += (sin((p.y + z) * cf + vec4(0.0, 1.0, 2.0, 3.0)) + 1.0) / d;
|
||||
}
|
||||
|
||||
o = tanh4(o * o * (uGlow * uBloom) / 1e5);
|
||||
|
||||
vec3 col = o.rgb;
|
||||
float n = rand(gl_FragCoord.xy + vec2(iTime));
|
||||
col += (n - 0.5) * uNoise;
|
||||
col = clamp(col, 0.0, 1.0);
|
||||
|
||||
float L = dot(col, vec3(0.2126, 0.7152, 0.0722));
|
||||
col = clamp(mix(vec3(L), col, uSaturation), 0.0, 1.0);
|
||||
|
||||
if(abs(uHueShift) > 0.0001){
|
||||
col = clamp(hueRotation(uHueShift) * col, 0.0, 1.0);
|
||||
}
|
||||
|
||||
gl_FragColor = vec4(col, o.a);
|
||||
}
|
||||
`
|
||||
|
||||
const geometry = new Triangle(gl)
|
||||
const iResBuf = new Float32Array(2)
|
||||
const offsetPxBuf = new Float32Array(2)
|
||||
|
||||
const program = new Program(gl, {
|
||||
vertex,
|
||||
fragment,
|
||||
uniforms: {
|
||||
iResolution: { value: iResBuf },
|
||||
iTime: { value: 0 },
|
||||
uHeight: { value: H },
|
||||
uBaseHalf: { value: BASE_HALF },
|
||||
uUseBaseWobble: { value: 1 },
|
||||
uRot: { value: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]) },
|
||||
uGlow: { value: GLOW },
|
||||
uOffsetPx: { value: offsetPxBuf },
|
||||
uNoise: { value: NOISE },
|
||||
uSaturation: { value: SAT },
|
||||
uScale: { value: SCALE },
|
||||
uHueShift: { value: HUE },
|
||||
uColorFreq: { value: CFREQ },
|
||||
uBloom: { value: BLOOM },
|
||||
uCenterShift: { value: H * 0.25 },
|
||||
uInvBaseHalf: { value: 1 / BASE_HALF },
|
||||
uInvHeight: { value: 1 / H },
|
||||
uMinAxis: { value: Math.min(BASE_HALF, H) },
|
||||
uPxScale: {
|
||||
value: 1 / ((gl.drawingBufferHeight || 1) * 0.1 * SCALE),
|
||||
},
|
||||
uTimeScale: { value: TS },
|
||||
},
|
||||
})
|
||||
const mesh = new Mesh(gl, { geometry, program })
|
||||
|
||||
const resize = () => {
|
||||
const w = container.clientWidth || 1
|
||||
const h = container.clientHeight || 1
|
||||
renderer.setSize(w, h)
|
||||
iResBuf[0] = gl.drawingBufferWidth
|
||||
iResBuf[1] = gl.drawingBufferHeight
|
||||
offsetPxBuf[0] = offX * dpr
|
||||
offsetPxBuf[1] = offY * dpr
|
||||
program.uniforms.uPxScale.value = 1 / ((gl.drawingBufferHeight || 1) * 0.1 * SCALE)
|
||||
}
|
||||
const ro = new ResizeObserver(resize)
|
||||
ro.observe(container)
|
||||
resize()
|
||||
|
||||
const rotBuf = new Float32Array(9)
|
||||
const setMat3FromEuler = (yawY: number, pitchX: number, rollZ: number, out: Float32Array) => {
|
||||
const cy = Math.cos(yawY)
|
||||
const sy = Math.sin(yawY)
|
||||
const cx = Math.cos(pitchX)
|
||||
const sx = Math.sin(pitchX)
|
||||
const cz = Math.cos(rollZ)
|
||||
const sz = Math.sin(rollZ)
|
||||
const r00 = cy * cz + sy * sx * sz
|
||||
const r01 = -cy * sz + sy * sx * cz
|
||||
const r02 = sy * cx
|
||||
|
||||
const r10 = cx * sz
|
||||
const r11 = cx * cz
|
||||
const r12 = -sx
|
||||
|
||||
const r20 = -sy * cz + cy * sx * sz
|
||||
const r21 = sy * sz + cy * sx * cz
|
||||
const r22 = cy * cx
|
||||
|
||||
out[0] = r00
|
||||
out[1] = r10
|
||||
out[2] = r20
|
||||
out[3] = r01
|
||||
out[4] = r11
|
||||
out[5] = r21
|
||||
out[6] = r02
|
||||
out[7] = r12
|
||||
out[8] = r22
|
||||
return out
|
||||
}
|
||||
|
||||
const NOISE_IS_ZERO = NOISE < 1e-6
|
||||
let raf = 0
|
||||
const t0 = performance.now()
|
||||
const startRAF = () => {
|
||||
if (raf)
|
||||
return
|
||||
// eslint-disable-next-line ts/no-use-before-define
|
||||
raf = requestAnimationFrame(render)
|
||||
}
|
||||
const stopRAF = () => {
|
||||
if (!raf)
|
||||
return
|
||||
cancelAnimationFrame(raf)
|
||||
raf = 0
|
||||
}
|
||||
|
||||
const rnd = () => Math.random()
|
||||
const wX = (0.3 + rnd() * 0.6) * RSX
|
||||
const wY = (0.2 + rnd() * 0.7) * RSY
|
||||
const wZ = (0.1 + rnd() * 0.5) * RSZ
|
||||
const phX = rnd() * Math.PI * 2
|
||||
const phZ = rnd() * Math.PI * 2
|
||||
|
||||
let yaw = 0
|
||||
let pitch = 0
|
||||
let roll = 0
|
||||
let targetYaw = 0
|
||||
let targetPitch = 0
|
||||
const lerp = (a: number, b: number, t: number) => a + (b - a) * t
|
||||
|
||||
const pointer = { x: 0, y: 0, inside: true }
|
||||
const onMove = (e: PointerEvent) => {
|
||||
const ww = Math.max(1, window.innerWidth)
|
||||
const wh = Math.max(1, window.innerHeight)
|
||||
const cx = ww * 0.5
|
||||
const cy = wh * 0.5
|
||||
const nx = (e.clientX - cx) / (ww * 0.5)
|
||||
const ny = (e.clientY - cy) / (wh * 0.5)
|
||||
pointer.x = Math.max(-1, Math.min(1, nx))
|
||||
pointer.y = Math.max(-1, Math.min(1, ny))
|
||||
pointer.inside = true
|
||||
}
|
||||
const onLeave = () => {
|
||||
pointer.inside = false
|
||||
}
|
||||
const onBlur = () => {
|
||||
pointer.inside = false
|
||||
}
|
||||
|
||||
let onPointerMove: ((e: PointerEvent) => void) | null = null
|
||||
if (props.animationType === 'hover') {
|
||||
onPointerMove = (e: PointerEvent) => {
|
||||
onMove(e)
|
||||
startRAF()
|
||||
}
|
||||
window.addEventListener('pointermove', onPointerMove, { passive: true })
|
||||
window.addEventListener('mouseleave', onLeave)
|
||||
window.addEventListener('blur', onBlur)
|
||||
program.uniforms.uUseBaseWobble.value = 0
|
||||
}
|
||||
else if (props.animationType === '3drotate') {
|
||||
program.uniforms.uUseBaseWobble.value = 0
|
||||
}
|
||||
else {
|
||||
program.uniforms.uUseBaseWobble.value = 1
|
||||
}
|
||||
|
||||
const render = (t: number) => {
|
||||
const time = (t - t0) * 0.001
|
||||
program.uniforms.iTime.value = time
|
||||
|
||||
let continueRAF = true
|
||||
|
||||
if (props.animationType === 'hover') {
|
||||
const maxPitch = 0.6 * HOVSTR
|
||||
const maxYaw = 0.6 * HOVSTR
|
||||
targetYaw = (pointer.inside ? -pointer.x : 0) * maxYaw
|
||||
targetPitch = (pointer.inside ? pointer.y : 0) * maxPitch
|
||||
const prevYaw = yaw
|
||||
const prevPitch = pitch
|
||||
const prevRoll = roll
|
||||
yaw = lerp(prevYaw, targetYaw, INERT)
|
||||
pitch = lerp(prevPitch, targetPitch, INERT)
|
||||
roll = lerp(prevRoll, 0, 0.1)
|
||||
program.uniforms.uRot.value = setMat3FromEuler(yaw, pitch, roll, rotBuf)
|
||||
|
||||
if (NOISE_IS_ZERO) {
|
||||
const settled
|
||||
= Math.abs(yaw - targetYaw) < 1e-4 && Math.abs(pitch - targetPitch) < 1e-4 && Math.abs(roll) < 1e-4
|
||||
if (settled)
|
||||
continueRAF = false
|
||||
}
|
||||
}
|
||||
else if (props.animationType === '3drotate') {
|
||||
const tScaled = time * TS
|
||||
yaw = tScaled * wY
|
||||
pitch = Math.sin(tScaled * wX + phX) * 0.6
|
||||
roll = Math.sin(tScaled * wZ + phZ) * 0.5
|
||||
program.uniforms.uRot.value = setMat3FromEuler(yaw, pitch, roll, rotBuf)
|
||||
if (TS < 1e-6)
|
||||
continueRAF = false
|
||||
}
|
||||
else {
|
||||
rotBuf[0] = 1
|
||||
rotBuf[1] = 0
|
||||
rotBuf[2] = 0
|
||||
rotBuf[3] = 0
|
||||
rotBuf[4] = 1
|
||||
rotBuf[5] = 0
|
||||
rotBuf[6] = 0
|
||||
rotBuf[7] = 0
|
||||
rotBuf[8] = 1
|
||||
program.uniforms.uRot.value = rotBuf
|
||||
if (TS < 1e-6)
|
||||
continueRAF = false
|
||||
}
|
||||
|
||||
renderer.render({ scene: mesh })
|
||||
if (continueRAF) {
|
||||
raf = requestAnimationFrame(render)
|
||||
}
|
||||
else {
|
||||
raf = 0
|
||||
}
|
||||
}
|
||||
|
||||
if (props.suspendWhenOffscreen) {
|
||||
const io = new IntersectionObserver((entries) => {
|
||||
const vis = entries.some(e => e.isIntersecting)
|
||||
if (vis)
|
||||
startRAF()
|
||||
else stopRAF()
|
||||
})
|
||||
io.observe(container)
|
||||
startRAF();
|
||||
(container as HTMLElement & { __prismIO?: IntersectionObserver }).__prismIO = io
|
||||
}
|
||||
else {
|
||||
startRAF()
|
||||
}
|
||||
|
||||
cleanup = () => {
|
||||
stopRAF()
|
||||
ro.disconnect()
|
||||
if (props.animationType === 'hover') {
|
||||
if (onPointerMove)
|
||||
window.removeEventListener('pointermove', onPointerMove as EventListener)
|
||||
window.removeEventListener('mouseleave', onLeave)
|
||||
window.removeEventListener('blur', onBlur)
|
||||
}
|
||||
if (props.suspendWhenOffscreen) {
|
||||
const io = (container as HTMLElement & { __prismIO?: IntersectionObserver }).__prismIO as
|
||||
| IntersectionObserver
|
||||
| undefined
|
||||
if (io)
|
||||
io.disconnect()
|
||||
delete (container as HTMLElement & { __prismIO?: IntersectionObserver }).__prismIO
|
||||
}
|
||||
if (gl.canvas.parentElement === container)
|
||||
container.removeChild(gl.canvas)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setup()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanup?.()
|
||||
})
|
||||
|
||||
watch(
|
||||
props,
|
||||
() => {
|
||||
cleanup?.()
|
||||
setup()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="home-hero-effect-prism" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-hero-effect-prism {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
</style>
|
||||
193
theme/src/client/components/background/TintPlate.vue
Normal file
@ -0,0 +1,193 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, useTemplateRef } from 'vue'
|
||||
import { useDarkMode } from '../../composables/index.js'
|
||||
|
||||
interface TintPlate {
|
||||
r: { value: number, offset: number }
|
||||
g: { value: number, offset: number }
|
||||
b: { value: number, offset: number }
|
||||
}
|
||||
|
||||
const { rgb, light, dark, r, g, b } = defineProps<{
|
||||
rgb?: string | number
|
||||
} & Partial<TintPlate> & { light?: TintPlate, dark?: TintPlate }>()
|
||||
|
||||
const config = computed(() => {
|
||||
if (rgb) {
|
||||
return rgb
|
||||
}
|
||||
if (light || dark) {
|
||||
return { light, dark }
|
||||
}
|
||||
return { r, g, b }
|
||||
})
|
||||
|
||||
const lightTint = {
|
||||
r: { value: 200, offset: 36 },
|
||||
g: { value: 200, offset: 36 },
|
||||
b: { value: 200, offset: 36 },
|
||||
}
|
||||
|
||||
const darkTint = {
|
||||
r: { value: 32, offset: 36 },
|
||||
g: { value: 32, offset: 36 },
|
||||
b: { value: 32, offset: 36 },
|
||||
}
|
||||
|
||||
function toPlate(plate: number | string) {
|
||||
return typeof plate === 'number' || Number(plate) === Number.parseInt(plate)
|
||||
? [plate, plate, plate].map(n => Number(n))
|
||||
: plate.includes(',') ? plate.replace(/\s/g, '').split(',').map(n => Number(n)) : []
|
||||
}
|
||||
|
||||
function toTint([r, g, b]: number[]) {
|
||||
return { r: toColor(r), g: toColor(g), b: toColor(b) }
|
||||
}
|
||||
|
||||
function toColor(num: number) {
|
||||
const offset = 256 - num
|
||||
return { value: num, offset: offset > 64 ? 64 : offset }
|
||||
}
|
||||
|
||||
function toNumber(tint: TintPlate): TintPlate {
|
||||
Object.keys(tint).forEach((key) => {
|
||||
const p = tint[key]
|
||||
p.value = Number(p.value)
|
||||
p.offset = Number(p.offset)
|
||||
})
|
||||
return tint
|
||||
}
|
||||
|
||||
const canvas = useTemplateRef<HTMLCanvasElement>('canvas')
|
||||
const isDark = useDarkMode()
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null
|
||||
let t = 0
|
||||
let timer: number
|
||||
|
||||
const plate = computed<TintPlate>(() => {
|
||||
const defaultTint = isDark.value ? darkTint : lightTint
|
||||
const plate = config.value
|
||||
if (!plate)
|
||||
return defaultTint
|
||||
|
||||
if (typeof plate === 'string' || typeof plate === 'number') {
|
||||
if (isDark.value)
|
||||
return darkTint
|
||||
const values = toPlate(plate)
|
||||
return values.length !== 3 ? lightTint : toTint(values)
|
||||
}
|
||||
|
||||
if (typeof plate === 'object') {
|
||||
if ('r' in plate) {
|
||||
if (isDark.value)
|
||||
return darkTint
|
||||
return toNumber({ ...lightTint, ...(plate as TintPlate) })
|
||||
}
|
||||
const key = isDark.value ? 'dark' : 'light'
|
||||
if (key in plate) {
|
||||
const _plate = plate[key]
|
||||
if (typeof _plate === 'string' || typeof _plate === 'number') {
|
||||
const values = toPlate(_plate)
|
||||
return values.length !== 3 ? lightTint : toTint(values)
|
||||
}
|
||||
return toNumber({ ...defaultTint, ...plate })
|
||||
}
|
||||
}
|
||||
return defaultTint
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (canvas.value) {
|
||||
ctx = canvas.value.getContext('2d')!
|
||||
if (timer) {
|
||||
window.cancelAnimationFrame(timer)
|
||||
}
|
||||
run()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
window.cancelAnimationFrame(timer)
|
||||
}
|
||||
})
|
||||
|
||||
function run() {
|
||||
for (let x = 0; x <= 35; x++) {
|
||||
for (let y = 0; y <= 35; y++)
|
||||
col(x, y, R(x, y, t), G(x, y, t), B(x, y, t))
|
||||
}
|
||||
t = t + 0.020
|
||||
timer = window.requestAnimationFrame(run)
|
||||
}
|
||||
|
||||
function col(x: number, y: number, r: number, g: number, b: number) {
|
||||
if (!ctx)
|
||||
return
|
||||
ctx.fillStyle = `rgb(${r},${g},${b})`
|
||||
ctx.fillRect(x, y, 1, 1)
|
||||
}
|
||||
|
||||
function R(x: number, y: number, t: number) {
|
||||
const r = plate.value.r
|
||||
return (Math.floor(r.value + r.offset * Math.cos((x * x - y * y) / 300 + t)))
|
||||
}
|
||||
|
||||
function G(x: number, y: number, t: number) {
|
||||
const g = plate.value.g
|
||||
return (Math.floor(g.value + g.offset * Math.sin((x * x * Math.cos(t / 4) + y * y * Math.sin(t / 3)) / 300)))
|
||||
}
|
||||
|
||||
function B(x: number, y: number, t: number) {
|
||||
const b = plate.value.b
|
||||
return (Math.floor(b.value + b.offset * Math.sin(5 * Math.sin(t / 9) + ((x - 100) * (x - 100) + (y - 100) * (y - 100)) / 1100)))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-filter">
|
||||
<canvas ref="canvas" width="32" height="32" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bg-filter {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.vp-home-hero.full.once .bg-filter {
|
||||
height: calc(100% + var(--vp-footer-height, 0px));
|
||||
}
|
||||
|
||||
@property --vp-home-hero-bg-filter {
|
||||
inherits: false;
|
||||
initial-value: #fff;
|
||||
syntax: "<color>";
|
||||
}
|
||||
|
||||
.bg-filter::after {
|
||||
--vp-home-hero-bg-filter: var(--vp-c-bg);
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: "";
|
||||
background: linear-gradient(to bottom, var(--vp-home-hero-bg-filter) 0, transparent 45%, transparent 55%, var(--vp-home-hero-bg-filter) 140%);
|
||||
transition: --vp-home-hero-bg-filter var(--vp-t-color);
|
||||
}
|
||||
|
||||
.bg-filter canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
31
theme/src/client/composables/css-var.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { computed, type ComputedRef, type MaybeRefOrGetter, shallowRef, toValue, watch } from 'vue'
|
||||
import { useDarkMode } from './dark-mode.js'
|
||||
|
||||
/**
|
||||
* Get css variable
|
||||
* @param prop css variable name
|
||||
* @param initialValue
|
||||
*/
|
||||
export function useCssVar(
|
||||
prop: MaybeRefOrGetter<string | null | undefined>,
|
||||
initialValue = '',
|
||||
): ComputedRef<string | null | undefined> {
|
||||
const isDark = useDarkMode()
|
||||
const variable = shallowRef(initialValue)
|
||||
|
||||
function updateCssVar() {
|
||||
const _window = typeof window ? window : null
|
||||
const target = _window?.document.documentElement
|
||||
const key = toValue(prop)
|
||||
if (target && key) {
|
||||
const value = _window.getComputedStyle(target).getPropertyValue(key)?.trim()
|
||||
variable.value = value || variable.value || initialValue
|
||||
}
|
||||
}
|
||||
|
||||
watch([isDark, () => toValue(prop)], () => {
|
||||
updateCssVar()
|
||||
}, { immediate: true, flush: 'post' })
|
||||
|
||||
return computed(() => variable.value)
|
||||
}
|
||||
@ -1,137 +0,0 @@
|
||||
import type { Ref } from 'vue'
|
||||
import type { ThemeHomeHero } from '../../shared/index.js'
|
||||
import { computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useDarkMode } from './dark-mode.js'
|
||||
|
||||
export interface TintPlate {
|
||||
r: { value: number, offset: number }
|
||||
g: { value: number, offset: number }
|
||||
b: { value: number, offset: number }
|
||||
}
|
||||
|
||||
const lightTint = {
|
||||
r: { value: 200, offset: 36 },
|
||||
g: { value: 200, offset: 36 },
|
||||
b: { value: 200, offset: 36 },
|
||||
}
|
||||
|
||||
const darkTint = {
|
||||
r: { value: 32, offset: 36 },
|
||||
g: { value: 32, offset: 36 },
|
||||
b: { value: 32, offset: 36 },
|
||||
}
|
||||
|
||||
export function useHomeHeroTintPlate(
|
||||
canvas: Ref<HTMLCanvasElement | undefined>,
|
||||
enable: Ref<boolean>,
|
||||
tintPlate: Ref<ThemeHomeHero['tintPlate']>,
|
||||
): void {
|
||||
const isDark = useDarkMode()
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null
|
||||
let t = 0
|
||||
let timer: number
|
||||
|
||||
const plate = computed<TintPlate>(() => {
|
||||
const defaultTint = isDark.value ? darkTint : lightTint
|
||||
if (!tintPlate.value)
|
||||
return defaultTint
|
||||
|
||||
const plate = tintPlate.value
|
||||
if (typeof plate === 'string' || typeof plate === 'number') {
|
||||
if (isDark.value)
|
||||
return darkTint
|
||||
const values = toPlate(plate)
|
||||
return values.length !== 3 ? lightTint : toTint(values)
|
||||
}
|
||||
|
||||
if (typeof plate === 'object') {
|
||||
if ('r' in plate) {
|
||||
if (isDark.value)
|
||||
return darkTint
|
||||
return toNumber({ ...lightTint, ...plate })
|
||||
}
|
||||
const key = isDark.value ? 'dark' : 'light'
|
||||
if (key in plate) {
|
||||
const _plate = plate[key]
|
||||
if (typeof _plate === 'string' || typeof _plate === 'number') {
|
||||
const values = toPlate(_plate)
|
||||
return values.length !== 3 ? lightTint : toTint(values)
|
||||
}
|
||||
return toNumber({ ...defaultTint, ...plate })
|
||||
}
|
||||
}
|
||||
return defaultTint
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (canvas.value && enable.value) {
|
||||
ctx = canvas.value.getContext('2d')!
|
||||
if (timer) {
|
||||
window.cancelAnimationFrame(timer)
|
||||
}
|
||||
run()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
window.cancelAnimationFrame(timer)
|
||||
}
|
||||
})
|
||||
|
||||
function run() {
|
||||
for (let x = 0; x <= 35; x++) {
|
||||
for (let y = 0; y <= 35; y++)
|
||||
col(x, y, R(x, y, t), G(x, y, t), B(x, y, t))
|
||||
}
|
||||
t = t + 0.020
|
||||
timer = window.requestAnimationFrame(run)
|
||||
}
|
||||
|
||||
function col(x: number, y: number, r: number, g: number, b: number) {
|
||||
if (!ctx)
|
||||
return
|
||||
ctx.fillStyle = `rgb(${r},${g},${b})`
|
||||
ctx.fillRect(x, y, 1, 1)
|
||||
}
|
||||
|
||||
function R(x: number, y: number, t: number) {
|
||||
const r = plate.value.r
|
||||
return (Math.floor(r.value + r.offset * Math.cos((x * x - y * y) / 300 + t)))
|
||||
}
|
||||
|
||||
function G(x: number, y: number, t: number) {
|
||||
const g = plate.value.g
|
||||
return (Math.floor(g.value + g.offset * Math.sin((x * x * Math.cos(t / 4) + y * y * Math.sin(t / 3)) / 300)))
|
||||
}
|
||||
|
||||
function B(x: number, y: number, t: number) {
|
||||
const b = plate.value.b
|
||||
return (Math.floor(b.value + b.offset * Math.sin(5 * Math.sin(t / 9) + ((x - 100) * (x - 100) + (y - 100) * (y - 100)) / 1100)))
|
||||
}
|
||||
}
|
||||
|
||||
function toPlate(plate: number | string) {
|
||||
return typeof plate === 'number' || Number(plate) === Number.parseInt(plate)
|
||||
? [plate, plate, plate].map(n => Number(n))
|
||||
: plate.includes(',') ? plate.replace(/\s/g, '').split(',').map(n => Number(n)) : []
|
||||
}
|
||||
|
||||
function toTint([r, g, b]: number[]) {
|
||||
return { r: toColor(r), g: toColor(g), b: toColor(b) }
|
||||
}
|
||||
|
||||
function toColor(num: number) {
|
||||
const offset = 256 - num
|
||||
return { value: num, offset: offset > 64 ? 64 : offset }
|
||||
}
|
||||
|
||||
function toNumber(tint: TintPlate): TintPlate {
|
||||
Object.keys(tint).forEach((key) => {
|
||||
const p = tint[key]
|
||||
p.value = Number(p.value)
|
||||
p.offset = Number(p.offset)
|
||||
})
|
||||
return tint
|
||||
}
|
||||
@ -3,13 +3,13 @@ export * from './bulletin.js'
|
||||
export * from './collections.js'
|
||||
export * from './contributors.js'
|
||||
export * from './copyright.js'
|
||||
export * from './css-var.js'
|
||||
export * from './dark-mode.js'
|
||||
export * from './data.js'
|
||||
export * from './edit-link.js'
|
||||
export * from './encrypt-data.js'
|
||||
export * from './encrypt.js'
|
||||
export * from './flyout.js'
|
||||
export * from './home.js'
|
||||
export * from './icons.js'
|
||||
export * from './internal-link.js'
|
||||
export * from './langs.js'
|
||||
|
||||
12
theme/src/client/shim.d.ts
vendored
@ -80,6 +80,18 @@ declare module '@internal/iconify' {
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@internal/home-hero-effects' {
|
||||
import type { ComponentOptions } from 'vue'
|
||||
|
||||
const effectComponents: Record<string, ComponentOptions>
|
||||
const effects: string[]
|
||||
|
||||
export {
|
||||
effectComponents,
|
||||
effects,
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'swiper/css' {
|
||||
const res: any
|
||||
export default res
|
||||
|
||||
@ -45,3 +45,9 @@ export function numToUnit(value?: string | number): string {
|
||||
}
|
||||
return value as string
|
||||
}
|
||||
|
||||
const gradient: string[] = ['linear-gradient', 'radial-gradient', 'repeating-linear-gradient', 'repeating-radial-gradient', 'conic-gradient']
|
||||
|
||||
export function isGradient(value: string): boolean {
|
||||
return gradient.some(v => value.startsWith(v))
|
||||
}
|
||||
|
||||
@ -31,4 +31,24 @@ export function extendsBundlerOptions(bundlerOptions: any, app: App): void {
|
||||
addViteOptimizeDepsInclude(bundlerOptions, app, ['swiper/modules', 'swiper/vue'])
|
||||
addViteSsrNoExternal(bundlerOptions, app, ['swiper'])
|
||||
}
|
||||
|
||||
if (isPackageExists('three')) {
|
||||
addViteOptimizeDepsInclude(bundlerOptions, app, ['three', 'three/src/math/MathUtils.js'])
|
||||
addViteSsrNoExternal(bundlerOptions, app, ['three', 'three/src/math/MathUtils.js'])
|
||||
}
|
||||
|
||||
if (isPackageExists('gsap')) {
|
||||
addViteOptimizeDepsInclude(bundlerOptions, app, ['gsap', 'gsap/InertiaPlugin'])
|
||||
addViteSsrNoExternal(bundlerOptions, app, ['gsap', 'gsap/InertiaPlugin'])
|
||||
}
|
||||
|
||||
if (isPackageExists('postprocessing')) {
|
||||
addViteOptimizeDepsInclude(bundlerOptions, app, ['postprocessing'])
|
||||
addViteSsrNoExternal(bundlerOptions, app, ['postprocessing'])
|
||||
}
|
||||
|
||||
if (isPackageExists('ogl')) {
|
||||
addViteOptimizeDepsInclude(bundlerOptions, app, ['ogl'])
|
||||
addViteSsrNoExternal(bundlerOptions, app, ['ogl'])
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { perf } from '../utils/index.js'
|
||||
import { prepareArticleTagColors } from './prepareArticleTagColor.js'
|
||||
import { prepareCollections } from './prepareCollections.js'
|
||||
import { prepareEncrypt } from './prepareEncrypt.js'
|
||||
import { prepareHomeHeroEffects } from './prepareHomeHeroEffects.js'
|
||||
import { prepareIcons } from './prepareIcons.js'
|
||||
import { preparedPostsData } from './preparePostsData.js'
|
||||
import { prepareSidebar } from './prepareSidebar.js'
|
||||
@ -18,6 +19,7 @@ export async function prepareData(app: App): Promise<void> {
|
||||
prepareCollections(app),
|
||||
prepareEncrypt(app),
|
||||
prepareIcons(app),
|
||||
prepareHomeHeroEffects(app),
|
||||
])
|
||||
|
||||
perf.log('prepare:data')
|
||||
|
||||
129
theme/src/node/prepare/prepareHomeHeroEffects.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import type { App } from 'vuepress'
|
||||
import type { ThemeHomeConfig } from '../../shared/index.js'
|
||||
import { isEmptyObject, uniq } from '@pengzhanbo/utils'
|
||||
import { isPackageExists } from 'local-pkg'
|
||||
import { getUserAgent, resolveCommand } from 'package-manager-detector'
|
||||
import { colors } from 'vuepress/utils'
|
||||
import { createTranslate, logger, perf, writeTemp } from '../utils/index.js'
|
||||
|
||||
const effectDeps: Record<string, string[]> = {
|
||||
'prism': ['ogl'],
|
||||
'pixel-blast': ['three', 'postprocessing'],
|
||||
'hyper-speed': ['three', 'postprocessing'],
|
||||
'liquid-ether': ['three'],
|
||||
'dot-grid': ['gsap'],
|
||||
'iridescence': ['ogl'],
|
||||
'orb': ['ogl'],
|
||||
'beams': ['three'],
|
||||
}
|
||||
|
||||
const effectMapping: Record<string, string> = {
|
||||
'tint-plate': 'TintPlate',
|
||||
'prism': 'Prism',
|
||||
'pixel-blast': 'PixelBlast',
|
||||
'hyper-speed': 'HyperSpeed',
|
||||
'liquid-ether': 'LiquidEther',
|
||||
'dot-grid': 'DotGrid',
|
||||
'iridescence': 'Iridescence',
|
||||
'orb': 'Orb',
|
||||
'beams': 'Beams',
|
||||
'lightning': 'Lightning',
|
||||
}
|
||||
|
||||
const allEffects = Object.keys(effectMapping)
|
||||
|
||||
const t = createTranslate({
|
||||
en: {
|
||||
unknown: `[Home hero background effect] Unknown effect: {{ effect }}`,
|
||||
uninstall: `[Home hero background effect] The following effect is missing necessary dependencies: {{ deps }}`,
|
||||
install: `Run the installation command: {{ command }}`,
|
||||
},
|
||||
zh: {
|
||||
unknown: `[首页 hero 背景效果] 未知的 effect: {{ effect }}`,
|
||||
uninstall: `[首页 hero 背景效果] 以下效果缺少必要依赖: {{ deps }}`,
|
||||
install: `运行安装命令: {{ command }}`,
|
||||
},
|
||||
})
|
||||
|
||||
export async function prepareHomeHeroEffects(app: App): Promise<void> {
|
||||
perf.mark('prepare:home-hero-effects')
|
||||
|
||||
const { effects, unknownEffects } = getEffectsByFrontmatter(app)
|
||||
|
||||
if (unknownEffects.length)
|
||||
logger.warn(t('unknown', { effect: colors.cyan(unknownEffects.join(', ')) }))
|
||||
|
||||
await writeToInternalTemp(app, effects)
|
||||
detectMissingDeps(effects)
|
||||
|
||||
perf.log('prepare:home-hero-effects')
|
||||
}
|
||||
|
||||
async function writeToInternalTemp(app: App, effects: string[]) {
|
||||
let imports: string = ''
|
||||
let exports: string = 'export const effectComponents = {\n'
|
||||
|
||||
for (const effect of effects) {
|
||||
const component = effectMapping[effect]
|
||||
imports += `import ${component} from '@theme/background/${component}.vue'\n`
|
||||
exports += ` '${effect}': ${component},\n`
|
||||
}
|
||||
exports += '}\n\nexport const effects = Object.keys(effectComponents)\n'
|
||||
const content = `${imports}\n${exports}`
|
||||
|
||||
await writeTemp(app, 'internal/home-hero-effects.js', content)
|
||||
}
|
||||
|
||||
function getEffectsByFrontmatter(app: App) {
|
||||
const effects: string[] = []
|
||||
const unknownEffects: string[] = []
|
||||
|
||||
for (const page of app.pages) {
|
||||
const fm = page.frontmatter
|
||||
const config = fm.config as ThemeHomeConfig[]
|
||||
if (!(fm.home || fm.pageLayout === 'home') || config?.length === 0)
|
||||
continue
|
||||
|
||||
for (const item of config) {
|
||||
if (item.type === 'hero') {
|
||||
const effect = item.effect
|
||||
if (effect) {
|
||||
if (allEffects.includes(effect)) {
|
||||
effects.push(effect)
|
||||
}
|
||||
else {
|
||||
unknownEffects.push(effect)
|
||||
}
|
||||
}
|
||||
// compatibility
|
||||
if (item.background) {
|
||||
if (allEffects.includes(item.background))
|
||||
effects.push(item.background)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { effects: uniq(effects), unknownEffects: uniq(unknownEffects) }
|
||||
}
|
||||
|
||||
function detectMissingDeps(effects: string[]) {
|
||||
const missingDeps: Record<string, string[]> = {}
|
||||
for (const effect of effects) {
|
||||
const deps = effectDeps[effect]
|
||||
if (deps?.length) {
|
||||
const uninstall = deps.filter(dep => !isPackageExists(dep))
|
||||
if (uninstall.length)
|
||||
missingDeps[effect] = uninstall
|
||||
}
|
||||
}
|
||||
if (isEmptyObject(missingDeps))
|
||||
return
|
||||
const dependencies = uniq(Object.values(missingDeps).flat())
|
||||
|
||||
logger.warn(t('uninstall', { deps: colors.bold(JSON.stringify(missingDeps)) }))
|
||||
const agent = getUserAgent()
|
||||
if (agent) {
|
||||
const { command = '', args = [] } = resolveCommand(agent, 'add', dependencies) || {}
|
||||
logger.info(t('install', { command: colors.cyan(`${command} ${args.join(' ')}`) }))
|
||||
}
|
||||
}
|
||||
@ -31,3 +31,5 @@ export interface ThemeBadge {
|
||||
bgColor?: string
|
||||
borderColor?: string
|
||||
}
|
||||
|
||||
export type ThemeLightDark<T> = T | { light?: T, dark?: T }
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import type { ThemeImage } from '../common/index.js'
|
||||
import type { ThemeImage, ThemeLightDark } from '../common/index.js'
|
||||
import type { LiteralUnion } from '../utils.js'
|
||||
import type { ThemeHomeHeroEffect, ThemeHomeHeroEffectConfig, ThemeHomeHeroTintPlate } from './homeHeroEffects.js'
|
||||
import type { ThemeNormalFrontmatter } from './normal.js'
|
||||
|
||||
export interface ThemeHomeFrontmatter extends ThemeNormalFrontmatter, Omit<ThemeHomeBanner, 'type'> {
|
||||
@ -9,6 +11,15 @@ export interface ThemeHomeFrontmatter extends ThemeNormalFrontmatter, Omit<Theme
|
||||
|
||||
export type ThemeHomeConfig = ThemeHomeBanner | ThemeHomeTextImage | ThemeHomeFeatures | ThemeHomeProfile | ThemeHomeHero | ThemeHomePosts
|
||||
|
||||
export interface ThemeHomeConfigBase {
|
||||
type: 'banner' | 'hero' | 'doc-hero' | 'text-image' | 'image-text' | 'features' | 'profile' | 'custom' | 'posts'
|
||||
full?: boolean
|
||||
backgroundImage?: ThemeLightDark<string>
|
||||
backgroundAttachment?: 'fixed' | 'local'
|
||||
onlyOnce?: boolean
|
||||
index: number
|
||||
}
|
||||
|
||||
export interface ThemeHero {
|
||||
name: string
|
||||
tagline?: string
|
||||
@ -16,10 +27,6 @@ export interface ThemeHero {
|
||||
actions: ThemeHeroAction[]
|
||||
}
|
||||
|
||||
export interface ThemeDocHero extends ThemeHero {
|
||||
image?: ThemeImage
|
||||
}
|
||||
|
||||
export interface ThemeHeroAction {
|
||||
theme?: 'brand' | 'alt'
|
||||
text: string
|
||||
@ -30,38 +37,29 @@ export interface ThemeHeroAction {
|
||||
suffixIcon?: string
|
||||
}
|
||||
|
||||
export interface ThemeHomeConfigBase {
|
||||
type: 'banner' | 'hero' | 'doc-hero' | 'text-image' | 'image-text' | 'features' | 'profile' | 'custom' | 'posts'
|
||||
full?: boolean
|
||||
backgroundImage?: string | { light: string, dark: string }
|
||||
backgroundAttachment?: 'fixed' | 'local'
|
||||
onlyOnce?: boolean
|
||||
export interface ThemeDocHero extends ThemeHero {
|
||||
image?: ThemeImage
|
||||
}
|
||||
|
||||
export interface ThemeHomeBanner extends Pick<ThemeHomeConfigBase, 'type' | 'onlyOnce' | 'full'> {
|
||||
type: 'banner'
|
||||
banner?: string
|
||||
bannerMask?: number | { light?: number, dark?: number }
|
||||
bannerMask?: ThemeLightDark<number>
|
||||
hero: ThemeHero
|
||||
}
|
||||
|
||||
export interface PlumeThemeHomeHeroTintPlate {
|
||||
r: { value: number, offset: number }
|
||||
g: { value: number, offset: number }
|
||||
b: { value: number, offset: number }
|
||||
}
|
||||
|
||||
export interface ThemeHomeHero extends ThemeHomeConfigBase {
|
||||
type: 'hero'
|
||||
hero: ThemeHero
|
||||
full?: boolean
|
||||
background?: 'tint-plate' | (string & { zz_IGNORE?: never })
|
||||
tintPlate?:
|
||||
| string | number
|
||||
| { light?: string | number, dark?: string | number }
|
||||
| PlumeThemeHomeHeroTintPlate
|
||||
| { light?: PlumeThemeHomeHeroTintPlate, dark?: PlumeThemeHomeHeroTintPlate }
|
||||
/** @deprecated use `effect` instead */
|
||||
background?: LiteralUnion<'tint-plate'>
|
||||
/** @deprecated use `effectConfig` instead */
|
||||
tintPlate?: ThemeHomeHeroTintPlate
|
||||
effect?: ThemeHomeHeroEffect
|
||||
effectConfig?: ThemeHomeHeroEffectConfig
|
||||
filter?: string
|
||||
forceDark?: boolean
|
||||
}
|
||||
|
||||
export interface ThemeHomeDocHero extends ThemeHomeConfigBase {
|
||||
|
||||
559
theme/src/shared/frontmatter/homeHeroEffects.ts
Normal file
@ -0,0 +1,559 @@
|
||||
import type * as THREE from 'three'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type { ThemeLightDark } from '../common/index.js'
|
||||
import type { LiteralUnion } from '../utils.js'
|
||||
|
||||
export type ThemeHomeHeroEffect = LiteralUnion<'tint-plate' | 'prism' | 'pixel-blast' | 'hyper-speed' | 'liquid-ether' | 'dot-grid' | 'iridescence' | 'orb' | 'beams' | 'lightning'>
|
||||
|
||||
export type ThemeHomeHeroEffectConfig
|
||||
= | ThemeHomeHeroTintPlate
|
||||
| ThemeHomeHeroPrism
|
||||
| ThemeHomeHeroPixelPixelBlast
|
||||
| Omit<ThemeHomeHeroHyperSpeed, 'onSpeedUp' | 'onSlowDown'>
|
||||
| ThemeHomeHeroLiquidEther
|
||||
| ThemeHomeHeroDotGrid
|
||||
| ThemeHomeHeroIridescence
|
||||
| ThemeHomeHeroOrb
|
||||
| ThemeHomeHeroBeams
|
||||
| ThemeHomeHeroLightning
|
||||
|
||||
export type ThemeHomeHeroTintPlate = ThemeLightDark<{ rgb: string | number } | {
|
||||
r: { value: number, offset: number }
|
||||
g: { value: number, offset: number }
|
||||
b: { value: number, offset: number }
|
||||
}>
|
||||
|
||||
export interface ThemeHomeHeroPrism {
|
||||
/**
|
||||
* Apex height of the prism (world units)
|
||||
* 棱柱的顶点高度(世界单位)
|
||||
*/
|
||||
height?: number
|
||||
/**
|
||||
* Total base width across X/Z (world units).
|
||||
* X/Z轴总基准宽度(世界单位)
|
||||
*/
|
||||
baseWidth?: number
|
||||
/**
|
||||
* Animation mode: shader wobble, pointer hover tilt, or full 3D rotation.
|
||||
* 动画模式:着色器摆动、指针悬停倾斜或完全三维旋转。
|
||||
*/
|
||||
animationType?: 'rotate' | 'hover' | '3drotate'
|
||||
/**
|
||||
* Glow/bleed intensity multiplier.
|
||||
* 发光/溢出强度乘数。
|
||||
*/
|
||||
glow?: number
|
||||
/**
|
||||
* Pixel offset within the canvas (x→right, y→down).
|
||||
* 画布内的像素偏移(x→向右,y→向下)。
|
||||
*/
|
||||
offset?: { x?: number, y?: number }
|
||||
/**
|
||||
* Film-grain noise amount added to final color (0 disables).
|
||||
* 最终颜色中添加的颗粒噪声量(0表示禁用)。
|
||||
*/
|
||||
noise?: number
|
||||
/**
|
||||
* Whether the canvas has an alpha channel (transparent background).
|
||||
* 画布是否具有Alpha通道(透明背景)。
|
||||
*/
|
||||
transparent?: boolean
|
||||
/**
|
||||
* Overall screen-space scale of the prism (bigger = larger).
|
||||
* 棱镜的整体屏幕空间比例(数值越大,显示尺寸越大)。
|
||||
*/
|
||||
scale?: number
|
||||
/**
|
||||
* Hue rotation (radians) applied to final color.
|
||||
* 色调旋转(弧度)应用于最终颜色。
|
||||
*/
|
||||
hueShift?: number
|
||||
/**
|
||||
* Frequency of internal sine bands controlling color variation.
|
||||
* 控制颜色变化的内部正弦带频率。
|
||||
*/
|
||||
colorFrequency?: number
|
||||
/**
|
||||
* Sensitivity of hover tilt (pitch/yaw amplitude).
|
||||
* 悬停倾斜(俯仰/偏航幅度)灵敏度。
|
||||
*/
|
||||
hoverStrength?: number
|
||||
/**
|
||||
* Easing factor for hover (0..1, higher = snappier).
|
||||
* 悬停缓动因子(0到1,数值越大响应越灵敏)。
|
||||
*/
|
||||
inertia?: number
|
||||
/**
|
||||
* Extra bloom factor layered on top of glow.
|
||||
* 在光泽之上叠加额外的绽放效果层次。
|
||||
*/
|
||||
bloom?: number
|
||||
/**
|
||||
* Pause rendering when the element is not in the viewport.
|
||||
* 当元素不在视口内时暂停渲染。
|
||||
*/
|
||||
suspendWhenOffscreen?: boolean
|
||||
/**
|
||||
* Global time multiplier for animations (0=frozen, 1=normal).
|
||||
* 动画全局时间倍率(0=冻结,1=正常)。
|
||||
*/
|
||||
timeScale?: number
|
||||
}
|
||||
|
||||
export type ThemeHomeHeroPixelBlastVariant = 'square' | 'circle' | 'triangle' | 'diamond'
|
||||
|
||||
export interface ThemeHomeHeroPixelPixelBlast {
|
||||
/**
|
||||
* Pixel shape variant
|
||||
* 像素形状变体
|
||||
*/
|
||||
variant?: ThemeHomeHeroPixelBlastVariant
|
||||
/**
|
||||
* Base pixel size (auto scaled for DPI).
|
||||
* 基础像素尺寸(根据DPI自动缩放)。
|
||||
*/
|
||||
pixelSize?: number
|
||||
/**
|
||||
* Pixel color.
|
||||
* 像素颜色
|
||||
*/
|
||||
color?: string
|
||||
/**
|
||||
* Additional CSS class.
|
||||
* 附加CSS类
|
||||
*/
|
||||
className?: string
|
||||
/**
|
||||
* Additional CSS style.
|
||||
* 附加CSS样式
|
||||
*/
|
||||
style?: CSSProperties
|
||||
/**
|
||||
* Enable antialiasing.
|
||||
* 启用抗锯齿。
|
||||
*/
|
||||
antialias?: boolean
|
||||
/**
|
||||
* Noise/pattern scale.
|
||||
* 噪声/图案比例。
|
||||
*/
|
||||
patternScale?: number
|
||||
/**
|
||||
* Pattern density adjustment.
|
||||
* 图案密度调整。
|
||||
*/
|
||||
patternDensity?: number
|
||||
/**
|
||||
* Enable liquid distortion effect.
|
||||
* 启用液体扭曲效果。
|
||||
*/
|
||||
liquid?: boolean
|
||||
/**
|
||||
* Liquid distortion strength.
|
||||
* 液体扭曲强度。
|
||||
*/
|
||||
liquidStrength?: number
|
||||
/**
|
||||
* Liquid touch brush radius scale.
|
||||
* 液体触摸笔刷半径比例。
|
||||
*/
|
||||
liquidRadius?: number
|
||||
/**
|
||||
* Random jitter applied to coverage.
|
||||
* 应用于覆盖范围的随机抖动。
|
||||
*/
|
||||
pixelSizeJitter?: number
|
||||
/**
|
||||
* Enable click ripple waves.
|
||||
* 启用点击涟漪效果。
|
||||
*/
|
||||
enableRipples?: boolean
|
||||
/**
|
||||
* Ripple intensity multiplier.
|
||||
* 纹波强度乘数。
|
||||
*/
|
||||
rippleIntensityScale?: number
|
||||
/**
|
||||
* Ripple ring thickness.
|
||||
* 纹波环厚度。
|
||||
*/
|
||||
rippleThickness?: number
|
||||
/**
|
||||
* Ripple propagation speed.
|
||||
* 纹波传播速度。
|
||||
*/
|
||||
rippleSpeed?: number
|
||||
/**
|
||||
* Liquid wobble frequency.
|
||||
* 液体晃动频率。
|
||||
*/
|
||||
liquidWobbleSpeed?: number
|
||||
/**
|
||||
* Enable auto-pausing when offscreen.
|
||||
*
|
||||
* 离屏时自动暂停
|
||||
*/
|
||||
autoPauseOffscreen?: boolean
|
||||
/**
|
||||
* Animation time scale.
|
||||
* 动画时间缩放比例。
|
||||
*/
|
||||
speed?: number
|
||||
/**
|
||||
* Transparent background.
|
||||
* 透明背景。
|
||||
*/
|
||||
transparent?: boolean
|
||||
/**
|
||||
* Edge fade distance (0-1).
|
||||
* 边缘淡入距离(0-1)。
|
||||
*/
|
||||
edgeFade?: number
|
||||
/**
|
||||
* Post noise amount.
|
||||
* 后处理噪声量。
|
||||
*/
|
||||
noiseAmount?: number
|
||||
backgroundImage?: ThemeLightDark<string>
|
||||
backgroundAttachment?: 'fixed' | 'local'
|
||||
}
|
||||
|
||||
export interface ThemeHomeHeroHyperSpeedDistortion {
|
||||
uniforms: Record<string, { value: unknown }>
|
||||
getDistortion: string
|
||||
getJS?: (progress: number, time: number) => THREE.Vector3
|
||||
}
|
||||
|
||||
export interface ThemeHomeHeroHyperSpeedDistortions {
|
||||
[key: string]: ThemeHomeHeroHyperSpeedDistortion
|
||||
}
|
||||
|
||||
export interface ThemeHomeHeroHyperSpeedColors {
|
||||
roadColor: number
|
||||
islandColor: number
|
||||
background: number
|
||||
shoulderLines: number
|
||||
brokenLines: number
|
||||
leftCars: number[]
|
||||
rightCars: number[]
|
||||
sticks: number
|
||||
}
|
||||
|
||||
export interface ThemeHomeHeroHyperSpeed {
|
||||
onSpeedUp?: (ev: MouseEvent) => void
|
||||
onSlowDown?: (ev: MouseEvent) => void
|
||||
distortion?: string | ThemeHomeHeroHyperSpeedDistortion
|
||||
length: number
|
||||
roadWidth: number
|
||||
islandWidth: number
|
||||
lanesPerRoad: number
|
||||
fov: number
|
||||
fovSpeedUp: number
|
||||
speedUp: number
|
||||
carLightsFade: number
|
||||
totalSideLightSticks: number
|
||||
lightPairsPerRoadWay: number
|
||||
shoulderLinesWidthPercentage: number
|
||||
brokenLinesWidthPercentage: number
|
||||
brokenLinesLengthPercentage: number
|
||||
lightStickWidth: [number, number]
|
||||
lightStickHeight: [number, number]
|
||||
movingAwaySpeed: [number, number]
|
||||
movingCloserSpeed: [number, number]
|
||||
carLightsLength: [number, number]
|
||||
carLightsRadius: [number, number]
|
||||
carWidthPercentage: [number, number]
|
||||
carShiftX: [number, number]
|
||||
carFloorSeparation: [number, number]
|
||||
colors: ThemeHomeHeroHyperSpeedColors
|
||||
isHyper?: boolean
|
||||
}
|
||||
|
||||
export interface ThemeHomeHeroLiquidEther {
|
||||
/**
|
||||
* Strength multiplier applied to mouse / touch movement when injecting velocity.
|
||||
* 鼠标/触摸移动注入速度时的强度系数。
|
||||
*/
|
||||
mouseForce?: number
|
||||
/**
|
||||
* Radius (in pixels at base resolution) of the force brush.
|
||||
* 力刷半径(以基本分辨率的像素为单位)。
|
||||
*/
|
||||
cursorSize?: number
|
||||
/**
|
||||
* Toggle iterative viscosity solve (smoother, thicker motion when enabled).
|
||||
* 启用迭代性质量解决方案(更平滑,更粗糙的运动)。
|
||||
*/
|
||||
isViscous?: boolean
|
||||
/**
|
||||
* Viscosity coefficient used when isViscous is true.
|
||||
* 当 isViscous 为 true 时使用的粘性系数。
|
||||
*/
|
||||
viscous?: number
|
||||
/**
|
||||
* Number of Gauss-Seidel iterations for viscosity (higher = smoother, slower).
|
||||
* 粘性的高斯-塞德尔迭代次数(值越大 = 更平滑,更慢)。
|
||||
*/
|
||||
iterationsViscous?: number
|
||||
/**
|
||||
* Number of pressure Poisson iterations to enforce incompressibility.
|
||||
* 用于确保不可压缩性的压力泊松迭代次数。
|
||||
*/
|
||||
iterationsPoisson?: number
|
||||
/**
|
||||
* Fixed simulation timestep used inside the advection / diffusion passes.
|
||||
* 内部的对流/扩散迭代中使用固定的模拟时间步长。
|
||||
*/
|
||||
dt?: number
|
||||
/**
|
||||
* Enable BFECC advection (error-compensated) for crisper flow; disable for slight performance gain.
|
||||
* 启用 BFECC 传输(错误补偿)以获得更清晰的流动,禁用以获得稍微的性能提升。
|
||||
*/
|
||||
BFECC?: boolean
|
||||
/**
|
||||
* Simulation texture scale relative to canvas size (lower = better performance, more blur).
|
||||
* 相对于画布大小的仿真纹理缩放(值越小,更好的性能,更模糊)。
|
||||
*/
|
||||
resolution?: number
|
||||
/**
|
||||
* If true, shows bounce boundaries (velocity clamped at edges).
|
||||
* 如果为 true,显示弹跳边界(速度在边缘上限)。
|
||||
*/
|
||||
isBounce?: boolean
|
||||
/**
|
||||
* Array of hex color stops used to build the velocity-to-color palette.
|
||||
* 用于构建速度-颜色映射调色板的十六进制颜色停止点数组。
|
||||
*/
|
||||
colors?: string[]
|
||||
/**
|
||||
* Inline styles applied to the root container.
|
||||
* 应用于根容器的内联样式。
|
||||
*/
|
||||
style?: Record<string, any>
|
||||
/**
|
||||
* Optional class for the root container.
|
||||
* 根容器的可选类。
|
||||
*/
|
||||
className?: string
|
||||
/**
|
||||
* Enable idle auto-driving of the pointer when no user interaction.
|
||||
* 启用无用户交互时的自动驾驶指针。
|
||||
*/
|
||||
autoDemo?: boolean
|
||||
/**
|
||||
* Speed (normalized units/sec) for auto pointer motion.
|
||||
* 自动指针运动的速度(标准化单位/秒)。
|
||||
*/
|
||||
autoSpeed?: number
|
||||
/**
|
||||
* Multiplier applied to velocity delta while in auto mode.
|
||||
* 在自动模式下应用于速度增量的乘数。
|
||||
*/
|
||||
autoIntensity?: number
|
||||
/**
|
||||
* Seconds to interpolate from auto pointer to real cursor when user moves mouse.
|
||||
* 在用户移动鼠标时从自动指针插值到实际光标的秒数。
|
||||
*/
|
||||
takeoverDuration?: number
|
||||
/**
|
||||
* Milliseconds of inactivity before auto mode resumes.
|
||||
* 在自动模式恢复之前的不活动时间(毫秒)。
|
||||
*/
|
||||
autoResumeDelay?: number
|
||||
/**
|
||||
* Seconds to ramp auto movement speed from 0 to full after activation.
|
||||
* 在激活后从 0 开始加速自动移动速度的秒数。
|
||||
*/
|
||||
autoRampDuration?: number
|
||||
}
|
||||
|
||||
export interface ThemeHomeHeroDotGrid {
|
||||
/**
|
||||
* Size of each dot in pixels.
|
||||
* 每个点的尺寸(像素)。
|
||||
*/
|
||||
dotSize?: number
|
||||
/**
|
||||
* Gap between each dot in pixels.
|
||||
* 每个点之间的间隙(像素)。
|
||||
*/
|
||||
gap?: number
|
||||
/**
|
||||
* Base color of the dots.
|
||||
* 点的基本颜色。
|
||||
*/
|
||||
baseColor?: string
|
||||
/**
|
||||
* Color of dots when hovered or activated.
|
||||
* 鼠标悬停或激活时点的颜色。
|
||||
*/
|
||||
activeColor?: string
|
||||
/**
|
||||
* Radius around the mouse pointer within which dots react.
|
||||
* 鼠标指针周围的半径,在此范围内点会响应。
|
||||
*/
|
||||
proximity?: number
|
||||
/**
|
||||
* Mouse speed threshold to trigger inertia effect.
|
||||
* 触发惯性效果的鼠标速度阈值。
|
||||
*/
|
||||
speedTrigger?: number
|
||||
/**
|
||||
* Radius of the shockwave effect on click.
|
||||
* 点击时的震动波半径。
|
||||
*/
|
||||
shockRadius?: number
|
||||
/**
|
||||
* Strength of the shockwave effect on click.
|
||||
* 点击时震动波的强度。
|
||||
*/
|
||||
shockStrength?: number
|
||||
/**
|
||||
* Maximum speed for inertia calculation.
|
||||
* 惯性计算的最大速度。
|
||||
*/
|
||||
maxSpeed?: number
|
||||
/**
|
||||
* Resistance for the inertia effect.
|
||||
* 惯性效果的阻力。
|
||||
*/
|
||||
resistance?: number
|
||||
/**
|
||||
* Duration for dots to return to their original position after inertia.
|
||||
* 惯性后点返回原始位置的持续时间。
|
||||
*/
|
||||
returnDuration?: number
|
||||
/**
|
||||
* Additional CSS classes for the component.
|
||||
* 为组件添加附加 CSS 类。
|
||||
*/
|
||||
className?: string
|
||||
/**
|
||||
* Inline styles for the component.
|
||||
* 为组件应用内联样式。
|
||||
*/
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface ThemeHomeHeroIridescence {
|
||||
/**
|
||||
* Base color as an array of RGB values (each between 0 and 1).
|
||||
* 基色以RGB值数组形式表示(每个数值范围在0到1之间)。
|
||||
*/
|
||||
color?: ThemeLightDark<readonly [number, number, number]>
|
||||
/**
|
||||
* Speed multiplier for the animation.
|
||||
* 动画的速度乘数。
|
||||
*/
|
||||
speed?: number
|
||||
/**
|
||||
* Amplitude for the mouse-driven effect.
|
||||
* 鼠标驱动效果的振幅。
|
||||
*/
|
||||
amplitude?: number
|
||||
/**
|
||||
* Enable or disable mouse interaction with the shader.
|
||||
* 启用或禁用鼠标与着色器的交互。
|
||||
*/
|
||||
mouseReact?: boolean
|
||||
}
|
||||
|
||||
export interface ThemeHomeHeroOrb {
|
||||
/**
|
||||
* The base hue for the orb (in degrees).
|
||||
* 球的基本色调(度)。
|
||||
*/
|
||||
hue?: number
|
||||
/**
|
||||
* Controls the intensity of the hover distortion effect.
|
||||
* 控制悬停扭曲效果的强度。
|
||||
*/
|
||||
hoverIntensity?: number
|
||||
/**
|
||||
* Toggle to enable or disable continuous rotation on hover.
|
||||
* 启用或禁用悬停时的持续旋转。
|
||||
*/
|
||||
rotateOnHover?: boolean
|
||||
/**
|
||||
* Force hover animations even when the orb is not actually hovered.
|
||||
* 即使没有悬停,也强制启用悬停动画。
|
||||
*/
|
||||
forceHoverState?: boolean
|
||||
/**
|
||||
* Additional CSS classes for the component.
|
||||
* 为组件添加附加 CSS 类。
|
||||
*/
|
||||
className?: string
|
||||
}
|
||||
|
||||
export interface ThemeHomeHeroBeams {
|
||||
/**
|
||||
* Width of each beam.
|
||||
* 每个激光束的宽度。
|
||||
*/
|
||||
beamWidth?: number
|
||||
/**
|
||||
* Height of each beam.
|
||||
* 每个激光束的高度。
|
||||
*/
|
||||
beamHeight?: number
|
||||
/**
|
||||
* Number of beams to display.
|
||||
* 要显示的激光束数量。
|
||||
*/
|
||||
beamNumber?: number
|
||||
/**
|
||||
* Color of the directional light.
|
||||
* 方向光的颜色。
|
||||
*/
|
||||
lightColor?: ThemeLightDark<string>
|
||||
/**
|
||||
* Speed of the animation.
|
||||
* 动画的速度。
|
||||
*/
|
||||
speed?: number
|
||||
/**
|
||||
* Intensity of the noise effect overlay.
|
||||
* 噪音效果的强度。
|
||||
*/
|
||||
noiseIntensity?: number
|
||||
/**
|
||||
* Scale of the noise pattern.
|
||||
* 噪音模式的缩放比例。
|
||||
*/
|
||||
scale?: number
|
||||
/**
|
||||
* Rotation of the entire beams system in degrees.
|
||||
* 整个激光束系统的旋转角度(度)。
|
||||
*/
|
||||
rotation?: number
|
||||
}
|
||||
|
||||
export interface ThemeHomeHeroLightning {
|
||||
/**
|
||||
* Hue of the lightning in degrees (0 to 360).
|
||||
* 光束的色调(度)(0到360)。
|
||||
*/
|
||||
hue?: number
|
||||
/**
|
||||
* Horizontal offset of the lightning in normalized units.
|
||||
* 光束的水平偏移量(标准化单位)。
|
||||
*/
|
||||
xOffset?: number
|
||||
/**
|
||||
* Animation speed multiplier for the lightning.
|
||||
* 光束的动画速度乘数。
|
||||
*/
|
||||
speed?: number
|
||||
/**
|
||||
* Brightness multiplier for the lightning.
|
||||
* 光束的亮度乘数。
|
||||
*/
|
||||
intensity?: number
|
||||
/**
|
||||
* Scale factor for the bolt size.
|
||||
* 光束的缩放因子。
|
||||
*/
|
||||
size?: number
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
export * from './friends.js'
|
||||
export * from './home.js'
|
||||
export * from './homeHeroEffects.js'
|
||||
export * from './page.js'
|
||||
export * from './post.js'
|
||||
|
||||