- 投稿日:2020-03-20T23:46:02+09:00
Electronでカレンダーを作る③
前回からの続き
前回はカレンダーの中身が動的に表示されるようにした。
表示する月を変えられるようにする
先月、来月に切り替えられるようにしたい。
先月と来月切り替えボタンを配置する。
index.html<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ElectronCalendar</title> <script text="text/javascript" src="./js/index.js"></script> <link rel="stylesheet" href="./css/index.css"> </head> <body> <div class="calendar-wrapper"> <div class="preMonthButton"> <button type="button" id="preMonth"><先月</button> </div> <div class="nextMonthButton"> <button type="button" id="nextMonth">来月></button> </div> <table id="table" class="calendar"> <caption id="caption"></caption> <thead> <tr> <th>日</th> <th>月</th> <th>火</th> <th>水</th> <th>木</th> <th>金</th> <th>土</th> </tr> </thead> <tbody> <tr id="row1"> <td id="cell1"></td> <td id="cell2"></td> <td id="cell3"></td> <td id="cell4"></td> <td id="cell5"></td> <td id="cell6"></td> <td id="cell7"></td> </tr> <tr id="row2"> <td id="cell8"></td> <td id="cell9"></td> ・・・略 <td id="cell33"></td> <td id="cell34"></td> <td id="cell35"></td> </tr> <tr id="row6"> ※ <td id="cell36"></td> <td id="cell37"></td> <td id="cell38"></td> <td id="cell39"></td> <td id="cell40"></td> <td id="cell41"></td> <td id="cell42"></td> </tr> </tbody> </table> </div> </body> </html>※カレンダーに6行目を追加している。
index.js'use strict'; const moment = require("moment"); window.onload = function() { createCallendar(moment()); //先月ボタン押下時 document.getElementById('preMonth').onclick = function() { createCallendar(moment(this.value)); } //来月ボタン押下時 document.getElementById('nextMonth').onclick = function() { createCallendar(moment(this.value)); } } /** * カレンダーを表示する。 * @param momentオブジェクト */ const createCallendar = function(localMoment) { //captionを表示 document.getElementById('caption').innerText = localMoment.format("YYYY年MM月"); //カレンダー初期化 clearCallendar(); //当月の日数を取得 const daysOfMonth = localMoment.daysInMonth(); //月初の曜日を取得(index.htmlと合わせるために+1する) const firstDayOfManth = localMoment.startOf('month').day() + 1; //カレンダーの各セルに日付を表示させる let cellIndex = 0; for(let i = 1; i < daysOfMonth + 1; i++) { if(i === 1) { cellIndex += firstDayOfManth; } else { cellIndex++; } document.getElementById("cell" + cellIndex).innerText = i; } //6行目の第1セルが空白なら6行目自体を非表示にする。 if(document.getElementById("cell36").innerText === "") { document.getElementById('row6').style.visibility = "hidden"; } //先月 document.getElementById('preMonth').value = localMoment.add(-1,'month').format("YYYY-MM"); //来月(先月のmomentオブジェクトとなっているので+2ヶ月) document.getElementById('nextMonth').value = localMoment.add(2,'month').format("YYYY-MM"); } /** * カレンダーを初期化する */ const clearCallendar = function() { //6行目を表示させておく if(document.getElementById('row6').style.visibility === "hidden") { document.getElementById('row6').style.visibility = "visible"; } for(let i = 1; i < 43; i++) { document.getElementById("cell" + i).innerText = ""; } }6行目は表示しない場合がある(表示しない場合の方が多い)ので、表示しない場合は隠す。
$ electron .とりあえず出来たけどボタンがダサい。
TODO
・月を切り替えた場合、同一html内の表示内容の切り替えとなっているため、いちいち初期化が必要で微妙。新しく画面を読み込むようにしたい。
・ボタンの見た目をナウい感じにしたい。あとがき
Electronの記事じゃ無く、HTMLとJSの記事みたいになってる・・・。
- 投稿日:2020-03-20T22:27:20+09:00
レスポンシブにしたのに、なぜかなっていない。そんな時の確認。
- 投稿日:2020-03-20T17:14:41+09:00
Electronでカレンダーを作る②
前回からの続き
前回は画面まで作成した。
カレンダーの内容を動的に表示する処理を作成していく。カレンダーの中身を作っていく
日付操作を楽にするためmomentモジュールを使う。
$ npm install -D momentindex.htmlを修正していく。
index.html<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ElectronCalendar</title> <script text="text/javascript" src="index.js"></script> <link rel="stylesheet" href="index.css"> </head> <body> <div class="calendar-wrapper"> <table class="calendar"> <caption id="caption"></caption> <thead> <tr> <th>日</th> <th>月</th> <th>火</th> <th>水</th> <th>木</th> <th>金</th> <th>土</th> </tr> </thead> <tbody> <tr> <td id="cell1"></td> <td id="cell2"></td> <td id="cell3"></td> <td id="cell4"></td> <td id="cell5"></td> <td id="cell6"></td> <td id="cell7"></td> </tr> ・・・略 <td id="cell27"></td> <td id="cell28"></td> </tr> <tr> <td id="cell29"></td> <td id="cell30"></td> <td id="cell31"></td> <td id="cell32"></td> <td id="cell33"></td> <td id="cell34"></td> <td id="cell35"></td> </tr> </tbody> </table> </div> </body> </html>7(曜日) × 5(週)の箱を作って各セルにIDを振っておく。
index.js'use strict'; const moment = require("moment"); window.onload = function() { //captionを表示 document.getElementById('caption').innerText = moment().format("YYYY年MM月"); //当月の日数を取得 const daysOfMonth = moment().daysInMonth(); //月初の曜日を取得(index.htmlと合わせるために+1する) const firstDayOfManth = moment().startOf('month').day() + 1; //カレンダーの各セルに日付を表示させる let cellIndex = 0; for(let i = 1; i < daysOfMonth + 1; i++) { if(i === 1) { cellIndex += firstDayOfManth; } else { cellIndex++; } document.getElementById("cell" + cellIndex).innerText = i; } }月初の曜日が分かればそれ以降の日付の曜日も一意に決まる。
カレンダーの特性上、1番上の行のセルには値が必ず一個は入るので、cellIndex += firstDayOfManth; で初日のセルの位置を設定して、後は日数分インクリメントしていく。月初はmomentモジュールのmoment().startOf('month')で楽に取得できる。
$ electron .祝日を赤字にするにはどっかでデータを持っておかないと無理そう。
TODO
年月を変更できるようにする。
あとがき
momentモジュールが便利。
- 投稿日:2020-03-20T17:06:16+09:00
Railsのtext_fieldにCSSをあてる方法
text_fieldにCSSってどうやってあてるんだっけ?
Railsアプリケーション作成時にフロント部分を作成していた時に
example.haml.html.item__name 商品名 .name--input = f.text_field :name, placeholder: "40文字まで"のようなhamlを作成していざscssを記述しようとしたときに
このtext_filed
にどうやってCSSをあてるのかが
ふと考えると分からなかったんです。同じような
number_field
やsubmit
などにも使えるやり方なので
是非覚えておいたほうがよいです。そもそも
text_field
で作られるHTMLは何なのか?上の
= f.text_field :name, placeholder: "40文字まで"
で作られるHTMLを
chromeの検証で確認してみると
<input placeholder="40文字まで" type="text" name="item[name]" id=item_name>
というものが作成されているのがわかるかと思います。text_fieldとかはRailsがviewを簡潔に記述するために用意してくれているヘルパーメソッドのため
簡単な記述で実際はこういうHTML文の作成もしてくれています。
この作成されたHTMLにあてるようにCSSを記述すればOKです。今回はSCSSを使い記述しました。
CSSをあててみよう
example.scss.item__name{ input[type="text"]{ width: 100%; height: 20px; font-size: 14px; } ::placeholder{ padding: 5px 5px; } }と指定して記述すると、
text_field
にCSSをあてることができます。
placholder
はCSSの擬似要素のため上記のような記述をする必要があります。ヘルバーメソッドを使って記述した場合にどのようなHTML文が作成されているのかを確認すると解決できる部分でしたね。
参考先
- 投稿日:2020-03-20T15:21:30+09:00
Electronでカレンダーを作る①
はじめに
皆さん、Electron触っていますでしょうか?
私は最近Electronの存在を知ったので、JavaScriptの勉強も兼ねてカレンダーアプリを作成してみようと思った次第です。とりあえずElectronプロジェクトを作る。
$ mkdir ElectronCalender $ cd ElectronCalender/ $ npm init -y $ npm install -D electron ※これでディレクトリ内にElectronを使う準備は整った。
※npm install -D ~ はpackage.jsonに書き込みらしい。画面を作ってアプリを立ち上げる。
package.jsonにエントリーポイント(main.js)を指定しとく。
package.json{ "name": "ElectronCalender", "version": "1.0.0", "description": "", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "electron": "^8.1.1" } }main.jsの中身
main.js'use strict'; // Electronのモジュール const electron = require("electron"); // アプリケーションをコントロールするモジュール const app = electron.app; // ウィンドウを作成するモジュール const BrowserWindow = electron.BrowserWindow; // メインウィンドウはGCされないようにグローバル宣言 let mainWindow; // 全てのウィンドウが閉じたら終了 app.on('window-all-closed', function() { if (process.platform != 'darwin') { app.quit(); } }); // Electronの初期化完了後に実行 app.on('ready', function() { // メイン画面の表示。ウィンドウの幅、高さを指定できる mainWindow = new BrowserWindow({width: 800, height: 600}); mainWindow.loadURL('file://' + __dirname + '/index.html'); // ウィンドウが閉じられたらアプリも終了 mainWindow.on('closed', function() { mainWindow = null; }); });mainWindow.loadURL('file://' + __dirname + '/index.html'); で表示する画面(htmlファイル)を読み込む。
html,cssはこんな感じ(ほぼ参考書の内容を丸写しした)
index.html<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ElectronCalender</title> <link rel="stylesheet" href="index.css"> </head> <body> <div class="calendar-wrapper"> <table class="calendar"> <caption>2020年3月</caption> <thead> <tr> <th>日</th> <th>月</th> <th>火</th> <th>水</th> <th>木</th> <th>金</th> <th>土</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>2</td> <td>3</td> <td>4</td> <td>5</td> <td>6</td> <td>7</td> </tr> <tr> <td>8</td> <td>9</td> <td>10</td> <td>11</td> ・・・略 </tr> </tbody> </table> </div> </body> </html>index.css.calendar-wrapper { width: 750px; } .calendar { table-layout: fixed; width: 100%; border-collapse: collapse; border: 1px solid #ccddcc; vertical-align: middle; } .calendar th, .calendar td { padding: 10px 0; border: 1px solid #cdcdcd; text-align: center; vertical-align: top; font-size: 35px; } .calendar th { background-color: #dedede; } /* 日曜日1行目(見出し) */ .calendar th:first-child { background-color: #e05557; color: #ffffff; } /* 日曜日 */ .calendar td:first-child { color: #e05557; } /* 土曜日1行目(見出し) */ .calendar th:last-child { background-color: #207bcf; color: #ffffff; } /* 土曜日 */ .calendar td:last-child { color: #207bcf; } /* 祝日 */ .holiday { color: #e05557; } .calendar caption { margin: 0 0 10px 0; padding: 10px; font-size: 23px; border: 5px solid #dedede; border-radius: 30px; font-weight: bold; }アプリを起動する。
ディレクトリ直下で $ electron .良い感じな気がする。
TODO
日付、祝日をhtml内にベタ書きしているのでjsを使って動的にカレンダーの内容が作成されるようにしたい。
あとがき
作っていて気がついたけどカレンダーのスペルは「calender」じゃなくて「calendar」だった。
- 投稿日:2020-03-20T14:36:55+09:00
Railsでページ毎にsubmitの表記を変えたい
submitボタンの表記を変えたい
submitボタンを実装した場合、デフォルトの日本語表記は下記のような形で表示されます
(色付けなどはCSSで加工済み)'登録する' というのがデフォルトのsubmitボタンとなります。
例えば、作成しているアプリで商品出品するページでは当然、'出品する'という表記にしたいですよね。
しかし、商品を編集して更新する場合は、'出品する'だと違和感があるので
'更新する'と表記をしたいところです。しかし、出品も更新も同じフォームを流用しているので
分岐分けで記入する必要があるところになります。単純にsunmitボタンのテキストを変えたい場合なら
<input type="submit" value="出品する">
を使えば表記は変わるわけなのですが、上記のように分岐わけさせたいときにどうするべきか。
出品と更新のページを別々に作って、各々でテキスト表記を変えるという手段もできますが
メンテナンス上、ほぼ同じ記述のコードのファイルが増えてしまうのは避けたいところです。ja.ymlを利用して分岐させる
ja.ymlといえば、英語表記を日本語化させる指定を記述する場所なのですが
こちらでアクション毎にsubmitの表記を指定させることができます。まずはi18nの導入が必要なので参考先にあるページを参照に導入を済ませておいてください
[初学者]Railsのi18nによる日本語化対応
config/locales/ja.yml
に記述するja.ymlja: helpers: submit: create: "出品する" update: "更新する"このように記述をするとcreate下のsubmitは出品すると表示され
update下のsubmitは更新すると表示されるようになります。実際にcreate時はこのように変更がかかります。
update画面も同様に記述した通りの変更がかかります。これでhtmlをcreateとupdateで別々に表記を分ける必要がなくなるので
ファイルを増やすことなく分岐して表示させることができます。参考先
http://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-submit
- 投稿日:2020-03-20T14:10:51+09:00
[scss]hamlへの反映方法は2種類あった 備忘録
今までscssのhamlへの反映方法といったら
application.scss
へ@import記述のみと思っていましたが。実はそんなことをしなくてもコマンド入力で作成したscssには
自動的に反映される記述が入っているんです。
知りませんでした。。。
コメントアウトされ得た以下の3行があるとOKらしいscss// Place all the styles related to the reset controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ですので、scssの反映方法を以下2種類をまとめたいと思います
コマンドで作成する方法
importいらないコマンド存在したー!!!
ターミナル$ rails g assets 作成したいファイル名以上です。
あとはscssに記述したものは自動的にhamlに反映されます。手動でファイルを作成する方法
右クリックでファイル作成をした場合scssにimport記述は自動生成されませんので
application.scss
にimportしてhamlにscssが当たるようにしましょう。やり方
・hamlに反映させたいscssのファイル名をアンダーバー「 _ 」始まりにします。
・application.scss
にimportします。
・終わり記述参考は下記
application.scss@import "home"; @import "いれたいscssファイル名(アンダーバーは入れない)"; ⬅︎終わりは必ずセミコロンを入れる #assetsの中でもフォルダの異なるものはURLをちゃんと記載しないと反映されない @import "config/reset"; @import "フォルダ名/ファイル名";参照記事
終わりに
自分のいた学校は極力コマンドを使用しない学校でしたが
コマンドはコマンドで別の仕様があることを今回知りました。
アウトプットもそうですがインプットも必要。
人との情報交換は本当に大事だなと改めて感じました。初学者な為、記事に不備やアドバイ等ございましたらご連絡頂けますと幸いです。
最後まで読んできただきありがとうございます。
- 投稿日:2020-03-20T12:34:09+09:00
css と簡単なjqueryでメニューを開閉する
久々に時間が取れたので、頭の整理も兼ねて書いてみることにしました。
フロントをメインに仕事をしていますが、cssをうまく利用すると簡単な動きなら表現できることを知ったので、その一例を書いてみます。
今回はヘッダーとかによくあるメニューを作ってみましょう。目的
ボタンとリストを作成し、ボタンをクリックするとリストが開閉するようにcssとjqueryで調整する。
html
htmlはボタンとリストだけのシンプルなもの、reset.cssの適用をお忘れなく。
index.html<html lang="jp"> <head> <meta charset="utf-8"/> <link href="css/reset.css" type="text/css" rel="stylesheet" /> <link href="css/open1.css" type="text/css" rel="stylesheet" /> <script src="js/jquery-3.2.1.min.js" type="text/javascript"></script> <script src="js/open1.js" type="text/javascript"></script> </head> <body> <div class="btn"></div> <ul> <li>リスト1</li> <li>リスト2</li> <li>リスト3</li> </ul> </body> </html>css(scss)
scssでかくとこんな感じです。ポイントは2つあります。
一つ目はulです。height:0;overflow:hidden;をつけることで中身のliが見えなくなります。また、transitionでheightが変更された時に時間をかけて変化してくれます。
もう一つのポイントは&.on(body.on)です。
ここにボタンを押した時の変化を記述します。open1.scssbody{ width:100%; height:100%; background-color: rgb(220,220,220); .btn{ width:100px; height:100px; background-color: blue; } ul{ width:200px; height:0; overflow: hidden; transition: height 1s; li{ width:100%; height:60px; border: 1px solid #000; } } &.on{ background-color: rgb(150,150,150); .btn{ background-color: red; } ul{ height:186px; } } }jquery
かなりシンプルに書いています。ボタンを押した時に、bodyタグにクラスを足すor消すだけ。
open1.scss$(function(){ $(document).on("click", ".btn", function(){ if(!$('body').hasClass('on')){ $('body').addClass('on'); }else{ $('body').removeClass('on'); } }); });動作説明
ボタンを押すとjqueryが発火し、bodyにonのクラスを足します。
すると、cssのbody.onが適用され、下記の変化が起こります。1.bodyの背景色が変更される
2.ボタンの背景色が変更される
3.ulの高さが変わったため、一秒間のアニメーションをへて変化もう一度ボタンを押すと元に戻ります。
ね、簡単でしょ?このように、ボタンのオンオフくらいのアニメーションならcssをメインにするとコード量が減り、jqueryの負担も軽くなります。複数の言語を組み合わせると面白いことが結構あるので、色々試してみるといいでしょう。
次はphpとjqueryを組み合したものを紹介できたらなと思います。
- 投稿日:2020-03-20T07:04:23+09:00
UIライブラリに学ぶz-indexの設計
背景
z-indexをどのように設定していますか?
はじめのうちは、一般コンテンツよりヘッダーが前で、オーバーレイはさらにその前で、モーダルはもっと前で。。。と、ざっくりと相対的に決めてしまえば問題なく実装できると思います。しかし、アプリケーションが大きくなってきたり、新しい開発者がどんどん入ってきたりすると、オレオレだと辛くなってきます。
私も無根拠にz-index: 99999;
などと書いてしまっていましたが、「何かルールを設けては」と指摘されてしまいました。デファクトになっているようなガイドラインはないっぽいので、
有名UIライブラリやOSSはどう設計しているのか、参考にするために調べてみました。CSSフレームワーク/UIライブラリ
Bootstrap
言わずとしれたBootstrapは次のように設計されています。
$zindex-dropdown: 1000 !default; $zindex-sticky: 1020 !default; $zindex-fixed: 1030 !default; $zindex-modal-backdrop: 1040 !default; $zindex-modal: 1050 !default; $zindex-popover: 1060 !default; $zindex-tooltip: 1070 !default;上記にないコンテンツは0~999でということでしょう。
次いでドロップダウンメニュー、スティッキーヘッダーと続いています。(ソース) https://getbootstrap.jp/docs/4.2/layout/overview/#z-index
Semantic UI
Semantic UIも人気のCSSフレームワークです。
Semantic UIはドキュメント等にz-indexの指標はまとまっていませんでしたが、
https://github.com/Semantic-Org/Semantic-UI/search?q=zIndex&unscoped_q=zIndex
を見る限り、ざっくり以下のような感じかと思います。メインコンテンツ: ~200くらい ヘッダー: 800 プログレスバー: 999 オーバーレイ: 1000 モーダル: 1001 ポップアップ: 1900Bootstrapほどちゃんと決まってない感じもしますが、数字のオーダー的にはBootstrapと似たような感じです。
Material-UI
人気のReactベースなUIコンポーネントライブラリです。
mobile stepper: 1000 speed dial: 1050 app bar: 1100 drawer: 1200 modal: 1300 snackbar: 1400 tooltip: 1500Bootstrapと似た設計です。
0~999は一般のコンテンツなのでしょう。
モーダルを表示したときのオーバーレイなどは、drawerとmodalの間の1201~1299のどこかであることが推測できます。(ソース) https://material-ui.com/customization/z-index/
UIライブラリまとめ
なんとなくUIライブラリではどのようにz-indexが設計されているのかがわかりました。
1000未満程度はテキストフィールドやボタンなど、一般的なコンテンツが占めるようにしていて、
1000以上でフローティング要素などを、その役割順に設定していっています。
Semantic UIのように、1しかz-indexの値を足さない事もあれば、10~100程度足して余裕をもたせることもあるようです。OSS(Web)
次に、有名なOSSではどんな感じでz-indexを設定しているのか見てみます。
GitLab
GitLabはOSSでコードを公開しています。
ざっくり以下のような感じです。popover: 240 dropdown: 300 notification-message: 999 performance-bar: notification-message + 1 footer-message: 1000 header: 1000 overlay: 1031popoverが結構低層で、dropdownメニューとかぶったとき下に行ってしまいますが、あまり起こり得ないので問題ないのでしょう。
performance-bar(進捗バー的なもの?)は、scssの定数で、notification-messageよりひとつ上に表示するようになっています。
このように、ある程度のルールだけ定数で持っておいて、細かい部分は+1
するといった運用は実践的に思えます。(ソース) https://github.com/gitlabhq/gitlabhq/search?q=z-index&unscoped_q=z-index
Mattermost(Web版)
MattermostはSlackライクなチャットツールです。
OSSなのでオンプレで運用できます。
Slackがセキュリティ上だめだから、Mattermostを自前のサーバに立てて使ったりしてる会社もあります。dropdown: 100 popover: 999 overlay: 1000 modal: 1030 その他フローティング要素(emoji pickerなどいろいろ): 1050あたりでちょこちょこ その他最上位要素(通知などいろいろ): 9999だいたいこんな感じでした。
こちらはGitLabと比べて、あまりしっかりとルールがあるようには思えませんでした。
9999が最上位ということは決まってるっぽいですが、9999の要素がいくつもあります。(ソース) https://github.com/mattermost/mattermost-webapp/search?q=z-index&unscoped_q=z-index
調査を踏まえて
今回調べたことを踏まえて、開発中のWebサービスのz-index設計は以下のようにしてみました。
~ 0 : 背景要素 ~ 1000 : 主なコンテンツ 1001 ~ 1099 : フローティング要素 1100 : ヘッダー 1150 : Overlay(ドロワー背面) 1200 : ドロワー、サイドバー 1250 : Overlay(ドロワー前面) 1300 : モーダル 1400 : Snackbar(通知ポップアップ) 1500 : Tooltip(要素の説明ポップアップ)上記すべて使うかはわかりませんが、プロジェクトが大きくなる前に指標を決めておくのは有効そうです。
実際の運用では、theme.ts
などと、テーマ用のファイルを作って、そこですべて定義してテーミングします。これがだけが絶対的な値となり、これ以外にz-indexを設定する必要がある場合は、以下のように相対的に設定することで、cssのマジックナンバーをなくします。
z-index: ${theme.zIndex.overlay + 1};何かUIライブラリを入れている場合は、そのライブラリのz-indexの設計を調べて、それを踏襲すると良いかと思います。
(自前ルールにライブラリの方を合わせるためにオーバーライドする手間がかかるため)このように設計することで、統一感があり堅牢なcssになるかと思います。
- 投稿日:2020-03-20T06:51:48+09:00
初心者によるプログラミング学習ログ 267日目
100日チャレンジの267日目
twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。
100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。
267日目は、
おはようございます
— ぱぺまぺ@webエンジニアを目指したい社畜 (@yudapinokio) March 19, 2020
267日目
・webサイト模写
・下層ページ1ページ目
・flexboxの並びがうまくできない#早起きチャレンジ#駆け出しエンジニアと繋がりたい#100DaysOfCode
- 投稿日:2020-03-20T02:44:00+09:00
lighten(red, 50%) はSassでは真っ白、Stylusでは薄赤
概要
SassとStylus、両CSSプリプロセッサには
lighten()
という同名の関数がありながら、その処理は実は違う。
結果の違いや原理の違いを把握し、ついでなのでそれぞれに互換する独自関数を作ってみる。関数紹介
lighten(color, amount)
color
:明るくする前の色。red
とか、#ff0000
とか。amount
:明るくする量。本記事では簡単のために、単位を10%
とかのパーセントのみ、範囲を0~100のみとする。Sassの挙動
lighten.sass* color: lighten(red, 50%) // => color: white;Stylusの挙動
lighten.styl* color: lighten(red, 50%) // => color: #ff8080;※ 執筆時点で、QiitaのシンタックスハイライトはStylusに未対応
違うね…
// => color: white; // Sass // => color: #ff8080; // Stylus違う。
違いに直面した当初は、自分が何をミスってるのかと焦った。
色空間の違いか? と思ったりもしたけど、調べたところどちらもHSL色空間の輝度(Lightness)を増やす関数なので、値の扱いは同じはず1。
結果的には、どのように明るくするかという仕様そのものの違いだった。なお、併せて述べていくが、暗くする
darken()
や、彩度を変えるsaturate()
とdesaturate()
も、やはり両言語間で仕様が違う。何が違うか
以下、公式情報を概訳しながら拾い上げていく。
Sassの
lighten()
公式ドキュメント:
https://sass-lang.com/documentation/modules/color#lightenご丁寧に目立つ注意書きがある。
⚠️ ちょっと待った!
lighten()
は、輝度の値を固定的に増加させる関数です。お望みの効果ではない場合があります。// #e1d7d2の輝度は85%なので、lighten()関数で30%の輝度を足すとただの白になる @debug lighten(#e1d7d2, 30%) // white// 輝度 46% → 66% @debug lighten(#6b717f, 20%) // #a1a5af // 輝度 20% → 80% @debug lighten(#036, 60%) // #99ccff // 輝度 85% → 100% @debug lighten(#e1d7d2, 30%) // whiteつまり、引数がパーセント値でありながらその実態は「旧パーセント値そのものを
amount
だけ底上げする」であり、「旧パーセント値をamount
の割合だけ増加させる」ではない(伝わるかな…)。
お望みではないかも、というのはこういうことだろう。なお一般的に、パーセント値そのものが差分的に変動する場合、その差分の単位の名称はパーセントではなくポイント(パーセントポイント、percentage point)となる。
例えば20%が25%になったとしたら、それは「5%増えた」ではなく「5ポイント増えた」という。
何せ、値としての20が5%増えたら $20 \times 1.05 = 21$ になるので、その紛らわしさを回避するためにはそりゃ必要な呼び分け方だろうなという感じがする。
…いやしかし、CSS的には大きさの単位にpt
(ポイント)があるからやっぱ紛らわしいな……。ということで、Sassの
lighten()
は元の色の輝度に対してポイント的処理をしている、といえる。タイトルの例について改めて触れると、
lighten.sass(再掲)* color: lighten(red, 50%) // => color: white;HSL色空間では
red
等の原色はその時点で既に50%の輝度を持つため、Sassのlighten(red, 50%)
で輝度が更に50ポイント足された結果、輝度100%のwhite
が得られる。お仲間の関数について
暗くする
darken()
や、彩度を変えるsaturate()
とdesaturate()
についても同じであり、ドキュメントにも全く同様の注意書きがある。Stylusの
lighten()
公式ドキュメント:
https://stylus-lang.com/docs/bifs.html#lightencolor-amount
特に注意書きは無い。オープンソースなので、ソースを覗いてみる。
https://github.com/stylus/stylus/blob/master/lib/functions/index.styl#L125index.styl// 与えられた amount で暗くする darken(color, amount) adjust(color, 'lightness', - amount) // 与えられた amount で明るくする lighten(color, amount) adjust(color, 'lightness', amount)暗くする方と共に
adjust()
という関数で定義されており、その差はamount
の正負にある模様。更に
adjust()
のソースを覗いてみる。
https://github.com/stylus/stylus/blob/master/lib/functions/adjust.js#L13adjust.js(抜粋)function adjust(color, prop, amount){ utils.assertColor(color, 'color'); utils.assertString(prop, 'prop'); utils.assertType(amount, 'unit', 'amount'); var hsl = color.hsla.clone(); prop = { hue: 'h', saturation: 's', lightness: 'l' }[prop.string]; if (!prop) throw new Error('invalid adjustment property'); var val = amount.val; if ('%' == amount.type){ val = 'l' == prop && val > 0 ? (100 - hsl[prop]) * val / 100 : hsl[prop] * (val / 100); } hsl[prop] += val; return hsl.rgba; };輝度操作のコア部分の処理をグラフで表すとこんな感じ。
https://www.desmos.com/calculator/m5j1th0unt
緑のラインは元の色の輝度。
入力値が正の領域はlighten()
に、負の領域はdarken()
に用いられている。見ての通り、これはSassとは違って「旧パーセント値を
amount
の割合だけ増加させる」ような計算になっている。
この計算なら、元の色によらず常に真っ白や真っ黒になるようなケースはamount
を100%
と指定した場合のみとなる。ということで、Stylusの
lighten()
は元の色の輝度に対して割合的処理をしている、といえる。タイトルの例について改めて触れると、
lighten.styl(再掲)* color: lighten(red, 50%) // => color: #ff8080;HSL色空間では
red
等の原色はその時点で既に50%の輝度を持つため、Stylusのlighten(red, 50%)
で「輝度50%から100%までの道のりを50%進む」ようにされた結果、輝度75%の#ff8080
が得られる。お仲間の関数について
暗くする
darken()
については既に述べた。彩度を変える
saturate()
とdesaturate()
については、少し状況が異なる。
上記コア部分の'l' == prop && val > 0
という場合分けの左辺(?)が示しているように、彩度関数では入力値が正の領域でも負の方と同じ1次関数が使われているため、amount
を100%
と指定してもせいぜい元の2倍の彩度にしかならない。
この仕様の意図は正直よくわからない。まぁそういうもんなんだろう。See the Pen Stylus HSL by 森の子リスのミーコの大冒険 (@phroneris) on CodePen.
(もしくはこちらから→ Try Stylus!)
「ご覧の通り、下げる方向なら輝度でも彩度でも同じなのに上げる方向では違うよね」…ってやろうとしたんだけど、下げる方向もそれはそれで意味わからん誤差があるな……。
といってもパーセント値の一の位で四捨五入すれば同じだし、この程度なら許容されてるのかな…(なお、rate = 43%
にすると四捨五入ですら不一致になる)。あとなんかこんな挙動もある→ Try Stylus!
どちらが良いのか
良いも悪いも無い。
無いが、どちらかのCSSプリプロセッサを主に使いながらもう片方のものにも多少首を突っ込むような使い方をする人の場合、そういう差があるということだけは知っておく必要がある。
…と思う。一応の比較考察
Sass側のポイント加算方式は、正直言って私達が「割合」の操作に期待するものではないところがある。
何せSassでは「#FF0000
を半分くらい明るくしたい」と思ってlighten(red, 50%)
と書いたら#FFFFFF
が返ってくるわけで、そりゃ公式ドキュメントにも警告が載るわなという感じがする。ただ一方で、「サイコロを6回振ったら1回は6が出るはず」とか「ハサミギロチン2を3回撃ったら命中率90%」とか思ってしまうのもまた人情であり、ポイント加算方式はそういう思考にはむしろマッチするのが面白い。
あと、HSL色空間は
- 色相(Hue):0~360 [°]
- 彩度(Saturation):0~100 [%]
- 輝度(Lightness):0~100 [%]
という定義上、彩度と輝度は元々がパーセント値なわけで、HSL色空間についてそこまで把握している人なら「そもそもパーセント値を更に割合操作する方がおかしい」と感じるところがあっても不思議ではないと思う。
じゃあどっちも使おうぜ!
違いはわかった。原理もわかった。
それならいっそ、両言語にある独自関数作成機能で、お相手のそれらに相当する関数を自作してしまおうじゃあないか。
そうすれば、言語を問わずいつでも好きな方を選んで使えるようになる。最強。以下、各CodePenの右側のResult画面が、どちらの言語の例でもおおよそ同じ結果になるのを確認されたし。
誤差は仕方無し。SassでStylusの
lighten()
を再現概訳時にこっそり省いていたが、公式ドキュメントの
lighten()
等の注意書きには、割合的な変更の実現方法としてscale-color()
が紹介されている。https://sass-lang.com/documentation/modules/color#scale
color.scale($color, $red: null, $green: null, $blue: null, $saturation: null, $lightness: null, $alpha: null) scale-color(...) //=> color
$color
の1つ以上の成分を流動的に拡縮します。使わない手は無い。
See the Pen Sass HSL Control: vs Stylus by 森の子リスのミーコの大冒険 (@phroneris) on CodePen.
(もしくはこちらから→ SassMeister)
StylusでSassの
lighten()
を再現単純な足し引きなので楽ちん。
See the Pen Stylus HSL Control: vs Sass by 森の子リスのミーコの大冒険 (@phroneris) on CodePen.
(もしくはこちらから→ Try Stylus!)
既知の誤差として、Kirakiratterのボタンの色を再現すべく
sass-lighten(sass-desaturate(#6a4643, 5%), 12%)
とすると、現物は#8a6461
なのにこの自作関数では#8a6460
になる。まとめ
SassとStylusという、どちらも同じCSSプリプロセッサの役目を担う言語の、同じ名前の関数であり、「HSL色空間に準じた輝度を増やす」という同じような機能を持つ、
lighten(color, amount)
。
しかし、その輝度をどのように増やすかという点で、両言語のそれには決定的な違いがある。
Sass Stylus 処理 元の輝度に単純に加算 元の輝度から割合的に増加 利点 元の輝度を知っていれば
計算結果が明白真っ白になるのは amount = 100%
の時だけ欠点 元の輝度を知らない場合は
予想外に白飛びする割合の割合を操作する
という疑問が残る例 lighten(red, 50%)
→white
lighten(red, 50%)
→#ff8080
また、輝度を下げる
darken()
、彩度を上げるsaturate()
、彩度を下げるdesaturate()
も同様。
ただし、Stylusのsaturate()
だけはまた少し違う仕様を持つので注意。そして、頑張ればどちらも自作関数で再現することができる。
ただし、どうしても細かい誤差が出る場合もあり、完全な互換性は望むべくもないので強く生きる。余談
なんでこんなことを気にして調べるに至ったかというと、Stylus(ブラウザ拡張機能の方)で使えるUserCSSとして、マストドンの見た目をカスタムするものを作っている過程で気付いたため。
- ブラウザ拡張機能「Stylus」: https://add0n.com/stylus.html
- Stylus用UserCSS(森): https://github.com/Phroneris/StylusUserCSS-Phroneris
↑この「Mastodon未収載アイコン変更」に色変え機能を付けようと思って…。私はもっぱらStylus使い3だが、マストドンにはSass(SCSS)が採用されており、両者は記法とか変数とか関数とかが似ていたり違ったりする部分が当然色々ある。
そういう色々について色々格闘していた中で、特に色調整関数の挙動の違いにはかなり困惑し、またそれについて紹介する既存の資料も見当たらなかったので、自分自身のためも兼ねてここにまとめたのであった。
おわり
- 投稿日:2020-03-20T00:18:38+09:00
永遠に続くクソ計算クイズ2 typescriptで
成果物
https://yuzuru2.github.io/neta1/dist/
リポジトリ
https://github.com/yuzuru2/yuzuru2.github.io/tree/master/neta1
UI
src/index.ts
// くそIEに対応させる import 'babel-polyfill'; class Calculation { private static instance: Calculation; // 秒数 private count = 15; // タイマーで使う private time_id; // シングルトン private constructor() {} // dom private readonly id_left = document.getElementById('left'); private readonly id_sign = document.getElementById('sign'); private readonly id_right = document.getElementById('right'); private readonly id_ans = document.getElementById('ans'); private readonly id_time = document.getElementById('time'); private readonly id_yes = document.getElementById('yes'); private readonly id_no = document.getElementById('no'); // 問題セット private set_question(left, sign, right) { this.id_left.textContent = left; this.id_sign.textContent = sign; this.id_right.textContent = right; } // 答えセット private set_answer(ans: string) { this.id_ans.textContent = ans; } // 足し算 private sum(ans: boolean) { const _left = Math.floor(Math.random() * 100) + 1; const _right = Math.floor(Math.random() * 100) + 1; this.set_question(_left, '+', _right); if (ans) { this.set_answer(`${_left + _right}`); return; } // 不正解作成パターン1 if (Math.floor(Math.random() * 2) === 0) { this.set_answer( `${_left + _right + (Math.floor(Math.random() * 3) + 1)}` ); } else { // 不正解作成パターン2 this.set_answer( `${_left + _right - (Math.floor(Math.random() * 3) + 1)}` ); } } // 引き算 private minus(ans: boolean) { const _left = Math.floor(Math.random() * 100) + 1; const _right = Math.floor(Math.random() * 100) + 1; this.set_question(_left, '-', _right); if (ans) { this.set_answer(`${_left - _right}`); return; } // 不正解作成パターン1 if (Math.floor(Math.random() * 2) === 0) { this.set_answer( `${_left - _right + (Math.floor(Math.random() * 3) + 1)}` ); } else { // 不正解作成パターン2 this.set_answer( `${_left - _right - (Math.floor(Math.random() * 3) + 1)}` ); } } // 掛け算 private multiplication(ans: boolean) { const _left = Math.floor(Math.random() * 10) + 1; const _right = Math.floor(Math.random() * 10) + 1; this.set_question(_left, '×', _right); if (ans) { this.set_answer(`${_left * _right}`); return; } // 不正解作成パターン1 if (Math.floor(Math.random() * 2) === 0) { this.set_answer( `${_left * _right + (Math.floor(Math.random() * 3) + 1)}` ); } else { // 不正解作成パターン2 this.set_answer( `${_left * _right - (Math.floor(Math.random() * 3) + 1)}` ); } } // 割り算 private division(ans: boolean) { let _left; let _right; while (1) { _left = Math.floor(Math.random() * 100) + 1; _right = Math.floor(Math.random() * 10) + 1; if (_left % _right === 0) { break; } } this.set_question(_left, '÷', _right); if (ans) { this.set_answer(`${_left / _right}`); return; } // 不正解作成パターン1 if (Math.floor(Math.random() * 2) === 0) { this.set_answer( `${_left / _right + (Math.floor(Math.random() * 3) + 1)}` ); } else { // 不正解作成パターン2 this.set_answer( `${_left / _right - (Math.floor(Math.random() * 3) + 1)}` ); } } // リセット private reset() { // タイマー処理 const time_method = () => { this.id_time.textContent = `${--this.count}`; if (this.count === 0) { alert('タイムアップ'); this.reset(); return; } this.time_id = setTimeout(time_method, 1000); }; clearTimeout(this.time_id); this.count = 15; this.time_id = setTimeout(time_method, 1000); // 時間セット this.id_time.textContent = `${this.count}`; // 正解・不正解どちらの問題を出すか決める const ans = Math.floor(Math.random() * 2) === 0; // 計算種類 const _sign = Math.floor(Math.random() * 4); switch (_sign) { case 0: this.sum(ans); break; case 1: this.minus(ans); break; case 2: this.multiplication(ans); break; case 3: this.division(ans); break; default: break; } // yesボタンを押したとき this.id_yes.onclick = () => { ans ? alert('正解') : alert('不正解'); this.reset(); }; // noボタンを押したとき this.id_no.onclick = () => { ans ? alert('不正解') : alert('正解'); this.reset(); }; } // シングルトン 単一のインスタンスを返す public static get_instance(): Calculation { if (!this.instance) { this.instance = new Calculation(); } // 生成済みのインスタンスを返す return this.instance; } // クイズをスタートする public start() { this.reset(); } } Calculation.get_instance().start();見返してみると、とんでもねーくそコードだな
- 投稿日:2020-03-20T00:00:21+09:00
CSS Modules の問題点をミックスインで解決する
はじめまして、よこけんです。(2回目)
今日は、CSS Modules の問題点を解決する方法について検討したのでそれを共有してみます。CSS Modules の問題点
CSS Modules ではクラスセレクタによるスタイル適用が基本です。
そしてクラス名は一つのコンポーネントに複数指定することができます。
しかし、複数指定した場合の適用順序は保証されません。クラス名の指定順序ではなく、クラスセレクタが読み込まれた順序に依存します。例えば下記の場合、背景色は赤色ではなく青色になります。
a.styl.a background-color: redb.styl.b background-color: blueHoge.tsximport * as aStyles from "./a.styl"; import * as bStyles from "./b.styl"; const Hoge = () => <div className={`${bStyles.b} ${aStyles.a}`}>Hoge</div>;サンプルコードでは
Hoge.tsx
がインポート順序をb.styl
->a.styl
に変えれば意図した結果を得ることができますが、例えばHoge.tsx
から参照される別のコンポーネントでb.styl
をインポートしている場合にはやはり.b
が優先されて青色になってしまいます。
この問題は非常に厄介な上に、直接的な解決方法というものはありません。ミックスインによる解決
本記事では、この問題に対する比較的扱いやすい解決方法として、Stylus や Sass のミックスイン機能を利用した解決方法を紹介します。
ミックスインはクラス継承と違い、適用位置にプロパティを全てコピーします。全てのプロパティが集約されることにより、単純にミックスインの指定順序に従ってプロパティが適用されます。ミックスイン本体の読み込み順序は関係ありません。次のサンプルコードでは、
mixHoge()
がmixB()
の後にmixA()
を呼んでいるため、背景色は必ず赤色になります。a.stylmixA() background-color: red .a mixA()b.stylmixB() background-color: blue .b mixB()Hoge.styl@import "a.styl" @import "b.styl" mixHoge() mixB() mixA() .hoge mixHoge()Hoge.tsximport * as styles from "./Hoge.styl"; const Hoge = () => <div className={styles.hoge}>Hoge</div>;シンプルなルール
前述のコードをルール化すると次のようになります。
- 一つのコンポーネントに複数のクラスを指定してはいけない
- クラスセレクタを用意する場合、対になる単一のミックスインを必ず用意する
- クラスセレクタでは常に、対になる単一のミックスインの適用のみを行い、スタイル記述はミックスイン内で行う
- クラス継承 (
@extends
) は一律禁止とし、代わりにミックスインを使用するクラス継承 (
@extends
) は一律禁止ということに気を付けてください。
単一継承であれば大丈夫のように思うかもしれませんが、クラス継承を使ってしまうと、そこから先をミックスインで派生させても全てのプロパティが一箇所に集約されなくなってしまい、読み込み順序に再び依存するようになってしまいます。この方式の欠点は、トランスパイルされた CSS ファイルのサイズが肥大化するリスクです。
前述の通り、ミックスインはクラス継承と違い、適用位置にプロパティを全てコピーします。だから読み込み順序に一切依存しなくなるわけですが、これはトランスパイル後の CSS ファイルのサイズに影響します。
スタイルの継承を多用するようなプロジェクトではリスクが顕在化するかもしれません。
リスクが顕在化してきた場合には、プロパティ数が多く継承も多く行われる特定のスタイルに対してのみクラス継承 (@extends
) を許可し、それらのクラスセレクタだけは読み込み順序を慎重に管理します。追加ルールでリスクを軽減
先ほどはクラス継承を使用すると問題に繋がるとしていましたが、厳密には、派生を許可しないクラスからであればクラス継承を使用しても問題には繋がりません。
合法的にクラス継承を使用できるケースが発生すると、ファイルサイズを抑える効果が期待できます。次のサンプルコードは、
.hoge
クラスの派生を禁止することで安全を確保できます。a.stylmixA() background-color: red .a mixA()b.stylmixB() background-color: blue .b mixB()Hoge.styl@import "a.styl" @import "b.styl" .hoge @extends .b mixA()Hoge.tsximport * as styles from "./Hoge.styl"; const Hoge = () => <div className={styles.hoge}>Hoge</div>;これをルール化すると次のようになります。
- 一つのコンポーネントに複数のクラスを指定してはいけない
- 派生を許可するクラスセレクタを用意する場合、対になる単一のミックスインを必ず用意する
- 派生を許可するクラスセレクタでは常に対になる単一のミックスインの適用のみを行い、スタイル記述はミックスイン内で行う
- ミックスイン内でのクラス継承 (
@extends
) は一律禁止とし、代わりにミックスインを使用する- 派生を許可しないクラスセレクタではスタイルを直接記述して良い
- 派生を許可しないクラスセレクタではクラスを一つだけ継承 (
@extends
) して良い (2つ以上のクラスを継承したい場合はミックスインを併用する)この方式の欠点は二つあります。
一つはルールが少し複雑になるために混乱を招いたりルール違反が発生しやすくなることです。
ただし、派生を許可するクラスセレクタがあまり多くない (整理されていて見通しが良い) プロジェクトなら、追加ルールを適用しても混乱やルール違反は最低限に抑えられると思います。もう一つは、リスクの低減はできても完全に回避することはできないということです。
リスクが顕在化してきたら最初の解決方法と同様、特定のスタイルに対してクラス継承 (@extends
) を許可し、それらのクラスセレクタだけは読み込み順序を慎重に管理します。
しかし、元々少し複雑なルールにこの例外措置が加わることになりますので、混乱やルール違反をより招きやすくなる恐れがあります。結論
- リスクが顕在化する可能性が低そうであればシンプルなルールを採用する
- 追加ルールを採用する場合、混乱やルール違反を招かないよう工夫する
なお、そもそも複数クラスの継承をしようとしなければ問題は起きません。ただし、そのためのルールは結局必要になります。 (そして恐らく、そのルールによって新たなリスクも発生します。)
根本的には、クラスセレクタの読み込み順序ではなくクラス指定順序で結果が決まってくれれば良いんですが、CSS の仕様のようなので。