[csswg-drafts] [css-values-4][cssom] Proposal to allow retrieval of sub-pixel border values (#10729)

jsnowranger has just created a new issue for https://github.com/w3c/csswg-drafts:

== [css-values-4][cssom] Proposal to allow retrieval of sub-pixel border values ==
# Background

Some properties support sub-pixels. [WebKit.org](https://www.webkit.org) has a breakdown in the [LayoutUnit - Use cases](https://trac.webkit.org/wiki/LayoutUnit#Usecases) section of their site.

Other properties, like `border-width`, don't support sub-pixels. [John Resig - Sub-Pixel Problems in CSS](https://johnresig.com/blog/sub-pixel-problems-in-css/) is a short read that summarizes the issue well.

In [Distance Units: the `<length>` type](https://drafts.csswg.org/css-values-4/#lengths), the specification describes the method of rounding computed values such as `border-width` that don't use sub-pixels:

> To snap a length as a border width given a [`<length>`](https://drafts.csswg.org/css-values-4/#length-value) `len`:
> 1. Assert: `len` is non-negative.
> 2. If `len` is an integer number of [device pixels](https://drafts.csswg.org/css-values-4/#device-pixel), do nothing.
> 3. If `len` is greater than zero, but less than 1 [device pixel](https://drafts.csswg.org/css-values-4/#device-pixel), round `len` up to 1 device pixel.
> 4. If `len` is greater than 1 [device pixel](https://drafts.csswg.org/css-values-4/#device-pixel), round it down to the nearest integer number of device pixels.

This is the intended and currently-implemented behavior, and I'm not proposing that it changes for elements rendered on-screen.

# Proposal

Implement an optional, opt-in feature to programmatically retrieve sub-pixel values when using functions such as [`getComputedStyle`](https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle).

```javascript
// The style is calculated using sub-pixels for all properties. Does not change the rendering behavior.
const computedStyle = getComputedStyle(element, options: { useSubPixels: true })
```

This would not change the behavior of how elements are rendered. Properties like `border-width` that don't render with sub-pixels would still not render with sub-pixels.

This would simply allow developers to retrieve the sub-pixel values of properties such as `border-width` before they are rounded. The browsers have access to this information, and it would be helpful if developers were able to programmatically access it too.

`useSubPixels` could apply to all properties, or there could be more granularity. I'm most interested in it applying to `border-width`.

# Reproducible Example

The contents of these two files are included below:

1. `puppeteer-script.js`
2. `package.json`

The script uses [Puppeteer](https://pptr.dev/), a library created and maintained by the Chrome DevTools team, to demonstrate how `border-width` values change due to sub-pixel rounding.

I've also included detailed instructions on how to set up and run this script, in case anyone is reading this but isn't familiar with all the tools being used. If you're familiar with everything, you could skip reading these steps and go straight to running the script. The instructions are for Windows, but the process should be similar on other operators systems.

### Steps to Run the Script

1. Create a folder anywhere on your computer, and save the attached `puppeteer-script.js` in that folder.

4. Install the latest stable version of Node.js by using the prebuilt installer found [here](https://nodejs.org/en/download/prebuilt-installer/). No changes beyond the default installation are necessary.

5. Open the command line interface by typing `cmd` or `Command Prompt` in your Windows Start menu/search field.

6. In the command line interface, verify that Node has been globally installed by typing `node -v` and pressing enter. Then, type and enter `npm -v` ([npm](https://www.npmjs.com/) is the Node Package Manager). Both commands should display the version number of the installed programs.

7. Once installation is confirmed, in the command line interface, change directories so you're in the folder created in step 1. For example, if you created the folder on your Desktop and named it Test, you could type `cd Desktop/Test` to navigate there.

8. Once you've navigated to the folder where `puppeteer-script.js` is saved, type and enter the command `npm install puppeteer` to install the latest version of Puppeteer. Once the installation is complete, the folder should have a few new items: a `node_modules` folder, a `package.json` file, and a `package-lock.json` file.

9. Update your `package.json` file to look like the version I've included here by adding `"type": "module",` to the JSON object. This will make it so you can call `import puppeteer from 'puppeteer'` in the script.

10. In the command line interface (still within the folder you created in step 1), type `node puppeteer-script.js` to run the script.

The command line interface should display the following:

```
border-left-width would have been 1800px * 0.0025 = 4.50px if sub-pixels were enabled
border-left-width is actually 4px since it has been rounded down
The computed width is 400.5px and the rect width is 400.5px
```

### Explanation of the Script

The script created a page with a width of `1800px`, and an element with `border-left-width: 0.25vw`, which means `0.25% of the screen's width`. The sub-pixel value of `border-left-width` is `1800px * 0.0025 = 4.50px`. However, per the specification, this value was rounded down to `4px`, as expected.

For a bit of added information, the script also shows that the element's width based on the results of `getComputedStyle` and `getBoundingClientRect` uses sub-pixels. This was added to show that it would be consistent with the behavior of other properties if some properties, like `border-width`, could also return sub-pixels on the developer side. `border-width` contributes to the width of the element, and while its possible to use calculate the sub-pixel value based on information like this, it would be much more straight-forward and consistent to be able to retrieve it directly.

### Script Code

`puppeteer-script.js`:

```javascript
'use strict'

import puppeteer from "puppeteer"

(async function main()
{
    try
    {
        const html = `<div id = "something"></div>`
        const css = `#something
        {
            box-sizing: border-box;
            width: 22.25vw;
            border-top: 96px solid blue;
            border-left: 0.25vw solid red;
            position: absolute;
        }`

        const browser = await puppeteer.launch({ headless: true })
        const page = await browser.newPage()

        await page.setViewport({ width: 1800, height: 2400 })
        await page.setContent(html)
        await page.addStyleTag({ content: css })

        const results = await page.evaluate(() =>
        {
            const element = document.getElementById('something')
            const computedStyle = getComputedStyle(element)
            const borderLeftWidth = computedStyle.getPropertyValue('border-left-width')
            const computedWidth = computedStyle.getPropertyValue('width')
            const rectWidth = element.getBoundingClientRect().width
            
            return {
                borderLeftWidth: borderLeftWidth,
                computedWidth: computedWidth,
                rectWidth: rectWidth
            }
        })

        const { borderLeftWidth, computedWidth, rectWidth } = results

        console.log(`\nborder-left-width would have been 1800px * 0.0025 = 4.50px if sub-pixels were enabled`)
        console.log(`\nborder-left-width is actually ${borderLeftWidth} since it has been rounded down`)
        console.log(`\nThe computed width is ${computedWidth} and the rect width is ${rectWidth}px`)

        await page.close()
        await browser.close()
    }
    catch (error)
    {
        console.error(error)
    }
})()
```

`package.json`:

```javascript
{
  "type": "module",
  "dependencies":
  {
    "puppeteer": "^22.15.0"
  }
}
```

# Example of Proposed Feature

If this feature were added, the element created in the script above would still render with a `border-left-width` of `4px`, but I would be able to call the following:

```javascript
// The style is calculated using sub-pixels for all properties. Does not change the rendering behavior.
const computedStyle = getComputedStyle(element, options: { useSubPixels: true })
const borderLeftWidth = computedStyle.getPropertyValue('border-left-width')
 ```

And receive a value of `4.50px` for `border-left-width` - the sub-pixel value before rounding. The rendered `border-left-width` is still `4px`, but the `useSubPixels` parameter allows me to compute the value as if it hadn't been rounded. Since the browser already has this information, it will hopefully not be difficult to implement.

Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/10729 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Monday, 12 August 2024 17:02:05 UTC