LoginSignup
29
25

More than 1 year has passed since last update.

React + Electron製の絵コンテエディタアプリ「Mizutama Conte」を支える技術

Posted at

screenshot.png

リポジトリ

Web版

絵コンテはアニメ制作における設計図です。カット割り、セリフ、トランジション、カメラワーク等、アニメ制作を進める上で必要な情報がぎっしり詰まっています。従来(そして多くの場合現在も)絵コンテは紙とペンで作られてきました。

アニメ制作では各工程において、タイムシート、脚本、字幕、レイアウト等、様々な形式のデータを使用しますが、これらのデータは全て絵コンテを元に作られます。何度も再利用されるデータの管理は、デジタルの最も得意とすることろです。そこで、スタジオみずたまではアニメ制作の経験を生かし、デジタル絵コンテエディタアプリを開発することにしました。

先行アプリ

Storyboard Pro

新海誠さんも使ってるプロプライエタリアプリです。あまりにも高額で自主制作への導入はハードルが高いです。

Storyboarder

無料で使える海外のOSSです。Electron製です。高機能ですが、カメラワークやトランジションを設定できない等、痒いところに手が届かない部分があります。

e-Conte Board

iPadアプリです。日本の映像制作において必要十分な機能が揃っていますが、iPad専用のため、データの共有が難しい部分があります。

Griffith

Webベースのアプリです。一般公開はされていないようです。

Mizutama Conteで実現したいこと

いつでもどこでもプレビューできる

絵コンテはいろんな工程でいろんな立場の人が参照します。専用ソフトがなくてもブラウザだけでプレビューできるよう、jsonファイルとpsdファイルで絵コンテファイルを設計しました。

作画は使い慣れた外部アプリで

CLIP STUDIO PAINTやPhotoshop等、使い慣れたペイントアプリは人それぞれです。誰もが満足する描き心地を実装することは不可能であり、また、絵コンテ制作のためだけに普段と異なる環境で作画することはストレスになり得ます。そこで、このアプリでは敢えてペイント機能は実装せず、作画はPSD形式に対応した好みの外部アプリを連携させる方針にしました。

バージョン管理

コアとなる情報をjsonファイルに記述することで、Gitによるバージョン管理が可能になります。

開発を支える技術の紹介

React Spectrum

Adobeのデザインシステム「Spectrum」のReactコンポーネントです。

基本的に既存のコンポーネントを使いましたが、足りない部分は独自にコンポーネントを作っています。

例1:アプリフレーム

const BackGround = styled.div`
  width: 100%;
  height: 100%;
  overflow: hidden;
  margin: 0;
  padding: 0;
  background-color: var(--spectrum-alias-appframe-border-color);
`;

const ToolArea = styled.div<{ gridArea: string }>`
  background-color: var(--spectrum-alias-toolbar-background-color);
  grid-area: ${({ gridArea }) => gridArea};
  height: 100%;
  overflow: hidden;
`;

const GlobalGrid: React.FC = ({ children }) => (
  <Provider theme={defaultTheme}>
    <GlobalStyle />
    <BackGround>
      <Grid
        areas={['header header header', 'toolbar content sidebar']}
        columns={['size-600', 'auto', 'size-3600']}
        rows={['size-500', 'auto']}
        height="100vh"
        gap="size-25"
      >
        {children}
      </Grid>
    </BackGround>
  </Provider>
);

 2021-07-26 at 19.45.36.png

例2:アコーディオンメニュー

const LabelHover = styled.div`
  width: 100%;
  margin: var(--spectrum-global-dimension-size-50, var(--spectrum-alias-size-50)) 0;
  padding: var(--spectrum-global-dimension-size-50, var(--spectrum-alias-size-50)) 0;
  :hover {
    background-color: var(--spectrum-alias-highlight-hover);
    border-radius: var(--spectrum-global-dimension-size-50, var(--spectrum-alias-size-50));
  }
`;

const Toggle = styled.input`
  display: none;
`;

const Ul = styled.ul`
  padding-left: var(--spectrum-global-dimension-size-300, var(--spectrum-alias-size-300));
  margin: 0;
`;

const Label: React.FC<{ labelFor: string }> = ({ labelFor, children }) => (
  <LabelHover>
    <label htmlFor={labelFor}>{children}</label>
  </LabelHover>
);

export const Accordion: React.FC<{ labelName: string }> = ({ labelName, children }) => {
  const [toggle, setToggle] = useState(false);
  return (
    <>
      <Toggle type="checkbox" id={labelName} checked={toggle} onClick={() => setToggle(!toggle)} />

      <Label labelFor={labelName}>
        <Flex direction="row" gap="size-100" alignItems="center">
          {toggle ? <ChevronDown size="S" /> : <ChevronRight size="S" />}
          <Text>{labelName}</Text>
        </Flex>
      </Label>

      <Ul style={{ display: `${toggle ? 'block' : 'none'}` }}>{children}</Ul>
    </>
  );
};

 2021-07-26 at 19.48.19.png

ag-psd

PSDファイルを読むためのJavaScriptライブラリです。Psd型のオブジェクトとしてPSDファイルを扱うことができます。

{
  "width": 300,
  "height": 200,
  "channels": 3,
  "bitsPerChannel": 8,
  "colorMode": 3,
  "children": [
    {
      "top": 0,
      "left": 0,
      "bottom": 200,
      "right": 300,
      "blendMode": "normal",
      "opacity": 1,
      "transparencyProtected": false,
      "hidden": true,
      "clipping": false,
      "name": "Layer 0",
      "canvas": [Canvas]
    },
    {
      "top": 0,
      "left": 0,
      "bottom": 0,
      "right": 0,
      "blendMode": "multiply",
      "opacity": 1,
      "transparencyProtected": true,
      "hidden": false,
      "clipping": false,
      "name": "Layer 3",
      "canvas": [Canvas]
    }
  ],
  "canvas": [Canvas]
}

canvas部分はHTML5のHTMLCanvasElementです。ReactのJSXにはそのまま書けないため、toDataURLしてからimg要素の中で表示させます。

<View gridArea="picture" width="100%" height="auto">
                    {cut.picture?.children
                      ?.filter((child: Psd['children'], layerindex: number) => layerindex !== 0)
                      .map((child: Layer) => {
                        const src = child.canvas?.toDataURL('image/png', 0.4);
                        return (
                          <div
                            style={{
                              height: `${child.canvas && child.canvas.height * 0.12}px`,
                              width: `${child.canvas && child.canvas.width * 0.12}px`,
                              backgroundColor: '#fff',
                              position: 'relative',
                            }}
                          >
                            <img
                              style={{ transform: 'scale(0.12)', transformOrigin: 'left top' }}
                              src={src}
                              alt="cut"
                            />
                        );
                      })}
</View>

ReactN

useGlobalでグローバル状態管理を可能にするエクステンションです。使い方はuseStateとほぼ同じです。

index.tsx
import React, { setGlobal } from 'reactn';
import ReactDOM from 'react-dom';
import App from 'App';
import reportWebVitals from 'reportWebVitals';
import { Psd } from 'ag-psd';

const prtPsd: Psd = { width: 1, height: 1 };
const prtCut: Cut = {
  picture: prtPsd,
};

setGlobal({
  mode: 'Edit',
  tool: new Set(['Select']),
  cut: prtCut,
  globalCuts: [prtCut],
  globalPsds: [prtPsd],
  globalFileName: '',
});

index.tsxsetGlobalを宣言します。

global.d.ts
import 'reactn';

declare module 'reactn/default' {
  export interface State {
    mode: string;
    tool: Set<string> | undefined;
    cut: Cut;
    globalCuts: Cut[];
    globalPsds: Psd[];
    globalFileName: string;
  }
}

global.d.tsで型情報を記述します。

const CutContainer: React.FC = () => {
  const prtPsd: Psd = { width: 1, height: 1 };
  const prtCut: Cut = {
    picture: prtPsd,
  };
  const [cuts, setCuts] = useState([prtCut]); 
  const globalCuts = useGlobal('globalCuts')[0];
  const globalPsds = useGlobal('globalPsds')[0];
//略
}

あとはuseStateのようにコンポーネント内で記述するだけです。

react-hotkeys-hook

useHotKeysでキーボードショートカットを簡単に実装できます。

const [selected, setSelected] = useGlobal('mode');

useHotkeys('e', () => {
  setSelected('Edit');
});

useHotkeys('p', () => {
  setSelected('Preview');
});

react-typescript-electron-sample-with-create-react-app-and-electron-builder

今回のアプリの雛形として使用しました。
メイン・レンダラーいずれのプロセスもホットリロード対応で、最高のDXです。

29
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
25