-
Notifications
You must be signed in to change notification settings - Fork 688
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
[css-images-3] image-rendering:pixelated should not force "nearest neighbor" (or similar) when the scale factor is far from an integer (e.g. 150%) #5837
Comments
It is indeed the case that So the question then is - are you doing something that really wants (There's a lot we could potentially do to make scaling different or better, but it needs to be driven by a demonstrated need that we have reason to believe is at least somewhat common, or uncommon but highly valuable for the small population that needs it and very painful to work around.) |
Sure. The problem is that in many cases, it is difficult to determine whether the scale factor is integer or non-integer at design time. The first problem is screen resolution: Websites are usually designed so that elements are the same real size on different screens, but that means what is 200% on a high-resolution screen may become 150% on a lower-resolution screen. Plus, there are some high-resolution screens with non-integer dppx (device pixels per CSS pixel), so even CSS pixels can be trusted. Also, when I want to get a better look at an image (such as a retro game screenshot), I may manually zoom in on it in the browser. Therefore, I want even images that are normally displayed at 150% to have the |
Oh, this is timely. I've just run into trouble with "pixelated" and a window.devicePixelRatio of 1.5 on my desktop pc.
[If you have a desktop pc with non-integral devicePixelRatio you can repro it yourself here https://www.puzzlescript.net/play.html?p=fd4e3445b63068676f72 by resizing the window down] You can see some horizontal lines like on the "S" of "simple block pushing game" are thick, while the "start game" lines are thin, even though both are one pixel high. The behaviour I'd like to have available to me for the purpose of a game engine is for it to display the biggest possible image that will fit within the bounds of the canvas, centered inside it, where all upscaled pixels are the same size. (It's a pretty standard approach). I've tried many different approaches to trying to get it to display "integer-multiple"-pixelated but they number of different display surfaces and zooming ratios that constitute the browser rendering stack make this feel like an impossible task. |
That's different from what's being asked about here, and is probably a good fit for an object-fit value, probably as a modifier alongside Okay, so the proposed behavior is to scale up to the nearest integer multiple of the natural size that doesn't overshoot the actual scaling factor, using NN to maintain pixelation, then use Important point for discussion - would this be okay to just build in as the behavior of |
I'll give it a shot, thanks for the suggestion.
For what it's worth I'd prefer if pixelated meant pixelated, even if resulted in weird artefacts like in the first post. There are so many ways to smoothly interpolate pixels in css, but only one way to keep them 'crisp'. |
Personally, I agree with baking this into |
The CSS Working Group just discussed
The full IRC log of that discussion<dael> Topic: [css-images-3] image-rendering:pixelated should not force "nearest neighbor" (or similar) when the scale factor is far from an integer (e.g. 150%)<astearns> zakim, open queue <Zakim> ok, astearns, the speaker queue is open <dael> github: https://github.com//issues/5837 <dael> TabAtkins: image rendering prop controls how browses render when scalling. Smooth or pixelated. pixelated uses nearest neighbor. Great so long as scaling up by int multiple of size. 2.5 times as big it's terrible <dael> TabAtkins: You don't get consistent line weight. Something could be 2 or 3 px depending on precise details. <dael> TabAtkins: At least 2 people in this issue brought up the problem. Want to remain pixel-ness but don't want it to look bad. Minor smoothing okay. <dael> TabAtkins: Prop is a value that does nearest neigbor scaling and use smooth scaling to close gap. <florian> Proposal makes sense to me. <dael> TabAtkins: Use cases seemed reasonable. Canvas-based using pixel art and you don't want jaggies but you don't want to force canvas. You want to scale as you can <smfr> q+ <dael> TabAtkins: Reasonable to me. Happy to add if reasonable to others <dael> fantasai: Overall makes sense. I think we should allow overshoot and scale down. If you're 2.8px might make sense <astearns> ack smfr <dael> TabAtkins: Right. Should test, but we should scale to nearest multiple and then go up or down smooth <vmpstr> q+ <dael> smfr: Does image-render pixelated apply to canvas <dael> TabAtkins: It's supposed to. It's an image source <dholbert> q+ <dael> smfr: With houdini? That's only way to get it in <dael> TabAtkins: canvas element is an image element. It's a replaced element with a raster display of content. Intended to be effected by image rendering <dael> smfr: For a UA to impl it means painting image would be 2 step. pixelated and then nearest neighbor to desitnation. Has cost. Fine feature request, but additional cost <astearns> ack fantasai <dael> TabAtkins: I think you're right. Obj or a note about don't use too much <astearns> ack vmpstr <dael> smfr: Note in spec about perf is good <dael> vmpstr: Suggesting to mandate an algo or allow a different? <dael> TabAtkins: Pixelated madates nearest neigbor. This mandates to nearest int and use whatever smoothing <astearns> ack dholbert <dael> vmpstr: Yeah. This would add cost <fantasai> generally people don't use pixelated unless they really want it, it's not the default <dael> dholbert: I think we have this behavior in spec for scale of less than 1. You do default image scaling. nice to harmonize. <dael> dholbert: Also, not clear. Is this prop for new value or change to pixelated <dael> TabAtkins: Asked in thread. Authors thought different value. I proposed merge into default. I could go either <jfkthame> q+ <dael> dholbert: If we did keep pure nearest neighbor, might be nice to remove <1 special case and have pixelated scaling separate. You can see as you spec <astearns> ack jfkthame <dael> jfkthame: My understanding of last comment in issue is the suggestion is this should be what pixelated does and true nearest neighbor would be new. That makes sense to me. This would be true pixelated and acutal nearest neighbor would be special <fantasai> +1 jfkthame <dael> astearns: Then you make current use of pixelated take the harder path <dael> jfkthame: True, but I think it's the better result. Arguable <dael> fantasai: I imagine it's not that common unless you want that effect <dael> TabAtkins: You want for int. If you use it inbetween is variable. <dael> TabAtkins: dholbert where are you seeing scale down? I'm looking at spec and there is no such difference between up and down <fantasai> Comment jfkthame was referring to: “Personally, I agree with baking this into pixelated. Yes, pixelated should mean pixelated, but I don't think nearest neighbor interpolation with 150% scaling ratio looks pixelated: Squares that vary in size from 1*1 to 2*2 do not look like pixels. I think it would be better to add a new keyword nearest-neighbor, which means nearest neighbor.” <fantasai> https://github.com//issues/5837#issuecomment-776951044 <dael> dholbert: I haven't looked at spec for a couple years. It was there a few years ago. If it's been removed, that's great <dael> TabAtkins: I'll research. not in current ED <dael> astearns: Do you want resolution? <dael> TabAtkins: Add this with caveats discussed in chat <dael> fantasai: New value or adding into pixelated and nearest is new <dael> TabAtkins: Also agree with jfkthame. If vmpstr and smfr don't think it would be problematic I would like to do that <dael> astearns: Smoothing only nec for non-int values? <dael> TabAtkins: Yea <fantasai> s/is new/as new? I personally agree with jfkthame / <dael> astearns: Prop: Bake the smoothing into non-int changes in current pixelated value. add a new value for nearest neighbor jaggedness <dael> myles: Flip the names? <dael> fantasai: I don't think so. Last commentor pointed out having a variety of squares doesn't look pixelated. You want each pixel same size. I think naming is better where pixelated is same size <dael> astearns: Is that okay myles? <dael> myles: No comment <fantasai> s/squares/squares and rectangles representing source pixels/ <dael> astearns: Objections? <dael> RESOLVED: Bake the smoothing into non-int changes in current pixelated value. Add a new value for nearest neighbor jaggedness |
Clarification: would downscaling always use linear scaling since the nearest integer is 1? I know nothing is "pixelated" in that case, but would still be nice to know |
Yes, that's how I'm writing the spec; it makes sure that you find the nearest integer multiple greater than zero to NN it to, then you use smooth scaling to take it the rest of the way, so downscaling is always smooth-only. (It doesn't have to bilinear scaled, you can use whatever algo you want to take it to the finish line.) |
…. Add new nearest-neighbor value for strict NN. #5837
Some relevant history: we had a csswg resolution a while ago, back in https://lists.w3.org/Archives/Public/www-style/2014Sep/0384.html , to allow "pixelated" to use a better scaling algorithm specifically when downscaling (i.e. for scale factors < 1.0). Though this edit didn't make it into the spec (yet). I think the feature-request here (and today's csswg resolution) is just an elegant generalization of that earlier resolution, to now also suggest (mandate?) a better scaling algorithm for fractional values that are larger than 1 as well -- not just fractional values that are less than 1. The previously-agreed-upon < 1.0 case is just a special case of the behavior that we resolved on today. |
For posterity, and to express my "this will add cost" concern a bit better: I think a viable implementation here is a two step process. First, upscale using NN to nearest integer multiple. Then, use something like bilerp to take it all the way to the right scale. The problem (at least in Chromium) is that we would need to store the intermediate result in memory before doing bilerp sampling. This means that if we have something like 1,000,001% scale of an image that is 150x150, previously we'd be able to sample directly out of the 150x150 image and obviously only sample where needed so that after all the clipping, we'd end up with the correct result. However, for this case the result should be a size 1500001.5 image (if my math is right), which means the new algorithm would say that we need to use NN up to 1500001x1500001 and then use bilerp. That's not something we can store, even temporarily :). I know that this is a contrived example, but I think there are less contrived examples that would suffer from similar problems. I'm also hoping there's a way to just sample out of the original image (150x150) with some clever math that would effectively end up with this algorithm, but I haven't really thought about it at length yet. |
Yes, NN can be trivially done with a single source pixel of margin around the window that you're scaling. If we can currently handle auto-scaling a 150x150 image to 150e6x150e6 (presumably by only scaling a window of the source), we can handle doing the same with an NN step first. |
Here is an idea for doing all of this in one pass. https://demos.skia.org/demo/up_scaling/ The top row shows the suggested one-pass technique: custom shader performing 4 samples per pixel |
Some points by a long-term pixel-perfect integer-scaling enthusiast, a six-year owner of a 4K monitor used at 200%, and the author of the article about integer scaling and of the SmartUpscale browser extension (the latter tries to do exactly what this proposal is about; has ~800+ users on Firefox and 2000+ users on Chrome):
Thanks. |
mikerreed's sharpened bilerp is pretty cool. It's kinda similar to the other nearest-neighbor-but-also-bilinear shader -- a somewhat common requirement, it seems. |
The specification for image-rendering:pixelated reads:
This feature is useful when zooming images such as pixel art to an integer multiple (300%, 400%, etc.). However, when the scale factor is non-integer, it can cause image distortion:
The larger the relative error between the scaling factor and the nearest integer, the worse the distortion. For example, 150% (like above) is really bad, while 1050% would probably be OK. I don't even think the above result successfully preserves a "pixelated" look, at least not in a good way.
Meanwhile, scaling the same image to 150% with a "smooth" algorithm might look like this:
It might be a little blurry, but I think it's overall easier to tell where the pixels are in this rendering of the image, and it is also not distorted.
The bottom line is that "pixelated" should mean that each pixel occupies a square-shaped area on the screen, and we should avoid blending those squares together as much as possible. However, when the boundaries of the squares do not coincide with physical pixels, it is impossible to avoid blending at all, so we should try to do the blending fairly instead of with a "nearest neighbor takes all" algorithm, while limiting the blending to the outermost physical pixels of each square.
One potential way to do this is to first scale the image to an integer multiple with the "nearest neighbor" algorithm, then scale to the target size with a "smooth" algorithm. For example, to scale a pixel art to 250%, first scale it to 300% with the "nearest neighbor" algorithm to get a pixelated look, then rescale that to 250%. I haven't experimented much to see if this approach gives the best results, though.
The text was updated successfully, but these errors were encountered: