Skip to content

Commit

Permalink
feat: add composable useEditor
Browse files Browse the repository at this point in the history
  • Loading branch information
robertrosman committed Jun 3, 2024
1 parent d7e29dd commit 6fa1854
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 123 deletions.
121 changes: 9 additions & 112 deletions src/components/VpEditor.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
<script setup lang="ts">
import { computed, onMounted, ref, toRef } from 'vue'
import { onMounted, ref, toRef } from 'vue'
import type { DrawEvent, SaveParameters, Settings, Shape, Tool } from '../types'
import VpImage from './VpImage.vue'
import VpToolbar from './VpToolbar.vue'
import { useDraw } from '@/composables/useDraw'
import { randomId } from '@/utils/randomId'
import { defaultSettings } from '@/utils/createSettings'
import { useEditor } from '@/composables/useEditor'
const emit = defineEmits<{
save: [event: SaveParameters]
Expand All @@ -29,86 +27,17 @@ const props = withDefaults(
)
const history = defineModel<Shape[]>('history', { default: [] })
const redoHistory = ref<Shape[]>([])
const vpImageRef = ref()
const activeShape = ref<Shape | undefined>()
const temporaryTool = ref<string>()
const vpImage = ref()
function getActiveTool() {
return props.tools?.find((tool) => tool.type === (temporaryTool.value ?? settings.value.tool))
}
const drawEvent = computed<DrawEvent>(() => ({
settings: settings.value,
activeShape: activeShape.value,
id: activeShape.value?.id ?? randomId(),
isDrawing: isDrawing.value,
const { activeShape, undo, redo, save, reset } = useEditor({
vpImage,
tools: props.tools,
posStart,
posEnd,
left: left.value,
right: right.value,
top: top.value,
bottom: bottom.value,
history: toRef(history),
settings: toRef(settings),
width: props.width,
height: props.height,
x: x.value,
y: y.value,
minX: minX.value,
maxX: maxX.value,
minY: minY.value,
maxY: maxY.value,
absoluteX: absoluteX.value,
absoluteY: absoluteY.value
}))
const {
x,
y,
minX,
minY,
maxX,
maxY,
top,
left,
bottom,
right,
posStart,
posEnd,
width,
height,
isDrawing,
absoluteX,
absoluteY
} = useDraw({
container: vpImageRef,
width: props.width,
height: props.height,
onDrawStart() {
temporaryTool.value = document.elementsFromPoint(absoluteX.value, absoluteY.value)?.[0]
?.getAttribute('class')?.split(' ')
.find(c => c.startsWith('use-tool-'))
?.substring(9)
activeShape.value = getActiveTool()?.onDrawStart?.(drawEvent.value) ?? activeShape.value
emit('drawStart', drawEvent.value)
},
onDraw() {
activeShape.value = getActiveTool()?.onDraw?.(drawEvent.value) ?? activeShape.value
emit('draw', drawEvent.value)
},
async onDrawEnd() {
activeShape.value = getActiveTool()?.onDrawEnd
? await getActiveTool()?.onDrawEnd?.(drawEvent.value)
: activeShape.value
temporaryTool.value = undefined
emit('drawEnd', drawEvent.value)
if (activeShape.value) {
history.value.push(activeShape.value)
redoHistory.value = []
activeShape.value = undefined
}
}
emit
})
onMounted(() => {
Expand All @@ -117,44 +46,12 @@ onMounted(() => {
}
})
function undo() {
if (history.value.length) {
redoHistory.value.push(...history.value.slice(-1))
history.value = history.value.slice(0, -1)
}
}
function redo() {
if (redoHistory.value.length) {
history.value.push(...redoHistory.value.slice(-1))
redoHistory.value = redoHistory.value.slice(0, -1)
}
}
function save() {
const svg = vpImageRef.value.$refs.svg
if (!svg) {
throw new Error("Couldn't find the svg")
}
emit('save', { svg, tools: props.tools, history: history.value })
}
async function reset() {
redoHistory.value = history.value.reverse()
history.value = []
const shapes = await Promise.all(
props.tools
.filter((tool) => 'onInitialize' in tool)
.flatMap(async (tool) => await tool.onInitialize?.({ tools: props.tools, settings: toRef(settings), history: toRef(history) }))
)
history.value = [...shapes.filter(Boolean), ...history.value]
emit('reset')
}
</script>

<template>
<div class="vue-paint vp-editor" :class="`active-tool-${settings.tool}`">
<vp-image ref="vpImageRef" :tools :activeShape :history :width="width" :height="height" />
<vp-image ref="vpImage" :tools :activeShape :history :width :height />

<slot name="toolbar" :undo :save :reset :settings>
<vp-toolbar v-model:settings="settings" @undo="undo" @redo="redo" @save="save" @reset="reset" :tools />
Expand Down
15 changes: 5 additions & 10 deletions src/composables/useDraw.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { type Position, usePointer, useElementBounding, tryOnMounted } from '@vueuse/core'
import { computed, reactive, ref, unref, watchEffect, type MaybeRef } from 'vue'
import { type Position, usePointer, useElementBounding, type MaybeElement } from '@vueuse/core'
import { computed, reactive, ref, watchEffect, type Ref } from 'vue'

export interface UseDrawOptions {
container: MaybeRef<HTMLElement | undefined>
target: Ref<MaybeElement>
onDrawStart?: () => void
onDraw?: () => void
onDrawEnd?: () => void
Expand All @@ -14,7 +14,7 @@ export interface UseDrawOptions {
let isDrawingSomewhere = false

export function useDraw({
container,
target,
onDrawStart,
onDraw,
onDrawEnd,
Expand All @@ -30,7 +30,7 @@ export function useDraw({
width: scaledWidth,
height: scaledHeight,
update
} = useElementBounding(container)
} = useElementBounding(target)
const isDrawing = ref(false)
const posStart = reactive<Position>({ x: 0, y: 0 })
const posEnd = reactive<Position>({ x: 0, y: 0 })
Expand Down Expand Up @@ -59,11 +59,6 @@ export function useDraw({
() => Math.floor(absoluteX.value) !== lastPos.x || Math.floor(absoluteY.value) !== lastPos.y
)

tryOnMounted(() => {
unref(container)?.style?.setProperty('touch-action', 'none')
unref(container)?.style?.setProperty('user-select', 'none')
})

watchEffect(() => {
const isTouchScrolling =
!isDrawing.value &&
Expand Down
152 changes: 152 additions & 0 deletions src/composables/useEditor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { useDraw } from '@/composables/useDraw'
import { randomId } from '@/utils/randomId'
import type { DrawEvent, ImageHistory, Settings, Shape, Tool } from '../types'
import { computed, ref, type ComponentPublicInstance, type Ref } from 'vue'

export interface UseEditorOptions {
vpImage: Ref<ComponentPublicInstance>
tools: Tool<any>[]
history: Ref<ImageHistory<Tool<any>[]>>
settings: Ref<Settings>
width: number
height: number
emit?: Function
}

export function useEditor({ vpImage, tools, history, settings, width, height, emit }: UseEditorOptions) {

const redoHistory = ref<Shape[]>([])
const activeShape = ref<Shape | undefined>()
const temporaryTool = ref<string>()

function getActiveTool() {
return tools?.find((tool) => tool.type === (temporaryTool.value ?? settings.value.tool))
}

const drawEvent = computed<DrawEvent>(() => ({
settings: settings.value,
activeShape: activeShape.value,
id: activeShape.value?.id ?? randomId(),
isDrawing: isDrawing.value,
tools,
posStart,
posEnd,
left: left.value,
right: right.value,
top: top.value,
bottom: bottom.value,
width,
height,
x: x.value,
y: y.value,
minX: minX.value,
maxX: maxX.value,
minY: minY.value,
maxY: maxY.value,
absoluteX: absoluteX.value,
absoluteY: absoluteY.value
}))

const {
x,
y,
minX,
minY,
maxX,
maxY,
top,
left,
bottom,
right,
posStart,
posEnd,
isDrawing,
absoluteX,
absoluteY
} = useDraw({
target: vpImage,
width,
height,
onDrawStart() {
temporaryTool.value = document.elementsFromPoint(absoluteX.value, absoluteY.value)?.[0]
?.getAttribute('class')?.split(' ')
.find(c => c.startsWith('use-tool-'))
?.substring(9)
activeShape.value = getActiveTool()?.onDrawStart?.(drawEvent.value) ?? activeShape.value
emit?.('drawStart', drawEvent.value)
},
onDraw() {
activeShape.value = getActiveTool()?.onDraw?.(drawEvent.value) ?? activeShape.value
emit?.('draw', drawEvent.value)
},
async onDrawEnd() {
activeShape.value = getActiveTool()?.onDrawEnd
? await getActiveTool()?.onDrawEnd?.(drawEvent.value)
: activeShape.value
temporaryTool.value = undefined
emit?.('drawEnd', drawEvent.value)
if (activeShape.value) {
history.value.push(activeShape.value)
redoHistory.value = []
activeShape.value = undefined
}
}
})
function undo() {
if (history.value.length) {
redoHistory.value.push(...history.value.slice(-1))
history.value = history.value.slice(0, -1)
}
}

function redo() {
if (redoHistory.value.length) {
history.value.push(...redoHistory.value.slice(-1))
redoHistory.value = redoHistory.value.slice(0, -1)
}
}

function save() {
const svg = vpImage.value.$refs.svg
if (!svg) {
throw new Error("Couldn't find the svg")
}
emit?.('save', { svg, tools, history: history.value })
}

async function reset() {
redoHistory.value = history.value.reverse()
history.value = []
const shapes = await Promise.all(
tools
.filter((tool) => 'onInitialize' in tool)
.flatMap(async (tool) => await tool.onInitialize?.({ tools, settings, history }))
)
history.value = [...shapes.filter(Boolean), ...history.value]
emit?.('reset')
}

return {
settings,
activeShape,
undo,
redo,
save,
reset,
x,
y,
minX,
minY,
maxX,
maxY,
top,
left,
bottom,
right,
posStart,
posEnd,
isDrawing,
absoluteX,
absoluteY
}
}
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export interface DrawEvent {
export interface InitializeEvent {
tools: Tool<Shape>[]
settings: Ref<Settings>
history: ImageHistory<Tool<any>[]>
history: Ref<ImageHistory<Tool<any>[]>>
}

export type Shape = Freehand | Crop | Rectangle | Line | Arrow | Background | Textarea | Eraser | Move
Expand Down

0 comments on commit 6fa1854

Please sign in to comment.