w3c / csswg-drafts

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

Proposal: Expose Pointer Position Information on Hovered Elements #6733

Open bramus opened 2 years ago

bramus commented 2 years ago

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

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.

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:

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)

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

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

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?

Performance

Privacy

Loirooriol commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

@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:

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

ghost commented 2 years ago

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

bramus commented 2 years ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 11 months ago

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 commented 10 months ago

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

lukewarlow commented 10 months ago

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 https://github.com/openui/open-ui/issues/963

Link2Twenty commented 10 months ago

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 commented 9 months ago

Some more recent demos where authors use the pointer position: