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; + } + } + } +}