1. Drawing a Playing Video Frame into a 2D Canvas
Code:
const canvas = document.querySelector('#canvas2d');
const ctx = canvas.getContext('2d');
let t = 0;
function drawVideoLikeFrame() {
t += 0.035;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, '#1d4ed8');
gradient.addColorStop(1, '#020617');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#22d3ee';
ctx.beginPath();
ctx.arc(210 + Math.sin(t) * 120, 120, 42, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#f59e0b';
ctx.beginPath();
ctx.arc(410 + Math.cos(t * 1.4) * 90, 150, 34, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.font = '700 24px system-ui';
ctx.fillText('2D canvas redraws every frame', 142, 230);
requestAnimationFrame(drawVideoLikeFrame);
}
drawVideoLikeFrame();
Result:
2. Treating Frames as WebGL Textures
Code:
const canvas = document.querySelector('#webglTexture');
const gl = canvas.getContext('webgl');
const vertexShaderSource = `
attribute vec2 position;
varying vec2 uv;
void main() {
uv = position * 0.5 + 0.5;
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const fragmentShaderSource = `
precision mediump float;
varying vec2 uv;
uniform float time;
void main() {
vec3 color = vec3(0.03, 0.07, 0.14);
float glow = 0.0;
glow += 0.18 / distance(uv, vec2(0.5 + sin(time) * 0.28, 0.45));
glow += 0.12 / distance(uv, vec2(0.34, 0.55 + cos(time * 1.6) * 0.18));
color += vec3(glow * 0.2, glow * 0.8, glow);
gl_FragColor = vec4(color, 1.0);
}
`;
function compile(type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
const program = gl.createProgram();
gl.attachShader(program, compile(gl.VERTEX_SHADER, vertexShaderSource));
gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fragmentShaderSource));
gl.linkProgram(program);
gl.useProgram(program);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
gl.STATIC_DRAW
);
const position = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(position);
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
const timeUniform = gl.getUniformLocation(program, 'time');
function render(time) {
gl.viewport(0, 0, canvas.width, canvas.height);
gl.uniform1f(timeUniform, time * 0.001);
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
Result:
3. CSS Screen Blend as a Cheap Transparency Trick
Code:
<div class="blend-demo">
<div class="subject"></div>
<div class="caption">mix-blend-mode: screen keeps bright effects and loses dark parts</div>
</div>
<style>
.blend-demo {
position: relative;
min-height: 260px;
padding: 26px;
background:
linear-gradient(135deg, #19324d, #391638),
repeating-linear-gradient(90deg, transparent 0 48px, rgba(255, 255, 255, 0.08) 48px 50px);
}
.blend-demo .subject {
position: absolute;
left: 50%;
top: 50%;
width: 220px;
height: 142px;
border-radius: 8px;
transform: translate(-50%, -50%);
background:
radial-gradient(circle at 22% 38%, #ffffff 0 8%, transparent 9%),
radial-gradient(circle at 76% 58%, #38bdf8 0 9%, transparent 10%),
linear-gradient(90deg, #020617 0 24%, #f8fafc 24% 52%, #111827 52% 76%, #fbbf24 76%);
mix-blend-mode: screen;
animation: pulse 1.8s ease-in-out infinite alternate;
}
</style>
Result:
mix-blend-mode: screen keeps bright effects and loses dark parts
4. Packing RGB and Alpha into One Video Frame
Code:
const canvas = document.querySelector('#alphaPacked');
const ctx = canvas.getContext('2d');
const alphaSlider = document.querySelector('#alphaStrength');
function drawPackedAlphaDemo() {
const alphaStrength = Number(alphaSlider.value) / 100;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#0f172a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#1d4ed8';
ctx.fillRect(0, 0, 340, 280);
ctx.fillStyle = '#020617';
ctx.fillRect(340, 0, 340, 280);
ctx.fillStyle = '#f97316';
ctx.beginPath();
ctx.arc(150, 136, 66, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#22c55e';
ctx.beginPath();
ctx.arc(220, 122, 48, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'white';
ctx.font = '700 18px system-ui';
ctx.fillText('RGB color area', 98, 254);
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(490, 136, 66, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = `rgba(255, 255, 255, ${alphaStrength})`;
ctx.beginPath();
ctx.arc(560, 122, 48, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'white';
ctx.fillText('Alpha mask area', 430, 254);
}
alphaSlider.addEventListener('input', drawPackedAlphaDemo);
drawPackedAlphaDemo();
Result:
5. Decoding Hidden Alpha Back into Real Transparency
Code:
const canvas = document.querySelector('#decodedAlpha');
const ctx = canvas.getContext('2d');
const toggle = document.querySelector('#toggleDecoded');
function drawDecodedAlpha() {
const useAlpha = toggle.checked;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#334155';
for (let y = 0; y < canvas.height; y += 28) {
for (let x = 0; x < canvas.width; x += 28) {
ctx.fillStyle = (x + y) % 56 === 0 ? '#e2e8f0' : '#cbd5e1';
ctx.fillRect(x, y, 28, 28);
}
}
const shapes = [
{ x: 260, y: 122, r: 74, color: '#f97316', alpha: 0.95 },
{ x: 360, y: 126, r: 58, color: '#22c55e', alpha: useAlpha ? 0.42 : 1 },
{ x: 320, y: 170, r: 42, color: '#38bdf8', alpha: useAlpha ? 0.18 : 1 }
];
shapes.forEach(shape => {
ctx.globalAlpha = shape.alpha;
ctx.fillStyle = shape.color;
ctx.beginPath();
ctx.arc(shape.x, shape.y, shape.r, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
ctx.fillStyle = '#0f172a';
ctx.font = '700 20px system-ui';
ctx.fillText(useAlpha ? 'Decoded with alpha' : 'Raw opaque color only', 236, 252);
}
toggle.addEventListener('change', drawDecodedAlpha);
drawDecodedAlpha();
Result:
6. Reading Vap-Like Config to Crop RGB and Alpha Regions
Code:
const config = {
info: {
v: 2,
f: 12,
w: 454,
h: 340,
fps: 24,
videoW: 464,
videoH: 528,
aFrame: [0, 344, 227, 170],
rgbFrame: [0, 0, 454, 340],
isVapx: 0,
orien: 0
}
};
const table = document.querySelector('#configTable');
const rows = [
['display size', `${config.info.w} × ${config.info.h}`],
['video texture', `${config.info.videoW} × ${config.info.videoH}`],
['rgbFrame', config.info.rgbFrame.join(', ')],
['aFrame', config.info.aFrame.join(', ')],
['fps', config.info.fps]
];
table.innerHTML = rows
.map(([key, value]) => `<tr><th>${key}</th><td>${value}</td></tr>`)
.join('');
Result:
Generated MP4 layout
RGB
ALPHA
vapc.json fields
7. Simulating the Vap Constructor Call
Code:
class MiniVap {
constructor(options) {
this.container = options.container;
this.width = options.width;
this.height = options.height;
this.fps = options.fps;
this.frame = 0;
this.canvas = document.createElement('canvas');
this.canvas.width = this.width;
this.canvas.height = this.height;
this.container.appendChild(this.canvas);
this.ctx = this.canvas.getContext('2d');
this.play();
}
play() {
this.frame += 1;
const t = this.frame / this.fps;
this.ctx.clearRect(0, 0, this.width, this.height);
this.ctx.fillStyle = '#07111f';
this.ctx.fillRect(0, 0, this.width, this.height);
this.ctx.globalAlpha = 0.85;
this.ctx.fillStyle = '#38bdf8';
this.ctx.beginPath();
this.ctx.arc(185 + Math.sin(t * 3) * 90, 134, 58, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.globalAlpha = 0.48;
this.ctx.fillStyle = '#f97316';
this.ctx.beginPath();
this.ctx.arc(300 + Math.cos(t * 2.2) * 70, 156, 72, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.globalAlpha = 1;
this.ctx.fillStyle = '#fff';
this.ctx.font = '700 22px system-ui';
this.ctx.fillText('new Vap({ container, src, config, fps })', 58, 282);
requestAnimationFrame(() => this.play());
}
}
new MiniVap({
container: document.querySelector('#miniVapContainer'),
src: './images/output/video.mp4',
width: 454,
height: 340,
config: { info: { w: 454, h: 340, fps: 24 } },
fps: 24
});
Result:
8. Why Picture and Sound Are Separate Concerns
Code:
const status = document.querySelector('#audioStatus');
const button = document.querySelector('#audioButton');
button.addEventListener('click', async () => {
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audio = new AudioContext();
const oscillator = audio.createOscillator();
const gain = audio.createGain();
oscillator.frequency.value = 220;
gain.gain.setValueAtTime(0.0001, audio.currentTime);
gain.gain.exponentialRampToValueAtTime(0.18, audio.currentTime + 0.04);
gain.gain.exponentialRampToValueAtTime(0.0001, audio.currentTime + 0.45);
oscillator.connect(gain);
gain.connect(audio.destination);
oscillator.start();
oscillator.stop(audio.currentTime + 0.5);
status.textContent = 'Audio was started by a user gesture.';
});
Result:
Canvas/WebGL draws pixels. Audio still needs browser playback permission.
Visual layer ≠ audio layer