Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions packages/core/src/core/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Rendering context abstraction
import { Matrix2D, TransformStack } from '../transform.ts';
import type {
BlendMode,
DrawArcStyleProps,
DrawCircleStyleProps,
DrawImageStyleProps,
Expand All @@ -12,6 +13,7 @@ import type {
DrawTextStyleProps,
FillStyle,
LineStyleProps,
ShadowProps,
TextMeasure,
TextMeasureProps,
} from './types.ts';
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Comment thread
frouaix marked this conversation as resolved.

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);
Expand Down
42 changes: 11 additions & 31 deletions packages/core/src/core/renderer-immediate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,25 +342,14 @@ export class ImmediateRenderer {
const opacityParent = this.context.opacity;
this.context.setOpacity(opacityParent * opacity);

// Apply shadow if present
// Apply shadow only when explicitly set; otherwise inherit from parent save state.
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);
}

// Apply CSS filter if present
const { filter } = props as any;
if (filter) {
const ctx = (this.context as any).ctx;
if (ctx) {
ctx.filter = filter;
}
// Apply CSS filter only when explicitly set; otherwise inherit from parent save state.
if (props.filter !== undefined) {
this.context.setFilter(props.filter);
}


Expand Down Expand Up @@ -409,24 +398,15 @@ 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;
}
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 any;
const { clip, dx: dxClip, dy: dyClip } = bl.props;
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);
}
Comment thread
frouaix marked this conversation as resolved.
break;
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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));
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
24 changes: 18 additions & 6 deletions packages/core/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Block['props']['tooltip']>;
} | 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;
}

Expand Down Expand Up @@ -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
};
Expand Down
12 changes: 7 additions & 5 deletions packages/core/src/hit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ export class TransformStack {
return this.xfCur;
}

setCurrent(xf: Matrix2D): void {
this.xfCur = xf;
}

apply(transform: {
x?: number;
y?: number;
Expand Down
5 changes: 3 additions & 2 deletions packages/demo/demos/line-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 })
])
Expand Down
29 changes: 24 additions & 5 deletions packages/gui/src/GUI/slider/transformSlider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GUIControlOfType<GUIControlType.Slider>['props'], SliderDragState>();

function getOrCreateSliderDragState(props: GUIControlOfType<GUIControlType.Slider>['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<GUIControlType.Slider>,
context: TransformContext,
Expand Down Expand Up @@ -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[] = [];

Expand Down
Loading