Skip to content

Instantly share code, notes, and snippets.

@Maxime66410
Created July 24, 2023 19:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Maxime66410/c0ecb1d514234610fa210b45da25d00f to your computer and use it in GitHub Desktop.
Save Maxime66410/c0ecb1d514234610fa210b45da25d00f to your computer and use it in GitHub Desktop.
/r/place
<div id="canvas"></div>
<div class="controls">
<a id="auth">
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300.00006 244.18703" height="30" width="30" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
<g style="" transform="translate(-539.18 -568.86)">
<path d="m633.9 812.04c112.46 0 173.96-93.168 173.96-173.96 0-2.6463-0.0539-5.2806-0.1726-7.903 11.938-8.6302 22.314-19.4 30.498-31.66-10.955 4.8694-22.744 8.1474-35.111 9.6255 12.623-7.5693 22.314-19.543 26.886-33.817-11.813 7.0031-24.895 12.093-38.824 14.841-11.157-11.884-27.041-19.317-44.629-19.317-33.764 0-61.144 27.381-61.144 61.132 0 4.7978 0.5364 9.4646 1.5854 13.941-50.815-2.5569-95.874-26.886-126.03-63.88-5.2508 9.0354-8.2785 19.531-8.2785 30.73 0 21.212 10.794 39.938 27.208 50.893-10.031-0.30992-19.454-3.0635-27.69-7.6468-0.009 0.25652-0.009 0.50661-0.009 0.78077 0 29.61 21.075 54.332 49.051 59.934-5.1376 1.4006-10.543 2.1516-16.122 2.1516-3.9336 0-7.766-0.38716-11.491-1.1026 7.7838 24.293 30.355 41.971 57.115 42.465-20.926 16.402-47.287 26.171-75.937 26.171-4.929 0-9.7983-0.28036-14.584-0.84634 27.059 17.344 59.189 27.464 93.722 27.464" fill="#ffffff"/>
</g>
</svg>
<span id="authButtonText">Login</span>
</a>
<ul id="colors">
<li class="" id="c-ffffff" ></li>
<li class="" id="c-e4e4e4" ></li>
<li class="" id="c-888888" ></li>
<li class="" id="c-222222" ></li>
<li class="" id="c-ffa7d1" ></li>
<li class="" id="c-e50000" ></li>
<li class="" id="c-e59500" ></li>
<li class="" id="c-a06a42" ></li>
<li class="" id="c-e5d900" ></li>
<li class="" id="c-94e044" ></li>
<li class="" id="c-02be01" ></li>
<li class="" id="c-00d3dd" ></li>
<li class="" id="c-0083c7" ></li>
<li class="" id="c-0000ea" ></li>
<li class="" id="c-cf6ee4" ></li>
<li class="" id="c-820080" ></li>
<div class="cooldown">
<span id="cooldown-text">1:00</span>
</div>
</ul>
<div class="face-space"></div>
</div>
<div class="info general">Click to zoom, click and drag to move.<!-- <span>If the screen is blank then I've probably hit my Firebase free tier limits :(</span> --></div>
<div class="info drawing">Click to draw pixel, click and drag to move.</div>
<div class="info loading">Loading...</div>
<div class="zoom-controls">
<div id="zoom-in">+</div>
<div id="zoom-out">-</div>
</div>

/r/place

A replica of Reddit's Place, a collaborative drawing app lasting 72hrs on April fools day 2017. https://www.reddit.com/r/place/.

Made using Firebase and Pixi.js.

If you want to see a view only version that is more zoomed out then head over to http://codepen.io/steveg3003/details/XRJpqG

If you'd like to set this up on your own Firebase account then you'll need to copy the real time database rules found at the bottom of the javascript code panel and setup Twitter authentication. Leave a comment if you want more info.

A Pen by Steve Gardner on CodePen.

License.

// First off, lets clear that blasted console
console.clear();
// TypeScript interfaces
interface Pixel
{
uid: string;
timestamp: number;
color: string;
}
interface Position
{
x: number;
y: number;
}
// Get DOM elements
let body:HTMLElement = document.body;
let authButton:HTMLElement = document.getElementById('auth');
let authButtonText:HTMLElement = document.getElementById('authButtonText');
let canvasContainer:HTMLElement = document.getElementById('canvas');
let coolDownText:HTMLElement = document.getElementById('cooldown-text');
let zoomInButton:HTMLElement = document.getElementById('zoom-in');
let zoomOutButton:HTMLElement = document.getElementById('zoom-out');
let colorOptions:HTMLElement[] = [];
// set loading state
body.classList.add('loading');
// Define consts
const colors = [ 'ffffff',
'e4e4e4',
'888888',
'222222',
'ffa7d1',
'e50000',
'e59500',
'a06a42',
'e5d900',
'94e044',
'02be01',
'00d3dd',
'0083c7',
'0000ea',
'cf6ee4',
'820080'
];
const gridSize = [1000, 1000];
const squareSize = [3, 3];
const coolDownTime = 500;
const zoomLevel = 6;
const clearColorSelectionOnCoolDown = false;
// Define variables
let uid:string;
let app:any;
let graphics:any;
let gridLines:any;
let container:any;
let dragging:boolean = false;
let mouseDown:boolean = false;
let start:Position;
let graphicsStart:Position;
let selectedColor:string;
let zoomed:boolean = false;
let coolCount:number = 0;
let coolInterval:any;
let scale:number = 1;
let currentlyWriting:string;
let ready:boolean = false;
// I'm adding 5 seconds before I begin downloading
// all the pixels, but only if the pen is running
// as a thumbnail preview.
// That way I'm hopefully not using up valuable
// bandwidth on my Firebase account.
let initWait = location.pathname.match(/fullcpgrid/i) ? 5000 : 0;
// Setup Firebase
var config = {
apiKey: "AIzaSyA4q8u7hRWWGFq_fvTzMxpVypy7W4cTfTk",
authDomain: "codepen-2.firebaseapp.com",
databaseURL: "https://codepen-2.firebaseio.com",
projectId: "codepen-2",
storageBucket: "codepen-2.appspot.com",
messagingSenderId: "270463661110"
};
firebase.initializeApp(config);
// Check if user is logged in
firebase.auth().onAuthStateChanged(function(user)
{
if (user)
{
// user is logged in
uid = user.uid;
// set logout button
authButtonText.innerHTML = 'Logout';
body.classList.add('logged-in')
body.classList.remove('logged-out');
}
else
{
// user is not logged in
uid = null;
// set login button
authButtonText.innerHTML = 'Login with Twitter';
body.classList.remove('logged-in')
body.classList.add('logged-out')
}
authButton.addEventListener("click", toggleLogin);
});
// run stage setup
setupStage();
setupColorOptions();
// start listening for new pixels
setTimeout(startListeners, initWait);
// Auth functions
function login()
{
// Use Twitter for login
var provider = new firebase.auth.TwitterAuthProvider();
// Open Twitter auth window
firebase.auth().signInWithPopup(provider).catch(error => console.log('error logging in', error));
}
function logout()
{
firebase.auth().signOut().catch(error => console.error('error logging out', error));
}
function toggleLogin()
{
if(uid) logout();
else login();
}
// Write pixel to database functions
function writePixel(x:number, y: number, color:string)
{
if(uid)
{
console.log('writing pixel...')
// First we need to get a valid timestamp.
// To stop spamming the rules on the database
// prevent creating a new timestamp within a
// set period.
getTimestamp().then( timestamp =>
{
// we've successfully set a new timestamp.
// This means the cooldown period is
// over and the user is free to save
// their new pixel to the database
var data:Pixel = {
uid: uid,
timestamp: timestamp,
color: color
};
currentlyWriting = x + 'x' + y;
// We set the new pixel data with the key 'XxY'
// for example "56x93"
var ref = firebase.database().ref('pixel/' + currentlyWriting).set(data)
.then( () =>
{
// Pixel successfully saved, we'll wait for
// the pixel listeners to pick up the new
// pixel before drawing it on the canvas.
currentlyWriting = null;
startCoolDown();
console.log('success!')
})
.catch( error =>
{
// Error here is probably due to the internet
// connection going down between generating
// the timestamp and saving the pixel.
// The database has a rule set to check the
// timestamp generated and the timestamp
// sent with the pixel.
// It could also be due to usage limits on
// the free tier of Firebase.
console.error('could not write pixel');
})
})
.catch( error =>
{
// Failed to create a new timestamp.
// Probably because the user hasn't
// waited for their cool down period
// to finish.
console.error('you need to wait for cool down period to finish')
})
}
}
function startCoolDown()
{
// deselect the color
if(clearColorSelectionOnCoolDown) selectColor(null);
// add the .cooling class to the body
// So the countdown clock appears
body.classList.add('cooling');
// start a timeout for the cooldown time
// in milliseconds, the milliseconds are
// also set in the database rules so removing
// this code doesn't allow the user to skip
// cooldown
setTimeout(() => endCoolDown(), coolDownTime);
// coolCount (😎) is used to write the countdown
// clock.
coolCount = coolDownTime;
// update countdown clock first
updateCoolCounter();
// start an interval to update the countdown
// clock every second
clearInterval(coolInterval);
coolInterval = setInterval(updateCoolCounter, 1000);
}
function updateCoolCounter()
{
// Work out minutes and seconds left from
// the remaining milliseconds in coolCount
let mins = String(Math.floor((coolCount/(1000*60))%60));
let secs = String((coolCount/1000)%60);
// update the cooldown counter in the DOM
coolDownText.innerHTML = mins + ':' + (secs.length < 2 ? '0' : '') + secs;
// remove 1 secound (1000 milliseconds)
// ready for the next update.
coolCount -= 1000;
}
function endCoolDown()
{
// set coolCount to 0, just in case it went
// over, intervals aren't perfect.
coolCount = 0;
// stop the update interval
clearInterval(coolInterval);
// remove the .cooling class from the body
// so that the countdown clock is hidden.
body.classList.remove('cooling')
}
function getTimestamp():Promise<any>
{
let promise = new Promise((resolve, reject) => {
// Update user's "last_write" with
// new timestamp
var ref = firebase.database().ref('last_write/' + uid);
ref.set(firebase.database.ServerValue.TIMESTAMP)
.then( () =>
{
// Timestamp is saved, but because
// the database generates this we
// don't know what it is, so we have
// to ask for it.
ref.once('value')
.then(timestamp =>
{
// We have the new timestamp.
resolve(timestamp.val());
})
.catch(reject)
})
.catch(reject)
})
return promise;
}
// Draw pixel functions
function startListeners()
{
console.log('Starting Firebase listeners');
// get a reference to the pixel table
// in the database
let placeRef = firebase.database().ref("pixel");
// get once update on all the values
// in the grid so we can draw everything
// on first load.
// placeRef.once('value')
// .then(snapshot =>
// {
// // draw all the pixels in the grid
// var grid = snapshot.val();
// for(let i in grid)
// {
// renderPixel(i, grid[i]);
// }
// start listening for changes to pixels
placeRef.on('child_changed', onChange);
// also start listening for new pixels,
// grid position that have never had a
// pixel drawn on them are new.
placeRef.on('child_added', onChange);
// })
// .catch(error => {
// console.log(error);
// })
ready = true;
}
function onChange(change)
{
body.classList.remove('loading');
// render the new pixel
// key will be the grid position,
// for example "34x764"
// val will be a pixel object defined
// by the Pixel interface at the top.
renderPixel(change.key, change.val());
}
function setupStage()
{
// Setting up canvas with Pixi.js
app = new PIXI.Application(window.innerWidth, window.innerHeight-60, { antialias: false, backgroundColor : 0xeeeeee });
canvasContainer.appendChild(app.view);
// create a container for the grid
// container will be used for zooming
container = new PIXI.Container();
// and container to the stage
app.stage.addChild(container);
// graphics is the cavas we draw the
// pixels on, well also move this around
// when the user drags around
graphics = new PIXI.Graphics();
graphics.beginFill(0xffffff, 1);
graphics.drawRect(0, 0, gridSize[0] * squareSize[0], gridSize[1] * squareSize[1]);
graphics.interactive = true;
// setup input listeners, we use
// pointerdown, pointermove, etc
// rather than mousedown, mousemove,
// etc, because it triggers on both
// mouse and touch
graphics.on('pointerdown', onDown);
graphics.on('pointermove', onMove);
graphics.on('pointerup', onUp);
graphics.on('pointerupoutside', onUp);
// move graphics so that it's center
// is at x0 y0
graphics.position.x = -graphics.width/2;
graphics.position.y = -graphics.height/2;
// place graphics into the container
container.addChild(graphics);
gridLines = new PIXI.Graphics();
gridLines.lineStyle(0.5, 0x888888, 1);
gridLines.alpha = 0;
gridLines.position.x = graphics.position.x;
gridLines.position.y = graphics.position.y;
for(let i = 0; i <= gridSize[0]; i++)
{
drawLine(0, i * squareSize[0], gridSize[0] * squareSize[0], i * squareSize[0])
}
for(let j = 0; j <= gridSize[1]; j++)
{
drawLine(j * squareSize[1], 0, j * squareSize[1], gridSize[1] * squareSize[1])
}
container.addChild(gridLines);
// start page resize listener, so
// we can keep the canvas the correct
// size
window.onresize = onResize;
// make canvas fill the screen.
onResize();
// add zoom button controls
zoomInButton.addEventListener("click", () => { toggleZoom({x: window.innerWidth / 2, y: window.innerHeight / 2}, true) });
zoomOutButton.addEventListener("click", () => { toggleZoom({x: window.innerWidth / 2, y: window.innerHeight / 2}, false) });
}
function drawLine(x, y, x2, y2)
{
gridLines.moveTo(x, y);
gridLines.lineTo(x2, y2);
}
function setupColorOptions()
{
// link up the color options with
// a click function.
for(let i in colors)
{
// each color button has an id="c-" then
// the color value, for example "c-ffffff"
// is the white color buttton.
let element = document.getElementById('c-' + colors[i]);
// on click send the color to the selectColor
// function
element.addEventListener("click", (e) => {selectColor(colors[i]) });
// add the DOM element to an array so
// we can use it again later
colorOptions.push(element);
}
}
function selectColor(color: string)
{
if(selectedColor !== color)
{
// if the new color does not match
// the current selected color then
// change it the new one
selectedColor = color;
// add the .selectedColor class to
// the body tag. We use this to update
// the info box instructions.
body.classList.add('selectedColor');
}
else
{
// if the new color matches the
// currently selected on the user
// is toggling the color off.
selectedColor = null;
// remove the .selectedColor class
// from the body.
body.classList.remove('selectedColor');
}
for(let i in colors)
{
// loop through all the colors in,
// if the color equals the selected
// color add the .active class to the
// button element
if(colors[i] == selectedColor) colorOptions[i].classList.add('active');
// otherwise remove the .active class
else colorOptions[i].classList.remove('active');
}
}
function onResize(e)
{
// resize the canvas to fill the screen
app.renderer.resize(window.innerWidth, window.innerHeight);
// center the container to the new
// window size.
container.position.x = window.innerWidth / 2;
container.position.y = window.innerHeight / 2;
}
function onDown(e)
{
// Pixi.js adds all its mouse listeners
// to the window, regardless of which
// element they are assigned to inside the
// canvas. So to avoid zooming in when
// selecting a color we first check if the
// click is not withing the bottom 60px where
// the color options are
if(e.data.global.y < window.innerHeight - 60 && ready)
{
// We save the mouse down position
start = {x: e.data.global.x, y: e.data.global.y};
// And set a flag to say the mouse
// is now down
mouseDown = true;
}
}
function onMove(e)
{
// check if mouse is down (in other words
// check if the user has clicked or touched
// down and not yet lifted off)
if(mouseDown)
{
// if not yet detected a drag then...
if(!dragging)
{
// we get the mouses current position
let pos = e.data.global;
// and check if that new position has
// move more than 5 pixels in any direction
// from the first mouse down position
if(Math.abs(start.x - pos.x) > 5 || Math.abs(start.y - pos.y) > 5)
{
// if it has we can assume the user
// is trying to draw the view around
// and not clicking. We store the
// graphics current position do we
// can offset its postion with the
// mouse position later.
graphicsStart = {x: graphics.position.x, y: graphics.position.y};
// set the dragging flag
dragging = true;
// add the .dragging class to the
// DOM so we can switch to the
// move cursor
body.classList.add('dragging');
}
}
if(dragging)
{
// update the graphics position based
// on the mouse position, offset with the
// start and graphics orginal positions
graphics.position.x = ((e.data.global.x - start.x)/scale) + graphicsStart.x;
graphics.position.y = ((e.data.global.y - start.y)/scale) + graphicsStart.y;
gridLines.position.x = ((e.data.global.x - start.x)/scale) + graphicsStart.x;
gridLines.position.y = ((e.data.global.y - start.y)/scale) + graphicsStart.y;
}
}
}
function onUp(e)
{
// clear the .dragging class from DOM
body.classList.remove('dragging');
// ignore the mouse up if the mouse down
// was out of bounds (e.g in the bottom
// 60px)
if(mouseDown && ready)
{
// clear mouseDown flag
mouseDown = false;
// if the dragging flag was never set
// during all the mouse moves then this
// is a click
if(!dragging)
{
// if a color has been selected and
// the view is zoomed in then this
// click is to draw a new pixel
if(selectedColor && zoomed)
{
// get the latest mouse position
let position = e.data.getLocalPosition(graphics);
// round the x and y down
let x = Math.floor(position.x / squareSize[0]);
let y = Math.floor(position.y / squareSize[1]);
writePixel(x, y, selectedColor);
}
else
{
// either a color has not been selected
// or it has but the user is zoomed out,
// either way this click is to toggle the
// zoom level
toggleZoom(e.data.global)
}
}
dragging = false;
}
}
function renderPixel(pos:string, pixel: Pixel)
{
// split the pos string at the 'x'
// so '100x200' would become an
// array ['100', '200']
let split = pos.split('x');
// assign the values to x and y
// vars using + to convert the
// string to a number
let x = +split[0];
let y = +split[1];
// grab the color from the pixel
// object
let color = pixel.color;
// draw the square on the graphics canvas
graphics.beginFill(parseInt('0x' + color), 1);
graphics.drawRect(x * squareSize[0], y * squareSize[1], squareSize[0], squareSize[1]);
}
function toggleZoom(offset:Position, forceZoom?:boolean)
{
console.log(forceZoom)
// toggle the zoomed varable
zoomed = forceZoom !== undefined ? forceZoom : !zoomed;
// scale will equal 4 if zoomed (so 4x bigger),
// other otherwise the scale will be 1
scale = zoomed ? zoomLevel : 1;
// add or remove the .zoomed class to the
// body tag. This is so we can change the
// info box instructions
if(zoomed) body.classList.add('zoomed');
else body.classList.remove('zoomed');
let opacity = zoomed ? 1 : 0;
// Use GSAP to animate between scales.
// We are scaling the container and not
// the graphics.
TweenMax.to(container.scale, 0.5, {x: scale, y: scale, ease:Power3.easeInOut});
let x = offset.x - (window.innerWidth / 2);
let y = offset.y - (window.innerHeight / 2);
let newX = zoomed ? graphics.position.x - x : graphics.position.x + x;
let newY = zoomed ? graphics.position.y - y : graphics.position.y + y;
TweenMax.to(graphics.position, 0.5, {x: newX, y: newY, ease:Power3.easeInOut});
TweenMax.to(gridLines.position, 0.5, {x: newX, y: newY, ease:Power3.easeInOut});
TweenMax.to(gridLines, 0.5, {alpha: opacity, ease:Power3.easeInOut});
}
/*
Firebase database rules are set to...
{
"rules": {
"last_write": {
"$user": {
".read": "auth.uid === $user",
".write": "newData.exists() && auth.uid === $user",
".validate": "newData.isNumber() && newData.val() === now && (!data.exists() || newData.val() > data.val()+10000)"
}
},
"pixel": {
".read": "true",
"$square": {
".write": "auth !== null",
".validate": "newData.hasChildren(['uid', 'color', 'timestamp']) && $square.matches(/^([0-9]|[1-9][0-9]|[1-9][0-9][0-9])x([0-9]|[1-9][0-9]|[1-9][0-9][0-9])$/)",
"uid": {
".validate": "newData.val() === auth.uid"
},
"timestamp": {
".validate": "newData.val() >= now - 500 && newData.val() === data.parent().parent().parent().child('last_write/'+auth.uid).val()"
},
"color": {
".validate": "newData.isString() && newData.val().length === 6 && newData.val().matches(/^(ffffff|e4e4e4|888888|222222|ffa7d1|e50000|e59500|a06a42|e5d900|94e044|02be01|00d3dd|0083c7|0000ea|cf6ee4|820080)$/)"
}
}
}
}
}
*/
// the rules to prevent adding pixels during cooldown
// were written with help from http://stackoverflow.com/a/24841859
<script src="//codepen.io/steveg3003/pen/zBVakw.js"></script>
<script src="https://www.gstatic.com/firebasejs/3.7.5/firebase.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.4.3/pixi.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.1/TweenMax.min.js"></script>
html, body
{
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
font-family: "Lato", "Lucida Grande","Lucida Sans Unicode", Tahoma, Sans-Serif;
user-select: none;
}
body
{
background-color: lightgray;
}
#canvas
{
position: absolute;
top: 0;
bottom: 60px;
left: 0;
right: 0;
//pointer-events: none;
}
.controls
{
position: absolute;
left: 0;
right: 0;
bottom: 0;
//height: 40px;
background-color: #343436;
padding: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
overflow: hidden;
.face-space
{
width: 60px;
}
}
#auth
{
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
color: white;
padding: 0 20px;
height: 100%;
min-height: 60px;
background-color: #66666E;
&:hover
{
background-color: #000;
}
svg
{
height: 100%;
}
span
{
margin-left: 10px;
}
}
#colors
{
margin: 0;
padding: 10px;
flex: 1;
text-align: center;
transition: transform 0.5s ease-in-out;
position: relative;
&:before
{
color: white;
display: block;
position: absolute;
bottom: 100%;
width: 100%;
padding: 10px;
//margin-bottom: 10px;
text-align: center;
content: "You're free to look around, but to help prevent spam you need to login before you can draw, sorry about that."
}
li
{
width: 25px;
height: 25px;
display: inline-block;
list-style: none;
margin: 0;
&.active
{
outline: 3px solid white;
}
&:not(:last-child)
{
margin-right: 5px;
}
&#c-ffffff { background-color: #ffffff; outline-color: #bbb; }
&#c-e4e4e4 { background-color: #e4e4e4; }
&#c-888888 { background-color: #888888; }
&#c-222222 { background-color: #222222; }
&#c-ffa7d1 { background-color: #ffa7d1; }
&#c-e50000 { background-color: #e50000; }
&#c-e59500 { background-color: #e59500; }
&#c-a06a42 { background-color: #a06a42; }
&#c-e5d900 { background-color: #e5d900; }
&#c-94e044 { background-color: #94e044; }
&#c-02be01 { background-color: #02be01; }
&#c-00d3dd { background-color: #00d3dd; }
&#c-0083c7 { background-color: #0083c7; }
&#c-0000ea { background-color: #0000ea; }
&#c-cf6ee4 { background-color: #cf6ee4; }
&#c-820080 { background-color: #820080; }
}
}
.cooldown
{
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: #343436;
flex-direction: row;
justify-content: center;
align-items: center;
color: white;
text-align: center;
display: none;
}
.info
{
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
color: white;
font-size: 12px;
span
{
color: #ff5555;
}
&.drawing { display: none; }
&.loading { display: none; }
&.general { display: block; }
}
.loading
{
.info
{
&.drawing { display: none !important; }
&.loading { display: block !important; }
&.general { display: none !important; }
}
}
.zoom-controls
{
position: absolute;
top: 10px;
right: 10px;
>div
{
background-color: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
color: white;
font-size: 12px;
&:hover
{
cursor: pointer;
background-color: black;
}
&:not(:first-child)
{
margin-top: 5px;
}
}
}
.selectedColor
{
&.zoomed
{
.info
{
&.drawing { display: block; }
&.general { display: none; }
}
}
}
.dragging
{
#canvas
{
cursor: move;
}
}
.logged-out
{
#auth
{
animation: 1s linear pulse infinite;
}
#colors
{
transform: translateY(100%);
}
}
.cooling
{
.cooldown
{
display: flex;
}
}
@keyframes pulse {
0% {
background: #66666E;
}
50% {
background: #DB5461;
}
100% {
background: #66666E;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment