@@ -40,7 +40,14 @@ import {
4040 makeMatrix ,
4141} from "../engine/util" ;
4242import { callbacks } from "./callbacks" ;
43- import { Goban , GobanMetrics , GobanSelectedThemes , GOBAN_FONT } from "./Goban" ;
43+ import {
44+ Goban ,
45+ GobanMetrics ,
46+ GobanSelectedThemes ,
47+ GOBAN_FONT ,
48+ CaptureDisplayConfig ,
49+ CaptureDisplay ,
50+ } from "./Goban" ;
4451
4552const __theme_cache : {
4653 [ bw : string ] : { [ name : string ] : { [ size : string ] : any } } ;
@@ -3763,6 +3770,137 @@ export class GobanCanvas extends Goban implements GobanCanvasInterface {
37633770 this . move_tree_drawPath ( ctx , node , viewport ) ;
37643771 //}
37653772 }
3773+
3774+ //
3775+ // Score display (captured stones)
3776+ //
3777+ public createCaptureDisplay ( config : CaptureDisplayConfig ) : CaptureDisplay {
3778+ const canvas = document . createElement ( "canvas" ) ;
3779+ const ctx = canvas . getContext ( "2d" ) ;
3780+ if ( ! ctx ) {
3781+ throw new Error ( "Failed to get 2d context for score display canvas" ) ;
3782+ }
3783+
3784+ const normalizedConfig = this . normalizeCaptureConfig ( config ) ;
3785+ let currentCount = normalizedConfig . stone_count ;
3786+
3787+ const render = ( ) => {
3788+ this . renderCaptureDisplay ( canvas , ctx , normalizedConfig , currentCount ) ;
3789+ } ;
3790+
3791+ render ( ) ;
3792+
3793+ return {
3794+ element : canvas ,
3795+ updateStoneCount : ( count : number ) => {
3796+ currentCount = Math . max ( 0 , Math . round ( count ) ) ;
3797+ normalizedConfig . stone_count = currentCount ;
3798+ render ( ) ;
3799+ } ,
3800+ destroy : ( ) => {
3801+ if ( canvas . parentNode ) {
3802+ canvas . parentNode . removeChild ( canvas ) ;
3803+ }
3804+ } ,
3805+ } ;
3806+ }
3807+
3808+ private renderCaptureDisplay (
3809+ canvas : HTMLCanvasElement ,
3810+ ctx : CanvasRenderingContext2D ,
3811+ config : Required < CaptureDisplayConfig > ,
3812+ count : number ,
3813+ ) : void {
3814+ const { radius, displayCount, stone_spacing, width, height } =
3815+ this . calculateCaptureDisplayDimensions ( config , count ) ;
3816+
3817+ canvas . width = width ;
3818+ canvas . height = height ;
3819+ canvas . style . width = `${ width } px` ;
3820+ canvas . style . height = `${ height } px` ;
3821+
3822+ ctx . clearRect ( 0 , 0 , width , height ) ;
3823+
3824+ if ( displayCount === 0 ) {
3825+ return ;
3826+ }
3827+
3828+ const stones = this . getCaptureDisplayStones ( config ) ;
3829+ if ( ! stones || stones . length === 0 ) {
3830+ return ;
3831+ }
3832+
3833+ const theme = config . stone_color === "black" ? this . theme_black : this . theme_white ;
3834+ const placeStone =
3835+ config . stone_color === "black"
3836+ ? theme . placeBlackStone . bind ( theme )
3837+ : theme . placeWhiteStone . bind ( theme ) ;
3838+
3839+ const cy = radius ;
3840+
3841+ for ( let i = 0 ; i < displayCount ; i ++ ) {
3842+ const cx = radius + i * stone_spacing ;
3843+ const stone_idx = ( i * 31 ) % stones . length ;
3844+ const stone = stones [ stone_idx ] ;
3845+ placeStone ( ctx , null , stone , cx , cy , radius ) ;
3846+ }
3847+ }
3848+
3849+ private getCaptureDisplayStones ( config : Required < CaptureDisplayConfig > ) : any [ ] {
3850+ const color = config . stone_color ;
3851+ const themeName = color === "black" ? this . themes . black : this . themes . white ;
3852+ const radius = config . stone_radius ;
3853+
3854+ this . ensureCaptureDisplayStonesPreRendered ( config ) ;
3855+
3856+ return __theme_cache [ color ] [ themeName ] [ radius ] ;
3857+ }
3858+
3859+ private ensureCaptureDisplayStonesPreRendered ( config : Required < CaptureDisplayConfig > ) : void {
3860+ const color = config . stone_color ;
3861+ const themeName = color === "black" ? this . themes . black : this . themes . white ;
3862+ const theme = color === "black" ? this . theme_black : this . theme_white ;
3863+ const radius = config . stone_radius ;
3864+
3865+ if ( ! __theme_cache [ color ] [ themeName ] ) {
3866+ __theme_cache [ color ] [ themeName ] = {
3867+ creation_order : [ ] ,
3868+ } ;
3869+ }
3870+
3871+ if ( ! __theme_cache [ color ] [ themeName ] [ radius ] ) {
3872+ const callback = ( ) => {
3873+ /* Stones are pre-rendered, no need for deferred callback */
3874+ } ;
3875+ __theme_cache [ color ] [ themeName ] [ radius ] =
3876+ color === "black"
3877+ ? theme . preRenderBlack ( radius , Math . random ( ) , callback )
3878+ : theme . preRenderWhite ( radius , Math . random ( ) , callback ) ;
3879+ __theme_cache [ color ] [ themeName ] . creation_order . push ( radius ) ;
3880+
3881+ // Evict old cache entries if we've exceeded the cache size
3882+ let max_cache_size = 24 ;
3883+ try {
3884+ // iOS devices have memory constraints, use smaller cache
3885+ if (
3886+ / i P ( a d | h o n e | o d ) .+ ( V e r s i o n \/ [ \d . ] | O S \d .* l i k e m a c o s x ) + .* S a f a r i / i. test (
3887+ navigator . userAgent ,
3888+ )
3889+ ) {
3890+ max_cache_size = 12 ;
3891+ }
3892+ } catch ( e ) {
3893+ // Ignore errors
3894+ }
3895+
3896+ if ( __theme_cache [ color ] [ themeName ] . creation_order . length > max_cache_size ) {
3897+ const old_radius = __theme_cache [ color ] [ themeName ] . creation_order . shift ( ) ;
3898+ if ( old_radius ) {
3899+ delete __theme_cache [ color ] [ themeName ] [ old_radius ] ;
3900+ }
3901+ }
3902+ }
3903+ }
37663904}
37673905
37683906const fitTextCache : { [ key : string ] : [ number , string , TextMetrics ] } = { } ;
0 commit comments