Creating a 3D Tetris Game for Dummies Like Me — IV

Oleksii Koshkin
17 min readJun 25, 2023

--

TL;DR: live demo or the advanced game + some shameless advertising wrapper.

Level: for beginners.
See detailed links and table of contents at the bottom of the article.

At the moment I have a 3D Next Figure component and a base Scene (not implemented yet, but it will be the last one anyway). Today I will create a KeyboardHelp component.

This is especially interesting because I’ll be rendering SVG shapes.

Let’s start. This time I’ll start by thinking, just for a change. How many keys I will support? Well, –

  1. ESC to interrupt current game,
  2. Left Arrow,
  3. Right Arrow to move the figure,
  4. Up Arrow to rotate,
  5. Down Arrow to fall while still pressed,
  6. Space to drop,
  7. P” for pause,
  8. S” to toggle sound. Yes, I will have sound here, in the Tetris. It is AAA title, it has to have a sound.

Wow, so many… let’s think about the layout…

2 rows, 8 columns + 1 extra space around arrow block and probably before space key. Good. Let’s say one single cap will take the size of 3x3x0.5, which means size 27x6 in terms of cells/ThreeJS coordinates, plus spaces in between. 30x6.

I would like to have a component height of 100px. Remember the layout?

Therefore, width will be 30 x (100 / (2x3)) = 500 pixels. Time to create a component.

I just copied the Next.Svelte component structure, removed the specific code, and added the base (temporary) cubes to see the overall composition:

<script lang="ts">
// components/Keys.svelte
import { onMount, onDestroy } from "svelte";
import * as THREE from "three";
import type { TThreeFrame } from "./types";

let Frame: TThreeFrame;
let canvas;

onMount(() => {
initScene();
});

onDestroy(() => {
if (!Frame) {
return;
}

Frame.renderer.dispose();
Frame.renderer.forceContextLoss();
Frame.renderer.domElement = null;
Frame.renderer = null;
});

function initScene() {
Frame = {
scene: new THREE.Scene(),
renderer: new THREE.WebGLRenderer({ alpha: false, antialias: true }),
camera: new THREE.PerspectiveCamera(
75,
canvas.offsetWidth / canvas.offsetHeight,
0.1,
500
),
};

Frame.camera.zoom = 25; // <-- some tuning in camera settings
Frame.camera.position.set(0, 0, 100); // <--
Frame.camera.lookAt(0, 0, 0);
Frame.camera.updateProjectionMatrix();

const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 4, 30);
light.castShadow = true;

const pointLight1 = new THREE.PointLight(0x88aaff, 0.8, 20);
pointLight1.position.set(8, 4, -4);
pointLight1.castShadow = true;

const pointLight2 = new THREE.PointLight(0xffaa88, 0.8, 60);
pointLight2.position.set(4, -3, -6);

Frame.scene.add(light);
Frame.scene.add(pointLight1);
Frame.scene.add(pointLight2);

// Temporary code below
const cubeGeometry = new THREE.BoxGeometry(2.8, 2.8, 0.5);
const cubeGeometrySpace = new THREE.BoxGeometry(5.8, 2.8, 0.5);
const cubeMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa });

const cubes = new THREE.Group();
const cubeEsc = new THREE.Mesh(cubeGeometry, cubeMaterial);
cubeEsc.position.x = -13.5;
cubeEsc.position.y = -1.5;
cubes.add(cubeEsc);

const cubeUp = new THREE.Mesh(cubeGeometry, cubeMaterial);
cubeUp.position.y = 1.5;
cubeUp.position.x = -5.5;
cubes.add(cubeUp);

const cubeDown = new THREE.Mesh(cubeGeometry, cubeMaterial);
cubeDown.position.y = -1.5;
cubeDown.position.x = -5.5;
cubes.add(cubeDown);

const cubeLeft = new THREE.Mesh(cubeGeometry, cubeMaterial);
cubeLeft.position.y = -1.5;
cubeLeft.position.x = -8.5;
cubes.add(cubeLeft);

const cubeRight = new THREE.Mesh(cubeGeometry, cubeMaterial);
cubeRight.position.y = -1.5;
cubeRight.position.x = -2.5;
cubes.add(cubeRight);

const cubeP = new THREE.Mesh(cubeGeometry, cubeMaterial);
cubeP.position.y = -1.5;
cubeP.position.x = 2.5;
cubes.add(cubeP);

const cubeS = new THREE.Mesh(cubeGeometry, cubeMaterial);
cubeS.position.y = -1.5;
cubeS.position.x = 5.5;
cubes.add(cubeS);

const cubeSpace = new THREE.Mesh(cubeGeometrySpace, cubeMaterial);
cubeSpace.position.x = 12;
cubeSpace.position.y = -1.5;
cubes.add(cubeSpace);

cubes.rotateX(-.5);
Frame.scene.add(cubes);
// the end of temporary code

Frame.renderer.shadowMap.enabled = true;
Frame.renderer.shadowMap.type = THREE.PCFSoftShadowMap;

Frame.renderer.setSize(canvas.offsetWidth, canvas.offsetHeight);

canvas.appendChild(Frame.renderer.domElement);

Frame.renderer.render(Frame.scene, Frame.camera);
}
</script>

<div id="keys" bind:this={canvas} />

<style>
#keys {
width: 500px;
height: 100px;
}
</style>

Add the component to App.svelte :

<script lang="ts">
import Keys from "./components/Keys.svelte"; // <- import
import Next from "./components/Next.svelte";

const figures = "SZLJITO".split("");
let figure = "";

setInterval(() => {
figure = figures[Math.floor(Math.random() * figures.length)];
}, 500);
</script>

<main>
<Next {figure} />
<Keys /> <!-- usage -->
</main>

<style>
/* nothing yet here */
</style>

And in the browser I see:

Wonderful. The composition is fine, so now I’m going to move on to SVG curves to make a real keys.

A bit of theory. To extrude a 3D object from an SVG path, I will use the ideas from this article.

First, I need to create SVG loader. I’m adding components/keys-utils.ts file with the service code:

// components/keys-utils.ts

import { SVGLoader } from "three/examples/jsm/loaders/SVGLoader";
import * as THREE from "three";
const loader = new SVGLoader();

function extrudeSVG(id, color, SVGScale, sizeL) {
// https://muffinman.io/blog/three-js-extrude-svg-path/

if (!document.getElementById('svg-' + id)) {
throw new Error(`SVG [${id}] not found!`);
}

const svgMarkup = document.getElementById('svg-' + id).outerHTML;

const svgData = loader.parse(svgMarkup);

const svgGroup = new THREE.Group();
const material = new THREE.MeshStandardMaterial({
color
});

svgData.paths.forEach(path => {
const shapes = path.toShapes(true);

shapes.forEach(shape => {
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: 100,
bevelEnabled: true
});

const mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;

mesh.scale.set(SVGScale, SVGScale, SVGScale);

mesh.position.x = -(sizeL / 2);
mesh.position.y = -(sizeL / 2);

svgGroup.add(mesh);
});
});

return svgGroup;
}


type TCreateKeyParams = {
symbol?: string
SVGScale: number
}

const keyColor = 0x223344;
const textColor = 0x777788;

export function createKey({
symbol = 'arrow',
SVGScale = 0,
}: TCreateKeyParams) {

const grpKey = new THREE.Group();

const keycapShape = extrudeSVG('key', keyColor, SVGScale, 3);
const symbolShape = extrudeSVG(symbol, textColor, SVGScale, 3);

keycapShape.position.z = -.5;
if (symbol === 'arrow' || symbol === 'rotate') {
symbolShape.scale.set(0.4, -0.4, 0.4);
} else {
symbolShape.scale.set(.8, -.8, .4);
}
symbolShape.position.z = -.1;

grpKey.add(symbolShape);
grpKey.add(keycapShape);
grpKey.scale.set(0.96, 0.96, 1);

return grpKey;
}

Rather simple logic. I need to call createKey() function passing the name of symbol, SVG- and logical scales (size in cells, size in pixels).

Some simplifications here:

  1. SVG must be embedded into page — be reachable in DOM by document.getElementById(symbol). For now, I don’t want to asynchronously load symbols, do the caching, etc.
  2. SVG must be normalised to 512x512 viewport. No problem to detect real scale, however, but it’s up to you. I just need to display a limited predefined set of shapes.
  3. Each SVG-based 3D object will have one material and therefore a color. Again, it’s easy to get the color from the path parameters.

Each key contains two shapes: a keycap and an embossed character, as in real expensive keyboards. This why createKey() includes two objects:

const keycapShape = extrudeSVG('key', keyColor, SVGScale, sizeL);
const symbolShape = extrudeSVG(symbol, textColor, SVGScale, sizeL);

First one is predefined form of key, second one — corresponding symbol.

Now I need to add symbols SVGs to DOM. I will do that in index.html :

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tetris 3D</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>


<svg id="svg-key" style="display: none" width="512" height="512" viewBox="0 0 512 512">
<path id="Rounded-Rectangle" fill="#00a2ff" fill-rule="evenodd" stroke="none"
d="M 0 472 C -0 494.0914 17.908609 512 40 512 L 472 512 C 494.0914 512 512 494.0914 512 472 L 512 40 C 512 17.9086 494.0914 0 472 0 L 40 0 C 17.908609 0 0 17.9086 0 40 Z"/>
</svg>
<svg id="svg-arrow" style="display: none" width="512" height="512" viewBox="0 0 512 512">
<path id="path1" fill="#a60000" fill-rule="evenodd" stroke="none"
d="M 401.134705 458.87146 L 356.756165 503.25 L 109.578125 256.07196 L 356.756165 8.89389 L 401.134705 53.27243 L 198.335175 256.07196 Z"/>
</svg>
<svg id="svg-rotate" style="display: none" width="512" height="512" viewBox="0 0 512 512">
<path d="M125.7 160H176c17.7 0 32 14.3 32 32s-14.3 32-32 32H48c-17.7 0-32-14.3-32-32V64c0-17.7 14.3-32 32-32s32 14.3 32 32v51.2L97.6 97.6c87.5-87.5 229.3-87.5 316.8 0s87.5 229.3 0 316.8s-229.3 87.5-316.8 0c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0c62.5 62.5 163.8 62.5 226.3 0s62.5-163.8 0-226.3s-163.8-62.5-226.3 0L125.7 160z"/>
</svg>
<svg id="svg-escape" style="display: none" width="512" height="512" viewBox="0 0 512 512">
<path d="M 114.785156 439 L 114.785156 340.398438 L 187.097656 340.398438 L 187.097656 357.857422 L 134.921875 357.857422 L 134.921875 378.794922 L 182.816406 378.794922 L 182.816406 395.919922 L 134.921875 395.919922 L 134.921875 421.273438 L 189.507813 421.273438 L 189.507813 439 L 114.785156 439 Z M 197.333984 439 L 229.041016 388.761719 L 198.871094 340.398438 L 223.488281 340.398438 L 241.082031 372.439453 L 259.144531 340.398438 L 282.958984 340.398438 L 252.789063 387.958984 L 284.832031 439 L 259.8125 439 L 241.082031 405.417969 L 221.28125 439 L 197.333984 439 Z M 295.333984 439 L 295.333984 340.398438 L 315.804688 340.398438 L 315.804688 439 L 295.333984 439 Z M 356.408203 439 L 356.408203 357.857422 L 326.773438 357.857422 L 326.773438 340.398438 L 406.646484 340.398438 L 406.646484 357.857422 L 377.146484 357.857422 L 377.146484 439 L 356.408203 439 Z M 422.171875 233.865234 C 397.017517 233.865234 377.242249 225.796906 362.845703 209.660156 C 348.449158 193.444305 341.251953 171.215912 341.251953 142.976563 C 341.251953 112.443268 349.438446 88.911163 365.8125 72.378906 C 380.050842 57.982361 398.165955 50.785156 420.15625 50.785156 C 449.582123 50.785156 471.097595 60.433563 484.703125 79.734375 C 492.217804 90.57135 496.250977 101.449158 496.804688 112.365234 L 460.259766 112.365234 C 457.886719 103.980438 454.840851 97.652374 451.123047 93.380859 C 444.478485 85.787079 434.631897 81.990234 421.580078 81.990234 C 408.290955 81.990234 397.809601 87.348083 390.136719 98.066406 C 382.463837 108.784729 378.626953 123.953094 378.626953 143.570313 C 378.626953 163.187531 382.681122 177.881287 390.789063 187.650391 C 398.897003 197.419495 409.199158 202.302734 421.697266 202.302734 C 434.51178 202.302734 444.282196 198.111359 451.005859 189.726563 C 454.723663 185.217743 457.807617 178.455109 460.259766 169.4375 L 496.449219 169.4375 C 493.285126 188.501007 485.216858 204.004822 472.244141 215.949219 C 459.192322 227.893616 442.501007 233.865234 422.171875 233.865234 Z M 251.669922 233.507813 C 228.888596 233.507813 210.97171 228.308167 197.919922 217.90625 C 184.868134 207.504333 178.341797 193.207062 178.341797 175.013672 L 213.226563 175.013672 C 214.333984 183.00296 216.508774 188.974579 219.751953 192.929688 C 225.684601 200.12796 235.849564 203.726563 250.246094 203.726563 C 258.868195 203.726563 265.867157 202.777344 271.246094 200.878906 C 281.450256 197.240204 286.552734 190.4776 286.552734 180.589844 C 286.552734 174.815399 284.021515 170.345734 278.958984 167.181641 C 273.896454 164.096649 265.94635 161.369141 255.109375 158.996094 L 236.599609 154.841797 C 218.406189 150.728485 205.830109 146.25882 198.869141 141.433594 C 187.082977 133.365204 181.189453 120.749054 181.189453 103.583984 C 181.189453 87.921844 186.884735 74.909241 198.275391 64.546875 C 209.666046 54.184509 226.395432 49.003906 248.464844 49.003906 C 266.895538 49.003906 282.616638 53.889099 295.628906 63.658203 C 308.641174 73.427307 315.464844 87.606415 316.097656 106.195313 L 280.976563 106.195313 C 280.34375 95.674744 275.75589 88.199249 267.212891 83.769531 C 261.517548 80.842773 254.438492 79.378906 245.974609 79.378906 C 236.561508 79.378906 229.045929 81.277313 223.429688 85.074219 C 217.813446 88.871124 215.005859 94.171844 215.005859 100.974609 C 215.005859 107.223663 217.773407 111.889618 223.310547 114.974609 C 226.870132 117.03125 234.463837 119.444336 246.091797 122.212891 L 276.230469 129.451172 C 289.440491 132.615265 299.407196 136.846649 306.130859 142.146484 C 316.572327 150.373077 321.792969 162.27829 321.792969 177.861328 C 321.792969 193.839874 315.68219 207.107849 303.460938 217.667969 C 291.239685 228.228088 273.976624 233.507813 251.669922 233.507813 Z M 25.755859 229 L 25.755859 54.105469 L 154.017578 54.105469 L 154.017578 85.074219 L 61.46875 85.074219 L 61.46875 122.212891 L 146.423828 122.212891 L 146.423828 152.587891 L 61.46875 152.587891 L 61.46875 197.556641 L 158.289063 197.556641 L 158.289063 229 L 25.755859 229 Z"/>
</svg>
<svg id="svg-pause" style="display: none" width="512" height="512" viewBox="0 0 512 512">
<path d="M 255.365234 441.542969 C 238.28476 441.542969 226.689804 436.191467 220.580078 425.488281 C 217.324524 419.646149 215.695313 411.4841 215.695313 401.003906 L 215.695313 340.398438 L 236.634766 340.398438 L 236.634766 401.003906 C 236.634766 407.782593 237.437485 412.733704 239.042969 415.855469 C 241.540375 421.385437 246.981094 424.148438 255.365234 424.148438 C 263.704803 424.148438 269.121735 421.385437 271.619141 415.855469 C 273.224609 412.733704 274.027344 407.782593 274.027344 401.003906 L 274.027344 340.398438 L 294.966797 340.398438 L 294.966797 401.003906 C 294.966797 411.4841 293.337585 419.646149 290.082031 425.488281 C 284.016907 436.191467 272.445679 441.542969 255.365234 441.542969 Z M 351.224609 441.542969 C 338.380798 441.542969 328.280304 438.610535 320.921875 432.746094 C 313.563446 426.881653 309.882813 418.819702 309.882813 408.5625 L 329.550781 408.5625 C 330.17514 413.066742 331.402008 416.434235 333.230469 418.664063 C 336.575226 422.722351 342.305298 424.751953 350.421875 424.751953 C 355.282898 424.751953 359.229156 424.216797 362.261719 423.146484 C 368.014679 421.095032 370.890625 417.281586 370.890625 411.707031 C 370.890625 408.451477 369.463562 405.932312 366.609375 404.148438 C 363.755188 402.40918 359.273804 400.869141 353.164063 399.53125 L 342.728516 397.191406 C 332.471313 394.872375 325.381531 392.35321 321.457031 389.632813 C 314.812134 385.083984 311.488281 377.970398 311.488281 368.292969 C 311.488281 359.46286 314.699188 352.127319 321.121094 346.285156 C 327.542999 340.442993 336.975525 337.521484 349.417969 337.521484 C 359.80896 337.521484 368.671661 340.275513 376.007813 345.783203 C 383.343964 351.290894 387.192047 359.283508 387.548828 369.763672 L 367.748047 369.763672 C 367.391266 363.832336 364.804718 359.6185 359.988281 357.121094 C 356.777313 355.471008 352.785492 354.646484 348.013672 354.646484 C 342.706665 354.646484 338.469086 355.716797 335.302734 357.857422 C 332.136383 359.998047 330.554688 362.984985 330.554688 366.820313 C 330.554688 370.343445 332.114563 372.975586 335.236328 374.714844 C 337.243164 375.874359 341.524384 377.234039 348.080078 378.794922 L 365.072266 382.875 C 372.519897 384.658875 378.138977 387.045227 381.929688 390.033203 C 387.816437 394.671234 390.759766 401.382446 390.759766 410.167969 C 390.759766 419.176453 387.313995 426.657684 380.423828 432.611328 C 373.533661 438.564972 363.800842 441.542969 351.224609 441.542969 Z M 35.951172 439 L 35.951172 340.398438 L 78.896484 340.398438 C 88.796913 340.398438 96.691383 342.939423 102.578125 348.023438 C 108.464867 353.107452 111.408203 360.978149 111.408203 371.636719 C 111.408203 383.276428 108.464867 391.503906 102.578125 396.320313 C 96.691383 401.136719 88.283577 403.546875 77.357422 403.546875 L 56.419922 403.546875 L 56.419922 439 L 35.951172 439 Z M 109.802734 439 L 144.988281 340.398438 L 168.267578 340.398438 L 203.1875 439 L 180.84375 439 L 174.488281 418.730469 L 138.166016 418.730469 L 131.341797 439 L 109.802734 439 Z M 406.613281 439 L 406.613281 340.398438 L 478.925781 340.398438 L 478.925781 357.857422 L 426.748047 357.857422 L 426.748047 378.794922 L 474.644531 378.794922 L 474.644531 395.919922 L 426.748047 395.919922 L 426.748047 421.273438 L 481.333984 421.273438 L 481.333984 439 L 406.613281 439 Z M 143.716797 401.740234 L 168.736328 401.740234 L 156.427734 362.941406 L 143.716797 401.740234 Z M 56.419922 386.554688 L 75.619141 386.554688 C 80.480164 386.554688 84.269844 385.373383 86.990234 383.009766 C 89.666031 380.646149 91.003906 376.900085 91.003906 371.771484 C 91.003906 366.642883 89.655121 362.986023 86.957031 360.800781 C 84.258942 358.61554 80.480164 357.523438 75.619141 357.523438 L 56.419922 357.523438 L 56.419922 386.554688 Z M 267.746582 166.114258 L 230.608398 166.114258 L 230.608398 229 L 194.300781 229 L 194.300781 54.106445 L 270.475586 54.106445 C 288.036224 54.106445 302.037048 58.615204 312.478516 67.632813 C 322.919983 76.650421 328.140625 90.611725 328.140625 109.51709 C 328.140625 130.162689 322.919983 144.756805 312.478516 153.299805 C 302.037048 161.842804 287.126556 166.114258 267.746582 166.114258 Z M 284.83252 129.687988 C 289.578644 125.495575 291.95166 118.851105 291.95166 109.754395 C 291.95166 100.657684 289.558868 94.171417 284.773193 90.29541 C 279.987518 86.419403 273.283722 84.481445 264.661621 84.481445 L 230.608398 84.481445 L 230.608398 135.976563 L 264.661621 135.976563 C 273.283722 135.976563 280.007294 133.880402 284.83252 129.687988 Z"/>
</svg>
<svg id="svg-drop" style="display: none" width="512" height="512" viewBox="0 0 512 512">
<path d="M 310.017578 441.742188 C 295.925049 441.742188 285.154663 437.906921 277.707031 430.236328 C 267.717407 420.826477 262.722656 407.269257 262.722656 389.564453 C 262.722656 371.502869 267.717407 357.945618 277.707031 348.892578 C 285.154663 341.222015 295.925049 337.386719 310.017578 337.386719 C 324.110107 337.386719 334.880493 341.222015 342.328125 348.892578 C 352.273163 357.945618 357.244141 371.502869 357.244141 389.564453 C 357.244141 407.269257 352.273163 420.826477 342.328125 430.236328 C 334.880493 437.906921 324.110107 441.742188 310.017578 441.742188 Z M 68.527344 439 L 68.527344 340.398438 L 111.005859 340.398438 C 117.11557 340.48761 122.199547 341.201172 126.257813 342.539063 C 133.170288 344.813477 138.767563 348.98175 143.048828 355.046875 C 146.482758 359.952515 148.823563 365.260376 150.072266 370.96875 C 151.320969 376.677124 151.945313 382.117798 151.945313 387.291016 C 151.945313 400.402374 149.315125 411.505829 144.052734 420.603516 C 136.917282 432.867523 125.90107 439 111.005859 439 L 68.527344 439 Z M 167.933594 439 L 167.933594 340.398438 L 216.298828 340.398438 C 223.211304 340.532227 228.528137 341.378571 232.251953 342.939453 C 235.975769 344.500336 239.132141 346.797516 241.71875 349.830078 C 243.85939 352.327484 245.554031 355.092438 246.802734 358.125 C 248.051437 361.157562 248.675781 364.612305 248.675781 368.492188 C 248.675781 373.174835 247.492523 377.78009 245.128906 382.306641 C 242.765289 386.83316 238.864609 390.033203 233.423828 391.90625 C 237.972672 393.734711 241.194489 396.332184 243.089844 399.699219 C 244.985199 403.066254 245.931641 408.206696 245.931641 415.119141 L 245.931641 421.742188 C 245.931641 426.246429 246.110031 429.300781 246.466797 430.90625 C 247.001953 433.448242 248.250641 435.321289 250.212891 436.525391 L 250.212891 439 L 227.537109 439 C 226.91275 436.814758 226.466797 435.052734 226.199219 433.714844 C 225.664063 430.94986 225.372726 428.117523 225.328125 425.21875 L 225.195313 416.054688 C 225.106125 409.766602 223.957535 405.574554 221.75 403.478516 C 219.542465 401.382477 215.406937 400.335938 209.341797 400.335938 L 188.068359 400.335938 L 188.068359 439 L 167.933594 439 Z M 373.433594 439 L 373.433594 340.398438 L 416.378906 340.398438 C 426.279358 340.398438 434.173798 342.939423 440.060547 348.023438 C 445.947296 353.107452 448.890625 360.97818 448.890625 371.636719 C 448.890625 383.276367 445.947296 391.503906 440.060547 396.320313 C 434.173798 401.136719 425.767944 403.546875 414.841797 403.546875 L 393.902344 403.546875 L 393.902344 439 L 373.433594 439 Z M 310.017578 424.283203 C 318.178741 424.283203 324.667938 421.272461 329.484375 415.251953 C 334.256195 409.231445 336.640625 400.668976 336.640625 389.564453 C 336.640625 378.504547 334.243347 369.953003 329.449219 363.910156 C 324.65509 357.86731 318.178741 354.847656 310.017578 354.847656 C 301.856415 354.847656 295.345398 357.856415 290.484375 363.876953 C 285.623352 369.897491 283.193359 378.459961 283.193359 389.564453 C 283.193359 400.668976 285.623352 409.231445 290.484375 415.251953 C 295.345398 421.272461 301.856415 424.283203 310.017578 424.283203 Z M 88.529297 421.875 L 107.527344 421.875 C 117.249374 421.875 124.028 417.080444 127.863281 407.492188 C 129.95932 402.229797 131.007813 395.964539 131.007813 388.695313 C 131.007813 378.661133 129.445984 370.94696 126.324219 365.550781 C 123.15786 360.199188 116.892601 357.523438 107.527344 357.523438 L 88.529297 357.523438 L 88.529297 421.875 Z M 393.902344 386.554688 L 413.101563 386.554688 C 417.962585 386.554688 421.754211 385.373383 424.474609 383.009766 C 427.150391 380.646149 428.488281 376.900085 428.488281 371.771484 C 428.488281 366.642883 427.139496 362.986023 424.441406 360.800781 C 421.743317 358.61554 417.962585 357.523438 413.101563 357.523438 L 393.902344 357.523438 L 393.902344 386.554688 Z M 188.068359 384.011719 L 211.414063 384.011719 C 216.052109 384.011719 219.530594 383.476563 221.849609 382.40625 C 225.952499 380.533203 228.003906 376.832703 228.003906 371.302734 C 228.003906 365.326782 226.019882 361.313171 222.050781 359.261719 C 219.820953 358.102203 216.477234 357.523438 212.017578 357.523438 L 188.068359 357.523438 L 188.068359 384.011719 Z M 21.484375 275.75 L 21.484375 52.445313 L 81.046875 52.445313 L 81.046875 77.125 L 53.519531 77.125 L 53.519531 250.951172 L 81.046875 250.951172 L 81.046875 275.75 L 21.484375 275.75 Z M 430.359375 275.75 L 430.359375 250.951172 L 457.886719 250.951172 L 457.886719 76.650391 L 430.359375 76.650391 L 430.359375 52.445313 L 489.923828 52.445313 L 489.923828 275.75 L 430.359375 275.75 Z M 102.404297 229 L 102.404297 193.640625 L 138.355469 193.640625 L 138.355469 229 L 102.404297 229 Z M 372.457031 229 L 372.457031 193.640625 L 408.408203 193.640625 L 408.408203 229 L 372.457031 229 Z"/>
</svg>
<svg id="svg-sound" style="display: none" width="512" height="512" viewBox="0 0 512 512">
<path d="M 458.352539 441.119995 L 415.874512 441.119995 L 415.874512 342.517456 L 458.352539 342.517456 C 464.46228 342.606659 469.546204 343.32019 473.604492 344.658081 C 480.516968 346.932495 486.113739 351.102234 490.39502 357.167358 C 493.828949 362.072998 496.170227 367.379883 497.418945 373.088257 C 498.667664 378.796631 499.291992 384.237335 499.291992 389.410522 C 499.291992 402.521912 496.660828 413.626312 491.398438 422.723999 C 484.26297 434.988068 473.247803 441.119995 458.352539 441.119995 Z M 473.671387 367.6698 C 470.505035 362.318237 464.239319 359.642456 454.874023 359.642456 L 435.875977 359.642456 L 435.875977 423.994995 L 454.874023 423.994995 C 464.596069 423.994995 471.374664 419.200928 475.209961 409.612671 C 477.306 404.350281 478.354004 398.084564 478.354004 390.815308 C 478.354004 380.781067 476.793152 373.065979 473.671387 367.6698 Z M 396.675781 441.119995 L 376.072266 441.119995 L 335.801758 371.081421 L 335.801758 441.119995 L 316.603027 441.119995 L 316.603027 342.517456 L 338.209961 342.517456 L 377.477051 411.351929 L 377.477051 342.517456 L 396.675781 342.517456 Z M 276.33252 403.123901 L 276.33252 342.517456 L 297.270508 342.517456 L 297.270508 403.123901 C 297.270508 413.604095 295.642761 421.765137 292.387207 427.6073 C 286.322083 438.310486 274.74942 443.661987 257.668945 443.661987 C 240.588455 443.661987 228.993515 438.310486 222.883789 427.6073 C 219.628235 421.765137 218.000488 413.604095 218.000488 403.123901 L 218.000488 342.517456 L 238.938477 342.517456 L 238.938477 403.123901 C 238.938477 409.902588 239.741196 414.852722 241.34668 417.974487 C 243.844086 423.504456 249.28479 426.269409 257.668945 426.269409 C 266.008514 426.269409 271.42691 423.504456 273.924316 417.974487 C 275.529785 414.852722 276.33252 409.902588 276.33252 403.123901 Z M 155.052734 443.862671 C 140.96022 443.862671 130.190308 440.027435 122.742676 432.356812 C 112.753044 422.94693 107.758301 409.389771 107.758301 391.684937 C 107.758301 373.623322 112.753044 360.066162 122.742676 351.013062 C 130.190308 343.342468 140.96022 339.507202 155.052734 339.507202 C 169.145248 339.507202 179.915161 343.342468 187.362793 351.013062 C 197.307831 360.066162 202.280273 373.623322 202.280273 391.684937 C 202.280273 409.389771 197.307831 422.94693 187.362793 432.356812 C 179.915161 440.027435 169.145248 443.862671 155.052734 443.862671 Z M 174.519043 417.372437 C 179.290878 411.351898 181.676758 402.78949 181.676758 391.684937 C 181.676758 380.625 179.279724 372.07373 174.485596 366.030884 C 169.691467 359.988037 163.213913 356.966675 155.052734 356.966675 C 146.891556 356.966675 140.380554 359.976898 135.519531 365.997437 C 130.658508 372.017975 128.228027 380.580383 128.228027 391.684937 C 128.228027 402.78949 130.658508 411.351898 135.519531 417.372437 C 140.380554 423.392975 146.891556 426.403198 155.052734 426.403198 C 163.213913 426.403198 169.702606 423.392975 174.519043 417.372437 Z M 55.848145 426.87146 C 60.709171 426.87146 64.655907 426.336304 67.688477 425.265991 C 73.441437 423.214539 76.317871 419.401611 76.317871 413.827026 C 76.317871 410.571472 74.8908 408.051819 72.036621 406.267944 C 69.182442 404.528687 64.700554 402.990112 58.59082 401.652222 L 48.155273 399.310913 C 37.89806 396.991882 30.807312 394.472229 26.882813 391.751831 C 20.237923 387.203003 16.915527 380.089935 16.915527 370.412476 C 16.915527 361.582336 20.126432 354.246338 26.54834 348.404175 C 32.970245 342.562012 42.402283 339.640991 54.844727 339.640991 C 65.235725 339.640991 74.099167 342.394775 81.435303 347.902466 C 88.771439 353.410156 92.617836 361.403961 92.974609 371.884155 L 73.173828 371.884155 C 72.817055 365.95282 70.230492 361.738495 65.414063 359.241089 C 62.20311 357.591003 58.211777 356.765991 53.439941 356.765991 C 48.13295 356.765991 43.896336 357.836304 40.72998 359.976929 C 37.563625 362.117554 35.980469 365.105499 35.980469 368.940796 C 35.980469 372.463928 37.541325 375.095093 40.663086 376.834351 C 42.669933 377.993866 46.951138 379.354034 53.506836 380.914917 L 70.498047 384.995483 C 77.945679 386.779358 83.564758 389.165222 87.355469 392.153198 C 93.242218 396.79126 96.185547 403.50293 96.185547 412.288452 C 96.185547 421.296967 92.740517 428.777924 85.850342 434.731567 C 78.960167 440.685211 69.227112 443.661987 56.650879 443.661987 C 43.807064 443.661987 33.706093 440.729797 26.347656 434.865356 C 18.989222 429.000916 15.310059 420.940186 15.310059 410.682983 L 34.977051 410.682983 C 35.601402 415.187256 36.827789 418.55423 38.65625 420.784058 C 42.000992 424.842346 47.731567 426.87146 55.848145 426.87146 Z M 257.90625 201.871094 C 266.138062 201.871094 272.821594 200.964844 277.957031 199.152344 C 287.69928 195.678375 292.570313 189.221405 292.570313 179.78125 C 292.570313 174.268188 290.153656 170.001312 285.320313 166.980469 C 280.486969 164.035156 272.897186 161.429688 262.550781 159.164063 L 244.878906 155.199219 C 227.509033 151.272125 215.501343 147.005219 208.855469 142.398438 C 197.602814 134.695282 191.976563 122.649811 191.976563 106.261719 C 191.976563 91.308533 197.414001 78.885468 208.289063 68.992188 C 219.164124 59.098907 235.136612 54.152344 256.207031 54.152344 C 273.803467 54.152344 288.81308 58.815704 301.236328 68.142578 C 313.659576 77.469452 320.173187 91.006409 320.777344 108.753906 L 287.246094 108.753906 C 286.641937 98.709595 282.261749 91.572937 274.105469 87.34375 C 268.667938 84.549469 261.908905 83.152344 253.828125 83.152344 C 244.841095 83.152344 237.666687 84.964813 232.304688 88.589844 C 226.942688 92.214874 224.261719 97.274719 224.261719 103.769531 C 224.261719 109.735718 226.904922 114.191406 232.191406 117.136719 C 235.589859 119.100281 242.839783 121.403625 253.941406 124.046875 L 282.714844 130.957031 C 295.326874 133.977875 304.842407 138.018219 311.261719 143.078125 C 321.23053 150.932343 326.214844 162.298096 326.214844 177.175781 C 326.214844 192.431061 320.38092 205.099548 308.712891 215.181641 C 297.044861 225.263733 280.562592 230.304688 259.265625 230.304688 C 237.515518 230.304688 220.410217 225.339233 207.949219 215.408203 C 195.48822 205.477173 189.257813 191.826904 189.257813 174.457031 L 222.5625 174.457031 C 223.619797 182.084686 225.696594 187.786438 228.792969 191.5625 C 234.457062 198.434937 244.161392 201.871094 257.90625 201.871094 Z"/>
</svg>
</body>
</html>

Pay attention please: style="display:none" because I don’t want to render the SVG, I just need access to its content.

Where did I get the SVG? I drew them in Pixelmator Pro.

Now I need to render the keys. Let’s return to Keys.svelte and make createKeys() function:

// Keys.svelte
import { createKey } from "./keys-utils";
...
onMount(() => {
initScene();
createKeys();
});

...
function createKeys() {
const sizeL = 3; // 3x3 cells per key
const sizePixel = 512; // base size of svg canvas
const SVGScale = sizeL / sizePixel;

const keyArrowLeft = createKey({ SVGScale});

const keyArrowDown = keyArrowLeft.clone();
keyArrowDown.rotateZ(Math.PI / 2);
keyArrowDown.children[0].position.x -= 0.2; // <-- move arrow a bit down

const keyArrowRight = keyArrowLeft.clone();
keyArrowRight.rotateZ(Math.PI);

const keyArrowUp = createKey({ symbol: "rotate", SVGScale});
const keyEscape = createKey({
symbol: "escape",
SVGScale,
});
const keyPause = createKey({ symbol: "pause", SVGScale});
const keySound = createKey({ symbol: "sound", SVGScale});

const keySpace = createKey({ symbol: "drop", SVGScale});
keySpace.children[1].scale.x = 2; // <-- double the width of keycap

const keys = new THREE.Group();

keys.add(keyArrowLeft);
keys.add(keyArrowDown);
keys.add(keyArrowRight);
keys.add(keyArrowUp);
keys.add(keyEscape);
keys.add(keyPause);
keys.add(keySound);
keys.add(keySpace);

keyEscape.position.x = -13.5;
keyEscape.position.y = -1.5;

keyArrowUp.position.y = 1.5;
keyArrowUp.position.x = -5.5;

keyArrowDown.position.y = -1.5;
keyArrowDown.position.x = -5.5;

keyArrowLeft.position.y = -1.5;
keyArrowLeft.position.x = -8.5;

keyArrowRight.position.y = -1.5;
keyArrowRight.position.x = -2.5;

keyPause.position.y = -1.5;
keyPause.position.x = 2.5;

keySound.position.y = -1.5;
keySound.position.x = 5.5;

keySpace.position.x = 12;
keySpace.position.y = -1.5;

keys.rotateX(-0.6);

Frame.scene.add(keys);

Frame.renderer.render(Frame.scene, Frame.camera);
}

Here I’m creating a left arrow key and then cloning it three times because the other arrows are just rotated versions of it.

For “Space” key I directly set double size of keycap by X axis:

const keySpace = createKey({ symbol: "drop", SVGScale});
keySpace.children[1].scale.x = 2; // <-- double the width of keycap

A dirty hack, of course, and the rounded corners will be a little distorted, but I’m tired of drawing these characters and don’t want to have some kind of exclusive keycap just for one key. At least on this scale, no one will notice.

Ah yes, almost forgot to remove temporary blocks from initScene() .

And it’s all! I have an SVG embosser that I can use for very, very complex shapes if I need to. I won’t, at least here, in Tetris, but yeah. Fun, remember?

So, the result of the fourth article: 3D component to display SVG-based shapes.

Here is the GitHub repository with a snapshot of the stage.

To be continued…

--

--

Oleksii Koshkin

AWS certified Solution Architect at GlobalLogic/Hitachi