Creative Visual Effects

Aurora Field interactive particle field

Complete an interactive particle field in an aurora meadow scene: real content, actionable controls, clear state feedback, and layouts that adapt across sizes.

Creative Visual EffectsInteractive Particle FieldHTMLCanvasJavaScript
With JavaScript enabled, you can edit, run, and save this example. The fixed source is as follows:
<!doctype html>
<html lang="en" dir="ltr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Aurora Field interactive particle field</title>
  <style>
    :root { --ink:#252525; --paper:#f7f2e8; --accent:#247ba0; --secondary:#f4b942; --line:color-mix(in srgb, var(--ink) 18%, transparent); --shadow:0 24px 60px color-mix(in srgb, var(--ink) 16%, transparent); color-scheme:light; }
    * { box-sizing:border-box; }
    html { min-width:320px; background:var(--paper); }
    body { min-height:100vh; margin:0; overflow-x:hidden; color:var(--ink); font-family:Avenir,"PingFang SC",sans-serif; background:var(--paper); }
    button,input,select,textarea { color:inherit; font:inherit; }
    button,[role="button"],input,select,textarea { accent-color:var(--accent); }
    button { cursor:pointer; }
    button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible { outline:3px solid var(--accent); outline-offset:3px; }
    [hidden] { display:none !important; }
    @media (max-width:700px) { .mast h1 { line-height:1.08 !important; letter-spacing:-.035em !important; overflow-wrap:anywhere; } }
    @media (prefers-reduced-motion:reduce) { *,*::before,*::after { scroll-behavior:auto !important; animation-duration:.01ms !important; animation-iteration-count:1 !important; transition-duration:.01ms !important; } }
    .page{width:min(1320px,calc(100% - 56px));margin:auto;padding:10vh 0}.mast{display:grid;grid-template-columns:minmax(0,1fr) 160px;gap:10vw;margin-block:6vh;align-items:start;margin-bottom:10vh}.mast h1{font-family:Avenir,"PingFang SC",sans-serif;font-weight:500;font-size:clamp(38px,8vw,88px);letter-spacing:-.055em;margin:0;line-height:.96}.eyebrow{display:block;margin-bottom:24px;font:600 10px/1 ui-monospace,monospace;letter-spacing:.22em}.intro{max-width:52ch;margin-top:28px;line-height:1.8}.scenario{font-size:13px;text-align:right}.demo{border-radius:1px;border-color:color-mix(in srgb,var(--ink) 35%,transparent);box-shadow:none;position:relative;min-height:420px;padding:clamp(28px,5vw,70px);background:transparent}@media(max-width:700px){.page{width:calc(100% - 24px);padding:36px 0}.mast{grid-template-columns:1fr;margin-bottom:30px}.scenario{text-align:left}.demo{padding:18px}}
    .panel{padding:22px;border:1px solid var(--line);background:color-mix(in srgb,#fff 58%,transparent)}.stack{display:grid;gap:15px}.row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}.action{min-height:44px;padding:10px 17px;border:1px solid var(--ink);border-radius:999px;background:transparent}.action.primary,.action.is-active{color:var(--paper);background:var(--accent)}.muted{opacity:.68}.status{min-height:24px;margin-top:15px;color:var(--accent);font-weight:800}.metric{font-size:clamp(30px,6vw,58px);font-weight:900}.meter{height:9px;overflow:hidden;border-radius:999px;background:color-mix(in srgb,var(--ink) 12%,transparent)}.meter>i{display:block;height:100%;background:var(--accent)}.chip{padding:6px 9px;border:1px solid var(--line);border-radius:999px;font-size:12px}
    .composition-deck{display:grid;grid-template-rows:auto 1fr;border:1px solid var(--line);border-radius:24px;overflow:hidden;background:color-mix(in srgb,var(--ink) 4%,var(--paper))}.deck-bar{display:flex;justify-content:space-between;gap:18px;align-items:center;padding:12px 18px;color:var(--paper);background:var(--ink);font:700 12px/1 ui-monospace,monospace}.deck-lights{display:flex;gap:7px}.deck-lights i{width:10px;height:10px;border-radius:50%;background:var(--accent)}.deck-lights i:nth-child(2){background:var(--secondary)}.deck-lights i:nth-child(3){background:color-mix(in srgb,var(--paper) 55%,transparent)}.composition-deck .demo{border:0;border-radius:0;box-shadow:none}
    .particle-field-lab{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(230px,.6fr);gap:16px}.particle-field-lab canvas{width:100%;height:460px;border:1px solid var(--line);border-radius:22px;background:#07131f;touch-action:none}.particle-controls{display:grid;align-content:center;gap:17px}.particle-controls label{display:grid;grid-template-columns:1fr auto;gap:8px;font-weight:800}.particle-controls input{grid-column:1/-1;width:100%}@media(max-width:760px){.particle-field-lab{grid-template-columns:1fr}.particle-field-lab canvas{height:330px}}.workflow-compare{display:grid;grid-template-columns:minmax(230px,.55fr) minmax(0,1.45fr);gap:18px}.compare-stage{display:grid;gap:12px;min-width:0}.compare-stage>header{display:flex;justify-content:space-between;gap:16px;align-items:end}.compare-stage h2{margin:.2em 0}.compare-core{--compare-split:50%;min-width:0;padding:14px;clip-path:inset(0 calc(100% - var(--compare-split)) 0 0);outline:1px dashed var(--accent);outline-offset:-8px;transition:clip-path .25s,filter .25s}.compare-core[data-compare-channel="B"]{filter:hue-rotate(32deg) saturate(1.15)}.compare-controls{display:grid;align-content:start;gap:16px;padding:20px;border:1px solid var(--line);border-radius:24px 4px 24px 4px;background:var(--paper)}.compare-controls fieldset,.compare-controls>label{display:grid;gap:8px}.compare-controls input[type="range"]{width:100%}#workflowCompareShelf{display:grid;gap:6px;padding:0;margin:0;list-style:none}#workflowCompareShelf li{display:grid;grid-template-columns:auto 1fr auto;gap:8px;padding:9px;border-top:1px solid var(--line);font-size:12px}@media(max-width:900px){.workflow-compare{grid-template-columns:1fr}.compare-stage{order:-1}.compare-core{clip-path:none;border-left:8px solid var(--accent)}}
  </style>
</head>
<body data-aesthetic="minimal" data-composition="control-deck">
  <main class="page">
    <header class="mast"><div><span class="eyebrow">minimal / Interactive Particle Field / INTERMEDIATE</span><h1>Aurora Field interactive particle field</h1><p class="intro">Complete an interactive particle field in an aurora meadow scene: real content, actionable controls, clear state feedback, and layouts that adapt across sizes.</p></div><span class="scenario">Aurora Field</span></header>
    <div class="composition-deck"><div class="deck-bar"><span>CONTROL DECK · 04</span><span>Interactive Particle Field</span><span class="deck-lights" aria-hidden="true"><i></i><i></i><i></i></span></div><section class="demo" aria-label="Aurora Field interactive particle field interaction area"><div class="workflow-compare"><form id="workflowCompareControls" class="compare-controls"><fieldset><legend>Reference Channel</legend><label><input type="radio" name="workflowChannel" value="A" checked>A · Green Light Curtain</label><label><input type="radio" name="workflowChannel" value="B">B · magenta light band</label></fieldset><label>Exposure Time Weight<input id="workflowCompareWeight" type="range" min="10" max="90" value="50"><output id="workflowCompareWeightValue">50 seconds</output></label><label><input id="workflowCompareFreeze" type="checkbox">Freeze current version</label><button id="workflowCompareCapture" class="action primary" type="button">Sample and save exposure</button><ol id="workflowCompareShelf" aria-label="Version Sampling Record"></ol></form><section class="compare-stage"><header><span class="eyebrow">Aurora Field COMPARE</span><h2>Aurora A/B Comparison</h2><output id="workflowCompareLive">Version A waiting for sampling</output></header><section id="workflowCore" class="workflow-core compare-core" data-compare-channel="A" style="--compare-split:50%"><!-- 11DZ_CORE_START:creative-effects-particle-field --><div class="particle-field-lab"><canvas id="particleCanvas" width="960" height="480" tabindex="0" aria-label="Aurora field interactive particle field; move the pointer or use the arrow keys to attract particles" aria-describedby="particleHelp">Your browser does not support Canvas.</canvas><aside class="particle-controls panel"><span class="eyebrow">PARTICLE FIELD</span><h2>Interactive Particle Field</h2><p id="particleHelp" class="muted">Move the pointer on the canvas, or focus the canvas and use the arrow keys to move the attractor point.</p><label>Particle Count <input id="particleAmount" type="range" min="24" max="110" value="64" data-core-bridge="creative-effects-particle-field"><output id="particleAmountValue">64</output></label><label>Attraction Intensity <input id="particlePull" type="range" min="0" max="100" value="45"><output id="particlePullValue">45%</output></label><div class="row"><button id="particleReset" class="action primary" type="button">Reset particles</button><button id="particlePause" class="action" type="button" aria-pressed="false">Pause</button></div></aside></div><!-- 11DZ_CORE_END --></section></section></div><p id="status" class="status" role="status" aria-live="polite"></p></section></div>
  </main>
  <script>
    'use strict';
    const statusOutput = document.querySelector('#status');
    const announce = message => { statusOutput.textContent = message; };
    /* 11DZ_CORE_SCRIPT_START:creative-effects-particle-field */
const particleCanvas=document.querySelector('#particleCanvas');const particleContext=particleCanvas.getContext('2d');const particleAmount=document.querySelector('#particleAmount');const particlePull=document.querySelector('#particlePull');const particleReduced=matchMedia('(prefers-reduced-motion: reduce)').matches;let particlePaused=particleReduced;let particleList=[];let particleTarget={x:0,y:0,active:false};let particleFrame=0;function sizeParticleCanvas(){const rect=particleCanvas.getBoundingClientRect();const ratio=Math.min(2,devicePixelRatio||1);particleCanvas.width=Math.max(320,Math.round(rect.width*ratio));particleCanvas.height=Math.max(260,Math.round(rect.height*ratio));particleContext.setTransform(ratio,0,0,ratio,0,0);if(!particleTarget.active){particleTarget.x=rect.width/2;particleTarget.y=rect.height/2;}}function resetParticles(){cancelAnimationFrame(particleFrame);const rect=particleCanvas.getBoundingClientRect();particleList=Array.from({length:Number(particleAmount.value)},(_,index)=>({x:(index*83%997)/997*rect.width,y:(index*47%509)/509*rect.height,vx:Math.sin(index*2.17)*.45,vy:Math.cos(index*1.73)*.45,r:1.5+index%4}));document.querySelector('#particleAmountValue').textContent=particleAmount.value;drawParticleField();announce('Particle field reset, total '+particleList.length+' particle');}function drawParticleField(){const rect=particleCanvas.getBoundingClientRect();particleContext.clearRect(0,0,rect.width,rect.height);particleContext.fillStyle='#07131f';particleContext.fillRect(0,0,rect.width,rect.height);const force=Number(particlePull.value)/17000;particleList.forEach((particle,index)=>{if(!particlePaused){if(particleTarget.active){const dx=particleTarget.x-particle.x;const dy=particleTarget.y-particle.y;const distance=Math.max(30,Math.hypot(dx,dy));particle.vx+=dx/distance*force*distance;particle.vy+=dy/distance*force*distance;}particle.vx*=.992;particle.vy*=.992;particle.x+=particle.vx;particle.y+=particle.vy;if(particle.x<0||particle.x>rect.width)particle.vx*=-1;if(particle.y<0||particle.y>rect.height)particle.vy*=-1;particle.x=Math.max(0,Math.min(rect.width,particle.x));particle.y=Math.max(0,Math.min(rect.height,particle.y));}particleContext.beginPath();particleContext.fillStyle=index%3?'#67e8f9':'#ff8a65';particleContext.arc(particle.x,particle.y,particle.r,0,Math.PI*2);particleContext.fill();particleList.slice(index+1,index+7).forEach(other=>{const distance=Math.hypot(other.x-particle.x,other.y-particle.y);if(distance<72){particleContext.beginPath();particleContext.strokeStyle='rgba(103,232,249,'+(1-distance/72)*.22+')';particleContext.moveTo(particle.x,particle.y);particleContext.lineTo(other.x,other.y);particleContext.stroke();}});});if(!particlePaused)particleFrame=requestAnimationFrame(drawParticleField);}function moveParticleTarget(clientX,clientY){const rect=particleCanvas.getBoundingClientRect();particleTarget={x:clientX-rect.left,y:clientY-rect.top,active:true};}particleCanvas.addEventListener('pointermove',event=>moveParticleTarget(event.clientX,event.clientY));particleCanvas.addEventListener('pointerleave',()=>{particleTarget.active=false;});particleCanvas.addEventListener('keydown',event=>{if(!['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(event.key))return;event.preventDefault();particleTarget.active=true;particleTarget.x+=event.key==='ArrowRight'?18:event.key==='ArrowLeft'?-18:0;particleTarget.y+=event.key==='ArrowDown'?18:event.key==='ArrowUp'?-18:0;particleTarget.x=Math.max(0,Math.min(particleCanvas.clientWidth,particleTarget.x));particleTarget.y=Math.max(0,Math.min(particleCanvas.clientHeight,particleTarget.y));drawParticleField();});particleAmount.addEventListener('input',resetParticles);particlePull.addEventListener('input',()=>{document.querySelector('#particlePullValue').textContent=particlePull.value+'%';announce('Particle attraction intensity '+particlePull.value+'%');});document.querySelector('#particleReset').addEventListener('click',resetParticles);document.querySelector('#particlePause').addEventListener('click',event=>{particlePaused=!particlePaused;event.currentTarget.textContent=particlePaused?'Continue':'Pause';event.currentTarget.setAttribute('aria-pressed',String(particlePaused));cancelAnimationFrame(particleFrame);drawParticleField();announce(particlePaused?'Particle field paused':'Particle field keeps moving');});addEventListener('resize',()=>{sizeParticleCanvas();resetParticles();},{passive:true});sizeParticleCanvas();resetParticles();
/* 11DZ_CORE_SCRIPT_END */
const workflowBridgeSpec={"name":"creative-effects-particle-field","selector":"[data-core-bridge=\"creative-effects-particle-field\"]","mode":"range","key":"ArrowRight"};const workflowBridgeCore=document.querySelector('#workflowCore');const workflowBridgeControl=workflowBridgeCore.querySelector(workflowBridgeSpec.selector);if(!workflowBridgeControl)throw new Error('Missing core bridge: '+workflowBridgeSpec.name);const workflowBridgeControlledId=workflowBridgeControl.getAttribute('aria-controls');const workflowBridgeControlled=workflowBridgeControlledId?document.getElementById(workflowBridgeControlledId):null;workflowBridgeCore.dataset.workflowBridge=workflowBridgeSpec.name;workflowBridgeCore.dataset.workflowVersion='0';function workflowBridgeSerialize(){const visualNode=workflowBridgeCore.firstElementChild||workflowBridgeCore;const visual=getComputedStyle(visualNode);const outputs=[...workflowBridgeCore.querySelectorAll('output,[role="status"],[aria-live],meter,progress')].map(node=>node.textContent.trim()+'|'+node.getAttribute('aria-valuenow')+'|'+node.getAttribute('value'));const canvas=[...workflowBridgeCore.querySelectorAll('canvas')].map(node=>{try{return node.toDataURL().slice(-160);}catch{return 'canvas-unavailable';}});return JSON.stringify({html:workflowBridgeCore.innerHTML,status:statusOutput.textContent,outputs,canvas,visual:[visual.display,visual.visibility,visual.opacity,visual.transform,visual.filter,visual.backgroundColor,visual.width,visual.height],scroll:workflowBridgeSpec.mode==='scroll'?workflowBridgeControl.scrollTop:null});}let workflowBridgeLastEffect=workflowBridgeSerialize();function workflowBridgeSummary(){const label=(workflowBridgeControl.getAttribute('aria-label')||workflowBridgeControl.textContent||workflowBridgeControl.value||workflowBridgeSpec.name).trim().replace(/\s+/g,' ').slice(0,72);return label+' · Core Version '+workflowBridgeCore.dataset.workflowVersion;}function workflowBridgeCommit(){const effect=workflowBridgeSerialize();if(effect===workflowBridgeLastEffect)return {changed:false,summary:workflowBridgeSummary()};workflowBridgeLastEffect=effect;workflowBridgeCore.dataset.workflowVersion=String(Number(workflowBridgeCore.dataset.workflowVersion||0)+1);return {changed:true,summary:workflowBridgeSummary()};}function workflowBridgeScheduleCommit(){queueMicrotask(workflowBridgeCommit);setTimeout(workflowBridgeCommit,0);}workflowBridgeControl.addEventListener("input",workflowBridgeScheduleCommit);workflowBridgeControl.addEventListener('change',workflowBridgeScheduleCommit);const workflowBridgeStateTargets=new Set(workflowBridgeCore.querySelectorAll('dialog'));if(workflowBridgeControlled&&workflowBridgeCore.contains(workflowBridgeControlled))workflowBridgeStateTargets.add(workflowBridgeControlled);workflowBridgeStateTargets.forEach(target=>['close','cancel','toggle'].forEach(eventName=>target.addEventListener(eventName,workflowBridgeScheduleCommit)));function workflowBridgeActivate(){workflowBridgeControl.focus();const step=Number(workflowBridgeControl.step)||1;const min=workflowBridgeControl.min===''?-Infinity:Number(workflowBridgeControl.min);const max=workflowBridgeControl.max===''?Infinity:Number(workflowBridgeControl.max);let next=Math.min(max,Math.max(min,(Number(workflowBridgeControl.value)||0)+step));if(next===Number(workflowBridgeControl.value))next=Math.max(min,Number(workflowBridgeControl.value)-step);workflowBridgeControl.value=String(next);workflowBridgeControl.dispatchEvent(new Event('input',{bubbles:true}));workflowBridgeControl.dispatchEvent(new Event('change',{bubbles:true}));const result=workflowBridgeCommit();setTimeout(workflowBridgeCommit,0);return result;}function workflowBridgeReset(){workflowBridgeCore.dataset.workflowVersion='0';workflowBridgeLastEffect=workflowBridgeSerialize();}
const workflowCompareCore=document.querySelector('#workflowCore');const workflowCompareWeight=document.querySelector('#workflowCompareWeight');const workflowCompareState={channel:'A',weight:50,frozen:false,snapshots:[]};function workflowCompareFingerprint(){const controls=[...workflowCompareCore.querySelectorAll('input,select,textarea')];const outputs=[...workflowCompareCore.querySelectorAll('output,[role="status"],strong')].map(node=>node.textContent.trim()).filter(Boolean);return {controlValues:controls.map(control=>control.type==='checkbox'||control.type==='radio'?control.checked:control.value),summary:outputs.slice(0,4).join(' / '),nodes:workflowCompareCore.querySelectorAll('*').length};}function workflowCompareDrive(){workflowCompareCore.dataset.compareChannel=workflowCompareState.channel;workflowCompareCore.style.setProperty('--compare-split',workflowCompareState.weight+'%');document.querySelector('#workflowCompareWeightValue').textContent=workflowCompareState.weight+' sec';document.querySelector('#workflowCompareLive').textContent=workflowCompareState.channel+' Version · '+workflowCompareState.weight+' sec'+(workflowCompareState.frozen?' · Frozen':'');announce('Aurora comparison in progress '+workflowCompareState.channel+' Version');}function workflowCompareCapture(source){const bridge=workflowBridgeActivate();const snapshot={...workflowCompareFingerprint(),channel:workflowCompareState.channel,weight:workflowCompareState.weight,source,bridge:bridge.summary};workflowCompareState.snapshots.push(snapshot);const item=document.createElement('li');item.innerHTML='<b>'+snapshot.channel+'</b><span>'+(snapshot.summary||snapshot.bridge)+'</span><small>'+snapshot.weight+' seconds · '+snapshot.bridge+'</small>';document.querySelector('#workflowCompareShelf').prepend(item);announce('sampled '+snapshot.channel+' Save exposure version · '+bridge.summary);}document.querySelectorAll('[name="workflowChannel"]').forEach(input=>input.addEventListener('change',()=>{workflowCompareState.channel=input.value;workflowCompareDrive();}));workflowCompareWeight.addEventListener('input',()=>{workflowCompareState.weight=Number(workflowCompareWeight.value);workflowCompareDrive();});document.querySelector('#workflowCompareFreeze').addEventListener('change',event=>{workflowCompareState.frozen=event.currentTarget.checked;workflowCompareDrive();});document.querySelector('#workflowCompareCapture').addEventListener('click',()=>workflowCompareCapture('Button Sampling'));workflowCompareCore.addEventListener('dblclick',()=>workflowCompareCapture('Main view double-click'));workflowCompareDrive();
workflowBridgeReset();
  </script>
</body>
</html>
Page feedbackFound an issue or have suggestions for improvement?
Feedback features need to be used in the online version

Please enable JavaScript in the online version before submitting; local features of the current tool or example are not affected.

Go to the online version for feedback