Avatar editor works
Before Width: | Height: | Size: 171 B After Width: | Height: | Size: 259 B |
Before Width: | Height: | Size: 242 B After Width: | Height: | Size: 335 B |
Before Width: | Height: | Size: 281 B After Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 265 B After Width: | Height: | Size: 338 B |
Before Width: | Height: | Size: 154 B After Width: | Height: | Size: 228 B |
Before Width: | Height: | Size: 171 B After Width: | Height: | Size: 260 B |
Before Width: | Height: | Size: 234 B After Width: | Height: | Size: 252 B |
Before Width: | Height: | Size: 199 B After Width: | Height: | Size: 280 B |
Before Width: | Height: | Size: 155 B After Width: | Height: | Size: 251 B |
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 298 B |
Before Width: | Height: | Size: 140 B After Width: | Height: | Size: 234 B |
Before Width: | Height: | Size: 195 B After Width: | Height: | Size: 286 B |
Before Width: | Height: | Size: 156 B After Width: | Height: | Size: 241 B |
Before Width: | Height: | Size: 220 B After Width: | Height: | Size: 285 B |
Before Width: | Height: | Size: 173 B After Width: | Height: | Size: 267 B |
Before Width: | Height: | Size: 248 B After Width: | Height: | Size: 338 B |
Before Width: | Height: | Size: 173 B After Width: | Height: | Size: 257 B |
Before Width: | Height: | Size: 238 B After Width: | Height: | Size: 348 B |
Before Width: | Height: | Size: 181 B After Width: | Height: | Size: 222 B |
@ -78,6 +78,8 @@ $mirage: #131e25 !default;
|
||||
$aztec: #0d171d !default;
|
||||
$cello-light: #21516e !default;
|
||||
$cello-dark: #1e465e !default;
|
||||
$pale-sky: #677181 !default;
|
||||
$oslo-gray: #8F9297 !default;
|
||||
|
||||
$success: $green !default;
|
||||
$info: $cyan !default;
|
||||
|
@ -2,15 +2,12 @@
|
||||
line-height: 0 !important;
|
||||
}
|
||||
|
||||
i {
|
||||
|
||||
&.icon {
|
||||
.icon {
|
||||
display: inline-block;
|
||||
outline: 0;
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
|
||||
&.icon-nitro-light {
|
||||
background-image: url('../images/nitro/nitro-n-light.svg');
|
||||
@ -190,6 +187,7 @@ i {
|
||||
background-image: url('../images/avatareditor/ca-icon.png');
|
||||
width: 30px;
|
||||
height: 24px;
|
||||
background
|
||||
|
||||
&.selected {
|
||||
background-image: url('../images/avatareditor/ca-selected-icon.png');
|
||||
@ -297,9 +295,9 @@ i {
|
||||
}
|
||||
|
||||
&.loading-icon {
|
||||
background-image: url('../images/avatareditor/loading-icon.png');
|
||||
width: 21px;
|
||||
height: 25px;
|
||||
background-image: url('../images/icons/loading-icon.png');
|
||||
width: 17px;
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
&.male-icon {
|
||||
@ -500,4 +498,3 @@ i {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.row-cols-4 {
|
||||
|
||||
.col {
|
||||
padding-right: 0.25rem;
|
||||
|
||||
&:nth-child(4n+4) {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row-cols-5 {
|
||||
|
||||
.col {
|
||||
|
@ -1,15 +1,18 @@
|
||||
import { FC } from 'react';
|
||||
import { NitroCardGridViewProps } from './NitroCardGridView.types';
|
||||
import { NitroCardGridContextProvider } from './context/NitroCardGridContext';
|
||||
import { NitroCardGridThemes, NitroCardGridViewProps } from './NitroCardGridView.types';
|
||||
|
||||
export const NitroCardGridView: FC<NitroCardGridViewProps> = props =>
|
||||
{
|
||||
const { columns = 5, children = null } = props;
|
||||
const { columns = 5, theme = NitroCardGridThemes.THEME_DEFAULT, children = null } = props;
|
||||
|
||||
return (
|
||||
<div className="h-100 overflow-hidden nitro-card-grid">
|
||||
<NitroCardGridContextProvider value={ { theme } }>
|
||||
<div className={ `h-100 overflow-hidden nitro-card-grid ${ theme }` }>
|
||||
<div className={ `row row-cols-${ columns } align-content-start g-0 w-100 h-100 overflow-auto` }>
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
</NitroCardGridContextProvider>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,11 @@
|
||||
export interface NitroCardGridViewProps
|
||||
{
|
||||
columns?: number;
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
export class NitroCardGridThemes
|
||||
{
|
||||
public static THEME_DEFAULT: string = 'theme-default';
|
||||
public static THEME_SHADOWED: string = 'theme-shadowed';
|
||||
}
|
||||
|
13
src/layout/card/grid/context/NitroCardGridContext.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { createContext, FC, useContext } from 'react';
|
||||
import { INitroCardGridContext, NitroCardGridContextProps } from './NitroCardGridContext.types';
|
||||
|
||||
const NitroCardGridContext = createContext<INitroCardGridContext>({
|
||||
theme: null
|
||||
});
|
||||
|
||||
export const NitroCardGridContextProvider: FC<NitroCardGridContextProps> = props =>
|
||||
{
|
||||
return <NitroCardGridContext.Provider value={ props.value }>{ props.children }</NitroCardGridContext.Provider>
|
||||
}
|
||||
|
||||
export const useNitroCardGridContext = () => useContext(NitroCardGridContext);
|
11
src/layout/card/grid/context/NitroCardGridContext.types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { ProviderProps } from 'react';
|
||||
|
||||
export interface INitroCardGridContext
|
||||
{
|
||||
theme: string;
|
||||
}
|
||||
|
||||
export interface NitroCardGridContextProps extends ProviderProps<INitroCardGridContext>
|
||||
{
|
||||
|
||||
}
|
2
src/layout/card/grid/context/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './NitroCardGridContext';
|
||||
export * from './NitroCardGridContext.types';
|
@ -1,16 +1,50 @@
|
||||
.grid-item-container {
|
||||
height: 48px;
|
||||
max-height: 48px;
|
||||
height: 50px;
|
||||
max-height: 50px;
|
||||
|
||||
.grid-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-color: $grid-border-color !important;
|
||||
background-color: $grid-bg-color;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
overflow: hidden;
|
||||
|
||||
&.theme-default {
|
||||
border-radius: $border-radius;
|
||||
border-color: $grid-border-color !important;
|
||||
background-color: $grid-bg-color;
|
||||
border: nth(map-values($border-widths), 2) solid;
|
||||
}
|
||||
|
||||
&.theme-shadowed {
|
||||
border-radius: $border-radius;
|
||||
background-color: $light;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: $border-radius;
|
||||
border-bottom: 2px solid white;
|
||||
border-right: 2px solid white;
|
||||
box-shadow: -2px -2px rgba(0, 0, 0, .4), inset 3px 3px rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border: nth(map-values($border-widths), 2) solid;
|
||||
border-color: $oslo-gray !important;
|
||||
background-color: #F5F5F5;
|
||||
|
||||
&:after {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: $grid-active-border-color !important;
|
||||
background-color: $grid-active-bg-color !important;
|
||||
|
@ -1,16 +1,19 @@
|
||||
import { FC } from 'react';
|
||||
import { LimitedEditionStyledNumberView } from '../../../../views/shared/limited-edition/styled-number/LimitedEditionStyledNumberView';
|
||||
import { useNitroCardGridContext } from '../context';
|
||||
import { NitroCardGridThemes } from '../NitroCardGridView.types';
|
||||
import { NitroCardGridItemViewProps } from './NitroCardGridItemView.types';
|
||||
|
||||
export const NitroCardGridItemView: FC<NitroCardGridItemViewProps> = props =>
|
||||
{
|
||||
const { itemImage = null, itemActive = false, itemCount = 1, itemUnique = false, itemUniqueNumber = 0, itemUnseen = false, className = '', style = {}, children = null, ...rest } = props;
|
||||
const { itemImage = undefined, itemColor = undefined, itemActive = false, itemCount = 1, itemUnique = false, itemUniqueNumber = 0, itemUnseen = false, className = '', style = {}, children = null, ...rest } = props;
|
||||
const { theme = NitroCardGridThemes.THEME_DEFAULT } = useNitroCardGridContext();
|
||||
|
||||
const imageUrl = `url(${ itemImage })`;
|
||||
|
||||
return (
|
||||
<div className="col pb-1 grid-item-container">
|
||||
<div className={ `position-relative border border-2 rounded grid-item cursor-pointer${ itemActive ? ' active' : '' }${ itemUnique ? ' unique-item' : '' }${ itemUnseen ? ' unseen' : ''} ${ className || '' }` } style={ itemImage ? { ...style, backgroundImage: imageUrl } : style } { ...rest }>
|
||||
<div className={ `grid-item ${ theme } cursor-pointer${ itemActive ? ' active' : '' }${ itemUnique ? ' unique-item' : '' }${ itemUnseen ? ' unseen' : ''}${ (itemImage === null ? ' icon loading-icon': '')} ${ className || '' }` } style={ itemImage ? { ...style, backgroundImage: imageUrl } : (itemColor ? { ...style, backgroundColor: itemColor } : style) } { ...rest }>
|
||||
{ (itemCount > 1) &&
|
||||
<span className="position-absolute badge border bg-danger px-1 rounded-circle">{ itemCount }</span> }
|
||||
{ itemUnique &&
|
||||
|
@ -3,6 +3,7 @@ import { DetailsHTMLAttributes } from 'react';
|
||||
export interface NitroCardGridItemViewProps extends DetailsHTMLAttributes<HTMLDivElement>
|
||||
{
|
||||
itemImage?: string;
|
||||
itemColor?: string;
|
||||
itemActive?: boolean;
|
||||
itemCount?: number;
|
||||
itemUnique?: boolean;
|
||||
|
@ -1,8 +1,51 @@
|
||||
.nitro-avatar-editor {
|
||||
width: 550px;
|
||||
width: 600px;
|
||||
|
||||
.content-area {
|
||||
height: 300px;
|
||||
max-height: 300px;
|
||||
height: 330px;
|
||||
max-height: 330px;
|
||||
}
|
||||
|
||||
.figure-preview-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-color: $pale-sky;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
|
||||
.avatar-image {
|
||||
margin: 45px auto 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.arrow-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
bottom: 12px;
|
||||
z-index: 3;
|
||||
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.arrow-left-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 75%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 50%;
|
||||
background-color: red;
|
||||
transform: scale(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { AvatarEditorFigureCategory } from 'nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useReducer, useState } from 'react';
|
||||
import { GetSessionDataManager } from '../../api';
|
||||
import { AvatarEditorEvent } from '../../events/avatar-editor';
|
||||
import { useUiEvent } from '../../hooks/events/ui/ui-event';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../layout';
|
||||
@ -7,6 +8,7 @@ import { LocalizeText } from '../../utils/LocalizeText';
|
||||
import { AvatarEditorViewProps } from './AvatarEditorView.types';
|
||||
import { AvatarEditor } from './common/AvatarEditor';
|
||||
import { BodyModel } from './common/BodyModel';
|
||||
import { FigureData } from './common/FigureData';
|
||||
import { HeadModel } from './common/HeadModel';
|
||||
import { IAvatarEditorCategoryModel } from './common/IAvatarEditorCategoryModel';
|
||||
import { LegModel } from './common/LegModel';
|
||||
@ -24,6 +26,79 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
|
||||
const [ activeCategory, setActiveCategory ] = useState<IAvatarEditorCategoryModel>(null);
|
||||
const [ isInitalized, setIsInitalized ] = useState(false);
|
||||
|
||||
const selectCategory = useCallback((name: string) =>
|
||||
{
|
||||
setActiveCategory(categories.get(name));
|
||||
}, [ categories ]);
|
||||
|
||||
const resetCategories = useCallback((editor: AvatarEditor) =>
|
||||
{
|
||||
const categories = new Map();
|
||||
|
||||
categories.set(AvatarEditorFigureCategory.GENERIC, new BodyModel(editor));
|
||||
categories.set(AvatarEditorFigureCategory.HEAD, new HeadModel(editor));
|
||||
categories.set(AvatarEditorFigureCategory.TORSO, new TorsoModel(editor));
|
||||
categories.set(AvatarEditorFigureCategory.LEGS, new LegModel(editor));
|
||||
|
||||
setCategories(categories);
|
||||
setActiveCategory(categories.get(AvatarEditorFigureCategory.GENERIC));
|
||||
}, []);
|
||||
|
||||
const selectGender = useCallback((gender: string) =>
|
||||
{
|
||||
if(gender === avatarEditor.gender) return;
|
||||
|
||||
avatarEditor.gender = gender;
|
||||
|
||||
resetCategories(avatarEditor);
|
||||
}, [ avatarEditor, resetCategories ]);
|
||||
|
||||
const loadAvatarInEditor = useCallback((figure: string, gender: string, reset: boolean = true) =>
|
||||
{
|
||||
if(!avatarEditor) return;
|
||||
|
||||
switch(gender)
|
||||
{
|
||||
case FigureData.MALE:
|
||||
case 'm':
|
||||
case 'M':
|
||||
gender = FigureData.MALE;
|
||||
break;
|
||||
case FigureData.FEMALE:
|
||||
case 'f':
|
||||
case 'F':
|
||||
gender = FigureData.FEMALE;
|
||||
break;
|
||||
default:
|
||||
gender = FigureData.MALE;
|
||||
}
|
||||
|
||||
let update = false;
|
||||
|
||||
if(gender !== avatarEditor.gender)
|
||||
{
|
||||
avatarEditor.gender = gender;
|
||||
|
||||
update = true;
|
||||
}
|
||||
|
||||
const figureData = avatarEditor.figureData;
|
||||
|
||||
if(!figureData) return;
|
||||
|
||||
if(figure !== figureData.getFigureString())
|
||||
{
|
||||
update = true;
|
||||
}
|
||||
|
||||
figureData.loadAvatarData(figure, gender);
|
||||
|
||||
if(update)
|
||||
{
|
||||
resetCategories(avatarEditor);
|
||||
}
|
||||
}, [ avatarEditor, resetCategories ]);
|
||||
|
||||
const onAvatarEditorEvent = useCallback((event: AvatarEditorEvent) =>
|
||||
{
|
||||
switch(event.type)
|
||||
@ -35,7 +110,7 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
|
||||
setIsVisible(false);
|
||||
return;
|
||||
case AvatarEditorEvent.TOGGLE_EDITOR:
|
||||
setIsVisible(value => !value);
|
||||
setIsVisible(prevValue => !prevValue);
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
@ -44,11 +119,6 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
|
||||
useUiEvent(AvatarEditorEvent.HIDE_EDITOR, onAvatarEditorEvent);
|
||||
useUiEvent(AvatarEditorEvent.TOGGLE_EDITOR, onAvatarEditorEvent);
|
||||
|
||||
const selectCategory = useCallback((name: string) =>
|
||||
{
|
||||
setActiveCategory(categories.get(name));
|
||||
}, [ categories ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible || isInitalized) return;
|
||||
@ -56,26 +126,23 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
|
||||
const newEditor = new AvatarEditor();
|
||||
|
||||
setAvatarEditor(newEditor);
|
||||
|
||||
const categories = new Map();
|
||||
|
||||
categories.set(AvatarEditorFigureCategory.GENERIC, new BodyModel(newEditor));
|
||||
categories.set(AvatarEditorFigureCategory.HEAD, new HeadModel(newEditor));
|
||||
categories.set(AvatarEditorFigureCategory.TORSO, new TorsoModel(newEditor));
|
||||
categories.set(AvatarEditorFigureCategory.LEGS, new LegModel(newEditor));
|
||||
|
||||
setCategories(categories);
|
||||
setActiveCategory(categories.get(AvatarEditorFigureCategory.GENERIC));
|
||||
setIsInitalized(true);
|
||||
}, [ isVisible, isInitalized ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible || !avatarEditor) return;
|
||||
|
||||
loadAvatarInEditor(GetSessionDataManager().figure, GetSessionDataManager().gender);
|
||||
}, [ isVisible, avatarEditor, loadAvatarInEditor ]);
|
||||
|
||||
return (
|
||||
<AvatarEditorContextProvider value={ { avatarEditorState, dispatchAvatarEditorState } }>
|
||||
{ isVisible &&
|
||||
<NitroCardView className="nitro-avatar-editor">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('avatareditor.title') } onCloseClick={ event => setIsVisible(false) } />
|
||||
<NitroCardTabsView>
|
||||
{ categories && Array.from(categories.keys()).map(category =>
|
||||
{ categories && (categories.size > 0) && Array.from(categories.keys()).map(category =>
|
||||
{
|
||||
return (
|
||||
<NitroCardTabsItemView key={ category } isActive={ (activeCategory.name === category) } onClick={ event => selectCategory(category) }>
|
||||
@ -85,7 +152,7 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
|
||||
})}
|
||||
</NitroCardTabsView>
|
||||
<NitroCardContentView>
|
||||
{ activeCategory && <AvatarEditorModelView model={ activeCategory } editor={ avatarEditor } /> }
|
||||
{ activeCategory && <AvatarEditorModelView model={ activeCategory } editor={ avatarEditor } selectGender={ selectGender } /> }
|
||||
</NitroCardContentView>
|
||||
</NitroCardView> }
|
||||
</AvatarEditorContextProvider>
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { FigureData, IPalette, IPartColor, ISetType, IStructureData } from 'nitro-renderer';
|
||||
import { IPalette, IPartColor, ISetType, IStructureData } from 'nitro-renderer';
|
||||
import { GetAvatarRenderManager, GetConfiguration, GetSessionDataManager } from '../../../api';
|
||||
import { AvatarEditorGridColorItem } from './AvatarEditorGridColorItem';
|
||||
import { AvatarEditorGridPartItem } from './AvatarEditorGridPartItem';
|
||||
import { CategoryBaseModel } from './CategoryBaseModel';
|
||||
import { CategoryData } from './CategoryData';
|
||||
import { FigureData } from './FigureData';
|
||||
|
||||
const MAX_PALETTES: number = 2;
|
||||
const DEFAULT_MALE_FIGURE: string = 'hr-100.hd-180-7.ch-215-66.lg-270-79.sh-305-62.ha-1002-70.wa-2007';
|
||||
@ -11,16 +12,13 @@ const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62
|
||||
|
||||
export class AvatarEditor
|
||||
{
|
||||
private _figureStructureData: IStructureData;
|
||||
private _figures: Map<string, FigureData>;
|
||||
private _gender: string;
|
||||
private _figureStructureData: IStructureData = GetAvatarRenderManager().structureData;
|
||||
private _figures: Map<string, FigureData> = new Map();
|
||||
private _gender: string = FigureData.MALE;
|
||||
private _notifier: () => void = null;
|
||||
|
||||
constructor()
|
||||
{
|
||||
this._figureStructureData = GetAvatarRenderManager().structureData;
|
||||
this._figures = new Map();
|
||||
this._gender = FigureData.MALE;
|
||||
|
||||
const maleFigure = new FigureData();
|
||||
const femaleFigure = new FigureData();
|
||||
|
||||
@ -160,6 +158,7 @@ export class AvatarEditor
|
||||
|
||||
if(partSet.isSellable)
|
||||
{
|
||||
isValid = false;
|
||||
//isValid = (this._inventoryService && this._inventoryService.hasFigureSetId(partSet.id));
|
||||
}
|
||||
|
||||
@ -284,6 +283,24 @@ export class AvatarEditor
|
||||
|
||||
public set gender(gender: string)
|
||||
{
|
||||
if(this._gender === gender) return;
|
||||
|
||||
this._gender = gender;
|
||||
|
||||
if(this.figureData) this.figureData.notify = this.notify;
|
||||
|
||||
if(this.notify) this.notify();
|
||||
}
|
||||
|
||||
public get notify(): () => void
|
||||
{
|
||||
return this._notifier;
|
||||
}
|
||||
|
||||
public set notify(notifier: () => void)
|
||||
{
|
||||
if(this.figureData) this.figureData.notify = notifier;
|
||||
|
||||
this._notifier = notifier;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ export class AvatarEditorGridColorItem
|
||||
private _isDisabled: boolean;
|
||||
private _isHC: boolean;
|
||||
private _isSelected: boolean;
|
||||
private _notifier: () => void;
|
||||
|
||||
constructor(partColor: IPartColor, isDisabled: boolean = false)
|
||||
{
|
||||
@ -48,5 +49,17 @@ export class AvatarEditorGridColorItem
|
||||
public set isSelected(flag: boolean)
|
||||
{
|
||||
this._isSelected = flag;
|
||||
|
||||
if(this.notify) this.notify();
|
||||
}
|
||||
|
||||
public get notify(): () => void
|
||||
{
|
||||
return this._notifier;
|
||||
}
|
||||
|
||||
public set notify(notifier: () => void)
|
||||
{
|
||||
this._notifier = notifier;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { AvatarFigurePartType, FigureData, IAvatarImageListener, IAvatarRenderManager, IFigurePart, IFigurePartSet, IGraphicAsset, IPartColor, NitroContainer, NitroSprite, TextureUtils } from 'nitro-renderer';
|
||||
import { AvatarFigurePartType, IAvatarImageListener, IAvatarRenderManager, IFigurePart, IFigurePartSet, IGraphicAsset, IPartColor, NitroContainer, NitroSprite, TextureUtils } from 'nitro-renderer';
|
||||
import { GetAvatarRenderManager } from '../../../api';
|
||||
import { FigureData } from './FigureData';
|
||||
|
||||
export class AvatarEditorGridPartItem implements IAvatarImageListener
|
||||
{
|
||||
@ -47,6 +48,7 @@ export class AvatarEditorGridPartItem implements IAvatarImageListener
|
||||
private _isSelected: boolean;
|
||||
private _disposed: boolean;
|
||||
private _isInitalized: boolean;
|
||||
private _notifier: () => void;
|
||||
|
||||
constructor(partSet: IFigurePartSet, partColors: IPartColor[], useColors: boolean = true, isDisabled: boolean = false)
|
||||
{
|
||||
@ -210,6 +212,8 @@ export class AvatarEditorGridPartItem implements IAvatarImageListener
|
||||
if(this._isDisabled) this.setAlpha(container, 0.2);
|
||||
|
||||
this._imageUrl = TextureUtils.generateImageUrl(container);
|
||||
|
||||
if(this.notify) this.notify();
|
||||
}
|
||||
|
||||
private setAlpha(container: NitroContainer, alpha: number): NitroContainer
|
||||
@ -259,21 +263,21 @@ export class AvatarEditorGridPartItem implements IAvatarImageListener
|
||||
return this._partSet;
|
||||
}
|
||||
|
||||
public set colors(partColors: IPartColor[])
|
||||
public set partColors(partColors: IPartColor[])
|
||||
{
|
||||
this._partColors = partColors;
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
public get isDisabledForWearing(): boolean
|
||||
public get isDisabled(): boolean
|
||||
{
|
||||
return this._isDisabled;
|
||||
}
|
||||
|
||||
public set iconImage(k: NitroContainer)
|
||||
public set thumbContainer(container: NitroContainer)
|
||||
{
|
||||
this._thumbContainer = k;
|
||||
this._thumbContainer = container;
|
||||
|
||||
this.update();
|
||||
}
|
||||
@ -283,7 +287,7 @@ export class AvatarEditorGridPartItem implements IAvatarImageListener
|
||||
return this._imageUrl;
|
||||
}
|
||||
|
||||
public get colorLayerCount(): number
|
||||
public get maxColorIndex(): number
|
||||
{
|
||||
return this._maxColorIndex;
|
||||
}
|
||||
@ -316,5 +320,17 @@ export class AvatarEditorGridPartItem implements IAvatarImageListener
|
||||
public set isSelected(flag: boolean)
|
||||
{
|
||||
this._isSelected = flag;
|
||||
|
||||
if(this.notify) this.notify();
|
||||
}
|
||||
|
||||
public get notify(): () => void
|
||||
{
|
||||
return this._notifier;
|
||||
}
|
||||
|
||||
public set notify(notifier: () => void)
|
||||
{
|
||||
this._notifier = notifier;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { AvatarEditorFigureCategory, AvatarScaleType, AvatarSetType, FigureData, IAvatarImageListener } from 'nitro-renderer';
|
||||
import { AvatarEditorFigureCategory, AvatarScaleType, AvatarSetType, IAvatarImageListener } from 'nitro-renderer';
|
||||
import { GetAvatarRenderManager } from '../../../api';
|
||||
import { CategoryBaseModel } from './CategoryBaseModel';
|
||||
import { FigureData } from './FigureData';
|
||||
|
||||
export class BodyModel extends CategoryBaseModel implements IAvatarImageListener
|
||||
{
|
||||
@ -50,7 +51,7 @@ export class BodyModel extends CategoryBaseModel implements IAvatarImageListener
|
||||
{
|
||||
sprite.y = 10;
|
||||
|
||||
part.iconImage = sprite;
|
||||
part.thumbContainer = sprite;
|
||||
|
||||
setTimeout(() => avatarImage.dispose(), 0);
|
||||
}
|
||||
|
@ -174,7 +174,7 @@ export class CategoryBaseModel implements IAvatarEditorCategoryModel
|
||||
|
||||
if(!partItem) return;
|
||||
|
||||
if(partItem.isDisabledForWearing)
|
||||
if(partItem.isDisabled)
|
||||
{
|
||||
categoryData.selectPartIndex(selectedPartIndex);
|
||||
|
||||
@ -183,7 +183,7 @@ export class CategoryBaseModel implements IAvatarEditorCategoryModel
|
||||
return;
|
||||
}
|
||||
|
||||
this._maxPaletteCount = partItem.colorLayerCount;
|
||||
this._maxPaletteCount = partItem.maxColorIndex;
|
||||
|
||||
this._editor.figureData.savePartData(category, partItem.id, categoryData.getSelectedColorIds(), true);
|
||||
}
|
||||
@ -233,7 +233,7 @@ export class CategoryBaseModel implements IAvatarEditorCategoryModel
|
||||
|
||||
public get maxPaletteCount(): number
|
||||
{
|
||||
return this._maxPaletteCount;
|
||||
return (this._maxPaletteCount || 1);
|
||||
}
|
||||
|
||||
public set maxPaletteCount(count: number)
|
||||
|
@ -266,7 +266,7 @@ export class CategoryData
|
||||
|
||||
if(!partItem) return null;
|
||||
|
||||
return colorIds.slice(0, Math.max(partItem.colorLayerCount, 1));
|
||||
return colorIds.slice(0, Math.max(partItem.maxColorIndex, 1));
|
||||
}
|
||||
|
||||
private getSelectedColors(): IPartColor[]
|
||||
@ -333,7 +333,7 @@ export class CategoryData
|
||||
|
||||
for(const partItem of this._parts)
|
||||
{
|
||||
if(partItem) partItem.colors = partColors;
|
||||
if(partItem) partItem.partColors = partColors;
|
||||
}
|
||||
}
|
||||
|
||||
|
271
src/views/avatar-editor/common/FigureData.ts
Normal file
@ -0,0 +1,271 @@
|
||||
export class FigureData
|
||||
{
|
||||
public static MALE: string = 'M';
|
||||
public static FEMALE: string = 'F';
|
||||
public static UNISEX: string = 'U';
|
||||
public static SCALE: string = 'h';
|
||||
public static STD: string = 'std';
|
||||
public static DEFAULT_FRAME: string = '0';
|
||||
public static FACE: string = 'hd';
|
||||
public static HAIR: string = 'hr';
|
||||
public static HAT: string = 'ha';
|
||||
public static HEAD_ACCESSORIES: string = 'he';
|
||||
public static EYE_ACCESSORIES: string = 'ea';
|
||||
public static FACE_ACCESSORIES: string = 'fa';
|
||||
public static JACKET: string = 'cc';
|
||||
public static SHIRT: string = 'ch';
|
||||
public static CHEST_ACCESSORIES: string = 'ca';
|
||||
public static CHEST_PRINTS: string = 'cp';
|
||||
public static TROUSERS: string = 'lg';
|
||||
public static SHOES: string = 'sh';
|
||||
public static TROUSER_ACCESSORIES: string = 'wa';
|
||||
public static PREVIEW_AVATAR_DIRECTION: number = 4;
|
||||
|
||||
private _data: Map<string, number>;
|
||||
private _colors: Map<string, number[]>;
|
||||
private _gender: string = 'M';
|
||||
private _avatarEffectType: number = -1;
|
||||
private _notifier: () => void = null;
|
||||
|
||||
public loadAvatarData(figureString: string, gender: string): void
|
||||
{
|
||||
this._data = new Map();
|
||||
this._colors = new Map();
|
||||
this._gender = gender;
|
||||
|
||||
this.parseFigureString(figureString);
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
private parseFigureString(figure: string): void
|
||||
{
|
||||
if(!figure) return;
|
||||
|
||||
const sets = figure.split('.');
|
||||
|
||||
if(!sets || !sets.length) return;
|
||||
|
||||
for(const set of sets)
|
||||
{
|
||||
const parts = set.split('-');
|
||||
|
||||
if(!parts.length) continue;
|
||||
|
||||
const setType = parts[0];
|
||||
const setId = parseInt(parts[1]);
|
||||
const colorIds: number[] = [];
|
||||
|
||||
let offset = 2;
|
||||
|
||||
while(offset < parts.length)
|
||||
{
|
||||
colorIds.push(parseInt(parts[offset]));
|
||||
|
||||
offset++;
|
||||
}
|
||||
|
||||
if(!colorIds.length) colorIds.push(0);
|
||||
|
||||
this.savePartSetId(setType, setId, false);
|
||||
this.savePartSetColourId(setType, colorIds, false);
|
||||
}
|
||||
}
|
||||
|
||||
public getPartSetId(partSetId: string): number
|
||||
{
|
||||
const existing = this._data.get(partSetId);
|
||||
|
||||
if(existing !== undefined) return existing;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public getColourIds(colorId: string): number[]
|
||||
{
|
||||
const existing = this._colors.get(colorId);
|
||||
|
||||
if(existing !== undefined) return existing;
|
||||
|
||||
return [];
|
||||
// return [this._avatarEditor._Str_24919(k)];
|
||||
}
|
||||
|
||||
public getFigureString(): string
|
||||
{
|
||||
let figureString = '';
|
||||
const setParts: string[] = [];
|
||||
|
||||
for(const [ setType, setId ] of this._data.entries())
|
||||
{
|
||||
const colorIds = this._colors.get(setType);
|
||||
|
||||
let setPart = ((setType + '-') + setId);
|
||||
|
||||
if(colorIds && colorIds.length)
|
||||
{
|
||||
let i = 0;
|
||||
|
||||
while(i < colorIds.length)
|
||||
{
|
||||
setPart = (setPart + ('-' + colorIds[i]));
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
setParts.push(setPart);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
|
||||
while(i < setParts.length)
|
||||
{
|
||||
figureString = (figureString + setParts[i]);
|
||||
|
||||
if(i < (setParts.length - 1)) figureString = (figureString + '.');
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return figureString;
|
||||
}
|
||||
|
||||
public savePartData(k: string, _arg_2: number, _arg_3: number[], _arg_4: boolean = false): void
|
||||
{
|
||||
this.savePartSetId(k, _arg_2, _arg_4);
|
||||
this.savePartSetColourId(k, _arg_3, _arg_4);
|
||||
}
|
||||
|
||||
private savePartSetId(k: string, _arg_2: number, _arg_3: boolean = true): void
|
||||
{
|
||||
switch(k)
|
||||
{
|
||||
case FigureData.FACE:
|
||||
case FigureData.HAIR:
|
||||
case FigureData.HAT:
|
||||
case FigureData.HEAD_ACCESSORIES:
|
||||
case FigureData.EYE_ACCESSORIES:
|
||||
case FigureData.FACE_ACCESSORIES:
|
||||
case FigureData.SHIRT:
|
||||
case FigureData.JACKET:
|
||||
case FigureData.CHEST_ACCESSORIES:
|
||||
case FigureData.CHEST_PRINTS:
|
||||
case FigureData.TROUSERS:
|
||||
case FigureData.SHOES:
|
||||
case FigureData.TROUSER_ACCESSORIES:
|
||||
if(_arg_2 >= 0)
|
||||
{
|
||||
this._data.set(k, _arg_2);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._data.delete(k);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if(_arg_3) this.updateView();
|
||||
}
|
||||
|
||||
public savePartSetColourId(k: string, _arg_2: number[], _arg_3: boolean = true): void
|
||||
{
|
||||
switch(k)
|
||||
{
|
||||
case FigureData.FACE:
|
||||
case FigureData.HAIR:
|
||||
case FigureData.HAT:
|
||||
case FigureData.HEAD_ACCESSORIES:
|
||||
case FigureData.EYE_ACCESSORIES:
|
||||
case FigureData.FACE_ACCESSORIES:
|
||||
case FigureData.SHIRT:
|
||||
case FigureData.JACKET:
|
||||
case FigureData.CHEST_ACCESSORIES:
|
||||
case FigureData.CHEST_PRINTS:
|
||||
case FigureData.TROUSERS:
|
||||
case FigureData.SHOES:
|
||||
case FigureData.TROUSER_ACCESSORIES:
|
||||
this._colors.set(k, _arg_2);
|
||||
break;
|
||||
}
|
||||
|
||||
if(_arg_3) this.updateView();
|
||||
}
|
||||
|
||||
public getFigureStringWithFace(k: number, override = true): string
|
||||
{
|
||||
let figureString = '';
|
||||
|
||||
const setTypes: string[] = [ FigureData.FACE ];
|
||||
const figureSets: string[] = [];
|
||||
|
||||
for(const setType of setTypes)
|
||||
{
|
||||
const colors = this._colors.get(setType);
|
||||
|
||||
if(colors === undefined) continue;
|
||||
|
||||
let setId = this._data.get(setType);
|
||||
|
||||
if((setType === FigureData.FACE) && override) setId = k;
|
||||
|
||||
let figureSet = ((setType + '-') + setId);
|
||||
|
||||
if(setId >= 0)
|
||||
{
|
||||
let i = 0;
|
||||
|
||||
while(i < colors.length)
|
||||
{
|
||||
figureSet = (figureSet + ('-' + colors[i]));
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
figureSets.push(figureSet);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
|
||||
while(i < figureSets.length)
|
||||
{
|
||||
figureString = (figureString + figureSets[i]);
|
||||
|
||||
if(i < (figureSets.length - 1)) figureString = (figureString + '.');
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return figureString;
|
||||
}
|
||||
|
||||
public updateView(): void
|
||||
{
|
||||
if(this.notify) this.notify();
|
||||
}
|
||||
|
||||
public get gender(): string
|
||||
{
|
||||
return this._gender;
|
||||
}
|
||||
|
||||
public set avatarEffectType(k: number)
|
||||
{
|
||||
this._avatarEffectType = k;
|
||||
}
|
||||
|
||||
public get avatarEffectType(): number
|
||||
{
|
||||
return this._avatarEffectType;
|
||||
}
|
||||
|
||||
public get notify(): () => void
|
||||
{
|
||||
return this._notifier;
|
||||
}
|
||||
|
||||
public set notify(notifier: () => void)
|
||||
{
|
||||
this._notifier = notifier;
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { AvatarEditorFigureCategory, FigureData } from 'nitro-renderer';
|
||||
import { AvatarEditorFigureCategory } from 'nitro-renderer';
|
||||
import { CategoryBaseModel } from './CategoryBaseModel';
|
||||
import { FigureData } from './FigureData';
|
||||
|
||||
export class HeadModel extends CategoryBaseModel
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { AvatarEditorFigureCategory, FigureData } from 'nitro-renderer';
|
||||
import { AvatarEditorFigureCategory } from 'nitro-renderer';
|
||||
import { CategoryBaseModel } from './CategoryBaseModel';
|
||||
import { FigureData } from './FigureData';
|
||||
|
||||
export class LegModel extends CategoryBaseModel
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { AvatarEditorFigureCategory, FigureData } from 'nitro-renderer';
|
||||
import { AvatarEditorFigureCategory } from 'nitro-renderer';
|
||||
import { CategoryBaseModel } from './CategoryBaseModel';
|
||||
import { FigureData } from './FigureData';
|
||||
|
||||
export class TorsoModel extends CategoryBaseModel
|
||||
{
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView';
|
||||
import { AvatarEditorFigurePreviewViewProps } from './AvatarEditorFigurePreviewView.types';
|
||||
|
||||
export const AvatarEditorFigurePreviewView: FC<AvatarEditorFigurePreviewViewProps> = props =>
|
||||
{
|
||||
const { editor = null } = props;
|
||||
const [ updateId, setUpdateId ] = useState(-1);
|
||||
|
||||
const rerender = useCallback(() =>
|
||||
{
|
||||
setUpdateId(prevValue => (prevValue + 1));
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!editor) return;
|
||||
|
||||
editor.notify = rerender;
|
||||
|
||||
return () =>
|
||||
{
|
||||
editor.notify = null;
|
||||
}
|
||||
}, [ editor, rerender ] );
|
||||
|
||||
return <AvatarImageView figure={ editor.figureData.getFigureString() } direction={ 4 } scale={ 2 } />
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { AvatarEditor } from '../../common/AvatarEditor';
|
||||
|
||||
export interface AvatarEditorFigurePreviewViewProps
|
||||
{
|
||||
editor: AvatarEditor;
|
||||
}
|
@ -1,16 +1,31 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { NitroCardGridItemView } from '../../../../layout/card/grid/item/NitroCardGridItemView';
|
||||
import { CurrencyIcon } from '../../../shared/currency-icon/CurrencyIcon';
|
||||
import { AvatarEditorFigureSetItemViewProps } from './AvatarEditorFigureSetItemView.types';
|
||||
|
||||
export const AvatarEditorFigureSetItemView: FC<AvatarEditorFigureSetItemViewProps> = props =>
|
||||
{
|
||||
const { partItem = null, onClick = null } = props;
|
||||
const [ imageUrl, setImageUrl ] = useState<string>(null);
|
||||
const [ updateId, setUpdateId ] = useState(-1);
|
||||
|
||||
const rerender = useCallback(() =>
|
||||
{
|
||||
setUpdateId(prevValue => (prevValue + 1));
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setImageUrl(partItem.imageUrl);
|
||||
}, [ partItem.imageUrl ]);
|
||||
partItem.notify = rerender;
|
||||
|
||||
return <NitroCardGridItemView itemImage={ imageUrl } onClick={ () => onClick(partItem) } />
|
||||
return () =>
|
||||
{
|
||||
partItem.notify = null;
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<NitroCardGridItemView itemImage={ partItem.imageUrl } itemActive={ partItem.isSelected } onClick={ () => onClick(partItem) }>
|
||||
{ partItem.isHC && <CurrencyIcon type={ 'hc' } /> }
|
||||
</NitroCardGridItemView>
|
||||
);
|
||||
}
|
||||
|
@ -1,28 +1,33 @@
|
||||
import { FC, useCallback } from 'react';
|
||||
import { NitroCardGridView } from '../../../../layout/card/grid/NitroCardGridView';
|
||||
import { NitroCardGridThemes } from '../../../../layout/card/grid/NitroCardGridView.types';
|
||||
import { AvatarEditorGridPartItem } from '../../common/AvatarEditorGridPartItem';
|
||||
import { AvatarEditorFigureSetItemView } from '../figure-set-item/AvatarEditorFigureSetItemView';
|
||||
import { AvatarEditorFigureSetViewProps } from './AvatarEditorFigureSetView.types';
|
||||
|
||||
export const AvatarEditorFigureSetView: FC<AvatarEditorFigureSetViewProps> = props =>
|
||||
{
|
||||
const { model = null, category = null } = props;
|
||||
const { model = null, category = null, setMaxPaletteCount = null } = props;
|
||||
|
||||
const selectPart = useCallback((part: AvatarEditorGridPartItem) =>
|
||||
const selectPart = useCallback((item: AvatarEditorGridPartItem) =>
|
||||
{
|
||||
const index = category.parts.indexOf(part);
|
||||
const index = category.parts.indexOf(item);
|
||||
|
||||
if(index === -1) return;
|
||||
|
||||
model.selectPart(category.name, index);
|
||||
}, [ model, category ]);
|
||||
|
||||
const partItem = category.getCurrentPart();
|
||||
|
||||
setMaxPaletteCount(partItem.maxColorIndex || 1);
|
||||
}, [ model, category, setMaxPaletteCount ]);
|
||||
|
||||
return (
|
||||
<NitroCardGridView columns={ 3 }>
|
||||
<NitroCardGridView columns={ 3 } theme={ NitroCardGridThemes.THEME_SHADOWED }>
|
||||
{ (category.parts.length > 0) && category.parts.map((item, index) =>
|
||||
{
|
||||
return <AvatarEditorFigureSetItemView key={ index } partItem={ item } onClick={ selectPart } />;
|
||||
}) }
|
||||
</NitroCardGridView>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { CategoryData } from '../../common/CategoryData';
|
||||
import { IAvatarEditorCategoryModel } from '../../common/IAvatarEditorCategoryModel';
|
||||
|
||||
@ -5,4 +6,5 @@ export interface AvatarEditorFigureSetViewProps
|
||||
{
|
||||
model: IAvatarEditorCategoryModel;
|
||||
category: CategoryData;
|
||||
setMaxPaletteCount: Dispatch<SetStateAction<number>>;
|
||||
}
|
||||
|
@ -1,17 +1,19 @@
|
||||
import { UserFigureComposer } from 'nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { SendMessageHook } from '../../../../hooks';
|
||||
import { LocalizeText } from '../../../../utils/LocalizeText';
|
||||
import { CategoryData } from '../../common/CategoryData';
|
||||
import { FigureData } from '../../common/FigureData';
|
||||
import { AvatarEditorFigurePreviewView } from '../figure-preview/AvatarEditorFigurePreviewView';
|
||||
import { AvatarEditorFigureSetView } from '../figure-set/AvatarEditorFigureSetView';
|
||||
import { AvatarEditorPaletteSetView } from '../palette-set/AvatarEditorPaletteSetView';
|
||||
import { AvatarEditorModelViewProps } from './AvatarEditorModelView.types';
|
||||
|
||||
export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
|
||||
{
|
||||
const { model = null, editor = null } = props;
|
||||
const { model = null, editor = null, selectGender = null } = props;
|
||||
const [ activeCategory, setActiveCategory ] = useState<CategoryData>(null);
|
||||
|
||||
const selectGender = useCallback((gender: string) =>
|
||||
{
|
||||
editor.gender = gender;
|
||||
}, [ editor ]);
|
||||
const [ maxPaletteCount, setMaxPaletteCount ] = useState(1);
|
||||
|
||||
const selectCategory = useCallback((name: string) =>
|
||||
{
|
||||
@ -27,12 +29,19 @@ export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
|
||||
{
|
||||
if(!part || !part.isSelected) continue;
|
||||
|
||||
model.maxPaletteCount = part.colorLayerCount;
|
||||
setMaxPaletteCount(part.maxColorIndex || 1);
|
||||
|
||||
break;
|
||||
}
|
||||
}, [ model ]);
|
||||
|
||||
const saveFigure = useCallback(() =>
|
||||
{
|
||||
const figureData = editor.figureData;
|
||||
|
||||
SendMessageHook(new UserFigureComposer(figureData.gender, figureData.getFigureString()));
|
||||
}, [ editor ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
model.init();
|
||||
@ -45,16 +54,54 @@ export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
|
||||
}
|
||||
}, [ model, selectCategory ]);
|
||||
|
||||
if(!activeCategory) return null;
|
||||
if(!model || !activeCategory) return null;
|
||||
|
||||
return (
|
||||
<div className="row h-100">
|
||||
<div className="col-2 d-flex flex-column h-100"></div>
|
||||
<div className="col-3 d-flex flex-column h-100">
|
||||
<AvatarEditorFigureSetView model={ model } category={ activeCategory } />
|
||||
<div className="col-1 d-flex flex-column align-items-center h-100 pe-0">
|
||||
{ model.canSetGender &&
|
||||
<>
|
||||
<i className={ `icon male-icon ${ (editor.gender === FigureData.MALE) ? ' selected' : ''}` } onClick={ event => selectGender(FigureData.MALE) } />
|
||||
<i className={ `icon female-icon ${ (editor.gender === FigureData.FEMALE) ? ' selected' : ''}` } onClick={ event => selectGender(FigureData.FEMALE) } />
|
||||
</> }
|
||||
{ !model.canSetGender && model.categories && (model.categories.size > 0) && Array.from(model.categories.keys()).map(name =>
|
||||
{
|
||||
const category = model.categories.get(name);
|
||||
|
||||
return (
|
||||
<i className={ `icon ${ category.name }-icon mb-2 ${ (activeCategory === category) ? ' selected' : ''}` } onClick={ event => selectCategory(name) } />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="col-4 d-flex flex-column h-100">
|
||||
<AvatarEditorFigureSetView model={ model } category={ activeCategory } setMaxPaletteCount={ setMaxPaletteCount } />
|
||||
</div>
|
||||
<div className="col-3 d-flex flex-column h-100">
|
||||
<div className="figure-preview-container mb-2">
|
||||
<AvatarEditorFigurePreviewView editor={ editor } />
|
||||
<div className="arrow-container">
|
||||
<i className="icon arrow-left-icon" />
|
||||
<i className="icon arrow-right-icon" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column">
|
||||
<div className="btn-group mb-1">
|
||||
<button type="button" className="btn btn-sm btn-secondary">
|
||||
<i className="fas fa-undo" />
|
||||
</button>
|
||||
<button type="button" className="btn btn-sm btn-secondary">
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" className="btn btn-success btn-sm w-100" onClick={ saveFigure }>{ LocalizeText('avatareditor.save') }</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-4 d-flex flex-column h-100">
|
||||
{ (maxPaletteCount >= 1) &&
|
||||
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(0) } paletteIndex={ 0 } /> }
|
||||
{ (maxPaletteCount === 2) &&
|
||||
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(1) } paletteIndex={ 1 } /> }
|
||||
</div>
|
||||
<div className="col-3 d-flex flex-column h-100"></div>
|
||||
<div className="col-4 d-flex flex-column h-100"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -5,4 +5,5 @@ export interface AvatarEditorModelViewProps
|
||||
{
|
||||
model: IAvatarEditorCategoryModel;
|
||||
editor: AvatarEditor;
|
||||
selectGender: (gender: string) => void;
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { NitroCardGridItemView } from '../../../../layout/card/grid/item/NitroCardGridItemView';
|
||||
import { AvatarEditorPaletteSetItemProps } from './AvatarEditorPaletteSetItem.types';
|
||||
|
||||
export const AvatarEditorPaletteSetItem: FC<AvatarEditorPaletteSetItemProps> = props =>
|
||||
{
|
||||
const { colorItem = null, onClick = null } = props;
|
||||
const [ updateId, setUpdateId ] = useState(-1);
|
||||
|
||||
const rerender = useCallback(() =>
|
||||
{
|
||||
setUpdateId(prevValue => (prevValue + 1));
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
colorItem.notify = rerender;
|
||||
|
||||
return () =>
|
||||
{
|
||||
colorItem.notify = null;
|
||||
}
|
||||
})
|
||||
|
||||
return <NitroCardGridItemView itemColor={ colorItem.color } itemActive={ colorItem.isSelected } onClick={ () => onClick(colorItem) } />
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import { AvatarEditorGridColorItem } from '../../common/AvatarEditorGridColorItem';
|
||||
|
||||
export interface AvatarEditorPaletteSetItemProps
|
||||
{
|
||||
colorItem: AvatarEditorGridColorItem;
|
||||
onClick: (item: AvatarEditorGridColorItem) => void;
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { FC, useCallback } from 'react';
|
||||
import { NitroCardGridView } from '../../../../layout/card/grid/NitroCardGridView';
|
||||
import { NitroCardGridThemes } from '../../../../layout/card/grid/NitroCardGridView.types';
|
||||
import { AvatarEditorGridColorItem } from '../../common/AvatarEditorGridColorItem';
|
||||
import { AvatarEditorPaletteSetItem } from '../palette-set-item/AvatarEditorPaletteSetItem';
|
||||
import { AvatarEditorPaletteSetViewProps } from './AvatarEditorPaletteSetView.types';
|
||||
|
||||
export const AvatarEditorPaletteSetView: FC<AvatarEditorPaletteSetViewProps> = props =>
|
||||
{
|
||||
const { model = null, category = null, paletteSet = [], paletteIndex = -1 } = props;
|
||||
|
||||
const selectColor = useCallback((item: AvatarEditorGridColorItem) =>
|
||||
{
|
||||
const index = paletteSet.indexOf(item);
|
||||
|
||||
if(index === -1) return;
|
||||
|
||||
model.selectColor(category.name, index, paletteIndex);
|
||||
}, [ model, category, paletteSet, paletteIndex ]);
|
||||
|
||||
return (
|
||||
<NitroCardGridView columns={ 3 } theme={ NitroCardGridThemes.THEME_SHADOWED }>
|
||||
{ (paletteSet.length > 0) && paletteSet.map((item, index) =>
|
||||
{
|
||||
return <AvatarEditorPaletteSetItem key={ index } colorItem={ item } onClick={ selectColor } />;
|
||||
}) }
|
||||
</NitroCardGridView>
|
||||
);
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { AvatarEditorGridColorItem } from '../../common/AvatarEditorGridColorItem';
|
||||
import { CategoryData } from '../../common/CategoryData';
|
||||
import { IAvatarEditorCategoryModel } from '../../common/IAvatarEditorCategoryModel';
|
||||
|
||||
export interface AvatarEditorPaletteSetViewProps
|
||||
{
|
||||
model: IAvatarEditorCategoryModel;
|
||||
category: CategoryData;
|
||||
paletteSet: AvatarEditorGridColorItem[];
|
||||
paletteIndex: number;
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import { FigureData, RedeemItemClothingComposer, RoomObjectCategory, UserFigureComposer } from 'nitro-renderer';
|
||||
import { RedeemItemClothingComposer, RoomObjectCategory, UserFigureComposer } from 'nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { GetAvatarRenderManager, GetConnection, GetFurnitureDataForRoomObject, GetSessionDataManager } from '../../../../../../../api';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../../../layout';
|
||||
import { LocalizeText } from '../../../../../../../utils/LocalizeText';
|
||||
import { FigureData } from '../../../../../../avatar-editor/common/FigureData';
|
||||
import { FurniCategory } from '../../../../../../inventory/common/FurniCategory';
|
||||
import { AvatarImageView } from '../../../../../../shared/avatar-image/AvatarImageView';
|
||||
import { useRoomContext } from '../../../../../context/RoomContext';
|
||||
|
@ -5,4 +5,5 @@
|
||||
background-repeat: no-repeat;
|
||||
background-position-x: center;
|
||||
background-position-y: -8px !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Dispose, DropBounce, EaseOut, JumpBy, Motions, NitroToolbarAnimateIconEvent, Queue, Wait } from 'nitro-renderer';
|
||||
import { Dispose, DropBounce, EaseOut, JumpBy, Motions, NitroToolbarAnimateIconEvent, Queue, UserFigureEvent, Wait } from 'nitro-renderer';
|
||||
import { UserInfoEvent } from 'nitro-renderer/src/nitro/communication/messages/incoming/user/data/UserInfoEvent';
|
||||
import { UserInfoDataParser } from 'nitro-renderer/src/nitro/communication/messages/parser/user/data/UserInfoDataParser';
|
||||
import { FC, useCallback, useState } from 'react';
|
||||
@ -17,6 +17,7 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
|
||||
const { isInRoom } = props;
|
||||
|
||||
const [ userInfo, setUserInfo ] = useState<UserInfoDataParser>(null);
|
||||
const [ userFigure, setUserFigure ] = useState<string>(null);
|
||||
const [ isMeExpanded, setMeExpanded ] = useState(false);
|
||||
const [ unseenInventoryCount, setUnseenInventoryCount ] = useState(0);
|
||||
|
||||
@ -28,10 +29,20 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
|
||||
const parser = event.getParser();
|
||||
|
||||
setUserInfo(parser.userInfo);
|
||||
setUserFigure(parser.userInfo.figure);
|
||||
}, []);
|
||||
|
||||
CreateMessageHook(UserInfoEvent, onUserInfoEvent);
|
||||
|
||||
const onUserFigureEvent = useCallback((event: UserFigureEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setUserFigure(parser.figure);
|
||||
}, []);
|
||||
|
||||
CreateMessageHook(UserFigureEvent, onUserFigureEvent);
|
||||
|
||||
const onUnseenItemTrackerUpdateEvent = useCallback((event: UnseenItemTrackerUpdateEvent) =>
|
||||
{
|
||||
setUnseenInventoryCount(event.count);
|
||||
@ -116,7 +127,7 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
|
||||
<div className="navigation-items navigation-avatar pe-1 me-2">
|
||||
<div className="navigation-item">
|
||||
<div className={ 'toolbar-avatar ' + (isMeExpanded ? 'active ' : '') } onClick={ event => setMeExpanded(!isMeExpanded) }>
|
||||
{ userInfo && <AvatarImageView figure={ userInfo.figure } direction={ 2 } /> }
|
||||
{ userFigure && <AvatarImageView figure={ userFigure } direction={ 2 } /> }
|
||||
</div>
|
||||
</div>
|
||||
{ (unseenAchievementsCount > 0) && (
|
||||
|