Differences
This shows you the differences between two versions of the page.
| Next revision | Previous revision | ||
| universaltech:start [2026/06/12 03:54] – created jumpstartadmin | universaltech:start [2026/06/22 06:57] (current) – jumpstartadmin | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== @@TITLE@@ ====== | + | ~~NOCACHE~~ |
| < | < | ||
| - | <div class="js-technique-wrap"> | + | <!DOCTYPE html> |
| + | <html lang="en"> | ||
| + | < | ||
| + | <meta charset=" | ||
| + | < | ||
| + | body.mode_show # | ||
| + | * { box-sizing: border-box; margin: 0; padding: 0; } | ||
| + | .navbar.navbar-default { z-index: 10000 !important; } | ||
| + | body: | ||
| - | <!-- ═══════════════════════════════════════ | + | #ut-page { |
| - | INFOSTRIP | + | |
| - | Edit the chips below to match this technique. | + | top: 0; |
| - | Difficulty: js-chip-beginner / js-chip-intermediate / js-chip-advanced / js-chip-expert | + | left: 0; right: 0; bottom: 0; |
| - | Tier: js-chip-tier-s / js-chip-tier-a / js-chip-tier-b / js-chip-tier-c | + | z-index: 9000; |
| - | | + | |
| - | | + | |
| + | overflow: hidden; | ||
| + | font-family: ' | ||
| + | | ||
| + | } | ||
| - | <span class="js-infostrip-label">Difficulty</span> | + | /* VIDEO BG */ |
| - | <div class=" | + | # |
| - | <span class=" | + | position: absolute; |
| - | | + | inset: 0; |
| + | z-index: 0; | ||
| + | } | ||
| + | # | ||
| + | width: 100%; height: 100%; | ||
| + | object-fit: cover; | ||
| + | opacity: 0.55; | ||
| + | } | ||
| + | # | ||
| + | content: ''; | ||
| + | position: absolute; | ||
| + | inset: 0; | ||
| + | background: linear-gradient(to right, rgba(10, | ||
| + | } | ||
| + | |||
| + | /* LEFT */ | ||
| + | #ut-left { | ||
| + | position: relative; | ||
| + | z-index: 10; | ||
| + | flex: 1; | ||
| + | display: flex; | ||
| + | flex-direction: | ||
| + | justify-content: | ||
| + | padding: 0 0 64px 64px; | ||
| + | pointer-events: | ||
| + | } | ||
| + | |||
| + | #ut-splash, #ut-content { | ||
| + | transition: opacity 0.35s, transform 0.35s; | ||
| + | } | ||
| + | # | ||
| + | opacity: 0; | ||
| + | transform: translateY(12px); | ||
| + | pointer-events: | ||
| + | position: absolute; | ||
| + | } | ||
| + | #ut-content { pointer-events: | ||
| + | |||
| + | .ut-eyebrow { | ||
| + | font-size: 11px; | ||
| + | letter-spacing: | ||
| + | text-transform: | ||
| + | color: #6B4FBB; | ||
| + | margin-bottom: | ||
| + | } | ||
| + | # | ||
| + | font-size: 72px; | ||
| + | font-weight: | ||
| + | line-height: | ||
| + | letter-spacing: | ||
| + | margin-bottom: | ||
| + | color: #EDE8F5; | ||
| + | } | ||
| + | # | ||
| + | # | ||
| + | font-size: 15px; | ||
| + | color: #9D85DB; | ||
| + | max-width: 400px; | ||
| + | line-height: | ||
| + | } | ||
| + | |||
| + | /* content state */ | ||
| + | # | ||
| + | font-size: 11px; | ||
| + | letter-spacing: | ||
| + | text-transform: | ||
| + | color: #6B4FBB; | ||
| + | margin-bottom: | ||
| + | } | ||
| + | # | ||
| + | font-size: 56px; | ||
| + | font-weight: | ||
| + | letter-spacing: | ||
| + | line-height: | ||
| + | margin-bottom: | ||
| + | color: #EDE8F5; | ||
| + | } | ||
| + | # | ||
| + | font-size: 14px; | ||
| + | color: #9D85DB; | ||
| + | max-width: 420px; | ||
| + | line-height: | ||
| + | margin-bottom: | ||
| + | } | ||
| + | # | ||
| + | display: flex; | ||
| + | flex-wrap: wrap; | ||
| + | gap: 8px; | ||
| + | margin-bottom: | ||
| + | pointer-events: | ||
| + | } | ||
| + | .ut-guide-card { | ||
| + | background: rgba(18, | ||
| + | border: 1px solid rgba(108, | ||
| + | border-radius: | ||
| + | padding: 12px 16px; | ||
| + | cursor: pointer; | ||
| + | transition: all 0.15s; | ||
| + | text-decoration: | ||
| + | display: block; | ||
| + | } | ||
| + | .ut-guide-card: | ||
| + | background: rgba(61, | ||
| + | border-color: | ||
| + | transform: translateY(-2px); | ||
| + | } | ||
| + | .ut-guide-card-name { font-size: 13px; font-weight: | ||
| + | .ut-guide-card-meta { font-size: 11px; color: #6B4FBB; } | ||
| + | |||
| + | # | ||
| + | display: inline-flex; | ||
| + | align-items: | ||
| + | gap: 8px; | ||
| + | background: rgba(108, | ||
| + | border: 1px solid rgba(108, | ||
| + | border-radius: | ||
| + | padding: 10px 20px; | ||
| + | color: #B8ADDB; | ||
| + | font-size: 13px; | ||
| + | font-family: | ||
| + | cursor: pointer; | ||
| + | text-decoration: | ||
| + | transition: all 0.15s; | ||
| + | pointer-events: | ||
| + | } | ||
| + | # | ||
| + | background: rgba(108, | ||
| + | color: #EDE8F5; | ||
| + | border-color: | ||
| + | } | ||
| + | |||
| + | /* RIGHT — osu carousel */ | ||
| + | # | ||
| + | position: relative; | ||
| + | z-index: 10; | ||
| + | width: 500px; | ||
| + | flex-shrink: | ||
| + | overflow: hidden; | ||
| + | display: flex; | ||
| + | align-items: | ||
| + | user-select: | ||
| + | } | ||
| + | |||
| + | # | ||
| + | position: absolute; | ||
| + | right: 0; | ||
| + | width: 100%; | ||
| + | } | ||
| + | |||
| + | /* tiles */ | ||
| + | .ut-item { | ||
| + | position: absolute; | ||
| + | right: 0; | ||
| + | border-radius: | ||
| + | overflow: hidden; | ||
| + | cursor: pointer; | ||
| + | border: 1px solid rgba(108, | ||
| + | border-right: | ||
| + | will-change: | ||
| + | transition: | ||
| + | width 0.22s cubic-bezier(0.4, | ||
| + | height 0.22s cubic-bezier(0.4, | ||
| + | opacity 0.22s; | ||
| + | } | ||
| + | |||
| + | .ut-item.is-cat | ||
| + | .ut-item.is-sub | ||
| + | .ut-item.is-cat.selected { width: 480px; height: 96px; } | ||
| + | .ut-item.is-sub.selected { width: 440px; height: 72px; } | ||
| + | |||
| + | .ut-item-bg { | ||
| + | position: absolute; inset: 0; | ||
| + | opacity: 0.2; | ||
| + | transition: opacity 0.2s; | ||
| + | } | ||
| + | .ut-item: | ||
| + | .ut-item.selected .ut-item-bg { opacity: 0.38; } | ||
| + | |||
| + | .ut-item-overlay { | ||
| + | position: absolute; inset: 0; | ||
| + | background: linear-gradient(90deg, | ||
| + | } | ||
| + | |||
| + | .ut-item-inner { | ||
| + | position: relative; | ||
| + | z-index: 2; | ||
| + | display: flex; | ||
| + | align-items: | ||
| + | gap: 16px; | ||
| + | padding: 0 22px; | ||
| + | width: 100%; | ||
| + | height: 100%; | ||
| + | } | ||
| + | |||
| + | .ut-item-icon { | ||
| + | font-size: 22px; | ||
| + | color: #9D85DB; | ||
| + | flex-shrink: | ||
| + | transition: color 0.2s, font-size 0.2s; | ||
| + | } | ||
| + | .ut-item.selected .ut-item-icon | ||
| + | .ut-item.is-sub .ut-item-icon | ||
| + | .ut-item.is-sub.selected .ut-item-icon { font-size: 17px; color: #9D85DB; } | ||
| + | |||
| + | .ut-item-text { flex: 1; min-width: 0; } | ||
| + | .ut-item-label { | ||
| + | font-size: 15px; | ||
| + | font-weight: | ||
| + | color: #C8BFED; | ||
| + | white-space: | ||
| + | overflow: hidden; | ||
| + | text-overflow: | ||
| + | transition: color 0.2s, font-size 0.2s; | ||
| + | } | ||
| + | .ut-item.selected .ut-item-label | ||
| + | .ut-item.is-sub .ut-item-label | ||
| + | .ut-item.is-sub.selected .ut-item-label { color: #C8BFED; font-size: 14px; } | ||
| + | |||
| + | .ut-item-sub-count { | ||
| + | font-size: 11px; | ||
| + | color: #6B4FBB; | ||
| + | margin-top: 3px; | ||
| + | } | ||
| + | |||
| + | .ut-item-accent-bar { | ||
| + | position: absolute; | ||
| + | left: 0; top: 0; bottom: 0; | ||
| + | width: 3px; | ||
| + | border-radius: | ||
| + | opacity: 0; | ||
| + | transition: opacity 0.2s; | ||
| + | } | ||
| + | .ut-item.selected .ut-item-accent-bar { opacity: 1; } | ||
| + | |||
| + | /* accent colors */ | ||
| + | .ut-acc-movement .ut-item-bg | ||
| + | .ut-acc-movement .ut-item-accent-bar { background: #9D85DB; } | ||
| + | .ut-acc-combat .ut-item-bg | ||
| + | .ut-acc-combat .ut-item-accent-bar | ||
| + | .ut-acc-items .ut-item-bg | ||
| + | .ut-acc-items .ut-item-accent-bar | ||
| + | .ut-acc-map .ut-item-bg | ||
| + | .ut-acc-map .ut-item-accent-bar | ||
| + | .ut-acc-mechanics .ut-item-bg { background: linear-gradient(90deg,# | ||
| + | .ut-acc-mechanics .ut-item-accent-bar{ background: #DBC085; } | ||
| + | .ut-acc-abilities .ut-item-bg { background: linear-gradient(90deg,# | ||
| + | .ut-acc-abilities .ut-item-accent-bar{ background: #C085DB; } | ||
| + | |||
| + | /* search */ | ||
| + | # | ||
| + | position: absolute; | ||
| + | top: 24px; right: 0; | ||
| + | width: 440px; | ||
| + | z-index: 20; | ||
| + | } | ||
| + | #ut-search { | ||
| + | width: 100%; | ||
| + | background: rgba(10, | ||
| + | border: 1px solid rgba(108, | ||
| + | border-right: | ||
| + | border-radius: | ||
| + | padding: 10px 16px; | ||
| + | color: #EDE8F5; | ||
| + | font-size: 13px; | ||
| + | font-family: | ||
| + | outline: none; | ||
| + | transition: border-color 0.15s; | ||
| + | } | ||
| + | # | ||
| + | # | ||
| + | </ | ||
| + | </ | ||
| + | < | ||
| + | <div id="ut-page"> | ||
| + | |||
| + | <div id="ut-video-bg"> | ||
| + | | ||
| + | <source src="/wiki/ | ||
| + | | ||
| + | </ | ||
| + | |||
| + | <div id=" | ||
| + | <div id=" | ||
| + | | ||
| + | <div id="ut-splash-title">Master the< | ||
| + | <div id=" | ||
| + | </ | ||
| + | <div id=" | ||
| + | <div id=" | ||
| + | <div id="ut-content-title"></ | ||
| + | | ||
| + | <div id=" | ||
| + | <a id=" | ||
| </ | </ | ||
| + | </ | ||
| - | | + | |
| - | < | + | < |
| - | <span class="js-chip-dot" | + | <input type="text" id="ut-search" |
| - | S-Tier | + | |
| </ | </ | ||
| + | <div id=" | ||
| + | </ | ||
| - | <span class=" | + | </div> |
| - | <a href="/ | + | <script> |
| - | < | + | (function(){ |
| - | All Heroes | + | |
| - | </a> | + | |
| - | <span class="js-infostrip-label"> | + | const CATS = [ |
| - | | + | { id:' |
| - | | + | desc:' |
| - | | + | wikiId:' |
| - | | + | subs:[ |
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | ]}, | ||
| + | { id:' | ||
| + | | ||
| + | wikiId:' | ||
| + | subs:[ | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | ]}, | ||
| + | { id:' | ||
| + | desc:' | ||
| + | wikiId:' | ||
| + | subs:[ | ||
| + | | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | ]}, | ||
| + | { id:' | ||
| + | desc:' | ||
| + | wikiId:' | ||
| + | subs:[ | ||
| + | | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | | ||
| + | { id:' | ||
| + | desc:' | ||
| + | wikiId:' | ||
| + | subs:[ | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | ]}, | ||
| + | { id:' | ||
| + | desc:' | ||
| + | wikiId:' | ||
| + | subs:[ | ||
| + | {id:' | ||
| + | {id:' | ||
| + | ]}, | ||
| + | { id:' | ||
| + | desc:' | ||
| + | wikiId:' | ||
| + | subs:[ | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | ]}, | ||
| + | { id:' | ||
| + | desc:' | ||
| + | wikiId:' | ||
| + | subs:[ | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | ]}, | ||
| + | { id:' | ||
| + | desc:' | ||
| + | wikiId:' | ||
| + | subs:[ | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | ]}, | ||
| + | { id:' | ||
| + | desc:' | ||
| + | wikiId:' | ||
| + | subs:[ | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | {id:' | ||
| + | ]}, | ||
| + | ]; | ||
| - | </aside> | + | const VIDS = [ |
| + | '/wiki/ | ||
| + | '/ | ||
| + | '/ | ||
| + | '/ | ||
| + | ]; | ||
| + | let vi = 0; | ||
| + | const video = document.getElementById(' | ||
| + | video.addEventListener(' | ||
| + | vi = (vi + 1) % VIDS.length; | ||
| + | video.src = VIDS[vi]; | ||
| + | video.play(); | ||
| + | }); | ||
| - | <!-- ═══════════════════════════════════════ | + | // ── STATE ────────────────────────────────────────────── |
| - | BODY | + | let activeCatId = null; |
| - | | + | let activeSubId = null; |
| - | < | + | let filter |
| - | | + | // ── LAYOUT CONSTANTS ──────────────────────────────────── |
| - | < | + | const CAT_H = 80; |
| - | < | + | const CAT_H_SEL |
| - | <p class=" | + | const SUB_H = 60; |
| - | One or two sentences describing what this technique is and why it matters. | + | const SUB_H_SEL |
| - | Keep it to the point — the overview section below handles the detail. | + | const GAP = 7; |
| - | </ | + | |
| - | </ | + | |
| - | </html> | + | // scrollY = offset into virtual list (px). Center of viewport = scrollY. |
| + | let scrollY | ||
| + | let velocity | ||
| + | let rafId = null; | ||
| + | let isDragging = false; | ||
| + | let lastDragY | ||
| + | let lastDragT | ||
| - | ===== Overview | + | // ── FLAT ITEM LIST ────────────────────────────────────── |
| + | function buildItemList() { | ||
| + | const items = []; | ||
| + | const q = filter.toLowerCase(); | ||
| + | CATS.forEach(cat | ||
| + | const subMatch | ||
| + | !q || s.label.toLowerCase().includes(q) || cat.label.toLowerCase().includes(q) | ||
| + | ); | ||
| + | if (q && subMatch.length | ||
| + | items.push({ type:' | ||
| + | if (activeCatId | ||
| + | (q ? subMatch : cat.subs).forEach(sub => items.push({ type:' | ||
| + | } | ||
| + | }); | ||
| + | return items; | ||
| + | } | ||
| - | Write your overview here. Explain what the technique does, when to use it, and any important context. | + | // ── POSITIONS ─────────────────────────────────────────── |
| + | function computePositions(items) { | ||
| + | let y = 0; | ||
| + | return items.map(item => { | ||
| + | const isSel = item.type === ' | ||
| + | ? activeCatId === item.cat.id && activeSubId === null | ||
| + | : activeSubId === item.sub.id; | ||
| + | const h = item.type === ' | ||
| + | const pos = { y, h, isSel }; | ||
| + | y += h + GAP; | ||
| + | return pos; | ||
| + | }); | ||
| + | } | ||
| - | < | + | // ── RENDER ───────────────────────────────────────────── |
| - | < | + | function render() { |
| - | < | + | const carousel |
| - | < | + | const wrapH |
| - | < | + | const items |
| - | <p class="js-callout-text"> | + | const pos |
| - | </ | + | const totalH |
| - | </ | + | |
| - | </ | + | |
| - | ===== How to Execute ===== | + | // clamp scroll: allow scrolling through the whole list |
| + | const minScroll | ||
| + | const maxScroll | ||
| + | scrollY | ||
| - | < | + | // track which uids exist |
| - | < | + | const existingEls |
| + | carousel.querySelectorAll(' | ||
| + | const newUids = new Set(); | ||
| + | items.forEach(item => newUids.add(uid(item))); | ||
| + | Object.keys(existingEls).forEach(u => { if (!newUids.has(u)) existingEls[u].remove(); | ||
| - | <div class=" | + | |
| - | <div class=" | + | const vc = wrapH / 2; |
| - | <div class=" | + | |
| - | <div class=" | + | |
| - | <p class=" | + | |
| - | </ | + | |
| - | </div> | + | |
| - | <div class=" | + | items.forEach((item, |
| - | < | + | const u = uid(item); |
| - | < | + | const { y: iy, h, isSel } = pos[i]; |
| - | <div class=" | + | |
| - | <p class=" | + | |
| - | </ | + | |
| - | </ | + | |
| - | <div class="js-step"> | + | // position relative to viewport center |
| - | < | + | const screenY |
| - | < | + | const itemCenter |
| - | < | + | const distFromCenter |
| - | <p class=" | + | const absD = Math.abs(distFromCenter); |
| - | </ | + | const maxD = wrapH * 0.52; |
| - | </ | + | |
| - | | + | // osu curve: items further away pushed right |
| - | </html> | + | const pushFrac = Math.min(absD |
| + | // ease it: more push at the edges | ||
| + | const push = Math.pow(pushFrac, | ||
| - | ===== Input Notation ===== | + | // fade near edges |
| + | const fadeFrac | ||
| + | const opacity | ||
| - | <html> | + | if (opacity |
| - | <table class=" | + | |
| - | | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | </ | + | |
| - | </ | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | </ | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | <span class=" | + | |
| - | <span class=" | + | |
| - | <span class=" | + | |
| - | </ | + | |
| - | < | + | |
| - | < | + | |
| - | </tr> | + | |
| - | | + | |
| - | | + | |
| - | </ | + | |
| - | ===== Stats ===== | + | let el = existingEls[u]; |
| + | if (!el) { | ||
| + | el = document.createElement(' | ||
| + | el.dataset.uid | ||
| + | el.innerHTML | ||
| + | el.addEventListener(' | ||
| + | carousel.appendChild(el); | ||
| + | } | ||
| - | < | + | el.className |
| - | <table class="js-stat-table"> | + | |
| - | | + | ' |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | </ | + | |
| - | </ | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | </ | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | </tr> | + | |
| - | | + | |
| - | </ | + | |
| - | </ | + | |
| - | ===== Video Examples ===== | + | el.style.top |
| + | el.style.height | ||
| + | el.style.transform | ||
| + | el.style.opacity | ||
| + | }); | ||
| - | < | + | carousel.style.height = wrapH + ' |
| - | <!-- Single video: use js-video-block alone. | + | } |
| - | | + | |
| - | < | + | |
| - | <div class=" | + | function uid(item) { |
| - | < | + | |
| - | <!-- Replace src with your fetch.php path, e.g.: | + | } |
| - | / | + | |
| - | <video autoplay muted loop playsinline> | + | |
| - | <source src="/ | + | |
| - | </ | + | |
| - | </ | + | |
| - | <div class=" | + | |
| - | </ | + | |
| - | | + | function buildInner(item) { |
| - | <div class=" | + | if (item.type === ' |
| - | <video autoplay muted loop playsinline> | + | return `<div class=" |
| - | <source src="/wiki/lib/exe/fetch.php? | + | <div class=" |
| - | </video> | + | <div class=" |
| - | </ | + | <div class="ut-item-inner"> |
| - | <div class=" | + | <i class=" |
| - | </ | + | <div class=" |
| + | <div class=" | ||
| + | <div class=" | ||
| + | </div> | ||
| + | <i class="ti ti-chevron-right" | ||
| + | </ | ||
| + | } else { | ||
| + | return `<div class="ut-item-bg"></ | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | <i class=" | ||
| + | <div class=" | ||
| + | < | ||
| + | </ | ||
| + | <i class=" | ||
| + | </ | ||
| + | } | ||
| + | } | ||
| - | </div> | + | // ── CLICK ────────────────────────────────────────────── |
| + | function onItemClick(item) { | ||
| + | if (item.type === ' | ||
| + | if (activeCatId === item.cat.id && activeSubId === null) { | ||
| + | activeCatId = null; | ||
| + | showSplash(); | ||
| + | } else { | ||
| + | activeCatId = item.cat.id; | ||
| + | activeSubId = null; | ||
| + | showCat(item.cat); | ||
| + | } | ||
| + | } else { | ||
| + | activeCatId = item.cat.id; | ||
| + | activeSubId = item.sub.id; | ||
| + | showSub(item.cat, | ||
| + | } | ||
| + | scrollToSelected(); | ||
| + | render(); | ||
| + | } | ||
| - | <!-- Warning callout — edit or delete as needed | + | function scrollToSelected() { |
| - | | + | const items = buildItemList(); |
| - | | + | const pos = computePositions(items); |
| - | | + | const wrapH = document.getElementById(' |
| - | <span class=" | + | let selY = null, selH = CAT_H; |
| - | <p class=" | + | items.forEach((item, |
| - | </ | + | |
| - | | + | |
| - | </html> | + | |
| + | if (isSel) { selY = pos[i].y; selH = pos[i].h; } | ||
| + | }); | ||
| + | if (selY !== null) { | ||
| + | | ||
| + | } | ||
| + | } | ||
| - | ===== Related Techniques | + | // Spring scroll — sets velocity toward target naturally instead of teleporting |
| + | function animateScroll(target) { | ||
| + | // Stop any current momentum and spring toward target | ||
| + | momentumRunning | ||
| + | velocity | ||
| + | const SPRING | ||
| + | const DAMP = 0.72; // damping to prevent overshoot | ||
| + | let springRunning | ||
| + | let sv = 0; // spring velocity separate from main velocity | ||
| + | function step() { | ||
| + | if (!springRunning) return; | ||
| + | const diff = target - scrollY; | ||
| + | if (Math.abs(diff) < 0.5 && Math.abs(sv) < 0.5) { | ||
| + | scrollY | ||
| + | render(); | ||
| + | springRunning | ||
| + | return; | ||
| + | } | ||
| + | sv += diff * SPRING; | ||
| + | sv *= DAMP; | ||
| + | scrollY += sv; | ||
| + | render(); | ||
| + | requestAnimationFrame(step); | ||
| + | } | ||
| + | requestAnimationFrame(step); | ||
| + | } | ||
| - | < | + | // ── CONTENT AREA ─────────────────────────────────────── |
| - | < | + | const splash |
| + | const content = document.getElementById(' | ||
| - | <a href="/ | + | function showSplash() { |
| - | < | + | splash.classList.remove(' |
| - | <span class="js-related-card-desc"> | + | content.classList.add(' |
| - | </ | + | } |
| + | function showCat(cat) { | ||
| + | splash.classList.add(' | ||
| + | content.classList.remove(' | ||
| + | document.getElementById(' | ||
| + | document.getElementById(' | ||
| + | document.getElementById(' | ||
| + | document.getElementById(' | ||
| + | document.getElementById(' | ||
| + | document.getElementById(' | ||
| + | } | ||
| + | function showSub(cat, | ||
| + | splash.classList.add(' | ||
| + | content.classList.remove(' | ||
| + | document.getElementById(' | ||
| + | | ||
| + | document.getElementById(' | ||
| + | document.getElementById(' | ||
| + | document.getElementById(' | ||
| + | document.getElementById(' | ||
| + | } | ||
| - | <a href="/wiki/doku.php? | + | // ── SCROLL ───────────────────────────────────────────── |
| - | < | + | const wrap = document.getElementById(' |
| - | < | + | let momentumRunning |
| - | </a> | + | |
| - | <a href="/wiki/doku.php? | + | // Wheel sensitivity: low so small scrolls move little, fast scrolls build up |
| - | < | + | const WHEEL_ACCEL |
| - | < | + | const FRICTION |
| - | | + | const VEL_CAP |
| + | const VEL_STOP | ||
| - | <a href="/ | + | function startMomentum() { |
| - | <span class=" | + | if (momentumRunning) return; |
| - | < | + | momentumRunning |
| - | | + | function step() { |
| + | velocity *= FRICTION; | ||
| + | if (Math.abs(velocity) | ||
| + | velocity | ||
| + | | ||
| + | | ||
| + | } | ||
| + | scrollY += velocity; | ||
| + | render(); | ||
| + | requestAnimationFrame(step); | ||
| + | } | ||
| + | requestAnimationFrame(step); | ||
| + | } | ||
| - | </div> | + | // WHEEL — accumulate velocity proportional to scroll speed |
| + | wrap.addEventListener(' | ||
| + | e.preventDefault(); | ||
| + | const scale = e.deltaMode === 1 ? 20 : e.deltaMode === 2 ? 300 : 1; | ||
| + | const delta = e.deltaY * scale; | ||
| + | // Add a fraction of delta to velocity — proportional feel | ||
| + | velocity += delta * WHEEL_ACCEL; | ||
| + | velocity = Math.max(-VEL_CAP, | ||
| + | startMomentum(); | ||
| + | }, { passive: false }); | ||
| - | </div><!-- end js-technique-body | + | // DRAG |
| - | </div><!-- end js-technique-wrap --> | + | wrap.addEventListener(' |
| + | momentumRunning = false; | ||
| + | velocity = 0; | ||
| + | isDragging = true; lastDragY = e.clientY; lastDragT = performance.now(); | ||
| + | wrap.style.cursor = ' | ||
| + | e.preventDefault(); | ||
| + | }); | ||
| + | window.addEventListener(' | ||
| + | if (!isDragging) return; | ||
| + | const now = performance.now(); | ||
| + | const dy = e.clientY | ||
| + | const dt = Math.max(now | ||
| + | velocity | ||
| + | scrollY | ||
| + | lastDragY = e.clientY; lastDragT = now; | ||
| + | render(); | ||
| + | }); | ||
| + | window.addEventListener(' | ||
| + | if (!isDragging) return; | ||
| + | isDragging = false; | ||
| + | wrap.style.cursor = ' | ||
| + | startMomentum(); | ||
| + | }); | ||
| + | |||
| + | // TOUCH | ||
| + | wrap.addEventListener(' | ||
| + | momentumRunning = false; velocity = 0; | ||
| + | isDragging = true; lastDragY = e.touches[0].clientY; | ||
| + | }, { passive: true }); | ||
| + | wrap.addEventListener(' | ||
| + | if (!isDragging) return; | ||
| + | const now = performance.now(); | ||
| + | const dy = e.touches[0].clientY | ||
| + | velocity | ||
| + | scrollY | ||
| + | lastDragY = e.touches[0].clientY; | ||
| + | render(); | ||
| + | }, { passive: true }); | ||
| + | wrap.addEventListener(' | ||
| + | |||
| + | // ── SEARCH ───────────────────────────────────────────── | ||
| + | document.getElementById(' | ||
| + | filter = e.target.value; | ||
| + | render(); | ||
| + | }); | ||
| + | |||
| + | // ── INIT ─────────────────────────────────────────────── | ||
| + | window.addEventListener(' | ||
| + | // start scroll so first item is near center | ||
| + | scrollY = -(document.getElementById(' | ||
| + | render(); | ||
| + | |||
| + | })(); | ||
| + | </ | ||
| + | </ | ||
| + | </html> | ||
| </ | </ | ||