Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Stretchable icons #8917

Closed
kkaefer opened this issue Oct 28, 2019 · 7 comments
Closed

RFC: Stretchable icons #8917

kkaefer opened this issue Oct 28, 2019 · 7 comments

Comments

@kkaefer
Copy link
Member

kkaefer commented Oct 28, 2019

nine-part icons

We intend to implement stretchable icons, aka 9-part icons. Mapbox GL currently supports icon-size, which linearly resizes the entire icon in both dimensions. Stretchable icons allow defining certain areas of an icon that may be stretched, leaving the remaining areas of an icon at a fixed size. This is useful for icons that need to accomodate text of varying width, e.g. highway shields, or text frames.

This is how we imagine our implementation will look like:

Sprite JSON format

The metadata file that describes a sprite image already contains information about where an image is within a sprite, as well as the pixel ratio. We're going to add three fields:

  • content: An array of 4 numbers, which represent two coordinates relative to the top left of the icon that span up a rectangle. Both coordinates must be within the dimensions of the icon, and the second coordinate must be greater than the first coordinate. The rect must have have a positive area. When sizing icons with icon-text-fit, the icon size will be adjusted so that the this content box fits exactly around the text. If one axis of the icon is not stretchable, the center of the content box will be aligned with the center of the text. Any icon-text-fit-padding values will be applied around the text, and the icon size will be adjusted appropriately.
    Example: "content": [ 2, 4, 16, 9 ]

  • stretch-x/stretch-y: An array of 2-element arrays that specify between which coordinates the icon may be stretched. Consequently, an image may have more than one stretch area, e.g. to allow for a central embellishment that should remain at fixed width. Stretch areas are specified independently for both axes, and either axis may not have any stretch areas at all, meaning that it should only be stretched one one axis. The 2-element arrays specify absolute distances in pixels to the top left corner. Stretch areas may not overlap and they must be ordered ascendingly.
    Example: "stretch-x": [[ 2, 8 ]]

Icon Rendering

Existing, non-stretchable icons are rendered with a quad (i.e. two triangles) that transfers (and scales) the texture from the image atlas. For stretchable icons, we still copy the icon as is into the image atlas, but instead use multiple quads to render the icons. The quads copy the appropriate portion of the icon from the image atlas, either in a true-to-pixels way, or stretched, based on the metadata found in the Sprite JSON.

This implementation strategy means that a stretchable icon in different sizes will not occupy more texture space, but it will occupy some vertex space. While the implementation will support an arbitrary number of stretch zones, we expect most icons will be limited to one or two stretch zones per axis.

Runtime API

We support runtime APIs to add images. They already support supplying metadata, like the pixelRatio, and will now also accept metadata for stretchable icons.

Icon Authoring

Icon designers will be able to add paths or shapes without and specific IDs (= layer names in Ilustrator and Affinity Designer):

<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
    <path d="M..."/>
    <path d="M..."/>
    <rect id="mapbox-icon-stretch-x" x="3" y="0" width="14" height="1" style="fill:none;"/>
</svg>

The Mapbox API will parse SVGs and extract the metadata. Multiple stretch zones can be defined by using multiple paths who’s id property begins with mapbox-icon-stretch-x (or mapbox-icon-stretch-y). Alternatively, user can define a single rectangle with the ID mapbox-icon-stretch to define the stretch zones for both axes simultaneously. The content area can be defined with a rect of mapbox-icon-content. All of these objects can be objects of any shape or path, but their bounding rectangle will be used for metadata generation.

/cc @mapbox/gl-js @mapbox/gl-native @mapbox/studio @mapbox/map-design-team

@kkaefer
Copy link
Member Author

kkaefer commented Oct 29, 2019

With this proposal, the only way to resize icons is icon-size, and icon-text-fit. We could introduce new data-driven layout properties to control the icon size, e.g. icon-width/icon-height, or allow specifying an array of width/height for icon-size.

@samanpwbb
Copy link
Contributor

samanpwbb commented Oct 29, 2019

This looks great – excited to get this in gl and eventually add support in Studio.

I have some specific questions about the SVG format:

  • What do we do when one of the keyword ids like mapbox-icon-content is inside a group with a transform? Skew, rotate, or scale transforms could potentially change the bounding box.
  • What happens when a keyword id is on a path or some other element rather than a rect? Should that throw an error, or should our service try to derive a bounding box anyway?

My hunch is that we may want to be strict rather than loose with enforcing constraints on the format. Likely if a keyword element is inside a transform, for example, the user just did something wrong and would benefit from a helpful error message but I can see arguments for both loose and strict enforcement.

We can hash some of these questions out in mapbox/spritezero#68 if we aren't sure, – then we should definitely document the SVG spec in detail when it's finalized.

@ansis
Copy link
Contributor

ansis commented Nov 11, 2019

camel case

stretch-x/stretch-y

I think this should be stretchX and stretchY since we have pixelRatio in the sprite json.

maybe a flat array?

Most commonly there will only be one stretch zone. It would be nice to avoid the nesting in that case maybe "stretch-x": [[ 2, 8 ]]. One way to do that would be to just flatten that array into one even length array instead of one array of two-length arrays. I'm not sure if that's better or worse

content vs padding

The icon-text-fit-padding does something similar to this so should we stick with padding for naming?

Also, what would the behavior of content be when the content a stretch zone exists outside of the content area? For example:

"width": 10,
"content": [3, 3, 7, 7],
"stretchX": [[1, 9]]

If the text is 40px would the icon be 82px wide?

@tmpsantos
Copy link
Contributor

Prior art:
https://doc.qt.io/qt-5/qml-qtquick-borderimage.html#details

BorderImage {
    width: 180; height: 180
    border { left: 30; top: 30; right: 30; bottom: 30 }
    horizontalTileMode: BorderImage.Stretch
    verticalTileMode: BorderImage.Stretch
    source: "pics/borderframe.png"
}

@mourner
Copy link
Member

mourner commented Nov 27, 2019

I agree with @ansis' points above on naming:

  • Most icons will have a single stretch area in each axis, and flat arrays are consistent with most of the style spec design — so, I think I'd prefer stretchX: [10, 30, 40, 60] over stretchX: [[10, 30], [40, 60]].
  • I was confused by what the content property does until I realized this is basically similar to CSS padding. Given the prior art, I think changing the semantics and naming to something like stretchPadding: [top, right, bottom, left] would be more intuitive. Also, most icons will have symmetrical padding, so this API makes it possible to add CSS-like shortcuts, like stretchPadding: padding (all sides), or stretchPadding: [vertical, horizontal].

@chloekraw
Copy link
Contributor

chloekraw commented Nov 27, 2019

I discussed some of these points with @ansis and @kkaefer last week but didn't post a ticket update:

camel case

We agreed to camel case for the sprite.json and GL-JS API to match existing precedent there, but keep

  • mapbox-stretch-x
  • mapbox-stretch-y
  • mapbox-content

in the svg metadata for greater clarity.

flat vs nested array

The relationship between each stretch zone is clearer with a nested array. With 2 (or more) stretch zones, stretchX, stretchY, and content | stretchPadding all become flat arrays of four numbers with different meaning.

It's harder to check your work and to keep your numbers straight. Also, the benefit of removing one set of brackets is minor.

content vs padding

With content, all options for addImage needed for this feature (stretchX, stretchY, and content) are consistent in what their input values represent: distance from (0,0), the top-left corner of the image. I see this as the major reason to prefer content over padding.

A secondary benefit: content would align the GL-JS API with the svg metadata and icon authoring instructions for this feature.

Finally, I understand that content is more frequently derived from padding than the reverse, and I'm sure we all know this, but content is also a CSS concept:

stretchPadding: [top, right, bottom, left] would be more intuitive.

stretchPadding explicitly introduces the idea of padding into this feature for the first time. We'd be asking users to think about their stretch zone one way (distance from (0,0)) and where their text goes in a different way (distance from the nearest edge).

When your stretchable icon is a rectangle, you might be indifferent between thinking about this in terms of content or padding. When your stretchable icon is a highway shield, it's more intuitive to think about it in terms of content. The edge of the icon you're thinking about isn't the edge of the image.

Finally, I think the name stretchPadding is problematic because it actually has no relationship to what's stretched in the image. stretchPadding sounds like you're defining the part of the icon that is or isn't Istretched, when in fact, you are not.

most icons will have symmetrical padding

I'm not sure that we know this will be true for users of this feature. Symmetrical padding is already possible with icon-text-fit and icon-text-fit-padding (sure, there are problems). (edit: just realized, asymmetrical padding is also possible today)

A background box design where the top padding or the bottom padding is larger than the other could be the more common for users of this feature.

My point is I wouldn't overindex on this specific use case of symmetrical padding.

this API makes it possible to add CSS-like shortcuts, like stretchPadding: padding (all sides), or stretchPadding: [vertical, horizontal].

I'm not sure if these enhancements would provide a net benefit or if they would just create greater confusion. More ways to do the same thing are not always better.

@ansis
Copy link
Contributor

ansis commented Nov 28, 2019

merged with the originally proposed parameters in #8997

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants