Last Updated: 2/6/2024, 5:44:57 AM

# CSS と Vue で
マーカーを引いてみる

Hello, world!

# はじめに

# (1) 使い方

スクロールすると下線が引かれます。

吾輩わがはいは猫である。  名前はまだ無い。 
吾輩は猫である - 夏目漱石
 片手に梅の枝をかざした儘、片手に紫匂むらさきにほひの袿うちぎの袖を軽さうにはらりと開きますと、  やさしくその猿を抱き上げて、若殿様の御前に小腰をかゞめながら 「恐れながら畜生でございます。どうか御勘弁遊ばしまし。」と、涼しい声で申し上げました。
地獄変 - 芥川龍之介
 私はそのさまを見ると,大層不憫に思いました. 「銀の滴降る降るまわりに, 金の滴降る降るまわりに.」という歌を 歌いながらゆっくりと大空に 私は輪をえがいていました.貧乏な子は 片足を遠く立て片足を近くたてて, 下唇をグッと噛みしめて,ねらっていて ひょうと射放しました.小さい矢は美しく飛んで 私の方へ来ました,それで私は手を 差しのべてその小さい矢を取りました. クルクルまわりながら私は 風をきって舞い下りました. すると,彼かの子供たちは走って 砂吹雪をたてながら競争しました.  
アイヌ神謡集 - 知里幸惠編訳

最初、props でテキストを渡していたのですが、長文になるとかなり苦しくなり、途中で v-slot に書き直しました。

# (2) 使い方

id に app が指定された DOM の中で、hl タグを書きます。 ちなみに2文字目は、数字のイチ 1 ではなくて小文字英字のエル l です。

<div id="app">
  吾輩わがはいは猫である。<hl color="#F6CECE">名前はまだ無い。</hl>
</div>

# (3) CodePen

全体のコードは、こちら。

中括弧 {}  辞書ではなくオブジェクト  です。 Python で言えば、クラスオブジェクトがないインスタンスオブジェクトです。

var person = {
  name: ['Bob', 'Smith'],
  age: 32,
  gender: 'male',
  interests: ['music', 'skiing'],
  bio: function() {
    alert(this.name[0] + ' ' + this.name[1] + ' is ' + this.age + ' years old. He likes ' + this.interests[0] + ' and ' + this.interests[1] + '.');
  },
  greeting: function() {
    alert('Hi! I\'m ' + this.name[0] + '.');
  }
};

こういうのをオブジェクトリテラル表記法と言うらしいです。 最初、知らなくてあたふたしました。

オブジェクトは new Object()、Object.create()、  リテラル表記法 (initializer 表記法)  を使用して初期化されます。 オブジェクト初期化子はオブジェクトのプロパティ名と関連した値のゼロ以上のペアのリストです。 中括弧 ({}) で囲まれます。
オブジェクト初期化子 - MDN web docs (opens new window)

Python ではドット. 以下のことを属性と呼んでいましたが、 JavaScript では  プロパティ  と呼びます。

オブジェクトとは関連のあるデータと機能の集合です。 (機能はたいていは変数と関数で構成されており、 オブジェクトの中ではそれぞれ  プロパティ  とメソッドと呼ばれます。)
JavaScript オブジェクトの基本 - MDN web docs (opens new window)

# 1. Vue.js の読み込み

Vue オブジェクトを使えるように読み込みます。

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>

CodePen では分かり辛いですが Settings -> Add External Scripts/Pens で こっそりURL を追記しています。

# 2. Vue のインスタンス化

Vue インスタンスを生成しています。 el プロパティで指定された DOM を解析します。 次に hl タグを展開しています。

new Vue({
    el: '#app',
    components: {
        hl: highlighter(),
    }
});

components に新しいタグを定義します。 id に app が指定された DOM 配下では hl タグを使えるようになる。 と考えておけばいいかなと...

自己定義したタグを解析して書き換えてくれるんだから、すごい...

# 3. Vue コンポーネント

関数 highlighter が返しているオブジェクトリテラルは  Vue コンポーネント  と呼ばれるものです。

function highlighter() {
    return {
        template:
        props:
        data:
        mounted:
        methods:
    }

function 定義文で関数を定義するときは、 呼び出されるところよりも後ろで定義しても大丈夫そう。

無理やり関数定義で後ろに持って来てしまったのですが、 Vue オブジェクトをインスタンス化する箇所、 全体像がわかる箇所を先に持って来たかったので、 このようにしました。

# 4. 設定

マジックナンバー (opens new window) は、ここに寄せておきました。

// 2-1) configuration
const startLineOffset = -100;
class Style {
    constructor(highlighter) {
        const color = highlighter.color
        this['background'] = `linear-gradient(${color}, ${color})`;
        this['background-size']      = '0% 45%';
        this['background-position']  = '0% 100%';
        this['background-repeat']    = 'no-repeat';
        this['transition-delay']     = '1s';
        this['transition-duration']  = '2.5s';
    }
}

# 4.1. startLineOffset

hl タグの DOM が、画面上のある線よりも上に来たら線を引くようにします。 大体のイメージは以下の通り。灰色の枠はページの全体。 青色の枠は描画画面 Viewport の全体。 イメージは以下のような具合です。

下から計測するようにしているのは、PC でもスマホでも対応できるようにするため。 上からだと PC と スマホの差がありすぎるので。

正確にやろうとしたら別個に対応しないといけないと思うのですが、 そこまでの余力がないためです。

変数の説明をいたします。 線を引く仕組みの全体像を、ざっくりとだけつかんでいただければと思います。

# 4.1.1. startLine

「表示画面 Viewport の底 window.innerHeight」から 「startLineOffset」 だけ離れた線 startLineを引きます。 startLine は次のように計算しています。

startLine: function() {
    return window.innerHeight + startLineOffset;
},

水平スクロールバー(表示されている場合)を含む、 ブラウザウィンドウの ビューポー」ト (viewport) の高さを返します。
window.innerHeight (opens new window)

# 4.1.1.1. Viewport, ビューポート

こういうわかりやすいの、ありがたいです...

viewport(ビューポート)とは、 日本語に訳すと「表示領域」という言葉がしっくりくると思います。
第1回 viewportとはなにか? - CodeGrid (opens new window)

公式ドキュメントは難しい。

ビューポートは、現在表示されているコンピューター画像の中の、 多角形 (通常は長方形) 領域を表します。 ウェブブラウザーの場合、閲覧中のドキュメントのうち、 ブラウザのウインドウから (フルスクリーンモードなら、画面上で) 見えている領域をさします。 ビューポートの外にあるコンテンツはスクロールされてビューの中に 移動されるまで画面上では見えません。
Viewport - MDN web docs (opens new window)

# 4.1.2. domRectTop

domRectTop は 「hl タグの DOM の頭」から「描画領域 Viewport の頭」までの距離です。 this.$el には hl タグの DOM が格納されます。

domRectTop: function() {
    return this.$el.getBoundingClientRect().top;
},
# 4.1.2.2. Element.getBoundingClientRect

Element.getBoundingClientRect は DOMRect (opens new window) オブジェクトを返します。

Element.getBoundingClientRect() (opens new window)
Element.getBoundingClientRect() メソッドは、 要素の寸法と、そのビューポートに対する位置を返します。

# 4.1.2.3. DOMRect.top

top はビューポートを基準にして値を返します。

width と height 以外のプロパティは、 ビューポートの左上を基準としています。
Element.getBoundingClientRect() (opens new window)

# 4.1.3. 描画

startLine が小さいとき、表示画面が hl タグの DOM よりも上にあるときは、線を引かない。 startLine が大きいとき、表示画面が hl タグの DOM よりも下に来たら、線を引く。

scroll: function() {
    if (this.startLine() < this.domRectTop()){
        this.style['background-size'] = '  0% 45%';
    } else {
        this.style['background-size'] = '100% 45%';
    }
    // console.log("%d, %d", this.startLine(), this.domRectTop);
},

# 4.2. Style

線を引く DOM に適用するスタイルです。 Vue では JavsScript のオブジェクトをそのまま CSS として割り当てることができます。

# 4.2.1. background

background を他の background-size, background-position, background-repeat などよりも あとに定義するとうまく動作しない。この辺で少しハマりました。

# 4.2.2. linear-gradient

上から下に透明 transparent から赤 red に変わります。

background: linear-gradient(transparent, red)

linear-gradient() - MSN web docs (opens new window)
以下の二つのグラデーションは等価です。

linear-gradient(red, orange, yellow, green, blue); 
linear-gradient(red 0%, orange 25%, yellow 50%, green 75%, blue 100%);

あとはサルワカ先生も参考にしました。

# 4.2.3. background-size

今回の主役。この子を '0% 45%' から '100% 45%' に変化させることで、 線を引く動作を加えます。

/* 値2つの構文 */
/* 1番目の値は画像の幅、2番目の値は高さ */
background-size: 50% auto;
background-size: 3em 25%;
background-size: auto 6px;
background-size: auto auto;

# 4.2.4. background-position

どこを基準にしているのかがポイント。 「linear-gradient の中心」が「親要素の縦横の何パーセントの位置にいるか」を表記している。

background-postion: 0% 100%

# 5. template

hl タグは template で指定されたように展開されます。 <slot></slot> には hl タグで囲まれた要素が置き換えられます。 今回は下線を引くテキストです。

template: 
    '<span v-bind:style="highlighter">' +
        '&nbsp;<slot></slot>&nbsp;' +
    '</span>',

v-bind:style="style" と書くと、 変数 style に定義したオブジェクトが CSS として適用されます。

v-bind:属性="変数"

# 6. props

タグの属性で color と指定されたものが受け取れるようになります。

props: ['color'],

# 7. data

component の中で使う変数を定義する。

data に直接オブジェクトではなく関数が指定するのは、 component が複数回呼び出されると、 その都度新しいオブジェクトを生成しないといけないから、らしいです。 その流れで Style も new しています。

data: function() {
    return {
        style: new style(color),
    };
},

# 8. mounted

mounted では DOM が描画し終わった後くらいに、呼び出される処理を定義します。 あまり僕も全くわかっていません。 とりあえず、ここではイベントを追加します。

mounted: function() {
    window.addEventListener('scroll', this.handleScroll);
},

# 9. methods

methods 配下では、通常のメソッドを定義します。

# おわりに

JavaScript を触ったことがないので、色々なところで壁にぶつかりました。 その分だけ、だいぶ MDN web docs の見方も慣れました。 ここまでお読みいただき、ありがとうございました。