colr-gradients-spec
colr-gradients-spec copied to clipboard
COLR for non-color use cases
In the context of COLRv1 being a superset of the features of composite glyphs, I wonder if it is too late to consider adding the possibility of rotation by a numeric value, rather than using the affine transformation matrix? Especially when combined with a variation axis, it would be useful not just for arrows, ornaments and clock faces, but also to enable chained rotations, so that mechanical objects, such as animal skeletons, can be represented efficiently. Such freedom of movement must currently be represented by multiple keyframes or by higher order interpolation (HOI). The former suffers from inaccuracy, the latter from an opaque build process, lack of support in UI, and effective limitation to 90°.
YES. Proper rotation would be huge!
Better support for rotation in variation (continuous variation of an angle θ) sounds useful, but it doesn't seem like burying it within the COLR table would be exactly what you want. I mean, it would be possible to have behaviour like a non-colour font—the glyph fill is only ever text foreground colour; but needing to add COLR and CPAL to get that seems kludgy.
What makes rotation as variation difficult now is that the basis function used to calculate variation scalar coefficients is strictly linear (in a single active segment—a "hockey stick" function—or in two segments going from 0 to 1 to 0—a "tooth" function). What if the format were extended to support non-linear basis functions?
Future-proofing this new format, with capabilities that are called for in ‘glyf’, is reasonable I think. COLRv1 in fact already does this to an extent by enabling proper handling of white-on-black shapes, which is nothing to do with “colour”. As @davelab6 wrote here https://github.com/googlefonts/colr-gradients-spec/issues/3#issuecomment-694888194 : “I don't see an issue with CPAL v2 [sic.] edging ahead, and then updating glyf to match the capability set”.
So I would propose rotation in turns or degrees. I would submit that rotation and reflection are the most common use of the affine matrix, and should have been in ‘glyf’ all along. Reflection is satisfactorily represented in Affine2x2, but for rotation the representation is poor. If rotation was recorded directly in turns or degrees, many of the most common rotations are represented exactly, without semantic loss and therefore with variation potential.
So, perhaps you have in mind something like the following?
PaintTransformed table:
| Type | Field name | Description |
|---|---|---|
| uint8 | format | Set to 6. |
| uint8 | flags | See below for flag details. |
| Offset24 | paintOffset | Offset to a Paint subtable, from start of PaintTransformed table. |
| Affine2x3 or VarFixed |
transform | An Affine2x3 record (inline), or a rotation angle, depending on flags—see below. |
Flags:
| Mask | Name | Description |
|---|---|---|
| 0x01 | TRANSFORM_AFFINE_2X3 | transform is an Affine2x3 record |
| 0x02 | TRANSFORM_ROTATION | transform is a VarFixed value for a rotation angle, in clockwise radians |
| 0x04 | TRANSFORM_X_REFLECTION | mirror in x-axis—ignored if an Affine2x3 is used |
| 0x08 | TRANSFORM_Y_REFLECTION | mirror in y-axis—ignored if an Affine2x3 is used |
| 0xF0 | RESERVED | reserved flag bits: must not be set |
A down-side of this design is that the size of the PaintTransformed table varies according to the flags set. On the other hand, if rotations are, indeed, the most commonly used, there'd be some size savings: 9 bytes vs. 56 bytes.
A (dx,dy) translation is still needed for reflections and rotations. It may be a good idea to reproduce the option in glyf composites for deciding whether translation occurs before or after transforms, to help glyf-style composites migrate easily to this structure when adding colour.
I would vote against radians, since it leads to irrational numbers for common rotations, and would recommend instead degrees (first choice, because of the excellent divisibility of 360) or turns (second choice).
A revision: this would add a variable-sized record, comparable to the ValueRecord used in GPOS:
PaintTransformed table:
| Type | Field name | Description |
|---|---|---|
| uint8 | format | Set to 6. |
| uint16 | flags | See below for flag details. |
| Offset24 | paintOffset | Offset to a Paint subtable, from start of PaintTransformed table. |
| Affine2x3 or TransformParam |
transform | An Affine2x3 or TransformParam record (inline), depending on flags—see below. |
Affine2x3 record, as currently spec'd.
TransformParam record:
| Type | Field name | Description |
|---|---|---|
| VarFixed | scaleX | |
| VarFixed | scaleY | |
| VarFixed | rotation | rotation angle, in clockwise degrees |
| VarFixed | dx | |
| VarFixed | dy |
The size of a TransformParam record instance is variable, depending on flags that are set.
Flags:
| Mask | Name | Description |
|---|---|---|
| 0x0001 | TRANSFORM_AFFINE_2X3 | transform is an Affine2x3 record |
| 0x0002 | TRANSFORM_SCALE_X | transform is TransformParam record, with scaleX field |
| 0x0004 | TRANSFORM_SCALE_Y | transform is TransformParam record, with scaleY field |
| 0x0008 | TRANSFORM_ROTATION | transform is TransformParam record, with rotation field |
| 0x0010 | TRANSFORM_TRANSLATE_X | transform is TransformParam record, with dx field |
| 0x0020 | TRANSFORM_TRANSLATE_Y | transform is TransformParam record, with dy field |
| 0x0040 | TRANSFORM_X_REFLECTION | mirror in x-axis—ignored if an Affine2x3 is used |
| 0x0080 | TRANSFORM_Y_REFLECTION | mirror in y-axis—ignored if an Affine2x3 is used |
| 0xFF00 | RESERVED | reserved flag bits: must not be set |
Flag bit 0 (0x0001) is mutually exclusive with other bits: if bit 0 is set, other bits are ignored.
Thanks for the continued conversation! True, the COLR table doesn’t really make sense as a place to deal with rotated components. Yes, using the glyf table does seem reasonable, as it would seem to sit nicely beside existing components attributes like x, y, and scale (as labeled in ttx – I’m not very familiar with the formal TTF labeling, and feel mostly out of my depth in this conversation).
I do agree that degrees would be (by far) the most direct & widely-understood way to specify this.
In a discussion with @jfkthame, Jonathan suggested that we might want to specify skew in degrees as well. This would require something like TRANFORM_SKEW_X_DEGREES, TRANFORM_SKEW_Y_DEGREES.
I would be in favour of reducing the flags to only Affine2x3 and separate ones for those where degrees are needed (TRANSFORM_ROTATE, plus the two above) as the linear transformations for scale, translate reflection could be expressed as nested PaintTransforms if they need to be varied separately and otherwise appear to be duplicating what Affine2x3 does.
@jfkthame good addition.
@drott sounds good, Affine2x3 respresents reflection, translate and scale without semantic loss.
IIUC the key thing we want to provide rotation with the angle as something you can directly vary, as opposed to indirectly by forming a PaintTransformed to rotate?
If so I think we should add a new Paint, not glue this onto PaintTransformed which cleanly represents a 2x3 transform.
I also vote for a new Paint format, with flags that select the meaning of the VarFixed field
PaintRotateSkew, with one VarFixed and flags?
It needs three values: angle of rotation, and x/y coords of the center of rotation.
(I'd lean toward separate PaintRotated and PaintSkewed; I don't see any benefit to merging them and requiring a flag to indicate which transform is desired.)
x/y coords of the center of rotation
good point 👍
separate PaintRotated and PaintSkewed
I'm also ok with two different paints.
Pardon my density [kids up all night...] but I'm not clear why skew isn't adequately represented by a PaintTransform? - a static skew at an angle should be fine so I suppose it's specifically to make variation of angle of skew easier?
+1 to PaintRotate {angle, center} and if we need it, to Paint At Angular Skew as it's own paint.
Sure, it can be represented by PaintTransform, but so can rotation. The point is that it's hard to control variation of the angles in this form.
Got it, ty for confirming. For whatever reason the issue with controlling rotation angle made perfect sense and the issue with skew angle didn't quite connect in my head :)
If we're going this way, IMO it'd make sense to also have PaintScaled (four values, for x/y scale factors and origin of scaling), and PaintTranslated (two values), for ease of authoring (and slightly more compact representation, in the case where just a single transform is required). It's fine to still have the general PaintTransformed that can express an arbitrary combination of the transform building blocks, and tools can generate that when it makes sense (e.g. for a complex but static transformation), but it'll often be more author-friendly to work with the individual components.
For rotation, expressing the angle in degrees is clearly more amenable to smooth variation. But what's the particular benefit for expressing skews using angle rather than the matrix?
@PeterConstable smooth and geometrically correct faux italics along the slnt axis :)
For scale and translate my immediate reaction is to be less enthused; it's only a nuisance if you hand-write your transforms. A tool can present them however it wishes and I don't immediately perceive the same issues with representing smooth variation in the underlying format.
So, might PaintRotate and PaintAngularSkew [that's a fun name that might need a visit to a bike shed] suffice?
@Lorp slnt example much appreciated :)
Right, scale and translate don't have the issue with variations; they'd purely be a more compact representation of these particular types of the more general transform. So I don't feel strongly about them one way or the other.
new commit for #113
Why is the rotation specified in clockwise degrees?
For reference:
- anticlockwise: OpenType slnt axis, mathematics in general
- clockwise: CSS transform property, SVG transform attribute
(BTW, the SVG and CSS specs really should specify rotation direction. @jfkthame, perhaps you can recommend the best way to add this to the specs?)
Arbitrarily chosen for initial draft. (At least it wasn't left unspecified.) Or, actually, I think maybe influenced by HTML Canvas 2D (probably the last thing I looked at that explicitly gave the angular direction).
But anti-clockwise is probably the better chose, particularly for consistency with slnt, but also because it makes better sense when considering the matrix representation (where the xform of the i basis vector is typically expressed as (cos(θ), sin(θ)).
+1 to anti-clockwise
The CSS spec does indicate that rotate() is specified clockwise, but I agree it's well hidden (and expressed as an example, rather than directly stated in spec text). I'd suggest filing an issue at https://github.com/w3c/csswg-drafts/ asking for it to be made more explicit.
As for SVG, I don't immediately see any mention of clockwise vs anticlockwise, though the spec does include examples that let one infer that it's clockwise. I'm not sure how best to raise that as an issue; maybe mail to [email protected]?
It's unfortunate that we have this discrepancy between standards that are likely to be used together, but I guess we're stuck with it. Here, I'd agree that anti-clockwise is probably the right choice.
I suggest we use VarFixed for the rotation’s centerX and centerY, not VarFWord. It was persuasively argued that dx and dy in the transform matrix should be VarFixed. Even a simple 90° rotation needs a resolution of 0.5 font units for centerX and centerY.
@Lorp VarFWord has already been changed to VarFixed (I saw you comment on this earlier).
and effective limitation to 90°
I wanted to object to this but then realized that I couldn't find a good explanation as to why this wouldn't be the case. I started writing a comment here about how HOI works (or at least one way it can work), but it got a bit long. So I put my response at https://bungeman.github.io/hoi.html . This shows how to do smooth interpolation along arbitrary contours of bezier curves (and discusses a bit about introducing discontinuities and their limitations).
Note that I'm not against variable rigid rotation of components about a variable point, there are many cases where that is exactly what is wanted so it's probably a good idea to support it. Just pointing out that HOI isn't limited in quite this way and it does have the advantage of being able to exactly follow other bezier contours of the glyph.
I can't argue with lack of editor support though... I created my HOI example font by hand in ttx and even then had to hex edit to get everything right.