「静かな美術館を一人で歩いているような感じにしたい」
自分の作品を並べるアプリを Rork に作らせようとして、最初に打ち込んだのがこの一文でした。技術的な指示としてはあまりに曖昧です。それでも、私の頭の中にあったのは画面の仕様ではなく、その感触のほうでした。
作品を「届ける箱」が欲しい、という動機で個人開発に入った人間には、この順番はどうにも動かせません。何ができるかより先に、どう感じてほしいかがある。問題は、その語彙が実装に届かないことでした。
Rork を使うようになって変わったのは、この翻訳の部分です。以下は、曖昧な一文を投げてから、返ってきたものを自分の手で固定するまでの実作業の記録です。
「こういう感じ」を言葉にできない、という詰まり方
プログラマーは「何ができるか」から画面を組み立てる傾向があります。クリエイターは「どんな体験になるか」から入る。この順番の違いが、最初の混乱を生みます。
画面のイメージは鮮明にあるのに、それをコードや AI への指示に翻訳する言葉が出てこない。かといって技術用語を先に学ぼうとすると、今度は体験のイメージのほうが遠のいていく。私が Rork を触りはじめた頃に感じていたのは、この行き来のつらさでした。
Rork のようなツールが効くのは、まさにこの隙間です。翻訳の一段目を引き受けてくれる。ただし、投げっぱなしにすると翻訳結果は毎回ぶれます。そこをどう扱うかが、この記事の実質です。
感情から書いたプロンプトと、機能から書いたプロンプトを並べてみる
同じギャラリー画面を、二通りの書き方で頼んでみたことがあります。
❌ 機能から書いた指示
写真をグリッドで表示して、タップするとフルスクリーンになるギャラリーアプリを作って
✅ 感情から書いた指示
静かな美術館を一人で歩いているような体験を作りたい。
作品(写真)が落ち着いた余白の中に縦一列で並んでいて、
タップするとゆっくりフェードインして大きく表示される。
BGM は入れず、静寂を感じられる暗めの画面にしてほしい。
操作した手応えは控えめでいい。
前者は 3 列グリッド・白背景・即時切り替えという、どこかで見たことのある画面が返ってきました。後者は縦一列・暗い背景・フェードつきの詳細表示という、こちらの頭の中に近いものが返ってきます。
抽象的な指示ほど精度が落ちる、と身構えていたのですが、実際は逆でした。「静か」「余白」「ゆっくり」といった言葉は、余白の量・アニメーションの時間・彩度の低さといった具体的な選択に、そこそこ素直に落ちてきます。指示の書き方については、Rork AI プロンプトで狙った UI を正確に引き出す方法でより細かい技法を扱っています。
ただし、素直に落ちてくることと、再現できることは別の話でした。
返ってきた「雰囲気」を数値に固定する
「ゆっくりフェードイン」で返ってきた実装を読むと、フェードの時間は 400ms 前後でした。感覚としては悪くない。ところが、別の画面を追加で頼むと、そちらは 250ms で返ってきます。同じ「ゆっくり」なのに、生成のたびに揺れる。
そこで私は、生成結果を読んで「気に入った値」を拾い出し、一箇所に固定してから、以降はそのファイルを参照させるようにしました。
// theme/tokens.ts — 生成結果から拾った値を、ここ一箇所に集める
export const tokens = {
space: { edge: 20, betweenWorks: 24, gallery: 32 },
motion: { fadeIn: 420, press: 120 },
color: {
bg: '#12100E',
surface: '#1C1A17',
text: '#E8E3DA',
muted: '#9A9184',
accent: '#C0703C',
},
} as const;そのうえで、画面側はこの値だけを見るようにします。
// components/WorkImage.tsx
import { useEffect, useRef } from 'react';
import { Animated, Easing, Pressable, StyleSheet } from 'react-native';
import { tokens } from '../theme/tokens';
export function WorkImage({ uri, onPress }: { uri: string; onPress: () => void }) {
const opacity = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(opacity, {
toValue: 1,
duration: tokens.motion.fadeIn, // 420ms —「ゆっくり」を数値にしたもの
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}).start();
}, [opacity]);
return (
<Pressable onPress={onPress} style={styles.frame}>
<Animated.Image
source={{ uri }}
style={[styles.image, { opacity }]}
resizeMode="contain"
/>
</Pressable>
);
}
const styles = StyleSheet.create({
frame: {
padding: tokens.space.edge,
marginBottom: tokens.space.betweenWorks,
backgroundColor: tokens.color.bg,
},
image: { width: '100%', aspectRatio: 3 / 4, borderRadius: 2 },
});useNativeDriver: true を付けているのは、透明度の変化を UI スレッドから切り離すためです。ここを外したまま作品を数十枚並べると、スクロール中にフェードがひっかかります。
こうしておくと、次に画面を足すときの指示が「ゆっくりフェードインして」ではなく「theme/tokens.ts の motion.fadeIn を使ってフェードインして」に変わります。言葉の解釈ではなく参照になるので、揺れが止まります。
私がやったのは、要するに自分の感覚に単位を与えることでした。
| 私が言いたかったこと | 返ってきた実装 | 固定した値 |
|---|---|---|
| ゆっくりフェードインしてほしい | Animated.timing + Easing.out | motion.fadeIn = 420(ms) |
| 作品どうしを近づけすぎない | marginBottom によるリズム | space.betweenWorks = 24(px) |
| 静けさのある余白 | padding と暗い背景 | space.edge = 20 / color.bg = #12100E |
| 押した手応えは控えめに | opacity のわずかな変化 | motion.press = 120(ms) |
この「値を一箇所に集めて再生成に耐えさせる」考え方そのものは、再生成のたびに色と余白が崩れる問題をデザイントークンで止めるで詳しく整理しています。
作品のパレットをアプリのデザインシステムに移す
色については、もう少し手前から始められます。並べたい作品そのものが、すでにパレットを持っているからです。
私は作品から色を数点抜き出して、役割を添えて渡すようにしています。色コードだけを並べて「これを使って」と言うと、アクセントを背景に使われたりして、意図がほどけます。
このパレットでデザインシステムを作ってください。
#12100E — 背景(作品より暗く、作品を沈ませない)
#E8E3DA — 本文(長文でも読める明るさ)
#C0703C — アクセント(1画面に1箇所だけ。多用しない)
tokens.ts に定義し、画面からは直接 hex を書かないでください。
括弧の中に書いているのは、色の説明ではなく運用のルールです。ここを書いておくと、後から画面を足したときに #C0703C がボタンにもタブにもバッジにも散る、という崩れ方をしにくくなります。
ひとつだけ、感覚に任せてはいけない箇所があります。本文色と背景色のコントラストです。作品に寄せて色を選ぶと、本文がどうしても沈みます。私も最初、補助テキストを #8A8378 にしていて読みづらく、明るい側へ寄せて #9A9184 に落ち着きました。WCAG の AA 基準では、本文相当のテキストに 4.5:1 以上のコントラスト比が求められます。作品の雰囲気を守りたい気持ちはありますが、読めない文字は雰囲気ではなく不具合です。
Figma で先にデザインを固めてから渡す進め方もあります。そちらは Figma のデザインを Rork でアプリにする にまとめています。
手前で止まる場所 — フォント・再生成・審査
うまくいった話だけを書いても役に立たないので、私が実際に止まった場所も残しておきます。
フォントの読み込みと権利。 作品に合わせて表示用の書体を入れたくなりますが、アプリへの埋め込みは商用ライセンスの範囲を確認してからにしてください。ライセンス的に問題がなくても、読み込み前の一瞬だけ既定のフォントで描画されて、文字が入れ替わって見えることがあります。私は expo-font の読み込み完了までスプラッシュを維持する形で回避しました。
// app/_layout.tsx
import { useEffect } from 'react';
import { Stack } from 'expo-router';
import { useFonts } from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded] = useFonts({
'Display-Regular': require('../assets/fonts/Display-Regular.otf'),
});
useEffect(() => {
if (loaded) SplashScreen.hideAsync();
}, [loaded]);
if (!loaded) return null;
return <Stack screenOptions={{ headerShown: false }} />;
}ちらつきの原因と対処は Rork アプリでカスタムフォントのちらつきを止める に分けて書いています。
手で直した箇所が再生成で消えること。 これが一番こたえました。コントラストを調整した色を、次の機能追加の再生成でまるごと書き戻されたことがあります。手を入れる場所と、Rork に任せる場所を先に分けておくと事故が減ります。境界の引き方は Rork の再生成と手動修正を共存させる が実践的です。
審査の手前の作業。 アプリは作れても、証明書・プロビジョニングプロファイル・ガイドラインの確認は残ります。ここは生成の外側です。Rork で App Store に公開するまでの手順 を先に一度読んでおくと、作り終えてから慌てずに済みます。
生成コードを読まないまま進むこと。 最初は意味がわからなくて構いません。私も読み飛ばしていた時期がありました。ただ、直したいときに「なぜこうなっているか」がわからないと、指示の出しようがなくなります。全部を読む必要はなく、自分がこだわった箇所だけでも追っておくと、後の指示が短くなります。
アプリを一つの作品として置く
Rork を使うようになって変わったのは、順番でした。
以前は「これをやりたいなら、先にこの技術を身につけなければならない」という段差がありました。今は、やりたい感触から始めて、実装への翻訳を Rork に一段目だけ担ってもらえる。二段目——返ってきたものに単位を与えて、崩れないように固定する作業——は、まだ自分の手の中にあります。そしてこの二段目こそが、作品としての手触りを決めている気がしています。
まずは、自分の作品を 5 枚だけ並べる画面を、感情から書いた一文で頼んでみてください。返ってきた余白と速度を読んで、気に入った数値を tokens.ts に移す。そこまでで、翻訳の往復は一周します。手を動かす順番として、Rork で初めてのアプリを30分で作る と組み合わせると始めやすいはずです。
私自身、この翻訳の精度はまだ上げている途中です。同じところで立ち止まっている方の手がかりになれば嬉しく思います。お読みいただきありがとうございました。