Giter Site home page Giter Site logo

Comments (12)

maxsei avatar maxsei commented on June 2, 2024 1

@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.

postspectacular avatar postspectacular commented on June 2, 2024

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.

maxsei avatar maxsei commented on June 2, 2024

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)

Screenshot from 2023-10-19 17-32-15
Screenshot from 2023-10-19 17-32-31
Screenshot from 2023-10-19 17-55-08

from umbrella.

postspectacular avatar postspectacular commented on June 2, 2024

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.

maxsei avatar maxsei commented on June 2, 2024

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.

postspectacular avatar postspectacular commented on June 2, 2024

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.

postspectacular avatar postspectacular commented on June 2, 2024

@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.

maxsei avatar maxsei commented on June 2, 2024

@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;
  }

image
image

from umbrella.

postspectacular avatar postspectacular commented on June 2, 2024

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.

maxsei avatar maxsei commented on June 2, 2024

@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.

postspectacular avatar postspectacular commented on June 2, 2024

@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.

postspectacular avatar postspectacular commented on June 2, 2024

Thank you very much! And yes, fit23() is/will be useful! I extracted it from related functionality (fitIntoBounds2() in thi.ng/geom here):

const __translateScale = (
tmat: MatOpV,
smat: MatOpNV,
shape: IShape,
preTrans: ReadonlyVec,
postTrans: ReadonlyVec,
scale: ReadonlyVec | number
) =>
transform(
shape,
concat([], tmat([], postTrans), smat([], scale), tmat([], preTrans))
);
/**
* Uniformly rescales & repositions given 2D `shape` such that it fits into
* destination bounds. Returns transformed copy of `shape`.
*
* @param shape
* @param dest
*/
export const fitIntoBounds2 = (shape: IShape, dest: Rect) => {
const src = <Rect>bounds(shape);
if (!src) return;
const c = centroid(src);
if (!c) return;
return __translateScale(
translation23,
scale23,
shape,
neg(null, c),
centroid(dest)!,
minNonZero2(
safeDiv(dest.size[0], src.size[0]),
safeDiv(dest.size[1], src.size[1])
)
);
};

from umbrella.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.