package com.projectsacrifice.game; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; /** Canvas-only adapter for the normalized volunteer skeleton. */ final class VolunteerCanvasRenderer { private static final int FRAME_COUNT = 16; private static final VolunteerRig.Pose[][][][] POSES = buildPoseCache(); void draw(Canvas canvas, Paint fill, Paint outline, Path path, float x, float y, float width, float height, VolunteerRig.Skin skin, VolunteerRig.Motion motion, float phase, int facing, float alpha, boolean highContrast) { VolunteerRig.Pose pose = cachedPose(skin, motion, phase, facing); int frameColor = highContrast ? Color.WHITE : pose.palette().frame(); int shellColor = highContrast ? Color.rgb(255, 214, 77) : pose.palette().shell(); int clothColor = highContrast ? Color.WHITE : pose.palette().cloth(); int glowColor = highContrast ? Color.BLACK : pose.palette().glow(); outline.setStyle(Paint.Style.STROKE); outline.setStrokeCap(Paint.Cap.ROUND); outline.setStrokeWidth(Math.max(3f, width * 0.075f)); outline.setColor(withAlpha(frameColor, alpha)); line(canvas, outline, pose, VolunteerRig.Joint.HIPS, VolunteerRig.Joint.CHEST, x, y, width, height); line(canvas, outline, pose, VolunteerRig.Joint.LEFT_SHOULDER, VolunteerRig.Joint.LEFT_HAND, x, y, width, height); line(canvas, outline, pose, VolunteerRig.Joint.RIGHT_SHOULDER, VolunteerRig.Joint.RIGHT_HAND, x, y, width, height); line(canvas, outline, pose, VolunteerRig.Joint.HIPS, VolunteerRig.Joint.LEFT_KNEE, x, y, width, height); line(canvas, outline, pose, VolunteerRig.Joint.LEFT_KNEE, VolunteerRig.Joint.LEFT_FOOT, x, y, width, height); line(canvas, outline, pose, VolunteerRig.Joint.HIPS, VolunteerRig.Joint.RIGHT_KNEE, x, y, width, height); line(canvas, outline, pose, VolunteerRig.Joint.RIGHT_KNEE, VolunteerRig.Joint.RIGHT_FOOT, x, y, width, height); float chestX = px(pose, VolunteerRig.Joint.CHEST, x, width); float chestY = py(pose, VolunteerRig.Joint.CHEST, y, height); float hipsX = px(pose, VolunteerRig.Joint.HIPS, x, width); float hipsY = py(pose, VolunteerRig.Joint.HIPS, y, height); float headX = px(pose, VolunteerRig.Joint.HEAD, x, width); float headY = py(pose, VolunteerRig.Joint.HEAD, y, height); fill.setColor(withAlpha(clothColor, alpha * 0.86f)); path.reset(); path.moveTo(chestX - width * 0.22f * facing, chestY + height * 0.04f); path.lineTo(hipsX - width * 0.36f * facing, hipsY + height * 0.17f); path.lineTo(hipsX, hipsY + height * 0.06f); path.close(); canvas.drawPath(path, fill); fill.setColor(withAlpha(shellColor, alpha)); canvas.drawRoundRect(chestX - width * 0.22f, chestY - height * 0.08f, chestX + width * 0.22f, hipsY + height * 0.04f, width * 0.13f, width * 0.13f, fill); canvas.drawCircle(headX, headY, width * 0.22f, fill); outline.setStrokeWidth(Math.max(2f, width * 0.045f)); canvas.drawCircle(headX, headY, width * 0.22f, outline); canvas.drawRoundRect(chestX - width * 0.22f, chestY - height * 0.08f, chestX + width * 0.22f, hipsY + height * 0.04f, width * 0.13f, width * 0.13f, outline); fill.setColor(withAlpha(pose.visorLit() ? glowColor : frameColor, pose.visorLit() ? alpha : alpha * 0.72f)); canvas.drawRoundRect(headX - width * 0.14f, headY - height * 0.025f, headX + width * 0.14f, headY + height * 0.025f, width * 0.025f, width * 0.025f, fill); drawJoint(canvas, fill, pose, VolunteerRig.Joint.LEFT_SHOULDER, x, y, width, height, frameColor, alpha); drawJoint(canvas, fill, pose, VolunteerRig.Joint.RIGHT_SHOULDER, x, y, width, height, frameColor, alpha); drawJoint(canvas, fill, pose, VolunteerRig.Joint.LEFT_KNEE, x, y, width, height, frameColor, alpha); drawJoint(canvas, fill, pose, VolunteerRig.Joint.RIGHT_KNEE, x, y, width, height, frameColor, alpha); if (pose.anchorBolt()) { fill.setColor(withAlpha(glowColor, alpha)); canvas.drawCircle(hipsX, hipsY, width * 0.14f, fill); outline.setColor(withAlpha(frameColor, alpha)); canvas.drawLine(hipsX - width * 0.1f, hipsY, hipsX + width * 0.1f, hipsY, outline); canvas.drawLine(hipsX, hipsY - width * 0.1f, hipsX, hipsY + width * 0.1f, outline); } if (pose.frozenShell()) drawFrozenShell(canvas, fill, outline, x, y, width, height, alpha); if (pose.conductiveArcs()) drawConductiveArcs(canvas, outline, path, x, y, width, height, phase, alpha); if (motion == VolunteerRig.Motion.RESPAWN) drawAssemblyRings(canvas, outline, x, y, width, height, pose.assemblyProgress(), alpha); } private static void drawFrozenShell(Canvas canvas, Paint fill, Paint outline, float x, float y, float width, float height, float alpha) { fill.setColor(withAlpha(Color.rgb(85, 205, 236), alpha * 0.34f)); canvas.drawRoundRect(x - width * 0.12f, y + height * 0.02f, x + width * 1.12f, y + height, width * 0.12f, width * 0.12f, fill); outline.setColor(withAlpha(Color.rgb(218, 252, 255), alpha)); outline.setStrokeWidth(Math.max(2f, width * 0.045f)); canvas.drawLine(x, y + height * 0.18f, x + width * 0.28f, y + height * 0.45f, outline); canvas.drawLine(x + width, y + height * 0.16f, x + width * 0.72f, y + height * 0.52f, outline); } private static void drawConductiveArcs(Canvas canvas, Paint outline, Path path, float x, float y, float width, float height, float phase, float alpha) { outline.setColor(withAlpha(Color.rgb(255, 239, 74), alpha)); outline.setStrokeWidth(Math.max(3f, width * 0.055f)); float pulse = ((int) (phase * 12f) & 1) == 0 ? 0f : width * 0.06f; path.reset(); path.moveTo(x - width * 0.15f, y + height * 0.3f); path.lineTo(x + width * 0.08f, y + height * 0.22f + pulse); path.lineTo(x - width * 0.02f, y + height * 0.52f); path.lineTo(x + width * 0.18f, y + height * 0.68f - pulse); canvas.drawPath(path, outline); path.reset(); path.moveTo(x + width * 1.15f, y + height * 0.28f); path.lineTo(x + width * 0.91f, y + height * 0.43f - pulse); path.lineTo(x + width * 1.03f, y + height * 0.66f); path.lineTo(x + width * 0.82f, y + height * 0.77f + pulse); canvas.drawPath(path, outline); } private static void drawAssemblyRings(Canvas canvas, Paint outline, float x, float y, float width, float height, float progress, float alpha) { outline.setColor(withAlpha(Color.rgb(116, 232, 213), alpha * (1f - progress * 0.3f))); outline.setStrokeWidth(Math.max(2f, width * 0.045f)); float cx = x + width * 0.5f; float cy = y + height * 0.55f; canvas.drawCircle(cx, cy, width * (0.25f + progress * 0.45f), outline); canvas.drawCircle(cx, cy, width * (0.48f + progress * 0.35f), outline); } private static void drawJoint(Canvas canvas, Paint fill, VolunteerRig.Pose pose, VolunteerRig.Joint joint, float x, float y, float width, float height, int color, float alpha) { fill.setColor(withAlpha(color, alpha)); canvas.drawCircle(px(pose, joint, x, width), py(pose, joint, y, height), Math.max(2.5f, width * 0.065f), fill); } private static void line(Canvas canvas, Paint outline, VolunteerRig.Pose pose, VolunteerRig.Joint from, VolunteerRig.Joint to, float x, float y, float width, float height) { canvas.drawLine(px(pose, from, x, width), py(pose, from, y, height), px(pose, to, x, width), py(pose, to, y, height), outline); } private static float px(VolunteerRig.Pose pose, VolunteerRig.Joint joint, float x, float width) { return x + pose.x(joint) * width; } private static float py(VolunteerRig.Pose pose, VolunteerRig.Joint joint, float y, float height) { return y + pose.y(joint) * height; } private static VolunteerRig.Pose cachedPose(VolunteerRig.Skin skin, VolunteerRig.Motion motion, float phase, int facing) { int frame = Math.min(FRAME_COUNT - 1, Math.max(0, (int) ((phase - Math.floor(phase)) * FRAME_COUNT))); return POSES[skin.ordinal()][motion.ordinal()][facing < 0 ? 0 : 1][frame]; } private static VolunteerRig.Pose[][][][] buildPoseCache() { VolunteerRig.Skin[] skins = VolunteerRig.Skin.values(); VolunteerRig.Motion[] motions = VolunteerRig.Motion.values(); VolunteerRig.Pose[][][][] result = new VolunteerRig.Pose[skins.length][motions.length][2][FRAME_COUNT]; for (VolunteerRig.Skin skin : skins) { for (VolunteerRig.Motion motion : motions) { for (int facingIndex = 0; facingIndex < 2; facingIndex++) { for (int frame = 0; frame < FRAME_COUNT; frame++) { result[skin.ordinal()][motion.ordinal()][facingIndex][frame] = VolunteerRig.pose(skin, motion, frame / (float) (FRAME_COUNT - 1), facingIndex == 0 ? -1 : 1); } } } } return result; } private static int withAlpha(int color, float alpha) { int clamped = Math.max(0, Math.min(255, Math.round(Color.alpha(color) * alpha))); return Color.argb(clamped, Color.red(color), Color.green(color), Color.blue(color)); } }