Colours
Every way to write a colour in a drawing, in 2D and in 3D: colour names, hex codes, rgb, hsl, see-through colours, gradients and patterns. Each one has a short program you can paste into a .js file and run.
In 2D a colour is a piece of text that you give to ctx.fillStyle or ctx.strokeStyle. The same text works in 3D too, along with a few extra forms shown at the end.
Colour names
The easiest way: just say the colour. There are about 140 names, from the plain ("red", "blue", "black") to the fancy ("tomato", "orchid", "papayawhip", "cornflowerblue"). Capital letters don't matter.
tomatogoldmediumseagreendeepskyblueorchidslategraypapayawhipcornflowerbluecrimsondarkorangekhakitealnavyhotpinksiennalavenderconst names = ["tomato", "gold", "mediumseagreen", "deepskyblue", "orchid", "slategray", "papayawhip", "cornflowerblue"]
function draw() {
ctx.fillStyle = "white"
ctx.fillRect(0, 0, width, height)
ctx.font = "16px sans-serif"
for (let i = 0; i < names.length; i++) {
ctx.fillStyle = names[i]
ctx.fillRect(20, 20 + i * 40, 60, 30)
ctx.fillStyle = "black"
ctx.fillText(names[i], 95, 41 + i * 40)
}
}
"transparent" is a colour too: it draws nothing.
Hex codes: #rrggbb
A hex code mixes red, green and blue light. It is a # and then three pairs of digits, one pair each for red, green and blue. Each pair goes from 00 (none) to ff (all), counting in hexadecimal: 0 1 2 3 4 5 6 7 8 9 a b c d e f.
| Code | Colour |
|---|---|
"#ff0000" | All red, no green, no blue: red |
"#00ff00" | Green |
"#0000ff" | Blue |
"#ffff00" | Red and green light together make yellow |
"#ffffff" | All three at full: white |
"#000000" | None at all: black |
"#808080" | All three at half: grey |
"#ff6347" | Tomato |
There are two more forms:
- Short hex,
"#f63": one digit per colour, each doubled, so"#f63"is"#ff6633". - Hex with see-through,
"#ff634780": a fourth pair says how solid the colour is, from00(invisible) toff(solid).80is about half. The short form has one too:"#f638".
function draw() {
ctx.fillStyle = "white"
ctx.fillRect(0, 0, width, height)
ctx.fillStyle = "#ff6347" // tomato
ctx.fillRect(20, 20, 120, 120)
ctx.fillStyle = "#f63" // short form of #ff6633
ctx.fillRect(160, 20, 120, 120)
ctx.fillStyle = "#3366ff80" // blue at about half strength, over the others
ctx.fillRect(80, 80, 160, 120)
}
Many design tools show colours as hex codes, so this is how you copy a colour you have seen somewhere else.
rgb() and rgba()
The same red, green and blue mix written with ordinary numbers from 0 to 255:
"rgb(255, 99, 71)"is tomato (the same as"#ff6347")."rgba(255, 99, 71, 0.5)"adds a fourth number, the alpha: how solid it is, from 0 (invisible) to 1 (solid).
Because these are numbers, they are easy to work out in code. Text between backticks can hold a value: ${red} is replaced by the value of red.
function draw() {
ctx.fillStyle = "black"
ctx.fillRect(0, 0, width, height)
// from black to full red, in 16 steps
for (let i = 0; i < 16; i++) {
const red = i * 17 // 0, 17, 34 ... 255
ctx.fillStyle = `rgb(${red}, 0, 0)`
ctx.fillRect(10 + i * 30, 20, 28, 80)
}
// see-through white, getting more solid
for (let i = 0; i < 10; i++) {
ctx.fillStyle = `rgba(255, 255, 255, ${i / 10})`
ctx.fillRect(10 + i * 48, 130, 44, 80)
}
}
hsl(): colours by hue, saturation and lightness
hsl describes a colour the way people talk about it: which colour it is, how vivid it is, and how light it is. That makes it the most useful colour form in code.
"hsl(hue, saturation%, lightness%)"
Hue: an angle round the colour wheel
Picture the colours of the rainbow bent into a circle. The hue is how far round the circle you go, in degrees:
| Hue | Colour |
|---|---|
| 0 | Red |
| 30 | Orange |
| 60 | Yellow |
| 120 | Green |
| 180 | Cyan |
| 240 | Blue |
| 300 | Magenta |
| 360 | Back to red |
Numbers past 360 carry on round the wheel, so 400 is the same as 40.
These are the hues at every 30 degrees, at full strength:
Saturation: how vivid
100% is the strongest colour; 50% is softer; 0% is no colour at all, just grey. Here is hue 200 from 0% to 100%:
Lightness: how light
0% is always black and 100% always white, whatever the hue. 50% is the full colour. Below 50% is darker, above it paler. Hue 200 from 0% to 100%:
Alpha with hsla()
"hsla(200, 80%, 50%, 0.5)" adds alpha, as rgba does: 0 is invisible, 1 is solid. Over a checkerboard, from 0 to 1:
Try it
Move the sliders, then copy the line into your program.
ctx.fill_style = "hsl(200, 80%, 50%)"function draw() {
ctx.fillStyle = "white"
ctx.fillRect(0, 0, width, height)
ctx.font = "13px sans-serif"
// hue: round the colour wheel
for (let i = 0; i < 12; i++) {
ctx.fillStyle = `hsl(${i * 30}, 90%, 50%)`
ctx.fillRect(10 + i * 36, 30, 32, 50)
}
ctx.fillStyle = "black"
ctx.fillText("hue 0 to 330", 10, 20)
// saturation: grey to vivid
for (let i = 0; i < 11; i++) {
ctx.fillStyle = `hsl(200, ${i * 10}%, 50%)`
ctx.fillRect(10 + i * 36, 115, 32, 50)
}
ctx.fillStyle = "black"
ctx.fillText("saturation 0% to 100%", 10, 105)
// lightness: black to white
for (let i = 0; i < 11; i++) {
ctx.fillStyle = `hsl(200, 90%, ${i * 10}%)`
ctx.fillRect(10 + i * 36, 200, 32, 50)
}
ctx.fillStyle = "black"
ctx.fillText("lightness 0% to 100%", 10, 190)
}
Why hsl is handy in code
A rainbow is one loop. Step the hue and keep the rest the same:
function draw() {
ctx.fillStyle = "black"
ctx.fillRect(0, 0, width, height)
for (let i = 0; i < 36; i++) {
ctx.fillStyle = `hsl(${i * 10}, 85%, 55%)`
ctx.beginPath()
ctx.arc(width / 2, height / 2, 150 - i * 4, 0, Math.PI * 2)
ctx.fill()
}
}
Animating colour is changing one number. Make the hue grow over time and the colour cycles round the wheel; change the lightness and it fades.
let t = 0
function update(dt) {
t += dt
}
function draw() {
const hue = t * 60 // once round the wheel every 6 seconds
ctx.fillStyle = `hsl(${hue}, 80%, 60%)`
ctx.fillRect(0, 0, width / 2, height)
const light = 50 + 40 * Math.abs((t % 2) - 1) // glides between 50% and 90%
ctx.fillStyle = `hsl(30, 90%, ${light}%)`
ctx.fillRect(width / 2, 0, width / 2, height)
}
A colour that matches its value. In a chart or a game, `hsl(${120 * health}, 80%, 50%)` goes from green (healthy, 1) through yellow to red (0).
Making things see-through
There are two ways, and they mix:
- Alpha in the colour:
"rgba(…, 0.5)","hsla(…, 0.5)"or"#rrggbb80". Only that one colour is see-through. ctx.globalAlpha = 0.5: everything drawn after it is see-through, whatever its colour, including pictures and text. Set it back to1when you are done, or wrap it inctx.save()andctx.restore().
When both are used, they multiply: half-see-through colour with globalAlpha = 0.5 shows at a quarter.
function draw() {
ctx.fillStyle = "white"
ctx.fillRect(0, 0, width, height)
// alpha in the colour
const hues = [0, 120, 240]
for (let i = 0; i < hues.length; i++) {
ctx.fillStyle = `hsla(${hues[i]}, 90%, 50%, 0.5)`
ctx.beginPath()
ctx.arc(110 + i * 50, 110, 70, 0, Math.PI * 2)
ctx.fill()
}
// globalAlpha: solid colours, drawn see-through
ctx.save()
ctx.globalAlpha = 0.4
ctx.fillStyle = "navy"
ctx.fillRect(320, 40, 140, 140)
ctx.font = "bold 40px sans-serif"
ctx.fillText("faded", 320, 240)
ctx.restore()
}
Gradients and patterns
A fill or stroke style can be more than one colour.
Gradients blend between colours. Make one, add colour stops (positions from 0 to 1), then use it as a style:
| Call | Blends |
|---|---|
ctx.createLinearGradient(x0, y0, x1, y1) | Along a line |
ctx.createRadialGradient(x0, y0, r0, x1, y1, r1) | Outwards from one circle to another |
ctx.createConicGradient(angle, x, y) | Round a centre, like a colour wheel |
gradient.addColorStop(position, colour) | Puts a colour at a point along it; any colour form works |
Patterns repeat a picture, or another canvas you drew on: ctx.createPattern(picture, "repeat"). The second word can be "repeat", "repeat-x", "repeat-y" or "no-repeat".
// a linear gradient: sunset
const sky = ctx.createLinearGradient(0, 0, 0, 300)
sky.addColorStop(0, "hsl(230, 60%, 25%)")
sky.addColorStop(0.6, "hsl(330, 70%, 60%)")
sky.addColorStop(1, "hsl(40, 100%, 70%)")
// a conic gradient: a colour wheel
const wheel = ctx.createConicGradient(0, 380, 150)
for (let i = 0; i < 7; i++) {
wheel.addColorStop(i / 6, `hsl(${i * 60}, 100%, 50%)`)
}
// a pattern from a small canvas we draw a check on
const tile = document.createElement("canvas")
tile.width = 20
tile.height = 20
const t = tile.getContext("2d")
t.fillStyle = "white"
t.fillRect(0, 0, 20, 20)
t.fillStyle = "lightgray"
t.fillRect(0, 0, 10, 10)
t.fillRect(10, 10, 10, 10)
const checks = ctx.createPattern(tile, "repeat")
function draw() {
ctx.fillStyle = checks
ctx.fillRect(0, 0, width, height)
ctx.fillStyle = sky
ctx.fillRect(20, 20, 240, 260)
ctx.fillStyle = wheel
ctx.beginPath()
ctx.arc(380, 150, 90, 0, Math.PI * 2)
ctx.fill()
}
Colours in 3D
Materials, lights and the background take colours too. Every text form above works, and 3D adds a few of its own.
| Form | Example | Notes |
|---|---|---|
| Text | color: "tomato", color: "#ff6347", color: "hsl(200, 80%, 50%)" | Any colour text from this page, without alpha |
| Hex number | color: 0xff6347 | The same six digits as a hex code, written as a number with 0x in front |
| A colour object | new THREE.Color("tomato"), new THREE.Color(0xff6347) | For scene.background, or to keep and change |
| Red, green, blue from 0 to 1 | new THREE.Color(1, 0.4, 0.3) | Note: 0 to 1, not 0 to 255 |
To change a colour that is already there, use the colour's own methods:
| Call | What it does |
|---|---|
mesh.material.color.set("gold") | Any colour form |
mesh.material.color.setHSL(h, s, l) | Hue, saturation and lightness all from 0 to 1 here, not degrees and percent. Hue 0.33 is green, 0.66 is blue. |
mesh.material.color.setRGB(r, g, b) | Each from 0 to 1 |
Materials have a few colour settings of their own:
color: the surface colour that light falls on.emissive: a colour the surface gives off by itself, as if it glows. It shows even in the dark.emissiveIntensitymakes it stronger.opacitywithtransparent: true: see-through, from 0 to 1. Opacity does nothing unlesstransparentis on.scene.background = new THREE.Color("…"): the colour behind everything.
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
renderer.setPixelRatio(pixelRatio)
renderer.setSize(width, height, false)
const scene = new THREE.Scene()
scene.background = new THREE.Color("hsl(220, 30%, 12%)")
const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 100)
camera.position.set(0, 0, 9)
scene.add(new THREE.AmbientLight("white", 0.4))
const sun = new THREE.DirectionalLight("white", 2)
sun.position.set(2, 4, 5)
scene.add(sun)
const balls = []
for (let i = 0; i < 5; i++) {
const material = new THREE.MeshStandardMaterial({ color: 0xff6347, roughness: 0.4 })
const ball = new THREE.Mesh(new THREE.SphereGeometry(0.6, 32, 16), material)
ball.position.x = (i - 2) * 1.5
ball.position.y = 1
scene.add(ball)
balls.push(ball)
}
const glow = new THREE.Mesh(
new THREE.TorusGeometry(0.6, 0.2, 16, 48),
new THREE.MeshStandardMaterial({ color: "black", emissive: "orange", emissiveIntensity: 1.5 }),
)
glow.position.set(-2, -1.4, 0)
scene.add(glow)
const glass = new THREE.Mesh(
new THREE.BoxGeometry(1.4, 1.4, 1.4),
new THREE.MeshStandardMaterial({ color: "deepskyblue", transparent: true, opacity: 0.4 }),
)
glass.position.set(2, -1.4, 0)
scene.add(glass)
let t = 0
function update(dt) {
t += dt
for (let i = 0; i < balls.length; i++) {
// setHSL takes 0 to 1: each ball a different hue, all drifting round the wheel
balls[i].material.color.setHSL((t * 0.1 + i / 5) % 1, 0.8, 0.5)
}
glass.rotation.y += dt
}
function draw() {
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height, false)
renderer.render(scene, camera)
}
Related
- 2D functions: every drawing call and setting
- 3D functions: materials, lights and everything else in 3D
- Drawing in 2D