diff --git a/.eslintrc.json b/.eslintrc.json index 3a97d05..ebe28de 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,6 +20,10 @@ "no-param-reassign": 2, "no-return-assign": 2, "no-unused-expressions": 2, + "no-unused-vars": [ + "error", + { "argsIgnorePattern": "^_" } + ], "no-use-before-define": 2, "radix": [2, "always"], "indent": [2, 2], diff --git a/CHANGELOG.md b/CHANGELOG.md index 522e284..9279848 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Breaking changes: New features: - Added `imageSmoothingEnabled` to toggle the image smoothing of a `Context2D`. +- Added `OffscreenCanvas` and its interface. +- Added `toBlob`, `toBlob'`, and a helper `BlobFormat` type for the arguments to `toBlob'`. Bugfixes: diff --git a/bower.json b/bower.json index bb8505d..0bbb812 100644 --- a/bower.json +++ b/bower.json @@ -20,10 +20,15 @@ "url": "https://github.com/purescript-web/purescript-canvas.git" }, "dependencies": { + "purescript-aff": "^7.0.0", + "purescript-aff-promise": "^4.0.0", "purescript-arraybuffer-types": "^3.0.2", "purescript-effect": "^4.0.0", + "purescript-either": "^6.1.0", "purescript-exceptions": "^6.0.0", "purescript-functions": "^6.0.0", - "purescript-maybe": "^6.0.0" + "purescript-maybe": "^6.0.0", + "purescript-media-types": "^6.0.0", + "purescript-web-file": "^4.0.0" } } diff --git a/src/Graphics/Canvas.js b/src/Graphics/Canvas.js index 59c3dab..a363e9c 100644 --- a/src/Graphics/Canvas.js +++ b/src/Graphics/Canvas.js @@ -474,6 +474,30 @@ export function createImageDataWith(arr) { }; } +export function toBlobDefault(canvas) { + return function(cb) { + return function() { + return canvas.toBlob(cb); + }; + }; +} + +export function toBlobFormat(canvas, format) { + return function(cb) { + return function() { + return canvas.toBlob(cb, {type: format}); + }; + }; +} + +export function toBlobFormatQuality(canvas, format, quality) { + return function(cb) { + return function() { + return canvas.toBlob(cb, {type: format, quality: quality}); + }; + }; +} + export function drawImage(ctx) { return function(image_source) { return function(dx) { diff --git a/src/Graphics/Canvas.purs b/src/Graphics/Canvas.purs index 8e55bc4..4c578d7 100644 --- a/src/Graphics/Canvas.purs +++ b/src/Graphics/Canvas.purs @@ -25,6 +25,7 @@ module Graphics.Canvas , RadialGradient , QuadraticCurve , BezierCurve + , BlobFormat(..) , getCanvasElementById , getContext2D @@ -99,6 +100,11 @@ module Graphics.Canvas , imageDataHeight , imageDataBuffer + , toBlob + , toBlob' + , blobPNG + , blobJPEG + , canvasElementToImageSource , drawImage , drawImageScale @@ -121,8 +127,13 @@ import Prelude import Effect (Effect) import Effect.Exception.Unsafe (unsafeThrow) import Data.ArrayBuffer.Types (Uint8ClampedArray) -import Data.Function.Uncurried (Fn3, runFn3) +import Data.Either (Either(Right)) +import Data.Function.Uncurried (Fn2, Fn3, runFn2, runFn3) import Data.Maybe (Maybe(..)) +import Data.MediaType (MediaType(..)) +import Data.MediaType.Common (imagePNG, imageJPEG) +import Web.File.Blob (Blob) +import Effect.Aff (Aff, makeAff) -- | A canvas HTML element. foreign import data CanvasElement :: Type @@ -651,6 +662,37 @@ foreign import imageDataHeight :: ImageData -> Int -- | Get the underlying buffer from an `ImageData` object. foreign import imageDataBuffer :: ImageData -> Uint8ClampedArray +-- | Format arguments for `toBlob'`. +-- | Quality for lossy compression is given as a `Number` between `0.0` and `1.0`. +data BlobFormat = Lossless MediaType | Lossy MediaType Number + +-- Testing in my Firefox, these are the only two types supported, +-- with no support for `image/gif` or `image/svg+xml`... but is that standard? + +blobPNG :: BlobFormat +blobPNG = Lossless imagePNG + +blobJPEG :: Number -> BlobFormat +blobJPEG = Lossy imageJPEG + +-- Thanks to mikesol for letting me know about makeAff! +blobCBToAff :: forall a. ((a -> Effect Unit) -> Effect Unit) -> Aff a +blobCBToAff b = makeAff \cb -> b (cb <<< Right) $> mempty + +foreign import toBlobDefault :: CanvasElement -> (Blob -> Effect Unit) -> Effect Unit + +-- | Create a `Blob` of the image data on the canvas, as a PNG file. +toBlob :: CanvasElement -> Aff Blob +toBlob = blobCBToAff <<< toBlobDefault + +foreign import toBlobFormat :: Fn2 String CanvasElement ((Blob -> Effect Unit) -> Effect Unit) +foreign import toBlobFormatQuality :: Fn3 String Number CanvasElement ((Blob -> Effect Unit) -> Effect Unit) + +-- | Create a `Blob` of the image data on the canvas, in the specified format. +toBlob' :: BlobFormat -> CanvasElement -> Aff Blob +toBlob' (Lossless (MediaType format)) = blobCBToAff <<< runFn2 toBlobFormat format +toBlob' (Lossy (MediaType format) quality) = blobCBToAff <<< runFn3 toBlobFormatQuality format quality + foreign import drawImage :: Context2D -> CanvasImageSource -> Number -> Number -> Effect Unit foreign import drawImageScale :: Context2D -> CanvasImageSource -> Number -> Number -> Number -> Number -> Effect Unit diff --git a/src/Graphics/Canvas/Offscreen.js b/src/Graphics/Canvas/Offscreen.js new file mode 100644 index 0000000..b91f4b3 --- /dev/null +++ b/src/Graphics/Canvas/Offscreen.js @@ -0,0 +1,53 @@ +export function createOffscreenCanvas(dims) { + return function() { + return new OffscreenCanvas(dims.width, dims.height); + }; +} + +export function getHeight(offscreen) { + return function() { + return offscreen.height; + }; +} + +export function getWidth(offscreen) { + return function() { + return offscreen.width; + }; +} + +export function setHeightImpl(offscreen, h) { + return function() { + offscreen.height = h; + }; +} + +export function setWidthImpl(offscreen, w) { + return function() { + offscreen.width = w; + }; +} + +export function toBlobDefault(offscreen) { + return function() { + return offscreen.toBlob(); + }; +} + +export function toBlobFormat(offscreen, format) { + return function() { + return offscreen.toBlob({type: format}); + }; +} + +export function toBlobFormatQuality(offscreen, format, quality) { + return function() { + return offscreen.toBlob({type: format, quality: quality}); + }; +} + +export function getContext2D(offscreen) { + return function() { + return offscreen.getContext("2d"); + }; +} diff --git a/src/Graphics/Canvas/Offscreen.purs b/src/Graphics/Canvas/Offscreen.purs new file mode 100644 index 0000000..8448659 --- /dev/null +++ b/src/Graphics/Canvas/Offscreen.purs @@ -0,0 +1,68 @@ +module Graphics.Canvas.Offscreen + ( OffscreenCanvas + , createOffscreenCanvas + , getHeight + , getWidth + , setHeight + , setWidth + , toBlob + , toBlob' + , getContext2D + -- , toImageBitmap + ) where + +import Prelude + +import Effect (Effect) +import Web.File.Blob (Blob) +import Graphics.Canvas (Context2D, BlobFormat(..)) +import Data.MediaType (MediaType(..)) +import Data.Function.Uncurried (Fn2, Fn3, runFn2, runFn3) +import Effect.Aff (Aff) +import Control.Promise (Promise, toAffE) + +-- | An OffscreenCanvas object, representing a virtual canvas which does not exist +-- | in the document and will not be rendered. +foreign import data OffscreenCanvas :: Type + +-- | Create a virtual canvas with the given width and height. +foreign import createOffscreenCanvas :: { width :: Int, height :: Int } -> (Effect OffscreenCanvas) + +-- | Get the logical height in pixels of the virtual canvas. +foreign import getHeight :: OffscreenCanvas -> Effect Int + +-- | Get the logical width in pixels of the virtual canvas. +foreign import getWidth :: OffscreenCanvas -> Effect Int + +foreign import setHeightImpl :: Fn2 OffscreenCanvas Int (Effect Unit) +foreign import setWidthImpl :: Fn2 OffscreenCanvas Int (Effect Unit) + +-- | Set the logical height in pixels of the virtual canvas. +setHeight :: OffscreenCanvas -> Int -> Effect Unit +setHeight = runFn2 setHeightImpl + +-- | Set the logical width in pixels of the virtual canvas. +setWidth :: OffscreenCanvas -> Int -> Effect Unit +setWidth = runFn2 setWidthImpl + +foreign import toBlobDefault :: OffscreenCanvas -> Effect (Promise Blob) + +-- | Create a `Blob` of the image data on the virtual canvas, as a PNG file. +toBlob :: OffscreenCanvas -> Aff Blob +toBlob = toAffE <<< toBlobDefault + +foreign import toBlobFormat :: Fn2 String OffscreenCanvas (Effect (Promise Blob)) +foreign import toBlobFormatQuality :: Fn3 String Number OffscreenCanvas (Effect (Promise Blob)) + +-- | Create a `Blob` of the image data on the virtual canvas, in the specified format. +toBlob' :: BlobFormat -> OffscreenCanvas -> Aff Blob +toBlob' (Lossless (MediaType format)) = toAffE <<< runFn2 toBlobFormat format +toBlob' (Lossy (MediaType format) quality) = toAffE <<< runFn3 toBlobFormatQuality format quality + +-- | Get the 2D graphics context for a virtual canvas element. +-- | Although this is a different type (`OffscreenCanvasRenderingContext2D`) +-- | in the underlying JavaScript, `Context2D` does not expose any of the +-- | discrepancies in functionality. +foreign import getContext2D :: OffscreenCanvas -> Effect Context2D + +-- transferToImageBitmap not added because ImageBitmap seems like a very large can of worms!