@canvas3/nuxt 0.1.32 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,68 +1,280 @@
1
- <!--
2
- Get your module up and running quickly.
1
+ # Canvas3 Nuxt Module
3
2
 
4
- Find and replace all on all files (CMD+SHIFT+F):
5
- - Name: My Module
6
- - Package name: my-module
7
- - Description: My new Nuxt module
8
- -->
3
+ 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.
9
4
 
10
- # Canvas3 Nuxt Module
5
+ [✨ Example use cases](https://canvas3.vercel.app/)
11
6
 
12
- Nuxt module for ThreeJS integration and additional features
7
+ ## Features
13
8
 
14
- [//]: # (- [✨ &nbsp;Release Notes]&#40;/CHANGELOG.md&#41;)
9
+ - 🎬 **Smooth scroll engine** – Custom lerp-based scroll with speed tracking, fixed-to-parent elements, and mobile breakpoint handling.
10
+ - 🖼 **WebGL image replacement** – Turn any `<img>` into a shader-driven ThreeJS mesh via the `v-canvas3-image` directive.
11
+ - 👀 **Scroll-triggered activation** – Activate/deactivate elements (and their linked meshes) as they enter/leave the viewport with `v-canvas3-scroll-action`.
12
+ - 🎨 **Custom shader support** – Bring your own vertex/fragment shaders per mesh, plus a global post-processing scroll shader pass.
13
+ - ⏱ **Animation scheduler** – Register callbacks that run conditionally on scroll, resize, mouse move, or custom render triggers, avoiding unnecessary renders.
14
+ - 🖱 **Mouse-reactive uniforms** – Automatic `uMouse` / `uMouseMovement` uniforms updated on mouse movement for interactive shaders.
15
+ - 📱 **Responsive & reduced-motion aware** – Built-in `isMobile` / `prefersReducedMotion` / `disabled` options to gracefully degrade.
16
+ - 🧩 **Global utility API** – `Canvas3` import exposes scene, camera, renderer, mesh, and scroll controls anywhere in your app.
17
+ - 🏗 **Layout-based setup** – Ships a ready-to-use `canvas3` Nuxt layout that wraps your page content with the scroll/canvas containers.
15
18
 
16
- [//]: # ( - [🏀 Online playground]&#40;https://stackblitz.com/github/your-org/my-module?file=playground%2Fapp.vue&#41;)
19
+ ## Quick Setup
17
20
 
18
- [//]: # ( - [📖 &nbsp;Documentation]&#40;https://example.com&#41;)
21
+ Install the module to your Nuxt application with one command:
19
22
 
20
- ## Features
23
+ ```bash
24
+ npx nuxi module add canvas3-nuxt
25
+ ```
21
26
 
22
- [✨ &nbsp;Example use cases](https://canvas3.vercel.app/)
27
+ That's it! You can now use the Canvas3 module in your Nuxt app
23
28
 
24
- <!-- Highlight some of the features your module provide here -->
29
+ ### 1. Use the `canvas3` layout
25
30
 
26
- [//]: # (- ⛰ &nbsp;Foo)
31
+ ```vue
32
+ <!-- pages/index.vue -->
33
+ <script setup>
34
+ definePageMeta({ layout: 'canvas3' })
35
+ </script>
36
+ ```
27
37
 
28
- [//]: # (- 🚠 &nbsp;Bar)
38
+ ### 2. Enable Canvas3 in the layout
29
39
 
30
- [//]: # (- 🌲 &nbsp;Baz)
40
+ The layout exposes `canvas3enabled` and `canvas3options` props. Enable it once your options (shaders, fonts, etc.) are ready:
31
41
 
32
- ## Quick Setup
42
+ ```vue
43
+ <!-- app.vue or a wrapping component -->
44
+ <template>
45
+ <NuxtLayout
46
+ :canvas3enabled="ready"
47
+ :canvas3options="canvas3Options"
48
+ @canvas3-ready="onReady"
49
+ >
50
+ <NuxtPage />
51
+ </NuxtLayout>
52
+ </template>
33
53
 
34
- Install the module to your Nuxt application with one command:
54
+ <script setup>
55
+ import { ref } from 'vue'
35
56
 
36
- ```bash
37
- npx nuxi module add canvas3-nuxt
57
+ const ready = ref(false)
58
+ const canvas3Options = {
59
+ shaders: {
60
+ default: { vertexShader: defaultVert, fragmentShader: defaultFrag },
61
+ scroll: { vertexShader: scrollVert, fragmentShader: scrollFrag },
62
+ },
63
+ activateMeshOptions: {
64
+ image: { uAniInImage: { value: 1, duration: 1, ease: 'power2.out' } },
65
+ },
66
+ canvasElement: { zIndex: -1 },
67
+ prefersReducedMotion: false,
68
+ isMobile: false,
69
+ disabled: false,
70
+ }
71
+
72
+ function onReady() {
73
+ ready.value = true
74
+ }
75
+
76
+ ready.value = true
77
+ </script>
38
78
  ```
39
- That's it! You can now use Canvas3 module in your Nuxt app ✨
79
+
80
+ ## Directives
81
+
82
+ ### `v-canvas3-image`
83
+
84
+ 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.
85
+
86
+ **Requirements:** must be applied to an `<img>` tag.
87
+
88
+ **Binding value** (`Canvas3ImageBinding`):
89
+
90
+ | Option | Type | Description |
91
+ |---|---|---|
92
+ | `shaderName` | `string` | Key of a custom shader registered in `canvas3Options.shaders`. Falls back to `shaders.default`. |
93
+ | `uniforms` | `MeshMaterialUniform` | Extra/overriding shader uniforms merged into the material on creation and reactively on update. |
94
+ | `activateMeshUniforms` | `MeshMaterialUniform` | Uniforms animated (0 → 1) when the mesh's linked scroll-action element becomes active/inactive. |
95
+
96
+ ```vue
97
+ <template>
98
+ <img
99
+ src="/images/hero.jpg"
100
+ alt="Hero"
101
+ v-canvas3-image="{
102
+ shaderName: 'wave',
103
+ uniforms: {
104
+ vectorVNoise: { value: [2, 2], duration: 0 },
105
+ },
106
+ activateMeshUniforms: {
107
+ uAniInImage: { value: 1, duration: 1.2, ease: 'power2.out' },
108
+ },
109
+ }"
110
+ >
111
+ </template>
112
+ ```
113
+
114
+ Behavior notes:
115
+ - Waits for the image to load (`complete`/`load` event) and for the canvas to be initiated before creating the mesh.
116
+ - Automatically re-creates the mesh if `src` changes.
117
+ - Cleans up (disposes geometry/material/texture, removes from scene) on unmount.
118
+ - Respects `canvas3options.disabled`, adding a `.reduced-motion` class instead of creating a mesh.
119
+
120
+ ### `v-canvas3-scroll-action`
121
+
122
+ 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`).
123
+
124
+ **Binding value** (`scrollActionBindOptionType`):
125
+
126
+ | Option | Type | Description |
127
+ |---|---|---|
128
+ | `activateOnce` | `boolean` | Once activated, the element is never deactivated again. |
129
+ | `trackOnly` | `boolean` | Skip class toggling and mesh activation; only fires callbacks/tracking. |
130
+ | `activeRange` | `number` | Fraction of viewport height used as the "active zone" (default `1`, i.e. full viewport). |
131
+ | `activeRangeMargin` | `number` | Extra pixel margin added to the active range for speed-tracking calculations. |
132
+ | `scrollSpeed` | `{ value: number }` | Applies a parallax translate based on scroll position, proportional to `value`. |
133
+ | `scrollSpeedSetTo` | `{ value: number, duration: number }` | Animates `scrollSpeed.value` to a new target over `duration` seconds via GSAP. |
134
+ | `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. |
135
+ | `activateCallback` | `(item) => void` | Called when the element becomes active. |
136
+ | `deactivateCallback` | `(item) => void` | Called when the element becomes inactive (unless `activateOnce`). |
137
+ | `onScrollCallback` | `(item, scrollSpeed, currentPosition) => void` | Called continuously while the element is in view and the page is scrolling. |
138
+
139
+ ```vue
140
+ <template>
141
+ <section
142
+ v-canvas3-scroll-action="{
143
+ activeRange: 0.8,
144
+ activateCallback: onSectionActive,
145
+ deactivateCallback: onSectionInactive,
146
+ }"
147
+ >
148
+ <img src="/images/panel.jpg" alt="Panel" v-canvas3-image="{}">
149
+ </section>
150
+ </template>
151
+
152
+ <script setup>
153
+ function onSectionActive(item) {
154
+ console.log('Section entered view', item.elNode)
155
+ }
156
+ function onSectionInactive(item) {
157
+ console.log('Section left view', item.elNode)
158
+ }
159
+ </script>
160
+ ```
161
+
162
+ **Parallax example:**
163
+
164
+ ```vue
165
+ <div v-canvas3-scroll-action="{ scrollSpeed: { value: 0.3 } }">
166
+ <div>Moves at 0.3x scroll speed</div>
167
+ </div>
168
+ ```
169
+
170
+ **Pin-to-parent example:**
171
+
172
+ ```vue
173
+ <div id="stickyContainer" style="height: 200vh;">
174
+ <div
175
+ v-canvas3-scroll-action="{
176
+ fixToParent: { containerId: 'stickyContainer', fixPosition: 0.5, margin: 0 },
177
+ }"
178
+ >
179
+ <div>Pinned child, centered at 50% viewport while parent is in view</div>
180
+ </div>
181
+ </div>
182
+ ```
183
+
184
+ ## Global `Canvas3` Utility
185
+
186
+ The module auto-imports a `Canvas3` utility (via Nuxt's `addImports`) that exposes scene/camera/renderer access and imperative controls, independent of the directives.
187
+
188
+ | Method | Description |
189
+ |---|---|
190
+ | `addMeshToScene(mesh)` | Adds a raw ThreeJS `Mesh` directly to the Canvas3 scene. |
191
+ | `getMeshFromSceneByName(name)` | Retrieves a scene object by its `name`. |
192
+ | `addImageAsMesh(imgEl, shaderName, meshId, uniforms, activateMeshUniforms)` | Lower-level API used internally by `v-canvas3-image`; can be called manually. |
193
+ | `removeMesh(id)` | Disposes and removes a mesh (and its material/texture) by id. |
194
+ | `getShaderMaterial(mesh)` | Type-casts and returns a mesh's `ShaderMaterial`. |
195
+ | `addAnimationToRender(name, setup)` | Registers a named animation callback (see below). |
196
+ | `removeAnimationFromRender(name)` | Unregisters a named animation callback. |
197
+ | `setAnimationsToRender(state)` | Globally toggles whether `onAnimationsRender` animations run. |
198
+ | `setAnimationToRender(name, state, id)` | Adds/removes an `animationId` driving a specific animation's `render` state. |
199
+ | `setRenderDisabled(state)` | Pauses/resumes the entire render loop. |
200
+ | `setMeshPositionsUpdate(state)` | Forces continuous recalculation of image mesh positions/sizes (e.g. during layout shifts). |
201
+ | `resizeOnChange()` | Recalculates canvas size, camera, and mesh positions — call on custom resize triggers. |
202
+ | `scrollTo(position, delay?)` | Smoothly animates scroll to a pixel position. |
203
+ | `scrollToTop(delay?)` | Smoothly animates scroll to the top. |
204
+ | `scrollToElBySelector(selector, delay?, margin?)` | Scrolls to an element matched by a CSS selector. |
205
+ | `getScrollPosition()` | Returns the current rendered scroll position. |
206
+ | `getScrollSpeed()` | Returns the current normalized scroll speed (0–1). |
207
+ | `getCamera()` | Returns the active `THREE.PerspectiveCamera`. |
208
+ | `getRenderer()` | Returns the active `THREE.WebGLRenderer`. |
209
+ | `getScene()` | Returns the active `THREE.Scene`. |
210
+
211
+ **Animation callback example:**
212
+
213
+ ```ts
214
+ import { Canvas3 } from '#imports'
215
+
216
+ Canvas3.addAnimationToRender('rotateLogo', {
217
+ onScroll: false,
218
+ onResize: false,
219
+ onMouseMove: true,
220
+ onAnimationsRender: false,
221
+ render: false,
222
+ animationCallback: () => {
223
+ const mesh = Canvas3.getMeshFromSceneByName('logoMesh')
224
+ if (mesh) mesh.rotation.y += 0.01
225
+ },
226
+ })
227
+
228
+ // later, to stop it:
229
+ Canvas3.removeAnimationFromRender('rotateLogo')
230
+ ```
231
+
232
+ **Manual scroll control example:**
233
+
234
+ ```ts
235
+ import { Canvas3 } from '#imports'
236
+
237
+ function goToSection() {
238
+ Canvas3.scrollToElBySelector('#contact', 0, -80)
239
+ }
240
+ ```
241
+
242
+ ## Options Reference (`Canvas3OptionsType`)
243
+
244
+ | Option | Type | Description |
245
+ |---|---|---|
246
+ | `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. |
247
+ | `activateMeshOptions.image` | `Record<string, MeshAnimation>` | Default in/out uniform animations applied to image meshes on activation (e.g. `uAniInImage`). |
248
+ | `canvasElement.zIndex` | `number` | z-index applied to the fixed WebGL canvas container. |
249
+ | `prefersReducedMotion` | `boolean` | Flag to adapt behavior for reduced-motion users. |
250
+ | `isMobile` | `boolean` | Flag to adapt behavior for mobile devices. |
251
+ | `disabled` | `boolean` | Globally disables mesh creation; `v-canvas3-image` falls back to a `.reduced-motion` class. |
40
252
 
41
253
  ## Contribution
42
254
 
43
255
  <details>
44
256
  <summary>Local development</summary>
45
-
257
+
46
258
  ```bash
47
259
  # Install dependencies
48
260
  npm install
49
-
261
+
50
262
  # Generate type stubs
51
263
  npm run dev:prepare
52
-
264
+
53
265
  # Develop with the playground
54
266
  npm run dev
55
-
267
+
56
268
  # Build the playground
57
269
  npm run dev:build
58
-
270
+
59
271
  # Run ESLint
60
272
  npm run lint
61
-
273
+
62
274
  # Run Vitest
63
275
  npm run test
64
276
  npm run test:watch
65
-
277
+
66
278
  # Release new version
67
279
  npm run release
68
280
  ```
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canvas-nuxt-layout",
3
3
  "configKey": "canvas3Layout",
4
- "version": "0.1.32",
4
+ "version": "0.1.33",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.0"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, addTemplate, addLayout, addComponent, addPlugin, addTypeTemplate, addImports } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addTemplate, addLayout, addPlugin, addTypeTemplate, addImports } from '@nuxt/kit';
2
2
 
3
3
  const module$1 = defineNuxtModule({
4
4
  meta: {
@@ -13,11 +13,6 @@ const module$1 = defineNuxtModule({
13
13
  write: true
14
14
  });
15
15
  addLayout(canvas3Layout, "canvas3");
16
- addComponent({
17
- name: "Canvas3Image",
18
- filePath: resolve("./runtime/components/canvas3-image.vue"),
19
- global: true
20
- });
21
16
  addPlugin(resolve("./runtime/plugins/directives"));
22
17
  addTypeTemplate({
23
18
  filename: "types/canvas3-directives.d.ts",
@@ -83,9 +83,7 @@ export type scrollActionBindOptionType = {
83
83
  deactivateCallback?: (item: ScrollActionType) => void;
84
84
  onScrollCallback?: (item: ScrollActionType, scrollSpeed: number, currentPosition: number) => void;
85
85
  activeRangeMargin?: number;
86
- activeRangeOrigin?: number;
87
86
  activeRange?: number;
88
- bidirectionalActivation?: boolean;
89
87
  };
90
88
  export type ScrollActionBinding = {
91
89
  elNode: HTMLElement;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canvas3/nuxt",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,76 +0,0 @@
1
- <template>
2
- <div class="webgl-img-wrapper">
3
- <img
4
- ref="image"
5
- class="webgl-img"
6
- :class="{ 'reduced-motion': reducedMotion ?? null }"
7
- :alt="props.options.alt"
8
- :src="props.options.src"
9
- :loading="props.options.loadStrategy"
10
- @load="addImageToCanvas"
11
- >
12
- </div>
13
- </template>
14
-
15
- <script setup>
16
- import { ref, computed, watch, onMounted, nextTick, onBeforeUnmount } from "vue";
17
- import { Canvas3 } from "#canvas3-nuxt/utils/canvas3/canvas3";
18
- const props = defineProps({
19
- options: { type: Object, required: false, default: () => ({
20
- alt: "",
21
- src: "",
22
- loadStrategy: "lazy",
23
- shaderName: void 0,
24
- uniforms: {},
25
- activateMeshUniforms: {}
26
- }) }
27
- });
28
- const reducedMotion = computed(() => Canvas3.options?.disabled ?? null);
29
- const image = ref();
30
- const generatedMeshId = "image_" + crypto.randomUUID();
31
- const imgAddedToCanvas = ref(false);
32
- const imgIsAdding = ref(false);
33
- const addImageToCanvas = async () => {
34
- if (imgAddedToCanvas.value || imgIsAdding.value) return;
35
- imgIsAdding.value = true;
36
- try {
37
- await nextTick();
38
- if (!image.value || Canvas3.canvasInitiated.value === false || Canvas3.options?.disabled || imgAddedToCanvas.value) return;
39
- if (image.value.naturalWidth === 0) return;
40
- image.value.dataset.meshId = generatedMeshId;
41
- await Canvas3.addImageAsMesh(
42
- image.value,
43
- props.options.shaderName ?? null,
44
- generatedMeshId,
45
- props.options.uniforms ?? {},
46
- props.options.activateMeshUniforms ?? {}
47
- );
48
- imgAddedToCanvas.value = true;
49
- } finally {
50
- imgIsAdding.value = false;
51
- }
52
- };
53
- watch(() => Canvas3.canvasInitiated.value, (ready) => {
54
- if (ready) addImageToCanvas();
55
- });
56
- watch(() => props.options.uniforms, (u) => {
57
- if (u)
58
- Canvas3.meshUniformsUpdate(generatedMeshId, u);
59
- }, { deep: true });
60
- watch(() => props.options.src, () => {
61
- imgAddedToCanvas.value = false;
62
- nextTick(() => {
63
- if (image.value?.complete) addImageToCanvas();
64
- });
65
- });
66
- onMounted(() => {
67
- if (image.value?.complete) addImageToCanvas();
68
- });
69
- onBeforeUnmount(() => {
70
- Canvas3.removeMesh(generatedMeshId);
71
- });
72
- </script>
73
-
74
- <style scoped>
75
- .webgl-img{max-height:100%;max-width:100%;opacity:0}.webgl-img.reduced-motion{height:100%;-o-object-fit:cover;object-fit:cover;opacity:1;width:100%}
76
- </style>
@@ -1,8 +0,0 @@
1
- import type { Canvas3ImageBindType } from '#canvas3-nuxt/types/types';
2
- type __VLS_Props = {
3
- options?: Canvas3ImageBindType;
4
- };
5
- declare const _default: import("vue").DefineComponent<__VLS_Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
6
- options: Canvas3ImageBindType;
7
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
- export default _default;
@@ -1,95 +0,0 @@
1
- <template>
2
- <span
3
- ref="htmlEl"
4
- class="text-wrapper"
5
- :class="{ 'reduced-motion': reducedMotion }"
6
- >
7
- <slot />
8
- </span>
9
- </template>
10
-
11
- <script setup>
12
- import { ref, watch, onMounted, onBeforeUnmount, defineProps, computed } from "vue";
13
- import { Canvas3 } from "#canvas3-nuxt/utils/canvas3/canvas3";
14
- const props = defineProps({
15
- options: {
16
- type: {
17
- shaderName: {
18
- type: String,
19
- default: null
20
- },
21
- theme: {
22
- type: String,
23
- default: "dark"
24
- },
25
- uniforms: {
26
- type: Object,
27
- default: () => {
28
- }
29
- },
30
- activateMeshUniforms: {
31
- type: Object,
32
- default: () => {
33
- }
34
- }
35
- },
36
- default: () => ({
37
- uniforms: {},
38
- activateMeshUniforms: {}
39
- })
40
- }
41
- });
42
- const reducedMotion = computed(() => {
43
- return Canvas3.options?.disabled ?? null;
44
- });
45
- const htmlEl = ref("htmlEl");
46
- const meshId = "text" + crypto.randomUUID();
47
- const getTrimmedText = () => {
48
- let innerHTML = htmlEl.value.innerHTML;
49
- if (innerHTML.includes("<!--]-->")) {
50
- const start = innerHTML.indexOf("<!--[-->") + 8;
51
- const end = innerHTML.indexOf("<!--]-->");
52
- innerHTML = innerHTML.slice(start, end);
53
- }
54
- return innerHTML;
55
- };
56
- watch(
57
- () => props.options.uniforms,
58
- (uniforms) => {
59
- if (Canvas3.options?.disabled)
60
- return;
61
- setTimeout(() => {
62
- Canvas3.meshUniformsUpdate(meshId, uniforms);
63
- }, 0);
64
- },
65
- { deep: true }
66
- );
67
- onMounted(() => {
68
- htmlEl.value.dataset.meshId = meshId;
69
- });
70
- onBeforeUnmount(() => {
71
- Canvas3.removeMesh(meshId);
72
- });
73
- watch(
74
- () => Canvas3.canvasInitiated.value,
75
- (newVal) => {
76
- if (Canvas3.options?.disabled || !newVal)
77
- return;
78
- setTimeout(() => {
79
- Canvas3.addTextAsMSDF(
80
- props.options.shaderName,
81
- meshId,
82
- htmlEl.value,
83
- getTrimmedText(),
84
- props.options.theme,
85
- props.options.uniforms,
86
- props.options.activateMeshUniforms
87
- );
88
- }, 0);
89
- }
90
- );
91
- </script>
92
-
93
- <style scoped>
94
- .text-wrapper{display:inline-block;opacity:0}.text-wrapper.reduced-motion{opacity:1}
95
- </style>
@@ -1,12 +0,0 @@
1
- declare var __VLS_1: {};
2
- type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_1) => any;
4
- };
5
- declare const __VLS_component: import("vue").DefineSetupFnComponent<Record<string, any>, {}, {}, Record<string, any> & {}, import("vue").PublicProps>;
6
- declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
7
- export default _default;
8
- type __VLS_WithSlots<T, S> = T & {
9
- new (): {
10
- $slots: S;
11
- };
12
- };