|
| 1 | +<script setup lang="ts"> |
| 2 | +import { ContactShadows, Environment, OrbitControls } from '@tresjs/cientos' |
| 3 | +import { TresCanvas } from '@tresjs/core' |
| 4 | +import { TresLeches, useControls } from '@tresjs/leches' |
| 5 | +import { NoToneMapping, Shape, Vector3 } from 'three' |
| 6 | +import { computed, onUnmounted, reactive, ref, shallowRef } from 'vue' |
| 7 | +import { DepthPickingPassPmndrs, EffectComposerPmndrs, ShockWavePmndrs } from '@tresjs/post-processing' |
| 8 | +import { useElementBounding, useMouse, useParentElement } from '@vueuse/core' |
| 9 | +import { gsap } from 'gsap' |
| 10 | +
|
| 11 | +import '@tresjs/leches/styles' |
| 12 | +
|
| 13 | +const gl = { |
| 14 | + clearColor: '#8D404A', |
| 15 | + toneMapping: NoToneMapping, |
| 16 | + multisampling: 8, |
| 17 | +} |
| 18 | +
|
| 19 | +const shockWaveEffectRef = shallowRef(null) |
| 20 | +const elCanvasRef = ref(null) |
| 21 | +const depthPickingPassRef = ref(null) |
| 22 | +const meshHeartRef = ref(null) |
| 23 | +const mousePosition = ref(new Vector3()) |
| 24 | +
|
| 25 | +function createHeartShape(scale: number) { |
| 26 | + const shape = new Shape() |
| 27 | + const x = 0 |
| 28 | + const y = 0 |
| 29 | +
|
| 30 | + shape.moveTo(x, y) |
| 31 | + shape.bezierCurveTo(x, y, x - 0.5 * scale, y + 2.5 * scale, x - 2.5 * scale, y + 2.5 * scale) |
| 32 | + shape.bezierCurveTo(x - 6.5 * scale, y + 2.5 * scale, x - 6.5 * scale, y - 1.5 * scale, x - 6.5 * scale, y - 1.5 * scale) |
| 33 | + shape.bezierCurveTo(x - 6.5 * scale, y - 4.5 * scale, x - 3.5 * scale, y - 7 * scale, x, y - 9.5 * scale) |
| 34 | + shape.bezierCurveTo(x + 3.5 * scale, y - 7 * scale, x + 6.5 * scale, y - 4.5 * scale, x + 6.5 * scale, y - 1.5 * scale) |
| 35 | + shape.bezierCurveTo(x + 6.5 * scale, y - 1.5 * scale, x + 6.5 * scale, y + 2.5 * scale, x + 2.5 * scale, y + 2.5 * scale) |
| 36 | + shape.bezierCurveTo(x + 0.5 * scale, y + 2.5 * scale, x, y, x, y) |
| 37 | +
|
| 38 | + return shape |
| 39 | +} |
| 40 | +
|
| 41 | +const heartShapeFront = createHeartShape(0.35) |
| 42 | +
|
| 43 | +const parentEl = useParentElement() |
| 44 | +const { x, y } = useMouse({ target: parentEl }) |
| 45 | +const { width, height, left, top } = useElementBounding(parentEl) |
| 46 | +
|
| 47 | +const extrudeSettings = reactive({ |
| 48 | + depth: 0.1, |
| 49 | + bevelEnabled: true, |
| 50 | + bevelSegments: 2, |
| 51 | + steps: 2, |
| 52 | + bevelSize: 0.25, |
| 53 | + bevelThickness: 0.25, |
| 54 | +}) |
| 55 | +
|
| 56 | +const materialProps = reactive({ |
| 57 | + color: '#FF9999', |
| 58 | + reflectivity: 0.75, |
| 59 | + ior: 1.5, |
| 60 | + roughness: 0.75, |
| 61 | + clearcoat: 0.01, |
| 62 | + clearcoatRoughness: 0.15, |
| 63 | + transmission: 0.7, |
| 64 | +}) |
| 65 | +
|
| 66 | +let tl: gsap.core.Timeline |
| 67 | +
|
| 68 | +const ctx = gsap.context(() => { }) |
| 69 | +
|
| 70 | +const { amplitude, waveSize, speed, maxRadius } = useControls({ |
| 71 | + amplitude: { value: 0.4, step: 0.01, max: 1.0 }, |
| 72 | + waveSize: { value: 0.5, step: 0.01, max: 1.0 }, |
| 73 | + speed: { value: 1.5, step: 0.1, max: 10.0 }, |
| 74 | + maxRadius: { value: 0.2, step: 0.01, max: 2 }, |
| 75 | +}) |
| 76 | +
|
| 77 | +const cursorX = computed(() => ((x.value - left.value - width.value) / width.value) * 2.0 + 1.0) |
| 78 | +const cursorY = computed(() => -((y.value - top.value - height.value) / height.value) * 2.0 - 1.0) |
| 79 | +
|
| 80 | +async function updateMousePosition() { |
| 81 | + if (!elCanvasRef.value || !shockWaveEffectRef.value || !depthPickingPassRef.value) { return } |
| 82 | +
|
| 83 | + const ndcPosition = new Vector3(cursorX.value, cursorY.value, 0) |
| 84 | +
|
| 85 | + // Read depth from depth picking pass |
| 86 | + ndcPosition.z = await depthPickingPassRef.value.pass.readDepth(ndcPosition) |
| 87 | +
|
| 88 | + ndcPosition.z = ndcPosition.z * 2.0 - 1.0 |
| 89 | +
|
| 90 | + mousePosition.value.copy(ndcPosition.unproject(elCanvasRef.value.context.camera.value)) |
| 91 | +} |
| 92 | +
|
| 93 | +function triggerShockWave() { |
| 94 | + if (!meshHeartRef.value || !shockWaveEffectRef.value) { return } |
| 95 | +
|
| 96 | + updateMousePosition() |
| 97 | +
|
| 98 | + shockWaveEffectRef.value.effect.explode() |
| 99 | +
|
| 100 | + const duration = getActiveDuration() |
| 101 | +
|
| 102 | + const durationSeconds = duration / 1000 |
| 103 | +
|
| 104 | + ctx.add(() => { |
| 105 | + tl?.kill() |
| 106 | +
|
| 107 | + tl = gsap.timeline() |
| 108 | +
|
| 109 | + tl.to(meshHeartRef.value.scale, { |
| 110 | + duration: durationSeconds / 9, |
| 111 | + x: 0.8, |
| 112 | + y: 0.8, |
| 113 | + z: 0.8, |
| 114 | + ease: 'power2.inOut', |
| 115 | + }).to(meshHeartRef.value.scale, { |
| 116 | + duration: durationSeconds / 9, |
| 117 | + x: 1.2, |
| 118 | + y: 1.2, |
| 119 | + z: 1.2, |
| 120 | + ease: 'power2.inOut', |
| 121 | + }).to(meshHeartRef.value.scale, { |
| 122 | + duration: durationSeconds / 9, |
| 123 | + x: 1, |
| 124 | + y: 1, |
| 125 | + z: 1, |
| 126 | + ease: 'power2.inOut', |
| 127 | + }) |
| 128 | + }) |
| 129 | +
|
| 130 | + // Fallback for onFinish explode Shock Wave |
| 131 | + // setTimeout(() => { |
| 132 | + // console.log('Explode effect animation done') |
| 133 | + // }, duration) |
| 134 | +} |
| 135 | +
|
| 136 | +function getActiveDuration() { |
| 137 | + // This function retrieves the duration for emitting the shock wave. |
| 138 | + // For more details, see: https://github.com/pmndrs/postprocessing/blob/3d3df0576b6d49aec9e763262d5a1ff7429fd91a/src/effects/ShockWaveEffectRef.js#L258-L301 |
| 139 | +
|
| 140 | + // To reduce the duration of the animation, you can decrease the values of maxRadius and waveSize. |
| 141 | + // Note that the speed affects how quickly the shock wave radius increases over time, but not the total duration of the emit explode. |
| 142 | +
|
| 143 | + // Retrieve the values dynamically |
| 144 | + const radiusMax = maxRadius.value.value |
| 145 | + const wave = waveSize.value.value |
| 146 | +
|
| 147 | + // Duration formula: 2 * maxRadius + 3 * waveSize |
| 148 | + const duration = 2 * radiusMax + 3 * wave |
| 149 | +
|
| 150 | + // Convert to milliseconds |
| 151 | + return duration * 1000 |
| 152 | +} |
| 153 | +
|
| 154 | +onUnmounted(() => { |
| 155 | + ctx.revert() |
| 156 | +}) |
| 157 | +</script> |
| 158 | + |
| 159 | +<template> |
| 160 | + <TresLeches style="left: initial;right:10px; top:10px;" /> |
| 161 | + |
| 162 | + <p class="doc-shock-wave-instructions text-xs font-semibold text-zinc-600">Click on the heart to distribute love</p> |
| 163 | + |
| 164 | + <TresCanvas |
| 165 | + ref="elCanvasRef" |
| 166 | + v-bind="gl" |
| 167 | + > |
| 168 | + <TresPerspectiveCamera |
| 169 | + :position="[0, 0, 10]" |
| 170 | + /> |
| 171 | + |
| 172 | + <OrbitControls make-default auto-rotate /> |
| 173 | + |
| 174 | + <TresMesh ref="meshHeartRef" :position-y="2" @click="triggerShockWave"> |
| 175 | + <TresExtrudeGeometry :args="[heartShapeFront, extrudeSettings]" /> |
| 176 | + <TresMeshPhysicalMaterial |
| 177 | + v-bind="materialProps" |
| 178 | + /> |
| 179 | + </TresMesh> |
| 180 | + |
| 181 | + <TresDirectionalLight |
| 182 | + :position="[5, 5, 7.5]" |
| 183 | + :intensity="2" |
| 184 | + /> |
| 185 | + |
| 186 | + <ContactShadows |
| 187 | + :opacity="1" |
| 188 | + :position-y="-2.75" |
| 189 | + :blur=".5" |
| 190 | + /> |
| 191 | + |
| 192 | + <Suspense> |
| 193 | + <Environment preset="night" /> |
| 194 | + </Suspense> |
| 195 | + |
| 196 | + <Suspense> |
| 197 | + <EffectComposerPmndrs> |
| 198 | + <DepthPickingPassPmndrs ref="depthPickingPassRef" /> |
| 199 | + <ShockWavePmndrs ref="shockWaveEffectRef" :position="mousePosition" :amplitude="amplitude.value" :waveSize="waveSize.value" :speed="speed.value" :maxRadius="maxRadius.value" /> |
| 200 | + </EffectComposerPmndrs> |
| 201 | + </Suspense> |
| 202 | + </TresCanvas> |
| 203 | +</template> |
| 204 | + |
| 205 | +<style scoped> |
| 206 | +.doc-shock-wave-instructions { |
| 207 | + position: absolute; |
| 208 | + bottom: 0; |
| 209 | + left: 0; |
| 210 | + padding: 0.65rem 0.85rem; |
| 211 | + text-align: center; |
| 212 | + color: #fff; |
| 213 | + z-index: 2; |
| 214 | + border-radius: 0px 10px 0px 0px; |
| 215 | + background: linear-gradient(90deg, hsla(24, 100%, 83%, 1) 0%, hsla(341, 91%, 68%, 1) 100%); |
| 216 | + margin: 0; |
| 217 | +} |
| 218 | +</style> |
0 commit comments