Animated Curved Nav Using JavaScript

Animated Curved Nav Using JavaScript
Code Snippet:Curved Nav
Author: Taha Shashtari
Published: January 28, 2024
Last Updated: February 3, 2024
Downloads: 54
License: MIT
Edit Code online: CodeHim
Read More

This code creates a visually engaging curved navigation. The JavaScript-based animation smoothly transitions between navigation items. Helpful for adding an eye-catching interactive element to your webpage.

The core functionality involves smoothly transitioning between navigation items as the user interacts with the curved menu. This code helps create engaging and interactive user interfaces, adding a touch of creativity to website navigation.

How to Create Animated Curved Nav Using Javascript

1. First of all, include the Veloxi library by adding the following CDN link to your HTML’s <head> section:

<script src='https://unpkg.com/veloxi/dist/veloxi.min.js'></script>

2. Next, structure your HTML with a container for the curved navigation and placeholders for content and navigation items:

<div class="curved-nav">
  <div class="content-wrapper">
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      A
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      B
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      C
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      D
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      E
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      F
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      G
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      H
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      I
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      J
    </div>
  </div>
  <div class="nav-container-wrapper">
    <div class="nav-container">
      <div
        class="nav-items"
        data-vel-plugin="CurvedNavPlugin"
        data-vel-view="itemsContainer"
        data-vel-data-active-index="0"
      >
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          A
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          B
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          C
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          D
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          E
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          F
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          G
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          H
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          I
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          J
        </div>
      </div>
    </div>
  </div>
</div>

To add your own content and navigation items, modify the HTML structure within the content-wrapper and nav-container sections. Adjust the text, images, or other elements to suit your website’s needs.

3. Apply basic styling to create a visually appealing layout. Customize the colors, sizes, and other styling properties as needed. The following CSS code includes styles for the curved effect and responsiveness.

* {
  box-sizing: border-box;
}
body {
  margin: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100vh;
  background: whitesmoke;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
    Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  overflow: hidden;
  max-width: 100%;
}

.curved-nav {
  width: 100%;
  height: 100%;
  position: relative;
  will-change: transform, opacity;
}

.curved-nav::before {
  position: absolute;
  content: '';
  width: 100%;
  aspect-ratio: 1;
  border-radius: 20rem;
  background: radial-gradient(
      63.62% 69.52% at 100% 0%,
      rgba(247, 214, 98, 0.8) 0%,
      rgba(247, 214, 98, 0.168) 52.08%,
      rgba(247, 214, 98, 0) 100%
    ),
    linear-gradient(
      208.42deg,
      #f0422a 7.46%,
      rgba(240, 88, 42, 0.18) 42.58%,
      rgba(240, 101, 42, 0) 64.13%
    ),
    radial-gradient(
      114.51% 122.83% at 0% -15.36%,
      #e74f6a 0%,
      rgba(231, 79, 106, 0.22) 66.72%,
      rgba(231, 79, 106, 0) 100%
    ),
    linear-gradient(
      333.95deg,
      rgba(83, 208, 236, 0.85) -7.76%,
      rgba(83, 208, 236, 0.204) 19.67%,
      rgba(138, 137, 190, 0) 35.42%
    ),
    radial-gradient(
      109.15% 148.57% at 4.46% 98.44%,
      #1b3180 0%,
      rgba(27, 49, 128, 0) 100%
    ),
    linear-gradient(141.57deg, #4eadeb 19.08%, rgba(78, 173, 235, 0) 98.72%);
  background-blend-mode: normal, normal, normal, normal, multiply, normal;
  filter: blur(84px);
  will-change: transform;
  backface-visibility: hidden;
  transform: translate3d(0, 0, 0);
}

.nav-container {
  display: flex;
  width: 100%;
  max-width: 375px;
  touch-action: none;
  will-change: transform, opacity;
}

.nav-container-wrapper {
  overflow: hidden;
  padding: 40px 50px;
}

.nav-items {
  display: flex;
  width: 100%;
  --item-size: calc(375px / 5);
}

.nav-item {
  width: var(--item-size);
  height: var(--item-size);
  border-radius: 10px;
  background: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  cursor: pointer;
  color: #222;
  user-select: none;
  -webkit-user-select: none;
  flex-shrink: 0;
  will-change: transform, opacity;
  touch-action: none;
}

.curved-nav {
  width: 100%;
  max-width: 375px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.content-wrapper {
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: 10px;
  width: 250px;
  height: 250px;
  display: flex;
  align-items: center;
  justify-content: center;
  will-change: transform, opacity;
}

.content-item {
  color: white;
  font-size: 300px;
  line-height: 0.7;
  position: absolute;
  will-change: opacity;
}

@media (max-width: 500px) {
  .nav-items {
    --item-size: calc(375px / 6);
  }
  .curved-nav {
    overflow: hidden;
  }
}

4. Finally, add the following JavaScript function between <script> tag or external JS file. This code leverages the Veloxi library to create the animated curved navigation. Ensure the Veloxi library is properly loaded in your HTML file.

// Built with Veloxi: https://veloxijs.com/
// Inspired by: https://twitter.com/huseyingayiran/status/1711294397009080397

const {
    EventBus,
    Events,
    Plugin,
    createApp,
    DragEventPlugin,
    DragEvent
  } = Veloxi

const OFFSET = 10

class SetActiveIndexEvent {
  index
  constructor({ index }) {
    this.index = index
  }
}

class NavItem {
  view
  index
  container
  initialized = false

  constructor(view, index, container) {
    this.view = view
    this.index = index
    this.container = container
    this.view.position.animator.set('dynamic', { speed: 5 })
    this.view.scale.animator.set('dynamic', { speed: 5 })
  }

  init() {
    requestAnimationFrame(() => {
      this.enableTransition()
    })
    this.initialized = true
  }

  enableTransition() {
    this.view.styles.transition = '0.2s opacity linear'
  }

  update() {
    this.updatePosition()
    this.updateOpacity()
    this.updateScale()
  }

  updateWithOffset(offset) {
    const targetSlot = offset > 0 ? this.nextSlot : this.previousSlot
    const percentage = Math.abs(offset / this.container.stepSize)
    this.updatePositionWithOffset(offset, targetSlot, percentage)
    this.updateOpacityWithPercentage(targetSlot, percentage)
    this.updateScaleWithPercentage(targetSlot, percentage)
  }

  updatePositionWithOffset(offset, targetSlot, percentage) {
    const x = this.currentPosition.x
    let y = this.currentPosition.y
    switch (targetSlot) {
      case slotIndex.ACTIVE:
        y -= 10 * percentage
        break
      case slotIndex.FIRST_NEXT:
      case slotIndex.FIRST_PREVIOUS:
        const fromActive = this.slotIndex === slotIndex.ACTIVE
        y += 25 * (fromActive ? 1 : -1) * percentage
        break
      case slotIndex.SECOND_NEXT:
      case slotIndex.SECOND_PREVIOUS:
        const fromFirst = [
          slotIndex.FIRST_NEXT,
          slotIndex.FIRST_PREVIOUS
        ].includes(this.slotIndex)
        y += 40 * percentage * (fromFirst ? 1 : -1)
        break
      default:
        const fromSecond = [
          slotIndex.SECOND_NEXT,
          slotIndex.SECOND_PREVIOUS
        ].includes(this.slotIndex)
        y += 40 * percentage * (fromSecond ? 1 : -1)
    }
    this.view.position.set({ x: x + offset, y })
  }

  updateScaleWithPercentage(targetSlot, percentage) {
    let scale = this.currentScale

    switch (targetSlot) {
      case slotIndex.ACTIVE:
        scale += 0.1 * percentage
        break
      case slotIndex.FIRST_NEXT:
      case slotIndex.FIRST_PREVIOUS:
        const fromActive = this.slotIndex === slotIndex.ACTIVE
        if (fromActive) {
          scale -= 0.1 * percentage
        }
        break
    }
    this.view.scale.set({ x: scale, y: scale })
  }

  updateOpacityWithPercentage(targetSlot, percentage) {
    let opacity = this.currentOpacity
    switch (targetSlot) {
      case slotIndex.ACTIVE:
        opacity += 0.2 * percentage
        break
      case slotIndex.FIRST_PREVIOUS:
      case slotIndex.FIRST_NEXT:
        const fromActive = this.slotIndex === slotIndex.ACTIVE
        opacity += 0.2 * percentage * (fromActive ? -1 : 1)
        break
      case slotIndex.SECOND_PREVIOUS:
      case slotIndex.SECOND_NEXT:
        const fromFirst = [
          slotIndex.FIRST_NEXT,
          slotIndex.FIRST_PREVIOUS
        ].includes(this.slotIndex)
        if (!fromFirst) {
          if (
            this.firstItemIndexInStart === this.index ||
            this.firstItemIndexInEnd === this.index
          ) {
            opacity += 0.2 * percentage * (fromFirst ? -1 : 1)
          } else {
            opacity = 0
          }
        } else {
          opacity += 0.2 * percentage * (fromFirst ? -1 : 1)
        }
        break
      default:
        const fromSecond = [
          slotIndex.SECOND_NEXT,
          slotIndex.SECOND_PREVIOUS
        ].includes(this.slotIndex)
        if (fromSecond) {
          opacity += 0.4 * percentage * (fromSecond ? -1 : 1)
        } else {
          opacity = 0
        }
    }
    this.view.styles.opacity = `${opacity}`
  }

  updatePosition() {
    const shouldAnimate = this.container.shouldAnimateMap.get(this.index)
    const x = this.slotPosition.x
    let y = this.slotPosition.y
    switch (this.slotIndex) {
      case slotIndex.ACTIVE:
        break
      case slotIndex.FIRST_NEXT:
      case slotIndex.FIRST_PREVIOUS:
        y += 10
        break
      case slotIndex.SECOND_NEXT:
      case slotIndex.SECOND_PREVIOUS:
        y += 40
        break
      default:
        y += 100
    }
    this.view.position.set({ x, y }, shouldAnimate)
  }

  get currentPosition() {
    const x = this.slotPosition.x
    let y = this.slotPosition.y
    switch (this.slotIndex) {
      case slotIndex.ACTIVE:
        break
      case slotIndex.FIRST_NEXT:
      case slotIndex.FIRST_PREVIOUS:
        y += 10
        break
      case slotIndex.SECOND_NEXT:
      case slotIndex.SECOND_PREVIOUS:
        y += 40
        break
      default:
        y += 80
    }
    return { x, y }
  }

  get currentOpacity() {
    let opacity = 0
    switch (this.slotIndex) {
      case slotIndex.ACTIVE:
        opacity = 1
        break
      case slotIndex.FIRST_PREVIOUS:
      case slotIndex.FIRST_NEXT:
        opacity = 0.425
        break
      case slotIndex.SECOND_PREVIOUS:
      case slotIndex.SECOND_NEXT:
        opacity = 0.2
        break
    }
    return opacity
  }

  get currentScale() {
    let scale = 0.75
    switch (this.slotIndex) {
      case slotIndex.ACTIVE:
        scale = 1
        break
      case slotIndex.FIRST_PREVIOUS:
      case slotIndex.FIRST_NEXT:
        scale = 0.75
        break
      case slotIndex.SECOND_PREVIOUS:
      case slotIndex.SECOND_NEXT:
        scale = 0.75
        break
    }
    return scale
  }

  updateOpacity() {
    let opacity = 0
    switch (this.slotIndex) {
      case slotIndex.ACTIVE:
        opacity = 1
        break
      case slotIndex.FIRST_PREVIOUS:
      case slotIndex.FIRST_NEXT:
        opacity = 0.425
        break
      case slotIndex.SECOND_PREVIOUS:
      case slotIndex.SECOND_NEXT:
        opacity = 0.2
        break
    }

    this.view.styles.opacity = `${opacity}`
  }

  updateScale() {
    let scale = 0.75
    switch (this.slotIndex) {
      case slotIndex.ACTIVE:
        scale = 1
        break
      case slotIndex.FIRST_PREVIOUS:
      case slotIndex.FIRST_NEXT:
        scale = 0.75
        break
      case slotIndex.SECOND_PREVIOUS:
      case slotIndex.SECOND_NEXT:
        scale = 0.75
        break
    }
    this.view.scale.set({ x: scale, y: scale }, this.initialized)
  }

  get nextSlot() {
    return wrapAround(this.slotIndex, Object.keys(slotIndex).length, 1)
  }

  get previousSlot() {
    return wrapAround(this.slotIndex, Object.keys(slotIndex).length, -1)
  }

  get slotPosition() {
    return this.container.getSlotPositionForItemIndex(this.index)
  }

  get activeIndex() {
    return this.container.activeIndex
  }

  get slotIndex() {
    return this.container.getSlotForIndex(this.index)
  }

  getItemIndexForSlot(slot) {
    return this.container.getItemIndeciesForSlot(slot)[0]
  }

  get firstItemIndexInStart() {
    const secondPreviousIndex = this.getItemIndexForSlot(
      slotIndex.SECOND_PREVIOUS
    )
    return wrapAround(secondPreviousIndex, this.container.totalItems, -1)
  }

  get firstItemIndexInEnd() {
    const secondNextItemIndex = this.getItemIndexForSlot(
      slotIndex.SECOND_NEXT
    )
    return wrapAround(secondNextItemIndex, this.container.totalItems, 1)
  }
}

const slotIndex = {
  START: 0,
  SECOND_PREVIOUS: 1,
  FIRST_PREVIOUS: 2,
  ACTIVE: 3,
  FIRST_NEXT: 4,
  SECOND_NEXT: 5,
  END: 6
}

function wrapAround(current, total, amount) {
  return (current + total + amount) % total
}

function flipMap(map) {
  const flippedMap = new Map()

  for (const [key, value] of map) {
    if (!flippedMap.has(value)) {
      flippedMap.set(value, [key])
    } else {
      flippedMap.get(value).push(key)
    }
  }

  return flippedMap
}

class NavContainer {
  plugin
  view
  activeIndex
  shouldAnimateMap = new Map()

  _itemIndexSlotMap = new Map()
  _slotItemIndexMap = new Map()

  constructor(plugin, view) {
    this.plugin = plugin
    this.view = view
    this.activeIndex = this.view.data.activeIndex
      ? parseInt(this.view.data.activeIndex)
      : 0

    for (let index = 0; index < this.totalItems; index++) {
      this.shouldAnimateMap.set(index, false)
    }
  }

  get stepSize() {
    return this.plugin.stepSize
  }

  get itemSize() {
    return this.plugin.itemSize
  }

  get totalItems() {
    return this.plugin.totalItems
  }

  updateWithOffset(offset) {
    const steps = Math.floor(Math.abs(offset / this.stepSize))
    const queue = []
    let currentIndex = this.activeIndex
    for (let step = 0; step < steps; step++) {
      const stepDirection = offset < 0 ? 1 : -1
      const itemIndex = wrapAround(
        currentIndex,
        this.totalItems,
        stepDirection
      )
      queue.push(itemIndex)
      currentIndex = itemIndex
    }
    queue.forEach((itemIndex, index) => {
      setTimeout(() => {
        this.plugin.setActiveIndex(itemIndex)
      }, 100 * index)
    })
  }

  setActiveIndex(newActiveIndex) {
    const previousItemIndexSlot = this.itemIndexSlotMap
    this.activeIndex = newActiveIndex
    this.setItemIndexSlotMap()
    const newItemIndexSlot = this.itemIndexSlotMap

    const visibleSlots = [
      slotIndex.ACTIVE,
      slotIndex.FIRST_PREVIOUS,
      slotIndex.SECOND_PREVIOUS,
      slotIndex.FIRST_NEXT,
      slotIndex.SECOND_NEXT
    ]
    for (let index = 0; index < this.totalItems; index++) {
      const shouldAnimate =
        visibleSlots.includes(previousItemIndexSlot.get(index)) ||
        visibleSlots.includes(newItemIndexSlot.get(index))
      this.shouldAnimateMap.set(index, shouldAnimate)
    }
  }

  get slotItemIndexMap() {
    return this._slotItemIndexMap
  }

  get itemIndexSlotMap() {
    return this._itemIndexSlotMap
  }

  getSlotForIndex(itemIndex) {
    return this.itemIndexSlotMap.get(itemIndex)
  }

  getItemIndeciesForSlot(slot) {
    return this.slotItemIndexMap.get(slot)
  }

  setItemIndexSlotMap() {
    this._itemIndexSlotMap.clear()
    const activeIndex = this.activeIndex

    const firstPreviousIndex = wrapAround(activeIndex, this.totalItems, -1)
    const secondPreviousIndex = wrapAround(activeIndex, this.totalItems, -2)
    const firstNextIndex = wrapAround(activeIndex, this.totalItems, 1)
    const secondNextIndex = wrapAround(activeIndex, this.totalItems, 2)

    for (let index = 0; index < this.totalItems; index++) {
      if (index === activeIndex) {
        this._itemIndexSlotMap.set(index, slotIndex.ACTIVE)
        continue
      }

      if (index === firstPreviousIndex) {
        this._itemIndexSlotMap.set(index, slotIndex.FIRST_PREVIOUS)
        continue
      }

      if (index === secondPreviousIndex) {
        this._itemIndexSlotMap.set(index, slotIndex.SECOND_PREVIOUS)
        continue
      }

      if (index === firstNextIndex) {
        this._itemIndexSlotMap.set(index, slotIndex.FIRST_NEXT)
        continue
      }

      if (index === secondNextIndex) {
        this._itemIndexSlotMap.set(index, slotIndex.SECOND_NEXT)
        continue
      }

      if (index === wrapAround(secondNextIndex, this.totalItems, 1)) {
        this._itemIndexSlotMap.set(index, slotIndex.END)
        continue
      }

      if (index === wrapAround(secondNextIndex, this.totalItems, 2)) {
        this._itemIndexSlotMap.set(index, slotIndex.END)
        continue
      }

      if (index === wrapAround(secondPreviousIndex, this.totalItems, -1)) {
        this._itemIndexSlotMap.set(index, slotIndex.START)
        continue
      }

      if (index === wrapAround(secondPreviousIndex, this.totalItems, -2)) {
        this._itemIndexSlotMap.set(index, slotIndex.START)
        continue
      }

      if (index > activeIndex) {
        this._itemIndexSlotMap.set(index, slotIndex.END)
        continue
      }

      if (index < activeIndex) {
        this._itemIndexSlotMap.set(index, slotIndex.START)
        continue
      }
    }
    this._slotItemIndexMap = flipMap(this.itemIndexSlotMap)
  }

  getSlotPositionForItemIndex(index) {
    const slot = this.itemIndexSlotMap.get(index)
    return this.slotPositions[slot]
  }

  get indicatorPosition() {
    return {
      x: this.view.position.x + this.view.size.width / 2,
      y: this.view.position.y + this.view.size.height / 2
    }
  }

  get slotPositions() {
    const result = []
    // Active Slot
    result[slotIndex.ACTIVE] = {
      x: this.indicatorPosition.x - this.itemSize / 2,
      y: this.indicatorPosition.y - this.itemSize / 2
    }

    result[slotIndex.FIRST_PREVIOUS] = {
      x: result[slotIndex.ACTIVE].x - OFFSET - this.itemSize,
      y: result[slotIndex.ACTIVE].y
    }

    result[slotIndex.SECOND_PREVIOUS] = {
      x: result[slotIndex.FIRST_PREVIOUS].x - this.itemSize,
      y: result[slotIndex.FIRST_PREVIOUS].y
    }

    result[slotIndex.START] = {
      x: result[slotIndex.SECOND_PREVIOUS].x - this.itemSize,
      y: result[slotIndex.SECOND_PREVIOUS].y
    }

    result[slotIndex.FIRST_NEXT] = {
      x: result[slotIndex.ACTIVE].x + OFFSET + this.itemSize,
      y: result[slotIndex.ACTIVE].y
    }

    result[slotIndex.SECOND_NEXT] = {
      x: result[slotIndex.FIRST_NEXT].x + this.itemSize,
      y: result[slotIndex.FIRST_NEXT].y
    }

    result[slotIndex.END] = {
      x: result[slotIndex.SECOND_NEXT].x + this.itemSize,
      y: result[slotIndex.SECOND_NEXT].y
    }

    return result
  }
}

class ContentItem {
  view
  index
  navItemsContainer

  constructor(view, index, navItemsContainer) {
    this.view = view
    this.index = index
    this.navItemsContainer = navItemsContainer
    this.init()
    this.update()
  }

  init() {
    requestAnimationFrame(() => {
      this.enableTransition()
    })
  }

  enableTransition() {
    this.view.styles.transition = '0.2s ease-in-out opacity'
  }

  update() {
    if (this.index !== this.activeIndex) {
      this.hide()
    } else {
      this.show()
    }
  }

  hide() {
    this.view.styles.opacity = '0'
  }

  show() {
    this.view.styles.opacity = '1'
  }

  get activeIndex() {
    return this.navItemsContainer.activeIndex
  }
}

class CurvedNavPlugin extends Plugin {
  static pluginName = 'CurvedNavPlugin'

  items
  itemsContainer

  contentItems

  lastDragOffset = 0
  isDragging = false

  dragEventPlugin = this.useEventPlugin(DragEventPlugin)

  setup() {
    const itemViews = this.getViews('item')
    const itemsContainerView = this.getView('itemsContainer')
    this.itemsContainer = new NavContainer(this, itemsContainerView)

    this.dragEventPlugin.addView(itemsContainerView)
    this.dragEventPlugin.on(DragEvent, this.onDrag.bind(this))

    this.items = itemViews.map(
      (view, index) => new NavItem(view, index, this.itemsContainer)
    )

    this.itemsContainer.setItemIndexSlotMap()

    this.items.forEach((item) => {
      item.update()
      item.init()
    })

    this.contentItems = this.getViews('contentItem').map(
      (view, index) => new ContentItem(view, index, this.itemsContainer)
    )
  }

  onDrag(event) {
    if (event.isDragging) {
      this.isDragging = true
    } else {
      requestAnimationFrame(() => {
        this.isDragging = false
      })
    }
    if (event.isDragging) {
      const diff = Math.abs(event.previousX - event.x)
      const damping = diff > 50 ? 0.2 : 1
      const offset = damping * (event.width + this.lastDragOffset * -1)
      if (Math.abs(offset) >= this.stepSize) {
        this.lastDragOffset = event.width
      }
      this.itemsContainer.updateWithOffset(offset)
      this.items.forEach((item) => {
        item.updateWithOffset(offset)
      })
    } else {
      this.lastDragOffset = 0
      this.items.forEach((item) => {
        item.updateWithOffset(0)
      })
    }
  }

  onDataChanged(data) {
    if (data.dataName === 'activeIndex') {
      const activeIndex = parseInt(data.dataValue)
      this.itemsContainer.setActiveIndex(activeIndex)
      this.items.forEach((item) => item.update())
      this.contentItems.forEach((item) => item.update())
    }
  }

  subscribeToEvents(eventBus) {
    eventBus.subscribeToEvent(Events.PointerClickEvent, ({ target }) => {
      if (this.isDragging) return
      this.items.forEach((item, index) => {
        if (target === item.view.element) {
          this.setActiveIndex(index)
        }
      })
    })
  }

  setActiveIndex(index) {
    this.emit(SetActiveIndexEvent, { index })
  }

  get totalItems() {
    return this.getViews('item').length
  }

  get itemSize() {
    return this.items[0].view.size.width
  }

  get stepSize() {
    return this.itemSize + OFFSET
  }
}

const app = createApp()
app.addPlugin(CurvedNavPlugin)
app.run()

const containerNav = document.querySelector(
  '[data-vel-view="itemsContainer"]'
)
app.onPluginEvent(CurvedNavPlugin, SetActiveIndexEvent, ({ index }) => {
  containerNav.dataset.velDataActiveIndex = `${index}`
})

That’s all! hopefully, you have successfully created an animated curved nav using JavaScript. If you have any questions or suggestions, feel free to comment below.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

About CodeHim

Free Web Design Code & Scripts - CodeHim is one of the BEST developer websites that provide web designers and developers with a simple way to preview and download a variety of free code & scripts. All codes published on CodeHim are open source, distributed under OSD-compliant license which grants all the rights to use, study, change and share the software in modified and unmodified form. Before publishing, we test and review each code snippet to avoid errors, but we cannot warrant the full correctness of all content. All trademarks, trade names, logos, and icons are the property of their respective owners... find out more...