Skip to content

Instantly share code, notes, and snippets.

@miklosme
Created February 3, 2022 17:20
Show Gist options
  • Save miklosme/31b65cf0fbef70644012a4dcbbe5647d to your computer and use it in GitHub Desktop.
Save miklosme/31b65cf0fbef70644012a4dcbbe5647d to your computer and use it in GitHub Desktop.
import { useEffect, useRef, useState } from "react"
import { addPropertyControls, ControlType } from "framer"
// This is an unanimated 4 color gradient code component for Framer.
// TODO: support animation
function rgbToHex(rgb) {
if (rgb.startsWith("rgb(")) {
// Choose correct separator
let sep = rgb.indexOf(",") > -1 ? "," : " "
// Turn "rgb(r,g,b)" into [r,g,b]
rgb = rgb.substr(4).split(")")[0].split(sep)
let r = (+rgb[0]).toString(16),
g = (+rgb[1]).toString(16),
b = (+rgb[2]).toString(16)
if (r.length == 1) r = "0" + r
if (g.length == 1) g = "0" + g
if (b.length == 1) b = "0" + b
return "#" + r + g + b
}
return rgb
}
function hexColorToNumber(hex) {
//Check if shorthand hex value was used and double the length so the conversion in normalizeColor will work.
if (4 === hex.length) {
const hexTemp = hex
.substr(1)
.split("")
.map((hexTemp) => hexTemp + hexTemp)
.join("")
hex = `#${hexTemp}`
}
return `0x${hex.substr(1)}`
}
function normalizeColor(hexCode) {
return [
((hexCode >> 16) & 255) / 255,
((hexCode >> 8) & 255) / 255,
(255 & hexCode) / 255,
]
}
function fixColorFormat(color) {
return normalizeColor(hexColorToNumber(rgbToHex(color)))
}
function createMulticolorMaterial({ colors }) {
colors = colors.map(rgbToHex).map(hexColorToNumber).map(normalizeColor)
const vertex = `
varying vec2 v_texcoord;
void main() {
// float time = u_time;
float tilt = resolution.y / 2.0 * uvNorm.y;
vec3 pos = vec3(
position.x,
position.y + tilt,
position.z
);
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
v_texcoord = uv;
}
`
const fragment = `
varying vec2 v_texcoord;
void main() {
vec3 l = mix(bl, tl, v_texcoord.t);
vec3 r = mix(br, tr, v_texcoord.t);
vec3 c = mix(l, r, v_texcoord.s);
gl_FragColor = vec4(c, 1);
}
`
const uniforms = {
u_time: new this.minigl.Uniform({
value: 0,
}),
tl: new this.minigl.Uniform({
value: colors[0],
type: "vec3",
}),
tr: new this.minigl.Uniform({
value: colors[1],
type: "vec3",
}),
bl: new this.minigl.Uniform({
value: colors[2],
type: "vec3",
}),
br: new this.minigl.Uniform({
value: colors[3],
type: "vec3",
}),
}
return new this.minigl.Material(vertex, fragment, uniforms)
}
class Gradient {
constructor({ createMaterial, materialArgs, height }) {
this.height = height
this.playing = false
this.time = 1253106
this.last = 0
this.createMaterial = createMaterial
this.materialArgs = materialArgs
}
init = (el) => {
this.minigl = new MiniGL(el)
this.material = this.createMaterial(this.materialArgs)
this.geometry = new this.minigl.PlaneGeometry()
this.mesh = new this.minigl.Mesh(this.geometry, this.material)
this.resize()
// this.playing = true
// requestAnimationFrame(this.animate)
// window.addEventListener("resize", this.resize)
}
resize = () => {
this.width = window.innerWidth
this.minigl.setSize(this.width, this.height)
this.minigl.setOrthographicCamera()
this.mesh.geometry.setSize(this.width, this.height)
}
// animate = (delta) => {
// if (0 !== this.last && this.isStatic) {
// this.minigl.render()
// this.disconnect()
// return
// }
// if (!this.shouldSkipFrame(delta)) {
// this.time += Math.min(delta - this.last, 1e3 / 15)
// this.last = delta
// this.mesh.material.uniforms.u_time.value = this.time
// this.minigl.render()
// }
// if (this.playing) {
// requestAnimationFrame(this.animate)
// }
// }
shouldSkipFrame = (delta) => {
return (
!!window.document.hidden ||
!this.playing ||
parseInt(delta, 10) % 2 === 0
)
}
disconnect = () => {
// window.removeEventListener("resize", this.resize)
}
}
/**
* These annotations control how your component sizes
* Learn more: https://www.framer.com/docs/guides/auto-sizing
*
* @framerSupportedLayoutWidth any
* @framerSupportedLayoutHeight any
*/
export default function FourColorGradient({ tl, tr, bl, br, style }) {
const [gradient] = useState(
() =>
new Gradient({
createMaterial: createMulticolorMaterial,
materialArgs: { colors: [tl, tr, bl, br] },
height: 300,
})
)
const ref = useRef(null)
useEffect(() => {
if (!ref.current) return
gradient.init(ref.current)
gradient.minigl.render()
}, [ref.current])
return <canvas ref={ref} style={style} />
}
FourColorGradient.defaultProps = {
tl: "#09F",
tr: "#09F",
bl: "#09F",
br: "#09F",
}
addPropertyControls(FourColorGradient, {
tl: {
title: "Top-left",
type: ControlType.Color,
},
tr: {
title: "Top-right",
type: ControlType.Color,
},
bl: {
title: "Bottom-left",
type: ControlType.Color,
},
br: {
title: "Bottom-right",
type: ControlType.Color,
},
})
//
// MiniGL
//
class MiniGL {
constructor(canvas, width, height, debug = () => {}) {
this.canvas = canvas
this.meshes = []
this.lastDebugMsg = 0
this.debug = debug
if (width && height) {
this.setSize(width, height)
}
const context = this.canvas.getContext("webgl", {
antialias: true,
})
this.gl = context
const _miniGl = this
Object.defineProperties(this, {
Material: {
enumerable: false,
value: class {
constructor(vertexShaders, fragments, uniforms = {}) {
const material = this
function getShaderByType(type, source) {
const shader = context.createShader(type)
return (
context.shaderSource(shader, source),
context.compileShader(shader),
context.getShaderParameter(
shader,
context.COMPILE_STATUS
) ||
console.error(
context.getShaderInfoLog(shader)
),
_miniGl.debug("Material.compileShaderSource", {
source: source,
}),
shader
)
}
function getUniformVariableDeclarations(
uniforms,
type
) {
return Object.entries(uniforms)
.map(([uniform, value]) =>
value.getDeclaration(uniform, type)
)
.join("\n")
}
;(material.uniforms = uniforms),
(material.uniformInstances = [])
const prefix =
"\n precision highp float;\n "
;(material.vertexSource = `\n ${prefix}\n attribute vec4 position;\n attribute vec2 uv;\n attribute vec2 uvNorm;\n ${getUniformVariableDeclarations(
_miniGl.commonUniforms,
"vertex"
)}\n ${getUniformVariableDeclarations(
uniforms,
"vertex"
)}\n ${vertexShaders}\n `),
(material.Source = `\n ${prefix}\n ${getUniformVariableDeclarations(
_miniGl.commonUniforms,
"fragment"
)}\n ${getUniformVariableDeclarations(
uniforms,
"fragment"
)}\n ${fragments}\n `),
(material.vertexShader = getShaderByType(
context.VERTEX_SHADER,
material.vertexSource
)),
(material.fragmentShader = getShaderByType(
context.FRAGMENT_SHADER,
material.Source
)),
(material.program = context.createProgram()),
context.attachShader(
material.program,
material.vertexShader
),
context.attachShader(
material.program,
material.fragmentShader
),
context.linkProgram(material.program),
context.getProgramParameter(
material.program,
context.LINK_STATUS
) ||
console.error(
context.getProgramInfoLog(material.program)
),
context.useProgram(material.program),
material.attachUniforms(
void 0,
_miniGl.commonUniforms
),
material.attachUniforms(void 0, material.uniforms)
}
//t = uniform
attachUniforms(name, uniforms) {
//n = material
const material = this
void 0 === name
? Object.entries(uniforms).forEach(
([name, uniform]) => {
material.attachUniforms(name, uniform)
}
)
: "array" == uniforms.type
? uniforms.value.forEach((uniform, i) =>
material.attachUniforms(
`${name}[${i}]`,
uniform
)
)
: "struct" == uniforms.type
? Object.entries(uniforms.value).forEach(
([uniform, i]) =>
material.attachUniforms(
`${name}.${uniform}`,
i
)
)
: (_miniGl.debug("Material.attachUniforms", {
name: name,
uniform: uniforms,
}),
material.uniformInstances.push({
uniform: uniforms,
location: context.getUniformLocation(
material.program,
name
),
}))
}
},
},
Uniform: {
enumerable: !1,
value: class {
constructor(e) {
;(this.type = "float"), Object.assign(this, e)
;(this.typeFn =
{
float: "1f",
int: "1i",
vec2: "2fv",
vec3: "3fv",
vec4: "4fv",
mat4: "Matrix4fv",
}[this.type] || "1f"),
this.update()
}
update(value) {
void 0 !== this.value &&
context[`uniform${this.typeFn}`](
value,
0 === this.typeFn.indexOf("Matrix")
? this.transpose
: this.value,
0 === this.typeFn.indexOf("Matrix")
? this.value
: null
)
}
//e - name
//t - type
//n - length
getDeclaration(name, type, length) {
const uniform = this
if (uniform.excludeFrom !== type) {
if ("array" === uniform.type)
return (
uniform.value[0].getDeclaration(
name,
type,
uniform.value.length
) +
`\nconst int ${name}_length = ${uniform.value.length};`
)
if ("struct" === uniform.type) {
let name_no_prefix = name.replace("u_", "")
return (
(name_no_prefix =
name_no_prefix.charAt(0).toUpperCase() +
name_no_prefix.slice(1)),
`uniform struct ${name_no_prefix}
{\n` +
Object.entries(uniform.value)
.map(([name, uniform]) =>
uniform
.getDeclaration(name, type)
.replace(/^uniform/, "")
)
.join("") +
`\n} ${name}${
length > 0 ? `[${length}]` : ""
};`
)
}
return `uniform ${uniform.type} ${name}${
length > 0 ? `[${length}]` : ""
};`
}
}
},
},
PlaneGeometry: {
enumerable: !1,
value: class {
constructor(width, height, n, i, orientation) {
context.createBuffer(),
(this.attributes = {
position: new _miniGl.Attribute({
target: context.ARRAY_BUFFER,
size: 3,
}),
uv: new _miniGl.Attribute({
target: context.ARRAY_BUFFER,
size: 2,
}),
uvNorm: new _miniGl.Attribute({
target: context.ARRAY_BUFFER,
size: 2,
}),
index: new _miniGl.Attribute({
target: context.ELEMENT_ARRAY_BUFFER,
size: 3,
type: context.UNSIGNED_SHORT,
}),
}),
this.setTopology(n, i),
this.setSize(width, height, orientation)
}
setTopology(e = 1, t = 1) {
const n = this
;(n.xSegCount = e),
(n.ySegCount = t),
(n.vertexCount =
(n.xSegCount + 1) * (n.ySegCount + 1)),
(n.quadCount = n.xSegCount * n.ySegCount * 2),
(n.attributes.uv.values = new Float32Array(
2 * n.vertexCount
)),
(n.attributes.uvNorm.values = new Float32Array(
2 * n.vertexCount
)),
(n.attributes.index.values = new Uint16Array(
3 * n.quadCount
))
for (let e = 0; e <= n.ySegCount; e++)
for (let t = 0; t <= n.xSegCount; t++) {
const i = e * (n.xSegCount + 1) + t
if (
((n.attributes.uv.values[2 * i] =
t / n.xSegCount),
(n.attributes.uv.values[2 * i + 1] =
1 - e / n.ySegCount),
(n.attributes.uvNorm.values[2 * i] =
(t / n.xSegCount) * 2 - 1),
(n.attributes.uvNorm.values[2 * i + 1] =
1 - (e / n.ySegCount) * 2),
t < n.xSegCount && e < n.ySegCount)
) {
const s = e * n.xSegCount + t
;(n.attributes.index.values[6 * s] = i),
(n.attributes.index.values[6 * s + 1] =
i + 1 + n.xSegCount),
(n.attributes.index.values[6 * s + 2] =
i + 1),
(n.attributes.index.values[6 * s + 3] =
i + 1),
(n.attributes.index.values[6 * s + 4] =
i + 1 + n.xSegCount),
(n.attributes.index.values[6 * s + 5] =
i + 2 + n.xSegCount)
}
}
n.attributes.uv.update(),
n.attributes.uvNorm.update(),
n.attributes.index.update(),
_miniGl.debug("Geometry.setTopology", {
uv: n.attributes.uv,
uvNorm: n.attributes.uvNorm,
index: n.attributes.index,
})
}
setSize(width = 1, height = 1, orientation = "xz") {
const geometry = this
;(geometry.width = width),
(geometry.height = height),
(geometry.orientation = orientation),
(geometry.attributes.position.values &&
geometry.attributes.position.values.length ===
3 * geometry.vertexCount) ||
(geometry.attributes.position.values =
new Float32Array(3 * geometry.vertexCount))
const o = width / -2,
r = height / -2,
segment_width = width / geometry.xSegCount,
segment_height = height / geometry.ySegCount
for (
let yIndex = 0;
yIndex <= geometry.ySegCount;
yIndex++
) {
const t = r + yIndex * segment_height
for (
let xIndex = 0;
xIndex <= geometry.xSegCount;
xIndex++
) {
const r = o + xIndex * segment_width,
l =
yIndex * (geometry.xSegCount + 1) +
xIndex
;(geometry.attributes.position.values[
3 * l + "xyz".indexOf(orientation[0])
] = r),
(geometry.attributes.position.values[
3 * l + "xyz".indexOf(orientation[1])
] = -t)
}
}
geometry.attributes.position.update(),
_miniGl.debug("Geometry.setSize", {
position: geometry.attributes.position,
})
}
},
},
Mesh: {
enumerable: !1,
value: class {
constructor(geometry, material) {
const mesh = this
;(mesh.geometry = geometry),
(mesh.material = material),
(mesh.wireframe = !1),
(mesh.attributeInstances = []),
Object.entries(mesh.geometry.attributes).forEach(
([e, attribute]) => {
mesh.attributeInstances.push({
attribute: attribute,
location: attribute.attach(
e,
mesh.material.program
),
})
}
),
_miniGl.meshes.push(mesh),
_miniGl.debug("Mesh.constructor", {
mesh: mesh,
})
}
draw() {
context.useProgram(this.material.program),
this.material.uniformInstances.forEach(
({ uniform: e, location: t }) => e.update(t)
),
this.attributeInstances.forEach(
({ attribute: e, location: t }) => e.use(t)
),
context.drawElements(
this.wireframe
? context.LINES
: context.TRIANGLES,
this.geometry.attributes.index.values.length,
context.UNSIGNED_SHORT,
0
)
}
remove() {
_miniGl.meshes = _miniGl.meshes.filter((e) => e != this)
}
},
},
Attribute: {
enumerable: !1,
value: class {
constructor(e) {
;(this.type = context.FLOAT),
(this.normalized = !1),
(this.buffer = context.createBuffer()),
Object.assign(this, e),
this.update()
}
update() {
void 0 !== this.values &&
(context.bindBuffer(this.target, this.buffer),
context.bufferData(
this.target,
this.values,
context.STATIC_DRAW
))
}
attach(e, t) {
const n = context.getAttribLocation(t, e)
return (
this.target === context.ARRAY_BUFFER &&
(context.enableVertexAttribArray(n),
context.vertexAttribPointer(
n,
this.size,
this.type,
this.normalized,
0,
0
)),
n
)
}
use(e) {
context.bindBuffer(this.target, this.buffer),
this.target === context.ARRAY_BUFFER &&
(context.enableVertexAttribArray(e),
context.vertexAttribPointer(
e,
this.size,
this.type,
this.normalized,
0,
0
))
}
},
},
})
const a = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
_miniGl.commonUniforms = {
projectionMatrix: new _miniGl.Uniform({
type: "mat4",
value: a,
}),
modelViewMatrix: new _miniGl.Uniform({
type: "mat4",
value: a,
}),
resolution: new _miniGl.Uniform({
type: "vec2",
value: [1, 1],
}),
aspectRatio: new _miniGl.Uniform({
type: "float",
value: 1,
}),
}
}
setSize(e = 640, t = 480) {
;(this.width = e),
(this.height = t),
(this.canvas.width = e),
(this.canvas.height = t),
this.gl.viewport(0, 0, e, t),
(this.commonUniforms.resolution.value = [e, t]),
(this.commonUniforms.aspectRatio.value = e / t),
this.debug("MiniGL.setSize", {
width: e,
height: t,
})
}
//left, right, top, bottom, near, far
setOrthographicCamera(e = 0, t = 0, n = 0, i = -2e3, s = 2e3) {
;(this.commonUniforms.projectionMatrix.value = [
2 / this.width,
0,
0,
0,
0,
2 / this.height,
0,
0,
0,
0,
2 / (i - s),
0,
e,
t,
n,
1,
]),
this.debug(
"setOrthographicCamera",
this.commonUniforms.projectionMatrix.value
)
}
render() {
this.gl.clearColor(0, 0, 0, 0),
this.gl.clearDepth(1),
this.meshes.forEach((e) => e.draw())
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment