Skip to content

Commit 666716b

Browse files
committed
feat: add onpa-svg.html
1 parent d950d11 commit 666716b

File tree

1 file changed

+185
-0
lines changed

1 file changed

+185
-0
lines changed

entries/onpa-svg.html

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<link
7+
rel="shortcut icon"
8+
href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%222048px%22%20height%3D%222048px%22%20viewBox%3D%220%2C0%2C128%2C128%22%3E%3Cg%20stroke%3D%22%23111%22%20stroke-width%3D%226%22%20fill%3D%22white%22%20stroke-linecap%3D%22round%22%3E%3Cpath%20d%3D%22M48%2C28a16%2C16%2C0%2C0%2C0%2C32%2C0%22%3E%3C%2Fpath%3E%3Cpath%20d%3D%22M26%2C68a72%2C32%2C0%2C0%2C1%2C74%2C0%22%3E%3C%2Fpath%3E%3Cpath%20d%3D%22M28%2C78a48%2C32%2C0%2C0%2C1%2C70%2C0%22%3E%3C%2Fpath%3E%3Cpath%20d%3D%22M40%2C96a48%2C64%2C0%2C0%2C1%2C48%2C0%22%3E%3C%2Fpath%3E%3Ccircle%20cx%3D%2264%22%20cy%3D%2264%22%20r%3D%2232%22%20fill%3D%22white%22%3E%3C%2Fcircle%3E%3Cpath%20d%3D%22M46%2C68a12%2C2%2C0%2C0%2C0%2C24%2C0%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3Cg%20fill%3D%22%23111%22%3E%3Ccircle%20cx%3D%2248%22%20cy%3D%2252%22%20r%3D%223%22%20%2F%3E%3Ccircle%20cx%3D%2270%22%20cy%3D%2252%22%20r%3D%223%22%20%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"
9+
type="image/svg+xml"
10+
/>
11+
<title>Onpa.svg👋</title>
12+
<meta
13+
name="description"
14+
content="You can save wave shapes of your voice with Onpa.svg."
15+
/>
16+
<meta name="author" content="Manybugs" />
17+
<meta name="github" content="manybugsdev" />
18+
<style>
19+
* {
20+
box-sizing: border-box;
21+
}
22+
body {
23+
background: #111;
24+
color: white;
25+
font-family: sans-serif;
26+
}
27+
main {
28+
width: 100%;
29+
padding: 0 1rem;
30+
margin: 0 auto;
31+
max-width: 600px;
32+
}
33+
svg {
34+
border: white 3px solid;
35+
width: 100%;
36+
aspect-ratio: 16 / 9;
37+
}
38+
button {
39+
background: white;
40+
color: #111;
41+
border: none;
42+
outline: none;
43+
font-weight: bold;
44+
border-radius: 9999px;
45+
cursor: pointer;
46+
margin: 0 0.2rem;
47+
}
48+
button:disabled {
49+
opacity: 0.3;
50+
cursor: default;
51+
}
52+
input[type="range"] {
53+
appearance: none;
54+
background: white;
55+
height: 1px;
56+
width: 100%;
57+
}
58+
</style>
59+
</head>
60+
<body>
61+
<main>
62+
<h1>Onpa.svg</h1>
63+
<p>Save wave shapes of your voice.</p>
64+
<svg
65+
id="view"
66+
viewBox="0 0 640 360"
67+
version="1.1"
68+
xmlns="http://www.w3.org/2000/svg"
69+
fill="gray"
70+
></svg>
71+
<p>
72+
<label
73+
>Timeline
74+
<input
75+
id="timeline"
76+
type="range"
77+
oninput="changeTimeline(event.target.value)"
78+
/></label>
79+
</p>
80+
<p>
81+
<button id="activateButton" onclick="activate()">Activate</button>
82+
<button id="toggleButton" onclick="toggle()">Start</button>
83+
<button id="saveButton" onclick="save()">Save</button>
84+
</p>
85+
</main>
86+
<script>
87+
/**
88+
* Constants
89+
*/
90+
const bufferLength = 256; // must be power of 2
91+
const recordsLength = 128;
92+
/**
93+
* States
94+
*/
95+
const states = {
96+
analyser: null,
97+
recording: false,
98+
records: Array.from({ length: recordsLength }, () =>
99+
new Uint8Array(bufferLength).fill(128)
100+
),
101+
index: recordsLength - 1,
102+
};
103+
/**
104+
* Change timelines.
105+
*/
106+
function changeTimeline(index) {
107+
states.index = index;
108+
update();
109+
}
110+
/**
111+
* Activate microphones.
112+
*/
113+
async function activate() {
114+
const stream = await navigator.mediaDevices.getUserMedia({
115+
audio: true,
116+
});
117+
const audioCtx = new (window.AudioContext ||
118+
window.webkitAudioContext)();
119+
states.analyser = audioCtx.createAnalyser();
120+
const source = audioCtx.createMediaStreamSource(stream);
121+
source.connect(states.analyser);
122+
toggle();
123+
}
124+
/**
125+
* Toggle recording.
126+
*/
127+
function toggle() {
128+
states.recording = !states.recording;
129+
requestAnimationFrame(frame);
130+
}
131+
/**
132+
* Animation frame
133+
*/
134+
function frame() {
135+
update();
136+
if (!states.recording) return;
137+
states.analyser.fftSize = 2 * bufferLength;
138+
const dataArray = new Uint8Array(bufferLength);
139+
states.analyser.getByteTimeDomainData(dataArray);
140+
states.records.push(dataArray);
141+
states.records.shift();
142+
requestAnimationFrame(frame);
143+
}
144+
/**
145+
* Save a svg file.
146+
*/
147+
function save() {
148+
const a = document.createElement("a");
149+
a.href = `data:image/svg+xml,${encodeURIComponent(view.outerHTML)}`;
150+
a.download = "onpa.svg";
151+
a.click();
152+
}
153+
/**
154+
* Update DOM with states.
155+
*/
156+
function update() {
157+
const ys = states.records[states.index];
158+
view.innerHTML = Array.from({ length: 63 }, (_, i) => i * 10 + 10)
159+
.map((x, i) => {
160+
let y = ys[Math.floor((i / 63) * bufferLength)] - 128;
161+
y = Math.abs(y);
162+
return `<rect x="${x}" y="${
163+
180 - y
164+
}" width="${3}" height="${Math.max(Math.abs(2 * y), 2)}"></rect>`;
165+
})
166+
.join("");
167+
activateButton.disabled = !!states.analyser;
168+
saveButton.disabled = toggleButton.disabled = !states.analyser;
169+
toggleButton.textContent = states.recording ? "Stop" : "Start";
170+
timeline.min = 0;
171+
timeline.max = recordsLength - 1;
172+
timeline.step = 1;
173+
timeline.value = states.index;
174+
}
175+
update();
176+
</script>
177+
</body>
178+
</html>
179+
<!--
180+
References
181+
----------
182+
183+
* https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API
184+
* https://developer.mozilla.org/ja/docs/Web/API/Media_Capture_and_Streams_API/Taking_still_photos
185+
-->

0 commit comments

Comments
 (0)