w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.41k stars 646 forks source link

[css-backgrounds-4] `background-filter` #4706

Open meyerweb opened 4 years ago

meyerweb commented 4 years ago

I’m proposing a property: background-filter. It allows the application of filters to background layers, parallel to the way background-blend-mode applies blending modes.

As an example:

body {
   background-image: url(waves.png),url(waves.png),url(horizon.jpg);
   background-position: center, 0 50%, 50% 100%;
   background-repeat: repeat-x;
   background-filter: none, blur(0.2px) opacity(0.8), blur(1px);
}

Unless browsers would like to just implement https://www.w3.org/TR/filter-effects-1/#FilterCSSImageValue, of course. That would address the need, but much more generally. (Thanks to Dan Wilson for the pointer!)

Crissov commented 4 years ago

Perhaps we should really just add pseudo elements for background (and foreground) layers and use the same (SVG) properties everywhere.

https://discourse.wicg.io/t/pseudo-elements-for-stacked-box-layers/4193

body::background {
   repeat: repeat-x;
   position: absolute; /* fixed*/
} 
body::layer(1) {
   content: url(waves.png);
   position: center; /* top: 50%; left: 50%; */
   filter: none;
}
body::layer(2) {
   content: url(waves.png);
   position: 0 50%; /* top: t0%; left: 0%; */
   filter: blur(0.2px) opacity(0.8);
}
body::layer(3) {
   content: url(horizon.jpg);
   position: 50% 100%; /* left: 50%; bottom: 0%; */
   filter: blur(1px);
}
AmeliaBR commented 4 years ago

As Eric notes, this use case was supposed to have been addressed by the filter() image function (which takes a CSS image + a list of filters and returns a new image with the filter applied, and which could be used anywhere a CSS image is accepted).

I believe WebKit has an implementation of filter(), not sure how comprehensive it is. No one else has implemented it, to my knowledge, even behind a flag.

If a background-filter property is more likely to be implemented, then it would address the most common use cases for filter(). There isn't a strong demand for filtering list images, after all. But filtering mask images would be useful, though. And when support for CSS images in SVG fill & stroke finally happens, filtering them would be useful, too. All of which could be supported by similar paired properties (fill-filter, mask-image-filter, etc.) if that's the route we want to take.

It might be slightly more author-friendly to have a separate property (or properties, considering the other use case) instead of trying to jam everything into background-image. But I think most CSS authors would just be happy for an easy way to do blurred backgrounds or semi-transparent background overlays, without having to hack around with absolutely positioned pseudo-elements.

faceless2 commented 4 years ago

No one else has implemented it, to my knowledge, even behind a flag.

Not sure how we missed the filter() function - it looks very useful. It was quite easy for us to do as it turns out, at least for bitmap images - so now there are two implementations!

I agree filter() seems much more versatile - careful use with border-image could give you a drop-shadow on the border alone, which might be useful. You could also use it within an image() function specifyng multiple resolutions - for instance, applying a 10px blur at higher resolution, and 5px otherwise. You couldn't do this with background-filter.

(bit off topic, but this last one is quite interesting for us. All the CSS to print engines that I know of have a custom property allowing you to specify the resolution that the SVG filter is supposed to apply at: generating a filtered image at 96dpi isn't really an option for print. Combining image() and filter() would allow a standardised way to achieve the same results, at least in some cases)

On the other hand, background-filter would be applied after compositing multiple background layers into one before filtering, which isn't an option with the filter() method(*), so it's not a direct replacement.

(*) possible with an SVG filter containing multiple feImage primitives, but impractical.

@Crissov a full pseudo-layer for backgrounds is quite a bit more work, not least because you'd then be able to set properties on it - z-index, border-radius, background, border etc. - and each of these would need to be excluded or have their behaviour defined. Same for the border pseudo-element you mentioned on your wicg page, which is particularly problematic for tables. It's a very big hammer for a use case which, as Amelia points it, is largely wanting to blur or add opacity.

una commented 4 years ago

Is this unique from backdrop-filter? See Example Link

meyerweb commented 4 years ago

@una Yes. This applies filters to the background images, instead of filtering the backdrop behind those images. The closest analogue is the filter() function, which is a better solution by dint of being more broadly usable; see https://www.w3.org/TR/filter-effects-1/#FilterCSSImageValue for details.

css-meeting-bot commented 4 years ago

The CSS Working Group just discussed background-filter, and agreed to the following:

The full IRC log of that discussion <TabAtkins> Topic: background-filter
<astearns> github: https://github.com/w3c/csswg-drafts/issues/4706
<TabAtkins> AmeliaBR: This issue, and the next about background-opacity, are basically the same issue
<TabAtkins> AmeliaBR: When you have layered backgrounds, sometimes you want to modify those backgrounds, or see one thru the other
<TabAtkins> AmeliaBR: Like have a dark overlay over a texture image
<TabAtkins> AmeliaBR: Or you want a slightly blurred image so it's not as distracting from the text over top
<TabAtkins> AmeliaBR: Right now no way to do that
<TabAtkins> AmeliaBR: Some things you can do with blend modes, but not a lot
<smfr> q+
<TabAtkins> AmeliaBR: So today people have to instead put the bg on a pseudo-element and abspos it behind the element's content, and then use 'filter' on it
<TabAtkins> AmeliaBR: Suggestion is to add more properties that describe filter-effects, or perhaps just a simple opacity.
<TabAtkins> AmeliaBR: One thing that immediately is brought up is that we have a spec for this fucntionality - in filter effects, there's a filter() function that should take an image, apply filters to it
<TabAtkins> AmeliaBR: So could say "background-image: filter(url(...) blur(...));"
<TabAtkins> AmeliaBR: But no impl of that
<faceless2_> We implemented t yesterday!
<TabAtkins> AmeliaBR: Don't know the reasons for that, if therea re technical issues or just priority
<fantasai> There's also https://drafts.fxtf.org/filter-effects-2/ which has no FPWD or anything
<TabAtkins> AmeliaBR: Other side, about separating to properties, that might be nicer from an authoring perspective to just reuse the existing bg model, rather than trying to squish everyting into a proeprty
<TabAtkins> AmeliaBR: But need to talk about whether people will implement if specced in a different way
<astearns> ack smfr
<TabAtkins> smfr: We do have stable impl of filter() in webkit
<TabAtkins> smfr: I like it because authors are used to filter property, this is a simple way to apply that in a diff context
<TabAtkins> smfr: And lets you do the classic thing of shipping one set of images and changing them on the fly with filters to do what you want
<TabAtkins> smfr: Plus you can animate the filters
<faceless2_> q+
<TabAtkins> smfr: So I like filter(), wold like to hear form othe rimplementors
<TabAtkins> smfr: There's also a proposal to support the asme filters for canvas
<TabAtkins> smfr: So if they don't have it already, will need it soon anyway
<TabAtkins> smfr: Also, filter() can be used anywhere, like border-image. If doing a background-* property, can't apply it to othe rimage props
<astearns> ack faceless2_
<TabAtkins> faceless2_: One difference between background-filter and filter(), background-filter aplies to all layers after merging, filter() is per-layer
<TabAtkins> faceless2_: Also when trying to blur image, filter doesn't apply beyond the bounds of the image
<TabAtkins> faceless2_: Not a problem if you have a single image covering the hwole bg
<TabAtkins> faceless2_: But if you're tiling the image, you won't get th edesired effect
<astearns> ack fantasai
<TabAtkins> fantasai: So i think there's still a usecase for a whole-layer filter
<TabAtkins> s/fantasai/faceless2_/
<TabAtkins> fantasai: Good point
<TabAtkins> fantasai: Another proposal for backdrop-filter property; it does something else entirely.
<smfr> q+
<TabAtkins> It's not the backgrounhd at all, actually
<TabAtkins> smfr: backdrop-filter is different
<TabAtkins> smfr: It renders what's behind the element, then filter it. *Then* the element itself, including its background, is composited atop it.
<TabAtkins> fantasai: So there's still another case for compositing the bg layers together before filtering
<AmeliaBR> q+
<TabAtkins> smfr: Yeah, definitely a difference there. Use-cases for both
<TabAtkins> smfr: For backgrounds, if you're using colors you can use alpha.
<TabAtkins> smfr: Would be curious to see use-cases for bg-filter that wants the whole bg at once
<faceless2_> q+
<TabAtkins> smfr: And if doing it for bg, why not for border? So you can blur your borders? Feature-creep potential.
<heycam> q+
<TabAtkins> fantasai: I think there are cases of peopl eusing bg to make a stretchtable scene in several layers, and there you'd want to composite them together berfore applying opacity
<TabAtkins> astearns: Any response to simon's question about other impls of the filter() fucntion?
<fantasai> i/astearns/smfr: that's a good use case/
<TabAtkins> iank_: Have to check wit the pain team
<astearns> q?
<TabAtkins> emilio: Seems like putting it in the image is the more flexible thing to do
<tantek> s/fucntion/function
<faceless2_> q+
<astearns> ack smfr
<astearns> ack dbaron
<Zakim> dbaron, you wanted to mention controls for treatment of edges
<TabAtkins> dbaron: Someone mentioned tiled bg images, and diff between blurring each tile vs the whole set
<TabAtkins> dbaron: One control that's common here is what's off the edge
<TabAtkins> dbaron: One option is the edge is transparent black, vs closest pixel, vs pretend you tiled
<TabAtkins> dbaron: So with that control you could get the effect you wanted
<astearns> ack AmeliaBR
<TabAtkins> dbaron: Filter controls like that are generally applicable, worth considering
<astearns> zakim, close queue
<Zakim> ok, astearns, the speaker queue is closed
<TabAtkins> AmeliaBR: That's call edgemode attribute on feGaussianBlur
<TabAtkins> AmeliaBR: filter() is, as defined in spec, should magically choose edgemode based on tiling or not
<smfr> “For the <blur()> function the edgeMode attribute on the feGaussianBlur element is set to duplicate. This produces more pleasant results on the edges of the filtered input image.”
<TabAtkins> AmeliaBR: If bg image is tiled, is does blurring smoothly.
<TabAtkins> AmeliaBR: So for what is getting filtered, I think that's worth talking about, but if there's a concern, there are ways to address that concern.
<TabAtkins> I can dust off the @filter rule proposal, dino expressed interest in it again a while back...
<TabAtkins> AmeliaBR: Maybe there are use-cases for both "filter each layer" vs "filter layers together"
<TabAtkins> AmeliaBR: But filter() function is def treating each image before tiling/compositing.
<TabAtkins> AmeliaBR: Other question is why only bg?
<TabAtkins> AmeliaBR: I brought this up as well. If we did it as a spearate property, all the other similar groups would want a matching prop, so could add a bunch of properties.
<TabAtkins> AmeliaBR: fill-image-filter, border-image-filter, et
<astearns> ack faceless2_
<TabAtkins> faceless2_: Re: compositing borders together, table borders would make me cry...
<TabAtkins> faceless2_: And table bgs are stacked up as well, phew
<astearns> ack heycam
<TabAtkins> heycam: I think it's not just an issue with tiling; agree we need something to fix that.
<TabAtkins> heycam: Also stretching - before or after filter() could affect things.
<TabAtkins> Yeah, blur() would act differently if applied before or after stretching.
<TabAtkins> AmeliaBR: So, we have filter() in the spec. There are clear use-cases.
<TabAtkins> AmeliaBR: Do we feel that, from an authoring or impl standpoint, filter() isn't good enough and we need to look at new options? Or is it good enough, we close this no-change, and continue working on filter()
<TabAtkins> fantasai: I think this addresses a number of the cases.
<TabAtkins> fantasai: But think it doesn't address "i want opacity on entire bg at once, but not on the text", which is a common use
<fantasai> TabAtkins: One of the things suggested there was a ::background pseudo
<fantasai> TabAtkins: if you could put filter on that, could achieve that case at least syntactically it's easy
<fantasai> astearns: Would end up with a lot of pseudos for each thing you might want to filter...
<TabAtkins> astearns: Then you run into "well if you do it for bgs, why not borders, etc"
<fantasai> TabAtkins: Well, so far it seems that filter per layer shouldn't be handled by separate properties
<fantasai> TabAtkins: Pursue filter() function as preferred method for that
<fantasai> astearns: Do we leave background-filter issue open to deal with the background-all-at-once case?
<fantasai> AmeliaBR: I think we should see more use of filter() before we say that use case isn't addressed
<TabAtkins> AmeliaBR: Think we need wider us eof the filter() before we can conclude we need to address more use-cases
<TabAtkins> astearns: So we can do that - close these as no-change and use filter() function
<TabAtkins> astearns: Objections?
<TabAtkins> RESOLVED: Close #4736 and #4715 as no-change; filter() is what's intended to solve these cases.
fantasai commented 4 years ago

@meyerweb CSSWG resolved to close this as no-change, in favor of using the filter() function. You may want to read the minutes above. Does this work for you, or do you think we need to reconsider something?

fantasai commented 4 years ago

Note that the use case of filtering all the layers of the background composited together, or or filtering the images after they've been stretched is not handled by using filter(), it would need a non-list-valued background-filter property.

fantasai commented 4 years ago

I suppose another thing that's not handled by filter() is just the poor cascading ergonomics: you can't cascade the filter independently of the image source. Which is often useful to do.

tabatkins commented 4 years ago

You can use variables to handle that, if you need it - set that layer as var(--bg, url(...)), then set in :hover --bg: filter(...);.

But yeah, "making the entire bg stack partially transparent" does need something other than filter(). Solving that completely probably requires porting over SVG's filter tree (I have a rough draft of a proposal for that!), but we probably can hit a large chunk of the use-cases with a single-valued background-filter that just applies a filter to the whole composited stack, yeah.

faceless2 commented 4 years ago

Regarding filter() on images that have been stretched, I've been doing some testing on this (see https://github.com/web-platform-tests/wpt/pull/23429/files).

In Webkit and now our implementation, the filter is applied after any stretching that occurs in background-image. I think this is almost certainly correct - for example:

div { background: filter(url(small-20x20.png), blur(5px)) left top / 200px 200px }

should give a 5px blur as specified by the user, not a 50px blur because the image has been scaled up by a factor of 10.

I suppose you could make this argument either way, but after a few days working with it this approach seems the right one. It makes resizing or substituting an image easier, and it means you can apply a consistent blur to image-set(), for example, without having to worry about the resolution of the images. It's also much easier to apply to border-image, where scaling is less intuitive.

Either way, the approach has to be consistent for all cases where images can be specified as a url() and have their size overridden: background-image, mask-image, border-image, mask-border and content (did I miss any?). Webkit does this with background-image and mask-image, doesn't with border-image and doesn't yet support images as replaced content, apparently. No other browser support filter() yet.


Aside: although not strictly relevant to the original issue, an example we've been working on is at https://jsbin.com/xisohum/edit?css,output - testing filter() on border-image and background-image, as well as initial-letters. It largely works in Safari, although there are a few issues - note the much-less-than-specified blur on the frame, due to the filter being applied to the very high resolution image before scaling: image

Here's how we render it:

image

Tomorrow's technology: bringing you the look of yesterday, today.

tabatkins commented 4 years ago

In Webkit and now our implementation, the filter is applied after any stretching that occurs in background-image. I think this is almost certainly correct - for example:

That is surprising!

I agree that the effect is likely what authors want, but it strongly feels like a layering violationg - filter() should return an image, which is then stretched by background-image. Instead, I guess filter() is returning an "image with filtering instructions", which is then stretched by background-image, and then the "filtering instructions" are applied before the image is composited with other layers?

If that's what we want, we'll have to be very careful to describe that correctly, along with the timing instructions for when, precisely, to apply the filtering instructions.

it means you can apply a consistent blur to image-set(), for example, without having to worry about the resolution of the images.

I'll note that this happens no matter what - image-set() returns an image with resolution-adjustment already applied, so if one of the options is twice the size but 2x resolution, it'll look the same size to future steps. (At least, conceptually.)

faceless2 commented 4 years ago

TBH it surprised me too, and I had the same recoil regarding the operations being in the wrong order. But treated as an image-with-filtering-instructions (which is exactly what we do) it works out nicely.

But the topic is wandering so I'll raise another issue to continue discussion.

meyerweb commented 4 years ago

@meyerweb CSSWG resolved to close this as no-change, in favor of using the filter() function. You may want to read the minutes above. Does this work for you, or do you think we need to reconsider something?

I think filter() works fine for the use case I had in mind, though the comments subsequent to yours (from you as well as others) do point to use cases I hadn’t had in mind, and seem to me to speak to a need for a new property or properties. (And I love your illustrations, @faceless2!)