mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
102 lines
3.5 KiB
Go
102 lines
3.5 KiB
Go
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package ofdgo
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
|
|
"github.com/tdewolff/canvas"
|
|
)
|
|
|
|
// InkPoint 手写采样点,X和Y为毫米,Pressure为0到1的压力
|
|
type InkPoint struct {
|
|
X float64 `json:"x"`
|
|
Y float64 `json:"y"`
|
|
Pressure float64 `json:"pressure"`
|
|
}
|
|
|
|
// NewInk 将采样点转为标准矢量路径,等宽笔保留中心线,压感笔使用一次填充的连续轮廓
|
|
// 不推测笔压、不修改输入;单点生成圆点,页面外路径由页面边界裁切
|
|
// 入参: points 采样点, width 等宽线宽或压感最大线宽,单位毫米, pressure 是否使用压力
|
|
// 返回: PathObject 黑色笔迹,可修改颜色和透明度后插入页面或注解, error 错误信息
|
|
func NewInk(points []InkPoint, width float64, pressure bool) (PathObject, error) {
|
|
if len(points) == 0 || !finite(width) || width <= 0 {
|
|
return PathObject{}, fmt.Errorf("ink requires points and a positive finite width")
|
|
}
|
|
path := &canvas.Path{}
|
|
stationary := true
|
|
for i, point := range points {
|
|
if !finite(point.X) || !finite(point.Y) || !finite(point.Pressure) || point.Pressure < 0 || point.Pressure > 1 {
|
|
return PathObject{}, fmt.Errorf("invalid ink point %d", i)
|
|
}
|
|
stationary = stationary && point.X == points[0].X && point.Y == points[0].Y
|
|
if !pressure {
|
|
if i == 0 {
|
|
path.MoveTo(point.X, point.Y)
|
|
} else {
|
|
path.LineTo(point.X, point.Y)
|
|
}
|
|
continue
|
|
}
|
|
radius := width * point.Pressure / 2
|
|
if radius > 0 {
|
|
path = path.Append(canvas.Circle(radius).Translate(point.X, point.Y))
|
|
}
|
|
if i == 0 {
|
|
continue
|
|
}
|
|
previous := points[i-1]
|
|
r := width * previous.Pressure / 2
|
|
dx, dy := point.X-previous.X, point.Y-previous.Y
|
|
length := math.Hypot(dx, dy)
|
|
if radius == 0 && r == 0 || length <= math.Abs(radius-r) {
|
|
continue
|
|
}
|
|
x, y := dx/length, dy/length
|
|
a := (r - radius) / length
|
|
b := math.Sqrt(1 - a*a)
|
|
nx, ny, mx, my := a*x-b*y, a*y+b*x, a*x+b*y, a*y-b*x
|
|
segment := &canvas.Path{}
|
|
segment.MoveTo(previous.X+r*mx, previous.Y+r*my)
|
|
segment.LineTo(point.X+radius*mx, point.Y+radius*my)
|
|
segment.LineTo(point.X+radius*nx, point.Y+radius*ny)
|
|
segment.LineTo(previous.X+r*nx, previous.Y+r*ny)
|
|
segment.Close()
|
|
path = path.Append(segment)
|
|
}
|
|
filled := pressure || stationary
|
|
if !pressure && stationary {
|
|
path = canvas.Circle(width/2).Translate(points[0].X, points[0].Y)
|
|
}
|
|
if path.Empty() {
|
|
return PathObject{}, fmt.Errorf("ink has no visible stroke")
|
|
}
|
|
box := path.Bounds()
|
|
padding := width / 2
|
|
boundary := Box{X: box.X0 - padding, Y: box.Y0 - padding, W: math.Max(box.W(), width) + width, H: math.Max(box.H(), width) + width}
|
|
object := editorClipPath(path.Translate(-boundary.X, -boundary.Y))
|
|
object.Boundary = editorBoxString(boundary)
|
|
stroke := !filled
|
|
object.Fill, object.Stroke = &filled, &stroke
|
|
object.Cap, object.Join, object.LineWidth = "Round", "Round", width
|
|
if filled {
|
|
object.FillColor = &FillColor{Value: "0 0 0"}
|
|
} else {
|
|
object.StrokeColor = &StrokeColor{Value: "0 0 0"}
|
|
}
|
|
return object, nil
|
|
}
|