| | |
| | | |
| | | // Colors (ARGB) |
| | | const ( |
| | | clrCapsuleBG = 0xD91C1C1E |
| | | clrBorder = 0x20FFFFFF |
| | | clrText = 0xFFB4B4B4 |
| | | clrCapsuleBG = 0xA6101318 |
| | | clrCapsuleHi = 0x18FFFFFF |
| | | clrBorder = 0x2EFFFFFF |
| | | clrText = 0xFFE7EDF5 |
| | | clrTextMuted = 0xFFB6C3D2 |
| | | clrBlue = 0xFF007AFF |
| | | clrCyan = 0xFF2ED3FF |
| | | clrViolet = 0xFF7A6CFF |
| | | clrPink = 0xFFFF5EB8 |
| | | clrGreen = 0xFF34C759 |
| | | clrRed = 0xFFFF3B30 |
| | | clrOrange = 0xFFFF9500 |
| | |
| | | |
| | | // Dimensions (logical px) |
| | | const ( |
| | | capW = 170 |
| | | capH = 48 |
| | | capR = 24 |
| | | barW = 5 |
| | | barGap = 4 |
| | | barR = 2.5 |
| | | nBars = 5 |
| | | txtSz = 12.0 |
| | | txtGap = 12 |
| | | capW = 420 |
| | | capH = 92 |
| | | capR = 46 |
| | | barW = 7 |
| | | barGap = 6 |
| | | barR = 3.5 |
| | | nBars = 11 |
| | | txtSz = 17.0 |
| | | txtGap = 20 |
| | | ) |
| | | |
| | | // ---- Win32 structs ---- |
| | |
| | | func statusColor(s Status) uint32 { |
| | | switch s { |
| | | case StatusLoading: |
| | | return clrBlue |
| | | return clrCyan |
| | | case StatusReady, StatusDone: |
| | | return clrGreen |
| | | case StatusRecording, StatusError: |
| | | case StatusRecording, StatusFreetalking: |
| | | return clrCyan |
| | | case StatusError: |
| | | return clrRed |
| | | case StatusFreetalking: |
| | | return clrBlue |
| | | case StatusProcessing, StatusCancelled, StatusNoVoice, StatusNoContent: |
| | | return clrOrange |
| | | default: |
| | |
| | | done: make(chan struct{}), |
| | | } |
| | | for i := range o.barHeights { |
| | | o.barHeights[i] = 14 |
| | | o.barHeights[i] = 16 |
| | | } |
| | | go o.run() |
| | | return o |
| | |
| | | text = cmd.text |
| | | barClr = statusColor(cmd.status) |
| | | case vol := <-o.volCh: |
| | | copy(volumes[0:4], volumes[1:5]) |
| | | volumes[4] = vol |
| | | copy(volumes[0:nBars-1], volumes[1:nBars]) |
| | | volumes[nBars-1] = vol |
| | | case pos := <-o.posCh: |
| | | o.posX, o.posY = pos[0], pos[1] |
| | | uSetWindowPos.Call(hwnd, 0, uintptr(o.posX), uintptr(o.posY), 0, 0, |
| | |
| | | |
| | | o.animTime += 0.033 |
| | | o.animate(status, volumes) |
| | | o.renderFrame(memDC, hwnd, screenDC, font, strFmt, barClr, text, alpha) |
| | | o.renderFrame(memDC, hwnd, screenDC, font, strFmt, status, barClr, text, alpha) |
| | | } |
| | | } |
| | | |
| | |
| | | switch status { |
| | | case StatusLoading: |
| | | for i := 0; i < nBars; i++ { |
| | | pulse := math.Sin(t*2+float64(i)*0.3)*0.5 + 0.5 |
| | | o.barHeights[i] = (10 + pulse*20) * s |
| | | pulse := math.Sin(t*2.4+float64(i)*0.42)*0.5 + 0.5 |
| | | o.barHeights[i] = (12 + pulse*28) * s |
| | | } |
| | | case StatusReady, StatusDone: |
| | | for i := 0; i < nBars; i++ { |
| | | o.barHeights[i] = o.barHeights[i]*0.85 + 14*s*0.15 |
| | | pulse := math.Sin(t*1.4+float64(i)*0.25)*0.5 + 0.5 |
| | | target := (15 + pulse*7) * s |
| | | o.barHeights[i] = o.barHeights[i]*0.78 + target*0.22 |
| | | } |
| | | case StatusRecording, StatusFreetalking: |
| | | hasReal := false |
| | |
| | | for i := 0; i < nBars; i++ { |
| | | var target float64 |
| | | if hasReal { |
| | | target = (8 + volumes[i]*32) * s |
| | | target = (10 + volumes[i]*44) * s |
| | | } else { |
| | | wave := math.Sin(t*4+float64(i)*0.8)*0.5 + 0.5 |
| | | rnd := math.Sin(t*7+float64(i)*1.5) * 0.3 |
| | | target = (10 + (wave+rnd)*25) * s |
| | | wave := math.Sin(t*4.6+float64(i)*0.55)*0.5 + 0.5 |
| | | rnd := math.Sin(t*7.5+float64(i)*1.1) * 0.2 |
| | | target = (12 + (wave+rnd)*30) * s |
| | | } |
| | | o.barHeights[i] = o.barHeights[i]*0.6 + target*0.4 |
| | | o.barHeights[i] = o.barHeights[i]*0.52 + target*0.48 |
| | | } |
| | | case StatusProcessing: |
| | | for i := 0; i < nBars; i++ { |
| | | wave := math.Sin(t*3+float64(i)*0.6)*0.5 + 0.5 |
| | | o.barHeights[i] = (12 + wave*18) * s |
| | | wave := math.Sin(t*3.2+float64(i)*0.5)*0.5 + 0.5 |
| | | o.barHeights[i] = (14 + wave*22) * s |
| | | } |
| | | default: |
| | | for i := 0; i < nBars; i++ { |
| | | o.barHeights[i] = o.barHeights[i]*0.85 + 14*s*0.15 |
| | | o.barHeights[i] = o.barHeights[i]*0.8 + 16*s*0.2 |
| | | } |
| | | } |
| | | } |
| | | |
| | | // ---- Rendering ---- |
| | | |
| | | func (o *winOverlay) renderFrame(memDC, hwnd, screenDC, font, strFmt uintptr, barClr uint32, text string, alpha byte) { |
| | | func (o *winOverlay) renderFrame(memDC, hwnd, screenDC, font, strFmt uintptr, status Status, barClr uint32, text string, alpha byte) { |
| | | var gfx uintptr |
| | | gpFromHDC.Call(memDC, uintptr(unsafe.Pointer(&gfx))) |
| | | if gfx == 0 { |
| | |
| | | gpSetTextHint.Call(gfx, textHintAntiAlias) |
| | | gpClear.Call(gfx, 0) // transparent |
| | | |
| | | // Capsule background |
| | | fillRoundRect(gfx, 0, 0, float64(o.sW), float64(o.sH), o.sCapR, clrCapsuleBG) |
| | | capsuleW := float64(o.sW) |
| | | capsuleH := float64(o.sH) |
| | | capsuleR := o.sCapR |
| | | edgeInset := 1.5 * o.scale |
| | | contentInsetX := 18.0 * o.scale |
| | | contentInsetY := 14.0 * o.scale |
| | | contentH := capsuleH - contentInsetY*2 |
| | | |
| | | // Colored halo + lighter translucent body. |
| | | fillRoundRect(gfx, 0, 0, capsuleW, capsuleH, capsuleR, scaleAlpha(barClr, 0.16)) |
| | | fillRoundRect(gfx, edgeInset, edgeInset, capsuleW-edgeInset*2, capsuleH-edgeInset*2, maxf(capsuleR-edgeInset, 1), clrCapsuleBG) |
| | | fillRoundRect(gfx, contentInsetX, contentInsetY, capsuleW-contentInsetX*2, contentH*0.58, maxf(contentH*0.29, 8*o.scale), clrCapsuleHi) |
| | | |
| | | // Inner border |
| | | strokeRoundRect(gfx, 0.5, 0.5, float64(o.sW)-1, float64(o.sH)-1, o.sCapR-0.5, clrBorder, 1) |
| | | strokeRoundRect(gfx, 0.5, 0.5, capsuleW-1, capsuleH-1, maxf(capsuleR-0.5, 1), clrBorder, float32(maxf(1.0*o.scale, 1))) |
| | | |
| | | // Layout: bars + optional text, centered |
| | | barsW := float32(float64(nBars)*o.sBarW + float64(nBars-1)*o.sBarGap) |
| | |
| | | if text != "" && font != 0 { |
| | | textW = measureText(gfx, font, strFmt, text) |
| | | } |
| | | maxTextW := float32(capsuleW-contentInsetX*2) - barsW - float32(o.sTxtGap) |
| | | if maxTextW < 0 { |
| | | maxTextW = 0 |
| | | } |
| | | if textW > maxTextW { |
| | | textW = maxTextW |
| | | } |
| | | totalW := barsW |
| | | if textW > 0 { |
| | | totalW += float32(o.sTxtGap) + textW |
| | | } |
| | | startX := (float32(o.sW) - totalW) / 2 |
| | | |
| | | // Bar bed glow |
| | | barBedX := float64(startX) - 12*o.scale |
| | | barBedW := float64(barsW) + 24*o.scale |
| | | fillRoundRect( |
| | | gfx, |
| | | barBedX, |
| | | contentInsetY+4*o.scale, |
| | | barBedW, |
| | | contentH-8*o.scale, |
| | | maxf((contentH-8*o.scale)/2, 6*o.scale), |
| | | scaleAlpha(barClr, 0.10), |
| | | ) |
| | | |
| | | // Bars |
| | | for i := 0; i < nBars; i++ { |
| | | bx := startX + float32(float64(i)*(o.sBarW+o.sBarGap)) |
| | | bh := float32(o.barHeights[i]) |
| | | if bh < float32(6*o.scale) { |
| | | bh = float32(6 * o.scale) |
| | | if bh < float32(8*o.scale) { |
| | | bh = float32(8 * o.scale) |
| | | } |
| | | if bh > float32(40*o.scale) { |
| | | bh = float32(40 * o.scale) |
| | | if bh > float32(56*o.scale) { |
| | | bh = float32(56 * o.scale) |
| | | } |
| | | by := (float32(o.sH) - bh) / 2 |
| | | fillRoundRect(gfx, float64(bx), float64(by), o.sBarW, float64(bh), o.sBarR, barClr) |
| | | barColor := barFillColor(status, i, o.animTime, barClr) |
| | | glowH := float64(bh) + 18*o.scale |
| | | glowY := float64(by) - 9*o.scale |
| | | fillRoundRect( |
| | | gfx, |
| | | float64(bx)-o.sBarW*0.4, |
| | | glowY, |
| | | o.sBarW*1.8, |
| | | glowH, |
| | | o.sBarR*1.8, |
| | | scaleAlpha(barColor, 0.18), |
| | | ) |
| | | fillRoundRect(gfx, float64(bx), float64(by), o.sBarW, float64(bh), o.sBarR, barColor) |
| | | } |
| | | |
| | | // Text |
| | | if text != "" && font != 0 && strFmt != 0 { |
| | | tx := startX + barsW + float32(o.sTxtGap) |
| | | drawText(gfx, font, strFmt, text, tx, 0, textW+4, float32(o.sH), clrText) |
| | | textColor := uint32(clrText) |
| | | if status == StatusReady || status == StatusDone { |
| | | textColor = uint32(clrTextMuted) |
| | | } |
| | | drawText(gfx, font, strFmt, text, tx, 0, textW+4, float32(o.sH), textColor) |
| | | } |
| | | |
| | | // UpdateLayeredWindow |
| | |
| | | memDC, uintptr(unsafe.Pointer(&ptSrc)), 0, uintptr(unsafe.Pointer(&blend)), ulwAlpha) |
| | | } |
| | | |
| | | func barFillColor(status Status, index int, t float64, base uint32) uint32 { |
| | | progress := 0.0 |
| | | if nBars > 1 { |
| | | progress = float64(index) / float64(nBars-1) |
| | | } |
| | | switch status { |
| | | case StatusRecording, StatusFreetalking: |
| | | return mixColor(spectralColor(progress+t*0.08), base, 0.18) |
| | | case StatusLoading, StatusProcessing: |
| | | return mixColor(spectralColor(progress+t*0.16), base, 0.32) |
| | | case StatusReady, StatusDone: |
| | | return mixColor(spectralColor(progress+t*0.03), clrGreen, 0.70) |
| | | case StatusError: |
| | | return mixColor(clrRed, clrPink, progress*0.35) |
| | | case StatusCancelled, StatusNoVoice, StatusNoContent: |
| | | return mixColor(clrOrange, clrPink, progress*0.22) |
| | | default: |
| | | return base |
| | | } |
| | | } |
| | | |
| | | // ---- GDI+ drawing helpers ---- |
| | | |
| | | func clamp01(v float64) float64 { |
| | | if v < 0 { |
| | | return 0 |
| | | } |
| | | if v > 1 { |
| | | return 1 |
| | | } |
| | | return v |
| | | } |
| | | |
| | | func maxf(a, b float64) float64 { |
| | | if a > b { |
| | | return a |
| | | } |
| | | return b |
| | | } |
| | | |
| | | func argb(a, r, g, b byte) uint32 { |
| | | return uint32(a)<<24 | uint32(r)<<16 | uint32(g)<<8 | uint32(b) |
| | | } |
| | | |
| | | func colorChannels(c uint32) (byte, byte, byte, byte) { |
| | | return byte(c >> 24), byte(c >> 16), byte(c >> 8), byte(c) |
| | | } |
| | | |
| | | func mixColor(a, b uint32, t float64) uint32 { |
| | | t = clamp01(t) |
| | | aa, ar, ag, ab := colorChannels(a) |
| | | ba, br, bg, bb := colorChannels(b) |
| | | mix := func(x, y byte) byte { |
| | | return byte(math.Round(float64(x) + (float64(y)-float64(x))*t)) |
| | | } |
| | | return argb(mix(aa, ba), mix(ar, br), mix(ag, bg), mix(ab, bb)) |
| | | } |
| | | |
| | | func scaleAlpha(c uint32, factor float64) uint32 { |
| | | factor = clamp01(factor) |
| | | a, r, g, b := colorChannels(c) |
| | | return argb(byte(math.Round(float64(a)*factor)), r, g, b) |
| | | } |
| | | |
| | | func spectralColor(progress float64) uint32 { |
| | | progress = progress - math.Floor(progress) |
| | | switch { |
| | | case progress < 0.25: |
| | | return mixColor(clrCyan, clrBlue, progress/0.25) |
| | | case progress < 0.55: |
| | | return mixColor(clrBlue, clrViolet, (progress-0.25)/0.30) |
| | | case progress < 0.80: |
| | | return mixColor(clrViolet, clrPink, (progress-0.55)/0.25) |
| | | default: |
| | | return mixColor(clrPink, 0xFFFF8AA8, (progress-0.80)/0.20) |
| | | } |
| | | } |
| | | |
| | | func makeRoundRectPath(x, y, w, h, r float64) uintptr { |
| | | var path uintptr |
| | | gpCreatePath.Call(fillModeAlternate, uintptr(unsafe.Pointer(&path))) |