Skip to content

Rotate and Delete from the ground up #660

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export { Overlay } from './selection/overlay.element'
export { BoxModel } from './selection/box-model.element'
export { Corners } from './selection/corners.element'
export { Grip } from './selection/grip.element'
export { Rotation } from './selection/rotation.element'
export { Delete } from './selection/delete.element'

export { Metatip } from './metatip/metatip.element'
export { Ally } from './metatip/ally.element'
Expand Down
34 changes: 34 additions & 0 deletions app/components/selection/delete.element.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@import "../_variables.css";

:host {
position: var(--position, absolute);
top: var(--top, 0);
left: var(--left, 0);
z-index: var(--layer-top);
}

:host button {
background: var(--neon-pink);
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
padding: 0;
transition: transform 0.2s ease;
}

:host button:hover {
transform: scale(1.1);
}

:host svg {
width: 16px;
height: 16px;
fill: currentColor;
}
82 changes: 82 additions & 0 deletions app/components/selection/delete.element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { DeleteStyles } from '../styles.store'
import { animateViewTransition } from '../../utilities/'

export class Delete extends HTMLElement {
constructor() {
super()
this.$shadow = this.attachShadow({mode: 'closed'})
this.styles = [DeleteStyles]
this.observers = []
}

addObservers(observers) {
this.observers = observers
}

set linkedElementsToDeleteToo(elements) {
this._linkedElementsToDeleteToo = elements
}

connectedCallback() {
this.$shadow.adoptedStyleSheets = this.styles
this.$shadow.innerHTML = this.render()

const deleteBtn = this.$shadow.querySelector('button')
deleteBtn.addEventListener('click', this.deleteElement.bind(this))
}

set position({el}) {
this.targetElement = el
const {top, right} = el.getBoundingClientRect()
const isFixed = getComputedStyle(el).position === 'fixed'

this.style.setProperty('--top', `${top + (isFixed ? 0 : window.scrollY)}px`)
this.style.setProperty('--left', `${right + 10}px`)
this.style.setProperty('--position', isFixed ? 'fixed' : 'absolute')
}

async deleteElement(e) {
e.preventDefault()
e.stopPropagation()

if (!this.targetElement) {
console.warn('No target element found to delete')
return
}

this.observers.forEach(observer => observer.disconnect())

const elements = [
this.targetElement,
...this._linkedElementsToDeleteToo || [],
...Array.from(document.querySelectorAll(`[data-label-id="${this.targetElement.getAttribute('data-label-id')}"]`)),
this
]

await animateViewTransition(elements, () => this.performDeletion())
}

performDeletion() {
this._linkedElementsToDeleteToo?.forEach(el => el.remove())
this.targetElement.remove()

const labelId = this.targetElement.getAttribute('data-label-id')
if (labelId) {
document.querySelectorAll(`[data-label-id="${labelId}"]`).forEach(el => el.remove())
}

this.remove()
}

render() {
return `
<button type="button" aria-label="Delete element">
<svg viewBox="0 0 24 24">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>
`
}
}

customElements.define('visbug-delete', Delete)
56 changes: 56 additions & 0 deletions app/components/selection/rotation.element.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
@import "../_variables.css";

:host {
position: var(--position);
top: var(--top);
left: var(--left);
pointer-events: none;
z-index: var(--layer-3);
}

:host .rotation-line {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}

:host .rotation-line.active {
display: block;
}

:host .rotation-line line {
stroke: var(--neon-pink);
stroke-width: 1;
}

:host .rotation-handle {
position: absolute;
width: 24px;
height: 24px;
top: -30px;
left: calc(var(--width) / 2 - 12px);
cursor: grab;
pointer-events: all;
background: var(--neon-pink);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
-webkit-user-select: none;
}

:host .rotation-handle:active {
cursor: grabbing;
}

:host .rotation-icon {
width: 16px;
height: 16px;
fill: white;
pointer-events: none;
}
132 changes: 132 additions & 0 deletions app/components/selection/rotation.element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { HandlesStyles, RotationStyles } from '../styles.store'

export class Rotation extends HTMLElement {
constructor() {
super()
this.$shadow = this.attachShadow({mode: 'closed'})
this.styles = [HandlesStyles, RotationStyles]
this.totalAngle = 0
}

connectedCallback() {
this.$shadow.adoptedStyleSheets = this.styles
}

set position({el}) {
this.targetElement = el

const computedStyle = getComputedStyle(el)
if (computedStyle.display === 'inline') el.style.display = 'inline-block'

const {left, top, width, height} = el.getBoundingClientRect()
const isFixed = computedStyle.position === 'fixed'

this.style.setProperty('--top', `${top + (isFixed ? 0 : window.scrollY)}px`)
this.style.setProperty('--left', `${left}px`)
this.style.setProperty('--position', isFixed ? 'fixed' : 'absolute')
this.style.setProperty('--width', `${width}px`)

this.$shadow.innerHTML = this.render()
this.setupRotationHandlers()
}

setupRotationHandlers() {
const handle = this.$shadow.querySelector('.rotation-handle')
const line = this.$shadow.querySelector('.rotation-line')

const onMouseDown = e => {
e.preventDefault()
const {left, top, width, height} = this.targetElement.getBoundingClientRect()
this.originalCenter = {
x: left + width / 2,
y: top + height / 2
}
this.lastAngle = Math.atan2(
e.clientY - this.originalCenter.y,
e.clientX - this.originalCenter.x
)

this.handleRadius = Math.sqrt(
Math.pow(e.clientX - this.originalCenter.x, 2) +
Math.pow(e.clientY - this.originalCenter.y, 2)
)

line.classList.add('active')

document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}

const onMouseMove = e => {
const currentAngle = Math.atan2(
e.clientY - this.originalCenter.y,
e.clientX - this.originalCenter.x
)

// track accumulated rotation to allow multiple revolutions
if (!this.lastAngle) this.lastAngle = currentAngle
let delta = currentAngle - this.lastAngle
// normalize the delta to avoid "flipping" at boundary crossing
if (delta > Math.PI) {
delta -= 2 * Math.PI
} else if (delta < -Math.PI) {
delta += 2 * Math.PI
}

this.totalAngle += delta
this.lastAngle = currentAngle

const rotationDegrees = this.totalAngle * (180 / Math.PI)
this.targetElement.style.transform = `rotate(${rotationDegrees}deg)`

const handleX = e.clientX
const handleY = e.clientY

const hostRect = this.getBoundingClientRect()
const handleRect = handle.getBoundingClientRect()
const handleSize = handleRect.width

// position handle centered on cursor
handle.style.left = `${handleX - hostRect.left - handleSize/2}px`
handle.style.top = `${handleY - hostRect.top - handleSize/2}px`

const lineSvg = line.querySelector('line')
lineSvg.setAttribute('x1', this.originalCenter.x)
lineSvg.setAttribute('y1', this.originalCenter.y)
lineSvg.setAttribute('x2', handleX)
lineSvg.setAttribute('y2', handleY)
}
Comment on lines +66 to +98
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love this so much

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

human + ai teamwork for the win! i remember cleaning up the comments and code and making suggestions and asking for eli5 explanations of why this weird math works haha


const onMouseUp = () => {
this.lastAngle = 0
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
line.classList.remove('active')
}

handle.addEventListener('mousedown', onMouseDown)

this.cleanup = () => {
handle.removeEventListener('mousedown', onMouseDown)
}
}

disconnectedCallback() {
this.cleanup && this.cleanup()
}

render() {
return `
<svg class="rotation-line">
<line/>
</svg>
<div class="rotation-handle">
<svg class="rotation-icon" viewBox="0 0 24 24">
<path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>
</svg>
</div>
`
}
}

customElements.define('visbug-rotation', Rotation)
4 changes: 4 additions & 0 deletions app/components/styles.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { default as boxmodel_css } from './selection/box-model.element.css'
import { default as metatip_css } from './metatip/metatip.element.css'
import { default as hotkeymap_css } from './hotkey-map/base.element.css'
import { default as grip_css } from './selection/grip.element.css'
import { default as rotation_css } from './selection/rotation.element.css'
import { default as delete_css } from './selection/delete.element.css'

import { default as light_css } from './_variables_light.css'
import { default as visbug_light_css } from './vis-bug/vis-bug.element_light.css'
Expand Down Expand Up @@ -44,6 +46,8 @@ export const OverlayStyles = constructStylesheet(overlay_css)
export const BoxModelStyles = constructStylesheet(boxmodel_css)
export const HotkeymapStyles = constructStylesheet(hotkeymap_css)
export const GripStyles = constructStylesheet(grip_css)
export const RotationStyles = constructStylesheet(rotation_css)
export const DeleteStyles = constructStylesheet(delete_css)

export const LightTheme = constructStylesheet(light_css)
export const VisBugLightStyles = constructStylesheet(visbug_light_css)
Expand Down
2 changes: 1 addition & 1 deletion app/components/vis-bug/vis-bug.element.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import hotkeys from 'hotkeys-js'

import {
Handles, Handle, Label, Overlay, Gridlines, Corners,
Hotkeys, Metatip, Ally, Distance, BoxModel, Grip
Hotkeys, Metatip, Ally, Distance, BoxModel, Grip, Rotation, Delete
} from '../'

import {
Expand Down
Loading