diff --git a/demo/music.html b/demo/music.html
new file mode 100644
index 0000000..440e537
--- /dev/null
+++ b/demo/music.html
@@ -0,0 +1,131 @@
+
+
+
+
+
+ Music
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/public/Feather.mp3 b/demo/public/Feather.mp3
new file mode 100644
index 0000000..f8d252f
Binary files /dev/null and b/demo/public/Feather.mp3 differ
diff --git a/demo/public/dancing-flower.mov b/demo/public/dancing-flower.mov
new file mode 100644
index 0000000..8f45948
Binary files /dev/null and b/demo/public/dancing-flower.mov differ
diff --git a/demo/public/dancing-flower.webm b/demo/public/dancing-flower.webm
new file mode 100644
index 0000000..1220d74
Binary files /dev/null and b/demo/public/dancing-flower.webm differ
diff --git a/src/canvas/spatial-geometry.ts b/src/canvas/spatial-geometry.ts
index ced4397..8aaa6c1 100644
--- a/src/canvas/spatial-geometry.ts
+++ b/src/canvas/spatial-geometry.ts
@@ -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,") 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 {
-
+
`;
}
@@ -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;
}
diff --git a/src/music/record-player.ts b/src/music/record-player.ts
new file mode 100644
index 0000000..d1b0598
--- /dev/null
+++ b/src/music/record-player.ts
@@ -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 = `
+
+`;
+
+ 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;
+ }
+ }
+ }
+}