fix: add missing BitcoinFaceAscii.vue (CI build fix)
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
190
neode-ui/src/views/discover/BitcoinFaceAscii.vue
Normal file
190
neode-ui/src/views/discover/BitcoinFaceAscii.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div class="btc-face-wrap">
|
||||
<pre class="btc-face-canvas" v-html="frameHtml"></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
const frameHtml = ref('')
|
||||
|
||||
const W = 74
|
||||
const H = 38
|
||||
const CX = W / 2
|
||||
const CY = H / 2
|
||||
const RX = 30
|
||||
const RY = 16
|
||||
|
||||
// Bitcoin symbol bitmap (12 wide, 11 tall)
|
||||
const SYM = [
|
||||
' # # ',
|
||||
' ######## ',
|
||||
' ### ## ',
|
||||
' ### ### ',
|
||||
' ### ## ',
|
||||
' ########## ',
|
||||
' ### ## ',
|
||||
' ### ### ',
|
||||
' ### ## ',
|
||||
' ######## ',
|
||||
' # # ',
|
||||
]
|
||||
const SYM_W = SYM[0]!.length
|
||||
const SYM_OX = -Math.floor(SYM_W / 2)
|
||||
const SYM_OY = -Math.floor(SYM.length / 2) + 1
|
||||
|
||||
function isInSym(fx: number, fy: number): boolean {
|
||||
const bx = Math.round(fx) - SYM_OX
|
||||
const by = Math.round(fy) - SYM_OY
|
||||
if (by < 0 || by >= SYM.length || bx < 0 || bx >= SYM_W) return false
|
||||
return SYM[by]![bx] === '#'
|
||||
}
|
||||
|
||||
// Eyes
|
||||
const EYE_Y = -7, EYE_H = 3, EYE_L = -9, EYE_R = 8
|
||||
|
||||
function isEye(fx: number, fy: number): boolean {
|
||||
const ry = Math.round(fy), rx = Math.round(fx)
|
||||
if (rx >= EYE_L - 2 && rx <= EYE_L + 2 && ry >= EYE_Y && ry <= EYE_Y + EYE_H - 1) return true
|
||||
if (rx >= EYE_R - 2 && rx <= EYE_R + 2 && ry >= EYE_Y && ry <= EYE_Y + EYE_H - 1) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function isPupil(fx: number, fy: number, lookX: number): boolean {
|
||||
const ry = Math.round(fy), rx = Math.round(fx)
|
||||
const p = Math.round(lookX * 0.8)
|
||||
if (rx >= EYE_L + p && rx <= EYE_L + p + 1 && ry >= EYE_Y && ry <= EYE_Y + 1) return true
|
||||
if (rx >= EYE_R + p && rx <= EYE_R + p + 1 && ry >= EYE_Y && ry <= EYE_Y + 1) return true
|
||||
return false
|
||||
}
|
||||
|
||||
// Mouth
|
||||
function isMouth(fx: number, fy: number): boolean {
|
||||
const ry = Math.round(fy), rx = Math.round(fx)
|
||||
if (ry === 9 && rx >= -5 && rx <= 5) return Math.abs(rx) >= 2
|
||||
if (ry === 8 && (rx === -5 || rx === 5)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function mouthChar(fx: number): string {
|
||||
const rx = Math.round(fx)
|
||||
return (rx === -5 || rx === 5) ? "'" : '~'
|
||||
}
|
||||
|
||||
const EDGE = '@#%*+=:. '
|
||||
|
||||
function render(f: number): string {
|
||||
const bob = Math.sin(f * 0.055) * 1.2
|
||||
const breathe = Math.sin(f * 0.028) * 0.4
|
||||
const lookX = Math.sin(f * 0.02)
|
||||
const blink = (f % 160 >= 152 && f % 160 <= 157) || (f % 480 >= 300 && f % 480 <= 305)
|
||||
|
||||
const lines: string[] = []
|
||||
for (let y = 0; y < H; y++) {
|
||||
let line = ''
|
||||
for (let x = 0; x < W; x++) {
|
||||
const fy = y - CY + bob, fx = x - CX
|
||||
const rx = RX + breathe * 2, ry = RY + breathe
|
||||
const dx = fx / rx, dy = fy / ry
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if (dist > 1.15) {
|
||||
line += ' '
|
||||
} else if (dist > 1.0) {
|
||||
const sh = Math.sin(f * 0.08 + Math.atan2(dy, dx) * 3) * 0.5 + 0.5
|
||||
const ci = Math.floor(((dist - 1.0) / 0.15) * 6 + sh * 2)
|
||||
const c = EDGE[Math.min(ci, EDGE.length - 1)]
|
||||
line += c === ' ' ? ' ' : `<span class="af-g">${c}</span>`
|
||||
} else if (dist > 0.9) {
|
||||
const sh = Math.sin(f * 0.1 + Math.atan2(dy, dx) * 4) * 0.5 + 0.5
|
||||
let ci = Math.floor((1 - (dist - 0.9) / 0.1) * 3 + sh * 2)
|
||||
ci = Math.max(0, Math.min(ci, 4))
|
||||
line += `<span class="af-b">${'@#%*+'[ci]}</span>`
|
||||
} else if (dist > 0.85) {
|
||||
const sh = Math.sin(f * 0.1 + Math.atan2(dy, dx) * 4 + 1) * 0.5 + 0.5
|
||||
line += `<span class="af-b">${'#%'[Math.floor(sh * 2)]}</span>`
|
||||
} else if (isEye(fx, fy)) {
|
||||
if (blink) line += '<span class="af-b">-</span>'
|
||||
else if (isPupil(fx, fy, lookX)) line += '<span class="af-p">@</span>'
|
||||
else line += ' '
|
||||
} else if (isInSym(fx, fy)) {
|
||||
const gl = Math.sin(f * 0.06 + fx * 0.3) * 0.3
|
||||
line += `<span class="${gl > 0.1 ? 'af-m' : 'af-b'}">$</span>`
|
||||
} else if (isMouth(fx, fy)) {
|
||||
line += `<span class="af-b">${mouthChar(fx)}</span>`
|
||||
} else {
|
||||
const w = Math.sin(f * 0.04 + x * 0.12 + y * 0.08)
|
||||
const d = (dist / 0.85) * 2 + w * 0.6
|
||||
line += d < 1.2 ? '=' : d < 1.6 ? '+' : d < 2.0 ? ':' : d < 2.3 ? '-' : '.'
|
||||
}
|
||||
}
|
||||
lines.push(line)
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
// Animation loop — 24fps (lighter than 30 for embedded use)
|
||||
let frame = 0, lastTime = -1, running = false, rafId: number | null = null
|
||||
const FT = 1000 / 24
|
||||
|
||||
function tick(ts: number) {
|
||||
if (lastTime === -1) lastTime = ts
|
||||
let dt = ts - lastTime
|
||||
let dirty = false
|
||||
while (dt >= FT) { frame++; dt -= FT; lastTime += FT; dirty = true }
|
||||
if (dirty) frameHtml.value = render(frame)
|
||||
if (running) rafId = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (!running) { running = true; lastTime = -1; rafId = requestAnimationFrame(tick) }
|
||||
}
|
||||
|
||||
function stop() {
|
||||
running = false
|
||||
if (rafId) { cancelAnimationFrame(rafId); rafId = null }
|
||||
}
|
||||
|
||||
function onVisChange() {
|
||||
if (document.hidden) stop(); else start()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
frameHtml.value = render(0)
|
||||
} else {
|
||||
document.addEventListener('visibilitychange', onVisChange)
|
||||
start()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stop()
|
||||
document.removeEventListener('visibilitychange', onVisChange)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.btc-face-wrap {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btc-face-canvas {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||
font-size: 7px;
|
||||
line-height: 1.15;
|
||||
white-space: pre;
|
||||
color: #6a6a6e;
|
||||
letter-spacing: 0.3px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btc-face-canvas :deep(.af-b) { color: #F7931A; }
|
||||
.btc-face-canvas :deep(.af-g) { color: #c98a20; }
|
||||
.btc-face-canvas :deep(.af-p) { color: #ffffff; }
|
||||
.btc-face-canvas :deep(.af-m) { color: #e8a43a; }
|
||||
.btc-face-canvas :deep(.af-d) { color: #52524e; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user