Buckets:
| import type { ColorInput } from "@opentui/core" | |
| import { RGBA } from "@opentui/core" | |
| import type { ColorGenerator } from "opentui-spinner" | |
| interface AdvancedGradientOptions { | |
| colors: ColorInput[] | |
| trailLength: number | |
| defaultColor?: ColorInput | |
| direction?: "forward" | "backward" | "bidirectional" | |
| holdFrames?: { start?: number; end?: number } | |
| enableFading?: boolean | |
| minAlpha?: number | |
| } | |
| interface ScannerState { | |
| activePosition: number | |
| isHolding: boolean | |
| holdProgress: number | |
| holdTotal: number | |
| movementProgress: number | |
| movementTotal: number | |
| isMovingForward: boolean | |
| } | |
| function getScannerState( | |
| frameIndex: number, | |
| totalChars: number, | |
| options: Pick<AdvancedGradientOptions, "direction" | "holdFrames">, | |
| ): ScannerState { | |
| const { direction = "forward", holdFrames = {} } = options | |
| if (direction === "bidirectional") { | |
| const forwardFrames = totalChars | |
| const holdEndFrames = holdFrames.end ?? 0 | |
| const backwardFrames = totalChars - 1 | |
| if (frameIndex < forwardFrames) { | |
| // Moving forward | |
| return { | |
| activePosition: frameIndex, | |
| isHolding: false, | |
| holdProgress: 0, | |
| holdTotal: 0, | |
| movementProgress: frameIndex, | |
| movementTotal: forwardFrames, | |
| isMovingForward: true, | |
| } | |
| } else if (frameIndex < forwardFrames + holdEndFrames) { | |
| // Holding at end | |
| return { | |
| activePosition: totalChars - 1, | |
| isHolding: true, | |
| holdProgress: frameIndex - forwardFrames, | |
| holdTotal: holdEndFrames, | |
| movementProgress: 0, | |
| movementTotal: 0, | |
| isMovingForward: true, | |
| } | |
| } else if (frameIndex < forwardFrames + holdEndFrames + backwardFrames) { | |
| // Moving backward | |
| const backwardIndex = frameIndex - forwardFrames - holdEndFrames | |
| return { | |
| activePosition: totalChars - 2 - backwardIndex, | |
| isHolding: false, | |
| holdProgress: 0, | |
| holdTotal: 0, | |
| movementProgress: backwardIndex, | |
| movementTotal: backwardFrames, | |
| isMovingForward: false, | |
| } | |
| } else { | |
| // Holding at start | |
| return { | |
| activePosition: 0, | |
| isHolding: true, | |
| holdProgress: frameIndex - forwardFrames - holdEndFrames - backwardFrames, | |
| holdTotal: holdFrames.start ?? 0, | |
| movementProgress: 0, | |
| movementTotal: 0, | |
| isMovingForward: false, | |
| } | |
| } | |
| } else if (direction === "backward") { | |
| return { | |
| activePosition: totalChars - 1 - (frameIndex % totalChars), | |
| isHolding: false, | |
| holdProgress: 0, | |
| holdTotal: 0, | |
| movementProgress: frameIndex % totalChars, | |
| movementTotal: totalChars, | |
| isMovingForward: false, | |
| } | |
| } else { | |
| return { | |
| activePosition: frameIndex % totalChars, | |
| isHolding: false, | |
| holdProgress: 0, | |
| holdTotal: 0, | |
| movementProgress: frameIndex % totalChars, | |
| movementTotal: totalChars, | |
| isMovingForward: true, | |
| } | |
| } | |
| } | |
| function calculateColorIndex( | |
| frameIndex: number, | |
| charIndex: number, | |
| totalChars: number, | |
| options: Pick<AdvancedGradientOptions, "direction" | "holdFrames" | "trailLength">, | |
| state?: ScannerState, | |
| ): number { | |
| const { trailLength } = options | |
| const { activePosition, isHolding, holdProgress, isMovingForward } = | |
| state ?? getScannerState(frameIndex, totalChars, options) | |
| // Calculate directional distance (positive means trailing behind) | |
| const directionalDistance = isMovingForward | |
| ? activePosition - charIndex // For forward: trail is to the left (lower indices) | |
| : charIndex - activePosition // For backward: trail is to the right (higher indices) | |
| // Handle hold frame fading: keep the lead bright, fade the trail | |
| if (isHolding) { | |
| // Shift the color index by how long we've been holding | |
| return directionalDistance + holdProgress | |
| } | |
| // Normal movement - show gradient trail only behind the movement direction | |
| if (directionalDistance > 0 && directionalDistance < trailLength) { | |
| return directionalDistance | |
| } | |
| // At the active position, show the brightest color | |
| if (directionalDistance === 0) { | |
| return 0 | |
| } | |
| return -1 | |
| } | |
| function createKnightRiderTrail(options: AdvancedGradientOptions): ColorGenerator { | |
| const { colors, defaultColor, enableFading = true, minAlpha = 0 } = options | |
| // Use the provided defaultColor if it's an RGBA instance, otherwise convert/default | |
| // We use RGBA.fromHex for the fallback to ensure we have an RGBA object. | |
| // Note: If defaultColor is a string, we convert it once here. | |
| const defaultRgba = defaultColor instanceof RGBA ? defaultColor : RGBA.fromHex((defaultColor as string) || "#000000") | |
| // Store the base alpha from the inactive factor | |
| const baseInactiveAlpha = defaultRgba.a | |
| let cachedFrameIndex = -1 | |
| let cachedState: ScannerState | null = null | |
| return (frameIndex: number, charIndex: number, _totalFrames: number, totalChars: number) => { | |
| if (frameIndex !== cachedFrameIndex) { | |
| cachedFrameIndex = frameIndex | |
| cachedState = getScannerState(frameIndex, totalChars, options) | |
| } | |
| const state = cachedState! | |
| const index = calculateColorIndex(frameIndex, charIndex, totalChars, options, state) | |
| // Calculate global fade for inactive dots during hold or movement | |
| const { isHolding, holdProgress, holdTotal, movementProgress, movementTotal } = state | |
| let fadeFactor = 1.0 | |
| if (enableFading) { | |
| if (isHolding && holdTotal > 0) { | |
| // Fade out linearly to minAlpha | |
| const progress = Math.min(holdProgress / holdTotal, 1) | |
| fadeFactor = Math.max(minAlpha, 1 - progress * (1 - minAlpha)) | |
| } else if (!isHolding && movementTotal > 0) { | |
| // Fade in linearly from minAlpha during movement | |
| const progress = Math.min(movementProgress / Math.max(1, movementTotal - 1), 1) | |
| fadeFactor = minAlpha + progress * (1 - minAlpha) | |
| } | |
| } | |
| // Combine base inactive alpha with the fade factor | |
| // This ensures inactiveFactor is respected while still allowing fading animation | |
| defaultRgba.a = baseInactiveAlpha * fadeFactor | |
| if (index === -1) { | |
| return defaultRgba | |
| } | |
| return colors[index] ?? defaultRgba | |
| } | |
| } | |
| /** | |
| * Derives a gradient of tail colors from a single bright color using alpha falloff | |
| * @param brightColor The brightest color (center/head of the scanner) | |
| * @param steps Number of gradient steps (default: 6) | |
| * @returns Array of RGBA colors with alpha-based trail fade (background-independent) | |
| */ | |
| export function deriveTrailColors(brightColor: ColorInput, steps: number = 6): RGBA[] { | |
| const baseRgba = brightColor instanceof RGBA ? brightColor : RGBA.fromHex(brightColor as string) | |
| const colors: RGBA[] = [] | |
| for (let i = 0; i < steps; i++) { | |
| // Alpha-based falloff with optional bloom effect | |
| let alpha: number | |
| let brightnessFactor: number | |
| if (i === 0) { | |
| // Lead position: full brightness and opacity | |
| alpha = 1.0 | |
| brightnessFactor = 1.0 | |
| } else if (i === 1) { | |
| // Slight bloom/glare effect: brighten color but reduce opacity slightly | |
| alpha = 0.9 | |
| brightnessFactor = 1.15 | |
| } else { | |
| // Exponential alpha decay for natural-looking trail fade | |
| alpha = Math.pow(0.65, i - 1) | |
| brightnessFactor = 1.0 | |
| } | |
| const r = Math.min(1.0, baseRgba.r * brightnessFactor) | |
| const g = Math.min(1.0, baseRgba.g * brightnessFactor) | |
| const b = Math.min(1.0, baseRgba.b * brightnessFactor) | |
| colors.push(RGBA.fromValues(r, g, b, alpha)) | |
| } | |
| return colors | |
| } | |
| /** | |
| * Derives the inactive/default color from a bright color using alpha | |
| * @param brightColor The brightest color (center/head of the scanner) | |
| * @param factor Alpha factor for inactive color (default: 0.2, range: 0-1) | |
| * @returns The same color with reduced alpha for background-independent dimming | |
| */ | |
| export function deriveInactiveColor(brightColor: ColorInput, factor: number = 0.2): RGBA { | |
| const baseRgba = brightColor instanceof RGBA ? brightColor : RGBA.fromHex(brightColor as string) | |
| // Use the full color brightness but adjust alpha for background-independent dimming | |
| return RGBA.fromValues(baseRgba.r, baseRgba.g, baseRgba.b, factor) | |
| } | |
| export type KnightRiderStyle = "blocks" | "diamonds" | |
| export interface KnightRiderOptions { | |
| width?: number | |
| style?: KnightRiderStyle | |
| holdStart?: number | |
| holdEnd?: number | |
| colors?: ColorInput[] | |
| /** Single color to derive trail from (alternative to providing colors array) */ | |
| color?: ColorInput | |
| /** Number of trail steps when using single color (default: 6) */ | |
| trailSteps?: number | |
| defaultColor?: ColorInput | |
| /** Alpha factor for inactive color when using single color (default: 0.2, range: 0-1) */ | |
| inactiveFactor?: number | |
| /** Enable fading of inactive dots during hold and movement (default: true) */ | |
| enableFading?: boolean | |
| /** Minimum alpha value when fading (default: 0, range: 0-1) */ | |
| minAlpha?: number | |
| } | |
| /** | |
| * Creates frame strings for a Knight Rider style scanner animation | |
| * @param options Configuration options for the Knight Rider effect | |
| * @returns Array of frame strings | |
| */ | |
| export function createFrames(options: KnightRiderOptions = {}): string[] { | |
| const width = options.width ?? 8 | |
| const style = options.style ?? "diamonds" | |
| const holdStart = options.holdStart ?? 30 | |
| const holdEnd = options.holdEnd ?? 9 | |
| const colors = | |
| options.colors ?? | |
| (options.color | |
| ? deriveTrailColors(options.color, options.trailSteps) | |
| : [ | |
| RGBA.fromHex("#ff0000"), // Brightest Red (Center) | |
| RGBA.fromHex("#ff5555"), // Glare/Bloom | |
| RGBA.fromHex("#dd0000"), // Trail 1 | |
| RGBA.fromHex("#aa0000"), // Trail 2 | |
| RGBA.fromHex("#770000"), // Trail 3 | |
| RGBA.fromHex("#440000"), // Trail 4 | |
| ]) | |
| const defaultColor = | |
| options.defaultColor ?? | |
| (options.color ? deriveInactiveColor(options.color, options.inactiveFactor) : RGBA.fromHex("#330000")) | |
| const trailOptions = { | |
| colors, | |
| trailLength: colors.length, | |
| defaultColor, | |
| direction: "bidirectional" as const, | |
| holdFrames: { start: holdStart, end: holdEnd }, | |
| enableFading: options.enableFading, | |
| minAlpha: options.minAlpha, | |
| } | |
| // Bidirectional cycle: Forward (width) + Hold End + Backward (width-1) + Hold Start | |
| const totalFrames = width + holdEnd + (width - 1) + holdStart | |
| // Generate dynamic frames where inactive pixels are dots and active ones are blocks | |
| const frames = Array.from({ length: totalFrames }, (_, frameIndex) => { | |
| return Array.from({ length: width }, (_, charIndex) => { | |
| const index = calculateColorIndex(frameIndex, charIndex, width, trailOptions) | |
| if (style === "diamonds") { | |
| const shapes = ["⬥", "◆", "⬩", "⬪"] | |
| if (index >= 0 && index < trailOptions.colors.length) { | |
| return shapes[Math.min(index, shapes.length - 1)] | |
| } | |
| return "·" | |
| } | |
| // Default to blocks | |
| // It's active if we have a valid color index that is within our colors array | |
| const isActive = index >= 0 && index < trailOptions.colors.length | |
| return isActive ? "■" : "⬝" | |
| }).join("") | |
| }) | |
| return frames | |
| } | |
| /** | |
| * Creates a color generator function for Knight Rider style scanner animation | |
| * @param options Configuration options for the Knight Rider effect | |
| * @returns ColorGenerator function | |
| */ | |
| export function createColors(options: KnightRiderOptions = {}): ColorGenerator { | |
| const holdStart = options.holdStart ?? 30 | |
| const holdEnd = options.holdEnd ?? 9 | |
| const colors = | |
| options.colors ?? | |
| (options.color | |
| ? deriveTrailColors(options.color, options.trailSteps) | |
| : [ | |
| RGBA.fromHex("#ff0000"), // Brightest Red (Center) | |
| RGBA.fromHex("#ff5555"), // Glare/Bloom | |
| RGBA.fromHex("#dd0000"), // Trail 1 | |
| RGBA.fromHex("#aa0000"), // Trail 2 | |
| RGBA.fromHex("#770000"), // Trail 3 | |
| RGBA.fromHex("#440000"), // Trail 4 | |
| ]) | |
| const defaultColor = | |
| options.defaultColor ?? | |
| (options.color ? deriveInactiveColor(options.color, options.inactiveFactor) : RGBA.fromHex("#330000")) | |
| const trailOptions = { | |
| colors, | |
| trailLength: colors.length, | |
| defaultColor, | |
| direction: "bidirectional" as const, | |
| holdFrames: { start: holdStart, end: holdEnd }, | |
| enableFading: options.enableFading, | |
| minAlpha: options.minAlpha, | |
| } | |
| return createKnightRiderTrail(trailOptions) | |
| } | |
Xet Storage Details
- Size:
- 12.4 kB
- Xet hash:
- 1c94602657618dac818a1102eb63a2f0ca20375c13d21be42630b3d9c569925e
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.