perf: bake the radial gradients into sprites, short-circuit the flat field grid
This commit is contained in:
parent
802879fc3e
commit
a5bf60cc43
15
src/bolts.js
15
src/bolts.js
@ -4,6 +4,7 @@ import { FX } from './fx.js';
|
|||||||
import { Audio } from './audio.js';
|
import { Audio } from './audio.js';
|
||||||
import { Juice } from './juice.js';
|
import { Juice } from './juice.js';
|
||||||
import { Overload } from './overload.js';
|
import { Overload } from './overload.js';
|
||||||
|
import { softSprite, blit } from './sprites.js';
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// LASERHANDS.
|
// LASERHANDS.
|
||||||
@ -197,12 +198,8 @@ export class Bolts {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
// head
|
// head
|
||||||
const g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, CFG.BOLT_R * 4);
|
blit(ctx, softSprite(col), b.x, b.y, CFG.BOLT_R * 4, 0.8);
|
||||||
g.addColorStop(0, rgba('#ffffff', 0.95));
|
blit(ctx, softSprite('#ffffff'), b.x, b.y, CFG.BOLT_R * 1.6, 0.95);
|
||||||
g.addColorStop(0.35, rgba(col, 0.8));
|
|
||||||
g.addColorStop(1, rgba(col, 0));
|
|
||||||
ctx.fillStyle = g;
|
|
||||||
ctx.beginPath(); ctx.arc(b.x, b.y, CFG.BOLT_R * 4, 0, Math.PI * 2); ctx.fill();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- muzzle flash ----
|
// ---- muzzle flash ----
|
||||||
@ -211,11 +208,7 @@ export class Bolts {
|
|||||||
const a = this.muzzle / CFG.MUZZLE_LIFE;
|
const a = this.muzzle / CFG.MUZZLE_LIFE;
|
||||||
const mx = m.x + this.muzzleDir.x * (m.r + 4);
|
const mx = m.x + this.muzzleDir.x * (m.r + 4);
|
||||||
const my = m.y + this.muzzleDir.y * (m.r + 4);
|
const my = m.y + this.muzzleDir.y * (m.r + 4);
|
||||||
const g = ctx.createRadialGradient(mx, my, 0, mx, my, 26 * a);
|
blit(ctx, softSprite('#ffffff'), mx, my, 26 * a, 0.85 * a);
|
||||||
g.addColorStop(0, rgba('#ffffff', 0.85 * a));
|
|
||||||
g.addColorStop(1, 'rgba(0,0,0,0)');
|
|
||||||
ctx.fillStyle = g;
|
|
||||||
ctx.beginPath(); ctx.arc(mx, my, 26 * a, 0, Math.PI * 2); ctx.fill();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- the ammo ring: 8 pips, spins faster as it empties (nervous) ----
|
// ---- the ammo ring: 8 pips, spins faster as it empties (nervous) ----
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Audio } from './audio.js';
|
|||||||
import { Juice } from './juice.js';
|
import { Juice } from './juice.js';
|
||||||
import { Radicals } from './radicals.js';
|
import { Radicals } from './radicals.js';
|
||||||
import { unbond } from './particle.js';
|
import { unbond } from './particle.js';
|
||||||
|
import { softSprite, blit } from './sprites.js';
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// OVERLOAD.
|
// OVERLOAD.
|
||||||
@ -121,14 +122,11 @@ export class Overload {
|
|||||||
const k = 1 - p.fuse / p.fuseMax; // 0..1 toward detonation
|
const k = 1 - p.fuse / p.fuseMax; // 0..1 toward detonation
|
||||||
const pulse = 0.5 + Math.sin(world.time * (14 + k * 46)) * 0.5;
|
const pulse = 0.5 + Math.sin(world.time * (14 + k * 46)) * 0.5;
|
||||||
|
|
||||||
// white-hot core
|
// white-hot core — baked sprites, layered, so a screen full of fuses
|
||||||
|
// doesn't allocate a gradient per fuse per frame
|
||||||
const R = p.r * (1.5 + k * 1.4 + pulse * 0.35);
|
const R = p.r * (1.5 + k * 1.4 + pulse * 0.35);
|
||||||
const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, R * 2);
|
blit(ctx, softSprite(CFG.COL_POS), p.x, p.y, R * 2, 0.4 + k * 0.35);
|
||||||
g.addColorStop(0, `rgba(255,255,255,${0.55 + k * 0.4})`);
|
blit(ctx, softSprite('#FFFFFF'), p.x, p.y, R * 0.9, 0.55 + k * 0.4);
|
||||||
g.addColorStop(0.4, rgba(CFG.COL_POS, 0.4 + k * 0.35));
|
|
||||||
g.addColorStop(1, 'rgba(0,0,0,0)');
|
|
||||||
ctx.fillStyle = g;
|
|
||||||
ctx.beginPath(); ctx.arc(p.x, p.y, R * 2, 0, Math.PI * 2); ctx.fill();
|
|
||||||
|
|
||||||
// the blast radius you're about to be inside of
|
// the blast radius you're about to be inside of
|
||||||
ctx.strokeStyle = `rgba(255,45,155,${0.1 + k * 0.4})`;
|
ctx.strokeStyle = `rgba(255,45,155,${0.1 + k * 0.4})`;
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { CFG } from './config.js';
|
|||||||
import { FX } from './fx.js';
|
import { FX } from './fx.js';
|
||||||
import { Audio } from './audio.js';
|
import { Audio } from './audio.js';
|
||||||
import { Juice } from './juice.js';
|
import { Juice } from './juice.js';
|
||||||
|
import { softSprite, blit } from './sprites.js';
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// THE RADICAL.
|
// THE RADICAL.
|
||||||
@ -312,13 +313,10 @@ export class Radicals {
|
|||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// the only pure-white glow in the game
|
// the only pure-white glow in the game — one baked sprite, not a fresh
|
||||||
const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, R * 2.1);
|
// gradient per radical per frame (at 30 radicals that was 30 allocations
|
||||||
g.addColorStop(0, `rgba(255,255,255,${0.5 * flick})`);
|
// and 30 gradient rasterisations every frame)
|
||||||
g.addColorStop(0.45, `rgba(255,255,255,${0.16 * flick})`);
|
blit(ctx, softSprite('#FFFFFF'), p.x, p.y, R * 2.1, 0.5 * flick);
|
||||||
g.addColorStop(1, 'rgba(255,255,255,0)');
|
|
||||||
ctx.fillStyle = g;
|
|
||||||
ctx.beginPath(); ctx.arc(p.x, p.y, R * 2.1, 0, Math.PI * 2); ctx.fill();
|
|
||||||
|
|
||||||
// solid white core — unmistakable
|
// solid white core — unmistakable
|
||||||
ctx.fillStyle = `rgba(255,255,255,${0.92 * flick})`;
|
ctx.fillStyle = `rgba(255,255,255,${0.92 * flick})`;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { CFG } from './config.js';
|
import { CFG } from './config.js';
|
||||||
import { clamp, mixHex, rgba, lerp } from './util.js';
|
import { clamp, mixHex, rgba, lerp } from './util.js';
|
||||||
import { FX } from './fx.js';
|
import { FX } from './fx.js';
|
||||||
|
import { bodySprite, glowSprite, haloSprite, softSprite, blit } from './sprites.js';
|
||||||
|
|
||||||
// charge -> color, continuous over displayCharge (-1..1)
|
// charge -> color, continuous over displayCharge (-1..1)
|
||||||
function chargeColor(dc) {
|
function chargeColor(dc) {
|
||||||
@ -23,12 +24,7 @@ function drawBody(ctx, x, y, r, dc, spin, col, fill) {
|
|||||||
// eats the element colours and the radicals' white.
|
// eats the element colours and the radicals' white.
|
||||||
if (neg > 0.03) {
|
if (neg > 0.03) {
|
||||||
const pulse = 1 + Math.sin(spin * 3) * 0.07;
|
const pulse = 1 + Math.sin(spin * 3) * 0.07;
|
||||||
const R = r * (1 + 0.95 * neg) * pulse;
|
blit(ctx, haloSprite(CFG.COL_NEG), x, y, r * (1 + 0.95 * neg) * pulse, 0.30 * neg);
|
||||||
const g = ctx.createRadialGradient(x, y, r * 0.65, x, y, R);
|
|
||||||
g.addColorStop(0, rgba(CFG.COL_NEG, 0.30 * neg));
|
|
||||||
g.addColorStop(1, rgba(CFG.COL_NEG, 0));
|
|
||||||
ctx.fillStyle = g;
|
|
||||||
ctx.beginPath(); ctx.arc(x, y, R, 0, Math.PI * 2); ctx.fill();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// positive: spiky outline
|
// positive: spiky outline
|
||||||
@ -51,18 +47,18 @@ function drawBody(ctx, x, y, r, dc, spin, col, fill) {
|
|||||||
|
|
||||||
// core — ELEMENT FILL. Matte and never blooms, but SATURATED: these twelve
|
// core — ELEMENT FILL. Matte and never blooms, but SATURATED: these twelve
|
||||||
// hues are the game's identity and they have to survive on a dark ground.
|
// hues are the game's identity and they have to survive on a dark ground.
|
||||||
|
if (fill) {
|
||||||
|
// pre-baked: same radial shading, none of the per-frame allocation
|
||||||
|
ctx.drawImage(bodySprite(fill, r), x - r, y - r, r * 2, r * 2);
|
||||||
|
} else {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, r, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = rgba('#0b1118', 0.9);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
// CHARGE RIM
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, r, 0, Math.PI * 2);
|
ctx.arc(x, y, r, 0, Math.PI * 2);
|
||||||
if (fill) {
|
|
||||||
// radial: brighter at the top-left shoulder so it reads as a solid body
|
|
||||||
const g = ctx.createRadialGradient(x - r * 0.3, y - r * 0.35, r * 0.15, x, y, r);
|
|
||||||
g.addColorStop(0, mixHex(fill, '#FFFFFF', 0.28));
|
|
||||||
g.addColorStop(0.55, fill);
|
|
||||||
g.addColorStop(1, mixHex('#0B0716', fill, 0.55));
|
|
||||||
ctx.fillStyle = g;
|
|
||||||
} else ctx.fillStyle = rgba('#0b1118', 0.9);
|
|
||||||
ctx.fill();
|
|
||||||
// CHARGE RIM
|
|
||||||
ctx.strokeStyle = col;
|
ctx.strokeStyle = col;
|
||||||
ctx.lineWidth = neg > 0.3 || pos > 0.3 ? 2.2 : 1.4;
|
ctx.lineWidth = neg > 0.3 || pos > 0.3 ? 2.2 : 1.4;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
@ -81,12 +77,7 @@ function drawBody(ctx, x, y, r, dc, spin, col, fill) {
|
|||||||
if (chg > 0.05) {
|
if (chg > 0.05) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalCompositeOperation = 'lighter';
|
ctx.globalCompositeOperation = 'lighter';
|
||||||
const g = ctx.createRadialGradient(x, y, r * 0.72, x, y, r * 1.5);
|
blit(ctx, glowSprite(pos > neg ? CFG.COL_POS : CFG.COL_NEG), x, y, r * 1.5, 0.42 * chg);
|
||||||
g.addColorStop(0, 'rgba(0,0,0,0)');
|
|
||||||
g.addColorStop(0.5, rgba(pos > neg ? CFG.COL_POS : CFG.COL_NEG, 0.42 * chg));
|
|
||||||
g.addColorStop(1, 'rgba(0,0,0,0)');
|
|
||||||
ctx.fillStyle = g;
|
|
||||||
ctx.beginPath(); ctx.arc(x, y, r * 1.5, 0, Math.PI * 2); ctx.fill();
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,6 +109,25 @@ function drawField(ctx, world) {
|
|||||||
for (const p of world.particles) if (p.charge) src.push(p);
|
for (const p of world.particles) if (p.charge) src.push(p);
|
||||||
if (world.molly.charge) src.push(world.molly);
|
if (world.molly.charge) src.push(world.molly);
|
||||||
|
|
||||||
|
// FAST PATH: nothing charged means zero displacement everywhere, so the
|
||||||
|
// warped lattice is just... a lattice. Walking 627 nodes to emit ~1250
|
||||||
|
// antialiased polyline segments to draw the same thing 62 straight lines
|
||||||
|
// draw is pure waste, and it's the whole cost of a tutorial room where a
|
||||||
|
// single atom is on screen.
|
||||||
|
if (!src.length) {
|
||||||
|
ctx.strokeStyle = CFG.COL_GRID;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let x = B.x; x <= B.x + B.w; x += FIELD_STEP) {
|
||||||
|
ctx.moveTo(x, B.y); ctx.lineTo(x, B.y + B.h);
|
||||||
|
}
|
||||||
|
for (let y = B.y; y <= B.y + B.h; y += FIELD_STEP) {
|
||||||
|
ctx.moveTo(B.x, y); ctx.lineTo(B.x + B.w, y);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const R2 = FIELD_REACH * FIELD_REACH;
|
const R2 = FIELD_REACH * FIELD_REACH;
|
||||||
for (let j = 0; j <= rows; j++) {
|
for (let j = 0; j <= rows; j++) {
|
||||||
for (let i = 0; i <= cols; i++) {
|
for (let i = 0; i <= cols; i++) {
|
||||||
@ -181,6 +191,7 @@ function drawField(ctx, world) {
|
|||||||
// corona and core sit on a dark hole instead of on top of a coloured atom.
|
// corona and core sit on a dark hole instead of on top of a coloured atom.
|
||||||
const RAD_FILL = '#241F2E';
|
const RAD_FILL = '#241F2E';
|
||||||
|
|
||||||
|
|
||||||
function drawFlash(ctx, p) {
|
function drawFlash(ctx, p) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalCompositeOperation = 'lighter';
|
ctx.globalCompositeOperation = 'lighter';
|
||||||
@ -324,24 +335,11 @@ function drawMolly(ctx, world, t) {
|
|||||||
const col = m.charge === 0 ? CFG.COL_MOLLY : chargeColor(m.displayCharge);
|
const col = m.charge === 0 ? CFG.COL_MOLLY : chargeColor(m.displayCharge);
|
||||||
|
|
||||||
// the hole: drawn straight, NOT additive, so brightness around her can't eat it
|
// the hole: drawn straight, NOT additive, so brightness around her can't eat it
|
||||||
ctx.save();
|
blit(ctx, haloSprite('#06040E'), m.x, m.y, m.r + 30, 0.85);
|
||||||
const dg = ctx.createRadialGradient(m.x, m.y, m.r + 4, m.x, m.y, m.r + 30);
|
|
||||||
dg.addColorStop(0, 'rgba(6,4,14,0.85)');
|
|
||||||
dg.addColorStop(1, 'rgba(6,4,14,0)');
|
|
||||||
ctx.fillStyle = dg;
|
|
||||||
ctx.beginPath(); ctx.arc(m.x, m.y, m.r + 30, 0, Math.PI * 2); ctx.fill();
|
|
||||||
ctx.restore();
|
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalCompositeOperation = 'lighter';
|
ctx.globalCompositeOperation = 'lighter';
|
||||||
|
blit(ctx, softSprite(CFG.COL_MOLLY), m.x, m.y, m.r * 5.2, 0.34);
|
||||||
const R = m.r * 5.2;
|
|
||||||
const g = ctx.createRadialGradient(m.x, m.y, 1, m.x, m.y, R);
|
|
||||||
g.addColorStop(0, rgba(CFG.COL_MOLLY, 0.30));
|
|
||||||
g.addColorStop(0.35, rgba(CFG.COL_MOLLY, 0.10));
|
|
||||||
g.addColorStop(1, 'rgba(0,0,0,0)');
|
|
||||||
ctx.fillStyle = g;
|
|
||||||
ctx.beginPath(); ctx.arc(m.x, m.y, R, 0, Math.PI * 2); ctx.fill();
|
|
||||||
|
|
||||||
// two arcs sweeping one way, two the other — motion the eye catches in chaos
|
// two arcs sweeping one way, two the other — motion the eye catches in chaos
|
||||||
ctx.strokeStyle = rgba(CFG.COL_MOLLY, 0.55);
|
ctx.strokeStyle = rgba(CFG.COL_MOLLY, 0.55);
|
||||||
@ -370,11 +368,7 @@ function drawMolly(ctx, world, t) {
|
|||||||
if (alive) {
|
if (alive) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalCompositeOperation = 'lighter';
|
ctx.globalCompositeOperation = 'lighter';
|
||||||
const g2 = ctx.createRadialGradient(x, y, 0, x, y, 7);
|
blit(ctx, softSprite('#7FE3C4'), x, y, 7, 0.95);
|
||||||
g2.addColorStop(0, rgba('#7FE3C4', 0.95));
|
|
||||||
g2.addColorStop(1, 'rgba(0,0,0,0)');
|
|
||||||
ctx.fillStyle = g2;
|
|
||||||
ctx.beginPath(); ctx.arc(x, y, 7, 0, Math.PI * 2); ctx.fill();
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
|||||||
97
src/sprites.js
Normal file
97
src/sprites.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { mixHex, rgba } from './util.js';
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SPRITE CACHE.
|
||||||
|
//
|
||||||
|
// Every radial fill in this game used to be a createRadialGradient() call
|
||||||
|
// made fresh each frame, per body. At 116 atoms that is 150-250 gradient
|
||||||
|
// objects allocated AND rasterised every frame, which measured 12.9ms of
|
||||||
|
// pure raster on an M3 Ultra — a whole frame budget spent before anything
|
||||||
|
// else drew, and the reason a ONE-ATOM tutorial room ran at 30fps.
|
||||||
|
//
|
||||||
|
// The design doc called it from the start: pre-render the glows and blit
|
||||||
|
// them, never createRadialGradient per frame. There are only ~14 distinct
|
||||||
|
// fills (twelve elements + Molly + the radical hole) and two charge
|
||||||
|
// colours, so the whole game needs a handful of sprites baked once.
|
||||||
|
//
|
||||||
|
// Lives in its own module rather than render.js because radicals.js needs
|
||||||
|
// it too, and radicals.js is pulled into the headless test graph — the
|
||||||
|
// renderer must never follow it there. Nothing here touches the DOM until
|
||||||
|
// a sprite is actually requested, so importing this in Node is safe.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
const SPR = new Map();
|
||||||
|
const SS = 3; // supersample: stays crisp when the camera zooms
|
||||||
|
|
||||||
|
function sprite(key, size, paint) {
|
||||||
|
let c = SPR.get(key);
|
||||||
|
if (c) return c;
|
||||||
|
c = document.createElement('canvas');
|
||||||
|
c.width = c.height = size;
|
||||||
|
paint(c.getContext('2d'), size);
|
||||||
|
SPR.set(key, c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the element core, radially shaded — brighter at the top-left shoulder
|
||||||
|
export function bodySprite(fill, r) {
|
||||||
|
const size = Math.max(8, Math.ceil(r * 2 * SS));
|
||||||
|
return sprite(`b|${fill}|${r}`, size, (g, s) => {
|
||||||
|
const R = s / 2;
|
||||||
|
const grad = g.createRadialGradient(R - R * 0.3, R - R * 0.35, R * 0.15, R, R, R);
|
||||||
|
grad.addColorStop(0, mixHex(fill, '#FFFFFF', 0.28));
|
||||||
|
grad.addColorStop(0.55, fill);
|
||||||
|
grad.addColorStop(1, mixHex('#0B0716', fill, 0.55));
|
||||||
|
g.fillStyle = grad;
|
||||||
|
g.beginPath(); g.arc(R, R, R, 0, Math.PI * 2); g.fill();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// the charge rim-light: an annulus baked at full alpha, modulated with
|
||||||
|
// globalAlpha at blit time so one sprite covers every charge level
|
||||||
|
export function glowSprite(col) {
|
||||||
|
return sprite(`g|${col}`, 128, (g, s) => {
|
||||||
|
const R = s / 2;
|
||||||
|
const grad = g.createRadialGradient(R, R, 0, R, R, R);
|
||||||
|
grad.addColorStop(0, 'rgba(0,0,0,0)');
|
||||||
|
grad.addColorStop(0.48, 'rgba(0,0,0,0)');
|
||||||
|
grad.addColorStop(0.74, rgba(col, 1));
|
||||||
|
grad.addColorStop(1, 'rgba(0,0,0,0)');
|
||||||
|
g.fillStyle = grad;
|
||||||
|
g.beginPath(); g.arc(R, R, R, 0, Math.PI * 2); g.fill();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// the negative body's soft halo
|
||||||
|
export function haloSprite(col) {
|
||||||
|
return sprite(`h|${col}`, 128, (g, s) => {
|
||||||
|
const R = s / 2;
|
||||||
|
const grad = g.createRadialGradient(R, R, R * 0.4, R, R, R);
|
||||||
|
grad.addColorStop(0, rgba(col, 1));
|
||||||
|
grad.addColorStop(1, rgba(col, 0));
|
||||||
|
g.fillStyle = grad;
|
||||||
|
g.beginPath(); g.arc(R, R, R, 0, Math.PI * 2); g.fill();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// a plain soft disc — anything that just needs a falloff blob
|
||||||
|
export function softSprite(col) {
|
||||||
|
return sprite(`s|${col}`, 128, (g, s) => {
|
||||||
|
const R = s / 2;
|
||||||
|
const grad = g.createRadialGradient(R, R, 0, R, R, R);
|
||||||
|
grad.addColorStop(0, rgba(col, 1));
|
||||||
|
grad.addColorStop(0.45, rgba(col, 0.32));
|
||||||
|
grad.addColorStop(1, rgba(col, 0));
|
||||||
|
g.fillStyle = grad;
|
||||||
|
g.beginPath(); g.arc(R, R, R, 0, Math.PI * 2); g.fill();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// blit a cached sprite centred on (x,y) at the given radius
|
||||||
|
export function blit(ctx, spr, x, y, r, alpha = 1) {
|
||||||
|
if (alpha <= 0.003) return;
|
||||||
|
const a = ctx.globalAlpha;
|
||||||
|
ctx.globalAlpha = a * (alpha < 1 ? alpha : 1);
|
||||||
|
ctx.drawImage(spr, x - r, y - r, r * 2, r * 2);
|
||||||
|
ctx.globalAlpha = a;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user