@figurestead/web 0.9.0-alpha.1
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/LICENSE +21 -0
- package/README.md +9 -0
- package/THIRD_PARTY_NOTICES.md +39 -0
- package/TRADEMARKS.md +13 -0
- package/package.json +31 -0
- package/src/accessibility.js +33 -0
- package/src/appearance.js +25 -0
- package/src/application-profiles.js +76 -0
- package/src/atmosphere.js +71 -0
- package/src/canvas-scene.js +323 -0
- package/src/clock.js +27 -0
- package/src/color-space.js +114 -0
- package/src/composition.js +201 -0
- package/src/core-renderers.js +33 -0
- package/src/create-matrix-plot.js +97 -0
- package/src/evidence-coverage.js +80 -0
- package/src/export-bundle.js +83 -0
- package/src/extensions/temporal/coverage.js +96 -0
- package/src/extensions/temporal/index.js +43 -0
- package/src/extensions/temporal/observations.js +87 -0
- package/src/extensions/temporal/shared.js +172 -0
- package/src/figure-layout.js +70 -0
- package/src/figure.js +42 -0
- package/src/index.js +30 -0
- package/src/layout.js +41 -0
- package/src/marks.js +218 -0
- package/src/motion-plan.js +66 -0
- package/src/motion-recipes.js +39 -0
- package/src/paper-profile.js +78 -0
- package/src/physical-export.js +34 -0
- package/src/presentation.js +127 -0
- package/src/primitives.js +24 -0
- package/src/random.js +26 -0
- package/src/registry.js +29 -0
- package/src/render-layers.js +35 -0
- package/src/renderer-test-kit.js +29 -0
- package/src/renderers/line.js +133 -0
- package/src/renderers/scatter.js +24 -0
- package/src/renderers/shared.js +21 -0
- package/src/renderers/strip-summary.js +40 -0
- package/src/resolved-scene.js +249 -0
- package/src/scales.js +68 -0
- package/src/schema.js +356 -0
- package/src/series-style.js +59 -0
- package/src/svg-export.js +232 -0
- package/src/terminal-scene.js +215 -0
- package/src/theme-catalog.js +34 -0
- package/src/theme-pack.js +268 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Figurestead contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @figurestead/web experimental public alpha
|
|
2
|
+
|
|
3
|
+
The package root provides Figurestead's accepted core rendering and custom
|
|
4
|
+
renderer-registry APIs. `EXTENSION_RENDERERS` and `FIGURESTEAD_REGISTRY` are
|
|
5
|
+
not root exports. The complete temporal extension is available from
|
|
6
|
+
`@figurestead/web/extensions/temporal`.
|
|
7
|
+
|
|
8
|
+
Version 0.9.0-alpha.1. Source:
|
|
9
|
+
<https://github.com/CharlesMish/figurestead/tree/main/web>.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Third-party factual notice inventory
|
|
2
|
+
|
|
3
|
+
Status: `EXPERIMENTAL PUBLIC-ALPHA FACTUAL NOTICE`
|
|
4
|
+
|
|
5
|
+
This inventory records the dependency evidence used for the Figurestead
|
|
6
|
+
experimental alpha; it makes no legal conclusion. Runtime dependencies are not
|
|
7
|
+
vendored into the wheel, sdist, or npm tarball. The canonical Figurestead MIT
|
|
8
|
+
text is supplied separately as `LICENSE`.
|
|
9
|
+
|
|
10
|
+
## Python dependency observations
|
|
11
|
+
|
|
12
|
+
An external Python 3.14.6 evidence environment resolved the current declared
|
|
13
|
+
constraints on 2026-08-05 to Matplotlib 3.11.1, NumPy 2.5.1, Pillow 12.3.0,
|
|
14
|
+
PyYAML 6.0.3, and setuptools 82.0.1, plus contourpy 1.3.3, cycler 0.12.1,
|
|
15
|
+
fonttools 4.63.0, kiwisolver 1.5.0, packaging 26.3, pyparsing 3.3.2,
|
|
16
|
+
python-dateutil 2.9.0.post0, and six 1.17.0. These versions are an evidence
|
|
17
|
+
snapshot, not an approved runtime lock.
|
|
18
|
+
|
|
19
|
+
The static-site PNGs are first-party deterministic synthetic Figurestead render
|
|
20
|
+
outputs produced through Matplotlib. No font file is embedded as a separate
|
|
21
|
+
site member. Matplotlib's evidence distribution includes its own license plus
|
|
22
|
+
DejaVu and STIX font notices; rendered glyph provenance remains a review
|
|
23
|
+
consideration. Site CSS names
|
|
24
|
+
only system font families and carries no downloaded font bytes or remote URL.
|
|
25
|
+
|
|
26
|
+
## Browser tooling observations
|
|
27
|
+
|
|
28
|
+
The alpha verification environment used Playwright 1.61.1 and
|
|
29
|
+
playwright-core 1.61.1 as
|
|
30
|
+
Apache-2.0 development/test tooling and optional fsevents 2.3.2 as MIT. These
|
|
31
|
+
packages and downloaded browsers are not intended npm-tarball members or
|
|
32
|
+
top-level archive members. They remain test-tool provenance only.
|
|
33
|
+
|
|
34
|
+
## Placement and maintenance
|
|
35
|
+
|
|
36
|
+
Each Figurestead wheel, sdist, npm package, static-site bundle, and composition
|
|
37
|
+
archive carries this file and `TRADEMARKS.md` beside its canonical `LICENSE`.
|
|
38
|
+
Any newly resolved dependency version, copied source, font, image, or other
|
|
39
|
+
byte requires a corresponding inventory update and review.
|
package/TRADEMARKS.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Trademarks and names
|
|
2
|
+
|
|
3
|
+
Figurestead is an independent project.
|
|
4
|
+
|
|
5
|
+
Names of third-party products, services, organizations, and trademarks remain
|
|
6
|
+
the property of their respective owners. Their appearance in factual dependency
|
|
7
|
+
or compatibility notices does not imply affiliation, sponsorship, or
|
|
8
|
+
endorsement.
|
|
9
|
+
|
|
10
|
+
The experimental public-alpha distribution is intentionally limited to six
|
|
11
|
+
curated Figurestead themes.
|
|
12
|
+
Legacy or franchise-derived internal themes are not part of the public
|
|
13
|
+
distribution.
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@figurestead/web",
|
|
3
|
+
"version": "0.9.0-alpha.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"description": "A framework-free scientific figure system for deterministic, inspectable Python and browser output.",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/CharlesMish/figurestead.git",
|
|
11
|
+
"directory": "web"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/CharlesMish/figurestead#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/CharlesMish/figurestead/issues"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=24 <25"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./src/index.js",
|
|
22
|
+
"./extensions/temporal": "./src/extensions/temporal/index.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"README.md",
|
|
28
|
+
"THIRD_PARTY_NOTICES.md",
|
|
29
|
+
"TRADEMARKS.md"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const el = (name, text) => { const node = document.createElement(name); if (text != null) node.textContent = text; return node; };
|
|
2
|
+
|
|
3
|
+
function appendTable(root, panel, description) {
|
|
4
|
+
if (!description?.headers?.length || !Array.isArray(description.rows)) return;
|
|
5
|
+
const details = el("details"), summary = el("summary", `View ${panel.spec.title || panel.renderer} data`), table = el("table");
|
|
6
|
+
table.append(el("caption", `${panel.spec.title || panel.renderer} data`));
|
|
7
|
+
const head = el("thead"), body = el("tbody"), headingRow = el("tr");
|
|
8
|
+
description.headers.forEach((value) => headingRow.append(el("th", value))); head.append(headingRow);
|
|
9
|
+
description.rows.forEach((row) => { const tr = el("tr"); row.forEach((value, index) => { const cell = el(index === 0 ? "th" : "td", String(value)); if (index === 0) cell.scope = "row"; tr.append(cell); }); body.append(tr); });
|
|
10
|
+
table.append(head, body); details.append(summary, table); root.append(details);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createAccessibilityCompanion(canvas, contract, registry, { visible = false, table = true } = {}) {
|
|
14
|
+
const id = `figurestead-${Math.random().toString(36).slice(2)}`, root = el("section");
|
|
15
|
+
root.className = visible ? "figurestead-accessibility" : "figurestead-accessibility figurestead-sr-only";
|
|
16
|
+
if (!visible) Object.assign(root.style, { position: "absolute", width: "1px", height: "1px", padding: "0", margin: "-1px", overflow: "hidden", clip: "rect(0, 0, 0, 0)", whiteSpace: "nowrap", border: "0" });
|
|
17
|
+
const title = el("h2", contract.spec.title); title.id = `${id}-title`; root.append(title);
|
|
18
|
+
if (contract.spec.subtitle) root.append(el("p", contract.spec.subtitle));
|
|
19
|
+
const description = el("p", contract.spec.description || `${contract.panels.length}-panel scientific figure.`); description.id = `${id}-description`; root.append(description);
|
|
20
|
+
contract.panels.forEach((panel, index) => {
|
|
21
|
+
const section = el("section"), heading = el("h3", panel.spec.title || `${panel.renderer.replaceAll("_", " ")} panel`), definition = registry.get(panel.renderer), described = definition.describe({ ...contract, renderer: panel.renderer, spec: { ...contract.spec, ...panel.spec }, data: panel.data });
|
|
22
|
+
heading.id = `${id}-panel-${index + 1}`; section.setAttribute("aria-labelledby", heading.id); section.append(heading);
|
|
23
|
+
if (panel.spec.description || described.summary) section.append(el("p", panel.spec.description || described.summary));
|
|
24
|
+
panel.annotations.filter((item) => item?.type === "focus" && typeof item.label === "string" && item.label.trim()).forEach((item) => {
|
|
25
|
+
section.append(el("p", `Focus annotation: ${item.label} at x ${item.x}, y ${item.y}.`));
|
|
26
|
+
});
|
|
27
|
+
const dl = el("dl");
|
|
28
|
+
[["Horizontal axis", panel.spec.xLabel || panel.xScale.label || "Unlabelled"], ["Vertical axis", panel.spec.yLabel || panel.yScale.label || "Unlabelled"], ["Renderer", panel.renderer.replaceAll("_", " ")]].forEach(([term, value]) => dl.append(el("dt", term), el("dd", value)));
|
|
29
|
+
section.append(dl); if (table) appendTable(section, panel, described); root.append(section);
|
|
30
|
+
});
|
|
31
|
+
canvas.setAttribute("role", "img"); canvas.setAttribute("aria-labelledby", `${title.id} ${description.id}`); canvas.after(root);
|
|
32
|
+
return { root, destroy() { root.remove(); canvas.removeAttribute("role"); canvas.removeAttribute("aria-labelledby"); } };
|
|
33
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { applyApplicationProfile, resolveApplicationProfile } from "./application-profiles.js";
|
|
2
|
+
import { cloneValue } from "./schema.js";
|
|
3
|
+
import { applyTheme, resolvePalette, resolveTheme, themeForProfile } from "./theme-pack.js";
|
|
4
|
+
|
|
5
|
+
export const APPEARANCE_VERSION = "figurestead.appearance/1";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve profile and palette/theme choices as one deliberate application step.
|
|
9
|
+
* Scientific data, encodings, domains, notes, and ordering are never rewritten.
|
|
10
|
+
*/
|
|
11
|
+
export function composeAppearance(input, options = {}) {
|
|
12
|
+
const profile = resolveApplicationProfile(options.profile ?? input.view?.profile ?? "atlas");
|
|
13
|
+
let result = cloneValue(input);
|
|
14
|
+
if (options.palettePack) {
|
|
15
|
+
if (!options.paletteKey) throw new TypeError("composeAppearance requires paletteKey with palettePack");
|
|
16
|
+
result = applyTheme(result, resolvePalette(options.palettePack, options.paletteKey, { mode: options.mode ?? profile.key }).theme);
|
|
17
|
+
} else if (options.themePack) {
|
|
18
|
+
if (!options.themeKey) throw new TypeError("composeAppearance requires themeKey with themePack");
|
|
19
|
+
result = applyTheme(result, resolveTheme(options.themePack, options.themeKey));
|
|
20
|
+
} else if (options.theme) {
|
|
21
|
+
result = applyTheme(result, options.theme);
|
|
22
|
+
}
|
|
23
|
+
result.theme = themeForProfile(result.theme, profile);
|
|
24
|
+
return applyApplicationProfile(result, profile, options);
|
|
25
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { cloneValue, FiguresteadConfigError } from "./schema.js";
|
|
2
|
+
|
|
3
|
+
export const APPLICATION_PROFILE_VERSION = "figurestead.application-profile/1";
|
|
4
|
+
|
|
5
|
+
const BUILT_INS = {
|
|
6
|
+
paper: {
|
|
7
|
+
key: "paper", name: "Paper", surface: "neutral", density: "compact",
|
|
8
|
+
typography: "publication", legend: "outside", ambient: "none", motion: "none",
|
|
9
|
+
lineWidth: 1.55, markerScale: 0.92, panelSurface: false, frame: false,
|
|
10
|
+
},
|
|
11
|
+
atlas: {
|
|
12
|
+
key: "atlas", name: "Atlas", surface: "themed", density: "balanced",
|
|
13
|
+
typography: "scientific", legend: "auto", ambient: "none", motion: "semantic",
|
|
14
|
+
lineWidth: 2.15, markerScale: 1.12, panelSurface: true, frame: true,
|
|
15
|
+
},
|
|
16
|
+
talk: {
|
|
17
|
+
key: "talk", name: "Talk", surface: "themed", density: "open",
|
|
18
|
+
typography: "display", legend: "auto", ambient: "none", motion: "semantic",
|
|
19
|
+
lineWidth: 2.75, markerScale: 1.34, panelSurface: true, frame: true,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const APPLICATION_PROFILES = Object.freeze(Object.fromEntries(
|
|
24
|
+
Object.entries(BUILT_INS).map(([key, value]) => [key, Object.freeze(value)]),
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
export function resolveApplicationProfile(value = "atlas") {
|
|
28
|
+
const source = typeof value === "string" ? APPLICATION_PROFILES[value] : value;
|
|
29
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
30
|
+
throw new FiguresteadConfigError(`unknown application profile; choose ${Object.keys(APPLICATION_PROFILES).join(", ")}`, "config.view.profile");
|
|
31
|
+
}
|
|
32
|
+
const key = source.key;
|
|
33
|
+
if (!APPLICATION_PROFILES[key] && typeof value === "string") {
|
|
34
|
+
throw new FiguresteadConfigError(`unknown application profile; choose ${Object.keys(APPLICATION_PROFILES).join(", ")}`, "config.view.profile");
|
|
35
|
+
}
|
|
36
|
+
for (const token of ["key", "name", "surface", "density", "typography", "legend", "ambient", "motion"]) {
|
|
37
|
+
if (typeof source[token] !== "string" || !source[token]) throw new FiguresteadConfigError("must be a non-empty string", `config.view.profile.${token}`);
|
|
38
|
+
}
|
|
39
|
+
for (const token of ["lineWidth", "markerScale"]) {
|
|
40
|
+
if (!Number.isFinite(source[token]) || source[token] <= 0) throw new FiguresteadConfigError("must be a positive number", `config.view.profile.${token}`);
|
|
41
|
+
}
|
|
42
|
+
for (const token of ["panelSurface", "frame"]) {
|
|
43
|
+
if (typeof source[token] !== "boolean") throw new FiguresteadConfigError("must be boolean", `config.view.profile.${token}`);
|
|
44
|
+
}
|
|
45
|
+
return Object.freeze(cloneValue(source));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function applyApplicationProfile(input, profile = "atlas", options = {}) {
|
|
49
|
+
const result = cloneValue(input), resolved = resolveApplicationProfile(profile);
|
|
50
|
+
const previous = resolveApplicationProfile(result.view?.profile ?? "atlas");
|
|
51
|
+
const previousPresentation = {
|
|
52
|
+
panelSurface: previous.panelSurface, frame: previous.frame,
|
|
53
|
+
legend: previous.legend === "outside" ? "outside-right" : previous.legend === "auto" ? "auto" : "bottom-right",
|
|
54
|
+
lineWidth: previous.lineWidth, markerScale: previous.markerScale,
|
|
55
|
+
};
|
|
56
|
+
const motion = options.motion ?? resolved.motion;
|
|
57
|
+
const ambient = options.ambient ?? resolved.ambient;
|
|
58
|
+
result.view = {
|
|
59
|
+
profile: resolved.key,
|
|
60
|
+
motion,
|
|
61
|
+
ambient,
|
|
62
|
+
strategy: options.strategy ?? (motion === "semantic" ? "auto" : "none"),
|
|
63
|
+
};
|
|
64
|
+
result.panels = result.panels.map((panel) => {
|
|
65
|
+
const overrides = Object.fromEntries(Object.entries(panel.presentation ?? {}).filter(([key, value]) => previousPresentation[key] !== value));
|
|
66
|
+
return { ...panel, presentation: {
|
|
67
|
+
panelSurface: resolved.panelSurface,
|
|
68
|
+
frame: resolved.frame,
|
|
69
|
+
legend: options.legend ?? (resolved.legend === "outside" ? "outside-right" : resolved.legend === "auto" ? "auto" : "bottom-right"),
|
|
70
|
+
lineWidth: resolved.lineWidth,
|
|
71
|
+
markerScale: resolved.markerScale,
|
|
72
|
+
...overrides,
|
|
73
|
+
} };
|
|
74
|
+
});
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { deriveSeed, mulberry32 } from "./random.js";
|
|
2
|
+
import { smooth } from "./marks.js";
|
|
3
|
+
import { windowProgress } from "./schema.js";
|
|
4
|
+
|
|
5
|
+
const GLYPHS = [..."0123456789ABCDEFXYZ<>[]{}+=αβ∑∫≈"];
|
|
6
|
+
|
|
7
|
+
export function prepareAtmosphere(motion) {
|
|
8
|
+
const random = mulberry32(deriveSeed(motion.seed, "atmosphere"));
|
|
9
|
+
return Array.from({ length: motion.rainStreams }, () => ({
|
|
10
|
+
x: 0.02 + random() * 0.96,
|
|
11
|
+
phase: random() * 1.45,
|
|
12
|
+
speed: 0.48 + random() * 0.47,
|
|
13
|
+
spacing: 0.045 + random() * 0.033,
|
|
14
|
+
base: 0.065 + random() * 0.065,
|
|
15
|
+
glyphs: Array.from({ length: motion.rainGlyphs }, () => GLYPHS[Math.floor(random() * GLYPHS.length)]),
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function atmosphereState(progress, timeline, reducedMotion = false) {
|
|
20
|
+
if (reducedMotion || progress >= 1) return { rainEnvelope: 0, lightEnvelope: 0 };
|
|
21
|
+
const rise = smooth(windowProgress(progress, timeline.rainIn));
|
|
22
|
+
const fall = 1 - smooth(windowProgress(progress, timeline.rainOut));
|
|
23
|
+
const rainEnvelope = rise * fall;
|
|
24
|
+
const mark = Math.sin(Math.PI * windowProgress(progress, timeline.marksEnter)) ** 2;
|
|
25
|
+
return { rainEnvelope, lightEnvelope: 0.72 * rainEnvelope + 0.28 * mark };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function drawAtmosphere(context, { config, layout, streams, progress, reducedMotion }) {
|
|
29
|
+
const state = atmosphereState(progress, config.timeline, reducedMotion);
|
|
30
|
+
if (state.rainEnvelope < 0.001 && state.lightEnvelope < 0.001) return;
|
|
31
|
+
const { plot } = layout;
|
|
32
|
+
context.save();
|
|
33
|
+
context.beginPath();
|
|
34
|
+
context.rect(plot.left, plot.top, plot.right - plot.left, plot.bottom - plot.top);
|
|
35
|
+
context.clip();
|
|
36
|
+
|
|
37
|
+
if (state.lightEnvelope > 0.001) {
|
|
38
|
+
streams.forEach((stream) => {
|
|
39
|
+
const x = plot.left + stream.x * (plot.right - plot.left);
|
|
40
|
+
const radius = Math.max(24, 55 * layout.scale);
|
|
41
|
+
const gradient = context.createLinearGradient(x - radius, 0, x + radius, 0);
|
|
42
|
+
gradient.addColorStop(0, "transparent");
|
|
43
|
+
gradient.addColorStop(0.5, config.theme.primary);
|
|
44
|
+
gradient.addColorStop(1, "transparent");
|
|
45
|
+
context.globalAlpha = config.motion.lightingPeak * state.lightEnvelope * 0.22;
|
|
46
|
+
context.fillStyle = gradient;
|
|
47
|
+
context.fillRect(x - radius, plot.top, radius * 2, plot.bottom - plot.top);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
context.font = `${Math.max(8, 10 * layout.scale)}px ui-monospace, SFMono-Regular, Menlo, monospace`;
|
|
52
|
+
context.textAlign = "center";
|
|
53
|
+
context.textBaseline = "middle";
|
|
54
|
+
streams.forEach((stream) => {
|
|
55
|
+
const head = 1.18 - ((stream.phase + progress * stream.speed * 2.25) % 1.42);
|
|
56
|
+
stream.glyphs.forEach((glyph, index) => {
|
|
57
|
+
const normalizedY = head + index * stream.spacing;
|
|
58
|
+
if (normalizedY < 0 || normalizedY > 1) return;
|
|
59
|
+
const fade = Math.max(0.04, 1 - index / stream.glyphs.length);
|
|
60
|
+
context.globalAlpha = stream.base * fade * state.rainEnvelope;
|
|
61
|
+
context.fillStyle = index === 0 ? config.theme.summaryCore : config.theme.primary;
|
|
62
|
+
context.fillText(
|
|
63
|
+
glyph,
|
|
64
|
+
plot.left + stream.x * (plot.right - plot.left),
|
|
65
|
+
plot.bottom - normalizedY * (plot.bottom - plot.top),
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
context.restore();
|
|
70
|
+
context.globalAlpha = 1;
|
|
71
|
+
}
|