@charming-art/charming
v0.0.9
Published
The declarative language for computational art with high performance.
Downloads
6
Readme
Charming: Charming Computing
Charming is still in testing; the APIs are not fully implemented and may not be stable.
Charming, short for Charming Computing, is a free, open-source creative coding language designed for computational and ASCII art, offering high performance. It has a declarative, concise, yet expressive API inspired by G2.js, D3.js and P5.js.
Charming is built on the observation that both visualization and generative art are, to some extent, data-driven. Therefore, it provides a novel flow-based API for processing data, appending and transforming shapes. Charming also supports batch rendering of 2D primitives using a WebGL renderer, and defining some GLSL functions to offload expensive computations to the GPU. Additionally, a terminal renderer for ASCII art, embedded in JavaScript, utilizes a software rasterizer written in Rust and compiled to WASM, aiming to achieve high performance. Charming also puts strong emphasis on extensible, composable, beginner-friendly and lightweight (29kb minified core bundle).
If you are new to programming or JavaScript, P5 is still a good starting point, otherwise you should consider Charming. My hope with Charming is that you spend less time wrangling the machinery of programming and more time "using computing to tell stories". Or put more simply: With Charming, you'll express more, and more easily.
D3 is the GOAT for visualization, let's make Charming the D3 of computational art.
Links
If you are new to Charming, I highly recommend first reading following links to get started with:
- What is Charming - a brief introduction
- Why Charming - inspiration
- How is Charming - features
And there are a plenty of examples to learn from, as well as API reference and core concepts:
- App - rendering app to DOM and animating it
- Flow - binding data to shapes
- Process - preparing data to be rendered
- Shape - appending geometric elements to canvas
- Transform - deriving shape attribute values
- Scale - mapping abstract data to visual representation
- Event - handling hooks and events
- Prop - returning properties of the app
- Attribute - defining attributes for shapes
If you want to have a comprehensive understanding of Charming, such as design choice, discussion and future work, you can skip links above and read Charming: Charming Computing directly.
Installing
Charming is typically installed via a package manager such as Yarn or NPM.
yarn add @charming-art/charming
npm install @charming-art/charming
Charming can then imported as a namespace:
import * as cm from "@charming-art/charming";
In vanilla HTML, Charming can be imported as an ES module, say from jsDelivr:
<script type="module">
import * as cm from "https://cdn.jsdelivr.net/npm/@charming-art/charming/+esm";
const app = cm.app();
// ...
document.body.append(app.render().node());
</script>
Charming is also available as a UMD bundle for legacy browsers.
<script src="https://cdn.jsdelivr.net/npm/@charming-art/charming"></script>
<script>
const app = cm.app();
// ...
document.body.append(app.render().node());
</script>
Quick Examples
import * as cm from "@charming-art/charming";
const app = cm.app({
width: 640,
height: 640,
});
app
.data(cm.range(240))
.process(cm.map, (_, i, data) => (i * Math.PI) / data.length)
.append(cm.circle, {
x: (t) => Math.cos(t) * Math.cos(t * 3),
y: (t) => Math.sin(t) * Math.cos(t * 3),
r: (_, i) => i,
})
.transform(cm.mapPosition, { padding: 15 })
.transform(cm.mapAttrs, {
r: { range: [8, 20] },
});
document.body.appendChild(app.render().node());
import * as cm from "@charming-art/charming";
const app = cm.app({
width: 1200,
renderer: await cm.terminal(),
});
app.append(cm.text, {
text: cm.figlet("hello world"),
x: app.prop("width") / 2,
y: app.prop("height") / 2,
fill: cm.gradientSineBowX(),
textAlign: "center",
textBaseline: "middle",
fontFamily: cm.fontGhost(),
});
document.body.appendChild(app.render().node(()));
API Reference
The core modules of Charming, which are included in core bundle:
- App - rendering app to DOM and animating it
- Flow - binding data to shapes
- Process - preparing data to be rendered
- Shape - appending geometric elements to canvas
- Transform - deriving shape attribute values
- Scale - mapping abstract data to visual representation
- Event - handling hooks and events
- Prop - returning properties of the app
- Attribute - defining attributes for shapes
The other modules are included in full bundle. Different renderers and related modules will be placed in the following separate modules:
For common problems, Charming provides a series of optional modules encapsulate reusable solutions, increasing efficiency and alleviating the burden of common tasks:
- Array - array generation and manipulation
- Math - processing numbers, randomness, etc.
- Constant - useful constants
- Vector - basics for simulating physical laws
- Helper - useful unities
App
Rendering app to DOM and animating it.
# cm.app([options])
Constructs a new app with the specified options. If no argument is specified, constructs with default options.
All the apps support the following options:
- width - the outer width of the app, number in pixels
- height - the outer height of the app, number in pixels
- frameRate - the number of frames to draw per second
- renderer - the renderer to draw shapes and handle events, defaults to canvas renderer
const app = cm.app({
width: 600,
height: 400,
renderer: cm.canvas(),
});
Apps with terminal renderer support the extra options:
- cols - the number of columns, with a priority level higher than the width
- rows - the number of rows, with a priority level higher than the height
- fontSize - the font size used to render text, see CSS font-size
- fontWeight - the font weight used to render text, see CSS font-weight
- fontFamily - the font family used to render text, see CSS font-family
- mode - the render mode, single or double, defaults to single
const app = cm.app({
cols: 30,
rows: 30,
renderer: await cm.terminal(),
fontSize: 20,
fontWeight: "bold",
fontFamily: "Georgia, serif",
mode: "double",
});
If mode is single, a cell in terminal renders both single-width and double-width character once . If mode is double, a cell in terminal renders single-width character twice and double-width character once.
Single-width characters include characters like A, a, 1, @, etc,. Double-width characters are characters include characters like 中, 🚀 and strings made of two single-width characters like AB. Double mode aims to address the overlapping issues that arise from the inconsistent widths of single-width and double-width characters.
# app.data(data)
Appends a new flow with the specified array of data to the root flow of app, returning the new flow.
app.data([1, 2, 3]);
# app.datum([datum])
Appends a new flow with an array containing the specified datum to the root flow app, returning the new flow.
app.datum(1);
The shorthand is thus equivalent to:
app.data([1]);
If no argument is specified, return groups of this app:
app.datum(); // [[1]]
# app.append(shape[, options])
Appends a shape with the specified options to this app, returning the new flow that contains the shape. Each shape has its own options, and different shape types support different options. See the respective shape type for details.
app.append(cm.circle, { x: 100, y: 100, r: 50, fill: "orange" });
# app.render()
Renders shapes in flows to canvas and removes existing flows, returning this app.
app.append(cm.circle, {
x: cm.random(50, 100),
y: cm.random(50, 100),
r: 25,
fill: "orange",
});
app.render();
app.append(cm.circle, {
x: cm.random(50, 100),
y: cm.random(50, 100),
r: 25,
fill: "steelblue",
});
app.render();
# app.start()
Starts this app and returns it, firing update event repeatedly until calling app.stop. This allows this app to invoke the update callback every delay milliseconds, which is controlled by the frameCount option. Note that app.render will be invoked automatically at the end of each frame, so there is no need to call it explicitly. For example, to draw a moving rect:
let x = 0;
function update() {
app.append(cm.rect, {
x: x++,
y: 0,
width: 100,
height: 50,
});
}
app.on("update", update);
app.start();
# app.stop()
Stops this app and returns it, cancelling firing update, thereby stops the animation.
app.on("update", update);
app.start();
// Stops animation after 5 seconds.
setTimeout(() => app.stop(), 5000);
# app.dispose()
Disposes this app and returns it, stopping the timer to firing update event and removing all event listeners.
app.on("update", update);
app.on("mouseMove", mouseMove);
app.dispose();
# app.node()
Returns the canvas element for drawing shapes.
document.body.append(app.node());
# app.prop(name)
Returns the property with the specified name for this app. See the respective property name for details.
app.prop("width"); // 640
# app.on(type, listener)
Adds a listener for the specified event type. Multiple listeners can be registered to receive the same event.
function mouseMove() {}
function mouseMove1() {}
app.on("mouseMove", mouseMove).on("mouseMove", mouseMove1);
See the respective event type for details.
# app.call(callback[, ...argument])
Calls the specified function on this app with any optional arguments and returns this app. This is equivalent to calling the function by hand but avoids to break method chaining. For example, to draw two concentric circles in a reusable function:
function ring(app, { x, y, r, r1, fill, fill1 }) {
app.append(cm.circle, { x, y, r, fill });
app.append(cm.circle, { x, y, r1, fill2 });
}
ring(app, {
x: 100,
y: 100,
r: 25,
r1: 50,
fill: "orange",
fill1: "steelblue",
});
Instead of invoking this function directly on app, now say:
app.call(ring, {
x: 100,
y: 100,
r: 25,
r1: 50,
fill: "orange",
fill2: "steelblue",
});
# app.textBBox(text, textOptions)
Computes the bounding box for the specified textOptions. The returned bounding box has the following properties:
- x - the x coordinate of the text
- y - the y coordinate of the text
- width - the width of the text
- height - the height of the text
const bbox = app.textBBox({
text: "hello world",
fontSize: 20,
fontWeight: "bold",
});
# cm.canvas()
Constructs a canvas renderer, drawing shapes with CanvasRenderingContext2D. It is the default renderer for app and there is no need to specify it explicitly.
const app = cm.app({
height: 200,
renderer: cm.canvas(), // not necessary
});
app.append(cm.circle, {
x: 100,
y: 100,
r: 50,
fill: "orange",
});
app.render();
Flow
Binding data to shapes.
# flow.data([data])
Returns a new flow that contains this specified data. The data is specified for each group in this flow.
If the specified data is an array of arbitrary values(e.g. number of objects), sets [data] as the group of this flow.
const group = app.append(cm.group, {
width: app.prop("width") / 3,
height: app.prop("heigh") / 3,
});
group.data([1, 2, 3]);
If the flow has multiple groups(such as flow.data followed by app.data), then data should typically be specified as a function. The function will be evaluated for each group in order, being passed the group's parent datum(d), the group index(i), all the groups(data) and this flow(flow).
app.data(matrix).data((d, i, data, flow) => {});
For example, to draw a matrix of characters in a terminal:
const matrix = [
[" +", "-", "+ "],
[" |", cm.wch("🚀"), "| "],
[" +", "-", "+ "],
];
app
.data(matrix)
.append(cm.group, { y: (_, i) => i })
.data((d) => d)
.append(cm.point, {
y: 0,
x: (_, i) => i,
stroke: (d) => cm.cfb(d),
});
If no argument is specified, return groups of this app:
app.data(); // [...]
# flow.datum(value)
Like flow.data, except receives a value instead of an array of data.
flow.datum(1);
// Equivalent
flow.data([1]);
# flow.process(process, options)
Processes the data of this flow with the specified process function receiving the specified options, returning a flow with the processed data. It provides a convenient mechanism to manipulate data before calling flow.append to bind it with shapes.
For example, to draw a particle system with two shape types:
const groups = app
.data(particles)
.process(cm.push, createParticle)
.process(cm.eachRight, removeDied)
.process(cm.each, decay)
.process(cm.each, move);
groups.process(cm.filter, isCircle).append(cm.circle, {});
groups.process(cm.filter, isSquare).append(cm.rect, {});
See the respective process function for details.
# flow.append(shape[, options])
Appends and binds shapes with the data of this flow, returning a flow with shapes. Shapes are created by the specified shape function with the specified options.
Shape function interprets attribute values and invokes the renderer of this flow to draw shapes. See the respective shape function for details. Each shape's options are typically specified as a object with corresponding attribute name.
For each attribute, if the value is constant, all the shapes are given the same attribute value; otherwise, if the value is a function, it is evaluated for each datum, in order, being passed the current datum(d), the current index(i), the data(data) and this flow(flow). The function's return value is then used to set each shapes' attribute.
const flow = app.data([1, 2, 3]);
flow.append(cm.circle, {
x: (d) => d * 100,
y: (d) => d * 100,
fill: "red",
});
# flow.transform(transform, options)
Transforms shapes' attribute values with the specified transform function receiving the specified options, returning a flow with the transformed attribute values. It provides a convenient mechanism to manipulate attribute values after calling flow.append to binding data with shapes.
For example, to map abstract values produced by Math.sin into visual values, drawing a sine wave:
app
.data(cm.range(50, cm.TWO_PI))
.append(cm.circle, {
x: (d) => d,
y: (d) => Math.sin(d),
r: 20,
fill: "rgba(175, 175, 175, 0.5)",
stroke: "#000",
strokeWidth: 1,
})
.transform(cm.mapPosition);
See the respective transform function for details.
# flow.call(callback[, ...argument])
Like app.call, except calls on this flow.
function scaleRadius(flow) {
flow.transform(cm.mapAttrs, {
r: { range: [10, 15] },
});
}
app
.data([1, 2, 3, 4])
.append(cm.circle, {
x: 100,
y: 100,
r: (d) => d,
fill: "steelblue",
})
.call(scaleRadius);
# flow.app()
Returns this app. It helps define some pure functions relaying some properties of this app.
function scale(d, i, data, flow) {
const app = flow.app();
const width = app.prop("width");
return d * width;
}
app.data([0.1, 0.2, 0.3]).process(cm.map, scale);
Process
Preparing data to be rendered.
# cm.each
Calls the specified function on each datum of a flow, and returns a new flow that contains the data. The function is being passed the current datum(d), the current index(i), the current group(data) and the flow(flow).
const data = [
{ name: "Locke", number: 4 },
{ name: "Reyes", number: 8 },
{ name: "Ford", number: 15 },
{ name: "Jarrah", number: 16 },
{ name: "Shephard", number: 23 },
{ name: "Kwon", number: 42 },
];
app.data(data).process(cm.each, (d) => d.number * 2);
# cm.eachRight
Like cm.each, except iterates from right to left.
const data = [
{ name: "Locke", number: 4 },
{ name: "Reyes", number: 8 },
{ name: "Ford", number: 15 },
{ name: "Jarrah", number: 16 },
{ name: "Shephard", number: 23 },
{ name: "Kwon", number: 42 },
];
app.data(data).process(cm.eachRight, (d, i, data) => {
if (d.number > 30) data.splice(i, 1);
});
# cm.filter
Calls the specified function on each datum of a flow, and returns a new flow that contains the data meeting true condition. The function is being passed the current datum(d), the current index(i), the current group(data) and the flow(flow).
const data = [
{ name: "Locke", number: 4 },
{ name: "Reyes", number: 8 },
{ name: "Ford", number: 15 },
{ name: "Jarrah", number: 16 },
{ name: "Shephard", number: 23 },
{ name: "Kwon", number: 42 },
];
app.data(data).process(cm.filter, (d) => d.number % 2 === 0);
# cm.map
Calls the specified function on each datum of a flow, and returns a new flow that contains the new data. The function is being passed the current datum(d), the current index(i), the current group(data) and the flow(flow).
const data = [
{ name: "Locke", number: 4 },
{ name: "Reyes", number: 8 },
{ name: "Ford", number: 15 },
{ name: "Jarrah", number: 16 },
{ name: "Shephard", number: 23 },
{ name: "Kwon", number: 42 },
];
app.data(data).process(cm.map, (d) => ({ ...d, number: d.number * 2 }));
# cm.derive
Calls value on each datum of a flow to derive a new field key for each entries of the specified object, and returns a new flow that contains the new data. The value is being passed the current datum(d), the current index(i), the current group(data) and the flow(flow).
const data = [
{ name: "Locke", number: 4 },
{ name: "Reyes", number: 8 },
{ name: "Ford", number: 15 },
{ name: "Jarrah", number: 16 },
{ name: "Shephard", number: 23 },
{ name: "Kwon", number: 42 },
];
app.data(data).process(cm.derive, {
double: (d) => d.name * 2,
upper: (d) => d.name.toUpperCase(),
});
# cm.push
Appends the specified datum to a flow, and returns a new flow that contains the new datum.
If datum is not a function, appends it to all groups.
const data = [
{ name: "Locke", number: 4 },
{ name: "Reyes", number: 8 },
{ name: "Ford", number: 15 },
{ name: "Jarrah", number: 16 },
{ name: "Shephard", number: 23 },
{ name: "Kwon", number: 42 },
];
app.data(data).process(cm.push, { name: "Jim", number: 25 });
If the flow has multiple groups(such as flow.data followed by app.data), then datum should typically be specified as a function. The function will be evaluated for each group in order, being passed the group(group), the group index(i), all the groups(data) and this flow(flow).
const matrix = [
[" +", "-", "+ "],
[" |", cm.wch("🚀"), "| "],
[" +", "-", "+ "],
];
app
.data(matrix)
.append(cm.group, { y: (_, i) => i })
.data((d) => d)
.process(cm.push, (group, i, groups) => ` ${i}`);
Shape
Appending geometric elements to canvas, most of shapes support the following attributes:
- fill - the fill color
- fillOpacity - fill opacity (a number between 0 and 1)
- stroke - stroke color
- strokeWidth - stroke width (in pixels)
- strokeOpacity - stroke opacity (a number between 0 and 1)
# cm.point
Appends dots positioned in x and y. In addition to the standard shape attributes, the following attributes are supported:
- x - the horizontal position, in pixels or in cells
- y - the vertical position, in pixels or in cells
app.append(cm.point, { x: 10, y: 10 });
# cm.link
Appends straight lines between two points [x, y] and [x1, y1]. In addition to the standard shape attributes, the following attributes are supported:
- x - the starting horizontal position, in pixels or in cells
- y - the starting vertical position, in pixels or in cells
- x1 - the ending horizontal position, in pixels or in cells
- y1 - the ending vertical position, in pixels or in cells
- rotate - the rotation angle in degrees clockwise
- strokeCap - the shape of end points, defaults to butt; round and square
- transformOrigin - the position of the origin point for rotation, defaults to start; center and end
app.append(cm.link, { x: 0, y: 0, x1: 100, y1: 100 });
# cm.rect
Appends rectangles defined by x, y, width and height. In addition to the standard shape attributes, the following attributes are supported:
- x - the horizontal position, in pixels or cells
- y - the vertical position, in pixels or cells
- width - the rectangle width, in pixels or cells
- height - the rectangle height, in pixels or cells
- anchor - how to position the rectangle, defaults to start; center
- rotate - the rotation angle in degrees clockwise
app.append(cm.rect, { x: 10, y: 10, width: 50, height: 40 });
# cm.circle
Appends circles positioned in x and y. In addition to the standard shape attributes, the following attributes are supported:
- x - the horizontal position, in pixels or cells
- y - the vertical position, in pixels or cells
- r - the circle radius, in pixels or cells
app.append(cm.circle, { x: 50, y: 50, r: 30 });
# cm.triangle
Appends triangles defined by x, y, x1, y1, x2, y2. In addition to the standard shape attributes, the following attributes are supported:
- x - the first horizontal position, in pixels or cells
- y - the first vertical position, in pixels or cells
- x1 - the second horizontal position, in pixels or cells
- y1 - the second vertical position, in pixels or cells
- x2 - the third horizontal position, in pixels or cells
- y2 - the third vertical position, in pixels or cells
app.append(cm.triangle, { x: 0, y: 0, x1: 10, y1: 0, x2: 10, y2: 10 });
# cm.polygon
Appends polygons defined by x and y. In addition to the standard shape attributes, the following attributes are supported:
- x - the horizontal positions of control points, in pixels or cells clockwise
- y - the vertical positions of control points, in pixels or cells clockwise
If appends one polygon defined by a column of x and a of y, assigns columns to its x and y attribute respectively.
app.append(cm.polygon, {
x: [0, 10, 10],
y: [0, 0, 10],
});
If only appends one polygon defined by an array of points [x, y] or objects, assigns accessors return number to its x and y attribute respectively.
const polygon = [
[0, 0],
[10, 0],
[10, 10],
];
app.data(polygon).append(cm.polygon, {
d => d[0],
d => d[1],
})
If appends multiple polygons defined by an array of objects, assigns accessors returns an array of numbers to its x and y attribute respectively.
const polygons = [
{ X: [0, 10, 10], Y: [0, 0, 10] },
{ X: [20, 30, 30], Y: [0, 0, 10] },
];
app.data(polygons).append(cm.polygon, {
d => d.X,
d => d.Y,
})
# cm.line
Appends draw two-dimensional lines defined by x and y. In addition to the standard shape attributes, the following attributes are supported:
- x - the horizontal positions of control points, in pixels or cells
- y - the vertical positions of control points, in pixels or cells
If appends one line defined by a column of x and a of y, assigns columns to its x and y attribute respectively.
app.append(cm.line, {
x: [0, 10, 20],
y: [10, 5, 15],
});
If only appends one line defined by an array of points [x, y] or objects, assigns accessors return number to its x and y attribute respectively.
const line = [
[0, 10],
[10, 5],
[20, 15],
];
app.data(line).append(cm.line, {
d => d[0],
d => d[1],
})
If appends multiple lines defined by an array of objects, assigns accessors returns an array of numbers to its x and y attribute respectively.
const lines = [
{ X: [0, 10, 20], Y: [10, 5, 15] },
{ X: [20, 40, 35], Y: [10, 5, 15] },
];
app.data(lines).append(cm.line, {
d => d.X,
d => d.Y,
})
# cm.path
Appends path defined by d. In addition to the standard shape attributes, the following attributes are supported:
- d: an array or string of path commands
If appends one path, assigns the specified path commands to its d attribute.
app.append(cm.path, {
d: [["M", 0, 0], ["L", 10, 0], ["L", 10, 10], ["Z"]],
});
If appends multiple paths, assigns accessors returns an array of path commands to its d attribute.
const paths = [
[["M", 0, 0], ["L", 10, 0], ["L", 10, 10], ["Z"]],
[["M", 10, 0], ["L", 20, 10], ["L", 20, 10], ["Z"]],
];
app.data(paths).append(cm.path, { d: (d) => d });
# cm.text
Appends texts at given position in x and y. In addition to the standard shape attributes, the following attributes are supported:
- x - the horizontal position
- y - the vertical position
- text - the text contents (a string)
- fontSize - the font size in pixels; defaults to 10
- fontFamily - the font name
- fontWeight - the font weight, defaults to normal
- textBaseline - the line anchor for vertical position; top, bottom, or middle
- textAlign - the text align for horizontal position; start, end, or middle
# cm.group
Appends groups at given position in x and y. In addition to the standard shape attributes, the following attributes are supported:
- y - the horizontal position
- x - the vertical position
- rotate - the rotation angle in degrees clockwise
All the child shapes are applied translate(x, y) and rotate(rotate) transformations.
const group = app.group({
width: 100,
height: 100,
});
group.append(cm.point, {
x: 0,
y: 0,
r: 10,
});
# cm.clear
Appends clear shape to clear the canvas background with the specified color. The following attributes are supported:
- fill - the clear color
# function(flow, value)
Defines a composite shape by a function, passing the current flow(flow) and attribute value(value).
// Defines a composite shape.
function arrow(flow, { length, angle, x, y, rotate, ...options }) {
const group = flow.append(cm.group, { x, y, rotate });
const l1 = length.map((d) => d / 2);
const l2 = length.map((d) => -d / 2);
const a1 = angle.map((d) => d);
const a2 = angle.map((d) => -d);
group.append(cm.link, { x: l2, y: 0, x1: l1, y1: 0, ...options });
group.append(cm.link, { x: 0, y: 0, x1: l1, y1: 0, rotate: a2, transformOrigin: "end", ...options });
group.append(cm.link, { x: 0, y: 0, x1: l1, y1: 0, rotate: a1, transformOrigin: "end", ...options });
}
// Uses a composite shape.
app
.data(fields)
.append(arrow, {
x: (d) => d.x * size + size / 2,
y: (d) => d.y * size + size / 2,
length: size * 0.8,
angle: Math.PI / 6,
rotate: (d) => d.value,
})
.transform(cm.mapAttrs, {
rotate: {
domain: [0, 1],
range: [0, cm.TWO_PI],
},
});
Transform
Deriving shape attribute values.
# cm.mapAttrs
Maps abstract attributes to visual attributes with scales. Each scale's options are specified as a nested options object with the corresponding attribute name.
app
.append(cm.circle, {
x: (d) => d[0],
y: (d) => d[1],
})
.transform(cm.mapAttrs, {
x: {}, // scale for x attribute
y: {}, // scale for y attribute
});
A scale's domain is typically inferred automatically. It can be customized explicitly by these options:
- scale - scale, defaults to scaleLinear
- domain - abstract values, typically [min, max]
- range - visual values, typically [min, max]
app
.append(cm.circle, {
x: (d) => d[0],
y: (d) => d[1],
})
.transform(cm.mapAttrs, {
x: {
scale: cm.scaleLog,
range: [0, app.prop("height")],
},
});
# cm.mapPosition
Map abstract position to visual position. Like mapAttrs, but only maps position attributes to corresponding dimension range.
For x attributes, such as x and x1, the scale's range is [0, app.prop("width")] by default. For y attributes, such as y and y1, the scale's range is [0, app.prop("height")] by default.
- scaleX - scale for x attributes, defaults to scaleLinear
- scaleY - scale for y attributes, defaults to scaleLinear
- domainX - abstract values for x attributes, typically [min, max]
- domainY - abstract values for y attributes, typically [min, max]
- reverseX - reverses range for x attributes, defaults to false
- reverseY - reverses range for y attributes, defaults to false
- padding - space between shapes and border, defaults to 0
app
.append(cm.line, {
x: (d) => d[0],
y: (d) => d[0],
})
.transform(cm.mapPosition, {
scaleX: cm.scaleLog,
reverseY: true,
padding: 15,
});
Scale
Mapping abstract data to visual representation.
# cm.scaleLinear(domain, range)
Constructs a new linear scale with the specified domain and range. Linear scales map a continuous, quantitative to a continuous output using a linear transformation.
const scale = cm.scaleLinear([0, 1], [0, 100]);
scale(0); // 0;
scale(0.5); // 50;
scale(1); // 100;
# cm.scaleSqrt(domain, range)
Constructs a new sqrt scale with the specified domain and range. Sqrt scales are similar to linear scale, except a square root transform is applied to the input domain value before the output range is computed.
const scale = cm.scaleSqrt([0, 1], [0, 100]);
scale(0.09); // 30
scale(0.64); // 80
scale(0.81); // 90
# cm.scaleLog(domain, range)
Constructs a new log scale with the specified domain and range. Log scales are similar to linear scale, except a logarithmic transform transform is applied to the input domain value before the output range is computed.
const scale = cm.scaleLog([1, 10], [0, 960]);
# cm.scaleOrdinal(domain, range)
Constructs a new ordinal scale with the specified domain and range. Unlike linear scale, ordinal scales have a discrete domain and range. Given a value in the input domain, returns the corresponding in the output range.
const scale = cm.scaleOrdinal(["A", "B", "C"], ["steelblue", "yellow", "red"]);
scale("A"); // "steelblue"
scale("B"); // "yellow"
scale("C"); // "red"
Event
Handling hooks and events.
# app.on("update", callback)
The update event is fired repeatedly until app.stop is called after calling app.start. For example, to draw a moving rect:
let x = 0;
function update() {
app.append(cm.rect, {
x: x++,
y: 0,
width: 100,
height: 50,
});
}
app.on("update", update);
# app.on("mouseDown", callback)
The mouseDown event is fired when a mouse button is pressed. For example, to change the background color:
let background = "red";
function mouseDown() {
background = "blue";
}
app.on("mouseDown", mouseDown);
# app.on("mouseUp", callback)
The mouseUp event is fired when a mouse button is released. For example, to change the background color:
let background = "red";
function mouseUp() {
background = "blue";
}
app.on("mouseUp", mouseUp);
# app.on("mouseClick", callback)
The mouseClick event is fired when a mouse button is clicked. For example, to change the background color:
let background = "red";
function mouseClick() {
background = "blue";
}
app.on("mouseClick", mouseClick);
# app.on("beforeEach", callback)
The beforeEach event is fired before each update event is fired. For example, to begin measuring frame rate by stats.js:
function measure(app) {
// ...
app.on("beforeEach", () => {
stats.begin();
});
}
app.call(measure);
# app.on("afterEach", callback)
The afterEach event is fired after each update event is fired. For example, to end measuring frame rate by stats.js:
function measure(app) {
// ...
app.on("afterEach", () => {
stats.end();
});
}
app.call(measure);
# app.on("beforeAll", callback)
The beforeAll event is fired before calling app.start. For example, to construct a Stats instance from stats.js:
function measure(app) {
let stats;
//...
app.on("beforeAll", () => {
const container = document.getElementById("tool");
stats = new Stats();
stats.dom.style.position = "inherit";
stats.dom.style.marginLeft = "1em";
container.appendChild(stats.dom);
});
}
app.call(measure);
# app.on("afterAll", callback)
The afterAll event is fired after calling app.dispose. For example, to remove the DOM of a Stats instance from stats.js:
function measure(app) {
let stats;
//...
app.on("afterAll", () => {
stats.dom.remove();
});
}
app.call(measure);
Prop
Returning properties of the app.
# app.prop("width")
If the renderer is not terminal, returns the width of this app in pixel.
const app = cm.app();
app.prop("width"); // 640;
If the renderer is terminal, returns the width of this app in cell.
const app = cm.app({ renderer: await cm.terminal() });
app.prop("width"); // 71
# app.prop("height")
If the renderer is not terminal, returns the height of this app in pixel.
const app = cm.app();
app.prop("height"); // 480;
If the renderer is terminal, returns the height of this app in cell.
const app = cm.app({ renderer: await cm.terminal() });
app.prop("height"); // 26
# app.prop("frameCount")
Returns the number of frames that have been displayed since this app started. For example, to draw a moving rect:
app.on("update", () =>
app.append(cm.rect, {
x: app.frameCount(),
y: 0,
width: 10,
height: 10,
}),
);
# app.prop("frameCount")
Returns the number of frames to be displayed per second.
app.prop("frameRate"); // 60
# app.prop("mouseX")
Returns the x coordinate of the mouse position.
app.prop("mouseX"); // 0
# app.prop("mouseY")
Returns the y coordinate of the mouse position.
app.prop("mouseY"); // 0
# app.prop("mode")
Returns the rendering mode of this app, which is only for app with a terminal renderer.
const app = cm.app({ renderer: await cm.terminal() });
app.prop("mode"); // "single"
# app.prop("pixelWidth")
Returns the computed width of this app in pixel, which is only for app with a terminal renderer.
const app = cm.app({ renderer: await cm.terminal() });
app.prop("pixelWidth"); // 639
# app.prop("pixelHeight")
Returns the computed height of this app in pixel, which is only for app with a terminal renderer.
const app = cm.app({ renderer: await cm.terminal() });
app.prop("pixelHeight"); // 468;
# app.prop("cellWidth")
Returns the computed width of the cells in pixel, which is only for app with a terminal renderer.
const app = cm.app({ renderer: await cm.terminal() });
app.prop("cellWidth"); // 9
# app.prop("cellHeight")
Returns the computed height of the cells in pixel, which is only for app with a terminal renderer.
const app = cm.app({ renderer: await cm.terminal() });
app.prop("cellHeight"); // 18
# app.prop("fontSize")
Returns the font size used to render text, which is only for app with a terminal renderer.
const app = cm.app({ renderer: await cm.terminal() });
app.prop("fontSize"); // 15
# app.prop("fontFamily")
Returns the font family used to render text, which is only for app with a terminal renderer.
const app = cm.app({ renderer: await cm.terminal() });
app.prop("fontFamily"); // "courier-new, courier, monospace"
# app.prop("fontWeight")
Returns the font weight used to render text, which is only for app with a terminal renderer.
const app = cm.app({ renderer: await cm.terminal() });
app.prop("fontWeight"); // "normal"
Attribute
Defining Attributes for shapes.
# cm.rgb(r[, g[, b]])
Returns a string representing the RGB color according to the CSS Object Model specification.
cm.rgb(234, 260, 180); // 'rgb(234, 260, 180)'
If only on argument is specified, sets all channels to the same value.
cm.rgb(100); // 'rgb(100, 100, 100)'
# cm.hsl(h[, s[, l]])
Returns a string representing the HSL color according to the CSS Object Model specification.
cm.hsl(234, 50, 50); // 'hsl(234, 50%, 50%)'
# cm.constant(value)
Defines a attribute with the specified constant value, which is useful for defining some non-data-driven options for composite shape.
app.data(["A", "B", "C"]).append(bar, {
x: (d) => d,
y: (_, i) => i,
axis: cm.constant(false),
colors: cm.constant(["yellow", "red"]),
});
function bar(
flow,
{
x, // ["A", "B", "C"]
y, // [0, 1, 2]
axis, // false
colors, // ["yellow", "red"]
},
) {
// ...
}
WebGL
WebGL renderer and related helpers.
# cm.terminal()
Returns a WebGL renderer, rendering shapes by WebGL.
const app = cm.app({
renderer: cm.webgl(),
});
# cm.glsl
Defines a attribute with the specified GLSL function through template literals. The name of the function should match the assigned attribute, being passed the current datum(d), returning value with corresponding type.
const r = cm.glsl`float r(float theta) {
float d = 0.2 + 0.12 * cos(theta * 9.0 - 2.0);
return d * 300.0;
}`;
app.data(theta).append(cm.circle, { r });
Some numbers can also be interpolated:
const scale = 300;
const width = 640;
const height = 480;
const position = cm.glsl`vec2 position(float theta) {
vec2 xy = vec2(
cos(theta),
sin(theta)) * (0.6 + 0.2 * cos(theta * 6.0 + cos(theta * 8.0 + ${time}))
);
return xy * ${scale} + vec2(${width / 2}, ${height / 2});
}`;
app.data(theta).append(cm.circle, { position });
Terminal
Terminal renderer and related helpers.
# cm.terminal()
Returns a promise resolved to a terminal renderer, drawing shapes in a terminal like context.
const app = cm.app({
renderer: await cm.terminal(),
});
Shapes drawn by terminal renders are (typically) not positioned in literal pixels, or colored in literal colors, as in a conventional graphics system. Instead they are positioned in count of terminal's cell and colored by characters.
const app = cm.app({
mode: "double",
renderer: await cm.terminal(),
});
app
.data(cm.range(240, 0, Math.PI * 2))
.append(cm.group, {
x: app.prop("width") / 2,
y: app.prop("height") / 2,
})
.append(cm.point, {
x: (t) => 10 * Math.cos(t) * Math.cos(t * 3),
y: (t) => 10 * Math.sin(t) * Math.cos(t * 3),
stroke: cm.cfb(cm.wch("🌟")),
});
app.render();
Moreover, it draws ASCII text powered by figlet.js.
const app = cm.app({
width: 800,
height: 200,
mode: "double",
renderer: await cm.terminal(),
});
app.append(cm.text, {
x: app.prop("width") / 2,
y: app.prop("height") / 2,
text: "charming",
textBaseline: "middle",
textAlign: "center",
});
app.render();
# cm.cfb(ch[, f[, b]])
Returns a terminal color object, which is only for app with a terminal renderer. A terminal color comprises the following three channels:
- ch: character
- f: CSS Color for the color of the character
- b: CSS color for the background cell of the character
If neither f or b are not specified, each defaults to null.
app.append(cm.rect, {
x: 0,
y: 0,
width: 10,
height: 5,
fill: cm.cfb("@", "steelblue", "orange"),
stroke: cm.cfg("+"),
});
# cm.wch(ch)
Returns a character marked as a wide character, which is only for app with a terminal in double mode.
const app = cm.app({
terminal: await cm.terminal(),
mode: "double",
});
app.append(cm.rect, {
x: 0,
y: 0,
width: 10,
height: 5,
fill: cm.cfb(cm.wch("😊")),
});
# cm.figlet(text)
Defines a figlet text with the specified text for terminal renderer.
const app = cm.app({
renderer: await cm.terminal(),
});
app.append(cm.text, {
text: cm.figlet("Charming"),
});
# cm.fontStandard()
Parses and returns the standard font for the fontFamily attribute.
app.append(cm.text, {
// ...
fontFamily: cm.fontStandard(),
});
# cm.fontGhost()
Parses and returns the ghost font for the fontFamily attribute.
app.append(cm.text, {
// ...
fontFamily: cm.fontGhost(),
});
# cm.gradientRainBowX()
Returns the fill attribute with the vertical rainbow gradient.
app.append(cm.text, {
// ...
fill: cm.gradientRainBowX(),
});
# cm.gradientSineBowX()
Returns the fill attribute with the vertical sinebox gradient.
app.append(cm.text, {
// ...
fill: cm.gradientRainBowX(),
});
Array
Array generation and manipulation.
# cm.range(count[, start[, end]])
Returns an array of exactly count uniformly-spaced values between start and end. If start is not specified, it defaults to 0. If end is not specified, it defaults to 1.
cm.range(10); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
cm.range(10, 5); // [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5]
cm.range(10, 5, 55); // [5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
# cm.cross(...arrays)
Returns the Cartesian product of the specified arrays.
cm.cross([1, 2, 3], [1, 2]); // [[1, 1], [1, 2], [2, 1], [2, 2], [3, 1], [3, 2]]
# cm.extent(array[, accessor])
Returns the minium and maximum value in given array using natural order.
cm.extent([4, 3, 2, 2, 7, 3, 5]); // [2, 7]
If an optional accessor function is specified, the extent is computed after calling array.map function.
cm.extent(people, (d) => d.age); // [10, 30]
Math
Processing numbers, randomness, etc.
# cm.clamp(value, min, max)
Constrains the input value within the specified range [min, max].
const x = 10;
cm.clamp(10, 2, 8); // 8
cm.clamp(10, 2, 12); // 10
cm.clamp(10, 12, 20); // 12
# cm.random([min[, max]])
Generates random number with a uniform distribution, which is within range [min, max). If min is not specified, it defaults to 0; if max is not specified, it defaults to 1.
cm.random(); // 0.4418278691734798
cm.random(10); // 3.747820060823679
cm.random(2, 10); // 6.649642684087617
# cm.randomInt([min[, max]])
Like cm.random
, expect returns integers.
cm.randomInt(0, 10); // 5
# cm.randomNoise(min[, max[, options]])
Returns a function with the specified options for generating random numbers with a smooth, continuous random-like distribution, commonly referred to as Perlin Noise, which is within range [min, max). If min is not specified, it defaults to 0; if max is not specified, it defaults to 1.
Supports following options:
- octaves - layers of noise, default to 4.
- seed - seed for generated sequence, a real number or as any integer, defaults to random number.
- falloff - falloff factor for each octave, a real number or as any integer, defaults to 0.5.
Increasing the number of octaves results in a more variable sequence, and two generators instanced with the same seed and octaves generate the same sequence.
The returned function accept two parameters: x is x coordinate in noise space; y is y coordinate in noise space.
cm.randomNoise()(0.2, 0.1); // 0.04076453205333332
cm.randomNoise(6, 2)(0.2, 0.1); // -0.08489767172063487
# cm.randomNormal([mu[, sigma]])
Returns a function for generating random numbers with a normal(Gaussian) distribution. The expected value of the generated number is mu, with given standard deviation sigma. If mu is not specified, it defaults to 0; if sigma is not specified, it defaults to 1.
cm.randomNormal()(); // -2.0897431210663022
cm.randomNormal(30, 10)(); // 31.94829616303788
# cm.randomChar()
Returns a random printable and non-empty character.
cm.randomChar(); // 'A'
Constant
Useful constants.
# cm.TWO_PI
It is twice the ratio of the circumference of a circle to its diameter.
Math.cos(cm.TOW_PI); // 1
Vector
Basics for simulating physical laws.
# cm.vec([x[, y]])
Constructs a vector with the specified x and y component. If either x or y are not specified, each defaults to 0. The returned vector has the following properties:
- x - x component of the vector
- y - y component of the vector
cm.vec(); // { x: 0, y: 0 }
cm.vec(1); // { x: 1, y: 0 }
cm.vec(2, 3); // { x: 2, y: 3 }
# cm.vecFromAngle(angle)
Constructs a vector from the specified angle in radians.
cm.vecFromAngle(Math.PI / 4); // { x: 1, y: 1 }
# cm.vecAdd(a, b)
Adds the specified vectors and returns a new vector.
const a = cm.vec(1, 2);
const b = cm.vec(2, 3);
const c = cm.vecAdd(a, b);
a; // { x: 1, y: 2 }
b; // { x: 2, y: 3 }
c; // { x: 3, y: 5 }
# cm.vecAngle(a)
Computes the angle of the specified vector.
const a = cm.vec(1, 1);
cm.vecAngle(a); // Math.PI / 4
# cm.vecClamp(a, min[, max])
Constrains the magnitude of the specified vector within the specified range [min, max], and returns a new vector.
const a = cm.vec(3, 4);
const b = cm.vecClamp(a, 10, 15);
a; // { x: 3, y: 4 }
b; // { x: 6, y: 8 }
If two arguments are specified, the second one is interpreted as the maximum magnitude, with the minium magnitude defaults to 0.
const a = cm.vec(6, 8);
cm.vecClamp(a, 5); // { a: 3, b: 4 }
# cm.vecClampX(a, min[, max])
Constrains the x component of the specified vector within the specified range [min, max], and returns a new vector.
const a = cm.vec(6, 8);
const b = cm.vecClampX(a, 10, 15);
a; // { x: 6, y: 8 }
b; // { x: 10, y: 8 }
If two arguments are specified, the second one is interpreted as the maximum value, with the minium value defaults to 0.
const a = cm.vec(6, 8);
const b = cm.vecClampX(a, 5);
a; // { x: 6, y: 8 }
b; // { x: 5, y: 8 }
# cm.vecClampY(a, min[, max])
Constrains the y component of the specified vector within the specified range [min, max], and returns a new vector.
const a = cm.vec(6, 8);
const b = cm.vecClampY(a, 10, 15);
a; // { x: 6, y: 8 }
b; // { x: 6, y: 10 }
If two arguments are specified, the second one is interpreted as the maximum value, with the minium value defaults to 0.
const a = cm.vec(6, 8);
const b = cm.vecClampY(a, 5);
a; // { x: 6, y: 8 }
b; // { x: 6, y: 5 }
# cm.vecCross(a, b)
Computes the cross product of the specified vectors.
const a = cm.vec(3, 4);
const b = cm.vec(1, 2);
cm.vecCross(a, b); // 2
# cm.vecDist(a, b)
Computes the distance of the specified vectors.
const a = cm.vec(4, 6);
const b = cm.vec(1, 2);
cm.vecDist(a, b); // 5
# cm.vecDist2(a, b)
Computes the square distance of the specified vectors.
const a = cm.vec(4, 6);
const b = cm.vec(1, 2);
cm.vecDist2(a, b); // 25
# cm.vecDiv(a, value)
Divides the specified vector's x and y component by the specified value, and returns a new vector.
const a = cm.vec(3, 4);
const b = cm.vecDiv(a, 0.5);
a; // { x: 3, y: 4 }
b; // { x: 6, y: 8 }
# cm.vecDot(a, b)
Computes the dot product of the specified vectors.
const a = cm.vec(3, 4);
const b = cm.vec(1, 2);
cm.vecDot(a, b); // 11
# cm.vecInX(a, min[, max])
Returns true if the specified vector's x component is within the specified range [min, max].
const a = cm.vec(3, 4);
cm.vecInX(a, 1, 2); // false
cm.vecInX(a, 1, 3); // true
cm.vecInX(a, 1, 4); // true
If two arguments are specified, the second one is interpreted as the maximum value, with the minium value defaults to 0.
const a = cm.vec(3, 4);
cm.vecInX(a, 2); // false
cm.vecInX(a, 3); // true
cm.vecInX(a, 4); // true
# cm.vecInY(a, x[, x1])
Returns true if the specified vector's y component is within the specified range [min, max].
const a = cm.vec(3, 4);
cm.vecInY(a, 1, 3); // false
cm.vecInY(a, 1, 4); // true
cm.vecInY(a, 1, 5); // true
If two arguments are specified, the second one is interpreted as the maximum value, with the minium value defaults to 0.
const a = cm.vec(3, 4);
cm.vecInY(a, 3); // false
cm.vecInY(a, 4); // true
cm.vecInY(a, 5); // true
# cm.vecMag(a[, value])
If only one argument is specified, computes the magnitude of the specified vector.
const a = cm.vec(3, 4);
cm.vecMag(a); // 5
If two arguments are specified, sets the magnitude of the specified vector to the specified value, and returns a new vector.
const a = cm.vec(3, 4);
const b = cm.vecMag(a, 10);
a; // { x: 3, y: 4 }
b; // { x: 6, y: 8 }
# cm.vecMult(a, value)
Multiplies the specified vector's x and y component by the specified value, and returns a new vector.
const a = cm.vec(3, 4);
const b = cm.vecMult(a, 2);
a; // { x: 3, y: 4 }
b; // { x: 6, y: 8 }
# cm.vecNeg(a)
Negates the specified vector's x and y component, and returns a new vector.
const a = cm.vec(3, 4);
const b = cm.vecNeg(a);
a; // { x: 3, y: 4 }
b; // { x: -3, y: -4 }
# cm.vecNegX(a)
Negates the specified vector's x component, and returns a new vector.
const a = cm.vec(3, 4);
const b = cm.vecNegX(a);
a; // { x: 3, y: 4 }
b; // { x: -3, y: 4 }
# cm.vecNegY(a)
Negates the specified vector's y component, and returns a new vector.
const a = cm.vec(3, 4);
const b = cm.vecNegY(a);
a; // { x: 3, y: 4 }
b; // { x: 3, y: -4 }
# cm.vecNorm(a)
Normalizes the specified vector, and returns a new vector.
const a = cm.vec(3, 4);
const b = cm.vecNorm(a);
a; // { x: 3, y: 4 }
b; // { x: 0.6, y: 0.8 }
# cm.vecRandom()
Returns a unit vector with a random heading, following a uniform distribution.
cm.vecRandom(); // { x: 0.9239434883837478, y: 0.688605153583981 }
# cm.vecSub(a, b)
Subtracts the specified vectors and returns a new vector.
const a = cm.vec(1, 2);
const b = cm.vec(2, 4);
const c = cm.vecSub(a, b);
a; // { x: 1, y: 2 }
b; // { x: 2, y: 4 }
c; // { x: -1, y: -2 }
# vec.clone()
Clones the vector and returns a new vector.
const a = cm.vec(1, 2);
const b = a.clone();
a === b; // false
b; // { x: 1, y: 2 }
# vec.add(a)
Adds the specified vector to the target vector, and returns the target vector.
const a = cm.vec(1, 2);
const b = cm.vec(3, 4);
a.add(b); // a
a; // { x: 4, y: 6 }
# vec.angle()
Computes the angle of the target vector.
const a = cm.vec(1, 1);
a.angle(); // Math.PI / 4
# vec.clamp(min[, max])
Constrains the magnitude of the target vector within the specified range [min, max], and returns it.
const a = cm.vec(3, 4);
a.clamp(10, 15); // a
a; // { x: 6, y: 8 }
If two arguments are specified, the second one is interpreted as the maximum magnitude, with the minium magnitude defaults to 0.
const a = cm.vec(6, 8);
a.clamp(5); // a
a; // { a: 3, b: 4 }
# vec.clampX(min[, max])
Constrains the x component of the target vector within the specified range [min, max], and returns it.
const a = cm.vec(6, 8);
a.clampX(10, 15); // a
a; // { x: 10, y: 8 }
If two arguments are specified, the second one is interpreted as the maximum value, with the minium value defaults to 0.
const a = cm.vec(6, 8);
a.clampX(5); // a
a; // { x: 5, y: 8 }
# vec.clampY(min[, max])
Constrains the y component of the target vector within the specified range [min, max], and returns it.
const a = cm.vec(6, 8);
a.clampY(10, 15); // a
a; // { x: 6, y: 10 }
If two arguments are specified, the second one is interpreted as the maximum value, with the minium value defaults to 0.
const a = cm.vec(6, 8);
a.clampY(5);
a; // { x: 6, y: 5 }
# vec.cross(a)
Computes the cross product of the specified vector and the target vector.
const a = cm.vec(3, 4);
const b = cm.vec(1, 2);
a.cross(b); // 2
# vec.dist(a)
Computes the distance of the specified vector and the target vector.
const a = cm.vec(4, 6);
const b = cm.vec(1, 2);
a.dist(b); // 5
# vec.dist2(a)
Computes the square distance of the specified vector and the target vector.
const a = cm.vec(4, 6);
const b = cm.vec(1, 2);
a.dist(b); // 25
# vec.div(value)
Divides the target vector' x and y component by the specified value, and returns it.
const a = cm.vec(3, 4);
a.div(0.5); // a
a; // { x: 6, y: 8 }
# vec.dot(a)
Computes the dot product of the specified vector and the target vector.
const a = cm.vec(3, 4);
const b = cm.vec(1, 2);
a.dot(b); // 11
# vec.inX(min[, max])
Returns true if the target vector's x component is within the specified range [min, max].
const a = cm.vec(3, 4);
a.inX(1, 2); // false
a.inX(1, 3); // true
a.inX(1, 4); // true
If two arguments are specified, the second one is interpreted as the maximum value, with the minium value defaults to 0.
const a = cm.vec(3, 4);
a.inX(2); // false
a.inX(3); // true
a.inX(4); // true
# vec.inY(min[, max])
Returns true if the target vector's y component is within the specified range [min, max].
const a = cm.vec(3, 4);
a.inY(1, 3); // false
a.inY(1, 4); // true
a.inY(1, 5); // true
If two arguments are specified, the second one is interpreted as the maximum value, with the minium value defaults to 0.
const a = cm.vec(3, 4);
a.inY(3); // false
a.inY(4); // true
a.inY(5); // true
# vec.mag([value])
If no argument is specified, computes the magnitude of the target vector.
const a = cm.vec(3, 4);
cm.mag(); // 5
If one argument is specified, sets the magnitude of the target to the specified value, and returns it.
const a = cm.vec(3, 4);
a.mag(10); // a
a; // { x: 6, y: 8 }
# vec.mult(m)
Multiplies the specified component's x and y by the specified value, and returns a new vector.
const a = cm.vec(3, 4);
const b = cm.vecMult(a, 2);
a; // { x: 3, y: 4 }
b; // { x: 6, y: 8 }
# vec.neg()
Negates the target vector's x and y component, and returns it.
const a = cm.vec(3, 4);
a.neg(); // a
a; // { x: -3, y: -4 }
# vec.negX()
Negates the target vector's x component, and returns it.
const a = cm.vec(3, 4);
a.negX(); // a
a; // { x: -3, y: 4 }
# vec.negY()
Negates the target vector's y component, and returns it.
const a = cm.vec(3, 4);
a.negY(); // a
a; // { x: 3, y: -4 }
# vec.norm()
Normalizes the target vector, and returns it.
const a = cm.vec(3, 4);
a.norm(); // a
a; // { x: 0.6, y: 0.8 }
# vec.set(x[, y])
If only one argument is specified and it is a vector instance, sets t