Skip to content

Commit

Permalink
feat: basic animation
Browse files Browse the repository at this point in the history
  • Loading branch information
devlzl committed Feb 2, 2024
1 parent 6f02e29 commit a96590d
Show file tree
Hide file tree
Showing 19 changed files with 401 additions and 33 deletions.
1 change: 1 addition & 0 deletions src/Const/slide.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const DEFAULT_SLIDE_WIDTH = 960
export const DEFAULT_SLIDE_HEIGHT = 540
export const SLIDE_RATIO = DEFAULT_SLIDE_WIDTH / DEFAULT_SLIDE_HEIGHT
14 changes: 14 additions & 0 deletions src/Kernel/Animation/Animation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { type Block } from '@BlockHub/Block/Block'

export class Animation {
constructor(
public type: 'Entrance' | 'Emphasis' | 'Exit',
public name: string,
public block: Block,
public propName: string,
public startValue: any,
public endValue: any,
public duration: number,
public delay: number
) {}
}
62 changes: 62 additions & 0 deletions src/Kernel/Animation/Timeline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { BasicPropName } from '@BlockHub/Block/Block'
import { Animation } from './Animation'

type TimelineState = 'initial' | 'preview' | 'running'

export class Timeline {
private _state: TimelineState = 'initial'
private _startTime = 0
private _animations: Set<Animation> = new Set()
private _previewAnimations: Set<Animation> = new Set()
private _runningAnimations: Set<Animation> = new Set()

private _tick() {
const animations = this._state === 'running' ? this._runningAnimations : this._previewAnimations
const elapsedTime = Date.now() - this._startTime
for (const animation of animations) {
const { block, propName, startValue, endValue, duration, delay } = animation
let progress
if (elapsedTime < delay) {
progress = 0
} else if (elapsedTime < delay + duration) {
// time progress -> effect progress
progress = (elapsedTime - delay) / duration
} else {
progress = 1
animations.delete(animation)
}
block[propName as BasicPropName] = startValue + (endValue - startValue) * progress
}
if (animations.size > 0) {
requestAnimationFrame(this._tick.bind(this))
} else {
this._state = 'initial'
}
}

get animations() {
return this._animations
}

add(animation: Animation) {
this._animations.add(animation)
}

delete(animation: Animation) {
this._animations.delete(animation)
}

preview() {
this._state = 'preview'
this._previewAnimations = new Set([...this._animations].slice(-1))
this._startTime = Date.now()
this._tick()
}

start() {
this._state = 'running'
this._runningAnimations = new Set([...this._animations])
this._startTime = Date.now()
this._tick()
}
}
30 changes: 29 additions & 1 deletion src/Kernel/Slide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@ import { type Block } from '@BlockHub/Block/Block'
import { EventManager } from './EventManager'
import { Command } from '@Kernel/HistoryManager'
import { history } from '@Kernel/index'
import { Timeline } from './Animation/Timeline'
import { Animation } from './Animation/Animation'

export class Slide {
static id = 0

private _id: number
private _blocks: Array<Block>
private _timeline: Timeline

events = {
blockChange: new EventManager(),
animationChange: new EventManager(),
}

constructor(blocks: Array<Block> = []) {
this._id = Slide.id++
this._blocks = blocks
this._timeline = new Timeline()
}

get id() {
Expand Down Expand Up @@ -43,7 +48,7 @@ export class Slide {
addBlock(block: Block) {
const command = new Command(
() => this._addBlock(block),
() => this._removeBlock(),
() => this._removeBlock()
)
history.exec(command)
}
Expand All @@ -55,4 +60,27 @@ export class Slide {
)
history.exec(command)
}

// Animation
get animations() {
return this._timeline.animations
}

addAnimation(animation: Animation) {
this._timeline.add(animation)
this.events.animationChange.emit()
}

deleteAnimation(animation: Animation) {
this._timeline.delete(animation)
this.events.animationChange.emit()
}

previewAnimation() {
this._timeline.preview()
}

startAnimation() {
this._timeline.start()
}
}
59 changes: 59 additions & 0 deletions src/UserInterface/AnimationPane/AnimationPane.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script setup lang="ts">
import { slideManager } from '@Kernel/index'
import { triggerRef } from 'vue'
import { shallowRef } from 'vue'
import { Delete } from '@icon-park/vue-next'
const slide = shallowRef(slideManager.currentSlide)
const animations = shallowRef(slide.value.animations)
const updateAnimations = () => {
slide.value.events.animationChange.on(() => {
animations.value = slide.value.animations
triggerRef(animations)
})
}
slideManager.events.update.on(() => {
slide.value = slideManager.currentSlide
updateAnimations()
})
updateAnimations()
</script>

<template>
<div
class="w-[250px] flex-none flex flex-col gap-[6px] items-center border-l border-secondary-border bg-secondary"
v-if="animations.size"
>
<div class="text-base pt-[10px] pb-[6px]">Animation</div>
<div v-for="animation of animations" class="flex flex-col gap-[2px] w-[220px] p-[10px] shadow bg-white">
<div class="flex justify-between items-center">
<span
:style="{
color: {
Entrance: '#309048FF',
Emphasis: '#DE6C00FF',
Exit: '#ED3D3BFF',
}[animation.type],
}"
>{{ animation.type }}: {{ animation.name }}</span
>
<Delete
theme="outline"
size="24"
fill="#333"
:strokeWidth="2"
class="menu-btn"
@click="slide.deleteAnimation(animation)"
/>
</div>
<div class="flex justify-between">
<span>Duration:</span><input type="number" class="border w-[90px]" v-model="animation.duration" />
</div>
<div class="flex justify-between">
<span>Delay:</span><input type="number" class="border w-[90px]" v-model="animation.delay" />
</div>
</div>
</div>
</template>
2 changes: 2 additions & 0 deletions src/UserInterface/Present/Present.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import StatusBar from '../StatusBar/StatusBar.vue'
import Show from '../Show/Show.vue'
import { type SlideMode, slideShowMode, history } from '@Kernel/index'
import { ref, onMounted } from 'vue'
import AnimationPane from '../AnimationPane/AnimationPane.vue'
const currentMode = ref<SlideMode>('edit')
slideShowMode.on(async (mode) => {
Expand Down Expand Up @@ -33,6 +34,7 @@ onMounted(() => {
<div class="flex flex-auto slide-area">
<SlideList />
<SlideContainer />
<AnimationPane />
</div>
<StatusBar />
</div>
Expand Down
62 changes: 33 additions & 29 deletions src/UserInterface/Show/Show.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<script setup lang="ts">
import { type SlideMode, slideManager } from '@Kernel/index'
import { BlockViews } from '@BlockHub/BlockHub'
import { onMounted, onUnmounted, shallowRef } from 'vue'
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
import { Logout, ArrowCircleLeft, ArrowCircleRight } from '@icon-park/vue-next'
import Block from './components/Block.vue'
import { DEFAULT_SLIDE_WIDTH, SLIDE_RATIO } from '@Const/slide'
const { mode } = defineProps<{
mode: SlideMode
Expand All @@ -14,8 +15,9 @@ onMounted(() => {
slideManager.changeSlide(0)
}
})
slideManager.events.update.on(() => {
slideManager.events.update.on(async () => {
slide.value = slideManager.currentSlide
slide.value.startAnimation()
})
const onKeyDown = (event: KeyboardEvent) => {
Expand All @@ -36,35 +38,37 @@ onUnmounted(() => {
const quit = async () => {
await document.exitFullscreen()
}
const height = ref(window.innerWidth / SLIDE_RATIO)
const scale = ref(window.innerWidth / DEFAULT_SLIDE_WIDTH)
const calcSize = () => {
height.value = window.innerWidth / SLIDE_RATIO
scale.value = window.innerWidth / DEFAULT_SLIDE_WIDTH
}
onMounted(() => {
window.addEventListener('resize', calcSize)
})
onUnmounted(() => {
window.removeEventListener('resize', calcSize)
})
</script>

<template>
<div class="relative w-screen h-screen" @click="slideManager.goNext">
<component
v-for="block of slide.blocks"
:key="block.id"
:is="BlockViews[block.type]"
:block="block"
:style="{
position: 'absolute',
left: `${block.x}px`,
top: `${block.y}px`,
width: `${block.width}px`,
height: `${block.height}px`,
rotate: `${block.rotate}deg`,
}"
></component>
<div class="absolute w-full h-full left-0 top-0 opacity-0"></div>
<div class="absolute left-[10px] bottom-[10px] flex gap-[10px]">
<button class="menu-btn" @click.stop="slideManager.goPrevious">
<ArrowCircleLeft theme="two-tone" size="36" :fill="['#333', '#ffffff']" />
</button>
<button class="menu-btn" @click.stop="slideManager.goNext">
<ArrowCircleRight theme="two-tone" size="36" :fill="['#333', '#ffffff']" />
</button>
<button class="menu-btn" @click.stop="quit">
<Logout theme="two-tone" size="36" :fill="['#333', '#ffffff']" />
</button>
<div class="relative w-screen h-screen bg-black flex items-center" @click="slideManager.goNext">
<div class="relative bg-white w-screen overflow-hidden" :style="{ height: `${height}px` }">
<Block v-for="block of slide.blocks" :block="block" :style="{ scale }" />
<div class="absolute w-full h-full left-0 top-0 opacity-0"></div>
<div class="absolute left-[10px] bottom-[10px] flex gap-[10px]">
<button class="menu-btn" @click.stop="slideManager.goPrevious">
<ArrowCircleLeft theme="two-tone" size="36" :fill="['#333', '#ffffff']" />
</button>
<button class="menu-btn" @click.stop="slideManager.goNext">
<ArrowCircleRight theme="two-tone" size="36" :fill="['#333', '#ffffff']" />
</button>
<button class="menu-btn" @click.stop="quit">
<Logout theme="two-tone" size="36" :fill="['#333', '#ffffff']" />
</button>
</div>
</div>
</div>
</template>
34 changes: 34 additions & 0 deletions src/UserInterface/Show/components/Block.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script setup lang="ts">
import { BasicPropName, type Block } from '@BlockHub/Block/Block'
import { BlockViews } from '@BlockHub/BlockHub'
import { ref } from 'vue'
const { block } = defineProps<{
block: Block
}>()
const { x, y, width, height, rotate } = block
const props = ref({ x, y, width, height, rotate })
block.props.events.update.on(({ key, to }) => {
if (['x', 'y', 'width', 'height', 'rotate'].includes(key)) {
const name = key as BasicPropName
props.value[name] = to as number
}
})
</script>

<template>
<component
:key="block.id"
:is="BlockViews[block.type]"
:block="block"
:style="{
position: 'absolute',
left: `${props.x}px`,
top: `${props.y}px`,
width: `${props.width}px`,
height: `${props.height}px`,
rotate: `${props.rotate}deg`,
}"
></component>
</template>
2 changes: 1 addition & 1 deletion src/UserInterface/Slide/Slide.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ toolBox.events.toolChange.on(() => {
<template>
<div
ref="slideRef"
class="relative bg-white shadow-lg"
class="relative bg-white shadow-lg overflow-hidden"
:style="{
width: `${DEFAULT_SLIDE_WIDTH}px`,
height: `${DEFAULT_SLIDE_HEIGHT}px`,
Expand Down
2 changes: 2 additions & 0 deletions src/UserInterface/ToolBar/ToolBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Home from './components/Home/Home.vue'
import Insert from './components/Insert/Insert.vue'
import Draw from './components/Draw/Draw.vue'
import SlideShow from './components/SlideShow/SlideShow.vue'
import Animations from './components/Animations/Animations.vue'
import { TOOL_TABS } from './const'
const activeTab = ref('Home')
Expand All @@ -18,6 +19,7 @@ const components: { [key: string]: Component } = {
Insert,
Draw,
SlideShow,
Animations,
}
</script>

Expand Down
11 changes: 11 additions & 0 deletions src/UserInterface/ToolBar/components/Animations/Animations.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script setup lang="ts">
import Entrance from './components/Entrance.vue'
import Emphasis from './components/Emphasis.vue'
import Exit from './components/Exit.vue'
</script>

<template>
<Entrance />
<Emphasis />
<Exit />
</template>
Loading

0 comments on commit a96590d

Please sign in to comment.