From f270d221dd1980f1da7c053219ebb91e5220947b Mon Sep 17 00:00:00 2001 From: frouaix Date: Thu, 30 Apr 2026 18:15:45 -0700 Subject: [PATCH 1/2] Remove `as any` Co-authored-by: Copilot --- packages/core/src/core/context.ts | 39 ++++++++++++++++ packages/core/src/core/renderer-immediate.ts | 46 ++++++------------- packages/core/src/events.ts | 24 +++++++--- packages/core/src/transform.ts | 4 ++ packages/demo/demos/line-styles.ts | 5 +- .../gui/src/GUI/slider/transformSlider.ts | 29 ++++++++++-- 6 files changed, 101 insertions(+), 46 deletions(-) diff --git a/packages/core/src/core/context.ts b/packages/core/src/core/context.ts index ef49f92..d49942f 100644 --- a/packages/core/src/core/context.ts +++ b/packages/core/src/core/context.ts @@ -3,6 +3,7 @@ // Rendering context abstraction import { Matrix2D, TransformStack } from '../transform.ts'; import type { + BlendMode, DrawArcStyleProps, DrawCircleStyleProps, DrawImageStyleProps, @@ -12,6 +13,7 @@ import type { DrawTextStyleProps, FillStyle, LineStyleProps, + ShadowProps, TextMeasure, TextMeasureProps, } from './types.ts'; @@ -31,6 +33,10 @@ export interface RenderContext { restore(): void; applyTransform(xf: Matrix2D): void; setOpacity(opacity: number): void; + setShadow(shadow: ShadowProps | null): void; + setFilter(filter: string | undefined): void; + setBlendMode(blendMode: BlendMode | undefined): void; + clipRect(xl: number, yl: number, dxl: number, dyl: number): void; // Drawing primitives - to be implemented by concrete renderers clear(): void; @@ -115,6 +121,39 @@ export class Canvas2DContext implements RenderContext { this.ctx.globalAlpha = opacity; } + setShadow(shadow: ShadowProps | null): void { + if (!shadow) { + this.ctx.shadowOffsetX = 0; + this.ctx.shadowOffsetY = 0; + this.ctx.shadowBlur = 0; + this.ctx.shadowColor = 'transparent'; + return; + } + + const { offsetX, offsetY, blur, color } = shadow; + this.ctx.shadowOffsetX = offsetX; + this.ctx.shadowOffsetY = offsetY; + this.ctx.shadowBlur = blur; + this.ctx.shadowColor = color; + } + + setFilter(filter: string | undefined): void { + this.ctx.filter = filter ?? 'none'; + } + + setBlendMode(blendMode: BlendMode | undefined): void { + const compositeOperation: GlobalCompositeOperation = blendMode && blendMode !== 'normal' + ? blendMode + : 'source-over'; + this.ctx.globalCompositeOperation = compositeOperation; + } + + clipRect(xl: number, yl: number, dxl: number, dyl: number): void { + this.ctx.beginPath(); + this.ctx.rect(xl, yl, dxl, dyl); + this.ctx.clip(); + } + clear(): void { this.ctx.save(); this.ctx.setTransform(1, 0, 0, 1, 0, 0); diff --git a/packages/core/src/core/renderer-immediate.ts b/packages/core/src/core/renderer-immediate.ts index bdf48a9..081c9cf 100644 --- a/packages/core/src/core/renderer-immediate.ts +++ b/packages/core/src/core/renderer-immediate.ts @@ -35,6 +35,12 @@ const PROPS_OUTLINE_DEBUG_HOVER = { strokeWidth: 2 } as const; +type ClipRectProps = { + clip?: boolean; + dx?: number; + dy?: number; +}; + export interface RendererConfig { canvas?: HTMLCanvasElement; dx?: number; @@ -343,25 +349,10 @@ export class ImmediateRenderer { this.context.setOpacity(opacityParent * opacity); // Apply shadow if present - if (shadow) { - const { offsetX, offsetY, blur, color } = shadow; - const ctx = (this.context as any).ctx; - if (ctx) { - ctx.shadowOffsetX = offsetX; - ctx.shadowOffsetY = offsetY; - ctx.shadowBlur = blur; - ctx.shadowColor = color; - } - } + this.context.setShadow(shadow ?? null); // Apply CSS filter if present - const { filter } = props as any; - if (filter) { - const ctx = (this.context as any).ctx; - if (ctx) { - ctx.filter = filter; - } - } + this.context.setFilter(props.filter); // Render based on block type @@ -409,24 +400,13 @@ export class ImmediateRenderer { case BlockType.Layer: { // Apply blend mode for Layer blocks if (bl.type === BlockType.Layer) { - const { blendMode } = props as any; - if (blendMode) { - const ctx = (this.context as any).ctx as CanvasRenderingContext2D; - if (ctx) { - ctx.globalCompositeOperation = blendMode; - } - } + this.context.setBlendMode(bl.props.blendMode); } // Apply clipping if clip is set with dimensions - const { clip, dx: dxClip, dy: dyClip } = props as any; + const { clip, dx: dxClip, dy: dyClip } = props as ClipRectProps; if (clip && dxClip !== undefined && dyClip !== undefined) { - const ctx = (this.context as any).ctx as CanvasRenderingContext2D; - if (ctx) { - ctx.beginPath(); - ctx.rect(0, 0, dxClip, dyClip); - ctx.clip(); - } + this.context.clipRect(0, 0, dxClip, dyClip); } break; } @@ -657,7 +637,7 @@ export class ImmediateRenderer { // We need to set this as the current transform for rendering children // Directly set the transform stack's current to the stored transform - (this.context.transformStack as any).current = transform.clone(); + this.context.transformStack.setCurrent(transform.clone()); // Apply to canvas context (with pixelRatio) if (this.pixelRatio !== 1) { @@ -775,7 +755,7 @@ export class ImmediateRenderer { // Render tooltip as overlay (identity transform + pixelRatio) this.context.save(); this.context.transformStack.save(); - (this.context.transformStack as any).current = Matrix2D.identity(); + this.context.transformStack.setCurrent(Matrix2D.identity()); if (this.pixelRatio !== 1) { this.context.applyTransform(Matrix2D.identity().scaleXY(this.pixelRatio, this.pixelRatio)); diff --git a/packages/core/src/events.ts b/packages/core/src/events.ts index 0556ce5..cb39064 100644 --- a/packages/core/src/events.ts +++ b/packages/core/src/events.ts @@ -240,11 +240,23 @@ export class EventManager { /** * Find the nearest block with a tooltip function, checking the hit block first then ancestors. */ - private static findBlockWithTooltip(hit: HitTestResult): Block | null { - if ((hit.block.props as any).tooltip) return hit.block; + private static findBlockWithTooltip(hit: HitTestResult): { + block: Block; + tooltip: NonNullable; + } | null { + const hitTooltip = hit.block.props.tooltip; + if (hitTooltip) { + return { block: hit.block, tooltip: hitTooltip }; + } + for (let i = hit.ancestors.length - 1; i >= 0; i--) { - if ((hit.ancestors[i].props as any).tooltip) return hit.ancestors[i]; + const ancestor = hit.ancestors[i]; + const tooltip = ancestor.props.tooltip; + if (tooltip) { + return { block: ancestor, tooltip }; + } } + return null; } @@ -350,10 +362,10 @@ export class EventManager { } // Track tooltip — find nearest block with tooltip property - const tooltipBlock = hit ? EventManager.findBlockWithTooltip(hit) : null; - if (tooltipBlock) { + const tooltipResult = hit ? EventManager.findBlockWithTooltip(hit) : null; + if (tooltipResult) { this.activeTooltip = { - fn: (tooltipBlock.props as any).tooltip, + fn: tooltipResult.tooltip, xs: sceneCoords.x, ys: sceneCoords.y }; diff --git a/packages/core/src/transform.ts b/packages/core/src/transform.ts index 5931520..cdc4403 100644 --- a/packages/core/src/transform.ts +++ b/packages/core/src/transform.ts @@ -130,6 +130,10 @@ export class TransformStack { return this.xfCur; } + setCurrent(xf: Matrix2D): void { + this.xfCur = xf; + } + apply(transform: { x?: number; y?: number; diff --git a/packages/demo/demos/line-styles.ts b/packages/demo/demos/line-styles.ts index 6db6e3d..da699f8 100644 --- a/packages/demo/demos/line-styles.ts +++ b/packages/demo/demos/line-styles.ts @@ -2,6 +2,7 @@ // Line Styles & Blend Modes Demo — showcases Phase 1 Canvas features import { group, rectangle, text, circle, line, arc, path, layer, ellipse } from 'vitrine'; +import type { BlendMode } from 'vitrine'; export const demo = { id: 'line-styles', @@ -160,10 +161,10 @@ function starPath(cx: number, cy: number, outerR: number, innerR: number): strin } /** Render a blend mode comparison tile. */ -function renderBlendDemo(x: number, y: number, mode: string, col1: string, col2: string) { +function renderBlendDemo(x: number, y: number, mode: BlendMode, col1: string, col2: string) { return group({ x, y }, [ text({ x: 30, y: -2, text: mode, fontSize: 11, fill: '#666', align: 'center' as const, baseline: 'bottom' as const }), - layer({ blendMode: mode as any }, [ + layer({ blendMode: mode }, [ rectangle({ x: 0, y: 0, dx: 40, dy: 40, fill: col1 }), rectangle({ x: 20, y: 15, dx: 40, dy: 40, fill: col2 }) ]) diff --git a/packages/gui/src/GUI/slider/transformSlider.ts b/packages/gui/src/GUI/slider/transformSlider.ts index 47627fb..514e170 100644 --- a/packages/gui/src/GUI/slider/transformSlider.ts +++ b/packages/gui/src/GUI/slider/transformSlider.ts @@ -7,6 +7,29 @@ import { GUIControlType } from '../types.ts'; import { SLIDER_DEFAULTS } from './defaultsSlider.ts'; import { COMMON_DEFAULTS, getControlStyle } from '../constants.ts'; +type SliderDragState = { + fDragging: boolean; + xsStart: number; + startValue: number; +}; + +const mpSliderProp_sliderDragState = new WeakMap['props'], SliderDragState>(); + +function getOrCreateSliderDragState(props: GUIControlOfType['props']): SliderDragState { + const stateExisting = mpSliderProp_sliderDragState.get(props); + if (stateExisting) { + return stateExisting; + } + + const stateNew: SliderDragState = { + fDragging: false, + xsStart: 0, + startValue: 0 + }; + mpSliderProp_sliderDragState.set(props, stateNew); + return stateNew; +} + export function transformSlider( control: GUIControlOfType, context: TransformContext, @@ -143,11 +166,7 @@ export function transformSlider( onChange?.(newValue); }; - // Persistent drag state (stored on props to survive re-renders) - if (!(props as any)._dragState) { - (props as any)._dragState = { fDragging: false, xsStart: 0, startValue: 0 }; - } - const dragState = (props as any)._dragState; + const dragState = getOrCreateSliderDragState(props); const children: Block[] = []; From 817ef619d0feb1f74624dd874b11d96e66ee3c8d Mon Sep 17 00:00:00 2001 From: frouaix Date: Fri, 1 May 2026 19:41:20 -0700 Subject: [PATCH 2/2] CR feedback Co-authored-by: Copilot --- packages/core/src/core/renderer-immediate.ts | 24 ++++++++++---------- packages/core/src/core/types.ts | 9 ++++++-- packages/core/src/hit-test.ts | 12 ++++++---- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/packages/core/src/core/renderer-immediate.ts b/packages/core/src/core/renderer-immediate.ts index 081c9cf..6850471 100644 --- a/packages/core/src/core/renderer-immediate.ts +++ b/packages/core/src/core/renderer-immediate.ts @@ -35,12 +35,6 @@ const PROPS_OUTLINE_DEBUG_HOVER = { strokeWidth: 2 } as const; -type ClipRectProps = { - clip?: boolean; - dx?: number; - dy?: number; -}; - export interface RendererConfig { canvas?: HTMLCanvasElement; dx?: number; @@ -348,11 +342,15 @@ export class ImmediateRenderer { const opacityParent = this.context.opacity; this.context.setOpacity(opacityParent * opacity); - // Apply shadow if present - this.context.setShadow(shadow ?? null); + // Apply shadow only when explicitly set; otherwise inherit from parent save state. + if (shadow) { + this.context.setShadow(shadow); + } - // Apply CSS filter if present - this.context.setFilter(props.filter); + // Apply CSS filter only when explicitly set; otherwise inherit from parent save state. + if (props.filter !== undefined) { + this.context.setFilter(props.filter); + } // Render based on block type @@ -400,11 +398,13 @@ export class ImmediateRenderer { case BlockType.Layer: { // Apply blend mode for Layer blocks if (bl.type === BlockType.Layer) { - this.context.setBlendMode(bl.props.blendMode); + if (bl.props.blendMode !== undefined) { + this.context.setBlendMode(bl.props.blendMode); + } } // Apply clipping if clip is set with dimensions - const { clip, dx: dxClip, dy: dyClip } = props as ClipRectProps; + const { clip, dx: dxClip, dy: dyClip } = bl.props; if (clip && dxClip !== undefined && dyClip !== undefined) { this.context.clipRect(0, 0, dxClip, dyClip); } diff --git a/packages/core/src/core/types.ts b/packages/core/src/core/types.ts index d8fba73..d4a9cb5 100644 --- a/packages/core/src/core/types.ts +++ b/packages/core/src/core/types.ts @@ -258,11 +258,16 @@ export interface ArcProps extends BaseBlockProps, StrokeProps, FillProps { export type DrawArcStyleProps = DrawPathStyleProps; -export interface GroupProps extends BaseBlockProps { +export interface ClipProps { clip?: boolean; + dx?: number; + dy?: number; +} + +export interface GroupProps extends BaseBlockProps, ClipProps { } -export interface LayerProps extends BaseBlockProps { +export interface LayerProps extends BaseBlockProps, ClipProps { blendMode?: BlendMode; cache?: boolean; } diff --git a/packages/core/src/hit-test.ts b/packages/core/src/hit-test.ts index b241f93..73d96a8 100644 --- a/packages/core/src/hit-test.ts +++ b/packages/core/src/hit-test.ts @@ -51,11 +51,13 @@ export class HitTester { const ptl = inverse.transformPoint(xs, ys); - // Reject points outside clip region - const { clip, dx: dxClip, dy: dyClip } = props as { clip?: boolean; dx?: number; dy?: number }; - if (clip && dxClip !== undefined && dyClip !== undefined) { - if (ptl.x < 0 || ptl.x > dxClip || ptl.y < 0 || ptl.y > dyClip) { - return null; + // Reject points outside clip region for container blocks that support clipping. + if (bl.type === BlockType.Group || bl.type === BlockType.Layer) { + const { clip, dx: dxClip, dy: dyClip } = bl.props; + if (clip && dxClip !== undefined && dyClip !== undefined) { + if (ptl.x < 0 || ptl.x > dxClip || ptl.y < 0 || ptl.y > dyClip) { + return null; + } } }