Skip to content

feat(kot): brand refinement + OBS FPS performance pass#5

Open
Kyonax wants to merge 1 commit intodevfrom
feat-brand_kot
Open

feat(kot): brand refinement + OBS FPS performance pass#5
Kyonax wants to merge 1 commit intodevfrom
feat-brand_kot

Conversation

@Kyonax
Copy link
Copy Markdown
Owner

@Kyonax Kyonax commented Apr 23, 2026

Checklist (check if it applies)

  • Contains testing instructions
  • Requires environment / credential changes
  • Requires special deployment steps
  • Has unit / integration tests
  • Touches licensing, security, or CI
  • License headers on new files (LICENSING.org)
  • Attribution preserved on modified files
  • Lint passes (npm run lint)
  • Security rules followed (SECURITY.org)
  • No credentials committed
  • All GitHub Checks have passed (no Pre-Check Failed label applied by CI)
  • CHANGELOG.org updated (release PRs only)
  • Version bumped (release PRs only)

What does this PR do?

Feature branch feat-brand_kot ("feature brand Kyonax on Tech"). Two intertwined workstreams land together: a refinement of every component the @kyonax_on_tech brand touches (cam-log restructured around a .hud-group layout system, brand.js schema extended with host + region, session-date prefix driven by brand metadata) AND a targeted OBS FPS performance pass triggered by a real regression — the cyberpunk-glow mixin applied via CSS-class cascade to every gold-text element was crushing fps on low-core-processor hardware. The mixin is gone, halo/glow is reborn as opt-in --hud-halo / --hud-halo-text / --hud-glow CSS custom properties, every OBS-WS composable is now a module-level singleton, the audio analyzer is zero-allocation event-driven, and <AudioMeter> bypasses Vue reactivity in the hot path. ~32 files changed, 1 new test file, net −73 LoC (the perf rewrite undoes more than it adds).

As a content creator running OBS on a multi-core low-clock laptop, I want the HUD overlay to hold full canvas fps while still carrying the cyberpunk glow and a live session-date, so that my recordings don't drop frames from the overlay's rendering cost.

Design / Reference: Decisions #116–#129 + §1.14 Performance Budget tracked in project architecture docs at CONTRIBUTING.org. HUD vocabulary + recording-light idiom defined alongside the brand assets at .github/assets/logo.txt. Scope tracked in CHANGELOG.org TODO block.

Implementation

[NEW] new file · [MOD] modified file · [DEL] removed · [MOV] renamed or relocated

Brand — @kyonax_on_tech (@kyonax_on_tech/)

  • [MOD] brand.js — schema gains host: "KYO-LABS" + region: "COL". region drives the session-date prefix in cam-log.vue (replaces the previously hardcoded "UTC"). Colors-in-SCSS rule unchanged — brand.js still must never carry a colors field.
  • [MOD] sources/hud/cam-log.vue — layout rewritten around a .hud-group structural system. Four positioned groups (.group--top-left, .group--top-right, .group--bottom-left, .group--identity) replace ad-hoc absolute-positioned classes (.rec-frame, .cam-online, .identity-block). Unified .hud-text + .hud-text--primary label markup. .dynamic-layer extracted as a sibling of <HudFrame> to separate static HUD chrome from OBS-state widgets. Top-right now renders [SESSION] name::T00 + CAM ONLINE (via explicit markup, not the deprecated <HudFrame> labels prop). Session-date format is now ${brand.region} ∇ DD.MM.YYYY // ddd → e.g. COL ∇ 23.04.2026 // WED.
  • [MOD] styles/_theme.scss--clr-neutral-100 lightened from hsl(0 0% 95%)hsl(0 0% 85%) so it keeps tonal separation from --clr-neutral-50 after the view-layer --clr-neutral-200 → --clr-neutral-100 harmonization. Section-divider comments stripped.

SCSS design tokens (src/app/scss/abstracts/)

  • [MOD] _mixins.scssremoves cyberpunk-glow mixin (perf-fatal — see Technical Details), max-media-query mixin (unused), @use "sass:math" import (only consumer was cyberpunk-glow). Adds hud-text-base mixin as the minimal label typography core (font family, size, uppercase, letter-spacing, color); hud-label-base now composes hud-text-base + adds position: absolute.
  • [MOD] _theme.scss — declares four new design tokens on :root: --hud-halo (filter: drop-shadow() chain, 3 layers), --hud-halo-text (text-shadow equivalent), --hud-glow (brand-primary glow, rebindable via --hud-glow-color so consumers can re-point to error/success colors without redefining the chain), --hud-group-gap (shared 0.6em vertical rhythm for HUD groups). Plus --clr-primary-100-80 / --clr-primary-100-40 via color-mix() for bar-shadow tinting. Removes the .cyberpunk-glow utility class (replaced by opt-in token references per consumer).

Shared composables — singleton conversion (src/shared/composables/)

  • [NEW] composables.test.js — 6 tests covering the three newly-singleton composables. Mocks useObsWebsocket at module scope (stub obs + a connected ref). For each of useRecordingStatus, useSceneName, useAudioAnalyzer: asserts documented initial state AND asserts the singleton identity contract (expect(a).toBe(b) with same sub-references).
  • [MOD] use-obs-websocket.js — comment strip (module-level singleton pattern was already in place).
  • [MOD] use-recording-status.js — converted to module-level singleton. let shared_state = null; at module scope; first call wires the WS handler + fills state; subsequent callers return the same object. Callers dropped the { obs, connected } params — composable calls useObsWebsocket() internally. watch(connected, fn, { immediate: true }) replaces the old onMounted fetch. onUnmounted cleanup removed (singletons live for page lifetime).
  • [MOD] use-scene-name.js — same singleton conversion.
  • [MOD] use-audio-analyzer.js — singleton + full hot-path rewrite. Preallocated Float32Array(bar_count) for levels + smoothed (mutated in place every tick). 256-entry JITTER_TABLE: Float32Array seeded once at module load, cursor-advanced per tick — replaces per-frame Math.random(). requestAnimationFrame loop deleted; analyzer now ticks off the OBS InputVolumeMeters event (~50 Hz). Reactivity surface reduced to a single tick: ref(0) counter + three low-churn refs (active, source_name); bar levels are NOT reactive. Target input hardcoded to Mic/Aux (eliminates per-event Array.find).

Shared components — HUD + UI primitives (src/shared/components/)

  • [MOD] hud/frame.vuefilter: var(--hud-halo) added to the root so bracket SVGs inherit the dark outline automatically. Dead .border-* divs removed (were display: none from a previous iteration).
  • [MOD] hud/timer.vue.rec.active now overrides --hud-glow-color to var(--clr-error-100) so the REC indicator glows red via the same halo-token pathway the rest of the HUD uses. contain: layout paint added to .recording-timer.
  • [MOD] ui/status-dot.vue — composition split for per-frame paint cost. Outer <span> owns the STATIC dark halo box-shadow: var(--hud-halo-text) (never animated). Inner <span class="glow"> owns a red box-shadow: var(--hud-glow) with opacity: 0 by default, animated to a 1 ↔ 0.35 pulse via a 2s ease-in-out breathe keyframe while .active. Only opacity changes per frame — the dark shadow stays cached. --hud-glow-color scoped inside .glow to var(--clr-error-100).
  • [MOD] ui/data-point.vuecontain: layout paint on root. Opt-in halo via text-shadow: var(--hud-halo-text) on the label and text-shadow: var(--hud-halo-text), var(--hud-glow) on the value.
  • [MOD] ui/badge.vue--dim variant color harmonization (--clr-neutral-200--clr-neutral-100).

Shared widgets — DOM-direct hot path (src/shared/widgets/)

  • [MOD] hud/audio-meter.vue — bypasses Vue reactivity in the per-frame hot path. Template renders v-for of static <div ref="bar_els" class="bar" /> (no :style binding). A watch(tick, ...) callback reads levels[i] synchronously and assigns el.style.transform = SCALE_STRINGS[idx]. SCALE_STRINGS is a precomputed 101-entry Array of scaleY(0.00)...scaleY(1.00) strings (quantized to 2 decimals) — no per-frame concat. Write-threshold skip: if |scale − last_scale[i]| < 0.01, skip the DOM write. update:state emit throttled to ~10 Hz via performance.now() delta. Bar CSS switched from animated heighttransform: scaleY() with transform-origin: bottom (GPU-composited, zero layout cost). source_name prop removed (analyzer owns the target).
  • [MOD] ui/live-readout.vue — skip no-op text writes (if (props.text !== displayed.value) displayed.value = props.text). Prevents unnecessary reactive updates when the interval fires but the upstream text hasn't changed.

Shared bootstrap / utilities (src/shared/, src/)

  • [MOD] brand-loader.js — comment strip.
  • [MOD] version.js — comment strip.
  • [MOD] ../App.vue — comment strip.
  • [MOD] ../main.js — comment strip.

Views — modal + sections + card (src/views/)

  • [MOD] components/elements/card.vue — handler binding renamed @consume_trigger@consume-kebab-case to match Vue's template-event convention. Secondary-text color harmonization (--clr-neutral-200--clr-neutral-100).
  • [MOD] components/modals/preview.vuewindow.resize handler now debounced to 100 ms (RESIZE_DEBOUNCE_MS) via setTimeout/clearTimeout. Before, every drag-resize fire (~60 Hz) recomputed getBoundingClientRect() + wrote a CSS custom property per frame. Emit event renamed consume_triggerconsume-trigger (defineEmits). Color harmonization on meta-labels.
  • [MOD] components/sections/footer.vue — color harmonization.
  • [MOD] components/sections/sources.vue — color harmonization across tabs, count badge, filter input, placeholder, toolbar, and empty-state text.
  • [MOD] utils/markup.js — comment strip.

Dependencies

  • Runtime added: dayjs (^1.11.20) — first project runtime dependency beyond vue / vue-router / obs-websocket-js. Used by cam-log.vue for the session-date format ([${region} ∇ ]DD.MM.YYYY[ // ]ddd). UTC plugin (dayjs/plugin/utc.js) extended at module scope. Small bundle cost (~2 kB min+gz), tree-shakes the UTC plugin separately.
  • Dev added:
  • Upgraded:
  • Removed:
  • Lockfile: package-lock.json updated with the dayjs tree (one new top-level entry).

CI & Tooling

  • [MOD] eslint.config.mjs — section-divider comments stripped (the // ── <Section> ────── banners). Rules unchanged. Lint output is identical before / after.
  • [MOD] vite.config.js — alias group-header comments stripped. Alias list unchanged.

Technical Details

  • FPS regression root cause — cyberpunk-glow via CSS-class cascade

    • Chose: delete the mixin + .cyberpunk-glow utility + sass:math import entirely. Re-express halo/glow as opt-in CSS custom properties on :root that consumers reference per element.
    • Over: keeping the mixin and pruning which elements apply it, or gating the animation behind prefers-reduced-motion, or reducing the shadow layer count.
    • Why: the compositor pays a layer-rasterization cost per element per frame when every gold-text element has its own animated box-shadow keyframe. With dozens of .cyberpunk-glow consumers on one HUD, the cost compounds — measured perceptual fps drop on a multi-core low-clock laptop. Opt-in per-element tokens let the developer see the cost at the use site, and let specific consumers group halo on a shared container (e.g. <HudFrame> applies filter: var(--hud-halo) once; bracket SVGs inherit the rasterization).
    • Trade-off: every consumer that wanted the glow must now opt in explicitly. That's ~8 sites across the shared primitives — minor migration cost, high ongoing discipline dividend. Legibility memory project_hud_legibility_strategies.md pointers to the mixin file became stale as a result; the architectural rationale ("why no blend modes") still holds.
  • OBS-WS composables → module-level singletons

    • Chose: let shared_state = null; at module scope; first call wires the WS handler + fills state; subsequent callers return the same object. No onUnmounted cleanup. Test asserts expect(a).toBe(b) identity.
    • Over: per-consumer composable instances with reference-counted teardown, or a single top-level provider/inject pattern.
    • Why: N mounted HUD leaves consuming the same OBS event used to register N handlers → N callbacks per event. Singleton collapses to 1 handler + 1 callback regardless of consumer count. provide/inject would have worked but requires a Vue app context (no plain-JS consumption); the singleton pattern is already in use by useObsWebsocket and is test-verifiable.
    • Trade-off: singletons live for the page lifetime and can't be torn down per-consumer. Acceptable because HUD overlays are single-page browser sources — the "last consumer unmount" never realistically fires. A multi-overlay app would need a different pattern.
  • Audio analyzer — event-driven + zero-allocation hot path

    • Chose: tick the analyzer off the OBS InputVolumeMeters event directly (~50 Hz). Preallocated Float32Array for levels + smoothed; precomputed 256-entry JITTER_TABLE: Float32Array + cursor-advanced per tick; classic for (let i = 0; i < len; i++) loops; hardcoded Mic/Aux target.
    • Over: keeping the requestAnimationFrame render loop + per-frame array allocation + Math.random() variation + configurable source_name option.
    • Why: two clocks (rAF + OBS event) compete and cost idle wake-ups; per-frame new Array(bar_count) + Math.random() trigger minor GC pauses that manifest as dropped frames; Array.find's iterator callback allocates closures each call; destructuring allocates binding records. Every one of these costs is avoidable, and the cumulative savings are what lets the HUD hold fps on constrained hardware.
    • Trade-off: the composable is now more opinionated — Mic/Aux is hardcoded, and the reactivity surface is a tick counter that consumers must watch (not directly-consumable levels refs). Documented in the composable's header; the singleton-contract test locks the shape.
  • <AudioMeter> — direct DOM writes via template refs

    • Chose: static v-for template; watch(tick, ...) callback reads levels[i] sync and writes el.style.transform = SCALE_STRINGS[idx] directly; quantized 101-entry SCALE_STRINGS lookup; write-threshold skip (|scale − last_scale| < 0.01); emit throttled to ~10 Hz.
    • Over: a reactive :style="{ height: \${scaleLevel(level)}px` }"binding on each bar, or a reactive ref backing per-bartransform` strings.
    • Why: reactive :style on 16 bars × 50 Hz = 800 reactive patch invocations/sec, each walking the dep graph + scheduling a patch. Direct DOM writes skip the entire reactivity machinery and land the value on the element in one statement. transform: scaleY() with transform-origin: bottom is GPU-composited — height animation triggers layout every frame. The quantized table removes all string allocation; the write-threshold removes 30–40% of would-be DOM writes at typical audio levels.
    • Trade-off: Vue reactivity is bypassed in the hot path, which means devtools won't show per-bar values changing. Acceptable — the tick counter + levels Float32Array are inspectable, and the visualization is the truth anyway.
  • <UiStatusDot> — split static halo from animated glow

    • Chose: outer <span> owns the static dark halo (box-shadow: var(--hud-halo-text), never in a keyframe). Inner <span class="glow"> owns a red box-shadow: var(--hud-glow) with opacity: 0 default, animated 1 ↔ 0.35 via a 2s ease-in-out breathe keyframe while .active.
    • Over: one span with both shadows inside the @keyframes (the pre-refactor form).
    • Why: when a box-shadow value is part of a keyframe, the browser re-rasterizes the shadow every frame. Keeping the expensive dark halo STATIC lets the browser cache its rasterization permanently — only the cheap opacity on the sibling changes per frame. This is the "split static from animated" discipline codified in §1.14.3.
    • Trade-off: one extra DOM element per status dot. Negligible — the component exists once per HUD, its consumers count is small.
  • Preview-modal resize — 100 ms trailing debounce

    • Chose: setTimeout(applyScale, 100) with clearTimeout on re-fire + onUnmounted.
    • Over: running applyScale directly on every resize event (the prior form).
    • Why: window.resize fires ~60 Hz during drag; applyScale calls getBoundingClientRect() (forces style flush) + writes a CSS custom property. 60 flushes/sec during a slow drag is pure cost — a single trailing recompute gives the correct final scale with 1/60th the cost.
    • Trade-off: 100 ms perceptual lag between the end of a drag and the final iframe scale. Imperceptible for this use case (the modal is a dev/preview surface, not a game); tunable via RESIZE_DEBOUNCE_MS.

Testing Coverage

Test runner: Vitest 4.1 (happy-dom env)
Command: npm run test

Automated tests

Test file Covers Tests Status
src/shared/brand-loader.test.js brand discovery + colors === undefined enforcement 26
src/shared/version.test.js version string derivation from package.json 1
src/shared/composables/composables.test.js singleton identity + initial state for useRecordingStatus, useSceneName, useAudioAnalyzer 6

Total: 33 tests across 3 files, all passing in 371 ms. Dynamic-behavior tests for the new composables (event-driven tick propagation, Float32Array mutation under simulated InputVolumeMeters events) are deferred to a follow-up PR — singleton identity + initial state contract is the first discipline stake to lock down.

Quality gates (run on every PR)

Gate Source Status
Lint eslint.config.mjs via npm run lint ✅ 0 errors
Unit tests vite.config.js via npm run test ✅ 33/33
Build vite.config.js via npm run build
Security scan .github/workflows/ci.yml
License headers .github/workflows/ci.yml
Pre-Check Failed label pre-check-label job in .github/workflows/ci.yml ✅ label absent

Lint warnings note: 15 non-blocking warnings surface on the perf-critical hot-path files. All are security/detect-object-injection false positives on intentional typed-array indexing (e.g. levels[i], JITTER_TABLE[cursor], els[i].style.transform = SCALE_STRINGS[idx]) — these patterns are MANDATED by the performance discipline (§1.14.6 / D1). One additional warning (no-magic-numbers on .toFixed(3) in cam-log.vue debug-text formatting) is cosmetic. Zero errors; the gate passes.

How to test this PR

feat(kot): brand refinement + OBS FPS performance pass
├─ Setup
├─ Brand refinement — cam-log layout + brand.host + brand.region
├─ FPS pass — CSS cost (halo tokens, cyberpunk-glow removal)
├─ FPS pass — OBS-WS composables as singletons
├─ FPS pass — Audio analyzer + AudioMeter direct DOM writes
├─ FPS pass — Status dot layered composition
├─ FPS pass — Preview modal resize debounce
└─ Regression check — existing Session 9 behaviors still intact

Setup

Prereqs: checkout feat-brand_kot, run npm ci, start the dev server, run the test suite.

  1. git checkout feat-brand_kot && npm ci
    Expected: dayjs@^1.11.20 installed under node_modules/dayjs/. No other new top-level deps.
  2. npm run lint
    Expected: ✖ 15 problems (0 errors, 15 warnings). All warnings on the hot-path files (audio analyzer, audio meter, cam-log). Zero errors.
  3. npm run test
    Expected: Test Files 3 passed (3) / Tests 33 passed (33) in <500 ms.
  4. npm run dev
    Expected: Vite listens on http://localhost:5173, zero console errors.

Brand refinement — cam-log layout + brand.host + brand.region

Prereqs: dev server running, modern browser with DevTools.

  1. Open http://localhost:5173/@kyonax_on_tech/cam-log.
    Expected: top-left renders KYO-LABS (secondary) above the primary gold session-date reading COL ∇ <DD>.<MM>.<YYYY> // <DDD> for today's UTC date — e.g. COL ∇ 23.04.2026 // WED. Glyph renders correctly.
  2. Inspect @kyonax_on_tech/brand.js in the source tree.
    Expected: exports include host: 'KYO-LABS' and region: 'COL'. cam-log.vue consumes both via getBrand('@kyonax_on_tech').
  3. Inspect the DOM structure of the HUD.
    Expected: four .hud-group elements (.group--top-left, .group--top-right, .group--bottom-left, .group--identity). Each group contains .hud-text / .hud-text--primary spans. .dynamic-layer is a sibling of <HudFrame> carrying .status-bar + .debug-info.
  4. Top-right.
    Expected: [SESSION] <scene_name>::T<take_count> non-primary label above the primary gold CAM ONLINE. If OBS is not connected, the label still renders (scene_name falls back to ---, take_count to 00).

FPS pass — CSS cost (halo tokens, cyberpunk-glow removal)

Prereqs: dev server running, Chrome DevTools open.

  1. Search the codebase for cyberpunk-glowgrep -rn 'cyberpunk-glow' src/ @kyonax_on_tech/ --include='*.scss' --include='*.vue'.
    Expected: zero matches. The mixin and the .cyberpunk-glow utility class are fully gone.
  2. Inspect src/app/scss/abstracts/_theme.scss.
    Expected: :root declares --hud-halo, --hud-halo-text, --hud-glow, --hud-glow-color, --hud-group-gap, --clr-primary-100-80, --clr-primary-100-40.
  3. In DevTools, inspect the cam-log .session-date element.
    Expected: computed text-shadow resolves to the full --hud-halo-text, --hud-glow chain. No animated filter or box-shadow in any keyframe — use the DevTools Animations panel to confirm no filter/shadow keyframes are registered.
  4. In DevTools → Performance, record 5 seconds of the HUD with a live webcam overlay (if OBS is running) or with the page idle.
    Expected: frame rate holds 60 fps. Paint cost per frame is low (compositing-only for animated layers).

FPS pass — OBS-WS composables as singletons

Prereqs: tests from Setup step 3 already passing.

  1. Inspect src/shared/composables/use-recording-status.js, use-scene-name.js, use-audio-analyzer.js.
    Expected: each starts with let shared_state = null; at module scope. Each function body early-returns shared_state if set. None call onUnmounted.
  2. Inspect src/shared/composables/composables.test.js.
    Expected: 6 tests, 3 describe blocks (one per composable). Each describe block has an initial-state test and a expect(a).toBe(b) singleton-identity test.
  3. Run the test suite — npm run test.
    Expected: all 6 new tests pass (useRecordingStatus (singleton) > returns the documented initial state, etc.).

FPS pass — Audio analyzer + <AudioMeter> direct DOM writes

Prereqs: OBS 32.1.1 running on port 4455, Mic/Aux input active.

  1. Start OBS, enable WebSocket server, start capturing with Mic/Aux picking up sound.
  2. Open http://localhost:5173/@kyonax_on_tech/cam-log.
    Expected: audio meter bars at the bottom of the HUD respond to the microphone level — bars grow from the bottom (transform: scaleY()) rather than from the top (no more animated height).
  3. In DevTools, inspect a .bar element inside .audio-meter.
    Expected: transform value changes over time (e.g. scaleY(0.42), scaleY(0.88)), not the height inline style. The element has transform-origin: bottom.
  4. In DevTools → Elements → Properties panel on the component root.
    Expected: .audio-meter has contain: layout paint in computed styles. Same for .status-bar, .hud-group, .recording-timer, .ui-data-point, .debug-info, .hud-frame, .cam-log-overlay.
  5. In DevTools → Performance, confirm the audio meter updates don't trigger layout.
    Expected: no purple "Layout" bars during audio-meter animation; only green "Composite Layers" and yellow "Paint" where needed.
  6. Check <AudioMeter> props and events.
    Expected: no source_name prop; no obs prop (analyzer owns the WS). update:state still fires but throttled — observe via a @update:state="console.log" temporary wrapper: events should fire at ~10 Hz max.

FPS pass — Status dot layered composition

Prereqs: OBS running with active recording capability, or use a temporary :active="true" prop bind on <UiStatusDot>.

  1. Start recording in OBS and reload /cam-log.
    Expected: the 6×6 square status dot turns red, breathes smoothly (2s ease-in-out, opacity 1 ↔ 0.35) with a soft red glow pulse. No hard step-flicker.
  2. Inspect the .ui-status-dot element in DevTools.
    Expected: two child elements — outer <span class="ui-status-dot active"> and inner <span class="glow">. Outer span has a static box-shadow (dark halo, --hud-halo-text chain, NOT in any keyframe). Inner .glow has animation: breathe 2s ease-in-out infinite with opacity as the only changing property.
  3. DevTools → Animations panel.
    Expected: only one animation registered (breathe), and its keyframes touch only opacity — no shadow / filter / transform keyframe entries.
  4. Stop OBS recording.
    Expected: dot returns to muted grey, .glow animation stops (opacity: 0 by default).

FPS pass — Preview modal resize debounce

Prereqs: dev server running; landing page at /.

  1. Open http://localhost:5173/, find the CAM-LOG overlay card, click "Preview".
    Expected: modal opens, iframe fits inside stage with no sub-pixel overflow (the Session 9 getBoundingClientRect() fix is still in place).
  2. Slowly drag the browser window edge to resize.
    Expected: the iframe rescales smoothly but NOT on every frame — applyScale fires only ~100 ms after you stop moving. During the drag, the scale is stale for up to 100 ms; after drag end, it snaps to the correct value.
  3. Inspect src/views/components/modals/preview.vue.
    Expected: handleResize wrapper uses setTimeout(applyScale, RESIZE_DEBOUNCE_MS) with clearTimeout on re-fire. onUnmounted clears the pending timer.
  4. Check the @consume_trigger@consume-trigger kebab-case rename in <PreviewModal> emit + <Card> handler binding.
    Expected: modal open + close flow still works (triggers emit reaches Card correctly). No console warnings about unknown emit names.

Regression check — Session 9 behaviors still intact

Prereqs: dev server running.

  1. Session-date format.
    Expected: reads COL ∇ <DD>.<MM>.<YYYY> // <DDD> (Session 9 was UTC ∇ ...; now parameterized via brand.region). Still one-shot computed at setup.
  2. Preview modal iframe sub-pixel flush.
    Expected: no ~1px right-edge overflow (the Session 9 getBoundingClientRect().width fix).
  3. Status dot "breathes" when recording.
    Expected: still breathes — but now via the layered composition (Session 10 refinement of the Session 9 behavior).

Documentation

VIDEO — OBS FPS before vs after with live webcam overlay

Side-by-side recording of the cam-log HUD composited over a live webcam feed in OBS on a low-core-processor laptop. BEFORE: HUD with cyberpunk-glow applied to every gold text, fps drops visibly under webcam load. AFTER: same HUD with opt-in halo tokens, GPU-composited audio meter, singleton composables, CSS containment — fps holds at 60.

SCREENSHOT — Cam-log HUD with brand.region-driven session-date

Top-left of the HUD now reads KYO-LABS (secondary) above COL ∇ 23.04.2026 // WED (primary gold). Top-right shows [SESSION] name::T00 + CAM ONLINE. Four .hud-group elements replace the old ad-hoc absolute-positioned classes.

DIAGRAM — Halo / glow token architecture

:root declares --hud-halo / --hud-halo-text / --hud-glow / --hud-glow-color / --hud-group-gap. Consumers opt in per element: <HudFrame> roots with filter: var(--hud-halo); .hud-text--primary composes text-shadow: var(--hud-halo-text), var(--hud-glow); <UiStatusDot>.glow scopes --hud-glow-color: var(--clr-error-100) to re-point the chain to red. Never auto-applied via utility class.

DIAGRAM — AudioMeter hot path (direct DOM write bypassing Vue reactivity)

InputVolumeMeters event at ~50 Hz → singleton useAudioAnalyzer mutates preallocated Float32Array(16) levels in place + increments tick.value++<AudioMeter> watches tick, reads levels[i] sync, assigns el.style.transform = SCALE_STRINGS[idx] via template ref. No :style binding, no per-frame allocation, no rAF loop, no reactive array writes.

Refine @kyonax_on_tech components and land a targeted perf pass
to restore OBS Browser Source fps on low-core hardware.

- cam-log: .hud-group layout; brand.js gains host + region;
  session-date now `${region} ∇ DD.MM.YYYY // ddd`
- remove cyberpunk-glow mixin (root cause of FPS drop); halo/glow
  reborn as opt-in :root tokens (--hud-halo, --hud-halo-text,
  --hud-glow)
- useRecordingStatus / useSceneName / useAudioAnalyzer →
  module-level singletons (one WS handler per event per page)
- audio analyzer: preallocated Float32Array + 256-entry
  JITTER_TABLE, event-driven off InputVolumeMeters, no rAF
- AudioMeter: direct DOM writes via template refs, quantized
  SCALE_STRINGS, write-threshold skip, ~10Hz emit,
  transform:scaleY() over animated height
- contain: layout paint on every frequently-updating HUD sub-tree
- UiStatusDot: static halo shell + animated-opacity .glow layer
  (dark shadow stays cached)
- preview modal: 100ms resize debounce; consume_trigger →
  consume-trigger (kebab-case)
- neutral-200 → neutral-100 across secondary text
- +composables.test.js: 6 singleton-contract tests (27 → 33)

Modified-by: Cristian D. Moreno (Kyonax) <kyonax25@gmail.com>
@Kyonax Kyonax self-assigned this Apr 23, 2026
@Kyonax Kyonax changed the base branch from master to dev April 23, 2026 06:33
@github-actions
Copy link
Copy Markdown

Protected Files Modified

One or more files in the protected set were changed in this PR. Each category below explains why the file matters.

Legal / Licensing

  • LICENSING.org was modified

Modifying these files changes the project's legal posture. Confirm with the maintainer before merging.

Governance

  • .github/SECURITY.org was modified
  • .github/PULL_REQUEST_TEMPLATE.md was modified

CODEOWNERS / SECURITY / PR template changes affect how every future PR is reviewed. Review carefully.

Supply Chain

  • package.json was modified
  • package-lock.json was modified

Dependency or lockfile changes. Verify the diff (no unexpected packages, no version downgrades).

CI / Security Config

  • .github/workflows/ci.yml was modified
  • .github/workflows/release.yml was modified
  • eslint.config.mjs was modified

Workflow / lint config. A quiet edit here can disable gates — diff against origin carefully.

Build / Config

  • vite.config.js was modified
  • .gitignore was modified
  • .env.example was modified

Build or gitignore config. Verify the build still passes and no ignored paths were accidentally un-ignored.

Release Artifact

  • CHANGELOG.org was modified
  • README.org was modified

Release-tracking files. Expected on release PRs; flag on non-release PRs.

@Kyonax Kyonax added the Require Code Review The PR requires Code Review label Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Require Code Review The PR requires Code Review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant