<canvas id="canvas" width="600" height="600">
</canvas>
<div class="text"><!--
--><div id="t"contenteditable>Hello 世界</div><!--
--><div id="t2" class="underline"></div>
</div>
xxxxxxxxxx
#canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
background: black;
}
.text {
position: fixed;
white-space: nowrap;
font-size: 10vw;
line-height: 1em;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: rgba(255, 255, 255, 0.8);
font-family: 'Comfortaa', 'Noto Sans TC', cursive, sans-serif;
text-shadow: rgba(255, 255, 255, 0.2) 0 0 0.1em
}
.text * {
white-space: nowrap;
}
.text .underline {
position: absolute;
bottom: calc(0 - calc(1vw + 30px));
border-top: rgba(255, 255, 255, 0.5) 1vw solid;
height: 30px;
left: -10px;
right: -10px;
}
xxxxxxxxxx
const dropPerPixel = 0.001
const speedRatio = 1
const tailLength = 150
const msaa = 1
const maxTickLength = 0.03
//----
const componentLists = new Map()
const entities = new Set()
const globals = {}
const nuzz = () => {}
const components = {
pos (e) {
e.x = 0
e.y = 0
},
trackCursor (e) {
e.trackCursor = true
},
collisionTarget (e) {
e.collisionTarget = true
e.collisionTargetCollided = false
e.collisionTargetHitOn = null
},
physic (e) {
e.weight = 0
e.vx = 0
e.vy = 0
e.ax = 0
e.ay = 0
},
collisionSource (e) {
e.collisionSource = true
e.bx1 = 0
e.bx2 = 0
e.by1 = 0
e.by2 = 0
},
event (e) {
e.cb = nuzz
},
draw (e) {
e.drawType = ''
},
resizeEvent (e) {
e.resizeCb = nuzz
},
followElement (e) {
e.el = null
},
generateSdf (e) {
e.sdfText = ''
e.sdfFont = ''
e.sdf = null
}
}
function addEntity () {
const e = {
destroyed: false
}
entities.add(e)
return e
}
function addComponent (e, name) {
components[name](e)
componentLists.set(name, componentLists.get(name) || new Set())
componentLists.get(name).add(e)
}
function destroy(e) {
e.destroyed = true
}
function gc () {
for (let e of entities) {
if (e.destroyed) {
entities.delete(e)
for (let list of componentLists.values()) {
list.delete(e)
}
}
}
}
function getByComponent (name) {
return componentLists.get(name) || new Set()
}
const systems = [
{
name: 'physic',
tick (s) {
for (let e of getByComponent ('physic')) {
e.vx += e.ax * s
e.vy += e.ay * s
e.x += e.vx * s
e.y += e.vy * s
}
}
},
{
name: 'collision',
tick (s) {
for (let e of getByComponent ('collisionTarget')) {
let collided = false
let collidedOn = null
for (let t of getByComponent ('collisionSource')) {
const collideOnThis = (
(t.bx1 - e.x) * (t.bx2 - e.x) <= 0 &&
(t.by1 - e.y) * (t.by2 - e.y) <= 0
)
collided = collided || collideOnThis
if (collideOnThis) {
collidedOn = t
}
}
e.collisionTargetCollided = collided
e.collisionTargetHitOn = collidedOn
}
}
},
{
name: 'event',
tick (s, g) {
for (let e of getByComponent ('event')) {
e.cb(e, g, s)
}
}
},
{
name: 'render',
init (g) {
// global resize callback
g.resizeCb = nuzz
g.dpi = (window.devicePixelRatio || 1) * msaa
g.canvas = document.getElementById('canvas')
g.ctx= g.canvas.getContext('2d')
g.bufferCanvas = document.createElement('canvas')
g.bufferCtx = g.bufferCanvas.getContext('2d')
g.windowWidth = Math.max(window.innerWidth, 1)
g.windowHeight = Math.max(window.innerHeight, 1)
g.canvas.width = g.windowWidth * g.dpi
g.canvas.height = g.windowHeight * g.dpi
g.bufferCanvas.width = g.windowWidth * g.dpi
g.bufferCanvas.height = g.windowHeight * g.dpi
g.ctx.scale(g.dpi, g.dpi);
g.ctx.save()
window.addEventListener('resize', () => {
g.dpi = (window.devicePixelRatio || 1) * msaa
g.bufferCtx.clearRect(0, 0, g.bufferCanvas.width, g.bufferCanvas.height)
g.windowWidth = Math.max(window.innerWidth, 1)
g.windowHeight = Math.max(window.innerHeight, 1)
g.canvas.width = g.windowWidth * g.dpi
g.canvas.height = g.windowHeight * g.dpi
g.bufferCanvas.width = g.windowWidth * g.dpi
g.bufferCanvas.height = g.windowHeight * g.dpi
g.ctx.clearRect(0, 0, g.canvas.width, g.canvas.height)
g.bufferCtx.clearRect(0, 0, g.bufferCanvas.width, g.bufferCanvas.height)
g.ctx.restore()
g.ctx.scale(g.dpi, g.dpi);
g.ctx.save()
// resize event
for (let e of getByComponent('resizeEvent')) {
e.resizeCb(e, g)
}
g.resizeCb(g)
})
},
tick (s, g) {
var grd = g.ctx.createLinearGradient(0, 0, 0, g.windowHeight);
grd.addColorStop(0, "#002");
grd.addColorStop(1, "#000");
g.ctx.fillStyle = grd
g.ctx.fillRect(0, 0, g.windowWidth, g.windowHeight);
g.ctx.globalAlpha = 0.8;
g.ctx.drawImage(
g.bufferCanvas,
0, 0,
g.windowWidth * g.dpi, g.windowHeight * g.dpi,
0, 0,
g.windowWidth, g.windowHeight
)
g.ctx.globalAlpha = 1;
g.ctx.save()
g.ctx.fillStyle = "rgba(255, 255, 255, 0.1)";
for (let e of getByComponent ('draw')) {
switch (e.drawType) {
case 'drop':
g.ctx.save()
g.ctx.translate(e.x, e.y)
g.ctx.rotate(-Math.atan(e.vx / e.vy))
g.ctx.fillRect(-0.5, -tailLength, 1, tailLength);
g.ctx.restore()
break
}
}
g.ctx.strokeStyle = "rgba(255, 255, 255, 0.15)";
for (let e of getByComponent ('draw')) {
switch (e.drawType) {
case 'ground':
g.ctx.beginPath();
g.ctx.arc(e.x, e.y, e.age * 200, 0, 2 * Math.PI);
g.ctx.stroke();
break
}
}
g.ctx.fillStyle = "rgba(255, 255, 255, 0.1)";
for (let e of getByComponent ('draw')) {
switch (e.drawType) {
case 'block':
g.ctx.fillStyle = "rgba(255, 255, 255, 0.1)";
g.ctx.fillRect(e.bx1, e.by1, e.bx2 - e.bx1, e.by2 - e.by1);
break
}
}
// g.ctx.font = "30px Arial";
// g.ctx.strokeText((s * 1000).toFixed(2) + "ms", 10, 50);
g.ctx.restore()
g.bufferCtx.clearRect(0, 0, g.bufferCanvas.width, g.bufferCanvas.height)
g.bufferCtx.drawImage(g.canvas, 0, 0)
}
},
{
name: 'elementTracking',
init (g) {
g.elementTrackingInterval = 20
g.elementTrackingTick = 0
},
tick (s, g) {
var canvasRect = g.canvas.getBoundingClientRect()
if (g.elementTrackingTick % g.elementTrackingInterval) {
for (let e of getByComponent ('followElement')) {
if (e.el) {
var rect = e.el.getBoundingClientRect()
e.bx1 = rect.left - canvasRect.left
e.bx2 = rect.right - canvasRect.left
e.by1 = rect.top - canvasRect.top
e.by2 = rect.bottom - canvasRect.top
}
}
}
g.elementTrackingTick++
}
},
{
name: 'text-sdf',
tick () {
function getSdf (text, font) {
text = text.trim()
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d');
ctx.fillStyle="#000"
ctx.font = font
const rect = ctx.measureText(text);
const width = Math.floor(rect.width)
const height = Math.min(200, width)
canvas.width = width
canvas.height = height
ctx.fillStyle="#000"
ctx.textBaseline = "top"
ctx.textAlign = "center"
ctx.font = font
ctx.fillText(text, width / 2, 0);
const imgd = ctx.getImageData(0, 0, canvas.width, canvas.height);
const sdf = []
for (let i = 0; i < width; i++) {
let hit = -1
for (let j = 0; j < height; j++) {
const opacity = imgd.data[(i + j * width) * 4 + 3]
if (opacity !== 0 && hit < 0) {
hit = j
}
}
sdf[i] = hit
}
return sdf
}
for (let e of getByComponent('generateSdf')) {
if (!e.sdf) {
e.sdf = getSdf (e.sdfText, e.sdfFont)
}
}
}
}
]
// ticker
let prev = null
function tick (ms) {
if (prev == null) {
prev = ms / 1000 - 0.016
}
const diff = Math.min(ms / 1000 - prev, maxTickLength) * speedRatio
prev = ms / 1000
for (let sys of systems) {
if (sys.tick) {
try {
sys.tick(diff, globals)
} catch (err) {}
}
}
gc()
requestAnimationFrame(tick)
}
// boot
function init () {
for (let sys of systems) {
if (sys.init) {
sys.init(globals)
}
}
}
// run
function start () {
requestAnimationFrame(tick)
}
// setup
function setup(g) {
function groundDropCb (e, g, dt) {
e.age += dt
if (e.age > e.limit) {
destroy(e)
}
}
function createGroundDrop (x = 0, y = 0) {
const e = addEntity()
addComponent(e, 'pos')
e.x = x
e.y = y
e.age = Math.random() * 0.01
e.limit = Math.random() * 0.1
addComponent(e, 'event')
e.cb = groundDropCb
addComponent(e, 'draw')
e.drawType = 'ground'
}
function createOrUpdate (x = 0, y = 0, vx = 0, old = null) {
const e = old || addEntity()
addComponent(e, 'pos')
addComponent(e, 'physic')
e.vy = 1000 * (Math.random() + 0.5)
e.x = x
e.y = y
e.vx = vx
function cb (e, g) {
if (e.y > g.windowHeight + 75) {
if (Math.random() < 0.5) {
createGroundDrop (e.x, g.windowHeight)
}
e.y -= g.windowHeight + 150
e.x = Math.random() * g.windowWidth
}
// if (e.y < 0) e.y += g.windowHeight
if (e.x > g.windowWidth) e.x -= g.windowWidth
if (e.x < 0) e.x += g.windowWidth
if (e.collisionTargetCollided) {
if (e.collisionTargetHitOn.sdf) {
const t = e.collisionTargetHitOn
const sdf = t.sdf
const depth = sdf[Math.floor(e.x) - Math.floor(t.bx1)]
if (depth != null && depth >= 0 && t.by1 + depth < e.y + 5) {
if (e.y - ( t.by1 + depth) < 20) {
createGroundDrop (e.x, t.by1 + depth)
}
e.y -= 300
e.x = Math.random() * g.windowWidth
}
} else {
const t = e.collisionTargetHitOn
createGroundDrop (e.x, t.by1)
e.y -= 300
e.x = Math.random() * g.windowWidth
}
}
}
addComponent(e, 'event')
e.cb = cb
addComponent(e, 'draw')
e.drawType = 'drop'
function handleResize(e, g) {
createOrUpdate(
Math.random() * g.windowWidth,
Math.random() * g.windowHeight,
200 * Math.random(),
e
)
}
addComponent(e, 'resizeEvent')
e.resizeCb = handleResize
addComponent(e, 'collisionTarget')
return e
}
let currentCount = Math.floor(g.windowWidth * g.windowHeight * dropPerPixel)
let list = []
for (let i = 0; i < currentCount; i++) {
list.push(
createOrUpdate(
Math.random() * g.windowWidth,
Math.random() * g.windowHeight,
200 * Math.random()
)
)
}
g.resizeCb = function (g) {
const newCount = Math.floor(g.windowWidth * g.windowHeight * dropPerPixel)
if (Math.abs(newCount - currentCount) > 10) {
if (newCount > currentCount) {
const toAdd = newCount - currentCount
for (let i = 0; i < toAdd; i++) {
list.push(
createOrUpdate(
Math.random() * g.windowWidth,
Math.random() * g.windowHeight,
200 * Math.random()
)
)
}
} else {
const toRemove = currentCount - newCount
for (let i = list.length - 1; i >= list.length - toRemove; i--) {
destroy(list[i])
}
list.length -= toRemove
}
currentCount = newCount
}
}
function createBoxForElement(el, withText = true) {
const e = addEntity()
addComponent(e, 'collisionSource')
e.bx1 = 0
e.bx2 = 0
e.by1 = 0
e.by2 = 0
addComponent(e, 'followElement')
e.el = el
addComponent(e, 'event')
e.oldbx2 = null
function getFont(el) {
const comp = getComputedStyle(el)
if (comp.font) return comp.font
return comp["font-size"] + ' ' + comp["font-family"]
}
if (withText) {
addComponent(e, 'generateSdf')
e.sdfText = el.textContent
e.sdfFont = getFont(e.el)
e.compositionProgressing = false
e.cb = (e) => {
if (!e.compositionProgressing) {
const ch = el.querySelectorAll('*')
if (ch.length) {
if (Array.from(ch).every(el => el.nodeName === 'BR')) {
Array.from(ch).forEach(el => el.remove())
} else {
el.textContent = el.textContent
}
}
if (el.textContent == '') {
el.textContent = 'Hello World'
return
}
}
e.oldbx2 = e.oldbx2 || e.bx2
if (e.sdf && e.sdf.length < (e.bx2 - e.bx1 - 3)) {
e.sdf = null
e.sdfText = el.textContent
e.sdfFont = getFont(e.el)
}
if (e.bx2 && e.oldbx2 !== e.bx2) {
e.sdf = null
e.sdfText = el.textContent
e.sdfFont = getFont(e.el)
}
e.oldbx2 = e.bx2
}
el.addEventListener('compositionstart', (ev) => {
e.compositionProgressing = true
})
el.addEventListener('compositionend', (ev) => {
e.compositionProgressing = false
})
}
}
createBoxForElement(document.getElementById('t'))
createBoxForElement(document.getElementById('t2'), false)
{
const e = addEntity()
}
}
init()
setup(globals)
start()
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.