Compare commits

..
7 Commits
Author SHA1 Message Date
Paul Brinkmeier 1df3566997 UTF-63: Snd version 2026-08-10 11:19:52 +02:00
Paul Brinkmeier 32e5f72dab UTF-63: Fix missing p tag 2026-08-05 13:09:27 +02:00
Paul Brinkmeier 1076663c8e UTF-63: First draft 2026-08-05 02:37:24 +02:00
Paul Brinkmeier a3ad633087 Switch main tracks 2025-07-23 23:25:34 +02:00
Paul Brinkmeier c689d1d048 Set up gyroscope before audio stuff 2025-07-22 23:57:35 +02:00
Paul Brinkmeier 729d140243 First audio draft 2025-07-22 23:36:18 +02:00
Paul Brinkmeier 26c35b27b4 Add working translation to sphere coordinates 2025-07-21 23:37:04 +02:00
16 changed files with 682 additions and 20 deletions
+1
View File
@@ -0,0 +1 @@
.DS_Store
+76
View File
@@ -0,0 +1,76 @@
<!doctype html>
<html>
<head>
<title>UTF-63</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="../../fonts/source-sans-pro.css">
<link rel="stylesheet" href="utf63.css">
</head>
<body>
<article>
<h1>UTF-63</h1>
<address>
<time>July 2026</time>
·
By <a rel="author" href="../..">Paul Brinkmeier</a>
</address>
<p>
<a href="https://www.unicode.org/versions/latest/core-spec/chapter-2/#G11165">UTF-8</a> is almost everywhere these days.
The Internet runs on it, most common file formats use it, macOS, Linux and increasingly even Windows systems use it as the default option.
It makes sense: It's quite compact for mostly-ASCII text and therefore most keywords and symbols of text-based file formats and protocols.
</p>
<p>
What if I told you that in this age of memory bandwidth-constrained workloads you are using a wasteful encoding for text storage?
Most text these days is 99% ASCII with a few emoji sprinkled in (source: Made it up).
<!-- Unicode also defines UTF-16 and UTF-32 but they're even worse for this case, see e.g. the <a href="https://utf8everywhere.org/">UTF-8 Everywhere Manifesto</a>. -->
Even if we ignore the costly multibyte emoji encodings, using UTF-8, 1 out of every 8 bits of memory is wasted on the encoding, because 7-bit ASCII characters are encoded using a whole byte, i.e. we have an encoding overhead of roughly 15%.
</p>
<p>
For example, consider the UTF-8 encoding of the string “🙈🙉🙊 encoding” (see <a href="https://utoy.beany.club/text/🙈🙉🙊 encoding" title="also cURLable!">here</a> for code points):
</p>
<pre>11110<span class="red2">000</span>10<span class="red2">011111</span>10<span class="red2">011001</span>10<span class="red2">001000</span> <span class="red2">🙈</span>
11110<span class="orange2">000</span>10<span class="orange2">011111</span>10<span class="orange2">011001</span>10<span class="orange2">001001</span> <span class="orange2">🙉</span>
11110<span class="yellow2">000</span>10<span class="yellow2">011111</span>10<span class="yellow2">011001</span>10<span class="yellow2">001010</span> <span class="yellow2">🙊</span>
0<span class="aqua2">0100000</span>0<span class="blue2">1100101</span>0<span class="purple2">1101110</span>0<span class="red2">1100011</span> <span class="aqua2"> </span><span class="blue2">e</span><span class="purple2">n</span><span class="red2">c</span>
0<span class="orange2">1101111</span>0<span class="yellow2">1100100</span>0<span class="aqua2">1101001</span>0<span class="blue2">1101110</span> <span class="orange2">o</span><span class="yellow2">d</span><span class="aqua2">i</span><span class="blue2">n</span>
0<span class="purple2">1100111</span> <span class="purple2">g</span></pre>
<p>
Colored spans show which code points are encoded by which bits.
Neutral (white on black) bits are encoding overhead required by UTF-8.
For emoji, the most important medium of modern communication, this encoding is obviously horrible.
Every monkey requires a full 32 bits to encode its 21 bits.
And even for good old reliable ASCII, useless bits are sprinkled here and there.
There's gotta be a better way!
</p>
<h2>A Better Way</h2>
<p>
Enter: UTF-63, an entirely new way to encode Unicode!
</p>
<pre>1<span class="red2">000011111011001001000</span><span class="orange2">0000111110</span> <span class="red2">🙈</span><span class="orange2">🙉</span>
<span class="orange2">11001001001</span><span class="yellow2">000011111011001001010</span> <span class="yellow2">🙊</span>
0<span class="aqua2">0100000</span><span class="blue2">1100101</span><span class="purple2">1101110</span><span class="red2">1100011</span><span class="orange2">110</span> <span class="aqua2"> </span><span class="blue2">e</span><span class="purple2">n</span><span class="red2">c</span><span class="orange2">o</span>
<span class="orange2">1111</span><span class="yellow2">1100100</span><span class="aqua2">1101001</span><span class="blue2">1101110</span><span class="purple2">1100111</span> <span class="yellow2">d</span><span class="aqua2">i</span><span class="blue2">n</span><span class="purple2">g</span></pre>
<p>
For our entirely random example, this UTF-63 encoding uses only 16 bytes, a clear win over the whopping 21 bytes required by UTF-8 (that's 30% more!).
So how does it work?
</p>
<p>
Decoding is dead simple:
We consider each set of 8 bytes a Big Endian 64-bit code unit.
If the most significant bit is 0, the other 63 bits contain nine Basic Latin code points (ASCII characters) at 7 bits each.
If it's 1, the other 63 bits contain any three Unicode code points at 21 bits each (the maximum code point is U+10FFFF).
When any of the encoded code points is U+0000 NULL, the string ends there (yes, we're bringing null-terminated strings back. So. many. applications. don't allow NULL anyways).
</p>
<h2>Usage</h2>
<p>
Admittedly, this page actually still uses UTF-8, simply because most modern browsers don't support UTF-63 yet.
To my knowledge, most OSes, desktop applications and programming languages don't either.
It wouldn't surprise me if VLC supported UTF-63 subtitles or something though.
You can do your part by spreading the word among friends, family, flatmates, coworkers and anybody in between.
</p>
<p>
If you enjoyed my Unicode ramblings you can read on over at <a href="https://www.channable.com/tech/so-long-surrogatesa">Channable</a>.
</p>
</article>
</body>
</html>
+83
View File
@@ -0,0 +1,83 @@
:root {
/* https://github.com/morhetz/gruvbox */
--red: #cc241d;
--red2: #fb4934;
--orange: #d65d0e;
--orange2: #fe8019;
--yellow: #d79921;
--yellow2: #fabd2f;
--aqua: #689d6a;
--aqua2: #8ec07c;
--blue: #458588;
--blue2: #83a598;
--purple: #b16286;
--purple2: #d3869b;
--bg: #928374;
--fg0_h: #fbf1c7;
--bg0_h: #1d2021;
}
html {
font-size: 20px;
}
body {
color: var(--bg0_h);
background-color: var(--fg0_h);
font-family: 'Source Sans Pro';
}
address {
font-size: .8em;
font-style: normal;
}
h1, h2, h3, h4, h5, h6 {
font-weight: normal;
}
p {
line-height: 1.5;
}
article {
margin: 0 auto;
width: 80ch;
}
pre {
padding: 1em;
background-color: var(--bg0_h);
color: var(--fg0_h);
line-height: 1.3;
}
.red2 {
color: var(--bg0_h);
background-color: var(--red2);
}
.orange2 {
color: var(--bg0_h);
background-color: var(--orange2);
}
.yellow2 {
color: var(--bg0_h);
background-color: var(--yellow2);
}
.aqua2 {
color: var(--bg0_h);
background-color: var(--aqua2);
}
.blue2 {
color: var(--bg0_h);
background-color: var(--blue2);
}
.purple2 {
color: var(--bg0_h);
background-color: var(--purple2);
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+47
View File
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
table {
width: 100%;
table-layout: fixed;
}
</style>
</head>
<body>
<h1>Is it working?</h1>
<button id="perm">click me for permissions</button>
<table>
<tr>
<th>α</th>
<td id="disp-a">?</td>
<th>x</th>
<td id="disp-x">?</td>
</tr>
<tr>
<th>β</th>
<td id="disp-b">?</td>
<th>y</th>
<td id="disp-y">?</td>
</tr>
<tr>
<th>γ</th>
<td id="disp-c">?</td>
<th>z</th>
<td id="disp-z">?</td>
</tr>
<tr>
<th>φ</th>
<td id="disp-phi">?</td>
<th>θ</th>
<td id="disp-theta">?</td>
</tr>
</table>
<script src="m3.js"></script>
<script src="util.js"></script>
<script src="v3.js"></script>
<script src="debug.js"></script>
</body>
</html>
+38
View File
@@ -0,0 +1,38 @@
"use strict";
(() => {
document.querySelector("#perm").addEventListener("click", e => {
util.getGyroPermission()
.then((response) => {
const disp = {
a: document.querySelector("#disp-a"),
b: document.querySelector("#disp-b"),
c: document.querySelector("#disp-c"),
x: document.querySelector("#disp-x"),
y: document.querySelector("#disp-y"),
z: document.querySelector("#disp-z"),
phi: document.querySelector("#disp-phi"),
theta: document.querySelector("#disp-theta"),
};
window.addEventListener("deviceorientation", e => {
const alpha = util.deg2rad(e.alpha);
const beta = util.deg2rad(e.beta);
const gamma = util.deg2rad(e.gamma);
const [screenNormal, phi, theta] = util.toPolarCoordinates(alpha, beta, gamma);
disp.a.innerHTML = `${Math.round(e.alpha)}`;
disp.b.innerHTML = `${Math.round(e.beta)}`;
disp.c.innerHTML = `${Math.round(e.gamma)}`;
disp.x.innerHTML = `${Math.round(100 * screenNormal[0])}`;
disp.y.innerHTML = `${Math.round(100 * screenNormal[1])}`;
disp.z.innerHTML = `${Math.round(100 * screenNormal[2])}`;
disp.phi.innerHTML = `${Math.round(phi / Math.PI * 180)}`;
disp.theta.innerHTML = `${Math.round(theta / Math.PI * 180)}`;
});
});
}, { once: true });
})();
+69 -20
View File
@@ -1,26 +1,75 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<body> <head>
<h1>Is it working?</h1> <meta charset="UTF-8">
<button id="perm">click me for permissions</button> <meta name="viewport" content="width=device-width, initial-scale=1">
<div style="color: red" id="disp">lalala</div> <style>
<script> * {
document.querySelector("#perm").addEventListener("click", e => { box-sizing: border-box;
const ac = new AudioContext(); }
const osc = ac.createOscillator();
osc.type = "square"; body {
osc.frequency.setValueAtTime(440, ac.currentTime); margin: 0;
osc.connect(ac.destination); }
osc.start();
DeviceMotionEvent.requestPermission().then(response => { table {
window.addEventListener("devicemotion", e => { width: 100%;
document.querySelector("#disp").innerHTML = Math.round(e.acceleration.x); table-layout: fixed;
osc.frequency.setValueAtTime(440 * Math.pow(2, e.acceleration.x / 10), ac.currentTime); }
});
}); .state {
}); display: none;
</script> width: 100vw;
height: 100vh;
}
[data-state=init] #init,
[data-state=error] #error {
display: block;
}
[data-state=main] #main {
display: flex;
}
#error {
border: 2px solid red;
}
#main {
justify-content: center;
align-items: center;
}
@media (max-aspect-ratio: 1) {
#canvas {
width: 100vw;
height: 100vw;
}
}
@media (min-aspect-ratio: 1) {
#canvas {
width: 100vh;
height: 100vh;
}
}
</style>
</head>
<body data-state="init">
<div class="state" id="init">
<p>Halte dein Handy flach in einer Hand.</p>
<button id="perm">start the e x p e r i e n c e</button>
<pre id="log"></pre>
</div>
<div class="state" id="main">
<canvas width="500" height="500" id="canvas"></canvas>
</div>
<div class="state" id="error">
</div>
<script src="m3.js"></script>
<script src="util.js"></script>
<script src="v3.js"></script>
<script src="index.js"></script>
</body> </body>
</html> </html>
+253
View File
@@ -0,0 +1,253 @@
"use strict";
(() => {
const TAU = 2 * Math.PI;
// Build buffers b1 and b2 such that
// the original buffer can be played
// in a loop where it overlaps itself
// by the duration specified in overlap.
// If the overlap is two units, the
// resulting buffers look like this:
//
// buffer: OOBBBBBOO
// b1: OOBBBBBOO-----
// b2: -----OOBBBBBOO
//
// B: Buffer data (plays normally)
// O: Affected by overlap
// -: Inserted silence
//
// By starting b2 overlap seconds after
// b1, overlapping looping playback is
//
//
// OOBBBBBOO-----OOBBBBBOO-----
// -----OOBBBBBOO-----OOBBBBBOO
function buildOverlappingBuffers (audioCtx, buffer, overlap) {
const nonOverlapDuration = buffer.duration - 2 * overlap;
const nonOverlapSamples = Math.floor(nonOverlapDuration * buffer.sampleRate);
const totalLength = buffer.length + nonOverlapSamples;
const b1 = audioCtx.createBuffer(buffer.numberOfChannels, totalLength, buffer.sampleRate);
const b2 = audioCtx.createBuffer(buffer.numberOfChannels, totalLength, buffer.sampleRate);
// TODO: May be faster using the copy methods on AudioBuffer
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
const channelBuffer = buffer.getChannelData(channel);
const b1ChannelBuffer = b1.getChannelData(channel);
const b2ChannelBuffer = b2.getChannelData(channel);
for (let i = 0; i < channelBuffer.length; i++) {
b1ChannelBuffer[i] = channelBuffer[i];
b2ChannelBuffer[nonOverlapSamples + i] = channelBuffer[i];
}
}
return { b1, b2 };
}
function baseGain (peak, rotH) {
const diff = Math.abs(util.diffAngles(peak, rotH));
if (diff < TAU / 8) {
return 1;
} else if (diff < TAU / 8 + TAU / 16) {
return 1 - (diff - TAU / 8) / (TAU / 16);
} else {
return 0;
}
}
function gain (peak, rotH, rotV) {
return baseGain(peak, rotH) * (1 - rotV / (Math.PI / 2));
}
function drawRing (ctx, r, f) {
const n = 48;
for (let i = 0; i < n; i++) {
const a = Math.PI * 2 * i / n;
const a_ = Math.PI * 2 * (i + 1) / n;
const g = f((a + a_) / 2);
if (g === 0) continue;
ctx.lineWidth = 0.1 * g;
ctx.beginPath();
ctx.arc(0, 0, r, a, a_);
ctx.stroke();
}
}
function loadTrack (audioCtx, trackHref) {
return (
fetch(trackHref)
.then(response => response.arrayBuffer())
.then(arrayBuffer => audioCtx.decodeAudioData(arrayBuffer))
.then(track => {
const { b1, b2 } = buildOverlappingBuffers(audioCtx, track, 2);
const gainNode = audioCtx.createGain();
const s1 = audioCtx.createBufferSource();
s1.buffer = b1;
s1.loop = true;
s1.connect(gainNode);
s1.start();
const s2 = audioCtx.createBufferSource();
s2.buffer = b2;
s2.loop = true;
s2.connect(gainNode);
s2.start(audioCtx.currentTime + 0.5);
return gainNode;
})
);
}
function setupAudioStuff (trackHrefs) {
const audioCtx = new AudioContext();
return (
Promise.all(trackHrefs.map((trackHref, i) =>
loadTrack(audioCtx, trackHref)
.then((track) => {
log.innerHTML += `* Track ${i}\n`;
return track;
})
))
.then(gains => {
log.innerHTML += `All tracks received\n`;
return {
ctx: audioCtx,
gains,
};
})
);
}
function waitForEvent (target, eventKey, waitTime) {
return new Promise((resolve, reject) => {
const waitTimeout = setTimeout(() => reject(new Error(`${eventKey} didnt fire after ${waitTime}ms`)), waitTime);
target.addEventListener(eventKey, (e) => {
clearTimeout(waitTimeout);
resolve(e);
});
});
}
function setupGyroscope () {
log.innerHTML += "waiting for gyroscope permission\n";
return (
util.getGyroPermission()
.then(response => {
if (response !== "granted") {
throw new Error("gyroscope permission denied");
}
return waitForEvent(window, "deviceorientation", 10000);
})
);
}
function setupCanvas () {
const ctx = canvas.getContext("2d");
const scale = Math.min(canvas.width, canvas.height) / 2;
ctx.setTransform(
scale, 0,
0, -scale,
canvas.width / 2, canvas.height / 2
);
return ctx;
}
function render (ctx, audio, rotH, rotV) {
for (const gainNode of audio.gains) {
gainNode.gain.value = 0;
}
const rotVScaled = 1 - rotV / (Math.PI / 2);
audio.gains[0].gain.value = rotVScaled < 0.75 ? 1 : 1 - (rotVScaled - 0.75) / 0.25;
audio.gains[1].gain.value = gain(0, rotH, rotV);
audio.gains[2].gain.value = gain(Math.PI / 2, rotH, rotV);
audio.gains[3].gain.value = gain(Math.PI, rotH, rotV);
audio.gains[4].gain.value = gain(Math.PI * (3 / 2), rotH, rotV);
audio.gains[5].gain.value = rotVScaled < 0.75 ? 0 : (rotVScaled - 0.75) / 0.25;
ctx.clearRect(-1, -1, 2, 2);
ctx.strokeStyle = "red";
drawRing(ctx, 0.8, (a) => gain(0, a, rotV));
ctx.strokeStyle = "blue";
drawRing(ctx, 0.7, (a) => gain(Math.PI / 2, a, rotV));
ctx.strokeStyle = "green";
drawRing(ctx, 0.8, (a) => gain(Math.PI, a, rotV));
ctx.strokeStyle = "orange";
drawRing(ctx, 0.7, (a) => gain(Math.PI * (3 / 2), a, rotV));
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(Math.cos(rotH), Math.sin(rotH));
ctx.stroke();
drawBar(ctx, -0.65, 0.8, 0.1, 0.2, "red", audio.gains[1].gain.value);
drawBar(ctx, -0.25, 0.8, 0.1, 0.2, "blue", audio.gains[2].gain.value);
drawBar(ctx, 0.15, 0.8, 0.1, 0.2, "green", audio.gains[3].gain.value);
drawBar(ctx, 0.55, 0.8, 0.1, 0.2, "orange", audio.gains[4].gain.value);
drawBar(ctx, -0.25, -1, 0.1, 0.2, "lime", audio.gains[0].gain.value);
drawBar(ctx, 0.15, -1, 0.1, 0.2, "grey", audio.gains[5].gain.value);
}
function drawBar (ctx, x, y, w, h, color, value) {
const hScaled = h * value;
ctx.fillStyle = color;
ctx.fillRect(x, y, w, hScaled);
ctx.fillStyle = "black";
ctx.fillRect(x, y + hScaled, w, h - hScaled);
}
perm.addEventListener("click", e => {
setupGyroscope()
.then(() => setupAudioStuff([
"Track6Fade.mp3",
"Track2Fade.mp3",
"Track3Fade.mp3",
"Track4Fade.mp3",
"Track5Fade.mp3",
"Track1Fade.mp3",
]))
.then(audio => {
document.body.dataset.state = "main";
for (const gainNode of audio.gains) {
gainNode.connect(audio.ctx.destination);
}
const ctx = setupCanvas();
window.addEventListener("deviceorientation", e => {
const alpha = util.deg2rad(e.alpha);
const beta = util.deg2rad(e.beta);
const gamma = util.deg2rad(e.gamma);
const [screenNormal, phi, theta] = util.toPolarCoordinates(alpha, beta, gamma);
// "horizontal rotation": phi offset by 90 degrees.
// rotH ∈ [-π, π]
const rotH = phi - Math.PI / 2;
// "vertical rotation": theta mirrored at xy plane.
// rotV ∈ [0, π / 2]
const rotV = Math.abs(theta - Math.PI / 2);
render(ctx, audio, rotH, rotV);
});
})
.catch(err => {
document.body.dataset.state = "error";
error.innerHTML = err;
});
}, { once: true });
})();
+63
View File
@@ -0,0 +1,63 @@
const m3 = {
xRotation: (phi) => {
const c = Math.cos(phi), s = Math.sin(phi);
return [
1, 0, 0,
0, c, -s,
0, s, c
];
},
yRotation: (phi) => {
const c = Math.cos(phi), s = Math.sin(phi);
return [
c, 0, s,
0, 1, 0,
-s, 0, c
];
},
zRotation: (phi) => {
const c = Math.cos(phi), s = Math.sin(phi);
return [
c, -s, 0,
s, c, 0,
0, 0, 1
];
},
add: (m1, m2) => [
m1[0] + m2[0],
m1[1] + m2[1],
m1[2] + m2[2],
m1[3] + m2[3],
m1[4] + m2[4],
m1[5] + m2[5],
m1[6] + m2[6],
m1[7] + m2[7],
m1[8] + m2[8],
],
mul: (m1, m2) => [
m1[0]*m2[0] + m1[1]*m2[3] + m1[2]*m2[6],
m1[0]*m2[1] + m1[1]*m2[4] + m1[2]*m2[7],
m1[0]*m2[2] + m1[1]*m2[5] + m1[2]*m2[8],
m1[3]*m2[0] + m1[4]*m2[3] + m1[5]*m2[6],
m1[3]*m2[1] + m1[4]*m2[4] + m1[5]*m2[7],
m1[3]*m2[2] + m1[4]*m2[5] + m1[5]*m2[8],
m1[6]*m2[0] + m1[7]*m2[3] + m1[8]*m2[6],
m1[6]*m2[1] + m1[7]*m2[4] + m1[8]*m2[7],
m1[6]*m2[2] + m1[7]*m2[5] + m1[8]*m2[8],
],
mulV: (m, v) => [
m[0]*v[0] + m[1]*v[1] + m[2]*v[2],
m[3]*v[0] + m[4]*v[1] + m[5]*v[2],
m[6]*v[0] + m[7]*v[1] + m[8]*v[2]
],
eq: (m1, m2) =>
m1[0] === m2[0] &&
m1[1] === m2[1] &&
m1[2] === m2[2] &&
m1[3] === m2[3] &&
m1[4] === m2[4] &&
m1[5] === m2[5] &&
m1[6] === m2[6] &&
m1[7] === m2[7] &&
m1[8] === m2[8],
};
+34
View File
@@ -0,0 +1,34 @@
const util = {
getGyroPermission: () => {
if(!DeviceOrientationEvent.requestPermission) {
return Promise.resolve("granted")
} else{
return DeviceOrientationEvent.requestPermission()
}
},
round: (x, decimalPlaces) =>
Math.round(x * Math.pow(10, decimalPlaces)) / Math.pow(10, decimalPlaces),
// parameters in radians
// return values in radians
toPolarCoordinates: (alpha, beta, gamma) => {
const rz = m3.zRotation(alpha);
const rx = m3.xRotation(beta);
const ry = m3.yRotation(gamma);
const rot = m3.mul(rz, m3.mul(rx, ry));
const screenNormal = m3.mulV(rot, [0, 0, 1]);
const phi = Math.atan2(screenNormal[1], screenNormal[0]);
const r = v3.norm2(screenNormal);
// theta ∈ [0, π]
// where 0 is lying flat and π is upside down
const theta = Math.acos(screenNormal[2] / r);
return [screenNormal, phi, theta];
},
rgb: (r, g, b) => `rgb(${255 * r}, ${255 * g}, ${255 * b})`,
rgba: (r, g, b, a) => `rgb(${255 * r}, ${255 * g}, ${255 * b}, ${a})`,
deg2rad: (deg) => deg / 180 * Math.PI,
diffAngles: (a, b) => Math.atan2(Math.sin(b - a), Math.cos(b - a)),
};
+18
View File
@@ -0,0 +1,18 @@
const v3 = {
add: (v1, v2) => [
v1[0] + v2[0],
v1[1] + v2[2],
v1[2] + v2[2],
],
subtract: (v1, v2) => [
v1[0] - v2[0],
v1[1] - v2[2],
v1[2] - v2[2],
],
norm2: (v) =>
Math.sqrt(
v[0] * v[0] +
v[1] * v[1] +
v[2] * v[2]
),
};