HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<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://github.com/devloop01/color-collision" target="_blank"><span>fork at</span><i class="fab fa-github"></i></a>
</div>
* {
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;
&._1 {
color: #3498db;
}
&._2 {
color: #e74c3c;
}
&._3 {
color: #21c758;
}
}
.retry-text {
position: absolute;
top: 38%;
pointer-events: none;
user-select: none;
.char {
font-size: 12vmin;
font-weight: 800;
letter-spacing: 5px;
&::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;
&.hide {
pointer-events: none;
opacity: 0;
}
&.show .char {
color: #fff;
}
&: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);
&.hide {
opacity: 0;
transform: translateY(100%);
pointer-events: none;
z-index: -1;
}
.game-data {
width: 100%;
height: 30%;
display: flex;
justify-content: center;
align-items: center;
.name {
font-size: 2rem;
color: #fff;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 6px;
span:nth-child(1) {
color: #3498db;
}
span:nth-child(2) {
color: #e74c3c;
}
}
}
.info {
width: 100%;
height: 50%;
padding: 5px 10px;
font-size: 14px;
line-height: 20px;
color: rgba(255, 255, 255, 0.7);
}
.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;
&::after,
&::before {
position: absolute;
width: 100%;
transition: all 400ms cubic-bezier(0.8, 0, 0.33, 1);
z-index: -1;
}
&::before {
content: "";
height: 0%;
left: 0;
bottom: 0;
border-radius: 50% 50% 0 0;
background: #fff;
}
&::after {
content: "PLAY";
height: 180%;
right: 0;
top: 0;
color: #000;
transform: translateY(-100%);
font-size: 1.4em;
}
&:hover {
color: transparent;
border-color: #fff;
&::after {
transform: translateY(15%);
}
&::before {
height: 180%;
}
}
&:focus {
transform: scale(0.9);
outline: none;
}
}
}
.support{
position: absolute;
right: 5px;
bottom: 5px;
padding: 5px 0;
a{
margin: 0 10px;
color: #fff;
font-size: 1.8rem;
transition: all 400ms ease;
text-decoration: none;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
padding: 8px;
display: flex;
align-items: center;
user-select: none;
span{
text-transform: uppercase;
font-size: 1rem;
margin-right: 10px;
}
&:hover{
background: rgba(0, 0, 0, 1);
}
}
}
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 canvasMaxHeight = 800,
canvasWidth = 400,
canvasHeight = innerHeight - 50 > canvasMaxHeight ? canvasMaxHeight : innerHeight - 50;
canvas.width = canvasWidth
canvas.height = canvasHeight
let retryBtn = document.querySelector('.retry-btn')
let retryText = document.querySelector('.retry-text')
let playBtn = document.querySelector('.btn.play')
let 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
let randPoints;
// 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)
randPoints = [
{
x: canvas.width / 2,
y: -50
},
{
x: canvas.width / 2,
y: canvas.height + 50
}
]
}
// 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', () => {
if(canvasHeight >= canvasMaxHeight){
canvasHeight = canvasMaxHeight;
}else{
canvasHeight = innerHeight - 50
}
canvas.height = canvasHeight
})
Also see: Tab Triggers