init
This commit is contained in:
commit
ec53fcbe95
1905 changed files with 513762 additions and 0 deletions
61
components/Spinner/FlashingChar.tsx
Normal file
61
components/Spinner/FlashingChar.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Text, useTheme } from '../../ink.js';
|
||||
import { getTheme, type Theme } from '../../utils/theme.js';
|
||||
import { interpolateColor, parseRGB, toRGBColor } from './utils.js';
|
||||
type Props = {
|
||||
char: string;
|
||||
flashOpacity: number;
|
||||
messageColor: keyof Theme;
|
||||
shimmerColor: keyof Theme;
|
||||
};
|
||||
export function FlashingChar(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
char,
|
||||
flashOpacity,
|
||||
messageColor,
|
||||
shimmerColor
|
||||
} = t0;
|
||||
const [themeName] = useTheme();
|
||||
let t1;
|
||||
if ($[0] !== char || $[1] !== flashOpacity || $[2] !== messageColor || $[3] !== shimmerColor || $[4] !== themeName) {
|
||||
t1 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const theme = getTheme(themeName);
|
||||
const baseColorStr = theme[messageColor];
|
||||
const shimmerColorStr = theme[shimmerColor];
|
||||
const baseRGB = baseColorStr ? parseRGB(baseColorStr) : null;
|
||||
const shimmerRGB = shimmerColorStr ? parseRGB(shimmerColorStr) : null;
|
||||
if (baseRGB && shimmerRGB) {
|
||||
const interpolated = interpolateColor(baseRGB, shimmerRGB, flashOpacity);
|
||||
t1 = <Text color={toRGBColor(interpolated)}>{char}</Text>;
|
||||
break bb0;
|
||||
}
|
||||
}
|
||||
$[0] = char;
|
||||
$[1] = flashOpacity;
|
||||
$[2] = messageColor;
|
||||
$[3] = shimmerColor;
|
||||
$[4] = themeName;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
if (t1 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t1;
|
||||
}
|
||||
const shouldUseShimmer = flashOpacity > 0.5;
|
||||
const t2 = shouldUseShimmer ? shimmerColor : messageColor;
|
||||
let t3;
|
||||
if ($[6] !== char || $[7] !== t2) {
|
||||
t3 = <Text color={t2}>{char}</Text>;
|
||||
$[6] = char;
|
||||
$[7] = t2;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlRleHQiLCJ1c2VUaGVtZSIsImdldFRoZW1lIiwiVGhlbWUiLCJpbnRlcnBvbGF0ZUNvbG9yIiwicGFyc2VSR0IiLCJ0b1JHQkNvbG9yIiwiUHJvcHMiLCJjaGFyIiwiZmxhc2hPcGFjaXR5IiwibWVzc2FnZUNvbG9yIiwic2hpbW1lckNvbG9yIiwiRmxhc2hpbmdDaGFyIiwidDAiLCIkIiwiX2MiLCJ0aGVtZU5hbWUiLCJ0MSIsIlN5bWJvbCIsImZvciIsImJiMCIsInRoZW1lIiwiYmFzZUNvbG9yU3RyIiwic2hpbW1lckNvbG9yU3RyIiwiYmFzZVJHQiIsInNoaW1tZXJSR0IiLCJpbnRlcnBvbGF0ZWQiLCJzaG91bGRVc2VTaGltbWVyIiwidDIiLCJ0MyJdLCJzb3VyY2VzIjpbIkZsYXNoaW5nQ2hhci50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBUZXh0LCB1c2VUaGVtZSB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB7IGdldFRoZW1lLCB0eXBlIFRoZW1lIH0gZnJvbSAnLi4vLi4vdXRpbHMvdGhlbWUuanMnXG5pbXBvcnQgeyBpbnRlcnBvbGF0ZUNvbG9yLCBwYXJzZVJHQiwgdG9SR0JDb2xvciB9IGZyb20gJy4vdXRpbHMuanMnXG5cbnR5cGUgUHJvcHMgPSB7XG4gIGNoYXI6IHN0cmluZ1xuICBmbGFzaE9wYWNpdHk6IG51bWJlclxuICBtZXNzYWdlQ29sb3I6IGtleW9mIFRoZW1lXG4gIHNoaW1tZXJDb2xvcjoga2V5b2YgVGhlbWVcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIEZsYXNoaW5nQ2hhcih7XG4gIGNoYXIsXG4gIGZsYXNoT3BhY2l0eSxcbiAgbWVzc2FnZUNvbG9yLFxuICBzaGltbWVyQ29sb3IsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IFt0aGVtZU5hbWVdID0gdXNlVGhlbWUoKVxuICBjb25zdCB0aGVtZSA9IGdldFRoZW1lKHRoZW1lTmFtZSlcblxuICBjb25zdCBiYXNlQ29sb3JTdHIgPSB0aGVtZVttZXNzYWdlQ29sb3JdXG4gIGNvbnN0IHNoaW1tZXJDb2xvclN0ciA9IHRoZW1lW3NoaW1tZXJDb2xvcl1cblxuICBjb25zdCBiYXNlUkdCID0gYmFzZUNvbG9yU3RyID8gcGFyc2VSR0IoYmFzZUNvbG9yU3RyKSA6IG51bGxcbiAgY29uc3Qgc2hpbW1lclJHQiA9IHNoaW1tZXJDb2xvclN0ciA/IHBhcnNlUkdCKHNoaW1tZXJDb2xvclN0cikgOiBudWxsXG5cbiAgaWYgKGJhc2VSR0IgJiYgc2hpbW1lclJHQikge1xuICAgIC8vIFNtb290aCBpbnRlcnBvbGF0aW9uIGJldHdlZW4gY29sb3JzXG4gICAgY29uc3QgaW50ZXJwb2xhdGVkID0gaW50ZXJwb2xhdGVDb2xvcihiYXNlUkdCLCBzaGltbWVyUkdCLCBmbGFzaE9wYWNpdHkpXG4gICAgcmV0dXJuIDxUZXh0IGNvbG9yPXt0b1JHQkNvbG9yKGludGVycG9sYXRlZCl9PntjaGFyfTwvVGV4dD5cbiAgfVxuXG4gIC8vIEZhbGxiYWNrIGZvciBBTlNJIHRoZW1lczogYmluYXJ5IHN3aXRjaFxuICBjb25zdCBzaG91bGRVc2VTaGltbWVyID0gZmxhc2hPcGFjaXR5ID4gMC41XG4gIHJldHVybiAoXG4gICAgPFRleHQgY29sb3I9e3Nob3VsZFVzZVNoaW1tZXIgPyBzaGltbWVyQ29sb3IgOiBtZXNzYWdlQ29sb3J9PntjaGFyfTwvVGV4dD5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBTyxLQUFLQSxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxJQUFJLEVBQUVDLFFBQVEsUUFBUSxjQUFjO0FBQzdDLFNBQVNDLFFBQVEsRUFBRSxLQUFLQyxLQUFLLFFBQVEsc0JBQXNCO0FBQzNELFNBQVNDLGdCQUFnQixFQUFFQyxRQUFRLEVBQUVDLFVBQVUsUUFBUSxZQUFZO0FBRW5FLEtBQUtDLEtBQUssR0FBRztFQUNYQyxJQUFJLEVBQUUsTUFBTTtFQUNa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
|
||||
328
components/Spinner/GlimmerMessage.tsx
Normal file
328
components/Spinner/GlimmerMessage.tsx
Normal file
File diff suppressed because one or more lines are too long
36
components/Spinner/ShimmerChar.tsx
Normal file
36
components/Spinner/ShimmerChar.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Text } from '../../ink.js';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
type Props = {
|
||||
char: string;
|
||||
index: number;
|
||||
glimmerIndex: number;
|
||||
messageColor: keyof Theme;
|
||||
shimmerColor: keyof Theme;
|
||||
};
|
||||
export function ShimmerChar(t0) {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
char,
|
||||
index,
|
||||
glimmerIndex,
|
||||
messageColor,
|
||||
shimmerColor
|
||||
} = t0;
|
||||
const isHighlighted = index === glimmerIndex;
|
||||
const isNearHighlight = Math.abs(index - glimmerIndex) === 1;
|
||||
const shouldUseShimmer = isHighlighted || isNearHighlight;
|
||||
const t1 = shouldUseShimmer ? shimmerColor : messageColor;
|
||||
let t2;
|
||||
if ($[0] !== char || $[1] !== t1) {
|
||||
t2 = <Text color={t1}>{char}</Text>;
|
||||
$[0] = char;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlRleHQiLCJUaGVtZSIsIlByb3BzIiwiY2hhciIsImluZGV4IiwiZ2xpbW1lckluZGV4IiwibWVzc2FnZUNvbG9yIiwic2hpbW1lckNvbG9yIiwiU2hpbW1lckNoYXIiLCJ0MCIsIiQiLCJfYyIsImlzSGlnaGxpZ2h0ZWQiLCJpc05lYXJIaWdobGlnaHQiLCJNYXRoIiwiYWJzIiwic2hvdWxkVXNlU2hpbW1lciIsInQxIiwidDIiXSwic291cmNlcyI6WyJTaGltbWVyQ2hhci50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBUZXh0IH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHR5cGUgeyBUaGVtZSB9IGZyb20gJy4uLy4uL3V0aWxzL3RoZW1lLmpzJ1xuXG50eXBlIFByb3BzID0ge1xuICBjaGFyOiBzdHJpbmdcbiAgaW5kZXg6IG51bWJlclxuICBnbGltbWVySW5kZXg6IG51bWJlclxuICBtZXNzYWdlQ29sb3I6IGtleW9mIFRoZW1lXG4gIHNoaW1tZXJDb2xvcjoga2V5b2YgVGhlbWVcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFNoaW1tZXJDaGFyKHtcbiAgY2hhcixcbiAgaW5kZXgsXG4gIGdsaW1tZXJJbmRleCxcbiAgbWVzc2FnZUNvbG9yLFxuICBzaGltbWVyQ29sb3IsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IGlzSGlnaGxpZ2h0ZWQgPSBpbmRleCA9PT0gZ2xpbW1lckluZGV4XG4gIGNvbnN0IGlzTmVhckhpZ2hsaWdodCA9IE1hdGguYWJzKGluZGV4IC0gZ2xpbW1lckluZGV4KSA9PT0gMVxuICBjb25zdCBzaG91bGRVc2VTaGltbWVyID0gaXNIaWdobGlnaHRlZCB8fCBpc05lYXJIaWdobGlnaHRcblxuICByZXR1cm4gKFxuICAgIDxUZXh0IGNvbG9yPXtzaG91bGRVc2VTaGltbWVyID8gc2hpbW1lckNvbG9yIDogbWVzc2FnZUNvbG9yfT57Y2hhcn08L1RleHQ+XG4gIClcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBLE9BQU8sS0FBS0EsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsSUFBSSxRQUFRLGNBQWM7QUFDbkMsY0FBY0MsS0FBSyxRQUFRLHNCQUFzQjtBQUVqRCxLQUFLQyxLQUFLLEdBQUc7RUFDWEMsSUFBSSxFQUFFLE1BQU07RUFDWkMsS0FBSyxFQUFFLE1BQU07RUFDYkMsWUFBWSxFQUFFLE1BQU07RUFDcEJDLFlBQVksRUFBRSxNQUFNTCxLQUFLO0VBQ3pCTSxZQUFZLEVBQUUsTUFBTU4sS0FBSztBQUMzQixDQUFDO0FBRUQsT0FBTyxTQUFBTyxZQUFBQyxFQUFBO0VBQUEsTUFBQUMsQ0FBQSxHQUFBQyxFQUFBO0VBQXFCO0lBQUFSLElBQUE7SUFBQUMsS0FBQTtJQUFBQyxZQUFBO0lBQUFDLFlBQUE7SUFBQUM7RUFBQSxJQUFBRSxFQU1wQjtFQUNOLE1BQUFHLGFBQUEsR0FBc0JSLEtBQUssS0FBS0MsWUFBWTtFQUM1QyxNQUFBUSxlQUFBLEdBQXdCQyxJQUFJLENBQUFDLEdBQUksQ0FBQ1gsS0FBSyxHQUFHQyxZQUFZLENBQUMsS0FBSyxDQUFDO0VBQzVELE1BQUFXLGdCQUFBLEdBQXlCSixhQUFnQyxJQUFoQ0MsZUFBZ0M7RUFHMUMsTUFBQUksRUFBQSxHQUFBRCxnQkFBZ0IsR0FBaEJULFlBQThDLEdBQTlDRCxZQUE4QztFQUFBLElBQUFZLEVBQUE7RUFBQSxJQUFBUixDQUFBLFFBQUFQLElBQUEsSUFBQU8sQ0FBQSxRQUFBTyxFQUFBO0lBQTNEQyxFQUFBLElBQUMsSUFBSSxDQUFRLEtBQThDLENBQTlDLENBQUFELEVBQTZDLENBQUMsQ0FBR2QsS0FBRyxDQUFFLEVBQWxFLElBQUksQ0FBcUU7SUFBQU8sQ0FBQSxNQUFBUCxJQUFBO0lBQUFPLENBQUEsTUFBQU8sRUFBQTtJQUFBUCxDQUFBLE1BQUFRLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUFSLENBQUE7RUFBQTtFQUFBLE9BQTFFUSxFQUEwRTtBQUFBIiwiaWdub3JlTGlzdCI6W119
|
||||
265
components/Spinner/SpinnerAnimationRow.tsx
Normal file
265
components/Spinner/SpinnerAnimationRow.tsx
Normal file
File diff suppressed because one or more lines are too long
80
components/Spinner/SpinnerGlyph.tsx
Normal file
80
components/Spinner/SpinnerGlyph.tsx
Normal file
File diff suppressed because one or more lines are too long
233
components/Spinner/TeammateSpinnerLine.tsx
Normal file
233
components/Spinner/TeammateSpinnerLine.tsx
Normal file
File diff suppressed because one or more lines are too long
272
components/Spinner/TeammateSpinnerTree.tsx
Normal file
272
components/Spinner/TeammateSpinnerTree.tsx
Normal file
File diff suppressed because one or more lines are too long
10
components/Spinner/index.ts
Normal file
10
components/Spinner/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export { FlashingChar } from './FlashingChar.js'
|
||||
export { GlimmerMessage } from './GlimmerMessage.js'
|
||||
export { ShimmerChar } from './ShimmerChar.js'
|
||||
export { SpinnerGlyph } from './SpinnerGlyph.js'
|
||||
export type { SpinnerMode } from './types.js'
|
||||
export { useShimmerAnimation } from './useShimmerAnimation.js'
|
||||
export { useStalledAnimation } from './useStalledAnimation.js'
|
||||
export { getDefaultCharacters, interpolateColor } from './utils.js'
|
||||
// Teammate components are NOT exported here - use dynamic require() to enable dead code elimination
|
||||
// See REPL.tsx and Spinner.tsx for the correct import pattern
|
||||
1
components/Spinner/teammateSelectHint.ts
Normal file
1
components/Spinner/teammateSelectHint.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const TEAMMATE_SELECT_HINT = 'shift + ↑/↓ to select'
|
||||
31
components/Spinner/useShimmerAnimation.ts
Normal file
31
components/Spinner/useShimmerAnimation.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { useMemo } from 'react'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { type DOMElement, useAnimationFrame } from '../../ink.js'
|
||||
import type { SpinnerMode } from './types.js'
|
||||
|
||||
export function useShimmerAnimation(
|
||||
mode: SpinnerMode,
|
||||
message: string,
|
||||
isStalled: boolean,
|
||||
): [ref: (element: DOMElement | null) => void, glimmerIndex: number] {
|
||||
const glimmerSpeed = mode === 'requesting' ? 50 : 200
|
||||
// Pass null when stalled to unsubscribe from the clock — otherwise the
|
||||
// setInterval keeps firing at 20fps even when the shimmer isn't visible.
|
||||
// Notably, if the caller never attaches `ref` (e.g. conditional JSX),
|
||||
// useTerminalViewport stays at its initial isVisible:true and the
|
||||
// viewport-pause never kicks in, so this is the only stop mechanism.
|
||||
const [ref, time] = useAnimationFrame(isStalled ? null : glimmerSpeed)
|
||||
const messageWidth = useMemo(() => stringWidth(message), [message])
|
||||
|
||||
if (isStalled) {
|
||||
return [ref, -100]
|
||||
}
|
||||
|
||||
const cyclePosition = Math.floor(time / glimmerSpeed)
|
||||
const cycleLength = messageWidth + 20
|
||||
|
||||
if (mode === 'requesting') {
|
||||
return [ref, (cyclePosition % cycleLength) - 10]
|
||||
}
|
||||
return [ref, messageWidth + 10 - (cyclePosition % cycleLength)]
|
||||
}
|
||||
75
components/Spinner/useStalledAnimation.ts
Normal file
75
components/Spinner/useStalledAnimation.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { useRef } from 'react'
|
||||
|
||||
// Hook to handle the transition to red when tokens stop flowing.
|
||||
// Driven by the parent's animation clock time instead of independent intervals,
|
||||
// so it slows down when the terminal is blurred.
|
||||
export function useStalledAnimation(
|
||||
time: number,
|
||||
currentResponseLength: number,
|
||||
hasActiveTools = false,
|
||||
reducedMotion = false,
|
||||
): {
|
||||
isStalled: boolean
|
||||
stalledIntensity: number
|
||||
} {
|
||||
const lastTokenTime = useRef(time)
|
||||
const lastResponseLength = useRef(currentResponseLength)
|
||||
const mountTime = useRef(time)
|
||||
const stalledIntensityRef = useRef(0)
|
||||
const lastSmoothTime = useRef(time)
|
||||
|
||||
// Reset timer when new tokens arrive (check actual length change)
|
||||
if (currentResponseLength > lastResponseLength.current) {
|
||||
lastTokenTime.current = time
|
||||
lastResponseLength.current = currentResponseLength
|
||||
stalledIntensityRef.current = 0
|
||||
lastSmoothTime.current = time
|
||||
}
|
||||
|
||||
// Derive time since last token from animation clock
|
||||
let timeSinceLastToken: number
|
||||
if (hasActiveTools) {
|
||||
timeSinceLastToken = 0
|
||||
lastTokenTime.current = time
|
||||
} else if (currentResponseLength > 0) {
|
||||
timeSinceLastToken = time - lastTokenTime.current
|
||||
} else {
|
||||
timeSinceLastToken = time - mountTime.current
|
||||
}
|
||||
|
||||
// Calculate stalled intensity based on time since last token
|
||||
// Start showing red after 3 seconds of no new tokens (only when no tools are active)
|
||||
const isStalled = timeSinceLastToken > 3000 && !hasActiveTools
|
||||
const intensity = isStalled
|
||||
? Math.min((timeSinceLastToken - 3000) / 2000, 1) // Fade over 2 seconds
|
||||
: 0
|
||||
|
||||
// Smooth intensity transition driven by animation frame ticks
|
||||
if (!reducedMotion && (intensity > 0 || stalledIntensityRef.current > 0)) {
|
||||
const dt = time - lastSmoothTime.current
|
||||
if (dt >= 50) {
|
||||
const steps = Math.floor(dt / 50)
|
||||
let current = stalledIntensityRef.current
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const diff = intensity - current
|
||||
if (Math.abs(diff) < 0.01) {
|
||||
current = intensity
|
||||
break
|
||||
}
|
||||
current += diff * 0.1
|
||||
}
|
||||
stalledIntensityRef.current = current
|
||||
lastSmoothTime.current = time
|
||||
}
|
||||
} else {
|
||||
stalledIntensityRef.current = intensity
|
||||
lastSmoothTime.current = time
|
||||
}
|
||||
|
||||
// When reducedMotion is enabled, use instant intensity change
|
||||
const effectiveIntensity = reducedMotion
|
||||
? intensity
|
||||
: stalledIntensityRef.current
|
||||
|
||||
return { isStalled, stalledIntensity: effectiveIntensity }
|
||||
}
|
||||
84
components/Spinner/utils.ts
Normal file
84
components/Spinner/utils.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import type { RGBColor as RGBColorString } from '../../ink/styles.js'
|
||||
import type { RGBColor as RGBColorType } from './types.js'
|
||||
|
||||
export function getDefaultCharacters(): string[] {
|
||||
if (process.env.TERM === 'xterm-ghostty') {
|
||||
return ['·', '✢', '✳', '✶', '✻', '*'] // Use * instead of ✽ for Ghostty because the latter renders in a way that's slightly offset
|
||||
}
|
||||
return process.platform === 'darwin'
|
||||
? ['·', '✢', '✳', '✶', '✻', '✽']
|
||||
: ['·', '✢', '*', '✶', '✻', '✽']
|
||||
}
|
||||
|
||||
// Interpolate between two RGB colors
|
||||
export function interpolateColor(
|
||||
color1: RGBColorType,
|
||||
color2: RGBColorType,
|
||||
t: number, // 0 to 1
|
||||
): RGBColorType {
|
||||
return {
|
||||
r: Math.round(color1.r + (color2.r - color1.r) * t),
|
||||
g: Math.round(color1.g + (color2.g - color1.g) * t),
|
||||
b: Math.round(color1.b + (color2.b - color1.b) * t),
|
||||
}
|
||||
}
|
||||
|
||||
// Convert RGB object to rgb() color string for Text component
|
||||
export function toRGBColor(color: RGBColorType): RGBColorString {
|
||||
return `rgb(${color.r},${color.g},${color.b})`
|
||||
}
|
||||
|
||||
// HSL hue (0-360) to RGB, using voice-mode waveform parameters (s=0.7, l=0.6).
|
||||
export function hueToRgb(hue: number): RGBColorType {
|
||||
const h = ((hue % 360) + 360) % 360
|
||||
const s = 0.7
|
||||
const l = 0.6
|
||||
const c = (1 - Math.abs(2 * l - 1)) * s
|
||||
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
|
||||
const m = l - c / 2
|
||||
let r = 0
|
||||
let g = 0
|
||||
let b = 0
|
||||
if (h < 60) {
|
||||
r = c
|
||||
g = x
|
||||
} else if (h < 120) {
|
||||
r = x
|
||||
g = c
|
||||
} else if (h < 180) {
|
||||
g = c
|
||||
b = x
|
||||
} else if (h < 240) {
|
||||
g = x
|
||||
b = c
|
||||
} else if (h < 300) {
|
||||
r = x
|
||||
b = c
|
||||
} else {
|
||||
r = c
|
||||
b = x
|
||||
}
|
||||
return {
|
||||
r: Math.round((r + m) * 255),
|
||||
g: Math.round((g + m) * 255),
|
||||
b: Math.round((b + m) * 255),
|
||||
}
|
||||
}
|
||||
|
||||
const RGB_CACHE = new Map<string, RGBColorType | null>()
|
||||
|
||||
export function parseRGB(colorStr: string): RGBColorType | null {
|
||||
const cached = RGB_CACHE.get(colorStr)
|
||||
if (cached !== undefined) return cached
|
||||
|
||||
const match = colorStr.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/)
|
||||
const result = match
|
||||
? {
|
||||
r: parseInt(match[1]!, 10),
|
||||
g: parseInt(match[2]!, 10),
|
||||
b: parseInt(match[3]!, 10),
|
||||
}
|
||||
: null
|
||||
RGB_CACHE.set(colorStr, result)
|
||||
return result
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue