我知道了!
好的,首先是一些数学理论。在几何中有几种描绘直线的方法。
第一种是“斜截距”形式:y = mx + c
然后是“点-斜”形式:y = y1 + m * (x - x1)
最后是“广义形式”:
然而,还有另一种形式,“参数形式”
R = P + tD
其中D是“位移矢量”,P是“起点”,R是“合成矢量”。
有了这个概念,我可以想象有人带着标记沿着线段走,每当他们走过一定距离时,用另一个标记替换标记,然后继续。
基于这个原则,这里是(quick-n-dirty)程序:
import math
from itertools import pairwise, cycle
from math import sqrt, isclose
from typing import NamedTuple
from PIL import Image, ImageDraw
class Point(NamedTuple):
x: float
y: float
def rounded(self) -> tuple[int, int]:
return round(self.x), round(self.y)
# Example data points
points: list[Point] = [
Point(108.0, 272.0),
Point(150.0, 227.0),
Point(171.0, 218.0),
Point(187.0, 221.0),
Point(192.0, 234.0),
Point(205, 315),
Point(216, 402),
Point(275, 565),
Point(289, 586),
Point(312, 603),
Point(343, 609),
Point(387, 601),
Point(420, 577),
Point(484, 513),
Point(505, 500),
Point(526, 500),
Point(551, 509),
Point(575, 550),
Point(575, 594),
Point(546, 656),
Point(496, 686),
Point(409, 712),
Point(329, 715),
Point(287, 701),
]
class ParametricLine:
def __init__(self, p1: Point, p2: Point):
self.p1 = p1
self.x1, self.y1 = p1
self.p2 = p2
self.x2, self.y2 = p2
self._len = -1.0
@property
def length(self):
if self._len < 0.0:
dx, dy = self.displacement
self._len = sqrt(dx ** 2 + dy ** 2)
return self._len
@property
def displacement(self):
return (self.x2 - self.x1), (self.y2 - self.y1)
def replace_start(self, p: Point):
self.p1 = p
self.x1, self.y1 = p
self._len = -1.0
def get_point(self, t: float) -> Point:
dx, dy = self.displacement
xr = self.x1 + (t / self.length) * dx
xy = self.y1 + (t / self.length) * dy
return Point(xr, xy)
image = Image.new("RGBA", (1000, 1000))
idraw = ImageDraw.Draw(image)
def draw(segments: list[tuple[Point, Point]], phase: str):
drawpoints = []
prev_p2 = segments[0][0]
p2 = None
for p1, p2 in segments:
assert isclose(p1.x, prev_p2.x)
assert isclose(p1.y, prev_p2.y)
drawpoints.append(p1.rounded())
prev_p2 = p2
drawpoints.append(p2.rounded())
if phase == "dash" or phase == "gapp":
idraw.line(drawpoints, fill=(255, 255, 0), width=10, joint="curve")
elif phase == "pip1" or phase == "pip2":
idraw.line(drawpoints, fill=(0, 0, 0), width=10, joint="curve")
def main():
limits: dict[str, float] = {
"dash": 40.0,
"pip1": 8.0,
"gapp": 8.0,
"pip2": 8.0,
}
pointpairs = pairwise(points)
climit = cycle(limits.items())
phase, tleft = next(climit)
segments: list[tuple[Point, Point]] = []
pline: ParametricLine | None = None
p1 = p2 = Point(math.nan, math.nan)
while True:
if pline is None:
try:
p1, p2 = next(pointpairs)
except StopIteration:
break
pline = ParametricLine(p1, p2)
if pline.length > tleft:
# The line segment is longer than our leftover budget.
# Find where we should truncate the line and draw the
# segments until the truncation point.
p3 = pline.get_point(tleft)
segments.append((p1, p3))
draw(segments, phase)
segments.clear()
pline.replace_start(p3)
p1 = p3
phase, tleft = next(climit)
else:
# The segment is shorter than our leftover budget.
# Record that and reduce the budget.
segments.append((p1, p2))
tleft -= pline.length
pline = None
if abs(tleft) < 0.01:
# The leftover is too small, let's just assume that
# this is insignificant and go to the next phase.
draw(segments, phase)
segments.clear()
phase, tleft = next(climit)
if segments:
draw(segments, phase)
image.save("results.png")
if __name__ == '__main__':
main()
结果如下:
有点粗糙,但可用于我的目的。
这个解决方案的美妙之处在于,通过改变draw()(以及limits的内容)中发生的事情,我的解决方案也可以很容易地处理虚线;只需让 limits 在 "dash" 和 "blank" 之间来回切换,而在 draw() 中,实际上只在 phase == "dash" 时画一条线。
注意:我 100% 确定算法可以进一步优化/整理。截至目前,我很高兴它能正常工作。我可能会偷偷跑到 CodeReview SE 上寻求优化建议。