- canvas стартует с opacity:0 и плавно проявляется (0.9s) при .is-ready - initPageMeshShader ставит is-ready после первого кадра - modulepreload page-mesh-shader.js для параллельной загрузки с main.js - пересборка страниц (build:pages)
386 lines
12 KiB
JavaScript
386 lines
12 KiB
JavaScript
/* * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
* Paper Shaders *
|
|
* https://github.com/paper-design/shaders *
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
const heatmapMeta = {
|
|
maxColorCount: 10
|
|
};
|
|
const heatmapFragmentShader = `#version 300 es
|
|
precision highp float;
|
|
|
|
in mediump vec2 v_imageUV;
|
|
in mediump vec2 v_objectUV;
|
|
out vec4 fragColor;
|
|
|
|
uniform sampler2D u_image;
|
|
uniform float u_time;
|
|
uniform mediump float u_imageAspectRatio;
|
|
|
|
uniform vec4 u_colorBack;
|
|
uniform vec4 u_colors[${heatmapMeta.maxColorCount}];
|
|
uniform float u_colorsCount;
|
|
|
|
uniform float u_angle;
|
|
uniform float u_noise;
|
|
uniform float u_innerGlow;
|
|
uniform float u_outerGlow;
|
|
uniform float u_contour;
|
|
|
|
#define TWO_PI 6.28318530718
|
|
#define PI 3.14159265358979323846
|
|
|
|
float getImgFrame(vec2 uv, float th) {
|
|
float frame = 1.;
|
|
frame *= smoothstep(0., th, uv.y);
|
|
frame *= 1. - smoothstep(1. - th, 1., uv.y);
|
|
frame *= smoothstep(0., th, uv.x);
|
|
frame *= 1. - smoothstep(1. - th, 1., uv.x);
|
|
return frame;
|
|
}
|
|
|
|
float circle(vec2 uv, vec2 c, vec2 r) {
|
|
return 1. - smoothstep(r[0], r[1], length(uv - c));
|
|
}
|
|
|
|
float lst(float edge0, float edge1, float x) {
|
|
return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
|
|
}
|
|
|
|
float sst(float edge0, float edge1, float x) {
|
|
return smoothstep(edge0, edge1, x);
|
|
}
|
|
|
|
float shadowShape(vec2 uv, float t, float contour) {
|
|
vec2 scaledUV = uv;
|
|
|
|
// base shape tranjectory
|
|
float posY = mix(-1., 2., t);
|
|
|
|
// scaleX when it's moving down
|
|
scaledUV.y -= .5;
|
|
float mainCircleScale = sst(0., .8, posY) * lst(1.4, .9, posY);
|
|
scaledUV *= vec2(1., 1. + 1.5 * mainCircleScale);
|
|
scaledUV.y += .5;
|
|
|
|
// base shape
|
|
float innerR = .4;
|
|
float outerR = 1. - .3 * (sst(.1, .2, t) * (1. - sst(.2, .5, t)));
|
|
float s = circle(scaledUV, vec2(.5, posY - .2), vec2(innerR, outerR));
|
|
float shapeSizing = sst(.2, .3, t) * sst(.6, .3, t);
|
|
s = pow(s, 1.4);
|
|
s *= 1.2;
|
|
|
|
// flat gradient to take over the shadow shape
|
|
float topFlattener = 0.;
|
|
{
|
|
float pos = posY - uv.y;
|
|
float edge = 1.2;
|
|
topFlattener = lst(-.4, 0., pos) * (1. - sst(.0, edge, pos));
|
|
topFlattener = pow(topFlattener, 3.);
|
|
float topFlattenerMixer = (1. - sst(.0, .3, pos));
|
|
s = mix(topFlattener, s, topFlattenerMixer);
|
|
}
|
|
|
|
// apple right circle
|
|
{
|
|
float visibility = sst(.6, .7, t) * (1. - sst(.8, .9, t));
|
|
float angle = -2. -t * TWO_PI;
|
|
float rightCircle = circle(uv, vec2(.95 - .2 * cos(angle), .4 - .1 * sin(angle)), vec2(.15, .3));
|
|
rightCircle *= visibility;
|
|
s = mix(s, 0., rightCircle);
|
|
}
|
|
|
|
// apple top circle
|
|
{
|
|
float topCircle = circle(uv, vec2(.5, .19), vec2(.05, .25));
|
|
topCircle += 2. * contour * circle(uv, vec2(.5, .19), vec2(.2, .5));
|
|
float visibility = .55 * sst(.2, .3, t) * (1. - sst(.3, .45, t));
|
|
topCircle *= visibility;
|
|
s = mix(s, 0., topCircle);
|
|
}
|
|
|
|
float leafMask = circle(uv, vec2(.53, .13), vec2(.08, .19));
|
|
leafMask = mix(leafMask, 0., 1. - sst(.4, .54, uv.x));
|
|
leafMask = mix(0., leafMask, sst(.0, .2, uv.y));
|
|
leafMask *= (sst(.5, 1.1, posY) * sst(1.5, 1.3, posY));
|
|
s += leafMask;
|
|
|
|
// apple bottom circle
|
|
{
|
|
float visibility = sst(.0, .4, t) * (1. - sst(.6, .8, t));
|
|
s = mix(s, 0., visibility * circle(uv, vec2(.52, .92), vec2(.09, .25)));
|
|
}
|
|
|
|
// random balls that are invisible if apple logo is selected
|
|
{
|
|
float pos = sst(.0, .6, t) * (1. - sst(.6, 1., t));
|
|
s = mix(s, .5, circle(uv, vec2(.0, 1.2 - .5 * pos), vec2(.1, .3)));
|
|
s = mix(s, .0, circle(uv, vec2(1., .5 + .5 * pos), vec2(.1, .3)));
|
|
|
|
s = mix(s, 1., circle(uv, vec2(.95, .2 + .2 * sst(.3, .4, t) * sst(.7, .5, t)), vec2(.07, .22)));
|
|
s = mix(s, 1., circle(uv, vec2(.95, .2 + .2 * sst(.3, .4, t) * (1. - sst(.5, .7, t))), vec2(.07, .22)));
|
|
s /= max(1e-4, sst(1., .85, uv.y));
|
|
}
|
|
|
|
s = clamp(0., 1., s);
|
|
return s;
|
|
}
|
|
|
|
float blurEdge3x3(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, float centerSample) {
|
|
vec2 texel = 1.0 / vec2(textureSize(tex, 0));
|
|
vec2 r = radius * texel;
|
|
|
|
float w1 = 1.0, w2 = 2.0, w4 = 4.0;
|
|
float norm = 16.0;
|
|
float sum = w4 * centerSample;
|
|
|
|
sum += w2 * textureGrad(tex, uv + vec2(0.0, -r.y), dudx, dudy).g;
|
|
sum += w2 * textureGrad(tex, uv + vec2(0.0, r.y), dudx, dudy).g;
|
|
sum += w2 * textureGrad(tex, uv + vec2(-r.x, 0.0), dudx, dudy).g;
|
|
sum += w2 * textureGrad(tex, uv + vec2(r.x, 0.0), dudx, dudy).g;
|
|
|
|
sum += w1 * textureGrad(tex, uv + vec2(-r.x, -r.y), dudx, dudy).g;
|
|
sum += w1 * textureGrad(tex, uv + vec2(r.x, -r.y), dudx, dudy).g;
|
|
sum += w1 * textureGrad(tex, uv + vec2(-r.x, r.y), dudx, dudy).g;
|
|
sum += w1 * textureGrad(tex, uv + vec2(r.x, r.y), dudx, dudy).g;
|
|
|
|
return sum / norm;
|
|
}
|
|
|
|
void main() {
|
|
vec2 uv = v_objectUV + .5;
|
|
uv.y = 1. - uv.y;
|
|
|
|
vec2 imgUV = v_imageUV;
|
|
imgUV -= .5;
|
|
imgUV *= 0.5714285714285714;
|
|
imgUV += .5;
|
|
float imgSoftFrame = getImgFrame(imgUV, .03);
|
|
|
|
vec4 img = texture(u_image, imgUV);
|
|
vec2 dudx = dFdx(imgUV);
|
|
vec2 dudy = dFdy(imgUV);
|
|
|
|
if (img.a == 0.) {
|
|
fragColor = u_colorBack;
|
|
return;
|
|
}
|
|
|
|
float t = .1 * u_time;
|
|
t -= .3;
|
|
|
|
float tCopy = t + 1. / 3.;
|
|
float tCopy2 = t + 2. / 3.;
|
|
|
|
t = mod(t, 1.);
|
|
tCopy = mod(tCopy, 1.);
|
|
tCopy2 = mod(tCopy2, 1.);
|
|
|
|
vec2 animationUV = imgUV - vec2(.5);
|
|
float angle = -u_angle * PI / 180.;
|
|
float cosA = cos(angle);
|
|
float sinA = sin(angle);
|
|
animationUV = vec2(
|
|
animationUV.x * cosA - animationUV.y * sinA,
|
|
animationUV.x * sinA + animationUV.y * cosA
|
|
) + vec2(.5);
|
|
|
|
float shape = img[0];
|
|
|
|
img[1] = blurEdge3x3(u_image, imgUV, dudx, dudy, 8., img[1]);
|
|
|
|
float outerBlur = 1. - mix(1., img[1], shape);
|
|
float innerBlur = mix(img[1], 0., shape);
|
|
float contour = mix(img[2], 0., shape);
|
|
|
|
outerBlur *= imgSoftFrame;
|
|
|
|
float shadow = shadowShape(animationUV, t, innerBlur);
|
|
float shadowCopy = shadowShape(animationUV, tCopy, innerBlur);
|
|
float shadowCopy2 = shadowShape(animationUV, tCopy2, innerBlur);
|
|
|
|
float inner = .8 + .8 * innerBlur;
|
|
inner = mix(inner, 0., shadow);
|
|
inner = mix(inner, 0., shadowCopy);
|
|
inner = mix(inner, 0., shadowCopy2);
|
|
|
|
inner *= mix(0., 2., u_innerGlow);
|
|
|
|
inner += (u_contour * 2.) * contour;
|
|
inner = min(1., inner);
|
|
inner *= (1. - shape);
|
|
|
|
float outer = 0.;
|
|
{
|
|
t *= 3.;
|
|
t = mod(t - .1, 1.);
|
|
|
|
outer = .9 * pow(outerBlur, .8);
|
|
float y = mod(animationUV.y - t, 1.);
|
|
float animatedMask = sst(.3, .65, y) * (1. - sst(.65, 1., y));
|
|
animatedMask = .5 + animatedMask;
|
|
outer *= animatedMask;
|
|
outer *= mix(0., 5., pow(u_outerGlow, 2.));
|
|
outer *= imgSoftFrame;
|
|
}
|
|
|
|
inner = pow(inner, 1.2);
|
|
float heat = clamp(inner + outer, 0., 1.);
|
|
|
|
heat += (.005 + .35 * u_noise) * (fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453123) - .5);
|
|
|
|
float mixer = heat * u_colorsCount;
|
|
vec4 gradient = u_colors[0];
|
|
gradient.rgb *= gradient.a;
|
|
float outerShape = 0.;
|
|
for (int i = 1; i < ${heatmapMeta.maxColorCount + 1}; i++) {
|
|
if (i > int(u_colorsCount)) break;
|
|
float m = clamp(mixer - float(i - 1), 0., 1.);
|
|
if (i == 1) {
|
|
outerShape = m;
|
|
}
|
|
vec4 c = u_colors[i - 1];
|
|
c.rgb *= c.a;
|
|
gradient = mix(gradient, c, m);
|
|
}
|
|
|
|
vec3 color = gradient.rgb * outerShape;
|
|
float opacity = gradient.a * outerShape;
|
|
|
|
vec3 bgColor = u_colorBack.rgb * u_colorBack.a;
|
|
color = color + bgColor * (1.0 - opacity);
|
|
opacity = opacity + u_colorBack.a * (1.0 - opacity);
|
|
|
|
color += .02 * (fract(sin(dot(uv + 1., vec2(12.9898, 78.233))) * 43758.5453123) - .5);
|
|
|
|
fragColor = vec4(color, opacity);
|
|
}
|
|
`;
|
|
function toProcessedHeatmap(file) {
|
|
const canvas = document.createElement("canvas");
|
|
const canvasSize = 1e3;
|
|
return new Promise((resolve, reject) => {
|
|
const image = new Image();
|
|
image.crossOrigin = "anonymous";
|
|
image.addEventListener("load", () => {
|
|
if (typeof file === "string" ? file.endsWith(".svg") : file.type === "image/svg+xml") {
|
|
image.width = canvasSize;
|
|
image.height = canvasSize;
|
|
}
|
|
const ratio = image.naturalWidth / image.naturalHeight;
|
|
const maxBlur = Math.floor(canvasSize * 0.15);
|
|
const padding = Math.ceil(maxBlur * 2.5);
|
|
let imgWidth = canvasSize;
|
|
let imgHeight = canvasSize;
|
|
if (ratio > 1) {
|
|
imgHeight = Math.floor(canvasSize / ratio);
|
|
} else {
|
|
imgWidth = Math.floor(canvasSize * ratio);
|
|
}
|
|
canvas.width = imgWidth + 2 * padding;
|
|
canvas.height = imgHeight + 2 * padding;
|
|
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
if (!ctx) {
|
|
throw new Error("Failed to get canvas 2d context");
|
|
}
|
|
ctx.fillStyle = "white";
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
ctx.drawImage(image, padding, padding, imgWidth, imgHeight);
|
|
const { width, height } = canvas;
|
|
const srcImageData = ctx.getImageData(0, 0, width, height);
|
|
const src = srcImageData.data;
|
|
const totalPixels = width * height;
|
|
const gray = new Uint8ClampedArray(totalPixels);
|
|
for (let i = 0; i < totalPixels; i++) {
|
|
const px = i * 4;
|
|
const r = src[px] ?? 0;
|
|
const g = src[px + 1] ?? 0;
|
|
const b = src[px + 2] ?? 0;
|
|
gray[i] = 0.299 * r + 0.587 * g + 0.114 * b | 0;
|
|
}
|
|
const bigBlurRadius = maxBlur;
|
|
const innerBlurRadius = Math.max(1, Math.round(0.12 * maxBlur));
|
|
const contourRadius = 5;
|
|
const bigBlurGray = multiPassBlurGray(gray, width, height, bigBlurRadius, 3);
|
|
const innerBlurGray = multiPassBlurGray(gray, width, height, innerBlurRadius, 3);
|
|
const contourGray = multiPassBlurGray(gray, width, height, contourRadius, 1);
|
|
const processedImageData = ctx.createImageData(width, height);
|
|
const dst = processedImageData.data;
|
|
for (let i = 0; i < totalPixels; i++) {
|
|
const px = i * 4;
|
|
dst[px] = contourGray[i] ?? 0;
|
|
dst[px + 1] = bigBlurGray[i] ?? 0;
|
|
dst[px + 2] = innerBlurGray[i] ?? 0;
|
|
dst[px + 3] = 255;
|
|
}
|
|
ctx.putImageData(processedImageData, 0, 0);
|
|
canvas.toBlob((blob) => {
|
|
if (!blob) {
|
|
reject(new Error("Failed to create PNG blob"));
|
|
return;
|
|
}
|
|
resolve({ blob });
|
|
}, "image/png");
|
|
});
|
|
image.addEventListener("error", () => {
|
|
reject(new Error("Failed to load image"));
|
|
});
|
|
image.src = typeof file === "string" ? file : URL.createObjectURL(file);
|
|
});
|
|
}
|
|
function blurGray(gray, width, height, radius) {
|
|
if (radius <= 0) {
|
|
return gray.slice();
|
|
}
|
|
const out = new Uint8ClampedArray(width * height);
|
|
const integral = new Uint32Array(width * height);
|
|
for (let y = 0; y < height; y++) {
|
|
let rowSum = 0;
|
|
for (let x = 0; x < width; x++) {
|
|
const idx = y * width + x;
|
|
const v = gray[idx] ?? 0;
|
|
rowSum += v;
|
|
integral[idx] = rowSum + (y > 0 ? integral[idx - width] ?? 0 : 0);
|
|
}
|
|
}
|
|
for (let y = 0; y < height; y++) {
|
|
const y1 = Math.max(0, y - radius);
|
|
const y2 = Math.min(height - 1, y + radius);
|
|
for (let x = 0; x < width; x++) {
|
|
const x1 = Math.max(0, x - radius);
|
|
const x2 = Math.min(width - 1, x + radius);
|
|
const idxA = y2 * width + x2;
|
|
const idxB = y2 * width + (x1 - 1);
|
|
const idxC = (y1 - 1) * width + x2;
|
|
const idxD = (y1 - 1) * width + (x1 - 1);
|
|
const A = integral[idxA] ?? 0;
|
|
const B = x1 > 0 ? integral[idxB] ?? 0 : 0;
|
|
const C = y1 > 0 ? integral[idxC] ?? 0 : 0;
|
|
const D = x1 > 0 && y1 > 0 ? integral[idxD] ?? 0 : 0;
|
|
const sum = A - B - C + D;
|
|
const area = (x2 - x1 + 1) * (y2 - y1 + 1);
|
|
out[y * width + x] = Math.round(sum / area);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
function multiPassBlurGray(gray, width, height, radius, passes) {
|
|
if (radius <= 0 || passes <= 1) {
|
|
return blurGray(gray, width, height, radius);
|
|
}
|
|
let input = gray;
|
|
let tmp = gray;
|
|
for (let p = 0; p < passes; p++) {
|
|
tmp = blurGray(input, width, height, radius);
|
|
input = tmp;
|
|
}
|
|
return tmp;
|
|
}
|
|
export {
|
|
heatmapFragmentShader,
|
|
heatmapMeta,
|
|
toProcessedHeatmap
|
|
};
|
|
//# sourceMappingURL=heatmap.js.map
|