diff --git a/docs/docs/shapes/patch.md b/docs/docs/shapes/patch.md index ea108540e9..98516b8dba 100644 --- a/docs/docs/shapes/patch.md +++ b/docs/docs/shapes/patch.md @@ -22,15 +22,15 @@ import {Canvas, Patch, vec} from "@shopify/react-native-skia"; const PatchDemo = () => { const colors = ["#61dafb", "#fb61da", "#61fbcf", "#dafb61"]; const C = 64; - const topLeft = { src: vec(0, 0), c1: vec(C, 0), c2: vec(0, C) }; - const topRight = { src: vec(256, 0), c1: vec(256 + C, 0), c2: vec(256, C) }; + const topLeft = { pos: vec(0, 0), c1: vec(C, 0), c2: vec(0, C) }; + const topRight = { pos: vec(256, 0), c1: vec(256 + C, 0), c2: vec(256, C) }; const bottomRight = { - src: vec(256, 256), + pos: vec(256, 256), c1: vec(256 - 2 * C, 256), c2: vec(256, 256 - 2 * C), }; const bottomLeft = { - src: vec(0, 256), + pos: vec(0, 256), c1: vec(-2 * C, 256), c2: vec(0, 256 - 2 * C), }; diff --git a/example/src/App.tsx b/example/src/App.tsx index 22c4367c95..3a2c43e613 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -10,6 +10,7 @@ import { Filters } from "./Examples/Filters"; import { Gooey } from "./Examples/Gooey"; import { Hue } from "./Examples/Hue"; import { Matrix } from "./Examples/Matrix"; +import { Aurora } from "./Examples/Aurora"; import { HomeScreen } from "./Home"; const App = () => { @@ -39,6 +40,13 @@ const App = () => { header: () => null, }} /> + null, + }} + /> diff --git a/example/src/Examples/API/Shapes2.tsx b/example/src/Examples/API/Shapes2.tsx index bc54b40496..b5268fa5a3 100644 --- a/example/src/Examples/API/Shapes2.tsx +++ b/example/src/Examples/API/Shapes2.tsx @@ -10,13 +10,13 @@ import { Oval, Line, Points, - Patch, vec, rect, rrect, Paint, DashPathEffect, RoundedRect, + Patch, } from "@shopify/react-native-skia"; import { Title } from "./components/Title"; @@ -59,10 +59,10 @@ const inner = rrect( 0 ); -const topLeft = { src: vec(0, 0), c1: vec(0, 15), c2: vec(15, 0) }; -const topRight = { src: vec(100, 0), c1: vec(100, 15), c2: vec(85, 0) }; -const bottomRight = { src: vec(100, 100), c1: vec(100, 85), c2: vec(85, 100) }; -const bottomLeft = { src: vec(0, 100), c1: vec(0, 85), c2: vec(15, 100) }; +const topLeft = { pos: vec(16, 0), c1: vec(0, 15), c2: vec(15, 0) }; +const topRight = { pos: vec(100, 0), c1: vec(80, 15), c2: vec(85, 0) }; +const bottomRight = { pos: vec(100, 100), c1: vec(100, 85), c2: vec(85, 100) }; +const bottomLeft = { pos: vec(16, 100), c1: vec(0, 85), c2: vec(15, 100) }; export const Shapes = () => { return ( @@ -111,7 +111,7 @@ export const Shapes = () => { diff --git a/example/src/Examples/Aurora/Aurora.tsx b/example/src/Examples/Aurora/Aurora.tsx new file mode 100644 index 0000000000..073dd29aa7 --- /dev/null +++ b/example/src/Examples/Aurora/Aurora.tsx @@ -0,0 +1,60 @@ +import React from "react"; + +import { CoonsPatchMeshGradient } from "./components/CoonsPatchMeshGradient"; + +export const Aurora = () => { + return ; +}; + +const palette = { + otto: [ + "#FEF8C4", + "#E1F1D5", + "#C4EBE5", + "#ECA171", + "#FFFCF3", + "#D4B3B7", + "#B5A8D2", + "#F068A1", + "#EDD9A2", + "#FEEFAB", + "#A666C0", + "#8556E5", + "#DC4C4C", + "#EC795A", + "#E599F0", + "#96EDF2", + ], + will: [ + "#2D4CD2", + "#36B6D9", + "#3CF2B5", + "#37FF5E", + "#59FB2D", + "#AFF12D", + "#DABC2D", + "#D35127", + "#D01252", + "#CF0CAA", + "#A80DD8", + "#5819D7", + ], + skia: [ + "#61DAFB", + "#dafb61", + "#61fbcf", + "#61DAFB", + "#fb61da", + "#61fbcf", + "#dafb61", + "#fb61da", + "#61DAFB", + "#fb61da", + "#dafb61", + "#61fbcf", + "#fb61da", + "#61DAFB", + "#dafb61", + "#61fbcf", + ], +}; diff --git a/example/src/Examples/Aurora/components/BilinearGradient.tsx b/example/src/Examples/Aurora/components/BilinearGradient.tsx new file mode 100644 index 0000000000..01f1bd63c3 --- /dev/null +++ b/example/src/Examples/Aurora/components/BilinearGradient.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import type { ColorProp, Vector } from "@shopify/react-native-skia"; +import { + processColorAsUnitArray, + Shader, + Skia, +} from "@shopify/react-native-skia"; + +const source = Skia.RuntimeEffect.Make(` +uniform vec2 size; +uniform vec4 color0; +uniform vec4 color1; +uniform vec4 color2; +uniform vec4 color3; + +vec4 main(vec2 pos) { + vec2 uv = pos/size; + vec4 colorA = mix(color0, color1, uv.x); + vec4 colorB = mix(color2, color3, uv.x); + return mix(colorA, colorB, uv.y); +}`)!; + +interface BilinearGradientProps { + size: Vector; + colors: ColorProp[]; +} + +export const BilinearGradient = ({ size, colors }: BilinearGradientProps) => { + const [color0, color1, color2, color3] = colors.map((cl) => + processColorAsUnitArray(cl, 1) + ); + return ( + + ); +}; diff --git a/example/src/Examples/Aurora/components/CoonsPatchMeshGradient.tsx b/example/src/Examples/Aurora/components/CoonsPatchMeshGradient.tsx new file mode 100644 index 0000000000..d0f47b9dfb --- /dev/null +++ b/example/src/Examples/Aurora/components/CoonsPatchMeshGradient.tsx @@ -0,0 +1,157 @@ +import React from "react"; +import type { + AnimationValue, + CubicBezierHandle, +} from "@shopify/react-native-skia"; +import { + add, + useValue, + Canvas, + ImageShader, + Patch, + vec, + Paint, + processColor, +} from "@shopify/react-native-skia"; +import { Dimensions } from "react-native"; + +import { bilinearInterpolate, symmetric } from "./Math"; +import { Cubic } from "./Cubic"; +import { Curves } from "./Curves"; +import { useHandles } from "./useHandles"; + +const { width, height } = Dimensions.get("window"); +const size = vec(width, height); + +const rectToTexture = ( + vertices: CubicBezierHandle[], + [tl, tr, br, bl]: readonly [number, number, number, number] +) => + [ + vertices[tl].pos, + vertices[tr].pos, + vertices[br].pos, + vertices[bl].pos, + ] as const; + +const rectToColors = ( + colors: number[], + [tl, tr, br, bl]: readonly [number, number, number, number] +) => [colors[tl], colors[tr], colors[br], colors[bl]] as const; + +const rectToPatch = + (mesh: AnimationValue, indices: readonly number[]) => + () => { + const tl = mesh.value[indices[0]]; + const tr = mesh.value[indices[1]]; + const br = mesh.value[indices[2]]; + const bl = mesh.value[indices[3]]; + return [ + { + pos: tl.pos, + c1: tl.c2, + c2: tl.c1, + }, + { + pos: tr.pos, + c1: symmetric(tr.c1, tr.pos), + c2: tr.c2, + }, + { + pos: br.pos, + c1: symmetric(br.c2, br.pos), + c2: symmetric(br.c1, br.pos), + }, + { + pos: bl.pos, + c1: bl.c1, + c2: symmetric(bl.c2, bl.pos), + }, + ] as const; + }; + +interface CoonsPatchMeshGradientProps { + rows: number; + cols: number; + colors: string[]; + debug?: boolean; + lines?: boolean; +} + +export const CoonsPatchMeshGradient = ({ + rows, + cols, + colors: rawColors, + debug, + lines, +}: CoonsPatchMeshGradientProps) => { + const colors = rawColors.map((color) => processColor(color, 1)); + const dx = width / cols; + const dy = height / rows; + const C = dx / 3; + + const defaultMesh = new Array(cols + 1) + .fill(0) + .map((_c, col) => + new Array(rows + 1).fill(0).map((_r, row) => { + const pos = vec(row * dx, col * dy); + return { + pos, + c1: add(pos, vec(C, 0)), + c2: add(pos, vec(0, C)), + }; + }) + ) + .flat(2); + + const mesh = useValue(defaultMesh); + const rects = new Array(rows) + .fill(0) + .map((_r, row) => + new Array(cols).fill(0).map((_c, col) => { + const l = cols + 1; + const tl = row * l + col; + const tr = tl + 1; + const bl = (row + 1) * l + col; + const br = bl + 1; + return [tl, tr, br, bl] as const; + }) + ) + .flat(); + + const onTouch = useHandles(mesh, defaultMesh, width, height); + return ( + + + + + {rects.map((r, i) => { + const patch = rectToPatch(mesh, r); + return ( + + + {lines && } + + ); + })} + {defaultMesh.map(({ pos }, index) => { + const edge = + pos.x === 0 || pos.y === 0 || pos.x === width || pos.y === height; + if (edge) { + return null; + } + return ( + + ); + })} + + ); +}; diff --git a/example/src/Examples/Aurora/components/Cubic.tsx b/example/src/Examples/Aurora/components/Cubic.tsx new file mode 100644 index 0000000000..9f9956b8ff --- /dev/null +++ b/example/src/Examples/Aurora/components/Cubic.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import type { + AnimationValue, + CubicBezierHandle, + Vector, +} from "@shopify/react-native-skia"; +import { Line, Paint, Circle } from "@shopify/react-native-skia"; + +import { symmetric } from "./Math"; + +interface CubicProps { + mesh: AnimationValue; + index: number; + color: number; +} + +export const Cubic = ({ mesh, index, color }: CubicProps) => { + return ( + <> + mesh.value[index].c1} + p2={() => symmetric(mesh.value[index].c1, mesh.value[index].pos)} + /> + mesh.value[index].c2} + p2={() => symmetric(mesh.value[index].c2, mesh.value[index].pos)} + /> + mesh.value[index].pos} r={16} color={() => color}> + + + mesh.value[index].c1} r={10} color="white" /> + mesh.value[index].c2} r={10} color="white" /> + symmetric(mesh.value[index].c1, mesh.value[index].pos)} + r={10} + color="white" + /> + symmetric(mesh.value[index].c2, mesh.value[index].pos)} + r={10} + color="white" + /> + + ); +}; diff --git a/example/src/Examples/Aurora/components/Curves.tsx b/example/src/Examples/Aurora/components/Curves.tsx new file mode 100644 index 0000000000..7b80dd5342 --- /dev/null +++ b/example/src/Examples/Aurora/components/Curves.tsx @@ -0,0 +1,27 @@ +import type { PatchProps } from "@shopify/react-native-skia"; +import { Path, Skia } from "@shopify/react-native-skia"; +import React from "react"; + +interface CurvesProps { + patch: () => PatchProps["patch"]; +} + +export const Curves = ({ patch }: CurvesProps) => { + return ( + { + const [p1, p2, p3, p4] = patch(); + const path = Skia.Path.Make(); + path.moveTo(p1.pos.x, p1.pos.y); + path.cubicTo(p1.c2.x, p1.c2.y, p2.c1.x, p2.c1.y, p2.pos.x, p2.pos.y); + path.cubicTo(p2.c2.x, p2.c2.y, p3.c1.x, p3.c1.y, p3.pos.x, p3.pos.y); + path.cubicTo(p3.c2.x, p3.c2.y, p4.c1.x, p4.c1.y, p4.pos.x, p4.pos.y); + path.cubicTo(p4.c2.x, p4.c2.y, p1.c1.x, p1.c1.y, p1.pos.x, p1.pos.y); + return path; + }} + color="white" + strokeWidth={2} + style="stroke" + /> + ); +}; diff --git a/example/src/Examples/Aurora/components/Math.ts b/example/src/Examples/Aurora/components/Math.ts new file mode 100644 index 0000000000..02b3eccc48 --- /dev/null +++ b/example/src/Examples/Aurora/components/Math.ts @@ -0,0 +1,27 @@ +import type { Vector } from "@shopify/react-native-skia"; +import { dist, vec, mixColors } from "@shopify/react-native-skia"; + +export const bilinearInterpolate = ( + [color0, color1, color2, color3]: number[], + size: Vector, + pos: Vector +) => { + const uv = vec(pos.x / size.x, pos.y / size.y); + const colorA = mixColors(uv.x, color0, color1); + const colorB = mixColors(uv.x, color2, color3); + return mixColors(uv.y, colorA, colorB); +}; + +export const inRadius = (a: Vector, b: Vector, r = 20) => dist(a, b) < r; + +export const getPointAtLength = (length: number, from: Vector, to: Vector) => { + const angle = Math.atan2(to.y - from.y, to.x - from.x); + const x = from.x + length * Math.cos(angle); + const y = from.y + length * Math.sin(angle); + return vec(x, y); +}; + +export const symmetric = (v: Vector, center: Vector) => { + const d = dist(v, center); + return getPointAtLength(d * 2, v, center); +}; diff --git a/example/src/Examples/Aurora/components/useHandles.ts b/example/src/Examples/Aurora/components/useHandles.ts new file mode 100644 index 0000000000..711c7b3588 --- /dev/null +++ b/example/src/Examples/Aurora/components/useHandles.ts @@ -0,0 +1,79 @@ +import type { + AnimationValue, + CubicBezierHandle, +} from "@shopify/react-native-skia"; +import { sub, useTouchHandler, useValue } from "@shopify/react-native-skia"; + +import { inRadius, symmetric } from "./Math"; + +type TouchSelection = null | { + index: number; + point: "c1" | "c2" | "c3" | "c4" | "pos"; +}; + +export const useHandles = ( + mesh: AnimationValue, + defaultMesh: CubicBezierHandle[], + width: number, + height: number +) => { + const selection = useValue(null); + return useTouchHandler({ + onActive: (pt) => { + if (selection.value) { + const { index, point } = selection.value; + const { pos, c1, c2 } = mesh.value[index]; + if (point === "pos") { + const delta = sub(pos, pt); + mesh.value[index].pos = pt; + mesh.value[index].c1 = sub(c1, delta); + mesh.value[index].c2 = sub(c2, delta); + } else if (point === "c3") { + mesh.value[index].c1 = symmetric(pt, mesh.value[index].pos); + } else if (point === "c4") { + mesh.value[index].c2 = symmetric(pt, mesh.value[index].pos); + } else { + mesh.value[index][point] = pt; + } + } else { + defaultMesh.every(({ pos: p }, index) => { + const edge = + p.x === 0 || p.y === 0 || p.x === width || p.y === height; + if (!edge) { + const { pos, c1, c2 } = mesh.value[index]; + const c3 = symmetric(c1, pos); + const c4 = symmetric(c2, pos); + if (inRadius(pt, pos)) { + const delta = sub(pos, pt); + mesh.value[index].pos = pt; + mesh.value[index].c1 = sub(c1, delta); + mesh.value[index].c2 = sub(c2, delta); + selection.value = { index, point: "pos" }; + return false; + } else if (inRadius(pt, c1)) { + mesh.value[index].c1 = pt; + selection.value = { index, point: "c1" }; + return false; + } else if (inRadius(pt, c2)) { + mesh.value[index].c2 = pt; + selection.value = { index, point: "c2" }; + return false; + } else if (inRadius(pt, c3)) { + mesh.value[index].c1 = symmetric(pt, mesh.value[index].pos); + selection.value = { index, point: "c3" }; + return false; + } else if (inRadius(pt, c4)) { + mesh.value[index].c2 = symmetric(pt, mesh.value[index].pos); + selection.value = { index, point: "c4" }; + return false; + } + } + return true; + }); + } + }, + onEnd: () => { + selection.value = null; + }, + }); +}; diff --git a/example/src/Examples/Aurora/index.ts b/example/src/Examples/Aurora/index.ts new file mode 100644 index 0000000000..f52b734ecf --- /dev/null +++ b/example/src/Examples/Aurora/index.ts @@ -0,0 +1 @@ +export * from "./Aurora"; diff --git a/example/src/Home/HomeScreen.tsx b/example/src/Home/HomeScreen.tsx index 7dc0039f6a..9cabb9face 100644 --- a/example/src/Home/HomeScreen.tsx +++ b/example/src/Home/HomeScreen.tsx @@ -3,7 +3,7 @@ import { StyleSheet, View } from "react-native"; import { HomeScreenButton } from "./HomeScreenButton"; -export const HomeScreen: React.FC = () => { +export const HomeScreen = () => { return ( @@ -32,6 +32,11 @@ export const HomeScreen: React.FC = () => { description="Digital Rain" route="Matrix" /> + uniformSize()) { - jsi::detail::throwJSError( - runtime, "Uniforms size differs from effect's uniform size."); + std::string msg = "Uniforms size differs from effect's uniform size. Received " + std::to_string(jsiUniformsSize) + " expected " + std::to_string(getObject()->uniformSize() / sizeof(float)); + jsi::detail::throwJSError(runtime, msg.c_str()); } auto uniforms = SkData::MakeUninitialized(getObject()->uniformSize()); diff --git a/package/src/animation/functions/interpolateColors.ts b/package/src/animation/functions/interpolateColors.ts index 359262df57..272cde3a23 100644 --- a/package/src/animation/functions/interpolateColors.ts +++ b/package/src/animation/functions/interpolateColors.ts @@ -1,11 +1,13 @@ import { Color } from "../../skia/Color"; +import type { ColorProp } from "../../renderer/processors/Colors"; import { red, green, blue, - alpha, + alphaf, rgbaColor, -} from "../../renderer/processors/Paint"; +} from "../../renderer/processors/Colors"; +import { mix } from "../../renderer/processors/math/Math"; import { interpolate } from "./interpolate"; @@ -40,7 +42,7 @@ const interpolateColorsRGB = ( const a = interpolate( value, inputRange, - outputRange.map((c) => alpha(c)), + outputRange.map((c) => alphaf(c)), CLAMP ); return rgbaColor(r, g, b, a); @@ -49,8 +51,19 @@ const interpolateColorsRGB = ( export const interpolateColors = ( value: number, inputRange: number[], - _outputRange: string[] + _outputRange: ColorProp[] ) => { const outputRange = _outputRange.map((cl) => Color(cl)); return interpolateColorsRGB(value, inputRange, outputRange); }; + +// This is fast. To be reconcilled with interpolateColors +// it looks like interpolateColors may not be working as expected +// these functions need to be tested more thoroughly on both platform +export const mixColors = (value: number, x: number, y: number) => { + const r = mix(value, red(x), red(y)); + const g = mix(value, green(x), green(y)); + const b = mix(value, blue(x), blue(y)); + const a = mix(value, alphaf(x), alphaf(y)); + return rgbaColor(r, g, b, a); +}; diff --git a/package/src/renderer/components/colorFilters/Blend.tsx b/package/src/renderer/components/colorFilters/Blend.tsx index 4fb088442c..b851b5092d 100644 --- a/package/src/renderer/components/colorFilters/Blend.tsx +++ b/package/src/renderer/components/colorFilters/Blend.tsx @@ -3,9 +3,8 @@ import type { ReactNode } from "react"; import { BlendMode, Skia } from "../../../skia"; import { useDeclaration } from "../../nodes/Declaration"; -import type { SkEnum, ColorProp } from "../../processors/Paint"; -import { enumKey } from "../../processors/Paint"; -import type { AnimatedProps } from "../../processors/Animations/Animations"; +import type { SkEnum, ColorProp, AnimatedProps } from "../../processors"; +import { enumKey } from "../../processors"; import { composeColorFilter } from "./Compose"; diff --git a/package/src/renderer/components/image/BoxFit.ts b/package/src/renderer/components/image/BoxFit.ts index 2d8251e12a..3c0d928783 100644 --- a/package/src/renderer/components/image/BoxFit.ts +++ b/package/src/renderer/components/image/BoxFit.ts @@ -12,12 +12,12 @@ export type Fit = | "none" | "scaleDown"; -interface Size { +export interface Size { width: number; height: number; } -const size = (width = 0, height = 0) => ({ width, height }); +export const size = (width = 0, height = 0) => ({ width, height }); export const rect2rect = (src: IRect, dst: IRect) => { const scaleX = dst.width / src.width; diff --git a/package/src/renderer/components/image/index.ts b/package/src/renderer/components/image/index.ts index 2a25b4e572..f9831dab9e 100644 --- a/package/src/renderer/components/image/index.ts +++ b/package/src/renderer/components/image/index.ts @@ -1,3 +1,4 @@ export * from "./Image"; export * from "./ImageShader"; export * from "./ImageSVG"; +export * from "./BoxFit"; diff --git a/package/src/renderer/components/shaders/Color.tsx b/package/src/renderer/components/shaders/Color.tsx index 17ee59f0a2..db2f2123bb 100644 --- a/package/src/renderer/components/shaders/Color.tsx +++ b/package/src/renderer/components/shaders/Color.tsx @@ -2,8 +2,8 @@ import React from "react"; import { Skia } from "../../../skia"; import { useDeclaration } from "../../nodes/Declaration"; -import type { ColorProp } from "../../processors/Paint"; -import { processColor } from "../../processors/Paint"; +import type { ColorProp } from "../../processors"; +import { processColor } from "../../processors"; import type { AnimatedProps } from "../../processors/Animations/Animations"; export interface ColorShaderProps { diff --git a/package/src/renderer/components/shaders/Gradient.ts b/package/src/renderer/components/shaders/Gradient.ts index 88ec3432e9..8f0b5129a0 100644 --- a/package/src/renderer/components/shaders/Gradient.ts +++ b/package/src/renderer/components/shaders/Gradient.ts @@ -1,8 +1,9 @@ import { TileMode, Skia } from "../../../skia"; -import type { SkEnum, ColorProp } from "../../processors/Paint"; +import type { SkEnum } from "../../processors/Paint"; import type { TransformProps } from "../../processors/Transform"; import { enumKey } from "../../processors/Paint"; import { localMatrix } from "../../processors/Transform"; +import type { ColorProp } from "../../processors/Colors"; export interface GradientProps extends TransformProps { colors: ColorProp[]; diff --git a/package/src/renderer/components/shaders/Shader.tsx b/package/src/renderer/components/shaders/Shader.tsx index e7e82d094e..b5af5620ff 100644 --- a/package/src/renderer/components/shaders/Shader.tsx +++ b/package/src/renderer/components/shaders/Shader.tsx @@ -12,7 +12,7 @@ const isVector = (obj: unknown): obj is Vector => // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj as any).x !== undefined && (obj as any).y !== undefined; -type Uniform = number | number[] | Vector; +type Uniform = number | readonly number[] | Vector; interface Uniforms { [name: string]: Uniform; @@ -21,14 +21,14 @@ interface Uniforms { export interface ShaderProps extends TransformProps { source: IRuntimeEffect; uniforms: Uniforms; - isOpaque?: boolean; + opaque?: boolean; children?: ReactNode | ReactNode[]; } export const Shader = (props: AnimatedProps) => { const declaration = useDeclaration( props, - ({ uniforms, source, isOpaque, ...transform }, children) => { + ({ uniforms, source, opaque, ...transform }, children) => { const processedUniforms = new Array(source.getUniformCount()) .fill(0) .map((_, i) => { @@ -42,7 +42,7 @@ export const Shader = (props: AnimatedProps) => { .flat(4); return source.makeShaderWithChildren( processedUniforms, - isOpaque, + opaque, children.filter(isShader), localMatrix(transform) ); diff --git a/package/src/renderer/components/shapes/Patch.tsx b/package/src/renderer/components/shapes/Patch.tsx index 1c8a0fa24f..074e838eac 100644 --- a/package/src/renderer/components/shapes/Patch.tsx +++ b/package/src/renderer/components/shapes/Patch.tsx @@ -1,36 +1,64 @@ import React from "react"; -import type { CustomPaintProps, SkEnum, Vector } from "../../processors"; +import type { + CustomPaintProps, + SkEnum, + Vector, + ColorProp, +} from "../../processors"; import { enumKey, processColor } from "../../processors"; import type { IPoint } from "../../../skia"; import { BlendMode } from "../../../skia/Paint/BlendMode"; import type { AnimatedProps } from "../../processors/Animations/Animations"; import { useDrawing } from "../../nodes/Drawing"; -interface CubicBezier { - src: Vector; +export interface CubicBezierHandle { + pos: Vector; c1: Vector; c2: Vector; } export interface PatchProps extends CustomPaintProps { - colors: string[]; - cubics: [CubicBezier, CubicBezier, CubicBezier, CubicBezier]; - texs?: IPoint[]; + colors?: readonly ColorProp[]; + patch: readonly [ + CubicBezierHandle, + CubicBezierHandle, + CubicBezierHandle, + CubicBezierHandle + ]; + texture?: readonly [IPoint, IPoint, IPoint, IPoint]; blendMode?: SkEnum; } export const Patch = (props: AnimatedProps) => { const onDraw = useDrawing( props, - ({ canvas, paint, opacity }, { colors, cubics, texs, blendMode }) => { + ({ canvas, paint, opacity }, { colors, patch, texture, blendMode }) => { // If the colors are provided, the default blendMode is set to dstOver, if not, the default is set to srcOver const defaultBlendMode = colors ? BlendMode.DstOver : BlendMode.SrcOver; const mode = blendMode ? BlendMode[enumKey(blendMode)] : defaultBlendMode; canvas.drawPatch( - cubics.map(({ src, c1, c2 }) => [src, c1, c2]).flat(), - colors.map((c) => processColor(c, opacity)), - texs, + // Patch requires a path with the following constraints: + // M tl + // C c1 c2 br + // C c1 c2 bl + // C c1 c2 tl (the redudand point in the last command is removed) + [ + patch[0].pos, + patch[0].c2, + patch[1].c1, + patch[1].pos, + patch[1].c2, + patch[2].c1, + patch[2].pos, + patch[2].c2, + patch[3].c1, + patch[3].pos, + patch[3].c2, + patch[0].c1, + ], + colors ? colors.map((c) => processColor(c, opacity)) : undefined, + texture, mode, paint ); diff --git a/package/src/renderer/processors/Colors.ts b/package/src/renderer/processors/Colors.ts new file mode 100644 index 0000000000..1a9f182f0b --- /dev/null +++ b/package/src/renderer/processors/Colors.ts @@ -0,0 +1,33 @@ +import { Skia } from "../../skia/Skia"; +export type ColorProp = string | number; + +export const alphaf = (c: number) => ((c >> 24) & 255) / 255; +export const red = (c: number) => (c >> 16) & 255; +export const green = (c: number) => (c >> 8) & 255; +export const blue = (c: number) => c & 255; +export const rgbaColor = (r: number, g: number, b: number, af: number) => { + const a = Math.round(af * 255); + return ((a << 24) | (r << 16) | (g << 8) | b) >>> 0; +}; + +const processColorAsArray = (cl: ColorProp) => { + const icl = typeof cl === "string" ? Skia.Color(cl) : cl; + const r = red(icl); + const g = green(icl); + const b = blue(icl); + const a = alphaf(icl); + return [r, g, b, a] as const; +}; + +export const processColor = (cl: ColorProp, currentOpacity: number) => { + const [r, g, b, a] = processColorAsArray(cl); + return rgbaColor(r, g, b, a * currentOpacity); +}; + +export const processColorAsUnitArray = ( + cl: ColorProp, + currentOpacity: number +) => { + const [r, g, b, a] = processColorAsArray(cl); + return [r / 255, g / 255, b / 255, a * currentOpacity] as const; +}; diff --git a/package/src/renderer/processors/Paint.ts b/package/src/renderer/processors/Paint.ts index 27cfb31130..562724e880 100644 --- a/package/src/renderer/processors/Paint.ts +++ b/package/src/renderer/processors/Paint.ts @@ -1,14 +1,14 @@ import type { RefObject } from "react"; -import { Skia, BlendMode, PaintStyle, StrokeJoin, StrokeCap } from "../../skia"; +import { BlendMode, PaintStyle, StrokeJoin, StrokeCap } from "../../skia"; import type { IPaint } from "../../skia"; import type { ChildrenProps } from "./Shapes"; +import type { ColorProp } from "./Colors"; +import { processColor } from "./Colors"; export type SkEnum = Uncapitalize; -export type ColorProp = string | number; - export interface CustomPaintProps extends ChildrenProps { paint?: RefObject; color?: ColorProp; @@ -24,24 +24,6 @@ export interface CustomPaintProps extends ChildrenProps { export const enumKey = (k: K) => (k.charAt(0).toUpperCase() + k.slice(1)) as Capitalize; -export const alpha = (c: number) => ((c >> 24) & 255) / 255; -export const red = (c: number) => (c >> 16) & 255; -export const green = (c: number) => (c >> 8) & 255; -export const blue = (c: number) => c & 255; -export const rgbaColor = (r: number, g: number, b: number, af: number) => { - const a = Math.round(af * 255); - return ((a << 24) | (r << 16) | (g << 8) | b) >>> 0; -}; - -export const processColor = (cl: ColorProp, currentOpacity: number) => { - const icl = typeof cl === "string" ? Skia.Color(cl) : cl; - const r = red(icl); - const g = green(icl); - const b = blue(icl); - const o = alpha(icl); - return rgbaColor(r, g, b, o * currentOpacity); -}; - export const processPaint = ( paint: IPaint, currentOpacity: number, diff --git a/package/src/renderer/processors/index.ts b/package/src/renderer/processors/index.ts index b4622581e4..08819bcde4 100644 --- a/package/src/renderer/processors/index.ts +++ b/package/src/renderer/processors/index.ts @@ -1,4 +1,5 @@ export * from "./Paint"; +export * from "./Colors"; export * from "./Transform"; export * from "./Animations"; export * from "./Shapes"; diff --git a/package/src/skia/Canvas.ts b/package/src/skia/Canvas.ts index 8b5c070e3b..427c6c6e5a 100644 --- a/package/src/skia/Canvas.ts +++ b/package/src/skia/Canvas.ts @@ -201,9 +201,9 @@ export interface ICanvas { * @param paint */ drawPatch( - cubics: IPoint[], - colors?: Color[] | null, - texs?: IPoint[] | null, + cubics: readonly IPoint[], + colors?: readonly Color[] | null, + texs?: readonly IPoint[] | null, mode?: BlendMode | null, paint?: IPaint ): void;