Skip to content

Commit d182ef2

Browse files
author
shih
committed
feat(unit-4.3): fill experiment width and integrate its controls
1 parent 1e31f01 commit d182ef2

6 files changed

Lines changed: 45 additions & 45 deletions

File tree

‎docs/decisions/unit-4.3-entry-detail.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,14 @@
6868
- 新增8種視窗寬高組合的行為檢查,從全景標題對齊可視區後,整座圖、角度控制、發射/暫停和開啟的近拍窗均在畫面內,手機計入頂部黏著導覽;與既有7種寬度及完整操作一同回歸。
6969

7070
沒有變更素材、光學、播放模型或控制器,其他教學區保持。維持第四階段修訂,待使用者檢視。
71+
72+
## 用滿全景寬度、整合工具與精簡近拍(2026-11-02)
73+
74+
使用者指出中央小圖周圍過多空白,要求在同屏操作前提下盡量放大全景,介質切換整合到全景右上方。本輪取代先前560px限寬方式。
75+
76+
- 全景與下方設定列同寬,不再預留左右大塊留白。使用原圖y180–960的寬幅視窗,讓水槽與光源放大,移除木桌等外圍裝飾;照片與光學SVG保持同一等比例變換,角度和光路不拉伸。
77+
- 實驗標題與水/玻璃切換放在全景上緣左右角,桌機疊於圖上;窄版合併在圖前同一標題列,避免工具被近拍窗遮擋。載入/播放狀態移到圖左下角,操作提示併入下方狀態。
78+
- 中央近拍精簡為薄框影像及角落關閉鈕,省去可見標題列與重複角度列;標題仍作為輔助閱讀名稱。唯一角度讀值統一放在全景下方,對應藍綠扇形。空氣標籤下移並與關閉鈕錯開。
79+
- 七種寬度檢查要求圖框與設定列等寬;八種寬高檢查持續要求完整觀察與播放控制可同屏使用。新窗口移動文字群組後,以實際座標變換檢查文字範圍,包含手機字級、點位對齊與控制不被遮擋。
80+
81+
沒有變更物理與播放模型、素材、其他教學區或引入新依賴。維持第四階段修訂,待使用者檢視。

‎tests/unit-4.3-oblique-lab.test.cjs‎

Lines changed: 4 additions & 4 deletions
Large diffs are not rendered by default.

‎units/4/parts/4.3/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,7 @@ node --test --test-concurrency=1 --test-reporter=tap tests/unit-4.3-normal-model
130130
2026-11-02 操作區高度修訂:依使用者回饋縮短第三區,桌機全景由最高760px正方形改為最高約560×482px,並依視窗高度進一步等比例縮小。背景與光學SVG使用相同裁切,隱去原圖y0–75與1155–1254的上下留白,水槽、桌腳、光路及角度不拉伸。窄版保留原方形比例及可讀尺寸;收緊標題、滑桿/讀值、播放控制與解說之間的間距。
131131

132132
中央放大仍在同一入射點開啟,尺寸與座標隨縮圖同步更新。新增8種寬高組合,確認從全景標題開始觀看時,水槽、調角度及發射/暫停按鈕能同時位於視窗內,開窗亦不擋控制;包含1366×768筆電、1440×650矮視窗與320×700手機。光學及播放模型、素材與控制器保持。
133+
134+
2026-11-02 全景用滿寬度與整合切換:依使用者回饋,第三區不再把整張器材等比例縮成中央小圖;全景改為與設定列同寬,裁切聚焦原圖y180至960,移除上方木板標牌及下方木桌裝飾,保留水槽、光源與全部光路。背景與SVG使用相同等比例變換,模型與素材保持。水/玻璃切換整合到全景右上角;窄版放在同一全景標題列右側,保持可點且不遮住中央近拍。
135+
136+
中央近拍改為薄框,只保留圖與關閉鈕。角度只在全景下方分色讀值呈現,移除窗口內重複讀值;近拍標題仍提供輔助閱讀名稱。空氣標籤移開關閉鈕,近拍與原入射點仍精確對齊,原播放、鍵盤、拖曳與觸控功能保持。提示併入操作狀態,避免另占整列。原7種寬度及8種寬高檢查更新為確認全景用滿內容寬度、切換在右側、讀值不重複、控制可同屏操作;角度標籤邊界檢查計入父群組的實際變換。

‎units/4/parts/4.3/assets/oblique-incidence-lab.css‎

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
/* A single experiment and a magnified view of the same entry point. */
22
#s3 .lesson-body { container-type:inline-size; container-name:oblique-card; }
33
#oblique-lab { --oblique-in:#165fc2; --oblique-out:#24783a; padding:0; border:0; background:transparent; }
4-
.oblique-topline { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:8px 14px; margin:0 0 12px; }
5-
.oblique-topline .oblique-hint { flex:1 1 280px; margin:0; }
6-
.oblique-toolbar { display:flex; justify-content:flex-end; align-items:center; gap:9px; margin:0; color:#315340; font-size:.87rem; font-weight:650; }
7-
.oblique-toolbar button { min-width:68px; min-height:42px; padding:7px 16px; }
8-
.oblique-hint { margin:0 0 12px; color:#607366; font-size:.8rem; }
4+
.oblique-toolbar { display:flex; align-items:center; gap:6px; margin:0; padding:3px 5px; border:1px solid #d2dfcf; border-radius:9px; background:#fffef7f2; color:#315340; font-size:.78rem; font-weight:650; box-shadow:0 1px 5px #263c2710; }
5+
.oblique-toolbar button { min-width:52px; min-height:36px; padding:4px 12px; }
96
.oblique-experiment { position:relative; }
107
.oblique-settings { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:6px 18px; align-items:center; margin-top:4px; }
11-
.oblique-overview { min-width:0; width:min(100%,560px,calc((100svh - 285px) * 1.16111)); margin:0 auto; display:flex; flex-direction:column; }
12-
.oblique-overview figcaption { display:flex; align-items:center; height:28px; margin:0 0 4px; color:#28523e; font-size:1rem; font-weight:750; }
13-
.oblique-table { position:relative; width:100%; aspect-ratio:1254 / 1080; overflow:hidden; border-radius:12px; background:#fffdf7; }
8+
.oblique-overview { position:relative; min-width:0; width:100%; margin:0; }
9+
.oblique-overview figcaption { position:absolute; inset:8px 8px auto; z-index:2; display:flex; justify-content:space-between; align-items:flex-start; gap:8px; margin:0; color:#28523e; font-size:.9rem; font-weight:750; pointer-events:none; }
10+
.oblique-overview-title { padding:4px 8px; border-radius:6px; background:#fffef7ed; }
11+
.oblique-overview figcaption .oblique-toolbar { pointer-events:auto; }
12+
.oblique-table { position:relative; width:100%; aspect-ratio:1254 / 780; overflow:hidden; border-radius:12px; background:#fffdf7; }
1413
#oblique-background { display:block; }
15-
#oblique-background, #oblique-scene { position:absolute; left:0; top:-6.944444%; width:100%; height:116.111111%; }
14+
#oblique-background, #oblique-scene { position:absolute; left:0; top:-23.076923%; width:100%; height:160.769231%; }
15+
#oblique-case-label { display:none; }
1616
#oblique-scene text, #oblique-zoom text { font-family:ui-sans-serif,system-ui,"Noto Sans TC",sans-serif; fill:#233d35; font-weight:650; }
1717
.oblique-normal { fill:none; stroke:#737e76; stroke-width:3; stroke-dasharray:12 9; }
1818
.oblique-angle-sector { stroke:none; }
@@ -36,26 +36,20 @@
3636
.oblique-source:focus-visible { outline:none; }
3737
.oblique-source:focus-visible .oblique-focus, .oblique-source:hover .oblique-focus { opacity:1; }
3838
.oblique-source[data-lit=true] .oblique-lens { fill:url(#oblique-lit-lens); }
39-
.oblique-state { position:absolute; right:3%; top:1%; padding:3px 7px; border:1px solid #b6cab3; border-radius:7px; background:#f2f8eaf5; color:#28654f; font-size:.66rem; font-weight:700; pointer-events:none; }
39+
.oblique-state { position:absolute; left:8px; bottom:8px; padding:3px 7px; border:1px solid #b6cab3; border-radius:7px; background:#f2f8eaf5; color:#28654f; font-size:.66rem; font-weight:700; pointer-events:none; }
4040
#oblique-magnify { position:absolute; z-index:2; width:44px; height:44px; min-height:44px; padding:0; transform:translate(-50%,-50%); border:2px solid #fffce9; border-radius:5px; background:#fffae51a; outline:1px solid #2a7350; box-shadow:0 0 0 3px #ffffed55; cursor:zoom-in; }
4141
#oblique-magnify:hover, #oblique-magnify:focus-visible { background:#fff4ba55; outline:3px solid #246747; }
4242
#oblique-magnify:disabled { cursor:default; opacity:.45; }
4343
#oblique-lab[data-detail-open=true] #oblique-magnify, #oblique-lab[data-detail-open=true] .oblique-magnify-label { opacity:0; pointer-events:none; }
4444
.oblique-magnify-label { position:absolute; z-index:2; left:var(--oblique-entry-x); top:var(--oblique-entry-y); transform:translate(32px,-28px); pointer-events:none; display:inline-flex; align-items:center; gap:5px; padding:6px 9px; border:1px solid #b9cdb7; border-radius:7px; background:#fffef4f5; color:#23583f; font-size:.8rem; font-weight:700; white-space:nowrap; box-shadow:0 2px 7px #35463212; }
4545
.oblique-magnify-label svg { width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:2; }
46-
.oblique-detail { position:absolute; z-index:3; width:clamp(200px,44%,340px); max-width:calc(100% - 24px); min-width:0; padding:10px; border:1px solid #8dac96; border-radius:12px; background:#fffef8; box-shadow:0 8px 28px #1d302735,0 2px 5px #1d30271a; }
46+
.oblique-detail { position:absolute; z-index:3; width:clamp(180px,40%,320px); max-width:calc(100% - 24px); min-width:0; padding:6px; border:1px solid #8dac96; border-radius:12px; background:#fffef8; box-shadow:0 8px 28px #1d302735,0 2px 5px #1d30271a; }
4747
.oblique-detail[hidden] { display:none; }
48-
.oblique-detail-readouts { display:grid; grid-template-columns:1fr 1fr; align-items:center; gap:5px; min-height:44px; margin-top:6px; font-size:.8rem; font-weight:700; }
49-
.oblique-detail-readouts > span { display:flex; flex-wrap:wrap; align-items:baseline; justify-content:center; gap:1px 5px; }
50-
.oblique-detail-readouts output { color:inherit; font-size:1.05rem; font-weight:750; font-variant-numeric:tabular-nums; }
51-
#oblique-close { display:grid; place-items:center; flex:none; width:32px; height:32px; min-width:32px; min-height:32px; padding:0; border:0; border-radius:50%; background:#e9f1e4; color:#2d5744; font-size:1.35rem; line-height:1; }
48+
#oblique-close { position:absolute; top:4px; right:4px; z-index:1; display:grid; place-items:center; flex:none; width:32px; height:32px; min-width:32px; min-height:32px; padding:0; border:0; border-radius:50%; background:#e9f1e4; color:#2d5744; font-size:1.35rem; line-height:1; }
5249
#oblique-close:hover, #oblique-close:focus-visible { background:#d9e9d3; outline:2px solid #5b8869; outline-offset:1px; }
5350
#oblique-lab[aria-busy=true] .oblique-detail #oblique-zoom { opacity:.45; }
5451

55-
.oblique-detail-heading { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:5px 6px; height:32px; margin-bottom:6px; }
56-
.oblique-detail-heading h3 { display:flex; align-items:center; gap:6px; margin:0; color:#25573f; font-size:.9rem; font-weight:750; }
57-
.oblique-detail-heading svg { width:20px; height:20px; fill:none; stroke:currentColor; stroke-width:2; }
58-
.oblique-detail-heading > span { color:#68796c; font-size:.72rem; }
52+
.oblique-detail-heading { height:0; margin:0; }
5953
#oblique-zoom { display:block; width:100%; height:auto; aspect-ratio:4 / 3; overflow:hidden; border:1px solid #dfe5d7; border-radius:10px; background:#fffdf6; }
6054
#oblique-zoom .oblique-normal { stroke:#526559; stroke-width:1.3; stroke-dasharray:5 4; }
6155
.oblique-normal-underlay { fill:none; stroke:#fffef5; stroke-opacity:.85; stroke-width:2.5; stroke-dasharray:5 4; }
@@ -105,31 +99,27 @@
10599
.oblique-model-note { padding-top:12px; border-top:1px solid #e1e9dc; color:#64736d; font-size:.76rem; }
106100
#oblique-retry[hidden] { display:none; }
107101
@container oblique-card (max-width:680px) {
108-
.oblique-overview { width:min(100%,520px,calc((100svh - 340px) * 1.16111)); }
109102
.oblique-console { grid-template-columns:1fr; gap:8px; margin-top:8px; padding-top:10px; }
110103
.oblique-actions { display:grid; grid-template-columns:1fr 1fr; }
111104
.oblique-status { grid-column:1; }
112105
.oblique-cart-pan-hint { display:block; }
113106
}
114107
@media(max-width:700px) {
115-
.oblique-toolbar { justify-content:flex-start; }
116108
.oblique-actions button { font-size:.86rem; padding-inline:8px; }
117109
.oblique-angle-control { gap:10px; font-size:.86rem; }
118110
.oblique-explanation summary { padding:15px 13px; font-size:.95rem; }
119111
.oblique-explanation-body { padding:14px 13px; }
120112
}
121113
@container oblique-card (max-width:520px) {
122-
.oblique-overview { width:100%; max-width:420px; }
123-
.oblique-table { aspect-ratio:1; }
124-
#oblique-background, #oblique-scene { top:0; height:100%; }
114+
.oblique-overview figcaption { position:static; height:46px; margin-bottom:4px; align-items:center; pointer-events:auto; }
115+
.oblique-overview-title { padding:0; background:transparent; }
116+
.oblique-toolbar { border:0; padding:0; box-shadow:none; background:transparent; }
117+
.oblique-toolbar > span { display:none; }
118+
.oblique-toolbar button { min-width:46px; min-height:40px; }
125119
.oblique-settings { grid-template-columns:1fr; gap:0; }
126120
.oblique-angle-reading { flex-direction:column; align-items:center; gap:3px; }
127121
.oblique-angle-readouts { padding-top:6px; }
128122
.oblique-magnify-label { transform:translate(-50%,calc(-100% - 30px)); }
129-
.oblique-detail { padding:8px; }
130-
.oblique-detail-heading h3 { font-size:.8rem; }
131-
.oblique-detail-heading h3 svg { display:none; }
132-
.oblique-detail-readouts > span { flex-direction:column; align-items:center; }
133123
.oblique-rule-divider { display:none; }
134124
#oblique-zoom-note { flex-basis:100%; margin-top:3px; }
135125
}

‎units/4/parts/4.3/assets/oblique-incidence-lab.mjs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ zoom.innerHTML=`<defs>
5757
<g class="oblique-angle-marks" visibility="hidden"><path class="oblique-angle-arc oblique-in-arc"/><path class="oblique-angle-arc oblique-out-arc"/></g>
5858
${beam()}
5959
<g class="oblique-zoom-labels">
60-
<g><rect x="657" y="458" width="43" height="24" rx="4"/><text class="oblique-normal-name" x="662" y="500">法線</text></g>
61-
<g><rect x="719" y="458" width="43" height="24" rx="4"/><text x="740" y="500" text-anchor="middle">空氣</text></g>
60+
<g><rect x="657" y="458" width="43" height="24" rx="4"/><text class="oblique-normal-name" x="662" y="506">法線</text></g>
61+
<g transform="translate(0 50)"><rect x="719" y="458" width="43" height="24" rx="4"/><text x="740" y="506" text-anchor="middle">空氣</text></g>
6262
<g><rect x="539" y="645" width="48" height="25" rx="4"/><text id="oblique-zoom-medium" x="546" y="648">水</text></g>
6363
<g><rect x="534" y="552" width="48" height="25" rx="4"/><text x="558" y="570" text-anchor="middle">介面</text></g>
6464
</g>`;
@@ -84,7 +84,7 @@ function drawGeometry(){
8484
const hit=source.querySelector('rect');const hitWidth=Math.max(96,44/(ratio*sourceScale));hit.setAttribute('x',String(-hitWidth/2));hit.setAttribute('width',String(hitWidth));
8585
scene.querySelector('#oblique-case-label').setAttribute('transform',`translate(650 155) scale(${fontScale})`);scene.querySelector('#oblique-case-name').textContent=`空氣 → ${name()}`;
8686
scene.querySelector('#oblique-air-label').setAttribute('transform',`translate(${1030-110*fontScale} 440) scale(${fontScale})`);scene.querySelector('#oblique-material-label').setAttribute('transform',`translate(230 740) scale(${fontScale})`);scene.querySelector('#oblique-material-label text').textContent=name();
87-
for(const kind of ['sector','base']){zoom.querySelector(`.oblique-in-${kind}`).setAttribute('d',sector(angle,true));zoom.querySelector(`.oblique-out-${kind}`).setAttribute('d',sector(g.outgoing,false));}zoom.querySelector('.oblique-in-arc').setAttribute('d',arc(angle,true));zoom.querySelector('.oblique-out-arc').setAttribute('d',arc(g.outgoing,false));$('#oblique-in-value').value=$('#oblique-detail-in-value').value=`${angle}°`;$('#oblique-out-value').value=$('#oblique-detail-out-value').value=`${g.outgoing.toFixed(1)}°`;zoom.querySelector('#oblique-zoom-medium').textContent=name();
87+
for(const kind of ['sector','base']){zoom.querySelector(`.oblique-in-${kind}`).setAttribute('d',sector(angle,true));zoom.querySelector(`.oblique-out-${kind}`).setAttribute('d',sector(g.outgoing,false));}zoom.querySelector('.oblique-in-arc').setAttribute('d',arc(angle,true));zoom.querySelector('.oblique-out-arc').setAttribute('d',arc(g.outgoing,false));$('#oblique-in-value').value=`${angle}°`;$('#oblique-out-value').value=`${g.outgoing.toFixed(1)}°`;zoom.querySelector('#oblique-zoom-medium').textContent=name();
8888
zoom.style.setProperty('--zoom-font-scale',String(Math.max(1,14*240/(11*Math.max(80,zoom.getBoundingClientRect().width||200)))));
8989
for(const group of detail.hidden?[]:zoom.querySelectorAll('.oblique-zoom-labels > g')){const box=group.querySelector('text').getBBox(),plate=group.querySelector('rect');for(const [key,value] of Object.entries({x:box.x-4,y:box.y-3,width:box.width+8,height:box.height+6}))plate.setAttribute(key,String(value));}
9090
const description=`空氣射入${name()},入射角${angle}度,折射角${g.outgoing.toFixed(1)}度。法線通過入射點;角度皆從法線量起。`;
@@ -122,7 +122,7 @@ function controls(){
122122
buttons.forEach(button=>button.setAttribute('aria-pressed',String(button.dataset.obliqueMedium===requestedMedium)));
123123
source.setAttribute('aria-disabled',String(start.disabled));source.setAttribute('tabindex',start.disabled?'-1':'0');start.classList.toggle('primary',!start.disabled&&state!=='running');pause.classList.toggle('primary',['running','paused'].includes(state));pause.dataset.action=state==='paused'?'resume':'pause';pause.querySelector('span').textContent=state==='paused'?'繼續觀察':'暫停觀察';
124124
badge.textContent={loading:'準備場景',error:'場景未就緒',idle:'準備發射',running:'光束穿越中',paused:'已暫停觀察',preview:'光路預覽',complete:'光路已展開'}[state];
125-
status.textContent={loading:'正在準備實驗場景……',error:`${requestedMedium==='water'?'水':'玻璃'}場景未能載入,請重試或切換介質。`,idle:'點燈或按發射光束,觀察入射點。',running:'全景與放大畫面同步前進。',paused:'已暫停,可仔細觀察入射點。',preview:'光路已更新,可重新發射觀察。',complete:'光路已展開,可調角度或切換介質。'}[state];
125+
status.textContent={loading:'正在準備實驗場景……',error:`${requestedMedium==='water'?'水':'玻璃'}場景未能載入,請重試或切換介質。`,idle:'點燈發射,拖曳光源可調角度。',running:'全景與放大畫面同步前進。',paused:'已暫停,可仔細觀察入射點。',preview:'光路已更新,可重新發射觀察。',complete:'光路已展開,可調角度或切換介質。'}[state];
126126
}
127127
function stop(){cancelAnimationFrame(frame);frame=0;}
128128
function tick(now){

0 commit comments

Comments
 (0)