diff --git a/docs/chapters/arclength/content.en-GB.md b/docs/chapters/arclength/content.en-GB.md
index 8ca6dca4..37a67ee9 100644
--- a/docs/chapters/arclength/content.en-GB.md
+++ b/docs/chapters/arclength/content.en-GB.md
@@ -24,12 +24,12 @@ So we turn to numerical approaches again. The method we'll look at here is the [
\int_{-1}^{1}f(t) dt
\simeq
\left [
- \underset{strip\ 1}{ \underbrace{ C_1 \cdot f\left(t_1\right) }}
- \ +\ ...
- \ +\ \underset{strip\ n}{ \underbrace{ C_n \cdot f\left(t_n\right) }}
+ \underset{strip~1}{ \underbrace{ C_1 \cdot f\left(t_1\right) }}
+ ~+~...
+ ~+~\underset{strip~n}{ \underbrace{ C_n \cdot f\left(t_n\right) }}
\right ]
=
- \underset{strips\ 1\ through\ n}{
+ \underset{strips~1~through~n}{
\underbrace{
\sum_{i=1}^{n}{
C_i \cdot f\left(t_i\right)
diff --git a/docs/chapters/bsplines/content.en-GB.md b/docs/chapters/bsplines/content.en-GB.md
index c808e0ee..26c3ed73 100644
--- a/docs/chapters/bsplines/content.en-GB.md
+++ b/docs/chapters/bsplines/content.en-GB.md
@@ -76,7 +76,7 @@ That looks complicated, but it's not. Computing alpha is just a fraction involvi
Of course, the recursion does need a stop condition:
\[
- d^k_0(t) = 0, \ d^0_i(t) = N_{i,1}(t) =
+ d^k_0(t) = 0, ~d^0_i(t) = N_{i,1}(t) =
\left\{\begin{matrix}
1& \text{if } t \in [knot_i,knot_{i+1}) \\
0& \text{otherwise}
@@ -90,39 +90,39 @@ Thanks to Cox and de Boor, we can compute points on a B-Spline pretty easily usi
\[
d^3_3 = \left \{
\begin{aligned}
- \alpha^3_3 \times d^2_3, & \ \textit{ with } d^2_3 = \left \{
+ \alpha^3_3 \times d^2_3, & ~\textit{ with } d^2_3 = \left \{
\begin{aligned}
- \alpha^2_3 \times d^1_3, & \ \textit{ with } d^1_3 =
+ \alpha^2_3 \times d^1_3, & ~\textit{ with } d^1_3 =
\left \{
\begin{aligned}
- \alpha^1_3 \times d^0_3, & \ \textit{ with } d^0_3 \textit{ either 0 or 1} \\
+ \alpha^1_3 \times d^0_3, & ~\textit{ with } d^0_3 \textit{ either 0 or 1} \\
+ & \\
- \left ( 1 - \alpha^1_3 \right ) \times d^0_2, & \ \textit{ with } d^0_2 \textit{ either 0 or 1} \\
+ \left ( 1 - \alpha^1_3 \right ) \times d^0_2, & ~\textit{ with } d^0_2 \textit{ either 0 or 1} \\
\end{aligned}
\right . \\
+ & \\
- \left ( 1 - \alpha^2_3 \right ) \times d^1_2, & \ \textit{ with } d^1_2 =
+ \left ( 1 - \alpha^2_3 \right ) \times d^1_2, & ~\textit{ with } d^1_2 =
\left \{
\begin{aligned}
\alpha^1_2 \times d^0_2 & \\
+ & \\
- \left ( 1 - \alpha^1_2 \right ) \times d^0_1, & \ \textit{ with } d^0_1 \textit{ either 0 or 1} \\
+ \left ( 1 - \alpha^1_2 \right ) \times d^0_1, & ~\textit{ with } d^0_1 \textit{ either 0 or 1} \\
\end{aligned}
\right . \\
\end{aligned}
\right . \\
+ & \\
- \left ( 1 - \alpha^3_3 \right ) \times d^2_2, & \ \textit{ with } d^2_2 = \left \{
+ \left ( 1 - \alpha^3_3 \right ) \times d^2_2, & ~\textit{ with } d^2_2 = \left \{
\begin{aligned}
\alpha^2_2 \times d^1_2 & \\
& \\
+ & \\
- \left ( 1 - \alpha^2_2 \right ) \times d^1_1, & \ \textit{ with } d^1_1 =
+ \left ( 1 - \alpha^2_2 \right ) \times d^1_1, & ~\textit{ with } d^1_1 =
\left \{
\begin{aligned}
\alpha^1_1 \times d^0_1 \\
+ & \\
- \left ( 1 - \alpha^1_1 \right ) \times d^0_0, & \ \textit{ with } d^0_0 \textit{ either 0 or 1} \\
+ \left ( 1 - \alpha^1_1 \right ) \times d^0_0, & ~\textit{ with } d^0_0 \textit{ either 0 or 1} \\
\end{aligned}
\right . \\
\end{aligned}
diff --git a/docs/chapters/circles/content.en-GB.md b/docs/chapters/circles/content.en-GB.md
index 3ddfd5d5..6fae14f0 100644
--- a/docs/chapters/circles/content.en-GB.md
+++ b/docs/chapters/circles/content.en-GB.md
@@ -19,13 +19,13 @@ As you can see, things go horribly wrong quite quickly; even trying to approxima
We start out with our start and end point, and for convenience we will place them on a unit circle (a circle around 0,0 with radius 1), at some angle *φ*:
\[
- S = \begin{pmatrix} 1 \\ 0 \end{pmatrix} \ , \ \ E = \begin{pmatrix} cos(φ) \\ sin(φ) \end{pmatrix}
+ S = \begin{pmatrix} 1 \\ 0 \end{pmatrix} ~, ~\ E = \begin{pmatrix} cos(φ) \\ sin(φ) \end{pmatrix}
\]
What we want to find is the intersection of the tangents, so we want a point C such that:
\[
- C = S + a \cdot \begin{pmatrix} 0 \\ 1 \end{pmatrix} \ , \ \ C = E + b \cdot \begin{pmatrix} -sin(φ) \\ cos(φ) \end{pmatrix}
+ C = S + a \cdot \begin{pmatrix} 0 \\ 1 \end{pmatrix} ~, ~\ C = E + b \cdot \begin{pmatrix} -sin(φ) \\ cos(φ) \end{pmatrix}
\]
i.e. we want a point that lies on the vertical line through S (at some distance *a* from S) and also lies on the tangent line through E (at some distance *b* from E). Solving this gives us:
@@ -41,8 +41,8 @@ First we solve for *b*:
\[
\begin{array}{l}
- 1 = cos(φ) + b \cdot -sin(φ) \ → \
- 1 - cos(φ) = -b \cdot sin(φ) \ → \
+ 1 = cos(φ) + b \cdot -sin(φ) ~→ \
+ 1 - cos(φ) = -b \cdot sin(φ) ~→ \
-1 + cos(φ) = b \cdot sin(φ)
\end{array}
\]
@@ -68,7 +68,7 @@ which we can then substitute in the expression for *a*:
A quick check shows that plugging these values for *a* and *b* into the expressions for Cx and Cy give the same x/y coordinates for both "*a* away from A" and "*b* away from B", so let's continue: now that we know the coordinate values for C, we know where our on-curve point T for *t=0.5* (or angle φ/2) is, because we can just evaluate the Bézier polynomial, and we know where the circle arc's actual point P is for angle φ/2:
\[
- P_x = cos(\frac{φ}{2}) \ , \ \ P_y = sin(\frac{φ}{2})
+ P_x = cos(\frac{φ}{2}) ~, ~\ P_y = sin(\frac{φ}{2})
\]
We compute T, observing that if *t=0.5*, the polynomial values (1-t)², 2(1-t)t, and t² are 0.25, 0.5, and 0.25 respectively:
@@ -93,10 +93,10 @@ And the distance between these two is the standard Euclidean distance:
\[
\begin{aligned}
- d_x(φ) &= T_x - P_x = \frac{1}{4}(3 + cos(φ)) - cos(\frac{φ}{2}) = 2sin^4\left(\frac{φ}{4}\right) \ , \\
- d_y(φ) &= T_y - P_y = \frac{1}{4}\left(2tan\left(\frac{φ}{2}\right) + sin(φ)\right) - sin(\frac{φ}{2}) \ , \\
+ d_x(φ) &= T_x - P_x = \frac{1}{4}(3 + cos(φ)) - cos(\frac{φ}{2}) = 2sin^4\left(\frac{φ}{4}\right) ~, \\
+ d_y(φ) &= T_y - P_y = \frac{1}{4}\left(2tan\left(\frac{φ}{2}\right) + sin(φ)\right) - sin(\frac{φ}{2}) ~, \\
&⇓\\
- d(φ) &= \sqrt{d^2_x + d^2_y} = \ ... \ = 2sin^4(\frac{φ}{4})\sqrt{\frac{1}{cos^2(\frac{φ}{2})}}
+ d(φ) &= \sqrt{d^2_x + d^2_y} = ~... ~ = 2sin^4(\frac{φ}{4})\sqrt{\frac{1}{cos^2(\frac{φ}{2})}}
\end{aligned}
\]
diff --git a/docs/chapters/circles_cubic/content.en-GB.md b/docs/chapters/circles_cubic/content.en-GB.md
index de372d2f..e3f18b0f 100644
--- a/docs/chapters/circles_cubic/content.en-GB.md
+++ b/docs/chapters/circles_cubic/content.en-GB.md
@@ -39,7 +39,7 @@ In fact, the precision of a cubic curve at a quarter circle is considered "good
So with the error analysis out of the way, how do we actually compute the coordinates needed to get that "true fit" cubic curve? The first observation is that we already know the start and end points, because they're the same as for the quadratic attempt:
-\[ S = \begin{pmatrix} 1 \\ 0 \end{pmatrix} \ , \ \ E = \begin{pmatrix} cos(φ) \\ sin(φ) \end{pmatrix} \]
+\[ S = \begin{pmatrix} 1 \\ 0 \end{pmatrix} ~, ~\ E = \begin{pmatrix} cos(φ) \\ sin(φ) \end{pmatrix} \]
But we now need to find two control points, rather than one. If we want the derivatives at the start and end point to match the circle, then the first control point can only lie somewhere on the vertical line through S, and the second control point can only lie somewhere on the line tangent to point E, which means:
@@ -74,17 +74,17 @@ The distance from our guessed point to the start point is exactly the same as th
So that just leaves us to find the distance from t=0.5 to the baseline for an arbitrary angle φ, which is the distance from the centre of the circle to our t=0.5 point, minus the distance from the centre to the line that runs from start point to end point. The first is the same as the point P we found for the quadratic curve:
\[
- P_x = cos(\frac{φ}{2}) \ , \ \ P_y = sin(\frac{φ}{2})
+ P_x = cos(\frac{φ}{2}) ~, ~\ P_y = sin(\frac{φ}{2})
\]
And the distance from the origin to the line start/end is another application of angles, since the triangle {origin,start,C} has known angles, and two known sides. We can find the length of the line {origin,C}, which lets us trivially compute the coordinate for C:
\[
\begin{array}{l}
- l = cos(\frac{φ}{2}) \ , \\
+ l = cos(\frac{φ}{2}) ~, \\
\left\{\begin{array}{l}
- C_x = l \cdot cos\left(\frac{φ}{2}\right) = cos^2\left(\frac{φ}{2}\right)\ , \\
- C_y = l \cdot sin\left(\frac{φ}{2}\right) = cos(\frac{φ}{2}) \cdot sin\left(\frac{φ}{2}\right)\ , \\
+ C_x = l \cdot cos\left(\frac{φ}{2}\right) = cos^2\left(\frac{φ}{2}\right)~, \\
+ C_y = l \cdot sin\left(\frac{φ}{2}\right) = cos(\frac{φ}{2}) \cdot sin\left(\frac{φ}{2}\right)~, \\
\end{array}\right.
\end{array}
\]
@@ -121,9 +121,9 @@ And after this tedious detour to find the coordinate for C1, we can f
\[
\begin{array}{l}
- E'_x = -sin(φ) \ ,\\
- E'_y = cos(φ) \ , \\
- ||E'|| = \sqrt{ (-sin(φ))^2 + cos^2(φ)} = 1 \ , \\
+ E'_x = -sin(φ) ~,\\
+ E'_y = cos(φ) ~, \\
+ ||E'|| = \sqrt{ (-sin(φ))^2 + cos^2(φ)} = 1 ~, \\
\\
\left\{\begin{array}{l}
C_2x = E_x - C_{1y} \cdot \frac{E_x'}{||E'||}
@@ -146,7 +146,7 @@ So, to recap, given an angle φ, the new control coordinates are:
C_1 = \left [ \begin{matrix}
1 \\
f
- \end{matrix} \right ],\ with\ f = \frac{4}{3} tan \left( \frac{φ}{4} \right)
+ \end{matrix} \right ],~with~f = \frac{4}{3} tan \left( \frac{φ}{4} \right)
\]
and
@@ -155,16 +155,16 @@ and
C_2 = \left [ \begin{matrix}
cos(φ) + f \cdot sin(φ) \\
sin(φ) - f \cdot cos(φ)
- \end{matrix} \right ],\ with\ f = \frac{4}{3} tan \left( \frac{φ}{4} \right)
+ \end{matrix} \right ],~with~f = \frac{4}{3} tan \left( \frac{φ}{4} \right)
\]
And, because the "quarter curve" special case comes up so incredibly often, let's look at what these new control points mean for the curve coordinates of a quarter curve, by simply filling in φ = π/2:
\[
\begin{array}{l}
- S = (1, 0) \ , \
- C_1 = \left ( 1, 4 \frac{\sqrt{2}-1}{3} \right ) \ , \
- C_2 = \left ( 4 \frac{\sqrt{2}-1}{3} , 1 \right ) \ , \
+ S = (1, 0) ~, \
+ C_1 = \left ( 1, 4 \frac{\sqrt{2}-1}{3} \right ) ~, \
+ C_2 = \left ( 4 \frac{\sqrt{2}-1}{3} , 1 \right ) ~, \
E = (0, 1)
\end{array}
\]
@@ -173,9 +173,9 @@ Which, in decimal values, rounded to six significant digits, is:
\[
\begin{array}{l}
- S = (1, 0) \ , \
- C_1 = (1, 0.55228) \ , \
- C_2 = (0.55228 , 1) \ , \
+ S = (1, 0) ~, \
+ C_1 = (1, 0.55228) ~, \
+ C_2 = (0.55228 , 1) ~, \
E = (0, 1)
\end{array}
\]
diff --git a/docs/chapters/control/content.en-GB.md b/docs/chapters/control/content.en-GB.md
index 07bca9e7..88f23919 100644
--- a/docs/chapters/control/content.en-GB.md
+++ b/docs/chapters/control/content.en-GB.md
@@ -22,9 +22,9 @@ If we want to change the curve, we need to change the weights of each point, eff
\[
Bézier(n,t) = \sum_{i=0}^{n}
- \underset{binomial\ term}{\underbrace{\binom{n}{i}}}
+ \underset{binomial~term}{\underbrace{\binom{n}{i}}}
\cdot\
- \underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}}
+ \underset{polynomial~term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}}
\cdot\
\underset{weight}{\underbrace{w_i}}
\]
diff --git a/docs/chapters/control/content.zh-CN.md b/docs/chapters/control/content.zh-CN.md
index 9f00f5f2..b5d58dac 100644
--- a/docs/chapters/control/content.zh-CN.md
+++ b/docs/chapters/control/content.zh-CN.md
@@ -22,9 +22,9 @@
\[
Bézier(n,t) = \sum_{i=0}^{n}
- \underset{binomial\ term}{\underbrace{\binom{n}{i}}}
+ \underset{binomial~term}{\underbrace{\binom{n}{i}}}
\cdot\
- \underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}}
+ \underset{polynomial~term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}}
\cdot\
\underset{weight}{\underbrace{w_i}}
\]
diff --git a/docs/chapters/derivatives/content.en-GB.md b/docs/chapters/derivatives/content.en-GB.md
index a9bf0c35..d51bb3ef 100644
--- a/docs/chapters/derivatives/content.en-GB.md
+++ b/docs/chapters/derivatives/content.en-GB.md
@@ -66,7 +66,7 @@ And that's the first part done: the two components inside the parentheses are ac
... = n \left (
\frac{x!}{y!(x-y)!} t^{y} (1-t)^{x-y} - \frac{x!}{k!(x-k)!} t^k (1-t)^{x-k}
\right )
- \ ,\ with\ x=n-1,\ y=k-1
+ ~,~with~x=n-1,~y=k-1
\\
... = n \left ( B_{(n-1),(k-1)}(t) - B_{(n-1),k}(t) \right )
\end{array}
@@ -98,7 +98,7 @@ Two of these terms fall way: the first term falls away because there is no -1ratio0 = 1
, ratio1 = 0.5
, and so on, and is effectively identical as if we were just using different weight. So far, nothing too special.
diff --git a/docs/chapters/whatis/content.en-GB.md b/docs/chapters/whatis/content.en-GB.md
index 60f4b918..61ba77ef 100644
--- a/docs/chapters/whatis/content.en-GB.md
+++ b/docs/chapters/whatis/content.en-GB.md
@@ -9,12 +9,12 @@ If we know the distance between those two points, and we want a new point that i
\[
Given \left (
\begin{aligned}
- p_1 &= some\ point \\
- p_2 &= some\ other\ point \\
+ p_1 &= some~point \\
+ p_2 &= some~other~point \\
distance &= (p_2 - p_1) \\
ratio &= \frac{percentage}{100} \\
\end{aligned}
-\right ),\ our\ new\ point = p_1 + distance \cdot ratio
+\right ),~our~new~point = p_1 + distance \cdot ratio
\]
So let's look at that in action: the following graphic is interactive in that you can use your up and down arrow keys to increase or decrease the interpolation ratio, to see what happens. We start with three points, which gives us two lines. Linear interpolation over those lines gives us two points, between which we can again perform linear interpolation, yielding a single point. And that point —and all points we can form in this way for all ratios taken together— form our Bézier curve:
diff --git a/docs/chapters/whatis/content.zh-CN.md b/docs/chapters/whatis/content.zh-CN.md
index 8f49c56c..5f618bf8 100644
--- a/docs/chapters/whatis/content.zh-CN.md
+++ b/docs/chapters/whatis/content.zh-CN.md
@@ -9,12 +9,12 @@
\[
Given \left (
\begin{aligned}
- p_1 &= some\ point \\
- p_2 &= some\ other\ point \\
+ p_1 &= some~point \\
+ p_2 &= some~other~point \\
distance &= (p_2 - p_1) \\
ratio &= \frac{percentage}{100} \\
\end{aligned}
-\right ),\ our\ new\ point = p_1 + distance \cdot ratio
+\right ),~our~new~point = p_1 + distance \cdot ratio
\]
让我们来通过实际操作看一下:下面的图形都是可交互的,因此你可以通过上下键来增加或减少插值距离,来观察图形的变化。我们从三个点构成的两条线段开始。通过对各条线段进行线性插值得到两个点,对点之间的线段再进行线性插值,产生一个新的点。最终这些点——所有的点都可以通过选取不同的距离插值产生——构成了贝塞尔曲线
diff --git a/docs/images/chapters/aligning/00480d8ea1d0b86eb66939bced85e14b.svg b/docs/images/chapters/aligning/00480d8ea1d0b86eb66939bced85e14b.svg
new file mode 100644
index 00000000..01665af8
--- /dev/null
+++ b/docs/images/chapters/aligning/00480d8ea1d0b86eb66939bced85e14b.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/images/chapters/aligning/041c2f2d5c115e27f059595775732dfa.svg b/docs/images/chapters/aligning/041c2f2d5c115e27f059595775732dfa.svg
deleted file mode 100644
index b69dd310..00000000
--- a/docs/images/chapters/aligning/041c2f2d5c115e27f059595775732dfa.svg
+++ /dev/null
@@ -1 +0,0 @@
-
So let's look at that in action: the following graphic is interactive in that you can use your up and down arrow keys to increase or decrease the interpolation ratio, to see what happens. We start with three points, which gives us two lines. Linear interpolation over @@ -557,18 +570,37 @@ function". An illustration: Let's say we have a function that maps some value, let's call it x, to some other value, using some kind of number manipulation:
+The notation f(x) is the standard way to show that it's a function (by convention called f if we're only listing one) and its output changes based on one variable (in this case, x). Change x, and the output for f(x) changes.
So far, so good. Now, let's look at parametric functions, and how they cheat. Let's take the following two functions:
+There's nothing really remarkable about them, they're just a sine and cosine function, but you'll notice the inputs have different names. If we change the value for a, we're not going to change the output value for f(b), since a isn't used in that function. Parametric functions cheat by changing that. In a parametric function all the different functions share a variable, like this:
+There we go. x/y coordinates, linked through some mystery value t.
@@ -606,6 +644,12 @@ binomial polynomials?
You may remember polynomials from high school. They're those sums that look like this:
+That looks complicated, but as it so happens, the "weights" are actually just the coordinate values we want our curve to have: for an nth order curve, w0 is our start coordinate, wn is our last coordinate, and everything in between is a controlling coordinate. Say we want a cubic curve that starts at (110,150), is controlled by (25,190) and (210,250) and ends at (210,30), we use this Bézier curve:
+Which gives us the curve we saw at the top of the article:
Adding these ratio values to the regular Bézier curve function is fairly easy. Where the regular function is the following:
+The function for rational Bézier curves has two more terms:
+
The obvious start and end values here need to be a=1, b=0
, so that the mixed value is 100% value 1, and 0% value 2, and
@@ -1267,6 +1386,12 @@ function RationalBezier(3,t,w[],r[]):
and end point, so we need to make sure we can never set "a" and "b" to some values that lead to a mix value that sums to more than 100%.
And that's easy:
With this we can guarantee that we never sum above 100%. By restricting a
to values in the interval [0,1], we will always be
@@ -1333,27 +1458,122 @@ function RationalBezier(3,t,w[],r[]):
coefficients matrix, and the actual coordinates as a matrix. Let's look at what this means for the cubic curve, using P... to
refer to coordinate values "in one or more dimensions":
Disregarding our actual coordinates for a moment, we have:
+We can write this as a sum of four expressions:
+And we can expand these expressions:
+Furthermore, we can make all the 1 and 0 factors explicit:
+And that, we can view as a series of four matrix operations:
+If we compact this into a single matrix operation, we get:
+
This kind of polynomial basis representation is generally written with the bases in increasing order, which means we need to flip our
t
matrix horizontally, and our big "mixing" matrix upside down:
And then finally, we can add in our original coordinates as a single third matrix:
+We can perform the same trick for the quadratic curve, in which case we end up with:
+
If we plug in a t
value, and then multiply the matrices, we will get exactly the same values as when we evaluate the original
@@ -1755,6 +1975,16 @@ function drawCurve(points[], t):
we saw that we can represent curves as matrix multiplications. Specifically, we saw these two forms for the quadratic and cubic curves
respectively: (we'll reverse the Bézier coefficients vector for legibility)
and
+and
+Excellent! Now we can form our new quadratic curve:
+If we want the interval [z,1], we will be evaluating this instead:
+[M · something]
:
+
So, our final second curve looks like:
+and
+and
+However, this rule also has as direct consequence that you cannot generally safely lower a curve from nth order to (n-1)th order, because the control points cannot be "pulled apart" cleanly. We can @@ -2037,22 +2522,58 @@ function drawCurve(points[], t): some things can be done much more easily with matrices than with calculus functions, and this is one of those things. So... let's go!
We start by taking the standard Bézier function, and condensing it a little:
+
Then, we apply one of those silly (actually, super useful) calculus tricks: since our t
value is always between zero and one
(inclusive), we know that (1-t)
plus t
always sums to 1. As such, we can express any value as a sum of
t
and 1-t
:
So, with that seemingly trivial observation, we rewrite that Bézier function by splitting it up into a sum of a (1-t)
and
t
component:
So far so good. Now, to see why we did this, let's write out the (1-t)
and t
parts, and see what that gives us.
I promise, it's about to make sense. We start with (1-t)
:
Let's do this:
+where the matrix M is an n+1
by n
matrix, and looks like:
First, let's look at the derivative rule for Bézier curves, which is:
+Which is hard to work with, so let's expand that properly:
+And that's the first part done: the two components inside the parentheses are actually regular, lower-order Bézier expressions:
+And that's just a summation of lower order curves:
+We can rewrite this as a normal summation, and we're done:
+Which is the "long" version of the following matrix transformation:
+And then we turn this into our solution for t
using basic arithmetics:
And then, using these v values, we can find out what our a, b, and c should be:
+v
values, where the v
values are
expressions of our original coordinate values, so we can do some substitution to get:
+
n=1
, we perform the following calculation until
fy(x)
is zero, so that the next t
is the same as the one we already have:
+
Then translating it so that the first coordinate lies on (0,0), moving all x coordinates by -120, and all y coordinates by -160, gives us:
-If we then rotate the curve so that its end point lies on the x-axis, the coordinates (integer-rounded for illustrative purposes here) become:
-If we drop all the zero-terms, this gives us:
-We can see that our original curve definition has been simplified considerably. The following graphics illustrate the result of aligning our example curves to the x-axis, with the cubic case using the coordinates that were just used in the example formulae: @@ -3554,12 +4458,23 @@ function getCubicRoots(pa, pb, pc, pd) { inflection, and we can find out where those happen relatively easily.
What we need to do is solve a simple equation:
+What we're saying here is that given the curvature function C(t), we want to know for which values of t this function is zero, meaning there is no "curvature", which will be exactly at the point between our circle being on one side of the curve, and our circle being on the other side of the curve. So what does C(t) look like? Actually something that seems not too hard:
+And of course the same functions for y:
+This is a plain quadratic curve, and we know how to solve C(t) = 0; we use the quadratic formula:
+...on the top (blue) edge, the curve's start point touches the curve, forming a loop. This edge is described by the function:
+Sweet! z stays 1, so we can effectively ignore it entirely, but we added some plain values to our x and y coordinates. So, if we want to subtract p1.x and p1.y, we use:
+Running all our coordinates through this transformation gives a new set of coordinates, let's call those U, where the @@ -3807,11 +4831,29 @@ function getCubicRoots(pa, pb, pc, pd) { currently have. This is called shearing, and the typical x-shear matrix and its transformation looks like this:
+So we want some shearing value that, when multiplied by y, yields -x, so our x coordinate becomes zero. That value is simply -x/y, because *-x/y * y = -x*. Done:
+Now, running this on all our points generates a new set of coordinates, let's call those V, which now have point 1 on @@ -3820,6 +4862,19 @@ function getCubicRoots(pa, pb, pc, pd) { point 3 to end up on (1,1), so we can also scale x to make sure its x-coordinate will be 1 after we run the transform. That means we'll be x-scaling by 1/point3x, and y-scaling by point2y. This is really easy:
+Then, finally, this generates a new set of coordinates, let's call those W, of which point 1 lies on (0,0), point 2 lies on (0,1), and @@ -3829,12 +4884,49 @@ function getCubicRoots(pa, pb, pc, pd) { but y-shearing. Additionally, we don't actually want to end up at zero (which is what we did before) so we need to shear towards an offset, in this case 1:
+And this generates our final set of four coordinates. Of these, we already know that points 1 through 3 are (0,0), (0,1) and (1,1), and only the last coordinate is "free". In fact, given any four starting coordinates, the resulting "transformation mapped" coordinate will be:
+Okay, well, that looks plain ridiculous, but: notice that every coordinate value is being offset by the initial translation, and also @@ -3845,12 +4937,36 @@ function getCubicRoots(pa, pb, pc, pd) { First, let's just do that translation step as a "preprocessing" operation so we don't have to subtract the values all the time. What does that leave?
+Suddenly things look a lot simpler: the mapped x is fairly straight forward to compute, and we see that the mapped y actually contains the mapped x in its entirety, so we'll have that part already available when we need to evaluate it. In fact, let's pull out all those common factors to see just how simple this is:
+That's kind of super-simple to write out in code, I think you'll agree. Coding math tends to be easier than the formulae initially make it @@ -3925,16 +5041,31 @@ function getCubicRoots(pa, pb, pc, pd) { cubic case either: because of the kind of curve we're starting with, we know there is only root, simplifying the code we need!
First, let's look at the function for x(t):
+We can rewrite this to a plain polynomial form, by just fully writing out the expansion and then collecting the polynomial factors, as:
+
Nothing special here: that's a standard cubic polynomial in "power" form (i.e. all the terms are ordered by their power of
t
). So, given that a
, b
, c
, d
, and x(t)
are all
known constants, we can trivially rewrite this (by moving the x(t)
across the equal sign) as:
You might be wondering "where did all the other 'minus x' for all the other values a, b, c, and d go?" and the answer there is that they @@ -4014,8 +5145,24 @@ y = curve.get(t).yfy(t), then the length of the curve, measured from start point to some point t = z, is computed using the following seemingly straight forward (if a bit overwhelming) formula:
+or, more commonly written using Leibnitz notation as:
+
This formula says that the length of a parametric curve is in fact equal to the area underneath a function that looks a
@@ -4042,7 +5189,18 @@ y = curve.get(t).y
-
+
+
In plain text: an integral function can always be treated as the sum of an (infinite) number of (infinitely thin) rectangular strips sitting "under" the function's plotted graph. To illustrate this idea, the following graph shows the integral for a sinusoid function. The @@ -4102,6 +5260,20 @@ y = curve.get(t).yz). Thankfully, we can quite easily transform any integral interval to any other integral interval, by shifting and scaling the inputs. Doing so, we get the following:
+Which means that in order for us to approximate the integral, we must plug these values into the approximate function, which gives us:
+So, what does the function look like? This:
+
Which is really just a "short form" that glosses over the fact that we're dealing with functions of t
, so let's expand that a
tiny bit:
And while that's a little more verbose, it's still just as simple to work with as the first function: the curvature at some point on any @@ -4343,6 +5562,13 @@ function kappa(t, B): just do that. But let's take it one step further: we can also compute the associated "radius of curvature", which gives us the implicit circle that "fits" the curve's curvature at any point, using what is possibly the simplest bit of maths found in this entire primer:
+
So let's revisit the previous graphic with the curvature visualised on both sides of our curves, as well as showing the circle that "fits"
@@ -4741,6 +5967,12 @@ lli = function(line1, line2):
So, how can we compute C
? We start with our observation that C
always lies somewhere between the start and ends
points, so logically C
will have a function that interpolates between those two coordinates:
If we can figure out what the function u(t)
looks like, we'll be done. Although we do need to remember that this
@@ -4750,8 +5982,26 @@ lli = function(line1, line2):
>
(with thanks to Boris Zbarsky) shows us the following two formulae:
And
+
So, if we know the start and end coordinates, and we know the t value, we know C, without having to calculate the
@@ -4760,10 +6010,35 @@ lli = function(line1, line2):
pure function of t
, too.
We start by observing that, given A
, B
, and C
, the following always holds:
Working out the maths for this, we see the following two formulae for quadratic and cubic curves:
+And
+
Which now leaves us with some powerful tools: given thee points (start, end, and "some point on the curve"), as well as a
@@ -4771,6 +6046,13 @@ lli = function(line1, line2):
u(t)
function, and once we have C
, we can use our on-curve point (B
) and the
ratio(t)
function to find A
:
With A
found, finding e1
and e2
for quadratic curves is a matter of running the linear
@@ -4779,8 +6061,32 @@ lli = function(line1, line2):
distance ratio between e1
to B
and B
to e2
is the Bézier ratio (1-t):t
,
we can reverse engineer v1
and v2
:
And then reverse engineer the curve's control control points:
+
So: if we have a curve's start and end point, then for any t
value we implicitly know all the ABC values, which (combined
@@ -4804,6 +6110,19 @@ lli = function(line1, line2):
in computing the ABC coordinates, but we can just as easily approximate one by treating the distance between the start and
B
point, and B
and end point as a ratio, using
e1
and
e2
coordinates must obey the standard de Casteljau rule for linear interpolation:
+
B
is located, so we don't up creating a funky curve
with a loop in it. To do this, we can use the atan2 function:
+
t
value and "wherever the cursor is" as target
B
, we can compute the associated C
:
+
And then the associated A
:
And we're done, because that's our new quadratic control point!
And then we (trivially) rearrange the terms across multiple lines:
+With that arrangement, we can easily decompose this as a matrix multiplication:
+We can do the same for the cubic curve, of course. We know the base function for cubics:
+So we write out the expansion and rearrange:
+Which we can then decompose:
+And, of course, we can do this for quartic curves too (skipping the expansion step):
+To get these values, we first compute the general "distance along the polygon" matrix:
+i=n
fall in the [0,1] interval, so we need to scale all values down by whatever the total length of the polygon is:
+
In which we can replace the rather cumbersome "squaring" operation with a more conventional matrix equivalent:
+Which, because of the first and last values in S, means:
+Now we can properly write out the error function as matrix operations:
+Computing T is really more "arranging the numbers":
+Thus:
+Replace point/tangent vector with the expression for all-coordinates:
+and merge the matrices:
+This looks a lot like the Bézier matrix form, which as we saw in the chapter on Bézier curves, should look like this:
+So, if we want to express a Catmull-Rom curve using a Bézier curve, we'll need to turn this Catmull-Rom bit:
+Into something that looks like this:
+And the way we do that is with a fairly straight forward bit of matrix rewriting. We start with the equality we need to ensure:
+Then we remove the coordinate vector from both sides without affecting the equality:
+Then we can "get rid of" the Bézier matrix on the right by left-multiply both with the inverse of the Bézier matrix:
+And now we're basically done. We just multiply those two matrices and we know what V is:
+Or, if your API allows you to specify Catmull-Rom curves using plain coordinates:
+We can effect this quite easily, because we know that the vector from a curve's last control point to its last on-curve point is equal to the derivative vector. If we want to ensure that the first control point of the next curve matches that, all we have to do is mirror that last control point through the last on-curve point. And mirroring any point A through any point B is really simple:
+
So let's implement that and see what it gets us. The following two graphics show a quadratic and a cubic poly-Bézier curve again, but this
@@ -6070,6 +7951,11 @@ for p = 1 to points.length-3 (inclusive):
From a mathematical point of view, an offset curve O(t)
is a curve such that, given our original curve B(t)
,
any point on O(t)
is a fixed distance d
away from coordinate B(t)
. So let's math that:
B(t)
. Easy enough:
+
B'(t)
by its magnitude:
+
b
, we must use the formula we saw earlier. Noting that "length" is usually
denoted with double vertical bars:
+
B'(t)
, with t = 0
as start and t = 1
as end:
+
What we want to find is the intersection of the tangents, so we want a point C such that:
-i.e. we want a point that lies on the vertical line through S (at some distance a from S) and also lies on the tangent line through E (at some distance b from E). Solving this gives us:
+First we solve for b:
-which yields:
+which we can then substitute in the expression for a:
+A quick check shows that plugging these values for a and b into the expressions for Cx and Cy give @@ -6332,13 +8299,54 @@ for p = 1 to points.length-3 (inclusive): coordinate values for C, we know where our on-curve point T for t=0.5 (or angle φ/2) is, because we can just evaluate the Bézier polynomial, and we know where the circle arc's actual point P is for angle φ/2:
-We compute T, observing that if t=0.5, the polynomial values (1-t)², 2(1-t)t, and t² are 0.25, 0.5, and 0.25 respectively:
+Which, worked out for the x and y components, gives:
+And the distance between these two is the standard Euclidean distance:
-So, what does this distance function look like when we plot it for a number of ranges for the angle φ, such as a half circle, quarter circle and eighth circle? @@ -6379,6 +8387,16 @@ for p = 1 to points.length-3 (inclusive): In fact, let's flip the function around, so that if we plug in the precision error, labelled ε, we get back the maximum angle for that precision:
+And frankly, things are starting to look a bit ridiculous at this point, we're doing way more maths than we've ever done, but thankfully @@ -6482,9 +8500,16 @@ for p = 1 to points.length-3 (inclusive): So with the error analysis out of the way, how do we actually compute the coordinates needed to get that "true fit" cubic curve? The first observation is that we already know the start and end points, because they're the same as for the quadratic attempt:
+where "a" is some scaling factor we'll need to find the expression for, and:
+So, to recap, given an angle φ, the new control coordinates are:
+and
+Which, in decimal values, rounded to six significant digits, is:
+t
in the interval [0,1] (where 0 is the start of the curve, and 1 the
end, just like for Bézier curves), by evaluating the following function:
+
Which, honestly, doesn't tell us all that much. All we can see is that a point on a B-Spline curve is defined as "a mix of all the control
@@ -6843,6 +8998,15 @@ for p = 1 to points.length-3 (inclusive):
k
subscript to the N() function applies to.
Then the N() function itself. What does it look like?
+
So this is where we see the interpolation: N(t) for an (i,k)
pair (that is, for a step in the above summation, on a specific
@@ -6850,6 +9014,13 @@ for p = 1 to points.length-3 (inclusive):
iteration where i
goes up, and k
goes down, so it seem reasonable to expect that this recursion has to stop at
some point; obviously, it does, and specifically it does so for the following i
/k
values:
And this function finally has a straight up evaluation: if a t
value lies within a knot-specific interval once we reach a
@@ -6867,11 +9038,25 @@ for p = 1 to points.length-3 (inclusive):
Carl de Boor — came to a mathematically pleasing solution: to compute a point
P(t), we can compute this point by evaluating d(t) on a curve section between knots i
and i+1
:
This is another recursive function, with k values decreasing from the curve order to 1, and the value α (alpha) defined by:
+That looks complicated, but it's not. Computing alpha is just a fraction involving known, plain numbers. And, once we have our alpha @@ -6880,7 +9065,14 @@ for p = 1 to points.length-3 (inclusive): recursion might see computationally expensive, the total algorithm is cheap, as each step only involves very simple maths.
Of course, the recursion does need a stop condition:
-
So, we actually see two stopping conditions: either i
becomes 0, in which case d()
is zero, or
k
becomes zero, in which case we get the same "either 1 or 0" that we saw in the N() function above.
@@ -6890,7 +9082,39 @@ for p = 1 to points.length-3 (inclusive):
Casteljau's algorithm. For instance, if we write out d()
for i=3
and k=3
, we get the following
recursion diagram:
That is, we compute d(3,3)
as a mixture of d(2,3)
and d(2,2)
, where those two are themselves a
mixture of d(1,3)
and d(1,2)
, and d(1,2)
and d(1,1)
, respectively, which are
diff --git a/docs/ja-JP/index.html b/docs/ja-JP/index.html
index d37298bb..26a6f398 100644
--- a/docs/ja-JP/index.html
+++ b/docs/ja-JP/index.html
@@ -33,7 +33,7 @@
-
+
@@ -492,6 +492,19 @@
例えば、2点間の距離がわかっているとして、一方の点から距離の20%だけ離れた(すなわち、もう一方の点から80%離れた)新しい点を求めたい場合、次のようにとても簡単に計算できます。
+では、実際に見てみましょう。下の図はインタラクティブになっています。上下キーで補間の比率が増減しますので、どうなるか確かめてみましょう。最初に3点があり、それを結んで2本の直線が引かれています。この直線の上でそれぞれ線形補間を行うと、2つの点が得られます。この2点の間でさらに線形補間を行うと、1つの点を得ることができます。そして、あらゆる比率に対して同様に点を求め、それをすべて集めると、このようにベジエ曲線ができるのです。 @@ -520,16 +533,35 @@
ベジエ曲線は「パラメトリック」関数の一種です。数学的に言えば、パラメトリック関数というのはインチキです。というのも、「関数」はきっちり定義された用語であり、いくつかの入力を1つの出力に対応させる写像を表すものだからです。いくつかの数値を入れると、1つの数値が出てきます。入れる数値が変わっても、出てくる数値はやはり1つだけです。パラメトリック関数はインチキです。基本的には「じゃあわかった、値を複数個出したいから、関数を複数個使うことにするよ」ということです。例として、ある値xに何らかの操作を行い、別の値へと写す関数があるとします。
+f(x)という記法は、これが関数(1つしかない場合は慣習的にfと呼びます)であり、その出力が1つの変数(この場合はxです)に応じて変化する、ということを示す標準的な方法です。xを変化させると、f(x)の出力が変化します。
ここまでは順調です。では、パラメトリック関数について、これがどうインチキなのかを見てみましょう。以下の2つの関数を考えます。
+注目すべき箇所は特に何もありません。ただの正弦関数と余弦関数です。ただし、入力が別々の名前になっていることに気づくでしょう。仮にaの値を変えたとしても、f(b)の出力の値は変わらないはずです。なぜなら、こちらの関数にはaは使われていないからです。パラメトリック関数は、これを変えてしまうのでインチキなのです。パラメトリック関数においては、どの関数も変数を共有しています。例えば、
+きました。x/y座標です。謎の値tを通して繫がっています。
@@ -558,6 +596,12 @@ ベジエ曲線はパラメトリック関数の一種であり、どの次元に対しても同じ基底関数を使うという点で特徴づけられます。先ほどの例では、xの値とyの値とで異なる関数(正弦関数と余弦関数)を使っていましたが、ベジエ曲線ではxとyの両方で「二項係数多項式」を使います。では、二項係数多項式とは何でしょう?
高校で習った、こんな形の多項式を思い出すかもしれません。
+複雑そうに見えますが、運がいいことに「重み」というのは実はただの座標値です。というのはn次の曲線の場合、w0が始点の座標、wnが終点の座標となり、その間はどれも制御点の座標になります。例えば、始点が(120,160)、制御点が(35,200)と(220,260)、終点が(220,40)となる3次ベジエ曲線は、次のようになります。
+この式からは、記事の冒頭に出てきた曲線が得られます。
Adding these ratio values to the regular Bézier curve function is fairly easy. Where the regular function is the following:
+The function for rational Bézier curves has two more terms:
+このことは、曲線の「始点」から曲線の「終点」までどうやって動かすか、ということにすべて関係しています。2つの値を混ぜ合わせて1つの値をつくる場合、一般の式は次のようになります。
+
明らかに、始点ではa=1, b=0
とする必要があります。こうすれば、値1が100%、値2が0%で混ぜ合わさるからです。また、終点ではa=0, b=1
とする必要があります。こうすれば、値1が0%、値2が100%で混ぜ合わさります。これに加えて、a
とb
を独立にしておきたくはありません。独立になっている場合、何でも好きな値にすることできますが、こうすると例えば「値1が100%かつ値2が100%」のようなことが可能になってしまいます。これはこれで原則としてはかまいませんが、ベジエ曲線の場合は混ぜ合わさった値が常に始点と終点の間になってほしいのです。というわけで、混ぜ合わせの和が100%を決して超えないように、a
とb
の値を設定する必要があります。これは次のようにすれば簡単です。
こうすれば、和が100%を超えることはないと保証できます。a
の値を区間[0,1]に制限してしまえば、混ぜ合わさった値は常に2つの値の間のどこか(両端を含む)になり、また和は常に100%になります。
@@ -1229,26 +1352,121 @@ function RationalBezier(3,t,w[],r[]):
ベジエ曲線は、行列演算の形でも表現することができます。ベジエ曲線の式を多項式基底と係数行列で表し、実際の座標も行列として表現するのです。これがどういうことを意味しているのか、3次ベジエ曲線について見てみましょう。
+実際の座標を一旦無視すると、次のようになります。
+これは、4つの項の和になっています。
+それぞれの項を展開します。
+その上で、係数の0や1もすべて明示的に書けば、このようになります。
+さらに、これは4つの行列演算の和として見ることができます。
+これを1つの行列演算にまとめると、以下のようになります。
+
多項式基底をこのような形で表現する場合、通常はその基底を昇冪の順に並べます。したがって、t
の行列を左右反転させ、大きな「混合」行列は上下に反転させる必要があります。
そして最後に、もともとあった座標を3番目の行列として付け加えます。
+2次ベジエ曲線の場合も同様に変形することができ、最終的には以下のようになります。
+
t
行列についての節では、行列の乗算で曲線が表現できることを確認しました。特に2次・3次のベジエ曲線に関しては、それぞれ以下のような形になりました(読みやすさのため、ベジエの係数ベクトルを反転させています)。
ならびに
+ならびに
+いいですね!これで、新しい2次ベジエ曲線が得られます。
+t = 1
の部分を得るためには、同様の計算をする必要があります。まず、今さっき行ったのは、一般の区間[0,z
]についての計算でした。これは0があるので簡単な形になっていましたが、実際には、次の式を計算していたということがわかります。
+
区間[z,1]を求めたい場合は、かわりに次のような計算になります。
+先ほどと同じ手法を使い、[なにか · M]を[M · なにか]に変えます。
+よって、後半部分の曲線は結局のところ以下のようになります。
+および
+および
+However, this rule also has as direct consequence that you cannot generally safely lower a curve from nth order to (n-1)th order, because the control points cannot be "pulled apart" cleanly. We can @@ -1882,22 +2365,58 @@ function drawCurve(points[], t): some things can be done much more easily with matrices than with calculus functions, and this is one of those things. So... let's go!
We start by taking the standard Bézier function, and condensing it a little:
+
Then, we apply one of those silly (actually, super useful) calculus tricks: since our t
value is always between zero and one
(inclusive), we know that (1-t)
plus t
always sums to 1. As such, we can express any value as a sum of
t
and 1-t
:
So, with that seemingly trivial observation, we rewrite that Bézier function by splitting it up into a sum of a (1-t)
and
t
component:
So far so good. Now, to see why we did this, let's write out the (1-t)
and t
parts, and see what that gives us.
I promise, it's about to make sense. We start with (1-t)
:
Let's do this:
+where the matrix M is an n+1
by n
matrix, and looks like:
First, let's look at the derivative rule for Bézier curves, which is:
+Which is hard to work with, so let's expand that properly:
+And that's the first part done: the two components inside the parentheses are actually regular, lower-order Bézier expressions:
+And that's just a summation of lower order curves:
+We can rewrite this as a normal summation, and we're done:
+Which is the "long" version of the following matrix transformation:
+And then we turn this into our solution for t
using basic arithmetics:
And then, using these v values, we can find out what our a, b, and c should be:
+v
values, where the v
values are
expressions of our original coordinate values, so we can do some substitution to get:
+
n=1
, we perform the following calculation until
fy(x)
is zero, so that the next t
is the same as the one we already have:
+
Then translating it so that the first coordinate lies on (0,0), moving all x coordinates by -120, and all y coordinates by -160, gives us:
-If we then rotate the curve so that its end point lies on the x-axis, the coordinates (integer-rounded for illustrative purposes here) become:
-If we drop all the zero-terms, this gives us:
-We can see that our original curve definition has been simplified considerably. The following graphics illustrate the result of aligning our example curves to the x-axis, with the cubic case using the coordinates that were just used in the example formulae: @@ -3399,12 +4301,23 @@ function getCubicRoots(pa, pb, pc, pd) { inflection, and we can find out where those happen relatively easily.
What we need to do is solve a simple equation:
+What we're saying here is that given the curvature function C(t), we want to know for which values of t this function is zero, meaning there is no "curvature", which will be exactly at the point between our circle being on one side of the curve, and our circle being on the other side of the curve. So what does C(t) look like? Actually something that seems not too hard:
+And of course the same functions for y:
+This is a plain quadratic curve, and we know how to solve C(t) = 0; we use the quadratic formula:
+...on the top (blue) edge, the curve's start point touches the curve, forming a loop. This edge is described by the function:
+Sweet! z stays 1, so we can effectively ignore it entirely, but we added some plain values to our x and y coordinates. So, if we want to subtract p1.x and p1.y, we use:
+Running all our coordinates through this transformation gives a new set of coordinates, let's call those U, where the @@ -3652,11 +4674,29 @@ function getCubicRoots(pa, pb, pc, pd) { currently have. This is called shearing, and the typical x-shear matrix and its transformation looks like this:
+So we want some shearing value that, when multiplied by y, yields -x, so our x coordinate becomes zero. That value is simply -x/y, because *-x/y * y = -x*. Done:
+Now, running this on all our points generates a new set of coordinates, let's call those V, which now have point 1 on @@ -3665,6 +4705,19 @@ function getCubicRoots(pa, pb, pc, pd) { point 3 to end up on (1,1), so we can also scale x to make sure its x-coordinate will be 1 after we run the transform. That means we'll be x-scaling by 1/point3x, and y-scaling by point2y. This is really easy:
+Then, finally, this generates a new set of coordinates, let's call those W, of which point 1 lies on (0,0), point 2 lies on (0,1), and @@ -3674,12 +4727,49 @@ function getCubicRoots(pa, pb, pc, pd) { but y-shearing. Additionally, we don't actually want to end up at zero (which is what we did before) so we need to shear towards an offset, in this case 1:
+And this generates our final set of four coordinates. Of these, we already know that points 1 through 3 are (0,0), (0,1) and (1,1), and only the last coordinate is "free". In fact, given any four starting coordinates, the resulting "transformation mapped" coordinate will be:
+Okay, well, that looks plain ridiculous, but: notice that every coordinate value is being offset by the initial translation, and also @@ -3690,12 +4780,36 @@ function getCubicRoots(pa, pb, pc, pd) { First, let's just do that translation step as a "preprocessing" operation so we don't have to subtract the values all the time. What does that leave?
+Suddenly things look a lot simpler: the mapped x is fairly straight forward to compute, and we see that the mapped y actually contains the mapped x in its entirety, so we'll have that part already available when we need to evaluate it. In fact, let's pull out all those common factors to see just how simple this is:
+That's kind of super-simple to write out in code, I think you'll agree. Coding math tends to be easier than the formulae initially make it @@ -3770,16 +4884,31 @@ function getCubicRoots(pa, pb, pc, pd) { cubic case either: because of the kind of curve we're starting with, we know there is only root, simplifying the code we need!
First, let's look at the function for x(t):
+We can rewrite this to a plain polynomial form, by just fully writing out the expansion and then collecting the polynomial factors, as:
+
Nothing special here: that's a standard cubic polynomial in "power" form (i.e. all the terms are ordered by their power of
t
). So, given that a
, b
, c
, d
, and x(t)
are all
known constants, we can trivially rewrite this (by moving the x(t)
across the equal sign) as:
You might be wondering "where did all the other 'minus x' for all the other values a, b, c, and d go?" and the answer there is that they @@ -3859,8 +4988,24 @@ y = curve.get(t).yfy(t), then the length of the curve, measured from start point to some point t = z, is computed using the following seemingly straight forward (if a bit overwhelming) formula:
+or, more commonly written using Leibnitz notation as:
+
This formula says that the length of a parametric curve is in fact equal to the area underneath a function that looks a
@@ -3887,7 +5032,18 @@ y = curve.get(t).y
-
+
+
In plain text: an integral function can always be treated as the sum of an (infinite) number of (infinitely thin) rectangular strips sitting "under" the function's plotted graph. To illustrate this idea, the following graph shows the integral for a sinusoid function. The @@ -3947,6 +5103,20 @@ y = curve.get(t).yz). Thankfully, we can quite easily transform any integral interval to any other integral interval, by shifting and scaling the inputs. Doing so, we get the following:
+Which means that in order for us to approximate the integral, we must plug these values into the approximate function, which gives us:
+So, what does the function look like? This:
+
Which is really just a "short form" that glosses over the fact that we're dealing with functions of t
, so let's expand that a
tiny bit:
And while that's a little more verbose, it's still just as simple to work with as the first function: the curvature at some point on any @@ -4188,6 +5405,13 @@ function kappa(t, B): just do that. But let's take it one step further: we can also compute the associated "radius of curvature", which gives us the implicit circle that "fits" the curve's curvature at any point, using what is possibly the simplest bit of maths found in this entire primer:
+
So let's revisit the previous graphic with the curvature visualised on both sides of our curves, as well as showing the circle that "fits"
@@ -4586,6 +5810,12 @@ lli = function(line1, line2):
So, how can we compute C
? We start with our observation that C
always lies somewhere between the start and ends
points, so logically C
will have a function that interpolates between those two coordinates:
If we can figure out what the function u(t)
looks like, we'll be done. Although we do need to remember that this
@@ -4595,8 +5825,26 @@ lli = function(line1, line2):
>
(with thanks to Boris Zbarsky) shows us the following two formulae:
And
+
So, if we know the start and end coordinates, and we know the t value, we know C, without having to calculate the
@@ -4605,10 +5853,35 @@ lli = function(line1, line2):
pure function of t
, too.
We start by observing that, given A
, B
, and C
, the following always holds:
Working out the maths for this, we see the following two formulae for quadratic and cubic curves:
+And
+
Which now leaves us with some powerful tools: given thee points (start, end, and "some point on the curve"), as well as a
@@ -4616,6 +5889,13 @@ lli = function(line1, line2):
u(t)
function, and once we have C
, we can use our on-curve point (B
) and the
ratio(t)
function to find A
:
With A
found, finding e1
and e2
for quadratic curves is a matter of running the linear
@@ -4624,8 +5904,32 @@ lli = function(line1, line2):
distance ratio between e1
to B
and B
to e2
is the Bézier ratio (1-t):t
,
we can reverse engineer v1
and v2
:
And then reverse engineer the curve's control control points:
+
So: if we have a curve's start and end point, then for any t
value we implicitly know all the ABC values, which (combined
@@ -4649,6 +5953,19 @@ lli = function(line1, line2):
in computing the ABC coordinates, but we can just as easily approximate one by treating the distance between the start and
B
point, and B
and end point as a ratio, using
e1
and
e2
coordinates must obey the standard de Casteljau rule for linear interpolation:
+
B
is located, so we don't up creating a funky curve
with a loop in it. To do this, we can use the atan2 function:
+
t
value and "wherever the cursor is" as target
B
, we can compute the associated C
:
+
And then the associated A
:
And we're done, because that's our new quadratic control point!
And then we (trivially) rearrange the terms across multiple lines:
+With that arrangement, we can easily decompose this as a matrix multiplication:
+We can do the same for the cubic curve, of course. We know the base function for cubics:
+So we write out the expansion and rearrange:
+Which we can then decompose:
+And, of course, we can do this for quartic curves too (skipping the expansion step):
+To get these values, we first compute the general "distance along the polygon" matrix:
+i=n
fall in the [0,1] interval, so we need to scale all values down by whatever the total length of the polygon is:
+
In which we can replace the rather cumbersome "squaring" operation with a more conventional matrix equivalent:
+Which, because of the first and last values in S, means:
+Now we can properly write out the error function as matrix operations:
+Computing T is really more "arranging the numbers":
+Thus:
+Replace point/tangent vector with the expression for all-coordinates:
+and merge the matrices:
+This looks a lot like the Bézier matrix form, which as we saw in the chapter on Bézier curves, should look like this:
+So, if we want to express a Catmull-Rom curve using a Bézier curve, we'll need to turn this Catmull-Rom bit:
+Into something that looks like this:
+And the way we do that is with a fairly straight forward bit of matrix rewriting. We start with the equality we need to ensure:
+Then we remove the coordinate vector from both sides without affecting the equality:
+Then we can "get rid of" the Bézier matrix on the right by left-multiply both with the inverse of the Bézier matrix:
+And now we're basically done. We just multiply those two matrices and we know what V is:
+Or, if your API allows you to specify Catmull-Rom curves using plain coordinates:
+We can effect this quite easily, because we know that the vector from a curve's last control point to its last on-curve point is equal to the derivative vector. If we want to ensure that the first control point of the next curve matches that, all we have to do is mirror that last control point through the last on-curve point. And mirroring any point A through any point B is really simple:
+
So let's implement that and see what it gets us. The following two graphics show a quadratic and a cubic poly-Bézier curve again, but this
@@ -5915,6 +7794,11 @@ for p = 1 to points.length-3 (inclusive):
From a mathematical point of view, an offset curve O(t)
is a curve such that, given our original curve B(t)
,
any point on O(t)
is a fixed distance d
away from coordinate B(t)
. So let's math that:
B(t)
. Easy enough:
+
B'(t)
by its magnitude:
+
b
, we must use the formula we saw earlier. Noting that "length" is usually
denoted with double vertical bars:
+
B'(t)
, with t = 0
as start and t = 1
as end:
+
What we want to find is the intersection of the tangents, so we want a point C such that:
-i.e. we want a point that lies on the vertical line through S (at some distance a from S) and also lies on the tangent line through E (at some distance b from E). Solving this gives us:
+First we solve for b:
-which yields:
+which we can then substitute in the expression for a:
+A quick check shows that plugging these values for a and b into the expressions for Cx and Cy give @@ -6177,13 +8142,54 @@ for p = 1 to points.length-3 (inclusive): coordinate values for C, we know where our on-curve point T for t=0.5 (or angle φ/2) is, because we can just evaluate the Bézier polynomial, and we know where the circle arc's actual point P is for angle φ/2:
-We compute T, observing that if t=0.5, the polynomial values (1-t)², 2(1-t)t, and t² are 0.25, 0.5, and 0.25 respectively:
+Which, worked out for the x and y components, gives:
+And the distance between these two is the standard Euclidean distance:
-So, what does this distance function look like when we plot it for a number of ranges for the angle φ, such as a half circle, quarter circle and eighth circle? @@ -6224,6 +8230,16 @@ for p = 1 to points.length-3 (inclusive): In fact, let's flip the function around, so that if we plug in the precision error, labelled ε, we get back the maximum angle for that precision:
+And frankly, things are starting to look a bit ridiculous at this point, we're doing way more maths than we've ever done, but thankfully @@ -6327,9 +8343,16 @@ for p = 1 to points.length-3 (inclusive): So with the error analysis out of the way, how do we actually compute the coordinates needed to get that "true fit" cubic curve? The first observation is that we already know the start and end points, because they're the same as for the quadratic attempt:
+where "a" is some scaling factor we'll need to find the expression for, and:
+So, to recap, given an angle φ, the new control coordinates are:
+and
+Which, in decimal values, rounded to six significant digits, is:
+t
in the interval [0,1] (where 0 is the start of the curve, and 1 the
end, just like for Bézier curves), by evaluating the following function:
+
Which, honestly, doesn't tell us all that much. All we can see is that a point on a B-Spline curve is defined as "a mix of all the control
@@ -6688,6 +8841,15 @@ for p = 1 to points.length-3 (inclusive):
k
subscript to the N() function applies to.
Then the N() function itself. What does it look like?
+
So this is where we see the interpolation: N(t) for an (i,k)
pair (that is, for a step in the above summation, on a specific
@@ -6695,6 +8857,13 @@ for p = 1 to points.length-3 (inclusive):
iteration where i
goes up, and k
goes down, so it seem reasonable to expect that this recursion has to stop at
some point; obviously, it does, and specifically it does so for the following i
/k
values:
And this function finally has a straight up evaluation: if a t
value lies within a knot-specific interval once we reach a
@@ -6712,11 +8881,25 @@ for p = 1 to points.length-3 (inclusive):
Carl de Boor — came to a mathematically pleasing solution: to compute a point
P(t), we can compute this point by evaluating d(t) on a curve section between knots i
and i+1
:
This is another recursive function, with k values decreasing from the curve order to 1, and the value α (alpha) defined by:
+That looks complicated, but it's not. Computing alpha is just a fraction involving known, plain numbers. And, once we have our alpha @@ -6725,7 +8908,14 @@ for p = 1 to points.length-3 (inclusive): recursion might see computationally expensive, the total algorithm is cheap, as each step only involves very simple maths.
Of course, the recursion does need a stop condition:
-
So, we actually see two stopping conditions: either i
becomes 0, in which case d()
is zero, or
k
becomes zero, in which case we get the same "either 1 or 0" that we saw in the N() function above.
@@ -6735,7 +8925,39 @@ for p = 1 to points.length-3 (inclusive):
Casteljau's algorithm. For instance, if we write out d()
for i=3
and k=3
, we get the following
recursion diagram:
That is, we compute d(3,3)
as a mixture of d(2,3)
and d(2,2)
, where those two are themselves a
mixture of d(1,3)
and d(1,2)
, and d(1,2)
and d(1,1)
, respectively, which are
diff --git a/docs/news/00-draft.md b/docs/news/00-draft.md
index 04f30b66..05a77571 100644
--- a/docs/news/00-draft.md
+++ b/docs/news/00-draft.md
@@ -1,5 +1,31 @@
# Writing a web page
+Lets talk about writing a web page, and perhaps more specifically, let's talk about not writing web _applications_.
+
+At their core, web pages are a way to make information publicly available, in a navigable and discoverable manner. Things like wikipedia or a news website are collections of webpages, and they use the web stack - HTML, CSS, and JS - to get that information in front of readers' eyes, ideally under all circumstances: if your browser's broken and JS or even CSS doesn't work, the information is still there. It'll just look less polished than intended.
+
+Web applications, on the other hand, are software that "let people do things", with the only thing distinguishing them from any other software being that they're built using the web stack, and presented using the browser as user interface library. These applications can only work if the tech stack works, and not having the correct styling or event handling means the software is considered broken.
+
+Sometimes it's obvious to say what is which: wikipedia is clearly a (collection of) web page(s), and a browser based game is clearly a web app. But what about things in between? A weather website, or your bank, or even a communications platform like gmail or facebook? All of these have an information aspect to them that is based entirely around how web pages work: what's the current weather and the short forecast? No "software use" should be required. What is your current balance and transaction history? Again, plain information. Even basic web mail itself (a list of emails, that you can click to read) or facebook (a list X messages posted to the platform) are informational. But the functionality _around_ that information is more software-like in nature, so we end up with systems that need to balance "being a web page" with "also being a web app", or depending on the service, "being a web app" while also trying to "be a web page" when the web stack fails.
+
+And the web stack essentially fails by default these days: the exploitation of people on the web through underhanded practices got so bad that your browser is more than likely to have a script blocker installed, as well as something will filter out all the advertisement spam, as well as an extension that tries to ensure your privacy on the web overall. Which means that most of the things that web apps rely on are disabled by default: no cookies, no tab-associated storage, not even JS by default. So if you're writing a web app, you need to also write a web page, which you can run your web app "on top of".
+
+So back in 2016, somewhere around 0.14/0.15, after I was tasked with investigating if it was a useful technology for things that my employer was creating at the time, I was quite taken by this technology: this was a real webapp framework: it didn't care that it happened to run in the browser, everything it did, _it did_, and the fact that it happened to use a browser to render itself was essentially a bonus, but also entirely irrelevant. For software development, it was _fantastic_.
+
+For web pages, not so much, and after a while React came with a way to "render" pages in an offline context, yielding static HTML that your server could serve up, which would then load React, which would then hook up that HTML to the React application that had generated that HTML. This was _very cool_ but meant you needed a server to generate that static content. It also meant you were serving _really large_ HTML files, because in order to hook everything up, React generated HTML with lots of additional markup that told it which elements corresponded to which parts of the UI. It was "cheaper" to just not serve pregenerated pages and instead say "you need JS to run this website".
+
+And that's what the Primer for Bezier curves did, too. I loved React, and the idea that you needed JS anyway made sense: how are you going to use interactive graphics if you don't have JS enabled?
+
+And for a while that was fine, but after a year or two it became obvious that in many ways, the older, more cumbersome to maintain web page was in fact superior as far as user experience went: at its core, the primer is an information source, and it shouldn't matter whether you're loading it in a state of the art browser, or using [lynx](https://en.wikipedia.org/wiki/Lynx_(web_browser)).
+
+For a while this stayed at a realisation: the technology simply made it way too much work to cleanly pregenerate the primer and then have React cleanly hook back into things, but as with all things 2020, I found myself with some time off and a strong itch to finally fix this situation. Had I had that itch half a year earlier, I wouldn't have been able to do this rewrite, but in August 2020, everything had lined up: every browser except for IE11 has modern JS support, Node.js v14 has native ES module support, the `node-canvas` package is far enough along to support (almost!) everything I needed. The only real problem was Firefox on Android, which was still stuck on an ancient version, but amazingly while I was working on the rewrite a new version came out that got it on par with everything else, and so this rewrite was a go: it was time to make a web page.
+
+Of course the days of "firing up Dreamweaver" are long gone, and these days if you want to make a web page, what you'll actually be doing is _assembling_ a web page. In my case, all the content was in Markdown format, and I had no intention of changing that: Markdown is super convenient for writing long stretches of text. But of course, my markdown is peppered with LaTeX and interactive graphics, so that needs to work too.
+
+
+
+
+
## Let's talk about React
- React vs HTML: web apps are not web pages
@@ -15,7 +41,7 @@
- will it mine crypto?
- will it break if someone doesn't trust Google?
- etc.
- - pregenerate everything that can be generated. You're building a web page, not a web page builder. It should work immediately.
+ - pregenerate everything that can be generated. You're building a web page, not an in-browser page builder. It should work immediately.
## Let's talk about modern JS
diff --git a/docs/news/2020-09-18.html b/docs/news/2020-09-18.html
index f8c4d89b..e8f53796 100644
--- a/docs/news/2020-09-18.html
+++ b/docs/news/2020-09-18.html
@@ -27,7 +27,7 @@
-
+
@@ -168,6 +168,19 @@
+diff --git a/docs/news/index.html b/docs/news/index.html index 38b0a3e0..ae73eabf 100644 --- a/docs/news/index.html +++ b/docs/news/index.html @@ -26,7 +26,7 @@ - + diff --git a/docs/news/rss.xml b/docs/news/rss.xml index 99b6043d..f54f28e9 100644 --- a/docs/news/rss.xml +++ b/docs/news/rss.xml @@ -6,7 +6,7 @@![]()
如果我们知道两点之间的距离,并想找出离第一个点20%间距的一个新的点(也就是离第二个点80%的间距),我们可以通过简单的计算来得到:
-让我们来通过实际操作看一下:下面的图形都是可交互的,因此你可以通过上下键来增加或减少插值距离,来观察图形的变化。我们从三个点构成的两条线段开始。通过对各条线段进行线性插值得到两个点,对点之间的线段再进行线性插值,产生一个新的点。最终这些点——所有的点都可以通过选取不同的距离插值产生——构成了贝塞尔曲线 : @@ -500,15 +513,34 @@
贝塞尔曲线是“参数”方程的一种形式。从数学上讲,参数方程作弊了:“方程”实际上是一个从输入到唯一输出的、良好定义的映射关系。几个输入进来,一个输出返回。改变输入变量,还是只有一个输出值。参数方程在这里作弊了。它们基本上干了这么件事,“好吧,我们想要更多的输出值,所以我们用了多个方程”。举个例子:假如我们有一个方程,通过一些计算,将假设为x的一些值映射到另外的值:
+记号f(x)是表示函数的标准方式(为了方便起见,如果只有一个的话,我们称函数为f),函数的输出根据一个变量(本例中是x)变化。改变x,f(x)的输出值也会变。
到目前没什么问题。现在,让我们来看一下参数方程,以及它们是怎么作弊的。我们取以下两个方程:
+这俩方程没什么让人印象深刻的,只不过是正弦函数和余弦函数,但正如你所见,输入变量有两个不同的名字。如果我们改变了a的值,f(b)的输出不会有变化,因为这个方程没有用到a。参数方程通过改变这点来作弊。在参数方程中,所有不同的方程共用一个变量,如下所示:
+好了,通过一些神秘的t值将x/y坐标系联系起来。
@@ -536,6 +574,12 @@ 贝塞尔曲线是(一种)参数方程,并在它的多个维度上使用相同的基本方程。在上述的例子中x值和y值使用了不同的方程,与此不同的是,贝塞尔曲线的x和y都用了“二项多项式”。那什么是二项多项式呢?
你可能记得高中所学的多项式,看起来像这样:
+我明白你在想什么:这看起来并不简单,但如果我们拿掉t并让系数乘以1,事情就会立马简单很多,看看这些二次项:
+看起来很复杂,但实际上“权重”只是我们想让曲线所拥有的坐标值:对于一条nth阶曲线,w0是起始坐标,wn是终点坐标,中间的所有点都是控制点坐标。假设说一条曲线的起点为(120,160),终点为(220,40),并受点(35,200)和点(220,260)的控制,贝塞尔曲线方程就为:
+这就是我们在文章开头看到的曲线:
Adding these ratio values to the regular Bézier curve function is fairly easy. Where the regular function is the following:
+The function for rational Bézier curves has two more terms:
+t=0
到t=1
。为什么是这个特殊区间?
这一切都与我们如何从曲线的“起点”变化到曲线“终点”有关。如果有一个值是另外两个值的混合,一般方程如下:
+
很显然,起始值需要a=1, b=0
,混合值就为100%的value 1和0%的value 2。终点值需要a=0, b=1
,则混合值是0%的value
1和100%的value
2。另外,我们不想让“a”和“b”是互相独立的:如果它们是互相独立的话,我们可以任意选出自己喜欢的值,并得到混合值,比如说100%的value1和100%的value2。原则上这是可以的,但是对于贝塞尔曲线来说,我们通常想要的是起始值和终点值之间的混合值,所以要确保我们不会设置一些“a”和"b"而导致混合值超过100%。这很简单:
用这个式子我们可以保证相加的值永远不会超过100%。通过将a
限制在区间[0,1],我们将会一直处于这两个值之间(包括这两个端点),并且相加为100%。
@@ -1192,24 +1317,119 @@ function RationalBezier(3,t,w[],r[]):
通过将贝塞尔公式表示成一个多项式基本方程、系数矩阵以及实际的坐标,我们也可以用矩阵运算来表示贝塞尔。让我们看一下这对三次曲线来说有什么含义:
+暂时不用管我们具体的坐标,现在有:
+可以将它写成四个表达式之和:
+我们可以扩展这些表达式:
+更进一步,我们可以加上所有的1和0系数,以便看得更清楚:
+现在,我们可以将它看作四个矩阵运算:
+如果我们将它压缩到一个矩阵操作里,就能得到:
+这种多项式表达式一般是以递增的顺序来写的,所以我们应该将t
矩阵水平翻转,并将大的那个“混合”矩阵上下颠倒:
最终,我们可以加入原始的坐标,作为第三个单独矩阵:
+我们可以对二次曲线运用相同的技巧,可以得到:
+如果我们代入t
值并乘以矩阵来计算,得到的值与解原始多项式方程或用逐步线性插值计算的结果一样。
@@ -1574,6 +1794,16 @@ function drawCurve(points[], t): we saw that we can represent curves as matrix multiplications. Specifically, we saw these two forms for the quadratic and cubic curves respectively: (we'll reverse the Bézier coefficients vector for legibility)
+and
+and
+Excellent! Now we can form our new quadratic curve:
+If we want the interval [z,1], we will be evaluating this instead:
+[M · something]
:
+
So, our final second curve looks like:
+and
+and
+However, this rule also has as direct consequence that you cannot generally safely lower a curve from nth order to (n-1)th order, because the control points cannot be "pulled apart" cleanly. We can @@ -1856,22 +2341,58 @@ function drawCurve(points[], t): some things can be done much more easily with matrices than with calculus functions, and this is one of those things. So... let's go!
We start by taking the standard Bézier function, and condensing it a little:
+
Then, we apply one of those silly (actually, super useful) calculus tricks: since our t
value is always between zero and one
(inclusive), we know that (1-t)
plus t
always sums to 1. As such, we can express any value as a sum of
t
and 1-t
:
So, with that seemingly trivial observation, we rewrite that Bézier function by splitting it up into a sum of a (1-t)
and
t
component:
So far so good. Now, to see why we did this, let's write out the (1-t)
and t
parts, and see what that gives us.
I promise, it's about to make sense. We start with (1-t)
:
Let's do this:
+where the matrix M is an n+1
by n
matrix, and looks like:
First, let's look at the derivative rule for Bézier curves, which is:
+Which is hard to work with, so let's expand that properly:
+And that's the first part done: the two components inside the parentheses are actually regular, lower-order Bézier expressions:
+And that's just a summation of lower order curves:
+We can rewrite this as a normal summation, and we're done:
+Which is the "long" version of the following matrix transformation:
+And then we turn this into our solution for t
using basic arithmetics:
And then, using these v values, we can find out what our a, b, and c should be:
+v
values, where the v
values are
expressions of our original coordinate values, so we can do some substitution to get:
+
n=1
, we perform the following calculation until
fy(x)
is zero, so that the next t
is the same as the one we already have:
+
Then translating it so that the first coordinate lies on (0,0), moving all x coordinates by -120, and all y coordinates by -160, gives us:
-If we then rotate the curve so that its end point lies on the x-axis, the coordinates (integer-rounded for illustrative purposes here) become:
-If we drop all the zero-terms, this gives us:
-We can see that our original curve definition has been simplified considerably. The following graphics illustrate the result of aligning our example curves to the x-axis, with the cubic case using the coordinates that were just used in the example formulae: @@ -3373,12 +4277,23 @@ function getCubicRoots(pa, pb, pc, pd) { inflection, and we can find out where those happen relatively easily.
What we need to do is solve a simple equation:
+What we're saying here is that given the curvature function C(t), we want to know for which values of t this function is zero, meaning there is no "curvature", which will be exactly at the point between our circle being on one side of the curve, and our circle being on the other side of the curve. So what does C(t) look like? Actually something that seems not too hard:
+And of course the same functions for y:
+This is a plain quadratic curve, and we know how to solve C(t) = 0; we use the quadratic formula:
+...on the top (blue) edge, the curve's start point touches the curve, forming a loop. This edge is described by the function:
+Sweet! z stays 1, so we can effectively ignore it entirely, but we added some plain values to our x and y coordinates. So, if we want to subtract p1.x and p1.y, we use:
+Running all our coordinates through this transformation gives a new set of coordinates, let's call those U, where the @@ -3626,11 +4650,29 @@ function getCubicRoots(pa, pb, pc, pd) { currently have. This is called shearing, and the typical x-shear matrix and its transformation looks like this:
+So we want some shearing value that, when multiplied by y, yields -x, so our x coordinate becomes zero. That value is simply -x/y, because *-x/y * y = -x*. Done:
+Now, running this on all our points generates a new set of coordinates, let's call those V, which now have point 1 on @@ -3639,6 +4681,19 @@ function getCubicRoots(pa, pb, pc, pd) { point 3 to end up on (1,1), so we can also scale x to make sure its x-coordinate will be 1 after we run the transform. That means we'll be x-scaling by 1/point3x, and y-scaling by point2y. This is really easy:
+Then, finally, this generates a new set of coordinates, let's call those W, of which point 1 lies on (0,0), point 2 lies on (0,1), and @@ -3648,12 +4703,49 @@ function getCubicRoots(pa, pb, pc, pd) { but y-shearing. Additionally, we don't actually want to end up at zero (which is what we did before) so we need to shear towards an offset, in this case 1:
+And this generates our final set of four coordinates. Of these, we already know that points 1 through 3 are (0,0), (0,1) and (1,1), and only the last coordinate is "free". In fact, given any four starting coordinates, the resulting "transformation mapped" coordinate will be:
+Okay, well, that looks plain ridiculous, but: notice that every coordinate value is being offset by the initial translation, and also @@ -3664,12 +4756,36 @@ function getCubicRoots(pa, pb, pc, pd) { First, let's just do that translation step as a "preprocessing" operation so we don't have to subtract the values all the time. What does that leave?
+Suddenly things look a lot simpler: the mapped x is fairly straight forward to compute, and we see that the mapped y actually contains the mapped x in its entirety, so we'll have that part already available when we need to evaluate it. In fact, let's pull out all those common factors to see just how simple this is:
+That's kind of super-simple to write out in code, I think you'll agree. Coding math tends to be easier than the formulae initially make it @@ -3744,16 +4860,31 @@ function getCubicRoots(pa, pb, pc, pd) { cubic case either: because of the kind of curve we're starting with, we know there is only root, simplifying the code we need!
First, let's look at the function for x(t):
+We can rewrite this to a plain polynomial form, by just fully writing out the expansion and then collecting the polynomial factors, as:
+
Nothing special here: that's a standard cubic polynomial in "power" form (i.e. all the terms are ordered by their power of
t
). So, given that a
, b
, c
, d
, and x(t)
are all
known constants, we can trivially rewrite this (by moving the x(t)
across the equal sign) as:
You might be wondering "where did all the other 'minus x' for all the other values a, b, c, and d go?" and the answer there is that they @@ -3833,8 +4964,24 @@ y = curve.get(t).yfy(t), then the length of the curve, measured from start point to some point t = z, is computed using the following seemingly straight forward (if a bit overwhelming) formula:
+or, more commonly written using Leibnitz notation as:
+
This formula says that the length of a parametric curve is in fact equal to the area underneath a function that looks a
@@ -3861,7 +5008,18 @@ y = curve.get(t).y
-
+
+
In plain text: an integral function can always be treated as the sum of an (infinite) number of (infinitely thin) rectangular strips sitting "under" the function's plotted graph. To illustrate this idea, the following graph shows the integral for a sinusoid function. The @@ -3921,6 +5079,20 @@ y = curve.get(t).yz). Thankfully, we can quite easily transform any integral interval to any other integral interval, by shifting and scaling the inputs. Doing so, we get the following:
+Which means that in order for us to approximate the integral, we must plug these values into the approximate function, which gives us:
+So, what does the function look like? This:
+
Which is really just a "short form" that glosses over the fact that we're dealing with functions of t
, so let's expand that a
tiny bit:
And while that's a little more verbose, it's still just as simple to work with as the first function: the curvature at some point on any @@ -4162,6 +5381,13 @@ function kappa(t, B): just do that. But let's take it one step further: we can also compute the associated "radius of curvature", which gives us the implicit circle that "fits" the curve's curvature at any point, using what is possibly the simplest bit of maths found in this entire primer:
+
So let's revisit the previous graphic with the curvature visualised on both sides of our curves, as well as showing the circle that "fits"
@@ -4560,6 +5786,12 @@ lli = function(line1, line2):
So, how can we compute C
? We start with our observation that C
always lies somewhere between the start and ends
points, so logically C
will have a function that interpolates between those two coordinates:
If we can figure out what the function u(t)
looks like, we'll be done. Although we do need to remember that this
@@ -4569,8 +5801,26 @@ lli = function(line1, line2):
>
(with thanks to Boris Zbarsky) shows us the following two formulae:
And
+
So, if we know the start and end coordinates, and we know the t value, we know C, without having to calculate the
@@ -4579,10 +5829,35 @@ lli = function(line1, line2):
pure function of t
, too.
We start by observing that, given A
, B
, and C
, the following always holds:
Working out the maths for this, we see the following two formulae for quadratic and cubic curves:
+And
+
Which now leaves us with some powerful tools: given thee points (start, end, and "some point on the curve"), as well as a
@@ -4590,6 +5865,13 @@ lli = function(line1, line2):
u(t)
function, and once we have C
, we can use our on-curve point (B
) and the
ratio(t)
function to find A
:
With A
found, finding e1
and e2
for quadratic curves is a matter of running the linear
@@ -4598,8 +5880,32 @@ lli = function(line1, line2):
distance ratio between e1
to B
and B
to e2
is the Bézier ratio (1-t):t
,
we can reverse engineer v1
and v2
:
And then reverse engineer the curve's control control points:
+
So: if we have a curve's start and end point, then for any t
value we implicitly know all the ABC values, which (combined
@@ -4623,6 +5929,19 @@ lli = function(line1, line2):
in computing the ABC coordinates, but we can just as easily approximate one by treating the distance between the start and
B
point, and B
and end point as a ratio, using
e1
and
e2
coordinates must obey the standard de Casteljau rule for linear interpolation:
+
B
is located, so we don't up creating a funky curve
with a loop in it. To do this, we can use the atan2 function:
+
t
value and "wherever the cursor is" as target
B
, we can compute the associated C
:
+
And then the associated A
:
And we're done, because that's our new quadratic control point!
And then we (trivially) rearrange the terms across multiple lines:
+With that arrangement, we can easily decompose this as a matrix multiplication:
+We can do the same for the cubic curve, of course. We know the base function for cubics:
+So we write out the expansion and rearrange:
+Which we can then decompose:
+And, of course, we can do this for quartic curves too (skipping the expansion step):
+To get these values, we first compute the general "distance along the polygon" matrix:
+i=n
fall in the [0,1] interval, so we need to scale all values down by whatever the total length of the polygon is:
+
In which we can replace the rather cumbersome "squaring" operation with a more conventional matrix equivalent:
+Which, because of the first and last values in S, means:
+Now we can properly write out the error function as matrix operations:
+Computing T is really more "arranging the numbers":
+Thus:
+Replace point/tangent vector with the expression for all-coordinates:
+and merge the matrices:
+This looks a lot like the Bézier matrix form, which as we saw in the chapter on Bézier curves, should look like this:
+So, if we want to express a Catmull-Rom curve using a Bézier curve, we'll need to turn this Catmull-Rom bit:
+Into something that looks like this:
+And the way we do that is with a fairly straight forward bit of matrix rewriting. We start with the equality we need to ensure:
+Then we remove the coordinate vector from both sides without affecting the equality:
+Then we can "get rid of" the Bézier matrix on the right by left-multiply both with the inverse of the Bézier matrix:
+And now we're basically done. We just multiply those two matrices and we know what V is:
+Or, if your API allows you to specify Catmull-Rom curves using plain coordinates:
+We can effect this quite easily, because we know that the vector from a curve's last control point to its last on-curve point is equal to the derivative vector. If we want to ensure that the first control point of the next curve matches that, all we have to do is mirror that last control point through the last on-curve point. And mirroring any point A through any point B is really simple:
+
So let's implement that and see what it gets us. The following two graphics show a quadratic and a cubic poly-Bézier curve again, but this
@@ -5889,6 +7770,11 @@ for p = 1 to points.length-3 (inclusive):
From a mathematical point of view, an offset curve O(t)
is a curve such that, given our original curve B(t)
,
any point on O(t)
is a fixed distance d
away from coordinate B(t)
. So let's math that:
B(t)
. Easy enough:
+
B'(t)
by its magnitude:
+
b
, we must use the formula we saw earlier. Noting that "length" is usually
denoted with double vertical bars:
+
B'(t)
, with t = 0
as start and t = 1
as end:
+
What we want to find is the intersection of the tangents, so we want a point C such that:
-i.e. we want a point that lies on the vertical line through S (at some distance a from S) and also lies on the tangent line through E (at some distance b from E). Solving this gives us:
+First we solve for b:
-which yields:
+which we can then substitute in the expression for a:
+A quick check shows that plugging these values for a and b into the expressions for Cx and Cy give @@ -6151,13 +8118,54 @@ for p = 1 to points.length-3 (inclusive): coordinate values for C, we know where our on-curve point T for t=0.5 (or angle φ/2) is, because we can just evaluate the Bézier polynomial, and we know where the circle arc's actual point P is for angle φ/2:
-We compute T, observing that if t=0.5, the polynomial values (1-t)², 2(1-t)t, and t² are 0.25, 0.5, and 0.25 respectively:
+Which, worked out for the x and y components, gives:
+And the distance between these two is the standard Euclidean distance:
-So, what does this distance function look like when we plot it for a number of ranges for the angle φ, such as a half circle, quarter circle and eighth circle? @@ -6198,6 +8206,16 @@ for p = 1 to points.length-3 (inclusive): In fact, let's flip the function around, so that if we plug in the precision error, labelled ε, we get back the maximum angle for that precision:
+And frankly, things are starting to look a bit ridiculous at this point, we're doing way more maths than we've ever done, but thankfully @@ -6301,9 +8319,16 @@ for p = 1 to points.length-3 (inclusive): So with the error analysis out of the way, how do we actually compute the coordinates needed to get that "true fit" cubic curve? The first observation is that we already know the start and end points, because they're the same as for the quadratic attempt:
+where "a" is some scaling factor we'll need to find the expression for, and:
+So, to recap, given an angle φ, the new control coordinates are:
+and
+Which, in decimal values, rounded to six significant digits, is:
+t
in the interval [0,1] (where 0 is the start of the curve, and 1 the
end, just like for Bézier curves), by evaluating the following function:
+
Which, honestly, doesn't tell us all that much. All we can see is that a point on a B-Spline curve is defined as "a mix of all the control
@@ -6662,6 +8817,15 @@ for p = 1 to points.length-3 (inclusive):
k
subscript to the N() function applies to.
Then the N() function itself. What does it look like?
+
So this is where we see the interpolation: N(t) for an (i,k)
pair (that is, for a step in the above summation, on a specific
@@ -6669,6 +8833,13 @@ for p = 1 to points.length-3 (inclusive):
iteration where i
goes up, and k
goes down, so it seem reasonable to expect that this recursion has to stop at
some point; obviously, it does, and specifically it does so for the following i
/k
values:
And this function finally has a straight up evaluation: if a t
value lies within a knot-specific interval once we reach a
@@ -6686,11 +8857,25 @@ for p = 1 to points.length-3 (inclusive):
Carl de Boor — came to a mathematically pleasing solution: to compute a point
P(t), we can compute this point by evaluating d(t) on a curve section between knots i
and i+1
:
This is another recursive function, with k values decreasing from the curve order to 1, and the value α (alpha) defined by:
+That looks complicated, but it's not. Computing alpha is just a fraction involving known, plain numbers. And, once we have our alpha @@ -6699,7 +8884,14 @@ for p = 1 to points.length-3 (inclusive): recursion might see computationally expensive, the total algorithm is cheap, as each step only involves very simple maths.
Of course, the recursion does need a stop condition:
-
So, we actually see two stopping conditions: either i
becomes 0, in which case d()
is zero, or
k
becomes zero, in which case we get the same "either 1 or 0" that we saw in the N() function above.
@@ -6709,7 +8901,39 @@ for p = 1 to points.length-3 (inclusive):
Casteljau's algorithm. For instance, if we write out d()
for i=3
and k=3
, we get the following
recursion diagram:
That is, we compute d(3,3)
as a mixture of d(2,3)
and d(2,2)
, where those two are themselves a
mixture of d(1,3)
and d(1,2)
, and d(1,2)
and d(1,1)
, respectively, which are
diff --git a/package.json b/package.json
index d34ccbb3..98a89330 100644
--- a/package.json
+++ b/package.json
@@ -17,34 +17,36 @@
"---": "--- These are the only three scripts you should care about ---",
"start": "run-s clean:* time lint:* build time clean:temp",
"test": "run-s start && run-p watch server browser",
- "deploy": "run-s start polish copy",
- "-------": "--- Note that due to github's naming policy, the public dir is called `docs` rather than `public` ---",
+ "deploy": "run-s deploy:nocopy copy",
+ "----": "--- The rest are just the various parts that make the previous three work ---",
"browser": "open-cli http://localhost:8000",
"build": "node ./src/build.js",
"time": "node ./src/mark.js",
"clean:temp": "rm -f .timing && rm -rf ./temp",
+ "-----": "--- Note that due to github's naming policy, the public dir is called `docs` rather than `public` ---",
"clean:news": "rm -f ./docs/news/*.html",
"polish": "run-s pretty link-checker",
- "----": "--- The copy commands rely on a parallel `bezierinfo` git repo existing ---",
+ "deploy:nocopy": "run-s start polish",
+ "------": "--- The copy commands rely on a parallel `bezierinfo` git repo existing ---",
"copy": "run-s copy:*",
"copy:prepare": "mkdir _ && mv ../bezierinfo/.git _ && rm -rf ../bezierinfo ",
"copy:files": "cp -r ./docs/. ../bezierinfo",
"copy:remove-md": "rm ../bezierinfo/chapters/**/*.md && rm ../bezierinfo/news/*.md",
"copy:cleanup": "mv _/.git ../bezierinfo && rm -rf _",
- "----------": "--- Prettier is your friend. ---",
+ "-------": "--- Prettier is your friend. ---",
"lint:tools": "prettier \"./src/**/*.js\" --print-width 150 --write",
"lint:lib": "prettier \"./docs/js/**/*.js\" --print-width 150 --write",
"lint:css": "prettier \"./docs/**/*.css\" --print-width 150 --write",
"link-checker": "link-checker \"docs/index.html\" --allow-hash-href --url-ignore \"(ja-JP|zh-CN|legendre).*\" --ignore-error-exit",
"pretty": "prettier \"./docs/{!(legendre-gauss),**/*}.html\" --use-tabs --print-width 150 --write",
- "------": "--- Server and watch tasks ---",
+ "--------": "--- Server and watch tasks ---",
"server": "cd docs && http-server -e -p 8000 --cors",
"watch": "run-p watch:*",
"watch:chapters": "chokidar \"./docs/chapters/**/*.(md|js)\" -c \"npm run build\"",
"watch:news": "chokidar \"./docs/news/**/*.md\" -c \"npm run build\"",
"watch:customelement": "chokidar \"./docs/js/custom-element/**/*.js\" -c \"npm run build\"",
"watch:src": "chokidar \"./src/**/*.*\" -c \"npm run build\"",
- "-----": "--- Used as part of LaTeX generation: ---",
+ "---------": "--- Used as part of LaTeX generation: ---",
"svgo": "svgo",
"svgo:pretty": "node ./src/svgo-pretty.js"
},
diff --git a/src/build/markdown/processors/latex/latex-to-svg.js b/src/build/markdown/processors/latex/latex-to-svg.js
index c562847d..aedcf53b 100644
--- a/src/build/markdown/processors/latex/latex-to-svg.js
+++ b/src/build/markdown/processors/latex/latex-to-svg.js
@@ -18,81 +18,101 @@ import toPOSIX from "../../../../to-posix.js";
*/
export default async function latexToSVG(latex, pathdata, localeStrings, block) {
const { imagepath, id } = pathdata;
+ fs.ensureDirSync(imagepath);
+ fs.ensureDirSync(path.join(paths.snippets, id));
+
latex = colorPreProcess(latex);
const locale = localeStrings.getCurrentLocale();
const hash = createHash(`md5`).update(latex).digest(`hex`);
const TeXfilename = path.join(paths.temp, `${hash}.tex`);
- fs.ensureDirSync(imagepath);
-
+ const ASCIIfilename = path.join(paths.snippets, id, `${hash}.ascii`);
+ const PDFfilename = TeXfilename.replace(`.tex`, `.pdf`);
+ const PDFfilenameCropped = TeXfilename.replace(`.tex`, `-crop.pdf`);
const SVGfilename = path.join(imagepath, `${hash}.svg`);
+
const srcURL = `./${toPOSIX(path.relative(paths.public, SVGfilename))}`;
- if (!fs.existsSync(SVGfilename)) {
+ const commands = {
+ xetex: `xelatex -output-directory "${path.dirname(TeXfilename)}" "${TeXfilename}"`,
+ crop: `pdfcrop "${PDFfilename}"`,
+ svg: `pdf2svg "${PDFfilenameCropped}" "${SVGfilename}"`,
+ svgo: `npm run svgo -- "${SVGfilename}"`,
+ tex2utf: `perl ./src/tex2utf/tex2utf.pl "${TeXfilename}" > "${ASCIIfilename}"`,
+ };
+
+ if (!fs.existsSync(TeXfilename)) {
// There is no SVG graphic for the LaTeX code yet, so we need to generate
// one, but it's possible we already tried and failed for a different locale.
// As the temp dir gets wiped on each run, we can check for the the existence
// of our TeX file: if it exists, we already unsuccessfully ran this code.
if (fs.existsSync(TeXfilename)) return fail(hash);
- const PDFfilename = TeXfilename.replace(`.tex`, `.pdf`);
- const PDFfilenameCropped = TeXfilename.replace(`.tex`, `-crop.pdf`);
-
- let fonts = ["\\setmainfont[Ligatures=TeX]{TeX Gyre Pagella}", "\\setmathfont{TeX Gyre Pagella Math}"];
+ let fonts = `
+ \\setmainfont[Ligatures=TeX]{TeX Gyre Pagella}
+ \\setmathfont{TeX Gyre Pagella Math}
+ `;
// For Chinese, we need the xeCJK package because there might be Chinese
// in maths context, which base XeLaTeX can't quite deal with.
if (locale === "zh-CN") {
- fonts = ["\\usepackage{xeCJK}", "\\xeCJKsetup{CJKmath=true}", "\\setCJKmainfont{gbsn00lp.ttf}"];
+ fonts = `
+ \\usepackage{xeCJK}
+ \\xeCJKsetup{CJKmath=true}
+ \\setCJKmainfont{gbsn00lp.ttf}
+ `;
}
// The same goes for Japanese, although we obviously want a different
// font than Chinese, as they are different languages entirely.
if (locale === "ja-JP") {
- fonts = ["\\usepackage{xeCJK}", "\\xeCJKsetup{CJKmath=true}", "\\setCJKmainfont{ipaexm.ttf}"];
+ fonts = `
+ \\usepackage{xeCJK}
+ \\xeCJKsetup{CJKmath=true}
+ \\setCJKmainfont{ipaexm.ttf}
+ `;
}
fs.writeFileSync(
TeXfilename,
- [
- `\\documentclass[12pt]{article}`,
- `\\usepackage[paperwidth=12in, paperheight=12in]{geometry}`,
- `\\pagestyle{empty}`,
- `\\usepackage[dvipsnames]{xcolor}`,
- `\\definecolor{darkred}{rgb}{0.6,0,0}`,
- `\\definecolor{darkgreen}{rgb}{0,0.6,0}`,
- `\\definecolor{darkblue}{rgb}{0,0,0.6}`,
- `\\definecolor{amber}{rgb}{0.9,0.6,0}`,
- `\\usepackage{amsmath}`,
- `\\usepackage{unicode-math}`,
- ]
- .concat(fonts)
- .concat([`\\begin{document}`, `\\[`, cleanUp(latex), `\\]`, `\\end{document}`])
- .join(`\n`)
+ `
+ \\documentclass[12pt]{article}
+ \\usepackage[paperwidth=12in, paperheight=12in]{geometry}
+ \\pagestyle{empty}
+ \\usepackage[dvipsnames]{xcolor}
+ \\definecolor{darkred}{rgb}{0.6,0,0}
+ \\definecolor{darkgreen}{rgb}{0,0.6,0}
+ \\definecolor{darkblue}{rgb}{0,0,0.6}
+ \\definecolor{amber}{rgb}{0.9,0.6,0}
+ \\usepackage{amsmath}
+ \\usepackage{unicode-math}
+ ${fonts}
+ \\begin{document}
+ \\[ ${cleanUp(latex)} \\]
+ \\end{document}
+ `
);
+ }
- const commands = {
- xetex: `xelatex -output-directory "${path.dirname(TeXfilename)}" "${TeXfilename}"`,
- crop: `pdfcrop "${PDFfilename}"`,
- svg: `pdf2svg "${PDFfilenameCropped}" "${SVGfilename}"`,
- svgo: `npm run svgo -- "${SVGfilename}"`,
- };
-
+ if (!fs.existsSync(SVGfilename)) {
// Finally: run the conversion
try {
process.stdout.write(`- running xelatex for block [${id}:${locale}:${block}] (${hash}.tex): `);
- runCmd(commands.xetex, hash);
+ runCmd(commands.xetex);
process.stdout.write(` - cropping PDF to document content: `);
- runCmd(commands.crop, hash);
+ runCmd(commands.crop);
process.stdout.write(` - converting cropped PDF to SVG: `);
- runCmd(commands.svg, hash);
+ runCmd(commands.svg);
process.stdout.write(` - cleaning up SVG: `);
- runCmd(commands.svgo, hash);
+ runCmd(commands.svgo);
} catch (e) {
+ // we don't really care about errors unless they happen as
+ // part of the xelatex command, in which case we need to know
+ // what actually went wrong so we can fix it.
if (e.cmd === commands.xetex) {
console.error(` | Error in ${TeXfilename}\n |`);
@@ -100,7 +120,7 @@ export default async function latexToSVG(latex, pathdata, localeStrings, block)
.filter((v) => !!v)
.map((v) => v.toString("utf8").replace(/\r\n/g, `\n`))[0]
.split(`\n`)
- .slice(-6); // this may depend on the xelatex version used...
+ .slice(-6); // this is a magic number that *may* depend on the xelatex version used.
loglines.splice(2, 1); // remove 'no pages of output'
@@ -113,16 +133,23 @@ export default async function latexToSVG(latex, pathdata, localeStrings, block)
}
}
+ // Generate the "ASCII art" version for comment embedding, independently
+ // of the TeX -> SVG chain, as it is in effect a separate TeX conversion.
+ if (!fs.existsSync(ASCIIfilename)) {
+ process.stdout.write(`creating ASCII snippet ${hash}: `);
+ runCmd(commands.tex2utf, () => cleanASCII(ASCIIfilename));
+ }
+
// Make sure we hardcode the size of this LaTeX SVG image, because we absolutely
- // do not want the page to resize in any possible noticable way if we can help it.
+ // do not want the page to resize in any possible noticeable way if we can help it.
let svg = fs.readFileSync(SVGfilename).toString(`utf8`);
try {
const vb = svg.match(/viewBox="([^"]+)"/)[1].split(/\s+/);
// The SVG contains values in "pt" units, but to maximise legibility we convert
- // these to "rem" instead, so that formulae are always sized based on the font
- // around them, rather than being sized independently of the document text.
+ // these to pixel values instead, so that formulae are always sized based on the
+ // font around them, rather than being sized independently of the document text.
// Base browser sizes are 16pt so the conversion factor is 4/3.
const w = Math.round(((parseFloat(vb[2]) - parseFloat(vb[0])) * 4) / 3);
const h = Math.round(((parseFloat(vb[3]) - parseFloat(vb[1])) * 4) / 3);
@@ -132,7 +159,15 @@ export default async function latexToSVG(latex, pathdata, localeStrings, block)
svg = svg.replace(`height="${vb[3]}pt"`, `height="${h}px"`);
fs.writeFileSync(SVGfilename, svg, `utf8`);
- return ``;
+ // Include the "ASCII" version of this formula as code comment
+ const ascii = fs.readFileSync(ASCIIfilename).toString(`utf8`);
+ const prefix = `\n`;
+ const img = `
`;
+ return `${prefix}${img}`;
+
+ // Note that, yes, this adds 100kb to the resulting .html file,
+ // but after gzip (which all sensible servers use) that's only 16kb
+ // and that's an acceptable amount of data.
} catch (e) {
// This code exists because sometimes a file turns out to be empty, and I suspect that's
// because of successive SVGO calls, but whatever the cause: it's bad and should break the build.
@@ -142,32 +177,48 @@ export default async function latexToSVG(latex, pathdata, localeStrings, block)
}
}
-// This function really needs better stdio capture,
-// so it can report _what went wrong_ when xelatex fails.
-function runCmd(cmd, hash) {
+// Run a command in an explicit unicode context.
+function runCmd(cmd, thenRunThis) {
try {
- execSync(cmd); //, { stdio: 'inherit' });
+ execSync(cmd, { encoding: `utf8` });
console.log(`✓`);
} catch (e) {
console.log(`✕`);
e.cmd = cmd;
throw e;
}
+ if (thenRunThis) return thenRunThis();
}
-// This function converst things like RED[a] into real LaTeX.
+// Remove latex preamble and superfluous indents from "ASCII" graphics.
+function cleanASCII(filename) {
+ let data = fs.readFileSync(filename).toString(`utf8`);
+ let lines = data.split(`\n`).slice(3);
+ let indent = lines.reduce((t, e) => {
+ if (!e.trim()) return t;
+ let m = e.match(/^\s+/);
+ let len = m ? m[0].length : 0;
+ return len < t ? len : t;
+ }, 1000);
+ let re = new RegExp(`^${" ".repeat(indent)}`);
+ data = lines.map((l) => l.replace(re, ``)).join(`\n`);
+ fs.writeFileSync(filename, data);
+ return data;
+}
+
+// This function converts things like RED[a] into real LaTeX.
function colorPreProcess(input) {
var regexp = new RegExp(`([A-Z]+)\\[([^\\]]+)\\]`, `g`);
var output = input.replace(regexp, function (_, color, content) {
if (content.indexOf(` `) !== -1) {
content = ` ${content}`;
}
- return `{\\color{${color.toLowerCase()}}${content.replace(/ /g, "\\ ")}}`;
+ return `{\\color{${color.toLowerCase()}}${content.replace(/ /g, "~")}}`;
});
return output;
}
// Failure state HTML code is simply a script that yells in the console
function fail(hash) {
- return ``;
+ return ``;
}
diff --git a/src/project-paths.js b/src/project-paths.js
index 01a79955..09a14488 100644
--- a/src/project-paths.js
+++ b/src/project-paths.js
@@ -19,6 +19,7 @@ const build = path.join(src, `build`);
const chapters = path.join(publicDir, `chapters`);
const html = path.join(src, `html`);
const images = path.join(publicDir, `images`);
+const snippets = path.join(images, `snippets`);
const news = path.join(publicDir, `news`);
const sitejs = path.join(publicDir, `js`);
const temp = path.join(project, `temp`);
@@ -32,6 +33,7 @@ const paths = {
project,
public: publicDir,
sitejs,
+ snippets,
src,
temp,
};
diff --git a/src/tex2utf/README.md b/src/tex2utf/README.md
new file mode 100644
index 00000000..73104712
--- /dev/null
+++ b/src/tex2utf/README.md
@@ -0,0 +1 @@
+See https://github.com/Pomax/tex2utf
diff --git a/src/tex2utf/tex2utf.pl b/src/tex2utf/tex2utf.pl
new file mode 100644
index 00000000..c5f66356
--- /dev/null
+++ b/src/tex2utf/tex2utf.pl
@@ -0,0 +1,2264 @@
+#!/usr/local/bin/perl
+
+# $Id: tex2utf.pl, v 1.0 2020/10/16 16:09:00 Pomax $
+#
+# UTF8-massaged version of https://ctan.org/pkg/tex2mail
+#
+# Updated October 2020 by pomax@nihongoressources.com,
+# original header immediately follows this comment block,
+# with spacing updated to something that looks uniform.
+
+# $Id: tex2mail.in,v 1.1 2000/10/27 19:13:53 karim Exp $
+#
+# Features:
+# % at the end of a line followed by \n\n is recognized as end of
+# paragraph :-(
+#
+# Change log is at bottom.
+#
+# Options:
+# linelength=75 # Cut at this line
+# maxdef=400 # definition loops: croak if many substitutions
+# debug=0
+# by_par=0 # Expect each paragraph to be terminated
+# # by *exactly* 2 "\n", and do not print
+# # an extra "\n" between paragraphs
+# TeX # Assume it is not LaTeX
+# ragged # leave right ragged
+# noindent # assume \noindent everywhere
+
+use Getopt::Long;
+
+$linelength = 150;
+$maxdef = 400;
+$debug = false;
+$opt_by_par = false;
+$opt_TeX = true;
+$opt_ragged = false;
+$opt_noindent = false;
+
+GetOptions(
+ "linelength=s" => \$linelength,
+ "maxdef=s" => \$maxdef,
+ "debug" => \$debug,
+ "by_par" => \$opt_by_par,
+ "TeX" => \$opt_TeX,
+ "ragged" => \$opt_ragged,
+ "noindent" => \$opt_noindent
+) or die "Could not parse provided runtime flag(s)";
+
+#
+# This part is a little different: enable utf8 and ensure that
+# even on Windows, the input/output is fully unicode conformant:
+#
+
+use utf8;
+
+use open ':std', ':encoding(UTF-8)';
+BEGIN {
+ if ($^O eq "MSWin32") {
+ require Win32::Unicode::File;
+ Win32::Unicode::File->import();
+ }
+}
+
+#
+# The original code then continues here...
+#
+
+$notusualtoks="\\\\" . '\${}^_~&@';
+$notusualtokenclass="[$notusualtoks]";
+$usualtokenclass="[^$notusualtoks]";
+$macro='\\\\([^a-zA-Z]|([a-zA-Z]+\s*))'; # Why \\\\? double interpretation!
+$active="$macro|\\\$\\\$|$notusualtokenclass";
+$tokenpattern="$usualtokenclass|$active";
+$multitokenpattern="$usualtokenclass+|$active";
+
+# Format of the record: height,length,baseline,expandable-spaces,string
+# The string is not terminated by \n, but separated into rows by \n.
+# height=0 denotes expandable string
+# Baseline=3 means the 4th row is the baseline
+
+sub debug_print_record {
+ local($h,$l,$b,$xs,$s) = split /,/, shift, 5;
+ local(@arr) = split /\n/, $s;
+ print STDERR "len=$l, h=$h, b=$b, exp_sp=$xs.\n";
+ local($i) = 0;
+ for (@arr) {
+ local($lead) = ($i++ == $b) ? 'b [' : ' [';
+ print STDERR "$lead$_]\n";
+ }
+ while ($i < $h) { # Empty lines may skipped
+ local($lead) = ($i++ == $b) ? 'b' : '';
+ print STDERR "$lead\n";
+ }
+}
+
+# Takes length and a record, returns 2 records
+
+sub cut {
+ local($length)=(shift);
+ local($h,$l,$b,$sp,$str)=split(/,/,shift,5);
+ local($st1,$st2)=("","");
+ local($sp1,$sp2,$first,$l2)=(0,0,1,$l-$length);
+ return (shift,&empty) if $l2<0;
+ if ($h) {
+ for (split(/\n/,$str,$h)) {
+ if (!$first) {
+ $st1 .= "\n";
+ $st2 .= "\n";
+ } else {$first=0;}
+ $st1 .= substr($_,0,$length);
+ $st2 .= substr($_,$length);
+ }
+ } else {
+ $st1 = substr($str,0,$length);
+ $st2 = substr($str,$length);
+ #if ($sp && ($st1 =~ /(\S)(\s+\S*)$/)) {
+ # $st2 = $2 . $st2;
+ # $st1 = $` . $1;
+ # $sp1 = ($st1 =~ /(\s)/g);
+ # $sp2 = ($st2 =~ /(\s)/g);
+ #}
+ }
+ return ("$h,$length,$b,$sp1,$st1","$h,$l2,$b,$sp2,$st2");
+}
+
+# Outputs a record
+
+sub printrecord {
+ warn "Printing $_[0]\n__ENDPRINT__\n" if $debug & $debug_record;
+ local($h,$l,$b,$sp,$str)=split(/,/,shift,5);
+ print $str,"\n";
+}
+
+# Joins two records
+
+sub join {
+ local($h1,$l1,$b1,$sp1,$str1)=split(/,/,shift,5);
+ local($h2,$l2,$b2,$sp2,$str2)=split(/,/,shift,5);
+ $h1 || $h1++;
+ $h2 || $h2++;
+ local($h,$l,$b,$sp,$str,@str,@str2)=(0,0,0,$sp1+$sp2,"");
+ $b = $b1 > $b2 ? $b1 : $b2;
+ # Calculate space below baseline
+ $h = $h1-$b1 > $h2-$b2 ? $h1-$b1 : $h2-$b2;
+ # And height
+ $h += $b;
+ $l=$l1+$l2;
+ @str="" x $h;
+ @str[$b-$b1 .. $b-$b1+$h1-1]=split(/\n/,$str1,$h1);
+ @str2[0..$h2-1]=split(/\n/,$str2,$h2);
+ unless (length($str2[$b2])) {
+ $str2[$b2] = ' ' x $l2; # Needed for length=0 "color" strings
+ # in the baseline.
+ }
+ if ($debug & $debug_record && (grep(/\n/,@str) || grep(/\n/,@str2))) {
+ warn "\\n found in \@str or \@str2";
+ warn "`$str1', need $h1 rows\n";
+ warn "`$str2', need $h2 rows\n";
+ }
+ # This is may be wrong if a zero-length record with escape sequences
+ # is appended to with something not on the same row... But
+ # apparently, it should be OK for PARI...
+ for (0..$h2-1) {
+ $str[$b-$b2+$_] .= " " x ($l1 - length ($str[$b-$b2+$_])) . $str2[$_];
+ }
+ return "$h,$l,$b,$sp," . join("\n",@str);
+}
+
+# The current line is contained in the array @out of records and, possibly,
+# one additional record $last. If $last exists, $islast is set to 1.
+# The output channel length is contained in $linelength, the accumulated
+# length of @out and $last is contained in $curlength.
+# We guaranty that if $curlength>$linelength, then @out is empty.
+
+# Gets a length of a record
+
+sub length {
+ (warn "Wrong format of a record `$_[0]'", return 0)
+ unless $_[0] =~ /^\d+,(\d+)/;
+ $1;
+}
+
+# Gets a height of a record
+
+sub height {
+ (warn "Wrong format of a record `$_[0]'", return 0)
+ unless $_[0] =~ /^(\d+),/;
+ $1;
+}
+
+# Sets baseline of a record, Usage s...(rec,base)
+
+sub setbaseline {
+ (warn("Wrong format of a record `$_[0]'"), return undef)
+ unless $_[0] =~ s/^(\d+,\d+,)(\d+)/\1$_[1]/;
+}
+
+# The hierarchical structure: the records to work are in the array @out.
+# The array @chunks keeps the beginning record of the chunks,
+# The array @level keeps the beginning chunks of the given level.
+# The last chunk can begin after the last record if this chunk is still empty.
+
+# We do not keep the inner structure of the chunk unless it is the last
+# chunk on the given level.
+
+# Each record is a rectangle to output to the "page".
+
+# Each chunk is a sequence of records which reflect one finished subgroup
+# on the given level.
+
+# Each level is a sequence of chunks which correspond to a
+# not-yet-finished group in TeX input.
+
+
+# The parallel to @level array @wait
+# contains an event we wait to complete the given level of array.
+
+# Chunks on a given level
+
+# Used to expand spaces
+
+sub exp_sp {$c1++;$c2=0 if $c1>$re; return " " x ($c2+$fr+1);}
+
+# Outputs the outermost level of the output list (until the start of level 1)
+# If gets a true argument, does not expand spaces
+
+sub print {
+ warn "Printing...\n" if $debug & $debug_flow;
+ local($last,$l,$exp) = ($#level? $chunks[$level[1]]-1: $#out);
+ ($last >=0) || return;
+ $l=&length($out[0]);
+ if ($last >= 1) {
+ for (1..$last) {
+ $l += &length($out[$_]);
+ }
+ }
+ if ($debug & $debug_length) {
+ if ($l != $curlength) {
+ for (0..$last) {
+ warn "Wrong lengths Record $_ $out[$_]\n__ENDREC__\n" ;
+ }
+ }
+ }
+ $curlength=$l;
+ warn "l=$l, linelength=$linelength, curlength=$curlength\n"
+ if $debug & $debug_length;
+ IF_L:
+ {
+ if (!shift && ($l=$linelength-$curlength)>=0) {
+ warn "entered branch for long string\n"
+ if $debug & $debug_length;
+ $exp=0;
+ (($out[$last] =~ s/\s+$//) && ($l+=length($&)))
+ if $out[$last] =~ /^0,/;
+ warn "l=$l with whitespace\n"
+ if $debug & $debug_length;
+ last IF_L if $l<=0;
+ local($str,$h,$fr,$re,$c1,$c2,@t);
+ for (0..$last) {
+ ($str,$h)=(split(/,/,$out[$_],5))[4,0];
+ (@t = ($str =~ /( )/g), $exp+=@t) if (!$h);
+ }
+ if ($exp) {
+ $re=$l % $exp;
+ $fr=int(($l-$re)/$exp);
+ warn "$l Extra spaces in $exp places, Fr=$fr," .
+ " Remainder=$re, LL=$linelength, CL=$curlength\n" if $debug & $debug_length;
+ $c1=0;
+ $c2=1;
+ for (0..$last) {
+ ($str,$h)=(split(/,/,$out[$_],5))[4,0];
+ unless ($h || $opt_ragged) {
+ $str =~ s/ /&exp_sp/ge;
+ $out[$_]=&string2record($str);
+ }
+ }
+ }
+ }
+ else {warn "Do not want to expand $l spaces\n" if $debug & $debug_length;}
+ }
+ if ($last >= 1) {
+ for (1..$last) {
+ $out[0] = &join($out[0],$out[$_]);
+ }
+ }
+ $l=&length($out[0]);
+ warn "LL=$linelength, CurL=$curlength, OutL=$l\n" if $debug & $debug_length;
+ &printrecord($out[0]);
+ $curlength=0;
+ if ($#out>$last) {
+ @out=@out[$last+1..$#out];
+ for (0..$#chunks) {$chunks[$_] -= $last+1;}
+ } else {
+ @out=();
+ }
+ if ($#level) {
+ splice(@chunks,1,$level[1]-2);
+ } else {
+ @chunks=(0);
+ }
+}
+
+# Cuts prepared piece and arg into printable parts (unfinished)
+# Suppose that level==0
+
+sub prepare_cut {
+ warn "Preparing to cut $_[0]\n" if $debug & $debug_flow;
+ warn "B:Last chunk number $#chunks, last record $#out\n" if $debug & $debug_flow;
+ (warn "\$#level non 0", return $_[0]) if ($#level!=0);
+ local($lenadd)=(&length($_[0]));
+ local($lenrem)=($linelength-$curlength);
+ if ($lenadd+$curlength<=$linelength) {
+ warn "No need to cut, extra=$lenrem\n" if $debug & $debug_flow;
+ return $_[0];
+ }
+ # Try to find a cut in the added record before $lenrem
+ local($rec)=@_;
+ local($h,$str,$ind,@p)=(split(/,/,$rec,5))[0,4];
+ local($good)=(0);
+ if ($h<2) {
+ while ($lenrem<$lenadd && ($ind=rindex($str," ",$lenrem))>-1) {
+ warn "Cut found at $ind, lenrem=$lenrem\n" if $debug & $debug_flow;
+ $good=1;
+ # $ind=1 means we can cut 2 chars
+ @p= &cut($ind+1,$rec);
+ warn "After cut: @p\n" if $debug & $debug_record;
+ push(@out,$p[0]);
+ $curlength+=$ind+1;
+ #if ($#out!=$chunks[$#chunks]) {push(@chunks,$#out);}
+ &print();
+ $rec=$p[1];
+ ($lenadd,$str)=(split(/,/,$rec,5))[1,4];
+ $lenrem=$linelength;
+ }
+ return $rec if $good;
+ }
+ # If the added record is too long, there is no sense in cutting
+ # things we have already, since we will cut the added record anyway...
+ local($forcedcut);
+ if ($lenadd > $linelength && $lenrem) {
+ @p= &cut($lenrem,$rec);
+ warn "After forced cut: @p\n" if $debug & $debug_record;
+ push(@out,$p[0]);
+ $curlength+=$lenrem;
+ &print();
+ $rec=$p[1];
+ ($lenadd,$str)=(split(/,/,$rec,5))[1,4];
+ $lenrem=$linelength;
+ }
+ # Now try to find a cut before the added record
+ if ($#out>=0 && !$forcedcut) {
+ for (0..$#out) {
+ ($h,$str)=(split(/,/,$out[$#out-$_],5))[0,4];
+ if ($h<2 && ($ind=rindex($str," "))>-1 && ($ind>0 || $_<$#out)) {
+ warn "Cut found at $ind, in chunk $#out-$_\n"
+ if $debug & $debug_flow;
+ # split at given position
+ @p=&cut($ind+1,$out[$#out-$_]);
+ $out[$#out-$_]=$p[0];
+ @p=($p[1],@out[$#out-$_+1..$#out]);
+ @out=@out[0..$#out-$_];
+warn "\@p is !", join('!', @p), "!\n\@out is !", join('!', @out), "!\n"
+ if $debug & $debug_flow;
+ &print();
+ warn "did reach that\n"
+ if $debug & $debug_length;
+ @out=@p;
+ $good=1;
+ $curlength=0;
+ for (@out) {$curlength+=&length($_);}
+ last;
+ }
+ warn "did reach wow-this\n"
+ if $debug & $debug_length;
+ }
+ warn "did reach this\n"
+ if $debug & $debug_length;
+ }
+ return &prepare_cut if $good;
+ warn "No cut found!\n" if $debug & $debug_flow;
+ # If anything else fails use force
+ &print();
+ while (&length($rec)>$linelength) {
+ @p=&cut($linelength,$rec);
+ @out=($p[0]);
+ &print();
+ $rec=$p[1];
+ }
+ $curlength=0;
+ return $rec;
+}
+
+# Adds a record to the output list
+
+sub commit {
+ warn "Adding $_[0]\n" if $debug & $debug_flow;
+ warn "B:Last chunk number $#chunks, last record $#out\n" if $debug & $debug_flow;
+ local($rec)=@_;
+ if ($#level==0) {
+ local($len)=&length($_[0]);
+ if ($curlength+$len>$linelength) {
+ $rec=&prepare_cut;
+ $len=&length($rec);
+ }
+ $curlength+=$len;
+ }
+ push(@out,$rec);
+ if ($#out!=$chunks[$#chunks]) {push(@chunks,$#out);}
+ warn "a:Last chunk number $#chunks, last record $#out, the first chunk\n" if $debug & $debug_flow;
+ warn " on the last level=$#level is $level[$#level], waiting for $wait[$#level]\n" if $debug & $debug_flow;
+ if ($#level && $wait[$#level] == $#chunks-$level[$#level]+1) {
+ local($sub,$arg)=($action[$#level]);
+ if ($sub eq "") {&finish($wait[$#level]);}
+ else {
+ &callsub($sub);
+ }
+ }
+ warn "curlength=$curlength on level=$#level\n" if $debug & $debug_length;
+}
+
+# Calls a subroutine, possibly with arguments
+
+sub callsub {
+ local($sub)=(shift);
+ index($sub,";")>=0?
+ (($sub,$arg)=split(";",$sub,2), &$sub($arg)):
+ &$sub;
+}
+
+# Simulates Removing a record from the output list (unfinished)
+
+sub uncommit {
+ warn "Deleting...\n" if $debug & $debug_flow;
+ warn "B:Last chunk number $#chunks, last record $#out\n" if $debug & $debug_flow;
+ (warn "Nothing to uncommit", return) if $#out<0;
+ if ($#level==0) {
+ local($len)=&length($out[$#out]);
+ $curlength-=$len;
+ }
+ local($rec);
+ $rec=$out[$#out];
+ $out[$#out]=&empty();
+ warn "UnCommit: now $chunks[$#chunks] $rec\n__ENDREC__\n"
+ if $debug & $debug_record;
+ #if ($#out<$chunks[$#chunks]) {pop(@chunks);}
+ warn "a:Last chunk number $#chunks, last record $#out, the first chunk\n" if $debug & $debug_flow;
+ warn " on the last level=$#level is $level[$#level], waiting for $wait[$#level]" if $debug & $debug_flow;
+ warn "curlength=$curlength on level=$#level\n" if $debug & $debug_length;
+ return $rec;
+}
+
+# finish($event, $force_one_group)
+
+# Finish the inner scope with the event $event. If this scope is empty,
+# add an empty record. If finishing the group would bring us to toplevel
+# and $force_one_group is not set, can break things into chunks to improve
+# line-breaking.
+
+# No additional action is executed
+
+sub finish {
+ warn "Finishing with $_[0]\n" if $debug & $debug_flow;
+ local($event,$len,$rec)=(shift);
+ if (($wait[$#level] ne "") && ($wait[$#level] ne $event)) {
+ warn "Got `$event' while waiting for `$wait[$#wait]', rest=$par";
+ }
+ warn "Got finishing event `$event' in the outermost block, rest=$par"
+ unless $#level;
+ if ($#out<$chunks[$level[$#level]]) {push(@out,&empty);}
+ # Make anything after $level[$#level] one chunk if there is anything
+ warn "B:Last $#chunks, the first on the last level=$#level is $level[$#level]" if $debug & $debug_flow;
+ $#chunks=$level[$#level]; #if $chunks[$level[$#level]]<=$#out;
+ local(@t);
+ if ($#level==1 && !$_[0]) {
+ @t=@out[$chunks[$#chunks]..$#out];
+ $#out=$chunks[$#chunks]-1;
+ }
+ # $#chunks-- if $chunks[$#chunks-1]==$chunks[$#chunks];
+ $#level--;
+ $#action--;
+ $#tokenByToken--;
+ $#wait--;
+ if ($#level==0 && !$_[0]) {
+ for (@t) {&commit($_);}
+ }
+ warn
+ "a:Last $#chunks, the first on the last level=$#level is $level[$#level]"
+ if $debug & $debug_flow;
+ if ($wait[$#level] == $#chunks-$level[$#level]+1) {
+ local($sub)=($action[$#level]);
+ if ($sub eq "") {&finish($wait[$#level]);}
+ else {&callsub($sub);}
+ }
+}
+
+# finish level and discard it
+
+sub finish_ignore {
+ warn "Finish_ignoring with $_[0]\n" if $debug & $debug_flow;
+ local($event,$len)=(shift);
+ if (($wait[$#level] ne "") && ($wait[$#level] ne $event)) {
+ warn "Got `$event' while waiting for `$wait[$#wait]', rest=$par";
+ }
+ warn "Got finishing event `$event' in the outermost block, rest=$par" unless $#level;
+ $#out=$chunks[$level[$#level]]-1;
+ pop(@level);
+ pop(@tokenByToken);
+ pop(@action);
+ pop(@wait);
+}
+
+# Begin a new level with waiting for $event
+
+# Special events: If number, wait this number of chunks
+
+sub start {
+ warn "Beginning with $_[0], $_[1]\n" if $debug & $debug_flow;
+ warn "B:Last $#chunks, the first on the last level=$#level is $level[$#level]" if $debug & $debug_flow;
+ if ($chunks[$level[$#level]] <= $#out && $chunks[$#chunks] <= $#out) {
+ # the last level is non empty
+ push(@chunks, $#out + 1);
+ }
+ push(@level, $#chunks);
+ push(@tokenByToken, 0);
+ $wait[$#level] = shift;
+ if ($#_<0) { $action[$#level] = ""; } else { $action[$#level] = shift; }
+ warn "a:Last $#chunks, the first on the last level=$#level is $level[$#level]" if $debug & $debug_flow;
+}
+
+# Asserts that the given number of chunks exists in the last level
+
+sub assertHave {
+ local($i,$ii)=(shift);
+ if (($ii=$#chunks-$level[$#level]+1)<$i) {
+ warn "Too few chunks ($ii) in inner level, expecting $i";
+ return 0;
+ }
+ return 1;
+}
+
+# Takes the last ARGUMENT chunks, collapse them to records
+
+sub collapse {
+ warn "Collapsing $_[0]...\n" if $debug & $debug_flow;
+ local($i,$ii,$_)=(shift);
+ if (($ii=$#chunks-$level[$#level]+1)<$i) {
+ warn "Too few chunks ($ii) in inner level, expecting $i";
+ $i=$ii;
+ }
+ if ($i>0) {
+ for (0..$i-1) {
+ &collapseOne($#chunks-$_);
+ }
+ for (1..$i-1) {
+ $chunks[$#chunks-$_+1]=$chunks[$#chunks-$i+1]+$i-$_;
+ }
+ }
+}
+
+# Collapses all the chunks on given level
+
+sub collapseAll {&collapse($#chunks-$level[$#level]+1);}
+
+# Collapses a given chunk in the array @out. No correction of @chunks is
+# performed
+
+sub collapseOne {
+ local($n)=(shift);
+ local($out,$last,$_)=($out[$chunks[$n]]);
+ if ($n==$#chunks) {$last=$#out;} else {$last=$chunks[$n+1]-1;}
+ warn "Collapsing_one $n, records $chunks[$n]..$last\n"
+ if $debug & $debug_flow;
+ return unless $last>$chunks[$n];
+ warn "Collapsing chunk $n beginning at $chunks[$n], ending at $last\n" if $debug & $debug_flow;
+ for ($chunks[$n]+1..$last) {
+ $out=&join($out,$out[$_]);
+ }
+ splice(@out,$chunks[$n],$last+1-$chunks[$n],$out);
+ # $#out-=$last-$chunks[$n]; #bug in perl?
+ warn "Collapsed $chunks[$n]: $out[$chunks[$n]]\n__END__\n" if $debug & $debug_record;
+}
+
+# Return an empty record
+
+sub empty {
+ return "0,0,0,0,";
+}
+
+# Commits a record with a sum symbol
+sub sum {
+ &commit("3,2,1,0," . <<'EOF');
+__
+❯
+‾‾
+EOF
+}
+
+# Additional argument specifies if to make not-expandable, not-trimmable
+
+sub string2record {
+ local($h,$sp)=(0);
+ if ($_[1]) {$h=1;$sp=0;}
+ else {
+ $sp=($_[0] =~ /(\s)/g);
+ $sp || ($sp=0); # Sometimes it is undef?
+ }
+ return "$h," . length($_[0]) . ",0,$sp,$_[0]";
+}
+
+# The second argument forces the block length no matter what is the
+# length the string (for strings with screen escapes).
+
+sub record_forcelength {
+ $_[0] =~ s/^(\d+),(\d+)/$1,$_[1]/;
+}
+
+sub finishBuffer {
+ while ($#level > 0) {
+ &finish("");
+ }
+ &print(1);
+}
+
+# Takes two records, returns a record that concatenates them vertically
+# To make fraction simpler, baseline is the last line of the first record
+
+sub vStack {
+ local($h1,$l1,$b1,$sp1,$str1)=split(/,/,shift,5);
+ local($h2,$l2,$b2,$sp2,$str2)=split(/,/,shift,5);
+ $h1 || $h1++;
+ $h2 || $h2++;
+ local($h,$l,$b)=($h1+$h2, ($l1>$l2 ? $l1: $l2), $h1-1);
+ warn "\$h1=$h1, \$h2=$h2, Vstacked: $h,$l,$b,0,$str1\n$str2\n__END__\n" if $debug & $debug_record;
+ return "$h,$l,$b,0,$str1\n$str2";
+}
+
+# Takes two records, returns a record that contains them and forms
+# SupSub block
+
+sub superSub {
+ local($h1,$l1,$b1,$sp1,$str1)=split(/,/,shift,5);
+ local($h2,$l2,$b2,$sp2,$str2)=split(/,/,shift,5);
+ $h1 || $h1++;
+ $h2 || $h2++;
+ local($h,$l)=($h1+$h2+1, ($l1>$l2 ? $l1: $l2));
+ return "$h,$l,$h1,0,$str1\n\n$str2";
+}
+
+# Takes two records, returns a record that contains them and forms
+# SupSub block
+
+sub subSuper {
+ local($h1,$l1,$b1,$sp1,$str1)=split(/,/,shift,5);
+ local($h2,$l2,$b2,$sp2,$str2)=split(/,/,shift,5);
+ $h1 || $h1++;
+ $h2 || $h2++;
+ local($h,$l)=($h1+$h2+1, ($l1>$l2 ? $l1: $l2));
+ return "$h,$l,$h1,0,$str2\n\n$str1";
+}
+
+# Takes the last two records, returns a record that contains them and forms
+# SupSub block
+
+sub f_subSuper {
+ warn "Entering f_subSuper...\n" if $debug & $debug_flow;
+ &trim(2);
+ &collapse(2);
+ &assertHave(2) || &finish("",1);
+ &sup_sub(0,1);
+}
+
+sub sup_sub {
+ local($p1,$p2)=($#out-shift,$#out-shift);
+ warn "Super $p1 $out[$p1]\nSub $p2 $out[$p2]\n__END__\n" if $debug & $debug_record;
+ local($h1,$l1,$b1,$sp1,$str1)=split(/,/,$out[$p1],5);
+ local($h2,$l2,$b2,$sp2,$str2)=split(/,/,$out[$p2],5);
+ if ($l1==0 && $l2==0) {return;}
+ $h1 || $h1++;
+ $h2 || $h2++;
+ local($h,$l)=($h1+$h2+1, ($l1>$l2 ? $l1: $l2));
+ $#chunks--;
+ $#out--;
+ if ($l1==0) {
+ $h2++;
+ $out[$#out]="$h2,$l,0,0,\n$str2";
+ } elsif ($l2==0) {
+ $h=$h1+1;
+ $out[$#out]="$h,$l,$h1,0,$str1\n";
+ } else {
+ $out[$#out]="$h,$l,$h1,0,$str1\n\n$str2";
+ }
+ warn "a:Last $#chunks, the first on the last level=$#level is $level[$#level]" if $debug & $debug_flow;
+ &finish(2,1);
+}
+
+# Takes the last two records, returns a record that contains them and forms
+# SupSub block
+
+sub f_superSub {
+ warn "Entering f_superSub...\n" if $debug & $debug_flow;
+ &trim(2);
+ &collapse(2);
+ &assertHave(2) || &finish("",1);
+ &sup_sub(1,0);
+}
+
+# digest \begin{...} and similar: handles argument to a subroutine
+# given as argument
+
+sub f_get1 {
+ warn "Entering f_get1...\n" if $debug & $debug_flow;
+ (warn "Argument of f_get1 consists of 2 or more chunks", return)
+ if $#out != $chunks[$#chunks];
+ local($rec,$sub);
+ #$rec=&uncommit;
+ $rec=$out[$#out];
+ $rec=~s/.*,//;
+ $sub=shift;
+ defined $sub ? return &$sub($rec): return $rec;
+}
+
+sub f_begin {
+ warn "Entering f_begin...\n" if $debug & $debug_flow;
+ &collapse(1);
+ &assertHave(1) || &finish("");
+ local($arg,$env)=(&f_get1());
+ &finish_ignore(1);
+ $arg=~s/^\s+//;
+ $arg=~s/\s+$//;
+ return if defined $environment_none{$arg};
+ if (defined ($env=$environment{$arg})) {
+ local($b,$e)=split(/,/,$env);
+ for (split(":",$b)) {&callsub($_);}
+ } else {&puts("\\begin{$arg}");}
+}
+
+sub f_end {
+ warn "Entering f_end...\n" if $debug & $debug_flow;
+ &collapse(1);
+ &assertHave(1) || &finish("");
+ local($arg,$env)=(&f_get1());
+ &finish_ignore(1);
+ $arg=~s/^\s+//;
+ $arg=~s/\s+$//;
+ return if defined $environment_none{$arg};
+ if (defined ($env=$environment{$arg})) {
+ local($b,$e)=split(/,/,$env,2);
+ for (split(":",$e)) {&callsub($_);}
+ } else {&puts("\\end{$arg}");}
+}
+
+
+sub f_literal_no_length {
+ warn "Entering f_literal_with_length...\n" if $debug & $debug_flow;
+ # &trim(1);
+ &collapse(1);
+ &assertHave(1) || &finish("",1);
+ record_forcelength($out[$#out], 0);
+ &finish(1,1);
+}
+
+sub f_discard {
+ warn "Entering f_discard...\n" if $debug & $debug_flow;
+ &finish_ignore($wait[$#level]);
+}
+
+# Takes a number and a record, returns a centered record
+
+sub center {
+ local($len,$left)=(shift,0);
+ warn "Entering center, ll=$len, rec=$_[0]\n__ENDREC__\n" if $debug & $debug_flow;
+ #$_[0]; # bug in perl?
+ local($h1,$l1,$b1,$sp1,$str1)=split(/,/,$_[0],5);
+ $h1 || $h1++;
+ if (($left=$len-$l1)<=0) {return $_[0];}
+ $left=int($left/2);
+ local($out,$first)=("",1);
+ for (split(/\n/,$str1,$h1)) {
+ if ($first) {$first=0;}
+ else {$out .= "\n";}
+ $out .= " " x $left . $_;
+ }
+ return "$h1,$len,$b1,0,$out";
+}
+
+# Example of radical
+#<<'EOF';
+# +--+
+#\|12
+#EOF
+<