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

Proposal: Expose Pointer Position Information on Hovered Elements #6733

Open
bramus opened this issue Oct 15, 2021 · 12 comments
Open

Proposal: Expose Pointer Position Information on Hovered Elements #6733

bramus opened this issue Oct 15, 2021 · 12 comments

Comments

@bramus
Copy link
Contributor

bramus commented Oct 15, 2021

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, Atropos, 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.

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

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/full/porJLgR/250186328bafbb5e63bb1a6f6f2ada044

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 inside the
        , or the
        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.

        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.

        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

    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

    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

    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

  • ?
@Loirooriol
Copy link
Contributor

Starting with -- would break this promise: https://drafts.csswg.org/css-variables-1/#custom-property

Custom properties are solely for use by authors and users; CSS will never give them a meaning beyond what is presented here.

Also, sometimes you use var() and sometimes env(). Environment variables must have the same value everywhere, can't depend on the element.

Then, pseudo-classes are just a way to select elements. They shouldn't which CSS features you can use in these elements.

I don't think there is a clear choice for the coordinates, either. You mention center of the box, but that could even change depending on whether we consider the content area, padding area, border area...

Note that saying that -1 is the left, 0 is the center, and 1 is the right doesn't imply that the range of values is [-1,1]. Because an element can be hovered when the pointer is outside of its border area (in front of an overflowing descendant).

Also, I guess that most usecases would need the sizes of the element. Or want the coordinates with respect to the screen, or the containing block.

Overall, it seems to me that this is better fitted for JS.

@tabatkins
Copy link
Member

Yeah, this isn't a var() value, but that's a small concern; we can just assume it's a different function.

env() is possible, but only for whole-window pointer position. env() values aren't allowed to change based on context.

So this could be done, just as a new pair of functions, for x and y, that each returned a <length>. Since the rest of the CSS uses "0,0 is top-left of box", it would work the same way. They'd probably take a <box> value to determine which box they're measuring relative to. Maybe another value to control whether they return a <length> or a <percentage>, since I can see use-cases for both and you can't easily convert between them.

@bramus
Copy link
Contributor Author

bramus commented Oct 17, 2021

@Loirooriol

Starting with -- would break this promise

Ah yes. Started off with Custom Properties in my explorations, and then worked my way back to something proposal-y which uses Env Vars but forgot to remove the -- prefix in the process.

Also, sometimes you use var() and sometimes env()

In the part where I compare the various coordinate systems I indeed use var() as they were code explorations built with Custom Properties. These snippets are more meant to illustrate the side-effects of the used coordinate system.

Environment variables must have the same value everywhere, can't depend on the element.

Oh, that I did not know. This would indeed make env vars not part of a possible solution here.

pseudo-classes are just a way to select elements. They shouldn't which CSS features you can use in these elements.

I started off from hover and built from there. I'm sure WG has better solutions to this that align with all other existing things :)


@tabatkins

env() values aren't allowed to change based on context

Time for a new function? j/k 🙃

Maybe another value to control whether they return a <length> or a <percentage>, since I can see use-cases for both and you can't easily convert between them.

In my experimentation I found that "percentage expressed as a float" hit the sweet spot:

  • Mutiply by 100% to get a percentage
  • Multiply by qi/qb to get a length

Could of course be that I'm overlooking things here.

@ghost
Copy link

ghost commented Oct 18, 2021

Are TiltJS/Atropos the only usecases you see this working for or do you feel there might be something else also?

@bramus
Copy link
Contributor Author

bramus commented Oct 18, 2021

@mystrdat I've mentioned some use-cases in the OP:

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

Another thing that came to mind is these two-up image comparison things, but then using hover.

@bramus
Copy link
Contributor Author

bramus commented Jul 25, 2023

Since we can nowadays define alternative animation timelines with animation-timeline, a slightly different approach to this issue could be to create some sort of HoverTimeline which authors can use.

@property --pointer-x { … }
@property --pointer-y { … }

@keyframes track-x-value {
  from { --pointer-x: -1; }
  to { --pointer-y: 1; }
}

@keyframes track-y-value {
  from { --pointer-y: -1; }
  to { --pointer-y: 1; }
}

el {
    animation: track-x-value auto linear, track-y-value auto linear;
    animation-timeline: hover(vertical), hover(horizontal); /* 👈 THIS */

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

See https://codepen.io/bramus/full/porJLgR for a demo that could be simplified using this.

/cc @ydaniv who was interested in this type of timeline.

@ydaniv
Copy link
Contributor

ydaniv commented Jul 25, 2023

Thanks, @bramus! I think we may need a new hover-timeline property for this to scope the range to an element, probably with a corresponding -inset property as well.
Only thing that's a bit different here is that the analogous of ScrollTimeline here is hover() timeline that matches the viewport and not an element, or perhaps this could simply be done with:

:root {
  hover-timeline: --hover-x x;
}

@thebabydino
Copy link

There have been a lot of cases where I used JS strictly to get the pointer position & store it in --x, --y variables I then used in the CSS.

First that comes to mind is this entry & exit aware button :hover effect. Or this highlight effect.

I also came across this mouse mask effect, which relies on JS for other things as well, but can be reduced to a version that only needs the relative position of the cursor.

@Link2Twenty
Copy link

Would this offer a CSS only solution for menu safe triangles too?

@lukewarlow
Copy link
Member

I suspect you'd still need JavaScript to run the logic for the safe area?

Fwiw we've been discussing a native safe area mechanism for popovers and interest based triggering (e.g. hover) in OpenUI openui/open-ui#963

@Link2Twenty
Copy link

I was thinking of using an ::after that takes the x and y of the mouse and draws a triangle to the target, with a delay on the triangle updating. Though I imaging triggering paint on the after would be heavy, so if there is a native solution in the works that would be way better (and flee like less of a hack).


As for syntax it almost feels like this should be wrapped in with anchor somehow.

div {
  /* Coordinates that indicate the center of the element */
  anchor-name: --pointer;

  background: transparent radial-gradient(25vw 25vw at calc(((anchor(--pointer 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 * (anchor(--pointer pointer-y)/ 2) + 0.5) * 100%),
      lightblue,
      rebeccapurple) no-repeat 0 0;
  transform: rotate3d(anchor(--pointer pointer-y),
      /* ✅ Values can be used directly */
      anchor(--pointer pointer-x),
      0,
      -15deg);
}

(Which after writing this I see is already discussed in #8639)

@bramus
Copy link
Contributor Author

bramus commented Jan 12, 2024

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

8 participants