feat(theme): add home hero effects (#738)

This commit is contained in:
pengzhanbo 2025-10-25 11:55:17 +08:00 committed by GitHub
parent 51e1f5260c
commit aa6168c31d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 9337 additions and 440 deletions

View File

@ -82,7 +82,7 @@ export default defineUserConfig({
// provider: 'algolia',
// appId: '',
// apiKey: '',
// indexName: '',
// indices: [''],
// },
/**

View File

@ -5,7 +5,7 @@ config:
-
type: hero
full: true
background: tint-plate
effect: lightning
hero:
name: Theme Plume
tagline: VuePress Next Theme

View File

@ -5,7 +5,7 @@ config:
-
type: hero
full: true
background: tint-plate
effect: lightning
hero:
name: Theme Plume
tagline: VuePress Next Theme

View File

@ -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',

View File

@ -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',

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 KiB

View File

@ -5,7 +5,8 @@ config:
-
type: hero
full: true
background: tint-plate
effect: lightning
forceDark: true
hero:
name: Theme Plume
tagline: VuePress Next Theme

View File

@ -4,7 +4,8 @@ config:
-
type: hero
full: true
background: tint-plate
effect: lightning
forceDark: true
hero:
name: Theme Plume
tagline: VuePress Next Theme

File diff suppressed because it is too large Load Diff

View 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`.

File diff suppressed because it is too large Load Diff

View 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` 的颜色。

View File

@ -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
View File

@ -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: {}

View File

@ -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

View File

@ -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"
}
}

View File

@ -104,6 +104,7 @@ onUnmounted(() => {
<component
:is="resolveComponentName(item.type)"
v-bind="item"
:index="index"
:only-once="onlyOnce"
/>
</div>

View File

@ -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})`,

View File

@ -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;
}
}

View File

@ -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" />

View File

@ -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>

View File

@ -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 }" />

View File

@ -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 }" />

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

View 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>

View 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)
}

View File

@ -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
}

View File

@ -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'

View File

@ -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

View File

@ -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))
}

View File

@ -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'])
}
}

View File

@ -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')

View 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(' ')}`) }))
}
}

View File

@ -31,3 +31,5 @@ export interface ThemeBadge {
bgColor?: string
borderColor?: string
}
export type ThemeLightDark<T> = T | { light?: T, dark?: T }

View File

@ -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 {

View 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 (xright, ydown).
* xy
*/
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).
* 01
*/
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值数组形式表示01
*/
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).
* 0360
*/
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
}

View File

@ -1,4 +1,5 @@
export * from './friends.js'
export * from './home.js'
export * from './homeHeroEffects.js'
export * from './page.js'
export * from './post.js'