Explorar el Código

chore: cookbook animations (#1085)

* docs: basic animations recipe

* docs: add advanced GSAP animations examples and documentation

* docs: update lighting components and correct snippets

* fix: add type annotations for positions array in GSAP examples

* docs: correct arrow function syntax and add missing commas in GSAP examples

* doc: frame-rate typo

Co-authored-by: Colin S. <19342760+colinscz@users.noreply.github.com>

* docs: cookbook animations typo

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Colin S. <19342760+colinscz@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Alvaro Saburido hace 3 días
padre
commit
e79e4cc5e7

+ 332 - 0
docs/app/components/TheGraph.vue

@@ -0,0 +1,332 @@
+<script setup lang="ts">
+// I was trying to make this a generic graph component that could be reused in the docs
+// but it got a bit out of hand. Still, it might be useful for future stuff
+// so I'm keeping it here for now.
+import { useElementSize } from '@vueuse/core'
+
+interface DataPoint {
+  x: number
+  y: number
+}
+
+interface Dataset {
+  label: string
+  points: DataPoint[]
+  color: string
+  strokeWidth?: number
+  strokeDasharray?: string | number
+  strokeDashoffset?: string | number
+}
+
+interface UIConfig {
+  strokeWidth?: number
+  fillColor?: string
+  backgroundColor?: string
+  gridColor?: string
+  showGrid?: boolean
+  showAxes?: [boolean, boolean] // [x-axis, y-axis]
+  showLabels?: [boolean, boolean] // [x-labels, y-labels]
+  axisColor?: string
+  labelColor?: string
+  fontSize?: number
+  padding?: number
+  dataPadding?: [number, number] // [x-padding %, y-padding %] - extends data bounds
+  labelIntervals?: [number, number] // [x-interval, y-interval] - step size for labels
+}
+
+const props = withDefaults(defineProps<{
+  data: Dataset[]
+  ui?: UIConfig
+}>(), {
+  ui: () => ({}),
+})
+
+const defaultUI: Required<UIConfig> = {
+  strokeWidth: 1.5,
+  fillColor: 'transparent',
+  backgroundColor: 'transparent',
+  gridColor: '#e5e7eb',
+  showGrid: false,
+  showAxes: [false, false], // [x-axis, y-axis]
+  showLabels: [false, false], // [x-labels, y-labels]
+  axisColor: '#6b7280',
+  labelColor: '#374151',
+  fontSize: 12,
+  padding: 40,
+  dataPadding: [5, 10], // [x-padding %, y-padding %] - 5% x, 10% y
+  labelIntervals: [1, 0.5], // [x-interval, y-interval] - auto-calculated if not set
+}
+
+const config = computed(() => ({ ...defaultUI, ...props.ui }))
+
+const containerRef = ref<HTMLElement>()
+const { width, height } = useElementSize(containerRef)
+
+const viewBox = computed(() => `0 0 ${width.value} ${height.value}`)
+
+const bounds = computed(() => {
+  if (props.data.length === 0) {
+    return { minX: 0, maxX: 1, minY: 0, maxY: 1 }
+  }
+
+  const allPoints = props.data.flatMap(dataset => dataset.points)
+  const xs = allPoints.map(d => d.x)
+  const ys = allPoints.map(d => d.y)
+
+  const rawMinX = Math.min(...xs)
+  const rawMaxX = Math.max(...xs)
+  const rawMinY = Math.min(...ys)
+  const rawMaxY = Math.max(...ys)
+
+  // Apply data padding (percentage of range)
+  const [xPaddingPercent, yPaddingPercent] = config.value.dataPadding
+
+  const xRange = rawMaxX - rawMinX
+  const yRange = rawMaxY - rawMinY
+
+  const xPadding = (xRange * xPaddingPercent) / 100
+  const yPadding = (yRange * yPaddingPercent) / 100
+
+  return {
+    minX: rawMinX - xPadding,
+    maxX: rawMaxX + xPadding,
+    minY: rawMinY - yPadding,
+    maxY: rawMaxY + yPadding,
+  }
+})
+
+const scale = computed(() => {
+  const { minX, maxX, minY, maxY } = bounds.value
+  const { padding } = config.value
+
+  const dataWidth = maxX - minX
+  const dataHeight = maxY - minY
+
+  return {
+    x: (width.value - 2 * padding) / (dataWidth || 1),
+    y: (height.value - 2 * padding) / (dataHeight || 1),
+  }
+})
+
+const transformPoint = (point: DataPoint) => {
+  const { minX, minY } = bounds.value
+  const { padding } = config.value
+
+  return {
+    x: padding + (point.x - minX) * scale.value.x,
+    y: height.value - padding - (point.y - minY) * scale.value.y,
+  }
+}
+
+const createPathData = (points: DataPoint[]) => {
+  if (points.length === 0) {
+    return ''
+  }
+
+  const transformedPoints = points.map(transformPoint)
+
+  return transformedPoints.reduce((path, point, index) => {
+    const command = index === 0 ? 'M' : 'L'
+    return `${path} ${command} ${point.x} ${point.y}`
+  }, '').trim()
+}
+
+const gridLines = computed(() => {
+  const { padding } = config.value
+  const lines = []
+
+  // Vertical grid lines
+  for (let i = 1; i < 10; i++) {
+    const x = padding + (width.value - 2 * padding) * (i / 10)
+    lines.push({ x1: x, y1: padding, x2: x, y2: height.value - padding })
+  }
+
+  // Horizontal grid lines
+  for (let i = 1; i < 5; i++) {
+    const y = padding + (height.value - 2 * padding) * (i / 5)
+    lines.push({ x1: padding, y1: y, x2: width.value - padding, y2: y })
+  }
+
+  return lines
+})
+
+const axisLabels = computed(() => {
+  const { minX, maxX, minY, maxY } = bounds.value
+  const { padding, fontSize, labelIntervals } = config.value
+  const [xInterval, yInterval] = labelIntervals
+  const labels = []
+
+  // Y-axis labels (left side) - use interval
+  if (yInterval > 0) {
+    const startY = Math.ceil(minY / yInterval) * yInterval
+    const endY = Math.floor(maxY / yInterval) * yInterval
+
+    for (let value = startY; value <= endY; value += yInterval) {
+      // Handle floating point precision
+      const roundedValue = Math.round(value / yInterval) * yInterval
+      const y = height.value - padding - (roundedValue - minY) * scale.value.y
+
+      labels.push({
+        type: 'y',
+        x: padding - 10,
+        y: y + fontSize / 3,
+        text: roundedValue.toFixed(yInterval < 1 ? 1 : 0),
+      })
+    }
+  }
+
+  // X-axis labels (bottom) - use interval
+  if (xInterval > 0) {
+    const startX = Math.ceil(minX / xInterval) * xInterval
+    const endX = Math.floor(maxX / xInterval) * xInterval
+
+    for (let value = startX; value <= endX; value += xInterval) {
+      // Handle floating point precision
+      const roundedValue = Math.round(value / xInterval) * xInterval
+      const x = padding + (roundedValue - minX) * scale.value.x
+
+      labels.push({
+        type: 'x',
+        x,
+        y: height.value - padding + fontSize + 5,
+        text: roundedValue.toFixed(xInterval < 1 ? 1 : 0),
+      })
+    }
+  }
+
+  return labels
+})
+</script>
+
+<template>
+  <div ref="containerRef" class="graph-container">
+    <svg
+      :viewBox="viewBox"
+      :width="width"
+      :height="height"
+      class="svg-graph"
+      :style="{ backgroundColor: config.backgroundColor }"
+    >
+      <!-- Background -->
+      <rect
+        :width="width"
+        :height="height"
+        :fill="config.backgroundColor"
+      />
+
+      <!-- Grid -->
+      <g v-if="config.showGrid" class="grid">
+        <line
+          v-for="(line, index) in gridLines"
+          :key="`grid-${index}`"
+          :x1="line.x1"
+          :y1="line.y1"
+          :x2="line.x2"
+          :y2="line.y2"
+          :stroke="config.gridColor"
+          stroke-width="1"
+          opacity="0.3"
+        />
+      </g>
+
+      <!-- Axes -->
+      <g class="axes">
+        <!-- X axis (y=0 line) -->
+        <line
+          v-if="config.showAxes[0]"
+          :x1="config.padding"
+          :y1="transformPoint({ x: 0, y: 0 }).y"
+          :x2="width - config.padding"
+          :y2="transformPoint({ x: 0, y: 0 }).y"
+          :stroke="config.axisColor"
+          stroke-width="2"
+        />
+
+        <!-- Y axis (x=0 line) -->
+        <line
+          v-if="config.showAxes[1]"
+          :x1="transformPoint({ x: 0, y: 0 }).x"
+          :y1="config.padding"
+          :x2="transformPoint({ x: 0, y: 0 }).x"
+          :y2="height - config.padding"
+          :stroke="config.axisColor"
+          stroke-width="2"
+        />
+      </g>
+
+      <!-- Axis labels -->
+      <g class="labels">
+        <text
+          v-for="(label, index) in axisLabels"
+          v-show="(label.type === 'x' && config.showLabels[0]) || (label.type === 'y' && config.showLabels[1])"
+          :key="`label-${index}`"
+          :x="label.x"
+          :y="label.y"
+          :fill="config.labelColor"
+          :font-size="config.fontSize"
+          :text-anchor="label.type === 'x' ? 'middle' : 'end'"
+          font-family="system-ui, sans-serif"
+        >
+          {{ label.text }}
+        </text>
+      </g>
+
+      <!-- Data paths and points for each dataset -->
+      <g v-for="(dataset, datasetIndex) in data" :key="`dataset-${datasetIndex}`">
+        <!-- Data path -->
+        <path
+          v-if="createPathData(dataset.points)"
+          :d="createPathData(dataset.points)"
+          :stroke="dataset.color"
+          :stroke-width="dataset.strokeWidth ?? config.strokeWidth"
+          v-bind="{
+            ...(dataset.strokeDasharray ? { 'stroke-dasharray': dataset.strokeDasharray } : {}),
+            ...(dataset.strokeDashoffset ? { 'stroke-dashoffset': dataset.strokeDashoffset } : {}),
+          }"
+          :fill="config.fillColor"
+          vector-effect="non-scaling-stroke"
+        />
+
+        <!-- Data points -->
+        <!-- <g class="data-points">
+          <circle
+            v-for="(point, index) in dataset.points"
+            :key="`point-${datasetIndex}-${index}`"
+            :cx="transformPoint(point).x"
+            :cy="transformPoint(point).y"
+            :r="(dataset.strokeWidth ?? config.strokeWidth) + 1"
+            :fill="dataset.color"
+          />
+        </g> -->
+      </g>
+    </svg>
+  </div>
+</template>
+
+<style scoped>
+.graph-container {
+  width: 100%;
+  height: 100%;
+  min-height: 200px;
+}
+
+.svg-graph {
+  width: 100%;
+  height: 100%;
+  transition: all 0.3s ease;
+}
+
+.grid line {
+  transition: opacity 0.3s ease;
+}
+
+path {
+  transition: d 0.5s ease;
+}
+
+circle {
+  transition:
+    cx 0.5s ease,
+    cy 0.5s ease;
+}
+</style>

+ 55 - 0
docs/app/components/examples/advanced-gsap-animations/Experience.vue

@@ -0,0 +1,55 @@
+<script setup lang="ts">
+import { shallowRef, watch } from 'vue'
+import { OrbitControls } from '@tresjs/cientos'
+import gsap from 'gsap'
+import type { TresObject } from '@tresjs/core'
+
+const boxesRef = shallowRef<TresObject>()
+const zs: number[] = []
+for (let z = -4.5; z <= 4.5; z++) {
+  zs.push(z)
+}
+
+watch(boxesRef, () => {
+  if (!boxesRef.value) { return }
+
+  // Getting positions for all the boxes
+  const positions = Array.from(boxesRef.value.children as TresObject[]).map(
+    child => child.position,
+  )
+  // Getting rotations for all the boxes
+  const rotations = Array.from(boxesRef.value.children as TresObject[]).map(
+    child => child.rotation,
+  )
+
+  const animProperties = {
+    ease: 'power1.inOut',
+    duration: 1,
+    stagger: {
+      each: 0.25,
+      repeat: -1,
+      yoyo: true,
+    },
+  }
+  gsap.to(positions, {
+    y: 2.5,
+    ...animProperties,
+  })
+  gsap.to(rotations, {
+    x: 2,
+    ...animProperties,
+  })
+})
+</script>
+
+<template>
+  <TresPerspectiveCamera :position="[-10, 5, 10]" />
+  <OrbitControls />
+  <TresGroup ref="boxesRef">
+    <TresMesh v-for="(z, i) of zs" :key="i" :position="[0, 0.5, z]">
+      <TresBoxGeometry />
+      <TresMeshNormalMaterial />
+    </TresMesh>
+  </TresGroup>
+  <TresGridHelper :args="[10, 10, 0x444444, 'teal']" />
+</template>

+ 14 - 0
docs/app/components/examples/advanced-gsap-animations/index.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import TheExperience from './Experience.vue'
+</script>
+
+<template>
+  <SceneWrapper>
+    <TresCanvas
+      clear-color="#82DBC5"
+    >
+      <TheExperience />
+    </TresCanvas>
+  </SceneWrapper>
+</template>

+ 86 - 0
docs/app/components/examples/advanced-gsap-timeline/Experience.vue

@@ -0,0 +1,86 @@
+<script setup lang="ts">
+import { onMounted, shallowRef, watch } from 'vue'
+import { OrbitControls } from '@tresjs/cientos'
+import gsap from 'gsap'
+import type { TresObject } from '@tresjs/core'
+
+const groupRef = shallowRef<TresObject>()
+const timeline = gsap.timeline({ paused: true })
+
+// Create array of positions for multiple objects
+const positions: [number, number, number][] = [
+  [-3, 0.5, 0],
+  [-1, 0.5, 0],
+  [1, 0.5, 0],
+  [3, 0.5, 0],
+]
+
+watch(groupRef, () => {
+  if (!groupRef.value) { return }
+
+  const children = Array.from(groupRef.value.children) as TresObject[]
+
+  // Clear existing timeline
+  timeline.clear()
+
+  // Add multiple animations to timeline
+  timeline
+    .to(children.map(child => child.position), {
+      y: 3,
+      duration: 1,
+      ease: 'back.out(1.7)',
+      stagger: 0.1,
+    })
+    .to(children.map(child => child.rotation), {
+      y: Math.PI * 2,
+      duration: 2,
+      ease: 'power2.inOut',
+      stagger: 0.1,
+    }, '-=0.5') // Start 0.5s before previous animation ends
+    .to(children.map(child => child.scale), {
+      x: 1.5,
+      y: 1.5,
+      z: 1.5,
+      duration: 0.5,
+      ease: 'elastic.out(1, 0.3)',
+      stagger: 0.05,
+    })
+    .to(children.map(child => child.position), {
+      y: 0.5,
+      duration: 1,
+      ease: 'bounce.out',
+      stagger: 0.1,
+    })
+    .to(children.map(child => child.scale), {
+      x: 1,
+      y: 1,
+      z: 1,
+      duration: 0.5,
+      ease: 'power2.out',
+      stagger: 0.05,
+    }, '-=0.8')
+})
+
+// Auto-play the timeline on mount
+onMounted(() => {
+  setTimeout(() => {
+    if (timeline) {
+      timeline.play()
+      // Restart timeline when it completes
+      timeline.repeat(-1).yoyo(false)
+    }
+  }, 500) // Small delay to ensure refs are ready
+})
+</script>
+
+<template>
+  <TresPerspectiveCamera :position="[-8, 6, 8]" />
+  <OrbitControls />
+  <TresGroup ref="groupRef">
+    <TresMesh v-for="(pos, i) of positions" :key="i" :position="pos">
+      <TresBoxGeometry />
+      <TresMeshNormalMaterial />
+    </TresMesh>
+  </TresGroup>
+  <TresGridHelper :args="[10, 10, 0x444444, 'teal']" />
+</template>

+ 14 - 0
docs/app/components/examples/advanced-gsap-timeline/index.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import TheExperience from './Experience.vue'
+</script>
+
+<template>
+  <SceneWrapper>
+    <TresCanvas
+      clear-color="#82DBC5"
+    >
+      <TheExperience />
+    </TresCanvas>
+  </SceneWrapper>
+</template>

+ 36 - 0
docs/app/components/examples/basic-animation-120fps/Experience.vue

@@ -0,0 +1,36 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useLoop } from '@tresjs/core'
+import type { Mesh } from 'three'
+
+const cubeRef = ref<Mesh | null>(null)
+
+const { onBeforeRender } = useLoop()
+
+let lastTime = 0
+const targetFPS = 120
+const frameInterval = 1000 / targetFPS
+
+onBeforeRender(({ elapsed }) => {
+  const currentTime = elapsed * 1000
+
+  if (currentTime - lastTime >= frameInterval) {
+    if (cubeRef.value) {
+      // Fixed rotation per frame (not time-based) - 120 FPS (faster)
+      cubeRef.value.rotation.x += 0.02
+      cubeRef.value.rotation.y += 0.02
+    }
+    lastTime = currentTime
+  }
+})
+</script>
+
+<template>
+  <TresPerspectiveCamera :position="[0, 1, 5]" />
+  <TresAmbientLight :intensity="0.5" />
+  <TresDirectionalLight :position="[0, 2, 4]" :intensity="1" />
+  <TresMesh ref="cubeRef" :position="[0, 1, 0]">
+    <TresBoxGeometry :args="[1, 1, 1]" />
+    <TresMeshStandardMaterial color="#4ecdc4" />
+  </TresMesh>
+</template>

+ 14 - 0
docs/app/components/examples/basic-animation-120fps/index.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import TheExperience from './Experience.vue'
+</script>
+
+<template>
+  <SceneWrapper class="!rounded-none">
+    <TresCanvas
+      clear-color="#82DBC5"
+    >
+      <TheExperience />
+    </TresCanvas>
+  </SceneWrapper>
+</template>

+ 36 - 0
docs/app/components/examples/basic-animation-60fps/Experience.vue

@@ -0,0 +1,36 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useLoop } from '@tresjs/core'
+import type { Mesh } from 'three'
+
+const cubeRef = ref<Mesh | null>(null)
+
+const { onBeforeRender } = useLoop()
+
+let lastTime = 0
+const targetFPS = 60
+const frameInterval = 1000 / targetFPS
+
+onBeforeRender(({ elapsed }) => {
+  const currentTime = elapsed * 1000
+
+  if (currentTime - lastTime >= frameInterval) {
+    if (cubeRef.value) {
+      // Fixed rotation per frame (not time-based) - 60 FPS
+      cubeRef.value.rotation.x += 0.02
+      cubeRef.value.rotation.y += 0.02
+    }
+    lastTime = currentTime
+  }
+})
+</script>
+
+<template>
+  <TresPerspectiveCamera :position="[0, 1, 5]" />
+  <TresAmbientLight :intensity="0.5" />
+  <TresDirectionalLight :position="[0, 2, 4]" :intensity="1" />
+  <TresMesh ref="cubeRef" :position="[0, 1, 0]">
+    <TresBoxGeometry :args="[1, 1, 1]" />
+    <TresMeshStandardMaterial color="#ff6b6b" />
+  </TresMesh>
+</template>

+ 14 - 0
docs/app/components/examples/basic-animation-60fps/index.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import TheExperience from './Experience.vue'
+</script>
+
+<template>
+  <SceneWrapper class="!rounded-none">
+    <TresCanvas
+      clear-color="#82DBC5"
+    >
+      <TheExperience />
+    </TresCanvas>
+  </SceneWrapper>
+</template>

+ 28 - 0
docs/app/components/examples/basic-animation-elapsed/Experience.vue

@@ -0,0 +1,28 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useLoop } from '@tresjs/core'
+import type { Mesh } from 'three'
+
+const cubeRef = ref<Mesh | null>(null)
+
+const { onBeforeRender } = useLoop()
+
+onBeforeRender(({ elapsed }) => {
+  const positionValue = Math.sin(elapsed) * 0.005
+
+  if (cubeRef.value) {
+    cubeRef.value.position.y += positionValue
+  }
+})
+</script>
+
+<template>
+  <TresPerspectiveCamera :position="[0, 1, 5]" :look-at="[0, 1, 0]" />
+  <TresMesh
+    ref="cubeRef"
+    :position="[0, 1, 0]"
+  >
+    <TresBoxGeometry :args="[1, 1, 1]" />
+    <TresMeshNormalMaterial />
+  </TresMesh>
+</template>

+ 14 - 0
docs/app/components/examples/basic-animation-elapsed/index.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import TheExperience from './Experience.vue'
+</script>
+
+<template>
+  <SceneWrapper>
+    <TresCanvas
+      clear-color="#82DBC5"
+    >
+      <TheExperience />
+    </TresCanvas>
+  </SceneWrapper>
+</template>

+ 24 - 0
docs/app/components/examples/basic-animation/Experience.vue

@@ -0,0 +1,24 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useLoop } from '@tresjs/core'
+import type { Mesh } from 'three'
+
+const cubeRef = ref<Mesh | null>(null)
+
+const { onBeforeRender } = useLoop()
+
+onBeforeRender(({ delta }) => {
+  if (cubeRef.value) {
+    cubeRef.value.rotation.x += delta
+    cubeRef.value.rotation.y += delta
+  }
+})
+</script>
+
+<template>
+  <TresPerspectiveCamera :position="[0, 1, 5]" />
+  <TresMesh ref="cubeRef" :position="[0, 1, 0]">
+    <TresBoxGeometry :args="[1, 1, 1]" />
+    <TresMeshNormalMaterial />
+  </TresMesh>
+</template>

+ 14 - 0
docs/app/components/examples/basic-animation/index.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import TheExperience from './Experience.vue'
+</script>
+
+<template>
+  <SceneWrapper>
+    <TresCanvas
+      clear-color="#82DBC5"
+    >
+      <TheExperience />
+    </TresCanvas>
+  </SceneWrapper>
+</template>

+ 120 - 0
docs/content/4.cookbook/2.basic-animations.md

@@ -0,0 +1,120 @@
+---
+title: Basic Animations
+description: Learn how to create basic animations in TresJS
+thumbnail: /recipes/model-n-animations/basic.png
+---
+
+::examples-basic-animation
+::
+
+This recipe covers the fundamentals of creating smooth animations in TresJS.
+
+::steps
+
+### Import `useLoop` composable
+
+The `useLoop` composable is the core of TresJS updates, which includes: **animations**. It allows you to register a callback that will be called every time the renderer updates the scene with the browser's refresh rate.
+
+
+```ts
+import { useLoop } from '@tresjs/core'
+
+const { onBeforeRender } = useLoop()
+
+onBeforeRender(() => {
+  // Animation logic here
+})
+
+```
+
+::read-more{to="/api/composables/use-loop"}
+::
+
+### Get a reference to the object you want to animate
+
+Similar to Vue, you can use [template refs](https://vuejs.org/guide/essentials/template-refs) to access the Three.js object instance and manipulate its properties. If you want to optimize even further, you can use `shallowRef` to avoid unnecessary reactivity.
+
+```vue [RotatingCube.vue]
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useLoop } from '@tresjs/core'
+
+const cubeRef = ref<THREE.Mesh | null>(null)
+// or use shallowRef if you want to avoid reactivity
+// const cubeRef = shallowRef<THREE.Mesh | null>(null)
+
+const { onBeforeRender } = useLoop()
+
+onBeforeRender(() => {
+  if (cubeRef.value) {
+    cubeRef.value.rotation.x += 0.01
+    cubeRef.value.rotation.y += 0.01
+  }
+})
+</script>
+
+<template>
+  <TresMesh ref="cubeRef" :position="[0, 1, 0]">
+    <TresBoxGeometry :args="[1, 1, 1]" />
+    <TresMeshNormalMaterial />
+  </TresMesh>
+</template>
+```
+
+::read-more{to="/api/advanced/performance#reactivity-and-performance"}
+To read more about reactivity and performance in TresJS.
+::
+
+## Use `delta`
+
+The `onBeforeRender` callback provides a `delta` parameter, which represents the time elapsed since the last frame. This is useful for creating frame rate independent animations.
+
+```ts
+onBeforeRender(({ delta }) => {
+  if (cubeRef.value) {
+    cubeRef.value.rotation.x += delta
+    cubeRef.value.rotation.y += delta
+  }
+})
+```
+
+Without using `delta`, the animation speed would vary depending on the frame rate, leading to inconsistent behavior across different devices, like the example below:
+
+<div class="w-full flex border border-gray-200 rounded-lg overflow-hidden">
+   <div class="w-1/2 border-r border-gray-200">
+      <div class="text-center p-2 border-b border-gray-200 font-bold">
+         60fps
+      </div>
+
+      ::examples-basic-animation-60fps
+      ::
+
+   </div>
+   <div class="w-1/2">
+      <div class="p-2 text-center p-2 border-b border-gray-200 font-bold">
+         120fps
+      </div>
+
+      ::examples-basic-animation-120fps
+      ::
+
+   </div>
+</div>
+<div class="p-2 text-xs text-gray-500 italic mt-2 block text-center">
+  The value of `delta` is 0.016 for 60fps and 0.008 for 120fps,  this difference ensures that the cube rotates at the same speed on both frame rates.
+</div>
+
+## Using `elapsed` 
+
+The `onBeforeRender` callback also provides an `elapsed` parameter, which represents the total time elapsed since the start of the animation. This can be useful for creating time-based animations like oscillations.
+
+```ts
+onBeforeRender(({ elapsed }) => {
+  if (cubeRef.value) {
+    cubeRef.value.position.y += Math.sin(elapsed) * 0.01
+  }
+})
+```
+
+::examples-basic-animation-elapsed
+::

+ 0 - 0
docs/content/4.cookbook/2.model-animation.md → docs/content/4.cookbook/3.model-animation.md


+ 238 - 0
docs/content/4.cookbook/4.advanced-gsap-animations.md

@@ -0,0 +1,238 @@
+---
+title: Advanced GSAP Animations
+description: Learn how to create complex animations using GSAP with TresJS
+thumbnail: /recipes/advance-animations-gsap.png
+---
+
+::examples-advanced-gsap-animations
+::
+
+This recipe demonstrates how to create sophisticated animations using GSAP (GreenSock Animation Platform) with TresJS for smooth, performance-optimized animations with advanced features like staggering and timeline control.
+
+::steps
+
+### Install GSAP
+
+First, install GSAP as a dependency in your project:
+
+```bash
+npm install gsap
+```
+
+### Import required modules
+
+Import GSAP and the necessary Vue composables. Use `shallowRef` for better performance with Three.js objects:
+
+```ts
+import { shallowRef, watch } from 'vue'
+import { OrbitControls } from '@tresjs/cientos'
+import gsap from 'gsap'
+```
+
+::tip
+Use `shallowRef` instead of `ref` to avoid unnecessary reactivity on Three.js objects, which improves performance.
+::
+
+### Create multiple objects to animate
+
+Set up an array of positions for multiple boxes that will be animated with stagger effects:
+
+```ts
+const boxesRef = shallowRef()
+const zs = []
+for (let z = -4.5; z <= 4.5; z++) {
+  zs.push(z)
+}
+```
+
+### Set up the scene structure
+
+Create a group of meshes that will be animated together:
+
+```vue
+<template>
+  <TresPerspectiveCamera :position="[-15, 10, 15]" />
+  <OrbitControls />
+  <TresGroup ref="boxesRef">
+    <TresMesh v-for="(z, i) of zs" :key="i" :position="[0, 0.5, z]">
+      <TresBoxGeometry />
+      <TresMeshNormalMaterial />
+    </TresMesh>
+  </TresGroup>
+  <TresGridHelper :args="[10, 10, 0x444444, 'teal']" />
+</template>
+```
+
+### Create the GSAP staggered animation
+
+Use Vue's `watch` to set up the animation when the template ref is available:
+
+```ts
+watch(boxesRef, () => {
+  if (!boxesRef.value) return
+
+  // Get positions and rotations for all boxes
+  const positions = Array.from(boxesRef.value.children).map(
+    (child) => child.position
+  )
+  const rotations = Array.from(boxesRef.value.children).map(
+    (child) => child.rotation
+  )
+
+  const animProperties = {
+    ease: 'power1.inOut',
+    duration: 1,
+    stagger: {
+      each: 0.25,
+      repeat: -1,
+      yoyo: true,
+    },
+  }
+
+  // Animate positions
+  gsap.to(positions, {
+    y: 2.5,
+    ...animProperties,
+  })
+
+  // Animate rotations
+  gsap.to(rotations, {
+    x: 2,
+    ...animProperties,
+  })
+})
+```
+
+### Understanding GSAP Stagger Options
+
+The `stagger` property provides powerful control over timing:
+
+```ts
+const animProperties = {
+  ease: 'power1.inOut',     // Easing function
+  duration: 1,              // Animation duration in seconds
+  stagger: {
+    each: 0.25,            // Delay between each object (0.25s)
+    repeat: -1,            // Infinite repeat (-1)
+    yoyo: true,            // Reverse on alternate cycles
+    from: 'start',         // Animation direction (start, center, end)
+  },
+}
+```
+
+::read-more{to="https://gsap.com/docs/v3/Staggers/"}
+Learn more about GSAP stagger options and configurations.
+::
+
+::
+
+## Advanced Techniques
+
+### Timeline Control
+
+::examples-advanced-gsap-timeline
+::
+
+For more complex sequences, use GSAP timelines to coordinate multiple animations:
+
+```vue [TimelineAnimation.vue]
+<script setup lang="ts">
+import { shallowRef, watch, onMounted } from 'vue'
+import gsap from 'gsap'
+
+const groupRef = shallowRef()
+const timeline = gsap.timeline({ paused: true })
+
+watch(groupRef, () => {
+  if (!groupRef.value) return
+
+  const children = Array.from(groupRef.value.children)
+
+  // Clear existing timeline
+  timeline.clear()
+
+  // Add multiple animations to timeline
+  timeline
+    .to(children.map(child => child.position), {
+      y: 3,
+      duration: 1,
+      ease: 'back.out(1.7)',
+      stagger: 0.1
+    })
+    .to(children.map(child => child.rotation), {
+      y: Math.PI * 2,
+      duration: 2,
+      ease: 'power2.inOut',
+      stagger: 0.1
+    }, '-=0.5') // Start 0.5s before previous animation ends
+    .to(children.map(child => child.scale), {
+      x: 1.5,
+      y: 1.5,
+      z: 1.5,
+      duration: 0.5,
+      ease: 'elastic.out(1, 0.3)',
+      stagger: 0.05
+    })
+})
+
+// Control functions
+const playAnimation = () => timeline.play()
+const pauseAnimation = () => timeline.pause()
+const reverseAnimation = () => timeline.reverse()
+const restartAnimation = () => timeline.restart()
+</script>
+```
+
+### Performance Optimization
+
+When animating many objects, optimize performance by:
+
+1. **Use `shallowRef`** for Three.js object references
+2. **Batch property access** to avoid repeated DOM queries
+3. **Use GSAP's `set()` method** for immediate property changes
+4. **Leverage hardware acceleration** with `force3D: true`
+
+```ts
+// Optimized animation setup
+const optimizedAnimation = () => {
+  // Get all properties at once
+  const meshes = Array.from(boxesRef.value.children)
+  const positions = meshes.map(mesh => mesh.position)
+  const rotations = meshes.map(mesh => mesh.rotation)
+
+  // Use force3D for hardware acceleration
+  gsap.to(positions, {
+    y: 2,
+    duration: 1,
+    force3D: true,
+    ease: 'power2.out'
+  })
+}
+```
+
+### Animation Events
+
+GSAP provides powerful callback events to sync with your application state:
+
+```ts
+gsap.to(positions, {
+  y: 2,
+  duration: 1,
+  stagger: 0.1,
+  onStart: () => console.log('Animation started'),
+  onComplete: () => console.log('Animation completed'),
+  onUpdate: function() {
+    // Called on every frame
+    console.log('Progress:', this.progress())
+  },
+  onRepeat: () => console.log('Animation repeated')
+})
+```
+
+::tip
+GSAP automatically handles frame rate optimization and provides better performance than manual animations for complex sequences.
+::
+
+::read-more{to="https://gsap.com/docs/v3/"}
+Explore the full GSAP documentation for advanced features and techniques.
+::

BIN
docs/public/recipes/advance-animations-gsap.png


BIN
docs/public/recipes/model-n-animations/basic.png