Update AvatarEditor

This commit is contained in:
Bill 2021-09-29 22:30:25 -04:00
parent f69b40bbfe
commit 33aac67bbe
9 changed files with 180 additions and 142 deletions

View File

@ -75,70 +75,71 @@
transform: scale(2); transform: scale(2);
} }
} }
}
.wardrobe-grid { .nitro-wardrobe-grid {
--nitro-grid-column-min-width: 80px !important;
.grid-item {
height: 140px;
max-height: 140px;
background-color: $ghost;
&:after {
position: absolute;
content: '';
top: 75%;
bottom: 0;
left: 0;
right: 0;
border-radius: 50%;
background-color: $gray-chateau;
box-shadow: 0 0 8px 2px rgba($white,.6);
transform: scale(2);
}
.avatar-image {
position: absolute;
bottom: 0;
background-position-y: -23px !important;
z-index: 4;
}
.figure-button-container {
background-color: $gray-chateau;
z-index: 3;
}
}
.grid-item-container {
height: 142px !important;
max-height: 142px !important;
.grid-item { .grid-item {
height: 140px;
max-height: 140px;
background-color: $ghost; background-color: $ghost;
&:after {
position: absolute;
content: '';
top: 75%;
bottom: 0;
left: 0;
right: 0;
border-radius: 50%;
background-color: $gray-chateau;
box-shadow: 0 0 8px 2px rgba($white,.6);
transform: scale(2);
}
.avatar-image { .avatar-image {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
background-position-y: -23px !important; background-position-y: -23px !important;
z-index: 4; z-index: 3;
} }
.figure-button-container { .figure-button-container {
background-color: $gray-chateau; background-color: $gray-chateau;
z-index: 3; z-index: 2;
} }
}
.grid-item-container { &:after {
height: 142px !important; position: absolute;
max-height: 142px !important; content: '';
height: 50%;
.grid-item { bottom: 0;
background-color: $ghost; left: 0;
right: 0;
.avatar-image { background-color: $gray-chateau;
position: absolute; box-shadow: 0 0 8px 2px rgba($white,.6);
bottom: 0; z-index: 1;
background-position-y: -23px !important;
z-index: 3;
}
.figure-button-container {
background-color: $gray-chateau;
z-index: 2;
}
&:after {
position: absolute;
content: '';
height: 50%;
bottom: 0;
left: 0;
right: 0;
background-color: $gray-chateau;
box-shadow: 0 0 8px 2px rgba($white,.6);
z-index: 1;
}
} }
} }
} }

View File

@ -1,11 +1,12 @@
import { AvatarDirectionAngle, AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetWardrobeMessageComposer, IAvatarFigureContainer, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer'; import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetWardrobeMessageComposer, IAvatarFigureContainer, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useState } from 'react';
import { GetAvatarRenderManager, GetClubMemberLevel, GetSessionDataManager, LocalizeText } from '../../api'; import { GetAvatarRenderManager, GetClubMemberLevel, GetSessionDataManager, LocalizeText } from '../../api';
import { AvatarEditorEvent } from '../../events/avatar-editor'; import { AvatarEditorEvent } from '../../events/avatar-editor';
import { CreateMessageHook, SendMessageHook } from '../../hooks'; import { CreateMessageHook, SendMessageHook } from '../../hooks';
import { useUiEvent } from '../../hooks/events/ui/ui-event'; import { useUiEvent } from '../../hooks/events/ui/ui-event';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../layout'; import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, NitroLayoutGrid, NitroLayoutGridColumn } from '../../layout';
import { AvatarEditorViewProps } from './AvatarEditorView.types'; import { AvatarEditorViewProps } from './AvatarEditorView.types';
import { AvatarEditorAction } from './common/AvatarEditorAction';
import { AvatarEditorUtilities } from './common/AvatarEditorUtilities'; import { AvatarEditorUtilities } from './common/AvatarEditorUtilities';
import { BodyModel } from './common/BodyModel'; import { BodyModel } from './common/BodyModel';
import { FigureData } from './common/FigureData'; import { FigureData } from './common/FigureData';
@ -14,6 +15,7 @@ import { HeadModel } from './common/HeadModel';
import { IAvatarEditorCategoryModel } from './common/IAvatarEditorCategoryModel'; import { IAvatarEditorCategoryModel } from './common/IAvatarEditorCategoryModel';
import { LegModel } from './common/LegModel'; import { LegModel } from './common/LegModel';
import { TorsoModel } from './common/TorsoModel'; import { TorsoModel } from './common/TorsoModel';
import { AvatarEditorFigureActionsView } from './views/figure-actions/AvatarEditorFigureActionsView';
import { AvatarEditorFigurePreviewView } from './views/figure-preview/AvatarEditorFigurePreviewView'; import { AvatarEditorFigurePreviewView } from './views/figure-preview/AvatarEditorFigurePreviewView';
import { AvatarEditorModelView } from './views/model/AvatarEditorModelView'; import { AvatarEditorModelView } from './views/model/AvatarEditorModelView';
import { AvatarEditorWardrobeView } from './views/wardrobe/AvatarEditorWardrobeView'; import { AvatarEditorWardrobeView } from './views/wardrobe/AvatarEditorWardrobeView';
@ -21,6 +23,10 @@ import { AvatarEditorWardrobeView } from './views/wardrobe/AvatarEditorWardrobeV
const DEFAULT_MALE_FIGURE: string = 'hr-100.hd-180-7.ch-215-66.lg-270-79.sh-305-62.ha-1002-70.wa-2007'; const DEFAULT_MALE_FIGURE: string = 'hr-100.hd-180-7.ch-215-66.lg-270-79.sh-305-62.ha-1002-70.wa-2007';
const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62.sh-735-68'; const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62.sh-735-68';
const MAX_SAVED_FIGURES: number = 10; const MAX_SAVED_FIGURES: number = 10;
const ACTION_CLEAR = 'action_clear';
const ACTION_RESET = 'action_reset';
const ACTION_RANDOMIZE = 'action_randomize';
const ACTION_SAVE = 'action_save';
export const AvatarEditorView: FC<AvatarEditorViewProps> = props => export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
{ {
@ -157,46 +163,30 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
} }
}, [ figures, figureData ]); }, [ figures, figureData ]);
const clearFigure = useCallback(() => const processAction = useCallback((action: string) =>
{ {
loadAvatarInEditor(figureData.getFigureStringWithFace(0, false), figureData.gender, false); switch(action)
resetCategories();
}, [ figureData, loadAvatarInEditor, resetCategories ]);
const resetFigure = useCallback(() =>
{
loadAvatarInEditor(lastFigure, lastGender);
resetCategories();
}, [ lastFigure, lastGender, loadAvatarInEditor, resetCategories ]);
const randomizeFigure = useCallback(() =>
{
const figure = generateRandomFigure(figureData, figureData.gender, GetClubMemberLevel(), figureSetIds, [ FigureData.FACE ]);
loadAvatarInEditor(figure, figureData.gender, false);
resetCategories();
}, [ figureData, figureSetIds, loadAvatarInEditor, resetCategories ]);
const rotateFigure = useCallback((direction: number) =>
{
if(direction < AvatarDirectionAngle.MIN_DIRECTION)
{ {
direction = (AvatarDirectionAngle.MAX_DIRECTION + (direction + 1)); case AvatarEditorAction.ACTION_CLEAR:
loadAvatarInEditor(figureData.getFigureStringWithFace(0, false), figureData.gender, false);
resetCategories();
return;
case AvatarEditorAction.ACTION_RESET:
loadAvatarInEditor(lastFigure, lastGender);
resetCategories();
return;
case AvatarEditorAction.ACTION_RANDOMIZE:
const figure = generateRandomFigure(figureData, figureData.gender, GetClubMemberLevel(), figureSetIds, [ FigureData.FACE ]);
loadAvatarInEditor(figure, figureData.gender, false);
resetCategories();
return;
case AvatarEditorAction.ACTION_SAVE:
SendMessageHook(new UserFigureComposer(figureData.gender, figureData.getFigureString()));
setIsVisible(false);
return;
} }
}, [ figureData, lastFigure, lastGender, figureSetIds, loadAvatarInEditor, resetCategories ])
if(direction > AvatarDirectionAngle.MAX_DIRECTION)
{
direction = (direction - (AvatarDirectionAngle.MAX_DIRECTION + 1));
}
figureData.direction = direction;
}, [ figureData ]);
const saveFigure = useCallback(() =>
{
SendMessageHook(new UserFigureComposer(figureData.gender, figureData.getFigureString()));
setIsVisible(false);
}, [ figureData ]);
const setGender = useCallback((gender: string) => const setGender = useCallback((gender: string) =>
{ {
@ -295,37 +285,18 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
</NitroCardTabsItemView> </NitroCardTabsItemView>
</NitroCardTabsView> </NitroCardTabsView>
<NitroCardContentView> <NitroCardContentView>
<div className="row h-100"> <NitroLayoutGrid>
<div className="col-9 d-flex flex-column h-100"> <NitroLayoutGridColumn size={ 9 }>
{ (activeCategory && !isWardrobeVisible) && <AvatarEditorModelView model={ activeCategory } gender={ figureData.gender } setGender={ setGender } /> } { (activeCategory && !isWardrobeVisible) &&
{ isWardrobeVisible && <AvatarEditorWardrobeView figureData={ figureData } savedFigures={ savedFigures } setSavedFigures={ setSavedFigures } loadAvatarInEditor={ loadAvatarInEditor } /> } <AvatarEditorModelView model={ activeCategory } gender={ figureData.gender } setGender={ setGender } /> }
</div> { isWardrobeVisible &&
<div className="col-3 d-flex flex-column h-100"> <AvatarEditorWardrobeView figureData={ figureData } savedFigures={ savedFigures } setSavedFigures={ setSavedFigures } loadAvatarInEditor={ loadAvatarInEditor } /> }
<div className="figure-preview-container"> </NitroLayoutGridColumn>
<AvatarEditorFigurePreviewView figureData={ figureData } /> <NitroLayoutGridColumn overflow="hidden" size={ 3 }>
<div className="avatar-spotlight" /> <AvatarEditorFigurePreviewView figureData={ figureData } />
<div className="avatar-shadow" /> <AvatarEditorFigureActionsView processAction={ processAction } />
<div className="arrow-container"> </NitroLayoutGridColumn>
<i className="icon arrow-left-icon" onClick={ event => rotateFigure(figureData.direction + 1) } /> </NitroLayoutGrid>
<i className="icon arrow-right-icon" onClick={ event => rotateFigure(figureData.direction - 1) } />
</div>
</div>
<div className="d-flex flex-column mt-1">
<div className="btn-group mb-1">
<button type="button" className="btn btn-sm btn-secondary" onClick={ resetFigure }>
<i className="fas fa-undo" />
</button>
<button type="button" className="btn btn-sm btn-secondary" onClick={ clearFigure }>
<i className="fas fa-trash" />
</button>
<button type="button" className="btn btn-sm btn-secondary" onClick={ randomizeFigure }>
<i className="fas fa-dice" />
</button>
</div>
<button type="button" className="btn btn-success btn-sm w-100" onClick={ saveFigure }>{ LocalizeText('avatareditor.save') }</button>
</div>
</div>
</div>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
); );

View File

@ -0,0 +1,7 @@
export class AvatarEditorAction
{
public static ACTION_SAVE: string = 'AEA_ACTION_SAVE';
public static ACTION_CLEAR: string = 'AEA_ACTION_CLEAR';
public static ACTION_RESET: string = 'AEA_ACTION_RESET';
public static ACTION_RANDOMIZE: string = 'AEA_ACTION_RANDOMIZE';
}

View File

@ -0,0 +1,29 @@
import { FC } from 'react';
import { LocalizeText } from '../../../../api';
import { NitroLayoutButton, NitroLayoutButtonGroup, NitroLayoutFlexColumn } from '../../../../layout';
import { AvatarEditorAction } from '../../common/AvatarEditorAction';
import { AvatarEditorFigureActionsViewProps } from './AvatarEditorFigureActionsView.types';
export const AvatarEditorFigureActionsView: FC<AvatarEditorFigureActionsViewProps> = props =>
{
const { processAction = null } = props;
return (
<NitroLayoutFlexColumn className="flex-grow-1" gap={ 2 }>
<NitroLayoutButtonGroup>
<NitroLayoutButton variant="secondary" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_RESET) }>
<i className="fas fa-undo" />
</NitroLayoutButton>
<NitroLayoutButton variant="secondary" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_CLEAR) }>
<i className="fas fa-trash" />
</NitroLayoutButton>
<NitroLayoutButton variant="secondary" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_RANDOMIZE) }>
<i className="fas fa-dice" />
</NitroLayoutButton>
</NitroLayoutButtonGroup>
<NitroLayoutButton className="w-100" variant="success" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_SAVE) }>
{ LocalizeText('avatareditor.save') }
</NitroLayoutButton>
</NitroLayoutFlexColumn>
)
}

View File

@ -0,0 +1,5 @@
export interface AvatarEditorFigureActionsViewProps
{
processAction: (action: string) => void;
}

View File

@ -1,4 +1,7 @@
import { AvatarDirectionAngle } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useState } from 'react';
import { NitroLayoutFlexColumn } from '../../../../layout';
import { NitroLayoutBase } from '../../../../layout/base';
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView'; import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView';
import { AvatarEditorFigurePreviewViewProps } from './AvatarEditorFigurePreviewView.types'; import { AvatarEditorFigurePreviewViewProps } from './AvatarEditorFigurePreviewView.types';
@ -12,6 +15,21 @@ export const AvatarEditorFigurePreviewView: FC<AvatarEditorFigurePreviewViewProp
setUpdateId(prevValue => (prevValue + 1)); setUpdateId(prevValue => (prevValue + 1));
}, []); }, []);
const rotateFigure = useCallback((direction: number) =>
{
if(direction < AvatarDirectionAngle.MIN_DIRECTION)
{
direction = (AvatarDirectionAngle.MAX_DIRECTION + (direction + 1));
}
if(direction > AvatarDirectionAngle.MAX_DIRECTION)
{
direction = (direction - (AvatarDirectionAngle.MAX_DIRECTION + 1));
}
figureData.direction = direction;
}, [ figureData ]);
useEffect(() => useEffect(() =>
{ {
if(!figureData) return; if(!figureData) return;
@ -24,5 +42,15 @@ export const AvatarEditorFigurePreviewView: FC<AvatarEditorFigurePreviewViewProp
} }
}, [ figureData, rerender ] ); }, [ figureData, rerender ] );
return <AvatarImageView figure={ figureData.getFigureString() } direction={ figureData.direction } scale={ 2 } /> return (
<NitroLayoutFlexColumn className="figure-preview-container" overflow="hidden" position="relative">
<AvatarImageView figure={ figureData.getFigureString() } direction={ figureData.direction } scale={ 2 } />
<NitroLayoutBase className="avatar-spotlight" />
<NitroLayoutBase className="avatar-shadow" />
<NitroLayoutBase className="arrow-container">
<i className="icon arrow-left-icon" onClick={ event => rotateFigure(figureData.direction + 1) } />
<i className="icon arrow-right-icon" onClick={ event => rotateFigure(figureData.direction - 1) } />
</NitroLayoutBase>
</NitroLayoutFlexColumn>
);
} }

View File

@ -24,7 +24,7 @@ export const AvatarEditorFigureSetItemView: FC<AvatarEditorFigureSetItemViewProp
}) })
return ( return (
<NitroCardGridItemView itemImage={ (partItem.isClear ? undefined : partItem.imageUrl) } itemActive={ partItem.isSelected } onClick={ () => onClick(partItem) }> <NitroCardGridItemView itemImage={ (partItem.isClear ? undefined : partItem.imageUrl) } itemActive={ partItem.isSelected } onClick={ event => onClick(partItem) }>
{ partItem.isHC && <CurrencyIcon className="position-absolute end-1 bottom-1" type={ 'hc' } /> } { partItem.isHC && <CurrencyIcon className="position-absolute end-1 bottom-1" type={ 'hc' } /> }
{ partItem.isClear && <i className="icon clear-icon" /> } { partItem.isClear && <i className="icon clear-icon" /> }
{ partItem.isSellable && <i className="position-absolute icon sellable-icon end-1 bottom-1" /> } { partItem.isSellable && <i className="position-absolute icon sellable-icon end-1 bottom-1" /> }

View File

@ -1,4 +1,5 @@
import { FC, useCallback, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useState } from 'react';
import { NitroLayoutFlex, NitroLayoutGrid, NitroLayoutGridColumn } from '../../../../layout';
import { CategoryData } from '../../common/CategoryData'; import { CategoryData } from '../../common/CategoryData';
import { FigureData } from '../../common/FigureData'; import { FigureData } from '../../common/FigureData';
import { AvatarEditorFigureSetView } from '../figure-set/AvatarEditorFigureSetView'; import { AvatarEditorFigureSetView } from '../figure-set/AvatarEditorFigureSetView';
@ -46,37 +47,37 @@ export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
if(!model || !activeCategory) return null; if(!model || !activeCategory) return null;
return ( return (
<div className="row h-100"> <NitroLayoutGrid>
<div className="col-2 d-flex flex-column align-items-center h-100"> <NitroLayoutGridColumn size={ 2 }>
{ model.canSetGender && { model.canSetGender &&
<> <>
<div className="d-flex justify-content-center align-items-center category-item cursor-pointer" onClick={ event => setGender(FigureData.MALE) }> <NitroLayoutFlex className="justify-content-center align-items-center category-item cursor-pointer" onClick={ event => setGender(FigureData.MALE) }>
<i className={ `icon male-icon ${ (gender === FigureData.MALE) ? ' selected' : ''}` } /> <i className={ `icon male-icon ${ (gender === FigureData.MALE) ? ' selected' : ''}` } />
</div> </NitroLayoutFlex>
<div className="d-flex justify-content-center align-items-center category-item cursor-pointer" onClick={ event => setGender(FigureData.FEMALE) }> <NitroLayoutFlex className="justify-content-center align-items-center category-item cursor-pointer" onClick={ event => setGender(FigureData.FEMALE) }>
<i className={ `icon female-icon ${ (gender === FigureData.FEMALE) ? ' selected' : ''}` } /> <i className={ `icon female-icon ${ (gender === FigureData.FEMALE) ? ' selected' : ''}` } />
</div> </NitroLayoutFlex>
</> } </> }
{ !model.canSetGender && model.categories && (model.categories.size > 0) && Array.from(model.categories.keys()).map(name => { !model.canSetGender && model.categories && (model.categories.size > 0) && Array.from(model.categories.keys()).map(name =>
{ {
const category = model.categories.get(name); const category = model.categories.get(name);
return ( return (
<div key={ name } className="d-flex justify-content-center align-items-center category-item cursor-pointer" onClick={ event => selectCategory(name) }> <NitroLayoutFlex key={ name } className="justify-content-center align-items-center category-item cursor-pointer" onClick={ event => selectCategory(name) }>
<i className={ `icon ${ category.name }-icon ${ (activeCategory === category) ? ' selected' : ''}` } /> <i className={ `icon ${ category.name }-icon ${ (activeCategory === category) ? ' selected' : ''}` } />
</div> </NitroLayoutFlex>
); );
})} })}
</div> </NitroLayoutGridColumn>
<div className="col-5 d-flex flex-column h-100"> <NitroLayoutGridColumn size={ 5 }>
<AvatarEditorFigureSetView model={ model } category={ activeCategory } setMaxPaletteCount={ setMaxPaletteCount } /> <AvatarEditorFigureSetView model={ model } category={ activeCategory } setMaxPaletteCount={ setMaxPaletteCount } />
</div> </NitroLayoutGridColumn>
<div className="col-5 d-flex flex-column h-100 gap-2"> <NitroLayoutGridColumn size={ 5 }>
{ (maxPaletteCount >= 1) && { (maxPaletteCount >= 1) &&
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(0) } paletteIndex={ 0 } /> } <AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(0) } paletteIndex={ 0 } /> }
{ (maxPaletteCount === 2) && { (maxPaletteCount === 2) &&
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(1) } paletteIndex={ 1 } className="mt-1" /> } <AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(1) } paletteIndex={ 1 } className="mt-1" /> }
</div> </NitroLayoutGridColumn>
</div> </NitroLayoutGrid>
); );
} }

View File

@ -65,12 +65,8 @@ export const AvatarEditorWardrobeView: FC<AvatarEditorWardrobeViewProps> = props
}, [ savedFigures, saveFigureAtWardrobeIndex, wearFigureAtIndex ]); }, [ savedFigures, saveFigureAtWardrobeIndex, wearFigureAtIndex ]);
return ( return (
<div className="row h-100"> <NitroCardGridView className="nitro-wardrobe-grid">
<div className="col-12 d-flex h-100"> { figures }
<NitroCardGridView className="wardrobe-grid"> </NitroCardGridView>
{ figures }
</NitroCardGridView>
</div>
</div>
); );
} }