銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

SVGの重なり順序をJavaScriptで制御する「svg-z-order」

f:id:takamints:20170423172653p:plain

JavaScriptからSVG要素の重なり順(Z-Order)を操作するモジュールのご紹介。

SVG要素には、HTMLに使えるz-indexスタイルが効きません。 なので、重なり順を変更するには、単純に要素を並べ替える必要があります。

コードからDOM要素を並べ替えるにはNodeクラスinsertBeforeメソッドが使えますNodeクラスはSVG要素SVGElementの基本クラスですから、SVG要素でも使えます。

SVG関連最新記事:
takamints.hatenablog.jp takamints.hatenablog.jp

ところで、SVG要素にはz-indexが効かないことを「SVGの致命的な欠陥」と言ってる人がいますが、さすがにそれは言い過ぎですよ。SVGは単体で存在できる画像ファイルであってHTMLとの直接的なつながりはありません。単体のSVGファイル中で図形の重なり順序をz-indexで定義されていると、画像としてどのように表示されるか理解しにくいはずです。要素の順序で重なり順序が決定される方が、SVG的に「よりシンプル」なんですね。

さらに「HTMLのDIVにSVGを入れてz-indexで…」などというヤヤコシイ方法が検索で出てくるのですが、これは既にSVGではなくなっていますから、一般的な解決法とはいえないように思います。全体のスケールを変えるとかViewportを移動したいとかいったシーンで困りそう。

というか、DOM要素の並べ替え自体は前述のようにinsertBeforeで簡単なんです。 DOMのベタな扱いに慣れていないと難しそうに思うのかもしれないですね。

ただし、ベタにやると「どれの前に持ってくる?」といったコードが煩雑になるのは確かです。 てことで怒りに任せて(?)npmを書きなぐりました。以降、このモジュールのサンプルとかAPIを説明します。

www.npmjs.com

とりあえずは動くサンプル

以下は、このモジュールを使ったサンプルです。コードは下の方に掲載してます。 3つの円をクリックすると、その要素にドロップダウンで選ばれているメソッドを適用します。

使い方

このモジュールを使って、特定要素を最前面へ持ってくるコードを少し冗長に書いてみました。

//モジュール取得
var svgz = require("svg-z-order");// (1)

//SVG要素 g#foo を最前面へ表示
var g = svg.getElementById("foo");
var svgzG = svgz.element(g); //(2)
svgzG.toTop(); //(3)
 
// D3.jsで使う(4)
var d3 = require("d3");
var d3g = d3.select("#foo");
svgz.element(d3g.node()).toTop();
  1. モジュールをインポートして、
  2. element メソッドで、DOM要素を参照するインスタンスを作り
  3. 最前面へ表示します。
  4. D3.jsを使っていますが、例として書いているだけで、依存関係はありません。

上の例では、Browserifyを使って、 var svgz = require("svg-z-orger");でモジュールを取り込んでいますが、 HTMLで<script src="svg-z-order.js"/>としている場合は、 (2)の行の svgz.element(g) を、 svgz_element(g) に変更して下さい。 requireできない環境ではグローバルスコープに定義します。

API

element(e:Element)

SVG要素を参照する SVGZElement クラスのインスタンスを返します。 Z-Orderを操作するAPIは SVGZElementクラスのメソッドです。

パラメータ

  • e:Element - 参照するDOM要素を指定します。

SVGZElementクラス

SVGZElement.toTop()

参照している要素を最前面に移動。

SVGZElement.toBottom()

参照している要素を最背面に移動。

SVGZElement.moveUp(e:Element / n:number)

参照している要素を上(前面)へ移動。

パラメータ

  • e:Element - この要素よりも上になるように移動します。
  • n:number - 要素の並びの中でこの回数分だけ上へ移動します。

要素を指定したときはその要素よりも上へ。数値指定した場合は、その回数だけ上へ移動。

SVGZElement.moveDown(e:Element / n:number)

参照している要素を下(背面)へ移動。

パラメータ

  • e:Element - この要素よりも下になるように移動します。
  • n:number - 要素の並びの中でこの回数分だけ下へ移動します。

要素を指定したときはその要素よりも下へ。数値指定の場合は、その回数だけ下へ移動。

サンプルコード

一番上にあるサンプルのコードです。

/sample/web/index.js

リポジトリ

www.npmjs.com github.com

いくつになってもお勉強

JavaScriptのプログラムからSVG要素を最前面へもってくる方法を検索したら、結構微妙な情報が出てきたので少しインターネッツに失望しました。

曰く、

  1. SVG単体では不可能なので、HTMLのDIVにSVGを書いて重ね合わせて、DIVのz-indexで制御するしかありません。
  2. 対象のSVG要素をディープコピーして appendChild 。元要素は removeChild。

こういう情報を見て、当初「エラいヤヤコシイな」と思いましたが、どうにも納得がいかなくて、いろいろやってみているとDOMの標準機能で可能だと判明。 当初「複製作って、追加して、元のを消す」という処理手順をイメージしていましたが、移動できますね。 Node.appendChildNode.insertBeforeは、既にDOMツリーにある要素は移動するのですが、メソッド名を見る限りではそう思えないところが落とし穴かも。

いくつになってもお勉強です。

Node.js / npm 関連記事

takamints.hatenablog.jp takamints.hatenablog.jp takamints.hatenablog.jp takamints.hatenablog.jp takamints.hatenablog.jp

D3.js v4 でドラッグするには d3.drag() で behavior を取得する

f:id:takamints:20170418212231p:plain

D3.jsでドラッグイベントを処理する必要があったのですよ。

ほぼ初めてのD3ですからグーグル先生にいろいろ聞いて、「ほうほうなるほど」と学習していたのですけど、 ドラッグに関して各所で示されていたサンプル通りにやってみたら、まさかのエラー。

結局は、大きな問題ではありませんで、ひと言で言ってしまえばバージョン違いだったのですが、どうにも日本語の情報が少ないようなので書いておきます。

英語版です。v4に関して日本語の書籍が見当たらないです。

D3.js/v3でのコード

「D3.js ドラッグ」などと検索して出てくるサンプルコードの多くが、D3.jsのv3(バージョン3)のために書かれたコードで、最新のv4では動かないんですね。

v3でドラッグするには、d3.behavior.drag()で、Drag Behavior を取得するのですが、v4ではこれが動かない。

// D3.js v3でDrag Behaviorを取得

var drag = d3.behavior.drag(); //V4ではエラー

D3.js/v4でのOKコード

じゃあどうやるんだって―と、v4以降では以下のように、d3.drag()とするのだそうだ。

// D3.js/V4 でDrag Behaviorを取得

var drag = d3.drag(); // これでOK

リンク

リポジトリCHANGES にはしっかり書いてありました。

CHANGES
github.com

Dragに関する詳細は以下に
github.com

いやしかし

メジャーバージョンが上がっているので、APIに互換性がなくなってても文句は言わない約束だけど、 レガシーコードには警告を出すとかって対応があればモアベターね。

JavaScriptのラムダ式(アロー関数)は丸括弧で括らなければ即時実行できませんのね

f:id:takamints:20170328172435p:plain
Link: Flickr PAGE - CC BY 2.0

Node.jsで以下のようにラムダ式を即時実行していたのですが、ブラウザでは構文エラーとなって動かないんです。

(()=>{
    console.log("これ動きません");
}());

まさかコレが動かないとか思いもよらず。 どう見直しても問題があるとは思えなかったのだが動かないから仕方がない。

追記(2017-03-29)JavaScriptの言葉的には「ラムダ式」ではなく「アロー関数」のようですので、タイトルにのみ追記しました。

色々やってみた結果、ブラウザで動作させるには、以下のようにラムダ式全体を丸括弧で括る必要があるようですね。 しかし、なんだかカッコが多すぎ。ラムダ的に台無し感がありますなー。

((()=>{
    console.log("これでヨシ。だがしかし・・・");
})());

丸括弧なしでは関数オブジェクトとして評価されていないようです。 アロー演算子=>)の優先度の問題かな?

そもそも、どちらが正しいのかわからなかったので、調べてみると、どうやらECMAScript6の仕様のバグだそうで、 Node.jsでは便利な様に解釈して実装されているのかも知れませんが、今の所ブラウザではどうにもならないようですね。

d.hatena.ne.jp

即時実行するときはラムダ式を使わないというのが安全・安心? 関数内のthisが定義時にバインドされちゃうようなので、単なる無名関数の構文糖として使うのはマズいらしいし・・・

JavaScriptでマイクロ秒単位の定期処理を実行する(npm fractional-timer)

JavaScriptで1ミリ秒より短いインターバルタイマー処理を提供するモジュール fractional-timer のご紹介。

f:id:takamints:20170327172258p:plain
Link: Flickr PAGE - CC BY-SA 2.0

実際のところ、精度はよくありませんので、クリティカルな用途には向きません。 単純な処理をなるべく高速にタイマーで実行したいけど、標準タイマーの1ミリ秒では遅すぎる・・・といった時に使える感じ。 処理が重い場合は、呼び出し間隔は長くなります。

Chrome と Node.Js で、正しく動いていることを確認していますが、 FirefoxやEdgeでは、まともに動いていなかったので、ご注意下さい。

使い方

使い方は遅延時間に実数を指定できる点を除けば、標準のsetIntervalclearIntervalと同じです。 遅延時間は標準と同じくミリ秒単位で指定します(0.1=100マイクロ秒)。

以下のように呼び出すことで、タイマーの数と遅延時間等を調整して、定期処理を実行します。

(function() {
    "use strict";
    var ft = require("fractional-timer");
    var ftid = ft.setInterval(
        function(){
            ft.clearInterval(ftid);
        },
        0.001);// 1 microsecond
}());

インストー

npmでインストールすればNode.jsから使えます。

> npm install fraction-timer

ブラウザのWEBアプリでは、browserifyrequire するか、 scriptタグで読み込んで、FractionalTimerクラスの静的メソッドsetInterval/clearIntervalを直接使ってくださいね。

リポジトリ

www.npmjs.com

github.com

その他、能書き

やっていることは単純です。複数のインターバルタイマーで一つの処理を呼び出しているだけです。

例えば、1マイクロ秒で実行したい場合、1ミリ秒のタイマーを1000個使って呼び出すということです。

実際には、指定された間隔から算出される単位時間あたりの実行回数を計算し、タイマーの数と間隔を自動的に算出、ベストエフォートで呼び出します。

MZ-700フルJavaScriptエミュレータ」で、エミュレーションをワーカースレッドの定期処理で実装していて、当初は単一のインターバルタイマーで、最も高速になるように調整していたのですが、ある時タイマーが二重に動いてしまったことがあって、速度がなんと2倍程度に跳ね上がっていたのです。

「うわあ!世紀の大発見!!!」と思ったけれど、単一スレッド内ではシーケンシャル動作が保証されているので、当然の挙動なんですよね。

ただ、こういうことをやってる人があまりいないところを見ると、需要はないみたいなんですけどね(笑)

.NETのアプリケーション設定を編集可能なXMLに保存する

f:id:takamints:20170221171853p:plain

.NETのプロジェクトでは「アプリケーション構成ファイル」というファイルに設定情報を書いておけます。 でもあれは実行時に書き換えるような利用方法は想定されていません。 インストーラーでセットアップすると、アプリケーションフォルダに置かれるため、読み取り専用で、編集するにも管理者権限が必要です(また、書き換えたつもりでも、何かのタイミングで元に戻るということも・・・どういう機構なのか知らないのですが・・・)

そこで、ここでは、編集可能なXMLファイルをアプリケーションの動的な設定情報として利用するためのサンプルコードをご紹介。 基本クラスは、お持ち帰り可能です。ご自由にご利用ください。

■■■ 目次 ■■■

  1. はじめに - 機能仕様
  2. XML設定クラス定義例:SampleSettingsクラス
  3. 設定ファイルの読み込み処理
  4. XMLファイルの例:SampleSettings.xml
  5. XML設定ファイルの基本クラス:XmlSettingFileクラス
  6. 設定ファイルはWindowsの特殊フォルダ以下へ
  7. 参考サイト
基礎からわかる C#
基礎からわかる C#
posted with amazlet at 17.02.21
シーアンドアール研究所 (2017-02-16)
売り上げランキング: 16,889

1. はじめに - 機能仕様

ここでやりたいことをザックリ以下に箇条書き。

  1. アプリ起動時に特定のフォルダに保存されたXMLの設定ファイルを読み込む。
  2. ファイルがない場合は、アプリ起動時に既定の内容で設定ファイルを作成する。
  3. 設定情報はクラスのインスタンスとしてプログラムからアクセスできる。
  4. 最低限プリミティブ型が正しく読み書きできること。

設定読み込み時の値のチェックはしていません。 バリデーション機能を持たせたメタクラスを導入すれば可能ですが、大げさになり過ぎるので、またおいおい。

2. XML設定クラス定義例:SampleSettingsクラス

以下がXML設定情報を保持するクラス。

設定ファイルに保存したい値をメンバとして定義したシンプルなクラスです。 XMLシリアライズを利用しているのでパブリックなデフォルトコンストラクタが必要です。 基本クラスの XmlSettingFileクラス は、下で説明します。

/// <summary>
/// XMLアプリケーション設定サンプル
/// </summary>
public class SampleSettings : Utils.XmlSettingFile
{
    /// <summary>
    /// 既定の設定情報を生成。
    /// (デフォルトコンストラクタは必ず必要)
    /// </summary>
    public SampleSettings() { }

    //以下のメンバーはXMLに保存される

    public string Name { get; set; } = "Koji Takami";
    public string Email { get; set; } = "vzg03566@gmail.com";
    public int FeatureOption { get; set; } = 0;
    public bool DebugOption { get; set; } = false;

    //保存したくないメンバーは以下のように宣言
    [System.Xml.Serialization.XmlIgnore]
    public string NotSaved;
}

3. 設定ファイルの読み込み処理

上のクラスのインスタンスXMLファイルから読み込むには、XmlSettingFile.Loadを使用します。 このメソッドには、保存場所のルートを決めるスペシャルフォルダの列挙子と、既定値を保持する設定情報のインスタンスを指定します。

以下のコードでは、設定ファイルを読み込んで、SampleSettingクラスのインスタンスを返します。

public partial class App : Application
{
    static public SampleSettings sampleSettings = new SampleSettings();
    public App()
    {
        try {
            sampleSettings = Utils.XmlSettingFile.Load(
                Environment.SpecialFolder.CommonApplicationData,
                new SampleSettings()) as SampleSettings;
        } catch (Exception ex) {
            Console.WriteLine("Error:{0}", ex.Message);
        } finally {
            sampleSettings.Save();
        }
    }
}
  • 上のコードで読み書きされる設定ファイルは、「C:\ProgramData\(CompanyName)\(ProductName)\(ProductVersion)\SampleSetting.xml」です。ここで、(CompanyName)(ProductName)(ProductVersion)アセンブリのファイルバージョン情報の設定値です。プロジェクトの初期状態ではCompanyNameが空白ですが、その部分抜きで正しいパスに生成されます。
  • 読み込み時にファイルがなければ、既定値のインスタンスを使用して作成されます。
  • ここでは、アプリケーションクラスのコンストラクタで読み出していますが、別のタイミングでも問題はありません。
  • 読み込んだ直後に保存しているのは、開発中に追加項目を反映したりObsoleteな設定項目をXMLから削除するためです。これがなくてもファイルがなければ作成されます。このSaveは、必ずしも必要ではありません。

4. XMLファイルの例:SampleSettings.xml

最初の実行時に生成されたファイルの中身は以下のようになります。 書き換えて読み込んだら、ちゃんと設定値が反映されます。それが目的ですからね。

<?xml version="1.0" encoding="utf-8"?>
<SampleSettings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Name>Takami Koji</Name>
  <Email>vzg03566@gmail.com</Email>
  <FeatureOption>0</FeatureOption>
  <DebugOption>false</DebugOption>
</SampleSettings>

5. XML設定ファイルの基本クラス:XmlSettingFileクラス

以下が、このエントリーの御本尊。 派生クラスのメンバーをXMLファイルへシリアライズ/デシリアライズするクラス。 (名前空間は適当に変えてください)

ファイル名の決定や、アクセス権の設定などで長くなっていますが、やってることは意外にシンプル。XmlSerializerのお陰様。

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Xml.Serialization;

namespace StudyDotNet.Utils
{
    public class XmlSettingFile
    {
        /// <summary>
        /// 設定ファイルを読み込む
        /// </summary>
        /// <param name="folderId">
        ///     スペシャルフォルダを指定する</param>
        /// <param name="initial">
        ///     ファイルがない場合の既定値を持つ
        ///     インスタンス</param>
        /// <param name="filename">
        ///     ファイル名。
        ///     省略時はクラス名を採用する</param>
        /// <returns></returns>
        public static XmlSettingFile Load(
            Environment.SpecialFolder folderId,
            XmlSettingFile initial,
            string filename = null)
        {
            //ファイル名を生成
            string pathname = CreatePathname(
                folderId, initial, filename);

            //設定ファイルがなければ既定の値で生成する
            if (!File.Exists(pathname)) {
                Save(pathname, initial);
                AllowUsersReadAndWrite(pathname);
            }

            //設定ファイルを読み込む
            return Read(pathname, initial.GetType());
        }

        /// <summary>
        /// 設定ファイル名。
        /// 実際に読み込んだもの。
        /// 書き戻す場合にも利用する。
        /// </summary>
        [XmlIgnore] public string Filename { get; private set; }

        /// <summary>
        /// 設定ファイルへ書き戻す。
        /// プログラムから設定値を変更した場合などに使用。
        /// </summary>
        public void Save() { Save(Filename, this); }

        #region StaticPrivate

        /// <summary>
        /// 与えられた情報からファイル名を生成する
        /// </summary>
        /// <param name="folderId"></param>
        /// <param name="initial"></param>
        /// <param name="filename"></param>
        /// <returns></returns>
        static private string CreatePathname(
            Environment.SpecialFolder folderId,
            XmlSettingFile initial,
            string filename)
        {
            //ファイル名が指定されていないならクラス名
            if(filename == null) {
                filename = initial.GetType().Name;
            }

            //拡張子がXMLでない場合はXMLに変更する
            if(Path.GetExtension(filename) == "") {
                filename = Path.ChangeExtension(filename, "xml");
            }

            //ファイル名を決定
            string pathname = Path.Combine(
                CreateFolder(folderId), filename);

            return pathname;
        }

        /// <summary>
        /// スペシャルフォルダ以下のパスを生成
        /// </summary>
        /// <param name="folderId"></param>
        /// <returns></returns>
        static private string CreateFolder(
            Environment.SpecialFolder folderId)
        {
            //アセンブリのファイルバージョン情報を利用してフォルダを決定
            var verinfo = FileVersionInfo.GetVersionInfo(
                Assembly.GetExecutingAssembly().Location);

            // 空白の項目はパスに含まれない。
            // 例えば CompanyName が空の場合、
            // @"C:\ProgramData\ProductName\1.0.0.0" などとなる
            string path = Path.Combine(new string[] {
                Environment.GetFolderPath(folderId),
                verinfo.CompanyName,
                verinfo.ProductName,
                verinfo.ProductVersion,
            });

            //フォルダがないなら生成する
            if(!Directory.Exists(path)) {
                Directory.CreateDirectory(path);
            }

            return path;
        }

        /// <summary>
        /// 設定を設定ファイルに保存
        /// </summary>
        /// <param name="filename"></param>
        /// <param name="setting"></param>
        static private void Save(
            string filename, XmlSettingFile setting)
        {
            var writer = new StreamWriter(
                filename, false, new UTF8Encoding(false));
            var xml = new XmlSerializer(setting.GetType());
            xml.Serialize(writer, setting);
            writer.Close();
        }

        /// <summary>
        /// 設定ファイルを読み込む
        /// </summary>
        /// <param name="filename">設定ファイル</param>
        /// <param name="type">設定データの型</param>
        /// <returns>type型のインスタンス。
        /// ファイルない場合はnullを返す。</returns>
        static private XmlSettingFile Read(
            string filename, Type type)
        {
            var reader = new StreamReader(
                filename, new UTF8Encoding(false));
            var xml = new XmlSerializer(type);
            var setting = xml.Deserialize(reader) as XmlSettingFile;
            reader.Close();
            if(setting != null)
            {
                setting.Filename = filename;
            }
            return setting;
        }

        /// <summary>
        /// 指定ファイルにUsersグループのユーザーの読み書き権限を与える
        /// </summary>
        /// <param name="filename"></param>
        static private void AllowUsersReadAndWrite(string filename) {
            FileSystemAccessRule rule = new FileSystemAccessRule(
                new NTAccount("Users"),
                FileSystemRights.Write | FileSystemRights.Read,
                AccessControlType.Allow);
            FileSecurity security = File.GetAccessControl(filename);
            security.AddAccessRule(rule);
            File.SetAccessControl(filename, security);
        }

        #endregion

    }
}

6. 設定ファイルはWindowsの特殊フォルダ以下へ

参考のため、アプリケーションの設定ファイルを保存するために適当なスペシャルフォルダを抜き出してみました。 下表はDOBON.NET プログラミング道:特殊ディレクトリのパスを取得するからの抜粋です。

Windows / .NET的に設定ファイルは、以下のようなフォルダ以下に、アプリケーションの企業名、製品名、バージョンなどのフォルダを掘って、保存するのが望ましいようです。

Environment.SpecialFolder列挙体のメンバ 具体例(参考)
1 CommonApplicationData C:\ProgramData
2 ApplicationData C:\Users\(UserName)\AppData\Roaming
3 LocalApplicationData C:\Users\(UserName)\AppData\Local
4 UserProfile C:\Users\(UserName)
6 MyDocuments C:\Users\(UserName)\Documents
5 CommonDocuments C:\Users\Public\Documents

全ユーザーに共通する設定なら1が良いと思います。ユーザー別なら2か3、4もあり。ただしローミングが必要なら2であるべきですね。

5と6については非推奨。Documentsに設定ファイルを置くのはちょっと違うかもしれないですからね。

7. 参考サイト