rectangleShimmerComponent.vue 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. <script setup lang="ts">
  2. import { useLoop } from '@tresjs/core'
  3. import { Line2, LineGeometry, LineMaterial } from 'three-stdlib'
  4. import * as THREE from 'three'
  5. import CustomShaderMaterial from 'three-custom-shader-material/vanilla'
  6. import wallFragmentShader from '../shader/rectangleShimmerComponent/wall.frag?raw'
  7. import wallVertexShader from '../shader/rectangleShimmerComponent/wall.vert?raw'
  8. import {
  9. computed,
  10. onBeforeUnmount,
  11. shallowRef,
  12. watch,
  13. } from 'vue'
  14. interface RunningLineSegment {
  15. geometry: LineGeometry
  16. line: Line2
  17. material: LineMaterial
  18. }
  19. type WallCustomMaterial = CustomShaderMaterial<typeof THREE.MeshBasicMaterial>
  20. const props = withDefaults(
  21. defineProps<{
  22. gradientLength?: number
  23. height?: number
  24. length?: number
  25. lineColor?: string
  26. lineSpeed?: number
  27. lineWidth?: number
  28. segmentCount?: number
  29. wallBottomColor?: string
  30. wallBottomOpacity?: number
  31. wallGradientProgress?: number
  32. wallTopColor?: string
  33. wallTopOpacity?: number
  34. width?: number
  35. }>(),
  36. {
  37. gradientLength: 2.6,
  38. height: 1.4,
  39. length: 4,
  40. lineColor: '#ffd166',
  41. lineSpeed: 0.95,
  42. lineWidth: 0.035,
  43. segmentCount: 6,
  44. wallBottomColor: '#0d1f8f',
  45. wallBottomOpacity: 0.92,
  46. wallGradientProgress: 68,
  47. wallTopColor: '#57f3ff',
  48. wallTopOpacity: 0.18,
  49. width: 2.6,
  50. },
  51. )
  52. const tempResolution = new THREE.Vector2(1, 1)
  53. const tempWorldScale = new THREE.Vector3(1, 1, 1)
  54. const lineSegments = shallowRef<RunningLineSegment[]>([])
  55. const groupRef = shallowRef<THREE.Group | null>(null)
  56. const clampNumber = (value: number | undefined, min: number, max: number, fallback: number) => {
  57. if (!Number.isFinite(value)) {
  58. return fallback
  59. }
  60. return THREE.MathUtils.clamp(value as number, min, max)
  61. }
  62. const safeLength = computed(() => clampNumber(props.length, 0.4, 20, 4))
  63. const safeWidth = computed(() => clampNumber(props.width, 0.4, 20, 2.6))
  64. const safeHeight = computed(() => clampNumber(props.height, 0.2, 10, 1.4))
  65. const safeLineWidth = computed(() => clampNumber(props.lineWidth, 0.01, 0.8, 0.08))
  66. const safeLineSpeed = computed(() => clampNumber(props.lineSpeed, 0, 20, 1.25))
  67. const safeSegmentCount = computed(() => Math.round(clampNumber(props.segmentCount, 1, 12, 3)))
  68. const safeWallBottomOpacity = computed(() => clampNumber(props.wallBottomOpacity, 0, 1, 1))
  69. const safeWallGradientProgress = computed(() => clampNumber(props.wallGradientProgress, 0, 100, 100) / 100)
  70. const safeWallTopOpacity = computed(() => clampNumber(props.wallTopOpacity, 0, 1, 0.5))
  71. const perimeterLength = computed(() => (safeLength.value + safeWidth.value) * 2)
  72. const safeGradientLength = computed(() => {
  73. return clampNumber(props.gradientLength, 0.1, perimeterLength.value * 0.9, 1.4)
  74. })
  75. const getScaledLineWidth = () => {
  76. if (!groupRef.value) {
  77. return safeLineWidth.value
  78. }
  79. groupRef.value.updateWorldMatrix(true, false)
  80. groupRef.value.getWorldScale(tempWorldScale)
  81. const horizontalScale = Math.max(
  82. (Math.abs(tempWorldScale.x) + Math.abs(tempWorldScale.z)) / 2,
  83. 0.0001,
  84. )
  85. return safeLineWidth.value * horizontalScale
  86. }
  87. const createWallMaterial = (): WallCustomMaterial => {
  88. return new CustomShaderMaterial({
  89. baseMaterial: THREE.MeshBasicMaterial,
  90. color: 0xFFFFFF,
  91. depthWrite: false,
  92. fragmentShader: wallFragmentShader,
  93. side: THREE.DoubleSide,
  94. silent: true,
  95. toneMapped: false,
  96. transparent: true,
  97. uniforms: {
  98. uBottomColor: { value: new THREE.Color(props.wallBottomColor) },
  99. uBottomOpacity: { value: safeWallBottomOpacity.value },
  100. uGradientProgress: { value: safeWallGradientProgress.value },
  101. uHeight: { value: safeHeight.value },
  102. uTopColor: { value: new THREE.Color(props.wallTopColor) },
  103. uTopOpacity: { value: safeWallTopOpacity.value },
  104. },
  105. vertexShader: wallVertexShader,
  106. })
  107. }
  108. const wallMaterials: WallCustomMaterial[] = Array.from({ length: 4 }, () => createWallMaterial())
  109. const buildPerimeterPoint = (distance: number) => {
  110. const total = perimeterLength.value
  111. let cursor = THREE.MathUtils.euclideanModulo(distance, total)
  112. const halfLength = safeLength.value / 2
  113. const halfWidth = safeWidth.value / 2
  114. const y = 0.001
  115. if (cursor <= safeLength.value) {
  116. return new THREE.Vector3(-halfLength + cursor, y, -halfWidth)
  117. }
  118. cursor -= safeLength.value
  119. if (cursor <= safeWidth.value) {
  120. return new THREE.Vector3(halfLength, y, -halfWidth + cursor)
  121. }
  122. cursor -= safeWidth.value
  123. if (cursor <= safeLength.value) {
  124. return new THREE.Vector3(halfLength - cursor, y, halfWidth)
  125. }
  126. cursor -= safeLength.value
  127. return new THREE.Vector3(-halfLength, y, halfWidth - cursor)
  128. }
  129. const disposeLineSegments = () => {
  130. lineSegments.value.forEach(({ geometry, material }) => {
  131. geometry.dispose()
  132. material.dispose()
  133. })
  134. lineSegments.value = []
  135. }
  136. const rebuildLineSegments = () => {
  137. disposeLineSegments()
  138. const nextSegments: RunningLineSegment[] = []
  139. for (let index = 0; index < safeSegmentCount.value; index += 1) {
  140. const geometry = new LineGeometry()
  141. const material = new LineMaterial({
  142. color: new THREE.Color(props.lineColor).getHex(),
  143. depthWrite: false,
  144. transparent: true,
  145. linewidth: getScaledLineWidth(),
  146. opacity: 1,
  147. toneMapped: false,
  148. vertexColors: true,
  149. worldUnits: true,
  150. })
  151. material.resolution.copy(tempResolution)
  152. const line = new Line2(geometry, material)
  153. line.renderOrder = 10
  154. nextSegments.push({ geometry, line, material })
  155. }
  156. lineSegments.value = nextSegments
  157. }
  158. const syncWallMaterials = () => {
  159. wallMaterials.forEach((material) => {
  160. material.uniforms.uBottomColor.value.set(props.wallBottomColor)
  161. material.uniforms.uBottomOpacity.value = safeWallBottomOpacity.value
  162. material.uniforms.uGradientProgress.value = safeWallGradientProgress.value
  163. material.uniforms.uHeight.value = safeHeight.value
  164. material.uniforms.uTopColor.value.set(props.wallTopColor)
  165. material.uniforms.uTopOpacity.value = safeWallTopOpacity.value
  166. })
  167. }
  168. const updateLineSegments = (elapsed: number, renderer?: { getSize: (target: THREE.Vector2) => THREE.Vector2 }) => {
  169. if (!lineSegments.value.length) {
  170. return
  171. }
  172. if (renderer) {
  173. renderer.getSize(tempResolution)
  174. }
  175. const segmentSpacing = perimeterLength.value / safeSegmentCount.value
  176. const lineColor = new THREE.Color(props.lineColor)
  177. const sampleCount = Math.max(24, Math.ceil(safeGradientLength.value * 18))
  178. const scaledLineWidth = getScaledLineWidth()
  179. lineSegments.value.forEach((segment, index) => {
  180. segment.material.color.set(props.lineColor)
  181. segment.material.linewidth = scaledLineWidth
  182. segment.material.resolution.copy(tempResolution)
  183. const positions: number[] = []
  184. const colors: number[] = []
  185. const startDistance = elapsed * safeLineSpeed.value + index * segmentSpacing
  186. for (let step = 0; step < sampleCount; step += 1) {
  187. const t = sampleCount === 1 ? 1 : step / (sampleCount - 1)
  188. const point = buildPerimeterPoint(startDistance + t * safeGradientLength.value)
  189. const fade = 0.12 + 0.88 * t
  190. positions.push(point.x, point.y, point.z)
  191. colors.push(lineColor.r * fade, lineColor.g * fade, lineColor.b * fade)
  192. }
  193. segment.geometry.setPositions(positions)
  194. segment.geometry.setColors(colors)
  195. segment.line.computeLineDistances()
  196. })
  197. }
  198. watch(
  199. [safeLength, safeWidth, safeLineWidth, safeGradientLength, safeSegmentCount, () => props.lineColor],
  200. () => {
  201. rebuildLineSegments()
  202. updateLineSegments(0)
  203. },
  204. { immediate: true },
  205. )
  206. watch(
  207. [() => props.wallBottomColor, () => props.wallTopColor, safeHeight, safeWallBottomOpacity, safeWallGradientProgress, safeWallTopOpacity],
  208. syncWallMaterials,
  209. { immediate: true },
  210. )
  211. const { onBeforeRender } = useLoop()
  212. onBeforeRender(({ elapsed, renderer }) => {
  213. updateLineSegments(elapsed, renderer)
  214. })
  215. onBeforeUnmount(() => {
  216. disposeLineSegments()
  217. wallMaterials.forEach(material => material.dispose())
  218. })
  219. </script>
  220. <template>
  221. <TresGroup ref="groupRef">
  222. <TresMesh :position="[0, safeHeight / 2, safeWidth / 2]" :render-order="6">
  223. <TresPlaneGeometry :args="[safeLength, safeHeight]" />
  224. <primitive :object="wallMaterials[0]" attach="material" />
  225. </TresMesh>
  226. <TresMesh :position="[0, safeHeight / 2, -safeWidth / 2]" :render-order="6" :rotation="[0, Math.PI, 0]">
  227. <TresPlaneGeometry :args="[safeLength, safeHeight]" />
  228. <primitive :object="wallMaterials[1]" attach="material" />
  229. </TresMesh>
  230. <TresMesh :position="[safeLength / 2, safeHeight / 2, 0]" :render-order="6" :rotation="[0, -Math.PI / 2, 0]">
  231. <TresPlaneGeometry :args="[safeWidth, safeHeight]" />
  232. <primitive :object="wallMaterials[2]" attach="material" />
  233. </TresMesh>
  234. <TresMesh :position="[-safeLength / 2, safeHeight / 2, 0]" :render-order="6" :rotation="[0, Math.PI / 2, 0]">
  235. <TresPlaneGeometry :args="[safeWidth, safeHeight]" />
  236. <primitive :object="wallMaterials[3]" attach="material" />
  237. </TresMesh>
  238. <primitive
  239. v-for="segment in lineSegments"
  240. :key="segment.line.uuid"
  241. :object="segment.line"
  242. />
  243. </TresGroup>
  244. </template>