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

ISO 8601(日付時刻表記法)の基本形式から拡張形式へ変換

こんにちは。 ISO 8601(日付時刻表記法)の基本形式から拡張形式へ変換しました。ただし処理ロジックが甘いです。 基本形式: 20211225T000000.000+0900 拡張形式: 2021-12-25T00:00:00.000+09:00 const getDateTimeExtended = str => { const len = str.length; const Y = str.slice(0, 4) + '-'; const M = str.slice(4, 6) + '-'; const Dh = str.slice(6, 11) + ':'; const m = str.slice(11, 13) + ':'; const st = str.slice(13, len-2) + ':'; const u = str.slice(len-2, len); const str_extended = Y + M + Dh + m + st + u; return str_extended; }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScriptチートシート4(応用)

インターセクション型 ・合併型(uniontypes):AかBどちらかの型を持つ ・交差型(intersection):AとB両方の型を持つ(共通する型ではない) type Engineer = { name: string; role: string; }; type Blogger = { name: string; follower: number; }; type EandB = Engineer & Blogger; const suda: EandB = { name: "masaki", role: "front-end", follower: 10000, }; Union 型の絞り込み interface Hoge { foo: string; bar: number; } interface Piyo { foo: number; baz: boolean; } type HogePiyo = Hoge | Piyo; //オブジェクトにbarとbaz両方あるとどちらの型になるのかわからないので、参照できない const obj: HogePiyo = { foo: 'hello', bar: 0, baz:false }; //プロパティ 'bar' は型 'HogePiyo' に存在しません。 //プロパティ 'bar' は型 'Piyo' に存在しません。 const aaa = obj.bar in 'bar' in objというのはbarというプロパティがobjに存在するならtrueを返し、そうでないならfalseを返す式 型を絞り込めば、正しく使える function useHogePiyo(obj: Hoge | Piyo): void { // ここではobjはHoge | Piyo型 if ('bar' in obj) { // barプロパティがあるのはHoge型なのでここではobjはHoge型 console.log('Hoge', obj.bar); } else { // barプロパティがないのでここではobjはPiyo型 console.log('Piyo', obj.baz); } } Type guard typeof演算子は与えられた値の型を文字列で返す演算子 function func(value: string | number): number { if ('string' === typeof value) { // valueはstring型なのでlengthプロパティを見ることができる return value.length; } else { // valueはnumber型 return value; } } instanceof class Dog { speak() { console.log("bow-bow"); } } class Bird { speak() { console.log("tweet-tweet"); } fly() { console.log("flutter"); } } type Pet = Dog | Bird; function havePet(pet: Pet) { pet.speak(); if (pet instanceof Bird) { pet.fly(); } } havePet(new Bird()); //コンソール tweet-tweet flutter havePet(new Dog()); //コンソール bow-bow タグ付きユニオン オブジェクトに対してユニオン型を使うときに役立つ //type: 'Some' //type: 'None' //というタグをつけておく //このタグを使って、型を分ける interface Some<T> { type: 'Some'; value: T; } interface None { type: 'None'; } type Option<T> = Some<T> | None; //ifまたはswitchで場合分け function map<T, U>(obj: Option<T>, f: (obj: T)=> U): Option<U> { if (obj.type === 'Some') { // ここではobjはSome<T>型 return { type: 'Some', value: f(obj.value), }; } else { return { type: 'None', }; } } //上と下は同じ function map<T, U>(obj: Option<T>, f: (obj: T)=> U): Option<U> { switch (obj.type) { case 'Some': return { type: 'Some', value: f(obj.value), }; case 'None': return { type: 'None', }; } } ユニオン型とインターセクション型を合わせて使ってみる interface Hoge { type: 'hoge'; foo: string; } interface Piyo { type: 'piyo'; bar: number; } interface Fuga { baz: boolean; } type Obj = (Hoge | Piyo) & Fuga; function func(obj: Obj) { // objはFugaなのでbazを参照可能 console.log(obj.baz); if (obj.type === 'hoge') { // ここではobjは Hoge & Fuga console.log(obj.foo); } else { // ここではobjはPiyo & Fuga console.log(obj.bar); } } 型アサーション 手動で型を上書きする as //今の状態の型 //const input: HTMLElement | null const input = document.getElementById('input') //input.valueなど使えない const input = <HTMLInputElement>document.getElementById('input') //または const input = document.getElementById('input') as HTMLInputElement input.value = 'value' ! nullじゃないと言い切る方法 if文で条件分岐しなくてもよくなる //今の状態の型 //const input: HTMLElement const input = document.getElementById('input')! インデックスシグニチャ ・オブジェクトが複数のプロパティを持つ可能性を示す ・[key:T]:U のように示す ・keyはstringかnumberのみ const capitals: { [countryName:string]:string } = { japan: 'tokyo', korea: 'soul', //エラー age:23 } 関数のオーバーロード 戻り値の型を正しく伝える function toUpperCase(x: string | number): string | number { if (typeof x === "string") { return x.toUpperCase(); } return x; } const upperHello = toUpperCase("hello") //upperHelloの型は、const upperHello: string | number //--------------------------------------------------- function toUpperCase(x: string ): string function toUpperCase(x: number): number function toUpperCase(x: string | number): string | number { if (typeof x === "string") { return x.toUpperCase(); } return x; } //オーバーロードすることによって関数の戻り値の型を正しくできる const upperHello = toUpperCase("hello") Optional Chaining プロパティが連結していくとき指定したプロパティがなければnullを返す //?はあってもなくてもいいプロパティ interface DownLoadedData { id: number; user?: { name?: { first: string; last: string } } } const downloadedData: DownLoadedData = { id:1 } //エラー //オブジェクトは 'undefined' である可能性があります console.log(downloadedData.user.name) //解決策の一つ //userがあったらnameを返す //なければundefinedを返す console.log(downloadedData.user?.name) Nullish Coalescing userがundefined or nullの場合??の後ろを返す const userData = downloadedData.user ?? 'no-user' LookUp型 オブジェクトの型のメンバが持っている型にアクセスする interface DownLoadedData { id: number; user: { name?: { first: string; last: string; }; }; } type id_1 = DownLoadedData['id'] //number type id_2 = DownLoadedData['user']['name'] //type id_2 = { // first: string; // last: string; //} | undefined 具体例 第一引数Tは{ foo: string; bar: number; }型 第二引数K extends keyof Tはfooまたはbarに限定する 戻り値T[K]は({ foo: string; bar: number; })['foo']型つまりstring function pick<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const obj = { foo: 'string', bar: 123, }; const str: string = pick(obj, 'foo'); const num: number = pick(obj, 'bar'); // エラー: Argument of type '"baz"' is not assignable to parameter of type '"foo" | "bar"'. pick(obj, 'baz'); constアサーション as const をつけると変えられなくなる let milk = 'milk' as const //let milk: "milk" const array = [10,20] //const array: number[] const array = [10,20] as const //const array = [10,20] as const
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ZOOM】コロナ禍のZOOM生活をちょっとだけ快適にしたい

毎日のオンライン会議・授業でちょっとだけ楽できるアプリをつくりました こんにちは.kakekakemiyaと申します. 現在は東京大学工学部電子情報工学科というところに所属しております. 今回は,私の作ったアプリの紹介をさせていただきたく投稿させていただきました. なお,本記事では「オンライン会議ツール」と書けばいいところを「ZOOM」で統一させていただきますが,国内シェアの高さと,自分が普段使っているというだけの理由なので,ZOOMアプリへの宣戦布告でもなければ他競合製品では使えないアプリであるというわけでもありません. ZOOMへの不満 ZOOMは今やコロナ禍において欠かせないアプリの一つとなりました. 特に,オンライン授業を行なっている大学では,学生は毎日各授業のURLに接続してリモート受講をしているので大学生活に欠かせない存在となっています. 私自身もコロナ直撃大学生なので,2年生のころから毎日オンライン授業を受けているわけですが,兼ねてから生じているZOOMへの不満が, URL管理が面倒くさいーーー ということです. メモ帳やslack等に自分の受けている授業のURLを保存しておいて,時間になったら今日は何曜日で今何時だからこのURLか!といちいち選ぶのが面倒くさがり屋の私にはなかなか鬱陶しいものでした. この不満に共感してくださる方がどれくらいいるのかわかりませんが,世界のどこかにはいるだろうということで,今回はこのイライラを解消するアプリをつくりました. その名も,AUTOZOOM3!! (↑アプリアイコンです) AUTOZOOM3 AUTOZOOM3の機能は非常にシンプルで,「事前に登録しておいたURLの中から,今の曜日の今の時間に入らなければならないミーティングに自動で入れてくれる.」というアプリになっています. 使い方 URLの登録 メニューからADD URLを選び,曜日,時刻,登録名,ZOOMのURLを入力してRegisterを押せばOKです!! このとき,時間についてはミーティングの開始時刻ではなく,自分が参加したい時刻にしてください 授業開始時刻にすると冒頭遅刻します 例えば,ミーティング A が 10:00,ミーティング B が 11:00 で登録されていた場合,10:00-10:59 は A に,11:00-は B に入ります. なお,登録済みのURLを消したい場合は一覧のdeleteを押すだけで消えます. ZOOMへの参加 ZOOMに参加したい時は,アプリを起動してJoin the Meetingをクリックするだけで,その曜日その時間に入るべきZOOMに入れてくれます. (gifがなんかもっさりしていますが,実際はもっとサクサク動きます) お気づきの方もいらっしゃるかとは思いますがAUTOZOOMによるJoinはURLを開くだけなので,ZOOM以外にもリンク踏む系のものなら使用できます!! 実装と仕様 HTML+CSS+JavsScriptで作ったものをElectronでアプリ化しています. 実装方針は非常に簡単で,現在の曜日と時刻を取得してきて,保存されたURLの中から入るべきURLに飛ばすという形になっています. その他ポイントになりうる実装・仕様としては, * URLは,Electron内で動いているChromium上のLocalStorageに保存している * Join the Meetingは該当URLを直に貼っているので,実は押した時ではなく起動時に入るべきURLを選択する(これは僕自身がアプリを常駐させない派なので問題を感じなかったのですが,常駐させる派からクリック時にしてほしいとの意見があったのでアップデートかけるかもです) * 既に登録済みの曜日時刻に登録すると上書きしてしまう あたりだと思われます. https://github.com/Kakeru-Miyazaki/autozoom3/ にソースコードは置いてあります.今後適宜アップデートしていきます. なお,このアプリ自体「こんなのあったら便利じゃない?笑」くらいのノリで AUTOZOOMコマンド として実装して自分で使っていたもの(初代AUTOZOOM)を友達に配布するためにGUIをつくって(AUTOZOOM2),それをさらに良い感じにしてできたのが AUTOZOOM3 なので構想が雑というか作り込みが甘いのはご了承ください アプリの配布 https://github.com/Kakeru-Miyazaki/autozoom3/releases/tag/ver2.0.0 にて,本アプリを配布しています! dmg,exe,debを用意しているので,Mac,Windows,Ubuntu等でお使いいただけます. 拙いアプリですが,気になった方はお試しいただけると嬉しいです! 最後まで読んでいただきありがとうございました. kakekakemiya
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ZOOM】毎日のZOOM生活をちょっとだけ快適にしたい

毎日のオンライン会議・授業でちょっとだけ楽できるアプリをつくりました こんにちは.kakekakemiyaと申します. 現在は東京大学工学部電子情報工学科というところに所属しております. 今回は,私の作ったアプリの紹介をさせていただきたく投稿させていただきました. なお,本記事では「オンライン会議ツール」と書けばいいところを「ZOOM」で統一させていただきますが,国内シェアの高さと,自分が普段使っているというだけの理由なので,ZOOMアプリへの宣戦布告でもなければ他競合製品では使えないアプリであるというわけでもありません. ZOOMへの不満 ZOOMは今やコロナ禍において欠かせないアプリの一つとなりました. 特に,オンライン授業を行なっている大学では,学生は毎日各授業のURLに接続してリモート受講をしているので大学生活に欠かせない存在となっています. 私自身もコロナ直撃大学生なので,2年生のころから毎日オンライン授業を受けているわけですが,兼ねてから生じているZOOMへの不満が, URL管理が面倒くさいーーー ということです. メモ帳やslack等に自分の受けている授業のURLを保存しておいて,時間になったら今日は何曜日で今何時だからこのURLか!といちいち選ぶのが面倒くさがり屋の私にはなかなか鬱陶しいものでした. この不満に共感してくださる方がどれくらいいるのかわかりませんが,世界のどこかにはいるだろうということで,今回はこのイライラを解消するアプリをつくりました. その名も,AUTOZOOM3!! (↑アプリアイコンです) AUTOZOOM3 AUTOZOOM3の機能は非常にシンプルで,「事前に登録しておいたURLの中から,今の曜日の今の時間に入らなければならないミーティングに自動で入れてくれる.」というアプリになっています. 使い方 URLの登録 メニューからADD URLを選び,曜日,時刻,登録名,ZOOMのURLを入力してRegisterを押せばOKです!! このとき,時間についてはミーティングの開始時刻ではなく,自分が参加したい時刻にしてください 授業開始時刻にすると冒頭遅刻します 例えば,ミーティング A が 10:00,ミーティング B が 11:00 で登録されていた場合,10:00-10:59 は A に,11:00-は B に入ります. なお,登録済みのURLを消したい場合は一覧のdeleteを押すだけで消えます. ZOOMへの参加 ZOOMに参加したい時は,アプリを起動してJoin the Meetingをクリックするだけで,その曜日その時間に入るべきZOOMに入れてくれます. (gifがなんかもっさりしていますが,実際はもっとサクサク動きます) お気づきの方もいらっしゃるかとは思いますがAUTOZOOMによるJoinはURLを開くだけなので,ZOOM以外にもリンク踏む系のものなら使用できます!! 実装と仕様 HTML+CSS+JavsScriptで作ったものをElectronでアプリ化しています. 実装方針は非常に簡単で,現在の曜日と時刻を取得してきて,保存されたURLの中から入るべきURLに飛ばすという形になっています. その他ポイントになりうる実装・仕様としては, URLは,Electron内で動いているChromium上のLocalStorageに保存している Join the Meetingは該当URLを直に貼っているので,実は押した時ではなく起動時に入るべきURLを選択する(これは僕自身がアプリを常駐させない派なので問題を感じなかったのですが,常駐させる派からクリック時にしてほしいとの意見があったのでアップデートかけるかもです) 既に登録済みの曜日時刻に登録すると上書きしてしまう あたりだと思われます. https://github.com/Kakeru-Miyazaki/autozoom3/ にソースコードは置いてあります.今後適宜アップデートしていきます. なお,このアプリ自体「こんなのあったら便利じゃない?笑」くらいのノリで AUTOZOOMコマンド として実装して自分で使っていたもの(初代AUTOZOOM)を友達に配布するためにGUIをつくって(AUTOZOOM2),それをさらに良い感じにしてできたのが AUTOZOOM3 なので構想が雑というか作り込みが甘いのはご了承ください アプリの配布 https://github.com/Kakeru-Miyazaki/autozoom3/releases/tag/ver2.0.0 にて,本アプリを配布しています! dmg,exe,debを用意しているので,Mac,Windows,Ubuntu等でお使いいただけます. 拙いアプリですが,気になった方はお試しいただけると嬉しいです! また,別記事の【VSCode】Undo/Redoに革命を起こしたい も読んでいただけたら幸いです. 最後まで読んでいただきありがとうございました. kakekakemiya
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GIF画像のデータ形式とHTML+JavaScript (ローカル生成)のサンプルプログラム

GIF のデータ形式 大雑把に以下の並びになっています。 バイト位置 バイト数 内容 0 6 ヘッダ (Header) 6 7 論理画面記述子 (Logical Screen Descriptor) (13) - (共通色表:存在する場合) - - 記述子 (Descriptor) ... ... ... - 1 終端子 (Trailer) ヘッダ (Header) バイト位置 バイト数 内容 0 3 花押 (Signature) 3 3 版 (Version) : 「87a」または「89a」 花押 (Signature) バイト位置 文字 10進 16進 0 'G' 71 0x47 1 'I' 73 0x49 2 'F' 70 0x46 版 (Version) 「87a」 バイト位置 文字 10進 16進 3 '8' 56 0x38 4 '7' 55 0x37 5 'a' 79 0x61 版 (Version) 「89a」 バイト位置 文字 10進 16進 3 '8' 56 0x38 4 '9' 57 0x39 5 'a' 79 0x61 論理画面記述子 (Logical Screen Descriptor) バイト位置 内容 6 画面の幅の下位8ビット (Logical Screen Width) 7 画面の幅の上位8ビット (Logical Screen Width) 8 画面の高さの下位8ビット (Logical Screen Height) 9 画面の高さの上位8ビット (Logical Screen Height) 10 混合情報 <Packed Fields> 11 背景色番号 (Background Color Index) 12 ピクセル縦横比 (Pixel Aspect Ratio) (13) (共通色表:存在する場合) (Global Color Table) 混合情報 <Packed Fields> ビット位置 ビット数 内容 備考 0 3 共通色数のビット数(Size of Global Color Table) SGCT と略す 3 1 共通色表は重要度順になっている(Sort Flag) 4 3 論理画面の色解像度(Color Resolution) ビット数(1〜8)を(0〜7)とする 7 1 共通色表あり(Global Color Table Flag) ピクセル縦横比 (Pixel Aspect Ratio) 計算式は次のとおり。 Aspect\ Ratio = \frac{ Pixel\ Aspect\ Ratio + 15 }{ 64 } = \frac{ 幅 } { 高 } 正方形 $\left[1:1\right]$ は $\left(Pixel\ Aspect\ Ratio = 49 \right)$ とする。 共通色表 (Global Color Table) 共有色表が存在する場合は $\left( n=2^{SGCT+1} \right)$ 個の色情報が必要です。 色番号 バイト位置(共通色表) +0 +1 +2 $0$ $0$ R[0] G[0] B[0] $1$ $3$ R[1] G[1] B[1] ... ... ... ... ... $n-1$ $3(n-1)$ R[n-1] G[n-1] B[n-1] $n$ は最大で $256$ です。 記述子 (Descriptor) 記述子には以下の種類があります。 先頭バイト値(Descriptor) 後続バイト値(Label) 内容 0x2C - 画像記述子 (Image Descriptor) 0x3B - 終端子 (Trailer) 0x21 必要 「89a」版による拡張 0x21 0xF9 図形制御拡張 (Graphic Control Extension) 0x21 0xFE 注釈拡張 (Comment Extension) 0x21 0x01 文字画面拡張 (Plain Text Extension) 0x21 0xFF 応用拡張 (Application Extension) 画像記述子 (Image Descriptor) バイト位置(画像記述子) 内容 0 画像記述子 (Image Separator) : 0x2C (Image Descriptor) 1 画像の位置(左)の下位8ビット (Image Left Position) 2 画像の位置(左)の上位8ビット (Image Left Position) 3 画像の位置(上)の下位8ビット (Image Top Position) 4 画像の位置(上)の上位8ビット (Image Top Position) 5 画像の幅の下位8ビット (Image Width) 6 画像の幅の上位8ビット (Image Width) 7 画像の高さの下位8ビット (Image Height) 8 画像の高さの上位8ビット (Image Height) 9 混合情報 <Packed Fields> (10) (固有色表:存在する場合) (Local Color Table) (?) 色番式画像 (Table Based Image Data) 混合情報 <Packed Fields> ビット位置 ビット数 内容 備考 0 3 固有色数のビット数(Size of Local Color Table) SLCT と略す 3 2 予約 (Reserved) 5 1 固有色表は重要度順になっている(Sort Flag) 6 1 縞状形式 (Interlace Flag) 7 1 固有色表あり(Local Color Table Flag) 縞状形式 (Interlace Flag) 縦方向を、次の順に並び替える。 先頭行(Y=0)から8行間隔 5行目(Y=4)から8行間隔 3行目(Y=2)から4行間隔 2行目(Y=1)から2行間隔 固有色表 (Local Color Table) 固有色表が存在する場合は $\left( n=2^{SLCT+1} \right)$ 個の色情報が必要です。 色番号 バイト位置(固有色表) +0 +1 +2 $0$ $0$ R[0] G[0] B[0] $1$ $3$ R[1] G[1] B[1] ... ... ... ... ... $n-1$ $3(n-1)$ R[n-1] G[n-1] B[n-1] $n$ は最大で $256$ です。 色番式画像 (Table Based Image Data) バイト位置(色番式画像) 内容 備考 0 LZW 最小コード幅 (LZW Minimum Code Size) LMCS と略す 1 圧縮画像(副塊情報形式 : Data Sub-blocks) LZW 圧縮 圧縮画像 画像の幅と高さを $w,h$ とすると $(w \times h)$ 個の色番号表が画像データとなります。 \ 0 1 ... w-1 0 $c_0$ $c_1$ ... $c_{w-1}$ 1 $c_w$ $c_{w+1}$ ... $c_{2w-1}$ ... ... ... ... ... h-1 $c_{w(h-1)}$ $c_{w(h-1)+1}$ ... $c_{w h - 1}$ 圧縮画像は、画像データを LZW アルゴリズムで圧縮して副塊情報形式 (Data Sub-blocks) で格納します。 副塊情報形式 (Data Sub-blocks) バイト位置(副塊情報形式) 内容 $0$ 後続のバイト数 $N$ $1$ データ列($N$ バイト) $N+2$ 後続のバイト数 $N+3$ データ列(続き) ... ... ... ... ... ... - 後続のバイト数 - データ列(続き) - 後続のバイト数 = 0 後続のバイト数は最大で 255 です。 「89a」版による拡張 「89a」版による拡張は、以下の形式となっています。 バイト位置(識別子) 内容 0 「89a」版拡張 (Extension Introducer):0x21 (Descriptor) 1 表題 (Label) 2 副塊情報形式 (Data Sub-blocks) - 〃 この形式により、未知の表題 (Label) を未知のデータとして処理可能になっています。 図形制御拡張 (Graphic Control Extension) バイト位置(図形制御拡張) 内容 0 「89a」版拡張 (Extension Introducer):0x21 (Descriptor) 1 図形制御表題 (Graphic Control Label):0xF9 2 情報の大きさ:0x04 (副塊の後続バイト数) 3 混合情報 <Packed Fields> 4 遅延時間(1/100秒単位)の下位8ビット (Delay Time) 5 遅延時間(1/100秒単位)の上位8ビット (Delay Time) 6 透明色とする色番号 (Transparet Color Index) 7 終端:0x00 (副塊の後続バイト数) 混合情報 <Packed Fields> ビット位置 ビット数 内容 0 3 予約 (Reserved) 3 3 消去方法 (Disposal Method) 6 1 ユーザー操作による遅延時間の短縮 (User Input Flag) 7 1 透明色が有効 (Transparent Color Flag) 消去方法 (Disposal Method) 値 内容 0 未定義(デコーダは何もしない) 1 消去しない 2 背景色 (論理画面記述子で指定) を使う 3 一つ前の画像に戻す 4-7 (予約) 注釈拡張 (Comment Extension) バイト位置(注釈拡張) 内容 0 「89a」版拡張 (Extension Introducer):0x21 (Descriptor) 1 注釈表題 (Comment Label):0xFE 2 注釈情報 (副塊情報形式:Data Sub-blocks) - 〃 文字画面拡張 (Plain Text Extension) バイト位置(文字画面拡張) 内容 0 「89a」版拡張 (Extension Introducer):0x21 (Descriptor) 1 文字画面表題 (Plain Text Label):0x01 2 文字画面設定のサイズ (副塊情報形式:Data Sub-blocks):0x0C 3 文字画面の位置(左)の下位8ビット (Text Grid Left Position) 4 文字画面の位置(左)の上位8ビット (Text Grid Left Position) 5 文字画面の位置(上)の下位8ビット (Text Grid Top Position) 6 文字画面の位置(上)の上位8ビット (Text Grid Top Position) 7 文字画面の幅の下位8ビット (Text Grid Width) 8 文字画面の幅の上位8ビット (Text Grid Width) 9 文字画面の高さの下位8ビット (Text Grid Height) 10 文字画面の高さの上位8ビット (Text Grid Height) 11 文字の幅 (Character Cell Width) 12 文字の高さ (Character Cell Height) 13 文字の共通色番号 (Text Foreground Color Index) 14 文字背景の共通色番号 (Text Background Color Index) 15 任意の文字列 (副塊情報形式:Data Sub-blocks) (Plain Text Data) - 〃 応用拡張 (Application Extension) バイト位置(応用拡張) 内容 0 「89a」版拡張 (Extension Introducer):0x21 (Descriptor) 1 応用拡張表題 (Application Extension):0xFF 2 識別子と認識コードのサイズ (副塊情報形式: Data Sub-blocks):0x0B 3 8バイトの識別子 (Application Identifier) 11 3バイトの認識コード (Application Authentication Code) 14 任意の応用拡張情報 (副塊情報形式:Data Sub-blocks) (Application Data) - 〃 終端子 (Trailer) バイト位置(オフセット) 内容 0 終端子 (Trailer):0x3B GIF における LZW のパラメータ LZW 最小コード幅 (LZW Minimum Code Size) を LMCS とします。 内容 値         各コードのビット幅(可変長データ) 3 〜 12 LMCS の最小値 2 初期化コード<Clear Code> $2^{LMCS}$ 最初のコード $2^{LMCS} + 1$ コードの最大値 $2^{12} - 1$ サンプル プログラム HTML+JavaScriptでローカル生成[少々長いので折りたたみ] gif_test.html <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" > <title>GIF プログラムのテスト</title> <style> body { margin-left: auto; margin-right: auto; width: 750px; } table { border: solid 1px gray; border-collaspe: collaspe; border-spacing: 0; } th, td { border: solid 1px gray; padding: 4px; } </style> </head> <body> <h2>GIF プログラムのテスト</h2> <table> <tr> <td style="text-align: center;"> Sample 1<br/> <span id="sample1"></span> </td> </tr> <tr> <td style="text-align: center;"> Sample 2<br/> <span id="sample2"></span><br/> 乱数を使用しているのでページを開く度に異なる </td> </tr> <tr> <td style="text-align: center;"> Sample 3<br/> <span id="sample3"></span><br/> 乱数を使用しているのでページを開く度に異なる </td> </tr> <tr> <td style="text-align: center;"> Sample 4<br/> <span id="sample4"></span><br/> 乱数を使用しているのでページを開く度に異なる </td> </tr> <tr> <td style="text-align: center;"> Sample 5<br/> <span id="sample5"></span><br/> 乱数を使用しているのでページを開く度に異なる </td> </tr> <!-- --> <tr> <td style="text-align: center;"> Sample A1<br/> <span id="sampleA1"></span><br/> 乱数を使用しているのでページを開く度に異なる </td> </tr> </table> <script type="text/javascript"> <!-- /* ********************************** */ /* * GIF 形式データを生成するクラス */ class MyGIF { static signature = [0x47, 0x49, 0x46]; static version87a = [0x38, 0x37, 0x61]; static version89a = [0x38, 0x39, 0x61]; // コンストラクタ. constructor(width, height, resolution, background, global_color, aspect) { const S = MyGIF; this.S = S; this.signature = S.signature; this.version = S.version89a; this.logicalScreen = S.createLogicalScreenDescriptor( width, height, resolution, background, global_color, aspect); this.descriptor = []; }; // GIF形式のバイナリを取得する. get_binary() { return [ this.signature, this.version, this.logicalScreen.get_binary(), this.descriptor.map((d, _) => d.get_binary()).flat(), ].flat(); }; // 任意の Descriptor を追加する. appendDescriptor(descriptor) { this.descriptor.push(descriptor); }; // Image Descriptor を追加する. appendImageDescriptor(x, y, w, h, data, color, interlace) { this.descriptor.push(this.S.createImageDescriptor(x, y, w, h, data, color, interlace)); }; // Graphic Control Extension を追加する. appendGraphicControlExtension(delay, method, transparent, input) { this.descriptor.push(this.S.createGraphicControlExtension(delay, method, transparent, input)); }; // Comment Extension を追加する. appendCommentExtension(text) { this.descriptor.push(this.S.createCommentExtension(text)); }; // Plain Text Extension を追加する. appendPlainTextExtension(x, y, w, h, cw, ch, fc, bc, text) { this.descriptor.push(this.S.createPlainTextExtension(x, y, w, h, cw, ch, fc, bc, text)); }; // Application Extension を追加する. appendApplicationExtension(id, code, data) { this.descriptor.push(this.S.createApplicationExtension(id, code, data)); }; // アニメーションのループ回数を設定する. appendApplicationExtensionForLoop(count) { this.appendApplicationExtension('NETSCAPE', '2.0', [1, (count & 0xff), ((count >> 8) & 0xff)]); }; // Trailer を追加する. appendTrailer() { this.descriptor.push(this.S.createTrailer()); }; // 8 ビット形式の取得. static uint8el(x) { return ((!x ? 0 : x) & 0xff); }; // 16 ビット・リトル・エンディアン形式の取得. static uint16el(x) { const y = (!x ? 0 : x); return [(y & 0xff), ((y >> 8) & 0xff)]; }; // 色情報の過不足を調整. static fixColorTable(depth, color) { const colen = (1 << (depth + 1)) * 3; let table = color.flat(); return ((table.length > colen) ? table.slice(0, colen) : table.concat(Array(colen - table.length).fill(0))); }; // 文字の UTF-8 バイナリ化. static charToUTF8(c) { if (c < 0x0080) return c; if (c < 0x0800) return [ (0xc0 | (c >> 6)), (0x80 | (c & 0x3f)), ]; return [ (0xe0 | (c >> 12)), (0x80 | ((c >> 6) & 0x3f)), (0x80 | (c & 0x3f)), ]; }; static stringToUTF8(s) { return [...Array(s.length)].map((_, i) => MyGIF.charToUTF8(s.charCodeAt(i))).flat(); }; // Data Sub-blocks の生成. static createDataSubBlocks(data) { let block = []; let pos = 0; let length = data.length; while (length) { let slen = (length < 256) ? length : 255; block.push(slen); block.push(data.slice(pos, pos + slen)); pos += slen; length -= slen; }; block.push(0); return block.flat(); }; // Logical Screen Descriptor の生成. static createLogicalScreenDescriptor(width, height, resolution, background, color, sort, aspect) { const S = MyGIF; const D = { logical_screen_width: width, logical_screen_height: height, size_of_global_color_table: 0, sort_flag: sort, color_resolution: resolution, global_color_table_flag: false, background_color_index: background, pixel_aspect_ratio: ((aspect == undefined) ? 49 : aspect), global_color_table: undefined, set_color: (function(table) { const flag = (table != undefined); D.size_of_global_color_table = (!flag ? 0 : (31 - Math.clz32(table.length-1))); D.global_color_table_flag = flag; D.global_color_table = table; }), get_binary: (function() { const sgct = (!D.size_of_global_color_table ? 0 : (7 & D.size_of_global_color_table)); const sort = (!D.sort_flag ? 0 : 1); const res = (!D.color_resolution ? 0 : (7 & D.color_resolution)); const gcf = (!D.global_color_table_flag ? 0 : 1); const bgc = (!D.background_color_index ? 0 : D.background_color_index); const aspect = ((D.pixel_aspect_ratio == undefined) ? 49 : D.pixel_aspect_ratio); const color = (!gcf ? [] : S.fixColorTable(sgct, D.global_color_table)); return [ S.uint16el(D.logical_screen_width), S.uint16el(D.logical_screen_height), (sgct | (sort << 3) | (res << 4) | (gcf << 7)), S.uint8el(bgc), S.uint8el(aspect), color.map((v, _) => S.uint8el(v)), ].flat(); }), }; D.set_color(color); return D; }; // Image Descriptor の生成. static createImageDescriptor(x, y, w, h, data, color, interlace) { const S = MyGIF; const D = { descriptor: 0x2c, image_left_position: x, image_top_position: y, image_width: w, image_height: h, size_of_local_color_table: 0, interlace_flag: interlace, local_color_table_flag: false, local_color_table: undefined, table_based_image_data: S.createTableBasedImageData(data), set_color_table: (function(table) { const flag = (table != undefined); D.size_of_local_color_table = (!flag ? 0 : (31 - Math.clz32(table.length-1))); D.local_color_table_flag = flag; D.local_color_table = table; }), get_binary: (function() { const slct = (!D.size_of_local_color_table ? 0 : (7 & D.size_of_local_color_table)); const interlace = (!D.interlace ? 0 : 1); const lcf = (!D.local_color_table_flag ? 0 : 1); const color = (!lcf ? [] : S.fixColorTable(slct, D.local_color_table)); return [ D.descriptor, S.uint16el(D.image_left_position), S.uint16el(D.image_top_position), S.uint16el(D.image_width), S.uint16el(D.image_height), (slct | (interlace << 6) | (lcf << 7)), color.map((v, _) => S.uint8el(v)), D.table_based_image_data.lzw_minimum_code_size, D.table_based_image_data.data, ].flat(); }), }; D.set_color_table(color); return D; }; // Table Based Image の生成 (LZW圧縮) static createTableBasedImageData(source) { const S = MyGIF; const source_size = source.length; // const src_max = Math.max.apply(null, source); // 'Maximum call stack size exceeded' がでる. const src_max = function() { let m = 0; for (const d of source) if (m < d) m = d; return m; }(); const src_max_clz = (32 - Math.clz32(src_max)); const bits_init = ((src_max_clz < 2) ? 2 : src_max_clz); let bits_curr = (bits_init + 1); const code_base = (1 << bits_init); const code_clear = code_base; const code_end = code_base + 1; const code_max = ((1 << 12) - 1); let code_curr = code_end; let code_step = (1 << bits_curr); const tree = [...Array(code_max + 2)].map((_, i) => ({ code: i, next: -1, down: -1, data: 0 })); const buffer = []; let bs_pos = 0; const write = function(data) { const bits = bits_curr; let idxs = (bs_pos >> 3); const idxe = ((bs_pos + bits - 1) >> 3); data <<= (bs_pos & 7); if (idxs < buffer.length) { buffer[idxs] |= (data & 0xff); data >>= 8; idxs++; }; while (idxs <= idxe) { buffer.push(data & 0xff); data >>= 8; idxs++; }; bs_pos += bits; }; write(code_clear); if (!source_size) { write(code_end); return { lzw_minimum_code_size: bits_init, data: S.createDataSubBlocks(buffer), }; }; let siter = source.values(); let sdat = siter.next(); lzw: for (;;) { let data, next; let node = tree[(data = sdat.value)]; scan: for (;;) { if ((sdat = siter.next()).done) { write(node.code); break lzw; }; data = sdat.value; if ((next = tree[node.down]) == undefined) break; while (data != next.data) if ((next = tree[next.next]) == undefined) break scan; node = next; }; write(node.code); { const next = tree[++code_curr]; // next.code = code_curr; next.next = node.down; next.down = -1; next.data = data; node.down = code_curr; } if (code_curr < code_step) continue; if (code_curr < code_max) { bits_curr++; if ((code_step <<= 1) < code_max) continue; code_step = code_max; continue; }; write(code_clear); bits_curr = bits_init + 1; code_step = (1 << bits_curr); code_curr = code_end; for (let i = 0; i < code_base; i++) { const node = tree[i]; // node.code = i; node.next = -1; node.down = -1; // node.data = 0; }; }; write(code_end); return { lzw_minimum_code_size: bits_init, data: S.createDataSubBlocks(buffer), }; }; // Graphic Control Extension の生成. static createGraphicControlExtension(delay, method, transparent, input) { const S = MyGIF; const D = { descriptor: 0x21, label: 0xf9, disposal_method: method, user_input_flag: input, transparent_color_flag: false, delay_time: delay, transparent_color_index: 0, set_transparent: (function(index) { D.transparent_color_flag = (index != undefined); D.transparent_color_index = (!index ? 0 : index); }), get_binary: (function() { const method = (!D.disposal_method ? 0 : (7 & D.disposal_method)); const input = (!D.user_input_flag ? 0 : 1); const ftrans = (!D.transparent_color_flag ? 0 : 1); return [ D.descriptor, D.label, 4, /* Data Sub-blocks */ ((method << 3) | (input << 6) | (ftrans << 7)), S.uint16el(D.delay_time), S.uint8el(D.transparent_color_index), 0, /* Data Sub-blocks */ ].flat(); }), }; D.set_transparent(transparent); return D; }; // Comment Extension の生成. static createCommentExtension(text) { const S = MyGIF; const D = { descriptor: 0x21, label: 0xfe, text: text, get_binary: (() => [ D.descriptor, D.label, S.createDataSubBlocks(S.stringToUTF8(D.tex)), ].flat()), }; return D; }; // Plain Text Extension の生成. static createPlainTextExtension(x, y, w, h, cw, ch, fc, bc, text) { const S = MyGIF; const D = { descriptor: 0x21, label: 0x01, text_grid_left_position: x, text_grid_top_position: y, text_grid_width: w, text_grid_height: h, character_cell_width: cw, character_cell_height: ch, text_foreground_color_index: fc, text_background_color_index: bc, plain_text_data: text, get_binary: (() => [ D.descriptor, D.label, 12, /* Data Sub-blocks */ S.uint16el(D.text_grid_left_position), S.uint16el(D.text_grid_top_position), S.uint16el(D.text_grid_width), S.uint16el(D.text_grid_height), S.uint8el(D.character_cell_width), S.uint8el(D.character_cell_height), S.uint8el(D.text_foreground_color_index), S.uint8el(D.text_background_color_index), S.createDataSubBlocks(S.stringToUTF8(D.plain_text_data)), ].flat()), }; }; // Application Extension の生成. static createApplicationExtension(id, code, data) { const S = MyGIF; const D = { descriptor: 0x21, label: 0xff, application_identifier: id, application_authentication_code: code, application_data: data, get_binary: (() => [ D.descriptor, D.label, 11, /* Data Sub-blocks */ S.stringToUTF8(D.application_identifier + ' ').slice(0, 8), S.stringToUTF8(D.application_authentication_code + ' ').slice(0, 3), S.createDataSubBlocks(D.application_data.map((v, _) => S.uint8el(v))), ].flat()), }; return D; }; // Trailer の生成. static createTrailer() { return { descriptor: 0x3b, get_binary: (() => 0x3b), }; }; }; /* **************************************** */ /* * Base 64 符号化 */ class MyBase64 { static EncodeTable = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; static encode(b) { const table = MyBase64.EncodeTable; const blen = b.length; const brem = blen % 3; const bcnt = blen - brem; let s = ''; let i = 0; while (i < bcnt) { const d0 = b[i++]; const d1 = b[i++]; const d2 = b[i++]; const d = (d0 << 16) | (d1 << 8) | d2; s += table[(d >> 18) & 0x3f]; s += table[(d >> 12) & 0x3f]; s += table[(d >> 6) & 0x3f]; s += table[d & 0x3f]; }; if (brem) { const b2 = (brem == 2); const d0 = b[i++]; const d1 = (b2 ? b[i++] : 0); const d = (d0 << 16) | (d1 << 8); s += table[(d >> 18) & 0x3f]; s += table[(d >> 12) & 0x3f]; s += (b2 ? table[(d >> 6) & 0x3f] : '='); s += '='; }; return s; }; }; /* ********************************** */ /* * GIF 画像生成用 */ class MyPixelBuffer { static grayScaleColor = function(count) { const m = 255; const n = count - 1; return [...Array(count)].map(function(_, k) { const l = Math.trunc((k * m) / n); return [l, l, l]; }); }; static grayScaleColor4 = MyPixelBuffer.grayScaleColor(4); static grayScaleColor256 = MyPixelBuffer.grayScaleColor(256); static simpleColorBase = [...Array(8)].map((_, i) => Math.trunc(i * 255 / 7)); static simpleColor = [...Array(256)].map((_, i) => [ MyPixelBuffer.simpleColorBase[(i >> 5) & 7], MyPixelBuffer.simpleColorBase[(i >> 2) & 7], [0, 85, 170, 255][i & 3], ]); static interlaceMap = new Map(); static createInterlaceTable(h) { const m = MyPixelBuffer.interlaceMap; if (m.has(h)) return m.get(h); const ph = [ [0, 8, ((h + 7) >> 3)], [4, 8, ((h + 3) >> 3)], [2, 4, ((h + 1) >> 2)], [1, 2, (h >> 1)], ]; const tab = [...Array(4)].map( (_, i) => [...Array(ph[i][2])].map( (_, j) => ph[i][0] + ph[i][1] * j)).flat(); m.set(h, tab); return tab; }; // コンストラクタ. constructor(color, width, height, defcol=0) { this.S = MyPixelBuffer; this.GIF = MyGIF; this.color = color; this.width = width; this.height = height; this.pixel = Array(width * height).fill(defcol); }; // 点を打つ. setPoint(x, y, c) { const w = this.width; if ((0 <= x) && (x < w) && (0 <= y) && (y < this.height)) { const p = y * w + x; if ((p < 0) || (this.pixel.length <= p)) { console.log(p); }; this.pixel[p] = c; } }; // 線を引く. setLine(x1, y1, x2, y2, c, sep) { const dx = Math.trunc(Math.abs(x1 - x2)); const dy = Math.trunc(Math.abs(y1 - y2)); if (!dx) this.setVirticalLine(x1, y1, y2, c); else if (!dy) this.setHorizontalLine(x1, x2, y1, c); else if (dx >= dy) this.setLineH(x1, y1, x2, y2, dx, dy, c, sep); else this.setLineV(x1, y1, x2, y2, dx, dy, c, sep); }; setLineH(x1, y1, x2, y2, dx, dy, c, sep) { const S = this.S; const w = this.width; const h = this.height; let xs, ys, xe, ye; if (x1 < x2) { xs = x1; ys = y1; xe = x2; ye = y2; } else { xs = x2; ys = y2; xe = x1; ye = y1; }; if ((xe < 0) || (w <= xs)) return; const ay = ((ys < ye) ? +1 : -1); const ap = (ay < 0) ? -w: +w; let k = (dy >> 1); dx++; if (sep) { dy++; k = 0; }; if (xs < 0) { const kt = k - (dy * xs); const yh = Math.trunc(kt / dx); xs = 0; ys += ((ay < 0) ? -yh : +yh); k = (kt % dx); }; if (w <= xe) xe = w - 1; const yt = Math.min(ys, ye); const yb = Math.max(ys, ye); if ((yb < 0) || (h <= yt)) return; if (ay < 0) { if (ye < 0) ye = -1; if (h <= ys) { const yh = ys - h + 1; const kt = k + (dx * yh); const xw = Math.trunc(kt / dy); xs += xw; ys = h - 1; k = kt % dy; }; } else { if (h <= ye) ye = h; if (ys < 0) { const kt = k - (dx * ys); const xw = Math.trunc(kt / dy); xs += xw; ys = 0; k = kt % dy; }; }; const pixel = this.pixel; let p = ys * w + xs; while (xs++ <= xe) { pixel[p++] = c; if ((k += dy) >= dx) { k -= dx; p += ap; if ((ys += ay) == ye) break; }; }; }; setLineV(x1, y1, x2, y2, dx, dy, c, sep) { const S = this.S; const w = this.width; const h = this.height; let xs, ys, xe, ye; if (y1 < y2) { xs = x1; ys = y1; xe = x2; ye = y2; } else { xs = x2; ys = y2; xe = x1; ye = y1; }; if ((ye < 0) || (h <= ys)) return; const ax = ((xs < xe) ? +1 : -1); let k = (dy >> 1); dy++; if (sep) { dx++; k = 0; }; if (ys < 0) { const kt = k - (dx * ys); const xw = Math.trunc(kt / dy); xs += ((ax < 0) ? -xw : +xw); ys = 0; k = (kt % dy); }; if (h <= ye) ye = h - 1; const xl = Math.min(xs, xe); const xr = Math.max(xs, xe); if ((xr < 0) || (w <= xl)) return; if (ax < 0) { if (xe < 0) xe = -1; if (w <= xs) { const xw = xs - w + 1; const kt = k + (dy * xw); const yh = Math.trunc(kt / dx); xs = w - 1; ys += yh; k = kt % dx; }; } else { if (w <= xe) xe = w; if (xs < 0) { const kt = k - (dy * xs); const yh = Math.trunc(kt / dx); xs = 0; ys += yh; k = kt % dx; }; }; const pixel = this.pixel; let p = ys * w + xs; while (ys++ <= ye) { pixel[p] = c; p += w; if ((k += dx) >= dy) { k -= dy; p += ax; if ((xs += ax) == xe) break; }; }; }; // 水平線を引く. setHorizontalLine(x1, x2, y, c) { const w = this.width; const h = this.height; const pixel = this.pixel; const xs = Math.trunc(Math.min(x1, x2)); const xe = Math.trunc(Math.max(x1, x2)); if ((xe < 0) || (w <= xs) || (y < 0) || (h <= y)) return; const us = Math.trunc(Math.max(xs, 0)); const ue = Math.trunc(Math.min(xe, w-1)); const yp = y * w; pixel.fill(c, yp + us, yp + ue + 1); } // 垂直線を引く. setVirticalLine(x, y1, y2, c) { const w = this.width; const h = this.height; const pixel = this.pixel; const ys = Math.trunc(Math.min(y1, y2)); const ye = Math.trunc(Math.max(y1, y2)); if ((x < 0) || (w <= x) || (ye < 0) || (h <= ys)) return; const vs = Math.trunc(Math.max(ys, 0)); const ve = Math.trunc(Math.min(ye, h-1)); const q = ve * w + x; for (let p = vs * w + x; p <= q; p += w) pixel[p] = c; } // 四角形を描く. setBox(x1, y1, x2, y2, c) { const xs = Math.trunc(Math.min(x1, x2)); const xe = Math.trunc(Math.max(x1, x2)); const ys = Math.trunc(Math.min(y1, y2)); const ye = Math.trunc(Math.max(y1, y2)); this.setHorizontalLine(xs, xe, ys, c); if (ys != ye) { this.setVirticalLine(xs, ys+1, ye-1, c); if (xs != xe) this.setVirticalLine(xe, ys+1, ye-1, c); this.setHorizontalLine(xs, xe, ye, c); }; }; // 四角形で塗りつぶす. setBoxFill(x1, y1, x2, y2, c) { const w = this.width; const h = this.height; const pixel = this.pixel; const xs = Math.min(x1, x2); const xe = Math.max(x1, x2); const ys = Math.min(y1, y2); const ye = Math.max(y1, y2); if ((xe < 0) || (w <= xs) || (ye < 0) || (h <= ys)) return; const us = Math.trunc(Math.max(xs, 0)); const ue = Math.trunc(Math.min(xe, w-1)); const vs = Math.trunc(Math.max(ys, 0)); const ve = Math.trunc(Math.min(ye, h-1)); const nu = (ue - us) + 1; const nv = (ve - vs) + 1; let vp = vs * w + us; const vq = nv * w + vp; for (; vp < vq; vp += w) pixel.fill(c, vp, vp + nu); }; // 円描画用の座標情報を生成する. static getCirclePos(radius) { if (radius <= 0) return [[0, 0]]; let x = radius; let y = 0; let s = 0; let t = 1; let u = (radius << 1) - 1; let v = u; /* 大きな円は(上下左右)端の直線を少し短くする */ u -= ((u >> 2) + (u >> 3) + (u >> 4)); let otmp = new Array(); const out1 = new Array(); const out2 = new Array(); while (x > y) { otmp.push(y); out2.push([x]); y = y + 1; s = s + t; t = t + 2; if (s < u) continue; out1.push(otmp); x = x - 1; v = v - 2; u = u + v; otmp = new Array(); }; if (x == y) out2.push([x]); if (otmp.length) out1.push(otmp); out1.reverse(); return out2.concat(out1); }; // 円を描く. setCircle(x, y, r, c) { const w = this.width; const h = this.height; if (((x + r + 1) < 0) || (w < (x - r)) || ((y + r + 1) < 0) || (h < (y - r))) return; const pixel = this.pixel; const ypos = this.S.getCirclePos(r); const ylen = ypos.length; { let yi = 0; let ys = y; if (ys < 0) { yi = -ys; ys = 0; }; let yp = ys * w; while (yi < ylen) { if (ys >= h) break; for (const xp of ypos[yi]) { const xl = x - xp; const xr = x + xp; if (w <= xl) continue; if (xr < 0) continue; if (xr < w) pixel[yp + xr] = c; if (0 <= xl) pixel[yp + xl] = c; }; yi++; ys++; yp += w; }; }; { let yi = 0; let ys = y; if (ys >= h) { yi += (ys - h + 1); ys = h - 1; }; let yp = ys * w; while (yi < ylen) { if (ys < 0) break; for (const xp of ypos[yi]) { const xl = x - xp; const xr = x + xp; if (w <= xl) continue; if (xr < 0) continue; if (xr < w) pixel[yp + xr] = c; if (0 <= xl) pixel[yp + xl] = c; }; ys--; yi++; yp -= w; }; }; }; // 円で塗りつぶす. setCircleFill(x, y, r, c) { const w = this.width; const h = this.height; if (((x + r + 1) < 0) || (w < (x - r)) || ((y + r + 1) < 0) || (h < (y - r))) return; const pixel = this.pixel; const ypos = this.S.getCirclePos(r); const ylen = ypos.length; { let yi = 0; let ys = y; if (ys < 0) { yi = -ys; ys = 0; }; let yp = ys * w; while (yi < ylen) { if (ys >= h) break; const yl = ypos[yi]; const xp = yl[yl.length - 1]; let xl = x - xp; let xr = x + xp; if ((0 <= xr) && (xl < w)) { if (xl < 0) xl = 0; if (xr >= w) xr = w - 1; pixel.fill(c, yp + xl, yp + xr + 1); } yi++; ys++; yp += w; }; }; { let yi = 0; let ys = y; if (ys >= h) { yi += (ys - h + 1); ys = h - 1; }; let yp = ys * w; while (yi < ylen) { if (ys < 0) break; const yl = ypos[yi]; const xp = yl[yl.length - 1]; let xl = x - xp; let xr = x + xp; if ((0 <= xr) && (xl < w)) { if (xl < 0) xl = 0; if (xr >= w) xr = w - 1; pixel.fill(c, yp + xl, yp + xr + 1); } ys--; yi++; yp -= w; }; }; }; // 凸多角形を描画する. setPolygon(vertices, c) { const w = this.width; const h = this.height; const pixel = this.pixel; const vlen = vertices.length; if (!vlen) return; let xmin = w, xmax = -1; let ymin = h, ymax = -1; for (const [x, y] of vertices) { xmin = Math.min(xmin, x); xmax = Math.max(xmax, x); ymin = Math.min(ymin, y); ymax = Math.max(ymax, y); }; xmin = Math.trunc(xmin); xmax = Math.trunc(xmax); ymin = Math.trunc(ymin); ymax = Math.trunc(ymax); if ((xmax < 0) || (w <= xmin) || (ymax < 0) || (h <= ymin)) return; const raster = [...Array(h)].map(() => []); let [nx, ny] = vertices[vlen - 1]; for (const np of vertices) { const lx = nx; const ly = ny; [nx, ny] = np; const ay = (ny < ly) ? -1 : +1; const dx = nx - lx; const dy = (ay < 0) ? (ly - ny) : (ny - ly); if (dy == 0) { // if ((0 <= ly) && (ly < h)) // raster[ly].push(lx, nx); continue; }; let ys = ly; let ye = ny; let yp = 0; if (ay >= 0) { if ((ye < 0) || (h <= ys)) continue; if (ys < 0) { yp -= ys; ys = 0; }; if (ye >= h) ye = h - 1; } else { if ((ys < 0) || (h <= ye)) continue; if (ys >= h) { yp += ys - h + 1; ys = h - 1; }; if (ye < 0) ye = -1; }; for (; ys != ye; ys += ay, yp++) raster[ys].push(lx + Math.trunc(dx * yp / dy)); }; const rcmp = ((a, b) => (a - b)); let rs = Math.trunc(Math.max(ymin, 0)); let re = Math.trunc(Math.min(ymax, h - 1)); for (let yp = rs * w; rs <= re; yp += w) { const rp = raster[rs++]; const rplen = rp.length; if (rplen == 1) continue; rp.sort(rcmp); for (let np = 0; np < rplen; np += 2) { let xl = rp[np]; let xr = rp[np + 1]; if (xl > xr) [xl, xr] = [xr, xl]; if ((xr < 0) || (w <= xl)) continue; if (xl < 0) xl = 0; if (xr >= w) xr = w - 1; pixel.fill(c, yp + xl, yp + xr + 1); } }; }; // インターレース画像を作成する. createInterlaced() { const tab = this.S.createInterlaceTable(h); const w = this.width; const h = this.height; const pixel = this.pixel; const line = [...Array(h)].map( function(_, y) { const p = y * w; return pixel.slice(p, p + w); }); return [...Array(h)].map((_, y) => line[tab[y]]).flat(); }; // GIF 処理用オブジェクトを作成する. createGIF(global_color) { const color = (global_color ? this.color : undefined); return new this.GIF(this.width, this.height, (8 - 1), 0, color); }; // Image Descriptor を作成する. createImageDescriptor(local_color, x, y, interlace) { const color = (local_color ? this.color : undefined); const w = this.width; const h = this.height; const pixel = this.pixel; const data = (!interlace ? pixel : this.createInterlaced()); return this.GIF.createImageDescriptor(x, y, w, h, data, color, interlace); }; }; /* ********************************** */ /* * テスト */ function createImgWithBase64(b64s, id) { const priority = 'important'; const img = document.createElement('img'); if (id != undefined) img.setAttribute('id', id); img.setAttribute('class', 'b64img'); img.setAttribute('src', 'data:image/gif;charset=utf-8;base64,' + b64s); img.style.setProperty('margin', '0', priority); img.style.setProperty('padding', '8px', priority); img.style.setProperty('image-rendering', 'pixelated', priority); return img; } function intRand(w) { return Math.trunc(Math.random() * w); } function makeSampleGIF(fb) { const gif = fb.createGIF(true); gif.appendDescriptor(fb.createImageDescriptor()); gif.appendTrailer(); return gif; } function makeSample1FB() { const fb = new MyPixelBuffer(MyPixelBuffer.grayScaleColor256, 640, 480); for (let p = 0; p < 256; p++) fb.setBox(p, p, 639-p, 479-p, p); return fb; } function makeSample1() { return makeSampleGIF(makeSample1FB()); } function makeSample2FB() { const fb = new MyPixelBuffer(MyPixelBuffer.simpleColor, 640, 480); for (let n = 0; n < 20; n++) { const x1 = intRand(640 + 320) - 160; const y1 = intRand(480 + 240) - 120; const w = intRand(320) + 16; const h = intRand(240) + 16; const c = intRand(256); const x2 = x1 + w; const y2 = y1 + h; fb.setLine(x1, y1, x2, y2, c); fb.setLine(x2, y1, x1, y2, c); fb.setBox(x1, y1, x2, y2, c); fb.setCircle(y1, x1, Math.min(x2, y2), c); } return fb; } function makeSample2() { return makeSampleGIF(makeSample2FB()); } function makeSample3FB() { const fb = new MyPixelBuffer(MyPixelBuffer.simpleColor, 640, 480); for (let n = 0; n < 100; n++) { const x = intRand(640+32) - 16; const y = intRand(480+32) - 16; const w = intRand(160); const h = intRand(120); const c = intRand(256); fb.setBoxFill(x, y, x+w, y+h, c); } return fb; } function makeSample3() { return makeSampleGIF(makeSample3FB()); } function makeSample4FB() { const fb = new MyPixelBuffer(MyPixelBuffer.simpleColor, 640, 480); for (let n = 0; n < 100; n++) { const x = intRand(640 + 320) - 160; const y = intRand(480 + 240) - 120; const r = intRand(80); const c = intRand(256); fb.setCircleFill(x, y, r, c); } return fb; } function makeSample4() { return makeSampleGIF(makeSample4FB()); } function makeSample5FB() { const fb = new MyPixelBuffer(MyPixelBuffer.simpleColor, 640, 480); for (let n1 = 0; n1 < 100; n1++) { const np = intRand(5) + 3; let x = intRand(640 + 320) - 160; let y = intRand(480 + 240) - 120; const c = intRand(256); const pl = new Array([x, y]); let da = intRand(360); let ra = 300; for (let n2 = 1; n2 < np; n2++) { const dr = da * Math.PI / 180; const dl = intRand(120) + 10; const dx = Math.trunc(dl * Math.cos(dr)); const dy = Math.trunc(dl * Math.sin(dr)); pl.push([(x += dx), (y += dy)]); const ma = Math.trunc(ra / (np - n2)) >> 1; const na = intRand(ma) + ma; da += na; ra -= na; } fb.setPolygon(pl, c); } fb.setPolygon([[10, 10], [100, 10], [10, 100]], 255); // fb.setPolygon([[640-10, 480-10], [640-100, 480-10], [640-100, 480-100], [640-10, 480-100]], 0x1c); fb.setPolygon([[640-10, 480-10], [640-100, 480-10], [640-10, 480-100]], 255); // fb.setPolygon([[640-10, 10], [10, 150], [640-150, 480-10]], 0x03); return fb; } function makeSample5() { return makeSampleGIF(makeSample5FB()); } function makeSampleA() { const fb1 = makeSample1FB(); const fb2 = makeSample2FB(); const fb3 = makeSample3FB(); const fb4 = makeSample4FB(); const fb5 = makeSample5FB(); const gif = fb2.createGIF(); gif.appendApplicationExtensionForLoop(0); gif.appendGraphicControlExtension(30); gif.appendDescriptor(fb1.createImageDescriptor(true)); gif.appendGraphicControlExtension(30); gif.appendDescriptor(fb2.createImageDescriptor(true)); gif.appendGraphicControlExtension(30); gif.appendDescriptor(fb3.createImageDescriptor(true)); gif.appendGraphicControlExtension(30); gif.appendDescriptor(fb4.createImageDescriptor(true)); gif.appendGraphicControlExtension(30); gif.appendDescriptor(fb5.createImageDescriptor(true)); gif.appendTrailer(); return gif; } function setSample(id, gif) { const tag = document.getElementById(id); if (tag == undefined) return; const img = createImgWithBase64(MyBase64.encode(gif().get_binary())); while (tag.firstChild) tag.removeChild(giftag.firstChild); tag.append(img); } function onLoad() { setSample('sample1', makeSample1); setSample('sample2', makeSample2); setSample('sample3', makeSample3); setSample('sample4', makeSample4); setSample('sample5', makeSample5); setSample('sampleA1', makeSampleA); } onLoad(); /* ********************************** */ // !--> </script> </body> </html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webの勉強はじめてみた その20 ~Node.js編 ライブラリの使い方~

N予備校「プログラミング入門Webアプリ」コースを受講しています。 今回は第3章4,5節です。 ライブラリを利用する require: 必要なモジュールだけをメモリに記録する CSVを読み込む 'use strict'; //FileSystem Module const fs = require('fs'); //ReadLine Module const readline = require('readline'); //ReadStream from CSV const rs = fs.createReadStream('./popu-pref.csv'); //ReadLine from rs const rl = readline.createInterface({input:rs, output:{}}); //DataMap key: 都道府県 value: 集計データのオブジェクト const prefectureDataMap = new Map(); //line Event // function(lineString){} rl.on('line', lineString=>{ //配列として取得 const columns = lineString.split(','); //年 const year = parseInt(columns[0]); //都道府県 const prefecture = columns[1]; //人口 const popu = parseInt(columns[3]); //2010年 と 2015年を抽出 if(year === 2010 || year === 2015){ //DataMapに都道府県が存在するかチェック let value = prefectureDataMap.get(prefecture); if(!value){ //存在しない場合、valueを初期化 value ={ //2010年人口 popu10: 0, //2015年人口 popu15: 0, //変化率 change: null }; } if(year === 2010){ value.popu10 = popu; } if(year === 2015){ value.popu15 = popu; } //DataMap に追加 prefectureDataMap.set(prefecture, value); } }) //close Event rl.on('close', ()=>{ //変化率 for(let [key, data] of prefectureDataMap){ data.change = data.popu15 / data.popu10; } //配列をソート 比較元(pair1)と比較先(pair2) const rankingArray = Array.from(prefectureDataMap).sort((pair1, pair2) =>{ //pair1[0], pair2[0]は都道府県(key) //pair1[1], pair2[1]は連想配列のオブジェクト //変化率を降順 return pair2[1].change - pair1[1].change; }); const rankingStrings = rankingArray.map(([key, data], num) =>{ let rank = num + 1; return `第 ${rank} 位: ${key} : ${data.popu10} => ${data.popu15} 変化率: ${data.change}`; }) console.log(rankingStrings); }) onメソッド:関数をイベント登録する。AddHandlerみたいなもの? parseInt: 整数値へ変換。CInt。 map: Array の要素それぞれを、与えられた関数を適用した内容に変換する。 yarn Node.jsのためのパッケージマネージャ 依存関係にあるパッケージのインストールも行ってくれる。 インストール curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.21.1 グローバルインストール: Node.js の実行環境自体に npm パッケージをインストール ローカルインストール: 現在のディレクトリにのみインストール 他のディレクトリから利用することは出来ない。 パッケージ yarn add パッケージ名 npmパッケージのインストール。 ローカルのフォルダを指定してインストールできる。 yarn init npmパッケージ化。package.jasonを作成する。 name : パッケージ名 version : バージョン description : 説明 entry point : ライブラリとして読み込まれるJSファイル repository url : Gitリポジトリに公開する際のURL author : npmに登録された際の著者名 license : デフォルトはMITライセンス private : npmパッケージを公開するかどうか パッケージ化するスクリプト 'use strict'; function add(numbers){ let result = 0; for(let num of numbers){ result = result + num; } return result; } function multi(numbers){ let result = 1; for(let num of numbers){ result = result * num; } return result; } module.exports = { add, multi }; module exports ={} オブジェクトのプロパティとして関数を登録 まとめ アロー関数にまだ戸惑う。 基本的には既存のライブラリを使いましょうということだけれど、そもそもフレームワークやAPIの違いってなんだろうかとふと思った。あとで調べたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】Promiseがよくわからないから改造して遊んでみた 〜「任意の状態が完了するまで待つ」という処理をつくる方法〜

最終的に完成したコード 結果から言え、と怒られそうな日記のような記事になってしまったので、 とりあえず「任意の状態が完了するまで待つ」という処理の書き方だけ頭に持ってきます。 ※ このコードはあくまでPromiseの理解をする過程で副産物的に生まれたコードであり、記事の本質はこのコードが生まれる過程で理解した内容です。 ※ このような変則的な書き方を利用すると、Promiseの本来の使い方(then/catch)を普通に理解できている人たちを混乱させてしまうので、ご利用は慎重にお願いします。 function generateControllablePromise( methodNameToResolve, methodNameToReject ){ let toResolve, toReject; const promise = new Promise((res,rej)=>{ toResolve = res; toReject = rej; }); promise[methodNameToResolve] = toResolve; promise[methodNameToReject] = toReject; return promise; } const 状態監視用の変数 = generateControllablePromise( '状態を完了させるメソッドの名前', '状態をエラーにするメソッドの名前' ); 使い方: 1. 状態が完了するまで待たせたい部分では await 状態監視用の変数; というコードを挿入しておく。 2. 任意のタイミングで状態監視用の変数.状態を完了させるメソッドの名前(); を実行する。 3. ↑の関数が実行されると、await 状態監視用の変数; で待たせていた処理が発動する。 例: const loading = generateControllablePromise( 'complete', 'error' ); async function anyProcess(){ await loading; // これ以降の処理の実行は、loadingが完了するまで待ってもらうことができる。 console.log('ロードが完了しました!'); } anyProcess(); if( isLoaded ){ // 状態を完了させるメソッドを実行することで、待たせていた処理を発動させることができる。 loading.complete(); } あらすじ いままで非同期処理をつかうときはPromiseやthen/catchを使う書き方を避けて、 async/awaitだったりtry/catchしか使ってこなかったので、 new Promise( ...してるコードなんかを見ると、死んだ魚の目になっていました。 しかし今回、作っている作品の中で GoogleAPIの読み込み状況を管理しなきゃいけない状況があり どうやってやればいいのかの調べていたら、 ひときわ「なんじゃこのPromiseの使い方は!!?」と思うような書き方と出会いました。 その書き方を読んでみたり、いじくってみたりして、理解できたことをメモがてらまとめようと思います。 ※ async/awaitの使い方も理解できてない人には、私が昔書いた記事をオススメしときます。 【JavaScript】非同期処理とasync/await ~難しいこと抜きで、まず使いはじめるための知識~ ( ´ε` )? 注意 この記事は、Promiseの説明サイトなどを読むのを避けてきた程度の知能の猿人が、 実際にPromiseを使うコードをいじってみることで、やっとPromiseのなんたるかを理解した という話をまとめているだけの記事です。 Promiseの説明を読んで普通に理解できる人は この記事を読むと、知能レベルの低すぎる文章によってストレスを感じる恐れがあります。 きっかけとなったPromiseの書き方 こちらの記事に書かれていたコードです。 How to use async functions with gapi, the Google Drive client, and the file picker和訳:gapi、Googleドライブクライアント、ファイルピッカーで非同期関数を使用する方法 ※ 「gapi」とは、GoogleAPIを<script>タグで読み込んだとき、グローバルに定義してくれる変数の名前です。(GoogleAPIを実行するために必要なものを詰め込んだオブジェクト) <script> const gapiPromise = new Promise((res, rej) => { window.gapiLoaded = res; window.gapiLoadError = rej; }); </script> <script async defer src="https://apis.google.com/js/api.js" onload="window.gapiLoaded()" onerror="window.gapiLoadError(event)" > この構造を使用すると、await gapiPromise;するだけで、gapiが読み込まれ、エラーを処理できる場所で適切にエラーが提供され、init関数を中心にアプリを再構築する必要がなくなります。 最初目にしたときは頭を抱えましたが、 「await gapiPromise;をすれば gapi というオブジェクトの生成を待つことができる」 というところから、onload="window.gapiLoaded()"という部分にPromiseの真髄が詰まっているような気がしました。 <script>のonload属性は、おそらく説明を読むまでもなく「外部スクリプトの読み込みが終わったときに実行される処理」だとおもうので、 window.gapiLoaded() という部分が理解がミソなのだろうと予測しました。 window.gapiLoaded()の理解 window.gapiLoaded()は、gapiPromiseの定義をする際に定義されている関数です。 これを叩くことで gapiPromise が完了状態になるということは、 gapiPromiseは、生成された時点では未完了状態、 gapiLoaded()を実行された時点で完了状態になる、ということです。 (いうまでもなく、きっとgapiLoadError()も同様の処理なのでしょう。) 改造してみる すこしずつ改造 window.○○○ としているのは、おそらく gapiLoaded() を new Promise のスコープの外に出してあげるための記述だと思うので、↓のように書き方を変えても動くハズ、と考えました。→ 無事動きました。 let gapiLoaded, gapiLoadError; const gapiPromise = new Promise((res, rej) => { gapiLoaded = res; gapiLoadError = rej; }); ということは、↑のコードは、下記のような動きをしているという事です。 1. gapiPromiseという変数には、状態が完了しているかどうかを確認できるPromiseインスタンスが代入される。 1. gapiLoadedという変数には、そのPromiseインスタンスを完了状態に持っていくための関数が代入される。 1. gapiLoadErrorという変数には、そのPromiseインスタンスをエラー状態に持っていくための関数が代入される。 promiseとかresとかrejとか、見慣れない単語で表現されると理解しづらいので、 日本語に翻訳してみました。 let 状態を完了させるための関数, 状態をエラーにするための関数; const 状態を監視するための変数 = new Promise((res, rej) => { 状態を完了させるための関数 = res; 状態をエラーにするための関数 = rej; }); 結果的に、new Promise( 関数 ) というコードを書くと、 引数に渡した関数の第一引数に 状態を完了させるための関数 が渡され、 引数に渡した関数の第二引数に 状態をエラーにするための関数 が渡される、ということがわかりました。 (多分どのサイトでもちゃんとそのように説明してあるのだけど、自分はこうやって紐解いてみるまでよくわからなかった。) 逆に言うと 状態を監視するための変数 の状態を完了させたいなら 状態を完了させるための関数 を実行すればいいし、 状態を監視するための変数 の状態をエラーにしたいなら 状態をエラーにするための関数 を実行すればいいということ。 Promiseがわかってきた感じがしました。 がっつり改造 ということは、本来Promiseを完了させたりエラーにさせたりの状態変化は、 new Promiseの処理の中で行うのが正しい形なんでしょう。 (そのうえで .then や .catch で処理を繋いでいく。) しかし、async/awaitのようにネストしないスタイルでの非同期処理に慣れている自分は、 それはとても使いづらいと感じました。 なので、ネストせずにPromiseの状態を動かせるように改造して遊んでみました。 応用1. 状態監視用の変数 と 完了にする関数 と エラーにする関数 これら3つをまとめて生成してくれる関数をつくってみた。 function generatePromiseSet(){ let onReady, onError; const promise = new Promise((res,rej)=>{ onReady = res; onError = rej; }); return { 状態監視用の変数: promise, 完了する関数: onReady, エラーにする関数: onError }; } 使ってみる ※ 「分割代入」の仕組みを利用しています。 const { 状態監視用の変数, 完了する関数, エラーにする関数 } = generatePromiseSet(); async function テスト(){ console.log('開始'); await 状態監視用の変数; console.log('完了!'); } テスト(); 結果: '開始'が出力されたあと、完了する関数()を実行するまで'完了!'は出力されなかった。 (完了する関数()を実行すると、'完了!'が出力された。) ぷち感動です。 ただ、グローバルに3つも状態監視に関する変数を定義するのは、なんだか汚い気がしたので、 オブジェクト指向的なかんじで、状態監視用の変数にすべてまとめることができないかも、試してみました。 応用2. 自分自身を完了・エラー状態に変更できる状態監視用の変数を生成してみた。 function generateControllablePromise(){ let onReady, onError; const promise = new Promise((res,rej)=>{ onReady = res; onError = rej; }); promise.完了する = onReady; promise.エラーにする = onError; return promise; } 使ってみる const 状態監視用の変数 = generateControllablePromise(); async function テスト(){ console.log('開始'); await 状態監視用の変数; console.log('完了!'); } テスト(); 結果: '開始'が出力されたあと、状態監視用の変数.完了する()を実行するまで'完了!'は出力されなかった。 (状態監視用の変数.完了する()を実行すると、'完了!'が出力された。) うん!いい感じです。 魔改造 どうせなら、使い回しが効くように、 状態を完了にもっていく関数の名前も、自由に設定できるようにしておきましょう。 function generateControllablePromise( 状態を完了にする関数の名前, 状態をエラーにする関数の名前 ){ let onReady, onError; const promise = new Promise((res,rej)=>{ onReady = res; onError = rej; }); promise[状態を完了にする関数の名前] = onReady; promise[状態をエラーにする関数の名前] = onError; return promise; } 使ってみる const 状態監視用の変数 = generateControllablePromise( '完了する', 'エラーにする' ); async function テスト(){ console.log('開始'); await 状態監視用の変数; console.log('完了!'); } テスト(); 結果: '開始'が出力されたあと、状態監視用の変数.完了する()を実行するまで'完了!'は出力されなかった。 (状態監視用の変数.完了する()を実行すると、'完了!'が出力された。) これで、使い回しがきくようになりました。 余は満足じゃ。 このコードを使いまわしてもらえるように、 日本語部分を英語になおして、 ready/error などの単語を resolve/reject (=公式と同じ言い回し)に直したうえで 記事のトップに掲載しました。 まとめ 任意のタイミングでPromiseを完了させることができるようになったので、 「状態が完了するまで待つ」という処理を柔軟につくれるようになりました。 実はいままで、そのような処理をつくりたいときは awaitできるsetTimeoutを1行で書く方法などを参考にして、 任意の状態になるまでwhileループでひたすら待つ、という鬼畜処理しか思いつかなかったのです。↓ const wait = async (ms)=>(new Promise(resolve => setTimeout(resolve, ms))); // awaitできるsetTimeout async function waitSomeStatus(){ while( !任意の状態かどうか ){ await wait(10); } } 今回Promiseの理解が深まったことで、 安心して「待つ」コードを書くことができるようになりました。 ほんとうに良かったです。 ただしちょっと疲れました。ひと眠りしようと思います………… ……… …… … ? 完
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

User-Agent クライアント ヒントを使用した Windows 11の判別

2021年12月時点では、ユーザーエージェント(UA)から、Windows10とWindows11を判別することが出来ませんでしたが、javascriptによってUser-Agent クライアント ヒントを使用してWindows11を判別できる記事を見つけたので、メモ。 script.js navigator.userAgentData.getHighEntropyValues(["platformVersion"]) .then(ua => { if (navigator.userAgentData.platform === "Windows") { const majorPlatformVersion = parseInt(ua.platformVersion.split('.')[0]); if (majorPlatformVersion >= 13) { console.log("Windows 11 or later"); } else if (majorPlatformVersion > 0) { console.log("Windows 10"); } else { console.log("Before Windows 10"); } } else { console.log("Not running on Windows"); } }); 【参考サイト】 https://docs.microsoft.com/ja-jp/microsoft-edge/web-platform/how-to-detect-win11
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firestoreをmapフィールドに存在する値で検索する

こんにちわ、CROSSER開発者です 今回はFirestoreにあるmapフィールドにある値で検索する方法を共有したいと思います どなたかの参考になれば嬉しいです Firestore Documentの例 例えばこんなドキュメントがあり、userAが2021/11/13よりも前のデータがあるmapのドキュメントのみを取得したい時にこの記事は参考になると思います。 user / user1 / like{'userA':2021/11/12, 'userB':2021/11/13,'userC':2022/1/1} user / user2 / like{'userA':2022/1/2} やり方 admin.firestore().collection('user').where('like.userA', <=, 2021/11/12(Timestamp型)).get(); これでuser1のドキュメントが取得できます 補足 userAを可変にしたい時はこれでいけます admin.firestore().collection('user').where(`like.${userName}`, <=, 2021/11/12(Timestamp型)).get();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Chrome拡張機能でTweet下書き機能を作ってみた

はじめに 皆さんは最近仕事以外で勉強していますか? 以前の自分は休日は勉強するとしても数時間程度。平日はほとんど勉強していませんでした。 しかし職場の面談で以下のようなお言葉をいただきました。 「今後どんな人生設計かにもよるが、結婚して子供ができたら今まで以上に学習時間を確保することは難しい。時間のあるうちに学習をしておくことが大切」 ごもっともだなと思いました。また、自分の場合は、色々ありエンジニアになるのに遠回りをしているためなおさらです。 そこで、この面談の日から毎日少しでも学習をすることを決め今日まで続けています。 今回は、学習に取り組む中で自分用のChrome拡張機能を作ってみたのでここに記録します。 開発したもの 今回開発したものは、Tweetの下書きができる拡張機能です。 開発するにあたり、大事なことの1つとして作り切ることだと思っています。 途中で飽きてしまって完成できなかったものもいくつかあるので、今回は最低限の機能のみとしました。 実装した機能は以下の通りです。 文字数のカウント処理(全角、半角、URL形式) ツイートのテンプレート保存機能 記入内容の途中保存機能 記入内容の復元機能 ストップウォッチ(拡張機能を閉じているときもカウント) 開発背景 毎日勉強をするにしても自分一人だと継続が難しいと感じたため、勉強熱心な友人を数人フォローしたツイッターのアカウントを作成し、そこで毎日の学習したことを呟くようにしました。 友人が勉強を頑張っているところをみると、自分も頑張ろうと思い今のところ毎日勉強は続いています。 そんな中、毎日勉強したことをTweetする際に、どのサイトを読んだか、参考にしたかなど忘れてしまうことが多々ありました。 また、特に勉強時間を計測したりもしていなかったのでどれくらい勉強したかも記録できていませんでした。 そこで、参考サイトなどをすぐに記録でき学習時間も計測できるような自分用のChrome拡張機能があったらいいなと思い開発しました。 仕様を決める とりあえず、先ほど記述したように以下の機能を実装することを決めました。 文字数のカウント処理(全角、半角、URL形式) ツイートのテンプレート保存機能 記入内容の途中保存機能 記入内容の復元機能 ストップウォッチ(拡張機能を閉じているときもカウント) Twitterの現状の仕様としては、以下のような仕様らしいです 半角文字換算で280文字まで 全角文字は2文字換算 URL形式は23文字の固定 そのため、上記ツイッターの仕様を考慮して実装しました。 テンプレート等の保存場所に関してはStorage APIでlocalに保存。 ストップウォッチに関しては、backgroundで処理を行うことで拡張機能を閉じてもカウントを継続するようにします。 参考 Chrome拡張機能について知る とはいえ、Chrome拡張機能を作ったこともなく、どのようにやるのか全くわからないのでざっくりとした概念に関しては以下の記事を参考にしました。 今回は特定のWebページを対象としないため「Brower Action」でボタンを押したらツイートの下書き用ポップアップが表示されるようにします。 機能紹介 ストップウォッチ 上部にある数値がストップウォッチのカウント部分。学習時間を記録するために使用。 拡張機能を閉じてもカウントするようbackground側で動かしている(ただ止め忘れると、ずっと動いてしまうため現状は5時間で自動的に止まるようにしている) 文字数のカウント 文字数は、半角を1文字、全角を2文字としURL形式のものは一律で23文字とするようになっている。 また、規定の280文字を超えるとカウント部分が赤色になるようにしている。 各種ボタン ①ストップウォッチのスタートボタン ②ストップウォッチのストップボタン ③ストップウォッチのリセットボタン ④テンプレートの貼り付けボタン ⑤テンプレートの保存ボタン ⑥入力欄の復元ボタン テンプレートに関しては1つのみ保存できるようにしている。 基本的に毎日ツイートする型が決まっているのでそれを保存している。 復元機能に関しては、テキストエリアを離れた際にテキストエリアの内容をlocalstorageに保存するようにしている。当初はこの復元ボタンで保存した内容を反映させていたが、最終的にはchrome拡張起動時に自動で復元するようにした。 ※復元機能のボタン画像が機能とあっていない気もしますが直すのが面倒なのでこのままで デザイン あまり力を入れていないので、かなりチープですがiPhoneをイメージして作ってみました。 まとめ 今回は、勉強の一環でChrome拡張機能を作ってみました。 意外と楽しかったので、今度は他の人にも使ってもらえるような拡張機能を作ってみたいと思います。 また、学習は引き続き継続していきたいと思います。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react hooks useState 実用的なオブジェクト配列管理

カスタムHooks useState でオブジェクト配列を扱う場合のテンプレート的なものです。 typescriptも便利な機能を十分に活用し簡潔で機能的な記述を行います。 オブジェクト配列を扱う場合には、1つのアイテムを追加したり、削除の必要が多いかと思います。 そのような機能を簡潔に実装し、汎用的に使える想定のカスタムステートです。 useItems.ts import { useState, useCallback } from 'react' type ItemBase = { id: string } type FilterOptions = { not: boolean } export const useCustomState = <T extends ItemBase>(defaultItems: T[] = []) => { const [items, setItems] = useState<T[]>(defaultItems) // Filter read const filter = useCallback( (key: keyof T, value: any, options?: FilterOptions) => { if (options?.not) { return items.filter((item) => item[key] !== value) } return items.filter((item) => item[key] === value) }, [items], ) // Create const add = (item: T) => { setItems((prev) => [...prev, item]) } // Update const update = (id: string, updateParams: Omit<Partial<T>, 'id'>) => { setItems((prev) => { const anotherItems = prev.filter((item) => item.id !== id) const currentItem = prev.find((item) => item.id === id) if (!currentItem) return prev return [ ...anotherItems, { ...currentItem, ...updateParams, }, ] }) } // Remove const remove = (id: string) => { setItems((prev) => prev.filter((item) => item.id !== id)) } return { items, filter, add, update, remove } } Stateオブジェクトの型を定義できるようにする type ItemBase = { id: string } export const useCustomState = <T extends ItemBase>(defaultItems: T[] = []) => {} typescript のジェネリックを使い、管理するオブジェクトの入力補完やコンパイルエラーが発生するようにします。 プロパティ "id" は必須にする T extends ItemBase とすることで、利用するオブジェクトにはIDを必須にします。これにより各メソッドの処理を保証します。 簡単なフィルターを実装する type FilterOptions = { not?: boolean } // Filter read const filter = useCallback( (key: keyof T, value: any, options?: FilterOptions) => { if (options?.not) { return items.filter((item) => item[key] !== value) } return items.filter((item) => item[key] === value) }, [items], ) 簡単なフィルタリングを用意。 keyof T とすることで、指定キーがオブジェクト内に存在することを保証しつつ、入力補完を効かせます。 アップデートではidの上書きを防ぐ // Update const update = (id: string, updateParams: Omit<Partial<T>, 'id'>) => { setItems((prev) => { const anotherItems = prev.filter((item) => item.id !== id) const currentItem = prev.find((item) => item.id === id) if (!currentItem) return prev return [ ...anotherItems, { ...currentItem, ...updateParams, }, ] }) } 1つのアイテムのみを更新する場合は、スプレッド演算子によるマージを行います。 その最、 Omit<Partial<T>, 'id'>によって更新データには id を含めてはいけないようにすることで、idの上書きを防ぐことができます。 Omit: 指定のプロパティを取り除く(今回は id を取り除く) Partial: プロパティを必須から外す これで、typescriptの機能を有効に使い、スッキリとオブジェクト配列管理のuseStateを書くことができました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

OSSのTypeScriptのソースでよくみかけるDictについて

Dict型 export type Dict<T = any> = Record<string, T>; OSSのソースなどを読んでるとこんな感じでDict型を指定してる事があります Dict型の目的 let obj={} obj.name="Alex" //Property 'name' does not exist on type '{}' これはjavascriptだとエラー起きないのですが、typescriptだとエラーが起きるので、 let obj:{[key:string]:string}={} obj.name="Alex" にする必要があります。ここでRecordを使うとより簡潔に let obj:Record<string,string>={} obj.name="Alex" とする事ができます。それを更に簡潔にしたのがDict型という感じですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Quaggajsでバーコードリーダー作ったらカメラ周りでハマった話

在庫管理に利用されているバーコード(JANコード)ってご存知ですよね?Quaggajsというライブラリでそのリーダーを簡単に実装できるらしいです。実際にスマホのWebブラウザで動くアプリを作ってみてハマったポイントをメモとして残しておきます。ライブラリの詳細は上記リンク先を参照ください。 とりあえず作ってみる とりあえずインストール。 $ npm install quagga LiveStreamで起動する場合、次のコードで起動できます。 import Quagga from "quagga"; // LiveStreamを表示するvideoDOMを挿入したい親DOM const camera = document.getElementById("camera"); // QuaggaJSをLiveStreamで起動する start(camera); function start(camera) { // QuaggaJs初期化のConfig const config = { inputStream: { type: "LiveStream", target: camera, constraints: { // MediaStreamConstraintsを参照 audio: false, video: { facingMode: "environment", }, }, }, decoder: { // バーコードのデコーダー readers: ["ean_reader"], // EANコードはJANコードの海外での名称 }, }; // QuaggaJSを起動 Quagga.init( config, // カメラの起動後に実行 async (err) => { if (err) { console.log(err); } else { // バーコード読み取りをスタート Quagga.start(); } } ); // コード読み取り完了時の処理 Quagga.onDetected((result) => { // 読み取れたコードをコンソールに表示 console.log(result.codeResult.code); // カメラを停止 Quagga.stop(); }); } このコードでWindows, Mac, iPhoneなどでは動きました。(localhost以外の場合はHTTPS通信に注意!) 少し古いライブラリなのでちょっとドキュメント読んだりするのに手間取るかもしれませんが、概ねこんな感じで実装できます。 ハマったポイント HTTPSじゃないと動かない ローカル開発しているときにこれでハマりました。スマホで起動したい時には、HTTPS通信じゃないとそもそもカメラが起動できないのを失念してました。https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia マルチカメラのレンズ選択 こいつが曲者で、一部のAndroidスマホではデフォルトのレンズが望遠レンズになるらしく、カメラ起動の際にMediaDeviceを選んでやる必要があります。QuaggaJsではカメラ選択はサポートされてないっぽい(やり方あったら教えてください!)ので、カメラは自分で起動する必要があります。この場合は次のコードで起動できます。 import Quagga from "quagga"; // LiveStreamを表示するvideoDOMを挿入したい親DOM const camera = document.getElementById("camera"); const video = document.createElement("video"); video.autoplay = true; video.playsInline = true; camera.appendChild(video); // カメラを起動しスナップショットをQuaggaJSで読み取る multiCameraStart(camera, video); async function multiCameraStart(camera, video) { // 使用したいカメラを選択 const devices = await navigator.mediaDevices.enumerateDevices(); // カメラを選ぶ。Androidでは"camera2 0"のラベルがついているものが標準らしい。 const videoDevices = devices.filter( (v) => v.kind == "videoinput" && v.label.indexOf("camera2 0, facing back") >= 0 ); if (videoDevices.length == 0) { // 該当のカメラがないならQuaggaJsをLivestreamで起動 start(camera); } else { // 該当のカメラを起動 const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { deviceId: videoDevices[0].deviceId, }, }); // videoにstreamを入力 video.srcObject = stream; // ビデオトラックを取得 const track = stream.getVideoTracks()[0]; // 読み取り用canvasの作成 const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const trackSettings = track.getSettings(); canvas.width = trackSettings.width; canvas.height = trackSettings.height; // 0.3秒ごとにcanvasにスナップショットをとり、QuaggaJsでバーコードを読み取る let interval = window.setInterval(() => { // canvasにスナップショットを作成 ctx.drawImage(video, 0, 0, trackSettings.width, trackSettings.height); // canvasの画像をblobに変換 canvas.toBlob( (blob) => { if (blob) { // blobをurlに変換 const reader = new FileReader(); reader.readAsDataURL(blob); reader.onload = () => { // quaggaを起動 const config = { decoder: { readers: ["ean_reader"], multiple: false, // 精度向上のため複数入力は解除 }, locate: true, // 精度向上のためlocateオプションを設定 src: reader.result, singleChannel: false, }; // QuaggaJsを画像読み取りで起動 Quagga.decodeSingle(config); }; } }, "image/jpeg", 1 ); }, 300); // 読み取りに成功した場合の設定 let code; let count = 0; Quagga.onDetected((result) => { // 3連続で同じコードを得られるまで検知 if (code == result.codeResult.code) { count++; } else { // 前回のコードと違った場合 count = 0; code = result.codeResult.code; } // 3連続で同じコードを得られた場合はカメラを停止 if (count >= 3) { // コンソールにコードを表示 console.log(code); // 画像の読み取りを停止 window.clearInterval(interval); // カメラを停止 const tracks = stream.getVideoTracks(); for (let i = 0; i < tracks.length; i++) { tracks[i].stop(); } } }); } } 順番に解説していきます。 カメラデバイスの起動 // 使用したいカメラを選択 const devices = await navigator.mediaDevices.enumerateDevices(); // カメラを選ぶ。Androidでは"camera2 0"のラベルがついているものが標準らしい。 const videoDevices = devices.filter( (v) => v.kind == "videoinput" && v.label.indexOf("camera2 0, facing back") >= 0 ); mediaDevices.enumerateDevicesでメディアデバイスを取得し、その中から適切なデバイスを選んでいます。Androidで調べてみるとラベルにcamera2 0が含まれているものが標準的なレンズみたいだったので、これを選んで起動しています。devicesのカメラデバイスをそれぞれ一度起動してみるのが良いかもしれません。なお、ブラウザのカメラ使用を許可していないとdevicesのラベルが正確に読みとられないので、事前にカメラ使用を許可させる必要があることにも注意してください! // 該当のカメラを起動 const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { deviceId: videoDevices[0].deviceId, }, }); // videoにstreamを入力 video.srcObject = stream; 先ほど選んだカメラデバイスのデバイスIDを設定して起動します。なお、全体のコードでは該当のビデオデバイスが取得できなかった場合は仕方なくQuaggaJsをLiveStreamで起動しています。 スナップショットを作成しBlobURLに変換してQuaggaJsでデコード // ビデオトラックを取得 const track = stream.getVideoTracks()[0]; // 読み取り用canvasの作成 const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const trackSettings = track.getSettings(); canvas.width = trackSettings.width; canvas.height = trackSettings.height; ここでは画像を描画するcanvas要素を作成しています。 // 0.3秒ごとにcanvasにスナップショットをとり、QuaggaJsでバーコードを読み取る let interval = window.setInterval(() => { // canvasにスナップショットを作成 ctx.drawImage(video, 0, 0, trackSettings.width, trackSettings.height); // canvasの画像をblobに変換 canvas.toBlob( (blob) => { if (blob) { // blobをurlに変換 const reader = new FileReader(); reader.readAsDataURL(blob); reader.onload = () => { // quaggaを起動 const config = { decoder: { readers: ["ean_reader"], multiple: false, // 精度向上のため複数入力は解除 }, locate: true, // 精度向上のためlocateオプションを設定 src: reader.result, singleChannel: false, }; // QuaggaJsを画像読み取りで起動 Quagga.decodeSingle(config); }; } }, "image/jpeg", 1 ); }, 300); ここでは適当なインターバルで先ほどのcanvas要素にvideo要素のスナップショットを描画しています。さらに、描画した画像データをblobに変換してQuaggaJsに渡し、画像解析してデコードしてもらいます。 デコードが完了した場合の処理 // 読み取りに成功した場合の設定 let code; let count = 0; Quagga.onDetected((result) => { // 3連続で同じコードを得られるまで検知 if (code == result.codeResult.code) { count++; } else { // 前回のコードと違った場合 count = 0; code = result.codeResult.code; } // 3連続で同じコードを得られた場合はカメラを停止 if (count >= 3) { // コンソールにコードを表示 console.log(code); // 画像の読み取りを停止 window.clearInterval(interval); // カメラを停止 const tracks = stream.getVideoTracks(); for (let i = 0; i < tracks.length; i++) { tracks[i].stop(); } } }); } 読み取り精度に若干問題があったので、ここでは3連続で同じコードが得られた場合のみ成功としています。カメラを終了する際には、streamを直接触ってカメラを停止しています。 まとめ ライブラリは便利ですが、mozillaなどのドキュメントを正しく理解するのが大切だと改めて気付かされました! サンプルアプリ サンプルアプリを作ってみたので、よかったら使ってみてください。バーコードリーダーサンプルアプリ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsで画像を配置する方法

Next.jsで画像を配置する方法 最近学んだことをoutputするために書きました。 やり方 ①まずpublicフォルダにstatic fileを作り、その中に画像をぶち込みます。 ②その次に画像を配置したいフォルダー内に行き、Imageタグで画像を表示していきます。この時、publicは"/"で表示できるので気をつけましょう。 <Image src="/static/logo.png" alt="logo" width="150px" height="50px" /> 以上で表示できます!以上グッピーでした! 参考にした記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】Twitterシェア ツイートの中身の指定(2週間後の日付、本のタイトル)

はじめに こんにちは! 今回は【Vue.js】Twitterシェアボタンの実装についてアウトプットしていきます! 前回の記事でTwitterシェアの実装を行いました!今回はツイートのコメント部分に、 ①2週間後の日付 ②診断結果ででた本のタイトル を自動的に記入される機能を実装していきます! 対象 ・vue.jsでTwitterシェアを実装したい方 ・vue.jsで日付を取得し表示させたい方 ・前回の記事までの内容を実装済みの方。 使用環境 使用技術 バージョン nuxt.js 2.15.7 @nuxtjs/tailwindcss 4.2.0 moment 2.29.1 nuxt-microcms-module 2.0.0 使用ファイル,概要 ファイル名 概要 pages/result.vue 診断結果表示ページ 実装 pages/result.vue <template> <div class="twitter_share"> <button @click="twitterShare" class=" border-2 border-blue-600 rounded-full h-14 w-64 items-center flex justify-center m-auto my-8 hover:bg-blue-200 duration-1000 " > Twitterで宣言する! </button> </div> </template> <script> import moment from "moment"; export default { async asyncData({ query, $microcms }) { const id = query.id; console.log(id); const book = await $microcms.get({ endpoint: "books", contentId: id, }); console.log(book); return { book, }; }, data() { return { book: "", }; }, methods: { twitterShare() { const today = new Date(); //今日この瞬間の情報を取得 const date_today = today.getDate(); //日 const after2Week = today.setDate(date_today + 14); //今日の日+14日 const formatDate = moment(after2Week).format("MM月DD日"); //2週間後の日にちを"MM月DD日"で表示 console.log(formatDate); //シェアする画面を設定 var shareURL = "https://twitter.com/intent/tweet?text=" + `${formatDate}までに「${this.book.title}」を読み、感想をツイートします!` + "%20%23NewSelf" + "%20%23書籍診断アプリ" + "&url=" + "https://www.google.com/?hl=ja"; //アプリURL //シェアようの画面へ移行 location.href = shareURL; }, }, }; </script> ・microCMSのAPI取得の処理の部分の解説は省かせていただきます。 ①2週間後の日付の実装 pages/result.vue <script> import moment from "moment"; export default { data() { return { moment: ``, }; }, filters: { moment: function (date) { return moment(date).format("MM月DD日"); }, }, methods: { twitterShare() { const today = new Date(); //今日この瞬間の情報を取得 const date_today = today.getDate(); //日 const after2Week = today.setDate(date_today + 14); //今日の日+14日 const formatDate = moment(after2Week).format("MM月DD日"); //2週間後の日にちを"MM月DD日"で表示 console.log(formatDate); //シェアする画面を設定 var shareURL = "https://twitter.com/intent/tweet?text=" + `${formatDate}までに「${this.book.title}」を読み、感想をツイートします!` + "%20%23NewSelf" + "%20%23書籍診断アプリ" + "&url=" + "https://www.google.com/?hl=ja"; //アプリURL //シェアようの画面へ移行 location.href = shareURL; }, }, }; </script> 1.momentのインストール 以下をターミナルに記述しmomentをインストールします。 npm install vue-moment インストールが完了したら、package.jsonに このように記載されていたらOKです! 2.日付を取得?2週間後の日付にカスタマイズ pages/result.vue <script> import moment from "moment"; export default { methods: { twitterShare() { const today = new Date(); //new Date()で今日この瞬間の情報を取得し、定数todayへ代入 const date_today = today.getDate(); //today.getDate()で今日の日にちのみを取得し定数date_todayへ代入 const after2Week = today.setDate(date_today + 14); //today.setDate(date_today + 14)で今日の日+14日に内容変更し、定数after2Weekへ代入 const formatDate = moment(after2Week).format("MM月DD日"); //2週間後の日にちを"MM月DD日"で表示される処理を定数formatDateに代入 console.log(formatDate); //シェアする画面を設定 var shareURL = "https://twitter.com/intent/tweet?text=" + `${formatDate}までに「${this.book.title}」を読み、感想をツイートします!` + //定数formatDateを${}で囲みツイートコメント部分に記載することで実装完了! "%20%23NewSelf" + "%20%23書籍診断アプリ" + "&url=" + "https://www.google.com/?hl=ja"; location.href = shareURL; }, }, </script> 上記の処理でOKです! ②診断結果ででた本のタイトルの表示 pages/result.vue <script> export default { async asyncData({ query, $microcms }) { const id = query.id; console.log(id); const book = await $microcms.get({ endpoint: "books", contentId: id, }); console.log(book); return { book, }; }, data() { return { book: "", }; }, //?での処理を簡単に解説すると、endpointとcontentIdを定数bookに返している。つまりmicroCMSにの登録情報がbookに入っている。なのであとはTwitterシェアのコメント部分に呼び出せばOK! methods: { twitterShare() { var shareURL = "https://twitter.com/intent/tweet?text=" + `${formatDate}までに「${this.book.title}」を読み、感想をツイートします!` + //?${this.book.title}で呼び出せば実装完了! "%20%23NewSelf" + "%20%23書籍診断アプリ" + "&url=" + "https://www.google.com/?hl=ja"; location.href = shareURL; }, }, </script> ・microCMSの処理に関してはこちらの記事に簡単な基礎がまとめてありますので参考にしてみてください。 動作確認 診断を終え、「Twitterで宣言する!」ボタンをクリック 2週間後の日付もタイトル名も無事実装できました!! まとめ 今回はTwitterシェア ツイートの中身の指定(2週間後の日付、本のタイトル)について記事にしました。 よりユーザーが使いやすく、手間のかからないアプリにするにはどうすればいいかを考えていきながら、今後も制作していきたいと思います!よりユーザー目線の思考を磨いていきます! ぜひ参考にしてみていただけるととても嬉しいです!! 今回の記事を読んで質問、間違い等あればコメント等頂けると幸いです。 最後までご愛読ありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】条件分岐と繰り返し処理

はじめに  本記事は、プログラミング初学者が、学習を進めていて疑問に思った点等について調べた結果を備忘録も兼ねてまとめたものです。  そのため、記事の内容に誤りが含まれている可能性があります。ご容赦ください。  間違いを見つけた方は、お手数ですが、ご指摘いただけますと幸いです。 JavaScriptの条件分岐と繰り返し処理 if() // 条件式が1つ if(条件1){ 処理1 } // 条件1を満たすときは、処理1を実行 // 条件式が1つで処理を2つに分ける if(条件1){ 処理1 } else{ 処理2 } // 条件1を満たすときは処理1、満たさないときは処理2を実行 // 条件式が2つで処理を分ける if(条件1){ 処理1 } else if(条件2){ 処理2 } else{ 処理3 } // 条件1を満たすときは処理1、条件2を満たすときは処理2、どちらも満たさないときは処理3を実行 while(),do...while(),for() // while() while(条件){ 処理 } // 引数の条件がtrueの間、波括弧内部の処理が繰り返される // do...while() do{ 最低1回は行われる処理 }while(条件式); // true,falseに関わらず最低1回は処理が実行される // for() for ( 初期値; 条件式; 増減値 ){ 繰り返し処理 } // 回数を決めて繰り返す際に使用
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTMLの<picture>要素をIE11でも使えるように見せかける簡易版JSを作った

IE11も対象の案件。ブラウザの画面幅に応じてページのレイアウトが変わる際、一部の<img>要素の画像については縦横比が異なるものを表示させる必要がありました。そこで、サイズや縦横比が異なる複数の画像をあらかじめ用意しておき、画面幅に応じて自動で切り替わるようにしたい。最近のブラウザならHTMLの<picture>要素をサポートしているので、mediaクエリを付けた<source>要素を並べて書くだけで実現できますが、IE11は<picture>要素が未対応。もちろん、polyfill picturefill.jsの存在は知っています。が、今回はとりあえずmax-widthだけを基準にして画像を切り替えできれば十分だったので、自分でIE11用に簡易版のコードをJavaScriptで書いてみることにしました1。 ※コンテントの画像 via Unsplash 簡易版の仕様 簡易版jsが扱うことができるシンタックスは以下のとおりです。 <source>要素にはsrcset属性とmedia属性のみ指定します。 media属性にはmax-width1つのみ、px単位で指定します。 <source>要素はmax-widthの値が小さい順に並べなければなりません。 example.html <picture> <source srcset="/path/to/img1" media="(max-width: 360px)"> <source srcset=/path/to/img2" media="(max-width: 420px)"> ..... <source srcset="/path/to/imgN" media="(max-width: 768px)"> <img src="/path/to/fallback/img" alt=""> </picture> See the Pen [JS] picture element in IE11 by Kazuhiro Hashimoto (@kaz_hashimoto) on CodePen. このCodePenはIE11以外でも動作を確認できるように2、HTMLネイティブの機能の代わりに簡易版jsを使って<picture>要素の画像を切り替え表示しています。実際の場面では、IE11以外には不要なコードなので、最初にブラウザのUA文字列によってIE11かどうかを判定し、IE11でなければ即returnさせます。 javascript document.addEventListener('DOMContentLoaded', function() { // 実際に使用する時は最初にUAでブラウザ判定を行い、 // IE以外のブラウザでは以降のコードが実行されないようにする。 // 例) // const parser = new UAParser(); // const ua = parser.getResult(); // if (ua.browser.name != 'IE') { // return; // } ..... 実装メモ 処理の本体はIE11でも実行可能なようにコーディングする必要があるため、NodeListの各要素に対してcallback関数を呼ぶ箇所は、一旦Arrayを作ってからforEachを呼ぶ形式で書いてあります。 javascript const targets = document.querySelectorAll('picture'); Array.prototype.forEach.call(targets, function(picture) { ... <source>要素に設定した一連のmedia="(max-width:は、区間が重ならないように "(min-width: A px) and (max-width: B px)" に変換したクエリでMediaQueryListオブジェクトを作成し、リスナーを設定します。 クエリとsrcsetの値のペアは、ハンドラ内からも参照するためMapオブジェクトで保持しておきます。ここで、Mapのキーに使うクエリ文字列は、matchMediaの戻り値から取り出した方のmql.mediaを使います。IE11の場合、それはmatchMedia(query)に指定したquery文字列と必ずしも一致しないためです。 javascript const mql = window.matchMedia(query); item.map.set(mql.media, source.getAttribute('srcset')); const func = handler.bind(item) mql.addListener(func); func(mql); 変数itemは<picture>要素ごとに設定情報を保持するオブジェクトです。ハンドラー内でitemをthisキーワードから参照可能にするため、bind()を呼んで生成した関数の方をリスナーに設定します。 javascript function handler(mql) { if (!mql.matches) { return; } const item = this; const src = item.map.get(mql.media); if (!src) { return; } item.img.src = src; } ハンドラーが呼ばれると、対象の<picture>要素に紐付けたitemを取り出し、ステータスが変化したmediaクエリをキーにsrcsetの値を取り出し、<img>要素のsrcに画像をセットします。 picturefill.jsは2017年3月リリースのver.3.0.3を最後に更新が止まっているようで、projectのstatusがよくわからなかったのも1つの理由。 ↩ CodePenはIE11をサポートしないため、このPenをIE11で確認したい時はPenのソース一式をExportし、localhost上のコンテンツとしてIE11からアクセスします。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AntDesignのAvatarを自作してみた

1. 自作した理由 通っていたスクールで依存度が高いAntDesignからの脱却を目指していたため、その流れで自分もチャレンジしてみようと感じた。 2. 開発環境 react v16.12.0 styled-components v5.3.3 typescript v4.1.4 3. 実装した機能 まずは、AntDesignのAvatarに搭載されている機能を紹介します。 正直使わない機能が多かったため、自作した際には最低限使いそうなsrc size border shapeのみを実装しました。 4. 実際のコード import React from 'react'; import styled, { css } from 'styled-components'; type Props = { src?: string | null; size?: number | 'large' | 'small' | 'default'; border?: boolean; shape?: 'circle' | 'square'; }; const AvatarStyle = styled.div<Props>` ${props => { if (!props.size) { return css` width: 40px; height: 40px; border-radius: 50%; `; } else { if (props.size === 'default') { return css` width: 40px; height: 40px; border-radius: 50%; `; } else if (props.size === 'small') { return css` width: 32px; height: 32px; border-radius: 50%; `; } else if (props.size === 'large') { return css` width: 64px; height: 64px; border-radius: 50%; `; } else { return css` width: ${props.size}px; height: ${props.size}px; border-radius: 50%; `; } } }} border: ${props => props.border && '1px solid blue'}; background-color: gray; border-radius: ${props => props.shape === 'square' && '0%'}; background-size: cover; background-repeat: no-repeat; background-position: center; background-image: url(${props => props.src}); `; const AtomAvatar: React.FC<Props> = ({ ...props }) => { return <AvatarStyle {...props} />; }; export default AtomAvatar; 5. 苦戦した点 sizeを設定する際にdefault、small、largeと数字指定の両立に苦戦した。 そこでdefaultなどの文字列が渡ってきた際には指定のピクセル数を指定し、それ以外の場合は入力した数字を渡し、Ant Designを忠実に再現した。 6. 未実装だが必要だと感じる機能 画像をユーザー自身で変更できる機能 現状は開発者側でsrcを指定すれば任意の画像は設定可能だが、ユーザー側では変えることはできない。 この機能に関してアドバイスなどありましたら、コメントとかで教えていただきたいです 7. 後書き アウトプットの目的で初めて書いたが、改めて書いた意図を理解することができた。 正直スクールの講師の方にアドバイスをいただいた部分が多いため、他のAnt Designも自作してみようかと考えている。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む