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.
<!-- Lost like non-janky frames in the rain... -->
<div id="vignette"></div>
body {
background: black;
margin: 0;
padding: 0;
overflow: hidden;
}
canvas {
background: transparent;
background-image: linear-gradient(black 20%,
#101 30%,
#211 40%,
#070702 52%,
#000 90%,
#000 100%);
background-repeat: no-repeat;
display: block;
margin: 0 auto;
width: 100%;
max-width: 1800px;
height: 300px;
//image-rendering: pixelated;
}
#vignette {
background-image: linear-gradient(right, black 0%, transparent 10%, transparent 90%, black 100%);
position: absolute;
top: 0;
left: 50%;
width: 100%;
height: 300px;
max-width: 1800px;
transform: translateX(-50%);
z-index: 50;
}
console.clear()
// OVERENGINEERED UNOPTIMIZED CANVAS BULLSH*T
// BUT IT'S OKAY SINCE IT'S BLADE RUNNER INNIT
// Some stuff left unoptimized / verbose to show the work.
// TODO:
// - optimize render loop, avoid overdraws, etc
// - smoothly fade rows in on the horizon
// Constants. Change at own risk
const CANVAS_WIDTH = 900
const CANVAS_HEIGHT = 300
const FRAME_TIME = 1000 / 16
const LIGHT_ROWS = 20
const LIGHT_ROW_DEPTH = 2
const LIGHT_SPACING = 0.6
const LIGHT_SIZE = 0.1
const LIGHT_SCATTER = 0.4
const BUILDING_ROWS = 38
const BUILDING_ROW_DEPTH = 1
const BUILDING_ROW_WIDTH = 60
const BUILDING_MIN_HEIGHT = 1.5
const BUILDING_MAX_HEIGHT = 3
const STACK_HEIGHT = 9
const STACK_THRESHOLD = 0.87
const STACK_LIGHT_CHANCE = 0.95
const STACK_LIGHT_SIZE = 0.13
const FADE_GRAY_VALUE = 25
const FADE_OFFSET = 0.35
// Virtual camera. Used in perspective calculations
const CAMERA = {
x: 0,
y: 10,
z: 0,
fov: 170,
dist: 30,
zSpeed: 0.005,
}
// Virtual vanishing point XY. Used in perspective calculations
const VP_OFS = {
x: 0.5,
y: 0.27,
}
// Global hoisted vars for rendering contexts and timers
let c, ctx, output_c, output_ctx
let _t, _dt, _ft
// Seedable random number generator.
// Not particularly well-distributed, but fine for this case.
// Allows us to emit the same set of random numbers on every frame
// so we can consistently re-render the scene.
const RNG = {
seed: 1,
random() {
const x = Math.sin(RNG.seed++) * 10000
return x - (x << 0)
},
randomInRange(min, max) {
return ((RNG.random() * (max - min + 1)) << 0) + min
}
}
// Module to get a random colour from a predefined list.
// Uses the seedable RNG
const Palette = (() => {
const PAL = ['black', '#111', '#113', 'white', 'sliver', '#f88', 'orange', 'oldlace', '#569']
const lastIndex = PAL.length - 1
function getRandomFromPalette() {
return PAL[RNG.randomInRange(0, lastIndex)]
}
return {
getRandom: getRandomFromPalette
}
})()
function ceil(n) {
var f = (n << 0), f = f == n ? f : f + 1
return f
}
// Update method of main loop
function update() {
// Update our global timestamp (used in rendering)
_t = Date.now() * 0.001
// Move the camera slowly 'forward'
CAMERA.z += CAMERA.zSpeed
}
// Draw a frame of the scene.
// Uses the current timestamp and the seeded RNG to render a
// pseudorandom cityscape with lights and buildings.
// We always generate and draw a set amount of city in front of
// the camera, so it appears to be endless as we 'fly over' it.
//
// 1. Clear the whole scene
// 2. Render random rows of lights
// 3. Render random rows of buildings
// 4. Blit scene to onscreen canvas
let _$ = {
vPointX: 0,
vPointY: 0,
rowScreenX: 0,
MAX_LIGHTS: 0,
closestLightRow: 0,
rowZ: 0,
rowRelativeZ: 0,
scalingFactor: 0,
rowScreenWidth: 0,
rowScreenHeight: 0,
rowScreenY: 0,
rowScreenLightSpacing: 0,
rowLightCount: 0,
lightSize: 0,
lightHalfSize: 0,
lightScreenX: 0,
lightScreenY: 0,
closestBuildingRow: 0,
rowBuildingCount: 0,
rowBuildingScreenWidth: 0,
rowShade: 0,
rowStyleString: '',
lightData: [],
isStack: false,
buildingHeight: 0,
buildingScreenHeight: 0,
buildingScreenX: 0,
buildingScreenY: 0,
lightSize: 0,
lightHalfSize: 0,
lightColor: 0,
}
function render() {
// Calculate the pixel XY of the vanishing point
// (could be done on init, but useful if we ever want to
// dynamically move the camera)
_$.vPointX = c.width * VP_OFS.x >> 0
_$.vPointY = c.height * VP_OFS.y >> 0
// If we wanted to, we could give each row an X offset
// and include it in perspective calculations,
// but we just use the centre alignment for each one here.
_$.rowScreenX = CAMERA.x + _$.vPointX
// 1. Clear the whole scene...
// (canvases are transparent so that the CSS 'sky' gradient can be seen)
ctx.clearRect(0, 0, c.width, c.height)
output_ctx.clearRect(0, 0, output_c.width, output_c.height)
// 2. Render random rows of lights...
// Calculate the closest row to the camera so we
// can render the required number of rows into the distance
_$.closestLightRow = Math.floor(CAMERA.z / LIGHT_ROW_DEPTH)
// Draw each row of lights
for (let i = 0; i < LIGHT_ROWS; i++) {
// Calculate this row's base Z position
// and Z relative to camera
_$.rowZ = (_$.closestLightRow * LIGHT_ROW_DEPTH) + (LIGHT_ROW_DEPTH * i)
_$.rowRelativeZ = _$.rowZ - CAMERA.z
// Don't draw the row if it's behind the camera,
// or beyond the camera's draw distance
if (_$.rowRelativeZ <= 0 || _$.rowRelativeZ > CAMERA.dist) {
continue
}
// Get the perspective scaling factor and pixel Y position for this row
_$.scalingFactor = CAMERA.fov / _$.rowRelativeZ
_$.rowScreenY = CAMERA.y * _$.scalingFactor + _$.vPointY
// Don't draw the row if it's off-canvas
if (_$.rowScreenY > c.height) {
continue
}
// Calculate the spacing and number of lights we need to render for this row
_$.rowScreenLightSpacing = LIGHT_SPACING * _$.scalingFactor
_$.rowLightCount = c.width / _$.rowScreenLightSpacing
// Seed the RNG in a way that gets us decent distribution
// for the random lights
RNG.seed = _$.rowZ * 0.573
// Render the random lights for this row
for (let j = 0; j < _$.rowLightCount; j++) {
// Randomize light size, with perspective
_$.lightSize = RNG.random() * (LIGHT_SIZE * _$.scalingFactor)
_$.lightHalfSize = _$.lightSize * 0.5
// Randomly offset the XY of the light, with perspective
_$.lightScreenX = (j * _$.rowScreenLightSpacing) +
(RNG.random() * LIGHT_SCATTER * _$.scalingFactor) - _$.lightHalfSize
_$.lightScreenY = (_$.rowScreenY + (RNG.random() * LIGHT_SCATTER) * _$.scalingFactor) - _$.lightHalfSize
// Don't render if the light is offscreen
if (_$.lightScreenX < 0 || _$.lightScreenX > c.width ||
_$.lightScreenY > c.height) {
// HACK: we still need to call the RNG the same number of times
// for every row to ensure consistency between frames. If we didn't
// do this, the lights would jump all over the place near the edges
// of the screen.
Palette.getRandom()
continue
}
// Pick a random colour for this light
ctx.fillStyle = Palette.getRandom()
// Render the light twice, mirrored either side of the centre vanishing point.
// Saves us having to do perspective offset calculation for every light,
// and won't be noitceable when we overlay the city buildings.
ctx.fillRect((_$.rowScreenX + _$.lightScreenX),
_$.lightScreenY, _$.lightSize, _$.lightSize)
ctx.fillRect((_$.rowScreenX - _$.lightScreenX),
_$.lightScreenY, _$.lightSize, _$.lightSize)
}
}
// 3. Render random rows of buildings...
// Calculate the closest row to the camera so we
// can render the required number of rows into the distance
_$.closestBuildingRow = Math.floor(CAMERA.z / BUILDING_ROW_DEPTH)
// Draw each row of buildings
for (let i = BUILDING_ROWS; i > 0; i--) {
// Calculate this row's base Z position
// and Z relative to camera
_$.rowZ = (_$.closestBuildingRow * BUILDING_ROW_DEPTH) + (BUILDING_ROW_DEPTH * i)
_$.rowRelativeZ = _$.rowZ - CAMERA.z
// Don't draw the row if it's behind the camera,
// or beyond the camera's draw distance
if (_$.rowRelativeZ <= 0 || _$.rowRelativeZ > CAMERA.dist) {
continue
}
// Get the perspective scaling factor and pixel Y position for this row
_$.scalingFactor = CAMERA.fov / _$.rowRelativeZ
// Calculate the perspective-scaled position and base size of our row.
// Offset the XY so that the row's 'origin' is at centre bottom (i.e. ground-up)
_$.rowScreenWidth = BUILDING_ROW_WIDTH * _$.scalingFactor;
_$.rowScreenHeight = BUILDING_MAX_HEIGHT * _$.scalingFactor;
_$.rowScreenX = CAMERA.x * _$.scalingFactor + _$.vPointX - (_$.rowScreenWidth * 0.5)
_$.rowScreenY = CAMERA.y * _$.scalingFactor + _$.vPointY - _$.rowScreenHeight
// Seed the RNG to keep rendering consistent for this row
RNG.seed = _$.rowZ
// Calculate a random number of buildings for this row
// and get their screen width
_$.rowBuildingCount = RNG.randomInRange(20, 70)
_$.rowBuildingScreenWidth = _$.rowScreenWidth / _$.rowBuildingCount
// Calculate the shade we want the buildings in this row to be.
// The tint is darker nearer the camera, giving a sort of crude distance fog
// near the horizon.
_$.rowShade = Math.round(FADE_GRAY_VALUE * (_$.rowRelativeZ / (CAMERA.dist) - FADE_OFFSET))
_$.rowStyleString = 'rgb(' + _$.rowShade + ',' + _$.rowShade + ',' + _$.rowShade + ')'
// Calclate and render each building
_$.lightData.length = 0
ctx.fillStyle = _$.rowStyleString
for (let j = 0; j < _$.rowBuildingCount; j++) {
// Buildings have a certain chance to become a 'stack' i.e. way taller than
// everything else. We calculate a random ranged height for the building,
// and if it exceeds a threshold, it gets turned into a stack.
_$.isStack = false
_$.buildingHeight = Math.max(BUILDING_MIN_HEIGHT, RNG.random() * BUILDING_MAX_HEIGHT)
if (_$.buildingHeight > (BUILDING_MAX_HEIGHT * STACK_THRESHOLD)) {
_$.isStack = true
// Stacks have 40% height variance
_$.buildingHeight = (STACK_HEIGHT * 0.6 + (RNG.random() * 0.4))
}
// Calculate the pixel size and position of this building, adjusted for perspective
_$.buildingScreenHeight = _$.buildingHeight * _$.scalingFactor
_$.buildingScreenX = _$.rowScreenX + (j * _$.rowBuildingScreenWidth)
_$.buildingScreenY = _$.rowScreenY + _$.rowScreenHeight - _$.buildingScreenHeight
// Draw the building on screen
ctx.fillRect(_$.buildingScreenX, _$.buildingScreenY, Math.ceil(_$.rowBuildingScreenWidth), _$.buildingScreenHeight)
// Seed the RNG for consistency when calculating stack lights (if needed)
RNG.seed = _$.buildingHeight + j
// Stacks have a chance to get lights on their top corners.
// Generate and store light data so we can render it on top of the buildings
if (_$.isStack && RNG.random() < STACK_LIGHT_CHANCE) {
// Get random light size and color.
// Slightly higher chance of red vs white lights
_$.lightSize = RNG.random() * (STACK_LIGHT_SIZE * _$.scalingFactor)
_$.lightColor = (RNG.random() > 0.6) ? 'white' : 'red'
// Save light info for rendering after we do all the buildings
// (helps minimixe changes to ctx.fillStyle)
_$.lightData.push(_$.buildingScreenX)
_$.lightData.push(_$.buildingScreenY)
_$.lightData.push(_$.lightSize)
_$.lightData.push(_$.lightColor)
}
}
// Draw any lights on stacks that need them in this row
for (let j = 0; j < _$.lightData.length; j += 4) {
_$.buildingScreenX = _$.lightData[j]
_$.buildingScreenY = _$.lightData[j+1]
_$.lightSize = _$.lightData[j+2]
_$.lightHalfSize = _$.lightSize * 0.5
_$.lightColor = _$.lightData[j+3]
// Draw lights centred at the top left and right corners of the stack
ctx.fillStyle = _$.lightColor
ctx.fillRect(_$.buildingScreenX - _$.lightHalfSize,
_$.buildingScreenY - _$.lightHalfSize, _$.lightSize, _$.lightSize)
ctx.fillRect(_$.buildingScreenX + _$.rowBuildingScreenWidth - _$.lightHalfSize,
_$.buildingScreenY - _$.lightHalfSize, _$.lightSize, _$.lightSize)
}
}
// 4. Blit scene to onscreen canvas.
// Now that we've built up the scene in-memory, we just render the image to
// our canvas in the DOM.
output_ctx.drawImage(c, 0, 0)
}
// Main loop.
// Maintains a consistent update rate, but draws the screen as often
// as the browser will allow.
function frame() {
requestAnimationFrame(frame)
_ft = Date.now()
update()
if (_ft - _dt > FRAME_TIME) {
render()
_dt = _ft
}
}
// Let's go!
function start() {
// Init frame timers (see frame())
_dt = _ft = Date.now()
// Create two canvases - one for in-memory compositing,
// and another to go in the DOM for our final render.
// Make them the same size as each other.
c = document.createElement('canvas')
ctx = c.getContext('2d')
output_c = document.createElement('canvas')
output_ctx = output_c.getContext('2d')
output_c.width = c.width = CANVAS_WIDTH
output_c.height = c.height = CANVAS_HEIGHT
document.body.appendChild(output_c)
// Start the main loop.
frame()
}
start()
Also see: Tab Triggers