- Dapatkan pautan
- X
- E-mel
- Apl Lain
Prt Scr di komputer |
Lukisan seni modern. Biar JS yang melukis di kanvas.
Demo: animasi dan corak di paparan mudah alih (henfon) mungkin tidak seperti di paparan web (komputer).
HTML view ▼
<canvas id="canvas"></canvas>
<style>
#canvas {
height: 100%;
width: 100%;
}
</style>
<script>
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth
canvas.height = window.innerHeight
const width = canvas.width;
const height = canvas.height;
canvas.style.background = "#fff";
canvas.style.filter = "blur(1px) ";
const types=[ // warna-warna
'red','green','blue', 'pink', 'cyan', 'magenta', 'yellow', 'black',
'white', 'orange', 'gray', 'brown', 'lime', 'purple', 'silver', 'navy', 'aqua',
'teal', 'olive', 'maroon', 'darkred', 'fuchsia', 'darkblue', 'darkgreen', 'darkcyan', 'darkmagenta', 'darkyellow', 'darkgray', 'darkkhaki', 'darkgoldenrod',
'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkturquoise', 'darkviolet',
'deeppink', 'deepskyblue', 'dimgray', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'gold', 'goldenrod', 'greenyellow',
'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral',
'lightcyan', 'lightgoldenrodyellow', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightsteelblue',
'lightyellow', 'limegreen', 'linen', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen',
'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'oldlace', 'olivedrab', 'orangered', 'orchid',
'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'plum', 'powderblue', 'rosybrown', 'royalblue', 'saddlebrown',
'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'skyblue', 'slateblue', 'slategray', 'snow', 'springgreen'
]
function random(min, max) { return Math.random() * (max - min) + min; }
class Particle {
constructor(x, y, vx, vy, ax, ay, r, type) {
this.x = x;
this.y = y;
this.r = r;
this.vx = vx;
this.vy = vy;
this.ax = ax;
this.ay = ay;
this.tx = 0;
this.ty = 0;
this.type = type;
this.age = 0;
this.birthdate = Date.now();
}
draw(particles) {
// jarak kurang dari 100px
let count = 0;
for (let i = 0; i < particles.length; i++) {
const dx = this.x - particles[i].x;
const dy = this.y - particles[i].y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 100) {
count++;
}
}
if(this.r * count /24 > 2) {
let sz = this.r * count/24;
sz = sz > 8 ? 8 : sz;
sz = sz < 4 ? 4 : sz;
ctx.beginPath();
ctx.arc(this.x, this.y, sz, 0, Math.PI * 2);
ctx.fillStyle = this.type;
ctx.fill();
}
}
}
class AttractionRepulsionRule {
constructor(strength, minDist, maxDist, forceVector) {
this.strength = strength;
this.minDist = minDist;
this.maxDist = maxDist;
this.forceVector = forceVector;
}
adjust(vecToAdd) {
this.forceVector.x += vecToAdd.x;
this.forceVector.y += vecToAdd.y;
}
apply(p1, p2, i, j) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < this.minDist || dist < p1.r + p2.r|| dist > this.maxDist) return;
const force = p1.r * p2.r * this.strength / (dist * dist);
const zx = (force * dx * this.forceVector.x) / dist;
const zy = (force * dy * this.forceVector.y) / dist;
p1.ax += zx;
p1.ay += zy;
const adjVec = { x: zx / dist * dist, y: zy / dist * dist };
if(adjVec.x&&adjVec.y&&Math.random()>0.999999999)this.adjust(adjVec);
return [p1]
}
}
class Ruleset {
rules;
constructor() {
this.rules = {};
}
addRule (type1, type2, rule) {
if (!this.rules[type1]) this.rules[type1] = {};
this.rules[type1][type2] = rule;
}
getRule(type1, type2) {
if (!this.rules[type1]) return null;
return this.rules[type1][type2];
}
apply (p1, p2, i, j) {
let rule = this.rules[p1.type][p2.type];
if(!rule) rule = this.rules[p2.type][p1.type];
if (rule) return rule.apply(p1, p2, i, j);
return [];
}
static createRuleset(types) {
const rules = new Ruleset();
types.forEach((type1) => {
types.forEach((type2) => {
const minDist = random(5, 100);
const maxDist = random(minDist + 1, 1500);
const fv = {
x: random(-1, 1),
y: random(-1, 1),
}
rules.addRule(type1, type2, new AttractionRepulsionRule(random(-1,1), minDist, maxDist, fv));
});
});
return rules;
}
}
class Iterator {
constructor(arr, ruleSet, update, draw) {
this.arr = arr;
this.ruleSet = ruleSet;
this.update = update;
this.draw = draw
this.arr.forEach((p1, i) => {
let collisions = [];
this.arr.forEach((p2, j) => {
this.ruleSet.apply(p1, p2, i, j);
if (this.update) this.update(this.arr[i], this.arr[j], i, j);
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < p1.r + p2.r && p1 !== p2) {
collisions.push([p1, p2]);
}
});
collisions.forEach((collision) => {
const p1 = collision[0];
const p2 = collision[1];
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const overlap = p1.r + p2.r - dist;
const overlapX = overlap * dx / dist;
const overlapY = overlap * dy / dist;
p1.x -= overlapX / 2;
p1.y -= overlapY / 2;
p2.x += overlapX / 2;
p2.y += overlapY / 2;
})
collisions = [];
// cantum bila bertemu
this.arr.forEach((p2, j) => {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < p1.r + p2.r * 2 && p1 !== p2) {
collisions.push([p1, p2]);
}
});
if (this.draw) this.draw(this.arr[i]);
});
}
}
function createParticles(num, types) {
const particles = [];
for (let i = 0; i < num; i++) {
particles.push(
new Particle(
random(0, canvas.width),
random(0, canvas.height),
0,
0,
0,
0,
random(10,10),
types[Math.floor(random(0, types.length))]
)
);
}
return particles;
}
const deadParticleTypes = {
}
const swatchSpacing = canvas.width / types.length;
const swatchRadius = 18
const swatchTextSize = 20;
function drawLegend() {
ctx.fillStyle = 'black';
ctx.textAlign = 'center';
const typesFirstHalf = types.slice(0, Math.floor(types.length / 2));
const typesSecondHalf = types.slice(Math.floor(types.length / 2));
typesFirstHalf.forEach((type, i) => {
ctx.beginPath();
ctx.arc(swatchSpacing * i*2 + swatchRadius, swatchRadius, swatchRadius, 0, Math.PI * 2);
ctx.fillStyle = type;
ctx.fill();
ctx.fillStyle = 'black';
ctx.fillText(deadParticleTypes[type] || '0', swatchSpacing * i*2 + swatchRadius, swatchRadius*1.5);
});
typesSecondHalf.forEach((type, i) => {
ctx.beginPath();
ctx.arc(swatchSpacing * i*2, canvas.height - swatchRadius, swatchRadius, 0, Math.PI * 2);
ctx.fillStyle = type;
ctx.fill();
ctx.fillStyle = 'black';
ctx.fillText(deadParticleTypes[type] || '0', swatchSpacing * i*2, canvas.height - swatchRadius*0.6);
});
}
function isAlive(p) {
const tx = p.tx;
const ty = p.ty;
const age = p.age;
// kurang 30 saat, particle terus kembang
if (!p.age || p.age < 3) return true;
let movement = Math.sqrt(tx * tx + ty * ty);
movement = movement || 1;
const deathLikelyhood = (1 / movement * movement * movement * movement);
return Math.random() > deathLikelyhood/10000000000000;
}
const ruleset = Ruleset.createRuleset(types);
let particles = createParticles(800, types);
const emitters = [];
function emitter(x, y, type) {
return {
x, y, type, emitted: 0,
}
}
// guna globalalpha padam particles
ctx.globalAlpha = 0.7;
// guna globalCompositeOperation utk gelapkan
ctx.globalCompositeOperation = "darker";
const loop = () => {
ctx.fillStyle = "rgba(0, 0, 0, 0.01)";
// kemas particles
particles.forEach((p) => {
p.np = 0;
p.age = Date.now() - p.birth;
});
new Iterator(particles, ruleset, (p1, p2, i, j) => {
const nx = p1.x + p1.vx;
const ny = p1.y + p1.vy;
const nd = Math.sqrt(nx * nx + ny * ny);
if(nd > p1.r + p1.r) {
p1.vx += p1.ax;
p1.vy += p1.ay;
p1.x += p1.vx;
p1.y += p1.vy;
p1.tx += Math.abs(p1.vx);
p1.ty += Math.abs(p1.vy);
p1.ax = 0;
p1.ay = 0;
}
// simpul
if (p1.x>= width) p1.x = p1.r
if (p1.x <= 0) p1.x = width - p1.r;
if (p1.y >= height) p1.y = p1.r;
if (p1.y <= 0) p1.y = height - p1.r;
// seret
p1.vx *= 0.994;
p1.vy *= 0.994;
}, (p) => {
// lukis particles
p.draw(particles);
p.age = Date.now() - p.birthdate;
});
let plen = particles.length;
plen -= particles.length;
const newParticles = [];
particles.forEach((p) => {
if (!isAlive(p)) {
deadParticleTypes[p.type] = deadParticleTypes[p.type] ? deadParticleTypes[p.type] + 1 : 1;
for(let z=0;z<2;z++)new Particle(
random(0, canvas.width),
random(0, canvas.height),
0,
0,
0,
0,
random(10,10),
types[Math.floor(random(0, types.length))]
)
} else {
newParticles.push(p);
}
})
particles = newParticles;
requestAnimationFrame(loop);
};
loop();
</script>
<style>
#canvas {
height: 100%;
width: 100%;
}
</style>
<script>
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth
canvas.height = window.innerHeight
const width = canvas.width;
const height = canvas.height;
canvas.style.background = "#fff";
canvas.style.filter = "blur(1px) ";
const types=[ // warna-warna
'red','green','blue', 'pink', 'cyan', 'magenta', 'yellow', 'black',
'white', 'orange', 'gray', 'brown', 'lime', 'purple', 'silver', 'navy', 'aqua',
'teal', 'olive', 'maroon', 'darkred', 'fuchsia', 'darkblue', 'darkgreen', 'darkcyan', 'darkmagenta', 'darkyellow', 'darkgray', 'darkkhaki', 'darkgoldenrod',
'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkturquoise', 'darkviolet',
'deeppink', 'deepskyblue', 'dimgray', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'gold', 'goldenrod', 'greenyellow',
'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral',
'lightcyan', 'lightgoldenrodyellow', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightsteelblue',
'lightyellow', 'limegreen', 'linen', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen',
'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'oldlace', 'olivedrab', 'orangered', 'orchid',
'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'plum', 'powderblue', 'rosybrown', 'royalblue', 'saddlebrown',
'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'skyblue', 'slateblue', 'slategray', 'snow', 'springgreen'
]
function random(min, max) { return Math.random() * (max - min) + min; }
class Particle {
constructor(x, y, vx, vy, ax, ay, r, type) {
this.x = x;
this.y = y;
this.r = r;
this.vx = vx;
this.vy = vy;
this.ax = ax;
this.ay = ay;
this.tx = 0;
this.ty = 0;
this.type = type;
this.age = 0;
this.birthdate = Date.now();
}
draw(particles) {
// jarak kurang dari 100px
let count = 0;
for (let i = 0; i < particles.length; i++) {
const dx = this.x - particles[i].x;
const dy = this.y - particles[i].y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 100) {
count++;
}
}
if(this.r * count /24 > 2) {
let sz = this.r * count/24;
sz = sz > 8 ? 8 : sz;
sz = sz < 4 ? 4 : sz;
ctx.beginPath();
ctx.arc(this.x, this.y, sz, 0, Math.PI * 2);
ctx.fillStyle = this.type;
ctx.fill();
}
}
}
class AttractionRepulsionRule {
constructor(strength, minDist, maxDist, forceVector) {
this.strength = strength;
this.minDist = minDist;
this.maxDist = maxDist;
this.forceVector = forceVector;
}
adjust(vecToAdd) {
this.forceVector.x += vecToAdd.x;
this.forceVector.y += vecToAdd.y;
}
apply(p1, p2, i, j) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < this.minDist || dist < p1.r + p2.r|| dist > this.maxDist) return;
const force = p1.r * p2.r * this.strength / (dist * dist);
const zx = (force * dx * this.forceVector.x) / dist;
const zy = (force * dy * this.forceVector.y) / dist;
p1.ax += zx;
p1.ay += zy;
const adjVec = { x: zx / dist * dist, y: zy / dist * dist };
if(adjVec.x&&adjVec.y&&Math.random()>0.999999999)this.adjust(adjVec);
return [p1]
}
}
class Ruleset {
rules;
constructor() {
this.rules = {};
}
addRule (type1, type2, rule) {
if (!this.rules[type1]) this.rules[type1] = {};
this.rules[type1][type2] = rule;
}
getRule(type1, type2) {
if (!this.rules[type1]) return null;
return this.rules[type1][type2];
}
apply (p1, p2, i, j) {
let rule = this.rules[p1.type][p2.type];
if(!rule) rule = this.rules[p2.type][p1.type];
if (rule) return rule.apply(p1, p2, i, j);
return [];
}
static createRuleset(types) {
const rules = new Ruleset();
types.forEach((type1) => {
types.forEach((type2) => {
const minDist = random(5, 100);
const maxDist = random(minDist + 1, 1500);
const fv = {
x: random(-1, 1),
y: random(-1, 1),
}
rules.addRule(type1, type2, new AttractionRepulsionRule(random(-1,1), minDist, maxDist, fv));
});
});
return rules;
}
}
class Iterator {
constructor(arr, ruleSet, update, draw) {
this.arr = arr;
this.ruleSet = ruleSet;
this.update = update;
this.draw = draw
this.arr.forEach((p1, i) => {
let collisions = [];
this.arr.forEach((p2, j) => {
this.ruleSet.apply(p1, p2, i, j);
if (this.update) this.update(this.arr[i], this.arr[j], i, j);
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < p1.r + p2.r && p1 !== p2) {
collisions.push([p1, p2]);
}
});
collisions.forEach((collision) => {
const p1 = collision[0];
const p2 = collision[1];
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const overlap = p1.r + p2.r - dist;
const overlapX = overlap * dx / dist;
const overlapY = overlap * dy / dist;
p1.x -= overlapX / 2;
p1.y -= overlapY / 2;
p2.x += overlapX / 2;
p2.y += overlapY / 2;
})
collisions = [];
// cantum bila bertemu
this.arr.forEach((p2, j) => {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < p1.r + p2.r * 2 && p1 !== p2) {
collisions.push([p1, p2]);
}
});
if (this.draw) this.draw(this.arr[i]);
});
}
}
function createParticles(num, types) {
const particles = [];
for (let i = 0; i < num; i++) {
particles.push(
new Particle(
random(0, canvas.width),
random(0, canvas.height),
0,
0,
0,
0,
random(10,10),
types[Math.floor(random(0, types.length))]
)
);
}
return particles;
}
const deadParticleTypes = {
}
const swatchSpacing = canvas.width / types.length;
const swatchRadius = 18
const swatchTextSize = 20;
function drawLegend() {
ctx.fillStyle = 'black';
ctx.textAlign = 'center';
const typesFirstHalf = types.slice(0, Math.floor(types.length / 2));
const typesSecondHalf = types.slice(Math.floor(types.length / 2));
typesFirstHalf.forEach((type, i) => {
ctx.beginPath();
ctx.arc(swatchSpacing * i*2 + swatchRadius, swatchRadius, swatchRadius, 0, Math.PI * 2);
ctx.fillStyle = type;
ctx.fill();
ctx.fillStyle = 'black';
ctx.fillText(deadParticleTypes[type] || '0', swatchSpacing * i*2 + swatchRadius, swatchRadius*1.5);
});
typesSecondHalf.forEach((type, i) => {
ctx.beginPath();
ctx.arc(swatchSpacing * i*2, canvas.height - swatchRadius, swatchRadius, 0, Math.PI * 2);
ctx.fillStyle = type;
ctx.fill();
ctx.fillStyle = 'black';
ctx.fillText(deadParticleTypes[type] || '0', swatchSpacing * i*2, canvas.height - swatchRadius*0.6);
});
}
function isAlive(p) {
const tx = p.tx;
const ty = p.ty;
const age = p.age;
// kurang 30 saat, particle terus kembang
if (!p.age || p.age < 3) return true;
let movement = Math.sqrt(tx * tx + ty * ty);
movement = movement || 1;
const deathLikelyhood = (1 / movement * movement * movement * movement);
return Math.random() > deathLikelyhood/10000000000000;
}
const ruleset = Ruleset.createRuleset(types);
let particles = createParticles(800, types);
const emitters = [];
function emitter(x, y, type) {
return {
x, y, type, emitted: 0,
}
}
// guna globalalpha padam particles
ctx.globalAlpha = 0.7;
// guna globalCompositeOperation utk gelapkan
ctx.globalCompositeOperation = "darker";
const loop = () => {
ctx.fillStyle = "rgba(0, 0, 0, 0.01)";
// kemas particles
particles.forEach((p) => {
p.np = 0;
p.age = Date.now() - p.birth;
});
new Iterator(particles, ruleset, (p1, p2, i, j) => {
const nx = p1.x + p1.vx;
const ny = p1.y + p1.vy;
const nd = Math.sqrt(nx * nx + ny * ny);
if(nd > p1.r + p1.r) {
p1.vx += p1.ax;
p1.vy += p1.ay;
p1.x += p1.vx;
p1.y += p1.vy;
p1.tx += Math.abs(p1.vx);
p1.ty += Math.abs(p1.vy);
p1.ax = 0;
p1.ay = 0;
}
// simpul
if (p1.x>= width) p1.x = p1.r
if (p1.x <= 0) p1.x = width - p1.r;
if (p1.y >= height) p1.y = p1.r;
if (p1.y <= 0) p1.y = height - p1.r;
// seret
p1.vx *= 0.994;
p1.vy *= 0.994;
}, (p) => {
// lukis particles
p.draw(particles);
p.age = Date.now() - p.birthdate;
});
let plen = particles.length;
plen -= particles.length;
const newParticles = [];
particles.forEach((p) => {
if (!isAlive(p)) {
deadParticleTypes[p.type] = deadParticleTypes[p.type] ? deadParticleTypes[p.type] + 1 : 1;
for(let z=0;z<2;z++)new Particle(
random(0, canvas.width),
random(0, canvas.height),
0,
0,
0,
0,
random(10,10),
types[Math.floor(random(0, types.length))]
)
} else {
newParticles.push(p);
}
})
particles = newParticles;
requestAnimationFrame(loop);
};
loop();
</script>
Demo conteng auto ini dipetik dari codepen/sschepis
Ulasan