feat(kot): brand refinement + OBS FPS performance pass#5
Open
Conversation
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>
Protected Files ModifiedOne or more files in the protected set were changed in this PR. Each category below explains why the file matters. Legal / Licensing
Modifying these files changes the project's legal posture. Confirm with the maintainer before merging. Governance
CODEOWNERS / SECURITY / PR template changes affect how every future PR is reviewed. Review carefully. Supply Chain
Dependency or lockfile changes. Verify the diff (no unexpected packages, no version downgrades). CI / Security Config
Workflow / lint config. A quiet edit here can disable gates — diff against origin carefully. Build / Config
Build or gitignore config. Verify the build still passes and no ignored paths were accidentally un-ignored. Release Artifact
Release-tracking files. Expected on release PRs; flag on non-release PRs. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Checklist (check if it applies)
npm run lint)Pre-Check Failedlabel applied by CI)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_techbrand touches (cam-log restructured around a.hud-grouplayout system,brand.jsschema extended withhost+region, session-date prefix driven by brand metadata) AND a targeted OBS FPS performance pass triggered by a real regression — thecyberpunk-glowmixin 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-glowCSS 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).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 inCHANGELOG.orgTODO block.Implementation
Brand — @kyonax_on_tech (
@kyonax_on_tech/)brand.js— schema gainshost: "KYO-LABS"+region: "COL".regiondrives the session-date prefix incam-log.vue(replaces the previously hardcoded"UTC"). Colors-in-SCSS rule unchanged —brand.jsstill must never carry acolorsfield.sources/hud/cam-log.vue— layout rewritten around a.hud-groupstructural 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--primarylabel markup..dynamic-layerextracted 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>labelsprop). Session-date format is now${brand.region} ∇ DD.MM.YYYY // ddd→ e.g.COL ∇ 23.04.2026 // WED.styles/_theme.scss—--clr-neutral-100lightened fromhsl(0 0% 95%)→hsl(0 0% 85%)so it keeps tonal separation from--clr-neutral-50after the view-layer--clr-neutral-200 → --clr-neutral-100harmonization. Section-divider comments stripped.SCSS design tokens (
src/app/scss/abstracts/)_mixins.scss— removescyberpunk-glowmixin (perf-fatal — see Technical Details),max-media-querymixin (unused),@use "sass:math"import (only consumer wascyberpunk-glow). Addshud-text-basemixin as the minimal label typography core (font family, size, uppercase, letter-spacing, color);hud-label-basenow composeshud-text-base+ addsposition: absolute._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-colorso 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-40viacolor-mix()for bar-shadow tinting. Removes the.cyberpunk-glowutility class (replaced by opt-in token references per consumer).Shared composables — singleton conversion (
src/shared/composables/)composables.test.js— 6 tests covering the three newly-singleton composables. MocksuseObsWebsocketat module scope (stubobs+ aconnectedref). For each ofuseRecordingStatus,useSceneName,useAudioAnalyzer: asserts documented initial state AND asserts the singleton identity contract (expect(a).toBe(b)with same sub-references).use-obs-websocket.js— comment strip (module-level singleton pattern was already in place).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 callsuseObsWebsocket()internally.watch(connected, fn, { immediate: true })replaces the oldonMountedfetch.onUnmountedcleanup removed (singletons live for page lifetime).use-scene-name.js— same singleton conversion.use-audio-analyzer.js— singleton + full hot-path rewrite. PreallocatedFloat32Array(bar_count)for levels + smoothed (mutated in place every tick). 256-entryJITTER_TABLE: Float32Arrayseeded once at module load, cursor-advanced per tick — replaces per-frameMath.random().requestAnimationFrameloop deleted; analyzer now ticks off the OBSInputVolumeMetersevent (~50 Hz). Reactivity surface reduced to a singletick: ref(0)counter + three low-churn refs (active,source_name); bar levels are NOT reactive. Target input hardcoded toMic/Aux(eliminates per-eventArray.find).Shared components — HUD + UI primitives (
src/shared/components/)hud/frame.vue—filter: var(--hud-halo)added to the root so bracket SVGs inherit the dark outline automatically. Dead.border-*divs removed (weredisplay: nonefrom a previous iteration).hud/timer.vue—.rec.activenow overrides--hud-glow-colortovar(--clr-error-100)so the REC indicator glows red via the same halo-token pathway the rest of the HUD uses.contain: layout paintadded to.recording-timer.ui/status-dot.vue— composition split for per-frame paint cost. Outer<span>owns the STATIC dark halobox-shadow: var(--hud-halo-text)(never animated). Inner<span class="glow">owns a redbox-shadow: var(--hud-glow)withopacity: 0by default, animated to a1 ↔ 0.35pulse via a 2s ease-in-outbreathekeyframe while.active. Onlyopacitychanges per frame — the dark shadow stays cached.--hud-glow-colorscoped inside.glowtovar(--clr-error-100).ui/data-point.vue—contain: layout painton root. Opt-in halo viatext-shadow: var(--hud-halo-text)on the label andtext-shadow: var(--hud-halo-text), var(--hud-glow)on the value.ui/badge.vue—--dimvariant color harmonization (--clr-neutral-200→--clr-neutral-100).Shared widgets — DOM-direct hot path (
src/shared/widgets/)hud/audio-meter.vue— bypasses Vue reactivity in the per-frame hot path. Template rendersv-forof static<div ref="bar_els" class="bar" />(no:stylebinding). Awatch(tick, ...)callback readslevels[i]synchronously and assignsel.style.transform = SCALE_STRINGS[idx].SCALE_STRINGSis a precomputed 101-entry Array ofscaleY(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:stateemit throttled to ~10 Hz viaperformance.now()delta. Bar CSS switched from animatedheight→transform: scaleY()withtransform-origin: bottom(GPU-composited, zero layout cost).source_nameprop removed (analyzer owns the target).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/)brand-loader.js— comment strip.version.js— comment strip.../App.vue— comment strip.../main.js— comment strip.Views — modal + sections + card (
src/views/)components/elements/card.vue— handler binding renamed@consume_trigger→@consume-kebab-caseto match Vue's template-event convention. Secondary-text color harmonization (--clr-neutral-200→--clr-neutral-100).components/modals/preview.vue—window.resizehandler now debounced to 100 ms (RESIZE_DEBOUNCE_MS) viasetTimeout/clearTimeout. Before, every drag-resize fire (~60 Hz) recomputedgetBoundingClientRect()+ wrote a CSS custom property per frame. Emit event renamedconsume_trigger→consume-trigger(defineEmits). Color harmonization on meta-labels.components/sections/footer.vue— color harmonization.components/sections/sources.vue— color harmonization across tabs, count badge, filter input, placeholder, toolbar, and empty-state text.utils/markup.js— comment strip.Dependencies
dayjs(^1.11.20) — first project runtime dependency beyondvue/vue-router/obs-websocket-js. Used bycam-log.vuefor 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.package-lock.jsonupdated with thedayjstree (one new top-level entry).CI & Tooling
eslint.config.mjs— section-divider comments stripped (the// ── <Section> ──────banners). Rules unchanged. Lint output is identical before / after.vite.config.js— alias group-header comments stripped. Alias list unchanged.Technical Details
FPS regression root cause —
cyberpunk-glowvia CSS-class cascade.cyberpunk-glowutility +sass:mathimport entirely. Re-express halo/glow as opt-in CSS custom properties on:rootthat consumers reference per element.prefers-reduced-motion, or reducing the shadow layer count.box-shadowkeyframe. With dozens of.cyberpunk-glowconsumers 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>appliesfilter: var(--hud-halo)once; bracket SVGs inherit the rasterization).project_hud_legibility_strategies.mdpointers to the mixin file became stale as a result; the architectural rationale ("why no blend modes") still holds.OBS-WS composables → module-level singletons
let shared_state = null;at module scope; first call wires the WS handler + fills state; subsequent callers return the same object. NoonUnmountedcleanup. Test assertsexpect(a).toBe(b)identity.provide/injectwould have worked but requires a Vue app context (no plain-JS consumption); the singleton pattern is already in use byuseObsWebsocketand is test-verifiable.Audio analyzer — event-driven + zero-allocation hot path
InputVolumeMetersevent directly (~50 Hz). PreallocatedFloat32Arrayfor levels + smoothed; precomputed 256-entryJITTER_TABLE: Float32Array+ cursor-advanced per tick; classicfor (let i = 0; i < len; i++)loops; hardcodedMic/Auxtarget.requestAnimationFramerender loop + per-frame array allocation +Math.random()variation + configurablesource_nameoption.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.Mic/Auxis hardcoded, and the reactivity surface is atickcounter that consumers must watch (not directly-consumablelevelsrefs). Documented in the composable's header; the singleton-contract test locks the shape.<AudioMeter>— direct DOM writes via template refsv-fortemplate;watch(tick, ...)callback readslevels[i]sync and writesel.style.transform = SCALE_STRINGS[idx]directly; quantized 101-entrySCALE_STRINGSlookup; write-threshold skip (|scale − last_scale| < 0.01); emit throttled to ~10 Hz.:style="{ height: \${scaleLevel(level)}px` }"binding on each bar, or a reactive ref backing per-bartransform` strings.:styleon 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()withtransform-origin: bottomis GPU-composited —heightanimation 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.tickcounter +levelsFloat32Array are inspectable, and the visualization is the truth anyway.<UiStatusDot>— split static halo from animated glow<span>owns the static dark halo (box-shadow: var(--hud-halo-text), never in a keyframe). Inner<span class="glow">owns a redbox-shadow: var(--hud-glow)withopacity: 0default, animated1 ↔ 0.35via a 2s ease-in-outbreathekeyframe while.active.@keyframes(the pre-refactor form).box-shadowvalue 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 cheapopacityon the sibling changes per frame. This is the "split static from animated" discipline codified in §1.14.3.Preview-modal resize — 100 ms trailing debounce
setTimeout(applyScale, 100)withclearTimeouton re-fire +onUnmounted.applyScaledirectly on every resize event (the prior form).window.resizefires ~60 Hz during drag;applyScalecallsgetBoundingClientRect()(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.RESIZE_DEBOUNCE_MS.Testing Coverage
Test runner: Vitest 4.1 (happy-dom env)
Command:
npm run testAutomated tests
src/shared/brand-loader.test.jscolors === undefinedenforcementsrc/shared/version.test.jspackage.jsonsrc/shared/composables/composables.test.jsuseRecordingStatus,useSceneName,useAudioAnalyzerTotal: 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
InputVolumeMetersevents) 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)
eslint.config.mjsvianpm run lintvite.config.jsvianpm run testvite.config.jsvianpm run build.github/workflows/ci.yml.github/workflows/ci.ymlPre-Check Failedlabelpre-check-labeljob in.github/workflows/ci.ymlLint warnings note: 15 non-blocking warnings surface on the perf-critical hot-path files. All are
security/detect-object-injectionfalse 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-numberson.toFixed(3)incam-log.vuedebug-text formatting) is cosmetic. Zero errors; the gate passes.How to test this PR
Setup
git checkout feat-brand_kot && npm ciExpected:
dayjs@^1.11.20installed undernode_modules/dayjs/. No other new top-level deps.npm run lintExpected:
✖ 15 problems (0 errors, 15 warnings). All warnings on the hot-path files (audio analyzer, audio meter, cam-log). Zero errors.npm run testExpected:
Test Files 3 passed (3)/Tests 33 passed (33)in <500 ms.npm run devExpected: Vite listens on
http://localhost:5173, zero console errors.Brand refinement — cam-log layout + brand.host + brand.region
http://localhost:5173/@kyonax_on_tech/cam-log.Expected: top-left renders
KYO-LABS(secondary) above the primary gold session-date readingCOL ∇ <DD>.<MM>.<YYYY> // <DDD>for today's UTC date — e.g.COL ∇ 23.04.2026 // WED. Glyph∇renders correctly.@kyonax_on_tech/brand.jsin the source tree.Expected: exports include
host: 'KYO-LABS'andregion: 'COL'.cam-log.vueconsumes both viagetBrand('@kyonax_on_tech').Expected: four
.hud-groupelements (.group--top-left,.group--top-right,.group--bottom-left,.group--identity). Each group contains.hud-text/.hud-text--primaryspans..dynamic-layeris a sibling of<HudFrame>carrying.status-bar+.debug-info.Expected:
[SESSION] <scene_name>::T<take_count>non-primary label above the primary goldCAM ONLINE. If OBS is not connected, the label still renders (scene_name falls back to---, take_count to00).FPS pass — CSS cost (halo tokens,
cyberpunk-glowremoval)cyberpunk-glow—grep -rn 'cyberpunk-glow' src/ @kyonax_on_tech/ --include='*.scss' --include='*.vue'.Expected: zero matches. The mixin and the
.cyberpunk-glowutility class are fully gone.src/app/scss/abstracts/_theme.scss.Expected:
:rootdeclares--hud-halo,--hud-halo-text,--hud-glow,--hud-glow-color,--hud-group-gap,--clr-primary-100-80,--clr-primary-100-40..session-dateelement.Expected: computed
text-shadowresolves to the full--hud-halo-text, --hud-glowchain. No animatedfilterorbox-shadowin any keyframe — use the DevTools Animations panel to confirm no filter/shadow keyframes are registered.Expected: frame rate holds 60 fps. Paint cost per frame is low (compositing-only for animated layers).
FPS pass — OBS-WS composables as singletons
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-returnsshared_stateif set. None callonUnmounted.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.npm run test.Expected: all 6 new tests pass (
useRecordingStatus (singleton) > returns the documented initial state, etc.).FPS pass — Audio analyzer +
<AudioMeter>direct DOM writeshttp://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 animatedheight)..barelement inside.audio-meter.Expected:
transformvalue changes over time (e.g.scaleY(0.42),scaleY(0.88)), not theheightinline style. The element hastransform-origin: bottom.Expected:
.audio-meterhascontain: layout paintin computed styles. Same for.status-bar,.hud-group,.recording-timer,.ui-data-point,.debug-info,.hud-frame,.cam-log-overlay.Expected: no purple "Layout" bars during audio-meter animation; only green "Composite Layers" and yellow "Paint" where needed.
<AudioMeter>props and events.Expected: no
source_nameprop; noobsprop (analyzer owns the WS).update:statestill 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
/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.
.ui-status-dotelement in DevTools.Expected: two child elements — outer
<span class="ui-status-dot active">and inner<span class="glow">. Outer span has a staticbox-shadow(dark halo,--hud-halo-textchain, NOT in any keyframe). Inner.glowhasanimation: breathe 2s ease-in-out infinitewithopacityas the only changing property.Expected: only one animation registered (
breathe), and its keyframes touch onlyopacity— no shadow / filter / transform keyframe entries.Expected: dot returns to muted grey,
.glowanimation stops (opacity: 0 by default).FPS pass — Preview modal resize debounce
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).Expected: the iframe rescales smoothly but NOT on every frame —
applyScalefires 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.src/views/components/modals/preview.vue.Expected:
handleResizewrapper usessetTimeout(applyScale, RESIZE_DEBOUNCE_MS)withclearTimeouton re-fire.onUnmountedclears the pending timer.@consume_trigger→@consume-triggerkebab-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
Expected: reads
COL ∇ <DD>.<MM>.<YYYY> // <DDD>(Session 9 wasUTC ∇ ...; now parameterized viabrand.region). Still one-shot computed at setup.Expected: no ~1px right-edge overflow (the Session 9
getBoundingClientRect().widthfix).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
SCREENSHOT — Cam-log HUD with brand.region-driven session-date
DIAGRAM — Halo / glow token architecture
DIAGRAM — AudioMeter hot path (direct DOM write bypassing Vue reactivity)