shaderfont 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +16 -0
- package/README.md +16 -0
- package/package.json +4 -2
- package/src/index.d.ts +2 -2
- package/src/index.js +5 -6
- package/src/shader-font-element.js +79 -29
package/README.ko.md
CHANGED
|
@@ -33,6 +33,19 @@ if (!result.ok) throw new Error(result.error);
|
|
|
33
33
|
|
|
34
34
|
루트 import는 SSR에서도 가능합니다. `registerShaderFont()`는 브라우저에서만 호출하고, 즉시 등록하는 `shaderfont/browser`도 브라우저에서만 사용합니다.
|
|
35
35
|
|
|
36
|
+
### CSP와 색상
|
|
37
|
+
|
|
38
|
+
인라인 스타일을 제한하는 사이트에서는 `<shader-font>`를 등록할 때 서버가 발급한 nonce를 전달합니다.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
const result = await registerShaderFont({ styleNonce: trustedNonce });
|
|
42
|
+
if (!result.ok) throw new Error(result.error);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
패키지는 스타일시트가 실제로 적용됐는지 확인합니다. 차단되면 `STYLES_BLOCKED`를 반환하며, 올바른 nonce로 다시 호출할 수 있습니다. DOM에서 임의의 nonce를 읽거나 CSP를 `unsafe-inline`으로 완화할 필요는 없습니다. 즉시 등록하는 `shaderfont/browser`에는 nonce 인자가 없으므로 CSP가 엄격한 페이지에서는 루트 등록 API를 사용합니다. 이미지도 제한한다면 절차적 재질과 디졸브·등장 마스크를 위해 `img-src`에 `data:`를 허용해야 합니다.
|
|
46
|
+
|
|
47
|
+
색상 우선순위는 `primary`·`secondary`·`accent` 속성, 요소나 상위에서 지정한 `--sf-primary`·`--sf-secondary`·`--sf-accent` CSS 변수, 기본 밝은 팔레트 순서입니다. 페이지 테마가 바뀌어도 저장된 색상은 자동으로 덮지 않습니다. 어두운 배경에서는 `SHADER_FONT_PALETTES.find(p => p.name === "dark")`의 세 색을 요소 속성에 지정하고, 밝은 배경으로 돌아올 때 다시 밝은 팔레트를 지정하면 됩니다. 전역 흰색 변수가 글자에 직접 지정한 색을 덮지도 않습니다.
|
|
48
|
+
|
|
36
49
|
## 데모
|
|
37
50
|
|
|
38
51
|
저장소를 복제한 뒤 실행합니다.
|
|
@@ -40,8 +53,11 @@ if (!result.ok) throw new Error(result.error);
|
|
|
40
53
|
```sh
|
|
41
54
|
npm run demo
|
|
42
55
|
npm run check
|
|
56
|
+
npm run check:browser
|
|
43
57
|
```
|
|
44
58
|
|
|
59
|
+
브라우저 검사는 로컬 Chrome에 엄격한 CSP를 적용해 nonce 실패·재시도, 밝고 어두운 색상, 홀로그램 애니메이션, 절차적 재질, 선택 디졸브, 타오르는 등장 효과와 움직임 줄이기를 확인합니다.
|
|
60
|
+
|
|
45
61
|
[영어 데모](http://127.0.0.1:8766/demo/index.html)와 [한글 데모](http://127.0.0.1:8766/demo/index.ko.html)는 같은 설정과 편집 가능한 텍스트를 제공합니다.
|
|
46
62
|
|
|
47
63
|
[MIT 라이선스](./LICENSE).
|
package/README.md
CHANGED
|
@@ -33,6 +33,19 @@ The browser lays out the text. CSS `background-clip: text` clips animated patter
|
|
|
33
33
|
|
|
34
34
|
The root import is safe for SSR. Call `registerShaderFont()` only in the browser; `shaderfont/browser` registers immediately and is browser-only.
|
|
35
35
|
|
|
36
|
+
### CSP and colors
|
|
37
|
+
|
|
38
|
+
On a site that disallows inline styles, pass the nonce supplied by your server **when registering** `<shader-font>`:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
const result = await registerShaderFont({ styleNonce: trustedNonce });
|
|
42
|
+
if (!result.ok) throw new Error(result.error);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The package checks that its stylesheet actually loaded. A blocked style returns `STYLES_BLOCKED`; a later call with the right nonce can retry. Do not read an arbitrary nonce from the DOM or relax CSP with `unsafe-inline`. The immediate `shaderfont/browser` entry has no nonce parameter, so use the root registration API on strict-CSP pages. If your policy also restricts images, allow `data:` in `img-src` for procedural materials and dissolve/reveal masks.
|
|
46
|
+
|
|
47
|
+
Colors follow this order: `primary`/`secondary`/`accent` attributes, then element or inherited `--sf-primary`/`--sf-secondary`/`--sf-accent` CSS variables, then the light palette. The package does not silently replace stored colors when the page changes theme. For a dark background, opt into `SHADER_FONT_PALETTES.find(p => p.name === "dark")` and set its three colors on the element; switch back to the light palette when needed. This also keeps an unrelated global white CSS variable from covering authored colors.
|
|
48
|
+
|
|
36
49
|
## Try the demo
|
|
37
50
|
|
|
38
51
|
Clone this repository and run:
|
|
@@ -40,8 +53,11 @@ Clone this repository and run:
|
|
|
40
53
|
```sh
|
|
41
54
|
npm run demo
|
|
42
55
|
npm run check
|
|
56
|
+
npm run check:browser
|
|
43
57
|
```
|
|
44
58
|
|
|
59
|
+
The browser check uses local Chrome and sends a strict CSP header. It covers nonce rejection/retry, light and dark colors, hologram animation, procedural material, selection dissolve, burn reveal and reduced motion.
|
|
60
|
+
|
|
45
61
|
Open the [English demo](http://127.0.0.1:8766/demo/index.html) or [Korean demo](http://127.0.0.1:8766/demo/index.ko.html). Both have the same controls and editable text.
|
|
46
62
|
|
|
47
63
|
[MIT license](./LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shaderfont",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Animated materials and selection effects on DOM text, with Canvas 2D and WebGL renderers.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"sideEffects": ["./src/shader-font.js"],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"check": "node --check src/core.js && node --check src/index.js && node --check src/options.js && node --check src/presets.js && node --check src/paint-layout.js && node --check src/selection-range.js && node --check src/selection-gesture.js && node --check src/selection-indicator.js && node --check src/webgl-surface.js && node --check src/material-core.js && node --check src/material-renderer.js && node --check src/mask-renderer.js && node --check src/reveal-core.js && node --check src/reveal-renderer.js && node --check src/shader-font-element.js && node --check demo/demo.js && node --check demo/copy.js && node --test",
|
|
20
|
+
"check:browser": "node tools/csp-browser-check.mjs",
|
|
20
21
|
"demo": "node tools/serve.mjs",
|
|
21
22
|
"prepublishOnly": "npm run check"
|
|
22
|
-
}
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": { "playwright": "1.62.1" }
|
|
23
25
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { SHADER_FONT_DEFAULTS, SHADER_FONT_PALETTES, SHADER_PRESETS, parseShaderFontOptions, shaderFontAttributes } from './options.js';
|
|
2
2
|
export type { ShaderFontOptions } from './options.js';
|
|
3
|
-
export function registerShaderFont(): Promise<
|
|
3
|
+
export function registerShaderFont(options?: { styleNonce?: string }): Promise<
|
|
4
4
|
| { ok: true }
|
|
5
|
-
| { ok: false; error: 'BROWSER_REQUIRED' | 'BROWSER_FEATURE_UNAVAILABLE' | 'REGISTRATION_FAILED'; cause?: string }
|
|
5
|
+
| { ok: false; error: 'BROWSER_REQUIRED' | 'BROWSER_FEATURE_UNAVAILABLE' | 'INVALID_STYLE_NONCE' | 'STYLES_CONFLICT' | 'STYLES_BLOCKED' | 'REGISTRATION_FAILED'; cause?: string }
|
|
6
6
|
>;
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { SHADER_FONT_DEFAULTS, SHADER_FONT_PALETTES, SHADER_PRESETS, parseShaderFontOptions, shaderFontAttributes } from "./options.js";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const STYLE_ERRORS = new Set(["INVALID_STYLE_NONCE", "STYLES_CONFLICT", "STYLES_BLOCKED"]);
|
|
4
4
|
|
|
5
5
|
/** No DOM globals are evaluated until a browser explicitly requests effects. */
|
|
6
|
-
export async function registerShaderFont() {
|
|
6
|
+
export async function registerShaderFont({ styleNonce } = {}) {
|
|
7
7
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
8
8
|
return { ok: false, error: "BROWSER_REQUIRED" };
|
|
9
9
|
}
|
|
@@ -12,15 +12,14 @@ export async function registerShaderFont() {
|
|
|
12
12
|
|| typeof IntersectionObserver === "undefined" || typeof matchMedia !== "function") {
|
|
13
13
|
return { ok: false, error: "BROWSER_FEATURE_UNAVAILABLE" };
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
return import("./shader-font-element.js")
|
|
16
16
|
.then(module => {
|
|
17
|
-
module.registerShaderFontElement();
|
|
17
|
+
module.registerShaderFontElement({ styleNonce });
|
|
18
18
|
return { ok: true };
|
|
19
19
|
})
|
|
20
20
|
.catch(cause => ({
|
|
21
21
|
ok: false,
|
|
22
|
-
error: "REGISTRATION_FAILED",
|
|
22
|
+
error: STYLE_ERRORS.has(cause?.code) ? cause.code : "REGISTRATION_FAILED",
|
|
23
23
|
cause: cause instanceof Error ? cause.name : typeof cause
|
|
24
24
|
}));
|
|
25
|
-
return registration;
|
|
26
25
|
}
|
|
@@ -45,11 +45,7 @@ const DEFAULT_COLORS = Object.freeze({
|
|
|
45
45
|
accent: SHADER_FONT_DEFAULTS.accent
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
if (document.getElementById(STYLE_ID)) return;
|
|
50
|
-
const style = document.createElement("style");
|
|
51
|
-
style.id = STYLE_ID;
|
|
52
|
-
style.textContent = `
|
|
48
|
+
const COMPONENT_STYLES = `
|
|
53
49
|
shader-font,
|
|
54
50
|
shaderfont {
|
|
55
51
|
--sf-x: 0px;
|
|
@@ -69,6 +65,9 @@ function ensureStyles() {
|
|
|
69
65
|
--_sf-primary: ${DEFAULT_COLORS.primary};
|
|
70
66
|
--_sf-secondary: ${DEFAULT_COLORS.secondary};
|
|
71
67
|
--_sf-accent: ${DEFAULT_COLORS.accent};
|
|
68
|
+
--_sf-author-primary: initial;
|
|
69
|
+
--_sf-author-secondary: initial;
|
|
70
|
+
--_sf-author-accent: initial;
|
|
72
71
|
display: inline;
|
|
73
72
|
box-decoration-break: slice;
|
|
74
73
|
-webkit-box-decoration-break: slice;
|
|
@@ -80,25 +79,25 @@ function ensureStyles() {
|
|
|
80
79
|
var(--sf-burn-edge-image, none),
|
|
81
80
|
repeating-linear-gradient(
|
|
82
81
|
180deg,
|
|
83
|
-
var(--sf-accent, var(--_sf-accent)) 0 2px,
|
|
84
|
-
var(--sf-secondary, var(--_sf-secondary)) 2px 4px,
|
|
85
|
-
var(--sf-primary, var(--_sf-primary)) 4px 5px,
|
|
86
|
-
var(--sf-accent, var(--_sf-accent)) 5px 7px
|
|
82
|
+
var(--_sf-author-accent, var(--sf-accent, var(--_sf-accent))) 0 2px,
|
|
83
|
+
var(--_sf-author-secondary, var(--sf-secondary, var(--_sf-secondary))) 2px 4px,
|
|
84
|
+
var(--_sf-author-primary, var(--sf-primary, var(--_sf-primary))) 4px 5px,
|
|
85
|
+
var(--_sf-author-accent, var(--sf-accent, var(--_sf-accent))) 5px 7px
|
|
87
86
|
),
|
|
88
87
|
linear-gradient(
|
|
89
88
|
135deg,
|
|
90
|
-
var(--sf-primary, var(--_sf-primary)) 0%,
|
|
91
|
-
var(--sf-accent, var(--_sf-accent)) 38%,
|
|
92
|
-
var(--sf-secondary, var(--_sf-secondary)) 72%,
|
|
93
|
-
var(--sf-primary, var(--_sf-primary)) 100%
|
|
89
|
+
var(--_sf-author-primary, var(--sf-primary, var(--_sf-primary))) 0%,
|
|
90
|
+
var(--_sf-author-accent, var(--sf-accent, var(--_sf-accent))) 38%,
|
|
91
|
+
var(--_sf-author-secondary, var(--sf-secondary, var(--_sf-secondary))) 72%,
|
|
92
|
+
var(--_sf-author-primary, var(--sf-primary, var(--_sf-primary))) 100%
|
|
94
93
|
);
|
|
95
94
|
background-size: 100% 100%, 100% 7px, 100% 100%;
|
|
96
95
|
background-clip: text;
|
|
97
96
|
-webkit-background-clip: text;
|
|
98
97
|
background-blend-mode: normal;
|
|
99
98
|
text-shadow:
|
|
100
|
-
-.018em 0 color-mix(in srgb, var(--sf-primary, var(--_sf-primary)) 70%, transparent),
|
|
101
|
-
.018em 0 color-mix(in srgb, var(--sf-secondary, var(--_sf-secondary)) 62%, transparent);
|
|
99
|
+
-.018em 0 color-mix(in srgb, var(--_sf-author-primary, var(--sf-primary, var(--_sf-primary))) 70%, transparent),
|
|
100
|
+
.018em 0 color-mix(in srgb, var(--_sf-author-secondary, var(--sf-secondary, var(--_sf-secondary))) 62%, transparent);
|
|
102
101
|
filter: none;
|
|
103
102
|
transform:
|
|
104
103
|
translate3d(var(--sf-x), var(--sf-y), 0)
|
|
@@ -107,7 +106,7 @@ function ensureStyles() {
|
|
|
107
106
|
scale(var(--sf-effect-scale-x), var(--sf-effect-scale-y));
|
|
108
107
|
transform-origin: center;
|
|
109
108
|
opacity: var(--sf-fade);
|
|
110
|
-
caret-color: var(--sf-primary, var(--_sf-primary));
|
|
109
|
+
caret-color: var(--_sf-author-primary, var(--sf-primary, var(--_sf-primary)));
|
|
111
110
|
-webkit-mask-repeat: no-repeat;
|
|
112
111
|
mask-repeat: no-repeat;
|
|
113
112
|
-webkit-mask-position: 0 0;
|
|
@@ -133,7 +132,7 @@ function ensureStyles() {
|
|
|
133
132
|
}
|
|
134
133
|
shader-font[interaction="none"] { pointer-events: none; }
|
|
135
134
|
shader-font[contenteditable]:focus {
|
|
136
|
-
outline: 1px solid color-mix(in srgb, var(--sf-primary, var(--_sf-primary)) 38%, transparent);
|
|
135
|
+
outline: 1px solid color-mix(in srgb, var(--_sf-author-primary, var(--sf-primary, var(--_sf-primary))) 38%, transparent);
|
|
137
136
|
outline-offset: .16em;
|
|
138
137
|
}
|
|
139
138
|
shader-font::selection, shaderfont::selection {
|
|
@@ -223,12 +222,12 @@ function ensureStyles() {
|
|
|
223
222
|
|
|
224
223
|
@keyframes shader-font-scan { to { background-position: 0 0, 0 7px, 0 0; } }
|
|
225
224
|
@keyframes shader-font-outline-jitter {
|
|
226
|
-
0%, 100% { text-shadow: -.018em 0 var(--sf-primary, var(--_sf-primary)), .018em 0 var(--sf-secondary, var(--_sf-secondary)); }
|
|
227
|
-
14% { text-shadow: -.052em -.004em var(--sf-primary, var(--_sf-primary)), .026em .003em var(--sf-secondary, var(--_sf-secondary)); }
|
|
228
|
-
31% { text-shadow: .024em 0 var(--sf-primary, var(--_sf-primary)), -.036em 0 var(--sf-secondary, var(--_sf-secondary)); }
|
|
229
|
-
48% { text-shadow: -.028em .005em var(--sf-primary, var(--_sf-primary)), .058em -.003em var(--sf-secondary, var(--_sf-secondary)); }
|
|
230
|
-
67% { text-shadow: -.062em 0 var(--sf-primary, var(--_sf-primary)), .032em .004em var(--sf-secondary, var(--_sf-secondary)); }
|
|
231
|
-
83% { text-shadow: .036em -.003em var(--sf-primary, var(--_sf-primary)), -.022em .003em var(--sf-secondary, var(--_sf-secondary)); }
|
|
225
|
+
0%, 100% { text-shadow: -.018em 0 var(--_sf-author-primary, var(--sf-primary, var(--_sf-primary))), .018em 0 var(--_sf-author-secondary, var(--sf-secondary, var(--_sf-secondary))); }
|
|
226
|
+
14% { text-shadow: -.052em -.004em var(--_sf-author-primary, var(--sf-primary, var(--_sf-primary))), .026em .003em var(--_sf-author-secondary, var(--sf-secondary, var(--_sf-secondary))); }
|
|
227
|
+
31% { text-shadow: .024em 0 var(--_sf-author-primary, var(--sf-primary, var(--_sf-primary))), -.036em 0 var(--_sf-author-secondary, var(--sf-secondary, var(--_sf-secondary))); }
|
|
228
|
+
48% { text-shadow: -.028em .005em var(--_sf-author-primary, var(--sf-primary, var(--_sf-primary))), .058em -.003em var(--_sf-author-secondary, var(--sf-secondary, var(--_sf-secondary))); }
|
|
229
|
+
67% { text-shadow: -.062em 0 var(--_sf-author-primary, var(--sf-primary, var(--_sf-primary))), .032em .004em var(--_sf-author-secondary, var(--sf-secondary, var(--_sf-secondary))); }
|
|
230
|
+
83% { text-shadow: .036em -.003em var(--_sf-author-primary, var(--sf-primary, var(--_sf-primary))), -.022em .003em var(--_sf-author-secondary, var(--sf-secondary, var(--_sf-secondary))); }
|
|
232
231
|
}
|
|
233
232
|
@keyframes shader-font-drag-shake {
|
|
234
233
|
0%, 100% { translate: var(--sf-shake-neg-small) var(--sf-shake-small); rotate: var(--sf-shake-neg-angle); }
|
|
@@ -241,7 +240,39 @@ function ensureStyles() {
|
|
|
241
240
|
shader-font, shaderfont { animation: none !important; translate: none; rotate: none; }
|
|
242
241
|
}
|
|
243
242
|
`;
|
|
243
|
+
|
|
244
|
+
let installedStyleNonce;
|
|
245
|
+
|
|
246
|
+
function styleError(code) {
|
|
247
|
+
return Object.assign(new Error(code), { code });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function ensureStyles(styleNonce = installedStyleNonce) {
|
|
251
|
+
if (styleNonce !== undefined && (typeof styleNonce !== "string" || !styleNonce || /\s/.test(styleNonce))) {
|
|
252
|
+
throw styleError("INVALID_STYLE_NONCE");
|
|
253
|
+
}
|
|
254
|
+
const existing = document.getElementById(STYLE_ID);
|
|
255
|
+
if (existing) {
|
|
256
|
+
if (existing.tagName !== "STYLE" || existing.textContent !== COMPONENT_STYLES) {
|
|
257
|
+
throw styleError("STYLES_CONFLICT");
|
|
258
|
+
}
|
|
259
|
+
if (existing.sheet && !existing.sheet.disabled) {
|
|
260
|
+
if (styleNonce && existing.nonce === styleNonce) installedStyleNonce = styleNonce;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (!styleNonce) throw styleError("STYLES_BLOCKED");
|
|
264
|
+
existing.remove();
|
|
265
|
+
}
|
|
266
|
+
const style = document.createElement("style");
|
|
267
|
+
style.id = STYLE_ID;
|
|
268
|
+
if (styleNonce) style.nonce = styleNonce;
|
|
269
|
+
style.textContent = COMPONENT_STYLES;
|
|
244
270
|
document.head.append(style);
|
|
271
|
+
if (!style.sheet || style.sheet.disabled) {
|
|
272
|
+
style.remove();
|
|
273
|
+
throw styleError("STYLES_BLOCKED");
|
|
274
|
+
}
|
|
275
|
+
installedStyleNonce = styleNonce;
|
|
245
276
|
}
|
|
246
277
|
|
|
247
278
|
function colorOrDefault(element, attribute) {
|
|
@@ -367,7 +398,15 @@ export class ShaderFont extends HTMLElement {
|
|
|
367
398
|
}
|
|
368
399
|
|
|
369
400
|
connectedCallback() {
|
|
370
|
-
|
|
401
|
+
try {
|
|
402
|
+
ensureStyles();
|
|
403
|
+
this.dataset.styleStatus = "ready";
|
|
404
|
+
delete this.dataset.styleError;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
this.dataset.styleStatus = "error";
|
|
407
|
+
this.dataset.styleError = error.code ?? "STYLE_INSTALL_FAILED";
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
371
410
|
this.applyAttributes();
|
|
372
411
|
if (this.dataset.shaderFontBound === "true") return;
|
|
373
412
|
this.dataset.shaderFontBound = "true";
|
|
@@ -456,7 +495,9 @@ export class ShaderFont extends HTMLElement {
|
|
|
456
495
|
|
|
457
496
|
applyAttributes() {
|
|
458
497
|
for (const name of Object.keys(DEFAULT_COLORS)) {
|
|
459
|
-
this.
|
|
498
|
+
const value = this.getAttribute(name)?.trim();
|
|
499
|
+
if (value && CSS.supports("color", value)) this.style.setProperty(`--_sf-author-${name}`, value);
|
|
500
|
+
else this.style.removeProperty(`--_sf-author-${name}`);
|
|
460
501
|
}
|
|
461
502
|
this.dataset.shader = this.shaderPreset();
|
|
462
503
|
this.dataset.dragEffect = this.dragEffect();
|
|
@@ -571,6 +612,10 @@ export class ShaderFont extends HTMLElement {
|
|
|
571
612
|
const tick = now => {
|
|
572
613
|
this.materialFrame = 0;
|
|
573
614
|
if (!this.isConnected || !this.inViewport || document.hidden || this.rendererError) return;
|
|
615
|
+
if (this.motionQuery.matches) {
|
|
616
|
+
this.syncMaterialPlayback();
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
574
619
|
const speed = numberAttribute(this, "speed", SHADER_FONT_DEFAULTS.speed, 0, 4);
|
|
575
620
|
if (this.materialLastTime !== null) this.materialElapsed += Math.max(0, now - this.materialLastTime) / 1000 * speed;
|
|
576
621
|
this.materialLastTime = now;
|
|
@@ -598,7 +643,8 @@ export class ShaderFont extends HTMLElement {
|
|
|
598
643
|
if (!this.materialInput) {
|
|
599
644
|
const computed = getComputedStyle(this);
|
|
600
645
|
const colors = ["primary", "secondary", "accent"].map(name => {
|
|
601
|
-
const color = computed.getPropertyValue(`--
|
|
646
|
+
const color = computed.getPropertyValue(`--_sf-author-${name}`).trim()
|
|
647
|
+
|| computed.getPropertyValue(`--sf-${name}`).trim() || colorOrDefault(this, name);
|
|
602
648
|
if (!/^#[0-9a-f]{6}$/i.test(color)) throw rendererError("INVALID_MATERIAL_COLOR", "Procedural materials require six-digit hex colors");
|
|
603
649
|
return [1, 3, 5].map(index => Number.parseInt(color.slice(index, index + 2), 16) / 255);
|
|
604
650
|
});
|
|
@@ -690,7 +736,11 @@ export class ShaderFont extends HTMLElement {
|
|
|
690
736
|
if (this.revealFrame || this.revealTimer) return;
|
|
691
737
|
const tick = now => {
|
|
692
738
|
this.revealFrame = 0;
|
|
693
|
-
if (
|
|
739
|
+
if (this.motionQuery.matches) {
|
|
740
|
+
this.syncRevealPlayback();
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (!this.isConnected || !this.inViewport || document.hidden || this.rendererError) return;
|
|
694
744
|
if (this.revealLastTime !== null) this.revealElapsed += Math.max(0, now - this.revealLastTime);
|
|
695
745
|
this.revealLastTime = now;
|
|
696
746
|
const duration = numberAttribute(this, "reveal-ms", SHADER_FONT_DEFAULTS.revealMs, 200, 10000);
|
|
@@ -1310,8 +1360,8 @@ export function upgradeLegacyShaderfont(root = document) {
|
|
|
1310
1360
|
|
|
1311
1361
|
let aliasObserver;
|
|
1312
1362
|
|
|
1313
|
-
export function registerShaderFontElement({ legacyAliases = false } = {}) {
|
|
1314
|
-
ensureStyles();
|
|
1363
|
+
export function registerShaderFontElement({ legacyAliases = false, styleNonce } = {}) {
|
|
1364
|
+
ensureStyles(styleNonce);
|
|
1315
1365
|
if (!customElements.get("shader-font")) customElements.define("shader-font", ShaderFont);
|
|
1316
1366
|
// Only the legacy script API needs a document-wide alias observer.
|
|
1317
1367
|
if (legacyAliases && !aliasObserver) {
|