diff --git a/packages/Chinese-chess/public/STXINGKAI.ttf b/packages/Chinese-chess/public/STXINGKAI.ttf new file mode 100644 index 0000000..353ba6a Binary files /dev/null and b/packages/Chinese-chess/public/STXINGKAI.ttf differ diff --git a/packages/Chinese-chess/public/sword.png b/packages/Chinese-chess/public/sword.png new file mode 100644 index 0000000..d15f1a5 Binary files /dev/null and b/packages/Chinese-chess/public/sword.png differ diff --git a/packages/Chinese-chess/public/win.png b/packages/Chinese-chess/public/win.png new file mode 100644 index 0000000..8ad28c3 Binary files /dev/null and b/packages/Chinese-chess/public/win.png differ diff --git a/packages/Chinese-chess/src/App.vue b/packages/Chinese-chess/src/App.vue index a972ec3..17caceb 100644 --- a/packages/Chinese-chess/src/App.vue +++ b/packages/Chinese-chess/src/App.vue @@ -15,6 +15,8 @@ @game:ready="handleGameReady" @game:change="handleGameChange" /> + + @@ -24,6 +26,7 @@ import { GameMode } from './definitions' import Message from './components/Message' import { getMoveList } from './utils' +import { createAnimation } from './libs/Animation' // eslint-disable-next-line @typescript-eslint/promise-function-async const Gamelobby = defineAsyncComponent(() => import('./pages/GameLobby.vue')) @@ -43,6 +46,66 @@ const comp = computed(() => { onMounted(() => { Message.install() ;(window as any).getMoveList = getMoveList + + // @todo + // 测试动画 + const c = document.querySelector('#canvas')! + const ctx = c.getContext('2d')! + const font = new FontFace('STXINGKAI', 'url(STXINGKAI.ttf)') + // eslint-disable-next-line @typescript-eslint/promise-function-async + const loadPic = (pic: string): Promise => { + return new Promise(resolve => { + const swordPic = new Image() + swordPic.onload = () => { + resolve(swordPic) + } + swordPic.src = pic + }) + } + font.load().then(f => { + (document.fonts as any).add(f) + }).then(async () => await document.fonts.ready.then()) + .then(async () => { + const swordPic = await loadPic('sword.png') + const winPic = await loadPic('win.png') + // const { run, stop } = registerCheckAnimation(ctx, 400, 300) + // const { run, stop } = registerCheckMateAnimation(ctx, 400, 300, swordPic) + // const { run, stop } = registerWinnerAnimation(ctx, 400, 300, winPic, 0) + // const { run, stop } = registerWinnerAnimation(ctx, 400, 300, winPic, 1) + // run() + + const animations = createAnimation(ctx, { + width: 400, + height: 300, + resource: { + swordPic, + winPic + }, + stopCallback: (name, camp) => { + console.log('stop: ', name) + switch (name) { + case 'check': + animations.checkMate.run() + break + case 'check-mate': + animations.redWin.run() + break + case 'win': + if (camp === 0) { + animations.blackWin.run() + } else { + // console.log('黑GG') + animations.check.run() + } + break + default: + break + } + } + }) + + animations.check.run() + }) }) const { diff --git a/packages/Chinese-chess/src/libs/Animation.ts b/packages/Chinese-chess/src/libs/Animation.ts new file mode 100644 index 0000000..90eecf0 --- /dev/null +++ b/packages/Chinese-chess/src/libs/Animation.ts @@ -0,0 +1,352 @@ +import { Camp } from '@/definitions' + +interface TextItem { + text: string + x: number + y: number + ratio: number + color: string +} + +interface ImageItem { + img: HTMLImageElement + x: number + y: number + alpha: number +} + +type Circle = [number, number, number] + +interface Value { + text?: TextItem[] + circle?: Circle[] + image?: ImageItem +} + +export type AnimationType = 'check' | 'check-mate' | 'win' + +export interface AnimationOptions { + width: number + height: number + resource: { + swordPic: HTMLImageElement + winPic: HTMLImageElement + } + stopCallback: (type: AnimationType, camp?: Camp) => void +} + +interface AnimationExecutor { + run: () => void + stop: () => void +} + +export interface AnimationReturnType { + check: AnimationExecutor + checkMate: AnimationExecutor + blackWin: AnimationExecutor + redWin: AnimationExecutor +} + +export const createAnimation = ( + ctx: CanvasRenderingContext2D, + { width, height, resource, stopCallback }: AnimationOptions +): AnimationReturnType => { + function drawText (text: string, x: number, y: number, ratio = 1, color = '#f6d59a'): void { + ctx.save() + ctx.scale(ratio, ratio) + ctx.beginPath() + + const gradient = ctx.createLinearGradient(-0, -2 * Math.abs(x), 0, 2 * Math.abs(y)) + + gradient.addColorStop(0, '#fff') + gradient.addColorStop(0.5, color) + gradient.addColorStop(1, '#fff') + + ctx.fillStyle = gradient + ctx.strokeStyle = 'rgba(0,0,0,0.6)' + ctx.lineWidth = 1 + + ctx.font = 'normal 64px STXINGKAI' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + + ctx.shadowBlur = 6 + ctx.shadowColor = 'rgba(0,0,0,0.4)' + ctx.shadowOffsetX = 2 + ctx.shadowOffsetY = 4 + + ctx.fillText(text, x, y) + ctx.shadowBlur = 0 + ctx.shadowColor = 'rgba(0,0,0,0)' + ctx.strokeText(text, x, y) + ctx.restore() + } + + function drawCirclePoint (x: number, y: number, r: number): void { + ctx.beginPath() + ctx.strokeStyle = 'black' + ctx.arc(x, y, r, 0, Math.PI * 2) + ctx.fill() + } + + function drawImage ({ img, x, y, alpha }: ImageItem): void { + ctx.save() + ctx.globalAlpha = alpha + ctx.drawImage(img, x, y) + ctx.restore() + } + + function clear (): void { + ctx.clearRect(0, 0, width, height) + } + + function draw (value: Value): void { + ctx.save() + ctx.translate(width / 2, height / 2) + + const { text, circle, image } = value + + if (image) { + drawImage(image) + } + if (circle) { + circle.forEach(item => { + drawCirclePoint(...item) + }) + } + + if (text) { + text.forEach(item => { + drawText(item.text, item.x, item.y, item.ratio, item.color) + }) + } + + ctx.restore() + } + + const registerCheckAnimation = (): AnimationReturnType['check'] => { + function init (): { + points: Circle[] + textList: TextItem[] + value: Value + } { + let r = 5 + const outerRadius = 32 + const step = Math.PI / 180 + const points: Circle[] = Array.from({ length: 145 }, (_, index) => { + // eslint-disable-next-line no-return-assign + return [ + outerRadius * Math.cos(index * 2 * step), + outerRadius * Math.sin(index * 2 * step), + (r -= step * 2, Math.max(1, r + step * 2)) + ] as Circle + }) + + const textList = [ + { + text: '将', + x: -16, + y: -12, + ratio: 2, + color: '#f6d59a' + }, + { + text: '军', + x: 16, + y: 12, + ratio: 2, + color: '#f6d59a' + } + ] + + const value: Value = { + text: [textList.shift()!], + circle: [] + } + + return { + value, + textList, + points + } + } + + function run (value: Value, textList: TextItem[], points: Circle[]): void { + reqId = requestAnimationFrame(() => run(value, textList, points)) + + const lastText = value.text!.at(-1) + if (lastText!.ratio > 1) { + lastText!.ratio -= 0.1 + } else if (textList.length > 0) { + value.text!.push(textList.shift()!) + } else { + value.circle!.push( + ...points.splice(0, 10) + ) + } + + clear() + draw(value) + + if (points.length <= 0) { + stop() + } + } + + function stop (): void { + cancelAnimationFrame(reqId) + stopCallback('check') + } + + return { + run: () => { + const { value, textList, points } = init() + run(value, textList, points) + }, + stop + } + } + + const registerCheckMateAnimation = (img: HTMLImageElement): AnimationReturnType['checkMate'] => { + function init (): { + textList: TextItem[] + value: Value + } { + const textList = [ + { + text: '绝', + x: -16, + y: -12, + ratio: 2, + color: '#f40' + }, + { + text: '杀', + x: 16, + y: 12, + ratio: 2, + color: '#f40' + } + ] + + const value = { + text: [textList.shift()!], + image: { + img, + x: -45, + y: -64, + alpha: 0 + } + } + return { + textList, + value + } + } + + function run (value: Value, textList: TextItem[]): void { + reqId = requestAnimationFrame(() => run(value, textList)) + + const lastText = value.text!.at(-1) + if (lastText!.ratio > 1) { + lastText!.ratio -= 0.1 + } else if (textList.length > 0) { + value.text!.push(textList.shift()!) + } else { + value.image!.alpha += 0.05 + } + + clear() + draw(value) + + if (value.image!.alpha > 1) { + stop() + } + } + + function stop (): void { + cancelAnimationFrame(reqId) + stopCallback('check-mate') + } + + return { + run: () => { + const { textList, value } = init() + run(value, textList) + }, + stop + } + } + + const registerWinAnimation = (img: HTMLImageElement, camp: Camp): AnimationReturnType['blackWin'] => { + function init (): { text: TextItem, value: Value } { + const text: TextItem = { + text: `${camp === 0 ? '红' : '黑'}胜`, + x: 0, + y: 32, + ratio: 2, + color: camp === 0 ? '#f40' : '#000' + } + + const value: Value = { + image: { + img, + x: -145, + y: -96, + alpha: 0 + }, + text: [] + } + + return { + text, + value + } + } + + function run (text: TextItem, value: Value): void { + reqId = requestAnimationFrame(() => run(text, value)) + + if (value.image!.alpha < 1) { + value.image!.alpha += 0.05 + } else { + if (value.text!.length === 0) { + value.text!.push(text) + } + const lastText = value.text!.at(-1)! + if (lastText.ratio > 1) { + lastText.ratio -= 0.1 + } + } + + clear() + draw(value) + + if (value.text?.at(-1) && value.text.at(-1)!.ratio <= 1) { + stop() + } + } + + function stop (): void { + cancelAnimationFrame(reqId) + stopCallback('win', camp) + } + + return { + run: () => { + const { text, value } = init() + run(text, value) + }, + stop + } + } + + let reqId: number + + return { + check: registerCheckAnimation(), + checkMate: registerCheckMateAnimation(resource.swordPic), + redWin: registerWinAnimation(resource.winPic, 0), + blackWin: registerWinAnimation(resource.winPic, 1) + } +} diff --git a/packages/Chinese-chess/src/pages/OfflineGame.vue b/packages/Chinese-chess/src/pages/OfflineGame.vue index 2509c3e..e391a6f 100644 --- a/packages/Chinese-chess/src/pages/OfflineGame.vue +++ b/packages/Chinese-chess/src/pages/OfflineGame.vue @@ -32,14 +32,20 @@ onMounted(() => { chatListRef.value && chatInputRef.value ) { - const ctrl = createController({ - oMain: mainRef.value, - oManual: manualRef.value, - oChatList: chatListRef.value, - oChatInput: chatInputRef.value - }) - ctrl.initGame() - ctrl.run() + const font = new FontFace('PieceFont', 'url(fzlsft.ttf)') + font.load().then(f => { + (document.fonts as any).add(f) + }).then(async () => await document.fonts.ready.then()) + .then(() => { + const ctrl = createController({ + oMain: mainRef.value!, + oManual: manualRef.value!, + oChatList: chatListRef.value!, + oChatInput: chatInputRef.value! + }) + ctrl.initGame() + ctrl.run() + }) } })