mirror of
https://github.com/hanzoai/insights-js.git
synced 2026-11-03 00:53:53 +00:00
in #2561 we need to test inline image resizing and canvas recording. this adds the examples i'll need to do that in the nextjs playground
418 lines
13 KiB
TypeScript
418 lines
13 KiB
TypeScript
import React from 'react'
|
|
import { useEffect, useRef } from 'react'
|
|
|
|
function Starfield2D() {
|
|
const ref = useRef<HTMLCanvasElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (ref.current) {
|
|
const canvas = ref.current
|
|
const context = canvas.getContext('2d')
|
|
|
|
if (canvas && context) {
|
|
const numStars = 1000
|
|
const radius = 1
|
|
let focalLength = canvas.width
|
|
|
|
let centerX = 0
|
|
let centerY = 0
|
|
|
|
let stars: { x: number; y: number; z: number }[] = []
|
|
let star = null
|
|
let i = 0
|
|
|
|
let animate = false
|
|
|
|
const executeFrame = () => {
|
|
if (animate) requestAnimationFrame(executeFrame)
|
|
moveStars()
|
|
drawStars()
|
|
}
|
|
|
|
const initializeStars = () => {
|
|
centerX = canvas.width / 2
|
|
centerY = canvas.height / 2
|
|
|
|
stars = []
|
|
for (i = 0; i < numStars; i++) {
|
|
star = {
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
z: Math.random() * canvas.width,
|
|
}
|
|
stars.push(star)
|
|
}
|
|
}
|
|
|
|
const moveStars = () => {
|
|
for (i = 0; i < numStars; i++) {
|
|
star = stars[i]
|
|
star.z--
|
|
|
|
if (star.z <= 0) {
|
|
star.z = canvas.width
|
|
}
|
|
}
|
|
}
|
|
|
|
const drawStars = () => {
|
|
let pixelX = 0
|
|
let pixelY = 0
|
|
let pixelRadius = 0
|
|
|
|
if (canvas.width != window.innerWidth || canvas.height != window.innerHeight) {
|
|
canvas.width = window.innerWidth
|
|
canvas.height = window.innerHeight
|
|
initializeStars()
|
|
}
|
|
|
|
context.fillStyle = 'black'
|
|
context.fillRect(0, 0, canvas.width, canvas.height)
|
|
context.fillStyle = 'white'
|
|
for (i = 0; i < numStars; i++) {
|
|
star = stars[i]
|
|
|
|
pixelX = (star.x - centerX) * (focalLength / star.z)
|
|
pixelX += centerX
|
|
pixelY = (star.y - centerY) * (focalLength / star.z)
|
|
pixelY += centerY
|
|
pixelRadius = radius * (focalLength / star.z)
|
|
|
|
context.beginPath()
|
|
context.arc(pixelX, pixelY, pixelRadius, 0, 2 * Math.PI)
|
|
context.fill()
|
|
}
|
|
}
|
|
|
|
canvas.addEventListener(
|
|
'mousemove',
|
|
function (e) {
|
|
focalLength = e.x
|
|
},
|
|
{ passive: true }
|
|
)
|
|
|
|
canvas.addEventListener(
|
|
'mouseover',
|
|
function () {
|
|
animate = true
|
|
executeFrame()
|
|
},
|
|
{ passive: true }
|
|
)
|
|
|
|
canvas.addEventListener(
|
|
'mouseout',
|
|
function () {
|
|
animate = false
|
|
},
|
|
{ passive: true }
|
|
)
|
|
|
|
initializeStars()
|
|
executeFrame()
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
return <canvas ref={ref} style={{ width: '200px', height: '200px', display: 'block' }}></canvas>
|
|
}
|
|
|
|
function RotatingCubeWebGL() {
|
|
const ref = useRef<HTMLCanvasElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (!ref.current) return
|
|
|
|
const canvas = ref.current
|
|
const gl = canvas.getContext('webgl')
|
|
|
|
if (!gl) {
|
|
console.error('WebGL not supported')
|
|
return
|
|
}
|
|
|
|
const vertexShaderSource = `
|
|
attribute vec4 aVertexPosition;
|
|
attribute vec4 aVertexColor;
|
|
uniform mat4 uModelViewMatrix;
|
|
uniform mat4 uProjectionMatrix;
|
|
varying lowp vec4 vColor;
|
|
void main(void) {
|
|
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
|
|
vColor = aVertexColor;
|
|
}
|
|
`
|
|
|
|
const fragmentShaderSource = `
|
|
varying lowp vec4 vColor;
|
|
void main(void) {
|
|
gl_FragColor = vColor;
|
|
}
|
|
`
|
|
|
|
const createShader = (type: number, source: string) => {
|
|
const shader = gl.createShader(type)
|
|
if (!shader) return null
|
|
gl.shaderSource(shader, source)
|
|
gl.compileShader(shader)
|
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
console.error('Shader compilation error:', gl.getShaderInfoLog(shader))
|
|
gl.deleteShader(shader)
|
|
return null
|
|
}
|
|
return shader
|
|
}
|
|
|
|
const vertexShader = createShader(gl.VERTEX_SHADER, vertexShaderSource)
|
|
const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentShaderSource)
|
|
|
|
if (!vertexShader || !fragmentShader) return
|
|
|
|
const shaderProgram = gl.createProgram()
|
|
if (!shaderProgram) return
|
|
|
|
gl.attachShader(shaderProgram, vertexShader)
|
|
gl.attachShader(shaderProgram, fragmentShader)
|
|
gl.linkProgram(shaderProgram)
|
|
|
|
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
|
|
console.error('Program linking error:', gl.getProgramInfoLog(shaderProgram))
|
|
return
|
|
}
|
|
|
|
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'),
|
|
},
|
|
}
|
|
|
|
const positions = [
|
|
-1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0,
|
|
-1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0,
|
|
1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
|
|
-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0,
|
|
]
|
|
|
|
const positionBuffer = gl.createBuffer()
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW)
|
|
|
|
const faceColors = [
|
|
[1.0, 0.0, 0.0, 1.0],
|
|
[0.0, 1.0, 0.0, 1.0],
|
|
[0.0, 0.0, 1.0, 1.0],
|
|
[1.0, 1.0, 0.0, 1.0],
|
|
[1.0, 0.0, 1.0, 1.0],
|
|
[0.0, 1.0, 1.0, 1.0],
|
|
]
|
|
|
|
let colors: number[] = []
|
|
for (let i = 0; i < faceColors.length; i++) {
|
|
const c = faceColors[i]
|
|
colors = colors.concat(c, c, c, c)
|
|
}
|
|
|
|
const colorBuffer = gl.createBuffer()
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer)
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW)
|
|
|
|
const indices = [
|
|
0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20,
|
|
21, 22, 20, 22, 23,
|
|
]
|
|
|
|
const indexBuffer = gl.createBuffer()
|
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
|
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW)
|
|
|
|
let rotation = 0
|
|
let animate = true
|
|
|
|
const drawScene = () => {
|
|
gl.clearColor(0.0, 0.0, 0.0, 1.0)
|
|
gl.clearDepth(1.0)
|
|
gl.enable(gl.DEPTH_TEST)
|
|
gl.depthFunc(gl.LEQUAL)
|
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
|
|
|
const fieldOfView = (45 * Math.PI) / 180
|
|
const aspect = canvas.clientWidth / canvas.clientHeight
|
|
const zNear = 0.1
|
|
const zFar = 100.0
|
|
const projectionMatrix = mat4Create()
|
|
mat4Perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar)
|
|
|
|
const modelViewMatrix = mat4Create()
|
|
mat4Translate(modelViewMatrix, modelViewMatrix, [0.0, 0.0, -6.0])
|
|
mat4RotateX(modelViewMatrix, modelViewMatrix, rotation)
|
|
mat4RotateY(modelViewMatrix, modelViewMatrix, rotation * 0.7)
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
|
|
gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, 3, gl.FLOAT, false, 0, 0)
|
|
gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition)
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer)
|
|
gl.vertexAttribPointer(programInfo.attribLocations.vertexColor, 4, gl.FLOAT, false, 0, 0)
|
|
gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor)
|
|
|
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
|
|
|
|
gl.useProgram(programInfo.program)
|
|
|
|
gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, projectionMatrix)
|
|
gl.uniformMatrix4fv(programInfo.uniformLocations.modelViewMatrix, false, modelViewMatrix)
|
|
|
|
gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0)
|
|
|
|
if (animate) {
|
|
rotation += 0.01
|
|
requestAnimationFrame(drawScene)
|
|
}
|
|
}
|
|
|
|
canvas.addEventListener(
|
|
'mouseover',
|
|
() => {
|
|
animate = true
|
|
drawScene()
|
|
},
|
|
{ passive: true }
|
|
)
|
|
|
|
canvas.addEventListener(
|
|
'mouseout',
|
|
() => {
|
|
animate = false
|
|
},
|
|
{ passive: true }
|
|
)
|
|
|
|
drawScene()
|
|
}, [])
|
|
|
|
return (
|
|
<canvas
|
|
ref={ref}
|
|
width={200}
|
|
height={200}
|
|
style={{ width: '200px', height: '200px', display: 'block' }}
|
|
></canvas>
|
|
)
|
|
}
|
|
|
|
function mat4Create(): Float32Array {
|
|
const out = new Float32Array(16)
|
|
out[0] = 1
|
|
out[5] = 1
|
|
out[10] = 1
|
|
out[15] = 1
|
|
return out
|
|
}
|
|
|
|
function mat4Perspective(out: Float32Array, fovy: number, aspect: number, near: number, far: number) {
|
|
const f = 1.0 / Math.tan(fovy / 2)
|
|
out[0] = f / aspect
|
|
out[1] = 0
|
|
out[2] = 0
|
|
out[3] = 0
|
|
out[4] = 0
|
|
out[5] = f
|
|
out[6] = 0
|
|
out[7] = 0
|
|
out[8] = 0
|
|
out[9] = 0
|
|
out[11] = -1
|
|
out[12] = 0
|
|
out[13] = 0
|
|
out[15] = 0
|
|
if (far != null && far !== Infinity) {
|
|
const nf = 1 / (near - far)
|
|
out[10] = (far + near) * nf
|
|
out[14] = 2 * far * near * nf
|
|
} else {
|
|
out[10] = -1
|
|
out[14] = -2 * near
|
|
}
|
|
}
|
|
|
|
function mat4Translate(out: Float32Array, a: Float32Array, v: number[]) {
|
|
const x = v[0],
|
|
y = v[1],
|
|
z = v[2]
|
|
out[12] = a[0] * x + a[4] * y + a[8] * z + a[12]
|
|
out[13] = a[1] * x + a[5] * y + a[9] * z + a[13]
|
|
out[14] = a[2] * x + a[6] * y + a[10] * z + a[14]
|
|
out[15] = a[3] * x + a[7] * y + a[11] * z + a[15]
|
|
}
|
|
|
|
function mat4RotateX(out: Float32Array, a: Float32Array, rad: number) {
|
|
const s = Math.sin(rad)
|
|
const c = Math.cos(rad)
|
|
const a10 = a[4]
|
|
const a11 = a[5]
|
|
const a12 = a[6]
|
|
const a13 = a[7]
|
|
const a20 = a[8]
|
|
const a21 = a[9]
|
|
const a22 = a[10]
|
|
const a23 = a[11]
|
|
|
|
out[4] = a10 * c + a20 * s
|
|
out[5] = a11 * c + a21 * s
|
|
out[6] = a12 * c + a22 * s
|
|
out[7] = a13 * c + a23 * s
|
|
out[8] = a20 * c - a10 * s
|
|
out[9] = a21 * c - a11 * s
|
|
out[10] = a22 * c - a12 * s
|
|
out[11] = a23 * c - a13 * s
|
|
}
|
|
|
|
function mat4RotateY(out: Float32Array, a: Float32Array, rad: number) {
|
|
const s = Math.sin(rad)
|
|
const c = Math.cos(rad)
|
|
const a00 = a[0]
|
|
const a01 = a[1]
|
|
const a02 = a[2]
|
|
const a03 = a[3]
|
|
const a20 = a[8]
|
|
const a21 = a[9]
|
|
const a22 = a[10]
|
|
const a23 = a[11]
|
|
|
|
out[0] = a00 * c - a20 * s
|
|
out[1] = a01 * c - a21 * s
|
|
out[2] = a02 * c - a22 * s
|
|
out[3] = a03 * c - a23 * s
|
|
out[8] = a00 * s + a20 * c
|
|
out[9] = a01 * s + a21 * c
|
|
out[10] = a02 * s + a22 * c
|
|
out[11] = a03 * s + a23 * c
|
|
}
|
|
|
|
export default function Canvas() {
|
|
return (
|
|
<>
|
|
<h1>Canvas Examples</h1>
|
|
|
|
<section style={{ marginBottom: '2rem' }}>
|
|
<h2>2D Canvas - Starfield</h2>
|
|
<p>Hover to animate. Move mouse to adjust focal length.</p>
|
|
<Starfield2D />
|
|
</section>
|
|
|
|
<section style={{ marginBottom: '2rem' }}>
|
|
<h2>WebGL Canvas - Rotating Cube</h2>
|
|
<p>Hover to animate the cube.</p>
|
|
<RotatingCubeWebGL />
|
|
</section>
|
|
</>
|
|
)
|
|
}
|