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.
ul.keyboard
img.keyboard__icon(src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/2361/keyboard.svg")
li.keyboard__item
| wipe the board
span.keyboard__key W
li.keyboard__item
| kill all agents
span.keyboard__key Q
li.keyboard__item
| toggle agent visibiity
span.keyboard__key T
li.keyboard__item
| add a creator
span.keyboard__key C
li.keyboard__item
| add a destroyer
span.keyboard__key D
li.keyboard__item
| pause/unpause agents
span.keyboard__key Space
@import url('https://fonts.googleapis.com/css?family=Ubuntu+Mono');
body {
margin: 0;
background: #000;
overflow: hidden;
min-height: 100%;
font-family: 'Ubuntu Mono', monospace;
}
canvas {
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
cursor: pointer;
}
.keyboard {
position: absolute;
overflow: hidden;
list-style: none;
z-index: 1;
bottom: 0;
right: 0;
padding: 0.2em 1em 1em;
margin: 0;
max-height: 2.6em;
max-width: 3.6em;
text-align: right;
font-size: 12px;
background-color: rgba(#000, 0);
color: #fff;
opacity: 0.4;
border-top-left-radius: 5px;
transition: opacity 0.3s ease, background-color 0.2s ease, max-height 0.3s ease-in-out, max-width 0s 0.3s ease-in-out;
&:hover {
transition: opacity 0.3s ease, background-color 0.2s ease, max-height 0.3s ease-in-out, max-width 0s 0s ease-in-out;
max-height: 20em;
max-width: 20em;
opacity: 1;
background-color: rgba(#000, 0.7);
}
&__icon {
width: 3.4em;
height: 3.4em;
cursor: pointer;
}
&__item {
margin: 0.5em 0 0;
}
&__key {
display: inline-block;
min-width: 3em;
text-align: center;
background: rgba(#fff, 0.2);
border-radius: 2px;
padding: 0.2em;
margin: -0.2em 0 0 1em;
}
}
// Variables
//======================================
const hexRadius = 18; // from center to one of the points
const hexLineWeight = 1; // thickness of drawing line
const hexDoubleLineOffset = 6; // space between double lines
const hexMargin = 2; // space around hexagons
const drawHex = true; // draw hexagon background
const drawLines = true; // draw all the lines
const drawPoints = false; // also draw lone double active hexagons as circles
const zenoSway = 0.2; // hexagon background colour transition scalar
// start with 1 creator and 3 destroyers
let creatorCount = 1;
let destroyerCount = 3;
let drawAgents = true; // colour in agent hexagons red/white
let agentsMoving = true; // animating the agents, toggled with spacebar
let hexHeight, hexWidth, columns, rows;
let hexagons = [];
let agents = [];
// Helper Functions
//======================================
function drawHexagon(pixelPos) {
// draws hexagon with the center pixelPos
push();
translate(pixelPos.x, pixelPos.y);
beginShape();
for (let i = 0; i < 6; i++) {
vertex((hexRadius-hexMargin/2)*cos(i*Math.PI/3), (hexRadius-hexMargin/2)*sin(i*Math.PI/3));
}
endShape(CLOSE);
pop();
}
function getEdgePos(i, offset) {
// return position of this edge of the hexagon
// if (offset == 1) clockwise from middle edge
// if (offset == 0) middle of edge
// if (offset == -1) anti-clockwise from middle edge
var pos = createVector(offset*hexDoubleLineOffset*0.5, -hexHeight/2);
pos.rotate(i*Math.PI/3);
return pos;
}
function wrap6(num) {
// -1 => 5
// 0 => 0
// 5 => 5
// 6 => 0
// 7 => 1
return (num+6) % 6;
}
// Setup
//======================================
function setup() {
// calculate width and height of hexagons
hexWidth = hexRadius * 2;
hexHeight = Math.sqrt(3)*hexRadius;
// set rows and columns to overlap page edge
columns = Math.ceil(window.innerWidth / (hexRadius * 3));
rows = Math.ceil(window.innerHeight / (hexHeight / 2)) + 1;
// set up canvas
createCanvas((columns + 1/4) * (hexRadius * 3), (rows + 1) * (hexHeight / 2));
frameRate(60);
fill(255, 100);
stroke(255);
strokeWeight(5);
noStroke();
// initialise 2D array of hexagons
for (let x = 0; x < columns; x++) {
hexagons.push([]);
for (let y = 0; y < rows; y++) {
hexagons[x].push(new Hex(x, y));
}
}
// neighbouring needs to be done after they're all initialised
for (let x = 0; x < columns; x++) {
for (let y = 0; y < rows; y++) {
hexagons[x][y].initialiseNeighbours(x, y);
}
}
// initialise agents
for (let i = 0; i < creatorCount + destroyerCount; i++) {
let creator = (i < creatorCount) ? true : false;
agents.push(new Agent(creator));
}
}
// Global Draw
//======================================
function draw() {
background(25);
// important to draw all hexagons before lines to avoid overlap
if (drawHex) {
for (let y = 0; y < rows; y++) {
for (let x = 0; x < columns; x++) {
hexagons[x][y].drawHex();
}
}
}
if (drawAgents) {
for (let i = 0; i < creatorCount + destroyerCount; i++) {
agents[i].draw();
}
}
if (drawLines) {
for (let y = 0; y < rows; y++) {
for (let x = 0; x < columns; x++) {
hexagons[x][y].drawLines();
}
}
}
update();
}
// Global Update
//======================================
function update() {
if (agentsMoving) {
for (let i = 0; i < creatorCount + destroyerCount; i++) {
agents[i].update();
}
}
for (let y = 0; y < rows; y++) {
for (let x = 0; x < columns; x++) {
hexagons[x][y].update();
}
}
}
// Agent Class
//======================================
class Agent {
constructor(creator) {
// randomly place near centre of screen
this.x = Math.round(columns * (0.3 + random(0.4)));
this.y = Math.round(rows * (0.3 + random(0.4)));
// set random direction 0-5
this.dir = Math.floor(random(0, 6));
// set its morality
this.creator = creator;
}
draw() {
noStroke();
if (this.creator) {
fill(255, 30);
} else {
fill(255, 0, 100, 40);
}
// grab pixel position from corresponding hexagon
drawHexagon(hexagons[this.x][this.y].pixelPos);
}
update() {
// get current hexagon by x, y
var curHex = hexagons[this.x][this.y];
// increment or decrement activity
// if creator and not double active
if (this.creator) {
if (curHex.nextActive < 2) {
curHex.nextActive++;
}
}
// if destroyer and active
else {
if (curHex.nextActive > 0) {
curHex.nextActive--;
}
}
// randomly chose direction -1 to 1
this.dir += -1 + Math.floor(random(3));
// make direction wrap around 0-5
this.dir = wrap6(this.dir);
// get next hexagon from current's neighbours
var nextHex = curHex.neighbours[this.dir];
// if next hexagon doesn't exist turn around
if (nextHex === false) {
this.dir = wrap6(this.dir + 3);
nextHex = curHex.neighbours[this.dir];
// if that doesn't work it's a corner
// return and try again next round
if (nextHex === false) return;
}
// update x and y from next hexagon
this.x = nextHex.pos.x;
this.y = nextHex.pos.y;
}
}
// Hexagon Class
//======================================
class Hex {
constructor(x, y) {
// establish grid position
this.pos = createVector(x, y);
// establish pixel position
this.pixelPos = createVector(0, 0);
this.pixelPos.x = hexWidth * (1.5 * x + 0.5 + y % 2 * 0.75);
this.pixelPos.y = hexHeight * (y * 0.5 + 0.5);
// establish state
// active can be 0, 1, 2
// double active results in double lines
this.active = 0;
this.nextActive = 0;
// establish neighbours
this.neighbours = [];
// chose random layout (1-3) for dense (4/5/6 neighbours) display
// regenerated when hex goes from inactive to active
this.denseLayout = Math.ceil(random(3));
// lazily updating count of active neighbours
// used to colour hexagons
this.zenosNeighbours = 0;
}
initialiseNeighbours(x, y) {
// initialise neighbours called after all hexagons are constructed
// because otherwise the hexagons array isn't full yet
// lots of conditionals to allow for edge hexagons
// start with array of falses
let n = [false, false, false, false, false, false];
const odd = y%2;
// above
if (y >= 2) {
n[0] = hexagons[x][y-2];
}
// top right
if (y >= 1) {
if (!odd || x < columns-1) {
n[1] = hexagons[x+odd][y-1];
}
}
// bottom right
if (y < rows-1) {
if (!odd || x < columns-1) {
n[2] = hexagons[x+odd][y+1];
}
}
// bottom
if (y < rows-2) {
n[3] = hexagons[x][y+2];
}
// bottom left
if (y < rows-1) {
if (odd || x >= 1) {
n[4] = hexagons[x-1+odd][y+1];
}
}
// top left
if (y >= 1) {
if (odd || x >= 1) {
n[5] = hexagons[x-1+odd][y-1];
}
}
this.neighbours = n;
}
update() {
// randomise layout if hex is becoming active
if (!this.active && this.nextActive) {
this.denseLayout = Math.ceil(random(3));
}
// update active from next active
this.active = this.nextActive;
// lazily update zenosNeighbours
// zenosNeighbours lazily equals the amount of active neighbours (0-6)
// plus the currect active state (0-2)
if (this.zenosNeighbours == 0) {
// make accurate if 0
this.zenosNeighbours = this.countActiveNeighbours() + this.active;
} else {
this.zenosNeighbours = this.zenosNeighbours*(1-zenoSway) + zenoSway*(this.countActiveNeighbours() + this.active);
}
}
countActiveNeighbours() {
// returns number of active neighbours
let activeNeighbours = 0;
for (let i = 0; i < 6; i++) {
if (this.neighbours[i] && this.neighbours[i].active) {
activeNeighbours++;
}
}
return activeNeighbours;
}
getActiveNeighbours() {
// returns array of booleans for active neighbours
let activeNeighbours = [];
for (let i = 0; i < 6; i++) {
// if neighbour exists and is active
if (this.neighbours[i] && this.neighbours[i].active) {
activeNeighbours.push(true);
} else {
activeNeighbours.push(false);
}
}
return activeNeighbours;
}
drawHex() {
// called in global draw
noStroke();
let brightness = this.zenosNeighbours;
fill(5*brightness,
6*Math.pow(brightness, 1.6),
16*brightness);
if (!this.active) fill(0);
drawHexagon(this.pixelPos);
}
drawLines() {
// called in global draw
push();
translate(this.pixelPos.x, this.pixelPos.y);
if (this.active) { // truthy
let activeNeighboursCount = this.countActiveNeighbours();
let activeNeighbours = this.getActiveNeighbours();
stroke(255);
strokeWeight(hexLineWeight);
noFill();
// no neighbours
if (activeNeighboursCount == 0) {
if (drawPoints && this.active == 2) {
ellipse(0, 0, hexDoubleLineOffset);
}
}
// one neighbour
else if (activeNeighboursCount == 1) {
let activeEdge = activeNeighbours.indexOf(true);
let activeNeighbour = this.neighbours[activeEdge];
// if it is double active
// or the active neighbour is double active
if (activeNeighbour.active == 2 ||
this.active == 2) {
// if drawPoints is inactive the neighbour must have > 1 active neighbour
// to avoid ellipses on an active edge
if (drawPoints || activeNeighbour.countActiveNeighbours() > 1) {
// get two edge points
var pos1 = getEdgePos(activeEdge, 1);
var pos2 = getEdgePos(activeEdge, -1);
// get two control points
var control1 = createVector(hexDoubleLineOffset*0.5, -hexHeight/2+hexDoubleLineOffset).rotate(activeEdge*Math.PI/3);
var control2 = createVector(-hexDoubleLineOffset*0.5, -hexHeight/2+hexDoubleLineOffset).rotate(activeEdge*Math.PI/3);
// draw bezier curve for arc cap
beginShape();
vertex(pos1.x, pos1.y);
bezierVertex(control1.x, control1.y, control2.x, control2.y, pos2.x, pos2.y);
endShape();
}
}
}
// two or three neighbours
else if (activeNeighboursCount == 2 || activeNeighboursCount == 3) {
// link up all the active neighbours
for (var i = 0; i < 6; i++) {
if (activeNeighbours[i]) {
for (var j = i+1; j < 6; j++) {
if (activeNeighbours[j]) {
this.drawCurveBetweenEdges(i, j);
}
}
}
}
}
// four neighbours
else if (activeNeighboursCount == 4) {
// get the index of each inactive edge
let skipped1 = activeNeighbours.indexOf(false);
let skipped2 = activeNeighbours.slice(skipped1+1).indexOf(false) + skipped1 + 1;
// make list of active edge positions
var positions = [];
for (let i = 0; i < 6; i++) {
if (i != skipped1 && i != skipped2) {
positions.push(i);
}
}
// skips are adjacent
if (skipped2 - skipped1 == 1) {
if (this.denseLayout == 3) {
// connect edges to adjacent edges, ignore straight line
this.drawCurveBetweenEdges(positions[0], positions[1]);
this.drawCurveBetweenEdges(positions[1], positions[2]);
this.drawCurveBetweenEdges(positions[2], positions[3]);
}
else if (this.denseLayout == 2) {
// cross over curves
this.drawCurveBetweenEdges(positions[0], positions[2]);
this.drawCurveBetweenEdges(positions[1], positions[3]);
}
else {
// pair edges with adjacent edges
this.drawCurveBetweenEdges(positions[0], positions[1]);
this.drawCurveBetweenEdges(positions[2], positions[3]);
}
}
// 1 and 3 situation
// or 2 and 2
else {
if (this.denseLayout == 3) {
// connect edges to adjacent edges
this.drawCurveBetweenEdges(positions[0], positions[1]);
this.drawCurveBetweenEdges(positions[1], positions[2]);
this.drawCurveBetweenEdges(positions[2], positions[3]);
this.drawCurveBetweenEdges(positions[3], positions[0]);
}
else if (this.denseLayout == 2) {
// pair edges with adjacent edges
this.drawCurveBetweenEdges(positions[3], positions[0]);
this.drawCurveBetweenEdges(positions[1], positions[2]);
}
else {
// pair edges with opposite adjacent edges
this.drawCurveBetweenEdges(positions[0], positions[1]);
this.drawCurveBetweenEdges(positions[2], positions[3]);
}
}
}
// five neighbours
else if (activeNeighboursCount == 5) {
let skipped = activeNeighbours.indexOf(false);
if (this.denseLayout == 3) {
// connect edges to adjacent edges
for (var i = skipped; i < 5 + skipped; i++) {
var edge1 = (i == skipped) ? i+5 : i;
this.drawCurveBetweenEdges(edge1, i+1);
}
}
else if (this.denseLayout == 2) {
// batman logo
// curve between the two skipped-adjacent edges
this.drawCurveBetweenEdges(skipped+1, skipped+5);
// connect other 3 to eachother
this.drawCurveBetweenEdges(skipped+2, skipped+3);
this.drawCurveBetweenEdges(skipped+3, skipped+4);
}
else if (this.denseLayout == 1) {
// evil M
// curve the two skipped-adjacent edges to the skipped-opposite edge
this.drawCurveBetweenEdges(skipped+1, skipped+3);
this.drawCurveBetweenEdges(skipped+5, skipped+3);
// curve the other two edges to the skipped-adjacent edges
this.drawCurveBetweenEdges(skipped+1, skipped+2);
this.drawCurveBetweenEdges(skipped+5, skipped+4);
}
}
// 6 neighbours
else {
if (this.denseLayout == 3) {
// connect edges to adjacent edges
for (var i = 0; i < 6; i++) {
this.drawCurveBetweenEdges(i, i+1);
}
}
else {
// pair edges with adjacent edges
// alternate using denseLayout == 2 or 1
for (var i = this.denseLayout - 1; i < 6; i+=2) {
this.drawCurveBetweenEdges(i, i+1);
}
}
}
}
pop();
}
drawCurveBetweenEdges(edge1, edge2) {
// called by drawLines()
// used to determine whether they should be single, double, or diverging
// also used to set curve offsets for inner/outer lines
// make sure edges are between 0-5
edge1 = wrap6(edge1);
edge2 = wrap6(edge2);
// should we draw it as a double line?
let double = false;
// if the tile is double active
if (this.active == 2) double = true;
// if both of the edge tiles exist and are double active
if ((this.neighbours[edge1] && this.neighbours[edge1].active == 2) &&
(this.neighbours[edge2] && this.neighbours[edge2].active == 2)) {
double = true;
}
if (double) {
// set outer control point slightly further than offset width
// to create even margin
// cos bezier curves, son
this.drawCurveWithOffset(edge1, edge2, 1, 1, -hexDoubleLineOffset*0.8);
this.drawCurveWithOffset(edge1, edge2, -1, -1, hexDoubleLineOffset*0.5);
}
// if tile is single active
// and not both of the edges are double active
else {
// if edge1 hexagon exists and is double active
if ((this.neighbours[edge1] && this.neighbours[edge1].active == 2)) {
// use half offset width for midpoint of diverging lines
// set outer control point slightly further to create even margin
this.drawCurveWithOffset(edge1, edge2, 1, 0, -hexDoubleLineOffset*0.4);
this.drawCurveWithOffset(edge1, edge2, -1, 0, hexDoubleLineOffset*0.25);
}
// if edge2 hexagon exists and is double active
else if ((this.neighbours[edge2] && this.neighbours[edge2].active == 2)) {
// use half offset width for midpoint between single and double
// set outer control point slightly further to create even margin
this.drawCurveWithOffset(edge1, edge2, 0, 1, -hexDoubleLineOffset*0.4);
this.drawCurveWithOffset(edge1, edge2, 0, -1, hexDoubleLineOffset*0.25);
}
// if everything is single
else {
this.drawCurveWithOffset(edge1, edge2, 0, 0);
}
}
}
drawCurveWithOffset(edge1, edge2, offset1, offset2, originOffset) {
// called by drawCurveBetweenEdges()
// determines which is the inner and outer line
// sets offset and draws line accordingly
// originOffset is the distance we move the origin point
// in the opposite direction of the average angle of the two points
let origin = createVector(0, 0);
if (originOffset) {
origin.y = originOffset;
}
// set up positions as per offsets
let pos1 = getEdgePos(edge1, offset1);
let pos2 = getEdgePos(edge2, offset2);
// if edge 2 is one clockwise from edge 1
if (edge1 == wrap6(edge2-1)) {
// flips offset 2
pos2 = getEdgePos(edge2, -offset2);
// brings offset in smooth curve
origin.y -= hexRadius * 0.25;
origin.rotate((edge1+0.5)*Math.PI/3);
}
// if edge 2 is one anti-clockwise from edge 1
else if (edge1 == wrap6(edge2+1)) {
// flips offset 1
pos1 = getEdgePos(edge1, -offset1);
// brings offset in smooth curve
origin.y -= hexRadius * 0.25;
origin.rotate((edge1-0.5)*Math.PI/3);
}
// if edge 2 is two clockwise from edge 1
else if (edge1 == wrap6(edge2-2)) {
// flips offset 2
pos2 = getEdgePos(edge2, -offset2);
origin.rotate((edge1+1)*Math.PI/3);
}
// if edge 2 is two anti-clockwise from edge 1
else if (edge1 == wrap6(edge2+2)) {
// flips offset 1
pos1 = getEdgePos(edge1, -offset1);
origin.rotate((edge1-1)*Math.PI/3);
}
// if edges are opposites
// using the line function everything is 1px off
// so we just use the quadratic bezier
if (Math.abs(edge2-edge1) == 3) {
// flip the offset 2 to create parallel lines
pos2 = getEdgePos(edge2, -offset2);
// reset origin offset so line is straight
origin.y = (hexDoubleLineOffset*0.5) * (offset1+offset2)*0.5 * (edge1-edge2)/3;
origin.rotate((edge1+1.5)*Math.PI/3);
}
// draw the line
beginShape();
vertex(pos1.x, pos1.y);
quadraticVertex(origin.x, origin.y, pos2.x, pos2.y);
endShape();
}
}
// Keyboard Events
//======================================
function keyPressed() {
// Spacebar
//------------------------------------
if (keyCode == 32) {
agentsMoving = !agentsMoving;
}
// W - wipe the board
//------------------------------------
if (keyCode == 87) {
for (let x = 0; x < columns; x++) {
for (let y = 0; y < rows; y++) {
hexagons[x][y].nextActive = false;
}
}
}
// Q - kill all agents
//------------------------------------
if (keyCode == 81) {
creatorCount = 0;
destroyerCount = 0;
agents = [];
}
// T - toggle agent visibility
//------------------------------------
if (keyCode == 84) {
drawAgents = !drawAgents;
}
// C - add a creator
//------------------------------------
if (keyCode == 67) {
creatorCount++;
agents.push(new Agent(true));
}
// D - add a destroyer
//------------------------------------
if (keyCode == 68) {
destroyerCount++;
agents.push(new Agent(false));
}
}
Also see: Tab Triggers