dssl / native /spec /08-effects.md
EHS-Studio's picture
Claude Sonnet 5.5
v0.16.3: native DSSL spec, and the seven bugs writing it found
9da4a9b
|
Raw History Blame Contribute Delete
15.9 kB
# 8. Effects
Effects are the parts of DSSL that CSS has no equivalent for, or only a clumsy one. Each effect
has two halves:
- a **description**: the text the author writes and the record the resolver produces from it.
This half is fully specified here and a Level 2 implementation MUST produce the same records.
- a **rendering contract**: what a Level 4 renderer must draw. Where the contract depends on
generator code that is not yet written down as prose, this chapter says so. Those parts are
*defined by the reference implementation* (`public/dssl/texture.js`, `aura.js`, `slider.js`)
until a prose definition replaces them. They are marked **Pending**.
All vocabulary tables (setting names, ranges, presets) are generated into appendix A.
## 8.1 Texture
`texture:` lays a generated image on top of an element's own background.
```dssl
.card { background: #c9a66b; texture: wood }
.sheet { background: linear-gradient(135deg, #ff7a00, #7a1fff); texture: grain intensity 0.6 }
.poster { background: #e9e2d0; texture: paper, scanlines intensity 0.1 }
```
**Description.**
- **FX-1.** The value is `none`, or a comma-separated list of layers. `none` writes
`@texture: null`. An empty value is the error `texture needs a name`. Any error anywhere in
the value drops the whole declaration.
- **FX-2.** Each layer starts with a preset name or a generator name (appendix A). Otherwise
the error is `unknown texture "<w>"`, with a did-you-mean over presets and generators.
- **FX-3.** After the name come settings. A setting is a key from the texture settings table
(appendix A), followed by as many values as that key takes. A key has one *kind*:
`flag` (no value), `number`, `numbers` (several), `word` (one of a list) or `color`.
- A number outside the key's range is the error `texture "<key>" must be between <min> and
<max>`. A non-number is `texture setting "<key>" needs a number, got "<v>"`.
- A word not in the list is `unknown <key> "<w>"` with a did-you-mean.
- A missing value is `texture setting "<key>" needs a value` (or `N values`). A value that is
itself a setting key counts as missing.
- A color value is resolved by chapter 3 (tokens, alpha and all). A bad color drops the
declaration.
- An unknown key is `unknown texture setting "<key>"` with a did-you-mean.
- A key marked *optional* (`symmetrical`) may stand alone, and then takes its first listed
value.
- **FX-4.** A preset is a stored list of layers. Writing a preset name gives those layers.
Settings apply to every layer of the preset, except that `color` and `color2` apply only to
the first layer when the preset has several. A generator name gives one layer made of the
generator's defaults.
- **FX-5.** The output is `@texture` with the list of layer records, each being the preset or
generator layer with the author's overrides applied, and a `preset` field naming the preset
(null for a bare generator).
**Rendering contract.**
- **FX-6.** The texture is drawn above the element's own background and below its content. The
author's background (a color, a gradient, or several layers, written as `background` or
`background-image`) is never replaced, and removing the texture gives back exactly the
original.
- **FX-7.** A texture is deterministic. The same layer record, including `seed`, MUST give the
same pixels on every machine and every engine. The tile MUST repeat without a visible seam.
(Why: the reference implementation draws from `seed` and plain arithmetic only.)
- **FX-8.** `symmetrical` mirrors every other tile on the chosen axis (`x`, `y`, or both), so
the pattern repeats twice as far apart and each tile edge meets its own reflection. It applies
to textures made as images (noise, bump, rings, waves, hex) and is ignored by the pure
pattern generators (lines, dots, grid, checker, carbon).
- **FX-9.** `animate N` steps the texture every `N` ms. It stands still when the user prefers
reduced motion.
- **FX-10.** Colors given as tokens follow the token, including under a theme change.
**Pending.** The generators (`noise`, `bump`, `lines`, `dots`, `grid`, `checker`, `weave`,
`carbon`, `waves`, `hex`, `rings`) and the seeded random number generator are defined by
`texture.js`. A prose definition of each is the first item of the Level 4 work.
## 8.2 Aura
An aura is light and particles around an element. There are no presets. Fire, snow and sparks
are written by the author, usually as a `behavior`.
**Description.**
- **FX-11.** The properties are `aura`, `aura-glow`, `aura-particle` (and `aura-particles`),
`glow-particle` (and `-particles`), `aura-glow-particle` (and `-particles`). They write two
slots, `@aura-glow` and `@aura-particles`. A property that does not touch a slot leaves it
out of its output.
| Property | Writes |
|---|---|
| `aura` | both slots, as `glow ..., particles ...` clauses; it replaces both |
| `aura-glow` | the glow slot |
| `aura-particle(s)` | the particles slot |
| `glow-particle(s)` | the particles slot, with every particle glowing |
| `aura-glow-particle(s)` | both slots, with shared settings; both a glow and particles are required |
- **FX-12.** `none` clears exactly the slots the property owns (`aura-glow: none` leaves the
particles alone). An empty value is the error `<prop> needs a value`. An empty list writes
`null` for the slot.
- **FX-13.** A value is a comma-separated list of layers. In `aura`, each layer starts with
`glow`, `particles` or `particle`, otherwise the error
`an aura layer starts with glow or particles, not "<w>"`.
- **FX-14.** Inside a layer, the first color (or `->`-joined color list) and the first number
are positional: for a glow they are the color and the radius, for particles they are the
colors and the count. Everything else is a named setting from the glow or particle table
(appendix A). A leftover or unknown word is an error, with a did-you-mean.
- **FX-15.** The output is `@aura-glow` and/or `@aura-particles`, each a list of layer records
or `null`.
**Rendering contract.** (From the reference implementation. All of it is **Pending** as prose.)
- **FX-16.** The glow is drawn only outside the element's box. `behind` does the same for
particles.
- **FX-17.** An element that is off screen, hidden, clipped away by an overflow container, or
removed emits nothing. When no aura is active, the renderer stops its loop.
- **FX-18.** At most 1800 particles are alive across the page. When requests exceed that, every
emitter gets a fair share.
- **FX-19.** Under reduced motion, glows stand still and particles are not emitted, unless the
layer says `essential`.
- **FX-20.** A state block can change the aura. Leaving the state lets live particles finish,
then the emitter goes.
- **FX-21.** `from thumb` makes particles leave a range slider's thumb at the current value.
## 8.3 Slider
`slider:` styles `<input type="range">` from one description. Without it, every engine needs
its own vendor pseudo-elements.
```dssl
input.volume {
slider: track 6 color.line radius full, fill color.accent,
thumb 24 color.paper, ring 3 color.accent, glow color.accent 14,
ticks 10 color.muted
}
```
**Description.**
- **FX-22.** The value is a comma-separated list of clauses. A clause starts with one of
`track`, `fill`, `thumb`, `shape`, `ring`, `glow`, `ticks`, `graph`, `graph-height`,
`graph-gap`. An unknown head is `unknown slider setting "<w>"` with a did-you-mean. An empty
value is `slider needs a description`.
- **FX-23.** Every word after a clause's head MUST be used by that clause. A leftover is the
error `unexpected "<words>" in the <head> setting`, with a hint about the missing comma.
- **FX-24.** The default record is: track height 6, color `var(--dssl-color-line)`, radius
`full`; fill `var(--dssl-color-accent)`; thumb size 22, color `var(--dssl-color-accent)`;
shape `circle`; no ring, no glow, no ticks, no graph; graph height 40, graph gap 2. Clauses
change the record in order.
- **FX-25.** Clause forms and limits are in appendix A. The notable ones: `ticks` needs a count
of at least 2, `graph` needs 2 to 120 numbers each in 0 to 100, `graph-height` is at least 8,
`graph-gap` is not negative.
- **FX-26.** The output is `@slider` with the record. When the rule is emitted (5.4), the
resolver turns the record into CSS: base declarations on the element, put *first* in the rule
so the author's own declarations in the same rule win, plus one shadow rule per vendor
pseudo-element. That CSS is defined by `sliderCss`. **Pending.**
**Rendering contract.**
- **FX-27.** The fill and the graph bars follow the value, which CSS cannot know. The renderer
keeps `--dssl-p` (the value as 0 to 1) up to date on the input, for typing, dragging, arrow
keys and programmatic changes.
- **FX-28.** A native engine that styles the slider parts itself does not need the vendor
pseudo-elements. It MUST give the same appearance as the record describes.
## 8.4 Glass and tilt
```dssl
.pane {
glass: 18 saturate 1.6
tilt: 10 lift 1.04 glare
}
```
**Glass (plain CSS).**
- **FX-29.** `glass: words` or `none`. `none` writes `backdrop-filter: none`. Otherwise the
words are: a number (blur in px, default 16, not negative), `saturate N` (default 1.5),
`bright N`, `tint <color>` or `tint none`, and a bare color word counts as a tint. A word
that is none of these is `unknown glass setting "<w>"`. A missing number is
`<word> needs a number`. A color is recognised as a bare tint when it starts with `#`, `rgb`,
`hsl`, `oklch`, `oklab` or `color`, contains `/`, looks like `name.key`, or is `white`,
`black` or `transparent`.
- **FX-30.** The output is `backdrop-filter: blur(Bpx) saturate(S)` followed by
` brightness(R)` when given. Then `background-color: <tint>` unless the tint is `none`, where
the default tint is `color-mix(in srgb, currentColor 8%, transparent)`. The author's own
`background` written after `glass:` replaces the wash.
**Tilt (needs the pointer).**
- **FX-31.** `tilt: words` or `none`. The words are a number (max degrees, 0 to 45, default 8),
`lift N` (scale on hover, above 0), `perspective N` (px, at least 100, default 900),
`speed N` (0.02 to 1, default 0.18), `invert` and `glare`. The record is
`{ max, lift, perspective, speed, invert, glare }`. `none` writes `@tilt: null`. An empty value
is `tilt needs a number of degrees`. Out-of-range values are errors that name the range.
- **FX-32.** The renderer adds, after the element's own `transform`:
`perspective(<P>px) rotateX(var(--dssl-tx, 0deg)) rotateY(var(--dssl-ty, 0deg))
scale(var(--dssl-ts, 1))`, and updates the three variables from the pointer, so that moving
the pointer never triggers a restyle. With `glare`, it also keeps `--dssl-gx` and
`--dssl-gy` (0% to 100%) pointing at the pointer.
- **FX-33.** Tilt ignores touch, stays flat under reduced motion, and stops when the page is
frozen. It combines with `translate`, `scale` and `rotate`. It does not combine with `bend`
or `fold`, because both write `transform`.
## 8.5 Deform
`bend`, `bend-y`, `bend-up`, `fold`, `stretch` and `bend-around` deform an element, or for
`bend-around` its children.
- **FX-34.** `bend`, `bend-y`, `bend-up`, `fold` take a bare number of degrees (or `Ndeg`).
Anything else is `<name> takes a bare number of degrees`. They write `@bend`, `@bend-y`,
`@bend-up`, `@fold` with the number.
- **FX-35.** `stretch: F [horizontal|vertical|x|y]` with `F` above 0. It writes `@stretch`
`{ factor, axis }`, axis default `horizontal`. A bad value is
`stretch needs a positive factor, ...`; `F <= 0` is `stretch: F would flatten the element to
nothing`.
- **FX-36.** `bend-around: D` is a bare number of degrees between -170 and 170, otherwise
`bend-around caps at ±170° ...`. It writes `@bend-around`.
- **FX-37.** *Composition.* The deform parts of an element combine into one transform string,
in this order, followed by the element's own transform (when it is not `none`). `d` is the
record of numbers, `h` the element height in px (0 when there is no box yet):
1. if any of bend, bend-y, fold, bend-up is non-zero: `perspective(900px)`;
2. if bend: `rotateX(<bend>deg)`; if bend-y: `rotateY(<bend-y>deg)`;
3. if fold: with `h > 0`: `translateY(<-h/2>px) rotateX(<fold>deg) translateY(<h/2>px)`;
without a box: `rotateX(<fold>deg)` and the origin `50% 0%`;
4. if bend-up: with `h > 0`: `translateY(<h/2>px) rotateX(<up>deg) translateY(<-h/2>px)`;
without a box: `rotateX(<up>deg)` and the origin `50% 100%`;
5. if stretch is set and not 1: for the horizontal axis
`scaleX(<f>) scaleY(<1/f rounded to 4 places>)`, for vertical
`scaleY(<f>) scaleX(<1/f>)`.
Translations are rounded to 2 decimal places. The result is `none` when there are no parts.
In keyframes (7.1) there is no box, so the second form of fold and bend-up is used.
- **FX-38.** *Arc layout.* `bend-around: D` lays `n` children along an arc. It does nothing
when `n < 2`, the container has no width, or `|D|` is too small (`sin(|D|/2) < 0.0001`).
Otherwise let `half = |D|/2` in radians, `R = round2((width/2) / sin(half))`, and the angle of
child `i` (from 0) is `round2(-D/2 + D*i/(n-1))` degrees. `round2(v)` is
`round(v * 100) / 100`. A positive `D` makes a dome, negative a smile.
## 8.6 Overlap
- **FX-39.** `overlap: allow|none|deny|auto`. It writes `@overlap` with `allow` (for `none` too),
`deny` or `auto`. Anything else is `unknown overlap policy "<v>"`. The value inherits down the
tree.
- **FX-40.** `deny`: the element yields out of any overlap with other elements. `auto`: it
yields only while its opacity is below 100%. `allow` is the default. How an element yields is
defined by the reference engine's `overlapNudge`. **Pending.**
## 8.7 Fonts
`fonts { }` loads fonts. A font is a stylesheet link or an `@font-face`, and neither belongs
in a rule, so it has its own block.
```dssl
fonts {
Fraunces: 500 700 800
"Hanken Grotesk": 400 700 italic
Inter: 100..900 display optional
Brand: from "/fonts/brand.woff2" 400
}
```
- **FX-41.** The body is verbatim text (2.7). Block comments are blanked, and a `//` comment
that starts a line or follows white space is removed. Each remaining line has a trailing `;`
removed and is `Family: words`. The family is everything before the first colon outside
quotes, unquoted, with runs of white space collapsed to one space. A line with no colon is
`expected "Family: weights" in "<text>"`.
- **FX-42.** The words are: weights (`400`, or a range `100..900`, each from 1 to 1000, a range
rising), `italic`, `display <swap|block|fallback|optional|auto>`, `from "<file>"`. Anything
else is `unknown font setting "<w>"`.
- **FX-43.** Without `from`, the font is from Google Fonts. All Google families together make
one link:
`https://fonts.googleapis.com/css2?family=<parts>&display=<d>` where each part is
`Name:wght@400;700` (sorted by number, spaces in the name as `+`), or `Name:ital,wght@0,400;
0,700;1,400;1,700` with italics (all weights, upright first), or `Name` alone for no weights,
or `Name:ital@0;1` for italics with no weights. The `display` is the last one given on any
Google line, default `swap`. The link records the line of the `fonts` block.
- **FX-44.** With `from`, the line makes an `@font-face` block (as a `raw` entry, so it goes into
the page stylesheet) with `font-family`, `src: url("<file>")` plus `format("woff2"|"woff"|
"truetype"|"opentype")` by extension, `font-weight` (a range written with a space),
`font-style` and `font-display` (default `swap`). A `from` line takes at most one weight.
- **FX-45.** Google's servers see every visitor who loads one of their fonts. Authors who need
to avoid that use `from`.