@agentprojectcontext/apx 1.15.5 → 1.16.0
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/package.json +40 -5
- package/src/cli/commands/log.js +113 -0
- package/src/cli/commands/overlay.js +253 -0
- package/src/cli/commands/sys.js +88 -16
- package/src/cli/index.js +23 -1
- package/src/cli/terminal-chat/renderer.js +71 -56
- package/src/cli-ts/commands/agent.ts +173 -0
- package/src/cli-ts/commands/chat.ts +119 -0
- package/src/cli-ts/commands/daemon.ts +112 -0
- package/src/cli-ts/commands/exec.ts +109 -0
- package/src/cli-ts/commands/mcp.ts +235 -0
- package/src/cli-ts/commands/session.ts +224 -0
- package/src/cli-ts/commands/status.ts +61 -0
- package/src/cli-ts/http.ts +36 -0
- package/src/cli-ts/index.ts +73 -0
- package/src/cli-ts/ui.ts +107 -0
- package/src/core/logging.js +81 -0
- package/src/daemon/api.js +58 -0
- package/src/daemon/engines/anthropic.js +60 -1
- package/src/daemon/engines/index.js +2 -1
- package/src/daemon/engines/ollama.js +70 -3
- package/src/daemon/index.js +58 -0
- package/src/daemon/overlay-ws.js +40 -0
- package/src/daemon/plugins/index.js +2 -1
- package/src/daemon/plugins/overlay.js +177 -0
- package/src/daemon/plugins/telegram.js +15 -3
- package/src/daemon/super-agent.js +102 -19
- package/src/daemon/transcription.js +262 -59
- package/src/daemon/wakeup.js +14 -19
- package/src/daemon/whisper-server.py +57 -6
- package/src/overlay/index.html +44 -0
- package/src/overlay/main.js +480 -0
- package/src/overlay/package.json +3 -0
- package/src/overlay/preload.js +34 -0
- package/src/overlay/renderer.js +371 -0
- package/src/overlay/style.css +250 -0
- package/src/tui/_shims/cli-error.ts +6 -0
- package/src/tui/_shims/cli-logo.ts +18 -0
- package/src/tui/_shims/cli-ui.ts +1 -0
- package/src/tui/_shims/config-console-state.ts +7 -0
- package/src/tui/_shims/core-any.ts +30 -0
- package/src/tui/_shims/core-binary.ts +13 -0
- package/src/tui/_shims/core-flag.ts +3 -0
- package/src/tui/_shims/core-log.ts +14 -0
- package/src/tui/_shims/lsp-language.ts +1 -0
- package/src/tui/_shims/opencode-any.ts +135 -0
- package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
- package/src/tui/_shims/plugin-tui.ts +13 -0
- package/src/tui/_shims/provider-provider.ts +10 -0
- package/src/tui/_shims/session-retry.ts +1 -0
- package/src/tui/_shims/session-schema.ts +15 -0
- package/src/tui/_shims/session-session.ts +3 -0
- package/src/tui/_shims/snapshot.ts +4 -0
- package/src/tui/_shims/tool-any.ts +18 -0
- package/src/tui/_shims/util-error.ts +7 -0
- package/src/tui/_shims/util-filesystem.ts +79 -0
- package/src/tui/_shims/util-format.ts +7 -0
- package/src/tui/_shims/util-iife.ts +3 -0
- package/src/tui/_shims/util-locale.ts +10 -0
- package/src/tui/_shims/util-process.ts +38 -0
- package/src/tui/app.tsx +783 -0
- package/src/tui/asset/charge.wav +0 -0
- package/src/tui/asset/pulse-a.wav +0 -0
- package/src/tui/asset/pulse-b.wav +0 -0
- package/src/tui/asset/pulse-c.wav +0 -0
- package/src/tui/attach.ts +100 -0
- package/src/tui/component/bg-pulse-render.ts +436 -0
- package/src/tui/component/bg-pulse.tsx +99 -0
- package/src/tui/component/border.tsx +21 -0
- package/src/tui/component/dialog-agent.tsx +31 -0
- package/src/tui/component/dialog-console-org.tsx +103 -0
- package/src/tui/component/dialog-mcp.tsx +85 -0
- package/src/tui/component/dialog-model.tsx +175 -0
- package/src/tui/component/dialog-provider.tsx +456 -0
- package/src/tui/component/dialog-retry-action.tsx +160 -0
- package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
- package/src/tui/component/dialog-session-list.tsx +323 -0
- package/src/tui/component/dialog-session-rename.tsx +31 -0
- package/src/tui/component/dialog-skill.tsx +36 -0
- package/src/tui/component/dialog-stash.tsx +87 -0
- package/src/tui/component/dialog-status.tsx +168 -0
- package/src/tui/component/dialog-tag.tsx +44 -0
- package/src/tui/component/dialog-theme-list.tsx +50 -0
- package/src/tui/component/dialog-variant.tsx +39 -0
- package/src/tui/component/dialog-workspace-create.tsx +302 -0
- package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
- package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/tui/component/error-component.tsx +92 -0
- package/src/tui/component/logo.tsx +896 -0
- package/src/tui/component/plugin-route-missing.tsx +14 -0
- package/src/tui/component/prompt/autocomplete.tsx +869 -0
- package/src/tui/component/prompt/cwd.ts +0 -0
- package/src/tui/component/prompt/frecency.tsx +90 -0
- package/src/tui/component/prompt/history.tsx +108 -0
- package/src/tui/component/prompt/index.tsx +1809 -0
- package/src/tui/component/prompt/part.ts +16 -0
- package/src/tui/component/prompt/stash.tsx +101 -0
- package/src/tui/component/prompt/traits.ts +35 -0
- package/src/tui/component/spinner.tsx +24 -0
- package/src/tui/component/startup-loading.tsx +63 -0
- package/src/tui/component/todo-item.tsx +32 -0
- package/src/tui/component/use-connected.tsx +9 -0
- package/src/tui/component/workspace-label.tsx +19 -0
- package/src/tui/config/cwd.ts +5 -0
- package/src/tui/config/keybind.ts +432 -0
- package/src/tui/config/tui-migrate.ts +154 -0
- package/src/tui/config/tui-schema.ts +34 -0
- package/src/tui/config/tui.ts +46 -0
- package/src/tui/context/aggregate-failures.ts +34 -0
- package/src/tui/context/args.tsx +15 -0
- package/src/tui/context/command-palette.tsx +163 -0
- package/src/tui/context/directory.ts +15 -0
- package/src/tui/context/editor-zed.ts +283 -0
- package/src/tui/context/editor.ts +468 -0
- package/src/tui/context/event-apx.ts +22 -0
- package/src/tui/context/event.ts +6 -0
- package/src/tui/context/exit.tsx +60 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/kv.tsx +81 -0
- package/src/tui/context/local.tsx +608 -0
- package/src/tui/context/path-format.tsx +39 -0
- package/src/tui/context/project-apx.tsx +48 -0
- package/src/tui/context/project.tsx +7 -0
- package/src/tui/context/prompt.tsx +18 -0
- package/src/tui/context/route.tsx +52 -0
- package/src/tui/context/sdk-apx.tsx +185 -0
- package/src/tui/context/sdk.tsx +6 -0
- package/src/tui/context/sync-apx.tsx +178 -0
- package/src/tui/context/sync-v2.tsx +16 -0
- package/src/tui/context/sync.tsx +118 -0
- package/src/tui/context/theme/aura.json +69 -0
- package/src/tui/context/theme/ayu.json +80 -0
- package/src/tui/context/theme/carbonfox.json +248 -0
- package/src/tui/context/theme/catppuccin-frappe.json +230 -0
- package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
- package/src/tui/context/theme/catppuccin.json +112 -0
- package/src/tui/context/theme/cobalt2.json +225 -0
- package/src/tui/context/theme/cursor.json +249 -0
- package/src/tui/context/theme/dracula.json +219 -0
- package/src/tui/context/theme/everforest.json +241 -0
- package/src/tui/context/theme/flexoki.json +237 -0
- package/src/tui/context/theme/github.json +233 -0
- package/src/tui/context/theme/gruvbox.json +242 -0
- package/src/tui/context/theme/kanagawa.json +77 -0
- package/src/tui/context/theme/lucent-orng.json +234 -0
- package/src/tui/context/theme/material.json +235 -0
- package/src/tui/context/theme/matrix.json +77 -0
- package/src/tui/context/theme/mercury.json +252 -0
- package/src/tui/context/theme/monokai.json +221 -0
- package/src/tui/context/theme/nightowl.json +221 -0
- package/src/tui/context/theme/nord.json +223 -0
- package/src/tui/context/theme/one-dark.json +84 -0
- package/src/tui/context/theme/opencode.json +245 -0
- package/src/tui/context/theme/orng.json +249 -0
- package/src/tui/context/theme/osaka-jade.json +93 -0
- package/src/tui/context/theme/palenight.json +222 -0
- package/src/tui/context/theme/rosepine.json +234 -0
- package/src/tui/context/theme/solarized.json +223 -0
- package/src/tui/context/theme/synthwave84.json +226 -0
- package/src/tui/context/theme/tokyonight.json +243 -0
- package/src/tui/context/theme/vercel.json +245 -0
- package/src/tui/context/theme/vesper.json +218 -0
- package/src/tui/context/theme/zenburn.json +223 -0
- package/src/tui/context/theme.tsx +1247 -0
- package/src/tui/context/tui-config.tsx +9 -0
- package/src/tui/event.ts +16 -0
- package/src/tui/feature-plugins/home/footer.tsx +94 -0
- package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
- package/src/tui/feature-plugins/home/tips.tsx +59 -0
- package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
- package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
- package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/tui/feature-plugins/system/plugins.tsx +269 -0
- package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
- package/src/tui/feature-plugins/system/which-key.tsx +608 -0
- package/src/tui/keymap.tsx +166 -0
- package/src/tui/layer.ts +6 -0
- package/src/tui/plugin/api.tsx +381 -0
- package/src/tui/plugin/command-shim.ts +109 -0
- package/src/tui/plugin/internal.ts +33 -0
- package/src/tui/plugin/runtime.ts +1069 -0
- package/src/tui/plugin/slots.tsx +60 -0
- package/src/tui/routes/home.tsx +96 -0
- package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/tui/routes/session/dialog-message.tsx +108 -0
- package/src/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/tui/routes/session/footer.tsx +91 -0
- package/src/tui/routes/session/index.tsx +188 -0
- package/src/tui/routes/session/permission.tsx +722 -0
- package/src/tui/routes/session/question.tsx +490 -0
- package/src/tui/routes/session/sidebar.tsx +102 -0
- package/src/tui/routes/session/subagent-footer.tsx +133 -0
- package/src/tui/run.ts +84 -0
- package/src/tui/thread.ts +261 -0
- package/src/tui/tsconfig.json +40 -0
- package/src/tui/ui/dialog-alert.tsx +66 -0
- package/src/tui/ui/dialog-confirm.tsx +108 -0
- package/src/tui/ui/dialog-export-options.tsx +217 -0
- package/src/tui/ui/dialog-help.tsx +40 -0
- package/src/tui/ui/dialog-prompt.tsx +101 -0
- package/src/tui/ui/dialog-select.tsx +553 -0
- package/src/tui/ui/dialog.tsx +211 -0
- package/src/tui/ui/link.tsx +34 -0
- package/src/tui/ui/spinner.ts +368 -0
- package/src/tui/ui/toast.tsx +111 -0
- package/src/tui/util/clipboard.ts +217 -0
- package/src/tui/util/editor.ts +37 -0
- package/src/tui/util/model.ts +23 -0
- package/src/tui/util/provider-origin.ts +7 -0
- package/src/tui/util/revert-diff.ts +18 -0
- package/src/tui/util/scroll.ts +25 -0
- package/src/tui/util/selection.ts +65 -0
- package/src/tui/util/signal.ts +41 -0
- package/src/tui/util/sound.ts +156 -0
- package/src/tui/util/transcript.ts +112 -0
- package/src/tui/validate-session.ts +29 -0
- package/src/tui/win32.ts +130 -0
- package/src/tui/worker.ts +104 -0
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core"
|
|
2
|
+
import { useRenderer } from "@opentui/solid"
|
|
3
|
+
import { For, createMemo, createSignal, onCleanup, onMount, type JSX } from "solid-js"
|
|
4
|
+
import { useTheme, tint } from "@tui/context/theme"
|
|
5
|
+
import * as Sound from "@tui/util/sound"
|
|
6
|
+
import { go, logo } from "@/cli/logo"
|
|
7
|
+
|
|
8
|
+
export type LogoShape = {
|
|
9
|
+
left: string[]
|
|
10
|
+
right: string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type ShimmerConfig = {
|
|
14
|
+
period: number
|
|
15
|
+
rings: number
|
|
16
|
+
sweepFraction: number
|
|
17
|
+
coreWidth: number
|
|
18
|
+
coreAmp: number
|
|
19
|
+
softWidth: number
|
|
20
|
+
softAmp: number
|
|
21
|
+
tail: number
|
|
22
|
+
tailAmp: number
|
|
23
|
+
haloWidth: number
|
|
24
|
+
haloOffset: number
|
|
25
|
+
haloAmp: number
|
|
26
|
+
breathBase: number
|
|
27
|
+
noise: number
|
|
28
|
+
ambientAmp: number
|
|
29
|
+
ambientCenter: number
|
|
30
|
+
ambientWidth: number
|
|
31
|
+
shadowMix: number
|
|
32
|
+
primaryMix: number
|
|
33
|
+
originX: number
|
|
34
|
+
originY: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const shimmerConfig: ShimmerConfig = {
|
|
38
|
+
period: 4600,
|
|
39
|
+
rings: 2,
|
|
40
|
+
sweepFraction: 1,
|
|
41
|
+
coreWidth: 1.2,
|
|
42
|
+
coreAmp: 1.9,
|
|
43
|
+
softWidth: 10,
|
|
44
|
+
softAmp: 1.6,
|
|
45
|
+
tail: 5,
|
|
46
|
+
tailAmp: 0.64,
|
|
47
|
+
haloWidth: 4.3,
|
|
48
|
+
haloOffset: 0.6,
|
|
49
|
+
haloAmp: 0.16,
|
|
50
|
+
breathBase: 0.04,
|
|
51
|
+
noise: 0.1,
|
|
52
|
+
ambientAmp: 0.36,
|
|
53
|
+
ambientCenter: 0.5,
|
|
54
|
+
ambientWidth: 0.34,
|
|
55
|
+
shadowMix: 0.1,
|
|
56
|
+
primaryMix: 0.3,
|
|
57
|
+
originX: 4.5,
|
|
58
|
+
originY: 13.5,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Shadow markers (rendered chars in parens):
|
|
62
|
+
// _ = full shadow cell (space with bg=shadow)
|
|
63
|
+
// ^ = letter top, shadow bottom (▀ with fg=letter, bg=shadow)
|
|
64
|
+
// ~ = shadow top only (▀ with fg=shadow)
|
|
65
|
+
const GAP = 1
|
|
66
|
+
const WIDTH = 0.76
|
|
67
|
+
const GAIN = 2.3
|
|
68
|
+
const FLASH = 2.15
|
|
69
|
+
const TRAIL = 0.28
|
|
70
|
+
const SWELL = 0.24
|
|
71
|
+
const WIDE = 1.85
|
|
72
|
+
const DRIFT = 1.45
|
|
73
|
+
const EXPAND = 1.62
|
|
74
|
+
const LIFE = 1020
|
|
75
|
+
const CHARGE = 3000
|
|
76
|
+
const HOLD = 90
|
|
77
|
+
const SINK = 40
|
|
78
|
+
const ARC = 2.2
|
|
79
|
+
const FORK = 1.2
|
|
80
|
+
const DIM = 1.04
|
|
81
|
+
const KICK = 0.86
|
|
82
|
+
const LAG = 60
|
|
83
|
+
const SUCK = 0.34
|
|
84
|
+
const SHIMMER_IN = 60
|
|
85
|
+
const SHIMMER_OUT = 2.8
|
|
86
|
+
const TRACE = 0.033
|
|
87
|
+
const TAIL = 1.8
|
|
88
|
+
const TRACE_IN = 200
|
|
89
|
+
const GLOW_OUT = 1600
|
|
90
|
+
const PEAK = RGBA.fromInts(255, 255, 255)
|
|
91
|
+
|
|
92
|
+
type Ring = {
|
|
93
|
+
x: number
|
|
94
|
+
y: number
|
|
95
|
+
at: number
|
|
96
|
+
force: number
|
|
97
|
+
kick: number
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type Hold = {
|
|
101
|
+
x: number
|
|
102
|
+
y: number
|
|
103
|
+
at: number
|
|
104
|
+
glyph: number | undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type Release = {
|
|
108
|
+
x: number
|
|
109
|
+
y: number
|
|
110
|
+
at: number
|
|
111
|
+
glyph: number | undefined
|
|
112
|
+
level: number
|
|
113
|
+
rise: number
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
type Glow = {
|
|
117
|
+
glyph: number
|
|
118
|
+
at: number
|
|
119
|
+
force: number
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type Frame = {
|
|
123
|
+
t: number
|
|
124
|
+
list: Ring[]
|
|
125
|
+
hold: Hold | undefined
|
|
126
|
+
release: Release | undefined
|
|
127
|
+
glow: Glow | undefined
|
|
128
|
+
spark: number
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const NEAR = [
|
|
132
|
+
[1, 0],
|
|
133
|
+
[1, 1],
|
|
134
|
+
[0, 1],
|
|
135
|
+
[-1, 1],
|
|
136
|
+
[-1, 0],
|
|
137
|
+
[-1, -1],
|
|
138
|
+
[0, -1],
|
|
139
|
+
[1, -1],
|
|
140
|
+
] as const
|
|
141
|
+
|
|
142
|
+
type Trace = {
|
|
143
|
+
glyph: number
|
|
144
|
+
i: number
|
|
145
|
+
l: number
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function clamp(n: number) {
|
|
149
|
+
return Math.max(0, Math.min(1, n))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function lerp(a: number, b: number, t: number) {
|
|
153
|
+
return a + (b - a) * clamp(t)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ease(t: number) {
|
|
157
|
+
const p = clamp(t)
|
|
158
|
+
return p * p * (3 - 2 * p)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function push(t: number) {
|
|
162
|
+
const p = clamp(t)
|
|
163
|
+
return ease(p * p)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ramp(t: number, start: number, end: number) {
|
|
167
|
+
if (end <= start) return ease(t >= end ? 1 : 0)
|
|
168
|
+
return ease((t - start) / (end - start))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function glow(base: RGBA, theme: ReturnType<typeof useTheme>["theme"], n: number) {
|
|
172
|
+
const mid = tint(base, theme.primary, 0.84)
|
|
173
|
+
const top = tint(theme.primary, PEAK, 0.96)
|
|
174
|
+
if (n <= 1) return tint(base, mid, Math.min(1, Math.sqrt(Math.max(0, n)) * 1.14))
|
|
175
|
+
return tint(mid, top, Math.min(1, 1 - Math.exp(-2.4 * (n - 1))))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function shade(base: RGBA, theme: ReturnType<typeof useTheme>["theme"], n: number) {
|
|
179
|
+
if (n >= 0) return glow(base, theme, n)
|
|
180
|
+
return tint(base, theme.background, Math.min(0.82, -n * 0.64))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function ghost(n: number, scale: number) {
|
|
184
|
+
if (n < 0) return n
|
|
185
|
+
return n * scale
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function noise(x: number, y: number, t: number) {
|
|
189
|
+
const n = Math.sin(x * 12.9898 + y * 78.233 + t * 0.043) * 43758.5453
|
|
190
|
+
return n - Math.floor(n)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function lit(char: string) {
|
|
194
|
+
return char !== " " && char !== "_" && char !== "~" && char !== ","
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function key(x: number, y: number) {
|
|
198
|
+
return `${x},${y}`
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function route(list: Array<{ x: number; y: number }>) {
|
|
202
|
+
const left = new Map(list.map((item) => [key(item.x, item.y), item]))
|
|
203
|
+
const path: Array<{ x: number; y: number }> = []
|
|
204
|
+
let cur = [...left.values()].sort((a, b) => a.y - b.y || a.x - b.x)[0]
|
|
205
|
+
let dir = { x: 1, y: 0 }
|
|
206
|
+
|
|
207
|
+
while (cur) {
|
|
208
|
+
path.push(cur)
|
|
209
|
+
left.delete(key(cur.x, cur.y))
|
|
210
|
+
if (!left.size) return path
|
|
211
|
+
|
|
212
|
+
const next = NEAR.map(([dx, dy]) => left.get(key(cur.x + dx, cur.y + dy)))
|
|
213
|
+
.filter((item): item is { x: number; y: number } => !!item)
|
|
214
|
+
.sort((a, b) => {
|
|
215
|
+
const ax = a.x - cur.x
|
|
216
|
+
const ay = a.y - cur.y
|
|
217
|
+
const bx = b.x - cur.x
|
|
218
|
+
const by = b.y - cur.y
|
|
219
|
+
const adot = ax * dir.x + ay * dir.y
|
|
220
|
+
const bdot = bx * dir.x + by * dir.y
|
|
221
|
+
if (adot !== bdot) return bdot - adot
|
|
222
|
+
return Math.abs(ax) + Math.abs(ay) - (Math.abs(bx) + Math.abs(by))
|
|
223
|
+
})[0]
|
|
224
|
+
|
|
225
|
+
if (!next) {
|
|
226
|
+
cur = [...left.values()].sort((a, b) => {
|
|
227
|
+
const da = (a.x - cur.x) ** 2 + (a.y - cur.y) ** 2
|
|
228
|
+
const db = (b.x - cur.x) ** 2 + (b.y - cur.y) ** 2
|
|
229
|
+
return da - db
|
|
230
|
+
})[0]
|
|
231
|
+
dir = { x: 1, y: 0 }
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
dir = { x: next.x - cur.x, y: next.y - cur.y }
|
|
236
|
+
cur = next
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return path
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function mapGlyphs(full: string[]) {
|
|
243
|
+
const cells = [] as Array<{ x: number; y: number }>
|
|
244
|
+
|
|
245
|
+
for (let y = 0; y < full.length; y++) {
|
|
246
|
+
for (let x = 0; x < (full[y]?.length ?? 0); x++) {
|
|
247
|
+
if (lit(full[y]?.[x] ?? " ")) cells.push({ x, y })
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const all = new Map(cells.map((item) => [key(item.x, item.y), item]))
|
|
252
|
+
const seen = new Set<string>()
|
|
253
|
+
const glyph = new Map<string, number>()
|
|
254
|
+
const trace = new Map<string, Trace>()
|
|
255
|
+
const center = new Map<number, { x: number; y: number }>()
|
|
256
|
+
let id = 0
|
|
257
|
+
|
|
258
|
+
for (const item of cells) {
|
|
259
|
+
const start = key(item.x, item.y)
|
|
260
|
+
if (seen.has(start)) continue
|
|
261
|
+
const stack = [item]
|
|
262
|
+
const part = [] as Array<{ x: number; y: number }>
|
|
263
|
+
seen.add(start)
|
|
264
|
+
|
|
265
|
+
while (stack.length) {
|
|
266
|
+
const cur = stack.pop()!
|
|
267
|
+
part.push(cur)
|
|
268
|
+
glyph.set(key(cur.x, cur.y), id)
|
|
269
|
+
for (const [dx, dy] of NEAR) {
|
|
270
|
+
const next = all.get(key(cur.x + dx, cur.y + dy))
|
|
271
|
+
if (!next) continue
|
|
272
|
+
const mark = key(next.x, next.y)
|
|
273
|
+
if (seen.has(mark)) continue
|
|
274
|
+
seen.add(mark)
|
|
275
|
+
stack.push(next)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const path = route(part)
|
|
280
|
+
path.forEach((cell, i) => trace.set(key(cell.x, cell.y), { glyph: id, i, l: path.length }))
|
|
281
|
+
center.set(id, {
|
|
282
|
+
x: part.reduce((sum, item) => sum + item.x, 0) / part.length + 0.5,
|
|
283
|
+
y: (part.reduce((sum, item) => sum + item.y, 0) / part.length) * 2 + 1,
|
|
284
|
+
})
|
|
285
|
+
id++
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { glyph, trace, center }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
type LogoContext = {
|
|
292
|
+
LEFT: number
|
|
293
|
+
FULL: string[]
|
|
294
|
+
SPAN: number
|
|
295
|
+
MAP: ReturnType<typeof mapGlyphs>
|
|
296
|
+
shape: LogoShape
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function build(shape: LogoShape): LogoContext {
|
|
300
|
+
const LEFT = shape.left[0]?.length ?? 0
|
|
301
|
+
const FULL = shape.left.map((line, i) => line + " ".repeat(GAP) + shape.right[i])
|
|
302
|
+
const SPAN = Math.hypot(FULL[0]?.length ?? 0, FULL.length * 2) * 0.94
|
|
303
|
+
return { LEFT, FULL, SPAN, MAP: mapGlyphs(FULL), shape }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const DEFAULT = build(logo)
|
|
307
|
+
const GO = build(go)
|
|
308
|
+
|
|
309
|
+
function shimmer(x: number, y: number, frame: Frame, ctx: LogoContext) {
|
|
310
|
+
return frame.list.reduce((best, item) => {
|
|
311
|
+
const age = frame.t - item.at
|
|
312
|
+
if (age < SHIMMER_IN || age > LIFE) return best
|
|
313
|
+
const dx = x + 0.5 - item.x
|
|
314
|
+
const dy = y * 2 + 1 - item.y
|
|
315
|
+
const dist = Math.hypot(dx, dy)
|
|
316
|
+
const p = age / LIFE
|
|
317
|
+
const r = ctx.SPAN * (1 - (1 - p) ** EXPAND)
|
|
318
|
+
const lag = r - dist
|
|
319
|
+
if (lag < 0.18 || lag > SHIMMER_OUT) return best
|
|
320
|
+
const band = Math.exp(-(((lag - 1.05) / 0.68) ** 2))
|
|
321
|
+
const wobble = 0.5 + 0.5 * Math.sin(frame.t * 0.035 + x * 0.9 + y * 1.7)
|
|
322
|
+
const n = band * wobble * (1 - p) ** 1.45
|
|
323
|
+
if (n > best) return n
|
|
324
|
+
return best
|
|
325
|
+
}, 0)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function remain(x: number, y: number, item: Release, t: number, ctx: LogoContext) {
|
|
329
|
+
const age = t - item.at
|
|
330
|
+
if (age < 0 || age > LIFE) return 0
|
|
331
|
+
const p = age / LIFE
|
|
332
|
+
const dx = x + 0.5 - item.x - 0.5
|
|
333
|
+
const dy = y * 2 + 1 - item.y * 2 - 1
|
|
334
|
+
const dist = Math.hypot(dx, dy)
|
|
335
|
+
const r = ctx.SPAN * (1 - (1 - p) ** EXPAND)
|
|
336
|
+
if (dist > r) return 1
|
|
337
|
+
return clamp((r - dist) / 1.35 < 1 ? 1 - (r - dist) / 1.35 : 0)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function wave(x: number, y: number, frame: Frame, live: boolean, ctx: LogoContext) {
|
|
341
|
+
return frame.list.reduce((sum, item) => {
|
|
342
|
+
const age = frame.t - item.at
|
|
343
|
+
if (age < 0 || age > LIFE) return sum
|
|
344
|
+
const p = age / LIFE
|
|
345
|
+
const dx = x + 0.5 - item.x
|
|
346
|
+
const dy = y * 2 + 1 - item.y
|
|
347
|
+
const dist = Math.hypot(dx, dy)
|
|
348
|
+
const r = ctx.SPAN * (1 - (1 - p) ** EXPAND)
|
|
349
|
+
const fade = (1 - p) ** 1.32
|
|
350
|
+
const j = 1.02 + noise(x + item.x * 0.7, y + item.y * 0.7, item.at * 0.002 + age * 0.06) * 0.52
|
|
351
|
+
const edge = Math.exp(-(((dist - r) / WIDTH) ** 2)) * GAIN * fade * item.force * j
|
|
352
|
+
const swell = Math.exp(-(((dist - Math.max(0, r - DRIFT)) / WIDE) ** 2)) * SWELL * fade * item.force
|
|
353
|
+
const trail = dist < r ? Math.exp(-(r - dist) / 2.4) * TRAIL * fade * item.force * lerp(0.92, 1.22, j) : 0
|
|
354
|
+
const flash = Math.exp(-(dist * dist) / 3.2) * FLASH * item.force * Math.max(0, 1 - age / 140) * lerp(0.95, 1.18, j)
|
|
355
|
+
const kick = Math.exp(-(dist * dist) / 2) * item.kick * Math.max(0, 1 - age / 100)
|
|
356
|
+
const suck = Math.exp(-(((dist - 1.25) / 0.75) ** 2)) * item.kick * SUCK * Math.max(0, 1 - age / 110)
|
|
357
|
+
const wake = live && dist < r ? Math.exp(-(r - dist) / 1.25) * 0.32 * fade : 0
|
|
358
|
+
return sum + edge + swell + trail + flash + wake - kick - suck
|
|
359
|
+
}, 0)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function field(x: number, y: number, frame: Frame, ctx: LogoContext) {
|
|
363
|
+
const held = frame.hold
|
|
364
|
+
const rest = frame.release
|
|
365
|
+
const item = held ?? rest
|
|
366
|
+
if (!item) return 0
|
|
367
|
+
const rise = held ? ramp(frame.t - held.at, HOLD, CHARGE) : rest!.rise
|
|
368
|
+
const level = held ? push(rise) : rest!.level
|
|
369
|
+
const body = rise
|
|
370
|
+
const storm = level * level
|
|
371
|
+
const sink = held ? ramp(frame.t - held.at, SINK, CHARGE) : rest!.rise
|
|
372
|
+
const dx = x + 0.5 - item.x - 0.5
|
|
373
|
+
const dy = y * 2 + 1 - item.y * 2 - 1
|
|
374
|
+
const dist = Math.hypot(dx, dy)
|
|
375
|
+
const angle = Math.atan2(dy, dx)
|
|
376
|
+
const spin = frame.t * lerp(0.008, 0.018, storm)
|
|
377
|
+
const dim = lerp(0, DIM, sink) * lerp(0.99, 1.01, 0.5 + 0.5 * Math.sin(frame.t * 0.014))
|
|
378
|
+
const core = Math.exp(-(dist * dist) / Math.max(0.22, lerp(0.22, 3.2, body))) * lerp(0.42, 2.45, body)
|
|
379
|
+
const shell =
|
|
380
|
+
Math.exp(-(((dist - lerp(0.16, 2.05, body)) / Math.max(0.18, lerp(0.18, 0.82, body))) ** 2)) * lerp(0.1, 0.95, body)
|
|
381
|
+
const ember =
|
|
382
|
+
Math.exp(-(((dist - lerp(0.45, 2.65, body)) / Math.max(0.14, lerp(0.14, 0.62, body))) ** 2)) *
|
|
383
|
+
lerp(0.02, 0.78, body)
|
|
384
|
+
const arc = Math.max(0, Math.cos(angle * 3 - spin + frame.spark * 2.2)) ** 8
|
|
385
|
+
const seam = Math.max(0, Math.cos(angle * 5 + spin * 1.55)) ** 12
|
|
386
|
+
const ring = Math.exp(-(((dist - lerp(1.05, 3, level)) / 0.48) ** 2)) * arc * lerp(0.03, 0.5 + ARC, storm)
|
|
387
|
+
const fork = Math.exp(-(((dist - (1.55 + storm * 2.1)) / 0.36) ** 2)) * seam * storm * FORK
|
|
388
|
+
const spark = Math.max(0, noise(x, y, frame.t) - lerp(0.94, 0.66, storm)) * lerp(0, 5.4, storm)
|
|
389
|
+
const glitch = spark * Math.exp(-dist / Math.max(1.2, 3.1 - storm))
|
|
390
|
+
const crack = Math.max(0, Math.cos((dx - dy) * 1.6 + spin * 2.1)) ** 18
|
|
391
|
+
const lash = crack * Math.exp(-(((dist - (1.95 + storm * 2)) / 0.28) ** 2)) * storm * 1.1
|
|
392
|
+
const flicker =
|
|
393
|
+
Math.max(0, noise(item.x * 3.1, item.y * 2.7, frame.t * 1.7) - 0.72) *
|
|
394
|
+
Math.exp(-(dist * dist) / 0.15) *
|
|
395
|
+
lerp(0.08, 0.42, body)
|
|
396
|
+
const fade = frame.release && !frame.hold ? remain(x, y, frame.release, frame.t, ctx) : 1
|
|
397
|
+
return (core + shell + ember + ring + fork + glitch + lash + flicker - dim) * fade
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function pick(x: number, y: number, frame: Frame, ctx: LogoContext) {
|
|
401
|
+
const held = frame.hold
|
|
402
|
+
const rest = frame.release
|
|
403
|
+
const item = held ?? rest
|
|
404
|
+
if (!item) return 0
|
|
405
|
+
const rise = held ? ramp(frame.t - held.at, HOLD, CHARGE) : rest!.rise
|
|
406
|
+
const dx = x + 0.5 - item.x - 0.5
|
|
407
|
+
const dy = y * 2 + 1 - item.y * 2 - 1
|
|
408
|
+
const dist = Math.hypot(dx, dy)
|
|
409
|
+
const fade = frame.release && !frame.hold ? remain(x, y, frame.release, frame.t, ctx) : 1
|
|
410
|
+
return Math.exp(-(dist * dist) / 1.7) * lerp(0.2, 0.96, rise) * fade
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function select(x: number, y: number, ctx: LogoContext) {
|
|
414
|
+
const direct = ctx.MAP.glyph.get(key(x, y))
|
|
415
|
+
if (direct !== undefined) return direct
|
|
416
|
+
|
|
417
|
+
const near = NEAR.map(([dx, dy]) => ctx.MAP.glyph.get(key(x + dx, y + dy))).find(
|
|
418
|
+
(item): item is number => item !== undefined,
|
|
419
|
+
)
|
|
420
|
+
return near
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function trace(x: number, y: number, frame: Frame, ctx: LogoContext) {
|
|
424
|
+
const held = frame.hold
|
|
425
|
+
const rest = frame.release
|
|
426
|
+
const item = held ?? rest
|
|
427
|
+
if (!item || item.glyph === undefined) return 0
|
|
428
|
+
const step = ctx.MAP.trace.get(key(x, y))
|
|
429
|
+
if (!step || step.glyph !== item.glyph || step.l < 2) return 0
|
|
430
|
+
const age = frame.t - item.at
|
|
431
|
+
const rise = held ? ramp(age, HOLD, CHARGE) : rest!.rise
|
|
432
|
+
const appear = held ? ramp(age, 0, TRACE_IN) : 1
|
|
433
|
+
const speed = lerp(TRACE * 0.48, TRACE * 0.88, rise)
|
|
434
|
+
const head = (age * speed) % step.l
|
|
435
|
+
const dist = Math.min(Math.abs(step.i - head), step.l - Math.abs(step.i - head))
|
|
436
|
+
const tail = (head - TAIL + step.l) % step.l
|
|
437
|
+
const lag = Math.min(Math.abs(step.i - tail), step.l - Math.abs(step.i - tail))
|
|
438
|
+
const fade = frame.release && !frame.hold ? remain(x, y, frame.release, frame.t, ctx) : 1
|
|
439
|
+
const core = Math.exp(-((dist / 1.05) ** 2)) * lerp(0.8, 2.35, rise)
|
|
440
|
+
const glow = Math.exp(-((dist / 1.85) ** 2)) * lerp(0.08, 0.34, rise)
|
|
441
|
+
const trail = Math.exp(-((lag / 1.45) ** 2)) * lerp(0.04, 0.42, rise)
|
|
442
|
+
return (core + glow + trail) * appear * fade
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function idle(
|
|
446
|
+
x: number,
|
|
447
|
+
pixelY: number,
|
|
448
|
+
frame: Frame,
|
|
449
|
+
ctx: LogoContext,
|
|
450
|
+
state: IdleState,
|
|
451
|
+
): { glow: number; peak: number; primary: number } {
|
|
452
|
+
const cfg = state.cfg
|
|
453
|
+
const dx = x + 0.5 - cfg.originX
|
|
454
|
+
const dy = pixelY - cfg.originY
|
|
455
|
+
const dist = Math.hypot(dx, dy)
|
|
456
|
+
const angle = Math.atan2(dy, dx)
|
|
457
|
+
const wob1 = noise(x * 0.32, pixelY * 0.25, frame.t * 0.0005) - 0.5
|
|
458
|
+
const wob2 = noise(x * 0.12, pixelY * 0.08, frame.t * 0.00022) - 0.5
|
|
459
|
+
const ripple = Math.sin(angle * 3 + frame.t * 0.0012) * 0.3
|
|
460
|
+
const jitter = (wob1 * 0.55 + wob2 * 0.32 + ripple * 0.18) * cfg.noise
|
|
461
|
+
const traveled = dist + jitter
|
|
462
|
+
let glow = 0
|
|
463
|
+
let peak = 0
|
|
464
|
+
let halo = 0
|
|
465
|
+
let primary = 0
|
|
466
|
+
let ambient = 0
|
|
467
|
+
for (const active of state.active) {
|
|
468
|
+
const head = active.head
|
|
469
|
+
const eased = active.eased
|
|
470
|
+
const delta = traveled - head
|
|
471
|
+
// Use shallower exponent (1.6 vs 2) for softer edges on the Gaussians
|
|
472
|
+
// so adjacent pixels have smaller brightness deltas
|
|
473
|
+
const core = Math.exp(-(Math.abs(delta / cfg.coreWidth) ** 1.8))
|
|
474
|
+
const soft = Math.exp(-(Math.abs(delta / cfg.softWidth) ** 1.6))
|
|
475
|
+
const tailRange = cfg.tail * 2.6
|
|
476
|
+
const tail = delta < 0 && delta > -tailRange ? (1 + delta / tailRange) ** 2.6 : 0
|
|
477
|
+
const haloDelta = delta + cfg.haloOffset
|
|
478
|
+
const haloBand = Math.exp(-(Math.abs(haloDelta / cfg.haloWidth) ** 1.6))
|
|
479
|
+
glow += (soft * cfg.softAmp + tail * cfg.tailAmp) * eased
|
|
480
|
+
peak += core * cfg.coreAmp * eased
|
|
481
|
+
halo += haloBand * cfg.haloAmp * eased
|
|
482
|
+
// Primary-tinted fringe follows the halo (which trails behind the core) and the tail
|
|
483
|
+
primary += (haloBand + tail * 0.6) * eased
|
|
484
|
+
ambient += active.ambient
|
|
485
|
+
}
|
|
486
|
+
ambient /= state.rings
|
|
487
|
+
return {
|
|
488
|
+
glow: glow / state.rings,
|
|
489
|
+
peak: cfg.breathBase + ambient + (peak + halo) / state.rings,
|
|
490
|
+
primary: (primary / state.rings) * cfg.primaryMix,
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function bloom(x: number, y: number, frame: Frame, ctx: LogoContext) {
|
|
495
|
+
const item = frame.glow
|
|
496
|
+
if (!item) return 0
|
|
497
|
+
const glyph = ctx.MAP.glyph.get(key(x, y))
|
|
498
|
+
if (glyph !== item.glyph) return 0
|
|
499
|
+
const age = frame.t - item.at
|
|
500
|
+
if (age < 0 || age > GLOW_OUT) return 0
|
|
501
|
+
const p = age / GLOW_OUT
|
|
502
|
+
const flash = (1 - p) ** 2
|
|
503
|
+
const dx = x + 0.5 - ctx.MAP.center.get(item.glyph)!.x
|
|
504
|
+
const dy = y * 2 + 1 - ctx.MAP.center.get(item.glyph)!.y
|
|
505
|
+
const bias = Math.exp(-((Math.hypot(dx, dy) / 2.8) ** 2))
|
|
506
|
+
return lerp(item.force, item.force * 0.18, p) * lerp(0.72, 1.1, bias) * flash
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
type IdleState = {
|
|
510
|
+
cfg: ShimmerConfig
|
|
511
|
+
reach: number
|
|
512
|
+
rings: number
|
|
513
|
+
active: Array<{
|
|
514
|
+
head: number
|
|
515
|
+
eased: number
|
|
516
|
+
ambient: number
|
|
517
|
+
}>
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function buildIdleState(t: number, ctx: LogoContext): IdleState {
|
|
521
|
+
const cfg = shimmerConfig
|
|
522
|
+
const w = ctx.FULL[0]?.length ?? 1
|
|
523
|
+
const h = ctx.FULL.length * 2
|
|
524
|
+
const corners: [number, number][] = [
|
|
525
|
+
[0, 0],
|
|
526
|
+
[w, 0],
|
|
527
|
+
[0, h],
|
|
528
|
+
[w, h],
|
|
529
|
+
]
|
|
530
|
+
let maxCorner = 0
|
|
531
|
+
for (const [cx, cy] of corners) {
|
|
532
|
+
const d = Math.hypot(cx - cfg.originX, cy - cfg.originY)
|
|
533
|
+
if (d > maxCorner) maxCorner = d
|
|
534
|
+
}
|
|
535
|
+
const reach = maxCorner + cfg.tail * 2
|
|
536
|
+
const rings = Math.max(1, Math.floor(cfg.rings))
|
|
537
|
+
const active = [] as IdleState["active"]
|
|
538
|
+
for (let i = 0; i < rings; i++) {
|
|
539
|
+
const offset = i / rings
|
|
540
|
+
const cyclePhase = (t / cfg.period + offset) % 1
|
|
541
|
+
if (cyclePhase >= cfg.sweepFraction) continue
|
|
542
|
+
const phase = cyclePhase / cfg.sweepFraction
|
|
543
|
+
const envelope = Math.sin(phase * Math.PI)
|
|
544
|
+
const eased = envelope * envelope * (3 - 2 * envelope)
|
|
545
|
+
const d = (phase - cfg.ambientCenter) / cfg.ambientWidth
|
|
546
|
+
active.push({
|
|
547
|
+
head: phase * reach,
|
|
548
|
+
eased,
|
|
549
|
+
ambient: Math.abs(d) < 1 ? (1 - d * d) ** 2 * cfg.ambientAmp : 0,
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
return { cfg, reach, rings, active }
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } = {}) {
|
|
556
|
+
const ctx = props.shape ? build(props.shape) : DEFAULT
|
|
557
|
+
const { theme } = useTheme()
|
|
558
|
+
const renderer = useRenderer()
|
|
559
|
+
const [rings, setRings] = createSignal<Ring[]>([])
|
|
560
|
+
const [hold, setHold] = createSignal<Hold>()
|
|
561
|
+
const [release, setRelease] = createSignal<Release>()
|
|
562
|
+
const [glow, setGlow] = createSignal<Glow>()
|
|
563
|
+
const [now, setNow] = createSignal(0)
|
|
564
|
+
let box: BoxRenderable | undefined
|
|
565
|
+
let timer: ReturnType<typeof setInterval> | undefined
|
|
566
|
+
let hum = false
|
|
567
|
+
|
|
568
|
+
const stop = () => {
|
|
569
|
+
if (!timer) return
|
|
570
|
+
clearInterval(timer)
|
|
571
|
+
timer = undefined
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const tick = () => {
|
|
575
|
+
const t = performance.now()
|
|
576
|
+
setNow(t)
|
|
577
|
+
const item = hold()
|
|
578
|
+
if (item && !hum && t - item.at >= HOLD) {
|
|
579
|
+
hum = true
|
|
580
|
+
Sound.start()
|
|
581
|
+
}
|
|
582
|
+
if (item && t - item.at >= CHARGE) {
|
|
583
|
+
burst(item.x, item.y)
|
|
584
|
+
}
|
|
585
|
+
let live = false
|
|
586
|
+
setRings((list) => {
|
|
587
|
+
const next = list.filter((item) => t - item.at < LIFE)
|
|
588
|
+
live = next.length > 0
|
|
589
|
+
return next
|
|
590
|
+
})
|
|
591
|
+
const flash = glow()
|
|
592
|
+
if (flash && t - flash.at >= GLOW_OUT) {
|
|
593
|
+
setGlow(undefined)
|
|
594
|
+
}
|
|
595
|
+
if (!live) setRelease(undefined)
|
|
596
|
+
if (live || hold() || release() || glow()) return
|
|
597
|
+
if (props.idle) return
|
|
598
|
+
stop()
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const start = () => {
|
|
602
|
+
if (timer) return
|
|
603
|
+
timer = setInterval(tick, 16)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
onCleanup(() => {
|
|
607
|
+
stop()
|
|
608
|
+
hum = false
|
|
609
|
+
Sound.dispose()
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
onMount(() => {
|
|
613
|
+
if (!props.idle) return
|
|
614
|
+
setNow(performance.now())
|
|
615
|
+
start()
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
const hit = (x: number, y: number) => {
|
|
619
|
+
const char = ctx.FULL[y]?.[x]
|
|
620
|
+
return char !== undefined && char !== " "
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const press = (x: number, y: number, t: number) => {
|
|
624
|
+
const last = hold()
|
|
625
|
+
if (last) burst(last.x, last.y)
|
|
626
|
+
setNow(t)
|
|
627
|
+
if (!last) setRelease(undefined)
|
|
628
|
+
setHold({ x, y, at: t, glyph: select(x, y, ctx) })
|
|
629
|
+
hum = false
|
|
630
|
+
start()
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const burst = (x: number, y: number) => {
|
|
634
|
+
const item = hold()
|
|
635
|
+
if (!item) return
|
|
636
|
+
hum = false
|
|
637
|
+
const t = performance.now()
|
|
638
|
+
const age = t - item.at
|
|
639
|
+
const rise = ramp(age, HOLD, CHARGE)
|
|
640
|
+
const level = push(rise)
|
|
641
|
+
setHold(undefined)
|
|
642
|
+
setRelease({ x, y, at: t, glyph: item.glyph, level, rise })
|
|
643
|
+
if (item.glyph !== undefined) {
|
|
644
|
+
setGlow({ glyph: item.glyph, at: t, force: lerp(0.18, 1.5, rise * level) })
|
|
645
|
+
}
|
|
646
|
+
setRings((list) => [
|
|
647
|
+
...list,
|
|
648
|
+
{
|
|
649
|
+
x: x + 0.5,
|
|
650
|
+
y: y * 2 + 1,
|
|
651
|
+
at: t,
|
|
652
|
+
force: lerp(0.82, 2.55, level),
|
|
653
|
+
kick: lerp(0.32, 0.32 + KICK, level),
|
|
654
|
+
},
|
|
655
|
+
])
|
|
656
|
+
setNow(t)
|
|
657
|
+
start()
|
|
658
|
+
Sound.pulse(lerp(0.8, 1, level))
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const frame = createMemo(() => {
|
|
662
|
+
const t = now()
|
|
663
|
+
const item = hold()
|
|
664
|
+
return {
|
|
665
|
+
t,
|
|
666
|
+
list: rings(),
|
|
667
|
+
hold: item,
|
|
668
|
+
release: release(),
|
|
669
|
+
glow: glow(),
|
|
670
|
+
spark: item ? noise(item.x, item.y, t) : 0,
|
|
671
|
+
}
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
const dusk = createMemo(() => {
|
|
675
|
+
const base = frame()
|
|
676
|
+
const t = base.t - LAG
|
|
677
|
+
const item = base.hold
|
|
678
|
+
return {
|
|
679
|
+
t,
|
|
680
|
+
list: base.list,
|
|
681
|
+
hold: item,
|
|
682
|
+
release: base.release,
|
|
683
|
+
glow: base.glow,
|
|
684
|
+
spark: item ? noise(item.x, item.y, t) : 0,
|
|
685
|
+
}
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
const idleState = createMemo(() => (props.idle ? buildIdleState(frame().t, ctx) : undefined))
|
|
689
|
+
const useSubpixelBlocks = () => renderer.capabilities?.rgb === true
|
|
690
|
+
|
|
691
|
+
const renderLine = (
|
|
692
|
+
line: string,
|
|
693
|
+
y: number,
|
|
694
|
+
ink: RGBA,
|
|
695
|
+
bold: boolean,
|
|
696
|
+
off: number,
|
|
697
|
+
frame: Frame,
|
|
698
|
+
dusk: Frame,
|
|
699
|
+
state: IdleState | undefined,
|
|
700
|
+
): JSX.Element[] => {
|
|
701
|
+
const shadow = tint(theme.background, ink, 0.25)
|
|
702
|
+
const attrs = bold ? TextAttributes.BOLD : undefined
|
|
703
|
+
|
|
704
|
+
return Array.from(line).map((char, i) => {
|
|
705
|
+
if (char === " ") {
|
|
706
|
+
return (
|
|
707
|
+
<text fg={ink} attributes={attrs} selectable={false}>
|
|
708
|
+
{char}
|
|
709
|
+
</text>
|
|
710
|
+
)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const h = field(off + i, y, frame, ctx)
|
|
714
|
+
const charLit = lit(char)
|
|
715
|
+
// Sub-pixel sampling: cells are 2 pixels tall. Sample at top (y*2) and bottom (y*2+1) pixel rows.
|
|
716
|
+
const pulseTop = state ? idle(off + i, y * 2, frame, ctx, state) : { glow: 0, peak: 0, primary: 0 }
|
|
717
|
+
const pulseBot = state ? idle(off + i, y * 2 + 1, frame, ctx, state) : { glow: 0, peak: 0, primary: 0 }
|
|
718
|
+
const peakMixTop = charLit ? Math.min(1, pulseTop.peak) : 0
|
|
719
|
+
const peakMixBot = charLit ? Math.min(1, pulseBot.peak) : 0
|
|
720
|
+
const primaryMixTop = charLit ? Math.min(1, pulseTop.primary) : 0
|
|
721
|
+
const primaryMixBot = charLit ? Math.min(1, pulseBot.primary) : 0
|
|
722
|
+
// Layer primary tint first, then white peak on top — so the halo/tail pulls toward primary,
|
|
723
|
+
// while the bright core stays pure white
|
|
724
|
+
const inkTopTint = primaryMixTop > 0 ? tint(ink, theme.primary, primaryMixTop) : ink
|
|
725
|
+
const inkBotTint = primaryMixBot > 0 ? tint(ink, theme.primary, primaryMixBot) : ink
|
|
726
|
+
const inkTop = peakMixTop > 0 ? tint(inkTopTint, PEAK, peakMixTop) : inkTopTint
|
|
727
|
+
const inkBot = peakMixBot > 0 ? tint(inkBotTint, PEAK, peakMixBot) : inkBotTint
|
|
728
|
+
// For the non-peak-aware brightness channels, use the average of top/bot
|
|
729
|
+
const pulse = {
|
|
730
|
+
glow: (pulseTop.glow + pulseBot.glow) / 2,
|
|
731
|
+
peak: (pulseTop.peak + pulseBot.peak) / 2,
|
|
732
|
+
primary: (pulseTop.primary + pulseBot.primary) / 2,
|
|
733
|
+
}
|
|
734
|
+
const peakMix = charLit ? Math.min(1, pulse.peak) : 0
|
|
735
|
+
const primaryMix = charLit ? Math.min(1, pulse.primary) : 0
|
|
736
|
+
const inkPrimary = primaryMix > 0 ? tint(ink, theme.primary, primaryMix) : ink
|
|
737
|
+
const inkTinted = peakMix > 0 ? tint(inkPrimary, PEAK, peakMix) : inkPrimary
|
|
738
|
+
const shadowMixCfg = state?.cfg.shadowMix ?? shimmerConfig.shadowMix
|
|
739
|
+
const shadowMixTop = Math.min(1, pulseTop.peak * shadowMixCfg)
|
|
740
|
+
const shadowMixBot = Math.min(1, pulseBot.peak * shadowMixCfg)
|
|
741
|
+
const shadowTop = shadowMixTop > 0 ? tint(shadow, PEAK, shadowMixTop) : shadow
|
|
742
|
+
const shadowBot = shadowMixBot > 0 ? tint(shadow, PEAK, shadowMixBot) : shadow
|
|
743
|
+
const shadowMix = Math.min(1, pulse.peak * shadowMixCfg)
|
|
744
|
+
const shadowTinted = shadowMix > 0 ? tint(shadow, PEAK, shadowMix) : shadow
|
|
745
|
+
const n = wave(off + i, y, frame, charLit, ctx) + h
|
|
746
|
+
const s = wave(off + i, y, dusk, false, ctx) + h
|
|
747
|
+
const p = charLit ? pick(off + i, y, frame, ctx) : 0
|
|
748
|
+
const e = charLit ? trace(off + i, y, frame, ctx) : 0
|
|
749
|
+
const b = charLit ? bloom(off + i, y, frame, ctx) : 0
|
|
750
|
+
const q = shimmer(off + i, y, frame, ctx)
|
|
751
|
+
|
|
752
|
+
if (char === "_") {
|
|
753
|
+
return (
|
|
754
|
+
<text
|
|
755
|
+
fg={shade(inkTinted, theme, s * 0.08)}
|
|
756
|
+
bg={shade(shadowTinted, theme, ghost(s, 0.24) + ghost(q, 0.06))}
|
|
757
|
+
attributes={attrs}
|
|
758
|
+
selectable={false}
|
|
759
|
+
>
|
|
760
|
+
{" "}
|
|
761
|
+
</text>
|
|
762
|
+
)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (char === "^") {
|
|
766
|
+
return (
|
|
767
|
+
<text
|
|
768
|
+
fg={shade(inkTop, theme, n + p + e + b)}
|
|
769
|
+
bg={shade(shadowBot, theme, ghost(s, 0.18) + ghost(q, 0.05) + ghost(b, 0.08))}
|
|
770
|
+
attributes={attrs}
|
|
771
|
+
selectable={false}
|
|
772
|
+
>
|
|
773
|
+
▀
|
|
774
|
+
</text>
|
|
775
|
+
)
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (char === "~") {
|
|
779
|
+
return (
|
|
780
|
+
<text fg={shade(shadowTop, theme, ghost(s, 0.22) + ghost(q, 0.05))} attributes={attrs} selectable={false}>
|
|
781
|
+
▀
|
|
782
|
+
</text>
|
|
783
|
+
)
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (char === ",") {
|
|
787
|
+
return (
|
|
788
|
+
<text fg={shade(shadowBot, theme, ghost(s, 0.22) + ghost(q, 0.05))} attributes={attrs} selectable={false}>
|
|
789
|
+
▄
|
|
790
|
+
</text>
|
|
791
|
+
)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Solid █: render as ▀ so the top pixel (fg) and bottom pixel (bg) can carry independent shimmer values
|
|
795
|
+
if (char === "█" && useSubpixelBlocks()) {
|
|
796
|
+
return (
|
|
797
|
+
<text
|
|
798
|
+
fg={shade(inkTop, theme, n + p + e + b)}
|
|
799
|
+
bg={shade(inkBot, theme, n + p + e + b)}
|
|
800
|
+
attributes={attrs}
|
|
801
|
+
selectable={false}
|
|
802
|
+
>
|
|
803
|
+
▀
|
|
804
|
+
</text>
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ▀ top-half-lit: fg uses top-pixel sample, bg stays transparent/panel
|
|
809
|
+
if (char === "▀") {
|
|
810
|
+
return (
|
|
811
|
+
<text fg={shade(inkTop, theme, n + p + e + b)} attributes={attrs} selectable={false}>
|
|
812
|
+
▀
|
|
813
|
+
</text>
|
|
814
|
+
)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ▄ bottom-half-lit: fg uses bottom-pixel sample
|
|
818
|
+
if (char === "▄") {
|
|
819
|
+
return (
|
|
820
|
+
<text fg={shade(inkBot, theme, n + p + e + b)} attributes={attrs} selectable={false}>
|
|
821
|
+
▄
|
|
822
|
+
</text>
|
|
823
|
+
)
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return (
|
|
827
|
+
<text fg={shade(inkTinted, theme, n + p + e + b)} attributes={attrs} selectable={false}>
|
|
828
|
+
{char}
|
|
829
|
+
</text>
|
|
830
|
+
)
|
|
831
|
+
})
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const mouse = (evt: MouseEvent) => {
|
|
835
|
+
if (!box) return
|
|
836
|
+
if ((evt.type === "down" || evt.type === "drag") && evt.button === MouseButton.LEFT) {
|
|
837
|
+
const x = evt.x - box.x
|
|
838
|
+
const y = evt.y - box.y
|
|
839
|
+
if (!hit(x, y)) return
|
|
840
|
+
if (evt.type === "drag" && hold()) return
|
|
841
|
+
evt.preventDefault()
|
|
842
|
+
evt.stopPropagation()
|
|
843
|
+
const t = performance.now()
|
|
844
|
+
press(x, y, t)
|
|
845
|
+
return
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (!hold()) return
|
|
849
|
+
if (evt.type === "up") {
|
|
850
|
+
const item = hold()
|
|
851
|
+
if (!item) return
|
|
852
|
+
burst(item.x, item.y)
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return (
|
|
857
|
+
<box ref={(item: BoxRenderable) => (box = item)}>
|
|
858
|
+
<box
|
|
859
|
+
position="absolute"
|
|
860
|
+
top={0}
|
|
861
|
+
left={0}
|
|
862
|
+
width={ctx.FULL[0]?.length ?? 0}
|
|
863
|
+
height={ctx.FULL.length}
|
|
864
|
+
zIndex={1}
|
|
865
|
+
onMouse={mouse}
|
|
866
|
+
/>
|
|
867
|
+
<For each={ctx.shape.left}>
|
|
868
|
+
{(line, index) => (
|
|
869
|
+
<box flexDirection="row" gap={1}>
|
|
870
|
+
<box flexDirection="row">
|
|
871
|
+
{renderLine(line, index(), props.ink ?? theme.textMuted, !!props.ink, 0, frame(), dusk(), idleState())}
|
|
872
|
+
</box>
|
|
873
|
+
<box flexDirection="row">
|
|
874
|
+
{renderLine(
|
|
875
|
+
ctx.shape.right[index()],
|
|
876
|
+
index(),
|
|
877
|
+
props.ink ?? theme.text,
|
|
878
|
+
true,
|
|
879
|
+
ctx.LEFT + GAP,
|
|
880
|
+
frame(),
|
|
881
|
+
dusk(),
|
|
882
|
+
idleState(),
|
|
883
|
+
)}
|
|
884
|
+
</box>
|
|
885
|
+
</box>
|
|
886
|
+
)}
|
|
887
|
+
</For>
|
|
888
|
+
</box>
|
|
889
|
+
)
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
export function GoLogo() {
|
|
893
|
+
const { theme } = useTheme()
|
|
894
|
+
const base = tint(theme.background, theme.text, 0.62)
|
|
895
|
+
return <Logo shape={go} ink={base} idle />
|
|
896
|
+
}
|