20220223のJavaScriptに関する記事は30件です。

「マイクロサービスパターン」の復習 6章

概要 Java読書会でせっかく勉強したのにつぎつぎと忘れていくので、印象に残ったところを記録していく 6章 イベントソーシングを使ったビジネスロジックの開発 モデリングした結果を反映したテーブルを作るのではなく、エンティティに対する生成・更新イベントをDBに記録していくという話 https://microservices.io/patterns/data/event-sourcing.html 従来のアプローチ やり方 なんらかモデリング 対応するクラスを作る 対応するテーブル作る ORマッパーでテーブルとクラスをつなぐ 欠点 履歴が残らない 後付で履歴つくるのは大変 生成・更新イベントも後付 イベントソーシング やり方 モデリングはする 対応するクラスは作る でも永続化するのは、生成・更新イベントのみ(永続化用のテーブルをイベントストアと呼ぶ) デシリアライズは、空オブジェクトにイベント履歴を繰り返し適用する 場合によっては、スナップショットテーブルを用意して、高速化する イベントの発行は、イベントテーブルのトランザクションログを見るなり、ポーリングするなりして発行する 利点 履歴ファーストなので、当然すべての履歴が残る イベント発行もネイティブに対応 欠点 イベントや対象となるオブジェクトのスキーマに追随するのが大変 従来手法とあまりに違いすぎるので習得難易度が高すぎる 履歴が残る分、削除が苦手 履歴しかないので、検索が困難⇒これは7章のCQRSで説明されている検索専用サービスを建てることで解決する 実現手段 基本的に作者の作成したEventuateフレームワークシリーズを使う Eventuate Local イベントストアの実装用ライブラリ イベントの永続化はMySQL、イベントのパブリッシュはKafkaベース スナップショット機能もあり Eventuage Client Eventuate Localを基盤として、サービスを実装するためのライブラリ Eventuate Tram Saga Sagaを実現するためのライブラリ Sagaは4章で説明されている。マイクロサービスだとサービスをまたがるところはトランザクションが効かないので、変わりにSagaと呼ばれるものを作る 感想 難しくてついていけません。 概要レベルなら分かるのですが。 Eventuateシリーズをよほど使いこなせないと実現できなさそう ピットフォールになる部分をよくしっているか トラブル時にどうデバッグするか? 6章、7章を合わせて読んでみると、マイクロサービスはイベントを発行するのが当たり前なのか? 世の中のマイクロサービス開発というのは本当にこの本に書いてあるようなことをやっているのか?それとも2、3種類程度のWebアプリがコンテナ化されて動いているだけでモノリシックアプリと大差ないのか?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Hardhatでpolygonテストネットワークへのデプロイ方法まとめ

概要 hardhatで作成したdappsをAlchemyを用いてPolygonテストネットワーク(Mumbai)にデプロイするまでのまとめ Hardhat Alchemy polygonscan 1. MetamaskにPolygonテストネットワークの設定をする 以下リンク先の通りに設定 2. テストネット用のトークンを取得する 以下リンク先で取得できる 3. Alchemyでアプリを作成する 以下リンク先で作成 アプリ作成時にHTTPのURLをコピーしておく 4. 作成したコントラクトをデプロイする コントラクト自体の作成は省略します 環境変数の設定 .envファイルを作成し、環境変数を設定する $ npm i --save-dev dotenv 以下を.envファイルへ記述 「3. Alchemyでアプリを作成する」 でコピーしたURL メタマスクアカウントの秘密鍵 メタマスクアカウントの秘密鍵取得方法 API_URL=https://polygon-mumbai.g.alchemy.com/v2/xxxxxxxxx PRIVATE_KEY=xxxxxxxxx hardhat.config.js require("dotenv").config(); //dotenvを読み込む require("@nomiclabs/hardhat-waffle"); /** * @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: "0.8.4", //mumbaiを追加 networks: { mumbai: { url: process.env.API_URL, accounts: [process.env.PRIVATE_KEY], }, }, }; コンパイル & デプロイ $ npx hardhat compile # デプロイスクリプトをdeploy.jsで記述している場合 $ npx hardhat run scripts/deploy.js --network mumbai # hardhat.config.jsのタスクで記述している場合 $ npx hardhat deploy --network mumbai デプロイ後にエラーなど出ていなければ成功 デプロイしたコントラクトは、alchemyのダッシュボードから確認できる 最後に 意外と簡単にデプロイはできますが、メタマスクの設定などが面倒でした><
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript関数ドリル】Lodash関数の実装【勉強用】_.join関数

【JavaScript関数ドリル】初級編の_.join関数の実装のアウトプット _.join関数の挙動 _.join(['a', 'b', 'c'], '~'); // => 'a~b~c' 第1引数の配列の要素の間に、第2引数を追加していく 第2引数がなければ初期値の','が追加される _.join関数の課題内容 _.join関数に取り組む前の状態 配列に任意の位置に順番に挿入するにはspliceを使えばよさそう 普段通りのfor(let i = 0; i < array.length; i++)でよさそう _.join関数に取り組んだ後の状態 かなり強引に実装している forループにspliceを用いてインクリメントした結果、当初ループの条件をi < array.lengthとしていたため無限ループとなってしまった 無限ループを改善できた後も、変わりゆく配列の要素数にiの2個飛ばしで無理やり対応させてしまった 変数名をnew○○とする癖があるので、解答のように配列のコピーであればcopiedArrayなど分かり易い変数名にしていくべきだと感じた 加算代入+=について理解していなかった _.join関数の実装コード const join = ((array, separator = ',') => { const newLength = array.length; const newArray = array.slice(); for (let i = 1; i < (newLength * 2) - 1; i = i + 2) { newArray.splice(i, 0, separator); } return newArray.toString().replace(/,/g, ''); }); const arrays = ['a', 'b', 'c', 'd'] console.log(join(arrays, '~')); // => 'a~b~c~d' _.join関数の解答コード function join(array, separator = ',') { const copiedArray = [...array]; let joinedString = copiedArray.shift(); for(let i = 0; i < copiedArray.length; i++) { joinedString += separator + copiedArray[i]; } return joinedString; } console.log( join(['a', 'b', 'c'], '---') ); // => 'a~b~c'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ワイ「Denoってなんぞや?」

?「やあ」 ワイ「前回はNode.jsについて知見を得たな」 ワイ「どんどん最強ぷよぐらまに近づいてまうで^^」 ?「やあ」 ワイ「急になんやこの恐竜」 ワイ「某J民か?」 ?「あんなのと一緒にしないでよ」 ?「僕はDenoだよ」 ワイ「誰だよ(ピネ」 ?「Node.jsの親戚みたいなものさ」 ワイ「Node.jsくんにも親戚がおったんか」 ワイ「初耳やわ(ハナホジ」 ?「Denoについて」 ?「Node.jsのついでに知ってもらおうと思って」 ?「今ここにいるんだ」 ワイ「ほーん しゃあない聞いたるわ」 ?「......」 ?「Deno(ディーノ) はRyan Dahlによって作られた比較的新しいJavascript/Typescriptランタイムだよ」 ワイ「誰やその人」 ?「Node.js を作った人だよ」 ワイ「なんやおんなじ人か」 ワイ「だから親戚なんやな」 ?「ちなみにDenoはNodeのアナグラムだよ」 ワイ「でもNode.jsがすでにあるのにわざわざ新しいのを作ったんや?」 ?「それはRyan DahlがNode.jsの開発において10個の後悔があって」 ?「その反省を活かして開発されたのがDenoなんだ」 ワイ「完璧主義やったんやろか」 ワイ「とりあえず使ってみるか」 ワイ「とりあえず使ってみるで」 ?「インストールはここに書いてるよ」 ワイ「ワイはWindowsを使ってるからPowerShellからインスコすればええんやな」 ワイ「楽な時代になったもんやで^^」 PowerShell iwr https://deno.land/install.ps1 -useb | iex ワイ「これやな」 ワイ「インスコ完了や」 ?「まずはサンプルを実行してみよう!」 PowerShell deno run https://deno.land/std/examples/welcome.ts ?「コマンドはこれだよ」 ワイ「オンライン上のファイルをそのまま実行できるんか便利やな」 ワイ「実行してみたで」 PowerShell Download https://deno.land/std/examples/welcome.ts Warning Implicitly using latest version (0.126.0) for https://deno.land/std/examples/welcome.ts Download https://deno.land/std@0.126.0/examples/welcome.ts Check https://deno.land/std/examples/welcome.ts Welcome to Deno! ?「無事成功だね」 ?「僕の特徴について」 ?「ここまでで早速Denoの特徴があったんだけどなにかわかる?」 ワイ「わからん(即答」 ?「......」 ?「正解はデフォルトでTypescriptに対応してる点だよ」 ワイ「確かに実行したファイルの拡張子は .tsになってるな」 ワイ「Node.jsでのTypescript開発やったらパッケージ必須やったもんな」 ?「まだまだあるよ」 ?「このコードを実行してみて」 test.ts import { serve } from "https://deno.land/std@0.126.0/http/server.ts"; console.log("http://localhost:8000/"); serve((req) => new Response("Hello World\n"), { port: 8000 }); PowerShell deno run .\test.ts PowerShell 結果 Download https://deno.land/std@0.126.0/http/server.ts Download https://deno.land/std@0.126.0/async/mod.ts Download https://deno.land/std@0.126.0/async/deadline.ts Download https://deno.land/std@0.126.0/async/debounce.ts Download https://deno.land/std@0.126.0/async/deferred.ts Download https://deno.land/std@0.126.0/async/delay.ts Download https://deno.land/std@0.126.0/async/mux_async_iterator.ts Download https://deno.land/std@0.126.0/async/pool.ts Download https://deno.land/std@0.126.0/async/tee.ts Check file:///C:/Users/kazum/Desktop/programings/typescript/dist.ts http://localhost:8000/ ⚠️ ️Deno requests net access to "0.0.0.0:8000". Run again with --allow-net to bypass this prompt. Allow? [y/n (y = yes allow, n = no deny)] y ワイ「なんか途中で許可を求めてきたな」 ?「それが特徴デフォルトでファイルやネットワークにアクセス出来ないだよ」 ワイ「まあセキュリティが高なってるって訳やな」 ワイ「ていうかimportもurlからになってるんやな」 ?「それも大きな特徴の一つnpm・requireの廃止だよ」 ?「わざわざnpm installをする必要がないんだ」 ワイ「それは便利な気がするなぁ」 ワイ「無事Hello Worldしてくれたな」 ワイ「まとめやで」 ワイ「また一つ賢くなってしまったわ^^」 ワイ「今後はDenoがNode.jsを追い越したりするんかいな」 ?「どうだろうね」 ワイ「いつか開発する機会があればDenoで開発しよかな」 ?「......(絶対やらないじゃん」
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SVG イメージを JavaScript から保存する方法

GitHub に Mermaid がサポートされるようになったので、マークダウン中にいい感じのグラフを描画できるようになりましたね。 しかし、 Mermaid で作成される画像は SVG 形式なので、ブラウザからだと「名前をつけて画像を保存」ができません。 この記事は、いい感じの図ができたから Slack で共有しようと思ったのに、保存できなくて焦った人など向けに作ったものです。 最後の方でブラウザの開発者ツールの話が出ますが、そこから HTML 文書をコピペするだけでいいのでは? と思いつく方は対象にしておりません。ごめんなさい。 TL;DR サンプルが以下です。 See the Pen Download SVG by Kitagawa (@aster_mnch) on CodePen. 保存処理だけを抜き出したのが以下です。 function downloadSvg(svgNode, filename) { const svgText = new XMLSerializer().serializeToString(svgNode); const svgBlob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' }); const svgUrl = URL.createObjectURL(svgBlob); const a = document.createElement('a'); a.href = svgUrl; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(svgUrl); } 説明 対象の SVG テキストを取得 const svgText = new XMLSerializer().serializeToString(svgNode); ファイル保存するために SVG テキストデータを取得する必要がありますが、単純に .toString() としても '[object SVGSVGElement]' しか返ってきません。 そのため XMLSerializer.serializeToString() を使って文字列として取得します。 保存用の Blob オブジェクトを作成 const svgBlob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' }); const svgUrl = URL.createObjectURL(svgBlob); // ... URL.revokeObjectURL(svgUrl); 先で取得した SVG テキストをファイル形式で保存するために Blob オブジェクトを作成します。 作成した Blob から URL.createObjectURL() でダウンロード用の URL を作成することができます。 この URL はスコープを抜けても解放されないので、不要になったら URL.revokeObjectURL() で解放しましょう。 a 要素を使ったダウンロード処理 const a = document.createElement('a'); a.href = svgUrl; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); 最後に、作成した Blob URL からファイルをダウンロードできるように a 要素を使います。 download 属性を使うことで、 href に指定した URL からファイルをダウンロードすることができます。 また、 Blob URL と同様に、 document.body.appendChild で追加したノードはドキュメントに紐づいているため、スコープを抜ける前に document.body.removeChild で削除しましょう。 おまけ 保存したい対象の SVG 要素を選択する場合は、一から作るか document.getElementById() や document.querySelector() を使う必要がありますが、ブラウザのコンソールから $0 を使うと要素で直前に選択したノードを使用することができます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クリップボードを共有するPWAアプリの作成

WindowsとAndroidでテキストを共有したいことがよくあり、最近はMacやiPhoneとも共有することが増えてきたので、クリップボードのテキストを共有するPWAアプリを作成します。 PWAにすれば、アプリっぽく使えるのと、後程示すショートカットが便利だったりします。 以下のGitHubにソースコードを上げておきます。 poruruba/ClipShare テキストの共有方法 単に、Node.jsサーバを立ち上げて、テキストを保持するようにし、HTTP Post呼び出して、Set/Getするようにしているだけです。 ソースコードを載せますが、大したことはやっていません。 一応、セキュリティを考慮して、APIKeyがあっていないと受け付けないようにするのと、1日間以上はテキストを保持しないようにしています。以下に示す変数APIKEYの部分です。 /api/controllers/clipshare-api/index.js 'use strict'; const HELPER_BASE = process.env.HELPER_BASE || "/opt/"; const Response = require(HELPER_BASE + 'response'); const APIKEY = "【任意のAPIKey】"; const EXPIRES_IN = 60 * 60 * 1000; var clip = null; exports.handler = async (event, context, callback) => { var body = JSON.parse(event.body); if( event.requestContext.apikeyAuth.apikey != APIKEY ) throw "apikey invalid"; switch(event.path){ case "/clipshare-get":{ if( !clip || new Date().getTime() > clip.created_at + EXPIRES_IN ) return new Response({status: "ng"}); return new Response({ status: "ok", clip: clip }); } case "/clipshare-set":{ clip = { text: body.text, created_at: new Date().getTime() }; return new Response({ status: "ok" } ); } } }; ※セキュリティ上の配慮はないので、自己責任でお願いします。 ※それから、HTTPSで通信するようにしないと、テキストの内容は漏洩します。 クライアント側 PWA化するために、マニフェストファイルを作成します。 /public/clipshare/manifest.json { "short_name": "クリップシェア", "name": "クリップシェア", "display": "standalone", "start_url": "index.html", "icons": [ { "src": "img/192x192.png", "sizes": "192x192" } ], "shortcuts": [ { "name": "Clip⇒Upload", "description": "クリップボードをアップロードします。", "url": "/clipshare/index.html?cmd=clip2upload", "icons": [ { "src": "img/192x192.png", "sizes": "192x192" } ] }, { "name": "Download⇒Clip", "description": "ダウンロードしてクリップボードにコピーします。", "url": "/clipshare/index.html?cmd=download2clip", "icons": [ { "src": "img/192x192.png", "sizes": "192x192" } ] } ] } 少し補足しますと、「shortcuts」という項目がありますが、これは、PWAとしてインストールした後に、右クリックまたは長押しで、ワンタッチでクリップボード上のテキストをサーバにアップしたり、サーバ上のテキストをクリップボードにコピーするためのショートカットの設定です。ホーム画面にショートカットとしてもおけるのがうれしい。 HTMLで指定します。 /public/clipshare/index.html <link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.webmanifest" /> <script async src="https://cdn.jsdelivr.net/npm/pwacompat" crossorigin="anonymous"></script> また、PWAのために、ServiceWorkerを立ち上げる必要があるため、以下のJSを作成します。 /public/clipshare/sw.js var CACHE_NAME = 'clipshare-pwa-caches'; self.addEventListener('fetch', function(event) { console.log('sw event: fetch called'); }); あとは、これを、ページロード後に読み出すようにします。 /public/clipshare/js/start.js if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js').then(async (registration) => { console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch((err) => { console.log('ServiceWorker registration failed: ', err); }); } PWAについては、以下を参考にしてください。  PWAを試してみよう クリップボードのコピー/ペースト 以下の通りです。 /public/clipshare/js/start.js clip_paste: async function(){ return navigator.clipboard.readText(); }, clip_copy: async function(text){ return navigator.clipboard.writeText(text); }, async/awaitなので注意です。 参考:Clipboard API  https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API クライアント側ソースコード Javascriptソースを以下に示します。 /public/clipshare/js/start.js 'use strict'; //const vConsole = new VConsole(); //window.datgui = new dat.GUI(); var vue_options = { el: "#top", mixins: [mixins_bootstrap], data: { dialog_input: {}, base_url: "", apikey: "", payload: "", message: "", }, computed: { }, methods: { clipshare_get: async function(){ try{ this.message = ""; var json = await do_post_with_apikey(this.base_url + "/clipshare-get", {}, this.apikey); if( json.clip ){ this.payload = json.clip.text; await this.clip_copy(this.payload); this.message = "DOWNLOAD created_at " + new Date(json.clip.created_at).toLocaleString( 'ja-JP', {} ); }else{ this.message = "NO Clip Data"; } }catch(error){ alert(error); } }, clipshare_set: async function(){ try{ this.message = ""; var params = { text: this.payload }; await do_post_with_apikey(this.base_url + "/clipshare-set", params, this.apikey); this.message = "UPLOAD at " + new Date().toLocaleString( 'ja-JP', {} ); }catch(error){ alert(error); } }, clipshare_paste: async function(){ this.payload = await this.clip_paste(); this.message = "clipboard PASTE"; }, clipshare_copy: async function(){ await this.clip_copy(this.payload); this.message = "clipboard COPY"; }, clipshare_clear: function(){ this.message = ""; this.payload = ""; }, set_apikey: function(){ this.dialog_input = { base_url: this.base_url || "", apikey: this.apikey || "" }; this.dialog_open('#apikey_dialog'); }, apkey_update: function(){ localStorage.setItem("base_url", this.dialog_input.base_url); localStorage.setItem("apikey", this.dialog_input.apikey); this.dialog_close('#apikey_dialog'); alert('設定しました。リロードしてください。'); } }, created: function(){ }, mounted: async function(){ proc_load(); if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js').then(async (registration) => { console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch((err) => { console.log('ServiceWorker registration failed: ', err); }); } this.base_url = localStorage.getItem('base_url'); this.apikey = localStorage.getItem('apikey'); if( this.apikey ){ switch(searchs.cmd){ case 'clip2upload':{ this.payload = await this.clip_paste(); await this.clipshare_set(); alert('クリップボードをアップロードしました。'); window.close(); break; } case 'download2clip':{ await this.clipshare_get() alert('ダウンロードしてクリップボードにコピーしました。'); window.close(); break; } default:{ this.clipshare_get(); break; } } }else{ setTimeout( () =>{ alert('API Keyを指定してください。'); }, 0); } } }; vue_add_data(vue_options, { progress_title: '' }); // for progress-dialog vue_add_global_components(components_bootstrap); vue_add_global_components(components_utils); /* add additional components */ window.vue = new Vue( vue_options ); function do_post_with_apikey(url, body, apikey) { const headers = new Headers({ "Content-Type": "application/json; charset=utf-8", "X-API-KEY": apikey }); return fetch(url, { method: 'POST', body: JSON.stringify(body), headers: headers }) .then((response) => { if (!response.ok) throw 'status is not 200'; return response.json(); // return response.text(); // return response.blob(); // return response.arrayBuffer(); }); } セットアップ 以下から、ZIPでダウンロードします。 その後、適当なフォルダで展開して、npm install; node app.js; でOKです。 HTTPSにするには、certフォルダを作ってそこにSSL証明書を置いてください。 クライアント側では最初に立ち上げたサーバのURLとAPIKeyを設定してください。 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

APIから気象情報を取得してグラフ化

はじめに OpenWeatherMapAPIから気象情報を取得してChart.jsを使ってグラフ化してみました。 例として、「現在の時刻から24h後までの東京都の気温の変化」を取得します。 環境 - Laravel6系(PHP) - OpenWeatherMapAPI - Chart.js(JavaScriptライブラリ) ソースコード例 ChartController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; class ChartController extends Controller { public function index() { // 天気API取得のための情報準備 $weatherConfig = array( 'appid' => '各自で取得', 'lat' => '26.231408', //沖縄の那覇市の緯度 'lon' => '127.685525', //沖縄の那覇市の経度 ); // 天気API取得とJSONデコード $weatherJson = file_get_contents('http://api.openweathermap.org/data/2.5/onecall?lat=' . $weatherConfig['lat'] . '&lon=' . $weatherConfig['lon'] . '&units=metric&lang=ja&APPID=' . $weatherConfig['appid']); $weatherData = json_decode($weatherJson, true); $hour = []; $temp = []; $dayLabels = []; $tempData = []; $weatherViewData = array(); // 1時間毎の天気の情報全て $weatherHours = $weatherData['hourly']; foreach($weatherHours as $weatherHour) { // 時間のラベル・気温のデータを取得 $dayLabels[] = date('m/d:H時', $weatherHour['dt']); $tempData[] = $weatherHour['temp']; $weatherViewData['dayLabels'] = $dayLabels; $weatherViewData['temp'] = $tempData; } // JSへ渡すためにエンコード $weatherViewData = json_encode($weatherViewData); return view('chart', ['weatherViewData' => $weatherViewData]); } } 解説 OpenWeatherMapAPIを取得 // 天気API取得のための情報準備 $weatherConfig = array( 'appid' => '各自で取得', 'lat' => '26.231408', //沖縄の那覇市の緯度 'lon' => '127.685525', //沖縄の那覇市の経度 ); // 天気API取得とJSONデコード $weatherJson = file_get_contents('http://api.openweathermap.org/data/2.5/onecall?lat=' . $weatherConfig['lat'] . '&lon=' . $weatherConfig['lon'] . '&units=metric&lang=ja&APPID=' . $weatherConfig['appid']); $weatherData = json_decode($weatherJson, true); APIのidの取得は公式HPで登録するだけでOK. https://openweathermap.org/ idの取得はこのブログが参考になる。 https://yuukiyg.hatenablog.jp/entry/2019/11/17/182410 緯度と経度は今回予め指定しているがフォームなどから取得しても良いし、APIのURLを変更すれば緯度・経度からではなく「東京都」のような都道府県名から取得することもできる。(割愛) JSONとして受け取るため、デコードが必要になる。 受け取ったJSONのデータを整形 // 1時間毎の天気の情報全て $weatherHours = $weatherData['hourly']; foreach($weatherHours as $weatherHour) { // 時間のラベル・気温のデータを取得 $dayLabels[] = date('m/d:H時', $weatherHour['dt']); $tempData[] = $weatherHour['temp']; $weatherViewData['dayLabels'] = $dayLabels; $weatherViewData['temp'] = $tempData; } // JSへ渡すためにエンコード $weatherViewData = json_encode($weatherViewData); JSONのデータをvar_dump()して頂くと分かるが、時間、気温、天候、天気を示すアイコンなど様々なものがJSONで取得できている。今回は一時間毎の気温を取得したいので、このような整形をしていて、最後にPHPからJavaScriptへ渡したいのでエンコードしている。 Chart.jsを使ったグラフの表示 表示部分に関わる部分のコードを抜粋↓ chart.blade.php <head> ...(割愛) <!-- chart.jsのCDN--> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.js"> </head> <body> <h1>気温グラフ</h1> <canvas id="chart"></canvas> <script> var weatherViewData = JSON.parse('<?php echo $weatherViewData; ?>'); // JSONデコード var ctx = document.getElementById("chart"); var chart = new Chart(ctx, { type: 'line', data: { labels: weatherViewData['dayLabels'], datasets: [ { label: '気温', data: weatherViewData['temp'], borderColor: 'rgba(255,0,0,1)', backgroundColor: 'rgba(0,0,0,0)' }, // { // * 複数のデータを追加可能 // }, ], }, options: { title: { display: true, text: '気温' }, scales: { yAxes: [{ ticks: { suggestedMax: 20, suggestedMin: 0, stepSize: 5, callback: function(value, index, values){ return value + '度' } } }] }, } }); </body> Chart.jsをCDNでネットワーク経由で利用 <head> ...(割愛) <!-- chart.jsのCDN--> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.js"> </head> 今回はCDNを使用。headタグの中にscriptを埋め込んでいる。 CDNを使う場合は、下記のURLからURLを取得できる。 https://cdnjs.com/libraries/Chart.js もちろんnpmでのインストールでもOK。 グラフ表示エリア <canvas id="chart"></canvas> HTML5から導入されたcanvasタグでグラフの表示エリアを指定。 Chart.jsの基本の使い方 var weatherViewData = JSON.parse('<?php echo $weatherViewData; ?>'); var ctx = document.getElementById("chart"); var chart = new Chart(ctx, { } PHPから渡ってきた$weatherViewDataをパースして、JavaScriptの変数weatherViewDataへ格納。 canvas要素のchartのidを指定して、Chartのライブラリを使っていく。 描画するグラフの値の設定 var chart = new Chart(ctx, { type: 'line', data: { labels: weatherViewData['dayLabels'], datasets: [ { label: '気温', data: weatherViewData['temp'], borderColor: 'rgba(255,0,0,1)', backgroundColor: 'rgba(0,0,0,0)' }, // { // * 複数のデータを追加可能 // }, ], }, options: { title: { display: true, text: '気温' }, scales: { yAxes: [{ ticks: { suggestedMax: 20, suggestedMin: 0, stepSize: 5, callback: function(value, index, values){ return value + '度' } } }] }, } 基本的にはtype, data, optionsを指定すればグラフは描けた。 dataの中でPHPから渡ってきた変数を使っている。 ・type => 今回は折れ線グラフを使用するために「line」を使用。 ・data => dataの中にlabelとdatasetを記入する。 ・options => オプションなので、グラフのタイトルや、Y軸のパラメータなどを指定する。 下記の記事が参考になる。 https://qiita.com/Haruka-Ogawa/items/59facd24f2a8bdb6d369#2-%E8%A8%98%E8%BF%B0%E5%BD%A2%E5%BC%8F Chart.jsでは他にも様々なグラフが書けるようなので挑戦してみたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【個人開発】嫁ブロックを突破するアプリをつくました。

はじめに こんにちわ!くのと申します! エンジニアの方ってガジェット好きな方多いですよね!! しかしながら、エンジニアにとってガジェットを購入するために突破しなければいけない壁っていくつかありますよね。。。 突然ですが、2022年に実施されたガジェット購入時における障壁ランキングの堂々の第4位は何かご存知でしょうか!!?? 。。。そうですね!!それは「嫁ブロック」問題ですね!! (すいません、これは僕の妄想アンケートの結果です。) というわけで、嫁ブロックを突破し、高スペックなマシンをゲットして、強強エンジニアを目指すクソアプリを開発してみました!! アプリ概要 スタートから、5秒以内にボタンを連打して嫁ブロックを突破するアプリです。 *連打回数が少ないと嫁ブロックは突破できないので、注意してくださいね。 嫁ブロックを突破するゲーム *スマホ向けにデザインしていますので、スマホの方が遊びやすくなっております。 使用技術 ・HTML・CSS・BootStrap・JavaScript ・プログラムを設置したサーバー:netlify 工夫したところ ユーザの操作としては、ボタンを連打することがメインなので、ボタンを押した際のアニメーションにはこだわりました。 また、制限時間はを秒数表示するのではなく、ピンクの丸を縮小させていくことで、制限時間を認識できるように調整しました! おわりに もし興味を持っていただければ、このアプリで遊んでいただけたらすごく嬉しいです! 記事を書いて思ったのですが、嫁ブロック突破してガジェット買っても、後でバレたら怒られてしまうので対話して納得してもらうのが一番ですね!!(笑) 皆様のいい夫婦生活を願ってます!!(笑) 5秒で遊べるアプリなので、興味を持っていただき遊んでいただければ幸いです! 「嫁ブロックを突破するゲーム」 ↓↓↓ こちらのアプリも5秒で遊べる内容ですので、よろしければ遊んでみていただけると嬉しいです! 「ロケットを発射するゲーム」 ここまで読んでいただき、ありがとうございました!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JavaScript][メモ]popstateイベントをサンプルコードで挙動確認

はじめに (注)「~が解決した」みたいな記事じゃなくて、popstateイベントについてのメモです。 業務中、とある画面でブラウザボタンを押すとbfcache(ブラウザバック時に使うキャッシュ)が悪さをして、想定外の挙動をしてしまう、という状態だった 状況 A画面 ①↓ ↑② ↓③ B画面 ①ボタンを押して画面遷移(POST送信による遷移) ②ブラウザバックで画面遷移 ③再度ボタンを押して画面遷移→①でキャッシュに保存された値でリクエストしてしまいエラー (具体的には、楽観ロックエラー) 対応策としてのpopstateイベント この時に②でブラウザバックで遷移後、この「ブラウザバックしたこと」を検知して、ブラウザバックしたのであれば、強制的にリロードしてしまおう、と考えました。 サンプルコードで試してみる いくつか記事を拝見しましたが、自分の理解力が乏しく自分のやりたいように動いてくれるかわからなかったので、サンプルコードを自作して挙動を確認 A画面 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>A</title> <script> history.replaceState(null, null, null); window.addEventListener('popstate', function(e) { alert('popstate!!'); }); </script> </head> <body> <h1>indexAです</h1> <a href="./indexB.html">Bへ遷移</a> </body> </html> いくつか書いてある参考記事(本記事文末)通りですが、 history.replaceState(null, null, null); これで、この画面の履歴(history)を置き換えます。 大事なのは第3引数で、ここはhistoryに保存するurlを指定します。 nullにすると現在のurlを保存するようになります。 そして、次に同じ画面に遷移した際にhistoryを上書きします。 window.addEventListener('popstate', function(e) { alert('popstate!!'); }); ここではブラウザバックされた際の処理を書いています。 今回は先ほどのreplaceStatusで必ずhistoryに履歴が保存されるので、必ずブラウザバックした際にこの処理を実行することになります。 popstatusはブラウザの履歴が変更された際に発火するイベント B画面 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>B</title> </head> <body> <h1>indexBです</h1> <p>ブラウザバックしてください</p> </body> </html> ただの遷移用のページです。 想定では、ここに遷移したらブラウザバックして、遷移先でalertが動く挙動です。 挙動結果 完全に想定していた挙動ではありませんでした。実際の挙動は以下の通り。 A画面・・・③ ①↓ ↑② B画面 ①ボタンを押して画面遷移(POST送信による遷移) ②ブラウザバックで画面遷移・・・※ここで検知してほしかったが機能せず ③再度ブラウザバックしようとするとpopstateイベントが発火し、alertが実行 ③でpopstateイベントが発火するので、今回のやりたいことは実現できなさそう。。。 結論 今回はこのpopstateを使った解決はできないと判断し、他の方法で解決しました。 とはいえ、完全にブラウザバックを禁止して単一的な遷移フローのサイトを作る場合はいいかもですね。 (あんまりユーザーライクじゃない気も、、、) 参考記事 参考にさせていただき、ありがとうございました。 https://zenn.dev/k_kudo/articles/9b3633f4beb0b2 https://yutaihara.com/archives/512 https://qiita.com/dkdkd335/items/5749cd740d876a9b88d5 https://rainbow-engine.com/javascript-popstate-trigger/ 最後に たかが1つのJSのAPIの理解に時間かけちゃいましたが、想定通りに動いてくれないとわかったので、まあよしとします。 こうやってゆっくりでも知見を広げて、JSと仲良くなれればと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTML+CSS+JSで電卓を作ってみた※ウェブカツブログ記事参照

経緯 Javascriptのトレーニングを兼ねて何か作りたいなーと思い、ネット記事を漁っていると... ウェブカツというブログが簡易的な電卓の作り方を掲載していたので、早速取り組んでみることに。下記URL参照。 https://webukatu.com/wordpress/blog/27277/ 自身で作成してみたので、学習のアウトプットとして自身なりの解釈を交えながらご紹介したいと思います。 対象者 ・HTML、CSSをProgate等で一通り学んでいる方。 ・Javascriptを少し齧り始めた方。 ・Javascriptでどんなことができるのか興味のある方。 今回紹介・作成するものは非常に簡易的なものになるので、複雑で高度な技術は一切必要ございません。 作成物 ブラウザ上で動く電卓を作成します。 一番上には計算結果が出力される液晶画面、 液晶以下には演算子・数字を入れ、簡単な四則演算ができるようにしています。 CSS内でボタンの色やhoverなどを追加しています。  今回Javascriptの中には、ボタンが押されたときに正しい回答が出力されるようなコードを記述していきます。 ※なお当記事の目的は投稿者のアウトプットを主たる目的としております。ウェブカツブログ様の投稿内容を盗用しQiita内でLGTMを稼ぐつもりは一切ございません。ご了承のほど何卒宜しくお願い申し上げます。 実践 index.html <!DOCTYPE html> <html lang="ja"> <head> <title>calculator</title> </head> <body> </body> </html> HTMLの記述をしていきます。 一番はじめはこの状態からスタートします。 主なコードの記述先はタグの中です。 まずは液晶画面を記述します。 index.html <!DOCTYPE html> <html lang="ja"> <head> <title>CALCULATOR</title>. //ここのタイトル部分は何を入れても問題ありません。 </head> <body> <form name="calculator"> //formタグ内のnameプロパティに後々Javascriptの動きをつけていきます。 <table> <!---液晶画面---> <tr> <td colspan="4"> <input type="text" class="display" name="display" value="" disabled> </td> </tr> </form> </body> </html> titleタグには「電卓」を意味するCALCULATORを記述し、 同様にフォームタグ内のnameプロパティにも"calculator"と記述します。 作成者が理解できれば。基本的にどのような文字列でも大丈夫ですが、なるべく関連語や英単語等を記述するような習慣を心がけましょう。 現状のコードをブラウザに表示させるとこのようになります。↓ trタグは「table header」の略です。 通常は表の見出しに使用されるタグです。 子要素であるtdタグは、表の見出しを指定するタグです。 tdタグ内のcolspan="4"を指定することで、セルを連結することができます。 tdタグ内では、inputタグを用いています。 これが液晶画面の核となる部分です。 disableを指定することで、液晶画面に直接文字を打てないようにしています。 まだスタイルもJavascriptもつけていないので、ただの入力欄のみが表示されています。 どっちみちstyleもjavascriptも読み込まないといけないので、この段階で読み込んでおきましょう。 index.html <!DOCTYPE html> <html lang="ja"> <head> <link rel="stylesheet" href="style.css"> <title>CALCULATOR</title>. //ここのタイトル部分は何を入れても問題ありません。 </head> <body> <form name="calculator"> //formタグ内のnameプロパティに後々Javascriptの動きをつけていきます。 <table> <!---液晶画面---> <tr> <td colspan="4"> <input type="text" class="display" name="display" value="" disabled> </td> </tr>          <script src="script.js></script> </form> </body> </html> ページの見た目に変更を加えるCSSファイルは。headタグ内に記述します。 Javascriptはformタグの最下部へ記述してください。 ちなみにファイルの場所ですが、私はCALCULATORというプロジェクトにそれぞれ個々のファイルを作成しております。 linux環境内で学習・開発中の方は、 mkdir <ディレクトリ名> でディレクトリ作成。 touch <ファイル名> でファイル作成ができます。試してみてね。 続いて、各ボタンを配置していきます。 電卓はピンからキリまで様々な仕様のものがございますが、今回は最低限の四則演算機能のみを実装する予定です。 よって配置するボタンは、 ・0~9 ・= ・C(クリア) ・四則演算子 以上です。 index.html <!DOCTYPE html> <html lang="ja"> <head> <link rel="stylesheet" href="style.css"> <title>CALCULATOR</title> </head> <body> <form name="calculator"> <table> <!---液晶画面---> <tr> <td colspan="4"> <input type="text" class="display" name="display" value="" disabled> </td> </tr> <!---上から1段目(7〜9+÷)---> <tr> <td><input type="button" value="7" onclick="get_calc(this)"></td> <td><input type="button" value="8" onclick="get_calc(this)"></td> <td><input type="button" value="9" onclick="get_calc(this)"></td> <td><input type="button" value="÷" class="operator" name="div_btn" onclick="get_calc(this)"></td> </tr> <!---上から2段目(4〜6+×)---> <tr> <td><input type="button" value="4" onclick="get_calc(this)"></td> <td><input type="button" value="5" onclick="get_calc(this)"></td> <td><input type="button" value="6" onclick="get_calc(this)"></td> <td><input type="button" value="×" class="operator" name="multi_btn" onclick="get_calc(this)"></td> </tr> <!---上から3段目(1〜3+-)---> <tr> <td><input type="button" value="1" onclick="get_calc(this)"></td> <td><input type="button" value="2" onclick="get_calc(this)"></td> <td><input type="button" value="3" onclick="get_calc(this)"></td> <td><input type="button" value="-" class="operator" onclick="get_calc(this)"></td> </tr> <!---上から4段目(0/C/=/+)---> <tr> <td><input type="button" value="0" onclick="get_calc(this)"></td> <td><input type="button" value="C" onclick="get_calc(this)"></td> <td><input type="button" value="=" class="equal" onclick="get_calc(this)"></td> <td><input type="button" value="+" class="operator" onclick="get_calc(this)"></td> </tr> </table> <script src="script.js"></script> </form> </body> </html> trタグを用いて、上から一段ずつ記述しています。 ここでもinputタグを使用しています。 第一引数でtype="button"とすることで、単純なボタンを描画します。 第二引数であるvalueでは各ボタンで描画する文字を記述します。 第三引数、第五引数に記述しているonclick属性は、クリックされたときにどのような動きをするかを設定することができます。 今回はJavascriptも活用するため、get_calcメソッドを入っています。 get_calcメソッドに関しては後で説明いたします。 進捗をブラウザで表示するとこのようになります。 スタイルを整えていないのでかなり簡素な見た目ですが、これでも十分に電卓っぽいと思います。 かなりあっさりですがこれでHTMLの記述は終了です。 続いてCSSに移ります。 見た目の調整 ここではコメントアウトを参照して各CSSの機能を確認してください。 style.css /* テーブルの装飾 */ table { /* 電卓のサイズ */ width: 300px; height: 400px; /* 電卓が浮き出るように影をつける */ border: solid 1px #dcdcdca4; border-right: solid 4px #dcdcdca4; border-bottom: solid 4px #dcdcdca4; border-radius: 10px; /* インライン要素を中央に配置 */ text-align: center; /* 余白調整 */ padding: 8px; margin: 20px; } input { /* ボタンのサイズ */ width: 70px; height: 70px; /* ボタンの文字サイズ */ font-size: x-large; /* 数字部分の背景色 */ background-color: #dcdcdca4; /* ボタンの詳細設定 */ border: none; border-radius: 20px; /* クリック時の黒枠を消す */ outline: none; } /* ディスプレイの詳細設定 */ .display { width: 250px; text-align: right; /*文字列を右詰めに*/ /*見た目の詳細設定*/ background: #ffffff; border-top: solid #dcdcdca4 5px; border-bottom: solid #dcdcdca4 5px; border-right: solid #dcdcdca4 6px; border-left: solid #b6b6b6 6px; border-radius: 5px; } /*演算子の背景色を上書きで設定*/ .operator { background-color: #87cefa; } /*記号=の部分の背景色を上書きで設定*/ .equal { background-color: #6b6b6b; } /*カーソルを上に乗せたときに色を濃くする*/ input:hover { background: #747373b9; } .display:hover { background: #ffffff; /*ディスプレイ部分は無効化*/ } .operator:hover { background: #339cdd; } /*クリック時に色を濃くする */ input:active { background: #5a5a5a; } .operator:active { background: #2c80b4; } 動きをつける(Javascriptの記述) 続いて、Javascriptの実装に入ります。 とはいってもボタンが押されたら液晶に表示されて計算してくれる程度の機能なので、記述量は少ないです。 script.js function get_calc(btn) { if(btn.value == "=") { document.calculator.display.value = eval(document.calculator.display.value); } else if (btn.value == "C") { document.calculator.display.value = ""; } else { if (btn.value == "×") { btn.value = "*"; } else if (btn.value == "÷") { btn.value = "/"; } document.calculator.display.value += btn.value; document.calculator.multi_btn.value = "×"; document.calculator.div_btn.value = "÷"; } } 1行目、function get_calc(btn) {}で先ほどHTMLにて記述したget_calcメソッドを定義しています。引数にはボタンを意味するbtnを入れています。 中括弧のなかで、if文を記述しています。 script.js if(btn.value == "=") {} // {}の中に、「=」が押された際の動きを記述します。 ここでは script.js document.calculator.display.value = eval(document.calculator.display.value); としています。 これは、「=」が押された際、evalメソッドで式を評価し演算を行うということを意味しています。 その下のelse ifでは, script.js else if (btn.value == "C") {![スクリーンショット 2022-02-23 17.35.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2494809/25600419-74f5-22fb-6938-6b1316b85185.png) document.calculator.display.value = ""; } 「=」が押されたときと同様に「C」が押されたときの動きを記述しています。 すでに上述の通り、「C」の機能は液晶表示をクリアにすることなので、空文字列を代入することで、液晶に表示されている文字列を消去することができます。 その下、 script.js else { if (btn.value == "×") { btn.value = "*"; } else if (btn.value == "÷") { btn.value = "/"; } 「×」が押されれば、掛け算を、「÷」が押されれば割り算をするように記述します。 完成、作ってみた感想 最終的にこのような形となります。 Javascriptの練習で始めましたが、ここまでサクッと且つ直感的に学べるとは思ってもいませんでした。 より大きく複雑なものを作成しようと思ったら、コード量もかなり多くなってしまうと思いますが、今回は1時間以内で作成できてしまうと思います。 Javascriptの学習のとっかかりとしては良い課題だと感じました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【個人開発】嫁ブロックを突破するアプリをつくました。

はじめに こんにちわ!くのと申します! エンジニアの方ってガジェット好きな方多いですよね!! しかしながら、エンジニアにとってガジェットを購入するために突破しなければいけない壁っていくつかありますよね。。。 突然ですが、2022年に実施されたガジェット購入時における障壁ランキングの堂々の第4位は何かご存知でしょうか?? 。。。そうですね!!それは「嫁ブロック」問題ですね!! (すいません、これは僕の妄想アンケートの結果です)。 というわけで、嫁ブロックを突破し、高スペックなマシンをゲットして、強強エンジニアを目指すクソアプリを開発してみました!! アプリ概要 スタートから、5秒以内にボタンを連打して嫁ブロックを突破するアプリです。 *連打回数が少ないと嫁ブロックは突破できないので、注意してくださいね。 嫁ブロックを突破するゲーム https://infallible-chandrasekhar-86218b.netlify.app/ *スマホ向けにデザインしていますので、スマホの方が遊びやすくなっております。 使用技術 ・HTML・CSS・BootStrap・JavaScript ・プログラムを設置したサーバー:netlify 工夫したところ ユーザの操作としては、ボタンを連打することがメインなので、ボタンを押した際のアニメーションにはこだわりました。 また、制限時間はを秒数表示するのではなく、ピンクの丸を縮小させていくことで、制限時間を認識できるように調整しました! おわりに もし、興味を持っていただきこのアプリを遊んでいただけたらすごく嬉しいです! 記事を書いて思ったのですが、嫁ブロック突破してガジェット買っても、後でバレたら怒られてしまうので対話して納得してもらうのが一番ですね!!(笑) 皆様のいい夫婦生活を願ってます!!(笑) 5秒で遊べるアプリなので、興味持っていただけたらぜひ遊んでいただければ幸いです! よろしくおねがいします!! 「嫁ブロックを突破するゲーム」 https://infallible-chandrasekhar-86218b.netlify.app/ ↓↓ こちらのもアプリも5秒で遊べる内容ですので、よかったら遊んでみていただけると嬉しいです! 「ロケットを発射するゲーム」 https://app.netlify.com/sites/zealous-heyrovsky-308820/overview ここまで読んでいただき、誠にありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JS]IF文の省略

JavaScriptのIF文の省略形 下記のYoutubeを参考にWebアプリケーションを作成している際に 学習の過程で見たことのなかった構文が使用されていたので紹介します。 初期段階ではこのように1~9秒の時に01~09の表記になっていません 完成図はこちらになります。 01~09の表示にするためにaddZeroという関数を作成します。 main.js function addZero(n) { return(parseInt(n, 10) < 10 ? '0' : '') + n; } if文を省略せずに記述すると以下のようになります。 main.js function addZero(n) { if(parseInt(n,10) < 10) { return '0' + n; } else { return '' + n; } } この様に、条件文に「?」を付けて、”True”の場合の処理を記載し、続いて「:」を付けて、”else”の場合の処理を記載することができます。 今回のようなreturnするだけの処理を書く場合には時短にもなりますし簡単に記述することができます。 このような書き方を学習の過程で目にすることがなかったので勉強になりました。 今回の記述法が実際に現場で使用される書き方かどうかはわかりませんが 書籍などの学習では得られないところが学べたので良かったと思います。 これからも積極的にアウトプットして新たな知識を蓄えていきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

トーナメント表作成HTML(まとめ)

はじめに この記事はまとめページの予定です。 内容を小分けにして各ページでコード及び簡単な解説をつけたいと思います。 最終的には以下のようなものができます↓。 リンク 各ページのリンクです 番号順に見ていけば混乱なく読める…と、思いたいです。 1. 実装機能イメージ、ベースのHTML 2. -工事中-Canvas(HTML5)の操作 3. -工事中-トーナメント表の構築(DOM要素の配置) 4. -工事中-トーナメント表の線引き 5. -工事中-トーナメント結果のファイル保存・読込-工事中- 6. -工事中-その他細々とした機能 順次作成更新出来たらなと思います。 なお、本人は作成時に設計メモを以下しか残してないので、お察しです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[OutSystems]コールバック関数を渡すと自動的にログを取ってくれるClient Action作ってみた

前回の記事でClient Actionにはコールバック関数を引数として渡せることがわかったので、今回は実際に役立ちそうな処理を作ってみました。 作ったもの 引数 Function(Object型):実際に実行したい関数 Arguments(Text型):その関数に渡す引数(JSON) Result(Long Integer型):関数実行結果(今回は例ではテーブルに登録したレコードのid) メイン処理 2行目で引数を与え関数を実行。どうもOutSystemsはこれをすぐに実行するのではなくPromiseとして非同期実行するようで戻り値はPromiseになります 4行目で関数の実行結果をPromiseResultから取得しています。おそらくOutSystemsはPromiseを作るときに次のような処理をしていてPromiseResultの中身が複雑ですが、一応この処理でテーブルに登録したレコードのidは取得できました(OutSystemsの内部仕様が変わればすぐに動かなくなりそうなコードですが) 7行目でJavaScript APIを呼び出し、ブラウザのコンソールとGeneral Logに実行時間のログを出力しています new Promise(function(resolve) { var obj = {}; // 実際に実行したい関数の呼び出し // objに色々追加 resolve(obj); }) 使い方 登録ボタン押下時に呼び出されるClient Actionの中で、登録処理を行うClient Action(もしくはServer Action)の代わりにCallWithLoggingを呼び出します。ここではCallWithLoggingの引数FunctionにProductCreateOrUpdate(Client Action)を設定しています。このようにすれば各ロジック(ProductCreateOrUpdateなど)はログのことを気にせず実装できますし、ログの保管先が変わったりした場合も修正するのはCallWithLoggingだけで済みます。 注意点 CallWithLoggingに汎用性を持たせるためArgumentsをJSONにしています。そのため本来ならばProductCreateOrUpdate(Client Action)の引数はProduct Entity型でいいのですが、CallWithLoggingを使うためにText型に変更する必要があります。ProductCreateOrUpdate(Server Action)も同様で、この中でJSONをProduct Entity型に変換しています。 CallWithLoggingは本当に使えるのか? ここまで書いておいて何なのですが、正直CallWithLoggingはあまり使えない(役に立たない)と思います。既に書いた通りOutSystemsの内部仕様と密接になっていますしJSON変換も面倒です。ResultがLong Integer型なのも汎用性の観点で改良の余地があります。さらに実行時間測定するだけならLifeTimeで賄えると思います。ただ、別の用途で今回のパターンが役に立つことがあるかもしれません。思いついたらまた記事にしようと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EJS記法まとめ

<%= ~ %> 出力タグ(エスケープあり) 変数をエスケープして出力する。 変数に禁則文字(<や&など)を含む場合に自動的にエスケープされる。 <!-- 変数を定義 --> <% const name = 'John' %> <!-- 定義した変数を出力 --> <p>Hello, <%= name %>.</p> <p>Hello, <%= 'Mr. ' + name %>.</p> <!-- 出力結果 --> <p>Hello, John.</p> <p>Hello, Mr. John.</p> <%- ~ %> 出力タグ(エスケープなし) 変数をエスケープせずに出力する。(利用時はXSS攻撃に配慮すること) <!-- 変数を定義 --> <% const myHtml = '<strong>Timothy</strong>' %> <% const myMaliciousHtml = '</p><script>document.write()</script><p>' %> <!-- 定義した変数を出力(エスケープなし) --> <p>Hello, <%- myHtml %>.</p> <p>Hello, <%- myMaliciousHtml %>.</p> <!-- 定義した変数を出力(エスケープあり) --> <p>Hello, <%= myHtml %>.</p> <p>Hello, <%= myMaliciousHtml %>.</p> <!-- 出力結果 --> <p>Hello, <strong>Timothy</strong>.</p> <p>Hello, </p><script>document.write()</script><p>.</p> <p>Hello, &lt;strong&gt;Timothy&lt;/strong&gt;.</p> <p>Hello, &lt;/p&gt;&lt;script&gt;document.write()&lt;/script&gt;&lt;p&gt;.</p> <%# ~ %> コメントタグ HTMLに出力されたり、実行されたりしないコメントの記載に利用する。 終了タグを後述する改行削除タグ -%> にすることでコメント行の不要な改行を除去できる。 <div> <%# comment %> </div> <div> <%# comment -%> </div> <!-- 出力結果 --> <div> </div> <div> </div> <% ~ %> スクリプトレットタグ このタグ内ではJavaScript構文を用いてロジックを埋め込むことができる。 波括弧{} スクリプトレットのみの場合は{}を使わなくても良いが、 EJSテンプレートと混在する場合は基本的に{}を省略しないほうが良いとのこと。 <!-- 良くない例({}がない) --> <% if (true) %> <p>Yay it's true!</p> <!-- 良い例({}で囲っている) --> <% if (true) { %> <p>Yay it's true!</p> <% } %> <!-- こちらも正常に動作する例 --> <% let output, exclamation = '', shouldOutput = false; if (true) output = 'true!'; if (true) { exclamation = 'Yay! '; } if (true) shouldOutput = true; %> <% if (shouldOutput) { %> <!-- EJSテンプレートと混在するため、{}で囲っている --> <p><%= exclamation + 'It\'s ' + output %></p> <% } %> <!-- 出力結果 --> <p>Yay it's true!</p> <p>Yay it's true!</p> <p>Yay! It&#39;s true!</p> タグ内の改行 EJSとJSのスクリプトレットを混在させる場合を除き、 <% ~ %>タグ内に完全なステートメントとして記載すること。 <!-- OK例 --> <% const stringToShow = booleanVariable ? 'OK' : 'not OK' %> <!-- NG例(ビルドエラーが起こる) --> <% const stringToShow = booleanVariable %> <% ? 'OK' %> <% : 'not OK' %> セミコロン Javascript同様、適切な改行がなされていればセミコロンはなくても動作する。 空白と改行 スクリプトレットを使用すると出力結果に無駄な空白が発生することがある。 これを防ぐには、開始タグに<%_を使い空白を削除し、終了タグに-%>を使い改行を削除する。 それぞれ<%_はこのタグ前の空白を削除し、-%>はスクリプトレットやコメントによって生まれる余分な改行を削除する。(出力タグには効果なし) <!-- 削除なし --> <ul> <% for (let i = 0; i < 3; i++) { %> <li><%= i %></li> <% } %> </ul> <!-- 削除あり --> <ul> <%_ for (let i = 0; i < 3; i++) { -%> <li><%= i %></li> <%_ } -%> </ul> <!-- 出力結果 --> <!-- 削除なし --> <ul> <li>0</li> <li>1</li> <li>2</li> </ul> <!-- 削除あり --> <ul> <li>0</li> <li>1</li> <li>2</li> </ul> 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

本のバーコードを読み取ってNotionで読書録を作成するアプリを作ってみた

はじめに はじめまして。インディーゲームを作っております、nyorokoと申します。 ゲームづくりの他に読書が好きで、「読書録を簡単に作成・管理することはできないか?」という問題意識があり、タイトルの通りのアプリを作ってみました。 完成したもの 以前Notion APIを用いて、本のバーコードを読み取る→ISBNを取得する→ISBNから本の情報を取得する→Notionのデータベースに追加するという一連の動作を行うアプリを開発しました??(´・_・`??) pic.twitter.com/GqPBPV7sin— nyoroko (@nyoroko_nyoro) February 21, 2022 Notionとは? Notion公式サイト Notionとは、タスク管理やメモ等を一元的に行うことのできるアプリです。 Evernoteと似ていると思うのですが、無料で複数のデバイスから使用可能であるなどの違いがあります。 私はPC、iPhone、iPadなど様々なデバイスを使用しているため、メモアプリとしてはNotionを使っております。 今回は、そのNotionを使って読書録を作ってみようと考えました。 ちなみに、この記事もNotionで下書きを作成しております。 アプリの実装方法について 大まかな処理の流れ 以下のようなフローで読書録を作成します。 本のバーコードを読み取りISBNを取得 ISBNをキーに本の情報を取得 取得した本の情報をNotionに送信 どうアプリ化するか? まず、ウェブアプリとして実装します。 理由としては、様々なデバイスで使用できることなどが挙げられます。 次に、ウェブアプリとして実装するにあたって、GAS(Google App Script)を活用します。 GASには、 Googleアカウントさえあれば開発環境の整備が不要 Google謹製であり情報が多く、安定している 無料である などの多くの利点があるためです。 詳細な実装方法 1. GASでプロジェクトを作成 GASについてはネット上に情報が多く存在しておりますので、詳細は割愛いたします。 例えば、この記事が参考になると思います。 2. 「index.html」を作成 デフォルトで作成される「コード.js」に、以下のコードを書き込みます。 そして、同階層に「index.html」を作成します。 これにより、ウェブアプリとして公開した際に、「index.html」が表示されます。 function doGet() { var template = 'index'; return HtmlService .createTemplateFromFile('index') .evaluate() .addMetaTag('viewport', 'width=device-width, initial-scale=1') //html側にメタタグを設定しても意味がなかったためこちらで } 3. quaggaJSの使用 バーコードを読み取ってISBNを取得するために、quaggaJSを使用します。 quaggaJSについては、この記事が参考になります。 今回は、2.で作成したindex.htmlにソースコードをそのまま追記させていただきました。 これで、バーコードを読み取ってISBNを取得する部分が完成しました。 いやはや、素晴らしいライブラリですね…! quaggaJSですが、2017年以降メンテナンスされていないようです。 また、今回はGASでの実装なので関係ないのですが、localhost以外のサイトではhttpsでないと利用できません。(私はここで一度ハマりました…) (注意事項についての参考記事) 4. ISBNから本の情報を取得 この目的を達成するためには、 Google Books API 楽天ブックスAPI 国立国会図書館サーチAPI など様々なAPIがありますが、今回は登録不要であることなどからGoogle Books APIを使用しました。 ただ、サムネイル画像が小さかったため、本の画像のみAmazonを使用しました。 今回は、 タイトル サブタイトル 著者 出版日 ページ数 画像 を取得することとし、まずは対応するinput要素をHTML部分に作成します。 (HTMLについても情報が多いため、詳細は割愛いたします。) 「index.html」のJavaScript部分におけるfunctionのサンプルコードは以下の通りです。 HTML部分のidに応じて改変してご利用ください。 function GetInfo(){ url = "https://www.googleapis.com/books/v1/volumes?q=isbn:"; //ISBNの手前まで isbn = document.getElementById("ISBN").value; let request = new XMLHttpRequest(); request.open('GET', url+isbn); request.responseType = 'json'; request.send(); request.onload = function() { const result = request.response; document.getElementById("title").value = result["items"][0]["volumeInfo"]["title"]; document.getElementById("subtitle").value = result["items"][0]["volumeInfo"]["subtitle"]; document.getElementById("authors").value = result["items"][0]["volumeInfo"]["authors"]; document.getElementById("publishedDate").value = result["items"][0]["volumeInfo"]["publishedDate"]; document.getElementById("pageCount").value = result["items"][0]["volumeInfo"]["pageCount"]; document.getElementById("imagePlace").src = "https://images-na.ssl-images-amazon.com/images/P/"+toISBN10(isbn)+".09.LZZZZZZZ.jpg"; document.getElementById("imageSrc").value = "https://images-na.ssl-images-amazon.com/images/P/"+toISBN10(isbn)+".09.LZZZZZZZ.jpg"; } } const toISBN10 = (isbn13) => { // 1. 先頭3文字と末尾1文字を除く const src = isbn13.slice(3, 12); // 2. 先頭の桁から順に10、9、8…2を掛けて合計する const sum = src.split('').map(s => parseInt(s)) .reduce((p, c, i) => (i === 1 ? p * 10 : p) + c * (10 - i)); // 3. 合計を11で割った余りを11から引く(※引き算の結果が11の場合は0、10の時はアルファベットのXにする) const rem = 11 - sum % 11; const checkdigit = rem === 11 ? 0 : (rem === 10 ? 'X' : rem); // 1.の末尾に3.の値を添えて出来上がり return `${src}${checkdigit}`; } AmazonはISBN10にしか対応していないため、ISBN13をISBN10に変換する必要があります。変換するコードは以下の記事から引用させていただきました。 JavaScriptで10桁のISBNを13桁に変換する 5. Notion APIの下準備 Notion APIを使用して、取得した本の情報を送信します。 下準備として、データベースの作成およびIDを取得する必要があり、この記事が参考になります。 データベースのフィールドとしては、上記で取得する項目の他に、「State」フィールドを追加しました。 後続の工程で、デフォルトでは「興味あり」というセレクト項目が送信されるようにします。 6. Notion APIを使って情報を送信 index.htmlから直接Notion APIに接続すると、CORSエラーが発生してしまいます。 そのため、間にGASを挟むことでCORSエラーを回避しました。 具体的には、「コード.gs」に以下のコードを追記し、「index.html」側のフォームからdoPostを呼び出すことでCORSエラーを回避しました。 なお、JSONの構造についてはこの記事を参考にしました。 function doPost(e){ SendToNotion(e.parameter.title,e.parameter.subtitle,e.parameter.authors,e.parameter.publishedDate, Number(e.parameter.pageCount),e.parameter.isbn,e.parameter.imageSrc); } // Notion API function SendToNotion(title,subtitle,authors,publishedDate,pageCount,isbn,imageSrc) { const notion_key = 'あなたのNotionキー'; const database_id = 'あなたのデータベースid'; json_data = { 'parent': {'database_id': database_id}, 'properties': { // ↓ここにプロパティをカンマで区切って記述していく 'Title': { 'title': [ { 'text': { 'content': title, } } ] }, 'Subtitle': { 'rich_text': [ { 'text': { 'content': subtitle, } } ] }, 'Author': { 'rich_text': [ { 'text': { 'content': authors, } } ] }, 'State': { 'select': { 'name': '興味あり', 'color': 'yellow' } }, 'PublishedDate': { 'rich_text': [ { 'text': { 'content': publishedDate, //本によって形式がバラバラなので文字列とする } } ] }, 'PageCount': { 'number': pageCount, }, 'ISBN': { 'rich_text': [ { 'text': { 'content': isbn, } } ] } // ↑ここまでプロパティ }, 'children': [ // ↓こっちは本文ブロック { 'object': 'block', 'type': 'image', "image": { "type": "external", "external": { "url": imageSrc } } } // ↑ここまで本文 ] }; UrlFetchApp.fetch( 'https://api.notion.com/v1/pages', { "method" : "post", "headers" : { 'Content-Type' : 'application/json; charset=UTF-8', 'Authorization': 'Bearer ' + notion_key, 'Notion-Version': '2021-05-13', }, "payload" : JSON.stringify( json_data ) }); } 7. 見栄えの調整(スキップ可) このままだと「index.html」の見栄えが良くないのですが、CSSを編集するのは手間であるため、今回はBootstrapを用いてイケてるデザインにしました。 8. ウェブアプリとして公開 GASの画面の「デプロイ」ボタンを押し、「ウェブアプリとして公開」することで完成です! なお、iOSの場合はウェブサイトをホーム画面に追加する機能がありますので、あたかもネイティブアプリかのような見栄えにすることができます。 最後に 初投稿ということで、至らぬところが多かったと思いますが、少しでもご参考になれば幸いでございます。 また、「アプリを作った」と言いつつ、先人が作った素晴らしいライブラリやAPIをフルに活用させていただいておりますので、感謝の念に堪えません。 最後に、私はインディーゲームを開発しております。無料でサクッと遊べますので、ぜひリンク先だけでもご覧いただけますと幸甚です。 Space Journey - Simple law, Complex behavior - nyorokoのitchプロフィールページ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Chart APIで円グラフを作る際に、jsからejsへのデータの渡し方。

はじめに グラフを表示したくなり、「【超簡単】Google Chartsによる円グラフの作り方【コピペでOK】」を参考に始めました。データベースなどからデータを取り込むことも考え、データの置く場所をjsにおくのに苦労したので記事にしました。 環境 windows 10 node 16.13.1 ejs 3.1.6 express 4.17.3 動いたコード 環境構築は「【Node.js】20分でできる!GoogleChart表示」を参考にしました。 Step1. Node.jsのプロジェクト作成 Step2. npm install、 (Expressをインストール、ejsをインストール、プロジェクトフォルダ直下に index.js を作成) Step3. GoogleChartのサイトからコードをコピー Step4. 処理をちょこっと書き換える index.js const express = require('express'); const app = express(); app.get('/healthz', (req,res,next) => { return res.status(200).send({'status': 'OK'}); }); // テンプレートエンジンの指定 app.set("view engine", "ejs"); // Google Chart API へ引き渡すデータ app.get('/charts', (req,res,next) => { let data = { items:{ datas:[ //['Category', '記事数'], ['Web', 24], ['Blog', 11], ['Business', 11], ['Gourmet', 6], ['Life', 16] ], options :{ title:"ブログカテゴリ別記事数", } } } return res.render("./chart.ejs", data); }) app.listen(3000, () => { console.log('start express ...'); }) views\chart.ejs <html> <head> <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script> <script type="text/javascript"> google.charts.load('current', {'packages':['corechart']}); google.charts.setOnLoadCallback(drawChart); function drawChart(){ let itemsEjs = JSON.parse('<%- JSON.stringify(items) %>'); let dataejs = new google.visualization.DataTable(); dataejs.addColumn( 'string','Category'); dataejs.addColumn( 'number','記事数'); dataejs.addRows(itemsEjs.datas) <% console.log(items.options); %> console.log(itemsEjs);//logでない var chart = new google.visualization.PieChart(document.getElementById('piechart')); chart.draw(dataejs,itemsEjs.options) } </script> </head> <body> <div id="piechart" style="width: 900px; height: 500px"></div> </body> </html> できたグラフ 最初のejs <script> //略 function drawChart(){ let itemsEjs =<%= items %> //略 何がいけないのか、当初さっぱりわからなかったが、開発者ツールでみると、itemsEjs=[object,Object]となっている。 価が渡ってないらしい。 検索していると、「javascriptとejsの変数の受け渡しについて」 が見つかる。 文字列以外は無理です。 略 オブジェクトやClassのインスタンスを無理やり文字列(String型)にキャストすると "[object Object]"という文字列になります。 略 シリアライズしましょう。 JSON形式の文字列にして持ち込むのが基本です。 これを踏まえ、「【Node.js】20分でできる!GoogleChart表示」を参考に let itemsEjs = JSON.parse('<%- JSON.stringify(items) %>'); 終わりに 【Node.js】20分でできる!・・・のコードのほうが短くて、速い(データが多いとaddRowsが遅い?参考にしたところが出てこなくて・・・)のでお勧めですが、今回は中でどのように動いているかの理解が自分の中で少し前進したので、あえてaddRowsの書き方にしました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript関数ドリル】last関数の実装【勉強用】_.slice関数

【JavaScript関数ドリル】初級編のlast関数の実装のアウトプット last関数の挙動 _.last([1, 2, 3]); // => 3 配列の末尾を返す last関数の課題内容 last関数に取り組む前の状態 array.length-1を変数に格納するだけでよさそう 関数ドリルの取り組む順番を間違えている気がする last関数に取り組んだ後の状態 そのままarray[array.length -1]を返せば1行で済んだ 今後も元の配列を破壊していないか確認することにする last関数の実装コード const last = (array) => { const lastNum = array.length - 1; return array[lastNum]; }; const nums = [1, 2, 3, 4, 5] console.log(last(nums)); // => 5 console.log(nums); // => [1, 2, 3, 4, 5] last関数の解答コード function last(array) { return array[array.length - 1]; } console.log( last([1, 2, 3]) ); // => 3
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript関数ドリル】Lodash関数の実装【勉強用】_.last関数

【JavaScript関数ドリル】初級編のlast関数の実装のアウトプット last関数の挙動 _.last([1, 2, 3]); // => 3 配列の末尾を返す last関数の課題内容 last関数に取り組む前の状態 array.length-1を変数に格納するだけでよさそう 関数ドリルの取り組む順番を間違えている気がする last関数に取り組んだ後の状態 そのままarray[array.length -1]を返せば1行で済んだ 今後も元の配列を破壊していないか確認することにする last関数の実装コード const last = (array) => { const lastNum = array.length - 1; return array[lastNum]; }; const nums = [1, 2, 3, 4, 5] console.log(last(nums)); // => 5 console.log(nums); // => [1, 2, 3, 4, 5] last関数の解答コード function last(array) { return array[array.length - 1]; } console.log( last([1, 2, 3]) ); // => 3
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スマホやPCのカメラでQRを連続して読み取り、スプレッドシートに記録するApps Scriptのウェブアプリ

JavaScriptのライブラリー html5-qrcode を使えば、スマートフォンやPCのカメラを使ってQRコードを連続して読み取り、スプレッドシートに記録するウェブアプリを無料で作れます。バーコードもOKです? QRコードを「ぴっ」「ぴっ」と連続して読み取って、スプレッドシートに追記していくApps Scriptを作ってみました! https://t.co/lYzWHGKhbh pic.twitter.com/XH3kcYuNqo— 高玉 広和 (@takatama_jp) February 23, 2022 デモに使ったスプレッドシートとApps Scriptはこちらです。どうぞコピーしてご自由にお使いください。1 使っている技術については後述していきます。 なお、GoogleのノーコードツールAppSheetを使えば、連続読み取りはできないものの、QRコードを使うアプリを簡単に作れます。以前記事にしているので、よろしければご覧ください。 注意事項 QRコードの読み取りは、端末やカメラの性能、光源などによって大きく左右されます。 QRコードリーダーとして使いたいスマホやPCと、読み取らせたいQRコードを準備の上、次のcodepen.ioのURLにアクセスし、期待通りの読み取りができることをお試しください。読み取りが成功すると、音が出ます。 使い方 スプレッドシートをコピーして、Apps Scriptをウェブアプリとして公開します。 スプレッドシートにアクセスして、上部メニュー「ファイル」>「コピーを作成」でコピーします。Apps Scriptも一緒にコピーされます。 コピーしたスプレッドシートの上部メニュー「拡張機能」>「Apps Script」でスクリプトエディターを開きます。 Apps Scriptのスクリプトエディター右上にある「デプロイ」ボタンから「新しいデプロイ」を選び、アプリの種類で「ウェブアプリ」を選択の上、デプロイします。 権限が求められたら付与してください。 URLが払い出されるので、PCもしくはスマートフォンのウェブブラウザーからアクセスしてください。 アクセスしたら、Request Camera Permissions ボタンを押して、カメラを許可します。 QRコードを読み取ると、スプレッドシートのシート「Data」に、日付と読み取り結果を追記していきます。 なお、スクリプトを書き換えた場合は、スクリプトエディターで再デプロイが必要になりますのでご注意ください(後述します)。 使っている技術 複数の技術を組み合わせて実現しています。 バックエンド Google Apps Script doGet() を使ったウェブアプリ化 スプレッドシートへのデータ追加 フロントエンド html5-qrcode QRコードのスキャン HTMLAudioElement 読み取り音の再生 Google Apps Script google.script.run() を使ったバックエンドのAPI呼び出し その他 IMAGE関数と、Google Charts API QRコードの作成 バックエンド Apps Script は次の通りです。 Code.gs const SHEET_NAME = 'Data'; function doGet() { return HtmlService.createHtmlOutputFromFile('index') .addMetaTag('viewport', 'width=device-width, initial-scale=1'); } function onScan(text) { SpreadsheetApp.getActiveSpreadsheet() .getSheetByName(SHEET_NAME).appendRow([new Date(), text]); } 後述しますが、HTMLファイルindexをApps Scriptに追加しています。 doGet()で、アクセスしてきたウェブブラウザーにindexを渡します。スマートフォンに対応するため、addMetaTag()で画面が拡大しないよう調整しています。 onScan(text)はフロントエンドから呼び出します。スプレッドシートに日付と受け取ったデータtextを追記します。 ウェブアプリの再デプロイ ウェブアプリには2種類のURLがあります。「あれ?スクリプトを書き直したのに反映されないな?」と思ったら、URLを確認してみてください。 開発用 末尾が/devのURLです。 「新しいデプロイ」の後、「デプロイをテスト」で表示されるURLです。 スクリプトエディターで保存したスクリプトがそのまま動きます。 本番用 末尾が/execのURLです。 「新しいデプロイ」や「スクリプトを管理」で表示されるURLです。 再デプロイした時点のスクリプトで動作します。 開発用のURLで正しく動作することを確認したら、再デプロイをして、本番用の動作を変更します。再デプロイするには「デプロイを管理」を使います。 スクリプトエディター右上の「デプロイ」>「デプロイを管理」を選択 鉛筆アイコンを押して、バージョンを「新しいバージョン」にして、デプロイ このやり方なら、新しいデプロイで払い出されたURLはそのままで、動作を変更していくことができます。 フロントエンド Apps Scriptは次の通りです。 index.html <!DOCTYPE html> <html> <head> <base target="_top"> </head> <body> <div id="reader" style="width:340px;"></div> <div id="result"></div> <script src="https://unpkg.com/html5-qrcode"></script> <script> let CURRENT_TEXT = ''; const append = (text) => { document.querySelector('#result').innerHTML += `${text}<br>`; }; const onScanSuccess = (decodedText, decodedResult) => { console.log('onScanSuccess', decodedText, CURRENT_TEXT); if (CURRENT_TEXT !== decodedText) { CURRENT_TEXT = decodedText; append(decodedText); beep(); google.script.run .withFailureHandler(function (error) { append(`"${decodedText}"の保存に失敗しました: ${error}`); }) .onScan(decodedText); } }; const html5QrcodeScanner = new Html5QrcodeScanner("reader", { fps: 10, qrbox: 240 }); html5QrcodeScanner.render(onScanSuccess); const beep = () => { const dataUrl = "data:audio/wav;base64,//uQRAAAAWMSLwUIYAAsYkXgoQwAEaYLWfkWgAI0wWs/ItAAAGDgYtAgAyN+QWaAAihwMWm4G8QQRDiMcCBcH3Cc+CDv/7xA4Tvh9Rz/y8QADBwMWgQAZG/ILNAARQ4GLTcDeIIIhxGOBAuD7hOfBB3/94gcJ3w+o5/5eIAIAAAVwWgQAVQ2ORaIQwEMAJiDg95G4nQL7mQVWI6GwRcfsZAcsKkJvxgxEjzFUgfHoSQ9Qq7KNwqHwuB13MA4a1q/DmBrHgPcmjiGoh//EwC5nGPEmS4RcfkVKOhJf+WOgoxJclFz3kgn//dBA+ya1GhurNn8zb//9NNutNuhz31f////9vt///z+IdAEAAAK4LQIAKobHItEIYCGAExBwe8jcToF9zIKrEdDYIuP2MgOWFSE34wYiR5iqQPj0JIeoVdlG4VD4XA67mAcNa1fhzA1jwHuTRxDUQ//iYBczjHiTJcIuPyKlHQkv/LHQUYkuSi57yQT//uggfZNajQ3Vmz+Zt//+mm3Wm3Q576v////+32///5/EOgAAADVghQAAAAA//uQZAUAB1WI0PZugAAAAAoQwAAAEk3nRd2qAAAAACiDgAAAAAAABCqEEQRLCgwpBGMlJkIz8jKhGvj4k6jzRnqasNKIeoh5gI7BJaC1A1AoNBjJgbyApVS4IDlZgDU5WUAxEKDNmmALHzZp0Fkz1FMTmGFl1FMEyodIavcCAUHDWrKAIA4aa2oCgILEBupZgHvAhEBcZ6joQBxS76AgccrFlczBvKLC0QI2cBoCFvfTDAo7eoOQInqDPBtvrDEZBNYN5xwNwxQRfw8ZQ5wQVLvO8OYU+mHvFLlDh05Mdg7BT6YrRPpCBznMB2r//xKJjyyOh+cImr2/4doscwD6neZjuZR4AgAABYAAAABy1xcdQtxYBYYZdifkUDgzzXaXn98Z0oi9ILU5mBjFANmRwlVJ3/6jYDAmxaiDG3/6xjQQCCKkRb/6kg/wW+kSJ5//rLobkLSiKmqP/0ikJuDaSaSf/6JiLYLEYnW/+kXg1WRVJL/9EmQ1YZIsv/6Qzwy5qk7/+tEU0nkls3/zIUMPKNX/6yZLf+kFgAfgGyLFAUwY//uQZAUABcd5UiNPVXAAAApAAAAAE0VZQKw9ISAAACgAAAAAVQIygIElVrFkBS+Jhi+EAuu+lKAkYUEIsmEAEoMeDmCETMvfSHTGkF5RWH7kz/ESHWPAq/kcCRhqBtMdokPdM7vil7RG98A2sc7zO6ZvTdM7pmOUAZTnJW+NXxqmd41dqJ6mLTXxrPpnV8avaIf5SvL7pndPvPpndJR9Kuu8fePvuiuhorgWjp7Mf/PRjxcFCPDkW31srioCExivv9lcwKEaHsf/7ow2Fl1T/9RkXgEhYElAoCLFtMArxwivDJJ+bR1HTKJdlEoTELCIqgEwVGSQ+hIm0NbK8WXcTEI0UPoa2NbG4y2K00JEWbZavJXkYaqo9CRHS55FcZTjKEk3NKoCYUnSQ0rWxrZbFKbKIhOKPZe1cJKzZSaQrIyULHDZmV5K4xySsDRKWOruanGtjLJXFEmwaIbDLX0hIPBUQPVFVkQkDoUNfSoDgQGKPekoxeGzA4DUvnn4bxzcZrtJyipKfPNy5w+9lnXwgqsiyHNeSVpemw4bWb9psYeq//uQZBoABQt4yMVxYAIAAAkQoAAAHvYpL5m6AAgAACXDAAAAD59jblTirQe9upFsmZbpMudy7Lz1X1DYsxOOSWpfPqNX2WqktK0DMvuGwlbNj44TleLPQ+Gsfb+GOWOKJoIrWb3cIMeeON6lz2umTqMXV8Mj30yWPpjoSa9ujK8SyeJP5y5mOW1D6hvLepeveEAEDo0mgCRClOEgANv3B9a6fikgUSu/DmAMATrGx7nng5p5iimPNZsfQLYB2sDLIkzRKZOHGAaUyDcpFBSLG9MCQALgAIgQs2YunOszLSAyQYPVC2YdGGeHD2dTdJk1pAHGAWDjnkcLKFymS3RQZTInzySoBwMG0QueC3gMsCEYxUqlrcxK6k1LQQcsmyYeQPdC2YfuGPASCBkcVMQQqpVJshui1tkXQJQV0OXGAZMXSOEEBRirXbVRQW7ugq7IM7rPWSZyDlM3IuNEkxzCOJ0ny2ThNkyRai1b6ev//3dzNGzNb//4uAvHT5sURcZCFcuKLhOFs8mLAAEAt4UWAAIABAAAAAB4qbHo0tIjVkUU//uQZAwABfSFz3ZqQAAAAAngwAAAE1HjMp2qAAAAACZDgAAAD5UkTE1UgZEUExqYynN1qZvqIOREEFmBcJQkwdxiFtw0qEOkGYfRDifBui9MQg4QAHAqWtAWHoCxu1Yf4VfWLPIM2mHDFsbQEVGwyqQoQcwnfHeIkNt9YnkiaS1oizycqJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU="; new Audio(dataUrl).play(); }; </script> </body> </html> <div id="reader">にQRコードリーダーを表示し、<div id="result">に読み取った文字を画面に追記していきます。追記にはappend()を使います。 <script src="https://unpkg.com/html5-qrcode"></script>でhtml5-qrcodeを読み込みます。 CURRENT_TEXTは2度読み防止のための変数です。 onScanSuccess()はQRコードの読み取りが成功したときの処理です。CURRENT_TEXTと違う文字だった場合は、新しい読み取りです。append()で文字を追記し、beep()で音をならし、google.script.runで文字をバックエンドに伝えます。 HTMLAudioElementで音を鳴らす HTMLAudioElementで音を鳴らせます。音のデータをデータURLとして文字列にエンコードしておき、new Audio(dataUrl).play()で非同期に再生します。 google.script.runでバックエンドのAPIを呼び出す google.script.runを使って、バックエンドのAPIを非同期に呼び出します。withFailureHandler()で呼び出しに失敗したときの処理を書いています。 その他 GoogleスプレッドシートにQRコードを表示する 例えば、A1セルに入った文字をQRコードとして表示するには、次のように書きます。ウェブページにアクセスするためのQRコードを作るときには、ENCODEURL関数を使うようにしてください。 =IMAGE("https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl="&ENCODEURL(A1)) トラブルシューティング Q. スプレッドシートをコピーして流用したいのですが、アクセスできないようです。 所属組織のGoogle Workspaceの設定で、組織外のスプレッドシートへのアクセスが禁止されている場合があります。ブラウザーのシークレットモードだとアクセスできることがあります。お試しください。 Q. スクリプトを書き換えて、ブラウザーからアクセスし直しているのですが、書き換えた機能が反映されていないようです。 開発用と本番用のURLがあります。開発用は最新のスクリプトが反映されていますが、本番用だと再デプロイが必要です。 Q. バックエンドからフロントエンドに値を渡すよう、スクリプトを書き換えてみました。Stringだけなら渡せるのですが、objectだとうまく渡せずnullになってしまいます。 google.script.runを使ったやり取りですが、バックエンドから渡せる値に制限があります。特にエラーが出るわけではないので気が付きにくいです。特にDate型のやりとりが制限されていることにご注意ください。 Date型をstring型に変換するのに、Utilities.formatDate()が便利です。 ただし、著作権は放棄しません。また、このスクリプトによって発生したいかなる不利益についても責任を負うことはできません。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue3でアニメーション(Web Animations API)を使ってみた

こんにちはSadoです。今回はVue3で開閉処理を行う際に、「Web Animations API」(JavaScript)を使ってゆっくり開閉するアニメーションを作成してみます!純粋に宣言した CSS とは違って、Web Animations APIを用いる場合 はプロパティからアニメーション時間を動的に設定することが出来るみたいです。 Web Animations APIについて やること シンプルに開閉ボタンと開いたときに表示される要素を用意して、そこにアニメーションを付け加えていきたいと思います。 準備 表示用のファイル(親)とアニメーション処理を書くファイル(子)に分けておきます。Vue3.2の<script setup>構文を使用しております。CSSはTailwindを使用。 Index.vue <script setup lang="ts"> import Child from "./Child.vue"; import { ref } from "vue"; const open = ref(true); </script> <template> <Child :open="open" /> </template> Child.vue <script setup lang="ts"> import { ref } from "vue"; const props = defineProps<{ open: boolean; }>(); const open = ref(props.open); </script> <template> <section> <button class="bg-gray-300 rounded p-4 mb-5"> 開閉ボタン </button> // デフォルトで開いている状態 <p class="bg-blue-100 p-10">open</p> </section> </template> 開閉ボタンと開閉部分の要素を準備(デフォルトで開いておく) 実際にやってみる ここから開閉処理と、開閉したときのアニメーションを付け加えていきます。 開閉処理追加 ここでは、開閉処理をボタン要素をクリックすることで開閉が動作するように処理を追加。 Child.vue <script setup lang="ts"> import { ref } from "vue"; const props = defineProps<{ open: boolean; }>(); const open = ref(props.open); </script> <template> <section> + <button class="bg-gray-300 rounded p-4 mb-5" @click="open = !open"> 開閉ボタン </button> + // デフォルトで開いている状態 + <div v-if="open"> + <p class="bg-blue-100 p-10">open</p> + </div> + <div v-else></div> </section> </template> アニメーション追加 アニメーションを行いたい要素である、開閉部分のdivタグを<transition>で囲む。そして、@enterと@leaveをつけることでscript内に記述してある、enterとleaveのアニメーション処理が行われる。 Child.vue <script setup lang="ts"> import { ref } from "vue"; const props = defineProps<{ open: boolean; }>(); const open = ref(props.open); + // 開くとき + async function enter(el: Element, done: () => void) { + el.classList.add("overflow-hidden"); + + await el.animate( + [ + { + height: 0, + }, + { + height: `${(el as HTMLElement).offsetHeight}px`, + }, + ], + { + duration: 500, + easing: "ease-out", + } + ).finished; + + el.classList.remove("overflow-hidden"); + + done(); + } + + // 閉まるとき + async function leave(el: Element, done: () => void) { + el.classList.add("overflow-hidden"); + + await el.animate( + [ + { + height: `${(el as HTMLElement).offsetHeight}px`, + }, + { + height: 0, + }, + ], + { + duration: 100, + easing: "ease-out", + } + ).finished; + + el.classList.remove("overflow-hidden"); + + done(); + } </script> <template> <section> <button class="bg-gray-300 rounded p-4 mb-5" @click="open = !open"> 開閉ボタン </button> // デフォルトで開いている状態 + <transition @enter="enter" @leave="leave"> <div v-if="open"> <p class="bg-blue-100 p-10">open</p> </div> <div v-else></div> + </transition> </section> </template> height: ${(el as HTMLElement).offsetHeight}px,の部分では、開閉する際の要素の高さを取得しています。enter(開くとき)では開く際の要素の高さ(0px)から、今回用意したdiv要素の高さ分までを指定。 そして、duration: 500の指定によって、開いたときにゆっくりと開かれているのが確認できました。 Child.vue [ { height: 0, },   { height: `${(el as HTMLElement).offsetHeight}px`,  }, ],   { duration: 500, easing: "ease-out", }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

1分で地図を作成する【Leaflet.js】

Leafletとは LeafletとはオープンソースのJavaScriptライブラリで、 地図を描画することができる 公式リファレンス https://leafletjs.com/index.html 地図を描画する HTMLのheadタグ内でLeafletのcssとjsを読み込む <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"> <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script> HTMLのbodyタグ内に地図を描画するタグを追加 <body> <div id='mapcontainer'></div> </body> JavaScriptで地図を描画する // 画面読み込み時実行 window.onload = () => { // Leafletの地図オブジェクト作成 // 地図を表示するdiv要素のidを設定 const map = L.map('mapcontainer'); // 地図の中心緯度経度とズームレベルを指定 map.setView([35.40, 136], 5); // タイルレイヤ(国土地理院地図)の設定 const mapApi = "https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png"; const tilelayer = L.tileLayer(mapApi, { attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>" }) // タイルレイヤを地図に追加 tilelayer.addTo(map); }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

if文でもin演算子が使える JavaScript

if文でもin演算子が使用できます。 プロパティがオブジェクトにある場合にtrueを返します。 if ('property' in obj) { // 処理 } 例(修正前) const card = { type: 'mtg', color: 'white' }; console.log('type' in card); // 出力:true delete card.color; if ('color' in card === false) { card.color = 'black'; } console.log(card.color); // 出力:black コメントでご指摘をいただき修正した例 ifの条件でfalseと比較せずに、否定演算子を使う const card = { type: 'mtg', color: 'white' }; console.log('type' in card); // 出力:true delete card.color; if (!('color' in card)) { card.color = 'black'; } console.log(card.color); // 出力:black
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PlantUMLからmermeidへの変換器を作ってみた

モチベーション 最近GitHubが対応したことで一気に熱を帯びているmermeidだが、 PlantUMLからmermeidへの変換器が意外と落ちていなかった。 そこで、今回はPlantUMLからmermeidへの変換器を自作してみることにした。 (こちらの内容は自分のブログでも公開していますのでぜひご覧下さい。変換器はこちらでリリース予定です。) 言葉の説明 PlantUML コードベースで作画ができるツール。古くから使われていて慣れ親しんでいる人も多いはず。 ER図、シーケンス図など, 様々な図の作成ができる。 PlantUML mermeid mermeidはpumlと同様、テキストベースで作図ができるライブラリである。 表現力はPlantUMLに比べ少し劣る印象ではあるものの、 javascript製のライブラリであるため、PlantUMLに比べ環境構築が容易で、web系のツールと親和性も良さそう。 (NotionやGithubが対応したのもそういった背景があるのかもしれない。) mermeid PlantUMLはかなり表現力のあるツールなので、今回はまずER図に絞って変換器を実装してみることにした。 両者の構文の比較 Entityの変換 Entityの書き方に対して、両者を比較してみる。 PlantUML entity "Entity01" as e01 { *e1_id : number <<generated>> -- *name : text description : text } mermeid Entity01 { e1_id number name text description text } mermeidの場合、entityであることを宣言する必要がなく、カラムもタイプと命名の2つだけでかなりシンプルだ。 さらにentityに対して代替の命名ができないことも特徴(これがRelation変換部にも関わってくる。) Relationの変換 Entityと同じく両者を比較してみる PlantUML e01 ||..o{ e02 e01 |o..o{ e03 mermeid Entity01 ||--o{ Entity02 : places Entity01 |o--o{ Entity03 : places まず、mermeidにはentityに対して命名を割り当てる機構がないため 名付けが変わっている場合は元のものに置き換えなくてはならない。 またそれぞれのリレーションに対しては、説明を割り当てなくてはならない。 (上記でいうところの"places"の部分) さらに(細かな違いではあるが)リレーションに対して『..』は使えないようだ。 (PlantUMLの場合は破線が表現できる。) その他 mermeidの場合、erDiagramという文を一番最初に入れ、ER図であることを明示的に宣言しなくてはいけないようである。 (PlantUMLに比べ文法が簡潔になっているのもこのためだろう。) コード 完成したコードがこちら(今回はwebでの公開を意識してnodeで書いてみた) const fs = require('fs') const text = fs.readFileSync("puml.txt", 'utf8') const lines = text.toString().split('\n') let mermeid_text = 'erDiagram\n' entity_name_array = [] for (const line of lines) { if(line.includes('entity')){ // entity開始 const entity_name = line.split(' ')[1].replace(/[\"]/g,"") if(line.includes('as')){ //entityに対して命名がなされている場合 const entity_replaced_name = line.split('as')[1].replace('{',"").replaceAll(' ',"") entity_name_array.push([entity_name, entity_replaced_name]) } mermeid_text += `${entity_name} {\n` } else if (line.includes(':')){ // カラム columns = line.split(':').map((column) => column.trim()) column_text = '' if(columns.length == 1){ column_text = `string ${columns[0].replace('*', '')}` }else{ column_text = `${columns[0].replace('*', '')} ${columns[1].split(' ')[0]}` } mermeid_text += ` ${column_text}\n` } else if(line == '}'){ //entity終了 mermeid_text += '}\n' } else if (line.includes('--') && line.length <= 4){ // 不要なので無視 } else if (line.match(new RegExp('[o|}]{0,2}(..|--)[o|{]{0,2}')) != null) { //relation new_relation = `${line.replace('..', '--')} : places\n` entity_name_array.forEach((entity_name_el) => { new_relation = new_relation.replace(entity_name_el[1], entity_name_el[0]) }) mermeid_text += new_relation } else { mermeid_text += `${line.replace('..', '--')}\n` } } // 書き込み fs.writeFile("mermeid.txt", mermeid_text, (err) => { if (err) throw err console.log('正常に書き込みが完了しました') }); 今後やろうとしていること package文への対応 PlantUMLのER図ではpackageの作成ができるが、mermeidは未対応のよう。 したがってPlantUMLにpackageに関する文があった場合、それを取り除く必要がある。 が、ここまでくると1行ごとの文解析では間に合わないので、良い解析の仕方がないかを模索中。 webでの公開 上記の通りmermeidはwebとの親和性が高い。 せっかくなので、puml to mermeidの変換器をwebで公開してみようと思う。 ER図以外への対応 こちらも上述の通り、PlantUMLは ER図以外にもシーケンス図など様々な図をサポートしている。 余裕があればこれらの図に関しても変換器を作ってみたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Azure Functions - 共通コード管理(JavaScript)

フォルダー構成 フォルダー構成を参考に以下構成で実施 FunctionsProject │ ├─ HttpTrigger1/ │ ├─ function.json │ ├─ index.js │ └─ sample.dat │ ├─ SharedCode/ │ └─ util.js │ ├─ node_modules/ ├─ .funcignore ├─ .gitignore ├─ host.json ├─ local.settings.json ├─ package.json └─ package-lock.json 共通コードの実装 SharedCode/util.js module.exports = { testFunction: (context, param) => { context.log(param); }, }; 共通コードの使用 HttpTrigger1/index.js const util = require('../SharedCode/util'); module.exports = async function (context, req) { util.testFunction(context, 'hello world'); };
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Azure Functions - 共通コード管理

フォルダー構成 フォルダー構成を参考に以下構成で実施 FunctionsProject │ ├─ HttpTrigger1/ │ ├─ function.json │ ├─ index.js │ └─ sample.dat │ ├─ SharedCode/ │ └─ util.js │ ├─ node_modules/ ├─ .funcignore ├─ .gitignore ├─ host.json ├─ local.settings.json ├─ package.json └─ package-lock.json 共通コードの実装 SharedCode/util.js module.exports = { testFunction: (context, param) => { context.log(param); }, }; 共通コードの使用 HttpTrigger1/index.js const util = require('../SharedCode/util'); module.exports = async function (context, req) { util.testFunction(context, 'hello world'); };
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Google Apps Script】FileIteratorとFolderIteratorをiterableにして使いやすくする

結論 最初に結論を言います。 これを const getGoogleFiles = () => { const files = DriveApp.getFiles(); const result = []; while (files.hasNext()) { const file = files.next(); const fileName = file.getName().toLowerCase(); if (fileName.includes('google')) result.push(file.getUrl()); } Logger.log(result); }; wrapper関数を1つ噛ませてこう書く方が好きという話です。 const getGoogleFiles = () => { const result = gasUtils.toIterable( DriveApp.getFiles(), { mapFn: (file) => file.getUrl(), filterFn: (file) => file.getName().toLowerCase().includes('google'), } ); Logger.log([...result]); ちなみにwrapper関数toIterableは以下です。 const gasUtils = { /** * DriveAppのFileIteratorとFolderIteratorをiterableにする関数 * @template {DriveApp.FileIterator | DriveApp.FolderIterator} GASIterator * @template {DriveApp.File | DriveApp.Folder} GASFileOrFolder * @param {GASIterator} iter - Google Apps Scriptのイテレータ * @param {{ * mapFn: (val: GASFileOrFolder, index: number) => any * filterFn: (val: GASFileOrFolder, index: number) => boolean * }} * @return {Generator} */ toIterable: (iter, { mapFn = (val) => val, filterFn = () => true, }) => { return { *[Symbol.iterator]() { let i = 0; while (iter.hasNext()) { const [nv, ni] = [iter.next(), i++]; if (filterFn(nv, ni)) yield mapFn(nv, ni); } } }; }, }; ご興味があればどぞ。 はじめに Google Apps Scriptは便利です。 ちょっとしたツールや自動化にはもってこいです。 Google系のサービスとは簡単に連携できます。 そんなわけで、Google Drive上のファイルやフォルダを操作したくなるわけです。 マイドライブの中でファイル名にgoogleの文字が入るもののURLの一覧が取得したい…! それもSpreadsheetに一発で貼り付けられるように配列で取得したい…!! 調べてみればDriveAppなるものが簡単に見つかります。 早速試してみましょう。 /** !!これは動きません!! */ const getGoogleFiles = () => { // ファイルを全て取得 const files = DriveApp.getFiles(); // 名前に'google'を含むファイルのURLを取得 const result = files.flatMap(file => { const fileName = file.getName().toLowerCase(); return fileName.includes('google')? [file.getUrl()] : []; }); // 出力を確認 Logger.log(result); }; これでエディタからgetGoogleFilesを実行すればめでたく目標のものがとれるはず。 とっても簡単だぁ。 と思いきや、帰ってくるのはエラー。 TypeError: files.flatMap is not a function な、なんだってー?! それもそのはず、DriveApp.getFilesの返り値の型はDriveApp.FileIterator。 配列じゃないのです。イテレータなのです。 というわけで、目的の結果を得るには次のように書く必要があります。 const getGoogleFiles = () => { // ファイルを全て取得 const files = DriveApp.getFiles(); // 名前に'google'を含むファイルのURLを取得 const result = []; while (files.hasNext()) { const file = files.next(); const fileName = file.getName().toLowerCase(); if (fileName.includes('google')) result.push(file.getUrl()); } // 出力を確認 Logger.log(result); }; うーん。なんともコレジャナイ感。 何が不満なのか 個人的な嗜好…もとい志向として以下が気に入りません 最初に空の配列を定義してwhileループでpushするのがつらい。破壊的メソッドは滅ぶべし 終端判定のメソッドがhasNextなところ。MDNのイテレータの説明ではdoneなのに iterableじゃない。反復可能ならスプレッド構文なんかで簡単に配列化できるのに といわけで、iterable(反復可能)にするwrapper関数を挟めば使い勝手が良くなりそう。 どちらにせよwrapper関数はあった方が将来的な保守性が上がるので、やらない手はない。 余談 反復可能にする。どうやって? (ご存じの方は飛ばしてください) JavaScriptのオブジェクトには2種類ある。 反復可能なオブジェクトと、反復可能ではないオブジェクトだ。 その違いは、反復可能なオブジェクトとは@@iteratorメソッドが実装されているか否かです。 // 反復可能じゃない const notIterable = { hoge: 'hoge', fuga: 'fuga', piyo: 'piyo', }; // 反復可能 const iterable = { [Symbol.iterator]: function* () { yield 'hoge'; yield 'huga'; yield 'piyo'; } }; // 省略記法 iterableと意味は同じ const shortIterable = { *[Symbol.iterator]() { yield 'hoge'; yield 'huga'; yield 'piyo'; } }; この*がついている厳めしい関数はジェネレータと呼ばれるものです。 肩肘張るようなものでもなく、yieldで値を返して実行を一旦停止する関数といった感じ。 重要なのはこのSymbol.iteratorという名前のメソッドがジェネレータで実装されていること。 これが反復可能か、そうでないかの分かれ道です。 とにかく、この名前のジェネレータのメソッドを持っていればそのオブジェクトは反復可能です。 では反復可能だと何が嬉しいの?といったところですが、for-ofやスプレッド構文で各値を取り出すことができます。 // for-ofで各値を取り出せる for (const item of iterable) { Logger.log(item); // expected output: // hoge // huga // piyo } // スプレッド構文で各値を取り出せる Logger.log([...iterable]); // expected output: // [hoge, huga, piyo] // for-ofで取り出せない for (const item of notIterable) { Logger.log(item); // TypeError: notIterable is not iterable // 地味にこのエラーメッセージ面白い } 察しの良い方はお気づきでしょうが、Arrayオブジェクトなんかには元々この@@iteratorメソッドが実装されています。 とっても便利だぁ! ともあれこの@@iteratorメソッドがApps Scriptのイテレータ、厳密にいうとDriveApp.FileIteratorやDriveApp.FolderIteratorといったオブジェクトに実装されていないことが問題なわけです。 ならどうすればいいか? Apps Scriptのイテレータの各値をyieldするジェネレータを@@iteratorメソッドとして持ったオブジェクトを返す関数があれば必要十分ですね。 そう考えると案外簡単かもしれない。 Simple is best 複雑なのは良くないので、できるだけシンプルに反復可能性だけ付与します。 // Apps Scriptのイテレータ類をiterableにする const toIterable = (iter) => { return { *[Symbol.iterator]() { while(iter.hasNext()) yield iter.next(); } }; }; const getGoogleFiles = () => { const files = toIterable(DriveApp.getFiles()); const result = [...files].flatMap(file => { const fileName = file.getName().toLowerCase(); return fileName.includes('google')? [file.getUrl()] : []; }); Logger.log(result); }; うむ。悪くない。 大分最初の姿に近づくことができました。 wrapper関数のコード量もそれほど多くなく、必要十分にシンプルだと思います。 ただ、ちょっと寄り道が多すぎるかもしれない。 特に、一旦配列にしてからflatMapするところ。 ものごとはできるかぎりシンプルにすべきだ。しかし、シンプルすぎてもいけない。 先程の実装はスタート地点としてはこれ以上なく優秀でした。 しかし、もう少しwrapper関数に機能を追加すれば格段に便利かもしれません。 例えば、次のようなmapFnとfilterFnを追加してみるのはどうでしょう。 // Apps Scriptのイテレータ類をiterableにする const toIterable = (iter, { mapFn = (val, index) => val, filterFn = (val, index) => true, }) => { return { *[Symbol.iterator]() { let i = 0; while (iter.hasNext()) { const [nv, ni] = [iter.next(), i++]; if (filterFn(nv, ni)) yield mapFn(nv, ni); } } }; }; const getGoogleFiles = () => { const result = toIterable( DriveApp.getFiles(), { mapFn: (file) => file.getUrl(), filterFn: (file) => file.getName().toLowerCase().includes('google'), } ); // これまでと同じ結果 Logger.log([...result]); }; filterFnで条件を満たしたものだけyieldされ、mapFnの処理を通したものを取得します。 それほどwrapper関数のコード量も増えず、かつ本関数内の変数がresult1つになります。 ループの回数も減るので、気持ちが穏やかになります。 ちなみにこのコード内では使用していませんが、mapFnとfilterFnの二番目の引数でインデックスも使えるようにしています。こんなんなんぼあってもいいですからね。 個人的にはこれが一番使いやすいと思います。 getGoogleFilesの中の処理の見通しもこれが一番良く感じます。 JSDocで補完が効くようしたり Apps ScriptではJSDocを利用してエディタの補完を効かせるようにすることができます。 よって以下のようにすればmapFnやfilterFnの中でもきちんと補完が効くようになります。 const gasUtils = { /** * DriveAppのFileIteratorとFolderIteratorをiterableにする関数 * @template {DriveApp.FileIterator | DriveApp.FolderIterator} GASIterator * @template {DriveApp.File | DriveApp.Folder} GASFileOrFolder * @param {GASIterator} iter - Google Apps Scriptのイテレータ * @param {{ * mapFn: (val: GASFileOrFolder, index: number) => any * filterFn: (val: GASFileOrFolder, index: number) => boolean * }} * @return {Generator} */ toIterable: (iter, { mapFn = (val) => val, filterFn = () => true, }) => { return { *[Symbol.iterator]() { let i = 0; while (iter.hasNext()) { const [nv, ni] = [iter.next(), i++]; if (filterFn(nv, ni)) yield mapFn(nv, ni); } } }; }, }; もちろん関数の動作は変わりません。 が、しれっとgasUtilsというオブジェクトで包みました。 なので、getGoogleFilesのtoIterableの参照をgasUtils.toIterableに変えてください。 ちなみにこれをすると 名前空間が切れる エディタの実行関数候補からtoIterableが消える という効用があります。 エディタから実行する予定がない関数は積極的にオブジェクトで包みましょう。 みなが幸せになります。 おわりに 大体伝えたかったことは以上です。 強いて言うなら速度比較とかもしたかったですが、とりあえずここまでで一旦投稿しておきます。 よろしくお願いします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

three.jsのGLTFLoader.jsのimportに失敗したときの解決備忘録

概要 2022年2月22日時点の現象 こちらのDownloadからZipダウンロードしたファイル群を下のように展開 ディレクトリ構造 root └ index.html └ index.js └ three.js-master/three.js-master └ build/three.module.js └ examples/jsm/loaders/GLTFLoader.js GLTFLoader.jsをimport index.js import * as THREE from './three.js-master/three.js-master/build/three.module.js' import {GLTFLoader} from './three.js-master/three.js-master/examples/jsm/loaders/GLTFLoader.js' サーバー立ち上げ npm start —force localhost:8080を確認するもエラー Uncaught TypeError: Failed to resolve module specifier "three". Relative references must start with either "/", "./", or "../" 解決法 GLTFLoader.jsの64行目付近の from文 元々はこちら GLTFLoader.js //中略 VectorKeyframeTrack, sRGBEncoding } from 'three'; //中略 from 'three'; の部分をfrom '../../../build/three.module.js';に変更 GLTFLoader.js //中略 VectorKeyframeTrack, sRGBEncoding } from '../../../build/three.module.js'; //中略 これで解決しました。 追記 (おそらくこちらの方が正着?) @o_obさんからTwitterでコメントをいただきました。(ありがとうございます!) 参考にした結果以下の書き方でも解決しました。 GLTFLoader.jsは元のまま GLTFLoader.js //中略 VectorKeyframeTrack, sRGBEncoding } from 'three'; //中略 エラー時のhtml index.html <html lang="ja"> <head> <meta charset="utf-8"/> <meta http-equiv="X-UA-Compatible"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>3D Model Loader</title> </head> <body> <canvas class="webgl" id="myCanvas"></canvas> <script src="index.js" type="module"></script> </body> </html> 修正後のhtml index.html <html lang="ja"> <head> <meta charset="utf-8"/> <meta http-equiv="X-UA-Compatible"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>3D Model Loader</title> <!-- 追加 --> <script type="importmap"> { "imports": { "three": "./three.js-master/three.js-master/build/three.module.js" } } </script> <!-- ここまで --> </head> <body> <canvas class="webgl" id="myCanvas"></canvas> <script src="index.js" type="module"></script> </body> </html> importMapとは 気になったので調べました。 「JavaScript の実際のファイル名」と「そのファイルを import するときに指定する名前」を切り離すことができます(2つの対応情報はJSON形式で用意しておきます)。 これにより、ファイル名が後から変わっても(ファイル名にバージョン番号が含まれている場合など)、一度記述した import 文を変更する必要はなくなります。(JSON形式による対応情報側のみを変えます) npm のパッケージ名のようなシンプルな名前でインポートできます。 静的インポート、動的インポートのどちらでも使えます。 とのこと。 これにより threeという名まえを使えるようになるということですね。 ちなみに、three.jsのサンプル実装では webgl_loader_gltf.html <!-- Import maps polyfill 古いブラウザーでも動かすためのコード --> <!-- Remove this when import maps will be widely supported この1行はimport mapが普及したら削除してよい --> <script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script> <script type="importmap"> { "imports": { "three": "../build/three.module.js" } } </script> とありました。こちら確認しておけば...! 補足議論 おそらく、three.js開発者はnode_modulesによるinstall想定だったため、今回のようなエラーが発生したのでは...?など予想しています。(詳しい人教えてください。)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PNG(8ビットカラーパレット形式)を使ってJavaScriptでパレットアニメーションさせてみた

はじめに 昔、絵本型のカートリッジの幼児向けゲームを作るお仕事をしているときに、パレットを操作してアニメーションなんかをさせてました。 主に点滅させたり、フェードアウトさせたりなんですが・・・ それをWebでできないかなー?あわよくばゲームで使えないかなー?というのがきっかけです。 とりあえず動画のようにパレットの操作ができたので書いておきます。 やってることの概要 やってることはそんなに難しくありませんが、ちょっと面倒です。 PNGをバイナリとして読み込み保持します。 PNGが8bitカラーでかつパレットを持っているか確認します 読み込んだPNGのパレットを書き換えて、dataURLに変換します。 ImageオブジェクトにdataURLをセットします。 その結果をCanvasに描画します 要はPNGを書き換えてるだけです。 詳細 3つに分けて解説します。 PNGの読み込み パレットの操作と描画 パレットアニメーションさせる 1.PNG画像の読み込み まずはPNGのフォーマットがどうなっているかがわからないといけなので、PNG ファイルフォーマットを参考に必要な部分だけ確認。 今回はPNG ファイルシグネチャとIDHRチャンク、PLTEチャンクまでわかればOKということが分かった。 という訳でPNGを読み込みます。 読み込んだPNGファイルはUint8Array型にします。 let response = await fetch(url); if(response.ok){ let data = await response.arrayBuffer(); var buff = new Uint8Array(data); 読み込んだら、PNGファイルシグネチャと一致するかを確認します。 // PNGファイルシグネチャ const PNG_SIGNATURE = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); ・・・中略・・・ let chkData = buff.slice(0, 8); if(JSON.stringify(PNG_SIGNATURE) != JSON.stringify(chkData)){ console.log("[" + url + "] : not png"); return; } 読み込んだバッファから8バイト取り出し、あらかじめ宣言しておいた定数と比較。 ほかに良い方法があるのかもしれませんが、とりあえず簡単に比較するためにJSONに変換して比較してます。 当然ですが、一致しない場合はPNGファイルとはみなしません。 次にビット深度とカラータイプをチェックします。 ビット深度が8なら8ビットカラー、256色ですね。 カラータイプが3ならパレットを使っています。 24バイト目がビット深度、25バイト目がカラータイプになります。 これらも用意した定数と比較します。 /** ビット深度とカラータイプ */ const IMAGE_FORMAT = new Uint8Array([0x08, 0x03]); /** ビット深度の位置 */ const BIT_DEPTH_POS = 24; ・・・中略・・・ let fmtData = buff.slice(BIT_DEPTH_POS, BIT_DEPTH_POS + 2); if(JSON.stringify(IMAGE_FORMAT) != JSON.stringify(fmtData)){ console.log("[" + url + "] : not support format"); return; } 一致しない場合は、対応するPNGの形式ではないと判断します。 8ビットカラーでパレットがあることはわかったので、PLETチャンクの長さを取得します。 8ビットカラーなので長さも決まっているので、定数との比較になります。 /** PLTEチャンク のサイズ */ const PLTE_SIZE = new Uint8Array([0x00, 0x00, 0x03, 0x00]); /** パレットデータの長さの位置 */ const PALETTE_DATA_LENGTH_POS = 33; ・・・中略・・・ let lenData = buff.slice(PALETTE_DATA_LENGTH_POS, PALETTE_DATA_LENGTH_POS + 4); if(JSON.stringify(PLTE_SIZE) != JSON.stringify(lenData)){ console.log("[" + url + "] : Palette size faild"); return; } ここでも一致しない場合は対応するデータではないと判断します。 次にChunk Typeを読み込んで定数と比較します。 /** PLTEチャンク名 */ const PLTE_CHUNKTYPE = new Uint8Array([0x50, 0x4c, 0x54, 0x45]); /** パレットのChunk Typeの位置 */ const PALETTE_CHUNK_TYPE_POS = 37; ・・・中略・・・ let chunkTypePLT = buff.slice(PALETTE_CHUNK_TYPE_POS, PALETTE_CHUNK_TYPE_POS + 4); if(JSON.stringify(PLTE_CHUNKTYPE) != JSON.stringify(chunkTypePLT)){ console.log("[" + url + "] : Palette data not found"); return; } ここでも一致しない場合は対応するデータではないと判断します。 ここでChunk Typeも確認できたので、PLETチャンク確定とみなし、PLET Chunkを取得します。 /** PLTEのサイズ */ const PLT_LENGTH = 0x0300; /** パレットのChunk Typeの位置 */ const PALETTE_CHUNK_TYPE_POS = 37; ・・・中略・・・ var paletdata = buff.slice(PALETTE_CHUNK_TYPE_POS, PALETTE_CHUNK_TYPE_POS + 4 + PLT_LENGTH); 4を足しているのはCRCも含んでいるためです。 4の位置がよくないのと、定数にしてわかりやすくしたほうがいいですね^^; ここまで終わったら、読み込んだデータを保持します。 let binStr = Array.from(buff, e => String.fromCharCode(e)).join(""); let dataUrl = "data:image/png;base64,"+btoa(binStr); let img = new Image(); img.src = dataUrl; imgMap[name] = { "buff":buff, "image" : img, "palette" : paletdata }; Imageオブジェクトを作成し、読み込んだPNGのデータをdataURLにして設定します。 そして、PNGのバイトデータ、Imageオブジェクト、パレットを保持します。 このデータ構成はいまだに悩み中。 名前付きで保持しているは、複数のPNGを扱うことを想定している為です。 2.パレットの操作と描画 PNGの描画は以下のようなメソッドを用意しています。 簡単に説明すると、Canvasを白で塗りつぶして、imgIdxの示すキーを使って保持しているPNGのImageオブジェクトをCanvasに描画しています。 function viewImage(){ let canvas = document.getElementById('canvas'); let ctx = canvas.getContext('2d'); ctx.mozImageSmoothingEnabled = false; ctx.webkitImageSmoothingEnabled = false; ctx.msImageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false; ctx.fillStyle = 'white'; ctx.fillRect(0, 0, canvas.clientWidth, canvas.clientHeight); let keys = Object.keys(imgMap); let img = imgMap[keys[imgIdx]].image; ctx.drawImage(img, 0, 0, img.width * 4, img.height * 4); } さて、本題のパレット操作です。 パレット操作を行うメソッドは以下のものになります。 /** * パレットの更新 * @param image Imageオブジェクト * @param buff 画像データ配列 * @param palette パレットのChunk TypeとChunk Data */ async function updatePalette(image, buff, palette){ // CRC32を計算 let crc = calcCRC32(palette); // バッファにパレットを適用 buff.set(palette, PALETTE_CHUNK_TYPE_POS); //CRC32を適用 buff.set(crc, PALETTE_DATA_POS + PLT_LENGTH); // バッファをバイナリ文字列に変換 let binStr = Array.from(buff, e => String.fromCharCode(e)).join(""); // dataURLの生成 let dataUrl = "data:image/png;base64,"+btoa(binStr); // Promiseでやる必要性があるかどうかはわからないけど // 試したいことがあったのでこんなことしてる。 let imgPromise = new Promise((resolve, reject) =>{ image.onload = () => { resolve(); } /* // 遅延ローディングはしない(意味はあまりない) image.loading = 'eager'; // 同期的な読み込み(意味はあまりない) image.decoding = 'sync'; */ image.src = dataUrl; }).then(() => { viewImage(); }); } CRCも求めているのでそのメソッドも書いておきます。 function calcCRC32(buff){ let crcTarget = Array.from(buff, e => String.fromCharCode(e)).join(""); var crc = CRC32.bstr(crcTarget) >>> 0; var crcBuff = [ (crc & 0xff000000) >>> (8 * 3), (crc & 0x00ff0000) >>> (8 * 2), (crc & 0x0000ff00) >>> (8 * 1), (crc & 0x000000ff) ]; return new Uint8Array(crcBuff); } CRCはcrc-32を使っています。 jsファイルCDNで定義されたものを利用しています。 <script src="https://cdnjs.cloudflare.com/ajax/libs/crc-32/1.2.1/crc32.min.js"></script> さて、パレットを操作するメソッドupdatePalette()の引数ですが、Imageオブジェクトと、パレットを適用するPNGのバイト配列、そして操作するPLETチャンクのバイト配列になります。 引数のパレットのCRCを求める必要があるので、calcCRC32()メソッドで求めておきます。 その後、PNGのバイト配列に、パレットとCRCを適用します。 // CRC32を計算 let crc = calcCRC32(palette); // バッファにパレットを適用 buff.set(palette, PALETTE_CHUNK_TYPE_POS); //CRC32を適用 buff.set(crc, PALETTE_DATA_POS + PLT_LENGTH); パレット適用はこれで終わりです。 あとは、適用したPNGのバイト配列をImageオブジェクトに適用しCanvasに書き込めば終わりです。 // バッファをバイナリ文字列に変換 let binStr = Array.from(buff, e => String.fromCharCode(e)).join(""); // dataURLの生成 let dataUrl = "data:image/png;base64,"+btoa(binStr); // Promiseでやる必要性があるかどうかはわからないけど // 試したいことがあったのでこんなことしてる。 let imgPromise = new Promise((resolve, reject) =>{ image.onload = () => { resolve(); } /* // 遅延ローディングはしない(意味はあまりない) image.loading = 'eager'; // 同期的な読み込み(意味はあまりない) image.decoding = 'sync'; */ image.src = dataUrl; }).then(() => { viewImage(); }); ここでPromiseを使っているのですが、ImageオブジェクトのsrcにdataURLを指定して、ロードが終わるまで非同期なんです。 Promiseを使ったらどうにかならないか?と苦戦した残骸になります。 この辺の知識が浅すぎるので何とかしたい! なので、現状ではPromise使わなくても純粋にImageオブジェクトのonLoadイベントで、Canvasに描画するメソッドviewImage()を呼び出すだけでいいはずです。 不十分なところはありますが。、これでパレットアニメーションさせる準備が整いました。 3.パレットアニメーションさせる 今回操作する画像はこちらです。 ちなみに、TAKABO SOFTさんのEDGEというドット絵を描くツールで作っています。 20年くらい使っています。 アニメーションは以下のメソッドで行います。 var anicnt = 0; function animation(){ try{ let keys = Object.keys(imgMap); let image = imgMap[keys[imgIdx]].image; let buff = imgMap[keys[imgIdx]].buff; let palette = imgMap[keys[imgIdx]].palette; // パレットアニメーション用のパレット情報 let animePlt = [ new Uint8Array([ 0, 0, 225, 72, 72, 225, 103, 103, 225, 162, 162, 225, ]), new Uint8Array([ 162, 162, 225, 0, 0, 225, 72, 72, 225, 103, 103, 225, ]), new Uint8Array([ 103, 103, 225, 162, 162, 225, 0, 0, 225, 72, 72, 225, ]), new Uint8Array([ 72, 72, 225, 103, 103, 225, 162, 162, 225, 0, 0, 225, ]), ]; let idx = Math.floor(anicnt / 20); palette.set(animePlt[idx], 10 * 3 + 4); anicnt++; if(anicnt >= 20 * 4){ anicnt = 0; } updatePalette(image, buff, palette); }catch(e){ console.log(e); } window.requestAnimationFrame(animation); } まず最初に、必要なPNGの情報を取得します。 let keys = Object.keys(imgMap); let image = imgMap[keys[imgIdx]].image; let buff = imgMap[keys[imgIdx]].buff; let palette = imgMap[keys[imgIdx]].palette; 次に更新するパレットの情報を定義しています。 let animePlt = [ new Uint8Array([ 0, 0, 225, 72, 72, 225, 103, 103, 225, 162, 162, 225, ]), new Uint8Array([ 162, 162, 225, 0, 0, 225, 72, 72, 225, 103, 103, 225, ]), new Uint8Array([ 103, 103, 225, 162, 162, 225, 0, 0, 225, 72, 72, 225, ]), new Uint8Array([ 72, 72, 225, 103, 103, 225, 162, 162, 225, 0, 0, 225, ]), ]; この情報はパレットの10番目の色から4色分の情報になります。 具体的には赤枠で囲った部分を今回描き替えます これを一定のタイミングでパレットに適用します。 パレットの変更は以下のコードです。 カウンタから適用するパレットを求め、そのパレットを適用しています。 描き替えはUint8Arrayのsetメソッドで行います。 10 * 3+4の3は3バイトを表し、3バイトはRGBを意味します。10は10パレット目になります。 4はChunk TypeのPLETのサイズになります。 let idx = Math.floor(anicnt / 20); palette.set(animePlt[idx], 10 * 3 + 4); anicnt++; if(anicnt >= 20 * 4){ anicnt = 0; } updatePalette(image, buff, palette); メソッドはwindow.requestAnimationFrame()メソッドで一定間隔で呼び出すようにしています。 window.requestAnimationFrame(animation); これで、パレットを書き換えてアニメーションさせています。 最後に ゲームなんかで使う場合はさらに工夫が必要になると思います。 例えば、描画用のPNGを用意して、そこにいろいろ書き込んで最後にパレットを適用して、最後にCanvasに描画することで無駄を省くなどです。 あと、どうしてもImageオブジェクトに適用する際にコストがかかるので、ここも問題になるかもしれません。 アドベンチャーやRPGならいいかもしれませんが、アクションやシューティングには向かないかもしれません。 とはいえ、自力で何とかするというロマンがあるので、作成中のRPGで使いたいと考えています。 (そして、RPGの進捗は牛歩のごとく遅いです・・・) ソース GitHubからダウンロードできます 参考 PNG ファイルフォーマット cec32のCDN 高機能ドット絵エディタ EDGE
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript の関数即時呼び出しでは関数文を関数式と解釈させる

勘違いしていたこと JavaScript で関数を即時呼び出しするときに、以下の (A) だと SyntaxError になるので (B) のように括弧で囲みます。 // (A) SyntaxError: Function statements require a function name function() { ... }() // (B) OK (function() { ... })() この「構文エラーになるので括弧で囲む」の理由を勘違いしていました。 関数文を関数式にするために括弧で囲む 結合順を制御するために括弧で囲むのだと思い込んでいたのですが、正しくは、関数文(function declaration / function statement)ではなく関数式(function expression)だと解釈させるためでした。 誤った理解 function(){...}() は function() と {...}() だと解釈されるため SyntaxError になる。結合順を制御するために (function(){...})() と括弧で囲むことで function(){...} と () だと解釈されるようになり、SyntaxError にならない。 正しい理解 function(){...}() は関数文だと解釈される。関数名が書かれていないので SyntaxError になる。最後の () はあっても無くても、同じ理由で SyntaxError になる。 グループ化演算子で関数文を関数式にする 「括弧で囲む」の「括弧」とは、グループ化演算子(grouping expression)のことです。 グループ化演算子 - JavaScript | MDN グループ化演算子は (1 + 2) * 3 のように式の中の評価順を制御します。function は文脈によって文と式のどちらかに解釈されますが、グループ化演算子の中に置くことで式だと解釈されます。 以下のコードはどちらも同じ結果となります。 (function(){...})() (function(){...}()) minify すると !function(){...}() になる理由 (function(){...})() を minify すると !function(){...}() になることがあります1。 // minify 前 (function(){...})() // minify 後 !function(){...}() 単項演算子(論理否定演算子)である ! を用いて function(){...}() を関数式だと解釈させています。また、グループ化演算子を用いる(括弧で囲む)よりも 1 文字少ないです。minify するなら単項演算子が良いということですね。 関数文を関数式と解釈させる方法いろいろ ここまでの内容で、関数文を関数式だと解釈させることができれば良いことが分かりました。 結局のところ、式の中に function(){...} を入れるだけなので、いくつもの実現方法を考えることができます。 // グループ化演算子 (function(){...})() (function(){...}()) // 配列リテラル [function(){...}()] // 単項演算子 !function(){...}() +function(){...}() -function(){...}() ~function(){...}() // void, delete, typeof も単項演算子 void function(){...}() delete function(){...}() typeof function(){...}() // 変数代入 x=function(){...}() // カンマ演算子 1,function(){...}() // テンプレートリテラル `${function(){...}()}` アロー関数式の場合 アロー関数式(arrow function expression)の場合も、即時呼び出しするには括弧で囲む必要があります。 // SyntaxError: Unexpected token '(' () => {...}() // OK (() => {...})() ただし、アロー関数式は名前の通り式です。function のように文脈によって文になったり式になることは無いため、即時呼び出しする方法にバリエーションはありません。 「なることがあります」と断定的な言い方をしない理由は、minify ツールに依存した結果であり、他の選択肢もあるためです。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む