Skip to main content

Tech lab

Where we test what browsers can really do. Everything here runs live on your device, written by hand: raw WebGL, our own physics engine and two tiny games. No plugins, no game engine, zero libraries.

  • WebGL + GLSL
  • GPU instancing
  • Custom physics
  • Canvas 2D
  • Pointer + touch + keyboard
  • 0 dependencies
Choose a demo
01 · WebGL

Thousands of bricks, one draw call

Hand-written GLSL shaders and GPU instancing animate every brick on the graphics card. Drag to orbit, move your pointer to push the wave.

CONTROL DECK LIVE
Scene
RebuildRain every brick back down from the sky.
—FPS
—Bricks
—Triangles
1Draw call
How it works The math behind the brick field

One brick, drawn thousands of times

Every brick shares one mesh (a box plus four 12-sided studs = 462 vertices), uploaded to the GPU once. Each brick only adds 8 numbers: position, layer, turn and color. GPU instancing reads one set per brick, so the whole field is a single draw call.

brick_count    = grid_size × grid_size   (wave)   e.g. 24 × 24 = 576
per_brick_data = (brick_x, brick_z, layer, brick_yaw) + (red, green, blue, rand)
vertex_attrib_divisor(attr, 1)   → advance once per brick
draw_arrays_instanced(triangles, 0, 462, brick_count)   → draw_calls = 1
Sources: [1] [2]

Placing each brick on the GPU

The vertex shader turns each brick around the vertical axis, lifts it to its layer, adds the animation height, then projects it through the camera. Projection uses the standard perspective matrix.

x_rot = x·cos(brick_yaw) − z·sin(brick_yaw)
z_rot = x·sin(brick_yaw) + z·cos(brick_yaw)
world_pos = (x_rot, y, z_rot) + (brick_x, 1.15·layer + anim_height, brick_z)
clip_pos  = proj_matrix · view_matrix · world_pos

focal = 1 / tan(field_of_view / 2)
field_of_view = 0.75 rad   near = 0.5   far = 600
proj_matrix = | focal/aspect  0      0                      0                      |
              | 0             focal  0                      0                      |
              | 0             0      (near+far)/(near−far)  2·near·far/(near−far)  |
              | 0             0      −1                     0                      |
Sources: [3]

The wave and the hover bump

Wave height is a sine of each brick’s distance from the center, so rings ripple outward. Your pointer adds a Gaussian bump, measured in screen space so it follows the cursor at any camera angle. With reduced motion on, both are scaled down.

dist_center  = √(x² + z²)
wave_height  = 2.8 · (½ + ½·sin(0.3·dist_center − 1.8·time)) · amplitude
pointer_dist = |brick_screen_pos − pointer_pos|   (aspect_corrected)
bump_height  = 4 · e^(−14·pointer_dist²) · amplitude
anim_height  = wave_height + bump_height
amplitude    = 1   (0.35 with prefers-reduced-motion)
Sources: [4]

The drop and the bounce

In Pyramid and Helix, each brick starts after a small random delay, plus a little more per layer, so the build rains down bottom-up. It falls along a parabola, then bounces with a decaying sine.

drop_time   = 1.4·(time − drop_start) − 1.4·rand − 0.16·layer   rand ∈ [0, 1) per brick
fall        = clamp(1 − drop_time, 0, 1)
bounce_time = max(drop_time − 1, 0)
anim_height = 36·fall² + 0.9·|sin(9·bounce_time)|·e^(−6·bounce_time)
drop_time < 0  → brick not drawn yet
Sources: [4]

Light, shine and haze

Lighting runs per pixel: Lambert diffuse for the matte plastic, a hemisphere term so tops are brighter than sides, a Blinn–Phong highlight for the toy shine, and distance fog toward the sky color.

color = albedo · (0.34 + 0.24·(½ + ½·normal_y) + 0.6·max(normal·light_dir, 0))
      + 0.22 · max(normal·half_vec, 0)^36
light_dir = normalize(0.45, 0.85, 0.32)
half_vec  = normalize(light_dir + (0, 0.5, 0.85))   (fixed view direction)
fog_amount  = clamp((clip_w − fog_start) / (1.4·fog_start), 0, 1)
fog_start   = 0.95 · camera_dist
final_color = mix(color, sky_color, 0.8 · fog_amount)
Sources: [5] [6]

Camera and frame loop

The camera orbits on a sphere around the scene, turning slowly on its own until you drag. Each frame is scheduled with requestAnimationFrame, and an IntersectionObserver pauses the loop when the demo is off screen.

eye_pos = target + camera_dist · (sin(cam_yaw)·cos(cam_pitch), sin(cam_pitch), cos(cam_yaw)·cos(cam_pitch))
cam_yaw += 0.07 rad/s   (auto_orbit)
cam_pitch ∈ [0.15, 1.35] rad
fps = frame_count / elapsed_sec   (updated every 0.5 s)
Sources: [3] [7]
Sources
  1. Khronos Group. “ANGLE_instanced_arrays” WebGL extension specification. registry.khronos.org/webgl/extensions/ANGLE_instanced_arrays/
  2. WebGL Fundamentals. “WebGL Optimization – Instanced Drawing.” webglfundamentals.org/webgl/lessons/webgl-instanced-drawing.html
  3. MDN Web Docs. “WebGL model view projection.” developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_model_view_projection
  4. Our own design for this demo (wave, Gaussian bump, drop and bounce curves); no external source.
  5. Lambert’s cosine law (diffuse reflection). Wikipedia. en.wikipedia.org/wiki/Lambert%27s_cosine_law
  6. Blinn, J. F. (1977). “Models of light reflection for computer synthesized pictures.” SIGGRAPH ’77, pp. 192–198. www.microsoft.com/en-us/research/wp-content/uploads/1977/01/p192-blinn.pdf
  7. MDN Web Docs. “Intersection Observer API.” developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
02 · Physics engine

Bricks with real weight

Our own little engine: gravity, mass, friction, bounce and stacking, solved 1,200 times a second. Tap empty space to drop a brick, grab anything and throw it.

Gravity
0Bodies
0Contacts / step
0 msSolve time
How it works The math behind the physics sandbox

A fixed clock for physics

Physics runs in fixed 1/120-second steps, whatever the screen’s frame rate. Each frame adds real time to an accumulator and spends it in whole steps (at most 6), so the bricks behave the same at 30 or 144 fps.

accumulator += frame_dt
while accumulator ≥ step_dt and steps < 6:
    step(step_dt)            step_dt = 1/120 s
    accumulator −= step_dt
Sources: [1]

Moving every body

Each step adds gravity to the velocity first, then moves the body by the new velocity (semi-implicit Euler, which stays stable for stacking). A grabbed brick follows your pointer like a stiff spring and acts as if it had infinite mass.

vel += gravity · step_dt     gravity = 1900 (Earth) · 320 (Moon) · 0 (Zero-G) px/s²
pos += vel · step_dt
zero_g:  vel *= 0.999 each step
|vel| ≤ 2600 px/s
grabbed: vel = 16 · (pointer_pos − pos),  inv_mass = 0
Sources: [2]

Finding contacts

Bricks are axis-aligned boxes, so two bricks touch when they overlap on both axes; they are pushed apart along the axis with the smaller overlap. The wrecking ball tests against the closest point of each box.

overlap_x = half_w_a + half_w_b − |dx|
overlap_y = half_h_a + half_h_b − |dy|
contact if overlap_x > 0 and overlap_y > 0
normal = axis of the smaller overlap,  penetration = that overlap

closest = clamp(ball_pos, box_min, box_max)
normal  = (ball_pos − closest) / |ball_pos − closest|
penetration = radius − |ball_pos − closest|
Sources: [2] [3]

Push apart, then bounce

Overlapping bodies are nudged apart in proportion to their inverse mass, leaving a small 0.3 px slop so resting stacks don’t jitter. Then an impulse cancels the closing speed. Only hard hits bounce, so stacks settle quietly.

inv_sum = inv_mass_a + inv_mass_b
correction = 0.8 · max(penetration − 0.3, 0) / inv_sum
pos_a −= normal · correction · inv_mass_a
pos_b += normal · correction · inv_mass_b

rel_vel_n = (vel_b − vel_a) · normal          (skip if > 0)
restitution = 0.22 if rel_vel_n < −260 else 0
impulse = −(1 + restitution) · rel_vel_n / inv_sum
vel_a −= impulse · normal · inv_mass_a
vel_b += impulse · normal · inv_mass_b
Sources: [2]

Friction

Sliding is resisted along the contact surface, capped at 55% of the normal impulse (Coulomb friction). That is what lets a wall of bricks hold its shape until the ball hits it.

tangent   = (−normal_y, normal_x)
rel_vel_t = (vel_b − vel_a) · tangent
friction  = clamp(−rel_vel_t / inv_sum, −0.55·impulse, 0.55·impulse)
vel_a −= friction · tangent · inv_mass_a
vel_b += friction · tangent · inv_mass_b
Sources: [2] [4]

Weight and stable stacks

Bigger bricks are heavier and the ball is heaviest. Every step sorts bodies bottom-first and sweeps all contacts 10 times, a Gauss–Seidel style iteration that lets support travel up through a stack.

inv_mass = 1 / studs        (2-stud brick = ½,  4-stud = ¼)
ball inv_mass = 1/14
solver_iterations = 10,  bodies sorted bottom-first
floor: bounce 0.18 (bricks) / 0.35 (ball) when impact > 320 px/s
floor friction: vel_x *= 0.97
Sources: [4]
Sources
  1. Fiedler, G. (2004). “Fix Your Timestep!” Gaffer On Games. gafferongames.com/post/fix_your_timestep/
  2. Envato Tuts+. “How to Create a Custom 2D Physics Engine: The Basics and Impulse Resolution.” code.tutsplus.com/how-to-create-a-custom-2d-physics-engine-the-basics-and-impulse-resolution--gamedev-6331t
  3. Ericson, C. (2004). Real-Time Collision Detection. Morgan Kaufmann (AABB overlap and closest-point tests).
  4. Catto, E. (2005). “Iterative Dynamics with Temporal Coherence.” box2d.org/files/ErinCatto_IterativeDynamics_GDC2005.pdf
030405 · Mini game

Take a quick break

Three tiny games, each a few kilobytes of plain JavaScript. Switch between them with the menu above. They work with mouse, touch and keyboard, and remember your best score.

Stack it

Best 0

Tap or press Space to drop. Anything hanging over the edge breaks off. Land it perfectly to keep your width.

How it works The math behind Stack it

The sliding brick

The new brick ping-pongs across the screen and speeds up with every brick you land, up to a cap.

speed = min(520, 170 + 10 · height) px/s
x += direction · speed · dt
turn around past −0.3·width (left) or canvas_w + 0.3·width (right)
Sources: [1]

Cutting the overhang

When you drop, only the part that overlaps the brick below stays. The rest breaks off; no overlap at all ends the game.

left    = max(x_new, x_top)
right   = min(x_new + w_new, x_top + w_top)
overlap = right − left
overlap ≤ 0  → game over
kept brick = (left, overlap)     scrap = w_new − overlap
Sources: [1]

Perfect drops

Land within 5 px of the brick below and nothing is cut. Three perfect drops in a row start growing the brick back.

perfect = |x_new − x_top| < 5 px
perfect → combo += 1, width unchanged
combo ≥ 3 → width += min(12, 0.6·canvas_w − width)
Sources: [1]

Falling scraps and the camera

Scraps fall with gravity and spin away. The camera eases upward with frame-rate–independent smoothing so the top of the tower stays in view.

scrap_vel_y += 1600 · dt        scrap_spin = ±2.4 rad/s
target = max(0, 30 · height − 0.5 · canvas_h)
camera += (target − camera) · min(1, 6 · dt)
Sources: [2]
Sources
  1. Our own implementation. The tap-to-stack mechanic comes from the one-tap stacking genre, popularized by Ketchapp’s “Stack” (2016).
  2. Exponential smoothing (frame-rate–aware easing). Wikipedia. en.wikipedia.org/wiki/Exponential_smoothing

Brick breaker

Best 0

Move the paddle with your pointer, finger or arrow keys. Tap or press Space to launch. Three lives.

How it works The math behind Brick breaker

Launch and speed

The ball leaves the paddle at a small random angle, faster each level. Each frame is split into 3 sub-steps so a fast ball can’t tunnel through a brick.

launch_angle ∈ [−0.45, 0.45] rad
speed = min(640, 400 + 40 · level) px/s
vel = speed · (sin launch_angle, −cos launch_angle)
sub_steps = 3 per frame
Sources: [1]

Walls and the paddle

Walls mirror the ball. The paddle sets a new angle from where the ball hits it: the center sends it straight up, the edges up to about 60° sideways. That is what gives you aim.

walls:   vel_x = ±|vel_x|,  ceiling: vel_y = |vel_y|
hit_offset   = clamp((ball_x − paddle_x) / (paddle_w / 2), −1, 1)
bounce_angle = 1.05 · hit_offset rad
vel = |vel| · (sin bounce_angle, −cos bounce_angle)
Sources: [1]

Hitting bricks

A brick is hit when the ball is within its radius of the brick’s closest point. The ball bounces off the side with the smaller overlap and gets 1.2% faster each hit. From level 2 the top row takes two hits.

closest = clamp(ball_pos, brick_min, brick_max)
hit if |ball_pos − closest|² ≤ radius²
flip vel_x if overlap_x < overlap_y, else flip vel_y
speed *= 1.012 while speed < 760
top_row_hits = 2 from level 2
Sources: [1] [2]

Bursts and levels

A broken brick throws 6 small pieces that fall and fade. Clearing the board starts the next level with more rows.

pieces = 6,  vel_x ∈ [−180, 180],  vel_y ∈ [−260, 0] px/s
piece_vel_y += 900 · dt,   life = 0.7 s
rows = min(7, 4 + level),  lives = 3
Sources: [3]
Sources
  1. MDN Web Docs. “2D breakout game using pure JavaScript.” developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript
  2. Ericson, C. (2004). Real-Time Collision Detection. Morgan Kaufmann (circle vs. box test).
  3. Our own tuning. The genre comes from Atari’s “Breakout” (1976).

Brick drop

Best level —

No score, just levels. Build your stack up into the red danger zone, then clear rows until a brick lands on the baseplate floor again: that is one comeback, and 5 comebacks pass a level. Each level the danger line rises one row closer to the top, so you have to dare more. A brick locking in the top row ends the game. Full rows snap off and loose bricks drop. Shapes: 2×1, 2×2, U, square hook, double-T and m. Keys: ← → move, ↑ rotate, ↓ soft drop, Space drop, P pause. Touch: tap to rotate, drag to move, flick down to drop.

How it works The math behind Brick drop

Board and the brick bag

The baseplate is a 15 × 22 grid. Bricks come from a bag of 8 that is shuffled (Fisher–Yates) and emptied before refilling, so small bricks show up twice as often as big ones and nothing goes missing for long.

bag = [2×1, 2×1, 2×2, 2×2, u, square_hook, double_t, m]
for i from len(bag) − 1 down to 1:
    j = rand_int(0, i)
    swap(bag[i], bag[j])
Sources: [1]

Turning a brick

A brick turns 90° by remapping its cells inside its bounding square, then is nudged to stay centered. If it hits something, it tries small sideways kicks, then one row up. The 2×2 never turns.

clockwise: (col, row) → (n − 1 − row, col)     n = max(width, height)
normalize to top-left, then
x += floor((width − new_width) / 2)
y += floor((height − new_height) / 2)
kicks = [0, −1, +1, −2, +2], then up 1
Sources: [3]

Falling and locking

A brick drops one row per tick and speeds up each level. It fits if every cell is on the board and empty; when it can’t move down, it locks and its studs “click”.

fall_interval = max(0.08, 0.85 − 0.07 · (level − 1)) s per row
fits = every cell inside the board and empty
can’t fall → lock; click on each cell resting on floor or another brick
Sources: [4]

Snap-off rows and cascades

Full rows snap off together over 0.5 s. Then any brick left hanging falls as a whole piece: cells of the same brick that still touch are grouped with a flood fill, and a group drops one row whenever everything under it is empty. Repeated every 0.06 s until nothing moves, then rows are checked again for chains.

snap_offset = (t / 0.5)²
groups = flood_fill(same brick, 4 neighbours)
group falls if, for every cell, the cell below is empty or its own
repeat every 0.06 s until stable → check rows again (chain)
Sources: [2]

Danger line and levels

No score. Climbing into the danger zone arms a comeback; landing a brick on the floor completes it. Five comebacks pass a level, and each level raises the line one row toward the top.

danger_line = max(1, 6 − (level − 1))          (rows from the top)
armed when stack_top ≤ danger_line, after being below it
comeback when armed and a locked brick touches the floor
comebacks = 5 → level += 1
locks in row 0 → topped out
Sources: [4]
Sources
  1. Fisher–Yates shuffle (Durstenfeld 1964; Knuth, Algorithm P). Wikipedia. en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
  2. Flood fill. Wikipedia. en.wikipedia.org/wiki/Flood_fill
  3. Rotation matrix (90° rotation on a grid). Wikipedia. en.wikipedia.org/wiki/Rotation_matrix
  4. Our own game design: board, brick set, stud lock, snap-off rows, cascades, comebacks and the rising danger line.

Want this in your product?

3D product viewers, configurators, playful onboarding, branded games. We build fast, accessible interactive experiences, and the heavy parts load only when someone opens them, so the rest of your site stays quick.

Start a project