しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

レスポンシブな大理石風背景を表示する

f:id:matatabi_ux:20140626062005p:plain

こんな感じの背景を表示させる場合、一枚画像で表示すると解像度が変化したときに柄が崩れたり、グラデが汚くなったりするのであまりよくありません

これをレスポンシブに解像度に合わせてきれいに表示されるようにしたいと思います

柄の部分とグラデの部分を分けて対応したいと思います

まずは柄の対応から

f:id:matatabi_ux:20140626062518p:plain

敷き詰められる繰り返しパターンの 100%、140%、180% のスケーリング用テクスチャ画像を用意します ピクセルサイズは 5 の倍数(250 x 250 px など)に合わせておき、各倍率の画像サイズで端数がでないようにします

次に繰り返し表示ですが、Windows Runtime にはタイルブラシがありません・・・なので Canvas を継承した繰り返し表示用パネルを自作します

/// <summary>
/// 繰り返し画像を表示する Canvas
/// </summary>
public class TiledCanvas : Canvas
{
    /// <summary>
    /// 画像ソースの依存関係プロパティ
    /// </summary>
    public static readonly DependencyProperty ImageSourceProperty = DependencyProperty.Register(
        "ImageSource",
        typeof(ImageSource),
        typeof(TiledCanvas),
        new PropertyMetadata(null, OnImageSourceChanged));

    /// <summary>
    /// 画像ソース変更イベントハンドラ
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="args">イベント引数</param>
    private static void OnImageSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
    {
        var panel = (TiledCanvas)sender;
        if (panel.ImageSource == null)
        {
            return;
        }

        panel.image = new Image
        {
            Source = panel.ImageSource,
            UseLayoutRounding = false,
            Stretch = Stretch.None
        };
        panel.image.ImageOpened += panel.OnImageOpened;
        panel.image.ImageFailed += panel.OnImageFailed;

        // ImageOpend イベントを起こすため、ビジュアルツリーに画像を追加する
        panel.Children.Add(panel.image);
    }

    /// <summary>
    /// 画像ソース
    /// </summary>
    public ImageSource ImageSource
    {
        get { return (ImageSource)this.GetValue(ImageSourceProperty); }
        set { this.SetValue(ImageSourceProperty, value); }
    }

    /// <summary>
    /// 直近のサイズ
    /// </summary>
    private Size lastActualSize;

    /// <summary>
    /// 画像
    /// </summary>
    private Image image;

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public TiledCanvas()
    {
        this.LayoutUpdated += this.OnLayoutUpdated;
        this.Unloaded += this.OnUnloaded;
    }

    /// <summary>
    /// 破棄イベントハンドラ
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="e">イベント引数</param>
    private void OnUnloaded(object sender, RoutedEventArgs e)
    {
        this.LayoutUpdated -= this.OnLayoutUpdated;
        this.Unloaded -= this.OnUnloaded;
        if (this.image == null)
        {
            return;
        }
        this.image.ImageFailed -= this.OnImageFailed;
        this.image.ImageOpened -= this.OnImageOpened;
        this.image = null;
    }

    /// <summary>
    /// レイアウト更新イベントハンドラ
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="args">イベント引数</param>
    private void OnLayoutUpdated(object sender, object args)
    {
        var newSize = new Size(this.ActualWidth, this.ActualHeight);
        if (this.lastActualSize.Equals(newSize))
        {
            return;
        }

        this.lastActualSize = newSize;
        this.Render();
    }

    /// <summary>
    /// 画像読み込み失敗イベントハンドラ
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="args">イベント引数</param>
    private void OnImageFailed(object sender, ExceptionRoutedEventArgs args)
    {
        this.image.ImageOpened -= this.OnImageOpened;
        this.image.ImageFailed -= this.OnImageFailed;
        this.image = null;
        this.Children.Clear();
    }

    /// <summary>
    /// 画像読み込み完了イベントハンドラ
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="args">イベント引数</param>
    private void OnImageOpened(object sender, RoutedEventArgs args)
    {
        this.image.ImageOpened -= this.OnImageOpened;
        this.image.ImageFailed -= this.OnImageFailed;
        this.image = null;
        this.Render();
    }

    /// <summary>
    /// 繰り返し画像をレンダリングする
    /// </summary>
    private void Render()
    {
        var bmp = ImageSource as BitmapSource;
        if (bmp == null)
        {
            return;
        }

        var width = bmp.PixelWidth;
        var height = bmp.PixelHeight;

        if (width == 0 || height == 0)
        {
            return;
        }

        this.Children.Clear();

        // 画像を敷き詰める
        for (double x = 0; x < this.ActualWidth; x += width)
        {
            for (double y = 0; y < this.ActualHeight; y += height)
            {
                var image = new Image
                {
                    Source = this.ImageSource,
                    UseLayoutRounding = false,
                    Stretch = Stretch.None
                };
                Canvas.SetLeft(image, x);
                Canvas.SetTop(image, y);
                this.Children.Add(image);
            }
        }

        // はみ出した部分をクリッピングする
        this.Clip = new RectangleGeometry 
        {
            Rect = new Rect(0, 0, this.ActualWidth, this.ActualHeight)
        };
    }
}

サイズ変更に合わせて画像を並べているだけです

XAML の方はこんな感じ

<Page x:Class="TextureGrad.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:ctrl="using:TextureGrad.Controls"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:local="using:TextureGrad"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        
        <!-- 柄の部分 -->
        <ctrl:TiledCanvas ImageSource="ms-appx:///Assets/texture.png" />

    </Grid>
</Page>

そうするとこんな感じにテクスチャ画像が敷き詰められます

f:id:matatabi_ux:20140626063825p:plain

お次はグラデの対応

<Page x:Class="TextureGrad.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:ctrl="using:TextureGrad.Controls"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:local="using:TextureGrad"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        
        <!-- 柄の部分 -->
        <ctrl:TiledCanvas ImageSource="ms-appx:///Assets/texture.png" />

        <!-- 半透過グラデの部分 -->
        <Rectangle>
            <Rectangle.Fill>
                <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                    <GradientStop Offset="0.85" Color="#BF000000" />
                    <GradientStop />
                </LinearGradientBrush>
            </Rectangle.Fill>
        </Rectangle>

    </Grid>
</Page>

これは半透過のグラデで塗りつぶした Rectangle をのせるだけ!

Opacity ではなく色のアルファ成分部分を変化させているのがポイントです

Opacity だとビジュアル要素全体に透過処理がかかるので性能的に不利だからです(そうはいっても敷き詰め処理の方で結構負担はかかりそうなんですが)

f:id:matatabi_ux:20140626062005p:plain

これで解像度に応じてきれいに背景が表示されるはず!