以下の内容はhttps://mangano-ito.hatenablog.com/entry/2026/01/10/100000より取得しました。


2026 年ですね。年末大掃除で見つけた昔作った謎スクリプト "solitarize" を紹介します。

この記事は はてなエンジニア Advent Calendar 20251 月 10 日の記事です。前の日の記事は id:rmatsuoka さんが書いた「ぼーっと眺めるためのRSSリーダーを自作したらインターネットが楽しい」でした。

本日は はてなAndroid エンジニアをやってる id:mangano-ito がお送りします。


さて、もう 2026 年です。

2025 年末、自宅の掃除などをしていました。デジタル大掃除というかんじで、NAS のデータの整理をしていましたところ、思いがけず懐かしのデータに出会ったのでこの機会に紹介したいと思います。

ソリティア

みなさんはソリティアって知ってますか。

ja.wikipedia.org

多分知らない方のほうが少ないんじゃないかと思います。 というのも、Windows に入ってるソリティアがとても有名だからです (小学生みたいな文章)。

プリインストールされている (Windows 98)

ソリティア (Windows 98)

ところで、上の Wikipedia にあるように、ソリティアは「一人だけで遊ぶことのできるゲームの総称」なので、 Windows の「ソリティア」は本当は「クロンダイク」というゲームであることも有名です。

ja.wikipedia.org

そして「フリーセル」もソリティアのひとつということです:

ja.wikipedia.org

いずれにしても、Windows にプリインストールされていた、カードゲームを覚えている人は多いでしょう。

演出

で、Windowsソリティアってなんかクリア (?) したら、異常にテンションの高い演出がはいって、すごく印象深いんですよね。

Windows 98ソリティアのアニメーション

これは実際に Windows 98仮想マシンで動かしたものをキャプチャしています。

仮想環境だからかアニメーションが異常に高速になってたので速度を半分にしたんですが、もっとゆっくりとポーンポーンって小気味良いよく落ちてくる感じの思い出だったので、Windows 11 に入ってたフリーセルをやってみたら同じ演出で、こっちのイメージのほうが近いですね:

Windows 11 のフリーセルのアニメーション

フリーセル」でググったらアプリ画像で同じ演出が使われているくらいなので、そのセンセーショナルさといったら推して知るべしかと思われます:

引用元: Google 画像検索

ふたたび昔の話

話を年末に戻しますと、昔このアニメーションをブラウザで再現する JavaScript のコードを作成しまして、それが NAS に残っていました。どういうものかというと、

(GIF 版)
ブログの DOM 要素をソリティアのように破壊

ブラウザの任意のページでそのユーザースクリプトを実行すると、ページが「ソリティア」できるというものです。

Wikipedia の「トランプ」のページでやるとまるでソリティア

Wikipedia の「トランプ」のページ を使うと爽快感があります。

カーソルで選択した DOM 要素をソリティアのカードのごとく地面に落とすことができるので、Web ページを破壊できます。

Google の「ソリティア」の検索結果もソリティア

Google 画像検索の「ソリティア」の画像もソリティア

利用価値はゼロですが、自由な感じが若さを感じてこんなもの作ってたな…と思いにさせた一品です。コピーとかされてて実際作った日時は定かではないのですが……

あと地面との衝突の計算が甘くて、たまに地面を突き抜けていくのがめちゃくちゃダサいなと思いました。

ミニ体験コーナー

↓ ミニソリティア体験コーナーを用意したので遊んでいってください ↓

このエリア内の DOM 要素はカーソルで選択して「ソリティア」できます。

虹色にアニメーション

動かなかったらすみません。

遊び終わったらリロードしてください。

ちょっとだけコードを紹介

流石に Advent Calendar として、これだけで終わるのはマズい気がしたので、コード見て思い出しながら実装の一部を省略しつつ紹介します。コーディングスタイル含め恥ずかしいですが当時のままを持ってきています。

まず、このアニメーションの軌跡は canvas とかではなく、本当に選択した DOM 要素をクローンしてきて、Shadow DOM なぞ考えもせずそれをドキュメント上のオーバーレイした要素に appendChild しています:

function clone( element )
{
    var dup       = element.cloneNode( false );
    dup.id        = "";
    dup.className = "";
  
    var children = element.childNodes;
    for ( var i = 0; i < children.length; ++i )
    {
       var child = clone( children[ i ] );    
       dup.appendChild( child );
    }

    // deep-copy computed styles
    // because the original style depends on the context where it is spawned
    var style = null;
    try
    {
        style = element.currentStyle || document.defaultView.getComputedStyle( element, "" );
    }
    catch ( exception )
    {}
  
    if ( style !== null )
    {
      for ( var i = 0; i < style.length; ++i )
      {
        var prop          = style[ i ];
        dup.style[ prop ] = style[ prop ];
      }
      dup.style[ "position" ] = "absolute";    
      dup.style[ "z-index" ]  = "250";
    }

    return dup;
}

ポイントとしては、getComputedStyle で計算済スタイルをそのまま適用したり、子要素をディープコピーしている点です。だから、要素のスタイルはそのまま生きていて、アニメーションはアニメーションするんですよね:

クローンされた要素なのでアニメーションはアニメーションしたままとなる

こんな雑な実装でよく動くなって思うんですが、意外とこれくらい愚直でもなめらかにレンダーされるので、マシンスペックもそうですがブラウザの最適化ってすごいなって感じです。

ほか、放物線を描くように位置を計算したり、地面 (ことビューポートの下端) にぶつかったらバウンドするような物理計算まわりです:

function cloneBound( object, dt )
{
    var element   = object.clone;
    
    if ( BOUNCE_CLONED )
    {
       var dup       = shallowClone( element, object.z++ );
       element       = dup;
    }

    var x         = object.x;
    var y         = object.y;

    var vx        = object.vX;
    var vy        = object.vY;

    var width     = object.width;
    var height    = object.height;

    var dimension = getViewportDimension();
    var offset    = getViewportOffset();

    var bottom      = y        + height;
    var groundY     = offset.y + dimension.height;
    var diffGroundY = ( groundY - bottom );

    var dvy       = GRAVITY_ACCELERATION * dt;
    var dy        = ( vy + dvy )         * dt;
    
    while ( dy > diffGroundY && dt > 0 )
    {
      // this means that the element is going to collide the ground at this update
      // so make a bounce and recalculate the position bounded
      var t  = ( groundY - bottom ) / ( vy );
      dt    -= t;

      vy    +=  GRAVITY_ACCELERATION * t;
      vy    *= -BOUND_COEFFICIENT;

      dvy    = GRAVITY_ACCELERATION * dt;
      dy     = ( vy + dvy )         * dt;
        
      vy    += dvy;
      y      = groundY - height + dy;

      dvy    = GRAVITY_ACCELERATION * dt;
      dy     = ( vy + dvy )         * dt;

      bottom      = y + height;
      diffGroundY = ( groundY - bottom );
    }

    vy       += dvy;
    x        += vx;
    y        += dy;

    object.x  = x;
    object.y  = y;
    object.vY = vy;
    setPosition( element, x, y );

    if ( ( x < offset.x - width ) || ( dimension.width + offset.x < x ) )
    {
      // stop solitarization
      // when object is out of the viewport range
      object.active = false;
      --_activeCount;
    }

    if ( BOUNCE_CLONED )
    {
       _layer.appendChild( dup );
    }
}

function callback( object, t )
{
  var now = Date.now();
  var dt  = now - t;

  cloneBound( object, dt )
  if ( !_running || !object.active )
  {
      console.log( "solitairization finished." );
      if ( _activeCount <= 0 )
      {
          _running = false;
      }
      return;
  }
    
  delayed( callback.bind( this, object, now ) );
};

アニメーションフレームごとに重力加速度を加えた位置に要素をクローンしていってる感じですね。地面に当たったらバウンドするように衝突したかもチェックしています。

このあたり、フレームごとに差分を計算しているがゆえに、加速度がものすごい場合は地面との衝突判定ができなくてバウンドせず突き抜けるような要因があると思います。

全体的に極めて荒々しく愚直なコードという感じがしますが、これでまあまあ動いているのが不思議でもあります。var を使ってるあたりも年代を察することもできるのではないでしょうか。

おわり

昔つくったコードをながめていると、なんか野暮ったくてダサいなという部分もあれば、そういえばこういう書き方にハマってたなとかが思い出されて、若干黒歴史を感じますが、アイデアとか勢いみたいなのが感じられ、掘り出すとしみじみと趣深くて、新年に向けた気力が戻ってきた感じもします。

以上です! 2026 年も Happy Coding!


次の日の はてなエンジニア Advent Calendar 2025 の記事もぜひお楽しみに!




以上の内容はhttps://mangano-ito.hatenablog.com/entry/2026/01/10/100000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14