threepipe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

canvas-snapshot.ts 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import {now} from 'ts-browser-helpers'
  2. export interface CanvasSnapshotRect {
  3. height: number;
  4. width: number;
  5. x: number;
  6. y: number;
  7. /**
  8. * Use if canvas.width !== canvas.clientWidth or height and rect is based on client rect
  9. * @default false
  10. */
  11. assumeClientRect?: boolean;
  12. /**
  13. * If true, assumes x, y, width, height are normalized to 0-1
  14. * @default false
  15. */
  16. normalized?: boolean;
  17. }
  18. export interface CanvasSnapshotOptions {
  19. getDataUrl?: boolean,
  20. mimeType?: string,
  21. quality?: number, // between 0 and 1, only for image/jpeg or image/webp
  22. /**
  23. * Crop Region to take snapshot. If not set, the whole canvas is used.
  24. */
  25. rect?: CanvasSnapshotRect,
  26. scale?: number,
  27. displayPixelRatio?: number,
  28. cloneCanvas?: boolean, // default = true if safari, false otherwise. required for safari where canvas is flipped if premultipliedAlpha is true
  29. }
  30. function isSafari() {
  31. return navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome')
  32. }
  33. export class CanvasSnapshot {
  34. public static Debug = false
  35. public static async GetClonedCanvas(
  36. canvas: HTMLCanvasElement,
  37. {
  38. rect = {x: 0, y: 0, width: canvas.width, height: canvas.height, assumeClientRect: false, normalized: false},
  39. displayPixelRatio = 1,
  40. scale = 1,
  41. }: CanvasSnapshotOptions): Promise<HTMLCanvasElement> {
  42. rect = {...rect}
  43. // return canvas.toDataURL(mimeType);
  44. // in Safari, images are flipped when premultipliedAlpha is true in canvas, so it works with 2d context, see: https://github.com/pixijs/pixi.js/blob/dev/packages/extract/src/Extract.ts and https://github.com/pixijs/pixi.js/issues/2951
  45. const destCanvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas') as HTMLCanvasElement
  46. // const iRect = {...rect}
  47. if (!rect.normalized) {
  48. if (rect.assumeClientRect) {
  49. rect.x = Math.floor(rect.x * canvas.width / (displayPixelRatio * canvas.clientWidth))
  50. rect.y = Math.floor(rect.y * canvas.height / (displayPixelRatio * canvas.clientHeight))
  51. rect.width = Math.floor(rect.width * canvas.width / (displayPixelRatio * canvas.clientWidth))
  52. rect.height = Math.floor(rect.height * canvas.height / (displayPixelRatio * canvas.clientHeight))
  53. }
  54. } else {
  55. rect.x = Math.floor(rect.x * canvas.width)
  56. rect.y = Math.floor(rect.y * canvas.height)
  57. rect.width = Math.floor(rect.width * canvas.width)
  58. rect.height = Math.floor(rect.height * canvas.height)
  59. if (rect.assumeClientRect) {
  60. console.warn('CanvasSnapshot: rect.assumeClientRect is ignored when rect is normalized')
  61. }
  62. }
  63. destCanvas.width = Math.floor(rect.width * scale * displayPixelRatio)
  64. destCanvas.height = Math.floor(rect.height * scale * displayPixelRatio)
  65. const destCtx = destCanvas.getContext('2d')
  66. if (!destCtx) {
  67. console.error('snapshot: cannot create context')
  68. return destCanvas
  69. }
  70. // console.log(canvas.style.background)
  71. const background = canvas.style.background || canvas.parentElement?.style.background || ''
  72. if (background.includes('url')) {
  73. const url = /url\("(.*)"\)/ig.exec(background)?.[1]
  74. if (url) {
  75. const img = new Image()
  76. img.src = url
  77. await new Promise<void>((resolve, reject) => {
  78. img.onload = () => resolve()
  79. img.onerror = () => reject()
  80. if (img.complete) resolve()
  81. })
  82. destCtx.drawImage(img,
  83. Math.floor(img.width * rect.x * displayPixelRatio / canvas.width), Math.floor(img.height * rect.y * displayPixelRatio / canvas.height),
  84. Math.floor(img.width * rect.width * displayPixelRatio / canvas.width), Math.floor(img.height * rect.height * displayPixelRatio / canvas.height),
  85. 0, 0,
  86. destCanvas.width,
  87. destCanvas.height,
  88. )
  89. }
  90. } else {
  91. destCtx.fillStyle = canvas.style.background || canvas.parentElement?.style.backgroundColor || '#00000000'
  92. destCtx.fillRect(0, 0, destCanvas.width, destCanvas.height)
  93. }
  94. destCtx?.drawImage(
  95. canvas,
  96. Math.floor(rect.x * displayPixelRatio), Math.floor(rect.y * displayPixelRatio), Math.floor(rect.width * displayPixelRatio), Math.floor(rect.height * displayPixelRatio),
  97. 0, 0, destCanvas.width, destCanvas.height,
  98. )
  99. const debug = this.Debug
  100. if (debug) {
  101. // console.log(
  102. // destCanvas,
  103. // )
  104. document.body.appendChild(destCanvas)
  105. destCanvas.style.position = 'absolute'
  106. destCanvas.style.top = '0'
  107. destCanvas.style.left = '0'
  108. destCanvas.style.borderWidth = '2px'
  109. destCanvas.style.borderColor = '#ff00ff'
  110. setTimeout(() => destCanvas.remove(), 5000)
  111. }
  112. return destCanvas
  113. }
  114. public static async GetDataUrl(canvas: HTMLCanvasElement, {mimeType = 'image/png', quality, ...options}: CanvasSnapshotOptions): Promise<string> {
  115. const doClone = isSafari() || options.cloneCanvas || options.rect || options.scale || options.displayPixelRatio
  116. if (!doClone && (options.rect || options.scale || options.displayPixelRatio)) console.warn('CanvasSnapshot: rect, scale and displayPixelRatio are ignored when cloneCanvas is false')
  117. const clone = !doClone ? canvas : await this.GetClonedCanvas(canvas, options)
  118. // const clone = options.cloneCanvas === false ? canvas : await this.GetClonedCanvas(canvas, options)
  119. const url = clone.toDataURL(mimeType, quality)
  120. if (!this.Debug && clone !== canvas) clone.remove()
  121. return url
  122. }
  123. // set one of canvas or context to draw in.
  124. public static async GetImage(canvas: HTMLCanvasElement, options: CanvasSnapshotOptions = {}): Promise<HTMLImageElement> {
  125. const imgUrl = await this.GetDataUrl(canvas, options)
  126. return new Promise<HTMLImageElement>((resolve, reject) => {
  127. const img = new Image()
  128. img.onload = () => resolve(img)
  129. img.onerror = () => reject()
  130. img.src = imgUrl
  131. })
  132. }
  133. public static async GetBlob(canvas: HTMLCanvasElement, options: CanvasSnapshotOptions = {}): Promise<Blob> {
  134. const doClone = isSafari() || options.cloneCanvas || options.rect || options.scale || options.displayPixelRatio
  135. if (!doClone && (options.rect || options.scale || options.displayPixelRatio)) console.warn('rect, scale and displayPixelRatio are ignored when cloneCanvas is false')
  136. const clone = !doClone ? canvas : await this.GetClonedCanvas(canvas, options)
  137. // const clone = options.cloneCanvas === false ? canvas : await this.GetClonedCanvas(canvas, options)
  138. const blob = await new Promise<Blob>((resolve, reject) => {
  139. clone.toBlob((b) => {
  140. if (b) resolve(b)
  141. else reject(new Error('CanvasSnapshot Failed to export blob from canvas'))
  142. }, options.mimeType ?? 'image/png', options.quality)
  143. })
  144. if (!this.Debug && clone !== canvas) clone.remove()
  145. return blob
  146. }
  147. public static async GetFile(canvas: HTMLCanvasElement, filename = 'image', options: CanvasSnapshotOptions = {}): Promise<File|string> {
  148. const suffix = '.' + (options.mimeType?.split('/')[1]?.toLowerCase() || 'png')
  149. const fname = !filename.toLowerCase().endsWith(suffix) ? filename + suffix : filename
  150. return options.getDataUrl ? await this.GetDataUrl(canvas, options) : new File([await this.GetBlob(canvas, options)], fname, {
  151. type: options.mimeType ?? 'image/png',
  152. lastModified: now(),
  153. })
  154. }
  155. public static async GetTiledFiles(canvas: HTMLCanvasElement, filePrefix = 'image', tileRows = 2, tileCols = 2, options: CanvasSnapshotOptions = {}): Promise<(File|string)[]> {
  156. const rect = options.rect ?? {x: 0, y: 0, width: 1, height: 1, assumeClientRect: false, normalized: true}
  157. // rect.width *= options.displayPixelRatio ?? 1
  158. // rect.height *= options.displayPixelRatio ?? 1
  159. const files = []
  160. for (let i = 0; i < tileCols; i++) {
  161. for (let j = 0; j < tileRows; j++) {
  162. const ext = options.mimeType?.split('/')[1] ?? 'png'
  163. const file = await this.GetFile(canvas, `${filePrefix}_${i}_${j}.${ext}`, {
  164. rect: {
  165. x: rect.x + i * rect.width / tileCols,
  166. y: rect.y + j * rect.height / tileRows,
  167. width: rect.width / tileCols,
  168. height: rect.height / tileRows,
  169. assumeClientRect: rect.assumeClientRect,
  170. normalized: rect.normalized,
  171. },
  172. }).catch(e => {
  173. console.error(`CanvasSnapshot - Error exporting tiled file ${i}, ${j}`, e)
  174. return null
  175. })
  176. if (file)
  177. files.push(file)
  178. }
  179. }
  180. return files
  181. }
  182. }