vue3-sketch-ruler
Version:
> In using vue3, the zoom operation used for page presentation
216 lines (198 loc) • 6.61 kB
text/typescript
// 标尺中每小格代表的宽度(根据scale的不同实时变化)
const getGridSize = (scale: number) => {
if (scale <= 0.25) return 40
if (scale <= 0.5) return 20
if (scale <= 1) return 10
if (scale <= 2) return 5
if (scale <= 4) return 2
return 1
}
export function setLast(
x: number,
value: number,
width: number,
height: number,
ctx: CanvasRenderingContext2D,
isHorizontal?: boolean
) {
isHorizontal ? ctx.moveTo(x, 0) : ctx.moveTo(0, x)
ctx.save()
isHorizontal ? ctx.translate(x + 5, height * 0.2) : ctx.translate(width * 0.1, x + 32)
if (!isHorizontal) ctx.rotate(-Math.PI / 2) // 旋转 -90 度
ctx.fillText(Math.round(value).toString(), 4, 7)
ctx.restore()
isHorizontal ? ctx.lineTo(x, height) : ctx.lineTo(width, x)
ctx.stroke()
ctx.closePath()
ctx.setTransform(1, 0, 0, 1, 0, 0)
}
export function drawShadowText(
x: number,
y: number,
num: number,
ctx: CanvasRenderingContext2D,
palette: any,
isHorizontal?: boolean
) {
ctx.fillStyle = palette.fontShadowColor
ctx.strokeStyle = palette.longfgColor
ctx.save()
ctx.translate(x, y)
if (!isHorizontal) ctx.rotate(-Math.PI / 2)
// 设置字体为加粗
ctx.font = 'bold 12px Aria'
// ctx.strokeText(String(num), 0, 0)
ctx.fillText(String(num), 0, 0)
ctx.restore()
}
export const drawCanvasRuler = (
ctx: CanvasRenderingContext2D,
start: number,
selectStart: number,
selectLength: number,
options: {
scale: number
width: number
height: number
ratio: number
gridRatio: number
palette: any
canvasWidth: number
canvasHeight: number
showShadowText: boolean
},
isHorizontal?: boolean //横向为true,纵向缺省
) => {
const { scale, width, height, ratio, palette, gridRatio, showShadowText } = options
const { bgColor, fontColor, shadowColor, longfgColor } = palette
const endNum = isHorizontal ? options.canvasWidth : options.canvasHeight
ctx.setTransform(1, 0, 0, 1, 0, 0) // 还原,否则scale变大后会错乱
// 缩放ctx, 以简化计算
ctx.scale(ratio, ratio)
ctx.clearRect(0, 0, width, height)
// 1. 画标尺底色
ctx.fillStyle = bgColor
ctx.fillRect(0, 0, width, height)
const gridSize = getGridSize(scale) * gridRatio // 每小格表示的宽度
const gridSize10 = gridSize * 10 // 每大格表示的宽度
const gridPixel10 = gridSize10 * scale
const startValue10 = Math.floor(start / gridSize10) * gridSize10
const offsetX10 = ((startValue10 - start) / gridSize10) * gridPixel10 // 长间隔起点刻度距离ctx原点(start)的px距离
const endValue = start + Math.ceil((isHorizontal ? width : height) / scale) // 终点刻度
// 2. 画阴影
if (selectLength) {
const shadowX = (selectStart - start) * scale // 阴影起点坐标
const shadowWidth = selectLength * scale // 阴影宽度
ctx.fillStyle = shadowColor
isHorizontal
? ctx.fillRect(shadowX, 0, shadowWidth, height)
: ctx.fillRect(0, shadowX, width, shadowWidth)
// 画阴影文字起始
if (showShadowText) {
if (isHorizontal) {
drawShadowText(shadowX, height * 0.4, Math.round(selectStart), ctx, palette, isHorizontal)
const shadowEnd = (selectStart + selectLength - start) * scale
drawShadowText(
shadowEnd,
height * 0.4,
Math.round(selectStart + selectLength),
ctx,
palette,
isHorizontal
)
} else {
drawShadowText(width * 0.4, shadowX, Math.round(selectStart), ctx, palette, isHorizontal)
const shadowEnd = (selectStart + selectLength - start) * scale
drawShadowText(
width * 0.4,
shadowEnd,
Math.round(selectStart + selectLength),
ctx,
palette,
isHorizontal
)
}
}
}
// 3. 画刻度和文字
ctx.beginPath()
ctx.fillStyle = fontColor
ctx.strokeStyle = longfgColor
// 绘制长间隔和文字
for (let value = startValue10, count = 0; value < endValue; value += gridSize10, count++) {
const x = offsetX10 + count * gridPixel10 + 0.5 // prevent canvas 1px line blurry
if ((value - gridSize10 < endNum && value > endNum) || value == endNum) {
// 如果尾数画最后一个刻度
const xl = offsetX10 + count * gridPixel10 + 0.5 + (endNum - value) * scale
setLast(xl, endNum, width, height, ctx, isHorizontal)
return
}
if (value >= 0 && value <= endNum) {
if (value == 0) {
if (isHorizontal) {
ctx.moveTo(x, 0)
ctx.lineTo(x, height)
} else {
ctx.moveTo(0, x)
ctx.lineTo(width, x)
}
} else {
if (isHorizontal) {
ctx.moveTo(x, 20)
ctx.lineTo(x, height / 1.3)
} else {
ctx.moveTo(20, x)
ctx.lineTo(width / 1.3, x)
}
}
ctx.save()
// 影响文字位置
if (value == 0) {
isHorizontal ? ctx.translate(x - 15, height * 0.01) : ctx.translate(width * 0.3, x - 5)
} else {
isHorizontal ? ctx.translate(x - 12, height * 0.05) : ctx.translate(width * 0.05, x + 12)
}
if (!isHorizontal) {
ctx.rotate(-Math.PI / 2) // 旋转 -90 度
}
// 如果最后一个大刻度挨着最后一个刻度, 不画文字
if (endNum - value > gridSize10 / 2) {
if (
!showShadowText ||
selectLength == 0 ||
(Math.abs(value - selectStart) > gridSize10 / 2 &&
Math.abs(value - (selectStart + selectLength)) > gridSize10 / 2)
) {
ctx.fillText(value.toString(), 4, 9)
}
}
ctx.restore()
}
}
ctx.stroke()
ctx.closePath()
}
interface IDebounce {
(): void
cancel(): void
}
// 定义一个泛型类型,确保T是一个函数类型
type FunctionType<T = any, R = any> = (this: T, ...args: any[]) => R
export function debounce<T extends FunctionType>(func: T, wait = 100): IDebounce {
let timeout: ReturnType<typeof setTimeout> | null = null
const debounced: IDebounce = function (...args: Parameters<T>) {
if (timeout !== null) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
func(...args)
}, wait)
}
debounced.cancel = function () {
if (timeout !== null) {
clearTimeout(timeout)
timeout = null
}
}
return debounced as IDebounce
}