Skip to content

Commit efa9ed9

Browse files
authored
fix: added support to border and radius for canvas2d (#425)
2 parents 17b7829 + 4669644 commit efa9ed9

File tree

7 files changed

+214
-10
lines changed

7 files changed

+214
-10
lines changed

src/core/renderers/canvas/CanvasCoreRenderer.ts

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ import {
3030
type QuadOptions,
3131
} from '../CoreRenderer.js';
3232
import { CanvasCoreTexture } from './CanvasCoreTexture.js';
33-
import { getRadius } from './internal/C2DShaderUtils.js';
33+
import { getBorder, getRadius, strokeLine } from './internal/C2DShaderUtils.js';
3434
import {
3535
formatRgba,
36+
parseColorRgba,
3637
parseColor,
3738
type IParsedColor,
3839
} from './internal/ColorUtils.js';
@@ -133,7 +134,9 @@ export class CanvasCoreRenderer extends CoreRenderer {
133134
const hasTransform = ta !== 1;
134135
const hasClipping = clippingRect.width !== 0 && clippingRect.height !== 0;
135136
const hasGradient = colorTl !== colorTr || colorTl !== colorBr;
136-
const radius = quad.shader ? getRadius(quad) : 0;
137+
const hasQuadShader = Boolean(quad.shader);
138+
const radius = hasQuadShader ? getRadius(quad) : 0;
139+
const border = hasQuadShader ? getBorder(quad) : undefined;
137140

138141
if (hasTransform || hasClipping || radius) {
139142
ctx.save();
@@ -211,6 +214,92 @@ export class CanvasCoreRenderer extends CoreRenderer {
211214
ctx.fillRect(tx, ty, width, height);
212215
}
213216

217+
if (border && border.width) {
218+
const borderWidth = border.width;
219+
const borderInnerWidth = border.width / 2;
220+
const borderColor = formatRgba(parseColorRgba(border.color ?? 0));
221+
222+
ctx.beginPath();
223+
ctx.lineWidth = borderWidth;
224+
ctx.strokeStyle = borderColor;
225+
ctx.globalAlpha = alpha;
226+
if (radius) {
227+
ctx.roundRect(
228+
tx + borderInnerWidth,
229+
ty + borderInnerWidth,
230+
width - borderWidth,
231+
height - borderWidth,
232+
radius,
233+
);
234+
ctx.stroke();
235+
} else {
236+
ctx.strokeRect(
237+
tx + borderInnerWidth,
238+
ty + borderInnerWidth,
239+
width - borderWidth,
240+
height - borderWidth,
241+
);
242+
}
243+
ctx.globalAlpha = 1;
244+
} else if (hasQuadShader) {
245+
const borderTop = getBorder(quad, 'Top');
246+
const borderRight = getBorder(quad, 'Right');
247+
const borderBottom = getBorder(quad, 'Bottom');
248+
const borderLeft = getBorder(quad, 'Left');
249+
250+
if (borderTop) {
251+
strokeLine(
252+
ctx,
253+
tx,
254+
ty,
255+
width,
256+
height,
257+
borderTop.width,
258+
borderTop.color,
259+
'Top',
260+
);
261+
}
262+
263+
if (borderRight) {
264+
strokeLine(
265+
ctx,
266+
tx,
267+
ty,
268+
width,
269+
height,
270+
borderRight.width,
271+
borderRight.color,
272+
'Right',
273+
);
274+
}
275+
276+
if (borderBottom) {
277+
strokeLine(
278+
ctx,
279+
tx,
280+
ty,
281+
width,
282+
height,
283+
borderBottom.width,
284+
borderBottom.color,
285+
'Bottom',
286+
);
287+
}
288+
289+
if (borderLeft) {
290+
strokeLine(
291+
ctx,
292+
tx,
293+
ty,
294+
width,
295+
height,
296+
borderLeft.width,
297+
borderLeft.color,
298+
'Left',
299+
);
300+
}
301+
}
302+
214303
if (hasTransform || hasClipping || radius) {
215304
ctx.restore();
216305
}

src/core/renderers/canvas/internal/C2DShaderUtils.ts

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,121 @@
1818
*/
1919

2020
import type { QuadOptions } from '../../CoreRenderer.js';
21+
import type { BorderEffectProps } from '../../webgl/shaders/effects/BorderEffect.js';
22+
import type { RadiusEffectProps } from '../../webgl/shaders/effects/RadiusEffect.js';
23+
import type { EffectDescUnion } from '../../webgl/shaders/effects/ShaderEffect.js';
2124
import {
2225
ROUNDED_RECTANGLE_SHADER_TYPE,
2326
UnsupportedShader,
2427
} from '../shaders/UnsupportedShader.js';
28+
import { formatRgba, parseColorRgba } from './ColorUtils.js';
29+
30+
type Direction = 'Top' | 'Right' | 'Bottom' | 'Left';
2531

2632
/**
2733
* Extract `RoundedRectangle` shader radius to apply as a clipping
2834
*/
29-
export function getRadius(quad: QuadOptions): number {
35+
export function getRadius(quad: QuadOptions): RadiusEffectProps['radius'] {
3036
if (quad.shader instanceof UnsupportedShader) {
3137
const shType = quad.shader.shType;
3238
if (shType === ROUNDED_RECTANGLE_SHADER_TYPE) {
3339
return (quad.shaderProps?.radius as number) ?? 0;
40+
} else if (shType === 'DynamicShader') {
41+
const effects = quad.shaderProps?.effects as
42+
| EffectDescUnion[]
43+
| undefined;
44+
45+
if (effects) {
46+
const effect = effects.find((effect: EffectDescUnion) => {
47+
return effect.type === 'radius' && effect?.props?.radius;
48+
});
49+
50+
return (effect && effect.type === 'radius' && effect.props.radius) || 0;
51+
}
3452
}
3553
}
3654
return 0;
3755
}
56+
57+
/**
58+
* Extract `RoundedRectangle` shader radius to apply as a clipping */
59+
export function getBorder(
60+
quad: QuadOptions,
61+
direction: '' | Direction = '',
62+
): BorderEffectProps | undefined {
63+
if (quad.shader instanceof UnsupportedShader) {
64+
const shType = quad.shader.shType;
65+
if (shType === 'DynamicShader') {
66+
const effects = quad.shaderProps?.effects as
67+
| EffectDescUnion[]
68+
| undefined;
69+
70+
if (effects && effects.length) {
71+
const effect = effects.find((effect: EffectDescUnion) => {
72+
return (
73+
effect.type === `border${direction}` &&
74+
effect.props &&
75+
effect.props.width
76+
);
77+
});
78+
79+
return effect && effect.props;
80+
}
81+
}
82+
}
83+
84+
return undefined;
85+
}
86+
87+
export function strokeLine(
88+
ctx: CanvasRenderingContext2D,
89+
x: number,
90+
y: number,
91+
width: number,
92+
height: number,
93+
lineWidth = 0,
94+
color: number | undefined,
95+
direction: Direction,
96+
) {
97+
if (!lineWidth) {
98+
return;
99+
}
100+
101+
let sx,
102+
sy = 0;
103+
let ex,
104+
ey = 0;
105+
106+
switch (direction) {
107+
case 'Top':
108+
sx = x;
109+
sy = y;
110+
ex = width + x;
111+
ey = y;
112+
break;
113+
case 'Right':
114+
sx = x + width;
115+
sy = y;
116+
ex = x + width;
117+
ey = y + height;
118+
break;
119+
case 'Bottom':
120+
sx = x;
121+
sy = y + height;
122+
ex = x + width;
123+
ey = y + height;
124+
break;
125+
case 'Left':
126+
sx = x;
127+
sy = y;
128+
ex = x;
129+
ey = y + height;
130+
break;
131+
}
132+
ctx.beginPath();
133+
ctx.lineWidth = lineWidth;
134+
ctx.strokeStyle = formatRgba(parseColorRgba(color ?? 0));
135+
ctx.moveTo(sx, sy);
136+
ctx.lineTo(ex, ey);
137+
ctx.stroke();
138+
}

src/core/renderers/canvas/internal/ColorUtils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ export function parseColor(abgr: number): IParsedColor {
4747
return { isWhite: false, a, r, g, b };
4848
}
4949

50+
/**
51+
* Extract color components
52+
*/
53+
export function parseColorRgba(rgba: number): IParsedColor {
54+
if (rgba === 0xffffffff) {
55+
return WHITE;
56+
}
57+
const r = (rgba >>> 24) & 0xff;
58+
const g = (rgba >>> 16) & 0xff & 0xff;
59+
const b = (rgba >>> 8) & 0xff & 0xff;
60+
const a = (rgba & 0xff & 0xff) / 255;
61+
return { isWhite: false, r, g, b, a };
62+
}
63+
5064
/**
5165
* Format a parsed color into a rgba CSS color
5266
*/

src/core/renderers/canvas/shaders/UnsupportedShader.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ export class UnsupportedShader extends CoreShader {
2727
constructor(shType: string) {
2828
super();
2929
this.shType = shType;
30-
if (shType !== ROUNDED_RECTANGLE_SHADER_TYPE) {
31-
console.warn('Unsupported shader:', shType);
32-
}
30+
// if (shType !== ROUNDED_RECTANGLE_SHADER_TYPE) {
31+
// console.warn('Unsupported shader:', shType);
32+
// }
3333
}
3434

3535
bindRenderOp(): void {

src/core/renderers/webgl/WebGlCoreCtxTexture.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
8888
this._nativeCtxTexture = this.createNativeCtxTexture();
8989
if (this._nativeCtxTexture === null) {
9090
this._state = 'failed';
91-
this.textureSource.setState('failed', new Error('Could not create WebGL Texture'));
91+
this.textureSource.setState(
92+
'failed',
93+
new Error('Could not create WebGL Texture'),
94+
);
9295
console.error('Could not create WebGL Texture');
9396
return;
9497
}

src/core/renderers/webgl/shaders/effects/RadiusEffect.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,11 @@
1616
* See the License for the specific language governing permissions and
1717
* limitations under the License.
1818
*/
19-
import type { DynamicShaderProps } from '../DynamicShader.js';
2019
import { updateWebSafeRadius, validateArrayLength4 } from './EffectUtils.js';
2120
import {
2221
ShaderEffect,
2322
type DefaultEffectProps,
2423
type ShaderEffectUniforms,
25-
type ShaderEffectValueMap,
2624
} from './ShaderEffect.js';
2725

2826
/**

src/core/renderers/webgl/shaders/effects/ShaderEffect.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { EffectMap } from '../../../../CoreShaderManager.js';
22
import type { ExtractProps } from '../../../../CoreTextureManager.js';
3-
import type { WebGlContextWrapper } from '../../../../lib/WebGlContextWrapper.js';
43
import type {
54
AlphaShaderProp,
65
DimensionsShaderProp,

0 commit comments

Comments
 (0)