1. Overall structure
The overall structure of the script is simple: There is a single block for each of the 5 possible commands.
/^#/ { ...
}
/^!/ { ...
}
/^%/ { ...
}
/^~/ { ...
}
/^\$.../ { ...
}
The state of the script is contained in the following eleven variables:
- rem: the prefix for shell comments, to be set in the gawk call.
- valuS[v]: value of scalar variable v.
- stepS[v]: increment of scalar variable v.
- moduS[v]: modulus of scalar variable v.
- frmtS[v]: print format of scalar variable v (necessary because modulus controls leading zeros).
- valuX[v]: x value of vector variable v.
- valuY[v]: x value of vector variable v.
- stepXOrM[v]: either a number → increment for x value of vector variable v; or a string starting with % → anchor for second format of vector increment.
- stepYOrA[v]: increment for y value of vector variable v (if stepXOrM[v] is a number); or angle increment (if stepXOrM[v] starts with %).
- currcmd: The previous command.
- out: The flag remembering the ~ state.
2. Explicit Comments
This is easy: We print rem, the line number, and the text; and then continue with the next line ("next"):
/^#/ { print rem " (" NR ":" $0 ")"
next
}
3.Scalar variable assignment
This straightforward code consists of four if statements:
- The first one checks that there are exactly three parameters present (a rudimentary attempt at error handling—more of this is needed ...). The print statements uses gawk's extension > "/dev/stderr" which works also under Windows.
- The other three store the respective values in valuS, stepS, and moduS, unless a dot was specified. frmtS is set when the value or the modulus is set.0 + ... is the AWK idiom for converting a string to a number:
/^!/ { if (NF != 4) {
print NR ": ERROR - exactly 3 parameters expected in " $0 > "/dev/stderr"
next
}
if ($2 != ".") {
valuS[$1] = 0 + $2
frmtS[$1] = "%0d"
}
if ($3 != ".") {
stepS[$1] = 0 + $3
}
if ($4 != ".") {
moduS[$1] = 0 + $4
frmtS[$1] = "%0" int(log(moduS[$1]) / log(10) + 1) "d"
}
print rem " v=" valuS[$1] ...more debugging output...
next
}
4. Vector variable assignment
Also vector variable assignments just store the values:
/^%/ { if (NF != 5) {
print NR ": ERROR - exactly 4 parameters expected in " $0 > "/dev/stderr"
next
}
if ($2 != ".") {
valuX[$1] = 0 + $2
}
if ($3 != ".") {
valuY[$1] = 0 + $3
}
if ($4 != ".") {
stepXOrM[$1] = $4
}
if ($5 != ".") {
stepYOrA[$1] = 0 + $5
}
print rem " v=" valuS[$1] ":" valuY[$1] " ...more debugging output...
next
}
5. Handling ~
This is simple:
/^~/ { out = $2
next
}
6. Command handling
Obviously, this is the interesting part. A $ line must contain a subsequent number of steps. Therefore, the script checks not only for a $ at the beginning of the line, but also for a subsequent number. The steps are remembered, then we collect the command if it is present. At last, the command is emitted multiple times. Here is the overall structure:
/^\$[ \t]+[0-9]+[ \t]+/ {
steps = 0 + $2
...remove leading $ and $2
if ($0 != "") {
...collect lines with trailing \ into currcmd
}
for (i = 0; i < steps; i++) {
cmd = currcmd
...interpolate scalar variables in cmd
...interpolate vector variables in cmd
print cmd
...increment scalar variables
...increment vector variables
}
}
Removing the leading $ and count is easy in AWK:
sub(/^\$[ \t]+[0-9]+[ \t]+/, "")
If there is a command after the count, we collect it in variable currcmd. Handling trailing \ for command concatenation requires five more lines:
if ($0 != "") {
currcmd = $0
while (/[\\]$/) {
sub(/[\\]$/, " ")
getline
currcmd = currcmd $0
}
}
Inside the emitting loop, we handle variables, print the output line, and finally increment the variables.
Replacing scalars has a few special points:
- First, all values are rounded to the nearest integer. The reason is that the images work with pixels.
- If modulus is set, we take the variable value modulo that modulus
- And finally, we format the value using the current format.
for (v in valuS) {
vv = int(valuS[v]+0.5)
if (moduS[v] > 0) vv %= moduS[v]
vv = sprintf(frmtS[v], vv);
gsub(v, vv, cmd)
}
Replacing vectors is actually easier than scalars, as there are no formats and modulus for vectors. Also here, we round to the nearest integer:
for (v in valuX) {
vx = int(valuX[v]+0.5);
vy = int(valuY[v]+0.5);
gsub(v ":x", vx, cmd)
gsub(v ":y", vy, cmd)
}
After (possibly) printing the command, we have to increment the values. For scalars, this is a "one-liner":
for (v in valuS) {
valuS[v] += stepS[v]
}
For vectors, we have two variants. One is easy—we just add the increments. The 0 + is needed here because stepXOrM is not coerced to a number when reading because it might be a %name:
for (v in valuX) {
if (stepXOrM[v] ~ /^%/) {
...angle increment - see below...
} else {
valuX[v] += 0 + stepXOrM[v]
valuY[v] += stepYOrA[v]
}
}
Finally, we need some simple trigonometry for the angle increment. Here is the algorithm:
- First, we need the coordinates of the rotation center (mx and my).
- Then, we compute the delta vector between the current location and the rotation center (dx and dy).
- We convert this to angular form (r and phi) and immediately add the angle increment, after converting it from degrees to radians.
- Finally, we compute the new location by transforming and adding the new delta vector to the rotation center:
mx = valuX[stepXOrM[v]]
my = valuY[stepXOrM[v]]
dx = valuX[v] - mx
dy = valuY[v] - my
r = sqrt(dx*dx + dy*dy)
phi = atan2(dy, dx) + (stepYOrA[v] / 180 * 3.1416)
valuX[v] = mx + r * cos(phi)
valuY[v] = my + r * sin(phi)
And this is it!
The next posting will contain two examples of simple animations and, additionally, some arguments on why cranks are difficult.
No comments:
Post a Comment