Visual polish and game design skill for browser games. Audits atmosphere, juice, color palette, animations, and player feedback, then implements spectacle effects and visual improvements that make a game feel alive.
When this skill is invoked:
A great browser game is felt before it is understood. The goal is to make every interaction produce a visible, satisfying response. This skill focuses on six core principles:
Read every file in the game project. Understand the complete codebase before suggesting changes. Specifically:
Present a brief summary of the game's architecture and current visual state.
Score each of the following design areas on a scale of 1-5:
| Area | Score | Notes |
|---|---|---|
| Background | ? | Is it a flat color? Gradient? Animated? |
| Color Palette | ? | How many colors? Cohesive? |
| Animations | ? | Are entity movements smooth? Any easing? |
| Particles | ? | Any particle effects on events? |
| Screen Transitions | ? | Between states (menu, play, game-over)? |
| Typography | ? | Score display, instructions, game-over text |
| Game Feel / Juice | ? | Screen shake, flash, scale on events? |
| Game Over | ? | Memorable? Or just "Game Over" text? |
| Entity Prominence | ? | Does the player entity stand out? |
| First Impression | ? | What does a new player see in the first 3 seconds? |
Present this report to the user with specific observations and a recommended priority order for improvements. Ask the user which areas they want to address.
For each area the user wants improved, implement the changes following the spectacle effects and patterns below. Always ask "May I write this to [filepath]?" before modifying any file, following the collaboration protocol.
These are battle-tested visual effects that add juice and atmosphere to browser games. Implement them as needed based on the audit results.
Floating text that appears when the player achieves consecutive hits or a combo. Rises and fades out.
function showComboText(x, y, text, color = '#ffdd00') {
const el = document.createElement('div');
el.textContent = text;
el.style.cssText = `
position: fixed;
left: ${x}px;
top: ${y}px;
font-size: 28px;
font-weight: 900;
color: ${color};
text-shadow: 0 0 10px ${color}, 2px 2px 0 #000;
pointer-events: none;
z-index: 1000;
transition: all 0.8s ease-out;
font-family: 'Arial Black', sans-serif;
`;
document.body.appendChild(el);
requestAnimationFrame(() => {
el.style.top = `${y - 80}px`;
el.style.opacity = '0';
el.style.transform = 'scale(1.5)';
});
setTimeout(() => el.remove(), 800);
}
A brief pause (freeze frame) on impact that makes hits feel heavy. Works on both player and enemy hits.
function freezeFrame(durationMs = 50) {
// For Canvas-based games
const originalRAF = window.requestAnimationFrame;
let frozen = true;
setTimeout(() => {
frozen = false;
}, durationMs);
// For simple game loops with a running flag:
if (typeof gameLoop !== 'undefined' && gameLoop.running) {
gameLoop.running = false;
setTimeout(() => { gameLoop.running = true; }, durationMs);
}
}
A slow-shifting gradient background that adds life without distracting from gameplay.
@keyframes rainbowShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.game-canvas {
background: linear-gradient(
-45deg,
#1a1a2e, #16213e, #0f3460, #533483,
#e94560, #533483, #0f3460, #16213e
);
background-size: 400% 400%;
animation: rainbowShift 15s ease infinite;
}
The background briefly brightens or shifts color when the player scores, creating a subconscious reward signal.
function pulseBackground() {
const canvas = document.querySelector('.game-canvas');
if (!canvas) return;
canvas.style.transition = 'filter 0.1s ease-out';
canvas.style.filter = 'brightness(1.3)';
setTimeout(() => {
canvas.style.filter = 'brightness(1.0)';
}, 100);
}
Enemies, obstacles, or collectibles should not just appear -- they should enter with a brief animation that draws attention without being slow.
@keyframes entityEntrance {
0% { transform: scale(0) rotate(180deg); opacity: 0; }
60% { transform: scale(1.2) rotate(-10deg); opacity: 1; }
100% { transform: scale(1) rotate(0deg); opacity: 1; }
}
.entity-enter {
animation: entityEntrance 0.3s ease-out forwards;
}
// For canvas-based games, apply entrance scaling in the draw loop
function drawEntityWithEntrance(entity, ctx) {
if (entity.entranceProgress < 1) {
entity.entranceProgress = Math.min(1, entity.entranceProgress + 0.05);
const scale = easeOutBack(entity.entranceProgress);
ctx.save();
ctx.translate(entity.x, entity.y);
ctx.scale(scale, scale);
// draw entity at origin
ctx.restore();
}
}
function easeOutBack(t) {
const c1 = 1.70158;
const c3 = c1 + 1;
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
}
A fading trail behind the player entity that creates a sense of speed and movement history.
class PlayerTrail {
constructor(maxLength = 12) {
this.positions = [];
this.maxLength = maxLength;
}
add(x, y) {
this.positions.push({ x, y, alpha: 1.0 });
if (this.positions.length > this.maxLength) {
this.positions.shift();
}
}
draw(ctx, radius, color) {
this.positions.forEach((pos, i) => {
const alpha = (i / this.positions.length) * 0.4;
const size = radius * (i / this.positions.length) * 0.8;
ctx.beginPath();
ctx.arc(pos.x, pos.y, size, 0, Math.PI * 2);
ctx.fillStyle = color.replace('1)', `${alpha})`);
ctx.fill();
});
}
}
When the player hits score milestones (every 10, 50, 100 points), show a full-screen announcement that celebrates progress.
function showMilestone(value, message) {
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: rgba(0, 0, 0, 0.5);
z-index: 2000;
pointer-events: none;
animation: fadeInOut 1.5s ease forwards;
`;
const score = document.createElement('div');
score.textContent = value;
score.style.cssText = `
font-size: 72px;
font-weight: 900;
color: #ffd700;
text-shadow: 0 0 20px #ffd700, 0 0 40px #ff8c00;
`;
const msg = document.createElement('div');
msg.textContent = message;
msg.style.cssText = `
font-size: 24px;
color: #fff;
margin-top: 10px;
text-shadow: 0 0 10px rgba(255,255,255,0.5);
`;
overlay.appendChild(score);
overlay.appendChild(msg);
document.body.appendChild(overlay);
setTimeout(() => overlay.remove(), 1500);
}
// Usage: Check after score updates
function checkMilestones(newScore) {
const milestones = {
10: 'Nice Start!',
25: 'Getting Warmed Up!',
50: 'Halfway There!',
100: 'Triple Digits!',
250: 'On Fire!',
500: 'Unstoppable!',
};
if (milestones[newScore]) {
showMilestone(newScore, milestones[newScore]);
}
}
Add the required keyframe to CSS:
@keyframes fadeInOut {
0% { opacity: 0; }
20% { opacity: 1; }
80% { opacity: 1; }
100% { opacity: 0; }
}
| Score | Description |
|---|---|
| 1 | Solid flat color (e.g., plain white or black) |
| 2 | Simple gradient, no movement |
| 3 | Animated gradient or subtle pattern |
| 4 | Multi-layer background with parallax or depth |
| 5 | Dynamic background that reacts to gameplay events |
| Score | Description |
|---|---|
| 1 | Default browser colors, no design intent |
| 2 | Some color choices but inconsistent |
| 3 | 3-5 color palette applied consistently |
| 4 | Palette with clear contrast hierarchy |
| 5 | Palette reinforces game theme and mood perfectly |
| Score | Description |
|---|---|
| 1 | No animations, entities teleport |
| 2 | Basic CSS transitions on some elements |
| 3 | Smooth movement with easing functions |
| 4 | Custom easing (bounce, elastic) on key actions |
| 5 | Every state change is animated with purpose |
| Score | Description |
|---|---|
| 1 | No particle effects |
| 2 | Particles on one event (e.g., death only) |
| 3 | Particles on 2-3 key events |
| 4 | Varied particle shapes and colors per event type |
| 5 | Particle system that integrates with score and gameplay |
| Score | Description |
|---|---|
| 1 | Instant cuts between states |
| 2 | Basic fade in/out |
| 3 | Themed transitions (slide, zoom) |
| 4 | Transition timing matches game rhythm |
| 5 | Transitions enhance the emotional arc of state changes |
| Score | Description |
|---|---|
| 1 | Default system font, no sizing hierarchy |
| 2 | Custom font but poor sizing or spacing |
| 3 | Clear hierarchy (score large, instructions smaller) |
| 4 | Text animations on change (score pop, instruction fade) |
| 5 | Typography is part of the game's visual identity |
| Score | Description |
|---|---|
| 1 | No feedback on any event |
| 2 | Visual feedback on one event type |
| 3 | Feedback on score, collision, and state change |
| 4 | Screen shake, flash, scale, and sound on events |
| 5 | Layered juice: visual + audio + haptic + particles |
| Score | Description |
|---|---|
| 1 | Alert box or plain text |
| 2 | Styled overlay with score |
| 3 | Animated overlay with stats and high score |
| 4 | Replay incentive ("1 more to beat high score") |
| 5 | Memorable sequence that makes failure feel fair and fun |
| Score | Description |
|---|---|
| 1 | Player entity blends into background |
| 2 | Player is visible but similar size to obstacles |
| 3 | Player has distinct color/size vs. obstacles |
| 4 | Player has glow, shadow, or outline |
| 5 | Player is unmistakably the focal point of the screen |
| Score | Description |
|---|---|
| 1 | Blank screen with no indication of what to do |
| 2 | Game visible but no instructions |
| 3 | Clear title and "Press to Start" prompt |
| 4 | Animated title screen with preview of gameplay |
| 5 | Title screen alone makes the player want to continue |
These areas are outside this skill's scope. Do not modify them unless explicitly asked:
If the audit reveals issues in these areas, note them in the report but do not
fix them. Suggest the user run /game-qa or handle them separately.
When implementing visual improvements, watch for these frequent mistakes:
Z-index conflicts: Spectacle overlays (combo text, milestones) must have
a higher z-index than game elements but not block input (use
pointer-events: none).
Animation stacking: Multiple simultaneous CSS animations can conflict. Use separate elements for independent effects rather than animating the same property twice on one element.
Memory leaks from DOM elements: Every dynamically created element (combo
text, particles, overlays) must be removed after its animation completes.
Always pair appendChild with a setTimeout(() => el.remove(), ...).
requestAnimationFrame without cancellation: If you add rAF-based animations, store the frame ID and cancel it on cleanup (game over, state change) to prevent ghost animations.
CSS transitions on transform while using JS transform: If JavaScript
directly sets style.transform, CSS transitions on transform will fight
with it. Pick one: either CSS transition or JS-driven transform, not both.
Canvas clear before draw: When adding visual effects to a canvas game, ensure the clearRect happens before all draw calls, not between them.
Layout thrashing: Reading offsetWidth/offsetHeight immediately
before writing style in a loop forces browser reflow. Batch reads
separately from writes.
Uninitialized color values: If the game uses dynamic colors, always
provide a fallback. ctx.fillStyle = entity.color || '#ffffff' prevents
black fills from undefined colors.
This skill follows the collaboration principle: