정보실

웹학교

정보실

javascript 컬러 충돌 게임

본문

색상 일치는 Dev Loop의 JavaScript 게임에서 이기는 것만큼 지는 것이 거의 재미 있습니다!


https://codepen.io/dev_loop/pen/gOYLbge 


HTML


<div class="start-screen">
	<div class="game-data">
		<span class="name"><span>Color</span> <span>Collision</span></span>
	</div>
	<span class="info">
		<span class="highlight _1">FOCUS</span> is the weapon.
		Don't make the wrong move.
		Tap / click to change the color and save the central balls.
		Because if the central balls don't stay alive <span class="highlight _2">YOU DIE</span>.
		Better play <span class="highlight _3">SAFE</span> and make a new highscore.
	</span>
	<button class="btn play">PLAY</button>
</div>

<span class="retry-text hide" data-splitting>Game Over!</span>
<button class="retry-btn hide" data-splitting>RETRY</button>
<canvas data-canvas></canvas>


<div class="support">
	<a href="https://twitter.com/DevLoop01" target="_blank"><i class="fab fa-twitter-square"></i></a>
	<a href="https://github.com/devloop01/color-collision" target="_blank"><i class="fab fa-github"></i></a>
</div>



CSS


* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  width: 100%;
  height: 100vh;
  overflow: hidden;
  display: flex;
  justify-content: center;
  align-items: center;
  font-family: Montserrat, sans-serif;
  background: radial-gradient(#e74c3c 4px, transparent 4px), radial-gradient(#e74c3c 4px, transparent 4px), linear-gradient(#222 4px, transparent 0), linear-gradient(45deg, transparent 74px, transparent 75px, #3498db 75px, #3498db 76px, transparent 77px, transparent 109px), linear-gradient(-45deg, transparent 75px, transparent 76px, #3498db 76px, #3498db 77px, transparent 78px, transparent 109px), #222;
  background-size: 109px 109px, 109px 109px, 100% 6px, 109px 109px, 109px 109px;
  background-position: 54px 55px, 0px 0px, 0px 0px, 0px 0px, 0px 0px;
}

.highlight {
  font-weight: 900;
}
.highlight._1 {
  color: #3498db;
}
.highlight._2 {
  color: #e74c3c;
}
.highlight._3 {
  color: #21c758;
}

.retry-text {
  position: absolute;
  top: 38%;
  pointer-events: none;
  user-select: none;
}
.retry-text .char {
  font-size: 12vmin;
  font-weight: 800;
  letter-spacing: 5px;
}
.retry-text .char::before {
  pointer-events: none;
  content: attr(data-char);
  position: absolute;
  visibility: visible;
  color: #fff;
  transition: all 200ms cubic-bezier(0.1, 0.1, 0.33, 1);
  transition-delay: calc(0.16s + (0.03s * (var(--char-index))));
}

.char {
  overflow: hidden;
  color: transparent;
}

.hide .char::before {
  transform: translateY(50%);
  opacity: 0;
}

.show .char::before {
  transform: translateY(0);
  opacity: 1;
}

.retry-btn {
  position: absolute;
  top: 58%;
  background: transparent;
  font-size: 1.1em;
  border: none;
  outline: none;
  color: #fff;
  width: 120px;
  height: 50px;
  border: 2px solid rgba(255, 255, 255, 0.6);
  cursor: pointer;
  transition: all 180ms cubic-bezier(0.075, 0.82, 0.165, 1);
  user-select: none;
}
.retry-btn.hide {
  pointer-events: none;
  opacity: 0;
}
.retry-btn.show .char {
  color: #fff;
}
.retry-btn:hover {
  background: rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(2px);
}

.retry-btn .char {
  font-size: 0.9em;
  font-weight: 900;
}

.retry-btn:focus {
  outline: none;
}

.start-screen {
  position: absolute;
  background: rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(20px);
  width: 450px;
  height: 250px;
  padding: 10px;
  display: flex;
  flex-direction: column;
  transition: all 300ms cubic-bezier(0.075, 0.82, 0.165, 1);
}
.start-screen.hide {
  opacity: 0;
  transform: translateY(100%);
  pointer-events: none;
  z-index: -1;
}
.start-screen .game-data {
  width: 100%;
  height: 30%;
  display: flex;
  justify-content: center;
  align-items: center;
}
.start-screen .game-data .name {
  font-size: 2rem;
  color: #fff;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: 6px;
}
.start-screen .game-data .name span:nth-child(1) {
  color: #3498db;
}
.start-screen .game-data .name span:nth-child(2) {
  color: #e74c3c;
}
.start-screen .info {
  width: 100%;
  height: 50%;
  padding: 5px 10px;
  font-size: 14px;
  line-height: 20px;
  color: rgba(255, 255, 255, 0.7);
}
.start-screen .btn.play {
  position: relative;
  width: 120px;
  height: 50px;
  border: none;
  border: 2px solid rgba(255, 255, 255, 0.6);
  cursor: pointer;
  letter-spacing: 2px;
  font-weight: 900;
  background: none;
  color: #fff;
  align-self: center;
  margin-top: -10px;
  transition: all 400ms cubic-bezier(0.8, 0, 0.33, 1);
  overflow: hidden;
}
.start-screen .btn.play::after, .start-screen .btn.play::before {
  position: absolute;
  width: 100%;
  transition: all 400ms cubic-bezier(0.8, 0, 0.33, 1);
  z-index: -1;
}
.start-screen .btn.play::before {
  content: "";
  height: 0%;
  left: 0;
  bottom: 0;
  border-radius: 50% 50% 0 0;
  background: #fff;
}
.start-screen .btn.play::after {
  content: "PLAY";
  height: 180%;
  right: 0;
  top: 0;
  color: #000;
  transform: translateY(-100%);
  font-size: 1.4em;
}
.start-screen .btn.play:hover {
  color: transparent;
  border-color: #fff;
}
.start-screen .btn.play:hover::after {
  transform: translateY(15%);
}
.start-screen .btn.play:hover::before {
  height: 180%;
}
.start-screen .btn.play:focus {
  transform: scale(0.9);
  outline: none;
}

.support {
  position: absolute;
  right: 5px;
  bottom: 5px;
  padding: 5px;
  display: flex;
}

a {
  margin: 0 15px;
  color: #fff;
  font-size: 1.8rem;
  transition: all 400ms ease;
}

a:hover {
  opacity: 0.6;
}



Javascript



console.clear()

// Utility Functions -->

// This func. gets a random float between the given range
function randomFloatFromRange(min, max){
	return (Math.random() * (max - min + 1) + min);
}

// This func. gets a random item from a given array
function randomFromArray(arr){
	return arr[Math.floor(Math.random() * arr.length)]
}

// This func. gets the distance between two given points
function getDist(x1, y1, x2, y2){
	return Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((y2 - y1), 2))
}


// PARTICLE CLASS
class Particle{
	constructor(canvas, ctx, x, y, radius, color, velX, velY){
		this.canvas = canvas
		this.ctx = ctx
		this.x = x
		this.y = y
		this.velocity = {
			x: (Math.random() - 0.5) * velX,
			y: (Math.random() - 0.5) * velY,
		}
		this.radius = radius
		this.color = color
		this.timeToLive = 250
		this.opacity = 1
		this.gravity = 0.25
	}
	draw(){ // This func. draws the particle
		this.ctx.save()
		this.ctx.beginPath()
		this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false)
		this.ctx.fillStyle = this.color
		this.ctx.shadowColor = this.color
		this.shadowBlur = 25
		this.ctx.globalAlpha = this.opacity
		this.ctx.fill()
		this.ctx.closePath()
		this.ctx.restore()        
	}
	update(){ // This func. updates the particle
		this.x += this.velocity.x
		this.y += this.velocity.y
		this.velocity.y += this.gravity

		this.timeToLive -= 1
		this.opacity -= 1 / this.timeToLive
		this.draw()
	}
}

// BALL CLASS
class Ball{
	constructor(canvas, ctx, x, y, radius, color, particlesArr, velX, velY, dontCheck){
		this.canvas = canvas
		this.ctx = ctx
		this.x = x
		this.y = y
		this.radius = radius
		this.color = color
		this.velocity = {
			x: velX || 0,
			y: velY || 0
		}
		this.acc = 0.01
		this.origin = { x: x, y: y }
		this.dontCheck = dontCheck
		this.opacity = 1
		this.particlesArr = particlesArr
		this.collided = false
	}
	draw(){ // This func. draws the ball
		this.ctx.save()
		this.ctx.beginPath()
		this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false)
		this.ctx.fillStyle = this.color
		this.ctx.shadowColor = this.color
		this.shadowBlur = 25
		this.ctx.shadowOffsetX = 0;
		this.ctx.shadowOffsetY = 0;
		this.ctx.globalAlpha = this.opacity
		this.ctx.fill()
		this.ctx.closePath()
		this.ctx.restore()
	}
	// This func. updates the ball. The first argument of this func. takes an array where all the insrtance of balls are stored
	// The second argument is opotional [default is False]. If set to true then the position of the ball changes with its respective Velocity
	update(ballsArr, updateVel = false){
		if(this.origin.y <= 0){
			this.y += this.velocity.y
		}
		else if(this.origin.y >= this.canvas.height){
			this.y -= this.velocity.y
		}
		if(updateVel == true){
			this.y += this.velocity.y
			this.x += this.velocity.x
		}

		this.collisionDetect(ballsArr)
		this.draw()
	}
	// This func. is used to detect the collisions bettween any two balls
	// The func. takes an argument which is the array where al the balls are stored
	collisionDetect(ballsArr){
		for(let i = 0; i < ballsArr.length; i++){
			if(this === ballsArr[i] || this.dontCheck) continue
			let distBetweenPoints = getDist(this.x, this.y, ballsArr[i].x, ballsArr[i].y) - this.radius * 2
			if(distBetweenPoints < 0){
				if(this.color == ballsArr[i].color){
					for(let j = 0; j < Math.floor(randomFloatFromRange(20, 25)); j++){
						this.break(this.particlesArr, 0.4, 0.8)
						this.collided = true
					}
					this.opacity = 0
				}
				else if(this.color != ballsArr[i].color){
					for(let j = 0; j < Math.floor(randomFloatFromRange(40, 55)); j++){
						ballsArr.forEach((ball) => {
							ball.opacity = 0
							this.break(this.particlesArr, 2, 5, ball.x, ball.y, ball.color)
						})
						this.particlesArr.forEach((particle) => {
							particle.gravity = 0
						})
					}
				}
			}
		}
	}
	// This func. is used to detect if the ball hits any of the corners of the canvas
	// If hits any of the canvas sides then the ball would change its velocity direction
	edgeDetect(){
		if (this.y + this.radius + this.velocity.y > this.canvas.height) {
			this.velocity.y *= -1
		}
		else if(this.y - this.radius <= 0){
			this.velocity.y *= -1
		}

		if (this.x + this.radius + this.velocity.x > this.canvas.width) {
			this.velocity.x *= -1
		}
		else if (this.x - this.radius <= 0) {
			this.velocity.x *= -1
		}
	}
	// This function is used to show that when any ball hits each other then they create many small particles [Which looks kinda like sparks]
	// This func. takes 6 arguments [too many]
	// The first accepts an array where the sparks OR the small particles would be stored
	// The second and the third argument is nothing but accepts a min and max radius
	// The forth and fifth args. tahes where the sparks would be spawned
	// The sixth is nothing but 'c' which means color. I want to make the sparks the same color as the ball
	break(arr, minRadius, maxRadius, x, y, c){
		var randRadius = randomFloatFromRange(minRadius, maxRadius)
		var randVel = {
			x: randomFloatFromRange(-20, 20),
			y: randomFloatFromRange(-20, 20),
		}
		if(this.origin.y <= 0){
			let spawnX , spawnY
			let color
			if(x && y){
				spawnX = x
				spawnY = y
				color = c
			}else{
				spawnX = this.x
				spawnY = this.y + this.radius
				color = this.color
			}
			arr.push(
				new Particle(
					this.canvas, this.ctx,
					spawnX, spawnY,
					randRadius, color , randVel.x, randVel.y
				)
			)
		}else{
			let spawnX , spawnY
			let color
			if(x && y){
				spawnX = x
				spawnY = y
				color = c
			}else{
				spawnX = this.x
				spawnY = this.y - this.radius
				color = this.color
			}
			arr.push(
				new Particle(
					this.canvas, this.ctx,
					spawnX, spawnY,
					randRadius, color , randVel.x, randVel.y
				)
			)
		}
	}
	// When this func. is called with two colors passed as args. then it swaps the color between the two color provided
	change(colorDefault, colorTochange){
		if(this.color != colorDefault){
			this.color = colorDefault
		}else{
			this.color = colorTochange
		}
	}
}

// calling the spliiting function
Splitting()

// Selecting the canvas
const canvas = document.querySelector('[data-canvas]')
// getting its context
const ctx = canvas.getContext('2d')

// Setting its width and height
let canvasHeight = innerHeight - 50,
	 canvasWidth = 400
canvas.width = canvasWidth
canvas.height = canvasHeight


var retryBtn = document.querySelector('.retry-btn')
var retryText = document.querySelector('.retry-text')
var playBtn = document.querySelector('.btn.play')
var startScreen = document.querySelector('.start-screen')


// Initializing everything

let balls = [], // balls array
	 particles = [] // sparks array
var redBall, blueBall // the TWO cantral balls
var separation = 35 // separation between central balls
var globalRadius = 18 // radius for all the Balls
let generateBall = false // generate a new ball or not
let timeInterval
let velocityOfBall // velocity of the ball
let failed = false // game failed or not
let timer = 0 // timer (increments every 1ms)
let score = 0 // score counter
let fillColor // Text fill color
// colors array
var colors = ['#e74c3c', '#3498db']
// random points where the ball would generate and start moving
var randPoints = [
	{
		x: canvas.width / 2,
		y: -50
	},
	{
		x: canvas.width / 2,
		y: canvas.height + 50
	}
]

// Function that initializes the canvas
function init(){
	balls = []
	particles = []
	uselessBalls = []
	generateBall = true
	timeInterval = 2000
	timer = 0
	velocityOfBall = 2.5
	score = 0
	fillColor = '#fff'

	blueBall = new Ball(
		canvas, ctx,
		canvasWidth/2, canvasHeight/2 - separation,
		globalRadius, colors[1], particles, 0, 0, true
	)
	redBall = new Ball(
		canvas, ctx,
		canvasWidth/2, canvasHeight/2 + separation,
		globalRadius, colors[0], particles, 0, 0, true
	)
	balls.push(redBall, blueBall)
}

// This is an array for a bunch of useless balls on the start of the game
var uselessBalls = []
// This function will push many useless balls balls to the useless array and then push all the useless balls to the default ballas array
function initUseless(){
	for(let i = 0; i < 20; i++){
		let randVelXY = {
			x: randomFloatFromRange(-5, 5),
			y: randomFloatFromRange(-5, 5)
		}
		let r = randomFloatFromRange(5, 10)
		uselessBalls.push(
			new Ball(
				canvas, ctx, canvasWidth / 2, canvasHeight / 2,
				r, colors[Math.floor(Math.random() * colors.length)],
				particles, randVelXY.x, randVelXY.y, true
			)
		)
	}
	balls.push([...uselessBalls])
}
// calling it on execution of code
initUseless()


// initialiazing the background in a variable
var background = BG_Gradient('#2c3e50', '#34495e')

// this func. calls itself again and again every 60ms and is the reason you can play this game
function loop(){
	// func. that will call itself
	requestAnimationFrame(loop)

	// seting the background
	ctx.fillStyle = background
	ctx.fillRect(0, 0, canvas.width, canvas.height)

	// setting the scorecard
	ctx.fillStyle = fillColor
	ctx.font = '21px sans-serif'
	ctx.fillText(`SCORE : ${score.toString()}`, 20, 35)

	// updating every uselessballs
	uselessBalls.forEach(ball => {
		ball.update(balls, true)
		ball.edgeDetect()
	})
	if(uselessBalls.length != 0){
		return
	}

	// updating every balls in the balls array
	if(balls.length != 0){
		balls.forEach((ball, index) => {
			ball.update(balls)
			if(ball.collided == true){
				score += 10
				fillColor = ball.color
			}
			if(ball.opacity <= 0){
				ball.dontCheck = true
				balls.splice(index, 1)
			}
		})
	}
	if(balls.length == 0 || balls.length == 1){
		failed = true
		generateBall = false
	}
	if(balls.length == 2){
		generateBall = true
	}
	if(timeInterval % timer == 0 && generateBall == true){
		generateBall = false
		pushNewBalls()
	}

	// updating every particles or sparks in the particles array
	if(particles.length != 0){
		particles.forEach((particle, index) => {
			particle.update()
			if(particle.opacity <= 0.05){
				particles.splice(index, 1)
			}
		})
	}

	// reseting the timer to 0 every 600ms
	if(timer == 600){
		timer = 0
	}

	// func. that is used to show and hide the UI options
	showHideOptions()
	// increment timer by 1 every 1ms
	timer++
}
// calling the func. once will make the recurrsion possible which in return will start the animations
loop()


// Function that is used tto push new balls to the Balls array whenever called
function pushNewBalls(){
	var randomPoint = randomFromArray(randPoints),
		 randomColor = randomFromArray(colors)
	balls.push(
		new Ball(
			canvas, ctx,
			randomPoint.x, randomPoint.y,
			globalRadius, randomColor, particles, 0, velocityOfBall, false
		)
	)
}

// Func. to show and hide the UI
function showHideOptions(){
	if(failed == true && generateBall == false){
		retryText.classList.remove('hide')
		retryText.classList.add('show')
		retryBtn.classList.remove('hide')
		retryBtn.classList.add('show')
	}else if(failed == false && generateBall == true){
		retryText.classList.add('hide')
		retryText.classList.remove('show')
		retryBtn.classList.add('hide')
		retryBtn.classList.remove('show')
	}
}

// Func. that returns simple color gradient by providing two colors
function BG_Gradient(color1, color2){
	let bg = ctx.createLinearGradient(0, 0, canvasWidth, canvasHeight)
	bg.addColorStop(0, color1)
	bg.addColorStop(1, color2)
	return bg
}

// This will be called every 1000ms and the code would execute
setInterval(() => {
	if(velocityOfBall <= 7){
		velocityOfBall += 0.08
	}else{
		velocityOfBall += 0
	}
}, 1500)


// EVENT LISTENERS

// Clicking on the canvas would change the color
canvas.addEventListener('mousedown', () => {
	redBall.change(colors[0], colors[1])
	blueBall.change(colors[1], colors[0])
})

// retry again by clicking the retry button
retryBtn.addEventListener('mousedown', () => {
	failed = false
	init()
})

// Start playing now.
playBtn.addEventListener('mousedown', () => {
	startScreen.classList.add('hide')
	init()
})

window.addEventListener('resize', () => {
	canvasHeight = innerHeight - 50
	canvas.height = canvasHeight
})



https://unpkg.com/splitting/dist/splitting.min.js





  • 트위터로 보내기
  • 페이스북으로 보내기
  • 구글플러스로 보내기
  • 카카오톡으로 보내기

페이지 정보

조회 27회 ]  작성일19-08-27 18:08

웹학교