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
4 changes: 2 additions & 2 deletions multi-platform/lynx-vrender/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const LIFECYCLE_MODES: Array<{ key: LifecycleMode; label: string }> = [

let activeController: ReturnType<typeof mountVRenderSmoke> | null = null;
let activeControllerSceneKey: SceneKey | null = null;
let requestedSceneKey: SceneKey = 'primitives';
let requestedSceneKey: SceneKey = 'tooltip';
let lastCanvasTouchAt = 0;
let canvasTouchActive = false;
let canvasMouseActive = false;
Expand Down Expand Up @@ -285,7 +285,7 @@ function getLifecycleModeLabel(mode: LifecycleMode) {
}

export function App() {
const [sceneKey, setSceneKey] = useState<SceneKey>('primitives');
const [sceneKey, setSceneKey] = useState<SceneKey>('tooltip');
const [lifecycleMode, setLifecycleMode] =
useState<LifecycleMode>('reuse-stage');
const [hasCanvasBridge, setHasCanvasBridge] = useState(
Expand Down
200 changes: 200 additions & 0 deletions multi-platform/lynx-vrender/src/smoke-scenes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const SCENES = [
{ key: 'states', label: '状态' },
{ key: 'transform', label: '变换' },
{ key: 'components', label: '组件' },
{ key: 'tooltip', label: 'Tooltip' },
{ key: 'baseline', label: '基准矩阵' },
{ key: 'events', label: '事件' },
{ key: 'stress', label: '批量' },
{ key: 'geometry', label: '几何' },
Expand Down Expand Up @@ -81,6 +83,14 @@ export const SCENE_META: Record<SceneKey, { title: string; hint: string }> = {
title: 'VRender Components',
hint: '首屏挂载 Tag、Segment、Axis、Legend、Slider、DataZoom、ScrollBar 和输入控件,失败组件显示 fallback。'
},
tooltip: {
title: 'Tooltip 文字对齐',
hint: '红色横线是内容行中心,应同时穿过 symbol 中心和文字视觉中心。'
},
baseline: {
title: 'Baseline Matrix(PROTOTYPE)',
hint: '10/16/24px × 五种 correction;横线是传给 alphabetic baseline 的原始 y。'
},
events: {
title: '事件拾取边界',
hint: 'touch 转发到 stage.window.dispatchEvent 后,验证重叠、zIndex、旋转组和裁剪子节点拾取。'
Expand Down Expand Up @@ -338,6 +348,10 @@ class LynxSmokeController {
this.drawTransformScene();
} else if (sceneKey === 'components') {
this.drawComponentsScene();
} else if (sceneKey === 'tooltip') {
this.drawTooltipScene();
} else if (sceneKey === 'baseline') {
this.drawBaselineMatrixScene();
} else if (sceneKey === 'events') {
this.drawEventsScene();
} else if (sceneKey === 'stress') {
Expand Down Expand Up @@ -1861,6 +1875,192 @@ class LynxSmokeController {
this.mountDeferredComponentControls();
}

private drawTooltipScene() {
const tooltip = new (componentCtor(Tooltip))({
x: 16,
y: 18,
visible: true,
autoMeasure: false,
autoCalculatePosition: false,
padding: [18, 20],
panel: {
visible: true,
width: 294,
height: 172,
fill: '#FFFFFF',
stroke: '#98A2B3',
lineWidth: 1,
cornerRadius: 10,
shadow: false
},
title: {
visible: true,
height: 30,
value: { text: 'Tooltip alignment' }
},
titleStyle: {
value: {
fill: '#101828',
fontFamily: 'Arial',
fontSize: 20,
lineHeight: 30,
textBaseline: 'middle'
},
spaceRow: 8
},
content: [
{
height: 18,
key: { text: 'Alpha' },
value: { text: '12' },
shape: { visible: true, fill: '#276EF1' }
},
{
height: 18,
key: { text: 'Beta' },
value: { text: '34' },
shape: { visible: true, fill: '#11A579' }
}
],
contentStyle: {
shape: { visible: true, size: 12, spacing: 12 },
key: {
fill: '#101828',
fontFamily: 'Arial',
fontSize: 18,
lineHeight: 18,
spacing: 52
},
value: {
fill: '#101828',
fontFamily: 'Arial',
fontSize: 18,
lineHeight: 18,
spacing: 0
},
spaceRow: 8
},
hasContentShape: true,
keyWidth: 65,
valueWidth: 24
});
this.stage.defaultLayer.add(tooltip);

for (const y of [83, 109]) {
this.stage.defaultLayer.add(
createLine({
points: [
{ x: 30, y },
{ x: 296, y }
],
stroke: '#D92D20',
lineWidth: 1,
lineDash: [4, 3],
zIndex: 501,
pickable: false
})
);
}
}

private drawBaselineMatrixScene() {
const context = this.stage.window.getContext();
const fontSizes = [10, 16, 24];
const columns = [112, 210, 308];
const variants = [
{ label: 'A none', offset: () => 0 },
{ label: 'B −0.5em', offset: (fontSize: number) => -0.5 * fontSize },
{
label: 'C −fontAsc',
offset: (fontSize: number, metrics: any) =>
-(metrics.fontBoundingBoxAscent ?? 0.79 * fontSize)
},
{ label: 'D −0.79em', offset: (fontSize: number) => -0.79 * fontSize },
{
label: 'E −actualAsc',
offset: (fontSize: number, metrics: any) =>
-(metrics.actualBoundingBoxAscent ?? 0.79 * fontSize)
}
];

this.stage.defaultLayer.add(
createText({
x: 18,
y: 24,
text: 'baseline matrix (PROTOTYPE)',
fontSize: 14,
fill: '#667085'
})
);

fontSizes.forEach((fontSize, index) => {
this.stage.defaultLayer.add(
createText({
x: columns[index],
y: 52,
text: `${fontSize}px`,
fontSize: 11,
fill: '#667085',
textAlign: 'center',
textBaseline: 'middle'
})
);
});

variants.forEach((variant, rowIndex) => {
const baselineY = 94 + rowIndex * 72;
this.stage.defaultLayer.add(
createText({
x: 16,
y: baselineY - 20,
text: variant.label,
fontSize: 10,
fill: '#475467'
})
);
this.stage.defaultLayer.add(
createLine({
points: [
{ x: 76, y: baselineY },
{ x: 348, y: baselineY }
],
stroke: '#98A2B3',
lineWidth: 1,
pickable: false
})
);

fontSizes.forEach((fontSize, columnIndex) => {
context.setTextStyleWithoutAlignBaseline({
fontFamily: 'Arial',
fontSize
});
const metrics = context.measureText('Hg') as any;
const x = columns[columnIndex];
this.stage.defaultLayer.add(
createCircle({
x,
y: baselineY,
radius: 3,
fill: '#D92D20',
pickable: false
})
);
this.stage.defaultLayer.add(
createText({
x: x + 8,
y: baselineY + variant.offset(fontSize, metrics),
text: 'Hg',
fontFamily: 'Arial',
fontSize,
fill: '#101828',
textBaseline: 'alphabetic'
})
);
});
});
}

private mountDeferredComponentControls() {
const nodes = this.componentNodes;
if (!nodes || nodes.deferredControlsMounted) {
Expand Down
32 changes: 32 additions & 0 deletions packages/vrender-kits/__tests__/unit/lynx-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { application } from '@visactor/vrender-core/application';
import { LynxContext2d } from '../../src/canvas/contributions/lynx/context';

test('lynx context normalizes device-scaled font metrics', () => {
const metrics = {
width: 80,
actualBoundingBoxAscent: 12,
actualBoundingBoxDescent: 4,
fontBoundingBoxAscent: 28,
fontBoundingBoxDescent: 10
};
const context = new LynxContext2d(
{
nativeCanvas: {
getContext: () => ({ measureText: () => metrics, setTransform: (): void => undefined })
}
} as any,
2
);
const global = application.global;
application.global = { devicePixelRatio: 1 } as any;

try {
expect(context.measureText('VRender', 'native')).toEqual({
...metrics,
fontBoundingBoxAscent: 14,
fontBoundingBoxDescent: 5
});
} finally {
application.global = global;
}
});
22 changes: 14 additions & 8 deletions packages/vrender-kits/src/canvas/contributions/lynx/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,21 @@ export class LynxContext2d extends BrowserContext2d implements IContext2d {
text: string,
method: 'native' | 'simple' | 'quick' = application.global.measureTextMethod
): { width: number } {
this.setTransform(1, 0, 0, 1, 0, 0, true, application.global.devicePixelRatio);
const data: any = super.measureText(text, method);
// lynx环境中的fontBoundingBoxDescent和fontBoundingBoxAscent有严重偏移,暂时规避
const dpr = this.dpr || 1;
this.setTransform(1, 0, 0, 1, 0, 0, true, dpr);
const metrics = super.measureText(text, method) as TextMetrics;
if (dpr === 1 || metrics.fontBoundingBoxAscent == null || metrics.fontBoundingBoxDescent == null) {
return metrics;
}
return {
width: data.width,
fontBoundingBoxDescent: undefined,
fontBoundingBoxAscent: undefined,
actualBoundingBoxAscent: undefined,
actualBoundingBoxDescent: undefined
...metrics,
width: metrics.width,
actualBoundingBoxLeft: metrics.actualBoundingBoxLeft,
actualBoundingBoxRight: metrics.actualBoundingBoxRight,
actualBoundingBoxAscent: metrics.actualBoundingBoxAscent,
actualBoundingBoxDescent: metrics.actualBoundingBoxDescent,
fontBoundingBoxAscent: metrics.fontBoundingBoxAscent / dpr,
fontBoundingBoxDescent: metrics.fontBoundingBoxDescent / dpr
} as any;
}

Expand Down
Loading