提交 7b6d9a2c authored 作者: lidongxu's avatar lidongxu

添加王小卤元素完成

上级 f2fef882
big-screen/public/images/bubble.png

346.8 KB | W: | H:

big-screen/public/images/bubble.png

463.6 KB | W: | H:

big-screen/public/images/bubble.png
big-screen/public/images/bubble.png
big-screen/public/images/bubble.png
big-screen/public/images/bubble.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -9,37 +9,37 @@ export const BUBBLE_RADIUS = SCREEN_WIDTH / 22 ...@@ -9,37 +9,37 @@ export const BUBBLE_RADIUS = SCREEN_WIDTH / 22
const ROW_HEIGHT = BUBBLE_RADIUS * Math.sqrt(3) const ROW_HEIGHT = BUBBLE_RADIUS * Math.sqrt(3)
/** /**
* 9 种泡泡颜色(颜色索引 1-9),用于爆炸粒子效果取色 * 9 种泡泡颜色(颜色索引 1-9),用于爆炸粒子效果取色。
* 排序与小程序端一致:按与鸡爪对比度从高到低排列。
*/ */
export const BUBBLE_COLORS = [ export const BUBBLE_COLORS = [
'', '',
'#E83030', '#2BC8E8', // 1 蓝
'#1DB85A', '#1DB85A', // 2 绿
'#2BC8E8', '#E8C000', // 3 黄
'#E8C000', '#8B35E0', // 4 紫
'#F07820', '#E060A0', // 5 粉
'#8B35E0', '#80C020', // 6 黄绿
'#E060A0', '#D8D0B0', // 7 奶白
'#D8D0B0', '#F07820', // 8 橙
'#80C020', '#E83030', // 9 红
] ]
/** /**
* 精灵图 bubble.png(1400×1400)中每个球的裁剪区域 * 精灵图 bubble.png(1400×1400)中每个球的裁剪区域
* 3×3 排列,从左上按行序编号 1-9 * 索引对应 colorIdx,映射到精灵图中正确的球(与小程序端一致)
* 字段:[sx, sy, sw, sh]
*/ */
const SPRITE_REGIONS = [ const SPRITE_REGIONS = [
null, null,
[159, 158, 296, 297], [1004, 562, 297, 296], // 1 蓝 ← 精灵图位置6(右中)
[581, 562, 297, 296], [581, 562, 297, 296], // 2 绿 ← 精灵图位置5(中中)
[1004, 562, 297, 296], [1004, 158, 297, 297], // 3 黄 ← 精灵图位置3(右上)
[1004, 158, 297, 297], [159, 965, 296, 297], // 4 紫 ← 精灵图位置7(左下)
[581, 158, 297, 297], [581, 965, 297, 297], // 5 粉 ← 精灵图位置8(中下)
[159, 965, 296, 297], [159, 562, 296, 296], // 6 黄绿 ← 精灵图位置4(左中)
[581, 965, 297, 297], [1004, 965, 297, 297], // 7 奶白 ← 精灵图位置9(右下)
[1004, 965, 297, 297], [581, 158, 297, 297], // 8 橙 ← 精灵图位置2(中上)
[159, 562, 296, 296], [159, 158, 296, 297], // 9 红 ← 精灵图位置1(左上)
] ]
// 浏览器端:使用 new Image() 加载精灵图 // 浏览器端:使用 new Image() 加载精灵图
......
/** /**
* 射击器渲染(移植自 minigame-1,仅绘制不含触摸) * 射击器渲染(大屏端,仅绘制不含触摸)
* 发射器底座、瞄准线、当前泡泡、下一颗预览泡泡 * 鸡身体朝向跟随瞄准方向倾斜 + 呼吸浮动 + 弹跳动画
*/ */
import { SCREEN_WIDTH, SCREEN_HEIGHT, SAFE_AREA_BOTTOM } from '../constants.js' import { SCREEN_WIDTH, SCREEN_HEIGHT, SAFE_AREA_BOTTOM } from '../constants.js'
import { BUBBLE_RADIUS, drawBubble3D } from './bubble.js' import { BUBBLE_RADIUS, drawBubble3D } from './bubble.js'
const CURRENT_Y_OFFSET = BUBBLE_RADIUS * 3.5 // 鸡的尺寸
const CHICKEN_WIDTH = BUBBLE_RADIUS * 5
const CHICKEN_HEIGHT = BUBBLE_RADIUS * 6
const CHICKEN_BOTTOM_MARGIN = BUBBLE_RADIUS * 0.8
const BUBBLE_TOP_OFFSET = BUBBLE_RADIUS * 0.4
const NEXT_X_OFFSET = BUBBLE_RADIUS * 3.2 const NEXT_X_OFFSET = BUBBLE_RADIUS * 3.2
const NEXT_Y_OFFSET = BUBBLE_RADIUS * 0.5 const NEXT_Y_OFFSET = BUBBLE_RADIUS * 0.5
const NEXT_SCALE = 0.65 const NEXT_SCALE = 0.65
/** // 鸡朝向参数
* 根据发射角度计算瞄准线折线点(含左右墙壁反弹,直到超出顶部) const MIN_ANGLE = 15 * Math.PI / 180
*/ const MAX_ANGLE = 165 * Math.PI / 180
const MAX_LEAN_ANGLE = 25 * Math.PI / 180
const LEAN_LERP = 0.15
const BOB_SPEED = 0.03
const BOB_AMOUNT = BUBBLE_RADIUS * 0.15
// 加载鸡图片
const _chickenImg = new Image()
_chickenImg.src = '/images/chicken.png'
// 模块级动画状态
let _currentLean = 0
let _bobPhase = 0
function calcAimPoints(originX, originY, angle) { function calcAimPoints(originX, originY, angle) {
const R = BUBBLE_RADIUS const R = BUBBLE_RADIUS
const points = [{ x: originX, y: originY }] const points = [{ x: originX, y: originY }]
let cx = originX let cx = originX, cy = originY
let cy = originY let vx = Math.cos(angle), vy = -Math.sin(angle)
let vx = Math.cos(angle)
let vy = -Math.sin(angle)
for (let i = 0; i < 6; i++) { for (let i = 0; i < 6; i++) {
const tLeft = vx < 0 ? (R - cx) / vx : Infinity const tLeft = vx < 0 ? (R - cx) / vx : Infinity
...@@ -36,37 +51,6 @@ function calcAimPoints(originX, originY, angle) { ...@@ -36,37 +51,6 @@ function calcAimPoints(originX, originY, angle) {
return points return points
} }
/**
* 绘制发射器底座和指向瞄准角度的箭头
*/
function drawLauncher(ctx, x, y, aimAngle) {
const R = BUBBLE_RADIUS
ctx.save()
ctx.beginPath()
ctx.arc(x, y, R * 1.25, 0, Math.PI)
ctx.fillStyle = 'rgba(255,255,255,0.12)'
ctx.fill()
ctx.strokeStyle = 'rgba(255,255,255,0.25)'
ctx.lineWidth = 1.5
ctx.stroke()
ctx.translate(x, y)
ctx.rotate(Math.PI / 2 - aimAngle)
const arrowTip = -R * 2.2
const arrowBase = -R * 0.8
ctx.beginPath()
ctx.moveTo(0, arrowTip)
ctx.lineTo(-R * 0.35, arrowBase)
ctx.lineTo(R * 0.35, arrowBase)
ctx.closePath()
ctx.fillStyle = 'rgba(255,255,255,0.5)'
ctx.fill()
ctx.restore()
}
/**
* 绘制虚线瞄准线
*/
function drawAimLine(ctx, aimPoints) { function drawAimLine(ctx, aimPoints) {
if (!aimPoints || aimPoints.length < 2) return if (!aimPoints || aimPoints.length < 2) return
ctx.save() ctx.save()
...@@ -85,29 +69,62 @@ function drawAimLine(ctx, aimPoints) { ...@@ -85,29 +69,62 @@ function drawAimLine(ctx, aimPoints) {
} }
/** /**
* 绘制射击器(底座、瞄准线、当前泡泡、下一颗预览) * 绘制射击器
* @param {CanvasRenderingContext2D} ctx * @param {CanvasRenderingContext2D} ctx
* @param {Object} shooterState { aimAngle, isAiming, currentColor, nextColor },可选 aimPoints;若不传则根据 aimAngle 计算 * @param {Object} shooterState { aimAngle, isAiming, currentColor, nextColor, bounceScaleX, bounceScaleY }
*/ */
export function drawShooter(ctx, shooterState) { export function drawShooter(ctx, shooterState) {
if (!shooterState) return if (!shooterState) return
const x = SCREEN_WIDTH / 2 const x = SCREEN_WIDTH / 2
const y = SCREEN_HEIGHT - CURRENT_Y_OFFSET - SAFE_AREA_BOTTOM const chickenBottom = SCREEN_HEIGHT - SAFE_AREA_BOTTOM - CHICKEN_BOTTOM_MARGIN
const bubbleBaseY = chickenBottom - CHICKEN_HEIGHT - BUBBLE_TOP_OFFSET
const R = BUBBLE_RADIUS const R = BUBBLE_RADIUS
const aimAngle = shooterState.aimAngle ?? Math.PI / 2 const aimAngle = shooterState.aimAngle ?? Math.PI / 2
const isAiming = shooterState.isAiming ?? false const isAiming = shooterState.isAiming ?? false
const currentColor = shooterState.currentColor ?? 1 const currentColor = shooterState.currentColor ?? 1
const nextColor = shooterState.nextColor ?? 1 const nextColor = shooterState.nextColor ?? 1
const bounceScaleX = shooterState.bounceScaleX ?? 1
const bounceScaleY = shooterState.bounceScaleY ?? 1
const bounceOffsetY = (1 - bounceScaleY) * CHICKEN_HEIGHT / 2
const aimPoints = shooterState.aimPoints ?? (isAiming ? calcAimPoints(x, y, aimAngle) : []) // 计算目标倾斜角度(跟随瞄准方向)
let targetLean = 0
if (isAiming) {
const normalized = (aimAngle - Math.PI / 2) / (MAX_ANGLE - Math.PI / 2)
targetLean = -normalized * MAX_LEAN_ANGLE
}
_currentLean += (targetLean - _currentLean) * LEAN_LERP
// 呼吸浮动
_bobPhase += BOB_SPEED
if (_bobPhase > Math.PI * 2) _bobPhase -= Math.PI * 2
const bobOffset = Math.sin(_bobPhase) * BOB_AMOUNT
// 瞄准线
const aimPoints = shooterState.aimPoints ?? (isAiming ? calcAimPoints(x, bubbleBaseY, aimAngle) : [])
drawAimLine(ctx, aimPoints) drawAimLine(ctx, aimPoints)
drawLauncher(ctx, x, y, aimAngle)
drawBubble3D(ctx, x, y, R, currentColor)
// 绘制鸡(以底部中心为旋转轴,跟随瞄准方向倾斜)
const bottomY = chickenBottom + bounceOffsetY + bobOffset
ctx.save()
ctx.translate(x, bottomY)
ctx.rotate(_currentLean)
ctx.scale(bounceScaleX, bounceScaleY)
ctx.drawImage(_chickenImg, -CHICKEN_WIDTH / 2, -CHICKEN_HEIGHT, CHICKEN_WIDTH, CHICKEN_HEIGHT)
ctx.restore()
// 当前待射泡泡(鸡头顶,跟随倾斜)
const headX = x + Math.sin(_currentLean) * CHICKEN_HEIGHT * bounceScaleY
const headY = bottomY - Math.cos(_currentLean) * CHICKEN_HEIGHT * bounceScaleY - BUBBLE_TOP_OFFSET
drawBubble3D(ctx, headX, headY, R, currentColor)
// 下一颗预览泡泡
const nextR = R * NEXT_SCALE const nextR = R * NEXT_SCALE
const nextX = x + NEXT_X_OFFSET const nextX = x + NEXT_X_OFFSET
const nextY = y + NEXT_Y_OFFSET const nextY = chickenBottom + NEXT_Y_OFFSET
drawBubble3D(ctx, nextX, nextY, nextR, nextColor) drawBubble3D(ctx, nextX, nextY, nextR, nextColor)
ctx.save() ctx.save()
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论