Generic whiteboard eraser for shapes and lines in react konva - konvajs

React konva has basic functionality and example of pen and eraser of free drawing. But here I am trying to include multiple shapes on my board. The issue is 'destination-out' eraser doesn't work well for all kind of shapes. As in my code below new circles are being overlapped by destination-out eraser lines.
Code-sandbox
import React from "react";
import { createRoot } from "react-dom/client";
import { Stage, Layer, Line, Text, Circle } from "react-konva";
const App = () => {
const [tool, setTool] = React.useState("pen");
const [lines, setLines] = React.useState([]);
const [circles, setCircles] = React.useState([]);
const isDrawing = React.useRef(false);
const getDist = (x1, y1, x2, y2) => {
const dx = Math.abs(x2 - x1);
const dy = Math.abs(y2 - y1);
return Math.sqrt(dx * dx + dy * dy);
};
const handleMouseDown = (e) => {
isDrawing.current = true;
const pos = e.target.getStage().getPointerPosition();
if (tool === "pen" || tool === "eraser")
setLines([...lines, { tool, points: [pos.x, pos.y] }]);
if (tool === "circle")
setCircles([...circles, { tool, x: pos.x, y: pos.y, r: 0 }]);
};
const handleMouseMove = (e) => {
// no drawing - skipping
if (!isDrawing.current) {
return;
}
const stage = e.target.getStage();
const point = stage.getPointerPosition();
if (tool === "pen" || tool === "eraser") {
let lastLine = lines[lines.length - 1];
// add point
lastLine.points = lastLine.points.concat([point.x, point.y]);
// replace last
lines.splice(lines.length - 1, 1, lastLine);
setLines(lines.concat());
}
if (tool === "circle") {
let lastCircle = circles[circles.length - 1];
// add circle
lastCircle.r = getDist(lastCircle.x, lastCircle.y, point.x, point.y);
// replace last
circles.splice(circles.length - 1, 1, lastCircle);
setCircles(circles.concat());
}
};
const handleMouseUp = () => {
isDrawing.current = false;
};
return (
<div>
<Stage
width={window.innerWidth}
height={window.innerHeight}
onMouseDown={handleMouseDown}
onMousemove={handleMouseMove}
onMouseup={handleMouseUp}
>
<Layer>
<Text text="Just start drawing" x={5} y={30} />
{circles.map((circle, i) => (
<Circle
key={i}
x={circle.x}
y={circle.y}
radius={circle.r}
stroke="#df4b26"
strokeWidth={5}
/>
))}
{lines.map((line, i) => (
<Line
key={i}
points={line.points}
stroke="#df4b26"
strokeWidth={5}
tension={0.5}
lineCap="round"
lineJoin="round"
globalCompositeOperation={
line.tool === "eraser" ? "destination-out" : "source-over"
}
/>
))}
</Layer>
</Stage>
<select
value={tool}
onChange={(e) => {
setTool(e.target.value);
}}
>
<option value="pen">Pen</option>
<option value="circle">Circle</option>
<option value="eraser">Eraser</option>
</select>
</div>
);
};
const container = document.getElementById("root");
const root = createRoot(container);
root.render(<App />);
I tried moving <Circle /> component below <Line /> component but it seems that react konva maintains the order of components on the canvas as well. So in that case eraser doesn't work at all over circles.

Related

Is there a image pattern stroke option for lines?

I've been trying to look over the Konva shape library and haven't found a stroke reapeating pattern method. I've been trying to look for a way to implement https://stackoverflow.com/a/32323610/20557085 into the shape's sceneFunc, but ended up with a static version that keeps itself in the top right corner of the canvas at all times, even if the canvas/camera is moved/dragged.
The end-goal would be to have a image that repeats itself following a line's bezier curve of points, that I can change the width of.
The question would be if there is something I am missing that is already a part of Konva, or if I should continue to trial my way through the sceneFunc?
The class component used in my attempt, that ended up static:
import React, { Component } from 'react';
import { createRoot } from 'react-dom/client';
import { Stage, Layer, Image, Shape } from 'react-konva';
var PI = Math.PI;
class URLImageStroke extends React.Component {
constructor(props) {
super(props)
this.state = {
image: null,
points: [{ x: 0, y: 0 }, { x: 100, y: 100 }, { x: 150, y: 50 }, { x: 200, y: 200 }]
};
}
componentDidMount() {
this.loadImage();
this.getPoints()
}
loadImage() {
// save to "this" to remove "load" handler on unmount
this.image = new window.Image();
this.image.src = this.props.src;
this.image.addEventListener('progress', (e) => console.log(e))
this.image.addEventListener('load', this.handleLoad);
}
handleLoad = () => {
this.setState({
image: this.image,
});
};
getPoints = () => {
let points = [];
//for (let i = 0; this.state.points.length > i; i++) {
const s = this.state.points[0];
const c1 = this.state.points[1];
const c2 = this.state.points[2];
const e = this.state.points[3];
for (var t = 0; t <= 100; t += 0.25) {
var T = t / 100;
// plot a point on the curve
var pos = getCubicBezierXYatT(s, c1, c2, e, T);
// calculate the tangent angle of the curve at that point
var tx = bezierTangent(s.x, c1.x, c2.x, e.x, T);
var ty = bezierTangent(s.y, c1.y, c2.y, e.y, T);
var a = Math.atan2(ty, tx) - PI / 2;
// save the x/y position of the point and the tangent angle
// in the points array
points.push({
x: pos.x,
y: pos.y,
angle: a
});
}
this.setState({
points: points
});
}
render() {
return (
<Shape
x={50}
y={50}
width={this.props?.width}
height={this.props?.height}
image={this.state.image}
points={this.state?.points}
sceneFunc={(ctx, shape) => {
const img = shape.attrs.image;
if (!img) {
console.log("no image")
return;
}
const points = shape.attrs.points;
if (!points) {
console.log("no points")
return;
}
// Note: increase the lineWidth if
// the gradient has noticable gaps
ctx.lineWidth = 8;
ctx.strokeStyle = 'skyblue';
let sliceCount = 0;
// draw a gradient-stroked line tangent to each point on the curve
for (let i = 0; i < points.length; i++) {
let p = points[i];
ctx.translate(p.x, p.y);
ctx.rotate(p.angle - PI / 2);
// draw multiple times to fill gaps on outside of rope slices
ctx.drawImage(img, sliceCount, 0, 1, img.height, 0, 0, 1, img.height);
ctx.drawImage(img, sliceCount, 0, 1, img.height, 0, 0, 1, img.height);
ctx.drawImage(img, sliceCount, 0, 1, img.height, 0, 0, 1, img.height);
ctx.setTransform(1, 0, 0, 1, 0, 0);
++sliceCount;
if (sliceCount > (img.width - 1)) { sliceCount = 0; }
}
//ctx.strokeShape(this);
}
}
/>
);
}
}
//////////////////////////////////////////
// helper functions
//////////////////////////////////////////
// calculate one XY point along Cubic Bezier at interval T
// (where T==0.00 at the start of the curve and T==1.00 at the end)
function getCubicBezierXYatT(startPt, controlPt1, controlPt2, endPt, T) {
var x = CubicN(T, startPt.x, controlPt1.x, controlPt2.x, endPt.x);
var y = CubicN(T, startPt.y, controlPt1.y, controlPt2.y, endPt.y);
return ({ x: x, y: y });
}
// cubic helper formula at T distance
function CubicN(T, a, b, c, d) {
var t2 = T * T;
var t3 = t2 * T;
return a + (-a * 3 + T * (3 * a - a * T)) * T
+ (3 * b + T * (-6 * b + b * 3 * T)) * T
+ (c * 3 - c * 3 * T) * t2
+ d * t3;
}
// calculate the tangent angle at interval T on the curve
function bezierTangent(a, b, c, d, t) {
return (3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b));
};
export default URLImageStroke;

Problems by coloring WebGL 3D Object

I'm trying to implement a WebGL app according to the documentation in the Mozilla Docs.
My code generates a sphere with is shaped by a scalefactor. The colors are generated according to the scalefactor. The shape is ok, but the colors are wrong. So what is going wrong - I have no clue. This code works on Android and in Java. I'm using the latest Chrome browser.
Here is the code:
export function createHcm3dObject(gl, diagram3D, deltaTheta, deltaPhi) {
let positions = [];
let colors = [];
let alpha = 1.0;
for (let theta = 0; theta < 360; theta += deltaTheta) {
for (let phi = 0; phi < 180; phi += deltaPhi) {
//r is scalefactor between 0 and 1 which shapes the sphere
let r = diagram3D[theta][phi];
//Color is generated according to the radius (alpha is currently set to 1.0)
let x1Color = generateColorArray(r, alpha);
let x1 = r * Math.sin(math3d.toRadians(phi)) * Math.cos(math3d.toRadians(theta));
let y1 = r * Math.sin(math3d.toRadians(phi)) * Math.sin(math3d.toRadians(theta));
let z1 = r * Math.cos(math3d.toRadians(phi));
r = diagram3D[theta + deltaTheta][phi];
let x2Color = generateColorArray(r, alpha);
let x2 = r * Math.sin(math3d.toRadians(phi)) * Math.cos(math3d.toRadians(theta + deltaTheta));
let y2 = r * Math.sin(math3d.toRadians(phi)) * Math.sin(math3d.toRadians(theta + deltaTheta));
let z2 = r * Math.cos(math3d.toRadians(phi));
r = diagram3D[theta][phi + deltaPhi];
let x3Color = generateColorArray(r, alpha);
let x3 = r * Math.sin(math3d.toRadians(phi + deltaPhi)) * Math.cos(math3d.toRadians(theta));
let y3 = r * Math.sin(math3d.toRadians(phi + deltaPhi)) * Math.sin(math3d.toRadians(theta));
let z3 = r * Math.cos(math3d.toRadians(phi + deltaPhi));
r = diagram3D[theta + deltaTheta][phi + deltaPhi];
let x4Color = generateColorArray(r, alpha);
let x4 = r * Math.sin(math3d.toRadians(phi + deltaPhi)) * Math.cos(math3d.toRadians(theta + deltaTheta));
let y4 = r * Math.sin(math3d.toRadians(phi + deltaPhi)) * Math.sin(math3d.toRadians(theta + deltaTheta));
let z4 = r * Math.cos(math3d.toRadians(phi + deltaPhi));
//1. Triangle
positions.push(x1, y1, z1);
positions.push(x3, y3, z3);
positions.push(x4, y4, z4);
//2. Triangle
positions.push(x2, y2, z2);
positions.push(x1, y1, z1);
positions.push(x4, y4, z4);
//Colors for 1. Triangle (red,green,blue,alpha=1.0)
colors.push(x1Color[0], x1Color[1], x1Color[2], x1Color[3]);
colors.push(x3Color[0], x3Color[1], x3Color[2], x3Color[3]);
colors.push(x4Color[0], x4Color[1], x4Color[2], x4Color[3]);
//Colors for 2. Triangle
colors.push(x2Color[0], x2Color[1], x2Color[2], x2Color[3]);
colors.push(x1Color[0], x1Color[1], x1Color[2], x1Color[3]);
colors.push(x4Color[0], x4Color[1], x4Color[2], x4Color[3]);
}
//console.log(positions);
//console.log(colors);
}
// Now pass the list of positions into WebGL to build the
// shape. We do this by creating a Float32Array from the
// JavaScript array, then use it to fill the current buffer.
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
return {
position: positionBuffer,
color: colorBuffer,
positionSize: positions.length,
deltaTheta,
deltaPhi
};
};
function generateColorArray(r, alpha) {
let colorQuad = [];
let green = Math.abs(Math.sin(2 * r * Math.PI));
let blue = Math.abs(Math.cos(2 * r * Math.PI));
colorQuad[0] = 0.0;
colorQuad[1] = green;
colorQuad[2] = blue;
colorQuad[3] = alpha;
if (r >= 0.5 / 2) {
let red = Math.abs(Math.cos(2 * r * Math.PI));
green = Math.abs(Math.sin(2 * r * Math.PI));
if (r < 0.5) {
green = 1.0;
}
colorQuad[0] = red;
colorQuad[1] = green;
colorQuad[2] = 0.0;
colorQuad[3] = alpha;
}
if (r >= 0.5) {
let red = Math.abs(Math.cos(2 * r * Math.PI));
green = Math.abs(Math.cos(2 * r * Math.PI));
if (r < 0.75) {
red = 1.0;
}
colorQuad[0] = red;
colorQuad[1] = green;
colorQuad[2] = 0.0;
colorQuad[3] = alpha;
}
if (r >= 0.75) {
let red = 1.0;
blue = Math.abs(Math.cos(2 * r * Math.PI));
colorQuad[0] = red;
colorQuad[1] = 0.0;
colorQuad[2] = blue;
colorQuad[3] = alpha;
}
return colorQuad;
}
React Class:
export class Viewer3d extends Component {
state = {
rotX: 0,
rotY: 0,
gl: null,
buffers: null,
programInfo: null,
};
componentDidMount() {
this.init();
}
init = () => {
console.log("Comp did mount");
const canvas = document.querySelector("#glCanvas");
/** #type {WebGLRenderingContext} */
const gl = canvas.getContext("webgl");
if (!gl) {
alert(
"Unable to initialize WebGL. Your browser or machine may not support it."
);
return;
}
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
let vs = document.getElementById("vshader").textContent;
let fs = document.getElementById("fshader").textContent;
//console.log(vs+" "+fs);
const shaderProgram = shader.initShaderProgram(gl, vs, fs);
let diagram3D = [];
let deltaTheta = 10;
let deltaPhi = 10;
for (let theta = 0; theta <= 360; theta += deltaTheta) {
let phiArray = [];
for (let phi = 0; phi <= 180; phi += deltaPhi) {
let eleCorr = 90 - phi;
let thetaCorr = 360 - theta;
let out = engine.antenna_correction(
thetaCorr,
0,
eleCorr,
0,
"012EA34",
"012EA34"
);
let att = out.a;
let logarithmic = false;
if (logarithmic) {
att = 1.0 - (-20.0 * Math.log10(att)) / 40.0;
}
phiArray[phi] = att;
}
diagram3D[theta] = phiArray;
}
//console.log(diagram3D);
const buffers = hcm3d.createHcm3dObject(
gl,
diagram3D,
deltaTheta,
deltaPhi
);
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
vertexColor: gl.getAttribLocation(shaderProgram,"aVertexColor"),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram,"uProjectionMatrix"),
modelViewMatrix: gl.getUniformLocation(shaderProgram,"uModelViewMatrix"),
},
};
this.setState({ gl, buffers, programInfo });
this.drawScene(gl, programInfo, buffers);
};
drawScene = (gl, programInfo, buffers) => {
gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque
gl.clearDepth(1.0); // Clear everything
gl.enable(gl.DEPTH_TEST); // Enable depth testing
gl.depthFunc(gl.LEQUAL); // Near things obscure far things
// Clear the canvas before we start drawing on it.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Create a perspective matrix, a special matrix that is
// used to simulate the distortion of perspective in a camera.
// Our field of view is 45 degrees, with a width/height
// ratio that matches the display size of the canvas
// and we only want to see objects between 0.1 units
// and 100 units away from the camera.
const fieldOfView = (45 * Math.PI) / 180; // in radians
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.1;
const zFar = 100.0;
const projectionMatrix = mat4.create();
// note: glmatrix.js always has the first argument
// as the destination to receive the result.
mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);
// Set the drawing position to the "identity" point, which is
// the center of the scene.
const modelViewMatrix = mat4.create();
// Now move the drawing position a bit to where we want to
// start drawing the square.
mat4.translate(
modelViewMatrix, // destination matrix
modelViewMatrix, // matrix to translate
[0, 0, -2.5]
);
mat4.rotate(modelViewMatrix, modelViewMatrix, this.state.rotY, [1, 0, 0]);
mat4.rotate(modelViewMatrix, modelViewMatrix, this.state.rotX, [0, 1, 0]);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexPosition,
3,
gl.FLOAT,
false,
0,
0
);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexColor,
4,
gl.FLOAT,
false,
0,
0
);
gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor);
gl.useProgram(programInfo.program);
gl.uniformMatrix4fv(
programInfo.uniformLocations.projectionMatrix,
false,
projectionMatrix
);
gl.uniformMatrix4fv(
programInfo.uniformLocations.modelViewMatrix,
false,
modelViewMatrix
);
gl.drawArrays(gl.TRIANGLES, 0, buffers.positionSize);
};
onMouseMove = (evt) => {
if (!mouseDown) {
return;
}
evt.preventDefault();
let deltaX = evt.clientX - mouseX;
let deltaY = evt.clientY - mouseY;
mouseX = evt.clientX;
mouseY = evt.clientY;
this.rotateScene(deltaX, deltaY);
};
onMouseDown = (evt) => {
evt.preventDefault();
mouseDown = true;
mouseX = evt.clientX;
mouseY = evt.clientY;
};
onMouseUp = (evt) => {
evt.preventDefault();
mouseDown = false;
};
rotateScene = (deltaX, deltaY) => {
this.setState({
rotX: this.state.rotX + deltaX / 100,
rotY: this.state.rotY + deltaY / 100,
});
this.drawScene(this.state.gl, this.state.programInfo, this.state.buffers);
};
render() {
return (
<div className="w3-container w3-padding-16">
<canvas
id="glCanvas"
width={1280}
height={720}
onMouseMove={this.onMouseMove}
onMouseDown={this.onMouseDown}
onMouseUp={this.onMouseUp}
></canvas>
</div>
);
}
}
export default Viewer3d;
Fragment Shader:
<script id="fshader" type="x-shader/x-fragment">
precision mediump float;
varying vec4 vColor;
void main(void) {
gl_FragColor = vColor;
}
</script>
Vertex Shader:
<script id="vshader" type="x-shader/x-vertex">
attribute vec4 aVertexPosition;
attribute vec4 aVertexColor;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying vec4 vColor;
void main(void) {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
vColor = aVertexColor;
}
</script>
You need to bind one buffer (say the color one), then use vertexAttribPointer to bind the set buffer to the color attribute. Then again, bind the vertex position buffer, and call vertexAttribPointer to bind it the vertex position attribute. Pseudocode:
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color);
gl.vertexAttribPointer(programInfo.attribLocations.vertexColor, ...);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, ...);

Highcharts - Markers in line charts move up when leaving hovering

I am trying to use Font Awesome icons as markers in HighCharts line chart. With help from fellow developers on Stack Overflow, I have managed to do that. One problem left is that now, whenever I hover over the markers and then leave hovering, they just move up a bit from the line and stay there forever. I really have no idea why.
This is the fiddle: https://jsfiddle.net/vf0g4u5k/12/. Appreciate any help.
The plug-in to use Font Awesome with HighCharts
(function (H) {
function symbolWrap(proceed, symbol, x, y, w, h, options) {
if (symbol.indexOf('text:') === 0) {
var text = symbol.split(':')[1],
svgElem = this.text(text, x, y + h)
.css({
fontFamily: '"Font Awesome 5 Free"',
fontSize: (h * 2 ) + "px"
});
if (svgElem.renderer.isVML) {
svgElem.fillSetter = function (value, key, element) {
element.style.color = H.Color(value).get('rgb');
};
}
return svgElem;
}
if (symbol.indexOf('textn:') === 0) {
var text = symbol.split(':')[1],
svgElem = this.text(text, x, y + h)
.css({
fontFamily: '"Font Awesome 5 Free"',
fontSize: (h * 2 ) + "px",
fontWeight: 900
});
if (svgElem.renderer.isVML) {
svgElem.fillSetter = function (value, key, element) {
element.style.color = H.Color(value).get('rgb');
};
}
return svgElem;
}
return proceed.apply(this, [].slice.call(arguments, 1));
}
H.wrap(H.SVGRenderer.prototype, 'symbol', symbolWrap);
if (H.VMLRenderer) {
H.wrap(H.VMLRenderer.prototype, 'symbol', symbolWrap);
}
// Load the font for SVG files also
H.wrap(H.Chart.prototype, 'getSVG', function (proceed) {
var svg = proceed.call(this);
svg = '<?xml-stylesheet type="text/css" ' +
'href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" ?>' +
svg;
return svg;
});
}(Highcharts));
I ended up doing like this. Posting in case it may help someone else.
point: {
events: {
mouseOut: function() {
var index = this.index
for( var series of this.series.chart.series){
for(var j=0;j<series.points.length;j++){
if(j === index && series.points[j].graphic){series.points[j].graphic.attr({'translateY': 8})}
}
};
}
}
}
You can adjust your custom marker settings by setting translateX and translateY like this
var text = symbol.split(':')[1],
svgElem = this.text(text, x, y)
.attr({
translateY: h,
translateX: -1
})
.css({
fontFamily: '"Font Awesome 5 Free"',
fontSize: (h * 2 ) + "px"
});

GLSL shader for texture cubic projection

I am trying to implement a texture cubic projection inside my WebGL shader, like in the picture below:
What I tried so far:
I am passing the bounding box of my object (the box in the middle of the picture) as follows:
uniform vec3 u_bbmin;
uniform vec3 u_bbmax;
... so the eight vertexes of my projection box are:
vec3 v1 = vec3(u_bbmin.x, u_bbmin.y, u_bbmin.z);
vec3 v2 = vec3(u_bbmax.x, u_bbmin.y, u_bbmin.z);
vec3 v3 = vec3(u_bbmin.x, u_bbmax.y, u_bbmin.z);
...other combinations
vec3 v8 = vec3(u_bbmax.x, u_bbmax.y, u_bbmax.z);
At the end, to sample from my texture I need a map in the form of:
varying vec3 v_modelPos;
...
uniform sampler2D s_texture;
vec2 tCoords = vec2(0.0);
tCoords.s = s(x,y,z)
tCoords.t = t(y,y,z)
vec4 color = texture2D(s_texture, tCoords);
I was able to implement spherical and cylindrical projections, but I am stuck now how to get this kind of cubic map, The texture shall stretch to the whole bounding box, aspect ratio doesn't matter.
Maybe I am missing some key points and I need some hints. How should the math for a cubic projection looks like?
I honestly don't know if this is correct or not but ...
Looking up how cube mapping works there's a table in the OpenGL ES 2.0 spec
Major Axis Direction| Target |sc |tc |ma |
--------------------+---------------------------+---+---+---+
+rx |TEXTURE_CUBE_MAP_POSITIVE_X|−rz|−ry| rx|
−rx |TEXTURE_CUBE_MAP_NEGATIVE_X| rz|−ry| rx|
+ry |TEXTURE_CUBE_MAP_POSITIVE_Y| rx| rz| ry|
−ry |TEXTURE_CUBE_MAP_NEGATIVE_Y| rx|−rz| ry|
+rz |TEXTURE_CUBE_MAP_POSITIVE_Z| rx|−ry| rz|
−rz |TEXTURE_CUBE_MAP_NEGATIVE_Z|−rx|−ry| rz|
--------------------+---------------------------+---+---+---+
Table 3.21: Selection of cube map images based on major axis direction of texture coordinates
Using that I wrote this function
#define RX 0
#define RY 1
#define RZ 2
#define S 0
#define T 1
void majorAxisDirection(vec3 normal, inout mat4 uvmat) {
vec3 absnorm = abs(normal);
if (absnorm.x > absnorm.y && absnorm.x > absnorm.z) {
// x major
if (normal.x >= 0.0) {
uvmat[RZ][S] = -1.;
uvmat[RY][T] = -1.;
} else {
uvmat[RZ][S] = 1.;
uvmat[RY][T] = -1.;
}
} else if (absnorm.y > absnorm.z) {
// y major
if (normal.y >= 0.0) {
uvmat[RX][S] = 1.;
uvmat[RZ][T] = 1.;
} else {
uvmat[RX][S] = 1.;
uvmat[RZ][T] = -1.;
}
} else {
// z major
if (normal.z >= 0.0) {
uvmat[RX][S] = 1.;
uvmat[RY][T] = -1.;
} else {
uvmat[RX][S] = -1.;
uvmat[RY][T] = -1.;
}
}
}
You pass in a matrix and it sets it up to move the correct X, Y, or Z to the X and Y columns (to convert to s and t). In other words you pass in normal and it returns s and t.
This would effectively give a unit cube projected on the positive side of the origin. Adding in another matrix we can move and scale that cube.
If you want it to fit the cube exactly then you need to set the scale, translation and orientation to match the cube.
"use strict";
/* global document, twgl, requestAnimationFrame */
const vs = `
uniform mat4 u_model;
uniform mat4 u_viewProjection;
attribute vec4 position;
attribute vec3 normal;
attribute vec2 texcoord;
varying vec2 v_texCoord;
varying vec3 v_normal;
varying vec3 v_position;
void main() {
v_texCoord = texcoord;
vec4 position = u_model * position;
gl_Position = u_viewProjection * position;
v_position = position.xyz;
v_normal = (u_model * vec4(normal, 0)).xyz;
}
`;
const fs = `
precision mediump float;
varying vec3 v_position;
varying vec2 v_texCoord;
varying vec3 v_normal;
uniform mat4 u_cubeProjection;
uniform sampler2D u_diffuse;
#define RX 0
#define RY 1
#define RZ 2
#define S 0
#define T 1
#if BOX_PROJECTION
void majorAxisDirection(vec3 normal, inout mat4 uvmat) {
vec3 absnorm = abs(normal);
if (absnorm.x > absnorm.y && absnorm.x > absnorm.z) {
// x major
if (normal.x >= 0.0) {
uvmat[RZ][S] = -1.;
uvmat[RY][T] = -1.;
} else {
uvmat[RZ][S] = 1.;
uvmat[RY][T] = -1.;
}
} else if (absnorm.y > absnorm.z) {
// y major
if (normal.y >= 0.0) {
uvmat[RX][S] = 1.;
uvmat[RZ][T] = 1.;
} else {
uvmat[RX][S] = 1.;
uvmat[RZ][T] = -1.;
}
} else {
// z major
if (normal.z >= 0.0) {
uvmat[RX][S] = 1.;
uvmat[RY][T] = -1.;
} else {
uvmat[RX][S] = -1.;
uvmat[RY][T] = -1.;
}
}
}
#else // cube projection
void majorAxisDirection(vec3 normal, inout mat4 uvmat) {
vec3 absnorm = abs(normal);
if (absnorm.x > absnorm.y && absnorm.x > absnorm.z) {
// x major
uvmat[RZ][S] = 1.;
uvmat[RY][T] = -1.;
} else if (absnorm.y > absnorm.z) {
uvmat[RX][S] = 1.;
uvmat[RZ][T] = 1.;
} else {
uvmat[RX][S] = 1.;
uvmat[RY][T] = -1.;
}
}
#endif
void main() {
vec3 normal = normalize(v_normal);
mat4 uvmat = mat4(
vec4(0, 0, 0, 0),
vec4(0, 0, 0, 0),
vec4(0, 0, 0, 0),
vec4(0, 0, 0, 1));
majorAxisDirection(normal, uvmat);
uvmat = mat4(
abs(uvmat[0]),
abs(uvmat[1]),
abs(uvmat[2]),
abs(uvmat[3]));
vec2 uv = (uvmat * u_cubeProjection * vec4(v_position, 1)).xy;
gl_FragColor = texture2D(u_diffuse, uv);
}
`;
const m4 = twgl.m4;
const gl = twgl.getWebGLContext(document.getElementById("c"));
// compile shaders, look up locations
const cubeProjProgramInfo = twgl.createProgramInfo(gl,
[vs, '#define BOX_PROJECTION 0\n' + fs]);
const boxProjProgramInfo = twgl.createProgramInfo(gl,
[vs, '#define BOX_PROJECTION 1\n' + fs]);
let progNdx = 1;
const programInfos = [
cubeProjProgramInfo,
boxProjProgramInfo,
];
// create buffers
const cubeBufferInfo = twgl.primitives.createCubeBufferInfo(gl, 2);
const sphereBufferInfo = twgl.primitives.createSphereBufferInfo(gl, 1, 60, 40);
const ctx = document.createElement("canvas").getContext("2d");
ctx.canvas.width = 256;
ctx.canvas.height = 256;
ctx.fillStyle = `hsl(${360}, 0%, 30%)`;
ctx.fillRect(0, 0, 256, 256);
for (let y = 0; y < 4; ++y) {
for (let x = 0; x < 4; x += 2) {
ctx.fillStyle = `hsl(${(x + y) / 16 * 360}, 100%, 75%)`;
ctx.fillRect((x + (y & 1)) * 64, y * 64, 64, 64);
}
}
ctx.lineWidth = 10;
ctx.strokeRect(0, 0, 256, 256);
ctx.font = "240px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = 'red';
ctx.fillText("F", 128, 128);
const texture = twgl.createTexture(gl, {
src: ctx.canvas,
wrap: gl.CLAMP_TO_EDGE,
min: gl.LINEAR, // no mips
});
function addElem(parent, type) {
const elem = document.createElement(type);
parent.appendChild(elem);
return elem;
}
function makeRange(parent, obj, prop, min, max, name) {
const divElem = addElem(parent, 'div');
const inputElem = addElem(divElem, 'input');
Object.assign(inputElem, {
type: 'range',
min: 0,
max: 1000,
value: (obj[prop] - min) / (max - min) * 1000,
});
const valueElem = addElem(divElem, 'span');
valueElem.textContent = obj[prop].toFixed(2);
const labelElem = addElem(divElem, 'label');
labelElem.textContent = name;
function update() {
inputElem.value = (obj[prop] - min) / (max - min) * 1000,
valueElem.textContent = obj[prop].toFixed(2);
}
inputElem.addEventListener('input', (e) => {
obj[prop] = (e.target.value / 1000 * (max - min) + min);
update();
});
return update;
}
const models = [
cubeBufferInfo,
sphereBufferInfo,
cubeBufferInfo,
];
const rotateSpeeds = [
1,
1,
0,
];
let modelNdx = 0;
const ui = document.querySelector('#ui');
const cubeMatrix = m4.translation([0.5, 0.5, 0.5]);
const updaters = [
makeRange(ui, cubeMatrix, 0, -2, 2, 'sx'),
makeRange(ui, cubeMatrix, 5, -2, 2, 'sy'),
makeRange(ui, cubeMatrix, 10, -2, 2, 'sz'),
makeRange(ui, cubeMatrix, 12, -2, 2, 'tx'),
makeRange(ui, cubeMatrix, 13, -2, 2, 'ty'),
makeRange(ui, cubeMatrix, 14, -2, 2, 'tz'),
];
document.querySelectorAll('input[name=shape]').forEach((elem) => {
elem.addEventListener('change', (e) => {
if (e.target.checked) {
modelNdx = parseInt(e.target.value);
if (modelNdx == 2) {
m4.scaling([1/2, 1/2, 1/2], cubeMatrix);
m4.translate(cubeMatrix, [1, 1, 1], cubeMatrix);
updaters.forEach(f => f());
}
}
})
});
document.querySelectorAll('input[name=proj]').forEach((elem) => {
elem.addEventListener('change', (e) => {
if (e.target.checked) {
progNdx = parseInt(e.target.value);
}
})
});
const uniforms = {
u_diffuse: texture,
u_cubeProjection: cubeMatrix,
};
function render(time) {
time *= 0.001;
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
const programInfo = programInfos[progNdx];
const bufferInfo = models[modelNdx];
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const fov = 30 * Math.PI / 180;
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.5;
const zFar = 10;
const projection = m4.perspective(fov, aspect, zNear, zFar);
const eye = [0, 4, -4];
const target = [0, 0, 0];
const up = [0, 1, 0];
const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
const viewProjection = m4.multiply(projection, view);
const model = m4.rotationY(time * rotateSpeeds[modelNdx]);
uniforms.u_viewProjection = viewProjection;
uniforms.u_model = model;
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
twgl.setUniforms(programInfo, uniforms);
gl.drawElements(gl.TRIANGLES, bufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
body {
margin: 0;
font-family: monospace;
color: white;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
background: #444;
}
#ui {
position: absolute;
left: 0;
top: 0;
}
#ui span {
display: inline-block;
width: 4em;
text-align: right;
}
<canvas id="c"></canvas>
<script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
<div id="ui">
<div>
<input type="radio" name="proj" id="sphere" value="0">
<label for="sphere">cubic projection</label>
<input type="radio" name="proj" id="cube" value="1" checked>
<label for="cube">box projection</label>
</div>
<div>
<input type="radio" name="shape" id="sphere" value="1">
<label for="sphere">sphere</label>
<input type="radio" name="shape" id="cube" value="0" checked>
<label for="cube">cube</label>
<input type="radio" name="shape" id="cube" value="2">
<label for="cube">cube match</label>
</div>
</div>
The key-point here is: normals shall be in object-space. Please note that gman's answer is more elegant than mine, by using a matrix for the uv computation. I am using instead the bounding box coordinates, which are already passed to the vertex shader as uniform for other general purposes.
Moreover, I don't even need to distinguish all the six major axis, I just only need three sides projection, so this can be simplified down. Of course, the texture will be mirrored on the opposite faces.
float sX = u_bbmax.x - u_bbmin.x;
float sY = u_bbmax.y - u_bbmin.y;
float sZ = u_bbmax.z - u_bbmin.z;
/* --- BOX PROJECTION - THREE SIDES --- */
if( (abs(modelNormal.x) > abs(modelNormal.y)) && (abs(modelNormal.x) > abs(modelNormal.z)) ) {
uvCoords = modelPos.yz / vec2(sY, -sZ); // X axis
} else if( (abs(modelNormal.z) > abs(modelNormal.x)) && (abs(modelNormal.z) > abs(modelNormal.y)) ) {
uvCoords = modelPos.xy / vec2(sX, -sY); // Z axis
} else {
uvCoords = modelPos.xz / vec2(sX, -sZ); // Y axis
}
uvCoords += vec2(0.5);
Explanation:
The direction of the texture projection is determined by the
order of the modelPos coordinates.
Example: the texture can be rotated by 90 degrees by using
modelPos.yx instead of modelPos.xy.
The orientation of the texture projection is determined by the sign of
the modelPos coordinates.
Example: the texture can be mirrored on the Y-axis by using
vec2(sX, sY) instead of vec2(sX, -sY).
Result:
EDIT:
It is worth to link here another answer from gman which contain additional information about this topic and also some cool optimization techniques to avoid conditionals inside GLSL shaders: How to implement textureCube using 6 sampler2D.

WebGL, how to transition from 3D to 2D and back

How do I transition between a 3D view and a 2D view in WebGL?
I have a 3D view of a scene and I want to show a 2D view as well, like a map view. How do I switch between the 2 types of views?
Generally to do switch from 3d to 2d you just use an orthographic projection instead of a perspective projection.
If you want to animate the transition lerping between the 2 seems to work
const ortho = someOrthoFunc(left, right, top, bottom, orthoZNear, orthZFar);
const persp = somePerspFunc(fov, aspect, perspZNear, perspZFar);
const projection = [];
for (let i = 0; i < 16; ++i) {
projection[i] = lerp(ortho[i], persp[i], mixAmount);
}
function lerp(a, b, l) {
return a + (b - a) * l;
}
Where mixAmount is 0 when you want the orthographic view (2d-ish) and mixAmount is 1 when you want the perspective view (3d) and you can animate that between 0 and 1.
Note that if you want the orthographic view match the perspective view you need to choose top, bottom, left, right values that match which fit your app. For transitioning between 2 different views (say first person on the ground vs looking straight down) you can pick whatever settings you want. But say you were looking down and just wanted to view to go from 3D to 2D with the same view. In that case you need to pick a left, right, top, bottom that matches the perspective view for a given number of units. For top and bottom that's probably how ever many units fit vertically the "ground" distance from the camera.
See this answer where distance is the distance to the ground, the formula will then give you the number of half the number of units at that distance which you can then plug into top and bottom. For left and right just multiply by the aspect of the canvas's display size
The other thing that changes is the camera. A common way to position a camera is using a lookAt function which, depending on the library might generate a view matrix or a camera matrix.
To look down
const cameraPosition = [x, groundHeight + distanceAboveGround, z];
const target = [x, groundHeight, z];
const up = [0, 0, 1];
const camera = someLookAtFunction(camearPosition, target, up);
You'd have a different set of cameraPosition, target, up for the 3d camera. You can animate the transition between them by lerping those 3 variables.
const vs = `
uniform mat4 u_worldViewProjection;
attribute vec4 a_position;
attribute vec2 a_texcoord;
varying vec4 v_position;
varying vec2 v_texcoord;
void main() {
v_texcoord = a_texcoord;
gl_Position = u_worldViewProjection * a_position;
}
`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
void main() {
gl_FragColor = texture2D(u_texture, v_texcoord);
}
`;
"use strict";
twgl.setDefaults({attribPrefix: "a_"});
const m4 = twgl.m4;
const v3 = twgl.v3;
const gl = document.getElementById("c").getContext("webgl");
// compiles shaders, links program, looks up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData for positions, texcoords
const bufferInfo = twgl.primitives.createCubeBufferInfo(gl);
// calls gl.createTexture, gl.bindTexture, gl.texImage2D, gl.texParameteri
const tex = twgl.createTexture(gl, {
min: gl.NEAREST,
mag: gl.NEAREST,
src: [
255, 0, 0, 255,
0, 192, 0, 255,
0, 0, 255, 255,
255, 224, 0, 255,
],
});
const settings = {
projectionMode: 2,
cameraMode: 2,
fov: 30,
};
function render(time) {
time *= 0.001;
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const fov = settings.fov * Math.PI / 180;
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const perspZNear = 0.5;
const perspZFar = 10;
const persp = m4.perspective(fov, aspect, perspZNear, perspZFar);
// the size to make the orthographic view is arbitrary.
// here we're choosing the number of units at ground level
// away from the top perspective camera
const heightAboveGroundInTopView = 7;
const halfSizeToFitOnScreen = heightAboveGroundInTopView * Math.tan(fov / 2);
const top = -halfSizeToFitOnScreen;
const bottom = +halfSizeToFitOnScreen;
const left = top * aspect;
const right = bottom * aspect;
const orthoZNear = 0.5;
const orthoZFar = 10;
const ortho = m4.ortho(left, right, top, bottom, orthoZNear, orthoZFar);
let perspMixAmount;
let camMixAmount;
switch (settings.projectionMode) {
case 0: // 2d
perspMixAmount = 0;
break;
case 1: // 3d
perspMixAmount = 1;
break;
case 2: // animated
perspMixAmount = Math.sin(time) * .5 + .5;
break;
}
switch (settings.cameraMode) {
case 0: // top
camMixAmount = 0;
break;
case 1: // angle
camMixAmount = 1;
break;
case 2: // animated
camMixAmount = Math.sin(time) * .5 + .5;
break;
}
const projection = [];
for (let i = 0; i < 16; ++i) {
projection[i] = lerp(ortho[i], persp[i], perspMixAmount);
}
const perspEye = [1, 4, -6];
const perspTarget = [0, 0, 0];
const perspUp = [0, 1, 0];
const orthoEye = [0, heightAboveGroundInTopView, 0];
const orthoTarget = [0, 0, 0];
const orthoUp = [0, 0, 1];
const eye = v3.lerp(orthoEye, perspEye, camMixAmount);
const target = v3.lerp(orthoTarget, perspTarget, camMixAmount);
const up = v3.lerp(orthoUp, perspUp, camMixAmount);
const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
const viewProjection = m4.multiply(projection, view);
gl.useProgram(programInfo.program);
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const t = time * .1;
for (let z = -1; z <= 1; ++z) {
for (let x = -1; x <= 1; ++x) {
const world = m4.translation([x * 1.4, 0, z * 1.4]);
m4.rotateY(world, t + z + x, world);
// calls gl.uniformXXX
twgl.setUniforms(programInfo, {
u_texture: tex,
u_worldViewProjection: m4.multiply(viewProjection, world),
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo);
}
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
setupRadioButtons("proj", "projectionMode");
setupRadioButtons("cam", "cameraMode");
setupSlider("#fovSlider", "#fov", "fov");
function setupSlider(sliderId, labelId, property) {
const slider = document.querySelector(sliderId);
const label = document.querySelector(labelId);
function updateLabel() {
label.textContent = settings[property];
}
slider.addEventListener('input', e => {
settings[property] = parseInt(slider.value);
updateLabel();
});
updateLabel();
slider.value = settings[property];
}
function setupRadioButtons(name, property) {
document.querySelectorAll(`input[name=${name}]`).forEach(elem => {
elem.addEventListener('change', e => {
if (e.target.checked) {
settings[property] = parseInt(e.target.value);
}
});
});
}
function lerp(a, b, l) {
return a + (b - a) * l;
}
body { margin: 0; }
canvas { display: block; width: 100vw; height: 100vh; }
#ui {
position: absolute;
left: 10px;
top: 10px;
z-index: 2;
background: rgba(255, 255, 255, 0.9);
padding: .5em;
}
<script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
<canvas id="c"></canvas>
<div id="ui">
<div>projection:</div>
<div><input type="radio" name="proj" value="0" /><label for="2d">orthographic</label></div>
<div><input type="radio" name="proj" value="1" /><label for="3d">perspective</label></div>
<div><input type="radio" name="proj" value="2" checked/><label for="animated">animated</label></div>
<div> </div>
<div>camera:</div>
<div><input type="radio" name="cam" value="0" /><label for="top">top</label></div>
<div><input type="radio" name="cam" value="1" /><label for="angle">angle</label></div>
<div><input type="radio" name="cam" value="2" checked/><label for="animated">animated</label></div>
<div> </div>
<div>field of view[<span id="fov"></span>]</div>
<div><input id="fovSlider" type="range" min="10" max="90" value="60"/></div>
</div>

Resources