music player

This commit is contained in:
“chrisshank” 2024-09-09 20:57:22 -07:00
parent 627149053c
commit 7523465c0b
6 changed files with 445 additions and 25 deletions

131
demo/music.html Normal file
View File

@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Music</title>
<style>
html {
height: 100%;
}
body {
min-height: 100%;
position: relative;
margin: 0;
}
spatial-geometry:has(record-player) {
&::part(resize-nw),
&::part(resize-ne),
&::part(resize-se),
&::part(resize-sw) {
display: none;
}
}
spatial-geometry > video {
height: 100%;
}
</style>
</head>
<body>
<!--
sips -c 326 276 --cropOffset 1 1 *.png --out pngs/
ffmpeg -framerate 25 -pattern_type glob -i '*.png' -c:v prores -pix_fmt yuva444p10le dancing-flower.mov
ffmpeg -framerate 25 -f image2 -pattern_type glob -i '*.png' -c:v libvpx-vp9 -pix_fmt yuva420p dancing-flowers.webm
-->
<spatial-geometry x="100" y="75" width="330" height="198">
<record-player>
<audio src="/Feather.mp3"></audio>
</record-player>
</spatial-geometry>
<spatial-geometry x="25" y="300" width="166" height="200">
<video loop>
<source src="/dancing-flower.mov" type="video/quicktime" />
<source src="/dancing-flower.webm" type="video/webm" />
</video>
</spatial-geometry>
<spatial-geometry x="155" y="315" width="166" height="200">
<video loop>
<source src="/dancing-flower.mov" type="video/quicktime" />
<source src="/dancing-flower.webm" type="video/webm" />
</video>
</spatial-geometry>
<spatial-geometry x="280" y="305" width="166" height="200">
<video loop>
<source src="/dancing-flower.mov" type="video/quicktime" />
<source src="/dancing-flower.webm" type="video/webm" />
</video>
</spatial-geometry>
<spatial-geometry x="415" y="295" width="166" height="200">
<video loop>
<source src="/dancing-flower.mov" type="video/quicktime" />
<source src="/dancing-flower.webm" type="video/webm" />
</video>
</spatial-geometry>
<spatial-geometry x="20" y="525" width="166" height="200">
<video loop>
<source src="/dancing-flower.mov" type="video/quicktime" />
<source src="/dancing-flower.webm" type="video/webm" />
</video>
</spatial-geometry>
<spatial-geometry x="160" y="545" width="166" height="200">
<video loop>
<source src="/dancing-flower.mov" type="video/quicktime" />
<source src="/dancing-flower.webm" type="video/webm" />
</video>
</spatial-geometry>
<spatial-geometry x="290" y="530" width="166" height="200">
<video loop>
<source src="/dancing-flower.mov" type="video/quicktime" />
<source src="/dancing-flower.webm" type="video/webm" />
</video>
</spatial-geometry>
<spatial-geometry x="420" y="520" width="166" height="200">
<video loop>
<source src="/dancing-flower.mov" type="video/quicktime" />
<source src="/dancing-flower.webm" type="video/webm" />
</video>
</spatial-geometry>
<script type="module">
import { SpatialGeometry } from '../src/canvas/spatial-geometry.ts';
import { RecordPlayer } from '../src/music/record-player.ts';
SpatialGeometry.register();
RecordPlayer.register();
const flowers = Array.from(document.querySelectorAll('video'));
const audio = document.querySelector('audio');
function setPlayback(e) {
e.target.playbackRate = (91 / 60) * e.target.duration;
e.target.removeEventListener('canplay', setPlayback);
}
flowers.forEach((el) => {
el.addEventListener('canplay', setPlayback);
});
audio.addEventListener('play', () => {
setTimeout(() => flowers.forEach((el) => el.play()), 1200);
});
audio.addEventListener('pause', () => {
flowers.forEach((el) => {
el.pause();
el.currentTime = 0;
});
});
</script>
</body>
</html>

BIN
demo/public/Feather.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -34,12 +34,22 @@ styles.replaceSync(`
content-visibility: auto;
}
::slotted(*) {
cursor: default;
}
:host > div {
position: relative;
width: 100%;
height: 100%;
}
:host > div > div {
width: 100%;
height: 100%;
overflow: hidden;
}
:host(:focus-within) > div {
outline: solid 1px hsl(214, 84%, 56%);
}
@ -62,7 +72,7 @@ styles.replaceSync(`
box-sizing: border-box;
padding: 0;
background: hsl(210, 20%, 98%);
z-index: calc(infinity); /* should the handlers always show? */
z-index: calc(infinity);
&[part="resize-nw"],
&[part="resize-ne"],
@ -105,6 +115,7 @@ styles.replaceSync(`
}
[part="rotate"] {
z-index: calc(infinity);
display: block;
position: absolute;
box-sizing: border-box;
@ -117,19 +128,7 @@ styles.replaceSync(`
top: 0;
left: 50%;
translate: -50% -150%;
z-index: 2;
cursor: url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(45 16 16)' filter='url(%23shadow)'><path d='M22.4789 9.45728L25.9935 12.9942L22.4789 16.5283V14.1032C18.126 14.1502 14.6071 17.6737 14.5675 22.0283H17.05L13.513 25.543L9.97889 22.0283H12.5674C12.6071 16.5691 17.0214 12.1503 22.4789 12.1031L22.4789 9.45728Z' fill='black'/><path fill-rule='evenodd' clip-rule='evenodd' d='M21.4789 7.03223L27.4035 12.9945L21.4789 18.9521V15.1868C18.4798 15.6549 16.1113 18.0273 15.649 21.0284H19.475L13.5128 26.953L7.55519 21.0284H11.6189C12.1243 15.8155 16.2679 11.6677 21.4789 11.1559L21.4789 7.03223ZM22.4789 12.1031C17.0214 12.1503 12.6071 16.5691 12.5674 22.0284H9.97889L13.513 25.543L17.05 22.0284H14.5675C14.5705 21.6896 14.5947 21.3558 14.6386 21.0284C15.1157 17.4741 17.9266 14.6592 21.4789 14.1761C21.8063 14.1316 22.1401 14.1069 22.4789 14.1032V16.5284L25.9935 12.9942L22.4789 9.45729L22.4789 12.1031Z' fill='white'/></g></svg>") 16 16, pointer;
}
[part="rotate"]::before {
box-sizing: border-box;
display: block;
position: absolute;
translate: -50% -150%;
z-index: 2;
border: 1px solid hsl(214, 84%, 56%);
height: 50%;
width: 1px;
}`);
// TODO: add z coordinate?
@ -147,8 +146,8 @@ export class SpatialGeometry extends HTMLElement {
this.addEventListener('pointerdown', this);
this.addEventListener('lostpointercapture', this);
this.addEventListener('touchstart', this);
this.addEventListener('dragstart', this);
// this.addEventListener('touchstart', this);
// this.addEventListener('dragstart', this);
const shadowRoot = this.attachShadow({ mode: 'open', delegatesFocus: true });
shadowRoot.adoptedStyleSheets.push(styles);
@ -162,7 +161,7 @@ export class SpatialGeometry extends HTMLElement {
<button part="resize-ne"></button>
<button part="resize-se"></button>
<button part="resize-sw"></button>
<slot />
<div><slot></slot></div>
</div>`;
}
@ -258,12 +257,10 @@ export class SpatialGeometry extends HTMLElement {
case 'pointerdown': {
if (event.button !== 0 || event.ctrlKey) return;
let target = event.composedPath()[0] as HTMLElement;
const target = event.composedPath()[0] as HTMLElement;
// if a resize handler isn't interacted with then we should move the element.
if (!target.hasAttribute('part')) {
target = this;
}
// ignore interactions from slotted elements.
if (target !== this && !target.hasAttribute('part')) return;
target.addEventListener('pointermove', this);
target.setPointerCapture(event.pointerId);
@ -314,11 +311,7 @@ export class SpatialGeometry extends HTMLElement {
var newAngle =
((Math.atan2(event.clientY - centerY, event.clientX - centerX) + Math.PI / 2) * 180) /
Math.PI;
console.log(newAngle);
this.rotate = newAngle;
// When a rotate handler is
// newAngle = (Math.atan2(centerY - mouseY, centerX - mouseX) * 180) / Math.PI - currentAngle;
return;
}

296
src/music/record-player.ts Normal file
View File

@ -0,0 +1,296 @@
// Ported from https://github.com/bitu467/record-player
const styles = new CSSStyleSheet();
styles.replaceSync(`
::slotted(*) {
display: none;
}
:host {
display: block;
}
.player {
background-color: #d52831;
width: 330px;
height: 190px;
position: absolute;
transform: translate(-50%, -50%);
left: 50%;
top: 50%;
border-radius: 10px;
box-shadow: 0 8px 0 0 #be2728;
margin-top: -4px;
}
.record {
width: 175px;
height: 175px;
background-color: #181312;
position: absolute;
border-radius: 50%;
top: 10px;
left: 20px;
display: flex;
justify-content: center;
align-items: center;
animation: spin 3s linear infinite;
animation-play-state: paused;
}
.record::before,
.record::after {
content: '';
position: absolute;
border: 5px solid transparent;
border-top-color: #2c2424;
border-bottom-color: #2c2424;
border-radius: 50%;
}
.record::before {
width: 135px;
height: 135px;
}
.record:after {
width: 95px;
height: 95px;
}
.label {
height: 15px;
width: 15px;
background-color: #181312;
border: 20px solid #ff8e00;
border-radius: 50%;
}
.tone-arm {
height: 90px;
width: 6px;
background-color: #ffffff;
position: absolute;
top: 25px;
right: 95px;
transform-origin: top;
}
.control {
background-color: #181312;
width: 17px;
height: 17px;
border: 10px solid #2c2c2c;
border-radius: 50%;
position: absolute;
top: -16px;
left: -16px;
}
.tone-arm::before {
content: '';
position: absolute;
height: 40px;
width: 6px;
background-color: #ffffff;
transform: rotate(30deg);
bottom: -36px;
right: 10px;
}
.tone-arm::after {
content: '';
position: absolute;
height: 0px;
width: 10px;
border-top: 18px solid #b2aea6;
border-left: 2px solid transparent;
border-right: 2px solid transparent;
top: 108px;
right: 12.5px;
transform: rotate(30deg);
}
.btn {
width: 28px;
height: 28px;
background-color: #ed5650;
border-radius: 50%;
position: absolute;
bottom: 20px;
right: 30px;
border: none;
border: 3.5px solid rgb(190, 39, 42);
outline: none;
cursor: pointer;
}
.slider {
-webkit-appearance: none;
appearance: none;
transform: rotate(-90deg);
width: 90px;
height: 7px;
position: absolute;
left: 233px;
top: 60px;
background-color: #be272a;
outline: none;
border-radius: 3px;
border: 6px solid #ed5650;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 10px;
height: 12px;
background-color: #ffffff;
cursor: pointer;
}
:host(:state(playing)) .tone-arm {
--move-time: 3s;
animation-fill-mode: forwards;
animation-timing-function: linear;
animation:
position-arm var(--move-time),
move-arm var(--duration) var(--move-time),
reset-arm var(--move-time) calc(var(--duration) + var(--move-time));
}
:host(:state(playing)) .record {
animation-play-state: running;
}
@keyframes position-arm {
20% {
transform: rotateX(20deg);
}
70% {
transform: rotateX(20deg);
rotate: 14deg;
}
100% {
rotate: 14deg;
}
}
@keyframes move-arm {
from {
rotate: 14deg;
}
to {
rotate: 43deg;
}
}
@keyframes reset-arm {
20% {
transform: rotateX(20deg);
rotate: 43deg;
}
80% {
transform: rotateX(20deg);
}
}
@keyframes spin {
from {
rotate: 0deg;
}
to {
rotate: 360deg;
}
}
`);
export class RecordPlayer extends HTMLElement {
static tagName = 'record-player';
static register() {
customElements.define(this.tagName, this);
}
#internals = this.attachInternals();
#audio = this.querySelector('audio')!;
#volumeInput: HTMLInputElement;
constructor() {
super();
this.addEventListener('click', this);
this.#audio.addEventListener('ended', this);
const shadow = this.attachShadow({ mode: 'open' });
shadow.adoptedStyleSheets.push(styles);
shadow.innerHTML = `
<div class="player">
<div class="record">
<div class="label"></div>
</div>
<div class="tone-arm">
<div class="control"></div>
</div>
<button class="btn"></button>
<input type="range" class="slider" min="0" max="1" step="0.05" value="0.75">
</div>
<slot></slot>`;
this.#volumeInput = shadow.querySelector('input[type="range"]')!;
this.#volumeInput.addEventListener('input', this);
}
get paused() {
return this.#audio.paused;
}
#playTimeout: number = -1;
play() {
if (!this.paused) return;
this.#audio.volume = this.#volumeInput.valueAsNumber;
this.style.setProperty('--duration', `${this.#audio.duration}s`);
this.#internals.states.add('playing');
this.#playTimeout = window.setTimeout(() => this.#audio.play(), 3000);
}
stop() {
if (this.paused) return;
clearTimeout(this.#playTimeout);
this.#internals.states.delete('playing');
this.#audio.pause();
this.#audio.currentTime = 0;
}
handleEvent(event: Event) {
switch (event.type) {
case 'click': {
const target = event.composedPath()[0] as HTMLElement;
if (target.tagName !== 'BUTTON') return;
this.paused ? this.play() : this.stop();
return;
}
case 'input': {
this.#audio.volume = this.#volumeInput.valueAsNumber;
return;
}
case 'ended': {
this.#internals.states.delete('playing');
return;
}
}
}
}