diff --git a/docs/index.html b/docs/index.html index ea4511df0..c9a866b7f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,8 +1,8 @@ - +
@@ -11,88 +11,127 @@
- - + + - + 初识算法 - + + 复杂度 - + + 数组与链表 - + + 栈与队列 - + + 哈希表 - + + 树 - + + 堆 - + + 图 - + + 搜索 - + + 排序 - + + 分治 - + + 回溯 - + + 动态规划 - + + 贪心 @@ -139,9 +178,9 @@
- Cover + Cover
-
-
-
- + diff --git a/en/docs/index.html b/en/docs/index.html index 59d13c12e..f8be1c6c2 100644 --- a/en/docs/index.html +++ b/en/docs/index.html @@ -1,8 +1,8 @@ - +
@@ -11,88 +11,127 @@ -
-
-
- + diff --git a/ja/docs/index.html b/ja/docs/index.html index fa8666841..bb51d2b84 100644 --- a/ja/docs/index.html +++ b/ja/docs/index.html @@ -1,8 +1,8 @@ - +
@@ -11,88 +11,127 @@ -
-
-
- + diff --git a/ko/docs/index.html b/ko/docs/index.html index ee7f1617a..9460f0145 100644 --- a/ko/docs/index.html +++ b/ko/docs/index.html @@ -1,8 +1,8 @@ - +
@@ -11,88 +11,127 @@ -
-
-
- + diff --git a/overrides/homepage/index.html.j2 b/overrides/homepage/index.html.j2 index 1c19e75f4..fdc4014f3 100644 --- a/overrides/homepage/index.html.j2 +++ b/overrides/homepage/index.html.j2 @@ -1,13 +1,14 @@ {% import 'icons.html.j2' as icons %} - +
@@ -77,15 +78,25 @@ {% for person in group.profiles %} {% endfor %} - {% else %}{{ group.banner.alt }}{% endif %} + {% else %}{{ group.banner.alt }}{% endif %} {% endfor %}
+ + diff --git a/overrides/javascripts/hero-planets.js b/overrides/javascripts/hero-planets.js index fffa549a8..853b4c5bf 100644 --- a/overrides/javascripts/hero-planets.js +++ b/overrides/javascripts/hero-planets.js @@ -2,7 +2,7 @@ import * as THREE from "./vendor/three/three.module.min.js"; import { planets, createPlanet, loadPlanetPaint, disposePlanetPaint } from "./hero-surfaces.js"; // The illustration supplies composition; each globe is independently lit and rotates fully. -export function createPlanets(map, items, onRestore) { +export function createPlanets(map, items, onRestore, restoreImage = () => {}) { const targets=planets.map(d=>({...d,item:items.find(i=>i.image.dataset.heroAsset === 'chapter_'+d.name)})).filter(d=>d.item); const hero=map.closest('.home-div'); const renderer=new THREE.WebGLRenderer({alpha:true,antialias:true,powerPreference:'low-power'}); @@ -12,8 +12,9 @@ export function createPlanets(map, items, onRestore) { const scene=new THREE.Scene(),camera=new THREE.OrthographicCamera(-1,1,1,-1,.1,5000); camera.position.z=2000; map.insertBefore(renderer.domElement,map.querySelector(':scope > a')); - let width=0,height=0,lost=false,disposed=false,ready=false,lastTime=0; - const showOriginal=show=>{ + let width=0,height=0,lost=false,disposed=false,ready=false,lastTime=0,startTime=null; + const showOriginal=(show,load=false)=>{ + if(load)targets.forEach(({item})=>restoreImage(item.image)); renderer.domElement.hidden=show;hero.classList.toggle('hero-three-active',!show); targets.forEach(({item})=>item.image.classList.toggle('hero-replaced',!show)); }; @@ -27,14 +28,15 @@ export function createPlanets(map, items, onRestore) { } function render(time,enabled,pointer={x:0,y:0}){ if(disposed)return; - if(!enabled||lost||!ready||!width||!height){showOriginal(true);return;} + if(!enabled||lost||!ready||!width||!height){showOriginal(true,!enabled||lost);return;} + if(startTime===null)startTime=time; const emphasisBlend=time===lastTime?1:1-Math.exp(-Math.min(.1,Math.abs(time-lastTime))*10);lastTime=time; for(const target of targets){ const {item,rect,root,sphere,material}=target,selected=item.anchor.classList.contains('hero-selected'); root.scale.setScalar(rect.height/Number(item.image.dataset.sourceHeight || item.image.naturalHeight)); root.position.set(rect.x+rect.width/2+item.x-width/2,height/2-rect.y-rect.height/2-item.y,0); root.rotation.set(pointer.y*.035,pointer.x*.045,-item.angle*Math.PI/180); - sphere.rotation.y=time*Math.PI*2/target.period; + sphere.rotation.y=(time-startTime)*Math.PI*2/target.period; if(target.moon)target.moon.rotation.y=sphere.rotation.y*2; material.uniforms.emphasis.value+=((selected?1:0)-material.uniforms.emphasis.value)*emphasisBlend; } @@ -42,18 +44,18 @@ export function createPlanets(map, items, onRestore) { Object.assign(renderer.domElement.dataset,{drawCalls:String(renderer.info.render.calls),triangles:String(renderer.info.render.triangles), art:'reference-authored-albedo',network:'original-brushwork',rotation:'continuous-360'}); } - const onLost=event=>{event.preventDefault();lost=true;showOriginal(true);}; + const onLost=event=>{event.preventDefault();lost=true;showOriginal(true,true);}; const onRestored=()=>{lost=false;resize();onRestore();}; renderer.domElement.addEventListener('webglcontextlost',onLost);renderer.domElement.addEventListener('webglcontextrestored',onRestored); showOriginal(true);resize(); - const readyPromise=Promise.allSettled(targets.map(async t=>{await t.item.image.decode();const map=await loadPlanetPaint(t);if(disposed){disposePlanetPaint(map);return;}t.paintMap=map;})).then(results=>{ + const readyPromise=Promise.allSettled(targets.map(async t=>{const map=await loadPlanetPaint(t);if(disposed){disposePlanetPaint(map);return;}t.paintMap=map;})).then(results=>{ const failure=results.find(r=>r.status==='rejected');if(failure)throw failure.reason; if(disposed)return; for(const target of targets){Object.assign(target,createPlanet(target.item.image,target,target.paintMap));scene.add(target.root);} ready=true;resize();onRestore(); - }).catch(error=>{if(!disposed){targets.forEach(t=>{disposePlanetPaint(t.paintMap);t.paintMap=null;});showOriginal(true);console.warn('Original planets retained: paint unavailable.',error);}}); + }).catch(error=>{if(!disposed){targets.forEach(t=>{disposePlanetPaint(t.paintMap);t.paintMap=null;});showOriginal(true,true);console.warn('Original planets retained: paint unavailable.',error);}}); return {resize,render,ready:readyPromise,dispose(){ - disposed=true;showOriginal(true); + disposed=true;showOriginal(true,true); renderer.domElement.removeEventListener('webglcontextlost',onLost);renderer.domElement.removeEventListener('webglcontextrestored',onRestored); const geometries=new Set(),materials=new Set();scene.traverse(o=>{if(o.geometry)geometries.add(o.geometry);if(o.material)materials.add(o.material);}); geometries.forEach(g=>g.dispose());materials.forEach(m=>m.dispose());targets.forEach(t=>disposePlanetPaint(t.paintMap));renderer.dispose();renderer.forceContextLoss();renderer.domElement.remove(); diff --git a/overrides/javascripts/homepage-media.js b/overrides/javascripts/homepage-media.js index a99f0e421..dff141703 100644 --- a/overrides/javascripts/homepage-media.js +++ b/overrides/javascripts/homepage-media.js @@ -1,16 +1,36 @@ -/* Below-fold demos load near the viewport and never animate in reduced motion. */ +/* Load the first view first, then prepare below-fold media without requiring scroll. */ (() => { const reduced = matchMedia('(prefers-reduced-motion: reduce)'); - let observer; const videos = [...document.querySelectorAll('[data-home-video]')]; const images = [...document.querySelectorAll('[data-home-src]')]; const visible = new Set(); + let observer, preloadTask = null; + + // A broken or stalled resource must not block everything below it. + function settled(element, events, start, timeout = 10000) { + return new Promise(resolve => { + const finish = () => { + clearTimeout(timer); + events.forEach(event => element.removeEventListener(event, finish)); + resolve(); + }; + const timer = setTimeout(finish, timeout); + events.forEach(event => element.addEventListener(event, finish)); + start(); + if (element instanceof HTMLImageElement && element.complete) finish(); + if (element instanceof HTMLVideoElement && element.readyState >= 4) finish(); + }); + } function reveal(element) { - if (element.dataset.homeSrc && !element.getAttribute('src')) element.src = element.dataset.homeSrc; + if (element.dataset.homeSrc && !element.getAttribute('src')) { + element.loading = 'eager'; + element.src = element.dataset.homeSrc; + } if (element.dataset.homePoster && !element.getAttribute('poster')) element.poster = element.dataset.homePoster; } function load(video) { if (!video.getAttribute('src')) { + video.preload = 'auto'; video.src = video.dataset.homeVideo; video.load(); } @@ -18,12 +38,52 @@ function update() { for (const video of videos) { if (document.hidden || reduced.matches || !visible.has(video)) video.pause(); - else { load(video); video.play().catch(() => {}); } + else { reveal(video); load(video); video.play().catch(() => {}); } + } + } + async function foreground() { + if (!document.hidden) return; + await new Promise(resolve => { + const resume = () => { + if (document.hidden) return; + document.removeEventListener('visibilitychange', resume); + resolve(); + }; + document.addEventListener('visibilitychange', resume); + }); + } + function preload() { + if (!preloadTask) preloadTask = prepare().finally(() => { preloadTask = null; }); + return preloadTask; + } + async function prepare() { + const heroReady = document.querySelector('.home-universe-bg')?.homeReady; + // Covers missing scene scripts or a stalled texture without holding the page forever. + await new Promise(resolve => { + const timer = setTimeout(resolve, 10000); + Promise.resolve(heroReady).then(() => { clearTimeout(timer); resolve(); }); + }); + // Still images in document order first, so videos cannot delay portraits and covers. + for (const element of document.querySelectorAll('[data-home-src], [data-home-poster]')) { + await foreground(); + if (element.dataset.homePoster) { + const poster = new Image(); + await settled(poster, ['load', 'error'], () => { reveal(element); poster.src = element.dataset.homePoster; }); + } else { + await settled(element, ['load', 'error'], () => reveal(element)); + } + } + for (const video of videos) { + await foreground(); + if (reduced.matches) continue; + // preload is a browser hint; it may limit buffering on constrained connections. + await settled(video, ['canplaythrough', 'error'], () => load(video)); } } function start() { observer?.disconnect(); - if (!('IntersectionObserver' in window)) { [...images, ...videos].forEach(reveal); return; } + if (!('IntersectionObserver' in window)) return; + // Scrolling ahead bypasses the background queue; playback requires actual visibility. observer = new IntersectionObserver(entries => { for (const entry of entries) { const element = entry.target; @@ -33,12 +93,19 @@ } else if (entry.isIntersecting) observer.unobserve(element); } update(); - }, {rootMargin: '120px 0px'}); + }); [...images, ...videos].forEach(element => observer.observe(element)); } document.addEventListener('visibilitychange', update); - reduced.addEventListener('change', update); + reduced.addEventListener('change', () => { + update(); + if (!reduced.matches) (preloadTask || Promise.resolve()).then(() => { + if (!reduced.matches) preload(); + }); + }); addEventListener('pagehide', () => {observer?.disconnect();visible.clear();update();}); addEventListener('pageshow', start); + if (document.readyState === 'complete') preload(); + else addEventListener('load', preload, {once: true}); start(); })(); diff --git a/overrides/javascripts/starfield.js b/overrides/javascripts/starfield.js index 27130ef3c..70acd0f44 100644 --- a/overrides/javascripts/starfield.js +++ b/overrides/javascripts/starfield.js @@ -3,6 +3,9 @@ "use strict"; let dispose = () => {}; + const restoreImage = image => { + if (image?.dataset.heroFallback && !image.getAttribute('src')) image.src = image.dataset.heroFallback; + }; const moduleBase = new URL(".", document.currentScript.src); function setup(config = {}) { @@ -11,6 +14,9 @@ const hero = container?.closest(".home-div"); const map = hero?.querySelector(".hero-div"); if (!map) return; + // Below-fold preloading waits for the enhancement (or its static fallback). + let settleReady; + hero.homeReady = new Promise(resolve => { settleReady = resolve; }); const reduced = matchMedia("(prefers-reduced-motion: reduce)"); const finePointer = matchMedia("(hover: hover) and (pointer: fine)"); @@ -74,11 +80,13 @@ function renderObjects() { const enabled = !reduced.matches && !listLayout(); + if (!enabled) items.forEach(item => restoreImage(item.image)); if (planets) { try { planets.render(time, enabled, pointer); } catch (error) { planets.dispose(); planets = null; + items.filter(item => item.image.dataset.heroAsset !== 'chapter_computational_complexity').forEach(item => restoreImage(item.image)); console.warn("Planet rendering unavailable; keeping the original illustrations.", error); } } @@ -88,18 +96,25 @@ function loadObjects() { if (objectsRequested || destroyed || !visible || reduced.matches || listLayout()) return; objectsRequested = true; - import(new URL("hero-planets.js", moduleBase).href).then(module => { + const planetReady = import(new URL("hero-planets.js", moduleBase).href).then(module => { if (destroyed) return; - planets = module.createPlanets(map, items, updatePlayback); + planets = module.createPlanets(map, items, updatePlayback, restoreImage); renderObjects(); resize(); - }).catch(error => console.warn("3D enhancement unavailable; keeping the original planets.", error)); - import(new URL("hero-hourglass.js", moduleBase).href).then(module => { + return planets.ready; + }).catch(error => { + items.filter(item => item.image.dataset.heroAsset !== 'chapter_computational_complexity').forEach(item => restoreImage(item.image)); + console.warn("3D enhancement unavailable; loading fallback planets.", error); + }); + const hourglassReady = import(new URL("hero-hourglass.js", moduleBase).href).then(module => { if (destroyed) return; hourglass = module.createHourglass(items); renderObjects(); - }).catch(error => console.warn("Hourglass enhancement unavailable; keeping the original illustration.", error)); - + }).catch(error => { + restoreImage(items.find(item => item.image.dataset.heroAsset === 'chapter_computational_complexity')?.image); + console.warn("Hourglass enhancement unavailable; loading fallback illustration.", error); + }); + Promise.allSettled([planetReady, hourglassReady]).then(settleReady); } function drawStars() { @@ -287,11 +302,13 @@ const intersectionObserver = new IntersectionObserver(entries => { visible = entries[0].isIntersecting; updatePlayback(); + if (!objectsRequested && (!visible || reduced.matches || listLayout())) settleReady(); }); intersectionObserver.observe(hero); dispose = () => { destroyed = true; + settleReady(); cancelAnimationFrame(raf); resizeObserver.disconnect(); intersectionObserver.disconnect(); @@ -317,5 +334,12 @@ } // Retain the build hook's entry point; all motion shares one cancellable loop. - root.Starfield = { setup, cleanup: () => dispose() }; + root.Starfield = { setup: config => { + try { setup(config); } + catch (error) { + dispose(); + document.querySelectorAll('[data-hero-fallback]').forEach(restoreImage); + console.warn('Scene unavailable; loading fallback illustrations.', error); + } + }, cleanup: () => dispose() }; })(window); diff --git a/overrides/stylesheets/hero-motion.css b/overrides/stylesheets/hero-motion.css index 1afd96cf1..0f7c58b87 100644 --- a/overrides/stylesheets/hero-motion.css +++ b/overrides/stylesheets/hero-motion.css @@ -178,12 +178,17 @@ } } -/* The PNG boxes remain the native link hit targets and responsive layout anchors. */ +/* Metadata-only boxes reserve layout and native hit targets without downloading fallback art. */ +.hero-div > a > noscript > img { width: auto; position: absolute; } +.hero-div img[data-hero-fallback]:not([src]) { opacity: 0 !important; } + +@keyframes hero-object-reveal { from { opacity: 0; } to { opacity: 1; } } .hero-universe .hero-div > a > img.hero-replaced { opacity: 0; } .hero-universe .hero-objects-canvas { + animation: hero-object-reveal 400ms ease-out both; position: absolute; inset: 0; width: 100%; @@ -240,6 +245,7 @@ .home-div > :is(.hero-div--en, .hero-div--ru, .hero-div--ja) { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .35rem .5rem; width: calc(100% - 1.5rem); height: auto; margin: .5rem auto 1rem; } .home-div > :is(.hero-div--en, .hero-div--ru, .hero-div--ja) > img { display: none; } .home-div > :is(.hero-div--en, .hero-div--ru, .hero-div--ja) > a { display: flex; align-items: center; gap: .4rem; min-width: 0; min-height: 48px; padding: .3rem .15rem; text-align: left; } + .home-div > :is(.hero-div--en, .hero-div--ru, .hero-div--ja) > a > noscript > img, .home-div > :is(.hero-div--en, .hero-div--ru, .hero-div--ja) > a > img { position: static; flex-shrink: 0; width: 32px !important; height: 32px !important; object-fit: contain; } .home-div > :is(.hero-div--en, .hero-div--ru, .hero-div--ja) > a > span { position: static; min-width: 0; transform: none; white-space: normal; font-size: 13px; line-height: 1.3; } .home-div > :is(.hero-div--en, .hero-div--ru, .hero-div--ja) + .heading-div { position: relative; left: auto; bottom: auto; transform: none; padding: 0 .75rem; } diff --git a/ru/docs/index.html b/ru/docs/index.html index a61dba915..6b49e277f 100644 --- a/ru/docs/index.html +++ b/ru/docs/index.html @@ -1,8 +1,8 @@ - +
@@ -11,88 +11,127 @@ - -
- + diff --git a/zh-hant/docs/index.html b/zh-hant/docs/index.html index ccbf8510d..31e4ae734 100644 --- a/zh-hant/docs/index.html +++ b/zh-hant/docs/index.html @@ -1,8 +1,8 @@ - +
@@ -11,88 +11,127 @@
-
-
- +