Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: キャラ編集画面のUIを実装 #58

Merged
merged 6 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(
return (
<Comp
className={cn(
"rounded-lg border-4 border-white bg-image-card shadow-lg",
"rounded-lg border-4 border-white bg-image-card bg-zinc-500 shadow-lg",
className,
)}
ref={ref}
Expand Down
27 changes: 27 additions & 0 deletions app/components/CardButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Meta, StoryObj } from "@storybook/react";

import { CardButton } from "./CardButton";

const meta: Meta<typeof CardButton> = {
title: "Components/CardButton",
component: CardButton,
decorators: [
(Story) => (
<div className="flex">
<Story />
</div>
),
],
};

export default meta;

export const Default: StoryObj<typeof CardButton> = {
args: {},
};

export const Checked: StoryObj<typeof CardButton> = {
args: {
checked: true,
},
};
46 changes: 46 additions & 0 deletions app/components/CardButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { ReactNode } from "react";
import { cn } from "~/libs/utils";

export interface CardButtonProps {
value: string;
name: string;
checked?: boolean;
onChange?: (value: string) => void;
src?: string;
color?: string;
}

export function CardButton({
value,
name,
checked,
onChange,
src,
color,
}: CardButtonProps): ReactNode {
return (
<label className="drop-shadow-lg">
<input
type="radio"
name={name}
value={value}
checked={checked}
onChange={() => onChange?.(value)}
className="sr-only"
/>
<div
className={cn(
"rotate-3 border-4 border-transparent",
checked && "border-orange-400",
)}
>
<img
src={src}
alt=""
className="aspect-square w-full border-4 border-white bg-zinc-400"
style={{ backgroundColor: color }}
/>
</div>
</label>
);
}
140 changes: 60 additions & 80 deletions app/components/ModelConfig.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import * as Tabs from "@radix-ui/react-tabs";
import { ACCESSORY_LIST, MODEL_LIST } from "~/features/profile/Profile";
import type {
Accessory,
CharacterSetting,
Model,
} from "~/features/profile/Profile";
import { CardButton } from "./CardButton";
import { TabBar } from "./TabBar";

const COLORS = [
const HAIR_COLORS = [
"#333333",
"#ededed",
"#582a22",
Expand All @@ -21,103 +24,80 @@ const COLORS = [

interface ModelConfigProps {
characterSetting: CharacterSetting;
onModelSelect?: (model: Model) => void;
onCostumeSelect?: (costume: number) => void;
onCostumeSelect?: (model: Model, idx: number) => void;
onAccessorySelect?: (accessory: Accessory) => void;
onHairColorChange?: (color: string) => void;
}

export function ModelConfig({
characterSetting,
onModelSelect,
onCostumeSelect,
onAccessorySelect,
onHairColorChange,
}: ModelConfigProps) {
return (
<div className="mx-auto grid w-full max-w-screen-sm gap-y-4 px-4">
<div className="flex gap-4">
<span className="flex-shrink-0">モデル:</span>
<div className="flex flex-grow flex-wrap justify-between gap-2">
{MODEL_LIST.map((model) => (
<label key={model}>
<input
type="radio"
name="model"
value={model}
checked={model === characterSetting.character}
onChange={() => onModelSelect?.(model)}
/>
<span>{model}</span>
</label>
))}
</div>
<Tabs.Root
className="mx-auto grid max-h-full w-full max-w-screen-sm grid-rows-[auto_1fr]"
defaultValue="costume"
>
<div className="-mt-2 w-full sm:mt-4 sm:px-4">
<TabBar />
</div>

<div className="flex gap-4">
<span className="flex-shrink-0">モデル:</span>
<div className="flex flex-grow flex-wrap justify-between gap-2">
{[0, 1, 2].map((idx) => (
<label key={idx}>
<input
type="radio"
<div className="min-h-0 overflow-auto pb-16">
<Tabs.Content
value="costume"
className='grid grid-cols-3 gap-2 p-4 data-[state="inactive"]:hidden sm:grid-cols-5'
>
{MODEL_LIST.flatMap((model) =>
[0, 1, 2].map((idx) => (
<CardButton
name="costume"
value={idx}
checked={idx === characterSetting.costume}
onChange={() => onCostumeSelect?.(idx)}
key={`${model}_${idx}`}
value={`${model}_${idx}`}
checked={
model === characterSetting.character &&
idx === characterSetting.costume
}
onChange={() => {
onCostumeSelect?.(model, idx);
}}
src={`https://placehold.jp/100x100.png?text=${model}_${idx}`}
/>
<span>{idx + 1}番</span>
</label>
))}
</div>
</div>

<div className="flex gap-4">
<span className="flex-shrink-0">アクセサリー:</span>
<div className="flex flex-grow flex-wrap justify-between gap-2">
)),
)}
</Tabs.Content>
<Tabs.Content
value="accessory"
className='grid grid-cols-3 gap-2 p-4 data-[state="inactive"]:hidden sm:grid-cols-5'
>
{ACCESSORY_LIST.map((accessory) => (
<label key={accessory}>
<input
type="radio"
name="accessory"
value={accessory}
checked={accessory === characterSetting.accessory}
onChange={() => onAccessorySelect?.(accessory)}
/>
<span>{accessory}</span>
</label>
<CardButton
name="accessory"
key={accessory}
value={accessory}
checked={accessory === characterSetting.accessory}
onChange={() => onAccessorySelect?.(accessory)}
src={`https://placehold.jp/100x100.png?text=${accessory}`}
/>
))}
</div>
</div>

<div>
<label className="flex gap-4">
<span className="flex-shrink-0">髪の色:</span>
<input
type="color"
value={characterSetting.hair}
onChange={(e) => {
// TODO: 色の更新処理が頻繁に呼び出されてしまうため、debounceする
onHairColorChange?.(e.target.value);
}}
/>
</label>

<div className="flex flex-grow flex-wrap justify-between gap-2">
{COLORS.map((color) => (
<label key={color}>
<input
type="radio"
name="hair"
value={color}
checked={color === characterSetting.hair}
onChange={() => onHairColorChange?.(color)}
/>
<span>{color}</span>
</label>
</Tabs.Content>
<Tabs.Content
value="hair"
className='grid grid-cols-3 gap-2 p-4 data-[state="inactive"]:hidden sm:grid-cols-5'
>
{HAIR_COLORS.map((color) => (
<CardButton
name="hair"
key={color}
value={color}
checked={color === characterSetting.hair}
onChange={() => onHairColorChange?.(color)}
color={color}
/>
))}
</div>
</Tabs.Content>
</div>
</div>
</Tabs.Root>
);
}
91 changes: 46 additions & 45 deletions app/components/ModelLoad.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,52 +147,53 @@ function Character({ characterSetting }: ModelProps): ReactNode {

export function ModelViewer({ characterSetting }: ModelProps): ReactNode {
return (
<Canvas
className="aspect-square h-auto w-full sm:max-h-[50dvh]"
scene={{
background: new THREE.Color("#0ea5e9"),
}}
camera={{
position: [1, 0, 2],
fov: 30,
}} // カメラの初期位置と視野角を設定
>
{/* ライトを設定 */}
<ambientLight />
<directionalLight position={[6, 5, 5]} intensity={1} />
{/* ポストプロセッシング */}
<EffectComposer autoClear={false}>
<Bloom intensity={1} luminanceThreshold={1} radius={0.8} mipmapBlur />
</EffectComposer>
<Suspense
fallback={
<Billboard>
<Text
fontSize={0.1}
font="/assets/font.ttf"
characters="読み込み中…"
>
読み込み中…
</Text>
</Billboard>
}
<div className="aspect-square h-auto w-full sm:max-h-[50dvh]">
<Canvas
scene={{
background: new THREE.Color("#0ea5e9"),
}}
camera={{
position: [1, 0, 2],
fov: 30,
}} // カメラの初期位置と視野角を設定
>
{/* GLBモデルの読み込みと表示 */}
<Character characterSetting={characterSetting} />
</Suspense>
{/* カメラコントロールの追加(ユーザーが自由にカメラを操作できるようにする) */}
<OrbitControls
enablePan={false}
minPolarAngle={(Math.PI / 5) * 2}
maxPolarAngle={(Math.PI / 5) * 2}
maxDistance={3}
minDistance={1}
autoRotate
autoRotateSpeed={2}
/>
{/* アウトラインエフェクト */}
{/* <OutlineRenderer /> */}
</Canvas>
{/* ライトを設定 */}
<ambientLight />
<directionalLight position={[6, 5, 5]} intensity={1} />
{/* ポストプロセッシング */}
<EffectComposer autoClear={false}>
<Bloom intensity={1} luminanceThreshold={1} radius={0.8} mipmapBlur />
</EffectComposer>
<Suspense
fallback={
<Billboard>
<Text
fontSize={0.1}
font="/assets/font.ttf"
characters="読み込み中…"
>
読み込み中…
</Text>
</Billboard>
}
>
{/* GLBモデルの読み込みと表示 */}
<Character characterSetting={characterSetting} />
</Suspense>
{/* カメラコントロールの追加(ユーザーが自由にカメラを操作できるようにする) */}
<OrbitControls
enablePan={false}
minPolarAngle={(Math.PI / 5) * 2}
maxPolarAngle={(Math.PI / 5) * 2}
maxDistance={3}
minDistance={1}
autoRotate
autoRotateSpeed={2}
/>
{/* アウトラインエフェクト */}
{/* <OutlineRenderer /> */}
</Canvas>
</div>
);
}

Expand Down
Loading