JavaScript に関する投稿を表示しています

視認しにくいテキストを探す | 最終回 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

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

日本語の折り返しを正規表現で解決する mikan.js を PHP に書き換えた

できたものがこちらです

作った背景

日本語の折り返しが中途半端になってつらい!機械学習で改善するぞ!という話が過去にありました。
google/budou: Budou is an automatic organizer tool for beautiful line breaking in CJK (Chinese Japanese and Korean).

それからしばらくして、いやいや機械学習じゃなくてもいいのでは?というものが出てきました。
mikan.js : 機械学習なしで、日本語の単語の改行を処理するライブラリを書いた

これって別に JS で表示するときにアレコレしなくても、普段のサーバサイドでいい感じにしてもいいのでは??と思い PHP に移植してみた次第です。

移植する流れ

元のコードを眺めて、同じような処理に書き換えていく簡単なおしごと。
幸いにも、そこまで難しいロジックでは無いので、動作をみながら書き換えていきました。

正規表現のあたりだけ、言語の違いでちょっと詰まったので、ドキュメントを見ながら動作を見ながら随時書き換えていくようなコツコツ作業でした。

言語の書き換え、双方の言語について理解が深まるのでオススメです。

マウスホバーでAjaxしてコンテンツを先読みキャッシュ

なんとなく思いついて、このブログに入れてみて機能の紹介。

link rel=prefetch

HTML の機能として、指定した URL の内容を先読みして、内容をキャッシュしておいてくれるものがあります。
詳しいことは MDN を見るとよいかとー。

rel="preload" によるコンテンツの先読み - HTML: HyperText Markup Language | MDN
Link prefetching FAQ - HTTP | MDN

preload と prefetch って??という場合にはこちらの記事もおすすめです。

Preload を用いたリソースプリローディングの最適化 | blog.jxck.io

なぜ onHover で prefetch ?

ちゃんと計測したことないのでわからないですけど、リンクをセカセカ、サササッ!とクリックすること、あまり無いですよね?読み物系ならとくに。

わずかにホバーする時間があってからクリックするのかなあと思ったので
じゃあそのタイミングで先読みしたら爆速 Web サイトが体験できるんじゃ?と思った次第です。

コード

// prefetch link
let prefetchTimers = {};
let prefetcedLinks = {};
document.querySelectorAll('a').forEach((element) => {
    element.addEventListener('mouseenter', (event) => {
        if (element.href.indexOf('#') !== -1 || element.href.indexOf('javascript:') !== -1) {
            return;
        }
        if (element.target === '_blank') {
            return;
        }
        if (prefetcedLinks[element.href] === true) {
            return;
        }
        prefetchTimers[element] = setTimeout(() => {
            const prefetchTag = document.createElement('link');
            prefetchTag.rel = 'prefetch';
            prefetchTag.href = element.href;
            document.head.appendChild(prefetchTag);
            prefetcedLinks[element.href] = true;
        }, 300);
    });

    element.addEventListener('mouseleave', (event) => {
        if (prefetchTimers[element] !== undefined) {
            clearTimeout(prefetchTimers[element]);
            delete prefetchTimers[element];
        }
    });
});

遷移先別に、ホバー用タイマーと読み込み有無を記録しています。
ホバーから外れるとタイマーを削除し、ホバーが外れずにタイマーが経過すると prefetch が登録されます。

爆速 web サイトは体験できるのか?

prefetch 無し

prefetch あり

1 秒ちょっとかかっていたのが 300ms ほどに収まるようになりました!すごい!

まとめ

prefetch で爆速 web 体験ができるかも。
結局作ってみた満足度が高いことが主になっているので、ガチプロダクトでやってくなら本当に効果あるの?その機能使われている?は別途ちゃんと計測しないとねーーー。
あとは prefetch すればギガ減るよね、というかスマホ対応できないなこれ。スマホ対応ってどうすんだろ。

視認しにくいテキストを探す | その 2 puppeteer を使って任意の web サイト上のテキストに対して色差を計算する

前回は色差を計算する方法の簡単な紹介と chroma.js を使った計算について扱いました。
視認しにくいテキストを探す | その 1 chroma.js を使って色差を計算する | ごみばこいん Blog

今回は実践編として、特定の Web ページに対して色差を計算して「視認しにくいテキストを探す」ことをやります。

Web ページに対して色を確認するには、キャプチャしたものから気合で探すか、 CSS の結果を持って確認するか考えられそう。ここでは後者を puppeteer でやってみます。
puppeteer は以前紹介したとおり、 Headless Chrome を Node.js からお手軽に扱うためのものです。
Node.js から Headless Chrome を操作できる puppeteer を試す | ごみばこいん Blog

ページを読み込んで色差を計算するためにこんなコードを書きました。

// ブラウザの起動
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 を子に持っている要素を探す
        let 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;
        }

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

        // 色差を計算する
        let colorDiff = chroma.deltaE(fontColor, backgroundColor);

        // 計算された色差に透明度の色差を加える(透明度の差分 * 0.5 * 100)
        colorDiff += (Math.abs(chroma(fontColor).alpha() - chroma(backgroundColor).alpha()) * 0.5) * 100;

        // 色差が大きいものは無視
        if (colorDiff > 40) {
            return;
        }

        // 色差が小さいものに色付けをする
        console.log(element.nodeName + " : " + colorDiff)
        element.style.cssText = element.style.cssText + 'border: 5px dashed red !important;';
    });
});


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


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

puppeteer の playground がオンラインで提供されているので、コピペで動作確認できるはず、お試しあれ。
Try Puppeteer

手元に Node.js の環境があるなら、こちらのリポジトリでも。コマンドライン引数で対象 URL とキャプチャの保存するファイル名を受け取って、マーカーが引かれたキャプチャ画像が出力されます。
sters/web-color-difference

たとえば medium でやってみるとこんな感じ。白背景の部分で、薄めグレーの箇所ががマークされている。

たとえば、ランダムに色を設定するページを用意して試してみるとこんな具合。

確かに見にくい!!!なものはマークされているようだけど、ちょっと怪しいライン。もうちょっと基準値を調整したり CIEDE2000 のロジックも試すかなどして詰めたほうがいいかも…?とりあえず、のところはこれでもいいんじゃなかろーか。
元々やりたかった「視認しにくいテキストを探す」は出来たとしておしまい。

…と思ったけど、視認しにくいテキストっていうと話題が広いな、小さすぎる文字とか他の要素と被っているとかそういうのもそうなんだよねきっと、うーん。

視認しにくいテキストを探す | その 1 chroma.js を使って色差を計算する

SEO が云々でも色被ってるの良くないよねっていうけど、いや普通にユーザビリティ悪いやん、適当に色選ぶと見にくいこと往々にしてあるやん、って思ったので、そういう何かがあるのかなあって思って調べたら、 Wikipedia 先生にそのまんまの項目が記載されていた。ので、これをまとめつつ、ライブラリがあったのでそれを使ってみる話。

数式、もといロジックをざっくり読んでみる編

前提として Wikipedia のものをガンガン鵜呑みにしていく。
数式とかなんでそうなの?みたいな理解はせず、そういうものがあるんだなーくらいに留める。

色差を計算するには?

Wikiepedia に色差というページがあって、もうこれ。英語版と微妙に内容が違うけど、それは後で触れる。

色差 - Wikipedia

色差を計算するには単純に距離を計算しては、人間の感覚とはだいぶずれてしまってだめ。どうやら CIEDE2000 というものを使うとよいらしい。 CIE は国際照明委員会のことで DE は Delta E だ。2000 は年代。1976 、 1994 と研究され数式が更新されている。で、この CIEDE2000 を計算するには Lab 色空間を使う必要がある。
ただ、ぼくらが普段生きている世界では RGB とか CMYK とか HSV とか HSL とかなので、そこから変換をしてあげないといけない。いや、デザイナーさんとかその手のグラフィック関連に詳しい人なら Lab の世界でも生きているのかもしれない。

(参考)
カラーネーム/RGB/HSL ウェブでのカラー指定いろいろ | アライドアーキテクツのクリエイターブログ

Lab 色空間に生きたい

Lab 色空間も Wikipedia 上にページがある。

Lab色空間 - Wikipedia

単に Lab というと Hunter と CIE の 2 種類があるが CIE の方を最近では指すらしいので、これを使ったらいいみたい。これまた国際照明委員会が決めたもので、明度( L )と、いい感じの色 a と b を使って示し、人間の目で見える全ての色を記述できて機器固有の基準として使えるようにしたものだそうだ。

RGB からどうやって Lab に変換するんだろうねっていうと、 RGB は機器固有の部分があるので、一発変換!とはできず、 RGB を XYZ 色空間 に変換する必要がある。

RGB の世界から XYZ の世界へ移動する

これまた Wikipedia 上にページがある。

CIE 1931 色空間 - Wikipedia

すげーざっくりの理解では「研究成果の結果、 RGB に対する人の感覚を重み付けしたもの」。

ここまでをまとめると?

RGB → XYZ → Lab → 色差が計算できる!

後に回した英語との差分って?

色差の英語版のページを見てみる。
Color difference - Wikipedia

すると「 CMC l:c (1984) 」という日本語にはない数式が出ている。なんか良いやつなんじゃないかな、きっと。色っぽい単語で調べてたらその手の調査したよって論文もでてきた。

DDCPにおける色域外特色近似再現のための色差式評価

優位性とか書いてあるので気になった時に読むとよさそうな気がする。

ライブラリの出番です chroma.js

こう研究された成果があるわけなので、まあきっと便利に使えるようにライブラリ作っている人とかいるんだろうなあ~~とか思っているとやっぱりある。

chroma.js api docs!

色関係の話題をいい感じに取り扱ってくれるライブラリだそうで、各種色空間の変換も任せろ、グラデーションも任せろ、な強いライブラリ。

chroma.js を使って色差を計算する

単純な距離、ユークリッド距離の計算をするには chroma.distance を使う。 mode パラメータで色空間の指定が出来るので、2つの色の間の位相をみたりなんかにもできそうだ。
色差を計算するには chroma.deltaE を使う。ご丁寧に deltaE という命名になっててわかりやすいぞ~。内部的には CMC を使っているらしい。 CIEDE じゃないんだ。

それを実際に試してみようってわけで、こんな HTML + JS を書いて様子を見てみる。

<script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/1.3.6/chroma.min.js"></script>

<style>
.color-difference td {
    min-width: 15px;
    min-height: 15px;
    padding: 3px;
}
</style>

<table class="color-difference">
    <tr>
        <td></td>
        <td></td>
        <td>distance(RGB)</td>
        <td>deltaE</td>
    </tr>
</table>

<script>
let colors = [
    ['rgb(255,0,0)', 'rgb(0,255,0)'],
    ['rgb(255,0,0)', 'rgb(0,0,255)'],
    ['rgb(255,0,0)', 'rgb(255,255,0)'],
    ['rgb(255,0,0)', 'rgb(255,0,255)'],
    ['rgb(255,0,0)', 'rgb(0,255,255)'],
    ['rgb(255,0,0)', 'rgb(255,128,0)'],
    ['rgb(255,0,0)', 'rgb(255,0,128)'],
    ['rgb(255,0,0)', 'rgb(255,255,255)'],
    ['rgb(255,255,0)', 'rgb(255,0,255)'],
    ['rgb(255,255,0)', 'rgb(0,255,255)'],
    ['rgb(255,255,0)', 'rgb(255,255,255)'],
];

let table = document.querySelector('.color-difference');

colors.forEach((color) => {
    let tr = document.createElement('tr');
    tr.innerHTML = `
        <td style="background-color:${color[0]}"></td>
        <td style="background-color:${color[1]}"></td>
        <td>${chroma.distance(color[0], color[1], 'rgb')}</td>
        <td>${chroma.deltaE(color[0], color[1])}</td>
    `;
    table.appendChild(tr);
});
</script>

こんな出力が得られる。

  • 赤と緑、青よりは、黄ピンクのほうが気持ち近い。水色、距離は遠いんだけど色差はあまり変わらず
  • 赤とオレンジ、マゼンダな色はやっぱり色差が少ない
  • 赤と緑や青を比べるより、赤と白を比べたほうが色差がすくない(!)
  • 黄とマゼンダよりも、黄と水、黄と白のほうが色差がすくない(!)
  • 距離 = 色差 に直結せず、色によって微妙に重み付けがされているのを感じる

わりと見た感覚と近い気がするのと、数字として出るとさらに面白い。

その 2 を書いた。

https://gomiba.co.in/blog/archives/1841

WordPress にプラグインを使わずに JavaScript だけで目次機能をつけてみた

目次機能が出来るプラグインとかってあるけど、別に自分で作ってもいいよねって思ったので作った。そのうちモリモリっとテーマやプラグインの整備に力を入れていくので、そのあたりに介入されると困るので~~ってくらいの理由で作った次第。

これこれ!この目次! h2 タグを使うんだよ

2階層までなら対応するよ! h3 タグだよ

ここは h3 タグ

ここは h2 タグなので 1 階層目になる

ここは 2 階層。 h3 タグだからね

デモおわり

ここまで見出しだらけで見にくいのは仕方ない。

仕組み

  1. h2 タグと h3 タグを投稿箇所から探す
  2. それぞれのタグに ID をつける
  3. それぞれへのリンクを生成する
  4. 目次用の HTML を作る
  5. 一番最初の見出しの直前に HTML を挿入する

タイトルこそ WordPress って言ってるけど JavaScript が差し込めるブログサービス的なものなら、投稿箇所を探す部分を調整する必要はあるけど、どれでもできるんじゃないかな。
jQuery を使わなくていいように作ったので jQuery ないんだけど、みたいな環境でもいいと思う。いやそもそもそういう環境はまた違う問題っぽいけど…。

JavaScript

(function() {
	setTimeout(function() {
		var headingIndex = 1;
		Array.prototype.forEach.call(document.querySelectorAll('.post_content'), function(postContent) {
			if (postContent.innerText.length < 100) {
				return;
			}

			var headings = postContent.querySelectorAll('h2,h3');
			if (headings.length <= 0) {
				return;
			}

			var indexHtml = '
    '; var lastElement = 'H2'; Array.prototype.forEach.call(headings, function(e) { var id = 'heading-' + headingIndex; e.id = id; if (lastElement != e.tagName) { if (lastElement == 'H2') { indexHtml += '
      '; } else { indexHtml += '
    '; } lastElement = e.tagName; } indexHtml += '
  • ' + e.innerHTML + '
  • '; headingIndex++; }); indexHtml += '
'; var wrapper = document.createElement('div'); wrapper.classList.add('content-index'); wrapper.innerHTML = indexHtml; headings[0].parentNode.insertBefore(wrapper, headings[0]); }); if (location.hash.length > 1) { setTimeout(function() { var element = document.querySelector(location.hash); if (element != null) { window.scroll(0, element.offsetTop); } }, 10); } }, 10); })();

CSS

.post_content .content-index {
	border: 2px solid #eee;
	border-radius: 20px;
	padding: 20px;
	background-color: #fefefe;
}

.post_content .content-index:before {
	content: '目次';
	display: block;
	font-size: 24.5px;
	margin-bottom: 15px;
}

他のスタイルとのバッティング回避した部分を除くとこれだけ。シンプルに線を引くことと色味の調整くらい。

SEO的な観点とか

目次足すと SEO に効果あるのかわからんけど、ふつうに見ていて、長めの投稿だったら合ったほうが便利だよね、とは感じる。

一応 Fetch as Google をして Google Bot にこの目次が認識されていることは確認したので、まあいいんじゃないかな。

激安 JS を作ってみた

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

こんにちは。ごみばこです。

某所で激安 CSS が盛り上がり、実際に使うとなったら JS に全部書かれていて、数字だけ入った要素に適用されると嬉しいだろうなあ~、と思ったので、作ってみました。

※激安 CSS についてはこちら > http://qiita.com/shiozaki/items/0e42e101b7483df13c8f

ソースはここ(gist)
✨安い!!お得!!激安JS!!✨

せっかくなので、ただ作るではなく、普段書かないようなメソッドや記述を盛り込んでみようかなあと。

  • ライブラリを使わない
  • document.querySelector と document.querySelectorAll で要素検索
  • テンプレートリテラルで CSS の定義
  • innerHTML じゃなくて createElement で地道に作っていく
  • let! let!
  • アロー関数でクロージャ

 

↓実際に実行するとこちらになります

 

あの大人気の掃除機が今だけナント!49800


<p>あの大人気の掃除機が今だけナント!<span class="gekiyasu">49800</span></p>
<script src="/tool/gekiyasu.js"></script>
<script>gekiyasu.initialize();</script>

 

元の CSS 作者に圧倒的感謝?

Web Audio API と IBM Bluemix のワトソン君で音声認識(追記あり)

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

こんにちはごみばこです。

この記事はteratailの「Bluemixアドベントカレンダー」の参加記事として書かれました。

昨日は yutori_828 さんによる「クラウドx IoT女子会で推し(二次元)の話をしてきた」でした。
クラウドx IoT女子会で推し(二次元)の話をしてきた – ユニバーサルIoT女子文明

やりたいこと

最近いろいろな人と話すことが多く、手書きなりPCなりスマホなりでメモしているものの、すべてメモしているわけではありませんし、覚えていないことも多くあります。じゃあお手軽にメモするソリューションないかなあというところで、パッと思い浮かんだのが録音×音声認識です。

スマホで録音しつつ、音声認識しつつ、クラウド上にメモすることができれば最高なのでは…?

こんなところから始まっています。

できたもの

こんなかんじのやつ。

watson_audio

※編集していません。クリックしてからしゃべっています。
※5秒録音→文字起こし→5秒録音→文字起こし→... という処理になっています。

使ったもの

IBM Bluemix

アドベントカレンダーのテーマにもなっている、IBMが提供するAPIやPaaSといったサービスの総称です。

簡単に登録手順をおさらいしておきます。

Bluemix への登録

bluemix_register

以下の URL から Bluemix への登録ができます。

https://console.ng.bluemix.net/registration/

僕の場合は IBM ID を登録してから Bluemix をしましたが、Bluemix だけの登録もできるようです。

登録するとメール認証が求められます。メールからリンクを踏むと登録完了です。ここから30日間はトライアルということで無料で Bluemix のすべて(?)の機能を使うことができます。思う存分に楽しみましょう!

アプリケーションの登録

Bluemix では Cloud Foundry アプリ と呼ばれる、heroku のような、環境が整っていてコードを自由に設置することができる、PaaS を利用することで、Web サイトを制作することができます。

Cloud Foundry アプリは以下のような手順で登録していきます。

アプリケーションの画面を開いて新規追加。

bluemix_app

テンプレートが選べるので使いたいものを選びます。僕はPHPを選択しています。

bluemix_app_template

あとは勝手に環境が作られ、アプリケーションが動き始めます。すごい。

bluemix_app_run

この画面上で「アプリの表示」を押すと、ブラウザでその Cloud Foundry アプリの URL が開かれます。この URL で確認をしていくことになります。

IBM Bluemix Watson

IBM Bluemix には Watson という人工知能なAPI群があります。この中に Speech to Text という音声認識を行うことのできるAPIがあります。ここに対して、音声データを送ることで、Watsonが解析し、最適な文章を取得することができます。

Watson のすごいポイントは、英語だけでなく、日本語にも対応し、さらに漢字変換もされているところです。

中身に詳しいわけではありませんが、いくつか結果を見ての推測ですが、音声をある程度区切り区切りにし、それぞれに対する適切な文章を機械学習なもので割り出しているようです。その文章はすでに漢字変換されているものなので、変換されている文章が取得できるのでは?と思っています。さらに出来上がった文章の前後関係を見て、補正し、最終的により整合性の高そうな文章を提供している……のかなあ、なんて。

Watsonを利用するには、まず、Bluemix上でWatsonを接続する必要があります。

アプリケーションを開いたとき、サイドメニューに「接続」があります。これを開くと次のような画面が開き、接続したサービスの一覧が表示されます。

bluemix_app_connect

新規接続を選び、画面をよしなに進めていくと、Watson を選べる画面になります。ここでは Speech to Text を選択しました。
ほかにもたくさんサービスがあって面白そうですね;)

bluemix_app_connect2

そうすると、先ほどの接続したサービス一覧に出てくるようになり、認証情報を得ることができます。

bluemix_app_connect3

認証情報に書かれている情報をもって以下のようなAPIを実行することになります。


※Speech to Text の例

■HTTP リクエスト
○URL
https://stream.watsonplatform.net/speech-to-text/api/v1/recognize?continuous=true&model=ja-JP_BroadbandModel

○HTTP ヘッダ
Authorization: Basic {base64_encode("username:password")}
Content-Type: {audio content type}

※ HTTP ヘッダの例
Authorization: Basic dGVzdDp0ZXN0
Content-Type: audio/wav

○HTTP メソッド
POST


■HTTP レスポンスの例
{
  "results": [
    {
      "alternatives": [
        {
          "confidence": 0.106,
          "transcript": "うん "
        }
      ],
      "final": true
    },
    {
      "alternatives": [
        {
          "confidence": 0.32,
          "transcript": "看板 見る "
        }
      ],
      "final": true
    }
  ],
  "result_index": 0
}

API詳細な説明については公式ドキュメントを見るとよいでしょう。

Speech to Text - API | IBM Watson Developer Cloud

Web Audio API と MediaStream Processing API

録音の部分では、スマホで、という想定ではありますが、やはり世界はWebに集約されるよう動いている(?)ので、ここはWebの資産を利用しましょう。

Web Audio APIを利用すると、音声に関する取り扱いを行うことができます。
そこに合わせて MediaStream Processing API を利用することで、ブラウザのみで動画や音声の記録ができます。

組み合わせることで、録音したデータをバイナリとして取得することができます。

作業していて、ちょっとだけ詰まったのですが、Chrome ではいつからか https な URL でないと Web Audio API を利用することができませんでした。音も隠さないといけない情報なので、というところだと思います。録音して外に送るってときに、パスワードとか個人情報とか社内機密をしゃべっているようなデータを http で送るとかめっちゃ怖いですよね…笑

詳しい仕様や実装などは僕もあまり詳しくないので、詳細はこちらの記事を参考にしました。

Media Capture and Streams と Web Audio API で実現する録画・録音・ WAVファイルの生成 | CYOKODOG

IBM Bluemix DevOps Services

Bluemix に初めて登録して、アプリケーションを登録したところで、いろいろ眺めていたのですが、 heroku のような、PaaSの触感がありました。

コードをデプロイするにはCLIを入れてね!なんて書いてあったのですが、ちょっとローカルPCにあれこれ入れるのは避けたいなあと思い、ひとまずなるべく手をかけずに…。

と思っていたら、Git からデプロイできるそうなので、そちらを試してみました。リモートリポジトリが提供されて、ローカルのコードを Push したらデプロイされる、といったものを想像していたのですが、それだけの機能ではありませんでした。

アプリケーションの Git を有効化すると「コードの編集」というボタンが現れます。

まさか、と思い押してみると、そこにはかっこいいオンラインエディタが…!

bluemix_web_editor

この中では、コードの編集はもちろんのこと、Gitの操作も行うことができます。そして何より、編集して、そのままデプロイができます。これにより、とりあえずBluemixを試す、という動きをサクサク行っていくことができました。

bluemix_web_editor2

まとめ

結果として出来上がったものは、微妙な精度ではあるものの、IBM Bluemix を初めて触ってみて、便利そうなところを体感するには十分でした。

ばーーーっと書いてみた程度のものですが、gistに貼ってあります。試してみたい方はどうぞ!

https://gist.github.com/sters/46e042e5976b6244119baaf26b096e82

登録してから30日間は無料で使えるので、もうしばらく無料枠の間に遊んでみようと思います。Speech to Text 以外の Watson API も、画像認識を始め言語に関するものなど、楽しそうなものが多いので、あんなものやこんなものが作れるんじゃないだろうか、なんて思っています。

追記:ドキュメントを眺めていたら WebSocket をつなげるそうなので、それを使えるとさらに良いかも…!

明日以降もお楽しみに! -> Bluemixアドベントカレンダー

ぼくのぼくによるぼくのためのマークダウンエディタを作ってるところだからちょっと待って

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

こんにちは、ごみばこです。

世の中にはさまざまなマークダウンエディタがあります。

そして、ふつうのエディタにも、プラグインとしてマークダウン機能を搭載することができるものも多々あります。

しかし、どれも自分のほしい機能が整っていないので、自分で作る事にしました。
地産地消!!(違)

ほしい機能としては、こんなところでしょうか……。

  • 画面が2分割されてエディタとプレビューが並べて見れる
    • いま編集している箇所までいい感じにスクロールしてくれる
  • azuさんのtextlint(http://textlint.github.io/)による文字列のlintをしたい
    • ルールを状況によって切り替えたり変更したりしたい
    • 怒られているポイントをハイライトしてほしい
  • "プロジェクト"で縛られず、自由にファイルを開け閉めしたい
    • ※atomはこれでNG
  • タブにしたい
  • 整形されたHTMLをコピーしたい
  • emojiも使いたい
  • 軽快に動いてくれる

イメージはkobitoがすごい近いです。あれにtextlintが乗った感じです。
http://kobito.qiita.com/win

というところで、Electronでさくさく作ってます。で、エディタっぽい動きまではできました☆
デザインも細かい機能もさっぱりや……、というわけで、これからブラッシュアップですね!><

ここまでできている

ちょっと時間がとれてなさ過ぎてアレなんですけど、もうちょっと出来上がってきたら、いろいろ出していきますかねー。

それではまたー

Electronで社内ツールを作った話をしました / #JSオジサン #6 2日目

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

JSオジサン#6 2日目 で喋っていました。


こんにちは。ごみばこです。

最近はLaravelオジサン…ですかね。たーまーにJSオジサンしてます。
…いやいや、年齢的にはまだまだオジサンじゃなくて、お兄さんですよ。

JSオジサン#6 2日目

満員御礼!! JSオジサン #6 「まさかの3日間連続開催だよ!」2日目
https://atnd.org/events/71990

今回のJSオジサンも21cafeさんでの開催です。
いつもありがとうございます!そこそこキャパが広くて、スライドも見やすくて、立地もよし。快適環境です!

さて、今回のJSオジサンでは、言ってしまえば勉強会駆動開発のような「せっかくだから」精神で登壇者側に参戦しました。

せっかくチャンスがあるから。
せっかくElectronやってみたかったから。

せっかくだから作ってみた系の話をしました。

Electronで社内ツールを作ったお話 from sters9

すこし解説をいれます。

ハイテンションキャラ

LT5分だし、Electron超感動したし、実際ほんとすごいから、
ハイテンションキャラで「サイコー感」を出していこう。

という思いのもとで、こういうスライドに仕上げました。
普段からこんなじゃないですよ!!!!!

偶然にも、ハイテンションノリが他の人と被っておらず、「Electron」という流行ネタも被らず、
いい感じに印象づけはできたのかなと思っていたりします。

社内ツール1

「なんとかかんとかーベイ」ってサービスの管理画面が、Flash製でもっさりでつらい。
というところから来ています。

当初はブックマークレット超がんばっていたのですが、人によっては動く動かないとかで、
こまめな対応や更新が面倒になってしまったので、いっそElectronでつくったろか、と。

いうのは実は後付けで、作ったのは一年前?とか、かなり前になります。
当時はnode-webkitを使って作ったのですが、「まあ、だいたいイメージ一緒だし…」というノリで入れました。

このときnode-webkitを触って、なんだかいい感じ!なんだけどなんだかなあ…
という気持ちがあったので、ここにきてElectronいい感じ!!という話を聞いてずっとやりたいと思っていたのでベストタイミングですね。

社内ツール2

Confuluenceのクライアントアプリです。

Qiitaで見かける、チャットワーククライアントアプリと同じように、
「webviewタグ」を使って、いわゆるガワネイティブと呼ばれる仕上がりにしています。

APIなんかも提供されているので、Confuluence内の色んなデータをばっさばっさ取れて、フルに画面を構築することも可能でしょう。ただGET系のリクエストは、JIRAやBitbucketなど、別のAtlassian製品を使ってると、そっちの情報が降ってきたり、 混在していたりすることもある(主にActivityStream)ので、扱いには気を付けないといけないこともあるかと思います。
オンプレの場合はちょっとわからないです…。ヘーシャはクラウドの方を利用しているので…。

そして、もしかしたら「最強のConfuluenceクライアント戦争」の到来…!?
参考:最強のTwitterクライアント戦争情報( http://r7kamura.hatenablog.com/entry/2015/08/25/154846


さてさてガワネイティブをしたとき、イケてない中身とかほしい機能を付けるためにもあれこれいじくりたくなりますよね?

ただ、Electronでは、Electronアプリを動かすプロセス、ウィンドウのプロセス、webviewのプロセス、といった具合に、それぞれの画面(?)は簡単にはつながっていないのです。
そんなとき、IPCと呼ばれるプロセス間通信の機能を使うことで…

うーん、長くなりそう…。このあとはまたどこかで書きます。


ツール2のConfuluenceクライアントアプリについては、、いかんせん急ごしらえな部分がおおく、正直見せられるようなものではない残念施構成なのでうーん。

そのうち、またElectronの話を書きます。
感動したので熱はしばらく冷めないことでしょう。

それではまたー(*'ω'*)


ア!1日目も参加者だったので、また参加レポあげます!!!

1 2 3