feat: add line and arrow annotations
This commit is contained in:
@@ -75,6 +75,10 @@ func ApplyAnnotationsWithScale(pngBytes []byte, annotations []Annotation, scaleX
|
||||
switch ann.Tool {
|
||||
case "pen":
|
||||
drawPolyline(dst, ann.Points, scaleX, scaleY, width, c)
|
||||
case "line":
|
||||
drawStraightLine(dst, ann.Points, scaleX, scaleY, width, c)
|
||||
case "arrow":
|
||||
drawArrow(dst, ann.Points, scaleX, scaleY, width, c)
|
||||
case "rect":
|
||||
drawRectOutline(dst, ann.Points, scaleX, scaleY, width, c)
|
||||
case "ellipse":
|
||||
@@ -95,6 +99,34 @@ func ApplyAnnotationsWithScale(pngBytes []byte, annotations []Annotation, scaleX
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func drawStraightLine(img *image.RGBA, points []Point, scaleX, scaleY float64, width int, c color.RGBA) {
|
||||
if len(points) < 2 {
|
||||
return
|
||||
}
|
||||
drawLine(img, scalePoint(points[0], scaleX, scaleY), scalePoint(points[len(points)-1], scaleX, scaleY), width, c)
|
||||
}
|
||||
|
||||
func drawArrow(img *image.RGBA, points []Point, scaleX, scaleY float64, width int, c color.RGBA) {
|
||||
if len(points) < 2 {
|
||||
return
|
||||
}
|
||||
start := scalePoint(points[0], scaleX, scaleY)
|
||||
end := scalePoint(points[len(points)-1], scaleX, scaleY)
|
||||
dx, dy := end.X-start.X, end.Y-start.Y
|
||||
length := math.Hypot(dx, dy)
|
||||
if length < 1 {
|
||||
return
|
||||
}
|
||||
drawLine(img, start, end, width, c)
|
||||
headLength := math.Min(length*0.45, math.Max(10*math.Max(scaleX, scaleY), float64(width)*4))
|
||||
angle := math.Atan2(dy, dx)
|
||||
spread := math.Pi / 6
|
||||
left := Point{X: end.X - headLength*math.Cos(angle-spread), Y: end.Y - headLength*math.Sin(angle-spread)}
|
||||
right := Point{X: end.X - headLength*math.Cos(angle+spread), Y: end.Y - headLength*math.Sin(angle+spread)}
|
||||
drawLine(img, end, left, width, c)
|
||||
drawLine(img, end, right, width, c)
|
||||
}
|
||||
|
||||
const mosaicBlockSize = 10
|
||||
|
||||
func applyMosaicBrush(img *image.RGBA, points []Point, scaleX, scaleY float64, width int) {
|
||||
|
||||
@@ -229,3 +229,52 @@ func TestApplyAnnotationsPixelatesOnlyMosaicBrushStroke(t *testing.T) {
|
||||
t.Fatalf("expected pixels outside brush path to remain unchanged")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyAnnotationsDrawsStraightLine(t *testing.T) {
|
||||
src := image.NewRGBA(image.Rect(0, 0, 60, 40))
|
||||
draw.Draw(src, src.Bounds(), &image.Uniform{C: color.White}, image.Point{}, draw.Src)
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, src); err != nil {
|
||||
t.Fatalf("encode source: %v", err)
|
||||
}
|
||||
out, err := ApplyAnnotations(buf.Bytes(), []Annotation{{
|
||||
Tool: "line", Color: "#ef4444", StrokeWidth: 4,
|
||||
Points: []Point{{X: 5, Y: 20}, {X: 50, Y: 20}},
|
||||
}}, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("apply annotations: %v", err)
|
||||
}
|
||||
img, err := png.Decode(bytes.NewReader(out))
|
||||
if err != nil {
|
||||
t.Fatalf("decode output: %v", err)
|
||||
}
|
||||
got := color.RGBAModel.Convert(img.At(30, 20)).(color.RGBA)
|
||||
if got.R < 180 || got.G > 180 || got.B > 180 {
|
||||
t.Fatalf("expected red line at center, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyAnnotationsDrawsArrowHead(t *testing.T) {
|
||||
src := image.NewRGBA(image.Rect(0, 0, 80, 60))
|
||||
draw.Draw(src, src.Bounds(), &image.Uniform{C: color.White}, image.Point{}, draw.Src)
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, src); err != nil {
|
||||
t.Fatalf("encode source: %v", err)
|
||||
}
|
||||
out, err := ApplyAnnotations(buf.Bytes(), []Annotation{{
|
||||
Tool: "arrow", Color: "#3b82f6", StrokeWidth: 3,
|
||||
Points: []Point{{X: 10, Y: 30}, {X: 60, Y: 30}},
|
||||
}}, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("apply annotations: %v", err)
|
||||
}
|
||||
img, err := png.Decode(bytes.NewReader(out))
|
||||
if err != nil {
|
||||
t.Fatalf("decode output: %v", err)
|
||||
}
|
||||
shaft := color.RGBAModel.Convert(img.At(35, 30)).(color.RGBA)
|
||||
head := color.RGBAModel.Convert(img.At(52, 25)).(color.RGBA)
|
||||
if shaft.B < 180 || shaft.R > 140 || head.B < 180 || head.R > 140 {
|
||||
t.Fatalf("expected blue arrow shaft and head, got shaft=%#v head=%#v", shaft, head)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user