[csswg-drafts] Proposal: Expose Pointer Position Information on Hovered Elements (#6733)

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

== Proposal: Expose Pointer Position Information on Hovered Elements ==
# Introduction

To implement effects based on the Pointer's position relative to the hovered element, one needs to resort to JavaScript. See example libraries like [Tilt.js](https://gijsroge.github.io/tilt.js/), [Atropos](https://atroposjs.com/), etc. that provide this functionality.

I would like CSS to have this ability built in: i.e. have CSS use the Pointer's position, without needing to rely on JavaScript nor [a clever but nasty hack that relies on injecting a few 100 extra elements](https://www.bram.us/2021/03/03/mapping-mouse-position-in-css-demo/).

This position information could be used for 3D effects, popover information boxes, Houdini code that takes the mouse position as input, etc.

# Proposed Solution

The proposed solution is two-fold:

1. Provide a way to activate positional tracking
2. Provide a way to use the calculated value in CSS

I came up with these:

1. Introduction of a `:hover-3d` pseudo-class
2. Introducion of some new Environment Variables
    - `--pointer-x`: X-position of the pointer
    - `--pointer-y`: Y-position of the pointer
    - `--pointer-angle`: Angle from the Origin to the Pointer Position
    - `--pointer-distance`: Distance from the Origin to the Pointer Position

## Syntax

```css
el:hover-3d {
    transform: rotate3d(
        env(--pointer-y),
        env(--pointer-x),
        0,
        -15deg
    );
}
```

## Demo

Here's a demo that exposes the 4 proposed env vars as Custom Properties, to see what you can do with them

https://codepen.io/bramus/debug/250186328bafbb5e63bb1a6f6f2ada04

# Considerations / Questions

I've given all of this some thought, which might come in handy in possible future discussions on this.

## Nesting

- Should nesting be allowed or not?
- Either way: I've have the children only gain access to the positional info from nearest parent with positional tracking activated.

## Why a pseudo-class, and why `:hover-3d`?

😬 As the relative Pointer Position can only be calculated while hovering I started off from `:hover` and built further upon that. Seemed like a logical thing to do.

- Advantages of using a pseudo-class other than `:hover`:
    - Rendering Engines should not unnecessarily calc the pointer's position for all elements, but only for those that request it.
    - Rendering Engines need to know which element to calculate against
        - E.g. is it the position of the mouse on top of the <span> inside the <div>, or the <div> itself?
- Disadvantages of using a pseudo-class:
    - You can't use the calculated values in non `:hover-3d` selectors
        - _(Clunky)_ Workaround: use Custom Properties and have the Env Vars overwrite their values.

            ```css
            div {
                --pointer-x: 0;
                --pointer-y: 0;

                transform: rotate3d(
                    var(--pointer-y),
                    var(--pointer-x),
                    0,
                    -15deg
                );
            }

            div:hover-3d {
                --pointer-x: env(--pointer-x);
                --pointer-y: env(--pointer-y);
            }
            ```

    - 🤔 Or would the calculated values also be applied to the targeted element (sans `:hover-3d`) itself?
        - If so: then `:hover-3d` loses some value, as the code could as well go in the regular selector.

            ```css
            div {
                transform: rotate3d(
                    env(--pointer-y, 0),
                    env(--pointer-x, 0),
                    0,
                    -15deg
                );
            }

            div:hover-3d {
                /* 🤔 What would be put here now? Or would we simply need a different way to activate positional tracking? */
            }
            ```

The choice for `:hover-3d` was purely based on the fact that I saw a 3D demo which sparked this idea. Don't have any strong opinion on this name. It can as well be `:hover-with-position-info-yolo-web3-crypto` if that's more in line with how things are named.

## Coordinate System

I played a bit with the Coordinate System that could be used with this. Main question I had here was: Should the origin be at center-center (X/Y Coordinate System), or at top-left (Page Coordinate System)?

The choice of Coordinate System has some side-effects for CSS Authors:

- Many things in CSS use the Page Coordinate System, so using a Page Coordinate System is handy for those type of things
- For 3D-rotations _(my initial intent)_ a X/Y Coordinate system with the Origin at the center-center of the element is more handy as the default `transform-origin` is `50% 50%`

I've compared various possible systems, but am personally leaning to the X/Y Coordinate System _(XYCS)_ + range `[-1,1]` system.

### Page Coordinate System _(PCS)_

- Params
    - Origin `0,0` sits at top-left of the box
        - Side-Effect: center-center is is at `[0.5,0.5]`
    - Range of values for `--pointer-x` and `--pointer-y`: `[0,1]`

- Demo _(using Custom Properties)_: https://codepen.io/bramus/pen/4b04dbf201c6a542d276506503a56e68

    ```css
    div {
        /* Coordinates that indicate the center of the element */
        --pointer-x: 0.5;
        --pointer-y: 0.5;
        
        background: transparent
            radial-gradient(
                25vw 25vw
                at
                    calc(var(--pointer-x) * 100%) /* ✅ Values can be used directly, as bg-position also uses PCS */
                    calc(var(--pointer-y) * 100%)
                ,
                lightblue,
                rebeccapurple
            )
            no-repeat 0 0
        ;
        transform: rotate3d(
            calc((-1 * var(--pointer-y)) + 0.5), /* ❌ Values need to be manually offset by 0.5 due to origins of transform and PCS not aligning */
            calc((1 * var(--pointer-x)) - 0.5),
            0, 
            -15deg
        );
    }

    div::before {
        /* ❌ Polar Coordinates don't make any sense here?! */
    }
    ```

- Advantages:
    - Handy for nested stuff that also use PCS _(positioned elements, backgrounds, etc.)_
- Disadvantages:
    - Less handy for 3D effects, as the used Origin (top-left) differs the default `transform-origin`
        - Values need to be offset by `0.5` before they can be used.
    - Calculating Polar Coordinates from this seems a bit useless here?

### X/Y Coordinate System _(XYCS)_ + range `[-0.5,0.5]`

- Params
    - Origin `0,0` sits at center-center of the box
    - Range of values for `--pointer-x` and `--pointer-y`: `[-0.5,0.5]`

- Demo _(using Custom Properties)_: https://codepen.io/bramus/pen/2711c3083d1c9892ad044dacf9526c26

    ```css
    div {
        /* Coordinates that indicate the center of the element */
        --pointer-x: 0;
        --pointer-y: 0;

        background: transparent
            radial-gradient(
                25vw 25vw
                at
                    calc((var(--pointer-x) + 0.5) * 100%) /* ❌ Values need to be manually offset by 0.5 due to origins of background (which uses PCS) not aligning with XYCS */
                    calc((-1 * (var(--pointer-y)) + 0.5) * 100%)
                ,
                lightblue,
                rebeccapurple
            )
            no-repeat 0 0
        ;
        transform: rotate3d(
            calc(var(--pointer-y) * 2), /* ⚠️ Values need to be multiplied by 2 if you want them to go from "nothing" to "full". Otherwise you'd end up with -7.5deg */
            calc(var(--pointer-x) * 2),
            0,
            -15deg
        );
    }

    div::before {
        content: '';
        pointer-events: none;

        display: block;
        height: 1px;
        width: 50%;
        background: red;
        
        position: absolute;
        top: 50%;
        left: 50%;
        
        transform-origin: 0 50%;
        transform:
            rotate(calc(var(--pointer-angle, 0) * -1deg)) /* ✅ Values can be used directly */
            scaleX(var(--pointer-distance))
        ;
    }
    ```

- Advantage(s):
    - Easy for 3D effects, as the used Origin is equal to the default `transform-origin`
    - `0.5` (or `50%`) resembles the actual distance that was travelled from the origin
        - E.g. A `--pointer-x` value of `0.5` is also _“`50%` of the width”_
            - Handy when used with `cqw` 😎
    - The resulting Polar Coordinates are handy
- Disadvantages:
    - A value of `0.5` is not-handy for scaling purposes: you need to multiply the values by 2.
    - Values needs manual adjustment to be used with Things™ that use the Page Coordinate System
        - E.g. `background-position`, `top`, `left`, …

### X/Y Coordinate System _(XYCS)_ + range `[-1,1]`

- Params
    - Origin `0,0` sits at center-center of the box
    - Range of values for `--pointer-x` and `--pointer-y`: `[-1,1]`

- Demo _(using Custom Properties)_: https://codepen.io/bramus/pen/250186328bafbb5e63bb1a6f6f2ada04

    ```css
    div {
        /* Coordinates that indicate the center of the element */
        --pointer-x: 0;
        --pointer-y: 0;

        background: transparent
            radial-gradient(
                25vw 25vw
                at
                    calc(((var(--pointer-x) / 2) + 0.5) * 100%) /* ❌ Values need to be manually divided by 2 and be offset by 0.5 due to origins of background (which uses PCS) not aligning with XYCS */
                    calc((-1 * (var(--pointer-y) / 2) + 0.5) * 100%)
                ,
                lightblue,
                rebeccapurple
            )
            no-repeat 0 0;
        transform: rotate3d(
            var(--pointer-y), /* ✅ Values can be used directly */
            var(--pointer-x),
            0,
            -15deg
        );
    }

    div::before {
        content: '';
        pointer-events: none;

        display: block;
        height: 1px;
        width: 50%;
        background: red;
        
        position: absolute;
        top: 50%;
        left: 50%;
        
        transform-origin: 0 50%;
        transform:
            rotate(calc(var(--pointer-angle, 0) * -1deg)) /* ✅ Values can be used directly */
            scaleX(var(--pointer-distance))
        ;
    }
    ```

- Advantage(s):
    - Easy for 3D effects, as the used Origin is equal to the default `transform-origin`
    - A max-value of `1` is handy for scaling purposes
    - The resulting Polar Coordinates are handy
- Disadvantages:
    - `1` does not resemble the actual distance that was travelled from the origin
        - E.g. A `--pointer-x` value of `1` is not “`100%` of the width”, but `50%`.
            - Not so handy when used with `cqw` 😎
    - Values needs manual adjustment to be used with Things™ that use the Page Coordinate System
        - E.g. `background-position`, `top`, `left`, …

## Can't we just do this with Houdini?

While the calculations to determine the position can indeed be done via Houdini, there's no way to bounce those calculated values back to the CSS. Custom Properties are Input for Worklets, not Output.

_(Feel free to correct me on this, would love to see that 🤩)_

## Which Pointer?

- First pointer only

## Performance

- Impact on performance needs to be measured
- Also see remark about “Nesting” from earlier

## Privacy

- ?

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


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

Received on Friday, 15 October 2021 14:39:16 UTC