pipipi-pikachu 5 роки тому
батько
коміт
636c27c37e

+ 10 - 2
src/App.vue

@@ -1,26 +1,34 @@
 <template>
-  <Editor />
+  <Editor v-if="!screening" />
+  <Screen v-else />
 </template>
 
 <script lang="ts">
-import { defineComponent, onMounted } from 'vue'
+import { computed, defineComponent, onMounted } from 'vue'
 import { useStore } from 'vuex'
 import { MutationTypes, ActionTypes, State } from '@/store'
 
 import Editor from './views/Editor/index.vue'
+import Screen from './views/Screen/index.vue'
 
 export default defineComponent({
   name: 'app',
   components: {
     Editor,
+    Screen,
   },
   setup() {
     const store = useStore<State>()
+    const screening = computed(() => store.state.screening)
 
     onMounted(() => {
       store.commit(MutationTypes.SET_AVAILABLE_FONTS)
       store.dispatch(ActionTypes.INIT_SNAPSHOT_DATABASE)
     })
+
+    return {
+      screening,
+    }
   },
 })
 </script>

+ 1 - 0
src/configs/hotkey.ts

@@ -6,6 +6,7 @@ export enum KEYS {
   A = 'A',
   G = 'G',
   L = 'L',
+  F = 'F',
   DELETE = 'DELETE',
   UP = 'ARROWUP',
   DOWN = 'ARROWDOWN',

+ 13 - 0
src/global.d.ts

@@ -0,0 +1,13 @@
+interface HTMLElement {
+  webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
+  mozRequestFullScreen(options?: FullscreenOptions): Promise<void>;
+}
+
+interface Document {
+  mozFullScreen: boolean;
+  webkitIsFullScreen: boolean;
+  webkitFullScreen: boolean;
+
+  mozCancelFullScreen(): Promise<void>;
+  webkitCancelFullScreen(): Promise<void>;
+}

+ 0 - 10
src/mocks/index.ts

@@ -16,7 +16,6 @@ export const slides: Slide[] = [
         width: 320,
         height: 104,
         rotate: 0,
-        fill: 'rgba(220, 220, 220, 0.8)',
         shadow: {
           h: 1,
           v: 1,
@@ -56,15 +55,6 @@ export const slides: Slide[] = [
         width: 150,
         height: 150,
         rotate: 0,
-        outline: {
-          width: 6,
-          style: 'solid',
-          color: '#333'
-        },
-        clip: {
-          range: [[0, 0], [100, 100]],
-          shape: 'roundRect'
-        },
         fixedRatio: true,
         lock: false,
         src: 'https://img.lessonplan.cn/IMG/Show/ppt/3ab74e91-c34f-499d-9711-166e423d4dd6/62d9adb3-e7a6-4dc4-a352-095cffb49f08/b1be1a2f-f893-47d3-a8a3-eac7d04d395f/1596159381259v2-b2c69096d25ae16bf6ca09e30add3e65_hd.jpg',

+ 3 - 0
src/store/constants.ts

@@ -27,6 +27,9 @@ export enum MutationTypes {
   // keyboard
   SET_CTRL_KEY_STATE = 'setCtrlKeyState',
   SET_SHIFT_KEY_STATE = 'setShiftKeyState',
+
+  // screen
+  SET_SCREENING = 'SET_SCREENING'
 }
 
 export enum ActionTypes {

+ 2 - 0
src/store/index.ts

@@ -26,6 +26,7 @@ export interface State {
   snapshotLength: number;
   ctrlKeyState: boolean;
   shiftKeyState: boolean;
+  screening: boolean;
 }
 
 const state: State = {
@@ -44,6 +45,7 @@ const state: State = {
   snapshotLength: 0,
   ctrlKeyState: false,
   shiftKeyState: false,
+  screening: false,
 }
 
 export default createStore({

+ 6 - 0
src/store/mutations.ts

@@ -120,4 +120,10 @@ export const mutations: MutationTree<State> = {
   [MutationTypes.SET_SHIFT_KEY_STATE](state, isActive: boolean) {
     state.shiftKeyState = isActive
   },
+
+  // screen
+
+  [MutationTypes.SET_SCREENING](state, screening) {
+    state.screening = screening
+  },
 }

+ 16 - 3
src/utils/fullscreen.ts

@@ -1,8 +1,21 @@
 // 进入全屏
-export const enterFullscreen = document.documentElement.requestFullscreen
+export const enterFullscreen = () => {
+  const docElm = document.documentElement
+  if(docElm.requestFullscreen) docElm.requestFullscreen() 
+  else if(docElm.mozRequestFullScreen) docElm.mozRequestFullScreen() 
+  else if(docElm.webkitRequestFullScreen) docElm.webkitRequestFullScreen()
+}
 
 // 退出全屏
-export const exitFullscreen = document.exitFullscreen
+export const exitFullscreen = () => {
+  if(document.exitFullscreen) document.exitFullscreen()
+  else if(document.mozCancelFullScreen) document.mozCancelFullScreen()
+  else if(document.webkitCancelFullScreen) document.webkitCancelFullScreen()
+}
 
 // 判断是否全屏
-export const isFullscreen = () => document.fullscreenEnabled
+export const isFullscreen = () => (
+  document.mozFullScreen ||                         
+  document.webkitIsFullScreen ||       
+  document.webkitFullScreen
+)

+ 11 - 0
src/views/Editor/useHotkey.ts

@@ -2,6 +2,7 @@ import { computed, onMounted, onUnmounted } from 'vue'
 import { useStore } from 'vuex'
 import { State, MutationTypes } from '@/store'
 import { KEYS } from '@/configs/hotkey'
+import { enterFullscreen } from '@/utils/fullscreen'
 
 import useSlideHandler from '@/hooks/useSlideHandler'
 import useLockElement from '@/hooks/useLockElement'
@@ -89,10 +90,20 @@ export default () => {
     createSlide()
   }
 
+  const enterScreening = () => {
+    enterFullscreen()
+    store.commit(MutationTypes.SET_SCREENING, true)
+  }
+
   const keydownListener = (e: KeyboardEvent) => {
     const { ctrlKey, shiftKey } = e
     const key = e.key.toUpperCase()
 
+    if(ctrlKey && key === KEYS.F) {
+      e.preventDefault()
+      enterScreening()
+    }
+
     if(ctrlKey && !ctrlKeyActive.value) store.commit(MutationTypes.SET_CTRL_KEY_STATE, true)
     if(shiftKey && !shiftKeyActive.value) store.commit(MutationTypes.SET_SHIFT_KEY_STATE, true)
     

+ 68 - 0
src/views/Screen/ScreenSlide.vue

@@ -0,0 +1,68 @@
+<template>
+  <div 
+    class="screen-slide"
+    :style="{
+      width: VIEWPORT_SIZE + 'px',
+      height: VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO + 'px',
+      transform: `scale(${scale})`,
+    }"
+  >
+    <div class="background" :style="{ ...backgroundStyle }"></div>
+    <BaseElement
+      v-for="(element, index) in slide.elements"
+      :key="element.id"
+      :elementInfo="element"
+      :elementIndex="index + 1"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, PropType, defineComponent } from 'vue'
+import { Slide } from '@/types/slides'
+import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
+import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
+
+import BaseElement from '@/views/_common/_element/BaseElement.vue'
+
+export default defineComponent({
+  name: 'screen-slide',
+  components: {
+    BaseElement,
+  },
+  props: {
+    slide: {
+      type: Object as PropType<Slide>,
+      required: true,
+    },
+    scale: {
+      type: Number,
+      required: true,
+    },
+  },
+  setup(props) {
+    const background = computed(() => props.slide.background)
+    const { backgroundStyle } = useSlideBackgroundStyle(background)
+
+    return {
+      backgroundStyle,
+      VIEWPORT_SIZE,
+      VIEWPORT_ASPECT_RATIO,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.screen-slide {
+  position: absolute;
+  top: 0;
+  left: 0;
+  transform-origin: 0 0;
+}
+.background {
+  background-position: center;
+  background-size: cover;
+  position: absolute;
+}
+</style>

+ 191 - 0
src/views/Screen/index.vue

@@ -0,0 +1,191 @@
+<template>
+  <div class="hamster-ppt-screen">
+    <div 
+      class="slide-list"
+      @mousewheel="$event => mousewheelListener($event)"
+      v-contextmenu="contextmenus"
+    >
+      <div 
+        :class="[
+          'slide-item', 
+          {
+            'show': index === slideIndex,
+            'prev': index < slideIndex,
+            'next': index > slideIndex,
+          }
+        ]" 
+        v-for="(slide, index) in slides" 
+        :key="slide.id"
+      >
+        <div 
+          class="slide-content" 
+          :style="{
+            width: slideWidth + 'px',
+            height: slideHeight + 'px',
+          }"
+        >
+          <ScreenSlide :scale="scale" :slide="slide" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, onMounted, onUnmounted, ref } from 'vue'
+import { useStore } from 'vuex'
+import throttle from 'lodash/throttle'
+import { MutationTypes, State } from '@/store'
+import { exitFullscreen, isFullscreen } from '@/utils/fullscreen'
+import { VIEWPORT_ASPECT_RATIO, VIEWPORT_SIZE } from '@/configs/canvas'
+import { KEYS } from '@/configs/hotkey'
+import { ContextmenuItem } from '@/components/Contextmenu/types'
+
+import ScreenSlide from './ScreenSlide.vue'
+
+export default defineComponent({
+  name: 'screen',
+  components: {
+    ScreenSlide,
+  },
+  setup() {
+    const store = useStore<State>()
+    const slides = computed(() => store.state.slides)
+    const slideIndex = computed(() => store.state.slideIndex)
+
+    const slideWidth = ref(0)
+    const slideHeight = ref(0)
+    const scale = computed(() => slideWidth.value / VIEWPORT_SIZE)
+
+    const setSlideContentSize = () => {
+      const winWidth = document.body.clientWidth
+      const winHeight = document.body.clientHeight
+      let width, height
+
+      if(winHeight / winWidth === VIEWPORT_ASPECT_RATIO) {
+        width = winWidth
+        height = winHeight
+      }
+      else if(winHeight / winWidth > VIEWPORT_ASPECT_RATIO) {
+        width = winWidth
+        height = winWidth * VIEWPORT_ASPECT_RATIO
+      }
+      else {
+        width = winHeight / VIEWPORT_ASPECT_RATIO
+        height = winHeight
+      }
+      slideWidth.value = width
+      slideHeight.value = height
+    }
+
+    const windowResizeListener = () => {
+      setSlideContentSize()
+      if(!isFullscreen()) store.commit(MutationTypes.SET_SCREENING, false)
+    }
+
+    const turnPrevSlide = () => {
+      if(slideIndex.value <= 0) return
+      store.commit(MutationTypes.UPDATE_SLIDE_INDEX, slideIndex.value - 1)
+    }
+    const turnNextSlide = () => {
+      if(slideIndex.value >= slides.value.length - 1) return
+      store.commit(MutationTypes.UPDATE_SLIDE_INDEX, slideIndex.value + 1)
+    }
+
+    const keydownListener = (e: KeyboardEvent) => {
+      const key = e.key.toUpperCase()
+      if(key === KEYS.UP || key === KEYS.LEFT) turnPrevSlide()
+      else if(key === KEYS.DOWN || key === KEYS.RIGHT) turnNextSlide()
+    }
+
+    const mousewheelListener = throttle(function(e: WheelEvent) {
+      if(e.deltaY > 0) turnNextSlide()
+      else if(e.deltaY < 0) turnPrevSlide()
+    }, 500, { leading: true, trailing: false })
+
+    onMounted(() => {
+      window.addEventListener('resize', windowResizeListener)
+      document.addEventListener('keydown', keydownListener)
+    })
+    onUnmounted(() => {
+      window.removeEventListener('resize', windowResizeListener)
+      document.removeEventListener('keydown', keydownListener)
+    })
+
+    const contextmenus = (): ContextmenuItem[] => {
+      return [
+        {
+          text: '上一页',
+          disable: slideIndex.value <= 0,
+          handler: () => turnPrevSlide(),
+        },
+        {
+          text: '下一页',
+          disable: slideIndex.value >= slides.value.length - 1,
+          handler: () => turnNextSlide(),
+        },
+        { divider: true },
+        {
+          text: '结束放映',
+          subText: 'ESC',
+          handler: exitFullscreen,
+        },
+      ]
+    }
+
+    return {
+      slides,
+      slideIndex,
+      slideWidth,
+      slideHeight,
+      scale,
+      mousewheelListener,
+      contextmenus,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.hamster-ppt-screen {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  background-color: #111;
+}
+.slide-list {
+  background: #1d1d1d;
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+.slide-item {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  transition-property: transform;
+  transition-duration: .4s;
+
+  &.show {
+    z-index: 2;
+  }
+  &.prev {
+    transform: translateX(-100%);
+  }
+  &.next {
+    transform: translateX(100%);
+  }
+}
+.slide-content {
+  background-color: #fff;
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+</style>

+ 0 - 13
src/views/SlideShow/index.vue

@@ -1,13 +0,0 @@
-<template>
-  <div class="hamster-ppt-slide-show">
-    slide-show
-  </div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue'
-
-export default defineComponent({
-  name: 'slide-show',
-})
-</script>