diff --git a/src/fontra_compile/builder.py b/src/fontra_compile/builder.py
index 67a9202..99a61f5 100644
--- a/src/fontra_compile/builder.py
+++ b/src/fontra_compile/builder.py
@@ -834,8 +834,29 @@ def applyAxisMapToAxisValues(axis) -> tuple[float, float, float]:
return (minValue, defaultValue, maxValue)
-def axisTuple(axis) -> tuple[float, float, float]:
- return (axis.minValue, axis.defaultValue, axis.maxValue)
+def axisTuple(axis, fixAsymmetricAxes=True) -> tuple[float, float, float]:
+ minValue, defaultValue, maxValue = axis.minValue, axis.defaultValue, axis.maxValue
+ if fixAsymmetricAxes and minValue < defaultValue < maxValue:
+ # Variable component axis values can interpolate across the "default" border.
+ # For example if an axis goes from 0 to 1000 with the default at 200, a variable
+ # component may interpolate this from 100 to 600. In the VARC table, all axis
+ # values will be normalized to (-1, 0, +1). So 100 would normalize to -0.5 and 600
+ # would normalize to +0.5. But this means that interpolation does not work the
+ # same in the normalized space. For example, the midpoint between -0.5 and +0.5
+ # is 0, but the midpoint between 100 and 600 is 350, which would normalize to
+ # 0.1875. This is obviously a problem.
+ # To work around it, we extend either side of the axis so the distance between
+ # minValue and defaultValue becomes the same as the distance between defaultValue
+ # and maxValue.
+ # The downside of this approach is that axis values will no longer be clipped to
+ # their original minimum or maximum, so we may create new edge cases here.
+ minDiff = defaultValue - minValue
+ maxDiff = maxValue - defaultValue
+ if minDiff > maxDiff:
+ maxValue = defaultValue + minDiff
+ elif minDiff < maxDiff:
+ minValue = defaultValue - maxDiff
+ return minValue, defaultValue, maxValue
def newAxisDescriptor(
diff --git a/tests/data/notosanscjksc.otf.ttx b/tests/data/notosanscjksc.otf.ttx
index 4c077a2..509535c 100644
--- a/tests/data/notosanscjksc.otf.ttx
+++ b/tests/data/notosanscjksc.otf.ttx
@@ -21,12 +21,12 @@
-
+
-
-
+
+
@@ -539,7 +539,7 @@
-
+
@@ -2053,6 +2053,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2124,7 +2196,7 @@
-
+
@@ -2196,7 +2268,7 @@
-
+
@@ -2268,7 +2340,7 @@
-
+
@@ -2340,7 +2412,7 @@
-
+
@@ -2412,7 +2484,7 @@
-
+
@@ -2484,7 +2556,7 @@
-
+
@@ -2556,7 +2628,7 @@
-
+
@@ -2628,7 +2700,7 @@
-
+
@@ -2700,7 +2772,7 @@
-
+
@@ -2772,7 +2844,7 @@
-
+
@@ -2844,7 +2916,7 @@
-
+
@@ -2916,7 +2988,7 @@
-
+
@@ -2988,7 +3060,7 @@
-
+
@@ -3060,7 +3132,7 @@
-
+
@@ -3132,7 +3204,7 @@
-
+
@@ -3204,7 +3276,7 @@
-
+
@@ -3276,7 +3348,7 @@
-
+
@@ -3348,7 +3420,7 @@
-
+
@@ -3420,7 +3492,7 @@
-
+
@@ -3492,7 +3564,79 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -3564,7 +3708,7 @@
-
+
@@ -3681,9 +3825,9 @@
-
-
-
+
+
+
@@ -3702,22 +3846,22 @@
-
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -3725,7 +3869,7 @@
-
+
@@ -3733,7 +3877,7 @@
-
+
@@ -3741,9 +3885,9 @@
-
+
-
+
@@ -3752,10 +3896,10 @@
-
-
-
-
+
+
+
+
@@ -3765,15 +3909,15 @@
-
+
-
-
+
+
@@ -3783,17 +3927,17 @@
-
+
-
-
-
-
+
+
+
+
@@ -3928,8 +4072,8 @@
-
-
+
+
@@ -4072,7 +4216,7 @@
-
+
@@ -4095,9 +4239,9 @@
-
+
-
+