egaki 0.9.0 → 0.11.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/cli/chatgpt-auth.d.ts +30 -5
  3. package/dist/cli/chatgpt-auth.d.ts.map +1 -1
  4. package/dist/cli/chatgpt-auth.js +257 -79
  5. package/dist/cli/cli.d.ts.map +1 -1
  6. package/dist/cli/cli.js +132 -28
  7. package/dist/cli/dev-server.d.ts +30 -0
  8. package/dist/cli/dev-server.d.ts.map +1 -0
  9. package/dist/cli/dev-server.js +266 -0
  10. package/dist/cli/dev-server.test.d.ts +2 -0
  11. package/dist/cli/dev-server.test.d.ts.map +1 -0
  12. package/dist/cli/dev-server.test.js +95 -0
  13. package/dist/cli/generate.d.ts +8 -0
  14. package/dist/cli/generate.d.ts.map +1 -1
  15. package/dist/cli/generate.js +170 -58
  16. package/dist/cli/generate.test.js +50 -1
  17. package/dist/cli/login.d.ts +38 -3
  18. package/dist/cli/login.d.ts.map +1 -1
  19. package/dist/cli/login.js +44 -17
  20. package/dist/cli/login.test.d.ts +2 -0
  21. package/dist/cli/login.test.d.ts.map +1 -0
  22. package/dist/cli/login.test.js +158 -0
  23. package/dist/cli/open-browser.d.ts +3 -1
  24. package/dist/cli/open-browser.d.ts.map +1 -1
  25. package/dist/cli/open-browser.js +2 -2
  26. package/dist/cli/xai-auth.d.ts +3 -1
  27. package/dist/cli/xai-auth.d.ts.map +1 -1
  28. package/dist/cli/xai-auth.js +13 -3
  29. package/dist/vite/angled-screen-shader.d.ts +5 -0
  30. package/dist/vite/angled-screen-shader.d.ts.map +1 -1
  31. package/dist/vite/angled-screen-shader.js +77 -34
  32. package/dist/vite/generated-media-client.d.ts.map +1 -1
  33. package/dist/vite/generated-media-client.js +2 -2
  34. package/dist/vite/vite-plugin.d.ts.map +1 -1
  35. package/dist/vite/vite-plugin.js +128 -10
  36. package/package.json +6 -10
  37. package/skills/egaki/SKILL.md +14 -0
  38. package/src/cli/chatgpt-auth.ts +379 -89
  39. package/src/cli/cli.ts +161 -27
  40. package/src/cli/dev-server.test.ts +109 -0
  41. package/src/cli/dev-server.ts +303 -0
  42. package/src/cli/generate.test.ts +57 -0
  43. package/src/cli/generate.ts +191 -61
  44. package/src/cli/login.test.ts +185 -0
  45. package/src/cli/login.ts +68 -16
  46. package/src/cli/open-browser.ts +7 -6
  47. package/src/cli/xai-auth.ts +15 -3
  48. package/src/vite/angled-screen-shader.tsx +81 -34
  49. package/src/vite/generated-media-client.tsx +6 -1
  50. package/src/vite/vite-plugin.ts +137 -10
@@ -0,0 +1,158 @@
1
+ // Tests login command routing before any prompt, key read, or OAuth side effect.
2
+ import { describe, expect, test } from 'vitest';
3
+ import { chatGptReLoginMessage, isRevokedChatGptAccessToken, oauthCallbackAction, pickChatGptResponsesModel, } from './chatgpt-auth.js';
4
+ import { describeChatGptExpiry, resolveLoginAction } from './login.js';
5
+ describe('resolveLoginAction', () => {
6
+ test('shows all configured providers', () => {
7
+ expect(resolveLoginAction({
8
+ options: { show: true },
9
+ daemonProvider: undefined,
10
+ isAgent: false,
11
+ })).toMatchInlineSnapshot(`
12
+ {
13
+ "type": "show",
14
+ }
15
+ `);
16
+ });
17
+ test('removes a configured provider', () => {
18
+ expect(resolveLoginAction({
19
+ options: { remove: 'google' },
20
+ daemonProvider: undefined,
21
+ isAgent: false,
22
+ })).toMatchInlineSnapshot(`
23
+ {
24
+ "provider": "google",
25
+ "type": "remove",
26
+ }
27
+ `);
28
+ });
29
+ test('runs the OAuth provider passed to the daemon', () => {
30
+ expect(resolveLoginAction({
31
+ options: {},
32
+ daemonProvider: 'chatgpt',
33
+ isAgent: true,
34
+ })).toMatchInlineSnapshot(`
35
+ {
36
+ "provider": "chatgpt",
37
+ "type": "oauth-daemon",
38
+ }
39
+ `);
40
+ });
41
+ test('rejects a non-OAuth daemon provider', () => {
42
+ expect(resolveLoginAction({
43
+ options: {},
44
+ daemonProvider: 'google',
45
+ isAgent: true,
46
+ })).toMatchInlineSnapshot(`
47
+ [Error: Invalid background login provider: google]
48
+ `);
49
+ });
50
+ test('starts an explicit OAuth provider through the daemon client', () => {
51
+ expect(resolveLoginAction({
52
+ options: { provider: 'xai-oauth' },
53
+ daemonProvider: undefined,
54
+ isAgent: true,
55
+ })).toMatchInlineSnapshot(`
56
+ {
57
+ "provider": "xai-oauth",
58
+ "type": "oauth-client",
59
+ }
60
+ `);
61
+ });
62
+ test('saves an explicit API key in the foreground', () => {
63
+ expect(resolveLoginAction({
64
+ options: { provider: 'google', key: 'secret' },
65
+ daemonProvider: undefined,
66
+ isAgent: true,
67
+ })).toMatchInlineSnapshot(`
68
+ {
69
+ "key": "secret",
70
+ "provider": "google",
71
+ "type": "api-key",
72
+ }
73
+ `);
74
+ });
75
+ test('reads an omitted API key from stdin', () => {
76
+ expect(resolveLoginAction({
77
+ options: { provider: 'google' },
78
+ daemonProvider: undefined,
79
+ isAgent: true,
80
+ })).toMatchInlineSnapshot(`
81
+ {
82
+ "key": undefined,
83
+ "provider": "google",
84
+ "type": "api-key",
85
+ }
86
+ `);
87
+ });
88
+ test('uses the provider picker for an interactive user', () => {
89
+ expect(resolveLoginAction({
90
+ options: {},
91
+ daemonProvider: undefined,
92
+ isAgent: false,
93
+ })).toMatchInlineSnapshot(`
94
+ {
95
+ "type": "interactive",
96
+ }
97
+ `);
98
+ });
99
+ test('does not call a locally unexpired ChatGPT token valid', () => {
100
+ expect(describeChatGptExpiry(2_000, 1_000)).toMatchInlineSnapshot(`"not expired locally"`);
101
+ expect(describeChatGptExpiry(1_000, 2_000)).toMatchInlineSnapshot(`"expired locally, the next request will try to refresh"`);
102
+ });
103
+ test('tells the user to sign in again when ChatGPT revokes the token', () => {
104
+ expect(isRevokedChatGptAccessToken(401, '{"message":"Encountered invalidated oauth token for user, failing request","code":"token_revoked"}')).toBe(true);
105
+ expect(isRevokedChatGptAccessToken(401, 'missing api key')).toBe(false);
106
+ expect(chatGptReLoginMessage({
107
+ status: 401,
108
+ body: 'Token refresh failed',
109
+ })).toMatchInlineSnapshot(`"ChatGPT rejected the saved login (401: Token refresh failed). Run: egaki login --provider chatgpt"`);
110
+ });
111
+ test('a stale callback does not cancel the active ChatGPT login', () => {
112
+ expect(oauthCallbackAction({
113
+ pendingState: 'new-state',
114
+ callbackState: 'old-state',
115
+ error: 'access_denied',
116
+ code: null,
117
+ })).toBe('ignore');
118
+ expect(oauthCallbackAction({
119
+ pendingState: 'new-state',
120
+ callbackState: 'new-state',
121
+ error: 'access_denied',
122
+ code: null,
123
+ })).toBe('provider-error');
124
+ expect(oauthCallbackAction({
125
+ pendingState: 'new-state',
126
+ callbackState: 'new-state',
127
+ error: null,
128
+ code: 'auth-code',
129
+ })).toBe('exchange');
130
+ });
131
+ test('picks the highest-priority ChatGPT model for the saved plan', () => {
132
+ expect(pickChatGptResponsesModel({
133
+ plan: 'prolite',
134
+ models: [
135
+ { slug: 'gpt-5.4', visibility: 'hide', priority: 0, plans: ['prolite'] },
136
+ { slug: 'gpt-6-astra', visibility: 'list', priority: 1, plans: ['prolite'] },
137
+ { slug: 'gpt-5.5', visibility: 'list', priority: 12, plans: ['pro'] },
138
+ ],
139
+ })).toBe('gpt-6-astra');
140
+ expect(pickChatGptResponsesModel({
141
+ plan: 'prolite',
142
+ exclude: ['gpt-6-astra'],
143
+ models: [
144
+ { slug: 'gpt-6-astra', visibility: 'list', priority: 1, plans: ['prolite'] },
145
+ { slug: 'gpt-5.5', visibility: 'list', priority: 12, plans: ['prolite'] },
146
+ ],
147
+ })).toBe('gpt-5.5');
148
+ });
149
+ test('does not open an interactive prompt for an agent', () => {
150
+ expect(resolveLoginAction({
151
+ options: {},
152
+ daemonProvider: undefined,
153
+ isAgent: true,
154
+ })).toMatchInlineSnapshot(`
155
+ [Error: Missing --provider. Run egaki login --provider <name>.]
156
+ `);
157
+ });
158
+ });
@@ -1,2 +1,4 @@
1
- export declare function openUrlInBrowser(url: string): boolean;
1
+ export declare function openUrlInBrowser(url: string, { allowNonInteractive }?: {
2
+ allowNonInteractive?: boolean | undefined;
3
+ }): boolean;
2
4
  //# sourceMappingURL=open-browser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"open-browser.d.ts","sourceRoot":"","sources":["../../src/cli/open-browser.ts"],"names":[],"mappings":"AAyBA,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAsBrD"}
1
+ {"version":3,"file":"open-browser.d.ts","sourceRoot":"","sources":["../../src/cli/open-browser.ts"],"names":[],"mappings":"AAuBA,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,EAAE,mBAA2B,EAAE;;CAAK,GACnC,OAAO,CAsBT"}
@@ -14,9 +14,9 @@ function getOpenCommands(url) {
14
14
  { command: 'gio', args: ['open', url] },
15
15
  ];
16
16
  }
17
- export function openUrlInBrowser(url) {
17
+ export function openUrlInBrowser(url, { allowNonInteractive = false } = {}) {
18
18
  // Avoid launching browser commands in non-interactive contexts (CI/pipes).
19
- if (!process.stdout.isTTY || !process.stdin.isTTY) {
19
+ if (!allowNonInteractive && (!process.stdout.isTTY || !process.stdin.isTTY)) {
20
20
  return false;
21
21
  }
22
22
  for (const candidate of getOpenCommands(url)) {
@@ -19,7 +19,9 @@ export declare function refreshXaiToken(refreshToken: string): Promise<TokenResp
19
19
  * Run the xAI OAuth browser flow. Opens the system browser for authorization,
20
20
  * waits for the callback, and returns an XaiAuth object with tokens.
21
21
  */
22
- export declare function xaiOAuthLogin(): Promise<XaiAuth>;
22
+ export declare function xaiOAuthLogin({ openInBackground, }?: {
23
+ openInBackground?: boolean | undefined;
24
+ }): Promise<XaiAuth>;
23
25
  /**
24
26
  * Returns a valid XaiAuth, refreshing the access token if it's expired or
25
27
  * about to expire. Calls `save` to persist the refreshed tokens.
@@ -1 +1 @@
1
- {"version":3,"file":"xai-auth.d.ts","sourceRoot":"","sources":["../../src/cli/xai-auth.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAwBvB,MAAM,MAAM,OAAO,GAAG;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,QAAA,MAAM,mBAAmB;;;;;iBAKvB,CAAA;AAEF,KAAK,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AA+DxD,8EAA8E;AAC9E,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,MAAM,GAAE,MAAqC,GAC5C,OAAO,CAKT;AA6BD,wBAAsB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAelF;AAwHD;;;GAGG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,CAoEtD;AAID;;;GAGG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAC5B,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC,CAuB1B"}
1
+ {"version":3,"file":"xai-auth.d.ts","sourceRoot":"","sources":["../../src/cli/xai-auth.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAwBvB,MAAM,MAAM,OAAO,GAAG;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,QAAA,MAAM,mBAAmB;;;;;iBAKvB,CAAA;AAEF,KAAK,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AA+DxD,8EAA8E;AAC9E,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,MAAM,GAAE,MAAqC,GAC5C,OAAO,CAKT;AA6BD,wBAAsB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAelF;AAwHD;;;GAGG;AACH,wBAAsB,aAAa,CAAC,EAClC,gBAAwB,GACzB;;CAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CA8ExB;AAID;;;GAGG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAC5B,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC,CAuB1B"}
@@ -12,7 +12,7 @@
12
12
  // when creating the @ai-sdk/xai provider instance.
13
13
  import { createServer } from 'node:http';
14
14
  import { spinner, log, note } from '@clack/prompts';
15
- import pc from 'picocolors';
15
+ import { colors as pc } from 'goke';
16
16
  import { z } from 'zod';
17
17
  import { openUrlInBrowser } from './open-browser.js';
18
18
  import pkg from '../../package.json' with { type: 'json' };
@@ -236,7 +236,7 @@ function waitForOAuthCallback(verifier, state) {
236
236
  * Run the xAI OAuth browser flow. Opens the system browser for authorization,
237
237
  * waits for the callback, and returns an XaiAuth object with tokens.
238
238
  */
239
- export async function xaiOAuthLogin() {
239
+ export async function xaiOAuthLogin({ openInBackground = false, } = {}) {
240
240
  const s = spinner();
241
241
  s.start('Starting xAI OAuth server...');
242
242
  let server;
@@ -273,7 +273,17 @@ export async function xaiOAuthLogin() {
273
273
  // the loopback server almost instantly. Registering after openUrlInBrowser
274
274
  // creates a race where the callback arrives before pendingOAuth exists.
275
275
  const callbackPromise = waitForOAuthCallback(pkce.verifier, state);
276
- await openUrlInBrowser(authUrl);
276
+ const opened = openUrlInBrowser(authUrl, {
277
+ allowNonInteractive: openInBackground,
278
+ });
279
+ if (openInBackground && !opened) {
280
+ const error = new Error('Could not open a browser for xAI authorization');
281
+ pendingOAuth?.reject(error);
282
+ pendingOAuth = undefined;
283
+ server.close();
284
+ await callbackPromise;
285
+ throw error;
286
+ }
277
287
  s.start('Waiting for authorization...');
278
288
  try {
279
289
  const tokens = await callbackPromise;
@@ -30,6 +30,11 @@ export interface AngledScreenProps {
30
30
  fog?: number;
31
31
  /** Film grain intensity. Default 0.02. */
32
32
  grainIntensity?: number;
33
+ /** Chromatic aberration strength. RGB channels separate along the radial
34
+ * axis from frame center, scaled up by local blur so fringing reads
35
+ * strongest in the bokeh (like real lens CA on the reference photos).
36
+ * 0 = off. Default 0.45. */
37
+ chromaticAberration?: number;
33
38
  /** Background color behind the tilted screen. Default '#000000'. */
34
39
  backgroundColor?: string;
35
40
  /** Width of the inner content wrapper. Default '80%'. */
@@ -1 +1 @@
1
- {"version":3,"file":"angled-screen-shader.d.ts","sourceRoot":"","sources":["../../src/vite/angled-screen-shader.tsx"],"names":[],"mappings":"AAyEA,OAAO,EAAwC,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAKhG,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,SAAS,CAAA;IAGnB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAA;IAGnB,mDAAmD;IACnD,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;2EAEuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;mFAC+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;uEAEmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAA;IAGd,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0CAA0C;IAC1C,cAAc,CAAC,EAAE,MAAM,CAAA;IAEvB,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,CAAA;IAGxB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAExB,uEAAuE;IACvE,KAAK,CAAC,EAAE,OAAO,CAAA;IAEf,oDAAoD;IACpD,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAwND,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BAuWpD"}
1
+ {"version":3,"file":"angled-screen-shader.d.ts","sourceRoot":"","sources":["../../src/vite/angled-screen-shader.tsx"],"names":[],"mappings":"AA4EA,OAAO,EAAwC,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAKhG,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,SAAS,CAAA;IAGnB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAA;IAGnB,mDAAmD;IACnD,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;2EAEuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;mFAC+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;uEAEmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAA;IAGd,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0CAA0C;IAC1C,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;iCAG6B;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAE5B,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,CAAA;IAGxB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAExB,uEAAuE;IACvE,KAAK,CAAC,EAAE,OAAO,CAAA;IAEf,oDAAoD;IACpD,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAwPD,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BA8WpD"}
@@ -11,7 +11,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  * intersection → plane-local UV → sample DOM texture
12
12
  *
13
13
  * The intersection distance gives true per-pixel depth, which drives:
14
- * - bokeh blur (poisson-disc, radius grows with distance from focus)
14
+ * - bokeh blur (sparse 6-blade hex iris, radius grows with distance from
15
+ * focus — fewer taps so the aperture shape reads as hard disks instead
16
+ * of smoothing into a gaussian)
17
+ * - chromatic aberration (radial R/B split, stronger in the bokeh)
15
18
  * - fog toward backgroundColor (like the original Three.js plugin's
16
19
  * scene fog, unframer-private/plugin-angled-screen BokehPass setup)
17
20
  * - film grain (from the plugin's filmGrainShader, u_time-seeded)
@@ -94,6 +97,7 @@ uniform float u_focusDist; // focus distance in px along the ray
94
97
  uniform float u_aspect; // width / height, for circular bokeh
95
98
  uniform float u_fog;
96
99
  uniform float u_grain;
100
+ uniform float u_ca; // chromatic aberration strength
97
101
  uniform vec4 u_background;
98
102
  uniform float u_debug;
99
103
  uniform float u_time;
@@ -105,21 +109,26 @@ float rand(vec2 co) {
105
109
  return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
106
110
  }
107
111
 
108
- // Three.js BokehShader ring kernel (Martins Upitis lens blur):
109
- // 16 taps at radius 1.0, 8 at 0.9, 8 at 0.7, 8 at 0.4, plus center = 41.
110
- const vec2 RING1[16] = vec2[](
111
- vec2(0.0, 0.4), vec2(0.15, 0.37), vec2(0.29, 0.29), vec2(-0.37, 0.15),
112
- vec2(0.40, 0.0), vec2(0.37, -0.15), vec2(0.29, -0.29), vec2(-0.15, -0.37),
113
- vec2(0.0, -0.4), vec2(-0.15, 0.37), vec2(-0.29, 0.29), vec2(0.37, 0.15),
114
- vec2(-0.4, 0.0), vec2(-0.37, -0.15), vec2(-0.29, -0.29), vec2(0.15, -0.37)
112
+ // Sparse 6-blade hexagonal iris. Far fewer taps than the 41-sample Martins
113
+ // Upitis kernel so the aperture SHAPE reads as discrete hex disks instead of
114
+ // smoothing into a gaussian. Outer ring is weighted heavier so bright points
115
+ // form hard-edged bokeh blobs (the look of real lens DOF on the refs).
116
+ // Unit circle coords at radius 1.0 / 0.55.
117
+ const vec2 HEX_OUTER[6] = vec2[](
118
+ vec2( 1.0000, 0.0000),
119
+ vec2( 0.5000, 0.8660),
120
+ vec2(-0.5000, 0.8660),
121
+ vec2(-1.0000, 0.0000),
122
+ vec2(-0.5000, -0.8660),
123
+ vec2( 0.5000, -0.8660)
115
124
  );
116
- const vec2 RING9[8] = vec2[](
117
- vec2(0.15, 0.37), vec2(-0.37, 0.15), vec2(0.37, -0.15), vec2(-0.15, -0.37),
118
- vec2(-0.15, 0.37), vec2(0.37, 0.15), vec2(-0.37, -0.15), vec2(0.15, -0.37)
119
- );
120
- const vec2 RING7[8] = vec2[](
121
- vec2(0.29, 0.29), vec2(0.40, 0.0), vec2(0.29, -0.29), vec2(0.0, -0.4),
122
- vec2(-0.29, 0.29), vec2(-0.4, 0.0), vec2(-0.29, -0.29), vec2(0.0, 0.4)
125
+ const vec2 HEX_MID[6] = vec2[](
126
+ vec2( 0.5500, 0.0000),
127
+ vec2( 0.2750, 0.4763),
128
+ vec2(-0.2750, 0.4763),
129
+ vec2(-0.5500, 0.0000),
130
+ vec2(-0.2750, -0.4763),
131
+ vec2( 0.2750, -0.4763)
123
132
  );
124
133
 
125
134
  // Intersect the camera ray through screen uv suv with the tilted plane.
@@ -154,6 +163,17 @@ vec4 sampleScene(vec2 suv) {
154
163
  return mix(u_background, c, coverage);
155
164
  }
156
165
 
166
+ // Per-channel sample with chromatic aberration: R and B pull apart along
167
+ // the radial axis from frame center (classic lens CA purple/cyan fringing).
168
+ vec4 sampleSceneCA(vec2 suv, vec2 caOffset) {
169
+ float r = sampleScene(suv + caOffset).r;
170
+ float g = sampleScene(suv).g;
171
+ float b = sampleScene(suv - caOffset).b;
172
+ // Coverage/alpha from the green (unshifted) sample keeps silhouette stable.
173
+ vec4 mid = sampleScene(suv);
174
+ return vec4(r, g, b, mid.a);
175
+ }
176
+
157
177
  void main() {
158
178
  vec4 bg = u_background;
159
179
 
@@ -178,27 +198,41 @@ void main() {
178
198
  // stays tack sharp. Background pixels (no hit) count as far away, so the
179
199
  // plane silhouette melts softly where it is out of focus.
180
200
  float factor = hit ? max(t - u_focusDist, 0.0) / u_perspective : 1.0;
181
- vec2 dofblur = vec2(clamp(factor * u_aperture, 0.0, u_maxblur)) * u_bokehEnabled;
201
+ float blurAmt = clamp(factor * u_aperture, 0.0, u_maxblur) * u_bokehEnabled;
202
+ vec2 dofblur = vec2(blurAmt);
182
203
 
183
204
  vec2 aspectcorrect = vec2(1.0, u_aspect);
184
- vec2 dofblur9 = dofblur * 0.9;
185
- vec2 dofblur7 = dofblur * 0.7;
186
- vec2 dofblur4 = dofblur * 0.4;
187
205
 
188
- vec4 col = sampleScene(v_uv);
189
- for (int i = 0; i < 16; i++) {
190
- col += sampleScene(v_uv + RING1[i] * aspectcorrect * dofblur);
191
- }
192
- for (int i = 0; i < 8; i++) {
193
- col += sampleScene(v_uv + RING9[i] * aspectcorrect * dofblur9);
194
- }
195
- for (int i = 0; i < 8; i++) {
196
- col += sampleScene(v_uv + RING7[i] * aspectcorrect * dofblur7);
206
+ // Radial CA direction from frame center. Strength grows with local blur so
207
+ // sharp regions stay clean and only the bokeh fringes with purple/cyan.
208
+ vec2 fromCenter = v_uv - 0.5;
209
+ float radial = length(fromCenter);
210
+ vec2 caDir = radial > 1e-5 ? fromCenter / radial : vec2(1.0, 0.0);
211
+ // Base fringe + blur-scaled fringe. u_ca ~0.45 reads like a real lens on
212
+ // the reference photos without turning the whole frame into a prism.
213
+ float caAmt = u_ca * (0.0015 + blurAmt * 0.55);
214
+ vec2 caOffset = caDir * caAmt * aspectcorrect;
215
+
216
+ // Sparse hex gather. Outer ring weight 2.0 so the 6-blade iris is visible
217
+ // as hard bokeh disks; mid ring 1.0 fills the disk; center 1.0 anchors.
218
+ // Total weight = 6*2 + 6*1 + 1 = 19.
219
+ const float W_OUTER = 2.0;
220
+ const float W_MID = 1.0;
221
+ const float W_CENTER = 1.0;
222
+ vec4 col = sampleSceneCA(v_uv, caOffset) * W_CENTER;
223
+ for (int i = 0; i < 6; i++) {
224
+ col += sampleSceneCA(v_uv + HEX_OUTER[i] * aspectcorrect * dofblur, caOffset) * W_OUTER;
197
225
  }
198
- for (int i = 0; i < 8; i++) {
199
- col += sampleScene(v_uv + RING7[i] * aspectcorrect * dofblur4);
226
+ for (int i = 0; i < 6; i++) {
227
+ col += sampleSceneCA(v_uv + HEX_MID[i] * aspectcorrect * dofblur, caOffset) * W_MID;
200
228
  }
201
- col /= 41.0;
229
+ col /= (6.0 * W_OUTER + 6.0 * W_MID + W_CENTER);
230
+
231
+ // Mild highlight bias: bright samples punch through more (classic optical
232
+ // bokeh where speculars form bright disks instead of averaging down).
233
+ // Applied as a soft lift so it doesn't blow whites on already-bright UI.
234
+ float luma = dot(col.rgb, vec3(0.299, 0.587, 0.114));
235
+ col.rgb += col.rgb * blurAmt * 1.8 * smoothstep(0.55, 0.95, luma);
202
236
 
203
237
  // Fog toward background with depth (plugin's THREE.Fog equivalent)
204
238
  col.rgb = mix(col.rgb, bg.rgb, u_fog * depth01 * (hit ? 1.0 : 0.0));
@@ -226,6 +260,7 @@ const UNIFORM_NAMES = [
226
260
  'u_aspect',
227
261
  'u_fog',
228
262
  'u_grain',
263
+ 'u_ca',
229
264
  'u_background',
230
265
  'u_debug',
231
266
  'u_time',
@@ -309,12 +344,18 @@ export function AngledScreen(props) {
309
344
  step: 0.01,
310
345
  },
311
346
  fog: { value: props.fog ?? 0.35, min: 0, max: 1, step: 0.01 },
312
- grainIntensity: { value: props.grainIntensity ?? 0.02, min: 0, max: 0.2, step: 0.005 },
347
+ grainIntensity: { value: props.grainIntensity ?? 0.02, min: 0, max: 0.3, step: 0.005 },
348
+ chromaticAberration: {
349
+ value: props.chromaticAberration ?? 0.45,
350
+ min: 0,
351
+ max: 2,
352
+ step: 0.01,
353
+ },
313
354
  backgroundColor: props.backgroundColor ?? '#000000',
314
355
  debug: props.debug ?? false,
315
356
  });
316
357
  const { perspective, rotateX, rotateY, rotateZ, translateX, translateZ } = tp;
317
- const { aperture, maxBlur, focus, fog, grainIntensity } = tp;
358
+ const { aperture, maxBlur, focus, fog, grainIntensity, chromaticAberration } = tp;
318
359
  const { bokeh, debug, backgroundColor } = tp;
319
360
  const time = frame / fps;
320
361
  const onInit = useCallback(({ canvas }) => {
@@ -473,6 +514,8 @@ export function AngledScreen(props) {
473
514
  gl.uniform1f(locs.u_fog, fog);
474
515
  if (locs.u_grain)
475
516
  gl.uniform1f(locs.u_grain, grainIntensity);
517
+ if (locs.u_ca)
518
+ gl.uniform1f(locs.u_ca, chromaticAberration);
476
519
  if (locs.u_background)
477
520
  gl.uniform4fv(locs.u_background, parseHexColor(backgroundColor));
478
521
  if (locs.u_debug)
@@ -507,7 +550,7 @@ export function AngledScreen(props) {
507
550
  }
508
551
  }, [
509
552
  compW, compH, perspective, rotateX, rotateY, rotateZ, translateX, translateZ,
510
- bokeh, aperture, maxBlur, focus, fog, grainIntensity,
553
+ bokeh, aperture, maxBlur, focus, fog, grainIntensity, chromaticAberration,
511
554
  backgroundColor, debug, time,
512
555
  ]);
513
556
  const supported = typeof document !== 'undefined' && HtmlInCanvas.isSupported();
@@ -1 +1 @@
1
- {"version":3,"file":"generated-media-client.d.ts","sourceRoot":"","sources":["../../src/vite/generated-media-client.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAkC,KAAK,cAAc,EAAkB,MAAM,OAAO,CAAA;AAE3F,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAkB,MAAM,iBAAiB,CAAA;AACnE,OAAO,EAAE,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAE7E,OAAO,EAAE,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,CAAA;AAmDrD,wBAAgB,oBAAoB,CAAC,EACnC,UAAU,EACV,WAAW,EACX,GAAG,IAAI,EACR,EAAE;IACD,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,cAAc,CAAC,OAAO,GAAG,CAAC,+BAM7B;AAED,wBAAgB,oBAAoB,CAAC,EACnC,UAAU,EACV,WAAW,EACX,GAAG,IAAI,EACR,EAAE;IACD,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,cAAc,CAAC,OAAO,KAAK,CAAC,+BAM/B;AAED,wBAAgB,qBAAqB,CAAC,EACpC,UAAU,EACV,WAAW,EACX,GAAG,IAAI,EACR,EAAE;IACD,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,cAAc,CAAC,OAAO,KAAK,CAAC,+BAM/B"}
1
+ {"version":3,"file":"generated-media-client.d.ts","sourceRoot":"","sources":["../../src/vite/generated-media-client.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAkC,KAAK,cAAc,EAAkB,MAAM,OAAO,CAAA;AAE3F,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAkB,MAAM,iBAAiB,CAAA;AACnE,OAAO,EAAE,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAE7E,OAAO,EAAE,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,CAAA;AAwDrD,wBAAgB,oBAAoB,CAAC,EACnC,UAAU,EACV,WAAW,EACX,GAAG,IAAI,EACR,EAAE;IACD,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,cAAc,CAAC,OAAO,GAAG,CAAC,+BAM7B;AAED,wBAAgB,oBAAoB,CAAC,EACnC,UAAU,EACV,WAAW,EACX,GAAG,IAAI,EACR,EAAE;IACD,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,cAAc,CAAC,OAAO,KAAK,CAAC,+BAM/B;AAED,wBAAgB,qBAAqB,CAAC,EACpC,UAAU,EACV,WAAW,EACX,GAAG,IAAI,EACR,EAAE;IACD,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,cAAc,CAAC,OAAO,KAAK,CAAC,+BAM/B"}
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /**
4
4
  * Client wrappers for server-generated media components.
5
5
  *
@@ -60,7 +60,7 @@ function ResolvedVideo({ srcPromise, ...rest }) {
60
60
  }
61
61
  function ResolvedAudio({ srcPromise, ...rest }) {
62
62
  const src = use(srcPromise);
63
- return _jsx(Audio, { src: src, ...rest });
63
+ return (_jsxs(_Fragment, { children: [_jsx("span", { "data-egaki-tts": true, style: { display: 'none' }, "aria-hidden": true }), _jsx(Audio, { src: src, ...rest })] }));
64
64
  }
65
65
  // ---------------------------------------------------------------------------
66
66
  // Exported client wrappers — render fallback while promise is pending
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../../src/vite/vite-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAU,YAAY,EAAE,MAAM,MAAM,CAAA;AA4BhD,MAAM,WAAW,kBAAkB;IACjC;;wEAEoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAiDD,wBAAgB,KAAK,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,YAAY,EAAE,CAoYlE"}
1
+ {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../../src/vite/vite-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAU,YAAY,EAAE,MAAM,MAAM,CAAA;AAgDhD,MAAM,WAAW,kBAAkB;IACjC;;wEAEoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAiDD,wBAAgB,KAAK,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,YAAY,EAAE,CA+elE"}