3D / 336

Exploded layers

部品を離して、構造を見せる
三つの模式的な部品を離し、重なって見えなかった構造を説明します。

見本の中を操作して変化を試してください。

ラボで詳しく試す ↗

ひとつ試すなら

どこが変わるかを観察して、似た表現と見比べてみよう。

こんなときに

違いを確かめたり、選択・編集の手応えを設計するとき。

仕組みと使い方

操作前後を見比べてください。ボタンやスライダーでも操作できます。

分解図商品構造
実装コードを見る
<!doctype html><html lang="ja"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{margin:0;min-height:330px}.sample{width:100%;height:100%;min-height:330px;padding:12px!important;display:flex!important;flex-direction:column;justify-content:center;align-items:center;gap:8px!important;background:#f0efeb;color:#232323;font-family:Arial,Meiryo,sans-serif;overflow:hidden;box-sizing:border-box}.sample *{box-sizing:border-box}.lab-scene{width:100%;max-width:430px;height:140px;position:relative;overflow:hidden;border-bottom:1px solid #ccc}.lab-scene:has(canvas){max-width:320px;height:auto;aspect-ratio:16/9;flex-shrink:0}.lab-canvas{display:block;width:100%;height:100%!important;outline-offset:-3px}.lab-canvas:focus-visible{outline:2px solid #222}.lab-controls{width:100%;max-width:390px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 14px}.lab-controls label{display:grid;grid-template-columns:1fr auto;gap:2px;font-size:11px;min-width:0}.lab-controls input{grid-column:1/-1;width:100%;min-width:0;margin:0;accent-color:#333;height:30px}.lab-controls output{font-variant-numeric:tabular-nums}.lab-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:center}.sample .lab-actions button{color:inherit;font:11px Arial,Meiryo,sans-serif;border:1px solid #aaa;border-radius:99px;background:transparent;min-height:44px;padding:8px 12px;cursor:pointer}.lab-status{font-size:10px;line-height:1.4;min-height:14px;margin:0;text-align:center}.lab-note{font-size:9px;line-height:1.3;color:#666;margin:0;text-align:center}.lab-actions button:focus-visible{outline:2px solid #222;outline-offset:2px}@media(prefers-reduced-motion:reduce){.sample *{animation-play-state:paused!important;transition:none!important}}.lab-scene{height:190px;min-height:160px;max-width:430px;border:0}.lab-scene button,.lab-scene input,.lab-scene select{font:12px Arial,Meiryo,sans-serif;color:#232323}.lab-scene button{min-height:36px;border:1px solid #999;background:#fff;padding:5px 9px;border-radius:4px;cursor:pointer}.lab-scene button:focus-visible,.lab-scene [tabindex]:focus-visible,.lab-scene input:focus-visible{outline:3px solid #327b94;outline-offset:-3px}.lab-scene button[aria-pressed=true]{background:#203c48;color:white}.lab-scene .row{display:flex;gap:5px;justify-content:center;flex-wrap:wrap}.picture{position:relative;width:100%;height:100%;overflow:hidden;touch-action:none;background:#c7e7eb}.picture svg{width:100%;height:100%;display:block;object-fit:cover}.picture .art{position:absolute;inset:0}.picture .mono{filter:grayscale(1)}.compare-line{position:absolute;top:0;bottom:0;left:50%;width:2px;background:white;pointer-events:none}.compare-line:after{content:'↔';position:absolute;top:45%;left:-17px;width:36px;height:36px;border-radius:50%;background:#fff;color:#222;display:grid;place-items:center}.lens{position:absolute;width:100px;height:100px;border:3px solid #fff;border-radius:50%;overflow:hidden;pointer-events:none;box-shadow:0 2px 12px #0006}.lens .art{width:430px;height:190px}.pair{height:100%;display:grid;grid-template-columns:1fr 1fr;gap:6px}.pair .picture{border:1px solid #aaa}.product-demo{height:140px;display:grid;place-items:center;perspective:600px}.product-object{width:190px;height:105px;border:1px solid #7897a4;background:linear-gradient(115deg,#e2f2f8,#8bcddd);box-shadow:9px 8px 0 #0001;display:grid;place-content:center;text-align:center;border-radius:8px;color:#164450}.product-object strong{font-size:24px}.product-object small{font-size:9px;margin-top:8px}.pieces{position:relative;width:200px;height:130px}.piece{position:absolute;left:20px;top:38px;width:160px;height:54px;display:grid;place-items:center;border:1px solid #4d727f;background:#b6dae2;color:#164450;font-size:12px;transform:translateY(calc(var(--i)*var(--spread,0)*1px));transition:transform .2s}.piece:nth-child(1){--i:-1;background:#d9edf1}.piece:nth-child(2){--i:0}.piece:nth-child(3){--i:1;background:#82b1be}.hotspot{position:absolute!important;border-radius:50%!important;width:36px;padding:0!important;background:#fff!important;color:#164450!important}.hotspot[aria-pressed=true]{box-shadow:0 0 0 4px #16445055}.annotation{position:absolute;left:4px;right:4px;bottom:0;background:#fff;padding:8px;margin:0;font-size:11px}.edit-items{list-style:none;display:flex;gap:6px;padding:4px;margin:4px 0}.edit-items button{touch-action:none;width:100%;height:55px;font-weight:bold;font-size:10px}.edit-items li{flex:1;min-width:0}.edit-items .selected button{outline:3px solid #327b94;outline-offset:-3px}.edit-items .dragging{opacity:.5}.transfer{display:grid;grid-template-columns:1fr 1fr;gap:8px}.bin{border:1px dashed #7a9298;min-height:115px;padding:4px}.bin>small{display:block;text-align:center;font-size:10px}.bin .edit-items{flex-wrap:wrap}.bin .edit-items li{flex-basis:40%}.bin .edit-items button{height:40px}.select-board{height:145px;position:relative;touch-action:none;border:1px solid #aab9bc;background:#e1e8e7}.tile{position:absolute;width:46px;height:36px;font-size:10px!important;padding:0!important}.selection-box{position:absolute;border:1px solid #216b83;background:#4087a430;pointer-events:none}.guide-object{position:absolute;width:70px;height:56px;left:calc(var(--x,50)*1% - 35px);top:calc(var(--y,50)*1% - 28px);background:#263f4a;color:#fff;display:grid;place-items:center;font-size:10px;touch-action:none}.guide-line{position:absolute;pointer-events:none;background:#297e9b;display:none}.guide-line.x{width:1px;height:100%;left:50%}.guide-line.y{height:1px;width:100%;top:50%}.search-demo{height:100%;padding:6px;overflow:auto}.search-demo label{display:block;font-size:11px;margin-bottom:6px}.search-demo input{display:block;width:100%;height:38px;border:1px solid #888;border-radius:3px;padding:6px}.suggestions{list-style:none;margin:0;padding:0;border:1px solid #aaa;max-height:120px;overflow:auto;background:white}.suggestions li{padding:9px;font-size:12px;cursor:pointer}.suggestions [aria-selected=true]{background:#234957;color:white}.search-results{font-size:12px;line-height:1.8;margin:5px 0}.search-results mark{background:#ffe893;color:#222}.chip{border-radius:20px!important;font-size:11px!important}.chip-list{display:flex;gap:5px;flex-wrap:wrap;margin:8px 0}.lab-scene [hidden]{display:none!important}</style><div class="sample"><div class="lab-scene"><div class="product-demo"><div class="pieces"><div class="piece">01 / 表面</div><div class="piece">02 / 中間層</div><div class="piece">03 / 台座</div></div></div></div><div class="lab-controls"><label>部品の間隔 px<output>0</output><input aria-label="部品の間隔 px" type="range" min="0" max="45" step="1" value="0" data-key="spread"></label></div><div class="lab-actions"><button data-play>停止</button><button data-reset>組み立て/分解</button></div><p class="lab-status" role="status"></p></div><script>const lab=(function runDomLab(root,cfg,setup){
 const abort=new AbortController(),values=Object.fromEntries(cfg.controls.map(c=>[c.key,c.value])),mq=matchMedia('(prefers-reduced-motion:reduce)');let auto=!mq.matches;
 const on=(el,event,fn)=>el.addEventListener(event,fn,{signal:abort.signal});
 const api=setup({root,values,kind:cfg.kind,on,reduced:mq.matches})||{};
 for(const input of root.querySelectorAll('[data-key]'))on(input,'input',()=>{values[input.dataset.key]=Number(input.value);input.closest('label').querySelector('output').textContent=input.value;api.update?.()});
 const play=root.querySelector('[data-play]');on(play,'click',()=>{auto=!auto;api.setAuto?.(auto);for(const a of root.querySelector('.lab-scene').getAnimations({subtree:true}))auto?a.play():a.pause();play.textContent=auto?'停止':'再生';play.setAttribute('aria-pressed',String(!auto))});
 const reset=root.querySelector('[data-reset]');on(reset,'click',()=>api.action?.());
 on(document,'visibilitychange',()=>api.setAuto?.(auto&&!document.hidden));
 api.update?.();if(cfg.static)play.hidden=true;
 return {setAuto(on){auto=on&&!mq.matches;api.setAuto?.(auto);for(const a of root.querySelector('.lab-scene').getAnimations({subtree:true}))auto?a.play():a.pause();play.textContent=auto?'停止':'再生'},destroy(){abort.abort();api.destroy?.()}};
})(document,{"kind":"product-explode","controls":[{"key":"spread","label":"部品の間隔 px","min":0,"max":45,"value":0,"step":1}],"static":true,"action":"組み立て/分解"},function setup({root,values:v,kind,on,reduced}){
 const $=s=>root.querySelector(s),$$=s=>[...root.querySelectorAll(s)],scene=$('.lab-scene'),say=s=>$('.lab-status').textContent=s;
 const clamp=(x,a,b)=>Math.max(a,Math.min(b,x));let cleanup=()=>{},action=()=>{},update=()=>{},selection=0;
 function set(key,value){v[key]=value;const input=$(`[data-key="${key}"]`);if(input){input.value=value;input.closest('label').querySelector('output').textContent=value}update()}
 function drag(el,move,end=()=>{}){let active=false;on(el,'pointerdown',e=>{active=true;el.setPointerCapture(e.pointerId);move(e)});on(el,'pointermove',e=>{if(active)move(e)});on(el,'pointerup',e=>{if(active){move(e);active=false;end(e)}});on(el,'pointercancel',()=>{active=false;end()})}
 if(kind==='compare-wipe'){
  const p=$('.picture');update=()=>{$('.mono').style.clipPath=`inset(0 ${100-v.position}% 0 0)`;$('.compare-line').style.left=v.position+'%';say(`モノクロ ${v.position}% / カラー ${100-v.position}%`)};
  drag(p,e=>set('position',Math.round(clamp((e.clientX-p.getBoundingClientRect().left)/p.clientWidth*100,0,100))));action=()=>set('position',50);
 }
 if(kind==='compare-loupe'){
  const p=$('.picture'),lens=$('.lens'),image=lens.querySelector('.art');let x=.5,y=.5;
  update=()=>{const b=p.getBoundingClientRect(),size=100; lens.style.left=(x*b.width-size/2)+'px';lens.style.top=(y*b.height-size/2)+'px';image.style.width=b.width+'px';image.style.height=b.height+'px';image.style.transformOrigin='0 0';image.style.transform=`translate(${size/2-x*b.width*v.zoom}px,${size/2-y*b.height*v.zoom}px) scale(${v.zoom})`;say(`${v.zoom}倍 · 矢印キーでも拡大位置を動かせます`)};
  on(p,'pointermove',e=>{const b=p.getBoundingClientRect();x=clamp((e.clientX-b.left)/b.width,0,1);y=clamp((e.clientY-b.top)/b.height,0,1);update()});on(p,'keydown',e=>{const d={ArrowLeft:[-.08,0],ArrowRight:[.08,0],ArrowUp:[0,-.08],ArrowDown:[0,.08]}[e.key];if(d){e.preventDefault();x=clamp(x+d[0],0,1);y=clamp(y+d[1],0,1);update()}});const ro=new ResizeObserver(update);ro.observe(p);cleanup=()=>ro.disconnect();action=()=>{x=y=.5;update()};
 }
 if(kind==='compare-sync'){
  update=()=>{$$('.picture .art').forEach(el=>{el.style.transformOrigin=`${v.position}% 50%`;el.style.transform=`scale(${v.zoom})`});say(`左右とも ${v.zoom}倍 / 位置 ${v.position}%`)};$$('.picture').forEach(p=>drag(p,e=>set('position',Math.round(clamp((e.clientX-p.getBoundingClientRect().left)/p.clientWidth*100,0,100)))));action=()=>{set('zoom',1);set('position',50)};
 }
 if(kind==='product-config'){
  const colors=['#a5dae7','#c8d0b4','#d6b99c'];let material=0;
  update=()=>{const color=colors[selection];$('.product-object').style.background=material?`repeating-linear-gradient(120deg,#fff7 0 1px,transparent 1px 4px),linear-gradient(110deg,#fff,${color},#fff)`:color;$$('[data-color]').forEach((b,i)=>b.setAttribute('aria-pressed',String(i===selection)));$('[data-material]').textContent=material?'素材 / 光沢':'素材 / マット';say(`色 ${selection+1}・${material?'光沢':'マット'}を選択 · 架空の素材見本`)};
  $$('[data-color]').forEach((b,i)=>on(b,'click',()=>{selection=i;update()}));on($('[data-material]'),'click',()=>{material=1-material;update()});action=()=>{selection=0;material=0;update()};
 }
 if(kind==='product-explode'){update=()=>{$('.pieces').style.setProperty('--spread',v.spread);say(v.spread?'層を離して、構造を観察':'層を重ねた状態')};action=()=>set('spread',v.spread?0:42)}
 if(kind==='product-hotspots'){
  const notes=['表面 / 印字の位置を示す注釈','側面 / 厚みを示す注釈','角 / 輪郭の丸みを示す注釈'];update=()=>{$$('.hotspot').forEach((b,i)=>b.setAttribute('aria-pressed',String(i===selection)));$('.annotation').textContent=notes[selection];say(notes[selection])};$$('.hotspot').forEach((b,i)=>on(b,'click',()=>{selection=i;update()}));action=()=>{selection=(selection+1)%3;update()};
 }
 if(kind==='edit-sort'){
  const list=$('.edit-items');let order=['MIX','WELL','PLAY','AGAIN'],selected='MIX',dragId=null;
  const paint=()=>{list.innerHTML=order.map(s=>`<li class="${s===selected?'selected':''}"><button data-item="${s}" aria-pressed="${s===selected}">${s}</button></li>`).join('');say(order.join(' → '))};
  function move(delta){const i=order.indexOf(selected),j=clamp(i+delta,0,order.length-1);[order[i],order[j]]=[order[j],order[i]];paint();list.querySelector(`[data-item="${selected}"]`).focus()}
  on(list,'click',e=>{const b=e.target.closest('[data-item]');if(b){selected=b.dataset.item;paint();list.querySelector(`[data-item="${selected}"]`).focus()}});
  on(list,'pointerdown',e=>{const b=e.target.closest('[data-item]');if(b){selected=dragId=b.dataset.item;list.setPointerCapture(e.pointerId);b.parentElement.classList.add('dragging')}});
  on(list,'pointerup',e=>{if(!dragId)return;const items=[...list.children];const hit=items.find(li=>{const b=li.getBoundingClientRect();return e.clientX>=b.left&&e.clientX<=b.right});if(hit){const target=hit.querySelector('button').dataset.item;order.splice(order.indexOf(dragId),1);order.splice(items.indexOf(hit),0,dragId)}dragId=null;paint()});on(list,'pointercancel',()=>{dragId=null;paint()});
  on(list,'keydown',e=>{if(e.altKey&&['ArrowLeft','ArrowRight'].includes(e.key)){e.preventDefault();move(e.key==='ArrowLeft'?-1:1)}});on($('[data-prev]'),'click',()=>move(-1));on($('[data-next]'),'click',()=>move(1));action=()=>{order=['MIX','WELL','PLAY','AGAIN'];selected='MIX';paint()};update=paint;
 }
 if(kind==='edit-transfer'){
  let groups=[['MIX','WELL'],['PLAY']],picked='MIX',dragging=false;const lists=$$('.bin .edit-items');
  const paint=()=>{lists.forEach((l,i)=>l.innerHTML=groups[i].map(s=>`<li><button data-item="${s}" aria-pressed="${s===picked}">${s}</button></li>`).join(''));say(`準備 ${groups[0].length} / 再生 ${groups[1].length}`)};
  function transfer(to){const from=groups.findIndex(g=>g.includes(picked));if(from!==to){groups[from]=groups[from].filter(x=>x!==picked);groups[to].push(picked)}paint()}
  on(scene,'click',e=>{const b=e.target.closest('[data-item]');if(b){picked=b.dataset.item;paint()}});on(scene,'pointerdown',e=>{const b=e.target.closest('[data-item]');if(b){picked=b.dataset.item;dragging=true;scene.setPointerCapture(e.pointerId)}});on(scene,'pointerup',e=>{if(!dragging)return;dragging=false;$$('.bin').forEach((b,i)=>{const r=b.getBoundingClientRect();if(e.clientX>=r.left&&e.clientX<=r.right&&e.clientY>=r.top&&e.clientY<=r.bottom)transfer(i)})});on(scene,'pointercancel',()=>dragging=false);on($('[data-transfer]'),'click',()=>transfer(groups[0].includes(picked)?1:0));action=()=>{groups=[['MIX','WELL'],['PLAY']];picked='MIX';paint()};update=paint;
 }
 if(kind==='edit-lasso'){
  const board=$('.select-board'),box=$('.selection-box');let start=null,active=false;
  const count=()=>say(`${$$('.tile[aria-pressed=true]').length}個を選択`);
  on(board,'pointerdown',e=>{if(e.target.closest('.tile'))return;const r=board.getBoundingClientRect();start=[e.clientX-r.left,e.clientY-r.top];active=true;board.setPointerCapture(e.pointerId);box.hidden=false});
  on(board,'pointermove',e=>{if(!active)return;const r=board.getBoundingClientRect(),x=clamp(e.clientX-r.left,0,r.width),y=clamp(e.clientY-r.top,0,r.height),left=Math.min(x,start[0]),top=Math.min(y,start[1]),w=Math.abs(x-start[0]),h=Math.abs(y-start[1]);Object.assign(box.style,{left:left+'px',top:top+'px',width:w+'px',height:h+'px'});$$('.tile').forEach(b=>{const q=b.getBoundingClientRect();b.setAttribute('aria-pressed',String(q.right>r.left+left&&q.left<r.left+left+w&&q.bottom>r.top+top&&q.top<r.top+top+h))});count()});
  const finish=()=>{active=false;box.hidden=true};on(board,'pointerup',finish);on(board,'pointercancel',finish);$$('.tile').forEach(b=>on(b,'click',()=>{b.setAttribute('aria-pressed',String(b.getAttribute('aria-pressed')!=='true'));count()}));action=()=>{$$('.tile').forEach(b=>b.setAttribute('aria-pressed','false'));count()};update=count;
 }
 if(kind==='edit-guides'){
  const board=$('.select-board');update=()=>{for(const k of ['x','y']){board.style.setProperty('--'+k,v[k]);$('.guide-line.'+k).style.display=v[k]===50?'block':'none'}say(v.x===50&&v.y===50?'中央のガイドに吸着':`横 ${v.x}% / 縦 ${v.y}%`)};drag($('.guide-object'),e=>{const b=board.getBoundingClientRect();let x=clamp(Math.round((e.clientX-b.left)/b.width*100),12,88),y=clamp(Math.round((e.clientY-b.top)/b.height*100),22,78);if(Math.abs(x-50)<6)x=50;if(Math.abs(y-50)<6)y=50;set('x',x);set('y',y)});action=()=>{set('x',50);set('y',50)};
 }
 if(kind==='search-suggest'){
  const input=$('input[type=text]'),list=$('[role=listbox]'),words=['MIXWELL','MINERALion','NAOYAMAN','FLYING BRAIN'];const uid='options-'+Math.random().toString(36).slice(2);list.id=uid;input.setAttribute('aria-controls',uid);let rows=[],active=-1;
  function paint(){rows=words.filter(w=>w.toLowerCase().includes(input.value.toLowerCase()));active=-1;list.replaceChildren();rows.forEach((w,i)=>{const li=document.createElement('li');li.id=uid+'-'+i;li.setAttribute('role','option');li.dataset.index=i;li.textContent=w;li.setAttribute('aria-selected','false');list.append(li)});list.hidden=!rows.length;input.setAttribute('aria-expanded',String(!!rows.length));input.removeAttribute('aria-activedescendant');say(rows.length?`${rows.length}件の候補 · ↑↓で選び Enterで決定`:'候補はありません')}
  function choose(i){if(!rows[i])return;input.value=rows[i];list.hidden=true;input.setAttribute('aria-expanded','false');input.removeAttribute('aria-activedescendant');say(rows[i]+' を選択')}
  on(input,'input',paint);on(input,'focus',paint);on(input,'keydown',e=>{if(['ArrowDown','ArrowUp'].includes(e.key)){e.preventDefault();if(list.hidden)paint();if(!rows.length)return;active=(active+(e.key==='ArrowDown'?1:-1)+rows.length)%rows.length;[...list.children].forEach((li,i)=>li.setAttribute('aria-selected',String(i===active)));input.setAttribute('aria-activedescendant',uid+'-'+active);list.children[active].scrollIntoView({block:'nearest'})}else if(e.key==='Enter'){e.preventDefault();choose(active)}else if(e.key==='Escape'){list.hidden=true;input.setAttribute('aria-expanded','false');input.removeAttribute('aria-activedescendant')}});on(list,'click',e=>{const li=e.target.closest('[data-index]');if(li)choose(Number(li.dataset.index))});on(input,'blur',()=>{list.hidden=true;input.setAttribute('aria-expanded','false');input.removeAttribute('aria-activedescendant')});on(list,'pointerdown',e=>e.preventDefault());action=()=>{input.value='MI';paint();input.focus()};update=()=>say('文字を入力すると、候補が絞り込まれます');
 }
 if(kind==='search-highlight'){
  const input=$('input[type=text]'),out=$('.search-results'),words=['MIXWELL — 音を混ぜる。','MINERALion — 色と素材の見本。','NAOYAMAN — 文字を動かす。','FLYING BRAIN — 空間を探索する。'];
  update=()=>{out.replaceChildren();let matches=0;for(const text of words){const line=document.createElement('div');const q=input.value.trim(),at=q?text.toLowerCase().indexOf(q.toLowerCase()):-1;if(at<0)line.textContent=text;else{matches++;line.append(document.createTextNode(text.slice(0,at)));const mark=document.createElement('mark');mark.textContent=text.slice(at,at+q.length);line.append(mark,document.createTextNode(text.slice(at+q.length)))}out.append(line)}say(`${matches}件に一致 · 入力は文字列として扱います`)};on(input,'input',update);action=()=>{input.value='M';update()};
 }
 if(kind==='search-chips'){
  const tags=['文字','質感','操作'],chosen=new Set();update=()=>{const list=$('.chip-list');list.replaceChildren();for(const tag of chosen){const b=document.createElement('button');b.className='chip';b.dataset.remove=tag;b.textContent=tag+' ×';b.setAttribute('aria-label',tag+'の条件を解除');list.append(b)}$$('[data-tag]').forEach(b=>b.setAttribute('aria-pressed',String(chosen.has(b.dataset.tag))));const rows=[['NAOYAMAN','文字'],['MINERALion','質感'],['MIXWELL','操作']].filter(x=>!chosen.size||chosen.has(x[1]));$('.search-results').textContent=rows.map(x=>x[0]).join(' / ')||'該当なし';say(`${rows.length}件 · 選択した条件のいずれかに一致`)};
  on(scene,'click',e=>{const b=e.target.closest('[data-tag],[data-remove]');if(!b)return;const tag=b.dataset.tag||b.dataset.remove;chosen.has(tag)?chosen.delete(tag):chosen.add(tag);update()});action=()=>{chosen.clear();update()};
 }
 return {update,action,destroy:cleanup};
});addEventListener('pagehide',()=>lab.destroy(),{once:true});</script></html>