20211130のJavaScriptに関する記事は18件です。

【toio 2021】 #toio の姿勢角(ロール・ピッチ・ヨー)をブラウザ上で可視化(Web Bluetooth API、Smoothie Charts を利用)

この記事は、2021年の toio のアドベントカレンダー の 1日目の記事です。 今年 4月にリリースされた toio の新機能の「姿勢角(ロール・ピッチ・ヨー)」を Web Bluetooth API で利用する話です。 #toio の新しく公開された仕様「姿勢角検出」で取得できる値をグラフ化してみた!以前も toio の制御で使った Web Bluetooth API で実装して、HTML のページで値を受け取りつつ Smoothie Charts でグラフ描画もする形。●姿勢角検出 · toio™コア キューブ 技術仕様 https://t.co/YP1K3Z4UTA pic.twitter.com/btzcyTtEZP— you (@youtoy) April 21, 2021 上記のツイートの内容は、新機能がリリースされた同月に実装していたのですが、記事などは書いていなかったので、アドベントカレンダー用の記事として書いてみようと思います。 利用する技術・ライブラリ ブラウザ 今回、HTML+JavaScript を利用するのですが、ブラウザは「Google Chrome」を用います。 これは、「Firefox」や「Safari」だと、次に出てくる Web Bluetooth API に対応していないためです。 ●"web bluetooth" | Can I use... Support tables for HTML5, CSS3, etc  https://caniuse.com/?search=web%20bluetooth ブラウザで toio を扱う「Web Bluetooth API」 今回、toio からのセンサー情報取得には「Web Bluetooth API」を用います。 Web Bluetooth API を用いた、toio との間の通信(「通知・読み出し・書き込み)は、過去に書いた記事で何度も使っており、またその部分だけをまとめた以下の記事も書いていました。 ●toio を Web Bluetooth API で制御(「通知・読み出し・書き込み」を行う) - Qiita  https://qiita.com/youtoy/items/791905964d871ac987d6 実装には、以下に出てくる Async/Await を用いた処理を使っています。 ●Web Bluetooth / Notifications (Async Await) Sample  https://googlechrome.github.io/samples/web-bluetooth/notifications-async-await.html UUID Web Bluetooth API で toio を扱う際に、いくつかの UUID の値(文字列)を利用する必要があります。 後に出てくるソースコード内にも書いていますが、以下のものを用います。 const TOIO_SERVICE_UUID = "10b20100-5b3b-4571-9508-cf3efcd7bbae"; const CONFIGURATION_CHARACTERISTIC_UUID = "10b201ff-5b3b-4571-9508-cf3efcd7bbae"; const TOLT_CHARACTERISTIC_UUID = "10b20106-5b3b-4571-9508-cf3efcd7bbae"; これらの UUID は、以下の toio の仕様が書かれたページで公開されています。 ●姿勢角検出 | toio™コア キューブ 技術仕様  https://toio.github.io/toio-spec/docs/ble_high_precision_tilt_sensor ●通信概要 | toio™コア キューブ 技術仕様  https://toio.github.io/toio-spec/docs/ble_communication_overview グラフライブラリ「Smoothie Charts」 今回、toio のセンサーから受けとった値をグラフ化するのですが、グラフ描画には以下のライブラリを用います。 ●Smoothie Charts: A JavaScript Charting Library for Streaming Data  http://smoothiecharts.org/ 詳細な仕様は、公式ページ内の説明などをご確認ください。 CSSフレームワーク「Bulma」 今回の HTML で作成するページで、手軽に良い感じの見た目にするのに CSSフレームワークを用います。 有名どころだけでも複数のものがありますが、自分が以前利用したことがある「Bulma」を用いました。 ●Bulma: Free, open source, and modern CSS framework based on Flexbox  https://bulma.io/ プログラム 今回、1ファイルでプログラムを書いていきます。 HTML+JavaScript のファイル 以下の HTML+JavaScript の1ファイルだけで、今回の内容は動作します。 内容の補足は、この後に書いていきます。 <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title> Web Bluetooth API による姿勢角検出のデータ取得とデータのグラフ化 </title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.css" /> <script src="https://cdn.jsdelivr.net/npm/smoothie@1.35.0/smoothie.min.js"></script> </head> <body> <section class="section"> <div class="container"> <h1 class="title">姿勢角検出(ロール・ピッチ・ヨー)</h1> <canvas id="mycanvas1" width="400" height="100"></canvas> <canvas id="mycanvas2" width="400" height="100"></canvas> <canvas id="mycanvas3" width="400" height="100"></canvas> <div class="buttons" style="margin-top: 1.5rem"> <button class="button is-success is-light" type="button" onclick="onStartButtonClick()"> 接続+通知ON </button> <button class="button is-danger is-light" type="button" onclick="onStopButtonClick()"> 通知OFF </button> <button class="button is-info is-light" type="button" onclick="onStartNotificationsButtonClick()"> 通知ON </button> </div> </div> </section> <script> const smoothie = [], line = [], colors = [[0, 255, 0], [0, 255, 0], [0, 255, 0]]; for (let i = 0; i < 3; i++) { smoothie.push(new SmoothieChart({ minValue: -180.0, maxValue: 180.0 })); line.push(new TimeSeries()); smoothie[i].addTimeSeries(line[i], { strokeStyle: `rgba(${colors[i]}, 1)`, fillStyle: `rgba(${colors[i]}, 0.2)`, lineWidth: 4, }); smoothie[i].streamTo(document.getElementById(`mycanvas${i + 1}`)); } const TOIO_SERVICE_UUID = "10b20100-5b3b-4571-9508-cf3efcd7bbae"; const CONFIGURATION_CHARACTERISTIC_UUID = "10b201ff-5b3b-4571-9508-cf3efcd7bbae"; const TILT_CHARACTERISTIC_UUID = "10b20106-5b3b-4571-9508-cf3efcd7bbae"; let myCharacteristic01, myCharacteristic02; async function onStartButtonClick() { const serviceUuid = TOIO_SERVICE_UUID; try { console.log("Requesting Bluetooth Device..."); const device = await navigator.bluetooth.requestDevice({ filters: [{ services: [serviceUuid] }], }); console.log("Connecting to GATT Server..."); const server = await device.gatt.connect(); console.log("Getting Service..."); const service = await server.getPrimaryService(serviceUuid); // Configuration https://toio.github.io/toio-spec/docs/ble_configuration#%E5%A7%BF%E5%8B%A2%E8%A7%92%E6%A4%9C%E5%87%BA%E3%81%AE%E8%A8%AD%E5%AE%9A console.log("Getting Characteristic..."); myCharacteristic01 = await service.getCharacteristic( CONFIGURATION_CHARACTERISTIC_UUID ); // Tilt https://toio.github.io/toio-spec/docs/ble_high_precision_tilt_sensor onWriteButtonClick(); console.log("Getting Characteristic..."); myCharacteristic02 = await service.getCharacteristic( TILT_CHARACTERISTIC_UUID ); await myCharacteristic02.startNotifications(); console.log("> Notifications started"); myCharacteristic02.addEventListener( "characteristicvaluechanged", handleNotifications ); } catch (error) { console.log("Argh! " + error); } } async function onWriteButtonClick() { // 姿勢角検出の設定 https://toio.github.io/toio-spec/docs/ble_configuration#%E5%A7%BF%E5%8B%A2%E8%A7%92%E6%A4%9C%E5%87%BA%E3%81%AE%E8%A8%AD%E5%AE%9A const configuration_buf = new Uint8Array([ 0x1d, 0x00, 0x01, 0x0a, 0x00, ]); if (!myCharacteristic01) { return; } let value = configuration_buf; try { await myCharacteristic01.writeValue(configuration_buf); } catch (error) { console.log("Argh! " + error); } } async function onStopButtonClick() { if (myCharacteristic02) { try { await myCharacteristic02.stopNotifications(); console.log("> Notifications stopped"); myCharacteristic02.removeEventListener( "characteristicvaluechanged", handleNotifications ); } catch (error) { console.log("Argh! " + error); } } } async function onStartNotificationsButtonClick() { try { console.log("Starting Notifications..."); await myCharacteristic02.startNotifications(); myCharacteristic02.addEventListener( "characteristicvaluechanged", handleNotifications ); console.log("> Notifications started"); } catch (error) { log("Argh! " + error); } } function handleNotifications(event) { let value = event.target.value; if (value.getUint8(0) === 3) { console.log( `ロール/X ${value.getInt16(2, true)}、 ピッチ/Y ${value.getInt16( 4, true )}、 ヨー/Z ${value.getInt16(6, true)}` ); const getTime = new Date().getTime(); for (let i = 0; i < 3; i++) { line[i].append(getTime, value.getInt16(i * 2 + 2, true)); } } } </script> </body> </html> プログラムの補足 ライブラリ・フレームワークの読み込み 上で登場していた Smoothie Charts と Bulma は、それぞれ CDN から読み込みます。 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.css" /> <script src="https://cdn.jsdelivr.net/npm/smoothie@1.35.0/smoothie.min.js"></script> HTML の bodyタグ内 bodyタグの中では、グラフ描画用のエリアと、操作用のボタンを用意します。 以下が画像に出ているように、タイトルの下に「グラフ描画用の Canvas 3つ」と「操作用ボタン 3つ」を用意しました。 ボタンの部分では、onclick属性で、ボタン押下時に実行する JavaScript の関数を指定しています。 また、クラスの部分で Bulma に関連した内容(is-success や is-light など)を加えています。 グラフ描画の部分 グラフ 3つを Smoothie Charts で描画しています。 個々の描画処理については、以下の公式サンプル「Hello world sample」を見ていただくのが良いかと思います。 ●smoothiecharts.org/examples/example1.html  http://smoothiecharts.org/examples/example1.html toio関連の処理の概要 toio 関連の処理では、主に以下の内容を行っています。 BLE接続 姿勢角検出の有効化 通知の有効化/無効化(センサー値の受け取りのためのもの) 受領したセンサーの値の処理 以下で、各項目の補足を書いていきます。 BLE接続 BLE接続の部分は、「 navigator.bluetooth.requestDevice() 」を使っている部分です。 Web Bluetooth API の作法に従って、UUID を用いた接続を行います。 server.getPrimaryService() や service.getCharacteristic() を書いている部分は、Web Bluetooth API を用いる際の基本の流れになります。 姿勢角検出の有効化 toio の「姿勢角検出の技術仕様」に書かれているとおり、デフォルトでは無効になっています。 そのため、利用するためには以下のあたりに書かれた機能の有効化を行う必要があります。 上記のプログラムの中では onWriteButtonClick() の中の処理が、それに該当します。 通知の有効化/無効化(センサー値の受け取りのためのもの) 上記の姿勢角検出の有効化を行うと、姿勢角のデータを通知として受けとることができるようになります(こちらから読みに行かなくても、値が送られてきます)。 startNotifications() ・ addEventListener() が出てくるあたりが、通知の有効化になります。 また、 stopNotifications() ・ removeEventListener() が出てくるあたりが、通知の無効化です。 受領したセンサーの値の処理 受領したセンサーのデータはバイナリになるため、 handleNotifications() の部分で値の取り出しを行っています。 受信できるバイナリ列の構成の情報は、toio公式の技術仕様のページで以下のように書かれているため、これを元に JavaScript の処理を書いています(なお、オイラー角で情報を受けとるようにしています)。 ここで取り出したロール・ピッチ・ヨーの値を、Smoothie Charts に関連した処理に受け渡しています。 実行結果 toio の電源を入れて、上記のプログラムを実行し、自分で作った Webページ上の「接続+通知ON」ボタンを押します。 そうすると、BLE接続が行われた後、Webページ上でグラフ描画が行われます(また、コンソールには値が出力されるようになっています)。 こちらは、冒頭のツイートの動画で使っていたプログラムを、今回用に少し書き直して実行したものです。 書き直したと言っても、目に見える部分(ボタンの見た目、グラフの描画の見た目など)は以前と同じになります。 #toio の「姿勢角検出」、ロール・ピッチ・ヨー の値を Web Bluetooth API 取得して HTML のページ上でグラフ化。グラフ描画には Smoothie Charts を使ってます。以前、姿勢角検出の仕様が公開された時に作ってたものがあり、それを #toiotomo のもくもく会の中でリファクタリングした形です。 pic.twitter.com/sI8QLytVCq— you (@youtoy) November 20, 2021 その後、今回の内容をもとに、p5.js による 3D描画との組み合わせも試してみたりしました。 #toio の「姿勢角検出」、ロール・ピッチ・ヨー の値を Web Bluetooth API 取得して、それを WEBGLモードの #p5js の描画と連動させてみました。#toiotomo のもくもく会の中でやった内容に、p5.js 関連の追加実装をしてみたものです。 pic.twitter.com/Xfi1IciFWY— you (@youtoy) November 20, 2021 おわりに 今回、toio の姿勢角(ロール・ピッチ・ヨー)のデータをブラウザで取得し、グラフによる可視化を行ってみました。 最後に書いていた、p5.js との連携の話は今回の内容には入っていないため、また別途記事に書いたりしてみようと思っています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SVG.jsのテキストについて詳しく調べてみる

SVG.jsのライブラリのテキスト関係の仕様について色々コードを書いて試しつつ確認していくための記事です。 使うもの SVG.js v3.1.1 JSFiddle (例 : https://jsfiddle.net/yar6x4hk/7/ ) 前提 SVGの仕様上、普通のHTMLのテキストでさくっとできることができないケースが結構あります(固定幅によるテキストの折り返しなど)。一方で複雑なアニメーションなどでSVGのテキストの方が自由度が高いケースがある・・・といった感じのようです。SVGで大半のことができる・・・というわけではないので使い分けが必要そうです。 Textクラス 以降の節でテキストを扱う基本のクラスとなるTextクラスについて触れていきます。 インスタンスの作り方 textメソッドで引数に文字列を指定することで作成できるようです。 var draw = SVG().addTo('body').size(500, 300); var text = draw.text( 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'); text.move(20, 20); もしくは関数を指定して、その中でtspanメソッドでテキスト要素を追加するということもできるようです(個別の設定などができるようになります)。 var draw = SVG().addTo('body').size(500, 300); var text = draw.text(function(add) { add.tspan('Lorem ipsum dolor sit amet, \n'); add.tspan('consectetur adipiscing elit.').fill('#0af'); }); text.move(20, 20); textメソッド textメソッドではTextインスタンスに設定されているテキスト内容の取得や更新を行うことができます。 var draw = SVG().addTo('body').size(500, 300); var text = draw.text('Lorem ipsum dolor sit amet, '); text.move(20, 20); text.text('consectetur adipiscing elit.'); fontメソッド fontメソッドではフォントの種類や文字のスタイル(サイズなど)を指定したり取得することができるようです。 設定は以下のように複数の値を辞書でまとめて行えます。 var draw = SVG().addTo('body').size(500, 300); var text_1 = draw.text('吾輩は猫である。'); text_1.move(20, 20); text_1.font({ family: 'MS Gothic', size: 16 }); var text_2 = draw.text('吾輩は猫である。'); text_2.move(20, 50); text_2.font({ family: 'メイリオ', size: 20 }); もしくは第一引数にキー、第二引数に値という形で個別に設定することも可能です。 var draw = SVG().addTo('body').size(500, 300); var text = draw.text('吾輩は猫である。'); text.move(20, 20); text.font('family', 'メイリオ'); text.font('size', 25); fontメソッドで設定できる各属性 fontメソッドで設定できる各属性についてベーシックなものについて以降の節で触れていきます。svg.jsのドキュメントに加えて以下のW3Cの資料もある程度確認しつつ進めます。 family属性 family属性はフォントの種類を指定したり取得したりすることができます。 var draw = SVG().addTo('body').size(500, 300); var text = draw.text('吾輩は猫である。'); text.move(20, 20); text.font('size', 25); text.font('family', 'メイリオ'); 複数のフォント指定はコンマ区切りなどで行えます。 var draw = SVG().addTo('body').size(500, 300); var text = draw.text('吾輩は猫である。'); text.move(20, 20); text.font('size', 25); text.font('family', 'Helvetica, メイリオ'); size属性 size属性ではテキストのサイズの変更や取得を行うことができます。 var draw = SVG().addTo('body').size(500, 300); var text_1 = draw.text('吾輩は猫である。'); text_1.move(20, 20); text_1.font('size', 12); var text_2 = draw.text('吾輩は猫である。'); text_2.move(20, 50); text_2.font('size', 18); var text_3 = draw.text('吾輩は猫である。'); text_3.move(20, 80); text_3.font('size', 24); leading属性 leading属性では行間の設定や取得を行うことができます。 以下のサンプルでは上の方のテキストには1.5、下の方には2.0の行間を設定しています。 var draw = SVG().addTo('body').size(600, 300); var text_1 = draw.text('Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam, \nquis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. '); text_1.move(20, 20); text_1.font('size', 14); text_1.font('leading', 1.5); var text_2 = draw.text('Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam, \nquis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. '); text_2.move(20, 170); text_2.font('size', 14); text_2.font('leading', 2.0); anchor属性 anchor属性はテキストの基準点を設定することができます。start, middle, endのどれかの値を設定することができます。デフォルトはstartです。それぞれ基準点がテキストの左端、中央、右端となります。行揃えもそれぞれ左揃え、中央揃え、右揃えとなります。 以下のコードでは上から順番にstart, middle, endと設定していっています。 var draw = SVG().addTo('body').size(600, 600); var text_1 = draw.text('Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam'); text_1.move(20, 20); text_1.font('size', 14); var text_2 = draw.text('Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam'); text_2.move(300, 120); text_2.font('size', 14); text_2.font('anchor', 'middle'); var text_3 = draw.text('Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam '); text_3.move(600, 220); text_3.font('size', 14); text_3.font('anchor', 'end'); 注意点として基準位置が変わるためX座標も調整が必要になります。例えばmiddleであればx=0の位置がテキストの中央位置になります。以下はmiddleとx=0を指定していますがテキストの半分しか表示されないことが確認できます。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text('Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam'); text.move(0, 20); text.font('size', 14); text.font('anchor', 'middle'); style属性 style属性では文字の斜体設定などを行うことができます。normal(デフォルト), italic, obliqueの設定があります。obliqueってなんだ・・・?と思いましたが、どうやらイタリック体と斜体という違いで一部のフォントで表示が変わるようです。対応していなければ両方とも同じように斜体となります。 欧文フォントの中には、「イタリック体」と「斜体」用にそれぞれ専用にデザインされたフォントが用意されているものがあるようです。イタリック体は筆記体っぽくデザインされたもので、斜体は単純に斜めに倒しただけのような形です。 以下の例では上がデフォルトのnormal, 2番目にitalicを設定してあります。 var draw = SVG().addTo('body').size(600, 600); var text_1 = draw.text('Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam'); text_1.move(20, 20); text_1.font('size', 14); var text_2 = draw.text('Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam'); text_2.move(20, 120); text_2.font('size', 14); text_2.font('style', 'italic'); weight属性 weight属性では文字の太さを設定することができます。 normal(デフォルト), bold(太字), bolder, lighter, 100, 200, 300, 400, 500, 600, 700, 800, 900といった値となります。色々ありますが大体使うのはnormalとboldかな・・・という所感です。 bolderは現在の属性の継承値よりも太い設定が可能であれば太い表示にし、lighterは逆に属性の継承値よりも細い設定が可能であれば細い表示にします。 100~900の値は細かい太さ指定です。数字が大きいほど太い表示となります。400がnormal相当、700がbold相当になります。400よりも小さい値を設定したり700よりも大きい値を設定したとしてもフォント側がそれらの太さに対応していない場合には表示は400や700などの時点のもので止まります(細かい指定に対応していないフォントも多いと思います)。 以下のコードでは上からデフォルトの設定、boldによる太字設定、700による設定をそれぞれ行っています。 var draw = SVG().addTo('body').size(600, 600); var text_1 = draw.text('Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam'); text_1.move(20, 20); text_1.font('size', 14); var text_2 = draw.text('Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam'); text_2.move(20, 120); text_2.font('size', 14); text_2.font('weight', 'bold'); var text_3 = draw.text('Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam'); text_3.move(20, 220); text_3.font('size', 14); text_3.font('weight', '700'); leadingメソッド leadingメソッドでは行間をダイレクトに設定できるようです。fontメソッドでも設定できましたが、行間を設定したいだけであればこちらを使う形でもシンプルに設定することができます。 以下は2つのテキストに行間1.5と2.0をそれぞれ設定しています。 var draw = SVG().addTo('body').size(600, 600); var text_1 = draw.text('Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam'); text_1.move(20, 20); text_1.leading(1.5); var text_2 = draw.text('Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam'); text_2.move(20, 160); text_2.leading(2.0); tspanメソッド tspanメソッドではテキストインスタンスの内容に指定された1つのtspan要素を設定します。他のテキストなどが設定されている場合には内容が上書きされるので注意してください。tspanに関しては後の節でも詳しく触れますが、特定の表示設定などを行うことができるspan要素的なもののようです。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text('Lorem ipsum dolor sit amet, '); text.move(20, 20); text.tspan('\nconsectetur adipiscing elit,'); lengthメソッド lengthメソッドではテキストインスタンスの幅(横の長さ)を取得することができます。Pythonとかに慣れ親しんでいる影響か初見の時には文字数取得のメソッドかな?と思いましたが違うようです。なお、後々触れるテキストパスなどを設定した場合でも数値は変動しないようです(見かけ上の幅とはずれうる値となります)。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text('Lorem ipsum'); text.move(20, 20); console.log(text.length()); 102.046875 pathメソッド pathメソッドではテキストにパス設定を行い、TextPathクラスのインスタンスを返却します(テキストが指定されたパスに沿う形になります)。 パス指定に関しては別途記事にしたためこちらをご確認ください。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text('Lorem ipsum'); text.move(20, 20); var path = 'M 50 100 Q 100 20 150 100 T 250 100'; var textPath = text.path(path); テキストがパスに沿って配置されるようになりました。パスの長さが足りない分のテキストは非表示になります。 textPathメソッド textPathメソッドでは設定されているパスのTextPathインスタンスを返却します。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text('Lorem ipsum'); text.move(20, 20); var path = 'M 50 100 Q 100 20 150 100 T 250 100'; text.path(path); var textPath = text.textPath(); clearメソッド clearメソッドでは設定されているテキスト内容のリセットを行います。呼び出した後は新たに設定などをしないと何も表示されなくなります。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text('Lorem ipsum'); text.move(20, 20); text.clear(); その他の共通設定 他のSVGの四角などの図形などと同じように各インターフェイスで塗り(fill)や線(stroke)の設定も行うことができます(ただし現状SVGで線を外側に配置したりができないので線の設定はそこまで利用する機会がないかもしれません・・・)。テキスト固有の設定ではないため今回の記事ではあまり触れずにスキップします。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text('Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam'); text.move(20, 20); text.leading(1.5); text.fill('#fff'); text.attr({'stroke-width': 0.4, stroke: '#0af'}); TextPathクラス Textクラスでもちらっと出てきましたが以降の節ではTextPathクラスについて色々触れていきます。 インスタンスの作り方 TextPathのインスタンスはTextクラスのtextPathメソッドなどで作成する他にもSVGのインスタンスのtextPathメソッドでも作成することができます。第一引数に設定するテキストの文字列、第二引数にパスの指定が必要になります。 var draw = SVG().addTo('body').size(600, 600); var textPath = draw.textPath( 'Lorem ipsum dolor sit amet, consectetur', 'M 50 100 Q 100 20 150 100 T 250 100'); なお、TextPathクラス自体はTextクラスのサブクラスになっているようで、Textクラスのようにテキストが表示されます。 サブクラスになっているのでTextで使えたような各インターフェイスはそのまま利用できます。 var draw = SVG().addTo('body').size(600, 600); var textPath = draw.textPath( 'Lorem ipsum dolor sit amet, consectetur', 'M 50 100 Q 100 20 150 100 T 250 100'); textPath.fill('#0af'); arrayメソッド arrayメソッドでは設定のグループごと(MやTなどの指定ごと)のデータを2次元配列で取得することができます。2次元目の先頭のインデックスは対象のグループの文字になります。 var draw = SVG().addTo('body').size(600, 600); var textPath = draw.textPath( 'Lorem ipsum dolor sit amet, consectetur', 'M 50 100 Q 100 20 150 100 T 250 100'); console.log(textPath.array()); [["M", 50, 100], ["Q", 100, 20, 150, 100], ["T", 250, 100]] plotメソッド plotメソッドを使うことでパスの設定を途中で変更することができます。 以下のコードでは途中で縦方向にパスを変更しています。 var draw = SVG().addTo('body').size(600, 600); var textPath = draw.textPath( 'Lorem ipsum dolor sit amet, consectetur', 'M 50 100 Q 100 20 150 100 T 250 100'); textPath.plot('M 50 50 V 300'); Tspanクラス Textクラスでも少し出てきましたがHTMLタグのspanタグのようにテキスト内で一部だけスタイルを変更したり・・・といった場合のためのクラスとしてTspanクラスがあります。こちらもTextクラスのサブクラスとなっているようで各種スタイルなどのインターフェイスを利用できるようです。 インスタンスの作り方 Tspanクラスのインスタンスは基本的にTextクラスもしくはTspanクラス内に格納する形で利用されるため、生成処理などはTextクラスなどと一緒に利用します(TextクラスやTspanクラスが生成用のtspanメソッドを持っています)。 いくつかtspan関係のインターフェイスはありますが、一例として以下のようにTextクラス関係の記述と一緒に使います。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text(function(add) { add.tspan('Lorem ipsum dolor sit amet, '); add.tspan('consectetur adipiscing elit.').fill('#0af'); }); text.move(20, 20); textメソッド textメソッドで特定のTspanインスタンスのテキスト(テキスト内で特定部分のみのテキスト)を更新することができます。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text(function(add) { var tspan1 = add.tspan('Lorem '); var tspan2 = add.tspan('ipsum').fill('#0af'); tspan1.text('dolor '); }); text.move(20, 20); dxメソッド dxメソッドは対象のTspanインスタンスと直前のテキスト要素の間にX方向の座標の調整値を設定します。先頭のテキスト部分に設定しても反映されません。2番目以降のテキストで使用する必要があります。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text(function(add) { add.tspan('Lorem ipsum dolor sit amet, '); add.tspan('consectetur adipiscing elit.').fill('#0af').dx(100); }); text.move(20, 20); dyメソッド dyメソッドはdxメソッドのY方向版です。Y方向に対して座標の調整値を設定できます。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text(function(add) { add.tspan('Lorem ipsum dolor sit amet, '); add.tspan('consectetur adipiscing elit.').fill('#0af').dy(30); }); text.move(20, 20); lengthメソッド TextクラスのようにTspanクラスもlengthメソッドを持っています。該当のTspanの幅のサイズを取ることができます(文字数などではありません)。 改行などの無い基本的なテキストであれば各Tspanのlengthメソッドの返却値の合算値と親となるTextのlengthメソッドの返却値は一致するようです。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text(function(add) { var tspan1 = add.tspan('Lorem ipsum dolor sit amet, '); var tspan2 = add.tspan('consectetur adipiscing elit.').fill('#0af'); console.log('tspan1 length:', tspan1.length()); console.log('tspan2 length:', tspan2.length()); }); text.move(20, 20); console.log('text length:', text.length()); "tspan1 length:", 226.8125 "tspan2 length:", 207.171875 "text length:", 433.984375 newLineメソッド newLineメソッドはTspanクラスの要素に改行を加えます。Textクラスみたく\nとかでは駄目なのか?と思い試してみたのですが、どうやらTspanクラスではテキストに\nなどの改行の指定があっても反映されないようです。 以下のようにテキスト内に\nの改行を入れてみても反映されていません。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text(function(add) { add.tspan('Lorem \nipsum \ndolor \nsit amet, '); add.tspan('consectetur adipiscing elit.').fill('#0af'); }); text.move(20, 20); 代わりに以下のようにnewLineメソッドを追加すると改行が追加されます。ちなみにテキストの最後に改行が付くのではなく、対象のTspan要素の配置位置の直前で改行が加わる形となります(メソッド呼び出し位置的に初見の時に末尾に改行が加わるのか・・・と思いましたがそうではないようです)。先頭のTspan要素でnewLineの呼び出しを行っても表示は変わりません。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text(function(add) { add.tspan('Lorem ipsum dolor sit amet, '); add.tspan('consectetur adipiscing elit.').newLine().fill('#0af'); }); text.move(20, 20); clearメソッド clearメソッドはTextクラスと同様に対象のTspan要素のリセットを行います。 以下のコードでは2つのTspanインスタンスを生成していますが、先頭の方のTspanではclearメソッドでリセットを行っているため2つ目の水色のテキストのみが残ります。最初のテキストが消えた分の位置の調整なども実行されるようです。 var draw = SVG().addTo('body').size(600, 600); var text = draw.text(function(add) { var tspan1 = add.tspan('Lorem ipsum dolor sit amet, '); var tspan2 = add.tspan('consectetur adipiscing elit.').fill('#0af'); tspan1.clear(); }); text.move(20, 20); 参考文献・参考サイトまとめ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ChatworkでチームにToできる拡張機能

この記事 is 何? Chatworkってチーム機能があるじゃないですか? https://help.chatwork.com/hc/ja/articles/115000182342-%E3%83%81%E3%83%BC%E3%83%A0%E6%A9%9F%E8%83%BD%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6 チームをグループチャットと紐付けて、メンバーが追加されたときに 自動的にルームのメンバーにできる便利機能なんですよ。 でもチームに To する機能はないんですよね。 関係するチームに連絡するときにそのチームのマネージャーを窓口に相談するんだけど これもなんか違うと思うんですよね。 マネージャーがお休みだったらどうなる?  部署異動があるたびに、それを意識しないといけない? チームにTOしたいんだ。 Chrome拡張機能作った 完全にベータ扱いですが作りました。 https://chrome.google.com/webstore/detail/chatwork-mention-to-team/gbofncipdgnliehdohjimpdelcmpdeap チームを選ぶと、チームメンバーかつそのルームに所属しているメンバーのToに展開してくれます。 制限 チョット制限があります。 チームに所属するメンバーが50人以上になると全員には To できません。 個人の感想 自分が抱えていた問題は解決できました。 チームに To できるってやっぱイイ! 技術情報 Chatworkの非公式内部APIを叩きたい人向けの情報です。 リポジトリ Chatworkの非公式内部API Chatwork公式APIにはチームに関する操作APIがありません。 なので非公式APIを探します。 グループチャットにチームを追加するときに叩いているAPI参考に内部APIをたたきます。 Chatworkの内部非公式APIを呼び出すために必要なこと ログインセッション 設定により Chrome 拡張機能ではログインしているセッション情報をそのまま使えます https://github.com/yamatomo73/chatwork-mention-to-team/blob/main/manifest.json#L13 アクセストークン Chatwork のチャット画面のソースをみると javascript にアクセストークンがあります。 このトークンが必要です。 Chrome拡張でサイトコンテンツのjavascrip変数にアクセスするハック セキュリティにより、Chrome拡張側からそのままサイトコンテンツのjavascrip変数にアクセスできません。 ソースがあるので詳しく説明しませんがギミックをつかって取得します。 チーム機能に関するAPI(2021/11/30現在) コールしているAPIをトレースしてなんとなくコールしてします。 非公式でインターフェースが変わることもあるとおもうので、参考程度に。 組織の全チーム ** https://github.com/yamatomo73/chatwork-mention-to-team/blob/main/scripts/background.js#L47 特定のグループチャットに所属しているチーム一覧 ** https://github.com/yamatomo73/chatwork-mention-to-team/blob/main/scripts/background.js#L79 特定のチームを指定して所属しているメンバー一覧取得 ** https://github.com/yamatomo73/chatwork-mention-to-team/blob/main/scripts/background.js#L113 さいごに Chatwork公式APIになくても内部APIで機能をカスタマイズしてみました。 Chrome拡張をつくったのは初めてでしたが、おもったよりもサクッとつくれました。 公式APIがもっと増えたらいいのになあと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ChatworkでチームにToできるChrome拡張

この記事 is 何? Chatworkってチーム機能があるじゃないですか? チームをグループチャットと紐付けて、メンバーが追加されたときに 自動的にルームのメンバーにできる便利機能なんですよ。 でもチームに To する機能はないんですよね。 関係するチームに連絡するときにそのチームのマネージャーを窓口に相談するんだけど これもなんか違うと思うんですよね。 マネージャーがお休みだったらどうなる?  部署異動があるたびに、それを意識しないといけない? チームにTOしたいんだ。 ないなら、作る。作ったから話を聞いて欲しいんだ。 チームにToできるChrome拡張作った 完全にベータ扱いですが作りました。 デザインとか操作感とか作り込めていません? https://chrome.google.com/webstore/detail/chatwork-mention-to-team/gbofncipdgnliehdohjimpdelcmpdeap チームを選ぶと、チームメンバーかつそのルームに所属しているメンバーのToに展開してくれます。 個人の感想 自分が抱えていた問題は解決できました。 チームに To できるってやっぱイイ! 多数にToするのは心理的ハードルが… → 所属しているチームに機械的に送っているから恣意的な配慮をしてないから負担軽減 マネージャーにToしたけど反応してくれないな… 応答ないな、お休み? ミーティング中? たまたま離席してるだけ? マネージャーのカレンダー、勤怠報告、休暇予定などをあさる、という切ない作業をしていた過去 メンバー全員にToすると誰かが反応してくれる 担当者がいないのでちょっと待って / チームで分かっている人いない / あとでチーム内で確認して回答する  などなど 技術情報 Chatworkの非公式内部APIを叩きたい人向けの情報です。 リポジトリ Chatworkの非公式内部API Chatwork公式APIにはチームに関する操作APIがありません。 なので非公式APIを探します。 内部APIを探る ブラウザのデベロッパーツールを開く グループチャットにチームを追加する画面を開いたり、設定してみたり、チームを削除してみたり… コールされているAPIとレスポンス参考に必要なAPIを探す Chatworkの内部非公式APIを呼び出すために必要なこと ログインセッション 設定により Chrome 拡張機能ではログインしているセッション情報をそのまま使えます https://github.com/yamatomo73/chatwork-mention-to-team/blob/main/manifest.json#L13 アクセストークン Chatwork のチャット画面のソースをみると javascript にアクセストークンがあります。 このトークンが必要です。 Chrome拡張でサイトコンテンツのjavascrip変数にアクセスするハック セキュリティにより、Chrome拡張側からそのままサイトコンテンツのjavascrip変数にアクセスできません。 ソースがあるので詳しく説明しませんがギミックをつかって取得します。 チーム機能に関するChatwork内部API(2021/11/30現在) コールしているAPIをトレースしてなんとなくコールしてします。 非公式でインターフェースが変わることもあるとおもうので、参考程度に。 組織の全チーム https://github.com/yamatomo73/chatwork-mention-to-team/blob/main/scripts/background.js#L47 特定のグループチャットに所属しているチーム一覧 https://github.com/yamatomo73/chatwork-mention-to-team/blob/main/scripts/background.js#L79 特定のチームを指定して所属しているメンバー一覧取得 https://github.com/yamatomo73/chatwork-mention-to-team/blob/main/scripts/background.js#L113 さいごに Chatwork公式APIになくても内部APIを使いChrome拡張で実現してみました。 Chrome拡張をつくったのは初めてでしたが、おもったよりもサクッとつくれました。 公式APIがもっと増えたらいいのになあと思います。 参考にした記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pixi.jsで円同士の衝突判定を実装する

はじめに Pixi.jsを使って円同士の衝突判定を実装する機会があったので、初めてPixi.jsを使用する方に出来るだけ分かりやすくまとめていきます。 ① PIXI.Application の作成 まずは、PIXI.Applicationを作成します。 PIXI.Applicationとは土台のようなもので、ここに色々なオブジェクトを追加することで、画像を表示させたり、図形を描画したりできます。 PIXI.Applicationには色々なオプションを設定することができますが、今回はサイズ、背景色を設定します。 Canvasの縦幅、横幅は画面いっぱいにして、背景色は白色にします。 Pixi.jsでの色の指定は全て16進数で行うので、白色を設定する場合には0xffffffと記述します。 import * as PIXI from 'pixi.js' const app = new PIXI.Application({ width: window.innerWidth, height: window.innerHeight, backgroundColor: 0xffffff }); // bodyにApplication追加 document.body.appendChild(app.view); ② 衝突させる円を作成する 次に、衝突させる円を作成していきます。 円を作成するにはPIXI.Graphicsの drawCircle を使用し、色指定にはbeginFillを使用して ff00ff に塗りつぶします。 // 円の半径 const radius = 50 const circle1 = new PIXI.Graphics() .beginFill(0xff00ff) .drawCircle(0, 0, radius) .endFill() 次に、作成した円をPIXI.Spriteに追加します。 PIXI.Spriteは基本的に画像を表示させるために使用しますが、今回はGraphicを追加します。 PIXI.Graphicsで座標の情報を保持してしまうと、基準点が左上になってしまうので Drag & Dropの際にズレが生じてしまいます。 そのため、描画する座標の情報はPIXI.Spriteで保持し、基準点は中心に設定します。 circleSprite1.anchor.set(0.5) circleSprite1.position.set(200, 200) また、物体の衝突判定の際に使用するため、作成したSpriteの情報を配列に格納します。 最後にapp.stageに作成したSpriteを追加して表示させます。 以下、ここまでのコードの全容です。 // 円の情報をもつ配列 let circles = [] // 円の半径 const radius = 50 // 衝突させる円1 const circleSprite1 = new PIXI.Sprite() const circle1 = new PIXI.Graphics() .beginFill(0xff00ff) .drawCircle(0, 0, radius) .endFill() circleSprite1.addChild(circle1) circleSprite1.anchor.set(0.5) circleSprite1.position.set(200, 200) circles.push(circleSprite1) // 衝突させる円2 const circleSprite2 = new PIXI.Sprite() const circle2 = new PIXI.Graphics() .beginFill(0xff00ff) .drawCircle(0, 0, radius) .endFill() circleSprite2.addChild(circle2) circleSprite2.anchor.set(0.5) circleSprite2.position.set(400,400) circles.push(circleSprite2) app.stage.addChild(circleSprite1) app.stage.addChild(circleSprite2) ③ drag and dropで操作する円を作成する 次にdrag and dropで操作する円を作成します。 ①と同じ要領でPIXI.Graphicsで円を描画し、Spriteに追加します。 // dragさせる円の半径 const dragCircleRadius = 50 // dragさせる円 const dragCircle = new PIXI.Graphics() .beginFill(0x0000ff) .drawCircle(0, 0, dragCircleRadius) .endFill() const dragCircleSprite = new PIXI.Sprite() dragCircleSprite.addChild(dragCircle) Spriteのpointer up や pointer moveイベントを有効化するために、interactive を true に設定します。 わかりやすいように buttonMode もtrueにして、カーソルがpointerになるようにします。 dragCircleSprite.interactive = true dragCircleSprite.buttonMode = true 次に作成したSpriteにイベントを登録させます。 第一引数にイベントの種類、第二引数に実行する関数を指定します。 dragCircleSprite.on('pointerdown', onPointerDown) dragCircleSprite.on('pointerup', onPointerUp) 今ドラッグしているか否かの状態は isDragging で保持します。 // ドラッグ状態を管理 let isDragging = false // pointer down時のevent function onPointerDown(){ isDragging = true dragCircleSprite.on('pointermove', onPointerMove) } // pointer up時のevent function onPointerUp() { isDragging = false dragCircleSprite.removeListener('pointermove') } pointer moveイベントではドラッグ状態がtrueの場合のみ、Spriteの座標を現在のマウスの位置に合わせます。 引数からInteraction Eventを参照できるので、座標の情報はそこから取得します。 // pointer move時のevent function onPointerMove(e) { if(isDragging) { const target = e.currentTarget const position = e.data.getLocalPosition(app.stage) target.position.set(position.x, position.y) } } 以下ここまでのコードの全容です。 公式Exampleの drag and drop も参考にしてみてください。 // ドラッグ状態を管理 let isDragging = false // dragさせる円の半径 const dragCircleRadius = 50 // dragさせる円 const dragCircle = new PIXI.Graphics() .beginFill(0x0000ff) .drawCircle(0, 0, dragCircleRadius) .endFill() const dragCircleSprite = new PIXI.Sprite() dragCircleSprite.addChild(dragCircle) // interactiveをtrueにしないとイベントが発火しない dragCircleSprite.interactive = true dragCircleSprite.buttonMode = true // 初期のポジション dragCircleSprite.position.set(200,400) // アンカーポイントを真ん中に設定 dragCircleSprite.anchor.set(0.5) // pointerイベントの登録 dragCircleSprite.on('pointerdown', onPointerDown) dragCircleSprite.on('pointerup', onPointerUp) // pointer down時のevent function onPointerDown(){ isDragging = true dragCircleSprite.on('mousemove', onPointerMove) } // pointer up時のevent function onPointerUp() { isDragging = false dragCircleSprite.removeListener('mousemove') } // pointer move時のevent function onPointerMove(e) { if(isDragging) { const target = e.currentTarget const position = e.data.getLocalPosition(app.stage) target.position.set(position.x, position.y) } } app.stage.addChild(dragCircleSprite) ③ 円同士の衝突を判定するロジック では、本題の円同士の衝突判定です。 まず、当たり判定となる基準ですが、ドラッグしている円が既に描画されている円に衝突した時(重なった時)に当たり判定とします。 これを実現するために、2つの円それぞれの中心からの距離と半径の和を比べて、半径の和 > 2つの円の距離となれば当たり、と判定します。 2つの円の距離は三平方の定理を使って求めます。 直角三角形では、斜辺cの2乗は,他の辺a,bをそれぞれ2乗した数の和に等しい、というやつです。 この定理をどのように使用するかと言いますと、、、 図のように、2つの円の中心を結んだものを斜辺cとし直角三角形を作ると、それぞれx座標同士、y座標同士の距離が2辺 a, bとなり、 直角三角形では,2つの辺の長さがわかると,三平方の定理を使って他の1辺の長さが計算できるので、中心座標の数字がわかればcの長さを計算することが可能になります。 Math.hypot() 関数は、各引数の二乗の合計値の平方根を返しますので、これを使用すると簡単に上記の式が実現できそうです。 コードに落とし込むと、、、 const a = x1 - x2 const b = y1 - y2 const c = Math.hypot(a, b) if(c < radius + dragCircleRadius) { // 当たり return } else { // 当たってない } ④ pointer moveの時に衝突判定 上記のロジックをpointer move時のイベント関数内に実装します。 衝突判定させる円が複数個あるため、円をまとめた配列をループして判定するようにします。 衝突した時にSpriteの透明度を下げるようにします。 function onPointerMove(e) { if(isDragging) { const target = e.currentTarget const position = e.data.getLocalPosition(app.stage) target.position.set(position.x, position.y) for(let i = 0; i < circles.length; i++) { const centerPosition = { x: circles[i].x, y: circles[i].y } const a = centerPosition.x - position.x const b = centerPosition.y - position.y const c = Math.hypot(a, b) if(c < radius + dragCircleRadius) { dragCircleSprite.alpha = 0.3 return } else { dragCircleSprite.alpha = 1 } } } } いい感じになりました。 コードの全容はこちらのcodepen に載せましたのでよければご参考までに!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

お礼の品の組み合わせをMutationObserverで監視する

この記事は、トラストバンク Advent Calendar 2021の3日目です。 この度、ふるさとチョイスのトップページや控除金額シミュレーションに控除上限額の中でもらえるお礼の品の組み合わせをおすすめする機能(以降、お礼の品の組み合わせ表示と省略)がリリースされました。 忙しい年末のふるさと納税に、ぜひご活用ください! お礼の品の組み合わせ表示を制作するにあたり、JavaScriptの監視インスタンスであるMutationObserverを利用しました。 複雑なDOM操作をシンプルに実装することができるMutationObserverの活用方法を、お礼の品組み合わせ表示の紹介も交えつつ説明します。 お礼の品の組み合わせ表示の複雑な状態管理 お礼の品の組み合わせ表示とは、控除上限額に合わせてお礼の品を組み合わせ、合計金額と共に表示する機能です。 表示された組み合わせは、入れ替えるボタンや削除ボタンなどを用いて自分好みにカスタマイズ。 寄付するリストに追加をクリックすることで、一括でお礼の品を寄付するリストに追加することができます。 制作時に悩まされたのは、お礼の品が入れ替わる条件や動作の多さです。 全ての動作に対して個別に寄付金総額や品数の変更を実装すると、更新漏れやソースの難読化に繋がります。 そこで活用するのがMutationObserverです。 <div id="mutation-wrap"></div> const mutationWrapElement = document.getElementById('mutation-wrap'); const observer = new MutationObserver(() => { // ここに寄付金総額と品数の再計算を記述する }); observer.observe(mutationWrapElement, { childList: true }); #mutation-wrapにDOMが追加されるたび、MutationObserver.observeのcallbackとして指定した関数が実行され、寄付金総額と品数が再計算されます。 再計算を実行する条件が、#mutation-wrapにDOMを追加・削除するである限り、新たにJavaScriptを書く必要はありません。 今回は、親となるDOMの直下に発生する操作のみを監視したいため、オプションにはchildListを追加しました。 他にも指定できるオプションは様々です。 オプション名 内容 subtree 孫を監視対象に含める attributes 属性値の変更を監視 attributeOldValue 属性値が変更された際、変更前の値を取得する attributeFilter 監視したい属性値をフィルタリングする characterData テキストの変更を監視 characterDataOldValue テキストが変更された際、変更前の値を取得する 参考:DOM Living Standard - Interface MutationObserver MutationObserverを用いる際の注意事項 工夫次第で何にでも活用できるMutationObserverですが、利用にあたって注意すべき点がいくつか存在します。 DOMの追加・削除に伴うパフォーマンスの低下 監視したいDOMを単体で指定することができないため、DOMが操作された分だけcallback関数が実行されます。 Document.createDocumentFragmentやElement.innerHTMLなどを用い、なるべく一括で操作を実行するようにします。 // NG children.forEach(child => { // childを追加した分実行される obseverWrapper.appendChild(child); }); children.forEach(child => { // childを削除した分実行される obseverWrapper.removeChild(child); }); // OK const fragment = document.createDocumentFragment(); children.forEach(child => { fragment.appendChild(child); }); // 追加を1回にまとめたので、observeも1回だけ実行される obseverWrapper.appendChild(fragment); // 削除を1回にまとめたので、observeも1回だけ実行される obseverWrapper.innerHTML = ''; 参考:DOM Living Standard - Interface DocumentFragment 無限ループに陥る場合がある callback関数の中にMutationObserver.observeで監視しているDOMに対する処理を記述すると、場合によっては無限ループに陥ります。 MutationObserver.disconnectを用いて、一時的に監視を止めてから処理を実行しましょう。 // NG const mutationWrapElement = document.getElementById('mutation-wrap'); const observer = new MutationObserver(() => { // 属性値の変更が監視されているので、無限ループに陥る mutationWrapElement.children[0].classList.add('view'); }); observer.observe(mutationWrapElement, { childList: true, attributes: true }); // OK const mutationWrapElement = document.getElementById('mutation-wrap'); const observer = new MutationObserver(() => { // 監視を停止 observer.disconnect(); // 監視が停止しているので、無限ループを回避できる mutationWrapElement.children[0].classList.add('view'); // 監視を再開 observer.observe(mutationWrapElement, { childList: true, attributes: true }); }); observer.observe(mutationWrapElement, { childList: true, attributes: true }); まとめ JavaScriptには他にも、交差監視を実現するIntersectionObserverやサイズの変化を監視するResizeObserverなど、多くの監視インスタンスが実装されています。 それぞれをうまく取り入れることで、いつかはフレームワークを利用しなくても、直感的で見やすいソースコードが書ける時代が来るかもしれません。 明日の記事は、@a_wis1056さんです。お楽しみに。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【javascript】例外処理, try

例外処理 エラーが発生した時に飛ぶ特別な処理 tryの基礎構文 try { //try直下の構文に何らかのerrorが発生したらcatch処理へ console.log('start') throw new Error('error message'); console.log('end') } catch(e){ console.error(e); } finally { //終了処理は必ず通る。 console.log('bye') } case 取得したいjsonファイルが空だったらerrorを出力する async function fetchUsers(){ const response = await fetch('users.json') if(response.ok){ //responseには成功可否を判断するokプロパティが存在する。 const json = await response.json(); //jsonファイルにアクセス。 if(!json.length){ //jsonデータが空だったらtrue throw new Error('no data found') } return json; } } async function init(){ try { const users = await fetchUsers(); //上で作成した関数を実行 for(const user of users){ console.log(`私は ${user.name}, ${user.age} 才です。`) } }catch(e){ //errorが飛んできたらここを処理 console.error(e); } finally { //終了処理は必ず行う console.log('bye') } } init();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JavaScript] sort()の使い方

文字列のソートにsort()を使用したので使い方と合わせて数値のソートについてもまとめておく。 sort()の使い方 このsort()は、配列の要素を順番に並び替えてくれるメソッド。このメソッドは、配列の要素を文字列に変換してから比較を行い、昇順にソートされる。そのため、文字列を比較する場合は次のように書ける。 array = ["red", "white", "blue", "green", "black"]; array.sort(); // [ 'black', 'blue', 'green', 'red', 'white' ] ここで、ソートされた結果は元の配列(上記だとarray)に上書きされるので、元の配列データは変更されてしまうことに注意が必要である。元の配列のデータを保持しておきたい場合は、あらかじめ配列をコピーしておく必要がある。 数値の比較の場合 上記のように文字列の比較はそのままメソッドを使用すればよいが、数値を比較する場合は比較関数を指定して記述する。比較関数はアロー関数(ラムダ式)を使って書くこともできる。一次元配列の数値のソートを行う場合は以下のように書ける。 // 昇順ソート array = [130, 90, 30, 7, 50]; array.sort((a,b) => a - b); // [ 7, 30, 50, 90, 130 ] // 降順ソート array = [130, 90, 30, 7, 50]; array.sort((a,b) => b - a); // [ 130, 90, 50, 30, 7 ]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【js基礎】動的import【ES6備忘録】

動的importとは こんな感じで関数でESmoduleを扱えるやつです。 ファイル構成 nodeのバージョンはv16.10.0です ESmoduleを扱うためにはpackage.jsonのtypeをmoduleにするか、拡張子をmjsにする必要があります。 結果 export.js export const hello = () => console.log("hello!") import.js //hello! (async () => { let module = await import("./export.mjs") module.hello() } )() //hello! import("./export.mjs").then(module => module.hello()) 戻り値はPromise
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Harmoware-VIS APIリファレンス

前提 Harmoware-VISについては別記事 「Harmoware-VISの紹介」 「Harmoware-VIS はじめに」 をご覧ください。 APIリファレンス Harmoware-VISのAPIのリファレンスを解説します。 Properties ルートで受け取る「props」オブジェクト内の項目を以下に示します。 actions 以下のPropertiesを更新するためのメソッド群 詳細は各項目の欄を参照 animatePause (Boolean) ・Default : false trueでアニメーションが一時停止。 値更新:actions.setAnimatePause animateReverse (Boolean) ・Default : false trueでアニメーションを再生。 値更新:actions.setAnimateReverse bounds (Object) ・Default : ※後述 シミュレーション領域の境界位置。 移動体データ設定時(actions.setMovesBase)に設定データ内のboundsをコピー 設定データ内にboundsが無い場合はDefault値のまま ※boundsのDefault値 westlongitiude(number) 最西端の経度位置 Default : 0 eastlongitiude(number) 最東端の経度位置 Default : 0 southlatitude(number) 最南端の緯度位置 Default : 0 northlatitude(number) 最北端の緯度位置 Default : 0 defaultPitch (Number) ・Default : 30 視点ピッチ(角度)のデフォルト値。 値更新:actions.setDefaultPitch 値更新:actions.setDefaultViewport sample.js actions.setDefaultViewport({defaultPitch:45, defaultZoom: 15}) defaultZoom (Number) ・Default : 11.1 デフォルトのズーム値。 値更新:actions.setDefaultViewport depotsBase (Object Array) ・Default : [] 停留場の位置情報の登録データフォーマット。 (Harmoware-VIS提供の「DepotsLayer」用のデータ) 値更新:actions.setDepotsBase ※depotsBaseのフォーマット(setDepotsBaseで設定するデータのフォーマット) sample.json [ { "position": [999.9999, 999.9999, 999], ...anyKey, ・・・・・・ },・・・・・・ ] or [ { "longitude": 999.9999, "latitude": 99.9999, ...anyKey, ・・・・・・ },・・・・・・ ] position (number[], required) 停留場の位置情報 [ 経度、緯度、[標高] ](標高は省略可) longitude(number, optional) 停留場の位置情報 [ 経度 ] latitude(number, optional) 停留場の位置情報 [ 緯度 ] type (string, optional) 停留場の種別を識別する際に設定 ...any key (any, optional) 必要に応じて設定。 depotsData (Object Array) ・Default : [] 停留場の位置情報の表示データフォーマット。 DepotsLayer表示用データとしてdepotsBaseを元に作成されます。 基本的に「depotsBaseのフォーマット」と同じフォーマット(※)ですが、getDepotsOptionFuncにコールバック関数を登録してカスタマイズが可能です。 ※depotsBaseでの位置情報がlongitudeとlatitudeで定義されている場合はpositionに変換されます。 sample.json [ { "settime": 9999999999, "position": [999.9999, 999.9999, 999], ...anyKey, ・・・・・・ },・・・・・・ ] settime(number) このdepotsDataを作成した時点のシミュレーション日時のUNIX時間(秒)を編集 position (number[]) depotsBaseより編集 ...any key (any, optional) depotsBaseのposition、longitude、latitude以外の項目とコールバック関数(getDepotsOptionFunc)より編集 getDepotsOptionFunc (Function) ・Default : null 停留場情報の表示データをカスタマイズするコールバック関数を設定。 値更新:actions.setDepotsOptionFunc オプショングラフの動的更新時などで使用。 コールバック関数の書式 <P>(props: P, i: number) => object 戻り値のobjectはdepotsData生成時にマージされます。 props(object) 最新のpropsデータ i(number) depotsBase配列の処理中要素インデックス getMovesOptionFunc (Function) ・Default : null 移動体情報の表示データをカスタマイズするコールバック関数を設定。 値更新:actions.setMovesOptionFunc オプショングラフの動的更新時などで使用。 コールバック関数の書式 <P>(props: P, i: number, j: number) => object 戻り値のobjectはmovedData生成時にマージされます。 props(object) 最新のpropsデータ i(number) movesbase配列の処理中要素インデックス j(number) movesbase配列の処理中要素のoperationの処理中要素インデックス つまり movesbase[i].operation[j] を指す leading (Number) ・Default : 100 シミュレーション開始時刻の前に追加する空白時間(単位:秒) 値更新:actions.setLeading movesbase (Object Array) ・Default : [] 移動体の経路情報のデータ。 ※movesbaseのフォーマット sample.json [ { "type": "xxxxx", "movesbaseidx": 9999999999, "departuretime": 9999999999, "arrivaltime": 9999999999, "operation": [ { "elapsedtime": 9999999999, "position": [999.9999, 999.9999, 999], "direction": 999, ...anyKey, ・・・・・・ },・・・・・・ ], ...anyKey, ・・・・・・ },・・・・・・ ] type(string) 移動体の識別情報を編集(定義有無は元データによる) movesbaseidx(number) movesbase配列のインデックス値を編集 departuretime(number) 移動体の出発時のelapsedtimeの値をUNIX時間(秒)で編集 arrivaltime(number) 移動体の到着時のelapsedtimeの値をUNIX時間(秒)で編集 operation(object[]) 移動体のルートと経過時間を移動体毎の配列で設定 elapsedtime(number) positionの通過時間をUNIX時間(秒)で設定 position (number[]) 移動体のelapsedtime時点の位置情報 [ 経度、緯度、標高 ] direction(number) 移動体の方向(角度) ...any key (any, required) setMovesBaseなどのデータ設定用のメソッドで設定したデータの項目より編集 ※移動体の経路情報のデータ設定用のメソッド 値更新:actions.setMovesBase 値更新:actions.updateMovesBase ※設定するデータのフォーマット sample.json // フォーマット例1 { "timeBegin": 9999999999, "timeLength": 99999, "elapsedtimeMode": "UNIXTIME", "bounds": { "northlatitude": 99.99999, "southlatitude": 99.99999, "westlongitiude" 999.99999, "eastlongitiude": 999.99999, }, "movesbase": [ { "type": "xxxxx", "operation": [ { "elapsedtime": 99999, "position": [999.9999, 999.9999, 999], "longitude": 999.9999, "latitude": 99.999, ...anyKey, ・・・・・・ },・・・・・・ ], ...anyKey, ・・・・・・ },・・・・・・ ], ...anyKey, ・・・・・・ } // フォーマット例2 [ { "type": "xxxxx", "operation": [ { "elapsedtime": 9999999999, "position": [999.9999, 999.9999, 999], "longitude": 999.9999, "latitude": 99.999, ...anyKey, ・・・・・・ },・・・・・・ ], ...anyKey, ・・・・・・ },・・・・・・ ] timeBegin(number, optional) シミュレーション開始日時のUNIX時間(秒) timeLength(number, optional) シミュレーション開始から終了までの時間長(秒) elapsedtimeMode(string, optional) movesbase.operationのelapsedtimeをUNIX時間(秒)で指定する場合は"UNIXTIME"を設定 bounds(object, optional) シミュレーションエリアの範囲宣言(東西南北端) movesbase(object[], required) 移動体のルートと経過時間、識別情報などを設定 type(string, optional) 移動体の識別情報を設定 operation(object[], required) 移動体のルートと経過時間を移動体毎の配列で設定 elapsedtimeの値が異なる2個以上の要素が無い場合は表示対象外 elapsedtime(number, required) フォーマット例1の場合は上記のtimeBeginからの経過時間(秒)で設定 但し、timeBeginが未定義の場合はUNIX時間(秒)で設定 更に、timeBeginが定義済かつelapsedtimeModeに"UNIXTIME"が定義済の場合はUNIX時間(秒)で設定 フォーマット例2の場合はUNIX時間(秒)で設定 position (number[], required) 移動体のelapsedtime時点の位置情報 [ 経度、緯度、[標高] ](標高は省略可) 位置情報をpositionまたはlongitude-latitudeで指定 positionとlongitude-latitudeの両方が定義された場合はpositionを優先で採用 longitude(number, required) 移動体のelapsedtime時点の位置情報 [ 経度 ] latitude(number, required) 移動体のelapsedtime時点の位置情報 [ 緯度 ] ...any key (any, required) 必要に応じて設定。 movedData (Object Array) ・Default : [] MovesLayer表示用データとしてmovesbaseを元に作成されます。 ※movedDataのフォーマット sample.json [ { "settime": 9999999999, "movesbaseidx": 9999999999, "type": "xxxxx", "position": [999.9999, 999.9999, 999], "sourcePosition": [999.9999, 999.9999, 999], "targetPosition": [999.9999, 999.9999, 999], "direction": 999, "color": [999, 999, 999], "sourceColor": [999, 999, 999], "targetColor": [999, 999, 999], ...anyKey, ・・・・・・ }, ・・・・・・ ] settime(number) このmovedDataを作成した時点のシミュレーション日時のUNIX時間(秒)を編集 movesbaseidx(number) このmovedDataに対応するmovesbase配列のインデックス値を編集 type(string) movesbaseより編集(定義有無はmovesbaseによる) position (number[]) movesbaseより編集 sourcePosition(number[]) 上記positionと同値 targetPosition(number[]) 次のデータのpositionと同値 direction(number) 移動体の方向(角度) color(number[]) movesbaseより編集(未定義時は[0,255,0]:”green”) sourceColor(number[]) 上記colorと同値 targetColor(number[]) 次のデータのcolorと同値(未定義時は[0,255,0]:”green”) ...any key (any, optional) setMovesBaseなどのデータ設定用のメソッドで設定したデータの項目とコールバック関数(getMovesOptionFunc)より編集 ExtractedData (any) ・Default : undefined getExtractedDataFunc関数で生成&登録したデータ。 getExtractedDataFunc (Function) ・Default : null 再生時の画面更新タイミングで表示させるデータを生成&登録する関数。 値更新:actions.setExtractedDataFunc movesbaseではコントロールできないカスタムデータを処理する関数 コールバック関数の書式 <P>(props: P) => any 戻り値の値がそのままExtractedDataに設定されます。 props(object) 最新のpropsデータ clickedObject (Object Array) ・Default : null クリックされた移動オブジェクト参照データの配列。 Harmoware-VIS標準機能で使用する情報。 クリックしたアイコンの情報を保持。 値更新:actions.setClicked routePaths (Object Array) ・Default : [] クリックされた移動オブジェクトの経路データの配列。 Harmoware-VIS標準機能で使用する情報。 クリックしたアイコンの経路情報を保持。 値更新:actions.setRoutePaths ※routePathsのフォーマット sample.json [ { "movesbaseidx": 9999999999, "sourcePosition": [999.9999, 999.9999, 999], "targetPosition": [999.9999, 999.9999, 999], "routeColor": [999, 999, 999], "routeWidth": 999, }, ・・・・・・ ] movesbaseidx(number) このroutePathsに対応するmovesbase配列のインデックス値を編集 sourcePosition(number[]) 線分始端位置 [ 経度、緯度、標高 ] targetPosition(number[]) 線分終端位置 [ 経度、緯度、標高 ] routeColor(number[]) 線分色 routeWidth(number) 線分幅(マップ縮尺でのメートル) secperhour (Number) ・Default : 180 再生速度(秒/時) 値更新:actions.setSecPerHour secperhour値を更新時にはmultiplySpeed値も対応する値に更新 multiplySpeed (Number) ・Default : 20 再生速度(倍速) 値更新:actions.setMultiplySpeed multiplySpeed値を更新時にはsecperhour値も対応する値に更新 settime (Number) ・Default : 0 再生時間(UNIX時間) 再生時には再生時刻に自動で更新 任意の値に更新する場合は以下のメソッド使用 値更新:actions.setTime 値を加減算する場合は以下のメソッド使用() 値更新:actions.addMinutes sample.js actions.addMinutes(10) // 指定の単位は分 timeBegin (Number) ・Default : 0 シミュレーション開始時刻(UNIX時間) 移動体の経路情報のデータ設定時に自動で設定 間接更新:actions.setMovesBase 間接更新:actions.updateMovesBase 任意の値に更新する場合は以下のメソッド使用 値更新:actions.setTimeBegin timeLength (Number) ・Default : 0 シミュレーション時間長(秒) 移動体の経路情報のデータ設定時に自動で設定 timeBegin値の時刻よりtimeLength後がシミュレーション終了時刻 間接更新:actions.setMovesBase 間接更新:actions.updateMovesBase 任意の値に更新する場合は以下のメソッド使用 値更新:actions.setTimeLength trailing (Number) ・Default : 180 シミュレーション終了後に追加する空白時間(単位:秒) 値更新:actions.setTrailing viewport (Object) ・Default : ※後述 マップ表示の視点情報(ビューポート)を指定 値更新:actions.setViewport ※viewportのDefault値 longitude(number,option) マップ表示中心経度 Default : 136.906428 latitude(number,option) マップ表示中心経度 Default : 136.906428 zoom(number,option) マップ表示ズーム値 Default : 11.1 maxZoom(number,option) マップ表示ズーム最大値 Default : 18 minZoom(number,option) マップ表示ズーム最小値 Default : 5 pitch(number,option) マップ表示視点角度値 Default : 30 bearing(number,option) マップ表示視点方角値 Default : 0 transitionDuration(number,option) マップ表示視点移動時間(ミリ秒) Default : 0 マップ位置移動時にかける時間(ミリ秒)を設定。 ゼロ以上の場合は移動がアニメーション化する ...any key (any, optional) その他react-map-glで扱う視点情報 linemapData (Object) ・Default : [] 線画LineMapLayer表示用データフォーマット 値更新:actions.setLinemapData ※linemapDataのフォーマット sample.json [ { "sourcePosition": [999.9999, 999.9999, 999], "targetPosition": [999.9999, 999.9999, 999], "color": [999, 999, 999], "path": [[999.9999, 999.9999, 999], ・・・・・・], "polygon": [[999.9999, 999.9999, 999], ・・・・・・], "coordinates": [[999.9999, 999.9999, 999], ・・・・・・], "elevation": 999, "strokeWidth": 999, "dash": [99, 99], }, ・・・・・・ ] sourcePosition(number[],option) LineMapLayer内のLineLayerで表示する直線始端位置 [ 経度、緯度、標高 ] targetPosition(number[],option) LineMapLayer内のLineLayerで表示する直線終端位置 [ 経度、緯度、標高 ] color(number[],option) 線分色 path(number[][],option) LineMapLayer内のPathLayerで表示する線分パス情報([ 経度、緯度、標高 ]の配列) polygon(number[][],option) LineMapLayer内のPolygonLayer(3D)で表示する線分パス情報([ 経度、緯度、標高 ]の配列) coordinates(number[][],option) LineMapLayer内のPolygonLayer(2D)で表示する線分パス情報([ 経度、緯度、標高 ]の配列) elevation(number,option) LineMapLayer内のPolygonLayerで表示する高さ情報 strokeWidth(number,option) LineMapLayer内のLineLayerとPathLayerで表示する線幅情報(マップ縮尺でのメートル) dash(number[],option) LineMapLayer内のPathLayerで表示する破線指示 破線指示情報の設定値の詳細はこちら loading (Boolean) ・Default : false trueで読み込み中のアイコンを表示。 (Harmoware-VIS提供のLoadingIconを使用時に有効) 値更新:actions.setLoading inputFileName (Object) ・Default : {} 画面表示用の入力ファイル名(※後述) Harmoware-VIS提供のMovesInputDepotsInputLinemapInputコンポーネントを用いてファイルデータを読み込んだ場合にファイル名を設定 ユーザで別のファイルを使用する際に使用しても構いません。 値更新:actions.setInputFilename sample.js actions.setInputFilename({customFileName:"xxxxxxxxxx.xxx"}) sample.json { "movesFileName": "xxxxxxxxxx.json", "depotsFileName": "xxxxxxxxxx.json", "linemapFileName": "xxxxxxxxxx.json", } movesFileName(number[],option) MovesInputコンポーネントでファイルを読み込んだ場合にファイル名を設定 depotsFileName(number[],option) DepotsInputコンポーネントでファイルを読み込んだ場合にファイル名を設定 linemapFileName(number[],option) LinemapInputコンポーネントでファイルを読み込んだ場合にファイル名を設定 noLoop (Boolean) ・Default : false trueでシミュレーション終了後に一時停止する。(ループしない) 値更新:actions.setNoLoop initialViewChange (Boolean) ・Default : true trueで移動体データを読み込んだ後、マップ表示の視点をデータ群の平均的な中心位置に移動 falseの場合は移動体データを読み込み後、マップ表示の視点移動しない(そのまま) 値更新:actions.setInitialViewChange iconGradation (Boolean) ・Default : false trueで移動体アイコンのカラー(movedData)変更を経過時間で遷移するグラデーション化 ⇒A地点(赤指定)→B地点へ(青指定)の場合、A→B移動中に徐々に色が変化 falseの場合、B地点到着まで赤のままで到着すると青に変化 値更新:actions.setIconGradationChange getCombinedReducer reduxのcreateStoreに渡すことができるHarmoware-VISのリデューサ関数を取得します。 また、引数にリデューサ関数を追加で記述することで、Harmoware-VISのリデューサ関数と統合することができます。 sample.js import { render } from 'react-dom'; import { getCombinedReducer } from 'harmoware-vis'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; import React from 'react'; import App from './containers'; import 'harmoware-vis/scss/harmoware.scss'; // 個別リデューサ関数の追加がない場合 const store = createStore(getCombinedReducer({customReducer})); // 個別リデューサ関数を追加する場合の例 import customReducer from './customReducer'; const store = createStore(getCombinedReducer({customReducer})); render( <Provider store={store}> <App /> </Provider>, document.getElementById('app') ); Container React.Componentを継承したクラスです。 Harmoware-VISライブラリのベースとなるコンポーネントで、settimeとanimation frameを更新します。 renderメソッドでDOMを出力してください。 sample.js // using mapbox import React from 'react'; import { Container, connectToHarmowareVis, HarmoVisLayers, ... } from 'harmoware-vis'; class App extends Container { render() { const { viewport, actions, ... } = this.props; return ( <HarmoVisLayers viewport={viewport} actions={actions} mapboxApiAccessToken={ ... } layers={[ ... ]} /> ); } } export default connectToHarmowareVis(App); connectToHarmowareVis connectToHarmowareVisの状態をコンテナ・コンポーネントのpropに同期させるためのUtilityです。 sample.js import { Container, connectToHarmowareVis } from 'harmoware-vis'; class App extends Container { ・・・ } // 追加のアクションが無い場合 export default connectToHarmowareVis(App); // 追加のアクションが有る場合 import * as moreActions from '../actions'; export default connectToHarmowareVis(App, moreActions); HarmoVisLayers deck.glのLayerクラスを継承したレイヤーを、mapboxで取得した地図上に表示します。 sample.js <HarmoVisLayers viewport={this.props.viewport} actions={this.props.actions} mapboxApiAccessToken={MAPBOX_TOKEN} layers={ [ ... ] } mapGlComponents={ this.getComponentsDom(this.props.movedData) } // Example /> visible(Boolean,option) ・Default : true trueの場合地図レイヤーを表示 actions(object,required) Propertiesのactionsを設定 APIリファレンスはリンク先を参照 viewport(object,required) Propertiesのviewportを設定 APIリファレンスはリンク先を参照 mapboxApiAccessToken(string,required) mapbox.comのアクセストークンを設定 但し、上記visibleでfalseを設定した場合は有効なアクセストークンである必要はない mapStyle(string,option) ・Default : 'mapbox://styles/mapbox/dark-v8' style URL を設定(Mapboxの style URL の情報はリンク先を参照) layers(array,required) レイヤーインスタンスの配列 レイヤーは、deck.glのLayerクラスを継承したレンダリングコンポーネントです。 onViewportChange(Function,option) ・Default : PropertiesのactionsのsetViewport viewportを更新するインターフェスを設定 mapGlComponents(any,option) ・Default : null "react-map-gl" カスタムデータ追加インターフェース カスタムデータ追加に関する情報は”react-map-gl”のリンク先を参照 mapboxAddLayerValue(mapboxgl.Layer[],option) ・Default : ※下記参照 Mapboxマップに重ねるレイヤーを設定 Mapboxマップに重ねるレイヤーに関する情報はリンク先を参照 mapboxAddLayerValue_Default.json [{ "id": "3d-buildings", "source": "composite", "source-layer": "building", "filter": ["==", "extrude", "true"], "type": "fill-extrusion", "paint": { "fill-extrusion-color": "#888", "fill-extrusion-height": ["interpolate", ["linear"], ["zoom"], 5, 0, 5.05, ["get", "height"]], "fill-extrusion-base": ["interpolate", ["linear"], ["zoom"], 5, 0, 5.05, ["get", "min_height"]], "fill-extrusion-opacity": 0.6 }, },{ "id": "sky", "type": "sky", "paint": { "sky-type": "atmosphere", "sky-atmosphere-sun": [180.0, 60.0], "sky-atmosphere-sun-intensity": 5 } }] mapboxAddSourceValue(object {id:string, source:object}[],option) ・Default : undefined Mapboxマップに重ねるソースを設定 Mapboxマップに重ねるソースに関する情報はリンク先を参照 terrain(Boolean,option) ・Default : false trueで3Ⅾ地形表示(下記terrainSourceとsetTerrainのデータを使用) Mapboxでの3Ⅾ地形表示に関する情報はリンク先を参照 terrainSource(object{id:string,source:object},option) ・Default : ※下記参照 terrainSource_Default.js { id:'mapbox-dem', source:{ 'type': 'raster-dem', 'url': 'mapbox://mapbox.mapbox-terrain-dem-v1', }} setTerrain(object { source:string, exaggeration?:number},option) ・Default : ※下記参照 setTerrain_Default.js {source:'mapbox-dem'} Layersリファレンス Harmoware-VISのLayersリファレンスについては別記事「Harmoware-VIS Layersリファレンス」をご覧ください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ウェブサイト作成用備忘録・20号:動画素材の取り扱いについての備忘録

今まではwebサイトの作成を通して基本的なプログラミングの学習を行ってきましたが、現在は更にAdobeツール各種も使用するようになりつつあり、一つの演出を実装するのに複数の方法を選択する事が出来るようになってきたと思います。 今回は動画素材の取り扱いを中心にwebサイトへの実装方法のアイデアを書き留めておこうと思います。 ・まずは著作権フリーの動画素材を探してくる 自分は下記のサイトを利用する事が多いです。(自前で動画を撮影したりAdobeツール等で動画自体を自作しようとすると、流石に学習が脱線してしまうので…) その他にも下記の記事で様々なフリー動画素材を扱うサイトが紹介されていたのでコチラもオススメです。 ・動画素材をwebサイトの背景として使用 例えば下記のサイトの様にページ背面全体に動画背景を実装して動きのあるサイトが作りたい時 フリー素材の中には最初からループ再生が可能なように編集してある動画もありますが、「今回作りたいサイトの雰囲気に合っている動画に限ってループ再生に対応していない…!」みたいな事が良くありました。 動画をループ再生に対応させるには、動画の始点と終点を一致させる必要があります。 主な方法は 動画の始点と終点にフェードイン・フェードアウトを追加し、動画の切れ目に暗転を加える事で始点と終点を一致させる 動画Aと動画Bの2種類の素材を用意し、「動画A→動画B→動画A」の順番にクロスフェードを追加し、始点と終点が一致する箇所でトリミングする(動きの少ない抽象的な動画なら動画Aだけのクロスフェードでも違和感のないループが成立します) 動画素材が再生し終わったタイミングでコピーした動画素材を逆再生させて元動画と繋ぎ、始点と終点を一致させる 動画内で始点と終点が一致している箇所がある場合、ループ可能な部分だけを元の動画素材からトリミングする これらを実現するには予め動画編集ソフトで動画素材をループ再生に対応させてから使用するか、未加工の動画をwebサイト上でループ再生に対応させることになりますが、それぞれメリットデメリットが存在していると考えています。 対処法1:動画編集で何とかする メリット ソースコードがシンプルにまとまる デメリット 動画編集ソフトが必要 後々の変更や修正に弱くなる 対処法2:コーディングで何とかする メリット 後々の変更や修正に強い javascriptによる動的処理との組み合わせが可能 動画編集ソフトが不要 デメリット 実装・デバッグに時間が掛かる ソースコードが複雑になる為、様々な環境から閲覧した際に不具合が起きやすくなる 動画編集ソフトで予めループ再生に対応させておくことで、HTMLでvideoタグを読み込み、動画背景としてCSSを調整するだけで実装が可能になります。 その代わり、予め「動画A→動画B→動画A」とクロスフェードさせて作成した動画素材が、後々「動画A→動画B→動画C→動画A」等に構成を変更しなければならない場合には1から作り直しになってしまう事もあります。 逆に、全てをコーディングで賄う場合、一度ループ化の仕組みを構築してしまえば上記の様な変更にも柔軟に対応できる反面、ソースコードが複雑になるほど様々な環境からwebサイトを閲覧した場合には不具合が起きやすくなる他、HTML上で複数の動画を読み込まなればならない為、動画のサイズによってはページの読み込みが重くなってしまうと思われます。 個人的には動画を取り扱う際はなるべく事前に編集ソフトで必要な工程を済ませておき、コーディングはなるべくシンプルにまとめておく方がトータルでは楽に実装が出来ると考えています。動画編集ソフトに編集ファイルのバックアップを残しておけば、最低限の修正で対応可能なのも大きいですね。 ・今後やってみたい表現リスト 動画サムネイルのホバーエフェクト javascriptのonmouseoverイベントパンドラでマウスオーバーとHTMLMediaElement.playbackRateを組み合わせて、videoタグへのホバー操作に反応して動画をスロー再生したり、ホバーした時だけ動画が再生されるボタン 動画サムネイルクリック時のページ遷移アクション 他のページにリンクしている動画サムネイル型のボタンをクリックした際に、ページが動画にズームして動画に飛び込んでいく様なアクション 等々…まだまだやってみたい事は色々あるので、引き続き幅広く学習を続けていきたいと考えています。 自分用の備忘録ではありますが、誰かの参考になれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascriptで配列や連想配列を並び替える方法

きっかけ インターン先でAPI側から取ってきたデータ(連想配列)をAlphabet順にsortしたかったが、一度もsortメソッドを使ったことがなかったのと比較関数が特殊だったのでまとめてみることにした。 概要 配列や連想配列をアルファベット順や数値の昇順、降順に並べたい。下記のarray.sortを使い並び替えてみる。 やってみたこと 文字列の配列をアルファベット順にする方法 数値の配列を昇順に並び替える方法 数値の配列を降順に並び替える方法 文字列の配列をアルファベット順に並び替える方法(nameプロパティ) 数値の配列を昇順に並び替える方法(scoreプロパティ) 数値の配列を降順に並び替える方法(scoreプロパティ) 配列 arrayAlphabet = ['kamome','ari','tonbo','batta','tomato'] arrayNumber = [3,1,5,8,-1,0] 文字列の配列と数値の配列を用意した。 1.文字列の配列をアルファベット順に並び替える方法 arrayAlphabet = ['kamome','ari','tonbo','batta','tomato'] arrayAlphabet.sort() console.log(arrayAlphabet) Array ["ari", "batta", "kamome", "tomato", "tonbo"] sortメソッドの引数を空にすればアルファベット順に並び替えることができる!ものすごく簡単 2.数値の配列を昇順に並び替える方法 上記のことをなすにはsortメソッドの引数に比較関数を用いなければならない。 sortメソッドの引数に比較関数を使用する 引数は配列から2つのa,bという値を取り出す。この2つの値を比較することで配列を並び替える。 関数の戻り値が正の時 → 引数1を引数2の後ろに並べ替え。 関数の戻り値が負の時 → 引数1を引数2の前に並べ替え。 関数の戻り値が0の時 → 何もしない 数値の配列を昇順にするを比較関数を使って書いてみる arrayNumber = [3,1,5,8,-1,0] arrayNumber.sort((a,b)=>{ return a - b }) console.log(arrayNumber) Array [-1, 0, 1, 3, 5, 8] 例えば 3(引数1),1(引数2) を3から1を引いて答えが 正 → 3を1の後ろに加える。 負 → 3を1の前に加える。 0 → 移動しない。 この場合であれば3−1は正であるため3を1の後ろに加える。今回は3,1で考えたが他の要素同士を比較して配列を並び替えることができる。 3.数値の配列を降順に並び替える方法 先ほど考え方を使えば降順は下記のようなコードで実装できる arrayNumber = [3,1,5,8,-1,0] arrayNumber.sort((a,b)=>{ return b - a }) console.log(arrayNumber) Array [8, 5, 3, 1, 0, -1] 連想配列 userList = [{name: 'amazon',score: 72}, {name: 'google',score: 11}, {name: 'facebook',score: 100}] 上記のような連想配列を作る。 1.文字列の配列をアルファベット順に並び替える方法(nameプロパティ) userList.sort((a,b)=>{ if(a.name < b.name) return -1; else if(a.name > b.name) return 1; return 0; }) console.log(userList) Array [Object { name: "amazon", score: 72 }, Object { name: "facebook", score: 100 }, Object { name: "google", score: 11 }] 上記では 例えば引数一番目amazon二番目にgoogleをとり、aの名前の方が小さい(アルファベットの最初の方)であれば負の値を返している(配列の最初に持ってくる) 補足 大文字の方が基本的に優先されるため、大文字小文字の区別を消すには以下のような記述を比較関数の最初に記述してあげれば良い a = a.toString().toLowerCase(); b = b.toString().toLowerCase(); 2.数値の配列を昇順に並び替える方法(scoreプロパティ) userList.sort((a,b)=>{ return a.score - b.score }) console.log(userList) Array [Object { name: "google", score: 11 }, Object { name: "amazon", score: 72 }, Object { name: "facebook", score: 100 }] 3.数値の配列を降順に並び替える方法(scoreプロパティ) userList.sort((a,b)=>{ return b.score - a.score }) console.log(userList) Array [Object { name: "facebook", score: 100 }, Object { name: "amazon", score: 72 }, Object { name: "google", score: 11 }] まとめ 比較関数は引数を二つしか取らないので気持ち悪い気がする、全ての要素を比べてる感がなかったが使っていくうちに慣れた。またsortメソッドは元の配列を変更してしまう破壊的メソッドなので使い所には注意したい。 参考にした記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[TypeScript] 小数点以下の桁数を指定して四捨五入する

const roundWithScale = (value: number, scale: number) => { return Math.round(value * 10 ** scale) / 10 ** scale; }; roundWithScale(1.456789, 0) // 1 roundWithScale(1.456789, 1) // 1.5 roundWithScale(1.456789, 2) // 1.46 roundWithScale(1.456789, 3) // 1.457
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】モジュール⑤ Strictモード

はじめに Udemyの【JS】ガチで学びたい人のためのJavaScriptメカニズムの講座の振り返りです。 前回の記事 目的 モジュールについての理解を深める 本題 1.Strictモード Strictモードとは、通常JavaScriptで使用できる(許容されている)一部の書き方を制限する なぜ制限するのかというと主な目的は以下の三つ 意図しないバグの混入の防止 予約後の確保 コードのセキュア化 大規模開発に対応できるようにしている Strictモードを有効化するにはファイルの先頭,もしくは関数内の先頭行に"use strict"と記述する 例1 main.js a = 0; console.log(a); 上記の場合ではそのまま0が出力されるが main.js "use strict" a = 0; // ReferenceErrorとなる console.log(a); 関数も同様 main.js a = 0; console.log(a); function fn() { "use strict" b = 1 console.log(b); } fn(); // この場合、bのみエラーとなる 従来のJSでは、エラーとならなかったものをエラーとすることで、バグが発生しにくいように制限を加えている 例2 予約後の確保の例 将来JSのアップグレードがあった際に変数として使えない単語がある main.js function fn() { "use strict" // b = 1 // Uncaught SyntaxError: Unexpected strict mode reserved wordと表示され使用不可 const implements, interface, packages } fn(); // console.log(b); 例3 セキュアなコードについて main.js function fn() { // このthisはWindowオブジェクトをさす return this; } console.log(fn()); これをstrictモードにすると main.js function fn() { "use strict" return this; } // undefinedと出力 console.log(fn()); 例4 通常の場合、thisはオブジェクトへの参照を取ることになるが, Strictモードではプリミティブ型も取ることができる main.js function fn() { "use strict" return this; } // callでthisの値を変更 console.log(fn.call(2)); // 2と出力される // Strictモードを外すとNumber {2}とオブジェクトでラップされたプリミティブ型の値が帰ってくる // 文字列も同様 console.log(fn.call("2")); // 2と帰ってくる 2.Strictモードとクラス Classのコンストラクターやメソッド内は自動的にstrictモードがオンになっている 例1 main.js // 下記は両方ともundefinedと出力される // コンストラクターもメソッドも内部で宣言した関数内のthisが表示されない class C { constructor() { function fn() { console.log(this) } fn(); } method() { function fn() { console.log(this) } fn(); } } const c = new C(); c.method(); 今日はここまで! 参考にさせて頂いた記事 【JS】ガチで学びたい人のためのJavaScriptメカニズム Let'sプログラミング JavaScript入門
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS SDK for JavaScript v3 で S3 の操作を行う方法

はじめに 現在Node.jsでAWSのサービスは「AWS SDK」というパッケージで操作することが可能になっています。 大まかに「v2」「v3」に分かれていて、v3は操作するサービスごとに個別にパッケージをインストールすることができたりTypescriptのサポートを進めたことから公式もv3の使用を推奨しているようです。 しかし、現状情報はv2の情報が多いこと、v2とv3で違いが大きかったりして使用する際にハマったことが多かったこと、また公式ドキュメントも正直わかりづらく情報を得るのに苦戦したので、v3でS3を操作する方法をここに記したいと思います。 パッケージのインストール 前述したようにv3は操作するサービスごとに個別にパッケージをインストールできるので、S3を操作するパッケージをインストールします。 npm npm i -S @aws-sdk/client-s3 yarn yarn add @aws-sdk/client-s3 S3の操作方法 1・インスタンスの生成 import {S3Client} from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'ap-northeast-1', credentials: { accessKeyId: 'sample', secretAccessKey:'sample', }, }) 2・バケットの作成 import { S3Client, CreateBucketCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'ap-northeast-1', credentials: { accessKeyId: 'sample', secretAccessKey:'sample', }, }); //S3バケットを作成する s3.send( new CreateBucketCommand({ Bucket: '作成したいバケット名' }) ); 3・バケットの削除 import { S3Client, DeleteBucketCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'ap-northeast-1', credentials: { accessKeyId: 'sample', secretAccessKey:'sample', }, }); //S3バケットを削除する s3.send( new DeleteBucketCommand({ Bucket: '削除したいバケット名' }) ); 4・バケットにオブジェクトを追加 import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'ap-northeast-1', credentials: { accessKeyId: 'sample', secretAccessKey:'sample', }, }); s3.send( new PutObjectCommand({ Bucket: '保存したいバケット名', Key: 'キーを設定。取り出す際はこのキーで参照する', Body: '保存したいオブジェクト本体' }) ) 5・複数のバケット内オブジェクトを取得 import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'ap-northeast-1', credentials: { accessKeyId: 'sample', secretAccessKey:'sample', }, }); await s3.send( new ListObjectsV2Command({ Bucket: 'バケット名', MaxKeys: 10, //取得件数を指定。最大1000件まで }) ); 6・バケット内の特定のオブジェクトを取得 ここが私が一番苦戦したところでした。 「GetObjectCommand」で取得したオブジェクトは「Result.Body」に格納されているのですが、このオブジェクトは「ReadableStream」オブジェクトになっているためそのままでは取得できません。 「fs.createWriteStream」などで書き込み専用のストリームを定義し、.pipe()メソッドで少しづつデータを渡してあげる必要があります。 import fs from 'fs'; import { Readable } from 'stream'; import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'ap-northeast-1', credentials: { accessKeyId: 'sample', secretAccessKey:'sample', }, }); const result = await s3.send( new GetObjectCommand({ Bucket: 'バケット名', Key: '取得したいオブジェクトのキー' }) ); const readableObj = result.Body as Readable; const writableObj = fs.createWriteStream('ファイル名'); //readableObjをwritableObjに少しづつ書き込む readableObj.pipe(writebleObj); expressの場合は、レスポンスオブジェクトにS3から取得したオブジェクトを.pipe()メソッドで書き込むことが可能です。 import fs from 'fs'; import { Readable } from 'stream'; import { Request, Response, Router} from 'express'; import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; //画像を取得するhandler const getImage = async (req: Request, res: Response) => { const s3 = new S3Client({ region: 'ap-northeast-1', credentials: { accessKeyId: 'sample', secretAccessKey:'sample', }, }); const result = await s3.send( new GetObjectCommand({ Bucket: 'バケット名', Key: '取得したいオブジェクトのキー' }) ); const readableObj = result.Body as Readable; res.setHeader('Content-Type', 'image/png'); res.setHeader('Content-Length', result.ContentLength as number); //readableObjをresに少しづつ書き込む readableObj.pipe(res); } const router = Router(); router.get('/image/get', getImage); 7・バケット内のオブジェクトを削除する import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'ap-northeast-1', credentials: { accessKeyId: 'sample', secretAccessKey:'sample', }, }); await s3.send( new DeleteObjectCommand({ Bucket: 'バケット名', Key: '削除したいオブジェクトのキー' }) ); まとめ 今回はAWSSDKのV3でS3の操作をする方法を紹介しました。 今後も随時情報を更新していきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2つのiframeを同期スクロールさせる

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <iframe id="iframeTxt1" src="longlong.txt"></iframe> <iframe id="iframeTxt2" src="longlong.txt"></iframe> <script> var iframe1 = document.getElementById("iframeTxt1"); var iframe2 = document.getElementById("iframeTxt2"); function scroll1() { iframe2.contentWindow.scrollTo(0,iframe1.contentWindow.pageYOffset); } function scroll2() { iframe1.contentWindow.scrollTo(0,iframe2.contentWindow.pageYOffset); } document.addEventListener('DOMContentLoaded', function() { iframe1.addEventListener('load', (function(element) { return function() { element.contentWindow.addEventListener('scroll', scroll1); }; })(iframe1, false)); iframe2.addEventListener('load', (function(element) { return function() { element.contentWindow.addEventListener('scroll', scroll2); }; })(iframe2, false)); }, false); </script> </body> </html> 参考 https://qiita.com/sinsia/items/54865025821cb9203cfc
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebTransportでも4K配信がしたい! 〜序章 : まずはストリーム編〜

ストリームだと4K配信には、、、もう一工夫必要!? 今を生きている皆さんこんにちは。 そろそろアドベントカレンダーの季節ですが、そんなことは気にせずガンガン書いていきます! さて、WebRTCで出来なくてWebTransportに期待されていることの一つに4K配信などの高画質配信があるのではないでしょうか? WebRTCではブラウザの制限により1.5Mbps - 5Mbpsという制限があるため、フルHDでさえ"もやっと"する画質でした。 データグラムは次回試してみます WebTransport(が使っているQUIC)にはストリームとデータグラムがあります。 リアルタイム性を追求するならデータグラムを使うのが良いのですが、 ~正直面倒なのとマスタリングTCP/IP RTP編を半分しか読めていないのと~すでに去年QuicTransportでの実装記事を書かれている方がいらっしゃるので、ストリームでやってみました。 まあ、道のりは険しそうです。 動くには動いたが、、、(数秒程度でカックカク) とりあえずスクショです。左が配信用画面で右が視聴用画面です。(数秒遅れで数秒だけ再生されて、取得できないフレームがあるためデコーダがエラーを吐く) Chrome側でバッファリングしているような挙動になるので、タイムスタンプを比較してオンタイムにMediaStreamに流してやらないといけないのかもしれません。 改善点や正しく実装できているか怪しいところも多いので、今回はWebCodecsとの繋ぎ込みとして記事を書いておきます。 今回作ったもの 家に4Kのウェブカメラがなかったので、動画をVideoタグに埋め込み、配信ページと視聴ページに分けて作りました。 ソースコードをgithubにアップしておきました。 (簡単に図にするとただこれだけのことです) おさらい : WebTransportって何? WebTransportとはブラウザで低レベルな通信をするためのAPIです。 HTTP/3を利用し、それはQUICの上で動きます。 UDPのように高速で信頼性の低いデータグラムと、TCPとUDPのいいとこ取りをしたストリームがあり、今回はいいとこ取りのストリームを使います。 WebRTCがP2Pなのに対してこっちはサーバー・クライアントモデルです。 今まで書いた記事 プログラム解説 さて、まだまだ課題は多そうですがWebTransport + WebCodecsという一番ホットな組み合わせを作れたということでまとめていきます。 QuicTransportからの差分 QuicTransportでビデオチャットネタは2,3記事くらいあったので、そこから新しくなったところをまず書いておきます。 Videoの読み取りは、VideoTrackReader から MediaTrackProcessor になった Audioのエンコーダ・デコーダが実装されいてる 出力するところは MediaTrackGenerator を使う WebCodecsとはなんぞや? WebCodescとはブラウザにおいて動画・音声フレームをエンコードしたりデコードしたりするためのAPIです。 Chrome M94にてリリースされました。 WebCodecs APIにはエンコード・デコードのためのインターフェースと、データモデルが定義されています。 入出力のためのAPIはまた別のようです。 以下はWebCodecsとデバイスとデータの流れを示したものです。 ウェブカメラや動画ファイルからは一度デコードされてしまいます。 また動画フォーマットは今はvp8しか実装されていません。 今後のハードウェアサポートやUVC(ウェブカメラのストリーム)周りの改良が期待されるところです。 全体像 用途に応じてWebTransportのコネクションを使い分けます。 具体的にはHTTP/3のパスを用途ごとに分けて接続し、サーバー側で処理を分けます。 こうすることでコネクションの種類ごとに何のデータが通信されるか、処理を分けることができます。 データの取り扱いですが、動画は1フレーム、音声は1dataごとにWebTransportのストリームを一つ消費して送信します。 ストリームを使えばQUIC側でパケットの並び替えや再送などをやってくれるためです。(データグラムより楽) フロントエンド側 フロントエンド側はエンコード処理を行うためにWebWorkerを使います。 これにより重たいエンコード処理を別スレッドに分けてパフォーマンスを良くすることができます。(わけないとUIが反応しづらくなります) WebWorkerは一つのjsファイルを別スレッドで起動し、postMessage()でデータをやり取りします。 WebWorkerを起動するには次のようにします。 // workerを読み込む streamWorker = new Worker("./stream_worker.js"); // worker からのメッセージはログに出力する streamWorker.addEventListener('message', function(e) { addToEventLog(e.data); }, false); // エンコード・デコード・ストップなどのコマンドを送る streamWorker.postMessage({ type: "connect", url, /* ストリーム情報など */}, [/* 所有権を渡すパラメータ */]); streamWorker.postMessage({ type: "stop" }); WorkerでのWebTransportの処理 コネクションを張り、受信ストリームを設定し、ストリームごとの受信処理を書きます。 WebTransport 配信側概要 まずは配信側です。 接続する -> 1フレーム読み込む -> エンコードする -> 送信する という順番です。 非同期処理だらけですが、基本的にはエンコードされたデータの順番がおかしくなることはなさそうです。(保証されているかは不明) type: "connect"でコネクションを開始させ、エンコード処理と配信を開始します。 type: "stop"で配信を停止してコネクションをクローズします。 let wt_video = null; let wt_audio = null; self.addEventListener('message', async (e) => { const type = e.data.type; if (type === "connect") { stopped = false; const {media: {video, audio}, url} = e.data; wt_video = new WebTransport(url + '/video/stream'); wt_audio = new WebTransport(url + '/audio/stream'); await wt_video.ready; await wt_audio.ready; wt_video.closed.then(() => { self.postMessage('video Connection closed normally.'); }) .catch(() => { self.postMessage('video Connection closed abruptl.'); }); wt_audio.closed.then(() => { self.postMessage('audio Connection closed normally.'); }) .catch(() => { self.postMessage('audio Connection closed abruptl.'); }); // 送信のみなのでストリームの受け入れは不要 // エンコード処理と送信する処理 streamVideo(video); streamAudio(audio); return; } // コネクションを閉じる if (type === "stop") { stopped = true; wt_video.close(); wt_audio.close(); } }, false) // 動画をフレームごとに送信する。 async function streamVideo(video) { // エンコーダを準備する ... enoder = new VideoEncoder({ output: (chunk) => { // エンコードされた時のコールバック ... // 圧縮されたデータに必要なメタデータをつける // フレームを送信する sendBinaryData(wt_video, payload); ... }); // 動画データを読み込む while(true) { // stopしたら抜ける if (stopped) { ... return; ] // 1フレーム読み込む ... // エンコードする encoder.encode(...); // 上記の VideoEncoder()に渡した outputコールバックが呼ばれる ... } } // 音声も同様 ... // バイナリデータを送信する async function sendBinaryData(transport, data) { let stream = await transport.createUnidirectionalStream(); let writer = stream.getWriter(); await writer.write(data); await writer.close(); } WebTransport 視聴側概要 次に視聴側です。 ストリームで動画・音声データを受信するようにします。 接続する -> ストリームを待ち受ける -> ストリームから受信する -> デコードする -> VideoTrackに追加する という順番です。 後ほど詳細は述べますが、受信にタイムラグがあったり、0バイトしか読み出せないことがあったり、Chromeが大量のエラーをプロンプトのほうに吐き出したりと不安定です。 実装上の問題なのかこちらの使い方が悪いのかは追って検証していきます。 // コネクションなどは(このjs内の)グローバル変数に持たせておく let stopped = false; let wait_keyframe = true; let wt_video = null, frameWriter = null; let wt_audio = null, audioWriter = null; self.addEventListener('message', async (e) => { if (type === "connect") { // WebTransportを接続する処理は上記と同じ ... streamVideo(video); // 動画を受信してデコードする streamAudio(audio); // 音声を受信してデコードする return; } ... }) // ビデオを取得してデコードする async function streamVideo(video) { // デコーダの準備 let decoder = new VideoDecoder({ output: (frame) => { // デコードされた時のコールバック // フレームをvideoタグのVideoTrackに書き込む (videoタグに表示される) frameWriter.write(frame); } }) ... // ストリームを受け付ける acceptUnidirectionalStreams(wt_video, async (payload) => { // 受信したデータのコールバック // データからメタデータを取り出す ... // key_frameが来るまで待つ ... // デコードする if (!wait_keyframe) { decoder.decode(chunk); // デコーダーの output コールバックが呼ばれる } }) } WebTransport ストリーム受信処理 データ受信の処理です。 ログを見る限りでは50Bから10KBほどのデータとして取得できますが、(ChromeのQUIC層である程度結合される) 一度に全データを取得できるわけではないので自分でデータを全て結合します。(その割に数秒程度バッファリングされてまとめて処理が走るのが謎) ストリームを受け付ける処理はリファレンスとほぼ同じです。 echoサンプルではリアルタイムにストリーム入力を受け付けてくれますが、 今回の動画配信だと、サーバーからデータを受け取っても1から10秒くらいは処理を開始してくれません。 // ストリームを受け付ける async function acceptUnidirectionalStreams(transport, onstream) { let reader = transport.incomingUnidirectionalStreams.getReader(); try { while (true) { const { value, done } = await reader.read(); if (done) { self.postMessage('Done accepting unidirectional streams!'); return; } let stream = value; readFromIncomingStream(stream, onstream); } } catch (e) { self.postMessage('Error while accepting streams: ' + e); } } データを読み込む処理です。 ここでも1/30の確率で0バイトのデータ読み取りになってしまいます。 逆に受信できたデータは、サイズを照合する限りでは送信側やサーバーのサイズと一致しています。 なので上記の問題と合わせてChromeの実装上の問題なのかもしれません。(アクティブでないタブの受信処理は後回しにされる??) // データを読み込む async function readFromIncomingStream(stream, onstream) { let reader = stream.getReader(); let payload = new Uint8Array(); while (true) { const { value, done } = await reader.read(); if (done) { // ここではvalueはundefinedになる // 1/30くらいの確率で0バイトのデータになることがある? if (payload.byteLength == 0) { console.log("invalud payload"); return; } // 細かい処理はコールバックでやる onstream(payload.buffer); return; } // データを結合する buffer = new Uint8Array(payload.byteLength + value.byteLength); buffer.set(payload, 0); buffer.set(new Uint8Array(value), payload.byteLength); payload = buffer; } } エンコードされたデータ構造について WebTransportで転送するのはEncodedVideoChunkとEncodedAudioChunkというデータになります。 動画なら1フレーム、音声なら1ブロックずつエンコードされてきます。 そのため、これらのデータをリアルタイムに送受信できれば良さそうです。 ちなみに、動画にはキーフレームかデルタ(差分)というのもがあり、データ容量を抑えるためにキーフレームからの差分をデータとして扱います。 最近ではあまりみなくなりましたが、破損した動画データを再生すると、動きのある部分がブロックノイズのようになるのをみたことがあると思います。 それはこのキーフレームと差分データによるものです。(キーフレームは一定間隔で送られてくるので、それを受信すると直ります。データ量は10倍くらい差があります) 付随するデータとして (動画のみ)type (key or delta)、timestampとdurationが必要になります。 そのため、これらのデータを一緒に送信する必要があります。 今回は単純にフレームデータの先頭にくっつけて送ることにします。 key size type 1byte timestamp 8byte duration 8byte data data.byteLength 配信側 async function streamVideo(video) { let encoder = new VideoEncoder({ output: (chunk) => { // データがエンコードされた時 ... // header(17) = type(1byte) + timestamp(8) + duration(8) let payload = new ArrayBuffer(17 + chunk.byteLength); const view = new DataView(payload); view.setUint8(0, (chunk.type === "key" ? 1 : 2)); view.setBigInt64(1, BigInt(chunk.timestamp)); // 仕様では long long だが実際はNumber view.setBigUint64(9, BigInt(chunk.duration)); // 仕様では unsigned long long だが実際はNumber chunk.copyTo(new DataView(payload, 17)); // フレームを送信する sendBinaryData(wt_video, payload); }); ... } 受信する側では逆に必要な情報を取り出します。 受信側 // ビデオを取得してデコードする async function streamVideo(video) { // デコーダーの準備 ... // 受信した処理のコールバック acceptUnidirectionalStreams(wt_video, async (payload) => { // payloadからデータを復元する // header(17) = type(1byte) + timestamp(8) + duration(8) let view = new DataView(payload, 0); const type = view.getUint8(0); const chunk = new EncodedVideoChunk({ type: (type === 1 ? 'key' : 'delta'), timestamp: Number(view.getBigInt64(1)), // 仕様では long long だが実際はNumber duration: Number(view.getBigInt64(9)), // 仕様では unsigned long long だが実際はNumber data: new DataView(payload, 17), }); ... // デコーダにデータを渡す ... }); ... } エンコード処理 さて、エンコード処理です。 動画・音声データの読み取りとエンコード処理に分かれます。 ウェブカメラを使う場合であっても getUserMedia()からMediaStreamを取得するのでほぼ同じです。 動画・音声フレーム取得 MediaStreamからVideoTrackもしくはAudioTrackを取得し、MediaStreamTrackProcessorでデータを読み取るためのオブジェクトを生成します。 stream.js // 動画を再生したらストリームを開始する。停止したらストリーム配信も止める video.onplay = () => { const [videoTrack] = document.getElementById('video').captureStream().getVideoTracks(); const [audioTrack] = document.getElementById('video').captureStream().getAudioTracks(); const videoProcessor = new MediaStreamTrackProcessor(videoTrack); const audioProcessor = new MediaStreamTrackProcessor(audioTrack); const frameStream = videoProcessor.readable; const audioStream = audioProcessor.readable; ... // workerに処理を投げる streamWorker.postMessage({ type: "connect", url, {video: {stream: frameStrea}, audio: {stream: audioStream}}}, [frameStream, audioStream]); }; 動画エンコード この読み取り用オブジェクトからデータを読み取り、エンコーダに渡していきます。 const frameReader = video.stream.getReader(); ... let encoder = new VideoEncoder({ output: (chunk) => { // 1フレーム送信する (分割・結合はQUICにお任せする) ... // データにtypeやtimestamp, durationをつける // フレームを送信する sendBinaryData(wt_video, payload); }, error: (e) => { // エラー処理 } }); encoder.configure({ codec: 'vp8', // これしか使えない width: video.width, height: video.height, framerate: 1, // 指定できない latencyMode: "realtime", // 特に変わらない }); // データを読み取る while(true) { // 動画を停止したら処理をやめる if (stopped) { frameReader.close(); encoder.close(); self.postMessage("frame stream stopped."); break; } // WebTransportのストリームの受信と似たような感じ const {value, done} = await frameReader.read(); if (done) { self.postMessage("frame stream ended."); break; } var frame = value; // 30フレームに一度キーフレームにする encoder.encode(frame, {keyFrame: (frameCount % 30 == 0 ? true : false)}); frame.close(); } 音声エンコード 音声データの場合もほぼ同様です。 async function streamAudio(audio) { const frameReader = audio.stream.getReader(); self.postMessage('Start audio frame encode.'); let encoder = new AudioEncoder({ output: (chunk) => { ... // timestamp, durationをバイナリデータに含める // フレームを送信する sendBinaryData(wt_audio, payload); }, error: (e) => { self.postMessage("encoding error. " + e.message) } }); encoder.configure({ codec: 'opus', numberOfChannels: 2, sampleRate: 48000, // AudioContextからサンプルレートを取得したいがここでは使えない }); // データを取得する while(true) { // 動画を停止した場合は止める if (stopped) { frameReader.close(); encoder.close(); self.postMessage("frame stream stopped."); break; } // 読み取る const {value, done} = await frameReader.read(); if (done) { self.postMessage("frame stream ended."); break; } var frame = value; encoder.encode(frame); // エンコードする frame.close(); } wt_audio.close(); } デコードする デコードの場合はデータ受信 -> デコード ->データ書き込み となるのでやや処理の流れが分かりにくいです。 まずは接続する際にMediaTrackGeneratorでトラックを作成し、これをvideoタグに紐付けます。 動画・音声ストリーム作成 viewer.js // WebTransportで接続する ... // デコードされた動画を受け取るためのストリームを作る const videoTrack = new MediaStreamTrackGenerator({ kind: 'video' }); const audioTrack = new MediaStreamTrackGenerator({ kind: 'audio' }); const frameStream = videoTrack.writable; const audioStream = audioTrack.writable; const media = { video: { stream: frameStream, }, audio: { stream: audioStream, }, }; // workerに受信とデコード処理を投げる viewerWorker.postMessage({type: "connect", url, media}, [frameStream, audioStream]); // ストリームをビデオタグに設定する const stream = new MediaStream(); stream.addTrack(videoTrack); stream.addTrack(audioTrack); document.getElementById('video').srcObject = stream; 動画デコード 受信したデータをデコーダに渡し、frameWriterに書き込みます。 // デコーダーの準備 frameWriter = video.stream.getWriter(); let decoder = new VideoDecoder({ output: (frame) => { frameWriter.write(frame); // ここで書き込む }, error: (e) => { console.log(e); } }); decoder.configure({ codec: 'vp8', // これしか使えない optimizeForLatency: true, // 有効かどうか不明 }); // ストリームを受信した時のコールバック acceptUnidirectionalStreams(wt_video, async (payload) => { ... decoder.decode(chunk); // デコードする }) 音声デコード 音声もほぼ同じです。 async function streamAudio(audio) { // デコーダーの準備 audioWriter = audio.stream.getWriter(); let frameCount = 0; let decodedFrameCount = 0; let decoder = new AudioDecoder({ output: (frame) => { audioWriter.write(frame); decodedFrameCount++ }, error: (e) => { console.log(e); self.postMessage(e) } }); decoder.configure({ codec: 'opus', numberOfChannels: 2, sampleRate: 48000, // audioCtx.sampleRate, }); // ストリームを受信した時のコールバック acceptUnidirectionalStreams(wt_audio, async (payload) => { ... // payloadからchunkを取り出す decoder.decode(chunk); // デコードする }); } なお、WebCodecsは8000ピクセルまでのwidthに対応しているようなので、8kも視野に入れているものかと思います。 WebTransportを使わずに別のvideoタグに書き出してみたところ、フルHDの動画ならば問題なくエンコード・デコードできました。 サーバー側 WebTransportが接続されるたびに class WebTransportProtocol(QuicConnectionProtocol): インスタンスが作成されます。 ハンドラの追加 HTTP/3のハンドラに動画・音声のハンドラを追加し、データ受信と配信のためのクラスを追加します。 class WebTransportProtocol(QuicConnectionProtocol): def __init__(self, *args, **kwargs) -> None: ... self._handler = None # pathに応じて ChatHandler, VideoReceiver, VideoSubscriber を使い分ける def _handshake_webtransport(self, if path == b"/chat": assert(self._handler is None) self._handler = ChatHandler(stream_id, self._http) self._send_response(stream_id, 200) elif path == b"/audio/stream": # 音声配信するときにアクセスするパス(サーバーから見れば、音声を受信する) assert(self._handler is None) self._handler = AudioReceiver(stream_id, self._http) self._send_response(stream_id, 200) elif path == b"/audio/view":# 音声受信するときにアクセスするパス(サーバーから見れば、音声を配信する) assert(self._handler is None) self._handler = AudioSubscriber(stream_id, self._http) self._send_response(stream_id, 200) elif path == b"/video/stream": # 動画配信するときにアクセスするパス assert(self._handler is None) self._handler = VideoReceiver(stream_id, self._http) self._send_response(stream_id, 200) elif path == b"/video/view": # 動画視聴するときにアクセスするパス assert(self._handler is None) self._handler = VideoSubscriber(stream_id, self._http) self._send_response(stream_id, 200) 動画・音声を受け取る処理 配信者が動画と音声を配信するための処理を書きます。 サーバーから見ればデータ受信処理になります。 受け取ったデータをそのまま流すだけなので特に難しいことはありません。 # 動画 class VideoReceiver: def __init__(self, session_id, http: H3ConnectionWithDatagram) -> None: self._session_id = session_id self._http = http self._buf = defaultdict(bytes) def h3_event_received(self, event: H3Event) -> None: if isinstance(event, WebTransportStreamDataReceived): # 1フレームを1ストリームで送る self._buf[event.stream_id] += event.data if event.stream_ended: broadcast_video(self._buf[event.stream_id]) self.stream_closed(event.stream_id) def stream_closed(self, stream_id) -> None: try: del self._buf[stream_id] except KeyError: pass def session_closed(self) -> None: # ビデオ送信がストップされた # 特に何もする必要はない return # 音声 class AudioReceiver: def __init__(self, session_id, http: H3ConnectionWithDatagram) -> None: self._session_id = session_id self._http = http self._buf = defaultdict(bytes) def h3_event_received(self, event: H3Event) -> None: if isinstance(event, WebTransportStreamDataReceived): # 1フレームを1ストリームで送る self._buf[event.stream_id] += event.data if event.stream_ended: broadcast_audio(self._buf[event.stream_id]) self.stream_closed(event.stream_id) def stream_closed(self, stream_id) -> None: try: del self._buf[stream_id] except KeyError: pass def session_closed(self) -> None: # 音声送信がストップされた # 特に何もする必要はない return 動画・音声を配信する処理 サーバーにコネクションリストを持ち、視聴用のパスに接続されたらリストに追加します。 あとは上記の動画・音声データを受信し終わったときに全員に配信します。 # 動画 # video_member = {"connection_id": {'connection': h3_connection, 'session_id': session_id}} viewers = defaultdict(Any) def broadcast_video(payload): print("send video " + str(len(payload))) for viewer in viewers.values(): stream_id = viewer['connection'].create_webtransport_stream( viewer['session_id'], is_unidirectional=True) viewer['connection']._quic.send_stream_data( stream_id, payload, end_stream=True) class VideoSubscriber: def __init__(self, session_id, http: H3ConnectionWithDatagram) -> None: # viewersに登録する print("add viewer.") self._connection_id = http._quic.host_cid viewers[self._connection_id] = {"connection": http, "session_id": session_id} def h3_event_received(self, event: H3Event) -> None: # 特に何も受け取らない return def session_closed(self) -> None: # viewersから削除する try: del viewers[self._connection_id] except KeyError: pass # 音声 # listeners = {"connection_id": {'connection': h3_connection, 'session_id': session_id}} listeners = defaultdict(Any) def broadcast_audio(payload): print("send audio " + str(len(payload))) for viewer in listeners.values(): stream_id = viewer['connection'].create_webtransport_stream( viewer['session_id'], is_unidirectional=True) viewer['connection']._quic.send_stream_data( stream_id, payload, end_stream=True) class AudioSubscriber: def __init__(self, session_id, http: H3ConnectionWithDatagram) -> None: # listenersに登録する print("add viewer.") self._connection_id = http._quic.host_cid listeners[self._connection_id] = {"connection": http, "session_id": session_id} def h3_event_received(self, event: H3Event) -> None: # 特に何も受け取らない return def session_closed(self) -> None: # listenersから削除する try: del listeners[self._connection_id] except KeyError: pass なお、PythonのサーバーはCPU負荷が高いものの、データが破損することもなくほぼ遅延なく処理を行なっているようです。 スムーズに動画が配信できない。何が原因か さて、ここまでの処理でエンコード・デコード・配信・受信などの処理ができました。 ただ、冒頭に書いた通り期待通りに動画が再生されることはなく、特にデータ受信が不安定でした。 確認できたこと 負荷が高くて処理が追いつかない $K動画をリアルタイムにエンコードするにはCPUが少し足りない PythonサーバーのCPU負荷が高すぎる (100KBのダミーデータを30フレーム送ると90%程度) 実装が枯れていない ストリームを使っているのにChromeがストリームを読み取るときに空のデータを返すことがある Chromeのストリームの受信バッファが溢れる そもそも考えることが多い 受信側で動画・音声のバッファリングしないといけない? データグラムの場合、RTP的なパケロス対策や誤り訂正などが必要 ストリームでもデータ結合やシリアライズが必要 非同期処理周りで何か間違っているかも どれくらい重たいか どこの処理が重たいのか、処理を切り出して比較してみました。 4K動画 使用したデータは焚き火を使ってみました。 key value サイズ 3840 x 2180 fps 29.97 bps 20.80Mbps フォーマット H264 そもそも4K動画を再生すること自体無理があるのでしょうか? 少しずつ処理を足して確認してみます。 4K動画をvideoタグで再生するだけ 流石にハードウェア支援が効くので全く負荷はありませんでした。 Chrome Helper (GPU) が7.8%使うくらいでした。 フレームを読み取るだけ デコードされたフレームを読み取る処理はどうでしょうか? エンコード・送信処理をコメントアウトして試してみます。 タスク CPU使用率 Chrome Helper(Renderer) 8.3% Chrome Helper(GPU) 7.8% ほぼ問題ないレベルです。 エンコードするだけ(送信はしない) VP8はCPUエンコードなので、かなり重たいもののギリギリ30フレーム遅延くらいで処理が間に合っていました。 下のスクリーンショットは4K動画をエンコードだけ行い、送信しなかった場合の30フレームごとのサイズと遅延フレーム数です。 フレームサイズは約60KB、30フレームなので、60KB x 30 = 14.4Mbpsくらいのデータ量です。 遅延フレーム数はおおよそ16 - 20くらいで安定しました。 Read 150 frames. last frame = 1 Read 150 frames. last frame = 22 Read 150 frames. last frame = 16 Read 150 frames. last frame = 16 Read 150 frames. last frame = 16 Read 150 frames. last frame = 21 タスクマネージャーを見ると案の定、CPU負荷が高くなっていました。 これにPythonサーバーへの通信処理を加えると、エンコード処理が追いつかずChromeが大量のエラーを出力してフリーズしました。 (last frame = 66, 76 が処理できずに溜まっているフレーム数) フルHDではどうか? 次に動画をフルHDにしてみました。 遅延フレーム数は1となり、エンコードと送信のCPU負荷は問題ないようです。 ただし、配信開始してから数秒から10秒程度反応せず、そのあと早送りで再生されるような挙動でした。 また、4K配信の時もそうですが1分ほど配信しているとitermの方に下記のエラーが延々と出力されて固まることがありました。 [85146:13571:1127/215410.468886:ERROR:quic_stream.cc(781)] Fin already buffered Chromeのソースコードを見るとこの辺なので、バッファ周りの処理なのかもしれません。 それではVGAでは? もっと軽いVGA動画ではどうでしょうか? 試してみたところ、負荷は全く問題ありませんでしたが10秒間隔でバッファリングされたような挙動になり、 期待していたようなスムーズな受信処理ができませんでした。 Chrome側の実装の問題でスケジューリング周りの処理がまだ不安定なだけなら今後の実装である程度スムーズな配信ができるかもしれません。 もし、ブラウザ側である程度バッファリングしてからまとめて処理が走るようであれば、独自にバッファリングしてタイムスタンプを見ながらMediaStreamに流し込む処理を実装する必要がありそうです。 今はローカルでの処理ですし、WebSocketでさえ10秒バッファリングされるようなこともないので、早いところ実装がこなれてくると良いなぁと思います。 WebWorkerだから? とか非同期処理周りなど色々変えてみたりしましたが、 そもそも前回のテキストチャットの時点でリアルタイムではなく数秒のラグがありました。 まとめ まだデータグラムを試していないので次はそれでやってみます。 ともあれ、エンコーダーが4Kに対応しているのと24Mbpsくらいのダミーデータであれば問題なく送信はできているので 十分に4K配信できるだけのポテンシャルはありそうです。 しかも! MozillaがRustで実装しているQUICサーバーのNeqoがWebTransport関連のプルリクをマージしたようです! もし使えるようであれば、いよいよWebTransportサーバーを本格的に実装していけるかもしれません! WebTransportは元々がクラウドゲーミングの需要から実装が始まっていますが、 自分のパソコンが完全にクラウドになってしまう日も近いのかもしれませんね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next2D入門

Next2Dプロジェクトとは JavaScript製FlashPlayer「swf2js」から派生したプロジェクトです。 swf2jsはエミュレーターとして完成し、そこで得た技術を次代に活用するべく、Next2Dプロジェクトを開始しました。 Next2Dには、大きく3つの機能があります。 Next2D Player WebGL対応の2D描画ライブラリで、ブラウザやデバイスの互換性を気にすることなく、リッチでインタラクティブなグラフィックス、クロスプラットフォームなアプリケーションやゲームを作成することができるライブラリ Next2D Playerの特徴 Next2D NoCode Tool NoCode開発を主体としたWebブラウザ上で動作するオーサリングツールです。 イメージしたアニメーションを直感的に作成することができ、書き出したデータはWebにアップロードすることで簡単に公開することができます。 出力したJSONは、Next2D Playerで読み込むことが可能です。 NoCode Toolの特徴 Next2D Framework MVVMアーキテクチャを採用。個人でもチームでも、規模を問わず、最新の開発手法で効率的な開発をサポートします。 Next2D Frameworkの特徴 主にこの3つの機能を有しています。 GitHubのリポジトリは以下となります。 - Player - NoCode Tool - Framework 昨年のアドベントカレンダーで、少しだけ「Next2D」の紹介をさせていただきました。 Flash Advent Calendar 25日目 - ありがとうFlashPlayer - 今日の記事は上記の記事の続きとなります。 FlashPlayerのサポート終了 昨年、FlashPlayerのサポートが終了しました。 その結果、世の中には沢山のSWF資産が冬眠することになったかと思います。 そんな資産を再利用できる(?)NoCode Toolの紹介を今日は行いたいと思います。 Next2D NoCode Toolとは 一言で紹介すると、Webブラウザ上で動作するオーサリングツールです。 インストール不要、会員登録も不要、アクセスすれば誰でも利用できるサービスです。 サイトはこちら:NoCode Tool 基本のワークスペースはこのような名称になっています。 Tool Area 描画ツール、テキストツールや言語設定や書き出し設定など各種ツールと設定項目が設置されているエリアになります。 Screen Area 描画エリア、表示するDisplayObjectの配置や重ね順などを操作するエリア Timeline Area アニメーションのレイヤーやキーフレームなどを操作するエリア Controller Area 拡大縮小/回転/カラー操作/BlendMode/Filterなど、指定のDisplayObjectを操作するエリア SWFを読み込む 本日は、昨年の続きという事でSWFファイルをどのように読み込むかを動画で紹介できればと思います。 動作動画 SWF読み込み手順 Controller AreaのLibraryを右クリックしてSWFを読み込む(直接ドロップも可能です) MovieClipをScreen Areaにドロップ Controller Areaで好きなサイズに変更 Screen Areaで右クリックして動作プレビュー SWFファイルはどのバージョンでも読み込み可能で、これまでの資産を再利用できます。 また、これまで通りAnimateCC(Flash)、Illustrator、AfterEffectsなどAdobe製品からSWFを書き出し、NoCode Toolで読み込む事でNext2D Playerで再生が可能です。 つまり、過去の資産の再利用もこれまで通りの運用スタイルも可能にすることができます。 SWFファイルの対応範囲 Shape Bitmap MovieClip Matrix ColorTransform Blend Mode Filter 非対応機能 ActionScript Text なぜ非対応機能があるのか swf2jsでエミュレートできている機能をなぜNoCode Toolで対応しないのか それは、このカレンダーを通して説明させて頂ければと思います。 本日は最後まで記事をご覧いただきありがとうございました。 NoCode ToolにはSWFを読み込む以外にも色々な機能があります。 そちらは明日以降でご紹介させて頂ければと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む