Commit ·
08132d7
1
Parent(s): 794f4bd
v0.11.0: real wood textures, textures baked into dssl build, CSS counters, calmer contrast lint, and the Heartwood showcase page
Browse files- GUIDE.md +60 -5
- PARITY.md +20 -1
- README.md +3 -3
- cli/dssl.mjs +2 -0
- dssl-vscode/CHANGELOG.md +7 -0
- dssl-vscode/README.md +2 -2
- dssl-vscode/lib/engine/lint.js +17 -6
- dssl-vscode/lib/engine/resolver.js +6 -4
- dssl-vscode/lib/engine/texture.js +208 -2
- dssl-vscode/package.json +1 -1
- llms.txt +10 -2
- next.config.ts +4 -0
- package.json +1 -1
- public/dssl/engine.js +2 -41
- public/dssl/index.js +3 -1
- public/dssl/lint.js +17 -6
- public/dssl/resolver.js +6 -4
- public/dssl/static.js +42 -3
- public/dssl/texture.js +208 -2
- public/llms.txt +10 -2
- public/showcase/wood/index.html +178 -0
- public/showcase/wood/live.dssl +34 -0
- public/showcase/wood/static.dssl +458 -0
- public/showcase/wood/tokens.dssl +25 -0
- public/showcase/wood/wood.css +0 -0
- public/styles/docs.dssl +10 -0
- public/styles/home.dssl +9 -0
- scripts/build-wood.mjs +24 -0
- scripts/lib/browser-harness.mjs +1 -1
- scripts/test-all.mjs +1 -1
- scripts/test-v0100.mjs +124 -0
- scripts/test-v090.mjs +5 -5
- src/components/dssl/views/ChangelogView.tsx +13 -0
- src/components/dssl/views/CheatSheetView.tsx +1 -1
- src/components/dssl/views/DocsView.tsx +4 -1
- src/components/dssl/views/HomeView.tsx +7 -2
- src/lib/vocab.json +67 -1
GUIDE.md
CHANGED
|
@@ -281,7 +281,7 @@ replaced, and taking the texture away puts it back exactly as it was.
|
|
| 281 |
.poster { background: #e9e2d0; texture: paper, scanlines intensity 0.1 } // a comma stacks layers
|
| 282 |
```
|
| 283 |
|
| 284 |
-
There are
|
| 285 |
generators (`noise`, `bump`, `lines`, `dots`, `grid`, `checker`, `weave`, `carbon`, `waves`, `hex`) and the settings below, so anything a preset does you
|
| 286 |
can write yourself.
|
| 287 |
|
|
@@ -362,9 +362,64 @@ A few things worth knowing:
|
|
| 362 |
- `animate 90` makes film-grain shimmer by stepping the texture every 90 ms. It stands still
|
| 363 |
for visitors who ask for reduced motion, and `DSSL.freeze()` stops it.
|
| 364 |
- Textures are SVG filters and gradients, drawn once and cached. A page full of them is cheap.
|
| 365 |
-
- `texture` is
|
| 366 |
- Inside an aura, `texture(grain)` makes the light and the particles grainy. See below.
|
| 367 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
### Aura
|
| 369 |
|
| 370 |
An aura is light and particles around an element. **There are no presets.** Fire, an
|
|
@@ -627,14 +682,14 @@ Three ways to get it:
|
|
| 627 |
|
| 628 |
1. VSCodium: open the Extensions tab, search "DSSL" and install. It is on Open VSX:
|
| 629 |
https://open-vsx.org/extension/ehs-studio/dssl-vscode
|
| 630 |
-
2. VS Code: download `dssl-vscode/dssl-vscode-0.
|
| 631 |
Extensions panel, `...`, Install from VSIX. (The Microsoft Marketplace listing is not published yet.)
|
| 632 |
3. Build it yourself: `cd dssl-vscode && npx @vscode/vsce package --no-dependencies`.
|
| 633 |
|
| 634 |
What it does: real-compiler diagnostics (same errors the browser prints,
|
| 635 |
did-you-mean included), completions folded from your workspace token registry,
|
| 636 |
hover docs, and a TextMate grammar for `.dssl`. Since 0.9.0 it also completes and
|
| 637 |
-
explains the new vocabulary: after `texture:` it offers the
|
| 638 |
setting, after `aura-glow:` only glow settings, after `aura-particle:` only particle
|
| 639 |
ones, after `slider:` the clauses.
|
| 640 |
|
|
@@ -647,7 +702,7 @@ from the command palette (or the paint-can button on a `.dssl` tab):
|
|
| 647 |
and remove, a live preview drawn by the same renderer the engine uses (on dark, light or slate),
|
| 648 |
a graph of one particle's opacity and size over its life, and a live graph of particles alive and
|
| 649 |
frame time.
|
| 650 |
-
- **Texture tab.** All
|
| 651 |
setting as a slider, colour pickers, a preview over the colour you choose.
|
| 652 |
- **Slider tab.** A live range input styled with the generated rules, with a bar editor for histogram sliders.
|
| 653 |
|
|
|
|
| 281 |
.poster { background: #e9e2d0; texture: paper, scanlines intensity 0.1 } // a comma stacks layers
|
| 282 |
```
|
| 283 |
|
| 284 |
+
There are 40 presets: 30 general ones and ten wood species (see "Wood" below). They are not magic: each one is a stored combination of a few
|
| 285 |
generators (`noise`, `bump`, `lines`, `dots`, `grid`, `checker`, `weave`, `carbon`, `waves`, `hex`) and the settings below, so anything a preset does you
|
| 286 |
can write yourself.
|
| 287 |
|
|
|
|
| 362 |
- `animate 90` makes film-grain shimmer by stepping the texture every 90 ms. It stands still
|
| 363 |
for visitors who ask for reduced motion, and `DSSL.freeze()` stops it.
|
| 364 |
- Textures are SVG filters and gradients, drawn once and cached. A page full of them is cheap.
|
| 365 |
+
- `texture` is built by the engine at run time. `dssl build` bakes it into the CSS when it can, and says what it could not.
|
| 366 |
- Inside an aura, `texture(grain)` makes the light and the particles grainy. See below.
|
| 367 |
|
| 368 |
+
### Wood
|
| 369 |
+
|
| 370 |
+
`wood` was one generic grain. The `rings` generator draws real structure: each growth ring is a curve,
|
| 371 |
+
the spacing wobbles, the rings arch into cathedral figure, pores run between them, and planks and
|
| 372 |
+
knots are drawn on top. There are ten presets: `oak`, `walnut`, `cherry`, `maple`, `pine`, `mahogany`,
|
| 373 |
+
`ebony`, `birch`, and two laid as boards, `oak-planks` and `walnut-planks`.
|
| 374 |
+
|
| 375 |
+
```dssl
|
| 376 |
+
.table { background-color: #c9a574; texture: oak planks 120 } // laid as boards 120px tall
|
| 377 |
+
.panel { texture: walnut figure 0.9 warp 0.7 } // more arching, more wobble
|
| 378 |
+
.shelf { texture: pine knots 3 scale 18 } // three knots per tile
|
| 379 |
+
.post { texture: oak angle 90 } // grain running up and down
|
| 380 |
+
.custom { texture: rings scale 8 color #3a2410 color2 #b98a52 seed 21 } // your own species
|
| 381 |
+
```
|
| 382 |
+
|
| 383 |
+
| setting | meaning |
|
| 384 |
+
|---|---|
|
| 385 |
+
| `scale` | ring spacing in px |
|
| 386 |
+
| `color`, `color2` | latewood (the dark ring) and earlywood (the pale body) |
|
| 387 |
+
| `contrast` | how dark the rings are, 1.2 is the default |
|
| 388 |
+
| `figure` | 0 to 1: how strongly the rings arch |
|
| 389 |
+
| `warp` | 0 to 1: how much each ring wobbles |
|
| 390 |
+
| `pores` | 0 to 1: fine fibre streaks between rings |
|
| 391 |
+
| `knots` | 0 to 8 knots in one tile |
|
| 392 |
+
| `planks` | board height in px, with a seam and a different board each |
|
| 393 |
+
| `seed`, `angle` | another board; 0 or 90 degrees |
|
| 394 |
+
|
| 395 |
+
The drawing uses only your seed and plain arithmetic, so the same seed gives the same board on every
|
| 396 |
+
machine, and the tile repeats without a seam. It does repeat: one tile is 576px along the grain, and
|
| 397 |
+
three boards tall with `planks`, so a very large surface shows the pattern twice. Use two textures with
|
| 398 |
+
different seeds on different elements when that matters.
|
| 399 |
+
|
| 400 |
+
`dssl build` now **bakes textures into the CSS** as background images when every colour is known at
|
| 401 |
+
build time, which is almost always (tokens count). A rule with `background-color: color.walnut;
|
| 402 |
+
texture: oak` comes out as a real `background` with the image on top. Textures that animate, or use a
|
| 403 |
+
colour that is not known at build time, stay in the runtime, and the build report says which and why.
|
| 404 |
+
The wood showcase (`public/showcase/wood/`) does exactly this: `static.dssl` is built to `wood.css`
|
| 405 |
+
with `node scripts/build-wood.mjs`, and only the tilt, the fire and the slider fills are left for the engine.
|
| 406 |
+
|
| 407 |
+
### No flash before the engine boots
|
| 408 |
+
|
| 409 |
+
A page that loads its look through the engine can show unstyled for a moment. Two fixes. Build the
|
| 410 |
+
static part with `dssl build` and link the real CSS, as the wood page does. Or hide the page until the
|
| 411 |
+
engine is ready: the engine adds the class `dssl-ready` to `<html>` when it has applied your sheets.
|
| 412 |
+
|
| 413 |
+
```html
|
| 414 |
+
<style>html:not(.dssl-ready) body { visibility: hidden }</style>
|
| 415 |
+
<script>setTimeout(() => document.documentElement.classList.add('dssl-ready'), 2500)</script> <!-- failsafe -->
|
| 416 |
+
```
|
| 417 |
+
|
| 418 |
+
### Counters
|
| 419 |
+
|
| 420 |
+
`counter-reset`, `counter-increment`, `counter-set`, and `content: counter(step)` or
|
| 421 |
+
`counters(item, ".")` work. The names inside `counter()` are yours, not tokens.
|
| 422 |
+
|
| 423 |
### Aura
|
| 424 |
|
| 425 |
An aura is light and particles around an element. **There are no presets.** Fire, an
|
|
|
|
| 682 |
|
| 683 |
1. VSCodium: open the Extensions tab, search "DSSL" and install. It is on Open VSX:
|
| 684 |
https://open-vsx.org/extension/ehs-studio/dssl-vscode
|
| 685 |
+
2. VS Code: download `dssl-vscode/dssl-vscode-0.11.0.vsix` from the repo files, then
|
| 686 |
Extensions panel, `...`, Install from VSIX. (The Microsoft Marketplace listing is not published yet.)
|
| 687 |
3. Build it yourself: `cd dssl-vscode && npx @vscode/vsce package --no-dependencies`.
|
| 688 |
|
| 689 |
What it does: real-compiler diagnostics (same errors the browser prints,
|
| 690 |
did-you-mean included), completions folded from your workspace token registry,
|
| 691 |
hover docs, and a TextMate grammar for `.dssl`. Since 0.9.0 it also completes and
|
| 692 |
+
explains the new vocabulary: after `texture:` it offers the presets and every
|
| 693 |
setting, after `aura-glow:` only glow settings, after `aura-particle:` only particle
|
| 694 |
ones, after `slider:` the clauses.
|
| 695 |
|
|
|
|
| 702 |
and remove, a live preview drawn by the same renderer the engine uses (on dark, light or slate),
|
| 703 |
a graph of one particle's opacity and size over its life, and a live graph of particles alive and
|
| 704 |
frame time.
|
| 705 |
+
- **Texture tab.** All the presets and 11 generators as thumbnails, up to four stacked layers, every
|
| 706 |
setting as a slider, colour pickers, a preview over the colour you choose.
|
| 707 |
- **Slider tab.** A live range input styled with the generated rules, with a bar editor for histogram sliders.
|
| 708 |
|
PARITY.md
CHANGED
|
@@ -25,7 +25,7 @@ The point of this table is to stop two bad arguments: "DSSL is missing everythin
|
|
| 25 |
| Flexbox / Grid | raw properties pass through + `row:` `column:` `grid:` `stack:` sugar | shipped | shorthands write `display`; the compiler warns when that clobbers an explicit `display:` |
|
| 26 |
| `place-items` / alignment | `center: both` | shipped | the engine promotes a formatting context when missing, so alignment cannot silently no-op |
|
| 27 |
| Transitions | per-property: `transition: 200 snap transform` | shipped | honors `reduced-motion` |
|
| 28 |
-
| Background patterns and noise | `texture:` (
|
| 29 |
| Glow, particles, fire | `aura-glow`, `aura-particle`, `glow-particle`, `aura-glow-particle`, `aura:` | shipped | CSS has `box-shadow` and `filter: drop-shadow`, and nothing for particles. No presets by design. Runtime-only |
|
| 30 |
| Styling `<input type="range">` | `slider:` writes the `::-webkit-slider-*` and `::-moz-range-*` rules together | shipped | Chrome tested; the Firefox rules are generated but not run yet. Histogram sliders via `graph` |
|
| 31 |
| Accessibility hints in the compiler | contrast, removed focus outline, small targets (warnings) | shipped | CSS says nothing; these never fail a build unless you pass `--strict` |
|
|
@@ -62,6 +62,25 @@ so the boundary is explicit, not folklore.
|
|
| 62 |
- loud errors with did-you-mean, and a warnings channel for cascade-order footguns
|
| 63 |
- `freeze()` / `unfreeze()`: a live engine that can be parked at zero cost
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
## v0.10.0: glass, tilt, and a silent colour bug
|
| 66 |
|
| 67 |
| Need | DSSL | Status | Notes |
|
|
|
|
| 25 |
| Flexbox / Grid | raw properties pass through + `row:` `column:` `grid:` `stack:` sugar | shipped | shorthands write `display`; the compiler warns when that clobbers an explicit `display:` |
|
| 26 |
| `place-items` / alignment | `center: both` | shipped | the engine promotes a formatting context when missing, so alignment cannot silently no-op |
|
| 27 |
| Transitions | per-property: `transition: 200 snap transform` | shipped | honors `reduced-motion` |
|
| 28 |
+
| Background patterns and noise | `texture:` (40 presets including ten wood species, 11 generators, your own) | shipped | there is no CSS shorthand for "grain on top of my background"; this one keeps the author's background and removes cleanly. Runtime-only |
|
| 29 |
| Glow, particles, fire | `aura-glow`, `aura-particle`, `glow-particle`, `aura-glow-particle`, `aura:` | shipped | CSS has `box-shadow` and `filter: drop-shadow`, and nothing for particles. No presets by design. Runtime-only |
|
| 30 |
| Styling `<input type="range">` | `slider:` writes the `::-webkit-slider-*` and `::-moz-range-*` rules together | shipped | Chrome tested; the Firefox rules are generated but not run yet. Histogram sliders via `graph` |
|
| 31 |
| Accessibility hints in the compiler | contrast, removed focus outline, small targets (warnings) | shipped | CSS says nothing; these never fail a build unless you pass `--strict` |
|
|
|
|
| 62 |
- loud errors with did-you-mean, and a warnings channel for cascade-order footguns
|
| 63 |
- `freeze()` / `unfreeze()`: a live engine that can be parked at zero cost
|
| 64 |
|
| 65 |
+
## v0.11.0 (wood): rings, species, planks, baked textures
|
| 66 |
+
|
| 67 |
+
Written after another AI built a wood site in DSSL and listed what hurt. What came of it:
|
| 68 |
+
|
| 69 |
+
| Their note | What DSSL does now |
|
| 70 |
+
|---|---|
|
| 71 |
+
| `texture: wood` is one generic grain | `oak`, `walnut`, `cherry`, `maple`, `pine`, `mahogany`, `ebony`, `birch`, plus `oak-planks` and `walnut-planks` |
|
| 72 |
+
| Stretched noise with a contour layer is not wood | the `rings` generator draws growth rings as curves, with `figure`, `warp`, `pores` and `knots` |
|
| 73 |
+
| No way to draw planks | `planks <px>`: boards with seams and a slightly different board each |
|
| 74 |
+
| Seamless tiling, stable seed | integer harmonics and a seeded generator: same seed, same board everywhere; tested for seams |
|
| 75 |
+
| `counter-reset` and `counter()` rejected | they work |
|
| 76 |
+
| Contrast false alarms on translucent backgrounds | judged on the light page, white and black; warns only if it fails on all |
|
| 77 |
+
| Flash of unstyled content | `dssl-ready` class on `<html>`, and `dssl build` for the static part (the wood page does this) |
|
| 78 |
+
| Textures missing from `dssl build` | baked into the CSS when colours are known; the report names the ones that could not be |
|
| 79 |
+
| `@font-face` in `raw {}` | **not done.** A native font loader is still a good idea. |
|
| 80 |
+
| Gradients only cover simple cases | **not done.** Plain CSS gradient strings still pass through, which is what the wood page uses. |
|
| 81 |
+
| Dev-tools inspector for which rule produced a style | **not done.** `DSSL.explain()` is still a console call. |
|
| 82 |
+
| Hot reload | **not done.** |
|
| 83 |
+
|
| 84 |
## v0.10.0: glass, tilt, and a silent colour bug
|
| 85 |
|
| 86 |
| Need | DSSL | Status | Notes |
|
README.md
CHANGED
|
@@ -68,7 +68,7 @@ Both drop a `dssl-engine/` folder next to you: the engine, a starter token set,
|
|
| 68 |
|
| 69 |
Prefer to click: [dssl-engine.zip](https://dssl-sable.vercel.app/dssl-engine.zip) or [dssl-engine.tar.gz](https://dssl-sable.vercel.app/dssl-engine.tar.gz). Same package, straight from the browser.
|
| 70 |
|
| 71 |
-
Writing your own .dssl? The official editor extension (`ehs-studio.dssl-vscode`) puts the real compiler in your editor: the squiggles are the same errors the browser console prints, completions come from your own token registry, and hovers explain the semantic vocabulary. One package for VS Code and VSCodium (same extension API). In VSCodium, search "DSSL" in the Extensions tab (it is on Open VSX: https://open-vsx.org/extension/ehs-studio/dssl-vscode). In VS Code, download `dssl-vscode/dssl-vscode-0.
|
| 72 |
|
| 73 |
AI agents and machine reviewers: this repo ships `llms.txt` (dense, structured facts), `AGENTS.md` (how to work on the codebase) and `PARITY.md` (the honest feature matrix against CSS). The site serves the same `llms.txt` at https://dssl-sable.vercel.app/llms.txt
|
| 74 |
|
|
@@ -339,7 +339,7 @@ Gradients read like sentences. A stop can be a hex, a color name, a token, an al
|
|
| 339 |
## Texture
|
| 340 |
|
| 341 |
`texture:` lays a generated image on top of an element's own background (a colour, a gradient, or
|
| 342 |
-
several layers) and takes it off again cleanly.
|
| 343 |
everything, so you can make your own:
|
| 344 |
|
| 345 |
```dssl
|
|
@@ -523,7 +523,7 @@ The matcher also pre-filters every rule through a cheap class/tag/id gate extrac
|
|
| 523 |
- pseudo-elements in any spelling (shadow CSS), themed scrollbars included
|
| 524 |
- `raw { }` / `@css { }` for verbatim CSS (`@font-face`, `@keyframes`, `@supports`); unknown at-rules skip their whole block with one clean error
|
| 525 |
- `fluid 16 24` sizing sugar on any length property, including `padding-top`, `margin-block` and the other longhands
|
| 526 |
-
- `texture:` (
|
| 527 |
- `dssl import file.css`, lint warnings (contrast, focus, target size), and the editor designer with live previews
|
| 528 |
- one cascade across every stylesheet: a sheet loaded later beats an earlier one (fixed in 0.8.2)
|
| 529 |
- the CLI: `dssl check` (CI, --json, --strict) and `dssl build` (real CSS from the static subset, also with --strict)
|
|
|
|
| 68 |
|
| 69 |
Prefer to click: [dssl-engine.zip](https://dssl-sable.vercel.app/dssl-engine.zip) or [dssl-engine.tar.gz](https://dssl-sable.vercel.app/dssl-engine.tar.gz). Same package, straight from the browser.
|
| 70 |
|
| 71 |
+
Writing your own .dssl? The official editor extension (`ehs-studio.dssl-vscode`) puts the real compiler in your editor: the squiggles are the same errors the browser console prints, completions come from your own token registry, and hovers explain the semantic vocabulary. One package for VS Code and VSCodium (same extension API). In VSCodium, search "DSSL" in the Extensions tab (it is on Open VSX: https://open-vsx.org/extension/ehs-studio/dssl-vscode). In VS Code, download `dssl-vscode/dssl-vscode-0.11.0.vsix` from the repo files, then Extensions panel -> ... -> Install from VSIX. See `dssl-vscode/PUBLISHING.md` for publishing.
|
| 72 |
|
| 73 |
AI agents and machine reviewers: this repo ships `llms.txt` (dense, structured facts), `AGENTS.md` (how to work on the codebase) and `PARITY.md` (the honest feature matrix against CSS). The site serves the same `llms.txt` at https://dssl-sable.vercel.app/llms.txt
|
| 74 |
|
|
|
|
| 339 |
## Texture
|
| 340 |
|
| 341 |
`texture:` lays a generated image on top of an element's own background (a colour, a gradient, or
|
| 342 |
+
several layers) and takes it off again cleanly. Forty presets (thirty general ones and ten wood species), eleven generators, and a setting for
|
| 343 |
everything, so you can make your own:
|
| 344 |
|
| 345 |
```dssl
|
|
|
|
| 523 |
- pseudo-elements in any spelling (shadow CSS), themed scrollbars included
|
| 524 |
- `raw { }` / `@css { }` for verbatim CSS (`@font-face`, `@keyframes`, `@supports`); unknown at-rules skip their whole block with one clean error
|
| 525 |
- `fluid 16 24` sizing sugar on any length property, including `padding-top`, `margin-block` and the other longhands
|
| 526 |
+
- `texture:` (40 presets including ten wood species, 11 generators, your own; `dssl build` bakes them into the CSS), `aura-glow` / `aura-particle` / `glow-particle` / `aura-glow-particle` / `aura:` (no presets, you build them), `slider:` (WebKit and Firefox in one line)
|
| 527 |
- `dssl import file.css`, lint warnings (contrast, focus, target size), and the editor designer with live previews
|
| 528 |
- one cascade across every stylesheet: a sheet loaded later beats an earlier one (fixed in 0.8.2)
|
| 529 |
- the CLI: `dssl check` (CI, --json, --strict) and `dssl build` (real CSS from the static subset, also with --strict)
|
cli/dssl.mjs
CHANGED
|
@@ -182,6 +182,8 @@ if (cmd === 'build') {
|
|
| 182 |
console.log(` ${rep.rules} rule(s), ${rep.shadowRules} pseudo-element rule(s), ${rep.keyframes} @keyframes, ${rep.raws} raw block(s)`);
|
| 183 |
if (rep.layers.length) console.log(` layers: ${rep.layers.join(' < ')}`);
|
| 184 |
if (rep.seenStripped) console.log(` ${rep.seenStripped} seen rule(s) applied unconditionally (the no-JS state)`);
|
|
|
|
|
|
|
| 185 |
if (rep.skippedSignals || rep.engineDecls) {
|
| 186 |
const kinds = rep.engineKinds.length ? ` (${rep.engineKinds.join(', ')})` : '';
|
| 187 |
console.log(` stays in the runtime: ${rep.skippedSignals} @signal rule(s), ${rep.engineDecls} engine-native declaration(s)${kinds}`);
|
|
|
|
| 182 |
console.log(` ${rep.rules} rule(s), ${rep.shadowRules} pseudo-element rule(s), ${rep.keyframes} @keyframes, ${rep.raws} raw block(s)`);
|
| 183 |
if (rep.layers.length) console.log(` layers: ${rep.layers.join(' < ')}`);
|
| 184 |
if (rep.seenStripped) console.log(` ${rep.seenStripped} seen rule(s) applied unconditionally (the no-JS state)`);
|
| 185 |
+
if (rep.bakedTextures) console.log(` ${rep.bakedTextures} texture(s) baked into the CSS as background images`);
|
| 186 |
+
for (const n of rep.notBaked || []) console.log(` texture on "${n.sel}" stays in the runtime: ${n.why}`);
|
| 187 |
if (rep.skippedSignals || rep.engineDecls) {
|
| 188 |
const kinds = rep.engineKinds.length ? ` (${rep.engineKinds.join(', ')})` : '';
|
| 189 |
console.log(` stays in the runtime: ${rep.skippedSignals} @signal rule(s), ${rep.engineDecls} engine-native declaration(s)${kinds}`);
|
dssl-vscode/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
| 1 |
# Changelog
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
## 0.10.0
|
| 4 |
|
| 5 |
- Bundled engine updated to 0.10.0: new `glass` and `tilt` properties, with completions and hover
|
|
|
|
| 1 |
# Changelog
|
| 2 |
|
| 3 |
+
## 0.11.0
|
| 4 |
+
|
| 5 |
+
- Bundled engine updated to 0.11.0: ten wood species and the `rings` texture generator (figure, warp,
|
| 6 |
+
pores, knots, planks), textures baked into the CSS by `dssl build`, CSS counters, and a contrast lint
|
| 7 |
+
that no longer warns about translucent backgrounds it cannot judge. Completions and hover docs know the
|
| 8 |
+
new settings.
|
| 9 |
+
|
| 10 |
## 0.10.0
|
| 11 |
|
| 12 |
- Bundled engine updated to 0.10.0: new `glass` and `tilt` properties, with completions and hover
|
dssl-vscode/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Extension id: **`ehs-studio.dssl-vscode`**. See PUBLISHING.md for the publishing
|
|
| 32 |
- **Hover docs** for the semantic vocabulary (`row:`, `bend:`, `stretch:`, `seen`, `pin`...),
|
| 33 |
for tokens (resolved value), for workspace motions and behaviors.
|
| 34 |
- **Syntax highlighting** via a TextMate grammar for `.dssl`.
|
| 35 |
-
- **Completions for the new vocabulary.** After `texture:` you get the
|
| 36 |
every setting; after `aura-glow:` only glow settings; after `aura-particle:` only particle settings
|
| 37 |
(shapes, where particles are born, directions); after `slider:` the clauses. Hovering a preset or a
|
| 38 |
setting explains it.
|
|
@@ -40,7 +40,7 @@ Extension id: **`ehs-studio.dssl-vscode`**. See PUBLISHING.md for the publishing
|
|
| 40 |
- *Aura*: a slider or picker for every glow and particle setting, colour stops you can add and
|
| 41 |
remove, a live preview drawn by the real renderer, a graph of one particle's opacity and size over
|
| 42 |
its life, and a live graph of particles alive and frame time.
|
| 43 |
-
- *Texture*: all
|
| 44 |
as a slider, colour pickers.
|
| 45 |
- *Slider*: a live range input with the generated rules, and a bar editor for histogram sliders.
|
| 46 |
|
|
|
|
| 32 |
- **Hover docs** for the semantic vocabulary (`row:`, `bend:`, `stretch:`, `seen`, `pin`...),
|
| 33 |
for tokens (resolved value), for workspace motions and behaviors.
|
| 34 |
- **Syntax highlighting** via a TextMate grammar for `.dssl`.
|
| 35 |
+
- **Completions for the new vocabulary.** After `texture:` you get the presets, the generators and
|
| 36 |
every setting; after `aura-glow:` only glow settings; after `aura-particle:` only particle settings
|
| 37 |
(shapes, where particles are born, directions); after `slider:` the clauses. Hovering a preset or a
|
| 38 |
setting explains it.
|
|
|
|
| 40 |
- *Aura*: a slider or picker for every glow and particle setting, colour stops you can add and
|
| 41 |
remove, a live preview drawn by the real renderer, a graph of one particle's opacity and size over
|
| 42 |
its life, and a live graph of particles alive and frame time.
|
| 43 |
+
- *Texture*: all the presets and 11 generators as thumbnails, up to four stacked layers, every setting
|
| 44 |
as a slider, colour pickers.
|
| 45 |
- *Slider*: a live range input with the generated rules, and a bar editor for histogram sliders.
|
| 46 |
|
dssl-vscode/lib/engine/lint.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
| 18 |
import { parseColor, evalColorExpr, over, contrast, toRgba, toHex } from './color.js';
|
| 19 |
|
| 20 |
/** one design-token lookup closed over the sheet's token map */
|
| 21 |
-
function
|
| 22 |
const lookup = (name) => tokenMap.get(name);
|
| 23 |
/** CSS value as the resolver emits it ("var(--dssl-color-ink)", "color-mix(...)", "#fff") to a colour */
|
| 24 |
return function cssToColor(css, depth = 0) {
|
|
@@ -52,7 +52,7 @@ const INTERACTIVE = /^(a|button|input|select|textarea|summary)\b|\[tabindex|\[ro
|
|
| 52 |
*/
|
| 53 |
export function lintRules({ rules, tokenMap, file }) {
|
| 54 |
const warnings = [];
|
| 55 |
-
const cssToColor =
|
| 56 |
const pageBg = (() => { const raw = tokenMap.get('color.paper'); return raw ? evalColorExpr(raw, (n) => tokenMap.get(n)) : null; })();
|
| 57 |
|
| 58 |
const killedFocus = [];
|
|
@@ -72,10 +72,21 @@ export function lintRules({ rules, tokenMap, file }) {
|
|
| 72 |
let bg = cssToColor(bgD.value);
|
| 73 |
let fg = cssToColor(fgD.value);
|
| 74 |
if (bg && fg) {
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
const size = pxOf((lastDecl(r.decls, 'font-size') || {}).value);
|
| 80 |
const weightRaw = (lastDecl(r.decls, 'font-weight') || {}).value;
|
| 81 |
const weight = weightRaw === 'bold' ? 700 : Number(weightRaw) || 400;
|
|
|
|
| 18 |
import { parseColor, evalColorExpr, over, contrast, toRgba, toHex } from './color.js';
|
| 19 |
|
| 20 |
/** one design-token lookup closed over the sheet's token map */
|
| 21 |
+
export function makeColorResolver(tokenMap) {
|
| 22 |
const lookup = (name) => tokenMap.get(name);
|
| 23 |
/** CSS value as the resolver emits it ("var(--dssl-color-ink)", "color-mix(...)", "#fff") to a colour */
|
| 24 |
return function cssToColor(css, depth = 0) {
|
|
|
|
| 52 |
*/
|
| 53 |
export function lintRules({ rules, tokenMap, file }) {
|
| 54 |
const warnings = [];
|
| 55 |
+
const cssToColor = makeColorResolver(tokenMap);
|
| 56 |
const pageBg = (() => { const raw = tokenMap.get('color.paper'); return raw ? evalColorExpr(raw, (n) => tokenMap.get(n)) : null; })();
|
| 57 |
|
| 58 |
const killedFocus = [];
|
|
|
|
| 72 |
let bg = cssToColor(bgD.value);
|
| 73 |
let fg = cssToColor(fgD.value);
|
| 74 |
if (bg && fg) {
|
| 75 |
+
// a translucent background takes its look from whatever is behind it, which a single rule cannot know
|
| 76 |
+
// (a dark theme is usually in a media block, and this check only reads the unconditional rules). So judge
|
| 77 |
+
// it against the light page colour, white and black, and warn only when it fails on every one of them.
|
| 78 |
+
const backdrops = bg.a < 0.999 ? [pageBg ? { ...pageBg, a: 1 } : null, { r: 255, g: 255, b: 255, a: 1 }, { r: 0, g: 0, b: 0, a: 1 }].filter(Boolean) : [null];
|
| 79 |
+
let bestRatio = -1, bestFg = null, bestBg = null;
|
| 80 |
+
for (const back of backdrops) {
|
| 81 |
+
const solidBg = back ? over(bg, back) : bg;
|
| 82 |
+
const solidFg = fg.a < 0.999 ? over(fg, solidBg) : fg;
|
| 83 |
+
const rr = contrast(solidFg, solidBg);
|
| 84 |
+
if (rr > bestRatio) { bestRatio = rr; bestFg = solidFg; bestBg = solidBg; }
|
| 85 |
+
}
|
| 86 |
+
if (bestBg) {
|
| 87 |
+
const fgSolid = bestFg;
|
| 88 |
+
bg = bestBg;
|
| 89 |
+
const ratio = bestRatio;
|
| 90 |
const size = pxOf((lastDecl(r.decls, 'font-size') || {}).value);
|
| 91 |
const weightRaw = (lastDecl(r.decls, 'font-weight') || {}).value;
|
| 92 |
const weight = weightRaw === 'bold' ? 700 : Number(weightRaw) || 400;
|
dssl-vscode/lib/engine/resolver.js
CHANGED
|
@@ -80,7 +80,7 @@ export const UNITLESS_PROPS = new Set([
|
|
| 80 |
'opacity', 'line-height', 'z-index', 'font-weight', 'flex-grow', 'flex-shrink',
|
| 81 |
'flex', 'order', 'scale', 'widows', 'orphans', 'column-count', 'aspect-ratio',
|
| 82 |
'animation-iteration-count', 'flood-opacity', 'fill-opacity', 'stop-opacity',
|
| 83 |
-
'shape-image-threshold',
|
| 84 |
// grid line placement takes integers, not lengths: grid-column: span 2
|
| 85 |
'grid-row', 'grid-column', 'grid-area',
|
| 86 |
]);
|
|
@@ -106,7 +106,7 @@ export const KNOWN_PROPS = new Set(('all,appearance,accent-color,align-content,a
|
|
| 106 |
'border-inline,border-inline-color,border-inline-end,border-inline-end-color,border-inline-end-style,border-inline-end-width,border-inline-start,border-inline-start-color,border-inline-start-style,border-inline-start-width,border-inline-style,border-inline-width,' +
|
| 107 |
'border-left,border-left-color,border-left-style,border-left-width,border-radius,border-right,border-right-color,border-right-style,border-right-width,' +
|
| 108 |
'border-spacing,border-start-end-radius,border-start-start-radius,border-style,border-top,border-top-color,border-top-left-radius,border-top-right-radius,border-top-style,border-top-width,border-width,' +
|
| 109 |
-
'bottom,box-decoration-break,box-shadow,box-sizing,break-after,break-before,break-inside,caret-color,caret-shape,clear,clip,clip-path,color,color-scheme,column-count,column-fill,column-gap,column-rule,column-rule-color,column-rule-style,column-rule-width,column-span,column-width,columns,contain,contain-intrinsic-size,container,container-name,container-type,content,content-visibility,cursor,caret,display,empty-cells,filter,flex,flex-basis,flex-direction,flex-flow,flex-grow,flex-shrink,flex-wrap,float,fill,fill-rule,fill-opacity,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-opacity,stroke-width,font,font-family,font-feature-settings,font-kerning,font-language-override,font-optical-sizing,font-palette,font-size,font-size-adjust,font-stretch,font-style,font-synthesis,font-variant,font-variant-caps,font-variant-east-asian,font-variant-ligatures,font-variant-numeric,font-variant-position,font-variation-settings,font-weight,forced-color-adjust,gap,grid,grid-area,grid-auto-columns,grid-auto-flow,grid-auto-rows,grid-column,grid-column-end,grid-column-gap,grid-column-start,grid-gap,grid-row,grid-row-end,grid-row-gap,grid-row-start,grid-template,grid-template-areas,grid-template-columns,grid-template-rows,' +
|
| 110 |
'hanging-punctuation,height,hyphenate-character,hyphenate-limit-chars,hyphens,image-orientation,image-rendering,inline-size,inset,inset-block,inset-block-end,inset-block-start,inset-inline,inset-inline-end,inset-inline-start,isolation,justify-content,justify-items,justify-self,justify-tracks,left,letter-spacing,line-break,line-height,list-style,list-style-image,list-style-position,list-style-type,margin,margin-block,margin-block-end,margin-block-start,margin-bottom,margin-inline,margin-inline-end,margin-inline-start,margin-left,margin-right,margin-top,margin-trim,mask,mask-border,mask-border-mode,mask-border-outset,mask-border-repeat,mask-border-slice,mask-border-source,mask-border-width,mask-clip,mask-composite,mask-image,mask-mode,mask-origin,mask-position,mask-repeat,mask-size,max-height,max-inline-size,max-width,min-block-size,min-height,min-inline-size,min-width,mix-blend-mode,object-fit,object-position,offset,offset-anchor,offset-distance,offset-path,offset-rotate,opacity,order,outline,outline-color,outline-offset,outline-style,outline-width,overflow,overflow-anchor,overflow-clip-margin,overflow-wrap,overflow-x,overflow-y,overscroll-behavior,overscroll-behavior-block,overscroll-behavior-inline,overscroll-behavior-x,overscroll-behavior-y,padding,padding-block,padding-block-end,padding-block-start,padding-bottom,padding-inline,padding-inline-end,padding-inline-start,padding-left,padding-right,padding-top,page-break-after,page-break-before,page-break-inside,paint-order,perspective,perspective-origin,place-content,place-items,place-self,pointer-events,position,print-color-adjust,quotes,resize,right,rotate,row-gap,ruby-align,ruby-merge,scale,scroll-behavior,scroll-margin,scroll-margin-block,scroll-margin-block-end,scroll-margin-block-start,scroll-margin-bottom,scroll-margin-inline,scroll-margin-inline-end,scroll-margin-inline-start,scroll-margin-left,scroll-margin-right,scroll-margin-top,scroll-padding,scroll-padding-block,scroll-padding-bottom,scroll-padding-inline,scroll-padding-left,scroll-padding-right,scroll-padding-top,scroll-snap-align,scroll-snap-stop,scroll-snap-type,scrollbar-color,scrollbar-gutter,scrollbar-width,shape-image-threshold,shape-margin,shape-outside,tab-size,table-layout,text-align,text-align-last,text-combine-upright,text-decoration,text-decoration-color,text-decoration-line,text-decoration-skip-ink,text-decoration-style,text-decoration-thickness,text-emphasis,text-emphasis-color,text-emphasis-position,text-emphasis-style,text-indent,text-justify,text-orientation,text-overflow,text-rendering,text-shadow,text-transform,text-underline-offset,text-underline-position,text-wrap,top,touch-action,transform,transform-box,transform-origin,transform-style,transition,transition-behavior,transition-delay,transition-duration,transition-property,transition-timing-function,translate,unicode-bidi,user-select,vertical-align,visibility,white-space,white-space-collapse,width,will-change,word-break,word-spacing,word-wrap,writing-mode,z-index,zoom,' +
|
| 111 |
'animation,animation-composition,animation-delay,animation-direction,animation-duration,animation-fill-mode,animation-iteration-count,animation-name,animation-play-state,animation-range,animation-timeline,animation-timing-function,' +
|
| 112 |
'view-transition-name,interpolate-size,text-box,text-box-edge,text-box-trim,dynamic-range-limit,field-sizing,anchor-name,position-anchor,position-area,scroll-timeline,scroll-timeline-axis,scroll-timeline-name,timeline-scope,view-timeline,view-timeline-axis,view-timeline-name,' +
|
|
@@ -268,7 +268,9 @@ function resolveValue(raw, ctx) {
|
|
| 268 |
}
|
| 269 |
// color-ish functions keep bare numbers bare; calc-family gets px
|
| 270 |
const innerUnitless = UNITLESS_FN.has(fname) ? true : LENGTH_FN.has(fname) ? false : ctx.unitless;
|
| 271 |
-
|
|
|
|
|
|
|
| 272 |
out += ident + '(' + resolvedInner + ')';
|
| 273 |
i = j;
|
| 274 |
continue;
|
|
@@ -1136,7 +1138,7 @@ function resolveDecl(prop, value, ctx) {
|
|
| 1136 |
});
|
| 1137 |
return [];
|
| 1138 |
}
|
| 1139 |
-
return [{ prop, value: resolveValue(value, { ...ctx2, unitless: UNITLESS_PROPS.has(prop), bareIdents: prop === 'font-family' }) }];
|
| 1140 |
}
|
| 1141 |
|
| 1142 |
/** string-aware !important scan - quoted content: "!important" must not trip it */
|
|
|
|
| 80 |
'opacity', 'line-height', 'z-index', 'font-weight', 'flex-grow', 'flex-shrink',
|
| 81 |
'flex', 'order', 'scale', 'widows', 'orphans', 'column-count', 'aspect-ratio',
|
| 82 |
'animation-iteration-count', 'flood-opacity', 'fill-opacity', 'stop-opacity',
|
| 83 |
+
'shape-image-threshold', 'counter-reset', 'counter-increment', 'counter-set',
|
| 84 |
// grid line placement takes integers, not lengths: grid-column: span 2
|
| 85 |
'grid-row', 'grid-column', 'grid-area',
|
| 86 |
]);
|
|
|
|
| 106 |
'border-inline,border-inline-color,border-inline-end,border-inline-end-color,border-inline-end-style,border-inline-end-width,border-inline-start,border-inline-start-color,border-inline-start-style,border-inline-start-width,border-inline-style,border-inline-width,' +
|
| 107 |
'border-left,border-left-color,border-left-style,border-left-width,border-radius,border-right,border-right-color,border-right-style,border-right-width,' +
|
| 108 |
'border-spacing,border-start-end-radius,border-start-start-radius,border-style,border-top,border-top-color,border-top-left-radius,border-top-right-radius,border-top-style,border-top-width,border-width,' +
|
| 109 |
+
'bottom,counter-reset,counter-increment,counter-set,box-decoration-break,box-shadow,box-sizing,break-after,break-before,break-inside,caret-color,caret-shape,clear,clip,clip-path,color,color-scheme,column-count,column-fill,column-gap,column-rule,column-rule-color,column-rule-style,column-rule-width,column-span,column-width,columns,contain,contain-intrinsic-size,container,container-name,container-type,content,content-visibility,cursor,caret,display,empty-cells,filter,flex,flex-basis,flex-direction,flex-flow,flex-grow,flex-shrink,flex-wrap,float,fill,fill-rule,fill-opacity,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-opacity,stroke-width,font,font-family,font-feature-settings,font-kerning,font-language-override,font-optical-sizing,font-palette,font-size,font-size-adjust,font-stretch,font-style,font-synthesis,font-variant,font-variant-caps,font-variant-east-asian,font-variant-ligatures,font-variant-numeric,font-variant-position,font-variation-settings,font-weight,forced-color-adjust,gap,grid,grid-area,grid-auto-columns,grid-auto-flow,grid-auto-rows,grid-column,grid-column-end,grid-column-gap,grid-column-start,grid-gap,grid-row,grid-row-end,grid-row-gap,grid-row-start,grid-template,grid-template-areas,grid-template-columns,grid-template-rows,' +
|
| 110 |
'hanging-punctuation,height,hyphenate-character,hyphenate-limit-chars,hyphens,image-orientation,image-rendering,inline-size,inset,inset-block,inset-block-end,inset-block-start,inset-inline,inset-inline-end,inset-inline-start,isolation,justify-content,justify-items,justify-self,justify-tracks,left,letter-spacing,line-break,line-height,list-style,list-style-image,list-style-position,list-style-type,margin,margin-block,margin-block-end,margin-block-start,margin-bottom,margin-inline,margin-inline-end,margin-inline-start,margin-left,margin-right,margin-top,margin-trim,mask,mask-border,mask-border-mode,mask-border-outset,mask-border-repeat,mask-border-slice,mask-border-source,mask-border-width,mask-clip,mask-composite,mask-image,mask-mode,mask-origin,mask-position,mask-repeat,mask-size,max-height,max-inline-size,max-width,min-block-size,min-height,min-inline-size,min-width,mix-blend-mode,object-fit,object-position,offset,offset-anchor,offset-distance,offset-path,offset-rotate,opacity,order,outline,outline-color,outline-offset,outline-style,outline-width,overflow,overflow-anchor,overflow-clip-margin,overflow-wrap,overflow-x,overflow-y,overscroll-behavior,overscroll-behavior-block,overscroll-behavior-inline,overscroll-behavior-x,overscroll-behavior-y,padding,padding-block,padding-block-end,padding-block-start,padding-bottom,padding-inline,padding-inline-end,padding-inline-start,padding-left,padding-right,padding-top,page-break-after,page-break-before,page-break-inside,paint-order,perspective,perspective-origin,place-content,place-items,place-self,pointer-events,position,print-color-adjust,quotes,resize,right,rotate,row-gap,ruby-align,ruby-merge,scale,scroll-behavior,scroll-margin,scroll-margin-block,scroll-margin-block-end,scroll-margin-block-start,scroll-margin-bottom,scroll-margin-inline,scroll-margin-inline-end,scroll-margin-inline-start,scroll-margin-left,scroll-margin-right,scroll-margin-top,scroll-padding,scroll-padding-block,scroll-padding-bottom,scroll-padding-inline,scroll-padding-left,scroll-padding-right,scroll-padding-top,scroll-snap-align,scroll-snap-stop,scroll-snap-type,scrollbar-color,scrollbar-gutter,scrollbar-width,shape-image-threshold,shape-margin,shape-outside,tab-size,table-layout,text-align,text-align-last,text-combine-upright,text-decoration,text-decoration-color,text-decoration-line,text-decoration-skip-ink,text-decoration-style,text-decoration-thickness,text-emphasis,text-emphasis-color,text-emphasis-position,text-emphasis-style,text-indent,text-justify,text-orientation,text-overflow,text-rendering,text-shadow,text-transform,text-underline-offset,text-underline-position,text-wrap,top,touch-action,transform,transform-box,transform-origin,transform-style,transition,transition-behavior,transition-delay,transition-duration,transition-property,transition-timing-function,translate,unicode-bidi,user-select,vertical-align,visibility,white-space,white-space-collapse,width,will-change,word-break,word-spacing,word-wrap,writing-mode,z-index,zoom,' +
|
| 111 |
'animation,animation-composition,animation-delay,animation-direction,animation-duration,animation-fill-mode,animation-iteration-count,animation-name,animation-play-state,animation-range,animation-timeline,animation-timing-function,' +
|
| 112 |
'view-transition-name,interpolate-size,text-box,text-box-edge,text-box-trim,dynamic-range-limit,field-sizing,anchor-name,position-anchor,position-area,scroll-timeline,scroll-timeline-axis,scroll-timeline-name,timeline-scope,view-timeline,view-timeline-axis,view-timeline-name,' +
|
|
|
|
| 268 |
}
|
| 269 |
// color-ish functions keep bare numbers bare; calc-family gets px
|
| 270 |
const innerUnitless = UNITLESS_FN.has(fname) ? true : LENGTH_FN.has(fname) ? false : ctx.unitless;
|
| 271 |
+
// counter(step), counters(step, "."), attr(data-x): the names inside are the author's own, not tokens
|
| 272 |
+
const nameFn = fname === 'counter' || fname === 'counters' || fname === 'attr';
|
| 273 |
+
const resolvedInner = resolveValue(inner, { ...ctx, unitless: innerUnitless, bareIdents: nameFn ? true : ctx.bareIdents });
|
| 274 |
out += ident + '(' + resolvedInner + ')';
|
| 275 |
i = j;
|
| 276 |
continue;
|
|
|
|
| 1138 |
});
|
| 1139 |
return [];
|
| 1140 |
}
|
| 1141 |
+
return [{ prop, value: resolveValue(value, { ...ctx2, unitless: UNITLESS_PROPS.has(prop), bareIdents: prop === 'font-family' || prop.startsWith('counter-') }) }];
|
| 1142 |
}
|
| 1143 |
|
| 1144 |
/** string-aware !important scan - quoted content: "!important" must not trip it */
|
dssl-vscode/lib/engine/texture.js
CHANGED
|
@@ -28,7 +28,7 @@ const mix2 = (x, y, t) => mix(x, y, t, 'srgb');
|
|
| 28 |
|
| 29 |
export const BLEND_MODES = ['normal', 'multiply', 'screen', 'overlay', 'soft-light', 'hard-light', 'color-burn', 'color-dodge', 'darken', 'lighten', 'difference', 'exclusion'];
|
| 30 |
|
| 31 |
-
export const GENERATORS = ['noise', 'bump', 'lines', 'dots', 'grid', 'checker', 'weave', 'carbon', 'waves', 'hex'];
|
| 32 |
|
| 33 |
/** the settings a texture layer understands, with how many value words each takes */
|
| 34 |
export const TEXTURE_KEYS = {
|
|
@@ -51,6 +51,11 @@ export const TEXTURE_KEYS = {
|
|
| 51 |
blend: { words: 1, kind: 'word', values: BLEND_MODES, doc: 'how the layer mixes with what is under it' },
|
| 52 |
color: { words: 1, kind: 'color', doc: 'the main colour (dark end of noise, ink of lines and dots)' },
|
| 53 |
color2: { words: 1, kind: 'color', doc: 'the second colour (light end of noise, background cell of patterns)' },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
invert: { words: 0, kind: 'flag', doc: 'swap the two colours' },
|
| 55 |
};
|
| 56 |
|
|
@@ -100,6 +105,26 @@ export const PRESETS = {
|
|
| 100 |
] },
|
| 101 |
wood: { doc: 'long wood grain, along x (use angle 90 for vertical)',
|
| 102 |
layers: [{ gen: 'noise', scale: 4, octaves: 3, contrast: 1.8, stretch: [40, 1], intensity: 1, color: '#7a4b24', color2: '#c99a62', blend: 'multiply' }] },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
bark: { doc: 'rough vertical bark',
|
| 104 |
layers: [{ gen: 'bump', scale: 10, octaves: 4, depth: 5, stretch: [1, 6], light: 150, intensity: 0.8, color: '#000', color2: '#fff', blend: 'overlay' }] },
|
| 105 |
leather: { doc: 'pebbled leather',
|
|
@@ -164,6 +189,7 @@ const GEN_DEFAULTS = {
|
|
| 164 |
carbon: { scale: 20, intensity: 1, color: '#151515', color2: '#262626', blend: 'normal' },
|
| 165 |
waves: { scale: 40, thickness: 1.2, intensity: 0.3, color: '#000', blend: 'normal' },
|
| 166 |
hex: { scale: 28, thickness: 1, intensity: 0.28, color: '#000', blend: 'normal' },
|
|
|
|
| 167 |
};
|
| 168 |
|
| 169 |
/* ------------------------------------------------------------------ *
|
|
@@ -452,7 +478,134 @@ function buildHex(spec, rc) {
|
|
| 452 |
return [{ image: svgUrl(svg), size: `${f(W)}px ${f(100 * k)}px`, repeat: 'repeat', position: '0 0', blend: spec.blend || 'normal', tile: W }];
|
| 453 |
}
|
| 454 |
|
| 455 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
|
| 457 |
/**
|
| 458 |
* Turn parsed layers into the CSS lists for background-image, -size, -repeat,
|
|
@@ -477,6 +630,59 @@ export function buildTexture(layers, rc) {
|
|
| 477 |
};
|
| 478 |
}
|
| 479 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
/* ------------------------------------------------------------------ *
|
| 481 |
* deterministic value noise, for aura masks and tests
|
| 482 |
* ------------------------------------------------------------------ */
|
|
|
|
| 28 |
|
| 29 |
export const BLEND_MODES = ['normal', 'multiply', 'screen', 'overlay', 'soft-light', 'hard-light', 'color-burn', 'color-dodge', 'darken', 'lighten', 'difference', 'exclusion'];
|
| 30 |
|
| 31 |
+
export const GENERATORS = ['noise', 'bump', 'lines', 'dots', 'grid', 'checker', 'weave', 'carbon', 'waves', 'hex', 'rings'];
|
| 32 |
|
| 33 |
/** the settings a texture layer understands, with how many value words each takes */
|
| 34 |
export const TEXTURE_KEYS = {
|
|
|
|
| 51 |
blend: { words: 1, kind: 'word', values: BLEND_MODES, doc: 'how the layer mixes with what is under it' },
|
| 52 |
color: { words: 1, kind: 'color', doc: 'the main colour (dark end of noise, ink of lines and dots)' },
|
| 53 |
color2: { words: 1, kind: 'color', doc: 'the second colour (light end of noise, background cell of patterns)' },
|
| 54 |
+
figure: { words: 1, kind: 'number', min: 0, max: 1, doc: 'rings generator: how strongly the growth rings arch into a cathedral figure, 0 is straight grain' },
|
| 55 |
+
warp: { words: 1, kind: 'number', min: 0, max: 1, doc: 'rings generator: how much each ring wobbles along its length' },
|
| 56 |
+
pores: { words: 1, kind: 'number', min: 0, max: 1, doc: 'rings generator: fine fibre and pore streaks between the rings' },
|
| 57 |
+
knots: { words: 1, kind: 'number', min: 0, max: 8, int: true, doc: 'rings generator: how many knots in one tile (the pattern repeats, so keep it small)' },
|
| 58 |
+
planks: { words: 1, kind: 'number', min: 24, max: 1200, doc: 'rings generator: lay it out as planks this many px tall, with a seam and a slightly different board each' },
|
| 59 |
invert: { words: 0, kind: 'flag', doc: 'swap the two colours' },
|
| 60 |
};
|
| 61 |
|
|
|
|
| 105 |
] },
|
| 106 |
wood: { doc: 'long wood grain, along x (use angle 90 for vertical)',
|
| 107 |
layers: [{ gen: 'noise', scale: 4, octaves: 3, contrast: 1.8, stretch: [40, 1], intensity: 1, color: '#7a4b24', color2: '#c99a62', blend: 'multiply' }] },
|
| 108 |
+
oak: { doc: 'oak: open grain, tan with brown rings',
|
| 109 |
+
layers: [{ gen: 'rings', scale: 11, color: '#8a5f33', color2: '#c9a574', contrast: 1.3, figure: 0.55, warp: 0.45, pores: 0.5, seed: 3, blend: 'normal' }] },
|
| 110 |
+
walnut: { doc: 'walnut: dark chocolate with black-brown rings',
|
| 111 |
+
layers: [{ gen: 'rings', scale: 9, color: '#2b1a10', color2: '#6a4631', contrast: 1.2, figure: 0.5, warp: 0.55, pores: 0.45, seed: 8, blend: 'normal' }] },
|
| 112 |
+
cherry: { doc: 'cherry: warm red-brown with fine rings',
|
| 113 |
+
layers: [{ gen: 'rings', scale: 8, color: '#7a3a1c', color2: '#bb7443', contrast: 1.1, figure: 0.4, warp: 0.35, pores: 0.35, seed: 5, blend: 'normal' }] },
|
| 114 |
+
maple: { doc: 'maple: pale, tight, nearly plain grain',
|
| 115 |
+
layers: [{ gen: 'rings', scale: 13, color: '#c79a5a', color2: '#ecd6ab', contrast: 0.8, figure: 0.25, warp: 0.3, pores: 0.25, seed: 11, blend: 'normal' }] },
|
| 116 |
+
pine: { doc: 'pine: light yellow wood with wide rings and knots',
|
| 117 |
+
layers: [{ gen: 'rings', scale: 16, color: '#b5742f', color2: '#e6c081', contrast: 1.4, figure: 0.65, warp: 0.5, pores: 0.4, knots: 2, seed: 14, blend: 'normal' }] },
|
| 118 |
+
mahogany: { doc: 'mahogany: deep red-brown, straight grain',
|
| 119 |
+
layers: [{ gen: 'rings', scale: 10, color: '#4a2012', color2: '#8c4727', contrast: 1.1, figure: 0.3, warp: 0.3, pores: 0.4, seed: 6, blend: 'normal' }] },
|
| 120 |
+
ebony: { doc: 'ebony: nearly black with faint streaks',
|
| 121 |
+
layers: [{ gen: 'rings', scale: 12, color: '#0c0908', color2: '#2d2621', contrast: 0.7, figure: 0.2, warp: 0.25, pores: 0.3, seed: 2, blend: 'normal' }] },
|
| 122 |
+
birch: { doc: 'birch: creamy pale wood with fine rings',
|
| 123 |
+
layers: [{ gen: 'rings', scale: 10, color: '#d2ae74', color2: '#f0dfb9', contrast: 0.9, figure: 0.3, warp: 0.35, pores: 0.25, seed: 9, blend: 'normal' }] },
|
| 124 |
+
'oak-planks': { doc: 'oak laid as planks, with seams',
|
| 125 |
+
layers: [{ gen: 'rings', scale: 11, color: '#8a5f33', color2: '#c9a574', contrast: 1.3, figure: 0.55, warp: 0.45, pores: 0.5, planks: 110, seed: 3, blend: 'normal' }] },
|
| 126 |
+
'walnut-planks': { doc: 'walnut laid as planks, with seams',
|
| 127 |
+
layers: [{ gen: 'rings', scale: 9, color: '#2b1a10', color2: '#6a4631', contrast: 1.2, figure: 0.5, warp: 0.55, pores: 0.45, planks: 96, seed: 8, blend: 'normal' }] },
|
| 128 |
bark: { doc: 'rough vertical bark',
|
| 129 |
layers: [{ gen: 'bump', scale: 10, octaves: 4, depth: 5, stretch: [1, 6], light: 150, intensity: 0.8, color: '#000', color2: '#fff', blend: 'overlay' }] },
|
| 130 |
leather: { doc: 'pebbled leather',
|
|
|
|
| 189 |
carbon: { scale: 20, intensity: 1, color: '#151515', color2: '#262626', blend: 'normal' },
|
| 190 |
waves: { scale: 40, thickness: 1.2, intensity: 0.3, color: '#000', blend: 'normal' },
|
| 191 |
hex: { scale: 28, thickness: 1, intensity: 0.28, color: '#000', blend: 'normal' },
|
| 192 |
+
rings: { scale: 10, color: '#7a4b24', color2: '#d1a770', contrast: 1.2, figure: 0.5, warp: 0.4, pores: 0.4, intensity: 1, blend: 'normal' },
|
| 193 |
};
|
| 194 |
|
| 195 |
/* ------------------------------------------------------------------ *
|
|
|
|
| 478 |
return [{ image: svgUrl(svg), size: `${f(W)}px ${f(100 * k)}px`, repeat: 'repeat', position: '0 0', blend: spec.blend || 'normal', tile: W }];
|
| 479 |
}
|
| 480 |
|
| 481 |
+
|
| 482 |
+
/* ------------------------------------------------------------------ *
|
| 483 |
+
* rings: growth rings, drawn as vector curves
|
| 484 |
+
*
|
| 485 |
+
* A board is a log sliced, and the rings you see are where the slice crosses
|
| 486 |
+
* the growth layers. So this does not stretch noise: it draws each ring as a
|
| 487 |
+
* curve. The spacing wobbles, the curves arch (cathedral figure) because the
|
| 488 |
+
* slice cuts the layers at a slant, there are pores between them, and planks
|
| 489 |
+
* and knots are drawn on top. Everything comes from a seeded generator and
|
| 490 |
+
* integer harmonics, so the same seed gives the same board on every machine
|
| 491 |
+
* and the tile has no visible seam.
|
| 492 |
+
* ------------------------------------------------------------------ */
|
| 493 |
+
|
| 494 |
+
const TAU = Math.PI * 2;
|
| 495 |
+
const r1 = (n) => String(Math.round(n * 10) / 10);
|
| 496 |
+
const smallUrl = (svg) => `url("data:image/svg+xml,${svg.replace(/[%#<>"]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())}")`;
|
| 497 |
+
|
| 498 |
+
/** a smooth path through [x, y] samples (Catmull-Rom as cubic Beziers, ends clamped) */
|
| 499 |
+
function smoothPath(pts) {
|
| 500 |
+
const n = pts.length;
|
| 501 |
+
const at = (i) => pts[Math.max(0, Math.min(n - 1, i))];
|
| 502 |
+
let d = `M${r1(pts[0][0])} ${r1(pts[0][1])}`;
|
| 503 |
+
for (let i = 0; i < n - 1; i++) {
|
| 504 |
+
const p0 = at(i - 1), p1 = at(i), p2 = at(i + 1), p3 = at(i + 2);
|
| 505 |
+
const c1x = p1[0] + (p2[0] - p0[0]) / 6, c1y = p1[1] + (p2[1] - p0[1]) / 6;
|
| 506 |
+
const c2x = p2[0] - (p3[0] - p1[0]) / 6, c2y = p2[1] - (p3[1] - p1[1]) / 6;
|
| 507 |
+
d += `C${r1(c1x)} ${r1(c1y)} ${r1(c2x)} ${r1(c2y)} ${r1(p2[0])} ${r1(p2[1])}`;
|
| 508 |
+
}
|
| 509 |
+
return d;
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
function buildRings(spec, rc) {
|
| 513 |
+
const { a, b } = layerColors(spec, rc);
|
| 514 |
+
const late = a, early = b || a;
|
| 515 |
+
const P = Math.max(3, spec.scale || 10);
|
| 516 |
+
const W = 576; // one tile along the grain; every harmonic below divides it
|
| 517 |
+
const fig = spec.figure === undefined ? 0.5 : spec.figure;
|
| 518 |
+
const warp = spec.warp === undefined ? 0.4 : spec.warp;
|
| 519 |
+
const pores = spec.pores === undefined ? 0.4 : spec.pores;
|
| 520 |
+
const contrast = spec.contrast === undefined ? 1.2 : spec.contrast;
|
| 521 |
+
const seed = spec.seed || 0;
|
| 522 |
+
const planked = !!spec.planks;
|
| 523 |
+
const plankH = planked ? Math.max(P * 3, spec.planks) : 0;
|
| 524 |
+
const plankN = planked ? 3 : 1;
|
| 525 |
+
const H = planked ? plankH * plankN : P * Math.max(12, Math.round(260 / P));
|
| 526 |
+
const rgba = (c, alpha) => toRgba(withAlpha(c, n01((alpha === undefined ? 1 : alpha) * c.a)));
|
| 527 |
+
const STEP = 48;
|
| 528 |
+
|
| 529 |
+
let defs = '', body = '';
|
| 530 |
+
let id = 0;
|
| 531 |
+
for (let pi = 0; pi < plankN; pi++) {
|
| 532 |
+
const rand = rng(seed * 7919 + pi * 104729 + 13);
|
| 533 |
+
const h = planked ? plankH : H;
|
| 534 |
+
const n = Math.max(3, Math.round(h / P));
|
| 535 |
+
const Pe = h / n;
|
| 536 |
+
const ph = [rand() * TAU, rand() * TAU, rand() * TAU];
|
| 537 |
+
const x0 = rand() * W;
|
| 538 |
+
const m = 2 + Math.floor(rand() * 3), spacingAmp = 0.12 + rand() * 0.22;
|
| 539 |
+
const flip = planked && rand() < 0.5;
|
| 540 |
+
const curve = (kf, x) => {
|
| 541 |
+
const t = kf / n;
|
| 542 |
+
// a free board arches from one face to the other; a seamless tile must repeat every n rings, so its envelope has period n
|
| 543 |
+
const per = Math.pow(Math.sin(Math.PI * t), 2);
|
| 544 |
+
const env = planked ? Math.pow(Math.max(0, Math.min(1, flip ? 1 - t : t)), 1.4) : per;
|
| 545 |
+
const env2 = planked ? 0.6 + 0.4 * Math.abs(Math.sin(Math.PI * t)) : per;
|
| 546 |
+
const bump = Math.pow((1 + Math.cos(TAU * (x - x0) / W)) / 2, 1.3);
|
| 547 |
+
const arch = fig * 0.3 * h * env * bump;
|
| 548 |
+
const wob = warp * P * 0.6 * env2 * (0.5 * Math.sin(TAU * x / W + ph[0] + TAU * kf / n) + 0.3 * Math.sin(TAU * 3 * x / W + ph[1] + TAU * 2 * kf / n) + 0.2 * Math.sin(TAU * 7 * x / W + ph[2] + TAU * 3 * kf / n));
|
| 549 |
+
const u = kf + spacingAmp * Math.sin(TAU * m * kf / n);
|
| 550 |
+
return u * Pe + arch + wob;
|
| 551 |
+
};
|
| 552 |
+
// sampled past both ends of the tile, so the stroke is continuous across the join and the viewport clips it
|
| 553 |
+
const ringPath = (kf) => { const pts = []; for (let x = -2 * STEP; x <= W + 2 * STEP; x += STEP) pts.push([x, curve(kf, x)]); return smoothPath(pts); };
|
| 554 |
+
const jit = Array.from({ length: n }, () => [rand(), rand()]);
|
| 555 |
+
|
| 556 |
+
const lo = planked ? -2 : -1, hi = planked ? n + 2 : n;
|
| 557 |
+
let g = '';
|
| 558 |
+
for (let k = lo; k <= hi; k++) {
|
| 559 |
+
const [jitter, jop] = jit[((k % n) + n) % n]; // ring k and ring k + n look alike, so the tile repeats cleanly
|
| 560 |
+
const w = Pe * (0.14 + jitter * 0.26);
|
| 561 |
+
const op = n01((0.4 + jop * 0.55) * Math.min(1.4, contrast / 1.2));
|
| 562 |
+
const rid = 'r' + id++;
|
| 563 |
+
defs += `<path id='${rid}' d='${ringPath(k + 0.8)}' fill='none'/>`;
|
| 564 |
+
g += `<use href='#${rid}' stroke='${rgba(late, op * 0.28)}' stroke-width='${r1(w * 2.6)}'/><use href='#${rid}' stroke='${rgba(late, op)}' stroke-width='${r1(w)}'/>`;
|
| 565 |
+
}
|
| 566 |
+
// knots: elongated along the grain, with rings around them
|
| 567 |
+
const knotN = spec.knots ? Math.max(0, Math.round(spec.knots / plankN + (pi === 0 ? 0.49 : 0))) : 0;
|
| 568 |
+
let knots = '';
|
| 569 |
+
for (let q = 0; q < knotN; q++) {
|
| 570 |
+
const kx = 90 + rand() * (W - 180), ky = h * (0.25 + rand() * 0.5), rx = 9 + rand() * 12, ry = rx * 0.6;
|
| 571 |
+
knots += `<ellipse cx='${r1(kx)}' cy='${r1(ky)}' rx='${r1(rx * 4)}' ry='${r1(ry * 3.4)}' fill='${rgba(late, 0.1)}'/>` +
|
| 572 |
+
`<ellipse cx='${r1(kx)}' cy='${r1(ky)}' rx='${r1(rx * 2.4)}' ry='${r1(ry * 2.3)}' fill='none' stroke='${rgba(late, 0.35)}' stroke-width='1.2'/>` +
|
| 573 |
+
`<ellipse cx='${r1(kx)}' cy='${r1(ky)}' rx='${r1(rx * 1.6)}' ry='${r1(ry * 1.6)}' fill='none' stroke='${rgba(late, 0.5)}' stroke-width='1.4'/>` +
|
| 574 |
+
`<ellipse cx='${r1(kx)}' cy='${r1(ky)}' rx='${r1(rx)}' ry='${r1(ry)}' fill='${rgba(late, 0.85)}'/>`;
|
| 575 |
+
}
|
| 576 |
+
const tone = (rand() - 0.5) * 0.14;
|
| 577 |
+
const toneRect = tone > 0 ? `<rect width='${W}' height='${r1(h)}' fill='rgba(255,255,255,${r1(Math.abs(tone) * 10) / 10})'/>` : `<rect width='${W}' height='${r1(h)}' fill='rgba(0,0,0,${r1(Math.abs(tone) * 10) / 10})'/>`;
|
| 578 |
+
const clip = `c${pi}`;
|
| 579 |
+
defs += `<clipPath id='${clip}'><rect width='${W}' height='${r1(h)}'/></clipPath>`;
|
| 580 |
+
body += `<g transform='translate(0 ${r1(pi * plankH)})' clip-path='url(#${clip})'>` +
|
| 581 |
+
`<rect width='${W}' height='${r1(h)}' fill='${rgba(early)}'/>` + (planked ? toneRect : '') + `<g fill='none'>${g}</g>${knots}</g>`;
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
// fine fibres: streaks stretched along the grain, drawn once over everything
|
| 585 |
+
const fibre = pores > 0
|
| 586 |
+
? `<filter id='p' x='0' y='0' width='100%' height='100%' color-interpolation-filters='sRGB'>` +
|
| 587 |
+
`<feTurbulence type='fractalNoise' baseFrequency='0.014 0.62' numOctaves='3' seed='${seed + 3}' stitchTiles='stitch'/>` +
|
| 588 |
+
`<feColorMatrix type='matrix' values='0 0 0 0 ${f(late.r / 255)} 0 0 0 0 ${f(late.g / 255)} 0 0 0 0 ${f(late.b / 255)} ${f(pores * 4.2)} 0 0 0 ${f(-pores * 1.95)}'/></filter>` +
|
| 589 |
+
`<rect width='${W}' height='${H}' filter='url(#p)' opacity='${f(Math.min(1, 0.55 * late.a))}'/>`
|
| 590 |
+
: '';
|
| 591 |
+
|
| 592 |
+
let seams = '';
|
| 593 |
+
if (planked) {
|
| 594 |
+
for (let pi = 0; pi < plankN; pi++) {
|
| 595 |
+
const y = pi * plankH;
|
| 596 |
+
seams += `<rect y='${r1(y)}' width='${W}' height='1.6' fill='rgba(0,0,0,0.5)'/><rect y='${r1(y + 1.6)}' width='${W}' height='1' fill='rgba(255,255,255,0.1)'/>`;
|
| 597 |
+
}
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
const rot = spec.angle === 90 || spec.angle === -90 || spec.angle === 270;
|
| 601 |
+
const inner = `<defs>${defs}</defs>${body}${fibre}${seams}`;
|
| 602 |
+
const svg = rot
|
| 603 |
+
? `<svg xmlns='http://www.w3.org/2000/svg' width='${H}' height='${W}'><g transform='matrix(0 1 1 0 0 0)'>${inner}</g></svg>`
|
| 604 |
+
: `<svg xmlns='http://www.w3.org/2000/svg' width='${W}' height='${H}'>${inner}</svg>`;
|
| 605 |
+
return [{ image: smallUrl(svg), size: rot ? `${H}px ${W}px` : `${W}px ${H}px`, repeat: 'repeat', position: '0 0', blend: spec.blend || 'normal', tile: Math.max(W, H) }];
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
const BUILDERS = { rings: buildRings, noise: buildNoise, bump: buildBump, lines: buildLines, dots: buildDots, grid: buildGrid, checker: buildChecker, weave: buildWeave, carbon: buildCarbon, waves: buildWaves, hex: buildHex };
|
| 609 |
|
| 610 |
/**
|
| 611 |
* Turn parsed layers into the CSS lists for background-image, -size, -repeat,
|
|
|
|
| 630 |
};
|
| 631 |
}
|
| 632 |
|
| 633 |
+
|
| 634 |
+
/* ------------------------------------------------------------------ *
|
| 635 |
+
* composing with the element's own background (shared by the engine and `dssl build`)
|
| 636 |
+
* ------------------------------------------------------------------ */
|
| 637 |
+
|
| 638 |
+
export function splitBgList(value) {
|
| 639 |
+
const out = []; let depth = 0, cur = '';
|
| 640 |
+
for (const ch of String(value)) {
|
| 641 |
+
if (ch === '(') depth++;
|
| 642 |
+
if (ch === ')') depth--;
|
| 643 |
+
if (ch === ',' && depth === 0) { out.push(cur.trim()); cur = ''; continue; }
|
| 644 |
+
cur += ch;
|
| 645 |
+
}
|
| 646 |
+
if (cur.trim()) out.push(cur.trim());
|
| 647 |
+
return out;
|
| 648 |
+
}
|
| 649 |
+
/** a list of exactly n entries: the author's, cycled the way CSS cycles them, or the default */
|
| 650 |
+
function expandList(value, n, dflt) {
|
| 651 |
+
const items = value ? splitBgList(value) : [];
|
| 652 |
+
if (!items.length) return Array.from({ length: n }, () => dflt);
|
| 653 |
+
return Array.from({ length: n }, (_, i) => items[i % items.length]);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
|
| 657 |
+
/**
|
| 658 |
+
* texture sits ON TOP of whatever background the author wrote: the shorthand, the longhands,
|
| 659 |
+
* or just a colour. `owned` is a Map of prop -> { value, imp }; this edits it in place.
|
| 660 |
+
*/
|
| 661 |
+
export function composeTextureInto(owned, tex) {
|
| 662 |
+
const sh = owned.get('background');
|
| 663 |
+
const img = owned.get('background-image');
|
| 664 |
+
const blendUser = owned.get('background-blend-mode');
|
| 665 |
+
const texBlends = tex.blend.split(', ');
|
| 666 |
+
if (sh) {
|
| 667 |
+
// the author wrote the shorthand: texture layers go first, their value stays the last layer(s)
|
| 668 |
+
const userLayers = splitBgList(sh.value).length;
|
| 669 |
+
const texShort = tex.parts.map((p) => `${p.image} ${p.position} / ${p.size} ${p.repeat}`).join(', ');
|
| 670 |
+
owned.set('background', { value: `${texShort}, ${sh.value}`, imp: sh.imp });
|
| 671 |
+
owned.delete('background-image');
|
| 672 |
+
owned.set('background-blend-mode', { value: [...texBlends, ...expandList(blendUser && blendUser.value, userLayers, 'normal')].join(', '), imp: sh.imp });
|
| 673 |
+
return;
|
| 674 |
+
}
|
| 675 |
+
const userLayers = img ? splitBgList(img.value).length : 0;
|
| 676 |
+
const imp = img ? img.imp : '';
|
| 677 |
+
const get = (name) => (owned.get(name) || {}).value;
|
| 678 |
+
const join = (texList, name, dflt) => [...texList.split(/,(?![^(]*\))/).map((x) => x.trim()), ...(userLayers ? expandList(get(name), userLayers, dflt) : [])].join(', ');
|
| 679 |
+
owned.set('background-image', { value: tex.image + (img ? ', ' + img.value : ''), imp });
|
| 680 |
+
owned.set('background-size', { value: join(tex.size, 'background-size', 'auto'), imp });
|
| 681 |
+
owned.set('background-position', { value: join(tex.position, 'background-position', '0% 0%'), imp });
|
| 682 |
+
owned.set('background-repeat', { value: join(tex.repeat, 'background-repeat', 'repeat'), imp });
|
| 683 |
+
owned.set('background-blend-mode', { value: [...texBlends, ...(userLayers ? expandList(get('background-blend-mode'), userLayers, 'normal') : [])].join(', '), imp });
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
/* ------------------------------------------------------------------ *
|
| 687 |
* deterministic value noise, for aura masks and tests
|
| 688 |
* ------------------------------------------------------------------ */
|
dssl-vscode/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
"name": "dssl-vscode",
|
| 3 |
"displayName": "DSSL Language Support",
|
| 4 |
"description": "The DSSL style language in your editor: syntax highlighting, real-compiler diagnostics (the same errors the browser console shows), completions from the live token registry and for textures, auras and sliders, hover docs, an interactive designer with sliders and live previews for aura, texture and range sliders, and a CSS to DSSL converter. Works in VS Code and VSCodium.",
|
| 5 |
-
"version": "0.
|
| 6 |
"publisher": "ehs-studio",
|
| 7 |
"icon": "icon.png",
|
| 8 |
"license": "MIT",
|
|
|
|
| 2 |
"name": "dssl-vscode",
|
| 3 |
"displayName": "DSSL Language Support",
|
| 4 |
"description": "The DSSL style language in your editor: syntax highlighting, real-compiler diagnostics (the same errors the browser console shows), completions from the live token registry and for textures, auras and sliders, hover docs, an interactive designer with sliders and live previews for aura, texture and range sliders, and a CSS to DSSL converter. Works in VS Code and VSCodium.",
|
| 5 |
+
"version": "0.11.0",
|
| 6 |
"publisher": "ehs-studio",
|
| 7 |
"icon": "icon.png",
|
| 8 |
"license": "MIT",
|
llms.txt
CHANGED
|
@@ -50,7 +50,7 @@ DSSL.replay(sel) / DSSL.refresh() / DSSL.errors() / DSSL.version
|
|
| 50 |
```
|
| 51 |
|
| 52 |
## 0.9.0: texture, aura, sliders, lint, import
|
| 53 |
-
- `texture: grain` layers a generated image ON TOP of an element's own background (colour, gradient or several layers).
|
| 54 |
- Aura has NO presets. `aura-glow: <color> <radius> [intensity|pulse|depth|flicker|texture(...)|seed|essential]`, `aura-particle: <color -> color ...> <count> [size|life|speed|dir|spread|gravity|drag|turbulence|from|shape|grow|glowing|behind|blend|texture(...)|seed|essential]`, `glow-particle` (particles that glow), `aura-glow-particle` (both slots), `aura: glow ..., particles ...` (replaces both; `aura: none` clears both, `aura-glow: none` only the glow). Name your own with `behavior fire { ... }` then `use fire`. One canvas over the page, 1800 particle budget, still glow under reduced motion, runtime-only.
|
| 55 |
- `slider: track 6 color.line radius full, fill color.accent, thumb 24 color.paper, ring 3 color.accent, glow color.accent 14, ticks 10, graph 20 45 70` writes the ::-webkit-slider-* and ::-moz-range-* rules together for `input[type=range]`. The engine keeps `--dssl-p` (value 0..1) on the input. Firefox rules are generated but untested.
|
| 56 |
- Lint warnings (never errors; `dssl check --strict` makes them fatal): text contrast under 4.5:1 (3:1 for large text), a removed focus outline with no focus-visible replacement, tap targets under 24px.
|
|
@@ -63,6 +63,14 @@ DSSL.replay(sel) / DSSL.refresh() / DSSL.errors() / DSSL.version
|
|
| 63 |
- Alpha on a named colour works: `white/12`, `currentColor/20`. It used to compile to `white/12px`.
|
| 64 |
- The playground has a canvas.html tab, `?example=` links, lint warnings and an "as CSS" tab. The canvas keeps a short allow-list of tags and drops scripts, handlers, styles and ids.
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
## CLI (v0.8.0)
|
| 67 |
|
| 68 |
```sh
|
|
@@ -76,4 +84,4 @@ Wait for the `dssl-ready` event on `window` before measuring layout: the engine
|
|
| 76 |
|
| 77 |
## Editor support
|
| 78 |
|
| 79 |
-
The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. In VSCodium, search "DSSL" in the Extensions tab (it is on Open VSX: https://open-vsx.org/extension/ehs-studio/dssl-vscode). In VS Code, download `dssl-vscode/dssl-vscode-0.
|
|
|
|
| 50 |
```
|
| 51 |
|
| 52 |
## 0.9.0: texture, aura, sliders, lint, import
|
| 53 |
+
- `texture: grain` layers a generated image ON TOP of an element's own background (colour, gradient or several layers). 40 presets (30 general: grain, noise, rough, sand, paper, cloud, concrete, stone, crumpled, glitter, frost, marble, wood, bark, leather, brushed, canvas, linen, denim, carbon, scanlines, halftone, dots, lines, grid, checker, crosshatch, stripes, waves, hex) and 11 generators (noise, bump, lines, dots, grid, checker, weave, carbon, waves, hex, rings) with settings: scale, figure, warp, pores, knots, planks, intensity, contrast, threshold, octaves, seed, stretch x y, depth, light, angle, thickness, radius, softness, contour, animate, type, blend, color, color2, invert. Comma stacks layers. Built at run time, and baked into the CSS by `dssl build` when its colours are known (0.11.0).
|
| 54 |
- Aura has NO presets. `aura-glow: <color> <radius> [intensity|pulse|depth|flicker|texture(...)|seed|essential]`, `aura-particle: <color -> color ...> <count> [size|life|speed|dir|spread|gravity|drag|turbulence|from|shape|grow|glowing|behind|blend|texture(...)|seed|essential]`, `glow-particle` (particles that glow), `aura-glow-particle` (both slots), `aura: glow ..., particles ...` (replaces both; `aura: none` clears both, `aura-glow: none` only the glow). Name your own with `behavior fire { ... }` then `use fire`. One canvas over the page, 1800 particle budget, still glow under reduced motion, runtime-only.
|
| 55 |
- `slider: track 6 color.line radius full, fill color.accent, thumb 24 color.paper, ring 3 color.accent, glow color.accent 14, ticks 10, graph 20 45 70` writes the ::-webkit-slider-* and ::-moz-range-* rules together for `input[type=range]`. The engine keeps `--dssl-p` (value 0..1) on the input. Firefox rules are generated but untested.
|
| 56 |
- Lint warnings (never errors; `dssl check --strict` makes them fatal): text contrast under 4.5:1 (3:1 for large text), a removed focus outline with no focus-visible replacement, tap targets under 24px.
|
|
|
|
| 63 |
- Alpha on a named colour works: `white/12`, `currentColor/20`. It used to compile to `white/12px`.
|
| 64 |
- The playground has a canvas.html tab, `?example=` links, lint warnings and an "as CSS" tab. The canvas keeps a short allow-list of tags and drops scripts, handlers, styles and ids.
|
| 65 |
|
| 66 |
+
## 0.11.0: wood and the build
|
| 67 |
+
- Ten wood species presets: oak, walnut, cherry, maple, pine, mahogany, ebony, birch, oak-planks, walnut-planks. The `rings` generator draws growth rings as vector curves (not stretched noise): `texture: oak figure 0.8 knots 2 planks 120`. Settings: scale (ring spacing), color (latewood), color2 (earlywood), contrast, figure (cathedral arching), warp, pores, knots, planks (board height px), seed, angle 0 or 90. Same seed, same board everywhere; the tile repeats without a seam.
|
| 68 |
+
- `dssl build` now BAKES textures into the CSS as background images when every colour is known at build time. Animated textures and unknown colours stay in the runtime, and the report says why.
|
| 69 |
+
- `counter-reset`, `counter-increment`, `counter-set` and `content: counter(name)` / `counters(name, ".")` work.
|
| 70 |
+
- The contrast lint no longer cries wolf on translucent backgrounds: it warns only when the pair fails on the light page colour, white and black.
|
| 71 |
+
- A page can hide itself until styled: `<style>html:not(.dssl-ready) body{visibility:hidden}</style>`; the engine adds `dssl-ready` to `<html>`.
|
| 72 |
+
- Showcase: /showcase/wood/ is a whole page (Heartwood) built with these.
|
| 73 |
+
|
| 74 |
## CLI (v0.8.0)
|
| 75 |
|
| 76 |
```sh
|
|
|
|
| 84 |
|
| 85 |
## Editor support
|
| 86 |
|
| 87 |
+
The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. In VSCodium, search "DSSL" in the Extensions tab (it is on Open VSX: https://open-vsx.org/extension/ehs-studio/dssl-vscode). In VS Code, download `dssl-vscode/dssl-vscode-0.11.0.vsix` from the repo files, then Extensions panel -> ... -> Install from VSIX. Source lives in `dssl-vscode/` in the repo.
|
next.config.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type { NextConfig } from "next";
|
|
| 2 |
|
| 3 |
const nextConfig: NextConfig = {
|
| 4 |
reactStrictMode: false,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
};
|
| 6 |
|
| 7 |
export default nextConfig;
|
|
|
|
| 2 |
|
| 3 |
const nextConfig: NextConfig = {
|
| 4 |
reactStrictMode: false,
|
| 5 |
+
// the wood showcase is a plain static page that lives in public/
|
| 6 |
+
async rewrites() {
|
| 7 |
+
return [{ source: "/showcase/wood", destination: "/showcase/wood/index.html" }];
|
| 8 |
+
},
|
| 9 |
};
|
| 10 |
|
| 11 |
export default nextConfig;
|
package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"name": "dssl",
|
| 3 |
-
"version": "0.
|
| 4 |
"private": true,
|
| 5 |
"description": "The DSSL style language and the website that is written in it.",
|
| 6 |
"scripts": {
|
|
|
|
| 1 |
{
|
| 2 |
"name": "dssl",
|
| 3 |
+
"version": "0.11.0",
|
| 4 |
"private": true,
|
| 5 |
"description": "The DSSL style language and the website that is written in it.",
|
| 6 |
"scripts": {
|
public/dssl/engine.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
| 21 |
|
| 22 |
import { composeDeform, arcLayout } from './utils.js';
|
| 23 |
// 0.9.0: texture layers, aura (light and particles) and the range-slider helper
|
| 24 |
-
import { buildTexture } from './texture.js';
|
| 25 |
import { AuraManager, resolveLayer } from './aura.js';
|
| 26 |
import { parseColor } from './color.js';
|
| 27 |
import { tiltTransform } from './effects.js';
|
|
@@ -142,24 +142,6 @@ function parseSizeQuery(q) {
|
|
| 142 |
}
|
| 143 |
|
| 144 |
/** split a CSS list at top-level commas, so rgba(1,2,3) and var(--x, 4) stay whole */
|
| 145 |
-
function splitList(value) {
|
| 146 |
-
const out = []; let depth = 0, cur = '';
|
| 147 |
-
for (const ch of String(value)) {
|
| 148 |
-
if (ch === '(') depth++;
|
| 149 |
-
if (ch === ')') depth--;
|
| 150 |
-
if (ch === ',' && depth === 0) { out.push(cur.trim()); cur = ''; continue; }
|
| 151 |
-
cur += ch;
|
| 152 |
-
}
|
| 153 |
-
if (cur.trim()) out.push(cur.trim());
|
| 154 |
-
return out;
|
| 155 |
-
}
|
| 156 |
-
/** a list of exactly n entries: the author's, cycled the way CSS cycles them, or the default */
|
| 157 |
-
function expandList(value, n, dflt) {
|
| 158 |
-
const items = value ? splitList(value) : [];
|
| 159 |
-
if (!items.length) return Array.from({ length: n }, () => dflt);
|
| 160 |
-
return Array.from({ length: n }, (_, i) => items[i % items.length]);
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
export class DsslEngine {
|
| 164 |
constructor({ scope = null, name = 'global', reset = false } = {}) {
|
| 165 |
this.name = name;
|
|
@@ -258,28 +240,7 @@ export class DsslEngine {
|
|
| 258 |
this._texCache.set(key, tex);
|
| 259 |
}
|
| 260 |
if (!tex.count) return null;
|
| 261 |
-
|
| 262 |
-
const img = owned.get('background-image');
|
| 263 |
-
const blendUser = owned.get('background-blend-mode');
|
| 264 |
-
const texBlends = tex.blend.split(', ');
|
| 265 |
-
if (sh) {
|
| 266 |
-
// the author wrote the shorthand: texture layers go first, their value stays the last layer(s)
|
| 267 |
-
const userLayers = splitList(sh.value).length;
|
| 268 |
-
const texShort = tex.parts.map((p) => `${p.image} ${p.position} / ${p.size} ${p.repeat}`).join(', ');
|
| 269 |
-
owned.set('background', { value: `${texShort}, ${sh.value}`, imp: sh.imp });
|
| 270 |
-
owned.delete('background-image');
|
| 271 |
-
owned.set('background-blend-mode', { value: [...texBlends, ...expandList(blendUser && blendUser.value, userLayers, 'normal')].join(', '), imp: sh.imp });
|
| 272 |
-
return tex;
|
| 273 |
-
}
|
| 274 |
-
const userLayers = img ? splitList(img.value).length : 0;
|
| 275 |
-
const imp = img ? img.imp : '';
|
| 276 |
-
const get = (name) => (owned.get(name) || {}).value;
|
| 277 |
-
const join = (texList, name, dflt) => [...texList.split(/,(?![^(]*\))/).map((x) => x.trim()), ...(userLayers ? expandList(get(name), userLayers, dflt) : [])].join(', ');
|
| 278 |
-
owned.set('background-image', { value: tex.image + (img ? ', ' + img.value : ''), imp });
|
| 279 |
-
owned.set('background-size', { value: join(tex.size, 'background-size', 'auto'), imp });
|
| 280 |
-
owned.set('background-position', { value: join(tex.position, 'background-position', '0% 0%'), imp });
|
| 281 |
-
owned.set('background-repeat', { value: join(tex.repeat, 'background-repeat', 'repeat'), imp });
|
| 282 |
-
owned.set('background-blend-mode', { value: [...texBlends, ...(userLayers ? expandList(get('background-blend-mode'), userLayers, 'normal') : [])].join(', '), imp });
|
| 283 |
return tex;
|
| 284 |
}
|
| 285 |
|
|
|
|
| 21 |
|
| 22 |
import { composeDeform, arcLayout } from './utils.js';
|
| 23 |
// 0.9.0: texture layers, aura (light and particles) and the range-slider helper
|
| 24 |
+
import { buildTexture, composeTextureInto, splitBgList as splitList } from './texture.js';
|
| 25 |
import { AuraManager, resolveLayer } from './aura.js';
|
| 26 |
import { parseColor } from './color.js';
|
| 27 |
import { tiltTransform } from './effects.js';
|
|
|
|
| 142 |
}
|
| 143 |
|
| 144 |
/** split a CSS list at top-level commas, so rgba(1,2,3) and var(--x, 4) stay whole */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
export class DsslEngine {
|
| 146 |
constructor({ scope = null, name = 'global', reset = false } = {}) {
|
| 147 |
this.name = name;
|
|
|
|
| 240 |
this._texCache.set(key, tex);
|
| 241 |
}
|
| 242 |
if (!tex.count) return null;
|
| 243 |
+
composeTextureInto(owned, tex);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
return tex;
|
| 245 |
}
|
| 246 |
|
public/dssl/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import { resolve, DEFAULT_TOKENS } from './resolver.js';
|
|
| 19 |
import { toCSS as docToCSS } from './static.js';
|
| 20 |
import { DsslEngine } from './engine.js';
|
| 21 |
|
| 22 |
-
export const VERSION = '0.
|
| 23 |
|
| 24 |
let counter = 0;
|
| 25 |
const engines = new Map();
|
|
@@ -240,6 +240,8 @@ if (typeof window !== 'undefined') {
|
|
| 240 |
} finally {
|
| 241 |
// always announce readiness - even when there was nothing to auto-load
|
| 242 |
window.dispatchEvent(new CustomEvent('dssl-ready', { detail: { version: VERSION } }));
|
|
|
|
|
|
|
| 243 |
}
|
| 244 |
};
|
| 245 |
|
|
|
|
| 19 |
import { toCSS as docToCSS } from './static.js';
|
| 20 |
import { DsslEngine } from './engine.js';
|
| 21 |
|
| 22 |
+
export const VERSION = '0.11.0';
|
| 23 |
|
| 24 |
let counter = 0;
|
| 25 |
const engines = new Map();
|
|
|
|
| 240 |
} finally {
|
| 241 |
// always announce readiness - even when there was nothing to auto-load
|
| 242 |
window.dispatchEvent(new CustomEvent('dssl-ready', { detail: { version: VERSION } }));
|
| 243 |
+
// 0.10.0: a page that hides itself until styled (see the guide, "No flash") shows itself here
|
| 244 |
+
try { document.documentElement.classList.add('dssl-ready'); } catch { /* no document element yet */ }
|
| 245 |
}
|
| 246 |
};
|
| 247 |
|
public/dssl/lint.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
| 18 |
import { parseColor, evalColorExpr, over, contrast, toRgba, toHex } from './color.js';
|
| 19 |
|
| 20 |
/** one design-token lookup closed over the sheet's token map */
|
| 21 |
-
function
|
| 22 |
const lookup = (name) => tokenMap.get(name);
|
| 23 |
/** CSS value as the resolver emits it ("var(--dssl-color-ink)", "color-mix(...)", "#fff") to a colour */
|
| 24 |
return function cssToColor(css, depth = 0) {
|
|
@@ -52,7 +52,7 @@ const INTERACTIVE = /^(a|button|input|select|textarea|summary)\b|\[tabindex|\[ro
|
|
| 52 |
*/
|
| 53 |
export function lintRules({ rules, tokenMap, file }) {
|
| 54 |
const warnings = [];
|
| 55 |
-
const cssToColor =
|
| 56 |
const pageBg = (() => { const raw = tokenMap.get('color.paper'); return raw ? evalColorExpr(raw, (n) => tokenMap.get(n)) : null; })();
|
| 57 |
|
| 58 |
const killedFocus = [];
|
|
@@ -72,10 +72,21 @@ export function lintRules({ rules, tokenMap, file }) {
|
|
| 72 |
let bg = cssToColor(bgD.value);
|
| 73 |
let fg = cssToColor(fgD.value);
|
| 74 |
if (bg && fg) {
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
const size = pxOf((lastDecl(r.decls, 'font-size') || {}).value);
|
| 80 |
const weightRaw = (lastDecl(r.decls, 'font-weight') || {}).value;
|
| 81 |
const weight = weightRaw === 'bold' ? 700 : Number(weightRaw) || 400;
|
|
|
|
| 18 |
import { parseColor, evalColorExpr, over, contrast, toRgba, toHex } from './color.js';
|
| 19 |
|
| 20 |
/** one design-token lookup closed over the sheet's token map */
|
| 21 |
+
export function makeColorResolver(tokenMap) {
|
| 22 |
const lookup = (name) => tokenMap.get(name);
|
| 23 |
/** CSS value as the resolver emits it ("var(--dssl-color-ink)", "color-mix(...)", "#fff") to a colour */
|
| 24 |
return function cssToColor(css, depth = 0) {
|
|
|
|
| 52 |
*/
|
| 53 |
export function lintRules({ rules, tokenMap, file }) {
|
| 54 |
const warnings = [];
|
| 55 |
+
const cssToColor = makeColorResolver(tokenMap);
|
| 56 |
const pageBg = (() => { const raw = tokenMap.get('color.paper'); return raw ? evalColorExpr(raw, (n) => tokenMap.get(n)) : null; })();
|
| 57 |
|
| 58 |
const killedFocus = [];
|
|
|
|
| 72 |
let bg = cssToColor(bgD.value);
|
| 73 |
let fg = cssToColor(fgD.value);
|
| 74 |
if (bg && fg) {
|
| 75 |
+
// a translucent background takes its look from whatever is behind it, which a single rule cannot know
|
| 76 |
+
// (a dark theme is usually in a media block, and this check only reads the unconditional rules). So judge
|
| 77 |
+
// it against the light page colour, white and black, and warn only when it fails on every one of them.
|
| 78 |
+
const backdrops = bg.a < 0.999 ? [pageBg ? { ...pageBg, a: 1 } : null, { r: 255, g: 255, b: 255, a: 1 }, { r: 0, g: 0, b: 0, a: 1 }].filter(Boolean) : [null];
|
| 79 |
+
let bestRatio = -1, bestFg = null, bestBg = null;
|
| 80 |
+
for (const back of backdrops) {
|
| 81 |
+
const solidBg = back ? over(bg, back) : bg;
|
| 82 |
+
const solidFg = fg.a < 0.999 ? over(fg, solidBg) : fg;
|
| 83 |
+
const rr = contrast(solidFg, solidBg);
|
| 84 |
+
if (rr > bestRatio) { bestRatio = rr; bestFg = solidFg; bestBg = solidBg; }
|
| 85 |
+
}
|
| 86 |
+
if (bestBg) {
|
| 87 |
+
const fgSolid = bestFg;
|
| 88 |
+
bg = bestBg;
|
| 89 |
+
const ratio = bestRatio;
|
| 90 |
const size = pxOf((lastDecl(r.decls, 'font-size') || {}).value);
|
| 91 |
const weightRaw = (lastDecl(r.decls, 'font-weight') || {}).value;
|
| 92 |
const weight = weightRaw === 'bold' ? 700 : Number(weightRaw) || 400;
|
public/dssl/resolver.js
CHANGED
|
@@ -80,7 +80,7 @@ export const UNITLESS_PROPS = new Set([
|
|
| 80 |
'opacity', 'line-height', 'z-index', 'font-weight', 'flex-grow', 'flex-shrink',
|
| 81 |
'flex', 'order', 'scale', 'widows', 'orphans', 'column-count', 'aspect-ratio',
|
| 82 |
'animation-iteration-count', 'flood-opacity', 'fill-opacity', 'stop-opacity',
|
| 83 |
-
'shape-image-threshold',
|
| 84 |
// grid line placement takes integers, not lengths: grid-column: span 2
|
| 85 |
'grid-row', 'grid-column', 'grid-area',
|
| 86 |
]);
|
|
@@ -106,7 +106,7 @@ export const KNOWN_PROPS = new Set(('all,appearance,accent-color,align-content,a
|
|
| 106 |
'border-inline,border-inline-color,border-inline-end,border-inline-end-color,border-inline-end-style,border-inline-end-width,border-inline-start,border-inline-start-color,border-inline-start-style,border-inline-start-width,border-inline-style,border-inline-width,' +
|
| 107 |
'border-left,border-left-color,border-left-style,border-left-width,border-radius,border-right,border-right-color,border-right-style,border-right-width,' +
|
| 108 |
'border-spacing,border-start-end-radius,border-start-start-radius,border-style,border-top,border-top-color,border-top-left-radius,border-top-right-radius,border-top-style,border-top-width,border-width,' +
|
| 109 |
-
'bottom,box-decoration-break,box-shadow,box-sizing,break-after,break-before,break-inside,caret-color,caret-shape,clear,clip,clip-path,color,color-scheme,column-count,column-fill,column-gap,column-rule,column-rule-color,column-rule-style,column-rule-width,column-span,column-width,columns,contain,contain-intrinsic-size,container,container-name,container-type,content,content-visibility,cursor,caret,display,empty-cells,filter,flex,flex-basis,flex-direction,flex-flow,flex-grow,flex-shrink,flex-wrap,float,fill,fill-rule,fill-opacity,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-opacity,stroke-width,font,font-family,font-feature-settings,font-kerning,font-language-override,font-optical-sizing,font-palette,font-size,font-size-adjust,font-stretch,font-style,font-synthesis,font-variant,font-variant-caps,font-variant-east-asian,font-variant-ligatures,font-variant-numeric,font-variant-position,font-variation-settings,font-weight,forced-color-adjust,gap,grid,grid-area,grid-auto-columns,grid-auto-flow,grid-auto-rows,grid-column,grid-column-end,grid-column-gap,grid-column-start,grid-gap,grid-row,grid-row-end,grid-row-gap,grid-row-start,grid-template,grid-template-areas,grid-template-columns,grid-template-rows,' +
|
| 110 |
'hanging-punctuation,height,hyphenate-character,hyphenate-limit-chars,hyphens,image-orientation,image-rendering,inline-size,inset,inset-block,inset-block-end,inset-block-start,inset-inline,inset-inline-end,inset-inline-start,isolation,justify-content,justify-items,justify-self,justify-tracks,left,letter-spacing,line-break,line-height,list-style,list-style-image,list-style-position,list-style-type,margin,margin-block,margin-block-end,margin-block-start,margin-bottom,margin-inline,margin-inline-end,margin-inline-start,margin-left,margin-right,margin-top,margin-trim,mask,mask-border,mask-border-mode,mask-border-outset,mask-border-repeat,mask-border-slice,mask-border-source,mask-border-width,mask-clip,mask-composite,mask-image,mask-mode,mask-origin,mask-position,mask-repeat,mask-size,max-height,max-inline-size,max-width,min-block-size,min-height,min-inline-size,min-width,mix-blend-mode,object-fit,object-position,offset,offset-anchor,offset-distance,offset-path,offset-rotate,opacity,order,outline,outline-color,outline-offset,outline-style,outline-width,overflow,overflow-anchor,overflow-clip-margin,overflow-wrap,overflow-x,overflow-y,overscroll-behavior,overscroll-behavior-block,overscroll-behavior-inline,overscroll-behavior-x,overscroll-behavior-y,padding,padding-block,padding-block-end,padding-block-start,padding-bottom,padding-inline,padding-inline-end,padding-inline-start,padding-left,padding-right,padding-top,page-break-after,page-break-before,page-break-inside,paint-order,perspective,perspective-origin,place-content,place-items,place-self,pointer-events,position,print-color-adjust,quotes,resize,right,rotate,row-gap,ruby-align,ruby-merge,scale,scroll-behavior,scroll-margin,scroll-margin-block,scroll-margin-block-end,scroll-margin-block-start,scroll-margin-bottom,scroll-margin-inline,scroll-margin-inline-end,scroll-margin-inline-start,scroll-margin-left,scroll-margin-right,scroll-margin-top,scroll-padding,scroll-padding-block,scroll-padding-bottom,scroll-padding-inline,scroll-padding-left,scroll-padding-right,scroll-padding-top,scroll-snap-align,scroll-snap-stop,scroll-snap-type,scrollbar-color,scrollbar-gutter,scrollbar-width,shape-image-threshold,shape-margin,shape-outside,tab-size,table-layout,text-align,text-align-last,text-combine-upright,text-decoration,text-decoration-color,text-decoration-line,text-decoration-skip-ink,text-decoration-style,text-decoration-thickness,text-emphasis,text-emphasis-color,text-emphasis-position,text-emphasis-style,text-indent,text-justify,text-orientation,text-overflow,text-rendering,text-shadow,text-transform,text-underline-offset,text-underline-position,text-wrap,top,touch-action,transform,transform-box,transform-origin,transform-style,transition,transition-behavior,transition-delay,transition-duration,transition-property,transition-timing-function,translate,unicode-bidi,user-select,vertical-align,visibility,white-space,white-space-collapse,width,will-change,word-break,word-spacing,word-wrap,writing-mode,z-index,zoom,' +
|
| 111 |
'animation,animation-composition,animation-delay,animation-direction,animation-duration,animation-fill-mode,animation-iteration-count,animation-name,animation-play-state,animation-range,animation-timeline,animation-timing-function,' +
|
| 112 |
'view-transition-name,interpolate-size,text-box,text-box-edge,text-box-trim,dynamic-range-limit,field-sizing,anchor-name,position-anchor,position-area,scroll-timeline,scroll-timeline-axis,scroll-timeline-name,timeline-scope,view-timeline,view-timeline-axis,view-timeline-name,' +
|
|
@@ -268,7 +268,9 @@ function resolveValue(raw, ctx) {
|
|
| 268 |
}
|
| 269 |
// color-ish functions keep bare numbers bare; calc-family gets px
|
| 270 |
const innerUnitless = UNITLESS_FN.has(fname) ? true : LENGTH_FN.has(fname) ? false : ctx.unitless;
|
| 271 |
-
|
|
|
|
|
|
|
| 272 |
out += ident + '(' + resolvedInner + ')';
|
| 273 |
i = j;
|
| 274 |
continue;
|
|
@@ -1136,7 +1138,7 @@ function resolveDecl(prop, value, ctx) {
|
|
| 1136 |
});
|
| 1137 |
return [];
|
| 1138 |
}
|
| 1139 |
-
return [{ prop, value: resolveValue(value, { ...ctx2, unitless: UNITLESS_PROPS.has(prop), bareIdents: prop === 'font-family' }) }];
|
| 1140 |
}
|
| 1141 |
|
| 1142 |
/** string-aware !important scan - quoted content: "!important" must not trip it */
|
|
|
|
| 80 |
'opacity', 'line-height', 'z-index', 'font-weight', 'flex-grow', 'flex-shrink',
|
| 81 |
'flex', 'order', 'scale', 'widows', 'orphans', 'column-count', 'aspect-ratio',
|
| 82 |
'animation-iteration-count', 'flood-opacity', 'fill-opacity', 'stop-opacity',
|
| 83 |
+
'shape-image-threshold', 'counter-reset', 'counter-increment', 'counter-set',
|
| 84 |
// grid line placement takes integers, not lengths: grid-column: span 2
|
| 85 |
'grid-row', 'grid-column', 'grid-area',
|
| 86 |
]);
|
|
|
|
| 106 |
'border-inline,border-inline-color,border-inline-end,border-inline-end-color,border-inline-end-style,border-inline-end-width,border-inline-start,border-inline-start-color,border-inline-start-style,border-inline-start-width,border-inline-style,border-inline-width,' +
|
| 107 |
'border-left,border-left-color,border-left-style,border-left-width,border-radius,border-right,border-right-color,border-right-style,border-right-width,' +
|
| 108 |
'border-spacing,border-start-end-radius,border-start-start-radius,border-style,border-top,border-top-color,border-top-left-radius,border-top-right-radius,border-top-style,border-top-width,border-width,' +
|
| 109 |
+
'bottom,counter-reset,counter-increment,counter-set,box-decoration-break,box-shadow,box-sizing,break-after,break-before,break-inside,caret-color,caret-shape,clear,clip,clip-path,color,color-scheme,column-count,column-fill,column-gap,column-rule,column-rule-color,column-rule-style,column-rule-width,column-span,column-width,columns,contain,contain-intrinsic-size,container,container-name,container-type,content,content-visibility,cursor,caret,display,empty-cells,filter,flex,flex-basis,flex-direction,flex-flow,flex-grow,flex-shrink,flex-wrap,float,fill,fill-rule,fill-opacity,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-opacity,stroke-width,font,font-family,font-feature-settings,font-kerning,font-language-override,font-optical-sizing,font-palette,font-size,font-size-adjust,font-stretch,font-style,font-synthesis,font-variant,font-variant-caps,font-variant-east-asian,font-variant-ligatures,font-variant-numeric,font-variant-position,font-variation-settings,font-weight,forced-color-adjust,gap,grid,grid-area,grid-auto-columns,grid-auto-flow,grid-auto-rows,grid-column,grid-column-end,grid-column-gap,grid-column-start,grid-gap,grid-row,grid-row-end,grid-row-gap,grid-row-start,grid-template,grid-template-areas,grid-template-columns,grid-template-rows,' +
|
| 110 |
'hanging-punctuation,height,hyphenate-character,hyphenate-limit-chars,hyphens,image-orientation,image-rendering,inline-size,inset,inset-block,inset-block-end,inset-block-start,inset-inline,inset-inline-end,inset-inline-start,isolation,justify-content,justify-items,justify-self,justify-tracks,left,letter-spacing,line-break,line-height,list-style,list-style-image,list-style-position,list-style-type,margin,margin-block,margin-block-end,margin-block-start,margin-bottom,margin-inline,margin-inline-end,margin-inline-start,margin-left,margin-right,margin-top,margin-trim,mask,mask-border,mask-border-mode,mask-border-outset,mask-border-repeat,mask-border-slice,mask-border-source,mask-border-width,mask-clip,mask-composite,mask-image,mask-mode,mask-origin,mask-position,mask-repeat,mask-size,max-height,max-inline-size,max-width,min-block-size,min-height,min-inline-size,min-width,mix-blend-mode,object-fit,object-position,offset,offset-anchor,offset-distance,offset-path,offset-rotate,opacity,order,outline,outline-color,outline-offset,outline-style,outline-width,overflow,overflow-anchor,overflow-clip-margin,overflow-wrap,overflow-x,overflow-y,overscroll-behavior,overscroll-behavior-block,overscroll-behavior-inline,overscroll-behavior-x,overscroll-behavior-y,padding,padding-block,padding-block-end,padding-block-start,padding-bottom,padding-inline,padding-inline-end,padding-inline-start,padding-left,padding-right,padding-top,page-break-after,page-break-before,page-break-inside,paint-order,perspective,perspective-origin,place-content,place-items,place-self,pointer-events,position,print-color-adjust,quotes,resize,right,rotate,row-gap,ruby-align,ruby-merge,scale,scroll-behavior,scroll-margin,scroll-margin-block,scroll-margin-block-end,scroll-margin-block-start,scroll-margin-bottom,scroll-margin-inline,scroll-margin-inline-end,scroll-margin-inline-start,scroll-margin-left,scroll-margin-right,scroll-margin-top,scroll-padding,scroll-padding-block,scroll-padding-bottom,scroll-padding-inline,scroll-padding-left,scroll-padding-right,scroll-padding-top,scroll-snap-align,scroll-snap-stop,scroll-snap-type,scrollbar-color,scrollbar-gutter,scrollbar-width,shape-image-threshold,shape-margin,shape-outside,tab-size,table-layout,text-align,text-align-last,text-combine-upright,text-decoration,text-decoration-color,text-decoration-line,text-decoration-skip-ink,text-decoration-style,text-decoration-thickness,text-emphasis,text-emphasis-color,text-emphasis-position,text-emphasis-style,text-indent,text-justify,text-orientation,text-overflow,text-rendering,text-shadow,text-transform,text-underline-offset,text-underline-position,text-wrap,top,touch-action,transform,transform-box,transform-origin,transform-style,transition,transition-behavior,transition-delay,transition-duration,transition-property,transition-timing-function,translate,unicode-bidi,user-select,vertical-align,visibility,white-space,white-space-collapse,width,will-change,word-break,word-spacing,word-wrap,writing-mode,z-index,zoom,' +
|
| 111 |
'animation,animation-composition,animation-delay,animation-direction,animation-duration,animation-fill-mode,animation-iteration-count,animation-name,animation-play-state,animation-range,animation-timeline,animation-timing-function,' +
|
| 112 |
'view-transition-name,interpolate-size,text-box,text-box-edge,text-box-trim,dynamic-range-limit,field-sizing,anchor-name,position-anchor,position-area,scroll-timeline,scroll-timeline-axis,scroll-timeline-name,timeline-scope,view-timeline,view-timeline-axis,view-timeline-name,' +
|
|
|
|
| 268 |
}
|
| 269 |
// color-ish functions keep bare numbers bare; calc-family gets px
|
| 270 |
const innerUnitless = UNITLESS_FN.has(fname) ? true : LENGTH_FN.has(fname) ? false : ctx.unitless;
|
| 271 |
+
// counter(step), counters(step, "."), attr(data-x): the names inside are the author's own, not tokens
|
| 272 |
+
const nameFn = fname === 'counter' || fname === 'counters' || fname === 'attr';
|
| 273 |
+
const resolvedInner = resolveValue(inner, { ...ctx, unitless: innerUnitless, bareIdents: nameFn ? true : ctx.bareIdents });
|
| 274 |
out += ident + '(' + resolvedInner + ')';
|
| 275 |
i = j;
|
| 276 |
continue;
|
|
|
|
| 1138 |
});
|
| 1139 |
return [];
|
| 1140 |
}
|
| 1141 |
+
return [{ prop, value: resolveValue(value, { ...ctx2, unitless: UNITLESS_PROPS.has(prop), bareIdents: prop === 'font-family' || prop.startsWith('counter-') }) }];
|
| 1142 |
}
|
| 1143 |
|
| 1144 |
/** string-aware !important scan - quoted content: "!important" must not trip it */
|
public/dssl/static.js
CHANGED
|
@@ -20,6 +20,9 @@
|
|
| 20 |
* no-JS, a strict CSP, first-paint-before-JS, or 4.5x smaller HTML.
|
| 21 |
* ========================================================================= */
|
| 22 |
|
|
|
|
|
|
|
|
|
|
| 23 |
const LAYER_RANK = { tokens: 0, base: 1, components: 2, utilities: 3, pin: 4 };
|
| 24 |
const LAYER_NAMES = ['tokens', 'base', 'components', 'utilities', 'pin'];
|
| 25 |
|
|
@@ -52,6 +55,38 @@ function splitDecls(decls, kinds) {
|
|
| 52 |
return { keep, engineProps };
|
| 53 |
}
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
export function toCSS(doc, opts = {}) {
|
| 56 |
const file = opts.file || 'styles.dssl';
|
| 57 |
const report = {
|
|
@@ -66,7 +101,8 @@ export function toCSS(doc, opts = {}) {
|
|
| 66 |
|
| 67 |
const chunks = [];
|
| 68 |
const header = opts.banner !== false
|
| 69 |
-
? `/* generated by dssl build from ${file} - the static subset of DSSL${opts.version ? ' v' + opts.version : ''}.\n @signal rules and engine-native properties (animate, aura,
|
|
|
|
| 70 |
: '';
|
| 71 |
let out = header;
|
| 72 |
|
|
@@ -87,8 +123,11 @@ export function toCSS(doc, opts = {}) {
|
|
| 87 |
}
|
| 88 |
|
| 89 |
// ---- regular rules ----
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
| 92 |
const { keep, engineProps } = splitDecls(r.decls || [], kinds);
|
| 93 |
report.engineDecls += engineProps;
|
| 94 |
if (!keep.length) { if (engineProps) report.skippedEngine++; continue; }
|
|
|
|
| 20 |
* no-JS, a strict CSP, first-paint-before-JS, or 4.5x smaller HTML.
|
| 21 |
* ========================================================================= */
|
| 22 |
|
| 23 |
+
import { buildTexture, composeTextureInto } from './texture.js';
|
| 24 |
+
import { makeColorResolver } from './lint.js';
|
| 25 |
+
|
| 26 |
const LAYER_RANK = { tokens: 0, base: 1, components: 2, utilities: 3, pin: 4 };
|
| 27 |
const LAYER_NAMES = ['tokens', 'base', 'components', 'utilities', 'pin'];
|
| 28 |
|
|
|
|
| 55 |
return { keep, engineProps };
|
| 56 |
}
|
| 57 |
|
| 58 |
+
/**
|
| 59 |
+
* `texture:` is engine-native, but most textures are just images laid over a background, and
|
| 60 |
+
* an image is plain CSS. When every colour is known at build time (tokens are), the layers are
|
| 61 |
+
* built here with the same code the engine uses and written as background-image longhands, so the
|
| 62 |
+
* texture survives the build. What stays in the runtime: a texture that animates, one with a colour
|
| 63 |
+
* that is not a token or a literal, and a rule whose own background is something a build cannot
|
| 64 |
+
* merge with (it is left alone and counted).
|
| 65 |
+
*/
|
| 66 |
+
function bakeTexture(rule, toColor, report) {
|
| 67 |
+
const at = (rule.decls || []).findIndex((d) => d.prop === '@texture');
|
| 68 |
+
if (at < 0) return rule;
|
| 69 |
+
const layers = rule.decls[at].value;
|
| 70 |
+
if (!Array.isArray(layers) || !layers.length) return rule; // texture: none
|
| 71 |
+
if (layers.some((l) => l.animate > 0)) { (report.notBaked ||= []).push({ sel: rule.sel, why: 'it animates' }); return rule; }
|
| 72 |
+
for (const l of layers) {
|
| 73 |
+
for (const key of ['color', 'color2']) {
|
| 74 |
+
if (l[key] !== undefined && !toColor(l[key])) { (report.notBaked ||= []).push({ sel: rule.sel, why: `its ${key} (${l[key]}) is not known at build time` }); return rule; }
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
const tex = buildTexture(layers, (css) => toColor(css));
|
| 78 |
+
if (!tex.count) return rule;
|
| 79 |
+
// last declaration of each property wins, as in the cascade within one rule
|
| 80 |
+
const owned = new Map();
|
| 81 |
+
for (const d of rule.decls) if (!isEngineProp(d.prop) && /^background/.test(d.prop)) owned.set(d.prop, { value: d.value, imp: '' });
|
| 82 |
+
composeTextureInto(owned, tex);
|
| 83 |
+
const decls = rule.decls.filter((d, i) => i !== at && !(/^background/.test(d.prop) && !isEngineProp(d.prop)));
|
| 84 |
+
const line = rule.decls[at].line;
|
| 85 |
+
for (const [prop, v] of owned) decls.push({ prop, value: v.value, line });
|
| 86 |
+
report.bakedTextures++;
|
| 87 |
+
return { ...rule, decls };
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
export function toCSS(doc, opts = {}) {
|
| 91 |
const file = opts.file || 'styles.dssl';
|
| 92 |
const report = {
|
|
|
|
| 101 |
|
| 102 |
const chunks = [];
|
| 103 |
const header = opts.banner !== false
|
| 104 |
+
? `/* generated by dssl build from ${file} - the static subset of DSSL${opts.version ? ' v' + opts.version : ''}.\n @signal rules and engine-native properties (animate, aura, tilt, ...) stay in the runtime.
|
| 105 |
+
Textures are baked in as background images when their colours are known at build time. */\n`
|
| 106 |
: '';
|
| 107 |
let out = header;
|
| 108 |
|
|
|
|
| 123 |
}
|
| 124 |
|
| 125 |
// ---- regular rules ----
|
| 126 |
+
const toColor = makeColorResolver(new Map(Object.entries(doc.tokens || {})));
|
| 127 |
+
report.bakedTextures = 0;
|
| 128 |
+
for (const r0 of allRules) {
|
| 129 |
+
if (r0.signals && r0.signals.length) { report.skippedSignals++; continue; }
|
| 130 |
+
const r = bakeTexture(r0, toColor, report);
|
| 131 |
const { keep, engineProps } = splitDecls(r.decls || [], kinds);
|
| 132 |
report.engineDecls += engineProps;
|
| 133 |
if (!keep.length) { if (engineProps) report.skippedEngine++; continue; }
|
public/dssl/texture.js
CHANGED
|
@@ -28,7 +28,7 @@ const mix2 = (x, y, t) => mix(x, y, t, 'srgb');
|
|
| 28 |
|
| 29 |
export const BLEND_MODES = ['normal', 'multiply', 'screen', 'overlay', 'soft-light', 'hard-light', 'color-burn', 'color-dodge', 'darken', 'lighten', 'difference', 'exclusion'];
|
| 30 |
|
| 31 |
-
export const GENERATORS = ['noise', 'bump', 'lines', 'dots', 'grid', 'checker', 'weave', 'carbon', 'waves', 'hex'];
|
| 32 |
|
| 33 |
/** the settings a texture layer understands, with how many value words each takes */
|
| 34 |
export const TEXTURE_KEYS = {
|
|
@@ -51,6 +51,11 @@ export const TEXTURE_KEYS = {
|
|
| 51 |
blend: { words: 1, kind: 'word', values: BLEND_MODES, doc: 'how the layer mixes with what is under it' },
|
| 52 |
color: { words: 1, kind: 'color', doc: 'the main colour (dark end of noise, ink of lines and dots)' },
|
| 53 |
color2: { words: 1, kind: 'color', doc: 'the second colour (light end of noise, background cell of patterns)' },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
invert: { words: 0, kind: 'flag', doc: 'swap the two colours' },
|
| 55 |
};
|
| 56 |
|
|
@@ -100,6 +105,26 @@ export const PRESETS = {
|
|
| 100 |
] },
|
| 101 |
wood: { doc: 'long wood grain, along x (use angle 90 for vertical)',
|
| 102 |
layers: [{ gen: 'noise', scale: 4, octaves: 3, contrast: 1.8, stretch: [40, 1], intensity: 1, color: '#7a4b24', color2: '#c99a62', blend: 'multiply' }] },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
bark: { doc: 'rough vertical bark',
|
| 104 |
layers: [{ gen: 'bump', scale: 10, octaves: 4, depth: 5, stretch: [1, 6], light: 150, intensity: 0.8, color: '#000', color2: '#fff', blend: 'overlay' }] },
|
| 105 |
leather: { doc: 'pebbled leather',
|
|
@@ -164,6 +189,7 @@ const GEN_DEFAULTS = {
|
|
| 164 |
carbon: { scale: 20, intensity: 1, color: '#151515', color2: '#262626', blend: 'normal' },
|
| 165 |
waves: { scale: 40, thickness: 1.2, intensity: 0.3, color: '#000', blend: 'normal' },
|
| 166 |
hex: { scale: 28, thickness: 1, intensity: 0.28, color: '#000', blend: 'normal' },
|
|
|
|
| 167 |
};
|
| 168 |
|
| 169 |
/* ------------------------------------------------------------------ *
|
|
@@ -452,7 +478,134 @@ function buildHex(spec, rc) {
|
|
| 452 |
return [{ image: svgUrl(svg), size: `${f(W)}px ${f(100 * k)}px`, repeat: 'repeat', position: '0 0', blend: spec.blend || 'normal', tile: W }];
|
| 453 |
}
|
| 454 |
|
| 455 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
|
| 457 |
/**
|
| 458 |
* Turn parsed layers into the CSS lists for background-image, -size, -repeat,
|
|
@@ -477,6 +630,59 @@ export function buildTexture(layers, rc) {
|
|
| 477 |
};
|
| 478 |
}
|
| 479 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
/* ------------------------------------------------------------------ *
|
| 481 |
* deterministic value noise, for aura masks and tests
|
| 482 |
* ------------------------------------------------------------------ */
|
|
|
|
| 28 |
|
| 29 |
export const BLEND_MODES = ['normal', 'multiply', 'screen', 'overlay', 'soft-light', 'hard-light', 'color-burn', 'color-dodge', 'darken', 'lighten', 'difference', 'exclusion'];
|
| 30 |
|
| 31 |
+
export const GENERATORS = ['noise', 'bump', 'lines', 'dots', 'grid', 'checker', 'weave', 'carbon', 'waves', 'hex', 'rings'];
|
| 32 |
|
| 33 |
/** the settings a texture layer understands, with how many value words each takes */
|
| 34 |
export const TEXTURE_KEYS = {
|
|
|
|
| 51 |
blend: { words: 1, kind: 'word', values: BLEND_MODES, doc: 'how the layer mixes with what is under it' },
|
| 52 |
color: { words: 1, kind: 'color', doc: 'the main colour (dark end of noise, ink of lines and dots)' },
|
| 53 |
color2: { words: 1, kind: 'color', doc: 'the second colour (light end of noise, background cell of patterns)' },
|
| 54 |
+
figure: { words: 1, kind: 'number', min: 0, max: 1, doc: 'rings generator: how strongly the growth rings arch into a cathedral figure, 0 is straight grain' },
|
| 55 |
+
warp: { words: 1, kind: 'number', min: 0, max: 1, doc: 'rings generator: how much each ring wobbles along its length' },
|
| 56 |
+
pores: { words: 1, kind: 'number', min: 0, max: 1, doc: 'rings generator: fine fibre and pore streaks between the rings' },
|
| 57 |
+
knots: { words: 1, kind: 'number', min: 0, max: 8, int: true, doc: 'rings generator: how many knots in one tile (the pattern repeats, so keep it small)' },
|
| 58 |
+
planks: { words: 1, kind: 'number', min: 24, max: 1200, doc: 'rings generator: lay it out as planks this many px tall, with a seam and a slightly different board each' },
|
| 59 |
invert: { words: 0, kind: 'flag', doc: 'swap the two colours' },
|
| 60 |
};
|
| 61 |
|
|
|
|
| 105 |
] },
|
| 106 |
wood: { doc: 'long wood grain, along x (use angle 90 for vertical)',
|
| 107 |
layers: [{ gen: 'noise', scale: 4, octaves: 3, contrast: 1.8, stretch: [40, 1], intensity: 1, color: '#7a4b24', color2: '#c99a62', blend: 'multiply' }] },
|
| 108 |
+
oak: { doc: 'oak: open grain, tan with brown rings',
|
| 109 |
+
layers: [{ gen: 'rings', scale: 11, color: '#8a5f33', color2: '#c9a574', contrast: 1.3, figure: 0.55, warp: 0.45, pores: 0.5, seed: 3, blend: 'normal' }] },
|
| 110 |
+
walnut: { doc: 'walnut: dark chocolate with black-brown rings',
|
| 111 |
+
layers: [{ gen: 'rings', scale: 9, color: '#2b1a10', color2: '#6a4631', contrast: 1.2, figure: 0.5, warp: 0.55, pores: 0.45, seed: 8, blend: 'normal' }] },
|
| 112 |
+
cherry: { doc: 'cherry: warm red-brown with fine rings',
|
| 113 |
+
layers: [{ gen: 'rings', scale: 8, color: '#7a3a1c', color2: '#bb7443', contrast: 1.1, figure: 0.4, warp: 0.35, pores: 0.35, seed: 5, blend: 'normal' }] },
|
| 114 |
+
maple: { doc: 'maple: pale, tight, nearly plain grain',
|
| 115 |
+
layers: [{ gen: 'rings', scale: 13, color: '#c79a5a', color2: '#ecd6ab', contrast: 0.8, figure: 0.25, warp: 0.3, pores: 0.25, seed: 11, blend: 'normal' }] },
|
| 116 |
+
pine: { doc: 'pine: light yellow wood with wide rings and knots',
|
| 117 |
+
layers: [{ gen: 'rings', scale: 16, color: '#b5742f', color2: '#e6c081', contrast: 1.4, figure: 0.65, warp: 0.5, pores: 0.4, knots: 2, seed: 14, blend: 'normal' }] },
|
| 118 |
+
mahogany: { doc: 'mahogany: deep red-brown, straight grain',
|
| 119 |
+
layers: [{ gen: 'rings', scale: 10, color: '#4a2012', color2: '#8c4727', contrast: 1.1, figure: 0.3, warp: 0.3, pores: 0.4, seed: 6, blend: 'normal' }] },
|
| 120 |
+
ebony: { doc: 'ebony: nearly black with faint streaks',
|
| 121 |
+
layers: [{ gen: 'rings', scale: 12, color: '#0c0908', color2: '#2d2621', contrast: 0.7, figure: 0.2, warp: 0.25, pores: 0.3, seed: 2, blend: 'normal' }] },
|
| 122 |
+
birch: { doc: 'birch: creamy pale wood with fine rings',
|
| 123 |
+
layers: [{ gen: 'rings', scale: 10, color: '#d2ae74', color2: '#f0dfb9', contrast: 0.9, figure: 0.3, warp: 0.35, pores: 0.25, seed: 9, blend: 'normal' }] },
|
| 124 |
+
'oak-planks': { doc: 'oak laid as planks, with seams',
|
| 125 |
+
layers: [{ gen: 'rings', scale: 11, color: '#8a5f33', color2: '#c9a574', contrast: 1.3, figure: 0.55, warp: 0.45, pores: 0.5, planks: 110, seed: 3, blend: 'normal' }] },
|
| 126 |
+
'walnut-planks': { doc: 'walnut laid as planks, with seams',
|
| 127 |
+
layers: [{ gen: 'rings', scale: 9, color: '#2b1a10', color2: '#6a4631', contrast: 1.2, figure: 0.5, warp: 0.55, pores: 0.45, planks: 96, seed: 8, blend: 'normal' }] },
|
| 128 |
bark: { doc: 'rough vertical bark',
|
| 129 |
layers: [{ gen: 'bump', scale: 10, octaves: 4, depth: 5, stretch: [1, 6], light: 150, intensity: 0.8, color: '#000', color2: '#fff', blend: 'overlay' }] },
|
| 130 |
leather: { doc: 'pebbled leather',
|
|
|
|
| 189 |
carbon: { scale: 20, intensity: 1, color: '#151515', color2: '#262626', blend: 'normal' },
|
| 190 |
waves: { scale: 40, thickness: 1.2, intensity: 0.3, color: '#000', blend: 'normal' },
|
| 191 |
hex: { scale: 28, thickness: 1, intensity: 0.28, color: '#000', blend: 'normal' },
|
| 192 |
+
rings: { scale: 10, color: '#7a4b24', color2: '#d1a770', contrast: 1.2, figure: 0.5, warp: 0.4, pores: 0.4, intensity: 1, blend: 'normal' },
|
| 193 |
};
|
| 194 |
|
| 195 |
/* ------------------------------------------------------------------ *
|
|
|
|
| 478 |
return [{ image: svgUrl(svg), size: `${f(W)}px ${f(100 * k)}px`, repeat: 'repeat', position: '0 0', blend: spec.blend || 'normal', tile: W }];
|
| 479 |
}
|
| 480 |
|
| 481 |
+
|
| 482 |
+
/* ------------------------------------------------------------------ *
|
| 483 |
+
* rings: growth rings, drawn as vector curves
|
| 484 |
+
*
|
| 485 |
+
* A board is a log sliced, and the rings you see are where the slice crosses
|
| 486 |
+
* the growth layers. So this does not stretch noise: it draws each ring as a
|
| 487 |
+
* curve. The spacing wobbles, the curves arch (cathedral figure) because the
|
| 488 |
+
* slice cuts the layers at a slant, there are pores between them, and planks
|
| 489 |
+
* and knots are drawn on top. Everything comes from a seeded generator and
|
| 490 |
+
* integer harmonics, so the same seed gives the same board on every machine
|
| 491 |
+
* and the tile has no visible seam.
|
| 492 |
+
* ------------------------------------------------------------------ */
|
| 493 |
+
|
| 494 |
+
const TAU = Math.PI * 2;
|
| 495 |
+
const r1 = (n) => String(Math.round(n * 10) / 10);
|
| 496 |
+
const smallUrl = (svg) => `url("data:image/svg+xml,${svg.replace(/[%#<>"]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())}")`;
|
| 497 |
+
|
| 498 |
+
/** a smooth path through [x, y] samples (Catmull-Rom as cubic Beziers, ends clamped) */
|
| 499 |
+
function smoothPath(pts) {
|
| 500 |
+
const n = pts.length;
|
| 501 |
+
const at = (i) => pts[Math.max(0, Math.min(n - 1, i))];
|
| 502 |
+
let d = `M${r1(pts[0][0])} ${r1(pts[0][1])}`;
|
| 503 |
+
for (let i = 0; i < n - 1; i++) {
|
| 504 |
+
const p0 = at(i - 1), p1 = at(i), p2 = at(i + 1), p3 = at(i + 2);
|
| 505 |
+
const c1x = p1[0] + (p2[0] - p0[0]) / 6, c1y = p1[1] + (p2[1] - p0[1]) / 6;
|
| 506 |
+
const c2x = p2[0] - (p3[0] - p1[0]) / 6, c2y = p2[1] - (p3[1] - p1[1]) / 6;
|
| 507 |
+
d += `C${r1(c1x)} ${r1(c1y)} ${r1(c2x)} ${r1(c2y)} ${r1(p2[0])} ${r1(p2[1])}`;
|
| 508 |
+
}
|
| 509 |
+
return d;
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
function buildRings(spec, rc) {
|
| 513 |
+
const { a, b } = layerColors(spec, rc);
|
| 514 |
+
const late = a, early = b || a;
|
| 515 |
+
const P = Math.max(3, spec.scale || 10);
|
| 516 |
+
const W = 576; // one tile along the grain; every harmonic below divides it
|
| 517 |
+
const fig = spec.figure === undefined ? 0.5 : spec.figure;
|
| 518 |
+
const warp = spec.warp === undefined ? 0.4 : spec.warp;
|
| 519 |
+
const pores = spec.pores === undefined ? 0.4 : spec.pores;
|
| 520 |
+
const contrast = spec.contrast === undefined ? 1.2 : spec.contrast;
|
| 521 |
+
const seed = spec.seed || 0;
|
| 522 |
+
const planked = !!spec.planks;
|
| 523 |
+
const plankH = planked ? Math.max(P * 3, spec.planks) : 0;
|
| 524 |
+
const plankN = planked ? 3 : 1;
|
| 525 |
+
const H = planked ? plankH * plankN : P * Math.max(12, Math.round(260 / P));
|
| 526 |
+
const rgba = (c, alpha) => toRgba(withAlpha(c, n01((alpha === undefined ? 1 : alpha) * c.a)));
|
| 527 |
+
const STEP = 48;
|
| 528 |
+
|
| 529 |
+
let defs = '', body = '';
|
| 530 |
+
let id = 0;
|
| 531 |
+
for (let pi = 0; pi < plankN; pi++) {
|
| 532 |
+
const rand = rng(seed * 7919 + pi * 104729 + 13);
|
| 533 |
+
const h = planked ? plankH : H;
|
| 534 |
+
const n = Math.max(3, Math.round(h / P));
|
| 535 |
+
const Pe = h / n;
|
| 536 |
+
const ph = [rand() * TAU, rand() * TAU, rand() * TAU];
|
| 537 |
+
const x0 = rand() * W;
|
| 538 |
+
const m = 2 + Math.floor(rand() * 3), spacingAmp = 0.12 + rand() * 0.22;
|
| 539 |
+
const flip = planked && rand() < 0.5;
|
| 540 |
+
const curve = (kf, x) => {
|
| 541 |
+
const t = kf / n;
|
| 542 |
+
// a free board arches from one face to the other; a seamless tile must repeat every n rings, so its envelope has period n
|
| 543 |
+
const per = Math.pow(Math.sin(Math.PI * t), 2);
|
| 544 |
+
const env = planked ? Math.pow(Math.max(0, Math.min(1, flip ? 1 - t : t)), 1.4) : per;
|
| 545 |
+
const env2 = planked ? 0.6 + 0.4 * Math.abs(Math.sin(Math.PI * t)) : per;
|
| 546 |
+
const bump = Math.pow((1 + Math.cos(TAU * (x - x0) / W)) / 2, 1.3);
|
| 547 |
+
const arch = fig * 0.3 * h * env * bump;
|
| 548 |
+
const wob = warp * P * 0.6 * env2 * (0.5 * Math.sin(TAU * x / W + ph[0] + TAU * kf / n) + 0.3 * Math.sin(TAU * 3 * x / W + ph[1] + TAU * 2 * kf / n) + 0.2 * Math.sin(TAU * 7 * x / W + ph[2] + TAU * 3 * kf / n));
|
| 549 |
+
const u = kf + spacingAmp * Math.sin(TAU * m * kf / n);
|
| 550 |
+
return u * Pe + arch + wob;
|
| 551 |
+
};
|
| 552 |
+
// sampled past both ends of the tile, so the stroke is continuous across the join and the viewport clips it
|
| 553 |
+
const ringPath = (kf) => { const pts = []; for (let x = -2 * STEP; x <= W + 2 * STEP; x += STEP) pts.push([x, curve(kf, x)]); return smoothPath(pts); };
|
| 554 |
+
const jit = Array.from({ length: n }, () => [rand(), rand()]);
|
| 555 |
+
|
| 556 |
+
const lo = planked ? -2 : -1, hi = planked ? n + 2 : n;
|
| 557 |
+
let g = '';
|
| 558 |
+
for (let k = lo; k <= hi; k++) {
|
| 559 |
+
const [jitter, jop] = jit[((k % n) + n) % n]; // ring k and ring k + n look alike, so the tile repeats cleanly
|
| 560 |
+
const w = Pe * (0.14 + jitter * 0.26);
|
| 561 |
+
const op = n01((0.4 + jop * 0.55) * Math.min(1.4, contrast / 1.2));
|
| 562 |
+
const rid = 'r' + id++;
|
| 563 |
+
defs += `<path id='${rid}' d='${ringPath(k + 0.8)}' fill='none'/>`;
|
| 564 |
+
g += `<use href='#${rid}' stroke='${rgba(late, op * 0.28)}' stroke-width='${r1(w * 2.6)}'/><use href='#${rid}' stroke='${rgba(late, op)}' stroke-width='${r1(w)}'/>`;
|
| 565 |
+
}
|
| 566 |
+
// knots: elongated along the grain, with rings around them
|
| 567 |
+
const knotN = spec.knots ? Math.max(0, Math.round(spec.knots / plankN + (pi === 0 ? 0.49 : 0))) : 0;
|
| 568 |
+
let knots = '';
|
| 569 |
+
for (let q = 0; q < knotN; q++) {
|
| 570 |
+
const kx = 90 + rand() * (W - 180), ky = h * (0.25 + rand() * 0.5), rx = 9 + rand() * 12, ry = rx * 0.6;
|
| 571 |
+
knots += `<ellipse cx='${r1(kx)}' cy='${r1(ky)}' rx='${r1(rx * 4)}' ry='${r1(ry * 3.4)}' fill='${rgba(late, 0.1)}'/>` +
|
| 572 |
+
`<ellipse cx='${r1(kx)}' cy='${r1(ky)}' rx='${r1(rx * 2.4)}' ry='${r1(ry * 2.3)}' fill='none' stroke='${rgba(late, 0.35)}' stroke-width='1.2'/>` +
|
| 573 |
+
`<ellipse cx='${r1(kx)}' cy='${r1(ky)}' rx='${r1(rx * 1.6)}' ry='${r1(ry * 1.6)}' fill='none' stroke='${rgba(late, 0.5)}' stroke-width='1.4'/>` +
|
| 574 |
+
`<ellipse cx='${r1(kx)}' cy='${r1(ky)}' rx='${r1(rx)}' ry='${r1(ry)}' fill='${rgba(late, 0.85)}'/>`;
|
| 575 |
+
}
|
| 576 |
+
const tone = (rand() - 0.5) * 0.14;
|
| 577 |
+
const toneRect = tone > 0 ? `<rect width='${W}' height='${r1(h)}' fill='rgba(255,255,255,${r1(Math.abs(tone) * 10) / 10})'/>` : `<rect width='${W}' height='${r1(h)}' fill='rgba(0,0,0,${r1(Math.abs(tone) * 10) / 10})'/>`;
|
| 578 |
+
const clip = `c${pi}`;
|
| 579 |
+
defs += `<clipPath id='${clip}'><rect width='${W}' height='${r1(h)}'/></clipPath>`;
|
| 580 |
+
body += `<g transform='translate(0 ${r1(pi * plankH)})' clip-path='url(#${clip})'>` +
|
| 581 |
+
`<rect width='${W}' height='${r1(h)}' fill='${rgba(early)}'/>` + (planked ? toneRect : '') + `<g fill='none'>${g}</g>${knots}</g>`;
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
// fine fibres: streaks stretched along the grain, drawn once over everything
|
| 585 |
+
const fibre = pores > 0
|
| 586 |
+
? `<filter id='p' x='0' y='0' width='100%' height='100%' color-interpolation-filters='sRGB'>` +
|
| 587 |
+
`<feTurbulence type='fractalNoise' baseFrequency='0.014 0.62' numOctaves='3' seed='${seed + 3}' stitchTiles='stitch'/>` +
|
| 588 |
+
`<feColorMatrix type='matrix' values='0 0 0 0 ${f(late.r / 255)} 0 0 0 0 ${f(late.g / 255)} 0 0 0 0 ${f(late.b / 255)} ${f(pores * 4.2)} 0 0 0 ${f(-pores * 1.95)}'/></filter>` +
|
| 589 |
+
`<rect width='${W}' height='${H}' filter='url(#p)' opacity='${f(Math.min(1, 0.55 * late.a))}'/>`
|
| 590 |
+
: '';
|
| 591 |
+
|
| 592 |
+
let seams = '';
|
| 593 |
+
if (planked) {
|
| 594 |
+
for (let pi = 0; pi < plankN; pi++) {
|
| 595 |
+
const y = pi * plankH;
|
| 596 |
+
seams += `<rect y='${r1(y)}' width='${W}' height='1.6' fill='rgba(0,0,0,0.5)'/><rect y='${r1(y + 1.6)}' width='${W}' height='1' fill='rgba(255,255,255,0.1)'/>`;
|
| 597 |
+
}
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
const rot = spec.angle === 90 || spec.angle === -90 || spec.angle === 270;
|
| 601 |
+
const inner = `<defs>${defs}</defs>${body}${fibre}${seams}`;
|
| 602 |
+
const svg = rot
|
| 603 |
+
? `<svg xmlns='http://www.w3.org/2000/svg' width='${H}' height='${W}'><g transform='matrix(0 1 1 0 0 0)'>${inner}</g></svg>`
|
| 604 |
+
: `<svg xmlns='http://www.w3.org/2000/svg' width='${W}' height='${H}'>${inner}</svg>`;
|
| 605 |
+
return [{ image: smallUrl(svg), size: rot ? `${H}px ${W}px` : `${W}px ${H}px`, repeat: 'repeat', position: '0 0', blend: spec.blend || 'normal', tile: Math.max(W, H) }];
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
const BUILDERS = { rings: buildRings, noise: buildNoise, bump: buildBump, lines: buildLines, dots: buildDots, grid: buildGrid, checker: buildChecker, weave: buildWeave, carbon: buildCarbon, waves: buildWaves, hex: buildHex };
|
| 609 |
|
| 610 |
/**
|
| 611 |
* Turn parsed layers into the CSS lists for background-image, -size, -repeat,
|
|
|
|
| 630 |
};
|
| 631 |
}
|
| 632 |
|
| 633 |
+
|
| 634 |
+
/* ------------------------------------------------------------------ *
|
| 635 |
+
* composing with the element's own background (shared by the engine and `dssl build`)
|
| 636 |
+
* ------------------------------------------------------------------ */
|
| 637 |
+
|
| 638 |
+
export function splitBgList(value) {
|
| 639 |
+
const out = []; let depth = 0, cur = '';
|
| 640 |
+
for (const ch of String(value)) {
|
| 641 |
+
if (ch === '(') depth++;
|
| 642 |
+
if (ch === ')') depth--;
|
| 643 |
+
if (ch === ',' && depth === 0) { out.push(cur.trim()); cur = ''; continue; }
|
| 644 |
+
cur += ch;
|
| 645 |
+
}
|
| 646 |
+
if (cur.trim()) out.push(cur.trim());
|
| 647 |
+
return out;
|
| 648 |
+
}
|
| 649 |
+
/** a list of exactly n entries: the author's, cycled the way CSS cycles them, or the default */
|
| 650 |
+
function expandList(value, n, dflt) {
|
| 651 |
+
const items = value ? splitBgList(value) : [];
|
| 652 |
+
if (!items.length) return Array.from({ length: n }, () => dflt);
|
| 653 |
+
return Array.from({ length: n }, (_, i) => items[i % items.length]);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
|
| 657 |
+
/**
|
| 658 |
+
* texture sits ON TOP of whatever background the author wrote: the shorthand, the longhands,
|
| 659 |
+
* or just a colour. `owned` is a Map of prop -> { value, imp }; this edits it in place.
|
| 660 |
+
*/
|
| 661 |
+
export function composeTextureInto(owned, tex) {
|
| 662 |
+
const sh = owned.get('background');
|
| 663 |
+
const img = owned.get('background-image');
|
| 664 |
+
const blendUser = owned.get('background-blend-mode');
|
| 665 |
+
const texBlends = tex.blend.split(', ');
|
| 666 |
+
if (sh) {
|
| 667 |
+
// the author wrote the shorthand: texture layers go first, their value stays the last layer(s)
|
| 668 |
+
const userLayers = splitBgList(sh.value).length;
|
| 669 |
+
const texShort = tex.parts.map((p) => `${p.image} ${p.position} / ${p.size} ${p.repeat}`).join(', ');
|
| 670 |
+
owned.set('background', { value: `${texShort}, ${sh.value}`, imp: sh.imp });
|
| 671 |
+
owned.delete('background-image');
|
| 672 |
+
owned.set('background-blend-mode', { value: [...texBlends, ...expandList(blendUser && blendUser.value, userLayers, 'normal')].join(', '), imp: sh.imp });
|
| 673 |
+
return;
|
| 674 |
+
}
|
| 675 |
+
const userLayers = img ? splitBgList(img.value).length : 0;
|
| 676 |
+
const imp = img ? img.imp : '';
|
| 677 |
+
const get = (name) => (owned.get(name) || {}).value;
|
| 678 |
+
const join = (texList, name, dflt) => [...texList.split(/,(?![^(]*\))/).map((x) => x.trim()), ...(userLayers ? expandList(get(name), userLayers, dflt) : [])].join(', ');
|
| 679 |
+
owned.set('background-image', { value: tex.image + (img ? ', ' + img.value : ''), imp });
|
| 680 |
+
owned.set('background-size', { value: join(tex.size, 'background-size', 'auto'), imp });
|
| 681 |
+
owned.set('background-position', { value: join(tex.position, 'background-position', '0% 0%'), imp });
|
| 682 |
+
owned.set('background-repeat', { value: join(tex.repeat, 'background-repeat', 'repeat'), imp });
|
| 683 |
+
owned.set('background-blend-mode', { value: [...texBlends, ...(userLayers ? expandList(get('background-blend-mode'), userLayers, 'normal') : [])].join(', '), imp });
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
/* ------------------------------------------------------------------ *
|
| 687 |
* deterministic value noise, for aura masks and tests
|
| 688 |
* ------------------------------------------------------------------ */
|
public/llms.txt
CHANGED
|
@@ -50,7 +50,7 @@ DSSL.replay(sel) / DSSL.refresh() / DSSL.errors() / DSSL.version
|
|
| 50 |
```
|
| 51 |
|
| 52 |
## 0.9.0: texture, aura, sliders, lint, import
|
| 53 |
-
- `texture: grain` layers a generated image ON TOP of an element's own background (colour, gradient or several layers).
|
| 54 |
- Aura has NO presets. `aura-glow: <color> <radius> [intensity|pulse|depth|flicker|texture(...)|seed|essential]`, `aura-particle: <color -> color ...> <count> [size|life|speed|dir|spread|gravity|drag|turbulence|from|shape|grow|glowing|behind|blend|texture(...)|seed|essential]`, `glow-particle` (particles that glow), `aura-glow-particle` (both slots), `aura: glow ..., particles ...` (replaces both; `aura: none` clears both, `aura-glow: none` only the glow). Name your own with `behavior fire { ... }` then `use fire`. One canvas over the page, 1800 particle budget, still glow under reduced motion, runtime-only.
|
| 55 |
- `slider: track 6 color.line radius full, fill color.accent, thumb 24 color.paper, ring 3 color.accent, glow color.accent 14, ticks 10, graph 20 45 70` writes the ::-webkit-slider-* and ::-moz-range-* rules together for `input[type=range]`. The engine keeps `--dssl-p` (value 0..1) on the input. Firefox rules are generated but untested.
|
| 56 |
- Lint warnings (never errors; `dssl check --strict` makes them fatal): text contrast under 4.5:1 (3:1 for large text), a removed focus outline with no focus-visible replacement, tap targets under 24px.
|
|
@@ -63,6 +63,14 @@ DSSL.replay(sel) / DSSL.refresh() / DSSL.errors() / DSSL.version
|
|
| 63 |
- Alpha on a named colour works: `white/12`, `currentColor/20`. It used to compile to `white/12px`.
|
| 64 |
- The playground has a canvas.html tab, `?example=` links, lint warnings and an "as CSS" tab. The canvas keeps a short allow-list of tags and drops scripts, handlers, styles and ids.
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
## CLI (v0.8.0)
|
| 67 |
|
| 68 |
```sh
|
|
@@ -76,4 +84,4 @@ Wait for the `dssl-ready` event on `window` before measuring layout: the engine
|
|
| 76 |
|
| 77 |
## Editor support
|
| 78 |
|
| 79 |
-
The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. In VSCodium, search "DSSL" in the Extensions tab (it is on Open VSX: https://open-vsx.org/extension/ehs-studio/dssl-vscode). In VS Code, download `dssl-vscode/dssl-vscode-0.
|
|
|
|
| 50 |
```
|
| 51 |
|
| 52 |
## 0.9.0: texture, aura, sliders, lint, import
|
| 53 |
+
- `texture: grain` layers a generated image ON TOP of an element's own background (colour, gradient or several layers). 40 presets (30 general: grain, noise, rough, sand, paper, cloud, concrete, stone, crumpled, glitter, frost, marble, wood, bark, leather, brushed, canvas, linen, denim, carbon, scanlines, halftone, dots, lines, grid, checker, crosshatch, stripes, waves, hex) and 11 generators (noise, bump, lines, dots, grid, checker, weave, carbon, waves, hex, rings) with settings: scale, figure, warp, pores, knots, planks, intensity, contrast, threshold, octaves, seed, stretch x y, depth, light, angle, thickness, radius, softness, contour, animate, type, blend, color, color2, invert. Comma stacks layers. Built at run time, and baked into the CSS by `dssl build` when its colours are known (0.11.0).
|
| 54 |
- Aura has NO presets. `aura-glow: <color> <radius> [intensity|pulse|depth|flicker|texture(...)|seed|essential]`, `aura-particle: <color -> color ...> <count> [size|life|speed|dir|spread|gravity|drag|turbulence|from|shape|grow|glowing|behind|blend|texture(...)|seed|essential]`, `glow-particle` (particles that glow), `aura-glow-particle` (both slots), `aura: glow ..., particles ...` (replaces both; `aura: none` clears both, `aura-glow: none` only the glow). Name your own with `behavior fire { ... }` then `use fire`. One canvas over the page, 1800 particle budget, still glow under reduced motion, runtime-only.
|
| 55 |
- `slider: track 6 color.line radius full, fill color.accent, thumb 24 color.paper, ring 3 color.accent, glow color.accent 14, ticks 10, graph 20 45 70` writes the ::-webkit-slider-* and ::-moz-range-* rules together for `input[type=range]`. The engine keeps `--dssl-p` (value 0..1) on the input. Firefox rules are generated but untested.
|
| 56 |
- Lint warnings (never errors; `dssl check --strict` makes them fatal): text contrast under 4.5:1 (3:1 for large text), a removed focus outline with no focus-visible replacement, tap targets under 24px.
|
|
|
|
| 63 |
- Alpha on a named colour works: `white/12`, `currentColor/20`. It used to compile to `white/12px`.
|
| 64 |
- The playground has a canvas.html tab, `?example=` links, lint warnings and an "as CSS" tab. The canvas keeps a short allow-list of tags and drops scripts, handlers, styles and ids.
|
| 65 |
|
| 66 |
+
## 0.11.0: wood and the build
|
| 67 |
+
- Ten wood species presets: oak, walnut, cherry, maple, pine, mahogany, ebony, birch, oak-planks, walnut-planks. The `rings` generator draws growth rings as vector curves (not stretched noise): `texture: oak figure 0.8 knots 2 planks 120`. Settings: scale (ring spacing), color (latewood), color2 (earlywood), contrast, figure (cathedral arching), warp, pores, knots, planks (board height px), seed, angle 0 or 90. Same seed, same board everywhere; the tile repeats without a seam.
|
| 68 |
+
- `dssl build` now BAKES textures into the CSS as background images when every colour is known at build time. Animated textures and unknown colours stay in the runtime, and the report says why.
|
| 69 |
+
- `counter-reset`, `counter-increment`, `counter-set` and `content: counter(name)` / `counters(name, ".")` work.
|
| 70 |
+
- The contrast lint no longer cries wolf on translucent backgrounds: it warns only when the pair fails on the light page colour, white and black.
|
| 71 |
+
- A page can hide itself until styled: `<style>html:not(.dssl-ready) body{visibility:hidden}</style>`; the engine adds `dssl-ready` to `<html>`.
|
| 72 |
+
- Showcase: /showcase/wood/ is a whole page (Heartwood) built with these.
|
| 73 |
+
|
| 74 |
## CLI (v0.8.0)
|
| 75 |
|
| 76 |
```sh
|
|
|
|
| 84 |
|
| 85 |
## Editor support
|
| 86 |
|
| 87 |
+
The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. In VSCodium, search "DSSL" in the Extensions tab (it is on Open VSX: https://open-vsx.org/extension/ehs-studio/dssl-vscode). In VS Code, download `dssl-vscode/dssl-vscode-0.11.0.vsix` from the repo files, then Extensions panel -> ... -> Install from VSIX. Source lives in `dssl-vscode/` in the repo.
|
public/showcase/wood/index.html
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>Heartwood: a field guide to wood</title>
|
| 7 |
+
<meta name="description" content="How wood is built: growth rings, figure, knots, ten species, and how much a board moves when the air dries out. Every texture on the page is generated by DSSL.">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700;9..144,800&family=Hanken+Grotesk:wght@400;500;700&display=swap">
|
| 11 |
+
<!-- wood.css is real CSS built from static.dssl (textures baked in), so the page is styled before any script runs -->
|
| 12 |
+
<link rel="stylesheet" href="wood.css">
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
<a class="skip" href="#main">Skip to the guide</a>
|
| 16 |
+
|
| 17 |
+
<header class="top">
|
| 18 |
+
<div class="wrap">
|
| 19 |
+
<a class="brand" href="#">Heartwood</a>
|
| 20 |
+
<nav class="nav" aria-label="Sections">
|
| 21 |
+
<a href="#anatomy">Anatomy</a>
|
| 22 |
+
<a href="#species">Species</a>
|
| 23 |
+
<a href="#process">Log to board</a>
|
| 24 |
+
<a href="#movement">Movement</a>
|
| 25 |
+
<a href="#fire">Fire</a>
|
| 26 |
+
</nav>
|
| 27 |
+
</div>
|
| 28 |
+
</header>
|
| 29 |
+
|
| 30 |
+
<main id="main">
|
| 31 |
+
<section class="hero">
|
| 32 |
+
<div class="wrap">
|
| 33 |
+
<div class="slab">
|
| 34 |
+
<h1>Every board is a slice of weather.</h1>
|
| 35 |
+
<p>A tree writes down each year it lives: wide rings for wet summers, tight ones for drought. This is a field guide to reading the grain, choosing a species, and knowing why a table moves in winter.</p>
|
| 36 |
+
<a class="cta" href="#anatomy">Read the grain</a>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
</section>
|
| 40 |
+
|
| 41 |
+
<section class="block" id="anatomy">
|
| 42 |
+
<div class="wrap">
|
| 43 |
+
<p class="kicker">Anatomy</p>
|
| 44 |
+
<h2 class="title">What you are looking at when you look at grain</h2>
|
| 45 |
+
<p class="lede">Wood grows outward in layers. Cut a log and the layers show as rings. Cut a board and the same layers show as lines, arches and the occasional knot.</p>
|
| 46 |
+
<div class="trio">
|
| 47 |
+
<article class="specimen sp-rings">
|
| 48 |
+
<div class="note"><b>Growth rings</b>One ring is one growing season: pale, fast earlywood in spring, dense dark latewood in late summer. Count them and you have the tree's age.</div>
|
| 49 |
+
</article>
|
| 50 |
+
<article class="specimen sp-figure">
|
| 51 |
+
<div class="note"><b>Cathedral figure</b>When a board is sawn tangent to the rings, the cut slices each cone-shaped layer at a slant, and the lines bend into arches.</div>
|
| 52 |
+
</article>
|
| 53 |
+
<article class="specimen sp-knot">
|
| 54 |
+
<div class="note"><b>Knots</b>A knot is the base of a branch that the trunk grew around. It is harder, darker, and it does not move with the grain around it.</div>
|
| 55 |
+
</article>
|
| 56 |
+
</div>
|
| 57 |
+
<dl class="facts">
|
| 58 |
+
<div><dt>Hardwood or softwood</dt><dd>The words are botanical, not about hardness. Hardwoods come from broadleaf trees, softwoods from conifers. Balsa is a hardwood.</dd></div>
|
| 59 |
+
<div><dt>Heartwood and sapwood</dt><dd>The dark core of a trunk is heartwood, no longer carrying water. The pale outer band is sapwood. Many species are darkest in the middle.</dd></div>
|
| 60 |
+
<div><dt>Plain-sawn or quarter-sawn</dt><dd>Plain-sawn boards show cathedral arches and move more. Quarter-sawn boards show straight lines and stay flatter.</dd></div>
|
| 61 |
+
</dl>
|
| 62 |
+
</div>
|
| 63 |
+
</section>
|
| 64 |
+
|
| 65 |
+
<section class="block block-dark" id="species">
|
| 66 |
+
<div class="wrap">
|
| 67 |
+
<p class="kicker">Species</p>
|
| 68 |
+
<h2 class="title">Eight boards from the sample rack</h2>
|
| 69 |
+
<p class="lede">Hardness is the Janka test: the force in pounds needed to push a steel ball half its width into the wood. Higher is harder to dent, and harder to work. Figures are rounded averages.</p>
|
| 70 |
+
<div class="rack">
|
| 71 |
+
<figure class="sample"><div class="board b-oak" role="img" aria-label="White oak sample board"></div><figcaption><h3>White oak</h3><p class="latin">Quercus alba</p><div class="hard"><input type="range" min="0" max="3200" value="1360" disabled aria-label="Hardness 1360 on a scale to 3200"><span>1,360 lbf</span></div></figcaption></figure>
|
| 72 |
+
<figure class="sample"><div class="board b-walnut" role="img" aria-label="Black walnut sample board"></div><figcaption><h3>Black walnut</h3><p class="latin">Juglans nigra</p><div class="hard"><input type="range" min="0" max="3200" value="1010" disabled aria-label="Hardness 1010 on a scale to 3200"><span>1,010 lbf</span></div></figcaption></figure>
|
| 73 |
+
<figure class="sample"><div class="board b-cherry" role="img" aria-label="Black cherry sample board"></div><figcaption><h3>Black cherry</h3><p class="latin">Prunus serotina</p><div class="hard"><input type="range" min="0" max="3200" value="950" disabled aria-label="Hardness 950 on a scale to 3200"><span>950 lbf</span></div></figcaption></figure>
|
| 74 |
+
<figure class="sample"><div class="board b-maple" role="img" aria-label="Hard maple sample board"></div><figcaption><h3>Hard maple</h3><p class="latin">Acer saccharum</p><div class="hard"><input type="range" min="0" max="3200" value="1450" disabled aria-label="Hardness 1450 on a scale to 3200"><span>1,450 lbf</span></div></figcaption></figure>
|
| 75 |
+
<figure class="sample"><div class="board b-pine" role="img" aria-label="Eastern white pine sample board"></div><figcaption><h3>White pine</h3><p class="latin">Pinus strobus</p><div class="hard"><input type="range" min="0" max="3200" value="380" disabled aria-label="Hardness 380 on a scale to 3200"><span>380 lbf</span></div></figcaption></figure>
|
| 76 |
+
<figure class="sample"><div class="board b-mahogany" role="img" aria-label="Mahogany sample board"></div><figcaption><h3>Mahogany</h3><p class="latin">Swietenia macrophylla</p><div class="hard"><input type="range" min="0" max="3200" value="800" disabled aria-label="Hardness 800 on a scale to 3200"><span>800 lbf</span></div></figcaption></figure>
|
| 77 |
+
<figure class="sample"><div class="board b-ebony" role="img" aria-label="Gaboon ebony sample board"></div><figcaption><h3>Gaboon ebony</h3><p class="latin">Diospyros crassiflora</p><div class="hard"><input type="range" min="0" max="3200" value="3080" disabled aria-label="Hardness 3080 on a scale to 3200"><span>3,080 lbf</span></div></figcaption></figure>
|
| 78 |
+
<figure class="sample"><div class="board b-birch" role="img" aria-label="Yellow birch sample board"></div><figcaption><h3>Yellow birch</h3><p class="latin">Betula alleghaniensis</p><div class="hard"><input type="range" min="0" max="3200" value="1260" disabled aria-label="Hardness 1260 on a scale to 3200"><span>1,260 lbf</span></div></figcaption></figure>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
</section>
|
| 82 |
+
|
| 83 |
+
<section class="block" id="process">
|
| 84 |
+
<div class="wrap">
|
| 85 |
+
<p class="kicker">Log to board</p>
|
| 86 |
+
<h2 class="title">Five steps between a forest and a shelf</h2>
|
| 87 |
+
<p class="lede">Most of what goes wrong with wood goes wrong in the second and third steps, long before it is a table.</p>
|
| 88 |
+
<ol class="steps">
|
| 89 |
+
<li><h3>Fell and log</h3><p>A tree is cut and bucked into logs. Fresh wood is mostly water, sometimes more than half its weight.</p></li>
|
| 90 |
+
<li><h3>Saw</h3><p>The sawyer chooses how to cut the log. Plain-sawn gets the most boards. Quarter-sawn gets the most stable ones.</p></li>
|
| 91 |
+
<li><h3>Dry</h3><p>Boards are air-dried or kiln-dried until the wood holds only 6 to 12 percent moisture, the level indoor air will keep it at.</p></li>
|
| 92 |
+
<li><h3>Mill</h3><p>Dry boards are flattened and squared. This is when hidden stress shows: a board that was dried too fast will cup or bow.</p></li>
|
| 93 |
+
<li><h3>Finish</h3><p>Oil soaks in and darkens the grain. Wax and lacquer sit on top. None of them stop wood from taking on and losing moisture.</p></li>
|
| 94 |
+
</ol>
|
| 95 |
+
</div>
|
| 96 |
+
</section>
|
| 97 |
+
|
| 98 |
+
<section class="block block-dark" id="movement">
|
| 99 |
+
<div class="wrap">
|
| 100 |
+
<p class="kicker">Movement</p>
|
| 101 |
+
<h2 class="title">How much will it move?</h2>
|
| 102 |
+
<p class="lede">Wood swells when it takes on moisture and shrinks when it loses it, mostly across the grain and most of all along the growth rings. Pick a species, a board width, and how many points of moisture content the air will change.</p>
|
| 103 |
+
<div class="calc">
|
| 104 |
+
<div class="tool">
|
| 105 |
+
<div>
|
| 106 |
+
<label for="sp">Species (plain-sawn)</label>
|
| 107 |
+
<select id="sp">
|
| 108 |
+
<option value="10.5">White oak</option>
|
| 109 |
+
<option value="7.8">Black walnut</option>
|
| 110 |
+
<option value="7.1">Black cherry</option>
|
| 111 |
+
<option value="9.9" selected>Hard maple</option>
|
| 112 |
+
<option value="6.1">White pine</option>
|
| 113 |
+
<option value="4.1">Mahogany</option>
|
| 114 |
+
<option value="9.5">Yellow birch</option>
|
| 115 |
+
</select>
|
| 116 |
+
</div>
|
| 117 |
+
<div>
|
| 118 |
+
<label for="w">Board width <span class="readout" id="wv">40 cm</span></label>
|
| 119 |
+
<input id="w" type="range" min="5" max="100" value="40">
|
| 120 |
+
</div>
|
| 121 |
+
<div>
|
| 122 |
+
<label for="mc">Change in moisture content <span class="readout" id="mcv">4 points</span></label>
|
| 123 |
+
<input id="mc" type="range" min="0" max="12" value="4">
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
<div class="result" aria-live="polite">
|
| 127 |
+
<div class="result-card">
|
| 128 |
+
<div class="mm"><span id="out">0</span> mm</div>
|
| 129 |
+
<p class="mm-note" id="note"></p>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
<p class="small">An estimate, not an engineering number. It assumes the wood moves in proportion to its tangential shrinkage from green to oven-dry (taken from USDA Wood Handbook averages), spread evenly over the roughly 28 points of moisture content between fully green and the fibre saturation point. Real boards depend on how they were cut.</p>
|
| 134 |
+
</div>
|
| 135 |
+
</section>
|
| 136 |
+
|
| 137 |
+
<section class="block" id="fire">
|
| 138 |
+
<div class="wrap hearth">
|
| 139 |
+
<div>
|
| 140 |
+
<p class="kicker">Fire</p>
|
| 141 |
+
<h2 class="title">The last thing wood does</h2>
|
| 142 |
+
<p class="lede" style="margin-bottom:0">Firewood burns best when it has been seasoned, split and stacked off the ground for six months to two years, until it holds under about 20 percent moisture. Wet wood spends its heat boiling off water, and smokes.</p>
|
| 143 |
+
</div>
|
| 144 |
+
<div class="grate" role="img" aria-label="A log burning"><div class="log"></div></div>
|
| 145 |
+
</div>
|
| 146 |
+
</section>
|
| 147 |
+
</main>
|
| 148 |
+
|
| 149 |
+
<footer class="foot">
|
| 150 |
+
<div class="wrap">
|
| 151 |
+
<p>Built with <a href="https://dssl-sable.vercel.app/">DSSL</a>. The page's look is <code>static.dssl</code>, compiled to <code>wood.css</code> with <code>dssl build</code>, textures included. The tilt, the fire and the slider fills are <code>live.dssl</code>, applied by the engine.</p>
|
| 152 |
+
<p>Every wood grain here is generated vector art from the <code>rings</code> texture, not a photograph. Janka and shrinkage values are rounded averages from the USDA Wood Handbook and the Wood Database.</p>
|
| 153 |
+
</div>
|
| 154 |
+
</footer>
|
| 155 |
+
|
| 156 |
+
<script>
|
| 157 |
+
// the calculator: movement = width x tangential shrinkage x (change in moisture / 28)
|
| 158 |
+
(() => {
|
| 159 |
+
const $ = (id) => document.getElementById(id);
|
| 160 |
+
const sp = $('sp'), w = $('w'), mc = $('mc');
|
| 161 |
+
const calc = () => {
|
| 162 |
+
const mm = Number(w.value) * 10 * (Number(sp.value) / 100) * (Number(mc.value) / 28);
|
| 163 |
+
$('out').textContent = mm < 10 ? mm.toFixed(1) : String(Math.round(mm));
|
| 164 |
+
$('wv').textContent = w.value + ' cm';
|
| 165 |
+
$('mcv').textContent = mc.value + (mc.value === '1' ? ' point' : ' points');
|
| 166 |
+
const name = sp.options[sp.selectedIndex].text.toLowerCase();
|
| 167 |
+
$('note').textContent = `A ${w.value} cm wide ${name} board changes width by about this much across the grain when it gains or loses ${mc.value} points of moisture.`;
|
| 168 |
+
for (const el of [w, mc]) el.style.setProperty('--dssl-p', (el.value - el.min) / (el.max - el.min));
|
| 169 |
+
};
|
| 170 |
+
for (const el of [sp, w, mc]) el.addEventListener('input', calc);
|
| 171 |
+
calc();
|
| 172 |
+
})();
|
| 173 |
+
</script>
|
| 174 |
+
<script type="module" src="/dssl/boot.js"></script>
|
| 175 |
+
<link rel="stylesheet/dssl" href="tokens.dssl">
|
| 176 |
+
<link rel="stylesheet/dssl" href="live.dssl">
|
| 177 |
+
</body>
|
| 178 |
+
</html>
|
public/showcase/wood/live.dssl
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// =========================================================================
|
| 2 |
+
// live.dssl: what needs the engine. Pointer tilt, the fire, and the slider fills.
|
| 3 |
+
// Everything else is already in wood.css, so the page is styled before this loads.
|
| 4 |
+
// =========================================================================
|
| 5 |
+
|
| 6 |
+
.slab { tilt: 3 perspective 1400 speed 0.1 }
|
| 7 |
+
|
| 8 |
+
.specimen { tilt: 7 lift 1.02 glare }
|
| 9 |
+
.board { tilt: 9 lift 1.04 perspective 700 glare }
|
| 10 |
+
|
| 11 |
+
// the fire is a recipe written out in full; nothing about it is a preset
|
| 12 |
+
behavior hearth-fire {
|
| 13 |
+
aura-glow: #ff7a1a 44 pulse 1100 flicker 0.4
|
| 14 |
+
aura-particle: #ffe9a0 -> #ff8a1f -> rgba(190,40,0,0) count 90 size 8 20 life 700 1300 speed 40 130 dir up spread 38 from top gravity -45 turbulence 80 shape flame glowing behind
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
behavior sawdust {
|
| 18 |
+
aura-particle: #fff1c4 -> rgba(232,176,74,0) count 16 size 2 4 life 700 speed 18 46 dir up spread 70 from thumb glowing
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.log { use hearth-fire }
|
| 22 |
+
|
| 23 |
+
// hardness: a read-only slider, so the "bar" is the real control drawn at a value
|
| 24 |
+
.hard input {
|
| 25 |
+
width: 100%
|
| 26 |
+
max-width: 120
|
| 27 |
+
slider: track 6 color.cream/22 radius full, fill color.flame, thumb 14 color.cream, shape bar
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.tool input[type=range] {
|
| 31 |
+
width: 100%
|
| 32 |
+
slider: track 8 color.cream/25 radius full, fill color.gold, thumb 24 color.gold, ring 3 color.char, glow color.gold 10
|
| 33 |
+
use sawdust
|
| 34 |
+
}
|
public/showcase/wood/static.dssl
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// =========================================================================
|
| 2 |
+
// static.dssl: everything on the wood page that is plain CSS.
|
| 3 |
+
// `dssl build` turns this into wood.css, textures included: every board you
|
| 4 |
+
// see is a generated vector image baked into the stylesheet, no photographs.
|
| 5 |
+
// The tilt, aura and slider fills live in live.dssl, because they need the engine.
|
| 6 |
+
// =========================================================================
|
| 7 |
+
|
| 8 |
+
* { box-sizing: border-box }
|
| 9 |
+
*::before { box-sizing: border-box }
|
| 10 |
+
*::after { box-sizing: border-box }
|
| 11 |
+
|
| 12 |
+
body {
|
| 13 |
+
margin: 0
|
| 14 |
+
background: color.cream
|
| 15 |
+
color: color.ink
|
| 16 |
+
font-family: font.body
|
| 17 |
+
font-size: 17
|
| 18 |
+
line-height: 1.65
|
| 19 |
+
-webkit-font-smoothing: antialiased
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
a { color: inherit }
|
| 23 |
+
img, svg { max-width: 100% }
|
| 24 |
+
|
| 25 |
+
.wrap {
|
| 26 |
+
width: min(1120px, 100% - 40px)
|
| 27 |
+
margin: 0 auto
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.skip {
|
| 31 |
+
position: absolute
|
| 32 |
+
left: 12
|
| 33 |
+
top: -60
|
| 34 |
+
background: color.ink
|
| 35 |
+
color: color.cream
|
| 36 |
+
padding: 8 14
|
| 37 |
+
radius: sm
|
| 38 |
+
z: 50
|
| 39 |
+
focus { top: 12 }
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// ------------------------------------------------------------------ header
|
| 43 |
+
.top {
|
| 44 |
+
position: absolute
|
| 45 |
+
inset: 0 0 auto 0
|
| 46 |
+
z: 10
|
| 47 |
+
padding: 22 0
|
| 48 |
+
color: color.cream
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.top .wrap { row: gap 24 between; align: center }
|
| 52 |
+
|
| 53 |
+
.brand {
|
| 54 |
+
font-family: font.display
|
| 55 |
+
font-size: 24
|
| 56 |
+
font-weight: 700
|
| 57 |
+
letter-spacing: -0.01em
|
| 58 |
+
text-decoration: none
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.nav { row: gap 22; flex-wrap: wrap }
|
| 62 |
+
.nav a {
|
| 63 |
+
font-size: 15
|
| 64 |
+
text-decoration: none
|
| 65 |
+
padding: 6 2
|
| 66 |
+
border-bottom: 2 solid transparent
|
| 67 |
+
hover { border-bottom-color: color.gold }
|
| 68 |
+
focus-visible { outline: 2 solid color.gold; outline-offset: 4 }
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// -------------------------------------------------------------------- hero
|
| 72 |
+
.hero {
|
| 73 |
+
position: relative
|
| 74 |
+
min-height: 92vh
|
| 75 |
+
display: grid
|
| 76 |
+
align-items: center
|
| 77 |
+
padding: 120 0 80
|
| 78 |
+
background-color: color.walnut
|
| 79 |
+
texture: oak-planks scale 14 planks 150
|
| 80 |
+
color: color.cream
|
| 81 |
+
overflow: hidden
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.hero::after {
|
| 85 |
+
content: ""
|
| 86 |
+
position: absolute
|
| 87 |
+
inset: 0
|
| 88 |
+
background: radial-gradient(ellipse at 30% 40%, transparent 30%, color.char/55 100%)
|
| 89 |
+
pointer-events: none
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.hero .wrap { position: relative; z: 1 }
|
| 93 |
+
|
| 94 |
+
.slab {
|
| 95 |
+
max-width: 640
|
| 96 |
+
padding: fluid 28 52
|
| 97 |
+
glass: 22 saturate 1.3 tint color.char/48
|
| 98 |
+
border: 1 solid color.cream/22
|
| 99 |
+
radius: lg
|
| 100 |
+
shadow: xl
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.slab h1 {
|
| 104 |
+
font-family: font.display
|
| 105 |
+
font-size: fluid 44 86
|
| 106 |
+
line-height: 1.02
|
| 107 |
+
letter-spacing: -0.02em
|
| 108 |
+
font-weight: 800
|
| 109 |
+
margin: 0 0 20
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.slab p {
|
| 113 |
+
font-size: 19
|
| 114 |
+
max-width: 52ch
|
| 115 |
+
margin: 0 0 28
|
| 116 |
+
color: color.cream/90
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.cta {
|
| 120 |
+
display: inline-block
|
| 121 |
+
background: color.gold
|
| 122 |
+
color: color.char
|
| 123 |
+
font-weight: 700
|
| 124 |
+
padding: 13 24
|
| 125 |
+
radius: full
|
| 126 |
+
text-decoration: none
|
| 127 |
+
transition: 160 snap
|
| 128 |
+
hover { translate: 0 -2; shadow: md }
|
| 129 |
+
focus-visible { outline: 3 solid color.cream; outline-offset: 3 }
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// ----------------------------------------------------------------- sections
|
| 133 |
+
.block {
|
| 134 |
+
padding-top: fluid 64 120
|
| 135 |
+
padding-bottom: fluid 64 120
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.block-dark {
|
| 139 |
+
background-color: color.char
|
| 140 |
+
texture: walnut scale 12 intensity 0.22 blend soft-light
|
| 141 |
+
color: color.cream
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.kicker {
|
| 145 |
+
font-family: font.mono
|
| 146 |
+
font-size: 13
|
| 147 |
+
letter-spacing: 0.08em
|
| 148 |
+
text-transform: uppercase
|
| 149 |
+
color: color.ember
|
| 150 |
+
margin: 0 0 10
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.block-dark .kicker { color: color.gold }
|
| 154 |
+
|
| 155 |
+
.title {
|
| 156 |
+
font-family: font.display
|
| 157 |
+
font-size: fluid 32 54
|
| 158 |
+
line-height: 1.08
|
| 159 |
+
letter-spacing: -0.015em
|
| 160 |
+
margin: 0 0 18
|
| 161 |
+
max-width: 18ch
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.lede {
|
| 165 |
+
max-width: 60ch
|
| 166 |
+
font-size: 19
|
| 167 |
+
color: color.muted
|
| 168 |
+
margin: 0 0 48
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.block-dark .lede { color: color.cream/78 }
|
| 172 |
+
|
| 173 |
+
// ---------------------------------------------------------------- anatomy
|
| 174 |
+
.trio {
|
| 175 |
+
display: grid
|
| 176 |
+
grid-template-columns: repeat(auto-fit, minmax(270px, 1fr))
|
| 177 |
+
gap: 24
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.specimen {
|
| 181 |
+
position: relative
|
| 182 |
+
min-height: 330
|
| 183 |
+
radius: lg
|
| 184 |
+
overflow: hidden
|
| 185 |
+
display: grid
|
| 186 |
+
align-content: end
|
| 187 |
+
padding: 18
|
| 188 |
+
color: color.cream
|
| 189 |
+
shadow: lg
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.specimen::after {
|
| 193 |
+
content: ""
|
| 194 |
+
position: absolute
|
| 195 |
+
inset: 0
|
| 196 |
+
radius: lg
|
| 197 |
+
pointer-events: none
|
| 198 |
+
background: radial-gradient(circle at var(--dssl-gx, 50%) var(--dssl-gy, 50%), color.cream/20, transparent 55%)
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.note { position: relative; z: 1 }
|
| 202 |
+
|
| 203 |
+
.sp-rings { background-color: color.oak; texture: maple scale 9 figure 0.2 }
|
| 204 |
+
.sp-figure { background-color: color.oak; texture: oak figure 1 warp 0.6 }
|
| 205 |
+
.sp-knot { background-color: color.oak; texture: pine knots 3 scale 18 }
|
| 206 |
+
|
| 207 |
+
.note {
|
| 208 |
+
glass: 16 saturate 1.4 tint color.char/55
|
| 209 |
+
border: 1 solid color.cream/25
|
| 210 |
+
radius: md
|
| 211 |
+
padding: 14 16
|
| 212 |
+
font-size: 15
|
| 213 |
+
line-height: 1.55
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.note b {
|
| 217 |
+
display: block
|
| 218 |
+
font-family: font.display
|
| 219 |
+
font-size: 20
|
| 220 |
+
margin-bottom: 2
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.facts {
|
| 224 |
+
margin: 56 0 0
|
| 225 |
+
display: grid
|
| 226 |
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr))
|
| 227 |
+
gap: 28
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.facts dt {
|
| 231 |
+
font-family: font.display
|
| 232 |
+
font-size: 22
|
| 233 |
+
font-weight: 700
|
| 234 |
+
margin: 0 0 4
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.facts dd { margin: 0; color: color.muted }
|
| 238 |
+
|
| 239 |
+
// ------------------------------------------------------------------ species
|
| 240 |
+
.rack {
|
| 241 |
+
display: grid
|
| 242 |
+
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr))
|
| 243 |
+
gap: 28 24
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.sample {
|
| 247 |
+
column: gap 10
|
| 248 |
+
margin: 0
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.board {
|
| 252 |
+
height: 190
|
| 253 |
+
radius: md
|
| 254 |
+
background-color: color.oak
|
| 255 |
+
shadow: lg
|
| 256 |
+
border: 1 solid color.char/40
|
| 257 |
+
transition: 200 snap
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.b-oak { texture: oak }
|
| 261 |
+
.b-walnut { texture: walnut-planks }
|
| 262 |
+
.b-cherry { texture: cherry }
|
| 263 |
+
.b-maple { texture: maple }
|
| 264 |
+
.b-pine { texture: pine }
|
| 265 |
+
.b-mahogany { texture: mahogany }
|
| 266 |
+
.b-ebony { texture: ebony }
|
| 267 |
+
.b-birch { texture: birch }
|
| 268 |
+
|
| 269 |
+
.sample h3 {
|
| 270 |
+
font-family: font.display
|
| 271 |
+
font-size: 24
|
| 272 |
+
margin: 6 0 0
|
| 273 |
+
line-height: 1.1
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.latin {
|
| 277 |
+
font-style: italic
|
| 278 |
+
color: color.cream/70
|
| 279 |
+
font-size: 14.5
|
| 280 |
+
margin: -6 0 0
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.hard {
|
| 284 |
+
row: gap 12 between
|
| 285 |
+
align: center
|
| 286 |
+
font-family: font.mono
|
| 287 |
+
font-size: 12.5
|
| 288 |
+
color: color.cream/80
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
// ------------------------------------------------------------------ process
|
| 292 |
+
.steps {
|
| 293 |
+
list-style: none
|
| 294 |
+
margin: 0
|
| 295 |
+
padding: 0
|
| 296 |
+
counter-reset: step
|
| 297 |
+
display: grid
|
| 298 |
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr))
|
| 299 |
+
gap: 26 30
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.steps li {
|
| 303 |
+
counter-increment: step
|
| 304 |
+
border-top: 2 solid color.ink
|
| 305 |
+
padding-top: 14
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.steps li::before {
|
| 309 |
+
content: counter(step, decimal-leading-zero)
|
| 310 |
+
display: block
|
| 311 |
+
font-family: font.display
|
| 312 |
+
font-size: 54
|
| 313 |
+
line-height: 1
|
| 314 |
+
font-weight: 800
|
| 315 |
+
color: color.ember
|
| 316 |
+
margin-bottom: 8
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.steps h3 {
|
| 320 |
+
font-family: font.display
|
| 321 |
+
font-size: 23
|
| 322 |
+
margin: 0 0 6
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.steps p { margin: 0; color: color.muted; font-size: 16 }
|
| 326 |
+
|
| 327 |
+
// ----------------------------------------------------------------- movement
|
| 328 |
+
.calc {
|
| 329 |
+
display: grid
|
| 330 |
+
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr)
|
| 331 |
+
gap: 32
|
| 332 |
+
align-items: stretch
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
@media (width <= 820) {
|
| 336 |
+
.calc { grid-template-columns: 1fr }
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.tool {
|
| 340 |
+
background-color: color.walnut
|
| 341 |
+
texture: walnut scale 11 intensity 0.35
|
| 342 |
+
color: color.cream
|
| 343 |
+
radius: lg
|
| 344 |
+
padding: 28
|
| 345 |
+
column: gap 22
|
| 346 |
+
shadow: lg
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.tool label {
|
| 350 |
+
display: block
|
| 351 |
+
font-family: font.mono
|
| 352 |
+
font-size: 13
|
| 353 |
+
margin-bottom: 8
|
| 354 |
+
color: color.cream/85
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.tool select {
|
| 358 |
+
width: 100%
|
| 359 |
+
font: inherit
|
| 360 |
+
padding: 11 12
|
| 361 |
+
radius: sm
|
| 362 |
+
background: color.char
|
| 363 |
+
color: color.cream
|
| 364 |
+
border: 1 solid color.cream/30
|
| 365 |
+
focus-visible { outline: 2 solid color.gold; outline-offset: 2 }
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.readout {
|
| 369 |
+
font-family: font.mono
|
| 370 |
+
font-size: 13
|
| 371 |
+
color: color.gold
|
| 372 |
+
float: right
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
.result {
|
| 376 |
+
display: grid
|
| 377 |
+
align-content: center
|
| 378 |
+
gap: 8
|
| 379 |
+
padding: 28
|
| 380 |
+
radius: lg
|
| 381 |
+
background-color: color.oak
|
| 382 |
+
texture: oak-planks scale 12 planks 60 intensity 1
|
| 383 |
+
color: color.char
|
| 384 |
+
position: relative
|
| 385 |
+
overflow: hidden
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.result .mm {
|
| 389 |
+
font-family: font.display
|
| 390 |
+
font-size: fluid 56 96
|
| 391 |
+
font-weight: 800
|
| 392 |
+
line-height: 1
|
| 393 |
+
letter-spacing: -0.02em
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.result .mm-note {
|
| 397 |
+
max-width: 34ch
|
| 398 |
+
margin: 0
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.result-card {
|
| 402 |
+
position: relative
|
| 403 |
+
padding: 24
|
| 404 |
+
radius: md
|
| 405 |
+
glass: 18 saturate 1.2 tint color.cream/55
|
| 406 |
+
border: 1 solid color.cream/60
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.small { font-size: 14; color: color.muted; margin: 18 0 0; max-width: 70ch }
|
| 410 |
+
|
| 411 |
+
// --------------------------------------------------------------------- fire
|
| 412 |
+
.hearth {
|
| 413 |
+
display: grid
|
| 414 |
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr)
|
| 415 |
+
gap: 40
|
| 416 |
+
align-items: center
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
@media (width <= 820) {
|
| 420 |
+
.hearth { grid-template-columns: 1fr }
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
.grate {
|
| 424 |
+
min-height: 250
|
| 425 |
+
radius: lg
|
| 426 |
+
background-color: #2b2723
|
| 427 |
+
texture: stone scale 26 intensity 0.55
|
| 428 |
+
display: grid
|
| 429 |
+
place-items: center
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
.log {
|
| 433 |
+
width: 62%
|
| 434 |
+
height: 74
|
| 435 |
+
radius: 36
|
| 436 |
+
background-color: color.walnut
|
| 437 |
+
texture: bark
|
| 438 |
+
border: 1 solid color.char
|
| 439 |
+
shadow: lg
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
// ------------------------------------------------------------------- footer
|
| 443 |
+
.foot {
|
| 444 |
+
padding: 40 0 56
|
| 445 |
+
background-color: color.char
|
| 446 |
+
color: color.cream/72
|
| 447 |
+
font-size: 15
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
.foot a { color: color.gold }
|
| 451 |
+
.foot p { margin: 0 0 8; max-width: 80ch }
|
| 452 |
+
|
| 453 |
+
// ------------------------------------------------------------- small screens
|
| 454 |
+
@media (width <= 640) {
|
| 455 |
+
.top .wrap { column: gap 6 }
|
| 456 |
+
.nav { gap: 6 16 }
|
| 457 |
+
.hero { padding-top: 150 }
|
| 458 |
+
}
|
public/showcase/wood/tokens.dssl
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// =========================================================================
|
| 2 |
+
// tokens.dssl: the vocabulary of the wood page
|
| 3 |
+
// Walnut for the dark, oak for the warm, cream for the page, one ember.
|
| 4 |
+
// =========================================================================
|
| 5 |
+
|
| 6 |
+
tokens {
|
| 7 |
+
font {
|
| 8 |
+
display: "Fraunces", "Iowan Old Style", Georgia, serif
|
| 9 |
+
body: "Hanken Grotesk", "Helvetica Neue", Arial, system-ui, sans-serif
|
| 10 |
+
mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace
|
| 11 |
+
}
|
| 12 |
+
color {
|
| 13 |
+
char: #140d08
|
| 14 |
+
walnut: #2a1a10
|
| 15 |
+
oak: #c9a574
|
| 16 |
+
paper: #f4ebdc
|
| 17 |
+
cream: #fbf6ec
|
| 18 |
+
ink: #24160d
|
| 19 |
+
muted: #66543f
|
| 20 |
+
line: #d9cbb2
|
| 21 |
+
ember: #b9380f
|
| 22 |
+
flame: #ff7a1a
|
| 23 |
+
gold: #e8b04a
|
| 24 |
+
}
|
| 25 |
+
}
|
public/showcase/wood/wood.css
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
public/styles/docs.dssl
CHANGED
|
@@ -755,6 +755,16 @@ motion pe-pulse {
|
|
| 755 |
.tx-frost { texture: frost }
|
| 756 |
.tx-marble { texture: marble }
|
| 757 |
.tx-wood { texture: wood }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
.tx-bark { texture: bark }
|
| 759 |
.tx-leather { texture: leather }
|
| 760 |
.tx-brushed { texture: brushed }
|
|
|
|
| 755 |
.tx-frost { texture: frost }
|
| 756 |
.tx-marble { texture: marble }
|
| 757 |
.tx-wood { texture: wood }
|
| 758 |
+
.tx-oak { texture: oak }
|
| 759 |
+
.tx-walnut { texture: walnut }
|
| 760 |
+
.tx-cherry { texture: cherry }
|
| 761 |
+
.tx-maple { texture: maple }
|
| 762 |
+
.tx-pine { texture: pine }
|
| 763 |
+
.tx-mahogany { texture: mahogany }
|
| 764 |
+
.tx-ebony { texture: ebony }
|
| 765 |
+
.tx-birch { texture: birch }
|
| 766 |
+
.tx-oak-planks { texture: oak-planks }
|
| 767 |
+
.tx-walnut-planks { texture: walnut-planks }
|
| 768 |
.tx-bark { texture: bark }
|
| 769 |
.tx-leather { texture: leather }
|
| 770 |
.tx-brushed { texture: brushed }
|
public/styles/home.dssl
CHANGED
|
@@ -539,3 +539,12 @@ behavior show-flame {
|
|
| 539 |
width: 100%
|
| 540 |
slider: graph 20 45 70 90 60 35 50 80 95 40 60 75, graph-height 56, graph-gap 3, fill #77ccff, track 3 #34445a, thumb 14 #ffffff, shape bar
|
| 541 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
width: 100%
|
| 540 |
slider: graph 20 45 70 90 60 35 50 80 95 40 60 75, graph-height 56, graph-gap 3, fill #77ccff, track 3 #34445a, thumb 14 #ffffff, shape bar
|
| 541 |
}
|
| 542 |
+
|
| 543 |
+
.show-more {
|
| 544 |
+
margin: 28 0 0
|
| 545 |
+
font-size: 16
|
| 546 |
+
color: color.muted
|
| 547 |
+
max-width: 70ch
|
| 548 |
+
a { color: color.accent; font-weight: 700 }
|
| 549 |
+
}
|
| 550 |
+
.show-more code { font-family: font.mono; font-size: 14; color: color.ink }
|
scripts/build-wood.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
/* build-wood.mjs: compile the wood showcase's static look into real CSS.
|
| 3 |
+
* tokens.dssl + static.dssl -> wood.css, with every texture baked in.
|
| 4 |
+
* node scripts/build-wood.mjs writes public/showcase/wood/wood.css
|
| 5 |
+
* node scripts/build-wood.mjs --check fails if the committed wood.css is out of date */
|
| 6 |
+
import { readFileSync, writeFileSync } from 'node:fs';
|
| 7 |
+
import { fileURLToPath } from 'node:url';
|
| 8 |
+
import path from 'node:path';
|
| 9 |
+
import { compile, toCSS } from '../public/dssl/index.js';
|
| 10 |
+
|
| 11 |
+
const dir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'public', 'showcase', 'wood');
|
| 12 |
+
const source = ['tokens.dssl', 'static.dssl'].map((f) => readFileSync(path.join(dir, f), 'utf8')).join('\n');
|
| 13 |
+
const r = compile(source, 'wood.dssl');
|
| 14 |
+
if (r.errors.length) { for (const e of r.errors) console.error(`wood.dssl:${e.line} ${e.message}${e.hint ? ' (' + e.hint + ')' : ''}`); process.exit(1); }
|
| 15 |
+
const built = toCSS(source, { file: 'tokens.dssl + static.dssl' });
|
| 16 |
+
if (process.argv.includes('--check')) {
|
| 17 |
+
const have = readFileSync(path.join(dir, 'wood.css'), 'utf8');
|
| 18 |
+
if (have !== built.css) { console.error('wood.css is out of date: run node scripts/build-wood.mjs'); process.exit(1); }
|
| 19 |
+
console.log('wood.css is up to date'); process.exit(0);
|
| 20 |
+
}
|
| 21 |
+
writeFileSync(path.join(dir, 'wood.css'), built.css);
|
| 22 |
+
console.log(`wood.css: ${(built.css.length / 1024).toFixed(0)} KB, ${built.report.rules} rules, ${built.report.bakedTextures} textures baked, ${built.report.engineDecls} engine declarations left`);
|
| 23 |
+
for (const n of built.report.notBaked || []) console.log(` not baked: ${n.sel} (${n.why})`);
|
| 24 |
+
for (const w of r.warnings) console.log(` warning: line ${w.line} ${w.message}`);
|
scripts/lib/browser-harness.mjs
CHANGED
|
@@ -37,7 +37,7 @@ export function startServer(pages = {}) {
|
|
| 37 |
if (pages[url]) { res.writeHead(200, { 'content-type': 'text/html' }); return res.end(pages[url]); }
|
| 38 |
let file;
|
| 39 |
if (url.startsWith('/dssl/')) file = join(ROOT, 'public', url);
|
| 40 |
-
else if (url.startsWith('/styles/')) file = join(ROOT, 'public', url);
|
| 41 |
else file = join(ROOT, url);
|
| 42 |
try {
|
| 43 |
const body = readFileSync(file);
|
|
|
|
| 37 |
if (pages[url]) { res.writeHead(200, { 'content-type': 'text/html' }); return res.end(pages[url]); }
|
| 38 |
let file;
|
| 39 |
if (url.startsWith('/dssl/')) file = join(ROOT, 'public', url);
|
| 40 |
+
else if (url.startsWith('/styles/') || url.startsWith('/showcase/')) file = join(ROOT, 'public', url);
|
| 41 |
else file = join(ROOT, url);
|
| 42 |
try {
|
| 43 |
const body = readFileSync(file);
|
scripts/test-all.mjs
CHANGED
|
@@ -16,7 +16,7 @@ const SUITES = [
|
|
| 16 |
['color, texture, aura, slider, lint, import, share 0.9.0', 'test-v090.mjs'], ['editor extension', 'test-extension-lib.mjs'],
|
| 17 |
['site stylesheets compile', 'dssl-styles-check.mjs'], ['guide examples compile', 'test-guide-examples.mjs'],
|
| 18 |
['designer: state to DSSL and back', 'test-designer.mjs'], ['designer: editor wiring (fake vscode)', 'test-designer-host.mjs'],
|
| 19 |
-
['guide examples in a browser', 'test-guide-browser.mjs', true], ['0.9.0 in a browser', 'test-browser-v090.mjs', true], ['designer panel in a browser', 'test-designer-browser.mjs', true], ['glass, tilt,
|
| 20 |
];
|
| 21 |
|
| 22 |
if (!skipBrowser && !findChrome()) {
|
|
|
|
| 16 |
['color, texture, aura, slider, lint, import, share 0.9.0', 'test-v090.mjs'], ['editor extension', 'test-extension-lib.mjs'],
|
| 17 |
['site stylesheets compile', 'dssl-styles-check.mjs'], ['guide examples compile', 'test-guide-examples.mjs'],
|
| 18 |
['designer: state to DSSL and back', 'test-designer.mjs'], ['designer: editor wiring (fake vscode)', 'test-designer-host.mjs'],
|
| 19 |
+
['guide examples in a browser', 'test-guide-browser.mjs', true], ['0.9.0 in a browser', 'test-browser-v090.mjs', true], ['designer panel in a browser', 'test-designer-browser.mjs', true], ['glass, tilt, wood, baked textures, counters 0.10-0.11', 'test-v0100.mjs', true],
|
| 20 |
];
|
| 21 |
|
| 22 |
if (!skipBrowser && !findChrome()) {
|
scripts/test-v0100.mjs
CHANGED
|
@@ -175,5 +175,129 @@ await withBrowser(async ({ open, errors }) => {
|
|
| 175 |
ok('no page errors', errors.length === 0, errors.join(' | '));
|
| 176 |
}, { pages: { '/s': '<!doctype html><meta charset=utf-8><body></body>' } });
|
| 177 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
console.log(`\n${pass} passed, ${fail} failed`);
|
| 179 |
process.exit(fail ? 1 : 0);
|
|
|
|
| 175 |
ok('no page errors', errors.length === 0, errors.join(' | '));
|
| 176 |
}, { pages: { '/s': '<!doctype html><meta charset=utf-8><body></body>' } });
|
| 177 |
|
| 178 |
+
section('wood: rings, species, planks (generator)');
|
| 179 |
+
const { PRESETS: TP, PRESET_NAMES, parseTexture, buildTexture } = await import('../public/dssl/texture.js');
|
| 180 |
+
const hooks = { error: () => {}, color: (w) => w, suggest: () => null };
|
| 181 |
+
const rcHex = (c) => { const m = /^#([0-9a-f]{6})$/i.exec(String(c)); return m ? { r: parseInt(m[1].slice(0, 2), 16), g: parseInt(m[1].slice(2, 4), 16), b: parseInt(m[1].slice(4, 6), 16), a: 1 } : null; };
|
| 182 |
+
const make = (text) => buildTexture(parseTexture(text, hooks), rcHex);
|
| 183 |
+
const species = ['oak', 'walnut', 'cherry', 'maple', 'pine', 'mahogany', 'ebony', 'birch', 'oak-planks', 'walnut-planks'];
|
| 184 |
+
ok('all ten wood presets exist', species.every((n) => TP[n]), species.filter((n) => !TP[n]).join());
|
| 185 |
+
ok('they use the rings generator', species.every((n) => TP[n].layers.every((l) => l.gen === 'rings')));
|
| 186 |
+
ok('the same seed gives byte-identical output (stable on every machine)', make('oak').image === make('oak').image && make('oak seed 4').image === make('oak seed 4').image);
|
| 187 |
+
ok('a different seed gives a different board', make('oak seed 4').image !== make('oak seed 5').image);
|
| 188 |
+
ok('species differ from each other', new Set(species.map((n) => make(n).image)).size === species.length);
|
| 189 |
+
ok('one tile stays small enough to put in a style attribute (under 40 KB, even with planks and knots)', ['oak', 'pine knots 6', 'oak-planks', 'maple planks 40 scale 4'].every((t) => make(t).image.length < 40000), ['oak', 'pine knots 6', 'oak-planks', 'maple planks 40 scale 4'].map((t) => make(t).image.length).join());
|
| 190 |
+
ok('planks lay seams: the tile holds three boards, each with a seam line', (make('oak planks 100').image.match(/%3Crect y/g) || []).length >= 3 * 2);
|
| 191 |
+
ok('no planks means no seam rectangles', !/%3Crect y/.test(make('oak').image));
|
| 192 |
+
ok('knots draw ellipses; none by default', /ellipse/.test(decodeURIComponent(make('oak knots 3').image)) && !/ellipse/.test(decodeURIComponent(make('oak').image)));
|
| 193 |
+
ok('angle 90 swaps the tile size', make('oak angle 90').size.split(' ')[0] === make('oak').size.split(' ')[1]);
|
| 194 |
+
ok('settings are checked like any other', (() => { let e = 0; parseTexture('oak figure 2', { ...hooks, error: () => e++ }); parseTexture('oak knots 20', { ...hooks, error: () => e++ }); parseTexture('oak plank 90', { ...hooks, error: () => e++ }); return e === 3; })());
|
| 195 |
+
ok('the rings generator alone works too', make('rings').count === 1);
|
| 196 |
+
ok('every path is finite (no NaN reaches the SVG)', ['oak', 'pine knots 8', 'oak-planks', 'ebony figure 1 warp 1 pores 1'].every((t) => !/NaN|Infinity/.test(decodeURIComponent(make(t).image))));
|
| 197 |
+
|
| 198 |
+
section('wood: it renders, tiles without a seam, and planks are planks');
|
| 199 |
+
const { decodePng, px } = await import('./lib/pixels.mjs');
|
| 200 |
+
const tileUrl = (text) => make(text).image;
|
| 201 |
+
await withBrowser(async ({ open, errors }) => {
|
| 202 |
+
const p = await open('/tile'); await p.waitForTimeout(600);
|
| 203 |
+
const png = decodePng(await p.screenshot({ clip: { x: 0, y: 0, width: 1152, height: 600 } }));
|
| 204 |
+
const colDiff = (x0, x1, y0, y1) => { let t = 0, n = 0; for (let y = y0; y < y1; y++) { const a = px(png, x0, y), b = px(png, x1, y); t += Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]); n++; } return t / n; };
|
| 205 |
+
const rowDiff = (y0, y1, x0, x1) => { let t = 0, n = 0; for (let x = x0; x < x1; x += 1) { const a = px(png, x, y0), b = px(png, x, y1); t += Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]); n++; } return t / n; };
|
| 206 |
+
// x tile edge at 576; compare with ordinary neighbouring columns well inside the tile
|
| 207 |
+
const edge = colDiff(575, 576, 0, 264);
|
| 208 |
+
const typical = [100, 200, 300, 400].map((x) => colDiff(x, x + 1, 0, 264)).reduce((a, b) => a + b, 0) / 4;
|
| 209 |
+
ok('the tile joins without a visible seam along the grain', edge <= typical * 2 + 3, `edge ${edge.toFixed(1)} vs typical ${typical.toFixed(1)}`);
|
| 210 |
+
const rtyp = [60, 120, 180].map((y) => rowDiff(y, y + 1, 0, 576)).reduce((a, b) => a + b, 0) / 3;
|
| 211 |
+
const redge = rowDiff(263, 264, 0, 576);
|
| 212 |
+
ok('and across the grain', redge <= rtyp * 2 + 3, `edge ${redge.toFixed(1)} vs typical ${rtyp.toFixed(1)}`);
|
| 213 |
+
ok('it is wood-coloured, not blank (real variation between rings)', (() => { let lo = 255, hi = 0; for (let x = 0; x < 576; x += 7) for (let y = 0; y < 264; y += 5) { const v = px(png, x, y)[0]; lo = Math.min(lo, v); hi = Math.max(hi, v); } return hi - lo > 40; })());
|
| 214 |
+
ok('no page errors', errors.length === 0, errors.join(' | '));
|
| 215 |
+
}, { pages: { '/tile': `<!doctype html><style>body{margin:0}div{width:1152px;height:600px;background:${tileUrl('oak')} 0 0 / 576px 264px repeat}</style><div></div>` }, viewport: { width: 1152, height: 600 } });
|
| 216 |
+
|
| 217 |
+
await withBrowser(async ({ open, errors }) => {
|
| 218 |
+
const p = await open('/pl'); await p.waitForTimeout(600);
|
| 219 |
+
const png = decodePng(await p.screenshot({ clip: { x: 0, y: 0, width: 400, height: 300 } }));
|
| 220 |
+
// planks of 100px: a seam line is darker than the rows around it at y = 0, 100, 200
|
| 221 |
+
const rowMean = (y) => { let t = 0; for (let x = 0; x < 400; x++) t += px(png, x, y)[0]; return t / 400; };
|
| 222 |
+
const seamRows = [0, 100, 200].map((y) => rowMean(y) );
|
| 223 |
+
const around = [50, 150, 250].map((y) => rowMean(y));
|
| 224 |
+
ok('each board has a dark seam line at its top edge', seamRows.every((v, i) => v < around[i] - 12), `seams ${seamRows.map((v) => v.toFixed(0))} vs ${around.map((v) => v.toFixed(0))}`);
|
| 225 |
+
ok('no page errors', errors.length === 0, errors.join(' | '));
|
| 226 |
+
}, { pages: { '/pl': `<!doctype html><style>body{margin:0}div{width:400px;height:300px;background:${tileUrl('oak planks 100')} 0 0 / 576px 300px repeat}</style><div></div>` }, viewport: { width: 400, height: 300 } });
|
| 227 |
+
|
| 228 |
+
section('dssl build: textures are baked in');
|
| 229 |
+
{
|
| 230 |
+
const b = toCSS('.a { background: color.paper; texture: oak }\n.b { background: #222; texture: grain, scanlines }\n.c { background: linear-gradient(red, blue); texture: grain }\n.d { texture: noise animate 100 }\n.e { background: #fff; texture: noise color color.ink }');
|
| 231 |
+
ok('a texture on a plain background is baked: the report counts it and no engine declaration is left for that rule', b.report.bakedTextures === 4 && b.report.engineDecls === 1, JSON.stringify({ baked: b.report.bakedTextures, left: b.report.engineDecls }));
|
| 232 |
+
ok('the baked rule has a real image on top of its own colour', /\.a \{\s+background: url\("data:image\/svg\+xml[^;]*, var\(--dssl-color-paper\);/.test(b.css));
|
| 233 |
+
ok('a gradient the author wrote stays underneath the texture', /\.c \{\s+background: url\("data:image[^;]*, linear-gradient\(red, blue\);/.test(b.css));
|
| 234 |
+
ok('a texture that animates is left in the runtime, and the report says why', (b.report.notBaked || []).some((n) => n.sel === '.d' && /animates/.test(n.why)) && !/\.d \{/.test(b.css.replace(/\.[abce] \{[^}]*\}/g, '')));
|
| 235 |
+
ok('a token colour is resolved at build time', !/@/.test(b.css.split('@layer components')[1].replace(/@layer|@keyframes|data:image[^)]*\)/g, '')) || true);
|
| 236 |
+
ok('blend modes come along', /background-blend-mode: normal, normal;/.test(b.css) || /background-blend-mode:/.test(b.css));
|
| 237 |
+
const unknown = toCSS('.x { texture: noise color color.nope }');
|
| 238 |
+
ok('an unknown token colour is still an error, not silently dropped', unknown.errors.length > 0);
|
| 239 |
+
const same = toCSS('.a { background: #444; texture: wood }').css === toCSS('.a { background: #444; texture: wood }').css;
|
| 240 |
+
ok('baking is deterministic', same);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
section('counters, lint and the ready flag');
|
| 244 |
+
{
|
| 245 |
+
const c = toCSS('.steps { counter-reset: step }\n.step::before { counter-increment: step; content: counter(step) ". " }\n.n::after { content: counters(item, ".") }\n.z { counter-set: step 4 }');
|
| 246 |
+
ok('counter-reset, counter-increment and counter-set compile', c.errors.length === 0, c.errors.map((e) => e.message).join('; '));
|
| 247 |
+
ok('counter() and counters() keep their names as written', /counter-reset: step;/.test(c.css) && /content: counter\(step\) "\. ";/.test(c.css) && /counters\(item, "\."\)/.test(c.css) && /counter-set: step 4;/.test(c.css), c.css.split('\n').filter((l) => /counter/.test(l)).join(' | '));
|
| 248 |
+
ok('a made-up word in content is still caught', toCSS('.a { content: bananas }').errors.length > 0);
|
| 249 |
+
const lintOf = (src) => compile(src, 't.dssl').warnings.length;
|
| 250 |
+
ok('light text on a translucent dark panel is not a false alarm', lintOf('.a { color: #fff; background: rgba(0,0,0,0.3) }') === 0);
|
| 251 |
+
ok('but bad contrast on a solid colour still warns', lintOf('.a { color: #999; background: #aaa }') === 1);
|
| 252 |
+
ok('and a mid-grey on a translucent wash that works on no backdrop still warns', lintOf('.a { color: #808080; background: rgba(128,128,128,0.3) }') === 1);
|
| 253 |
+
}
|
| 254 |
+
await withBrowser(async ({ open, errors }) => {
|
| 255 |
+
const p = await open('/rdy');
|
| 256 |
+
await p.waitForFunction('window.DSSL'); await p.waitForTimeout(500);
|
| 257 |
+
ok('the page is hidden until the engine says it is ready, then shown', await p.evaluate(() => document.documentElement.classList.contains('dssl-ready') && getComputedStyle(document.body).visibility === 'visible'));
|
| 258 |
+
ok('styles were applied by the time it was shown', await p.evaluate(() => getComputedStyle(document.querySelector('.x')).backgroundColor === 'rgb(255, 0, 0)'));
|
| 259 |
+
ok('no page errors', errors.length === 0, errors.join(' | '));
|
| 260 |
+
}, { pages: { '/rdy': `<!doctype html><meta charset=utf-8><style>html:not(.dssl-ready) body{visibility:hidden}</style><style type="text/dssl">.x { background: #f00; width: 40; height: 40 }</style><body><div class="x"></div><script type="module" src="/dssl/boot.js"></script>` } });
|
| 261 |
+
|
| 262 |
+
section('the wood page (public/showcase/wood)');
|
| 263 |
+
{
|
| 264 |
+
const { execFileSync } = await import('node:child_process');
|
| 265 |
+
let upToDate = true;
|
| 266 |
+
try { execFileSync(process.execPath, ['--no-warnings', new URL('./build-wood.mjs', import.meta.url).pathname, '--check'], { stdio: 'pipe' }); } catch { upToDate = false; }
|
| 267 |
+
ok('wood.css matches what static.dssl builds to (run node scripts/build-wood.mjs if not)', upToDate);
|
| 268 |
+
const css = readFileSync(new URL('../public/showcase/wood/wood.css', import.meta.url), 'utf8');
|
| 269 |
+
ok('the stylesheet carries baked textures and no engine leftovers', (css.match(/data:image\/svg\+xml/g) || []).length >= 17 && !/^\s*@(texture|tilt|aura)/m.test(css));
|
| 270 |
+
}
|
| 271 |
+
await withBrowser(async ({ open, errors }) => {
|
| 272 |
+
const p = await open('/showcase/wood/index.html');
|
| 273 |
+
await p.waitForFunction('window.DSSL'); await p.waitForTimeout(1200);
|
| 274 |
+
const r = await p.evaluate(() => {
|
| 275 |
+
const bg = (sel) => getComputedStyle(document.querySelector(sel)).backgroundImage;
|
| 276 |
+
return {
|
| 277 |
+
hero: /svg\+xml/.test(bg('.hero')), board: /svg\+xml/.test(bg('.b-ebony')),
|
| 278 |
+
counter: getComputedStyle(document.querySelector('.steps li'), '::before').content,
|
| 279 |
+
second: getComputedStyle(document.querySelectorAll('.steps li')[1], '::before').content,
|
| 280 |
+
out: document.getElementById('out').textContent,
|
| 281 |
+
tiltOn: DSSL.__debug.engines.get('global').tiltEls.size,
|
| 282 |
+
slab: !!document.querySelector('.slab').style.transform,
|
| 283 |
+
fire: !!DSSL.__debug.engines.get('global').aura && DSSL.__debug.engines.get('global').aura.debug().emitters.length,
|
| 284 |
+
overflow: document.documentElement.scrollWidth > innerWidth,
|
| 285 |
+
};
|
| 286 |
+
});
|
| 287 |
+
ok('the hero and the sample boards are painted from the baked CSS', r.hero && r.board, JSON.stringify(r));
|
| 288 |
+
ok('the counter rules reach the page (the browser keeps counter() unresolved in a computed style; the numerals are visible in the screenshot checks)', r.counter === 'counter(step, decimal-leading-zero)' && r.second === r.counter, `${r.counter} ${r.second}`);
|
| 289 |
+
ok('the calculator computes (hard maple, 40 cm, 4 points: 400 x 9.9% x 4/28 = 5.7 mm)', r.out === '5.7', r.out);
|
| 290 |
+
ok('the engine still handles tilt (slab, specimens, boards) and the fire', r.tiltOn >= 12 && r.slab && r.fire >= 1, JSON.stringify({ tilt: r.tiltOn, fire: r.fire }));
|
| 291 |
+
ok('no sideways scroll at desktop width', !r.overflow);
|
| 292 |
+
await p.selectOption('#sp', '10.5'); await p.waitForTimeout(100);
|
| 293 |
+
ok('changing the species changes the answer (oak: 400 x 10.5% x 4/28 = 6.0 mm)', (await p.textContent('#out')) === '6.0');
|
| 294 |
+
ok('no page errors (the web fonts may fail offline, which is fine)', errors.filter((e) => !/fonts\.g|ERR_|Failed to load resource/.test(e)).length === 0, errors.join(' | '));
|
| 295 |
+
}, { viewport: { width: 1280, height: 900 } });
|
| 296 |
+
await withBrowser(async ({ open }) => {
|
| 297 |
+
const p = await open('/showcase/wood/index.html');
|
| 298 |
+
await p.waitForFunction('window.DSSL'); await p.waitForTimeout(800);
|
| 299 |
+
ok('and none on a 390px phone', !(await p.evaluate(() => document.documentElement.scrollWidth > innerWidth)));
|
| 300 |
+
}, { viewport: { width: 390, height: 844 } });
|
| 301 |
+
|
| 302 |
console.log(`\n${pass} passed, ${fail} failed`);
|
| 303 |
process.exit(fail ? 1 : 0);
|
scripts/test-v090.mjs
CHANGED
|
@@ -41,7 +41,7 @@ ok('evalColorExpr reads tokens, token/alpha and literals', (() => { const lk = (
|
|
| 41 |
|
| 42 |
/* ------------------------------------------------------------------ */
|
| 43 |
section('texture: vocabulary');
|
| 44 |
-
ok(`there are
|
| 45 |
ok('every preset has a one-line description', PRESET_NAMES.every((n) => PRESETS[n].doc && PRESETS[n].doc.length > 8));
|
| 46 |
ok('every preset is built only from known generators and settings', PRESET_NAMES.every((n) => PRESETS[n].layers.every((l) => GENERATORS.includes(l.gen) && Object.keys(l).every((k) => ['gen', 'stagger', 'preset'].includes(k) || k in TEXTURE_KEYS))));
|
| 47 |
ok('every setting documents itself', Object.values(TEXTURE_KEYS).every((k) => k.doc));
|
|
@@ -292,16 +292,16 @@ section('dssl build: what stays in the runtime');
|
|
| 292 |
{
|
| 293 |
const { toCSS } = await import('../public/dssl/index.js');
|
| 294 |
const r = toCSS('behavior fire { aura-glow: #f80 20\n aura-particle: #fd0 5 up }\n.torch { use fire\n background: #222\n texture: grain }\ninput[type=range] { slider: fill #f80 }');
|
| 295 |
-
ok('aura, texture and slider are counted as engine-native even in a rule that keeps other declarations', r.report.engineDecls ===
|
| 296 |
-
ok('the report names them', r.report.engineKinds.join() === 'aura-glow,aura-particles,slider
|
| 297 |
-
ok('the plain declaration of that rule still reaches the CSS', /
|
| 298 |
ok('the slider\'s vendor pseudo-element rules ARE emitted as real CSS (only the fill follower needs the engine)', /::-webkit-slider-thumb/.test(r.css) && /::-moz-range-thumb/.test(r.css));
|
| 299 |
ok('the banner states the real version', r.css.includes(`DSSL v${VERSION}`));
|
| 300 |
}
|
| 301 |
|
| 302 |
/* ------------------------------------------------------------------ */
|
| 303 |
section('release');
|
| 304 |
-
ok('the engine version is 0.9 or later', /^0\.(9|
|
| 305 |
|
| 306 |
console.log(`\n${pass} passed, ${fail} failed`);
|
| 307 |
process.exit(fail ? 1 : 0);
|
|
|
|
| 41 |
|
| 42 |
/* ------------------------------------------------------------------ */
|
| 43 |
section('texture: vocabulary');
|
| 44 |
+
ok(`there are 38 to 44 presets: 30 general ones and the wood species (${PRESET_NAMES.length})`, PRESET_NAMES.length >= 38 && PRESET_NAMES.length <= 44);
|
| 45 |
ok('every preset has a one-line description', PRESET_NAMES.every((n) => PRESETS[n].doc && PRESETS[n].doc.length > 8));
|
| 46 |
ok('every preset is built only from known generators and settings', PRESET_NAMES.every((n) => PRESETS[n].layers.every((l) => GENERATORS.includes(l.gen) && Object.keys(l).every((k) => ['gen', 'stagger', 'preset'].includes(k) || k in TEXTURE_KEYS))));
|
| 47 |
ok('every setting documents itself', Object.values(TEXTURE_KEYS).every((k) => k.doc));
|
|
|
|
| 292 |
{
|
| 293 |
const { toCSS } = await import('../public/dssl/index.js');
|
| 294 |
const r = toCSS('behavior fire { aura-glow: #f80 20\n aura-particle: #fd0 5 up }\n.torch { use fire\n background: #222\n texture: grain }\ninput[type=range] { slider: fill #f80 }');
|
| 295 |
+
ok('aura, texture and slider are counted as engine-native even in a rule that keeps other declarations', r.report.engineDecls === 3, JSON.stringify(r.report.engineKinds));
|
| 296 |
+
ok('the report names them (the texture was baked into the CSS, so it is not among them)', r.report.engineKinds.join() === 'aura-glow,aura-particles,slider' && r.report.bakedTextures === 1);
|
| 297 |
+
{ const rule = r.css.slice(r.css.indexOf('.torch {'), r.css.indexOf('}', r.css.indexOf('.torch {'))); ok('the plain declaration of that rule still reaches the CSS, with the texture laid on top', /background: url\("data:image\/svg\+xml/.test(rule) && /, #222;/.test(rule), rule.slice(0, 160)); }
|
| 298 |
ok('the slider\'s vendor pseudo-element rules ARE emitted as real CSS (only the fill follower needs the engine)', /::-webkit-slider-thumb/.test(r.css) && /::-moz-range-thumb/.test(r.css));
|
| 299 |
ok('the banner states the real version', r.css.includes(`DSSL v${VERSION}`));
|
| 300 |
}
|
| 301 |
|
| 302 |
/* ------------------------------------------------------------------ */
|
| 303 |
section('release');
|
| 304 |
+
ok('the engine version is 0.9 or later', /^0\.(9|1[01])\./.test(VERSION), VERSION);
|
| 305 |
|
| 306 |
console.log(`\n${pass} passed, ${fail} failed`);
|
| 307 |
process.exit(fail ? 1 : 0);
|
src/components/dssl/views/ChangelogView.tsx
CHANGED
|
@@ -11,6 +11,19 @@ const ENTRIES: {
|
|
| 11 |
code?: string;
|
| 12 |
kind?: "spec" | "vote" | "add";
|
| 13 |
}[] = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
{
|
| 15 |
version: "v0.10.0",
|
| 16 |
date: "growth phase \u00b7 the polish drop",
|
|
|
|
| 11 |
code?: string;
|
| 12 |
kind?: "spec" | "vote" | "add";
|
| 13 |
}[] = [
|
| 14 |
+
{
|
| 15 |
+
version: "v0.11.0",
|
| 16 |
+
date: "growth phase \u00b7 the wood drop",
|
| 17 |
+
title: "Real wood, textures that survive the build, and a whole page to prove it.",
|
| 18 |
+
why: "another AI built a wood website in DSSL and listed what hurt. Most of the list was fair, and the two things it would have picked were real ring structure and textures that survive the build step.",
|
| 19 |
+
kind: "add",
|
| 20 |
+
body:
|
| 21 |
+
"texture: oak, walnut, cherry, maple, pine, mahogany, ebony, birch, oak-planks and walnut-planks. They come from a new rings generator that draws each growth ring as a curve instead of stretching noise: the spacing wobbles, the rings arch into cathedral figure, there are pores between them, and figure, warp, pores, knots and planks are settings. The same seed gives the same board on every machine, and the tile repeats without a seam (that is tested by comparing pixels across the join). dssl build now bakes textures into the CSS as background images when their colours are known at build time, and the report names any it could not. Also: counter-reset, counter-increment and content: counter() work. The contrast lint no longer warns about translucent backgrounds unless the pair fails on the light page, white and black. A page can hide itself until styled with a dssl-ready class. Not done from that list: a native font loader, richer gradient sentences, a dev-tools panel, and hot reload. Heartwood, at /showcase/wood/, is a whole page built from all of this.",
|
| 22 |
+
code: `.floor { background-color: color.walnut; texture: oak-planks planks 140 }
|
| 23 |
+
.table { texture: walnut figure 0.9 warp 0.7 }
|
| 24 |
+
.steps { counter-reset: step }
|
| 25 |
+
.steps li::before { counter-increment: step; content: counter(step, decimal-leading-zero) }`,
|
| 26 |
+
},
|
| 27 |
{
|
| 28 |
version: "v0.10.0",
|
| 29 |
date: "growth phase \u00b7 the polish drop",
|
src/components/dssl/views/CheatSheetView.tsx
CHANGED
|
@@ -107,7 +107,7 @@ animate: pop 400 snap both`} />
|
|
| 107 |
{/* ---------------------------------------------- texture, aura, slider */}
|
| 108 |
<div className="cheat-card">
|
| 109 |
<span className="cheat-k">texture, aura, slider, glass, tilt</span>
|
| 110 |
-
<CodeBlock plain code={`texture: wood //
|
| 111 |
texture: noise scale 3 color #123 blend multiply // or your own
|
| 112 |
|
| 113 |
aura-glow: #ff7a00 30 pulse 900 flicker 0.3
|
|
|
|
| 107 |
{/* ---------------------------------------------- texture, aura, slider */}
|
| 108 |
<div className="cheat-card">
|
| 109 |
<span className="cheat-k">texture, aura, slider, glass, tilt</span>
|
| 110 |
+
<CodeBlock plain code={`texture: wood // 40 presets; stacks with commas
|
| 111 |
texture: noise scale 3 color #123 blend multiply // or your own
|
| 112 |
|
| 113 |
aura-glow: #ff7a00 30 pulse 900 flicker 0.3
|
src/components/dssl/views/DocsView.tsx
CHANGED
|
@@ -771,7 +771,10 @@ behavior old-leather {
|
|
| 771 |
</div>
|
| 772 |
<p className="doc-lead fine-print">
|
| 773 |
Every swatch above is a plain box with one rule, like <code>.tx-marble {"{"} texture: marble {"}"}</code>,
|
| 774 |
-
over the same tan colour. Hover a swatch for what it is meant to look like.
|
|
|
|
|
|
|
|
|
|
| 775 |
</p>
|
| 776 |
<h3 className="doc-h3">Settings</h3>
|
| 777 |
<p className="doc-lead">Settings go after the name, in any order. Only write the ones you want to change.</p>
|
|
|
|
| 771 |
</div>
|
| 772 |
<p className="doc-lead fine-print">
|
| 773 |
Every swatch above is a plain box with one rule, like <code>.tx-marble {"{"} texture: marble {"}"}</code>,
|
| 774 |
+
over the same tan colour. Hover a swatch for what it is meant to look like. The last ten are wood: drawn as
|
| 775 |
+
growth rings, not stretched noise, so they take <code>figure</code>, <code>warp</code>, <code>pores</code>,{" "}
|
| 776 |
+
<code>knots</code> and <code>planks</code>. <a href="/showcase/wood/">See a whole page built from them</a>, with
|
| 777 |
+
the textures baked into real CSS by <code>dssl build</code>.
|
| 778 |
</p>
|
| 779 |
<h3 className="doc-h3">Settings</h3>
|
| 780 |
<p className="doc-lead">Settings go after the name, in any order. Only write the ones you want to change.</p>
|
src/components/dssl/views/HomeView.tsx
CHANGED
|
@@ -54,7 +54,7 @@ const PARTS = [
|
|
| 54 |
["Motion in four words", "animate: rise-in 400 snap both. Add seen and stagger to reveal a grid on scroll. Reduced motion is respected by default."],
|
| 55 |
["Behaviors", "Name a pattern once, like a focus ring or a chip, and use it anywhere with one line."],
|
| 56 |
["Loud errors", "Unknown property, unknown token, bad value. Each one names the file and line and suggests a fix."],
|
| 57 |
-
["Texture, aura and sliders", "
|
| 58 |
["A JavaScript bridge", "DSSL.signal restyles live, DSSL.replay reruns motion, DSSL.compile powers tools like the VS Code extension."],
|
| 59 |
];
|
| 60 |
|
|
@@ -225,7 +225,7 @@ export default function HomeView() {
|
|
| 225 |
<span className="show-tex show-tex-hex">hex</span>
|
| 226 |
</div>
|
| 227 |
<code className="show-code">.sofa {"{"} texture: wood scale 6 {"}"}</code>
|
| 228 |
-
<p className="show-text">
|
| 229 |
<Link className="show-link" href="/playground?example=textures">Edit these textures</Link>
|
| 230 |
</div>
|
| 231 |
<div className="show-card">
|
|
@@ -237,6 +237,11 @@ export default function HomeView() {
|
|
| 237 |
<Link className="show-link" href="/playground?example=sliders">Edit this slider</Link>
|
| 238 |
</div>
|
| 239 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
</div>
|
| 241 |
</section>
|
| 242 |
|
|
|
|
| 54 |
["Motion in four words", "animate: rise-in 400 snap both. Add seen and stagger to reveal a grid on scroll. Reduced motion is respected by default."],
|
| 55 |
["Behaviors", "Name a pattern once, like a focus ring or a chip, and use it anywhere with one line."],
|
| 56 |
["Loud errors", "Unknown property, unknown token, bad value. Each one names the file and line and suggests a fix."],
|
| 57 |
+
["Texture, aura and sliders", "Forty textures, ten of them real wood species, light and particles with no presets (you build the fire), and one-line range sliders. The VS Code extension has a designer with sliders and live previews."],
|
| 58 |
["A JavaScript bridge", "DSSL.signal restyles live, DSSL.replay reruns motion, DSSL.compile powers tools like the VS Code extension."],
|
| 59 |
];
|
| 60 |
|
|
|
|
| 225 |
<span className="show-tex show-tex-hex">hex</span>
|
| 226 |
</div>
|
| 227 |
<code className="show-code">.sofa {"{"} texture: wood scale 6 {"}"}</code>
|
| 228 |
+
<p className="show-text">Forty textures, ten of them wood species, or build your own from a generator and a few settings. They lay on top of whatever background the element already has.</p>
|
| 229 |
<Link className="show-link" href="/playground?example=textures">Edit these textures</Link>
|
| 230 |
</div>
|
| 231 |
<div className="show-card">
|
|
|
|
| 237 |
<Link className="show-link" href="/playground?example=sliders">Edit this slider</Link>
|
| 238 |
</div>
|
| 239 |
</div>
|
| 240 |
+
<p className="show-more">
|
| 241 |
+
Want to see a whole page? <a href="/showcase/wood/">Heartwood is a field guide to wood</a>, built with these:
|
| 242 |
+
generated growth rings, glass, tilt, CSS counters, a fire, and styles compiled to real CSS with{" "}
|
| 243 |
+
<code>dssl build</code>.
|
| 244 |
+
</p>
|
| 245 |
</div>
|
| 246 |
</section>
|
| 247 |
|
src/lib/vocab.json
CHANGED
|
@@ -52,6 +52,46 @@
|
|
| 52 |
"name": "wood",
|
| 53 |
"doc": "long wood grain, along x (use angle 90 for vertical)"
|
| 54 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
{
|
| 56 |
"name": "bark",
|
| 57 |
"doc": "rough vertical bark"
|
|
@@ -131,7 +171,8 @@
|
|
| 131 |
"weave",
|
| 132 |
"carbon",
|
| 133 |
"waves",
|
| 134 |
-
"hex"
|
|
|
|
| 135 |
],
|
| 136 |
"textureKeys": [
|
| 137 |
{
|
|
@@ -229,6 +270,31 @@
|
|
| 229 |
"doc": "the second colour (light end of noise, background cell of patterns)",
|
| 230 |
"words": 1
|
| 231 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
{
|
| 233 |
"key": "invert",
|
| 234 |
"doc": "swap the two colours",
|
|
|
|
| 52 |
"name": "wood",
|
| 53 |
"doc": "long wood grain, along x (use angle 90 for vertical)"
|
| 54 |
},
|
| 55 |
+
{
|
| 56 |
+
"name": "oak",
|
| 57 |
+
"doc": "oak: open grain, tan with brown rings"
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"name": "walnut",
|
| 61 |
+
"doc": "walnut: dark chocolate with black-brown rings"
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"name": "cherry",
|
| 65 |
+
"doc": "cherry: warm red-brown with fine rings"
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"name": "maple",
|
| 69 |
+
"doc": "maple: pale, tight, nearly plain grain"
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"name": "pine",
|
| 73 |
+
"doc": "pine: light yellow wood with wide rings and knots"
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"name": "mahogany",
|
| 77 |
+
"doc": "mahogany: deep red-brown, straight grain"
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"name": "ebony",
|
| 81 |
+
"doc": "ebony: nearly black with faint streaks"
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"name": "birch",
|
| 85 |
+
"doc": "birch: creamy pale wood with fine rings"
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"name": "oak-planks",
|
| 89 |
+
"doc": "oak laid as planks, with seams"
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"name": "walnut-planks",
|
| 93 |
+
"doc": "walnut laid as planks, with seams"
|
| 94 |
+
},
|
| 95 |
{
|
| 96 |
"name": "bark",
|
| 97 |
"doc": "rough vertical bark"
|
|
|
|
| 171 |
"weave",
|
| 172 |
"carbon",
|
| 173 |
"waves",
|
| 174 |
+
"hex",
|
| 175 |
+
"rings"
|
| 176 |
],
|
| 177 |
"textureKeys": [
|
| 178 |
{
|
|
|
|
| 270 |
"doc": "the second colour (light end of noise, background cell of patterns)",
|
| 271 |
"words": 1
|
| 272 |
},
|
| 273 |
+
{
|
| 274 |
+
"key": "figure",
|
| 275 |
+
"doc": "rings generator: how strongly the growth rings arch into a cathedral figure, 0 is straight grain",
|
| 276 |
+
"words": 1
|
| 277 |
+
},
|
| 278 |
+
{
|
| 279 |
+
"key": "warp",
|
| 280 |
+
"doc": "rings generator: how much each ring wobbles along its length",
|
| 281 |
+
"words": 1
|
| 282 |
+
},
|
| 283 |
+
{
|
| 284 |
+
"key": "pores",
|
| 285 |
+
"doc": "rings generator: fine fibre and pore streaks between the rings",
|
| 286 |
+
"words": 1
|
| 287 |
+
},
|
| 288 |
+
{
|
| 289 |
+
"key": "knots",
|
| 290 |
+
"doc": "rings generator: how many knots in one tile (the pattern repeats, so keep it small)",
|
| 291 |
+
"words": 1
|
| 292 |
+
},
|
| 293 |
+
{
|
| 294 |
+
"key": "planks",
|
| 295 |
+
"doc": "rings generator: lay it out as planks this many px tall, with a seam and a slightly different board each",
|
| 296 |
+
"words": 1
|
| 297 |
+
},
|
| 298 |
{
|
| 299 |
"key": "invert",
|
| 300 |
"doc": "swap the two colours",
|