Skip to content
3 changes: 3 additions & 0 deletions frontend/src/components/PropertiesPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import SlideProperties from '@/components/SlideProperties.vue'
import TextProperties from '@/components/TextProperties.vue'
import ImageProperties from '@/components/ImageProperties.vue'
import VideoProperties from '@/components/VideoProperties.vue'
import ShapeProperties from '@/components/ShapeProperties.vue'
import AlignmentControls from '@/components/AlignmentControls.vue'
import PlacementProperties from '@/components/PlacementProperties.vue'
import AppearanceProperties from '@/components/AppearanceProperties.vue'
Expand All @@ -42,6 +43,8 @@ const activeProperties = computed(() => {
return ImageProperties
case 'video':
return VideoProperties
case 'shape':
return ShapeProperties
}
})
</script>
Expand Down
47 changes: 45 additions & 2 deletions frontend/src/components/ResizeHandle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,35 @@ const baseStyles = {
}

const getWidthResizerStyles = () => {
const resizer = props.direction

const offsetX = `-${3 / slideBounds.scale}px`
return {
...baseStyles,
cursor: 'ew-resize',
left: props.direction === 'left' ? offsetX : 'auto',
right: props.direction === 'right' ? offsetX : 'auto',
left: resizer.includes('left') ? offsetX : 'auto',
right: resizer.includes('right') ? offsetX : 'auto',
top: `calc(50% - ${7 / slideBounds.scale}px)`,
width: `${4 / slideBounds.scale}px`,
height: `${14 / slideBounds.scale}px`,
}
}

const getHeightResizerStyles = () => {
const resizer = props.direction

const offsetX = `-${3 / slideBounds.scale}px`
return {
...baseStyles,
cursor: 'ns-resize',
left: `calc(50% - ${7 / slideBounds.scale}px)`,
top: resizer.includes('top') ? offsetX : 'auto',
bottom: resizer.includes('bottom') ? offsetX : 'auto',
width: `${14 / slideBounds.scale}px`,
height: `${4 / slideBounds.scale}px`,
}
}

const getDimensionResizerStyles = () => {
const resizer = props.direction
const cursorStyles = {
Expand All @@ -63,16 +80,42 @@ const getDimensionResizerStyles = () => {
}
}

const getLineResizerStyles = () => {
const resizer = props.direction

const offsetX = props.currentResizer
? `-${5 / slideBounds.scale}px`
: `-${4.5 / slideBounds.scale}px`
const size = props.currentResizer ? `${10 / slideBounds.scale}px` : `${7 / slideBounds.scale}px`
return {
...baseStyles,
cursor: 'ew-resize',
left: resizer === 'line-left' ? offsetX : 'auto',
right: resizer === 'line-right' ? offsetX : 'auto',
top: `calc(50% - ${size} / 2)`,
width: size,
height: size,
}
}

const resizerStyles = computed(() => {
switch (props.direction) {
case 'text-left':
case 'text-right':
case 'left':
case 'right':
return getWidthResizerStyles()
case 'top':
case 'bottom':
return getHeightResizerStyles()
case 'top-left':
case 'top-right':
case 'bottom-left':
case 'bottom-right':
return getDimensionResizerStyles()
case 'line-left':
case 'line-right':
return getLineResizerStyles()
default:
return {}
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/ResizeIndicator.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div :style="styles" :class="indicatorClasses">
<div v-if="type === 'text'">{{ Math.round(dimensions.width) }}</div>
<div v-if="['text', 'line'].includes(type)">{{ Math.round(dimensions.width) }}</div>
<template v-else>
<div>{{ Math.round(dimensions.width) }} × {{ Math.round(dimensions.height) }}</div>
</template>
Expand Down
40 changes: 34 additions & 6 deletions frontend/src/components/Resizer.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<template>
<div>
<RotateHandle />

<ResizeHandle
v-for="resizeHandle in resizeHandles"
v-show="resizeHandle.isVisible"
Expand All @@ -8,19 +10,20 @@
@startResize="(e) => startResize(e, resizeHandle.direction)"
/>

<ResizeIndicator
<!-- <ResizeIndicator
v-show="currentResizer"
:type="elementType"
:dimensions="dimensions"
:indicatorStyles="indicatorStyles"
/>
/> -->
</div>
</template>

<script setup>
import { computed, inject } from 'vue'

import ResizeHandle from '@/components/ResizeHandle.vue'
import RotateHandle from '@/components/RotateHandle.vue'
import ResizeIndicator from '@/components/ResizeIndicator.vue'

import { selectionBounds, slideBounds } from '@/stores/slide'
Expand All @@ -45,8 +48,21 @@ const isResizeHandleVisible = (resizer) => {

const resizeHandles = computed(() => {
let directions = []
if (props.elementType === 'text') {
directions = ['left', 'right']
if (['rectangle', 'circle'].includes(props.elementType)) {
directions = [
'left',
'right',
'top',
'bottom',
'top-left',
'top-right',
'bottom-left',
'bottom-right',
]
} else if (props.elementType === 'line') {
directions = ['line-left', 'line-right']
} else if (props.elementType === 'text') {
directions = ['text-left', 'text-right']
} else {
directions = ['top-left', 'top-right', 'bottom-left', 'bottom-right']
}
Expand All @@ -65,12 +81,22 @@ const getTextIndicatorPosition = () => {
const offsetY = getScaledValue(12)

return {
left: resizer.includes('right') ? offsetX : 'auto',
right: resizer.includes('left') ? offsetX : 'auto',
left: resizer.includes('text-right') ? offsetX : 'auto',
right: resizer.includes('text-left') ? offsetX : 'auto',
top: `calc(50% - ${offsetY})`,
}
}

const getLineIndicatorPosition = () => {
const offset = getScaledValue(8)

return {
left: currentResizer.value === 'line-left' ? offset : 'auto',
right: currentResizer.value === 'line-right' ? offset : 'auto',
top: offset,
}
}

const getMediaIndicatorPosition = () => {
const resizer = currentResizer.value
const offset = getScaledValue(8)
Expand All @@ -86,6 +112,8 @@ const getMediaIndicatorPosition = () => {
const getPositionStyles = () => {
if (props.elementType === 'text') {
return getTextIndicatorPosition()
} else if (props.elementType === 'line') {
return getLineIndicatorPosition()
}
return getMediaIndicatorPosition()
}
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/components/RotateHandle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<template>
<div :style="rotateHandleStyles" class="flex items-center justify-center">
<LucideRotateCw class="stroke-[1.5] text-white" :style="rotateIconStyles" />
</div>
</template>
<script setup>
import { computed } from 'vue'

import { slideBounds } from '@/stores/slide'

const rotateHandleStyles = computed(() => {
const size = 20 / slideBounds.scale
const offsetY = -20 / slideBounds.scale - size / 2
return {
position: 'absolute',
zIndex: 9999,
backgroundColor: '#70b6f0',
borderRadius: '50%',
cursor: 'grab',
left: `calc(50% - ${size / 2}px)`,
top: `${offsetY}px`,
width: `${size}px`,
height: `${size}px`,
opacity: 0.8,
}
})

const rotateIconStyles = computed(() => {
const iconSize = 12 / slideBounds.scale
return {
width: `${iconSize}px`,
height: `${iconSize}px`,
}
})
</script>
4 changes: 3 additions & 1 deletion frontend/src/components/SelectionBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div v-show="selectionBounds.width" ref="selected" :style="boxStyles">
<Resizer
v-if="showResizers"
:elementType="activeElement.type"
:elementType="activeElement.shapeType || activeElement.type"
:dimensions="selectionBounds"
:style="{ pointerEvents: 'auto' }"
/>
Expand Down Expand Up @@ -50,6 +50,8 @@ let longpressDuration = 200
let mousedownStart

const outline = computed(() => {
if (activeElement.value?.shapeType == 'line') return 'none'

if (activeElementIds.value.length == 1) return `#70B6F0 solid ${2 / slideBounds.scale}px`
return `#70B6F092 solid ${0.1 / slideBounds.scale}px`
})
Expand Down
74 changes: 74 additions & 0 deletions frontend/src/components/ShapeElement.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<template>
<svg :style="shapeStyle">
<rect
v-if="element.shapeType == 'rectangle'"
x="0"
y="0"
:width="'100%'"
:height="'100%'"
:fill="element.fillColor"
:stroke="element.strokeColor"
:stroke-width="`${element.strokeWidth}px`"
:rx="element.borderRadius"
:ry="element.borderRadius"
/>

<ellipse
v-else-if="element.shapeType == 'circle'"
cx="50%"
cy="50%"
:rx="'calc(50% - ' + element.strokeWidth / 2 + 'px)'"
:ry="'calc(50% - ' + element.strokeWidth / 2 + 'px)'"
:fill="element.fillColor"
:stroke="element.strokeColor"
:stroke-width="`${element.strokeWidth}px`"
/>

<line
v-else-if="element.shapeType == 'line'"
:x1="0"
:x2="element.width"
:y1="element.strokeWidth / 2"
:y2="element.strokeWidth / 2"
:stroke="`${element.strokeColor}`"
:stroke-width="`${element.strokeWidth}px`"
/>
</svg>
</template>

<script setup>
import { computed } from 'vue'
import { activeElementIds } from '@/stores/element'

const props = defineProps({
transitionStyles: {
type: Object,
default: () => ({}),
},
elementOffset: {
type: Object,
default: () => ({ left: 0, top: 0 }),
},
})

const element = defineModel('element', {
type: Object,
default: null,
})

const isActive = computed(() => {
return activeElementIds.value.includes(element.value.id)
})

const shapeStyle = computed(() => {
const styles = {
width: '100%',
height: '100%',
opacity: element.value.opacity / 100,
}
return {
...styles,
...props.transitionStyles,
}
})
</script>
53 changes: 53 additions & 0 deletions frontend/src/components/ShapeProperties.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<template>
<CollapsibleSection title="Style">
<template #default>
<div class="flex items-center justify-between">
<div :class="fieldLabelClasses">Fill Color</div>
<ColorPicker class="pe-[0.2px]" v-model="activeElement.fillColor" />
</div>

<div
class="flex items-center justify-between"
v-if="activeElement.shapeType == 'rectangle'"
>
<div :class="fieldLabelClasses">Border Radius</div>
<div class="w-28">
<NumberInput
v-model="activeElement.borderRadius"
suffix="px"
:rangeStart="0"
:rangeEnd="50"
:rangeStep="0.5"
/>
</div>
</div>

<div class="flex items-center justify-between">
<div :class="fieldLabelClasses">Stroke Width</div>
<div class="w-28">
<NumberInput
v-model="activeElement.strokeWidth"
suffix="px"
:rangeStart="0"
:rangeEnd="50"
:rangeStep="0.5"
/>
</div>
</div>

<div class="flex items-center justify-between">
<div :class="fieldLabelClasses">Stroke Color</div>
<ColorPicker class="pe-[0.2px]" v-model="activeElement.strokeColor" />
</div>
</template>
</CollapsibleSection>
</template>

<script setup>
import CollapsibleSection from '@/components/controls/CollapsibleSection.vue'
import ColorPicker from '@/components/controls/ColorPicker.vue'
import NumberInput from '@/components/controls/NumberInput.vue'

import { activeElement } from '@/stores/element'
import { fieldLabelClasses } from '@/utils/constants'
</script>
Loading