Passed
Push — main ( 1b309e...40b6fc )
by Dylan
04:20
created

src/canvas/index.tsx   A

Complexity

Total Complexity 12
Complexity/F 0

Size

Lines of Code 152
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 12
eloc 117
mnd 12
bc 12
fnc 0
dl 0
loc 152
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import preact, { FunctionalComponent, createRef } from 'preact'
2
import { useEffect } from 'preact/hooks'
3
4
type GetContextFunction = (canvas: HTMLCanvasElement) => CanvasRenderingContext2D
5
6
type InitFunction = (ctx: CanvasRenderingContext2D) => void
7
8
type ReadyFunction = (whenReady: VoidFunction) => void
9
10
type DrawFunction = (ctx: CanvasRenderingContext2D, frameCount: number) => void
11
12
type ResizeFunction = (ctx: CanvasRenderingContext2D) => void
13
14
type RenderFunction = () => void
15
16
export interface CanvasOptions {
17
  contextType?: string;
18
  framesPerSecond?: number;
19
}
20
21
export interface CanvasMethods {
22
  render: RenderFunction;
23
}
24
25
export interface CanvasProps {
26
  className?: string;
27
  getContext?: GetContextFunction;
28
  init?: InitFunction;
29
  ready?: ReadyFunction;
30
  draw: DrawFunction;
31
  onResize?: ResizeFunction;
32
  animate?: boolean;
33
  framesPerSecond?: number;
34
  options?: CanvasOptions;
35
  canvasMethodRefs?: CanvasMethods;
36
}
37
38
export const Canvas: FunctionalComponent<CanvasProps> = (props: CanvasProps) => {
39
  const { getContext, init, ready, draw, onResize, animate, framesPerSecond, className, ...rest } = props
40
  const frameMilliseconds = framesPerSecond ? 1000 / framesPerSecond : undefined
41
  const ref = createRef()
42
  let paused = false
43
  let frame = 0
44
  
45
  // Pause animation when window is not focused
46
  useEffect(() => {
47
    const handleBlur = (): void => {
48
      paused = true
49
    }
50
    window.addEventListener('blur', handleBlur)
51
    const handleFocus = (): void => {
52
      paused = false
53
    }
54
    window.addEventListener('focus', handleFocus)
55
    return (): void => {
56
      window.removeEventListener('blur', handleBlur)
57
      window.removeEventListener('focus', handleFocus)
58
    }
59
  }, [])
60
61
  // Update canvas dimensions when resized
62
  useEffect(() => {
63
    const canvas = ref.current as HTMLCanvasElement
64
    const ctx = getContext ? getContext(canvas) : canvas.getContext('2d') as CanvasRenderingContext2D
65
    const container = ctx.canvas.parentNode as HTMLElement
66
    const handleResize = (): void => {
67
      ctx.canvas.width = container.clientWidth
68
      ctx.canvas.height = container.clientHeight
69
      if (onResize) onResize(ctx)
70
    }
71
    const observer = new ResizeObserver(handleResize)
72
    observer.observe(container)
73
    handleResize()
74
    return (): void => {
75
      observer.disconnect()
76
    }
77
  }, [ref])
78
79
  // Set fullscreen on double click
80
  useEffect(() => {
81
    const setFullscreen = (): void => {
82
      if (!document.fullscreenElement) {
83
        document.body.requestFullscreen().catch(err => {
84
          console.error('Fullscreen fail', err)
85
        })
86
      }
87
    }
88
    window.addEventListener('dblclick', setFullscreen)
89
    return (): void => {
90
      window.removeEventListener('dblclick', setFullscreen)
91
    }
92
  }, [ref])
93
94
  // Start render loop
95
  useEffect(() => {
96
    const canvas = ref.current as HTMLCanvasElement
97
    const ctx = getContext ? getContext(canvas) : canvas.getContext('2d') as CanvasRenderingContext2D
98
    let loopCallbackID: number
99
100
    if (init) init(ctx)
101
102
    const render = (): void => {
103
      draw(ctx, frame++)
104
    }
105
106
    const loop = (): void => {
107
      if (paused) {
108
        loopCallbackID = window.setTimeout(loop, 128)
109
        return
110
      }
111
      if (frameMilliseconds) {
112
        loopCallbackID = window.setTimeout(loop, frameMilliseconds)
113
      }
114
      else {
115
        loopCallbackID = requestAnimationFrame(loop)
116
      }
117
      draw(ctx, frame++)
118
    }
119
    
120
    // expose methods to parent
121
    // @todo seems parents calling their children's methods is react antipattern, better way?
122
    if (props.canvasMethodRefs) {
123
      props.canvasMethodRefs.render = render
124
    }
125
126
    const whenReady = (): void => {
127
      if (animate===false) {
128
        loopCallbackID = window.setTimeout(render)
129
      }
130
      else {
131
        loopCallbackID = window.setTimeout(loop)
132
      }
133
    }
134
135
    if (ready===undefined) whenReady()
136
    else ready(whenReady)
137
138
    return (): void => {
139
      if (frameMilliseconds) {
140
        window.clearTimeout(loopCallbackID)
141
      }
142
      else {
143
        cancelAnimationFrame(loopCallbackID)
144
      }
145
    }
146
  }, [ref])
147
148
  return <canvas ref={ref} {...rest} />
149
}
150
151
export default Canvas
152