1- import { spriteURL } from '@deities/art/Sprites.tsx' ;
1+ import { spriteImage } from '@deities/art/Sprites.tsx' ;
22import { UnitInfo } from '@deities/athena/info/Unit.tsx' ;
33import {
44 DynamicPlayerID ,
55 encodeDynamicPlayerID ,
66} from '@deities/athena/map/Player.tsx' ;
77import clipBorder from '@deities/ui/clipBorder.tsx' ;
8- import { CSSVariables } from '@deities/ui/cssVar.tsx' ;
9- import { css , cx , keyframes } from '@emotion/css' ;
10- import { memo } from 'react' ;
8+ import { css , cx } from '@emotion/css' ;
9+ import { memo , useLayoutEffect , useRef } from 'react' ;
1110import { useSprites } from '../hooks/useSprites.tsx' ;
1211
1312export const PortraitWidth = 55 ;
@@ -30,68 +29,76 @@ export default memo(function Portrait({
3029 unit : UnitInfo ;
3130 variant ?: number ;
3231} ) {
32+ const ref = useRef < HTMLCanvasElement > ( null ) ;
3333 const hasPortraits = useSprites ( 'portraits' ) ;
34- const sprite = hasPortraits
35- ? spriteURL ( 'Portraits' , encodeDynamicPlayerID ( player ) )
36- : '' ;
37-
3834 const { position } = unit . sprite . portrait ;
39- const y = - ( position . y + ( variant || 0 ) ) * PortraitHeight + 'px' ;
40- const alternateY = - ( position . y + ( variant || 0 ) + 6 ) * PortraitHeight + 'px' ;
35+
36+ useLayoutEffect ( ( ) => {
37+ if ( ! hasPortraits ) {
38+ return ;
39+ }
40+
41+ const canvas = ref . current ;
42+ if ( ! canvas ) {
43+ return ;
44+ }
45+
46+ const image = spriteImage ( 'Portraits' , encodeDynamicPlayerID ( player ) ) ;
47+ const context = canvas . getContext ( '2d' ) ! ;
48+ const positions = [
49+ {
50+ x : position . x * PortraitWidth ,
51+ y : ( position . y + ( variant || 0 ) ) * PortraitHeight ,
52+ } ,
53+ {
54+ x : position . x * PortraitWidth ,
55+ y : ( position . y + ( variant || 0 ) + 6 ) * PortraitHeight ,
56+ } ,
57+ ] ;
58+ let currentPosition =
59+ ( ( Number ( document . timeline . currentTime ) || 0 ) / 1000 ) % 1 < 0.5 ? 0 : 1 ;
60+
61+ const draw = ( ) => {
62+ context . drawImage (
63+ image ,
64+ positions [ currentPosition ] . x ,
65+ positions [ currentPosition ] . y ,
66+ PortraitWidth ,
67+ PortraitHeight ,
68+ 0 ,
69+ 0 ,
70+ PortraitWidth ,
71+ PortraitHeight ,
72+ ) ;
73+ currentPosition = ( currentPosition + 1 ) % positions . length ;
74+ } ;
75+
76+ draw ( ) ;
77+
78+ if ( animate && ! paused ) {
79+ const interval = setInterval ( draw , 1000 / positions . length ) ;
80+ return ( ) => clearInterval ( interval ) ;
81+ }
82+ } , [ hasPortraits , animate , paused , player , position , variant ] ) ;
4183
4284 return (
43- < div
85+ < canvas
4486 className = { cx ( portraitStyle , clip && clipStyle ) }
87+ height = { PortraitHeight }
88+ ref = { ref }
4589 style = { {
46- [ vars . set ( 'x' ) ] : - position . x * PortraitWidth + 'px' ,
47- [ vars . set ( 'y' ) ] : y ,
48- [ vars . set ( 'alternate-y' ) ] : alternateY ,
49- height : PortraitHeight ,
50- width : PortraitWidth ,
5190 zoom : scale ,
5291 } }
53- >
54- { sprite && (
55- < img
56- className = { cx (
57- spriteStyle ,
58- animate && animateStyle ,
59- paused && pausedStyle ,
60- ) }
61- src = { sprite }
62- />
63- ) }
64- </ div >
92+ width = { PortraitWidth }
93+ />
6594 ) ;
6695} ) ;
6796
68- const vars = new CSSVariables < 'x' | 'y' | 'alternate-y' > ( 'p' ) ;
69-
7097const portraitStyle = css `
71- contain : content;
7298 image-rendering : pixelated;
7399 pointer-events : none;
74100` ;
75101
76102const clipStyle = css `
77103 ${ clipBorder ( 2 ) }
78104` ;
79-
80- const spriteStyle = css `
81- transform : translate3d(${ vars . apply ( 'x' ) } , ${ vars . apply ( 'y' ) } , 0 );
82- ` ;
83-
84- const animateStyle = css `
85- animation : ${ keyframes `
86- 0%, 100% {
87- transform : translate3d(${ vars . apply ( 'x' ) } , ${ vars . apply ( 'y' ) } , 0 );
88- }
89- 50% {
90- transfor m: translate3d(${ vars . apply ( 'x' ) } , ${ vars . apply ( 'alternate-y' ) } , 0);
91- }
92- ` } 1s infinite steps(1);
93- ` ;
94-
95- const pausedStyle = css `
96- animation- play- state: paused;
97- ` ;
0 commit comments