opencode-skills-collection 4.1.2 → 4.1.3

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 (68) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +9 -1
  2. package/bundled-skills/antigravity-maintainer-batch-release/SKILL.md +15 -1
  3. package/bundled-skills/deep-research-framework/SKILL.md +81 -0
  4. package/bundled-skills/five-axis-code-review/SKILL.md +78 -0
  5. package/bundled-skills/git-commit-message/SKILL.md +76 -0
  6. package/bundled-skills/idea-to-blueprint/LICENSE +21 -0
  7. package/bundled-skills/idea-to-blueprint/SKILL.md +260 -0
  8. package/bundled-skills/idea-to-blueprint/assets/AGENTS.md.template +38 -0
  9. package/bundled-skills/idea-to-blueprint/assets/CLAUDE.md.template +38 -0
  10. package/bundled-skills/idea-to-blueprint/assets/DECISIONS.md.template +14 -0
  11. package/bundled-skills/idea-to-blueprint/assets/PROGRESS.md.template +47 -0
  12. package/bundled-skills/idea-to-blueprint/assets/session-prompt.md +23 -0
  13. package/bundled-skills/idea-to-blueprint/references/architecture-decisions.md +202 -0
  14. package/bundled-skills/idea-to-blueprint/references/blueprint-template.md +259 -0
  15. package/bundled-skills/idea-to-blueprint/references/code-quality-and-review.md +216 -0
  16. package/bundled-skills/idea-to-blueprint/references/design-minimal.md +96 -0
  17. package/bundled-skills/idea-to-blueprint/references/edge-case-catalog.md +239 -0
  18. package/bundled-skills/idea-to-blueprint/references/intake-questions.md +105 -0
  19. package/bundled-skills/idea-to-blueprint/references/research-protocol.md +196 -0
  20. package/bundled-skills/idea-to-blueprint/references/session-protocol.md +133 -0
  21. package/bundled-skills/idea-to-blueprint/references/stories-ac-tests.md +194 -0
  22. package/bundled-skills/idea-to-blueprint/references/ux-writing-guide.md +128 -0
  23. package/bundled-skills/idea-to-blueprint/scripts/lint_blueprint.py +460 -0
  24. package/bundled-skills/liuguang-banlan-ui/SKILL.md +22 -18
  25. package/bundled-skills/liuguang-banlan-ui/assets/starter/launcher.css +2 -2
  26. package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/index.html +5 -4
  27. package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/theme-config.js +3 -10
  28. package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/theme.css +48 -62
  29. package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/index.html +5 -4
  30. package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/theme-config.js +27 -35
  31. package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/theme.css +48 -62
  32. package/bundled-skills/liuguang-banlan-ui/assets/starter/shared/spectral-field.js +135 -148
  33. package/bundled-skills/liuguang-banlan-ui/references/style-contract.md +26 -7
  34. package/bundled-skills/liuguang-banlan-ui/references/verification.md +19 -7
  35. package/bundled-skills/liuguang-banlan-ui/scripts/measure_preview.py +78 -47
  36. package/bundled-skills/liuguang-banlan-ui/scripts/serve_preview.py +35 -0
  37. package/bundled-skills/liuguang-banlan-ui/scripts/validate_manifest.py +34 -3
  38. package/bundled-skills/liuguang-banlan-ui/tests/test_measure_preview.py +119 -0
  39. package/bundled-skills/liuguang-banlan-ui/tests/test_serve_preview.py +38 -0
  40. package/bundled-skills/liuguang-banlan-ui/tests/test_validate_manifest.py +73 -0
  41. package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package-lock.json +12 -12
  42. package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package.json +1 -1
  43. package/bundled-skills/meeting-notes/SKILL.md +80 -0
  44. package/bundled-skills/repo-foundation/SKILL.md +166 -0
  45. package/bundled-skills/repo-foundation/references/LICENSE.md +21 -0
  46. package/bundled-skills/repo-foundation/references/bootstrap.md +63 -0
  47. package/bundled-skills/repo-foundation/references/companion.md +9 -0
  48. package/bundled-skills/repo-foundation/references/continuity.md +50 -0
  49. package/bundled-skills/repo-foundation/references/evolution.md +60 -0
  50. package/bundled-skills/repo-foundation/references/migration-examples.md +53 -0
  51. package/bundled-skills/repo-foundation/references/shared-contracts.md +48 -0
  52. package/bundled-skills/repo-foundation/references/verification.md +50 -0
  53. package/bundled-skills/repo-native-refactor/SKILL.md +225 -0
  54. package/bundled-skills/repo-native-refactor/references/LICENSE.md +21 -0
  55. package/bundled-skills/repo-native-refactor/references/deterministic-tooling.md +101 -0
  56. package/bundled-skills/repo-native-refactor/references/error-reliability.md +117 -0
  57. package/bundled-skills/repo-native-refactor/references/finding-taxonomy.md +181 -0
  58. package/bundled-skills/repo-native-refactor/references/refactor-examples.md +77 -0
  59. package/bundled-skills/repo-native-refactor/references/repository-prose.md +157 -0
  60. package/bundled-skills/repo-native-refactor/references/repository-rehabilitation.md +94 -0
  61. package/bundled-skills/repo-native-refactor/references/semantic-risk.md +187 -0
  62. package/bundled-skills/repo-native-refactor/references/shared-contracts.md +48 -0
  63. package/bundled-skills/repo-native-refactor/references/testing-integrity.md +103 -0
  64. package/bundled-skills/tech-writing-proofread/SKILL.md +78 -0
  65. package/bundled-skills/typescript-expert/scripts/ts_diagnostic.py +40 -19
  66. package/bundled-skills/youtube-summarizer/SKILL.md +3 -3
  67. package/package.json +1 -1
  68. package/skills_index.json +254 -0
@@ -1,7 +1,11 @@
1
1
  (function () {
2
2
  "use strict";
3
3
 
4
- const MAX_COLORS = 12;
4
+ // The field drifts slowly. Spacing frames so each advances field time by at most
5
+ // MAX_FIELD_STEP keeps the change per frame below a visible step at less GPU cost.
6
+ const MIN_FRAME_INTERVAL = 50;
7
+ const MAX_FRAME_INTERVAL = 250;
8
+ const MAX_FIELD_STEP = 0.003;
5
9
 
6
10
  const VERTEX_SHADER = `
7
11
  attribute vec2 aPosition;
@@ -23,12 +27,12 @@
23
27
  uniform float uWarp;
24
28
  uniform float uDither;
25
29
  uniform float uLuminanceCap;
30
+ uniform float uLevels;
26
31
  uniform vec3 uBase;
27
- uniform vec3 uColors[12];
28
- uniform float uStrengths[12];
29
- uniform float uFieldScales[12];
30
- uniform vec2 uPhases[12];
31
- uniform float uColorCount;
32
+ uniform vec3 uColors[6];
33
+ uniform float uStrengths[6];
34
+ uniform float uFieldScales[6];
35
+ uniform vec2 uPhases[6];
32
36
 
33
37
  float hash21(vec2 p) {
34
38
  p = fract(p * vec2(123.34, 456.21));
@@ -52,8 +56,8 @@
52
56
  float amplitude = 0.52;
53
57
  mat2 rotation = mat2(0.80, 0.60, -0.60, 0.80);
54
58
  for (int i = 0; i < 5; i++) {
55
- float enabled = 1.0 - step(uOctaves, float(i) + 0.5);
56
- value += amplitude * noise(p) * enabled;
59
+ if (float(i) + 0.5 >= uOctaves) break;
60
+ value += amplitude * noise(p);
57
61
  p = rotation * p * 2.03 + vec2(13.1, 7.7);
58
62
  amplitude *= 0.48;
59
63
  }
@@ -77,6 +81,19 @@
77
81
  return c / (1.0 + max(c - 1.0, 0.0));
78
82
  }
79
83
 
84
+ vec3 linearToSrgb(vec3 c) {
85
+ vec3 high = 1.055 * pow(max(c, vec3(0.0031308)), vec3(1.0 / 2.4)) - 0.055;
86
+ return mix(c * 12.92, high, step(vec3(0.0031308), c));
87
+ }
88
+
89
+ // Not hash21: offsetting pixel coordinates by the seed pushes them past
90
+ // float precision, and the dither turns into visible stripes.
91
+ float ditherHash(vec2 p) {
92
+ vec3 p3 = fract(vec3(p.xyx) * 0.1031);
93
+ p3 += dot(p3, p3.yzx + 33.33);
94
+ return fract((p3.x + p3.y) * p3.z);
95
+ }
96
+
80
97
  void main() {
81
98
  vec2 uv = gl_FragCoord.xy / uResolution.xy;
82
99
  vec2 p = (uv - 0.5) * vec2(uResolution.x / uResolution.y, 1.0);
@@ -104,7 +121,12 @@
104
121
  + dot(warped, vec2(0.72, -0.49))
105
122
  + uSeed * 0.000017
106
123
  );
107
- for (int i = 0; i < 12; i++) {
124
+ for (int i = 0; i < 6; i++) {
125
+ float hueStop = float(i) / 6.0;
126
+ float hueDistance = abs(spectralFlow - hueStop);
127
+ hueDistance = min(hueDistance, 1.0 - hueDistance);
128
+ float hueBand = 1.0 - smoothstep(0.035, 0.205, hueDistance);
129
+ if (hueBand <= 0.0) continue;
108
130
  float field = colorField(
109
131
  warped + q * (0.21 + float(i) * 0.025),
110
132
  uPhases[i],
@@ -112,16 +134,11 @@
112
134
  uFieldScales[i]
113
135
  );
114
136
  float softBand = smoothstep(0.22, 0.78, field);
115
- float active = step(float(i) + 0.5, uColorCount);
116
- float hueStop = float(i) / max(uColorCount, 1.0);
117
- float hueDistance = abs(spectralFlow - hueStop);
118
- hueDistance = min(hueDistance, 1.0 - hueDistance);
119
- float hueBand = 1.0 - smoothstep(0.035, 0.205, hueDistance);
120
137
  float shapedBand = pow(hueBand, 2.2) * (0.64 + softBand * 0.36);
121
- float weight = shapedBand * sqrt(max(uStrengths[i], 0.0)) * active;
138
+ float weight = shapedBand * sqrt(max(uStrengths[i], 0.0));
122
139
  colorSum += uColors[i] * weight;
123
140
  weightSum += weight;
124
- energySum += shapedBand * uStrengths[i] * active;
141
+ energySum += shapedBand * uStrengths[i];
125
142
  strongestBand = max(strongestBand, shapedBand);
126
143
  }
127
144
 
@@ -145,11 +162,13 @@
145
162
  }
146
163
  }
147
164
 
148
- float dither = hash21(floor(gl_FragCoord.xy) + vec2(uSeed, -uSeed)) - 0.5;
149
- color += dither * (uDither / 255.0);
150
- color = toneMap(max(color, 0.0));
151
- color = pow(color, vec3(1.0 / 2.2));
152
- gl_FragColor = vec4(color, 1.0);
165
+ vec2 pixel = floor(gl_FragCoord.xy);
166
+ float triangle = ditherHash(pixel) + ditherHash(pixel + vec2(47.0, 113.0)) - 1.0;
167
+ vec3 encoded = linearToSrgb(toneMap(max(color, 0.0)));
168
+ // Quantize here so the result does not depend on whether the GPU rounds or
169
+ // truncates when it stores floats; +0.25 lands inside the chosen code either way.
170
+ vec3 code = clamp(floor(encoded * uLevels + 0.5 + triangle * uDither), 0.0, uLevels);
171
+ gl_FragColor = vec4((code + 0.25) / uLevels, 1.0);
153
172
  }
154
173
  `;
155
174
 
@@ -170,6 +189,8 @@
170
189
  ];
171
190
  }
172
191
 
192
+ const MAX_COLORS = 12;
193
+
173
194
  function clamp01(value) {
174
195
  const number = Number(value);
175
196
  return Number.isFinite(number) ? Math.min(1, Math.max(0, number)) : 0;
@@ -243,89 +264,72 @@
243
264
  this.applyFallbackConfig(config);
244
265
  this.available = false;
245
266
  this.error = null;
246
- this.motionPreference = window.matchMedia("(prefers-reduced-motion: reduce)");
247
- this.motionEnabled = !this.motionPreference.matches;
267
+ this.motionEnabled = !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
248
268
  this.visible = !document.hidden;
249
- this.startTime = performance.now();
250
- this.phaseTime = null;
269
+ this.fieldTime = config.field.staticTime;
270
+ this.lastTick = null;
251
271
  this.lastFrame = 0;
252
272
  this.frameHandle = 0;
253
273
  try {
254
- this.gl = canvas.getContext("webgl", {
255
- alpha: false,
256
- antialias: false,
257
- depth: false,
258
- stencil: false,
259
- powerPreference: "low-power",
260
- preserveDrawingBuffer: true
261
- });
262
- if (!this.gl) throw new Error("WebGL context unavailable");
263
-
264
- this.program = createProgram(this.gl);
265
- this.gl.useProgram(this.program);
266
- const buffer = this.gl.createBuffer();
267
- if (!buffer) throw new Error("WebGL buffer creation failed");
268
- this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
269
- this.gl.bufferData(
270
- this.gl.ARRAY_BUFFER,
271
- new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
272
- this.gl.STATIC_DRAW
273
- );
274
- const position = this.gl.getAttribLocation(this.program, "aPosition");
275
- if (position < 0) throw new Error("WebGL position attribute unavailable");
276
- this.gl.enableVertexAttribArray(position);
277
- this.gl.vertexAttribPointer(position, 2, this.gl.FLOAT, false, 0, 0);
278
-
279
- this.locations = {
280
- resolution: uniform(this.gl, this.program, "uResolution"),
281
- time: uniform(this.gl, this.program, "uTime"),
282
- seed: uniform(this.gl, this.program, "uSeed"),
283
- mode: uniform(this.gl, this.program, "uMode"),
284
- overall: uniform(this.gl, this.program, "uOverall"),
285
- colorCount: uniform(this.gl, this.program, "uColorCount"),
286
- scale: uniform(this.gl, this.program, "uScale"),
287
- octaves: uniform(this.gl, this.program, "uOctaves"),
288
- warp: uniform(this.gl, this.program, "uWarp"),
289
- dither: uniform(this.gl, this.program, "uDither"),
290
- luminanceCap: uniform(this.gl, this.program, "uLuminanceCap"),
291
- base: uniform(this.gl, this.program, "uBase"),
292
- colors: uniform(this.gl, this.program, "uColors[0]"),
293
- strengths: uniform(this.gl, this.program, "uStrengths[0]"),
294
- fieldScales: uniform(this.gl, this.program, "uFieldScales[0]"),
295
- phases: uniform(this.gl, this.program, "uPhases[0]")
296
- };
297
-
298
- this.onResize = () => this.resize();
299
- this.onVisibility = () => {
300
- const now = performance.now();
301
- if (document.hidden) {
302
- this.capturePhase(now);
303
- this.visible = false;
304
- if (this.frameHandle) {
305
- cancelAnimationFrame(this.frameHandle);
306
- this.frameHandle = 0;
307
- }
308
- return;
309
- }
310
-
311
- this.visible = true;
312
- if (this.motionEnabled) {
313
- const speed = this.config.field.motionSpeed;
314
- this.startTime = speed > 0 && this.phaseTime !== null
315
- ? now - (this.phaseTime / speed) * 1000
316
- : now;
317
- }
318
- this.render(now, true);
319
- this.schedule();
320
- };
321
- this.onMotionPreference = (event) => this.setMotion(!event.matches);
322
- window.addEventListener("resize", this.onResize, { passive: true });
323
- document.addEventListener("visibilitychange", this.onVisibility);
324
- this.motionPreference.addEventListener("change", this.onMotionPreference);
325
- this.available = true;
326
- this.resize();
327
- this.uploadConfig();
328
- this.schedule();
274
+ this.gl = canvas.getContext("webgl", {
275
+ alpha: false,
276
+ antialias: false,
277
+ depth: false,
278
+ stencil: false,
279
+ powerPreference: "low-power",
280
+ preserveDrawingBuffer: true
281
+ });
282
+
283
+ if (!this.gl) throw new Error("WebGL context unavailable");
284
+
285
+ this.available = true;
286
+ this.levels = Math.pow(2, this.gl.getParameter(this.gl.RED_BITS) || 8) - 1;
287
+ this.program = createProgram(this.gl);
288
+ this.gl.useProgram(this.program);
289
+ const buffer = this.gl.createBuffer();
290
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
291
+ this.gl.bufferData(
292
+ this.gl.ARRAY_BUFFER,
293
+ new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
294
+ this.gl.STATIC_DRAW
295
+ );
296
+ const position = this.gl.getAttribLocation(this.program, "aPosition");
297
+ this.gl.enableVertexAttribArray(position);
298
+ this.gl.vertexAttribPointer(position, 2, this.gl.FLOAT, false, 0, 0);
299
+
300
+ this.locations = {
301
+ resolution: uniform(this.gl, this.program, "uResolution"),
302
+ time: uniform(this.gl, this.program, "uTime"),
303
+ seed: uniform(this.gl, this.program, "uSeed"),
304
+ mode: uniform(this.gl, this.program, "uMode"),
305
+ overall: uniform(this.gl, this.program, "uOverall"),
306
+ scale: uniform(this.gl, this.program, "uScale"),
307
+ octaves: uniform(this.gl, this.program, "uOctaves"),
308
+ warp: uniform(this.gl, this.program, "uWarp"),
309
+ dither: uniform(this.gl, this.program, "uDither"),
310
+ luminanceCap: uniform(this.gl, this.program, "uLuminanceCap"),
311
+ levels: uniform(this.gl, this.program, "uLevels"),
312
+ base: uniform(this.gl, this.program, "uBase"),
313
+ colors: uniform(this.gl, this.program, "uColors[0]"),
314
+ strengths: uniform(this.gl, this.program, "uStrengths[0]"),
315
+ fieldScales: uniform(this.gl, this.program, "uFieldScales[0]"),
316
+ phases: uniform(this.gl, this.program, "uPhases[0]")
317
+ };
318
+
319
+ // Resizing clears the drawing buffer; a paused field has no next frame to repaint it.
320
+ this.onResize = () => {
321
+ if (this.resize()) this.render(performance.now(), true);
322
+ };
323
+ this.onVisibility = () => {
324
+ this.visible = !document.hidden;
325
+ this.lastTick = null;
326
+ if (this.visible) this.schedule();
327
+ };
328
+ window.addEventListener("resize", this.onResize, { passive: true });
329
+ document.addEventListener("visibilitychange", this.onVisibility);
330
+ this.resize();
331
+ this.uploadConfig();
332
+ this.schedule();
329
333
  } catch (error) {
330
334
  this.error = error instanceof Error ? error.message : String(error);
331
335
  this.available = false;
@@ -334,36 +338,28 @@
334
338
  }
335
339
  }
336
340
 
341
+ applyFallbackConfig(config) {
342
+ document.documentElement.style.setProperty("--field-fallback-background", fallbackBackground(config));
343
+ }
344
+
337
345
  resize() {
338
- if (!this.available) return;
346
+ if (!this.available) return false;
339
347
  const pixelRatio = Math.min(window.devicePixelRatio || 1, 1.5);
340
348
  const width = Math.max(1, Math.round(this.canvas.clientWidth * pixelRatio));
341
349
  const height = Math.max(1, Math.round(this.canvas.clientHeight * pixelRatio));
342
- if (this.canvas.width !== width || this.canvas.height !== height) {
343
- this.canvas.width = width;
344
- this.canvas.height = height;
345
- this.gl.viewport(0, 0, width, height);
346
- }
347
- }
348
-
349
- applyFallbackConfig(config) {
350
- document.documentElement.style.setProperty("--field-fallback-background", fallbackBackground(config));
350
+ if (this.canvas.width === width && this.canvas.height === height) return false;
351
+ this.canvas.width = width;
352
+ this.canvas.height = height;
353
+ this.gl.viewport(0, 0, width, height);
354
+ return true;
351
355
  }
352
356
 
353
357
  uploadConfig() {
354
358
  if (!this.available) return;
355
359
  const gl = this.gl;
356
360
  const config = this.config;
357
- const sourceColors = Array.isArray(config.colors) ? config.colors.slice(0, MAX_COLORS) : [];
358
- const fallbackColor = sourceColors[0] || {
359
- oklch: config.base.oklch,
360
- intensity: 0,
361
- peakOpacity: 0,
362
- fieldScale: config.field.scale,
363
- phase: [0, 0]
364
- };
365
- const colors = sourceColors.slice();
366
- while (colors.length < MAX_COLORS) colors.push(fallbackColor);
361
+ const colors = config.colors.slice(0, 6);
362
+ while (colors.length < 6) colors.push(colors[colors.length - 1]);
367
363
  const colorValues = colors.flatMap((entry) => oklchToLinearRgb(entry.oklch));
368
364
  const strengthValues = colors.map((entry) => entry.intensity * entry.peakOpacity * 3.0);
369
365
  const fieldScaleValues = colors.map((entry) => entry.fieldScale);
@@ -372,12 +368,12 @@
372
368
  gl.uniform1f(this.locations.seed, config.seed);
373
369
  gl.uniform1f(this.locations.mode, config.mode === "obsidian" ? 1 : 0);
374
370
  gl.uniform1f(this.locations.overall, config.overallColorIntensity);
375
- gl.uniform1f(this.locations.colorCount, sourceColors.length);
376
371
  gl.uniform1f(this.locations.scale, config.field.scale);
377
372
  gl.uniform1f(this.locations.octaves, config.field.octaves);
378
373
  gl.uniform1f(this.locations.warp, config.field.warpStrength);
379
374
  gl.uniform1f(this.locations.dither, config.field.ditherStrength);
380
- gl.uniform1f(this.locations.luminanceCap, config.field.luminanceCap);
375
+ gl.uniform1f(this.locations.luminanceCap, config.field.luminanceCap ?? 1);
376
+ gl.uniform1f(this.locations.levels, this.levels);
381
377
  gl.uniform3fv(this.locations.base, new Float32Array(oklchToLinearRgb(config.base.oklch)));
382
378
  gl.uniform3fv(this.locations.colors, new Float32Array(colorValues));
383
379
  gl.uniform1fv(this.locations.strengths, new Float32Array(strengthValues));
@@ -385,40 +381,20 @@
385
381
  gl.uniform2fv(this.locations.phases, new Float32Array(phaseValues));
386
382
  }
387
383
 
388
- capturePhase(now) {
389
- if (this.motionEnabled) {
390
- this.phaseTime = ((now - this.startTime) / 1000) * this.config.field.motionSpeed;
391
- } else if (this.phaseTime === null) {
392
- this.phaseTime = this.config.field.staticTime;
393
- }
394
- return this.phaseTime;
395
- }
396
-
397
384
  updateConfig(config) {
398
- const now = performance.now();
399
- this.capturePhase(now);
400
385
  this.config = config;
401
386
  this.applyFallbackConfig(config);
402
387
  this.uploadConfig();
403
- this.render(now, true);
388
+ this.render(performance.now(), true);
404
389
  }
405
390
 
406
391
  setMotion(enabled) {
407
- if (enabled === this.motionEnabled) {
408
- const now = performance.now();
409
- if (!enabled) this.capturePhase(now);
410
- this.render(now, true);
411
- return;
412
- }
413
- const now = performance.now();
414
- const phase = this.capturePhase(now);
415
392
  this.motionEnabled = enabled;
393
+ this.lastTick = null;
416
394
  if (enabled) {
417
- const speed = this.config.field.motionSpeed;
418
- this.startTime = speed > 0 ? now - (phase / speed) * 1000 : now;
419
395
  this.schedule();
420
396
  } else {
421
- this.render(now, true);
397
+ this.render(performance.now(), true);
422
398
  }
423
399
  }
424
400
 
@@ -431,17 +407,28 @@
431
407
  });
432
408
  }
433
409
 
410
+ frameInterval() {
411
+ const speed = this.config.field.motionSpeed;
412
+ if (!(speed > 0)) return MAX_FRAME_INTERVAL;
413
+ return Math.min(MAX_FRAME_INTERVAL, Math.max(MIN_FRAME_INTERVAL, (MAX_FIELD_STEP / speed) * 1000));
414
+ }
415
+
434
416
  render(now, force) {
435
417
  if (!this.available) return;
436
- if (!force && now - this.lastFrame < 50) {
418
+ if (!force && now - this.lastFrame < this.frameInterval()) {
437
419
  this.schedule();
438
420
  return;
439
421
  }
440
422
  this.lastFrame = now;
441
423
  this.resize();
442
- const elapsed = this.capturePhase(now);
424
+ if (this.motionEnabled) {
425
+ if (this.lastTick !== null) {
426
+ this.fieldTime += (Math.max(0, now - this.lastTick) / 1000) * this.config.field.motionSpeed;
427
+ }
428
+ this.lastTick = now;
429
+ }
443
430
  this.gl.uniform2f(this.locations.resolution, this.canvas.width, this.canvas.height);
444
- this.gl.uniform1f(this.locations.time, elapsed);
431
+ this.gl.uniform1f(this.locations.time, this.fieldTime);
445
432
  this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
446
433
  }
447
434
  }
@@ -2,10 +2,11 @@
2
2
 
3
3
  ## 共通原则
4
4
 
5
- - 把色场当作环境现象,不把它当成组件皮肤或装饰贴纸。
5
+ - 把色场当作环境现象,不把它当成组件皮肤或装饰贴纸。logo、头像、分隔线等界面部件保持中性,不用调色板渐变上色。
6
6
  - 先建立稳定的信息层级:导航、列表/队列、详情、元数据、状态带。
7
7
  - 让颜色跨越整个场景,但保持文字、边界、按钮和选择态可读。
8
8
  - 用 OKLCH 编写颜色;为降级路径保留 sRGB 值。
9
+ - 颜色数组按色相顺序排列。渲染器把颜色依次排在一条首尾相接的色相轴上,相邻两色在交界处混合;近互补色相邻时,交界处色度互相抵消,只剩变暗。
9
10
  - 使用连续的 fBm/domain-warp、流带或方向性流域。禁止明显的圆形光斑、中心聚光和硬彩虹条。
10
11
  - 维持静、薄、净、密、活的工作台气质:表面轻、信息密、状态清楚、背景有生命但不抢语义。
11
12
 
@@ -13,6 +14,8 @@
13
14
 
14
15
  定位:白色是第一视觉,色彩像被光照亮的珠光薄层,能看到变化但不破坏“白”。
15
16
 
17
+ 下表按 starter 工作台标定:顶栏、侧栏和工作区等面板以 66%–92% 的不透明度盖在色场上。色场直接露出时,按下一节调整。
18
+
16
19
  | 参数 | 建议范围 | 作用 |
17
20
  |---|---:|---|
18
21
  | `overallColorIntensity` | 0.60–0.90 | 控制全局彩色预算,默认约 0.82 |
@@ -22,9 +25,23 @@
22
25
  | 颜色 `oklch.c` | 0.030–0.065 | 足够识别,但避免糖果霓虹 |
23
26
  | `peakOpacity` | 0.055–0.110 | 决定局部峰值,先调它再调总体 |
24
27
  | `field.warpStrength` | 0.22–0.42 | 让颜色互相流动而非分块 |
25
- | `field.luminanceCap` | 0.94–1.00 | 白场通常不需强行压暗 |
26
28
 
27
- 建议使用玫瑰、海玻璃青、丁香、珠杏、薄荷和远空蓝等低饱和色相。至少让三种色相在同一截图中可辨,不要让整张图退化为普通暖白、灰雾或单一冷白。
29
+ 白版不使用 `field.luminanceCap`。建议使用玫瑰、海玻璃青、丁香、珠杏、薄荷和远空蓝等低饱和色相。至少让三种色相在同一截图中可辨,不要让整张图退化为普通暖白、灰雾或单一冷白。
30
+
31
+ ### 色场直接露出
32
+
33
+ 个人主页、文章页这类页面没有面板盖住色场,按上表的强度会显得花,也会把整页压暗。下表是一个真实站点在一位用户屏幕上的判断。`tint.p50` 是 `measure_preview.py` 测得的相对底色色偏中位数(OKLab),用用户当时看到的那一版渲染器重新测得;换站点或屏幕需要重新确认。
34
+
35
+ | 用户判断 | `tint.p50` | 配置 |
36
+ |---|---:|---|
37
+ | 看不出色场 | 约 0.004 | overall 0.30 |
38
+ | 合适:仔细看能看出,不发灰 | 约 0.0056 | overall 0.45 |
39
+ | 显得花且乱 | 约 0.009 | 上表默认参数,色相交错排列 |
40
+ | 阅读页,接近全白 | 约 0.0017 | overall 0.15 |
41
+
42
+ 那个站点的其他参数:底色 `l` 0.988,颜色按色相排序,`field.scale` 0.8,`octaves` 3,`warpStrength` 0.26。当前渲染器下,这组参数在 overall 0.45 时测得约 0.0052,0.55 时约 0.0063。
43
+
44
+ 白版的颜色都比底色暗,着色的同时一定变暗。同一组颜色下,变暗和色偏按同一比例随强度变化,`shiftToTint` 基本不变,所以降低强度只会让色场更淡,不会让它更干净。要减少发灰,先按色相排序并检查 `grayRatio`。把颜色亮度调得更接近底色也能减少变暗,但越接近白色,sRGB 能容纳的色度越小;这一点还没有在当前渲染器上实测。
28
45
 
29
46
  ## 五彩斑斓黑 / `obsidian`
30
47
 
@@ -45,9 +62,11 @@
45
62
 
46
63
  ## 参数解释
47
64
 
48
- - `intensity`:该色相参与场的权重,必须可被滑杆实时改变。
49
- - `peakOpacity`:该色相的局部峰值预算;不要把它误写成整个页面透明度。
65
+ - `intensity`:该色相参与场的权重,必须可被滑杆实时改变。渲染器把它和 `peakOpacity` 相乘,作为该色相的强度。
66
+ - `peakOpacity`:`intensity` 为 1 时该色相的强度,是校准时定下的值;不要把它误写成整个页面透明度。
50
67
  - `fieldScale`:该色相的空间频率;不同值能避免所有颜色同相叠加。
51
68
  - `phase`:确定性相位,长度为 2 的数组;同一 seed 下保持稳定。
52
- - `measuredCoverage`:截图中超过基材色度阈值、并归属该色相的像素占比。
53
- - `effectiveShare`:可见色度能量中该色相的相对份额;这是近似归属,不是 shader 内部精确贡献。
69
+ - `ditherStrength`:输出码值上的三角抖动幅度,以一个色阶为单位。1 能消除量化偏差;小于 1 会留下一部分偏差,强度越低越明显。
70
+ - `luminanceCap`:只在黑版生效,限制混色后的线性亮度。
71
+ - `measuredCoverage`:相对底色的色偏超过可见阈值、并按偏离方向归属该色相的像素块占比。
72
+ - `effectiveShare`:可见色偏能量中该色相的相对份额;这是近似归属,不是 shader 内部精确贡献。
@@ -13,7 +13,7 @@
13
13
 
14
14
  ## 浏览器检查
15
15
 
16
- 用真实浏览器生成至少两种视口:桌面(建议 1440×960)和窄屏(建议 390×844)。至少检查:
16
+ 用真实浏览器生成至少两种视口:桌面(建议 1440×960)和窄屏(建议 390×844)。预览用 `scripts/serve_preview.py`:`python -m http.server` 不发 `Cache-Control`,改完 manifest 后浏览器可能继续用旧文件,截图测到的就是上一版。至少检查:
17
17
 
18
18
  1. 页面初始渲染与 WebGL/CSS fallback 状态;
19
19
  2. 参数面板打开、关闭、Escape 返回焦点;
@@ -21,7 +21,8 @@
21
21
  4. 恢复预设、导出 JSON、复制失败时的可用提示;
22
22
  5. 导航、记录选择、标签切换、移动端导航收起;
23
23
  6. 控制台错误/警告和本地静态资源请求;
24
- 7. reduced-motion 与页面隐藏时暂停/冻结行为。
24
+ 7. reduced-motion 与页面隐藏时暂停/冻结行为;
25
+ 8. 暂停后改变窗口尺寸,色场不能变黑;暂停和继续时画面不能跳。
25
26
 
26
27
  截图必须直接看过才能产生审美结论。重点检查:白版是否仍以白为主、黑版是否仍以黑为底、六色是否形成连续流域、文字是否可读,以及是否出现圆形大光斑、RGB 霓虹、灰雾、黑压死或全页玻璃。
27
28
 
@@ -30,16 +31,27 @@
30
31
  用 `scripts/measure_preview.py` 测量纯色场截图,而不是只测被面板遮挡的完整页面。脚本会:
31
32
 
32
33
  - 读取 JS manifest 中的 base 与 palette;
33
- - 将截图转换到 OKLab;
34
- - 以“高于基材色度阈值”的像素作为可见色;
35
- - 将可见像素分配给最近的配置色相;
36
- - 输出可见色像素比例、OKLab chroma、亮度均值/P95/最大值、每色覆盖率和有效色度份额。
34
+ - 在线性光里按像素块(默认 8×8)取平均,抵消抖动,再转换到 OKLab;
35
+ - 计算每个像素块相对 manifest 底色的偏离:亮度偏移,以及 a、b 上的色偏;
36
+ - 把色偏超过可见阈值的像素块算作可见色,按偏离方向归属到最近的配置色相;
37
+ - 输出可见色比例、色偏分位数、亮度偏移分位数、亮度偏移与色偏之比(`shiftToTint`)、灰块比例(`grayRatio`)、每色覆盖率和有效份额。
38
+
39
+ 偏离相对底色计算,底色自身的色度不影响结果。当前渲染器按 sRGB 曲线编码并在量化前抖动,overall 为 0 时截图应与 manifest 底色一致。换用别的渲染器时,先用 overall 0 的截图验证这一点,否则测到的偏离里混有渲染器自身的偏差。
37
40
 
38
41
  低色度白场的色相归属只能作为近似统计,不能替代视觉检查。黑版必须同时检查最大亮度不超过 manifest 的 `luminanceCap` 意图;不要把截图中的 sRGB 值直接当成线性亮度。
39
42
 
43
+ ## 变暗检查
44
+
45
+ 白版的颜色都比底色暗,着色一定伴随变暗。看两项:
46
+
47
+ - `lightnessShift.mean`:整页平均比底色暗多少。
48
+ - `grayRatio`:亮度明显改变、色偏却很小的像素块占比,只在白版计算。它应接近 0;偏高时先检查颜色是否按色相排序,`validate_manifest.py` 会对乱序给出警告。
49
+
50
+ 同一组颜色下,`shiftToTint` 基本不随强度变化,调低强度不会让它变小。色场直接露出的页面,按 `style-contract.md` 里“色场直接露出”一节确认强度。
51
+
40
52
  ## 报告结构
41
53
 
42
- 最终报告至少输出:
54
+ 最终报告至少输出下面这些字段,另外写出 `lightnessShift.mean` 和 `grayRatio`(黑版为 `null`):
43
55
 
44
56
  ```json
45
57
  {