entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

pixivFACTORYへ図形機能を追加した経緯と実装のお話

こんにちは、mrble(@tubdaka616)です。pixivFACTORYのエンジニアとして、主にフロントエンドの開発を担当しています。

2023年1月、新たに「図形機能」をリリースしました。🎉

予め用意された図形をグッズエディタ上から追加して、色や形を自由に変形することが可能です。2023年3月現在、「◯・□・△・♡」の4種類が利用可能です。ぜひ触ってみてください!!

「図形機能」追加の経緯

pixivFACTORYには元々「文字入れ機能」があって、グッズエディタ上から自由にテキストを追加することが可能でした。名前の通り「文字」として利用されることを想定していたのですが、これで頑張って図形を挿入するユースケースも度々見かけていました。

「図形を追加したい需要があるのなら、機能として提供すべきでは…🤔」という事で、図形機能をMVPとして実装しました。機能自体は様々なソフトウェアに存在する汎用的なものですが、pixivFACTORYというドメイン上でどのように活用されていくかは未知です。実際の使われ方を観測しつつ、今後の機能追加方針を検討していく予定です。

さて、ここからは図形機能の実装に関する技術的な知見について、お話していゆきます!

前提として、グッズエディタのフロントエンドはReact、バックエンドはRailsで作られています。

inside.pixiv.blog inside.pixiv.blog

過去に文字入れ機能追加時のinside記事も書いてます。興味があれば読んでみてください〜〜

ベースとなる図形の作成

まずグッズエディタに挿入した時の初期表示になるベースの図形をFigmaで作成しました。Figmaから図形をSVG形式で出力することが可能なので、属性を一部調整して利用しています。

問題はこのSVGをどうやってDBに持たせて、フロント側へ配信するか…。以下の4案が候補として上がりました。

1.width、height、rx、ry のような属性カラムのみDBにもたせる
😓 > 特定の図形でしか利用されないnullableなカラムが増えてしまう。種類が増えるほど辛い。

2.全ての図形を<path d="…" />で表現する
😓 > カラムは1つになるが、SVGの表現にd属性以外使えない。

3.SVGを丸ごと文字列として保存するbodyカラムを作る
😳 > sanitizeに気をつけないといけないが、それ以外の問題は無いのでは。

4.DBはenumのkindカラムのみを持ち SVGは別に定義した定数を利用する
😄 > 3案の派生。定数として定義したSVGは追加したら一生変更出来ないがDBがめっちゃシンプルになる。
(※後から変更・削除をすると、既に作られたグッズのデザインも書き変わってしまう)

最終的に「テーブルが一番シンプルに保てる」という理由で4案目で行くことにしました。

module Diagram::Heart1
  BODY_SVG = <<~SVG.squish.freeze
    <svg viewBox="0 0 109 109" preserveAspectRatio="none " xmlns="http://www.w3.org/2000/svg">
      <path d="M54.5005 99.35C55.0388 99.35 81.267 73.6638 90.7196 61.9762C99.5943 50.9723 101.431 43.7255 101.961 36.4629C102.492 29.2004 97.6784 12.3173 80.1982 11.0755C62.7181 9.83361 57.1763 24.2408 54.5005 30.4423C51.8246 24.2408 46.2908 9.84147 28.8107 11.0755C11.3305 12.3095 6.50128 29.2004 7.03962 36.4629C7.57796 43.7255 9.41464 51.0116 18.2814 61.9762C27.734 73.6638 53.9621 99.35 54.5005 99.35Z" fill="currentColor"/>
    </svg>
  SVG

  THUMBNAIL_SVG = BODY_SVG
end

SVGを丸ごと文字列として定義している実際の定数はこんな感じです。Rails側でkindをkey、SVGの文字列をvalueとしたjsonを生成してフロント側に返しています。名前が「Heart1」なのは、他の形状の♡を追加したくなった時に命名で頭抱えそうだったので連番にしています。

<svg
  dangerouslySetInnerHTML={{
    __html: DOMPurify.sanitize(svg, {
      USE_PROFILES: {
        svg: true,
        svgFilters: true,
      },
    }),
  }}
/>

フロント側はAPI経由で文字列として取得した物を、dangerouslySetInnerHTMLを使いSVGのDOMとして画面に描画しています。そのまま使うとXSSの脆弱性となるので、DOMPurifyでsanitizeしてます。

図形を自由変形可能にする

ベースの図形はviewBoxが正方形になる様に設定していますが、実際のグッズエディタでは元の縦横比率を無視して自由変形が可能です。これはSVG属性のpreserveAspectRatio="none"で実現しました。preserveAspectRatioは元の比率を保持すべきか制御できる属性です。

参考:https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio

拡大・縮小機能は親のBoudingBoxコンポーネントの機能として備わってて、図形は元の比率を無視してBoudingBoxの領域全体に変形させる事で図形の自由変形を表現しています。

speakerdeck.com BoudingBox自体のリサイズ処理など、グッズエディタ自体の設計に関してはこちらの資料に記載されています。

実は「自由変形が可能なレイヤー」はpixivFACTORYに今まで存在してなかったので、操作に関する細かい機能改修も行ってます。

図形の色を変更可能にする

<g style={{ color: `#${fill}` }}>
  <svg>
    <rect fill="currentColor"/>
  </svg>
</g>

図形のSVG自体は実際の表示色を持っていませんが、fill="currentColor"とすることで、親要素から色の変更を可能にしました。

図形の移動可能範囲を制限する

無限に移動出来るとレイヤーが画面外から帰ってこれなくなるので、移動は操作可能な範囲までに制限する必要があります。

特に「△・♡」の様にviewBox内に広めの余白がある図形は、拡大や回転操作の時に意図せず画面外に出てしまう可能性が考えられます。

例えば上記の状態でフォーカスを外すと以降操作不可能です。詰みます。
(キャンバスサイズが大きい別のアイテムに変更すれば一応救出可能ですが…)

また図形の選択判定は「図形部分」のみで、BoudingBoxにはありません。

「持たせればいいじゃん!」って自分も最初思いましたがレイヤーを重ねた時、手前のレイヤーに選択判定が吸われるので案外使い勝手が悪いです。

この問題に関しては、図形の移動可能範囲を設定し、超えた場合は範囲内に移動させる事で対処しました。実際にグッズとなるデザイン面は内側の青枠なので、デザインの自由度が下がるということは無いです。

図形の移動可能範囲は「図形の中心 + 幅の1/4が画面内」と定義しています。図形の大きさを元に可動域を計算するので、拡大/縮小、縦横比率変にも対応できます。中心の座標を元に計算するので、図形が回転した時に基準が崩れることもありません。

最後に

与えられた作業をこなすのではなく「どうすればユーザーへ自由なデザイン体験が提供できるか」エンジニアも一緒に考えて実装できるのがpixivFACTORYチームの魅力です。

ピクシブ株式会社はプロダクトを技術で良くしたいオーナーシップ溢れるフロントエンドエンジニアを募集しています!

hrmos.co

20220927102812
mrble
2022年6月中途入社。かわいい絵を描くのがすき。pixivFACTORYの開発をしています。