| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 |
- import { OptimizedBuffer, RGBA, TextAttributes } from "@opentui/core"
- import { go } from "../logo"
- const PERIOD = 4600
- const RINGS = 3
- const WIDTH = 3.8
- const TAIL = 9.5
- const AMP = 0.55
- const TAIL_AMP = 0.16
- const BREATH_AMP = 0.05
- const BREATH_SPEED = 0.0008
- // Offset so the bg ring emits from the estimated GO center when the logo shimmer peaks.
- const PHASE_OFFSET = 0.29
- const LOGO_GAP = 1
- const LOGO_TOP_BIAS = -1
- const LOGO_LEFT_WIDTH = go.left[0]?.length ?? 0
- const LOGO_LINES = go.left.map((line, index) => line + " ".repeat(LOGO_GAP) + go.right[index])
- const LOGO_WIDTH = LOGO_LINES[0]?.length ?? 0
- const LOGO_HEIGHT = LOGO_LINES.length
- const SPACE = " ".codePointAt(0)!
- const TOP_HALF = "▀".codePointAt(0)!
- const FULL_BLOCK = "█".codePointAt(0)!
- const RING_SCALE = 1 / RINGS
- const TAIL_SCALE = 1 / TAIL
- const LOGO_REACH = Math.hypot(LOGO_WIDTH, LOGO_HEIGHT * 2) + 3
- const enum LogoCellKind {
- Background,
- Top,
- ShadowTop,
- Solid,
- Char,
- }
- type LogoTemplateCell = {
- x: number
- y: number
- kind: LogoCellKind
- charCode: number
- attributes: number
- topDist: number
- bottomDist: number
- }
- const LOGO_TEMPLATE: LogoTemplateCell[] = LOGO_LINES.flatMap((line, y) =>
- Array.from(line)
- .map((char, x) => {
- if (char === " ") return
- const kind =
- char === "_"
- ? LogoCellKind.Background
- : char === "^"
- ? LogoCellKind.Top
- : char === "~"
- ? LogoCellKind.ShadowTop
- : char === "█"
- ? LogoCellKind.Solid
- : LogoCellKind.Char
- return {
- x,
- y,
- kind,
- charCode: char.codePointAt(0) ?? SPACE,
- attributes: x > LOGO_LEFT_WIDTH ? TextAttributes.BOLD : 0,
- topDist: Math.hypot(x + 0.5 - LOGO_WIDTH / 2, y * 2 - LOGO_HEIGHT),
- bottomDist: Math.hypot(x + 0.5 - LOGO_WIDTH / 2, y * 2 + 1 - LOGO_HEIGHT),
- }
- })
- .filter((cell): cell is LogoTemplateCell => !!cell),
- )
- export type Rgb = [number, number, number]
- export type GoUpsellArtRenderOptions = {
- deltaTime?: number
- rgb?: boolean
- cache?: boolean
- }
- const CACHE_FRAME_COUNT = Math.round(PERIOD / (1000 / 30))
- const CACHE_FRAMES_PER_RENDER = 1
- export function toRgb(color: RGBA): Rgb {
- const [r, g, b] = color.toInts()
- return [r, g, b]
- }
- function clamp(n: number) {
- return Math.max(0, Math.min(1, n))
- }
- function writeRgb(buffer: Uint16Array, offset: number, r: number, g: number, b: number, a = 255) {
- buffer[offset] = r
- buffer[offset + 1] = g
- buffer[offset + 2] = b
- buffer[offset + 3] = a
- }
- function mixChannel(base: number, overlay: number, alpha: number) {
- return Math.round(base + (overlay - base) * clamp(alpha))
- }
- function writeLogoTint(
- buffer: Uint16Array,
- offset: number,
- base: Rgb,
- primary: Rgb,
- primaryMix: number,
- peakMix: number,
- ) {
- const p = clamp(primaryMix)
- const q = clamp(peakMix)
- const r = mixChannel(mixChannel(base[0], primary[0], p), 255, q)
- const g = mixChannel(mixChannel(base[1], primary[1], p), 255, q)
- const b = mixChannel(mixChannel(base[2], primary[2], p), 255, q)
- writeRgb(buffer, offset, r, g, b)
- }
- function sameRgb(a: Rgb, b: Rgb) {
- return a[0] === b[0] && a[1] === b[1] && a[2] === b[2]
- }
- export class GoUpsellArtPainter {
- private panelRgb: Rgb = [0, 0, 0]
- private primaryRgb: Rgb = [255, 255, 255]
- private logoBaseRgb: Rgb = [180, 180, 180]
- private elapsed = 0
- private distances = new Float32Array(0)
- private edgeFalloff = new Float32Array(0)
- private geometryWidth = 0
- private geometryHeight = 0
- private reach = 1
- private logoX = 0
- private logoY = 0
- private logoIndexes = new Int32Array(0)
- private logoRgb: boolean | undefined
- private pulsePeak = 0
- private pulsePrimary = 0
- private cacheDirty = true
- private frameCache: Array<{ fg: Uint16Array; bg: Uint16Array }> = []
- private cacheBuildIndex = 0
- setBackgroundPanel(value: RGBA | Rgb | undefined) {
- if (!value) return false
- const next = value instanceof RGBA ? toRgb(value) : value
- if (sameRgb(this.panelRgb, next)) return false
- this.panelRgb = next
- this.invalidateCache()
- return true
- }
- setLogoBase(value: RGBA | Rgb | undefined) {
- if (!value) return false
- const next = value instanceof RGBA ? toRgb(value) : value
- if (sameRgb(this.logoBaseRgb, next)) return false
- this.logoBaseRgb = next
- this.invalidateCache()
- return true
- }
- setPrimary(value: RGBA | Rgb | undefined) {
- if (!value) return false
- const next = value instanceof RGBA ? toRgb(value) : value
- if (sameRgb(this.primaryRgb, next)) return false
- this.primaryRgb = next
- this.invalidateCache()
- return true
- }
- render(frameBuffer: OptimizedBuffer, options: GoUpsellArtRenderOptions = {}) {
- const rgb = options.rgb === true
- this.elapsed = (this.elapsed + (options.deltaTime ?? 0)) % PERIOD
- this.rebuildGeometry(frameBuffer, rgb)
- if (options.cache !== false) {
- this.drawCached(frameBuffer, rgb)
- return
- }
- this.drawBackground(frameBuffer, this.elapsed)
- this.drawLogo(frameBuffer, this.elapsed, rgb)
- }
- private invalidateCache() {
- this.cacheDirty = true
- this.cacheBuildIndex = 0
- this.frameCache = []
- }
- private rebuildGeometry(frameBuffer: OptimizedBuffer, rgb: boolean) {
- const width = frameBuffer.width
- const height = frameBuffer.height
- const geometryChanged = width !== this.geometryWidth || height !== this.geometryHeight
- const logoTemplateChanged = this.logoRgb !== rgb
- if (!geometryChanged && !logoTemplateChanged) return
- if (geometryChanged) {
- this.geometryWidth = width
- this.geometryHeight = height
- this.logoX = Math.max(0, Math.floor((width - LOGO_WIDTH) / 2))
- this.logoY = Math.max(
- 0,
- Math.min(Math.max(0, height - LOGO_HEIGHT), Math.round((height - LOGO_HEIGHT) / 2) + LOGO_TOP_BIAS),
- )
- const centerX = this.logoX + LOGO_WIDTH / 2
- const centerY = this.logoY + LOGO_HEIGHT / 2
- this.reach = Math.hypot(Math.max(centerX, width - centerX), Math.max(centerY, height - centerY) * 2) + TAIL
- this.distances = new Float32Array(width * height)
- this.edgeFalloff = new Float32Array(width * height)
- for (let y = 0; y < height; y++) {
- for (let x = 0; x < width; x++) {
- const index = y * width + x
- const dist = Math.hypot(x + 0.5 - centerX, (y + 0.5 - centerY) * 2)
- this.distances[index] = dist
- this.edgeFalloff[index] = Math.max(0, 1 - (dist / (this.reach * 0.85)) ** 2)
- }
- }
- }
- this.logoRgb = rgb
- this.invalidateCache()
- this.rebuildCellTemplate(frameBuffer, rgb)
- }
- private drawCached(frameBuffer: OptimizedBuffer, rgb: boolean) {
- if (this.cacheDirty) this.startFrameCache(frameBuffer, rgb)
- if (this.cacheBuildIndex < CACHE_FRAME_COUNT) {
- this.buildFrameCache(frameBuffer, rgb)
- this.drawBackground(frameBuffer, this.elapsed)
- this.drawLogo(frameBuffer, this.elapsed, rgb)
- return
- }
- const frame = this.frameCache[Math.floor((this.elapsed / PERIOD) * CACHE_FRAME_COUNT) % CACHE_FRAME_COUNT]
- if (frame) {
- frameBuffer.buffers.fg.set(frame.fg)
- frameBuffer.buffers.bg.set(frame.bg)
- }
- }
- private startFrameCache(frameBuffer: OptimizedBuffer, rgb: boolean) {
- this.frameCache = []
- this.cacheBuildIndex = 0
- this.rebuildCellTemplate(frameBuffer, rgb)
- this.cacheDirty = false
- }
- private buildFrameCache(frameBuffer: OptimizedBuffer, rgb: boolean) {
- const end = Math.min(CACHE_FRAME_COUNT, this.cacheBuildIndex + CACHE_FRAMES_PER_RENDER)
- for (; this.cacheBuildIndex < end; this.cacheBuildIndex++) {
- const t = (this.cacheBuildIndex / CACHE_FRAME_COUNT) * PERIOD
- this.drawBackground(frameBuffer, t)
- this.drawLogo(frameBuffer, t, rgb)
- this.frameCache.push({
- fg: new Uint16Array(frameBuffer.buffers.fg),
- bg: new Uint16Array(frameBuffer.buffers.bg),
- })
- }
- }
- private rebuildCellTemplate(frameBuffer: OptimizedBuffer, rgb: boolean) {
- const buffers = frameBuffer.buffers
- buffers.char.fill(SPACE)
- buffers.attributes.fill(0)
- if (this.geometryWidth < LOGO_WIDTH || this.geometryHeight < LOGO_HEIGHT) {
- this.logoIndexes = new Int32Array(0)
- return
- }
- this.logoIndexes = new Int32Array(LOGO_TEMPLATE.length)
- for (let i = 0; i < LOGO_TEMPLATE.length; i++) {
- const cell = LOGO_TEMPLATE[i]!
- const index = (this.logoY + cell.y) * this.geometryWidth + this.logoX + cell.x
- this.logoIndexes[i] = index
- buffers.attributes[index] = cell.attributes
- buffers.char[index] =
- cell.kind === LogoCellKind.Background
- ? SPACE
- : cell.kind === LogoCellKind.Top || cell.kind === LogoCellKind.ShadowTop
- ? TOP_HALF
- : cell.kind === LogoCellKind.Solid
- ? rgb
- ? TOP_HALF
- : FULL_BLOCK
- : cell.charCode
- }
- }
- private drawBackground(frameBuffer: OptimizedBuffer, t: number) {
- const buffers = frameBuffer.buffers
- const fg = buffers.fg
- const bg = buffers.bg
- const distances = this.distances
- const edgeFalloff = this.edgeFalloff
- const baseR = this.panelRgb[0]
- const baseG = this.panelRgb[1]
- const baseB = this.panelRgb[2]
- const deltaR = this.primaryRgb[0] - baseR
- const deltaG = this.primaryRgb[1] - baseG
- const deltaB = this.primaryRgb[2] - baseB
- const breath = (0.5 + 0.5 * Math.sin(t * BREATH_SPEED)) * BREATH_AMP
- const phase0 = (t / PERIOD - PHASE_OFFSET + 1) % 1
- const phase1 = (t / PERIOD + 1 / RINGS - PHASE_OFFSET + 1) % 1
- const phase2 = (t / PERIOD + 2 / RINGS - PHASE_OFFSET + 1) % 1
- const envelope0 = Math.sin(phase0 * Math.PI)
- const envelope1 = Math.sin(phase1 * Math.PI)
- const envelope2 = Math.sin(phase2 * Math.PI)
- const eased0 = envelope0 * envelope0 * (3 - 2 * envelope0)
- const eased1 = envelope1 * envelope1 * (3 - 2 * envelope1)
- const eased2 = envelope2 * envelope2 * (3 - 2 * envelope2)
- const head0 = phase0 * this.reach
- const head1 = phase1 * this.reach
- const head2 = phase2 * this.reach
- for (let index = 0; index < distances.length; index++) {
- const dist = distances[index]
- const delta0 = dist - head0
- const abs0 = delta0 < 0 ? -delta0 : delta0
- const crest0 = abs0 < WIDTH ? 0.5 + 0.5 * Math.cos((delta0 / WIDTH) * Math.PI) : 0
- const tail0 = delta0 < 0 && delta0 > -TAIL ? (1 + delta0 * TAIL_SCALE) ** 2.3 : 0
- const delta1 = dist - head1
- const abs1 = delta1 < 0 ? -delta1 : delta1
- const crest1 = abs1 < WIDTH ? 0.5 + 0.5 * Math.cos((delta1 / WIDTH) * Math.PI) : 0
- const tail1 = delta1 < 0 && delta1 > -TAIL ? (1 + delta1 * TAIL_SCALE) ** 2.3 : 0
- const delta2 = dist - head2
- const abs2 = delta2 < 0 ? -delta2 : delta2
- const crest2 = abs2 < WIDTH ? 0.5 + 0.5 * Math.cos((delta2 / WIDTH) * Math.PI) : 0
- const tail2 = delta2 < 0 && delta2 > -TAIL ? (1 + delta2 * TAIL_SCALE) ** 2.3 : 0
- const level =
- (crest0 * AMP + tail0 * TAIL_AMP) * eased0 +
- (crest1 * AMP + tail1 * TAIL_AMP) * eased1 +
- (crest2 * AMP + tail2 * TAIL_AMP) * eased2
- const rawStrength = (level * RING_SCALE + breath) * edgeFalloff[index]
- const strength = (rawStrength > 1 ? 1 : rawStrength) * 0.7
- const offset = index * 4
- const r = Math.round(baseR + deltaR * strength)
- const g = Math.round(baseG + deltaG * strength)
- const b = Math.round(baseB + deltaB * strength)
- bg[offset] = fg[offset] = r
- bg[offset + 1] = fg[offset + 1] = g
- bg[offset + 2] = fg[offset + 2] = b
- bg[offset + 3] = fg[offset + 3] = 255
- }
- }
- private setLogoPulse(dist: number, head0: number, eased0: number, head1: number, eased1: number) {
- let peak = 0.04
- let primary = 0
- const delta0 = dist - head0
- const core0 = Math.exp(-(Math.abs(delta0 / 1.2) ** 1.8))
- const soft0 = Math.exp(-(Math.abs(delta0 / 7) ** 1.6))
- const tail0 = delta0 < 0 && delta0 > -7 ? (1 + delta0 / 7) ** 2.6 : 0
- peak += core0 * 0.65 * eased0
- primary += (soft0 * 0.16 + tail0 * 0.22) * eased0
- const delta1 = dist - head1
- const core1 = Math.exp(-(Math.abs(delta1 / 1.2) ** 1.8))
- const soft1 = Math.exp(-(Math.abs(delta1 / 7) ** 1.6))
- const tail1 = delta1 < 0 && delta1 > -7 ? (1 + delta1 / 7) ** 2.6 : 0
- peak += core1 * 0.65 * eased1
- primary += (soft1 * 0.16 + tail1 * 0.22) * eased1
- this.pulsePeak = peak > 1 ? 1 : peak
- this.pulsePrimary = primary > 1 ? 1 : primary
- }
- private drawLogo(frameBuffer: OptimizedBuffer, t: number, rgb: boolean) {
- if (this.logoIndexes.length === 0) return
- const buffers = frameBuffer.buffers
- const fg = buffers.fg
- const bg = buffers.bg
- const shadow: Rgb = [
- mixChannel(this.panelRgb[0], this.logoBaseRgb[0], 0.25),
- mixChannel(this.panelRgb[1], this.logoBaseRgb[1], 0.25),
- mixChannel(this.panelRgb[2], this.logoBaseRgb[2], 0.25),
- ]
- const phase0 = (t / PERIOD) % 1
- const phase1 = (t / PERIOD + 0.5) % 1
- const envelope0 = Math.sin(phase0 * Math.PI)
- const envelope1 = Math.sin(phase1 * Math.PI)
- const eased0 = envelope0 * envelope0 * (3 - 2 * envelope0)
- const eased1 = envelope1 * envelope1 * (3 - 2 * envelope1)
- const head0 = phase0 * LOGO_REACH
- const head1 = phase1 * LOGO_REACH
- for (let i = 0; i < LOGO_TEMPLATE.length; i++) {
- const cell = LOGO_TEMPLATE[i]!
- const index = this.logoIndexes[i]!
- const offset = index * 4
- this.setLogoPulse(cell.topDist, head0, eased0, head1, eased1)
- const topPeak = this.pulsePeak
- const topPrimary = this.pulsePrimary
- this.setLogoPulse(cell.bottomDist, head0, eased0, head1, eased1)
- const bottomPeak = this.pulsePeak
- const bottomPrimary = this.pulsePrimary
- if (cell.kind === LogoCellKind.Background) {
- writeLogoTint(bg, offset, shadow, this.primaryRgb, 0, Math.max(topPeak, bottomPeak) * 0.18)
- continue
- }
- if (cell.kind === LogoCellKind.Top) {
- writeLogoTint(fg, offset, this.logoBaseRgb, this.primaryRgb, topPrimary, topPeak)
- writeLogoTint(bg, offset, shadow, this.primaryRgb, 0, bottomPeak * 0.18)
- continue
- }
- if (cell.kind === LogoCellKind.ShadowTop) {
- writeLogoTint(fg, offset, shadow, this.primaryRgb, 0, topPeak * 0.18)
- continue
- }
- if (cell.kind === LogoCellKind.Solid && rgb) {
- writeLogoTint(fg, offset, this.logoBaseRgb, this.primaryRgb, topPrimary, topPeak)
- writeLogoTint(bg, offset, this.logoBaseRgb, this.primaryRgb, bottomPrimary, bottomPeak)
- continue
- }
- writeLogoTint(
- fg,
- offset,
- this.logoBaseRgb,
- this.primaryRgb,
- (topPrimary + bottomPrimary) / 2,
- (topPeak + bottomPeak) / 2,
- )
- }
- }
- }
|