技術的な話題 に関する投稿を表示しています

Go言語のスイッチが一瞬わからなくなったのメモ

一瞬わからなくなったのでメモ。
https://play.golang.org/p/AmpjLMDFPf8

そもそも型スイッチは interface{} は値を任意の型になっているか確認して、その型にキャストされた状態にしてくれるすごいやつ。
A Tour of Go に出てくるので知っている人も多いハズ。
Type switches

Go なりの型の持ち方、ようはダックタイピングをイマイチちゃんと使ってなかったのもあって理解ができてなかったが
"値がこういうものを持っていたらこの型だよね" という話なだけと、とりあえずのところは理解した

ある値が String() な関数を持っている interface{} ならそれは fmt.Stringer() を満たしているのでキャストができるし
他にも String() だけを要求する interface があれば、それを満たしているともいえるので、その型キャストが出来る。

 

あと混乱をきたすので深掘りせずにイイ感じに応対したいんだけど、そこにポインタが絡んでくると謎っぽい。

https://play.golang.org/p/MHIX7i1Royk

まあ、特に理由がなければ struct の扱いはポインタでよいんじゃないだろうか。
CodeReviewComments にも怪しかったらポインタレシーバを使うのだって書いてあった。
CodeReviewComments · golang/go Wiki · GitHub

 

話がそれてきたのでおわり。

モバイル端末の傾きを JavaScript で受け取る

Device Orientation API というものを JavaScript から触ることで、デバイスの傾きを取得することができる。

デスクトップ PC だとかノート PC だとか、あるいはタブレットとかそのほかモロモロの端末でも加速度センサーがサポートしていれば Chrome なり Firefox なりブラウザがよしなに対応してくれているはず。

Detecting device orientation - Web API reference | MDN

Can I use... Support tables for HTML5 CSS3 etc

ちょっと気になったのだけど W3C の TR を見たら、メンテしないぜ!的なことが書いてあって!?!?な感じ。

DeviceOrientation Event Specification

加えて Geolocation Working Group なのもちょっと不思議な気がしたのだけど、これはきっと、デバイスの傾きを取るというのが本題ではなくて、方角や座標系をいい感じにとるための API なり仕様なりを定めようとしていたのかもしれない、という推測。

 

話を戻して W3C の仕様は少し怪しいものの Can I Use を見る限りはなんとなく幅広いブラウザで一部サポートがされている。でもって MDN を見ると使い方が書いてある。

ざっくりとはこういう形で値を取得することができる。
ようは傾きに応じてイベントがバシバシ発行されるからそれを各自拾ってくれスタイル。

window.addEventListener('deviceorientation', function(event) {
    console.log({
        beta: event.beta   // x 軸
        gamma: event.gamma // y 軸
        alpha: event.alpha // z 軸
    })
});

これをお試すべく、ちょっと計算が間違っている…というか 3 次元の傾きを 2 次元に投影するっていうのがよくわかんないので
若干適当になったものの、ごみばこいんのトップページをスマホで開いたときに傾きを使って重力をコントロールできるようにした。

モバイル端末からお試しあれ。

 

余談になるけど Chrome の DevTools からセンサーのエミュレーションもできることに気づいた。
More Tools から Sensors を開く。

こういうものが見えるようになるので Orientation のところでドロップダウンリストから選んだり、スマホの形状をしているものをドラッグしてぐるぐる回して 遊べる 回転を調整したものを Chrome がエミュレーションしてくれる。

 

ちなみにちなみに、すでにページを開いている状態からこの Sensors のエミュレーションをしようとすると以下のような文言が出てくる。

A reload is required so that the existing AbsoluteOrientationSensor and RelativeOrientationSensor objects on this page use the overridden values that have been provided. Close the inspector and reload again to return to the normal behavior.

これはそのままページをリロードすれば解決する。
冷静に考えて動的にデバイスのエミュレーションを差し込むってすげー大変だと思うよ。

zapcore.Field を生成する関数を自分で定義する

uber-go/zap: Blazing fast structured leveled logging in Go.

zap を使ってログを出すときに自分でフィールドを追加できる。

こういう雰囲気で。

logger.Info(
    "invalid address",
    zap.String("address" address)
)

このとき zap.String って書いているけど、これを自分で定義できるのかな?と zap のソースコードを読んだりしてたらできそうなことがわかった。
でもって試したのが、これ。

sters/zap-hashed-field: Define hashed zapcore.Field

具体的なユースケースがわからんけど、例えば入力されたデータをもとに動きを追いかけたい、とか、なにがしかの都合で、センシティブなデータ(パスワード、メールや電話番号、トークン値、…etc)を出したいときがある、と思う。そういうときに使えるんじゃね?的な妄想をしながら作った。

こういう風に使ってハッシュ値にして出力できる。

logger.Info(
    "invalid address",
    hashedfield.Sha1("address" address)
)

 

これをどうやってやるかっていうと、単に zapcore.Field が構造体になっているので、それを生成して zap.Logger で出力する場所に渡してあげるだけでよかった。

zapcore.Field は以下のようなフィールドを持っている。
zap/field.go at master · uber-go/zap

type Field struct {
	Key       string
	Type      FieldType
	Integer   int64
	String    string
	Interface interface{}
}

このとき Type に指定したものに応じて zap.Logger がよろしく値を変換して出力してくれる。文字列を出したいなら StringType を指定して String フィールドに値を入れればよい。
zap.String がまさにそう。
zap/field.go at master · uber-go/zap

func String(key string val string) Field {
	return Field{Key: key Type: zapcore.StringType String: val}
}

 

使い方のサンプルってどうやって書いたらいいのかなあと思いながら zap のコードを眺めていたら
examples_test.go というファイルがいて、なんだろうな、と。

zap/example_test.go at master · uber-go/zap

そこから調べてたら Examples Test を知った。

Testable Examples in Go - The Go Blog

要するに、テストコードの最後に期待する出力をコメントで書いておくと出力を内部でハンドリング、比較して様子を見てくれるとのこと。
便利~~~~。

DOMの並び替えアニメーションをしてみたい(追記

(追記ここから)

Enter/Leave とトランジション一覧 — Vue.js

Vue.jsで同じようなやつあるしこれ使ったらええやんという話を聞いた。よさそう。

結果同じような見た目だけど、translateで動かして、そのあとDOMの並びを調整しているっぽい。

データの並び = DOMの並びに完全にリンクしているので、Vueの中でも調整しやすいよね的な感じなのかな。

(追記ここまで)


DOM ががーーーーっとならんだものを並び替えるときにアニメーションできないかなあ、というもの。
うにょーんって動いたら面白いのでは?と思って試してみた。

このページで動く様子が確認できるようにした(動いてるときに再度押すとバグる)

https://gomiba.co.in/blog/tags

 

そのまま動かそうとすると、移動先の座標がわからず動かせないので、いくつかのステップを通してみた。

1. 今の表示されている座標を保存
2. 今の表示されている座標の場所に固定した DOM を生成(この時点ではメモリに乗っているだけで表示されない)
3. コンテンツを並び替えて新しい座標を計算
4. 今の表示されている DOM を非表示 2 で作った DOM を表示する(見え方は変わらないが表示されているものを切り変える)
5. 3 の座標に向けて 2 の DOM を CSS アニメーションで動かす
6. 終わったころあいで 2 と 3 の DOM を入れ替えて 2 の DOM を捨てる。

要は動かす用の DOM を生成して、動かすだけ動かして、前後は元の DOM のまま、というもの。
パッと思いついたやり方はこれ。

 

ちゃんとやろうとすると、再描画が無茶苦茶大変になって遅くなるとおもうので、何か対策考えないといけなさそう。
透過色まずやめよう、とか。

 

コードはこんな感じ。
ちょっと座標のところが面倒になって jQuery を使っている。が、そんなに難しいことはやっていないので生 DOM でもわりかし簡単にいけるはず。

連打対策は入れてないので、連打するとブラウザ固まる。

const DomSortAnimation = ($target, AnimationTimeSec, AnimationCompleteDelaySec, SortFunction) => {
    // fixed now position
    const targetPositions = $target.map((_, e) => {
        const $t = $(e);
        return $t.position()
    });
    const $newTarget = $target.clone()
    $newTarget.each((i, e) => {
        $(e).css({
            top: targetPositions[i].top,
            left: targetPositions[i].left,
            position: 'absolute',
            display: 'block',
            transition: AnimationTimeSec + 's all ease',
        });
    });

    // calculate new position
    const targetContents = $target.toArray()
        .sort(SortFunction)
        .map((a) => {
            return {
                html: a.innerHTML,
                href: a.href
            }
        });

    $target.each((i, e) => {
        const $e = $(e);
        $e.html(targetContents[i].html);
        $e.attr({href:targetContents[i].href});
    });

    const newPositions = {};
    $target.each((_, e) => {
        const $e = $(e)
        newPositions[$e.attr('href')] = $e.position();
    })

    // swap DOM
    $newTarget.appendTo($target.parent());
    $target.hide();

    // animation start
    setTimeout(() => {
        $newTarget.each((i, e) => {
            const $e = $(e);
            const pos = newPositions[$e.attr('href')];
            $e.css({
                top: pos.top,
                left: pos.left,
            });
        });
    }, 0);

    // complete animation restore DOM
    setTimeout(() => {
        $newTarget.remove();
        $target.show();
    }, (AnimationTimeSec + AnimationCompleteDelaySec) * 1000);
};
DomSortAnimation(
        $(".c-article-content .c-badge"),
        2,
        0.2,
        (a, b) => {
            const aContent = a.textContent.trim();
            const bContent = b.textContent.trim();
            if(aContent < bContent){
                return -1;
            }else if(aContent > bContent){
                return 1;
            }
            return 0;
        }
    );

Zapier を使ってあと読む君をつくる

Slack 流量が多いとすぐ流れちゃうので、いまは置いておいて後で見よう!とか、メモ用途に別のチャンネルに転送する、みたいなことを簡単にやりたい。
それ Zapier でできる。

トリガー: Slack: New Reaction Added

リアクションがついたら、をトリガーにできる。
:atodeyomu: とか作るなどして、わかりやすい何かに設定しておくとよい

User の指定をしないと誰かがリアクションつけたら飛ぶ、になってしまうので注意が必要。
みんなで情報集めよう!みたいにするなら、もっとわかりやすい名前のリアクションにしたほうがよい。

ステップ: Slack > Send Channel Message

これで別のチャンネルに、トリガーで拾った発言の URL を送信できる。

おまけ: リマインダ機能

上記のタスクへさらに設定する。

  • ステップ: Storage > Push Value Onto List
    • 任意の名前で良い

以下のような別のタスクを作る

  • トリガー: Schedule > Every Day
  • ステップ: Storage > Get List Values
    • New Reaction Added したときに追加する Storage と同じもの
  • ステップ: Filter > Only Continue If...
    • Get List Values が空っぽではないかチェック
  • ステップ: Storage > Remove Value
    • Storage を空っぽにする
  • ステップ: Slack > Send Channel Message
    • Get List Values で取得した内容を送信

すると、毎日好きな時間に、それまでに溜められたリアクションのついた発言をリマインドできる。

こんな感じ。

スレッドでも bot でも、どんな発言でも拾える。

Zapier を使って JIRA のチケットを通知する

JIRA のチケットで動いていることもあって、今日やること = JIRA を見ればわかる状態になっているので
毎日確認できるようにリマインドしたらいいなあと思ってやってみている。

Zapier

Zapier | The easiest way to automate your work

IFTTT みたいなやつ IFTTT を使ったことないので何が違うかはわからない。
何かをトリガーにして、何かをやることができる。
やることを複数のステップにして、条件チェックしたりもできる。
cron のように、定期実行することもできる。
(無課金だとその恩恵が揃っていないみたい)

Zapier を使って JIRA のチケットを通知するくんを作る

トリガー: Schedule > Everyday

毎日実行するだけ。
時間は朝とかお昼くらいがおすすめ。

Zapier のプロフィールからタイムゾーンを設定してないと、ずれた時間にくることに注意。

ステップ 1: Webhook > GET

リクエスト先は自分たちの JIRA の API

https://XXXXX.atlassian.net/rest/api/v2/search

パラメータとしてこんなものを設定する。

// パラメータ名
jql

// パラメータ値
updated >= -4w AND (assignee = currentUser() OR reporter = currentUser() OR comment ~ currentUser() OR text ~ currentUser() OR watcher = currentUser() OR voter = currentUser() OR creator = currentUser()) ORDER BY updated DESC, status DESC, lastViewed DESC
// Basic Auth
{{ログインメールアドレス}}|{{トークン}}

JQL と言われる JIRA 上でチケットを探すためのクエリを使い、あらゆる箇所に自分が含まれているかを探すような記載をしている。加えて、最終更新が 4 週間以内のものに絞っているので、古いものは出てこない。

詳しい開設はコッチ。
高度な検索 - アトラシアン製品ドキュメント

Basic Auth は JIRA からアクセストークンを発行して、メールアドレス|アクセストークンの形で記載する。
ここからできる。
https://id.atlassian.com/profile/profile.action

ステップ 2: Code > Run by Javascript

JSON で返ってきたデータをそのままだと Slack にまとめてウマいこと送ることが出来ないので、スクリプトで頑張る。

// Input Data
// 以下の変数に同様の名前のJIRAからのレスポンスをマッピングする
issue_summary
issue_status_name
issue_key
let text = [];
const issue_summary = inputData.issue_summary.split(',');
const issue_status_name = inputData.issue_status_name.split(',');
const issue_key = inputData.issue_key.split(',');

for (let i = 0, m = issue_summary.length; i < m; i++) {
  text.push(`[${issue_status_name[i]}] ${issue_summary[i]} https://XXXXX.atlassian.net/browse/${issue_key[i]}`);
}

output = [{text: text.join('\n')}];

Zapier と、ここの split の都合によって、JIRA チケット名に「,」が入っているとうまく表示されないので注意が必要。
とって来た値でうまく回させてくれ!

ステップ 3: Slack > Send channel message

あとは整形されたチケット一覧を Slack に送るだけ。

出力結果

最終的にこのように表示される。

全面モザイクで申し訳なさあるけど見せられないんや…スマン…

map の構造をチェックした

テストを書くとき、値はどうでも良いが、キーだけちゃんとチェックしたい。要はmapなんだけどものによって期待する構造が微妙に違うんじゃ????ってときがあったのでやってみる。
ふわっとした形をがんばって扱うのGo言語っぽくない気がするーーー、うーーーーーーんんんん。。

package hoge_test

import (
	"testing"
	"sort"
	"reflect"
)

func checkMapStructure(t *testing.T, expectMap map[string]interface{}, actualMap map[string]interface{}) {
	var expectKeys []string
	var actualKeys []string

	for expectKey, expectVal := range expectMap {
		expectKeys = append(expectKeys, expectKey)

		if expectChildMap, ok := expectVal.(map[string]interface{}); ok {
			actualVal, ok := actualMap[expectKey]
			if !ok {
				t.Fatalf("wants actualMap[%+v], but not found", expectKey)
			}

			actualChildMap, ok := actualVal.(map[string]interface{})
			if !ok {
				t.Fatalf("wants actualMap[%+v] is map, but not map", expectKey)
			}

			checkMapStructure(t, expectChildMap, actualChildMap)
		}
	}

	for actualKey := range actualMap {
		actualKeys = append(actualKeys, actualKey)
	}

	sort.Strings(expectKeys)
	sort.Strings(actualKeys)

	if !reflect.DeepEqual(expectKeys, actualKeys) {
		t.Fatalf("expectKeys = %s, actualKeys = %s", expectKeys, actualKeys)
	}
}

func TestHoge(t *testing.T) {
	x := map[string]interface{}{}
	ex := map[string]interface{}{}

	x["1"] = 111
	x["huga"] = map[string]interface{}{
		"a": 1,
		"b": 1,
		"c": 1,
	}

	ex["1"] = "hoge"
	ex["huga"] = map[string]interface{}{
		"a": "1234",
		"b": "1234",
		"c": "1234",
	}

	checkMapStructure(t, ex, x)

	t.Fail()
}

とりあえず string なキーだけやっているけれど reflect.DeepEqual で検証しているので string じゃなくてもなんでもできると思う。
逆に string 以外のキーを持った map って需要あるんかな、わからん。

PHP の $_REQUEST を Golang でもやりたい

いや要らないだろ…と思いつつもやる必要がうっすらとでてきたのでやってみた。

ようは、クエリパラメータと POST の中身とクッキーから読み取れれば良い。

PHP: $_REQUEST - Manual

やや雑ではあるがお試しするとこんな感じになると思う。

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"time"
)

func PHP_REQUEST(r *http.Request, key string) string {
	// Get parameter
	if val, ok := r.URL.Query()[key]; ok {
		return val[0]
	}

	// POST
	tmp := r.PostFormValue(key)
	if tmp != "" {
		return tmp
	}

	// Cookie
	for _, cookie := range r.Cookies() {
		if cookie.Name == key {
			return cookie.Value
		}
	}

	return ""
}

func main() {
	go func() {
		http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
			w.Write([]byte("hoge = " + PHP_REQUEST(r, "hoge")))
		})
		http.ListenAndServe("0.0.0.0:8000", nil)
	}()

	time.Sleep(time.Millisecond)

	var client *http.Client
	var r *http.Response
	var b []byte

	// nothing
	client = &http.Client{}
	r, _ = client.Get("http://0.0.0.0:8000/test")
	b, _ = ioutil.ReadAll(r.Body)
	fmt.Println(string(b))

	// get
	client = &http.Client{}
	r, _ = client.Get("http://0.0.0.0:8000/test?hoge=get")
	b, _ = ioutil.ReadAll(r.Body)
	fmt.Println(string(b))

	// get / cookie
	client = &http.Client{}
	u, _ := url.Parse("http://0.0.0.0:8000/test")
	client.Jar, _ = cookiejar.New(nil)
	client.Jar.SetCookies(u, []*http.Cookie{
		{
			Name:  "hoge",
			Value: "cookie",
		},
	})
	r, _ = client.Get("http://0.0.0.0:8000/test")
	b, _ = ioutil.ReadAll(r.Body)
	fmt.Println(string(b))

	// post
	client = &http.Client{}
	v := url.Values{}
	v.Add("hoge", "post")
	r, _ = client.PostForm("http://0.0.0.0:8000/test", v)
	b, _ = ioutil.ReadAll(r.Body)
	fmt.Println(string(b))

	time.Sleep(time.Millisecond)
}

視認しにくいテキストを探す | 最終回 puppeteer を使って任意の web サイト上のテキストに対してコントラストレートを計算する

前回までは色差をいい感じに計算して、じゃあどれくらい近いんですか!を感覚で設定していました。
視認しにくいテキストを探す | その 2 puppeteer を使って任意の web サイト上のテキストに対して色差を計算する | ごみばこいん Blog

でもそういうのって誰かが研究した成果だったり指針が一定あるはずで、それがアクセシビリティという形でまとまっているのです。

Web Content Accessibility Guidelines (WCAG) 2.0

コントラストレート

ところで、いま Chrome の DevTools を見ると、文字色のカラーピッカーにコントラストレートという情報が増えていることに気づきました。

このコントラストレートは、アクセシビリティの話の中でも出てきます。

Web Content Accessibility Guidelines (WCAG) 2.0 - 1.4.3 Contrast (Minimum)

WebAIM: WebAIM's WCAG 2 Checklist

おおざっぱにいうと、背景と文字とのコントラスト比が 3:1 とか 4.5:1 を超えないと見にくいテキストですよね、という判断のようです。

というわけで、今回はコントラストレートを使って、見にくいテキストを探してみます。

Chrome 上の実装

ところで Chrome ではコントラストレートの計算はどのように実装されているのでしょうか。
透明度もいい感じになっているのでしょうか。
その謎を知るためにソースコードへ飛び立ちます。

Code Search

ソースの検索が提供されているので、ここで、画面上にでている文言をさがして追いかけていきます。
検索してみるとそれっぽいソースが出てきました。

https://cs.chromium.org/chromium/src/third_party/blink/renderer/devtools/front_end/color_picker/ContrastDetails.js?q=contrast+ratio+lang:%5Ejavascript$&sq=package:chromium&dr=CSs&l=5

ここからコントラストレート AA の判定を追ってみます。

https://cs.chromium.org/chromium/src/third_party/blink/renderer/devtools/front_end/color_picker/ContrastDetails.js?sq=package:chromium&dr=CSs&g=0&l=121

const aa = this._contrastInfo.contrastRatioThreshold('aa');
this._passesAA = this._contrastInfo.contrastRatio() >= aa;

this._contrastInfo.contrastRatioThreshold('aa') が AA のコントラストレート基準値を持ってきているようです。
this._contrastInfo.contrastRatio() は指定された要素のコントラストレートを計算していそうです。

まずは this._contrastInfo.contrastRatioThreshold から追ってみます。

https://cs.chromium.org/chromium/src/third_party/blink/renderer/devtools/front_end/color_picker/ContrastInfo.js?sq=package:chromium&dr=CSs&g=0&l=147

contrastRatioThreshold(level) {
  if (!this._contrastRatioThresholds)
    return null;
  return this._contrastRatioThresholds[level];
}

this._contrastRatioThresholds という配列から値を取っているようです。
level は'aa'が入ってきます。

this._contrastRatioThresholds が設定されるところを見てみます。

https://cs.chromium.org/chromium/src/third_party/blink/renderer/devtools/front_end/color_picker/ContrastInfo.js?sq=package:chromium&dr=CSs&g=0&l=32

/**
 * @param {?SDK.CSSModel.ContrastInfo} contrastInfo
 */
update(contrastInfo) {
  ...

  if (contrastInfo.computedFontSize && contrastInfo.computedFontWeight && contrastInfo.computedBodyFontSize) {
    this._isNull = false;
    const isLargeFont = ColorPicker.ContrastInfo.computeIsLargeFont(
        contrastInfo.computedFontSize, contrastInfo.computedFontWeight, contrastInfo.computedBodyFontSize);

    this._contrastRatioThresholds =
        ColorPicker.ContrastInfo._ContrastThresholds[(isLargeFont ? 'largeFont' : 'normalFont')];
  }

  ...
}

... で一部省略していますが、キモは残しています。

この update というメソッドは外から定期的に呼ばれるようです。どこから呼ばれるのかちょっとわからず。。察するに要素が更新されたり、値が更新されるときに呼ばれるものだと思います。

肝心の this._contrastRatioThresholds は ColorPicker.ContrastInfo._ContrastThresholds から取得して、
そのために isLargeFont の判定をしています。

isLargeFont の判定に必要な、引数で渡ってくる contrastInfo はSDK.CSSModel.ContrastInfo という型です。

そこを追ってみます。

https://cs.chromium.org/chromium/src/third_party/blink/renderer/devtools/front_end/sdk/CSSModel.js?sq=package:chromium&dr=CSs&g=0&l=644

/** @typedef {{backgroundColors: ?Array<string>, computedFontSize: string, computedFontWeights: string, computedBodyFontSize: string}} */
SDK.CSSModel.ContrastInfo;

んー、ちょっとよく分からないですね。
devtools の js サイドではなく、もっとネイティブ側でもっている値なのでしょうか。

ある要素に対するすべての背景色と、フォントサイズ、フォントの太さ、本文フォントサイズが入っているオブジェクトのようです。

続いて isLargeFont の計算をしている箇所です。

https://cs.chromium.org/chromium/src/third_party/blink/renderer/devtools/front_end/color_picker/ContrastInfo.js?sq=package:chromium&dr=CSs&g=0&l=159

static computeIsLargeFont(fontSize, fontWeight, bodyFontSize) {
  const boldWeights = ['bold', 'bolder', '600', '700', '800', '900'];

  const fontSizePx = parseFloat(fontSize.replace('px', ''));
  const isBold = (boldWeights.indexOf(fontWeight) !== -1);

  const fontSizePt = fontSizePx * 72 / 96;
  if (isBold)
    return fontSizePt >= 14;
  else
    return fontSizePt >= 18;
}

先程の SDK.CSSModelContrastInfo から渡されるフォントサイズと太さの情報を使って、大きいフォント判定をします。
これはアクセシビリティのサイトに書いてあった通りのものです。

Web Content Accessibility Guidelines (WCAG) 2.0 - large scale (text)

そして ColorPicker.ContrastInfo._ContrastThresholds です。

https://cs.chromium.org/chromium/src/third_party/blink/renderer/devtools/front_end/color_picker/ContrastInfo.js?sq=package:chromium&dr=CSs&g=0&l=178

ColorPicker.ContrastInfo._ContrastThresholds = {
  largeFont: {aa: 3.0, aaa: 4.5},
  normalFont: {aa: 4.5, aaa: 7.0}
};

ラージフォントか通常かによって aa と aaa の基準値が設定されています。
こちらもアクセシビリティのサイトに書いてあったものです。

> The visual presentation of text and images of text has a contrast ratio of at least 4.5:1 except for the following: (Level AA)
> Large Text: Large-scale text and images of large-scale text have a contrast ratio of at least 3:1;

 

ここまでスレッショルドの取得は追いかけ終わりました。

次に this._contrastInfo.contrastRatio() を追います。

https://cs.chromium.org/chromium/src/third_party/blink/renderer/devtools/front_end/color_picker/ContrastInfo.js?sq=package:chromium&dr=CSs&g=0&l=81

contrastRatio() {
  return this._contrastRatio;
}

メソッドは単に getter になっているだけですね。

https://cs.chromium.org/chromium/src/third_party/blink/renderer/devtools/front_end/color_picker/ContrastInfo.js?sq=package:chromium&dr=CSs&g=0&l=140

this._contrastRatio = Common.Color.calculateContrastRatio(this._fgColor.rgba(), this._bgColor.rgba());

計算された値が設定されていそうです。
_fgColor と_bgColor は要素の color と background-color に当たるものでしょうか。

https://cs.chromium.org/chromium/src/third_party/blink/renderer/devtools/front_end/color_picker/ContrastInfo.js?sq=package:chromium&dr=CSs&g=0&l=12

/** @type {?Common.Color} */
this._fgColor = null;

/** @type {?Common.Color} */
this._bgColor = null;

コントラストレート計算の処理を追ってみます。

https://cs.chromium.org/chromium/src/third_party/blink/renderer/devtools/front_end/common/Color.js?dr=CSs&q=Common.Color&sq=package:chromium&g=0&l=364

/**
 * Calculate the contrast ratio between a foreground and a background color.
 * Returns the ratio to 1, for example for two two colors with a contrast ratio of 21:1, this function will return 21.
 * See http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
 * @param {!Array<number>} fgRGBA
 * @param {!Array<number>} bgRGBA
 * @return {number}
 */
static calculateContrastRatio(fgRGBA, bgRGBA) {
  Common.Color.blendColors(fgRGBA, bgRGBA, Common.Color.calculateContrastRatio._blendedFg);

  const fgLuminance = Common.Color.luminance(Common.Color.calculateContrastRatio._blendedFg);
  const bgLuminance = Common.Color.luminance(bgRGBA);
  const contrastRatio = (Math.max(fgLuminance, bgLuminance) + 0.05) / (Math.min(fgLuminance, bgLuminance) + 0.05);

  for (let i = 0; i < Common.Color.calculateContrastRatio._blendedFg.length; i++)
    Common.Color.calculateContrastRatio._blendedFg[i] = 0;

  return contrastRatio;
}

重たいロジックが出てきましたが、見るとリンクが付いています。
それを実装しているだけのようです。

Web Content Accessibility Guidelines (WCAG) 2.0 - contrast ratio

というところで、ここから先は追わなくてもいいかなと思ったので止めておきます。
ロジックのざっくりとした理解では、背景色と文字色をブレンドして、明るさを計算して、いい感じの係数を合わせたら出来上がり!という具合でしょうか。

 

ここまでで AA の判定をすることができます。
大きくやっていることは 2 つで、コントラストレートのしきい値を計算することと、実際のコントラストレートを計算すること、です。

実装してみる

該当のソースは devtool の中にいるので puppeteer からウマいこと利用する手立てはないと思います。
ので、前回までと同様に、開いたページに対してスクリプトを実行する形でやってみます。

前回もお世話になった chroma を使うと2つの色に対してコントラストレートを計算することができます。

chroma.js api docs! - chroma.contrast(color1 color2)

でもってソースはこんな感じで try puppeteer してみます。
(前回とほぼ同じソース)

// ブラウザの起動
const browser = await puppeteer.launch();
const page = await browser.newPage();
page.setViewport({
    width: 1600,
    height: 900,
})

// デバッグ用に console.log を nodejs 側に渡す
page.on('console', msg => console.log(msg.text()));

// サイトにアクセスする
await page.goto('https://gomiba.co.in/test/color_difference.html');

// 色差を計算するために chroma をページ内で読み込む
await page.addScriptTag({
    url: 'https://cdnjs.cloudflare.com/ajax/libs/chroma-js/1.3.6/chroma.min.js',
});

// 色差が一定以上のものを探す
await page.evaluate(() => {
    // 全要素を探索していく
    document.querySelectorAll('*').forEach((element) => {
        // テキストノード、または SVG を子に持っている要素を探す
        const foundChildNode = Array.prototype.filter.call(element.childNodes, (e) => {
            let status = false;
            status = status || (e.nodeType === Node.TEXT_NODE && e.textContent.trim().length > 0);
            status = status || e.nodeName.toLowerCase() === 'svg';
            return status;
        });
        if (foundChildNode.length === 0) {
            return;
        }

        // 計算されたスタイルから色を取得
        const elementStyle = window.getComputedStyle(element);
        const fontColor = elementStyle.color;
        const backgroundColor = elementStyle.backgroundColor;

        // コントラストレートを計算する
        const contrastRatio = chroma.contrast(fontColor, backgroundColor);

        // フォントサイズからコントラストレートのしきい値を決める
        const fontSizePx = parseInt(elementStyle.fontSize);
        const fontSizePt = fontSizePx * 72 / 96;
        const isLargeFont = fontSizePt >= 18;
        const contrastRatioThreshold =
              isLargeFont ? 3.0 : 4.5
      
        // しきい値を超えたもの = 見やすいものを色付けする
        console.log(element.nodeName + " : " + contrastRatio)
        if (contrastRatio > contrastRatioThreshold) {
          element.style.cssText = element.style.cssText + 'border: 5px dashed red !important;';
        }
    });
});


// スクリーンショットを撮ってみる
await page.screenshot({path: 'example.png'});

// ブラウザを終了する
await browser.close();

isLargeFont のところで太字判定をサボっていたりしますが、とりあえずのところは。
前回の記事では見にくい!を出していましたが、わからんので逆にして、見やすいものだけをマークするようにしました。

これを試すとこんな感じになります。

が、これがどうなのかわからない…、ので、前回の結果と見比べられるようにしました。

要素の左側に点線があるのが今回、要素の右側に点線があるのが前回の検出した見やすいものです。

こうしてみると、前回のときにチョット見やすいとはいいにくいなあというものも今回のでいい感じに検出されているような気がします。
とはいえ、前回の方法でもしきい値がかなりガバガバなので、もうちょっと詰めていくと、良い結果がでるんじゃないかなあと。

 

とりあえず、コントラストレートを計算してあげるのがアクセシビリティの確認方法として挙げられているので
手動でいい感じに検出するなら今回の方法でやるほうがよさそうです。

その他

lighthouse を使うとアクセシビリティ以外にも様々な観点について自動チェックできておすすめです。

GoogleChrome/lighthouse: Auditing performance metrics and best practices for Progressive Web Apps

今回はとりあえず色の様子だけシュッと見たかったので、単品で調査する方法がないかなあと取り組んでみたものです。

mapから指定したキーを検索するやつ

map[string]interface{} が入れ子になったものを扱うときがあって
たぶん、こういうなんだかわからないけど、いい感じに区分けされたデータ列があったときに使うんじゃないかな、きっと。
(前段階のアプローチがそもそも悪いかも)

{
  "case-1": {
    "a" : {
      "id" : 1
    },
    "b" : {
      "id" : 2
    }
  },
  "case-2": {
    "c" : {
      "id" : 3
    }
  }
}

その中から特定のキーをいい感じに取り出したいなあってなったので作ってみた。
GitHub - sters/maputils

 

余談になるけど
どこかで再帰呼び出しとループだと、ループのほうが速いよって書いてあったので、再帰的にキーを探すところはループでやってみた。

それでもって、ベンチも取ってみた。
gist:521e2be8b3aeed0c87fe75f7335f259e

謎な map の作り方してるけど、これくらいの規模感でやらないと、手元だとまったくもって差がでなかった。
逆にいえば、これくらいの規模感を取り扱う or これくらい負荷がかかるようになって初めて効果あるくらいなのかあという気持ち。


書いてて思ったけど []struct から特定のフィールドだけ取り出すほうが需要高そうだしもうありそう。

1 2 3 4 5 6 7 8 9 10 11 12