Skip to content

Commit

Permalink
feat: 带压感画板
Browse files Browse the repository at this point in the history
  • Loading branch information
klren0312 committed Feb 21, 2021
0 parents commit 99f440c
Show file tree
Hide file tree
Showing 17 changed files with 1,073 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Vue 3 + Typescript + Vite

This template should help get you started developing with Vue 3 and Typescript in Vite.

## Recommended IDE Setup

[VSCode](https://code.visualstudio.com/) + [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur). Make sure to enable `vetur.experimental.templateInterpolationService` in settings!

### If Using `<script setup>`

[`<script setup>`](https://github.com/vuejs/rfcs/pull/227) is a feature that is currently in RFC stage. To get proper IDE support for the syntax, use [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) instead of Vetur (and disable Vetur).

## Type Support For `.vue` Imports in TS

Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can use the following:

### If Using Volar

Run `Volar: Switch TS Plugin on/off` from VSCode command palette.

### If Using Vetur

1. Install and add `@vuedx/typescript-plugin-vue` to the [plugins section](https://www.typescriptlang.org/tsconfig#plugins) in `tsconfig.json`
2. Delete `src/shims-vue.d.ts` as it is no longer needed to provide module info to Typescript
3. Open `src/main.ts` in VSCode
4. Open the VSCode command palette 5. Search and run "Select TypeScript version" -> "Use workspace version"
13 changes: 13 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "draw",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"vue": "^3.0.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.1.4",
"@vue/compiler-sfc": "^3.0.5",
"sass": "^1.32.8",
"typescript": "^4.1.3",
"vite": "^2.0.1"
}
}
Binary file added public/favicon.ico
Binary file not shown.
22 changes: 22 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<DrawBoard/>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import DrawBoard from './components/DrawBoard.vue'
export default defineComponent({
name: 'App',
components: {
DrawBoard
}
})
</script>

<style>
body {
padding: 0;
margin: 0;
}
</style>
Binary file added src/assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
171 changes: 171 additions & 0 deletions src/components/DrawBoard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<template>
<div class="canvas-block">
<canvas id="canvas"></canvas>
<button class="undo-btn" @click="undoHandle">undo</button>
</div>
</template>

<script lang="ts">
import { ref, defineComponent, onMounted } from 'vue'
import { useEventListener } from '/@/hooks/useEventListener'
export default defineComponent({
name: 'DrawBoard',
setup: () => {
let canvas: HTMLCanvasElement
let ctx: CanvasRenderingContext2D
let lineColor: string = '#000' // 颜色
let painting: boolean = false // 是否处于状态
let historyList: Array<ImageData> = []
onMounted(() => {
initCanvas()
})
/**
* @description 初始化canvas
*/
function initCanvas() {
canvas = <HTMLCanvasElement>document.getElementById('canvas')
ctx = <CanvasRenderingContext2D>canvas.getContext('2d')
let width: number = window.innerWidth
let height: number = window.innerHeight
if (window.devicePixelRatio) {
canvas.style.width = width + 'px'
canvas.style.height = height + 'px'
canvas.height = height * window.devicePixelRatio
canvas.width = width * window.devicePixelRatio
ctx.scale(window.devicePixelRatio, window.devicePixelRatio)
}
ctx.imageSmoothingEnabled = true
useEventListener({
el: canvas,
name: 'pointerdown',
listener: e => startDraw(<PointerEvent>e)
})
useEventListener({
el: canvas,
name: 'pointermove',
listener: e => moveDraw(<PointerEvent>e)
})
useEventListener({
el: canvas,
name: 'pointerup',
listener: e => cancelDraw(<PointerEvent>e)
})
useEventListener({
el: canvas,
name: 'pointerleave',
listener: e => cancelDraw(<PointerEvent>e)
})
}
/**
* @description 开始
* @param {PointerEvent} e 事件
*/
function startDraw(e: PointerEvent) {
painting = true
const event: PointerEvent = e || window.event
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
const x = event.offsetX
const y = event.offsetY
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineWidth = getLineWidth(e)
ctx.strokeStyle = lineColor
}
/**
* @description 移动
* @param {PointerEvent} e 事件
*/
function moveDraw(e: PointerEvent) {
if (!painting) {
return
} else {
console.log('move', e.pointerType, e.pressure, e.shiftKey)
const event = e || window.event
const x = event.offsetX
const y = event.offsetY
ctx.lineWidth = getLineWidth(e)
ctx.lineTo(x, y)
ctx.stroke()
}
}
function cancelDraw(e: PointerEvent) {
if (!painting) {
return false
}
painting = false
historyList.push(ctx.getImageData(0, 0, canvas.width, canvas.height))
ctx.closePath()
}
/**
* @description 计算线宽
* @param {PointerEvent} e 事件
* @return {number} 线宽
*/
function getLineWidth(e: PointerEvent): number {
switch (e.pointerType) {
case 'touch': {
if (e.width < 10 && e.height < 10) {
return (e.width + e.height) * 2 + 10;
} else {
return (e.width + e.height - 40) / 2;
}
}
case 'pen': return e.pressure * 8;
default: return (e.pressure) ? e.pressure * 8 : 4;
}
}
/**
* @description 撤销操作
*/
function undoHandle() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
try {
historyList.pop()
const history: ImageData = historyList[historyList.length - 1]
if (history) {
ctx.putImageData(history, 0, 0)
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
} catch (error) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
}
return {
undoHandle
}
}
})
</script>

<style lang="scss">
.canvas-block {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
#canvas {
position: absolute;
top: 0;
left: 0;
touch-action: none;
cursor: crosshair;
}
.undo-btn {
position: absolute;
top: 10px;
left: 10px;
}
}
</style>
35 changes: 35 additions & 0 deletions src/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export interface DebounceAndThrottleOptions {
// 立即执行
immediate?: boolean
// 是否为debounce
debuonce?: boolean
// 只执行一次
once?: boolean
}

export type CancelFn = () => void

export type DebounceAndThrottleProcedure<T extends unknown[]> = (...args: T) => unknown

export type DebounceAndThrottleProcedureResult<T extends unknown[]> = [
DebounceAndThrottleProcedure<T>,
CancelFn
]

import {
useThrottle
} from './useThrottle'

export function useDebounce<T extends unknown[]>(
handle: DebounceAndThrottleProcedure<T>,
wait: number,
options: DebounceAndThrottleOptions = {}
): DebounceAndThrottleProcedureResult<T> {
return useThrottle(
handle,
wait,
Object.assign(options, {
debounce: true
})
)
}
61 changes: 61 additions & 0 deletions src/hooks/useEventListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { Ref } from 'vue'

import { ref, watch, unref } from 'vue'
import { useDebounce } from './useDebounce'
import { useThrottle } from './useThrottle'

export type RemoveEventFn = () => void

export interface UseEventParams {
el?: Element | Ref<Element | undefined> | Window | any
name: string
listener: EventListener
options?: boolean | AddEventListenerOptions
autoRemove?: boolean
isDebounce?: boolean
wait?: number
}
export function useEventListener({
el = window,
name,
listener,
options,
autoRemove = true,
isDebounce = true,
wait = 80,
}: UseEventParams): { removeEvent: RemoveEventFn } {
/* eslint-disable-next-line */
let remove: RemoveEventFn = () => {}
const isAddRef = ref(false)

if (el) {
const element: Ref<Element> = ref(el as Element)

const [handler] = isDebounce ? useDebounce(listener, wait) : useThrottle(listener, wait)
const realHandler = wait ? handler : listener
const removeEventListener = (e: Element) => {
isAddRef.value = true
e.removeEventListener(name, realHandler, options)
}
const addEventListener = (e: Element) => e.addEventListener(name, realHandler, options)

const removeWatch = watch(
element,
(v, _ov, cleanUp) => {
if (v) {
!unref(isAddRef) && addEventListener(v)
cleanUp(() => {
autoRemove && removeEventListener(v)
})
}
},
{ immediate: true }
)

remove = () => {
removeEventListener(element.value)
removeWatch()
}
}
return { removeEvent: remove }
}
Loading

0 comments on commit 99f440c

Please sign in to comment.