Vue 3 Integration Guide
This guide covers best practices for integrating Fuderu with Vue 3 applications, including composables, component patterns, and performance optimization.
Basic Setup
Installation
npm install fuderu
# or
yarn add fuderu
Basic Usage
<template>
<canvas ref="canvasRef" width="800" height="600" />
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { Canvas } from "fuderu";
const canvasRef = ref(null);
let painter = null;
onMounted(() => {
if (!canvasRef.value) return;
painter = new Canvas({
canvas: canvasRef.value,
document: {
width: 800,
height: 600,
},
});
});
onBeforeUnmount(() => {
if (painter) {
painter.destroy();
painter = null;
}
});
</script>
Composables
useCanvas Composable
Create a reusable composable for canvas management:
// composables/useCanvas.js
import { ref, onMounted, onBeforeUnmount } from "vue";
import { Canvas } from "fuderu";
export function useCanvas(options = {}) {
const canvasRef = ref(null);
const painter = ref(null);
const width = ref(options.width ?? 800);
const height = ref(options.height ?? 600);
const brushConfig = ref(options.brushConfig ?? {});
const initialize = () => {
if (!canvasRef.value) return;
painter.value = new Canvas({
canvas: canvasRef.value,
document: { width: width.value, height: height.value },
brush: brushConfig.value,
});
};
const reset = () => {
if (painter.value) {
painter.value.destroy();
painter.value = null;
initialize();
}
};
onMounted(initialize);
onBeforeUnmount(() => {
if (painter.value) {
painter.value.destroy();
painter.value = null;
}
});
// Expose brush control methods
const setBrushColor = (color) => {
brushConfig.value.color = color;
if (painter.value) {
painter.value.brush.config.color = color;
}
};
const setBrushSize = (size) => {
brushConfig.value.size = size;
if (painter.value) {
painter.value.brush.config.size = size;
}
};
return {
canvasRef,
painter,
width,
height,
brushConfig,
reset,
setBrushColor,
setBrushSize,
};
}
Usage in Components
<template>
<div>
<canvas ref="canvasRef" :width="width" :height="height" />
<div class="controls">
<label>
Color:
<input
type="color"
v-model="brushConfig.color"
@input="setBrushColor"
/>
</label>
<label>
Size:
<input
type="range"
min="1"
max="100"
v-model.number="brushConfig.size"
@input="setBrushSize"
/>
<span>{{ brushConfig.size }}px</span>
</label>
</div>
</div>
</template>
<script setup>
import { useCanvas } from "@/composables/useCanvas";
const { canvasRef, width, height, brushConfig, setBrushColor, setBrushSize } =
useCanvas({
width: 800,
height: 600,
brushConfig: {
color: "#000000",
size: 20,
},
});
</script>
Component Patterns
DrawingToolbar Component
<!-- DrawingToolbar.vue -->
<template>
<div class="toolbar">
<div class="toolbar-group">
<label>Color:</label>
<input type="color" v-model="color" @input="updateBrush" />
</div>
<div class="toolbar-group">
<label>Size:</label>
<input
type="range"
min="1"
max="100"
v-model.number="size"
@input="updateBrush"
/>
<span>{{ size }}px</span>
</div>
<div class="toolbar-group">
<label>Opacity:</label>
<input
type="range"
min="0"
max="1"
step="0.01"
v-model.number="opacity"
@input="updateBrush"
/>
<span>{{ (opacity * 100).toFixed(0) }}%</span>
</div>
<div class="toolbar-group">
<label>
<input type="checkbox" v-model="isEraser" @change="updateBrush" />
Eraser
</label>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
const props = defineProps({
color: { default: "#000000" },
size: { default: 20 },
opacity: { default: 1 },
isEraser: { default: false },
});
const emit = defineEmits(["update"]);
const updateBrush = () => {
emit("update", {
color: color,
size: size,
opacity: opacity,
eraser: isEraser,
});
};
</script>
LayerPanel Component
<!-- LayerPanel.vue -->
<template>
<div class="layer-panel">
<h3>Layers</h3>
<button @click="addLayer">+ Add Layer</button>
<div class="layer-list">
<div
v-for="layer in layers"
:key="layer.id"
class="layer-item"
:class="{ active: layer.id === activeLayerId }"
@click="selectLayer(layer.id)"
>
<div class="layer-controls">
<input
type="checkbox"
v-model="layer.visible"
@change="updateLayer(layer.id, { visible: layer.visible })"
/>
<input
type="text"
v-model="layer.name"
@input="updateLayer(layer.id, { name: layer.name })"
placeholder="Layer name"
/>
</div>
<div class="layer-properties">
<div>
<label>Opacity:</label>
<input
type="range"
min="0"
max="1"
step="0.01"
v-model.number="layer.opacity"
@input="updateLayer(layer.id, { opacity: layer.opacity })"
/>
</div>
<div>
<label>Blend Mode:</label>
<select
v-model="layer.blendMode"
@change="updateLayer(layer.id, { blendMode: layer.blendMode })"
>
<option value="source-over">Normal</option>
<option value="multiply">Multiply</option>
<option value="screen">Screen</option>
<option value="overlay">Overlay</option>
<!-- Add more blend modes as needed -->
</select>
</div>
</div>
<button
@click.stop="removeLayer(layer.id)"
class="remove-layer"
>
×
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
layers: {
type: Array,
required: true
},
activeLayerId: {
type: String,
required: true
}
});
const emit = defineEmits([
'select-layer',
'update-layer',
'add-layer',
'remove-layer'
]);
const selectLayer = (id) => {
emit('select-layer', id);
};
const updateLayer = (id, updates) => {
emit('update-layer', id, updates);
};
const addLayer = () => {
emit('add-layer');
};
const removeLayer = (id) => {
emit('remove-layer', id);
};
</script>
Advanced Patterns
Providing Canvas via Dependency Injection
// provideCanvas.js
import { inject, provide } from "vue";
import { Canvas } from "fuderu";
const CANVAS_KEY = Symbol("canvas");
export function provideCanvas(canvasOptions) {
const canvasRef = ref(null);
const painter = ref(null);
onMounted(() => {
if (!canvasRef.value) return;
painter.value = new Canvas({
canvas: canvasRef.value,
...canvasOptions,
});
});
onBeforeUnmount(() => {
if (painter.value) {
painter.value.destroy();
painter.value = null;
}
});
provide(CANVAS_KEY, {
canvasRef,
painter,
getCanvas: () => painter.value,
});
return canvasRef;
}
export function useCanvas() {
return inject(CANVAS_KEY);
}
Usage:
<!-- Parent.vue -->
<template>
<div>
<ChildCanvas />
<Toolbar :canvas="canvas" />
</div>
</template>
<script setup>
import { provideCanvas } from "@/providers/provideCanvas";
import ChildCanvas from "./ChildCanvas.vue";
import Toolbar from "./Toolbar.vue";
const canvas = provideCanvas({
width: 800,
height: 600,
brush: { color: "#000000", size: 20 },
});
</script>
<!-- ChildCanvas.vue -->
<script setup>
import { useCanvas } from "@/providers/provideCanvas";
const { canvasRef, painter } = useCanvas();
// Access painter.value for direct manipulation
</script>
<template>
<canvas ref="canvasRef" width="800" height="600" />
</template>
Performance Optimization
useRequestAnimationFrame Composable
// composables/useRequestAnimationFrame.js
import { ref } from "vue";
export function useRequestAnimationFrame(callback) {
const requestRef = ref(null);
const isRunning = ref(false);
const start = () => {
if (isRunning.value) return;
isRunning.value = true;
const loop = (time) => {
callback(time);
requestRef.value = requestAnimationFrame(loop);
};
requestRef.value = requestAnimationFrame(loop);
};
const stop = () => {
if (!isRunning.value) return;
isRunning.value = false;
if (requestRef.value) {
cancelAnimationFrame(requestRef.value);
requestRef.value = null;
}
};
// Auto-stop when component unmounts
onBeforeUnmount(stop);
return { start, stop };
}
Usage in canvas component:
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { Canvas } from "fuderu";
import { useRequestAnimationFrame } from "@/composables/useRequestAnimationFrame";
const canvasRef = ref(null);
let painter = null;
let needsRender = ref(false);
const { start: startRenderLoop, stop: stopRenderLoop } =
useRequestAnimationFrame(() => {
if (needsRender.value && painter) {
painter.render();
needsRender.value = false;
}
});
onMounted(() => {
if (!canvasRef.value) return;
painter = new Canvas({
canvas: canvasRef.value,
// ... options
});
startRenderLoop();
});
onBeforeUnmount(() => {
stopRenderLoop();
if (painter) {
painter.destroy();
}
});
// Instead of calling painter.render() directly:
const queueRender = () => {
needsRender.value = true;
};
// Call queueRender() when you need to update the canvas
</script>
Debouncing User Input
// composables/useDebounce.js
import { ref } from "vue";
import { watch } from "vue";
export function useDebounce(value, delay = 100) {
const debouncedValue = ref(value.value);
watch(
value,
(newValue) => {
if (timer.value) {
clearTimeout(timer.value);
}
timer.value = setTimeout(() => {
debouncedValue.value = newValue;
}, delay);
},
{ immediate: true },
);
const timer = ref(null);
// Cleanup
onBeforeUnmount(() => {
if (timer.value) {
clearTimeout(timer.value);
}
});
return debouncedValue;
}
Usage in brush controls:
<script setup>
import { ref } from "vue";
import { useDebounce } from "@/composables/useDebounce";
const brushSize = ref(20);
const debouncedBrushSize = useDebounce(brushSize, 150); // 150ms debounce
// Apply debounced value to canvas
watch(debouncedBrushSize, (newSize) => {
if (painter.value) {
painter.value.brush.config.size = newSize;
}
});
</script>
<template>
<input
type="range"
min="1"
max="100"
v-model.number="brushSize"
@input="onSizeChange"
/>
</template>
Handling Vue Specific Features
Vue 3 Reactivity with getters/setters
// When working with Vue's reactivity system, be careful with direct object mutation
import { reactive } from "vue";
function createBrushState() {
return reactive({
size: 20,
color: "#000000",
opacity: 1,
flow: 1,
});
}
// Better approach - use individual refs for primitive values
import { ref } from "vue";
function createBrushState() {
return {
size: ref(20),
color: ref("#000000"),
opacity: ref(1),
flow: ref(1),
};
}
// Then use .value when accessing/setting
// brushState.size.value = 24;
Teleport for Overlays
Use Teleport for popups/modals that should render outside the canvas container:
<template>
<div>
<canvas ref="canvasRef" width="800" height="600" />
<!-- Teleport color picker to body to avoid overflow issues -->
<Teleport to="body">
<div v-if="showColorPicker" class="color-picker">
<!-- Color picker implementation -->
<button @click="showColorPicker = false">Close</button>
</div>
</Teleport>
</div>
</template>
Server-Side Rendering (SSR) Considerations
Nuxt 3 Example
<!-- components/Canvas.client.vue (client-only) -->
<template>
<canvas ref="canvasRef" width="800" height="600" />
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { Canvas } from "fuderu";
const canvasRef = ref(null);
let painter = null;
onMounted(() => {
if (!canvasRef.value) return;
painter = new Canvas({
canvas: canvasRef.value,
// ... options
});
});
onBeforeUnmount(() => {
if (painter) {
painter.destroy();
}
});
</script>
Then use it in your page:
<template>
<div>
<h1>Drawing App</h1>
<ClientOnly>
<Canvas />
</ClientOnly>
</div>
</template>
Testing Vue Components
Unit Testing with Vue Test Utils and Vitest
import { mount } from "@vue/test-utils";
import { nextTick } from "vue";
import DrawingCanvas from "@/components/DrawingCanvas.vue";
import { Canvas } from "fuderu";
// Mock Canvas
vi.mock("fuderu", () => ({
Canvas: vi.fn().mockImplementation(() => ({
destroy: vi.fn(),
brush: {
config: { size: 20, color: "#000000" },
},
putPoint: vi.fn(),
render: vi.fn(),
clear: vi.fa(),
})),
}));
describe("DrawingCanvas.vue", () => {
let wrapper;
beforeEach(() => {
vi.clearAllMocks();
wrapper = mount(DrawingCanvas);
});
it("renders canvas element", () => {
expect(wrapper.find("canvas").exists()).toBe(true);
});
it("initializes canvas on mount", () => {
expect(Canvas).toHaveBeenCalledTimes(1);
const canvasInstance = Canvas.mock.instances[0];
expect(canvasInstance.canvas).toBeInstanceOf(HTMLCanvasElement);
});
it("handles color input changes", async () => {
const colorInput = wrapper.find('input[type="color"]');
await colorInput.setValue("#ff0000");
await nextTick();
// Verify the mock was called with updated color
// Implementation-specific assertions would go here
});
});
Best Practices Summary
Do:
- Use composition API for logic reuse - Create composables for canvas management
- Handle component lifecycle properly - Initialize in onMounted, cleanup in onBeforeUnmount
- Leverage Vue's reactivity system - Use refs and reactive for state management
- Implement performance optimizations - Use requestAnimationFrame loops, debouncing
- Handle SSR appropriately - Use client-only components or conditional rendering
- Use provide/inject for deep component communication - Avoid prop drilling
- Test thoroughly - Cover mounting, user interactions, and cleanup
Don't:
- Manipulate DOM directly outside lifecycle hooks - Can cause race conditions
- Forget to destroy Canvas instances - Leads to memory leaks
- Update canvas properties on every render - Causes unnecessary work
- Ignore Vue's reactivity caveats - Especially with object/array mutations
- Block the main thread - Use async patterns for heavy computations
- Assume immediate DOM availability - Always check refs before using
- Neglect touch events - Remember to handle touch alongside mouse events
Troubleshooting
Canvas Not Initializing
- Check if canvas ref is null during onMounted
- Verify browser compatibility with required APIs
- Ensure proper cleanup/recreation on route changes
- Look for console errors during initialization
Performance Issues
- Measure actual render times with performance.now()
- Check for unnecessary re-renders in Vue Devtools
- Verify you're not creating new Canvas instances excessively
- Look for synchronous layouts or forced reflows
- Consider worker-based approaches for heavy computations
State Not Updating
- Verify reactivity is properly set up (refs vs reactive)
- Check for mutation detection issues with objects/arrays
- Ensure you're using .value when accessing ref values
- Confirm watchers are firing as expected
- Look for accidental replacement of reactive objects
SSR Hydration Mismatches
- Ensure client and server render identical initial content
- Use client-only components for browser-specific features
- Handle window/document checks properly
- Consider using useState or similar patterns for client-only state
Resources
- Vue 3 Composition API Guide
- Vue 3 Teleport
- Vue 3 Provide/Inject
- Vue Testing Library
- Nuxt 3 Documentation
- Vue Performance Best Practices
With these patterns and practices, you can create robust, performant Fuderu integrations in Vue 3 applications that follow Vue best practices while leveraging the full capabilities of the drawing engine.