初めてのSvelte開発！AIイラスト補助アプリを作ってみた感想

# 目的

この記事では、AIイラストレーターのためのプロンプト生成補助アプリ、
**Cyber Prompter**を制作しましたのでご紹介します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384142/fcebd641-1665-7be0-fa52-b274f7573405.png)
本記事の目的は大きく２つです。
- Cyber Prompterの**概要と目的**
- (Reactをよく使う人目線での)**Svelteを使ってみての感想**


# アプリデモ動画

まず、どんなアプリか知っていただきたく、デモ動画を御覧ください！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Cyber PrompterというイラストAIの呪文(プロンプト)生成を補助するアプリをチームで作りましたー！<br>動画制作は <a href="https://twitter.com/mskz2021?ref_src=twsrc%5Etfw">@mskz2021</a> さんです！<br>使ってみてね<a href="https://t.co/pfVZPeLEnl">https://t.co/pfVZPeLEnl</a> <a href="https://t.co/r5DHPop4qO">pic.twitter.com/r5DHPop4qO</a></p>&mdash; toromo (@toromo12) <a href="https://twitter.com/toromo12/status/1645096610718973954?ref_src=twsrc%5Etfw">April 9, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 使い方

1. 検索ワードの入力
   - ユーザーは自分の検索したいワードを入力します。

2. プロンプトの選択
   - 右から左へ流れていくプロンプト候補から、複数のプロンプトを選択して自分の好きなプロンプトを作成します。

3. プロンプトの作成
   - 選択したプロンプトをコピーボタンを押してクリップボードにコピーします。

4. AI画像の生成
   - 各々が好きなAIのプロンプトから生成するAI画像アプリにコピーしたプロンプトを入れて、画像を好きに生成します。

https://cyber-prompter.vercel.app/

# アプリのUIについて

アプリのUIについて、以下の特徴を説明します。

<img width="270" alt="b0079458-11fb-612b-37e5-4abad170ae06.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384142/b0079458-11fb-612b-37e5-4abad170ae06.gif">

- プロンプトの候補が右から左に流れるデザイン
- オリジナリティの高くインタラクティブな操作
- 直感かつ簡潔で使いやすいUI(パッと見で使える)

https://cyber-prompter.vercel.app/

# 使用された技術について

Cyber Prompterで使用された技術について、package.jsonの内容を紹介し、各技術の役割や特徴について説明します。

- `@smui`
  - Svelteでマテリアルデザインに則った仕組みを提供してくれるライブラリ
- `@sveltejs/kit`
  - ブラウザーで最小限の作業を行う息をのむほど簡潔なコンポーネントを、HTML、CSS、JavaScript などの既におなじみの言語を使用して記述できます
- `unplugin-icons`
  - 非常に多くのアイコンにコンポーネントとしてアクセスできるライブラリ
- `prettier, prettier-plugin-svelte, eslint, eslint-config-prettier, eslint-plugin-svelte3`
  - コードをキレイにしたり、静的解析してくれるツールたち

<details><summary>package.jsonを展開</summary><div>

```json:package.json
"devDependencies": {
		"@smui/button": "^7.0.0-beta.0",
		"@smui/circular-progress": "^7.0.0-beta.0",
		"@smui/common": "^7.0.0-beta.0",
		"@smui/fab": "^7.0.0-beta.0",
		"@smui/icon-button": "^7.0.0-beta.0",
		"@smui/paper": "^7.0.0-beta.0",
		"@smui/textfield": "^7.0.0-beta.0",
		"@sveltejs/adapter-auto": "^2.0.0",
		"@sveltejs/kit": "^1.10.0",
		"@types/cookie": "^0.5.1",
		"@typescript-eslint/eslint-plugin": "^5.54.0",
		"@typescript-eslint/parser": "^5.54.0",
		"eslint": "^8.35.0",
		"eslint-config-prettier": "^8.6.0",
		"eslint-plugin-svelte3": "^4.0.0",
		"normalize.css": "^8.0.1",
		"prettier": "^2.8.4",
		"prettier-plugin-svelte": "^2.9.0",
		"smui-theme": "^7.0.0-beta.0",
		"svelte": "^3.55.1",
		"svelte-preprocess": "^5.0.1",
		"tslib": "^2.5.0",
		"typescript": "^4.9.5",
		"unplugin-icons": "^0.15.3",
		"vite": "^4.1.4"
	}
```

</div></details>

viteの設定はこんな感じでsveltekitやアイコンの設定を噛ませています。


```ts:vite.config.ts
import { defineConfig } from 'vite';
import { sveltekit } from '@sveltejs/kit/vite';
import Icons from 'unplugin-icons/vite';

export default defineConfig({
	plugins: [
		sveltekit(),
		Icons({
			compiler: 'svelte'
		})
	]
});
```

## 表現技法

単語が右から左に流れる部分はオリジナリティにこだわりました。
いい感じではないでしょうか？

<img width="270" alt="b0079458-11fb-612b-37e5-4abad170ae06.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384142/b0079458-11fb-612b-37e5-4abad170ae06.gif">

その結果動作はかなり重くなったのですが、そんなの知りません。
スペックの高い端末を使って下さい！(丸投げ)

動きにはCSSの仕組みを使いました。

```css: 横に流れる
@keyframes right-to-left {
  from {
    right: -300px;
  }
  to {
    right: 100vw;
  }
}
```

```css: 上下にゆらゆら
@keyframes swing {
  0%,
  100% {
    transform: translateY(0px);
  }
  50% {
    transform: translateY(12px);
  }
}
```

背景とイイ感じに馴染む部分には`mix-blend-mode`を使いました。
この機能、スゴイです。
背景と効果をつけて馴染ませることができます。
設定値は`color-dodge`にしましたが、他にも`multiply`,`hard-light`,`difference`などたくさんあります。
[参考：mix-blend-mode - CSS: カスケーディングスタイルシート | MDN](https://developer.mozilla.org/ja/docs/Web/CSS/mix-blend-mode)

```scss:背景とイイ感じに馴染む
* :global(.prompt-text-image) {
  color: rgba(0, 0, 0, 0);
  background-size: cover;
  background-position: center center;
  mix-blend-mode: color-dodge;  // color-dodgeを指定！
  filter: blur(2px);
  &:hover {
    mix-blend-mode: normal;  // 選択時はblendをしないことで選択状況が分かりやすい
    filter: blur(2px);
  }
}
```

ただ、`mix-blend-mode`を使うと**テキストまで背景と馴染んで**しまい、文字が見づらくなってしまいました。
そこで、**テキストとその背景画像を分ける**ことにしました。(ちなみにテキストの背景は桃です。どんぶらこです。)
全く同じ場所にテキストを配置し別要素とすることで`mix-blend-mode`の適用を受けません。
要素数が倍になりパフォーマンスが悪化してしまったので、他にいいやり方があったら良かったのですが、
どうしても他の方法が思いつかなかったのでこうしました。(良いやり方があったら教えてください...!)
下記が流れる部分のhtmlで`button`が**背景要素**で`div`が**テキスト要素**です。

```html:流れる部分のhtml(svelte記法あり)
{#each fallingItems as prompt (prompt.id)}
  <button
    class="button-reset prompt-text prompt-text-image {outerWidth > WIDE_PX ? 'wide' : ''}"
    style="top: {(0.1 + prompt.random * 0.7) *
      outerHeight}px;background-image: url('textbox/1.png');"
    on:click={handleSelectPrompt(prompt.id)}
  >
    <!-- / expand side  -->
    /{prompt.prompt}/</button
  >
  <div
    class="prompt-text prompt-text-content {outerWidth > WIDE_PX ? 'wide' : ''}"
    style="top: {(0.1 + prompt.random * 0.7) * outerHeight}px"
  >
    <!-- for not blurring text -->
    <span class="prompt-text-not-display">/</span>{prompt.prompt}<span
      class="prompt-text-not-display">/</span
    >
  </div>
{/each}
```

## CSS フレームワーク

今回は CSS フレームワークにMaterial UIを使いました。
Reactの方はいつも使っているので慣れているのですが、Svelteのは元のフレームは同じでも使い方がかなり違ったので大変でした。
特にアイコンに関しては書き方がかなり違っていました。
Svelteの場合は以下のように書きます。

```html
<script lang="ts">
  import { Icon } from '@smui/button';
</script>
<section>
  <Icon class="material-icons">content_copy</Icon>
<section>
```

Reactの場合はコンポーネントを呼び出せばOKです。

```tsx
import React from 'react'
import ContentCopyIcon from '@mui/icons-material/ContentCopy';

export const Test = () => {
  return (
    <ContentCopyIcon />
  )
}
```

Svelteの場合、Introducing Material Symbolsの方法が採用されていて**クラス名**と**テキストコンテンツ部**に文字を入力して使います。
テキストコンテンツにテキストを入力しているのにも関わらず表示内容がアイコンになることが非常に違和感でした。
ここは**Reactと比べても表記の方法が異なり慣れない**点でした。

あとはイベントアタッチの記述方法がかなり違うのでそこは戸惑いました。

```html
<form on:submit={handleGenerate}>  // on:event_nameで表記する
```
ただ、ここら辺は慣れの問題かなと思います。

あとは **CSS の書き方**が**素の CSS というか SCSSで書けるのが新鮮**でした。
素の CSS の書きが新鮮になる日が来るとは思っていませんでした。

例えば、Svelte(普通のCSS)だと`justify-contents: "center";`とケバブケースで書きますが、Reactだと`justifyContents: "center",`とキャメルケースで書きます。
Reactだとjavascriptって感じなのですが、Svelteだとcssとjs(とhtml)が共存していて違和感がすごかったです。
(vueとかもSvelte側だと思うので、React固有の感じ方かもです。)

Chromeの開発者ツールでスタイルを調整して**直接コピペできるのは割と嬉しかった**です。
Reactでは変換しないとできないので。


## ストア機能

ストアの機能もよくできていて 作成が簡単でした。
こんな感じで`writable`を使って変数をエクスポートします。
シンプル。

```typescript
import { writable } from 'svelte/store';

export type Prompt = {
	id: string;
	prompt: string;
	createdAt: Date;
	selectedAt: Date | null;
	random: number;  // 出現位置用
};

export const prompts = writable<Prompt[]>([]);
```

その後、update()関数などが提供されるので、それを用いて更新します。
```typescript
prompts.update((prev) => {
	return [
		...prev,
		{
			id: crypto.randomUUID(),
			prompt: word,
			createdAt: new Date(),
			selectedAt: null,
			random: Math.random()
		}
	];
});
```

ここは、スゴイ便利だなーと感じました。
余計な手続きが非常に少なくていいですよね。

## React vs Svelte 所感

ここまで振り返ってきて、やはり**慣れの問題は重要**だと思いました。
頭を切り替えるのが結構たいへんでした。
そういう意味ではSvelteが流行ってどんどん使うようになれば、
慣れてきて記述も少ないので素早く実装できるのかもしれないと思いました。
新しい言語に触れるという意味では良い経験になりました。

**エコシステムに不安がある点**も採用するのが難しいところです。
Reactはサードパーティー製の良いライブラリが充実しており採用時にも安心できます。
Svelteはベータ版が多いのでかなり短い頻度でいきなり記述方法や設定の仕方が変わったりして困りごとが増えそうな印象です。
今回の開発時も前上がっていた設定が上手くいかずvercerl上のビルドが失敗しまくってハマりました。
内部のバージョンが意図せず上がってしまったことが原因で、固定したら上手く行きましたが、割とそういうことが起きそう。
プロジェクトで使うとすると、ここら辺は運用保守コストにそのまま乗っかってくる部分なのでシビアにならざるを得ないと思いました。

仕事や業務また使える技術者の数を考えると、**Reactがまだまだ人気の高く、需要の高いフレームワーク**であるのは間違いないです。
私としては一旦Svelte使わずReactに戻ろうと思っています。

# さいごに

ご覧いただき、ありがとうございました。
この記事では、AIイラストレーターのための新しいアプリ、**Cyber Prompter**について紹介しました。
本記事の目的は以下の２つでした。
- Cyber Prompterの**概要と目的**
- (Reactをよく使う人目線での)**Svelteを使ってみての感想**

**この記事を読んでいただき、ありがとうございます。**
Svelteでの開発やCyber Prompterに興味を持っていただけたら嬉しいです。
ぜひ、開発したアプリを使ってみて、**感想やフィードバックをいただけると幸い**です。
今後も新しい技術に挑戦し、記事を通じて情報発信していきたいと思います。
フォローもぜひよろしくお願いします！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Cyber PrompterというイラストAIの呪文(プロンプト)生成を補助するアプリをチームで作りましたー！<br>動画制作は <a href="https://twitter.com/mskz2021?ref_src=twsrc%5Etfw">@mskz2021</a> さんです！<br>使ってみてね<a href="https://t.co/pfVZPeLEnl">https://t.co/pfVZPeLEnl</a> <a href="https://t.co/r5DHPop4qO">pic.twitter.com/r5DHPop4qO</a></p>&mdash; toromo (@toromo12) <a href="https://twitter.com/toromo12/status/1645096610718973954?ref_src=twsrc%5Etfw">April 9, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>