Bezier
Einige Funktionen um Kubische Bezier-Kurven. Eine Bezier-Kurve hat zwei Richtungs- und zwei Endpunkte. Sie wird repräsentiert durch eine Liste von acht Fließkommazahlen. Schnittpunkte werden repräsentiert durch eine Fließkommazahl zwischen 0 und 1.
Einrichtung
- Quelltext unten kopieren
- einfügen in eine leere Textdatei
- Textdatei speichern unter Dateinamen in oberster Textzeile, hier:
Bezier-0.2.tm - Textdatei einstellen in das lokale Verzeichnis für Module, beim Autor etwa:
~/bin/TM
Anwendung
package require Bezier- lädt das Paket, macht dieses Prozeduren verfügbar:
bezier bbox(Kurve)- übergibt Koordinaten des umgebenden Rechtecks
bezier tangens2(Kurve1) (Kurve2)- ermittelt die gemeinsam verbindende Tangente zweier Kurven, übergibt die Berührpunkte der Kurven
bezier segment(Kurve) (Start) (Ende)- übergibt das Segment im Bereich Start, Ende
bezier cut(Kurve1) (Kurve2)- übergibt ein Schnittpunktpaar der Kurven
bezier cuts(Kurve1) (Kurve2)- übergibt alle Schnittpunktpaare der Kurven als unsortierte Liste
bezier help- gibt eine kurze Übersicht über verfügbare Prozeduren
Anmerkungen
- Die Bounding Box wird numerisch ermittelt durch erste Ableitung der kubischen Gleichung, folgt also der Schulmathematik und ist – anders als Näherung durch Intervallschachtelung – auf natürlichem Weg präzise.
- Die Neigung der verbindenden Tangente bei
bezier tangens2wird angenähert durch Intervallschachtelung. - Die Schnittpunkte werden angenähert durch Intervallschachtelung.
- Die begrenzende Intervallgröße ist auf
1e-5voreingestellt. Sie kann den Funktionencutundcutsals optionaler dritter Parameter übergeben werden, etwabezier cut(Kurve1) (Kurve2)1e-6. - Schnittpunkte genau auf Endpunkten werden veworfen.
- Als Schnittpunkt zählt nur echte Kreuzung, nicht tangentialer Berührpunkt.
- Einfache optische Überlegung zeigt, dass Kubische Bezier-Kurven zwischen null und neun Schnittpunkte haben können.
- Schnittpunktpaare können nach Bedarf sortiert werden. Eine bestimmte Sortierung vorzugeben ist wenig sinnvoll, weil die Kurven gegenläufig sein können.
# file: Bezier-0.2.tm
package provide Bezier 0.2
namespace eval bezier namespace import\
::tcl::mathfunc::*\
::tcl::mathop::*
proc bezier::Extrema {a b c d} {
# taken a, b, c, d as x-coords of bezier dots,
# return list of fractions where the appropriate function has extrema
set factor2 [expr {3.0 * (-$a + (3.0 * $b) + (-3.0 * $c) + $d)}]
set factor1 [expr {2.0 * ((3.0 * $a) + (-6.0 * $b) + (3.0 * $c))}]
set factor0 [expr {(-3.0 * $a) + (3.0 * $b)}]
if {$factor2 != 0} then {
set p [expr { $factor1 / $factor2 / 2 }]
set q [expr { $factor0 / $factor2 }]
set p2q [expr { ($p * $p) - $q }]
if {$p2q > 0} then {
set sqrtp2q [expr {sqrt($p2q)}]
list [expr { -$p - $sqrtp2q }] [expr { -$p + $sqrtp2q }]
} elseif {$p2q == 0} then {
expr {-$p}
}
} elseif { $factor1 != 0 } then {
expr { -$factor0 / $factor1 }
}
}
proc bezier::bbox coords {
lassign $coords ax ay bx by cx cy dx dy
lappend allX $ax $dx
lappend allY $ay $dy
foreach frac [Extrema $ax $bx $cx $dx] {
if {$frac > 0.0 && $frac < 1.0} then {
foreach {x y} [At $coords $frac] {
lappend allX $x
lappend allY $y
}
}
}
foreach frac [Extrema $ay $by $cy $dy] {
if {$frac > 0.0 && $frac < 1.0} then {
foreach {x y} [At $coords $frac] {
lappend allX $x
lappend allY $y
}
}
}
list [min {*}$allX] [min {*}$allY] [max {*}$allX] [max {*}$allY]
}
proc bezier::Rotate {x y phi {cX 0.0} {cY 0.0}} {
# coords x, y rotated by phi around cX, cY
set c [cos $phi]
set s [sin $phi]
list\
[expr {($x - $cX) * $c - ($y - $cY) * $s + $cX}]\
[expr {($y - $cY) * $c + ($x - $cX) * $s + $cY}]
}
proc bezier::RotateCoords {coords phi {cX 200.0} {cY 150.0}} {
concat {*}[lmap {x y} $coords {
Rotate $x $y $phi $cX $cY
}]
}
proc bezier::ScaleCoords {coords fx fy {cx 200} {cy 150}} {
concat {*}[lmap {x y} $coords {
list\
[expr {($x - $cx) * $fx + $cx}]\
[expr {($y - $cy) * $fy + $cy}]
}]
}
# === updated ===
# outdated version returned rotated copies of beziers
# new version TangensAngle below returns just the angle
proc bezier::AngleOfTwoCurves {curve1 curve2 {side top}} {
# angle of bounding boxes on top, bottom, or alternating
switch -exact -- $side {
top {
lassign [bbox $curve1] x0 y0 - -
lassign [bbox $curve2] - y1 x1 -
}
bottom {
lassign [bbox $curve1] x0 - - y0
lassign [bbox $curve2] - - x1 y1
}
topBottom {
lassign [bbox $curve1] x0 y0 - -
lassign [bbox $curve2] - - x1 y1
}
bottomTop {
lassign [bbox $curve1] x0 - - y0
lassign [bbox $curve2] - y1 x1 -
}
}
atan2 [- $y1 $y0] [- $x1 $x0]
}
proc bezier::CurveCenter coords {
# center of bounding box
lassign [bbox $coords] x0 y0 x1 y1
list [/ [+ $x0 $x1] 2.0] [/ [+ $y0 $y1] 2.0]
}
proc bezier::CurveIsArc curve {
# return true if bent like Arc
# return false if bent like Bowl
lassign $curve x0 y0 - - - - x1 y1
if {$x0 > $x1} then {
lassign "$x0 $y0 $x1 $y1" x1 y1 x0 y0
}
set phi [atan2 [- $y1 $y0] [- $x1 $x0]]
set c1 [RotateCoords $curve [- $phi]]
lassign $c1 - y2 - y3
expr {
$y3 < $y2 ? yes : no
}
}
proc bezier::TangensAngle {curve1 curve2 {how auto}} {
# return angle of common tangential line
#
# calc left/right
#
lassign [CurveCenter $curve1] x0 y0
lassign [CurveCenter $curve2] x1 y1
set phi [atan2 [- $y1 $y0] [- $x1 $x0]]
if {$phi < 0} then {
lassign [list $curve1 $curve2] c2 c1
} else {
lassign [list $curve1 $curve2] c1 c2
}
#
# adjust horizontally
#
lassign [CurveCenter $c1] x0 y0
lassign [CurveCenter $c2] x1 y1
set phi [atan2 [- $y1 $y0] [- $x1 $x0]]
set c1 [RotateCoords $c1 [- $phi]]
set c2 [RotateCoords $c2 [- $phi]]
set result [- $phi]
#
# check if bowl or arc
#
if {$how ne "auto"} then {
set side $how
} else {
if {[CurveIsArc $c1]} then {
# $c1 yes
if {[CurveIsArc $c2]} then {
# $c1 yes, $c2 yes
set side top
} else {
# $c1 yes, $c2 no
set side topBottom
}
} else {
# $c1 no
if {[CurveIsArc $c2]} then {
# $c1 no, $c2 yes
set side bottomTop
} else {
# $c1 no, $c2 no
set side bottom
}
}
}
for {set i 0} {$i < 10} {incr i} {
set phi [AngleOfTwoCurves $c1 $c2 $side]
set result [- $result $phi]
set c1 [RotateCoords $c1 [- $phi]]
set c2 [RotateCoords $c2 [- $phi]]
if {abs($phi) < 0.0001} break
}
set result
}
# ===
proc bezier::tangens2 {left right} {
# return fractions of beziers left, right
# where common tangens touches
set phi [TangensAngle $left $right]
set left [RotateCoords $left $phi]
set right [RotateCoords $right $phi]
# extrema for $left
lassign $left - y0 - y1 - y2 - y3
set fracsLeft {}
foreach frac [Extrema $y0 $y1 $y2 $y3] {
if {$frac <= 1 && $frac >= 0} then {
lappend fracsLeft $frac
}
}
# extrema for $right
lassign $right - y0 - y1 - y2 - y3
set fracsRight {}
foreach frac [Extrema $y0 $y1 $y2 $y3] {
if {$frac <= 1 && $frac >= 0} then {
lappend fracsRight $frac
}
}
if {[llength $fracsLeft] == 1 && [llength $fracsRight] == 1} then {
concat $fracsLeft $fracsRight
}
}
proc bezier::Part {coords frac} {
lassign $coords ax ay bx by cx cy dx dy
#
set abx [expr {$ax + ($bx - $ax) * $frac}]
set bcx [expr {$bx + ($cx - $bx) * $frac}]
set cdx [expr {$cx + ($dx - $cx) * $frac}]
set abcx [expr {$abx + ($bcx - $abx) * $frac}]
set bcdx [expr {$bcx + ($cdx - $bcx) * $frac}]
set abcdx [expr {$abcx + ($bcdx - $abcx) * $frac}]
#
set aby [expr {$ay + ($by - $ay) * $frac}]
set bcy [expr {$by + ($cy - $by) * $frac}]
set cdy [expr {$cy + ($dy - $cy) * $frac}]
set abcy [expr {$aby + ($bcy - $aby) * $frac}]
set bcdy [expr {$bcy + ($cdy - $bcy) * $frac}]
set abcdy [expr {$abcy + ($bcdy - $abcy) * $frac}]
#
list $ax $ay $abx $aby $abcx $abcy $abcdx $abcdy
}
proc bezier::segment {coords from to} {
set frac0 $to
set coords1 [Part $coords $frac0]
set frac1 [expr {($to - $from) / $to}]
lreverse [Part [lreverse $coords1] $frac1]
}
proc bezier::At {coords frac} {
lrange [Part $coords $frac] end-1 end
}
proc bezier::Det2val det {
# return numeric value of 2x2 determinante $det
lassign $det l1 l2
lassign $l1 a1 a2
lassign $l2 b1 b2
# - [* $a1 $b2] [* $a2 $b1]
expr {double($a1 * $b2 - $a2 * $b1)}
}
proc bezier::lineCutLine {l0 l1 args} {
# return list of fracs where lines $l0, $l1 cross each other
# ---
# 2 lines l0=P-Q, l1R-S
# P + u( Q - P ) = R + v( S - R )
# ---
# px + u(qx - px) = rx + v(sx - rx)
# py + u(qy - py) = ry + v(sy - ry)
# ---
# solve equation by u, v
lassign $l0 px py qx qy
lassign $l1 rx ry sx sy
set x1 [expr {$qx - $px}]
set x2 [expr {$rx - $sx}]
set xr [expr {$rx - $px}]
set y1 [expr {$qy - $py}]
set y2 [expr {$ry - $sy}]
set yr [expr {$ry - $py}]
set det [list [list $x1 $x2] [list $y1 $y2]]
set divisor [Det2val $det]
if {$divisor != 0} then {
set uDet [list [list $xr $x2] [list $yr $y2]]
set uDetVal [Det2val $uDet]
set u [expr {$uDetVal / $divisor}]
set vDet [list [list $x1 $xr] [list $y1 $yr]]
set vDetVal [Det2val $vDet]
set v [expr {$vDetVal / $divisor}]
if {0 < $u && $u < 1 && 0 < $v && $v < 1} then {
list $u $v
}
}
}
proc bezier::Cross1 {bez1 bez2 {limit 1e-5}} {
lassign $bez1 ax0 ay0 ax1 ay1 ax2 ay2 ax3 ay3
lassign $bez2 bx0 by0 bx1 by1 bx2 by2 bx3 by3
set ax "$ax0 $ax1 $ax2 $ax3"
set ay "$ay0 $ay1 $ay2 $ay3"
set bx "$bx0 $bx1 $bx2 $bx3"
set by "$by0 $by1 $by2 $by3"
if {
[min {*}$ax] < [max {*}$bx] &&
[min {*}$ay] < [max {*}$by] &&
[max {*}$ax] > [min {*}$bx] &&
[max {*}$ay] > [min {*}$by]
} then {
set width [- [max {*}$ax {*}$bx] [min {*}$ax {*}$bx]]
set height [- [max {*}$ay {*}$by] [min {*}$ay {*}$by]]
if {max($width, $height) < $limit} then {
# only true crossing, no touching!
lineCutLine [lreplace $bez1 2 end-2] [lreplace $bez2 2 end-2]
} else {
set bez1a [segment $bez1 0 0.5]
set bez2a [segment $bez2 0 0.5]
set bez1b [segment $bez1 0.5 1]
set bez2b [segment $bez2 0.5 1]
lassign [Cross1 $bez1a $bez2a $limit] frac1 frac2
if {$frac1 ne ""} then {
list [expr {$frac1 / 2.0}] [expr {$frac2 / 2.0}]
} else {
lassign [Cross1 $bez1a $bez2b $limit] frac1 frac2
if {$frac1 ne ""} then {
list [expr {$frac1 / 2.0}] [expr {$frac2 / 2.0 + 0.5}]
} else {
lassign [Cross1 $bez1b $bez2a $limit] frac1 frac2
if {$frac1 ne ""} then {
list [expr {$frac1 / 2.0 + 0.5}] [expr {$frac2 / 2.0}]
} else {
lassign [Cross1 $bez1b $bez2b $limit] frac1 frac2
if {$frac1 ne ""} then {
list [expr {$frac1 / 2.0 + 0.5}] [expr {$frac2 / 2.0 + 0.5}]
}
}
}
}
}
}
}
proc bezier::Nearby {x0 y0 x1 y1 {limit 1e-5}} {
expr {
abs($x1 - $x0) <= $limit && abs($y1 - $y0) <= $limit
}
}
proc bezier::BezierPosNearby {coords pos1 pos2 {limit 1e-5}} {
Nearby {*}[At $coords $pos1] {*}[At $coords $pos2] $limit
}
proc bezier::CoordsReverse coords {
concat {*}[lreverse [lmap {x y} $coords {list $x $y}]]
}
proc bezier::cut {bez1 bez2 {limit 1e-5} {tolerant no}} {
if {
!$tolerant &&
( [Nearby {*}[At $bez1 0.0] {*}[At $bez2 0.0] $limit] ||
[Nearby {*}[At $bez1 1.0] {*}[At $bez2 1.0] $limit] )
} then {
lassign [cut [CoordsReverse $bez1] $bez2 $limit yes] frac1 frac2
if {$frac2 ne {}} then {
list [expr {1.0 - $frac1}] $frac2
}
} else {
lassign [Cross1 $bez1 $bez2 $limit] frac1 frac2
if {$frac2 ne {}} then {
if {[BezierPosNearby $bez1 $frac1 0]} then {
set frac1 0.0
} elseif {[BezierPosNearby $bez1 $frac1 1.0]} then {
set frac1 1.0
}
if {[BezierPosNearby $bez2 $frac2 0]} then {
set frac2 0.0
} elseif {[BezierPosNearby $bez2 $frac2 1.0]} then {
set frac2 1.0
}
if {(0 < $frac1 && $frac1 < 1) || (0 < $frac2 && $frac2 < 1)} then {
list $frac1 $frac2
}
}
}
}
proc bezier::cuts {bez1 bez2 {limit 1e-5}} {
lassign [cut $bez1 $bez2 $limit] frac1 frac2
if {$frac2 ne {}} then {
lappend result $frac1 $frac2
#
set seg1a [segment $bez1 0 $frac1]
set seg2a [segment $bez2 0 $frac2]
set seg1b [segment $bez1 $frac1 1.0]
set seg2b [segment $bez2 $frac2 1.0]
#
foreach {f1 f2} [cuts $seg1a $seg2a $limit] {
lappend result\
[expr {$f1 * $frac1}]\
[expr {$f2 * $frac2}]
}
foreach {f1 f2} [cuts $seg1a $seg2b $limit] {
lappend result\
[expr {$f1 * $frac1}]\
[expr {$frac2 + $f2 * (1-$frac2)}]
}
foreach {f1 f2} [cuts $seg1b $seg2a $limit] {
lappend result\
[expr {$frac1 + $f1 * (1-$frac1)}]\
[expr {$f2 * $frac2}]
}
foreach {f1 f2} [cuts $seg1b $seg2b $limit] {
lappend result\
[expr {$frac1 + $f1 * (1-$frac1)}]\
[expr {$frac2 + $f2 * (1-$frac2)}]
}
set result
}
}
proc bezier::help {} {
foreach proc [namespace export] {
set line "[namespace tail [namespace current]] $proc"
foreach arg [info args $proc] {
if {[info default $proc $arg def]} then {
lappend line [list $arg $def]
} else {
lappend line $arg
}
}
lappend result $line
}
join $result \n
}
namespace eval bezier {
namespace export bbox tangens2 segment cut cuts help lineCutLine
namespace ensemble create
}
29.3.2022
<< | Heimatseite | Verzeichnis | Stichworte | Autor | >>