- 投稿日:2020-09-24T23:19:46+09:00
(´ω`) 箇条書きを樹形図にするアプリを作った(ドッキングパネルを添えて...
わいや。おっさんとはわいのことや。おっさんが言ったことの全てが間違っているとは思わんが確かに受け入れにくい"きもちわるさ"みたいなものを感じ取ったと思う。殺伐とした世の中で、そういう気持ちを忘れないで欲しい。そう思ってこの記事を書いたんだ。って前置きはそのぐらいにして今のとこ毎日書きまくってるな。書きなぐって丸めてぽいするブログみたいな感覚だ。マークダウンも##と```のハイライトしか使ってない。フォロワーが増えることはもうないと悟った。4記事目にして住人として世界の無常さを悟るが如く俺の書く記事ではフォロワーが増えないという事実の目の当たりにし愕然とした。希望などない。だから俺が希望になるよ。QIITAに変わるサービスを作ろうと思った。って前置きを入れて解説してくよ。
何作った
箇条書き。ってよく書くはずだ。学生でも社会人でも隣のおばちゃん家もそうさ。多分誰もが書く。1マス空けて。2マス開けて...それを樹形図に出来たら更に判りやすくなるんじゃねぇかな。って発想で作った。議事録もメモ程度なら箇条書きにする。するとこうなる...
縦に長くなる箇条書きより判りやすい
機能について
save tree imageを押せばSVG形式でダウンロードできる。
save indented textを押せばTXT形式でダウンロードできる。
木の高さも深さも調整可能だ。ドラッグでパンできてホイールでズームできる。
resize:bothしてあるおかげでウィンドウ自体も拡縮できる
ドッキングパネル化するフレームワークのおかげでエディタもビュアーも移動可能だ
利用しているライブラリ
入力/設定/出力とドッキングパネル化する (golden-layout)
テキストエリアをタブ入力可能にする (TabIndent.js)
ウィンドウのリサイズを検知する (resize-event)
箇条書きをjSON化する (indent2obj)
樹形図を出力するソース
sample.html<!doctype html> <html> <head> <title></title> <meta charset='utf-8'> <meta content='' name='author'> <meta content='' name='application-name'> <meta content='' name='description'> <meta content='telephone=no,address=no,email=no,date=no,url=no' name='format-detection'> <meta content='noimageindex,notranslate,nosnippet,noarchive,nofollow,noindex' name='robots'> <meta content='width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no' name='viewport'> <link href='js/jquery/golden-layout/theme/base.css' rel='stylesheet'> <link href='asset/manifest.json' rel='manifest'> <link href='asset/favicon.ico' rel='icon'> <style> @font-face{ font-family:'M+2VM+IPAG circle'; src:url('asset/m+2vm+ipag-circle.ttf'); } html, body{ margin:0; width:100%; height:100%; font-size:12px; } /* * 中央 */ main{ top:50%; left:50%; position:absolute; transform:translate(-50%,-50%); } /* * 背景 */ .lm_goldenlayout .lm_content{ background:white; } /* * 変更 */ main .wrap{ width:80vw; height:80vh; resize:both; overflow:scroll; } /* * 見栄 */ main .wrap{ border-bottom:2px solid rgba(34,36,38,.15); box-shadow:rgba(16, 36, 94, 0.4) 0 2px 6px 0; } /* * 設定 */ .proper .dg, .proper .dg .close-button{ width:100% !important; } .proper .dg.main .close-button{ display:none; } </style> </head> <body> <main> <div id='ui' class='wrap'> </div> </main> </body> </html> <!-- native --> <script src='js/native/d3-5.12.0.min.js'></script> <script src='js/native/dat-gui/0.7.6.min.js'></script> <script src='js/native/tab-indent-0.1.8.min.js'></script> <script src='js/native/indent2obj-0.0.3.min.js'></script> <!-- jquery --> <script src='js/jquery/3.4.1.min.js'></script> <script src='js/jquery/golden-layout/1.5.9.min.js'></script> <script src='js/jquery/resize-event-1.2.1.min.js'></script> <script> var app={ layout:{ content: [ { type:'row', content: [ { width:29.3, type:'column', content: [ {type:'component',componentName:'view',title:'editor'}, {type:'component',componentName:'view',title:'proper'} ] }, { width:70.7, type:'stack', content: [ {type:'component',componentName:'view',title:'viewer'} ] } ] } ] }, plugin:{ editor:{ elem:'<textarea></textarea>', option:{ width:'100%', height:'100%', addClass:'tabIndent', css:{ font:"12px 'M+2VM+IPAG circle", outline:'none', border:'none' } } }, viewer:{ elem:'<div></div>', option:{ id:'svg', addClass:'tree', css:{ background:'white' } } } }, config:{ save:function(){ localStorage.setItem('tree',$editor.val()) }, download:function(){ var data = d3.select('svg').node().outerHTML var blob = new Blob([data]) var link = document.createElement("a") link.href = URL.createObjectURL(blob) link.download = new Date().toISOString() + '.svg' link.click() }, height:0.3, width:11, } } var $ui = $('#ui') /* * 最低限 */ var dat = new dat.GUI({autoPlace:false}) var golden = new GoldenLayout(app.layout,$ui) // GOLDENを初期化()すると呼び出されるDOM挿入先の$コンテナを準備 golden.registerComponent('view',function(container,state){ var key = container._config.title, ele = container.getElement() ele.addClass(key) golden[key]=ele }) /* * 1.初期化 */ function init(){ golden.init() } /* * DOM生成 */ var $editor = $(app.plugin.editor.elem,app.plugin.editor.option), $viewer = $(app.plugin.viewer.elem,app.plugin.viewer.option), $proper = $(this.dat.domElement) $editor.on('keyup',function(){ update() }) /* * 2.GOLDENにDOMを構築 */ function construct(){ // 生成したDOMを$コンテナに挿入 golden['proper'].append($proper) golden['editor'].append($editor) golden['viewer'].append($viewer) } /* * 3.ビュアーをセットアップ */ function setup(){ dat.add(app.config,'height').onChange(update).name('tree height') dat.add(app.config,'width').onChange(update).name('tree width') dat.add(app.config,'download').name('save tree image') dat.add(app.config,'save').name('save indented text') $(window).on('resize',function(){ golden.updateSize() }) $ui.onResize({},function(){ golden.updateSize() }) } /* * ツリーはSVG */ var view = d3.select($viewer.get(0)) .append('svg') view.append('g') view.attr("xmlns", "http://www.w3.org/2000/svg") view.append('style') .text(` circle { stroke-width:2px; stroke:#05668D; fill:white; r:6; } text { font:12px 'M+2VM+IPAG circle'; } rect { transform:translateY(-5px); stroke:#0cf; width:10px; heiht:10px; fill:white; } path { storke-width:2px; stroke:#ccc; fill:none; } `) function getLinks(arr) { return arr .enter() .append('path') .attr('d', d3.linkHorizontal() .x(function(d){ return d.y }) .y(function(d){ return d.x }) ) } function getNodes(arr) { return arr .enter() .append('g') .attr('class',function(d){ return 'node ' + (d.children ? 'node--internal' : 'node--leaf') }) .attr('transform',function(d){ return "translate(" + d.y + "," + d.x + ")"; }) } function update() { var w = golden.viewer.width() var h = golden.viewer.height() view .attr('width',w) .attr('height',h) var g = d3.select('svg > g') d3.select('svg > g').selectAll("*").remove() view.call(d3.zoom().on('zoom',function(){ g.attr('transform',d3.event.transform) }) ) var vData = indent2obj($editor.val(),'\t') var vRoot = d3.hierarchy(vData[0]); var vNodes = vRoot.descendants(); var vLayout = d3.tree().size([ app.config.width * vNodes.reverse().length, app.config.height * h ]); var vLinks = vLayout(vRoot).links(); var maxDepth = d3.max(vNodes,function(d) { return d.depth }) var links = getLinks(g.selectAll('.link').data(vLinks)) var nodes = getNodes(g.selectAll(".node").data(vNodes)) nodes.append('circle') nodes.append('text') .attr('dy','.35em') .attr('x',function(d){ return d.children ? -13 : 13; }) .attr('text-anchor',function(d){ return d.children || d._children ? 'end' : 'start' }) .text(function(d){ return d.data.name }) } document.addEventListener('DOMContentLoaded',function(){ init() construct() setup() if('tree' in localStorage){ $editor.val( localStorage.getItem('tree') ) update() } tabIndent.config.tab = '\t'; tabIndent.renderAll() }) </script> <style> @import url('js/jquery/golden-layout/theme/light.css'); @import url('js/native/dat-gui/theme/light.min.css'); </style/>備考
ウィンドウの陰影が恰好よくねぇ?
- 投稿日:2020-09-24T23:19:46+09:00
(´ω`) 箇条書き?樹形図にしたら見やすいっておっさんがいっとった
わいや。おっさんとはわいのことや。おっさんが言ったことの全てが間違っているとは思わんが確かに受け入れにくい"きもちわるさ"みたいなものを感じ取ったと思う。殺伐とした世の中で、そういう気持ちを忘れないで欲しい。そう思ってこの記事を書いたんだ。って前置きはそのぐらいにして今のとこ毎日書きまくってるな。書きなぐって丸めてぽいするブログみたいな感覚だ。マークダウンも##と```のハイライトしか使ってない。フォロワーが増えることはもうないと悟った。4記事目にして住人として世界の無常さを悟るが如く俺の書く記事ではフォロワーが増えないという事実の目の当たりにし愕然とした。希望などない。だから俺が希望になるよ。QIITAに変わるサービスを作ろうと思った。って前置きを入れて解説してくよ。
何作った
箇条書き。ってよく書くはずだ。学生でも社会人でも隣のおばちゃん家もそうさ。多分誰もが書く。1マス空けて。2マス開けて...それを樹形図に出来たら更に判りやすくなるんじゃねぇかな。って発想で作った。議事録もメモ程度なら箇条書きにする。するとこうなる...
縦に長くなる箇条書きより判りやすい
機能について
save tree imageを押せばSVG形式でダウンロードできる。
save indented textを押せばTXT形式でダウンロードできる。
木の高さも深さも調整可能だ。ドラッグでパンできてホイールでズームできる。
resize:bothしてあるおかげでウィンドウ自体も拡縮できる
ドッキングパネル化するフレームワークのおかげでエディタもビュアーも移動可能だ
利用しているライブラリ
入力/設定/出力とドッキングパネル化する (golden-layout)
テキストエリアをタブ入力可能にする (TabIndent.js)
ウィンドウのリサイズを検知する (resize-event)
箇条書きをjSON化する (indent2obj)ソース
sample.html<!doctype html> <html> <head> <title></title> <meta charset='utf-8'> <meta content='' name='author'> <meta content='' name='application-name'> <meta content='' name='description'> <meta content='telephone=no,address=no,email=no,date=no,url=no' name='format-detection'> <meta content='noimageindex,notranslate,nosnippet,noarchive,nofollow,noindex' name='robots'> <meta content='width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no' name='viewport'> <link href='js/jquery/golden-layout/theme/base.css' rel='stylesheet'> <link href='asset/manifest.json' rel='manifest'> <link href='asset/favicon.ico' rel='icon'> <style> @font-face{ font-family:'M+2VM+IPAG circle'; src:url('asset/m+2vm+ipag-circle.ttf'); } html, body{ margin:0; width:100%; height:100%; font-size:12px; } /* * 中央 */ main{ top:50%; left:50%; position:absolute; transform:translate(-50%,-50%); } /* * 背景 */ .lm_goldenlayout .lm_content{ background:white; } /* * 変更 */ main .wrap{ width:80vw; height:80vh; resize:both; overflow:scroll; } /* * 見栄 */ main .wrap{ border-bottom:2px solid rgba(34,36,38,.15); box-shadow:rgba(16, 36, 94, 0.4) 0 2px 6px 0; } /* * 設定 */ .proper .dg, .proper .dg .close-button{ width:100% !important; } .proper .dg.main .close-button{ display:none; } </style> </head> <body> <main> <div id='ui' class='wrap'> </div> </main> </body> </html> <!-- native --> <script src='js/native/d3-5.12.0.min.js'></script> <script src='js/native/dat-gui/0.7.6.min.js'></script> <script src='js/native/tab-indent-0.1.8.min.js'></script> <script src='js/native/indent2obj-0.0.3.min.js'></script> <!-- jquery --> <script src='js/jquery/3.4.1.min.js'></script> <script src='js/jquery/golden-layout/1.5.9.min.js'></script> <script src='js/jquery/resize-event-1.2.1.min.js'></script> <script> var app={ layout:{ content: [ { type:'row', content: [ { width:29.3, type:'column', content: [ {type:'component',componentName:'view',title:'editor'}, {type:'component',componentName:'view',title:'proper'} ] }, { width:70.7, type:'stack', content: [ {type:'component',componentName:'view',title:'viewer'} ] } ] } ] }, plugin:{ editor:{ elem:'<textarea></textarea>', option:{ width:'100%', height:'100%', addClass:'tabIndent', css:{ font:"12px 'M+2VM+IPAG circle", outline:'none', border:'none' } } }, viewer:{ elem:'<div></div>', option:{ id:'svg', addClass:'tree', css:{ background:'white' } } } }, config:{ save:function(){ localStorage.setItem('tree',$editor.val()) }, download:function(){ var data = d3.select('svg').node().outerHTML var blob = new Blob([data]) var link = document.createElement("a") link.href = URL.createObjectURL(blob) link.download = new Date().toISOString() + '.svg' link.click() }, height:0.3, width:11, } } var $ui = $('#ui') /* * 最低限 */ var dat = new dat.GUI({autoPlace:false}) var golden = new GoldenLayout(app.layout,$ui) // GOLDENを初期化()すると呼び出されるDOM挿入先の$コンテナを準備 golden.registerComponent('view',function(container,state){ var key = container._config.title, ele = container.getElement() ele.addClass(key) golden[key]=ele }) /* * 1.初期化 */ function init(){ golden.init() } /* * DOM生成 */ var $editor = $(app.plugin.editor.elem,app.plugin.editor.option), $viewer = $(app.plugin.viewer.elem,app.plugin.viewer.option), $proper = $(this.dat.domElement) $editor.on('keyup',function(){ update() }) /* * 2.GOLDENにDOMを構築 */ function construct(){ // 生成したDOMを$コンテナに挿入 golden['proper'].append($proper) golden['editor'].append($editor) golden['viewer'].append($viewer) } /* * 3.ビュアーをセットアップ */ function setup(){ dat.add(app.config,'height').onChange(update).name('tree height') dat.add(app.config,'width').onChange(update).name('tree width') dat.add(app.config,'download').name('save tree image') dat.add(app.config,'save').name('save indented text') $(window).on('resize',function(){ golden.updateSize() }) $ui.onResize({},function(){ golden.updateSize() }) } /* * ツリーはSVG */ var view = d3.select($viewer.get(0)) .append('svg') view.append('g') view.attr("xmlns", "http://www.w3.org/2000/svg") view.append('style') .text(` circle { stroke-width:2px; stroke:#05668D; fill:white; r:6; } text { font:12px 'M+2VM+IPAG circle'; } rect { transform:translateY(-5px); stroke:#0cf; width:10px; heiht:10px; fill:white; } path { storke-width:2px; stroke:#ccc; fill:none; } `) function getLinks(arr) { return arr .enter() .append('path') .attr('d', d3.linkHorizontal() .x(function(d){ return d.y }) .y(function(d){ return d.x }) ) } function getNodes(arr) { return arr .enter() .append('g') .attr('class',function(d){ return 'node ' + (d.children ? 'node--internal' : 'node--leaf') }) .attr('transform',function(d){ return "translate(" + d.y + "," + d.x + ")"; }) } function update() { var w = golden.viewer.width() var h = golden.viewer.height() view .attr('width',w) .attr('height',h) var g = d3.select('svg > g') d3.select('svg > g').selectAll("*").remove() view.call(d3.zoom().on('zoom',function(){ g.attr('transform',d3.event.transform) }) ) var vData = indent2obj($editor.val(),'\t') var vRoot = d3.hierarchy(vData[0]); var vNodes = vRoot.descendants(); var vLayout = d3.tree().size([ app.config.width * vNodes.reverse().length, app.config.height * h ]); var vLinks = vLayout(vRoot).links(); var maxDepth = d3.max(vNodes,function(d) { return d.depth }) var links = getLinks(g.selectAll('.link').data(vLinks)) var nodes = getNodes(g.selectAll(".node").data(vNodes)) nodes.append('circle') nodes.append('text') .attr('dy','.35em') .attr('x',function(d){ return d.children ? -13 : 13; }) .attr('text-anchor',function(d){ return d.children || d._children ? 'end' : 'start' }) .text(function(d){ return d.data.name }) } document.addEventListener('DOMContentLoaded',function(){ init() construct() setup() if('tree' in localStorage){ $editor.val( localStorage.getItem('tree') ) update() } tabIndent.config.tab = '\t'; tabIndent.renderAll() }) </script> <style> @import url('js/jquery/golden-layout/theme/light.css'); @import url('js/native/dat-gui/theme/light.min.css'); </style/>備考
ウィンドウの陰影が恰好よくねぇ?
- 投稿日:2020-09-24T19:56:52+09:00
Tyハロトレ26日目
CSS
タイピング練習で月一回提出
疑似要素
<p><a href="#">未閲覧は青色</a></p> <p><a href="#">閲覧済みは灰色</a></p> <p><a href="#">マウスポインタが重なっている</a></p> <p><a href="#">クリックされている(押されている)間は赤色</a></p>優先順位
読み込まれる順番で最新のものが優先される
外部CSS < ヘッド < 本文!important
index.html<p style="color: blue;">山田</p>
!important
は、index.htmlに書いたCSSよりも優先されます。
font-family
text-align
vertical-align
line-height
おうちをイラレで描く
はさみ、消しゴム、ナイフツール
- 投稿日:2020-09-24T18:10:08+09:00
CSSのみでタブとタブコンテンツの表示・非表示を切り替える
HTMLコーダーさんのJSを使わないタブ切り替えが、シンプルな実装で素晴らしかったので忘備録的にメモ。
概要
- タブは『インプットボックス』+『ラベル』を使用
- コンテンツエリアは常時非表示にしておく
- コンテンツエリアは選択されたときのみ表示する
サンプルコード
動かしてないので不具合あるかもですが、こんな感じでした。
<div class="tabs"> <!-- タブ --> <input id="tab_1" type="radio" name="tab" checked=""> <label class="tab_label" for="tab_1">月別</label> <input id="tab_2" type="radio" name="tab"> <label class="tab_label" for="tab_2">週間別</label> <input id="tab_3" type="radio" name="tab"> <label class="tab_label" for="tab_3">曜日別</label> <div class="tab_content" id="content_1">コンテンツ1</div> <div class="tab_content" id="content_2">コンテンツ2</div> <div class="tab_content" id="content_3">コンテンツ3</div> </div>input[name="tab"] { display: none; } .tabs { width: 100%; } .tab_label { border-top: 1px solid #999999; border-bottom: 1px solid #999999; } .tabs input:checked + .tab_label { border-bottom: 2px solid #55C501; } #tab_1:checked ~ #content_1, #tab_2:checked ~ #content_2, #tab_3:checked ~ #content_3 { display: block; } .tab_content { display: none; }
- 投稿日:2020-09-24T16:46:59+09:00
プログラミング初めて3ヶ月
- 投稿日:2020-09-24T13:55:59+09:00
【初心者でもわかる】CSSだけ。カーソルを載せたら補足説明のバルーンポップアップを出す方法
どうも7noteです。?マークにカーソルを当てると、補足説明が出るやつ作ります。
こういうやつ、作ります。
最初は
display: none;
で非表示にしておき、hover時に表示するような作りにします。作り方
1) ハテナ(?)を用意。
index.html<div class="box"> <div class="ques">?</div> </div>簡単に装飾を入れます。
style.css.ques { background: #EEE; /* 背景色に灰色を指定 */ width: 1.5em; /* 横幅を1.5文字分にする */ line-height: 1.5em; /* 文字が上下中央にくるように工夫 */ text-align: center; /* 文字が左右中央にくるように工夫 */ border-radius: 50%; /* 円形に変更 */ }2) 補足説明のバルーンポップアップを作る。(動きはまだ。)
index.html<div class="box"> <div class="ques">?</div> <!-- 以下の1行を追加。 --> <div class="ex">ここに補足説明文を入れる</div> </div>style.css.box { position: relative; /* 表示位置の基準値とする */ } .ex { position: absolute; /* boxを基準にする */ top: 0; /* 自由に調整 */ left: 30px; /* 自由に調整 */ color: #fff; /* 文字色を白にする */ font-size: 14px; background: rgba(0,0,0,0.5); /* 黒バックを半透明にするため、rgbaで指定 */ padding: 2px 5px; /* 余白を少々 */ }3) hover時の動きを入れる。
style.css.ques:hover { cursor: pointer; /* カーソルを指の形にする。 */ } .ex { display: none; /* 最初は非表示にする。 */ } .ques:hover + .ex { display: block; /* quesの上にカーソルが乗っている時だけ表示 */ }+α)ふわっと出したい場合は、以下のように変更。
.ex { display: none; /*↓に変更*/ opacity: 0; transition : all .3s; } .ques:hover + .ex { display: block; /*↓に変更*/ opacity: 1; }完成!
まとめ
結構簡単に作れます。
表示位置だけtopとleftでpx指定しているので、実際に運用レベルで使うには表示位置の指定方法は少してを加えたほうがよいかも。
ハテナ(?)の位置がかわらなければ今回の方法で十分対応可能。おそまつ!
~ Qiitaで毎日投稿中!! ~
【初心者向け】HTML・CSSのちょいテク詰め合わせ
- 投稿日:2020-09-24T13:55:59+09:00
【初心者でもわかる】CSSだけ。カーソルを載せたら補足説明のミニモーダルを出す方法
どうも7noteです。?マークにカーソルを当てると、補足説明が出るやつ作ります。
こういうやつ、作ります。
最初は
display: none;
で非表示にしておき、hover時に表示するような作りにします。作り方
1) ハテナ(?)を用意。
index.html<div class="box"> <div class="ques">?</div> </div>簡単に装飾を入れます。
style.css.ques { background: #EEE; /* 背景色に灰色を指定 */ width: 1.5em; /* 横幅を1.5文字分にする */ line-height: 1.5em; /* 文字が上下中央にくるように工夫 */ text-align: center; /* 文字が左右中央にくるように工夫 */ border-radius: 50%; /* 円形に変更 */ }2) 補足説明のミニモーダルを作る。(動きはまだ。)
index.html<div class="box"> <div class="ques">?</div> <!-- 以下の1行を追加。 --> <div class="ex">ここに補足説明文を入れる</div> </div>style.css.box { position: relative; /* 表示位置の基準値とする */ } .ex { position: absolute; /* boxを基準にする */ top: 0; /* 自由に調整 */ left: 30px; /* 自由に調整 */ color: #fff; /* 文字色を白にする */ font-size: 14px; background: rgba(0,0,0,0.5); /* 黒バックを半透明にするため、rgbaで指定 */ padding: 2px 5px; /* 余白を少々 */ }3) hover時の動きを入れる。
style.css.ques:hover { cursor: pointer; /* カーソルを指の形にする。 */ } .ex { display: none; /* 最初は非表示にする。 */ } .ques:hover + .ex { display: block; /* quesの上にカーソルが乗っている時だけ表示 */ }+α)ふわっと出したい場合は、以下のように変更。
.ex { display: none; /*↓に変更*/ opacity: 0; transition : all .3s; } .ques:hover + .ex { display: block; /*↓に変更*/ opacity: 1; }完成!
まとめ
結構簡単に作れます。
表示位置だけtopとleftでpx指定しているので、実際に運用レベルで使うには表示位置の指定方法は少してを加えたほうがよいかも。
ハテナ(?)の位置がかわらなければ今回の方法で十分対応可能。おそまつ!
~ Qiitaで毎日投稿中!! ~
【初心者向け】HTML・CSSのちょいテク詰め合わせ
- 投稿日:2020-09-24T09:37:14+09:00
amp-img から学ぶ画像の表示のベストプラクティス
AMP は Google が推奨しているウェブコンポーネントフレームワークで、その実装には Web サイトのパフォーマンスを向上させるための知見が詰まっています。
AMP コンポーネントの実装を詳しく見ていくと、 AMP を導入せずにサイトを作る際にも役に立つベストプラクティスを学べるのではないかと思います。
以下では amp-img コンポーネントに注目して web サイト開発における画像表示の実装について掘り下げます。amp-img
amp-img は AMP 対応のサイトで画像を表示する際に、 HTML の img タグの代わりに使用します。
AMP の built-in 要素であるため、 amp-img 専用の js を追加で読む必要はなく、AMP のランタイムを通して自動的に使用できます。amp-img の基本の使い方
<amp-img src="/static/sample.jpg" width="1080" height="720" layout="fixed" alt="sample"></amp-img>参考
画面の解像度に合わせた画像を表示
HTML の img タグと同様に amp-img タグでも
srcset
属性やsize
属性を使用することができます。
高解像度の画面(Retina ディスプレイなど)で高解像な画像を配信する場合きれいな画像を表示できるメリットがありますが、低解像度の画面でも必要以上に重い画像を時間をかけて読み込まなくてはなりません。
srcset
属性やsize
属性を使用することで、画面の解像度に合わせて適切な画像を表示することができます。<amp-img alt="sample" src="/static/sample-640.jpg" width="640" height="400" srcset="/static/sample-640.jpg 640w, /static/sample-320.jpg 320w" sizes="(min-width: 650px) 50vw, 100vw"> </amp-img>アートディレクション
ブレークポイントの条件に合わせて画像を変更することをアートディレクションと言います。
amp-img ではmedia
属性によってブレークポイントを指定して画像自体を切り替えることができます。
srcset
属性 とsize
属性による画像の切り替えは、基本的に解像度のみ異なる同一の画像を切り替えることが想定されていますが、media
属性の場合は画像のアスペクト比が異なっていても問題ありません。<amp-img alt="sample" media="(max-width: 768px)" width="226" height="340" src="/static/sample-medium.jpg"></amp-img> <amp-img alt="sample" media="(mim-width: 769px)" width="450" height="340" src="/static/sample-small.jpg"></amp-img>通常の HTML タグでアートディレクションを実現する場合、picture タグを使用します。1
<picture> <source srcset="/static/sample-medium.jpg" media="(max-width: 768px)"> <source srcset="/static/sample-small.jpg" media="(mim-width: 769px)"> <img src="/static/sample-small.jpg" alt="sample"> </picture>参考
遅延ロード
画像の遅延ロードは、画面に表示されていない画像は読み込まずに、画面のスクロールに応じてあとから画像を読み込むことでウェブページの表示を高速化する手法です。
通常の img タグを使用した場合、ページアクセス時にすべての img タグの画像を読み込みます。
これによりページが最低限の操作を受け付けるようになるまでに時間がかかってしまったり、スクロールしない場合に不要な通信が発生したり、といったことが起きます。amp-img は画面のスクロール位置に応じて、amp-img タグ内に img タグを生成します。この時に初めて画像の取得のための通信が発生するため効率的に画像をロードすることができます。
<amp-img src="/static/sample.jpg" width="1080" height="720" layout="fixed" alt="sample"></amp-img>↓
<amp-img src="/static/sample.jpg" width="1080" height="720" layout="fixed" alt="sample" class="i-amphtml-element i-amphtml-layout-fixed i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fixed" style="width: 1080px; height: 610px;"> <img decoding="async" alt="sample" src="/static/sample.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content"> </amp-img>amp-img を使用せずに遅延ロードを行う場合、スクラッチで実装するほか、lazyloadなどのライブラリの利用や、img タグの loading 属性を使用する方法があります。(ただし、Safari と IE11 は未対応)
非同期で画像をデコード
展開されたコードの img タグを見ると、
decoding="async"
の指定があります。
画像データは通常ファイルサイズを小さくするためにエンコードされているため、ブラウザは画像を表示する際はデータをデコードする必要があります。
img タグにdecoding
属性によってブラウザに同期/非同期のどちらでデコードするかのヒントを提供でき、全てのモダンブラウザに対応しています。1
decoding
属性に指定できる値は下記の通りです。
値 説明 sync 画像を同期的にデコードする async 画像を非同期でデコードする auto 優先設定なし(デフォルト値) amp-img コンポーネントは、img タグに
decoding="async"
を付与することで、メインスレッドの処理をブロックせずに画像を非同期的にデコードさせています。参考
- https://html.spec.whatwg.org/multipage/images.html#decoding-images
- 画像埋め込み要素 - HTML: HyperText Markup Language | MDN
画像のフォールバック
amp-img では、例えば webp の画像を使用する際に、webp 未対応のブラウザ用に jpg の画像を設定したい場合など、画像の読み込みに失敗した際のフォールバック画像を指定することができます。
<amp-img alt="sample" width="1080" height="720" src="/static/sample.webp"> <amp-img alt="sample" fallback width="1080" height="720" src="/static/samples.jpg"></amp-img> </amp-img>amp-img を使用しない場合、picture タグを使用することで実現できます。
<picture> <source type="image/webp" srcset="/static/sample.webp" /> // wepb 対応の場合は source タグの画像を提供 <img src="/static/samples.jpg" width="1080" height="720" alt="sample" /> // wepb 未対応ブラウザで source タグの画像を提供できない場合の代替画像 </picture>参考
- 画像要素 - HTML: HyperText Markup Language | MDNレイアウトシフト対策
Google の提唱する UX 指標である Core Web Vitals の一つに Cumulative Layout Shift(CLS) があります。
これは予期せぬレイアウトのズレや崩れを独自に指標化し評価しているもので、画像が読み込まれた際のこのレイアウトの移動によってスコアが下がってしまいます。
amp-img コンポーネントは、画面幅によって画像の幅 / 高さが変化しない場合、画像を表示する明示的なサイズをwidth
とheight
に指定することで amp-img のインラインスタイルとして展開され、レイアウト時に画像の表示領域が確保されます。2
画面幅によって画像の表示サイズが変化する場合は、適切なスタイルを付与することによりレイアウトシフトを防いでいます(後述)。通常の img タグの場合も
width
/height
属性への値を指定や、CSS でwidth
/height
に具体値を指定することで画像を読み込む前に場所を確保し、レイアウトシフトを防ぐことができます。
2019 年 10 月に WHATWG が img タグのwidht
/height
属性に基づいてデフォルトのアスペクト比を設定できる仕様が標準化されました。
そのため現在のモダンブラウザでは、画面の幅によって画像の表示サイズが変化するレスポンシブな表示でもwidth
とheight
利用してレイアウトシフトを防ぐことができます。例えば、下記のようにな img タグを設置した場合、3 : 2 のアスペクト比でブラウザが表示場所を計算して確保してくれます。
<img src="/static/samples.jpg" width="1080" height="720" alt="sample">img { width: 100%; height: auto; }参考
レスポンシブ対応のスタイル
layout 属性
amp-img コンポーネントは
layout
属性を指定することにより、AMP のレイアウトシステムを利用できます。
layout
属性は AMP コンポーネント共通の属性ですが、コンポーネントによってサポートされる値が異なります。
amp-img コンポーネントはcontainer
以外の下記のレイアウトに対応しています。
- fill
- fixed
- fixed-height
- responsive
- flex-item
- intrinsic
- nodisplay
amp-img タグとその子要素の img タグには、
layout
属性の指定に応じた class が指定されます。
レスポンシブ対応をする際に、ウインドウや親のコンテンツのサイズの変化に伴ってどのように画像の表示を変化させるのかは往々にして悩ましいものです。
そんな時にlayout
属性による画像を様々な表示パターンとそのスタイルは参考になります。
下記ではそれぞれのlayout
の値を amp-img に指定した場合のスタイルを詳しく見ていきます。
なお、layout
属性は amp-img 専用の属性ではなく、AMP コンポーネント共通のものであるため、画像以外のコンテンツにも応用できます。
各layout
値を指定した際のアニメーション画像は公式ドキュメントからお借りしました。参考
layout="fill"
親要素のコンテンツボックス全体を埋めるように拡大縮小されます。
width
とheight
に指定したアスペクト比が親要素のアスペクト比と合わない場合は引き伸ばされて表示されます。
<amp-img src="/static/samples.jpg" width="1080" height="720" layout="fill" alt="sample"></amp-img>↓ 画像ロード後
<amp-img src="/static/samples.jpg" layout="fill" alt="sample" class="i-amphtml-element i-amphtml-layout-fill i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fill" style="--loader-delay-offset:434ms !important;"> <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content"> </amp-img>/* amp-img タグのスタイル */ .i-amphtml-layout-size-defined { overflow: hidden!important; } .i-amphtml-layout-fixed { display: block; overflow: hidden!important; /* .i-amphtml-layout-size-defined のスタイルによって打ち消される */ position: absolute; top: 0; left: 0; bottom: 0; right: 0; } /* img タグのスタイル */ .i-amphtml-layout-size-defined .i-amphtml-fill-content { position: absolute; top: 0; left: 0; bottom: 0; right: 0; } .i-amphtml-replaced-content { padding: 0!important; border: none!important; } .i-amphtml-replaced-content { padding: 0!important; border: none!important; } .i-amphtml-fill-content { display: block; height: 0; max-height: 100%; max-width: 100%; min-height: 100%; min-width: 100%; width: 0; margin: auto; }amp-img タグは
.i-amphtml-layout-fixed
に指定されているスタイルにより、position: absolute;
であるため、直近のposition: static;
以外のposition
の値を持つ親要素の上下左右いっぱいに広がるように表示されます。img タグは
.i-amphtml-layout-size-defined .i-amphtml-fill-content
に指定されているスタイルにより、直近のposition: static;
以外のposition
の値を持つ親要素、つまり amp-img の上下左右いっぱいに広がるように表示されます。
img が amp-img のサイズいっぱいになる構造は、どのlayout
を指定した場合でも共通しています。
layout="fill"
の場合は amp-img タグと img タグが共にposition: absolute;
であるため、基準となる親要素に高さを明示的に指定する必要があります。
.i-amphtml-replaced-content
に指定されているスタイルは、ユーザーエージェントスタイルを打ち消すためのスタイルです。
また、.i-amphtml-fill-content
に指定されているスタイルは、iOS で iframe をレスポンシブ対応させる際にコンテンツのサイズをコンテナーに収めるための指定のようです。(issue)
layout
属性は他のコンポーネントでも使用されるため、amp-img タグの場合でもこのようなスタイルが適応されます。
これらのスタイルは他のlayout
値を指定した場合でも登場しますが、今回の内容には直接関係ないため以下では割愛します。layout="fixed"
ウインドウのサイズが変化しても画像の大きさは変化せず、
width
とheight
属性に基づいて固定の寸法のまま表示されます。<amp-img src="/static/samples.jpg" width="1080" height="720" layout="fixed" alt="sample"></amp-img>↓ 画像ロード後
<amp-img src="/static/samples.jpg" width="1080" height="720" layout="fixed" alt="sample" class="i-amphtml-element i-amphtml-layout-fixed i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fixed" style="width: 1080px; height: 610px; --loader-delay-offset:226ms !important;"> <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content"> </amp-img>/* amp-img タグのスタイル */ element.style { width: 1080px; height: 610px; --loader-delay-offset: 188ms !important; } .i-amphtml-layout-size-defined { overflow: hidden!important; } .i-amphtml-layout-fixed { display: inline-block; position: relative; }amp-img タグにはインラインスタイルとして
width
とheight
属性で指定した値が指定されています。
overflow: hidden;
が指定されているため、画像の表示サイズよりウインドウが小さくなった場合もスクロールは発生せずに画像は見切れて表示されます。layout="fixed-height"
高さは固定のまま、幅は使用可能なスペースに合わせて広がります。
<amp-img src="/static/samples.jpg" height="720" layout="fixed-height" alt="sample"></amp-img>↓ 画像ロード後
<amp-img src="/static/samples.jpg" layout="fixed-height" height="720" alt="sample" class="i-amphtml-element i-amphtml-layout-fixed-height i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fixed-height" style="height: 720px; --loader-delay-offset:377ms !important;"> <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content"> </amp-img>/* amp-img タグのスタイル */ element.style { height: 720px; } .i-amphtml-layout-size-defined { overflow: hidden!important; } .i-amphtml-layout-fixed-height { display: block; position: relative; }layout="responsive"
width
/height
属性で指定されたアスペクト比を維持したまま、使用可能なスペースに合わせて拡大・縮小します。<amp-img src="/static/samples.jpg" width="1080" height="720" layout="responsive" alt="sample"></amp-img>↓ 画像ロード後
<amp-img src="/static/samples.jpg" layout="fixed-height" height="720" alt="sample" class="i-amphtml-element i-amphtml-layout-fixed-height i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fixed-height" style="--loader-delay-offset:377ms !important;"> <i-amphtml-sizer slot="i-amphtml-svc" style="padding-top: 66.6667%;"></i-amphtml-sizer> <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content"> </amp-img>/* amp-img タグのスタイル */ .i-amphtml-layout-size-defined { overflow: hidden!important; } /* i-amphtml-sizer タグのスタイル */ element.style { padding-top: 66.6667%; } .i-amphtml-sizer { display: block!important; }
layout="responsive
の場合、amp-img の直下に img タグに加えて i-amphtml-sizer タグが挿入されます。
amp-img タグのwidth
とheight
に指定した値から、画像のアスペクト比は1080:720 = 3:2
となります。
このアスペクト比を維持した状態で、表示可能な領域内で画像を拡大・縮小させるために、2 / 3 * 100 ≒ 66.6667%;
をpadding-top
として与えています。
i-amphtml-sizer タグによって親要素の amp-img の大きさが確保され、その amp-img の大きさに合わせて img タグが広がることで、画像のアスペクト比を維持したまま画像が拡大・縮小されます。
この方法であれば、IE11 でもレイアウトシフトを防ぐことができます。layout="flex-item"
フレックスボックスの親要素の直下でフレックスアイテムとして画像を扱いたい場合に使用します。
親要素のスタイルと兄弟要素の数によって画像の大きさが変化します。<amp-img src="/static/samples.jpg" layout="flex-item" alt="sample"></amp-img>↓ 画像ロード後
<amp-img src="/static/samples.jpg" layout="flex-item" class="i-amphtml-layout-flex-item i-amphtml-layout-size-defined i-amphtml-element i-amphtml-layout" i-amphtml-layout="flex-item" alt="sample"> <img decoding="async" alt="sample" src="//static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content"> </amp-img>/* amp-img タグのスタイル */ .i-amphtml-layout-flex-item { display: block; position: relative; flex: 1 1 auto; }
flex: 1 1 auto;
が指定されているため、フレックスボックスの直下にlayout="flex-item"
の amp-img を隣接して配置して横に並べた場合、同じ幅で並びます。
また、兄弟要素がlayout="flex-item"
の要素のみで高さを持たない場合は、親要素のフレックスボックスに高さを明示的に与える必要があります。layout="intrinsic"
画像自体の本来のサイズか CSS による制限(max-width など)に達するまで、
width
とheight
属性で指定されたアスペクト比を維持したまま使用可能なスペースに合わせて拡大・縮小します。<amp-img src="/static/samples.jpg" width="1080" height="720" layout="intrinsic" alt="sample"></amp-img>↓ 画像ロード後
<amp-img src="/static/samples.jpg" width="1080" height="720" layout="intrinsic" alt="sample" class="i-amphtml-element i-amphtml-layout-intrinsic i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="intrinsic" style="--loader-delay-offset:241ms !important;"> <i-amphtml-sizer class="i-amphtml-sizer" slot="i-amphtml-svc"> <img alt="" role="presentation" aria-hidden="true" class="i-amphtml-intrinsic-sizer" src="data:image/svg+xml;charset=utf-8,<svg height="720px" width="1080px" xmlns="http://www.w3.org/2000/svg" version="1.1"/>"> </i-amphtml-sizer> <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content"> </amp-img>/* amp-img タグのスタイル */ .i-amphtml-layout-size-defined { overflow: hidden!important; } .i-amphtml-layout-intrinsic { display: inline-block; position: relative; max-width: 100%; } /* i-amphtml-sizer タグのスタイル */ .i-amphtml-layout-intrinsic .i-amphtml-sizer { max-width: 100%; } i-amphtml-sizer { display: block!important; } /* i-amphtml-sizer タグ直下の img タグのスタイル */ .i-amphtml-intrinsic-sizer { max-width: 100%; display: block!important; }
layout="responsive"
と同様に amp-img の直下に img タグに加えて i-amphtml-sizer タグが挿入されます。
しかし、layout="intrinsic"
の場合は i-amphtml-sizer の直下にさらにもう一つ img タグが挿入されます。
layout="responsive"
では、空の i-amphtml-sizer タグに指定されたpadding
が画像の表示領域を確保していたのに対し、layout="intrinsic"
では i-amphtml-sizer タグの直下の img タグによって表示される透明な svg 画像によって画像の表示領域を確保します。
svg 画像のサイズは amp-img に指定したwidth
とheight
属性によって決まり、img タグであるため画像自体の大きさより大きくなることはありません。また、i-amphtml-sizer タグ内の img タグには、
role="presentation"
とaria-hidden="true"
を指定することで、見た目を変えるために使用されている重要な意味を持たない要素であることを明示してあり、アクセシビリティにも考慮されていることがわかります。layout="nodisplay"
要素は非表示になり、スペースを占有しません。
<amp-img layout="nodisplay" src="/static/sample.jpg" width="1080" height="720" layout="nodisplay"></amp-img>↓ 画像ロード後
<amp-img layout="nodisplay" src="/static/sample.jpg" width="1080" height="720" class="i-amphtml-layout-nodisplay i-amphtml-element" hidden="" i-amphtml-layout="nodisplay"></amp-img>/* amp-img タグのスタイル */ [hidden] { display: none!important; }AMP コンポーネント共通の
hidden
属性が付与され、display: none;
が適用されます。まとめ
amp-img を使用しない場合でも
width
やheight
、decoding
といった属性を適切に指定するなど、パフォーマンス改善のために画像周りで工夫できることが多くあることがわかりました。
amp-img は遅延ロードやフォールバックといった機能を提供してくれるので、改めて便利だと感じました。
完全に AMP 対応したサイトではないとしても画像を便利に扱うために amp-img を使用するのも一つの選択肢だと思いました。
画像の扱いは web サイトのパフォーマンスを低下させる大きな要因になり得るので、先人の知恵を借りながらベストな実装を追いかけていきたいですね。
IEは非対応: https://caniuse.com/mdn-html_elements_img_decoding ↩
layout
の値により、width
/height
が必須かどうかは異なります。 ↩