@canvas3/nuxt
v0.1.35
Published
Module for ThreeJS
Readme
Canvas3 Nuxt Module
A Nuxt module that integrates ThreeJS into your Nuxt app, providing a smooth-scroll engine, WebGL image effects, scroll-triggered animations, and a shader/animation API — all wired together through Vue directives and a global Canvas3 composable-like utility.
Features
- 🎬 Smooth scroll engine – Custom lerp-based scroll with speed tracking, fixed-to-parent elements, and mobile breakpoint handling.
- 🖼 WebGL image replacement – Turn any
<img>into a shader-driven ThreeJS mesh via thev-canvas3-imagedirective. - 👀 Scroll-triggered activation – Activate/deactivate elements (and their linked meshes) as they enter/leave the viewport with
v-canvas3-scroll-action. - 🎨 Custom shader support – Bring your own vertex/fragment shaders per mesh, plus a global post-processing scroll shader pass.
- ⏱ Animation scheduler – Register callbacks that run conditionally on scroll, resize, mouse move, or custom render triggers, avoiding unnecessary renders.
- 🖱 Mouse-reactive uniforms – Automatic
uMouse/uMouseMovementuniforms updated on mouse movement for interactive shaders. - 📱 Responsive & reduced-motion aware – Built-in
isMobile/prefersReducedMotion/disabledoptions to gracefully degrade. - 🧩 Global utility API –
Canvas3import exposes scene, camera, renderer, mesh, and scroll controls anywhere in your app. - 🏗 Layout-based setup – Ships a ready-to-use
canvas3Nuxt layout that wraps your page content with the scroll/canvas containers.
Quick Setup
Install the module to your Nuxt application with one command:
npx nuxi module add canvas3-nuxtThat's it! You can now use the Canvas3 module in your Nuxt app ✨
1. Use the canvas3 layout
<!-- pages/index.vue -->
<script setup>
definePageMeta({ layout: 'canvas3' })
</script>2. Enable Canvas3 in the layout
The layout exposes canvas3enabled and canvas3options props. Enable it once your options (shaders, fonts, etc.) are ready:
<!-- app.vue or a wrapping component -->
<template>
<NuxtLayout
:canvas3enabled="ready"
:canvas3options="canvas3Options"
@canvas3-ready="onReady"
>
<NuxtPage />
</NuxtLayout>
</template>
<script setup>
import { ref } from 'vue'
const ready = ref(false)
const canvas3Options = {
shaders: {
default: { vertexShader: defaultVert, fragmentShader: defaultFrag },
scroll: { vertexShader: scrollVert, fragmentShader: scrollFrag },
},
activateMeshOptions: {
image: { uAniInImage: { value: 1, duration: 1, ease: 'power2.out' } },
},
canvasElement: { zIndex: -1 },
prefersReducedMotion: false,
isMobile: false,
disabled: false,
}
function onReady() {
ready.value = true
}
ready.value = true
</script>Directives
v-canvas3-image
Converts an <img> element into a ThreeJS mesh rendered in sync with the DOM element's position, size, and scroll offset. The original image is hidden (opacity set to 0) and replaced visually by the WebGL mesh.
Requirements: must be applied to an <img> tag.
Binding value (Canvas3ImageBinding):
| Option | Type | Description |
|---|---|---|
| shaderName | string | Key of a custom shader registered in canvas3Options.shaders. Falls back to shaders.default. |
| uniforms | MeshMaterialUniform | Extra/overriding shader uniforms merged into the material on creation and reactively on update. |
| activateMeshUniforms | MeshMaterialUniform | Uniforms animated (0 → 1) when the mesh's linked scroll-action element becomes active/inactive. |
<template>
<img
src="/images/hero.jpg"
alt="Hero"
v-canvas3-image="{
shaderName: 'wave',
uniforms: {
vectorVNoise: { value: [2, 2], duration: 0 },
},
activateMeshUniforms: {
uAniInImage: { value: 1, duration: 1.2, ease: 'power2.out' },
},
}"
>
</template>Behavior notes:
- Waits for the image to load (
complete/loadevent) and for the canvas to be initiated before creating the mesh. - Automatically re-creates the mesh if
srcchanges. - Cleans up (disposes geometry/material/texture, removes from scene) on unmount.
- Respects
canvas3options.disabled, adding a.reduced-motionclass instead of creating a mesh.
v-canvas3-scroll-action
Marks an element to be tracked by the scroll engine. When the element enters/exits the configured viewport range, it toggles an active class, fires callbacks, and (optionally) activates any WebGL image meshes nested inside it (matched via data-mesh-id, set automatically by v-canvas3-image).
Binding value (scrollActionBindOptionType):
| Option | Type | Description |
|---|---|---|
| activateOnce | boolean | Once activated, the element is never deactivated again. |
| trackOnly | boolean | Skip class toggling and mesh activation; only fires callbacks/tracking. |
| activeRange | number | Fraction of viewport height used as the "active zone" (default 1, i.e. full viewport). |
| activeRangeMargin | number | Extra pixel margin added to the active range for speed-tracking calculations. |
| scrollSpeed | { value: number } | Applies a parallax translate based on scroll position, proportional to value. |
| scrollSpeedSetTo | { value: number, duration: number } | Animates scrollSpeed.value to a new target over duration seconds via GSAP. |
| fixToParent | { containerId: string, fixPosition: number, margin: number } | Pins the element's first child inside a container (by id) at a given viewport position while the container is in view. |
| activateCallback | (item) => void | Called when the element becomes active. |
| deactivateCallback | (item) => void | Called when the element becomes inactive (unless activateOnce). |
| onScrollCallback | (item, scrollSpeed, currentPosition) => void | Called continuously while the element is in view and the page is scrolling. |
<template>
<section
v-canvas3-scroll-action="{
activeRange: 0.8,
activateCallback: onSectionActive,
deactivateCallback: onSectionInactive,
}"
>
<img src="/images/panel.jpg" alt="Panel" v-canvas3-image="{}">
</section>
</template>
<script setup>
function onSectionActive(item) {
console.log('Section entered view', item.elNode)
}
function onSectionInactive(item) {
console.log('Section left view', item.elNode)
}
</script>Parallax example:
<div v-canvas3-scroll-action="{ scrollSpeed: { value: 0.3 } }">
<div>Moves at 0.3x scroll speed</div>
</div>Pin-to-parent example:
<div id="stickyContainer" style="height: 200vh;">
<div
v-canvas3-scroll-action="{
fixToParent: { containerId: 'stickyContainer', fixPosition: 0.5, margin: 0 },
}"
>
<div>Pinned child, centered at 50% viewport while parent is in view</div>
</div>
</div>Global Canvas3 Utility
The module auto-imports a Canvas3 utility (via Nuxt's addImports) that exposes scene/camera/renderer access and imperative controls, independent of the directives.
| Method | Description |
|---|---|
| addMeshToScene(mesh) | Adds a raw ThreeJS Mesh directly to the Canvas3 scene. |
| getMeshFromSceneByName(name) | Retrieves a scene object by its name. |
| addImageAsMesh(imgEl, shaderName, meshId, uniforms, activateMeshUniforms) | Lower-level API used internally by v-canvas3-image; can be called manually. |
| removeMesh(id) | Disposes and removes a mesh (and its material/texture) by id. |
| getShaderMaterial(mesh) | Type-casts and returns a mesh's ShaderMaterial. |
| addAnimationToRender(name, setup) | Registers a named animation callback (see below). |
| removeAnimationFromRender(name) | Unregisters a named animation callback. |
| setAnimationsToRender(state) | Globally toggles whether onAnimationsRender animations run. |
| setAnimationToRender(name, state, id) | Adds/removes an animationId driving a specific animation's render state. |
| setRenderDisabled(state) | Pauses/resumes the entire render loop. |
| setMeshPositionsUpdate(state) | Forces continuous recalculation of image mesh positions/sizes (e.g. during layout shifts). |
| resizeOnChange() | Recalculates canvas size, camera, and mesh positions — call on custom resize triggers. |
| scrollTo(position, delay?) | Smoothly animates scroll to a pixel position. |
| scrollToTop(delay?) | Smoothly animates scroll to the top. |
| scrollToElBySelector(selector, delay?, margin?) | Scrolls to an element matched by a CSS selector. |
| getScrollPosition() | Returns the current rendered scroll position. |
| getScrollSpeed() | Returns the current normalized scroll speed (0–1). |
| getCamera() | Returns the active THREE.PerspectiveCamera. |
| getRenderer() | Returns the active THREE.WebGLRenderer. |
| getScene() | Returns the active THREE.Scene. |
Animation callback example:
import { Canvas3 } from '#imports'
Canvas3.addAnimationToRender('rotateLogo', {
onScroll: false,
onResize: false,
onMouseMove: true,
onAnimationsRender: false,
render: false,
animationCallback: () => {
const mesh = Canvas3.getMeshFromSceneByName('logoMesh')
if (mesh) mesh.rotation.y += 0.01
},
})
// later, to stop it:
Canvas3.removeAnimationFromRender('rotateLogo')Manual scroll control example:
import { Canvas3 } from '#imports'
function goToSection() {
Canvas3.scrollToElBySelector('#contact', 0, -80)
}Options Reference (Canvas3OptionsType)
| Option | Type | Description |
|---|---|---|
| shaders | { default: Shader, scroll?: Shader, [name]: Shader } | Registry of vertex/fragment shader pairs. default is required; scroll enables the post-processing scroll-speed effect pass. |
| activateMeshOptions.image | Record<string, MeshAnimation> | Default in/out uniform animations applied to image meshes on activation (e.g. uAniInImage). |
| canvasElement.zIndex | number | z-index applied to the fixed WebGL canvas container. |
| prefersReducedMotion | boolean | Flag to adapt behavior for reduced-motion users. |
| isMobile | boolean | Flag to adapt behavior for mobile devices. |
| disabled | boolean | Globally disables mesh creation; v-canvas3-image falls back to a .reduced-motion class. |
Contribution
# Install dependencies
npm install
# Generate type stubs
npm run dev:prepare
# Develop with the playground
npm run dev
# Build the playground
npm run dev:build
# Run ESLint
npm run lint
# Run Vitest
npm run test
npm run test:watch
# Release new version
npm run release