Room Widget Camera

This commit is contained in:
MyNameIsBatman 2021-06-15 14:40:02 -03:00
parent 8efaf59889
commit 8d05daf376
17 changed files with 615 additions and 69 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 B

View File

@ -446,6 +446,18 @@ i {
height: 20px;
}
&.icon-camera-colormatrix {
background: url('../images/icons/camera-colormatrix.png');
width: 32px;
height: 14px;
}
&.icon-camera-composite {
background: url('../images/icons/camera-composite.png');
width: 32px;
height: 14px;
}
&.spin {
animation: rotating 1s linear infinite;
}

View File

@ -4,7 +4,7 @@ import { NitroCardContentViewProps } from './NitroCardContextView.types';
export const NitroCardContentView: FC<NitroCardContentViewProps> = props =>
{
return (
<div className="container-fluid bg-light content-area">
<div className={ 'container-fluid bg-light content-area ' + props.className }>
{ props.children }
</div>
);

View File

@ -1,4 +1,4 @@
export interface NitroCardContentViewProps
{
className?: string;
}

View File

@ -1,10 +1,10 @@
import { RoomObjectCategory } from 'nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { GetRoomObjectNameData } from '../../../../api/nitro/session/GetRoomObjectNameData';
import { RoomObjectNameData } from '../../../../api/nitro/session/RoomObjectNameData';
import { CreateEventDispatcherHook } from '../../../../hooks/events/event-dispatcher.base';
import { RoomWidgetRoomEngineUpdateEvent, RoomWidgetRoomObjectUpdateEvent } from '../events';
import { AvatarInfoWidgetViewProps } from './AvatarInfoWidgetView.types';
import { GetRoomObjectNameData } from './utils/GetRoomObjectNameData';
import { RoomObjectNameData } from './utils/RoomObjectNameData';
import { AvatarInfoWidgetNameView } from './views/name/AvatarInfoWidgetNameView';
export const AvatarInfoWidgetView: FC<AvatarInfoWidgetViewProps> = props =>

View File

@ -1,4 +1,4 @@
import { RoomObjectNameData } from '../../utils/RoomObjectNameData';
import { RoomObjectNameData } from '../../../../../../api/nitro/session/RoomObjectNameData';
export interface AvatarInfoWidgetNameViewProps
{

View File

@ -1,29 +1,94 @@
.nitro-camera {
width: 340px;
height: 462px;
.nitro-camera-capture {
.camera-canvas {
width: 340px;
height: 462px;
background-image: url('../../../../assets/images/room-widgets/camera-widget/camera-spritesheet.png');
background-image: url('../../../../assets/images/room-widgets/camera-widget/camera-spritesheet.png');
.camera-frame {
position: absolute;
width: 318px;
height: 318px;
margin-top: 9px;
margin-left: 11.4px;
.camera-frame {
width: 300px;
height: 300px;
&.bg {
background: black;
}
.camera-frame-preview-actions {
background: rgba(0, 0, 0, .5);
}
}
.camera-button {
width: 94px;
height: 94px;
cursor: pointer;
margin-top: 334px;
background-image: url('../../../../assets/images/room-widgets/camera-widget/camera-spritesheet.png');
background-position: -340px 0px;
&:hover {
background-position: -340px -94px;
}
&:active {
background-position: -340px -188px;
}
}
}
.camera-button {
width: 94px;
height: 94px;
cursor: pointer;
margin-top: 334px;
.camera-roll {
width: 330px;
background: #bab8b4;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
border: 1px solid black;
box-shadow: inset 1px 0px white, inset -1px -1px white;
background-image: url('../../../../assets/images/room-widgets/camera-widget/camera-spritesheet.png');
background-position: -340px 0px;
&:hover {
background-position: -340px -94px;
}
&:active {
background-position: -340px -188px;
img {
width: 56px;
height: 56px;
border: 1px solid black;
}
}
}
.nitro-camera-editor {
width: 600px;
.effects {
max-height: 354px;
.effect-thumbnail {
border-color: $grid-border-color !important;
background-color: $grid-bg-color;
&.active {
border-color: $grid-active-border-color !important;
background-color: $grid-active-bg-color;
}
.effect-thumbnail-image {
background: black;
img {
width: 56px;
height: 56px;
}
}
}
}
.picture-preview {
width: 318px;
height: 318px;
&.zoomed {
background-size: 636px;
background-position: center;
}
}
}

View File

@ -1,74 +1,75 @@
import { NitroRectangle } from 'nitro-renderer';
import { FC, useCallback, useRef, useState } from 'react';
import { GetRoomEngine, GetRoomSession } from '../../../../api';
import { RoomCameraWidgetEditorEffect } from 'nitro-renderer/src/nitro/room/camera-widget/RoomCameraWidgetEditorEffect';
import { RoomCameraWidgetManagerEvent } from 'nitro-renderer/src/nitro/room/events/RoomCameraWidgetManagerEvent';
import { FC, useCallback, useState } from 'react';
import { GetRoomEngine } from '../../../../api';
import { RoomWidgetCameraEvent } from '../../../../events/room-widgets/camera/RoomWidgetCameraEvent';
import { DraggableWindow } from '../../../../hooks/draggable-window/DraggableWindow';
import { useRoomEngineEvent } from '../../../../hooks/events/nitro/room/room-engine-event';
import { useUiEvent } from '../../../../hooks/events/ui/ui-event';
import { CameraWidgetViewProps } from './CameraWidgetView.types';
import { CameraWidgetCaptureView } from './views/capture/CameraWidgetCaptureView';
import { CameraWidgetEditorView } from './views/editor/CameraWidgetEditorView';
export const CameraWidgetView: FC<CameraWidgetViewProps> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const cameraFrameRef = useRef<HTMLDivElement>();
const [ isCaptureVisible, setIsCaptureVisible ] = useState(false);
const [ isEditorVisible, setIsEditorVisible ] = useState(false);
const [ chosenPicture, setChosenPicture ] = useState<HTMLImageElement>(null);
const [ availableEffects, setAvailableEffects ] = useState<RoomCameraWidgetEditorEffect[]>(null);
const onRoomWidgetCameraEvent = useCallback((event: RoomWidgetCameraEvent) =>
const getAvailableEffects = useCallback(() =>
{
if(GetRoomEngine().roomCameraWidgetManager.isLoaded)
{
setAvailableEffects(Array.from(GetRoomEngine().roomCameraWidgetManager.loadedEffects.values()));
}
}, []);
const onNitroEvent = useCallback((event: RoomWidgetCameraEvent) =>
{
switch(event.type)
{
case RoomWidgetCameraEvent.SHOW_CAMERA:
setIsVisible(true);
setIsCaptureVisible(true);
getAvailableEffects();
return;
case RoomWidgetCameraEvent.HIDE_CAMERA:
setIsVisible(false);
setIsCaptureVisible(false);
setIsEditorVisible(false);
return;
case RoomWidgetCameraEvent.TOGGLE_CAMERA:
setIsVisible(value => !value);
setIsEditorVisible(false);
setIsCaptureVisible(value => !value);
getAvailableEffects();
return;
case RoomCameraWidgetManagerEvent.INITIALIZED:
getAvailableEffects();
return;
}
}, []);
useUiEvent(RoomWidgetCameraEvent.SHOW_CAMERA, onRoomWidgetCameraEvent);
useUiEvent(RoomWidgetCameraEvent.HIDE_CAMERA, onRoomWidgetCameraEvent);
useUiEvent(RoomWidgetCameraEvent.TOGGLE_CAMERA, onRoomWidgetCameraEvent);
useUiEvent(RoomWidgetCameraEvent.SHOW_CAMERA, onNitroEvent);
useUiEvent(RoomWidgetCameraEvent.HIDE_CAMERA, onNitroEvent);
useUiEvent(RoomWidgetCameraEvent.TOGGLE_CAMERA, onNitroEvent);
useRoomEngineEvent(RoomCameraWidgetManagerEvent.INITIALIZED, onNitroEvent);
const processAction = useCallback((type: string, value: string = null) =>
const processAction = useCallback((type: string, value: any = null) =>
{
switch(type)
{
case 'close':
setIsVisible(false);
setIsCaptureVisible(false);
setIsEditorVisible(false);
return;
case 'capture_choose_picture':
setChosenPicture(value);
setIsCaptureVisible(false);
setIsEditorVisible(true);
return;
}
}, []);
const takePicture = useCallback(() =>
{
const frameBounds = cameraFrameRef.current.getBoundingClientRect();
if(!frameBounds) return;
const rectangle = new NitroRectangle(Math.floor(frameBounds.x), Math.floor(frameBounds.y), Math.floor(frameBounds.width), Math.floor(frameBounds.height));
console.log(rectangle);
GetRoomEngine().createRoomScreenshot(GetRoomSession().roomId, 1, rectangle);
}, []);
if(!isVisible) return null;
return (
<DraggableWindow handle=".nitro-camera">
<div className="nitro-camera">
<div className="overflow-auto">
<div className="cursor-pointer float-end me-3 mt-2" onClick={ event => processAction('close') }>
<i className="fas fa-times"></i>
</div>
</div>
<div className="d-flex justify-content-center">
<div ref={ cameraFrameRef } className="camera-frame"></div>
<div className="camera-button" onClick={ takePicture }></div>
</div>
</div>
</DraggableWindow>
( isCaptureVisible && <CameraWidgetCaptureView onCloseClick={ () => processAction('close') } onChoosePicture={ (picture) => processAction('capture_choose_picture', picture) } /> ) ||
( isEditorVisible && <CameraWidgetEditorView onCloseClick={ () => processAction('close') } picture={ chosenPicture } availableEffects={ availableEffects } /> )
);
}

View File

@ -0,0 +1,102 @@
import classNames from 'classnames';
import { NitroRectangle } from 'nitro-renderer';
import { FC, useCallback, useRef, useState } from 'react';
import { GetRoomEngine } from '../../../../../../api/nitro/room/GetRoomEngine';
import { GetRoomSession } from '../../../../../../api/nitro/session/GetRoomSession';
import { DraggableWindow } from '../../../../../../hooks/draggable-window/DraggableWindow';
import { LocalizeText } from '../../../../../../utils/LocalizeText';
import { CameraWidgetCaptureViewProps } from './CameraWidgetCaptureView.types';
export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =>
{
const CAMERA_ROLL_LIMIT: number = 5;
const [ picturesTaken, setPicturesTaken ] = useState<HTMLImageElement[]>([]);
const [ selectedPictureIndex, setSelectedPictureIndex ] = useState(-1);
const cameraFrameRef = useRef<HTMLDivElement>();
const takePicture = useCallback(() =>
{
if(selectedPictureIndex > -1)
{
setSelectedPictureIndex(-1);
return;
}
const frameBounds = cameraFrameRef.current.getBoundingClientRect();
if(!frameBounds) return;
const rectangle = new NitroRectangle(Math.floor(frameBounds.x), Math.floor(frameBounds.y), Math.floor(frameBounds.width), Math.floor(frameBounds.height));
const image = GetRoomEngine().createRoomScreenshot(GetRoomSession().roomId, 1, rectangle);
if(picturesTaken.length + 1 === CAMERA_ROLL_LIMIT)
{
alert(LocalizeText('camera.full.body'));
}
if(picturesTaken.length === CAMERA_ROLL_LIMIT)
{
setPicturesTaken(picturesTaken => [ ...picturesTaken.slice(0, CAMERA_ROLL_LIMIT - 1), image ]);
}
else
{
setPicturesTaken(picturesTaken => [ ...picturesTaken, image ]);
}
}, [ picturesTaken, selectedPictureIndex ]);
const processAction = useCallback((type: string, value: string | number = null) =>
{
switch(type)
{
case 'take_picture':
takePicture();
return;
case 'preview_picture':
setSelectedPictureIndex(Number(value));
return;
case 'discard_picture':
setSelectedPictureIndex(-1);
const newPicturesTaken = picturesTaken;
picturesTaken.splice(selectedPictureIndex, 1);
setPicturesTaken(newPicturesTaken);
return;
case 'edit_picture':
props.onChoosePicture(picturesTaken[selectedPictureIndex]);
return;
}
}, [ picturesTaken, selectedPictureIndex ]);
return (
<DraggableWindow handle=".nitro-camera-capture">
<div className="nitro-camera-capture d-flex flex-column justify-content-center align-items-center">
<div className="camera-canvas">
<div className="overflow-auto">
<div className="cursor-pointer float-end me-3 mt-2" onClick={ props.onCloseClick }>
<i className="fas fa-times"></i>
</div>
</div>
<div ref={ cameraFrameRef } className={'camera-frame ' + classNames({'bg': selectedPictureIndex > -1}) }>
{ selectedPictureIndex > -1 && <div>
<img src={ picturesTaken[selectedPictureIndex].src } />
<div className="camera-frame-preview-actions w-100 position-absolute bottom-0 py-2 text-center">
<button className="btn btn-success me-3" onClick={ event => processAction('edit_picture') }>{ LocalizeText('camera.editor.button.text') }</button>
<button className="btn btn-danger" onClick={ event => processAction('discard_picture') }>{ LocalizeText('camera.delete.button.text') }</button>
</div>
</div> }
</div>
<div className="d-flex justify-content-center">
<div className="camera-button" onClick={ takePicture }></div>
</div>
</div>
{ picturesTaken.length > 0 && <div className="camera-roll d-flex justify-content-center py-2">
{ picturesTaken.map((picture, index) =>
{
return <img key={ index } className={ (index < picturesTaken.length - 1 ? 'me-2' : '') } src={ picture.src } onClick={ event => processAction('preview_picture', index) } />;
}) }
</div> }
</div>
</DraggableWindow>
);
}

View File

@ -0,0 +1,5 @@
export interface CameraWidgetCaptureViewProps
{
onCloseClick: () => void;
onChoosePicture: (picture: HTMLImageElement) => void;
}

View File

@ -0,0 +1,127 @@
import classNames from 'classnames';
import { RoomCameraWidgetEditorSelectedEffect } from 'nitro-renderer/src/nitro/room/camera-widget/RoomCameraWidgetEditorSelectedEffect';
import { FC, useCallback, useEffect, useState } from 'react';
import { GetRoomEngine } from '../../../../../../api';
import { NitroCardContentView } from '../../../../../../layout/card/content/NitroCardContentView';
import { NitroCardHeaderView } from '../../../../../../layout/card/header/NitroCardHeaderView';
import { NitroCardView } from '../../../../../../layout/card/NitroCardView';
import { NitroCardTabsView } from '../../../../../../layout/card/tabs/NitroCardTabsView';
import { NitroCardTabsItemView } from '../../../../../../layout/card/tabs/tabs-item/NitroCardTabsItemView';
import { LocalizeText } from '../../../../../../utils/LocalizeText';
import { CameraWidgetEditorTabs, CameraWidgetEditorViewProps } from './CameraWidgetEditorView.types';
export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
{
const TABS: string[] = [ CameraWidgetEditorTabs.COLORMATRIX, CameraWidgetEditorTabs.COMPOSITE ];
const MY_LEVEL: number = 0;
const [ currentTab, setCurrentTab ] = useState(CameraWidgetEditorTabs.COLORMATRIX);
const [ isZoomed, setIsZoomed ] = useState(false);
const [ selectedEffects, setSelectedEffects ] = useState<RoomCameraWidgetEditorSelectedEffect[]>([]);
const [ effectsThumbnails, setEffectThumbnails ] = useState<{name: string, image: HTMLImageElement}[]>([]);
useEffect(() =>
{
const thumbnails = [];
for(const effect of props.availableEffects)
{
let alpha = 126;
if(effect.colorMatrix.length > 0) alpha = 0.5;
thumbnails.push({name: effect.name, image: GetRoomEngine().roomCameraWidgetManager.editImage(props.picture, [new RoomCameraWidgetEditorSelectedEffect(effect, alpha)])});
}
setEffectThumbnails(thumbnails);
}, [ props.availableEffects ]);
const getEffectThumbnail = useCallback((effectName: string) =>
{
const search = effectsThumbnails.find(thumbnail => thumbnail.name === effectName);
if(search) return search.image.src;
return null;
}, [ effectsThumbnails ]);
const getEffectList = useCallback(() =>
{
if(currentTab === CameraWidgetEditorTabs.COLORMATRIX)
{
return props.availableEffects.filter(effect => effect.colorMatrix.length > 0);
}
else
{
return props.availableEffects.filter(effect => effect.colorMatrix.length === 0);
}
}, [ currentTab, props.availableEffects ]);
const processAction = useCallback((type: string, value: string | number = null) =>
{
switch(type)
{
case 'close':
props.onCloseClick();
return;
case 'change_tab':
setCurrentTab(String(value));
return;
case 'zoom':
setIsZoomed(isZoomed => !isZoomed);
return;
}
}, []);
return (
<NitroCardView className="nitro-camera-editor">
<NitroCardHeaderView headerText={ LocalizeText('camera.editor.button.text') } onCloseClick={ event => processAction('close') } />
<div className="d-flex">
<div className="w-100">
<NitroCardTabsView>
{ TABS.map(tab =>
{
return <NitroCardTabsItemView key={ tab } isActive={ currentTab === tab } onClick={ event => processAction('change_tab', tab) }><i className={ 'icon icon-camera-' + tab }></i></NitroCardTabsItemView>
}) }
</NitroCardTabsView>
<NitroCardContentView>
<div className="d-flex h-100 overflow-auto effects px-2">
<div className="row row-cols-3">
{ getEffectList().map(effect =>
{
return (
<div key={ effect.name } className="col mb-3">
<div title={ LocalizeText('camera.effect.name.' + effect.name) } className="effect-thumbnail cursor-pointer position-relative border border-2 rounded d-flex flex-column justify-content-center align-items-center py-1">
<div className="effect-thumbnail-image rounded">
<img className="rounded" src={ getEffectThumbnail(effect.name) } />
</div>
</div>
</div>
);
}) }
</div>
</div>
</NitroCardContentView>
</div>
<div className="w-100">
<NitroCardTabsView></NitroCardTabsView>
<NitroCardContentView>
<div className="bg-black rounded">
<div className={ 'picture-preview rounded' + classNames({ ' zoomed': isZoomed }) } style={ { backgroundImage: 'url(' + props.picture.src + ')' } }></div>
</div>
<div className="d-flex mt-2">
<button className="btn btn-primary btn-sm me-2">{ LocalizeText('save') }</button>
<button className="btn btn-primary btn-sm" onClick={ event => processAction('zoom') }>{ LocalizeText('room.zoom.button.text') }</button>
</div>
</NitroCardContentView>
</div>
</div>
<NitroCardContentView>
<div className="d-flex justify-content-end">
<button className="btn btn-primary me-2">{ LocalizeText('generic.cancel') }</button>
<button className="btn btn-success">{ LocalizeText('camera.preview.button.text') }</button>
</div>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,14 @@
import { RoomCameraWidgetEditorEffect } from '../../../../../../../../nitro-renderer/src/nitro/room/camera-widget/RoomCameraWidgetEditorEffect';
export interface CameraWidgetEditorViewProps
{
onCloseClick: () => void;
picture: HTMLImageElement;
availableEffects: RoomCameraWidgetEditorEffect[];
}
export class CameraWidgetEditorTabs
{
public static readonly COLORMATRIX: string = 'colormatrix';
public static readonly COMPOSITE: string = 'composite';
}