vega-scenegraph
Version:
Vega scenegraph and renderers.
206 lines (173 loc) • 5.52 kB
JavaScript
import {hasCornerRadius, rectangle} from '../path/shapes';
import boundStroke from '../bound/boundStroke';
import {intersectRect} from '../util/intersect';
import value from '../util/value';
import {pickVisit, visit} from '../util/visit';
import blend from '../util/canvas/blend';
import {clipGroup} from '../util/canvas/clip';
import fill from '../util/canvas/fill';
import stroke from '../util/canvas/stroke';
import {hitPath} from '../util/canvas/pick';
import clip from '../util/svg/clip';
import {translateItem} from '../util/svg/transform';
function offset(item) {
const sw = value(item.strokeWidth, 1);
return item.strokeOffset != null ? item.strokeOffset
: item.stroke && sw > 0.5 && sw < 1.5 ? 0.5 - Math.abs(sw - 1)
: 0;
}
function attr(emit, item) {
emit('transform', translateItem(item));
}
function emitRectangle(emit, item) {
const off = offset(item);
emit('d', rectangle(null, item, off, off));
}
function background(emit, item) {
emit('class', 'background');
emit('aria-hidden', true);
emitRectangle(emit, item);
}
function foreground(emit, item) {
emit('class', 'foreground');
emit('aria-hidden', true);
if (item.strokeForeground) {
emitRectangle(emit, item);
} else {
emit('d', '');
}
}
function content(emit, item, renderer) {
const url = item.clip ? clip(renderer, item, item) : null;
emit('clip-path', url);
}
function bound(bounds, group) {
if (!group.clip && group.items) {
const items = group.items, m = items.length;
for (let j=0; j<m; ++j) {
bounds.union(items[j].bounds);
}
}
if ((group.clip || group.width || group.height) && !group.noBound) {
bounds.add(0, 0).add(group.width || 0, group.height || 0);
}
boundStroke(bounds, group);
return bounds.translate(group.x || 0, group.y || 0);
}
function rectanglePath(context, group, x, y) {
const off = offset(group);
context.beginPath();
rectangle(context, group, (x || 0) + off, (y || 0) + off);
}
const hitBackground = hitPath(rectanglePath);
const hitForeground = hitPath(rectanglePath, false);
const hitCorner = hitPath(rectanglePath, true);
function draw(context, scene, bounds, markTypes) {
visit(scene, group => {
const gx = group.x || 0,
gy = group.y || 0,
fore = group.strokeForeground,
opacity = group.opacity == null ? 1 : group.opacity;
// draw group background
if ((group.stroke || group.fill) && opacity) {
rectanglePath(context, group, gx, gy);
blend(context, group);
if (group.fill && fill(context, group, opacity)) {
context.fill();
}
if (group.stroke && !fore && stroke(context, group, opacity)) {
context.stroke();
}
}
// setup graphics context, set clip and bounds
context.save();
context.translate(gx, gy);
if (group.clip) clipGroup(context, group);
if (bounds) bounds.translate(-gx, -gy);
// draw group contents
visit(group, item => {
if (item.marktype === 'group' || markTypes == null || markTypes.includes(item.marktype)) {
this.draw(context, item, bounds, markTypes);
}
});
// restore graphics context
if (bounds) bounds.translate(gx, gy);
context.restore();
// draw group foreground
if (fore && group.stroke && opacity) {
rectanglePath(context, group, gx, gy);
blend(context, group);
if (stroke(context, group, opacity)) {
context.stroke();
}
}
});
}
function pick(context, scene, x, y, gx, gy) {
if (scene.bounds && !scene.bounds.contains(gx, gy) || !scene.items) {
return null;
}
const cx = x * context.pixelRatio,
cy = y * context.pixelRatio;
return pickVisit(scene, group => {
let hit, dx, dy;
// first hit test bounding box
const b = group.bounds;
if (b && !b.contains(gx, gy)) return;
// passed bounds check, test rectangular clip
dx = group.x || 0;
dy = group.y || 0;
const dw = dx + (group.width || 0),
dh = dy + (group.height || 0),
c = group.clip;
if (c && (gx < dx || gx > dw || gy < dy || gy > dh)) return;
// adjust coordinate system
context.save();
context.translate(dx, dy);
dx = gx - dx;
dy = gy - dy;
// test background for rounded corner clip
if (c && hasCornerRadius(group) && !hitCorner(context, group, cx, cy)) {
context.restore();
return null;
}
const fore = group.strokeForeground,
ix = scene.interactive !== false;
// hit test against group foreground
if (ix && fore && group.stroke
&& hitForeground(context, group, cx, cy)) {
context.restore();
return group;
}
// hit test against contained marks
hit = pickVisit(group, mark => pickMark(mark, dx, dy)
? this.pick(mark, x, y, dx, dy)
: null
);
// hit test against group background
if (!hit && ix && (group.fill || (!fore && group.stroke))
&& hitBackground(context, group, cx, cy)) {
hit = group;
}
// restore state and return
context.restore();
return hit || null;
});
}
function pickMark(mark, x, y) {
return (mark.interactive !== false || mark.marktype === 'group')
&& mark.bounds && mark.bounds.contains(x, y);
}
export default {
type: 'group',
tag: 'g',
nested: false,
attr: attr,
bound: bound,
draw: draw,
pick: pick,
isect: intersectRect,
content: content,
background: background,
foreground: foreground
};