Skip to content

Commit

Permalink
feat: svg tablature
Browse files Browse the repository at this point in the history
  • Loading branch information
liningzhu committed May 14, 2024
1 parent d4ac819 commit 74f74e0
Show file tree
Hide file tree
Showing 6 changed files with 356 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import React, { HTMLProps } from 'react'
import { forwardRef, useMemo } from 'react'

export type SvgChordPoint = {
/**
* 品 -1代表该弦不发声
*/
fret: number
string: number
tone?: string
color?: string
}
import type { SvgChordPoint } from './svg-chord'

export interface SvgChordProps extends HTMLProps<SVGSVGElement> {
/**
Expand Down
200 changes: 200 additions & 0 deletions packages/svg-chord/lib/SvgTablature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import React, { FC, Fragment, HTMLProps, useMemo } from 'react'
import type { SvgChordPoint } from './svg-chord'

export interface SvgTablatureProps extends HTMLProps<SVGSVGElement> {
/**
* 按钮数组,数组长度表示弦数
*/
points: SvgChordPoint[]
/**
* 像素单位大小
*/
size?: number
/**
* 颜色
*/
color?: string
/**
* 按钮字体颜色
*/
fontColor?: string
/**
* 指板名称
*/
title?: string
/**
* 品位范围
*/
range?: [number, number]
/**
* 弦数量
*/
strings?: number
/**
* 水平展示
*/
horizontal?: boolean
}

export const SvgTablature: FC<SvgTablatureProps> = ({
points,
size = 200,
color = '#FFF',
fontColor = '#444',
title,
range,
strings = 6,
horizontal = false,
...attrs
}) => {
const lineStyle = { strokeWidth: size / 100, stroke: color }
const [startGrade, endGrade] = useMemo(() => {
if (range) return range
const grades = points
.filter((point) => point.fret > -1)
.map((point) => point.fret)
.sort((a, b) => a - b)
return [grades[0] || 0, grades[grades.length - 1] || 12]
}, [range])
const gradeNums = useMemo(() => endGrade - startGrade + 1, [startGrade, endGrade])
const gradeWidth = size * 0.8 // 品丝宽度
const itemX = gradeWidth / (strings - 1) // 两弦之间距离
const itemY = itemX * 1.6 // 两品之间距离
const dotSize = itemX * 0.8 // 按钮尺寸
const stringHeight = (gradeNums - 1) * itemY // 弦总长
const titleHeight = size * 0.1 // 标题高度
const padding = (size - gradeWidth) / 2 // 内边距
const paddingY = padding + titleHeight
const sizeY = stringHeight + titleHeight + padding + paddingY // 总高度

/**
* 网格线
*/
const drawLines = useMemo(() => {
const lines = []
// 水平线条:品线
for (let i = 0; i < gradeNums; i++) {
let x1 = padding
let y1 = paddingY + i * itemY
let x2 = padding + gradeWidth
let y2 = paddingY + i * itemY
if (horizontal) {
// 使用解构赋值交换 x1 和 y1 的值
;[x1, y1] = [y1, x1]
;[x2, y2] = [y2, x2]
}
const line = (
<line
key={`grade-${i}`}
data-set-grade={`grade-${i}`}
x1={x1}
y1={y1}
x2={x2}
y2={y2}
style={i === 0 ? { ...lineStyle, strokeWidth: lineStyle.strokeWidth * 3 } : lineStyle}
/>
)
lines.push(line)
}
// 垂直线条:弦
for (let j = 0; j < strings; j++) {
let x1 = padding + j * itemX
let y1 = paddingY
let x2 = padding + j * itemX
let y2 = paddingY + stringHeight
if (horizontal) {
// 使用解构赋值交换 x1 和 y1 的值
;[x1, y1] = [y1, x1]
;[x2, y2] = [y2, x2]
}
const line = (
<line key={`string-${j}`} data-set-grade={`string-${j}`} x1={x1} y1={y1} x2={x2} y2={y2} style={lineStyle} />
)
lines.push(line)
}
return lines
}, [size, color]) // size color都需要触发重渲染

const drawPoints = useMemo(() => {
return points
.filter((point) => point.fret > -1)
.map((point, index) => {
let pointX = padding + (point.string - 1) * itemX
let pointY =
point.fret === 0
? paddingY + (point.fret - startGrade) * itemY // 0品按钮
: paddingY + (point.fret - startGrade - 0.5) * itemY
if (horizontal) {
pointX = pointY
pointY = padding + (strings - point.string) * itemX
}
return (
<Fragment key={`point-${index}`}>
<circle
data-set-point={`point-${index}`}
cx={pointX}
cy={pointY}
r={dotSize / 2}
fill={point.color ?? color}
/>
<text
x={pointX}
y={pointY}
textAnchor="middle"
dominantBaseline="central"
fontSize={dotSize * 0.8}
fill={fontColor}
style={{ fontWeight: 700 }}
>
{point.tone}
</text>
</Fragment>
)
})
}, [points, color, fontColor, startGrade])

const drawTitle = useMemo(() => {
let x = size / 2
const y = paddingY / 2
if (horizontal) {
x = -x
}
return (
<text
x={x}
y={y}
textAnchor="middle"
dominantBaseline="central"
fontSize={titleHeight}
fill={color}
style={{ fontWeight: 700, transform: horizontal ? 'rotate(-90deg)' : 'none' }}
>
{title}
</text>
)
}, [title])

const sizeObj = useMemo(
() =>
horizontal
? {
width: sizeY,
height: size,
}
: {
width: size,
height: sizeY,
},
[]
)

return (
<svg {...(attrs as any)} {...sizeObj} xmlns="http://www.w3.org/2000/svg" version="1.1">
{drawLines}
{drawPoints}
{drawTitle}
</svg>
)
}

export default SvgTablature
2 changes: 2 additions & 0 deletions packages/svg-chord/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { SvgChord } from './SvgChord'
export { SvgTablature } from './SvgTablature'
15 changes: 15 additions & 0 deletions packages/svg-chord/lib/svg-chord.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type SvgChordPoint = {
/**
* 品 -1代表该弦不发声
*/
fret: number
/**
* 弦 1开始 比如吉他 1~6
*/
string: number
/**
* 音 名
*/
tone?: string
color?: string
}
2 changes: 1 addition & 1 deletion packages/svg-chord/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@buitar/svg-chord",
"version": "0.0.4",
"version": "0.0.5",
"type": "module",
"files": [
"dist"
Expand Down
Loading

0 comments on commit 74f74e0

Please sign in to comment.