@shopify/react-native-skia
Version:
High-performance React Native Graphics using Skia
316 lines (311 loc) • 10.5 kB
JavaScript
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
/* global HTMLCanvasElement */
import React, { useRef, useEffect, useCallback, useImperativeHandle } from "react";
import { JsiSkSurface } from "../skia/web/JsiSkSurface";
import { Platform } from "../Platform";
import { SkiaViewNativeId } from "./SkiaViewNativeId";
const dp2Pixel = (pd, rect) => {
if (!rect) {
return undefined;
}
return {
x: rect.x * pd,
y: rect.y * pd,
width: rect.width * pd,
height: rect.height * pd
};
};
class WebGLRenderer {
constructor(canvas, pd) {
this.canvas = canvas;
this.pd = pd;
_defineProperty(this, "surface", null);
this.onResize();
}
makeImageSnapshot(picture, rect) {
if (!this.surface) {
return null;
}
const canvas = this.surface.getCanvas();
canvas.clear(CanvasKit.TRANSPARENT);
this.draw(picture);
this.surface.ref.flush();
return this.surface.makeImageSnapshot(dp2Pixel(this.pd, rect));
}
onResize() {
const {
canvas,
pd
} = this;
canvas.width = canvas.clientWidth * pd;
canvas.height = canvas.clientHeight * pd;
const surface = CanvasKit.MakeWebGLCanvasSurface(canvas);
const ctx = canvas.getContext("webgl2");
if (ctx) {
ctx.drawingBufferColorSpace = "display-p3";
}
if (!surface) {
throw new Error("Could not create surface");
}
this.surface = new JsiSkSurface(CanvasKit, surface);
}
draw(picture) {
if (this.surface) {
const canvas = this.surface.getCanvas();
canvas.clear(Float32Array.of(0, 0, 0, 0));
canvas.save();
canvas.scale(pd, pd);
canvas.drawPicture(picture);
canvas.restore();
this.surface.ref.flush();
}
}
dispose() {
if (this.surface) {
var _this$canvas;
(_this$canvas = this.canvas) === null || _this$canvas === void 0 || (_this$canvas = _this$canvas.getContext("webgl2")) === null || _this$canvas === void 0 || (_this$canvas = _this$canvas.getExtension("WEBGL_lose_context")) === null || _this$canvas === void 0 || _this$canvas.loseContext();
this.surface.ref.delete();
this.surface = null;
}
}
}
class StaticWebGLRenderer {
constructor(canvas, pd) {
this.canvas = canvas;
this.pd = pd;
_defineProperty(this, "cachedImage", null);
}
onResize() {
this.cachedImage = null;
}
renderPictureToSurface(picture) {
const tempCanvas = new OffscreenCanvas(this.canvas.clientWidth * this.pd, this.canvas.clientHeight * this.pd);
let surface = null;
try {
const webglSurface = CanvasKit.MakeWebGLCanvasSurface(tempCanvas);
const ctx = tempCanvas.getContext("webgl2");
if (ctx) {
ctx.drawingBufferColorSpace = "display-p3";
}
if (!webglSurface) {
throw new Error("Could not create WebGL surface");
}
surface = new JsiSkSurface(CanvasKit, webglSurface);
const skiaCanvas = surface.getCanvas();
skiaCanvas.clear(Float32Array.of(0, 0, 0, 0));
skiaCanvas.save();
skiaCanvas.scale(this.pd, this.pd);
skiaCanvas.drawPicture(picture);
skiaCanvas.restore();
surface.ref.flush();
return {
surface,
tempCanvas
};
} catch (error) {
if (surface) {
surface.ref.delete();
}
this.cleanupWebGLContext(tempCanvas);
return null;
}
}
cleanupWebGLContext(tempCanvas) {
const ctx = tempCanvas.getContext("webgl2");
if (ctx) {
const loseContext = ctx.getExtension("WEBGL_lose_context");
if (loseContext) {
loseContext.loseContext();
}
}
}
draw(picture) {
const renderResult = this.renderPictureToSurface(picture);
if (!renderResult) {
return;
}
const {
tempCanvas
} = renderResult;
const ctx2d = this.canvas.getContext("2d");
if (!ctx2d) {
throw new Error("Could not get 2D context");
}
// Set canvas dimensions to match pixel density
this.canvas.width = this.canvas.clientWidth * this.pd;
this.canvas.height = this.canvas.clientHeight * this.pd;
// Draw the tempCanvas scaled down to the display size
ctx2d.drawImage(tempCanvas, 0, 0, tempCanvas.width, tempCanvas.height, 0, 0, this.canvas.clientWidth * this.pd, this.canvas.clientHeight * this.pd);
this.cleanupWebGLContext(tempCanvas);
}
makeImageSnapshot(picture, rect) {
if (!this.cachedImage) {
const renderResult = this.renderPictureToSurface(picture);
if (!renderResult) {
return null;
}
const {
surface,
tempCanvas
} = renderResult;
try {
this.cachedImage = surface.makeImageSnapshot(dp2Pixel(this.pd, rect));
} catch (error) {
console.error("Error creating image snapshot:", error);
} finally {
surface.ref.delete();
this.cleanupWebGLContext(tempCanvas);
}
}
return this.cachedImage;
}
dispose() {
var _this$cachedImage;
(_this$cachedImage = this.cachedImage) === null || _this$cachedImage === void 0 || _this$cachedImage.dispose();
this.cachedImage = null;
}
}
const pd = Platform.PixelRatio;
export const SkiaPictureView = props => {
const {
ref
} = props;
const canvasRef = useRef(null);
const renderer = useRef(null);
const redrawRequestsRef = useRef(0);
const requestIdRef = useRef(0);
const pictureRef = useRef(null);
const {
picture,
onLayout
} = props;
const redraw = useCallback(() => {
redrawRequestsRef.current++;
}, []);
const getSize = useCallback(() => {
var _canvasRef$current, _canvasRef$current2;
return {
width: ((_canvasRef$current = canvasRef.current) === null || _canvasRef$current === void 0 ? void 0 : _canvasRef$current.clientWidth) || 0,
height: ((_canvasRef$current2 = canvasRef.current) === null || _canvasRef$current2 === void 0 ? void 0 : _canvasRef$current2.clientHeight) || 0
};
}, []);
const setPicture = useCallback(newPicture => {
pictureRef.current = newPicture;
redraw();
}, [redraw]);
const makeImageSnapshot = useCallback(rect => {
if (renderer.current && pictureRef.current) {
return renderer.current.makeImageSnapshot(pictureRef.current, rect);
}
return null;
}, []);
const measure = useCallback(callback => {
if (canvasRef.current) {
const rect = canvasRef.current.getBoundingClientRect();
const parentElement = canvasRef.current.offsetParent;
const parentRect = (parentElement === null || parentElement === void 0 ? void 0 : parentElement.getBoundingClientRect()) || {
left: 0,
top: 0
};
// x, y are relative to the parent
const x = rect.left - parentRect.left;
const y = rect.top - parentRect.top;
// pageX, pageY are absolute screen coordinates
const pageX = rect.left + window.scrollX;
const pageY = rect.top + window.scrollY;
callback(x, y, rect.width, rect.height, pageX, pageY);
}
}, []);
const measureInWindow = useCallback(callback => {
if (canvasRef.current) {
const rect = canvasRef.current.getBoundingClientRect();
// x, y are the absolute coordinates in the window
const x = rect.left;
const y = rect.top;
callback(x, y, rect.width, rect.height);
}
}, []);
const tick = useCallback(() => {
if (redrawRequestsRef.current > 0) {
redrawRequestsRef.current = 0;
if (renderer.current && pictureRef.current) {
renderer.current.draw(pictureRef.current);
}
}
requestIdRef.current = requestAnimationFrame(tick);
}, []);
const onLayoutEvent = useCallback(evt => {
const canvas = canvasRef.current;
if (canvas) {
renderer.current = props.__destroyWebGLContextAfterRender === true ? new StaticWebGLRenderer(canvas, pd) : new WebGLRenderer(canvas, pd);
if (pictureRef.current) {
renderer.current.draw(pictureRef.current);
}
}
if (onLayout) {
onLayout(evt);
}
}, [onLayout, props.__destroyWebGLContextAfterRender]);
useImperativeHandle(ref, () => ({
setPicture,
getSize,
redraw,
makeImageSnapshot,
measure,
measureInWindow,
get canvasRef() {
return () => canvasRef.current;
}
}), [setPicture, getSize, redraw, makeImageSnapshot, measure, measureInWindow]);
useEffect(() => {
var _props$nativeID;
const nativeID = (_props$nativeID = props.nativeID) !== null && _props$nativeID !== void 0 ? _props$nativeID : `${SkiaViewNativeId.current++}`;
global.SkiaViewApi.registerView(nativeID, {
setPicture,
getSize,
redraw,
makeImageSnapshot,
measure,
measureInWindow
});
}, [setPicture, getSize, redraw, makeImageSnapshot, measure, measureInWindow, props.nativeID]);
useEffect(() => {
if (props.picture) {
setPicture(props.picture);
}
}, [setPicture, props.picture]);
useEffect(() => {
tick();
return () => {
cancelAnimationFrame(requestIdRef.current);
if (renderer.current) {
renderer.current.dispose();
renderer.current = null;
}
};
}, [tick]);
useEffect(() => {
if (renderer.current && pictureRef.current) {
renderer.current.draw(pictureRef.current);
}
}, [picture, redraw]);
const {
debug = false,
ref: _ref,
...viewProps
} = props;
return /*#__PURE__*/React.createElement(Platform.View, _extends({}, viewProps, {
onLayout: onLayoutEvent
}), /*#__PURE__*/React.createElement("canvas", {
ref: canvasRef,
style: {
display: "block",
width: "100%",
height: "100%"
}
}));
};
//# sourceMappingURL=SkiaPictureView.web.js.map