Skip to content

Commit 4cc1689

Browse files
britalmeidafsiddi
authored andcommitted
Timeline & Renderer: add 'views' for panning and zooming
A View is an area in pixels with its own transform (offset and scale). Shapes are added to a view in the base coordinate space. The renderer will transform the shapes according to the current zoom and pan and it will clip shapes that fall on the boundary of the view area. Renderer: - Add support for 'views' which get pushed as state. - When there is a view, shapes will get transformed by it. - Scale and offset are indenpendent in X and Y. Uniform distances (corner and line width) get scaled by the smallest independent scale. TODO: maybe these shouldn't get scaled at all? Optional? - Shapes get accurately clipped in the fragment shader. (new command) - AddPrimitveShape can now return false and prevent emitting a shape. - Pass viewport size to the shader, not just height. - Remove unused MVP. Timeline: - Add an horizontal zoom area for the timeline range. Height is not affected. - Custom view logic was needed to handle: - text rendering, because that is rendered by a separate 2D canvas. - playhead, which should have a pos set by the view, but not the geo. - playhead click interaction. - Interaction controls are MMB to pan, scroll to zoom and a button to fit. More to follow.
1 parent 7a36e4d commit 4cc1689

File tree

3 files changed

+142
-37
lines changed

3 files changed

+142
-37
lines changed

glsl/fragment.glsl

+21-4
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ const int CMD_TRIANGLE = 2;
88
const int CMD_RECT = 3;
99
const int CMD_FRAME = 4;
1010
const int CMD_IMAGE = 5;
11+
const int CMD_CLIP = 9;
12+
1113

1214
// Inputs
13-
uniform float viewport_height;
15+
uniform vec2 viewport_size;
1416
uniform int num_cmds;
1517
uniform sampler2D cmd_data;
1618

@@ -107,14 +109,16 @@ void main() {
107109
// OpenGL provides the fragment coordinate in pixels where (0,0) is bottom-left.
108110
// Because the renderer and client UI code has (0,0) top-left, flip the y.
109111
// Use pixel top-left coordinates instead of center. (0.5, 0.5) -> (0.0, 0.0)
110-
vec2 frag_coord = vec2(gl_FragCoord.x, viewport_height - gl_FragCoord.y);
112+
vec2 frag_coord = vec2(gl_FragCoord.x, viewport_size.y - gl_FragCoord.y);
111113
frag_coord -= 0.5;
112114

113115
// Default fragment background color and opacity.
114116
// Will be overwritten if this pixel is determined to be inside shapes.
115117
vec3 px_color = vec3(0.18, 0.18, 0.18);
116118
float px_alpha = 1.0;
117119

120+
vec4 view_clip_rect = vec4(0, 0, viewport_size.x, viewport_size.y);
121+
118122
// Process the commands with the procedural shape definitions in order.
119123
// Check if this pixel is inside (or partially inside) each shape and update its color.
120124
int data_idx = 0;
@@ -124,15 +128,28 @@ void main() {
124128
int cmd_type = int(cmd[0]);
125129
int style_idx = int(cmd[1]);
126130

131+
if (cmd_type == CMD_CLIP) {
132+
view_clip_rect = get_cmd_data(data_idx++);
133+
continue;
134+
}
135+
127136
vec4 style = get_style_data(style_idx);
128137
float line_width = style[0];
129138
float corner_radius = style[1];
130139
vec4 shape_color = get_style_data(style_idx+1);
131140
vec4 shape_bounds = get_cmd_data(data_idx++);
132141

142+
// Get the intersection of the shape bounds and the clip rect.
143+
vec4 clip_rect = vec4(
144+
max(shape_bounds.x, view_clip_rect.x),
145+
max(shape_bounds.y, view_clip_rect.y),
146+
min(shape_bounds.z, view_clip_rect.z),
147+
min(shape_bounds.w, view_clip_rect.w)
148+
);
149+
// Check if this pixel is within the area where it may draw.
133150
vec2 clip_clamp = vec2(
134-
clamp(frag_coord.x, shape_bounds.x, shape_bounds.z),
135-
clamp(frag_coord.y, shape_bounds.y, shape_bounds.w));
151+
clamp(frag_coord.x, clip_rect.x, clip_rect.z),
152+
clamp(frag_coord.y, clip_rect.y, clip_rect.w));
136153
// clip_dist: 0 = not clipped,
137154
// ]0,1[ sub-pixel clipping (shape is not aligned pixel perfect)
138155
// [1,...[ clipped

glsl/vertex.glsl

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
#version 300 es
22

33
in vec2 v_pos;
4-
uniform mat4 mvp;
54

65
void main() {
7-
gl_Position = mvp * vec4(v_pos, 0.0, 1.0);
6+
gl_Position = vec4(v_pos, 0.0, 1.0);
87
}

lib/shading.js

+120-31
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,47 @@ export function Rect (x, y, w, h) {
3737
}
3838

3939

40+
export function View (x, y, w, h, scale, offset) {
41+
this.left = x;
42+
this.right = x + w;
43+
this.top = y;
44+
this.bottom = y + h;
45+
this.width = w;
46+
this.height = h;
47+
this.scaleX = scale[0];
48+
this.scaleY = scale[1];
49+
this.offsetX = offset[0];
50+
this.offsetY = offset[1];
51+
52+
this.transformPosX = function(p) {
53+
return (p - this.left - this.offsetX) * this.scaleX + this.left;
54+
}
55+
56+
this.transformPosY = function(p) {
57+
return (p - this.top - this.offsetY) * this.scaleY + this.top;
58+
}
59+
60+
this.transformDistX = function(d) {
61+
return d * this.scaleX;
62+
}
63+
64+
this.transformDistY = function(d) {
65+
return d * this.scaleY;
66+
}
67+
68+
this.transformRect = function(r) {
69+
return new Rect(
70+
this.transformPosX(r.left),
71+
this.transformPosY(r.top),
72+
this.transformDistX(r.width),
73+
this.transformDistY(r.height));
74+
}
75+
this.getXYScale = function() {
76+
return Math.min(this.scaleX, this.scaleY);
77+
}
78+
}
79+
80+
4081
export function UIRenderer(canvas, redrawCallback) {
4182

4283
// Rendering context
@@ -50,10 +91,7 @@ export function UIRenderer(canvas, redrawCallback) {
5091
this.redrawCallback = redrawCallback;
5192

5293
// Viewport transform
53-
this.transform = [ 1, 0, 0, 0,
54-
0, 1, 0, 0,
55-
0, 0, 1, 0,
56-
0, 0, 0, 1 ];
94+
this.views = [];
5795

5896
// Shader data
5997
this.shaderInfo = {};
@@ -72,6 +110,7 @@ export function UIRenderer(canvas, redrawCallback) {
72110
const CMD_RECT = 3;
73111
const CMD_FRAME = 4;
74112
const CMD_IMAGE = 5;
113+
const CMD_CLIP = 9;
75114

76115
// Style
77116
this.styleDataStartIdx = (MAX_CMD_DATA - MAX_STYLE_CMDS) * 4; // Start writing style to the last cmd data texture line.
@@ -99,32 +138,38 @@ export function UIRenderer(canvas, redrawCallback) {
99138
let bounds = new Rect(p1[0], p1[1], 0, 0);
100139
bounds.encapsulate(p2);
101140
bounds.widen(Math.round(width * 0.5 + 0.01));
102-
let w = this.addPrimitiveShape(CMD_LINE, bounds, color, width, null);
103-
// Data 3 - Shape parameters
104-
this.cmdData[w++] = p1[0];
105-
this.cmdData[w++] = p1[1];
106-
this.cmdData[w++] = p2[0];
107-
this.cmdData[w++] = p2[1];
108-
109-
this.cmdDataIdx = w;
141+
if (this.addPrimitiveShape(CMD_LINE, bounds, color, width, null)) {
142+
let w = this.cmdDataIdx;
143+
const v = this.getView();
144+
// Data 3 - Shape parameters
145+
this.cmdData[w++] = v ? v.transformPosX(p1[0]) : p1[0];
146+
this.cmdData[w++] = v ? v.transformPosY(p1[1]) : p1[1];
147+
this.cmdData[w++] = v ? v.transformPosX(p2[0]) : p2[0];
148+
this.cmdData[w++] = v ? v.transformPosY(p2[1]) : p2[1];
149+
150+
this.cmdDataIdx = w;
151+
}
110152
}
111153

112154
this.addTriangle = function (p1, p2, p3, color) {
113155
let bounds = new Rect(p1[0], p1[1], 0, 0);
114156
bounds.encapsulate(p2);
115157
bounds.encapsulate(p3);
116-
let w = this.addPrimitiveShape(CMD_TRIANGLE, bounds, color, null, null);
117-
// Data 3 - Shape parameters
118-
this.cmdData[w++] = p1[0];
119-
this.cmdData[w++] = p1[1];
120-
this.cmdData[w++] = p2[0];
121-
this.cmdData[w++] = p2[1];
122-
// Data 4 - Shape parameters II
123-
this.cmdData[w++] = p3[0];
124-
this.cmdData[w++] = p3[1];
125-
w+=2;
126-
127-
this.cmdDataIdx = w;
158+
if (this.addPrimitiveShape(CMD_TRIANGLE, bounds, color, null, null)) {
159+
let w = this.cmdDataIdx;
160+
const v = this.getView();
161+
// Data 3 - Shape parameters
162+
this.cmdData[w++] = v ? v.transformPosX(p1[0]) : p1[0];
163+
this.cmdData[w++] = v ? v.transformPosY(p1[1]) : p1[1];
164+
this.cmdData[w++] = v ? v.transformPosX(p2[0]) : p2[0];
165+
this.cmdData[w++] = v ? v.transformPosY(p2[1]) : p2[1];
166+
// Data 4 - Shape parameters II
167+
this.cmdData[w++] = v ? v.transformPosX(p3[0]) : p3[0];
168+
this.cmdData[w++] = v ? v.transformPosY(p3[1]) : p3[1];
169+
w += 2;
170+
171+
this.cmdDataIdx = w;
172+
}
128173
}
129174

130175
this.addCircle = function (p1, radius, color) {
@@ -194,16 +239,29 @@ export function UIRenderer(canvas, redrawCallback) {
194239
}
195240

196241
this.addPrimitiveShape = function (cmdType, bounds, color, lineWidth, corner) {
242+
243+
const v = this.getView();
244+
bounds = v ? v.transformRect(bounds) : bounds;
245+
246+
// Clip bounds.
247+
if (v &&
248+
(bounds.right < v.left || bounds.left > v.right
249+
|| bounds.bottom < v.top || bounds.top > v.bottom)) {
250+
return false;
251+
}
252+
253+
corner = v ? corner * v.getXYScale() : corner;
254+
lineWidth = v ? lineWidth * v.getXYScale() : lineWidth;
255+
197256
let w = this.cmdDataIdx;
198257
// Check for at least 4 free command slots as that's the maximum a shape might need.
199258
if (w/4 + 4 > MAX_SHAPE_CMDS) {
200259
console.warn("Too many shapes to draw.", w/4 + 4, "of", MAX_SHAPE_CMDS);
201-
// Overwrite the start of the command buffer.
202-
return 0;
260+
return false;
203261
}
204262

205263
// Check for a change of style and push a new style if needed.
206-
if (!this.stateColor.every((v, i) => v === color[i]) // Is color array different?
264+
if (!this.stateColor.every((c, i) => c === color[i]) // Is color array different?
207265
|| (lineWidth !== null && this.stateLineWidth !== lineWidth) // Is line width used for this shape and different?
208266
|| (corner !== null && this.stateCorner !== corner)
209267
) {
@@ -244,6 +302,20 @@ export function UIRenderer(canvas, redrawCallback) {
244302
return w;
245303
}
246304

305+
this.addClipRect = function (left, top, right, bottom) {
306+
// Write clip rect information for the shader.
307+
let w = this.cmdDataIdx;
308+
// Data 0 - Header
309+
this.cmdData[w++] = CMD_CLIP;
310+
w += 3;
311+
// Data 1 - Bounds
312+
this.cmdData[w++] = left;
313+
this.cmdData[w++] = top;
314+
this.cmdData[w++] = right;
315+
this.cmdData[w++] = bottom;
316+
this.cmdDataIdx = w;
317+
}
318+
247319
// Create a GPU texture object (returns the ID, usable immediately) and
248320
// asynchronously load the image data from the given url onto it.
249321
this.loadImage = function (url) {
@@ -327,6 +399,24 @@ export function UIRenderer(canvas, redrawCallback) {
327399
return textureID;
328400
}
329401

402+
this.getView = function() {
403+
return this.views.length ? this.views[this.views.length - 1] : null;
404+
}
405+
406+
this.pushView = function(x, y, w, h, scale, offset) {
407+
const view = new View(x, y, w, h, scale, offset);
408+
this.views.push(view);
409+
this.addClipRect(x +1, y +1, x + w -1, y + h -1);
410+
return view;
411+
}
412+
413+
this.popView = function() {
414+
this.views.pop();
415+
const v = this.getView();
416+
if (v) { this.addClipRect(v.left, v.top, v.right, v.bottom); }
417+
else { this.addClipRect(0, 0, this.gl.canvas.width, this.gl.canvas.height); }
418+
}
419+
330420
// Draw a frame with the current primitive commands.
331421
this.draw = function() {
332422
const gl = this.gl;
@@ -340,8 +430,7 @@ export function UIRenderer(canvas, redrawCallback) {
340430
gl.invalidateFramebuffer(gl.FRAMEBUFFER, [gl.COLOR]);
341431

342432
// Set the transform.
343-
gl.uniformMatrix4fv(this.shaderInfo.uniforms.modelViewProj, false, this.transform);
344-
gl.uniform1f(this.shaderInfo.uniforms.vpHeight, gl.canvas.height);
433+
gl.uniform2f(this.shaderInfo.uniforms.vpSize, gl.canvas.width, gl.canvas.height);
345434

346435
// Bind the vertex data for the shader to use and specify how to interpret it.
347436
// The shader works as a full size rect, new coordinates don't need to be set per frame.
@@ -413,6 +502,7 @@ export function UIRenderer(canvas, redrawCallback) {
413502
this.textureIDs = [];
414503
this.textureBundleIDs = [];
415504
// Clear the state.
505+
this.views = [];
416506
this.stateColor = [-1, -1, -1, -1];
417507
// Clear the style list.
418508
this.styleDataIdx = this.styleDataStartIdx;
@@ -445,8 +535,7 @@ export function UIRenderer(canvas, redrawCallback) {
445535
vertexPos: bind_attr(gl, shaderProgram, 'v_pos'),
446536
},
447537
uniforms: {
448-
modelViewProj: bind_uniform(gl, shaderProgram, 'mvp'),
449-
vpHeight: bind_uniform(gl, shaderProgram, 'viewport_height'),
538+
vpSize: bind_uniform(gl, shaderProgram, 'viewport_size'),
450539
numCmds: bind_uniform(gl, shaderProgram, 'num_cmds'),
451540
cmdBufferTex: bind_uniform(gl, shaderProgram, 'cmd_data'),
452541
samplers: [

0 commit comments

Comments
 (0)