diff --git a/src/core/renderers/canvas/CanvasCoreRenderer.ts b/src/core/renderers/canvas/CanvasCoreRenderer.ts index f9d5bd80..67d91489 100644 --- a/src/core/renderers/canvas/CanvasCoreRenderer.ts +++ b/src/core/renderers/canvas/CanvasCoreRenderer.ts @@ -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'; @@ -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(); @@ -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(); } diff --git a/src/core/renderers/canvas/internal/C2DShaderUtils.ts b/src/core/renderers/canvas/internal/C2DShaderUtils.ts index ffc6a6cf..52fb7586 100644 --- a/src/core/renderers/canvas/internal/C2DShaderUtils.ts +++ b/src/core/renderers/canvas/internal/C2DShaderUtils.ts @@ -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(); +} diff --git a/src/core/renderers/canvas/internal/ColorUtils.ts b/src/core/renderers/canvas/internal/ColorUtils.ts index 3265ca59..541ccaee 100644 --- a/src/core/renderers/canvas/internal/ColorUtils.ts +++ b/src/core/renderers/canvas/internal/ColorUtils.ts @@ -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 */ diff --git a/src/core/renderers/canvas/shaders/UnsupportedShader.ts b/src/core/renderers/canvas/shaders/UnsupportedShader.ts index 11636cd4..2f60abf6 100644 --- a/src/core/renderers/canvas/shaders/UnsupportedShader.ts +++ b/src/core/renderers/canvas/shaders/UnsupportedShader.ts @@ -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 { diff --git a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts index d3bf6ed9..085210cc 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts @@ -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; } diff --git a/src/core/renderers/webgl/shaders/effects/RadiusEffect.ts b/src/core/renderers/webgl/shaders/effects/RadiusEffect.ts index 40e9b212..2182dec3 100644 --- a/src/core/renderers/webgl/shaders/effects/RadiusEffect.ts +++ b/src/core/renderers/webgl/shaders/effects/RadiusEffect.ts @@ -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'; /** diff --git a/src/core/renderers/webgl/shaders/effects/ShaderEffect.ts b/src/core/renderers/webgl/shaders/effects/ShaderEffect.ts index c3fd5296..d71eea32 100644 --- a/src/core/renderers/webgl/shaders/effects/ShaderEffect.ts +++ b/src/core/renderers/webgl/shaders/effects/ShaderEffect.ts @@ -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,