blessing-skin-server/resources/assets/src/components/Viewer.tsx
2020-05-16 21:56:08 +08:00

294 lines
8.1 KiB
TypeScript

/** @jsx jsx */
import React, { useState, useEffect, useRef } from 'react'
import { jsx, css } from '@emotion/core'
import styled from '@emotion/styled'
import * as skinview3d from 'skinview3d'
import { t } from '@/scripts/i18n'
import * as cssUtils from '@/styles/utils'
import * as breakpoints from '@/styles/breakpoints'
import SkinSteve from '../../../misc/textures/steve.png'
import bg1 from '../../../misc/backgrounds/1.png'
import bg2 from '../../../misc/backgrounds/2.png'
import bg3 from '../../../misc/backgrounds/3.png'
import bg4 from '../../../misc/backgrounds/4.png'
import bg5 from '../../../misc/backgrounds/5.png'
import bg6 from '../../../misc/backgrounds/6.png'
import bg7 from '../../../misc/backgrounds/7.png'
const backgrounds = [bg1, bg2, bg3, bg4, bg5, bg6, bg7]
export const PICTURES_COUNT = backgrounds.length
interface Props {
skin?: string
cape?: string
isAlex: boolean
showIndicator?: boolean
initPositionZ?: number
}
type ViewerStuff = {
handles: {
walk: skinview3d.AnimationHandle
run: skinview3d.AnimationHandle
rotate: skinview3d.AnimationHandle
}
control: skinview3d.OrbitControls
firstRun: boolean
}
const emptyStuff: ViewerStuff = {
handles: {
walk: {} as skinview3d.AnimationHandle,
run: {} as skinview3d.AnimationHandle,
rotate: {} as skinview3d.AnimationHandle,
},
control: {} as skinview3d.OrbitControls,
firstRun: true,
}
const ActionButton = styled.i`
display: inline;
padding: 0.5em 0.5em;
&:hover {
color: #555;
cursor: pointer;
}
`
const cssCardBody = css`
background-repeat: no-repeat;
background-position: center;
background-size: cover;
`
const cssViewer = css`
${breakpoints.greaterThan(breakpoints.Breakpoint.lg)} {
min-height: 500px;
}
canvas {
cursor: move;
}
`
const Viewer: React.FC<Props> = (props) => {
const { initPositionZ = 70 } = props
const viewRef: React.MutableRefObject<skinview3d.SkinViewer> = useRef(null!)
const containerRef = useRef<HTMLDivElement>(null)
const stuffRef = useRef(emptyStuff)
const [paused, setPaused] = useState(false)
const [running, setRunning] = useState(false)
const [reset, setReset] = useState(0)
const [background, setBackground] = useState('#fff')
const [bgPicture, setBgPicture] = useState(0)
const indicator = (() => {
const { skin, cape } = props
if (skin && cape) {
return `${t('general.skin')} & ${t('general.cape')}`
} else if (skin) {
return t('general.skin')
} else if (cape) {
return t('general.cape')
}
return ''
})()
useEffect(() => {
const container = containerRef.current!
const viewer = new skinview3d.SkinViewer({
domElement: container,
width: container.clientWidth,
height: container.clientHeight,
skinUrl: props.skin || SkinSteve,
capeUrl: props.cape || '',
detectModel: false,
})
viewer.camera.position.z = initPositionZ
viewer.playerObject.skin.slim = props.isAlex
const animation = new skinview3d.CompositeAnimation()
stuffRef.current.handles = {
walk: animation.add(skinview3d.WalkingAnimation),
run: animation.add(skinview3d.RunningAnimation),
rotate: animation.add(skinview3d.RotatingAnimation),
}
stuffRef.current.handles.run.paused = true
// @ts-ignore
viewer.animation = animation as skinview3d.Animation
stuffRef.current.control = skinview3d.createOrbitControls(viewer)
if (!stuffRef.current.firstRun) {
const { handles } = stuffRef.current
handles.walk.paused = true
handles.run.paused = true
handles.rotate.paused = true
viewer.camera.position.z = 70
}
viewRef.current = viewer
return () => {
viewer.dispose()
stuffRef.current.firstRun = false
}
}, [reset])
useEffect(() => {
return () => {
stuffRef.current.firstRun = true
}
}, [])
useEffect(() => {
const viewer = viewRef.current
viewer.skinUrl = props.skin || SkinSteve
}, [props.skin])
useEffect(() => {
const viewer = viewRef.current
if (props.cape) {
viewer.capeUrl = props.cape
} else {
viewer.playerObject.cape.visible = false
}
}, [props.cape])
useEffect(() => {
const viewer = viewRef.current
viewer.playerObject.skin.slim = props.isAlex
}, [props.isAlex])
const togglePause = () => {
setPaused((paused) => !paused)
viewRef.current.animationPaused = !viewRef.current.animationPaused
}
const toggleRun = () => {
setRunning((running) => !running)
const { handles } = stuffRef.current
handles.run.paused = !handles.run.paused
handles.walk.paused = false
}
const toggleRotate = () => {
const { handles } = stuffRef.current
handles.rotate.paused = !handles.rotate.paused
}
const handleReset = () => {
setReset((c) => c + 1)
}
const setWhite = () => setBackground('#fff')
const setGray = () => setBackground('#6c757d')
const setBlack = () => setBackground('#000')
const setPrevPicture = () => {
let index = bgPicture
if (bgPicture <= 0) {
index = PICTURES_COUNT - 1
} else {
index -= 1
}
setBgPicture(index)
setBackground(`url('${backgrounds[index]}')`)
}
const setNextPicture = () => {
let index = bgPicture
if (bgPicture >= PICTURES_COUNT - 1) {
index = 1
} else {
index += 1
}
setBgPicture(index)
setBackground(`url('${backgrounds[index]}')`)
}
return (
<div className="card">
<div className="card-header">
<div className="d-flex justify-content-between">
<h3 className="card-title">
<span>{t('general.texturePreview')}</span>
{props.showIndicator && (
<span className="badge bg-olive ml-1">{indicator}</span>
)}
</h3>
<div>
<ActionButton
className={`fas fa-${running ? 'walking' : 'running'}`}
data-toggle="tooltip"
data-placement="bottom"
title={`${t('general.walk')} / ${t('general.run')}`}
onClick={toggleRun}
></ActionButton>
<ActionButton
className="fas fa-redo-alt"
data-toggle="tooltip"
data-placement="bottom"
title={t('general.rotation')}
onClick={toggleRotate}
></ActionButton>
<ActionButton
className={`fas fa-${paused ? 'play' : 'pause'}`}
data-toggle="tooltip"
data-placement="bottom"
title={t('general.pause')}
onClick={togglePause}
></ActionButton>
<ActionButton
className="fas fa-stop"
data-toggle="tooltip"
data-placement="bottom"
title={t('general.reset')}
onClick={handleReset}
></ActionButton>
</div>
</div>
</div>
<div className="card-body" css={cssCardBody} style={{ background }}>
<div ref={containerRef} css={cssViewer}></div>
</div>
<div className="card-footer">
<div className="mt-2 mb-3 d-flex">
<div
className="btn-color bg-white rounded-pill mr-2 elevation-2"
title={t('colors.white')}
onClick={setWhite}
/>
<div
className="btn-color bg-black rounded-pill mr-2 elevation-2"
title={t('colors.black')}
onClick={setBlack}
/>
<div
className="btn-color bg-gray rounded-pill mr-2 elevation-2"
title={t('colors.gray')}
onClick={setGray}
/>
<div
className="btn-color bg-green rounded-pill mr-2 elevation-2"
css={cssUtils.center}
title={t('colors.prev')}
onClick={setPrevPicture}
>
<i className="fas fa-arrow-left"></i>
</div>
<div
className="btn-color bg-green rounded-pill mr-2 elevation-2"
css={cssUtils.center}
title={t('colors.next')}
onClick={setNextPicture}
>
<i className="fas fa-arrow-right"></i>
</div>
</div>
{props.children}
</div>
</div>
)
}
export default Viewer