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

[css-values] random() function #2826

Closed
bendc opened this issue Jun 26, 2018 · 79 comments
Closed

[css-values] random() function #2826

bendc opened this issue Jun 26, 2018 · 79 comments

Comments

@bendc
Copy link

bendc commented Jun 26, 2018

Hi,

I see many authors (myself included) rely on JavaScript's Math.random() to generate a number they can then use to randomize colors, positions, animation delays etc. While it's fairly straightforward to do so (notably with custom properties), a potential random function (or keyword) in CSS would make a lot of sense, especially in conjunction with calc() and friends.

AFAICT @tabatkins proposed something similar quite a few years ago, but I don't think we've discussed it recently, and I think we should reevaluate this option now that we have more context and background on how custom properties and mathematical expressions are being used in CSS.

@emilio
Copy link
Collaborator

emilio commented Jun 26, 2018

I don't think random() would make sense. When should it be evaluated? Whenever the style of an element is resolved?

That's right now implementation dependent, there are engines that optimize style changes better than others.

@tabatkins
Copy link
Member

In my previous musings on the topic, I stated that there are two ways we can evaluate random(), both of which are useful:

  1. Evaluated once per occurrence in the stylesheet, at parse time.
  2. Evaluated once per occurrence in the stylesheet per element, at specified-value time.

The first means that .foo:hover { color: rgb(random(0, 255), 0, 0); } evaluates to the same color for every .foo element, and doesn't change if you repeatedly hover/unhover. The second means it'll be different on each .foo element, but will still be consistent if you repeatedly hover/unhover a given element.

There's possibly a third, even less stable mode: evaluated freshly at "application" time, whenever the rule would trigger a transition due to it winning the cascade over a different specified value. This is different per element, and is different each time you hover the element.


We'd also want a few different types of randomness; numeric ranges are okay, but getting a random value from a list is very useful and handles colors well, too.

@bendc
Copy link
Author

bendc commented Jul 4, 2018

@tabatkins I'm definitely in favor of the third option. Would you mind clarifying what you mean by "getting a random value from a list" though?

@octref
Copy link

octref commented Jul 4, 2018

@bendc Probably like random([1, 2, 4]) when each gets 1/3 probability.

I’d say also provide an option to distinguish between int / float random.

@emilio
Copy link
Collaborator

emilio commented Jul 4, 2018

There's possibly a third, even less stable mode: evaluated freshly at "application" time, whenever the rule would trigger a transition due to it winning the cascade over a different specified value. This is different per element, and is different each time you hover the element.

This is not deterministic, right?

@tabatkins
Copy link
Member

What do you mean by that?

@emilio
Copy link
Collaborator

emilio commented Jul 5, 2018

That the time you apply properties as a result of a style change is not defined, in the sense that engines can do wasted work for that in different ways.

For example, if you have .foo div and you have <div><span></span></div>, adding the class foo to the <div> will cause some engines to re-resolve the style of the <span> and not in others, even though the <span>s style didn't change.

@tabatkins
Copy link
Member

That's why I didn't base it on style application, which is indeed undefined, but based it on transition triggering, which is. (The timing is still a little undefined, but assuming you can approach steady state, whether or not a transition occurs is well-defined.)

@emilio
Copy link
Collaborator

emilio commented Jul 5, 2018

What do you mean with 'transition triggering'? You mean only resolve random() iff any other style actually changed? You need to resolve the style to know that, so that'd be slow.

@tabatkins
Copy link
Member

If a random()-containing declaration would newly apply (such that a transition on the property would trigger, if you treated the random()-containing declaration as being a different computed value from anything but itself), then at that point you resolve the random() to a fresh random value and apply it.

@emilio
Copy link
Collaborator

emilio commented Jul 6, 2018

What happens if the OM touches that declaration? Should we re-compute random() to elements that already match it? Such a model sounds complicated to me.

@JamesCoyle
Copy link

I think the most logical option from a usage standpoint would be to calculate the random number when a rule is applied to an element. It should only be recalculated if that rule has been removed and then added again (class addition/removal, hover triggered etc).

That would allow for the random value to persist and update controllably rather than in some undefined fashion whenever the browser feels like it and would mean multiple elements targeted by the same selector would receive a different random value. I'm not sure how easy it would be for a browser to implement though.

@smfr
Copy link
Contributor

smfr commented Jan 17, 2019

It seems like authors might want different things:

  • random value assigned at parse time
  • new random value every time style is updated (whatever that means)

Rather than invent different random functions, I think this functionality is better served with CSS variables, and the ability to set those from script.

@JamesCoyle
Copy link

The issue with that approach is having to generate a custom property for every element you are targeting. At that point you may as well just style the elements directly with JS.

A CSS solution is much more convenient. I believe it would be worth developing some use cases to determine the requirements.

@tabatkins
Copy link
Member

(Note: significant reasoning and discussion here; simple proposal sketch in the next post if you want to just skip to that.)

Yeah, just using script-set variables only solves the "randomly determined at parse time" case. It doesn't help if you want to get a different random value per element, unless you go significantly greater lengths.

Dealing with random value "persistence" is the hard problem to solve here. We need to define precisely what data gets fed into the random generator; or more simply, what data we use to cache the random values, so we can retrieve the same value when we reevaluate style.

That gets us to the rub; how are you caching the values when you have a rule like

.foo { 
 color: rgb(rand-int(0, 255) rand-int(0, 255) rand-int(0, 255)); 
}

The three rand-int calls all need to give distinct values, but need to be persistent across style recalcs. Let's also assume these are meant to be different values on each element, so you can't just do parse-time resolution and call it a day; the functions have to persist until probably computed-value time, at which point they turn into an integer.

Obviously, the element itself is part of the cache key.

What else? Can't just use property; there are three instances and they need to all be distinct. Maybe property + occurrence count (first, second, third)? That still means that different rules would reuse cached values, which would probably be unexpected: a hover rule setting it to rgb(127 rand-int(0, 255) rand-int(0, 255)) would use the same value for Green as the other style did for Red, and the same for Blue that the other did for Green.

Maybe key it off of rule as well, but we don't have a good, stable notion of rule identity. Changes to a stylesheet can cause a reparse and create totally new objects in the CSSOM, so using OM object-identity is probably bad. Dont' want to, like, hash the rule contents either, as it would mean that changing other properties would alter the random value.

Ultimately, I think the only reasonable cache key, that works reliably and with a minimum of surprise, is an author-provided custom identifier. As long as you provide the same identifier, the random() function should resolve to the same value.

One more wrinkle, then: we don't want to expose internal details of the random data, to allow implementations flexibility to change and be different from each other. Thus, we don't want to reuse the same random number for different random functions; the obvious trivial implementation of rand-int() would just modulo a random 32-bit int or something, and using the same value for 1-3 as 1-5 will expose information about the number being used (its value modulo 3 and modulo 5). So we also need to take the range start and end as cache keys.

So: the random values are generated on demand, and cached against:

  • an author-supplied custom ident
  • the start/end of the range
  • the element, if it's meant to vary per-element

This, in addition to being consistent and understandable, has the additional benefit that you can jam a random() into a custom property, and it'll Just Work© without you having to think about it; each use of the var() will get the same value. None of the other possible solutions do this, afaict.


Now, output spaces. I think it's valuable to have both random integers and random numerics (reals, lengths, angles, etc). Just using a random real in a place that expects integers will not work as expected: rand-real(1, 3) will generate a number that rounds to 1 25% of the time, that rounds to 2 50% of the time, and that rounds to 3 25% of the time, a far cry from the 33% you'd like each integer to be generated. You need a floor() function to write a rand-int() out of rand-real(), and it's non obvious how to do so; I write Math.randInt() in JS all the time, and never trust my impl until I run a statistical test on it.

I don't think we need integer-valued dimensions; that's rarely, if ever, actually useful (as opposed to rounding to larger integers, like a random multiple of 100px), and you can just use calc(1px * rand-int(1,10)) to get it if you want. (Or calc(100px * rand-int(3, 5)) for a more realistic case - 300px, 400px, or 500px.)

The real-valued function should accept any numeric value as its start/end, and just require that their types match. (Specifically, that adding the two types is successful, with a null percent hint.)


Lists of values (like, grab a random color from these possibilities) is a valid use-case, but I don't want to solve it directly via a different random function. I think a reasonable use-case is to have sets of values that are valid to use together, but that you want a random choice of. As such, I think we should add an nth(n, val1, val2, ...) function, and then we can just use rand-int() on it if we want.

@tabatkins
Copy link
Member

tabatkins commented Jan 17, 2019

So, proposal:

rand-int(<custom-ident> per-element?, <integer>, <integer>)
rand-val(<custom-ident> per-element?, <numeric>, <numeric>)
nth(<integer>, <value>, <value>, <value>, ...)

Like calc(), the rand-* functions evaluates at computed-value time if possible (if their arguments can be resolved at computed-value time), and at used-value time otherwise. They resolve to either a random integer between the first and second value (inclusive at both ends, so rand-int(foo, 1, 3) will resolve to 1, 2, or 3), or a random real-valued value between the first and second value (inclusive of start, exclusive of end, so rand-val(foo, 100px, 200px) will give a random length >= 100px and < 200px).

For either function, you must supply a <custom-ident> as the first argument. The ident is meaningless, but it ensures that you'll get the same random value from a given function regardless of when it's evaluated - every identical rand-*() invocation, with the same custom-ident, start, and end, will resolve to the same value.

If you additionally pass the per-element keyword, then the random value will be different on every element the property is applied to. (But, again, is guaranteed to be the same on every invocation of an identical rand-*() function on that element.)

rand-int() is an <integer>. rand-val() is the addition of its arguments' types.

The UA maintains a cache of random values, keyed by a tuple of:

  • "int" or "val", depending on function called
  • custom-ident argument
  • fully resolved and absolutized start value
  • fully resolved and absolutized end value
  • the element the property is resolved on, if per-element is specified; null otherwise

Whenever the UA wants to resolve a rand-* function, it grabs a value from the cache if it exists; otherwise generate a fresh random value and put it in the cache. Then it turns that random value into a value in the appropriate range, via whatever means you want. (More than likely, a simple modulo and addition for integers, and a rescale and addition for reals.)

The nth() function takes an integer, and a succession of comma-separated values. It returns the nth value (1-indexed, as usual for CSS). You can use this with rand-int() to get a random value from a list of pre-supplied ones, such as a set of chosen colors.

@Crissov
Copy link
Contributor

Crissov commented Jan 18, 2019

What would happen in case of unclear integers, e. g. rand-int(foo, 10pt, 5mm)?

Could this be solved with counters instead, which are always integers? counter-shuffle: foo, bar 5, baz 1 5; would set foo to a random value between (initial) 0 and its current value, bar to a random value between 0 (or its current value?) and 5, and baz to a random value between 1 and 5. For this to work, of course, counters must be legal inside calc().
random(a, b, id) would require a and b to be the same dimension and yield a random value between them. Rounding functions have already been proposed to deal with finer nuances, e. g. round(random(10pt, 5mm, foo), 1px).

@bendc
Copy link
Author

bendc commented Jan 18, 2019

Thanks a lot for following up on this thread, super excited to see this!

Before I comment on the proposals above, I just wanted to make sure we're philosophically on board with the idea of extending the capabilities of CSS as such. I guess it's hard to find an argument against it since we already have calc() but, adding something like rand() presumably means adding many other things as well (rounding, lists, etc.) and it's undeniable that it's a pretty significant departure from today's CSS. I'm pretty sure, for example, that implementing these functions will result in many requests for a console.log-like feature in CSS.

Is that a problem per se? I'm not sure, but I'd rather make sure before we jump on the implementation details :)

@tabatkins
Copy link
Member

What would happen in case of unclear integers, e. g. rand-int(foo, 10pt, 5mm)?

rand-int() is integers only, not dimensions. But for rand-val(), it's not resolved until computed or used value time, so the start and end points are already fully absolutized, and thus totally clear.

Could this be solved with counters instead, which are always integers?

Counters are... not something we want to build other features on top of. They have a lot of strange quirks.

Before I comment on the proposals above, I just wanted to make sure we're philosophically on board with the idea of extending the capabilities of CSS as such.

I am, but I'm utopian about these things. ^_^ I've been thinking about randomness in CSS for many years. At bare minimum, exploring this space will serve as a great case-study for what we need to be sure that we expose for Houdini Custom Functions; I want to ensure that authors could create a --rand-int() function with this same functionality. So even if we never get this natively implemented, it'll still be useful.

@tabatkins
Copy link
Member

@emilio was concerned about the design implying a global hash of ident=>random state, as it would imply trouble with parallelizing.

This shouldn't be the case; the "random()" function doesn't actually need to invoke a random generator (and, as far as I can tell, can't do so in any reasonable capacity). Instead, it's a hash function + mapping the result into the specified numeric range, relying on the following information only, all of which should be parallelism-friendly afaict:

  • whether you're rand-int() or rand-val()
  • the custom-ident
  • the start and end point (properly canonicalized)
  • if per-element is specified, some unique identifier from the element in question
  • something from the page itself that changes on each page load (so you don't get stable "random" values across refreshes; a timestamp from the document would likely suffice here)

Concerns, @emilio ?

@emilio
Copy link
Collaborator

emilio commented Feb 28, 2019

Not particularly, I guess.

@llebout
Copy link

llebout commented Feb 28, 2019

@bendc Yes and you should keep using Math.random() for this. It is not CSS's role to do compute. Please don't mix things up.

HTML is for markup.
CSS is for markup styling.
JavaScript is for compute.

Each have their reasons to be, if you need compute, use JavaScript.
I'd appreciate if you did not add more bloat to the already very bloated web.

@tomhodgins
Copy link

I've been looking for something like Math.random() in CSS for a few years, and experimenting with different use cases and ways they can be achieved. If I were to set this up today I'd use CSS variables as the handoff between JavaScript and CSS, which also gives you full control over how and when these values are calculated. Here's a demo:

<div>I use random from CSS</div>
<div style="--random-bg: 360;">I use random from DOM</div>
<div class=load>I am randomized once per load event</div>
<div class=click>I am randomized once per click event</div>

<style>
  div {
    --random-bg: 360;
    background-color: hsl(calc(var(--random-bg) * 1deg), 75%, 50%);
  }
  .load {
    --randomized-on-load: 360;
    background-color: hsl(calc(var(--randomized-on-load) * 1deg), 75%, 50%);
  }
  .click {
    --randomized-on-click: 360;
    background-color: hsl(calc(var(--randomized-on-click) * 1deg), 75%, 50%);
  }
</style>

<script type=module>
  import computedVariables from 'https://unpkg.com/computed-variables/index.es.js'

  computedVariables(
    '--random-',
    value => Math.random() * value,
    window,
    ['load']
  )

  computedVariables(
    '--randomized-on-load',
    value => Math.random() * value,
    window,
    ['load']
  )

  computedVariables(
    '--randomized-on-click',
    value => Math.random() * value,
    window,
    ['click']
  )
</script>

You can create a CSS variable like --example: 10; and use the Javascript function value => Math.random() * value to generate a random number between 0 and the supplied value. In this demo I've created a few different background colors and picked random numbers between 0 and 360. The next part is where the control comes in:

  • you can have things calculated once per load
  • you can have things calculated once per event (like every click event)
  • the values cascade nicely in CSS and DOM exactly how you'd expect

It seems like everything people want is possible already! In the past I'd have said that this was a much-needed feature that should be added to CSS for animation and other things, but considering how flexible and powerful it is to use JavaScript's own Math.random() with styles by way of CSS variables I think a lot of that pressure is removed.

It's also easy to create random choice function in JavaScript that can pick randomly from a list of values given in CSS too, like --choose: ['lime', 'hotpink'] and assign the chosen value to the CSS variable for CSS to make use of in styles.

@llebout
Copy link

llebout commented Feb 28, 2019

@tomhodgins Thank you for your comment, I'm not necessarily an experienced web developer and was pointing this out solely from a software design perspective. I am glad it makes sense to web developers like you too.

@ldhasson
Copy link

ldhasson commented Mar 1, 2019

Being able to have control over the randomization is important. @tabatkins raises all the scenarios, and the discussion of using variables from @smfr and @tomhodgins is important, but i feel being independent from scripting is necessary.

Anyways, i did some work on those ideas with AliceJS a while back: http://blackberry.github.io/Alice. Also a sample demo at http://blackberry.github.io/Alice/demos/fx/bounce.html.

@llebout
Copy link

llebout commented Mar 1, 2019

@ldhasson Problem is, if you start with this, you're going to implement another JavaScript inside CSS.
If having separate things is a problem, then, HTML, CSS and JavaScript should not have been separated from the start. It does not make sense to re-invent HTML, CSS and JavaScript in each of them.

@jakearchibald
Copy link
Contributor

Since frameworks don't really give developers full control over referential equality of elements, I imagine the custom ident method will be more reliable.

Does anything prevent var(--whatever) being used to set the custom ident?

@tabatkins
Copy link
Member

Since frameworks don't really give developers full control over referential equality of elements, I imagine the custom ident method will be more reliable.

While I know that React/etc usage is very common, there's plenty of HTML written without it. ^_^

Does anything prevent var(--whatever) being used to set the custom ident?

Nope, as it's just a keyword in the function, you can use vars to substitute it in as normal.

@jakearchibald
Copy link
Contributor

While I know that React/etc usage is very common, there's plenty of HTML written without it. ^_^

Of course, I was just thinking of the kind of guidance we'd give to developers.

@brandonmcconnell
Copy link

@tabatkins Is there a copy of the WIP CSS-Values-5 spec somewhere that I can use to submit it as an entry for Interop 2023?

Based on that conversation, would we also need to include the upgrades to "calc() per values-4" per your comment in the CSSWG IRC log?

Thanks!

@jakearchibald
Copy link
Contributor

Having a per-element value might be generally useful (eg #8320), so I'd encourage using something like element-uuid() instead of something random() specific like per-element.

@brandonmcconnell
Copy link

@jakearchibald @tabatkins That also begs the question whether per-element or element-uuid() (whichever is used) is unique only per DOM element, or if it will also be unique per unique pseudo-elements.

I think the latter would be the most sensible, personally. In that case, if we did implement something like element-uuid(), pseudo-elements would need to have their own element-uuid as well.

@tabatkins
Copy link
Member

Good point, it should def be unique per pseudo as well.

@tabatkins
Copy link
Member

First draft up at https://w3c.github.io/csswg-drafts/css-values-5/#randomness

@smfr
Copy link
Contributor

smfr commented Jan 25, 2023

Instead of random-item(), should we instead introduce into calc() a way to select the nth item of a list?

@brandonmcconnell
Copy link

@smfr I think this spec was discussed previously:

nth(<integer>; <value>; <value>; <value>, ...)

I agree that it would still be worthwhile to introduce a way to select an item from a list, whether via nth(), calc(), or by some other means, though personally, I think it would be beneficial to include support for random-item as it provides a simpler API for selecting a random item from a list using the same options from random, rather than nesting functions like this:

nth(random(per-element, 1, 3, by 1); <value>; <value>; <value>)

The benefits include:

  • simpler API (and similar to random()) for selecting a random item from a list, lower bar of entry
  • you do not need to reference the list's length, which would be required as the max value arg in the nested random() function (you'd have to explicitly set 1 and [max] values each time you wanted to do something like that)

@brandonmcconnell
Copy link

brandonmcconnell commented Jan 25, 2023

@tabatkins One question with either approach (from re my previous comment)— if someone wants to populate some items into their list that are contained in a variable, is there a way to spread them into the random function to distinguish whether the CS variable itself is a value or the items it contains are?

For example, how would this be treated:

* {
  --multiple-items: 1, 2, 3, 4, 5;
  --single-item: 6;
  some-prop: random-item(var(--multiple-items), var(--multiple-items));
}

Would this return either (1, 2, 3, 4, 5) or (6), or any of those 6 values: 1, 2, 3, 4, 5, or 6?

@tabatkins
Copy link
Member

Instead of random-item(), should we instead introduce into calc() a way to select the nth item of a list?

As we discussed at the f2f when this was brought up, doing it like nth-item(random(...); items....) is essentially identical complexity (nth-item() is still gonna have to be var-like in the same way), but it also requires you to write in the length of the list, which limits the ability to do more dynamic lists. Aka it's equivalent in power but more annoying.

(We still want to do nth-item() as well, but having the random-item() convenience seemed worthwhile.)

[Brandon's question]

Assuming you meant random-item(--x; var(--multiple-items); var(--single-item)), then it'll resolve to either 1, 2, 3, 4, 5 or 6, with a 50% chance of each.

The items have to be semicolon-separated, so with the way things are specified right now it's actually not possible to put several items into a single variable. (Both custom properties and the fallback of var() use <declaration-value>, which excludes semicolons at the top level.)

@Crissov
Copy link
Contributor

Crissov commented Jan 26, 2023

Instead of separate random-item() and nth-item() functions, I think I would prefer a single function choose(), select() or index() with parameters (<index> [to <index>|<index>#]? from <list>) with something like <index> = [<int>|random|first|last] and <list> = <any-value> [';' <any-value>]*, so you could also get sub-lists.

People would then probably also want sort() and shuffle() for lists/sets, but that shall be another issue.

@zcorpan
Copy link
Member

zcorpan commented Jun 12, 2023

What are the use cases for CSS-only random numbers?

@jakearchibald
Copy link
Contributor

Here's an exploration I did of "random" effects with the paint API. https://jakearchibald.com/2020/css-paint-predictably-random/

@kizu
Copy link
Member

kizu commented Jun 16, 2023

Common use-cases for [pseudo-]randomness in CSS is for creating a visual variance between elements. Right now the most common way to achieve this is through “cicada principle” technique and adjacent methods (original article, article by @LeaVerou), but it can be very tricky to set up.

Actual usage from the top of my head (encountered in the last few months and remembered about them):

@ShayDavidson
Copy link

ShayDavidson commented Jun 19, 2023

Been using a RNG with a seed property quite a lot in my paint worklets experiments and they work rather well.

However I don't think the use-cases for paint worklets are the same as for managing random effects across "collection" of items (like others have suggested here).

The way I mostly imagine using random is for lerp functions (which surprisingly I didn't find in this discussion, e.g.:

width: min(0, max(100, calc(random(--seed) * 100));

or even a color lerp

background-color: color-lerp(#fff, #000, random,()) // yes - this requires a new CSS function 

While --seed can be an <integer> type, some of the suggestion here to generate seed by n-th item for example should work as well. Something like:

width: min(0, max(100, calc(random(nth-item) * 100));

@brandonmcconnell
Copy link

@ShayDavidson I've specifically been considering the use case of a seed PLUS an index/nth value for unique values per sibling as well.

The per-element keyword might cover that use case as well, as far as ensuring that each sibling's value is unique, but to make the seed values reliable and reproducible, something like this would be useful:

#some-id :nth-child(odd) {
  --ident: [static-text] sibling-index();
  --random-value: random(--ident, 0px, 100px, by 1);
  width: var(--random-value);
}

Here, we are building the custom ident using any arbitrary ident-like string as well as another type such as the sibling-index() which would be a number.

The addition of sibling-index() is already being discussed in #4559. I'm simply pointing out how it could be useful in building unique sibling-index-based idents for use in random().

Hypothetically, you can use as many parts inside one of these idents as you like, similar to how in some keyed blocks in JS, you can build the key like this:

(`${category}-${type}-${focus}`)

@chriscoyier
Copy link

What are the use cases for CSS-only random numbers?

Check out the "Blinking Switchboard" part of this post about the recent Next.js website: https://rauno.me/craft/nextjs

Seems to me randomization of which dots animate and how much and how long would make for a super cool effect in the realm of what CSS should be able to do.

@fantasai
Copy link
Collaborator

This was resolved by the CSS Working Group at the New York City F2F in 2022 and has now been edited in and published in the css-values-5 FPWD.

Please file any follow-up considerations as new issues. :) Closing this one out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Wednesday
Development

No branches or pull requests