Comments (12)
@postspectacular I'm stoked on the example and the new features, especially fit23. Also, feel welcome to use my words that is fine by me
from umbrella.
Yes, the transform matrix of a shape will be active until the shape is completely drawn, same way as it is for SVG shapes. Your example is creating a manual intervention/separation between submitting transformed points to the canvas and then painting them untransformed... For hiccup-canvas to support drawing nested groups of shapes and each shape potentially temporarily overriding attribs of its parent, canvas state (incl. stroke/fill/lineWidth etc.) is being saved/restored before & after completely processing a shape. I'm not sure how this separation you're after could be supported without breaking existing behavior (nor would I consider this expected behavior either). Is it not possible for you to apply the inverse scaling factor to the weight
attribute of the shape instead? I've never used non-uniform scaling of the canvas context, so I can't really tell how the canvas API handles those cases...
const scale = [2, 0.5];
const desiredLineWeight = 2;
polyline([[10,10], [20,30], [50,40]], { stroke: "red", scale, weight: desiredLineWeight/Math.max(...scale) })
Hope that explanation makes some sense. If not please let me know...
from umbrella.
Yes, I think that inverse scaling the weight of the line would be a reasonable assumption to make however in practice is inadequate.
In the below example I show three different values for the x component of the scaling vector. You can notice that as the scale of x increases the y component of the line weight becomes further from the desired weight.
import { polyline } from 'https://cdn.skypack.dev/@thi.ng/geom';
import { $canvas } from 'https://cdn.skypack.dev/@thi.ng/rdom-canvas';
import { $compile, $inputNum } from 'https://cdn.skypack.dev/@thi.ng/rdom';
import { reactive } from 'https://cdn.skypack.dev/@thi.ng/rstream';
const dims = [400, 100]
const desiredLineWeight = 5;
const scaleX = reactive(1)
const canvasBody = scaleX.map((scaleX)=> {
const scale = [scaleX, 0.5];
const weight = desiredLineWeight / Math.max(...scale)
const ret = polyline([
[10, 20],
[20, 60],
[50, 80],
[50, 100],
[60, 100],
], { __background: "#000", stroke: "red", scale, weight })
return ret
})
const root = document.getElementById("root")
$compile([
"div.w-100.h-100.pa4", {},
$canvas(canvasBody, dims),
["label", {}, "scaleX"],
["input", { type: "number", oninput: $inputNum(scaleX), value: scaleX }],
]).mount(root)
from umbrella.
Hmm... Shame, in that case I'm afraid you'll be left with creating pre-transformed geometry. But you don't have to do that for every single shape individually, just wrap all the ones which need that treatment and put them inside a group, then scale that one:
const a = group({}, [
circle(50),
polyline([[-100,0], [0,100], [100,100]])
])
// non-uniformly scale all of the group's children
const a2 = scale(a, [2, 0.5])
// original group as hiccup
a.toHiccup()
// [
// 'g',
// {},
// [ 'circle', undefined, [ 0, 0 ], 50 ],
// [ 'polyline', { fill: 'none' }, [ [ -100, 0 ], [ 0, 100 ], [ 100, 100 ] ] ]
// ]
// transformed group as hiccup
// here you can see that due to non-uniform scaling the circle had to be converted
// into an ellipse, some other shapes will be auto-converted too
a2.toHiccup()
// [
// 'g',
// {},
// [ 'ellipse', undefined, [ 0, 0 ], [ 100, 25 ] ],
// [ 'polyline', { fill: 'none' }, [ [ -200, 0 ], [ 0, 50 ], [ 200, 50 ] ] ]
// ]
(Some shape types will be automatically converted to other types prior to transformation because they cannot be reliably represented in their original type anymore.)
Hope that helps! Aside from that, I'd still be interested in learning more about your usecase/reason for the non-uniform scaling is...
from umbrella.
My use case is displaying streamed sparse multivariate time series in an interactive canvas similar to this. However, I have found that most of the out-of-the-box solutions are not efficient enough to handle the throughput and volume.
I was hoping that the transforms could be done by the browser for performance reasons because to my knowledge pre-transforming geometry requires materializing transformed points into a JavaScript arrays (unless done in place). I've been pre-transforming the geometry already in my project but have recently switched to using scene graphs to make my code easier to maintain. Here is how I have extended the scenegraphs abstract node for working with pre-transformed geometry
class PreTxNode2D<T extends ISceneNode<any>> extends ANode<T> {
constructor(
id: string,
parent: Nullable<T>,
mat: Vec,
body?: IShape<IShape<any>>
) {
super(id, parent, body);
this.mat = mat;
this.update();
}
copy() {
return new PreTxNode2D(this.id, this.parent, [...this.mat], this.body);
}
deleteChild(node: number | T): boolean {
return this._deleteChild(node, PreTxNode2D);
}
update() {
if (this.enabled) {
if (this.parent) {
mats.mulM23(this.mat, this.parent.mat, this.mat);
}
mats.invert23(this.invMat, this.mat);
for (let c of this.children) {
c.update();
}
}
}
mapGlobalPoint(p: ReadonlyVec): Vec | undefined {
return mats.mulV23([], this.invMat, p);
}
mapLocalPointToGlobal(p: ReadonlyVec): Vec | undefined {
return mats.mulV23([], this.mat, p);
}
mapLocalPointToNode(dest: T, p: ReadonlyVec): Vec | undefined {
return mats.mulV23(null, dest.invMat, mats.mulV23([], this.mat, p));
}
toHiccup(ctx: unknown) {
const body = isFunction(this.body) ? this.body(ctx) : deref(this.body);
return this.enabled && this.display
? this.children.length
? [
"g",
{},
this.body ? ["g", {}, g.transform(body, this.mat)] : undefined,
...this.children.map((c) => c.toHiccup(ctx)),
]
: body
? ["g", {}, g.transform(body, this.mat)]
: undefined
: undefined;
}
}
from umbrella.
Hi @maxsei - will look at your latest code later, but just wanted to mention a (potential) alternative solution/consideration: The thi.ng/vectors package allows creating memory mapped views of arbitrary strided typed array buffers which could be used to store your data and update it in-place. You could then also use the thi.ng/simd package to perform your re-scaling via WASM, both of these features are meant (and have been used) for similar use cases... Just a rough example of the overall approach:
import { asSvg, polyline } from "@thi.ng/geom";
import { init } from "@thi.ng/simd";
import { Vec2 } from "@thi.ng/vectors";
const simd = await init(new WebAssembly.Memory({ initial: 1 }));
// buffer view of WASM memory region, sufficient for 4 vec2
// IMPORTANT: for SIMD the number of resulting coordinates MUST be a multiple of 4!
const buf = simd.f32.subarray(0, 4*2);
// set coordinates
buf.set([1,10, 2,20, 3,-30, 4,40])
// create vec2 views
// see docs for more options to control striding setup (AOS vs SOA etc.)
// https://docs.thi.ng/umbrella/vectors/classes/Vec2.html#mapBuffer
const points=Vec2.mapBuffer(buf);
// wrap as shape
const plot = polyline(points);
// inspect
asSvg(plot)
// <polyline fill="none" points="1,10 2,20 3,-30 4,40"/>
// create buffer for scaling vector
// SIMD is always using vec4, so we need to duplicate the values here
// (IMPORTANT: make sure memory region is not overlapping with your point data!)
const scale = simd.f32.subarray(16, 16+4);
scale.set([2,0.5, 2,0.5]);
// scale the points as WASM batch process, scaling 4 floats at once
simd.mul4_f32(
// result address (here we want to modify in place)
buf.byteOffset,
// address of coordinates
buf.byteOffset,
// address of scaling vector
scale.byteOffset,
// number of vectors to process (as vec4!)
2,
// output stride (in floats, i.e. 4 = 1x vec4 = densely packed, no gaps)
4,
// stride of coordinates
4,
// since we only have a single scaling vector,
// we don't want progress the memory pointer for that one
0
);
// verify the polyline has been updated...
asSvg(plot)
// <polyline fill="none" points="2,5 4,10 6,-15 8,20"/>
from umbrella.
@maxsei Btw. If you go down the SIMD route and assuming the Y-coordinates (data points) are more frequently changing than the X-coordinates of your plot points (which might NOT be changing every frame), you can also arrange the data differently in memory, using an SOA (structure of arrays) layout: I.e. instead of storing point coordinates in interleaved X,Y,X,Y... order, you keep all the X's in one chunk of memory and the Y's in another. This then allows you to use the SIMD batch ops to only scale one or the other, but you'd have to change the Vec2.mapBuffer()
config like so:
const num = 8;
const buf = simd.f32.subarray(0, num*2);
buf.set([
// first all X coords
1,2,3,4,5,6,7,8,
// then all Y coords
10,20,-30,40,-50,-60,70,80
])
const points=Vec2.mapBuffer(
buf,
// num points
8,
// start index
0,
// vector component stride (here `num` floats between the X/Y components of each vector)
8,
// element stride (successive vectors are next to each other in memory)
1);
[...points[0]]
// [ 1, 10 ]
[...points[1]]
// [ 2, 20 ]
asSvg(polyline(points))
// <polyline fill="none" points="1,10 2,20 3,-30 4,40 5,-50 6,-60 7,70 8,80"/>
Btw. Alternatively, there're another 2 dedicated packages for this (not saying you should use those instead, but might be helpful for related aspects): https://thi.ng/soa, https://thi.ng/vector-pools
from umbrella.
@postspectacular the simd approach seems reasonable to me. Here is my attempt at using simd alongside the geom package's transform.
I'm not entirely sure why the simd transformed values are not displaying properly despite the values being approximately identical to the geom.transform
'd values. I also might be misunderstanding the memory management as well because occasionally the end of the transformed buffer appears to be uninitialized.
I've included the critical section of the code if you could point out any inconsistent usages of the simd api
const [_, series] = tx.pairs(features).next().value; // series is an Array of [number, number]
// SIMD Implementation
const buf = simd.f32.subarray(0, series.length * 2);
buf.set([...tx.flatten1(series)]);
const points = Vec2.mapBuffer(buf);
const lineSimd = g.polyline(points, { stroke: "#fff" });
const src = g.bounds(lineSimd)!;
const dst = g.rect([0, 0], dims);
const tsl = translation23([], sub2([], dst.pos, src.pos));
const scl = scaleWithCenter23([], dst.pos, safeDiv2([], dst.size, src.size));
const tfm = mulM23([], scl, tsl);
const tfmBuf = simd.f32.subarray(points.length + 8, points.length + 8 + tfm.length);
tfmBuf.set(tfm);
simd.mul_m23v2_aos(buf.byteOffset, tfmBuf.byteOffset, buf.byteOffset, series.length);
// Geom Implementation
{
const linePre = g.polyline(series, { stroke: "#fff" });
const src = g.bounds(linePre)!;
const dst = g.rect([0, 0], dims);
const tsl = translation23([], sub2([], dst.pos, src.pos));
const scl = scaleWithCenter23([], dst.pos, safeDiv2([], dst.size, src.size));
const tfm = mulM23([], scl, tsl);
const lineGeom = g.transform(linePre, tfm);
lineSimd;
debugger;
}
from umbrella.
Not having any of your test data and having updated your code to use yet another thi.ng package to help you with the manual memory management, I'm getting identical results for both cases:
Complete example:
import { polyline, bounds, rect, transform, asSvg } from "@thi.ng/geom";
import { translation23, scaleWithCenter23, mulM23 } from "@thi.ng/matrices";
import { MemPool } from "@thi.ng/malloc";
import { init } from "@thi.ng/simd";
import { flatten1 } from "@thi.ng/transducers";
import { Vec2, safeDiv2, sub2 } from "@thi.ng/vectors";
const dims = [640, 480];
const simd = init(new WebAssembly.Memory({ initial: 1 }))!;
// use memory allocator/manager to avoid having to deal with address juggling
const pool = new MemPool({
// use the WASM memory as backing buffer
buf: simd.memory.buffer,
// must be 16 for SIMD purposes (i.e. vec4 = 16 bytes)
align: 16,
});
const series = [
[1, 10],
[2, 20],
[3, -30],
[4, -40],
[5, 50],
[6, -66],
[7, 70],
[8, 80],
];
// SIMD Implementation
// allocate buffer via managed mem pool
const buf = pool.mallocAs("f32", series.length * 2)!;
buf.set([...flatten1<number>(series)]);
const points = Vec2.mapBuffer(buf);
const lineSimd = polyline(points, { stroke: "#fff" });
const src = bounds(lineSimd)!;
const dst = rect([0, 0], dims);
const tsl = translation23([], sub2([], dst.pos, src.pos));
const scl = scaleWithCenter23([], dst.pos, safeDiv2([], dst.size, src.size));
// allocate buffer via managed mem pool
const tfm = pool.mallocAs("f32", tsl.length)!;
// write result matrix directly into tfm
mulM23(tfm, scl, tsl);
simd.mul_m23v2_aos(
buf.byteOffset,
tfm.byteOffset,
buf.byteOffset,
series.length
);
{
// Geom Implementation
const linePre = polyline(series, { stroke: "#fff" });
const src = bounds(linePre)!;
const dst = rect([0, 0], dims);
const tsl = translation23([], sub2([], dst.pos, src.pos));
const scl = scaleWithCenter23(
[],
dst.pos,
safeDiv2([], dst.size, src.size)
);
const tfm = mulM23([], scl, tsl);
const lineGeom = transform(linePre, tfm);
console.log("simd", asSvg(lineSimd));
console.log("geom", asSvg(lineGeom));
}
// house keeping, free allocated memory after use (just like in C)
// (see https://thi.ng/malloc readme for details)
pool.free(buf);
pool.free(tfm);
Output:
simd <polyline fill="none" points="0,249.863 91.429,282.740 182.857,118.356 274.286,85.479 365.714,381.370 457.143,-0.000 548.571,447.123 640,480" stroke="#fff"/>
geom <polyline fill="none" points="0,249.863 91.429,282.740 182.857,118.356 274.286,85.479 365.714,381.370 457.143,0 548.571,447.123 640,480" stroke="#fff"/>
The only difference is that that negative-zero coordinate in the 3rd last point. This is due to SIMD only using f32 math (vs. f64 for normal JS numbers), but that's really negligible...
from umbrella.
@postspectacular After further inspection of my data (which I apologize for not including directly, but is generated from this linked gist), there does appear to be a loss of precision when transforming the data because the Y's are millisecond timestamps.
Despite this issue going somewhat off-topic, I hope that others might read this discourse and like I did, learn the distinction between transforming point geometry and using options provided for actually rendering the geometry (line width, stroke, etc).
I really appreciate your feedback and I find this package ecosystem a joy to use amongst the swath of complicated tools on the web
from umbrella.
@maxsei Thank you so much for the kind words! I too enjoyed that issue very much, since it finally also triggered me to deal with some of these related issues. I ended up refactoring the example into a new #HowToThing example, incl. adding some new small features for the hiccup-canvas and matrices packages...
Ps. I might move this issue over to discussions, since it might be of interest to more people...
Pps. May I use that last sentence of your last reply as a quote on the thi.ng website (at some point in the near future)? Let me know please...
from umbrella.
Thank you very much! And yes, fit23()
is/will be useful! I extracted it from related functionality (fitIntoBounds2()
in thi.ng/geom here):
umbrella/packages/geom/src/fit-into-bounds.ts
Lines 21 to 57 in dc7eeab
from umbrella.
Related Issues (20)
- [shader-ast] Dependency for ternary operators, while and for loop HOT 1
- [shader-ast] Array texture types are missing HOT 1
- [shader-ast] Missing matrix types HOT 1
- [shader-ast] Better design for `defn` with anonymous qualified arguments HOT 7
- [malloc] Multi-threaded allocations HOT 4
- Prototype Pollution vulnerability affecting @thi.ng/paths, versions <=5.1.62 HOT 7
- [hiccup-canvas] "packedPolyline" needs separate implementation or packedPoly needs to accept stride options?
- [color] ? [binary] Comparison with -0 using the "===" operator will also match 0 [equals-negative-zero] HOT 2
- RDMS based on rstream-query HOT 1
- [examples] Issue running the examples (failed to resolve entry for package) HOT 3
- [hiccup-svg] hiccup-svg's attributes don't seem to handle rstream HOT 7
- [rstream] error when using skypack build HOT 5
- [geom-accel] SpatialGrid2 hangs runtime when using `set` method HOT 3
- Polygons with holes? HOT 8
- [shader-ast] "Building a shader graph editor (WebGL, shader AST transpiler, UI)" HOT 2
- [rdom] Issue understanding $klist HOT 4
- [geom] update polymorphic function signatures to provide better return types
- [atom & rstream] Asking for advice on good practices
- [rdom] SVG el add class > Uncaught (in promise) TypeError HOT 1
- [rdom] Error: illegal state: operation not allowed in state 3 HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from umbrella.