Skip to content

Commit

Permalink
fix: added support to border and radius for canvas2d (#425)
Browse files Browse the repository at this point in the history
  • Loading branch information
wouterlucas authored Nov 7, 2024
2 parents 17b7829 + 4669644 commit efa9ed9
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 10 deletions.
93 changes: 91 additions & 2 deletions src/core/renderers/canvas/CanvasCoreRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ import {
type QuadOptions,
} from '../CoreRenderer.js';
import { CanvasCoreTexture } from './CanvasCoreTexture.js';
import { getRadius } from './internal/C2DShaderUtils.js';
import { getBorder, getRadius, strokeLine } from './internal/C2DShaderUtils.js';
import {
formatRgba,
parseColorRgba,
parseColor,
type IParsedColor,
} from './internal/ColorUtils.js';
Expand Down Expand Up @@ -133,7 +134,9 @@ export class CanvasCoreRenderer extends CoreRenderer {
const hasTransform = ta !== 1;
const hasClipping = clippingRect.width !== 0 && clippingRect.height !== 0;
const hasGradient = colorTl !== colorTr || colorTl !== colorBr;
const radius = quad.shader ? getRadius(quad) : 0;
const hasQuadShader = Boolean(quad.shader);
const radius = hasQuadShader ? getRadius(quad) : 0;
const border = hasQuadShader ? getBorder(quad) : undefined;

if (hasTransform || hasClipping || radius) {
ctx.save();
Expand Down Expand Up @@ -211,6 +214,92 @@ export class CanvasCoreRenderer extends CoreRenderer {
ctx.fillRect(tx, ty, width, height);
}

if (border && border.width) {
const borderWidth = border.width;
const borderInnerWidth = border.width / 2;
const borderColor = formatRgba(parseColorRgba(border.color ?? 0));

ctx.beginPath();
ctx.lineWidth = borderWidth;
ctx.strokeStyle = borderColor;
ctx.globalAlpha = alpha;
if (radius) {
ctx.roundRect(
tx + borderInnerWidth,
ty + borderInnerWidth,
width - borderWidth,
height - borderWidth,
radius,
);
ctx.stroke();
} else {
ctx.strokeRect(
tx + borderInnerWidth,
ty + borderInnerWidth,
width - borderWidth,
height - borderWidth,
);
}
ctx.globalAlpha = 1;
} else if (hasQuadShader) {
const borderTop = getBorder(quad, 'Top');
const borderRight = getBorder(quad, 'Right');
const borderBottom = getBorder(quad, 'Bottom');
const borderLeft = getBorder(quad, 'Left');

if (borderTop) {
strokeLine(
ctx,
tx,
ty,
width,
height,
borderTop.width,
borderTop.color,
'Top',
);
}

if (borderRight) {
strokeLine(
ctx,
tx,
ty,
width,
height,
borderRight.width,
borderRight.color,
'Right',
);
}

if (borderBottom) {
strokeLine(
ctx,
tx,
ty,
width,
height,
borderBottom.width,
borderBottom.color,
'Bottom',
);
}

if (borderLeft) {
strokeLine(
ctx,
tx,
ty,
width,
height,
borderLeft.width,
borderLeft.color,
'Left',
);
}
}

if (hasTransform || hasClipping || radius) {
ctx.restore();
}
Expand Down
103 changes: 102 additions & 1 deletion src/core/renderers/canvas/internal/C2DShaderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,121 @@
*/

import type { QuadOptions } from '../../CoreRenderer.js';
import type { BorderEffectProps } from '../../webgl/shaders/effects/BorderEffect.js';
import type { RadiusEffectProps } from '../../webgl/shaders/effects/RadiusEffect.js';
import type { EffectDescUnion } from '../../webgl/shaders/effects/ShaderEffect.js';
import {
ROUNDED_RECTANGLE_SHADER_TYPE,
UnsupportedShader,
} from '../shaders/UnsupportedShader.js';
import { formatRgba, parseColorRgba } from './ColorUtils.js';

type Direction = 'Top' | 'Right' | 'Bottom' | 'Left';

/**
* Extract `RoundedRectangle` shader radius to apply as a clipping
*/
export function getRadius(quad: QuadOptions): number {
export function getRadius(quad: QuadOptions): RadiusEffectProps['radius'] {
if (quad.shader instanceof UnsupportedShader) {
const shType = quad.shader.shType;
if (shType === ROUNDED_RECTANGLE_SHADER_TYPE) {
return (quad.shaderProps?.radius as number) ?? 0;
} else if (shType === 'DynamicShader') {
const effects = quad.shaderProps?.effects as
| EffectDescUnion[]
| undefined;

if (effects) {
const effect = effects.find((effect: EffectDescUnion) => {
return effect.type === 'radius' && effect?.props?.radius;
});

return (effect && effect.type === 'radius' && effect.props.radius) || 0;
}
}
}
return 0;
}

/**
* Extract `RoundedRectangle` shader radius to apply as a clipping */
export function getBorder(
quad: QuadOptions,
direction: '' | Direction = '',
): BorderEffectProps | undefined {
if (quad.shader instanceof UnsupportedShader) {
const shType = quad.shader.shType;
if (shType === 'DynamicShader') {
const effects = quad.shaderProps?.effects as
| EffectDescUnion[]
| undefined;

if (effects && effects.length) {
const effect = effects.find((effect: EffectDescUnion) => {
return (
effect.type === `border${direction}` &&
effect.props &&
effect.props.width
);
});

return effect && effect.props;
}
}
}

return undefined;
}

export function strokeLine(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
lineWidth = 0,
color: number | undefined,
direction: Direction,
) {
if (!lineWidth) {
return;
}

let sx,
sy = 0;
let ex,
ey = 0;

switch (direction) {
case 'Top':
sx = x;
sy = y;
ex = width + x;
ey = y;
break;
case 'Right':
sx = x + width;
sy = y;
ex = x + width;
ey = y + height;
break;
case 'Bottom':
sx = x;
sy = y + height;
ex = x + width;
ey = y + height;
break;
case 'Left':
sx = x;
sy = y;
ex = x;
ey = y + height;
break;
}
ctx.beginPath();
ctx.lineWidth = lineWidth;
ctx.strokeStyle = formatRgba(parseColorRgba(color ?? 0));
ctx.moveTo(sx, sy);
ctx.lineTo(ex, ey);
ctx.stroke();
}
14 changes: 14 additions & 0 deletions src/core/renderers/canvas/internal/ColorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ export function parseColor(abgr: number): IParsedColor {
return { isWhite: false, a, r, g, b };
}

/**
* Extract color components
*/
export function parseColorRgba(rgba: number): IParsedColor {
if (rgba === 0xffffffff) {
return WHITE;
}
const r = (rgba >>> 24) & 0xff;
const g = (rgba >>> 16) & 0xff & 0xff;
const b = (rgba >>> 8) & 0xff & 0xff;
const a = (rgba & 0xff & 0xff) / 255;
return { isWhite: false, r, g, b, a };
}

/**
* Format a parsed color into a rgba CSS color
*/
Expand Down
6 changes: 3 additions & 3 deletions src/core/renderers/canvas/shaders/UnsupportedShader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ export class UnsupportedShader extends CoreShader {
constructor(shType: string) {
super();
this.shType = shType;
if (shType !== ROUNDED_RECTANGLE_SHADER_TYPE) {
console.warn('Unsupported shader:', shType);
}
// if (shType !== ROUNDED_RECTANGLE_SHADER_TYPE) {
// console.warn('Unsupported shader:', shType);
// }
}

bindRenderOp(): void {
Expand Down
5 changes: 4 additions & 1 deletion src/core/renderers/webgl/WebGlCoreCtxTexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
this._nativeCtxTexture = this.createNativeCtxTexture();
if (this._nativeCtxTexture === null) {
this._state = 'failed';
this.textureSource.setState('failed', new Error('Could not create WebGL Texture'));
this.textureSource.setState(
'failed',
new Error('Could not create WebGL Texture'),
);
console.error('Could not create WebGL Texture');
return;
}
Expand Down
2 changes: 0 additions & 2 deletions src/core/renderers/webgl/shaders/effects/RadiusEffect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { DynamicShaderProps } from '../DynamicShader.js';
import { updateWebSafeRadius, validateArrayLength4 } from './EffectUtils.js';
import {
ShaderEffect,
type DefaultEffectProps,
type ShaderEffectUniforms,
type ShaderEffectValueMap,
} from './ShaderEffect.js';

/**
Expand Down
1 change: 0 additions & 1 deletion src/core/renderers/webgl/shaders/effects/ShaderEffect.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { EffectMap } from '../../../../CoreShaderManager.js';
import type { ExtractProps } from '../../../../CoreTextureManager.js';
import type { WebGlContextWrapper } from '../../../../lib/WebGlContextWrapper.js';
import type {
AlphaShaderProp,
DimensionsShaderProp,
Expand Down

0 comments on commit efa9ed9

Please sign in to comment.