返回 slidev
useDrawings.ts
根目录 / packages / client / composables / useDrawings.ts
1 import type { Brush, Options as DrauuOptions, DrawingMode } from 'drauu'
2 import { createSharedComposable, useLocalStorage } from '@vueuse/core'
3 import { createDrauu } from 'drauu'
4 import { computed, markRaw, nextTick, reactive, ref, watch } from 'vue'
5 import { configs } from '../env'
6 import { isInputting } from '../state'
7 import { drawingState, onPatchDrawingState, patchDrawingState } from '../state/drawings'
8 import { useNav } from './useNav'
9
10 export const useDrawings = createSharedComposable(() => {
11 const { currentSlideNo, isPresenter } = useNav()
12
13 const brushColors = [
14 '#ff595e',
15 '#ffca3a',
16 '#8ac926',
17 '#1982c4',
18 '#6a4c93',
19 '#ffffff',
20 '#000000',
21 ]
22
23 const drawingEnabled = useLocalStorage('slidev-drawing-enabled', false)
24 const drawingPinned = useLocalStorage('slidev-drawing-pinned', false)
25 const brush = useLocalStorage<Brush>('slidev-drawing-brush', {
26 color: brushColors[0],
27 size: 4,
28 mode: 'stylus',
29 })
30
31 const isDrawing = ref(false)
32 const canUndo = ref(false)
33 const canRedo = ref(false)
34 const canClear = ref(false)
35
36 const _mode = ref<DrawingMode | 'arrow'>('stylus')
37 const syncUp = computed(() => configs.drawings.syncAll || isPresenter.value)
38 let disableDump = false
39
40 const drawingMode = computed({
41 get() {
42 return _mode.value
43 },
44 set(v: DrawingMode | 'arrow') {
45 _mode.value = v
46 if (v === 'arrow') {
47 // eslint-disable-next-line ts/no-use-before-define
48 drauu.mode = 'line'
49 brush.value.arrowEnd = true
50 }
51 else {
52 // eslint-disable-next-line ts/no-use-before-define
53 drauu.mode = v
54 brush.value.arrowEnd = false
55 }
56 },
57 })
58
59 const drauuOptions: DrauuOptions = reactive({
60 brush,
61 acceptsInputTypes: computed(() => (drawingEnabled.value && (!configs.drawings.presenterOnly || isPresenter.value)) ? undefined : ['pen' as const]),
62 coordinateTransform: false,
63 })
64
65 const drauu = markRaw(createDrauu(drauuOptions))
66
67 function clearDrauu() {
68 drauu.clear()
69 if (syncUp.value)
70 patchDrawingState(currentSlideNo.value, '')
71 }
72
73 function updateState() {
74 canRedo.value = drauu.canRedo()
75 canUndo.value = drauu.canUndo()
76 canClear.value = !!drauu.el?.children.length
77 }
78
79 function loadCanvas(page?: number) {
80 disableDump = true
81 const data = drawingState[page || currentSlideNo.value]
82 if (data != null)
83 drauu.load(data)
84 else
85 drauu.clear()
86 updateState()
87 disableDump = false
88 }
89
90 drauu.on('changed', () => {
91 updateState()
92 if (!disableDump) {
93 const dump = drauu.dump()
94 const key = currentSlideNo.value
95 if ((drawingState[key] || '') !== dump && syncUp.value)
96 patchDrawingState(key, drauu.dump())
97 }
98 })
99
100 onPatchDrawingState((state) => {
101 disableDump = true
102 if (state[currentSlideNo.value] != null)
103 drauu.load(state[currentSlideNo.value] || '')
104 disableDump = false
105 updateState()
106 })
107
108 nextTick(() => {
109 watch(currentSlideNo, () => {
110 if (!drauu.mounted)
111 return
112 loadCanvas()
113 }, { immediate: true })
114 })
115
116 drauu.on('start', () => isDrawing.value = true)
117 drauu.on('end', () => isDrawing.value = false)
118
119 window.addEventListener('keydown', (e) => {
120 if (!drawingEnabled.value || isInputting.value)
121 return
122
123 const noModifier = !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey
124 let handled = true
125 if (e.code === 'KeyZ' && (e.ctrlKey || e.metaKey)) {
126 if (e.shiftKey)
127 drauu.redo()
128 else
129 drauu.undo()
130 }
131 else if (e.code === 'Escape') {
132 drawingEnabled.value = false
133 }
134 else if (e.code === 'KeyL' && noModifier) {
135 drawingMode.value = 'line'
136 }
137 else if (e.code === 'KeyA' && noModifier) {
138 drawingMode.value = 'arrow'
139 }
140 else if (e.code === 'KeyS' && noModifier) {
141 drawingMode.value = 'stylus'
142 }
143 else if (e.code === 'KeyR' && noModifier) {
144 drawingMode.value = 'rectangle'
145 }
146 else if (e.code === 'KeyE' && noModifier) {
147 drawingMode.value = 'ellipse'
148 }
149 else if (e.code === 'KeyC' && noModifier) {
150 clearDrauu()
151 }
152 else if (e.code.startsWith('Digit') && noModifier && +e.code[5] <= brushColors.length) {
153 brush.value.color = brushColors[+e.code[5] - 1]
154 }
155 else {
156 handled = false
157 }
158
159 if (handled) {
160 e.preventDefault()
161 e.stopPropagation()
162 }
163 }, false)
164
165 return {
166 brush,
167 brushColors,
168 canClear,
169 canRedo,
170 canUndo,
171 clear: clearDrauu,
172 drauu,
173 drauuOptions,
174 drawingEnabled,
175 drawingMode,
176 drawingPinned,
177 drawingState,
178 isDrawing,
179 loadCanvas,
180 }
181 })
182
182 lines TYPESCRIPT