Skip to content

OffscreenCanvas support and Blob creation #93

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
7 changes: 6 additions & 1 deletion bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
24 changes: 24 additions & 0 deletions src/Graphics/Canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
44 changes: 43 additions & 1 deletion src/Graphics/Canvas.purs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module Graphics.Canvas
, RadialGradient
, QuadraticCurve
, BezierCurve
, BlobFormat(..)

, getCanvasElementById
, getContext2D
Expand Down Expand Up @@ -99,6 +100,11 @@ module Graphics.Canvas
, imageDataHeight
, imageDataBuffer

, toBlob
, toBlob'
, blobPNG
, blobJPEG

, canvasElementToImageSource
, drawImage
, drawImageScale
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions src/Graphics/Canvas/Offscreen.js
Original file line number Diff line number Diff line change
@@ -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");
};
}
68 changes: 68 additions & 0 deletions src/Graphics/Canvas/Offscreen.purs
Original file line number Diff line number Diff line change
@@ -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!